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:
parent
477faf4c0b
commit
2a7860b416
@ -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 = {}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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')})
|
||||
|
@ -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'),
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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 %}
|
@ -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 %}
|
@ -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')})
|
||||
|
@ -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)
|
||||
|
@ -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), {
|
||||
|
Loading…
Reference in New Issue
Block a user