heat cli : initial heat-watch cloudwatch API client

Implements new client to demonstrate new Cloudwatch API

Currently only provides options for DescribeAlarms,
ListMetrics, PutMetricData and SetAlarmState

Signed-off-by: Steven Hardy <shardy@redhat.com>
Change-Id: I3963a07694cec9af96d9d7369cc7d18d629fcd2d
This commit is contained in:
Steven Hardy 2012-08-22 10:30:46 +01:00
parent 311092a294
commit 51dc63bb07
5 changed files with 500 additions and 2 deletions

281
bin/heat-watch Executable file
View File

@ -0,0 +1,281 @@
#!/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.
"""
This is the administration program for heat-api-cloudwatch.
It is simply a command-line interface for adding, modifying, and retrieving
information about the cloudwatch alarms and metrics belonging to a user.
It is a convenience application that talks to the heat Cloudwatch API server.
"""
import gettext
import optparse
import os
import os.path
import sys
import time
import json
import logging
from urlparse import urlparse
# 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)
scriptname = os.path.basename(sys.argv[0])
gettext.install('heat', unicode=1)
from heat import boto_client_cloudwatch as heat_client
from heat import version
from heat.common import config
from heat.common import exception
from heat import utils
DEFAULT_PORT=8003
@utils.catch_error('alarm-describe')
def alarm_describe(options, arguments):
'''
Describe detail for specified alarm, or all alarms
if no AlarmName is specified
'''
parameters={}
try:
parameters['AlarmName'] = arguments.pop(0)
except IndexError:
logging.info("No AlarmName passed, getting results for ALL alarms")
c = heat_client.get_client(options.port)
result = c.describe_alarm(**parameters)
print c.format_metric_alarm(result)
@utils.catch_error('alarm-set-state')
def alarm_set_state(options, arguments):
'''
Temporarily set state for specified alarm
'''
usage = ('''Usage:
%s alarm-set-state AlarmName StateValue [StateReason]''' %
(scriptname))
parameters={}
try:
parameters['AlarmName'] = arguments.pop(0)
parameters['StateValue'] = arguments.pop(0)
except IndexError:
logging.error("Must specify AlarmName and StateValue")
print usage
print "StateValue must be one of %s, %s or %s" % (
heat_client.BotoCWClient.ALARM_STATES)
return utils.FAILURE
try:
parameters['StateReason'] = arguments.pop(0)
except IndexError:
parameters['StateReason'] = ""
# We don't handle attaching data to state via this cli tool (yet)
parameters['StateReasonData'] = None
c = heat_client.get_client(options.port)
result = c.set_alarm_state(**parameters)
print result
@utils.catch_error('metric-list')
def metric_list(options, arguments):
'''
List all metric data for a given metric (or all metrics if none specified)
'''
parameters={}
try:
parameters['MetricName'] = arguments.pop(0)
except IndexError:
logging.info("No MetricName passed, getting results for ALL alarms")
c = heat_client.get_client(options.port)
result = c.list_metrics(**parameters)
print c.format_metric(result)
@utils.catch_error('metric-put-data')
def metric_put_data(options, arguments):
'''
Create a datapoint for a specified metric
'''
usage = ('''Usage:
%s metric-put-data AlarmName Namespace MetricName Units MetricValue
e.g
%s metric-put-data HttpFailureAlarm system/linux ServiceFailure Count 1
''' % (scriptname, scriptname))
# NOTE : we currently only support metric datapoints associated with a
# specific AlarmName, due to the current engine/db cloudwatch
# implementation, we should probably revisit this so we can support
# more generic metric data collection
parameters={}
try:
parameters['AlarmName'] = arguments.pop(0)
parameters['Namespace'] = arguments.pop(0)
parameters['MetricName'] = arguments.pop(0)
parameters['MetricUnit'] = arguments.pop(0)
parameters['MetricValue'] = arguments.pop(0)
except IndexError:
logging.error("Please specify the alarm, metric, unit and value")
print usage
return utils.FAILURE
c = heat_client.get_client(options.port)
result = c.put_metric_data(**parameters)
print result
def create_options(parser):
"""
Sets up the CLI and config-file options that may be
parsed and program commands.
:param parser: The option parser
"""
parser.add_option('-v', '--verbose', default=False, action="store_true",
help="Print more verbose output")
parser.add_option('-d', '--debug', default=False, action="store_true",
help="Print more verbose output")
parser.add_option('-p', '--port', dest="port", type=int,
default=DEFAULT_PORT,
help="Port the heat API host listens on. "
"Default: %default")
def parse_options(parser, cli_args):
"""
Returns the parsed CLI options, command to run and its arguments, merged
with any same-named options found in a configuration file
:param parser: The option parser
"""
if not cli_args:
cli_args.append('-h') # Show options in usage output...
(options, args) = parser.parse_args(cli_args)
# HACK(sirp): Make the parser available to the print_help method
# print_help is a command, so it only accepts (options, args); we could
# one-off have it take (parser, options, args), however, for now, I think
# this little hack will suffice
options.__parser = parser
if not args:
parser.print_usage()
sys.exit(0)
command_name = args.pop(0)
command = lookup_command(parser, command_name)
if options.debug:
logging.basicConfig(format='%(levelname)s:%(message)s',
level=logging.DEBUG)
logging.debug("Debug level logging enabled")
elif options.verbose:
logging.basicConfig(format='%(levelname)s:%(message)s',
level=logging.INFO)
else:
logging.basicConfig(format='%(levelname)s:%(message)s',
level=logging.WARNING)
return (options, command, args)
def print_help(options, args):
"""
Print help specific to a command
"""
parser = options.__parser
if not args:
parser.print_usage()
subst = {'prog': os.path.basename(sys.argv[0])}
docs = [lookup_command(parser, cmd).__doc__ % subst for cmd in args]
print '\n\n'.join(docs)
def lookup_command(parser, command_name):
base_commands = {'help': print_help}
watch_commands = {
'describe': alarm_describe,
'set-state': alarm_set_state,
'metric-list': metric_list,
'metric-put-data': metric_put_data}
commands = {}
for command_set in (base_commands, watch_commands):
commands.update(command_set)
try:
command = commands[command_name]
except KeyError:
parser.print_usage()
sys.exit("Unknown command: %s" % command_name)
return command
def main():
'''
'''
usage = """
%prog <command> [options] [args]
Commands:
help <command> Output help for one of the commands below
describe Describe a specified alarm (or all alarms)
set-state Temporarily set the state of an alarm
metric-list List data-points for specified metric
metric-put-data Publish data-point for specified metric
"""
oparser = optparse.OptionParser(version='%%prog %s'
% version.version_string(),
usage=usage.strip())
create_options(oparser)
(opts, cmd, args) = parse_options(oparser, sys.argv[1:])
try:
start_time = time.time()
result = cmd(opts, args)
end_time = time.time()
logging.debug("Completed in %-0.4f sec." % (end_time - start_time))
sys.exit(result)
except (RuntimeError,
NotImplementedError,
exception.ClientConfigurationError), ex:
oparser.print_usage()
logging.error("ERROR: %s" % ex)
sys.exit(1)
if __name__ == '__main__':
main()

