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 <bartosz.fic@intel.com>
This commit is contained in:
Bartosz Fic 2015-02-20 22:27:09 +01:00
parent 477faf4c0b
commit 2a7860b416
11 changed files with 422 additions and 2 deletions

View File

@ -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 = {}

View File

@ -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

View File

@ -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
)

View File

@ -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')})

View File

@ -24,4 +24,7 @@ urlpatterns = patterns(
url(r'^(?P<compute_host>[^/]+)/disable_service$',
views.DisableServiceView.as_view(),
name='disable_service'),
url(r'^(?P<compute_host>[^/]+)/migrate_host$',
views.MigrateHostView.as_view(),
name='migrate_host'),
)

View File

@ -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

View File

@ -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 %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description:" %}</h3>
<p>{% 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." %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Migrate Host" %}" />
<a href="{% url 'horizon:admin:hypervisors:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -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 %}

View File

@ -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')})

View File

@ -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)

View File

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