1""" 2Connection module for Amazon CloudWatch 3 4.. versionadded:: 2014.7.0 5 6:configuration: This module accepts explicit credentials but can also utilize 7 IAM roles assigned to the instance through Instance Profiles. Dynamic 8 credentials are then automatically obtained from AWS API and no further 9 configuration is necessary. More Information available at: 10 11 .. code-block:: text 12 13 http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html 14 15 If IAM roles are not used you need to specify them either in a pillar or 16 in the minion's config file: 17 18 .. code-block:: yaml 19 20 cloudwatch.keyid: GKTADJGHEIQSXMKKRBJ08H 21 cloudwatch.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs 22 23 A region may also be specified in the configuration: 24 25 .. code-block:: yaml 26 27 cloudwatch.region: us-east-1 28 29 If a region is not specified, the default is us-east-1. 30 31 It's also possible to specify key, keyid and region via a profile, either 32 as a passed in dict, or as a string to pull from pillars or minion config: 33 34 .. code-block:: yaml 35 36 myprofile: 37 keyid: GKTADJGHEIQSXMKKRBJ08H 38 key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs 39 region: us-east-1 40 41:depends: boto 42""" 43# keep lint from choking on _get_conn and _cache_id 44# pylint: disable=E0602 45 46import logging 47 48import salt.utils.json 49import salt.utils.odict as odict 50import salt.utils.versions 51import yaml # pylint: disable=blacklisted-import 52 53try: 54 import boto 55 import boto.ec2.cloudwatch 56 import boto.ec2.cloudwatch.listelement 57 import boto.ec2.cloudwatch.dimension 58 59 logging.getLogger("boto").setLevel(logging.CRITICAL) 60 HAS_BOTO = True 61except ImportError: 62 HAS_BOTO = False 63 64log = logging.getLogger(__name__) 65 66 67def __virtual__(): 68 """ 69 Only load if boto libraries exist. 70 """ 71 has_boto_reqs = salt.utils.versions.check_boto_reqs(check_boto3=False) 72 if has_boto_reqs is True: 73 __utils__["boto.assign_funcs"]( 74 __name__, "cloudwatch", module="ec2.cloudwatch", pack=__salt__ 75 ) 76 return has_boto_reqs 77 78 79def get_alarm(name, region=None, key=None, keyid=None, profile=None): 80 """ 81 Get alarm details. Also can be used to check to see if an alarm exists. 82 83 CLI Example: 84 85 .. code-block:: bash 86 87 salt myminion boto_cloudwatch.get_alarm myalarm region=us-east-1 88 """ 89 conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) 90 91 alarms = conn.describe_alarms(alarm_names=[name]) 92 if not alarms: 93 return None 94 if len(alarms) > 1: 95 log.error("multiple alarms matched name '%s'", name) 96 return _metric_alarm_to_dict(alarms[0]) 97 98 99def _safe_dump(data): 100 """ 101 this presenter magic makes yaml.safe_dump 102 work with the objects returned from 103 boto.describe_alarms() 104 """ 105 custom_dumper = __utils__["yaml.get_dumper"]("SafeOrderedDumper") 106 107 def boto_listelement_presenter(dumper, data): 108 return dumper.represent_list(list(data)) 109 110 yaml.add_representer( 111 boto.ec2.cloudwatch.listelement.ListElement, 112 boto_listelement_presenter, 113 Dumper=custom_dumper, 114 ) 115 116 def dimension_presenter(dumper, data): 117 return dumper.represent_dict(dict(data)) 118 119 yaml.add_representer( 120 boto.ec2.cloudwatch.dimension.Dimension, 121 dimension_presenter, 122 Dumper=custom_dumper, 123 ) 124 125 return __utils__["yaml.dump"](data, Dumper=custom_dumper) 126 127 128def get_all_alarms(region=None, prefix=None, key=None, keyid=None, profile=None): 129 """ 130 Get all alarm details. Produces results that can be used to create an sls 131 file. 132 133 If prefix parameter is given, alarm names in the output will be prepended 134 with the prefix; alarms that have the prefix will be skipped. This can be 135 used to convert existing alarms to be managed by salt, as follows: 136 137 1. Make a "backup" of all existing alarms 138 $ salt-call boto_cloudwatch.get_all_alarms --out=txt | sed "s/local: //" > legacy_alarms.sls 139 140 2. Get all alarms with new prefixed names 141 $ salt-call boto_cloudwatch.get_all_alarms "prefix=**MANAGED BY SALT** " --out=txt | sed "s/local: //" > managed_alarms.sls 142 143 3. Insert the managed alarms into cloudwatch 144 $ salt-call state.template managed_alarms.sls 145 146 4. Manually verify that the new alarms look right 147 148 5. Delete the original alarms 149 $ sed s/present/absent/ legacy_alarms.sls > remove_legacy_alarms.sls 150 $ salt-call state.template remove_legacy_alarms.sls 151 152 6. Get all alarms again, verify no changes 153 $ salt-call boto_cloudwatch.get_all_alarms --out=txt | sed "s/local: //" > final_alarms.sls 154 $ diff final_alarms.sls managed_alarms.sls 155 156 CLI Example: 157 158 .. code-block:: bash 159 160 salt myminion boto_cloudwatch.get_all_alarms region=us-east-1 --out=txt 161 """ 162 conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) 163 164 alarms = conn.describe_alarms() 165 results = odict.OrderedDict() 166 for alarm in alarms: 167 alarm = _metric_alarm_to_dict(alarm) 168 name = alarm["name"] 169 if prefix: 170 if name.startswith(prefix): 171 continue 172 name = prefix + alarm["name"] 173 del alarm["name"] 174 alarm_sls = [{"name": name}, {"attributes": alarm}] 175 results["manage alarm " + name] = {"boto_cloudwatch_alarm.present": alarm_sls} 176 return _safe_dump(results) 177 178 179def create_or_update_alarm( 180 connection=None, 181 name=None, 182 metric=None, 183 namespace=None, 184 statistic=None, 185 comparison=None, 186 threshold=None, 187 period=None, 188 evaluation_periods=None, 189 unit=None, 190 description="", 191 dimensions=None, 192 alarm_actions=None, 193 insufficient_data_actions=None, 194 ok_actions=None, 195 region=None, 196 key=None, 197 keyid=None, 198 profile=None, 199): 200 """ 201 Create or update a cloudwatch alarm. 202 203 Params are the same as: 204 https://boto.readthedocs.io/en/latest/ref/cloudwatch.html#boto.ec2.cloudwatch.alarm.MetricAlarm. 205 206 Dimensions must be a dict. If the value of Dimensions is a string, it will 207 be json decoded to produce a dict. alarm_actions, insufficient_data_actions, 208 and ok_actions must be lists of string. If the passed-in value is a string, 209 it will be split on "," to produce a list. The strings themselves for 210 alarm_actions, insufficient_data_actions, and ok_actions must be Amazon 211 resource names (ARN's); however, this method also supports an arn lookup 212 notation, as follows: 213 214 arn:aws:.... ARN as per http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html 215 scaling_policy:<as_name>:<scaling_policy_name> The named autoscale group scaling policy, for the named group (e.g. scaling_policy:my-asg:ScaleDown) 216 217 This is convenient for setting up autoscaling as follows. First specify a 218 boto_asg.present state for an ASG with scaling_policies, and then set up 219 boto_cloudwatch_alarm.present states which have alarm_actions that 220 reference the scaling_policy. 221 222 CLI Example: 223 224 .. code-block:: bash 225 226 salt myminion boto_cloudwatch.create_alarm name=myalarm ... region=us-east-1 227 """ 228 # clean up argument types, so that CLI works 229 if threshold: 230 threshold = float(threshold) 231 if period: 232 period = int(period) 233 if evaluation_periods: 234 evaluation_periods = int(evaluation_periods) 235 if isinstance(dimensions, str): 236 dimensions = salt.utils.json.loads(dimensions) 237 if not isinstance(dimensions, dict): 238 log.error( 239 "could not parse dimensions argument: must be json encoding of a dict:" 240 " '%s'", 241 dimensions, 242 ) 243 return False 244 if isinstance(alarm_actions, str): 245 alarm_actions = alarm_actions.split(",") 246 if isinstance(insufficient_data_actions, str): 247 insufficient_data_actions = insufficient_data_actions.split(",") 248 if isinstance(ok_actions, str): 249 ok_actions = ok_actions.split(",") 250 251 # convert provided action names into ARN's 252 if alarm_actions: 253 alarm_actions = convert_to_arn( 254 alarm_actions, region=region, key=key, keyid=keyid, profile=profile 255 ) 256 if insufficient_data_actions: 257 insufficient_data_actions = convert_to_arn( 258 insufficient_data_actions, 259 region=region, 260 key=key, 261 keyid=keyid, 262 profile=profile, 263 ) 264 if ok_actions: 265 ok_actions = convert_to_arn( 266 ok_actions, region=region, key=key, keyid=keyid, profile=profile 267 ) 268 269 conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) 270 271 alarm = boto.ec2.cloudwatch.alarm.MetricAlarm( 272 connection=connection, 273 name=name, 274 metric=metric, 275 namespace=namespace, 276 statistic=statistic, 277 comparison=comparison, 278 threshold=threshold, 279 period=period, 280 evaluation_periods=evaluation_periods, 281 unit=unit, 282 description=description, 283 dimensions=dimensions, 284 alarm_actions=alarm_actions, 285 insufficient_data_actions=insufficient_data_actions, 286 ok_actions=ok_actions, 287 ) 288 conn.create_alarm(alarm) 289 log.info("Created/updated alarm %s", name) 290 return True 291 292 293def convert_to_arn(arns, region=None, key=None, keyid=None, profile=None): 294 """ 295 Convert a list of strings into actual arns. Converts convenience names such 296 as 'scaling_policy:...' 297 298 CLI Example: 299 300 .. code-block:: bash 301 302 salt '*' convert_to_arn 'scaling_policy:' 303 """ 304 results = [] 305 for arn in arns: 306 if arn.startswith("scaling_policy:"): 307 _, as_group, scaling_policy_name = arn.split(":") 308 policy_arn = __salt__["boto_asg.get_scaling_policy_arn"]( 309 as_group, scaling_policy_name, region, key, keyid, profile 310 ) 311 if policy_arn: 312 results.append(policy_arn) 313 else: 314 log.error("Could not convert: %s", arn) 315 else: 316 results.append(arn) 317 return results 318 319 320def delete_alarm(name, region=None, key=None, keyid=None, profile=None): 321 """ 322 Delete a cloudwatch alarm 323 324 CLI example to delete a queue:: 325 326 salt myminion boto_cloudwatch.delete_alarm myalarm region=us-east-1 327 """ 328 conn = _get_conn(region=region, key=key, keyid=keyid, profile=profile) 329 330 conn.delete_alarms([name]) 331 log.info("Deleted alarm %s", name) 332 return True 333 334 335def _metric_alarm_to_dict(alarm): 336 """ 337 Convert a boto.ec2.cloudwatch.alarm.MetricAlarm into a dict. Convenience 338 for pretty printing. 339 """ 340 d = odict.OrderedDict() 341 fields = [ 342 "name", 343 "metric", 344 "namespace", 345 "statistic", 346 "comparison", 347 "threshold", 348 "period", 349 "evaluation_periods", 350 "unit", 351 "description", 352 "dimensions", 353 "alarm_actions", 354 "insufficient_data_actions", 355 "ok_actions", 356 ] 357 for f in fields: 358 if hasattr(alarm, f): 359 d[f] = getattr(alarm, f) 360 return d 361