From a286e558c6c47143c84f0e80d9f8c3a9a3ae7bb0 Mon Sep 17 00:00:00 2001 From: Juan Manuel Olle Date: Mon, 24 Feb 2014 14:37:32 -0300 Subject: [PATCH] Need ability to evacuate host in syspanel Implement host evacuate in the hypervisors panel. An extra tab was added to show in the first one hypervisors and on the second one compute host. on each compute host that is down an evacuate host button was added. If the user press the button a modal windows is shown to request the needed data to perform the evacuation. blueprint evacuate-host Co-Authored-By: Leandro Costantino Co-Authored-By: David Lyle Change-Id: I57a16f99fddd84c287429085c7e90beb59a17aa3 --- openstack_dashboard/api/nova.py | 48 ++++++++- .../admin/hypervisors/compute/__init__.py | 0 .../admin/hypervisors/compute/forms.py | 68 +++++++++++++ .../admin/hypervisors/compute/tables.py | 69 +++++++++++++ .../admin/hypervisors/compute/tabs.py | 35 +++++++ .../admin/hypervisors/compute/tests.py | 97 +++++++++++++++++++ .../admin/hypervisors/compute/urls.py | 24 +++++ .../admin/hypervisors/compute/views.py | 53 ++++++++++ .../dashboards/admin/hypervisors/tabs.py | 44 +++++++++ .../hypervisors/compute/_evacuate_host.html | 25 +++++ .../hypervisors/compute/evacuate_host.html | 11 +++ .../templates/hypervisors/index.html | 6 +- .../dashboards/admin/hypervisors/tests.py | 27 +++++- .../dashboards/admin/hypervisors/urls.py | 6 +- .../dashboards/admin/hypervisors/views.py | 8 +- .../test/test_data/nova_data.py | 12 +++ 16 files changed, 524 insertions(+), 9 deletions(-) create mode 100644 openstack_dashboard/dashboards/admin/hypervisors/compute/__init__.py create mode 100644 openstack_dashboard/dashboards/admin/hypervisors/compute/forms.py create mode 100644 openstack_dashboard/dashboards/admin/hypervisors/compute/tables.py create mode 100644 openstack_dashboard/dashboards/admin/hypervisors/compute/tabs.py create mode 100644 openstack_dashboard/dashboards/admin/hypervisors/compute/tests.py create mode 100644 openstack_dashboard/dashboards/admin/hypervisors/compute/urls.py create mode 100644 openstack_dashboard/dashboards/admin/hypervisors/compute/views.py create mode 100644 openstack_dashboard/dashboards/admin/hypervisors/tabs.py create mode 100644 openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/compute/_evacuate_host.html create mode 100644 openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/compute/evacuate_host.html diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 46520be1f1..5a92c961f2 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -120,6 +120,24 @@ class Server(base.APIResourceWrapper): return getattr(self, 'OS-EXT-AZ:availability_zone', "") +class Hypervisor(base.APIDictWrapper): + """Simple wrapper around novaclient.hypervisors.Hypervisor.""" + + _attrs = ['manager', '_loaded', '_info', 'hypervisor_hostname', 'id', + 'servers'] + + @property + def servers(self): + # if hypervisor doesn't have servers, the attribute is not present + servers = [] + try: + servers = self._apidict.servers + except Exception: + pass + + return servers + + class NovaUsage(base.APIResourceWrapper): """Simple wrapper around contrib/simple_usage.py.""" @@ -707,6 +725,32 @@ def hypervisor_search(request, query, servers=True): return novaclient(request).hypervisors.search(query, servers) +def evacuate_host(request, host, target=None, on_shared_storage=False): + # TODO(jmolle) This should be change for nova atomic api host_evacuate + hypervisors = novaclient(request).hypervisors.search(host, True) + response = [] + err_code = None + for hypervisor in hypervisors: + hyper = Hypervisor(hypervisor) + # if hypervisor doesn't have servers, the attribute is not present + for server in hyper.servers: + try: + novaclient(request).servers.evacuate(server['uuid'], + target, + on_shared_storage) + except nova_exceptions.ClientException as err: + err_code = err.code + msg = _("Name: %(name)s ID: %(uuid)s") + msg = msg % {'name': server['name'], 'uuid': server['uuid']} + response.append(msg) + + if err_code: + msg = _('Failed to evacuate instances: %s') % ', '.join(response) + raise nova_exceptions.ClientException(err_code, msg) + + return True + + def tenant_absolute_limits(request, reserved=False): limits = novaclient(request).limits.get(reserved=reserved).absolute limits_dict = {} @@ -723,8 +767,8 @@ def availability_zone_list(request, detailed=False): return novaclient(request).availability_zones.list(detailed=detailed) -def service_list(request): - return novaclient(request).services.list() +def service_list(request, binary=None): + return novaclient(request).services.list(binary=binary) def aggregate_details_list(request): diff --git a/openstack_dashboard/dashboards/admin/hypervisors/compute/__init__.py b/openstack_dashboard/dashboards/admin/hypervisors/compute/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/admin/hypervisors/compute/forms.py b/openstack_dashboard/dashboards/admin/hypervisors/compute/forms.py new file mode 100644 index 0000000000..a480b00e85 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/hypervisors/compute/forms.py @@ -0,0 +1,68 @@ +# 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.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from openstack_dashboard import api + + +class EvacuateHostForm(forms.SelfHandlingForm): + + current_host = forms.CharField(label=_("Current Host"), + widget=forms.TextInput( + attrs={'readonly': 'readonly'})) + target_host = forms.ChoiceField(label=_("Target Host"), + help_text=_("Choose a Host to evacuate servers to.")) + + on_shared_storage = forms.BooleanField(label=_("Shared Storage"), + initial=False, required=False) + + def __init__(self, request, *args, **kwargs): + super(EvacuateHostForm, self).__init__(request, *args, **kwargs) + initial = kwargs.get('initial', {}) + self.fields['target_host'].choices = \ + self.populate_host_choices(request, initial) + + def populate_host_choices(self, request, initial): + hosts = initial.get('hosts') + current_host = initial.get('current_host') + host_list = sorted([(host, host) + for host in hosts + if host != current_host]) + if host_list: + host_list.insert(0, ("", _("Select a target host"))) + else: + host_list.insert(0, ("", _("No other hosts available."))) + return host_list + + def handle(self, request, data): + try: + current_host = data['current_host'] + target_host = data['target_host'] + on_shared_storage = data['on_shared_storage'] + api.nova.evacuate_host(request, current_host, + target_host, on_shared_storage) + + msg = _('Starting evacuation from %(current)s to %(target)s.') % \ + {'current': current_host, 'target': target_host} + messages.success(request, msg) + return True + except Exception: + redirect = reverse('horizon:admin:hypervisors:index') + msg = _('Failed to evacuate host: %s.') % data['current_host'] + exceptions.handle(request, message=msg, redirect=redirect) + return False diff --git a/openstack_dashboard/dashboards/admin/hypervisors/compute/tables.py b/openstack_dashboard/dashboards/admin/hypervisors/compute/tables.py new file mode 100644 index 0000000000..86157b1174 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/hypervisors/compute/tables.py @@ -0,0 +1,69 @@ +# 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 horizon.utils import filters as utils_filters + +from openstack_dashboard import api + + +class EvacuateHost(tables.LinkAction): + name = "evacuate" + data_type_singular = _("Host") + data_type_plural = _("Hosts") + verbose_name = _("Evacuate Host") + url = "horizon:admin:hypervisors:compute:evacuate_host" + classes = ("ajax-modal", "btn-migrate") + policy_rules = (("compute", "compute_extension:evacuate"),) + + def __init__(self, **kwargs): + super(EvacuateHost, self).__init__(**kwargs) + self.name = kwargs.get('name', self.name) + self.action_present = kwargs.get('action_present', _("Evacuate")) + self.action_past = kwargs.get('action_past', _("Evacuated")) + + def allowed(self, request, instance): + if not api.nova.extension_supported('AdminActions', request): + return False + + return self.datum.state == "down" + + +class ComputeHostFilterAction(tables.FilterAction): + def filter(self, table, services, filter_string): + q = filter_string.lower() + + return filter(lambda service: q in service.type.lower(), services) + + +class ComputeHostTable(tables.DataTable): + host = tables.Column('host', verbose_name=_('Host')) + zone = tables.Column('zone', verbose_name=_('Zone')) + status = tables.Column('status', verbose_name=_('Status')) + state = tables.Column('state', verbose_name=_('State')) + updated_at = tables.Column('updated_at', + verbose_name=_('Updated At'), + filters=(utils_filters.parse_isotime, + filters.timesince)) + + def get_object_id(self, obj): + return obj.host + + class Meta: + name = "compute_host" + verbose_name = _("Compute Host") + table_actions = (ComputeHostFilterAction,) + multi_select = False + row_actions = (EvacuateHost,) diff --git a/openstack_dashboard/dashboards/admin/hypervisors/compute/tabs.py b/openstack_dashboard/dashboards/admin/hypervisors/compute/tabs.py new file mode 100644 index 0000000000..d95065d4bc --- /dev/null +++ b/openstack_dashboard/dashboards/admin/hypervisors/compute/tabs.py @@ -0,0 +1,35 @@ +# 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 tabs + +from openstack_dashboard.api import nova +from openstack_dashboard.dashboards.admin.hypervisors.compute import tables + + +class ComputeHostTab(tabs.TableTab): + table_classes = (tables.ComputeHostTable,) + name = _("Compute Host") + slug = "compute_host" + template_name = "horizon/common/_detail_table.html" + + def get_compute_host_data(self): + try: + services = nova.service_list(self.tab_group.request) + return [service for service in services + if service.binary == 'nova-compute'] + except Exception: + msg = _('Unable to get nova services list.') + exceptions.handle(self.tab_group.request, msg) diff --git a/openstack_dashboard/dashboards/admin/hypervisors/compute/tests.py b/openstack_dashboard/dashboards/admin/hypervisors/compute/tests.py new file mode 100644 index 0000000000..b79fa48b8d --- /dev/null +++ b/openstack_dashboard/dashboards/admin/hypervisors/compute/tests.py @@ -0,0 +1,97 @@ +# 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.test import helpers as test + + +class EvacuateHostViewTest(test.BaseAdminViewTests): + @test.create_stubs({api.nova: ('hypervisor_list', + 'hypervisor_stats', + 'service_list')}) + def test_index(self): + hypervisor = self.hypervisors.list().pop().hypervisor_hostname + services = [service for service in self.services.list() + if service.binary == 'nova-compute'] + api.nova.service_list(IsA(http.HttpRequest), + binary='nova-compute').AndReturn(services) + + self.mox.ReplayAll() + + url = reverse('horizon:admin:hypervisors:compute:evacuate_host', + args=[hypervisor]) + res = self.client.get(url) + self.assertTemplateUsed(res, + 'admin/hypervisors/compute/evacuate_host.html') + + @test.create_stubs({api.nova: ('hypervisor_list', + 'hypervisor_stats', + 'service_list', + 'evacuate_host')}) + def test_successful_post(self): + hypervisor = self.hypervisors.list().pop().hypervisor_hostname + services = [service for service in self.services.list() + if service.binary == 'nova-compute'] + + api.nova.service_list(IsA(http.HttpRequest), + binary='nova-compute').AndReturn(services) + api.nova.evacuate_host(IsA(http.HttpRequest), + services[1].host, + services[0].host, + False).AndReturn(True) + self.mox.ReplayAll() + + url = reverse('horizon:admin:hypervisors:compute:evacuate_host', + args=[hypervisor]) + + form_data = {'current_host': services[1].host, + 'target_host': services[0].host, + 'on_shared_storage': False} + + res = self.client.post(url, form_data) + dest_url = reverse('horizon:admin:hypervisors:index') + self.assertNoFormErrors(res) + self.assertMessageCount(success=1) + self.assertRedirectsNoFollow(res, dest_url) + + @test.create_stubs({api.nova: ('hypervisor_list', + 'hypervisor_stats', + 'service_list', + 'evacuate_host')}) + def test_failing_nova_call_post(self): + hypervisor = self.hypervisors.list().pop().hypervisor_hostname + services = [service for service in self.services.list() + if service.binary == 'nova-compute'] + + api.nova.service_list(IsA(http.HttpRequest), + binary='nova-compute').AndReturn(services) + api.nova.evacuate_host(IsA(http.HttpRequest), + services[1].host, + services[0].host, + False).AndRaise(self.exceptions.nova) + self.mox.ReplayAll() + + url = reverse('horizon:admin:hypervisors:compute:evacuate_host', + args=[hypervisor]) + + form_data = {'current_host': services[1].host, + 'target_host': services[0].host, + 'on_shared_storage': False} + + res = self.client.post(url, form_data) + dest_url = reverse('horizon:admin:hypervisors:index') + self.assertMessageCount(error=1) + self.assertRedirectsNoFollow(res, dest_url) \ No newline at end of file diff --git a/openstack_dashboard/dashboards/admin/hypervisors/compute/urls.py b/openstack_dashboard/dashboards/admin/hypervisors/compute/urls.py new file mode 100644 index 0000000000..1717593bfd --- /dev/null +++ b/openstack_dashboard/dashboards/admin/hypervisors/compute/urls.py @@ -0,0 +1,24 @@ +# 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.hypervisors.compute import views + + +urlpatterns = patterns( + 'openstack_dashboard.dashboards.admin.hypervisors.compute.views', + url(r'^(?P[^/]+)/evacuate_host$', + views.EvacuateHostView.as_view(), + name='evacuate_host'), +) diff --git a/openstack_dashboard/dashboards/admin/hypervisors/compute/views.py b/openstack_dashboard/dashboards/admin/hypervisors/compute/views.py new file mode 100644 index 0000000000..c7ab6895a3 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/hypervisors/compute/views.py @@ -0,0 +1,53 @@ +# 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.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms + +from openstack_dashboard import api +from openstack_dashboard.dashboards.admin.hypervisors.compute \ + import forms as project_forms + + +class EvacuateHostView(forms.ModalFormView): + form_class = project_forms.EvacuateHostForm + template_name = 'admin/hypervisors/compute/evacuate_host.html' + context_object_name = 'compute_host' + success_url = reverse_lazy("horizon:admin:hypervisors:index") + + def get_context_data(self, **kwargs): + context = super(EvacuateHostView, self).get_context_data(**kwargs) + context["compute_host"] = self.kwargs['compute_host'] + return context + + def get_active_compute_hosts_names(self, *args, **kwargs): + try: + services = api.nova.service_list(self.request, + binary='nova-compute') + return [service.host for service in services + if service.state == 'up'] + except Exception: + redirect = reverse("horizon:admin:hypervisors:index") + msg = _('Unable to retrieve compute host information.') + exceptions.handle(self.request, msg, redirect=redirect) + + def get_initial(self): + initial = super(EvacuateHostView, self).get_initial() + hosts = self.get_active_compute_hosts_names() + current_host = self.kwargs['compute_host'] + initial.update({'current_host': current_host, + 'hosts': hosts}) + return initial diff --git a/openstack_dashboard/dashboards/admin/hypervisors/tabs.py b/openstack_dashboard/dashboards/admin/hypervisors/tabs.py new file mode 100644 index 0000000000..e8dc9e1e84 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/hypervisors/tabs.py @@ -0,0 +1,44 @@ +# 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 tabs + +from openstack_dashboard.api import nova +from openstack_dashboard.dashboards.admin.hypervisors.compute \ + import tabs as cmp_tabs +from openstack_dashboard.dashboards.admin.hypervisors import tables + + +class HypervisorTab(tabs.TableTab): + table_classes = (tables.AdminHypervisorsTable,) + name = _("Hypervisor") + slug = "hypervisor" + template_name = "horizon/common/_detail_table.html" + + def get_hypervisors_data(self): + hypervisors = [] + try: + hypervisors = nova.hypervisor_list(self.request) + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve hypervisor information.')) + + return hypervisors + + +class HypervisorHostTabs(tabs.TabGroup): + slug = "hypervisor_info" + tabs = (HypervisorTab, cmp_tabs.ComputeHostTab) + sticky = True diff --git a/openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/compute/_evacuate_host.html b/openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/compute/_evacuate_host.html new file mode 100644 index 0000000000..eb94654cef --- /dev/null +++ b/openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/compute/_evacuate_host.html @@ -0,0 +1,25 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% load url from future %} + +{% block form_id %}evacuate_host_form{% endblock %} +{% block form_action %}{% url 'horizon:admin:hypervisors:compute:evacuate_host' compute_host %}{% endblock %} + +{% block modal-header %}{% trans "Evacuate Host" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description:" %}

