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:
parent
ac8ef2a6ba
commit
ce690963e7
|
@ -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]
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue