# # Copyright 2013-2016 Red Hat, Inc # Copyright Ericsson AB 2014. All rights reserved # # 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 import warnings from oslo_serialization import jsonutils from oslo_utils import strutils import six from six import moves 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'] GNOCCHI_AGGREGATION = ['last', 'min', 'median', 'sum', 'std', 'first', 'mean', 'count', 'moving-average', 'max'] GNOCCHI_AGGREGATION.extend(['%spct' % num for num in moves.xrange(1, 100)]) 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 = ["=", "!=", "<", "<=", '>', '>='] DEFAULT_API_LIMIT = ('API server limits result to ' 'rows if no limit provided. Option is configured in ' 'ceilometer.conf [api] group') 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. %s' % DEFAULT_API_LIMIT) 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 a sample.''' try: sample = cc.new_samples.get(args.sample_id) except exc.HTTPNotFound: 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 ' '(configurable by admin users only).') @utils.arg('--user-id', metavar='', dest='sample_user_id', help='User to associate with sample ' '(configurable by admin users only).') @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.') @utils.arg('--direct', metavar='', default=False, help='Post sample to storage directly.') @_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': try: fields[k] = json.loads(v) except ValueError: msg = ('Invalid resource metadata, it should be a json' ' string, like: \'{"foo":"bar"}\'') raise exc.CommandError(msg) 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.') @utils.arg('-l', '--limit', metavar='', help='Maximum number of meters to return. %s' % DEFAULT_API_LIMIT) @utils.arg('--unique', dest='unique', metavar='{True|False}', type=lambda v: strutils.bool_from_string(v, True), help='Retrieves unique list of meters.') def do_meter_list(cc, args={}): """List the user's meters.""" meters = cc.meters.list(q=options.cli_to_array(args.query), limit=args.limit, unique=args.unique) 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) @utils.arg('samples_list', metavar='', action=NotEmptyAction, help='Json array with samples to create.') @utils.arg('--direct', metavar='', default=False, help='Post samples to storage directly.') def do_sample_create_list(cc, args={}): """Create a sample list.""" sample_list_array = json.loads(args.samples_list) samples = cc.samples.create_list(sample_list_array, direct=args.direct) 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 _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 ('%(statistic)s(%(meter_name)s) %(comparison_operator)s ' '%(threshold)s during %(evaluation_periods)s x %(period)ss' % { 'meter_name': rule['meter_name'], 'threshold': rule['threshold'], 'statistic': rule['statistic'], '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', 'severity']: 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.""" warnings.warn("Alarm commands are deprecated, please use aodhclient") 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.""" warnings.warn("Alarm commands are deprecated, please use aodhclient") alarm = cc.alarms.get(args.alarm_id) # alarm.get actually catches the HTTPNotFound exception and turns the # result into None if the alarm wasn't found. 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 ' '(configurable by admin users only).') @utils.arg('--user-id', metavar='', dest='alarm_user_id', help='User to associate with alarm ' '(configurable by admin users only).') @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=lambda v: strutils.bool_from_string(v, True), 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='