+

{% trans "Evacuate the servers from the selected down host to an active target host." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/compute/evacuate_host.html b/openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/compute/evacuate_host.html new file mode 100644 index 0000000000..50d065bcc3 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/compute/evacuate_host.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Evacuate Host" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Evacuate Host") %} +{% endblock page_header %} + +{% block main %} + {% include 'admin/hypervisors/compute/_evacuate_host.html' %} +{% endblock %} \ No newline at end of file diff --git a/openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/index.html b/openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/index.html index e524dfa107..0be290b7c3 100644 --- a/openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/index.html +++ b/openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/index.html @@ -31,5 +31,9 @@ -{{ table.render }} +
+
+ {{ tab_group.render }} +
+
{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/hypervisors/tests.py b/openstack_dashboard/dashboards/admin/hypervisors/tests.py index e0c51c4707..88c57f55c2 100644 --- a/openstack_dashboard/dashboards/admin/hypervisors/tests.py +++ b/openstack_dashboard/dashboards/admin/hypervisors/tests.py @@ -21,18 +21,39 @@ from openstack_dashboard.test import helpers as test class HypervisorViewTest(test.BaseAdminViewTests): - @test.create_stubs({api.nova: ('hypervisor_list', - 'hypervisor_stats')}) + @test.create_stubs({api.nova: ('extension_supported', + 'hypervisor_list', + 'hypervisor_stats', + 'service_list')}) def test_index(self): hypervisors = self.hypervisors.list() + services = self.services.list() stats = self.hypervisors.stats + api.nova.extension_supported('AdminActions', + IsA(http.HttpRequest)) \ + .MultipleTimes().AndReturn(True) api.nova.hypervisor_list(IsA(http.HttpRequest)).AndReturn(hypervisors) api.nova.hypervisor_stats(IsA(http.HttpRequest)).AndReturn(stats) + api.nova.service_list(IsA(http.HttpRequest)).AndReturn(services) self.mox.ReplayAll() res = self.client.get(reverse('horizon:admin:hypervisors:index')) self.assertTemplateUsed(res, 'admin/hypervisors/index.html') - self.assertItemsEqual(res.context['table'].data, hypervisors) + + hypervisors_tab = res.context['tab_group'].get_tab('hypervisor') + self.assertItemsEqual(hypervisors_tab._tables['hypervisors'].data, + hypervisors) + + host_tab = res.context['tab_group'].get_tab('compute_host') + host_table = host_tab._tables['compute_host'] + compute_services = [service for service in services + if service.binary == 'nova-compute'] + self.assertItemsEqual(host_table.data, compute_services) + actions_host_up = host_table.get_row_actions(host_table.data[0]) + self.assertEqual(0, len(actions_host_up)) + actions_host_down = host_table.get_row_actions(host_table.data[1]) + self.assertEqual(1, len(actions_host_down)) + self.assertEqual('evacuate', actions_host_down[0].name) class HypervisorDetailViewTest(test.BaseAdminViewTests): diff --git a/openstack_dashboard/dashboards/admin/hypervisors/urls.py b/openstack_dashboard/dashboards/admin/hypervisors/urls.py index 3200a50ea6..30a6c62581 100644 --- a/openstack_dashboard/dashboards/admin/hypervisors/urls.py +++ b/openstack_dashboard/dashboards/admin/hypervisors/urls.py @@ -12,9 +12,12 @@ # License for the specific language governing permissions and limitations # under the License. +from django.conf.urls import include # noqa from django.conf.urls import patterns # noqa from django.conf.urls import url # noqa +from openstack_dashboard.dashboards.admin.hypervisors.compute \ + import urls as compute_urls from openstack_dashboard.dashboards.admin.hypervisors import views @@ -23,5 +26,6 @@ urlpatterns = patterns( url(r'^(?P[^/]+)/$', views.AdminDetailView.as_view(), name='detail'), - url(r'^$', views.AdminIndexView.as_view(), name='index') + url(r'^$', views.AdminIndexView.as_view(), name='index'), + url(r'', include(compute_urls, namespace='compute')), ) diff --git a/openstack_dashboard/dashboards/admin/hypervisors/views.py b/openstack_dashboard/dashboards/admin/hypervisors/views.py index 539e6a819f..4631caa6e9 100644 --- a/openstack_dashboard/dashboards/admin/hypervisors/views.py +++ b/openstack_dashboard/dashboards/admin/hypervisors/views.py @@ -16,14 +16,18 @@ from django.utils.translation import ugettext_lazy as _ from horizon import exceptions from horizon import tables +from horizon import tabs from horizon.utils import functions as utils + from openstack_dashboard import api from openstack_dashboard.dashboards.admin.hypervisors \ import tables as project_tables +from openstack_dashboard.dashboards.admin.hypervisors \ + import tabs as project_tabs -class AdminIndexView(tables.DataTableView): - table_class = project_tables.AdminHypervisorsTable +class AdminIndexView(tabs.TabbedTableView): + tab_group_class = project_tabs.HypervisorHostTabs template_name = 'admin/hypervisors/index.html' def get_data(self): diff --git a/openstack_dashboard/test/test_data/nova_data.py b/openstack_dashboard/test/test_data/nova_data.py index 2c14da6f8d..c82c6072ee 100644 --- a/openstack_dashboard/test/test_data/nova_data.py +++ b/openstack_dashboard/test/test_data/nova_data.py @@ -679,8 +679,20 @@ def data(TEST): "host": "devstack001", "disabled_reason": None, }) + + service_3 = services.Service(services.ServiceManager(None), { + "status": "enabled", + "binary": "nova-compute", + "zone": "nova", + "state": "down", + "updated_at": "2013-07-08T04:20:51.000000", + "host": "devstack002", + "disabled_reason": None, + }) + TEST.services.add(service_1) TEST.services.add(service_2) + TEST.services.add(service_3) # Aggregates aggregate_1 = aggregates.Aggregate(aggregates.AggregateManager(None), {