1# Copyright (c) 2006-2011 Mitch Garnaat http://garnaat.org/
2#
3# Permission is hereby granted, free of charge, to any person obtaining a
4# copy of this software and associated documentation files (the
5# "Software"), to deal in the Software without restriction, including
6# without limitation the rights to use, copy, modify, merge, publish, dis-
7# tribute, sublicense, and/or sell copies of the Software, and to permit
8# persons to whom the Software is furnished to do so, subject to the fol-
9# lowing conditions:
10#
11# The above copyright notice and this permission notice shall be included
12# in all copies or substantial portions of the Software.
13#
14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
16# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
17# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
18# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20# IN THE SOFTWARE.
21#
22"""
23This module provides an interface to the Elastic Compute Cloud (EC2)
24CloudWatch service from AWS.
25"""
26from boto.compat import json, map, six, zip
27from boto.connection import AWSQueryConnection
28from boto.ec2.cloudwatch.metric import Metric
29from boto.ec2.cloudwatch.alarm import MetricAlarm, MetricAlarms, AlarmHistoryItem
30from boto.ec2.cloudwatch.datapoint import Datapoint
31from boto.regioninfo import RegionInfo, get_regions, load_regions
32import boto
33
34RegionData = load_regions().get('cloudwatch', {})
35
36
37def regions():
38    """
39    Get all available regions for the CloudWatch service.
40
41    :rtype: list
42    :return: A list of :class:`boto.RegionInfo` instances
43    """
44    return get_regions('cloudwatch', connection_cls=CloudWatchConnection)
45
46
47def connect_to_region(region_name, **kw_params):
48    """
49    Given a valid region name, return a
50    :class:`boto.ec2.cloudwatch.CloudWatchConnection`.
51
52    :param str region_name: The name of the region to connect to.
53
54    :rtype: :class:`boto.ec2.CloudWatchConnection` or ``None``
55    :return: A connection to the given region, or None if an invalid region
56        name is given
57    """
58    for region in regions():
59        if region.name == region_name:
60            return region.connect(**kw_params)
61    return None
62
63
64class CloudWatchConnection(AWSQueryConnection):
65
66    APIVersion = boto.config.get('Boto', 'cloudwatch_version', '2010-08-01')
67    DefaultRegionName = boto.config.get('Boto', 'cloudwatch_region_name',
68                                        'us-east-1')
69    DefaultRegionEndpoint = boto.config.get('Boto',
70                                            'cloudwatch_region_endpoint',
71                                            'monitoring.us-east-1.amazonaws.com')
72
73    def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
74                 is_secure=True, port=None, proxy=None, proxy_port=None,
75                 proxy_user=None, proxy_pass=None, debug=0,
76                 https_connection_factory=None, region=None, path='/',
77                 security_token=None, validate_certs=True, profile_name=None):
78        """
79        Init method to create a new connection to EC2 Monitoring Service.
80
81        B{Note:} The host argument is overridden by the host specified in the
82        boto configuration file.
83        """
84        if not region:
85            region = RegionInfo(self, self.DefaultRegionName,
86                                self.DefaultRegionEndpoint)
87        self.region = region
88
89        # Ugly hack to get around both a bug in Python and a
90        # misconfigured SSL cert for the eu-west-1 endpoint
91        if self.region.name == 'eu-west-1':
92            validate_certs = False
93
94        super(CloudWatchConnection, self).__init__(aws_access_key_id,
95                                                   aws_secret_access_key,
96                                                   is_secure, port, proxy, proxy_port,
97                                                   proxy_user, proxy_pass,
98                                                   self.region.endpoint, debug,
99                                                   https_connection_factory, path,
100                                                   security_token,
101                                                   validate_certs=validate_certs,
102                                                   profile_name=profile_name)
103
104    def _required_auth_capability(self):
105        return ['hmac-v4']
106
107    def build_dimension_param(self, dimension, params):
108        prefix = 'Dimensions.member'
109        i = 0
110        for dim_name in dimension:
111            dim_value = dimension[dim_name]
112            if dim_value:
113                if isinstance(dim_value, six.string_types):
114                    dim_value = [dim_value]
115                for value in dim_value:
116                    params['%s.%d.Name' % (prefix, i + 1)] = dim_name
117                    params['%s.%d.Value' % (prefix, i + 1)] = value
118                    i += 1
119            else:
120                params['%s.%d.Name' % (prefix, i + 1)] = dim_name
121                i += 1
122
123    def build_list_params(self, params, items, label):
124        if isinstance(items, six.string_types):
125            items = [items]
126        for index, item in enumerate(items):
127            i = index + 1
128            if isinstance(item, dict):
129                for k, v in six.iteritems(item):
130                    params[label % (i, 'Name')] = k
131                    if v is not None:
132                        params[label % (i, 'Value')] = v
133            else:
134                params[label % i] = item
135
136    def build_put_params(self, params, name, value=None, timestamp=None,
137                         unit=None, dimensions=None, statistics=None):
138        args = (name, value, unit, dimensions, statistics, timestamp)
139        length = max(map(lambda a: len(a) if isinstance(a, list) else 1, args))
140
141        def aslist(a):
142            if isinstance(a, list):
143                if len(a) != length:
144                    raise Exception('Must specify equal number of elements; expected %d.' % length)
145                return a
146            return [a] * length
147
148        for index, (n, v, u, d, s, t) in enumerate(zip(*map(aslist, args))):
149            metric_data = {'MetricName': n}
150
151            if timestamp:
152                metric_data['Timestamp'] = t.isoformat()
153
154            if unit:
155                metric_data['Unit'] = u
156
157            if dimensions:
158                self.build_dimension_param(d, metric_data)
159
160            if statistics:
161                metric_data['StatisticValues.Maximum'] = s['maximum']
162                metric_data['StatisticValues.Minimum'] = s['minimum']
163                metric_data['StatisticValues.SampleCount'] = s['samplecount']
164                metric_data['StatisticValues.Sum'] = s['sum']
165                if value is not None:
166                    msg = 'You supplied a value and statistics for a ' + \
167                          'metric.Posting statistics and not value.'
168                    boto.log.warn(msg)
169            elif value is not None:
170                metric_data['Value'] = v
171            else:
172                raise Exception('Must specify a value or statistics to put.')
173
174            for key, val in six.iteritems(metric_data):
175                params['MetricData.member.%d.%s' % (index + 1, key)] = val
176
177    def get_metric_statistics(self, period, start_time, end_time, metric_name,
178                              namespace, statistics, dimensions=None,
179                              unit=None):
180        """
181        Get time-series data for one or more statistics of a given metric.
182
183        :type period: integer
184        :param period: The granularity, in seconds, of the returned datapoints.
185            Period must be at least 60 seconds and must be a multiple
186            of 60. The default value is 60.
187
188        :type start_time: datetime
189        :param start_time: The time stamp to use for determining the
190            first datapoint to return. The value specified is
191            inclusive; results include datapoints with the time stamp
192            specified.
193
194        :type end_time: datetime
195        :param end_time: The time stamp to use for determining the
196            last datapoint to return. The value specified is
197            exclusive; results will include datapoints up to the time
198            stamp specified.
199
200        :type metric_name: string
201        :param metric_name: The metric name.
202
203        :type namespace: string
204        :param namespace: The metric's namespace.
205
206        :type statistics: list
207        :param statistics: A list of statistics names Valid values:
208            Average | Sum | SampleCount | Maximum | Minimum
209
210        :type dimensions: dict
211        :param dimensions: A dictionary of dimension key/values where
212                           the key is the dimension name and the value
213                           is either a scalar value or an iterator
214                           of values to be associated with that
215                           dimension.
216
217        :type unit: string
218        :param unit: The unit for the metric.  Value values are:
219            Seconds | Microseconds | Milliseconds | Bytes | Kilobytes |
220            Megabytes | Gigabytes | Terabytes | Bits | Kilobits |
221            Megabits | Gigabits | Terabits | Percent | Count |
222            Bytes/Second | Kilobytes/Second | Megabytes/Second |
223            Gigabytes/Second | Terabytes/Second | Bits/Second |
224            Kilobits/Second | Megabits/Second | Gigabits/Second |
225            Terabits/Second | Count/Second | None
226
227        :rtype: list
228        """
229        params = {'Period': period,
230                  'MetricName': metric_name,
231                  'Namespace': namespace,
232                  'StartTime': start_time.isoformat(),
233                  'EndTime': end_time.isoformat()}
234        self.build_list_params(params, statistics, 'Statistics.member.%d')
235        if dimensions:
236            self.build_dimension_param(dimensions, params)
237        if unit:
238            params['Unit'] = unit
239        return self.get_list('GetMetricStatistics', params,
240                             [('member', Datapoint)])
241
242    def list_metrics(self, next_token=None, dimensions=None,
243                     metric_name=None, namespace=None):
244        """
245        Returns a list of the valid metrics for which there is recorded
246        data available.
247
248        :type next_token: str
249        :param next_token: A maximum of 500 metrics will be returned
250            at one time.  If more results are available, the ResultSet
251            returned will contain a non-Null next_token attribute.
252            Passing that token as a parameter to list_metrics will
253            retrieve the next page of metrics.
254
255        :type dimensions: dict
256        :param dimensions: A dictionary containing name/value
257            pairs that will be used to filter the results.  The key in
258            the dictionary is the name of a Dimension.  The value in
259            the dictionary is either a scalar value of that Dimension
260            name that you want to filter on or None if you want all
261            metrics with that Dimension name.  To be included in the
262            result a metric must contain all specified dimensions,
263            although the metric may contain additional dimensions beyond
264            the requested metrics.  The Dimension names, and values must
265            be strings between 1 and 250 characters long. A maximum of
266            10 dimensions are allowed.
267
268        :type metric_name: str
269        :param metric_name: The name of the Metric to filter against.  If None,
270            all Metric names will be returned.
271
272        :type namespace: str
273        :param namespace: A Metric namespace to filter against (e.g. AWS/EC2).
274            If None, Metrics from all namespaces will be returned.
275        """
276        params = {}
277        if next_token:
278            params['NextToken'] = next_token
279        if dimensions:
280            self.build_dimension_param(dimensions, params)
281        if metric_name:
282            params['MetricName'] = metric_name
283        if namespace:
284            params['Namespace'] = namespace
285
286        return self.get_list('ListMetrics', params, [('member', Metric)])
287
288    def put_metric_data(self, namespace, name, value=None, timestamp=None,
289                        unit=None, dimensions=None, statistics=None):
290        """
291        Publishes metric data points to Amazon CloudWatch. Amazon Cloudwatch
292        associates the data points with the specified metric. If the specified
293        metric does not exist, Amazon CloudWatch creates the metric. If a list
294        is specified for some, but not all, of the arguments, the remaining
295        arguments are repeated a corresponding number of times.
296
297        :type namespace: str
298        :param namespace: The namespace of the metric.
299
300        :type name: str or list
301        :param name: The name of the metric.
302
303        :type value: float or list
304        :param value: The value for the metric.
305
306        :type timestamp: datetime or list
307        :param timestamp: The time stamp used for the metric. If not specified,
308            the default value is set to the time the metric data was received.
309
310        :type unit: string or list
311        :param unit: The unit of the metric.  Valid Values: Seconds |
312            Microseconds | Milliseconds | Bytes | Kilobytes |
313            Megabytes | Gigabytes | Terabytes | Bits | Kilobits |
314            Megabits | Gigabits | Terabits | Percent | Count |
315            Bytes/Second | Kilobytes/Second | Megabytes/Second |
316            Gigabytes/Second | Terabytes/Second | Bits/Second |
317            Kilobits/Second | Megabits/Second | Gigabits/Second |
318            Terabits/Second | Count/Second | None
319
320        :type dimensions: dict
321        :param dimensions: Add extra name value pairs to associate
322            with the metric, i.e.:
323            {'name1': value1, 'name2': (value2, value3)}
324
325        :type statistics: dict or list
326        :param statistics: Use a statistic set instead of a value, for example::
327
328            {'maximum': 30, 'minimum': 1, 'samplecount': 100, 'sum': 10000}
329        """
330        params = {'Namespace': namespace}
331        self.build_put_params(params, name, value=value, timestamp=timestamp,
332                              unit=unit, dimensions=dimensions, statistics=statistics)
333
334        return self.get_status('PutMetricData', params, verb="POST")
335
336    def describe_alarms(self, action_prefix=None, alarm_name_prefix=None,
337                        alarm_names=None, max_records=None, state_value=None,
338                        next_token=None):
339        """
340        Retrieves alarms with the specified names. If no name is specified, all
341        alarms for the user are returned. Alarms can be retrieved by using only
342        a prefix for the alarm name, the alarm state, or a prefix for any
343        action.
344
345        :type action_prefix: string
346        :param action_name: The action name prefix.
347
348        :type alarm_name_prefix: string
349        :param alarm_name_prefix: The alarm name prefix. AlarmNames cannot
350            be specified if this parameter is specified.
351
352        :type alarm_names: list
353        :param alarm_names: A list of alarm names to retrieve information for.
354
355        :type max_records: int
356        :param max_records: The maximum number of alarm descriptions
357            to retrieve.
358
359        :type state_value: string
360        :param state_value: The state value to be used in matching alarms.
361
362        :type next_token: string
363        :param next_token: The token returned by a previous call to
364            indicate that there is more data.
365
366        :rtype list
367        """
368        params = {}
369        if action_prefix:
370            params['ActionPrefix'] = action_prefix
371        if alarm_name_prefix:
372            params['AlarmNamePrefix'] = alarm_name_prefix
373        elif alarm_names:
374            self.build_list_params(params, alarm_names, 'AlarmNames.member.%s')
375        if max_records:
376            params['MaxRecords'] = max_records
377        if next_token:
378            params['NextToken'] = next_token
379        if state_value:
380            params['StateValue'] = state_value
381
382        result = self.get_list('DescribeAlarms', params,
383                               [('MetricAlarms', MetricAlarms)])
384        ret = result[0]
385        ret.next_token = result.next_token
386        return ret
387
388    def describe_alarm_history(self, alarm_name=None,
389                               start_date=None, end_date=None,
390                               max_records=None, history_item_type=None,
391                               next_token=None):
392        """
393        Retrieves history for the specified alarm. Filter alarms by date range
394        or item type. If an alarm name is not specified, Amazon CloudWatch
395        returns histories for all of the owner's alarms.
396
397        Amazon CloudWatch retains the history of deleted alarms for a period of
398        six weeks. If an alarm has been deleted, its history can still be
399        queried.
400
401        :type alarm_name: string
402        :param alarm_name: The name of the alarm.
403
404        :type start_date: datetime
405        :param start_date: The starting date to retrieve alarm history.
406
407        :type end_date: datetime
408        :param end_date: The starting date to retrieve alarm history.
409
410        :type history_item_type: string
411        :param history_item_type: The type of alarm histories to retreive
412            (ConfigurationUpdate | StateUpdate | Action)
413
414        :type max_records: int
415        :param max_records: The maximum number of alarm descriptions
416            to retrieve.
417
418        :type next_token: string
419        :param next_token: The token returned by a previous call to indicate
420            that there is more data.
421
422        :rtype list
423        """
424        params = {}
425        if alarm_name:
426            params['AlarmName'] = alarm_name
427        if start_date:
428            params['StartDate'] = start_date.isoformat()
429        if end_date:
430            params['EndDate'] = end_date.isoformat()
431        if history_item_type:
432            params['HistoryItemType'] = history_item_type
433        if max_records:
434            params['MaxRecords'] = max_records
435        if next_token:
436            params['NextToken'] = next_token
437        return self.get_list('DescribeAlarmHistory', params,
438                             [('member', AlarmHistoryItem)])
439
440    def describe_alarms_for_metric(self, metric_name, namespace, period=None,
441                                   statistic=None, dimensions=None, unit=None):
442        """
443        Retrieves all alarms for a single metric. Specify a statistic, period,
444        or unit to filter the set of alarms further.
445
446        :type metric_name: string
447        :param metric_name: The name of the metric
448
449        :type namespace: string
450        :param namespace: The namespace of the metric.
451
452        :type period: int
453        :param period: The period in seconds over which the statistic
454            is applied.
455
456        :type statistic: string
457        :param statistic: The statistic for the metric.
458
459        :param dimension_filters: A dictionary containing name/value
460            pairs that will be used to filter the results.  The key in
461            the dictionary is the name of a Dimension.  The value in
462            the dictionary is either a scalar value of that Dimension
463            name that you want to filter on, a list of values to
464            filter on or None if you want all metrics with that
465            Dimension name.
466
467        :type unit: string
468
469        :rtype list
470        """
471        params = {'MetricName': metric_name,
472                  'Namespace': namespace}
473        if period:
474            params['Period'] = period
475        if statistic:
476            params['Statistic'] = statistic
477        if dimensions:
478            self.build_dimension_param(dimensions, params)
479        if unit:
480            params['Unit'] = unit
481        return self.get_list('DescribeAlarmsForMetric', params,
482                             [('member', MetricAlarm)])
483
484    def put_metric_alarm(self, alarm):
485        """
486        Creates or updates an alarm and associates it with the specified Amazon
487        CloudWatch metric. Optionally, this operation can associate one or more
488        Amazon Simple Notification Service resources with the alarm.
489
490        When this operation creates an alarm, the alarm state is immediately
491        set to INSUFFICIENT_DATA. The alarm is evaluated and its StateValue is
492        set appropriately. Any actions associated with the StateValue is then
493        executed.
494
495        When updating an existing alarm, its StateValue is left unchanged.
496
497        :type alarm: boto.ec2.cloudwatch.alarm.MetricAlarm
498        :param alarm: MetricAlarm object.
499        """
500        params = {
501            'AlarmName': alarm.name,
502            'MetricName': alarm.metric,
503            'Namespace': alarm.namespace,
504            'Statistic': alarm.statistic,
505            'ComparisonOperator': alarm.comparison,
506            'Threshold': alarm.threshold,
507            'EvaluationPeriods': alarm.evaluation_periods,
508            'Period': alarm.period,
509        }
510        if alarm.actions_enabled is not None:
511            params['ActionsEnabled'] = alarm.actions_enabled
512        if alarm.alarm_actions:
513            self.build_list_params(params, alarm.alarm_actions,
514                                   'AlarmActions.member.%s')
515        if alarm.description:
516            params['AlarmDescription'] = alarm.description
517        if alarm.dimensions:
518            self.build_dimension_param(alarm.dimensions, params)
519        if alarm.insufficient_data_actions:
520            self.build_list_params(params, alarm.insufficient_data_actions,
521                                   'InsufficientDataActions.member.%s')
522        if alarm.ok_actions:
523            self.build_list_params(params, alarm.ok_actions,
524                                   'OKActions.member.%s')
525        if alarm.unit:
526            params['Unit'] = alarm.unit
527        alarm.connection = self
528        return self.get_status('PutMetricAlarm', params)
529    create_alarm = put_metric_alarm
530    update_alarm = put_metric_alarm
531
532    def delete_alarms(self, alarms):
533        """
534        Deletes all specified alarms. In the event of an error, no
535        alarms are deleted.
536
537        :type alarms: list
538        :param alarms: List of alarm names.
539        """
540        params = {}
541        self.build_list_params(params, alarms, 'AlarmNames.member.%s')
542        return self.get_status('DeleteAlarms', params)
543
544    def set_alarm_state(self, alarm_name, state_reason, state_value,
545                        state_reason_data=None):
546        """
547        Temporarily sets the state of an alarm. When the updated StateValue
548        differs from the previous value, the action configured for the
549        appropriate state is invoked. This is not a permanent change. The next
550        periodic alarm check (in about a minute) will set the alarm to its
551        actual state.
552
553        :type alarm_name: string
554        :param alarm_name: Descriptive name for alarm.
555
556        :type state_reason: string
557        :param state_reason: Human readable reason.
558
559        :type state_value: string
560        :param state_value: OK | ALARM | INSUFFICIENT_DATA
561
562        :type state_reason_data: string
563        :param state_reason_data: Reason string (will be jsonified).
564        """
565        params = {'AlarmName': alarm_name,
566                  'StateReason': state_reason,
567                  'StateValue': state_value}
568        if state_reason_data:
569            params['StateReasonData'] = json.dumps(state_reason_data)
570
571        return self.get_status('SetAlarmState', params)
572
573    def enable_alarm_actions(self, alarm_names):
574        """
575        Enables actions for the specified alarms.
576
577        :type alarms: list
578        :param alarms: List of alarm names.
579        """
580        params = {}
581        self.build_list_params(params, alarm_names, 'AlarmNames.member.%s')
582        return self.get_status('EnableAlarmActions', params)
583
584    def disable_alarm_actions(self, alarm_names):
585        """
586        Disables actions for the specified alarms.
587
588        :type alarms: list
589        :param alarms: List of alarm names.
590        """
591        params = {}
592        self.build_list_params(params, alarm_names, 'AlarmNames.member.%s')
593        return self.get_status('DisableAlarmActions', params)
594