--- /dev/null
+#!/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()
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
class BotoClient(CloudFormationConnection):
+ '''
+ Wrapper class for boto CloudFormationConnection class
+ '''
def list_stacks(self, **kwargs):
return super(BotoClient, self).list_stacks()
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
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
--- /dev/null
+# 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
'bin/heat-boto',
'bin/heat-metadata',
'bin/heat-engine',
+ 'bin/heat-watch',
'bin/heat-db-setup'],
py_modules=[])