def load_config(self): """ Grab the target, tpg and portal objects from LIO and store in this Gateway object """ try: self.target = Target(ISCSIFabricModule(), self.iqn, "lookup") # clear list so we can rebuild with the current values below if self.tpg_list: del self.tpg_list[:] # there could/should be multiple tpg's for the target for tpg in self.target.tpgs: self.tpg_list.append(tpg) # self.portal = self.tpg.network_portals.next() except RTSLibError as err: self.error_msg = err self.error = True self.logger.info("(Gateway.load_config) successfully loaded existing " "target definition")
def create_target(self): """ Add an iSCSI target to LIO with this objects iqn name, and bind to the IP that aligns with the given iscsi_network """ try: iscsi_fabric = ISCSIFabricModule() self.target = Target(iscsi_fabric, wwn=self.iqn) logger.debug( "(Gateway.create_target) Added iscsi target - {}".format( self.iqn)) self.tpg = TPG(self.target) logger.debug("(Gateway.create_target) Added tpg") self.tpg.enable = True self.portal = NetworkPortal(self.tpg, self.ip_address) logger.debug( "(Gateway.create_target) Added portal IP '{}' to tpg".format( self.ip_address)) except RTSLibError as err: self.error_msg = err self.error = True self.delete() self.changes_made = True logger.info( "(Gateway.create_target) created an iscsi target with iqn of '{}'". format(self.iqn))
def get_client_info(target_iqn, client_iqn): result = { "alias": '', "state": '', "ip_address": [] } iscsi_fabric = ISCSIFabricModule() try: target = Target(iscsi_fabric, target_iqn, 'lookup') except RTSLibNotInCFS: return result for tpg in target.tpgs: if tpg.enable: for client in tpg.node_acls: if client.node_wwn != client_iqn: continue session = client.session if session is None: break result['alias'] = session.get('alias') state = session.get('state').upper() result['state'] = state ips = set() if state == 'LOGGED_IN': for conn in session.get('connections'): ips.add(conn.get('address')) result['ip_address'] = list(ips) break return result
def create_target(self): """ Add an iSCSI target to LIO with this objects iqn name, and bind to the IP that aligns with the given iscsi_network """ try: iscsi_fabric = ISCSIFabricModule() self.target = Target(iscsi_fabric, wwn=self.iqn) self.logger.debug("(Gateway.create_target) Added iscsi target - " "{}".format(self.iqn)) # tpg's are defined in the sequence provide by the gateway_ip_list, # so across multiple gateways the same tpg number will be # associated with the same IP - however, only the tpg with an IP on # the host will be in an enabled state. The other tpgs are # necessary for systems like ESX who issue a rtpg scsi inquiry # only to one of the gateways - so that gateway must provide # details for the whole configuration self.logger.debug("Creating tpgs") for ip in self.gateway_ip_list: self.create_tpg(ip) if self.error: self.logger.critical("Unable to create the TPG for {} " "- {}".format(ip, self.error_msg)) self.update_tpg_controls() except RTSLibError as err: self.error_msg = err self.logger.critical("Unable to create the Target definition " "- {}".format(self.error_msg)) self.error = True if self.error: if self.target: self.target.delete() else: self.changes_made = True self.logger.info("(Gateway.create_target) created an iscsi target " "with iqn of '{}'".format(self.iqn))
def get_tpgs(self, target_iqn): """ determine the number of tpgs in the current target :return: count of the defined tpgs """ try: target = Target(ISCSIFabricModule(), target_iqn, "lookup") return len([tpg.tag for tpg in target.tpgs]) except RTSLibError: return 0
def define_client(self): """ Establish the links for this object to the corresponding ACL and TPG objects from LIO :return: """ iscsi_fabric = ISCSIFabricModule() target = Target(iscsi_fabric, self.target_iqn, 'lookup') # NB. this will check all tpg's for a matching iqn for tpg in target.tpgs: if tpg.enable: for client in tpg.node_acls: if client.node_wwn == self.iqn: self.acl = client self.tpg = client.parent_tpg try: self.update_acl_controls() except RTSLibError as err: self.logger.error( "(Client.define_client) FAILED to update " "{}".format(self.iqn)) self.error = True self.error_msg = err self.logger.debug( "(Client.define_client) - {} already " "defined".format(self.iqn)) return # at this point the client does not exist, so create it # The configuration only has one active tpg, so pick that one for any # acl definitions for tpg in target.tpgs: if tpg.enable: self.tpg = tpg try: self.acl = NodeACL(self.tpg, self.iqn) self.update_acl_controls() except RTSLibError as err: self.logger.error("(Client.define_client) FAILED to define " "{}".format(self.iqn)) self.logger.debug("(Client.define_client) failure msg " "{}".format(err)) self.error = True self.error_msg = err else: self.logger.info("(Client.define_client) {} added " "successfully".format(self.iqn)) self.change_count += 1
def load_config(self): """ Grab the target, tpg and portal objects from LIO and store in this Gateway object """ try: self.target = Target(ISCSIFabricModule(), self.iqn, "lookup") # clear list so we can rebuild with the current values below if self.tpg_list: del self.tpg_list[:] if self.tpg_tag_by_gateway_name: self.tpg_tag_by_gateway_name = {} # there could/should be multiple tpg's for the target for tpg in self.target.tpgs: self.tpg_list.append(tpg) network_portals = list(tpg.network_portals) if network_portals: ip_address = network_portals[0].ip_address gateway_name = self._get_gateway_name(ip_address) if gateway_name: self.tpg_tag_by_gateway_name[gateway_name] = tpg.tag else: self.logger.info("No available network portal for target " "with iqn of '{}'".format(self.iqn)) # self.portal = self.tpg.network_portals.next() except RTSLibError as err: self.error_msg = err self.error = True self.logger.info("(Gateway.load_config) successfully loaded existing " "target definition")
class GWTarget(GWObject): """ Class representing the state of the local LIO environment """ # iscsi tpg specific settings. TPG_SETTINGS = [ "dataout_timeout", "immediate_data", "initial_r2t", "max_outstanding_r2t", "first_burst_length", "max_burst_length", "max_recv_data_segment_length", "max_xmit_data_segment_length" ] # Settings for all transport/fabric objects. Using this allows apps like # gwcli to get/set all tpgs/clients under the target instead of per obj. SETTINGS = TPG_SETTINGS + GWClient.SETTINGS def __init__(self, logger, iqn, gateway_ip_list, enable_portal=True): """ Instantiate the class :param iqn: iscsi iqn name for the gateway :param gateway_ip_list: list of IP addresses to be defined as portals to LIO :return: gateway object """ self.error = False self.error_msg = '' self.enable_portal = enable_portal # boolean to trigger portal IP creation self.logger = logger # logger object try: iqn, iqn_type = normalize_wwn(['iqn'], iqn) except RTSLibError as err: self.error = True self.error_msg = "Invalid iSCSI target name - {}".format(err) self.iqn = iqn # Ensure IPv6 addresses are in the normalized address (not literal) format gateway_ip_list = [normalize_ip_address(x) for x in gateway_ip_list] # If the ip list received has data in it, this is a target we need to # act on the IP's provided, otherwise just set to null if gateway_ip_list: # if the ip list provided doesn't match any ip of this host, abort # the assumption here is that we'll only have one matching ip in # the list! matching_ip = set(gateway_ip_list).intersection(ip_addresses()) if len(list(matching_ip)) == 0: self.error = True self.error_msg = ("gateway IP addresses provided do not match" " any ip on this host") return self.active_portal_ip = list(matching_ip)[0] self.logger.debug("active portal will use " "{}".format(self.active_portal_ip)) self.gateway_ip_list = gateway_ip_list self.logger.debug("tpg's will be defined in this order" " - {}".format(self.gateway_ip_list)) else: # without gateway_ip_list passed in this is a 'init' or # 'clearconfig' request self.gateway_ip_list = [] self.active_portal_ip = [] self.changes_made = False self.config_updated = False # self.portal = None self.target = None self.tpg = None self.tpg_list = [] try: super(GWTarget, self).__init__('targets', iqn, logger, GWTarget.SETTINGS) except CephiSCSIError as err: self.error = True self.error_msg = err def exists(self): """ Basic check to see whether this iqn already exists in kernel's configFS directory :return: boolean """ return os.path.exists('/sys/kernel/config/target/iscsi/' '{}'.format(self.iqn)) def _get_portals(self, tpg): """ return a list of network portal IPs allocated to a specfic tpg :param tpg: tpg to check (object) :return: list of IP's this tpg has (list) """ return [ normalize_ip_address(portal.ip_address) for portal in tpg.network_portals ] def check_tpgs(self): # process the portal IP's in order to preserve the tpg sequence # across gateways requested_tpg_ips = list(self.gateway_ip_list) current_tpgs = list(self.tpg_list) for portal_ip in self.gateway_ip_list: for tpg in current_tpgs: if portal_ip in self._get_portals(tpg): # portal requested is defined, so remove from the list requested_tpg_ips.remove(portal_ip) current_tpgs.remove(tpg) break # if the requested_tpg_ips list has entries, we need to add new tpg's if requested_tpg_ips: self.logger.info("An additional {} tpg's are " "required".format(len(requested_tpg_ips))) for ip in requested_tpg_ips: self.create_tpg(ip) try: self.update_tpg_controls() except RTSLibError as err: self.error = True self.error_msg = "Failed to update TPG control parameters - {}".format( err) def update_tpg_controls(self): self.logger.debug("(GWGateway.update_tpg_controls) {}".format( self.controls)) for tpg in self.tpg_list: tpg.set_parameter('ImmediateData', format_lio_yes_no(self.immediate_data)) tpg.set_parameter('InitialR2T', format_lio_yes_no(self.initial_r2t)) tpg.set_parameter('MaxOutstandingR2T', str(self.max_outstanding_r2t)) tpg.set_parameter('FirstBurstLength', str(self.first_burst_length)) tpg.set_parameter('MaxBurstLength', str(self.max_burst_length)) tpg.set_parameter('MaxRecvDataSegmentLength', str(self.max_recv_data_segment_length)) tpg.set_parameter('MaxXmitDataSegmentLength', str(self.max_xmit_data_segment_length)) def enable_active_tpg(self, config): """ Add the relevant ip to the active/enabled tpg within the target and bind the tpg's luns to an ALUA group. :return: None """ for tpg in self.tpg_list: if tpg._get_enable(): for lun in tpg.luns: try: self.bind_alua_group_to_lun( config, lun, tpg_ip_address=self.active_portal_ip) except CephiSCSIInval as err: self.error = True self.error_msg = err return try: NetworkPortal(tpg, normalize_ip_literal(self.active_portal_ip)) except RTSLibError as e: self.error = True self.error_msg = e else: break def clear_config(self): """ Remove the target definition form LIO :return: None """ # check that there aren't any disks or clients in the configuration clients = [] disks = set() for tpg in self.tpg_list: tpg_clients = [node for node in tpg._list_node_acls()] clients += tpg_clients disks.update([lun.storage_object.name for lun in tpg.luns]) client_count = len(clients) disk_count = len(disks) if disk_count > 0 or client_count > 0: self.error = True self.error_msg = ("Clients({}) and disks({}) must be removed " "before the gateways".format( client_count, disk_count)) return self.logger.debug("Clients defined :{}".format(client_count)) self.logger.debug("Disks defined :{}".format(disk_count)) self.logger.info("Removing target configuration") try: self.delete() except RTSLibError as err: self.error = True self.error_msg = "Unable to delete target - {}".format(err) def create_tpg(self, ip): try: tpg = TPG(self.target) # Use initiator name based ACL by default. tpg.set_attribute('authentication', '0') self.logger.debug("(Gateway.create_tpg) Added tpg for portal " "ip {}".format(ip)) if ip == self.active_portal_ip: if self.enable_portal: NetworkPortal(tpg, normalize_ip_literal(ip)) tpg.enable = True self.logger.debug("(Gateway.create_tpg) Added tpg for " "portal ip {} is enabled".format(ip)) else: NetworkPortal(tpg, normalize_ip_literal(ip)) # disable the tpg on this host tpg.enable = False # by disabling tpg_enabled_sendtargets, discovery to just one # node will return all portals (default is 1) tpg.set_attribute('tpg_enabled_sendtargets', '0') self.logger.debug("(Gateway.create_tpg) Added tpg for " "portal ip {} as disabled".format(ip)) self.tpg_list.append(tpg) except RTSLibError as err: self.error_msg = err self.error = True else: self.changes_made = True self.logger.info("(Gateway.create_tpg) created TPG '{}' " "for target iqn '{}'".format(tpg.tag, self.iqn)) def create_target(self): """ Add an iSCSI target to LIO with this objects iqn name, and bind to the IP that aligns with the given iscsi_network """ try: iscsi_fabric = ISCSIFabricModule() self.target = Target(iscsi_fabric, wwn=self.iqn) self.logger.debug("(Gateway.create_target) Added iscsi target - " "{}".format(self.iqn)) # tpg's are defined in the sequence provide by the gateway_ip_list, # so across multiple gateways the same tpg number will be # associated with the same IP - however, only the tpg with an IP on # the host will be in an enabled state. The other tpgs are # necessary for systems like ESX who issue a rtpg scsi inquiry # only to one of the gateways - so that gateway must provide # details for the whole configuration self.logger.debug("Creating tpgs") for ip in self.gateway_ip_list: self.create_tpg(ip) if self.error: self.logger.critical("Unable to create the TPG for {} " "- {}".format(ip, self.error_msg)) self.update_tpg_controls() except RTSLibError as err: self.error_msg = err self.logger.critical("Unable to create the Target definition " "- {}".format(self.error_msg)) self.error = True if self.error: self.delete() else: self.changes_made = True self.logger.info("(Gateway.create_target) created an iscsi target " "with iqn of '{}'".format(self.iqn)) def load_config(self): """ Grab the target, tpg and portal objects from LIO and store in this Gateway object """ try: lio_root = root.RTSRoot() self.target = [ tgt for tgt in lio_root.targets if tgt.wwn == self.iqn ][0] self.tpg_list = [] # there could/should be multiple tpg's for the target for tpg in self.target.tpgs: self.tpg_list.append(tpg) # self.portal = self.tpg.network_portals.next() except RTSLibError as err: self.error_msg = err self.error = True self.logger.info("(Gateway.load_config) successfully loaded existing " "target definition") def bind_alua_group_to_lun(self, config, lun, tpg_ip_address=None): """ bind lun to one of the alua groups. Query the config to see who 'owns' the primary path for this LUN. Then either bind the LUN to the ALUA 'AO' group if the host matches, or default to the 'ANO'/'Standby' alua group param config: Config object param lun: lun object on the tpg param tpg_ip: IP of Network Portal for the lun's tpg. """ stg_object = lun.storage_object owning_gw = config.config['disks'][stg_object.name]['owner'] tpg = lun.parent_tpg if not tpg_ip_address: # just need to check one portal for ip in tpg.network_portals: tpg_ip_address = normalize_ip_address(ip.ip_address) break if tpg_ip_address is None: # this is being run during boot so the NP is not setup yet. return target_config = config.config["targets"][self.iqn] is_owner = False if target_config['portals'][owning_gw][ "portal_ip_address"] == tpg_ip_address: is_owner = True try: alua_tpg = alua_create_group(settings.config.alua_failover_type, tpg, stg_object, is_owner) except CephiSCSIInval: raise except RTSLibError: self.logger.info("ALUA group id {} for stg obj {} lun {} " "already made".format(tpg.tag, stg_object, lun)) group_name = alua_format_group_name( tpg, settings.config.alua_failover_type, is_owner) # someone mapped a LU then unmapped it without deleting the # stg_object, or we are reloading the config. alua_tpg = ALUATargetPortGroup(stg_object, group_name) if alua_tpg.tg_pt_gp_id != tpg.tag: # ports and owner were rearranged. Not sure we support that. raise CephiSCSIInval("Existing ALUA group tag for group {} " "in invalid state.\n".format(group_name)) # drop down in case we are restarting due to error and we # were not able to bind to a lun last time. self.logger.info( "Setup group {} for {} on tpg {} (state {}, owner {}, " "failover type {})".format(alua_tpg.name, stg_object.name, tpg.tag, alua_tpg.alua_access_state, is_owner, alua_tpg.alua_access_type)) self.logger.debug("Setting Luns tg_pt_gp to {}".format(alua_tpg.name)) lun.alua_tg_pt_gp_name = alua_tpg.name self.logger.debug("Bound {} on tpg{} to {}".format( stg_object.name, tpg.tag, alua_tpg.name)) def map_luns(self, config): """ LIO will have objects already defined by the lun module, so this method, brings those objects into the gateways TPG """ lio_root = root.RTSRoot() target_config = config.config["targets"][self.iqn] target_stg_object = [ stg_object for stg_object in lio_root.storage_objects if stg_object.name in target_config['disks'] ] # a dict, key with tpg and storage object name tpg_stg_object_dict = {} for tpg in self.tpg_list: for l in tpg.luns: key = str(tpg.tag) + "-" + l.storage_object.name tpg_stg_object_dict[key] = l.storage_object.name # process each storage object added to the gateway, and map to the tpg for stg_object in target_stg_object: for tpg in self.tpg_list: self.logger.debug("processing tpg{}".format(tpg.tag)) if str(tpg.tag ) + "-" + stg_object.name not in tpg_stg_object_dict: self.logger.debug("{} needed mapping to " "tpg{}".format(stg_object.name, tpg.tag)) lun_id = int(stg_object.path.split('/')[-2].split('_')[1]) try: mapped_lun = LUN(tpg, lun=lun_id, storage_object=stg_object) self.changes_made = True except RTSLibError as err: self.logger.error("LUN mapping failed: {}".format(err)) self.error = True self.error_msg = err return try: self.bind_alua_group_to_lun(config, mapped_lun) except CephiSCSIInval as err: self.logger.error("Could not bind LUN to ALUA group: " "{}".format(err)) self.error = True self.error_msg = err return def delete(self): self.target.delete() def manage(self, mode): """ Manage the definition of the gateway, given a mode of 'target', 'map', 'init' or 'clearconfig'. In 'target' mode the LIO TPG is defined, whereas in map mode, the required LUNs are added to the existing TPG :param mode: run mode - target, map, init or clearconfig (str) :return: None - but sets the objects error flags to be checked by the caller """ config = Config(self.logger) if config.error: self.error = True self.error_msg = config.error_msg return local_gw = this_host() if mode == 'target': if self.exists(): self.load_config() self.check_tpgs() else: self.create_target() if self.error: # return to caller, with error state set return Discovery.set_discovery_auth_lio( config.config['discovery_auth']['chap'], config.config['discovery_auth']['chap_mutual']) target_config = config.config["targets"][self.iqn] gateway_group = config.config["gateways"].keys() if "ip_list" not in target_config: target_config['ip_list'] = self.gateway_ip_list config.update_item("targets", self.iqn, target_config) self.config_updated = True if self.controls != target_config.get('controls', {}): target_config['controls'] = self.controls.copy() config.update_item("targets", self.iqn, target_config) self.config_updated = True if local_gw not in gateway_group: gateway_metadata = {"active_luns": 0} config.add_item("gateways", local_gw) config.update_item("gateways", local_gw, gateway_metadata) self.config_updated = True if local_gw not in target_config['portals']: inactive_portal_ip = list(self.gateway_ip_list) inactive_portal_ip.remove(self.active_portal_ip) portal_metadata = { "tpgs": len(self.tpg_list), "gateway_ip_list": self.gateway_ip_list, "portal_ip_address": self.active_portal_ip, "inactive_portal_ips": inactive_portal_ip } target_config['portals'][local_gw] = portal_metadata target_config['ip_list'] = self.gateway_ip_list config.update_item("targets", self.iqn, target_config) self.config_updated = True else: # gateway already defined, so check that the IP list it has # matches the current request portal_details = target_config['portals'][local_gw] if portal_details['gateway_ip_list'] != self.gateway_ip_list: inactive_portal_ip = list(self.gateway_ip_list) inactive_portal_ip.remove(self.active_portal_ip) portal_details['gateway_ip_list'] = self.gateway_ip_list portal_details['tpgs'] = len(self.tpg_list) portal_details['inactive_portal_ips'] = inactive_portal_ip target_config['portals'][local_gw] = portal_details config.update_item("targets", self.iqn, target_config) self.config_updated = True if self.config_updated: config.commit() elif mode == 'map': if self.exists(): self.load_config() self.map_luns(config) else: self.error = True self.error_msg = ("Attempted to map to a gateway '{}' that " "hasn't been defined yet...out of order " "steps?".format(self.iqn)) elif mode == 'init': # init mode just creates the iscsi target definition and updates # the config object. It is used by the CLI only if self.exists(): self.logger.info("GWTarget init request skipped - target " "already exists") else: # create the target self.create_target() seed_target = { 'disks': [], 'clients': {}, 'portals': {}, 'groups': {}, 'controls': {} } config.add_item("targets", self.iqn, seed_target) config.commit() Discovery.set_discovery_auth_lio( config.config['discovery_auth']['chap'], config.config['discovery_auth']['chap_mutual']) elif mode == 'clearconfig': # Called by API from CLI clearconfig command if self.exists(): self.load_config() else: self.error = True self.error_msg = "Target {} does not exist on {}".format( self.iqn, local_gw) return target_config = config.config["targets"][self.iqn] self.clear_config() if not self.error: if len(target_config['portals']) == 0: config.del_item('targets', self.iqn) else: gw_ip = target_config['portals'][local_gw][ 'portal_ip_address'] target_config['portals'].pop(local_gw) ip_list = target_config['ip_list'] ip_list.remove(gw_ip) if len(ip_list) > 0 and len( target_config['portals'].keys()) > 0: config.update_item('targets', self.iqn, target_config) else: # no more portals in the list, so delete the target config.del_item('targets', self.iqn) remove_gateway = True for _, target in config.config["targets"].items(): if local_gw in target['portals']: remove_gateway = False break if remove_gateway: # gateway is no longer used, so delete it config.del_item('gateways', local_gw) config.commit()
class GWTarget(object): """ Class representing the state of the local LIO environment """ def __init__(self, logger, iqn, gateway_ip_list, enable_portal=True): """ Instantiate the class :param iqn: iscsi iqn name for the gateway :param gateway_ip_list: list of IP addresses to be defined as portals to LIO :return: gateway object """ self.error = False self.error_msg = '' self.enable_portal = enable_portal # boolean to trigger portal IP creation self.logger = logger # logger object self.iqn = iqn # if the ip list provided doesn't match any ip of this host, abort # the assumption here is that we'll only have one matching ip in the list! matching_ip = set(gateway_ip_list).intersection(ipv4_addresses()) if len(list(matching_ip)) == 0: self.error = True self.error_msg = "gateway IP addresses provided do not match any ip on this host" return self.active_portal_ip = list(matching_ip)[0] self.logger.debug("active portal will use {}".format( self.active_portal_ip)) self.gateway_ip_list = gateway_ip_list self.logger.debug("tpg's will be defined in this order - {}".format( self.gateway_ip_list)) self.changes_made = False self.config_updated = False # self.portal = None self.target = None self.tpg = None self.tpg_list = [] def exists(self): """ Basic check to see whether this iqn already exists in kernel's configFS directory :return: boolean """ return os.path.exists('/sys/kernel/config/target/iscsi/{}'.format( self.iqn)) def _get_portals(self, tpg): """ return a list of network portal IPs allocated to a specfic tpg :param tpg: tpg to check (object) :return: list of IP's this tpg has (list) """ return [portal.ip_address for portal in tpg.network_portals] def check_tpgs(self): # process the portal IP's in order to preserve the tpg sequence across gateways requested_tpg_ips = list(self.gateway_ip_list) current_tpgs = list(self.tpg_list) for portal_ip in self.gateway_ip_list: for tpg in current_tpgs: if portal_ip in self._get_portals(tpg): # portal requested is defined, so remove from the list requested_tpg_ips.remove(portal_ip) current_tpgs.remove(tpg) break # if the requested_tpg_ips list has entries, we need to add new tpg's if requested_tpg_ips: self.logger.info("An additional {} tpg's are required".format( len(requested_tpg_ips))) for ip in requested_tpg_ips: self.create_tpg(ip) def enable_active_tpg(self, config): """ Add the relevant ip to the active/enabled tpg within the target and bind the tpg's luns to an ALUA group. :return: None """ for tpg in self.tpg_list: if tpg._get_enable(): for lun in tpg.luns: self.bind_alua_group_to_lun( config, lun, tpg_ip_address=self.active_portal_ip) try: NetworkPortal(tpg, self.active_portal_ip) except RTSLibError as e: self.error = True self.error_msg = e else: break def create_tpg(self, ip): try: tpg = TPG(self.target) self.logger.debug( "(Gateway.create_target) Added tpg for portal ip {}".format( ip)) if ip == self.active_portal_ip: if self.enable_portal: NetworkPortal(tpg, ip) tpg.enable = True self.logger.debug( "(Gateway.create_target) Added tpg for portal ip {} is enabled" .format(ip)) else: NetworkPortal(tpg, ip) # disable the tpg on this host tpg.enable = False # by disabling tpg_enabled_sendtargets, discovery to just one node will return all portals # default is 1 tpg.set_attribute('tpg_enabled_sendtargets', '0') self.logger.debug( "(Gateway.create_target) Added tpg for portal ip {} as disabled" .format(ip)) self.tpg_list.append(tpg) except RTSLibError as err: self.error_msg = err self.error = True else: self.changes_made = True self.logger.info( "(Gateway.create_target) created an iscsi target with iqn of '{}'" .format(self.iqn)) def create_target(self): """ Add an iSCSI target to LIO with this objects iqn name, and bind to the IP that aligns with the given iscsi_network """ try: iscsi_fabric = ISCSIFabricModule() self.target = Target(iscsi_fabric, wwn=self.iqn) self.logger.debug( "(Gateway.create_target) Added iscsi target - {}".format( self.iqn)) # tpg's are defined in the sequence provide by the gateway_ip_list, so across multiple gateways the # same tpg number will be associated with the same IP - however, only the tpg with an IP on the host # will be in an enabled state. The other tpgs are necessary for systems like ESX who issue a rtpg scsi # inquiry only to one of the gateways - so that gateway must provide details for the whole configuration self.logger.debug("Creating tpgs") for ip in self.gateway_ip_list: self.create_tpg(ip) if self.error: self.logger.critical( "Unable to create the TPG for {} - {}".format( ip, self.error_msg)) except RTSLibError as err: self.error_msg = err self.logger.critical( "Unable to create the Target definition - {}".format( self.error_msg)) self.error = True if self.error: self.delete() else: self.changes_made = True self.logger.info( "(Gateway.create_target) created an iscsi target with iqn of '{}'" .format(self.iqn)) def load_config(self): """ Grab the target, tpg and portal objects from LIO and store in this Gateway object """ try: lio_root = root.RTSRoot() # since we only support one target, we just grab the first iterable self.target = lio_root.targets.next() # but there could/should be multiple tpg's for the target for tpg in self.target.tpgs: self.tpg_list.append(tpg) # self.portal = self.tpg.network_portals.next() except RTSLibError as err: self.error_msg = err self.error = True self.logger.info( "(Gateway.load_config) successfully loaded existing target definition" ) def bind_alua_group_to_lun(self, config, lun, tpg_ip_address=None): """ bind lun to one of the alua groups. Query the config to see who 'owns' the primary path for this LUN. Then either bind the LUN to the ALUA 'AO' group if the host matches, or default to the 'ANO' alua group param config: Config object param stg_object: Storage object param lun: lun object on the tpg param tpg_ip: IP of Network Portal for the lun's tpg. """ stg_object = lun.storage_object owning_gw = config.config['disks'][stg_object.name]['owner'] tpg = lun.parent_tpg if tpg_ip_address is None: # just need to check one portal for ip in tpg.network_portals: tpg_ip_address = ip.ip_address break if tpg_ip_address is None: # this is being run during boot so the NP is not setup yet. return # TODO: The ports in a alua group must export the same state for a LU # group. For different LUs we are exporting different states, so # we should be creating different LU groups or creating different # alua groups for each LU. try: if config.config["gateways"][owning_gw][ "portal_ip_address"] == tpg_ip_address: self.logger.info( "setting {} to ALUA/ActiveOptimised group id {}".format( stg_object.name, tpg.tag)) group_name = "ao" alua_tpg = ALUATargetPortGroup(stg_object, group_name, tpg.tag) alua_tpg.alua_access_state = 0 else: self.logger.info( "setting {} to ALUA/ActiveNONOptimised group id {}".format( stg_object.name, tpg.tag)) group_name = "ano{}".format(tpg.tag) alua_tpg = ALUATargetPortGroup(stg_object, group_name, tpg.tag) alua_tpg.alua_access_state = 1 except RTSLibError as err: self.logger.info( "ALUA group id {} for stg obj {} lun {} already made".format( tpg.tag, stg_object, lun)) # someone mapped a LU then unmapped it without deleting the # stg_object, or we are reloading the config. alua_tpg = ALUATargetPortGroup(stg_object, group_name) if alua_tpg.tpg_id != tpg.tag: # ports and owner were rearranged. Not sure we support that. raise RTSLibError # drop down in case we are restarting due to error and we # were not able to bind to a lun last time. alua_tpg.alua_access_type = 1 alua_tpg.alua_support_offline = 0 alua_tpg.alua_support_unavailable = 0 alua_tpg.alua_support_standby = 0 alua_tpg.nonop_delay_msecs = 0 alua_tpg.bind_to_lun(lun) def map_luns(self, config): """ LIO will have blockstorage objects already defined by the igw_lun module, so this method, brings those objects into the gateways TPG """ lio_root = root.RTSRoot() # process each storage object added to the gateway, and map to the tpg for stg_object in lio_root.storage_objects: for tpg in self.tpg_list: if not self.lun_mapped(tpg, stg_object): # use the iblock number for the lun id - /sys/kernel/config/target/core/iblock_1/ansible4 # ^ lun_id = int(stg_object._path.split('/')[-2].split('_')[1]) try: mapped_lun = LUN(tpg, lun=lun_id, storage_object=stg_object) self.changes_made = True except RTSLibError as err: self.error = True self.error_msg = err break self.bind_alua_group_to_lun(config, mapped_lun) def lun_mapped(self, tpg, storage_object): """ Check to see if a given storage object (i.e. block device) is already mapped to the gateway's TPG :param storage_object: storage object to look for :return: boolean - is the storage object mapped or not """ mapped_state = False for l in tpg.luns: if l.storage_object.name == storage_object.name: mapped_state = True break return mapped_state def delete(self): self.target.delete() def manage(self, mode): """ Manage the definition of the gateway, given a mode of 'target' or 'map'. In 'target' mode the LIO TPG is defined, whereas in map mode, the required LUNs are added to the existing TPG :param mode: run mode - target or map (str) :return: None - but sets the objects error flags to be checked by the caller """ config = Config(self.logger) if config.error: self.error = True self.error_msg = config.error_msg return if mode == 'target': if self.exists(): self.load_config() self.check_tpgs() else: self.create_target() if self.error: # return to caller, with error state set return # ensure that the config object has an entry for this gateway this_host = socket.gethostname().split('.')[0] gateway_group = config.config["gateways"].keys() # this action could be carried out by multiple nodes concurrently, but since the value # is the same (i.e all gateway nodes use the same iqn) it's not worth worrying about! if "iqn" not in gateway_group: self.config_updated = True config.add_item("gateways", "iqn", initial_value=self.iqn) if "ip_list" not in gateway_group: self.config_updated = True config.add_item("gateways", "ip_list", initial_value=self.gateway_ip_list) if this_host not in gateway_group: inactive_portal_ip = list(self.gateway_ip_list) inactive_portal_ip.remove(self.active_portal_ip) gateway_metadata = { "portal_ip_address": self.active_portal_ip, "iqn": self.iqn, "active_luns": 0, "tpgs": len(self.tpg_list), "inactive_portal_ips": inactive_portal_ip, "gateway_ip_list": self.gateway_ip_list } config.add_item("gateways", this_host) config.update_item("gateways", this_host, gateway_metadata) self.config_updated = True else: # gateway already defined, so check that the IP list it has matches the # current request gw_details = config.config['gateways'][this_host] if cmp(gw_details['gateway_ip_list'], self.gateway_ip_list) != 0: inactive_portal_ip = list(self.gateway_ip_list) inactive_portal_ip.remove(self.active_portal_ip) gw_details['tpgs'] = len(self.tpg_list) gw_details['gateway_ip_list'] = self.gateway_ip_list gw_details['inactive_portal_ips'] = inactive_portal_ip config.update_item('gateways', this_host, gw_details) self.config_updated = True if self.config_updated: config.commit() elif mode == 'map': if self.exists(): self.load_config() self.map_luns(config) else: self.error = True self.error_msg = ( "Attempted to map to a gateway '{}' that hasn't been defined yet..." "out of order steps?".format(self.iqn))
class GWTarget(GWObject): """ Class representing the state of the local LIO environment """ # Settings for all transport/fabric objects. Using this allows apps like # gwcli to get/set all tpgs/clients under the target instead of per obj. SETTINGS = TGT_SETTINGS def __init__(self, logger, iqn, gateway_ip_list, enable_portal=True): """ Instantiate the class :param iqn: iscsi iqn name for the gateway :param gateway_ip_list: list of IP addresses to be defined as portals to LIO :return: gateway object """ self.error = False self.error_msg = '' self.enable_portal = enable_portal # boolean to trigger portal IP creation self.logger = logger # logger object try: iqn, iqn_type = normalize_wwn(['iqn'], iqn) except RTSLibError as err: self.error = True self.error_msg = "Invalid iSCSI target name - {}".format(err) self.iqn = iqn # Ensure IPv6 addresses are in the normalized address (not literal) format gateway_ip_list = [normalize_ip_address(x) for x in gateway_ip_list] # If the ip list received has data in it, this is a target we need to # act on the IP's provided, otherwise just set to null if gateway_ip_list: # if the ip list provided doesn't match any ip of this host, abort # the assumption here is that we'll only have one matching ip in # the list! matching_ip = set(gateway_ip_list).intersection(ip_addresses()) if len(list(matching_ip)) == 0: self.error = True self.error_msg = ("gateway IP addresses provided do not match" " any ip on this host") return self.active_portal_ips = list(matching_ip) self.logger.debug("active portal will use " "{}".format(self.active_portal_ips)) self.gateway_ip_list = gateway_ip_list self.logger.debug("tpg's will be defined in this order" " - {}".format(self.gateway_ip_list)) else: # without gateway_ip_list passed in this is a 'init' or # 'clearconfig' request self.gateway_ip_list = [] self.active_portal_ips = [] self.changes_made = False self.config_updated = False # self.portal = None self.target = None self.tpg = None self.tpg_list = [] self.tpg_tag_by_gateway_name = {} try: super(GWTarget, self).__init__('targets', iqn, logger, GWTarget.SETTINGS) except CephiSCSIError as err: self.error = True self.error_msg = err def exists(self): """ Basic check to see whether this iqn already exists in kernel's configFS directory :return: boolean """ return GWTarget._exists(self.iqn) @staticmethod def _exists(target_iqn): return os.path.exists('/sys/kernel/config/target/iscsi/' '{}'.format(target_iqn)) def _get_portals(self, tpg): """ return a list of network portal IPs allocated to a specfic tpg :param tpg: tpg to check (object) :return: list of IP's this tpg has (list) """ return [ normalize_ip_address(portal.ip_address) for portal in tpg.network_portals ] def check_tpgs(self): # process the portal IP's in order to preserve the tpg sequence # across gateways requested_tpg_ips = list(self.gateway_ip_list) current_tpgs = list(self.tpg_list) for portal_ip in self.gateway_ip_list: for tpg in current_tpgs: if portal_ip in self._get_portals(tpg): # portal requested is defined, so remove from the list requested_tpg_ips.remove(portal_ip) current_tpgs.remove(tpg) break # if the requested_tpg_ips list has entries, we need to add new tpg's if requested_tpg_ips: self.logger.info("An additional {} tpg's are " "required".format(len(requested_tpg_ips))) for ip in requested_tpg_ips: self.create_tpg(ip) try: self.update_tpg_controls() except RTSLibError as err: self.error = True self.error_msg = "Failed to update TPG control parameters - {}".format( err) def update_tpg_controls(self): self.logger.debug("(GWGateway.update_tpg_controls) {}".format( self.controls)) for tpg in self.tpg_list: tpg.set_parameter('ImmediateData', format_lio_yes_no(self.immediate_data)) tpg.set_parameter('InitialR2T', format_lio_yes_no(self.initial_r2t)) tpg.set_parameter('MaxOutstandingR2T', str(self.max_outstanding_r2t)) tpg.set_parameter('FirstBurstLength', str(self.first_burst_length)) tpg.set_parameter('MaxBurstLength', str(self.max_burst_length)) tpg.set_parameter('MaxRecvDataSegmentLength', str(self.max_recv_data_segment_length)) tpg.set_parameter('MaxXmitDataSegmentLength', str(self.max_xmit_data_segment_length)) def enable_active_tpg(self, config): """ Add the relevant ip to the active/enabled tpg within the target and bind the tpg's luns to an ALUA group. :return: None """ index = 0 for tpg in self.tpg_list: if tpg._get_enable(): for lun in tpg.luns: try: self.bind_alua_group_to_lun( config, lun, tpg_ip_address=self.active_portal_ips[index]) except CephiSCSIInval as err: self.error = True self.error_msg = err return try: NetworkPortal( tpg, normalize_ip_literal(self.active_portal_ips[index])) except RTSLibError as e: self.error = True self.error_msg = e index += 1 def clear_config(self, config): """ Remove the target definition form LIO :return: None """ # check that there aren't any disks or clients in the configuration clients = [] disks = set() for tpg in self.tpg_list: tpg_clients = [node for node in tpg._list_node_acls()] clients += tpg_clients disks.update([lun.storage_object.name for lun in tpg.luns]) client_count = len(clients) disk_count = len(disks) if disk_count > 0 or client_count > 0: self.error = True self.error_msg = ("Clients({}) and disks({}) must be removed " "before the gateways".format( client_count, disk_count)) return self.logger.debug("Clients defined :{}".format(client_count)) self.logger.debug("Disks defined :{}".format(disk_count)) self.logger.info("Removing target configuration") try: self.delete(config) except RTSLibError as err: self.error = True self.error_msg = "Unable to delete target - {}".format(err) def update_acl(self, acl_enabled): for tpg in self.tpg_list: if acl_enabled: tpg.set_attribute('generate_node_acls', 0) tpg.set_attribute('demo_mode_write_protect', 1) else: tpg.set_attribute('generate_node_acls', 1) tpg.set_attribute('demo_mode_write_protect', 0) def _get_gateway_name(self, ip): if ip in self.active_portal_ips: return this_host() target_config = self.config.config['targets'][self.iqn] for portal_name, portal_config in target_config['portals'].items(): if ip in portal_config['portal_ip_addresses']: return portal_name return None def get_tpg_by_gateway_name(self, gateway_name): tpg_tag = self.tpg_tag_by_gateway_name.get(gateway_name) if tpg_tag: for tpg_item in self.tpg_list: if tpg_item.tag == tpg_tag: return tpg_item return None def update_auth(self, tpg, username=None, password=None, mutual_username=None, mutual_password=None): tpg.chap_userid = username tpg.chap_password = password tpg.chap_mutual_userid = mutual_username tpg.chap_mutual_password = mutual_password auth_enabled = (username and password) if auth_enabled: tpg.set_attribute('authentication', '1') else: GWClient.try_disable_auth(tpg) def create_tpg(self, ip): try: gateway_name = self._get_gateway_name(ip) tpg = self.get_tpg_by_gateway_name(gateway_name) if not tpg: tpg = TPG(self.target) # Use initiator name based ACL by default. tpg.set_attribute('authentication', '0') self.logger.debug("(Gateway.create_tpg) Added tpg for portal " "ip {}".format(ip)) if ip in self.active_portal_ips: target_config = self.config.config['targets'][self.iqn] auth_config = target_config['auth'] config_chap = CHAP(auth_config['username'], auth_config['password'], auth_config['password_encryption_enabled']) if config_chap.error: self.error = True self.error_msg = config_chap.error_msg return config_chap_mutual = CHAP( auth_config['mutual_username'], auth_config['mutual_password'], auth_config['mutual_password_encryption_enabled']) if config_chap_mutual.error: self.error = True self.error_msg = config_chap_mutual.error_msg return self.update_auth(tpg, config_chap.user, config_chap.password, config_chap_mutual.user, config_chap_mutual.password) if self.enable_portal: NetworkPortal(tpg, normalize_ip_literal(ip)) tpg.enable = True self.logger.debug("(Gateway.create_tpg) Added tpg for " "portal ip {} is enabled".format(ip)) else: NetworkPortal(tpg, normalize_ip_literal(ip)) # disable the tpg on this host tpg.enable = False # by disabling tpg_enabled_sendtargets, discovery to just one # node will return all portals (default is 1) tpg.set_attribute('tpg_enabled_sendtargets', '0') self.logger.debug("(Gateway.create_tpg) Added tpg for " "portal ip {} as disabled".format(ip)) self.tpg_list.append(tpg) self.tpg_tag_by_gateway_name[gateway_name] = tpg.tag except RTSLibError as err: self.error_msg = err self.error = True else: self.changes_made = True self.logger.info("(Gateway.create_tpg) created TPG '{}' " "for target iqn '{}'".format(tpg.tag, self.iqn)) def create_target(self): """ Add an iSCSI target to LIO with this objects iqn name, and bind to the IP that aligns with the given iscsi_network """ try: iscsi_fabric = ISCSIFabricModule() self.target = Target(iscsi_fabric, wwn=self.iqn) self.logger.debug("(Gateway.create_target) Added iscsi target - " "{}".format(self.iqn)) # tpg's are defined in the sequence provide by the gateway_ip_list, # so across multiple gateways the same tpg number will be # associated with the same IP - however, only the tpg with an IP on # the host will be in an enabled state. The other tpgs are # necessary for systems like ESX who issue a rtpg scsi inquiry # only to one of the gateways - so that gateway must provide # details for the whole configuration self.logger.debug("Creating tpgs") for ip in self.gateway_ip_list: self.create_tpg(ip) if self.error: self.logger.critical("Unable to create the TPG for {} " "- {}".format(ip, self.error_msg)) self.update_tpg_controls() except RTSLibError as err: self.error_msg = err self.logger.critical("Unable to create the Target definition " "- {}".format(self.error_msg)) self.error = True if self.error: if self.target: self.target.delete() else: self.changes_made = True self.logger.info("(Gateway.create_target) created an iscsi target " "with iqn of '{}'".format(self.iqn)) def load_config(self): """ Grab the target, tpg and portal objects from LIO and store in this Gateway object """ try: self.target = Target(ISCSIFabricModule(), self.iqn, "lookup") # clear list so we can rebuild with the current values below if self.tpg_list: del self.tpg_list[:] if self.tpg_tag_by_gateway_name: self.tpg_tag_by_gateway_name = {} # there could/should be multiple tpg's for the target for tpg in self.target.tpgs: self.tpg_list.append(tpg) ip_address = list(tpg.network_portals)[0].ip_address gateway_name = self._get_gateway_name(ip_address) if gateway_name: self.tpg_tag_by_gateway_name[gateway_name] = tpg.tag # self.portal = self.tpg.network_portals.next() except RTSLibError as err: self.error_msg = err self.error = True self.logger.info("(Gateway.load_config) successfully loaded existing " "target definition") def bind_alua_group_to_lun(self, config, lun, tpg_ip_address=None): """ bind lun to one of the alua groups. Query the config to see who 'owns' the primary path for this LUN. Then either bind the LUN to the ALUA 'AO' group if the host matches, or default to the 'ANO'/'Standby' alua group param config: Config object param lun: lun object on the tpg param tpg_ip: IP of Network Portal for the lun's tpg. """ stg_object = lun.storage_object disk_config = [ disk for _, disk in config.config['disks'].items() if disk['backstore_object_name'] == stg_object.name ][0] owning_gw = disk_config['owner'] tpg = lun.parent_tpg if not tpg_ip_address: # just need to check one portal for ip in tpg.network_portals: tpg_ip_address = normalize_ip_address(ip.ip_address) break if tpg_ip_address is None: # this is being run during boot so the NP is not setup yet. return target_config = config.config["targets"][self.iqn] is_owner = False gw_config = target_config['portals'].get(owning_gw, None) # If the user has exported a disk through multiple targets but # they do not have a common gw the owning gw may not exist here. # The LUN will just have all ANO paths then. if gw_config: if tpg_ip_address in gw_config["portal_ip_addresses"]: is_owner = True try: alua_tpg = alua_create_group(settings.config.alua_failover_type, tpg, stg_object, is_owner) except CephiSCSIInval: raise except RTSLibError: self.logger.info("ALUA group id {} for stg obj {} lun {} " "already made".format(tpg.tag, stg_object, lun)) group_name = alua_format_group_name( tpg, settings.config.alua_failover_type, is_owner) # someone mapped a LU then unmapped it without deleting the # stg_object, or we are reloading the config. alua_tpg = ALUATargetPortGroup(stg_object, group_name) if alua_tpg.tg_pt_gp_id != tpg.tag: # ports and owner were rearranged. Not sure we support that. raise CephiSCSIInval("Existing ALUA group tag for group {} " "in invalid state.\n".format(group_name)) # drop down in case we are restarting due to error and we # were not able to bind to a lun last time. self.logger.info( "Setup group {} for {} on tpg {} (state {}, owner {}, " "failover type {})".format(alua_tpg.name, stg_object.name, tpg.tag, alua_tpg.alua_access_state, is_owner, alua_tpg.alua_access_type)) self.logger.debug("Setting Luns tg_pt_gp to {}".format(alua_tpg.name)) lun.alua_tg_pt_gp_name = alua_tpg.name self.logger.debug("Bound {} on tpg{} to {}".format( stg_object.name, tpg.tag, alua_tpg.name)) def _map_lun(self, config, stg_object, target_disk_config): for tpg in self.tpg_list: self.logger.debug("processing tpg{}".format(tpg.tag)) lun_id = target_disk_config['lun_id'] try: mapped_lun = LUN(tpg, lun=lun_id, storage_object=stg_object) self.changes_made = True except RTSLibError as err: if "already exists in configFS" not in str(err): self.logger.error("LUN mapping failed: {}".format(err)) self.error = True self.error_msg = err return # Already created. Ignore and loop to the next tpg. continue try: self.bind_alua_group_to_lun(config, mapped_lun) except CephiSCSIInval as err: self.logger.error("Could not bind LUN to ALUA group: " "{}".format(err)) self.error = True self.error_msg = err return def map_lun(self, config, stg_object, target_disk_config): self.load_config() self._map_lun(config, stg_object, target_disk_config) def map_luns(self, config): """ LIO will have objects already defined by the lun module, so this method, brings those objects into the gateways TPG """ target_config = config.config["targets"][self.iqn] for disk_id, disk in target_config['disks'].items(): stg_object = lookup_storage_object_by_disk(config, disk_id) if stg_object is None: err_msg = "Could not map {} to LUN. Disk not found".format( disk_id) self.logger.error(err_msg) self.error = True self.error_msg = err_msg return self._map_lun(config, stg_object, disk) if self.error: return def delete(self, config): saved_err = None if self.target is None: self.load_config() # Ignore errors. Target was probably not setup. Try to clean up # disks. if self.target: try: self.target.delete() except RTSLibError as err: self.logger.error("lio target deletion failed {}".format(err)) saved_err = err # drop down and try to delete disks for disk in config.config['targets'][self.iqn]['disks'].keys(): so = lookup_storage_object_by_disk(config, disk) if so is None: self.logger.debug("lio disk lookup failed {}") # SO may not have got setup. Ignore. continue if so.status == 'activated': # Still mapped so ignore. continue try: so.delete() except RTSLibError as err: self.logger.error("lio disk deletion failed {}".format(err)) if saved_err is None: saved_err = err # Try the other disks. if saved_err: raise RTSLibError(saved_err) def manage(self, mode): """ Manage the definition of the gateway, given a mode of 'target', 'map', 'init' or 'clearconfig'. In 'target' mode the LIO TPG is defined, whereas in map mode, the required LUNs are added to the existing TPG :param mode: run mode - target, map, init or clearconfig (str) :return: None - but sets the objects error flags to be checked by the caller """ config = Config(self.logger) if config.error: self.error = True self.error_msg = config.error_msg return local_gw = this_host() if mode == 'target': if self.exists(): self.load_config() self.check_tpgs() else: self.create_target() if self.error: # return to caller, with error state set return target_config = config.config["targets"][self.iqn] self.update_acl(target_config['acl_enabled']) discovery_auth_config = config.config['discovery_auth'] Discovery.set_discovery_auth_lio( discovery_auth_config['username'], discovery_auth_config['password'], discovery_auth_config['password_encryption_enabled'], discovery_auth_config['mutual_username'], discovery_auth_config['mutual_password'], discovery_auth_config['mutual_password_encryption_enabled']) gateway_group = config.config["gateways"].keys() if "ip_list" not in target_config: target_config['ip_list'] = self.gateway_ip_list config.update_item("targets", self.iqn, target_config) self.config_updated = True if self.controls != target_config.get('controls', {}): target_config['controls'] = self.controls.copy() config.update_item("targets", self.iqn, target_config) self.config_updated = True if local_gw not in gateway_group: gateway_metadata = {"active_luns": 0} config.add_item("gateways", local_gw) config.update_item("gateways", local_gw, gateway_metadata) self.config_updated = True if local_gw not in target_config['portals']: # Update existing gws with the new gw for remote_gw, remote_gw_config in target_config[ 'portals'].items(): if remote_gw_config[ 'gateway_ip_list'] == self.gateway_ip_list: continue inactive_portal_ip = list(self.gateway_ip_list) for portal_ip_address in remote_gw_config[ "portal_ip_addresses"]: inactive_portal_ip.remove(portal_ip_address) remote_gw_config['gateway_ip_list'] = self.gateway_ip_list remote_gw_config['tpgs'] = len(self.tpg_list) remote_gw_config[ 'inactive_portal_ips'] = inactive_portal_ip target_config['portals'][remote_gw] = remote_gw_config # Add the new gw inactive_portal_ip = list(self.gateway_ip_list) for active_portal_ip in self.active_portal_ips: inactive_portal_ip.remove(active_portal_ip) portal_metadata = { "tpgs": len(self.tpg_list), "gateway_ip_list": self.gateway_ip_list, "portal_ip_addresses": self.active_portal_ips, "inactive_portal_ips": inactive_portal_ip } target_config['portals'][local_gw] = portal_metadata target_config['ip_list'] = self.gateway_ip_list config.update_item("targets", self.iqn, target_config) self.config_updated = True if self.config_updated: config.commit() elif mode == 'map': if self.exists(): self.load_config() self.map_luns(config) target_config = config.config["targets"][self.iqn] self.update_acl(target_config['acl_enabled']) else: self.error = True self.error_msg = ("Attempted to map to a gateway '{}' that " "hasn't been defined yet...out of order " "steps?".format(self.iqn)) elif mode == 'init': # init mode just creates the iscsi target definition and updates # the config object. It is used by the CLI only if self.exists(): self.logger.info("GWTarget init request skipped - target " "already exists") else: # create the target self.create_target() # if error happens, we should never store this target to config if self.error: return seed_target = { 'disks': {}, 'clients': {}, 'acl_enabled': True, 'auth': { 'username': '', 'password': '', 'password_encryption_enabled': False, 'mutual_username': '', 'mutual_password': '', 'mutual_password_encryption_enabled': False }, 'portals': {}, 'groups': {}, 'controls': {} } config.add_item("targets", self.iqn, seed_target) config.commit() discovery_auth_config = config.config['discovery_auth'] Discovery.set_discovery_auth_lio( discovery_auth_config['username'], discovery_auth_config['password'], discovery_auth_config['password_encryption_enabled'], discovery_auth_config['mutual_username'], discovery_auth_config['mutual_password'], discovery_auth_config['mutual_password_encryption_enabled'] ) elif mode == 'clearconfig': # Called by API from CLI clearconfig command if self.exists(): self.load_config() self.clear_config(config) if self.error: return target_config = config.config["targets"][self.iqn] if len(target_config['portals']) == 0: config.del_item('targets', self.iqn) else: gw_ips = target_config['portals'][local_gw][ 'portal_ip_addresses'] target_config['portals'].pop(local_gw) ip_list = target_config['ip_list'] for gw_ip in gw_ips: ip_list.remove(gw_ip) if len(ip_list) > 0 and len( target_config['portals'].keys()) > 0: config.update_item('targets', self.iqn, target_config) else: # no more portals in the list, so delete the target config.del_item('targets', self.iqn) remove_gateway = True for _, target in config.config["targets"].items(): if local_gw in target['portals']: remove_gateway = False break if remove_gateway: # gateway is no longer used, so delete it config.del_item('gateways', local_gw) config.commit() @staticmethod def get_num_sessions(target_iqn): if not GWTarget._exists(target_iqn): return 0 with open( '/sys/kernel/config/target/iscsi/{}/fabric_statistics/iscsi_instance' '/sessions'.format(target_iqn)) as sessions_file: return int(sessions_file.read().rstrip('\n'))
class GWTarget(object): """ Class representing the state of the local LIO environment """ def __init__(self, logger, iqn, gateway_ip_list, enable_portal=True): """ Instantiate the class :param iqn: iscsi iqn name for the gateway :param gateway_ip_list: list of IP addresses to be defined as portals to LIO :return: gateway object """ self.error = False self.error_msg = '' self.enable_portal = enable_portal # boolean to trigger portal # IP creation self.logger = logger # logger object self.iqn = iqn # If the ip list received has data in it, this is a target we need to # act on the IP's provided, otherwise just set to null if gateway_ip_list: # if the ip list provided doesn't match any ip of this host, abort # the assumption here is that we'll only have one matching ip in # the list! matching_ip = set(gateway_ip_list).intersection(ipv4_addresses()) if len(list(matching_ip)) == 0: self.error = True self.error_msg = ("gateway IP addresses provided do not match" " any ip on this host") return self.active_portal_ip = list(matching_ip)[0] self.logger.debug("active portal will use " "{}".format(self.active_portal_ip)) self.gateway_ip_list = gateway_ip_list self.logger.debug("tpg's will be defined in this order" " - {}".format(self.gateway_ip_list)) else: # without gateway_ip_list passed in this is a 'init' or # 'clearconfig' request self.gateway_ip_list = [] self.active_portal_ip = [] self.changes_made = False self.config_updated = False # self.portal = None self.target = None self.tpg = None self.tpg_list = [] def exists(self): """ Basic check to see whether this iqn already exists in kernel's configFS directory :return: boolean """ return os.path.exists('/sys/kernel/config/target/iscsi/' '{}'.format(self.iqn)) def _get_portals(self, tpg): """ return a list of network portal IPs allocated to a specfic tpg :param tpg: tpg to check (object) :return: list of IP's this tpg has (list) """ return [portal.ip_address for portal in tpg.network_portals] def check_tpgs(self): # process the portal IP's in order to preserve the tpg sequence # across gateways requested_tpg_ips = list(self.gateway_ip_list) current_tpgs = list(self.tpg_list) for portal_ip in self.gateway_ip_list: for tpg in current_tpgs: if portal_ip in self._get_portals(tpg): # portal requested is defined, so remove from the list requested_tpg_ips.remove(portal_ip) current_tpgs.remove(tpg) break # if the requested_tpg_ips list has entries, we need to add new tpg's if requested_tpg_ips: self.logger.info("An additional {} tpg's are " "required".format(len(requested_tpg_ips))) for ip in requested_tpg_ips: self.create_tpg(ip) def enable_active_tpg(self, config): """ Add the relevant ip to the active/enabled tpg within the target and bind the tpg's luns to an ALUA group. :return: None """ for tpg in self.tpg_list: if tpg._get_enable(): for lun in tpg.luns: self.bind_alua_group_to_lun( config, lun, tpg_ip_address=self.active_portal_ip) try: NetworkPortal(tpg, self.active_portal_ip) except RTSLibError as e: self.error = True self.error_msg = e else: break def clear_config(self): """ Remove the target definition form LIO :return: None """ # check that there aren't any disks or clients in the configuration lio_root = root.RTSRoot() disk_count = len([disk for disk in lio_root.storage_objects]) clients = [] for tpg in self.tpg_list: tpg_clients = [node for node in tpg._list_node_acls()] clients += tpg_clients client_count = len(clients) if disk_count > 0 or client_count > 0: self.error = True self.error_msg = ("Clients({}) and disks({}) must be removed" "before the gateways".format( client_count, disk_count)) return self.logger.debug("Clients defined :{}".format(client_count)) self.logger.debug("Disks defined :{}".format(disk_count)) self.logger.info("Removing target configuration") try: self.delete() except RTSLibError as err: self.error = True self.error_msg = "Unable to delete target - {}".format(err) def create_tpg(self, ip): try: tpg = TPG(self.target) # Use initiator name based ACL by default. tpg.set_attribute('authentication', '0') self.logger.debug("(Gateway.create_tpg) Added tpg for portal " "ip {}".format(ip)) if ip == self.active_portal_ip: if self.enable_portal: NetworkPortal(tpg, ip) tpg.enable = True self.logger.debug("(Gateway.create_tpg) Added tpg for " "portal ip {} is enabled".format(ip)) else: NetworkPortal(tpg, ip) # disable the tpg on this host tpg.enable = False # by disabling tpg_enabled_sendtargets, discovery to just one # node will return all portals (default is 1) tpg.set_attribute('tpg_enabled_sendtargets', '0') self.logger.debug("(Gateway.create_tpg) Added tpg for " "portal ip {} as disabled".format(ip)) self.tpg_list.append(tpg) except RTSLibError as err: self.error_msg = err self.error = True else: self.changes_made = True self.logger.info("(Gateway.create_tpg) created TPG '{}' " "for target iqn '{}'".format(tpg.tag, self.iqn)) def create_target(self): """ Add an iSCSI target to LIO with this objects iqn name, and bind to the IP that aligns with the given iscsi_network """ try: iscsi_fabric = ISCSIFabricModule() self.target = Target(iscsi_fabric, wwn=self.iqn) self.logger.debug("(Gateway.create_target) Added iscsi target - " "{}".format(self.iqn)) # tpg's are defined in the sequence provide by the gateway_ip_list, # so across multiple gateways the same tpg number will be # associated with the same IP - however, only the tpg with an IP on # the host will be in an enabled state. The other tpgs are # necessary for systems like ESX who issue a rtpg scsi inquiry # only to one of the gateways - so that gateway must provide # details for the whole configuration self.logger.debug("Creating tpgs") for ip in self.gateway_ip_list: self.create_tpg(ip) if self.error: self.logger.critical("Unable to create the TPG for {} " "- {}".format(ip, self.error_msg)) except RTSLibError as err: self.error_msg = err self.logger.critical("Unable to create the Target definition " "- {}".format(self.error_msg)) self.error = True if self.error: self.delete() else: self.changes_made = True self.logger.info("(Gateway.create_target) created an iscsi target " "with iqn of '{}'".format(self.iqn)) def load_config(self): """ Grab the target, tpg and portal objects from LIO and store in this Gateway object """ try: lio_root = root.RTSRoot() self.target = [ tgt for tgt in lio_root.targets if tgt.wwn == self.iqn ][0] # there could/should be multiple tpg's for the target for tpg in self.target.tpgs: self.tpg_list.append(tpg) # self.portal = self.tpg.network_portals.next() except RTSLibError as err: self.error_msg = err self.error = True self.logger.info("(Gateway.load_config) successfully loaded existing " "target definition") def bind_alua_group_to_lun(self, config, lun, tpg_ip_address=None): """ bind lun to one of the alua groups. Query the config to see who 'owns' the primary path for this LUN. Then either bind the LUN to the ALUA 'AO' group if the host matches, or default to the 'ANO' alua group param config: Config object param lun: lun object on the tpg param tpg_ip: IP of Network Portal for the lun's tpg. """ # return stg_object = lun.storage_object owning_gw = config.config['disks'][stg_object.name]['owner'] tpg = lun.parent_tpg if tpg_ip_address is None: # just need to check one portal for ip in tpg.network_portals: tpg_ip_address = ip.ip_address break if tpg_ip_address is None: # this is being run during boot so the NP is not setup yet. return # TODO: The ports in a alua group must export the same state for a LU # group. For different LUs we are exporting different states, so # we should be creating different LU groups or creating different # alua groups for each LU. try: if config.config["gateways"][owning_gw][ "portal_ip_address"] == tpg_ip_address: self.logger.info("setting {} to ALUA/ActiveOptimised " "group id {}".format(stg_object.name, tpg.tag)) group_name = "ao" alua_tpg = ALUATargetPortGroup(stg_object, group_name, tpg.tag) alua_tpg.preferred = 1 else: self.logger.info("setting {} to ALUA/Standby" "group id {}".format(stg_object.name, tpg.tag)) group_name = "standby{}".format(tpg.tag) alua_tpg = ALUATargetPortGroup(stg_object, group_name, tpg.tag) except RTSLibError as err: self.logger.info("ALUA group id {} for stg obj {} lun {} " "already made".format(tpg.tag, stg_object, lun)) # someone mapped a LU then unmapped it without deleting the # stg_object, or we are reloading the config. alua_tpg = ALUATargetPortGroup(stg_object, group_name) if alua_tpg.tpg_id != tpg.tag: # ports and owner were rearranged. Not sure we support that. raise RTSLibError # drop down in case we are restarting due to error and we # were not able to bind to a lun last time. self.logger.debug("ALUA defined, updating state") # Use Explicit but also set the Implicit bit so we can # update the kernel from configfs. alua_tpg.alua_access_type = 3 # start ports in Standby, and let the initiator drive the initial # transition to AO. alua_tpg.alua_access_state = 2 alua_tpg.alua_support_offline = 0 alua_tpg.alua_support_unavailable = 0 alua_tpg.alua_support_standby = 1 alua_tpg.alua_support_transitioning = 1 alua_tpg.implicit_trans_secs = 60 alua_tpg.nonop_delay_msecs = 0 # alua_tpg.bind_to_lun(lun) self.logger.debug("Setting Luns tg_pt_gp to {}".format(group_name)) lun.alua_tg_pt_gp_name = group_name self.logger.debug("Bound {} on tpg{} to {}".format( stg_object.name, tpg.tag, group_name)) def map_luns(self, config): """ LIO will have objects already defined by the lun module, so this method, brings those objects into the gateways TPG """ lio_root = root.RTSRoot() # process each storage object added to the gateway, and map to the tpg for stg_object in lio_root.storage_objects: for tpg in self.tpg_list: self.logger.debug("processing tpg{}".format(tpg.tag)) if not self.lun_mapped(tpg, stg_object): self.logger.debug("{} needed mapping to " "tpg{}".format(stg_object.name, tpg.tag)) lun_id = int(stg_object.path.split('/')[-2].split('_')[1]) try: mapped_lun = LUN(tpg, lun=lun_id, storage_object=stg_object) self.changes_made = True except RTSLibError as err: self.logger.error("LUN mapping failed: {}".format(err)) self.error = True self.error_msg = err return self.bind_alua_group_to_lun(config, mapped_lun) def lun_mapped(self, tpg, storage_object): """ Check to see if a given storage object (i.e. block device) is already mapped to the gateway's TPG :param storage_object: storage object to look for :return: boolean - is the storage object mapped or not """ mapped_state = False for l in tpg.luns: if l.storage_object.name == storage_object.name: mapped_state = True break return mapped_state def delete(self): self.target.delete() def manage(self, mode): """ Manage the definition of the gateway, given a mode of 'target', 'map', 'init' or 'clearconfig'. In 'target' mode the LIO TPG is defined, whereas in map mode, the required LUNs are added to the existing TPG :param mode: run mode - target, map, init or clearconfig (str) :return: None - but sets the objects error flags to be checked by the caller """ config = Config(self.logger) if config.error: self.error = True self.error_msg = config.error_msg return local_gw = this_host() if mode == 'target': if self.exists(): self.load_config() self.check_tpgs() else: self.create_target() if self.error: # return to caller, with error state set return gateway_group = config.config["gateways"].keys() # this action could be carried out by multiple nodes concurrently, # but since the value is the same (i.e all gateway nodes use the # same iqn) it's not worth worrying about! if "iqn" not in gateway_group: self.config_updated = True config.add_item("gateways", "iqn", initial_value=self.iqn) if "ip_list" not in gateway_group: self.config_updated = True config.add_item("gateways", "ip_list", initial_value=self.gateway_ip_list) if local_gw not in gateway_group: inactive_portal_ip = list(self.gateway_ip_list) inactive_portal_ip.remove(self.active_portal_ip) gateway_metadata = { "portal_ip_address": self.active_portal_ip, "iqn": self.iqn, "active_luns": 0, "tpgs": len(self.tpg_list), "inactive_portal_ips": inactive_portal_ip, "gateway_ip_list": self.gateway_ip_list } config.add_item("gateways", local_gw) config.update_item("gateways", local_gw, gateway_metadata) config.update_item("gateways", "ip_list", self.gateway_ip_list) self.config_updated = True else: # gateway already defined, so check that the IP list it has # matches the current request gw_details = config.config['gateways'][local_gw] if cmp(gw_details['gateway_ip_list'], self.gateway_ip_list) != 0: inactive_portal_ip = list(self.gateway_ip_list) inactive_portal_ip.remove(self.active_portal_ip) gw_details['tpgs'] = len(self.tpg_list) gw_details['gateway_ip_list'] = self.gateway_ip_list gw_details['inactive_portal_ips'] = inactive_portal_ip config.update_item('gateways', local_gw, gw_details) self.config_updated = True if self.config_updated: config.commit() elif mode == 'map': if self.exists(): self.load_config() self.map_luns(config) else: self.error = True self.error_msg = ("Attempted to map to a gateway '{}' that " "hasn't been defined yet...out of order " "steps?".format(self.iqn)) elif mode == 'init': # init mode just creates the iscsi target definition and updates # the config object. It is used by the CLI only if self.exists(): self.logger.info("GWTarget init request skipped - target " "already exists") else: # create the target self.create_target() current_iqn = config.config['gateways'].get('iqn', '') # First gateway asked to create the target will update the # config object if not current_iqn: config.add_item("gateways", "iqn", initial_value=self.iqn) config.commit() elif mode == 'clearconfig': # Called by API from CLI clearconfig command if self.exists(): self.load_config() else: self.error = True self.error_msg = "IQN provided does not exist" self.clear_config() if not self.error: gw_ip = config.config['gateways'][local_gw][ 'portal_ip_address'] config.del_item('gateways', local_gw) ip_list = config.config['gateways']['ip_list'] ip_list.remove(gw_ip) if len(ip_list) > 0: config.update_item('gateways', 'ip_list', ip_list) else: # no more gateways in the list, so delete remaining items config.del_item('gateways', 'ip_list') config.del_item('gateways', 'iqn') config.del_item('gateways', 'created') config.commit()
class Gateway(object): """ Class representing the state of the local LIO environment """ def __init__(self, iqn, iscsi_network): """ Instantiate the class :param iqn: iscsi iqn name for the gateway :param iscsi_network: network subnet to bind to (i.e. use for the portal IP) :return: gateway object """ self.error = False self.error_msg = '' self.iqn = iqn self.ip_address = get_ip_address(iscsi_network) if not self.ip_address: self.error = True self.error_msg = ( "Unable to find an IP on this host, that matches" " the iscsi_network setting {}".format(iscsi_network)) self.type = Config.get_platform() self.changes_made = False self.portal = None self.target = None self.tpg = None def exists(self): """ Basic check to see whether this iqn already exists in kernel's configFS directory :return: boolean """ return os.path.exists('/sys/kernel/config/target/iscsi/{}'.format( self.iqn)) def create_target(self): """ Add an iSCSI target to LIO with this objects iqn name, and bind to the IP that aligns with the given iscsi_network """ try: iscsi_fabric = ISCSIFabricModule() self.target = Target(iscsi_fabric, wwn=self.iqn) logger.debug( "(Gateway.create_target) Added iscsi target - {}".format( self.iqn)) self.tpg = TPG(self.target) logger.debug("(Gateway.create_target) Added tpg") self.tpg.enable = True self.portal = NetworkPortal(self.tpg, self.ip_address) logger.debug( "(Gateway.create_target) Added portal IP '{}' to tpg".format( self.ip_address)) except RTSLibError as err: self.error_msg = err self.error = True self.delete() self.changes_made = True logger.info( "(Gateway.create_target) created an iscsi target with iqn of '{}'". format(self.iqn)) def load_config(self): """ Grab the target, tpg and portal objects from LIO and store in this Gateway object """ try: # since we only support one target/TPG, we just grab the first iterable lio_root = root.RTSRoot() self.target = lio_root.targets.next() self.tpg = self.target.tpgs.next() self.portal = self.tpg.network_portals.next() except RTSLibError as err: self.error_msg = err self.error = True logger.info( "(Gateway.load_config) successfully loaded existing target definition" ) def map_luns(self): """ LIO will have blockstorage objects already defined by the igw_lun module, so this method, brings those objects into the gateways TPG """ lio_root = root.RTSRoot() # process each storage object added to the gateway, and map to the tpg for stg_object in lio_root.storage_objects: if not self.lun_mapped(stg_object): # use the iblock number for the lun id - /sys/kernel/config/target/core/iblock_1/ansible4 # ^ lun_id = int(stg_object._path.split('/')[-2].split('_')[1]) try: mapped_lun = LUN(self.tpg, lun=lun_id, storage_object=stg_object) self.changes_made = True except RTSLibError as err: self.error = True self.error_msg = err break def lun_mapped(self, storage_object): """ Check to see if a given storage object (i.e. block device) is already mapped to the gateway's TPG :param storage_object: storage object to look for :return: boolean - is the storage object mapped or not """ mapped_state = False for l in self.tpg.luns: if l.storage_object.name == storage_object.name: mapped_state = True break return mapped_state def delete(self): self.target.delete()