Repository: getmoto/moto

Issue:
Cloudwatch.moto support for alarm states
Feature request regarding cloudwatch moto.
===========================================

Hello, I need following apis:
* `boto3.client('cloudwatch').describe_alarms(StateValue=)`
* `boto3.client('cloudwatch').set_alarm_state`

Currently those are not supported and raises NotImplementedError:
* https://github.com/spulec/moto/blob/eb220cc6fb120a978de93be3905c50799875ba8d/moto/cloudwatch/responses.py#L166
* https://github.com/spulec/moto/blob/eb220cc6fb120a978de93be3905c50799875ba8d/moto/cloudwatch/models.py#L125

Thanks!


Discussion:
API docs for [set_alarm_state](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_SetAlarmState.html), and [describe_alarms](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_DescribeAlarms.html)

Will have a look.


Resolving patch:
diff --git a/moto/cloudwatch/models.py b/moto/cloudwatch/models.py
--- 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 @@ def __init__(self, name, namespace, metric_name, comparison_operator, evaluation
         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 @@ def get_alarms_by_alarm_names(self, alarm_names):
             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 @@ def delete_dashboards(self, dashboards):
     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
--- 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 @@ def _error(self, code, message, status=400):
         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 @@ def put_metric_alarm(self):
         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 @@ def describe_alarms(self):
         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 @@ def put_metric_data(self):
         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 @@ def delete_dashboards(self):
         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 @@ def get_dashboard(self):
         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 @@ def list_dashboards(self):
         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 @@ def put_dashboard(self):
         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 = """<PutMetricAlarmResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
    <ResponseMetadata>
       <RequestId>
-         2690d7eb-ed86-11dd-9877-6fad448a8419
+         {{ request_id }}
       </RequestId>
    </ResponseMetadata>
 </PutMetricAlarmResponse>"""
@@ -229,7 +253,7 @@ def set_alarm_state(self):
 DELETE_METRIC_ALARMS_TEMPLATE = """<DeleteMetricAlarmResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
    <ResponseMetadata>
       <RequestId>
-         2690d7eb-ed86-11dd-9877-6fad448a8419
+         {{ request_id }}
       </RequestId>
    </ResponseMetadata>
 </DeleteMetricAlarmResponse>"""
@@ -237,7 +261,7 @@ def set_alarm_state(self):
 PUT_METRIC_DATA_TEMPLATE = """<PutMetricDataResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
    <ResponseMetadata>
       <RequestId>
-         2690d7eb-ed86-11dd-9877-6fad448a8419
+         {{ request_id }}
       </RequestId>
    </ResponseMetadata>
 </PutMetricDataResponse>"""
@@ -271,7 +295,7 @@ def set_alarm_state(self):
     <DashboardValidationMessages/>
   </PutDashboardResult>
   <ResponseMetadata>
-    <RequestId>44b1d4d8-9fa3-11e7-8ad3-41b86ac5e49e</RequestId>
+    <RequestId>{{ request_id }}</RequestId>
   </ResponseMetadata>
 </PutDashboardResponse>"""
 
@@ -289,14 +313,14 @@ def set_alarm_state(self):
     </DashboardEntries>
   </ListDashboardsResult>
   <ResponseMetadata>
-    <RequestId>c3773873-9fa5-11e7-b315-31fcc9275d62</RequestId>
+    <RequestId>{{ request_id }}</RequestId>
   </ResponseMetadata>
 </ListDashboardsResponse>"""
 
 DELETE_DASHBOARD_TEMPLATE = """<DeleteDashboardsResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
   <DeleteDashboardsResult/>
   <ResponseMetadata>
-    <RequestId>68d1dc8c-9faa-11e7-a694-df2715690df2</RequestId>
+    <RequestId>{{ request_id }}</RequestId>
   </ResponseMetadata>
 </DeleteDashboardsResponse>"""
 
@@ -307,16 +331,22 @@ def set_alarm_state(self):
     <DashboardName>{{ dashboard.name }}</DashboardName>
   </GetDashboardResult>
   <ResponseMetadata>
-    <RequestId>e3c16bb0-9faa-11e7-b315-31fcc9275d62</RequestId>
+    <RequestId>{{ request_id }}</RequestId>
   </ResponseMetadata>
 </GetDashboardResponse>
 """
 
+SET_ALARM_STATE_TEMPLATE = """<SetAlarmStateResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
+  <ResponseMetadata>
+    <RequestId>{{ request_id }}</RequestId>
+  </ResponseMetadata>
+</SetAlarmStateResponse>"""
+
 ERROR_RESPONSE_TEMPLATE = """<ErrorResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
   <Error>
     <Type>Sender</Type>
     <Code>{{ code }}</Code>
     <Message>{{ message }}</Message>
   </Error>
-  <RequestId>5e45fd1e-9fa3-11e7-b720-89e8821d38c4</RequestId>
+  <RequestId>{{ request_id }}</RequestId>
 </ErrorResponse>"""
diff --git a/moto/core/utils.py b/moto/core/utils.py
--- a/moto/core/utils.py
+++ b/moto/core/utils.py
@@ -272,9 +272,6 @@ def _wrapper(*args, **kwargs):
             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)
 


Test patch:
diff --git a/tests/test_cloudwatch/test_cloudwatch.py b/tests/test_cloudwatch/test_cloudwatch.py
--- 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
--- 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)