[Kimchi-devel] [PATCH] Live migration backend: non-shared storage VM migration
Aline Manera
alinefm at linux.vnet.ibm.com
Fri Nov 13 16:52:53 UTC 2015
On 10/11/2015 16:23, dhbarboza82 at gmail.com wrote:
> From: Daniel Henrique Barboza <dhbarboza82 at gmail.com>
>
> This patch implements non-shared storage VM migration in
> the existing live migration backend.
>
> It was necessary to complement libvirt support by creating
> any disk or ISO prior to the migration process in the remote
> destination host.
>
> Signed-off-by: Daniel Henrique Barboza <dhbarboza82 at gmail.com>
> ---
> src/wok/plugins/kimchi/i18n.py | 4 +
> src/wok/plugins/kimchi/model/vms.py | 105 +++++++++++++++++++-
> src/wok/plugins/kimchi/tests/test_livemigration.py | 107 ++++++++++++++++-----
> 3 files changed, 190 insertions(+), 26 deletions(-)
>
> diff --git a/src/wok/plugins/kimchi/i18n.py b/src/wok/plugins/kimchi/i18n.py
> index 42a5e16..4208095 100644
> --- a/src/wok/plugins/kimchi/i18n.py
> +++ b/src/wok/plugins/kimchi/i18n.py
> @@ -115,6 +115,10 @@ messages = {
> "KCHVM0058E": _("Failed to migrate virtual machine %(name)s due error: %(err)s"),
> "KCHVM0059E": _("User name of the remote server must be a string."),
> "KCHVM0060E": _("Destination host of the migration must be a string."),
> + "KCHVM0061E": _("Unable to create file %(path)s at %(host)s using user %(user)s."),
> + "KCHVM0062E": _("Unable to read disk size of %(path)s, error: %(error)s"),
> + "KCHVM0063E": _("Unable to create disk image %(path)s at %(host)s using user %(user)s. Error: %(error)s"),
> +
>
> "KCHVMHDEV0001E": _("VM %(vmid)s does not contain directly assigned host device %(dev_name)s."),
> "KCHVMHDEV0002E": _("The host device %(dev_name)s is not allowed to directly assign to VM."),
> diff --git a/src/wok/plugins/kimchi/model/vms.py b/src/wok/plugins/kimchi/model/vms.py
> index 0641ae8..c427c55 100644
> --- a/src/wok/plugins/kimchi/model/vms.py
> +++ b/src/wok/plugins/kimchi/model/vms.py
> @@ -18,6 +18,7 @@
> # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
>
> import copy
> +import json
> import libvirt
> import lxml.etree as ET
> import os
> @@ -55,6 +56,7 @@ from wok.plugins.kimchi.model.utils import set_metadata_node
> from wok.plugins.kimchi.screenshot import VMScreenshot
> from wok.plugins.kimchi.utils import template_name_from_uri
> from wok.plugins.kimchi.xmlutils.cpu import get_cpu_xml, get_numa_xml
> +from wok.plugins.kimchi.xmlutils.disk import get_vm_disk_info, get_vm_disks
>
>
> DOM_STATE_MAP = {0: 'nostate',
> @@ -1338,6 +1340,87 @@ class VMModel(object):
> self._check_if_host_not_localhost(remote_host)
> self._check_if_password_less_login_enabled(remote_host, user)
>
> + def _check_if_path_exists_in_remote_host(self, path, remote_host, user):
> + username_host = "%s@%s" % (user, remote_host)
> + cmd = ['ssh', '-oStrictHostKeyChecking=no', username_host,
> + 'test', '-f', path]
> + _, _, returncode = run_command(cmd, 5, silent=True)
> + return returncode == 0
> +
> + def _get_vm_devices_infos(self, vm_name):
> + dom = VMModel.get_vm(vm_name, self.conn)
> + infos = [get_vm_disk_info(dom, dev_name)
> + for dev_name in get_vm_disks(dom).keys()]
> + return infos
> +
> + def _check_if_nonshared_migration(self, vm_name, remote_host, user):
> + for dev_info in self._get_vm_devices_infos(vm_name):
> + dev_path = dev_info.get('path')
> + if not self._check_if_path_exists_in_remote_host(
> + dev_path, remote_host, user):
> + return True
> + return False
> +
> + def _create_remote_path(self, path, remote_host, user):
> + username_host = "%s@%s" % (user, remote_host)
> + cmd = ['ssh', '-oStrictHostKeyChecking=no', username_host,
> + 'touch', path]
> + _, _, returncode = run_command(cmd, 5, silent=True)
> + if returncode != 0:
> + raise OperationFailed(
> + "KCHVM0061E",
> + {'path': path, 'host': remote_host, 'user': user}
> + )
> +
> + def _get_img_size(self, disk_path):
> + cmd = ['qemu-img', 'info', '--output=json', disk_path]
> + output, err, returncode = run_command(cmd, silent=True)
> + if returncode != 0:
> + raise OperationFailed(
> + "KCHVM0062E",
> + {'path': disk_path, 'error': err}
> + )
> + output_dict = json.loads(output)
> + return output_dict.get('virtual-size')
> +
Please, use the libvirt API to get the storage volume size instead
running external command. Check "pydoc libvirt.virStorageVol.info"
> + def _create_remote_disk(self, disk_info, remote_host, user):
> + username_host = "%s@%s" % (user, remote_host)
> + disk_fmt = disk_info.get('format')
> + disk_path = disk_info.get('path')
> + disk_size = self._get_img_size(disk_path)
> + cmd = ['ssh', '-oStrictHostKeyChecking=no', username_host,
> + 'qemu-img', 'create', '-f', disk_fmt,
> + disk_path, str(disk_size)]
> + out, err, returncode = run_command(cmd, silent=True)
> + if returncode != 0:
> + raise OperationFailed(
> + "KCHVM0063E",
> + {
> + 'error': err,
> + 'path': disk_path,
> + 'host': remote_host,
> + 'user': user
> + }
> + )
> +
> + def _create_vm_remote_paths(self, vm_name, remote_host, user):
> + for dev_info in self._get_vm_devices_infos(vm_name):
> + dev_path = dev_info.get('path')
> + if not self._check_if_path_exists_in_remote_host(
> + dev_path, remote_host, user):
> + if dev_info.get('type') == 'cdrom':
> + self._create_remote_path(
> + dev_path,
> + remote_host,
> + user
> + )
> + else:
> + self._create_remote_disk(
> + dev_info,
> + remote_host,
> + user
> + )
> +
> def migrate(self, name, remote_host, user='root'):
> name = name.decode('utf-8')
> remote_host = remote_host.decode('utf-8')
> @@ -1345,8 +1428,17 @@ class VMModel(object):
> self.migration_pre_check(remote_host, user)
> dest_conn = self._get_remote_libvirt_conn(remote_host)
>
> + non_shared = self._check_if_nonshared_migration(
> + name,
> + remote_host,
> + user
> + )
> +
> params = {'name': name,
> - 'dest_conn': dest_conn}
> + 'dest_conn': dest_conn,
> + 'non_shared': non_shared,
> + 'remote_host': remote_host,
> + 'user': user}
> task_id = add_task('/vms/%s/migrate' % name, self._migrate_task,
> self.objstore, params)
>
> @@ -1355,6 +1447,9 @@ class VMModel(object):
> def _migrate_task(self, cb, params):
> name = params['name'].decode('utf-8')
> dest_conn = params['dest_conn']
> + non_shared = params['non_shared']
> + remote_host = params['remote_host']
> + user = params['user']
>
> cb('starting a migration')
>
> @@ -1373,6 +1468,14 @@ class VMModel(object):
> dest_conn.close()
> raise OperationFailed("KCHVM0057E", {'name': name,
> 'state': state})
> + if non_shared:
> + flags |= libvirt.VIR_MIGRATE_NON_SHARED_DISK
> + self._create_vm_remote_paths(
> + name,
> + remote_host,
> + user
> + )
> +
> try:
> dom.migrate(dest_conn, flags)
> except libvirt.libvirtError as e:
> diff --git a/src/wok/plugins/kimchi/tests/test_livemigration.py b/src/wok/plugins/kimchi/tests/test_livemigration.py
> index 1fdcd65..b9eb9a4 100644
> --- a/src/wok/plugins/kimchi/tests/test_livemigration.py
> +++ b/src/wok/plugins/kimchi/tests/test_livemigration.py
> @@ -21,7 +21,6 @@ import json
> import libvirt
> import os
> import socket
> -import shutil
> import unittest
> from functools import partial
>
> @@ -29,6 +28,7 @@ from functools import partial
> from wok.basemodel import Singleton
> from wok.exception import OperationFailed
> from wok.rollbackcontext import RollbackContext
> +from wok.utils import run_command
>
>
> from wok.plugins.kimchi.model import model
> @@ -42,14 +42,14 @@ from utils import get_free_port, patch_auth, request
> from utils import run_server, wait_task
>
>
> -TMP_DIR = '/var/lib/kimchi/tests/'
> -UBUNTU_ISO = TMP_DIR + 'ubuntu14.04.iso'
> +ISO_DIR = '/var/lib/libvirt/images/'
> +UBUNTU_ISO = ISO_DIR + 'ubuntu_kimchi_migration_test_14.04.iso'
> KIMCHI_LIVE_MIGRATION_TEST = None
>
>
> def setUpModule():
> - if not os.path.exists(TMP_DIR):
> - os.makedirs(TMP_DIR)
> + if not os.path.exists(ISO_DIR):
> + os.makedirs(ISO_DIR)
> iso_gen.construct_fake_iso(UBUNTU_ISO, True, '14.04', 'ubuntu')
> # Some FeatureTests functions depend on server to validate their result.
> # As CapabilitiesModel is a Singleton class it will get the first result
> @@ -61,7 +61,7 @@ def setUpModule():
>
>
> def tearDownModule():
> - shutil.rmtree(TMP_DIR)
> + os.remove(UBUNTU_ISO)
>
>
> def remoteserver_environment_defined():
> @@ -89,24 +89,40 @@ def check_if_vm_migration_test_possible():
> class LiveMigrationTests(unittest.TestCase):
> def setUp(self):
> self.tmp_store = '/tmp/kimchi-store-test'
> - self.inst = model.Model(objstore_loc=self.tmp_store)
> + self.inst = model.Model(
> + 'qemu:///system',
> + objstore_loc=self.tmp_store
> + )
> params = {'name': u'template_test_vm_migrate',
> 'disks': [],
> 'cdrom': UBUNTU_ISO,
> 'memory': 2048,
> 'max_memory': 4096*1024}
> self.inst.templates_create(params)
> + params = {'name': u'template_test_vm_migrate_nonshared',
> + 'disks': [{'name': 'test_vm_migrate.img', 'size': 1}],
> + 'cdrom': UBUNTU_ISO,
> + 'memory': 2048,
> + 'max_memory': 4096*1024}
> + self.inst.templates_create(params)
>
> def tearDown(self):
> self.inst.template_delete('template_test_vm_migrate')
> + self.inst.template_delete('template_test_vm_migrate_nonshared')
>
> os.unlink(self.tmp_store)
>
> - def create_vm_test(self):
> + def create_vm_test(self, non_shared_storage=False):
> params = {
> 'name': u'test_vm_migrate',
> 'template': u'/plugins/kimchi/templates/template_test_vm_migrate'
> }
> + if non_shared_storage:
> + params = {
> + 'name': u'test_vm_migrate',
> + 'template': u'/plugins/kimchi/templates/'
> + 'template_test_vm_migrate_nonshared'
> + }
> task = self.inst.vms_create(params)
> self.inst.task_wait(task['id'])
>
> @@ -171,8 +187,6 @@ class LiveMigrationTests(unittest.TestCase):
> @unittest.skipUnless(check_if_vm_migration_test_possible(),
> 'not possible to test a live migration')
> def test_vm_livemigrate_persistent(self):
> - inst = model.Model(libvirt_uri='qemu:///system',
> - objstore_loc=self.tmp_store)
>
> with RollbackContext() as rollback:
> self.create_vm_test()
> @@ -189,9 +203,9 @@ class LiveMigrationTests(unittest.TestCase):
> except Exception, e:
> self.fail('Failed to start the vm, reason: %s' % e.message)
> try:
> - task = inst.vm_migrate('test_vm_migrate',
> - KIMCHI_LIVE_MIGRATION_TEST)
> - inst.task_wait(task['id'])
> + task = self.inst.vm_migrate('test_vm_migrate',
> + KIMCHI_LIVE_MIGRATION_TEST)
> + self.inst.task_wait(task['id'])
> self.assertIn('test_vm_migrate', self.get_remote_vm_list())
>
> remote_conn = self.get_remote_conn()
> @@ -208,9 +222,6 @@ class LiveMigrationTests(unittest.TestCase):
> @unittest.skipUnless(check_if_vm_migration_test_possible(),
> 'not possible to test a live migration')
> def test_vm_livemigrate_transient(self):
> - inst = model.Model(libvirt_uri='qemu:///system',
> - objstore_loc=self.tmp_store)
> -
> self.create_vm_test()
>
> with RollbackContext() as rollback:
> @@ -229,9 +240,9 @@ class LiveMigrationTests(unittest.TestCase):
> )
> vm.undefine()
>
> - task = inst.vm_migrate('test_vm_migrate',
> - KIMCHI_LIVE_MIGRATION_TEST)
> - inst.task_wait(task['id'])
> + task = self.inst.vm_migrate('test_vm_migrate',
> + KIMCHI_LIVE_MIGRATION_TEST)
> + self.inst.task_wait(task['id'])
> self.assertIn('test_vm_migrate', self.get_remote_vm_list())
>
> remote_conn = self.get_remote_conn()
> @@ -258,9 +269,6 @@ class LiveMigrationTests(unittest.TestCase):
> @unittest.skipUnless(check_if_vm_migration_test_possible(),
> 'not possible to test shutdown migration')
> def test_vm_coldmigrate(self):
> - inst = model.Model(libvirt_uri='qemu:///system',
> - objstore_loc=self.tmp_store)
> -
> with RollbackContext() as rollback:
> self.create_vm_test()
> rollback.prependDefer(utils.rollback_wrapper, self.inst.vm_delete,
> @@ -272,9 +280,9 @@ class LiveMigrationTests(unittest.TestCase):
> self.inst.vmstorage_delete('test_vm_migrate', dev_list[0])
>
> try:
> - task = inst.vm_migrate('test_vm_migrate',
> - KIMCHI_LIVE_MIGRATION_TEST)
> - inst.task_wait(task['id'])
> + task = self.inst.vm_migrate('test_vm_migrate',
> + KIMCHI_LIVE_MIGRATION_TEST)
> + self.inst.task_wait(task['id'])
> self.assertIn('test_vm_migrate', self.get_remote_vm_list())
>
> remote_conn = self.get_remote_conn()
> @@ -290,6 +298,55 @@ class LiveMigrationTests(unittest.TestCase):
> except Exception, e:
> self.fail('Migration test failed: %s' % e.message)
>
> + def _erase_remote_file(self, path):
> + username_host = "root@%s" % KIMCHI_LIVE_MIGRATION_TEST
> + cmd = ['ssh', '-oStrictHostKeyChecking=no', username_host,
> + 'rm', '-f', path]
> + _, _, returncode = run_command(cmd, silent=True)
> + if returncode != 0:
> + print 'cannot erase remote file ', path
> +
> + @unittest.skipUnless(check_if_vm_migration_test_possible(),
> + 'not possible to test a live migration')
> + def test_vm_livemigrate_persistent_nonshared(self):
> +
> + with RollbackContext() as rollback:
> + self.create_vm_test(non_shared_storage=True)
> + rollback.prependDefer(utils.rollback_wrapper, self.inst.vm_delete,
> + u'test_vm_migrate')
> +
> + # getting disk path info to clean it up later
> + storage_list = self.inst.vmstorages_get_list('test_vm_migrate')
> + disk_info = self.inst.vmstorage_lookup(
> + 'test_vm_migrate',
> + storage_list[0]
> + )
> + disk_path = disk_info.get('path')
> +
> + try:
> + self.inst.vm_start('test_vm_migrate')
> + except Exception, e:
> + self.fail('Failed to start the vm, reason: %s' % e.message)
> + try:
> + task = self.inst.vm_migrate('test_vm_migrate',
> + KIMCHI_LIVE_MIGRATION_TEST)
> + self.inst.task_wait(task['id'], 3600)
> + self.assertIn('test_vm_migrate', self.get_remote_vm_list())
> +
> + remote_conn = self.get_remote_conn()
> + rollback.prependDefer(remote_conn.close)
> +
> + remote_vm = remote_conn.lookupByName('test_vm_migrate')
> + self.assertTrue(remote_vm.isPersistent())
> +
> + remote_vm.destroy()
> + remote_vm.undefine()
> +
> + self._erase_remote_file(disk_path)
> + self._erase_remote_file(UBUNTU_ISO)
> + except Exception, e:
> + self.fail('Migration test failed: %s' % e.message)
> +
> def _task_lookup(self, taskid):
> return json.loads(
> self.request('/plugins/kimchi/tasks/%s' % taskid).read()
More information about the Kimchi-devel
mailing list