Support for playback of old events.
Introduces the time_sync object which can sync time with an external time service (like the time_sync service in notigen). This is used for playback of old events. It prevents premature expiry triggers. Also some tweaks to the UsageHandler, work in progress and used for testing. Change-Id: I45033fbd9c12d98f96816a4f90cf7dc8c915ef51
This commit is contained in:
parent
ac49955386
commit
e0256a3fdb
@ -17,7 +17,7 @@
|
|||||||
# memory_mb:
|
# memory_mb:
|
||||||
# numeric: "> 4096"
|
# numeric: "> 4096"
|
||||||
- event_type: compute.instance.exists
|
- event_type: compute.instance.exists
|
||||||
map_distingushed_trait:
|
map_distingushed_by:
|
||||||
timestamp: audit_period_beginning
|
timestamp: audit_period_beginning
|
||||||
fire_criteria:
|
fire_criteria:
|
||||||
- event_type: compute.instance.exists
|
- event_type: compute.instance.exists
|
||||||
|
@ -6,3 +6,5 @@ MySQL-python
|
|||||||
alembic>=0.4.1
|
alembic>=0.4.1
|
||||||
enum34>=1.0
|
enum34>=1.0
|
||||||
SQLAlchemy>=0.9.6
|
SQLAlchemy>=0.9.6
|
||||||
|
python-dateutil
|
||||||
|
requests
|
||||||
|
@ -15,6 +15,8 @@ class TestPipeline(unittest.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestPipeline, self).setUp()
|
super(TestPipeline, self).setUp()
|
||||||
self.debugger = debugging.NoOpDebugger()
|
self.debugger = debugging.NoOpDebugger()
|
||||||
|
self.fake_stream = mock.MagicMock(name="fake_stream")
|
||||||
|
self.fake_stream.id = "stream-1234"
|
||||||
|
|
||||||
def test_check_handler_config(self):
|
def test_check_handler_config(self):
|
||||||
|
|
||||||
@ -82,7 +84,7 @@ class TestPipeline(unittest.TestCase):
|
|||||||
p.commit = mock.MagicMock(name='commit')
|
p.commit = mock.MagicMock(name='commit')
|
||||||
p.rollback = mock.MagicMock(name='rollback')
|
p.rollback = mock.MagicMock(name='rollback')
|
||||||
|
|
||||||
ret = p.handle_events(test_events, self.debugger)
|
ret = p.handle_events(test_events, self.fake_stream, self.debugger)
|
||||||
handler_class1.return_value.handle_events.assert_called_once_with(test_events, p.env)
|
handler_class1.return_value.handle_events.assert_called_once_with(test_events, p.env)
|
||||||
events1 = handler_class1.return_value.handle_events.return_value
|
events1 = handler_class1.return_value.handle_events.return_value
|
||||||
handler_class2.return_value.handle_events.assert_called_once_with(events1, p.env)
|
handler_class2.return_value.handle_events.assert_called_once_with(events1, p.env)
|
||||||
@ -116,7 +118,7 @@ class TestPipeline(unittest.TestCase):
|
|||||||
p.rollback = mock.MagicMock(name='rollback')
|
p.rollback = mock.MagicMock(name='rollback')
|
||||||
|
|
||||||
with self.assertRaises(pipeline_manager.PipelineExecutionError):
|
with self.assertRaises(pipeline_manager.PipelineExecutionError):
|
||||||
p.handle_events(test_events, self.debugger)
|
p.handle_events(test_events, self.fake_stream, self.debugger)
|
||||||
p.rollback.assert_called_once_with(self.debugger)
|
p.rollback.assert_called_once_with(self.debugger)
|
||||||
self.assertFalse(p.commit.called)
|
self.assertFalse(p.commit.called)
|
||||||
|
|
||||||
@ -261,7 +263,7 @@ class TestPipelineManager(unittest.TestCase):
|
|||||||
|
|
||||||
pipeline = mock_pipeline.return_value
|
pipeline = mock_pipeline.return_value
|
||||||
pipeline.handle_events.assert_called_once_with(
|
pipeline.handle_events.assert_called_once_with(
|
||||||
pm.db.get_stream_events.return_value, self.debugger)
|
pm.db.get_stream_events.return_value, stream, self.debugger)
|
||||||
pm.add_new_events.assert_called_once_with(
|
pm.add_new_events.assert_called_once_with(
|
||||||
mock_pipeline.return_value.handle_events.return_value)
|
mock_pipeline.return_value.handle_events.return_value)
|
||||||
self.assertTrue(ret)
|
self.assertTrue(ret)
|
||||||
@ -291,7 +293,7 @@ class TestPipelineManager(unittest.TestCase):
|
|||||||
pm.pipeline_handlers)
|
pm.pipeline_handlers)
|
||||||
|
|
||||||
pipeline.handle_events.assert_called_once_with(
|
pipeline.handle_events.assert_called_once_with(
|
||||||
pm.db.get_stream_events.return_value, self.debugger)
|
pm.db.get_stream_events.return_value, stream, self.debugger)
|
||||||
self.assertFalse(pm.add_new_events.called)
|
self.assertFalse(pm.add_new_events.called)
|
||||||
self.assertFalse(ret)
|
self.assertFalse(ret)
|
||||||
|
|
||||||
|
132
tests/test_time_sync.py
Normal file
132
tests/test_time_sync.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import unittest2 as unittest
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from winchester import time_sync
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimeSyncNoEndpoint(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestTimeSyncNoEndpoint, self).setUp()
|
||||||
|
self.time_sync = time_sync.TimeSync({})
|
||||||
|
|
||||||
|
def test_should_update(self):
|
||||||
|
now = datetime.datetime.utcnow()
|
||||||
|
self.assertIsNone(self.time_sync.last_update)
|
||||||
|
self.assertTrue(self.time_sync._should_update(now))
|
||||||
|
|
||||||
|
short = now + datetime.timedelta(seconds=1)
|
||||||
|
lng = now + datetime.timedelta(seconds=60)
|
||||||
|
self.time_sync.last_update = now
|
||||||
|
|
||||||
|
self.assertFalse(self.time_sync._should_update(short))
|
||||||
|
self.assertTrue(self.time_sync._should_update(lng))
|
||||||
|
|
||||||
|
def test_current_time(self):
|
||||||
|
with mock.patch.object(self.time_sync, "_get_now") as w:
|
||||||
|
w.return_value = "123"
|
||||||
|
self.assertEqual(self.time_sync.current_time(), "123")
|
||||||
|
|
||||||
|
def test_publish(self):
|
||||||
|
with mock.patch.object(time_sync.dateutil.parser, "parse") as p:
|
||||||
|
self.time_sync.publish("foo")
|
||||||
|
self.assertEquals(0, p.call_count)
|
||||||
|
|
||||||
|
|
||||||
|
class BlowUp(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimeSyncEndpointPublisher(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestTimeSyncEndpointPublisher, self).setUp()
|
||||||
|
self.time_sync = time_sync.TimeSync(
|
||||||
|
{"time_sync_endpoint":"example.com"}, publishes=True)
|
||||||
|
|
||||||
|
def test_fetch_good(self):
|
||||||
|
with mock.patch.object(time_sync.requests, "get") as r:
|
||||||
|
response = mock.MagicMock()
|
||||||
|
response.text = "now"
|
||||||
|
r.return_value = response
|
||||||
|
self.assertEquals("now", self.time_sync._fetch())
|
||||||
|
|
||||||
|
def test_fetch_empty(self):
|
||||||
|
with mock.patch.object(time_sync.time, "sleep") as t:
|
||||||
|
with mock.patch.object(time_sync.requests, "get") as r:
|
||||||
|
response = mock.MagicMock()
|
||||||
|
response.text = ""
|
||||||
|
r.return_value = response
|
||||||
|
t.side_effect = BlowUp
|
||||||
|
with self.assertRaises(BlowUp):
|
||||||
|
self.time_sync._fetch()
|
||||||
|
|
||||||
|
def test_fetch_None(self):
|
||||||
|
with mock.patch.object(time_sync.time, "sleep") as t:
|
||||||
|
with mock.patch.object(time_sync.requests, "get") as r:
|
||||||
|
response = mock.MagicMock()
|
||||||
|
response.text = "None"
|
||||||
|
r.return_value = response
|
||||||
|
t.side_effect = BlowUp
|
||||||
|
with self.assertRaises(BlowUp):
|
||||||
|
self.time_sync._fetch()
|
||||||
|
|
||||||
|
def test_current_time(self):
|
||||||
|
self.time_sync.last_tyme = "now"
|
||||||
|
with mock.patch.object(self.time_sync, "_should_update") as u:
|
||||||
|
self.assertEquals("now", self.time_sync.current_time())
|
||||||
|
self.assertEquals(0, u.call_count)
|
||||||
|
|
||||||
|
def test_publish(self):
|
||||||
|
with mock.patch.object(time_sync.dateutil.parser, "parse") as p:
|
||||||
|
p.return_value = "datetime object"
|
||||||
|
with mock.patch.object(self.time_sync, "_should_update") as u:
|
||||||
|
u.return_value = True
|
||||||
|
|
||||||
|
with mock.patch.object(time_sync.requests, "post") as r:
|
||||||
|
r.return_value = ""
|
||||||
|
|
||||||
|
self.time_sync.publish("string datetime")
|
||||||
|
|
||||||
|
r.assert_called_once_with("example.com/time",
|
||||||
|
data="string datetime")
|
||||||
|
|
||||||
|
def test_publish_fails(self):
|
||||||
|
with mock.patch.object(time_sync.dateutil.parser, "parse") as p:
|
||||||
|
p.return_value = "datetime object"
|
||||||
|
with mock.patch.object(self.time_sync, "_should_update") as u:
|
||||||
|
u.return_value = True
|
||||||
|
with mock.patch.object(time_sync.requests, "post") as r:
|
||||||
|
r.side_effect = BlowUp
|
||||||
|
with mock.patch.object(time_sync.logger, "exception") as e:
|
||||||
|
self.time_sync.publish("string datetime")
|
||||||
|
self.assertEquals(1, e.call_count)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimeSyncEndpointConsumer(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestTimeSyncEndpointConsumer, self).setUp()
|
||||||
|
self.time_sync = time_sync.TimeSync(
|
||||||
|
{"time_sync_endpoint":"example.com"})
|
||||||
|
|
||||||
|
def test_current_time(self):
|
||||||
|
with mock.patch.object(self.time_sync, "_should_update") as u:
|
||||||
|
u.return_value = True
|
||||||
|
with mock.patch.object(time_sync.dateutil.parser, "parse") as p:
|
||||||
|
p.return_value = "datetime object"
|
||||||
|
with mock.patch.object(self.time_sync, "_fetch") as r:
|
||||||
|
r.return_value = "string datetime"
|
||||||
|
|
||||||
|
self.assertEquals(self.time_sync.current_time(),
|
||||||
|
"datetime object")
|
||||||
|
|
||||||
|
def test_current_time_fails(self):
|
||||||
|
self.time_sync.last_tyme = "no change"
|
||||||
|
with mock.patch.object(self.time_sync, "_should_update") as u:
|
||||||
|
u.return_value = True
|
||||||
|
with mock.patch.object(self.time_sync, "_fetch") as r:
|
||||||
|
r.side_effect = BlowUp
|
||||||
|
with mock.patch.object(time_sync.logger, "exception") as e:
|
||||||
|
self.assertEquals(self.time_sync.current_time(),
|
||||||
|
"no change")
|
||||||
|
self.assertEquals(1, e.call_count)
|
200
tests/test_usage_handler.py
Normal file
200
tests/test_usage_handler.py
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
import unittest2 as unittest
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from winchester import pipeline_handler
|
||||||
|
|
||||||
|
|
||||||
|
class TestUsageHandler(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestUsageHandler, self).setUp()
|
||||||
|
self.handler = pipeline_handler.UsageHandler()
|
||||||
|
|
||||||
|
def test_find_exists_happyday(self):
|
||||||
|
start = datetime.datetime(2014, 12, 31, 0, 0, 0)
|
||||||
|
end = start + datetime.timedelta(days=1)
|
||||||
|
events = [{'event_type': 'event_1'},
|
||||||
|
{'event_type': 'event_2'},
|
||||||
|
{'event_type': 'compute.instance.exists',
|
||||||
|
'audit_period_beginning': start,
|
||||||
|
'audit_period_ending': end}]
|
||||||
|
|
||||||
|
exists = self.handler._find_exists(events)
|
||||||
|
self.assertEquals(exists, events[2])
|
||||||
|
|
||||||
|
def test_find_exists_none(self):
|
||||||
|
events = [{'event_type': 'event_1'},
|
||||||
|
{'event_type': 'event_2'}]
|
||||||
|
|
||||||
|
with self.assertRaises(pipeline_handler.UsageException):
|
||||||
|
self.handler._find_exists(events)
|
||||||
|
|
||||||
|
def test_find_exists_midday(self):
|
||||||
|
start = datetime.datetime(2014, 12, 31, 1, 1, 1)
|
||||||
|
end = datetime.datetime(2014, 12, 31, 1, 1, 2)
|
||||||
|
events = [{'event_type': 'event_1'},
|
||||||
|
{'event_type': 'event_2'},
|
||||||
|
{'event_type': 'compute.instance.exists',
|
||||||
|
'audit_period_beginning': start,
|
||||||
|
'audit_period_ending': end}]
|
||||||
|
|
||||||
|
with self.assertRaises(pipeline_handler.UsageException):
|
||||||
|
self.handler._find_exists(events)
|
||||||
|
|
||||||
|
def test_find_exists_long(self):
|
||||||
|
start = datetime.datetime(2014, 12, 31, 0, 0, 0)
|
||||||
|
end = start + datetime.timedelta(days=2)
|
||||||
|
events = [{'event_type': 'event_1'},
|
||||||
|
{'event_type': 'event_2'},
|
||||||
|
{'event_type': 'compute.instance.exists',
|
||||||
|
'audit_period_beginning': start,
|
||||||
|
'audit_period_ending': end}]
|
||||||
|
|
||||||
|
with self.assertRaises(pipeline_handler.UsageException):
|
||||||
|
self.handler._find_exists(events)
|
||||||
|
|
||||||
|
def test_find_exists_no_audit_periods(self):
|
||||||
|
events = [{'event_type': 'event_1'},
|
||||||
|
{'event_type': 'event_2'},
|
||||||
|
{'event_type': 'compute.instance.exists'}]
|
||||||
|
|
||||||
|
with self.assertRaises(pipeline_handler.UsageException):
|
||||||
|
self.handler._find_exists(events)
|
||||||
|
|
||||||
|
def test_extract_launched_at(self):
|
||||||
|
with self.assertRaises(pipeline_handler.UsageException):
|
||||||
|
self.handler._extract_launched_at({})
|
||||||
|
self.assertEquals("foo", self.handler._extract_launched_at(
|
||||||
|
{'launched_at': 'foo'}))
|
||||||
|
|
||||||
|
def test_extract_interesting(self):
|
||||||
|
interesting = ["a", "b", "c"]
|
||||||
|
e1 = {'event_type': 'a'}
|
||||||
|
e2 = {'event_type': 'b'}
|
||||||
|
e3 = {'event_type': 'c'}
|
||||||
|
e4 = {'event_type': 'd'}
|
||||||
|
e5 = {'event_type': 'e'}
|
||||||
|
self.assertEquals([e1, e2, e3],
|
||||||
|
self.handler._extract_interesting_events(
|
||||||
|
[e4, e1, e2, e3, e5], interesting))
|
||||||
|
|
||||||
|
def test_verify_fields_no_match(self):
|
||||||
|
exists = {'a': 1, 'b': 2, 'c': 3}
|
||||||
|
launched = exists
|
||||||
|
self.handler._verify_fields(exists, launched, ['d', 'e', 'f'])
|
||||||
|
|
||||||
|
def test_verify_fields_happyday(self):
|
||||||
|
exists = {'a': 1, 'b': 2, 'c': 3}
|
||||||
|
launched = exists
|
||||||
|
self.handler._verify_fields(exists, launched, ['a', 'b', 'c'])
|
||||||
|
|
||||||
|
def test_verify_fields_mismatch(self):
|
||||||
|
exists = {'a': 1, 'b': 2, 'c': 3}
|
||||||
|
launched = {'a': 10, 'b': 20, 'c': 30}
|
||||||
|
with self.assertRaises(pipeline_handler.UsageException):
|
||||||
|
self.handler._verify_fields(exists, launched, ['a', 'b', 'c'])
|
||||||
|
|
||||||
|
def test_confirm_delete_no_delete_events(self):
|
||||||
|
with self.assertRaises(pipeline_handler.UsageException) as e:
|
||||||
|
self.handler._confirm_delete({'deleted_at': 'now',
|
||||||
|
'state': 'active'}, [], [])
|
||||||
|
self.assertEquals("U3", e.code)
|
||||||
|
|
||||||
|
deleted_at = datetime.datetime(2014, 12, 31, 1, 0, 0)
|
||||||
|
launched_at = datetime.datetime(2014, 12, 31, 2, 0, 0)
|
||||||
|
with self.assertRaises(pipeline_handler.UsageException) as e:
|
||||||
|
self.handler._confirm_delete({'deleted_at': deleted_at,
|
||||||
|
'launched_at': launched_at,
|
||||||
|
'state': 'deleted'}, [], [])
|
||||||
|
self.assertEquals("U4", e.code)
|
||||||
|
|
||||||
|
self.handler.audit_beginning = datetime.datetime(2014, 12, 30, 0, 0, 0)
|
||||||
|
self.handler.audit_ending = datetime.datetime(2014, 12, 31, 0, 0, 0)
|
||||||
|
deleted_at = datetime.datetime(2014, 12, 30, 2, 0, 0)
|
||||||
|
launched_at = datetime.datetime(2014, 12, 30, 1, 0, 0)
|
||||||
|
with self.assertRaises(pipeline_handler.UsageException) as e:
|
||||||
|
self.handler._confirm_delete({'deleted_at': deleted_at,
|
||||||
|
'launched_at': launched_at,
|
||||||
|
'state': 'deleted'}, [], [])
|
||||||
|
self.assertEquals("U5", e.code)
|
||||||
|
|
||||||
|
# Test the do-nothing scenario
|
||||||
|
self.handler._confirm_delete({}, [], [])
|
||||||
|
|
||||||
|
def test_confirm_delete_delete_events(self):
|
||||||
|
with self.assertRaises(pipeline_handler.UsageException) as e:
|
||||||
|
self.handler._confirm_delete({}, [{}], [])
|
||||||
|
self.assertEquals("U6", e.code)
|
||||||
|
|
||||||
|
with self.assertRaises(pipeline_handler.UsageException) as e:
|
||||||
|
self.handler._confirm_delete({'deleted_at': 'now'}, [{}, {}], [])
|
||||||
|
self.assertEquals("U7", e.code)
|
||||||
|
|
||||||
|
with mock.patch.object(self.handler, "_verify_fields") as v:
|
||||||
|
exists = {'deleted_at': 'now', 'state': 'deleted'}
|
||||||
|
deleted = {'foo': 1}
|
||||||
|
self.handler._confirm_delete(exists, [deleted], ['a'])
|
||||||
|
v.assert_called_with(exists, deleted, ['a'])
|
||||||
|
|
||||||
|
def test_confirm_launched_at(self):
|
||||||
|
self.handler._confirm_launched_at({'state': 'deleted'}, [])
|
||||||
|
|
||||||
|
self.handler.audit_beginning = datetime.datetime(2014, 12, 30, 0, 0, 0)
|
||||||
|
self.handler.audit_ending = datetime.datetime(2014, 12, 31, 0, 0, 0)
|
||||||
|
launched_at = datetime.datetime(2014, 12, 30, 1, 0, 0)
|
||||||
|
with self.assertRaises(pipeline_handler.UsageException) as e:
|
||||||
|
self.handler._confirm_launched_at({'state': 'active',
|
||||||
|
'launched_at': launched_at},
|
||||||
|
[{}])
|
||||||
|
self.assertEquals("U8", e.code)
|
||||||
|
|
||||||
|
def test_handle_events_no_exists(self):
|
||||||
|
env = {'stream_id': 'stream'}
|
||||||
|
with mock.patch.object(self.handler, "_find_exists") as c:
|
||||||
|
c.side_effect = pipeline_handler.UsageException("UX", "Error")
|
||||||
|
events = self.handler.handle_events([], env)
|
||||||
|
self.assertEquals(0, len(events))
|
||||||
|
|
||||||
|
def test_handle_events_exists(self):
|
||||||
|
env = {'stream_id': 123}
|
||||||
|
with mock.patch.object(self.handler, "_find_exists") as ex:
|
||||||
|
ex.return_value = {'timestamp':'now', 'instance_id':'inst'}
|
||||||
|
with mock.patch.object(self.handler, "_do_checks") as c:
|
||||||
|
events = self.handler.handle_events([], env)
|
||||||
|
self.assertEquals(1, len(events))
|
||||||
|
f = events[0]
|
||||||
|
self.assertEquals("compute.instance.exists.verified",
|
||||||
|
f['event_type'])
|
||||||
|
self.assertEquals("now", f['timestamp'])
|
||||||
|
self.assertEquals(123, f['stream_id'])
|
||||||
|
self.assertEquals("inst", f['instance_id'])
|
||||||
|
self.assertEquals("None", f['error'])
|
||||||
|
self.assertIsNone(f['error_code'])
|
||||||
|
|
||||||
|
def test_handle_events_bad(self):
|
||||||
|
env = {'stream_id': 123}
|
||||||
|
with mock.patch.object(self.handler, "_find_exists") as ex:
|
||||||
|
ex.return_value = {'timestamp':'now', 'instance_id':'inst'}
|
||||||
|
with mock.patch.object(self.handler, "_do_checks") as c:
|
||||||
|
c.side_effect = pipeline_handler.UsageException("UX", "Error")
|
||||||
|
events = self.handler.handle_events([], env)
|
||||||
|
self.assertEquals(1, len(events))
|
||||||
|
f = events[0]
|
||||||
|
self.assertEquals("compute.instance.exists.failed",
|
||||||
|
f['event_type'])
|
||||||
|
self.assertEquals("now", f['timestamp'])
|
||||||
|
self.assertEquals(123, f['stream_id'])
|
||||||
|
self.assertEquals("inst", f['instance_id'])
|
||||||
|
self.assertEquals("Error", f['error'])
|
||||||
|
self.assertEquals("UX", f['error_code'])
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch.object(pipeline_handler.UsageHandler, '_get_core_fields')
|
||||||
|
@mock.patch.object(pipeline_handler.UsageHandler, '_extract_launched_at')
|
||||||
|
@mock.patch.object(pipeline_handler.UsageHandler, '_find_events')
|
||||||
|
@mock.patch.object(pipeline_handler.UsageHandler, '_confirm_launched_at')
|
||||||
|
@mock.patch.object(pipeline_handler.UsageHandler, '_confirm_delete')
|
||||||
|
def test_do_check(self, cd, cla, fe, ela, gcf):
|
||||||
|
fe.return_value = [1,2,3]
|
||||||
|
self.handler._do_checks({}, [])
|
@ -1,6 +1,8 @@
|
|||||||
import abc
|
import abc
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import six
|
import six
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -90,3 +92,186 @@ class LoggingHandler(PipelineHandlerBase):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UsageException(Exception):
|
||||||
|
def __init__(self, code, message):
|
||||||
|
super(UsageException, self).__init__(message)
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
|
||||||
|
class UsageHandler(PipelineHandlerBase):
|
||||||
|
def _find_exists(self, events):
|
||||||
|
exists = None
|
||||||
|
|
||||||
|
# We could have several .exists records, but only the
|
||||||
|
# end-of-day .exists will have audit_period_* time of
|
||||||
|
# 00:00:00 and be 24hrs apart.
|
||||||
|
for event in events:
|
||||||
|
apb = event.get('audit_period_beginning')
|
||||||
|
ape = event.get('audit_period_ending')
|
||||||
|
if (event['event_type'] == 'compute.instance.exists'
|
||||||
|
and apb and ape and apb.time() == datetime.time(0, 0, 0)
|
||||||
|
and ape.time() == datetime.time(0, 0, 0)
|
||||||
|
and ape.date() == (apb.date() + datetime.timedelta(days=1))):
|
||||||
|
exists = event
|
||||||
|
self.audit_beginning = apb
|
||||||
|
self.audit_ending = ape
|
||||||
|
break
|
||||||
|
|
||||||
|
if not exists:
|
||||||
|
raise UsageException("U0", "No .exists record.")
|
||||||
|
|
||||||
|
return exists
|
||||||
|
|
||||||
|
def _extract_launched_at(self, exists):
|
||||||
|
if not exists.get('launched_at'):
|
||||||
|
raise UsageException("U1", ".exists has no launched_at value.")
|
||||||
|
return exists['launched_at']
|
||||||
|
|
||||||
|
def _extract_interesting_events(self, events, interesting):
|
||||||
|
return [event for event in events
|
||||||
|
if event['event_type'] in interesting]
|
||||||
|
|
||||||
|
def _find_events(self, events):
|
||||||
|
interesting = ['compute.instance.rebuild.start',
|
||||||
|
'compute.instance.resize.prep.start',
|
||||||
|
'compute.instance.resize.revert.start',
|
||||||
|
'compute.instance.rescue.start',
|
||||||
|
'compute.instance.create.end',
|
||||||
|
'compute.instance.rebuild.end',
|
||||||
|
'compute.instance.resize.finish.end',
|
||||||
|
'compute.instance.resize.revert.end',
|
||||||
|
'compute.instance.rescue.end']
|
||||||
|
|
||||||
|
# We could easily end up with no events in final_set if
|
||||||
|
# there were no operations performed on an instance that day.
|
||||||
|
# We'll still get a .exists for every active instance though.
|
||||||
|
|
||||||
|
return self._extract_interesting_events(events, interesting)
|
||||||
|
|
||||||
|
def _find_deleted_events(self, events):
|
||||||
|
interesting = ['compute.instance.delete.end']
|
||||||
|
return self._extract_interesting_events(events, interesting)
|
||||||
|
|
||||||
|
def _verify_fields(self, exists, launch, fields):
|
||||||
|
for field in fields:
|
||||||
|
if field not in exists and field not in launch:
|
||||||
|
continue
|
||||||
|
if exists[field] != launch[field]:
|
||||||
|
raise UsageException("U2",
|
||||||
|
"Conflicting '%s' values ('%s' != '%s')"
|
||||||
|
% (field, exists[field], launch[field]))
|
||||||
|
|
||||||
|
def _confirm_delete(self, exists, deleted, fields):
|
||||||
|
deleted_at = exists.get('deleted_at')
|
||||||
|
state = exists.get('state')
|
||||||
|
|
||||||
|
if deleted_at and state != "deleted":
|
||||||
|
raise UsageException("U3", ".exists state not 'deleted' but "
|
||||||
|
"deleted_at is set.")
|
||||||
|
|
||||||
|
if deleted_at and not deleted:
|
||||||
|
# We've already confirmed it's in the "deleted" state.
|
||||||
|
launched_at = exists.get('launched_at')
|
||||||
|
if deleted_at < launched_at:
|
||||||
|
raise UsageException("U4",
|
||||||
|
".exists deleted_at < launched_at.")
|
||||||
|
|
||||||
|
# Is the deleted_at within this audit period?
|
||||||
|
if (deleted_at >= self.audit_beginning
|
||||||
|
and deleted_at <= self.audit_ending):
|
||||||
|
raise UsageException("U5", ".exists deleted_at in audit "
|
||||||
|
"period, but no matching .delete event found.")
|
||||||
|
|
||||||
|
if not deleted_at and deleted:
|
||||||
|
raise UsageException("U6", ".deleted events found but .exists "
|
||||||
|
"has no deleted_at value.")
|
||||||
|
|
||||||
|
if len(deleted) > 1:
|
||||||
|
raise UsageException("U7", "Multiple .delete.end events")
|
||||||
|
|
||||||
|
if deleted:
|
||||||
|
self._verify_fields(exists, deleted[0], fields)
|
||||||
|
|
||||||
|
def _confirm_launched_at(self, exists, events):
|
||||||
|
if exists.get('state') != 'active':
|
||||||
|
return
|
||||||
|
|
||||||
|
# Does launched_at have a value within this audit period?
|
||||||
|
# If so, we should have a related event. Otherwise, this
|
||||||
|
# instance was created previously.
|
||||||
|
launched_at = exists['launched_at']
|
||||||
|
if (launched_at >= self.audit_beginning
|
||||||
|
and launched_at <= self.audit_ending and len(events) == 1):
|
||||||
|
raise UsageException("U8", ".exists launched_at in audit "
|
||||||
|
"period, but no related events found.")
|
||||||
|
|
||||||
|
# TODO(sandy): Confirm the events we got set launched_at
|
||||||
|
# properly.
|
||||||
|
|
||||||
|
def _get_core_fields(self):
|
||||||
|
"""Broken out so derived classes can define their
|
||||||
|
own trait list."""
|
||||||
|
return ['launched_at', 'instance_type_id', 'tenant_id',
|
||||||
|
'os_architecture', 'os_version', 'os_distro']
|
||||||
|
|
||||||
|
def _do_checks(self, exists, events):
|
||||||
|
core_fields = self._get_core_fields()
|
||||||
|
delete_fields = ['launched_at', 'deleted_at']
|
||||||
|
|
||||||
|
self._extract_launched_at(exists)
|
||||||
|
deleted = self._find_deleted_events(events)
|
||||||
|
for c in self._find_events(events):
|
||||||
|
self._verify_fields(exists, c, core_fields)
|
||||||
|
|
||||||
|
self._confirm_launched_at(exists, events)
|
||||||
|
self._confirm_delete(exists, deleted, delete_fields)
|
||||||
|
|
||||||
|
def handle_events(self, events, env):
|
||||||
|
self.env = env
|
||||||
|
self.stream_id = env['stream_id']
|
||||||
|
|
||||||
|
exists = None
|
||||||
|
error = None
|
||||||
|
try:
|
||||||
|
exists = self._find_exists(events)
|
||||||
|
self._do_checks(exists, events)
|
||||||
|
event_type = "compute.instance.exists.verified"
|
||||||
|
except UsageException as e:
|
||||||
|
error = e
|
||||||
|
event_type = "compute.instance.exists.failed"
|
||||||
|
logger.warn("Stream %s UsageException: (%s) %s" %
|
||||||
|
(self.stream_id, e.code, e))
|
||||||
|
if exists:
|
||||||
|
logger.warn("Stream %s deleted_at: %s, launched_at: %s, "
|
||||||
|
"state: %s, APB: %s, APE: %s, #events: %s" %
|
||||||
|
(self.stream_id, exists.get("deleted_at"),
|
||||||
|
exists.get("launched_at"), exists.get("state"),
|
||||||
|
exists.get("audit_period_beginning"),
|
||||||
|
exists.get("audit_period_ending"), len(events)))
|
||||||
|
|
||||||
|
if len(events) > 1:
|
||||||
|
logger.warn("Events for Stream: %s" % self.stream_id)
|
||||||
|
for event in events:
|
||||||
|
logger.warn("^Event: %s - %s" %
|
||||||
|
(event['timestamp'], event['event_type']))
|
||||||
|
|
||||||
|
if exists:
|
||||||
|
new_event = {'event_type': event_type,
|
||||||
|
'message_id': str(uuid.uuid4()),
|
||||||
|
'timestamp': exists.get('timestamp',
|
||||||
|
datetime.datetime.utcnow()),
|
||||||
|
'stream_id': int(self.stream_id),
|
||||||
|
'instance_id': exists.get('instance_id'),
|
||||||
|
'error': str(error),
|
||||||
|
'error_code': error and error.code
|
||||||
|
}
|
||||||
|
events.append(new_event)
|
||||||
|
else:
|
||||||
|
logger.debug("No .exists record")
|
||||||
|
return events
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def rollback(self):
|
||||||
|
pass
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import datetime
|
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
@ -9,6 +8,7 @@ from winchester.db import DBInterface, DuplicateError, LockError
|
|||||||
from winchester.config import ConfigManager, ConfigSection, ConfigItem
|
from winchester.config import ConfigManager, ConfigSection, ConfigItem
|
||||||
from winchester.definition import TriggerDefinition
|
from winchester.definition import TriggerDefinition
|
||||||
from winchester.models import StreamState
|
from winchester.models import StreamState
|
||||||
|
from winchester import time_sync as ts
|
||||||
from winchester.trigger_manager import TriggerManager
|
from winchester.trigger_manager import TriggerManager
|
||||||
|
|
||||||
|
|
||||||
@ -59,7 +59,8 @@ class Pipeline(object):
|
|||||||
raise PipelineExecutionError("Error loading pipeline", e)
|
raise PipelineExecutionError("Error loading pipeline", e)
|
||||||
self.handlers.append(handler)
|
self.handlers.append(handler)
|
||||||
|
|
||||||
def handle_events(self, events, debugger):
|
def handle_events(self, events, stream, debugger):
|
||||||
|
self.env['stream_id'] = stream.id
|
||||||
event_ids = set(e['message_id'] for e in events)
|
event_ids = set(e['message_id'] for e in events)
|
||||||
try:
|
try:
|
||||||
for handler in self.handlers:
|
for handler in self.handlers:
|
||||||
@ -123,12 +124,16 @@ class PipelineManager(object):
|
|||||||
return configs
|
return configs
|
||||||
|
|
||||||
def __init__(self, config, db=None, pipeline_handlers=None,
|
def __init__(self, config, db=None, pipeline_handlers=None,
|
||||||
pipeline_config=None, trigger_defs=None):
|
pipeline_config=None, trigger_defs=None, time_sync=None):
|
||||||
logger.debug("PipelineManager: Using config: %s" % str(config))
|
logger.debug("PipelineManager: Using config: %s" % str(config))
|
||||||
config = ConfigManager.wrap(config, self.config_description())
|
config = ConfigManager.wrap(config, self.config_description())
|
||||||
self.config = config
|
self.config = config
|
||||||
config.check_config()
|
config.check_config()
|
||||||
config.add_config_path(*config['config_path'])
|
config.add_config_path(*config['config_path'])
|
||||||
|
if time_sync is None:
|
||||||
|
time_sync = ts.TimeSync()
|
||||||
|
self.time_sync = time_sync
|
||||||
|
|
||||||
if db is not None:
|
if db is not None:
|
||||||
self.db = db
|
self.db = db
|
||||||
else:
|
else:
|
||||||
@ -160,7 +165,8 @@ class PipelineManager(object):
|
|||||||
self.trigger_map = dict((tdef.name, tdef) for tdef in self.trigger_definitions)
|
self.trigger_map = dict((tdef.name, tdef) for tdef in self.trigger_definitions)
|
||||||
|
|
||||||
self.trigger_manager = TriggerManager(self.config, db=self.db,
|
self.trigger_manager = TriggerManager(self.config, db=self.db,
|
||||||
trigger_defs=self.trigger_definitions)
|
trigger_defs=self.trigger_definitions,
|
||||||
|
time_sync=time_sync)
|
||||||
|
|
||||||
self.pipeline_worker_batch_size = config['pipeline_worker_batch_size']
|
self.pipeline_worker_batch_size = config['pipeline_worker_batch_size']
|
||||||
self.pipeline_worker_delay = config['pipeline_worker_delay']
|
self.pipeline_worker_delay = config['pipeline_worker_delay']
|
||||||
@ -191,7 +197,7 @@ class PipelineManager(object):
|
|||||||
|
|
||||||
def current_time(self):
|
def current_time(self):
|
||||||
# here so it's easily overridden.
|
# here so it's easily overridden.
|
||||||
return datetime.datetime.utcnow()
|
return self.time_sync.current_time()
|
||||||
|
|
||||||
def _log_statistics(self):
|
def _log_statistics(self):
|
||||||
logger.info("Loaded %s streams. Fired %s, Expired %s." % (
|
logger.info("Loaded %s streams. Fired %s, Expired %s." % (
|
||||||
@ -213,7 +219,7 @@ class PipelineManager(object):
|
|||||||
debugger = trigger_def.debugger
|
debugger = trigger_def.debugger
|
||||||
try:
|
try:
|
||||||
pipeline = Pipeline(pipeline_name, pipeline_config, self.pipeline_handlers)
|
pipeline = Pipeline(pipeline_name, pipeline_config, self.pipeline_handlers)
|
||||||
new_events = pipeline.handle_events(events, debugger)
|
new_events = pipeline.handle_events(events, stream, debugger)
|
||||||
except PipelineExecutionError:
|
except PipelineExecutionError:
|
||||||
logger.error("Exception in pipeline %s handling stream %s" % (
|
logger.error("Exception in pipeline %s handling stream %s" % (
|
||||||
pipeline_name, stream.id))
|
pipeline_name, stream.id))
|
||||||
|
65
winchester/time_sync.py
Normal file
65
winchester/time_sync.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import datetime
|
||||||
|
import dateutil.parser
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TimeSync(object):
|
||||||
|
def __init__(self, config={}, publishes=False):
|
||||||
|
url = config.get('time_sync_endpoint')
|
||||||
|
self.endpoint = None
|
||||||
|
if url:
|
||||||
|
self.endpoint = "%s/time" % url
|
||||||
|
logger.debug("Time sync endpoint=%s" % self.endpoint)
|
||||||
|
self.last_update = None
|
||||||
|
self.last_tyme = self._get_now()
|
||||||
|
self.publishes = publishes
|
||||||
|
|
||||||
|
def _get_now(self):
|
||||||
|
# Broken out for testing
|
||||||
|
return datetime.datetime.utcnow()
|
||||||
|
|
||||||
|
def _should_update(self, now):
|
||||||
|
return (not self.last_update or (now - self.last_update).seconds > 20)
|
||||||
|
|
||||||
|
def _fetch(self):
|
||||||
|
while True:
|
||||||
|
tyme = requests.get(self.endpoint).text
|
||||||
|
if tyme and tyme != "None":
|
||||||
|
return tyme
|
||||||
|
logger.debug("No time published yet. Waiting ...")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def current_time(self):
|
||||||
|
now = self._get_now()
|
||||||
|
if not self.endpoint:
|
||||||
|
return now
|
||||||
|
|
||||||
|
if not self.publishes and self._should_update(now):
|
||||||
|
try:
|
||||||
|
tyme = self._fetch()
|
||||||
|
logger.debug("Requested time, got '%s'" % tyme)
|
||||||
|
self.last_tyme = dateutil.parser.parse(tyme)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Could not get time: %s" % e)
|
||||||
|
self.last_update = now
|
||||||
|
|
||||||
|
return self.last_tyme
|
||||||
|
|
||||||
|
def publish(self, tyme):
|
||||||
|
if not self.endpoint:
|
||||||
|
return
|
||||||
|
|
||||||
|
daittyme = dateutil.parser.parse(tyme)
|
||||||
|
self.last_tyme = daittyme
|
||||||
|
if self._should_update(daittyme):
|
||||||
|
self.last_update = daittyme
|
||||||
|
try:
|
||||||
|
requests.post(self.endpoint, data=tyme)
|
||||||
|
logger.debug("Published time: %s" % tyme)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Could not publish time: %s" % e)
|
@ -7,7 +7,7 @@ from winchester.config import ConfigManager, ConfigSection, ConfigItem
|
|||||||
from winchester import debugging
|
from winchester import debugging
|
||||||
from winchester.db import DBInterface, DuplicateError
|
from winchester.db import DBInterface, DuplicateError
|
||||||
from winchester.definition import TriggerDefinition
|
from winchester.definition import TriggerDefinition
|
||||||
|
from winchester import time_sync as ts
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -79,6 +79,10 @@ class TriggerManager(object):
|
|||||||
"for stackdistiller. Classes specified with "
|
"for stackdistiller. Classes specified with "
|
||||||
"simport syntax. See stackdistiller and "
|
"simport syntax. See stackdistiller and "
|
||||||
"simport docs for more info", default=dict()),
|
"simport docs for more info", default=dict()),
|
||||||
|
time_sync_endpoint=ConfigItem(
|
||||||
|
help="URL of time sync service for use with"
|
||||||
|
" replying old events.",
|
||||||
|
default=None),
|
||||||
catch_all_notifications=ConfigItem(
|
catch_all_notifications=ConfigItem(
|
||||||
help="Store basic info for all notifications,"
|
help="Store basic info for all notifications,"
|
||||||
" even if not listed in distiller config",
|
" even if not listed in distiller config",
|
||||||
@ -94,12 +98,16 @@ class TriggerManager(object):
|
|||||||
"process for each stream"),
|
"process for each stream"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, config, db=None, stackdistiller=None, trigger_defs=None):
|
def __init__(self, config, db=None, stackdistiller=None, trigger_defs=None,
|
||||||
|
time_sync=None):
|
||||||
config = ConfigManager.wrap(config, self.config_description())
|
config = ConfigManager.wrap(config, self.config_description())
|
||||||
self.config = config
|
self.config = config
|
||||||
self.debug_manager = debugging.DebugManager()
|
self.debug_manager = debugging.DebugManager()
|
||||||
config.check_config()
|
config.check_config()
|
||||||
config.add_config_path(*config['config_path'])
|
config.add_config_path(*config['config_path'])
|
||||||
|
if time_sync is None:
|
||||||
|
time_sync = ts.TimeSync()
|
||||||
|
self.time_sync = time_sync
|
||||||
|
|
||||||
if db is not None:
|
if db is not None:
|
||||||
self.db = db
|
self.db = db
|
||||||
@ -147,7 +155,7 @@ class TriggerManager(object):
|
|||||||
|
|
||||||
def current_time(self):
|
def current_time(self):
|
||||||
# here so it's easily overridden.
|
# here so it's easily overridden.
|
||||||
return datetime.datetime.utcnow()
|
return self.time_sync.current_time()
|
||||||
|
|
||||||
def save_event(self, event):
|
def save_event(self, event):
|
||||||
traits = event.copy()
|
traits = event.copy()
|
||||||
|
@ -9,6 +9,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
from winchester.config import ConfigManager
|
from winchester.config import ConfigManager
|
||||||
from winchester.pipeline_manager import PipelineManager
|
from winchester.pipeline_manager import PipelineManager
|
||||||
|
from winchester import time_sync
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@ -27,7 +28,8 @@ def main():
|
|||||||
level = conf['log_level']
|
level = conf['log_level']
|
||||||
level = getattr(logging, level.upper())
|
level = getattr(logging, level.upper())
|
||||||
logging.getLogger('winchester').setLevel(level)
|
logging.getLogger('winchester').setLevel(level)
|
||||||
pipe = PipelineManager(conf)
|
timesync = time_sync.TimeSync(conf)
|
||||||
|
pipe = PipelineManager(conf, time_sync=timesync)
|
||||||
if args.daemon:
|
if args.daemon:
|
||||||
print "Backgrounding for daemon mode."
|
print "Backgrounding for daemon mode."
|
||||||
with daemon.DaemonContext():
|
with daemon.DaemonContext():
|
||||||
|
@ -5,6 +5,7 @@ import yagi.config
|
|||||||
|
|
||||||
from winchester.trigger_manager import TriggerManager
|
from winchester.trigger_manager import TriggerManager
|
||||||
from winchester.config import ConfigManager
|
from winchester.config import ConfigManager
|
||||||
|
from winchester import time_sync
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -22,10 +23,13 @@ class WinchesterHandler(BaseHandler):
|
|||||||
super(WinchesterHandler, self).__init__(app=app, queue_name=queue_name)
|
super(WinchesterHandler, self).__init__(app=app, queue_name=queue_name)
|
||||||
conf_file = self.config_get("config_file")
|
conf_file = self.config_get("config_file")
|
||||||
config = ConfigManager.load_config_file(conf_file)
|
config = ConfigManager.load_config_file(conf_file)
|
||||||
self.trigger_manager = TriggerManager(config)
|
self.time_sync = time_sync.TimeSync(config, publishes=True)
|
||||||
|
self.trigger_manager = TriggerManager(config, time_sync=self.time_sync)
|
||||||
|
|
||||||
def handle_messages(self, messages, env):
|
def handle_messages(self, messages, env):
|
||||||
for notification in self.iterate_payloads(messages, env):
|
for notification in self.iterate_payloads(messages, env):
|
||||||
|
tyme = notification['timestamp']
|
||||||
|
self.time_sync.publish(tyme)
|
||||||
self.trigger_manager.add_notification(notification)
|
self.trigger_manager.add_notification(notification)
|
||||||
|
|
||||||
def on_idle(self, num_messages, queue_name):
|
def on_idle(self, num_messages, queue_name):
|
||||||
|
Loading…
Reference in New Issue
Block a user