diff --git a/openstack_dashboard/api/nova.py b/openstack_dashboard/api/nova.py
index 80f23e153c..440cb40011 100644
--- a/openstack_dashboard/api/nova.py
+++ b/openstack_dashboard/api/nova.py
@@ -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)
diff --git a/openstack_dashboard/dashboards/admin/instances/forms.py b/openstack_dashboard/dashboards/admin/instances/forms.py
new file mode 100644
index 0000000000..4098a048ca
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/instances/forms.py
@@ -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)
diff --git a/openstack_dashboard/dashboards/admin/instances/tables.py b/openstack_dashboard/dashboards/admin/instances/tables.py
index 0a49aad672..535c7606fe 100644
--- a/openstack_dashboard/dashboards/admin/instances/tables.py
+++ b/openstack_dashboard/dashboards/admin/instances/tables.py
@@ -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)
diff --git a/openstack_dashboard/dashboards/admin/instances/templates/instances/_live_migrate.html b/openstack_dashboard/dashboards/admin/instances/templates/instances/_live_migrate.html
new file mode 100644
index 0000000000..a92e18be2f
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/instances/templates/instances/_live_migrate.html
@@ -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 %}
+
+
+
+
+
{% trans "Description:" %}
+
{% trans "From here you can live migrate an instance to a specific host." %}
+
+{% endblock %}
+
+{% block modal-footer %}
+
+ {% trans "Cancel" %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/admin/instances/templates/instances/live_migrate.html b/openstack_dashboard/dashboards/admin/instances/templates/instances/live_migrate.html
new file mode 100644
index 0000000000..749883b540
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/instances/templates/instances/live_migrate.html
@@ -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 %}
diff --git a/openstack_dashboard/dashboards/admin/instances/tests.py b/openstack_dashboard/dashboards/admin/instances/tests.py
index 6beb8fad72..2e20c4cb40 100644
--- a/openstack_dashboard/dashboards/admin/instances/tests.py
+++ b/openstack_dashboard/dashboards/admin/instances/tests.py
@@ -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)
diff --git a/openstack_dashboard/dashboards/admin/instances/urls.py b/openstack_dashboard/dashboards/admin/instances/urls.py
index c0528dad67..ecf0f97148 100644
--- a/openstack_dashboard/dashboards/admin/instances/urls.py
+++ b/openstack_dashboard/dashboards/admin/instances/urls.py
@@ -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'),
)
diff --git a/openstack_dashboard/dashboards/admin/instances/views.py b/openstack_dashboard/dashboards/admin/instances/views.py
index 3397f25994..33f8ad6d76 100644
--- a/openstack_dashboard/dashboards/admin/instances/views.py
+++ b/openstack_dashboard/dashboards/admin/instances/views.py
@@ -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