
Crístian, have you considered using virt-clone? http://linux.die.net/man/1/virt-clone It is a tool that seems to be widely supported by the distros and it could simplify your code at the cost of depending on yet another command line utility. On 10/17/2014 05:26 AM, Crístian Viana wrote:
Signed-off-by: Crístian Viana <vianac@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: