summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xbin/heat-api-cloudwatch59
-rw-r--r--etc/heat-api-cloudwatch-paste.ini88
-rw-r--r--etc/heat-api-cloudwatch.conf27
-rw-r--r--heat/api/cloudwatch/__init__.py81
-rw-r--r--heat/api/cloudwatch/watch.py334
-rw-r--r--heat/tests/test_api_cloudwatch.py487
-rwxr-xr-xsetup.py1
7 files changed, 1077 insertions, 0 deletions
diff --git a/bin/heat-api-cloudwatch b/bin/heat-api-cloudwatch
new file mode 100755
index 000000000..7c88518b2
--- /dev/null
+++ b/bin/heat-api-cloudwatch
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+# 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.
+
+"""
+Heat API Server. This implements an approximation of the Amazon
+CloudWatch API and translates it into a native representation. It then
+calls the heat-engine via AMQP RPC to implement them.
+"""
+
+import gettext
+import os
+import sys
+
+# If ../heat/__init__.py exists, add ../ to Python search path, so that
+# it will override what happens to be installed in /usr/(local/)lib/python...
+possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
+ os.pardir,
+ os.pardir))
+if os.path.exists(os.path.join(possible_topdir, 'heat', '__init__.py')):
+ sys.path.insert(0, possible_topdir)
+
+gettext.install('heat', unicode=1)
+
+from heat.common import config
+from heat.common import wsgi
+
+from heat.openstack.common import cfg
+from heat.openstack.common import log as logging
+
+LOG = logging.getLogger('heat.api.cloudwatch')
+
+if __name__ == '__main__':
+ try:
+ cfg.CONF(project='heat', prog='heat-api-cloudwatch')
+ config.setup_logging()
+ config.register_api_opts()
+
+ app = config.load_paste_app()
+
+ port = cfg.CONF.bind_port
+ host = cfg.CONF.bind_host
+ LOG.info('Starting Heat CloudWatch API on %s:%s' % (host, port))
+ server = wsgi.Server()
+ server.start(app, cfg.CONF, default_port=port)
+ server.wait()
+ except RuntimeError, e:
+ sys.exit("ERROR: %s" % e)
diff --git a/etc/heat-api-cloudwatch-paste.ini b/etc/heat-api-cloudwatch-paste.ini
new file mode 100644
index 000000000..da1e83d59
--- /dev/null
+++ b/etc/heat-api-cloudwatch-paste.ini
@@ -0,0 +1,88 @@
+
+# Default pipeline
+[pipeline:heat-api-cloudwatch]
+pipeline = versionnegotiation ec2authtoken authtoken context apicwapp
+
+# Use the following pipeline for keystone auth
+# i.e. in heat-api-cloudwatch.conf:
+# [paste_deploy]
+# flavor = keystone
+#
+[pipeline:heat-api-cloudwatch-keystone]
+pipeline = versionnegotiation ec2authtoken authtoken context apicwapp
+
+# Use the following pipeline to enable transparent caching of image files
+# i.e. in heat-api-cloudwatch.conf:
+# [paste_deploy]
+# flavor = caching
+#
+[pipeline:heat-api-cloudwatch-caching]
+pipeline = versionnegotiation ec2authtoken authtoken context cache apicwapp
+
+# Use the following pipeline for keystone auth with caching
+# i.e. in heat-api-cloudwatch.conf:
+# [paste_deploy]
+# flavor = keystone+caching
+#
+[pipeline:heat-api-cloudwatch-keystone+caching]
+pipeline = versionnegotiation ec2authtoken authtoken context cache apicwapp
+
+# Use the following pipeline to enable the Image Cache Management API
+# i.e. in heat-api-cloudwatch.conf:
+# [paste_deploy]
+# flavor = cachemanagement
+#
+[pipeline:heat-api-cloudwatch-cachemanagement]
+pipeline = versionnegotiation ec2authtoken authtoken context cache cachemanage apicwapp
+
+# Use the following pipeline for keystone auth with cache management
+# i.e. in heat-api-cloudwatch.conf:
+# [paste_deploy]
+# flavor = keystone+cachemanagement
+#
+[pipeline:heat-api-cloudwatch-keystone+cachemanagement]
+pipeline = versionnegotiation ec2authtoken authtoken auth-context cache cachemanage apicwapp
+
+[app:apicwapp]
+paste.app_factory = heat.common.wsgi:app_factory
+heat.app_factory = heat.api.cloudwatch:API
+
+[filter:versionnegotiation]
+paste.filter_factory = heat.common.wsgi:filter_factory
+heat.filter_factory = heat.api.middleware.version_negotiation:VersionNegotiationFilter
+
+[filter:cache]
+paste.filter_factory = heat.common.wsgi:filter_factory
+heat.filter_factory = heat.api.middleware.cache:CacheFilter
+
+[filter:cachemanage]
+paste.filter_factory = heat.common.wsgi:filter_factory
+heat.filter_factory = heat.api.middleware.cache_manage:CacheManageFilter
+
+[filter:context]
+paste.filter_factory = heat.common.context:ContextMiddleware_filter_factory
+
+[filter:ec2authtoken]
+paste.filter_factory = heat.api.aws.ec2token:EC2Token_filter_factory
+auth_uri = http://127.0.0.1:5000/v2.0
+keystone_ec2_uri = http://localhost:5000/v2.0/ec2tokens
+
+[filter:authtoken]
+paste.filter_factory = heat.common.auth_token:filter_factory
+service_protocol = http
+service_host = 127.0.0.1
+service_port = 5000
+auth_host = 127.0.0.1
+auth_port = 35357
+auth_protocol = http
+auth_uri = http://127.0.0.1:5000/v2.0
+
+# These must be set to your local values in order for the token
+# authentication to work.
+admin_tenant_name = admin
+admin_user = admin
+admin_password = verybadpass
+
+[filter:auth-context]
+paste.filter_factory = heat.common.wsgi:filter_factory
+heat.filter_factory = keystone.middleware.heat_auth_token:KeystoneContextMiddleware
diff --git a/etc/heat-api-cloudwatch.conf b/etc/heat-api-cloudwatch.conf
new file mode 100644
index 000000000..564a0412b
--- /dev/null
+++ b/etc/heat-api-cloudwatch.conf
@@ -0,0 +1,27 @@
+[DEFAULT]
+# Show more verbose log output (sets INFO log level output)
+verbose = True
+
+# Show debugging output in logs (sets DEBUG log level output)
+debug = True
+
+# Address to bind the server to
+bind_host = 0.0.0.0
+
+# Port the bind the server to
+bind_port = 8003
+
+# Log to this file. Make sure the user running heat-api has
+# permissions to write to this file!
+log_file = /var/log/heat/api-cloudwatch.log
+
+# ================= Syslog Options ============================
+
+# Send logs to syslog (/dev/log) instead of to file specified
+# by `log_file`
+use_syslog = False
+
+# Facility to use. If unset defaults to LOG_USER.
+# syslog_log_facility = LOG_LOCAL0
+
+rpc_backend=heat.openstack.common.rpc.impl_qpid
diff --git a/heat/api/cloudwatch/__init__.py b/heat/api/cloudwatch/__init__.py
new file mode 100644
index 000000000..877857857
--- /dev/null
+++ b/heat/api/cloudwatch/__init__.py
@@ -0,0 +1,81 @@
+# 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 json
+import urlparse
+import httplib
+import routes
+import gettext
+
+gettext.install('heat', unicode=1)
+
+from heat.api.cloudwatch import watch
+from heat.common import wsgi
+
+from webob import Request
+import webob
+from heat import utils
+from heat.common import context
+from heat.api.aws import exception
+
+from heat.openstack.common import log as logging
+
+logger = logging.getLogger(__name__)
+
+
+class API(wsgi.Router):
+
+ """
+ WSGI router for Heat CloudWatch API
+ """
+
+ _actions = {
+ 'delete_alarms': 'DeleteAlarms',
+ 'describe_alarm_history': 'DescribeAlarmHistory',
+ 'describe_alarms': 'DescribeAlarms',
+ 'describe_alarms_for_metric': 'DescribeAlarmsForMetric',
+ 'disable_alarm_actions': 'DisableAlarmActions',
+ 'enable_alarm_actions': 'EnableAlarmActions',
+ 'get_metric_statistics': 'GetMetricStatistics',
+ 'list_metrics': 'ListMetrics',
+ 'put_metric_alarm': 'PutMetricAlarm',
+ 'put_metric_data': 'PutMetricData',
+ 'set_alarm_state': 'SetAlarmState',
+ }
+
+ def __init__(self, conf, **local_conf):
+ self.conf = conf
+ mapper = routes.Mapper()
+
+ mapper = routes.Mapper()
+ controller_resource = watch.create_resource(conf)
+
+ def conditions(action):
+ api_action = self._actions[action]
+
+ def action_match(environ, result):
+ req = Request(environ)
+ env_action = req.params.get("Action")
+ return env_action == api_action
+
+ return {'function': action_match}
+
+ for action in self._actions:
+ mapper.connect("/", controller=controller_resource, action=action,
+ conditions=conditions(action))
+
+ mapper.connect("/", controller=controller_resource, action="index")
+
+ super(API, self).__init__(mapper)
diff --git a/heat/api/cloudwatch/watch.py b/heat/api/cloudwatch/watch.py
new file mode 100644
index 000000000..726146bea
--- /dev/null
+++ b/heat/api/cloudwatch/watch.py
@@ -0,0 +1,334 @@
+# 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)
diff --git a/heat/tests/test_api_cloudwatch.py b/heat/tests/test_api_cloudwatch.py
new file mode 100644
index 000000000..3e5fb7acf
--- /dev/null
+++ b/heat/tests/test_api_cloudwatch.py
@@ -0,0 +1,487 @@
+# 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()
diff --git a/setup.py b/setup.py
index 6cafbb08b..092c02032 100755
--- a/setup.py
+++ b/setup.py
@@ -45,6 +45,7 @@ setuptools.setup(
],
scripts=['bin/heat',
'bin/heat-api',
+ 'bin/heat-api-cloudwatch',
'bin/heat-boto',
'bin/heat-metadata',
'bin/heat-engine',