Permalink
Browse files

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
  • Loading branch information...
1 parent bab694e commit f2d2e4cb0621b27b2b3f864c4352a94174174240 @markmc markmc committed Sep 3, 2012
Showing with 146 additions and 5 deletions.
  1. +126 −0 novaclient/openstack/common/timeutils.py
  2. +5 −4 novaclient/v1_1/shell.py
  3. +1 −1 openstack-common.conf
  4. +13 −0 tests/v1_1/test_shell.py
  5. +1 −0 tools/pip-requires
@@ -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'])
View
@@ -22,6 +22,7 @@
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)
View
@@ -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
View
@@ -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 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 @@ def tearDown(self):
#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 @@ def test_usage_list(self):
'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')
View
@@ -1,4 +1,5 @@
argparse
httplib2
+iso8601>=0.1.4
prettytable>=0.6,<0.7
simplejson

0 comments on commit f2d2e4c

Please sign in to comment.