[PATCHv6 0/7] Support image based template

From: Royce Lv <lvroyce@linux.vnet.ibm.com> v5>v6, Delete wrong volume type parameter in to_volume_list to avoid breaking volume filtering v4>v5, Because '/tmp' used tmpfs, but kimchi used 'cache=none' when create vm, so when create image on '/tmp' result in create vm fails, fix this error. Also clear some files created in test. (Aline) v3>v4, Aggreated image scanning and name generate logic to vmtemplate.py to avoid duplicate code. Updated testcases accordingly. v2>v3, Clear unused iso link, Adding mockmodel and tests How to test: create a image using: POST /templates {'name':'mytemp', 'disks':[{'base':'a_base_img_path'}]} create a vm using: POST /vms {'template': '/templates/mytemp', 'pool'....} Royce Lv (7): Add image probe function Change doc and api specification Change 'cdrom' to a optional param Fix: Prevent iso links filling in osinfo.py Create volume based on backing store image Update mockmodel of base img vm Add tests for image based template Makefile.am | 1 + contrib/DEBIAN/control.in | 4 +- contrib/kimchi.spec.fedora.in | 2 + contrib/kimchi.spec.suse.in | 2 + docs/API.md | 3 +- docs/README.md | 9 ++- src/kimchi/API.json | 8 ++- src/kimchi/control/storagevolumes.py | 2 +- src/kimchi/control/templates.py | 2 +- src/kimchi/exception.py | 4 ++ src/kimchi/i18n.py | 8 ++- src/kimchi/imageinfo.py | 66 ++++++++++++++++++++++ src/kimchi/mockmodel.py | 23 ++++---- src/kimchi/model/templates.py | 12 +--- src/kimchi/model/vms.py | 1 + src/kimchi/osinfo.py | 24 +------- src/kimchi/vmtemplate.py | 105 +++++++++++++++++++++++++---------- tests/test_mockmodel.py | 12 ++-- tests/test_model.py | 31 +++++++++++ tests/test_osinfo.py | 8 --- tests/test_rest.py | 44 +++++++++++++-- tests/test_vmtemplate.py | 25 ++++++--- 22 files changed, 288 insertions(+), 108 deletions(-) create mode 100644 src/kimchi/imageinfo.py -- 1.8.3.2

