Make Horizon timezone-aware.

This systematically replaces anyplace that deals with dates or
times in Horizon with Django's timezone-aware machinery, and
enables timezone support in settings.

The assumption is that the server time should *always* be UTC.

TO DO: Add a setting for allowing the user to change their preferred
timezone display and add timezone indicators anywhere times are
displayed to the user.

Implements blueprint timezones. Also fixes bug 927974.

Change-Id: I5e462ba86e64b97b46873a017f87f328acee1b1d
This commit is contained in:
Gabriel Hurley 2012-06-21 19:33:42 -07:00
parent 810bf6340d
commit 6174eae5ae
12 changed files with 72 additions and 90 deletions

View File

@ -94,7 +94,6 @@ class VolumeViewTests(test.TestCase):
"7f2293ff3775</dd>", 1, 200)
self.assertContains(res, "<dd>Available</dd>", 1, 200)
self.assertContains(res, "<dd>40 GB</dd>", 1, 200)
self.assertContains(res, "<dd>04/01/12 at 10:30:00</dd>", 1, 200)
self.assertContains(res, "<a href=\"/nova/instances_and_volumes/"
"instances/1/detail\">server_1</a>", 1, 200)

View File

@ -22,7 +22,8 @@ import datetime
from django import http
from django.core.urlresolvers import reverse
from mox import IsA
from django.utils import timezone
from mox import IsA, Func
from horizon import api
from horizon import test
@ -34,13 +35,12 @@ INDEX_URL = reverse('horizon:nova:overview:index')
class UsageViewTests(test.TestCase):
def test_usage(self):
now = datetime.datetime.utcnow()
now = timezone.now()
usage_obj = api.nova.Usage(self.usages.first())
self.mox.StubOutWithMock(api, 'usage_get')
api.usage_get(IsA(http.HttpRequest), self.tenant.id,
datetime.datetime(now.year, now.month, 1, 0, 0, 0),
datetime.datetime(now.year, now.month, now.day, now.hour,
now.minute, now.second)) \
Func(usage.almost_now)) \
.AndReturn(usage_obj)
self.mox.ReplayAll()
@ -50,15 +50,14 @@ class UsageViewTests(test.TestCase):
self.assertContains(res, 'form-horizontal')
def test_usage_csv(self):
now = datetime.datetime.utcnow()
now = timezone.now()
usage_obj = api.nova.Usage(self.usages.first())
self.mox.StubOutWithMock(api, 'usage_get')
timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0)
api.usage_get(IsA(http.HttpRequest),
self.tenant.id,
timestamp,
datetime.datetime(now.year, now.month, now.day, now.hour,
now.minute, now.second)) \
Func(usage.almost_now)) \
.AndReturn(usage_obj)
self.mox.ReplayAll()
@ -68,14 +67,13 @@ class UsageViewTests(test.TestCase):
self.assertTrue(isinstance(res.context['usage'], usage.TenantUsage))
def test_usage_exception(self):
now = datetime.datetime.utcnow()
now = timezone.now()
self.mox.StubOutWithMock(api, 'usage_get')
timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0)
api.usage_get(IsA(http.HttpRequest),
self.tenant.id,
timestamp,
datetime.datetime(now.year, now.month, now.day, now.hour,
now.minute, now.second)) \
Func(usage.almost_now)) \
.AndRaise(self.exceptions.nova)
self.mox.ReplayAll()
@ -84,15 +82,14 @@ class UsageViewTests(test.TestCase):
self.assertEqual(res.context['usage'].usage_list, [])
def test_usage_default_tenant(self):
now = datetime.datetime.utcnow()
now = timezone.now()
usage_obj = api.nova.Usage(self.usages.first())
self.mox.StubOutWithMock(api, 'usage_get')
timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0)
api.usage_get(IsA(http.HttpRequest),
self.tenant.id,
timestamp,
datetime.datetime(now.year, now.month, now.day, now.hour,
now.minute, now.second)) \
Func(usage.almost_now)) \
.AndReturn(usage_obj)
self.mox.ReplayAll()

View File

@ -1,5 +1,5 @@
{% extends 'nova/base.html' %}
{% load i18n parse_date sizeformat %}
{% load i18n %}
{% block title %}Instance Overview{% endblock %}
{% block page_header %}

View File

