Change Stack timestamps to save correct info

Stack.updated_time is currently being treated as modification time of
the object in the database.  Since it's not a very useful thing to have
and the CFN API treats this value as "the last time an update call was
issued", it makes sense to have the same behavior here as well.

This changes the current real-time retrieval of created_time and
updated_time in favor of the attributes already present in the stack and
changes the information stored in these two fields to be the time the
stack was created and the time the stack was last updated by the user,
respectively.

Co-Authored-By: Anderson Mesquita <andersonvom@gmail.com>
Closes-Bug: #1193269
Change-Id: I1fd6586b827b3f2babb5af5c85f9de25da74fbb6
This commit is contained in:
Richard Lee 2014-02-18 16:46:31 -05:00 committed by Anderson Mesquita
parent ac8ef2a6ba
commit ce690963e7
8 changed files with 134 additions and 29 deletions

View File

@ -191,7 +191,6 @@ class StackController(object):
engine_api.STACK_CREATION_TIME: 'CreationTime',
engine_api.STACK_DESCRIPTION: 'Description',
engine_api.STACK_DISABLE_ROLLBACK: 'DisableRollback',
engine_api.STACK_UPDATED_TIME: 'LastUpdatedTime',
engine_api.STACK_NOTIFICATION_TOPICS: 'NotificationARNs',
engine_api.STACK_PARAMETERS: 'Parameters',
engine_api.STACK_ID: 'StackId',
@ -200,6 +199,9 @@ class StackController(object):
engine_api.STACK_TIMEOUT: 'TimeoutInMinutes',
}
if s[engine_api.STACK_UPDATED_TIME] is not None:
keymap[engine_api.STACK_UPDATED_TIME] = 'LastUpdatedTime'
result = api_utils.reformat_dict_keys(keymap, s)
action = s[engine_api.STACK_ACTION]

View File

@ -119,6 +119,11 @@ class Stack(BASE, HeatBase, SoftDelete):
stack_user_project_id = sqlalchemy.Column(sqlalchemy.String(64),
nullable=True)
# Override timestamp column to store the correct value: it should be the
# time the create/update call was issued, not the time the DB entry is
# created/modified. (bug #1193269)
updated_at = sqlalchemy.Column(sqlalchemy.DateTime)
class StackLock(BASE, HeatBase):
"""Store stack locks for deployments with multiple-engines."""

View File

@ -81,11 +81,12 @@ def format_stack(stack):
Return a representation of the given stack that matches the API output
expectations.
'''
updated_time = stack.updated_time and timeutils.isotime(stack.updated_time)
info = {
api.STACK_NAME: stack.name,
api.STACK_ID: dict(stack.identifier()),
api.STACK_CREATION_TIME: timeutils.isotime(stack.created_time),
api.STACK_UPDATED_TIME: timeutils.isotime(stack.updated_time),
api.STACK_UPDATED_TIME: updated_time,
api.STACK_NOTIFICATION_TOPICS: [], # TODO Not implemented yet
api.STACK_PARAMETERS: stack.parameters.map(str),
api.STACK_DESCRIPTION: stack.t[stack.t.DESCRIPTION],

View File

@ -15,7 +15,7 @@
import collections
import copy
import functools
from datetime import datetime
import re
import six
@ -29,7 +29,6 @@ from heat.engine import function
from heat.engine import resource
from heat.engine import resources
from heat.engine import scheduler
from heat.engine import timestamp
from heat.engine import update
from heat.engine.notification import stack as notification
from heat.engine.parameter_groups import ParameterGroups
@ -55,20 +54,14 @@ class Stack(collections.Mapping):
STATUSES = (IN_PROGRESS, FAILED, COMPLETE
) = ('IN_PROGRESS', 'FAILED', 'COMPLETE')
created_time = timestamp.Timestamp(functools.partial(db_api.stack_get,
show_deleted=True),
'created_at')
updated_time = timestamp.Timestamp(functools.partial(db_api.stack_get,
show_deleted=True),
'updated_at')
_zones = None
def __init__(self, context, stack_name, tmpl, env=None,
stack_id=None, action=None, status=None,
status_reason='', timeout_mins=60, resolve_data=True,
disable_rollback=True, parent_resource=None, owner_id=None,
adopt_stack_data=None, stack_user_project_id=None):
adopt_stack_data=None, stack_user_project_id=None,
created_time=None, updated_time=None):
'''
Initialise from a context, name, Template object and (optionally)
Environment object. The database ID may also be initialised, if the
@ -99,6 +92,8 @@ class Stack(collections.Mapping):
self._access_allowed_handlers = {}
self.adopt_stack_data = adopt_stack_data
self.stack_user_project_id = stack_user_project_id
self.created_time = created_time
self.updated_time = updated_time
resources.initialise()
@ -191,7 +186,9 @@ class Stack(collections.Mapping):
stack.id, stack.action, stack.status, stack.status_reason,
stack.timeout, resolve_data, stack.disable_rollback,
parent_resource, owner_id=stack.owner_id,
stack_user_project_id=stack.stack_user_project_id)
stack_user_project_id=stack.stack_user_project_id,
created_time=stack.created_at,
updated_time=stack.updated_at)
return stack
@ -200,7 +197,6 @@ class Stack(collections.Mapping):
Store the stack in the database and return its ID
If self.id is set, we update the existing stack
'''
s = {
'name': self._backup_name() if backup else self.name,
'raw_template_id': self.t.store(self.context),
@ -214,6 +210,7 @@ class Stack(collections.Mapping):
'timeout': self.timeout_mins,
'disable_rollback': self.disable_rollback,
'stack_user_project_id': self.stack_user_project_id,
'updated_at': self.updated_time,
}
if self.id:
db_api.stack_update(self.context, self.id, s)
@ -229,6 +226,7 @@ class Stack(collections.Mapping):
new_s = db_api.stack_create(self.context, s)
self.id = new_s.id
self.created_time = new_s.created_at
self._set_param_stackid()
@ -518,6 +516,7 @@ class Stack(collections.Mapping):
Update will fail if it exceeds the specified timeout. The default is
60 minutes, set in the constructor
'''
self.updated_time = datetime.utcnow()
updater = scheduler.TaskRunner(self.update_task, newstack)
updater()

