[PATCH v6 (BACKEND) 0/5] Storagepool SCSI/FC

V6: - Fix minor issues from previous reviews - Change code to use new i18n translation system for exception error messages V5: - Add entries to API.md related to host pci devices API - Fix issues with wwpn and wwnn fields in lookup returns - Fix issues in mock model - Fix UI minor issues - Add UI strings to i18n - Refactored code accordig to code review suggestions V4: - Implements mockmodel and tests - Fix UI - Fix other minor issues V3: - Changed API to only receive the scsi host name when creating new pool - Changed API to require LUN when creating new VM on SCSI pool - Created feature test and removed libvirt test function - Rebased with new model structure - Added error function handlers to UIs - Fixed LUN selection window V2: - Implements Fibre Channel devices discover in the host - Allow vms_create receive a volume to create the disk (if pool is SCSI) - Create basic UI to select SCSI Host when creating SCSI FC pool - Draft of UI to select LUN to create new VM when template has a SCSI pool configured. (Need help of UI guys here!) Rodrigo Trujillo (5): Storagepool SCSI/FC: Implement node devices API backend Storagepool SCSI/FC: API.md entries related to host pci devices Storagepool SCSI/FC: Backend implementation Storagepool SCSI/FC: Modifies mockmodel and implements tests for FC pool Storagepool SCSI/FC: Modify backend errors messages to use i18n system docs/API.md | 33 +++++++++++++++++- src/kimchi/API.json | 16 +++++++-- src/kimchi/control/host.py | 16 +++++++++ src/kimchi/featuretests.py | 29 ++++++++++++++++ src/kimchi/i18n.py | 4 +++ src/kimchi/mockmodel.py | 56 +++++++++++++++++++++++++++--- src/kimchi/model/config.py | 2 ++ src/kimchi/model/host.py | 55 ++++++++++++++++++++++++++++++ src/kimchi/model/libvirtstoragepool.py | 48 ++++++++++++++++++++++++-- src/kimchi/model/storagepools.py | 30 +++++++++++++--- src/kimchi/model/templates.py | 5 +++ src/kimchi/model/vms.py | 27 +++++++++++++-- src/kimchi/vmtemplate.py | 31 ++++++++++++++++- tests/test_rest.py | 62 ++++++++++++++++++++++++++++++++++ tests/test_storagepool.py | 21 ++++++++++++ 15 files changed, 417 insertions(+), 18 deletions(-) -- 1.8.5.3

In order to implement support to SCSI/FC UI, it is necessary to retrieve node devices. Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- src/kimchi/control/host.py | 16 ++++++++++++++ src/kimchi/model/host.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/src/kimchi/control/host.py b/src/kimchi/control/host.py index 053c822..936d298 100644 --- a/src/kimchi/control/host.py +++ b/src/kimchi/control/host.py @@ -36,6 +36,7 @@ class Host(Resource): self.shutdown = self.generate_action_handler('shutdown') self.stats = HostStats(self.model) self.partitions = Partitions(self.model) + self.devices = Devices(self.model) @property def data(self): @@ -61,3 +62,18 @@ class Partition(Resource): @property def data(self): return self.info + + +class Devices(Collection): + def __init__(self, model): + super(Devices, self).__init__(model) + self.resource = Device + + +class Device(Resource): + def __init__(self, model, id): + super(Device, self).__init__(model, id) + + @property + def data(self): + return self.info diff --git a/src/kimchi/model/host.py b/src/kimchi/model/host.py index 80f93db..bc75ec4 100644 --- a/src/kimchi/model/host.py +++ b/src/kimchi/model/host.py @@ -30,8 +30,10 @@ from cherrypy.process.plugins import BackgroundTask from kimchi import disks from kimchi import netinfo +from kimchi import xmlutils from kimchi.basemodel import Singleton from kimchi.exception import NotFoundError, OperationFailed +from kimchi.model.config import CapabilitiesModel from kimchi.model.vms import DOM_STATE_MAP from kimchi.utils import kimchi_log @@ -201,3 +203,56 @@ class PartitionModel(object): raise NotFoundError("KCHPART0001E", {'name': name}) return disks.get_partition_details(name) + + +class DevicesModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def get_list(self, _cap=None): + conn = self.conn.get() + if _cap == None: + dev_names = [name.name() for name in conn.listAllDevices(0)] + elif _cap == 'fc_host': + dev_names = self._get_devices_fc_host() + else: + # Get devices with required capability + dev_names = conn.listDevices(_cap,0) + return dev_names + + def _get_devices_fc_host(self): + conn = self.conn.get() + # Libvirt < 1.0.5 does not support fc_host capability + if not CapabilitiesModel().fc_host_support: + ret = [] + scsi_hosts = conn.listDevices('scsi_host',0) + for host in scsi_hosts: + xml = conn.nodeDeviceLookupByName(host).XMLDesc(0) + path = '/device/capability/capability/@type' + if 'fc_host' in xmlutils.xpath_get_text(xml, path): + ret.append(host) + return ret + return conn.listDevices('fc_host',0) + + +class DeviceModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def lookup(self, nodedev_name): + conn = self.conn.get() + try: + dev_xml = conn.nodeDeviceLookupByName(nodedev_name).XMLDesc(0) + except: + raise NotFoundError('Node device "%s" not found' % nodedev_name) + cap_type = xmlutils.xpath_get_text( + dev_xml, '/device/capability/capability/@type') + wwnn = xmlutils.xpath_get_text( + dev_xml, '/device/capability/capability/wwnn') + wwpn = xmlutils.xpath_get_text( + dev_xml, '/device/capability/capability/wwpn') + return { + 'name': nodedev_name, + 'adapter_type': cap_type[0] if len(cap_type) >= 1 else '', + 'wwnn': wwnn[0] if len(wwnn) == 1 else '', + 'wwpn': wwpn[0] if len(wwpn) == 1 else ''} -- 1.8.5.3

Reviewed-by: Aline Manera <alinefm@linux.vnet.ibm.com> On 02/13/2014 04:32 PM, Rodrigo Trujillo wrote:
In order to implement support to SCSI/FC UI, it is necessary to retrieve node devices.
Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- src/kimchi/control/host.py | 16 ++++++++++++++ src/kimchi/model/host.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+)
diff --git a/src/kimchi/control/host.py b/src/kimchi/control/host.py index 053c822..936d298 100644 --- a/src/kimchi/control/host.py +++ b/src/kimchi/control/host.py @@ -36,6 +36,7 @@ class Host(Resource): self.shutdown = self.generate_action_handler('shutdown') self.stats = HostStats(self.model) self.partitions = Partitions(self.model) + self.devices = Devices(self.model)
@property def data(self): @@ -61,3 +62,18 @@ class Partition(Resource): @property def data(self): return self.info + + +class Devices(Collection): + def __init__(self, model): + super(Devices, self).__init__(model) + self.resource = Device + + +class Device(Resource): + def __init__(self, model, id): + super(Device, self).__init__(model, id) + + @property + def data(self): + return self.info diff --git a/src/kimchi/model/host.py b/src/kimchi/model/host.py index 80f93db..bc75ec4 100644 --- a/src/kimchi/model/host.py +++ b/src/kimchi/model/host.py @@ -30,8 +30,10 @@ from cherrypy.process.plugins import BackgroundTask
from kimchi import disks from kimchi import netinfo +from kimchi import xmlutils from kimchi.basemodel import Singleton from kimchi.exception import NotFoundError, OperationFailed +from kimchi.model.config import CapabilitiesModel from kimchi.model.vms import DOM_STATE_MAP from kimchi.utils import kimchi_log
@@ -201,3 +203,56 @@ class PartitionModel(object): raise NotFoundError("KCHPART0001E", {'name': name})
return disks.get_partition_details(name) + + +class DevicesModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def get_list(self, _cap=None): + conn = self.conn.get() + if _cap == None: + dev_names = [name.name() for name in conn.listAllDevices(0)] + elif _cap == 'fc_host': + dev_names = self._get_devices_fc_host() + else: + # Get devices with required capability + dev_names = conn.listDevices(_cap,0) + return dev_names + + def _get_devices_fc_host(self): + conn = self.conn.get() + # Libvirt < 1.0.5 does not support fc_host capability + if not CapabilitiesModel().fc_host_support: + ret = [] + scsi_hosts = conn.listDevices('scsi_host',0) + for host in scsi_hosts: + xml = conn.nodeDeviceLookupByName(host).XMLDesc(0) + path = '/device/capability/capability/@type' + if 'fc_host' in xmlutils.xpath_get_text(xml, path): + ret.append(host) + return ret + return conn.listDevices('fc_host',0) + + +class DeviceModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def lookup(self, nodedev_name): + conn = self.conn.get() + try: + dev_xml = conn.nodeDeviceLookupByName(nodedev_name).XMLDesc(0) + except: + raise NotFoundError('Node device "%s" not found' % nodedev_name) + cap_type = xmlutils.xpath_get_text( + dev_xml, '/device/capability/capability/@type') + wwnn = xmlutils.xpath_get_text( + dev_xml, '/device/capability/capability/wwnn') + wwpn = xmlutils.xpath_get_text( + dev_xml, '/device/capability/capability/wwpn') + return { + 'name': nodedev_name, + 'adapter_type': cap_type[0] if len(cap_type) >= 1 else '', + 'wwnn': wwnn[0] if len(wwnn) == 1 else '', + 'wwpn': wwpn[0] if len(wwpn) == 1 else ''}

