From 2a7860b416d38301eda43ddf424bebe2e0f31c94 Mon Sep 17 00:00:00 2001 From: Bartosz Fic Date: Fri, 20 Feb 2015 22:27:09 +0100 Subject: [PATCH] Migrate all instances from host marked for maintenance This patch adds migrate capability to Horizon for host already marked for maintenance. All instances could be cold migrated. There is an option also for running instance to allow making live migration to them. All running instances will be migrated as the same migrating configuration, if the administrator wants to migrate a specific instance in a specific configuration he could do it from the instance dashboard. blueprint migrate-all-instances-from-hosts-in-maintenance-mode Change-Id: Ia1260831e79ede66a9d4320b092bebeb023796bc Co-Authored-By: Bartosz Fic --- openstack_dashboard/api/nova.py | 36 +++++++ .../admin/hypervisors/compute/forms.py | 73 ++++++++++++++ .../admin/hypervisors/compute/tables.py | 37 +++++++- .../admin/hypervisors/compute/tests.py | 92 ++++++++++++++++++ .../admin/hypervisors/compute/urls.py | 3 + .../admin/hypervisors/compute/views.py | 24 +++++ .../hypervisors/compute/_migrate_host.html | 24 +++++ .../hypervisors/compute/migrate_host.html | 11 +++ .../dashboards/admin/hypervisors/tests.py | 11 +++ .../test/api_tests/nova_tests.py | 94 +++++++++++++++++++ .../test/test_data/nova_data.py | 19 +++- 11 files changed, 422 insertions(+), 2 deletions(-) create mode 100644 openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/compute/_migrate_host.html create mode 100644 openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/compute/migrate_host.html diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py index 15bf8cb74c..1103b2e4d6 100644 --- a/openstack_dashboard/api/nova.py +++ b/openstack_dashboard/api/nova.py @@ -792,6 +792,42 @@ def evacuate_host(request, host, target=None, on_shared_storage=False): return True +def migrate_host(request, host, live_migrate=False, disk_over_commit=False, + block_migration=False): + hypervisors = novaclient(request).hypervisors.search(host, True) + response = [] + err_code = None + for hyper in hypervisors: + for server in getattr(hyper, "servers", []): + try: + if live_migrate: + instance = server_get(request, server['uuid']) + + # Checking that instance can be live-migrated + if instance.status in ["ACTIVE", "PAUSED"]: + novaclient(request).servers.live_migrate( + server['uuid'], + None, + block_migration, + disk_over_commit + ) + else: + novaclient(request).servers.migrate(server['uuid']) + else: + novaclient(request).servers.migrate(server['uuid']) + 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 migrate 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 = {} diff --git a/openstack_dashboard/dashboards/admin/hypervisors/compute/forms.py b/openstack_dashboard/dashboards/admin/hypervisors/compute/forms.py index 8083b5d6ea..407eff0adc 100644 --- a/openstack_dashboard/dashboards/admin/hypervisors/compute/forms.py +++ b/openstack_dashboard/dashboards/admin/hypervisors/compute/forms.py @@ -92,3 +92,76 @@ class DisableServiceForm(forms.SelfHandlingForm): data["host"] exceptions.handle(request, message=msg, redirect=redirect) return False + + +class MigrateHostForm(forms.SelfHandlingForm): + current_host = forms.CharField( + label=_("Current Host"), + required=False, + widget=forms.TextInput( + attrs={'readonly': 'readonly'}) + ) + + migrate_type = forms.ChoiceField( + label=_('Running Instance Migration Type'), + choices=[ + ('live_migrate', _('Live Migrate')), + ('cold_migrate', _('Cold Migrate')) + ], + widget=forms.Select( + attrs={ + 'class': 'switchable', + 'data-slug': 'source' + } + ) + ) + + disk_over_commit = forms.BooleanField( + label=_("Disk Over Commit"), + initial=False, + required=False, + widget=forms.CheckboxInput( + attrs={ + 'class': 'switched', + 'data-switch-on': 'source', + 'data-source-live_migrate': _('Disk Over Commit') + } + ) + ) + + block_migration = forms.BooleanField( + label=_("Block Migration"), + initial=False, + required=False, + widget=forms.CheckboxInput( + attrs={ + 'class': 'switched', + 'data-switch-on': 'source', + 'data-source-live_migrate': _('Block Migration') + } + ) + ) + + def handle(self, request, data): + try: + current_host = data['current_host'] + migrate_type = data['migrate_type'] + disk_over_commit = data['disk_over_commit'] + block_migration = data['block_migration'] + live_migrate = migrate_type == 'live_migrate' + api.nova.migrate_host( + request, + current_host, + live_migrate=live_migrate, + disk_over_commit=disk_over_commit, + block_migration=block_migration + ) + msg = _('Starting to migrate host: %(current)s') % \ + {'current': current_host} + messages.success(request, msg) + return True + except Exception: + msg = _('Failed to migrate host "%s".') % data['current_host'] + redirect = reverse('horizon:admin:hypervisors:index') + 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 index 248d936d35..70f48d97f3 100644 --- a/openstack_dashboard/dashboards/admin/hypervisors/compute/tables.py +++ b/openstack_dashboard/dashboards/admin/hypervisors/compute/tables.py @@ -83,6 +83,36 @@ class EnableService(policy.PolicyTargetMixin, tables.BatchAction): api.nova.service_enable(request, obj_id, 'nova-compute') +class MigrateMaintenanceHost(tables.LinkAction): + name = "migrate_maintenance" + policy_rules = (("compute", "compute_extension:admin_actions:migrate"),) + classes = ('ajax-modal', 'btn-migrate', 'btn-danger') + verbose_name = _("Migrate Host") + url = "horizon:admin:hypervisors:compute:migrate_host" + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Migrate Host", + u"Migrate Hosts", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Migrated Host", + u"Migrated Hosts", + count + ) + + def allowed(self, request, service): + if not api.nova.extension_supported('AdminActions', request): + return False + + return service.status == "disabled" + + class ComputeHostFilterAction(tables.FilterAction): def filter(self, table, services, filter_string): q = filter_string.lower() @@ -111,4 +141,9 @@ class ComputeHostTable(tables.DataTable): verbose_name = _("Compute Host") table_actions = (ComputeHostFilterAction,) multi_select = False - row_actions = (EvacuateHost, DisableService, EnableService) + row_actions = ( + EvacuateHost, + DisableService, + EnableService, + MigrateMaintenanceHost + ) diff --git a/openstack_dashboard/dashboards/admin/hypervisors/compute/tests.py b/openstack_dashboard/dashboards/admin/hypervisors/compute/tests.py index 4af9c13b83..9e8f4b3edc 100644 --- a/openstack_dashboard/dashboards/admin/hypervisors/compute/tests.py +++ b/openstack_dashboard/dashboards/admin/hypervisors/compute/tests.py @@ -97,6 +97,98 @@ class EvacuateHostViewTest(test.BaseAdminViewTests): self.assertRedirectsNoFollow(res, dest_url) +class MigrateHostViewTest(test.BaseAdminViewTests): + def test_index(self): + disabled_services = [service for service in self.services.list() + if service.binary == 'nova-compute' + and service.status == 'disabled'] + disabled_service = disabled_services[0] + self.mox.ReplayAll() + url = reverse('horizon:admin:hypervisors:compute:migrate_host', + args=[disabled_service.host]) + res = self.client.get(url) + self.assertNoMessages() + self.assertTemplateUsed(res, + 'admin/hypervisors/compute/migrate_host.html') + + @test.create_stubs({api.nova: ('migrate_host',)}) + def test_maintenance_host_cold_migration_suceed(self): + disabled_services = [service for service in self.services.list() + if service.binary == 'nova-compute' + and service.status == 'disabled'] + disabled_service = disabled_services[0] + api.nova.migrate_host( + IsA(http.HttpRequest), + disabled_service.host, + live_migrate=False, + disk_over_commit=False, + block_migration=False + ).AndReturn(True) + self.mox.ReplayAll() + url = reverse('horizon:admin:hypervisors:compute:migrate_host', + args=[disabled_service.host]) + form_data = {'current_host': disabled_service.host, + 'migrate_type': 'cold_migrate', + 'disk_over_commit': False, + 'block_migration': 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: ('migrate_host',)}) + def test_maintenance_host_live_migration_succeed(self): + disabled_services = [service for service in self.services.list() + if service.binary == 'nova-compute' + and service.status == 'disabled'] + disabled_service = disabled_services[0] + api.nova.migrate_host( + IsA(http.HttpRequest), + disabled_service.host, + live_migrate=True, + disk_over_commit=False, + block_migration=True + ).AndReturn(True) + self.mox.ReplayAll() + url = reverse('horizon:admin:hypervisors:compute:migrate_host', + args=[disabled_service.host]) + form_data = {'current_host': disabled_service.host, + 'migrate_type': 'live_migrate', + 'disk_over_commit': False, + 'block_migration': True} + 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: ('migrate_host',)}) + def test_maintenance_host_migration_fails(self): + disabled_services = [service for service in self.services.list() + if service.binary == 'nova-compute' + and service.status == 'disabled'] + disabled_service = disabled_services[0] + api.nova.migrate_host( + IsA(http.HttpRequest), + disabled_service.host, + live_migrate=True, + disk_over_commit=False, + block_migration=True + ).AndRaise(self.exceptions.nova) + self.mox.ReplayAll() + url = reverse('horizon:admin:hypervisors:compute:migrate_host', + args=[disabled_service.host]) + form_data = {'current_host': disabled_service.host, + 'migrate_type': 'live_migrate', + 'disk_over_commit': False, + 'block_migration': True} + res = self.client.post(url, form_data) + dest_url = reverse('horizon:admin:hypervisors:index') + self.assertMessageCount(error=1) + self.assertRedirectsNoFollow(res, dest_url) + + class DisableServiceViewTest(test.BaseAdminViewTests): @test.create_stubs({api.nova: ('hypervisor_list', 'hypervisor_stats')}) diff --git a/openstack_dashboard/dashboards/admin/hypervisors/compute/urls.py b/openstack_dashboard/dashboards/admin/hypervisors/compute/urls.py index e23a2ad7c8..aa67e23aec 100644 --- a/openstack_dashboard/dashboards/admin/hypervisors/compute/urls.py +++ b/openstack_dashboard/dashboards/admin/hypervisors/compute/urls.py @@ -24,4 +24,7 @@ urlpatterns = patterns( url(r'^(?P[^/]+)/disable_service$', views.DisableServiceView.as_view(), name='disable_service'), + url(r'^(?P[^/]+)/migrate_host$', + views.MigrateHostView.as_view(), + name='migrate_host'), ) diff --git a/openstack_dashboard/dashboards/admin/hypervisors/compute/views.py b/openstack_dashboard/dashboards/admin/hypervisors/compute/views.py index 439d659619..a944f8cecf 100644 --- a/openstack_dashboard/dashboards/admin/hypervisors/compute/views.py +++ b/openstack_dashboard/dashboards/admin/hypervisors/compute/views.py @@ -70,3 +70,27 @@ class DisableServiceView(forms.ModalFormView): initial = super(DisableServiceView, self).get_initial() initial.update({'host': self.kwargs['compute_host']}) return initial + + +class MigrateHostView(forms.ModalFormView): + form_class = project_forms.MigrateHostForm + template_name = 'admin/hypervisors/compute/migrate_host.html' + context_object_name = 'compute_host' + success_url = reverse_lazy("horizon:admin:hypervisors:index") + + def get_context_data(self, **kwargs): + context = super(MigrateHostView, self).get_context_data(**kwargs) + context["compute_host"] = self.kwargs['compute_host'] + return context + + def get_initial(self): + initial = super(MigrateHostView, self).get_initial() + current_host = self.kwargs['compute_host'] + + initial.update({ + 'current_host': current_host, + 'live_migrate': True, + 'block_migration': False, + 'disk_over_commit': False + }) + return initial diff --git a/openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/compute/_migrate_host.html b/openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/compute/_migrate_host.html new file mode 100644 index 0000000000..fef86a9f37 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/compute/_migrate_host.html @@ -0,0 +1,24 @@ +{% 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:migrate_host' compute_host %}{% endblock %} + +{% block modal-header %}{% trans "Migrate Host" %}{% endblock %} +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description:" %}

