diff options
| -rw-r--r-- | ceilometerclient/tests/v2/test_alarms.py | 81 | ||||
| -rw-r--r-- | ceilometerclient/tests/v2/test_shell.py | 98 | ||||
| -rw-r--r-- | ceilometerclient/v2/alarms.py | 13 | ||||
| -rw-r--r-- | ceilometerclient/v2/shell.py | 78 |
4 files changed, 256 insertions, 14 deletions
diff --git a/ceilometerclient/tests/v2/test_alarms.py b/ceilometerclient/tests/v2/test_alarms.py index f64ded1..0c2fa72 100644 --- a/ceilometerclient/tests/v2/test_alarms.py +++ b/ceilometerclient/tests/v2/test_alarms.py @@ -20,7 +20,7 @@ import copy import testtools from ceilometerclient.tests import utils -import ceilometerclient.v2.alarms +from ceilometerclient.v2 import alarms AN_ALARM = {u'alarm_actions': [u'http://site:8000/alarm'], u'ok_actions': [u'http://site:8000/ok'], @@ -108,6 +108,47 @@ del UPDATE_LEGACY_ALARM['alarm_id'] del UPDATE_LEGACY_ALARM['timestamp'] del UPDATE_LEGACY_ALARM['state_timestamp'] +FULL_DETAIL = ('{"alarm_actions": [], ' + '"user_id": "8185aa72421a4fd396d4122cba50e1b5", ' + '"name": "scombo", ' + '"timestamp": "2013-10-03T08:58:33.647912", ' + '"enabled": true, ' + '"state_timestamp": "2013-10-03T08:58:33.647912", ' + '"rule": {"operator": "or", "alarm_ids": ' + '["062cc907-3a9f-4867-ab3b-fa83212b39f7"]}, ' + '"alarm_id": "alarm-id, ' + '"state": "insufficient data", ' + '"insufficient_data_actions": [], ' + '"repeat_actions": false, ' + '"ok_actions": [], ' + '"project_id": "57d04f24d0824b78b1ea9bcecedbda8f", ' + '"type": "combination", ' + '"description": "Combined state of alarms ' + '062cc907-3a9f-4867-ab3b-fa83212b39f7"}') +ALARM_HISTORY = [{'on_behalf_of': '57d04f24d0824b78b1ea9bcecedbda8f', + 'user_id': '8185aa72421a4fd396d4122cba50e1b5', + 'event_id': 'c74a8611-6553-4764-a860-c15a6aabb5d0', + 'timestamp': '2013-10-03T08:59:28.326000', + 'detail': '{"state": "alarm"}', + 'alarm_id': 'alarm-id', + 'project_id': '57d04f24d0824b78b1ea9bcecedbda8f', + 'type': 'state transition'}, + {'on_behalf_of': '57d04f24d0824b78b1ea9bcecedbda8f', + 'user_id': '8185aa72421a4fd396d4122cba50e1b5', + 'event_id': 'c74a8611-6553-4764-a860-c15a6aabb5d0', + 'timestamp': '2013-10-03T08:59:28.326000', + 'detail': '{"description": "combination of one"}', + 'alarm_id': 'alarm-id', + 'project_id': '57d04f24d0824b78b1ea9bcecedbda8f', + 'type': 'rule change'}, + {'on_behalf_of': '57d04f24d0824b78b1ea9bcecedbda8f', + 'user_id': '8185aa72421a4fd396d4122cba50e1b5', + 'event_id': '4fd7df9e-190d-4471-8884-dc5a33d5d4bb', + 'timestamp': '2013-10-03T08:58:33.647000', + 'detail': FULL_DETAIL, + 'alarm_id': 'alarm-id', + 'project_id': '57d04f24d0824b78b1ea9bcecedbda8f', + 'type': 'creation'}] fixtures = { '/v2/alarms': @@ -159,6 +200,20 @@ fixtures = { None, ), }, + '/v2/alarms/alarm-id/history': + { + 'GET': ( + {}, + ALARM_HISTORY, + ), + }, + '/v2/alarms/alarm-id/history?q.op=&q.value=NOW&q.field=timestamp': + { + 'GET': ( + {}, + ALARM_HISTORY, + ), + }, } @@ -167,7 +222,7 @@ class AlarmManagerTest(testtools.TestCase): def setUp(self): super(AlarmManagerTest, self).setUp() self.api = utils.FakeAPI(fixtures) - self.mgr = ceilometerclient.v2.alarms.AlarmManager(self.api) + self.mgr = alarms.AlarmManager(self.api) def test_list_all(self): alarms = list(self.mgr.list()) @@ -261,13 +316,33 @@ class AlarmManagerTest(testtools.TestCase): self.assertEqual(self.api.calls, expect) self.assertTrue(deleted is None) + def _do_test_get_history(self, q, url): + history = self.mgr.get_history(q=q, alarm_id='alarm-id') + expect = [('GET', url, {}, None)] + self.assertEqual(self.api.calls, expect) + for i in xrange(len(history)): + change = history[i] + self.assertTrue(isinstance(change, alarms.AlarmChange)) + for k, v in ALARM_HISTORY[i].iteritems(): + self.assertEqual(getattr(change, k), v) + + def test_get_all_history(self): + url = '/v2/alarms/alarm-id/history' + self._do_test_get_history(None, url) + + def test_get_constrained_history(self): + q = [dict(field='timestamp', value='NOW')] + url = ('/v2/alarms/alarm-id/history' + '?q.op=&q.value=NOW&q.field=timestamp') + self._do_test_get_history(q, url) + class AlarmLegacyManagerTest(testtools.TestCase): def setUp(self): super(AlarmLegacyManagerTest, self).setUp() self.api = utils.FakeAPI(fixtures) - self.mgr = ceilometerclient.v2.alarms.AlarmManager(self.api) + self.mgr = alarms.AlarmManager(self.api) def test_create(self): alarm = self.mgr.create(**CREATE_LEGACY_ALARM) diff --git a/ceilometerclient/tests/v2/test_shell.py b/ceilometerclient/tests/v2/test_shell.py index 6d5da90..402343b 100644 --- a/ceilometerclient/tests/v2/test_shell.py +++ b/ceilometerclient/tests/v2/test_shell.py @@ -10,9 +10,15 @@ # License for the specific language governing permissions and limitations # under the License. +import cStringIO import mock +import re +import sys + +from testtools import matchers from ceilometerclient.tests import utils +from ceilometerclient.v2 import alarms from ceilometerclient.v2 import shell as ceilometer_shell @@ -37,3 +43,95 @@ class ShellAlarmStateCommandsTest(utils.BaseTestCase): ceilometer_shell.do_alarm_state_set(self.cc, self.args) self.cc.alarms.set_state.assert_called_once_with(self.ALARM_ID, 'ok') self.assertFalse(self.cc.alarms.get_state.called) + + +class ShellAlarmHistoryCommandTest(utils.BaseTestCase): + + ALARM_ID = '768ff714-8cfb-4db9-9753-d484cb33a1cc' + FULL_DETAIL = ('{"alarm_actions": [], ' + '"user_id": "8185aa72421a4fd396d4122cba50e1b5", ' + '"name": "scombo", ' + '"timestamp": "2013-10-03T08:58:33.647912", ' + '"enabled": true, ' + '"state_timestamp": "2013-10-03T08:58:33.647912", ' + '"rule": {"operator": "or", "alarm_ids": ' + '["062cc907-3a9f-4867-ab3b-fa83212b39f7"]}, ' + '"alarm_id": "768ff714-8cfb-4db9-9753-d484cb33a1cc", ' + '"state": "insufficient data", ' + '"insufficient_data_actions": [], ' + '"repeat_actions": false, ' + '"ok_actions": [], ' + '"project_id": "57d04f24d0824b78b1ea9bcecedbda8f", ' + '"type": "combination", ' + '"description": "Combined state of alarms ' + '062cc907-3a9f-4867-ab3b-fa83212b39f7"}') + ALARM_HISTORY = [{'on_behalf_of': '57d04f24d0824b78b1ea9bcecedbda8f', + 'user_id': '8185aa72421a4fd396d4122cba50e1b5', + 'event_id': 'c74a8611-6553-4764-a860-c15a6aabb5d0', + 'timestamp': '2013-10-03T08:59:28.326000', + 'detail': '{"state": "alarm"}', + 'alarm_id': '768ff714-8cfb-4db9-9753-d484cb33a1cc', + 'project_id': '57d04f24d0824b78b1ea9bcecedbda8f', + 'type': 'state transition'}, + {'on_behalf_of': '57d04f24d0824b78b1ea9bcecedbda8f', + 'user_id': '8185aa72421a4fd396d4122cba50e1b5', + 'event_id': 'c74a8611-6553-4764-a860-c15a6aabb5d0', + 'timestamp': '2013-10-03T08:59:28.326000', + 'detail': '{"description": "combination of one"}', + 'alarm_id': '768ff714-8cfb-4db9-9753-d484cb33a1cc', + 'project_id': '57d04f24d0824b78b1ea9bcecedbda8f', + 'type': 'rule change'}, + {'on_behalf_of': '57d04f24d0824b78b1ea9bcecedbda8f', + 'user_id': '8185aa72421a4fd396d4122cba50e1b5', + 'event_id': '4fd7df9e-190d-4471-8884-dc5a33d5d4bb', + 'timestamp': '2013-10-03T08:58:33.647000', + 'detail': FULL_DETAIL, + 'alarm_id': '768ff714-8cfb-4db9-9753-d484cb33a1cc', + 'project_id': '57d04f24d0824b78b1ea9bcecedbda8f', + 'type': 'creation'}] + TIMESTAMP_RE = (' +\| (\d{4})-(\d{2})-(\d{2})T' + '(\d{2})\:(\d{2})\:(\d{2})\.(\d{6}) \| +') + + def setUp(self): + super(ShellAlarmHistoryCommandTest, self).setUp() + self.cc = mock.Mock() + self.cc.alarms = mock.Mock() + self.args = mock.Mock() + self.args.alarm_id = self.ALARM_ID + + def _do_test_alarm_history(self, raw_query=None, parsed_query=None): + self.args.query = raw_query + orig = sys.stdout + sys.stdout = cStringIO.StringIO() + history = [alarms.AlarmChange(mock.Mock(), change) + for change in self.ALARM_HISTORY] + self.cc.alarms.get_history.return_value = history + + try: + ceilometer_shell.do_alarm_history(self.cc, self.args) + self.cc.alarms.get_history.assert_called_once_with( + q=parsed_query, + alarm_id=self.ALARM_ID + ) + out = sys.stdout.getvalue() + required = [ + '.*creation%sname: scombo.*' % self.TIMESTAMP_RE, + '.*rule change%sdescription: combination of one.*' % + self.TIMESTAMP_RE, + '.*state transition%sstate: alarm.*' % self.TIMESTAMP_RE, + ] + for r in required: + self.assertThat(out, matchers.MatchesRegex(r, re.DOTALL)) + finally: + sys.stdout.close() + sys.stdout = orig + + def test_alarm_all_history(self): + self._do_test_alarm_history() + + def test_alarm_constrained_history(self): + parsed_query = [dict(field='timestamp', + value='2013-10-03T08:59:28', + op='gt')] + self._do_test_alarm_history(raw_query='timestamp>2013-10-03T08:59:28', + parsed_query=parsed_query) diff --git a/ceilometerclient/v2/alarms.py b/ceilometerclient/v2/alarms.py index 71cc0ac..42f398e 100644 --- a/ceilometerclient/v2/alarms.py +++ b/ceilometerclient/v2/alarms.py @@ -51,6 +51,14 @@ class Alarm(base.Resource): return super(Alarm, self).__getattr__(k) +class AlarmChange(base.Resource): + def __repr__(self): + return "<AlarmChange %s>" % self._info + + def __getattr__(self, k): + return super(AlarmChange, self).__getattr__(k) + + class AlarmManager(base.Manager): resource_class = Alarm @@ -130,3 +138,8 @@ class AlarmManager(base.Manager): resp, body = self.api.json_request('GET', "%s/state" % self._path(alarm_id)) return body + + def get_history(self, alarm_id, q=None): + path = '%s/history' % self._path(alarm_id) + url = options.build_url(path, q) + return self._list(url, obj_class=AlarmChange) diff --git a/ceilometerclient/v2/shell.py b/ceilometerclient/v2/shell.py index e90be02..24b15b9 100644 --- a/ceilometerclient/v2/shell.py +++ b/ceilometerclient/v2/shell.py @@ -127,26 +127,63 @@ def do_meter_list(cc, args={}): sortby=0) -def alarm_rule_formatter(alarm): - if alarm.type == 'threshold': +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': alarm.rule['meter_name'], - 'threshold': alarm.rule['threshold'], - 'evaluation_periods': alarm.rule['evaluation_periods'], - 'period': alarm.rule['period'], + 'meter_name': rule['meter_name'], + 'threshold': rule['threshold'], + 'evaluation_periods': rule['evaluation_periods'], + 'period': rule['period'], 'comparison_operator': OPERATORS_STRING.get( - alarm.rule['comparison_operator']) + rule['comparison_operator']) }) - elif alarm.type == 'combination': + elif type == 'combination': return ('combinated states (%(operator)s) of %(alarms)s' % { - 'operator': alarm.rule['operator'].upper(), - 'alarms': ", ".join(alarm.rule['alarm_ids'])}) + 'operator': rule['operator'].upper(), + 'alarms': ", ".join(rule['alarm_ids'])}) else: # just dump all return "\n".join(["%s: %s" % (f, v) - for f, v in alarm.rule.iteritems()]) + for f, v in rule.iteritems()]) + + +def alarm_rule_formatter(alarm): + return _display_rule(alarm.type, alarm.rule) + + +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])) + elif change.type == 'rule change': + for k, v in detail.iteritems(): + 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='<QUERY>', @@ -444,6 +481,25 @@ def do_alarm_state_get(cc, args={}): utils.print_dict({'state': state}, wrap=72) +@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>', required=True, + help='ID of the alarm for which history is shown.') +@utils.arg('-q', '--query', metavar='<QUERY>', + help='key[op]value; list.') +def do_alarm_history(cc, args={}): + '''Display the change history of an alarm.''' + kwargs = dict(alarm_id=args.alarm_id, + q=options.cli_to_array(args.query)) + try: + history = cc.alarms.get_history(**kwargs) + except exc.HTTPNotFound: + raise exc.CommandError('Alarm not found: %s' % args.alarm_id) + field_labels = ['Type', 'Timestamp', 'Detail'] + fields = ['type', 'timestamp', 'detail'] + utils.print_list(history, fields, field_labels, + formatters={'detail': alarm_change_detail_formatter}, + sortby=1) + + @utils.arg('-q', '--query', metavar='<QUERY>', help='key[op]value; list.') def do_resource_list(cc, args={}): |
