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:
parent
311092a294
commit
51dc63bb07
281
bin/heat-watch
Executable file
281
bin/heat-watch
Executable 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()
|
@ -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
|
||||
|
@ -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
|
||||
|
210
heat/boto_client_cloudwatch.py
Normal file
210
heat/boto_client_cloudwatch.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user