heat API : Implement initial CloudWatch API

Initial AWS-compatible CloudWatch API implementation
Supports the following API actions:
- DescribeAlarms : describe alarm/watch details
- ListMetrics : List watch metric datapoints
- PutMetricData : Create metric datapoint
- SetAlarmState : temporarily set alarm state

Skeleton implementation of all other TODO actions which
returns HeatAPINotImplementedError.

Only basic filtering parameters supported at this time.

Signed-off-by: Steven Hardy <shardy@redhat.com>
Change-Id: I8628854a135fff07b675e85150ea0b50184ed2e1
This commit is contained in:
Steven Hardy 2012-08-22 10:30:03 +01:00
parent 7a382d5ff1
commit 311092a294
7 changed files with 1077 additions and 0 deletions

59
bin/heat-api-cloudwatch Executable file
View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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',