View File

@ -11,6 +11,9 @@ debug = 0
cfn_region_name = heat
cfn_region_endpoint = 127.0.0.1
cloudwatch_region_name = heat
cloudwatch_region_endpoint = 127.0.0.1
# Set the client retries to 1, or errors connecting to heat repeat
# which is not useful when debugging API issues
num_retries = 1

View File

@ -25,6 +25,9 @@ import json
class BotoClient(CloudFormationConnection):
'''
Wrapper class for boto CloudFormationConnection class
'''
def list_stacks(self, **kwargs):
return super(BotoClient, self).list_stacks()
@ -274,7 +277,7 @@ def get_client(host, port=None, username=None,
is_silent_upload=False, insecure=True):
"""
Returns a new boto client object to a heat server
Returns a new boto Cloudformation client connection to a heat server
"""
# Note we pass None/None for the keys so boto reads /etc/boto.cfg
@ -286,7 +289,7 @@ def get_client(host, port=None, username=None,
if cloudformation:
logger.debug("Got CF connection object OK")
else:
logger.error("Error establishing connection!")
logger.error("Error establishing Cloudformation connection!")
sys.exit(1)
return cloudformation

View File

@ -0,0 +1,210 @@
# 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.
"""
Client implementation based on the boto AWS client library
"""
from heat.openstack.common import log as logging
logger = logging.getLogger(__name__)
from boto.ec2.cloudwatch import CloudWatchConnection
from boto.ec2.cloudwatch.metric import Metric
from boto.ec2.cloudwatch.alarm import MetricAlarm, MetricAlarms
from boto.ec2.cloudwatch.alarm import AlarmHistoryItem
from boto.ec2.cloudwatch.datapoint import Datapoint
class BotoCWClient(CloudWatchConnection):
'''
Wrapper class for boto CloudWatchConnection class
'''
# TODO : These should probably go in the CW API and be imported
DEFAULT_NAMESPACE = "heat/unknown"
METRIC_UNITS = ("Seconds", "Microseconds", "Milliseconds", "Bytes",
"Kilobytes", "Megabytes", "Gigabytes", "Terabytes",
"Bits", "Kilobits", "Megabits", "Gigabits", "Terabits",
"Percent", "Count", "Bytes/Second", "Kilobytes/Second",
"Megabytes/Second", "Gigabytes/Second", "Terabytes/Second",
"Bits/Second", "Kilobits/Second", "Megabits/Second",
"Gigabits/Second", "Terabits/Second", "Count/Second", None)
METRIC_COMPARISONS = (">=", ">", "<", "<=")
ALARM_STATES = ("OK", "ALARM", "INSUFFICIENT_DATA")
METRIC_STATISTICS = ("Average", "Sum", "SampleCount", "Maximum", "Minimum")
# Note, several of these boto calls take a list of alarm names, so
# we could easily handle multiple alarms per-action, but in the
# interests of keeping the client simple, we just handle one 'AlarmName'
def describe_alarm(self, **kwargs):
# If no AlarmName specified, we pass None, which returns
# results for ALL alarms
try:
name = kwargs['AlarmName']
except KeyError:
name = None
return super(BotoCWClient, self).describe_alarms(
alarm_names=[name])
def list_metrics(self, **kwargs):
# list_metrics returns non-null index in next_token if there
# are more than 500 metric results, in which case we have to
# re-read with the token to get the next batch of results
#
# Also note that we can do more advanced filtering by dimension
# and/or namespace, but for simplicity we only filter by
# MetricName for the time being
try:
name = kwargs['MetricName']
except KeyError:
name = None
results = []
token = None
while True:
results.append(super(BotoCWClient, self).list_metrics(
next_token=token,
dimensions=None,
metric_name=name,
namespace=None))
if not token:
break
return results
def put_metric_data(self, **kwargs):
'''
Publish metric data points to CloudWatch
'''
try:
metric_name = kwargs['MetricName']
metric_unit = kwargs['MetricUnit']
metric_value = kwargs['MetricValue']
metric_namespace = kwargs['Namespace']
except KeyError:
logger.error("Must pass MetricName, MetricUnit, " +\
"Namespace, MetricValue!")
return
try:
metric_unit = kwargs['MetricUnit']
except KeyError:
metric_unit = None
# If we're passed AlarmName, we attach it to the metric
# as a dimension
try:
metric_dims = [{'AlarmName': kwargs['AlarmName']}]
except KeyError:
metric_dims = []
if metric_unit not in self.METRIC_UNITS:
logger.error("MetricUnit not an allowed value")
logger.error("MetricUnit must be one of %s" % self.METRIC_UNITS)
return
return super(BotoCWClient, self).put_metric_data(
namespace=metric_namespace,
name=metric_name,
value=metric_value,
timestamp=None, # This means use "now" in the engine
unit=metric_unit,
dimensions=metric_dims,
statistics=None)
def set_alarm_state(self, **kwargs):
return super(BotoCWClient, self).set_alarm_state(
alarm_name=kwargs['AlarmName'],
state_reason=kwargs['StateReason'],
state_value=kwargs['StateValue'],
state_reason_data=kwargs['StateReasonData'])
def format_metric_alarm(self, alarms):
'''
Return string formatted representation of
boto.ec2.cloudwatch.alarm.MetricAlarm objects
'''
ret = []
for s in alarms:
ret.append("AlarmName : %s" % s.name)
ret.append("AlarmDescription : %s" % s.description)
ret.append("ActionsEnabled : %s" % s.actions_enabled)
ret.append("AlarmActions : %s" % s.alarm_actions)
ret.append("AlarmArn : %s" % s.alarm_arn)
ret.append("AlarmConfigurationUpdatedTimestamp : %s" %
s.last_updated)
ret.append("ComparisonOperator : %s" % s.comparison)
ret.append("Dimensions : %s" % s.dimensions)
ret.append("EvaluationPeriods : %s" % s.evaluation_periods)
ret.append("InsufficientDataActions : %s" %
s.insufficient_data_actions)
ret.append("MetricName : %s" % s.metric)
ret.append("Namespace : %s" % s.namespace)
ret.append("OKActions : %s" % s.ok_actions)
ret.append("Period : %s" % s.period)
ret.append("StateReason : %s" % s.state_reason)
ret.append("StateUpdatedTimestamp : %s" %
s.last_updated)
ret.append("StateValue : %s" % s.state_value)
ret.append("Statistic : %s" % s.statistic)
ret.append("Threshold : %s" % s.threshold)
ret.append("Unit : %s" % s.unit)
ret.append("--")
return '\n'.join(ret)
def format_metric(self, metrics):
'''
Return string formatted representation of
boto.ec2.cloudwatch.metric.Metric objects
'''
# Boto appears to return metrics as a list-inside-a-list
# probably a bug in boto, but work around here
if len(metrics) == 1:
metlist = metrics[0]
elif len(metrics) == 0:
metlist = []
else:
# Shouldn't get here, unless boto gets fixed..
logger.error("Unexpected metric list-of-list length (boto fixed?)")
return "ERROR\n--"
ret = []
for m in metlist:
ret.append("MetricName : %s" % m.name)
ret.append("Namespace : %s" % m.namespace)
ret.append("Dimensions : %s" % m.dimensions)
ret.append("--")
return '\n'.join(ret)
def get_client(port=None):
"""
Returns a new boto CloudWatch client connection to a heat server
Note : Configuration goes in /etc/boto.cfg, not via arguments
"""
# Note we pass None/None for the keys so boto reads /etc/boto.cfg
# Also note is_secure is defaulted to False as HTTPS connections
# don't seem to work atm, FIXME
cloudwatch = BotoCWClient(aws_access_key_id=None,
aws_secret_access_key=None, is_secure=False,
port=port, path="/v1")
if cloudwatch:
logger.debug("Got CW connection object OK")
else:
logger.error("Error establishing CloudWatch connection!")
sys.exit(1)
return cloudwatch

View File

@ -49,5 +49,6 @@ setuptools.setup(
'bin/heat-boto',
'bin/heat-metadata',
'bin/heat-engine',
'bin/heat-watch',
'bin/heat-db-setup'],
py_modules=[])