diff options
41 files changed, 615 insertions, 164 deletions
@@ -9,3 +9,4 @@ <ke.wu@ibeca.me> <ke.wu@nebula.com> Zhongyue Luo <zhongyue.nah@intel.com> <lzyeval@gmail.com> Joe Gordon <joe.gordon0@gmail.com> <jogo@cloudscaling.com> +Kun Huang <gareth@unitedstack.com> <academicgareth@gmail.com> diff --git a/horizon/exceptions.py b/horizon/exceptions.py index d81c8c41..a5d48497 100644 --- a/horizon/exceptions.py +++ b/horizon/exceptions.py @@ -23,8 +23,8 @@ import os import sys from django.contrib.auth import logout +from django.core.management import color_style from django.http import HttpRequest -from django.utils import termcolors from django.utils.translation import ugettext_lazy as _ from django.views.debug import CLEANSED_SUBSTITUTE from django.views.debug import SafeExceptionReporterFilter @@ -33,7 +33,6 @@ from horizon.conf import HORIZON_CONFIG from horizon import messages LOG = logging.getLogger(__name__) -PALETTE = termcolors.PALETTES[termcolors.DEFAULT_PALETTE] class HorizonReporterFilter(SafeExceptionReporterFilter): @@ -202,7 +201,7 @@ RECOVERABLE += tuple(HORIZON_CONFIG['exceptions']['recoverable']) def error_color(msg): - return termcolors.colorize(msg, **PALETTE['ERROR']) + return color_style().ERROR_OUTPUT(msg) def check_message(keywords, message): diff --git a/horizon/forms/__init__.py b/horizon/forms/__init__.py index 611d59b7..4df93321 100644 --- a/horizon/forms/__init__.py +++ b/horizon/forms/__init__.py @@ -15,7 +15,7 @@ # under the License. # FIXME(gabriel): Legacy imports for API compatibility. -from django.forms import * +from django.forms import * # noqa from django.forms import widgets # Convenience imports for public API components. diff --git a/horizon/forms/base.py b/horizon/forms/base.py index af6510ee..aa6b0232 100644 --- a/horizon/forms/base.py +++ b/horizon/forms/base.py @@ -20,8 +20,6 @@ from django import forms from django.forms.forms import NON_FIELD_ERRORS -from django.utils import dates -from django.utils import timezone class SelfHandlingMixin(object): @@ -49,13 +47,11 @@ class SelfHandlingForm(SelfHandlingMixin, forms.Form): class DateForm(forms.Form): - """ A simple form for selecting a start date. """ - month = forms.ChoiceField(choices=dates.MONTHS.items()) - year = forms.ChoiceField() + """ A simple form for selecting a range of time. """ + start = forms.DateField(input_formats=("%Y-%m-%d",)) + end = forms.DateField(input_formats=("%Y-%m-%d",)) def __init__(self, *args, **kwargs): super(DateForm, self).__init__(*args, **kwargs) - years = [(year, year) for year - in xrange(2009, timezone.now().year + 1)] - years.reverse() - self.fields['year'].choices = years + self.fields['start'].widget.attrs['data-date-format'] = "yyyy-mm-dd" + self.fields['end'].widget.attrs['data-date-format'] = "yyyy-mm-dd" diff --git a/horizon/static/horizon/js/horizon.forms.js b/horizon/static/horizon/js/horizon.forms.js index 2147e6f0..a26f04f5 100644 --- a/horizon/static/horizon/js/horizon.forms.js +++ b/horizon/static/horizon/js/horizon.forms.js @@ -10,6 +10,7 @@ horizon.forms = { $volSize.val($option.data("size")); }); }, + handle_image_source: function() { $("div.table_wrapper, #modal_wrapper").on("change", "select#id_image_source", function(evt) { var $option = $(this).find("option:selected"); @@ -19,6 +20,27 @@ horizon.forms = { var $volSize = $form.find('input#id_size'); $volSize.val($option.data("size")); }); + }, + + datepicker: function() { + var startDate = $('input#id_start').datepicker() + .on('changeDate', function(ev) { + if (ev.date.valueOf() > endDate.date.valueOf()) { + var newDate = new Date(ev.date) + newDate.setDate(newDate.getDate() + 1); + endDate.setValue(newDate); + $('input#id_end')[0].focus(); + } + startDate.hide(); + }).data('datepicker'); + + var endDate = $('input#id_end').datepicker({ + onRender: function(date) { + return date.valueOf() < startDate.date.valueOf() ? 'disabled' : ''; + } + }).on('changeDate', function(ev) { + endDate.hide(); + }).data('datepicker'); } }; @@ -78,6 +100,7 @@ horizon.addInitFunction(function () { horizon.forms.handle_snapshot_source(); horizon.forms.handle_image_source(); + horizon.forms.datepicker(); // Bind event handlers to confirm dangerous actions. $("body").on("click", "form button.btn-danger", function (evt) { diff --git a/horizon/templates/horizon/common/_usage_summary.html b/horizon/templates/horizon/common/_usage_summary.html index eda41361..6b34d3ac 100644 --- a/horizon/templates/horizon/common/_usage_summary.html +++ b/horizon/templates/horizon/common/_usage_summary.html @@ -2,18 +2,19 @@ <div class="usage_info_wrapper"> <form action="" method="get" id="date_form" class="form-horizontal"> - <h3>{% trans "Select a month to query its usage" %}: </h3> - <div class="form-row"> - {{ form.month }} - {{ form.year }} + <h3>{% trans "Select a period of time to query its usage" %}: </h3> + <div class="datepicker"> + <span>{% trans "From" %}: {{ form.start }} </span> + <span>{% trans "To" %}: {{ form.end }} </span> <button class="btn btn-small" type="submit">{% trans "Submit" %}</button> + <small>{% trans "The date should be in YYYY-mm-dd format." %}</small> </div> </form> <p id="activity"> <span><strong>{% trans "Active Instances" %}:</strong> {{ usage.summary.instances|default:'-' }}</span> <span><strong>{% trans "Active RAM" %}:</strong> {{ usage.summary.memory_mb|mbformat|default:'-' }}</span> - <span><strong>{% trans "This Month's VCPU-Hours" %}:</strong> {{ usage.summary.vcpu_hours|floatformat:2|default:'-' }}</span> - <span><strong>{% trans "This Month's GB-Hours" %}:</strong> {{ usage.summary.disk_gb_hours|floatformat:2|default:'-' }}</span> + <span><strong>{% trans "This Period's VCPU-Hours" %}:</strong> {{ usage.summary.vcpu_hours|floatformat:2|default:'-' }}</span> + <span><strong>{% trans "This Period's GB-Hours" %}:</strong> {{ usage.summary.disk_gb_hours|floatformat:2|default:'-' }}</span> </p> </div> diff --git a/openstack_dashboard/api/base.py b/openstack_dashboard/api/base.py index 17ef4ee3..3edb5226 100644 --- a/openstack_dashboard/api/base.py +++ b/openstack_dashboard/api/base.py @@ -91,9 +91,9 @@ class APIResourceWrapper(object): def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, - dict((attr, - getattr(self, attr)) - for attr in self._attrs)) + dict((attr, getattr(self, attr)) + for attr in self._attrs + if hasattr(self, attr))) class APIDictWrapper(object): diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 6815439f..ba42c9b0 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -28,6 +28,7 @@ from django.conf import settings from django.utils.translation import ugettext_lazy as _ from novaclient.v1_1 import client as nova_client +from novaclient.v1_1.contrib.list_extensions import ListExtManager from novaclient.v1_1 import security_group_rules as nova_rules from novaclient.v1_1.security_groups import SecurityGroup as NovaSecurityGroup from novaclient.v1_1.servers import REBOOT_HARD @@ -75,7 +76,7 @@ class Server(APIResourceWrapper): 'image_name', 'VirtualInterfaces', 'flavor', 'key_name', 'tenant_id', 'user_id', 'OS-EXT-STS:power_state', 'OS-EXT-STS:task_state', 'OS-EXT-SRV-ATTR:instance_name', - 'OS-EXT-SRV-ATTR:host'] + 'OS-EXT-SRV-ATTR:host', 'created'] def __init__(self, apiresource, request): super(Server, self).__init__(apiresource) @@ -366,9 +367,10 @@ def server_spice_console(request, instance_id, console_type='spice-html5'): instance_id, console_type)['console']) -def flavor_create(request, name, memory, vcpu, disk, ephemeral=0, swap=0, - metadata=None): +def flavor_create(request, name, memory, vcpu, disk, flavorid='auto', + ephemeral=0, swap=0, metadata=None): flavor = novaclient(request).flavors.create(name, memory, vcpu, disk, + flavorid=flavorid, ephemeral=ephemeral, swap=swap) if (metadata): @@ -620,3 +622,30 @@ def availability_zone_list(request, detailed=False): def service_list(request): return novaclient(request).services.list() + + +def aggregate_list(request): + result = [] + for aggregate in novaclient(request).aggregates.list(): + result.append(novaclient(request).aggregates.get(aggregate.id)) + + return result + + +@memoized +def list_extensions(request): + return ListExtManager(novaclient(request)).show_all() + + +@memoized +def extension_supported(extension_name, request): + """ + this method will determine if nova supports a given extension name. + example values for the extension_name include AdminActions, ConsoleOutput, + etc. + """ + extensions = list_extensions(request) + for extension in extensions: + if extension.name == extension_name: + return True + return False diff --git a/openstack_dashboard/dashboards/admin/aggregates/__init__.py b/openstack_dashboard/dashboards/admin/aggregates/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/__init__.py diff --git a/openstack_dashboard/dashboards/admin/aggregates/panel.py b/openstack_dashboard/dashboards/admin/aggregates/panel.py new file mode 100644 index 00000000..a95f83ed --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/panel.py @@ -0,0 +1,29 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 B1 Systems GmbH +# +# 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. + +from django.utils.translation import ugettext_lazy as _ + +import horizon +from openstack_dashboard.dashboards.admin import dashboard + + +class Aggregates(horizon.Panel): + name = _("Aggregates") + slug = 'aggregates' + permissions = ('openstack.roles.admin',) + + +dashboard.Admin.register(Aggregates) diff --git a/openstack_dashboard/dashboards/admin/aggregates/tables.py b/openstack_dashboard/dashboards/admin/aggregates/tables.py new file mode 100644 index 00000000..378e1a18 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/tables.py @@ -0,0 +1,54 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 B1 Systems GmbH +# +# 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. + +import logging + +from django import template +from django.utils.translation import ugettext_lazy as _ + +from horizon import tables + +LOG = logging.getLogger(__name__) + + +def get_hosts(aggregate): + template_name = 'admin/aggregates/_aggregate_hosts.html' + context = {"aggregate": aggregate} + return template.loader.render_to_string(template_name, context) + + +def get_metadata(aggregate): + template_name = 'admin/aggregates/_aggregate_metadata.html' + context = {"aggregate": aggregate} + return template.loader.render_to_string(template_name, context) + + +class AdminAggregatesTable(tables.DataTable): + name = tables.Column("name", + verbose_name=_("Name")) + + availability_zone = tables.Column("availability_zone", + verbose_name=_("Availability Zone")) + + hosts = tables.Column(get_hosts, + verbose_name=_("Hosts")) + + metadata = tables.Column(get_metadata, + verbose_name=_("Metadata")) + + class Meta: + name = "aggregates" + verbose_name = _("Aggregates") diff --git a/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_aggregate_hosts.html b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_aggregate_hosts.html new file mode 100644 index 00000000..c85c3e90 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_aggregate_hosts.html @@ -0,0 +1,5 @@ +<ul> +{% for host in aggregate.hosts %} + <li>{{ host }}</li> +{% endfor %} +</ul> diff --git a/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_aggregate_metadata.html b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_aggregate_metadata.html new file mode 100644 index 00000000..ab8be7c3 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_aggregate_metadata.html @@ -0,0 +1,5 @@ +<ul> +{% for key, value in aggregate.metadata.iteritems %} + <li>{{ key }} = {{ value }}</li> +{% endfor %} +</ul> diff --git a/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/index.html b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/index.html new file mode 100644 index 00000000..42b7cab7 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Aggregates" %}{% endblock %} + +{% block page_header %} +{% include "horizon/common/_page_header.html" with title=_("All Aggregates") %} +{% endblock page_header %} + +{% block main %} +{{ table.render }} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/aggregates/tests.py b/openstack_dashboard/dashboards/admin/aggregates/tests.py new file mode 100644 index 00000000..babcd735 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/tests.py @@ -0,0 +1,34 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 B1 Systems GmbH +# +# 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. + +from django.core.urlresolvers import reverse +from django import http +from mox import IsA + +from openstack_dashboard import api +from openstack_dashboard.test import helpers as test + + +class AggregateViewTest(test.BaseAdminViewTests): + @test.create_stubs({api.nova: ('aggregate_list',)}) + def test_index(self): + aggregates = self.aggregates.list() + api.nova.aggregate_list(IsA(http.HttpRequest)).AndReturn(aggregates) + self.mox.ReplayAll() + + res = self.client.get(reverse('horizon:admin:aggregates:index')) + self.assertTemplateUsed(res, 'admin/aggregates/index.html') + self.assertItemsEqual(res.context['table'].data, aggregates) diff --git a/openstack_dashboard/dashboards/admin/aggregates/urls.py b/openstack_dashboard/dashboards/admin/aggregates/urls.py new file mode 100644 index 00000000..f8649a2f --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/urls.py @@ -0,0 +1,27 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 B1 Systems GmbH +# +# 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. + +from django.conf.urls.defaults import patterns +from django.conf.urls.defaults import url + +from openstack_dashboard.dashboards.admin.aggregates.views \ + import AdminIndexView + + +urlpatterns = patterns( + 'openstack_dashboard.dashboards.admin.aggregates.views', + url(r'^$', AdminIndexView.as_view(), name='index') +) diff --git a/openstack_dashboard/dashboards/admin/aggregates/views.py b/openstack_dashboard/dashboards/admin/aggregates/views.py new file mode 100644 index 00000000..31cf3767 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/views.py @@ -0,0 +1,42 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 B1 Systems GmbH +# +# 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. + +import logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import tables +from openstack_dashboard import api +from openstack_dashboard.dashboards.admin.aggregates.tables import \ + AdminAggregatesTable + +LOG = logging.getLogger(__name__) + + +class AdminIndexView(tables.DataTableView): + table_class = AdminAggregatesTable + template_name = 'admin/aggregates/index.html' + + def get_data(self): + aggregates = [] + try: + aggregates = api.nova.aggregate_list(self.request) + except: + exceptions.handle(self.request, + _('Unable to retrieve aggregate list.')) + + return aggregates diff --git a/openstack_dashboard/dashboards/admin/dashboard.py b/openstack_dashboard/dashboards/admin/dashboard.py index 38eb578c..9291aea5 100644 --- a/openstack_dashboard/dashboards/admin/dashboard.py +++ b/openstack_dashboard/dashboards/admin/dashboard.py @@ -22,8 +22,8 @@ import horizon class SystemPanels(horizon.PanelGroup): slug = "admin" name = _("System Panel") - panels = ('overview', 'hypervisors', 'instances', 'volumes', 'flavors', - 'images', 'networks', 'routers', 'info') + panels = ('overview', 'aggregates', 'hypervisors', 'instances', 'volumes', + 'flavors', 'images', 'networks', 'routers', 'info') class IdentityPanels(horizon.PanelGroup): diff --git a/openstack_dashboard/dashboards/admin/flavors/forms.py b/openstack_dashboard/dashboards/admin/flavors/forms.py index 43007fd1..4f17e8a6 100644 --- a/openstack_dashboard/dashboards/admin/flavors/forms.py +++ b/openstack_dashboard/dashboards/admin/flavors/forms.py @@ -33,12 +33,22 @@ LOG = logging.getLogger(__name__) class CreateFlavor(forms.SelfHandlingForm): + _flavor_id_regex = (r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-' + r'[0-9a-fA-F]{4}-[0-9a-fA-F]{12}|[0-9]+|auto$') + _flavor_id_help_text = _("Flavor ID should be UUID4 or integer. " + "Leave this field blank or use 'auto' to set " + "a random UUID4.") name = forms.RegexField(label=_("Name"), max_length=25, regex=r'^[\w\.\- ]+$', error_messages={'invalid': _('Name may only ' 'contain letters, numbers, underscores, ' 'periods and hyphens.')}) + flavor_id = forms.RegexField(label=_("ID"), + regex=_flavor_id_regex, + required=False, + initial='auto', + help_text=_flavor_id_help_text) vcpus = forms.IntegerField(label=_("VCPUs")) memory_mb = forms.IntegerField(label=_("RAM MB")) disk_gb = forms.IntegerField(label=_("Root Disk GB")) @@ -63,6 +73,24 @@ class CreateFlavor(forms.SelfHandlingForm): ) return name + def clean_flavor_id(self): + flavor_id = self.data.get('flavor_id') + try: + flavors = api.nova.flavor_list(self.request) + except: + flavors = [] + msg = _('Unable to get flavor list') + exceptions.check_message(["Connection", "refused"], msg) + raise + if flavors is not None: + for flavor in flavors: + if flavor.id == flavor_id: + raise forms.ValidationError( + _('The ID "%s" is already used by another flavor.') + % flavor_id + ) + return flavor_id + def handle(self, request, data): try: flavor = api.nova.flavor_create(request, @@ -70,6 +98,7 @@ class CreateFlavor(forms.SelfHandlingForm): data['memory_mb'], data['vcpus'], data['disk_gb'], + flavorid=data["flavor_id"], ephemeral=data['eph_gb'], swap=data['swap_mb']) msg = _('Created flavor "%s".') % data['name'] @@ -82,29 +111,8 @@ class CreateFlavor(forms.SelfHandlingForm): class EditFlavor(CreateFlavor): flavor_id = forms.CharField(widget=forms.widgets.HiddenInput) - def clean_name(self): - return self.cleaned_data['name'] - - def clean(self): - cleaned_data = super(EditFlavor, self).clean() - name = cleaned_data.get('name') - flavor_id = cleaned_data.get('flavor_id') - try: - flavors = api.nova.flavor_list(self.request) - except: - flavors = [] - msg = _('Unable to get flavor list') - exceptions.check_message(["Connection", "refused"], msg) - raise - # Check if there is no flavor with the same name - if flavors is not None: - for flavor in flavors: - if flavor.name == name and flavor.id != flavor_id: - raise forms.ValidationError( - _('The name "%s" is already used by another flavor.') - % name - ) - return cleaned_data + def clean_flavor_id(self): + return self.data.get('flavor_id') def handle(self, request, data): try: diff --git a/openstack_dashboard/dashboards/admin/flavors/tests.py b/openstack_dashboard/dashboards/admin/flavors/tests.py index 4f56dbed..82c33e3d 100644 --- a/openstack_dashboard/dashboards/admin/flavors/tests.py +++ b/openstack_dashboard/dashboards/admin/flavors/tests.py @@ -20,9 +20,11 @@ class FlavorsTests(test.BaseAdminViewTests): flavor.ram, flavor.vcpus, flavor.disk, + flavorid=flavor.id, swap=flavor.swap, ephemeral=eph).AndReturn(flavor) api.nova.flavor_list(IsA(http.HttpRequest)) + api.nova.flavor_list(IsA(http.HttpRequest)) self.mox.ReplayAll() url = reverse('horizon:admin:flavors:create') @@ -31,6 +33,7 @@ class FlavorsTests(test.BaseAdminViewTests): self.assertTemplateUsed(resp, "admin/flavors/create.html") data = {'name': flavor.name, + 'flavor_id': flavor.id, 'vcpus': flavor.vcpus, 'memory_mb': flavor.ram, 'disk_gb': flavor.disk, @@ -172,8 +175,6 @@ class FlavorsTests(test.BaseAdminViewTests): flavor_a.id).AndReturn(flavor_a) # POST - api.nova.flavor_list(IsA(http.HttpRequest)) \ - .AndReturn(self.flavors.list()) api.nova.flavor_get(IsA(http.HttpRequest), flavor_a.id).AndReturn(flavor_a) self.mox.ReplayAll() diff --git a/openstack_dashboard/dashboards/admin/instances/tables.py b/openstack_dashboard/dashboards/admin/instances/tables.py index 08b84ff1..265a9529 100644 --- a/openstack_dashboard/dashboards/admin/instances/tables.py +++ b/openstack_dashboard/dashboards/admin/instances/tables.py @@ -17,10 +17,12 @@ import logging +from django.template.defaultfilters import timesince from django.template.defaultfilters import title from django.utils.translation import ugettext_lazy as _ from horizon import tables +from horizon.utils.filters import parse_isotime from horizon.utils.filters import replace_underscores from openstack_dashboard import api @@ -152,6 +154,9 @@ class AdminInstancesTable(tables.DataTable): state = tables.Column(get_power_state, filters=(title, replace_underscores), verbose_name=_("Power State")) + created = tables.Column("created", + verbose_name=_("Uptime"), + filters=(parse_isotime, timesince)) class Meta: name = "instances" diff --git a/openstack_dashboard/dashboards/admin/instances/tests.py b/openstack_dashboard/dashboards/admin/instances/tests.py index 8e379ef6..be2373fa 100644 --- a/openstack_dashboard/dashboards/admin/instances/tests.py +++ b/openstack_dashboard/dashboards/admin/instances/tests.py @@ -27,12 +27,15 @@ from openstack_dashboard.test import helpers as test class InstanceViewTest(test.BaseAdminViewTests): - @test.create_stubs({api.nova: ('flavor_list', 'server_list',), + @test.create_stubs({api.nova: ('flavor_list', 'server_list', + 'extension_supported',), api.keystone: ('tenant_list',)}) def test_index(self): servers = self.servers.list() flavors = self.flavors.list() tenants = self.tenants.list() + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.keystone.tenant_list(IsA(http.HttpRequest)).\ AndReturn([tenants, False]) search_opts = {'marker': None, 'paginate': True} @@ -48,7 +51,7 @@ class InstanceViewTest(test.BaseAdminViewTests): self.assertItemsEqual(instances, servers) @test.create_stubs({api.nova: ('flavor_list', 'flavor_get', - 'server_list',), + 'server_list', 'extension_supported',), api.keystone: ('tenant_list',)}) def test_index_flavor_list_exception(self): servers = self.servers.list() @@ -60,6 +63,8 @@ class InstanceViewTest(test.BaseAdminViewTests): api.nova.server_list(IsA(http.HttpRequest), all_tenants=True, search_opts=search_opts) \ .AndReturn([servers, False]) + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)). \ AndRaise(self.exceptions.nova) api.keystone.tenant_list(IsA(http.HttpRequest)).\ @@ -76,7 +81,7 @@ class InstanceViewTest(test.BaseAdminViewTests): self.assertItemsEqual(instances, servers) @test.create_stubs({api.nova: ('flavor_list', 'flavor_get', - 'server_list',), + 'server_list', 'extension_supported', ), api.keystone: ('tenant_list',)}) def test_index_flavor_get_exception(self): servers = self.servers.list() @@ -91,6 +96,8 @@ class InstanceViewTest(test.BaseAdminViewTests): api.nova.server_list(IsA(http.HttpRequest), all_tenants=True, search_opts=search_opts) \ .AndReturn([servers, False]) + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)). \ AndReturn(flavors) api.keystone.tenant_list(IsA(http.HttpRequest)).\ @@ -119,7 +126,8 @@ class InstanceViewTest(test.BaseAdminViewTests): self.assertTemplateUsed(res, 'admin/instances/index.html') self.assertEqual(len(res.context['instances_table'].data), 0) - @test.create_stubs({api.nova: ('server_get', 'flavor_get',), + @test.create_stubs({api.nova: ('server_get', 'flavor_get', + 'extension_supported', ), api.keystone: ('tenant_get',)}) def test_ajax_loading_instances(self): server = self.servers.first() @@ -127,6 +135,8 @@ class InstanceViewTest(test.BaseAdminViewTests): tenant = self.tenants.list()[0] api.nova.server_get(IsA(http.HttpRequest), server.id).AndReturn(server) + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.nova.flavor_get(IsA(http.HttpRequest), server.flavor['id']).AndReturn(flavor) api.keystone.tenant_get(IsA(http.HttpRequest), @@ -150,7 +160,8 @@ class InstanceViewTest(test.BaseAdminViewTests): self.assertContains(res, "Active", 1, 200) self.assertContains(res, "Running", 1, 200) - @test.create_stubs({api.nova: ('flavor_list', 'server_list',), + @test.create_stubs({api.nova: ('flavor_list', 'server_list', + 'extension_supported', ), api.keystone: ('tenant_list',)}) def test_index_options_before_migrate(self): api.keystone.tenant_list(IsA(http.HttpRequest)).\ @@ -159,6 +170,8 @@ class InstanceViewTest(test.BaseAdminViewTests): api.nova.server_list(IsA(http.HttpRequest), all_tenants=True, search_opts=search_opts) \ .AndReturn([self.servers.list(), False]) + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)).\ AndReturn(self.flavors.list()) self.mox.ReplayAll() @@ -168,7 +181,8 @@ class InstanceViewTest(test.BaseAdminViewTests): self.assertNotContains(res, "instances__confirm") self.assertNotContains(res, "instances__revert") - @test.create_stubs({api.nova: ('flavor_list', 'server_list',), + @test.create_stubs({api.nova: ('flavor_list', 'server_list', + 'extension_supported', ), api.keystone: ('tenant_list',)}) def test_index_options_after_migrate(self): servers = self.servers.list() @@ -179,6 +193,8 @@ class InstanceViewTest(test.BaseAdminViewTests): api.keystone.tenant_list(IsA(http.HttpRequest)) \ .AndReturn([self.tenants.list(), False]) search_opts = {'marker': None, 'paginate': True} + api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.nova.server_list(IsA(http.HttpRequest), all_tenants=True, search_opts=search_opts) \ .AndReturn([self.servers.list(), False]) diff --git a/openstack_dashboard/dashboards/admin/overview/tests.py b/openstack_dashboard/dashboards/admin/overview/tests.py index 0c08529e..22499285 100644 --- a/openstack_dashboard/dashboards/admin/overview/tests.py +++ b/openstack_dashboard/dashboards/admin/overview/tests.py @@ -24,7 +24,6 @@ from django.core.urlresolvers import reverse from django import http from django.utils import timezone -from mox import Func from mox import IsA from horizon.templatetags.sizeformat import mbformat @@ -47,10 +46,14 @@ class UsageViewTests(test.BaseAdminViewTests): api.keystone.tenant_list(IsA(http.HttpRequest)) \ .AndReturn([self.tenants.list(), False]) api.nova.usage_list(IsA(http.HttpRequest), - datetime.datetime(now.year, now.month, 1, 0, 0, 0), - Func(usage.almost_now)) \ - .AndReturn([usage_obj]) - api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ + datetime.datetime(now.year, + now.month, + now.day, 0, 0, 0, 0), + datetime.datetime(now.year, + now.month, + now.day, 23, 59, 59, 0)) \ + .AndReturn([usage_obj]) + api.nova.tenant_absolute_limits(IsA(http.HttpRequest)) \ .AndReturn(self.limits['absolute']) self.mox.ReplayAll() res = self.client.get(reverse('horizon:admin:overview:index')) @@ -78,9 +81,13 @@ class UsageViewTests(test.BaseAdminViewTests): api.keystone.tenant_list(IsA(http.HttpRequest)) \ .AndReturn([self.tenants.list(), False]) api.nova.usage_list(IsA(http.HttpRequest), - datetime.datetime(now.year, now.month, 1, 0, 0, 0), - Func(usage.almost_now)) \ - .AndReturn(usage_obj) + datetime.datetime(now.year, + now.month, + now.day, 0, 0, 0, 0), + datetime.datetime(now.year, + now.month, + now.day, 23, 59, 59, 0)) \ + .AndReturn(usage_obj) api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ .AndReturn(self.limits['absolute']) self.mox.ReplayAll() diff --git a/openstack_dashboard/dashboards/admin/users/forms.py b/openstack_dashboard/dashboards/admin/users/forms.py index f2deec06..c0039a3c 100644 --- a/openstack_dashboard/dashboards/admin/users/forms.py +++ b/openstack_dashboard/dashboards/admin/users/forms.py @@ -70,7 +70,9 @@ ADD_PROJECT_URL = "horizon:admin:projects:create" class CreateUserForm(BaseUserForm): name = forms.CharField(label=_("User Name")) - email = forms.EmailField(label=_("Email")) + email = forms.EmailField( + label=_("Email"), + required=False) password = forms.RegexField( label=_("Password"), widget=forms.PasswordInput(render_value=False), @@ -115,7 +117,7 @@ class CreateUserForm(BaseUserForm): data['role_id']) except: exceptions.handle(request, - _('Unable to add user' + _('Unable to add user ' 'to primary project.')) return new_user except: @@ -125,7 +127,9 @@ class CreateUserForm(BaseUserForm): class UpdateUserForm(BaseUserForm): id = forms.CharField(label=_("ID"), widget=forms.HiddenInput) name = forms.CharField(label=_("User Name")) - email = forms.EmailField(label=_("Email")) + email = forms.EmailField( + label=_("Email"), + required=False) password = forms.RegexField( label=_("Password"), widget=forms.PasswordInput(render_value=False), diff --git a/openstack_dashboard/dashboards/project/images_and_snapshots/urls.py b/openstack_dashboard/dashboards/project/images_and_snapshots/urls.py index bb0704c5..2e2b4cd6 100644 --- a/openstack_dashboard/dashboards/project/images_and_snapshots/urls.py +++ b/openstack_dashboard/dashboards/project/images_and_snapshots/urls.py @@ -18,7 +18,9 @@ # License for the specific language governing permissions and limitations # under the License. -from django.conf.urls.defaults import * +from django.conf.urls.defaults import include +from django.conf.urls.defaults import patterns +from django.conf.urls.defaults import url from openstack_dashboard.dashboards.project.images_and_snapshots.images \ import urls as image_urls diff --git a/openstack_dashboard/dashboards/project/instances/tables.py b/openstack_dashboard/dashboards/project/instances/tables.py index e30a62a7..b3e932a6 100644 --- a/openstack_dashboard/dashboards/project/instances/tables.py +++ b/openstack_dashboard/dashboards/project/instances/tables.py @@ -18,6 +18,7 @@ from django.core import urlresolvers from django import shortcuts from django import template +from django.template.defaultfilters import timesince from django.template.defaultfilters import title from django.utils.http import urlencode from django.utils.translation import string_concat @@ -28,6 +29,7 @@ from horizon import exceptions from horizon import messages from horizon import tables from horizon.templatetags import sizeformat +from horizon.utils.filters import parse_isotime from horizon.utils.filters import replace_underscores import logging @@ -125,6 +127,9 @@ class TogglePause(tables.BatchAction): classes = ("btn-pause",) def allowed(self, request, instance=None): + if not api.nova.extension_supported('AdminActions', + request): + return False self.paused = False if not instance: return self.paused @@ -154,6 +159,9 @@ class ToggleSuspend(tables.BatchAction): classes = ("btn-suspend",) def allowed(self, request, instance=None): + if not api.nova.extension_supported('AdminActions', + request): + return False self.suspended = False if not instance: self.suspended @@ -548,6 +556,9 @@ class InstancesTable(tables.DataTable): state = tables.Column(get_power_state, filters=(title, replace_underscores), verbose_name=_("Power State")) + created = tables.Column("created", + verbose_name=_("Uptime"), + filters=(parse_isotime, timesince)) class Meta: name = "instances" diff --git a/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_overview.html b/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_overview.html index 8d5fc284..1b1006ff 100644 --- a/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_overview.html +++ b/openstack_dashboard/dashboards/project/instances/templates/instances/_detail_overview.html @@ -13,6 +13,10 @@ <dd>{{ instance.id }}</dd> <dt>{% trans "Status" %}</dt> <dd>{{ instance.status|title }}</dd> + <dt>{% trans "Created" %}</dt> + <dd>{{ instance.created|parse_isotime }}</dd> + <dt>{% trans "Uptime" %}</dt> + <dd>{{ instance.created|parse_isotime|timesince }}</dd> </dl> </div> diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index 5e0c045e..0e51d94a 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -49,8 +49,12 @@ INDEX_URL = reverse('horizon:project:instances:index') class InstanceTests(test.TestCase): @test.create_stubs({api.nova: ('flavor_list', 'server_list', - 'tenant_absolute_limits')}) + 'tenant_absolute_limits', + 'extension_supported',)}) def test_index(self): + api.nova.extension_supported('AdminActions', + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) search_opts = {'marker': None, 'paginate': True} @@ -61,8 +65,7 @@ class InstanceTests(test.TestCase): self.mox.ReplayAll() - res = self.client.get( - reverse('horizon:project:instances:index')) + res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, 'project/instances/index.html') @@ -71,7 +74,7 @@ class InstanceTests(test.TestCase): self.assertItemsEqual(instances, self.servers.list()) @test.create_stubs({api.nova: ('server_list', - 'tenant_absolute_limits')}) + 'tenant_absolute_limits',)}) def test_index_server_list_exception(self): search_opts = {'marker': None, 'paginate': True} api.nova.server_list(IsA(http.HttpRequest), search_opts=search_opts) \ @@ -81,7 +84,7 @@ class InstanceTests(test.TestCase): self.mox.ReplayAll() - res = self.client.get(reverse('horizon:project:instances:index')) + res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, 'project/instances/index.html') self.assertEqual(len(res.context['instances_table'].data), 0) @@ -90,12 +93,16 @@ class InstanceTests(test.TestCase): @test.create_stubs({api.nova: ('flavor_list', 'server_list', 'flavor_get', - 'tenant_absolute_limits')}) + 'tenant_absolute_limits', + 'extension_supported',)}) def test_index_flavor_list_exception(self): servers = self.servers.list() flavors = self.flavors.list() full_flavors = SortedDict([(f.id, f) for f in flavors]) search_opts = {'marker': None, 'paginate': True} + api.nova.extension_supported('AdminActions', + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.nova.server_list(IsA(http.HttpRequest), search_opts=search_opts) \ .AndReturn([servers, False]) api.nova.flavor_list(IsA(http.HttpRequest)) \ @@ -108,7 +115,7 @@ class InstanceTests(test.TestCase): self.mox.ReplayAll() - res = self.client.get(reverse('horizon:project:instances:index')) + res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, 'project/instances/index.html') instances = res.context['instances_table'].data @@ -118,10 +125,14 @@ class InstanceTests(test.TestCase): @test.create_stubs({api.nova: ('flavor_list', 'server_list', 'flavor_get', - 'tenant_absolute_limits')}) + 'tenant_absolute_limits', + 'extension_supported',)}) def test_index_flavor_get_exception(self): servers = self.servers.list() flavors = self.flavors.list() + api.nova.extension_supported('AdminActions', + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) # UUIDs generated using indexes are unlikely to match # any of existing flavor ids and are guaranteed to be deterministic. for i, server in enumerate(servers): @@ -139,7 +150,7 @@ class InstanceTests(test.TestCase): self.mox.ReplayAll() - res = self.client.get(reverse('horizon:project:instances:index')) + res = self.client.get(INDEX_URL) instances = res.context['instances_table'].data @@ -188,10 +199,14 @@ class InstanceTests(test.TestCase): @test.create_stubs({api.nova: ('server_pause', 'server_list', - 'flavor_list',)}) + 'flavor_list', + 'extension_supported',)}) def test_pause_instance(self): server = self.servers.first() + api.nova.extension_supported('AdminActions', + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) search_opts = {'marker': None, 'paginate': True} @@ -208,10 +223,14 @@ class InstanceTests(test.TestCase): @test.create_stubs({api.nova: ('server_pause', 'server_list', - 'flavor_list',)}) + 'flavor_list', + 'extension_supported',)}) def test_pause_instance_exception(self): server = self.servers.first() + api.nova.extension_supported('AdminActions', + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) search_opts = {'marker': None, 'paginate': True} @@ -229,11 +248,14 @@ class InstanceTests(test.TestCase): @test.create_stubs({api.nova: ('server_unpause', 'server_list', - 'flavor_list',)}) + 'flavor_list', + 'extension_supported',)}) def test_unpause_instance(self): server = self.servers.first() server.status = "PAUSED" - + api.nova.extension_supported('AdminActions', + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) search_opts = {'marker': None, 'paginate': True} @@ -250,11 +272,15 @@ class InstanceTests(test.TestCase): @test.create_stubs({api.nova: ('server_unpause', 'server_list', - 'flavor_list',)}) + 'flavor_list', + 'extension_supported',)}) def test_unpause_instance_exception(self): server = self.servers.first() server.status = "PAUSED" + api.nova.extension_supported('AdminActions', + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) search_opts = {'marker': None, 'paginate': True} @@ -275,7 +301,6 @@ class InstanceTests(test.TestCase): 'flavor_list',)}) def test_reboot_instance(self): server = self.servers.first() - api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) search_opts = {'marker': None, 'paginate': True} @@ -336,10 +361,14 @@ class InstanceTests(test.TestCase): @test.create_stubs({api.nova: ('server_suspend', 'server_list', - 'flavor_list',)}) + 'flavor_list', + 'extension_supported',)}) def test_suspend_instance(self): server = self.servers.first() + api.nova.extension_supported('AdminActions', + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) search_opts = {'marker': None, 'paginate': True} @@ -356,10 +385,14 @@ class InstanceTests(test.TestCase): @test.create_stubs({api.nova: ('server_suspend', 'server_list', - 'flavor_list',)}) + 'flavor_list', + 'extension_supported',)}) def test_suspend_instance_exception(self): server = self.servers.first() + api.nova.extension_supported('AdminActions', + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) search_opts = {'marker': None, 'paginate': True} @@ -377,11 +410,15 @@ class InstanceTests(test.TestCase): @test.create_stubs({api.nova: ('server_resume', 'server_list', - 'flavor_list',)}) + 'flavor_list', + 'extension_supported',)}) def test_resume_instance(self): server = self.servers.first() server.status = "SUSPENDED" + api.nova.extension_supported('AdminActions', + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) search_opts = {'marker': None, 'paginate': True} @@ -398,11 +435,15 @@ class InstanceTests(test.TestCase): @test.create_stubs({api.nova: ('server_resume', 'server_list', - 'flavor_list',)}) + 'flavor_list', + 'extension_supported',)}) def test_resume_instance_exception(self): server = self.servers.first() server.status = "SUSPENDED" + api.nova.extension_supported('AdminActions', + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) search_opts = {'marker': None, 'paginate': True} @@ -1429,11 +1470,15 @@ class InstanceTests(test.TestCase): self.assertContains(res, "greater than or equal to 1") @test.create_stubs({api.nova: ('flavor_list', 'server_list', - 'tenant_absolute_limits',)}) + 'tenant_absolute_limits', + 'extension_supported',)}) def test_launch_button_disabled_when_quota_exceeded(self): limits = self.limits['absolute'] limits['totalInstancesUsed'] = limits['maxTotalInstances'] + api.nova.extension_supported('AdminActions', + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) search_opts = {'marker': None, 'paginate': True} @@ -1458,11 +1503,14 @@ class InstanceTests(test.TestCase): msg_prefix="The launch button is not disabled") @test.create_stubs({api.nova: ('flavor_list', 'server_list', - 'tenant_absolute_limits')}) + 'tenant_absolute_limits', + 'extension_supported',)}) def test_index_options_after_migrate(self): server = self.servers.first() server.status = "VERIFY_RESIZE" - + api.nova.extension_supported('AdminActions', + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.nova.flavor_list(IsA(http.HttpRequest)) \ .AndReturn(self.flavors.list()) search_opts = {'marker': None, 'paginate': True} diff --git a/openstack_dashboard/dashboards/project/instances/workflows/__init__.py b/openstack_dashboard/dashboards/project/instances/workflows/__init__.py index 1525d964..2c19082f 100644 --- a/openstack_dashboard/dashboards/project/instances/workflows/__init__.py +++ b/openstack_dashboard/dashboards/project/instances/workflows/__init__.py @@ -1,3 +1,7 @@ -from create_instance import * -from update_instance import * -from resize_instance import * +from create_instance import LaunchInstance +from resize_instance import ResizeInstance +from update_instance import UpdateInstance + +assert LaunchInstance +assert UpdateInstance +assert ResizeInstance diff --git a/openstack_dashboard/dashboards/project/loadbalancers/workflows.py b/openstack_dashboard/dashboards/project/loadbalancers/workflows.py index 2525703f..69e7067b 100644 --- a/openstack_dashboard/dashboards/project/loadbalancers/workflows.py +++ b/openstack_dashboard/dashboards/project/loadbalancers/workflows.py @@ -72,7 +72,7 @@ class AddPoolAction(workflows.Action): class Meta: name = _("Add New Pool") permissions = ('openstack.services.network',) - help_text = _("Create Pool for current tenant.\n\n" + help_text = _("Create Pool for current project.\n\n" "Assign a name and description for the pool. " "Choose one subnet where all members of this " "pool must be on. " diff --git a/openstack_dashboard/dashboards/project/overview/tests.py b/openstack_dashboard/dashboards/project/overview/tests.py index 5cb62e02..47da3e50 100644 --- a/openstack_dashboard/dashboards/project/overview/tests.py +++ b/openstack_dashboard/dashboards/project/overview/tests.py @@ -24,7 +24,6 @@ from django.core.urlresolvers import reverse from django import http from django.utils import timezone -from mox import Func from mox import IsA from openstack_dashboard import api @@ -42,8 +41,12 @@ class UsageViewTests(test.TestCase): self.mox.StubOutWithMock(api.nova, 'usage_get') self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id, - datetime.datetime(now.year, now.month, 1, 0, 0, 0), - Func(usage.almost_now)) \ + datetime.datetime(now.year, + now.month, + now.day, 0, 0, 0, 0), + datetime.datetime(now.year, + now.month, + now.day, 23, 59, 59, 0)) \ .AndReturn(usage_obj) api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ .AndReturn(self.limits['absolute']) @@ -60,8 +63,12 @@ class UsageViewTests(test.TestCase): self.mox.StubOutWithMock(api.nova, 'usage_get') self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id, - datetime.datetime(now.year, now.month, 1, 0, 0, 0), - Func(usage.almost_now)) \ + datetime.datetime(now.year, + now.month, + now.day, 0, 0, 0, 0), + datetime.datetime(now.year, + now.month, + now.day, 23, 59, 59, 0)) \ .AndRaise(exc) api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ .AndReturn(self.limits['absolute']) @@ -78,14 +85,13 @@ class UsageViewTests(test.TestCase): usage_obj = api.nova.NovaUsage(self.usages.first()) self.mox.StubOutWithMock(api.nova, 'usage_get') self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') - timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0) + start = datetime.datetime(now.year, now.month, now.day, 0, 0, 0, 0) + end = datetime.datetime(now.year, now.month, now.day, 23, 59, 59, 0) api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id, - timestamp, - Func(usage.almost_now)) \ - .AndReturn(usage_obj) + start, end).AndReturn(usage_obj) api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ - .AndReturn(self.limits['absolute']) + .AndReturn(self.limits['absolute']) self.mox.ReplayAll() res = self.client.get(reverse('horizon:project:overview:index') + @@ -97,14 +103,14 @@ class UsageViewTests(test.TestCase): now = timezone.now() self.mox.StubOutWithMock(api.nova, 'usage_get') self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') - timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0) + start = datetime.datetime(now.year, now.month, now.day, 0, 0, 0, 0) + end = datetime.datetime(now.year, now.month, now.day, 23, 59, 59, 0) api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id, - timestamp, - Func(usage.almost_now)) \ - .AndRaise(self.exceptions.nova) + start, end).AndRaise(self.exceptions.nova) api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ - .AndReturn(self.limits['absolute']) + .AndReturn(self.limits['absolute']) + self.mox.ReplayAll() res = self.client.get(reverse('horizon:project:overview:index')) @@ -116,12 +122,11 @@ class UsageViewTests(test.TestCase): usage_obj = api.nova.NovaUsage(self.usages.first()) self.mox.StubOutWithMock(api.nova, 'usage_get') self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') - timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0) + start = datetime.datetime(now.year, now.month, now.day, 0, 0, 0, 0) + end = datetime.datetime(now.year, now.month, now.day, 23, 59, 59, 0) api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id, - timestamp, - Func(usage.almost_now)) \ - .AndReturn(usage_obj) + start, end).AndReturn(usage_obj) api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ .AndRaise(self.exceptions.nova) self.mox.ReplayAll() @@ -135,14 +140,13 @@ class UsageViewTests(test.TestCase): usage_obj = api.nova.NovaUsage(self.usages.first()) self.mox.StubOutWithMock(api.nova, 'usage_get') self.mox.StubOutWithMock(api.nova, 'tenant_absolute_limits') - timestamp = datetime.datetime(now.year, now.month, 1, 0, 0, 0) + start = datetime.datetime(now.year, now.month, now.day, 0, 0, 0, 0) + end = datetime.datetime(now.year, now.month, now.day, 23, 59, 59, 0) api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id, - timestamp, - Func(usage.almost_now)) \ - .AndReturn(usage_obj) + start, end).AndReturn(usage_obj) api.nova.tenant_absolute_limits(IsA(http.HttpRequest))\ - .AndReturn(self.limits['absolute']) + .AndReturn(self.limits['absolute']) self.mox.ReplayAll() res = self.client.get(reverse('horizon:project:overview:index')) diff --git a/openstack_dashboard/settings.py b/openstack_dashboard/settings.py index 9613f589..b05cd98c 100644 --- a/openstack_dashboard/settings.py +++ b/openstack_dashboard/settings.py @@ -179,7 +179,7 @@ OPENSTACK_KEYSTONE_DEFAULT_ROLE = 'Member' DEFAULT_EXCEPTION_REPORTER_FILTER = 'horizon.exceptions.HorizonReporterFilter' try: - from local.local_settings import * + from local.local_settings import * # noqa except ImportError: logging.warning("No local_settings file found.") diff --git a/openstack_dashboard/static/bootstrap/less/datepicker.less b/openstack_dashboard/static/bootstrap/less/datepicker.less index 7990c3db..76967442 100644 --- a/openstack_dashboard/static/bootstrap/less/datepicker.less +++ b/openstack_dashboard/static/bootstrap/less/datepicker.less @@ -6,6 +6,7 @@ * http://www.apache.org/licenses/LICENSE-2.0 * */ + .datepicker { top: 0; left: 0; @@ -179,4 +180,4 @@ cursor: pointer; width: 16px; height: 16px; -} +}
\ No newline at end of file diff --git a/openstack_dashboard/static/dashboard/less/horizon.less b/openstack_dashboard/static/dashboard/less/horizon.less index 9d6fa717..3a97df0f 100644 --- a/openstack_dashboard/static/dashboard/less/horizon.less +++ b/openstack_dashboard/static/dashboard/less/horizon.less @@ -724,6 +724,19 @@ form label { width: 150px; } +.datepicker { + margin-top: 10px; +} + +.datepicker input { + width: 65px; + margin-right: 10px; +} + +.datepicker .btn { + margin-right: 10px; +} + form.horizontal .form-field { float: left; } @@ -940,6 +953,10 @@ td.actions_column .btn-action-required { font-weight: bold; } +.tab-content { + overflow: visible; +} + /* Makes size consistent across browsers when mixing "btn-group" and "small" */ .btn.hide, .btn-group .hide { display: none; @@ -2102,4 +2119,4 @@ div.network { #info_box h3 {font-size:9pt;line-height:20px;} #info_box p {margin:0;font-size:9pt;line-height:14px;} #info_box a {margin:0;font-size:9pt;line-height:14px;} -#info_box .error {color:darkred;} +#info_box .error {color:darkred;}
\ No newline at end of file diff --git a/openstack_dashboard/test/api_tests/base_tests.py b/openstack_dashboard/test/api_tests/base_tests.py index c6aa0460..d488757c 100644 --- a/openstack_dashboard/test/api_tests/base_tests.py +++ b/openstack_dashboard/test/api_tests/base_tests.py @@ -73,6 +73,13 @@ class APIResourceWrapperTests(test.TestCase): with self.assertRaises(AttributeError): resource.baz + def test_repr(self): + resource = APIResource.get_instance() + resource_str = resource.__repr__() + self.assertIn('foo', resource_str) + self.assertIn('bar', resource_str) + self.assertNotIn('baz', resource_str) + class APIDictWrapperTests(test.TestCase): # APIDict allows for both attribute access and dictionary style [element] diff --git a/openstack_dashboard/test/settings.py b/openstack_dashboard/test/settings.py index c89b2b35..dfcaa22c 100644 --- a/openstack_dashboard/test/settings.py +++ b/openstack_dashboard/test/settings.py @@ -2,7 +2,7 @@ import os from django.utils.translation import ugettext_lazy as _ -from horizon.test.settings import * +from horizon.test.settings import * # noqa from horizon.utils.secret_key import generate_or_read_from_file from openstack_dashboard.exceptions import NOT_FOUND diff --git a/openstack_dashboard/test/test_data/nova_data.py b/openstack_dashboard/test/test_data/nova_data.py index 52648230..326683d8 100644 --- a/openstack_dashboard/test/test_data/nova_data.py +++ b/openstack_dashboard/test/test_data/nova_data.py @@ -15,6 +15,7 @@ import json import uuid +from novaclient.v1_1 import aggregates from novaclient.v1_1 import availability_zones from novaclient.v1_1 import certs from novaclient.v1_1 import flavors @@ -166,6 +167,7 @@ def data(TEST): TEST.availability_zones = TestDataContainer() TEST.hypervisors = TestDataContainer() TEST.services = TestDataContainer() + TEST.aggregates = TestDataContainer() # Data return by novaclient. # It is used if API layer does data conversion. @@ -541,3 +543,41 @@ def data(TEST): ) TEST.services.add(service_1) TEST.services.add(service_2) + + # Aggregates + aggregate_1 = aggregates.Aggregate(aggregates.AggregateManager(None), + { + "name": "foo", + "availability_zone": None, + "deleted": 0, + "created_at": "2013-07-04T13:34:38.000000", + "updated_at": None, + "hosts": ["foo", "bar"], + "deleted_at": None, + "id": 1, + "metadata": { + "foo": "testing", + "bar": "testing" + } + } + ) + + aggregate_2 = aggregates.Aggregate(aggregates.AggregateManager(None), + { + "name": "bar", + "availability_zone": "testing", + "deleted": 0, + "created_at": "2013-07-04T13:34:38.000000", + "updated_at": None, + "hosts": ["foo", "bar"], + "deleted_at": None, + "id": 2, + "metadata": { + "foo": "testing", + "bar": "testing" + } + } + ) + + TEST.aggregates.add(aggregate_1) + TEST.aggregates.add(aggregate_2) diff --git a/openstack_dashboard/usage/__init__.py b/openstack_dashboard/usage/__init__.py index 4aa498d7..a1f6ef61 100644 --- a/openstack_dashboard/usage/__init__.py +++ b/openstack_dashboard/usage/__init__.py @@ -14,7 +14,6 @@ # License for the specific language governing permissions and limitations # under the License. -from openstack_dashboard.usage.base import almost_now from openstack_dashboard.usage.base import BaseUsage from openstack_dashboard.usage.base import GlobalUsage from openstack_dashboard.usage.base import ProjectUsage @@ -26,7 +25,6 @@ from openstack_dashboard.usage.views import UsageView assert BaseUsage assert ProjectUsage assert GlobalUsage -assert almost_now assert UsageView assert BaseUsageTable assert ProjectUsageTable diff --git a/openstack_dashboard/usage/base.py b/openstack_dashboard/usage/base.py index 002207a8..4e540981 100644 --- a/openstack_dashboard/usage/base.py +++ b/openstack_dashboard/usage/base.py @@ -1,8 +1,8 @@ from __future__ import division -from calendar import monthrange from csv import DictWriter from csv import writer + import datetime import logging from StringIO import StringIO @@ -24,12 +24,6 @@ from openstack_dashboard.usage import quotas 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 @@ -46,21 +40,14 @@ class BaseUsage(object): return timezone.now() @staticmethod - def get_start(year, month, day=1): + def get_start(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): - 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_end(year, month, day): + end = datetime.datetime(year, month, day, 23, 59, 59) + return timezone.make_aware(end, timezone.utc) def get_instances(self): instance_list = [] @@ -69,24 +56,50 @@ class BaseUsage(object): def get_date_range(self): if not hasattr(self, "start") or not hasattr(self, "end"): - args = (self.today.year, self.today.month) + args_start = args_end = (self.today.year, + self.today.month, + self.today.day) form = self.get_form() if form.is_valid(): - args = (int(form.cleaned_data['year']), - int(form.cleaned_data['month'])) - self.start = self.get_start(*args) - self.end = self.get_end(*args) + start = form.cleaned_data['start'] + end = form.cleaned_data['end'] + args_start = (start.year, + start.month, + start.day) + args_end = (end.year, + end.month, + end.day) + elif form.is_bound: + messages.error(self.request, + _("Invalid date format: " + "Using today as default.")) + self.start = self.get_start(*args_start) + self.end = self.get_end(*args_end) + return self.start, self.end + + def init_form(self): + today = datetime.date.today() + first = datetime.date(day=1, month=today.month, year=today.year) + if today.day in range(5): + self.end = first - datetime.timedelta(days=1) + self.start = datetime.date(day=1, + month=self.end.month, + year=self.end.year) + else: + self.end = today + self.start = first return self.start, self.end def get_form(self): if not hasattr(self, 'form'): - if any(key in ['month', 'year'] for key in self.request.GET): + if any(key in ['start', 'end'] for key in self.request.GET): # bound form self.form = forms.DateForm(self.request.GET) else: # non-bound form - self.form = forms.DateForm(initial={'month': self.today.month, - 'year': self.today.year}) + init = self.init_form() + self.form = forms.DateForm(initial={'start': init[0], + 'end': init[1]}) return self.form def get_limits(self): @@ -100,7 +113,7 @@ class BaseUsage(object): raise NotImplementedError("You must define a get_usage method.") def summarize(self, start, end): - if start <= end <= self.today: + if start <= end and start <= 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) @@ -110,10 +123,14 @@ class BaseUsage(object): except: exceptions.handle(self.request, _('Unable to retrieve usage information.')) - else: - messages.info(self.request, - _("You are viewing data for the future, " - "which may or may not exist.")) + elif end < start: + messages.error(self.request, + _("Invalid time period. The end date should be " + "more recent than the start date.")) + elif start > self.today: + messages.error(self.request, + _("Invalid time period. You are requesting " + "data from the future which may not exist.")) for project_usage in self.usage_list: project_summary = project_usage.get_summary() @@ -130,11 +147,13 @@ class BaseUsage(object): def csv_link(self): form = self.get_form() + data = {} if hasattr(form, "cleaned_data"): data = form.cleaned_data - else: - data = {"month": self.today.month, "year": self.today.year} - return "?month=%s&year=%s&format=csv" % (data['month'], data['year']) + if not ('start' in data and 'end' in data): + data = {"start": self.today.date(), "end": self.today.date()} + return "?start=%s&end=%s&format=csv" % (data['start'], + data['end']) class GlobalUsage(BaseUsage): diff --git a/requirements.txt b/requirements.txt index bfd9cddd..b5ef7e67 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,12 +10,12 @@ iso8601>=0.1.4 netaddr python-cinderclient>=1.0.4 python-glanceclient>=0.9.0 -python-heatclient>=0.2.2 +python-heatclient>=0.2.3 python-keystoneclient>=0.3.0 python-novaclient>=2.12.0 python-neutronclient>=2.2.3,<3 python-swiftclient>=1.2 -python-ceilometerclient>=1.0.1 +python-ceilometerclient>=1.0.2 pytz>=2010h # Horizon Utility Requirements # for SECURE_KEY generation @@ -40,8 +40,7 @@ exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,p # F999 syntax error in doctest # H201 no 'except:' at least use 'except Exception:' # H302 import only modules.'from optparse import make_option' does not import a module -# H303 No wildcard (*) import. # H4xx docstrings # H701 empty localization string # H702 Formatting operation should be outside of localization method call -ignore = E121,E126,E127,E128,F403,F999,H201,H302,H303,H4,H701,H702 +ignore = E121,E126,E127,E128,F403,F999,H201,H302,H4,H701,H702 |