--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+endpoint for heat AWS-compatible CloudWatch API
+"""
+import os
+import sys
+import re
+import webob
+from heat.api.aws import exception
+from heat.api.aws import utils as api_utils
+from heat.common import wsgi
+from heat.common import config
+from heat.common import context
+from heat import utils
+from heat.engine import rpcapi as engine_rpcapi
+import heat.engine.api as engine_api
+
+from heat.openstack.common import rpc
+import heat.openstack.common.rpc.common as rpc_common
+from heat.openstack.common import log as logging
+
+logger = logging.getLogger('heat.api.cloudwatch.controller')
+
+
+class WatchController(object):
+
+ """
+ WSGI controller for CloudWatch resource in heat API
+ Implements the API actions
+ """
+
+ def __init__(self, options):
+ self.options = options
+ self.engine_rpcapi = engine_rpcapi.EngineAPI()
+
+ @staticmethod
+ def _reformat_dimensions(dims):
+ '''
+ Reformat dimensions list into AWS API format
+ Parameter dims is a list of dicts
+ '''
+ newdims = []
+ for count, d in enumerate(dims, 1):
+ for key in d.keys():
+ newdims.append({'Name': key, 'Value': d[key]})
+ return newdims
+
+ def delete_alarms(self, req):
+ """
+ Implements DeleteAlarms API action
+ """
+ return exception.HeatAPINotImplementedError()
+
+ def describe_alarm_history(self, req):
+ """
+ Implements DescribeAlarmHistory API action
+ """
+ return exception.HeatAPINotImplementedError()
+
+ def describe_alarms(self, req):
+ """
+ Implements DescribeAlarms API action
+ """
+
+ def format_metric_alarm(a):
+ """
+ Reformat engine output into the AWS "MetricAlarm" format
+ """
+ keymap = {
+ engine_api.WATCH_ACTIONS_ENABLED: 'ActionsEnabled',
+ engine_api.WATCH_ALARM_ACTIONS: 'AlarmActions',
+ engine_api.WATCH_TOPIC: 'AlarmArn',
+ engine_api.WATCH_UPDATED_TIME:
+ 'AlarmConfigurationUpdatedTimestamp',
+ engine_api.WATCH_DESCRIPTION: 'AlarmDescription',
+ engine_api.WATCH_NAME: 'AlarmName',
+ engine_api.WATCH_COMPARISON: 'ComparisonOperator',
+ engine_api.WATCH_DIMENSIONS: 'Dimensions',
+ engine_api.WATCH_PERIODS: 'EvaluationPeriods',
+ engine_api.WATCH_INSUFFICIENT_ACTIONS: 'InsufficientDataActions',
+ engine_api.WATCH_METRIC_NAME: 'MetricName',
+ engine_api.WATCH_NAMESPACE: 'Namespace',
+ engine_api.WATCH_OK_ACTIONS: 'OKActions',
+ engine_api.WATCH_PERIOD: 'Period',
+ engine_api.WATCH_STATE_REASON: 'StateReason',
+ engine_api.WATCH_STATE_REASON_DATA: 'StateReasonData',
+ engine_api.WATCH_STATE_UPDATED_TIME: 'StateUpdatedTimestamp',
+ engine_api.WATCH_STATE_VALUE: 'StateValue',
+ engine_api.WATCH_STATISTIC: 'Statistic',
+ engine_api.WATCH_THRESHOLD: 'Threshold',
+ engine_api.WATCH_UNIT: 'Unit'}
+
+ # AWS doesn't return StackName in the main MetricAlarm
+ # structure, so we add StackName as a dimension to all responses
+ a[engine_api.WATCH_DIMENSIONS].append({'StackName':
+ a[engine_api.WATCH_STACK_NAME]})
+
+ # Reformat dimensions list into AWS API format
+ a[engine_api.WATCH_DIMENSIONS] = self._reformat_dimensions(
+ a[engine_api.WATCH_DIMENSIONS])
+
+ return api_utils.reformat_dict_keys(keymap, a)
+
+ con = req.context
+ parms = dict(req.params)
+ try:
+ name = parms['AlarmName']
+ except KeyError:
+ name = None
+
+ try:
+ watch_list = self.engine_rpcapi.show_watch(con, watch_name=name)
+ except rpc_common.RemoteError as ex:
+ return exception.map_remote_error(ex)
+
+ res = {'MetricAlarms': [format_metric_alarm(a)
+ for a in watch_list]}
+
+ result = api_utils.format_response("DescribeAlarms", res)
+ return result
+
+ def describe_alarms_for_metric(self, req):
+ """
+ Implements DescribeAlarmsForMetric API action
+ """
+ return exception.HeatAPINotImplementedError()
+
+ def disable_alarm_actions(self, req):
+ """
+ Implements DisableAlarmActions API action
+ """
+ return exception.HeatAPINotImplementedError()
+
+ def enable_alarm_actions(self, req):
+ """
+ Implements EnableAlarmActions API action
+ """
+ return exception.HeatAPINotImplementedError()
+
+ def get_metric_statistics(self, req):
+ """
+ Implements GetMetricStatistics API action
+ """
+ return exception.HeatAPINotImplementedError()
+
+ def list_metrics(self, req):
+ """
+ Implements ListMetrics API action
+ Lists metric datapoints associated with a particular alarm,
+ or all alarms if none specified
+ """
+ def format_metric_data(d, fil={}):
+ """
+ Reformat engine output into the AWS "Metric" format
+ Takes an optional filter dict, which is traversed
+ so a metric dict is only returned if all keys match
+ the filter dict
+ """
+ dimensions = [
+ {'AlarmName': d[engine_api.WATCH_DATA_ALARM]},
+ {'Timestamp': d[engine_api.WATCH_DATA_TIME]}
+ ]
+ for key in d[engine_api.WATCH_DATA]:
+ dimensions.append({key: d[engine_api.WATCH_DATA][key]})
+
+ newdims = self._reformat_dimensions(dimensions)
+
+ result = {
+ 'MetricName': d[engine_api.WATCH_DATA_METRIC],
+ 'Dimensions': newdims,
+ 'Namespace': d[engine_api.WATCH_DATA_NAMESPACE],
+ }
+
+ for f in fil:
+ try:
+ value = result[f]
+ if value != fil[f]:
+ # Filter criteria not met, return None
+ return
+ except KeyError:
+ logger.warning("Invalid filter key %s, ignoring" % f)
+
+ return result
+
+ con = req.context
+ parms = dict(req.params)
+ # FIXME : Don't yet handle filtering by Dimensions
+ filter_result = dict((k, v) for (k, v) in parms.iteritems() if k in
+ ("MetricName", "Namespace"))
+ logger.debug("filter parameters : %s" % filter_result)
+
+ try:
+ # Engine does not currently support query by namespace/metric
+ # so we pass None/None and do any filtering locally
+ watch_data = self.engine_rpcapi.show_watch_metric(con,
+ namespace=None,
+ metric_name=None)
+ except rpc_common.RemoteError as ex:
+ return exception.map_remote_error(ex)
+
+ res = {'Metrics': []}
+ for d in watch_data:
+ metric = format_metric_data(d, filter_result)
+ if metric:
+ res['Metrics'].append(metric)
+
+ result = api_utils.format_response("ListMetrics", res)
+ return result
+
+ def put_metric_alarm(self, req):
+ """
+ Implements PutMetricAlarm API action
+ """
+ return exception.HeatAPINotImplementedError()
+
+ def put_metric_data(self, req):
+ """
+ Implements PutMetricData API action
+ """
+
+ con = req.context
+ parms = dict(req.params)
+ namespace = api_utils.get_param_value(parms, 'Namespace')
+
+ # Extract data from the request so we can pass it to the engine
+ # We have to do this in two passes, because the AWS
+ # query format nests the dimensions within the MetricData
+ # query-parameter-list (see AWS PutMetricData docs)
+ # extract_param_list gives a list-of-dict, which we then
+ # need to process (each dict) for dimensions
+ metric_data = api_utils.extract_param_list(parms, prefix='MetricData')
+ if not len(metric_data):
+ logger.error("Request does not contain required MetricData")
+ return exception.HeatMissingParameterError("MetricData list")
+
+ watch_name = None
+ dimensions = []
+ for p in metric_data:
+ dimension = api_utils.extract_param_pairs(p,
+ prefix='Dimensions',
+ keyname='Name',
+ valuename='Value')
+ if 'AlarmName' in dimension:
+ watch_name = dimension['AlarmName']
+ else:
+ dimensions.append(dimension)
+
+ # We expect an AlarmName dimension as currently the engine
+ # implementation requires metric data to be associated
+ # with an alarm. When this is fixed, we can simply
+ # parse the user-defined dimensions and add the list to
+ # the metric data
+ if not watch_name:
+ logger.error("Request does not contain AlarmName dimension!")
+ return exception.HeatMissingParameterError("AlarmName dimension")
+
+ # Extract the required data from the metric_data
+ # and format dict to pass to engine
+ data = {'Namespace': namespace,
+ api_utils.get_param_value(metric_data[0], 'MetricName'): {
+ 'Unit': api_utils.get_param_value(metric_data[0], 'Unit'),
+ 'Value': api_utils.get_param_value(metric_data[0],
+ 'Value'),
+ 'Dimensions': dimensions}}
+
+ try:
+ res = self.engine_rpcapi.create_watch_data(con, watch_name, data)
+ except rpc_common.RemoteError as ex:
+ return exception.map_remote_error(ex)
+
+ result = {'ResponseMetadata': None}
+ return api_utils.format_response("PutMetricData", result)
+
+ def set_alarm_state(self, req):
+ """
+ Implements SetAlarmState API action
+ """
+ # Map from AWS state names to those used in the engine
+ state_map = {'OK': engine_api.WATCH_STATE_OK,
+ 'ALARM': engine_api.WATCH_STATE_ALARM,
+ 'INSUFFICIENT_DATA': engine_api.WATCH_STATE_NODATA}
+
+ con = req.context
+ parms = dict(req.params)
+
+ # Get mandatory parameters
+ name = api_utils.get_param_value(parms, 'AlarmName')
+ state = api_utils.get_param_value(parms, 'StateValue')
+
+ if state not in state_map:
+ logger.error("Invalid state %s, expecting one of %s" %
+ (state, state_map.keys()))
+ return exception.HeatInvalidParameterValueError("Invalid state %s"
+ % state)
+
+ # Check for optional parameters
+ # FIXME : We don't actually do anything with these in the engine yet..
+ state_reason = None
+ state_reason_data = None
+ if 'StateReason' in parms:
+ state_reason = parms['StateReason']
+ if 'StateReasonData' in parms:
+ state_reason_data = parms['StateReasonData']
+
+ logger.debug("setting %s to %s" % (name, state_map[state]))
+ try:
+ ret = self.engine_rpcapi.set_watch_state(con, watch_name=name,
+ state=state_map[state])
+ except rpc_common.RemoteError as ex:
+ return exception.map_remote_error(ex)
+
+ return api_utils.format_response("SetAlarmState", "")
+
+
+def create_resource(options):
+ """
+ Watch resource factory method.
+ """
+ deserializer = wsgi.JSONRequestDeserializer()
+ return wsgi.Resource(WatchController(options), deserializer)
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+
+import sys
+import socket
+import nose
+import mox
+import json
+import unittest
+from nose.plugins.attrib import attr
+
+import httplib
+import json
+import urlparse
+
+from heat.common import config
+from heat.common import context
+from heat.engine import auth
+from heat.openstack.common import cfg
+from heat.openstack.common import rpc
+import heat.openstack.common.rpc.common as rpc_common
+from heat.common.wsgi import Request
+from heat.api.aws import exception
+import heat.api.cloudwatch.watch as watches
+import heat.engine.api as engine_api
+
+
+@attr(tag=['unit', 'api-cloudwatch', 'WatchController'])
+@attr(speed='fast')
+class WatchControllerTest(unittest.TestCase):
+ '''
+ Tests the API class which acts as the WSGI controller,
+ the endpoint processing API requests after they are routed
+ '''
+ # Utility functions
+ def _create_context(self, user='api_test_user'):
+ ctx = context.get_admin_context()
+ self.m.StubOutWithMock(ctx, 'username')
+ ctx.username = user
+ self.m.StubOutWithMock(auth, 'authenticate')
+ return ctx
+
+ def _dummy_GET_request(self, params={}):
+ # Mangle the params dict into a query string
+ qs = "&".join(["=".join([k, str(params[k])]) for k in params])
+ environ = {'REQUEST_METHOD': 'GET', 'QUERY_STRING': qs}
+ req = Request(environ)
+ req.context = self._create_context()
+ return req
+
+ # The tests
+ def test_reformat_dimensions(self):
+
+ dims = [{'StackName': u'wordpress_ha5',
+ 'Foo': 'bar'}]
+ response = self.controller._reformat_dimensions(dims)
+ expected = [{'Name': 'StackName', 'Value': u'wordpress_ha5'},
+ {'Name': 'Foo', 'Value': 'bar'}]
+ self.assert_(response == expected)
+
+ def test_delete(self):
+ # Not yet implemented, should raise HeatAPINotImplementedError
+ params = {'Action': 'DeleteAlarms'}
+ dummy_req = self._dummy_GET_request(params)
+ result = self.controller.delete_alarms(dummy_req)
+ self.assert_(type(result) == exception.HeatAPINotImplementedError)
+
+ def test_describe_alarm_history(self):
+ # Not yet implemented, should raise HeatAPINotImplementedError
+ params = {'Action': 'DescribeAlarmHistory'}
+ dummy_req = self._dummy_GET_request(params)
+ result = self.controller.describe_alarm_history(dummy_req)
+ self.assert_(type(result) == exception.HeatAPINotImplementedError)
+
+ def test_describe_all(self):
+ watch_name = None # Get all watches
+
+ # Format a dummy GET request to pass into the WSGI handler
+ params = {'Action': 'DescribeAlarms'}
+ dummy_req = self._dummy_GET_request(params)
+
+ # Stub out the RPC call to the engine with a pre-canned response
+ engine_resp = [{u'state_updated_time': u'2012-08-30T14:13:21Z',
+ u'stack_name': u'wordpress_ha5',
+ u'period': u'300',
+ u'actions': [u'WebServerRestartPolicy'],
+ u'topic': None,
+ u'periods': u'1',
+ u'statistic': u'SampleCount',
+ u'threshold': u'2',
+ u'unit': None,
+ u'state_reason': None,
+ u'dimensions': [],
+ u'namespace': u'system/linux',
+ u'state_value': u'NORMAL',
+ u'ok_actions': None,
+ u'description': u'Restart the WikiDatabase',
+ u'actions_enabled': None,
+ u'state_reason_data': None,
+ u'insufficient_actions': None,
+ u'metric_name': u'ServiceFailure',
+ u'comparison': u'GreaterThanThreshold',
+ u'name': u'HttpFailureAlarm',
+ u'updated_time': u'2012-08-30T14:10:46Z'}]
+
+ self.m.StubOutWithMock(rpc, 'call')
+ rpc.call(dummy_req.context, self.topic, {'args':
+ {'watch_name': watch_name},
+ 'method': 'show_watch',
+ 'version': self.api_version},
+ None).AndReturn(engine_resp)
+
+ self.m.ReplayAll()
+
+ # Call the list controller function and compare the response
+ response = self.controller.describe_alarms(dummy_req)
+
+ expected = {'DescribeAlarmsResponse': {'DescribeAlarmsResult':
+ {'MetricAlarms': [
+ {'EvaluationPeriods': u'1',
+ 'StateReasonData': None,
+ 'AlarmArn': None,
+ 'StateUpdatedTimestamp': u'2012-08-30T14:13:21Z',
+ 'AlarmConfigurationUpdatedTimestamp':
+ u'2012-08-30T14:10:46Z',
+ 'AlarmActions': [u'WebServerRestartPolicy'],
+ 'Threshold': u'2',
+ 'AlarmDescription': u'Restart the WikiDatabase',
+ 'Namespace': u'system/linux',
+ 'Period': u'300',
+ 'StateValue': u'NORMAL',
+ 'ComparisonOperator': u'GreaterThanThreshold',
+ 'AlarmName': u'HttpFailureAlarm',
+ 'Unit': None,
+ 'Statistic': u'SampleCount',
+ 'StateReason': None,
+ 'InsufficientDataActions': None,
+ 'OKActions': None,
+ 'MetricName': u'ServiceFailure',
+ 'ActionsEnabled': None,
+ 'Dimensions': [
+ {'Name': 'StackName',
+ 'Value': u'wordpress_ha5'}]}]}}}
+
+ self.assert_(response == expected)
+
+ def test_describe_alarms_for_metric(self):
+ # Not yet implemented, should raise HeatAPINotImplementedError
+ params = {'Action': 'DescribeAlarmsForMetric'}
+ dummy_req = self._dummy_GET_request(params)
+ result = self.controller.describe_alarms_for_metric(dummy_req)
+ self.assert_(type(result) == exception.HeatAPINotImplementedError)
+
+ def test_disable_alarm_actions(self):
+ # Not yet implemented, should raise HeatAPINotImplementedError
+ params = {'Action': 'DisableAlarmActions'}
+ dummy_req = self._dummy_GET_request(params)
+ result = self.controller.disable_alarm_actions(dummy_req)
+ self.assert_(type(result) == exception.HeatAPINotImplementedError)
+
+ def test_enable_alarm_actions(self):
+ # Not yet implemented, should raise HeatAPINotImplementedError
+ params = {'Action': 'EnableAlarmActions'}
+ dummy_req = self._dummy_GET_request(params)
+ result = self.controller.enable_alarm_actions(dummy_req)
+ self.assert_(type(result) == exception.HeatAPINotImplementedError)
+
+ def test_get_metric_statistics(self):
+ # Not yet implemented, should raise HeatAPINotImplementedError
+ params = {'Action': 'GetMetricStatistics'}
+ dummy_req = self._dummy_GET_request(params)
+ result = self.controller.get_metric_statistics(dummy_req)
+ self.assert_(type(result) == exception.HeatAPINotImplementedError)
+
+ def test_list_metrics_all(self):
+ params = {'Action': 'ListMetrics'}
+ dummy_req = self._dummy_GET_request(params)
+
+ # Stub out the RPC call to the engine with a pre-canned response
+ # We dummy three different metrics and namespaces to test
+ # filtering by parameter
+ engine_resp = [
+ {u'timestamp': u'2012-08-30T15:09:02Z',
+ u'watch_name': u'HttpFailureAlarm',
+ u'namespace': u'system/linux',
+ u'metric_name': u'ServiceFailure',
+ u'data': {u'Units': u'Counter', u'Value': 1}},
+
+ {u'timestamp': u'2012-08-30T15:10:03Z',
+ u'watch_name': u'HttpFailureAlarm2',
+ u'namespace': u'system/linux2',
+ u'metric_name': u'ServiceFailure2',
+ u'data': {u'Units': u'Counter', u'Value': 1}},
+
+ {u'timestamp': u'2012-08-30T15:16:03Z',
+ u'watch_name': u'HttpFailureAlar3m',
+ u'namespace': u'system/linux3',
+ u'metric_name': u'ServiceFailure3',
+ u'data': {u'Units': u'Counter', u'Value': 1}}]
+
+ self.m.StubOutWithMock(rpc, 'call')
+ # Current engine implementation means we filter in the API
+ # and pass None/None for namespace/watch_name which returns
+ # all metric data which we post-process in the API
+ rpc.call(dummy_req.context, self.topic, {'args':
+ {'namespace': None,
+ 'metric_name': None},
+ 'method': 'show_watch_metric', 'version': self.api_version},
+ None).AndReturn(engine_resp)
+
+ self.m.ReplayAll()
+
+ # First pass no query paramters filtering, should get all three
+ response = self.controller.list_metrics(dummy_req)
+ expected = {'ListMetricsResponse': {'ListMetricsResult': {'Metrics': [
+ {'Namespace': u'system/linux',
+ 'Dimensions': [
+ {'Name': 'AlarmName', 'Value': u'HttpFailureAlarm'},
+ {'Name': 'Timestamp',
+ 'Value': u'2012-08-30T15:09:02Z'},
+ {'Name': u'Units', 'Value': u'Counter'},
+ {'Name': u'Value', 'Value': 1}],
+ 'MetricName': u'ServiceFailure'},
+
+ {'Namespace': u'system/linux2',
+ 'Dimensions': [
+ {'Name': 'AlarmName', 'Value': u'HttpFailureAlarm2'},
+ {'Name': 'Timestamp',
+ 'Value': u'2012-08-30T15:10:03Z'},
+ {'Name': u'Units', 'Value': u'Counter'},
+ {'Name': u'Value', 'Value': 1}],
+ 'MetricName': u'ServiceFailure2'},
+
+ {'Namespace': u'system/linux3',
+ 'Dimensions': [
+ {'Name': 'AlarmName', 'Value': u'HttpFailureAlar3m'},
+ {'Name': 'Timestamp',
+ 'Value': u'2012-08-30T15:16:03Z'},
+ {'Name': u'Units', 'Value': u'Counter'},
+ {'Name': u'Value', 'Value': 1}],
+ 'MetricName': u'ServiceFailure3'}]}}}
+ self.assert_(response == expected)
+
+ def test_list_metrics_filter_name(self):
+
+ # Add a MetricName filter, so we should only get one of the three
+ params = {'Action': 'ListMetrics',
+ 'MetricName': 'ServiceFailure'}
+ dummy_req = self._dummy_GET_request(params)
+
+ # Stub out the RPC call to the engine with a pre-canned response
+ # We dummy three different metrics and namespaces to test
+ # filtering by parameter
+ engine_resp = [
+ {u'timestamp': u'2012-08-30T15:09:02Z',
+ u'watch_name': u'HttpFailureAlarm',
+ u'namespace': u'system/linux',
+ u'metric_name': u'ServiceFailure',
+ u'data': {u'Units': u'Counter', u'Value': 1}},
+
+ {u'timestamp': u'2012-08-30T15:10:03Z',
+ u'watch_name': u'HttpFailureAlarm2',
+ u'namespace': u'system/linux2',
+ u'metric_name': u'ServiceFailure2',
+ u'data': {u'Units': u'Counter', u'Value': 1}},
+
+ {u'timestamp': u'2012-08-30T15:16:03Z',
+ u'watch_name': u'HttpFailureAlar3m',
+ u'namespace': u'system/linux3',
+ u'metric_name': u'ServiceFailure3',
+ u'data': {u'Units': u'Counter', u'Value': 1}}]
+
+ self.m.StubOutWithMock(rpc, 'call')
+ # Current engine implementation means we filter in the API
+ # and pass None/None for namespace/watch_name which returns
+ # all metric data which we post-process in the API
+ rpc.call(dummy_req.context, self.topic, {'args':
+ {'namespace': None,
+ 'metric_name': None},
+ 'method': 'show_watch_metric', 'version': self.api_version},
+ None).AndReturn(engine_resp)
+
+ self.m.ReplayAll()
+
+ # First pass no query paramters filtering, should get all three
+ response = self.controller.list_metrics(dummy_req)
+ expected = {'ListMetricsResponse': {'ListMetricsResult': {'Metrics': [
+ {'Namespace': u'system/linux',
+ 'Dimensions': [
+ {'Name': 'AlarmName', 'Value': u'HttpFailureAlarm'},
+ {'Name': 'Timestamp',
+ 'Value': u'2012-08-30T15:09:02Z'},
+ {'Name': u'Units', 'Value': u'Counter'},
+ {'Name': u'Value', 'Value': 1}],
+ 'MetricName': u'ServiceFailure'},
+ ]}}}
+ self.assert_(response == expected)
+
+ def test_list_metrics_filter_namespace(self):
+
+ # Add a Namespace filter and change the engine response so
+ # we should get two reponses
+ params = {'Action': 'ListMetrics',
+ 'Namespace': 'atestnamespace/foo'}
+ dummy_req = self._dummy_GET_request(params)
+
+ # Stub out the RPC call to the engine with a pre-canned response
+ # We dummy three different metrics and namespaces to test
+ # filtering by parameter
+ engine_resp = [
+ {u'timestamp': u'2012-08-30T15:09:02Z',
+ u'watch_name': u'HttpFailureAlarm',
+ u'namespace': u'atestnamespace/foo',
+ u'metric_name': u'ServiceFailure',
+ u'data': {u'Units': u'Counter', u'Value': 1}},
+
+ {u'timestamp': u'2012-08-30T15:10:03Z',
+ u'watch_name': u'HttpFailureAlarm2',
+ u'namespace': u'atestnamespace/foo',
+ u'metric_name': u'ServiceFailure2',
+ u'data': {u'Units': u'Counter', u'Value': 1}},
+
+ {u'timestamp': u'2012-08-30T15:16:03Z',
+ u'watch_name': u'HttpFailureAlar3m',
+ u'namespace': u'system/linux3',
+ u'metric_name': u'ServiceFailure3',
+ u'data': {u'Units': u'Counter', u'Value': 1}}]
+
+ self.m.StubOutWithMock(rpc, 'call')
+ # Current engine implementation means we filter in the API
+ # and pass None/None for namespace/watch_name which returns
+ # all metric data which we post-process in the API
+ rpc.call(dummy_req.context, self.topic, {'args':
+ {'namespace': None,
+ 'metric_name': None},
+ 'method': 'show_watch_metric', 'version': self.api_version},
+ None).AndReturn(engine_resp)
+
+ self.m.ReplayAll()
+
+ response = self.controller.list_metrics(dummy_req)
+ expected = {'ListMetricsResponse': {'ListMetricsResult': {'Metrics': [
+ {'Namespace': u'atestnamespace/foo',
+ 'Dimensions': [
+ {'Name': 'AlarmName', 'Value': u'HttpFailureAlarm'},
+ {'Name': 'Timestamp', 'Value': u'2012-08-30T15:09:02Z'},
+ {'Name': u'Units', 'Value': u'Counter'},
+ {'Name': u'Value', 'Value': 1}],
+ 'MetricName': u'ServiceFailure'},
+
+ {'Namespace': u'atestnamespace/foo',
+ 'Dimensions': [
+ {'Name': 'AlarmName', 'Value': u'HttpFailureAlarm2'},
+ {'Name': 'Timestamp', 'Value': u'2012-08-30T15:10:03Z'},
+ {'Name': u'Units', 'Value': u'Counter'},
+ {'Name': u'Value', 'Value': 1}],
+ 'MetricName': u'ServiceFailure2'}]}}}
+ self.assert_(response == expected)
+
+ def test_put_metric_alarm(self):
+ # Not yet implemented, should raise HeatAPINotImplementedError
+ params = {'Action': 'PutMetricAlarm'}
+ dummy_req = self._dummy_GET_request(params)
+ result = self.controller.put_metric_alarm(dummy_req)
+ self.assert_(type(result) == exception.HeatAPINotImplementedError)
+
+ def test_put_metric_data(self):
+
+ params = {u'Namespace': u'system/linux',
+ u'MetricData.member.1.Unit': u'Count',
+ u'MetricData.member.1.Value': u'1',
+ u'MetricData.member.1.MetricName': u'ServiceFailure',
+ u'MetricData.member.1.Dimensions.member.1.Name':
+ u'AlarmName',
+ u'MetricData.member.1.Dimensions.member.1.Value':
+ u'HttpFailureAlarm',
+ u'Action': u'PutMetricData'}
+
+ dummy_req = self._dummy_GET_request(params)
+
+ # Stub out the RPC call to verify the engine call parameters
+ engine_resp = {}
+
+ self.m.StubOutWithMock(rpc, 'call')
+ rpc.call(dummy_req.context, self.topic, {'args': {'stats_data':
+ {'Namespace': u'system/linux',
+ u'ServiceFailure':
+ {'Value': u'1',
+ 'Unit': u'Count',
+ 'Dimensions': []}},
+ 'watch_name': u'HttpFailureAlarm'},
+ 'method': 'create_watch_data',
+ 'version': self.api_version},
+ None).AndReturn(engine_resp)
+
+ self.m.ReplayAll()
+
+ response = self.controller.put_metric_data(dummy_req)
+ expected = {'PutMetricDataResponse': {'PutMetricDataResult':
+ {'ResponseMetadata': None}}}
+ self.assert_(response == expected)
+
+ def test_set_alarm_state(self):
+ state_map = {'OK': engine_api.WATCH_STATE_OK,
+ 'ALARM': engine_api.WATCH_STATE_ALARM,
+ 'INSUFFICIENT_DATA': engine_api.WATCH_STATE_NODATA}
+
+ for state in state_map.keys():
+ params = {u'StateValue': state,
+ u'StateReason': u'',
+ u'AlarmName': u'HttpFailureAlarm',
+ u'Action': u'SetAlarmState'}
+
+ dummy_req = self._dummy_GET_request(params)
+
+ # Stub out the RPC call to verify the engine call parameters
+ # The real engine response is the same as show_watch but with
+ # the state overridden, but since the API doesn't make use
+ # of the response at present we pass nothing back from the stub
+ engine_resp = {}
+
+ self.m.StubOutWithMock(rpc, 'call')
+ rpc.call(dummy_req.context, self.topic, {'args':
+ {'state': state_map[state],
+ 'watch_name': u'HttpFailureAlarm'},
+ 'method': 'set_watch_state',
+ 'version': self.api_version},
+ None).AndReturn(engine_resp)
+
+ self.m.ReplayAll()
+
+ response = self.controller.set_alarm_state(dummy_req)
+ expected = {'SetAlarmStateResponse': {'SetAlarmStateResult': ''}}
+ self.assert_(response == expected)
+
+ self.m.UnsetStubs()
+ self.m.VerifyAll()
+
+ def test_set_alarm_state_badstate(self):
+ params = {u'StateValue': "baaaaad",
+ u'StateReason': u'',
+ u'AlarmName': u'HttpFailureAlarm',
+ u'Action': u'SetAlarmState'}
+ dummy_req = self._dummy_GET_request(params)
+
+ # should raise HeatInvalidParameterValueError
+ result = self.controller.set_alarm_state(dummy_req)
+ self.assert_(type(result) == exception.HeatInvalidParameterValueError)
+
+ def setUp(self):
+ self.maxDiff = None
+ self.m = mox.Mox()
+
+ config.register_engine_opts()
+ cfg.CONF.set_default('engine_topic', 'engine')
+ cfg.CONF.set_default('host', 'host')
+ self.topic = '%s.%s' % (cfg.CONF.engine_topic, cfg.CONF.host)
+ self.api_version = '1.0'
+
+ # Create WSGI controller instance
+ class DummyConfig():
+ bind_port = 8003
+ cfgopts = DummyConfig()
+ self.controller = watches.WatchController(options=cfgopts)
+ print "setup complete"
+
+ def tearDown(self):
+ self.m.UnsetStubs()
+ self.m.VerifyAll()
+ print "teardown complete"
+
+
+if __name__ == '__main__':
+ sys.argv.append(__file__)
+ nose.main()