531 lines
23 KiB
Python
531 lines
23 KiB
Python
#
|
|
# 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
|
|
|
|
from cliff import command
|
|
from cliff import lister
|
|
from cliff import show
|
|
from oslo_serialization import jsonutils
|
|
from oslo_utils import strutils
|
|
from oslo_utils import uuidutils
|
|
|
|
from aodhclient import exceptions
|
|
from aodhclient.i18n import _
|
|
from aodhclient import utils
|
|
|
|
ALARM_TYPES = ['event', 'composite',
|
|
'gnocchi_resources_threshold',
|
|
'gnocchi_aggregation_by_metrics_threshold',
|
|
'gnocchi_aggregation_by_resources_threshold']
|
|
ALARM_STATES = ['ok', 'alarm', 'insufficient data']
|
|
ALARM_SEVERITY = ['low', 'moderate', 'critical']
|
|
ALARM_OPERATORS = ['lt', 'le', 'eq', 'ne', 'ge', 'gt']
|
|
ALARM_OP_MAP = dict(zip(ALARM_OPERATORS, ('<', '<=', '=', '!=', '>=', '>')))
|
|
|
|
ALARM_LIST_COLS = ['alarm_id', 'type', 'name', 'state', 'severity', 'enabled']
|
|
|
|
|
|
class CliAlarmList(lister.Lister):
|
|
"""List alarms"""
|
|
|
|
@staticmethod
|
|
def split_filter_param(param):
|
|
key, eq_op, value = param.partition('=')
|
|
if not eq_op:
|
|
msg = 'Malformed parameter(%s). Use the key=value format.' % param
|
|
raise ValueError(msg)
|
|
return key, value
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(CliAlarmList, self).get_parser(prog_name)
|
|
exclusive_group = parser.add_mutually_exclusive_group()
|
|
exclusive_group.add_argument("--query",
|
|
help="Rich query supported by aodh, "
|
|
"e.g. project_id!=my-id "
|
|
"user_id=foo or user_id=bar")
|
|
exclusive_group.add_argument('--filter', dest='filter',
|
|
metavar='<KEY1=VALUE1;KEY2=VALUE2...>',
|
|
type=self.split_filter_param,
|
|
action='append',
|
|
help='Filter parameters to apply on'
|
|
' returned alarms.')
|
|
parser.add_argument("--limit", type=int, metavar="<LIMIT>",
|
|
help="Number of resources to return "
|
|
"(Default is server default)")
|
|
parser.add_argument("--marker", metavar="<MARKER>",
|
|
help="Last item of the previous listing. "
|
|
"Return the next results after this value,"
|
|
"the supported marker is alarm_id.")
|
|
parser.add_argument("--sort", action="append",
|
|
metavar="<SORT_KEY:SORT_DIR>",
|
|
help="Sort of resource attribute, "
|
|
"e.g. name:asc")
|
|
return parser
|
|
|
|
def take_action(self, parsed_args):
|
|
if parsed_args.query:
|
|
if any([parsed_args.limit, parsed_args.sort, parsed_args.marker]):
|
|
raise exceptions.CommandError(
|
|
"Query and pagination options are mutually "
|
|
"exclusive.")
|
|
query = jsonutils.dumps(
|
|
utils.search_query_builder(parsed_args.query))
|
|
alarms = utils.get_client(self).alarm.query(query=query)
|
|
else:
|
|
filters = dict(parsed_args.filter) if parsed_args.filter else None
|
|
alarms = utils.get_client(self).alarm.list(
|
|
filters=filters, sorts=parsed_args.sort,
|
|
limit=parsed_args.limit, marker=parsed_args.marker)
|
|
return utils.list2cols(ALARM_LIST_COLS, alarms)
|
|
|
|
|
|
def _format_alarm(alarm):
|
|
if alarm.get('composite_rule'):
|
|
composite_rule = jsonutils.dumps(alarm['composite_rule'], indent=2)
|
|
alarm['composite_rule'] = composite_rule
|
|
return alarm
|
|
for alarm_type in ALARM_TYPES:
|
|
if alarm.get('%s_rule' % alarm_type):
|
|
alarm.update(alarm.pop('%s_rule' % alarm_type))
|
|
if alarm["time_constraints"]:
|
|
alarm["time_constraints"] = jsonutils.dumps(alarm["time_constraints"],
|
|
sort_keys=True,
|
|
indent=2)
|
|
# only works for event alarm
|
|
if isinstance(alarm.get('query'), list):
|
|
query_rows = []
|
|
for q in alarm['query']:
|
|
op = ALARM_OP_MAP.get(q['op'], q['op'])
|
|
query_rows.append('%s %s %s' % (q['field'], op, q['value']))
|
|
alarm['query'] = ' AND\n'.join(query_rows)
|
|
return alarm
|
|
|
|
|
|
def _find_alarm_by_name(client, name):
|
|
# then try to get entity as name
|
|
query = jsonutils.dumps({"=": {"name": name}})
|
|
alarms = client.alarm.query(query)
|
|
if len(alarms) > 1:
|
|
msg = (_("Multiple alarms matches found for '%s', "
|
|
"use an ID to be more specific.") % name)
|
|
raise exceptions.NoUniqueMatch(msg)
|
|
elif not alarms:
|
|
msg = (_("Alarm %s not found") % name)
|
|
raise exceptions.NotFound(msg)
|
|
else:
|
|
return alarms[0]
|
|
|
|
|
|
def _find_alarm_id_by_name(client, name):
|
|
alarm = _find_alarm_by_name(client, name)
|
|
return alarm['alarm_id']
|
|
|
|
|
|
def _check_name_and_id_coexist(parsed_args, action):
|
|
if parsed_args.id and parsed_args.name:
|
|
raise exceptions.CommandError(
|
|
"You should provide only one of "
|
|
"alarm ID and alarm name(--name) "
|
|
"to %s an alarm." % action)
|
|
|
|
|
|
def _check_name_and_id_exist(parsed_args, action):
|
|
if not parsed_args.id and not parsed_args.name:
|
|
msg = (_("You need to specify one of "
|
|
"alarm ID and alarm name(--name) "
|
|
"to %s an alarm.") % action)
|
|
raise exceptions.CommandError(msg)
|
|
|
|
|
|
def _check_name_and_id(parsed_args, action):
|
|
_check_name_and_id_coexist(parsed_args, action)
|
|
_check_name_and_id_exist(parsed_args, action)
|
|
|
|
|
|
def _add_name_to_parser(parser, required=False):
|
|
parser.add_argument('--name', metavar='<NAME>',
|
|
required=required,
|
|
help='Name of the alarm')
|
|
return parser
|
|
|
|
|
|
def _add_id_to_parser(parser):
|
|
parser.add_argument("id", nargs='?',
|
|
metavar='<ALARM ID or NAME>',
|
|
help="ID or name of an alarm.")
|
|
return parser
|
|
|
|
|
|
class CliAlarmShow(show.ShowOne):
|
|
"""Show an alarm"""
|
|
|
|
def get_parser(self, prog_name):
|
|
return _add_name_to_parser(
|
|
_add_id_to_parser(
|
|
super(CliAlarmShow, self).get_parser(prog_name)))
|
|
|
|
def take_action(self, parsed_args):
|
|
_check_name_and_id(parsed_args, 'query')
|
|
c = utils.get_client(self)
|
|
if parsed_args.name:
|
|
alarm = _find_alarm_by_name(c, parsed_args.name)
|
|
else:
|
|
if uuidutils.is_uuid_like(parsed_args.id):
|
|
try:
|
|
alarm = c.alarm.get(alarm_id=parsed_args.id)
|
|
except exceptions.NotFound:
|
|
# Maybe it's a name
|
|
alarm = _find_alarm_by_name(c, parsed_args.id)
|
|
else:
|
|
alarm = _find_alarm_by_name(c, parsed_args.id)
|
|
|
|
return self.dict2columns(_format_alarm(alarm))
|
|
|
|
|
|
class CliAlarmCreate(show.ShowOne):
|
|
"""Create an alarm"""
|
|
|
|
create = True
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = _add_name_to_parser(
|
|
super(CliAlarmCreate, self).get_parser(prog_name),
|
|
required=self.create)
|
|
|
|
parser.add_argument('-t', '--type', metavar='<TYPE>',
|
|
required=self.create,
|
|
choices=ALARM_TYPES,
|
|
help='Type of alarm, should be one of: '
|
|
'%s.' % ', '.join(ALARM_TYPES))
|
|
parser.add_argument('--project-id', metavar='<PROJECT_ID>',
|
|
help='Project to associate with alarm '
|
|
'(configurable by admin users only)')
|
|
parser.add_argument('--user-id', metavar='<USER_ID>',
|
|
help='User to associate with alarm '
|
|
'(configurable by admin users only)')
|
|
parser.add_argument('--description', metavar='<DESCRIPTION>',
|
|
help='Free text description of the alarm')
|
|
parser.add_argument('--state', metavar='<STATE>',
|
|
choices=ALARM_STATES,
|
|
help='State of the alarm, one of: '
|
|
+ str(ALARM_STATES))
|
|
parser.add_argument('--severity', metavar='<SEVERITY>',
|
|
choices=ALARM_SEVERITY,
|
|
help='Severity of the alarm, one of: '
|
|
+ str(ALARM_SEVERITY))
|
|
parser.add_argument('--enabled', type=strutils.bool_from_string,
|
|
metavar='{True|False}',
|
|
help=('True if alarm evaluation is enabled'))
|
|
parser.add_argument('--alarm-action', dest='alarm_actions',
|
|
metavar='<Webhook URL>', action='append',
|
|
help=('URL to invoke when state transitions to '
|
|
'alarm. May be used multiple times'))
|
|
parser.add_argument('--ok-action', dest='ok_actions',
|
|
metavar='<Webhook URL>', action='append',
|
|
help=('URL to invoke when state transitions to '
|
|
'OK. May be used multiple times'))
|
|
parser.add_argument('--insufficient-data-action',
|
|
dest='insufficient_data_actions',
|
|
metavar='<Webhook URL>', action='append',
|
|
help=('URL to invoke when state transitions to '
|
|
'insufficient data. May be used multiple '
|
|
'times'))
|
|
parser.add_argument(
|
|
'--time-constraint', dest='time_constraints',
|
|
metavar='<Time Constraint>', action='append',
|
|
type=self.validate_time_constraint,
|
|
help=('Only evaluate the alarm if the time at evaluation '
|
|
'is within this time constraint. Start point(s) of '
|
|
'the constraint are specified with a cron expression'
|
|
', whereas its duration is given in seconds. '
|
|
'Can be specified multiple times for multiple '
|
|
'time constraints, format is: '
|
|
'name=<CONSTRAINT_NAME>;start=<CRON>;'
|
|
'duration=<SECONDS>;[description=<DESCRIPTION>;'
|
|
'[timezone=<IANA Timezone>]]'))
|
|
parser.add_argument('--repeat-actions', dest='repeat_actions',
|
|
metavar='{True|False}',
|
|
type=strutils.bool_from_string,
|
|
help=('True if actions should be repeatedly '
|
|
'notified while alarm remains in target '
|
|
'state'))
|
|
|
|
common_group = parser.add_argument_group('common alarm rules')
|
|
common_group.add_argument(
|
|
'--query', metavar='<QUERY>', dest='query',
|
|
help="For alarms of type event: "
|
|
"key[op]data_type::value; list. data_type is optional, "
|
|
"but if supplied must be string, integer, float, or boolean. "
|
|
'For alarms of '
|
|
'type gnocchi_aggregation_by_resources_threshold: '
|
|
'need to specify a complex query json string, like:'
|
|
' {"and": [{"=": {"ended_at": null}}, ...]}.')
|
|
common_group.add_argument(
|
|
'--comparison-operator', metavar='<OPERATOR>',
|
|
dest='comparison_operator', choices=ALARM_OPERATORS,
|
|
help='Operator to compare with, one of: ' + str(ALARM_OPERATORS))
|
|
common_group.add_argument(
|
|
'--evaluation-periods', type=int, metavar='<EVAL_PERIODS>',
|
|
dest='evaluation_periods',
|
|
help='Number of periods to evaluate over')
|
|
common_group.add_argument(
|
|
'--threshold', type=float, metavar='<THRESHOLD>',
|
|
dest='threshold', help='Threshold to evaluate against.')
|
|
|
|
event_group = parser.add_argument_group('event alarm')
|
|
event_group.add_argument(
|
|
'--event-type', metavar='<EVENT_TYPE>',
|
|
dest='event_type', help='Event type to evaluate against')
|
|
|
|
gnocchi_common_group = parser.add_argument_group(
|
|
'common gnocchi alarm rules')
|
|
gnocchi_common_group.add_argument(
|
|
'--granularity', metavar='<GRANULARITY>',
|
|
dest='granularity',
|
|
help='The time range in seconds over which to query.')
|
|
gnocchi_common_group.add_argument(
|
|
'--aggregation-method', metavar='<AGGR_METHOD>',
|
|
dest='aggregation_method',
|
|
help='The aggregation_method to compare to the threshold.')
|
|
gnocchi_common_group.add_argument(
|
|
'--metric', '--metrics', metavar='<METRIC>', action='append',
|
|
dest='metrics', help='The metric id or name '
|
|
'depending of the alarm type')
|
|
|
|
gnocchi_resource_threshold_group = parser.add_argument_group(
|
|
'gnocchi resource threshold alarm')
|
|
gnocchi_resource_threshold_group.add_argument(
|
|
'--resource-type', metavar='<RESOURCE_TYPE>',
|
|
dest='resource_type', help='The type of resource.')
|
|
gnocchi_resource_threshold_group.add_argument(
|
|
'--resource-id', metavar='<RESOURCE_ID>',
|
|
dest='resource_id', help='The id of a resource.')
|
|
|
|
composite_group = parser.add_argument_group('composite alarm')
|
|
composite_group.add_argument(
|
|
'--composite-rule', metavar='<COMPOSITE_RULE>',
|
|
dest='composite_rule',
|
|
type=jsonutils.loads,
|
|
help='Composite threshold rule with JSON format, the form can '
|
|
'be a nested dict which combine gnocchi rules by '
|
|
'"and", "or". For example, the form is like: '
|
|
'{"or":[RULE1, RULE2, {"and": [RULE3, RULE4]}]}.'
|
|
)
|
|
self.parser = parser
|
|
return parser
|
|
|
|
def validate_time_constraint(self, values_to_convert):
|
|
"""Converts 'a=1;b=2' to {a:1,b:2}."""
|
|
|
|
try:
|
|
return dict((item.strip(" \"'")
|
|
for item in kv.split("=", 1))
|
|
for kv in values_to_convert.split(";"))
|
|
except ValueError:
|
|
msg = ('must be a list of '
|
|
'key1=value1;key2=value2;... not %s'
|
|
% values_to_convert)
|
|
raise argparse.ArgumentTypeError(msg)
|
|
|
|
def _validate_args(self, parsed_args):
|
|
if (parsed_args.type == 'gnocchi_resources_threshold' and
|
|
not (parsed_args.metrics and parsed_args.threshold is not None
|
|
and parsed_args.resource_id and parsed_args.resource_type
|
|
and parsed_args.aggregation_method)):
|
|
self.parser.error('gnocchi_resources_threshold requires --metric, '
|
|
'--threshold, --resource-id, --resource-type '
|
|
'and --aggregation-method')
|
|
elif (parsed_args.type == 'gnocchi_aggregation_by_metrics_threshold'
|
|
and not (parsed_args.metrics
|
|
and parsed_args.threshold is not None
|
|
and parsed_args.aggregation_method)):
|
|
self.parser.error('gnocchi_aggregation_by_metrics_threshold '
|
|
'requires --metric, --threshold and '
|
|
'--aggregation-method')
|
|
elif (parsed_args.type == 'gnocchi_aggregation_by_resources_threshold'
|
|
and not (parsed_args.metrics
|
|
and parsed_args.threshold is not None
|
|
and parsed_args.query and parsed_args.resource_type and
|
|
parsed_args.aggregation_method)):
|
|
self.parser.error('gnocchi_aggregation_by_resources_threshold '
|
|
'requires --metric, --threshold, '
|
|
'--aggregation-method, --query and '
|
|
'--resource-type')
|
|
elif (parsed_args.type == 'composite' and
|
|
not parsed_args.composite_rule):
|
|
self.parser.error('Composite alarm requires'
|
|
' --composite-rule parameter')
|
|
|
|
def _alarm_from_args(self, parsed_args):
|
|
alarm = utils.dict_from_parsed_args(
|
|
parsed_args, ['name', 'type', 'project_id', 'user_id',
|
|
'description', 'state', 'severity', 'enabled',
|
|
'alarm_actions', 'ok_actions',
|
|
'insufficient_data_actions',
|
|
'time_constraints', 'repeat_actions'])
|
|
if parsed_args.type == 'event' and parsed_args.query:
|
|
parsed_args.query = utils.cli_to_array(parsed_args.query)
|
|
alarm['event_rule'] = utils.dict_from_parsed_args(
|
|
parsed_args, ['event_type', 'query'])
|
|
alarm['gnocchi_resources_threshold_rule'] = (
|
|
utils.dict_from_parsed_args(parsed_args,
|
|
['granularity', 'comparison_operator',
|
|
'threshold', 'aggregation_method',
|
|
'evaluation_periods', 'metric',
|
|
'resource_id', 'resource_type']))
|
|
alarm['gnocchi_aggregation_by_metrics_threshold_rule'] = (
|
|
utils.dict_from_parsed_args(parsed_args,
|
|
['granularity', 'comparison_operator',
|
|
'threshold', 'aggregation_method',
|
|
'evaluation_periods', 'metrics']))
|
|
alarm['gnocchi_aggregation_by_resources_threshold_rule'] = (
|
|
utils.dict_from_parsed_args(parsed_args,
|
|
['granularity', 'comparison_operator',
|
|
'threshold', 'aggregation_method',
|
|
'evaluation_periods', 'metric',
|
|
'query', 'resource_type']))
|
|
|
|
alarm['composite_rule'] = parsed_args.composite_rule
|
|
if self.create:
|
|
alarm['type'] = parsed_args.type
|
|
self._validate_args(parsed_args)
|
|
return alarm
|
|
|
|
def take_action(self, parsed_args):
|
|
alarm = utils.get_client(self).alarm.create(
|
|
alarm=self._alarm_from_args(parsed_args))
|
|
return self.dict2columns(_format_alarm(alarm))
|
|
|
|
|
|
class CliAlarmUpdate(CliAlarmCreate):
|
|
"""Update an alarm"""
|
|
|
|
create = False
|
|
|
|
def get_parser(self, prog_name):
|
|
return _add_id_to_parser(
|
|
super(CliAlarmUpdate, self).get_parser(prog_name))
|
|
|
|
def take_action(self, parsed_args):
|
|
attributes = self._alarm_from_args(parsed_args)
|
|
_check_name_and_id_exist(parsed_args, 'update')
|
|
c = utils.get_client(self)
|
|
|
|
if uuidutils.is_uuid_like(parsed_args.id):
|
|
try:
|
|
alarm = c.alarm.update(alarm_id=parsed_args.id,
|
|
alarm_update=attributes)
|
|
except exceptions.NotFound:
|
|
# Maybe it was not an ID but a name, damn
|
|
_id = _find_alarm_id_by_name(c, parsed_args.id)
|
|
else:
|
|
return self.dict2columns(_format_alarm(alarm))
|
|
elif parsed_args.id:
|
|
_id = _find_alarm_id_by_name(c, parsed_args.id)
|
|
else:
|
|
_id = _find_alarm_id_by_name(c, parsed_args.name)
|
|
|
|
alarm = c.alarm.update(alarm_id=_id, alarm_update=attributes)
|
|
return self.dict2columns(_format_alarm(alarm))
|
|
|
|
|
|
class CliAlarmDelete(command.Command):
|
|
"""Delete an alarm"""
|
|
|
|
def get_parser(self, prog_name):
|
|
return _add_name_to_parser(
|
|
_add_id_to_parser(
|
|
super(CliAlarmDelete, self).get_parser(prog_name)))
|
|
|
|
def take_action(self, parsed_args):
|
|
_check_name_and_id(parsed_args, 'delete')
|
|
c = utils.get_client(self)
|
|
|
|
if parsed_args.name:
|
|
_id = _find_alarm_id_by_name(c, parsed_args.name)
|
|
elif uuidutils.is_uuid_like(parsed_args.id):
|
|
try:
|
|
return c.alarm.delete(parsed_args.id)
|
|
except exceptions.NotFound:
|
|
# Maybe it was not an ID after all
|
|
_id = _find_alarm_id_by_name(c, parsed_args.id)
|
|
else:
|
|
_id = _find_alarm_id_by_name(c, parsed_args.id)
|
|
|
|
c.alarm.delete(_id)
|
|
|
|
|
|
class CliAlarmStateGet(show.ShowOne):
|
|
"""Get state of an alarm"""
|
|
|
|
def get_parser(self, prog_name):
|
|
return _add_name_to_parser(
|
|
_add_id_to_parser(
|
|
super(CliAlarmStateGet, self).get_parser(prog_name)))
|
|
|
|
def take_action(self, parsed_args):
|
|
_check_name_and_id(parsed_args, 'get state of')
|
|
c = utils.get_client(self)
|
|
|
|
if parsed_args.name:
|
|
_id = _find_alarm_id_by_name(c, parsed_args.name)
|
|
elif uuidutils.is_uuid_like(parsed_args.id):
|
|
try:
|
|
state = c.alarm.get_state(parsed_args.id)
|
|
except exceptions.NotFound:
|
|
# Maybe it was not an ID after all
|
|
_id = _find_alarm_id_by_name(c, parsed_args.id)
|
|
else:
|
|
return self.dict2columns({'state': state})
|
|
else:
|
|
_id = _find_alarm_id_by_name(c, parsed_args.id)
|
|
|
|
state = c.alarm.get_state(_id)
|
|
return self.dict2columns({'state': state})
|
|
|
|
|
|
class CliAlarmStateSet(show.ShowOne):
|
|
"""Set state of an alarm"""
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = _add_name_to_parser(
|
|
_add_id_to_parser(
|
|
super(CliAlarmStateSet, self).get_parser(prog_name)))
|
|
parser.add_argument('--state', metavar='<STATE>',
|
|
required=True,
|
|
choices=ALARM_STATES,
|
|
help='State of the alarm, one of: '
|
|
+ str(ALARM_STATES))
|
|
return parser
|
|
|
|
def take_action(self, parsed_args):
|
|
_check_name_and_id(parsed_args, 'set state of')
|
|
c = utils.get_client(self)
|
|
|
|
if parsed_args.name:
|
|
_id = _find_alarm_id_by_name(c, parsed_args.name)
|
|
elif uuidutils.is_uuid_like(parsed_args.id):
|
|
try:
|
|
state = c.alarm.set_state(parsed_args.id, parsed_args.state)
|
|
except exceptions.NotFound:
|
|
# Maybe it was not an ID after all
|
|
_id = _find_alarm_id_by_name(c, parsed_args.id)
|
|
else:
|
|
return self.dict2columns({'state': state})
|
|
else:
|
|
_id = _find_alarm_id_by_name(c, parsed_args.id)
|
|
|
|
state = c.alarm.set_state(_id, parsed_args.state)
|
|
return self.dict2columns({'state': state})
|