Merge "Live migration support"
This commit is contained in:
commit
7508917199
@ -563,6 +563,13 @@ def server_migrate(request, instance_id):
|
||||
novaclient(request).servers.migrate(instance_id)
|
||||
|
||||
|
||||
def server_live_migrate(request, instance_id, host, block_migration=False,
|
||||
disk_over_commit=False):
|
||||
novaclient(request).servers.live_migrate(instance_id, host,
|
||||
block_migration,
|
||||
disk_over_commit)
|
||||
|
||||
|
||||
def server_resize(request, instance_id, flavor, **kwargs):
|
||||
novaclient(request).servers.resize(instance_id, flavor, **kwargs)
|
||||
|
||||
|
80
openstack_dashboard/dashboards/admin/instances/forms.py
Normal file
80
openstack_dashboard/dashboards/admin/instances/forms.py
Normal file
@ -0,0 +1,80 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 Kylin OS, Inc
|
||||
#
|
||||
# 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 # noqa
|
||||
from django.utils.translation import ugettext_lazy as _ # noqa
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import messages
|
||||
|
||||
from openstack_dashboard import api
|
||||
|
||||
|
||||
class LiveMigrateForm(forms.SelfHandlingForm):
|
||||
current_host = forms.CharField(label=_("Current Host"),
|
||||
required=False,
|
||||
widget=forms.TextInput(
|
||||
attrs={'readonly': 'readonly'}))
|
||||
host = forms.ChoiceField(label=_("New Host"),
|
||||
required=True,
|
||||
help_text=_("Choose a Host to migrate to."))
|
||||
disk_over_commit = forms.BooleanField(label=_("Disk Over Commit"),
|
||||
initial=False, required=False)
|
||||
block_migration = forms.BooleanField(label=_("Block Migration"),
|
||||
initial=False, required=False)
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(LiveMigrateForm, self).__init__(request, *args, **kwargs)
|
||||
initial = kwargs.get('initial', {})
|
||||
instance_id = initial.get('instance_id')
|
||||
self.fields['instance_id'] = forms.CharField(widget=forms.HiddenInput,
|
||||
initial=instance_id)
|
||||
self.fields['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 = [(host.hypervisor_hostname,
|
||||
host.hypervisor_hostname)
|
||||
for host in hosts
|
||||
if host.hypervisor_hostname != current_host]
|
||||
if host_list:
|
||||
host_list.insert(0, ("", _("Select a new host")))
|
||||
else:
|
||||
host_list.insert(0, ("", _("No other hosts available.")))
|
||||
return sorted(host_list)
|
||||
|
||||
def handle(self, request, data):
|
||||
try:
|
||||
block_migration = data['block_migration']
|
||||
disk_over_commit = data['disk_over_commit']
|
||||
api.nova.server_live_migrate(request,
|
||||
data['instance_id'],
|
||||
data['host'],
|
||||
block_migration=block_migration,
|
||||
disk_over_commit=disk_over_commit)
|
||||
msg = _('The instance is preparing the live migration '
|
||||
'to host "%s".') % data['host']
|
||||
messages.success(request, msg)
|
||||
return True
|
||||
except Exception:
|
||||
msg = _('Failed to live migrate instance to '
|
||||
'host "%s".') % data['host']
|
||||
redirect = reverse('horizon:admin:instances:index')
|
||||
exceptions.handle(request, msg, redirect=redirect)
|
@ -15,8 +15,10 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.core.urlresolvers import reverse # noqa
|
||||
from django.template.defaultfilters import timesince # noqa
|
||||
from django.template.defaultfilters import title # noqa
|
||||
from django.utils.http import urlencode # noqa
|
||||
from django.utils.translation import ugettext_lazy as _ # noqa
|
||||
|
||||
from horizon import tables
|
||||
@ -48,6 +50,17 @@ class MigrateInstance(tables.BatchAction):
|
||||
api.nova.server_migrate(request, obj_id)
|
||||
|
||||
|
||||
class LiveMigrateInstance(tables.LinkAction):
|
||||
name = "live_migrate"
|
||||
verbose_name = _("Live Migrate Instance")
|
||||
url = "horizon:admin:instances:live_migrate"
|
||||
classes = ("ajax-modal", "btn-migrate", "btn-danger")
|
||||
|
||||
def allowed(self, request, instance):
|
||||
return ((instance.status in project_tables.ACTIVE_STATES)
|
||||
and not project_tables.is_deleting(instance))
|
||||
|
||||
|
||||
class AdminUpdateRow(project_tables.UpdateRow):
|
||||
def get_data(self, request, instance_id):
|
||||
instance = super(AdminUpdateRow, self).get_data(request, instance_id)
|
||||
@ -137,6 +150,7 @@ class AdminInstancesTable(tables.DataTable):
|
||||
project_tables.TogglePause,
|
||||
project_tables.ToggleSuspend,
|
||||
MigrateInstance,
|
||||
LiveMigrateInstance,
|
||||
project_tables.SoftRebootInstance,
|
||||
project_tables.RebootInstance,
|
||||
project_tables.TerminateInstance)
|
||||
|
@ -0,0 +1,25 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
{% block form_id %}live_migrate_form{% endblock %}
|
||||
{% block form_action %}{% url 'horizon:admin:instances:live_migrate' instance_id %}{% endblock %}
|
||||
|
||||
{% block modal-header %}{% trans "Live Migrate" %}{% 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 "From here you can live migrate an instance to a specific host." %}</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modal-footer %}
|
||||
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Live Migrate Instance" %}" />
|
||||
<a href="{% url 'horizon:admin:instances:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
|
||||
{% endblock %}
|
@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Live Migrate" %}{% endblock %}
|
||||
|
||||
{% block page_header %}
|
||||
{% include "horizon/common/_page_header.html" with title=_("Live Migrate") %}
|
||||
{% endblock page_header %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'admin/instances/_live_migrate.html' %}
|
||||
{% endblock %}
|
@ -26,6 +26,9 @@ from openstack_dashboard import api
|
||||
from openstack_dashboard.test import helpers as test
|
||||
|
||||
|
||||
INDEX_URL = reverse('horizon:admin:instances:index')
|
||||
|
||||
|
||||
class InstanceViewTest(test.BaseAdminViewTests):
|
||||
@test.create_stubs({api.nova: ('flavor_list', 'server_list',
|
||||
'extension_supported',),
|
||||
@ -45,7 +48,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||
api.nova.flavor_list(IsA(http.HttpRequest)).AndReturn(flavors)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse('horizon:admin:instances:index'))
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertTemplateUsed(res, 'admin/instances/index.html')
|
||||
instances = res.context['table'].data
|
||||
self.assertItemsEqual(instances, servers)
|
||||
@ -75,7 +78,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse('horizon:admin:instances:index'))
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertTemplateUsed(res, 'admin/instances/index.html')
|
||||
instances = res.context['table'].data
|
||||
self.assertItemsEqual(instances, servers)
|
||||
@ -107,7 +110,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||
AndRaise(self.exceptions.nova)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse('horizon:admin:instances:index'))
|
||||
res = self.client.get(INDEX_URL)
|
||||
instances = res.context['table'].data
|
||||
self.assertTemplateUsed(res, 'admin/instances/index.html')
|
||||
self.assertMessageCount(res, error=len(servers))
|
||||
@ -122,7 +125,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse('horizon:admin:instances:index'))
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertTemplateUsed(res, 'admin/instances/index.html')
|
||||
self.assertEqual(len(res.context['instances_table'].data), 0)
|
||||
|
||||
@ -144,7 +147,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||
admin=True).AndReturn(tenant)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:admin:instances:index') + \
|
||||
url = INDEX_URL + \
|
||||
"?action=row_update&table=instances&obj_id=" + server.id
|
||||
|
||||
res = self.client.get(url, {},
|
||||
@ -176,7 +179,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||
AndReturn(self.flavors.list())
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse('horizon:admin:instances:index'))
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertContains(res, "instances__migrate")
|
||||
self.assertNotContains(res, "instances__confirm")
|
||||
self.assertNotContains(res, "instances__revert")
|
||||
@ -202,7 +205,103 @@ class InstanceViewTest(test.BaseAdminViewTests):
|
||||
AndReturn(self.flavors.list())
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(reverse('horizon:admin:instances:index'))
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertContains(res, "instances__confirm")
|
||||
self.assertContains(res, "instances__revert")
|
||||
self.assertNotContains(res, "instances__migrate")
|
||||
|
||||
@test.create_stubs({api.nova: ('hypervisor_list',
|
||||
'server_get',)})
|
||||
def test_instance_live_migrate_get(self):
|
||||
server = self.servers.first()
|
||||
api.nova.server_get(IsA(http.HttpRequest), server.id) \
|
||||
.AndReturn(server)
|
||||
api.nova.hypervisor_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn(self.hypervisors.list())
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:admin:instances:live_migrate',
|
||||
args=[server.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
self.assertTemplateUsed(res, 'admin/instances/live_migrate.html')
|
||||
|
||||
@test.create_stubs({api.nova: ('server_get',)})
|
||||
def test_instance_live_migrate_get_server_get_exception(self):
|
||||
server = self.servers.first()
|
||||
api.nova.server_get(IsA(http.HttpRequest), server.id) \
|
||||
.AndRaise(self.exceptions.nova)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:admin:instances:live_migrate',
|
||||
args=[server.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
@test.create_stubs({api.nova: ('hypervisor_list',
|
||||
'server_get',)})
|
||||
def test_instance_live_migrate_list_hypervisor_get_exception(self):
|
||||
server = self.servers.first()
|
||||
api.nova.server_get(IsA(http.HttpRequest), server.id) \
|
||||
.AndReturn(server)
|
||||
api.nova.hypervisor_list(IsA(http.HttpRequest)) \
|
||||
.AndRaise(self.exceptions.nova)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
url = reverse('horizon:admin:instances:live_migrate',
|
||||
args=[server.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
@test.create_stubs({api.nova: ('hypervisor_list',
|
||||
'server_get',
|
||||
'server_live_migrate',)})
|
||||
def test_instance_live_migrate_post(self):
|
||||
server = self.servers.first()
|
||||
hypervisor = self.hypervisors.first()
|
||||
host = hypervisor.hypervisor_hostname
|
||||
|
||||
api.nova.server_get(IsA(http.HttpRequest), server.id) \
|
||||
.AndReturn(server)
|
||||
api.nova.hypervisor_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn(self.hypervisors.list())
|
||||
api.nova.server_live_migrate(IsA(http.HttpRequest), server.id, host,
|
||||
block_migration=False,
|
||||
disk_over_commit=False) \
|
||||
.AndReturn([])
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:admin:instances:live_migrate',
|
||||
args=[server.id])
|
||||
res = self.client.post(url, {'host': host, 'instance_id': server.id})
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
@test.create_stubs({api.nova: ('hypervisor_list',
|
||||
'server_get',
|
||||
'server_live_migrate',)})
|
||||
def test_instance_live_migrate_post_api_exception(self):
|
||||
server = self.servers.first()
|
||||
hypervisor = self.hypervisors.first()
|
||||
host = hypervisor.hypervisor_hostname
|
||||
|
||||
api.nova.server_get(IsA(http.HttpRequest), server.id) \
|
||||
.AndReturn(server)
|
||||
api.nova.hypervisor_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn(self.hypervisors.list())
|
||||
api.nova.server_live_migrate(IsA(http.HttpRequest), server.id, host,
|
||||
block_migration=False,
|
||||
disk_over_commit=False) \
|
||||
.AndRaise(self.exceptions.nova)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:admin:instances:live_migrate',
|
||||
args=[server.id])
|
||||
res = self.client.post(url, {'host': host, 'instance_id': server.id})
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
@ -37,4 +37,6 @@ urlpatterns = patterns('openstack_dashboard.dashboards.admin.instances.views',
|
||||
url(INSTANCES % 'console', 'console', name='console'),
|
||||
url(INSTANCES % 'vnc', 'vnc', name='vnc'),
|
||||
url(INSTANCES % 'spice', 'spice', name='spice'),
|
||||
url(INSTANCES % 'live_migrate', views.LiveMigrateView.as_view(),
|
||||
name='live_migrate'),
|
||||
)
|
||||
|
@ -19,13 +19,18 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.core.urlresolvers import reverse # noqa
|
||||
from django.core.urlresolvers import reverse_lazy # noqa
|
||||
from django.utils.datastructures import SortedDict # noqa
|
||||
from django.utils.translation import ugettext_lazy as _ # noqa
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import tables
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.dashboards.admin.instances \
|
||||
import forms as project_forms
|
||||
from openstack_dashboard.dashboards.admin.instances \
|
||||
import tables as project_tables
|
||||
from openstack_dashboard.dashboards.project.instances import views
|
||||
@ -108,3 +113,46 @@ class AdminIndexView(tables.DataTableView):
|
||||
tenant = tenant_dict.get(inst.tenant_id, None)
|
||||
inst.tenant_name = getattr(tenant, "name", None)
|
||||
return instances
|
||||
|
||||
|
||||
class LiveMigrateView(forms.ModalFormView):
|
||||
form_class = project_forms.LiveMigrateForm
|
||||
template_name = 'admin/instances/live_migrate.html'
|
||||
context_object_name = 'instance'
|
||||
success_url = reverse_lazy("horizon:admin:instances:index")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(LiveMigrateView, self).get_context_data(**kwargs)
|
||||
context["instance_id"] = self.kwargs['instance_id']
|
||||
return context
|
||||
|
||||
def get_hosts(self, *args, **kwargs):
|
||||
if not hasattr(self, "_hosts"):
|
||||
try:
|
||||
self._hosts = api.nova.hypervisor_list(self.request)
|
||||
except Exception:
|
||||
redirect = reverse("horizon:admin:instances:index")
|
||||
msg = _('Unable to retrieve hypervisor information.')
|
||||
exceptions.handle(self.request, msg, redirect=redirect)
|
||||
return self._hosts
|
||||
|
||||
def get_object(self, *args, **kwargs):
|
||||
if not hasattr(self, "_object"):
|
||||
instance_id = self.kwargs['instance_id']
|
||||
try:
|
||||
self._object = api.nova.server_get(self.request, instance_id)
|
||||
except Exception:
|
||||
redirect = reverse("horizon:admin:instances:index")
|
||||
msg = _('Unable to retrieve instance details.')
|
||||
exceptions.handle(self.request, msg, redirect=redirect)
|
||||
return self._object
|
||||
|
||||
def get_initial(self):
|
||||
initial = super(LiveMigrateView, self).get_initial()
|
||||
_object = self.get_object()
|
||||
if _object:
|
||||
current_host = getattr(_object, 'OS-EXT-SRV-ATTR:host', '')
|
||||
initial.update({'instance_id': self.kwargs['instance_id'],
|
||||
'current_host': current_host,
|
||||
'hosts': self.get_hosts()})
|
||||
return initial
|
||||
|
Loading…
Reference in New Issue
Block a user