This patch modifies the API.md to list get functions provided by backend. Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- docs/API.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/API.md b/docs/API.md index 04d2d97..d4a1716 100644 --- a/docs/API.md +++ b/docs/API.md @@ -766,3 +766,31 @@ Contains the host sample data. * size: The total size of the partition, in bytes * mountpoint: If the partition is mounted, represents the mountpoint. Otherwise blank. + +### Collection: Devices + +**URI:** /host/devices + +**Methods:** + +* **GET**: Retrieves list of host pci devices (Node Devices). + Currently only scsi_host devices are supported: + * Parameters: + * _cap: Filter node device list with given node device capability. + To list Fibre Channel SCSI Host devices, use "_cap=fc_host". + +### Resource: Device + +**URI:** /host/devices/*:name* + +**Methods:** + +* **GET**: Retrieve information of a single pci device. + Currently only scsi_host devices are supported: + * name: The name of the device. + * adapter_type: The capability type of the scsi_host device (fc_host). + Empty if pci device is not scsi_host. + * wwnn: The HBA Word Wide Node Name. + Empty if pci device is not scsi_host. + * wwpn: The HBA Word Wide Port Name + Empty if pci device is not scsi_host. -- 1.8.5.3

Reviewed-by: Aline Manera <alinefm@linux.vnet.ibm.com> On 02/13/2014 04:32 PM, Rodrigo Trujillo wrote:
This patch modifies the API.md to list get functions provided by backend.
Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- docs/API.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+)
diff --git a/docs/API.md b/docs/API.md index 04d2d97..d4a1716 100644 --- a/docs/API.md +++ b/docs/API.md @@ -766,3 +766,31 @@ Contains the host sample data. * size: The total size of the partition, in bytes * mountpoint: If the partition is mounted, represents the mountpoint. Otherwise blank. + +### Collection: Devices + +**URI:** /host/devices + +**Methods:** + +* **GET**: Retrieves list of host pci devices (Node Devices). + Currently only scsi_host devices are supported: + * Parameters: + * _cap: Filter node device list with given node device capability. + To list Fibre Channel SCSI Host devices, use "_cap=fc_host". + +### Resource: Device + +**URI:** /host/devices/*:name* + +**Methods:** + +* **GET**: Retrieve information of a single pci device. + Currently only scsi_host devices are supported: + * name: The name of the device. + * adapter_type: The capability type of the scsi_host device (fc_host). + Empty if pci device is not scsi_host. + * wwnn: The HBA Word Wide Node Name. + Empty if pci device is not scsi_host. + * wwpn: The HBA Word Wide Port Name + Empty if pci device is not scsi_host.

