From 65b3c0ee2bd10ed4fe689db558f11f704b3a5211 Mon Sep 17 00:00:00 2001 From: gordon chung Date: Tue, 5 Jan 2016 14:59:50 -0500 Subject: [PATCH] add alarm interface this adds functionality to support ceilometer+aodh. it supports: - aodh alarm list - aodh alarm show - aodh alarm search - aodh alarm create - aodh alarm update - aodh alarm delete Change-Id: I50a3578fed543185094e0ae545a09ca72039372e --- aodhclient/shell.py | 7 + aodhclient/tests/functional/test_alarm.py | 112 ++++++++++++ aodhclient/v2/alarm.py | 80 +++++++++ aodhclient/v2/alarm_cli.py | 202 ++++++++++++++++++++++ aodhclient/v2/client.py | 2 + 5 files changed, 403 insertions(+) create mode 100644 aodhclient/tests/functional/test_alarm.py create mode 100644 aodhclient/v2/alarm.py create mode 100644 aodhclient/v2/alarm_cli.py diff --git a/aodhclient/shell.py b/aodhclient/shell.py index d10f7ce..ea4e7dd 100644 --- a/aodhclient/shell.py +++ b/aodhclient/shell.py @@ -25,12 +25,19 @@ from keystoneauth1 import loading from aodhclient import client from aodhclient import noauth +from aodhclient.v2 import alarm_cli from aodhclient.v2 import capabilities_cli from aodhclient.version import __version__ class AodhCommandManager(commandmanager.CommandManager): SHELL_COMMANDS = { + "alarm create": alarm_cli.CliAlarmCreate, + "alarm delete": alarm_cli.CliAlarmDelete, + "alarm list": alarm_cli.CliAlarmList, + "alarm show": alarm_cli.CliAlarmShow, + "alarm search": alarm_cli.CliAlarmSearch, + "alarm update": alarm_cli.CliAlarmUpdate, "capabilities list": capabilities_cli.CliCapabilitiesList, } diff --git a/aodhclient/tests/functional/test_alarm.py b/aodhclient/tests/functional/test_alarm.py new file mode 100644 index 0000000..1b0f12c --- /dev/null +++ b/aodhclient/tests/functional/test_alarm.py @@ -0,0 +1,112 @@ +# 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 uuid + +from aodhclient.tests.functional import base + + +class AodhClientTest(base.ClientTestBase): + + def test_help(self): + self.aodh("help", params="alarm create") + self.aodh("help", params="alarm delete") + self.aodh("help", params="alarm list") + self.aodh("help", params="alarm search") + self.aodh("help", params="alarm show") + self.aodh("help", params="alarm update") + + def test_alarm_scenario(self): + + PROJECT_ID = str(uuid.uuid4()) + + # CREATE + result = self.aodh(u'alarm', + params=(u"create --name alarm1 -m meter_name " + " --threshold 5 " + "--project-id %s" % PROJECT_ID)) + alarm = self.details_multiple(result)[0] + ALARM_ID = alarm['alarm_id'] + self.assertEqual('alarm1', alarm['name']) + self.assertEqual('meter_name', alarm['meter_name']) + self.assertEqual('5.0', alarm['threshold']) + + # CREATE FAIL + result = self.aodh(u'alarm', + params=(u"create --name alarm1 -m meter_name " + " --threshold 5 " + "--project-id %s" % PROJECT_ID), + fail_ok=True, merge_stderr=True) + self.assertEqual(result.strip(), 'Conflict (HTTP 409)') + + # UPDATE + result = self.aodh( + 'alarm', params=("update %s --severity critical --threshold 10" + % ALARM_ID)) + alarm_updated = self.details_multiple(result)[0] + self.assertEqual(ALARM_ID, alarm_updated["alarm_id"]) + self.assertEqual('critical', alarm_updated['severity']) + self.assertEqual('10.0', alarm_updated["threshold"]) + + # GET + result = self.aodh( + 'alarm', params="show %s" % ALARM_ID) + alarm_show = self.details_multiple(result)[0] + self.assertEqual(ALARM_ID, alarm_show["alarm_id"]) + self.assertEqual(PROJECT_ID, alarm_show["project_id"]) + self.assertEqual('alarm1', alarm_show['name']) + self.assertEqual('meter_name', alarm_show['meter_name']) + self.assertEqual('10.0', alarm_show['threshold']) + + # LIST + result = self.aodh('alarm', params="list") + self.assertIn(ALARM_ID, + [r['alarm_id'] for r in self.parser.listing(result)]) + for alarm_list in self.parser.listing(result): + if alarm_list["alarm_id"] == ALARM_ID: + self.assertEqual('alarm1', alarm_list['name']) + + # SEARCH ALL + result = self.aodh('alarm', params=("search")) + self.assertIn(ALARM_ID, + [r['alarm_id'] for r in self.parser.listing(result)]) + for alarm_list in self.parser.listing(result): + if alarm_list["alarm_id"] == ALARM_ID: + self.assertEqual('alarm1', alarm_list['name']) + + # SEARCH SOME + result = self.aodh('alarm', + params=("search --query " + "'{\"=\": {\"project_id\": \"%s\"}}'" + % PROJECT_ID)) + alarm_list = self.parser.listing(result)[0] + self.assertEqual(ALARM_ID, alarm_list["alarm_id"]) + self.assertEqual('alarm1', alarm_list['name']) + + # DELETE + result = self.aodh('alarm', params="delete %s" % ALARM_ID) + self.assertEqual("", result) + + # GET FAIL + result = self.aodh('alarm', params="show %s" % ALARM_ID, + fail_ok=True, merge_stderr=True) + self.assertEqual(result.strip(), "Not found (HTTP 404)") + + # DELETE FAIL + result = self.aodh('alarm', params="delete %s" % ALARM_ID, + fail_ok=True, merge_stderr=True) + self.assertEqual(result.strip(), "Not found (HTTP 404)") + + # LIST DOES NOT HAVE ALARM + result = self.aodh('alarm', params="list") + self.assertNotIn(ALARM_ID, + [r['alarm_id'] for r in self.parser.listing(result)]) diff --git a/aodhclient/v2/alarm.py b/aodhclient/v2/alarm.py new file mode 100644 index 0000000..44417e1 --- /dev/null +++ b/aodhclient/v2/alarm.py @@ -0,0 +1,80 @@ +# +# 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. + +from oslo_serialization import jsonutils + +from aodhclient.v2 import base + + +class AlarmManager(base.Manager): + + url = "v2/alarms" + + def list(self): + """List alarms""" + return self._get(self.url).json() + + def get(self, alarm_id): + """Get an alarm + + :param alarm_id: ID of the alarm + :type alarm_id: str + """ + return self._get(self.url + '/' + alarm_id).json() + + def create(self, alarm): + """Create an alarm + + :param alarm: the alarm + :type alarm: dict + """ + return self._post( + self.url, headers={'Content-Type': "application/json"}, + data=jsonutils.dumps(alarm)).json() + + def update(self, alarm_id, alarm_update): + """Update an alarm + + :param alarm_id: ID of the alarm + :type alarm_id: str + :param attributes: Attributes of the alarm + :type attributes: dict + """ + alarm = self._get(self.url + '/' + alarm_id).json() + if alarm_update.get('threshold_rule'): + alarm['threshold_rule'].update(alarm_update.get('threshold_rule')) + alarm_update.pop('threshold_rule') + alarm.update(alarm_update) + return self._put( + self.url + '/' + alarm_id, + headers={'Content-Type': "application/json"}, + data=jsonutils.dumps(alarm)).json() + + def delete(self, alarm_id): + """Delete an alarm + + :param alarm_id: ID of the alarm + :type alarm_id: str + """ + self._delete(self.url + '/' + alarm_id) + + def search(self, query=None): + """List alarms + + :param query: The query dictionary + :type query: dict + """ + query = {'filter': query} if query else {} + url = "v2/query/alarms" + return self._post(url, headers={'Content-Type': "application/json"}, + data=jsonutils.dumps(query)).json() diff --git a/aodhclient/v2/alarm_cli.py b/aodhclient/v2/alarm_cli.py new file mode 100644 index 0000000..9279a16 --- /dev/null +++ b/aodhclient/v2/alarm_cli.py @@ -0,0 +1,202 @@ +# +# 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. +from cliff import command +from cliff import lister +from cliff import show +from oslo_utils import strutils + +from aodhclient import utils + + +ALARM_STATES = ['ok', 'alarm', 'insufficient data'] +ALARM_SEVERITY = ['low', 'moderate', 'critical'] +ALARM_OPERATORS = ['lt', 'le', 'eq', 'ne', 'ge', 'gt'] +STATISTICS = ['max', 'min', 'avg', 'sum', 'count'] + + +class CliAlarmList(lister.Lister): + """List alarms""" + + COLS = ('alarm_id', 'name', 'state', 'severity', 'enabled', + 'repeat_actions', 'threshold_rule', 'time_constraints') + + def take_action(self, parsed_args): + alarms = self.app.client.alarm.list() + return utils.list2cols(self.COLS, alarms) + + +class CliAlarmSearch(CliAlarmList): + """Search alarms with specified query rules""" + + def get_parser(self, prog_name): + parser = super(CliAlarmSearch, self).get_parser(prog_name) + parser.add_argument("--query", help="Query"), + return parser + + def take_action(self, parsed_args): + alarms = self.app.client.alarm.search(query=parsed_args.query) + return utils.list2cols(self.COLS, alarms) + + +def _format_alarm(alarm): + alarm.update(alarm.pop('threshold_rule')) + return alarm + + +class CliAlarmShow(show.ShowOne): + """Show an alarm""" + + def get_parser(self, prog_name): + parser = super(CliAlarmShow, self).get_parser(prog_name) + parser.add_argument("alarm_id", help="ID of an alarm") + return parser + + def take_action(self, parsed_args): + alarm = self.app.client.alarm.get(alarm_id=parsed_args.alarm_id) + return self.dict2columns(_format_alarm(alarm)) + + +class CliAlarmCreate(show.ShowOne): + """Create an alarm""" + + create = True + + def get_parser(self, prog_name): + parser = super(CliAlarmCreate, self).get_parser(prog_name) + parser.add_argument('--name', metavar='', required=self.create, + help='Name of the alarm') + parser.add_argument('--project-id', metavar='', + help='Project to associate with alarm ' + '(configurable by admin users only)') + parser.add_argument('--user-id', metavar='', + help='User to associate with alarm ' + '(configurable by admin users only)') + parser.add_argument('--description', metavar='', + help='Free text description of the alarm') + parser.add_argument('--state', metavar='', + help='State of the alarm, one of: ' + + str(ALARM_STATES)) + parser.add_argument('--severity', metavar='', + 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/actioning is ' + 'enabled')) + parser.add_argument('--alarm-action', dest='alarm_actions', + metavar='', 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='', 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='', 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='