diff options
Diffstat (limited to 'rtsadmin/ui_target.py')
| -rw-r--r-- | rtsadmin/ui_target.py | 846 |
1 files changed, 846 insertions, 0 deletions
diff --git a/rtsadmin/ui_target.py b/rtsadmin/ui_target.py new file mode 100644 index 0000000..f3e95dc --- /dev/null +++ b/rtsadmin/ui_target.py @@ -0,0 +1,846 @@ +''' +Implements the rtsadmin target related UI. + +This file is part of RTSAdmin Community Edition. +Copyright (c) 2011 by RisingTide Systems LLC + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, version 3 (AGPLv3). + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +''' + +from ui_node import UINode, UIAttributes, UIParameters +from rtslib import RTSLibError, RTSLibBrokenLink, utils +from rtslib import NodeACL, NetworkPortal, MappedLUN +from rtslib import RTSRoot, Target, TPG, LUN + +class UIFabricModule(UINode): + ''' + A fabric module UI. + ''' + def __init__(self, fabric_module): + UINode.__init__(self) + self.name = fabric_module.name + self.fabric_module = fabric_module + self.cfs_cwd = fabric_module.path + self.refresh() + + def refresh(self): + self._children = set([]) + for target in self.fabric_module.targets: + self.log.debug("Found target %s under fabric module %s." + % (target.wwn, target.fabric_module)) + if target.has_feature('tpgts'): + self.add_child(UIMultiTPGTarget(target)) + else: + self.add_child(UITarget(target)) + + 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} + ''' + target = Target(self.fabric_module, wwn, mode='create') + wwn = target.wwn + self.add_child(UITarget(target)) + self.log.info("Created target %s." % wwn) + + 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.fabric_module.spec + if current_param == 'wwn' and spec['wwn_list'] is not None: + existing_wwns = [child.wwn for child in self.fabric_module.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} + ''' + target = Target(self.fabric_module, wwn, mode='lookup') + target.delete() + self.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.fabric_module.spec + self.log.info("Fabric module name: %s" + % self.name) + self.log.info("ConfigFS path: %s" + % self.fabric_module.path) + if spec['wwn_list'] is not None: + self.log.info("Allowed WWNs list (%s type): %s" + % (spec['wwn_type'], ', '.join(spec['wwn_list']))) + else: + self.log.info("Supported WWN type: %s" + % spec['wwn_type']) + self.log.info("Fabric module specfile: %s" + % self.fabric_module.spec_file) + self.log.info("Fabric module features: %s" + % ', '.join(spec['features'])) + self.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.fabric_module.name, self.fabric_module.version) + self.con.display(version.strip()) + +class UIMultiTPGTarget(UINode): + ''' + A generic target UI that has multiple TPGs. + ''' + def __init__(self, target): + UINode.__init__(self) + self.target = target + self.name = target.wwn + self.cfs_cwd = target.path + self.refresh() + + def refresh(self): + self._children = set([]) + for tpg in self.target.tpgs: + self.add_child(UITPG(tpg)) + + def summary(self): + if not self.target.fabric_module.is_valid_wwn(self.target.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} + ''' + if tag is None: + tags = [tpg.tag for tpg in self.target.tpgs] + for index in range(1048576): + if index not in tags and index > 0: + tag = index + break + if tag is None: + self.log.error("Cannot find an available TPG Tag.") + return + else: + self.log.info("Selected TPG Tag %d." % tag) + else: + try: + tag = int(tag) + except ValueError: + self.log.error("The TPG Tag must be an integer value.") + return + else: + if tag < 1: + self.log.error("The TPG Tag must be >0.") + return + + tpg = TPG(self.target, tag, mode='create') + if self.prefs['auto_enable_tpgt']: + tpg.enable = True + self.log.info("Successfully created TPG %s." % tpg.tag) + self.add_child(UITPG(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} + ''' + tpg = TPG(self.target, tag, mode='lookup') + tpg.delete() + self.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(UINode, UIAttributes, UIParameters): + ''' + A generic TPG UI. + ''' + def __init__(self, tpg): + UINode.__init__(self) + UIAttributes.__init__(self, tpg) + UIParameters.__init__(self, tpg) + self.tpg = tpg + self.name = "tpgt%d" % tpg.tag + self.cfs_cwd = tpg.path + self.refresh() + + self.add_child(UILUNs(tpg)) + + if tpg.has_feature('acls'): + self.add_child(UINodeACLs(self.tpg)) + if tpg.has_feature('nps'): + self.add_child(UIPortals(self.tpg)) + + def summary(self): + if self.tpg.has_feature('nexus'): + description = "%s" % self.tpg.nexus + elif self.tpg.enable: + description = "enabled" + else: + description = "disabled" + return (description, True) + + def ui_command_enable(self): + ''' + Enables the TPG. + + SEE ALSO + ======== + B{disable status} + ''' + if self.tpg.enable: + self.log.info("The TPGT is already enabled.") + else: + self.tpg.enable = True + self.log.info("The TPGT has been enabled.") + + def ui_command_disable(self): + ''' + Disables the TPG. + + SEE ALSO + ======== + B{enable status} + ''' + if self.tpg.enable: + self.tpg.enable = False + self.log.info("The TPGT has been disabled.") + else: + self.log.info("The TPGT is already disabled.") + +class UITarget(UITPG): + ''' + A generic target UI merged with its only TPG. + ''' + # FIXME: This is a workaround for rtslib/kernel-side LIO enforcing at least + # one TPG layer even for non-iSCSI fabric modules, for legacy reasons. + def __init__(self, target): + UITPG.__init__(self, TPG(target, 1)) + self.target = target + self.tpg.enable = True + self.name = target.wwn + + 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): + UINode.__init__(self) + self.name = "acls" + 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: + self.add_child(UINodeACL(node_acl)) + + 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): + ''' + 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. + + SEE ALSO + ======== + B{delete} + ''' + spec = self.tpg.parent_target.fabric_module.spec + if not utils.is_valid_wwn(spec['wwn_type'], wwn): + self.log.error("'%s' is not a valid %s WWN." + % (wwn, spec['wwn_type'])) + return + try: + node_acl = NodeACL(self.tpg, wwn, mode="create") + except RTSLibError, msg: + self.log.error(msg) + return + else: + self.log.info("Successfully created Node ACL for %s" + % node_acl.node_wwn) + self.add_child(UINodeACL(node_acl)) + + def ui_command_delete(self, wwn): + ''' + Deletes the Node ACL with the specified I{wwn}. + + SEE ALSO + ======== + B{create} + ''' + node_acl = NodeACL(self.tpg, wwn, mode='lookup') + node_acl.delete() + self.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(UINode, UIAttributes, UIParameters): + ''' + A generic UI for a node ACL. + ''' + def __init__(self, node_acl): + UINode.__init__(self) + UIAttributes.__init__(self, node_acl) + UIParameters.__init__(self, node_acl) + self.name = node_acl.node_wwn + self.node_acl = node_acl + self.cfs_cwd = node_acl.path + self.refresh() + + def refresh(self): + self._children = set([]) + for mlun in self.node_acl.mapped_luns: + self.add_child(UIMappedLUN(mlun)) + + 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} + ''' + try: + tpg_lun = int(tpg_lun) + mapped_lun = int(mapped_lun) + except ValueError: + self.log.error("Incorrect LUN value.") + return + + mlun = MappedLUN(self.node_acl, mapped_lun, tpg_lun, write_protect) + self.add_child(UIMappedLUN(mlun)) + self.log.info("Created Mapped LUN %s." % mlun.mapped_lun) + + def ui_command_delete(self, mapped_lun): + ''' + Deletes the specified I{mapped_lun}. + + SEE ALSO + ======== + B{create} + ''' + mlun = MappedLUN(self.node_acl, mapped_lun) + mlun.delete() + self.log.info("Deleted Mapped LUN %s." % mapped_lun) + self.refresh() + +class UIMappedLUN(UINode): + ''' + A generic UI for MappedLUN objects. + ''' + def __init__(self, mapped_lun): + UINode.__init__(self) + self.mapped_lun = mapped_lun + self.name = "mapped_lun%d" % mapped_lun.mapped_lun + self.cfs_cwd = mapped_lun.path + self.refresh() + + def summary(self): + mapped_lun = self.mapped_lun + 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): + UINode.__init__(self) + self.name = "luns" + self.cfs_cwd = "%s/lun" % tpg.path + self.tpg = tpg + self.refresh() + + def refresh(self): + self._children = set([]) + for lun in self.tpg.luns: + self.add_child(UILUN(lun)) + + 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): + ''' + 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. 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}. + + SEE ALSO + ======== + B{delete} + ''' + 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.log.error("Cannot find an available LUN.") + return + else: + self.log.info("Selected LUN %d." % lun) + else: + try: + lun = int(lun) + except ValueError: + self.log.error("The LUN must be an integer value.") + return + else: + if lun < 0: + self.log.error("The LUN cannot be negative.") + return + + try: + storage_object = self.get_node(storage_object).storage_object + except AttributeError: + self.log.error("Wrong storage object path.") + + lun_object = LUN(self.tpg, lun, storage_object) + self.log.info("Successfully created LUN %s." % lun_object.lun) + self.add_child(UILUN(lun_object)) + + 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. + + SEE ALSO + ======== + B{create} + ''' + lun_object = LUN(self.tpg, lun) + lun_object.delete() + self.log.info("Successfully deleted LUN %s." % 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 == '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(UINode): + ''' + A generic UI for LUN objects. + ''' + def __init__(self, lun): + UINode.__init__(self) + self.lun = lun + self.name = "lun%d" % lun.lun + self.cfs_cwd = lun.path + self.refresh() + + def summary(self): + lun = self.lun + 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 + description = "%s%s/%s (%s)" % (backstore.plugin, backstore.index, + storage_object.name, path) + + return (description, is_healthy) + +class UIPortals(UINode): + ''' + A generic UI for TPG network portals. + ''' + def __init__(self, tpg): + UINode.__init__(self) + self.tpg = tpg + self.name = "portals" + self.cfs_cwd = "%s/np" % tpg.path + self.refresh() + + def refresh(self): + self._children = set([]) + for portal in self.tpg.network_portals: + self.add_child(UIPortal(portal)) + + 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} + ''' + if ip_port is None: + # FIXME: Add a specfile parameter to determine that + ip_port = 3260 + self.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.log.info("Automatically selected IP address %s." + % ip_address) + else: + self.log.error("Cannot find a usable IP address to " + + "create the Network Portal, aborting.") + return + elif ip_address not in utils.list_eth_ips(): + self.log.error("Provided IP address %s does not exist, aborting." + % ip_address) + return + + try: + ip_port = int(ip_port) + except ValueError: + self.log.error("The ip_port must be an integer value.") + return + + portal = NetworkPortal(self.tpg, ip_address, + ip_port, mode='create') + self.log.info("Successfully created network portal %s:%d." + % (ip_address, ip_port)) + self.add_child(UIPortal(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} + ''' + portal = NetworkPortal(self.tpg, ip_address, ip_port, mode='lookup') + portal.delete() + self.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(UINode): + ''' + A generic UI for a network portal. + ''' + def __init__(self, portal): + UINode.__init__(self) + self.portal = portal + self.name = "%s:%s" % (portal.ip_address, portal.port) + self.cfs_cwd = portal.path + self.refresh() + + def summary(self): + return ('', True) + |
