Пример #1
0
    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")
Пример #2
0
    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))
Пример #3
0
 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
Пример #4
0
    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))
Пример #5
0
    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
Пример #6
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
Пример #7
0
    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")
Пример #8
0
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()
Пример #9
0
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))
Пример #10
0
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'))
Пример #11
0
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()
Пример #12
0
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()