From: Royce Lv <lvroyce@linux.vnet.ibm.com> Image file probe will be used in identify image file os info and generate reasonable configuration for it. This will be useful when import image and create a vm from it. Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- Makefile.am | 1 + contrib/DEBIAN/control.in | 4 +++- contrib/kimchi.spec.fedora.in | 2 ++ contrib/kimchi.spec.suse.in | 2 ++ docs/README.md | 9 +++++--- src/kimchi/exception.py | 4 ++++ src/kimchi/i18n.py | 4 ++++ src/kimchi/imageinfo.py | 49 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 src/kimchi/imageinfo.py diff --git a/Makefile.am b/Makefile.am index 44c2515..3293d9e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -55,6 +55,7 @@ PEP8_WHITELIST = \ src/kimchi/distroloader.py \ src/kimchi/exception.py \ src/kimchi/featuretests.py \ + src/kimchi/imageinfo.py \ src/kimchi/iscsi.py \ src/kimchi/isoinfo.py \ src/kimchi/kvmusertests.py \ diff --git a/contrib/DEBIAN/control.in b/contrib/DEBIAN/control.in index aac1a24..3754fa2 100644 --- a/contrib/DEBIAN/control.in +++ b/contrib/DEBIAN/control.in @@ -23,7 +23,9 @@ Depends: python-cherrypy3 (>= 3.2.0), python-lxml, open-iscsi, firewalld, - nginx + nginx, + python-guestfs, + libguestfs-tools Build-Depends: libxslt, python-libxml2 Maintainer: Aline Manera <alinefm@br.ibm.com> diff --git a/contrib/kimchi.spec.fedora.in b/contrib/kimchi.spec.fedora.in index acc9bc0..4c575b5 100644 --- a/contrib/kimchi.spec.fedora.in +++ b/contrib/kimchi.spec.fedora.in @@ -27,6 +27,8 @@ Requires: nfs-utils Requires: nginx Requires: iscsi-initiator-utils Requires: policycoreutils-python +Requires: python-libguestfs +Requires: libguestfs-tools BuildRequires: libxslt BuildRequires: libxml2-python diff --git a/contrib/kimchi.spec.suse.in b/contrib/kimchi.spec.suse.in index 7e082dc..1e13162 100644 --- a/contrib/kimchi.spec.suse.in +++ b/contrib/kimchi.spec.suse.in @@ -26,6 +26,8 @@ Requires: python-xml Requires: nfs-client Requires: nginx Requires: open-iscsi +Requires: python-libguestfs +Requires: guestfs-tools BuildRequires: libxslt-tools BuildRequires: python-libxml2 diff --git a/docs/README.md b/docs/README.md index ab03918..24537e1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -54,7 +54,8 @@ Install Dependencies qemu-kvm python-psutil python-ethtool sos \ python-ipaddr python-lxml nfs-utils \ iscsi-initiator-utils libxslt pyparted nginx \ - policycoreutils-python + policycoreutils-python python-libguestfs \ + libguestfs-tools # If using RHEL6, install the following additional packages: $ sudo yum install python-unittest2 python-ordereddict # Restart libvirt to allow configuration changes to take effect @@ -76,7 +77,8 @@ for more information on how to configure your system to access this repository. python-pam python-m2crypto python-jsonschema \ qemu-kvm libtool python-psutil python-ethtool \ sosreport python-ipaddr python-lxml nfs-common \ - open-iscsi lvm2 xsltproc python-parted nginx firewalld + open-iscsi lvm2 xsltproc python-parted nginx \ + firewalld python-guestfs libguestfs-tools Packages version requirement: python-jsonschema >= 1.3.0 @@ -90,7 +92,8 @@ for more information on how to configure your system to access this repository. python-pam python-M2Crypto python-jsonschema \ rpm-build kvm python-psutil python-ethtool \ python-ipaddr python-lxml nfs-client open-iscsi \ - libxslt-tools python-xml python-parted + libxslt-tools python-xml python-parted \ + python-libguestfs guestfs-tools Packages version requirement: python-psutil >= 0.6.0 diff --git a/src/kimchi/exception.py b/src/kimchi/exception.py index 3325d51..039152a 100644 --- a/src/kimchi/exception.py +++ b/src/kimchi/exception.py @@ -90,6 +90,10 @@ class IsoFormatError(KimchiException): pass +class ImageFormatError(KimchiException): + pass + + class TimeoutExpired(KimchiException): pass diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index c06de28..acac67b 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -62,6 +62,10 @@ messages = { "'%(user)s' to the ISO path group, or (not recommended) 'chmod -R o+x 'path_to_iso'." "Details: %(err)s" ), + "KCHIMG0001E": _("Error occurs when probing image os information."), + "KCHIMG0002E": _("No OS information found in given image."), + "KCHIMG0003E": _("Unable to find/read image file %(filename)s"), + "KCHVM0001E": _("Virtual machine %(name)s already exists"), "KCHVM0002E": _("Virtual machine %(name)s does not exist"), "KCHVM0003E": _("Unable to rename virtual machine %(name)s. The name %(new_name)s already exists or it is not powered off."), diff --git a/src/kimchi/imageinfo.py b/src/kimchi/imageinfo.py new file mode 100644 index 0000000..f874ece --- /dev/null +++ b/src/kimchi/imageinfo.py @@ -0,0 +1,49 @@ +# +# Kimchi +# +# Copyright IBM Corp, 2014 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import sys +import guestfs + +from kimchi.exception import ImageFormatError + + +def probe_image(image_path): + g = guestfs.GuestFS(python_return_dict=True) + g.add_drive_opts(image_path, readonly=1) + g.launch() + if not os.access(image_path, os.R_OK): + raise ImageFormatError("KCHIMG0003E", {'filename': image_path}) + try: + roots = g.inspect_os() + except: + raise ImageFormatError("KCHIMG0001E") + if len(roots) == 0: + raise ImageFormatError("KCHIMG0002E") + + for root in roots: + version = "%d.%d" % (g.inspect_get_major_version(root), + g.inspect_get_minor_version(root)) + distro = "%s" % (g.inspect_get_distro(root)) + + return (distro, version) + + +if __name__ == '__main__': + print probe_image(sys.argv[1]) -- 1.8.3.2

