diff -Nru python-rtslib-fb-2.1.69/debian/changelog python-rtslib-fb-2.1.71/debian/changelog --- python-rtslib-fb-2.1.69/debian/changelog 2019-10-21 08:22:07.000000000 +0000 +++ python-rtslib-fb-2.1.71/debian/changelog 2020-02-25 13:36:13.000000000 +0000 @@ -1,3 +1,9 @@ +python-rtslib-fb (2.1.71-0ubuntu1) focal; urgency=medium + + * New upstream release. + + -- James Page Tue, 25 Feb 2020 13:36:13 +0000 + python-rtslib-fb (2.1.69-3) unstable; urgency=medium [ Ondřej Nový ] diff -Nru python-rtslib-fb-2.1.69/debian/control python-rtslib-fb-2.1.71/debian/control --- python-rtslib-fb-2.1.69/debian/control 2019-10-21 08:22:07.000000000 +0000 +++ python-rtslib-fb-2.1.71/debian/control 2020-02-25 11:16:40.000000000 +0000 @@ -1,7 +1,8 @@ Source: python-rtslib-fb Section: python Priority: optional -Maintainer: Debian OpenStack +Maintainer: Ubuntu Developers +XSBC-Original-Maintainer: Debian OpenStack Uploaders: Thomas Goirand , Andy Grover , diff -Nru python-rtslib-fb-2.1.69/debian/patches/fix-path-of-etc-saveconfig.json.patch python-rtslib-fb-2.1.71/debian/patches/fix-path-of-etc-saveconfig.json.patch --- python-rtslib-fb-2.1.69/debian/patches/fix-path-of-etc-saveconfig.json.patch 2019-10-21 08:22:07.000000000 +0000 +++ python-rtslib-fb-2.1.71/debian/patches/fix-path-of-etc-saveconfig.json.patch 2020-02-25 11:16:50.000000000 +0000 @@ -5,10 +5,8 @@ Forwarded: no Last-Update: 2014-01-25 -Index: python-rtslib-fb/scripts/targetctl -=================================================================== ---- python-rtslib-fb.orig/scripts/targetctl -+++ python-rtslib-fb/scripts/targetctl +--- a/scripts/targetctl ++++ b/scripts/targetctl @@ -28,7 +28,7 @@ from rtslib_fb import RTSRoot import os import sys @@ -18,11 +16,9 @@ err = sys.stderr def usage(): -Index: python-rtslib-fb/rtslib/root.py -=================================================================== ---- python-rtslib-fb.orig/rtslib/root.py -+++ python-rtslib-fb/rtslib/root.py -@@ -33,7 +33,7 @@ from .utils import dict_remove, set_attr +--- a/rtslib/root.py ++++ b/rtslib/root.py +@@ -34,7 +34,7 @@ from .utils import dict_remove, set_attr from .utils import fread, fwrite from .alua import ALUATargetPortGroup @@ -31,7 +27,7 @@ class RTSRoot(CFSNode): ''' -@@ -64,7 +64,7 @@ class RTSRoot(CFSNode): +@@ -65,7 +65,7 @@ class RTSRoot(CFSNode): # this should match the kernel target driver default db dir _default_dbroot = "/var/target" # this is where the target DB is to be located (instead of the default) @@ -40,7 +36,7 @@ def __init__(self): ''' -@@ -376,7 +376,7 @@ class RTSRoot(CFSNode): +@@ -441,7 +441,7 @@ class RTSRoot(CFSNode): def save_to_file(self, save_file=None, so_path=None): ''' Write the configuration in json format to a file. @@ -49,8 +45,8 @@ ''' if not save_file: save_file = default_save_file -@@ -399,7 +399,7 @@ class RTSRoot(CFSNode): - def restore_from_file(self, restore_file=None, clear_existing=True, abort_on_error=False): +@@ -469,7 +469,7 @@ class RTSRoot(CFSNode): + abort_on_error=False): ''' Restore the configuration from a file in json format. - Restore file defaults to '/etc/targets/saveconfig.json'. diff -Nru python-rtslib-fb-2.1.69/rtslib/__init__.py python-rtslib-fb-2.1.71/rtslib/__init__.py --- python-rtslib-fb-2.1.69/rtslib/__init__.py 2018-09-18 06:47:13.000000000 +0000 +++ python-rtslib-fb-2.1.71/rtslib/__init__.py 2019-11-06 12:35:08.000000000 +0000 @@ -16,7 +16,7 @@ under the License. ''' -if __name__ == "rtslib": +if __name__ == "rtslib-fb": from warnings import warn warn("'rtslib' package name for rtslib-fb is deprecated, please" + " instead import 'rtslib_fb'", UserWarning, stacklevel=2) @@ -36,8 +36,8 @@ from .alua import ALUATargetPortGroup -__version__ = 'GIT_VERSION' +__version__ = '2.1.71' __author__ = "Jerome Martin " -__url__ = "http://www.risingtidesystems.com" -__description__ = "API for RisingTide Systems generic SCSI target." -__license__ = __doc__ +__url__ = 'http://github.com/open-iscsi/rtslib-fb' +__description__ = 'API for Linux kernel SCSI target (aka LIO)' +__license__ = 'Apache 2.0' diff -Nru python-rtslib-fb-2.1.69/rtslib/node.py python-rtslib-fb-2.1.71/rtslib/node.py --- python-rtslib-fb-2.1.69/rtslib/node.py 2018-09-18 06:47:13.000000000 +0000 +++ python-rtslib-fb-2.1.71/rtslib/node.py 2019-11-06 12:35:08.000000000 +0000 @@ -81,59 +81,78 @@ raise RTSLibNotInCFS("This %s does not exist in configFS" % self.__class__.__name__) - def _list_files(self, path, writable=None): + def _list_files(self, path, writable=None, readable=None): ''' List files under a path depending on their owner's write permissions. @param path: The path under which the files are expected to be. If the path itself is not a directory, an empty list will be returned. @type path: str - @param writable: If None (default), returns all parameters, if True, - returns read-write parameters, if False, returns just the read-only - parameters. + @param writable: If None (default), return all files despite their + writability. If True, return only writable files. If False, return + only non-writable files. @type writable: bool or None - @return: List of file names filtered according to their write perms. + @param readable: If None (default), return all files despite their + readability. If True, return only readable files. If False, return + only non-readable files. + @type readable: bool or None + @return: List of file names filtered according to their + read/write perms. ''' if not os.path.isdir(path): return [] - if writable is None: + if writable is None and readable is None: names = os.listdir(path) - elif writable: - names = [name for name in os.listdir(path) - if (os.stat("%s/%s" % (path, name))[stat.ST_MODE] \ - & stat.S_IWUSR)] else: - names = [os.path.basename(name) for name in os.listdir(path) - if not (os.stat("%s/%s" % (path, name))[stat.ST_MODE] \ - & stat.S_IWUSR)] + names = [] + for name in os.listdir(path): + sres = os.stat("%s/%s" % (path, name)) + if writable is not None: + if writable != ((sres[stat.ST_MODE] & stat.S_IWUSR) == \ + stat.S_IWUSR): + continue + if readable is not None: + if readable != ((sres[stat.ST_MODE] & stat.S_IRUSR) == \ + stat.S_IRUSR): + continue + names.append(name) + names.sort() return names # CFSNode public stuff - def list_parameters(self, writable=None): + def list_parameters(self, writable=None, readable=None): ''' - @param writable: If None (default), returns all parameters, if True, - returns read-write parameters, if False, returns just the read-only - parameters. + @param writable: If None (default), return all parameters despite + their writability. If True, return only writable parameters. If + False, return only non-writable parameters. @type writable: bool or None + @param readable: If None (default), return all parameters despite + their readability. If True, return only readable parameters. If + False, return only non-readable parameters. + @type readable: bool or None @return: The list of existing RFC-3720 parameter names. ''' self._check_self() path = "%s/param" % self.path - return self._list_files(path, writable) + return self._list_files(path, writable, readable) - def list_attributes(self, writable=None): + def list_attributes(self, writable=None, readable=None): ''' - @param writable: If None (default), returns all attributes, if True, - returns read-write attributes, if False, returns just the read-only - attributes. + @param writable: If None (default), return all files despite their + writability. If True, return only writable files. If False, return + only non-writable files. @type writable: bool or None + @param readable: If None (default), return all files despite their + readability. If True, return only readable files. If False, return + only non-readable files. + @type readable: bool or None @return: A list of existing attribute names as strings. ''' self._check_self() path = "%s/attrib" % self.path - return self._list_files(path, writable) + return self._list_files(path, writable, readable) def set_attribute(self, attribute, value): ''' @@ -220,14 +239,14 @@ d = {} attrs = {} params = {} - for item in self.list_attributes(writable=True): + for item in self.list_attributes(writable=True, readable=True): try: attrs[item] = int(self.get_attribute(item)) except ValueError: attrs[item] = self.get_attribute(item) if attrs: d['attributes'] = attrs - for item in self.list_parameters(writable=True): + for item in self.list_parameters(writable=True, readable=True): params[item] = self.get_parameter(item) if params: d['parameters'] = params diff -Nru python-rtslib-fb-2.1.69/rtslib/root.py python-rtslib-fb-2.1.71/rtslib/root.py --- python-rtslib-fb-2.1.69/rtslib/root.py 2018-09-18 06:47:13.000000000 +0000 +++ python-rtslib-fb-2.1.71/rtslib/root.py 2019-11-06 12:35:08.000000000 +0000 @@ -23,6 +23,7 @@ import json import glob import errno +import shutil from .node import CFSNode from .target import Target @@ -224,7 +225,7 @@ if '/backstores/' + sobj['plugin'] + '/' + sobj['name'] == so_path: # Merge StorageObj if fetch_cur_so: - saveconf['storage_objects'][sidx] = current_so; + saveconf['storage_objects'][sidx] = current_so # Remove StorageObj else: saveconf['storage_objects'].remove(saveconf['storage_objects'][sidx]) @@ -243,7 +244,7 @@ if lun['storage_object'] == so_path: # Merge target if fetch_cur_tg: - saveconf['targets'][tidx] = current_tg; + saveconf['targets'][tidx] = current_tg # Remove target else: saveconf['targets'].remove(saveconf['targets'][tidx]) @@ -275,7 +276,7 @@ if f.discovery_enable_auth] return d - def clear_existing(self, confirm=False): + def clear_existing(self, target=None, storage_object=None, confirm=False): ''' Remove entire current configuration. ''' @@ -284,18 +285,40 @@ # Targets depend on storage objects, delete them first. for t in self.targets: - t.delete() + # * Delete the single matching target if target=iqn.xxx was supplied + # with restoreconfig command + # * If only storage_object=blockx option is supplied then do not + # delete any targets + # * If restoreconfig was not supplied with neither target=iqn.xxx + # nor storage_object=blockx then delete all targets + if (not storage_object and not target) or (target and t.wwn == target): + t.delete() + if target: + break + for fm in (f for f in self.fabric_modules if f.has_feature("discovery_auth")): fm.clear_discovery_auth_settings() + for so in self.storage_objects: - so.delete() + # * Delete the single matching storage object if storage_object=blockx + # was supplied with restoreconfig command + # * If only target=iqn.xxx option is supplied then do not + # delete any storage_object's + # * If restoreconfig was not supplied with neither target=iqn.xxx + # nor storage_object=blockx then delete all storage_object's + if (not storage_object and not target) or (storage_object and so.name == storage_object): + so.delete() + if storage_object: + break # If somehow some hbas still exist (no storage object within?) clean # them up too. - for hba_dir in glob.glob("%s/core/*_*" % self.configfs_dir): - os.rmdir(hba_dir) + if not (storage_object or target): + for hba_dir in glob.glob("%s/core/*_*" % self.configfs_dir): + os.rmdir(hba_dir) - def restore(self, config, clear_existing=False, abort_on_error=False): + def restore(self, config, target=None, storage_object=None, + clear_existing=False, abort_on_error=False): ''' Takes a dict generated by dump() and reconfigures the target to match. Returns list of non-fatal errors that were encountered. @@ -303,10 +326,22 @@ is True. ''' if clear_existing: - self.clear_existing(confirm=True) + self.clear_existing(target, storage_object, confirm=True) elif any(self.storage_objects) or any(self.targets): - raise RTSLibError("storageobjects or targets present, not restoring") - + if any(self.storage_objects): + for config_so in config.get('storage_objects', []): + for loaded_so in self.storage_objects: + if config_so['name'] == loaded_so.name and \ + config_so['plugin'] == loaded_so.plugin: + raise RTSLibError("storageobject '%s:%s' exist not restoring" + %(loaded_so.plugin, loaded_so.name)) + + if any(self.targets): + for config_tg in config.get('targets', []): + for loaded_tg in self.targets: + if config_tg['wwn'] == loaded_tg.wwn: + raise RTSLibError("target with wwn %s exist, not restoring" + %(loaded_tg.wwn)) errors = [] if abort_on_error: @@ -320,30 +355,45 @@ if 'name' not in so: err_func("'name' not defined in storage object %d" % index) continue - try: - so_cls = so_mapping[so['plugin']] - except KeyError: - err_func("'plugin' not defined or invalid in storageobject %s" % so['name']) - continue - kwargs = so.copy() - dict_remove(kwargs, ('exists', 'attributes', 'plugin', 'buffered_mode', 'alua_tpgs')) - try: - so_obj = so_cls(**kwargs) - except Exception as e: - err_func("Could not create StorageObject %s: %s" % (so['name'], e)) - continue - # Custom err func to include block name - def so_err_func(x): - return err_func("Storage Object %s/%s: %s" % (so['plugin'], so['name'], x)) - - set_attributes(so_obj, so.get('attributes', {}), so_err_func) - - for alua_tpg in so.get('alua_tpgs', {}): - try: - ALUATargetPortGroup.setup(so_obj, alua_tpg, err_func) - except RTSLibALUANotSupported: - pass + # * Restore/load the single matching storage object if + # storage_object=blockx was supplied with restoreconfig command + # * In case if no storage_object was supplied but only target=iqn.xxx + # was supplied then do not load any storage_object's + # * If neither storage_object nor target option was supplied to + # restoreconfig, then go ahead and load all storage_object's + if (not storage_object and not target) or (storage_object and so['name'] == storage_object): + try: + so_cls = so_mapping[so['plugin']] + except KeyError: + err_func("'plugin' not defined or invalid in storageobject %s" % so['name']) + if storage_object: + break + continue + kwargs = so.copy() + dict_remove(kwargs, ('exists', 'attributes', 'plugin', 'buffered_mode', 'alua_tpgs')) + try: + so_obj = so_cls(**kwargs) + except Exception as e: + err_func("Could not create StorageObject %s: %s" % (so['name'], e)) + if storage_object: + break + continue + + # Custom err func to include block name + def so_err_func(x): + return err_func("Storage Object %s/%s: %s" % (so['plugin'], so['name'], x)) + + set_attributes(so_obj, so.get('attributes', {}), so_err_func) + + for alua_tpg in so.get('alua_tpgs', {}): + try: + ALUATargetPortGroup.setup(so_obj, alua_tpg, err_func) + except RTSLibALUANotSupported: + pass + + if storage_object: + break # Don't need to create fabric modules for index, fm in enumerate(config.get('fabric_modules', [])): @@ -359,17 +409,32 @@ if 'wwn' not in t: err_func("'wwn' not defined in target %d" % index) continue - if 'fabric' not in t: - err_func("target %s missing 'fabric' field" % t['wwn']) - continue - if t['fabric'] not in (f.name for f in self.fabric_modules): - err_func("Unknown fabric '%s'" % t['fabric']) - continue - fm_obj = FabricModule(t['fabric']) + # * Restore/load the single matching target if target=iqn.xxx was + # supplied with restoreconfig command + # * In case if no target was supplied but only storage_object=blockx + # was supplied then do not load any targets + # * If neither storage_object nor target option was supplied to + # restoreconfig, then go ahead and load all targets + if (not storage_object and not target) or (target and t['wwn'] == target): + if 'fabric' not in t: + err_func("target %s missing 'fabric' field" % t['wwn']) + if target: + break + continue + if t['fabric'] not in (f.name for f in self.fabric_modules): + err_func("Unknown fabric '%s'" % t['fabric']) + if target: + break + continue + + fm_obj = FabricModule(t['fabric']) - # Instantiate target - Target.setup(fm_obj, t, err_func) + # Instantiate target + Target.setup(fm_obj, t, err_func) + + if target: + break return errors @@ -386,7 +451,9 @@ else: saveconf = self.dump() - with open(save_file+".temp", "w+") as f: + tmp_file = save_file + ".temp" + + with open(tmp_file, "w+") as f: os.fchmod(f.fileno(), stat.S_IRUSR | stat.S_IWUSR) f.write(json.dumps(saveconf, sort_keys=True, indent=2)) f.write("\n") @@ -394,9 +461,12 @@ os.fsync(f.fileno()) f.close() - os.rename(save_file+".temp", save_file) + shutil.copyfile(tmp_file, save_file) + os.remove(tmp_file) - def restore_from_file(self, restore_file=None, clear_existing=True, abort_on_error=False): + def restore_from_file(self, restore_file=None, clear_existing=True, + target=None, storage_object=None, + abort_on_error=False): ''' Restore the configuration from a file in json format. Restore file defaults to '/etc/targets/saveconfig.json'. @@ -408,7 +478,8 @@ with open(restore_file, "r") as f: config = json.loads(f.read()) - return self.restore(config, clear_existing=clear_existing, + return self.restore(config, target, storage_object, + clear_existing=clear_existing, abort_on_error=abort_on_error) def invalidate_caches(self): diff -Nru python-rtslib-fb-2.1.69/rtslib/target.py python-rtslib-fb-2.1.71/rtslib/target.py --- python-rtslib-fb-2.1.69/rtslib/target.py 2018-09-18 06:47:13.000000000 +0000 +++ python-rtslib-fb-2.1.71/rtslib/target.py 2019-11-06 12:35:08.000000000 +0000 @@ -1302,11 +1302,11 @@ for mem in self._mem_func(self): setattr(mem, prop, value) - def list_attributes(self, writable=None): - return self._get_first_member().list_attributes(writable) + def list_attributes(self, writable=None, readable=None): + return self._get_first_member().list_attributes(writable, readable) - def list_parameters(self, writable=None): - return self._get_first_member().list_parameters(writable) + def list_parameters(self, writable=None, readable=None): + return self._get_first_member().list_parameters(writable, readable) def set_attribute(self, attribute, value): for obj in self._mem_func(self): diff -Nru python-rtslib-fb-2.1.69/rtslib/tcm.py python-rtslib-fb-2.1.71/rtslib/tcm.py --- python-rtslib-fb-2.1.69/rtslib/tcm.py 2018-09-18 06:47:13.000000000 +0000 +++ python-rtslib-fb-2.1.71/rtslib/tcm.py 2019-11-06 12:35:08.000000000 +0000 @@ -56,14 +56,14 @@ def __repr__(self): return "<%s %s/%s>" % (self.__class__.__name__, self.plugin, self.name) - def __init__(self, name, mode): + def __init__(self, name, mode, index=None): super(StorageObject, self).__init__() if "/" in name or " " in name or "\t" in name or "\n" in name: raise RTSLibError("A storage object's name cannot contain " " /, newline or spaces/tabs") else: self._name = name - self._backstore = _Backstore(name, type(self), mode) + self._backstore = _Backstore(name, type(self), mode, index) self._path = "%s/%s" % (self._backstore.path, self.name) self.plugin = self._backstore.plugin try: @@ -126,8 +126,8 @@ Build a StorageObject of the correct type from a configfs path. ''' so_name = os.path.basename(path) - so_type = path.split("/")[-2].rsplit("_", 1)[0] - return so_mapping[so_type](so_name) + so_type, so_index = path.split("/")[-2].rsplit("_", 1) + return so_mapping[so_type](so_name, index=so_index) def _get_wwn(self): self._check_self() @@ -325,7 +325,7 @@ # PSCSIStorageObject private stuff - def __init__(self, name, dev=None): + def __init__(self, name, dev=None, index=None): ''' A PSCSIStorageObject can be instantiated in two ways: - B{Creation mode}: If I{dev} is specified, the underlying configFS @@ -347,14 +347,14 @@ @return: A PSCSIStorageObject object. ''' if dev is not None: - super(PSCSIStorageObject, self).__init__(name, 'create') + super(PSCSIStorageObject, self).__init__(name, 'create', index) try: self._configure(dev) except: self.delete() raise else: - super(PSCSIStorageObject, self).__init__(name, 'lookup') + super(PSCSIStorageObject, self).__init__(name, 'lookup', index) def _configure(self, dev): self._check_self() @@ -477,7 +477,7 @@ # RDMCPStorageObject private stuff - def __init__(self, name, size=None, wwn=None, nullio=False): + def __init__(self, name, size=None, wwn=None, nullio=False, index=None): ''' A RDMCPStorageObject can be instantiated in two ways: - B{Creation mode}: If I{size} is specified, the underlying @@ -502,14 +502,14 @@ ''' if size is not None: - super(RDMCPStorageObject, self).__init__(name, 'create') + super(RDMCPStorageObject, self).__init__(name, 'create', index) try: self._configure(size, wwn, nullio) except: self.delete() raise else: - super(RDMCPStorageObject, self).__init__(name, 'lookup') + super(RDMCPStorageObject, self).__init__(name, 'lookup', index) def _configure(self, size, wwn, nullio): self._check_self() @@ -575,7 +575,7 @@ # FileIOStorageObject private stuff def __init__(self, name, dev=None, size=None, - wwn=None, write_back=False, aio=False): + wwn=None, write_back=False, aio=False, index=None): ''' A FileIOStorageObject can be instantiated in two ways: - B{Creation mode}: If I{dev} and I{size} are specified, the @@ -607,14 +607,14 @@ ''' if dev is not None: - super(FileIOStorageObject, self).__init__(name, 'create') + super(FileIOStorageObject, self).__init__(name, 'create', index) try: self._configure(dev, size, wwn, write_back, aio) except: self.delete() raise else: - super(FileIOStorageObject, self).__init__(name, 'lookup') + super(FileIOStorageObject, self).__init__(name, 'lookup', index) def _configure(self, dev, size, wwn, write_back, aio): self._check_self() @@ -707,7 +707,7 @@ # BlockStorageObject private stuff def __init__(self, name, dev=None, wwn=None, readonly=False, - write_back=False): + write_back=False, index=None): ''' A BlockIOStorageObject can be instantiated in two ways: - B{Creation mode}: If I{dev} is specified, the underlying configFS @@ -733,14 +733,14 @@ ''' if dev is not None: - super(BlockStorageObject, self).__init__(name, 'create') + super(BlockStorageObject, self).__init__(name, 'create', index) try: self._configure(dev, wwn, readonly) except: self.delete() raise else: - super(BlockStorageObject, self).__init__(name, 'lookup') + super(BlockStorageObject, self).__init__(name, 'lookup', index) def _configure(self, dev, wwn, readonly): self._check_self() @@ -808,7 +808,7 @@ ''' def __init__(self, name, config=None, size=None, wwn=None, - hw_max_sectors=None, control=None): + hw_max_sectors=None, control=None, index=None): ''' @param name: The name of the UserBackedStorageObject. @type name: string @@ -834,14 +834,14 @@ if '/' not in config: raise RTSLibError("'config' must contain a '/' separating subtype " "from its configuration string") - super(UserBackedStorageObject, self).__init__(name, 'create') + super(UserBackedStorageObject, self).__init__(name, 'create', index) try: self._configure(config, size, wwn, hw_max_sectors, control) except: self.delete() raise else: - super(UserBackedStorageObject, self).__init__(name, 'lookup') + super(UserBackedStorageObject, self).__init__(name, 'lookup', index) def _configure(self, config, size, wwn, hw_max_sectors, control): self._check_self() @@ -873,7 +873,10 @@ val = self._parse_info('MaxDataAreaMB') if val != "NULL": tuples.append("max_data_area_mb=%s" % val) - # 2. add next ... + val = self.get_attribute('hw_block_size') + if val != "NULL": + tuples.append("hw_block_size=%s" % val) + # 3. add next ... return ",".join(tuples) @@ -958,15 +961,16 @@ Created by storageobject ctor before SO configfs entry. """ - def __init__(self, name, storage_object_cls, mode): + def __init__(self, name, storage_object_cls, mode, index=None): super(_Backstore, self).__init__() self._so_cls = storage_object_cls self._plugin = bs_params[self._so_cls]['name'] dirp = bs_params[self._so_cls].get("alt_dirprefix", self._plugin) + # if the caller knows the index then skip the cache global bs_cache - if not bs_cache: + if index is None and not bs_cache: for dir in glob.iglob("%s/core/*_*/*/" % self.configfs_dir): parts = dir.split("/") bs_name = parts[-2] @@ -974,14 +978,16 @@ current_key = "%s/%s" % (bs_dirp, bs_name) bs_cache[current_key] = int(bs_index) - # mapping in cache? self._lookup_key = "%s/%s" % (dirp, name) - self._index = bs_cache.get(self._lookup_key, None) + if index is None: + self._index = bs_cache.get(self._lookup_key, None) + if self._index != None and mode == 'create': + raise RTSLibError("Storage object %s/%s exists" % + (self._plugin, name)) + else: + self._index = int(index) - if self._index != None and mode == 'create': - raise RTSLibError("Storage object %s/%s exists" % - (self._plugin, name)) - elif self._index == None: + if self._index == None: if mode == 'lookup': raise RTSLibNotInCFS("Storage object %s/%s not found" % (self._plugin, name)) @@ -1002,12 +1008,14 @@ try: self._create_in_cfs_ine(mode) except: - del bs_cache[self._lookup_key] + if self._lookup_key in bs_cache: + del bs_cache[self._lookup_key] raise def delete(self): super(_Backstore, self).delete() - del bs_cache[self._lookup_key] + if self._lookup_key in bs_cache: + del bs_cache[self._lookup_key] def _get_index(self): return self._index diff -Nru python-rtslib-fb-2.1.69/rtslib/utils.py python-rtslib-fb-2.1.71/rtslib/utils.py --- python-rtslib-fb-2.1.69/rtslib/utils.py 2018-09-18 06:47:13.000000000 +0000 +++ python-rtslib-fb-2.1.71/rtslib/utils.py 2019-11-06 12:35:08.000000000 +0000 @@ -134,6 +134,9 @@ except (KeyError, UnicodeDecodeError, ValueError): return 0 + if device['DEVTYPE'] == 'partition': + attributes = device.parent.attributes + try: logical_block_size = attributes.asint('queue/logical_block_size') except (KeyError, UnicodeDecodeError, ValueError): diff -Nru python-rtslib-fb-2.1.69/rtslib_fb/__init__.py python-rtslib-fb-2.1.71/rtslib_fb/__init__.py --- python-rtslib-fb-2.1.69/rtslib_fb/__init__.py 2018-09-18 06:47:13.000000000 +0000 +++ python-rtslib-fb-2.1.71/rtslib_fb/__init__.py 2019-11-06 12:35:08.000000000 +0000 @@ -16,7 +16,7 @@ under the License. ''' -if __name__ == "rtslib": +if __name__ == "rtslib-fb": from warnings import warn warn("'rtslib' package name for rtslib-fb is deprecated, please" + " instead import 'rtslib_fb'", UserWarning, stacklevel=2) @@ -36,8 +36,8 @@ from .alua import ALUATargetPortGroup -__version__ = 'GIT_VERSION' +__version__ = '2.1.71' __author__ = "Jerome Martin " -__url__ = "http://www.risingtidesystems.com" -__description__ = "API for RisingTide Systems generic SCSI target." -__license__ = __doc__ +__url__ = 'http://github.com/open-iscsi/rtslib-fb' +__description__ = 'API for Linux kernel SCSI target (aka LIO)' +__license__ = 'Apache 2.0' diff -Nru python-rtslib-fb-2.1.69/rtslib_fb/node.py python-rtslib-fb-2.1.71/rtslib_fb/node.py --- python-rtslib-fb-2.1.69/rtslib_fb/node.py 2018-09-18 06:47:13.000000000 +0000 +++ python-rtslib-fb-2.1.71/rtslib_fb/node.py 2019-11-06 12:35:08.000000000 +0000 @@ -81,59 +81,78 @@ raise RTSLibNotInCFS("This %s does not exist in configFS" % self.__class__.__name__) - def _list_files(self, path, writable=None): + def _list_files(self, path, writable=None, readable=None): ''' List files under a path depending on their owner's write permissions. @param path: The path under which the files are expected to be. If the path itself is not a directory, an empty list will be returned. @type path: str - @param writable: If None (default), returns all parameters, if True, - returns read-write parameters, if False, returns just the read-only - parameters. + @param writable: If None (default), return all files despite their + writability. If True, return only writable files. If False, return + only non-writable files. @type writable: bool or None - @return: List of file names filtered according to their write perms. + @param readable: If None (default), return all files despite their + readability. If True, return only readable files. If False, return + only non-readable files. + @type readable: bool or None + @return: List of file names filtered according to their + read/write perms. ''' if not os.path.isdir(path): return [] - if writable is None: + if writable is None and readable is None: names = os.listdir(path) - elif writable: - names = [name for name in os.listdir(path) - if (os.stat("%s/%s" % (path, name))[stat.ST_MODE] \ - & stat.S_IWUSR)] else: - names = [os.path.basename(name) for name in os.listdir(path) - if not (os.stat("%s/%s" % (path, name))[stat.ST_MODE] \ - & stat.S_IWUSR)] + names = [] + for name in os.listdir(path): + sres = os.stat("%s/%s" % (path, name)) + if writable is not None: + if writable != ((sres[stat.ST_MODE] & stat.S_IWUSR) == \ + stat.S_IWUSR): + continue + if readable is not None: + if readable != ((sres[stat.ST_MODE] & stat.S_IRUSR) == \ + stat.S_IRUSR): + continue + names.append(name) + names.sort() return names # CFSNode public stuff - def list_parameters(self, writable=None): + def list_parameters(self, writable=None, readable=None): ''' - @param writable: If None (default), returns all parameters, if True, - returns read-write parameters, if False, returns just the read-only - parameters. + @param writable: If None (default), return all parameters despite + their writability. If True, return only writable parameters. If + False, return only non-writable parameters. @type writable: bool or None + @param readable: If None (default), return all parameters despite + their readability. If True, return only readable parameters. If + False, return only non-readable parameters. + @type readable: bool or None @return: The list of existing RFC-3720 parameter names. ''' self._check_self() path = "%s/param" % self.path - return self._list_files(path, writable) + return self._list_files(path, writable, readable) - def list_attributes(self, writable=None): + def list_attributes(self, writable=None, readable=None): ''' - @param writable: If None (default), returns all attributes, if True, - returns read-write attributes, if False, returns just the read-only - attributes. + @param writable: If None (default), return all files despite their + writability. If True, return only writable files. If False, return + only non-writable files. @type writable: bool or None + @param readable: If None (default), return all files despite their + readability. If True, return only readable files. If False, return + only non-readable files. + @type readable: bool or None @return: A list of existing attribute names as strings. ''' self._check_self() path = "%s/attrib" % self.path - return self._list_files(path, writable) + return self._list_files(path, writable, readable) def set_attribute(self, attribute, value): ''' @@ -220,14 +239,14 @@ d = {} attrs = {} params = {} - for item in self.list_attributes(writable=True): + for item in self.list_attributes(writable=True, readable=True): try: attrs[item] = int(self.get_attribute(item)) except ValueError: attrs[item] = self.get_attribute(item) if attrs: d['attributes'] = attrs - for item in self.list_parameters(writable=True): + for item in self.list_parameters(writable=True, readable=True): params[item] = self.get_parameter(item) if params: d['parameters'] = params diff -Nru python-rtslib-fb-2.1.69/rtslib_fb/root.py python-rtslib-fb-2.1.71/rtslib_fb/root.py --- python-rtslib-fb-2.1.69/rtslib_fb/root.py 2018-09-18 06:47:13.000000000 +0000 +++ python-rtslib-fb-2.1.71/rtslib_fb/root.py 2019-11-06 12:35:08.000000000 +0000 @@ -23,6 +23,7 @@ import json import glob import errno +import shutil from .node import CFSNode from .target import Target @@ -224,7 +225,7 @@ if '/backstores/' + sobj['plugin'] + '/' + sobj['name'] == so_path: # Merge StorageObj if fetch_cur_so: - saveconf['storage_objects'][sidx] = current_so; + saveconf['storage_objects'][sidx] = current_so # Remove StorageObj else: saveconf['storage_objects'].remove(saveconf['storage_objects'][sidx]) @@ -243,7 +244,7 @@ if lun['storage_object'] == so_path: # Merge target if fetch_cur_tg: - saveconf['targets'][tidx] = current_tg; + saveconf['targets'][tidx] = current_tg # Remove target else: saveconf['targets'].remove(saveconf['targets'][tidx]) @@ -275,7 +276,7 @@ if f.discovery_enable_auth] return d - def clear_existing(self, confirm=False): + def clear_existing(self, target=None, storage_object=None, confirm=False): ''' Remove entire current configuration. ''' @@ -284,18 +285,40 @@ # Targets depend on storage objects, delete them first. for t in self.targets: - t.delete() + # * Delete the single matching target if target=iqn.xxx was supplied + # with restoreconfig command + # * If only storage_object=blockx option is supplied then do not + # delete any targets + # * If restoreconfig was not supplied with neither target=iqn.xxx + # nor storage_object=blockx then delete all targets + if (not storage_object and not target) or (target and t.wwn == target): + t.delete() + if target: + break + for fm in (f for f in self.fabric_modules if f.has_feature("discovery_auth")): fm.clear_discovery_auth_settings() + for so in self.storage_objects: - so.delete() + # * Delete the single matching storage object if storage_object=blockx + # was supplied with restoreconfig command + # * If only target=iqn.xxx option is supplied then do not + # delete any storage_object's + # * If restoreconfig was not supplied with neither target=iqn.xxx + # nor storage_object=blockx then delete all storage_object's + if (not storage_object and not target) or (storage_object and so.name == storage_object): + so.delete() + if storage_object: + break # If somehow some hbas still exist (no storage object within?) clean # them up too. - for hba_dir in glob.glob("%s/core/*_*" % self.configfs_dir): - os.rmdir(hba_dir) + if not (storage_object or target): + for hba_dir in glob.glob("%s/core/*_*" % self.configfs_dir): + os.rmdir(hba_dir) - def restore(self, config, clear_existing=False, abort_on_error=False): + def restore(self, config, target=None, storage_object=None, + clear_existing=False, abort_on_error=False): ''' Takes a dict generated by dump() and reconfigures the target to match. Returns list of non-fatal errors that were encountered. @@ -303,10 +326,22 @@ is True. ''' if clear_existing: - self.clear_existing(confirm=True) + self.clear_existing(target, storage_object, confirm=True) elif any(self.storage_objects) or any(self.targets): - raise RTSLibError("storageobjects or targets present, not restoring") - + if any(self.storage_objects): + for config_so in config.get('storage_objects', []): + for loaded_so in self.storage_objects: + if config_so['name'] == loaded_so.name and \ + config_so['plugin'] == loaded_so.plugin: + raise RTSLibError("storageobject '%s:%s' exist not restoring" + %(loaded_so.plugin, loaded_so.name)) + + if any(self.targets): + for config_tg in config.get('targets', []): + for loaded_tg in self.targets: + if config_tg['wwn'] == loaded_tg.wwn: + raise RTSLibError("target with wwn %s exist, not restoring" + %(loaded_tg.wwn)) errors = [] if abort_on_error: @@ -320,30 +355,45 @@ if 'name' not in so: err_func("'name' not defined in storage object %d" % index) continue - try: - so_cls = so_mapping[so['plugin']] - except KeyError: - err_func("'plugin' not defined or invalid in storageobject %s" % so['name']) - continue - kwargs = so.copy() - dict_remove(kwargs, ('exists', 'attributes', 'plugin', 'buffered_mode', 'alua_tpgs')) - try: - so_obj = so_cls(**kwargs) - except Exception as e: - err_func("Could not create StorageObject %s: %s" % (so['name'], e)) - continue - # Custom err func to include block name - def so_err_func(x): - return err_func("Storage Object %s/%s: %s" % (so['plugin'], so['name'], x)) - - set_attributes(so_obj, so.get('attributes', {}), so_err_func) - - for alua_tpg in so.get('alua_tpgs', {}): - try: - ALUATargetPortGroup.setup(so_obj, alua_tpg, err_func) - except RTSLibALUANotSupported: - pass + # * Restore/load the single matching storage object if + # storage_object=blockx was supplied with restoreconfig command + # * In case if no storage_object was supplied but only target=iqn.xxx + # was supplied then do not load any storage_object's + # * If neither storage_object nor target option was supplied to + # restoreconfig, then go ahead and load all storage_object's + if (not storage_object and not target) or (storage_object and so['name'] == storage_object): + try: + so_cls = so_mapping[so['plugin']] + except KeyError: + err_func("'plugin' not defined or invalid in storageobject %s" % so['name']) + if storage_object: + break + continue + kwargs = so.copy() + dict_remove(kwargs, ('exists', 'attributes', 'plugin', 'buffered_mode', 'alua_tpgs')) + try: + so_obj = so_cls(**kwargs) + except Exception as e: + err_func("Could not create StorageObject %s: %s" % (so['name'], e)) + if storage_object: + break + continue + + # Custom err func to include block name + def so_err_func(x): + return err_func("Storage Object %s/%s: %s" % (so['plugin'], so['name'], x)) + + set_attributes(so_obj, so.get('attributes', {}), so_err_func) + + for alua_tpg in so.get('alua_tpgs', {}): + try: + ALUATargetPortGroup.setup(so_obj, alua_tpg, err_func) + except RTSLibALUANotSupported: + pass + + if storage_object: + break # Don't need to create fabric modules for index, fm in enumerate(config.get('fabric_modules', [])): @@ -359,17 +409,32 @@ if 'wwn' not in t: err_func("'wwn' not defined in target %d" % index) continue - if 'fabric' not in t: - err_func("target %s missing 'fabric' field" % t['wwn']) - continue - if t['fabric'] not in (f.name for f in self.fabric_modules): - err_func("Unknown fabric '%s'" % t['fabric']) - continue - fm_obj = FabricModule(t['fabric']) + # * Restore/load the single matching target if target=iqn.xxx was + # supplied with restoreconfig command + # * In case if no target was supplied but only storage_object=blockx + # was supplied then do not load any targets + # * If neither storage_object nor target option was supplied to + # restoreconfig, then go ahead and load all targets + if (not storage_object and not target) or (target and t['wwn'] == target): + if 'fabric' not in t: + err_func("target %s missing 'fabric' field" % t['wwn']) + if target: + break + continue + if t['fabric'] not in (f.name for f in self.fabric_modules): + err_func("Unknown fabric '%s'" % t['fabric']) + if target: + break + continue + + fm_obj = FabricModule(t['fabric']) - # Instantiate target - Target.setup(fm_obj, t, err_func) + # Instantiate target + Target.setup(fm_obj, t, err_func) + + if target: + break return errors @@ -386,7 +451,9 @@ else: saveconf = self.dump() - with open(save_file+".temp", "w+") as f: + tmp_file = save_file + ".temp" + + with open(tmp_file, "w+") as f: os.fchmod(f.fileno(), stat.S_IRUSR | stat.S_IWUSR) f.write(json.dumps(saveconf, sort_keys=True, indent=2)) f.write("\n") @@ -394,9 +461,12 @@ os.fsync(f.fileno()) f.close() - os.rename(save_file+".temp", save_file) + shutil.copyfile(tmp_file, save_file) + os.remove(tmp_file) - def restore_from_file(self, restore_file=None, clear_existing=True, abort_on_error=False): + def restore_from_file(self, restore_file=None, clear_existing=True, + target=None, storage_object=None, + abort_on_error=False): ''' Restore the configuration from a file in json format. Restore file defaults to '/etc/targets/saveconfig.json'. @@ -408,7 +478,8 @@ with open(restore_file, "r") as f: config = json.loads(f.read()) - return self.restore(config, clear_existing=clear_existing, + return self.restore(config, target, storage_object, + clear_existing=clear_existing, abort_on_error=abort_on_error) def invalidate_caches(self): diff -Nru python-rtslib-fb-2.1.69/rtslib_fb/target.py python-rtslib-fb-2.1.71/rtslib_fb/target.py --- python-rtslib-fb-2.1.69/rtslib_fb/target.py 2018-09-18 06:47:13.000000000 +0000 +++ python-rtslib-fb-2.1.71/rtslib_fb/target.py 2019-11-06 12:35:08.000000000 +0000 @@ -1302,11 +1302,11 @@ for mem in self._mem_func(self): setattr(mem, prop, value) - def list_attributes(self, writable=None): - return self._get_first_member().list_attributes(writable) + def list_attributes(self, writable=None, readable=None): + return self._get_first_member().list_attributes(writable, readable) - def list_parameters(self, writable=None): - return self._get_first_member().list_parameters(writable) + def list_parameters(self, writable=None, readable=None): + return self._get_first_member().list_parameters(writable, readable) def set_attribute(self, attribute, value): for obj in self._mem_func(self): diff -Nru python-rtslib-fb-2.1.69/rtslib_fb/tcm.py python-rtslib-fb-2.1.71/rtslib_fb/tcm.py --- python-rtslib-fb-2.1.69/rtslib_fb/tcm.py 2018-09-18 06:47:13.000000000 +0000 +++ python-rtslib-fb-2.1.71/rtslib_fb/tcm.py 2019-11-06 12:35:08.000000000 +0000 @@ -56,14 +56,14 @@ def __repr__(self): return "<%s %s/%s>" % (self.__class__.__name__, self.plugin, self.name) - def __init__(self, name, mode): + def __init__(self, name, mode, index=None): super(StorageObject, self).__init__() if "/" in name or " " in name or "\t" in name or "\n" in name: raise RTSLibError("A storage object's name cannot contain " " /, newline or spaces/tabs") else: self._name = name - self._backstore = _Backstore(name, type(self), mode) + self._backstore = _Backstore(name, type(self), mode, index) self._path = "%s/%s" % (self._backstore.path, self.name) self.plugin = self._backstore.plugin try: @@ -126,8 +126,8 @@ Build a StorageObject of the correct type from a configfs path. ''' so_name = os.path.basename(path) - so_type = path.split("/")[-2].rsplit("_", 1)[0] - return so_mapping[so_type](so_name) + so_type, so_index = path.split("/")[-2].rsplit("_", 1) + return so_mapping[so_type](so_name, index=so_index) def _get_wwn(self): self._check_self() @@ -325,7 +325,7 @@ # PSCSIStorageObject private stuff - def __init__(self, name, dev=None): + def __init__(self, name, dev=None, index=None): ''' A PSCSIStorageObject can be instantiated in two ways: - B{Creation mode}: If I{dev} is specified, the underlying configFS @@ -347,14 +347,14 @@ @return: A PSCSIStorageObject object. ''' if dev is not None: - super(PSCSIStorageObject, self).__init__(name, 'create') + super(PSCSIStorageObject, self).__init__(name, 'create', index) try: self._configure(dev) except: self.delete() raise else: - super(PSCSIStorageObject, self).__init__(name, 'lookup') + super(PSCSIStorageObject, self).__init__(name, 'lookup', index) def _configure(self, dev): self._check_self() @@ -477,7 +477,7 @@ # RDMCPStorageObject private stuff - def __init__(self, name, size=None, wwn=None, nullio=False): + def __init__(self, name, size=None, wwn=None, nullio=False, index=None): ''' A RDMCPStorageObject can be instantiated in two ways: - B{Creation mode}: If I{size} is specified, the underlying @@ -502,14 +502,14 @@ ''' if size is not None: - super(RDMCPStorageObject, self).__init__(name, 'create') + super(RDMCPStorageObject, self).__init__(name, 'create', index) try: self._configure(size, wwn, nullio) except: self.delete() raise else: - super(RDMCPStorageObject, self).__init__(name, 'lookup') + super(RDMCPStorageObject, self).__init__(name, 'lookup', index) def _configure(self, size, wwn, nullio): self._check_self() @@ -575,7 +575,7 @@ # FileIOStorageObject private stuff def __init__(self, name, dev=None, size=None, - wwn=None, write_back=False, aio=False): + wwn=None, write_back=False, aio=False, index=None): ''' A FileIOStorageObject can be instantiated in two ways: - B{Creation mode}: If I{dev} and I{size} are specified, the @@ -607,14 +607,14 @@ ''' if dev is not None: - super(FileIOStorageObject, self).__init__(name, 'create') + super(FileIOStorageObject, self).__init__(name, 'create', index) try: self._configure(dev, size, wwn, write_back, aio) except: self.delete() raise else: - super(FileIOStorageObject, self).__init__(name, 'lookup') + super(FileIOStorageObject, self).__init__(name, 'lookup', index) def _configure(self, dev, size, wwn, write_back, aio): self._check_self() @@ -707,7 +707,7 @@ # BlockStorageObject private stuff def __init__(self, name, dev=None, wwn=None, readonly=False, - write_back=False): + write_back=False, index=None): ''' A BlockIOStorageObject can be instantiated in two ways: - B{Creation mode}: If I{dev} is specified, the underlying configFS @@ -733,14 +733,14 @@ ''' if dev is not None: - super(BlockStorageObject, self).__init__(name, 'create') + super(BlockStorageObject, self).__init__(name, 'create', index) try: self._configure(dev, wwn, readonly) except: self.delete() raise else: - super(BlockStorageObject, self).__init__(name, 'lookup') + super(BlockStorageObject, self).__init__(name, 'lookup', index) def _configure(self, dev, wwn, readonly): self._check_self() @@ -808,7 +808,7 @@ ''' def __init__(self, name, config=None, size=None, wwn=None, - hw_max_sectors=None, control=None): + hw_max_sectors=None, control=None, index=None): ''' @param name: The name of the UserBackedStorageObject. @type name: string @@ -834,14 +834,14 @@ if '/' not in config: raise RTSLibError("'config' must contain a '/' separating subtype " "from its configuration string") - super(UserBackedStorageObject, self).__init__(name, 'create') + super(UserBackedStorageObject, self).__init__(name, 'create', index) try: self._configure(config, size, wwn, hw_max_sectors, control) except: self.delete() raise else: - super(UserBackedStorageObject, self).__init__(name, 'lookup') + super(UserBackedStorageObject, self).__init__(name, 'lookup', index) def _configure(self, config, size, wwn, hw_max_sectors, control): self._check_self() @@ -873,7 +873,10 @@ val = self._parse_info('MaxDataAreaMB') if val != "NULL": tuples.append("max_data_area_mb=%s" % val) - # 2. add next ... + val = self.get_attribute('hw_block_size') + if val != "NULL": + tuples.append("hw_block_size=%s" % val) + # 3. add next ... return ",".join(tuples) @@ -958,15 +961,16 @@ Created by storageobject ctor before SO configfs entry. """ - def __init__(self, name, storage_object_cls, mode): + def __init__(self, name, storage_object_cls, mode, index=None): super(_Backstore, self).__init__() self._so_cls = storage_object_cls self._plugin = bs_params[self._so_cls]['name'] dirp = bs_params[self._so_cls].get("alt_dirprefix", self._plugin) + # if the caller knows the index then skip the cache global bs_cache - if not bs_cache: + if index is None and not bs_cache: for dir in glob.iglob("%s/core/*_*/*/" % self.configfs_dir): parts = dir.split("/") bs_name = parts[-2] @@ -974,14 +978,16 @@ current_key = "%s/%s" % (bs_dirp, bs_name) bs_cache[current_key] = int(bs_index) - # mapping in cache? self._lookup_key = "%s/%s" % (dirp, name) - self._index = bs_cache.get(self._lookup_key, None) + if index is None: + self._index = bs_cache.get(self._lookup_key, None) + if self._index != None and mode == 'create': + raise RTSLibError("Storage object %s/%s exists" % + (self._plugin, name)) + else: + self._index = int(index) - if self._index != None and mode == 'create': - raise RTSLibError("Storage object %s/%s exists" % - (self._plugin, name)) - elif self._index == None: + if self._index == None: if mode == 'lookup': raise RTSLibNotInCFS("Storage object %s/%s not found" % (self._plugin, name)) @@ -1002,12 +1008,14 @@ try: self._create_in_cfs_ine(mode) except: - del bs_cache[self._lookup_key] + if self._lookup_key in bs_cache: + del bs_cache[self._lookup_key] raise def delete(self): super(_Backstore, self).delete() - del bs_cache[self._lookup_key] + if self._lookup_key in bs_cache: + del bs_cache[self._lookup_key] def _get_index(self): return self._index diff -Nru python-rtslib-fb-2.1.69/rtslib_fb/utils.py python-rtslib-fb-2.1.71/rtslib_fb/utils.py --- python-rtslib-fb-2.1.69/rtslib_fb/utils.py 2018-09-18 06:47:13.000000000 +0000 +++ python-rtslib-fb-2.1.71/rtslib_fb/utils.py 2019-11-06 12:35:08.000000000 +0000 @@ -134,6 +134,9 @@ except (KeyError, UnicodeDecodeError, ValueError): return 0 + if device['DEVTYPE'] == 'partition': + attributes = device.parent.attributes + try: logical_block_size = attributes.asint('queue/logical_block_size') except (KeyError, UnicodeDecodeError, ValueError): diff -Nru python-rtslib-fb-2.1.69/setup.py python-rtslib-fb-2.1.71/setup.py --- python-rtslib-fb-2.1.69/setup.py 2018-09-18 06:47:13.000000000 +0000 +++ python-rtslib-fb-2.1.71/setup.py 2019-11-06 12:35:08.000000000 +0000 @@ -16,11 +16,25 @@ under the License. ''' +import os +import re from setuptools import setup +# Get version without importing. +init_file_path = os.path.join(os.path.dirname(__file__), 'rtslib/__init__.py') + +with open(init_file_path) as f: + for line in f: + match = re.match(r"__version__.*'([0-9.]+)'", line) + if match: + version = match.group(1) + break + else: + raise Exception("Couldn't find version in setup.py") + setup ( name = 'rtslib-fb', - version = '2.1.69', + version = version, description = 'API for Linux kernel SCSI target (aka LIO)', license = 'Apache 2.0', maintainer = 'Andy Grover',