View File

@ -15,7 +15,6 @@
import copy
import base64
from datetime import datetime
from heat.engine import event
from heat.common import exception
@ -751,9 +750,6 @@ class Resource(object):
new_rs = db_api.resource_create(self.context, rs)
self.id = new_rs.id
self.stack.updated_time = datetime.utcnow()
except Exception as ex:
logger.error(_('DB error %s') % str(ex))
@ -781,8 +777,6 @@ class Resource(object):
'status_reason': reason,
'stack_id': self.stack.id,
'nova_instance': self.resource_id})
self.stack.updated_time = datetime.utcnow()
except Exception as ex:
logger.error(_('DB error %s') % str(ex))

View File

@ -207,6 +207,54 @@ class CfnStackControllerTest(HeatTestCase):
'version': self.api_version},
None)
def test_describe_last_updated_time(self):
params = {'Action': 'DescribeStacks'}
dummy_req = self._dummy_GET_request(params)
self._stub_enforce(dummy_req, 'DescribeStacks')
engine_resp = [{u'updated_time': '1970-01-01',
u'parameters': {},
u'stack_action': u'CREATE',
u'stack_status': u'COMPLETE'}]
self.m.StubOutWithMock(rpc, 'call')
rpc.call(dummy_req.context, self.topic,
{'namespace': None,
'method': 'show_stack',
'args': {'stack_identity': None},
'version': self.api_version}, None).AndReturn(engine_resp)
self.m.ReplayAll()
response = self.controller.describe(dummy_req)
result = response['DescribeStacksResponse']['DescribeStacksResult']
stack = result['Stacks'][0]
self.assertEqual('1970-01-01', stack['LastUpdatedTime'])
def test_describe_no_last_updated_time(self):
params = {'Action': 'DescribeStacks'}
dummy_req = self._dummy_GET_request(params)
self._stub_enforce(dummy_req, 'DescribeStacks')
engine_resp = [{u'updated_time': None,
u'parameters': {},
u'stack_action': u'CREATE',
u'stack_status': u'COMPLETE'}]
self.m.StubOutWithMock(rpc, 'call')
rpc.call(dummy_req.context, self.topic,
{'namespace': None,
'method': 'show_stack',
'args': {'stack_identity': None},
'version': self.api_version}, None).AndReturn(engine_resp)
self.m.ReplayAll()
response = self.controller.describe(dummy_req)
result = response['DescribeStacksResponse']['DescribeStacksResult']
stack = result['Stacks'][0]
self.assertNotIn('LastUpdatedTime', stack)
def test_describe(self):
# Format a dummy GET request to pass into the WSGI handler
stack_name = u"wordpress"

View File