This patch creates functions that allow kimchi users to create an libvirt SCSI storagepool using the rest API. This patch creates the feature test to check fc_host capability in libvirt. This patch implements basic routines to add a disk (scsi) to a new vm template, based on given volumes (LUN name) from UI or API directly. Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- docs/API.md | 5 +++- src/kimchi/API.json | 14 ++++++++-- src/kimchi/featuretests.py | 29 ++++++++++++++++++++ src/kimchi/model/config.py | 2 ++ src/kimchi/model/libvirtstoragepool.py | 48 ++++++++++++++++++++++++++++++++-- src/kimchi/model/storagepools.py | 30 ++++++++++++++++++--- src/kimchi/model/templates.py | 5 ++++ src/kimchi/model/vms.py | 27 ++++++++++++++++--- src/kimchi/vmtemplate.py | 31 +++++++++++++++++++++- 9 files changed, 178 insertions(+), 13 deletions(-) diff --git a/docs/API.md b/docs/API.md index d4a1716..ee89fe0 100644 --- a/docs/API.md +++ b/docs/API.md @@ -55,6 +55,8 @@ the following general conventions: Independent Computing Environments * null: Graphics is disabled or type not supported * listen: The network which the vnc/spice server listens on. + * volumes *(optional)*: List of Fibre channel LUN names to be assigned as + disk to VM. Required if pool is type SCSI. ### Resource: Virtual Machine @@ -291,7 +293,7 @@ A interface represents available network interface on VM. * **POST**: Create a new Storage Pool * name: The name of the Storage Pool. * type: The type of the defined Storage Pool. - Supported types: 'dir', 'kimchi-iso', 'netfs', 'logical', 'iscsi' + Supported types: 'dir', 'kimchi-iso', 'netfs', 'logical', 'iscsi', 'scsi' * path: The path of the defined Storage Pool. For 'kimchi-iso' pool refers to targeted deep scan path. Pool types: 'dir', 'kimchi-iso'. @@ -310,6 +312,7 @@ A interface represents available network interface on VM. Pool types: 'iscsi'. * username: Login username of the iSCSI target. * password: Login password of the iSCSI target. + * adapter_name *(optional)*: Scsi host name. ### Resource: Storage Pool diff --git a/src/kimchi/API.json b/src/kimchi/API.json index 264ec57..84a9da7 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -43,7 +43,7 @@ "type": { "description": "The type of the defined Storage Pool", "type": "string", - "pattern": "^dir|netfs|logical|kimchi-iso$", + "pattern": "^dir|netfs|logical|kimchi-iso|iscsi|scsi$", "required": true, "error": "KCHPOOL0017E" }, @@ -90,6 +90,10 @@ "maximum": 65535, "error": "KCHPOOL0023E" }, + "adapter_name": { + "description": "SCSI host name", + "type": "string" + }, "auth": { "description": "Storage back-end authentication information", "type": "object", @@ -148,7 +152,13 @@ "pattern": "^/storagepools/[^/]+/?$", "error": "KCHVM0013E" }, - "graphics": { "$ref": "#/kimchitype/graphics" } + "graphics": { "$ref": "#/kimchitype/graphics" }, + "volumes": { + "description": "list of scsi volumes to be assigned to the new VM.", + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true + } } }, "vm_update": { diff --git a/src/kimchi/featuretests.py b/src/kimchi/featuretests.py index d924050..e759b6d 100644 --- a/src/kimchi/featuretests.py +++ b/src/kimchi/featuretests.py @@ -57,6 +57,18 @@ ISO_STREAM_XML = """ </devices> </domain>""" +SCSI_FC_XML = """ +<pool type='scsi'> + <name>TEST_SCSI_FC_POOL</name> + <source> + <adapter type='fc_host' wwnn='1234567890abcdef' wwpn='abcdef1234567890'/> + </source> + <target> + <path>/dev/disk/by-path</path> + </target> +</pool> +""" + class FeatureTests(object): @@ -150,3 +162,20 @@ class FeatureTests(object): return False return True + + @staticmethod + def libvirt_support_fc_host(): + try: + FeatureTests.disable_screen_error_logging() + conn = libvirt.open('qemu:///system') + pool = None + pool = conn.storagePoolDefineXML(SCSI_FC_XML, 0) + except libvirt.libvirtError as e: + if e.get_error_code() == 27: + # Libvirt requires adapter name, not needed when supports to FC + return False + finally: + FeatureTests.enable_screen_error_logging + pool is None or pool.undefine() + conn is None or conn.close() + return True diff --git a/src/kimchi/model/config.py b/src/kimchi/model/config.py index 9b5814a..6641cce 100644 --- a/src/kimchi/model/config.py +++ b/src/kimchi/model/config.py @@ -49,6 +49,7 @@ class CapabilitiesModel(object): self.qemu_stream = False self.qemu_stream_dns = False self.libvirt_stream_protocols = [] + self.fc_host_support = False # Subscribe function to set host capabilities to be run when cherrypy # server is up @@ -60,6 +61,7 @@ class CapabilitiesModel(object): self.qemu_stream = FeatureTests.qemu_supports_iso_stream() self.qemu_stream_dns = FeatureTests.qemu_iso_stream_dns() self.nfs_target_probe = FeatureTests.libvirt_support_nfs_probe() + self.fc_host_support = FeatureTests.libvirt_support_fc_host() self.libvirt_stream_protocols = [] for p in ['http', 'https', 'ftp', 'ftps', 'tftp']: diff --git a/src/kimchi/model/libvirtstoragepool.py b/src/kimchi/model/libvirtstoragepool.py index f045f1c..adffc55 100644 --- a/src/kimchi/model/libvirtstoragepool.py +++ b/src/kimchi/model/libvirtstoragepool.py @@ -29,8 +29,7 @@ import libvirt from kimchi.exception import InvalidParameter, OperationFailed, TimeoutExpired from kimchi.iscsi import TargetClient from kimchi.rollbackcontext import RollbackContext -from kimchi.utils import parse_cmd_output, run_command - +from kimchi.utils import kimchi_log, parse_cmd_output, run_command class StoragePoolDef(object): @classmethod @@ -173,6 +172,51 @@ class LogicalPoolDef(StoragePoolDef): return xml +class ScsiPoolDef(StoragePoolDef): + poolType = 'scsi' + + def prepare(self, conn=None): + tmp_name = self.poolArgs['source']['name'] + self.poolArgs['source']['name'] = tmp_name.replace('scsi_','') + # fc_host adapters type are only available in libvirt >= 1.0.5 + if not self.poolArgs['fc_host_support']: + self.poolArgs['source']['adapter_type'] = 'scsi_host' + msg = "Libvirt version <= 1.0.5. Setting SCSI host name as '%s'; "\ + "setting SCSI adapter type as 'scsi_host'; "\ + "ignoring wwnn and wwpn." %tmp_name + kimchi_log.info(msg) + # Path for Fibre Channel scsi hosts + self.poolArgs['path'] = '/dev/disk/by-path' + if not self.poolArgs['source']['adapter_type']: + self.poolArgs['source']['adapter_type'] = 'scsi_host' + + @property + def xml(self): + # Required parameters + # name: + # source[adapter_type]: + # source[name]: + # source[wwnn]: + # source[wwpn]: + # path: + + xml = """ + <pool type='scsi'> + <name>{name}</name> + <source> + <adapter type='{source[adapter_type]}'\ + name='{source[name]}'\ + wwnn='{source[wwnn]}'\ + wwpn='{source[wwpn]}'/> + </source> + <target> + <path>{path}</path> + </target> + </pool> + """.format(**self.poolArgs) + return xml + + class IscsiPoolDef(StoragePoolDef): poolType = 'iscsi' diff --git a/src/kimchi/model/storagepools.py b/src/kimchi/model/storagepools.py index ff664c4..69113b2 100644 --- a/src/kimchi/model/storagepools.py +++ b/src/kimchi/model/storagepools.py @@ -26,6 +26,8 @@ from kimchi import xmlutils from kimchi.scan import Scanner from kimchi.exception import InvalidOperation, MissingParameter from kimchi.exception import NotFoundError, OperationFailed +from kimchi.model.config import CapabilitiesModel +from kimchi.model.host import DeviceModel from kimchi.model.libvirtstoragepool import StoragePoolDef from kimchi.utils import add_task, kimchi_log from kimchi.utils import run_command @@ -39,7 +41,11 @@ POOL_STATE_MAP = {0: 'inactive', 4: 'inaccessible'} STORAGE_SOURCES = {'netfs': {'addr': '/pool/source/host/@name', - 'path': '/pool/source/dir/@path'}} + 'path': '/pool/source/dir/@path'}, + 'scsi': {'adapter_type': '/pool/source/adapter/@type', + 'adapter_name': '/pool/source/adapter/@name', + 'wwnn': '/pool/source/adapter/@wwnn', + 'wwpn': '/pool/source/adapter/@wwpn'}} class StoragePoolsModel(object): @@ -48,6 +54,8 @@ class StoragePoolsModel(object): self.objstore = kargs['objstore'] self.scanner = Scanner(self._clean_scan) self.scanner.delete() + self.caps = CapabilitiesModel() + self.device = DeviceModel(**kargs) def get_list(self): try: @@ -69,6 +77,14 @@ class StoragePoolsModel(object): if params['type'] == 'kimchi-iso': task_id = self._do_deep_scan(params) + + if params['type'] == 'scsi': + extra_params = self.device.lookup( + params['source']['adapter_name']) + # Adds name, adapter_type, wwpn and wwnn to source information + params['source'].update(extra_params) + params['fc_host_support'] = self.caps.fc_host_support + poolDef = StoragePoolDef.create(params) poolDef.prepare(conn) xml = poolDef.xml.encode("utf-8") @@ -86,9 +102,10 @@ class StoragePoolsModel(object): return name pool = conn.storagePoolDefineXML(xml, 0) - if params['type'] in ['logical', 'dir', 'netfs']: + if params['type'] in ['logical', 'dir', 'netfs', 'scsi']: pool.build(libvirt.VIR_STORAGE_POOL_BUILD_NEW) - # autostart dir and logical storage pool created from kimchi + # autostart dir, logical, netfs and scsi storage pools created + # from kimchi pool.setAutostart(1) else: # disable autostart for others @@ -168,7 +185,12 @@ class StoragePoolModel(object): for key, val in STORAGE_SOURCES[pool_type].items(): res = xmlutils.xpath_get_text(pool_xml, val) - source[key] = res[0] if len(res) == 1 else res + if len(res) == 1: + source[key] = res[0] + elif len(res) == 0: + source[key] = "" + else: + souce[key] = res return source diff --git a/src/kimchi/model/templates.py b/src/kimchi/model/templates.py index fd93414..3d05e80 100644 --- a/src/kimchi/model/templates.py +++ b/src/kimchi/model/templates.py @@ -161,6 +161,11 @@ class LibvirtVMTemplate(VMTemplate): xml = pool.XMLDesc(0) return xmlutils.xpath_get_text(xml, "/pool/target/path")[0] + def _get_storage_type(self): + pool = self._storage_validate() + xml = pool.XMLDesc(0) + return xmlutils.xpath_get_text(xml, "/pool/@type")[0] + def fork_vm_storage(self, vm_uuid): # Provision storage: # TODO: Rebase on the storage API once upstream diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index a5868c3..a215352 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -35,7 +35,7 @@ from cherrypy.process.plugins import BackgroundTask from kimchi import vnc from kimchi import xmlutils from kimchi.exception import InvalidOperation, InvalidParameter -from kimchi.exception import NotFoundError, OperationFailed +from kimchi.exception import MissingParameter, NotFoundError, OperationFailed from kimchi.model.config import CapabilitiesModel from kimchi.model.templates import TemplateModel from kimchi.model.utils import get_vm_name @@ -164,6 +164,11 @@ class VMsModel(object): 'diskRdKB': diskRdKB, 'diskWrKB': diskWrKB}) + def _get_volume_path(self, pool, vol): + conn = self.conn.get() + pool = conn.storagePoolLookupByName(pool) + return pool.storageVolLookupByName(vol).path() + def create(self, params): conn = self.conn.get() t_name = template_name_from_uri(params['template']) @@ -178,6 +183,7 @@ class VMsModel(object): pool_uri = params.get('storagepool') if pool_uri: vm_overrides['storagepool'] = pool_uri + vm_overrides['fc_host_support'] = self.caps.fc_host_support t = TemplateModel.get_template(t_name, self.objstore, self.conn, vm_overrides) @@ -185,7 +191,21 @@ class VMsModel(object): raise InvalidOperation("KCHVM0005E") t.validate() - vol_list = t.fork_vm_storage(vm_uuid) + + # If storagepool is SCSI, volumes will be LUNs and must be passed by + # the user from UI or manually. + vol_list = [] + if t._get_storage_type() == 'scsi': + if not params.get('volumes'): + raise MissingParameter("Volume list (LUNs names) not given.") + else: + # Get system path of the LUNs + pool = t.info['storagepool'].split('/')[-1] + for vol in params.get('volumes'): + path = self._get_volume_path(pool, vol) + vol_list.append((vol, path)) + else: + vol_list = t.fork_vm_storage(vm_uuid) # Store the icon for displaying later icon = t.info.get('icon') @@ -201,7 +221,8 @@ class VMsModel(object): xml = t.to_vm_xml(name, vm_uuid, libvirt_stream=libvirt_stream, qemu_stream_dns=self.caps.qemu_stream_dns, - graphics=graphics) + graphics=graphics, + volumes=vol_list) try: conn.defineXML(xml.encode('utf-8')) diff --git a/src/kimchi/vmtemplate.py b/src/kimchi/vmtemplate.py index 3545de4..37994c2 100644 --- a/src/kimchi/vmtemplate.py +++ b/src/kimchi/vmtemplate.py @@ -49,6 +49,7 @@ class VMTemplate(object): """ 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' @@ -180,6 +181,25 @@ class VMTemplate(object): graphics_xml = graphics_xml + spicevmc_xml return graphics_xml + def _get_scsi_disks_xml(self, luns): + ret = "" + # Passthrough configuration + disk_xml = """ + <disk type='volume' device='lun'> + <driver name='qemu' type='raw'/> + <source dev='%(src)s'/> + <target dev='%(dev)s' bus='scsi'/> + </disk>""" + if not self.fc_host_support: + disk_xml = disk_xml.replace('volume','block') + + # Creating disk xml for each lun passed + for index,(lun, path) in enumerate(luns): + dev = "sd%s" % string.lowercase[index] + params = {'src': path, 'dev': dev} + ret = ret + disk_xml % params + return ret + def to_volume_list(self, vm_uuid): storage_path = self._get_storage_path() ret = [] @@ -225,7 +245,6 @@ class VMTemplate(object): params = dict(self.info) params['name'] = vm_name params['uuid'] = vm_uuid - params['disks'] = self._get_disks_xml(vm_uuid) params['networks'] = self._get_networks_xml() params['qemu-namespace'] = '' params['cdroms'] = '' @@ -233,6 +252,13 @@ class VMTemplate(object): graphics = kwargs.get('graphics') params['graphics'] = self._get_graphics_xml(graphics) + # Current implementation just allows to create disk in one single + # storage pool, so we cannot mix the types (scsi volumes vs img file) + if self._get_storage_type() == 'scsi': + params['disks'] = self._get_scsi_disks_xml(kwargs.get('volumes')) + else: + params['disks'] = self._get_disks_xml(vm_uuid) + qemu_stream_dns = kwargs.get('qemu_stream_dns', False) libvirt_stream = kwargs.get('libvirt_stream', False) cdrom_xml = self._get_cdrom_xml(libvirt_stream, qemu_stream_dns) @@ -292,3 +318,6 @@ class VMTemplate(object): def _get_storage_path(self): return '' + + def _get_storage_type(self): + return '' -- 1.8.5.3

