[Kimchi-devel] [WIP PATCH 5/5] Add feature to clone VMs
Daniel H Barboza
danielhb at linux.vnet.ibm.com
Fri Oct 17 17:38:14 UTC 2014
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 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:
More information about the Kimchi-devel
mailing list