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:
parent
7a382d5ff1
commit
311092a294
59
bin/heat-api-cloudwatch
Executable file
59
bin/heat-api-cloudwatch
Executable 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)
|
88
etc/heat-api-cloudwatch-paste.ini
Normal file
88
etc/heat-api-cloudwatch-paste.ini
Normal 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
|
27
etc/heat-api-cloudwatch.conf
Normal file
27
etc/heat-api-cloudwatch.conf
Normal 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
|
81
heat/api/cloudwatch/__init__.py
Normal file
81
heat/api/cloudwatch/__init__.py
Normal 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)
|
334
heat/api/cloudwatch/watch.py
Normal file
334
heat/api/cloudwatch/watch.py
Normal 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)
|
487
heat/tests/test_api_cloudwatch.py
Normal file
487
heat/tests/test_api_cloudwatch.py
Normal 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()
|
Loading…
x
Reference in New Issue
Block a user