diff --git a/moto/cloudwatch/models.py b/moto/cloudwatch/models.py index ed0086d93..ac328def2 100644 --- a/moto/cloudwatch/models.py +++ b/moto/cloudwatch/models.py @@ -2,6 +2,11 @@ from moto.core import BaseBackend, BaseModel import boto.ec2.cloudwatch import datetime +from .utils import make_arn_for_dashboard + + +DEFAULT_ACCOUNT_ID = 123456789012 + class Dimension(object): @@ -44,10 +49,34 @@ class MetricDatum(BaseModel): 'value']) for dimension in dimensions] +class Dashboard(BaseModel): + def __init__(self, name, body): + # Guaranteed to be unique for now as the name is also the key of a dictionary where they are stored + self.arn = make_arn_for_dashboard(DEFAULT_ACCOUNT_ID, name) + self.name = name + self.body = body + self.last_modified = datetime.datetime.now() + + @property + def last_modified_iso(self): + return self.last_modified.isoformat() + + @property + def size(self): + return len(self) + + def __len__(self): + return len(self.body) + + def __repr__(self): + return ''.format(self.name) + + class CloudWatchBackend(BaseBackend): def __init__(self): self.alarms = {} + self.dashboards = {} self.metric_data = [] def put_metric_alarm(self, name, namespace, metric_name, comparison_operator, evaluation_periods, @@ -110,6 +139,31 @@ class CloudWatchBackend(BaseBackend): def get_all_metrics(self): return self.metric_data + def put_dashboard(self, name, body): + self.dashboards[name] = Dashboard(name, body) + + def list_dashboards(self, prefix=''): + for key, value in self.dashboards.items(): + if key.startswith(prefix): + yield value + + def delete_dashboards(self, dashboards): + to_delete = set(dashboards) + all_dashboards = set(self.dashboards.keys()) + + left_over = to_delete - all_dashboards + if len(left_over) > 0: + # Some dashboards are not found + return False, 'The specified dashboard does not exist. [{0}]'.format(', '.join(left_over)) + + for dashboard in to_delete: + del self.dashboards[dashboard] + + return True, None + + def get_dashboard(self, dashboard): + return self.dashboards.get(dashboard) + class LogGroup(BaseModel): diff --git a/moto/cloudwatch/responses.py b/moto/cloudwatch/responses.py index f114260dc..cd7ce123e 100644 --- a/moto/cloudwatch/responses.py +++ b/moto/cloudwatch/responses.py @@ -1,9 +1,18 @@ +import json from moto.core.responses import BaseResponse from .models import cloudwatch_backends class CloudWatchResponse(BaseResponse): + @property + def cloudwatch_backend(self): + return cloudwatch_backends[self.region] + + def _error(self, code, message, status=400): + template = self.response_template(ERROR_RESPONSE_TEMPLATE) + return template.render(code=code, message=message), dict(status=status) + def put_metric_alarm(self): name = self._get_param('AlarmName') namespace = self._get_param('Namespace') @@ -20,15 +29,14 @@ class CloudWatchResponse(BaseResponse): insufficient_data_actions = self._get_multi_param( "InsufficientDataActions.member") unit = self._get_param('Unit') - cloudwatch_backend = cloudwatch_backends[self.region] - alarm = cloudwatch_backend.put_metric_alarm(name, namespace, metric_name, - comparison_operator, - evaluation_periods, period, - threshold, statistic, - description, dimensions, - alarm_actions, ok_actions, - insufficient_data_actions, - unit) + alarm = self.cloudwatch_backend.put_metric_alarm(name, namespace, metric_name, + comparison_operator, + evaluation_periods, period, + threshold, statistic, + description, dimensions, + alarm_actions, ok_actions, + insufficient_data_actions, + unit) template = self.response_template(PUT_METRIC_ALARM_TEMPLATE) return template.render(alarm=alarm) @@ -37,28 +45,26 @@ class CloudWatchResponse(BaseResponse): alarm_name_prefix = self._get_param('AlarmNamePrefix') alarm_names = self._get_multi_param('AlarmNames.member') state_value = self._get_param('StateValue') - cloudwatch_backend = cloudwatch_backends[self.region] if action_prefix: - alarms = cloudwatch_backend.get_alarms_by_action_prefix( + alarms = self.cloudwatch_backend.get_alarms_by_action_prefix( action_prefix) elif alarm_name_prefix: - alarms = cloudwatch_backend.get_alarms_by_alarm_name_prefix( + alarms = self.cloudwatch_backend.get_alarms_by_alarm_name_prefix( alarm_name_prefix) elif alarm_names: - alarms = cloudwatch_backend.get_alarms_by_alarm_names(alarm_names) + alarms = self.cloudwatch_backend.get_alarms_by_alarm_names(alarm_names) elif state_value: - alarms = cloudwatch_backend.get_alarms_by_state_value(state_value) + alarms = self.cloudwatch_backend.get_alarms_by_state_value(state_value) else: - alarms = cloudwatch_backend.get_all_alarms() + alarms = self.cloudwatch_backend.get_all_alarms() template = self.response_template(DESCRIBE_ALARMS_TEMPLATE) return template.render(alarms=alarms) def delete_alarms(self): alarm_names = self._get_multi_param('AlarmNames.member') - cloudwatch_backend = cloudwatch_backends[self.region] - cloudwatch_backend.delete_alarms(alarm_names) + self.cloudwatch_backend.delete_alarms(alarm_names) template = self.response_template(DELETE_METRIC_ALARMS_TEMPLATE) return template.render() @@ -89,19 +95,26 @@ class CloudWatchResponse(BaseResponse): dimension_index += 1 metric_data.append([metric_name, value, dimensions]) metric_index += 1 - cloudwatch_backend = cloudwatch_backends[self.region] - cloudwatch_backend.put_metric_data(namespace, metric_data) + self.cloudwatch_backend.put_metric_data(namespace, metric_data) template = self.response_template(PUT_METRIC_DATA_TEMPLATE) return template.render() def list_metrics(self): - cloudwatch_backend = cloudwatch_backends[self.region] - metrics = cloudwatch_backend.get_all_metrics() + metrics = self.cloudwatch_backend.get_all_metrics() template = self.response_template(LIST_METRICS_TEMPLATE) return template.render(metrics=metrics) def delete_dashboards(self): - raise NotImplementedError() + dashboards = self._get_multi_param('DashboardNames.member') + if dashboards is None: + return self._error('InvalidParameterValue', 'Need at least 1 dashboard') + + status, error = self.cloudwatch_backend.delete_dashboards(dashboards) + if not status: + return self._error('ResourceNotFound', error) + + template = self.response_template(DELETE_DASHBOARD_TEMPLATE) + return template.render() def describe_alarm_history(self): raise NotImplementedError() @@ -116,16 +129,39 @@ class CloudWatchResponse(BaseResponse): raise NotImplementedError() def get_dashboard(self): - raise NotImplementedError() + dashboard_name = self._get_param('DashboardName') + + dashboard = self.cloudwatch_backend.get_dashboard(dashboard_name) + if dashboard is None: + return self._error('ResourceNotFound', 'Dashboard does not exist') + + template = self.response_template(GET_DASHBOARD_TEMPLATE) + return template.render(dashboard=dashboard) def get_metric_statistics(self): raise NotImplementedError() def list_dashboards(self): - raise NotImplementedError() + prefix = self._get_param('DashboardNamePrefix', '') + + dashboards = self.cloudwatch_backend.list_dashboards(prefix) + + template = self.response_template(LIST_DASHBOARD_RESPONSE) + return template.render(dashboards=dashboards) def put_dashboard(self): - raise NotImplementedError() + name = self._get_param('DashboardName') + body = self._get_param('DashboardBody') + + try: + json.loads(body) + except ValueError: + return self._error('InvalidParameterInput', 'Body is invalid JSON') + + self.cloudwatch_backend.put_dashboard(name, body) + + template = self.response_template(PUT_DASHBOARD_RESPONSE) + return template.render() def set_alarm_state(self): raise NotImplementedError() @@ -229,3 +265,58 @@ LIST_METRICS_TEMPLATE = """ + + + + + 44b1d4d8-9fa3-11e7-8ad3-41b86ac5e49e + +""" + +LIST_DASHBOARD_RESPONSE = """ + + + {% for dashboard in dashboards %} + + {{ dashboard.arn }} + {{ dashboard.last_modified_iso }} + {{ dashboard.size }} + {{ dashboard.name }} + + {% endfor %} + + + + c3773873-9fa5-11e7-b315-31fcc9275d62 + +""" + +DELETE_DASHBOARD_TEMPLATE = """ + + + 68d1dc8c-9faa-11e7-a694-df2715690df2 + +""" + +GET_DASHBOARD_TEMPLATE = """ + + {{ dashboard.arn }} + {{ dashboard.body }} + {{ dashboard.name }} + + + e3c16bb0-9faa-11e7-b315-31fcc9275d62 + + +""" + +ERROR_RESPONSE_TEMPLATE = """ + + Sender + {{ code }} + {{ message }} + + 5e45fd1e-9fa3-11e7-b720-89e8821d38c4 +""" diff --git a/moto/cloudwatch/utils.py b/moto/cloudwatch/utils.py new file mode 100644 index 000000000..ee33a4402 --- /dev/null +++ b/moto/cloudwatch/utils.py @@ -0,0 +1,5 @@ +from __future__ import unicode_literals + + +def make_arn_for_dashboard(account_id, name): + return "arn:aws:cloudwatch::{0}dashboard/{1}".format(account_id, name) diff --git a/tests/test_cloudwatch/test_cloudwatch_boto3.py b/tests/test_cloudwatch/test_cloudwatch_boto3.py new file mode 100644 index 000000000..923ba0b75 --- /dev/null +++ b/tests/test_cloudwatch/test_cloudwatch_boto3.py @@ -0,0 +1,94 @@ +from __future__ import unicode_literals + +import boto3 +from botocore.exceptions import ClientError +import sure # noqa + +from moto import mock_cloudwatch + + +@mock_cloudwatch +def test_put_list_dashboard(): + client = boto3.client('cloudwatch', region_name='eu-central-1') + widget = '{"widgets": [{"type": "text", "x": 0, "y": 7, "width": 3, "height": 3, "properties": {"markdown": "Hello world"}}]}' + + client.put_dashboard(DashboardName='test1', DashboardBody=widget) + resp = client.list_dashboards() + + len(resp['DashboardEntries']).should.equal(1) + + +@mock_cloudwatch +def test_put_list_prefix_nomatch_dashboard(): + client = boto3.client('cloudwatch', region_name='eu-central-1') + widget = '{"widgets": [{"type": "text", "x": 0, "y": 7, "width": 3, "height": 3, "properties": {"markdown": "Hello world"}}]}' + + client.put_dashboard(DashboardName='test1', DashboardBody=widget) + resp = client.list_dashboards(DashboardNamePrefix='nomatch') + + len(resp['DashboardEntries']).should.equal(0) + + +@mock_cloudwatch +def test_delete_dashboard(): + client = boto3.client('cloudwatch', region_name='eu-central-1') + widget = '{"widgets": [{"type": "text", "x": 0, "y": 7, "width": 3, "height": 3, "properties": {"markdown": "Hello world"}}]}' + + client.put_dashboard(DashboardName='test1', DashboardBody=widget) + client.put_dashboard(DashboardName='test2', DashboardBody=widget) + client.put_dashboard(DashboardName='test3', DashboardBody=widget) + client.delete_dashboards(DashboardNames=['test2', 'test1']) + + resp = client.list_dashboards(DashboardNamePrefix='test3') + len(resp['DashboardEntries']).should.equal(1) + + +@mock_cloudwatch +def test_delete_dashboard_fail(): + client = boto3.client('cloudwatch', region_name='eu-central-1') + widget = '{"widgets": [{"type": "text", "x": 0, "y": 7, "width": 3, "height": 3, "properties": {"markdown": "Hello world"}}]}' + + client.put_dashboard(DashboardName='test1', DashboardBody=widget) + client.put_dashboard(DashboardName='test2', DashboardBody=widget) + client.put_dashboard(DashboardName='test3', DashboardBody=widget) + # Doesnt delete anything if all dashboards to be deleted do not exist + try: + client.delete_dashboards(DashboardNames=['test2', 'test1', 'test_no_match']) + except ClientError as err: + err.response['Error']['Code'].should.equal('ResourceNotFound') + else: + raise RuntimeError('Should of raised error') + + resp = client.list_dashboards() + len(resp['DashboardEntries']).should.equal(3) + + +@mock_cloudwatch +def test_get_dashboard(): + client = boto3.client('cloudwatch', region_name='eu-central-1') + widget = '{"widgets": [{"type": "text", "x": 0, "y": 7, "width": 3, "height": 3, "properties": {"markdown": "Hello world"}}]}' + client.put_dashboard(DashboardName='test1', DashboardBody=widget) + + resp = client.get_dashboard(DashboardName='test1') + resp.should.contain('DashboardArn') + resp.should.contain('DashboardBody') + resp['DashboardName'].should.equal('test1') + + +@mock_cloudwatch +def test_get_dashboard_fail(): + client = boto3.client('cloudwatch', region_name='eu-central-1') + + try: + client.get_dashboard(DashboardName='test1') + except ClientError as err: + err.response['Error']['Code'].should.equal('ResourceNotFound') + else: + raise RuntimeError('Should of raised error') + + + + + + +