[Kimchi-devel] [WIP PATCH 5/5] Add feature to clone VMs

Crístian Viana vianac at linux.vnet.ibm.com
Fri Oct 17 08:26:52 UTC 2014


Signed-off-by: Crístian Viana <vianac at linux.vnet.ibm.com>
---
 docs/API.md               |   5 ++
 src/kimchi/control/vms.py |   1 +
 src/kimchi/i18n.py        |   1 +
 src/kimchi/model/vms.py   | 116 +++++++++++++++++++++++++++++++++++++++++++++-
 4 files changed, 121 insertions(+), 2 deletions(-)

diff --git a/docs/API.md b/docs/API.md
index 92fbbd5..db8aab0 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -133,6 +133,11 @@ the following general conventions:
          risk of data loss caused by reset without the guest OS shutdown.
 * connect: Prepare the connection for spice or vnc
 
+* clone: Create a new VM identical to this VM. The new VM's name, UUID and
+         network MAC addresses will be generated automatically. Each existing
+         disks will be copied to a new volume in the same storage pool. This
+         action returns a Task.
+
 ### Sub-resource: Virtual Machine Screenshot
 
 **URI:** /vms/*:name*/screenshot
diff --git a/src/kimchi/control/vms.py b/src/kimchi/control/vms.py
index 88d8a81..a1589ef 100644
--- a/src/kimchi/control/vms.py
+++ b/src/kimchi/control/vms.py
@@ -46,6 +46,7 @@ class VM(Resource):
         self.shutdown = self.generate_action_handler('shutdown')
         self.reset = self.generate_action_handler('reset')
         self.connect = self.generate_action_handler('connect')
+        self.clone = self.generate_action_handler_task('clone')
 
     @property
     def data(self):
diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py
index 75fb076..5ac65ef 100644
--- a/src/kimchi/i18n.py
+++ b/src/kimchi/i18n.py
@@ -101,6 +101,7 @@ messages = {
     "KCHVM0030E": _("Unable to get access metadata of virtual machine %(name)s. Details: %(err)s"),
     "KCHVM0031E": _("The guest console password must be a string."),
     "KCHVM0032E": _("The life time for the guest console password must be a number."),
+    "KCHVM0033E": _("Virtual machine '%(name)s' must be stopped before cloning it."),
 
     "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/kimchi/model/vms.py b/src/kimchi/model/vms.py
index 58686cd..5143c2a 100644
--- a/src/kimchi/model/vms.py
+++ b/src/kimchi/model/vms.py
@@ -22,6 +22,8 @@ import lxml.etree as ET
 from lxml import etree, objectify
 import os
 import random
+import re
+import shutil
 import string
 import time
 import uuid
@@ -30,19 +32,21 @@ from xml.etree import ElementTree
 import libvirt
 from cherrypy.process.plugins import BackgroundTask
 
+import kimchi.model
 from kimchi import vnc
 from kimchi import xmlutils
 from kimchi.config import READONLY_POOL_TYPE
 from kimchi.exception import InvalidOperation, InvalidParameter
 from kimchi.exception import NotFoundError, OperationFailed
 from kimchi.model.config import CapabilitiesModel
+from kimchi.model.tasks import TaskModel
 from kimchi.model.templates import TemplateModel
 from kimchi.model.utils import get_vm_name
 from kimchi.model.utils import get_metadata_node
 from kimchi.model.utils import set_metadata_node
 from kimchi.screenshot import VMScreenshot
-from kimchi.utils import import_class, kimchi_log, run_setfacl_set_attr
-from kimchi.utils import template_name_from_uri
+from kimchi.utils import add_task, import_class, kimchi_log
+from kimchi.utils import run_setfacl_set_attr, template_name_from_uri
 from kimchi.xmlutils import xpath_get_text
 
 
@@ -64,6 +68,13 @@ VM_LIVE_UPDATE_PARAMS = {}
 stats = {}
 
 
+XPATH_DISK_SOURCE = "/domain/devices/disk[@device='disk']/source/@file"
+XPATH_INTERFACE_MAC = "/domain/devices/interface[@type='network']/mac/@address"
+XPATH_DISK_BY_FILE = "./devices/disk[@device='disk']/source[@file='%s']"
+XPATH_INTERFACE_BY_ADDRESS = "./devices/interface[@type='network']/"\
+                             "mac[@address='%s']"
+
+
 class VMsModel(object):
     def __init__(self, **kargs):
         self.conn = kargs['conn']
@@ -245,6 +256,8 @@ class VMModel(object):
         self.vmscreenshot = VMScreenshotModel(**kargs)
         self.users = import_class('kimchi.model.host.UsersModel')(**kargs)
         self.groups = import_class('kimchi.model.host.GroupsModel')(**kargs)
+        self.vms = VMsModel(**kargs)
+        self.task = TaskModel(**kargs)
 
     def update(self, name, params):
         dom = self.get_vm(name, self.conn)
@@ -252,6 +265,105 @@ class VMModel(object):
         self._live_vm_update(dom, params)
         return dom.name().decode('utf-8')
 
+    def clone(self, name):
+        name = name.decode('utf-8')
+
+        # VM must be shutoff in order to clone it
+        info = self.lookup(name)
+        if info['state'] != 'shutoff':
+            raise InvalidParameter('KCHVM0033E', {'name': name})
+
+        # find out the next available clone number for that VM:
+        # if any VM named "<name>-clone<number>" is found, use the
+        # maximum "number" + 1; else, use 1.
+        max_num = 0
+        re_group_num = 'num'
+        re_cloned_vm = re.compile(u'%s-clone(?P<%s>\d+)' %
+                                  (name, re_group_num))
+
+        all_vms = self.vms.get_list()
+
+        for v in all_vms:
+            match = re_cloned_vm.match(v)
+            if match is not None:
+                max_num = max(max_num, int(match.group(re_group_num)))
+
+        new_name = u'%s-clone%d' % (name, max_num + 1)
+
+        # create a task with the actual clone function
+        taskid = add_task(u'/vms/%s' % new_name, self._do_clone, self.objstore,
+                          {'name': name, 'new_name': new_name})
+
+        return self.task.lookup(taskid)
+
+    def _do_clone(self, cb, params):
+        name = params['name']
+        new_name = params['new_name']
+        conn = self.conn.get()
+
+        # keep track of the new disks so we can remove them later should an
+        # error occur
+        new_disk_paths = []
+
+        try:
+            # fetch base XML
+            cb('reading source VM XML')
+            dom = conn.lookupByName(name)
+            xml = dom.XMLDesc(libvirt.VIR_DOMAIN_XML_SECURE).decode('utf-8')
+
+            # update name
+            cb('updating VM name')
+            xml = xmlutils.xml_item_update(xml, './name', new_name)
+
+            # update UUID
+            cb('updating VM UUID')
+            new_uuid = unicode(uuid.uuid4())
+            xml = xmlutils.xml_item_update(xml, './uuid', new_uuid)
+
+            # update MAC addresses
+            cb('updating VM MAC addresses...')
+            mac_addresses = xmlutils.xpath_get_text(xml, XPATH_INTERFACE_MAC)
+            for i, mac in enumerate(mac_addresses):
+                while True:
+                    new_mac = kimchi.model.vmifaces.VMIfacesModel.random_mac()
+                    if new_mac not in mac_addresses:
+                        mac_addresses[i] = new_mac
+                        break
+
+                xpath_expr = XPATH_INTERFACE_BY_ADDRESS % mac
+                xml = xmlutils.xml_item_update(xml, xpath_expr, new_mac,
+                                               'address')
+
+            # copy disks
+            disk_paths = xmlutils.xpath_get_text(xml, XPATH_DISK_SOURCE)
+            for i, path in enumerate(disk_paths):
+                cb('copying VM disk \'%s\'' % path)
+                dir = os.path.dirname(path)
+                file_ext = os.path.splitext(path)[1]
+                new_path = os.path.join(dir, u'%s-%d.%s' % (new_uuid, i,
+                                                            file_ext))
+                shutil.copy(path, new_path)
+                xml = xmlutils.xml_item_update(xml, XPATH_DISK_BY_FILE % path,
+                                               new_path, 'file')
+                new_disk_paths.append(new_path)
+
+            # create new guest
+            cb('defining new VM')
+            conn.defineXML(xml)
+
+            cb('OK', True)
+        except Exception, e:
+            # remove newly created disks
+            for path in new_disk_paths:
+                try:
+                    os.remove(path)
+                except OSError, e_remove:
+                    kimchi_log.error('error removing new disk \'%s\': %s' %
+                                     (path, e_remove.message))
+
+            kimchi_log.error('error cloning VM \'%s\': %s' % (name, e.message))
+            raise
+
     def _build_access_elem(self, users, groups):
         access = E.access()
         for user in users:
-- 
1.9.3




More information about the Kimchi-devel mailing list