From ed1525bc91e1ca68a33117f0dbef4a241f33971f Mon Sep 17 00:00:00 2001 From: Facundo Farias Date: Fri, 7 Feb 2014 18:05:27 -0300 Subject: [PATCH] Host aggregates panel. On this panel, aggregates could be added, deleted and edited. This patch takes the aggregates panel out of System Info and puts it back in the main admin panel list, as now, aggregates are not static information. The host can be associated to a host aggregate on this panel as well. Change-Id: I4ef2d87c33981db36d4ebd3de2f4841cdfa9dbfd Closes-Bug: #1261932 Implements: blueprint manage-host-aggregates Co-Authored-By: Santiago Baldassin Co-Authored-By: Alejandro Paredes --- openstack_dashboard/api/nova.py | 31 ++- .../dashboards/admin/aggregates/__init__.py | 0 .../dashboards/admin/aggregates/constants.py | 21 ++ .../dashboards/admin/aggregates/forms.py | 48 ++++ .../dashboards/admin/aggregates/panel.py | 25 ++ .../dashboards/admin/aggregates/tables.py | 127 +++++++++ .../templates/aggregates/_manage_hosts.html | 29 ++ .../templates/aggregates/_update.html | 26 ++ .../templates/aggregates/create.html | 11 + .../templates/aggregates/index.html | 17 ++ .../templates/aggregates/manage_hosts.html | 11 + .../templates/aggregates/update.html | 12 + .../dashboards/admin/aggregates/tests.py | 256 ++++++++++++++++++ .../dashboards/admin/aggregates/urls.py | 29 ++ .../dashboards/admin/aggregates/views.py | 108 ++++++++ .../dashboards/admin/aggregates/workflows.py | 238 ++++++++++++++++ .../dashboards/admin/dashboard.py | 5 +- .../dashboards/admin/info/constants.py | 17 ++ .../dashboards/admin/info/tables.py | 59 ---- .../dashboards/admin/info/tabs.py | 45 +-- .../dashboards/admin/info/tests.py | 16 +- .../dashboards/admin/info/views.py | 5 +- .../test/test_data/nova_data.py | 23 +- 23 files changed, 1036 insertions(+), 123 deletions(-) create mode 100644 openstack_dashboard/dashboards/admin/aggregates/__init__.py create mode 100644 openstack_dashboard/dashboards/admin/aggregates/constants.py create mode 100644 openstack_dashboard/dashboards/admin/aggregates/forms.py create mode 100644 openstack_dashboard/dashboards/admin/aggregates/panel.py create mode 100644 openstack_dashboard/dashboards/admin/aggregates/tables.py create mode 100644 openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_manage_hosts.html create mode 100644 openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_update.html create mode 100644 openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/create.html create mode 100644 openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/index.html create mode 100644 openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/manage_hosts.html create mode 100644 openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/update.html create mode 100644 openstack_dashboard/dashboards/admin/aggregates/tests.py create mode 100644 openstack_dashboard/dashboards/admin/aggregates/urls.py create mode 100644 openstack_dashboard/dashboards/admin/aggregates/views.py create mode 100644 openstack_dashboard/dashboards/admin/aggregates/workflows.py create mode 100644 openstack_dashboard/dashboards/admin/info/constants.py diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index a74135e67d..ab47c88abd 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -690,15 +690,42 @@ def service_list(request): return novaclient(request).services.list() -def aggregate_list(request): +def aggregate_details_list(request): result = [] c = novaclient(request) for aggregate in c.aggregates.list(): result.append(c.aggregates.get_details(aggregate.id)) - return result +def aggregate_create(request, name, availability_zone=None): + return novaclient(request).aggregates.create(name, availability_zone) + + +def aggregate_delete(request, aggregate_id): + return novaclient(request).aggregates.delete(aggregate_id) + + +def aggregate_get(request, aggregate_id): + return novaclient(request).aggregates.get(aggregate_id) + + +def aggregate_update(request, aggregate_id, values): + return novaclient(request).aggregates.update(aggregate_id, values) + + +def host_list(request): + return novaclient(request).hosts.list() + + +def add_host_to_aggregate(request, aggregate_id, host): + return novaclient(request).aggregates.add_host(aggregate_id, host) + + +def remove_host_from_aggregate(request, aggregate_id, host): + return novaclient(request).aggregates.remove_host(aggregate_id, host) + + @memoized def list_extensions(request): return nova_list_extensions.ListExtManager(novaclient(request)).show_all() diff --git a/openstack_dashboard/dashboards/admin/aggregates/__init__.py b/openstack_dashboard/dashboards/admin/aggregates/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/admin/aggregates/constants.py b/openstack_dashboard/dashboards/admin/aggregates/constants.py new file mode 100644 index 0000000000..4b915c2798 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/constants.py @@ -0,0 +1,21 @@ +# 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. + +AGGREGATES_TEMPLATE_NAME = 'admin/aggregates/index.html' +AGGREGATES_INDEX_URL = 'horizon:admin:aggregates:index' +AGGREGATES_INDEX_VIEW_TEMPLATE = 'admin/aggregates/index.html' +AGGREGATES_CREATE_URL = 'horizon:admin:aggregates:create' +AGGREGATES_CREATE_VIEW_TEMPLATE = 'admin/aggregates/create.html' +AGGREGATES_MANAGE_HOSTS_URL = 'horizon:admin:aggregates:manage_hosts' +AGGREGATES_MANAGE_HOSTS_TEMPLATE = 'admin/aggregates/manage_hosts.html' +AGGREGATES_UPDATE_URL = 'horizon:admin:aggregates:update' +AGGREGATES_UPDATE_VIEW_TEMPLATE = 'admin/aggregates/update.html' diff --git a/openstack_dashboard/dashboards/admin/aggregates/forms.py b/openstack_dashboard/dashboards/admin/aggregates/forms.py new file mode 100644 index 0000000000..752772fbed --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/forms.py @@ -0,0 +1,48 @@ +# 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 _ + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from openstack_dashboard import api +from openstack_dashboard.dashboards.admin.aggregates import constants + +INDEX_URL = constants.AGGREGATES_INDEX_URL + + +class UpdateAggregateForm(forms.SelfHandlingForm): + name = forms.CharField(max_length="255", label=_("Name")) + availability_zone = forms.CharField(label=_("Availability zones"), + required=False) + + def __init__(self, request, *args, **kwargs): + super(UpdateAggregateForm, self).__init__(request, *args, **kwargs) + + def handle(self, request, data): + id = self.initial['id'] + name = data['name'] + availability_zone = data['availability_zone'] + aggregate = {'name': name} + if availability_zone: + aggregate['availability_zone'] = availability_zone + try: + api.nova.aggregate_update(request, id, aggregate) + message = _('Successfully updated aggregate: "%s."') \ + % data['name'] + messages.success(request, message) + except Exception: + exceptions.handle(request, + _('Unable to update the aggregate.')) + return True diff --git a/openstack_dashboard/dashboards/admin/aggregates/panel.py b/openstack_dashboard/dashboards/admin/aggregates/panel.py new file mode 100644 index 0000000000..e6e7c42aab --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/panel.py @@ -0,0 +1,25 @@ +# 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 = _("Host Aggregates") + slug = 'aggregates' + + +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 0000000000..4241142d08 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/tables.py @@ -0,0 +1,127 @@ +# 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.template import defaultfilters as filters +from django.utils.translation import ugettext_lazy as _ + +from horizon import tables + +from openstack_dashboard import api +from openstack_dashboard.dashboards.admin.aggregates import constants + + +class DeleteAggregateAction(tables.DeleteAction): + data_type_singular = _("Host Aggregate") + data_type_plural = _("Host Aggregates") + + def delete(self, request, obj_id): + api.nova.aggregate_delete(request, obj_id) + + +class CreateAggregateAction(tables.LinkAction): + name = "create" + verbose_name = _("Create Host Aggregate") + url = constants.AGGREGATES_CREATE_URL + classes = ("ajax-modal", "btn-create") + + +class ManageHostsAction(tables.LinkAction): + name = "manage" + verbose_name = _("Manage Hosts") + url = constants.AGGREGATES_MANAGE_HOSTS_URL + classes = ("ajax-modal", "btn-create") + + +class UpdateAggregateAction(tables.LinkAction): + name = "update" + verbose_name = _("Edit Host Aggregate") + url = constants.AGGREGATES_UPDATE_URL + classes = ("ajax-modal", "btn-edit") + + +class AggregateFilterAction(tables.FilterAction): + def filter(self, table, aggregates, filter_string): + q = filter_string.lower() + + def comp(aggregate): + return q in aggregate.name.lower() + + return filter(comp, aggregates) + + +class AvailabilityZoneFilterAction(tables.FilterAction): + def filter(self, table, availability_zones, filter_string): + q = filter_string.lower() + + def comp(availabilityZone): + return q in availabilityZone.name.lower() + + return filter(comp, availability_zones) + + +def get_aggregate_hosts(aggregate): + return [host for host in aggregate.hosts] + + +def get_available(zone): + return zone.zoneState['available'] + + +def get_zone_hosts(zone): + hosts = zone.hosts + host_details = [] + for name, services in hosts.items(): + up = all([s['active'] and s['available'] for k, s in services.items()]) + up = _("Services Up") if up else _("Services Down") + host_details.append("%(host)s (%(up)s)" % {'host': name, 'up': up}) + return host_details + + +class HostAggregatesTable(tables.DataTable): + name = tables.Column('name', verbose_name=_('Name')) + availability_zone = tables.Column('availability_zone', + verbose_name=_('Availability Zone')) + hosts = tables.Column(get_aggregate_hosts, + verbose_name=_("Hosts"), + wrap_list=True, + filters=(filters.unordered_list,)) + + class Meta: + name = "host_aggregates" + verbose_name = _("Host Aggregates") + table_actions = (AggregateFilterAction, + CreateAggregateAction, + DeleteAggregateAction) + row_actions = (UpdateAggregateAction, + ManageHostsAction, + DeleteAggregateAction) + + +class AvailabilityZonesTable(tables.DataTable): + name = tables.Column('zoneName', + verbose_name=_('Availability Zone Name')) + hosts = tables.Column(get_zone_hosts, + verbose_name=_('Hosts'), + wrap_list=True, + filters=(filters.unordered_list,)) + available = tables.Column(get_available, + verbose_name=_('Available'), + status=True, + filters=(filters.yesno, filters.capfirst)) + + def get_object_id(self, zone): + return zone.zoneName + + class Meta: + name = "availability_zones" + verbose_name = _("Availability Zones") + table_actions = (AggregateFilterAction,) diff --git a/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_manage_hosts.html b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_manage_hosts.html new file mode 100644 index 0000000000..bafc137fa7 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_manage_hosts.html @@ -0,0 +1,29 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% load url from future %} + +{% block form_id %}{% endblock %} +{% block form_action %}{% url 'horizon:admin:aggregates:manage_hosts' id%}{% endblock %} + +{% block modal_id %}add_aggregate_modal{% endblock %} +{% block modal-header %}{% trans "Manage Hosts" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description" %}:

