Merge "Live migration support"

This commit is contained in:
Jenkins 2013-12-03 08:00:09 +00:00 committed by Gerrit Code Review
commit 7508917199
8 changed files with 293 additions and 7 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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