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