From: Royce Lv <lvroyce@linux.vnet.ibm.com> Add 'base' to 'disks' param to create template, so that we can support create template from image. Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- docs/API.md | 3 ++- src/kimchi/API.json | 8 +++++++- src/kimchi/i18n.py | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/API.md b/docs/API.md index aebf563..d75c55f 100644 --- a/docs/API.md +++ b/docs/API.md @@ -181,7 +181,7 @@ Represents a snapshot of the Virtual Machine's primary monitor. * cpus *(optional)*: The number of CPUs assigned to the VM. Default is 1. * memory *(optional)*: The amount of memory assigned to the VM. Default is 1024M. - * cdrom *(required)*: A volume name or URI to an ISO image. + * cdrom *(optional)*: A volume name or URI to an ISO image. * storagepool *(optional)*: URI of the storagepool. Default is '/storagepools/default' * networks *(optional)*: list of networks will be assigned to the new VM. @@ -190,6 +190,7 @@ Represents a snapshot of the Virtual Machine's primary monitor. (either *size* or *volume* must be specified): * index: The device index * size: The device size in GB + * base: Base image of this disk * graphics *(optional)*: The graphics paramenters of this template * type: The type of graphics. It can be VNC or spice or None. diff --git a/src/kimchi/API.json b/src/kimchi/API.json index 4b432a2..a8bc61f 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -389,7 +389,6 @@ "description": "Path for cdrom", "type": "string", "pattern": "^((/)|(http)[s]?:|[t]?(ftp)[s]?:)+.*$", - "required": true, "error": "KCHTMPL0014E" }, "disks": { @@ -408,7 +407,14 @@ "type": "number", "minimum": 1, "error": "KCHTMPL0022E" + }, + "base": { + "description": "Base image of the disk", + "type": "string", + "pattern": "^/.+$", + "error": "KCHTMPL0023E" } + } }, "minItems": 1, diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index acac67b..dc19bde 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -128,6 +128,7 @@ messages = { "KCHTMPL0020E": _("Unable to create template due error: %(err)s"), "KCHTMPL0021E": _("Unable to delete template due error: %(err)s"), "KCHTMPL0022E": _("Disk size must be greater than 1GB."), + "KCHTMPL0023E": _("Template base image must be a valid local image file"), "KCHPOOL0001E": _("Storage pool %(name)s already exists"), "KCHPOOL0002E": _("Storage pool %(name)s does not exist"), -- 1.8.3.2

From: Royce Lv <lvroyce@linux.vnet.ibm.com> Multiple files modification for change 'cdrom' to optional param. Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/control/templates.py | 2 +- src/kimchi/i18n.py | 2 +- src/kimchi/imageinfo.py | 19 ++++++++++- src/kimchi/mockmodel.py | 17 ++++------ src/kimchi/model/templates.py | 12 ++----- src/kimchi/vmtemplate.py | 73 +++++++++++++++++++++++++++++++---------- tests/test_mockmodel.py | 12 ++++--- tests/test_rest.py | 23 +++++++++---- tests/test_vmtemplate.py | 25 +++++++++----- 9 files changed, 128 insertions(+), 57 deletions(-) diff --git a/src/kimchi/control/templates.py b/src/kimchi/control/templates.py index 97fdd20..e17fa54 100644 --- a/src/kimchi/control/templates.py +++ b/src/kimchi/control/templates.py @@ -51,7 +51,7 @@ class Template(Resource): 'os_version': self.info['os_version'], 'cpus': self.info['cpus'], 'memory': self.info['memory'], - 'cdrom': self.info['cdrom'], + 'cdrom': self.info.get('cdrom', None), 'disks': self.info['disks'], 'storagepool': self.info['storagepool'], 'networks': self.info['networks'], diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index dc19bde..9e79ee2 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -121,7 +121,7 @@ messages = { "KCHTMPL0013E": _("Amount of memory (MB) must be an integer greater than 512"), "KCHTMPL0014E": _("Template CDROM must be a local or remote ISO file"), "KCHTMPL0015E": _("Invalid storage pool URI %(value)s specified for template"), - "KCHTMPL0016E": _("Specify an ISO image as CDROM to create a template"), + "KCHTMPL0016E": _("Specify an ISO image as CDROM or a base image to create a template"), "KCHTMPL0017E": _("All networks for the template must be specified in a list."), "KCHTMPL0018E": _("Must specify a volume to a template, when storage pool is iscsi or scsi"), "KCHTMPL0019E": _("The volume: %(volume)s in not in storage pool %(pool)s"), diff --git a/src/kimchi/imageinfo.py b/src/kimchi/imageinfo.py index f874ece..f4c6356 100644 --- a/src/kimchi/imageinfo.py +++ b/src/kimchi/imageinfo.py @@ -17,11 +17,28 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +import json import os import sys import guestfs -from kimchi.exception import ImageFormatError +from kimchi.exception import ImageFormatError, TimeoutExpired +from kimchi.utils import run_command, kimchi_log + + +def probe_img_info(path): + cmd = ["qemu-img", "info", "--output=json", path] + info = dict() + try: + out = run_command(cmd, 10)[0] + except TimeoutExpired: + kimchi_log.warning("Cannot decide format of base img %s", path) + return None + + info = json.loads(out) + info['virtual-size'] = info['virtual-size'] >> 30 + info['actual-size'] = info['actual-size'] >> 30 + return info def probe_image(image_path): diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index a42f2dd..27ee50d 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -217,8 +217,10 @@ class MockModel(object): index += 1 cdrom = "hd" + string.ascii_lowercase[index + 1] - cdrom_params = {'dev': cdrom, 'path': t_info['cdrom'], 'type': 'cdrom'} - vm.storagedevices[cdrom] = MockVMStorageDevice(cdrom_params) + if t_info.get('cdrom'): + cdrom_params = { + 'dev': cdrom, 'path': t_info['cdrom'], 'type': 'cdrom'} + vm.storagedevices[cdrom] = MockVMStorageDevice(cdrom_params) self._mock_vms[name] = vm return name @@ -254,14 +256,6 @@ class MockModel(object): def templates_create(self, params): name = params.get('name', '').strip() - if not name: - iso = params['cdrom'] - iso_name = os.path.splitext(iso[iso.rfind('/') + 1:])[0] - name = iso_name + str(int(time.time() * 1000)) - params['name'] = name - - if name in self._mock_templates: - raise InvalidOperation("KCHTMPL0001E", {'name': name}) for net_name in params.get(u'networks', []): try: @@ -271,6 +265,9 @@ class MockModel(object): raise InvalidParameter("KCHTMPL0003E", msg_args) t = MockVMTemplate(params, self) + if t.name in self._mock_templates: + raise InvalidOperation("KCHTMPL0001E", {'name': name}) + self._mock_templates[name] = t return name diff --git a/src/kimchi/model/templates.py b/src/kimchi/model/templates.py index 9b47d50..bf04304 100644 --- a/src/kimchi/model/templates.py +++ b/src/kimchi/model/templates.py @@ -19,7 +19,6 @@ import copy import os -import time import libvirt @@ -40,9 +39,9 @@ class TemplatesModel(object): def create(self, params): name = params.get('name', '').strip() - iso = params['cdrom'] + iso = params.get('cdrom') # check search permission - if iso.startswith('/') and os.path.isfile(iso): + if iso and iso.startswith('/') and os.path.isfile(iso): user = UserTests().probe_user() ret, excp = probe_file_permission_as_user(iso, user) if ret is False: @@ -50,11 +49,6 @@ class TemplatesModel(object): {'filename': iso, 'user': user, 'err': excp}) - if not name: - iso_name = os.path.splitext(iso[iso.rfind('/') + 1:])[0] - name = iso_name + str(int(time.time() * 1000)) - params['name'] = name - conn = self.conn.get() pool_uri = params.get(u'storagepool', '') if pool_uri: @@ -78,7 +72,7 @@ class TemplatesModel(object): # Checkings will be done while creating this class, so any exception # will be raised here t = LibvirtVMTemplate(params, scan=True) - + name = params['name'] try: with self.objstore as session: if name in session.get_list('template'): diff --git a/src/kimchi/vmtemplate.py b/src/kimchi/vmtemplate.py index 05b5c50..09bed6a 100644 --- a/src/kimchi/vmtemplate.py +++ b/src/kimchi/vmtemplate.py @@ -20,14 +20,18 @@ import os import string import socket +import time import urlparse +import uuid from distutils.version import LooseVersion from kimchi import osinfo -from kimchi.exception import InvalidParameter, IsoFormatError +from kimchi.exception import InvalidParameter, IsoFormatError, ImageFormatError +from kimchi.exception import MissingParameter +from kimchi.imageinfo import probe_image, probe_img_info from kimchi.isoinfo import IsoImage from kimchi.utils import check_url_path, pool_name_from_uri from lxml import etree @@ -49,22 +53,17 @@ class VMTemplate(object): defaults. If scan is True and a cdrom is present, the operating system will be detected by probing the installation media. """ - self.name = args['name'] self.info = {} self.fc_host_support = args.get('fc_host_support') - # Identify the cdrom if present - iso_distro = iso_version = 'unknown' - iso = args.get('cdrom', '') - - if scan and len(iso) > 0: - iso_distro, iso_version = self.get_iso_info(iso) - if not iso.startswith('/'): - self.info.update({'iso_stream': True}) - + distro, version = self._get_os_info(args, scan) # Fetch defaults based on the os distro and version - os_distro = args.get('os_distro', iso_distro) - os_version = args.get('os_version', iso_version) + os_distro = args.get('os_distro', distro) + os_version = args.get('os_version', version) + if 'name' not in args or args['name'] == '': + args['name'] = self._gen_name(os_distro, os_version) + self.name = args['name'] + entry = osinfo.lookup(os_distro, os_version) self.info.update(entry) @@ -76,6 +75,43 @@ class VMTemplate(object): args['graphics'] = graphics self.info.update(args) + def _get_os_info(self, args, scan): + # Identify the cdrom if present + distro = version = 'unknown' + iso = args.get('cdrom', '') + valid = False + # if ISO not specified and base disk image specified, + # prevent cdrom from filling automatically + if len(iso) == 0 and 'disks' in args: + for d in args['disks']: + if 'base' in d: + valid = True + try: + distro, version = probe_image(d['base']) + except ImageFormatError: + pass + if 'size' not in d: + d['size'] = probe_img_info(d['base'])['virtual-size'] + + if len(iso) > 0: + valid = True + if scan: + distro, version = self.get_iso_info(iso) + if not iso.startswith('/'): + self.info.update({'iso_stream': True}) + + if not valid: + raise MissingParameter("KCHTMPL0016E") + + return distro, version + + def _gen_name(self, distro, version): + if distro == 'unknown': + name = str(uuid.uuid4()) + else: + name = distro + version + '.' + str(int(time.time() * 1000)) + return name + def get_iso_info(self, iso): iso_prefixes = ['/', 'http', 'https', 'ftp', 'ftps', 'tftp'] if len(filter(iso.startswith, iso_prefixes)) == 0: @@ -87,6 +123,8 @@ class VMTemplate(object): raise InvalidParameter("KCHISO0001E", {'filename': iso}) def _get_cdrom_xml(self, libvirt_stream_protocols, qemu_stream_dns): + if 'cdrom' not in self.info: + return '' bus = self.info['cdrom_bus'] dev = "%s%s" % (self._bus_to_dev[bus], string.lowercase[self.info['cdrom_index']]) @@ -341,8 +379,9 @@ drive=drive-%(bus)s0-1-0,id=%(bus)s0-1-0'/> cdrom_xml = self._get_cdrom_xml(libvirt_stream_protocols, qemu_stream_dns) - if not urlparse.urlparse(self.info['cdrom']).scheme in \ - libvirt_stream_protocols and params.get('iso_stream', False): + if not urlparse.urlparse(self.info.get('cdrom', "")).scheme in \ + libvirt_stream_protocols and \ + params.get('iso_stream', False): params['qemu-namespace'] = QEMU_NAMESPACE params['qemu-stream-cmdline'] = cdrom_xml else: @@ -429,8 +468,8 @@ drive=drive-%(bus)s0-1-0,id=%(bus)s0-1-0'/> # validate iso integrity # FIXME when we support multiples cdrom devices - iso = self.info['cdrom'] - if not (os.path.isfile(iso) or check_url_path(iso)): + iso = self.info.get('cdrom') + if iso and not (os.path.isfile(iso) or check_url_path(iso)): invalid['cdrom'] = [iso] self.info['invalid'] = invalid diff --git a/tests/test_mockmodel.py b/tests/test_mockmodel.py index 9def33b..b7e6a48 100644 --- a/tests/test_mockmodel.py +++ b/tests/test_mockmodel.py @@ -34,11 +34,12 @@ model = None host = None port = None ssl_port = None +fake_iso = None class MockModelTests(unittest.TestCase): def setUp(self): - global host, port, ssl_port, model, test_server + global host, port, ssl_port, model, test_server, fake_iso cherrypy.request.headers = {'Accept': 'application/json'} model = kimchi.mockmodel.MockModel('/tmp/obj-store-test') patch_auth() @@ -47,10 +48,13 @@ class MockModelTests(unittest.TestCase): host = '127.0.0.1' test_server = run_server(host, port, ssl_port, test_mode=True, model=model) + fake_iso = '/tmp/fake.iso' + open(fake_iso, 'w').close() def tearDown(self): test_server.stop() os.unlink('/tmp/obj-store-test') + os.unlink(fake_iso) def test_collection(self): c = Collection(model) @@ -88,7 +92,7 @@ class MockModelTests(unittest.TestCase): def test_screenshot_refresh(self): # Create a VM - req = json.dumps({'name': 'test', 'cdrom': '/nonexistent.iso'}) + req = json.dumps({'name': 'test', 'cdrom': fake_iso}) request(host, ssl_port, '/templates', req, 'POST') req = json.dumps({'name': 'test-vm', 'template': '/templates/test'}) request(host, ssl_port, '/vms', req, 'POST') @@ -113,7 +117,7 @@ class MockModelTests(unittest.TestCase): resp.getheader('last-modified')) def test_vm_list_sorted(self): - req = json.dumps({'name': 'test', 'cdrom': '/nonexistent.iso'}) + req = json.dumps({'name': 'test', 'cdrom': fake_iso}) request(host, ssl_port, '/templates', req, 'POST') def add_vm(name): @@ -131,7 +135,7 @@ class MockModelTests(unittest.TestCase): def test_vm_info(self): model.templates_create({'name': u'test', - 'cdrom': '/nonexistent.iso'}) + 'cdrom': fake_iso}) model.vms_create({'name': u'test', 'template': '/templates/test'}) vms = model.vms_get_list() self.assertEquals(1, len(vms)) diff --git a/tests/test_rest.py b/tests/test_rest.py index 1455205..a97cf90 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -497,12 +497,12 @@ class RestTests(unittest.TestCase): self.assertEquals(201, resp.status) # Attach cdrom with both path and volume specified - open('/tmp/mock.iso', 'w').close() + open('/tmp/existent.iso', 'w').close() req = json.dumps({'dev': 'hdx', 'type': 'cdrom', 'pool': 'tmp', 'vol': 'attach-volume', - 'path': '/tmp/mock.iso'}) + 'path': '/tmp/existent.iso'}) resp = self.request('/vms/test-vm/storages', req, 'POST') self.assertEquals(400, resp.status) @@ -511,7 +511,7 @@ class RestTests(unittest.TestCase): 'type': 'disk', 'pool': 'tmp', 'vol': 'attach-volume', - 'path': '/tmp/mock.iso'}) + 'path': '/tmp/existent.iso'}) resp = self.request('/vms/test-vm/storages', req, 'POST') self.assertEquals(400, resp.status) @@ -536,7 +536,6 @@ class RestTests(unittest.TestCase): self.assertEquals('attach-volume', cd_info['vol']) # Attach a cdrom with existent dev name - open('/tmp/existent.iso', 'w').close() req = json.dumps({'dev': 'hdk', 'type': 'cdrom', 'path': '/tmp/existent.iso'}) @@ -1083,7 +1082,7 @@ class RestTests(unittest.TestCase): self.assertEquals(200, resp.status) self.assertEquals(0, len(json.loads(resp.read()))) - # Create a template without cdrom fails with 400 + # Create a template without cdrom and disk specified fails with 400 t = {'name': 'test', 'os_distro': 'ImagineOS', 'os_version': '1.0', 'memory': 1024, 'cpus': 1, 'storagepool': '/storagepools/alt'} @@ -1091,15 +1090,27 @@ class RestTests(unittest.TestCase): resp = self.request('/templates', req, 'POST') self.assertEquals(400, resp.status) + # Create an image based template + open('/tmp/mock.img', 'w').close() + t = {'name': 'test_img_template', 'os_distro': 'ImagineOS', + 'os_version': '1.0', 'memory': 1024, 'cpus': 1, + 'storagepool': '/storagepools/alt', 'disks': [{'base': '/tmp/mock.img'}]} + req = json.dumps(t) + resp = self.request('/templates', req, 'POST') + self.assertEquals(201, resp.status) + os.remove('/tmp/mock.img') + # Create a template + open('/tmp/mock.iso', 'w').close() graphics = {'type': 'spice', 'listen': '127.0.0.1'} t = {'name': 'test', 'os_distro': 'ImagineOS', 'os_version': '1.0', 'memory': 1024, 'cpus': 1, - 'storagepool': '/storagepools/alt', 'cdrom': '/nonexistent.iso', + 'storagepool': '/storagepools/alt', 'cdrom': '/tmp/mock.iso', 'graphics': graphics} req = json.dumps(t) resp = self.request('/templates', req, 'POST') self.assertEquals(201, resp.status) + os.remove('/tmp/mock.iso') # Verify the template res = json.loads(self.request('/templates/test').read()) diff --git a/tests/test_vmtemplate.py b/tests/test_vmtemplate.py index b5c2809..4ae1d36 100644 --- a/tests/test_vmtemplate.py +++ b/tests/test_vmtemplate.py @@ -17,6 +17,7 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +import os import unittest import uuid @@ -26,14 +27,22 @@ from kimchi.xmlutils import xpath_get_text class VMTemplateTests(unittest.TestCase): + def setUp(self): + self.iso = '/tmp/mock.iso' + open(self.iso, 'w').close() + + def tearDown(self): + os.unlink(self.iso) + def test_minimal_construct(self): fields = (('name', 'test'), ('os_distro', 'unknown'), ('os_version', 'unknown'), ('cpus', 1), - ('memory', 1024), ('cdrom', ''), ('networks', ['default']), + ('memory', 1024), ('networks', ['default']), ('disk_bus', 'ide'), ('nic_model', 'e1000'), - ('graphics', {'type': 'vnc', 'listen': '127.0.0.1'})) + ('graphics', {'type': 'vnc', 'listen': '127.0.0.1'}), + ('cdrom', self.iso)) - args = {'name': 'test'} + args = {'name': 'test', 'cdrom': self.iso} t = VMTemplate(args) for name, val in fields: self.assertEquals(val, t.info.get(name)) @@ -41,7 +50,7 @@ class VMTemplateTests(unittest.TestCase): def test_construct_overrides(self): graphics = {'type': 'spice', 'listen': '127.0.0.1'} args = {'name': 'test', 'disks': [{'size': 10}, {'size': 20}], - 'graphics': graphics} + 'graphics': graphics, "cdrom": self.iso} t = VMTemplate(args) self.assertEquals(2, len(t.info['disks'])) self.assertEquals(graphics, t.info['graphics']) @@ -50,7 +59,7 @@ class VMTemplateTests(unittest.TestCase): # Test specified listen graphics = {'type': 'vnc', 'listen': '127.0.0.1'} args = {'name': 'test', 'disks': [{'size': 10}, {'size': 20}], - 'graphics': graphics} + 'graphics': graphics, 'cdrom': self.iso} t = VMTemplate(args) self.assertEquals(graphics, t.info['graphics']) @@ -70,7 +79,7 @@ class VMTemplateTests(unittest.TestCase): def test_to_xml(self): graphics = {'type': 'spice', 'listen': '127.0.0.1'} vm_uuid = str(uuid.uuid4()).replace('-', '') - t = VMTemplate({'name': 'test-template'}) + t = VMTemplate({'name': 'test-template', 'cdrom': self.iso}) xml = t.to_vm_xml('test-vm', vm_uuid, graphics=graphics) self.assertEquals(vm_uuid, xpath_get_text(xml, "/domain/uuid")[0]) self.assertEquals('test-vm', xpath_get_text(xml, "/domain/name")[0]) @@ -87,10 +96,10 @@ class VMTemplateTests(unittest.TestCase): graphics = {'type': 'vnc', 'listen': '127.0.0.1'} args = {'name': 'test', 'os_distro': 'opensuse', 'os_version': '12.3', 'cpus': 2, 'memory': 2048, 'networks': ['foo'], - 'cdrom': '/cd.iso', 'graphics': graphics} + 'cdrom': self.iso, 'graphics': graphics} t = VMTemplate(args) self.assertEquals(2, t.info.get('cpus')) self.assertEquals(2048, t.info.get('memory')) self.assertEquals(['foo'], t.info.get('networks')) - self.assertEquals('/cd.iso', t.info.get('cdrom')) + self.assertEquals(self.iso, t.info.get('cdrom')) self.assertEquals(graphics, t.info.get('graphics')) -- 1.8.3.2

From: Royce Lv <lvroyce@linux.vnet.ibm.com> As we already has distro json schema based template creation, we need to stop filling iso links filling in osinfo.py. Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/osinfo.py | 24 +----------------------- tests/test_osinfo.py | 8 -------- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/src/kimchi/osinfo.py b/src/kimchi/osinfo.py index b542fea..1ad353c 100644 --- a/src/kimchi/osinfo.py +++ b/src/kimchi/osinfo.py @@ -65,31 +65,10 @@ modern_version_bases = {'x86': {'debian': '6.0', 'ubuntu': '7.10', 'opensuse': '13.1', 'sles': '11sp3'}} + icon_available_distros = [icon[5:-4] for icon in glob.glob1('%s/images/' % paths.ui_dir, 'icon-*.png')] -isolinks = { - 'debian': { - 'squeeze': 'http://cdimage.debian.org/debian-cd/6.0.7-live/amd64/' - 'iso-hybrid/debian-live-6.0.7-amd64-gnome-desktop.iso', - }, - 'ubuntu': { - 'raring': 'http://ubuntu-releases.cs.umn.edu/13.04/' - 'ubuntu-13.04-desktop-amd64.iso', - }, - 'opensuse': { - '12.3': 'http://suse.mirrors.tds.net/pub/opensuse/distribution/12.3/' - 'iso/openSUSE-12.3-DVD-x86_64.iso', - }, - 'fedora': { - '16': 'http://fedora.mirrors.tds.net/pub/fedora/releases/16/Live/' - 'x86_64/Fedora-16-x86_64-Live-Desktop.iso', - '17': 'http://fedora.mirrors.tds.net/pub/fedora/releases/17/Live/' - 'x86_64/Fedora-17-x86_64-Live-Desktop.iso', - '18': 'http://fedora.mirrors.tds.net/pub/fedora/releases/18/Live/' - 'x86_64/Fedora-18-x86_64-Live-Desktop.iso', - }, -} defaults = {'networks': ['default'], 'storagepool': '/storagepools/default', @@ -113,7 +92,6 @@ def lookup(distro, version): params = copy.deepcopy(defaults) params['os_distro'] = distro params['os_version'] = version - params['cdrom'] = isolinks.get(distro, {}).get(version, '') arch = _get_arch() if distro in modern_version_bases[arch]: diff --git a/tests/test_osinfo.py b/tests/test_osinfo.py index 78788ca..d5e90b4 100644 --- a/tests/test_osinfo.py +++ b/tests/test_osinfo.py @@ -30,14 +30,6 @@ class OSInfoTests(unittest.TestCase): self.assertEquals('unknown', entry['os_version']) self.assertEquals(['default'], entry['networks']) - def test_fedora_lookup(self): - cd = ('http://fedora.mirrors.tds.net/pub/fedora/releases/17/Live/' - 'x86_64/Fedora-17-x86_64-Live-Desktop.iso') - entry = lookup('fedora', '17') - self.assertEquals(10, entry['disks'][0]['size']) - self.assertEquals(cd, entry['cdrom']) - self.assertEquals('/storagepools/default', entry['storagepool']) - def test_old_distros(self): old_versions = {'debian': '5.0', 'ubuntu': '7.04', 'opensuse': '10.1', 'centos': '5.1', 'rhel': '5.1', 'fedora': '15'} -- 1.8.3.2

From: Royce Lv <lvroyce@linux.vnet.ibm.com> Creating volume base on backing store so that we can create vm from this cow volume. Also change volume xml generation method to lxml. Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/i18n.py | 1 + src/kimchi/model/vms.py | 1 + src/kimchi/vmtemplate.py | 32 +++++++++++++++++++------------- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index 9e79ee2..0e3b978 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -129,6 +129,7 @@ messages = { "KCHTMPL0021E": _("Unable to delete template due error: %(err)s"), "KCHTMPL0022E": _("Disk size must be greater than 1GB."), "KCHTMPL0023E": _("Template base image must be a valid local image file"), + "KCHTMPL0024E": _("Cannot identify base image %(path)s format"), "KCHPOOL0001E": _("Storage pool %(name)s already exists"), "KCHPOOL0002E": _("Storage pool %(name)s does not exist"), diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index a0e69b2..95abe68 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -204,6 +204,7 @@ class VMsModel(object): # the user from UI or manually. vol_list = [] if t._get_storage_type() in ["iscsi", "scsi"]: + # FIXME: iscsi and scsi storage work with base image needs to be fixed. vol_list = [] else: vol_list = t.fork_vm_storage(vm_uuid) diff --git a/src/kimchi/vmtemplate.py b/src/kimchi/vmtemplate.py index 09bed6a..47982ad 100644 --- a/src/kimchi/vmtemplate.py +++ b/src/kimchi/vmtemplate.py @@ -287,22 +287,28 @@ drive=drive-%(bus)s0-1-0,id=%(bus)s0-1-0'/> info = {'name': volume, 'capacity': d['size'], - 'type': 'disk', 'format': fmt, 'path': '%s/%s' % (storage_path, volume)} - info['allocation'] = 0 if fmt == 'qcow2' else info['capacity'] - info['xml'] = """ - <volume> - <name>%(name)s</name> - <allocation unit="G">%(allocation)s</allocation> - <capacity unit="G">%(capacity)s</capacity> - <target> - <format type='%(format)s'/> - <path>%(path)s</path> - </target> - </volume> - """ % info + + if 'base' in d: + info['base'] = dict() + base_fmt = probe_img_info(d['base'])['format'] + if base_fmt is None: + raise InvalidParameter("KCHTMPL0024E", {'path': d['base']}) + info['base']['path'] = d['base'] + info['base']['format'] = base_fmt + + v_tree = E.volume(E.name(info['name'])) + v_tree.append(E.allocation(str(info['allocation']), unit='G')) + v_tree.append(E.capacity(str(info['capacity']), unit='G')) + target = E.target( + E.format(type=info['format']), E.path(info['path'])) + if 'base' in d: + v_tree.append(E.backingStore( + E.path(info['base']['path']), E.format(type=info['base']['format']))) + v_tree.append(target) + info['xml'] = etree.tostring(v_tree) ret.append(info) return ret -- 1.8.3.2

From: Royce Lv <lvroyce@linux.vnet.ibm.com> Adding base img report in mockmodel Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- src/kimchi/control/storagevolumes.py | 2 +- src/kimchi/mockmodel.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/kimchi/control/storagevolumes.py b/src/kimchi/control/storagevolumes.py index edd696f..327bf75 100644 --- a/src/kimchi/control/storagevolumes.py +++ b/src/kimchi/control/storagevolumes.py @@ -58,7 +58,7 @@ class StorageVolume(Resource): 'ref_cnt': self.info['ref_cnt'], 'format': self.info['format']} - for key in ('os_version', 'os_distro', 'bootable'): + for key in ('os_version', 'os_distro', 'bootable', 'base'): val = self.info.get(key) if val: res[key] = val diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 27ee50d..1b55c42 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -480,11 +480,13 @@ class MockModel(object): try: name = params['name'] volume = MockStorageVolume(pool, name, params) - volume.info['type'] = params['type'] + volume.info['type'] = 'file' volume.info['ref_cnt'] = params.get('ref_cnt', 0) volume.info['format'] = params['format'] volume.info['path'] = os.path.join( pool.info['path'], name) + if 'base' in params: + volume.info['base'] = copy.deepcopy(params['base']) except KeyError, item: raise MissingParameter("KCHVOL0004E", {'item': str(item), 'volume': name}) @@ -1002,6 +1004,8 @@ class MockVMTemplate(VMTemplate): for vol_info in volumes: vol_info['capacity'] = vol_info['capacity'] << 10 vol_info['ref_cnt'] = 1 + if 'base' in self.info: + vol_info['base'] = copy.deepcopy(self.info['base']) self.model.storagevolumes_create(pool.name, vol_info) disk_paths.append({'pool': pool.name, 'volume': vol_info['name']}) return disk_paths -- 1.8.3.2

From: Royce Lv <lvroyce@linux.vnet.ibm.com> Add model tests for image based template, validated vm creation. Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> --- tests/test_model.py | 31 +++++++++++++++++++++++++++++++ tests/test_rest.py | 21 +++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/tests/test_model.py b/tests/test_model.py index 9cfa312..80aadc7 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -107,6 +107,37 @@ class ModelTests(unittest.TestCase): self.assertFalse('kimchi-vm' in vms) @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') + def test_image_based_template(self): + inst = model.Model(objstore_loc=self.tmp_store) + + with RollbackContext() as rollback: + vol = 'base-vol.img' + params = {'name': vol, + 'capacity': 1024, + 'allocation': 1, + 'format': 'qcow2'} + inst.storagevolumes_create('default', params) + vol_path = inst.storagevolume_lookup('default', vol)['path'] + rollback.prependDefer(inst.storagevolume_delete, 'default', vol) + + params = {'name': 'test', 'disks': [{'base': vol_path}]} + inst.templates_create(params) + rollback.prependDefer(inst.template_delete, 'test') + + params = {'name': 'kimchi-vm', 'template': '/templates/test'} + inst.vms_create(params) + rollback.prependDefer(inst.vm_delete, 'kimchi-vm') + + vms = inst.vms_get_list() + self.assertTrue('kimchi-vm' in vms) + + inst.vm_start('kimchi-vm') + rollback.prependDefer(inst.vm_poweroff, 'kimchi-vm') + + info = inst.vm_lookup('kimchi-vm') + self.assertEquals('running', info['state']) + + @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') def test_vm_graphics(self): inst = model.Model(objstore_loc=self.tmp_store) params = {'name': 'test', 'disks': [], 'cdrom': self.kimchi_iso} diff --git a/tests/test_rest.py b/tests/test_rest.py index a97cf90..e5164f9 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -868,6 +868,27 @@ class RestTests(unittest.TestCase): resp = json.loads(resp.read()) self.assertIn(u"KCHVM0012E", resp['reason']) + def test_create_vm_with_img_based_template(self): + resp = json.loads( + self.request('/storagepools/default/storagevolumes').read()) + self.assertEquals(0, len(resp)) + + # Create a Template + mock_base = '/tmp/mock.img' + open(mock_base, 'w').close() + req = json.dumps({'name': 'test', 'disks': [{'base': mock_base}]}) + resp = self.request('/templates', req, 'POST') + self.assertEquals(201, resp.status) + + req = json.dumps({'template': '/templates/test'}) + json.loads(self.request('/vms', req, 'POST').read()) + + # Test storage volume created with backing store of base file + resp = json.loads( + self.request('/storagepools/default/storagevolumes').read()) + self.assertEquals(1, len(resp)) + self.assertEquals(mock_base, resp[0]['base']['path']) + def test_get_storagepools(self): storagepools = json.loads(self.request('/storagepools').read()) self.assertEquals(2, len(storagepools)) -- 1.8.3.2
participants (2)
-
Aline Manera
-
lvroyce@linux.vnet.ibm.com