[PATCH V2 0/4] CDROM Management

V2: - Add devices to mockmodel and add test cases to rest API - Assign name automatically to new devices, if not passed by user - Fix minor errors V1: This patch set implements host storage devices management. It implements full CDROM device add, remove and update functionality. It implements basic Disk functionalities. Rodrigo Trujillo (4): Add storage sub-collection to sub-resource to guest resource Update controller and API.json for guest storages Devices management model implementation Guest vm storage devices mockmodel and rest api test cases docs/API.md | 22 +++++ src/kimchi/API.json | 33 +++++++ src/kimchi/control/vm/storages.py | 49 ++++++++++ src/kimchi/mockmodel.py | 63 +++++++++++++ src/kimchi/model/vms.py | 190 +++++++++++++++++++++++++++++++++++++- src/kimchi/xmlutils.py | 5 + tests/test_rest.py | 73 +++++++++++++++ 7 files changed, 434 insertions(+), 1 deletion(-) create mode 100644 src/kimchi/control/vm/storages.py -- 1.8.5.3

This patch changes API.md with new storage sub-collection/sub-resource information. Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- docs/API.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/API.md b/docs/API.md index 7f0628d..fb5ec17 100644 --- a/docs/API.md +++ b/docs/API.md @@ -117,6 +117,28 @@ Represents a snapshot of the Virtual Machine's primary monitor. * **GET**: Redirect to the latest screenshot of a Virtual Machine in PNG format + +### Sub-collection: Virtual Machine storages +**URI:** /vms/*:name*/storages +* **GET**: Retrieve a summarized list of all storages of specified guest +* **POST**: Attach a new storage to specified virtual machine. + Simulate add new cdrom + * dev : The name of the storage in the vm. + * type : The type of the storage (cdrom, disk). + * path : Path of cdrom iso or disk. + +### Sub-resource: storage +**URI:** /vms/*:name*/storages/*:dev* +* **GET**: Retrieve storage information + * dev : The name of the storage in the vm. + * type : The type of the storage (cdrom, disk). + * path : Path of cdrom iso or disk. +* **PUT**: Update storage information + * path: Path of cdrom iso or disk. +* **DELETE**: Remove the storage. Simulate eject a cdrom + + + ### Collection: Templates **URI:** /templates -- 1.8.5.3

On 02/11/2014 06:46 AM, Rodrigo Trujillo wrote:
This patch changes API.md with new storage sub-collection/sub-resource information.
Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- docs/API.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+)
diff --git a/docs/API.md b/docs/API.md index 7f0628d..fb5ec17 100644 --- a/docs/API.md +++ b/docs/API.md @@ -117,6 +117,28 @@ Represents a snapshot of the Virtual Machine's primary monitor.
* **GET**: Redirect to the latest screenshot of a Virtual Machine in PNG format
+ +### Sub-collection: Virtual Machine storages +**URI:** /vms/*:name*/storages +* **GET**: Retrieve a summarized list of all storages of specified guest +* **POST**: Attach a new storage to specified virtual machine. + Simulate add new cdrom
Simulate add a new cdrom ?
+ * dev : The name of the storage in the vm. a space between parameter and ":". seems different format with others parameters. + * type : The type of the storage (cdrom, disk). + * path : Path of cdrom iso or disk. + +### Sub-resource: storage +**URI:** /vms/*:name*/storages/*:dev* +* **GET**: Retrieve storage information + * dev : The name of the storage in the vm. + * type : The type of the storage (cdrom, disk). + * path : Path of cdrom iso or disk. +* **PUT**: Update storage information + * path: Path of cdrom iso or disk. here no space between parameter and ":" +* **DELETE**: Remove the storage. Simulate eject a cdrom + + + ### Collection: Templates
**URI:** /templates
-- Thanks and best regards! Sheldon Feng(冯少合)<shaohef@linux.vnet.ibm.com> IBM Linux Technology Center

