<html>
  <head>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
  </head>
  <body bgcolor="#FFFFFF" text="#000000">
    <br>
    <div class="moz-cite-prefix">On 11/04/2014 09:07 AM, Aline Manera
      wrote:<br>
    </div>
    <blockquote cite="mid:5458B38B.8010802@linux.vnet.ibm.com"
      type="cite">
      <br>
      On 11/02/2014 11:05 PM, Crístian Viana wrote:
      <br>
      <blockquote type="cite">The new command "POST
        /vms/&lt;vm-name&gt;/clone" will create a new virtual
        <br>
        machine based on &lt;vm-name&gt;. The new VM will have the exact
        same
        <br>
        settings, except the name, UUID, MAC address and disk paths;
        those
        <br>
        values will be generated automatically.
        <br>
        <br>
        Signed-off-by: Crístian Viana <a class="moz-txt-link-rfc2396E" href="mailto:vianac@linux.vnet.ibm.com">&lt;vianac@linux.vnet.ibm.com&gt;</a>
        <br>
        ---
        <br>
          docs/API.md               |   7 ++
        <br>
          src/kimchi/control/vms.py |   1 +
        <br>
          src/kimchi/i18n.py        |   2 +
        <br>
          src/kimchi/model/vms.py   | 297
        +++++++++++++++++++++++++++++++++++++++++++++-
        <br>
          4 files changed, 305 insertions(+), 2 deletions(-)
        <br>
        <br>
        diff --git a/docs/API.md b/docs/API.md
        <br>
        index 29ae4e1..d5f8df6 100644
        <br>
        --- a/docs/API.md
        <br>
        +++ b/docs/API.md
        <br>
        @@ -133,6 +133,13 @@ the following general conventions:
        <br>
                   risk of data loss caused by reset without the guest
        OS shutdown.
        <br>
          * connect: Prepare the connection for spice or vnc
        <br>
          +* clone: Create a new VM identical to this VM. The new VM's
        name, UUID and
        <br>
        +         network MAC addresses will be generated automatically.
        Each existing
        <br>
        +         disks will be copied to a new volume in the same
        storage pool. If
        <br>
        +         there is no available space on that storage pool to
        hold the new
        <br>
        +         volume, it will be created on the pool 'default'. This
        action returns
        <br>
        +         a Task.
        <br>
        +
        <br>
          ### Sub-resource: Virtual Machine Screenshot
        <br>
            **URI:** /vms/*:name*/screenshot
        <br>
        diff --git a/src/kimchi/control/vms.py
        b/src/kimchi/control/vms.py
        <br>
        index 88d8a81..a1589ef 100644
        <br>
        --- a/src/kimchi/control/vms.py
        <br>
        +++ b/src/kimchi/control/vms.py
        <br>
        @@ -46,6 +46,7 @@ class VM(Resource):
        <br>
                  self.shutdown =
        self.generate_action_handler('shutdown')
        <br>
                  self.reset = self.generate_action_handler('reset')
        <br>
                  self.connect = self.generate_action_handler('connect')
        <br>
        +        self.clone = self.generate_action_handler_task('clone')
        <br>
                @property
        <br>
              def data(self):
        <br>
        diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py
        <br>
        index 712e5a6..304c870 100644
        <br>
        --- a/src/kimchi/i18n.py
        <br>
        +++ b/src/kimchi/i18n.py
        <br>
        @@ -102,6 +102,8 @@ messages = {
        <br>
              "KCHVM0030E": _("Unable to get access metadata of virtual
        machine %(name)s. Details: %(err)s"),
        <br>
              "KCHVM0031E": _("The guest console password must be a
        string."),
        <br>
              "KCHVM0032E": _("The life time for the guest console
        password must be a number."),
        <br>
        +    "KCHVM0033E": _("Virtual machine '%(name)s' must be stopped
        before cloning it."),
        <br>
        +    "KCHVM0034E": _("Insufficient disk space to clone virtual
        machine '%(name)s'"),
        <br>
                "KCHVMHDEV0001E": _("VM %(vmid)s does not contain
        directly assigned host device %(dev_name)s."),
        <br>
              "KCHVMHDEV0002E": _("The host device %(dev_name)s is not
        allowed to directly assign to VM."),
        <br>
        diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py
        <br>
        index f2e4ae3..17fe2f4 100644
        <br>
        --- a/src/kimchi/model/vms.py
        <br>
        +++ b/src/kimchi/model/vms.py
        <br>
        @@ -22,6 +22,7 @@ import lxml.etree as ET
        <br>
          from lxml import etree, objectify
        <br>
          import os
        <br>
          import random
        <br>
        +import re
        <br>
          import string
        <br>
          import time
        <br>
          import uuid
        <br>
        @@ -30,17 +31,20 @@ from xml.etree import ElementTree
        <br>
          import libvirt
        <br>
          from cherrypy.process.plugins import BackgroundTask
        <br>
          -from kimchi import vnc
        <br>
        +from kimchi import model, vnc
        <br>
          from kimchi.config import READONLY_POOL_TYPE
        <br>
          from kimchi.exception import InvalidOperation,
        InvalidParameter
        <br>
          from kimchi.exception import NotFoundError, OperationFailed
        <br>
          from kimchi.model.config import CapabilitiesModel
        <br>
        +from kimchi.model.tasks import TaskModel
        <br>
          from kimchi.model.templates import TemplateModel
        <br>
          from kimchi.model.utils import get_vm_name
        <br>
          from kimchi.model.utils import get_metadata_node
        <br>
          from kimchi.model.utils import set_metadata_node
        <br>
        +from kimchi.rollbackcontext import RollbackContext
        <br>
          from kimchi.screenshot import VMScreenshot
        <br>
        -from kimchi.utils import import_class, kimchi_log,
        run_setfacl_set_attr
        <br>
        +from kimchi.utils import add_task, import_class, kimchi_log
        <br>
        +from kimchi.utils import run_setfacl_set_attr
        <br>
          from kimchi.utils import template_name_from_uri
        <br>
          from kimchi.xmlutils.utils import xpath_get_text,
        xml_item_update
        <br>
          @@ -63,6 +67,15 @@ VM_LIVE_UPDATE_PARAMS = {}
        <br>
          stats = {}
        <br>
            +XPATH_DOMAIN_DISK =
        "/domain/devices/disk[@device='disk']/source/@file"
        <br>
        +XPATH_DOMAIN_DISK_BY_FILE =
        "./devices/disk[@device='disk']/source[@file='%s']"
        <br>
        +XPATH_DOMAIN_NAME = '/domain/name'
        <br>
        +XPATH_DOMAIN_MAC =
        "/domain/devices/interface[@type='network']/mac/@address"
        <br>
        +XPATH_DOMAIN_MAC_BY_ADDRESS =
        "./devices/interface[@type='network']/"\
        <br>
        +                              "mac[@address='%s']"
        <br>
        +XPATH_DOMAIN_UUID = '/domain/uuid'
        <br>
        +
        <br>
        +
        <br>
          class VMsModel(object):
        <br>
              def __init__(self, **kargs):
        <br>
                  self.conn = kargs['conn']
        <br>
        @@ -251,6 +264,11 @@ class VMModel(object):
        <br>
                  self.vmscreenshot = VMScreenshotModel(**kargs)
        <br>
                  self.users =
        import_class('kimchi.model.host.UsersModel')(**kargs)
        <br>
                  self.groups =
        import_class('kimchi.model.host.GroupsModel')(**kargs)
        <br>
        +        self.vms = VMsModel(**kargs)
        <br>
        +        self.task = TaskModel(**kargs)
        <br>
        +        self.storagepool =
        model.storagepools.StoragePoolModel(**kargs)
        <br>
        +        self.storagevolume =
        model.storagevolumes.StorageVolumeModel(**kargs)
        <br>
        +        self.storagevolumes =
        model.storagevolumes.StorageVolumesModel(**kargs)
        <br>
                def update(self, name, params):
        <br>
                  dom = self.get_vm(name, self.conn)
        <br>
        @@ -258,6 +276,281 @@ class VMModel(object):
        <br>
                  self._live_vm_update(dom, params)
        <br>
                  return dom.name().decode('utf-8')
        <br>
          +    def clone(self, name):
        <br>
        +        """Clone a virtual machine based on an existing one.
        <br>
        +
        <br>
        +        The new virtual machine will have the exact same
        configuration as the
        <br>
        +        original VM, except for the name, UUID, MAC addresses
        and disks. The
        <br>
        +        name will have the form
        "&lt;name&gt;-clone-&lt;number&gt;", with &lt;number&gt;
        starting
        <br>
        +        at 1; the UUID will be generated randomly; the MAC
        addresses will be
        <br>
        +        generated randomly with no conflicts within the
        original and the new
        <br>
        +        VM; and the disks will be new volumes [mostly] on the
        same storage
        <br>
        +        pool, with the same content as the original disks. The
        storage pool
        <br>
        +        'default' will always be used when cloning SCSI and
        iSCSI disks and
        <br>
        +        when the original storage pool cannot hold the new
        volume.
        <br>
        +
        <br>
        +        An exception will be raised if the virtual machine
        &lt;name&gt; is not
        <br>
        +        shutoff, if there is no available space to copy a new
        volume to the
        <br>
        +        storage pool 'default' (when there was also no space to
        copy it to the
        <br>
        +        original storage pool) and if one of the virtual
        machine's disks belong
        <br>
        +        to a storage pool not supported by Kimchi.
        <br>
        +
        <br>
        +        Parameters:
        <br>
        +        name -- The name of the existing virtual machine to be
        cloned.
        <br>
        +
        <br>
        +        Return:
        <br>
        +        A Task running the clone operation.
        <br>
        +        """
        <br>
        +        name = name.decode('utf-8')
        <br>
        +
        <br>
        +        # VM must be shutoff in order to clone it
        <br>
        +        info = self.lookup(name)
        <br>
        +        if info['state'] != u'shutoff':
        <br>
        +            raise InvalidParameter('KCHVM0033E', {'name':
        name})
        <br>
        +
        <br>
        +        # this name will be used as the Task's 'target_uri' so
        it needs to be
        <br>
        +        # defined now.
        <br>
        +        new_name = self._clone_get_next_name(name)
        <br>
        +
        <br>
        +        # create a task with the actual clone function
        <br>
        +        taskid = add_task(u'/vms/%s' % new_name,
        self._clone_task,
        <br>
        +                          self.objstore,
        <br>
        +                          {'name': name, 'new_name': new_name})
        <br>
        +
        <br>
        +        return self.task.lookup(taskid)
        <br>
        +
        <br>
        +    def _clone_task(self, cb, params):
        <br>
        +        """Asynchronous function which performs the clone
        operation.
        <br>
        +
        <br>
        +        Parameters:
        <br>
        +        cb -- A callback function to signal the Task's
        progress.
        <br>
        +        params -- A dict with the following values:
        <br>
        +            "name": the name of the original VM.
        <br>
        +            "new_name": the name of the new VM.
        <br>
        +        """
        <br>
        +        name = params['name']
        <br>
        +        new_name = params['new_name']
        <br>
        +        vir_conn = self.conn.get()
        <br>
        +
        <br>
        +        # fetch base XML
        <br>
        +        cb('reading source VM XML')
        <br>
        +        vir_dom = vir_conn.lookupByName(name)
        <br>
        +        xml =
        vir_dom.XMLDesc(libvirt.VIR_DOMAIN_XML_SECURE).decode('utf-8')
        <br>
        +
        <br>
        +        # update name
        <br>
        +        cb('updating VM name')
        <br>
        +        xml = xml_item_update(xml, './name', new_name)
        <br>
        +
        <br>
        +        # update UUID
        <br>
        +        cb('updating VM UUID')
        <br>
        +        new_uuid = unicode(uuid.uuid4())
        <br>
        +        xml = xml_item_update(xml, './uuid', new_uuid)
        <br>
        +
        <br>
        +        # update MAC addresses
        <br>
        +        cb('updating VM MAC addresses')
        <br>
        +        xml = self._clone_update_mac_addresses(xml)
        <br>
        +
        <br>
        +        with RollbackContext() as rollback:
        <br>
        +            # copy disks
        <br>
        +            cb('copying VM disks')
        <br>
        +            xml = self._clone_update_disks(xml, rollback)
        <br>
        +
        <br>
        +            # update objstore entry
        <br>
        +            cb('updating object store')
        <br>
        +            self._clone_update_objstore(vir_dom, new_uuid,
        rollback)
        <br>
        +
        <br>
        +            # create new guest
        <br>
        +            cb('defining new VM')
        <br>
        +            vir_conn.defineXML(xml)
        <br>
        +
        <br>
        +            rollback.commitAll()
        <br>
        +
        <br>
        +        cb('OK', True)
        <br>
        +
        <br>
        +    def _clone_get_next_name(self, basename):
        <br>
        +        """Find the next available name for a clone VM.
        <br>
        +
        <br>
        +        If any VM named "&lt;basename&gt;-clone-&lt;number&gt;"
        is found, use
        <br>
        +        the maximum "number" + 1; else, use 1.
        <br>
        +
        <br>
        +        Argument:
        <br>
        +        basename -- the name of the original VM.
        <br>
        +
        <br>
        +        Return:
        <br>
        +        A UTF-8 string in the format
        "&lt;basename&gt;-clone-&lt;number&gt;".
        <br>
        +        """
        <br>
        +        re_group_num = 'num'
        <br>
        +
        <br>
        +        max_num = 0
        <br>
        +        re_cloned_vm = re.compile(u'%s-clone-(?P&lt;%s&gt;\d+)'
        %
        <br>
        +                                  (basename, re_group_num))
        <br>
        +
        <br>
        +        vm_names = self.vms.get_list()
        <br>
        +
        <br>
        +        for v in vm_names:
        <br>
        +            match = re_cloned_vm.match(v)
        <br>
        +            if match is not None:
        <br>
        +                max_num = max(max_num,
        int(match.group(re_group_num)))
        <br>
        +
        <br>
        +        # increments the maximum "clone number" found
        <br>
        +        return u'%s-clone-%d' % (basename, max_num + 1)
        <br>
        +
        <br>
        +    @staticmethod
        <br>
        +    def _clone_update_mac_addresses(xml):
        <br>
        +        """Update the MAC addresses with new values in the XML
        descriptor of a
        <br>
        +        cloning domain.
        <br>
        +
        <br>
        +        The new MAC addresses will be generated randomly, and
        their values are
        <br>
        +        guaranteed to be distinct from the ones in the original
        VM.
        <br>
        +
        <br>
        +        Arguments:
        <br>
        +        xml -- The XML descriptor of the original domain.
        <br>
        +
        <br>
        +        Return:
        <br>
        +        The XML descriptor &lt;xml&gt; with the new MAC
        addresses instead of the
        <br>
        +        old ones.
        <br>
        +        """
        <br>
        +        old_macs = xpath_get_text(xml, XPATH_DOMAIN_MAC)
        <br>
        +        new_macs = []
        <br>
        +
        <br>
        +        for mac in old_macs:
        <br>
        +            while True:
        <br>
        +                new_mac =
        model.vmifaces.VMIfacesModel.random_mac()
        <br>
        +                # make sure the new MAC doesn't conflict with
        the original VM
        <br>
        +                # and with the new values on the new VM.
        <br>
        +                if new_mac not in (old_macs + new_macs):
        <br>
        +                    new_macs.append(new_mac)
        <br>
        +                    break
        <br>
        +
        <br>
        +            xml = xml_item_update(xml,
        XPATH_DOMAIN_MAC_BY_ADDRESS % mac,
        <br>
        +                                  new_mac, 'address')
        <br>
        +
        <br>
        +        return xml
        <br>
        +
        <br>
        +    def _clone_update_disks(self, xml, rollback):
        <br>
        +        """Clone disks from a virtual machine. The disks are
        copied as new
        <br>
        +        volumes and the new VM's XML is updated accordingly.
        <br>
        +
        <br>
        +        Arguments:
        <br>
        +        xml -- The XML descriptor of the original VM + new
        values for
        <br>
        +            "/domain/name" and "/domain/uuid".
        <br>
        +        rollback -- A rollback context so the new volumes can
        be removed if an
        <br>
        +            error occurs during the cloning operation.
        <br>
        +
        <br>
        +        Return:
        <br>
        +        The XML descriptor &lt;xml&gt; with the new disk paths
        instead of the
        <br>
        +        old ones.
        <br>
        +        """
        <br>
        +        # the UUID will be used to create the disk paths
        <br>
        +        uuid = xpath_get_text(xml, XPATH_DOMAIN_UUID)[0]
        <br>
        +        all_paths = xpath_get_text(xml, XPATH_DOMAIN_DISK)
        <br>
        +
        <br>
        +        vir_conn = self.conn.get()
        <br>
        +
        <br>
      </blockquote>
      <br>
      <blockquote type="cite">+        for i, path in
        enumerate(all_paths):
        <br>
        +            vir_orig_vol =
        vir_conn.storageVolLookupByPath(path)
        <br>
        +            vir_pool = vir_orig_vol.storagePoolLookupByVolume()
        <br>
        +
        <br>
      </blockquote>
      <br>
      I've just thought about the case the disk is not in a pool.
      <br>
      For example, we allow user create a Template (and then a VM) using
      an Image file. That image file is not necessarily on a pool. ie,
      it is a volume from libvirt perspective.
      <br>
    </blockquote>
    <br>
    it is <b>NOT</b> a volume from libvirt perspective<br>
    <br>
    <blockquote cite="mid:5458B38B.8010802@linux.vnet.ibm.com"
      type="cite">So in that case the call
      vir_conn.storageVolLookupByPath(path) will raise an exception and
      stop the clone operation.
      <br>
      We need to have this scenario in mind and work with it properly.
      <br>
      <br>
      <br>
      <blockquote type="cite">+            orig_pool_name =
        vir_pool.name().decode('utf-8')
        <br>
        +            orig_vol_name = vir_orig_vol.name().decode('utf-8')
        <br>
        +
        <br>
        +            orig_pool = self.storagepool.lookup(orig_pool_name)
        <br>
        +            orig_vol =
        self.storagevolume.lookup(orig_pool_name, orig_vol_name)
        <br>
        +
        <br>
        +            new_pool_name = orig_pool_name
        <br>
        +            new_pool = orig_pool
        <br>
        +
        <br>
        +            if orig_pool['type'] in ['dir', 'netfs',
        'logical']:
        <br>
        +                # if a volume in a pool 'dir', 'netfs' or
        'logical' cannot hold
        <br>
        +                # a new volume with the same size, the pool
        'default' should
        <br>
        +                # be used
        <br>
        +                if orig_vol['capacity'] &gt;
        orig_pool['available']:
        <br>
        +                    kimchi_log.warning('storage pool \'%s\'
        doesn\'t have '
        <br>
        +                                       'enough free space to
        store image '
        <br>
        +                                       '\'%s\'; falling back to
        \'default\'',
        <br>
        +                                       orig_pool_name, path)
        <br>
        +                    new_pool_name = u'default'
        <br>
        +                    new_pool =
        self.storagepool.lookup(u'default')
        <br>
        +
        <br>
        +                    # ...and if even the pool 'default' cannot
        hold a new
        <br>
        +                    # volume, raise an exception
        <br>
        +                    if orig_vol['capacity'] &gt;
        new_pool['available']:
        <br>
        +                        domain_name = xpath_get_text(xml,
        XPATH_DOMAIN_NAME)[0]
        <br>
        +                        raise InvalidOperation('KCHVM0034E',
        <br>
        +                                               {'name':
        domain_name})
        <br>
        +
        <br>
        +            elif orig_pool['type'] in ['scsi', 'iscsi']:
        <br>
        +                # SCSI and iSCSI always fall back to the
        storage pool 'default'
        <br>
        +                kimchi_log.warning('cannot create new volume
        for clone in '
        <br>
        +                                   'storage pool \'%s\';
        falling back to '
        <br>
        +                                   '\'default\'',
        orig_pool_name)
        <br>
        +                new_pool_name = u'default'
        <br>
        +                new_pool = self.storagepool.lookup(u'default')
        <br>
        +
        <br>
        +                # if the pool 'default' cannot hold a new
        volume, raise
        <br>
        +                # an exception
        <br>
        +                if orig_vol['capacity'] &gt;
        new_pool['available']:
        <br>
        +                    domain_name = xpath_get_text(xml,
        XPATH_DOMAIN_NAME)[0]
        <br>
        +                    raise InvalidOperation('KCHVM0034E',
        {'name': domain_name})
        <br>
        +
        <br>
        +            else:
        <br>
        +                # unexpected storage pool type
        <br>
        +                raise InvalidOperation('KCHPOOL0014E',
        <br>
        +                                       {'type':
        orig_pool['type']})
        <br>
        +
        <br>
        +            # new volume name:
        &lt;UUID&gt;-&lt;loop-index&gt;.&lt;original extension&gt;
        <br>
        +            # e.g. 1234-5678-9012-3456-0.img
        <br>
        +            ext = os.path.splitext(path)[1]
        <br>
        +            new_vol_name = u'%s-%d%s' % (uuid, i, ext)
        <br>
        +            new_vol_params = {'name': new_vol_name,
        'volume_path': path}
        <br>
        +            task = self.storagevolumes.create(new_pool_name,
        new_vol_params)
        <br>
        +            self.task.wait(task['id'], 3600)  # 1 h
        <br>
        +
        <br>
        +            # get the new volume path and update the XML
        descriptor
        <br>
        +            new_vol = self.storagevolume.lookup(new_pool_name,
        new_vol_name)
        <br>
        +            xml = xml_item_update(xml,
        XPATH_DOMAIN_DISK_BY_FILE % path,
        <br>
        +                                  new_vol['path'], 'file')
        <br>
        +
        <br>
        +            # remove the new volume should an error occur later
        <br>
        +            rollback.prependDefer(self.storagevolume.delete,
        new_pool_name,
        <br>
        +                                  new_vol_name)
        <br>
        +
        <br>
        +        return xml
        <br>
        +
        <br>
        +    def _clone_update_objstore(self, dom, new_uuid, rollback):
        <br>
        +        """Update Kimchi's object store with the cloning VM.
        <br>
        +
        <br>
        +        Arguments:
        <br>
        +        dom -- The native domain object of the original VM.
        <br>
        +        new_uuid -- The UUID of the new, clonning VM.
        <br>
        +        rollback -- A rollback context so the object store
        entry can be removed
        <br>
        +            if an error occurs during the cloning operation.
        <br>
        +        """
        <br>
        +        xml =
        dom.XMLDesc(libvirt.VIR_DOMAIN_XML_SECURE).decode('utf-8')
        <br>
        +        old_uuid = xpath_get_text(xml, XPATH_DOMAIN_UUID)[0]
        <br>
        +
        <br>
        +        with self.objstore as session:
        <br>
        +            try:
        <br>
        +                vm = session.get('vm', old_uuid)
        <br>
        +                icon = vm['icon']
        <br>
        +                session.store('vm', new_uuid, {'icon': icon})
        <br>
        +            except NotFoundError:
        <br>
        +                # if we cannot find an object store entry for
        the original VM,
        <br>
        +                # don't store one with an empty value.
        <br>
        +                pass
        <br>
        +            else:
        <br>
        +                # we need to define a custom function to
        prepend to the
        <br>
        +                # rollback context because the object store
        session needs to be
        <br>
        +                # opened and closed correctly (i.e.
        "prependDefer" only
        <br>
        +                # accepts one command at a time but we need
        more than one to
        <br>
        +                # handle an object store).
        <br>
        +                def _rollback_objstore():
        <br>
        +                    with self.objstore as session_rb:
        <br>
        +                        session_rb.delete('vm', new_uuid,
        ignore_missing=True)
        <br>
        +
        <br>
        +                # remove the new object store entry should an
        error occur later
        <br>
        +                rollback.prependDefer(_rollback_objstore)
        <br>
        +
        <br>
              def _build_access_elem(self, users, groups):
        <br>
                  access = E.access()
        <br>
                  for user in users:
        <br>
      </blockquote>
      <br>
      _______________________________________________
      <br>
      Kimchi-devel mailing list
      <br>
      <a class="moz-txt-link-abbreviated" href="mailto:Kimchi-devel@ovirt.org">Kimchi-devel@ovirt.org</a>
      <br>
      <a class="moz-txt-link-freetext" href="http://lists.ovirt.org/mailman/listinfo/kimchi-devel">http://lists.ovirt.org/mailman/listinfo/kimchi-devel</a>
      <br>
    </blockquote>
    <br>
  </body>
</html>