@ -22,7 +22,8 @@ import datetime
from django import http
from django.core.urlresolvers import reverse
from mox import IsA
from django.utils import timezone
from mox import IsA, Func
from horizon import api
from horizon import test
@ -37,14 +38,13 @@ class UsageViewTests(test.BaseAdminViewTests):
@test.create_stubs({api: ('usage_list',),
api.keystone: ('tenant_list',)})
def test_usage(self):
now = datetime.datetime.utcnow()
now = timezone.now()
usage_obj = api.nova.Usage(self.usages.first())
api.keystone.tenant_list(IsA(http.HttpRequest), admin=True) \
.AndReturn(self.tenants.list())
api.usage_list(IsA(http.HttpRequest),
datetime.datetime(now.year, now.month, 1, 0, 0, 0),
datetime.datetime(now.year, now.month, now.day, now.hour,
now.minute, now.second)) \
Func(usage.almost_now)) \
.AndReturn([usage_obj])
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:syspanel:overview:index'))
@ -66,14 +66,13 @@ class UsageViewTests(test.BaseAdminViewTests):
@test.create_stubs({api: ('usage_list',),
api.keystone: ('tenant_list',)})
def test_usage_csv(self):
now = datetime.datetime.utcnow()
now = timezone.now()
usage_obj = api.nova.Usage(self.usages.first())
api.keystone.tenant_list(IsA(http.HttpRequest), admin=True) \
.AndReturn(self.tenants.list())
api.usage_list(IsA(http.HttpRequest),
datetime.datetime(now.year, now.month, 1, 0, 0, 0),
datetime.datetime(now.year, now.month, now.day, now.hour,
now.minute, now.second)) \
Func(usage.almost_now)) \
.AndReturn([usage_obj])
self.mox.ReplayAll()
csv_url = reverse('horizon:syspanel:overview:index') + "?format=csv"

View File

@ -18,13 +18,12 @@
# License for the specific language governing permissions and limitations
# under the License.
from datetime import date
import logging
from django import forms
from django.forms.forms import NON_FIELD_ERRORS
from django.core.urlresolvers import reverse
from django.utils import dates
from django.utils import dates, timezone
from horizon import exceptions
@ -119,6 +118,7 @@ class DateForm(forms.Form):
def __init__(self, *args, **kwargs):
super(DateForm, self).__init__(*args, **kwargs)
years = [(year, year) for year in xrange(2009, date.today().year + 1)]
years = [(year, year) for year
in xrange(2009, timezone.now().year + 1)]
years.reverse()
self.fields['year'].choices = years

View File