On 02/13/2014 04:32 PM, Rodrigo Trujillo wrote:
This patch creates functions that allow kimchi users to create an libvirt SCSI storagepool using the rest API. This patch creates the feature test to check fc_host capability in libvirt. This patch implements basic routines to add a disk (scsi) to a new vm template, based on given volumes (LUN name) from UI or API directly.
Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- docs/API.md | 5 +++- src/kimchi/API.json | 14 ++++++++-- src/kimchi/featuretests.py | 29 ++++++++++++++++++++ src/kimchi/model/config.py | 2 ++ src/kimchi/model/libvirtstoragepool.py | 48 ++++++++++++++++++++++++++++++++-- src/kimchi/model/storagepools.py | 30 ++++++++++++++++++--- src/kimchi/model/templates.py | 5 ++++ src/kimchi/model/vms.py | 27 ++++++++++++++++--- src/kimchi/vmtemplate.py | 31 +++++++++++++++++++++- 9 files changed, 178 insertions(+), 13 deletions(-)
diff --git a/docs/API.md b/docs/API.md index d4a1716..ee89fe0 100644 --- a/docs/API.md +++ b/docs/API.md @@ -55,6 +55,8 @@ the following general conventions: Independent Computing Environments * null: Graphics is disabled or type not supported * listen: The network which the vnc/spice server listens on. + * volumes *(optional)*: List of Fibre channel LUN names to be assigned as + disk to VM. Required if pool is type SCSI.
### Resource: Virtual Machine @@ -291,7 +293,7 @@ A interface represents available network interface on VM. * **POST**: Create a new Storage Pool * name: The name of the Storage Pool. * type: The type of the defined Storage Pool. - Supported types: 'dir', 'kimchi-iso', 'netfs', 'logical', 'iscsi' + Supported types: 'dir', 'kimchi-iso', 'netfs', 'logical', 'iscsi', 'scsi' * path: The path of the defined Storage Pool. For 'kimchi-iso' pool refers to targeted deep scan path. Pool types: 'dir', 'kimchi-iso'. @@ -310,6 +312,7 @@ A interface represents available network interface on VM. Pool types: 'iscsi'. * username: Login username of the iSCSI target. * password: Login password of the iSCSI target. + * adapter_name *(optional)*: Scsi host name.
### Resource: Storage Pool
diff --git a/src/kimchi/API.json b/src/kimchi/API.json index 264ec57..84a9da7 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -43,7 +43,7 @@ "type": { "description": "The type of the defined Storage Pool", "type": "string", - "pattern": "^dir|netfs|logical|kimchi-iso$", + "pattern": "^dir|netfs|logical|kimchi-iso|iscsi|scsi$", "required": true, "error": "KCHPOOL0017E" }, @@ -90,6 +90,10 @@ "maximum": 65535, "error": "KCHPOOL0023E" }, + "adapter_name": { + "description": "SCSI host name", + "type": "string" + }, "auth": { "description": "Storage back-end authentication information", "type": "object", @@ -148,7 +152,13 @@ "pattern": "^/storagepools/[^/]+/?$", "error": "KCHVM0013E" }, - "graphics": { "$ref": "#/kimchitype/graphics" } + "graphics": { "$ref": "#/kimchitype/graphics" }, + "volumes": { + "description": "list of scsi volumes to be assigned to the new VM.", + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true + } } }, "vm_update": { diff --git a/src/kimchi/featuretests.py b/src/kimchi/featuretests.py index d924050..e759b6d 100644 --- a/src/kimchi/featuretests.py +++ b/src/kimchi/featuretests.py @@ -57,6 +57,18 @@ ISO_STREAM_XML = """ </devices> </domain>"""
+SCSI_FC_XML = """ +<pool type='scsi'> + <name>TEST_SCSI_FC_POOL</name> + <source> + <adapter type='fc_host' wwnn='1234567890abcdef' wwpn='abcdef1234567890'/> + </source> + <target> + <path>/dev/disk/by-path</path> + </target> +</pool> +""" +
class FeatureTests(object):
@@ -150,3 +162,20 @@ class FeatureTests(object): return False
return True + + @staticmethod + def libvirt_support_fc_host(): + try: + FeatureTests.disable_screen_error_logging() + conn = libvirt.open('qemu:///system') + pool = None + pool = conn.storagePoolDefineXML(SCSI_FC_XML, 0) + except libvirt.libvirtError as e: + if e.get_error_code() == 27: + # Libvirt requires adapter name, not needed when supports to FC + return False + finally: + FeatureTests.enable_screen_error_logging + pool is None or pool.undefine() + conn is None or conn.close() + return True diff --git a/src/kimchi/model/config.py b/src/kimchi/model/config.py index 9b5814a..6641cce 100644 --- a/src/kimchi/model/config.py +++ b/src/kimchi/model/config.py @@ -49,6 +49,7 @@ class CapabilitiesModel(object): self.qemu_stream = False self.qemu_stream_dns = False self.libvirt_stream_protocols = [] + self.fc_host_support = False
# Subscribe function to set host capabilities to be run when cherrypy # server is up @@ -60,6 +61,7 @@ class CapabilitiesModel(object): self.qemu_stream = FeatureTests.qemu_supports_iso_stream() self.qemu_stream_dns = FeatureTests.qemu_iso_stream_dns() self.nfs_target_probe = FeatureTests.libvirt_support_nfs_probe() + self.fc_host_support = FeatureTests.libvirt_support_fc_host()
self.libvirt_stream_protocols = [] for p in ['http', 'https', 'ftp', 'ftps', 'tftp']: diff --git a/src/kimchi/model/libvirtstoragepool.py b/src/kimchi/model/libvirtstoragepool.py index f045f1c..adffc55 100644 --- a/src/kimchi/model/libvirtstoragepool.py +++ b/src/kimchi/model/libvirtstoragepool.py @@ -29,8 +29,7 @@ import libvirt from kimchi.exception import InvalidParameter, OperationFailed, TimeoutExpired from kimchi.iscsi import TargetClient from kimchi.rollbackcontext import RollbackContext -from kimchi.utils import parse_cmd_output, run_command - +from kimchi.utils import kimchi_log, parse_cmd_output, run_command
class StoragePoolDef(object): @classmethod @@ -173,6 +172,51 @@ class LogicalPoolDef(StoragePoolDef): return xml
+class ScsiPoolDef(StoragePoolDef): + poolType = 'scsi' + + def prepare(self, conn=None): + tmp_name = self.poolArgs['source']['name'] + self.poolArgs['source']['name'] = tmp_name.replace('scsi_','') + # fc_host adapters type are only available in libvirt >= 1.0.5 + if not self.poolArgs['fc_host_support']: + self.poolArgs['source']['adapter_type'] = 'scsi_host' + msg = "Libvirt version <= 1.0.5. Setting SCSI host name as '%s'; "\ + "setting SCSI adapter type as 'scsi_host'; "\ + "ignoring wwnn and wwpn." %tmp_name + kimchi_log.info(msg) + # Path for Fibre Channel scsi hosts + self.poolArgs['path'] = '/dev/disk/by-path' + if not self.poolArgs['source']['adapter_type']: + self.poolArgs['source']['adapter_type'] = 'scsi_host' + + @property + def xml(self): + # Required parameters + # name: + # source[adapter_type]: + # source[name]: + # source[wwnn]: + # source[wwpn]: + # path: + + xml = """ + <pool type='scsi'> + <name>{name}</name> + <source> + <adapter type='{source[adapter_type]}'\ + name='{source[name]}'\ + wwnn='{source[wwnn]}'\ + wwpn='{source[wwpn]}'/> + </source> + <target> + <path>{path}</path> + </target> + </pool> + """.format(**self.poolArgs) + return xml + + class IscsiPoolDef(StoragePoolDef): poolType = 'iscsi'
diff --git a/src/kimchi/model/storagepools.py b/src/kimchi/model/storagepools.py index ff664c4..69113b2 100644 --- a/src/kimchi/model/storagepools.py +++ b/src/kimchi/model/storagepools.py @@ -26,6 +26,8 @@ from kimchi import xmlutils from kimchi.scan import Scanner from kimchi.exception import InvalidOperation, MissingParameter from kimchi.exception import NotFoundError, OperationFailed +from kimchi.model.config import CapabilitiesModel +from kimchi.model.host import DeviceModel from kimchi.model.libvirtstoragepool import StoragePoolDef from kimchi.utils import add_task, kimchi_log from kimchi.utils import run_command @@ -39,7 +41,11 @@ POOL_STATE_MAP = {0: 'inactive', 4: 'inaccessible'}
STORAGE_SOURCES = {'netfs': {'addr': '/pool/source/host/@name', - 'path': '/pool/source/dir/@path'}} + 'path': '/pool/source/dir/@path'}, + 'scsi': {'adapter_type': '/pool/source/adapter/@type', + 'adapter_name': '/pool/source/adapter/@name', + 'wwnn': '/pool/source/adapter/@wwnn', + 'wwpn': '/pool/source/adapter/@wwpn'}}
class StoragePoolsModel(object): @@ -48,6 +54,8 @@ class StoragePoolsModel(object): self.objstore = kargs['objstore'] self.scanner = Scanner(self._clean_scan) self.scanner.delete() + self.caps = CapabilitiesModel() + self.device = DeviceModel(**kargs)
def get_list(self): try: @@ -69,6 +77,14 @@ class StoragePoolsModel(object):
if params['type'] == 'kimchi-iso': task_id = self._do_deep_scan(params) + + if params['type'] == 'scsi': + extra_params = self.device.lookup( + params['source']['adapter_name']) + # Adds name, adapter_type, wwpn and wwnn to source information + params['source'].update(extra_params) + params['fc_host_support'] = self.caps.fc_host_support + poolDef = StoragePoolDef.create(params) poolDef.prepare(conn) xml = poolDef.xml.encode("utf-8") @@ -86,9 +102,10 @@ class StoragePoolsModel(object): return name
pool = conn.storagePoolDefineXML(xml, 0) - if params['type'] in ['logical', 'dir', 'netfs']: + if params['type'] in ['logical', 'dir', 'netfs', 'scsi']: pool.build(libvirt.VIR_STORAGE_POOL_BUILD_NEW) - # autostart dir and logical storage pool created from kimchi + # autostart dir, logical, netfs and scsi storage pools created + # from kimchi pool.setAutostart(1) else: # disable autostart for others @@ -168,7 +185,12 @@ class StoragePoolModel(object):
for key, val in STORAGE_SOURCES[pool_type].items(): res = xmlutils.xpath_get_text(pool_xml, val) - source[key] = res[0] if len(res) == 1 else res + if len(res) == 1: + source[key] = res[0] + elif len(res) == 0: + source[key] = "" + else: + souce[key] = res
return source
diff --git a/src/kimchi/model/templates.py b/src/kimchi/model/templates.py index fd93414..3d05e80 100644 --- a/src/kimchi/model/templates.py +++ b/src/kimchi/model/templates.py @@ -161,6 +161,11 @@ class LibvirtVMTemplate(VMTemplate): xml = pool.XMLDesc(0) return xmlutils.xpath_get_text(xml, "/pool/target/path")[0]
+ def _get_storage_type(self): + pool = self._storage_validate() + xml = pool.XMLDesc(0) + return xmlutils.xpath_get_text(xml, "/pool/@type")[0] + def fork_vm_storage(self, vm_uuid): # Provision storage: # TODO: Rebase on the storage API once upstream diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index a5868c3..a215352 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -35,7 +35,7 @@ from cherrypy.process.plugins import BackgroundTask from kimchi import vnc from kimchi import xmlutils from kimchi.exception import InvalidOperation, InvalidParameter -from kimchi.exception import NotFoundError, OperationFailed +from kimchi.exception import MissingParameter, NotFoundError, OperationFailed from kimchi.model.config import CapabilitiesModel from kimchi.model.templates import TemplateModel from kimchi.model.utils import get_vm_name @@ -164,6 +164,11 @@ class VMsModel(object): 'diskRdKB': diskRdKB, 'diskWrKB': diskWrKB})
+ def _get_volume_path(self, pool, vol): + conn = self.conn.get() + pool = conn.storagePoolLookupByName(pool) + return pool.storageVolLookupByName(vol).path() + def create(self, params): conn = self.conn.get() t_name = template_name_from_uri(params['template']) @@ -178,6 +183,7 @@ class VMsModel(object): pool_uri = params.get('storagepool') if pool_uri: vm_overrides['storagepool'] = pool_uri + vm_overrides['fc_host_support'] = self.caps.fc_host_support t = TemplateModel.get_template(t_name, self.objstore, self.conn, vm_overrides)
@@ -185,7 +191,21 @@ class VMsModel(object): raise InvalidOperation("KCHVM0005E")
t.validate() - vol_list = t.fork_vm_storage(vm_uuid) + + # If storagepool is SCSI, volumes will be LUNs and must be passed by + # the user from UI or manually. + vol_list = [] + if t._get_storage_type() == 'scsi': + if not params.get('volumes'):
+ raise MissingParameter("Volume list (LUNs names) not given.")
Needs to do it according to refactor exception patches
+ else: + # Get system path of the LUNs + pool = t.info['storagepool'].split('/')[-1] + for vol in params.get('volumes'): + path = self._get_volume_path(pool, vol) + vol_list.append((vol, path)) + else: + vol_list = t.fork_vm_storage(vm_uuid)
# Store the icon for displaying later icon = t.info.get('icon') @@ -201,7 +221,8 @@ class VMsModel(object): xml = t.to_vm_xml(name, vm_uuid, libvirt_stream=libvirt_stream, qemu_stream_dns=self.caps.qemu_stream_dns, - graphics=graphics) + graphics=graphics, + volumes=vol_list)
try: conn.defineXML(xml.encode('utf-8')) diff --git a/src/kimchi/vmtemplate.py b/src/kimchi/vmtemplate.py index 3545de4..37994c2 100644 --- a/src/kimchi/vmtemplate.py +++ b/src/kimchi/vmtemplate.py @@ -49,6 +49,7 @@ class VMTemplate(object): """ 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' @@ -180,6 +181,25 @@ class VMTemplate(object): graphics_xml = graphics_xml + spicevmc_xml return graphics_xml
+ def _get_scsi_disks_xml(self, luns): + ret = "" + # Passthrough configuration + disk_xml = """ + <disk type='volume' device='lun'> + <driver name='qemu' type='raw'/> + <source dev='%(src)s'/> + <target dev='%(dev)s' bus='scsi'/> + </disk>""" + if not self.fc_host_support: + disk_xml = disk_xml.replace('volume','block') + + # Creating disk xml for each lun passed + for index,(lun, path) in enumerate(luns): + dev = "sd%s" % string.lowercase[index] + params = {'src': path, 'dev': dev} + ret = ret + disk_xml % params + return ret + def to_volume_list(self, vm_uuid): storage_path = self._get_storage_path() ret = [] @@ -225,7 +245,6 @@ class VMTemplate(object): params = dict(self.info) params['name'] = vm_name params['uuid'] = vm_uuid - params['disks'] = self._get_disks_xml(vm_uuid) params['networks'] = self._get_networks_xml() params['qemu-namespace'] = '' params['cdroms'] = '' @@ -233,6 +252,13 @@ class VMTemplate(object): graphics = kwargs.get('graphics') params['graphics'] = self._get_graphics_xml(graphics)
+ # Current implementation just allows to create disk in one single + # storage pool, so we cannot mix the types (scsi volumes vs img file) + if self._get_storage_type() == 'scsi': + params['disks'] = self._get_scsi_disks_xml(kwargs.get('volumes')) + else: + params['disks'] = self._get_disks_xml(vm_uuid) + qemu_stream_dns = kwargs.get('qemu_stream_dns', False) libvirt_stream = kwargs.get('libvirt_stream', False) cdrom_xml = self._get_cdrom_xml(libvirt_stream, qemu_stream_dns) @@ -292,3 +318,6 @@ class VMTemplate(object):
def _get_storage_path(self): return '' + + def _get_storage_type(self): + return ''

This patch modifies the Mock model functions to allow user to create a SCSI FC pool in test environment. Then implements functions to test API and rest. Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- src/kimchi/mockmodel.py | 55 +++++++++++++++++++++++++++++++++++++---- tests/test_rest.py | 62 +++++++++++++++++++++++++++++++++++++++++++++++ tests/test_storagepool.py | 21 ++++++++++++++++ 3 files changed, 133 insertions(+), 5 deletions(-) diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index f0f96eb..98715af 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -156,7 +156,16 @@ class MockModel(object): if icon: vm.info['icon'] = icon - vm.disk_paths = t.fork_vm_storage(vm_uuid) + pool = t._storage_validate() + if pool.info['type'] == 'scsi': + vm.disk_paths = [] + if not params.get('volumes'): + raise MissingParameter("Volume list (LUNs names) not given.") + for vol in params['volumes']: + vm.disk_paths.append({'pool': pool.name, + 'volume': vol}) + else: + vm.disk_paths = t.fork_vm_storage(vm_uuid) self._mock_vms[name] = vm return name @@ -302,8 +311,19 @@ class MockModel(object): name = params['name'] pool = MockStoragePool(name) pool.info['type'] = params['type'] - pool.info['path'] = params['path'] - if params['type'] == 'dir': + if params['type'] == 'scsi': + pool.info['path'] = '/dev/disk/by-path' + pool.info['source'] = params['source'] + if not pool.info['source'].get('adapter_name'): + raise MissingParameter('adapter_name') + for vol in ['unit:0:0:1','unit:0:0:2', + 'unit:0:0:3','unit:0:0:4']: + mockvol = MockStorageVolume(name, vol, + dict([('type','lun')])) + pool._volumes[vol] = mockvol + else: + pool.info['path'] = params['path'] + if params['type'] in ['dir','scsi']: pool.info['autostart'] = True else: pool.info['autostart'] = False @@ -405,6 +425,16 @@ class MockModel(object): raise InvalidOperation("KCHVOL0006E", {'pool': pool}) return res._volumes.keys() + def devices_get_list(self, _cap=None): + return ['scsi_host3', 'scsi_host4','scsi_host5'] + + def device_lookup(self, nodedev_name): + return { + 'name': nodedev_name, + 'adapter_type': 'fc_host', + 'wwnn': uuid.uuid4().hex[:16], + 'wwpn': uuid.uuid4().hex[:16]} + def isopool_lookup(self, name): return {'state': 'active', 'type': 'kimchi-iso'} @@ -873,11 +903,15 @@ class MockStorageVolume(object): def __init__(self, pool, name, params={}): self.name = name self.pool = pool + # Check if volume should be scsi lun + if params.get('type') == 'lun': + params = self._def_lun(name) fmt = params.get('format', 'raw') capacity = params.get('capacity', 1024) - self.info = {'type': 'disk', + self.info = {'type': params.get('type','disk'), 'capacity': capacity << 20, - 'allocation': 512, + 'allocation': params.get('allocation','512'), + 'path': params.get('path'), 'format': fmt} if fmt == 'iso': self.info['allocation'] = self.info['capacity'] @@ -885,6 +919,17 @@ class MockStorageVolume(object): self.info['os_distro'] = 'fedora' self.info['bootable'] = True + def _def_lun(self, name): + capacity = int(random.uniform(100, 300)) << 20 + path = "/dev/disk/by-path/pci-0000:0e:00.0-fc-0x20999980e52e4492-lun" + return { + "capacity": capacity, + "name": name, + "format": random.choice(['dos','unknown']), + "allocation": capacity, + "path": path + name[-1], + "type": "block" } + class MockVMScreenshot(VMScreenshot): OUTDATED_SECS = 5 diff --git a/tests/test_rest.py b/tests/test_rest.py index cd3a462..ce3e471 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -24,6 +24,7 @@ import base64 import json import os +import random import time import unittest @@ -145,6 +146,18 @@ class RestTests(unittest.TestCase): h = {'Accept': 'text/plain'} self.assertHTTPStatus(406, "/", None, 'GET', h) + def test_host_devices(self): + nodedevs = json.loads(self.request('/host/devices').read()) + # Mockmodel brings 3 preconfigured scsi fc_host + self.assertEquals(3, len(nodedevs)) + + nodedev = json.loads(self.request('/host/devices/scsi_host4').read()) + # Mockmodel generates random wwpn and wwnn + self.assertEquals('scsi_host4', nodedev['name']) + self.assertEquals('fc_host', nodedev['adapter_type']) + self.assertEquals(16, len(nodedev['wwpn'])) + self.assertEquals(16, len(nodedev['wwnn'])) + def test_get_vms(self): vms = json.loads(self.request('/vms').read()) self.assertEquals(0, len(vms)) @@ -514,6 +527,55 @@ class RestTests(unittest.TestCase): # Verify the volume was deleted self.assertHTTPStatus(404, vol_uri) + def test_scsi_fc_storage(self): + # Create scsi fc pool + req = json.dumps({'name': 'scsi_fc_pool', + 'type': 'scsi', + 'source': {'adapter_name': 'scsi_host3'}}) + resp = self.request('/storagepools', req, 'POST') + self.assertEquals(201, resp.status) + + # Create template with this pool + req = json.dumps({'name': 'test_fc_pool', 'cdrom': '/nonexistent.iso', + 'storagepool': '/storagepools/scsi_fc_pool'}) + resp = self.request('/templates', req, 'POST') + self.assertEquals(201, resp.status) + + # Test create vms using lun of this pool + ### activate the storage pool + resp = self.request('/storagepools/scsi_fc_pool/activate', '{}', 'POST') + + ### Get scsi pool luns and choose one + resp = self.request('/storagepools/scsi_fc_pool/storagevolumes') + luns = json.loads(resp.read()) + lun_name = random.choice(luns).get('name') + + ### Create vm in scsi pool without volumes: Error + req = json.dumps({'template': '/templates/test_fc_pool'}) + resp = self.request('/vms', req, 'POST') + self.assertEquals(400, resp.status) + + ### Create vm in scsi pool + req = json.dumps({'name': 'test-vm', + 'template': '/templates/test_fc_pool', + 'volumes': [lun_name]}) + resp = self.request('/vms', req, 'POST') + self.assertEquals(201, resp.status) + + # Start the VM + resp = self.request('/vms/test-vm/start', '{}', 'POST') + vm = json.loads(self.request('/vms/test-vm').read()) + self.assertEquals('running', vm['state']) + + # Force stop the VM + resp = self.request('/vms/test-vm/stop', '{}', 'POST') + vm = json.loads(self.request('/vms/test-vm').read()) + self.assertEquals('shutoff', vm['state']) + + # Delete the VM + resp = self.request('/vms/test-vm', '{}', 'DELETE') + self.assertEquals(204, resp.status) + def test_template_customise_storage(self): req = json.dumps({'name': 'test', 'cdrom': '/nonexistent.iso', 'disks': [{'size': 1}]}) diff --git a/tests/test_storagepool.py b/tests/test_storagepool.py index a3f4983..700c66e 100644 --- a/tests/test_storagepool.py +++ b/tests/test_storagepool.py @@ -141,6 +141,27 @@ class storagepoolTests(unittest.TestCase): <path>/dev/disk/by-id</path> </target> </pool> + """}, + {'def': + {'type': 'scsi', + 'name': 'unitTestSCSIFCPool', + 'path': '/dev/disk/by-path', + 'source': { + 'name': 'scsi_host3', + 'adapter_type': 'fc_host', + 'wwpn': '0123456789abcdef', + 'wwnn': 'abcdef0123456789' }}, + 'xml': + """ + <pool type='scsi'> + <name>unitTestSCSIFCPool</name> + <source> + <adapter type='fc_host' name='scsi_host3' wwnn='abcdef0123456789' wwpn='0123456789abcdef'></adapter> + </source> + <target> + <path>/dev/disk/by-path</path> + </target> + </pool> """}] for poolDef in poolDefs: -- 1.8.5.3

