From c30193fbf5c0f2e77b09a44803246732c10e211d Mon Sep 17 00:00:00 2001 From: Eoghan Glynn Date: Wed, 15 Feb 2012 16:48:50 +0000 Subject: [PATCH] Support non-UTC timestamps in changes-since filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes nova aspect of lp 837464 Prevously only Zulu time was supported in the changes-since filter, i.e. timestamps formatted as %Y-%m-%dT%H:%M:%SZ We now support arbitrary timezones, with the offset from UTC expressed via the ISO 8601 ±hh:mm notation. Microsecond accurracy is also optionally supported in timestamps. Notes: - nova.utils.parse_isotime(), isotime() & normalized_time() are prime candidates for promotion to openstack-common, as these methods were duplicated from my corresponding glance patch: https://review.openstack.org/#change,4198 - this patch introduces a new dependency on python-iso8601, which has already been packaged for Fedora, EPEL and Ubuntu/Debian. Change-Id: I89b45f4f3d910606c578d927420f78cea94f4e3b --- nova/db/sqlalchemy/api.py | 2 +- .../api/openstack/compute/test_servers.py | 4 +- nova/tests/test_utils.py | 109 +++++++++++++++++- nova/utils.py | 23 +++- tools/pip-requires | 1 + 5 files changed, 132 insertions(+), 7 deletions(-) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index b3e558bf9001..84e2a6e445a1 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1528,7 +1528,7 @@ def instance_get_all_by_filters(context, filters): filters = filters.copy() if 'changes-since' in filters: - changes_since = filters['changes-since'] + changes_since = utils.normalize_time(filters['changes-since']) query_prefix = query_prefix.\ filter(models.Instance.updated_at > changes_since) diff --git a/nova/tests/api/openstack/compute/test_servers.py b/nova/tests/api/openstack/compute/test_servers.py index 281ba88e6a16..f692e7269f15 100644 --- a/nova/tests/api/openstack/compute/test_servers.py +++ b/nova/tests/api/openstack/compute/test_servers.py @@ -21,6 +21,7 @@ import json import urlparse import uuid +import iso8601 from lxml import etree import webob @@ -889,7 +890,8 @@ class ServersControllerTest(test.TestCase): def fake_get_all(compute_self, context, search_opts=None): self.assertNotEqual(search_opts, None) self.assertTrue('changes-since' in search_opts) - changes_since = datetime.datetime(2011, 1, 24, 17, 8, 1) + changes_since = datetime.datetime(2011, 1, 24, 17, 8, 1, + tzinfo=iso8601.iso8601.UTC) self.assertEqual(search_opts['changes-since'], changes_since) self.assertTrue('deleted' not in search_opts) return [fakes.stub_instance(100, uuid=server_uuid)] diff --git a/nova/tests/test_utils.py b/nova/tests/test_utils.py index 5da717bee39e..c60dc26a9934 100644 --- a/nova/tests/test_utils.py +++ b/nova/tests/test_utils.py @@ -15,13 +15,15 @@ # under the License. import __builtin__ -import mox import datetime import hashlib import os import StringIO import tempfile +import iso8601 +import mox + import nova from nova import exception from nova import flags @@ -700,3 +702,108 @@ class DeprecationTest(test.TestCase): h1 = utils.hash_file(flo) h2 = hashlib.sha1(data).hexdigest() self.assertEquals(h1, h2) + + +class Iso8601TimeTest(test.TestCase): + + def _instaneous(self, timestamp, yr, mon, day, hr, min, sec, micro): + self.assertEquals(timestamp.year, yr) + self.assertEquals(timestamp.month, mon) + self.assertEquals(timestamp.day, day) + self.assertEquals(timestamp.hour, hr) + self.assertEquals(timestamp.minute, min) + self.assertEquals(timestamp.second, sec) + self.assertEquals(timestamp.microsecond, micro) + + def _do_test(self, str, yr, mon, day, hr, min, sec, micro, shift): + DAY_SECONDS = 24 * 60 * 60 + timestamp = utils.parse_isotime(str) + self._instaneous(timestamp, yr, mon, day, hr, min, sec, micro) + offset = timestamp.tzinfo.utcoffset(None) + self.assertEqual(offset.seconds + offset.days * DAY_SECONDS, shift) + + def test_zulu(self): + str = '2012-02-14T20:53:07Z' + self._do_test(str, 2012, 02, 14, 20, 53, 7, 0, 0) + + def test_zulu_micros(self): + str = '2012-02-14T20:53:07.123Z' + self._do_test(str, 2012, 02, 14, 20, 53, 7, 123000, 0) + + def test_offset_east(self): + str = '2012-02-14T20:53:07+04:30' + offset = 4.5 * 60 * 60 + self._do_test(str, 2012, 02, 14, 20, 53, 7, 0, offset) + + def test_offset_east_micros(self): + str = '2012-02-14T20:53:07.42+04:30' + offset = 4.5 * 60 * 60 + self._do_test(str, 2012, 02, 14, 20, 53, 7, 420000, offset) + + def test_offset_west(self): + str = '2012-02-14T20:53:07-05:30' + offset = -5.5 * 60 * 60 + self._do_test(str, 2012, 02, 14, 20, 53, 7, 0, offset) + + def test_offset_west_micros(self): + str = '2012-02-14T20:53:07.654321-05:30' + offset = -5.5 * 60 * 60 + self._do_test(str, 2012, 02, 14, 20, 53, 7, 654321, offset) + + def test_compare(self): + zulu = utils.parse_isotime('2012-02-14T20:53:07') + east = utils.parse_isotime('2012-02-14T20:53:07-01:00') + west = utils.parse_isotime('2012-02-14T20:53:07+01:00') + self.assertTrue(east > west) + self.assertTrue(east > zulu) + self.assertTrue(zulu > west) + + def test_compare_micros(self): + zulu = utils.parse_isotime('2012-02-14T20:53:07.6544') + east = utils.parse_isotime('2012-02-14T19:53:07.654321-01:00') + west = utils.parse_isotime('2012-02-14T21:53:07.655+01:00') + self.assertTrue(east < west) + self.assertTrue(east < zulu) + self.assertTrue(zulu < west) + + def test_zulu_roundtrip(self): + str = '2012-02-14T20:53:07Z' + zulu = utils.parse_isotime(str) + self.assertEquals(zulu.tzinfo, iso8601.iso8601.UTC) + self.assertEquals(utils.isotime(zulu), str) + + def test_east_roundtrip(self): + str = '2012-02-14T20:53:07-07:00' + east = utils.parse_isotime(str) + self.assertEquals(east.tzinfo.tzname(None), '-07:00') + self.assertEquals(utils.isotime(east), str) + + def test_west_roundtrip(self): + str = '2012-02-14T20:53:07+11:30' + west = utils.parse_isotime(str) + self.assertEquals(west.tzinfo.tzname(None), '+11:30') + self.assertEquals(utils.isotime(west), str) + + def test_now_roundtrip(self): + str = utils.isotime() + now = utils.parse_isotime(str) + self.assertEquals(now.tzinfo, iso8601.iso8601.UTC) + self.assertEquals(utils.isotime(now), str) + + def test_zulu_normalize(self): + str = '2012-02-14T20:53:07Z' + zulu = utils.parse_isotime(str) + normed = utils.normalize_time(zulu) + self._instaneous(normed, 2012, 2, 14, 20, 53, 07, 0) + + def test_east_normalize(self): + str = '2012-02-14T20:53:07-07:00' + east = utils.parse_isotime(str) + normed = utils.normalize_time(east) + self._instaneous(normed, 2012, 2, 15, 03, 53, 07, 0) + + def test_west_normalize(self): + str = '2012-02-14T20:53:07+21:00' + west = utils.parse_isotime(str) + normed = utils.normalize_time(west) + self._instaneous(normed, 2012, 2, 13, 23, 53, 07, 0) diff --git a/nova/utils.py b/nova/utils.py index 96f0a57f076f..85f39dbe0734 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -44,6 +44,7 @@ from eventlet import event from eventlet import greenthread from eventlet import semaphore from eventlet.green import subprocess +import iso8601 import netaddr from nova import exception @@ -53,7 +54,7 @@ from nova.openstack.common import cfg LOG = logging.getLogger(__name__) -ISO_TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" +ISO_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S" PERFECT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" FLAGS = flags.FLAGS @@ -534,13 +535,27 @@ def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT): def isotime(at=None): - """Returns iso formatted utcnow.""" - return strtime(at, ISO_TIME_FORMAT) + """Stringify time in ISO 8601 format""" + if not at: + at = datetime.datetime.utcnow() + str = at.strftime(ISO_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): """Turn an iso formatted time back into a datetime.""" - return parse_strtime(timestr, ISO_TIME_FORMAT) + try: + return iso8601.parse_date(timestr) + except (iso8601.ParseError, TypeError) as e: + raise ValueError(e.message) + + +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 parse_mailmap(mailmap='.mailmap'): diff --git a/tools/pip-requires b/tools/pip-requires index 2b691ad73910..808a95530313 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -33,3 +33,4 @@ paramiko feedparser pycrypto Babel>=0.9.6 +iso8601>=0.1.4