diff --git a/moto/cloudwatch/models.py b/moto/cloudwatch/models.py index ac328def2..f9d571a23 100644 --- a/moto/cloudwatch/models.py +++ b/moto/cloudwatch/models.py @@ -1,4 +1,7 @@ +import json + from moto.core import BaseBackend, BaseModel +from moto.core.exceptions import RESTError import boto.ec2.cloudwatch import datetime @@ -35,9 +38,26 @@ class FakeAlarm(BaseModel): self.ok_actions = ok_actions self.insufficient_data_actions = insufficient_data_actions self.unit = unit - self.state_updated_timestamp = datetime.datetime.utcnow() self.configuration_updated_timestamp = datetime.datetime.utcnow() + self.history = [] + + self.state_reason = '' + self.state_reason_data = '{}' + self.state = 'OK' + self.state_updated_timestamp = datetime.datetime.utcnow() + + def update_state(self, reason, reason_data, state_value): + # History type, that then decides what the rest of the items are, can be one of ConfigurationUpdate | StateUpdate | Action + self.history.append( + ('StateUpdate', self.state_reason, self.state_reason_data, self.state, self.state_updated_timestamp) + ) + + self.state_reason = reason + self.state_reason_data = reason_data + self.state = state_value + self.state_updated_timestamp = datetime.datetime.utcnow() + class MetricDatum(BaseModel): @@ -122,10 +142,8 @@ class CloudWatchBackend(BaseBackend): if alarm.name in alarm_names ] - def get_alarms_by_state_value(self, state): - raise NotImplementedError( - "DescribeAlarm by state is not implemented in moto." - ) + def get_alarms_by_state_value(self, target_state): + return filter(lambda alarm: alarm.state == target_state, self.alarms.values()) def delete_alarms(self, alarm_names): for alarm_name in alarm_names: @@ -164,6 +182,21 @@ class CloudWatchBackend(BaseBackend): def get_dashboard(self, dashboard): return self.dashboards.get(dashboard) + def set_alarm_state(self, alarm_name, reason, reason_data, state_value): + try: + if reason_data is not None: + json.loads(reason_data) + except ValueError: + raise RESTError('InvalidFormat', 'StateReasonData is invalid JSON') + + if alarm_name not in self.alarms: + raise RESTError('ResourceNotFound', 'Alarm {0} not found'.format(alarm_name), status=404) + + if state_value not in ('OK', 'ALARM', 'INSUFFICIENT_DATA'): + raise RESTError('InvalidParameterValue', 'StateValue is not one of OK | ALARM | INSUFFICIENT_DATA') + + self.alarms[alarm_name].update_state(reason, reason_data, state_value) + class LogGroup(BaseModel): diff --git a/moto/cloudwatch/responses.py b/moto/cloudwatch/responses.py index cd7ce123e..7a5fa5ebd 100644 --- a/moto/cloudwatch/responses.py +++ b/moto/cloudwatch/responses.py @@ -1,4 +1,5 @@ import json +from moto.core.utils import amzn_request_id from moto.core.responses import BaseResponse from .models import cloudwatch_backends @@ -13,6 +14,7 @@ class CloudWatchResponse(BaseResponse): template = self.response_template(ERROR_RESPONSE_TEMPLATE) return template.render(code=code, message=message), dict(status=status) + @amzn_request_id def put_metric_alarm(self): name = self._get_param('AlarmName') namespace = self._get_param('Namespace') @@ -40,6 +42,7 @@ class CloudWatchResponse(BaseResponse): template = self.response_template(PUT_METRIC_ALARM_TEMPLATE) return template.render(alarm=alarm) + @amzn_request_id def describe_alarms(self): action_prefix = self._get_param('ActionPrefix') alarm_name_prefix = self._get_param('AlarmNamePrefix') @@ -62,12 +65,14 @@ class CloudWatchResponse(BaseResponse): template = self.response_template(DESCRIBE_ALARMS_TEMPLATE) return template.render(alarms=alarms) + @amzn_request_id def delete_alarms(self): alarm_names = self._get_multi_param('AlarmNames.member') self.cloudwatch_backend.delete_alarms(alarm_names) template = self.response_template(DELETE_METRIC_ALARMS_TEMPLATE) return template.render() + @amzn_request_id def put_metric_data(self): namespace = self._get_param('Namespace') metric_data = [] @@ -99,11 +104,13 @@ class CloudWatchResponse(BaseResponse): template = self.response_template(PUT_METRIC_DATA_TEMPLATE) return template.render() + @amzn_request_id def list_metrics(self): metrics = self.cloudwatch_backend.get_all_metrics() template = self.response_template(LIST_METRICS_TEMPLATE) return template.render(metrics=metrics) + @amzn_request_id def delete_dashboards(self): dashboards = self._get_multi_param('DashboardNames.member') if dashboards is None: @@ -116,18 +123,23 @@ class CloudWatchResponse(BaseResponse): template = self.response_template(DELETE_DASHBOARD_TEMPLATE) return template.render() + @amzn_request_id def describe_alarm_history(self): raise NotImplementedError() + @amzn_request_id def describe_alarms_for_metric(self): raise NotImplementedError() + @amzn_request_id def disable_alarm_actions(self): raise NotImplementedError() + @amzn_request_id def enable_alarm_actions(self): raise NotImplementedError() + @amzn_request_id def get_dashboard(self): dashboard_name = self._get_param('DashboardName') @@ -138,9 +150,11 @@ class CloudWatchResponse(BaseResponse): template = self.response_template(GET_DASHBOARD_TEMPLATE) return template.render(dashboard=dashboard) + @amzn_request_id def get_metric_statistics(self): raise NotImplementedError() + @amzn_request_id def list_dashboards(self): prefix = self._get_param('DashboardNamePrefix', '') @@ -149,6 +163,7 @@ class CloudWatchResponse(BaseResponse): template = self.response_template(LIST_DASHBOARD_RESPONSE) return template.render(dashboards=dashboards) + @amzn_request_id def put_dashboard(self): name = self._get_param('DashboardName') body = self._get_param('DashboardBody') @@ -163,14 +178,23 @@ class CloudWatchResponse(BaseResponse): template = self.response_template(PUT_DASHBOARD_RESPONSE) return template.render() + @amzn_request_id def set_alarm_state(self): - raise NotImplementedError() + alarm_name = self._get_param('AlarmName') + reason = self._get_param('StateReason') + reason_data = self._get_param('StateReasonData') + state_value = self._get_param('StateValue') + + self.cloudwatch_backend.set_alarm_state(alarm_name, reason, reason_data, state_value) + + template = self.response_template(SET_ALARM_STATE_TEMPLATE) + return template.render() PUT_METRIC_ALARM_TEMPLATE = """ - 2690d7eb-ed86-11dd-9877-6fad448a8419 + {{ request_id }} """ @@ -229,7 +253,7 @@ DESCRIBE_ALARMS_TEMPLATE = """ - 2690d7eb-ed86-11dd-9877-6fad448a8419 + {{ request_id }} """ @@ -237,7 +261,7 @@ DELETE_METRIC_ALARMS_TEMPLATE = """ - 2690d7eb-ed86-11dd-9877-6fad448a8419 + {{ request_id }} """ @@ -271,7 +295,7 @@ PUT_DASHBOARD_RESPONSE = """ - 68d1dc8c-9faa-11e7-a694-df2715690df2 + {{ request_id }} """ @@ -307,16 +331,22 @@ GET_DASHBOARD_TEMPLATE = """ + + {{ request_id }} + +""" + ERROR_RESPONSE_TEMPLATE = """ Sender {{ code }} {{ message }} - 5e45fd1e-9fa3-11e7-b720-89e8821d38c4 + {{ request_id }} """ diff --git a/moto/core/utils.py b/moto/core/utils.py index 2ea4dc4a8..43f05672e 100644 --- a/moto/core/utils.py +++ b/moto/core/utils.py @@ -272,9 +272,6 @@ def amzn_request_id(f): else: status, new_headers, body = response headers.update(new_headers) - # Cast status to string - if "status" in headers: - headers['status'] = str(headers['status']) request_id = gen_amzn_requestid_long(headers) diff --git a/tests/test_cloudwatch/test_cloudwatch.py b/tests/test_cloudwatch/test_cloudwatch.py index 9b3f76c36..2f8528855 100644 --- a/tests/test_cloudwatch/test_cloudwatch.py +++ b/tests/test_cloudwatch/test_cloudwatch.py @@ -118,12 +118,3 @@ def test_describe_alarms(): alarms = conn.describe_alarms() alarms.should.have.length_of(0) - - -@mock_cloudwatch_deprecated -def test_describe_state_value_unimplemented(): - conn = boto.connect_cloudwatch() - - conn.describe_alarms() - conn.describe_alarms.when.called_with( - state_value="foo").should.throw(NotImplementedError) diff --git a/tests/test_cloudwatch/test_cloudwatch_boto3.py b/tests/test_cloudwatch/test_cloudwatch_boto3.py index 923ba0b75..e621a642a 100644 --- a/tests/test_cloudwatch/test_cloudwatch_boto3.py +++ b/tests/test_cloudwatch/test_cloudwatch_boto3.py @@ -87,6 +87,54 @@ def test_get_dashboard_fail(): raise RuntimeError('Should of raised error') +@mock_cloudwatch +def test_alarm_state(): + client = boto3.client('cloudwatch', region_name='eu-central-1') + + client.put_metric_alarm( + AlarmName='testalarm1', + MetricName='cpu', + Namespace='blah', + Period=10, + EvaluationPeriods=5, + Statistic='Average', + Threshold=2, + ComparisonOperator='GreaterThanThreshold', + ) + client.put_metric_alarm( + AlarmName='testalarm2', + MetricName='cpu', + Namespace='blah', + Period=10, + EvaluationPeriods=5, + Statistic='Average', + Threshold=2, + ComparisonOperator='GreaterThanThreshold', + ) + + # This is tested implicitly as if it doesnt work the rest will die + client.set_alarm_state( + AlarmName='testalarm1', + StateValue='ALARM', + StateReason='testreason', + StateReasonData='{"some": "json_data"}' + ) + + resp = client.describe_alarms( + StateValue='ALARM' + ) + len(resp['MetricAlarms']).should.equal(1) + resp['MetricAlarms'][0]['AlarmName'].should.equal('testalarm1') + + resp = client.describe_alarms( + StateValue='OK' + ) + len(resp['MetricAlarms']).should.equal(1) + resp['MetricAlarms'][0]['AlarmName'].should.equal('testalarm2') + + # Just for sanity + resp = client.describe_alarms() + len(resp['MetricAlarms']).should.equal(2)