[PATCH v5 0/5] Support Creating iSCSI storage pool

This patch series is about creating iSCSI storage pool. After this patch, you can create an ISCSI storage pool as the following. curl -u root -H 'Content-type: application/json' \ -H 'Accept: application/json' \ -d '{"source": { "target": "iqn.2013-01.example.com.targetname", "host": "192.168.X.X", "port": 326X, "auth": {"username": "testUser", "password": "123456"}}, "type": "iscsi", "name": "testIscsiPool"}' \ http://127.0.0.1:8000/storagepools Note the "port" and "auth" is optional. Thanks the reviewers. I took most of reviewer advices and submit this v4 patch series. Tested the patch over LIO iSCSI target and libvirt-1.1.4-1.fc19. http://linux-iscsi.org/wiki/ISCSI v4 -> v5 Rebase to master and solve conflicts of file list in Makefile and import statements in .py files. Zhou Zheng Sheng (5): storagepool: refactor _get_pool_xml() storagepool: rename and consolidate arguments of creating (back-end) storagepool: rename and consolidate arguments of creating (front-end) storagepool: Support Creating iSCSI storagepool in model.py test_model: test creating iSCSI storage pool Makefile.am | 2 + contrib/DEBIAN/control.in | 3 +- contrib/kimchi.spec.fedora.in | 1 + contrib/kimchi.spec.suse.in | 1 + docs/API.md | 28 ++-- src/kimchi/API.json | 69 ++++++++++ src/kimchi/Makefile.am | 1 + src/kimchi/iscsi.py | 89 ++++++++++++ src/kimchi/model.py | 226 +++++++++++++++++++++++++------ tests/Makefile.am | 1 + tests/test_model.py | 91 +++++++------ tests/test_storagepool.py | 156 +++++++++++++++++++++ ui/js/src/kimchi.storagepool_add_main.js | 13 +- 13 files changed, 590 insertions(+), 91 deletions(-) create mode 100644 src/kimchi/iscsi.py create mode 100644 tests/test_storagepool.py -- 1.7.11.7

