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…
Reference in New Issue
Block a user