summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ceilometerclient/tests/v2/test_alarms.py81
-rw-r--r--ceilometerclient/tests/v2/test_shell.py98
-rw-r--r--ceilometerclient/v2/alarms.py13
-rw-r--r--ceilometerclient/v2/shell.py78
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={}):