In src/kimchi/model.py, _get_pool_xml() is to generate libvirt storage pool XML definition from arguments provided by the POST data (a JSON dict). It has to support various types of pool such as dir, netfs and logical. Now it uses "if ... elif ... elif" to check the requested type of pool and go in to the respective branch and generates the correct XML. It also performs some preparations such as create target directory. This is OK for now but in future we'll have more types of pool, and add more validation and preparation code. So it needs a little refactor. In this patch we define StoragePoolDef object along with a factory methoed named "create". The factory method instantiates DirPoolDef, NetfsPoolDef or LogicalPoolDef according to the requested pool type. Then we can call defObj.prepare() to check NFS mount and iSCSI login/password. We can inspect defObj.xml to get the generated XML for the respective poolType. v2 -> v3 Call pool.build() for netfs, dir and logical pool. So do not need to create dirs any longer. Since NFS and iSCSI pool need validation, extract a StoragePoolDef object with "prepare" and "get_xml" methods. v3 -> v4 Turn 'defObj.get_xml()' into a lazily calculated property 'defObj.xml'. v4 -> v5 Do not cache generated xml, have subclass of StoragePoolDef override the '.xml' property. This makes the interface cleaner. Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- src/kimchi/model.py | 122 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 92 insertions(+), 30 deletions(-) diff --git a/src/kimchi/model.py b/src/kimchi/model.py index a6790b8..269fc97 100644 --- a/src/kimchi/model.py +++ b/src/kimchi/model.py @@ -994,7 +994,9 @@ class Model(object): if params['type'] == 'kimchi-iso': task_id = self._do_deep_scan(params) - xml = _get_pool_xml(**params) + poolDef = StoragePoolDef.create(params) + poolDef.prepare() + xml = poolDef.xml except KeyError, key: raise MissingParameter(key) @@ -1009,7 +1011,7 @@ class Model(object): return name pool = conn.storagePoolDefineXML(xml, 0) - if params['type'] in ['logical', 'dir']: + if params['type'] in ['logical', 'dir', 'netfs']: pool.build(libvirt.VIR_STORAGE_POOL_BUILD_NEW) # autostart dir and logical storage pool created from kimchi pool.setAutostart(1) @@ -1430,28 +1432,74 @@ class LibvirtVMScreenshot(VMScreenshot): finally: os.close(fd) -def _get_pool_xml(**kwargs): - # Required parameters - # name: - # type: - # path: - # nfspath: - if kwargs['type'] == 'dir': + +class StoragePoolDef(object): + @classmethod + def create(cls, poolArgs): + for klass in cls.__subclasses__(): + if poolArgs['type'] == klass.poolType: + return klass(poolArgs) + raise OperationFailed('Unsupported pool type: %s' % poolArgs['type']) + + def __init__(self, poolArgs): + self.poolArgs = poolArgs + + def prepare(self): + ''' Validate pool arguments and perform preparations. Operation which + would cause side effect should be put here. Subclasses can optionally + override this method, or it always succeeds by default. ''' + pass + + @property + def xml(self): + ''' Subclasses have to override this method to actually generate the + storage pool XML definition. Should cause no side effect and be + idempotent''' + raise OperationFailed('self.xml is not implemented: %s' % self) + + +class DirPoolDef(StoragePoolDef): + poolType = 'dir' + + @property + def xml(self): + # Required parameters + # name: + # type: + # path: xml = """ - <pool type='%(type)s'> + <pool type='dir'> <name>%(name)s</name> <target> <path>%(path)s</path> </target> </pool> - """ - elif kwargs['type'] == 'netfs': - path = '/var/lib/kimchi/nfs_mount/' + kwargs['name']; - if not os.path.exists(path): - os.makedirs(path) - kwargs.update({'path': path}) + """ % self.poolArgs + return xml + + +class NetfsPoolDef(StoragePoolDef): + poolType = 'netfs' + + def __init__(self, poolArgs): + super(NetfsPoolDef, self).__init__(poolArgs) + self.path = '/var/lib/kimchi/nfs_mount/' + self.poolArgs['name'] + + def prepare(self): + # TODO: Verify the NFS export can be actually mounted. + pass + + @property + def xml(self): + # Required parameters + # name: + # type: + # nfsserver: + # nfspath: + poolArgs = copy.deepcopy(self.poolArgs) + poolArgs['path'] = self.path xml = """ - <pool type='%(type)s'> + <pool type='netfs'> <name>%(name)s</name> <source> <host name='%(nfsserver)s'/> @@ -1461,13 +1509,32 @@ def _get_pool_xml(**kwargs): <path>%(path)s</path> </target> </pool> - """ - elif kwargs['type'] == 'logical': - path = '/var/lib/kimchi/logical_mount/' + kwargs['name']; - if not os.path.exists(path): - os.makedirs(path) + """ % poolArgs + return xml + + +class LogicalPoolDef(StoragePoolDef): + poolType = 'logical' + + def __init__(self, poolArgs): + super(LogicalPoolDef, self).__init__(poolArgs) + self.path = '/var/lib/kimchi/logical_mount/' + self.poolArgs['name'] + + @property + def xml(self): + # Required parameters + # name: + # type: + # devices: + poolArgs = copy.deepcopy(self.poolArgs) + devices = [] + for device_path in poolArgs['devices']: + devices.append('<device path="%s" />' % device_path) + + poolArgs.update({'devices': ''.join(devices), + 'path': self.path}) xml = """ - <pool type='%(type)s'> + <pool type='logical'> <name>%(name)s</name> <source> %(devices)s @@ -1476,14 +1543,9 @@ def _get_pool_xml(**kwargs): <path>%(path)s</path> </target> </pool> - """ - devices = '' - for device_path in kwargs['devices']: - devices += "<device path=\""+device_path+"\" />" + """ % poolArgs + return xml - kwargs.update({'devices': devices}) - kwargs.update({'path': path}) - return xml % kwargs def _get_volume_xml(**kwargs): # Required parameters -- 1.7.11.7

Reviewed-by: Aline Manera <alinefm@linux.vnet.ibm.com> On 01/02/2014 07:58 AM, Zhou Zheng Sheng wrote:
In src/kimchi/model.py, _get_pool_xml() is to generate libvirt storage pool XML definition from arguments provided by the POST data (a JSON dict). It has to support various types of pool such as dir, netfs and logical. Now it uses "if ... elif ... elif" to check the requested type of pool and go in to the respective branch and generates the correct XML. It also performs some preparations such as create target directory. This is OK for now but in future we'll have more types of pool, and add more validation and preparation code. So it needs a little refactor.
In this patch we define StoragePoolDef object along with a factory methoed named "create". The factory method instantiates DirPoolDef, NetfsPoolDef or LogicalPoolDef according to the requested pool type. Then we can call defObj.prepare() to check NFS mount and iSCSI login/password. We can inspect defObj.xml to get the generated XML for the respective poolType.
v2 -> v3 Call pool.build() for netfs, dir and logical pool. So do not need to create dirs any longer. Since NFS and iSCSI pool need validation, extract a StoragePoolDef object with "prepare" and "get_xml" methods.
v3 -> v4 Turn 'defObj.get_xml()' into a lazily calculated property 'defObj.xml'.
v4 -> v5 Do not cache generated xml, have subclass of StoragePoolDef override the '.xml' property. This makes the interface cleaner.
Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- src/kimchi/model.py | 122 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 92 insertions(+), 30 deletions(-)
diff --git a/src/kimchi/model.py b/src/kimchi/model.py index a6790b8..269fc97 100644 --- a/src/kimchi/model.py +++ b/src/kimchi/model.py @@ -994,7 +994,9 @@ class Model(object):
if params['type'] == 'kimchi-iso': task_id = self._do_deep_scan(params) - xml = _get_pool_xml(**params) + poolDef = StoragePoolDef.create(params) + poolDef.prepare() + xml = poolDef.xml except KeyError, key: raise MissingParameter(key)
@@ -1009,7 +1011,7 @@ class Model(object): return name
pool = conn.storagePoolDefineXML(xml, 0) - if params['type'] in ['logical', 'dir']: + if params['type'] in ['logical', 'dir', 'netfs']: pool.build(libvirt.VIR_STORAGE_POOL_BUILD_NEW) # autostart dir and logical storage pool created from kimchi pool.setAutostart(1) @@ -1430,28 +1432,74 @@ class LibvirtVMScreenshot(VMScreenshot): finally: os.close(fd)
-def _get_pool_xml(**kwargs): - # Required parameters - # name: - # type: - # path: - # nfspath: - if kwargs['type'] == 'dir': + +class StoragePoolDef(object): + @classmethod + def create(cls, poolArgs): + for klass in cls.__subclasses__(): + if poolArgs['type'] == klass.poolType: + return klass(poolArgs) + raise OperationFailed('Unsupported pool type: %s' % poolArgs['type']) + + def __init__(self, poolArgs): + self.poolArgs = poolArgs + + def prepare(self): + ''' Validate pool arguments and perform preparations. Operation which + would cause side effect should be put here. Subclasses can optionally + override this method, or it always succeeds by default. ''' + pass + + @property + def xml(self): + ''' Subclasses have to override this method to actually generate the + storage pool XML definition. Should cause no side effect and be + idempotent''' + raise OperationFailed('self.xml is not implemented: %s' % self) + + +class DirPoolDef(StoragePoolDef): + poolType = 'dir' + + @property + def xml(self): + # Required parameters + # name: + # type: + # path: xml = """ - <pool type='%(type)s'> + <pool type='dir'> <name>%(name)s</name> <target> <path>%(path)s</path> </target> </pool> - """ - elif kwargs['type'] == 'netfs': - path = '/var/lib/kimchi/nfs_mount/' + kwargs['name']; - if not os.path.exists(path): - os.makedirs(path) - kwargs.update({'path': path}) + """ % self.poolArgs + return xml + + +class NetfsPoolDef(StoragePoolDef): + poolType = 'netfs' + + def __init__(self, poolArgs): + super(NetfsPoolDef, self).__init__(poolArgs) + self.path = '/var/lib/kimchi/nfs_mount/' + self.poolArgs['name'] + + def prepare(self): + # TODO: Verify the NFS export can be actually mounted. + pass + + @property + def xml(self): + # Required parameters + # name: + # type: + # nfsserver: + # nfspath: + poolArgs = copy.deepcopy(self.poolArgs) + poolArgs['path'] = self.path xml = """ - <pool type='%(type)s'> + <pool type='netfs'> <name>%(name)s</name> <source> <host name='%(nfsserver)s'/> @@ -1461,13 +1509,32 @@ def _get_pool_xml(**kwargs): <path>%(path)s</path> </target> </pool> - """ - elif kwargs['type'] == 'logical': - path = '/var/lib/kimchi/logical_mount/' + kwargs['name']; - if not os.path.exists(path): - os.makedirs(path) + """ % poolArgs + return xml + + +class LogicalPoolDef(StoragePoolDef): + poolType = 'logical' + + def __init__(self, poolArgs): + super(LogicalPoolDef, self).__init__(poolArgs) + self.path = '/var/lib/kimchi/logical_mount/' + self.poolArgs['name'] + + @property + def xml(self): + # Required parameters + # name: + # type: + # devices: + poolArgs = copy.deepcopy(self.poolArgs) + devices = [] + for device_path in poolArgs['devices']: + devices.append('<device path="%s" />' % device_path) + + poolArgs.update({'devices': ''.join(devices), + 'path': self.path}) xml = """ - <pool type='%(type)s'> + <pool type='logical'> <name>%(name)s</name> <source> %(devices)s @@ -1476,14 +1543,9 @@ def _get_pool_xml(**kwargs): <path>%(path)s</path> </target> </pool> - """ - devices = '' - for device_path in kwargs['devices']: - devices += "<device path=\""+device_path+"\" />" + """ % poolArgs + return xml
- kwargs.update({'devices': devices}) - kwargs.update({'path': path}) - return xml % kwargs
def _get_volume_xml(**kwargs): # Required parameters

Reviewed-by: ShaoHe Feng <shaohef@linux.vnet.ibm.com> On 01/02/2014 05:58 PM, Zhou Zheng Sheng wrote:
In src/kimchi/model.py, _get_pool_xml() is to generate libvirt storage pool XML definition from arguments provided by the POST data (a JSON dict). It has to support various types of pool such as dir, netfs and logical. Now it uses "if ... elif ... elif" to check the requested type of pool and go in to the respective branch and generates the correct XML. It also performs some preparations such as create target directory. This is OK for now but in future we'll have more types of pool, and add more validation and preparation code. So it needs a little refactor.
In this patch we define StoragePoolDef object along with a factory methoed named "create". The factory method instantiates DirPoolDef, NetfsPoolDef or LogicalPoolDef according to the requested pool type. Then we can call defObj.prepare() to check NFS mount and iSCSI login/password. We can inspect defObj.xml to get the generated XML for the respective poolType.
v2 -> v3 Call pool.build() for netfs, dir and logical pool. So do not need to create dirs any longer. Since NFS and iSCSI pool need validation, extract a StoragePoolDef object with "prepare" and "get_xml" methods.
v3 -> v4 Turn 'defObj.get_xml()' into a lazily calculated property 'defObj.xml'.
v4 -> v5 Do not cache generated xml, have subclass of StoragePoolDef override the '.xml' property. This makes the interface cleaner.
Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- src/kimchi/model.py | 122 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 92 insertions(+), 30 deletions(-)
diff --git a/src/kimchi/model.py b/src/kimchi/model.py index a6790b8..269fc97 100644 --- a/src/kimchi/model.py +++ b/src/kimchi/model.py @@ -994,7 +994,9 @@ class Model(object):
if params['type'] == 'kimchi-iso': task_id = self._do_deep_scan(params) - xml = _get_pool_xml(**params) + poolDef = StoragePoolDef.create(params) + poolDef.prepare() + xml = poolDef.xml except KeyError, key: raise MissingParameter(key)
@@ -1009,7 +1011,7 @@ class Model(object): return name
pool = conn.storagePoolDefineXML(xml, 0) - if params['type'] in ['logical', 'dir']: + if params['type'] in ['logical', 'dir', 'netfs']: pool.build(libvirt.VIR_STORAGE_POOL_BUILD_NEW) # autostart dir and logical storage pool created from kimchi pool.setAutostart(1) @@ -1430,28 +1432,74 @@ class LibvirtVMScreenshot(VMScreenshot): finally: os.close(fd)
-def _get_pool_xml(**kwargs): - # Required parameters - # name: - # type: - # path: - # nfspath: - if kwargs['type'] == 'dir': + +class StoragePoolDef(object): + @classmethod + def create(cls, poolArgs): + for klass in cls.__subclasses__(): + if poolArgs['type'] == klass.poolType: + return klass(poolArgs) + raise OperationFailed('Unsupported pool type: %s' % poolArgs['type']) + + def __init__(self, poolArgs): + self.poolArgs = poolArgs + + def prepare(self): + ''' Validate pool arguments and perform preparations. Operation which + would cause side effect should be put here. Subclasses can optionally + override this method, or it always succeeds by default. ''' + pass + + @property + def xml(self): + ''' Subclasses have to override this method to actually generate the + storage pool XML definition. Should cause no side effect and be + idempotent''' + raise OperationFailed('self.xml is not implemented: %s' % self) + + +class DirPoolDef(StoragePoolDef): + poolType = 'dir' + + @property + def xml(self): + # Required parameters + # name: + # type: + # path: xml = """ - <pool type='%(type)s'> + <pool type='dir'> <name>%(name)s</name> <target> <path>%(path)s</path> </target> </pool> - """ - elif kwargs['type'] == 'netfs': - path = '/var/lib/kimchi/nfs_mount/' + kwargs['name']; - if not os.path.exists(path): - os.makedirs(path) - kwargs.update({'path': path}) + """ % self.poolArgs + return xml + + +class NetfsPoolDef(StoragePoolDef): + poolType = 'netfs' + + def __init__(self, poolArgs): + super(NetfsPoolDef, self).__init__(poolArgs) + self.path = '/var/lib/kimchi/nfs_mount/' + self.poolArgs['name'] + + def prepare(self): + # TODO: Verify the NFS export can be actually mounted. + pass + + @property + def xml(self): + # Required parameters + # name: + # type: + # nfsserver: + # nfspath: + poolArgs = copy.deepcopy(self.poolArgs) + poolArgs['path'] = self.path xml = """ - <pool type='%(type)s'> + <pool type='netfs'> <name>%(name)s</name> <source> <host name='%(nfsserver)s'/> @@ -1461,13 +1509,32 @@ def _get_pool_xml(**kwargs): <path>%(path)s</path> </target> </pool> - """ - elif kwargs['type'] == 'logical': - path = '/var/lib/kimchi/logical_mount/' + kwargs['name']; - if not os.path.exists(path): - os.makedirs(path) + """ % poolArgs + return xml + + +class LogicalPoolDef(StoragePoolDef): + poolType = 'logical' + + def __init__(self, poolArgs): + super(LogicalPoolDef, self).__init__(poolArgs) + self.path = '/var/lib/kimchi/logical_mount/' + self.poolArgs['name'] + + @property + def xml(self): + # Required parameters + # name: + # type: + # devices: + poolArgs = copy.deepcopy(self.poolArgs) + devices = [] + for device_path in poolArgs['devices']: + devices.append('<device path="%s" />' % device_path) + + poolArgs.update({'devices': ''.join(devices), + 'path': self.path}) xml = """ - <pool type='%(type)s'> + <pool type='logical'> <name>%(name)s</name> <source> %(devices)s @@ -1476,14 +1543,9 @@ def _get_pool_xml(**kwargs): <path>%(path)s</path> </target> </pool> - """ - devices = '' - for device_path in kwargs['devices']: - devices += "<device path=\""+device_path+"\" />" + """ % poolArgs + return xml
- kwargs.update({'devices': devices}) - kwargs.update({'path': path}) - return xml % kwargs
def _get_volume_xml(**kwargs): # Required parameters
-- Thanks and best regards! Sheldon Feng(冯少合)<shaohef@linux.vnet.ibm.com> IBM Linux Technology Center

Reviewed-by: Royce Lv<lvroyce@linux.vnet.ibm.com> On 2014年01月02日 17:58, Zhou Zheng Sheng wrote:
In src/kimchi/model.py, _get_pool_xml() is to generate libvirt storage pool XML definition from arguments provided by the POST data (a JSON dict). It has to support various types of pool such as dir, netfs and logical. Now it uses "if ... elif ... elif" to check the requested type of pool and go in to the respective branch and generates the correct XML. It also performs some preparations such as create target directory. This is OK for now but in future we'll have more types of pool, and add more validation and preparation code. So it needs a little refactor.
In this patch we define StoragePoolDef object along with a factory methoed named "create". The factory method instantiates DirPoolDef, NetfsPoolDef or LogicalPoolDef according to the requested pool type. Then we can call defObj.prepare() to check NFS mount and iSCSI login/password. We can inspect defObj.xml to get the generated XML for the respective poolType.
v2 -> v3 Call pool.build() for netfs, dir and logical pool. So do not need to create dirs any longer. Since NFS and iSCSI pool need validation, extract a StoragePoolDef object with "prepare" and "get_xml" methods.
v3 -> v4 Turn 'defObj.get_xml()' into a lazily calculated property 'defObj.xml'.
v4 -> v5 Do not cache generated xml, have subclass of StoragePoolDef override the '.xml' property. This makes the interface cleaner.
Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- src/kimchi/model.py | 122 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 92 insertions(+), 30 deletions(-)
diff --git a/src/kimchi/model.py b/src/kimchi/model.py index a6790b8..269fc97 100644 --- a/src/kimchi/model.py +++ b/src/kimchi/model.py @@ -994,7 +994,9 @@ class Model(object):
if params['type'] == 'kimchi-iso': task_id = self._do_deep_scan(params) - xml = _get_pool_xml(**params) + poolDef = StoragePoolDef.create(params) + poolDef.prepare() + xml = poolDef.xml except KeyError, key: raise MissingParameter(key)
@@ -1009,7 +1011,7 @@ class Model(object): return name
pool = conn.storagePoolDefineXML(xml, 0) - if params['type'] in ['logical', 'dir']: + if params['type'] in ['logical', 'dir', 'netfs']: pool.build(libvirt.VIR_STORAGE_POOL_BUILD_NEW) # autostart dir and logical storage pool created from kimchi pool.setAutostart(1) @@ -1430,28 +1432,74 @@ class LibvirtVMScreenshot(VMScreenshot): finally: os.close(fd)
-def _get_pool_xml(**kwargs): - # Required parameters - # name: - # type: - # path: - # nfspath: - if kwargs['type'] == 'dir': + +class StoragePoolDef(object): + @classmethod + def create(cls, poolArgs): + for klass in cls.__subclasses__(): + if poolArgs['type'] == klass.poolType: + return klass(poolArgs) + raise OperationFailed('Unsupported pool type: %s' % poolArgs['type']) + + def __init__(self, poolArgs): + self.poolArgs = poolArgs + + def prepare(self): + ''' Validate pool arguments and perform preparations. Operation which + would cause side effect should be put here. Subclasses can optionally + override this method, or it always succeeds by default. ''' + pass + + @property + def xml(self): + ''' Subclasses have to override this method to actually generate the + storage pool XML definition. Should cause no side effect and be + idempotent''' + raise OperationFailed('self.xml is not implemented: %s' % self) + + +class DirPoolDef(StoragePoolDef): + poolType = 'dir' + + @property + def xml(self): + # Required parameters + # name: + # type: + # path: xml = """ - <pool type='%(type)s'> + <pool type='dir'> <name>%(name)s</name> <target> <path>%(path)s</path> </target> </pool> - """ - elif kwargs['type'] == 'netfs': - path = '/var/lib/kimchi/nfs_mount/' + kwargs['name']; - if not os.path.exists(path): - os.makedirs(path) - kwargs.update({'path': path}) + """ % self.poolArgs + return xml + + +class NetfsPoolDef(StoragePoolDef): + poolType = 'netfs' + + def __init__(self, poolArgs): + super(NetfsPoolDef, self).__init__(poolArgs) + self.path = '/var/lib/kimchi/nfs_mount/' + self.poolArgs['name'] + + def prepare(self): + # TODO: Verify the NFS export can be actually mounted. + pass + + @property + def xml(self): + # Required parameters + # name: + # type: + # nfsserver: + # nfspath: + poolArgs = copy.deepcopy(self.poolArgs) + poolArgs['path'] = self.path xml = """ - <pool type='%(type)s'> + <pool type='netfs'> <name>%(name)s</name> <source> <host name='%(nfsserver)s'/> @@ -1461,13 +1509,32 @@ def _get_pool_xml(**kwargs): <path>%(path)s</path> </target> </pool> - """ - elif kwargs['type'] == 'logical': - path = '/var/lib/kimchi/logical_mount/' + kwargs['name']; - if not os.path.exists(path): - os.makedirs(path) + """ % poolArgs + return xml + + +class LogicalPoolDef(StoragePoolDef): + poolType = 'logical' + + def __init__(self, poolArgs): + super(LogicalPoolDef, self).__init__(poolArgs) + self.path = '/var/lib/kimchi/logical_mount/' + self.poolArgs['name'] + + @property + def xml(self): + # Required parameters + # name: + # type: + # devices: + poolArgs = copy.deepcopy(self.poolArgs) + devices = [] + for device_path in poolArgs['devices']: + devices.append('<device path="%s" />' % device_path) + + poolArgs.update({'devices': ''.join(devices), + 'path': self.path}) xml = """ - <pool type='%(type)s'> + <pool type='logical'> <name>%(name)s</name> <source> %(devices)s @@ -1476,14 +1543,9 @@ def _get_pool_xml(**kwargs): <path>%(path)s</path> </target> </pool> - """ - devices = '' - for device_path in kwargs['devices']: - devices += "<device path=\""+device_path+"\" />" + """ % poolArgs + return xml
- kwargs.update({'devices': devices}) - kwargs.update({'path': path}) - return xml % kwargs
def _get_volume_xml(**kwargs): # Required parameters

As we are adding support to new type of storage pool, the current naming scheme of the storage pool creating arguments should be rearranged to be more extendable. This patch renames some arguments and consolidates the argument of the same purposes as follow. This patch moves storage source related arguments under "source" key, while "source" itself is a dictionary. nfsserver -> source.host This is because in future patches, iSCSI pool can use this host info as well. Other network backed storage pool can also make use of this argument. nfspath -> source.path This is because other netfs pool can also make use of this argument. devices -> source.devices To differentiate source arguments from the target arguments, we move this argument under "source" key. The patch also adds unit test to check the generated XML is correct. v2 -> v3 test_storagepool.py: fix typo, reorganize import. v3 -> v4 Update API.json according to the changes in model.py. v4 -> v5 No changes. Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- Makefile.am | 1 + docs/API.md | 20 ++++++---- src/kimchi/API.json | 45 +++++++++++++++++++++++ src/kimchi/model.py | 39 +++++++++++--------- tests/Makefile.am | 1 + tests/test_storagepool.py | 93 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 173 insertions(+), 26 deletions(-) create mode 100644 tests/test_storagepool.py diff --git a/Makefile.am b/Makefile.am index 1fb3502..01efa1b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -55,6 +55,7 @@ PEP8_WHITELIST = \ tests/test_model.py \ tests/test_plugin.py \ tests/test_rest.py \ + tests/test_storagepool.py \ tests/utils.py \ $(NULL) diff --git a/docs/API.md b/docs/API.md index 9edc551..a8b9ea0 100644 --- a/docs/API.md +++ b/docs/API.md @@ -180,15 +180,19 @@ Represents a snapshot of the Virtual Machine's primary monitor. * **GET**: Retrieve a summarized list of all defined Storage Pools * **POST**: Create a new Storage Pool - * name: The name of the Storage Pool - * path: The path of the defined Storage Pool, + * name: The name of the Storage Pool. + * type: The type of the defined Storage Pool. + Supported types: 'dir', 'kimchi-iso', 'netfs', 'logical' + * path: The path of the defined Storage Pool. For 'kimchi-iso' pool refers to targeted deep scan path. - * type: The type of the defined Storage Pool, - Supported types: 'dir', 'kimchi-iso', 'netfs' - * nfsserver: IP or hostname of NFS server to create NFS pool. - * nfspath: export path on nfs server for NFS pool. - * devices: Array of devices to be used in the Storage Pool - Exclusive to the 'logical' storage pool type. + Pool types: 'dir', 'kimchi-iso'. + * source: Dictionary containing source information of the pool. + * host: IP or hostname of server for a pool backed from a remote host. + Pool types: 'netfs'. + * path: Export path on NFS server for NFS pool. + Pool types: 'netfs'. + * devices: Array of devices to be used in the Storage Pool + Pool types: 'logical'. ### Resource: Storage Pool diff --git a/src/kimchi/API.json b/src/kimchi/API.json index 7b90826..6519c21 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -4,6 +4,51 @@ "description": "Json schema for Kimchi API", "type": "object", "properties": { + "storagepools_create": { + "type": "object", + "properties": { + "name": { + "description": "The name of the Storage Pool", + "type": "string", + "minLength": 1, + "required": true + }, + "type": { + "description": "The type of the defined Storage Pool", + "type": "string", + "pattern": "^dir|netfs|logical$", + "required": true + }, + "path": { + "description": "The path of the defined Storage Pool", + "type": "string" + }, + "source": { + "description": "Dictionary containing source information of the pool", + "type": "object", + "properties": { + "host": { + "description": "IP or hostname of server for a pool backed from a remote host", + "type": "string" + }, + "path": { + "description": "Export path on NFS server for NFS pool", + "type": "string" + }, + "devices": { + "description": "Array of devices to be used in the Storage Pool", + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "description": "Full path of the block device node", + "type": "string" + } + } + } + } + } + }, "vms_create": { "type": "object", "properties": { diff --git a/src/kimchi/model.py b/src/kimchi/model.py index 269fc97..2b46121 100644 --- a/src/kimchi/model.py +++ b/src/kimchi/model.py @@ -1455,6 +1455,8 @@ class StoragePoolDef(object): ''' Subclasses have to override this method to actually generate the storage pool XML definition. Should cause no side effect and be idempotent''' + # TODO: When add new pool type, should also add the related test in + # tests/test_storagepool.py raise OperationFailed('self.xml is not implemented: %s' % self) @@ -1469,12 +1471,12 @@ class DirPoolDef(StoragePoolDef): # path: xml = """ <pool type='dir'> - <name>%(name)s</name> + <name>{name}</name> <target> - <path>%(path)s</path> + <path>{path}</path> </target> </pool> - """ % self.poolArgs + """.format(**self.poolArgs) return xml @@ -1494,22 +1496,22 @@ class NetfsPoolDef(StoragePoolDef): # Required parameters # name: # type: - # nfsserver: - # nfspath: + # source[host]: + # source[path]: poolArgs = copy.deepcopy(self.poolArgs) poolArgs['path'] = self.path xml = """ <pool type='netfs'> - <name>%(name)s</name> + <name>{name}</name> <source> - <host name='%(nfsserver)s'/> - <dir path='%(nfspath)s'/> + <host name='{source[host]}'/> + <dir path='{source[path]}'/> </source> <target> - <path>%(path)s</path> + <path>{path}</path> </target> </pool> - """ % poolArgs + """.format(**poolArgs) return xml @@ -1525,25 +1527,26 @@ class LogicalPoolDef(StoragePoolDef): # Required parameters # name: # type: - # devices: + # source[devices]: poolArgs = copy.deepcopy(self.poolArgs) devices = [] - for device_path in poolArgs['devices']: + for device_path in poolArgs['source']['devices']: devices.append('<device path="%s" />' % device_path) - poolArgs.update({'devices': ''.join(devices), - 'path': self.path}) + poolArgs['source']['devices'] = ''.join(devices) + poolArgs['path'] = self.path + xml = """ <pool type='logical'> - <name>%(name)s</name> + <name>{name}</name> <source> - %(devices)s + {source[devices]} </source> <target> - <path>%(path)s</path> + <path>{path}</path> </target> </pool> - """ % poolArgs + """.format(**poolArgs) return xml diff --git a/tests/Makefile.am b/tests/Makefile.am index 5fb00c0..55d3ab4 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -32,6 +32,7 @@ EXTRA_DIST = \ test_plugin.py \ test_rest.py \ test_server.py \ + test_storagepool.py \ test_vmtemplate.py \ utils.py \ $(NULL) diff --git a/tests/test_storagepool.py b/tests/test_storagepool.py new file mode 100644 index 0000000..1a7786f --- /dev/null +++ b/tests/test_storagepool.py @@ -0,0 +1,93 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Zhou Zheng Sheng <zhshzhou@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 + +import libxml2 +import unittest + + +import kimchi.model +from kimchi.rollbackcontext import RollbackContext + + +class storagepoolTests(unittest.TestCase): + def test_get_storagepool_xml(self): + poolDefs = [ + {'def': + {'type': 'dir', + 'name': 'unitTestDirPool', + 'path': '/var/temp/images'}, + 'xml': + """ + <pool type='dir'> + <name>unitTestDirPool</name> + <target> + <path>/var/temp/images</path> + </target> + </pool> + """}, + {'def': + {'type': 'netfs', + 'name': 'unitTestNFSPool', + 'source': {'host': '127.0.0.1', + 'path': '/var/export'}}, + 'xml': + """ + <pool type='netfs'> + <name>unitTestNFSPool</name> + <source> + <host name='127.0.0.1'/> + <dir path='/var/export'/> + </source> + <target> + <path>/var/lib/kimchi/nfs_mount/unitTestNFSPool</path> + </target> + </pool> + """}, + {'def': + {'type': 'logical', + 'name': 'unitTestLogicalPool', + 'source': {'devices': ['/dev/hda', '/dev/hdb']}}, + 'xml': + """ + <pool type='logical'> + <name>unitTestLogicalPool</name> + <source> + <device path="/dev/hda" /> + <device path="/dev/hdb" /> + </source> + <target> + <path>/var/lib/kimchi/logical_mount/unitTestLogicalPool</path> + </target> + </pool> + """}] + + for poolDef in poolDefs: + defObj = kimchi.model.StoragePoolDef.create(poolDef['def']) + xmlStr = defObj.xml + with RollbackContext() as rollback: + t1 = libxml2.readDoc(xmlStr, URL='', encoding='UTF-8', + options=libxml2.XML_PARSE_NOBLANKS) + rollback.prependDefer(t1.freeDoc) + t2 = libxml2.readDoc(poolDef['xml'], URL='', encoding='UTF-8', + options=libxml2.XML_PARSE_NOBLANKS) + rollback.prependDefer(t2.freeDoc) + self.assertEquals(t1.serialize(), t2.serialize()) -- 1.7.11.7

Reviewed-by: Aline Manera <alinefm@linux.vnet.ibm.com> On 01/02/2014 07:58 AM, Zhou Zheng Sheng wrote:
As we are adding support to new type of storage pool, the current naming scheme of the storage pool creating arguments should be rearranged to be more extendable. This patch renames some arguments and consolidates the argument of the same purposes as follow. This patch moves storage source related arguments under "source" key, while "source" itself is a dictionary.
nfsserver -> source.host This is because in future patches, iSCSI pool can use this host info as well. Other network backed storage pool can also make use of this argument.
nfspath -> source.path This is because other netfs pool can also make use of this argument.
devices -> source.devices To differentiate source arguments from the target arguments, we move this argument under "source" key.
The patch also adds unit test to check the generated XML is correct.
v2 -> v3 test_storagepool.py: fix typo, reorganize import.
v3 -> v4 Update API.json according to the changes in model.py.
v4 -> v5 No changes.
Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- Makefile.am | 1 + docs/API.md | 20 ++++++---- src/kimchi/API.json | 45 +++++++++++++++++++++++ src/kimchi/model.py | 39 +++++++++++--------- tests/Makefile.am | 1 + tests/test_storagepool.py | 93 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 173 insertions(+), 26 deletions(-) create mode 100644 tests/test_storagepool.py
diff --git a/Makefile.am b/Makefile.am index 1fb3502..01efa1b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -55,6 +55,7 @@ PEP8_WHITELIST = \ tests/test_model.py \ tests/test_plugin.py \ tests/test_rest.py \ + tests/test_storagepool.py \ tests/utils.py \ $(NULL)
diff --git a/docs/API.md b/docs/API.md index 9edc551..a8b9ea0 100644 --- a/docs/API.md +++ b/docs/API.md @@ -180,15 +180,19 @@ Represents a snapshot of the Virtual Machine's primary monitor.
* **GET**: Retrieve a summarized list of all defined Storage Pools * **POST**: Create a new Storage Pool - * name: The name of the Storage Pool - * path: The path of the defined Storage Pool, + * name: The name of the Storage Pool. + * type: The type of the defined Storage Pool. + Supported types: 'dir', 'kimchi-iso', 'netfs', 'logical' + * path: The path of the defined Storage Pool. For 'kimchi-iso' pool refers to targeted deep scan path. - * type: The type of the defined Storage Pool, - Supported types: 'dir', 'kimchi-iso', 'netfs' - * nfsserver: IP or hostname of NFS server to create NFS pool. - * nfspath: export path on nfs server for NFS pool. - * devices: Array of devices to be used in the Storage Pool - Exclusive to the 'logical' storage pool type. + Pool types: 'dir', 'kimchi-iso'. + * source: Dictionary containing source information of the pool. + * host: IP or hostname of server for a pool backed from a remote host. + Pool types: 'netfs'. + * path: Export path on NFS server for NFS pool. + Pool types: 'netfs'. + * devices: Array of devices to be used in the Storage Pool + Pool types: 'logical'.
### Resource: Storage Pool
diff --git a/src/kimchi/API.json b/src/kimchi/API.json index 7b90826..6519c21 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -4,6 +4,51 @@ "description": "Json schema for Kimchi API", "type": "object", "properties": { + "storagepools_create": { + "type": "object", + "properties": { + "name": { + "description": "The name of the Storage Pool", + "type": "string", + "minLength": 1, + "required": true + }, + "type": { + "description": "The type of the defined Storage Pool", + "type": "string", + "pattern": "^dir|netfs|logical$", + "required": true + }, + "path": { + "description": "The path of the defined Storage Pool", + "type": "string" + }, + "source": { + "description": "Dictionary containing source information of the pool", + "type": "object", + "properties": { + "host": { + "description": "IP or hostname of server for a pool backed from a remote host", + "type": "string" + }, + "path": { + "description": "Export path on NFS server for NFS pool", + "type": "string" + }, + "devices": { + "description": "Array of devices to be used in the Storage Pool", + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "description": "Full path of the block device node", + "type": "string" + } + } + } + } + } + }, "vms_create": { "type": "object", "properties": { diff --git a/src/kimchi/model.py b/src/kimchi/model.py index 269fc97..2b46121 100644 --- a/src/kimchi/model.py +++ b/src/kimchi/model.py @@ -1455,6 +1455,8 @@ class StoragePoolDef(object): ''' Subclasses have to override this method to actually generate the storage pool XML definition. Should cause no side effect and be idempotent''' + # TODO: When add new pool type, should also add the related test in + # tests/test_storagepool.py raise OperationFailed('self.xml is not implemented: %s' % self)
@@ -1469,12 +1471,12 @@ class DirPoolDef(StoragePoolDef): # path: xml = """ <pool type='dir'> - <name>%(name)s</name> + <name>{name}</name> <target> - <path>%(path)s</path> + <path>{path}</path> </target> </pool> - """ % self.poolArgs + """.format(**self.poolArgs) return xml
@@ -1494,22 +1496,22 @@ class NetfsPoolDef(StoragePoolDef): # Required parameters # name: # type: - # nfsserver: - # nfspath: + # source[host]: + # source[path]: poolArgs = copy.deepcopy(self.poolArgs) poolArgs['path'] = self.path xml = """ <pool type='netfs'> - <name>%(name)s</name> + <name>{name}</name> <source> - <host name='%(nfsserver)s'/> - <dir path='%(nfspath)s'/> + <host name='{source[host]}'/> + <dir path='{source[path]}'/> </source> <target> - <path>%(path)s</path> + <path>{path}</path> </target> </pool> - """ % poolArgs + """.format(**poolArgs) return xml
@@ -1525,25 +1527,26 @@ class LogicalPoolDef(StoragePoolDef): # Required parameters # name: # type: - # devices: + # source[devices]: poolArgs = copy.deepcopy(self.poolArgs) devices = [] - for device_path in poolArgs['devices']: + for device_path in poolArgs['source']['devices']: devices.append('<device path="%s" />' % device_path)
- poolArgs.update({'devices': ''.join(devices), - 'path': self.path}) + poolArgs['source']['devices'] = ''.join(devices) + poolArgs['path'] = self.path + xml = """ <pool type='logical'> - <name>%(name)s</name> + <name>{name}</name> <source> - %(devices)s + {source[devices]} </source> <target> - <path>%(path)s</path> + <path>{path}</path> </target> </pool> - """ % poolArgs + """.format(**poolArgs) return xml
diff --git a/tests/Makefile.am b/tests/Makefile.am index 5fb00c0..55d3ab4 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -32,6 +32,7 @@ EXTRA_DIST = \ test_plugin.py \ test_rest.py \ test_server.py \ + test_storagepool.py \ test_vmtemplate.py \ utils.py \ $(NULL) diff --git a/tests/test_storagepool.py b/tests/test_storagepool.py new file mode 100644 index 0000000..1a7786f --- /dev/null +++ b/tests/test_storagepool.py @@ -0,0 +1,93 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Zhou Zheng Sheng <zhshzhou@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 + +import libxml2 +import unittest + + +import kimchi.model +from kimchi.rollbackcontext import RollbackContext + + +class storagepoolTests(unittest.TestCase): + def test_get_storagepool_xml(self): + poolDefs = [ + {'def': + {'type': 'dir', + 'name': 'unitTestDirPool', + 'path': '/var/temp/images'}, + 'xml': + """ + <pool type='dir'> + <name>unitTestDirPool</name> + <target> + <path>/var/temp/images</path> + </target> + </pool> + """}, + {'def': + {'type': 'netfs', + 'name': 'unitTestNFSPool', + 'source': {'host': '127.0.0.1', + 'path': '/var/export'}}, + 'xml': + """ + <pool type='netfs'> + <name>unitTestNFSPool</name> + <source> + <host name='127.0.0.1'/> + <dir path='/var/export'/> + </source> + <target> + <path>/var/lib/kimchi/nfs_mount/unitTestNFSPool</path> + </target> + </pool> + """}, + {'def': + {'type': 'logical', + 'name': 'unitTestLogicalPool', + 'source': {'devices': ['/dev/hda', '/dev/hdb']}}, + 'xml': + """ + <pool type='logical'> + <name>unitTestLogicalPool</name> + <source> + <device path="/dev/hda" /> + <device path="/dev/hdb" /> + </source> + <target> + <path>/var/lib/kimchi/logical_mount/unitTestLogicalPool</path> + </target> + </pool> + """}] + + for poolDef in poolDefs: + defObj = kimchi.model.StoragePoolDef.create(poolDef['def']) + xmlStr = defObj.xml + with RollbackContext() as rollback: + t1 = libxml2.readDoc(xmlStr, URL='', encoding='UTF-8', + options=libxml2.XML_PARSE_NOBLANKS) + rollback.prependDefer(t1.freeDoc) + t2 = libxml2.readDoc(poolDef['xml'], URL='', encoding='UTF-8', + options=libxml2.XML_PARSE_NOBLANKS) + rollback.prependDefer(t2.freeDoc) + self.assertEquals(t1.serialize(), t2.serialize())

Reviewed-by: ShaoHe Feng <shaohef@linux.vnet.ibm.com> On 01/02/2014 05:58 PM, Zhou Zheng Sheng wrote:
As we are adding support to new type of storage pool, the current naming scheme of the storage pool creating arguments should be rearranged to be more extendable. This patch renames some arguments and consolidates the argument of the same purposes as follow. This patch moves storage source related arguments under "source" key, while "source" itself is a dictionary.
nfsserver -> source.host This is because in future patches, iSCSI pool can use this host info as well. Other network backed storage pool can also make use of this argument.
nfspath -> source.path This is because other netfs pool can also make use of this argument.
devices -> source.devices To differentiate source arguments from the target arguments, we move this argument under "source" key.
The patch also adds unit test to check the generated XML is correct.
v2 -> v3 test_storagepool.py: fix typo, reorganize import.
v3 -> v4 Update API.json according to the changes in model.py.
v4 -> v5 No changes.
Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- Makefile.am | 1 + docs/API.md | 20 ++++++---- src/kimchi/API.json | 45 +++++++++++++++++++++++ src/kimchi/model.py | 39 +++++++++++--------- tests/Makefile.am | 1 + tests/test_storagepool.py | 93 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 173 insertions(+), 26 deletions(-) create mode 100644 tests/test_storagepool.py
diff --git a/Makefile.am b/Makefile.am index 1fb3502..01efa1b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -55,6 +55,7 @@ PEP8_WHITELIST = \ tests/test_model.py \ tests/test_plugin.py \ tests/test_rest.py \ + tests/test_storagepool.py \ tests/utils.py \ $(NULL)
diff --git a/docs/API.md b/docs/API.md index 9edc551..a8b9ea0 100644 --- a/docs/API.md +++ b/docs/API.md @@ -180,15 +180,19 @@ Represents a snapshot of the Virtual Machine's primary monitor.
* **GET**: Retrieve a summarized list of all defined Storage Pools * **POST**: Create a new Storage Pool - * name: The name of the Storage Pool - * path: The path of the defined Storage Pool, + * name: The name of the Storage Pool. + * type: The type of the defined Storage Pool. + Supported types: 'dir', 'kimchi-iso', 'netfs', 'logical' + * path: The path of the defined Storage Pool. For 'kimchi-iso' pool refers to targeted deep scan path. - * type: The type of the defined Storage Pool, - Supported types: 'dir', 'kimchi-iso', 'netfs' - * nfsserver: IP or hostname of NFS server to create NFS pool. - * nfspath: export path on nfs server for NFS pool. - * devices: Array of devices to be used in the Storage Pool - Exclusive to the 'logical' storage pool type. + Pool types: 'dir', 'kimchi-iso'. + * source: Dictionary containing source information of the pool. + * host: IP or hostname of server for a pool backed from a remote host. + Pool types: 'netfs'. + * path: Export path on NFS server for NFS pool. + Pool types: 'netfs'. + * devices: Array of devices to be used in the Storage Pool + Pool types: 'logical'.
### Resource: Storage Pool
diff --git a/src/kimchi/API.json b/src/kimchi/API.json index 7b90826..6519c21 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -4,6 +4,51 @@ "description": "Json schema for Kimchi API", "type": "object", "properties": { + "storagepools_create": { + "type": "object", + "properties": { + "name": { + "description": "The name of the Storage Pool", + "type": "string", + "minLength": 1, + "required": true + }, + "type": { + "description": "The type of the defined Storage Pool", + "type": "string", + "pattern": "^dir|netfs|logical$", + "required": true + }, + "path": { + "description": "The path of the defined Storage Pool", + "type": "string" + }, + "source": { + "description": "Dictionary containing source information of the pool", + "type": "object", + "properties": { + "host": { + "description": "IP or hostname of server for a pool backed from a remote host", + "type": "string" + }, + "path": { + "description": "Export path on NFS server for NFS pool", + "type": "string" + }, + "devices": { + "description": "Array of devices to be used in the Storage Pool", + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "description": "Full path of the block device node", + "type": "string" + } + } + } + } + } + }, "vms_create": { "type": "object", "properties": { diff --git a/src/kimchi/model.py b/src/kimchi/model.py index 269fc97..2b46121 100644 --- a/src/kimchi/model.py +++ b/src/kimchi/model.py @@ -1455,6 +1455,8 @@ class StoragePoolDef(object): ''' Subclasses have to override this method to actually generate the storage pool XML definition. Should cause no side effect and be idempotent''' + # TODO: When add new pool type, should also add the related test in + # tests/test_storagepool.py raise OperationFailed('self.xml is not implemented: %s' % self)
@@ -1469,12 +1471,12 @@ class DirPoolDef(StoragePoolDef): # path: xml = """ <pool type='dir'> - <name>%(name)s</name> + <name>{name}</name> <target> - <path>%(path)s</path> + <path>{path}</path> </target> </pool> - """ % self.poolArgs + """.format(**self.poolArgs) return xml
@@ -1494,22 +1496,22 @@ class NetfsPoolDef(StoragePoolDef): # Required parameters # name: # type: - # nfsserver: - # nfspath: + # source[host]: + # source[path]: poolArgs = copy.deepcopy(self.poolArgs) poolArgs['path'] = self.path xml = """ <pool type='netfs'> - <name>%(name)s</name> + <name>{name}</name> <source> - <host name='%(nfsserver)s'/> - <dir path='%(nfspath)s'/> + <host name='{source[host]}'/> + <dir path='{source[path]}'/> </source> <target> - <path>%(path)s</path> + <path>{path}</path> </target> </pool> - """ % poolArgs + """.format(**poolArgs) return xml
@@ -1525,25 +1527,26 @@ class LogicalPoolDef(StoragePoolDef): # Required parameters # name: # type: - # devices: + # source[devices]: poolArgs = copy.deepcopy(self.poolArgs) devices = [] - for device_path in poolArgs['devices']: + for device_path in poolArgs['source']['devices']: devices.append('<device path="%s" />' % device_path)
- poolArgs.update({'devices': ''.join(devices), - 'path': self.path}) + poolArgs['source']['devices'] = ''.join(devices) + poolArgs['path'] = self.path + xml = """ <pool type='logical'> - <name>%(name)s</name> + <name>{name}</name> <source> - %(devices)s + {source[devices]} </source> <target> - <path>%(path)s</path> + <path>{path}</path> </target> </pool> - """ % poolArgs + """.format(**poolArgs) return xml
diff --git a/tests/Makefile.am b/tests/Makefile.am index 5fb00c0..55d3ab4 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -32,6 +32,7 @@ EXTRA_DIST = \ test_plugin.py \ test_rest.py \ test_server.py \ + test_storagepool.py \ test_vmtemplate.py \ utils.py \ $(NULL) diff --git a/tests/test_storagepool.py b/tests/test_storagepool.py new file mode 100644 index 0000000..1a7786f --- /dev/null +++ b/tests/test_storagepool.py @@ -0,0 +1,93 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Zhou Zheng Sheng <zhshzhou@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 + +import libxml2 +import unittest + + +import kimchi.model +from kimchi.rollbackcontext import RollbackContext + + +class storagepoolTests(unittest.TestCase): + def test_get_storagepool_xml(self): + poolDefs = [ + {'def': + {'type': 'dir', + 'name': 'unitTestDirPool', + 'path': '/var/temp/images'}, + 'xml': + """ + <pool type='dir'> + <name>unitTestDirPool</name> + <target> + <path>/var/temp/images</path> + </target> + </pool> + """}, + {'def': + {'type': 'netfs', + 'name': 'unitTestNFSPool', + 'source': {'host': '127.0.0.1', + 'path': '/var/export'}}, + 'xml': + """ + <pool type='netfs'> + <name>unitTestNFSPool</name> + <source> + <host name='127.0.0.1'/> + <dir path='/var/export'/> + </source> + <target> + <path>/var/lib/kimchi/nfs_mount/unitTestNFSPool</path> + </target> + </pool> + """}, + {'def': + {'type': 'logical', + 'name': 'unitTestLogicalPool', + 'source': {'devices': ['/dev/hda', '/dev/hdb']}}, + 'xml': + """ + <pool type='logical'> + <name>unitTestLogicalPool</name> + <source> + <device path="/dev/hda" /> + <device path="/dev/hdb" /> + </source> + <target> + <path>/var/lib/kimchi/logical_mount/unitTestLogicalPool</path> + </target> + </pool> + """}] + + for poolDef in poolDefs: + defObj = kimchi.model.StoragePoolDef.create(poolDef['def']) + xmlStr = defObj.xml + with RollbackContext() as rollback: + t1 = libxml2.readDoc(xmlStr, URL='', encoding='UTF-8', + options=libxml2.XML_PARSE_NOBLANKS) + rollback.prependDefer(t1.freeDoc) + t2 = libxml2.readDoc(poolDef['xml'], URL='', encoding='UTF-8', + options=libxml2.XML_PARSE_NOBLANKS) + rollback.prependDefer(t2.freeDoc) + self.assertEquals(t1.serialize(), t2.serialize())
-- Thanks and best regards! Sheldon Feng(冯少合)<shaohef@linux.vnet.ibm.com> IBM Linux Technology Center

Reviewed-by: Royce Lv<lvroyce@linux.vnet.ibm.com> On 2014年01月02日 17:58, Zhou Zheng Sheng wrote:
As we are adding support to new type of storage pool, the current naming scheme of the storage pool creating arguments should be rearranged to be more extendable. This patch renames some arguments and consolidates the argument of the same purposes as follow. This patch moves storage source related arguments under "source" key, while "source" itself is a dictionary.
nfsserver -> source.host This is because in future patches, iSCSI pool can use this host info as well. Other network backed storage pool can also make use of this argument.
nfspath -> source.path This is because other netfs pool can also make use of this argument.
devices -> source.devices To differentiate source arguments from the target arguments, we move this argument under "source" key.
The patch also adds unit test to check the generated XML is correct.
v2 -> v3 test_storagepool.py: fix typo, reorganize import.
v3 -> v4 Update API.json according to the changes in model.py.
v4 -> v5 No changes.
Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- Makefile.am | 1 + docs/API.md | 20 ++++++---- src/kimchi/API.json | 45 +++++++++++++++++++++++ src/kimchi/model.py | 39 +++++++++++--------- tests/Makefile.am | 1 + tests/test_storagepool.py | 93 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 173 insertions(+), 26 deletions(-) create mode 100644 tests/test_storagepool.py
diff --git a/Makefile.am b/Makefile.am index 1fb3502..01efa1b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -55,6 +55,7 @@ PEP8_WHITELIST = \ tests/test_model.py \ tests/test_plugin.py \ tests/test_rest.py \ + tests/test_storagepool.py \ tests/utils.py \ $(NULL)
diff --git a/docs/API.md b/docs/API.md index 9edc551..a8b9ea0 100644 --- a/docs/API.md +++ b/docs/API.md @@ -180,15 +180,19 @@ Represents a snapshot of the Virtual Machine's primary monitor.
* **GET**: Retrieve a summarized list of all defined Storage Pools * **POST**: Create a new Storage Pool - * name: The name of the Storage Pool - * path: The path of the defined Storage Pool, + * name: The name of the Storage Pool. + * type: The type of the defined Storage Pool. + Supported types: 'dir', 'kimchi-iso', 'netfs', 'logical' + * path: The path of the defined Storage Pool. For 'kimchi-iso' pool refers to targeted deep scan path. - * type: The type of the defined Storage Pool, - Supported types: 'dir', 'kimchi-iso', 'netfs' - * nfsserver: IP or hostname of NFS server to create NFS pool. - * nfspath: export path on nfs server for NFS pool. - * devices: Array of devices to be used in the Storage Pool - Exclusive to the 'logical' storage pool type. + Pool types: 'dir', 'kimchi-iso'. + * source: Dictionary containing source information of the pool. + * host: IP or hostname of server for a pool backed from a remote host. + Pool types: 'netfs'. + * path: Export path on NFS server for NFS pool. + Pool types: 'netfs'. + * devices: Array of devices to be used in the Storage Pool + Pool types: 'logical'.
### Resource: Storage Pool
diff --git a/src/kimchi/API.json b/src/kimchi/API.json index 7b90826..6519c21 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -4,6 +4,51 @@ "description": "Json schema for Kimchi API", "type": "object", "properties": { + "storagepools_create": { + "type": "object", + "properties": { + "name": { + "description": "The name of the Storage Pool", + "type": "string", + "minLength": 1, + "required": true + }, + "type": { + "description": "The type of the defined Storage Pool", + "type": "string", + "pattern": "^dir|netfs|logical$", + "required": true + }, + "path": { + "description": "The path of the defined Storage Pool", + "type": "string" + }, + "source": { + "description": "Dictionary containing source information of the pool", + "type": "object", + "properties": { + "host": { + "description": "IP or hostname of server for a pool backed from a remote host", + "type": "string" + }, + "path": { + "description": "Export path on NFS server for NFS pool", + "type": "string" + }, + "devices": { + "description": "Array of devices to be used in the Storage Pool", + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "description": "Full path of the block device node", + "type": "string" + } + } + } + } + } + }, "vms_create": { "type": "object", "properties": { diff --git a/src/kimchi/model.py b/src/kimchi/model.py index 269fc97..2b46121 100644 --- a/src/kimchi/model.py +++ b/src/kimchi/model.py @@ -1455,6 +1455,8 @@ class StoragePoolDef(object): ''' Subclasses have to override this method to actually generate the storage pool XML definition. Should cause no side effect and be idempotent''' + # TODO: When add new pool type, should also add the related test in + # tests/test_storagepool.py raise OperationFailed('self.xml is not implemented: %s' % self)
@@ -1469,12 +1471,12 @@ class DirPoolDef(StoragePoolDef): # path: xml = """ <pool type='dir'> - <name>%(name)s</name> + <name>{name}</name> <target> - <path>%(path)s</path> + <path>{path}</path> </target> </pool> - """ % self.poolArgs + """.format(**self.poolArgs) return xml
@@ -1494,22 +1496,22 @@ class NetfsPoolDef(StoragePoolDef): # Required parameters # name: # type: - # nfsserver: - # nfspath: + # source[host]: + # source[path]: poolArgs = copy.deepcopy(self.poolArgs) poolArgs['path'] = self.path xml = """ <pool type='netfs'> - <name>%(name)s</name> + <name>{name}</name> <source> - <host name='%(nfsserver)s'/> - <dir path='%(nfspath)s'/> + <host name='{source[host]}'/> + <dir path='{source[path]}'/> </source> <target> - <path>%(path)s</path> + <path>{path}</path> </target> </pool> - """ % poolArgs + """.format(**poolArgs) return xml
@@ -1525,25 +1527,26 @@ class LogicalPoolDef(StoragePoolDef): # Required parameters # name: # type: - # devices: + # source[devices]: poolArgs = copy.deepcopy(self.poolArgs) devices = [] - for device_path in poolArgs['devices']: + for device_path in poolArgs['source']['devices']: devices.append('<device path="%s" />' % device_path)
- poolArgs.update({'devices': ''.join(devices), - 'path': self.path}) + poolArgs['source']['devices'] = ''.join(devices) + poolArgs['path'] = self.path + xml = """ <pool type='logical'> - <name>%(name)s</name> + <name>{name}</name> <source> - %(devices)s + {source[devices]} </source> <target> - <path>%(path)s</path> + <path>{path}</path> </target> </pool> - """ % poolArgs + """.format(**poolArgs) return xml
diff --git a/tests/Makefile.am b/tests/Makefile.am index 5fb00c0..55d3ab4 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -32,6 +32,7 @@ EXTRA_DIST = \ test_plugin.py \ test_rest.py \ test_server.py \ + test_storagepool.py \ test_vmtemplate.py \ utils.py \ $(NULL) diff --git a/tests/test_storagepool.py b/tests/test_storagepool.py new file mode 100644 index 0000000..1a7786f --- /dev/null +++ b/tests/test_storagepool.py @@ -0,0 +1,93 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Zhou Zheng Sheng <zhshzhou@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 + +import libxml2 +import unittest + + +import kimchi.model +from kimchi.rollbackcontext import RollbackContext + + +class storagepoolTests(unittest.TestCase): + def test_get_storagepool_xml(self): + poolDefs = [ + {'def': + {'type': 'dir', + 'name': 'unitTestDirPool', + 'path': '/var/temp/images'}, + 'xml': + """ + <pool type='dir'> + <name>unitTestDirPool</name> + <target> + <path>/var/temp/images</path> + </target> + </pool> + """}, + {'def': + {'type': 'netfs', + 'name': 'unitTestNFSPool', + 'source': {'host': '127.0.0.1', + 'path': '/var/export'}}, + 'xml': + """ + <pool type='netfs'> + <name>unitTestNFSPool</name> + <source> + <host name='127.0.0.1'/> + <dir path='/var/export'/> + </source> + <target> + <path>/var/lib/kimchi/nfs_mount/unitTestNFSPool</path> + </target> + </pool> + """}, + {'def': + {'type': 'logical', + 'name': 'unitTestLogicalPool', + 'source': {'devices': ['/dev/hda', '/dev/hdb']}}, + 'xml': + """ + <pool type='logical'> + <name>unitTestLogicalPool</name> + <source> + <device path="/dev/hda" /> + <device path="/dev/hdb" /> + </source> + <target> + <path>/var/lib/kimchi/logical_mount/unitTestLogicalPool</path> + </target> + </pool> + """}] + + for poolDef in poolDefs: + defObj = kimchi.model.StoragePoolDef.create(poolDef['def']) + xmlStr = defObj.xml + with RollbackContext() as rollback: + t1 = libxml2.readDoc(xmlStr, URL='', encoding='UTF-8', + options=libxml2.XML_PARSE_NOBLANKS) + rollback.prependDefer(t1.freeDoc) + t2 = libxml2.readDoc(poolDef['xml'], URL='', encoding='UTF-8', + options=libxml2.XML_PARSE_NOBLANKS) + rollback.prependDefer(t2.freeDoc) + self.assertEquals(t1.serialize(), t2.serialize())

As we are adding support to new type of storage pool, the current naming scheme of the storage pool creating arguments should be rearranged to be more extendable. This patch renames some arguments and consolidates the argument of the same purposes as follow. nfsserver -> source.host nfspath -> source.path devices -> source.devices This patch contains the front end changes of the consolidation work. It just changes the formData transferred to kimchi backend, and leaves the templates and other related scripts untouched. v2 -> v5 No changes Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- ui/js/src/kimchi.storagepool_add_main.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/ui/js/src/kimchi.storagepool_add_main.js b/ui/js/src/kimchi.storagepool_add_main.js index b31610a..a154933 100644 --- a/ui/js/src/kimchi.storagepool_add_main.js +++ b/ui/js/src/kimchi.storagepool_add_main.js @@ -148,14 +148,21 @@ kimchi.addPool = function(event) { if (poolType === 'dir') { formData.path = $('#pathId').val(); } else if (poolType === 'logical') { + var source = {}; if (!$.isArray(formData.devices)) { var deviceObj = []; deviceObj[0] = formData.devices; - formData.devices = deviceObj; + source.devices = deviceObj; + } else { + source.devices = formData.devices; } + delete formData.devices; + formData.source = source; } else { - formData.nfspath = $('#nfspathId').val(); - formData.nfsserver = $('#nfsserverId').val(); + var source = {}; + source.path = $('#nfspathId').val(); + source.host = $('#nfsserverId').val(); + formData.source = source; } if (poolType === 'logical') { var settings = { -- 1.7.11.7

Reviewed-by: Aline Manera <alinefm@linux.vnet.ibm.com> On 01/02/2014 07:58 AM, Zhou Zheng Sheng wrote:
As we are adding support to new type of storage pool, the current naming scheme of the storage pool creating arguments should be rearranged to be more extendable. This patch renames some arguments and consolidates the argument of the same purposes as follow.
nfsserver -> source.host nfspath -> source.path devices -> source.devices
This patch contains the front end changes of the consolidation work. It just changes the formData transferred to kimchi backend, and leaves the templates and other related scripts untouched.
v2 -> v5 No changes
Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- ui/js/src/kimchi.storagepool_add_main.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/ui/js/src/kimchi.storagepool_add_main.js b/ui/js/src/kimchi.storagepool_add_main.js index b31610a..a154933 100644 --- a/ui/js/src/kimchi.storagepool_add_main.js +++ b/ui/js/src/kimchi.storagepool_add_main.js @@ -148,14 +148,21 @@ kimchi.addPool = function(event) { if (poolType === 'dir') { formData.path = $('#pathId').val(); } else if (poolType === 'logical') { + var source = {}; if (!$.isArray(formData.devices)) { var deviceObj = []; deviceObj[0] = formData.devices; - formData.devices = deviceObj; + source.devices = deviceObj; + } else { + source.devices = formData.devices; } + delete formData.devices; + formData.source = source; } else { - formData.nfspath = $('#nfspathId').val(); - formData.nfsserver = $('#nfsserverId').val(); + var source = {}; + source.path = $('#nfspathId').val(); + source.host = $('#nfsserverId').val(); + formData.source = source; } if (poolType === 'logical') { var settings = {

Reviewed-by: ShaoHe Feng <shaohef@linux.vnet.ibm.com> On 01/02/2014 05:58 PM, Zhou Zheng Sheng wrote:
As we are adding support to new type of storage pool, the current naming scheme of the storage pool creating arguments should be rearranged to be more extendable. This patch renames some arguments and consolidates the argument of the same purposes as follow.
nfsserver -> source.host nfspath -> source.path devices -> source.devices
This patch contains the front end changes of the consolidation work. It just changes the formData transferred to kimchi backend, and leaves the templates and other related scripts untouched.
v2 -> v5 No changes
Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- ui/js/src/kimchi.storagepool_add_main.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/ui/js/src/kimchi.storagepool_add_main.js b/ui/js/src/kimchi.storagepool_add_main.js index b31610a..a154933 100644 --- a/ui/js/src/kimchi.storagepool_add_main.js +++ b/ui/js/src/kimchi.storagepool_add_main.js @@ -148,14 +148,21 @@ kimchi.addPool = function(event) { if (poolType === 'dir') { formData.path = $('#pathId').val(); } else if (poolType === 'logical') { + var source = {}; if (!$.isArray(formData.devices)) { var deviceObj = []; deviceObj[0] = formData.devices; - formData.devices = deviceObj; + source.devices = deviceObj; + } else { + source.devices = formData.devices; } + delete formData.devices; + formData.source = source; } else { - formData.nfspath = $('#nfspathId').val(); - formData.nfsserver = $('#nfsserverId').val(); + var source = {}; + source.path = $('#nfspathId').val(); + source.host = $('#nfsserverId').val(); + formData.source = source; } if (poolType === 'logical') { var settings = {
-- Thanks and best regards! Sheldon Feng(冯少合)<shaohef@linux.vnet.ibm.com> IBM Linux Technology Center

This patch implements creating iSCSI storagepool for libvirt. Each LUN in iSCSI storagepool can be used as a volume, but iSCSI storagepool does not provide ability to create volume. For now in kimchi we create volume for each newly created VM. Next is to implement attaching existing volume to a new VM. This patch also adds validation for the iSCSI back-end. It tries to login the target using the auth before actually creating the pool. On RedHat family distributions, libiscsi comes with a Python binding, but not on other distributions. So in this patch it invokes iscsiadm to check iSCSI target. As regard to iSCSI CHAP authentication, it's somewhat broken before libvirt 1.1.1. It was reworked in libvirt commit eb0d79, and this patch goes with libvirt 1.1.1, but it doesn't require libvirt in the spec file. This is because libvirt 1.1.1 is not available on Ubuntu 12.04 LTS. The user can just ignore this feature. How to test it manually Prerequisite An running iSCSI target on the network. Create: curl -u root -H 'Content-type: application/json' \ -H 'Accept: application/json' \ -d '{"source": { "target": "iqn.YouriSCSITargetIQN.XXX", "host": "10.0.0.X"}, "type": "iscsi", "name": "iscsipool"}' \ http://127.0.0.1:8000/storagepools Show Info: curl -u root -H 'Accept: application/json' \ http://127.0.0.1:8000/storagepools/iscsipool Activate: curl -u root -H 'Content-type: application/json' \ -H 'Accept: application/json' \ -d '' http://127.0.0.1:8000/storagepools/iscsipool/activate Examine: iscsiadm -m session Deactivate: curl -u root -H 'Content-type: application/json' \ -H 'Accept: application/json' \ -d '' http://127.0.0.1:8000/storagepools/iscsipool/deactivate Delete: curl -u root -X DELETE -H 'Accept: application/json' \ http://127.0.0.1:8000/storagepools/iscsipool v2 -> v3 1) Support CHAP authentication. 2) Validate iSCSI target before creating the pool. 3) Add XML generation unit test with "port" and "auth". v3 -> v4 1) Update RPM spec file and Debian control file. 2) Update API.json. 3) Give a proper name for the iSCSI target client class, TargetClient. v4 -> v5 Fix description string of 'auth' in API.json. Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- Makefile.am | 1 + contrib/DEBIAN/control.in | 3 +- contrib/kimchi.spec.fedora.in | 1 + contrib/kimchi.spec.suse.in | 1 + docs/API.md | 12 +++++- src/kimchi/API.json | 24 ++++++++++++ src/kimchi/Makefile.am | 1 + src/kimchi/iscsi.py | 89 +++++++++++++++++++++++++++++++++++++++++++ src/kimchi/model.py | 89 +++++++++++++++++++++++++++++++++++++++++-- tests/test_storagepool.py | 63 ++++++++++++++++++++++++++++++ 10 files changed, 278 insertions(+), 6 deletions(-) create mode 100644 src/kimchi/iscsi.py diff --git a/Makefile.am b/Makefile.am index 01efa1b..b01a8e7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -47,6 +47,7 @@ PEP8_WHITELIST = \ src/kimchi/config.py.in \ src/kimchi/disks.py \ src/kimchi/featuretests.py \ + src/kimchi/iscsi.py \ src/kimchi/rollbackcontext.py \ src/kimchi/root.py \ src/kimchi/server.py \ diff --git a/contrib/DEBIAN/control.in b/contrib/DEBIAN/control.in index 380584c..eecfb27 100644 --- a/contrib/DEBIAN/control.in +++ b/contrib/DEBIAN/control.in @@ -17,7 +17,8 @@ Depends: python-cherrypy3 (>= 3.2.0), python-psutil (>= 0.6.0), python-ethtool, sosreport, - python-ipaddr + python-ipaddr, + open-iscsi Build-Depends: Maintainer: Aline Manera <alinefm@br.ibm.com> Description: Kimchi web server diff --git a/contrib/kimchi.spec.fedora.in b/contrib/kimchi.spec.fedora.in index 3044fc8..75435b3 100644 --- a/contrib/kimchi.spec.fedora.in +++ b/contrib/kimchi.spec.fedora.in @@ -25,6 +25,7 @@ Requires: python-ethtool Requires: sos Requires: python-ipaddr Requires: nfs-utils +Requires: iscsi-initiator-utils %if 0%{?rhel} == 6 Requires: python-ordereddict diff --git a/contrib/kimchi.spec.suse.in b/contrib/kimchi.spec.suse.in index 190b2be..bcfb6db 100644 --- a/contrib/kimchi.spec.suse.in +++ b/contrib/kimchi.spec.suse.in @@ -20,6 +20,7 @@ Requires: python-jsonschema >= 1.3.0 Requires: python-ethtool Requires: python-ipaddr Requires: nfs-client +Requires: iscsi-initiator-utils %if 0%{?sles_version} == 11 Requires: python-ordereddict diff --git a/docs/API.md b/docs/API.md index a8b9ea0..c347340 100644 --- a/docs/API.md +++ b/docs/API.md @@ -182,17 +182,25 @@ Represents a snapshot of the Virtual Machine's primary monitor. * **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' + Supported types: 'dir', 'kimchi-iso', 'netfs', 'logical', 'iscsi' * path: The path of the defined Storage Pool. For 'kimchi-iso' pool refers to targeted deep scan path. Pool types: 'dir', 'kimchi-iso'. * source: Dictionary containing source information of the pool. * host: IP or hostname of server for a pool backed from a remote host. - Pool types: 'netfs'. + Pool types: 'netfs', 'iscsi'. * path: Export path on NFS server for NFS pool. Pool types: 'netfs'. * devices: Array of devices to be used in the Storage Pool Pool types: 'logical'. + * target: Target IQN of an iSCSI pool. + Pool types: 'iscsi'. + * port *(optional)*: Listening port of a remote storage server. + Pool types: 'iscsi'. + * auth *(optional)*: Storage back-end authentication information. + Pool types: 'iscsi'. + * username: Login username of the iSCSI target. + * password: Login password of the iSCSI target. ### Resource: Storage Pool diff --git a/src/kimchi/API.json b/src/kimchi/API.json index 6519c21..6f1dd03 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -44,6 +44,30 @@ "description": "Full path of the block device node", "type": "string" } + }, + "target": { + "description": "Target IQN of an iSCSI pool", + "type": "string" + }, + "port": { + "description": "Listening port of a remote storage server", + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "auth": { + "description": "Storage back-end authentication information", + "type": "object", + "properties": { + "username": { + "description": "Login username of the iSCSI target", + "type": "string" + }, + "password": { + "description": "Login password of the iSCSI target", + "type": "string" + } + } } } } diff --git a/src/kimchi/Makefile.am b/src/kimchi/Makefile.am index 88ccbf7..261c331 100644 --- a/src/kimchi/Makefile.am +++ b/src/kimchi/Makefile.am @@ -31,6 +31,7 @@ kimchi_PYTHON = \ distroloader.py \ exception.py \ featuretests.py \ + iscsi.py \ isoinfo.py \ mockmodel.py \ model.py \ diff --git a/src/kimchi/iscsi.py b/src/kimchi/iscsi.py new file mode 100644 index 0000000..35c0b8a --- /dev/null +++ b/src/kimchi/iscsi.py @@ -0,0 +1,89 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Zhou Zheng Sheng <zhshzhou@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-1301USA + +import subprocess + + +from kimchi.exception import OperationFailed + + +class TargetClient(object): + def __init__(self, target, host, port=None, auth=None): + self.portal = host + ("" if port is None else ":%s" % port) + self.target = target + self.auth = auth + self.targetCmd = ['iscsiadm', '--mode', 'node', '--targetname', + self.target, '--portal', self.portal] + + def _update_db(self, Name, Value): + self._run_cmd(['--op=update', '--name', Name, '--value', Value]) + + def _update_auth(self): + if self.auth is None: + items = (('node.session.auth.authmethod', 'None'), + ('node.session.auth.username', ''), + ('node.session.auth.password', '')) + else: + items = (('node.session.auth.authmethod', 'CHAP'), + ('node.session.auth.username', self.auth['username']), + ('node.session.auth.password', self.auth['password'])) + for name, value in items: + self._update_db(name, value) + + def _run_cmd(self, cmd): + iscsiadm = subprocess.Popen( + self.targetCmd + cmd, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = iscsiadm.communicate() + if iscsiadm.returncode != 0: + raise OperationFailed('Error executing iscsiadm: %s' % err) + return out + + def _discover(self): + iscsiadm = subprocess.Popen( + ['iscsiadm', '--mode', 'discovery', '--type', 'sendtargets', + '--portal', self.portal], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = iscsiadm.communicate() + if iscsiadm.returncode != 0: + raise OperationFailed('Error executing iscsiadm: %s' % err) + return out + + def _run_op(self, op): + self._run_cmd(['--' + op]) + + def login(self): + self._discover() + self._update_auth() + self._run_op('login') + + def logout(self): + self._run_op('logout') + + def validate(self): + try: + self.login() + except OperationFailed: + return False + + self.logout() + return True diff --git a/src/kimchi/model.py b/src/kimchi/model.py index 2b46121..17d5485 100644 --- a/src/kimchi/model.py +++ b/src/kimchi/model.py @@ -65,6 +65,7 @@ from kimchi.distroloader import DistroLoader from kimchi.exception import InvalidOperation, InvalidParameter, MissingParameter from kimchi.exception import NotFoundError, OperationFailed from kimchi.featuretests import FeatureTests +from kimchi.iscsi import TargetClient from kimchi.networkxml import to_network_xml from kimchi.objectstore import ObjectStore from kimchi.scan import Scanner @@ -995,7 +996,7 @@ class Model(object): if params['type'] == 'kimchi-iso': task_id = self._do_deep_scan(params) poolDef = StoragePoolDef.create(params) - poolDef.prepare() + poolDef.prepare(conn) xml = poolDef.xml except KeyError, key: raise MissingParameter(key) @@ -1444,7 +1445,7 @@ class StoragePoolDef(object): def __init__(self, poolArgs): self.poolArgs = poolArgs - def prepare(self): + def prepare(self, conn): ''' Validate pool arguments and perform preparations. Operation which would cause side effect should be put here. Subclasses can optionally override this method, or it always succeeds by default. ''' @@ -1487,7 +1488,7 @@ class NetfsPoolDef(StoragePoolDef): super(NetfsPoolDef, self).__init__(poolArgs) self.path = '/var/lib/kimchi/nfs_mount/' + self.poolArgs['name'] - def prepare(self): + def prepare(self, conn): # TODO: Verify the NFS export can be actually mounted. pass @@ -1550,6 +1551,88 @@ class LogicalPoolDef(StoragePoolDef): return xml +class IscsiPoolDef(StoragePoolDef): + poolType = 'iscsi' + + def prepare(self, conn): + source = self.poolArgs['source'] + if not TargetClient(**source).validate(): + raise OperationFailed("Can not login to iSCSI host %s target %s" % + (source['host'], source['target'])) + self._prepare_auth(conn) + + def _prepare_auth(self, conn): + try: + auth = self.poolArgs['source']['auth'] + except KeyError: + return + + try: + virSecret = conn.secretLookupByUsage( + libvirt.VIR_SECRET_USAGE_TYPE_ISCSI, self.poolArgs['name']) + except libvirt.libvirtError: + xml = ''' + <secret ephemeral='no' private='yes'> + <description>Secret for iSCSI storage pool {name}</description> + <auth type='chap' username='{username}'/> + <usage type='iscsi'> + <target>{name}</target> + </usage> + </secret>'''.format(name=self.poolArgs['name'], + username=auth['username']) + virSecret = conn.secretDefineXML(xml) + + virSecret.setValue(auth['password']) + + def _format_port(self, poolArgs): + try: + port = poolArgs['source']['port'] + except KeyError: + return "" + return "port='%s'" % port + + def _format_auth(self, poolArgs): + try: + auth = poolArgs['source']['auth'] + except KeyError: + return "" + + return ''' + <auth type='chap' username='{username}'> + <secret type='iscsi' usage='{name}'/> + </auth>'''.format(name=poolArgs['name'], username=auth['username']) + + @property + def xml(self): + # Required parameters + # name: + # type: + # source[host]: + # source[target]: + # + # Optional parameters + # source[port]: + poolArgs = copy.deepcopy(self.poolArgs) + poolArgs['source'].update({'port': self._format_port(poolArgs), + 'auth': self._format_auth(poolArgs)}) + poolArgs['path'] = '/dev/disk/by-id' + + xml = """ + <pool type='iscsi'> + <name>{name}</name> + <source> + <host name='{source[host]}' {source[port]}/> + <device path='{source[target]}'/> + {source[auth]} + </source> + <target> + <path>{path}</path> + </target> + </pool> + """.format(**poolArgs) + return xml + + def _get_volume_xml(**kwargs): # Required parameters # name: diff --git a/tests/test_storagepool.py b/tests/test_storagepool.py index 1a7786f..8341537 100644 --- a/tests/test_storagepool.py +++ b/tests/test_storagepool.py @@ -78,6 +78,69 @@ class storagepoolTests(unittest.TestCase): <path>/var/lib/kimchi/logical_mount/unitTestLogicalPool</path> </target> </pool> + """}, + {'def': + {'type': 'iscsi', + 'name': 'unitTestISCSIPool', + 'source': { + 'host': '127.0.0.1', + 'target': 'iqn.2003-01.org.linux-iscsi.localhost'}}, + 'xml': + """ + <pool type='iscsi'> + <name>unitTestISCSIPool</name> + <source> + <host name='127.0.0.1' /> + <device path='iqn.2003-01.org.linux-iscsi.localhost'/> + </source> + <target> + <path>/dev/disk/by-id</path> + </target> + </pool> + """}, + {'def': + {'type': 'iscsi', + 'name': 'unitTestISCSIPoolPort', + 'source': { + 'host': '127.0.0.1', + 'port': 3266, + 'target': 'iqn.2003-01.org.linux-iscsi.localhost'}}, + 'xml': + """ + <pool type='iscsi'> + <name>unitTestISCSIPoolPort</name> + <source> + <host name='127.0.0.1' port='3266' /> + <device path='iqn.2003-01.org.linux-iscsi.localhost'/> + </source> + <target> + <path>/dev/disk/by-id</path> + </target> + </pool> + """}, + {'def': + {'type': 'iscsi', + 'name': 'unitTestISCSIPoolAuth', + 'source': { + 'host': '127.0.0.1', + 'target': 'iqn.2003-01.org.linux-iscsi.localhost', + 'auth': {'username': 'testUser', + 'password': 'ActuallyNotUsedInPoolXML'}}}, + 'xml': + """ + <pool type='iscsi'> + <name>unitTestISCSIPoolAuth</name> + <source> + <host name='127.0.0.1' /> + <device path='iqn.2003-01.org.linux-iscsi.localhost'/> + <auth type='chap' username='testUser'> + <secret type='iscsi' usage='unitTestISCSIPoolAuth'/> + </auth> + </source> + <target> + <path>/dev/disk/by-id</path> + </target> + </pool> """}] for poolDef in poolDefs: -- 1.7.11.7

Reviewed-by: Aline Manera <alinefm@linux.vnet.ibm.com> On 01/02/2014 07:58 AM, Zhou Zheng Sheng wrote:
This patch implements creating iSCSI storagepool for libvirt. Each LUN in iSCSI storagepool can be used as a volume, but iSCSI storagepool does not provide ability to create volume. For now in kimchi we create volume for each newly created VM. Next is to implement attaching existing volume to a new VM.
This patch also adds validation for the iSCSI back-end. It tries to login the target using the auth before actually creating the pool. On RedHat family distributions, libiscsi comes with a Python binding, but not on other distributions. So in this patch it invokes iscsiadm to check iSCSI target.
As regard to iSCSI CHAP authentication, it's somewhat broken before libvirt 1.1.1. It was reworked in libvirt commit eb0d79, and this patch goes with libvirt 1.1.1, but it doesn't require libvirt in the spec file. This is because libvirt 1.1.1 is not available on Ubuntu 12.04 LTS. The user can just ignore this feature.
How to test it manually
Prerequisite An running iSCSI target on the network.
Create: curl -u root -H 'Content-type: application/json' \ -H 'Accept: application/json' \ -d '{"source": { "target": "iqn.YouriSCSITargetIQN.XXX", "host": "10.0.0.X"}, "type": "iscsi", "name": "iscsipool"}' \ http://127.0.0.1:8000/storagepools
Show Info: curl -u root -H 'Accept: application/json' \ http://127.0.0.1:8000/storagepools/iscsipool
Activate: curl -u root -H 'Content-type: application/json' \ -H 'Accept: application/json' \ -d '' http://127.0.0.1:8000/storagepools/iscsipool/activate
Examine: iscsiadm -m session
Deactivate: curl -u root -H 'Content-type: application/json' \ -H 'Accept: application/json' \ -d '' http://127.0.0.1:8000/storagepools/iscsipool/deactivate
Delete: curl -u root -X DELETE -H 'Accept: application/json' \ http://127.0.0.1:8000/storagepools/iscsipool
v2 -> v3 1) Support CHAP authentication. 2) Validate iSCSI target before creating the pool. 3) Add XML generation unit test with "port" and "auth".
v3 -> v4 1) Update RPM spec file and Debian control file. 2) Update API.json. 3) Give a proper name for the iSCSI target client class, TargetClient.
v4 -> v5 Fix description string of 'auth' in API.json.
Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- Makefile.am | 1 + contrib/DEBIAN/control.in | 3 +- contrib/kimchi.spec.fedora.in | 1 + contrib/kimchi.spec.suse.in | 1 + docs/API.md | 12 +++++- src/kimchi/API.json | 24 ++++++++++++ src/kimchi/Makefile.am | 1 + src/kimchi/iscsi.py | 89 +++++++++++++++++++++++++++++++++++++++++++ src/kimchi/model.py | 89 +++++++++++++++++++++++++++++++++++++++++-- tests/test_storagepool.py | 63 ++++++++++++++++++++++++++++++ 10 files changed, 278 insertions(+), 6 deletions(-) create mode 100644 src/kimchi/iscsi.py
diff --git a/Makefile.am b/Makefile.am index 01efa1b..b01a8e7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -47,6 +47,7 @@ PEP8_WHITELIST = \ src/kimchi/config.py.in \ src/kimchi/disks.py \ src/kimchi/featuretests.py \ + src/kimchi/iscsi.py \ src/kimchi/rollbackcontext.py \ src/kimchi/root.py \ src/kimchi/server.py \ diff --git a/contrib/DEBIAN/control.in b/contrib/DEBIAN/control.in index 380584c..eecfb27 100644 --- a/contrib/DEBIAN/control.in +++ b/contrib/DEBIAN/control.in @@ -17,7 +17,8 @@ Depends: python-cherrypy3 (>= 3.2.0), python-psutil (>= 0.6.0), python-ethtool, sosreport, - python-ipaddr + python-ipaddr, + open-iscsi Build-Depends: Maintainer: Aline Manera <alinefm@br.ibm.com> Description: Kimchi web server diff --git a/contrib/kimchi.spec.fedora.in b/contrib/kimchi.spec.fedora.in index 3044fc8..75435b3 100644 --- a/contrib/kimchi.spec.fedora.in +++ b/contrib/kimchi.spec.fedora.in @@ -25,6 +25,7 @@ Requires: python-ethtool Requires: sos Requires: python-ipaddr Requires: nfs-utils +Requires: iscsi-initiator-utils
%if 0%{?rhel} == 6 Requires: python-ordereddict diff --git a/contrib/kimchi.spec.suse.in b/contrib/kimchi.spec.suse.in index 190b2be..bcfb6db 100644 --- a/contrib/kimchi.spec.suse.in +++ b/contrib/kimchi.spec.suse.in @@ -20,6 +20,7 @@ Requires: python-jsonschema >= 1.3.0 Requires: python-ethtool Requires: python-ipaddr Requires: nfs-client +Requires: iscsi-initiator-utils
%if 0%{?sles_version} == 11 Requires: python-ordereddict diff --git a/docs/API.md b/docs/API.md index a8b9ea0..c347340 100644 --- a/docs/API.md +++ b/docs/API.md @@ -182,17 +182,25 @@ Represents a snapshot of the Virtual Machine's primary monitor. * **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' + Supported types: 'dir', 'kimchi-iso', 'netfs', 'logical', 'iscsi' * path: The path of the defined Storage Pool. For 'kimchi-iso' pool refers to targeted deep scan path. Pool types: 'dir', 'kimchi-iso'. * source: Dictionary containing source information of the pool. * host: IP or hostname of server for a pool backed from a remote host. - Pool types: 'netfs'. + Pool types: 'netfs', 'iscsi'. * path: Export path on NFS server for NFS pool. Pool types: 'netfs'. * devices: Array of devices to be used in the Storage Pool Pool types: 'logical'. + * target: Target IQN of an iSCSI pool. + Pool types: 'iscsi'. + * port *(optional)*: Listening port of a remote storage server. + Pool types: 'iscsi'. + * auth *(optional)*: Storage back-end authentication information. + Pool types: 'iscsi'. + * username: Login username of the iSCSI target. + * password: Login password of the iSCSI target.
### Resource: Storage Pool
diff --git a/src/kimchi/API.json b/src/kimchi/API.json index 6519c21..6f1dd03 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -44,6 +44,30 @@ "description": "Full path of the block device node", "type": "string" } + }, + "target": { + "description": "Target IQN of an iSCSI pool", + "type": "string" + }, + "port": { + "description": "Listening port of a remote storage server", + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "auth": { + "description": "Storage back-end authentication information", + "type": "object", + "properties": { + "username": { + "description": "Login username of the iSCSI target", + "type": "string" + }, + "password": { + "description": "Login password of the iSCSI target", + "type": "string" + } + } } } } diff --git a/src/kimchi/Makefile.am b/src/kimchi/Makefile.am index 88ccbf7..261c331 100644 --- a/src/kimchi/Makefile.am +++ b/src/kimchi/Makefile.am @@ -31,6 +31,7 @@ kimchi_PYTHON = \ distroloader.py \ exception.py \ featuretests.py \ + iscsi.py \ isoinfo.py \ mockmodel.py \ model.py \ diff --git a/src/kimchi/iscsi.py b/src/kimchi/iscsi.py new file mode 100644 index 0000000..35c0b8a --- /dev/null +++ b/src/kimchi/iscsi.py @@ -0,0 +1,89 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Zhou Zheng Sheng <zhshzhou@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-1301USA + +import subprocess + + +from kimchi.exception import OperationFailed + + +class TargetClient(object): + def __init__(self, target, host, port=None, auth=None): + self.portal = host + ("" if port is None else ":%s" % port) + self.target = target + self.auth = auth + self.targetCmd = ['iscsiadm', '--mode', 'node', '--targetname', + self.target, '--portal', self.portal] + + def _update_db(self, Name, Value): + self._run_cmd(['--op=update', '--name', Name, '--value', Value]) + + def _update_auth(self): + if self.auth is None: + items = (('node.session.auth.authmethod', 'None'), + ('node.session.auth.username', ''), + ('node.session.auth.password', '')) + else: + items = (('node.session.auth.authmethod', 'CHAP'), + ('node.session.auth.username', self.auth['username']), + ('node.session.auth.password', self.auth['password'])) + for name, value in items: + self._update_db(name, value) + + def _run_cmd(self, cmd): + iscsiadm = subprocess.Popen( + self.targetCmd + cmd, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = iscsiadm.communicate() + if iscsiadm.returncode != 0: + raise OperationFailed('Error executing iscsiadm: %s' % err) + return out + + def _discover(self): + iscsiadm = subprocess.Popen( + ['iscsiadm', '--mode', 'discovery', '--type', 'sendtargets', + '--portal', self.portal], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = iscsiadm.communicate() + if iscsiadm.returncode != 0: + raise OperationFailed('Error executing iscsiadm: %s' % err) + return out + + def _run_op(self, op): + self._run_cmd(['--' + op]) + + def login(self): + self._discover() + self._update_auth() + self._run_op('login') + + def logout(self): + self._run_op('logout') + + def validate(self): + try: + self.login() + except OperationFailed: + return False + + self.logout() + return True diff --git a/src/kimchi/model.py b/src/kimchi/model.py index 2b46121..17d5485 100644 --- a/src/kimchi/model.py +++ b/src/kimchi/model.py @@ -65,6 +65,7 @@ from kimchi.distroloader import DistroLoader from kimchi.exception import InvalidOperation, InvalidParameter, MissingParameter from kimchi.exception import NotFoundError, OperationFailed from kimchi.featuretests import FeatureTests +from kimchi.iscsi import TargetClient from kimchi.networkxml import to_network_xml from kimchi.objectstore import ObjectStore from kimchi.scan import Scanner @@ -995,7 +996,7 @@ class Model(object): if params['type'] == 'kimchi-iso': task_id = self._do_deep_scan(params) poolDef = StoragePoolDef.create(params) - poolDef.prepare() + poolDef.prepare(conn) xml = poolDef.xml except KeyError, key: raise MissingParameter(key) @@ -1444,7 +1445,7 @@ class StoragePoolDef(object): def __init__(self, poolArgs): self.poolArgs = poolArgs
- def prepare(self): + def prepare(self, conn): ''' Validate pool arguments and perform preparations. Operation which would cause side effect should be put here. Subclasses can optionally override this method, or it always succeeds by default. ''' @@ -1487,7 +1488,7 @@ class NetfsPoolDef(StoragePoolDef): super(NetfsPoolDef, self).__init__(poolArgs) self.path = '/var/lib/kimchi/nfs_mount/' + self.poolArgs['name']
- def prepare(self): + def prepare(self, conn): # TODO: Verify the NFS export can be actually mounted. pass
@@ -1550,6 +1551,88 @@ class LogicalPoolDef(StoragePoolDef): return xml
+class IscsiPoolDef(StoragePoolDef): + poolType = 'iscsi' + + def prepare(self, conn): + source = self.poolArgs['source'] + if not TargetClient(**source).validate(): + raise OperationFailed("Can not login to iSCSI host %s target %s" % + (source['host'], source['target'])) + self._prepare_auth(conn) + + def _prepare_auth(self, conn): + try: + auth = self.poolArgs['source']['auth'] + except KeyError: + return + + try: + virSecret = conn.secretLookupByUsage( + libvirt.VIR_SECRET_USAGE_TYPE_ISCSI, self.poolArgs['name']) + except libvirt.libvirtError: + xml = ''' + <secret ephemeral='no' private='yes'> + <description>Secret for iSCSI storage pool {name}</description> + <auth type='chap' username='{username}'/> + <usage type='iscsi'> + <target>{name}</target> + </usage> + </secret>'''.format(name=self.poolArgs['name'], + username=auth['username']) + virSecret = conn.secretDefineXML(xml) + + virSecret.setValue(auth['password']) + + def _format_port(self, poolArgs): + try: + port = poolArgs['source']['port'] + except KeyError: + return "" + return "port='%s'" % port + + def _format_auth(self, poolArgs): + try: + auth = poolArgs['source']['auth'] + except KeyError: + return "" + + return ''' + <auth type='chap' username='{username}'> + <secret type='iscsi' usage='{name}'/> + </auth>'''.format(name=poolArgs['name'], username=auth['username']) + + @property + def xml(self): + # Required parameters + # name: + # type: + # source[host]: + # source[target]: + # + # Optional parameters + # source[port]: + poolArgs = copy.deepcopy(self.poolArgs) + poolArgs['source'].update({'port': self._format_port(poolArgs), + 'auth': self._format_auth(poolArgs)}) + poolArgs['path'] = '/dev/disk/by-id' + + xml = """ + <pool type='iscsi'> + <name>{name}</name> + <source> + <host name='{source[host]}' {source[port]}/> + <device path='{source[target]}'/> + {source[auth]} + </source> + <target> + <path>{path}</path> + </target> + </pool> + """.format(**poolArgs) + return xml + + def _get_volume_xml(**kwargs): # Required parameters # name: diff --git a/tests/test_storagepool.py b/tests/test_storagepool.py index 1a7786f..8341537 100644 --- a/tests/test_storagepool.py +++ b/tests/test_storagepool.py @@ -78,6 +78,69 @@ class storagepoolTests(unittest.TestCase): <path>/var/lib/kimchi/logical_mount/unitTestLogicalPool</path> </target> </pool> + """}, + {'def': + {'type': 'iscsi', + 'name': 'unitTestISCSIPool', + 'source': { + 'host': '127.0.0.1', + 'target': 'iqn.2003-01.org.linux-iscsi.localhost'}}, + 'xml': + """ + <pool type='iscsi'> + <name>unitTestISCSIPool</name> + <source> + <host name='127.0.0.1' /> + <device path='iqn.2003-01.org.linux-iscsi.localhost'/> + </source> + <target> + <path>/dev/disk/by-id</path> + </target> + </pool> + """}, + {'def': + {'type': 'iscsi', + 'name': 'unitTestISCSIPoolPort', + 'source': { + 'host': '127.0.0.1', + 'port': 3266, + 'target': 'iqn.2003-01.org.linux-iscsi.localhost'}}, + 'xml': + """ + <pool type='iscsi'> + <name>unitTestISCSIPoolPort</name> + <source> + <host name='127.0.0.1' port='3266' /> + <device path='iqn.2003-01.org.linux-iscsi.localhost'/> + </source> + <target> + <path>/dev/disk/by-id</path> + </target> + </pool> + """}, + {'def': + {'type': 'iscsi', + 'name': 'unitTestISCSIPoolAuth', + 'source': { + 'host': '127.0.0.1', + 'target': 'iqn.2003-01.org.linux-iscsi.localhost', + 'auth': {'username': 'testUser', + 'password': 'ActuallyNotUsedInPoolXML'}}}, + 'xml': + """ + <pool type='iscsi'> + <name>unitTestISCSIPoolAuth</name> + <source> + <host name='127.0.0.1' /> + <device path='iqn.2003-01.org.linux-iscsi.localhost'/> + <auth type='chap' username='testUser'> + <secret type='iscsi' usage='unitTestISCSIPoolAuth'/> + </auth> + </source> + <target> + <path>/dev/disk/by-id</path> + </target> + </pool> """}]
for poolDef in poolDefs:

Reviewed-by: ShaoHe Feng <shaohef@linux.vnet.ibm.com> On 01/02/2014 05:58 PM, Zhou Zheng Sheng wrote:
This patch implements creating iSCSI storagepool for libvirt. Each LUN in iSCSI storagepool can be used as a volume, but iSCSI storagepool does not provide ability to create volume. For now in kimchi we create volume for each newly created VM. Next is to implement attaching existing volume to a new VM.
This patch also adds validation for the iSCSI back-end. It tries to login the target using the auth before actually creating the pool. On RedHat family distributions, libiscsi comes with a Python binding, but not on other distributions. So in this patch it invokes iscsiadm to check iSCSI target.
As regard to iSCSI CHAP authentication, it's somewhat broken before libvirt 1.1.1. It was reworked in libvirt commit eb0d79, and this patch goes with libvirt 1.1.1, but it doesn't require libvirt in the spec file. This is because libvirt 1.1.1 is not available on Ubuntu 12.04 LTS. The user can just ignore this feature.
How to test it manually
Prerequisite An running iSCSI target on the network.
Create: curl -u root -H 'Content-type: application/json' \ -H 'Accept: application/json' \ -d '{"source": { "target": "iqn.YouriSCSITargetIQN.XXX", "host": "10.0.0.X"}, "type": "iscsi", "name": "iscsipool"}' \ http://127.0.0.1:8000/storagepools
Show Info: curl -u root -H 'Accept: application/json' \ http://127.0.0.1:8000/storagepools/iscsipool
Activate: curl -u root -H 'Content-type: application/json' \ -H 'Accept: application/json' \ -d '' http://127.0.0.1:8000/storagepools/iscsipool/activate
Examine: iscsiadm -m session
Deactivate: curl -u root -H 'Content-type: application/json' \ -H 'Accept: application/json' \ -d '' http://127.0.0.1:8000/storagepools/iscsipool/deactivate
Delete: curl -u root -X DELETE -H 'Accept: application/json' \ http://127.0.0.1:8000/storagepools/iscsipool
v2 -> v3 1) Support CHAP authentication. 2) Validate iSCSI target before creating the pool. 3) Add XML generation unit test with "port" and "auth".
v3 -> v4 1) Update RPM spec file and Debian control file. 2) Update API.json. 3) Give a proper name for the iSCSI target client class, TargetClient.
v4 -> v5 Fix description string of 'auth' in API.json.
Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- Makefile.am | 1 + contrib/DEBIAN/control.in | 3 +- contrib/kimchi.spec.fedora.in | 1 + contrib/kimchi.spec.suse.in | 1 + docs/API.md | 12 +++++- src/kimchi/API.json | 24 ++++++++++++ src/kimchi/Makefile.am | 1 + src/kimchi/iscsi.py | 89 +++++++++++++++++++++++++++++++++++++++++++ src/kimchi/model.py | 89 +++++++++++++++++++++++++++++++++++++++++-- tests/test_storagepool.py | 63 ++++++++++++++++++++++++++++++ 10 files changed, 278 insertions(+), 6 deletions(-) create mode 100644 src/kimchi/iscsi.py
diff --git a/Makefile.am b/Makefile.am index 01efa1b..b01a8e7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -47,6 +47,7 @@ PEP8_WHITELIST = \ src/kimchi/config.py.in \ src/kimchi/disks.py \ src/kimchi/featuretests.py \ + src/kimchi/iscsi.py \ src/kimchi/rollbackcontext.py \ src/kimchi/root.py \ src/kimchi/server.py \ diff --git a/contrib/DEBIAN/control.in b/contrib/DEBIAN/control.in index 380584c..eecfb27 100644 --- a/contrib/DEBIAN/control.in +++ b/contrib/DEBIAN/control.in @@ -17,7 +17,8 @@ Depends: python-cherrypy3 (>= 3.2.0), python-psutil (>= 0.6.0), python-ethtool, sosreport, - python-ipaddr + python-ipaddr, + open-iscsi Build-Depends: Maintainer: Aline Manera <alinefm@br.ibm.com> Description: Kimchi web server diff --git a/contrib/kimchi.spec.fedora.in b/contrib/kimchi.spec.fedora.in index 3044fc8..75435b3 100644 --- a/contrib/kimchi.spec.fedora.in +++ b/contrib/kimchi.spec.fedora.in @@ -25,6 +25,7 @@ Requires: python-ethtool Requires: sos Requires: python-ipaddr Requires: nfs-utils +Requires: iscsi-initiator-utils
%if 0%{?rhel} == 6 Requires: python-ordereddict diff --git a/contrib/kimchi.spec.suse.in b/contrib/kimchi.spec.suse.in index 190b2be..bcfb6db 100644 --- a/contrib/kimchi.spec.suse.in +++ b/contrib/kimchi.spec.suse.in @@ -20,6 +20,7 @@ Requires: python-jsonschema >= 1.3.0 Requires: python-ethtool Requires: python-ipaddr Requires: nfs-client +Requires: iscsi-initiator-utils
%if 0%{?sles_version} == 11 Requires: python-ordereddict diff --git a/docs/API.md b/docs/API.md index a8b9ea0..c347340 100644 --- a/docs/API.md +++ b/docs/API.md @@ -182,17 +182,25 @@ Represents a snapshot of the Virtual Machine's primary monitor. * **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' + Supported types: 'dir', 'kimchi-iso', 'netfs', 'logical', 'iscsi' * path: The path of the defined Storage Pool. For 'kimchi-iso' pool refers to targeted deep scan path. Pool types: 'dir', 'kimchi-iso'. * source: Dictionary containing source information of the pool. * host: IP or hostname of server for a pool backed from a remote host. - Pool types: 'netfs'. + Pool types: 'netfs', 'iscsi'. * path: Export path on NFS server for NFS pool. Pool types: 'netfs'. * devices: Array of devices to be used in the Storage Pool Pool types: 'logical'. + * target: Target IQN of an iSCSI pool. + Pool types: 'iscsi'. + * port *(optional)*: Listening port of a remote storage server. + Pool types: 'iscsi'. + * auth *(optional)*: Storage back-end authentication information. + Pool types: 'iscsi'. + * username: Login username of the iSCSI target. + * password: Login password of the iSCSI target.
### Resource: Storage Pool
diff --git a/src/kimchi/API.json b/src/kimchi/API.json index 6519c21..6f1dd03 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -44,6 +44,30 @@ "description": "Full path of the block device node", "type": "string" } + }, + "target": { + "description": "Target IQN of an iSCSI pool", + "type": "string" + }, + "port": { + "description": "Listening port of a remote storage server", + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "auth": { + "description": "Storage back-end authentication information", + "type": "object", + "properties": { + "username": { + "description": "Login username of the iSCSI target", + "type": "string" + }, + "password": { + "description": "Login password of the iSCSI target", + "type": "string" + } + } } } } diff --git a/src/kimchi/Makefile.am b/src/kimchi/Makefile.am index 88ccbf7..261c331 100644 --- a/src/kimchi/Makefile.am +++ b/src/kimchi/Makefile.am @@ -31,6 +31,7 @@ kimchi_PYTHON = \ distroloader.py \ exception.py \ featuretests.py \ + iscsi.py \ isoinfo.py \ mockmodel.py \ model.py \ diff --git a/src/kimchi/iscsi.py b/src/kimchi/iscsi.py new file mode 100644 index 0000000..35c0b8a --- /dev/null +++ b/src/kimchi/iscsi.py @@ -0,0 +1,89 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Zhou Zheng Sheng <zhshzhou@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-1301USA + +import subprocess + + +from kimchi.exception import OperationFailed + + +class TargetClient(object): + def __init__(self, target, host, port=None, auth=None): + self.portal = host + ("" if port is None else ":%s" % port) + self.target = target + self.auth = auth + self.targetCmd = ['iscsiadm', '--mode', 'node', '--targetname', + self.target, '--portal', self.portal] + + def _update_db(self, Name, Value): + self._run_cmd(['--op=update', '--name', Name, '--value', Value]) + + def _update_auth(self): + if self.auth is None: + items = (('node.session.auth.authmethod', 'None'), + ('node.session.auth.username', ''), + ('node.session.auth.password', '')) + else: + items = (('node.session.auth.authmethod', 'CHAP'), + ('node.session.auth.username', self.auth['username']), + ('node.session.auth.password', self.auth['password'])) + for name, value in items: + self._update_db(name, value) + + def _run_cmd(self, cmd): + iscsiadm = subprocess.Popen( + self.targetCmd + cmd, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = iscsiadm.communicate() + if iscsiadm.returncode != 0: + raise OperationFailed('Error executing iscsiadm: %s' % err) + return out + + def _discover(self): + iscsiadm = subprocess.Popen( + ['iscsiadm', '--mode', 'discovery', '--type', 'sendtargets', + '--portal', self.portal], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = iscsiadm.communicate() + if iscsiadm.returncode != 0: + raise OperationFailed('Error executing iscsiadm: %s' % err) + return out + + def _run_op(self, op): + self._run_cmd(['--' + op]) + + def login(self): + self._discover() + self._update_auth() + self._run_op('login') + + def logout(self): + self._run_op('logout') + + def validate(self): + try: + self.login() + except OperationFailed: + return False + + self.logout() + return True diff --git a/src/kimchi/model.py b/src/kimchi/model.py index 2b46121..17d5485 100644 --- a/src/kimchi/model.py +++ b/src/kimchi/model.py @@ -65,6 +65,7 @@ from kimchi.distroloader import DistroLoader from kimchi.exception import InvalidOperation, InvalidParameter, MissingParameter from kimchi.exception import NotFoundError, OperationFailed from kimchi.featuretests import FeatureTests +from kimchi.iscsi import TargetClient from kimchi.networkxml import to_network_xml from kimchi.objectstore import ObjectStore from kimchi.scan import Scanner @@ -995,7 +996,7 @@ class Model(object): if params['type'] == 'kimchi-iso': task_id = self._do_deep_scan(params) poolDef = StoragePoolDef.create(params) - poolDef.prepare() + poolDef.prepare(conn) xml = poolDef.xml except KeyError, key: raise MissingParameter(key) @@ -1444,7 +1445,7 @@ class StoragePoolDef(object): def __init__(self, poolArgs): self.poolArgs = poolArgs
- def prepare(self): + def prepare(self, conn): ''' Validate pool arguments and perform preparations. Operation which would cause side effect should be put here. Subclasses can optionally override this method, or it always succeeds by default. ''' @@ -1487,7 +1488,7 @@ class NetfsPoolDef(StoragePoolDef): super(NetfsPoolDef, self).__init__(poolArgs) self.path = '/var/lib/kimchi/nfs_mount/' + self.poolArgs['name']
- def prepare(self): + def prepare(self, conn): # TODO: Verify the NFS export can be actually mounted. pass
@@ -1550,6 +1551,88 @@ class LogicalPoolDef(StoragePoolDef): return xml
+class IscsiPoolDef(StoragePoolDef): + poolType = 'iscsi' + + def prepare(self, conn): + source = self.poolArgs['source'] + if not TargetClient(**source).validate(): + raise OperationFailed("Can not login to iSCSI host %s target %s" % + (source['host'], source['target'])) + self._prepare_auth(conn) + + def _prepare_auth(self, conn): + try: + auth = self.poolArgs['source']['auth'] + except KeyError: + return + + try: + virSecret = conn.secretLookupByUsage( + libvirt.VIR_SECRET_USAGE_TYPE_ISCSI, self.poolArgs['name']) + except libvirt.libvirtError: + xml = ''' + <secret ephemeral='no' private='yes'> + <description>Secret for iSCSI storage pool {name}</description> + <auth type='chap' username='{username}'/> + <usage type='iscsi'> + <target>{name}</target> + </usage> + </secret>'''.format(name=self.poolArgs['name'], + username=auth['username']) + virSecret = conn.secretDefineXML(xml) + + virSecret.setValue(auth['password']) + + def _format_port(self, poolArgs): + try: + port = poolArgs['source']['port'] + except KeyError: + return "" + return "port='%s'" % port + + def _format_auth(self, poolArgs): + try: + auth = poolArgs['source']['auth'] + except KeyError: + return "" + + return ''' + <auth type='chap' username='{username}'> + <secret type='iscsi' usage='{name}'/> + </auth>'''.format(name=poolArgs['name'], username=auth['username']) + + @property + def xml(self): + # Required parameters + # name: + # type: + # source[host]: + # source[target]: + # + # Optional parameters + # source[port]: + poolArgs = copy.deepcopy(self.poolArgs) + poolArgs['source'].update({'port': self._format_port(poolArgs), + 'auth': self._format_auth(poolArgs)}) + poolArgs['path'] = '/dev/disk/by-id' + + xml = """ + <pool type='iscsi'> + <name>{name}</name> + <source> + <host name='{source[host]}' {source[port]}/> + <device path='{source[target]}'/> + {source[auth]} + </source> + <target> + <path>{path}</path> + </target> + </pool> + """.format(**poolArgs) + return xml + + def _get_volume_xml(**kwargs): # Required parameters # name: diff --git a/tests/test_storagepool.py b/tests/test_storagepool.py index 1a7786f..8341537 100644 --- a/tests/test_storagepool.py +++ b/tests/test_storagepool.py @@ -78,6 +78,69 @@ class storagepoolTests(unittest.TestCase): <path>/var/lib/kimchi/logical_mount/unitTestLogicalPool</path> </target> </pool> + """}, + {'def': + {'type': 'iscsi', + 'name': 'unitTestISCSIPool', + 'source': { + 'host': '127.0.0.1', + 'target': 'iqn.2003-01.org.linux-iscsi.localhost'}}, + 'xml': + """ + <pool type='iscsi'> + <name>unitTestISCSIPool</name> + <source> + <host name='127.0.0.1' /> + <device path='iqn.2003-01.org.linux-iscsi.localhost'/> + </source> + <target> + <path>/dev/disk/by-id</path> + </target> + </pool> + """}, + {'def': + {'type': 'iscsi', + 'name': 'unitTestISCSIPoolPort', + 'source': { + 'host': '127.0.0.1', + 'port': 3266, + 'target': 'iqn.2003-01.org.linux-iscsi.localhost'}}, + 'xml': + """ + <pool type='iscsi'> + <name>unitTestISCSIPoolPort</name> + <source> + <host name='127.0.0.1' port='3266' /> + <device path='iqn.2003-01.org.linux-iscsi.localhost'/> + </source> + <target> + <path>/dev/disk/by-id</path> + </target> + </pool> + """}, + {'def': + {'type': 'iscsi', + 'name': 'unitTestISCSIPoolAuth', + 'source': { + 'host': '127.0.0.1', + 'target': 'iqn.2003-01.org.linux-iscsi.localhost', + 'auth': {'username': 'testUser', + 'password': 'ActuallyNotUsedInPoolXML'}}}, + 'xml': + """ + <pool type='iscsi'> + <name>unitTestISCSIPoolAuth</name> + <source> + <host name='127.0.0.1' /> + <device path='iqn.2003-01.org.linux-iscsi.localhost'/> + <auth type='chap' username='testUser'> + <secret type='iscsi' usage='unitTestISCSIPoolAuth'/> + </auth> + </source> + <target> + <path>/dev/disk/by-id</path> + </target> + </pool> """}]
for poolDef in poolDefs:
-- Thanks and best regards! Sheldon Feng(冯少合)<shaohef@linux.vnet.ibm.com> IBM Linux Technology Center

Reviewed-by: Royce Lv<lvroyce@linux.vnet.ibm.com> On 2014年01月02日 17:58, Zhou Zheng Sheng wrote:
This patch implements creating iSCSI storagepool for libvirt. Each LUN in iSCSI storagepool can be used as a volume, but iSCSI storagepool does not provide ability to create volume. For now in kimchi we create volume for each newly created VM. Next is to implement attaching existing volume to a new VM.
This patch also adds validation for the iSCSI back-end. It tries to login the target using the auth before actually creating the pool. On RedHat family distributions, libiscsi comes with a Python binding, but not on other distributions. So in this patch it invokes iscsiadm to check iSCSI target.
As regard to iSCSI CHAP authentication, it's somewhat broken before libvirt 1.1.1. It was reworked in libvirt commit eb0d79, and this patch goes with libvirt 1.1.1, but it doesn't require libvirt in the spec file. This is because libvirt 1.1.1 is not available on Ubuntu 12.04 LTS. The user can just ignore this feature.
How to test it manually
Prerequisite An running iSCSI target on the network.
Create: curl -u root -H 'Content-type: application/json' \ -H 'Accept: application/json' \ -d '{"source": { "target": "iqn.YouriSCSITargetIQN.XXX", "host": "10.0.0.X"}, "type": "iscsi", "name": "iscsipool"}' \ http://127.0.0.1:8000/storagepools
Show Info: curl -u root -H 'Accept: application/json' \ http://127.0.0.1:8000/storagepools/iscsipool
Activate: curl -u root -H 'Content-type: application/json' \ -H 'Accept: application/json' \ -d '' http://127.0.0.1:8000/storagepools/iscsipool/activate
Examine: iscsiadm -m session
Deactivate: curl -u root -H 'Content-type: application/json' \ -H 'Accept: application/json' \ -d '' http://127.0.0.1:8000/storagepools/iscsipool/deactivate
Delete: curl -u root -X DELETE -H 'Accept: application/json' \ http://127.0.0.1:8000/storagepools/iscsipool
v2 -> v3 1) Support CHAP authentication. 2) Validate iSCSI target before creating the pool. 3) Add XML generation unit test with "port" and "auth".
v3 -> v4 1) Update RPM spec file and Debian control file. 2) Update API.json. 3) Give a proper name for the iSCSI target client class, TargetClient.
v4 -> v5 Fix description string of 'auth' in API.json.
Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- Makefile.am | 1 + contrib/DEBIAN/control.in | 3 +- contrib/kimchi.spec.fedora.in | 1 + contrib/kimchi.spec.suse.in | 1 + docs/API.md | 12 +++++- src/kimchi/API.json | 24 ++++++++++++ src/kimchi/Makefile.am | 1 + src/kimchi/iscsi.py | 89 +++++++++++++++++++++++++++++++++++++++++++ src/kimchi/model.py | 89 +++++++++++++++++++++++++++++++++++++++++-- tests/test_storagepool.py | 63 ++++++++++++++++++++++++++++++ 10 files changed, 278 insertions(+), 6 deletions(-) create mode 100644 src/kimchi/iscsi.py
diff --git a/Makefile.am b/Makefile.am index 01efa1b..b01a8e7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -47,6 +47,7 @@ PEP8_WHITELIST = \ src/kimchi/config.py.in \ src/kimchi/disks.py \ src/kimchi/featuretests.py \ + src/kimchi/iscsi.py \ src/kimchi/rollbackcontext.py \ src/kimchi/root.py \ src/kimchi/server.py \ diff --git a/contrib/DEBIAN/control.in b/contrib/DEBIAN/control.in index 380584c..eecfb27 100644 --- a/contrib/DEBIAN/control.in +++ b/contrib/DEBIAN/control.in @@ -17,7 +17,8 @@ Depends: python-cherrypy3 (>= 3.2.0), python-psutil (>= 0.6.0), python-ethtool, sosreport, - python-ipaddr + python-ipaddr, + open-iscsi Build-Depends: Maintainer: Aline Manera <alinefm@br.ibm.com> Description: Kimchi web server diff --git a/contrib/kimchi.spec.fedora.in b/contrib/kimchi.spec.fedora.in index 3044fc8..75435b3 100644 --- a/contrib/kimchi.spec.fedora.in +++ b/contrib/kimchi.spec.fedora.in @@ -25,6 +25,7 @@ Requires: python-ethtool Requires: sos Requires: python-ipaddr Requires: nfs-utils +Requires: iscsi-initiator-utils
%if 0%{?rhel} == 6 Requires: python-ordereddict diff --git a/contrib/kimchi.spec.suse.in b/contrib/kimchi.spec.suse.in index 190b2be..bcfb6db 100644 --- a/contrib/kimchi.spec.suse.in +++ b/contrib/kimchi.spec.suse.in @@ -20,6 +20,7 @@ Requires: python-jsonschema >= 1.3.0 Requires: python-ethtool Requires: python-ipaddr Requires: nfs-client +Requires: iscsi-initiator-utils
%if 0%{?sles_version} == 11 Requires: python-ordereddict diff --git a/docs/API.md b/docs/API.md index a8b9ea0..c347340 100644 --- a/docs/API.md +++ b/docs/API.md @@ -182,17 +182,25 @@ Represents a snapshot of the Virtual Machine's primary monitor. * **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' + Supported types: 'dir', 'kimchi-iso', 'netfs', 'logical', 'iscsi' * path: The path of the defined Storage Pool. For 'kimchi-iso' pool refers to targeted deep scan path. Pool types: 'dir', 'kimchi-iso'. * source: Dictionary containing source information of the pool. * host: IP or hostname of server for a pool backed from a remote host. - Pool types: 'netfs'. + Pool types: 'netfs', 'iscsi'. * path: Export path on NFS server for NFS pool. Pool types: 'netfs'. * devices: Array of devices to be used in the Storage Pool Pool types: 'logical'. + * target: Target IQN of an iSCSI pool. + Pool types: 'iscsi'. + * port *(optional)*: Listening port of a remote storage server. + Pool types: 'iscsi'. + * auth *(optional)*: Storage back-end authentication information. + Pool types: 'iscsi'. + * username: Login username of the iSCSI target. + * password: Login password of the iSCSI target.
### Resource: Storage Pool
diff --git a/src/kimchi/API.json b/src/kimchi/API.json index 6519c21..6f1dd03 100644 --- a/src/kimchi/API.json +++ b/src/kimchi/API.json @@ -44,6 +44,30 @@ "description": "Full path of the block device node", "type": "string" } + }, + "target": { + "description": "Target IQN of an iSCSI pool", + "type": "string" + }, + "port": { + "description": "Listening port of a remote storage server", + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "auth": { + "description": "Storage back-end authentication information", + "type": "object", + "properties": { + "username": { + "description": "Login username of the iSCSI target", + "type": "string" + }, + "password": { + "description": "Login password of the iSCSI target", + "type": "string" + } + } } } } diff --git a/src/kimchi/Makefile.am b/src/kimchi/Makefile.am index 88ccbf7..261c331 100644 --- a/src/kimchi/Makefile.am +++ b/src/kimchi/Makefile.am @@ -31,6 +31,7 @@ kimchi_PYTHON = \ distroloader.py \ exception.py \ featuretests.py \ + iscsi.py \ isoinfo.py \ mockmodel.py \ model.py \ diff --git a/src/kimchi/iscsi.py b/src/kimchi/iscsi.py new file mode 100644 index 0000000..35c0b8a --- /dev/null +++ b/src/kimchi/iscsi.py @@ -0,0 +1,89 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2013 +# +# Authors: +# Zhou Zheng Sheng <zhshzhou@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-1301USA + +import subprocess + + +from kimchi.exception import OperationFailed + + +class TargetClient(object): + def __init__(self, target, host, port=None, auth=None): + self.portal = host + ("" if port is None else ":%s" % port) + self.target = target + self.auth = auth + self.targetCmd = ['iscsiadm', '--mode', 'node', '--targetname', + self.target, '--portal', self.portal] + + def _update_db(self, Name, Value): + self._run_cmd(['--op=update', '--name', Name, '--value', Value]) + + def _update_auth(self): + if self.auth is None: + items = (('node.session.auth.authmethod', 'None'), + ('node.session.auth.username', ''), + ('node.session.auth.password', '')) + else: + items = (('node.session.auth.authmethod', 'CHAP'), + ('node.session.auth.username', self.auth['username']), + ('node.session.auth.password', self.auth['password'])) + for name, value in items: + self._update_db(name, value) + + def _run_cmd(self, cmd): + iscsiadm = subprocess.Popen( + self.targetCmd + cmd, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = iscsiadm.communicate() + if iscsiadm.returncode != 0: + raise OperationFailed('Error executing iscsiadm: %s' % err) + return out + + def _discover(self): + iscsiadm = subprocess.Popen( + ['iscsiadm', '--mode', 'discovery', '--type', 'sendtargets', + '--portal', self.portal], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = iscsiadm.communicate() + if iscsiadm.returncode != 0: + raise OperationFailed('Error executing iscsiadm: %s' % err) + return out + + def _run_op(self, op): + self._run_cmd(['--' + op]) + + def login(self): + self._discover() + self._update_auth() + self._run_op('login') + + def logout(self): + self._run_op('logout') + + def validate(self): + try: + self.login() + except OperationFailed: + return False + + self.logout() + return True diff --git a/src/kimchi/model.py b/src/kimchi/model.py index 2b46121..17d5485 100644 --- a/src/kimchi/model.py +++ b/src/kimchi/model.py @@ -65,6 +65,7 @@ from kimchi.distroloader import DistroLoader from kimchi.exception import InvalidOperation, InvalidParameter, MissingParameter from kimchi.exception import NotFoundError, OperationFailed from kimchi.featuretests import FeatureTests +from kimchi.iscsi import TargetClient from kimchi.networkxml import to_network_xml from kimchi.objectstore import ObjectStore from kimchi.scan import Scanner @@ -995,7 +996,7 @@ class Model(object): if params['type'] == 'kimchi-iso': task_id = self._do_deep_scan(params) poolDef = StoragePoolDef.create(params) - poolDef.prepare() + poolDef.prepare(conn) xml = poolDef.xml except KeyError, key: raise MissingParameter(key) @@ -1444,7 +1445,7 @@ class StoragePoolDef(object): def __init__(self, poolArgs): self.poolArgs = poolArgs
- def prepare(self): + def prepare(self, conn): ''' Validate pool arguments and perform preparations. Operation which would cause side effect should be put here. Subclasses can optionally override this method, or it always succeeds by default. ''' @@ -1487,7 +1488,7 @@ class NetfsPoolDef(StoragePoolDef): super(NetfsPoolDef, self).__init__(poolArgs) self.path = '/var/lib/kimchi/nfs_mount/' + self.poolArgs['name']
- def prepare(self): + def prepare(self, conn): # TODO: Verify the NFS export can be actually mounted. pass
@@ -1550,6 +1551,88 @@ class LogicalPoolDef(StoragePoolDef): return xml
+class IscsiPoolDef(StoragePoolDef): + poolType = 'iscsi' + + def prepare(self, conn): + source = self.poolArgs['source'] + if not TargetClient(**source).validate(): + raise OperationFailed("Can not login to iSCSI host %s target %s" % + (source['host'], source['target'])) + self._prepare_auth(conn) + + def _prepare_auth(self, conn): + try: + auth = self.poolArgs['source']['auth'] + except KeyError: + return + + try: + virSecret = conn.secretLookupByUsage( + libvirt.VIR_SECRET_USAGE_TYPE_ISCSI, self.poolArgs['name']) + except libvirt.libvirtError: + xml = ''' + <secret ephemeral='no' private='yes'> + <description>Secret for iSCSI storage pool {name}</description> + <auth type='chap' username='{username}'/> + <usage type='iscsi'> + <target>{name}</target> + </usage> + </secret>'''.format(name=self.poolArgs['name'], + username=auth['username']) + virSecret = conn.secretDefineXML(xml) + + virSecret.setValue(auth['password']) + + def _format_port(self, poolArgs): + try: + port = poolArgs['source']['port'] + except KeyError: + return "" + return "port='%s'" % port + + def _format_auth(self, poolArgs): + try: + auth = poolArgs['source']['auth'] + except KeyError: + return "" + + return ''' + <auth type='chap' username='{username}'> + <secret type='iscsi' usage='{name}'/> + </auth>'''.format(name=poolArgs['name'], username=auth['username']) + + @property + def xml(self): + # Required parameters + # name: + # type: + # source[host]: + # source[target]: + # + # Optional parameters + # source[port]: + poolArgs = copy.deepcopy(self.poolArgs) + poolArgs['source'].update({'port': self._format_port(poolArgs), + 'auth': self._format_auth(poolArgs)}) + poolArgs['path'] = '/dev/disk/by-id' + + xml = """ + <pool type='iscsi'> + <name>{name}</name> + <source> + <host name='{source[host]}' {source[port]}/> + <device path='{source[target]}'/> + {source[auth]} + </source> + <target> + <path>{path}</path> + </target> + </pool> + """.format(**poolArgs) + return xml + + def _get_volume_xml(**kwargs): # Required parameters # name: diff --git a/tests/test_storagepool.py b/tests/test_storagepool.py index 1a7786f..8341537 100644 --- a/tests/test_storagepool.py +++ b/tests/test_storagepool.py @@ -78,6 +78,69 @@ class storagepoolTests(unittest.TestCase): <path>/var/lib/kimchi/logical_mount/unitTestLogicalPool</path> </target> </pool> + """}, + {'def': + {'type': 'iscsi', + 'name': 'unitTestISCSIPool', + 'source': { + 'host': '127.0.0.1', + 'target': 'iqn.2003-01.org.linux-iscsi.localhost'}}, + 'xml': + """ + <pool type='iscsi'> + <name>unitTestISCSIPool</name> + <source> + <host name='127.0.0.1' /> + <device path='iqn.2003-01.org.linux-iscsi.localhost'/> + </source> + <target> + <path>/dev/disk/by-id</path> + </target> + </pool> + """}, + {'def': + {'type': 'iscsi', + 'name': 'unitTestISCSIPoolPort', + 'source': { + 'host': '127.0.0.1', + 'port': 3266, + 'target': 'iqn.2003-01.org.linux-iscsi.localhost'}}, + 'xml': + """ + <pool type='iscsi'> + <name>unitTestISCSIPoolPort</name> + <source> + <host name='127.0.0.1' port='3266' /> + <device path='iqn.2003-01.org.linux-iscsi.localhost'/> + </source> + <target> + <path>/dev/disk/by-id</path> + </target> + </pool> + """}, + {'def': + {'type': 'iscsi', + 'name': 'unitTestISCSIPoolAuth', + 'source': { + 'host': '127.0.0.1', + 'target': 'iqn.2003-01.org.linux-iscsi.localhost', + 'auth': {'username': 'testUser', + 'password': 'ActuallyNotUsedInPoolXML'}}}, + 'xml': + """ + <pool type='iscsi'> + <name>unitTestISCSIPoolAuth</name> + <source> + <host name='127.0.0.1' /> + <device path='iqn.2003-01.org.linux-iscsi.localhost'/> + <auth type='chap' username='testUser'> + <secret type='iscsi' usage='unitTestISCSIPoolAuth'/> + </auth> + </source> + <target> + <path>/dev/disk/by-id</path> + </target> + </pool> """}]
for poolDef in poolDefs:

This patch adds iSCSI storage pool test to test_model.ModelTests.test_storagepool. It firstly checks if there exist an iSCSI target named "iqn.2013-12.localhost.kimchiUnitTest" on local host. If not, skip the test; if yes, continue to run the test already defined in test_storagepool. v2 -> v3 Do not create dir for the pool, pool.build() called by storagepools_create() does this automatically. v3 -> v5 No changes. Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- tests/test_model.py | 91 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 40 deletions(-) diff --git a/tests/test_model.py b/tests/test_model.py index c03cc3f..fa5c1d2 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -38,6 +38,7 @@ import utils from kimchi import netinfo from kimchi.exception import InvalidOperation, InvalidParameter from kimchi.exception import NotFoundError, OperationFailed +from kimchi.iscsi import TargetClient from kimchi.rollbackcontext import RollbackContext @@ -116,49 +117,59 @@ class ModelTests(unittest.TestCase): def test_storagepool(self): inst = kimchi.model.Model('qemu:///system', self.tmp_store) - with RollbackContext() as rollback: - path = '/tmp/kimchi-images' - name = 'test-pool' - if not os.path.exists(path): - os.mkdir(path) + poolDefs = [ + {'type': 'dir', + 'name': 'unitTestDirPool', + 'path': '/tmp/kimchi-images'}, + {'type': 'iscsi', + 'name': 'unitTestISCSIPool', + 'source': {'host': '127.0.0.1', + 'target': 'iqn.2013-12.localhost.kimchiUnitTest'}}] - pools = inst.storagepools_get_list() - num = len(pools) + 1 + for poolDef in poolDefs: + with RollbackContext() as rollback: + path = poolDef.get('path') + name = poolDef['name'] + + if poolDef['type'] == 'iscsi': + if not TargetClient(**poolDef['source']).validate(): + continue + + pools = inst.storagepools_get_list() + num = len(pools) + 1 + + inst.storagepools_create(poolDef) + rollback.prependDefer(inst.storagepool_delete, name) + + pools = inst.storagepools_get_list() + self.assertEquals(num, len(pools)) + + poolinfo = inst.storagepool_lookup(name) + if path is not None: + self.assertEquals(path, poolinfo['path']) + self.assertEquals('inactive', poolinfo['state']) + if poolinfo['type'] == 'dir': + self.assertEquals(True, poolinfo['autostart']) + else: + self.assertEquals(False, poolinfo['autostart']) + + inst.storagepool_activate(name) + rollback.prependDefer(inst.storagepool_deactivate, name) - args = {'name': name, - 'path': path, - 'type': 'dir'} - inst.storagepools_create(args) - rollback.prependDefer(inst.storagepool_delete, name) - - pools = inst.storagepools_get_list() - self.assertEquals(num, len(pools)) - - poolinfo = inst.storagepool_lookup(name) - self.assertEquals(path, poolinfo['path']) - self.assertEquals('inactive', poolinfo['state']) - if poolinfo['type'] == 'dir': - self.assertEquals(True, poolinfo['autostart']) - else: - self.assertEquals(False, poolinfo['autostart']) - - inst.storagepool_activate(name) - rollback.prependDefer(inst.storagepool_deactivate, name) - - poolinfo = inst.storagepool_lookup(name) - self.assertEquals('active', poolinfo['state']) - - autostart = poolinfo['autostart'] - ori_params = {'autostart': - True} if autostart else {'autostart': False} - for i in [True, False]: - params = {'autostart': i} - inst.storagepool_update(name, params) - rollback.prependDefer(inst.storagepool_update, name, - ori_params) poolinfo = inst.storagepool_lookup(name) - self.assertEquals(i, poolinfo['autostart']) - inst.storagepool_update(name, ori_params) + self.assertEquals('active', poolinfo['state']) + + autostart = poolinfo['autostart'] + ori_params = {'autostart': + True} if autostart else {'autostart': False} + for i in [True, False]: + params = {'autostart': i} + inst.storagepool_update(name, params) + rollback.prependDefer(inst.storagepool_update, name, + ori_params) + poolinfo = inst.storagepool_lookup(name) + self.assertEquals(i, poolinfo['autostart']) + inst.storagepool_update(name, ori_params) pools = inst.storagepools_get_list() self.assertIn('default', pools) -- 1.7.11.7

Reviewed-by: Aline Manera <alinefm@linux.vnet.ibm.com> On 01/02/2014 07:58 AM, Zhou Zheng Sheng wrote:
This patch adds iSCSI storage pool test to test_model.ModelTests.test_storagepool. It firstly checks if there exist an iSCSI target named "iqn.2013-12.localhost.kimchiUnitTest" on local host. If not, skip the test; if yes, continue to run the test already defined in test_storagepool.
v2 -> v3 Do not create dir for the pool, pool.build() called by storagepools_create() does this automatically.
v3 -> v5 No changes.
Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- tests/test_model.py | 91 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 40 deletions(-)
diff --git a/tests/test_model.py b/tests/test_model.py index c03cc3f..fa5c1d2 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -38,6 +38,7 @@ import utils from kimchi import netinfo from kimchi.exception import InvalidOperation, InvalidParameter from kimchi.exception import NotFoundError, OperationFailed +from kimchi.iscsi import TargetClient from kimchi.rollbackcontext import RollbackContext
@@ -116,49 +117,59 @@ class ModelTests(unittest.TestCase): def test_storagepool(self): inst = kimchi.model.Model('qemu:///system', self.tmp_store)
- with RollbackContext() as rollback: - path = '/tmp/kimchi-images' - name = 'test-pool' - if not os.path.exists(path): - os.mkdir(path) + poolDefs = [ + {'type': 'dir', + 'name': 'unitTestDirPool', + 'path': '/tmp/kimchi-images'}, + {'type': 'iscsi', + 'name': 'unitTestISCSIPool', + 'source': {'host': '127.0.0.1', + 'target': 'iqn.2013-12.localhost.kimchiUnitTest'}}]
- pools = inst.storagepools_get_list() - num = len(pools) + 1 + for poolDef in poolDefs: + with RollbackContext() as rollback: + path = poolDef.get('path') + name = poolDef['name'] + + if poolDef['type'] == 'iscsi': + if not TargetClient(**poolDef['source']).validate(): + continue + + pools = inst.storagepools_get_list() + num = len(pools) + 1 + + inst.storagepools_create(poolDef) + rollback.prependDefer(inst.storagepool_delete, name) + + pools = inst.storagepools_get_list() + self.assertEquals(num, len(pools)) + + poolinfo = inst.storagepool_lookup(name) + if path is not None: + self.assertEquals(path, poolinfo['path']) + self.assertEquals('inactive', poolinfo['state']) + if poolinfo['type'] == 'dir': + self.assertEquals(True, poolinfo['autostart']) + else: + self.assertEquals(False, poolinfo['autostart']) + + inst.storagepool_activate(name) + rollback.prependDefer(inst.storagepool_deactivate, name)
- args = {'name': name, - 'path': path, - 'type': 'dir'} - inst.storagepools_create(args) - rollback.prependDefer(inst.storagepool_delete, name) - - pools = inst.storagepools_get_list() - self.assertEquals(num, len(pools)) - - poolinfo = inst.storagepool_lookup(name) - self.assertEquals(path, poolinfo['path']) - self.assertEquals('inactive', poolinfo['state']) - if poolinfo['type'] == 'dir': - self.assertEquals(True, poolinfo['autostart']) - else: - self.assertEquals(False, poolinfo['autostart']) - - inst.storagepool_activate(name) - rollback.prependDefer(inst.storagepool_deactivate, name) - - poolinfo = inst.storagepool_lookup(name) - self.assertEquals('active', poolinfo['state']) - - autostart = poolinfo['autostart'] - ori_params = {'autostart': - True} if autostart else {'autostart': False} - for i in [True, False]: - params = {'autostart': i} - inst.storagepool_update(name, params) - rollback.prependDefer(inst.storagepool_update, name, - ori_params) poolinfo = inst.storagepool_lookup(name) - self.assertEquals(i, poolinfo['autostart']) - inst.storagepool_update(name, ori_params) + self.assertEquals('active', poolinfo['state']) + + autostart = poolinfo['autostart'] + ori_params = {'autostart': + True} if autostart else {'autostart': False} + for i in [True, False]: + params = {'autostart': i} + inst.storagepool_update(name, params) + rollback.prependDefer(inst.storagepool_update, name, + ori_params) + poolinfo = inst.storagepool_lookup(name) + self.assertEquals(i, poolinfo['autostart']) + inst.storagepool_update(name, ori_params)
pools = inst.storagepools_get_list() self.assertIn('default', pools)

Reviewed-by: ShaoHe Feng <shaohef@linux.vnet.ibm.com> On 01/02/2014 05:58 PM, Zhou Zheng Sheng wrote:
This patch adds iSCSI storage pool test to test_model.ModelTests.test_storagepool. It firstly checks if there exist an iSCSI target named "iqn.2013-12.localhost.kimchiUnitTest" on local host. If not, skip the test; if yes, continue to run the test already defined in test_storagepool.
v2 -> v3 Do not create dir for the pool, pool.build() called by storagepools_create() does this automatically.
v3 -> v5 No changes.
Signed-off-by: Zhou Zheng Sheng <zhshzhou@linux.vnet.ibm.com> --- tests/test_model.py | 91 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 40 deletions(-)
diff --git a/tests/test_model.py b/tests/test_model.py index c03cc3f..fa5c1d2 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -38,6 +38,7 @@ import utils from kimchi import netinfo from kimchi.exception import InvalidOperation, InvalidParameter from kimchi.exception import NotFoundError, OperationFailed +from kimchi.iscsi import TargetClient from kimchi.rollbackcontext import RollbackContext
@@ -116,49 +117,59 @@ class ModelTests(unittest.TestCase): def test_storagepool(self): inst = kimchi.model.Model('qemu:///system', self.tmp_store)
- with RollbackContext() as rollback: - path = '/tmp/kimchi-images' - name = 'test-pool' - if not os.path.exists(path): - os.mkdir(path) + poolDefs = [ + {'type': 'dir', + 'name': 'unitTestDirPool', + 'path': '/tmp/kimchi-images'}, + {'type': 'iscsi', + 'name': 'unitTestISCSIPool', + 'source': {'host': '127.0.0.1', + 'target': 'iqn.2013-12.localhost.kimchiUnitTest'}}]
- pools = inst.storagepools_get_list() - num = len(pools) + 1 + for poolDef in poolDefs: + with RollbackContext() as rollback: + path = poolDef.get('path') + name = poolDef['name'] + + if poolDef['type'] == 'iscsi': + if not TargetClient(**poolDef['source']).validate(): + continue + + pools = inst.storagepools_get_list() + num = len(pools) + 1 + + inst.storagepools_create(poolDef) + rollback.prependDefer(inst.storagepool_delete, name) + + pools = inst.storagepools_get_list() + self.assertEquals(num, len(pools)) + + poolinfo = inst.storagepool_lookup(name) + if path is not None: + self.assertEquals(path, poolinfo['path']) + self.assertEquals('inactive', poolinfo['state']) + if poolinfo['type'] == 'dir': + self.assertEquals(True, poolinfo['autostart']) + else: + self.assertEquals(False, poolinfo['autostart']) + + inst.storagepool_activate(name) + rollback.prependDefer(inst.storagepool_deactivate, name)
- args = {'name': name, - 'path': path, - 'type': 'dir'} - inst.storagepools_create(args) - rollback.prependDefer(inst.storagepool_delete, name) - - pools = inst.storagepools_get_list() - self.assertEquals(num, len(pools)) - - poolinfo = inst.storagepool_lookup(name) - self.assertEquals(path, poolinfo['path']) - self.assertEquals('inactive', poolinfo['state']) - if poolinfo['type'] == 'dir': - self.assertEquals(True, poolinfo['autostart']) - else: - self.assertEquals(False, poolinfo['autostart']) - - inst.storagepool_activate(name) - rollback.prependDefer(inst.storagepool_deactivate, name) - - poolinfo = inst.storagepool_lookup(name) - self.assertEquals('active', poolinfo['state']) - - autostart = poolinfo['autostart'] - ori_params = {'autostart': - True} if autostart else {'autostart': False} - for i in [True, False]: - params = {'autostart': i} - inst.storagepool_update(name, params) - rollback.prependDefer(inst.storagepool_update, name, - ori_params) poolinfo = inst.storagepool_lookup(name) - self.assertEquals(i, poolinfo['autostart']) - inst.storagepool_update(name, ori_params) + self.assertEquals('active', poolinfo['state']) + + autostart = poolinfo['autostart'] + ori_params = {'autostart': + True} if autostart else {'autostart': False} + for i in [True, False]: + params = {'autostart': i} + inst.storagepool_update(name, params) + rollback.prependDefer(inst.storagepool_update, name, + ori_params) + poolinfo = inst.storagepool_lookup(name) + self.assertEquals(i, poolinfo['autostart']) + inst.storagepool_update(name, ori_params)
pools = inst.storagepools_get_list() self.assertIn('default', pools)
-- Thanks and best regards! Sheldon Feng(冯少合)<shaohef@linux.vnet.ibm.com> IBM Linux Technology Center
participants (4)
-
Aline Manera
-
Royce Lv
-
Sheldon
-
Zhou Zheng Sheng