This patch adds the control classes for guest storages Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- src/kimchi/API.json | 33 ++++++++++++++++++++++++++ src/kimchi/control/vm/storages.py | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 src/kimchi/control/vm/storages.py diff --git a/src/kimchi/API.json b/src/kimchi/API.json index 842fb11..6ca3f6a 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -290,6 +290,39 @@ }, "additionalProperties": false }, + "storages_create": { + "type": "object", + "properties": { + "dev": { + "description": "The storage device name", + "type": "string", + "pattern": "^hd[b-z]$" + }, + "type": { + "description": "The storage type", + "type": "string", + "pattern": "^cdrom|disk$", + "required": true + }, + "path": { + "description": "Path of iso image file or disk mount point", + "type": "string", + "pattern": "^((/)|(http)[s]?:|[t]?(ftp)[s]?:)+.*$", + "required": true + } + } + }, + "storage_update": { + "type": "object", + "properties": { + "path": { + "description": "Path of iso image file or disk mount point", + "type": "string", + "pattern": "^((/)|(http)[s]?:|[t]?(ftp)[s]?:)+.*$", + "required": true + } + } + }, "template_update": { "type": "object", "properties": { diff --git a/src/kimchi/control/vm/storages.py b/src/kimchi/control/vm/storages.py new file mode 100644 index 0000000..8878d6e --- /dev/null +++ b/src/kimchi/control/vm/storages.py @@ -0,0 +1,49 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# ShaoHe Feng <shaohef@linux.vnet.ibm.com> +# +# 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 + +from kimchi.control.base import Collection, Resource +from kimchi.control.utils import UrlSubNode + + +@UrlSubNode("storages") +class Storages(Collection): + def __init__(self, model, vm): + super(Storages, self).__init__(model) + self.resource = Storage + self.vm = vm + self.resource_args = [self.vm, ] + self.model_args = [self.vm, ] + + +class Storage(Resource): + def __init__(self, model, vm, ident): + super(Storage, self).__init__(model, ident) + self.vm = vm + self.ident = ident + self.info = {} + self.model_args = [self.vm, self.ident] + self.uri_fmt = '/vms/%s/storages/%s' + self.update_params = ['path'] + + @property + def data(self): + return self.info -- 1.8.5.3

On 02/11/2014 06:46 AM, Rodrigo Trujillo wrote:
This patch adds the control classes for guest storages
Signed-off-by: Royce Lv <lvroyce@linux.vnet.ibm.com> Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- src/kimchi/API.json | 33 ++++++++++++++++++++++++++ src/kimchi/control/vm/storages.py | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 src/kimchi/control/vm/storages.py
diff --git a/src/kimchi/API.json b/src/kimchi/API.json index 842fb11..6ca3f6a 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -290,6 +290,39 @@ }, "additionalProperties": false }, + "storages_create": { + "type": "object", + "properties": { + "dev": { + "description": "The storage device name", + "type": "string", + "pattern": "^hd[b-z]$" oh, for a virtio device, it will be vda, vdb. + }, + "type": { + "description": "The storage type", + "type": "string", + "pattern": "^cdrom|disk$", + "required": true + }, + "path": { + "description": "Path of iso image file or disk mount point", + "type": "string", + "pattern": "^((/)|(http)[s]?:|[t]?(ftp)[s]?:)+.*$", + "required": true + } + } + }, + "storage_update": { + "type": "object", + "properties": { + "path": { + "description": "Path of iso image file or disk mount point", + "type": "string", + "pattern": "^((/)|(http)[s]?:|[t]?(ftp)[s]?:)+.*$", + "required": true + } + } + }, "template_update": { "type": "object", "properties": { diff --git a/src/kimchi/control/vm/storages.py b/src/kimchi/control/vm/storages.py new file mode 100644 index 0000000..8878d6e --- /dev/null +++ b/src/kimchi/control/vm/storages.py @@ -0,0 +1,49 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# ShaoHe Feng <shaohef@linux.vnet.ibm.com> +# +# 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 + +from kimchi.control.base import Collection, Resource +from kimchi.control.utils import UrlSubNode + + +@UrlSubNode("storages") +class Storages(Collection): + def __init__(self, model, vm): + super(Storages, self).__init__(model) + self.resource = Storage + self.vm = vm + self.resource_args = [self.vm, ] + self.model_args = [self.vm, ] + + +class Storage(Resource): + def __init__(self, model, vm, ident): + super(Storage, self).__init__(model, ident) + self.vm = vm + self.ident = ident + self.info = {} + self.model_args = [self.vm, self.ident] + self.uri_fmt = '/vms/%s/storages/%s' + self.update_params = ['path'] + + @property + def data(self): + return self.info
-- Thanks and best regards! Sheldon Feng(冯少合)<shaohef@linux.vnet.ibm.com> IBM Linux Technology Center

This patch adds the CREATE, LOOKUP, UPDATE and DELETE functionalities to guest storage devices support. Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- src/kimchi/model/vms.py | 190 +++++++++++++++++++++++++++++++++++++++++++++++- src/kimchi/xmlutils.py | 5 ++ 2 files changed, 194 insertions(+), 1 deletion(-) diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index 4623e28..dcde3b8 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -20,8 +20,12 @@ # 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 libvirt import os +import socket +import string import time +import urlparse import uuid from xml.etree import ElementTree @@ -36,7 +40,8 @@ from kimchi.model.config import CapabilitiesModel from kimchi.model.templates import TemplateModel from kimchi.model.utils import get_vm_name from kimchi.screenshot import VMScreenshot -from kimchi.utils import run_setfacl_set_attr, template_name_from_uri +from kimchi.utils import kimchi_log, run_setfacl_set_attr +from kimchi.utils import template_name_from_uri DOM_STATE_MAP = {0: 'nostate', @@ -47,6 +52,10 @@ DOM_STATE_MAP = {0: 'nostate', 5: 'shutoff', 6: 'crashed'} +DEV_TYPE_SRC_ATTR_MAP = {'file': 'file', + 'block': 'dev', + 'dir': 'dir'} + GUESTS_STATS_INTERVAL = 5 VM_STATIC_UPDATE_PARAMS = {'name': './name'} VM_LIVE_UPDATE_PARAMS = {} @@ -474,3 +483,182 @@ class LibvirtVMScreenshot(VMScreenshot): stream.finish() finally: os.close(fd) + + +class StoragesModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def _get_storage_xml(self, params): + storage = """ + <disk type='%(src_type)s' device='%(dev_type)s'> + <driver name='qemu' type='raw'/>%(source)s + <target dev='%(dev)s' bus='ide'/> + </disk> + """ + info = {} + info['dev'] = params.get('dev') + info['dev_type'] = params.get('type') + info['src_type'] = params.get('src_type') + + # Working with url paths + if info['src_type'] == 'network': + net = {} + output = urlparse.urlparse(params.get('path')) + net['protocol'] = output.scheme + net['port'] = output.port or socket.getservbyname(output.scheme) + net['hostname'] = output.hostname + net['url_path'] = output.path + info['source'] = """ + <source protocol='%(protocol)s' name='%(url_path)s'> + <host name='%(hostname)s' port='%(port)s'/> + </source>""" % net + else: + # Fixing source attribute + info['source'] = """ + <source %s='%s'/>""" % (DEV_TYPE_SRC_ATTR_MAP[info['src_type']], + params.get('path')) + return storage % info + + def create(self, vm_name, params): + #TODO: Check if device name is already use + path = params.get('path') + + # Checking if path exist, if not url + if path.startswith('/'): + if not os.path.exists(path): + msg = "Path specified for device is not valide" + raise InvalidParameter(msg) + elif path.endswith('.iso') or os.path.isfile(path): + params['src_type'] = 'file' + elif os.path.isdir(path): + params['src_type'] = 'dir' + else: + params['src_type'] = 'block' + else: + params['src_type'] = 'network' + + # Use device name passed or pick next + dev_name = params.get('dev') + if not dev_name: + params['dev'] = self._get_storage_device_name(vm_name) + + # Add device to VM + dev_xml = self._get_storage_xml(params) + try: + conn = self.conn.get() + dom = conn.lookupByName(vm_name) + dom.attachDeviceFlags(dev_xml, libvirt.VIR_DOMAIN_AFFECT_CURRENT) + except Exception as e: + msg = 'Was not possible to attach storage device: %s' % e.message + kimchi_log.error(msg) + raise OperationFailed(e.message) + return params['dev'] + + def _get_storage_device_name(self, vm_name): + dev_list = [dev for dev in self.get_list(vm_name) \ + if dev.startswith('hd')] + if len(dev_list) == 0: + return 'hda' + dev_list.sort() + last_dev = dev_list.pop() + # TODO: Improve to device names "greater then" hdz + next_dev_letter_pos = string.ascii_lowercase.index(last_dev[2]) + 1 + return 'hd' + string.ascii_lowercase[next_dev_letter_pos] + + def get_list(self, vm_name): + dom = VMModel.get_vm(vm_name, self.conn) + xml = dom.XMLDesc(0) + device_xml = xmlutils.xml_get_child(xml, './devices') + storages = self._parse_vm_disks(device_xml, ['disk', 'cdrom']) + return storages + + def _parse_vm_disks(self, xml_str, filter_list): + # xml_str: xml_str of device with all devices + # filter_list: List of which device type to retrieve + root = ElementTree.fromstring(xml_str) + ret = [] + for opt in filter_list: + xpath = ".disk/[@device='%s']" % opt + for disk in root.findall(xpath): + name = disk.find('./target').attrib['dev'] + ret.append(name) + return ret + + +class StorageModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.kargs = kargs + + def _get_device_xml(self, vm_name, dev_name): + # Get VM xml and then devices xml + dom = VMModel.get_vm(vm_name, self.conn) + device_xml = xmlutils.xml_get_child(dom.XMLDesc(0), './devices') + dev_root = ElementTree.fromstring(device_xml) + xpath = "./disk/target[@dev='%s']/.." % dev_name + return dev_root.find(xpath) + + def lookup(self, vm_name, dev_name): + # Retrieve disk xml and format return dict + disk = self._get_device_xml(vm_name, dev_name) + if disk is None: + msg = 'The storage device "%s" does not exist in the guest "%s"' % ( + dev_name,vm_name) + raise NotFoundError(msg) + + source = disk.find('./source') + path = "" + if source is not None: + src_type = disk.attrib.get('type') + if src_type == 'network': + host = source.find('./host') + path = source.attrib.get('protocol') + '://' +\ + host.attrib.get('name') + ':' +\ + host.attrib.get('port') + source.attrib.get('name') + else: + path = source.attrib.get(DEV_TYPE_SRC_ATTR_MAP[src_type]) + dev_type = disk.attrib['device'] + return { 'dev': dev_name, + 'type': dev_type, + 'path': path} + + def delete(self, vm_name, dev_name): + # Get storage device xml + disk = self._get_device_xml(vm_name, dev_name) + if disk is None: + msg = 'The storage device "%s" does not exist in the guest "%s"' % ( + dev_name,vm_name) + raise NotFoundError(msg) + try: + conn = self.conn.get() + dom = conn.lookupByName(vm_name) + dom.detachDeviceFlags(ElementTree.tostring(disk), + libvirt.VIR_DOMAIN_AFFECT_CURRENT) + except Exception as e: + msg = 'Was not possible to detach storage device: %s' % e.message + kimchi_log.error(msg) + raise OperationFailed(e.message) + + def update(self, vm_name, dev_name, params): + info = self.lookup(vm_name, dev_name) + backup_params = info.copy() + try: + self.delete(vm_name, dev_name) + except: + msg = 'Was not possible to update storage device: %s' % e.message + kimchi_log.error(msg) + raise OperationFailed(e.message) + + info.update(params) + kargs = {'conn': self.conn} + stgModel = StoragesModel(**kargs) + try: + dev_name = stgModel.create(vm_name, info) + return dev_name + except Exception as e: + # Restoring previous device + dev_name = stgModel.create(vm_name, backup_params) + msg = 'Was not possible to update storage device: %s' % e.message + kimchi_log.error(msg) + raise OperationFailed(e.message) diff --git a/src/kimchi/xmlutils.py b/src/kimchi/xmlutils.py index 51ff0ec..176ceb1 100644 --- a/src/kimchi/xmlutils.py +++ b/src/kimchi/xmlutils.py @@ -40,3 +40,8 @@ def xml_item_update(xml, xpath, value): item = root.find(xpath) item.text = value return ElementTree.tostring(root, encoding="utf-8") + +def xml_get_child(xml, xpath): + root = ElementTree.fromstring(xml) + item = root.find(xpath) + return ElementTree.tostring(item, encoding="utf-8") -- 1.8.5.3

On 02/11/2014 06:46 AM, Rodrigo Trujillo wrote:
This patch adds the CREATE, LOOKUP, UPDATE and DELETE functionalities to guest storage devices support.
Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- src/kimchi/model/vms.py | 190 +++++++++++++++++++++++++++++++++++++++++++++++- src/kimchi/xmlutils.py | 5 ++ 2 files changed, 194 insertions(+), 1 deletion(-)
diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index 4623e28..dcde3b8 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -20,8 +20,12 @@ # 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 libvirt import os +import socket +import string import time +import urlparse import uuid from xml.etree import ElementTree
@@ -36,7 +40,8 @@ from kimchi.model.config import CapabilitiesModel from kimchi.model.templates import TemplateModel from kimchi.model.utils import get_vm_name from kimchi.screenshot import VMScreenshot -from kimchi.utils import run_setfacl_set_attr, template_name_from_uri +from kimchi.utils import kimchi_log, run_setfacl_set_attr +from kimchi.utils import template_name_from_uri
DOM_STATE_MAP = {0: 'nostate', @@ -47,6 +52,10 @@ DOM_STATE_MAP = {0: 'nostate', 5: 'shutoff', 6: 'crashed'}
+DEV_TYPE_SRC_ATTR_MAP = {'file': 'file', + 'block': 'dev', + 'dir': 'dir'} + GUESTS_STATS_INTERVAL = 5 VM_STATIC_UPDATE_PARAMS = {'name': './name'} VM_LIVE_UPDATE_PARAMS = {} @@ -474,3 +483,182 @@ class LibvirtVMScreenshot(VMScreenshot): stream.finish() finally: os.close(fd) + + +class StoragesModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def _get_storage_xml(self, params): + storage = """ + <disk type='%(src_type)s' device='%(dev_type)s'> + <driver name='qemu' type='raw'/>%(source)s + <target dev='%(dev)s' bus='ide'/> + </disk> + """ + info = {} + info['dev'] = params.get('dev') + info['dev_type'] = params.get('type') + info['src_type'] = params.get('src_type') + + # Working with url paths + if info['src_type'] == 'network': + net = {} + output = urlparse.urlparse(params.get('path')) + net['protocol'] = output.scheme + net['port'] = output.port or socket.getservbyname(output.scheme) + net['hostname'] = output.hostname + net['url_path'] = output.path + info['source'] = """ + <source protocol='%(protocol)s' name='%(url_path)s'> + <host name='%(hostname)s' port='%(port)s'/> + </source>""" % net + else: + # Fixing source attribute + info['source'] = """ + <source %s='%s'/>""" % (DEV_TYPE_SRC_ATTR_MAP[info['src_type']], + params.get('path')) + return storage % info + + def create(self, vm_name, params): + #TODO: Check if device name is already use + path = params.get('path') + + # Checking if path exist, if not url + if path.startswith('/'): + if not os.path.exists(path): + msg = "Path specified for device is not valide" + raise InvalidParameter(msg) + elif path.endswith('.iso') or os.path.isfile(path): + params['src_type'] = 'file' + elif os.path.isdir(path): + params['src_type'] = 'dir' + else: + params['src_type'] = 'block' + else: + params['src_type'] = 'network' + + # Use device name passed or pick next + dev_name = params.get('dev') + if not dev_name: + params['dev'] = self._get_storage_device_name(vm_name) + + # Add device to VM + dev_xml = self._get_storage_xml(params) + try: + conn = self.conn.get() + dom = conn.lookupByName(vm_name) + dom.attachDeviceFlags(dev_xml, libvirt.VIR_DOMAIN_AFFECT_CURRENT) + except Exception as e: + msg = 'Was not possible to attach storage device: %s' % e.message + kimchi_log.error(msg) + raise OperationFailed(e.message) + return params['dev'] + + def _get_storage_device_name(self, vm_name): + dev_list = [dev for dev in self.get_list(vm_name) \ + if dev.startswith('hd')] + if len(dev_list) == 0: + return 'hda' + dev_list.sort() + last_dev = dev_list.pop() + # TODO: Improve to device names "greater then" hdz + next_dev_letter_pos = string.ascii_lowercase.index(last_dev[2]) + 1 + return 'hd' + string.ascii_lowercase[next_dev_letter_pos] + + def get_list(self, vm_name): + dom = VMModel.get_vm(vm_name, self.conn) + xml = dom.XMLDesc(0) + device_xml = xmlutils.xml_get_child(xml, './devices') why add xml_get_child? how about use lxml? from lxml import objectify
devices = objectify.fromstring(xml).devices return [disk.target.attrib['dev'] for disk in devices.xpath("./disk[@device='disk']")] or return [disk.target.get('dev') for disk in devices.xpath("./disk[@device='disk']")]
+ storages = self._parse_vm_disks(device_xml, ['disk', 'cdrom']) + return storages + + def _parse_vm_disks(self, xml_str, filter_list): + # xml_str: xml_str of device with all devices + # filter_list: List of which device type to retrieve + root = ElementTree.fromstring(xml_str) + ret = [] + for opt in filter_list: + xpath = ".disk/[@device='%s']" % opt + for disk in root.findall(xpath): + name = disk.find('./target').attrib['dev'] + ret.append(name) + return ret + + +class StorageModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.kargs = kargs + + def _get_device_xml(self, vm_name, dev_name): + # Get VM xml and then devices xml + dom = VMModel.get_vm(vm_name, self.conn) + device_xml = xmlutils.xml_get_child(dom.XMLDesc(0), './devices') + dev_root = ElementTree.fromstring(device_xml) + xpath = "./disk/target[@dev='%s']/.." % dev_name + return dev_root.find(xpath) ditto + + def lookup(self, vm_name, dev_name): + # Retrieve disk xml and format return dict + disk = self._get_device_xml(vm_name, dev_name) + if disk is None: + msg = 'The storage device "%s" does not exist in the guest "%s"' % ( + dev_name,vm_name) + raise NotFoundError(msg) + + source = disk.find('./source') + path = "" + if source is not None: + src_type = disk.attrib.get('type') + if src_type == 'network': + host = source.find('./host') + path = source.attrib.get('protocol') + '://' +\ + host.attrib.get('name') + ':' +\ + host.attrib.get('port') + source.attrib.get('name') + else: + path = source.attrib.get(DEV_TYPE_SRC_ATTR_MAP[src_type]) + dev_type = disk.attrib['device'] + return { 'dev': dev_name, + 'type': dev_type, + 'path': path} + + def delete(self, vm_name, dev_name): + # Get storage device xml + disk = self._get_device_xml(vm_name, dev_name) + if disk is None: + msg = 'The storage device "%s" does not exist in the guest "%s"' % ( + dev_name,vm_name) + raise NotFoundError(msg) + try: + conn = self.conn.get() + dom = conn.lookupByName(vm_name) + dom.detachDeviceFlags(ElementTree.tostring(disk), + libvirt.VIR_DOMAIN_AFFECT_CURRENT) + except Exception as e: + msg = 'Was not possible to detach storage device: %s' % e.message + kimchi_log.error(msg) + raise OperationFailed(e.message) + + def update(self, vm_name, dev_name, params): + info = self.lookup(vm_name, dev_name) + backup_params = info.copy() + try: + self.delete(vm_name, dev_name) + except: + msg = 'Was not possible to update storage device: %s' % e.message + kimchi_log.error(msg) + raise OperationFailed(e.message) + + info.update(params) + kargs = {'conn': self.conn} + stgModel = StoragesModel(**kargs) + try: + dev_name = stgModel.create(vm_name, info) + return dev_name + except Exception as e: + # Restoring previous device + dev_name = stgModel.create(vm_name, backup_params) + msg = 'Was not possible to update storage device: %s' % e.message + kimchi_log.error(msg) + raise OperationFailed(e.message) diff --git a/src/kimchi/xmlutils.py b/src/kimchi/xmlutils.py index 51ff0ec..176ceb1 100644 --- a/src/kimchi/xmlutils.py +++ b/src/kimchi/xmlutils.py @@ -40,3 +40,8 @@ def xml_item_update(xml, xpath, value): item = root.find(xpath) item.text = value return ElementTree.tostring(root, encoding="utf-8") + +def xml_get_child(xml, xpath): + root = ElementTree.fromstring(xml) + item = root.find(xpath) + return ElementTree.tostring(item, encoding="utf-8")
-- Thanks and best regards! Sheldon Feng(冯少合)<shaohef@linux.vnet.ibm.com> IBM Linux Technology Center

This patch implements the mockmodel class to simulate lookup, add, remove, update of devices in a guest vm. Also, it adds test cases to test the rest API. Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- src/kimchi/mockmodel.py | 63 ++++++++++++++++++++++++++++++++++++++++++ tests/test_rest.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index f6400a4..3161eb0 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -560,6 +560,48 @@ class MockModel(object): def networks_get_list(self): return sorted(self._mock_networks.keys()) + def storages_create(self, vm_name, params): + path = params.get('path') + if path.startswith('/') and not os.path.exists(path): + msg = "Path specified for device is not valide" + raise InvalidParameter(msg) + + dom = self._get_vm(vm_name) + if params['dev'] in self.storages_get_list(vm_name): + return OperationFailed('Device name already in use.') + vmdev = MockVMStorageDevice(params) + dom.storagedevices[params['dev']] = vmdev + return params['dev'] + + def storages_get_list(self, vm_name): + dom = self._get_vm(vm_name) + return dom.storagedevices.keys() + + def storage_lookup(self, vm_name, dev_name): + dom = self._get_vm(vm_name) + if dev_name not in self.storages_get_list(vm_name): + msg = 'The storage device "%s" does not exist in the guest "%s"' \ + % (dev_name,vm_name) + raise NotFoundError(msg) + return dom.storagedevices.get('dev_name').info + + def storage_delete(self, vm_name, dev_name): + dom = self._get_vm(vm_name) + if dev_name not in self.storages_get_list(vm_name): + msg = 'The storage device "%s" does not exist in the guest "%s"' \ + % (dev_name,vm_name) + raise NotFoundError(msg) + dom.storagedevices.pop(dev_name) + + def storage_update(self, vm_name, dev_name, params): + try: + dom = self._get_vm(vm_name) + dom.storagedevices[dev_name].info.update(params) + except Exception as e: + msg = 'Was not possible to update storage device: %s' % e.message + raise OperationFailed(e.message) + return dev_name + def vmifaces_create(self, vm, params): if (params["type"] == "network" and params["network"] not in self.networks_get_list()): @@ -738,6 +780,24 @@ class MockVMTemplate(VMTemplate): return disk_paths +class MockVMStorageDevice(object): + def __init__(self, params): + # Defaults + if params['dev'] == 'hda': + self.info = {'dev': params.get('dev'), + 'type': 'disk', + 'path': '/tmp/myimage.img'} + elif params['dev'] == 'hdc': + self.info = {'dev': params.get('dev'), + 'type': 'cdrom', + 'path': ''} + # New devices + else: + self.info = {'dev': params.get('dev'), + 'type': params.get('type'), + 'path': params.get('path')} + + class MockVMIface(object): counter = 0 @@ -763,6 +823,9 @@ class MockVM(object): self.disk_paths = [] self.networks = template_info['networks'] ifaces = [MockVMIface(net) for net in self.networks] + default_devices = [{'dev':'hda'}, {'dev':'hdc'}] + self.storagedevices = dict([(dev['dev'], MockVMStorageDevice(dev)) \ + for dev in default_devices]) self.ifaces = dict([(iface.info['mac'], iface) for iface in ifaces]) self.info = {'state': 'shutoff', 'stats': "{'cpu_utilization': 20, 'net_throughput' : 35, \ diff --git a/tests/test_rest.py b/tests/test_rest.py index 8b033ae..3cf87c2 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -353,6 +353,79 @@ class RestTests(unittest.TestCase): resp = self.request('/templates/test', '{}', 'DELETE') self.assertEquals(204, resp.status) + def test_vm_storage_devices(self): + + with RollbackContext() as rollback: + # Create a template as a base for our VMs + req = json.dumps({'name': 'test', 'cdrom': '/nonexistent.iso'}) + resp = self.request('/templates', req, 'POST') + self.assertEquals(201, resp.status) + # Delete the template + rollback.prependDefer(self.request, + '/templates/test', '{}', 'DELETE') + + # Create a VM with default args + req = json.dumps({'name': 'test-vm', + 'template': '/templates/test'}) + resp = self.request('/vms', req, 'POST') + self.assertEquals(201, resp.status) + # Delete the VM + rollback.prependDefer(self.request, + '/vms/test-vm', '{}', 'DELETE') + + # Attach a storage disk + req = json.dumps({'dev': 'hdx', + 'type': 'disk', + 'path': '/tmp'}) + resp = self.request('/vms/test-vm/storages', req, 'POST') + self.assertEquals(201, resp.status) + # Delete the disk + rollback.prependDefer(self.request, + '/vms/test-vm/storages/hdx', '{}', 'DELETE') + + # Detach storage disk + resp = self.request('/vms/test-vm/storages/hdx', '{}', 'DELETE') + self.assertEquals(204, resp.status) + + # Detach storage disk that does not exist + resp = self.request('/vms/test-vm/storages/hdx', '{}', 'DELETE') + self.assertEquals(404, resp.status) + + # Attach cdrom with nonexistent iso + req = json.dumps({'dev': 'hdx', + 'type': 'cdrom', + 'path': '/tmp/nonexistent.iso'}) + resp = self.request('/vms/test-vm/storages', req, 'POST') + self.assertEquals(400, resp.status) + + open('/tmp/existent.iso', 'w').close() + # Attach a cdrom with existent dev name + req = json.dumps({'dev': 'hdx', + 'type': 'cdrom', + 'path': '/tmp/existent.iso'}) + resp = self.request('/vms/test-vm/storages', req, 'POST') + self.assertEquals(201, resp.status) + # Delete the cdrom + rollback.prependDefer(self.request, + '/vms/test-vm/storages/hdx', '{}', 'DELETE') + + # Change path of storage cdrom + req = json.dumps({'path': 'http://myserver.com/myiso.iso'}) + resp = self.request('/vms/test-vm/storages/hdx', req, 'PUT') + self.assertEquals(200, resp.status) + + # Test GET + devs = json.loads(self.request('/vms/test-vm/storages').read()) + self.assertEquals(3, len(devs)) + + # Detach storage cdrom + resp = self.request('/vms/test-vm/storages/hdx', '{}', 'DELETE') + self.assertEquals(204, resp.status) + + # Test GET + devs = json.loads(self.request('/vms/test-vm/storages').read()) + self.assertEquals(2, len(devs)) + def test_vm_iface(self): with RollbackContext() as rollback: -- 1.8.5.3

On 02/10/2014 08:46 PM, Rodrigo Trujillo wrote:
This patch implements the mockmodel class to simulate lookup, add, remove, update of devices in a guest vm. Also, it adds test cases to test the rest API.
Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- src/kimchi/mockmodel.py | 63 ++++++++++++++++++++++++++++++++++++++++++ tests/test_rest.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+)
diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index f6400a4..3161eb0 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -560,6 +560,48 @@ class MockModel(object): def networks_get_list(self): return sorted(self._mock_networks.keys())
+ def storages_create(self, vm_name, params): + path = params.get('path') + if path.startswith('/') and not os.path.exists(path): + msg = "Path specified for device is not valide"
typo: valid
+ raise InvalidParameter(msg) + + dom = self._get_vm(vm_name) + if params['dev'] in self.storages_get_list(vm_name): + return OperationFailed('Device name already in use.') + vmdev = MockVMStorageDevice(params) + dom.storagedevices[params['dev']] = vmdev + return params['dev'] + + def storages_get_list(self, vm_name): + dom = self._get_vm(vm_name) + return dom.storagedevices.keys() + + def storage_lookup(self, vm_name, dev_name): + dom = self._get_vm(vm_name) + if dev_name not in self.storages_get_list(vm_name): + msg = 'The storage device "%s" does not exist in the guest "%s"' \ + % (dev_name,vm_name) + raise NotFoundError(msg) + return dom.storagedevices.get('dev_name').info + + def storage_delete(self, vm_name, dev_name): + dom = self._get_vm(vm_name) + if dev_name not in self.storages_get_list(vm_name): + msg = 'The storage device "%s" does not exist in the guest "%s"' \ + % (dev_name,vm_name) + raise NotFoundError(msg) + dom.storagedevices.pop(dev_name) + + def storage_update(self, vm_name, dev_name, params): + try: + dom = self._get_vm(vm_name) + dom.storagedevices[dev_name].info.update(params) + except Exception as e: + msg = 'Was not possible to update storage device: %s' % e.message + raise OperationFailed(e.message) + return dev_name + def vmifaces_create(self, vm, params): if (params["type"] == "network" and params["network"] not in self.networks_get_list()): @@ -738,6 +780,24 @@ class MockVMTemplate(VMTemplate): return disk_paths
+class MockVMStorageDevice(object): + def __init__(self, params): + # Defaults + if params['dev'] == 'hda': + self.info = {'dev': params.get('dev'), + 'type': 'disk', + 'path': '/tmp/myimage.img'} + elif params['dev'] == 'hdc': + self.info = {'dev': params.get('dev'), + 'type': 'cdrom', + 'path': ''} + # New devices + else: + self.info = {'dev': params.get('dev'), + 'type': params.get('type'), + 'path': params.get('path')} + + class MockVMIface(object): counter = 0
@@ -763,6 +823,9 @@ class MockVM(object): self.disk_paths = [] self.networks = template_info['networks'] ifaces = [MockVMIface(net) for net in self.networks] + default_devices = [{'dev':'hda'}, {'dev':'hdc'}] + self.storagedevices = dict([(dev['dev'], MockVMStorageDevice(dev)) \ + for dev in default_devices]) self.ifaces = dict([(iface.info['mac'], iface) for iface in ifaces]) self.info = {'state': 'shutoff', 'stats': "{'cpu_utilization': 20, 'net_throughput' : 35, \ diff --git a/tests/test_rest.py b/tests/test_rest.py index 8b033ae..3cf87c2 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -353,6 +353,79 @@ class RestTests(unittest.TestCase): resp = self.request('/templates/test', '{}', 'DELETE') self.assertEquals(204, resp.status)
+ def test_vm_storage_devices(self): + + with RollbackContext() as rollback: + # Create a template as a base for our VMs + req = json.dumps({'name': 'test', 'cdrom': '/nonexistent.iso'}) + resp = self.request('/templates', req, 'POST') + self.assertEquals(201, resp.status) + # Delete the template + rollback.prependDefer(self.request, + '/templates/test', '{}', 'DELETE') + + # Create a VM with default args + req = json.dumps({'name': 'test-vm', + 'template': '/templates/test'}) + resp = self.request('/vms', req, 'POST') + self.assertEquals(201, resp.status) + # Delete the VM + rollback.prependDefer(self.request, + '/vms/test-vm', '{}', 'DELETE') + + # Attach a storage disk + req = json.dumps({'dev': 'hdx', + 'type': 'disk', + 'path': '/tmp'}) + resp = self.request('/vms/test-vm/storages', req, 'POST') + self.assertEquals(201, resp.status) + # Delete the disk + rollback.prependDefer(self.request, + '/vms/test-vm/storages/hdx', '{}', 'DELETE') + + # Detach storage disk + resp = self.request('/vms/test-vm/storages/hdx', '{}', 'DELETE') + self.assertEquals(204, resp.status) + + # Detach storage disk that does not exist + resp = self.request('/vms/test-vm/storages/hdx', '{}', 'DELETE') + self.assertEquals(404, resp.status) + + # Attach cdrom with nonexistent iso + req = json.dumps({'dev': 'hdx', + 'type': 'cdrom', + 'path': '/tmp/nonexistent.iso'}) + resp = self.request('/vms/test-vm/storages', req, 'POST') + self.assertEquals(400, resp.status) + + open('/tmp/existent.iso', 'w').close()
Delete the file after the test is completed?
+ # Attach a cdrom with existent dev name + req = json.dumps({'dev': 'hdx', + 'type': 'cdrom', + 'path': '/tmp/existent.iso'}) + resp = self.request('/vms/test-vm/storages', req, 'POST') + self.assertEquals(201, resp.status) + # Delete the cdrom + rollback.prependDefer(self.request, + '/vms/test-vm/storages/hdx', '{}', 'DELETE') + + # Change path of storage cdrom + req = json.dumps({'path': 'http://myserver.com/myiso.iso'}) + resp = self.request('/vms/test-vm/storages/hdx', req, 'PUT') + self.assertEquals(200, resp.status) + + # Test GET + devs = json.loads(self.request('/vms/test-vm/storages').read()) + self.assertEquals(3, len(devs)) + + # Detach storage cdrom + resp = self.request('/vms/test-vm/storages/hdx', '{}', 'DELETE') + self.assertEquals(204, resp.status) + + # Test GET + devs = json.loads(self.request('/vms/test-vm/storages').read()) + self.assertEquals(2, len(devs)) + def test_vm_iface(self):
with RollbackContext() as rollback:

On 02/10/2014 08:46 PM, Rodrigo Trujillo wrote:
This patch implements the mockmodel class to simulate lookup, add, remove, update of devices in a guest vm. Also, it adds test cases to test the rest API.
Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- src/kimchi/mockmodel.py | 63 ++++++++++++++++++++++++++++++++++++++++++ tests/test_rest.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+)
diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index f6400a4..3161eb0 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -560,6 +560,48 @@ class MockModel(object): def networks_get_list(self): return sorted(self._mock_networks.keys())
+ def storages_create(self, vm_name, params): + path = params.get('path') + if path.startswith('/') and not os.path.exists(path): + msg = "Path specified for device is not valide" + raise InvalidParameter(msg) + + dom = self._get_vm(vm_name) + if params['dev'] in self.storages_get_list(vm_name):
Ops... it will fail if I don't pass a 'dev' to create the cdrom It should be. dev = param.get('dev', None) if dev is not None and dev in self.storages_get_list(vm_name): raise
+ return OperationFailed('Device name already in use.') + vmdev = MockVMStorageDevice(params)
+ dom.storagedevices[params['dev']] = vmdev + return params['dev']
You need to choose a 'dev' if the user does not provide one
+ + def storages_get_list(self, vm_name): + dom = self._get_vm(vm_name) + return dom.storagedevices.keys() + + def storage_lookup(self, vm_name, dev_name): + dom = self._get_vm(vm_name) + if dev_name not in self.storages_get_list(vm_name): + msg = 'The storage device "%s" does not exist in the guest "%s"' \ + % (dev_name,vm_name) + raise NotFoundError(msg) + return dom.storagedevices.get('dev_name').info + + def storage_delete(self, vm_name, dev_name): + dom = self._get_vm(vm_name) + if dev_name not in self.storages_get_list(vm_name): + msg = 'The storage device "%s" does not exist in the guest "%s"' \ + % (dev_name,vm_name) + raise NotFoundError(msg) + dom.storagedevices.pop(dev_name) + + def storage_update(self, vm_name, dev_name, params): + try: + dom = self._get_vm(vm_name) + dom.storagedevices[dev_name].info.update(params) + except Exception as e: + msg = 'Was not possible to update storage device: %s' % e.message + raise OperationFailed(e.message) + return dev_name + def vmifaces_create(self, vm, params): if (params["type"] == "network" and params["network"] not in self.networks_get_list()): @@ -738,6 +780,24 @@ class MockVMTemplate(VMTemplate): return disk_paths
+class MockVMStorageDevice(object): + def __init__(self, params): + # Defaults + if params['dev'] == 'hda': + self.info = {'dev': params.get('dev'), + 'type': 'disk', + 'path': '/tmp/myimage.img'} + elif params['dev'] == 'hdc': + self.info = {'dev': params.get('dev'), + 'type': 'cdrom', + 'path': ''} + # New devices + else: + self.info = {'dev': params.get('dev'), + 'type': params.get('type'), + 'path': params.get('path')} + + class MockVMIface(object): counter = 0
@@ -763,6 +823,9 @@ class MockVM(object): self.disk_paths = [] self.networks = template_info['networks'] ifaces = [MockVMIface(net) for net in self.networks] + default_devices = [{'dev':'hda'}, {'dev':'hdc'}] + self.storagedevices = dict([(dev['dev'], MockVMStorageDevice(dev)) \ + for dev in default_devices]) self.ifaces = dict([(iface.info['mac'], iface) for iface in ifaces]) self.info = {'state': 'shutoff', 'stats': "{'cpu_utilization': 20, 'net_throughput' : 35, \ diff --git a/tests/test_rest.py b/tests/test_rest.py index 8b033ae..3cf87c2 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -353,6 +353,79 @@ class RestTests(unittest.TestCase): resp = self.request('/templates/test', '{}', 'DELETE') self.assertEquals(204, resp.status)
+ def test_vm_storage_devices(self): + + with RollbackContext() as rollback: + # Create a template as a base for our VMs + req = json.dumps({'name': 'test', 'cdrom': '/nonexistent.iso'}) + resp = self.request('/templates', req, 'POST') + self.assertEquals(201, resp.status) + # Delete the template + rollback.prependDefer(self.request, + '/templates/test', '{}', 'DELETE') + + # Create a VM with default args + req = json.dumps({'name': 'test-vm', + 'template': '/templates/test'}) + resp = self.request('/vms', req, 'POST') + self.assertEquals(201, resp.status) + # Delete the VM + rollback.prependDefer(self.request, + '/vms/test-vm', '{}', 'DELETE') + + # Attach a storage disk + req = json.dumps({'dev': 'hdx', + 'type': 'disk', + 'path': '/tmp'})
Add a test to create a cdrom without passing the "dev" parameter
+ resp = self.request('/vms/test-vm/storages', req, 'POST') + self.assertEquals(201, resp.status) + # Delete the disk + rollback.prependDefer(self.request, + '/vms/test-vm/storages/hdx', '{}', 'DELETE') + + # Detach storage disk + resp = self.request('/vms/test-vm/storages/hdx', '{}', 'DELETE') + self.assertEquals(204, resp.status) + + # Detach storage disk that does not exist + resp = self.request('/vms/test-vm/storages/hdx', '{}', 'DELETE') + self.assertEquals(404, resp.status) + + # Attach cdrom with nonexistent iso + req = json.dumps({'dev': 'hdx', + 'type': 'cdrom', + 'path': '/tmp/nonexistent.iso'}) + resp = self.request('/vms/test-vm/storages', req, 'POST') + self.assertEquals(400, resp.status) + + open('/tmp/existent.iso', 'w').close() + # Attach a cdrom with existent dev name + req = json.dumps({'dev': 'hdx', + 'type': 'cdrom', + 'path': '/tmp/existent.iso'}) + resp = self.request('/vms/test-vm/storages', req, 'POST') + self.assertEquals(201, resp.status) + # Delete the cdrom + rollback.prependDefer(self.request, + '/vms/test-vm/storages/hdx', '{}', 'DELETE') + + # Change path of storage cdrom + req = json.dumps({'path': 'http://myserver.com/myiso.iso'}) + resp = self.request('/vms/test-vm/storages/hdx', req, 'PUT') + self.assertEquals(200, resp.status) + + # Test GET + devs = json.loads(self.request('/vms/test-vm/storages').read()) + self.assertEquals(3, len(devs)) + + # Detach storage cdrom + resp = self.request('/vms/test-vm/storages/hdx', '{}', 'DELETE') + self.assertEquals(204, resp.status) + + # Test GET + devs = json.loads(self.request('/vms/test-vm/storages').read()) + self.assertEquals(2, len(devs)) + def test_vm_iface(self):
with RollbackContext() as rollback:
participants (3)
-
Aline Manera
-
Rodrigo Trujillo
-
Sheldon