Generate stack events for stack state transitions

Currently, the event-list output is very resource-centric, despite
being scoped to the stack from an API path perspective.

This, combined with the fact that the stack updated_at timestamp
is only updated after a succesful update (ref bug #1193269) makes
it impossible to derive the time when an update started via any API.

This is a problem when trying to use the new hook/breakpoint API,
because it's necessary to poll for all events since the most recent
update started, disregarding any stale hook events from previous
updates (which may have failed or timed out without the hooks getting
cleared).

To work around this, add an event for each stack state transition,
such that you can detect the transition to UPDATE_IN_PROGRESS,
then use that event as a marker to get post-update-started events.

Without this (or some other way to determine when the stack update
started), the hooks pre-update functionality landed for kilo is
not really usable (particularly mechanically via scripts).

Change-Id: Idff342b3aecc2d145dfbc7c0f610ad0ca8e52c8b
Partial-Bug: #1448155
This commit is contained in:
Steven Hardy 2015-05-01 16:40:03 +01:00
parent 7c8619a205
commit d5793c08e0
3 changed files with 65 additions and 34 deletions

View File

@ -34,6 +34,7 @@ from heat.common import identifier
from heat.common import lifecycle_plugin_utils
from heat.engine import dependencies
from heat.engine import environment
from heat.engine import event
from heat.engine import function
from heat.engine.notification import stack as notification
from heat.engine import parameter_groups as param_groups
@ -669,6 +670,14 @@ class Stack(collections.Mapping):
'''
return any(res.requires_deferred_auth for res in six.itervalues(self))
def _add_event(self, action, status, reason):
'''Add a state change event to the database.'''
ev = event.Event(self.context, self, action, status, reason,
self.id, {},
self.name, 'OS::Heat::Stack')
ev.store()
@profiler.trace('Stack.state_set', hide_args=False)
def state_set(self, action, status, reason):
'''Update the stack state in the database.'''
@ -697,6 +706,7 @@ class Stack(collections.Mapping):
'name': self.name,
'reason': reason})
notification.send(self)
self._add_event(action, status, reason)
@property
def state(self):

View File

@ -781,21 +781,24 @@ class SqlAlchemyTest(common.HeatTestCase):
self.m.UnsetStubs()
events = db_api.event_get_all_by_stack(self.ctx, UUID1)
self.assertEqual(2, len(events))
self.assertEqual(4, len(events))
# test filter by resource_status
filters = {'resource_status': 'COMPLETE'}
events = db_api.event_get_all_by_stack(self.ctx, UUID1,
filters=filters)
self.assertEqual(1, len(events))
self.assertEqual(2, len(events))
self.assertEqual('COMPLETE', events[0].resource_status)
self.assertEqual('COMPLETE', events[1].resource_status)
# test filter by resource_action
filters = {'resource_action': 'CREATE'}
events = db_api.event_get_all_by_stack(self.ctx, UUID1,
filters=filters)
self.assertEqual(2, len(events))
self.assertEqual(4, len(events))
self.assertEqual('CREATE', events[0].resource_action)
self.assertEqual('CREATE', events[1].resource_action)
self.assertEqual('CREATE', events[2].resource_action)
self.assertEqual('CREATE', events[3].resource_action)
# test filter by resource_type
filters = {'resource_type': 'AWS::EC2::Instance'}
events = db_api.event_get_all_by_stack(self.ctx, UUID1,
@ -825,20 +828,24 @@ class SqlAlchemyTest(common.HeatTestCase):
filters = {'resource_status': 'COMPLETE'}
events = db_api.event_get_all_by_stack(self.ctx, UUID1,
filters=filters)
self.assertEqual(2, len(events))
self.assertEqual(4, len(events))
self.assertEqual('COMPLETE', events[0].resource_status)
self.assertEqual('COMPLETE', events[1].resource_status)
self.assertEqual('COMPLETE', events[2].resource_status)
self.assertEqual('COMPLETE', events[3].resource_status)
# test filter by resource_action
filters = {'resource_action': 'DELETE',
'resource_status': 'COMPLETE'}
events = db_api.event_get_all_by_stack(self.ctx, UUID1,
filters=filters)
self.assertEqual(1, len(events))
self.assertEqual(2, len(events))
self.assertEqual('DELETE', events[0].resource_action)
self.assertEqual('COMPLETE', events[0].resource_status)
self.assertEqual('DELETE', events[1].resource_action)
self.assertEqual('COMPLETE', events[1].resource_status)
# test limit and marker
events_all = db_api.event_get_all_by_stack(self.ctx, UUID1)
self.assertEqual(4, len(events_all))
self.assertEqual(8, len(events_all))
marker = events_all[1].uuid
events2_uuid = events_all[2].uuid
@ -865,14 +872,14 @@ class SqlAlchemyTest(common.HeatTestCase):
self.m.UnsetStubs()
num_events = db_api.event_count_all_by_stack(self.ctx, UUID1)
self.assertEqual(2, num_events)
self.assertEqual(4, num_events)
self._mock_delete(self.m)
self.m.ReplayAll()
stack.delete()
num_events = db_api.event_count_all_by_stack(self.ctx, UUID1)
self.assertEqual(4, num_events)
self.assertEqual(8, num_events)
self.m.VerifyAll()
@ -885,7 +892,7 @@ class SqlAlchemyTest(common.HeatTestCase):
self.m.UnsetStubs()
events = db_api.event_get_all_by_tenant(self.ctx)
self.assertEqual(6, len(events))
self.assertEqual(12, len(events))
self._mock_delete(self.m)
self.m.ReplayAll()
@ -905,14 +912,14 @@ class SqlAlchemyTest(common.HeatTestCase):
self.m.UnsetStubs()
events = db_api.event_get_all(self.ctx)
self.assertEqual(6, len(events))
self.assertEqual(12, len(events))
self._mock_delete(self.m)
self.m.ReplayAll()
stacks[0].delete()
events = db_api.event_get_all(self.ctx)
self.assertEqual(4, len(events))
self.assertEqual(8, len(events))
self.m.VerifyAll()

View File

@ -1521,35 +1521,41 @@ class StackServiceTest(common.HeatTestCase):
events = self.eng.list_events(self.ctx, self.stack.identifier())
self.assertEqual(2, len(events))
self.assertEqual(4, len(events))
for ev in events:
self.assertIn('event_identity', ev)
self.assertIsInstance(ev['event_identity'], dict)
self.assertTrue(ev['event_identity']['path'].rsplit('/', 1)[1])
self.assertIn('resource_name', ev)
self.assertEqual('WebServer', ev['resource_name'])
self.assertIn(ev['resource_name'],
('service_event_list_test_stack', 'WebServer'))
self.assertIn('physical_resource_id', ev)
self.assertIn('resource_properties', ev)
# Big long user data field.. it mentions 'wordpress'
# a few times so this should work.
user_data = ev['resource_properties']['UserData']
self.assertIn('wordpress', user_data)
self.assertEqual('F17-x86_64-gold',
ev['resource_properties']['ImageId'])
self.assertEqual('m1.large',
ev['resource_properties']['InstanceType'])
if ev.get('resource_properties'):
user_data = ev['resource_properties']['UserData']
self.assertIn('wordpress', user_data)
self.assertEqual('F17-x86_64-gold',
ev['resource_properties']['ImageId'])
self.assertEqual('m1.large',
ev['resource_properties']['InstanceType'])
self.assertEqual('CREATE', ev['resource_action'])
self.assertIn(ev['resource_status'], ('IN_PROGRESS', 'COMPLETE'))
self.assertIn('resource_status_reason', ev)
self.assertEqual('state changed', ev['resource_status_reason'])
self.assertIn(ev['resource_status_reason'],
('state changed',
'Stack CREATE started',
'Stack CREATE completed successfully'))
self.assertIn('resource_type', ev)
self.assertEqual('AWS::EC2::Instance', ev['resource_type'])
self.assertIn(ev['resource_type'],
('AWS::EC2::Instance', 'OS::Heat::Stack'))
self.assertIn('stack_identity', ev)
@ -1597,7 +1603,7 @@ class StackServiceTest(common.HeatTestCase):
self.assertTrue(result['stack_id'])
events = self.eng.list_events(self.ctx, self.stack.identifier())
self.assertEqual(6, len(events))
self.assertEqual(9, len(events))
for ev in events:
self.assertIn('event_identity', ev)
@ -1609,12 +1615,14 @@ class StackServiceTest(common.HeatTestCase):
self.assertIn('resource_properties', ev)
self.assertIn('resource_status_reason', ev)
self.assertIn(ev['resource_action'], ('CREATE', 'DELETE'))
self.assertIn(ev['resource_action'],
('CREATE', 'UPDATE', 'DELETE'))
self.assertIn(ev['resource_status'], ('IN_PROGRESS', 'COMPLETE'))
self.assertIn('resource_type', ev)
self.assertIn(ev['resource_type'], ('AWS::EC2::Instance',
'GenericResourceType'))
'GenericResourceType',
'OS::Heat::Stack'))
self.assertIn('stack_identity', ev)
@ -1629,35 +1637,41 @@ class StackServiceTest(common.HeatTestCase):
def test_stack_event_list_by_tenant(self):
events = self.eng.list_events(self.ctx, None)
self.assertEqual(2, len(events))
self.assertEqual(4, len(events))
for ev in events:
self.assertIn('event_identity', ev)
self.assertIsInstance(ev['event_identity'], dict)
self.assertTrue(ev['event_identity']['path'].rsplit('/', 1)[1])
self.assertIn('resource_name', ev)
self.assertEqual('WebServer', ev['resource_name'])
self.assertIn(ev['resource_name'],
('WebServer', 'service_event_list_test_stack'))
self.assertIn('physical_resource_id', ev)
self.assertIn('resource_properties', ev)
# Big long user data field.. it mentions 'wordpress'
# a few times so this should work.
user_data = ev['resource_properties']['UserData']
self.assertIn('wordpress', user_data)
self.assertEqual('F17-x86_64-gold',
ev['resource_properties']['ImageId'])
self.assertEqual('m1.large',
ev['resource_properties']['InstanceType'])
if ev.get('resource_properties'):
user_data = ev['resource_properties']['UserData']
self.assertIn('wordpress', user_data)
self.assertEqual('F17-x86_64-gold',
ev['resource_properties']['ImageId'])
self.assertEqual('m1.large',
ev['resource_properties']['InstanceType'])
self.assertEqual('CREATE', ev['resource_action'])
self.assertIn(ev['resource_status'], ('IN_PROGRESS', 'COMPLETE'))
self.assertIn('resource_status_reason', ev)
self.assertEqual('state changed', ev['resource_status_reason'])
self.assertIn(ev['resource_status_reason'],
('state changed',
'Stack CREATE started',
'Stack CREATE completed successfully'))
self.assertIn('resource_type', ev)
self.assertEqual('AWS::EC2::Instance', ev['resource_type'])
self.assertIn(ev['resource_type'],
('AWS::EC2::Instance', 'OS::Heat::Stack'))
self.assertIn('stack_identity', ev)