@ -22,56 +22,33 @@
Template tags for parsing date strings.
"""
import datetime
from datetime import datetime
from django import template
from dateutil import tz
from django.utils import timezone
register = template.Library()
def _parse_datetime(dtstr):
if not dtstr:
return "None"
fmts = ["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%d %H:%M:%S.%f",
"%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"]
for fmt in fmts:
try:
return datetime.datetime.strptime(dtstr, fmt)
except:
pass
class ParseDateNode(template.Node):
def render(self, context):
"""Turn an iso formatted time back into a datetime."""
if not context:
return "None"
date_obj = _parse_datetime(context)
return date_obj.strftime("%m/%d/%y at %H:%M:%S")
def render(self, datestring):
"""
Parses a date-like input string into a timezone aware Python datetime.
"""
formats = ["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%d %H:%M:%S.%f",
"%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"]
if datestring:
for format in formats:
try:
parsed = datetime.strptime(datestring, format)
if not timezone.is_aware(parsed):
parsed = timezone.make_aware(parsed, timezone.utc)
return parsed
except:
pass
return None
@register.filter(name='parse_date')
def parse_date(value):
return ParseDateNode().render(value)
@register.filter(name='parse_datetime')
def parse_datetime(value):
return _parse_datetime(value)
@register.filter(name='parse_local_datetime')
def parse_local_datetime(value):
dt = _parse_datetime(value)
local_tz = tz.tzlocal()
utc = tz.gettz('UTC')
local_dt = dt.replace(tzinfo=utc)
return local_dt.astimezone(local_tz)
@register.filter(name='pretty_date')
def pretty_date(value):
if not value:
return "None"
return value.strftime("%d/%m/%y at %H:%M:%S")

View File

@ -29,6 +29,10 @@ ROOT_PATH = os.path.dirname(os.path.abspath(__file__))
DEBUG = True
TESTSERVER = 'http://testserver'
USE_I18N = True
USE_L10N = True
USE_TZ = True
DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3'}}
INSTALLED_APPS = (

View File

@ -14,6 +14,6 @@
# License for the specific language governing permissions and limitations
# under the License.
from .base import BaseUsage, TenantUsage, GlobalUsage
from .base import BaseUsage, TenantUsage, GlobalUsage, almost_now
from .views import UsageView
from .tables import BaseUsageTable, TenantUsageTable, GlobalUsageTable

View File

@ -1,11 +1,12 @@
from __future__ import division
from calendar import monthrange
import datetime
import logging
from dateutil.relativedelta import relativedelta
from django.contrib import messages
from django.utils.translation import ugettext as _
from django.utils import timezone
from horizon import api
from horizon import exceptions
@ -15,6 +16,12 @@ from horizon import forms
LOG = logging.getLogger(__name__)
def almost_now(input_time):
now = timezone.make_naive(timezone.now(), timezone.utc)
# If we're less than a minute apart we'll assume success here.
return now - input_time < datetime.timedelta(seconds=30)
class BaseUsage(object):
show_terminated = False
@ -26,27 +33,23 @@ class BaseUsage(object):
@property
def today(self):
return datetime.date.today()
@staticmethod
def get_datetime(date, now=False):
if now:
now = datetime.datetime.utcnow()
current_time = datetime.time(now.hour, now.minute, now.second)
else:
current_time = datetime.time()
return datetime.datetime.combine(date, current_time)
return timezone.now()
@staticmethod
def get_start(year, month, day=1):
return datetime.date(year, month, day)
start = datetime.datetime(year, month, day, 0, 0, 0)
return timezone.make_aware(start, timezone.utc)
@staticmethod
def get_end(year, month, day=1):
period = relativedelta(months=1)
date_end = BaseUsage.get_start(year, month, day) + period
if date_end > datetime.date.today():
date_end = datetime.date.today()
days_in_month = monthrange(year, month)[1]
period = datetime.timedelta(days=days_in_month)
end = BaseUsage.get_start(year, month, day) + period
# End our calculation at midnight of the given day.
date_end = datetime.datetime.combine(end, datetime.time(0, 0, 0))
date_end = timezone.make_aware(date_end, timezone.utc)
if date_end > timezone.now():
date_end = timezone.now()
return date_end
def get_instances(self):
@ -82,10 +85,11 @@ class BaseUsage(object):
raise NotImplementedError("You must define a get_usage method.")
def summarize(self, start, end):
if start <= end <= datetime.date.today():
# Convert to datetime.datetime just for API call.
start = BaseUsage.get_datetime(start)
end = BaseUsage.get_datetime(end, now=True)
if start <= end <= self.today:
# The API can't handle timezone aware datetime, so convert back
# to naive UTC just for this last step.
start = timezone.make_naive(start, timezone.utc)
end = timezone.make_naive(end, timezone.utc)
try:
self.usage_list = self.get_usage_list(start, end)
except:
@ -125,7 +129,7 @@ class TenantUsage(BaseUsage):
usage = api.usage_get(self.request, self.tenant_id, start, end)
# Attribute may not exist if there are no instances
if hasattr(usage, 'server_usages'):
now = datetime.datetime.now()
now = self.today
for server_usage in usage.server_usages:
# This is a way to phrase uptime in a way that is compatible
# with the 'timesince' filter. (Use of local time intentional.)

View File

@ -137,6 +137,8 @@ LANGUAGES = (
)
LANGUAGE_CODE = 'en'
USE_I18N = True
USE_L10N = True
USE_TZ = True
OPENSTACK_KEYSTONE_DEFAULT_ROLE = 'Member'

View File

@ -6,7 +6,7 @@ set -o errexit
# Increment me any time the environment should be rebuilt.
# This includes dependncy changes, directory renames, etc.
# Simple integer secuence: 1, 2, 3...
environment_version=18
environment_version=19
#--------------------------------------------------------#
function usage {

View File

@ -2,7 +2,7 @@
Django>=1.4
django_compressor
python-cloudfiles
python-dateutil
pytz
# Horizon Non-pip Requirements
https://github.com/openstack/python-novaclient/zipball/master#egg=python-novaclient