targetcli-2.1/0000755000175000017500000000000012165224231011526 5ustar rrsrrstargetcli-2.1/targetcli/0000755000175000017500000000000012165224231013504 5ustar rrsrrstargetcli-2.1/targetcli/__init__.py0000644000175000017500000000152612165224231015621 0ustar rrsrrs''' This file is part of targetcli. Copyright (c) 2011-2013 by Datera, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' from ui_root import UIRoot __version__ = 'GIT_VERSION' __author__ = "Jerome Martin " __url__ = "http://www.risingtidesystems.com" __description__ = "An administration shell for RTS storage targets." __license__ = __doc__ targetcli-2.1/targetcli/ui_target.py0000644000175000017500000010754212165224231016052 0ustar rrsrrs''' Implements the targetcli target related UI. This file is part of targetcli. Copyright (c) 2011-2013 by Datera, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' from ui_node import UINode, UIRTSLibNode from ui_backstore import dedup_so_name from rtslib import RTSLibError, RTSLibBrokenLink, utils from rtslib import NodeACL, NetworkPortal, MappedLUN from rtslib import Target, TPG, LUN class UIFabricModule(UIRTSLibNode): ''' A fabric module UI. ''' def __init__(self, fabric_module, parent): UIRTSLibNode.__init__(self, fabric_module.name, fabric_module, parent) self.cfs_cwd = fabric_module.path self.refresh() if self.rtsnode.has_feature('discovery_auth'): for param in ['userid', 'password', 'mutual_userid', 'mutual_password', 'enable']: self.define_config_group_param('discovery_auth', param, 'string') self.refresh() def ui_getgroup_discovery_auth(self, auth_attr): ''' This is the backend method for getting discovery_auth attributes. @param auth_attr: The auth attribute to get the value of. @type auth_attr: str @return: The auth attribute's value @rtype: str ''' value = None if auth_attr == 'password': value = self.rtsnode.discovery_password elif auth_attr == 'userid': value = self.rtsnode.discovery_userid elif auth_attr == 'mutual_password': value = self.rtsnode.discovery_mutual_password elif auth_attr == 'mutual_userid': value = self.rtsnode.discovery_mutual_userid elif auth_attr == 'enable': value = self.rtsnode.discovery_enable_auth return value def ui_setgroup_discovery_auth(self, auth_attr, value): ''' This is the backend method for setting discovery auth attributes. @param auth_attr: The auth attribute to set the value of. @type auth_attr: str @param value: The auth's value @type value: str ''' self.assert_root() if value is None: value = '' if auth_attr == 'password': self.rtsnode.discovery_password = value elif auth_attr == 'userid': self.rtsnode.discovery_userid = value elif auth_attr == 'mutual_password': self.rtsnode.discovery_mutual_password = value elif auth_attr == 'mutual_userid': self.rtsnode.discovery_mutual_userid = value elif auth_attr == 'enable': self.rtsnode.discovery_enable_auth = value def refresh(self): self._children = set([]) for target in self.rtsnode.targets: self.shell.log.debug("Found target %s under fabric module %s." % (target.wwn, target.fabric_module)) if target.has_feature('tpgts'): UIMultiTPGTarget(target, self) else: UITarget(target, self) def summary(self): no_targets = len(self._children) if no_targets != 1: msg = "%d Targets" % no_targets else: msg = "%d Target" % no_targets return (msg, None) def ui_command_create(self, wwn=None): ''' Creates a new target. The I{wwn} format depends on the transport(s) supported by the fabric module. If the I{wwn} is ommited, then a target will be created using either a randomly generated WWN of the proper type, or the first unused WWN in the list of possible WWNs if one is available. If WWNs are constrained to a list (i.e. for hardware targets addresses) and all WWNs are in use, the target creation will fail. Use the B{info} command to get more information abour WWN type and possible values. SEE ALSO ======== B{info} ''' self.assert_root() target = Target(self.rtsnode, wwn, mode='create') wwn = target.wwn if target.has_feature('tpgts'): ui_target = UIMultiTPGTarget(target, self) self.shell.log.info("Created target %s." % wwn) return ui_target.ui_command_create() else: ui_target = UITarget(target, self) self.shell.log.info("Created target %s." % wwn) return self.new_node(ui_target) def ui_complete_create(self, parameters, text, current_param): ''' Parameter auto-completion method for user command create. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' spec = self.rtsnode.spec if current_param == 'wwn' and spec['wwn_list'] is not None: existing_wwns = [child.wwn for child in self.rtsnode.targets] completions = [wwn for wwn in spec['wwn_list'] if wwn.startswith(text) if wwn not in existing_wwns] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions def ui_command_delete(self, wwn): ''' Recursively deletes the target with the specified I{wwn}, and all objects hanging under it. SEE ALSO ======== B{create} ''' self.assert_root() target = Target(self.rtsnode, wwn, mode='lookup') target.delete() self.shell.log.info("Deleted Target %s." % wwn) self.refresh() def ui_complete_delete(self, parameters, text, current_param): ''' Parameter auto-completion method for user command delete. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'wwn': wwns = [child.name for child in self.children] completions = [wwn for wwn in wwns if wwn.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions def ui_command_info(self): ''' Displays information about the fabric module, notably the supported transports(s) and accepted B{wwn} format(s), as long as supported features. ''' spec = self.rtsnode.spec self.shell.log.info("Fabric module name: %s" % self.name) self.shell.log.info("ConfigFS path: %s" % self.rtsnode.path) if spec['wwn_list'] is not None: self.shell.log.info("Allowed WWNs list (%s type): %s" % (spec['wwn_type'], ', '.join(spec['wwn_list']))) else: self.shell.log.info("Supported WWN type: %s" % spec['wwn_type']) self.shell.log.info("Fabric module specfile: %s" % self.rtsnode.spec_file) self.shell.log.info("Fabric module features: %s" % ', '.join(spec['features'])) self.shell.log.info("Corresponding kernel module: %s" % spec['kernel_module']) def ui_command_version(self): ''' Displays the target fabric module version. ''' version = "Target fabric module %s: %s" \ % (self.rtsnode.name, self.rtsnode.version) self.shell.con.display(version.strip()) class UIMultiTPGTarget(UIRTSLibNode): ''' A generic target UI that has multiple TPGs. ''' def __init__(self, target, parent): UIRTSLibNode.__init__(self, target.wwn, target, parent) self.cfs_cwd = target.path self.refresh() def refresh(self): self._children = set([]) for tpg in self.rtsnode.tpgs: UITPG(tpg, self) def summary(self): if not self.rtsnode.fabric_module.is_valid_wwn(self.rtsnode.wwn): description = "INVALID WWN" is_healthy = False else: is_healthy = None no_tpgs = len(self._children) if no_tpgs != 1: description = "%d TPGs" % no_tpgs else: description = "%d TPG" % no_tpgs return (description, is_healthy) def ui_command_create(self, tag=None): ''' Creates a new Target Portal Group within the target. The I{tag} must be a strictly positive integer value. If omitted, the next available Target Portal Group Tag (TPGT) will be used. SEE ALSO ======== B{delete} ''' self.assert_root() if tag is None: tags = [tpg.tag for tpg in self.rtsnode.tpgs] for index in range(1048576): if index not in tags and index > 0: tag = index break if tag is None: self.shell.log.error("Cannot find an available TPG Tag.") return else: self.shell.log.info("Selected TPG Tag %d." % tag) else: try: tag = int(tag) except ValueError: self.shell.log.error("The TPG Tag must be an integer value.") return else: if tag < 1: self.shell.log.error("The TPG Tag must be >0.") return tpg = TPG(self.rtsnode, tag, mode='create') if self.shell.prefs['auto_enable_tpgt']: tpg.enable = True self.shell.log.info("Successfully created TPG %s." % tpg.tag) ui_tpg = UITPG(tpg, self) return self.new_node(ui_tpg) def ui_command_delete(self, tag): ''' Deletes the Target Portal Group with TPGT I{tag} from the target. The I{tag} must be a positive integer matching an existing TPGT. SEE ALSO ======== B{create} ''' self.assert_root() tpg = TPG(self.rtsnode, tag, mode='lookup') tpg.delete() self.shell.log.info("Deleted TPGT %s." % tag) self.refresh() def ui_complete_delete(self, parameters, text, current_param): ''' Parameter auto-completion method for user command delete. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'tag': tags = [child.name[4:] for child in self.children] completions = [tag for tag in tags if tag.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions class UITPG(UIRTSLibNode): ''' A generic TPG UI. ''' def __init__(self, tpg, parent): name = "tpgt%d" % tpg.tag UIRTSLibNode.__init__(self, name, tpg, parent) self.cfs_cwd = tpg.path self.refresh() UILUNs(tpg, self) if tpg.has_feature('acls'): UINodeACLs(self.rtsnode, self) if tpg.has_feature('nps'): UIPortals(self.rtsnode, self) def summary(self): if self.rtsnode.has_feature('nexus'): description = "%s" % self.rtsnode.nexus elif self.rtsnode.enable: description = "enabled" else: description = "disabled" return (description, True) def ui_command_enable(self): ''' Enables the TPG. SEE ALSO ======== B{disable status} ''' self.assert_root() if self.rtsnode.enable: self.shell.log.info("The TPGT is already enabled.") else: self.rtsnode.enable = True self.shell.log.info("The TPGT has been enabled.") def ui_command_disable(self): ''' Disables the TPG. SEE ALSO ======== B{enable status} ''' self.assert_root() if self.rtsnode.enable: self.rtsnode.enable = False self.shell.log.info("The TPGT has been disabled.") else: self.shell.log.info("The TPGT is already disabled.") class UITarget(UITPG): ''' A generic target UI merged with its only TPG. ''' def __init__(self, target, parent): UITPG.__init__(self, TPG(target, 1), parent) self._name = target.wwn self.target = target self.rtsnode.enable = True def summary(self): if not self.target.fabric_module.is_valid_wwn(self.target.wwn): return ("INVALID WWN", False) else: return UITPG.summary(self) class UINodeACLs(UINode): ''' A generic UI for node ACLs. ''' def __init__(self, tpg, parent): UINode.__init__(self, "acls", parent) self.tpg = tpg self.cfs_cwd = "%s/acls" % tpg.path self.refresh() def refresh(self): self._children = set([]) for node_acl in self.tpg.node_acls: UINodeACL(node_acl, self) def summary(self): no_acls = len(self._children) if no_acls != 1: msg = "%d ACLs" % no_acls else: msg = "%d ACL" % no_acls return (msg, None) def ui_command_create(self, wwn, add_mapped_luns=None): ''' Creates a Node ACL for the initiator node with the specified I{wwn}. The node's I{wwn} must match the expected WWN Type of the target's fabric module. If I{add_mapped_luns} is omitted, the global parameter B{auto_add_mapped_luns} will be used, else B{true} or B{false} are accepted. If B{true}, then after creating the ACL, mapped LUNs will be automatically created for all existing LUNs. SEE ALSO ======== B{delete} ''' self.assert_root() spec = self.tpg.parent_target.fabric_module.spec if not utils.is_valid_wwn(spec['wwn_type'], wwn): self.shell.log.error("'%s' is not a valid %s WWN." % (wwn, spec['wwn_type'])) return add_mapped_luns = \ self.ui_eval_param(add_mapped_luns, 'bool', self.shell.prefs['auto_add_mapped_luns']) try: node_acl = NodeACL(self.tpg, wwn, mode="create") except RTSLibError, msg: self.shell.log.error(msg) return else: self.shell.log.info("Successfully created Node ACL for %s" % node_acl.node_wwn) ui_node_acl = UINodeACL(node_acl, self) if add_mapped_luns: for lun in self.tpg.luns: MappedLUN(node_acl, lun.lun, lun.lun, write_protect=False) self.shell.log.info("Created mapped LUN %d." % lun.lun) self.refresh() return self.new_node(ui_node_acl) def ui_command_delete(self, wwn): ''' Deletes the Node ACL with the specified I{wwn}. SEE ALSO ======== B{create} ''' self.assert_root() node_acl = NodeACL(self.tpg, wwn, mode='lookup') node_acl.delete() self.shell.log.info("Successfully deleted Node ACL %s." % wwn) self.refresh() def ui_complete_delete(self, parameters, text, current_param): ''' Parameter auto-completion method for user command delete. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'wwn': wwns = [acl.node_wwn for acl in self.tpg.node_acls] completions = [wwn for wwn in wwns if wwn.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions class UINodeACL(UIRTSLibNode): ''' A generic UI for a node ACL. ''' def __init__(self, node_acl, parent): UIRTSLibNode.__init__(self, node_acl.node_wwn, node_acl, parent) self.define_config_group_param( 'attribute', 'tcq_depth', 'string', "Command queue depth.", True) self.cfs_cwd = node_acl.path self.refresh() def ui_getgroup_attribute(self, attribute): ''' This is the backend method for getting attributes. @param attribute: The attribute to get the value of. @type attribute: str @return: The attribute's value @rtype: arbitrary ''' if attribute == 'tcq_depth': return self.rtsnode.tcq_depth else: return self.rtsnode.get_attribute(attribute) def ui_setgroup_attribute(self, attribute, value): ''' This is the backend method for setting attributes. @param attribute: The attribute to set the value of. @type attribute: str @param value: The attribute's value @type value: arbitrary ''' self.assert_root() if attribute == 'tcq_depth': self.rtsnode.tcq_depth = value else: self.rtsnode.set_attribute(attribute, value) def refresh(self): self._children = set([]) for mlun in self.rtsnode.mapped_luns: UIMappedLUN(mlun, self) def summary(self): no_mluns = len(self._children) if no_mluns != 1: msg = "%d Mapped LUNs" % no_mluns else: msg = "%d Mapped LUN" % no_mluns return (msg, None) def ui_command_create(self, mapped_lun, tpg_lun, write_protect=None): ''' Creates a mapping to one of the TPG LUNs for the initiator referenced by the ACL. The provided I{tpg_lun} will appear to that initiator as LUN I{mapped_lun}. If the I{write_protect} flag is set to B{1}, the initiator will not have write access to the Mapped LUN. SEE ALSO ======== B{delete} ''' self.assert_root() try: tpg_lun = int(tpg_lun) mapped_lun = int(mapped_lun) except ValueError: self.shell.log.error("Incorrect LUN value.") return mlun = MappedLUN(self.rtsnode, mapped_lun, tpg_lun, write_protect) ui_mlun = UIMappedLUN(mlun, self) self.shell.log.info("Created Mapped LUN %s." % mlun.mapped_lun) return self.new_node(ui_mlun) def ui_command_delete(self, mapped_lun): ''' Deletes the specified I{mapped_lun}. SEE ALSO ======== B{create} ''' self.assert_root() mlun = MappedLUN(self.rtsnode, mapped_lun) mlun.delete() self.shell.log.info("Deleted Mapped LUN %s." % mapped_lun) self.refresh() def ui_complete_delete(self, parameters, text, current_param): ''' Parameter auto-completion method for user command delete. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'mapped_lun': mluns = [str(mlun.mapped_lun) for mlun in self.rtsnode.mapped_luns] completions = [mlun for mlun in mluns if mlun.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions class UIMappedLUN(UIRTSLibNode): ''' A generic UI for MappedLUN objects. ''' def __init__(self, mapped_lun, parent): name = "mapped_lun%d" % mapped_lun.mapped_lun UIRTSLibNode.__init__(self, name, mapped_lun, parent) self.cfs_cwd = mapped_lun.path self.refresh() def summary(self): mapped_lun = self.rtsnode is_healthy = True try: tpg_lun = mapped_lun.tpg_lun except RTSLibBrokenLink: description = "BROKEN LUN LINK" is_healthy = False else: if mapped_lun.write_protect: access_mode = 'ro' else: access_mode = 'rw' description = "lun%d (%s)" % (tpg_lun.lun, access_mode) return (description, is_healthy) class UILUNs(UINode): ''' A generic UI for TPG LUNs. ''' def __init__(self, tpg, parent): UINode.__init__(self, "luns", parent) self.cfs_cwd = "%s/lun" % tpg.path self.tpg = tpg self.refresh() def refresh(self): self._children = set([]) for lun in self.tpg.luns: UILUN(lun, self) def summary(self): no_luns = len(self._children) if no_luns != 1: msg = "%d LUNs" % no_luns else: msg = "%d LUN" % no_luns return (msg, None) def ui_command_create(self, storage_object, lun=None, add_mapped_luns=None): ''' Creates a new LUN in the Target Portal Group, attached to a storage object. If the I{lun} parameter is omitted, the first available LUN in the TPG will be used. If present, it must be a number greater than 0. Alternatively, the syntax I{lunX} where I{X} is a positive number is also accepted. The I{storage_object} must be the path of an existing storage object, i.e. B{/backstore/pscsi0/mydisk} to reference the B{mydisk} storage object of the virtual HBA B{pscsi0}. If I{add_mapped_luns} is omitted, the global parameter B{auto_add_mapped_luns} will be used, else B{true} or B{false} are accepted. If B{true}, then after creating the LUN, mapped LUNs will be automatically created for all existing node ACLs, mapping the new LUN. SEE ALSO ======== B{delete} ''' self.assert_root() if lun is None: luns = [lun.lun for lun in self.tpg.luns] for index in range(1048576): if index not in luns: lun = index break if lun is None: self.shell.log.error("Cannot find an available LUN.") return else: self.shell.log.info("Selected LUN %d." % lun) else: try: if lun.startswith('lun'): lun = lun[3:] lun = int(lun) except ValueError: self.shell.log.error("The LUN must be an integer value.") return else: if lun < 0: self.shell.log.error("The LUN cannot be negative.") return add_mapped_luns = \ self.ui_eval_param(add_mapped_luns, 'bool', self.shell.prefs['auto_add_mapped_luns']) try: storage_object = self.get_node(storage_object).rtsnode except ValueError: self.shell.log.error("Invalid storage object %s." % storage_object) return lun_object = LUN(self.tpg, lun, storage_object) self.shell.log.info("Successfully created LUN %s." % lun_object.lun) ui_lun = UILUN(lun_object, self) if add_mapped_luns: for acl in self.tpg.node_acls: mapped_lun = lun existing_mluns = [mlun.mapped_lun for mlun in acl.mapped_luns] if mapped_lun in existing_mluns: tentative_mlun = 0 while mapped_lun == lun: if tentative_mlun not in existing_mluns: mapped_lun = tentative_mlun self.shell.log.warning( "Mapped LUN %d already " % lun + "exists in ACL %s, using %d instead." % (acl.node_wwn, mapped_lun)) else: tentative_mlun += 1 mlun = MappedLUN(acl, mapped_lun, lun, write_protect=False) self.shell.log.info("Created mapped LUN %d in node ACL %s" % (mapped_lun, acl.node_wwn)) self.parent.refresh() return self.new_node(ui_lun) def ui_complete_create(self, parameters, text, current_param): ''' Parameter auto-completion method for user command create. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'storage_object': storage_objects = [] for backstore in self.get_node('/backstores').children: for storage_object in backstore.children: storage_objects.append(storage_object.path) completions = [so for so in storage_objects if so.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions def ui_command_delete(self, lun): ''' Deletes the supplied LUN from the Target Portal Group. The I{lun} must be a positive number matching an existing LUN. Alternatively, the syntax I{lunX} where I{X} is a positive number is also accepted. SEE ALSO ======== B{create} ''' self.assert_root() if lun.startswith('lun'): lun = lun[3:] lun_object = LUN(self.tpg, lun) lun_object.delete() self.shell.log.info("Successfully deleted LUN %s." % lun) # Refresh the TPG as we need to also refresh acls MappedLUNs self.parent.refresh() def ui_complete_delete(self, parameters, text, current_param): ''' Parameter auto-completion method for user command delete. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'lun': luns = [str(lun.lun) for lun in self.tpg.luns] completions = [lun for lun in luns if lun.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions class UILUN(UIRTSLibNode): ''' A generic UI for LUN objects. ''' def __init__(self, lun, parent): name = "lun%d" % lun.lun UIRTSLibNode.__init__(self, name, lun, parent) self.cfs_cwd = lun.path self.refresh() def summary(self): lun = self.rtsnode is_healthy = True try: storage_object = lun.storage_object except RTSLibBrokenLink: description = "BROKEN STORAGE LINK" is_healthy = False else: backstore = storage_object.backstore if backstore.plugin.startswith("rd"): path = "ramdisk" else: path = storage_object.udev_path if self.shell.prefs['legacy_hba_view']: description = "%s%s/%s (%s)" % (backstore.plugin, backstore.index, storage_object.name, path) else: description = "%s/%s (%s)" % (backstore.plugin, dedup_so_name(storage_object), path) return (description, is_healthy) class UIPortals(UINode): ''' A generic UI for TPG network portals. ''' def __init__(self, tpg, parent): UINode.__init__(self, "portals", parent) self.tpg = tpg self.cfs_cwd = "%s/np" % tpg.path self.refresh() def refresh(self): self._children = set([]) for portal in self.tpg.network_portals: UIPortal(portal, self) def summary(self): no_portals = len(self._children) if no_portals != 1: msg = "%d Portals" % no_portals else: msg = "%d Portal" % no_portals return (msg, None) def ui_command_create(self, ip_address=None, ip_port=None): ''' Creates a Network Portal with specified I{ip_address} and I{ip_port}. If I{ip_port} is omitted, the default port for the target fabric will be used. If I{ip_address} is omitted, the first IP address found matching the local hostname will be used. SEE ALSO ======== B{delete} ''' self.assert_root() if ip_port is None: # FIXME: Add a specfile parameter to determine that ip_port = 3260 self.shell.log.info("Using default IP port %d" % ip_port) if ip_address is None: if not ip_address: ip_address = utils.get_main_ip() if ip_address: self.shell.log.info("Automatically selected IP address %s." % ip_address) else: self.shell.log.error("Cannot find a usable IP address to " + "create the Network Portal.") return elif ip_address not in utils.list_eth_ips(): self.shell.log.error("IP address does not exist: %s" % ip_address) return try: ip_port = int(ip_port) except ValueError: self.shell.log.error("The ip_port must be an integer value.") return portal = NetworkPortal(self.tpg, ip_address, ip_port, mode='create') self.shell.log.info("Successfully created network portal %s:%d." % (ip_address, ip_port)) ui_portal = UIPortal(portal, self) return self.new_node(ui_portal) def ui_complete_create(self, parameters, text, current_param): ''' Parameter auto-completion method for user command create. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'ip_address': completions = [addr for addr in utils.list_eth_ips() if addr.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions def ui_command_delete(self, ip_address, ip_port): ''' Deletes the Network Portal with specified I{ip_address} and I{ip_port}. SEE ALSO ======== B{create} ''' self.assert_root() portal = NetworkPortal(self.tpg, ip_address, ip_port, mode='lookup') portal.delete() self.shell.log.info("Deleted network portal %s:%s" % (ip_address, ip_port)) self.refresh() def ui_complete_delete(self, parameters, text, current_param): ''' Parameter auto-completion method for user command delete. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' completions = [] # TODO: Check if a dict comprehension is acceptable here with supported # XXX: python versions. portals = {} all_ports = set([]) for portal in self.tpg.network_portals: all_ports.add(str(portal.port)) if not portal.ip_address in portals: portals[portal.ip_address] = [] portals[portal.ip_address].append(str(portal.port)) if current_param == 'ip_address': if 'ip_port' in parameters: port = parameters['ip_port'] completions = [addr for addr in portals if port in portals[addr] if addr.startswith(text)] else: completions = [addr for addr in portals if addr.startswith(text)] elif current_param == 'ip_port': if 'ip_address' in parameters: addr = parameters['ip_address'] if addr in portals: completions = [port for port in portals[addr] if port.startswith(text)] else: completions = [port for port in all_ports if port.startswith(text)] if len(completions) == 1: return [completions[0] + ' '] else: return completions class UIPortal(UIRTSLibNode): ''' A generic UI for a network portal. ''' def __init__(self, portal, parent): name = "%s:%s" % (portal.ip_address, portal.port) UIRTSLibNode.__init__(self, name, portal, parent) self.cfs_cwd = portal.path self.portal = portal self.refresh() def summary(self): if self.portal._get_iser_attr(): return ('OK, iser enabled', True) else: return ('OK, iser disabled', True) def ui_command_iser_enable(self): ''' Enables iser operation on an network portal. ''' if self.portal._get_iser_attr() == True: self.shell.log.info("iser operation has already been enabled") else: self.portal._set_iser_attr(True) self.shell.log.info("iser operation has been enabled") def ui_command_iser_disable(self): ''' Disabled iser operation on an network portal. ''' if self.portal._get_iser_attr() == False: self.shell.log.info("iser operation has already been disabled") else: self.portal._set_iser_attr(False) self.shell.log.info("iser operation has been disabled") targetcli-2.1/targetcli/ui_backstore.py0000644000175000017500000003756012165224231016543 0ustar rrsrrs''' Implements the targetcli backstores related UI. This file is part of targetcli. Copyright (c) 2011-2013 by Datera, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' from ui_node import UINode, UIRTSLibNode from rtslib import RTSRoot from rtslib import FileIOBackstore, IBlockBackstore from rtslib import PSCSIBackstore, RDDRBackstore, RDMCPBackstore from rtslib import FileIOStorageObject, IBlockStorageObject from rtslib import PSCSIStorageObject, RDDRStorageObject, RDMCPStorageObject from rtslib.utils import get_block_type, is_disk_partition from configshell import ExecutionError def dedup_so_name(storage_object): ''' Useful for migration from ui_backstore_legacy to new style with 1:1 hba:so mapping. If name is a duplicate in a backstore, returns name_X where X is the HBA index. ''' names = [so.name for so in RTSRoot().storage_objects if so.backstore.plugin == storage_object.backstore.plugin] if names.count(storage_object.name) > 1: return "%s_%d" % (storage_object.name, storage_object.backstore.index) else: return storage_object.name class UIBackstores(UINode): ''' The backstores container UI. ''' def __init__(self, parent): UINode.__init__(self, 'backstores', parent) self.cfs_cwd = "%s/core" % self.cfs_cwd self.refresh() def refresh(self): self._children = set([]) UIPSCSIBackstore(self) UIRDDRBackstore(self) UIRDMCPBackstore(self) UIFileIOBackstore(self) UIIBlockBackstore(self) class UIBackstore(UINode): ''' A backstore UI. ''' def __init__(self, plugin, parent): UINode.__init__(self, plugin, parent) self.cfs_cwd = "%s/core" % self.cfs_cwd self.refresh() def refresh(self): self._children = set([]) for so in RTSRoot().storage_objects: if so.backstore.plugin == self.name: ui_so = UIStorageObject(so, self) ui_so.name = dedup_so_name(so) def summary(self): no_storage_objects = len(self._children) if no_storage_objects > 1: msg = "%d Storage Objects" % no_storage_objects else: msg = "%d Storage Object" % no_storage_objects return (msg, None) def prm_gen_wwn(self, generate_wwn): generate_wwn = \ self.ui_eval_param(generate_wwn, 'bool', True) if generate_wwn: self.shell.log.info("Generating a wwn serial.") else: self.shell.log.info("Not generating a wwn serial.") return generate_wwn def prm_buffered(self, buffered): generate_wwn = \ self.ui_eval_param(buffered, 'bool', True) if buffered: self.shell.log.info("Using buffered mode.") else: self.shell.log.info("Not using buffered mode.") return buffered def ui_command_delete(self, name): ''' Recursively deletes the storage object having the specified I{name}. If there are LUNs using this storage object, they will be deleted too. EXAMPLE ======= B{delete mystorage} ------------------- Deletes the storage object named mystorage, and all associated LUNs. ''' self.assert_root() try: child = self.get_child(name) except ValueError: self.shell.log.error("No storage object named %s." % name) else: hba = child.rtsnode.backstore child.rtsnode.delete() if not hba.storage_objects: hba.delete() self.remove_child(child) self.shell.log.info("Deleted storage object %s." % name) self.parent.parent.refresh() def ui_complete_delete(self, parameters, text, current_param): ''' Parameter auto-completion method for user command delete. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'name': names = [child.name for child in self.children] completions = [name for name in names if name.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions def next_hba_index(self): self.shell.log.debug("%r" % [(backstore.plugin, backstore.index) for backstore in RTSRoot().backstores]) indexes = [backstore.index for backstore in RTSRoot().backstores if backstore.plugin == self.name] self.shell.log.debug("Existing %s backstore indexes: %r" % (self.name, indexes)) for index in range(1048576): if index not in indexes: backstore_index = index break if backstore_index is None: raise ExecutionError("Cannot find an available backstore index.") else: self.shell.log.debug("First available %s backstore index is %d." % (self.name, backstore_index)) return backstore_index def assert_available_so_name(self, name): names = [child.name for child in self.children] if name in names: raise ExecutionError("Storage object %s/%s already exist." % (self.name, name)) class UIPSCSIBackstore(UIBackstore): ''' PSCSI backstore UI. ''' def __init__(self, parent): UIBackstore.__init__(self, 'pscsi', parent) def ui_command_create(self, name, dev): ''' Creates a PSCSI storage object, with supplied name and SCSI device. The SCSI device I{dev} can either be a path name to the device, in which case it is recommended to use the /dev/disk/by-id hierarchy to have consistent naming should your physical SCSI system be modified, or an SCSI device ID in the H:C:T:L format, which is not recommended as SCSI IDs may vary in time. ''' self.assert_root() self.assert_available_so_name(name) backstore = PSCSIBackstore(self.next_hba_index(), mode='create') try: so = PSCSIStorageObject(backstore, name, dev) except Exception, exception: backstore.delete() raise exception ui_so = UIStorageObject(so, self) self.shell.log.info("Created pscsi storage object %s using %s" % (name, dev)) return self.new_node(ui_so) class UIRDDRBackstore(UIBackstore): ''' RDDR backstore UI. ''' def __init__(self, parent): UIBackstore.__init__(self, 'rd_dr', parent) def ui_command_create(self, name, size, generate_wwn=None): ''' Creates an RDDR storage object. I{size} is the size of the ramdisk, and the optional I{generate_wwn} parameter is a boolean specifying whether or not we should generate a T10 wwn serial for the unit (by default, yes). SIZE SYNTAX =========== - If size is an int, it represents a number of bytes. - If size is a string, the following units can be used: - B{B} or no unit present for bytes - B{k}, B{K}, B{kB}, B{KB} for kB (kilobytes) - B{m}, B{M}, B{mB}, B{MB} for MB (megabytes) - B{g}, B{G}, B{gB}, B{GB} for GB (gigabytes) - B{t}, B{T}, B{tB}, B{TB} for TB (terabytes) ''' self.assert_root() self.assert_available_so_name(name) backstore = RDDRBackstore(self.next_hba_index(), mode='create') try: so = RDDRStorageObject(backstore, name, size, self.prm_gen_wwn(generate_wwn)) except Exception, exception: backstore.delete() raise exception ui_so = UIStorageObject(so, self) self.shell.log.info("Created rd_dr ramdisk %s with size %s." % (name, size)) return self.new_node(ui_so) class UIRDMCPBackstore(UIBackstore): ''' RDMCP backstore UI. ''' def __init__(self, parent): UIBackstore.__init__(self, 'rd_mcp', parent) def ui_command_create(self, name, size, generate_wwn=None): ''' Creates an RDMCP storage object. I{size} is the size of the ramdisk, and the optional I{generate_wwn} parameter is a boolean specifying whether or not we should generate a T10 wwn Serial for the unit (by default, yes). SIZE SYNTAX =========== - If size is an int, it represents a number of bytes. - If size is a string, the following units can be used: - B{B} or no unit present for bytes - B{k}, B{K}, B{kB}, B{KB} for kB (kilobytes) - B{m}, B{M}, B{mB}, B{MB} for MB (megabytes) - B{g}, B{G}, B{gB}, B{GB} for GB (gigabytes) - B{t}, B{T}, B{tB}, B{TB} for TB (terabytes) ''' self.assert_root() self.assert_available_so_name(name) backstore = RDMCPBackstore(self.next_hba_index(), mode='create') try: so = RDMCPStorageObject(backstore, name, size, self.prm_gen_wwn(generate_wwn)) except Exception, exception: backstore.delete() raise exception ui_so = UIStorageObject(so, self) self.shell.log.info("Created rd_mcp ramdisk %s with size %s." % (name, size)) return self.new_node(ui_so) class UIFileIOBackstore(UIBackstore): ''' FileIO backstore UI. ''' def __init__(self, parent): UIBackstore.__init__(self, 'fileio', parent) def ui_command_create(self, name, file_or_dev, size=None, generate_wwn=None, buffered=None): ''' Creates a FileIO storage object. If I{file_or_dev} is a path to a regular file to be used as backend, then the I{size} parameter is mandatory. Else, if I{file_or_dev} is a path to a block device, the size parameter B{must} be ommited. If present, I{size} is the size of the file to be used, I{file} the path to the file or I{dev} the path to a block device. The optional I{generate_wwn} parameter is a boolean specifying whether or not we should generate a T10 wwn Serial for the unit (by default, yes). The I{buffered} parameter is a boolean stating whether or not to enable buffered mode. It is disabled by default (synchronous mode). SIZE SYNTAX =========== - If size is an int, it represents a number of bytes. - If size is a string, the following units can be used: - B{B} or no unit present for bytes - B{k}, B{K}, B{kB}, B{KB} for kB (kilobytes) - B{m}, B{M}, B{mB}, B{MB} for MB (megabytes) - B{g}, B{G}, B{gB}, B{GB} for GB (gigabytes) - B{t}, B{T}, B{tB}, B{TB} for TB (terabytes) ''' self.assert_root() self.assert_available_so_name(name) self.shell.log.debug("Using params size=%s generate_wwn=%s buffered=%s" % (size, generate_wwn, buffered)) is_dev = get_block_type(file_or_dev) is not None \ or is_disk_partition(file_or_dev) if size is None and is_dev: backstore = FileIOBackstore(self.next_hba_index(), mode='create') try: so = FileIOStorageObject( backstore, name, file_or_dev, gen_wwn=self.prm_gen_wwn(generate_wwn), buffered_mode=self.prm_buffered(buffered)) except Exception, exception: backstore.delete() raise exception self.shell.log.info("Created fileio %s with size %s." % (name, size)) ui_so = UIStorageObject(so, self) return self.new_node(ui_so) elif size is not None and not is_dev: backstore = FileIOBackstore(self.next_hba_index(), mode='create') try: so = FileIOStorageObject( backstore, name, file_or_dev, size, gen_wwn=self.prm_gen_wwn(generate_wwn), buffered_mode=self.prm_buffered(buffered)) except Exception, exception: backstore.delete() raise exception self.shell.log.info("Created fileio %s." % name) ui_so = UIStorageObject(so, self) return self.new_node(ui_so) else: self.shell.log.error("For fileio, you must either specify both a " + "file and a size, or just a device path.") class UIIBlockBackstore(UIBackstore): ''' IBlock backstore UI. ''' def __init__(self, parent): UIBackstore.__init__(self, 'iblock', parent) def ui_command_create(self, name, dev, generate_wwn=None): ''' Creates an IBlock Storage object. I{dev} is the path to the TYPE_DISK block device to use and the optional I{generate_wwn} parameter is a boolean specifying whether or not we should generate a T10 wwn Serial for the unit (by default, yes). ''' self.assert_root() self.assert_available_so_name(name) backstore = IBlockBackstore(self.next_hba_index(), mode='create') try: so = IBlockStorageObject(backstore, name, dev, self.prm_gen_wwn(generate_wwn)) except Exception, exception: backstore.delete() raise exception ui_so = UIStorageObject(so, self) self.shell.log.info("Created iblock storage object %s using %s." % (name, dev)) return self.new_node(ui_so) class UIStorageObject(UIRTSLibNode): ''' A storage object UI. ''' def __init__(self, storage_object, parent): name = storage_object.name UIRTSLibNode.__init__(self, name, storage_object, parent) self.cfs_cwd = storage_object.path self.refresh() def ui_command_version(self): ''' Displays the version of the current backstore's plugin. ''' backstore = self.rtsnode.backstore self.shell.con.display("Backstore plugin %s %s" % (backstore.plugin, backstore.version)) def summary(self): so = self.rtsnode errors = [] if so.backstore.plugin.startswith("rd"): path = "ramdisk" else: path = so.udev_path if not path: errors.append("BROKEN STORAGE LINK") legacy = [] if self.rtsnode.name != self.name: legacy.append("ADDED SUFFIX") if len(self.rtsnode.backstore.storage_objects) > 1: legacy.append("SHARED HBA") if legacy: errors.append("LEGACY: " + ", ".join(legacy)) if errors: msg = ", ".join(errors) if path: msg += " (%s %s)" % (path, so.status) return (msg, False) else: return ("%s %s" % (path, so.status), True) targetcli-2.1/targetcli/ui_root.py0000644000175000017500000001122612165224231015540 0ustar rrsrrs''' Implements the targetcli root UI. This file is part of targetcli. Copyright (c) 2011-2013 by Datera, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' from os import system from rtslib import RTSRoot from ui_node import UINode from socket import gethostname from ui_target import UIFabricModule from tcm_dump import tcm_full_backup from ui_backstore import UIBackstores from ui_backstore_legacy import UIBackstoresLegacy class UIRoot(UINode): ''' The targetcli hierarchy root node. ''' def __init__(self, shell, as_root=False): self.loaded = False UINode.__init__(self, '/', shell=shell) self.as_root = as_root def refresh(self): ''' Refreshes the tree of target fabric modules. ''' self._children = set([]) if self.shell.prefs['legacy_hba_view']: UIBackstoresLegacy(self) else: UIBackstores(self) if not self.loaded: self.shell.log.debug("Refreshing in non-loaded mode.") for fabric_module in RTSRoot().fabric_modules: if fabric_module: self.shell.log.debug("Using %s fabric module." \ % fabric_module.name) UIFabricModule(fabric_module, self) elif self.as_root: try: for step in fabric_module.load(yield_steps=True): (action, taken, desc) = step if taken: self.shell.log.info(desc) self.shell.log.debug("Done loading %s fabric module." \ % fabric_module.name) except Exception, msg: self.shell.log.debug("Can't load fabric module %s." % fabric_module.name) self.shell.log.debug(msg) else: UIFabricModule(fabric_module, self) self.loaded = True else: self.shell.log.debug("Refreshing in loaded mode.") for fabric_module in RTSRoot().loaded_fabric_modules: self.shell.log.debug("Loading %s." % fabric_module.name) UIFabricModule(fabric_module, self) def ui_command_saveconfig(self): ''' Saves the whole configuration tree to disk so that it will be restored on next boot. Unless you do that, changes are lost accross reboots. ''' self.assert_root() self.shell.con.display("WARNING: Saving %s current configuration to " % gethostname() + "disk will overwrite your boot settings.") self.shell.con.display("The current target configuration will become " + "the default boot config.") try: input = raw_input("Are you sure? Type 'yes': ") except EOFError: input = None self.shell.con.display('') if input == "yes": tcm_full_backup(None, None, '1', None) else: self.shell.log.warning("Aborted, configuration left untouched.") def ui_command_version(self): ''' Displays the targetcli and support libraries versions. ''' from rtslib import __version__ as rtslib_version from targetcli import __version__ as targetcli_version from configshell import __version__ as configshell_version for package, version in dict(targetcli=targetcli_version, rtslib=rtslib_version, configshell=configshell_version).items(): if version == 'GIT_VERSION': self.shell.log.error("Cannot find %s version. The %s package " % (package, package) + "has probably not been built properly " + "from either the git repository or a " + "public tarball.") else: self.shell.log.info("Using %s version %s" % (package, version)) targetcli-2.1/targetcli/ui_node.py0000644000175000017500000002435712165224231015513 0ustar rrsrrs''' Implements the targetcli base UI node. This file is part of targetcli. Copyright (c) 2011-2013 by Datera, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' from configshell import ConfigNode, ExecutionError from rtslib import RTSLibError, RTSRoot from subprocess import PIPE, Popen from os.path import isfile from os import getuid def exec3(cmd): ''' Executes a shell command **cmd** and returns **(retcode, stdout, stderr)**. ''' process = Popen(cmd, shell=True, bufsize=1024*1024, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) (out, err) = process.communicate() retcode = process.returncode return (retcode, out, err) class UINode(ConfigNode): ''' Our targetcli basic UI node. ''' def __init__(self, name, parent=None, shell=None): ConfigNode.__init__(self, name, parent, shell) self.cfs_cwd = RTSRoot.configfs_dir self.define_config_group_param( 'global', 'auto_enable_tpgt', 'bool', 'If true, automatically enables TPGTs upon creation.') self.define_config_group_param( 'global', 'auto_add_mapped_luns', 'bool', 'If true, automatically create node ACLs mapped LUNs ' + 'after creating a new target LUN or a new node ACL') self.define_config_group_param( 'global', 'legacy_hba_view', 'bool', 'If true, use legacy HBA view, allowing to create more ' + 'than one storage object per HBA.') self.define_config_group_param( 'global', 'auto_cd_after_create', 'bool', 'If true, changes current path to newly created objects.') def assert_root(self): ''' For commands requiring root privileges, disable command if not the root node's as_root attribute is False. ''' root_node = self.get_root() if hasattr(root_node, 'as_root') and not self.get_root().as_root: raise ExecutionError("This privileged command is disabled: " + "you are not root.") def new_node(self, new_node): ''' Used to honor global 'auto_cd_after_create'. Either returns None if the global is False, or the new_node if the global is True. In both cases, set the @last bookmark to last_node. ''' self.shell.prefs['bookmarks']['last'] = new_node.path self.shell.prefs.save() if self.shell.prefs['auto_cd_after_create']: self.shell.log.info("Entering new node %s" % new_node.path) # Piggy backs on cd instead of just returning new_node, # so we update navigation history. return self.ui_command_cd(new_node.path) else: return None def refresh(self): ''' Refreshes and updates the objects tree from the current path. ''' for child in self.children: child.refresh() def execute_command(self, command, pparams=[], kparams={}): ''' We overload this one in order to handle our own exceptions cleanly, and not just configshell's ExecutionError. ''' try: result = ConfigNode.execute_command(self, command, pparams, kparams) except RTSLibError, msg: self.shell.log.error(msg) else: self.shell.log.debug("Command %s succeeded." % command) return result def ui_command_exit(self): ''' Exits the command line interface. ''' config_needs_save = False config_paths = {'tcm': "/etc/target/tcm_start.sh", 'lio': "/etc/target/lio_start.sh"} for mod_name, config_path in config_paths.items(): saved_config = '' live_config = exec3("%s_dump --stdout" % mod_name)[1] if isfile(config_path): with open(config_path) as config_fh: saved_config = config_fh.read() if saved_config != live_config: config_needs_save = True break if config_needs_save and getuid() == 0: self.shell.con.display("There are unsaved configuration changes.\n" "If you exit now, configuration will not " "be updated and changes will be lost upon " "reboot.") try: input = raw_input("Type 'exit' if you want to exit anyway: ") except EOFError: input = None self.shell.con.display('') if input == "exit": return 'EXIT' else: self.shell.log.warning("Aborted exit, use 'saveconfig' to " "save the current configuration.") else: return 'EXIT' def ui_command_refresh(self): ''' Refreshes and updates the objects tree from the current path. ''' self.refresh() def ui_command_status(self): ''' Displays the current node's status summary. SEE ALSO ======== B{ls} ''' description, is_healthy = self.summary() self.shell.log.info("Status for %s: %s" % (self.path, description)) def ui_setgroup_global(self, parameter, value): ConfigNode.ui_setgroup_global(self, parameter, value) self.get_root().refresh() class UIRTSLibNode(UINode): ''' A subclass of UINode for nodes with an underlying RTSLib object. ''' def __init__(self, name, rtslib_object, parent): ''' Call from the class that inherits this, with the rtslib object that should be checked upon. ''' UINode.__init__(self, name, parent) self.rtsnode = rtslib_object # If the rtsnode has parameters, use them parameters = self.rtsnode.list_parameters() parameters_ro = self.rtsnode.list_parameters(writable=False) for parameter in parameters: writable = parameter not in parameters_ro description = "The %s parameter." % parameter self.define_config_group_param( 'parameter', parameter, 'string', description, writable) # If the rtsnode has attributes, enable them attributes = self.rtsnode.list_attributes() attributes_ro = self.rtsnode.list_attributes(writable=False) for attribute in attributes: writable = attribute not in attributes_ro description = "The %s attribute." % attribute self.define_config_group_param( 'attribute', attribute, 'string', description, writable) # If the rtsnode has auth_attrs, use them auth_attrs = self.rtsnode.list_auth_attrs() auth_attrs_ro = self.rtsnode.list_auth_attrs(writable=False) for auth_attr in auth_attrs: writable = auth_attr not in auth_attrs_ro description = "The %s auth_attr." % auth_attr self.define_config_group_param( 'auth', auth_attr, 'string', description, writable) def execute_command(self, command, pparams=[], kparams={}): ''' Overrides the parent's execute_command() to check if the underlying RTSLib object still exists before returning. ''' if not self.rtsnode.exists: self.shell.log.error("The underlying rtslib object for " + "%s does not exist." % self.path) root = self.get_root() root.refresh() return root else: return UINode.execute_command(self, command, pparams, kparams) def ui_getgroup_attribute(self, attribute): ''' This is the backend method for getting attributes. @param attribute: The attribute to get the value of. @type attribute: str @return: The attribute's value @rtype: arbitrary ''' return self.rtsnode.get_attribute(attribute) def ui_setgroup_attribute(self, attribute, value): ''' This is the backend method for setting attributes. @param attribute: The attribute to set the value of. @type attribute: str @param value: The attribute's value @type value: arbitrary ''' self.assert_root() self.rtsnode.set_attribute(attribute, value) def ui_getgroup_parameter(self, parameter): ''' This is the backend method for getting parameters. @param parameter: The parameter to get the value of. @type parameter: str @return: The parameter's value @rtype: arbitrary ''' return self.rtsnode.get_parameter(parameter) def ui_setgroup_parameter(self, parameter, value): ''' This is the backend method for setting parameters. @param parameter: The parameter to set the value of. @type parameter: str @param value: The parameter's value @type value: arbitrary ''' self.assert_root() self.rtsnode.set_parameter(parameter, value) def ui_getgroup_auth(self, auth_attr): ''' This is the backend method for getting auth_attrs. @param auth_attr: The auth_attr to get the value of. @type auth_attr: str @return: The auth_attr's value @rtype: arbitrary ''' return self.rtsnode.get_auth_attr(auth_attr) def ui_setgroup_auth(self, auth_attr, value): ''' This is the backend method for setting auth_attrs. @param auth_attr: The auth_attr to set the value of. @type auth_attr: str @param value: The auth_attr's value @type value: arbitrary ''' self.assert_root() self.rtsnode.set_auth_attr(auth_attr, value) targetcli-2.1/targetcli/ui_backstore_legacy.py0000644000175000017500000004670012165224231020063 0ustar rrsrrs''' Implements the targetcli backstores related UI. his file is part of targetcli. Copyright (c) 2011-2013 by Datera, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' from ui_node import UINode, UIRTSLibNode from rtslib import RTSRoot from rtslib import FileIOBackstore, IBlockBackstore from rtslib import PSCSIBackstore, RDDRBackstore, RDMCPBackstore from rtslib import FileIOStorageObject, IBlockStorageObject from rtslib import PSCSIStorageObject, RDDRStorageObject, RDMCPStorageObject from rtslib.utils import get_block_type, is_disk_partition class UIBackstoresLegacy(UINode): ''' The backstores container UI. ''' def __init__(self, parent): UINode.__init__(self, 'backstores', parent) self.cfs_cwd = "%s/core" % self.cfs_cwd self.refresh() def refresh(self): self._children = set([]) for backstore in RTSRoot().backstores: backstore_plugin = backstore.plugin if backstore_plugin == 'pscsi': UIPSCSIBackstoreLegacy(backstore, self) elif backstore_plugin == 'rd_dr': UIRDDRBackstoreLegacy(backstore, self) elif backstore_plugin == 'rd_mcp': UIRDMCPBackstoreLegacy(backstore, self) elif backstore_plugin == 'fileio': UIFileIOBackstoreLegacy(backstore, self) elif backstore_plugin == 'iblock': UIIBlockBackstoreLegacy(backstore, self) def summary(self): no_backstores = len(self._children) if no_backstores > 1: msg = "%d Backstores (legacy mode)" % no_backstores else: msg = "%d Backstore (legacy mode)" % no_backstores return (msg, None) def ui_command_create(self, backstore_plugin): ''' Creates a new backstore, using the chosen I{backstore_plugin}. More than one backstores using the same I{backstore_plugin} can co-exist. They will be identified by incremental index numbers, starting from 0. AVAILABLE BACKSTORE PLUGINS =========================== B{iblock} --------- This I{backstore_plugin} provides I{SPC-4}, along with I{ALUA} and I{Persistent Reservations} emulation on top of Linux BLOCK devices: B{any block device} that appears in /sys/block. B{pscsi} -------- Provides pass-through for Linux physical SCSI devices. It can be used with any storage object that does B{direct pass-through} of SCSI commands without SCSI emulation. This assumes an underlying SCSI device that appears with lsscsi in /proc/scsi/scsi, such as a SAS hard drive, such as any SCSI device. The Linux kernel code for device SCSI drivers resides in linux/drivers/scsi. SCSI-3 and higher is supported with this subsystem, but only for control CDBs capable by the device firmware. B{fileio} --------- This I{backstore_plugin} provides I{SPC-4}, along with I{ALUA} and I{Persistent Reservations} emulation on top of Linux VFS devices: B{any file on a mounted filesystem}. It may be backed by a file or an underlying real block device. FILEIO is using struct file to serve block I/O with various methods (synchronous or asynchronous) and (buffered or direct). B{rd_dr} ------- This I{backstore_plugin} provides the same level of SCSI emulation than the I{fileio} and I{iblock} backstores, but uses a B{ramdisk}, based on direct memory mapping. It is the fastest of all backstores, and is typically used for bandwidth testing. B{rd_mcp} -------- This I{backstore_plugin} is a bit slower than B{rd_dr}, but more robust with multiple initiators, with a separate memory mapping using memory copy. Also typically used for bandwidth testing. EXAMPLE ======= B{create iblock} ---------------- Creates a new backstore, using the B{iblock} I{backstore_plugin}. ''' self.assert_root() self.shell.log.debug("%r" % [(backstore.plugin, backstore.index) for backstore in RTSRoot().backstores]) indexes = [backstore.index for backstore in RTSRoot().backstores if backstore.plugin == backstore_plugin] self.shell.log.debug("Existing %s backstore indexes: %r" % (backstore_plugin, indexes)) for index in range(1048576): if index not in indexes: backstore_index = index break if backstore_index is None: self.shell.log.error("Cannot find an available backstore index.") return else: self.shell.log.info("First available %s backstore index is %d." % (backstore_plugin, backstore_index)) if backstore_plugin == 'pscsi': backstore = PSCSIBackstore(backstore_index, mode='create') return self.new_node(UIPSCSIBackstoreLegacy(backstore, self)) elif backstore_plugin == 'rd_dr': backstore = RDDRBackstore(backstore_index, mode='create') return self.new_node(UIRDDRBackstoreLegacy(backstore, self)) elif backstore_plugin == 'rd_mcp': backstore = RDMCPBackstore(backstore_index, mode='create') return self.new_node(UIRDMCPBackstoreLegacy(backstore, self)) elif backstore_plugin == 'fileio': backstore = FileIOBackstore(backstore_index, mode='create') return self.new_node(UIFileIOBackstoreLegacy(backstore, self)) elif backstore_plugin == 'iblock': backstore = IBlockBackstore(backstore_index, mode='create') return self.new_node(UIIBlockBackstoreLegacy(backstore, self)) else: self.shell.log.error("Invalid backstore plugin %s" % backstore_plugin) return self.shell.log.info("Created new backstore %s" % backstore.name) def ui_complete_create(self, parameters, text, current_param): ''' Parameter auto-completion method for user command create. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'backstore_plugin': plugins = ['pscsi', 'rd_dr', 'rd_mcp', 'fileio', 'iblock'] completions = [plugin for plugin in plugins if plugin.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions def ui_command_delete(self, backstore): ''' Deletes a I{backstore}, and recursively all defined storage objects hanging under it. If there are existing LUNs making use of those storage objects, they will be deleted too. EXAMPLE ======= B{delete iblock2} ----------------- That would recursively delete the B{iblock} backstore with index 2. ''' self.assert_root() try: child = self.get_child(backstore) except ValueError: self.shell.log.error("No backstore named %s." % backstore) else: child.rtsnode.delete() self.remove_child(child) self.shell.log.info("Deleted backstore %s." % backstore) self.parent.refresh() def ui_complete_delete(self, parameters, text, current_param): ''' Parameter auto-completion method for user command delete. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'backstore': backstores = [child.name for child in self.children] completions = [backstore for backstore in backstores if backstore.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions class UIBackstoreLegacy(UIRTSLibNode): ''' A backstore UI. ''' def __init__(self, backstore, parent): UIRTSLibNode.__init__(self, backstore.name, backstore, parent) self.cfs_cwd = backstore.path self.refresh() def refresh(self): self._children = set([]) for storage_object in self.rtsnode.storage_objects: UIStorageObjectLegacy(storage_object, self) def summary(self): no_storage_objects = len(self._children) if no_storage_objects > 1: msg = "%d Storage Objects" % no_storage_objects else: msg = "%d Storage Object" % no_storage_objects return (msg, None) def prm_gen_wwn(self, generate_wwn): generate_wwn = \ self.ui_eval_param(generate_wwn, 'bool', True) if generate_wwn: self.shell.log.info("Generating a wwn serial.") else: self.shell.log.info("Not generating a wwn serial.") return generate_wwn def prm_buffered(self, buffered): generate_wwn = \ self.ui_eval_param(buffered, 'bool', True) if buffered: self.shell.log.info("Using buffered mode.") else: self.shell.log.info("Not using buffered mode.") return buffered def ui_command_version(self): ''' Displays the version of the current backstore's plugin. ''' self.shell.con.display("Backstore plugin %s %s" % (self.rtsnode.plugin, self.rtsnode.version)) def ui_command_delete(self, name): ''' Recursively deletes the storage object having the specified I{name}. If there are LUNs using this storage object, they will be deleted too. EXAMPLE ======= B{delete mystorage} ------------------- Deletes the storage object named mystorage, and all associated LUNs. ''' self.assert_root() try: child = self.get_child(name) except ValueError: self.shell.log.error("No storage object named %s." % name) else: child.rtsnode.delete() self.remove_child(child) self.shell.log.info("Deleted storage object %s." % name) self.parent.parent.refresh() def ui_complete_delete(self, parameters, text, current_param): ''' Parameter auto-completion method for user command delete. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'name': names = [child.name for child in self.children] completions = [name for name in names if name.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions class UIPSCSIBackstoreLegacy(UIBackstoreLegacy): ''' PSCSI backstore UI. ''' def ui_command_create(self, name, dev): ''' Creates a PSCSI storage object, with supplied name and SCSI device. The SCSI device I{dev} can either be a path name to the device, in which case it is recommended to use the /dev/disk/by-id hierarchy to have consistent naming should your physical SCSI system be modified, or an SCSI device ID in the H:C:T:L format, which is not recommended as SCSI IDs may vary in time. ''' self.assert_root() so = PSCSIStorageObject(self.rtsnode, name, dev) ui_so = UIStorageObjectLegacy(so, self) self.shell.log.info("Created pscsi storage object %s using %s." % (name, dev)) return self.new_node(ui_so) class UIRDDRBackstoreLegacy(UIBackstoreLegacy): ''' RDDR backstore UI. ''' def ui_command_create(self, name, size, generate_wwn=None): ''' Creates an RDDR storage object. I{size} is the size of the ramdisk, and the optional I{generate_wwn} parameter is a boolean specifying whether or not we should generate a T10 wwn serial for the unit (by default, yes). SIZE SYNTAX =========== - If size is an int, it represents a number of bytes. - If size is a string, the following units can be used: - B{B} or no unit present for bytes - B{k}, B{K}, B{kB}, B{KB} for kB (kilobytes) - B{m}, B{M}, B{mB}, B{MB} for MB (megabytes) - B{g}, B{G}, B{gB}, B{GB} for GB (gigabytes) - B{t}, B{T}, B{tB}, B{TB} for TB (terabytes) ''' self.assert_root() so = RDDRStorageObject(self.rtsnode, name, size, self.prm_gen_wwn(generate_wwn)) ui_so = UIStorageObjectLegacy(so, self) self.shell.log.info("Created rd_dr ramdisk %s with size %s." % (name, size)) return self.new_node(ui_so) class UIRDMCPBackstoreLegacy(UIBackstoreLegacy): ''' RDMCP backstore UI. ''' def ui_command_create(self, name, size, generate_wwn=None): ''' Creates an RDMCP storage object. I{size} is the size of the ramdisk, and the optional I{generate_wwn} parameter is a boolean specifying whether or not we should generate a T10 wwn Serial for the unit (by default, yes). SIZE SYNTAX =========== - If size is an int, it represents a number of bytes. - If size is a string, the following units can be used: - B{B} or no unit present for bytes - B{k}, B{K}, B{kB}, B{KB} for kB (kilobytes) - B{m}, B{M}, B{mB}, B{MB} for MB (megabytes) - B{g}, B{G}, B{gB}, B{GB} for GB (gigabytes) - B{t}, B{T}, B{tB}, B{TB} for TB (terabytes) ''' self.assert_root() so = RDMCPStorageObject(self.rtsnode, name, size, self.prm_gen_wwn(generate_wwn)) ui_so = UIStorageObjectLegacy(so, self) self.shell.log.info("Created rd_mcp ramdisk %s with size %s." % (name, size)) return self.new_node(ui_so) class UIFileIOBackstoreLegacy(UIBackstoreLegacy): ''' FileIO backstore UI. ''' def ui_command_create(self, name, file_or_dev, size=None, generate_wwn=None, buffered=None): ''' Creates a FileIO storage object. If I{file_or_dev} is a path to a regular file to be used as backend, then the I{size} parameter is mandatory. Else, if I{file_or_dev} is a path to a block device, the size parameter B{must} be ommited. If present, I{size} is the size of the file to be used, I{file} the path to the file or I{dev} the path to a block device. The optional I{generate_wwn} parameter is a boolean specifying whether or not we should generate a T10 wwn Serial for the unit (by default, yes). The I{buffered} parameter is a boolean stating whether or not to enable buffered mode. It is disabled by default (synchronous mode). SIZE SYNTAX =========== - If size is an int, it represents a number of bytes. - If size is a string, the following units can be used: - B{B} or no unit present for bytes - B{k}, B{K}, B{kB}, B{KB} for kB (kilobytes) - B{m}, B{M}, B{mB}, B{MB} for MB (megabytes) - B{g}, B{G}, B{gB}, B{GB} for GB (gigabytes) - B{t}, B{T}, B{tB}, B{TB} for TB (terabytes) ''' self.assert_root() self.shell.log.debug('Using params size=%s generate_wwn=%s buffered=%s' % (size, generate_wwn, buffered)) is_dev = get_block_type(file_or_dev) is not None \ or is_disk_partition(file_or_dev) if size is None and is_dev: so = FileIOStorageObject(self.rtsnode, name, file_or_dev, gen_wwn=self.prm_gen_wwn(generate_wwn), buffered_mode=self.prm_buffered(buffered)) self.shell.log.info("Created fileio %s with size %s." % (name, size)) ui_so = UIStorageObjectLegacy(so, self) return self.new_node(ui_so) elif size is not None and not is_dev: so = FileIOStorageObject(self.rtsnode, name, file_or_dev, size, gen_wwn=self.prm_gen_wwn(generate_wwn), buffered_mode=self.prm_buffered(buffered)) self.shell.log.info("Created fileio storage object %s." % name) ui_so = UIStorageObjectLegacy(so, self) return self.new_node(ui_so) else: self.shell.log.error("For fileio, you must either specify both a " + "file and a size, or just a device path.") class UIIBlockBackstoreLegacy(UIBackstoreLegacy): ''' IBlock backstore UI. ''' def ui_command_create(self, name, dev, generate_wwn=None): ''' Creates an IBlock Storage object. I{dev} is the path to the TYPE_DISK block device to use and the optional I{generate_wwn} parameter is a boolean specifying whether or not we should generate a T10 wwn Serial for the unit (by default, yes). ''' self.assert_root() so = IBlockStorageObject(self.rtsnode, name, dev, self.prm_gen_wwn(generate_wwn)) ui_so = UIStorageObjectLegacy(so, self) self.shell.log.info("Created iblock storage object %s using %s." % (name, dev)) return self.new_node(ui_so) class UIStorageObjectLegacy(UIRTSLibNode): ''' A storage object UI. ''' def __init__(self, storage_object, parent): name = storage_object.name UIRTSLibNode.__init__(self, name, storage_object, parent) self.cfs_cwd = storage_object.path self.refresh() def summary(self): so = self.rtsnode if so.backstore.plugin.startswith("rd"): path = "ramdisk" else: path = so.udev_path if not path: return ("BROKEN STORAGE LINK", False) else: return ("%s %s" % (path, so.status), True) targetcli-2.1/scripts/0000755000175000017500000000000012165224231013215 5ustar rrsrrstargetcli-2.1/scripts/targetcli0000755000175000017500000000474712165224231015135 0ustar rrsrrs#!/usr/bin/python ''' Starts the targetcli CLI shell. This file is part of targetcli. Copyright (c) 2011-2013 by Datera, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' from os import getuid from targetcli import UIRoot from rtslib import RTSLibError from configshell import ConfigShell from rtslib import __version__ as rtslib_version from targetcli import __version__ as targetcli_version class TargetCLI(ConfigShell): default_prefs = {'color_path': 'magenta', 'color_command': 'cyan', 'color_parameter': 'magenta', 'color_keyword': 'cyan', 'completions_in_columns': True, 'logfile': None, 'loglevel_console': 'info', 'loglevel_file': 'debug9', 'color_mode': True, 'prompt_length': 30, 'tree_max_depth': 0, 'tree_status_mode': True, 'tree_round_nodes': True, 'tree_show_root': True, 'auto_enable_tpgt': True, 'auto_add_mapped_luns': True, 'auto_cd_after_create': False, 'legacy_hba_view': False } def main(): ''' Start the targetcli shell. ''' if getuid() == 0: is_root = True else: is_root = False shell = TargetCLI('~/.targetcli') shell.con.display("targetcli %s (rtslib %s)\n" "Copyright (c) 2011-2013 by Datera, Inc.\n" "All rights reserved." % (targetcli_version, rtslib_version)) if not is_root: shell.con.display("You are not root, disabling privileged commands.\n") root_node = UIRoot(shell, as_root=is_root) try: root_node.refresh() except RTSLibError, error: shell.con.display(shell.con.render_text(str(error), 'red')) else: shell.run_interactive() if __name__ == "__main__": main() targetcli-2.1/COPYING0000644000175000017500000002363712165224231012574 0ustar rrsrrs Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. targetcli-2.1/setup.py0000755000175000017500000000221712165224231013245 0ustar rrsrrs#! /usr/bin/env python ''' This file is part of targetcli. Copyright (c) 2011-2013 by Datera, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' import re from distutils.core import setup import targetcli PKG = targetcli VERSION = str(PKG.__version__) (AUTHOR, EMAIL) = re.match('^(.*?)\s*<(.*)>$', PKG.__author__).groups() URL = PKG.__url__ LICENSE = PKG.__license__ SCRIPTS = ["scripts/targetcli"] DESCRIPTION = PKG.__description__ setup(name=PKG.__name__, description=DESCRIPTION, version=VERSION, author=AUTHOR, author_email=EMAIL, license=LICENSE, url=URL, scripts=SCRIPTS, packages=[PKG.__name__], package_data = {'':[]}) targetcli-2.1/rpm/0000755000175000017500000000000012165224231012324 5ustar rrsrrstargetcli-2.1/rpm/targetcli.spec.tmpl0000644000175000017500000000167112165224231016136 0ustar rrsrrs%define oname targetcli Name: targetcli License: Apache License 2.0 Group: Applications/System Summary: RisingTide Systems generic SCSI target CLI shell. Version: VERSION Release: 1%{?dist} URL: http://www.risingtidesystems.com/git/ Source: %{oname}-%{version}.tar.gz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-rpmroot BuildArch: noarch BuildRequires: python-devel, python-rtslib, python-configshell Requires: python-rtslib, python-configshell, lio-utils Vendor: Datera, Inc. %description RisingTide Systems generic SCSI target CLI shell. %prep %setup -q -n %{oname}-%{version} %build %{__python} setup.py build %install rm -rf %{buildroot} %{__python} setup.py install --skip-build --root=%{buildroot} --prefix=usr %clean rm -rf %{buildroot} %files %defattr(-,root,root,-) %{python_sitelib} %{_bindir}/targetcli %doc COPYING README %changelog targetcli-2.1/Makefile0000644000175000017500000001073012165224231013167 0ustar rrsrrsNAME = targetcli GIT_BRANCH = $$(git branch | grep \* | tr -d \*) VERSION = $$(basename $$(git describe --tags | tr - .)) all: @echo "Usage:" @echo @echo " make deb - Builds debian packages." @echo " make rpm - Builds rpm packages." @echo " make release - Generates the release tarball." @echo @echo " make clean - Cleanup the local repository build files." @echo " make cleanall - Also remove dist/*" clean: @rm -fv ${NAME}/*.pyc ${NAME}/*.html @rm -frv doc @rm -frv ${NAME}.egg-info MANIFEST build @rm -frv debian/tmp @rm -fv build-stamp @rm -fv dpkg-buildpackage.log dpkg-buildpackage.version @rm -frv *.rpm @rm -fv debian/files debian/*.log debian/*.substvars @rm -frv debian/${NAME}-doc/ debian/python2.5-${NAME}/ @rm -frv debian/python2.6-${NAME}/ debian/python-${NAME}/ @rm -frv results @rm -fv rpm/*.spec *.spec rpm/sed* sed* @rm -frv ${NAME}-* @rm -frv *.rpm warn${NAME}.txt build${NAME} @rm -fv debian/*.debhelper.log debian/*.debhelper debian/*.substvars debian/files @rm -fvr debian/${NAME}-frozen/ debian/${NAME}-python2.5/ @rm -fvr debian/${NAME}-python2.6/ debian/${NAME}/ debian/${NAME}-doc/ @rm -frv log/ @echo "Finished cleanup." cleanall: clean @rm -frv dist release: build/release-stamp build/release-stamp: @mkdir -p build @echo "Exporting the repository files..." @git archive ${GIT_BRANCH} --prefix ${NAME}-${VERSION}/ \ | (cd build; tar xfp -) @echo "Cleaning up the target tree..." @rm -f build/${NAME}-${VERSION}/Makefile @rm -f build/${NAME}-${VERSION}/.gitignore @rm -rf build/${NAME}-${VERSION}/bin @echo "Fixing version string..." @sed -i "s/__version__ = .*/__version__ = '${VERSION}'/g" \ build/${NAME}-${VERSION}/${NAME}/__init__.py @echo "Generating rpm specfile from template..." @cd build/${NAME}-${VERSION}; \ for spectmpl in rpm/*.spec.tmpl; do \ sed -i "s/Version:\( *\).*/Version:\1${VERSION}/g" $${spectmpl}; \ mv $${spectmpl} $$(basename $${spectmpl} .tmpl); \ done; \ rmdir rpm @echo "Generating rpm changelog..." @( \ version=$$(basename $$(git describe HEAD --tags | tr - .)); \ author=$$(git show HEAD --format="format:%an <%ae>" -s); \ date=$$(git show HEAD --format="format:%ad" -s \ | awk '{print $$1,$$2,$$3,$$5}'); \ hash=$$(git show HEAD --format="format:%H" -s); \ echo '* '"$${date} $${author} $${version}-1"; \ echo " - Generated from git commit $${hash}."; \ ) >> $$(ls build/${NAME}-${VERSION}/*.spec) @echo "Generating debian changelog..." @( \ version=$$(basename $$(git describe HEAD --tags | tr - .)); \ author=$$(git show HEAD --format="format:%an <%ae>" -s); \ date=$$(git show HEAD --format="format:%aD" -s); \ day=$$(git show HEAD --format='format:%ai' -s \ | awk '{print $$1}' \ | awk -F '-' '{print $$3}' | sed 's/^0/ /g'); \ date=$$(echo $${date} \ | awk '{print $$1, "'"$${day}"'", $$3, $$4, $$5, $$6}'); \ hash=$$(git show HEAD --format="format:%H" -s); \ echo "${NAME} ($${version}) unstable; urgency=low"; \ echo; \ echo " * Generated from git commit $${hash}."; \ echo; \ echo " -- $${author} $${date}"; \ echo; \ ) > build/${NAME}-${VERSION}/debian/changelog @find build/${NAME}-${VERSION}/ -exec \ touch -t $$(date -d @$$(git show -s --format="format:%at") \ +"%Y%m%d%H%M.%S") {} \; @mkdir -p dist @cd build; tar -c --owner=0 --group=0 --numeric-owner \ --format=gnu -b20 --quoting-style=escape \ -f ../dist/${NAME}-${VERSION}.tar \ $$(find ${NAME}-${VERSION} -type f | sort) @gzip -6 -n dist/${NAME}-${VERSION}.tar @echo "Generated release tarball:" @echo " $$(ls dist/${NAME}-${VERSION}.tar.gz)" @touch build/release-stamp deb: release build/deb-stamp build/deb-stamp: @echo "Building debian packages..." @cd build/${NAME}-${VERSION}; \ dpkg-buildpackage -rfakeroot -us -uc @mv build/*_${VERSION}_*.deb dist/ @echo "Generated debian packages:" @for pkg in $$(ls dist/*_${VERSION}_*.deb); do echo " $${pkg}"; done @touch build/deb-stamp rpm: release build/rpm-stamp build/rpm-stamp: @echo "Building rpm packages..." @mkdir -p build/rpm @build=$$(pwd)/build/rpm; dist=$$(pwd)/dist/; rpmbuild \ --define "_topdir $${build}" --define "_sourcedir $${dist}" \ --define "_rpmdir $${build}" --define "_buildir $${build}" \ --define "_srcrpmdir $${build}" -ba build/${NAME}-${VERSION}/*.spec @mv build/rpm/*-${VERSION}*.src.rpm dist/ @mv build/rpm/*/*-${VERSION}*.rpm dist/ @echo "Generated rpm packages:" @for pkg in $$(ls dist/*-${VERSION}*.rpm); do echo " $${pkg}"; done @touch build/rpm-stamp targetcli-2.1/README0000644000175000017500000000062512165224231012411 0ustar rrsrrstargetcli is an administration tool for managing RisingTide Systems storage targets using the kernel LIO core target and compatible target fabric modules. The targetcli CLI is built on top of the python configshell CLI framework. The latest version of this program might be obtained at: http://www.risingtidesystems.com/git/ To run the CLI from this directory use: sudo PYTHONPATH=. ./scripts/targetcli targetcli-2.1/.gitignore0000644000175000017500000000057312165224231013523 0ustar rrsrrsdebian/changelog dpkg-buildpackage.log dpkg-buildpackage.version build-stamp build/ debian/files debian/rtsadmin-doc.debhelper.log debian/rtsadmin-doc.substvars debian/rtsadmin-doc/ debian/rtsadmin.debhelper.log debian/rtsadmin.substvars debian/rtsadmin/ debian/tmp/ dist/ doc/ *.pyc *.swp dpkg-buildpackage.log dpkg-buildpackage.version ./*.spec redhat/*.spec ./rtsadmin-* log/