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), {