Restore timezone information in API response

When oslo_utils isotime was deprecated, we switch to isoformat and lost
the timezone information. This switches back to a function which put the
Z suffix indicating UTC. It's a potential backward incompatible change,
but presumably better than sending incomplete information.

Change-Id: I78ab2dd025f1d00d4afaf72070e481aa8e09c5e8
Closes-Bug: #1607562
This commit is contained in:
Thomas Herve 2016-09-07 16:49:40 +02:00
parent 49226daee0
commit f655726560
4 changed files with 58 additions and 36 deletions

View File

@ -85,3 +85,13 @@ def round_to_seconds(dt):
rounding = 1 rounding = 1
return dt + datetime.timedelta(0, rounding, return dt + datetime.timedelta(0, rounding,
-dt.microsecond) -dt.microsecond)
def isotime(at):
"""Stringify UTC time in ISO 8601 format.
:param at: Timestamp in UTC to format.
"""
if at is None:
return None
return at.strftime('%Y-%m-%dT%H:%M:%SZ')

View File

@ -21,6 +21,7 @@ from heat.common.i18n import _
from heat.common.i18n import _LE from heat.common.i18n import _LE
from heat.common import param_utils from heat.common import param_utils
from heat.common import template_format from heat.common import template_format
from heat.common import timeutils as heat_timeutils
from heat.engine import constraints as constr from heat.engine import constraints as constr
from heat.rpc import api as rpc_api from heat.rpc import api as rpc_api
@ -210,13 +211,14 @@ def format_stack(stack, preview=False, resolve_outputs=True):
Return a representation of the given stack that matches the API output Return a representation of the given stack that matches the API output
expectations. expectations.
""" """
updated_time = stack.updated_time and stack.updated_time.isoformat() updated_time = heat_timeutils.isotime(stack.updated_time)
created_time = stack.created_time or timeutils.utcnow() created_time = heat_timeutils.isotime(stack.created_time or
deleted_time = stack.deleted_time and stack.deleted_time.isoformat() timeutils.utcnow())
deleted_time = heat_timeutils.isotime(stack.deleted_time)
info = { info = {
rpc_api.STACK_NAME: stack.name, rpc_api.STACK_NAME: stack.name,
rpc_api.STACK_ID: dict(stack.identifier()), rpc_api.STACK_ID: dict(stack.identifier()),
rpc_api.STACK_CREATION_TIME: created_time.isoformat(), rpc_api.STACK_CREATION_TIME: created_time,
rpc_api.STACK_UPDATED_TIME: updated_time, rpc_api.STACK_UPDATED_TIME: updated_time,
rpc_api.STACK_DELETION_TIME: deleted_time, rpc_api.STACK_DELETION_TIME: deleted_time,
rpc_api.STACK_NOTIFICATION_TOPICS: [], # TODO(therve) Not implemented rpc_api.STACK_NOTIFICATION_TOPICS: [], # TODO(therve) Not implemented
@ -255,9 +257,9 @@ def format_stack_db_object(stack):
Given a stack versioned db object, return a representation of the given Given a stack versioned db object, return a representation of the given
stack for a stack listing. stack for a stack listing.
""" """
updated_time = stack.updated_at and stack.updated_at.isoformat() updated_time = heat_timeutils.isotime(stack.updated_at)
created_time = stack.created_at created_time = heat_timeutils.isotime(stack.created_at)
deleted_time = stack.deleted_at and stack.deleted_at.isoformat() deleted_time = heat_timeutils.isotime(stack.deleted_at)
tags = None tags = None
if stack.tags: if stack.tags:
@ -269,7 +271,7 @@ def format_stack_db_object(stack):
rpc_api.STACK_ACTION: stack.action, rpc_api.STACK_ACTION: stack.action,
rpc_api.STACK_STATUS: stack.status, rpc_api.STACK_STATUS: stack.status,
rpc_api.STACK_STATUS_DATA: stack.status_reason, rpc_api.STACK_STATUS_DATA: stack.status_reason,
rpc_api.STACK_CREATION_TIME: created_time.isoformat(), rpc_api.STACK_CREATION_TIME: created_time,
rpc_api.STACK_UPDATED_TIME: updated_time, rpc_api.STACK_UPDATED_TIME: updated_time,
rpc_api.STACK_DELETION_TIME: deleted_time, rpc_api.STACK_DELETION_TIME: deleted_time,
rpc_api.STACK_OWNER: stack.username, rpc_api.STACK_OWNER: stack.username,
@ -330,9 +332,9 @@ def format_stack_resource(resource, detail=True, with_props=False,
Return a representation of the given resource that matches the API output Return a representation of the given resource that matches the API output
expectations. expectations.
""" """
created_time = resource.created_time and resource.created_time.isoformat() created_time = heat_timeutils.isotime(resource.created_time)
last_updated_time = (resource.updated_time and last_updated_time = heat_timeutils.isotime(
resource.updated_time.isoformat()) or created_time resource.updated_time or resource.created_time)
res = { res = {
rpc_api.RES_UPDATED_TIME: last_updated_time, rpc_api.RES_UPDATED_TIME: last_updated_time,
rpc_api.RES_CREATION_TIME: created_time, rpc_api.RES_CREATION_TIME: created_time,
@ -386,7 +388,7 @@ def format_event(event, stack_identifier, root_stack_identifier=None):
rpc_api.EVENT_ID: dict(event.identifier(stack_identifier)), rpc_api.EVENT_ID: dict(event.identifier(stack_identifier)),
rpc_api.EVENT_STACK_ID: dict(stack_identifier), rpc_api.EVENT_STACK_ID: dict(stack_identifier),
rpc_api.EVENT_STACK_NAME: stack_identifier.stack_name, rpc_api.EVENT_STACK_NAME: stack_identifier.stack_name,
rpc_api.EVENT_TIMESTAMP: event.created_at.isoformat(), rpc_api.EVENT_TIMESTAMP: heat_timeutils.isotime(event.created_at),
rpc_api.EVENT_RES_NAME: event.resource_name, rpc_api.EVENT_RES_NAME: event.resource_name,
rpc_api.EVENT_RES_PHYSICAL_ID: event.physical_resource_id, rpc_api.EVENT_RES_PHYSICAL_ID: event.physical_resource_id,
rpc_api.EVENT_RES_ACTION: event.resource_action, rpc_api.EVENT_RES_ACTION: event.resource_action,
@ -411,7 +413,7 @@ def format_notification_body(stack):
else: else:
state = 'Unknown' state = 'Unknown'
updated_at = stack.updated_time and stack.updated_time.isoformat() updated_at = heat_timeutils.isotime(stack.updated_time)
result = { result = {
rpc_api.NOTIFY_TENANT_ID: stack.context.tenant_id, rpc_api.NOTIFY_TENANT_ID: stack.context.tenant_id,
rpc_api.NOTIFY_USER_ID: stack.context.username, rpc_api.NOTIFY_USER_ID: stack.context.username,
@ -423,7 +425,7 @@ def format_notification_body(stack):
rpc_api.NOTIFY_STACK_NAME: stack.name, rpc_api.NOTIFY_STACK_NAME: stack.name,
rpc_api.NOTIFY_STATE: state, rpc_api.NOTIFY_STATE: state,
rpc_api.NOTIFY_STATE_REASON: stack.status_reason, rpc_api.NOTIFY_STATE_REASON: stack.status_reason,
rpc_api.NOTIFY_CREATE_AT: stack.created_time.isoformat(), rpc_api.NOTIFY_CREATE_AT: heat_timeutils.isotime(stack.created_time),
rpc_api.NOTIFY_DESCRIPTION: stack.t[stack.t.DESCRIPTION], rpc_api.NOTIFY_DESCRIPTION: stack.t[stack.t.DESCRIPTION],
rpc_api.NOTIFY_TAGS: stack.tags, rpc_api.NOTIFY_TAGS: stack.tags,
rpc_api.NOTIFY_UPDATE_AT: updated_at rpc_api.NOTIFY_UPDATE_AT: updated_at
@ -433,14 +435,15 @@ def format_notification_body(stack):
def format_watch(watch): def format_watch(watch):
updated_at = watch.updated_at or timeutils.utcnow() updated_time = heat_timeutils.isotime(watch.updated_at or
timeutils.utcnow())
result = { result = {
rpc_api.WATCH_ACTIONS_ENABLED: watch.rule.get( rpc_api.WATCH_ACTIONS_ENABLED: watch.rule.get(
rpc_api.RULE_ACTIONS_ENABLED), rpc_api.RULE_ACTIONS_ENABLED),
rpc_api.WATCH_ALARM_ACTIONS: watch.rule.get( rpc_api.WATCH_ALARM_ACTIONS: watch.rule.get(
rpc_api.RULE_ALARM_ACTIONS), rpc_api.RULE_ALARM_ACTIONS),
rpc_api.WATCH_TOPIC: watch.rule.get(rpc_api.RULE_TOPIC), rpc_api.WATCH_TOPIC: watch.rule.get(rpc_api.RULE_TOPIC),
rpc_api.WATCH_UPDATED_TIME: updated_at.isoformat(), rpc_api.WATCH_UPDATED_TIME: updated_time,
rpc_api.WATCH_DESCRIPTION: watch.rule.get(rpc_api.RULE_DESCRIPTION), rpc_api.WATCH_DESCRIPTION: watch.rule.get(rpc_api.RULE_DESCRIPTION),
rpc_api.WATCH_NAME: watch.name, rpc_api.WATCH_NAME: watch.name,
rpc_api.WATCH_COMPARISON: watch.rule.get(rpc_api.RULE_COMPARISON), rpc_api.WATCH_COMPARISON: watch.rule.get(rpc_api.RULE_COMPARISON),
@ -456,8 +459,9 @@ def format_watch(watch):
rpc_api.WATCH_STATE_REASON: watch.rule.get(rpc_api.RULE_STATE_REASON), rpc_api.WATCH_STATE_REASON: watch.rule.get(rpc_api.RULE_STATE_REASON),
rpc_api.WATCH_STATE_REASON_DATA: rpc_api.WATCH_STATE_REASON_DATA:
watch.rule.get(rpc_api.RULE_STATE_REASON_DATA), watch.rule.get(rpc_api.RULE_STATE_REASON_DATA),
rpc_api.WATCH_STATE_UPDATED_TIME: watch.rule.get( rpc_api.WATCH_STATE_UPDATED_TIME: heat_timeutils.isotime(
rpc_api.RULE_STATE_UPDATED_TIME, timeutils.utcnow()).isoformat(), watch.rule.get(rpc_api.RULE_STATE_UPDATED_TIME,
timeutils.utcnow())),
rpc_api.WATCH_STATE_VALUE: watch.state, rpc_api.WATCH_STATE_VALUE: watch.state,
rpc_api.WATCH_STATISTIC: watch.rule.get(rpc_api.RULE_STATISTIC), rpc_api.WATCH_STATISTIC: watch.rule.get(rpc_api.RULE_STATISTIC),
rpc_api.WATCH_THRESHOLD: watch.rule.get(rpc_api.RULE_THRESHOLD), rpc_api.WATCH_THRESHOLD: watch.rule.get(rpc_api.RULE_THRESHOLD),
@ -484,7 +488,7 @@ def format_watch_data(wd, rule_names):
result = { result = {
rpc_api.WATCH_DATA_ALARM: rule_names.get(wd.watch_rule_id), rpc_api.WATCH_DATA_ALARM: rule_names.get(wd.watch_rule_id),
rpc_api.WATCH_DATA_METRIC: metric_name, rpc_api.WATCH_DATA_METRIC: metric_name,
rpc_api.WATCH_DATA_TIME: wd.created_at.isoformat(), rpc_api.WATCH_DATA_TIME: heat_timeutils.isotime(wd.created_at),
rpc_api.WATCH_DATA_NAMESPACE: namespace, rpc_api.WATCH_DATA_NAMESPACE: namespace,
rpc_api.WATCH_DATA: metric_data rpc_api.WATCH_DATA: metric_data
} }
@ -568,7 +572,8 @@ def format_software_config(sc, detail=True):
rpc_api.SOFTWARE_CONFIG_ID: sc.id, rpc_api.SOFTWARE_CONFIG_ID: sc.id,
rpc_api.SOFTWARE_CONFIG_NAME: sc.name, rpc_api.SOFTWARE_CONFIG_NAME: sc.name,
rpc_api.SOFTWARE_CONFIG_GROUP: sc.group, rpc_api.SOFTWARE_CONFIG_GROUP: sc.group,
rpc_api.SOFTWARE_CONFIG_CREATION_TIME: sc.created_at.isoformat() rpc_api.SOFTWARE_CONFIG_CREATION_TIME:
heat_timeutils.isotime(sc.created_at)
} }
if detail: if detail:
result[rpc_api.SOFTWARE_CONFIG_CONFIG] = sc.config['config'] result[rpc_api.SOFTWARE_CONFIG_CONFIG] = sc.config['config']
@ -590,11 +595,12 @@ def format_software_deployment(sd):
rpc_api.SOFTWARE_DEPLOYMENT_STATUS: sd.status, rpc_api.SOFTWARE_DEPLOYMENT_STATUS: sd.status,
rpc_api.SOFTWARE_DEPLOYMENT_STATUS_REASON: sd.status_reason, rpc_api.SOFTWARE_DEPLOYMENT_STATUS_REASON: sd.status_reason,
rpc_api.SOFTWARE_DEPLOYMENT_CONFIG_ID: sd.config.id, rpc_api.SOFTWARE_DEPLOYMENT_CONFIG_ID: sd.config.id,
rpc_api.SOFTWARE_DEPLOYMENT_CREATION_TIME: sd.created_at.isoformat(), rpc_api.SOFTWARE_DEPLOYMENT_CREATION_TIME:
heat_timeutils.isotime(sd.created_at),
} }
if sd.updated_at: if sd.updated_at:
result[rpc_api.SOFTWARE_DEPLOYMENT_UPDATED_TIME] = ( result[rpc_api.SOFTWARE_DEPLOYMENT_UPDATED_TIME] = (
sd.updated_at.isoformat()) heat_timeutils.isotime(sd.updated_at))
return result return result
@ -607,7 +613,8 @@ def format_snapshot(snapshot):
rpc_api.SNAPSHOT_STATUS: snapshot.status, rpc_api.SNAPSHOT_STATUS: snapshot.status,
rpc_api.SNAPSHOT_STATUS_REASON: snapshot.status_reason, rpc_api.SNAPSHOT_STATUS_REASON: snapshot.status_reason,
rpc_api.SNAPSHOT_DATA: snapshot.data, rpc_api.SNAPSHOT_DATA: snapshot.data,
rpc_api.SNAPSHOT_CREATION_TIME: snapshot.created_at.isoformat(), rpc_api.SNAPSHOT_CREATION_TIME:
heat_timeutils.isotime(snapshot.created_at),
} }
return result return result

View File

@ -21,6 +21,7 @@ import six
from heat.common import exception from heat.common import exception
from heat.common import template_format from heat.common import template_format
from heat.common import timeutils as heat_timeutils
from heat.engine import api from heat.engine import api
from heat.engine import event from heat.engine import event
from heat.engine import parameters from heat.engine import parameters
@ -94,9 +95,9 @@ class FormatTest(common.HeatTestCase):
formatted = api.format_stack_resource(res, False) formatted = api.format_stack_resource(res, False)
self.assertEqual(resource_keys, set(formatted.keys())) self.assertEqual(resource_keys, set(formatted.keys()))
self.assertEqual(self.stack.created_time.isoformat(), self.assertEqual(heat_timeutils.isotime(self.stack.created_time),
formatted[rpc_api.RES_CREATION_TIME]) formatted[rpc_api.RES_CREATION_TIME])
self.assertEqual(self.stack.updated_time.isoformat(), self.assertEqual(heat_timeutils.isotime(self.stack.updated_time),
formatted[rpc_api.RES_UPDATED_TIME]) formatted[rpc_api.RES_UPDATED_TIME])
self.assertEqual(res.INIT, formatted[rpc_api.RES_ACTION]) self.assertEqual(res.INIT, formatted[rpc_api.RES_ACTION])
@ -347,7 +348,7 @@ class FormatTest(common.HeatTestCase):
'stacks/test_stack/' + self.stack.id) 'stacks/test_stack/' + self.stack.id)
expected_stack_info = { expected_stack_info = {
'capabilities': [], 'capabilities': [],
'creation_time': '1970-01-01T00:00:00', 'creation_time': '1970-01-01T00:00:00Z',
'deletion_time': None, 'deletion_time': None,
'description': 'No description', 'description': 'No description',
'disable_rollback': True, 'disable_rollback': True,
@ -387,7 +388,7 @@ class FormatTest(common.HeatTestCase):
self.stack.updated_time = datetime(1970, 1, 1) self.stack.updated_time = datetime(1970, 1, 1)
info = api.format_stack(self.stack) info = api.format_stack(self.stack)
self.assertEqual('1970-01-01T00:00:00', info['updated_time']) self.assertEqual('1970-01-01T00:00:00Z', info['updated_time'])
@mock.patch.object(api, 'format_stack_outputs') @mock.patch.object(api, 'format_stack_outputs')
def test_format_stack_adds_outputs(self, mock_fmt_outputs): def test_format_stack_adds_outputs(self, mock_fmt_outputs):
@ -1053,7 +1054,8 @@ class FormatSoftwareConfigDeploymentTest(common.HeatTestCase):
self.assertEqual([{'name': 'result'}], result['outputs']) self.assertEqual([{'name': 'result'}], result['outputs'])
self.assertEqual([{'name': 'result'}], result['outputs']) self.assertEqual([{'name': 'result'}], result['outputs'])
self.assertEqual({}, result['options']) self.assertEqual({}, result['options'])
self.assertEqual(self.now.isoformat(), result['creation_time']) self.assertEqual(heat_timeutils.isotime(self.now),
result['creation_time'])
def test_format_software_config_none(self): def test_format_software_config_none(self):
self.assertIsNone(api.format_software_config(None)) self.assertIsNone(api.format_software_config(None))
@ -1070,8 +1072,10 @@ class FormatSoftwareConfigDeploymentTest(common.HeatTestCase):
self.assertEqual(deployment.action, result['action']) self.assertEqual(deployment.action, result['action'])
self.assertEqual(deployment.status, result['status']) self.assertEqual(deployment.status, result['status'])
self.assertEqual(deployment.status_reason, result['status_reason']) self.assertEqual(deployment.status_reason, result['status_reason'])
self.assertEqual(self.now.isoformat(), result['creation_time']) self.assertEqual(heat_timeutils.isotime(self.now),
self.assertEqual(self.now.isoformat(), result['updated_time']) result['creation_time'])
self.assertEqual(heat_timeutils.isotime(self.now),
result['updated_time'])
def test_format_software_deployment_none(self): def test_format_software_deployment_none(self):
self.assertIsNone(api.format_software_deployment(None)) self.assertIsNone(api.format_software_deployment(None))

View File

@ -14,6 +14,7 @@
import mock import mock
from oslo_utils import timeutils from oslo_utils import timeutils
from heat.common import timeutils as heat_timeutils
from heat.engine import notification from heat.engine import notification
from heat.tests import common from heat.tests import common
from heat.tests import utils from heat.tests import utils
@ -54,11 +55,11 @@ class StackTest(common.HeatTestCase):
'stack_identity': 'hay-are-en', 'stack_identity': 'hay-are-en',
'stack_name': 'fred', 'stack_name': 'fred',
'tenant_id': 'test_tenant_id', 'tenant_id': 'test_tenant_id',
'create_at': created_time.isoformat(), 'create_at': heat_timeutils.isotime(created_time),
'state': 'x_f', 'state': 'x_f',
'description': 'for test', 'description': 'for test',
'tags': ['tag1', 'tag2'], 'tags': ['tag1', 'tag2'],
'updated_at': updated_time.isoformat()}) 'updated_at': heat_timeutils.isotime(updated_time)})
class AutoScaleTest(common.HeatTestCase): class AutoScaleTest(common.HeatTestCase):
@ -106,10 +107,10 @@ class AutoScaleTest(common.HeatTestCase):
'stack_identity': 'hay-are-en', 'stack_identity': 'hay-are-en',
'stack_name': 'fred', 'stack_name': 'fred',
'tenant_id': 'test_tenant_id', 'tenant_id': 'test_tenant_id',
'create_at': stack.created_time.isoformat(), 'create_at': heat_timeutils.isotime(stack.created_time),
'description': 'for test', 'description': 'for test',
'tags': ['tag1', 'tag2'], 'tags': ['tag1', 'tag2'],
'updated_at': stack.updated_time.isoformat(), 'updated_at': heat_timeutils.isotime(stack.updated_time),
'state': 'x_f', 'adjustment_type': 'y', 'state': 'x_f', 'adjustment_type': 'y',
'groupname': 'c', 'capacity': '5', 'groupname': 'c', 'capacity': '5',
'message': 'fred', 'adjustment': 'x'}) 'message': 'fred', 'adjustment': 'x'})
@ -132,10 +133,10 @@ class AutoScaleTest(common.HeatTestCase):
'stack_identity': 'hay-are-en', 'stack_identity': 'hay-are-en',
'stack_name': 'fred', 'stack_name': 'fred',
'tenant_id': 'test_tenant_id', 'tenant_id': 'test_tenant_id',
'create_at': stack.created_time.isoformat(), 'create_at': heat_timeutils.isotime(stack.created_time),
'description': 'for test', 'description': 'for test',
'tags': ['tag1', 'tag2'], 'tags': ['tag1', 'tag2'],
'updated_at': stack.updated_time.isoformat(), 'updated_at': heat_timeutils.isotime(stack.updated_time),
'state': 'x_f', 'adjustment_type': 'y', 'state': 'x_f', 'adjustment_type': 'y',
'groupname': 'c', 'capacity': '5', 'groupname': 'c', 'capacity': '5',
'message': 'error', 'adjustment': 'x'}) 'message': 'error', 'adjustment': 'x'})