+

{% trans "Migrate all instances from a host with disabled nova-compute service. Optionally you can choose type of migration. All running instances of the host can be Live Migrated. Cold Migration is trying to use 'nova migrate' on each instance of migrated host." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/compute/migrate_host.html b/openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/compute/migrate_host.html new file mode 100644 index 0000000000..570d8629cd --- /dev/null +++ b/openstack_dashboard/dashboards/admin/hypervisors/templates/hypervisors/compute/migrate_host.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Migrate Host" %}{% endblock %} + +{% block page_header %} +{% include "horizon/common/_page_header.html" with title=_("Migrate Host") %} +{% endblock page_header %} + +{% block main %} + +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/hypervisors/tests.py b/openstack_dashboard/dashboards/admin/hypervisors/tests.py index a854405fbf..7553494e24 100644 --- a/openstack_dashboard/dashboards/admin/hypervisors/tests.py +++ b/openstack_dashboard/dashboards/admin/hypervisors/tests.py @@ -55,6 +55,17 @@ class HypervisorViewTest(test.BaseAdminViewTests): self.assertEqual(2, len(actions_host_down)) self.assertEqual('evacuate', actions_host_down[0].name) + actions_service_enabled = host_table.get_row_actions( + host_table.data[1]) + self.assertEqual('evacuate', actions_service_enabled[0].name) + self.assertEqual('disable', actions_service_enabled[1].name) + + actions_service_disabled = host_table.get_row_actions( + host_table.data[2]) + self.assertEqual('enable', actions_service_disabled[0].name) + self.assertEqual('migrate_maintenance', + actions_service_disabled[1].name) + @test.create_stubs({api.nova: ('hypervisor_list', 'hypervisor_stats', 'service_list')}) diff --git a/openstack_dashboard/test/api_tests/nova_tests.py b/openstack_dashboard/test/api_tests/nova_tests.py index 1a8b911cab..09e0c72382 100644 --- a/openstack_dashboard/test/api_tests/nova_tests.py +++ b/openstack_dashboard/test/api_tests/nova_tests.py @@ -24,6 +24,7 @@ from django import http from django.test.utils import override_settings from mox import IsA # noqa +from novaclient import exceptions as nova_exceptions from novaclient.v1_1 import servers import six @@ -253,3 +254,96 @@ class ComputeApiTests(test.APITestCase): "totalFloatingIpsUsed": 0, } self._test_absolute_limits(values, expected_results) + + def test_cold_migrate_host_succeed(self): + hypervisor = self.hypervisors.first() + novaclient = self.stub_novaclient() + + novaclient.hypervisors = self.mox.CreateMockAnything() + novaclient.hypervisors.search('host', True).AndReturn([hypervisor]) + + novaclient.servers = self.mox.CreateMockAnything() + novaclient.servers.migrate("test_uuid") + + self.mox.ReplayAll() + + ret_val = api.nova.migrate_host(self.request, "host", False, True, + True) + + self.assertTrue(ret_val) + + def test_cold_migrate_host_fails(self): + hypervisor = self.hypervisors.first() + novaclient = self.stub_novaclient() + + novaclient.hypervisors = self.mox.CreateMockAnything() + novaclient.hypervisors.search('host', True).AndReturn([hypervisor]) + + novaclient.servers = self.mox.CreateMockAnything() + novaclient.servers.migrate("test_uuid").AndRaise( + nova_exceptions.ClientException(404)) + + self.mox.ReplayAll() + + self.assertRaises(nova_exceptions.ClientException, + api.nova.migrate_host, + self.request, "host", False, True, True) + + def test_live_migrate_host_with_active_vm(self): + hypervisor = self.hypervisors.first() + server = self.servers.first() + novaclient = self.stub_novaclient() + server_uuid = hypervisor.servers[0]["uuid"] + + novaclient.hypervisors = self.mox.CreateMockAnything() + novaclient.hypervisors.search('host', True).AndReturn([hypervisor]) + + novaclient.servers = self.mox.CreateMockAnything() + novaclient.servers.get(server_uuid).AndReturn(server) + novaclient.servers.live_migrate(server_uuid, None, True, True) + + self.mox.ReplayAll() + + ret_val = api.nova.migrate_host(self.request, "host", True, True, + True) + + self.assertTrue(ret_val) + + def test_live_migrate_host_with_paused_vm(self): + hypervisor = self.hypervisors.first() + server = self.servers.list()[3] + novaclient = self.stub_novaclient() + server_uuid = hypervisor.servers[0]["uuid"] + + novaclient.hypervisors = self.mox.CreateMockAnything() + novaclient.hypervisors.search('host', True).AndReturn([hypervisor]) + + novaclient.servers = self.mox.CreateMockAnything() + novaclient.servers.get(server_uuid).AndReturn(server) + novaclient.servers.live_migrate(server_uuid, None, True, True) + + self.mox.ReplayAll() + + ret_val = api.nova.migrate_host(self.request, "host", True, True, + True) + + self.assertTrue(ret_val) + + def test_live_migrate_host_without_running_vm(self): + hypervisor = self.hypervisors.first() + server = self.servers.list()[1] + novaclient = self.stub_novaclient() + server_uuid = hypervisor.servers[0]["uuid"] + + novaclient.hypervisors = self.mox.CreateMockAnything() + novaclient.hypervisors.search('host', True).AndReturn([hypervisor]) + + novaclient.servers = self.mox.CreateMockAnything() + novaclient.servers.get(server_uuid).AndReturn(server) + novaclient.servers.migrate(server_uuid) + + self.mox.ReplayAll() + + ret_val = api.nova.migrate_host(self.request, "host", True, True, + True) + self.assertTrue(ret_val) diff --git a/openstack_dashboard/test/test_data/nova_data.py b/openstack_dashboard/test/test_data/nova_data.py index 1894c60828..53f46d9026 100644 --- a/openstack_dashboard/test/test_data/nova_data.py +++ b/openstack_dashboard/test/test_data/nova_data.py @@ -481,7 +481,12 @@ def data(TEST): "server_id": "3"}) server_3 = servers.Server(servers.ServerManager(None), json.loads(SERVER_DATA % vals)['server']) - TEST.servers.add(server_1, server_2, server_3) + vals.update({"name": "server_4", + "status": "PAUSED", + "server_id": "4"}) + server_4 = servers.Server(servers.ServerManager(None), + json.loads(SERVER_DATA % vals)['server']) + TEST.servers.add(server_1, server_2, server_3, server_4) # VNC Console Data console = {u'console': {u'url': u'http://example.com:6080/vnc_auto.html', @@ -618,6 +623,7 @@ def data(TEST): "local_gb": 29, "free_ram_mb": 500, "id": 1, + "servers": [{"name": "test_name", "uuid": "test_uuid"}] }, ) @@ -723,9 +729,20 @@ def data(TEST): "disabled_reason": None, }) + service_4 = services.Service(services.ServiceManager(None), { + "status": "disabled", + "binary": "nova-compute", + "zone": "nova", + "state": "up", + "updated_at": "2013-07-08T04:20:51.000000", + "host": "devstack003", + "disabled_reason": None, + }) + TEST.services.add(service_1) TEST.services.add(service_2) TEST.services.add(service_3) + TEST.services.add(service_4) # Aggregates aggregate_1 = aggregates.Aggregate(aggregates.AggregateManager(None), {