Signed-off-by: Crístian Viana <vianac(a)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