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 %}
+
+
+
+
+
{% 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 %}
+
+
+
+
+
{% 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)