+

{% blocktrans %} + Here you can add/remove hosts to the selected aggregate host. + Note that while a host can be a member of multiple aggregates, it can belong to one availability zone at most. + {% endblocktrans %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} \ No newline at end of file diff --git a/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_update.html b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_update.html new file mode 100644 index 0000000000..f6f2edb5f6 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/_update.html @@ -0,0 +1,26 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% load url from future %} + +{% block form_id %}edit_aggregate_form{% endblock %} +{% block form_action %}{% url 'horizon:admin:aggregates:update' id %}{% endblock %} + +{% block modal_id %}edit_aggregate_modal{% endblock %} +{% block modal-header %}{% trans "Edit Host Aggregate" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description" %}:

+

{% trans "From here you can edit the aggregate name and availability zone" %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} \ No newline at end of file diff --git a/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/create.html b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/create.html new file mode 100644 index 0000000000..72792c1f7c --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/create.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Host Aggregate" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Host Aggregate") %} +{% endblock page_header %} + +{% block main %} + {% include 'horizon/common/_workflow.html' %} +{% endblock %} 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 0000000000..260d342624 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/index.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Host Aggregates" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Host Aggregates") %} +{% endblock page_header %} + +{% block main %} +
+ {{ host_aggregates_table.render }} +
+ +
+ {{ availability_zones_table.render }} +
+{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/manage_hosts.html b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/manage_hosts.html new file mode 100644 index 0000000000..382efb5431 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/manage_hosts.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Manage Hosts Aggregate" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Manage Hosts Aggregate") %} +{% endblock page_header %} + +{% block main %} + {% include 'admin/aggregates/_manage_hosts.html' %} +{% endblock %} \ No newline at end of file diff --git a/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/update.html b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/update.html new file mode 100644 index 0000000000..45b6b51400 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/templates/aggregates/update.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Edit Host Aggregate" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Edit Host Aggregate") %} +{% endblock page_header %} + + +{% block main %} + {% include 'admin/aggregates/_update.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/aggregates/tests.py b/openstack_dashboard/dashboards/admin/aggregates/tests.py new file mode 100644 index 0000000000..cfe5c7af60 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/tests.py @@ -0,0 +1,256 @@ +# 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 # noqa + +from openstack_dashboard import api +from openstack_dashboard.dashboards.admin.aggregates import constants +from openstack_dashboard.dashboards.admin.aggregates import workflows +from openstack_dashboard.test import helpers as test + + +class BaseAggregateWorkflowTests(test.BaseAdminViewTests): + + def _get_create_workflow_data(self, aggregate, hosts=None): + aggregate_info = {"name": aggregate.name, + "availability_zone": aggregate.availability_zone} + + if hosts: + compute_hosts = [] + for host in hosts: + if host.service == 'compute': + compute_hosts.append(host) + + host_field_name = 'add_host_to_aggregate_role_member' + aggregate_info[host_field_name] = \ + [h.host_name for h in compute_hosts] + + return aggregate_info + + def _get_manage_workflow_data(self, aggregate, hosts=None, ): + aggregate_info = {"id": aggregate.id} + + if hosts: + compute_hosts = [] + for host in hosts: + if host.service == 'compute': + compute_hosts.append(host) + + host_field_name = 'add_host_to_aggregate_role_member' + aggregate_info[host_field_name] = \ + [h.host_name for h in compute_hosts] + + return aggregate_info + + +class CreateAggregateWorkflowTests(BaseAggregateWorkflowTests): + + @test.create_stubs({api.nova: ('host_list', ), }) + def test_workflow_get(self): + + api.nova.host_list(IsA(http.HttpRequest)).AndReturn(self.hosts.list()) + self.mox.ReplayAll() + + url = reverse(constants.AGGREGATES_CREATE_URL) + res = self.client.get(url) + workflow = res.context['workflow'] + + self.assertTemplateUsed(res, constants.AGGREGATES_CREATE_VIEW_TEMPLATE) + self.assertEqual(workflow.name, workflows.CreateAggregateWorkflow.name) + self.assertQuerysetEqual(workflow.steps, + ['', + '']) + + @test.create_stubs({api.nova: ('host_list', 'aggregate_details_list', + 'aggregate_create'), }) + def test_create_aggregate(self): + + aggregate = self.aggregates.first() + + api.nova.host_list(IsA(http.HttpRequest)).AndReturn(self.hosts.list()) + api.nova.aggregate_details_list(IsA(http.HttpRequest)).AndReturn([]) + + workflow_data = self._get_create_workflow_data(aggregate) + api.nova.aggregate_create(IsA(http.HttpRequest), + name=workflow_data['name'], + availability_zone= + workflow_data['availability_zone'])\ + .AndReturn(aggregate) + + self.mox.ReplayAll() + + url = reverse(constants.AGGREGATES_CREATE_URL) + res = self.client.post(url, workflow_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, + reverse(constants.AGGREGATES_INDEX_URL)) + + @test.create_stubs({api.nova: ('host_list', + 'aggregate_details_list', + 'aggregate_create', + 'add_host_to_aggregate'), }) + def test_create_aggregate_with_hosts(self): + + aggregate = self.aggregates.first() + hosts = self.hosts.list() + + api.nova.host_list(IsA(http.HttpRequest)).AndReturn(self.hosts.list()) + api.nova.aggregate_details_list(IsA(http.HttpRequest)).AndReturn([]) + + workflow_data = self._get_create_workflow_data(aggregate, hosts) + api.nova.aggregate_create(IsA(http.HttpRequest), + name=workflow_data['name'], + availability_zone= + workflow_data['availability_zone'])\ + .AndReturn(aggregate) + + compute_hosts = [] + for host in hosts: + if host.service == 'compute': + compute_hosts.append(host) + + for host in compute_hosts: + api.nova.add_host_to_aggregate(IsA(http.HttpRequest), + aggregate.id, host.host_name) + + self.mox.ReplayAll() + + url = reverse(constants.AGGREGATES_CREATE_URL) + res = self.client.post(url, workflow_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, + reverse(constants.AGGREGATES_INDEX_URL)) + + @test.create_stubs({api.nova: ('host_list', 'aggregate_details_list', ), }) + def test_host_list_nova_compute(self): + + hosts = self.hosts.list() + compute_hosts = [] + + for host in hosts: + if host.service == 'compute': + compute_hosts.append(host) + + api.nova.host_list(IsA(http.HttpRequest)).AndReturn(self.hosts.list()) + + self.mox.ReplayAll() + + url = reverse(constants.AGGREGATES_CREATE_URL) + res = self.client.get(url) + workflow = res.context['workflow'] + step = workflow.get_step("add_host_to_aggregate") + field_name = step.get_member_field_name('member') + self.assertEqual(len(step.action.fields[field_name].choices), + len(compute_hosts)) + + +class AggregatesViewTests(test.BaseAdminViewTests): + + @test.create_stubs({api.nova: ('aggregate_details_list', + 'availability_zone_list',), }) + def test_index(self): + api.nova.aggregate_details_list(IsA(http.HttpRequest)) \ + .AndReturn(self.aggregates.list()) + api.nova.availability_zone_list(IsA(http.HttpRequest), detailed=True) \ + .AndReturn(self.availability_zones.list()) + self.mox.ReplayAll() + + res = self.client.get(reverse(constants.AGGREGATES_INDEX_URL)) + self.assertTemplateUsed(res, constants.AGGREGATES_INDEX_VIEW_TEMPLATE) + self.assertItemsEqual(res.context['host_aggregates_table'].data, + self.aggregates.list()) + self.assertItemsEqual(res.context['availability_zones_table'].data, + self.availability_zones.list()) + + @test.create_stubs({api.nova: ('aggregate_update', 'aggregate_get',), }) + def _test_generic_update_aggregate(self, form_data, aggregate, + error_count=0, + expected_error_message=None): + api.nova.aggregate_get(IsA(http.HttpRequest), str(aggregate.id))\ + .AndReturn(aggregate) + if not expected_error_message: + az = form_data['availability_zone'] + aggregate_data = {'name': form_data['name'], + 'availability_zone': az} + api.nova.aggregate_update(IsA(http.HttpRequest), str(aggregate.id), + aggregate_data) + self.mox.ReplayAll() + + res = self.client.post(reverse(constants.AGGREGATES_UPDATE_URL, + args=[aggregate.id]), + form_data) + + if not expected_error_message: + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, + reverse(constants.AGGREGATES_INDEX_URL)) + else: + self.assertFormErrors(res, error_count, expected_error_message) + + def test_update_aggregate(self): + aggregate = self.aggregates.first() + form_data = {'id': aggregate.id, + 'name': 'my_new_name', + 'availability_zone': 'my_new_zone'} + + self._test_generic_update_aggregate(form_data, aggregate) + + def test_update_aggregate_fails_missing_fields(self): + aggregate = self.aggregates.first() + form_data = {'id': aggregate.id} + + self._test_generic_update_aggregate(form_data, aggregate, 1, + u'This field is required') + + +class ManageHostsTests(test.BaseAdminViewTests): + + def test_manage_hosts(self): + aggregate = self.aggregates.first() + res = self.client.get(reverse(constants.AGGREGATES_MANAGE_HOSTS_URL, + args=[aggregate.id])) + self.assertEqual(res.status_code, 200) + self.assertTemplateUsed(res, + constants.AGGREGATES_MANAGE_HOSTS_TEMPLATE) + + @test.create_stubs({api.nova: ('aggregate_get', 'add_host_to_aggregate', + 'host_list')}) + def test_manage_hosts_update_empty_aggregate(self): + aggregate = self.aggregates.first() + aggregate.hosts = [] + host = self.hosts.get(service="compute") + + form_data = {'manageaggregatehostsaction_role_member': + [host.host_name]} + + api.nova.aggregate_get(IsA(http.HttpRequest), str(aggregate.id)) \ + .AndReturn(aggregate) + api.nova.host_list(IsA(http.HttpRequest)) \ + .AndReturn(self.hosts.list()) + api.nova.aggregate_get(IsA(http.HttpRequest), str(aggregate.id)) \ + .AndReturn(aggregate) + api.nova.add_host_to_aggregate(IsA(http.HttpRequest), + str(aggregate.id), host.host_name) + self.mox.ReplayAll() + + res = self.client.post(reverse(constants.AGGREGATES_MANAGE_HOSTS_URL, + args=[aggregate.id]), + form_data) + + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, + reverse(constants.AGGREGATES_INDEX_URL)) diff --git a/openstack_dashboard/dashboards/admin/aggregates/urls.py b/openstack_dashboard/dashboards/admin/aggregates/urls.py new file mode 100644 index 0000000000..2318754b75 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/urls.py @@ -0,0 +1,29 @@ +# 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 import patterns # noqa +from django.conf.urls import url # noqa + +from openstack_dashboard.dashboards.admin.aggregates \ + import views + + +urlpatterns = patterns('openstack_dashboard.dashboards.admin.aggregates.views', + url(r'^$', + views.IndexView.as_view(), name='index'), + url(r'^create/$', + views.CreateView.as_view(), name='create'), + url(r'^(?P[^/]+)/update/$', + views.UpdateView.as_view(), name='update'), + url(r'^(?P[^/]+)/manage_hosts/$', + views.ManageHostsView.as_view(), name='manage_hosts'), +) diff --git a/openstack_dashboard/dashboards/admin/aggregates/views.py b/openstack_dashboard/dashboards/admin/aggregates/views.py new file mode 100644 index 0000000000..aeaf43b497 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/views.py @@ -0,0 +1,108 @@ +# 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_lazy +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import tables +from horizon import workflows + +from openstack_dashboard import api +from openstack_dashboard.dashboards.admin.aggregates \ + import constants +from openstack_dashboard.dashboards.admin.aggregates \ + import forms as aggregate_forms +from openstack_dashboard.dashboards.admin.aggregates \ + import tables as project_tables +from openstack_dashboard.dashboards.admin.aggregates \ + import workflows as aggregate_workflows + + +INDEX_URL = constants.AGGREGATES_INDEX_URL + + +class IndexView(tables.MultiTableView): + table_classes = (project_tables.HostAggregatesTable, + project_tables.AvailabilityZonesTable) + template_name = constants.AGGREGATES_TEMPLATE_NAME + + def get_host_aggregates_data(self): + request = self.request + aggregates = [] + try: + aggregates = api.nova.aggregate_details_list(self.request) + except Exception: + exceptions.handle(request, + _('Unable to retrieve host aggregates list.')) + aggregates.sort(key=lambda aggregate: aggregate.name.lower()) + return aggregates + + def get_availability_zones_data(self): + request = self.request + availability_zones = [] + try: + availability_zones = \ + api.nova.availability_zone_list(self.request, detailed=True) + except Exception: + exceptions.handle(request, + _('Unable to retrieve availability zone list.')) + availability_zones.sort(key=lambda az: az.zoneName.lower()) + return availability_zones + + +class CreateView(workflows.WorkflowView): + workflow_class = aggregate_workflows.CreateAggregateWorkflow + template_name = constants.AGGREGATES_CREATE_VIEW_TEMPLATE + + +class UpdateView(forms.ModalFormView): + template_name = constants.AGGREGATES_UPDATE_VIEW_TEMPLATE + form_class = aggregate_forms.UpdateAggregateForm + success_url = reverse_lazy(constants.AGGREGATES_INDEX_URL) + + def get_initial(self): + aggregate = self.get_object() + return {'id': self.kwargs["id"], + 'name': aggregate.name, + 'availability_zone': aggregate.availability_zone} + + def get_context_data(self, **kwargs): + context = super(UpdateView, self).get_context_data(**kwargs) + context['id'] = self.kwargs['id'] + return context + + def get_object(self): + if not hasattr(self, "_object"): + aggregate_id = self.kwargs['id'] + try: + self._object = \ + api.nova.aggregate_get(self.request, aggregate_id) + except Exception: + msg = _('Unable to retrieve the aggregate to be updated') + exceptions.handle(self.request, msg) + return self._object + + +class ManageHostsView(workflows.WorkflowView): + template_name = constants.AGGREGATES_MANAGE_HOSTS_TEMPLATE + workflow_class = aggregate_workflows.ManageAggregateHostsWorkflow + success_url = reverse_lazy(constants.AGGREGATES_INDEX_URL) + + def get_initial(self): + return {'id': self.kwargs["id"]} + + def get_context_data(self, **kwargs): + context = super(ManageHostsView, self).get_context_data(**kwargs) + context['id'] = self.kwargs['id'] + return context diff --git a/openstack_dashboard/dashboards/admin/aggregates/workflows.py b/openstack_dashboard/dashboards/admin/aggregates/workflows.py new file mode 100644 index 0000000000..c35a87ba7f --- /dev/null +++ b/openstack_dashboard/dashboards/admin/aggregates/workflows.py @@ -0,0 +1,238 @@ +# 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 _ + +from horizon import exceptions +from horizon import forms +from horizon import workflows + +from openstack_dashboard import api +from openstack_dashboard.dashboards.admin.aggregates import constants + + +class SetAggregateInfoAction(workflows.Action): + name = forms.CharField(label=_("Name"), + max_length=255) + + availability_zone = forms.CharField(label=_("Availability Zone"), + max_length=255, + required=False) + + class Meta: + name = _("Host Aggregate Info") + help_text = _("From here you can create a new " + "host aggregate to organize instances.") + slug = "set_aggregate_info" + + def clean(self): + cleaned_data = super(SetAggregateInfoAction, self).clean() + name = cleaned_data.get('name') + + try: + aggregates = api.nova.aggregate_details_list(self.request) + except Exception: + msg = _('Unable to get host aggregate list') + exceptions.check_message(["Connection", "refused"], msg) + raise + if aggregates is not None: + for aggregate in aggregates: + if aggregate.name.lower() == name.lower(): + raise forms.ValidationError( + _('The name "%s" is already used by ' + 'another host aggregate.') + % name + ) + return cleaned_data + + +class SetAggregateInfoStep(workflows.Step): + action_class = SetAggregateInfoAction + contributes = ("availability_zone", + "name") + + +class AddHostsToAggregateAction(workflows.MembershipAction): + def __init__(self, request, *args, **kwargs): + super(AddHostsToAggregateAction, self).__init__(request, + *args, + **kwargs) + err_msg = _('Unable to get the available hosts') + + default_role_field_name = self.get_default_role_field_name() + self.fields[default_role_field_name] = forms.CharField(required=False) + self.fields[default_role_field_name].initial = 'member' + + field_name = self.get_member_field_name('member') + self.fields[field_name] = forms.MultipleChoiceField(required=False) + + hosts = [] + try: + hosts = api.nova.host_list(request) + except Exception: + exceptions.handle(request, err_msg) + + host_names = [] + for host in hosts: + if host.host_name not in host_names and host.service == u'compute': + host_names.append(host.host_name) + host_names.sort() + + self.fields[field_name].choices = \ + [(host_name, host_name) for host_name in host_names] + + class Meta: + name = _("Hosts within aggregate") + slug = "add_host_to_aggregate" + + +class ManageAggregateHostsAction(workflows.MembershipAction): + def __init__(self, request, *args, **kwargs): + super(ManageAggregateHostsAction, self).__init__(request, + *args, + **kwargs) + err_msg = _('Unable to get the available hosts') + + default_role_field_name = self.get_default_role_field_name() + self.fields[default_role_field_name] = forms.CharField(required=False) + self.fields[default_role_field_name].initial = 'member' + + field_name = self.get_member_field_name('member') + self.fields[field_name] = forms.MultipleChoiceField(required=False) + + aggregate_id = self.initial['id'] + aggregate = api.nova.aggregate_get(request, aggregate_id) + aggregate_hosts = aggregate.hosts + + hosts = [] + try: + hosts = api.nova.host_list(request) + except Exception: + exceptions.handle(request, err_msg) + + host_names = [] + for host in hosts: + if host.host_name not in host_names and host.service == u'compute': + host_names.append(host.host_name) + host_names.sort() + + self.fields[field_name].choices = \ + [(host_name, host_name) for host_name in host_names] + + self.fields[field_name].initial = aggregate_hosts + + class Meta: + name = _("Hosts within aggregate") + + +class AddHostsToAggregateStep(workflows.UpdateMembersStep): + action_class = AddHostsToAggregateAction + help_text = _("You can add hosts to this aggregate. One host can be added " + "to one or more aggregate. You can also add the hosts later " + "by editing the aggregate.") + available_list_title = _("All available hosts") + members_list_title = _("Selected hosts") + no_available_text = _("No hosts found.") + no_members_text = _("No host selected.") + show_roles = False + contributes = ("hosts_aggregate",) + + def contribute(self, data, context): + if data: + member_field_name = self.get_member_field_name('member') + context['hosts_aggregate'] = data.get(member_field_name, []) + return context + + +class ManageAggregateHostsStep(workflows.UpdateMembersStep): + action_class = ManageAggregateHostsAction + help_text = _("You can add hosts to this aggregate, as well as remove " + "hosts from it.") + available_list_title = _("All Available Hosts") + members_list_title = _("Selected Hosts") + no_available_text = _("No Hosts found.") + no_members_text = _("No Host selected.") + show_roles = False + depends_on = ("id",) + contributes = ("hosts_aggregate",) + + def contribute(self, data, context): + if data: + member_field_name = self.get_member_field_name('member') + context['hosts_aggregate'] = data.get(member_field_name, []) + return context + + +class CreateAggregateWorkflow(workflows.Workflow): + slug = "create_aggregate" + name = _("Create Host Aggregate") + finalize_button_name = _("Create Host Aggregate") + success_message = _('Created new host aggregate "%s".') + failure_message = _('Unable to create host aggregate "%s".') + success_url = constants.AGGREGATES_INDEX_URL + default_steps = (SetAggregateInfoStep, AddHostsToAggregateStep) + + def format_status_message(self, message): + return message % self.context['name'] + + def handle(self, request, context): + try: + self.object = \ + api.nova.aggregate_create( + request, + name=context['name'], + availability_zone=context['availability_zone']) + except Exception: + exceptions.handle(request, _('Unable to create host aggregate.')) + return False + + hosts = context['hosts_aggregate'] + for host in hosts: + try: + api.nova.add_host_to_aggregate(request, self.object.id, host) + except Exception: + exceptions.handle( + request, _('Error adding Hosts to the aggregate.')) + return False + + return True + + +class ManageAggregateHostsWorkflow(workflows.Workflow): + slug = "manage_hosts_aggregate" + name = _("Add/Remove Hosts to Aggregate") + finalize_button_name = _("Save") + success_message = _('The Aggregate was updated.') + failure_message = _('Unable to update the aggregate.') + success_url = constants.AGGREGATES_INDEX_URL + default_steps = (ManageAggregateHostsStep, ) + + def format_status_message(self, message): + return message + + def handle(self, request, context): + hosts_aggregate = context['hosts_aggregate'] + aggregate_id = context['id'] + aggregate = api.nova.aggregate_get(request, aggregate_id) + aggregate_hosts = aggregate.hosts + for host in aggregate_hosts: + api.nova.remove_host_from_aggregate(request, aggregate_id, host) + + for host in hosts_aggregate: + try: + api.nova.add_host_to_aggregate(request, aggregate_id, host) + except Exception: + exceptions.handle( + request, _('Error updating the aggregate.')) + return False + + return True diff --git a/openstack_dashboard/dashboards/admin/dashboard.py b/openstack_dashboard/dashboards/admin/dashboard.py index 06c9d9d3a8..a4ad7a588f 100644 --- a/openstack_dashboard/dashboards/admin/dashboard.py +++ b/openstack_dashboard/dashboards/admin/dashboard.py @@ -22,8 +22,9 @@ import horizon class SystemPanels(horizon.PanelGroup): slug = "admin" name = _("System Panel") - panels = ('overview', 'metering', 'hypervisors', 'instances', 'volumes', - 'flavors', 'images', 'networks', 'routers', 'defaults', 'info') + panels = ('overview', 'metering', 'hypervisors', 'aggregates', + 'instances', 'volumes', 'flavors', 'images', + 'networks', 'routers', 'defaults', 'info') class IdentityPanels(horizon.PanelGroup): diff --git a/openstack_dashboard/dashboards/admin/info/constants.py b/openstack_dashboard/dashboards/admin/info/constants.py new file mode 100644 index 0000000000..f88419dd21 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/info/constants.py @@ -0,0 +1,17 @@ +# Copyright 2014 Intel Corporation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +INFO_TEMPLATE_NAME = 'admin/info/index.html' +INFO_DETAIL_TEMPLATE_NAME = 'horizon/common/_detail_table.html' diff --git a/openstack_dashboard/dashboards/admin/info/tables.py b/openstack_dashboard/dashboards/admin/info/tables.py index 953c9a3a3e..3250395adc 100644 --- a/openstack_dashboard/dashboards/admin/info/tables.py +++ b/openstack_dashboard/dashboards/admin/info/tables.py @@ -66,37 +66,6 @@ def get_available(zone): return zone.zoneState['available'] -def get_zone_hosts(zone): - hosts = zone.hosts - host_details = [] - for name, services in hosts.items(): - up = all([s['active'] and s['available'] for k, s in services.items()]) - up = _("Services Up") if up else _("Services Down") - host_details.append("%(host)s (%(up)s)" % {'host': name, 'up': up}) - return host_details - - -class ZonesTable(tables.DataTable): - name = tables.Column('zoneName', verbose_name=_('Name')) - hosts = tables.Column(get_zone_hosts, - verbose_name=_('Hosts'), - wrap_list=True, - filters=(filters.unordered_list,)) - available = tables.Column(get_available, - verbose_name=_('Available'), - status=True, - filters=(filters.yesno, filters.capfirst)) - - def get_object_id(self, zone): - return zone.zoneName - - class Meta: - name = "zones" - verbose_name = _("Availability Zones") - multi_select = False - status_columns = ["available"] - - class NovaServiceFilterAction(tables.FilterAction): def filter(self, table, services, filter_string): q = filter_string.lower() @@ -130,34 +99,6 @@ class NovaServicesTable(tables.DataTable): multi_select = False -def get_aggregate_hosts(aggregate): - return [host for host in aggregate.hosts] - - -def get_metadata(aggregate): - return [' = '.join([key, val]) for key, val - in aggregate.metadata.iteritems()] - - -class AggregatesTable(tables.DataTable): - name = tables.Column("name", - verbose_name=_("Name")) - availability_zone = tables.Column("availability_zone", - verbose_name=_("Availability Zone")) - hosts = tables.Column(get_aggregate_hosts, - verbose_name=_("Hosts"), - wrap_list=True, - filters=(filters.unordered_list,)) - metadata = tables.Column(get_metadata, - verbose_name=_("Metadata"), - wrap_list=True, - filters=(filters.unordered_list,)) - - class Meta: - name = "aggregates" - verbose_name = _("Host Aggregates") - - class NetworkAgentsFilterAction(tables.FilterAction): def filter(self, table, agents, filter_string): q = filter_string.lower() diff --git a/openstack_dashboard/dashboards/admin/info/tabs.py b/openstack_dashboard/dashboards/admin/info/tabs.py index 0595f91450..e33b1536c8 100644 --- a/openstack_dashboard/dashboards/admin/info/tabs.py +++ b/openstack_dashboard/dashboards/admin/info/tabs.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 Nebula, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -24,6 +22,7 @@ from openstack_dashboard.api import keystone from openstack_dashboard.api import neutron from openstack_dashboard.api import nova +from openstack_dashboard.dashboards.admin.info import constants from openstack_dashboard.dashboards.admin.info import tables @@ -31,7 +30,7 @@ class ServicesTab(tabs.TableTab): table_classes = (tables.ServicesTable,) name = _("Services") slug = "services" - template_name = ("horizon/common/_detail_table.html") + template_name = constants.INFO_DETAIL_TEMPLATE_NAME def get_services_data(self): request = self.tab_group.request @@ -43,50 +42,16 @@ class ServicesTab(tabs.TableTab): return services -class ZonesTab(tabs.TableTab): - table_classes = (tables.ZonesTable,) - name = _("Availability Zones") - slug = "zones" - template_name = ("horizon/common/_detail_table.html") - - def get_zones_data(self): - request = self.tab_group.request - zones = [] - try: - zones = nova.availability_zone_list(request, detailed=True) - except Exception: - msg = _('Unable to retrieve availability zone data.') - exceptions.handle(request, msg) - return zones - - -class HostAggregatesTab(tabs.TableTab): - table_classes = (tables.AggregatesTable,) - name = _("Host Aggregates") - slug = "aggregates" - template_name = ("horizon/common/_detail_table.html") - - def get_aggregates_data(self): - aggregates = [] - try: - aggregates = nova.aggregate_list(self.tab_group.request) - except Exception: - exceptions.handle(self.request, - _('Unable to retrieve host aggregates list.')) - return aggregates - - class NovaServicesTab(tabs.TableTab): table_classes = (tables.NovaServicesTable,) name = _("Compute Services") slug = "nova_services" - template_name = ("horizon/common/_detail_table.html") + template_name = constants.INFO_DETAIL_TEMPLATE_NAME def get_nova_services_data(self): try: services = nova.service_list(self.tab_group.request) except Exception: - services = [] msg = _('Unable to get nova services list.') exceptions.check_message(["Connection", "refused"], msg) raise @@ -98,7 +63,7 @@ class NetworkAgentsTab(tabs.TableTab): table_classes = (tables.NetworkAgentsTable,) name = _("Network Agents") slug = "network_agents" - template_name = ("horizon/common/_detail_table.html") + template_name = constants.INFO_DETAIL_TEMPLATE_NAME def allowed(self, request): return base.is_service_enabled(request, 'network') @@ -107,7 +72,6 @@ class NetworkAgentsTab(tabs.TableTab): try: agents = neutron.agent_list(self.tab_group.request) except Exception: - agents = [] msg = _('Unable to get network agents list.') exceptions.check_message(["Connection", "refused"], msg) raise @@ -118,6 +82,5 @@ class NetworkAgentsTab(tabs.TableTab): class SystemInfoTabs(tabs.TabGroup): slug = "system_info" tabs = (ServicesTab, NovaServicesTab, - ZonesTab, HostAggregatesTab, NetworkAgentsTab) sticky = True diff --git a/openstack_dashboard/dashboards/admin/info/tests.py b/openstack_dashboard/dashboards/admin/info/tests.py index b5bcb56bde..655ecd4e64 100644 --- a/openstack_dashboard/dashboards/admin/info/tests.py +++ b/openstack_dashboard/dashboards/admin/info/tests.py @@ -26,17 +26,11 @@ INDEX_URL = reverse('horizon:admin:info:index') class SystemInfoViewTests(test.BaseAdminViewTests): - @test.create_stubs({api.nova: ('service_list', - 'availability_zone_list', - 'aggregate_list'), + @test.create_stubs({api.nova: ('service_list',), api.neutron: ('agent_list',)}) def test_index(self): services = self.services.list() api.nova.service_list(IsA(http.HttpRequest)).AndReturn(services) - api.nova.availability_zone_list(IsA(http.HttpRequest), detailed=True) \ - .AndReturn(self.availability_zones.list()) - api.nova.aggregate_list(IsA(http.HttpRequest)) \ - .AndReturn(self.aggregates.list()) agents = self.agents.list() api.neutron.agent_list(IsA(http.HttpRequest)).AndReturn(agents) @@ -59,14 +53,6 @@ class SystemInfoViewTests(test.BaseAdminViewTests): '', '']) - zones_tab = res.context['tab_group'].get_tab('zones') - self.assertQuerysetEqual(zones_tab._tables['zones'].data, - ['']) - - aggregates_tab = res.context['tab_group'].get_tab('aggregates') - self.assertQuerysetEqual(aggregates_tab._tables['aggregates'].data, - ['', '']) - network_agents_tab = res.context['tab_group'].get_tab('network_agents') self.assertQuerysetEqual( network_agents_tab._tables['network_agents'].data, diff --git a/openstack_dashboard/dashboards/admin/info/views.py b/openstack_dashboard/dashboards/admin/info/views.py index fca4f27a4e..29b9bfc53d 100644 --- a/openstack_dashboard/dashboards/admin/info/views.py +++ b/openstack_dashboard/dashboards/admin/info/views.py @@ -1,5 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - # Copyright 2012 United States Government as represented by the # Administrator of the National Aeronautics and Space Administration. # All Rights Reserved. @@ -20,9 +18,10 @@ from horizon import tabs +from openstack_dashboard.dashboards.admin.info import constants from openstack_dashboard.dashboards.admin.info import tabs as project_tabs class IndexView(tabs.TabbedTableView): tab_group_class = project_tabs.SystemInfoTabs - template_name = 'admin/info/index.html' + template_name = constants.INFO_TEMPLATE_NAME diff --git a/openstack_dashboard/test/test_data/nova_data.py b/openstack_dashboard/test/test_data/nova_data.py index e15b28876d..7cd0eadb78 100644 --- a/openstack_dashboard/test/test_data/nova_data.py +++ b/openstack_dashboard/test/test_data/nova_data.py @@ -21,6 +21,7 @@ from novaclient.v1_1 import certs from novaclient.v1_1 import flavor_access from novaclient.v1_1 import flavors from novaclient.v1_1 import floating_ips +from novaclient.v1_1 import hosts from novaclient.v1_1 import hypervisors from novaclient.v1_1 import keypairs from novaclient.v1_1 import quotas @@ -170,6 +171,7 @@ def data(TEST): TEST.hypervisors = utils.TestDataContainer() TEST.services = utils.TestDataContainer() TEST.aggregates = utils.TestDataContainer() + TEST.hosts = utils.TestDataContainer() # Data return by novaclient. # It is used if API layer does data conversion. @@ -616,7 +618,7 @@ def data(TEST): aggregate_1 = aggregates.Aggregate(aggregates.AggregateManager(None), { "name": "foo", - "availability_zone": None, + "availability_zone": "testing", "deleted": 0, "created_at": "2013-07-04T13:34:38.000000", "updated_at": None, @@ -649,3 +651,22 @@ def data(TEST): TEST.aggregates.add(aggregate_1) TEST.aggregates.add(aggregate_2) + + host1 = hosts.Host(hosts.HostManager(None), + { + "host_name": "devstack001", + "service": "compute", + "zone": "testing" + } + ) + + host2 = hosts.Host(hosts.HostManager(None), + { + "host_name": "devstack002", + "service": "nova-conductor", + "zone": "testing" + } + ) + + TEST.hosts.add(host1) + TEST.hosts.add(host2)