class UIRoot(UINode): ''' The targetcli hierarchy root node. ''' def __init__(self, shell, as_root=False): UINode.__init__(self, '/', shell=shell) self.as_root = as_root self.rtsroot = RTSRoot() def refresh(self): ''' Refreshes the tree of target fabric modules. ''' self._children = set([]) # Invalidate any rtslib caches if 'invalidate_caches' in dir(RTSRoot): self.rtsroot.invalidate_caches() UIBackstores(self) # only show fabrics present in the system for fm in self.rtsroot.fabric_modules: if fm.wwns == None or any(fm.wwns): UIFabricModule(fm, self) def _compare_files(self, backupfile, savefile): ''' Compare backfile and saveconfig file ''' if (os.path.splitext(backupfile)[1] == '.gz'): try: with gzip.open(backupfile, 'rb') as fbkp: fdata_bkp = fbkp.read() except IOError as e: self.shell.log.warning( "Could not gzip open backupfile %s: %s" % (backupfile, e.strerror)) else: try: with open(backupfile, 'rb') as fbkp: fdata_bkp = fbkp.read() except IOError as e: self.shell.log.warning("Could not open backupfile %s: %s" % (backupfile, e.strerror)) try: with open(savefile, 'rb') as f: fdata = f.read() except IOError as e: self.shell.log.warning("Could not open saveconfig file %s: %s" % (savefile, e.strerror)) if fdata_bkp == fdata: return True else: return False def _save_backups(self, savefile): ''' Take backup of config-file if needed. ''' # Only save backups if saving to default location if savefile != default_save_file: return backup_dir = os.path.dirname(savefile) + "/backup/" backup_name = "saveconfig-" + \ datetime.now().strftime("%Y%m%d-%H:%M:%S") + "-json.gz" backupfile = backup_dir + backup_name backup_error = None if not os.path.exists(backup_dir): try: os.makedirs(backup_dir) except OSError as exe: raise ExecutionError( "Cannot create backup directory [%s] %s." % (backup_dir, exe.strerror)) # Only save backups if savefile exits if not os.path.exists(savefile): return backed_files_list = sorted(glob(os.path.dirname(savefile) + \ "/backup/saveconfig-*json*")) # Save backup if backup dir is empty, or savefile is differnt from recent backup copy if not backed_files_list or not self._compare_files( backed_files_list[-1], savefile): try: with open(savefile, 'rb') as f_in, gzip.open(backupfile, 'wb') as f_out: shutil.copyfileobj(f_in, f_out) f_out.flush() except IOError as ioe: backup_error = ioe.strerror or "Unknown error" if backup_error == None: # remove excess backups max_backup_files = int(self.shell.prefs['max_backup_files']) try: with open(universal_prefs_file) as prefs: backups = [ line for line in prefs.read().splitlines() if re.match('^max_backup_files\s*=', line) ] if max_backup_files < int( backups[0].split('=')[1].strip()): max_backup_files = int( backups[0].split('=')[1].strip()) except: self.shell.log.debug("No universal prefs file '%s'." % universal_prefs_file) files_to_unlink = list( reversed(backed_files_list))[max_backup_files - 1:] for f in files_to_unlink: with ignored(IOError): os.unlink(f) self.shell.log.info("Last %d configs saved in %s." % (max_backup_files, backup_dir)) else: self.shell.log.warning("Could not create backup file %s: %s." % (backupfile, backup_error)) def ui_command_saveconfig(self, savefile=default_save_file): ''' Saves the current configuration to a file so that it can be restored on next boot. ''' self.assert_root() if not savefile: savefile = default_save_file savefile = os.path.expanduser(savefile) self._save_backups(savefile) self.rtsroot.save_to_file(savefile) self.shell.log.info("Configuration saved to %s" % savefile) def ui_command_restoreconfig(self, savefile=default_save_file, clear_existing=False, target=None, storage_object=None): ''' Restores configuration from a file. ''' self.assert_root() savefile = os.path.expanduser(savefile) if not os.path.isfile(savefile): self.shell.log.info("Restore file %s not found" % savefile) return target = self.ui_eval_param(target, 'string', None) storage_object = self.ui_eval_param(storage_object, 'string', None) errors = self.rtsroot.restore_from_file(savefile, clear_existing, target, storage_object) self.refresh() if errors: raise ExecutionError("Configuration restored, %d recoverable errors:\n%s" % \ (len(errors), "\n".join(errors))) self.shell.log.info("Configuration restored from %s" % savefile) def ui_complete_saveconfig(self, parameters, text, current_param): ''' Auto-completes the file name ''' if current_param != 'savefile': return [] completions = complete_path(text, stat.S_ISREG) if len(completions) == 1 and not completions[0].endswith('/'): completions = [completions[0] + ' '] return completions ui_complete_restoreconfig = ui_complete_saveconfig def ui_command_clearconfig(self, confirm=None): ''' Removes entire configuration of backstores and targets ''' self.assert_root() confirm = self.ui_eval_param(confirm, 'bool', False) self.rtsroot.clear_existing(confirm=confirm) self.shell.log.info("All configuration cleared") self.refresh() def ui_command_version(self): ''' Displays the targetcli and support libraries versions. ''' from targetcli import __version__ as targetcli_version self.shell.log.info("targetcli version %s" % targetcli_version) def ui_command_sessions(self, action="list", sid=None): ''' Displays a detailed list of all open sessions. PARAMETERS ========== action ------ The action is one of: - `list`` gives a short session list - `detail` gives a detailed list sid --- You can specify an "sid" to only list this one, with or without details. SEE ALSO ======== status ''' indent_step = 4 base_steps = 0 action_list = ("list", "detail") if action not in action_list: raise ExecutionError("action must be one of: %s" % ", ".join(action_list)) if sid is not None: try: int(sid) except ValueError: raise ExecutionError("sid must be a number, '%s' given" % sid) def indent_print(text, steps): console = self.shell.con console.display(console.indent(text, indent_step * steps), no_lf=True) def print_session(session): acl = session['parent_nodeacl'] indent_print("alias: %(alias)s\tsid: %(id)i type: " \ "%(type)s session-state: %(state)s" % session, base_steps) if action == 'detail': if self.as_root: if acl.authenticate_target: auth = " (authenticated)" else: auth = " (NOT AUTHENTICATED)" else: auth = "" indent_print("name: %s%s" % (acl.node_wwn, auth), base_steps + 1) for mlun in acl.mapped_luns: plugin = mlun.tpg_lun.storage_object.plugin name = mlun.tpg_lun.storage_object.name if mlun.write_protect: mode = "r" else: mode = "rw" indent_print( "mapped-lun: %d backstore: %s/%s mode: %s" % (mlun.mapped_lun, plugin, name, mode), base_steps + 1) for connection in session['connections']: indent_print("address: %(address)s (%(transport)s) cid: " \ "%(cid)i connection-state: %(cstate)s" % \ connection, base_steps + 1) if sid: printed_sessions = [ x for x in self.rtsroot.sessions if x['id'] == int(sid) ] else: printed_sessions = list(self.rtsroot.sessions) if len(printed_sessions): for session in printed_sessions: print_session(session) else: if sid is None: indent_print("(no open sessions)", base_steps) else: raise ExecutionError("no session found with sid %i" % int(sid))
class UIRoot(UINode): ''' The targetcli hierarchy root node. ''' def __init__(self, shell, as_root=False): UINode.__init__(self, '/', shell=shell) self.as_root = as_root self.rtsroot = RTSRoot() def refresh(self): ''' Refreshes the tree of target fabric modules. ''' self._children = set([]) UIBackstores(self) # only show fabrics present in the system for fm in self.rtsroot.fabric_modules: if fm.wwns == None or any(fm.wwns): UIFabricModule(fm, self) def ui_command_saveconfig(self, savefile=default_save_file): ''' Saves the current configuration to a file so that it can be restored on next boot. ''' self.assert_root() savefile = os.path.expanduser(savefile) # Only save backups if saving to default location if savefile == default_save_file: backup_dir = os.path.dirname(savefile) + "/backup" backup_name = "saveconfig-" + \ datetime.now().strftime("%Y%m%d-%H:%M:%S") + ".json" backupfile = backup_dir + "/" + backup_name with ignored(IOError): shutil.copy(savefile, backupfile) # Kill excess backups backups = sorted( glob(os.path.dirname(savefile) + "/backup/*.json")) files_to_unlink = list(reversed(backups))[kept_backups:] for f in files_to_unlink: os.unlink(f) self.shell.log.info("Last %d configs saved in %s." % \ (kept_backups, backup_dir)) self.rtsroot.save_to_file(savefile) self.shell.log.info("Configuration saved to %s" % savefile) def ui_command_restoreconfig(self, savefile=default_save_file, clear_existing=False): ''' Restores configuration from a file. ''' self.assert_root() savefile = os.path.expanduser(savefile) if not os.path.isfile(savefile): self.shell.log.info("Restore file %s not found" % savefile) return errors = self.rtsroot.restore_from_file(savefile, clear_existing) self.refresh() if errors: raise ExecutionError("Configuration restored, %d recoverable errors:\n%s" % \ (len(errors), "\n".join(errors))) self.shell.log.info("Configuration restored from %s" % savefile) def ui_complete_saveconfig(self, parameters, text, current_param): ''' Auto-completes the file name ''' if current_param != 'savefile': return [] completions = complete_path(text, stat.S_ISREG) if len(completions) == 1 and not completions[0].endswith('/'): completions = [completions[0] + ' '] return completions ui_complete_restoreconfig = ui_complete_saveconfig def ui_command_clearconfig(self, confirm=None): ''' Removes entire configuration of backstores and targets ''' self.assert_root() confirm = self.ui_eval_param(confirm, 'bool', False) self.rtsroot.clear_existing(confirm=confirm) self.shell.log.info("All configuration cleared") self.refresh() def ui_command_version(self): ''' Displays the targetcli and support libraries versions. ''' from targetcli import __version__ as targetcli_version self.shell.log.info("targetcli version %s" % targetcli_version) def ui_command_sessions(self, action="list", sid=None): ''' Displays a detailed list of all open sessions. PARAMETERS ========== I{action} --------- The I{action} is one of: - B{list} gives a short session list - B{detail} gives a detailed list I{sid} ------ You can specify an I{sid} to only list this one, with or without details. SEE ALSO ======== status ''' indent_step = 4 base_steps = 0 action_list = ("list", "detail") if action not in action_list: raise ExecutionError("action must be one of: %s" % ", ".join(action_list)) if sid is not None: try: int(sid) except ValueError: raise ExecutionError("sid must be a number, '%s' given" % sid) def indent_print(text, steps): console = self.shell.con console.display(console.indent(text, indent_step * steps), no_lf=True) def print_session(session): acl = session['parent_nodeacl'] indent_print("alias: %(alias)s\tsid: %(id)i type: " \ "%(type)s session-state: %(state)s" % session, base_steps) if action == 'detail': if self.as_root: if acl.authenticate_target: auth = " (authenticated)" else: auth = " (NOT AUTHENTICATED)" else: auth = "" indent_print("name: %s%s" % (acl.node_wwn, auth), base_steps + 1) for mlun in acl.mapped_luns: plugin = mlun.tpg_lun.storage_object.plugin name = mlun.tpg_lun.storage_object.name if mlun.write_protect: mode = "r" else: mode = "rw" indent_print( "mapped-lun: %d backstore: %s/%s mode: %s" % (mlun.mapped_lun, plugin, name, mode), base_steps + 1) for connection in session['connections']: indent_print("address: %(address)s (%(transport)s) cid: " \ "%(cid)i connection-state: %(cstate)s" % \ connection, base_steps + 1) if sid: printed_sessions = [ x for x in self.rtsroot.sessions if x['id'] == int(sid) ] else: printed_sessions = list(self.rtsroot.sessions) if len(printed_sessions): for session in printed_sessions: print_session(session) else: if sid is None: indent_print("(no open sessions)", base_steps) else: raise ExecutionError("no session found with sid %i" % int(sid))
class TargetManager: 'Manages ZVOL based iSCSI targets for Emulab diskless booting' # Constructor def __init__(self): self.block_store = {} self.target = {} self.root = RTSRoot() self.iscsi = FabricModule('iscsi') self.mapped_luns = {} self.get_block_store_objects() self.get_targets() def save(self): '''Save the current configuration''' self.root.save_to_file() # Get list of block storage objects def get_block_store_objects(self): self.block_store = {} for storage_object in self.root.storage_objects: if storage_object.plugin == "block": self.block_store[storage_object.name] = storage_object # Get a list of iscsi targets and associated luns, acls and portals # This builds a data structure that is a hash that hash other hashes # as values, and then other hashes, etc. To see what the data structure # looks like, run targetcli from the command line and issue the ls command. # # This data structure mimics that list for fast lookup for creating # shares for lots of nodes. # # This code is really confusing, in case you couldn't tell. # # target 0..N -> target.wwn # | # +---tpgs List of target portal groups, this code assumes only one # | self.target[wwn]['tpg'][tpg.tag]['acl'][initiator_name'] # = mapped_lun # | # +--acls List of initiator names that can log into this iscsi # target # | self.target[wwn]['tpg'][tpg.tag]['acl'] = { # initiator_name : acl # } # | # +--luns List of LUNS for this TPG # | self.target[wwn]['lun'][lun.storage_object.name] = lun # | # +--portals List of portals for this TPG # self.target[wwn]['portal'][portal.ip_address:portal.port] = portal # There can be any number of targets, each uniquely identified by its wwn # (World Wide Name) which is also known as the initiator name. This is # the unique name assigned to each client. The client knows about this # name either by looking at its kernel parameters, the initiator name # stored in the BIOS, but usually in /etc/iscsi/initiatorname.iscsi # # self.target[wwn]['tpg'] [tpg.tag] ['acl'] [initiator_name] = # MappedLUN object # self.target[wwn]['lun'] [lun_storage_object.name] = LUN object # self.target[wwn]['portal'] [portal_id] = Portal object # def get_targets(self): for target in list(self.iscsi.targets): wwn = target.wwn self.target[wwn] = {'target': target, 'tpg': {}} for tpg in target.tpgs: self.target[wwn]['tpg'][tpg.tag] = { 'tpg': tpg, 'acl': {}, 'lun': {}, 'portal': {} } tpg_tag = self.target[wwn]['tpg'][tpg.tag] for acl in tpg.node_acls: tpg_tag['acl'][acl.node_wwn] = acl for lun in tpg.luns: tpg_tag['lun'][lun.storage_object.name] = lun for portal in tpg.network_portals: portal_id = portal.ip_address + ":" + str(portal.port) tpg_tag['portal'][portal_id] = portal # Create a share def create_iscsi_target(self, params): """Create an iSCSI target Parameters ---------- params : dict Dictionary of parameters wwn: The World Wide Name of the share, eg, the IQN device: the backing device initiators: list of initiators """ wwn = params.get('wwn', None) device = params.get('device', None) initiators = params.get('initiators', None) ip = params.get('ip', '0.0.0.0') port = params.get('port', 3260) # Something outside this library lowercase the wwn, so # we lowercase the input to stay consistent if wwn is not None: wwn = wwn.lower() # If at any step, something needs to be created, # then true is returned to the caller to show that # this iscsi target needed to be created. # # It is possible to call this method for an existing # iscsi target, in which case this method does nothing # # By tracking this behavior, the caller can be informed # whether or not any action was taken created = None # Create blockstore, if needed blockstore = self.get_block_store(wwn) if blockstore is None: blockstore = self.create_block_store(wwn, device) created = True else: Log.info('block backstore %s already exists, not creating' % (wwn)) # Create target target = self.get_target(wwn) if target is None: target = self.create_target(wwn) created = True else: Log.info('target %s already exists, not creating' % (wwn)) # Create TPG tag = 1 tpg = self.get_tpg(target, tag) if tpg is None: tpg = self.create_tpg(target, tag) created = True else: Log.info('tpg (%s, %s) already exists, not creating' % (target, tag)) # Create LUN # First, check to see if there are any LUNs. More than one LUN is not # supported, so we just iterate over all (eg, the one) lun and set it. # If there's more than one LUN, then the last one will be the LUN that # is used, which may result in undefined behavior lun = None for lun in tpg.luns: pass if lun is None: lun = self.create_lun(tpg, blockstore) created = True else: Log.info('lun %s already exists, not creating' % (blockstore.name)) # Create portal portal = self.get_portal(tpg, ip, port) if portal is None: portal = self.create_portal(tpg, ip, port) created = True else: portal_id = self.get_portal_id(ip, port) Log.info('portal %s already exists, not creating' % (portal_id)) # Set up ACLs and mapped LUNs for initiator in initiators: # Create ACL acl = self.get_acl(tpg, initiator) if acl is None: acl = self.create_acl(tpg, initiator) created = True else: Log.info('acl (%s, %s) already exists, not creating' % (tpg, initiator)) # Map LUN num = 0 # Like with LUNs, only one mapped lun is supported. Check for # a mapped lun by iterating over the entire set of mapped luns, # use the last one in the list, if any exist. # # If things are working properly, there should be only one mapped_lun = None for mapped_lun in acl.mapped_luns: pass if mapped_lun is None: mapped_lun = self.create_mapped_lun(acl, num, lun) created = True else: Log.info('mapped lun (%s, %s, %s) already exists' % (acl, num, lun)) return created def delete_target_and_block_store(self, params): """Delete an iSCSI target and block store. This does not delete the underlying storage Parameters ---------- target_wwn : string The world wide name of the share to remove """ wwn = params.get('wwn', None) if wwn is None: raise ValueError('No wwn specified') # Delete target self.delete_target(wwn) # Delete blockstore self.delete_block_store(wwn) def get_block_store(self, wwn): """Get an existing block store, if it exists Parameters ---------- wwn : string World Wide Name for the block store device : string Path to a block device Returns: -------- If the block store exists, then that object is returned. Otherwise, None is returned """ return self.block_store.get(wwn, None) def create_block_store(self, wwn, device): """Create a blockstore with the given wwn. It is assumed that the blockstore does not already exists. Calling this method when the storage already exists can potentially result in an exception being thrown. Call get_block_store first to check for existence. Parameters ---------- wwn : string World Wide Name for the block store device : string Path to a block device Returns: -------- blockstore object, if it was successfully created """ Log.info('creating block backstore %s for device %s' % (wwn, device)) storage = BlockStorageObject(wwn, device, wwn) self.block_store[wwn] = storage return storage # Delete blockstore, if it exists def delete_block_store(self, name): store = self.block_store.get(name) # If blockstore doesn't exist, do not proceed if store is None: Log.info('No block store %s. Not deleting' % name) return Log.info('deleting block store %s' % (name)) # Delete the block store. The backing device, file, etc, still exists store.delete() del self.block_store[name] # Delete target, if it exists def delete_target(self, wwn): # See if the target exists target_dict = self.target.get(wwn, None) # Doesn't exist, don't proceed if target_dict is None: Log.info('No target %s. Not deleting' % wwn) return target = target_dict.get('target', None) # Surprising, but possible, because processes can die # and the state can strange if target is None: return Log.info('deleting target %s' % (wwn)) # Delete the target target.delete() del self.target[wwn] def get_target(self, wwn): '''Get an existing target object for the wwn Parameters ---------- wwn : string The wwn of the target Returns ------- The target object if it exists, None otherwise ''' target_dict = self.target.get(wwn, None) target = None if target_dict is not None: target = target_dict['target'] return target # Create target, if needed def create_target(self, wwn): target_dict = self.target.get(wwn, None) target = None if target_dict is None: Log.info('creating target with wwn %s' % (wwn)) # The wwn will be lowercased automatically by something # outside this library. I'm not sure if its RTSLib or # the underlying Linux target system target = Target(self.iscsi, wwn) # Add target to data structure, initialize empty child nodes self.target[wwn] = {'target': target, 'tpg': {}} else: Log.info('target %s already exists, not creating' % (wwn)) target = target_dict['target'] return target def get_tpg(self, target, tag): '''Get a target portal group Parameters ---------- target: Target The target tag: Tag The tag Returns ------- The target portal group, if it exists, None otherwise ''' tpg_list = self.target[target.wwn]['tpg'] tpg_list_tag = tpg_list.get(tag, None) tpg = None if tpg_list_tag is not None: tpg = tpg_list[tag]['tpg'] return tpg # Create TPG, if needed def create_tpg(self, target, tag): tpg_list = self.target[target.wwn]['tpg'] tpg_list_tag = tpg_list.get(tag, None) if tpg_list_tag is None: Log.info('creating tpg (%s, %s)' % (target, tag)) # Create and configure the target portal group tpg = TPG(target, tag) tpg.set_attribute("authentication", 0) tpg.enable = 1 # Set up the list of TPGs for this target tpg_list[tag] = { 'tpg': tpg, 'acl': { 'mapped_lun': {} }, 'lun': {}, 'portal': {} } else: Log.info('tpg (%s, %s) already exists, not creating' % (target, tag)) tpg = tpg_list[tag]['tpg'] return tpg # Create LUN, if needed def create_lun(self, tpg, blockstore): wwn = tpg.parent_target.wwn lun_list = self.target[wwn]['tpg'][tpg.tag]['lun'] lun = lun_list.get(blockstore.name, None) if lun is None: Log.info('creating lun %s, blockstore %s' % (tpg, blockstore)) # Create the LUN lun = LUN(tpg, 0, blockstore) # Add it to the local data structure for tracking LUNs lun_list[blockstore.name] = lun else: # LUN already exists Log.info('lun %s already exists, not creating' % (blockstore.name)) return lun def get_portal_id(self, ip, port): return '%s:%d' % (ip, port) def get_portal(self, tpg, ip, port): portal = None portal_id = self.get_portal_id(ip, port) wwn = tpg.parent_target.wwn portal_list = self.target[wwn]['tpg'][tpg.tag]['portal'] return portal_list.get(portal_id, None) # Create portal, if needed def create_portal(self, tpg, ip, port): portal = None portal_id = self.get_portal_id(ip, port) wwn = tpg.parent_target.wwn portal_list = self.target[wwn]['tpg'][tpg.tag]['portal'] if portal_id in portal_list: Log.info('portal %s already exists, not creating' % (portal_id)) portal = portal_list[portal_id] else: Log.info('creating portal (%s, %s, %s)' % (tpg, ip, port)) portal = NetworkPortal(tpg, ip, port) portal_list[portal_id] = portal return portal def get_acl(self, tpg, initiator_name): acl = None wwn = tpg.parent_target.wwn acl_list = self.target[wwn]['tpg'][tpg.tag]['acl'] return acl_list.get(initiator_name, None) # Create ACL, if needed def create_acl(self, tpg, initiator_name): acl = None wwn = tpg.parent_target.wwn acl_list = self.target[wwn]['tpg'][tpg.tag]['acl'] if initiator_name in acl_list: Log.info('acl (%s, %s) already exists, not creating' % (tpg, initiator_name)) acl = acl_list[initiator_name] else: Log.info('creating acl (%s, %s)' % (tpg, initiator_name)) acl = NodeACL(tpg, initiator_name) acl_list[initiator_name] = acl return acl # Create mapped lun, if needed def create_mapped_lun(self, acl, num, lun): mapped_lun = None if not list(acl.mapped_luns): Log.info('creating mapped lun (%s, %s, %s)' % (acl, num, lun)) mapped_lun = MappedLUN(acl, num, lun) else: Log.info('mapped lun (%s, %s, %s) already exists' % (acl, num, lun)) return mapped_lun
class UIRoot(UINode): """ The targetcli hierarchy root node. """ def __init__(self, shell, as_root=False): UINode.__init__(self, "/", shell=shell) self.as_root = as_root self.rtsroot = RTSRoot() def refresh(self): """ Refreshes the tree of target fabric modules. """ self._children = set([]) UIBackstores(self) # only show fabrics present in the system for fm in self.rtsroot.fabric_modules: if fm.wwns == None or any(fm.wwns): UIFabricModule(fm, self) def ui_command_saveconfig(self, savefile=default_save_file): """ Saves the current configuration to a file so that it can be restored on next boot. """ self.assert_root() savefile = os.path.expanduser(savefile) # Only save backups if saving to default location if savefile == default_save_file: backup_dir = os.path.dirname(savefile) + "/backup" backup_name = "saveconfig-" + datetime.now().strftime("%Y%m%d-%H:%M:%S") + ".json" backupfile = backup_dir + "/" + backup_name with ignored(IOError): shutil.copy(savefile, backupfile) # Kill excess backups backups = sorted(glob(os.path.dirname(savefile) + "/backup/*.json")) files_to_unlink = list(reversed(backups))[kept_backups:] for f in files_to_unlink: os.unlink(f) self.shell.log.info("Last %d configs saved in %s." % (kept_backups, backup_dir)) self.rtsroot.save_to_file(savefile) self.shell.log.info("Configuration saved to %s" % savefile) def ui_command_restoreconfig(self, savefile=default_save_file, clear_existing=False): """ Restores configuration from a file. """ self.assert_root() savefile = os.path.expanduser(savefile) if not os.path.isfile(savefile): self.shell.log.info("Restore file %s not found" % savefile) return errors = self.rtsroot.restore_from_file(savefile, clear_existing) self.refresh() if errors: raise ExecutionError( "Configuration restored, %d recoverable errors:\n%s" % (len(errors), "\n".join(errors)) ) self.shell.log.info("Configuration restored from %s" % savefile) def ui_complete_saveconfig(self, parameters, text, current_param): """ Auto-completes the file name """ if current_param != "savefile": return [] completions = complete_path(text, stat.S_ISREG) if len(completions) == 1 and not completions[0].endswith("/"): completions = [completions[0] + " "] return completions ui_complete_restoreconfig = ui_complete_saveconfig def ui_command_clearconfig(self, confirm=None): """ Removes entire configuration of backstores and targets """ self.assert_root() confirm = self.ui_eval_param(confirm, "bool", False) self.rtsroot.clear_existing(confirm=confirm) self.shell.log.info("All configuration cleared") self.refresh() def ui_command_version(self): """ Displays the targetcli and support libraries versions. """ from targetcli import __version__ as targetcli_version self.shell.log.info("targetcli version %s" % targetcli_version) def ui_command_sessions(self, action="list", sid=None): """ Displays a detailed list of all open sessions. PARAMETERS ========== I{action} --------- The I{action} is one of: - B{list} gives a short session list - B{detail} gives a detailed list I{sid} ------ You can specify an I{sid} to only list this one, with or without details. SEE ALSO ======== status """ indent_step = 4 base_steps = 0 action_list = ("list", "detail") if action not in action_list: raise ExecutionError("action must be one of: %s" % ", ".join(action_list)) if sid is not None: try: int(sid) except ValueError: raise ExecutionError("sid must be a number, '%s' given" % sid) def indent_print(text, steps): console = self.shell.con console.display(console.indent(text, indent_step * steps), no_lf=True) def print_session(session): acl = session["parent_nodeacl"] indent_print( "alias: %(alias)s\tsid: %(id)i type: " "%(type)s session-state: %(state)s" % session, base_steps ) if action == "detail": if self.as_root: if acl.authenticate_target: auth = " (authenticated)" else: auth = " (NOT AUTHENTICATED)" else: auth = "" indent_print("name: %s%s" % (acl.node_wwn, auth), base_steps + 1) for mlun in acl.mapped_luns: plugin = mlun.tpg_lun.storage_object.plugin name = mlun.tpg_lun.storage_object.name if mlun.write_protect: mode = "r" else: mode = "rw" indent_print( "mapped-lun: %d backstore: %s/%s mode: %s" % (mlun.mapped_lun, plugin, name, mode), base_steps + 1, ) for connection in session["connections"]: indent_print( "address: %(address)s (%(transport)s) cid: " "%(cid)i connection-state: %(cstate)s" % connection, base_steps + 1, ) if sid: printed_sessions = [x for x in self.rtsroot.sessions if x["id"] == int(sid)] else: printed_sessions = list(self.rtsroot.sessions) if len(printed_sessions): for session in printed_sessions: print_session(session) else: if sid is None: indent_print("(no open sessions)", base_steps) else: raise ExecutionError("no session found with sid %i" % int(sid))
class TargetManager: 'Manages ZVOL based iSCSI targets for Emulab diskless booting' # Constructor def __init__(self): self.block_store = {} self.target = {} self.root = RTSRoot() self.iscsi = FabricModule('iscsi') self.mapped_luns = {} self.get_block_store_objects() self.get_targets() def save(self): '''Save the current configuration''' self.root.save_to_file() # Get list of block storage objects def get_block_store_objects(self): self.block_store = {} for storage_object in self.root.storage_objects: if storage_object.plugin == "block": self.block_store[storage_object.name] = storage_object # Get a list of iscsi targets and associated luns, acls and portals # This builds a data structure that is a hash that hash other hashes # as values, and then other hashes, etc. To see what the data structure # looks like, run targetcli from the command line and issue the ls command. # # This data structure mimics that list for fast lookup for creating # shares for lots of nodes. # # This code is really confusing, in case you couldn't tell. # # target 0..N -> target.wwn # | # +---tpgs List of target portal groups, this code assumes only one # | self.target[wwn]['tpg'][tpg.tag]['acl'][initiator_name'] # = mapped_lun # | # +--acls List of initiator names that can log into this iscsi # target # | self.target[wwn]['tpg'][tpg.tag]['acl'] = { # initiator_name : acl # } # | # +--luns List of LUNS for this TPG # | self.target[wwn]['lun'][lun.storage_object.name] = lun # | # +--portals List of portals for this TPG # self.target[wwn]['portal'][portal.ip_address:portal.port] = portal # There can be any number of targets, each uniquely identified by its wwn # (World Wide Name) which is also known as the initiator name. This is # the unique name assigned to each client. The client knows about this # name either by looking at its kernel parameters, the initiator name # stored in the BIOS, but usually in /etc/iscsi/initiatorname.iscsi # # self.target[wwn]['tpg'] [tpg.tag] ['acl'] [initiator_name] = # MappedLUN object # self.target[wwn]['lun'] [lun_storage_object.name] = LUN object # self.target[wwn]['portal'] [portal_id] = Portal object # def get_targets(self): for target in list(self.iscsi.targets): wwn = target.wwn self.target[wwn] = {'target': target, 'tpg': {}} for tpg in target.tpgs: self.target[wwn]['tpg'][tpg.tag] = { 'tpg': tpg, 'acl': {}, 'lun': {}, 'portal': {} } tpg_tag = self.target[wwn]['tpg'][tpg.tag] for acl in tpg.node_acls: tpg_tag['acl'][acl.node_wwn] = acl for lun in tpg.luns: tpg_tag['lun'][lun.storage_object.name] = lun for portal in tpg.network_portals: portal_id = portal.ip_address + ":" + str(portal.port) tpg_tag['portal'][portal_id] = portal # Create a share def create_iscsi_target(self, params): """Create an iSCSI target Parameters ---------- params : dict Dictionary of parameters wwn: The World Wide Name of the share, eg, the IQN device: the backing device initiators: list of initiators """ wwn = params.get('wwn', None) device = params.get('device', None) initiators = params.get('initiators', None) ip = params.get('ip', '0.0.0.0') port = params.get('port', 3260) # Something outside this library lowercase the wwn, so # we lowercase the input to stay consistent if wwn is not None: wwn = wwn.lower() # If at any step, something needs to be created, # then true is returned to the caller to show that # this iscsi target needed to be created. # # It is possible to call this method for an existing # iscsi target, in which case this method does nothing # # By tracking this behavior, the caller can be informed # whether or not any action was taken created = None # Create blockstore, if needed blockstore = self.get_block_store(wwn) if blockstore is None: blockstore = self.create_block_store(wwn, device) created = True else: Log.info('block backstore %s already exists, not creating' % (wwn)) # Create target target = self.get_target(wwn) if target is None: target = self.create_target(wwn) created = True else: Log.info('target %s already exists, not creating' % (wwn)) # Create TPG tag = 1 tpg = self.get_tpg(target, tag) if tpg is None: tpg = self.create_tpg(target, tag) created = True else: Log.info('tpg (%s, %s) already exists, not creating' % (target, tag)) # Create LUN # First, check to see if there are any LUNs. More than one LUN is not # supported, so we just iterate over all (eg, the one) lun and set it. # If there's more than one LUN, then the last one will be the LUN that # is used, which may result in undefined behavior lun = None for lun in tpg.luns: pass if lun is None: lun = self.create_lun(tpg, blockstore) created = True else: Log.info('lun %s already exists, not creating' % (blockstore.name)) # Create portal portal = self.get_portal(tpg, ip, port) if portal is None: portal = self.create_portal(tpg, ip, port) created = True else: portal_id = self.get_portal_id(ip, port) Log.info('portal %s already exists, not creating' % (portal_id)) # Set up ACLs and mapped LUNs for initiator in initiators: # Create ACL acl = self.get_acl(tpg, initiator) if acl is None: acl = self.create_acl(tpg, initiator) created = True else: Log.info('acl (%s, %s) already exists, not creating' % (tpg, initiator)) # Map LUN num = 0 # Like with LUNs, only one mapped lun is supported. Check for # a mapped lun by iterating over the entire set of mapped luns, # use the last one in the list, if any exist. # # If things are working properly, there should be only one mapped_lun = None for mapped_lun in acl.mapped_luns: pass if mapped_lun is None: mapped_lun = self.create_mapped_lun(acl, num, lun) created = True else: Log.info('mapped lun (%s, %s, %s) already exists' % (acl, num, lun)) return created def delete_target_and_block_store(self, params): """Delete an iSCSI target and block store. This does not delete the underlying storage Parameters ---------- target_wwn : string The world wide name of the share to remove """ wwn = params.get('wwn', None) if wwn is None: raise ValueError('No wwn specified') # Delete target self.delete_target(wwn) # Delete blockstore self.delete_block_store(wwn) def get_block_store(self, wwn): """Get an existing block store, if it exists Parameters ---------- wwn : string World Wide Name for the block store device : string Path to a block device Returns: -------- If the block store exists, then that object is returned. Otherwise, None is returned """ return self.block_store.get(wwn, None) def create_block_store(self, wwn, device): """Create a blockstore with the given wwn. It is assumed that the blockstore does not already exists. Calling this method when the storage already exists can potentially result in an exception being thrown. Call get_block_store first to check for existence. Parameters ---------- wwn : string World Wide Name for the block store device : string Path to a block device Returns: -------- blockstore object, if it was successfully created """ Log.info('creating block backstore %s for device %s' % (wwn, device)) storage = BlockStorageObject(wwn, device, wwn) self.block_store[wwn] = storage return storage # Delete blockstore, if it exists def delete_block_store(self, name): store = self.block_store.get(name) # If blockstore doesn't exist, do not proceed if store is None: Log.info('No block store %s. Not deleting' % name) return Log.info('deleting block store %s' % (name)) # Delete the block store. The backing device, file, etc, still exists store.delete() del self.block_store[name] # Delete target, if it exists def delete_target(self, wwn): # See if the target exists target_dict = self.target.get(wwn, None) # Doesn't exist, don't proceed if target_dict is None: Log.info('No target %s. Not deleting' % wwn) return target = target_dict.get('target', None) # Surprising, but possible, because processes can die # and the state can strange if target is None: return Log.info('deleting target %s' % (wwn)) # Delete the target target.delete() del self.target[wwn] def get_target(self, wwn): '''Get an existing target object for the wwn Parameters ---------- wwn : string The wwn of the target Returns ------- The target object if it exists, None otherwise ''' target_dict = self.target.get(wwn, None) target = None if target_dict is not None: target = target_dict['target'] return target # Create target, if needed def create_target(self, wwn): target_dict = self.target.get(wwn, None) target = None if target_dict is None: Log.info('creating target with wwn %s' % (wwn)) # The wwn will be lowercased automatically by something # outside this library. I'm not sure if its RTSLib or # the underlying Linux target system target = Target(self.iscsi, wwn) # Add target to data structure, initialize empty child nodes self.target[wwn] = {'target': target, 'tpg': {}} else: Log.info('target %s already exists, not creating' % (wwn)) target = target_dict['target'] return target def get_tpg(self, target, tag): '''Get a target portal group Parameters ---------- target: Target The target tag: Tag The tag Returns ------- The target portal group, if it exists, None otherwise ''' tpg_list = self.target[target.wwn]['tpg'] tpg_list_tag = tpg_list.get(tag, None) tpg = None if tpg_list_tag is not None: tpg = tpg_list[tag]['tpg'] return tpg # Create TPG, if needed def create_tpg(self, target, tag): tpg_list = self.target[target.wwn]['tpg'] tpg_list_tag = tpg_list.get(tag, None) if tpg_list_tag is None: Log.info('creating tpg (%s, %s)' % (target, tag)) # Create and configure the target portal group tpg = TPG(target, tag) tpg.set_attribute("authentication", 0) tpg.enable = 1 # Set up the list of TPGs for this target tpg_list[tag] = { 'tpg': tpg, 'acl': {'mapped_lun': {}}, 'lun': {}, 'portal': {} } else: Log.info('tpg (%s, %s) already exists, not creating' % (target, tag)) tpg = tpg_list[tag]['tpg'] return tpg # Create LUN, if needed def create_lun(self, tpg, blockstore): wwn = tpg.parent_target.wwn lun_list = self.target[wwn]['tpg'][tpg.tag]['lun'] lun = lun_list.get(blockstore.name, None) if lun is None: Log.info('creating lun %s, blockstore %s' % (tpg, blockstore)) # Create the LUN lun = LUN(tpg, 0, blockstore) # Add it to the local data structure for tracking LUNs lun_list[blockstore.name] = lun else: # LUN already exists Log.info('lun %s already exists, not creating' % (blockstore.name)) return lun def get_portal_id(self, ip, port): return '%s:%d' % (ip, port) def get_portal(self, tpg, ip, port): portal = None portal_id = self.get_portal_id(ip, port) wwn = tpg.parent_target.wwn portal_list = self.target[wwn]['tpg'][tpg.tag]['portal'] return portal_list.get(portal_id, None) # Create portal, if needed def create_portal(self, tpg, ip, port): portal = None portal_id = self.get_portal_id(ip, port) wwn = tpg.parent_target.wwn portal_list = self.target[wwn]['tpg'][tpg.tag]['portal'] if portal_id in portal_list: Log.info('portal %s already exists, not creating' % (portal_id)) portal = portal_list[portal_id] else: Log.info('creating portal (%s, %s, %s)' % (tpg, ip, port)) portal = NetworkPortal(tpg, ip, port) portal_list[portal_id] = portal return portal def get_acl(self, tpg, initiator_name): acl = None wwn = tpg.parent_target.wwn acl_list = self.target[wwn]['tpg'][tpg.tag]['acl'] return acl_list.get(initiator_name, None) # Create ACL, if needed def create_acl(self, tpg, initiator_name): acl = None wwn = tpg.parent_target.wwn acl_list = self.target[wwn]['tpg'][tpg.tag]['acl'] if initiator_name in acl_list: Log.info('acl (%s, %s) already exists, not creating' % (tpg, initiator_name)) acl = acl_list[initiator_name] else: Log.info('creating acl (%s, %s)' % (tpg, initiator_name)) acl = NodeACL(tpg, initiator_name) acl_list[initiator_name] = acl return acl # Create mapped lun, if needed def create_mapped_lun(self, acl, num, lun): mapped_lun = None if not list(acl.mapped_luns): Log.info('creating mapped lun (%s, %s, %s)' % (acl, num, lun)) mapped_lun = MappedLUN(acl, num, lun) else: Log.info('mapped lun (%s, %s, %s) already exists' % (acl, num, lun)) return mapped_lun
class UIRoot(UINode): ''' The targetcli hierarchy root node. ''' def __init__(self, shell, as_root=False): UINode.__init__(self, '/', shell=shell) self.as_root = as_root self.rtsroot = RTSRoot() def refresh(self): ''' Refreshes the tree of target fabric modules. ''' self._children = set([]) # Invalidate any rtslib caches if 'invalidate_caches' in dir(RTSRoot): self.rtsroot.invalidate_caches() UIBackstores(self) # only show fabrics present in the system for fm in self.rtsroot.fabric_modules: if fm.wwns == None or any(fm.wwns): UIFabricModule(fm, self) def _compare_files(self, backupfile, savefile): ''' Compare backfile and saveconfig file ''' if (os.path.splitext(backupfile)[1] == '.gz'): try: with gzip.open(backupfile, 'rb') as fbkp: fdata_bkp = fbkp.read() except IOError as e: self.shell.log.warning("Could not gzip open backupfile %s: %s" % (backupfile, e.strerror)) else: try: with open(backupfile, 'rb') as fbkp: fdata_bkp = fbkp.read() except IOError as e: self.shell.log.warning("Could not open backupfile %s: %s" % (backupfile, e.strerror)) try: with open(savefile, 'rb') as f: fdata = f.read() except IOError as e: self.shell.log.warning("Could not open saveconfig file %s: %s" % (savefile, e.strerror)) if fdata_bkp == fdata: return True else: return False def _save_backups(self, savefile): ''' Take backup of config-file if needed. ''' # Only save backups if saving to default location if savefile != default_save_file: return backup_dir = os.path.dirname(savefile) + "/backup/" backup_name = "saveconfig-" + \ datetime.now().strftime("%Y%m%d-%H:%M:%S") + "-json.gz" backupfile = backup_dir + backup_name backup_error = None if not os.path.exists(backup_dir): try: os.makedirs(backup_dir) except OSError as exe: raise ExecutionError("Cannot create backup directory [%s] %s." % (backup_dir, exe.strerror)) # Only save backups if savefile exits if not os.path.exists(savefile): return backed_files_list = sorted(glob(os.path.dirname(savefile) + \ "/backup/saveconfig-*json*")) # Save backup if backup dir is empty, or savefile is differnt from recent backup copy if not backed_files_list or not self._compare_files(backed_files_list[-1], savefile): try: with open(savefile, 'rb') as f_in, gzip.open(backupfile, 'wb') as f_out: shutil.copyfileobj(f_in, f_out) f_out.flush() except IOError as ioe: backup_error = ioe.strerror or "Unknown error" if backup_error == None: # remove excess backups max_backup_files = int(self.shell.prefs['max_backup_files']) try: with open(universal_prefs_file) as prefs: backups = [line for line in prefs.read().splitlines() if re.match('^max_backup_files\s*=', line)] if max_backup_files < int(backups[0].split('=')[1].strip()): max_backup_files = int(backups[0].split('=')[1].strip()) except: self.shell.log.debug("No universal prefs file '%s'." % universal_prefs_file) files_to_unlink = list(reversed(backed_files_list))[max_backup_files - 1:] for f in files_to_unlink: with ignored(IOError): os.unlink(f) self.shell.log.info("Last %d configs saved in %s." % (max_backup_files, backup_dir)) else: self.shell.log.warning("Could not create backup file %s: %s." % (backupfile, backup_error)) def ui_command_saveconfig(self, savefile=default_save_file): ''' Saves the current configuration to a file so that it can be restored on next boot. ''' self.assert_root() if not savefile: savefile = default_save_file savefile = os.path.expanduser(savefile) self._save_backups(savefile) self.rtsroot.save_to_file(savefile) self.shell.log.info("Configuration saved to %s" % savefile) def ui_command_restoreconfig(self, savefile=default_save_file, clear_existing=False): ''' Restores configuration from a file. ''' self.assert_root() savefile = os.path.expanduser(savefile) if not os.path.isfile(savefile): self.shell.log.info("Restore file %s not found" % savefile) return errors = self.rtsroot.restore_from_file(savefile, clear_existing) self.refresh() if errors: raise ExecutionError("Configuration restored, %d recoverable errors:\n%s" % \ (len(errors), "\n".join(errors))) self.shell.log.info("Configuration restored from %s" % savefile) def ui_complete_saveconfig(self, parameters, text, current_param): ''' Auto-completes the file name ''' if current_param != 'savefile': return [] completions = complete_path(text, stat.S_ISREG) if len(completions) == 1 and not completions[0].endswith('/'): completions = [completions[0] + ' '] return completions ui_complete_restoreconfig = ui_complete_saveconfig def ui_command_clearconfig(self, confirm=None): ''' Removes entire configuration of backstores and targets ''' self.assert_root() confirm = self.ui_eval_param(confirm, 'bool', False) self.rtsroot.clear_existing(confirm=confirm) self.shell.log.info("All configuration cleared") self.refresh() def ui_command_version(self): ''' Displays the targetcli and support libraries versions. ''' from targetcli import __version__ as targetcli_version self.shell.log.info("targetcli version %s" % targetcli_version) def ui_command_sessions(self, action="list", sid=None): ''' Displays a detailed list of all open sessions. PARAMETERS ========== I{action} --------- The I{action} is one of: - B{list} gives a short session list - B{detail} gives a detailed list I{sid} ------ You can specify an I{sid} to only list this one, with or without details. SEE ALSO ======== status ''' indent_step = 4 base_steps = 0 action_list = ("list", "detail") if action not in action_list: raise ExecutionError("action must be one of: %s" % ", ".join(action_list)) if sid is not None: try: int(sid) except ValueError: raise ExecutionError("sid must be a number, '%s' given" % sid) def indent_print(text, steps): console = self.shell.con console.display(console.indent(text, indent_step * steps), no_lf=True) def print_session(session): acl = session['parent_nodeacl'] indent_print("alias: %(alias)s\tsid: %(id)i type: " \ "%(type)s session-state: %(state)s" % session, base_steps) if action == 'detail': if self.as_root: if acl.authenticate_target: auth = " (authenticated)" else: auth = " (NOT AUTHENTICATED)" else: auth = "" indent_print("name: %s%s" % (acl.node_wwn, auth), base_steps + 1) for mlun in acl.mapped_luns: plugin = mlun.tpg_lun.storage_object.plugin name = mlun.tpg_lun.storage_object.name if mlun.write_protect: mode = "r" else: mode = "rw" indent_print("mapped-lun: %d backstore: %s/%s mode: %s" % (mlun.mapped_lun, plugin, name, mode), base_steps + 1) for connection in session['connections']: indent_print("address: %(address)s (%(transport)s) cid: " \ "%(cid)i connection-state: %(cstate)s" % \ connection, base_steps + 1) if sid: printed_sessions = [x for x in self.rtsroot.sessions if x['id'] == int(sid)] else: printed_sessions = list(self.rtsroot.sessions) if len(printed_sessions): for session in printed_sessions: print_session(session) else: if sid is None: indent_print("(no open sessions)", base_steps) else: raise ExecutionError("no session found with sid %i" % int(sid))