[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