# # Copyright 2013 Red Hat, Inc # Copyright Ericsson AB 2014. All rights reserved # # Authors: Angus Salkeld # Balazs Gibizer # Ildiko Vancsa # # 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 argparse import functools import json from oslo.serialization import jsonutils from oslo.utils import strutils import six from ceilometerclient.common import utils from ceilometerclient import exc from ceilometerclient.v2 import options ALARM_STATES = ['ok', 'alarm', 'insufficient data'] ALARM_SEVERITY = ['low', 'moderate', 'critical'] ALARM_OPERATORS = ['lt', 'le', 'eq', 'ne', 'ge', 'gt'] ALARM_COMBINATION_OPERATORS = ['and', 'or'] STATISTICS = ['max', 'min', 'avg', 'sum', 'count'] AGGREGATES = {'avg': 'Avg', 'count': 'Count', 'max': 'Max', 'min': 'Min', 'sum': 'Sum', 'stddev': 'Standard deviation', 'cardinality': 'Cardinality'} OPERATORS_STRING = dict(gt='>', ge='>=', lt='<', le="<=", eq='==', ne='!=') ORDER_DIRECTIONS = ['asc', 'desc'] COMPLEX_OPERATORS = ['and', 'or'] SIMPLE_OPERATORS = ["=", "!=", "<", "<=", '>', '>='] class NotEmptyAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): values = values or getattr(namespace, self.dest) if not values or values.isspace(): raise exc.CommandError('%s should not be empty' % self.dest) setattr(namespace, self.dest, values) def obsoleted_by(new_dest): class ObsoletedByAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): old_dest = option_string or self.dest print('%s is obsolete! See help for more details.' % old_dest) setattr(namespace, new_dest, values) return ObsoletedByAction @utils.arg('-q', '--query', metavar='', help='key[op]data_type::value; list. data_type is optional, ' 'but if supplied must be string, integer, float, or boolean.') @utils.arg('-m', '--meter', metavar='', required=True, action=NotEmptyAction, help='Name of meter to list statistics for.') @utils.arg('-p', '--period', metavar='', help='Period in seconds over which to group samples.') @utils.arg('-g', '--groupby', metavar='', action='append', help='Field for group by.') @utils.arg('-a', '--aggregate', metavar='[<-]', action='append', default=[], help=('Function for data aggregation. ' 'Available aggregates are: ' '%s.' % ", ".join(AGGREGATES.keys()))) def do_statistics(cc, args): """List the statistics for a meter.""" aggregates = [] for a in args.aggregate: aggregates.append(dict(zip(('func', 'param'), a.split("<-")))) api_args = {'meter_name': args.meter, 'q': options.cli_to_array(args.query), 'period': args.period, 'groupby': args.groupby, 'aggregates': aggregates} try: statistics = cc.statistics.list(**api_args) except exc.HTTPNotFound: raise exc.CommandError('Samples not found: %s' % args.meter) else: fields_display = {'duration': 'Duration', 'duration_end': 'Duration End', 'duration_start': 'Duration Start', 'period': 'Period', 'period_end': 'Period End', 'period_start': 'Period Start', 'groupby': 'Group By'} fields_display.update(AGGREGATES) fields = ['period', 'period_start', 'period_end'] if args.groupby: fields.append('groupby') if args.aggregate: for a in aggregates: if 'param' in a: fields.append("%(func)s/%(param)s" % a) else: fields.append(a['func']) for stat in statistics: stat.__dict__.update(stat.aggregate) else: fields.extend(['max', 'min', 'avg', 'sum', 'count']) fields.extend(['duration', 'duration_start', 'duration_end']) cols = [fields_display.get(f, f) for f in fields] utils.print_list(statistics, fields, cols) @utils.arg('-q', '--query', metavar='', help='key[op]data_type::value; list. data_type is optional, ' 'but if supplied must be string, integer, float, or boolean.') @utils.arg('-m', '--meter', metavar='', action=NotEmptyAction, help='Name of meter to show samples for.') @utils.arg('-l', '--limit', metavar='', help='Maximum number of samples to return.') def do_sample_list(cc, args): """List the samples (return OldSample objects if -m/--meter is set).""" if not args.meter: return _do_sample_list(cc, args) else: return _do_old_sample_list(cc, args) def _do_old_sample_list(cc, args): fields = {'meter_name': args.meter, 'q': options.cli_to_array(args.query), 'limit': args.limit} try: samples = cc.samples.list(**fields) except exc.HTTPNotFound: raise exc.CommandError('Samples not found: %s' % args.meter) else: field_labels = ['Resource ID', 'Name', 'Type', 'Volume', 'Unit', 'Timestamp'] fields = ['resource_id', 'counter_name', 'counter_type', 'counter_volume', 'counter_unit', 'timestamp'] utils.print_list(samples, fields, field_labels, sortby=None) def _do_sample_list(cc, args): fields = { 'q': options.cli_to_array(args.query), 'limit': args.limit } samples = cc.new_samples.list(**fields) field_labels = ['ID', 'Resource ID', 'Name', 'Type', 'Volume', 'Unit', 'Timestamp'] fields = ['id', 'resource_id', 'meter', 'type', 'volume', 'unit', 'timestamp'] utils.print_list(samples, fields, field_labels, sortby=None) @utils.arg('sample_id', metavar='', action=NotEmptyAction, help='ID (aka message ID) of the sample to show.') def do_sample_show(cc, args): '''Show an sample.''' sample = cc.new_samples.get(args.sample_id) if sample is None: raise exc.CommandError('Sample not found: %s' % args.sample_id) fields = ['id', 'meter', 'volume', 'type', 'unit', 'source', 'resource_id', 'user_id', 'project_id', 'timestamp', 'recorded_at', 'metadata'] data = dict((f, getattr(sample, f, '')) for f in fields) utils.print_dict(data, wrap=72) def _restore_shadowed_arg(shadowed, observed): def wrapper(func): @functools.wraps(func) def wrapped(cc, args): v = getattr(args, observed, None) setattr(args, shadowed, v) return func(cc, args) return wrapped return wrapper @utils.arg('--project-id', metavar='', dest='sample_project_id', help='Tenant to associate with sample ' '(only settable by admin users).') @utils.arg('--user-id', metavar='', dest='sample_user_id', help='User to associate with sample ' '(only settable by admin users).') @utils.arg('-r', '--resource-id', metavar='', required=True, help='ID of the resource.') @utils.arg('-m', '--meter-name', metavar='', required=True, action=NotEmptyAction, help='The meter name.') @utils.arg('--meter-type', metavar='', required=True, help='The meter type.') @utils.arg('--meter-unit', metavar='', required=True, help='The meter unit.') @utils.arg('--sample-volume', metavar='', required=True, help='The sample volume.') @utils.arg('--resource-metadata', metavar='', help='Resource metadata. Provided value should be a set of ' 'key-value pairs e.g. {"key":"value"}.') @utils.arg('--timestamp', metavar='', help='The sample timestamp.') @_restore_shadowed_arg('project_id', 'sample_project_id') @_restore_shadowed_arg('user_id', 'sample_user_id') def do_sample_create(cc, args={}): """Create a sample.""" arg_to_field_mapping = { 'meter_name': 'counter_name', 'meter_unit': 'counter_unit', 'meter_type': 'counter_type', 'sample_volume': 'counter_volume', } fields = {} for var in vars(args).items(): k, v = var[0], var[1] if v is not None: if k == 'resource_metadata': fields[k] = json.loads(v) else: fields[arg_to_field_mapping.get(k, k)] = v sample = cc.samples.create(**fields) fields = ['counter_name', 'user_id', 'resource_id', 'timestamp', 'message_id', 'source', 'counter_unit', 'counter_volume', 'project_id', 'resource_metadata', 'counter_type'] data = dict([(f.replace('counter_', ''), getattr(sample[0], f, '')) for f in fields]) utils.print_dict(data, wrap=72) @utils.arg('-q', '--query', metavar='', help='key[op]data_type::value; list. data_type is optional, ' 'but if supplied must be string, integer, float, or boolean.') def do_meter_list(cc, args={}): """List the user's meters.""" meters = cc.meters.list(q=options.cli_to_array(args.query)) field_labels = ['Name', 'Type', 'Unit', 'Resource ID', 'User ID', 'Project ID'] fields = ['name', 'type', 'unit', 'resource_id', 'user_id', 'project_id'] utils.print_list(meters, fields, field_labels, sortby=0) def _display_alarm_list(alarms, sortby=None): # omit action initially to keep output width sane # (can switch over to vertical formatting when available from CLIFF) field_labels = ['Alarm ID', 'Name', 'State', 'Severity', 'Enabled', 'Continuous', 'Alarm condition', 'Time constraints'] fields = ['alarm_id', 'name', 'state', 'severity', 'enabled', 'repeat_actions', 'rule', 'time_constraints'] utils.print_list( alarms, fields, field_labels, formatters={'rule': alarm_rule_formatter, 'time_constraints': time_constraints_formatter_brief}, sortby=sortby) def _display_rule(type, rule): if type == 'threshold': return ('%(meter_name)s %(comparison_operator)s ' '%(threshold)s during %(evaluation_periods)s x %(period)ss' % { 'meter_name': rule['meter_name'], 'threshold': rule['threshold'], 'evaluation_periods': rule['evaluation_periods'], 'period': rule['period'], 'comparison_operator': OPERATORS_STRING.get( rule['comparison_operator']) }) elif type == 'combination': return ('combinated states (%(operator)s) of %(alarms)s' % { 'operator': rule['operator'].upper(), 'alarms': ", ".join(rule['alarm_ids'])}) else: # just dump all return "\n".join(["%s: %s" % (f, v) for f, v in six.iteritems(rule)]) def alarm_rule_formatter(alarm): return _display_rule(alarm.type, alarm.rule) def _display_time_constraints_brief(time_constraints): if time_constraints: return ', '.join('%(name)s at %(start)s %(timezone)s for %(duration)ss' % { 'name': tc['name'], 'start': tc['start'], 'duration': tc['duration'], 'timezone': tc.get('timezone', '') } for tc in time_constraints) else: return 'None' def time_constraints_formatter_brief(alarm): return _display_time_constraints_brief(getattr(alarm, 'time_constraints', None)) def _infer_type(detail): if 'type' in detail: return detail['type'] elif 'meter_name' in detail['rule']: return 'threshold' elif 'alarms' in detail['rule']: return 'combination' else: return 'unknown' def alarm_change_detail_formatter(change): detail = json.loads(change.detail) fields = [] if change.type == 'state transition': fields.append('state: %s' % detail['state']) elif change.type == 'creation' or change.type == 'deletion': for k in ['name', 'description', 'type', 'rule']: if k == 'rule': fields.append('rule: %s' % _display_rule(detail['type'], detail[k])) else: fields.append('%s: %s' % (k, detail[k])) if 'time_constraints' in detail: fields.append('time_constraints: %s' % _display_time_constraints_brief( detail['time_constraints'])) elif change.type == 'rule change': for k, v in six.iteritems(detail): if k == 'rule': fields.append('rule: %s' % _display_rule(_infer_type(detail), v)) else: fields.append('%s: %s' % (k, v)) return '\n'.join(fields) @utils.arg('-q', '--query', metavar='', help='key[op]data_type::value; list. data_type is optional, ' 'but if supplied must be string, integer, float, or boolean.') def do_alarm_list(cc, args={}): """List the user's alarms.""" alarms = cc.alarms.list(q=options.cli_to_array(args.query)) _display_alarm_list(alarms, sortby=0) def alarm_query_formater(alarm): qs = [] for q in alarm.rule['query']: qs.append('%s %s %s' % ( q['field'], OPERATORS_STRING.get(q['op']), q['value'])) return r' AND\n'.join(qs) def time_constraints_formatter_full(alarm): time_constraints = [] for tc in alarm.time_constraints: lines = [] for k in ['name', 'description', 'start', 'duration', 'timezone']: if k in tc and tc[k]: lines.append(r'%s: %s' % (k, tc[k])) time_constraints.append('{' + r',\n '.join(lines) + '}') return '[' + r',\n '.join(time_constraints) + ']' def _display_alarm(alarm): fields = ['name', 'description', 'type', 'state', 'severity', 'enabled', 'alarm_id', 'user_id', 'project_id', 'alarm_actions', 'ok_actions', 'insufficient_data_actions', 'repeat_actions'] data = dict([(f, getattr(alarm, f, '')) for f in fields]) data.update(alarm.rule) if alarm.type == 'threshold': data['query'] = alarm_query_formater(alarm) if alarm.time_constraints: data['time_constraints'] = time_constraints_formatter_full(alarm) utils.print_dict(data, wrap=72) @utils.arg('-a', '--alarm_id', metavar='', action=obsoleted_by('alarm_id'), help=argparse.SUPPRESS, dest='alarm_id_deprecated') @utils.arg('alarm_id', metavar='', nargs='?', action=NotEmptyAction, help='ID of the alarm to show.') def do_alarm_show(cc, args={}): """Show an alarm.""" alarm = cc.alarms.get(args.alarm_id) if alarm is None: raise exc.CommandError('Alarm not found: %s' % args.alarm_id) else: _display_alarm(alarm) def common_alarm_arguments(create=False): def _wrapper(func): @utils.arg('--name', metavar='', required=create, help='Name of the alarm (must be unique per tenant).') @utils.arg('--project-id', metavar='', dest='alarm_project_id', help='Tenant to associate with alarm ' '(only settable by admin users).') @utils.arg('--user-id', metavar='', dest='alarm_user_id', help='User to associate with alarm ' '(only settable by admin users).') @utils.arg('--description', metavar='', help='Free text description of the alarm.') @utils.arg('--state', metavar='', help='State of the alarm, one of: ' + str(ALARM_STATES)) @utils.arg('--severity', metavar='', help='Severity of the alarm, one of: ' + str(ALARM_SEVERITY)) @utils.arg('--enabled', type=strutils.bool_from_string, metavar='{True|False}', help='True if alarm evaluation/actioning is enabled.') @utils.arg('--alarm-action', dest='alarm_actions', metavar='', action='append', default=None, help=('URL to invoke when state transitions to alarm. ' 'May be used multiple times.')) @utils.arg('--ok-action', dest='ok_actions', metavar='', action='append', default=None, help=('URL to invoke when state transitions to OK. ' 'May be used multiple times.')) @utils.arg('--insufficient-data-action', dest='insufficient_data_actions', metavar='', action='append', default=None, help=('URL to invoke when state transitions to ' 'insufficient data. May be used multiple times.')) @utils.arg('--time-constraint', dest='time_constraints', metavar='