On 02/13/2014 04:33 PM, Rodrigo Trujillo wrote:
This patch modifies the Mock model functions to allow user to create a SCSI FC pool in test environment. Then implements functions to test API and rest.
Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- src/kimchi/mockmodel.py | 55 +++++++++++++++++++++++++++++++++++++---- tests/test_rest.py | 62 +++++++++++++++++++++++++++++++++++++++++++++++ tests/test_storagepool.py | 21 ++++++++++++++++ 3 files changed, 133 insertions(+), 5 deletions(-)
diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index f0f96eb..98715af 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -156,7 +156,16 @@ class MockModel(object): if icon: vm.info['icon'] = icon
- vm.disk_paths = t.fork_vm_storage(vm_uuid) + pool = t._storage_validate() + if pool.info['type'] == 'scsi': + vm.disk_paths = [] + if not params.get('volumes'):
+ raise MissingParameter("Volume list (LUNs names) not given.")
Here too.
+ for vol in params['volumes']: + vm.disk_paths.append({'pool': pool.name, + 'volume': vol}) + else: + vm.disk_paths = t.fork_vm_storage(vm_uuid) self._mock_vms[name] = vm return name
@@ -302,8 +311,19 @@ class MockModel(object): name = params['name'] pool = MockStoragePool(name) pool.info['type'] = params['type'] - pool.info['path'] = params['path'] - if params['type'] == 'dir': + if params['type'] == 'scsi': + pool.info['path'] = '/dev/disk/by-path' + pool.info['source'] = params['source'] + if not pool.info['source'].get('adapter_name'):
+ raise MissingParameter('adapter_name')
And here
+ for vol in ['unit:0:0:1','unit:0:0:2', + 'unit:0:0:3','unit:0:0:4']: + mockvol = MockStorageVolume(name, vol, + dict([('type','lun')])) + pool._volumes[vol] = mockvol + else: + pool.info['path'] = params['path'] + if params['type'] in ['dir','scsi']: pool.info['autostart'] = True else: pool.info['autostart'] = False @@ -405,6 +425,16 @@ class MockModel(object): raise InvalidOperation("KCHVOL0006E", {'pool': pool}) return res._volumes.keys()
+ def devices_get_list(self, _cap=None): + return ['scsi_host3', 'scsi_host4','scsi_host5'] + + def device_lookup(self, nodedev_name): + return { + 'name': nodedev_name, + 'adapter_type': 'fc_host', + 'wwnn': uuid.uuid4().hex[:16], + 'wwpn': uuid.uuid4().hex[:16]} + def isopool_lookup(self, name): return {'state': 'active', 'type': 'kimchi-iso'} @@ -873,11 +903,15 @@ class MockStorageVolume(object): def __init__(self, pool, name, params={}): self.name = name self.pool = pool + # Check if volume should be scsi lun + if params.get('type') == 'lun': + params = self._def_lun(name) fmt = params.get('format', 'raw') capacity = params.get('capacity', 1024) - self.info = {'type': 'disk', + self.info = {'type': params.get('type','disk'), 'capacity': capacity << 20, - 'allocation': 512, + 'allocation': params.get('allocation','512'), + 'path': params.get('path'), 'format': fmt} if fmt == 'iso': self.info['allocation'] = self.info['capacity'] @@ -885,6 +919,17 @@ class MockStorageVolume(object): self.info['os_distro'] = 'fedora' self.info['bootable'] = True
+ def _def_lun(self, name): + capacity = int(random.uniform(100, 300)) << 20 + path = "/dev/disk/by-path/pci-0000:0e:00.0-fc-0x20999980e52e4492-lun" + return { + "capacity": capacity, + "name": name, + "format": random.choice(['dos','unknown']), + "allocation": capacity, + "path": path + name[-1], + "type": "block" } +
class MockVMScreenshot(VMScreenshot): OUTDATED_SECS = 5 diff --git a/tests/test_rest.py b/tests/test_rest.py index cd3a462..ce3e471 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -24,6 +24,7 @@ import base64 import json import os +import random import time import unittest
@@ -145,6 +146,18 @@ class RestTests(unittest.TestCase): h = {'Accept': 'text/plain'} self.assertHTTPStatus(406, "/", None, 'GET', h)
+ def test_host_devices(self): + nodedevs = json.loads(self.request('/host/devices').read()) + # Mockmodel brings 3 preconfigured scsi fc_host + self.assertEquals(3, len(nodedevs)) + + nodedev = json.loads(self.request('/host/devices/scsi_host4').read()) + # Mockmodel generates random wwpn and wwnn + self.assertEquals('scsi_host4', nodedev['name']) + self.assertEquals('fc_host', nodedev['adapter_type']) + self.assertEquals(16, len(nodedev['wwpn'])) + self.assertEquals(16, len(nodedev['wwnn'])) + def test_get_vms(self): vms = json.loads(self.request('/vms').read()) self.assertEquals(0, len(vms)) @@ -514,6 +527,55 @@ class RestTests(unittest.TestCase): # Verify the volume was deleted self.assertHTTPStatus(404, vol_uri)
+ def test_scsi_fc_storage(self): + # Create scsi fc pool + req = json.dumps({'name': 'scsi_fc_pool', + 'type': 'scsi', + 'source': {'adapter_name': 'scsi_host3'}}) + resp = self.request('/storagepools', req, 'POST') + self.assertEquals(201, resp.status) + + # Create template with this pool + req = json.dumps({'name': 'test_fc_pool', 'cdrom': '/nonexistent.iso', + 'storagepool': '/storagepools/scsi_fc_pool'}) + resp = self.request('/templates', req, 'POST') + self.assertEquals(201, resp.status) + + # Test create vms using lun of this pool + ### activate the storage pool + resp = self.request('/storagepools/scsi_fc_pool/activate', '{}', 'POST') + + ### Get scsi pool luns and choose one + resp = self.request('/storagepools/scsi_fc_pool/storagevolumes') + luns = json.loads(resp.read()) + lun_name = random.choice(luns).get('name') + + ### Create vm in scsi pool without volumes: Error + req = json.dumps({'template': '/templates/test_fc_pool'}) + resp = self.request('/vms', req, 'POST') + self.assertEquals(400, resp.status) + + ### Create vm in scsi pool + req = json.dumps({'name': 'test-vm', + 'template': '/templates/test_fc_pool', + 'volumes': [lun_name]}) + resp = self.request('/vms', req, 'POST') + self.assertEquals(201, resp.status) + + # Start the VM + resp = self.request('/vms/test-vm/start', '{}', 'POST') + vm = json.loads(self.request('/vms/test-vm').read()) + self.assertEquals('running', vm['state']) + + # Force stop the VM + resp = self.request('/vms/test-vm/stop', '{}', 'POST') + vm = json.loads(self.request('/vms/test-vm').read()) + self.assertEquals('shutoff', vm['state']) + + # Delete the VM + resp = self.request('/vms/test-vm', '{}', 'DELETE') + self.assertEquals(204, resp.status) + def test_template_customise_storage(self): req = json.dumps({'name': 'test', 'cdrom': '/nonexistent.iso', 'disks': [{'size': 1}]}) diff --git a/tests/test_storagepool.py b/tests/test_storagepool.py index a3f4983..700c66e 100644 --- a/tests/test_storagepool.py +++ b/tests/test_storagepool.py @@ -141,6 +141,27 @@ class storagepoolTests(unittest.TestCase): <path>/dev/disk/by-id</path> </target> </pool> + """}, + {'def': + {'type': 'scsi', + 'name': 'unitTestSCSIFCPool', + 'path': '/dev/disk/by-path', + 'source': { + 'name': 'scsi_host3', + 'adapter_type': 'fc_host', + 'wwpn': '0123456789abcdef', + 'wwnn': 'abcdef0123456789' }}, + 'xml': + """ + <pool type='scsi'> + <name>unitTestSCSIFCPool</name> + <source> + <adapter type='fc_host' name='scsi_host3' wwnn='abcdef0123456789' wwpn='0123456789abcdef'></adapter> + </source> + <target> + <path>/dev/disk/by-path</path> + </target> + </pool> """}]
for poolDef in poolDefs:

