summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.mailmap1
-rw-r--r--horizon/exceptions.py5
-rw-r--r--horizon/forms/__init__.py2
-rw-r--r--horizon/forms/base.py14
-rw-r--r--horizon/static/horizon/js/horizon.forms.js23
-rw-r--r--horizon/templates/horizon/common/_usage_summary.html13
-rw-r--r--openstack_dashboard/api/base.py6
-rw-r--r--openstack_dashboard/api/nova.py35
-rw-r--r--openstack_dashboard/dashboards/admin/aggregates/__init__.py0
-rw-r--r--openstack_dashboard/dashboards/admin/aggregates/panel.py29
-rw-r--r--openstack_dashboard/dashboards/admin/aggregates/tables.py54
-rw-r--r--openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_aggregate_hosts.html5
-rw-r--r--openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_aggregate_metadata.html5
-rw-r--r--openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/index.html11
-rw-r--r--openstack_dashboard/dashboards/admin/aggregates/tests.py34
-rw-r--r--openstack_dashboard/dashboards/admin/aggregates/urls.py27
-rw-r--r--openstack_dashboard/dashboards/admin/aggregates/views.py42
-rw-r--r--openstack_dashboard/dashboards/admin/dashboard.py4
-rw-r--r--openstack_dashboard/dashboards/admin/flavors/forms.py54
-rw-r--r--openstack_dashboard/dashboards/admin/flavors/tests.py5
-rw-r--r--openstack_dashboard/dashboards/admin/instances/tables.py5
-rw-r--r--openstack_dashboard/dashboards/admin/instances/tests.py28
-rw-r--r--openstack_dashboard/dashboards/admin/overview/tests.py23
-rw-r--r--openstack_dashboard/dashboards/admin/users/forms.py10
-rw-r--r--openstack_dashboard/dashboards/project/images_and_snapshots/urls.py4
-rw-r--r--openstack_dashboard/dashboards/project/instances/tables.py11
-rw-r--r--openstack_dashboard/dashboards/project/instances/templates/instances/_detail_overview.html4
-rw-r--r--openstack_dashboard/dashboards/project/instances/tests.py92
-rw-r--r--openstack_dashboard/dashboards/project/instances/workflows/__init__.py10
-rw-r--r--openstack_dashboard/dashboards/project/loadbalancers/workflows.py2
-rw-r--r--openstack_dashboard/dashboards/project/overview/tests.py52
-rw-r--r--openstack_dashboard/settings.py2
-rw-r--r--openstack_dashboard/static/bootstrap/less/datepicker.less3
-rw-r--r--openstack_dashboard/static/dashboard/less/horizon.less19
-rw-r--r--openstack_dashboard/test/api_tests/base_tests.py7
-rw-r--r--openstack_dashboard/test/settings.py2
-rw-r--r--openstack_dashboard/test/test_data/nova_data.py40
-rw-r--r--openstack_dashboard/usage/__init__.py2
-rw-r--r--openstack_dashboard/usage/base.py87
-rw-r--r--requirements.txt4
-rw-r--r--tox.ini3
41 files changed, 615 insertions, 164 deletions
diff --git a/.mailmap b/.mailmap
index 771ab656..f59873d1 100644
--- a/.mailmap
+++ b/.mailmap
@@ -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
diff --git a/tox.ini b/tox.ini
index b31c67d3..e413e8d4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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