@ -15,6 +15,7 @@
import json
import mock
import uuid
from datetime import datetime
import heat.engine.api as api
@ -189,8 +190,6 @@ class FormatTest(HeatTestCase):
event_id_formatted['path'])
self.assertEqual(event_id, event_identifier.event_id)
@mock.patch.object(parser.Stack, 'updated_time', new=None)
@mock.patch.object(parser.Stack, 'created_time', new=None)
@mock.patch.object(api, 'format_stack_resource')
def test_format_stack_preview(self, mock_fmt_resource):
def mock_format_resources(res):
@ -206,6 +205,58 @@ class FormatTest(HeatTestCase):
self.assertIn('resources', stack)
self.assertEqual(['fmt1', ['fmt2', ['fmt3']]], stack['resources'])
def test_format_stack(self):
self.stack.created_time = datetime(1970, 1, 1)
info = api.format_stack(self.stack)
aws_id = ('arn:openstack:heat::test_tenant_id:'
'stacks/test_stack/' + self.stack.id)
expected_stack_info = {
'capabilities': [],
'creation_time': '1970-01-01T00:00:00Z',
'description': 'No description',
'disable_rollback': True,
'notification_topics': [],
'stack_action': '',
'stack_name': 'test_stack',
'stack_status': '',
'stack_status_reason': '',
'template_description': 'No description',
'timeout_mins': 60,
'parameters': {
'AWS::Region': 'ap-southeast-1',
'AWS::StackId': aws_id,
'AWS::StackName': 'test_stack'},
'stack_identity': {
'path': '',
'stack_id': self.stack.id,
'stack_name': 'test_stack',
'tenant': 'test_tenant_id'},
'updated_time': None}
self.assertEqual(expected_stack_info, info)
def test_format_stack_created_time(self):
self.stack.created_time = None
info = api.format_stack(self.stack)
self.assertIsNotNone(info['creation_time'])
def test_format_stack_updated_time(self):
self.stack.updated_time = None
info = api.format_stack(self.stack)
self.assertIsNone(info['updated_time'])
self.stack.updated_time = datetime(1970, 1, 1)
info = api.format_stack(self.stack)
self.assertEqual('1970-01-01T00:00:00Z', info['updated_time'])
@mock.patch.object(api, 'format_stack_outputs')
def test_format_stack_adds_outputs(self, mock_fmt_outputs):
mock_fmt_outputs.return_value = 'foobar'
self.stack.action = 'CREATE'
self.stack.status = 'COMPLETE'
info = api.format_stack(self.stack)
self.assertEqual('foobar', info[rpc_api.STACK_OUTPUTS])
class FormatValidateParameterTest(HeatTestCase):

View File

@ -17,7 +17,7 @@ import mock
import time
from keystoneclient import exceptions as kc_exceptions
from mox import IgnoreArg
from oslo.config import cfg
from heat.engine import environment
@ -885,7 +885,9 @@ class StackTest(HeatTestCase):
stack.action, stack.status, stack.status_reason,
stack.timeout, True, stack.disable_rollback,
'parent', owner_id=None,
stack_user_project_id=None)
stack_user_project_id=None,
created_time=IgnoreArg(),
updated_time=None)
self.m.ReplayAll()
parser.Stack.load(self.ctx, stack_id=self.stack.id,
@ -1012,14 +1014,17 @@ class StackTest(HeatTestCase):
@utils.stack_delete_after
def test_updated_time(self):
self.stack = parser.Stack(self.ctx, 'update_time_test',
self.stack = parser.Stack(self.ctx, 'updated_time_test',
parser.Template({}))
self.assertIsNone(self.stack.updated_time)
self.stack.store()
stored_time = self.stack.updated_time
self.stack.state_set(self.stack.CREATE, self.stack.IN_PROGRESS, 'test')
self.stack.create()
tmpl = {'Resources': {'R1': {'Type': 'GenericResourceType'}}}
newstack = parser.Stack(self.ctx, 'updated_time_test',
parser.Template(tmpl))
self.stack.update(newstack)
self.assertIsNotNone(self.stack.updated_time)
self.assertNotEqual(self.stack.updated_time, stored_time)
@utils.stack_delete_after
def test_delete(self):
@ -1367,7 +1372,7 @@ class StackTest(HeatTestCase):
self.stack.store()
self.assertEqual((parser.Stack.CREATE, parser.Stack.FAILED),
self.stack.state)
self.stack.update({})
self.stack.update(mock.Mock())
self.assertEqual((parser.Stack.UPDATE, parser.Stack.FAILED),
self.stack.state)