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