This patch modifies the backend error exception return messages to use i18n.py and be tranlatable. Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- src/kimchi/API.json | 6 ++++-- src/kimchi/i18n.py | 4 ++++ src/kimchi/mockmodel.py | 5 +++-- src/kimchi/model/host.py | 2 +- src/kimchi/model/vms.py | 2 +- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/kimchi/API.json b/src/kimchi/API.json index 84a9da7..f93f540 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -92,7 +92,8 @@ }, "adapter_name": { "description": "SCSI host name", - "type": "string" + "type": "string", + "error": "KCHPOOL0030E" }, "auth": { "description": "Storage back-end authentication information", @@ -157,7 +158,8 @@ "description": "list of scsi volumes to be assigned to the new VM.", "type": "array", "items": { "type": "string" }, - "uniqueItems": true + "uniqueItems": true, + "error": "KCHVM0018E" } } }, diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index 03d1052..603adab 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -73,6 +73,8 @@ messages = { "KCHVM0014E": _("Supported virtual machine graphics are spice or VNC"), "KCHVM0015E": _("Graphics address to listen on must be IPv4 or IPv6"), "KCHVM0016E": _("Specify a template to create a virtual machine from"), + "KCHVM0017E": _("Volume list (LUNs names) not given."), + "KCHVM0018E": _("Virtual machine volumes must be a list of strings with distinct LUNs names."), "KCHVMIF0001E": _("Interface %(iface)s does not exist in virtual machine %(name)s"), "KCHVMIF0002E": _("Network %(network)s specified for virtual machine %(name)s does not exist"), @@ -129,6 +131,7 @@ messages = { "KCHPOOL0027E": _("%(disk)s is not a valid disk/partition. Could not add it to the pool %(pool)s."), "KCHPOOL0028E": _("Error while extending logical pool %(pool)s. Details: %(err)s"), "KCHPOOL0029E": _("The parameter disks only can be updated for logical storage pool."), + "KCHPOOL0030E": _("The SCSI host adapter name must be a string."), "KCHVOL0001E": _("Storage volume %(name)s already exists"), "KCHVOL0002E": _("Storage volume %(name)s does not exist in storage pool %(pool)s"), @@ -175,6 +178,7 @@ messages = { "KCHHOST0001E": _("Unable to shutdown host machine as there are running virtual machines"), "KCHHOST0002E": _("Unable to reboot host machine as there are running virtual machines"), + "KCHHOST0003E": _("Node device '%(name)s' not found"), "KCHOBJST0001E": _("Unable to find %(item)s in datastore"), diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 98715af..481498f 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -160,7 +160,7 @@ class MockModel(object): if pool.info['type'] == 'scsi': vm.disk_paths = [] if not params.get('volumes'): - raise MissingParameter("Volume list (LUNs names) not given.") + raise MissingParameter('KCHVM0017E') for vol in params['volumes']: vm.disk_paths.append({'pool': pool.name, 'volume': vol}) @@ -315,7 +315,8 @@ class MockModel(object): pool.info['path'] = '/dev/disk/by-path' pool.info['source'] = params['source'] if not pool.info['source'].get('adapter_name'): - raise MissingParameter('adapter_name') + raise MissingParameter('KCHPOOL0004E', + {'item': 'adapter_name', 'name': name}) for vol in ['unit:0:0:1','unit:0:0:2', 'unit:0:0:3','unit:0:0:4']: mockvol = MockStorageVolume(name, vol, diff --git a/src/kimchi/model/host.py b/src/kimchi/model/host.py index bc75ec4..514f672 100644 --- a/src/kimchi/model/host.py +++ b/src/kimchi/model/host.py @@ -244,7 +244,7 @@ class DeviceModel(object): try: dev_xml = conn.nodeDeviceLookupByName(nodedev_name).XMLDesc(0) except: - raise NotFoundError('Node device "%s" not found' % nodedev_name) + raise NotFoundError('KCHHOST0003E', {'name': nodedev_name}) cap_type = xmlutils.xpath_get_text( dev_xml, '/device/capability/capability/@type') wwnn = xmlutils.xpath_get_text( diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index a215352..d8807ec 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -197,7 +197,7 @@ class VMsModel(object): vol_list = [] if t._get_storage_type() == 'scsi': if not params.get('volumes'): - raise MissingParameter("Volume list (LUNs names) not given.") + raise MissingParameter('KCHVM0017E') else: # Get system path of the LUNs pool = t.info['storagepool'].split('/')[-1] -- 1.8.5.3

Reviewed-by: Aline Manera <alinefm@linux.vnet.ibm.com> I will join this patch to those when the changes were made at first time before applying On 02/13/2014 04:33 PM, Rodrigo Trujillo wrote:
This patch modifies the backend error exception return messages to use i18n.py and be tranlatable.
Signed-off-by: Rodrigo Trujillo <rodrigo.trujillo@linux.vnet.ibm.com> --- src/kimchi/API.json | 6 ++++-- src/kimchi/i18n.py | 4 ++++ src/kimchi/mockmodel.py | 5 +++-- src/kimchi/model/host.py | 2 +- src/kimchi/model/vms.py | 2 +- 5 files changed, 13 insertions(+), 6 deletions(-)
diff --git a/src/kimchi/API.json b/src/kimchi/API.json index 84a9da7..f93f540 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -92,7 +92,8 @@ }, "adapter_name": { "description": "SCSI host name", - "type": "string" + "type": "string", + "error": "KCHPOOL0030E" }, "auth": { "description": "Storage back-end authentication information", @@ -157,7 +158,8 @@ "description": "list of scsi volumes to be assigned to the new VM.", "type": "array", "items": { "type": "string" }, - "uniqueItems": true + "uniqueItems": true, + "error": "KCHVM0018E" } } }, diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index 03d1052..603adab 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -73,6 +73,8 @@ messages = { "KCHVM0014E": _("Supported virtual machine graphics are spice or VNC"), "KCHVM0015E": _("Graphics address to listen on must be IPv4 or IPv6"), "KCHVM0016E": _("Specify a template to create a virtual machine from"), + "KCHVM0017E": _("Volume list (LUNs names) not given."), + "KCHVM0018E": _("Virtual machine volumes must be a list of strings with distinct LUNs names."),
"KCHVMIF0001E": _("Interface %(iface)s does not exist in virtual machine %(name)s"), "KCHVMIF0002E": _("Network %(network)s specified for virtual machine %(name)s does not exist"), @@ -129,6 +131,7 @@ messages = { "KCHPOOL0027E": _("%(disk)s is not a valid disk/partition. Could not add it to the pool %(pool)s."), "KCHPOOL0028E": _("Error while extending logical pool %(pool)s. Details: %(err)s"), "KCHPOOL0029E": _("The parameter disks only can be updated for logical storage pool."), + "KCHPOOL0030E": _("The SCSI host adapter name must be a string."),
"KCHVOL0001E": _("Storage volume %(name)s already exists"), "KCHVOL0002E": _("Storage volume %(name)s does not exist in storage pool %(pool)s"), @@ -175,6 +178,7 @@ messages = {
"KCHHOST0001E": _("Unable to shutdown host machine as there are running virtual machines"), "KCHHOST0002E": _("Unable to reboot host machine as there are running virtual machines"), + "KCHHOST0003E": _("Node device '%(name)s' not found"),
"KCHOBJST0001E": _("Unable to find %(item)s in datastore"),
diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 98715af..481498f 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -160,7 +160,7 @@ class MockModel(object): if pool.info['type'] == 'scsi': vm.disk_paths = [] if not params.get('volumes'): - raise MissingParameter("Volume list (LUNs names) not given.") + raise MissingParameter('KCHVM0017E') for vol in params['volumes']: vm.disk_paths.append({'pool': pool.name, 'volume': vol}) @@ -315,7 +315,8 @@ class MockModel(object): pool.info['path'] = '/dev/disk/by-path' pool.info['source'] = params['source'] if not pool.info['source'].get('adapter_name'): - raise MissingParameter('adapter_name') + raise MissingParameter('KCHPOOL0004E', + {'item': 'adapter_name', 'name': name}) for vol in ['unit:0:0:1','unit:0:0:2', 'unit:0:0:3','unit:0:0:4']: mockvol = MockStorageVolume(name, vol, diff --git a/src/kimchi/model/host.py b/src/kimchi/model/host.py index bc75ec4..514f672 100644 --- a/src/kimchi/model/host.py +++ b/src/kimchi/model/host.py @@ -244,7 +244,7 @@ class DeviceModel(object): try: dev_xml = conn.nodeDeviceLookupByName(nodedev_name).XMLDesc(0) except: - raise NotFoundError('Node device "%s" not found' % nodedev_name) + raise NotFoundError('KCHHOST0003E', {'name': nodedev_name}) cap_type = xmlutils.xpath_get_text( dev_xml, '/device/capability/capability/@type') wwnn = xmlutils.xpath_get_text( diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index a215352..d8807ec 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -197,7 +197,7 @@ class VMsModel(object): vol_list = [] if t._get_storage_type() == 'scsi': if not params.get('volumes'): - raise MissingParameter("Volume list (LUNs names) not given.") + raise MissingParameter('KCHVM0017E') else: # Get system path of the LUNs pool = t.info['storagepool'].split('/')[-1]
participants (2)
-
Aline Manera
-
Rodrigo Trujillo