~ubuntu-branches/ubuntu/vivid/ceilometer/vivid

« back to all changes in this revision

Viewing changes to ceilometer/tests/alarm/evaluator/test_threshold.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2014-03-06 14:44:28 UTC
  • mto: (28.1.1 utopic-proposed) (1.2.1)
  • mto: This revision was merged to the branch mainline in revision 19.
  • Revision ID: package-import@ubuntu.com-20140306144428-rvphsh4igwyulzf0
Tags: upstream-2014.1~b3
ImportĀ upstreamĀ versionĀ 2014.1~b3

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
"""
20
20
import datetime
21
21
import mock
 
22
import pytz
22
23
import uuid
23
24
 
 
25
from six import moves
 
26
 
24
27
from ceilometer.alarm.evaluator import threshold
25
28
from ceilometer.openstack.common import timeutils
26
29
from ceilometer.storage import models
49
52
                         ok_actions=[],
50
53
                         alarm_actions=[],
51
54
                         repeat_actions=False,
 
55
                         time_constraints=[],
52
56
                         rule=dict(
53
57
                             comparison_operator='gt',
54
58
                             threshold=80.0,
77
81
                         alarm_actions=[],
78
82
                         repeat_actions=False,
79
83
                         alarm_id=str(uuid.uuid4()),
 
84
                         time_constraints=[],
80
85
                         rule=dict(
81
86
                             comparison_operator='le',
82
87
                             threshold=10.0,
94
99
        ]
95
100
 
96
101
    @staticmethod
97
 
    def _get_stat(attr, value):
98
 
        return statistics.Statistics(None, {attr: value})
 
102
    def _get_stat(attr, value, count=1):
 
103
        return statistics.Statistics(None, {attr: value, 'count': count})
 
104
 
 
105
    @staticmethod
 
106
    def _reason_data(disposition, count, most_recent):
 
107
        return {'type': 'threshold', 'disposition': disposition,
 
108
                'count': count, 'most_recent': most_recent}
 
109
 
 
110
    def _set_all_rules(self, field, value):
 
111
        for alarm in self.alarms:
 
112
            alarm.rule[field] = value
99
113
 
100
114
    def test_retry_transient_api_failure(self):
101
115
        with mock.patch('ceilometerclient.client.get_client',
102
116
                        return_value=self.api_client):
103
117
            broken = exc.CommunicationError(message='broken')
104
118
            avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] - v)
105
 
                    for v in xrange(5)]
 
119
                    for v in moves.xrange(5)]
106
120
            maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] + v)
107
 
                    for v in xrange(1, 4)]
 
121
                    for v in moves.xrange(1, 4)]
108
122
            self.api_client.statistics.list.side_effect = [broken,
109
123
                                                           broken,
110
124
                                                           avgs,
125
139
                        for alarm in self.alarms]
126
140
            update_calls = self.api_client.alarms.set_state.call_args_list
127
141
            self.assertEqual(update_calls, expected)
128
 
            expected = [mock.call(alarm,
129
 
                                  'ok',
130
 
                                  ('%d datapoints are unknown' %
131
 
                                   alarm.rule['evaluation_periods']))
132
 
                        for alarm in self.alarms]
 
142
            expected = [mock.call(
 
143
                alarm,
 
144
                'ok',
 
145
                ('%d datapoints are unknown'
 
146
                 % alarm.rule['evaluation_periods']),
 
147
                self._reason_data('unknown',
 
148
                                  alarm.rule['evaluation_periods'],
 
149
                                  None))
 
150
                for alarm in self.alarms]
133
151
            self.assertEqual(self.notifier.notify.call_args_list, expected)
134
152
 
135
153
    def test_simple_alarm_trip(self):
137
155
        with mock.patch('ceilometerclient.client.get_client',
138
156
                        return_value=self.api_client):
139
157
            avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] + v)
140
 
                    for v in xrange(1, 6)]
 
158
                    for v in moves.xrange(1, 6)]
141
159
            maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] - v)
142
 
                    for v in xrange(4)]
 
160
                    for v in moves.xrange(4)]
143
161
            self.api_client.statistics.list.side_effect = [avgs, maxs]
144
162
            self._evaluate_all_alarms()
145
163
            self._assert_all_alarms('alarm')
148
166
            update_calls = self.api_client.alarms.set_state.call_args_list
149
167
            self.assertEqual(update_calls, expected)
150
168
            reasons = ['Transition to alarm due to 5 samples outside'
151
 
                       ' threshold, most recent: 85.0',
 
169
                       ' threshold, most recent: %s' % avgs[-1].avg,
152
170
                       'Transition to alarm due to 4 samples outside'
153
 
                       ' threshold, most recent: 7.0']
154
 
            expected = [mock.call(alarm, 'ok', reason)
155
 
                        for alarm, reason in zip(self.alarms, reasons)]
 
171
                       ' threshold, most recent: %s' % maxs[-1].max]
 
172
            reason_datas = [self._reason_data('outside', 5, avgs[-1].avg),
 
173
                            self._reason_data('outside', 4, maxs[-1].max)]
 
174
            expected = [mock.call(alarm, 'ok', reason, reason_data)
 
175
                        for alarm, reason, reason_data
 
176
                        in zip(self.alarms, reasons, reason_datas)]
156
177
            self.assertEqual(self.notifier.notify.call_args_list, expected)
157
178
 
158
179
    def test_simple_alarm_clear(self):
160
181
        with mock.patch('ceilometerclient.client.get_client',
161
182
                        return_value=self.api_client):
162
183
            avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] - v)
163
 
                    for v in xrange(5)]
 
184
                    for v in moves.xrange(5)]
164
185
            maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] + v)
165
 
                    for v in xrange(1, 5)]
 
186
                    for v in moves.xrange(1, 5)]
166
187
            self.api_client.statistics.list.side_effect = [avgs, maxs]
167
188
            self._evaluate_all_alarms()
168
189
            self._assert_all_alarms('ok')
171
192
            update_calls = self.api_client.alarms.set_state.call_args_list
172
193
            self.assertEqual(update_calls, expected)
173
194
            reasons = ['Transition to ok due to 5 samples inside'
174
 
                       ' threshold, most recent: 76.0',
 
195
                       ' threshold, most recent: %s' % avgs[-1].avg,
175
196
                       'Transition to ok due to 4 samples inside'
176
 
                       ' threshold, most recent: 14.0']
177
 
            expected = [mock.call(alarm, 'alarm', reason)
178
 
                        for alarm, reason in zip(self.alarms, reasons)]
 
197
                       ' threshold, most recent: %s' % maxs[-1].max]
 
198
            reason_datas = [self._reason_data('inside', 5, avgs[-1].avg),
 
199
                            self._reason_data('inside', 4, maxs[-1].max)]
 
200
            expected = [mock.call(alarm, 'alarm', reason, reason_data)
 
201
                        for alarm, reason, reason_data
 
202
                        in zip(self.alarms, reasons, reason_datas)]
179
203
            self.assertEqual(self.notifier.notify.call_args_list, expected)
180
204
 
181
205
    def test_equivocal_from_known_state(self):
183
207
        with mock.patch('ceilometerclient.client.get_client',
184
208
                        return_value=self.api_client):
185
209
            avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] + v)
186
 
                    for v in xrange(5)]
 
210
                    for v in moves.xrange(5)]
187
211
            maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] - v)
188
 
                    for v in xrange(-1, 3)]
 
212
                    for v in moves.xrange(-1, 3)]
189
213
            self.api_client.statistics.list.side_effect = [avgs, maxs]
190
214
            self._evaluate_all_alarms()
191
215
            self._assert_all_alarms('ok')
199
223
        with mock.patch('ceilometerclient.client.get_client',
200
224
                        return_value=self.api_client):
201
225
            avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] + v)
202
 
                    for v in xrange(5)]
 
226
                    for v in moves.xrange(5)]
203
227
            maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] - v)
204
 
                    for v in xrange(-1, 3)]
 
228
                    for v in moves.xrange(-1, 3)]
205
229
            self.api_client.statistics.list.side_effect = [avgs, maxs]
206
230
            self._evaluate_all_alarms()
207
231
            self._assert_all_alarms('ok')
209
233
                             [])
210
234
            reason = 'Remaining as ok due to 4 samples inside' \
211
235
                     ' threshold, most recent: 8.0'
212
 
            expected = [mock.call(self.alarms[1], 'ok', reason)]
 
236
            reason_datas = self._reason_data('inside', 4, 8.0)
 
237
            expected = [mock.call(self.alarms[1], 'ok', reason, reason_datas)]
213
238
            self.assertEqual(self.notifier.notify.call_args_list, expected)
214
239
 
215
240
    def test_unequivocal_from_known_state_and_repeat_actions(self):
218
243
        with mock.patch('ceilometerclient.client.get_client',
219
244
                        return_value=self.api_client):
220
245
            avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] + v)
221
 
                    for v in xrange(1, 6)]
 
246
                    for v in moves.xrange(1, 6)]
222
247
            maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] - v)
223
 
                    for v in xrange(4)]
 
248
                    for v in moves.xrange(4)]
224
249
            self.api_client.statistics.list.side_effect = [avgs, maxs]
225
250
            self._evaluate_all_alarms()
226
251
            self._assert_all_alarms('alarm')
228
253
                             [])
229
254
            reason = 'Remaining as alarm due to 4 samples outside' \
230
255
                     ' threshold, most recent: 7.0'
231
 
            expected = [mock.call(self.alarms[1], 'alarm', reason)]
 
256
            reason_datas = self._reason_data('outside', 4, 7.0)
 
257
            expected = [mock.call(self.alarms[1], 'alarm',
 
258
                                  reason, reason_datas)]
232
259
            self.assertEqual(self.notifier.notify.call_args_list, expected)
233
260
 
234
261
    def test_state_change_and_repeat_actions(self):
238
265
        with mock.patch('ceilometerclient.client.get_client',
239
266
                        return_value=self.api_client):
240
267
            avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] + v)
241
 
                    for v in xrange(1, 6)]
 
268
                    for v in moves.xrange(1, 6)]
242
269
            maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] - v)
243
 
                    for v in xrange(4)]
 
270
                    for v in moves.xrange(4)]
244
271
            self.api_client.statistics.list.side_effect = [avgs, maxs]
245
272
            self._evaluate_all_alarms()
246
273
            self._assert_all_alarms('alarm')
249
276
            update_calls = self.api_client.alarms.set_state.call_args_list
250
277
            self.assertEqual(update_calls, expected)
251
278
            reasons = ['Transition to alarm due to 5 samples outside'
252
 
                       ' threshold, most recent: 85.0',
 
279
                       ' threshold, most recent: %s' % avgs[-1].avg,
253
280
                       'Transition to alarm due to 4 samples outside'
254
 
                       ' threshold, most recent: 7.0']
255
 
            expected = [mock.call(alarm, 'ok', reason)
256
 
                        for alarm, reason in zip(self.alarms, reasons)]
 
281
                       ' threshold, most recent: %s' % maxs[-1].max]
 
282
            reason_datas = [self._reason_data('outside', 5, avgs[-1].avg),
 
283
                            self._reason_data('outside', 4, maxs[-1].max)]
 
284
            expected = [mock.call(alarm, 'ok', reason, reason_data)
 
285
                        for alarm, reason, reason_data
 
286
                        in zip(self.alarms, reasons, reason_datas)]
257
287
            self.assertEqual(self.notifier.notify.call_args_list, expected)
258
288
 
259
289
    def test_equivocal_from_unknown(self):
261
291
        with mock.patch('ceilometerclient.client.get_client',
262
292
                        return_value=self.api_client):
263
293
            avgs = [self._get_stat('avg', self.alarms[0].rule['threshold'] + v)
264
 
                    for v in xrange(1, 6)]
 
294
                    for v in moves.xrange(1, 6)]
265
295
            maxs = [self._get_stat('max', self.alarms[1].rule['threshold'] - v)
266
 
                    for v in xrange(4)]
 
296
                    for v in moves.xrange(4)]
267
297
            self.api_client.statistics.list.side_effect = [avgs, maxs]
268
298
            self._evaluate_all_alarms()
269
299
            self._assert_all_alarms('alarm')
272
302
            update_calls = self.api_client.alarms.set_state.call_args_list
273
303
            self.assertEqual(update_calls, expected)
274
304
            reasons = ['Transition to alarm due to 5 samples outside'
275
 
                       ' threshold, most recent: 85.0',
 
305
                       ' threshold, most recent: %s' % avgs[-1].avg,
276
306
                       'Transition to alarm due to 4 samples outside'
277
 
                       ' threshold, most recent: 7.0']
278
 
            expected = [mock.call(alarm, 'insufficient data', reason)
279
 
                        for alarm, reason in zip(self.alarms, reasons)]
 
307
                       ' threshold, most recent: %s' % maxs[-1].max]
 
308
            reason_datas = [self._reason_data('outside', 5, avgs[-1].avg),
 
309
                            self._reason_data('outside', 4, maxs[-1].max)]
 
310
            expected = [mock.call(alarm, 'insufficient data',
 
311
                                  reason, reason_data)
 
312
                        for alarm, reason, reason_data
 
313
                        in zip(self.alarms, reasons, reason_datas)]
280
314
            self.assertEqual(self.notifier.notify.call_args_list, expected)
281
315
 
282
 
    def test_bound_duration(self):
 
316
    def _do_test_bound_duration(self, start, exclude_outliers=None):
 
317
        alarm = self.alarms[0]
 
318
        if exclude_outliers is not None:
 
319
            alarm.rule['exclude_outliers'] = exclude_outliers
283
320
        timeutils.utcnow.override_time = datetime.datetime(2012, 7, 2, 10, 45)
284
 
        constraint = self.evaluator._bound_duration(self.alarms[0], [])
 
321
        constraint = self.evaluator._bound_duration(alarm, [])
285
322
        self.assertEqual(constraint, [
286
323
            {'field': 'timestamp',
287
324
             'op': 'le',
288
325
             'value': timeutils.utcnow().isoformat()},
289
326
            {'field': 'timestamp',
290
327
             'op': 'ge',
291
 
             'value': '2012-07-02T10:39:00'},
 
328
             'value': start},
292
329
        ])
293
330
 
 
331
    def test_bound_duration_outlier_exclusion_defaulted(self):
 
332
        self._do_test_bound_duration('2012-07-02T10:39:00')
 
333
 
 
334
    def test_bound_duration_outlier_exclusion_clear(self):
 
335
        self._do_test_bound_duration('2012-07-02T10:39:00', False)
 
336
 
 
337
    def test_bound_duration_outlier_exclusion_set(self):
 
338
        self._do_test_bound_duration('2012-07-02T10:35:00', True)
 
339
 
294
340
    def test_threshold_endpoint_types(self):
295
341
        endpoint_types = ["internalURL", "publicURL"]
296
342
        for endpoint_type in endpoint_types:
311
357
                                      os_endpoint_type=conf.os_endpoint_type)]
312
358
                actual = client.call_args_list
313
359
                self.assertEqual(actual, expected)
 
360
 
 
361
    def _do_test_simple_alarm_trip_outlier_exclusion(self, exclude_outliers):
 
362
        self._set_all_rules('exclude_outliers', exclude_outliers)
 
363
        self._set_all_alarms('ok')
 
364
        with mock.patch('ceilometerclient.client.get_client',
 
365
                        return_value=self.api_client):
 
366
            # most recent datapoints inside threshold but with
 
367
            # anomolously low sample count
 
368
            threshold = self.alarms[0].rule['threshold']
 
369
            avgs = [self._get_stat('avg',
 
370
                                   threshold + (v if v < 10 else -v),
 
371
                                   count=20 if v < 10 else 1)
 
372
                    for v in xrange(1, 11)]
 
373
            threshold = self.alarms[1].rule['threshold']
 
374
            maxs = [self._get_stat('max',
 
375
                                   threshold - (v if v < 7 else -v),
 
376
                                   count=20 if v < 7 else 1)
 
377
                    for v in xrange(8)]
 
378
            self.api_client.statistics.list.side_effect = [avgs, maxs]
 
379
            self._evaluate_all_alarms()
 
380
            self._assert_all_alarms('alarm' if exclude_outliers else 'ok')
 
381
            if exclude_outliers:
 
382
                expected = [mock.call(alarm.alarm_id, state='alarm')
 
383
                            for alarm in self.alarms]
 
384
                update_calls = self.api_client.alarms.set_state.call_args_list
 
385
                self.assertEqual(update_calls, expected)
 
386
                reasons = ['Transition to alarm due to 5 samples outside'
 
387
                           ' threshold, most recent: %s' % avgs[-2].avg,
 
388
                           'Transition to alarm due to 4 samples outside'
 
389
                           ' threshold, most recent: %s' % maxs[-2].max]
 
390
                reason_datas = [self._reason_data('outside', 5, avgs[-2].avg),
 
391
                                self._reason_data('outside', 4, maxs[-2].max)]
 
392
                expected = [mock.call(alarm, 'ok', reason, reason_data)
 
393
                            for alarm, reason, reason_data
 
394
                            in zip(self.alarms, reasons, reason_datas)]
 
395
                self.assertEqual(self.notifier.notify.call_args_list, expected)
 
396
 
 
397
    def test_simple_alarm_trip_with_outlier_exclusion(self):
 
398
        self. _do_test_simple_alarm_trip_outlier_exclusion(True)
 
399
 
 
400
    def test_simple_alarm_no_trip_without_outlier_exclusion(self):
 
401
        self. _do_test_simple_alarm_trip_outlier_exclusion(False)
 
402
 
 
403
    def _do_test_simple_alarm_clear_outlier_exclusion(self, exclude_outliers):
 
404
        self._set_all_rules('exclude_outliers', exclude_outliers)
 
405
        self._set_all_alarms('alarm')
 
406
        with mock.patch('ceilometerclient.client.get_client',
 
407
                        return_value=self.api_client):
 
408
            # most recent datapoints outside threshold but with
 
409
            # anomolously low sample count
 
410
            threshold = self.alarms[0].rule['threshold']
 
411
            avgs = [self._get_stat('avg',
 
412
                                   threshold - (v if v < 9 else -v),
 
413
                                   count=20 if v < 9 else 1)
 
414
                    for v in xrange(10)]
 
415
            threshold = self.alarms[1].rule['threshold']
 
416
            maxs = [self._get_stat('max',
 
417
                                   threshold + (v if v < 8 else -v),
 
418
                                   count=20 if v < 8 else 1)
 
419
                    for v in xrange(1, 9)]
 
420
            self.api_client.statistics.list.side_effect = [avgs, maxs]
 
421
            self._evaluate_all_alarms()
 
422
            self._assert_all_alarms('ok' if exclude_outliers else 'alarm')
 
423
            if exclude_outliers:
 
424
                expected = [mock.call(alarm.alarm_id, state='ok')
 
425
                            for alarm in self.alarms]
 
426
                update_calls = self.api_client.alarms.set_state.call_args_list
 
427
                self.assertEqual(update_calls, expected)
 
428
                reasons = ['Transition to ok due to 5 samples inside'
 
429
                           ' threshold, most recent: %s' % avgs[-2].avg,
 
430
                           'Transition to ok due to 4 samples inside'
 
431
                           ' threshold, most recent: %s' % maxs[-2].max]
 
432
                reason_datas = [self._reason_data('inside', 5, avgs[-2].avg),
 
433
                                self._reason_data('inside', 4, maxs[-2].max)]
 
434
                expected = [mock.call(alarm, 'alarm', reason, reason_data)
 
435
                            for alarm, reason, reason_data
 
436
                            in zip(self.alarms, reasons, reason_datas)]
 
437
                self.assertEqual(self.notifier.notify.call_args_list, expected)
 
438
 
 
439
    def test_simple_alarm_clear_with_outlier_exclusion(self):
 
440
        self. _do_test_simple_alarm_clear_outlier_exclusion(True)
 
441
 
 
442
    def test_simple_alarm_no_clear_without_outlier_exclusion(self):
 
443
        self. _do_test_simple_alarm_clear_outlier_exclusion(False)
 
444
 
 
445
    def test_state_change_inside_time_constraint(self):
 
446
        self._set_all_alarms('ok')
 
447
        self.alarms[0].time_constraints = [
 
448
            {'name': 'test',
 
449
             'description': 'test',
 
450
             'start': '0 11 * * *',  # daily at 11:00
 
451
             'duration': 10800,  # 3 hours
 
452
             'timezone': 'Europe/Ljubljana'}
 
453
        ]
 
454
        self.alarms[1].time_constraints = self.alarms[0].time_constraints
 
455
        dt = datetime.datetime(2014, 1, 1, 12, 0, 0,
 
456
                               tzinfo=pytz.timezone('Europe/Ljubljana'))
 
457
        with mock.patch('ceilometerclient.client.get_client',
 
458
                        return_value=self.api_client):
 
459
            timeutils.set_time_override(dt.astimezone(pytz.UTC))
 
460
            # the following part based on test_simple_insufficient
 
461
            self.api_client.statistics.list.return_value = []
 
462
            self._evaluate_all_alarms()
 
463
            self._assert_all_alarms('insufficient data')
 
464
            expected = [mock.call(alarm.alarm_id,
 
465
                                  state='insufficient data')
 
466
                        for alarm in self.alarms]
 
467
            update_calls = self.api_client.alarms.set_state.call_args_list
 
468
            self.assertEqual(expected, update_calls,
 
469
                             "Alarm should change state if the current "
 
470
                             "time is inside its time constraint.")
 
471
            expected = [mock.call(
 
472
                alarm,
 
473
                'ok',
 
474
                ('%d datapoints are unknown'
 
475
                 % alarm.rule['evaluation_periods']),
 
476
                self._reason_data('unknown',
 
477
                                  alarm.rule['evaluation_periods'],
 
478
                                  None))
 
479
                for alarm in self.alarms]
 
480
            self.assertEqual(expected, self.notifier.notify.call_args_list)
 
481
 
 
482
    def test_no_state_change_outside_time_constraint(self):
 
483
        self._set_all_alarms('ok')
 
484
        self.alarms[0].time_constraints = [
 
485
            {'name': 'test',
 
486
             'description': 'test',
 
487
             'start': '0 11 * * *',  # daily at 11:00
 
488
             'duration': 10800,  # 3 hours
 
489
             'timezone': 'Europe/Ljubljana'}
 
490
        ]
 
491
        self.alarms[1].time_constraints = self.alarms[0].time_constraints
 
492
        dt = datetime.datetime(2014, 1, 1, 15, 0, 0,
 
493
                               tzinfo=pytz.timezone('Europe/Ljubljana'))
 
494
        with mock.patch('ceilometerclient.client.get_client',
 
495
                        return_value=self.api_client):
 
496
            timeutils.set_time_override(dt.astimezone(pytz.UTC))
 
497
            self.api_client.statistics.list.return_value = []
 
498
            self._evaluate_all_alarms()
 
499
            self._assert_all_alarms('ok')
 
500
            update_calls = self.api_client.alarms.set_state.call_args_list
 
501
            self.assertEqual([], update_calls,
 
502
                             "Alarm should not change state if the current "
 
503
                             " time is outside its time constraint.")
 
504
            self.assertEqual([], self.notifier.notify.call_args_list)