From f2d2e4cb0621b27b2b3f864c4352a94174174240 Mon Sep 17 00:00:00 2001 From: Mark McLoughlin Date: Mon, 3 Sep 2012 20:06:09 +0100 Subject: [PATCH] Fix usage-list date range to use UTC time Fixes bug #1045456 The date range in Nova's os-simple-tenant-usage is expected to be in UTC time since launch/termination dates are stored in the DB in UTC time and we use the client supplied parameters to query DB without conversion. Switch from using datetime.today() to datetime.utcnow() to fix the issue. Add a test for the default date range. Import timeutils from openstack-common so we can control the return value of utcnow() in the tests. Change-Id: Iac77e3a4cc9561714d1492c54cef931f9764531e --- novaclient/openstack/common/timeutils.py | 126 +++++++++++++++++++++++ novaclient/v1_1/shell.py | 9 +- openstack-common.conf | 2 +- tests/v1_1/test_shell.py | 13 +++ tools/pip-requires | 1 + 5 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 novaclient/openstack/common/timeutils.py diff --git a/novaclient/openstack/common/timeutils.py b/novaclient/openstack/common/timeutils.py new file mode 100644 index 000000000..c4f6cf049 --- /dev/null +++ b/novaclient/openstack/common/timeutils.py @@ -0,0 +1,126 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +Time related utilities and helper functions. +""" + +import calendar +import datetime + +import iso8601 + + +TIME_FORMAT = "%Y-%m-%dT%H:%M:%S" +PERFECT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" + + +def isotime(at=None): + """Stringify time in ISO 8601 format""" + if not at: + at = utcnow() + str = at.strftime(TIME_FORMAT) + tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC' + str += ('Z' if tz == 'UTC' else tz) + return str + + +def parse_isotime(timestr): + """Parse time from ISO 8601 format""" + try: + return iso8601.parse_date(timestr) + except iso8601.ParseError as e: + raise ValueError(e.message) + except TypeError as e: + raise ValueError(e.message) + + +def strtime(at=None, fmt=PERFECT_TIME_FORMAT): + """Returns formatted utcnow.""" + if not at: + at = utcnow() + return at.strftime(fmt) + + +def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT): + """Turn a formatted time back into a datetime.""" + return datetime.datetime.strptime(timestr, fmt) + + +def normalize_time(timestamp): + """Normalize time in arbitrary timezone to UTC""" + offset = timestamp.utcoffset() + return timestamp.replace(tzinfo=None) - offset if offset else timestamp + + +def is_older_than(before, seconds): + """Return True if before is older than seconds.""" + return utcnow() - before > datetime.timedelta(seconds=seconds) + + +def utcnow_ts(): + """Timestamp version of our utcnow function.""" + return calendar.timegm(utcnow().timetuple()) + + +def utcnow(): + """Overridable version of utils.utcnow.""" + if utcnow.override_time: + return utcnow.override_time + return datetime.datetime.utcnow() + + +utcnow.override_time = None + + +def set_time_override(override_time=datetime.datetime.utcnow()): + """Override utils.utcnow to return a constant time.""" + utcnow.override_time = override_time + + +def advance_time_delta(timedelta): + """Advance overridden time using a datetime.timedelta.""" + assert(not utcnow.override_time is None) + utcnow.override_time += timedelta + + +def advance_time_seconds(seconds): + """Advance overridden time by seconds.""" + advance_time_delta(datetime.timedelta(0, seconds)) + + +def clear_time_override(): + """Remove the overridden time.""" + utcnow.override_time = None + + +def marshall_now(now=None): + """Make an rpc-safe datetime with microseconds. + + Note: tzinfo is stripped, but not required for relative times.""" + if not now: + now = utcnow() + return dict(day=now.day, month=now.month, year=now.year, hour=now.hour, + minute=now.minute, second=now.second, + microsecond=now.microsecond) + + +def unmarshall_time(tyme): + """Unmarshall a datetime dict.""" + return datetime.datetime(day=tyme['day'], month=tyme['month'], + year=tyme['year'], hour=tyme['hour'], minute=tyme['minute'], + second=tyme['second'], microsecond=tyme['microsecond']) diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py index d7f047ab0..f62dacf88 100644 --- a/novaclient/v1_1/shell.py +++ b/novaclient/v1_1/shell.py @@ -22,6 +22,7 @@ import sys import time from novaclient import exceptions +from novaclient.openstack.common import timeutils from novaclient import utils from novaclient.v1_1 import servers @@ -1474,17 +1475,17 @@ def do_usage_list(cs, args): rows = ["Tenant ID", "Instances", "RAM MB-Hours", "CPU Hours", "Disk GB-Hours"] + now = timeutils.utcnow() + if args.start: start = datetime.datetime.strptime(args.start, dateformat) else: - start = (datetime.datetime.today() - - datetime.timedelta(weeks=4)) + start = now - datetime.timedelta(weeks=4) if args.end: end = datetime.datetime.strptime(args.end, dateformat) else: - end = (datetime.datetime.today() + - datetime.timedelta(days=1)) + end = now + datetime.timedelta(days=1) def simplify_usage(u): simplerows = map(lambda x: x.lower().replace(" ", "_"), rows) diff --git a/openstack-common.conf b/openstack-common.conf index eaa804595..76f08056e 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,7 +1,7 @@ [DEFAULT] # The list of modules to copy from openstack-common -modules=setup +modules=setup,timeutils # The base module to hold the copy of openstack.common base=novaclient diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py index 896959758..b4de2f9cc 100644 --- a/tests/v1_1/test_shell.py +++ b/tests/v1_1/test_shell.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime import os import mock import sys @@ -23,6 +24,7 @@ import tempfile import novaclient.shell import novaclient.client from novaclient import exceptions +from novaclient.openstack.common import timeutils from tests.v1_1 import fakes from tests import utils @@ -59,6 +61,8 @@ class ShellTest(utils.TestCase): #HACK(bcwaldon): replace this when we start using stubs novaclient.client.get_client_class = self.old_get_client_class + timeutils.clear_time_override() + def run_command(self, cmd): self.shell.main(cmd.split()) @@ -353,6 +357,15 @@ class ShellTest(utils.TestCase): 'end=2005-02-01T00:00:00&' + 'detailed=1') + def test_usage_list_no_args(self): + timeutils.set_time_override(datetime.datetime(2005, 2, 1, 0, 0)) + self.run_command('usage-list') + self.assert_called('GET', + '/os-simple-tenant-usage?' + + 'start=2005-01-04T00:00:00&' + + 'end=2005-02-02T00:00:00&' + + 'detailed=1') + def test_flavor_delete(self): self.run_command("flavor-delete flavordelete") self.assert_called('DELETE', '/flavors/flavordelete') diff --git a/tools/pip-requires b/tools/pip-requires index 97b9a58c1..2c409b3c2 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -1,4 +1,5 @@ argparse httplib2 +iso8601>=0.1.4 prettytable>=0.6,<0.7 simplejson