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