示例#1
0
    def define_client(self):
        """
        Establish the links for this object to the corresponding ACL and TPG objects from LIO
        :return:
        """

        r = lio_root.RTSRoot()

        # NB. this will check all tpg's for a matching iqn
        for client in r.node_acls:
            if client.node_wwn == self.iqn:
                self.acl = client
                self.tpg = client.parent_tpg
                logger.debug("(Client.define_client) - {} already defined".format(self.iqn))
                return

        # at this point the client does not exist, so create it
        # NB. The solution supports only a single tpg definition, so simply grabbing the
        # first tpg is fine. If multiple tpgs are required this will need more work
        self.tpg = r.tpgs.next()

        try:
            self.acl = NodeACL(self.tpg, self.iqn)
        except RTSLibError as err:
            logger.error("(Client.define_client) FAILED to define {}".format(self.iqn))
            logger.debug("(Client.define_client) failure msg {}".format(err))
            self.error = True
            self.error_msg = err
        else:
            self.change_count += 1
            logger.info("(Client.define_client) {} added successfully".format(self.iqn))
示例#2
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
示例#3
0
    def define_client(self):
        """
        Establish the links for this object to the corresponding ACL and TPG
        objects from LIO
        :return:
        """

        r = lio_root.RTSRoot()

        # NB. this will check all tpg's for a matching iqn
        for client in r.node_acls:
            if client.node_wwn == self.iqn:
                self.acl = client
                self.tpg = client.parent_tpg
                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 r.tpgs:
            if tpg.enable:
                self.tpg = tpg

        try:
            self.acl = NodeACL(self.tpg, self.iqn)
            # Try to detect network problems so we can kill connections
            # and cleanup before the initiator has begun recovery and
            # failed over.
            self.acl.set_attribute('dataout_timeout', '20')  # default  3
            # LIO default 30
            self.acl.set_attribute(
                'nopin_response_timeout',
                '{}'.format(settings.config.nopin_response_timeout))
            # LIO default 15
            self.acl.set_attribute('nopin_timeout',
                                   '{}'.format(settings.config.nopin_timeout))
        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
示例#4
0
class GWClient(GWObject):
    """
    This class holds a representation of a client connecting to LIO
    """
    SETTINGS = ["dataout_timeout",
                "nopin_response_timeout",
                "nopin_timeout",
                "cmdsn_depth"]

    seed_metadata = {"auth": {"username": '',
                              "password": '',
                              "password_encryption_enabled": False,
                              "mutual_username": '',
                              "mutual_password": '',
                              "mutual_password_encryption_enabled": False},
                     "luns": {},
                     "group_name": ""
                     }

    def __init__(self, logger, client_iqn, image_list, username, password, mutual_username,
                 mutual_password, target_iqn):
        """
        Instantiate an instance of an LIO client
        :param client_iqn: (str) iscsi iqn string
        :param image_list: (list) list of rbd images (pool/image) to attach
                           to this client or list of tuples (disk, lunid)
        :param username: (str) chap username
        :param password: (str) chap password
        :param mutual_username: (str) chap mutual username
        :param mutual_password: (str) chap mutual password
        :param target_iqn: (str) target iqn string
        :return:
        """

        self.target_iqn = target_iqn
        self.lun_lookup = {}        # only used for hostgroup based definitions
        self.requested_images = []

        # image_list is normally a list of strings (pool/image_name) but
        # group processing forces a specific lun id allocation to masked disks
        # in this scenario the image list is a tuple
        if image_list:

            if isinstance(image_list[0], tuple):
                # tuple format ('disk_name', {'lun_id': 0})...
                for disk_item in image_list:
                    disk_name = disk_item[0]
                    lun_id = disk_item[1].get('lun_id')
                    self.requested_images.append(disk_name)
                    self.lun_lookup[disk_name] = lun_id
            else:
                self.requested_images = image_list

        self.username = username
        self.password = password
        self.mutual_username = mutual_username
        self.mutual_password = mutual_password
        self.mutual = ''
        self.tpgauth = ''
        self.metadata = {}
        self.acl = None
        self.client_luns = {}
        self.tpg = None
        self.tpg_luns = {}
        self.lun_id_list = list(range(256))           # available LUN ids 0..255
        self.change_count = 0

        # enable commit to the config for changes by default
        self.commit_enabled = True

        self.logger = logger
        self.current_config = {}
        self.error = False
        self.error_msg = ''

        try:
            client_iqn, iqn_type = normalize_wwn(['iqn'], client_iqn)
        except RTSLibError as err:
            self.error = True
            self.error_msg = "Invalid iSCSI client name - {}".format(err)

        self.iqn = client_iqn

        # Validate the images list doesn't contain duplicate entries
        dup_images = set([rbd for rbd in image_list
                          if image_list.count(rbd) >= 2])
        if len(dup_images) > 0:
            self.error = True
            dup_string = ','.join(dup_images)
            self.error_msg = ("Client's image list contains duplicate rbd's"
                              ": {}".format(dup_string))

        try:
            super(GWClient, self).__init__('targets', target_iqn, logger,
                                           GWClient.SETTINGS)
        except CephiSCSIError as err:
            self.error = True
            self.error_msg = err

    def setup_luns(self, disks_config):
        """
        Add the requested LUNs to the node ACL definition. The image list
        defined for the client is compared to the current runtime settings,
        resulting in new images being added, or images removed.
        """

        # first drop the current lunid's used from the candidate list
        # this allows luns to be added/removed, and new id's to occupy free lun-id
        # slots rather than simply tag on the end. In a high churn environment,
        # adding new lun(s) at highest lun +1 could lead to exhausting the
        # 255 lun limit per target
        self.client_luns = self.get_images(self.acl)
        for image_name in self.client_luns:
            lun_id = self.client_luns[image_name]['lun_id']
            self.lun_id_list.remove(lun_id)
            self.logger.debug("(Client.setup_luns) {} has id of "
                              "{}".format(image_name, lun_id))

        self.tpg_luns = self.get_images(self.tpg)
        current_map = dict(self.client_luns)

        for image in self.requested_images:
            backstore_object_name = disks_config[image]['backstore_object_name']
            if backstore_object_name in self.client_luns:
                del current_map[backstore_object_name]
                continue
            else:
                rc = self._add_lun(image, self.tpg_luns[backstore_object_name])
                if rc != 0:
                    self.error = True
                    self.error_msg = ("{} is missing from the tpg - unable "
                                      "to map".format(image))
                    self.logger.debug("(Client.setup) tpg luns "
                                      "{}".format(self.tpg_luns))
                    self.logger.error("(Client.setup) missing image '{}' from "
                                      "the tpg".format(image))
                    return

        # 'current_map' should be empty, if not the remaining images need
        # to be removed from the client
        if current_map:
            for backstore_object_name in current_map:
                self._del_lun_map(backstore_object_name, disks_config)
                if self.error:
                    self.logger.error("(Client.setup) unable to delete {} from"
                                      " {}".format(self.iqn,
                                                   backstore_object_name))
                    return

    def update_acl_controls(self):
        self.logger.debug("(update_acl_controls) controls: {}".format(self.controls))
        self.acl.set_attribute('dataout_timeout', str(self.dataout_timeout))

        # Try to detect network problems so we can kill connections
        # and cleanup before the initiator has begun recovery and
        # failed over.

        # LIO default 30
        self.acl.set_attribute('nopin_response_timeout',
                               str(self.nopin_response_timeout))
        # LIO default 15
        self.acl.set_attribute('nopin_timeout', str(self.nopin_timeout))

        # LIO default 64
        self.acl.tcq_depth = self.cmdsn_depth

    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

    @staticmethod
    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

    @staticmethod
    def define_clients(logger, config, target_iqn):
        """
        define the clients (nodeACLs) to the gateway definition
        :param logger: logger object to print to
        :param config: configuration dict from the rados pool
        :raises CephiSCSIError.
        """

        # Client configurations (NodeACL's)
        target_config = config.config['targets'][target_iqn]
        for client_iqn in target_config['clients']:
            client_metadata = target_config['clients'][client_iqn]
            client_chap = CHAP(client_metadata['auth']['username'],
                               client_metadata['auth']['password'],
                               client_metadata['auth']['password_encryption_enabled'])
            client_chap_mutual = CHAP(client_metadata['auth']['mutual_username'],
                                      client_metadata['auth']['mutual_password'],
                                      client_metadata['auth'][
                                          'mutual_password_encryption_enabled'])

            image_list = list(client_metadata['luns'].keys())

            if client_chap.error:
                raise CephiSCSIError("Unable to decode password for {}. "
                                     "CHAP error: {}".format(client_iqn,
                                                             client_chap.error_msg))
            if client_chap_mutual.error:
                raise CephiSCSIError("Unable to decode password for {}. "
                                     "CHAP_MUTUAL error: {}".format(client_iqn,
                                                                    client_chap.error_msg))

            client = GWClient(logger,
                              client_iqn,
                              image_list,
                              client_chap.user,
                              client_chap.password,
                              client_chap_mutual.user,
                              client_chap_mutual.password,
                              target_iqn)

            client.manage('present')  # ensure the client exists

    def try_disable_auth(self, tpg):
        """
        Disable authentication (enable ACL mode) if this is the last CHAP user.

        LIO doesn't allow us to mix and match ACLs and auth under a tpg. We
        only allow ACL mode if there are not CHAP users.
        """

        for client in tpg.node_acls:
            if client.chap_userid or client.chap_password:
                return

        tpg.set_attribute('authentication', '0')

    def configure_auth(self, username, password, mutual_username, mutual_password, target_config):
        """
        Attempt to configure authentication for the client
        :return:
        """

        auth_enabled = (username and password)

        self.logger.debug("configuring auth username={}, password={}, mutual_username={}, "
                          "mutual_password={}".format(username, password, mutual_username,
                                                      mutual_password))
        acl_chap_userid = self.acl.chap_userid
        acl_chap_password = self.acl.chap_password
        acl_chap_mutual_userid = self.acl.chap_mutual_userid
        acl_chap_mutual_password = self.acl.chap_mutual_password

        try:
            self.logger.debug("Updating the ACL")
            if username != acl_chap_userid or \
                    password != acl_chap_password:
                self.acl.chap_userid = username
                self.acl.chap_password = password

                new_chap = CHAP(username, password, False)
                self.logger.debug("chap object set to: {},{},{}".format(
                    new_chap.user, new_chap.password, new_chap.password_str))

                if new_chap.error:
                    self.error = True
                    self.error_msg = new_chap.error_msg
                    return

            if mutual_username != acl_chap_mutual_userid or \
                    mutual_password != acl_chap_mutual_password:
                self.acl.chap_mutual_userid = mutual_username
                self.acl.chap_mutual_password = mutual_password

                new_chap_mutual = CHAP(mutual_username, mutual_password, False)
                self.logger.debug("chap mutual object set to: {},{},{}".format(
                    new_chap_mutual.user, new_chap_mutual.password,
                    new_chap_mutual.password_str))

                if new_chap_mutual.error:
                    self.error = True
                    self.error_msg = new_chap_mutual.error_msg
                    return

            if auth_enabled:
                self.tpg.set_attribute('authentication', '1')
            else:
                self.try_disable_auth(self.tpg)

            self.logger.debug("Updating config object meta data")
            encryption_enabled = encryption_available()
            if username != acl_chap_userid:
                self.metadata['auth']['username'] = new_chap.user
            if password != acl_chap_password:
                self.metadata['auth']['password'] = new_chap.encrypted_password(encryption_enabled)
                self.metadata['auth']['password_encryption_enabled'] = encryption_enabled
            if mutual_username != acl_chap_mutual_userid:
                self.metadata['auth']['mutual_username'] = new_chap_mutual.user
            if mutual_password != acl_chap_mutual_password:
                self.metadata['auth']['mutual_password'] = \
                    new_chap_mutual.encrypted_password(encryption_enabled)
                self.metadata['auth']['mutual_password_encryption_enabled'] = encryption_enabled

        except RTSLibError as err:
            self.error = True
            self.error_msg = ("Unable to configure authentication "
                              "for {} - ".format(self.iqn,
                                                 err))
            self.logger.error("(Client.configure_auth) failed to set "
                              "credentials for {}".format(self.iqn))
        else:
            self.change_count += 1

        self._update_acl(target_config)

    def _update_acl(self, target_config):
        if self.tpg.node_acls:
            self.tpg.set_attribute('generate_node_acls', 0)
            self.tpg.set_attribute('demo_mode_write_protect', 1)
            if not target_config['acl_enabled']:
                target_config['acl_enabled'] = True
                self.change_count += 1

    def _add_lun(self, image, lun):
        """
        Add a given image to the client ACL
        :param image: rbd image name of the form pool/image (str)
        :param lun: rtslib lun object
        :return:
        """

        rc = 0
        # get the tpg lun to map this client to
        tpg_lun = lun['tpg_lun']

        # lunid allocated from the current config object setting, or if this is
        # a new device from the next free lun id 'position'
        if image in self.metadata['luns'].keys():
            lun_id = self.metadata['luns'][image]['lun_id']
        else:
            if image in self.lun_lookup:
                # this indicates a lun map for a group managed client
                lun_id = self.lun_lookup[image]
            else:
                lun_id = self.lun_id_list[0]  # pick lowest available lun ID

        self.logger.debug("(Client._add_lun) Adding {} to {} at "
                          "id {}".format(image, self.iqn, lun_id))

        try:
            m_lun = self.acl.mapped_lun(lun_id, tpg_lun=tpg_lun)
        except RTSLibError as err:
            self.logger.error("Client.add_lun RTSLibError for lun id {} -"
                              " {}".format(lun_id, err))
            rc = 12
        else:

            self.client_luns[image] = {"lun_id": lun_id,
                                       "mapped_lun": m_lun,
                                       "tpg_lun": tpg_lun}

            self.metadata['luns'][image] = {"lun_id": lun_id}
            self.lun_id_list.remove(lun_id)
            self.logger.info("(Client.add_lun) added image '{}' to "
                             "{}".format(image, self.iqn))
            self.change_count += 1

        return rc

    def _del_lun_map(self, backstore_object_name, disks_config):
        """
        Delete a lun from the client's ACL
        :param backstore_object_name: rbd image name to remove
        :return:
        """

        lun = self.client_luns[backstore_object_name]['mapped_lun']
        try:
            lun.delete()
        except RTSLibError as err:
            self.error = True
            self.error_msg = err
        else:
            self.change_count += 1

            disk_id = [disk_id for disk_id, disk in disks_config.items()
                       if disk['backstore_object_name'] == backstore_object_name][0]
            # the lun entry could have been deleted by another host, so before
            # we try and delete - make sure it's in our local copy of the
            # metadata!
            if disk_id in self.metadata['luns']:
                del self.metadata['luns'][disk_id]

    def delete(self):
        """
        Delete the client definition from LIO
        :return:
        """

        try:
            self.acl.delete()
            self.try_disable_auth(self.tpg)
            self.change_count += 1
            self.logger.info("(Client.delete) deleted NodeACL for "
                             "{}".format(self.iqn))
        except RTSLibError as err:
            self.error = True
            self.error_msg = "RTS NodeACL delete failure"
            self.logger.error("(Client.delete) failed to delete client {} "
                              "- error: {}".format(self.iqn,
                                                   err))

    def exists(self):
        """
        This function determines whether this instances iqn is already defined
        to LIO
        :return: Boolean
        """

        r = lio_root.RTSRoot()
        client_list = [client.node_wwn for client in r.node_acls]
        return self.iqn in client_list

    def seed_config(self, config):
        """
        function to seed the config object with a new client definition
        """
        target_config = config.config["targets"][self.target_iqn]
        target_config['clients'][self.iqn] = GWClient.seed_metadata
        config.update_item("targets", self.target_iqn, target_config)

        # persist the config update, and leave the connection to the ceph
        # object open since adding just the iqn is only the start of the
        # definition
        config.commit("retain")

    def manage(self, rqst_type, committer=None):
        """
        Manage the allocation or removal of this client
        :param rqst_type is either 'present' (try and create the nodeACL), or
        'absent' - delete the nodeACL
        :param committer is the host responsible for any commits to the
        configuration - this is not needed for Ansible management, but is used
        by the CLI->API->GWClient interaction
        """
        # Build a local object representing the rados configuration object
        config_object = Config(self.logger)
        if config_object.error:
            self.error = True
            self.error_msg = config_object.error_msg
            return

        # use current config to hold a copy of the current rados config
        # object (dict)
        self.current_config = config_object.config
        target_config = self.current_config['targets'][self.target_iqn]
        update_host = committer

        self.logger.debug("GWClient.manage) update host to handle any config "
                          "update is {}".format(update_host))

        if rqst_type == "present":

            ###################################################################
            # Ensure the client exists in LIO                                 #
            ###################################################################

            # first look at the request to see if it matches the settings
            # already in the config object - if so this is just a rerun, or a
            # reboot so config object updates are not needed when we change
            # the LIO environment
            if self.iqn in target_config['clients'].keys():
                self.metadata = target_config['clients'][self.iqn]
                config_image_list = sorted(self.metadata['luns'].keys())

                #
                # Does the request match the current config?

                auth_config = self.metadata['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
                # extract the chap_mutual_str from the config object entry
                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

                if self.username == config_chap.user and \
                   self.password == config_chap.password and \
                   self.mutual_username == config_chap_mutual.user and \
                   self.mutual_password == config_chap_mutual.password and \
                   config_image_list == sorted(self.requested_images):
                    self.commit_enabled = False
            else:
                # requested iqn is not in the config object
                self.seed_config(config_object)
                self.metadata = GWClient.seed_metadata

            self.logger.debug("(manage) config updates to be applied from "
                              "this host: {}".format(self.commit_enabled))

            client_exists = self.exists()
            self.define_client()
            if self.error:
                # unable to define the client!
                return

            if client_exists and self.metadata["group_name"]:
                # bypass setup_luns for existing clients that have an
                # associated host group
                pass
            else:
                # either the client didn't exist (new or boot time), or the
                # group_name is not defined so run setup_luns for this client
                disks_config = self.current_config['disks']
                bad_images = self.validate_images(disks_config)
                if not bad_images:

                    self.setup_luns(disks_config)
                    if self.error:
                        return
                else:
                    # request for images to map to this client that haven't
                    # been added to LIO yet!
                    self.error = True
                    self.error_msg = ("Non-existent images {} requested "
                                      "for {}".format(bad_images, self.iqn))
                    return

            if not self.username and not self.password and \
               not self.mutual_username and not self.mutual_password:
                self.logger.warning("(main) client '{}' configured without"
                                    " security".format(self.iqn))

            self.configure_auth(self.username, self.password, self.mutual_username,
                                self.mutual_password, target_config)

            if self.error:
                return

            # check the client object's change count, and update the config
            # object if this is the updating host
            if self.change_count > 0:

                if self.commit_enabled:

                    if update_host == this_host():
                        # update the config object with this clients settings
                        self.logger.debug("Updating config object metadata "
                                          "for '{}'".format(self.iqn))
                        target_config['clients'][self.iqn] = self.metadata
                        config_object.update_item("targets",
                                                  self.target_iqn,
                                                  target_config)

                        # persist the config update
                        config_object.commit()

        elif rqst_type == 'reconfigure':
            self.define_client()

        else:
            ###################################################################
            # Remove the requested client from the config object and LIO      #
            ###################################################################
            if self.exists():
                self.define_client()   # grab the client and parent tpg objects
                self.delete()          # deletes from the local LIO instance
                if self.error:
                    return
                else:
                    # remove this client from the config

                    if update_host == this_host():
                        self.logger.debug("Removing {} from the config "
                                          "object".format(self.iqn))
                        target_config['clients'].pop(self.iqn)
                        config_object.update_item("targets", self.target_iqn, target_config)
                        config_object.commit()

            else:
                # desired state is absent, but the client does not exist
                # in LIO - Nothing to do!
                self.logger.info("(main) client {} removal request, but it's"
                                 "not in LIO...skipping".format(self.iqn))

    def validate_images(self, disks_config):
        """
        Confirm that the images listed are actually allocated to the tpg and
        can therefore be used by a client
        :return: a list of images that are NOT in the tpg ... should be empty!
        """
        bad_images = []
        tpg_lun_list = self.get_images(self.tpg).keys()
        self.logger.debug("tpg images: {}".format(tpg_lun_list))
        self.logger.debug("request images: {}".format(self.requested_images))
        backstore_object_names = [disk['backstore_object_name'] for disk_id, disk
                                  in disks_config.items()
                                  if disk_id in self.requested_images]
        self.logger.debug("backstore object names: {}".format(backstore_object_names))
        for backstore_object_name in backstore_object_names:
            if backstore_object_name not in tpg_lun_list:
                bad_images.append(backstore_object_name)

        return bad_images

    @staticmethod
    def get_update_host(config):
        """
        decide which gateway host should be responsible for any config object
        updates
        :param config: configuration dict from the rados pool
        :return: a suitable gateway host that is online
        """

        ptr = 0
        potential_hosts = [host_name for host_name in config["gateways"].keys()
                           if isinstance(config["gateways"][host_name], dict)]

        # Assume the 1st element from the list is OK for now
        # TODO check the potential hosts are online/available

        return potential_hosts[ptr]

    def get_images(self, rts_object):
        """
        Funtion to return a dict of luns mapped to either a node ACL or the
        TPG, based on the passed object type
        :param rts_object: rtslib object - either NodeACL or TPG
        :return: dict indexed by image name of LUN object attributes
        """

        luns_mapped = {}

        if isinstance(rts_object, NodeACL):
            # return a dict of images assigned to this client
            for m_lun in rts_object.mapped_luns:

                key = m_lun.tpg_lun.storage_object.name
                luns_mapped[key] = {"lun_id": m_lun.mapped_lun,
                                    "mapped_lun": m_lun,
                                    "tpg_lun": m_lun.tpg_lun}

        elif isinstance(rts_object, TPG):
            # return a dict of *all* images available to this tpg
            for m_lun in rts_object.luns:

                key = m_lun.storage_object.name
                luns_mapped[key] = {"lun_id": m_lun.lun,
                                    "mapped_lun": None,
                                    "tpg_lun": m_lun}
        return luns_mapped
示例#5
0
class GWClient(object):
    """
    This class holds a representation of a client connecting to LIO
    """

    seed_metadata = {"auth": {"chap": ''}, "luns": {}, "group_name": ""}

    def __init__(self, logger, client_iqn, image_list, chap):
        """
        Instantiate an instance of an LIO client
        :param client_iqn: (str) iscsi iqn string
        :param image_list: (list) list of rbd images (pool/image) to attach
                           to this client or list of tuples (disk, lunid)
        :param chap: (str) chap credentials in the format 'user/password'
        :return:
        """

        self.iqn = client_iqn
        self.lun_lookup = {}  # only used for hostgroup based definitions
        self.requested_images = []

        # image_list is normally a list of strings (pool.image_name) but
        # group processing forces a specific lun id allocation to masked disks
        # in this scenario the image list is a tuple
        if image_list:

            if isinstance(image_list[0], tuple):
                # tuple format ('disk_name', {'lun_id': 0})...
                for disk_item in image_list:
                    disk_name = disk_item[0]
                    lun_id = disk_item[1].get('lun_id')
                    self.requested_images.append(disk_name)
                    self.lun_lookup[disk_name] = lun_id
            else:
                self.requested_images = image_list

        self.chap = chap  # parameters for auth
        self.mutual = ''
        self.tpgauth = ''
        self.metadata = {}
        self.acl = None
        self.client_luns = {}
        self.tpg = None
        self.tpg_luns = {}
        self.lun_id_list = range(256)  # available LUN ids 0..255
        self.change_count = 0

        # enable commit to the config for changes by default
        self.commit_enabled = True

        self.logger = logger
        self.current_config = {}
        self.error = False
        self.error_msg = ''

        try:
            valid_iqn = normalize_wwn(['iqn'], client_iqn)
        except RTSLibError as err:
            self.error = True
            self.error_msg = "Invalid client name for iSCSI - {}".format(err)

        # Validate the images list doesn't contain duplicate entries
        dup_images = set(
            [rbd for rbd in image_list if image_list.count(rbd) >= 2])
        if len(dup_images) > 0:
            self.error = True
            dup_string = ','.join(dup_images)
            self.error_msg = ("Client's image list contains duplicate rbd's"
                              ": {}".format(dup_string))

    def setup_luns(self):
        """
        Add the requested LUNs to the node ACL definition. The image list
        defined for the client is compared to the current runtime settings,
        resulting in new images being added, or images removed.
        """

        # first drop the current lunid's used from the candidate list
        # this allows luns to be added/removed, and new id's to occupy free lun-id
        # slots rather than simply tag on the end. In a high churn environment,
        # adding new lun(s) at highest lun +1 could lead to exhausting the
        # 255 lun limit per target
        self.client_luns = self.get_images(self.acl)
        for image_name in self.client_luns:
            lun_id = self.client_luns[image_name]['lun_id']
            self.lun_id_list.remove(lun_id)
            self.logger.debug("(Client.setup_luns) {} has id of "
                              "{}".format(image_name, lun_id))

        self.tpg_luns = self.get_images(self.tpg)
        current_map = dict(self.client_luns)

        for image in self.requested_images:
            if image in self.client_luns:
                del current_map[image]
                continue
            else:
                rc = self._add_lun(image, self.tpg_luns[image])
                if rc != 0:
                    self.error = True
                    self.error_msg = ("{} is missing from the tpg - unable "
                                      "to map".format(image))
                    self.logger.debug("(Client.setup) tpg luns "
                                      "{}".format(self.tpg_luns))
                    self.logger.error("(Client.setup) missing image '{}' from "
                                      "the tpg".format(image))
                    return

        # 'current_map' should be empty, if not the remaining images need
        # to be removed from the client
        if current_map:
            for image in current_map:
                self._del_lun_map(image)
                if self.error:
                    self.logger.error("(Client.setup) unable to delete {} from"
                                      " {}".format(self.iqn, image))
                    return

    def define_client(self):
        """
        Establish the links for this object to the corresponding ACL and TPG
        objects from LIO
        :return:
        """

        r = lio_root.RTSRoot()

        # NB. this will check all tpg's for a matching iqn
        for client in r.node_acls:
            if client.node_wwn == self.iqn:
                self.acl = client
                self.tpg = client.parent_tpg
                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 r.tpgs:
            if tpg.enable:
                self.tpg = tpg

        try:
            self.acl = NodeACL(self.tpg, self.iqn)
            # Try to detect network problems so we can kill connections
            # and cleanup before the initiator has begun recovery and
            # failed over.
            self.acl.set_attribute('dataout_timeout', '20')  # default  3
            # LIO default 30
            self.acl.set_attribute(
                'nopin_response_timeout',
                '{}'.format(settings.config.nopin_response_timeout))
            # LIO default 15
            self.acl.set_attribute('nopin_timeout',
                                   '{}'.format(settings.config.nopin_timeout))
        except RTSLibError as err:
            self.logger.error("(Client.define_client) FAILED to define "
                              "{}".format(self.iqn))
            self.logger.debug("(Client.define_client) failure msg "
                              "{}".format(err))
            self.error = True
            self.error_msg = err
        else:
            self.logger.info("(Client.define_client) {} added "
                             "successfully".format(self.iqn))
            self.change_count += 1

    def try_disable_auth(self, tpg):
        """
        Disable authentication (enable ACL mode) if this is the last CHAP user.

        LIO doesn't allow us to mix and match ACLs and auth under a tpg. We
        only allow ACL mode if there are not CHAP users.
        """

        for client in tpg.node_acls:
            if client.chap_userid or client.chap_password:
                return

        tpg.set_attribute('authentication', '0')

    def configure_auth(self, auth_type, credentials):
        """
        Attempt to configure authentication for the client
        :return:
        """

        chap_enabled = False
        if '/' in credentials:
            client_username, client_password = credentials.split('/', 1)
            chap_enabled = True
        elif not credentials:
            client_username = ''
            client_password = ''
            credentials = "/"

        self.logger.debug("configuring auth {}".format(credentials))

        if auth_type == 'chap':

            acl_credentials = "{}/{}".format(self.acl.chap_userid,
                                             self.acl.chap_password)

            # if the credentials match... nothing to do
            if credentials == acl_credentials:
                return
            else:
                # credentials defined on the ACL don't match parms passed from
                # caller so update the acl definition
                try:
                    if auth_type == 'chap':
                        self.logger.debug("Updating the ACL")
                        self.acl.chap_userid = client_username
                        self.acl.chap_password = client_password

                        new_chap = CHAP(credentials)
                        self.logger.debug(
                            "chap object set to: {},{},{},{}".format(
                                new_chap.chap_str, new_chap.user,
                                new_chap.password, new_chap.password_str))

                        new_chap.chap_str = "{}/{}".format(
                            client_username, client_password)
                        if new_chap.error:
                            self.error = True
                            self.error_msg = new_chap.error_msg
                            return

                        if chap_enabled:
                            self.tpg.set_attribute('authentication', '1')
                        else:
                            self.try_disable_auth(self.tpg)

                        self.logger.debug("Updating config object meta data")
                        self.metadata['auth']['chap'] = new_chap.chap_str

                except RTSLibError as err:
                    self.error = True
                    self.error_msg = ("Unable to configure {} authentication "
                                      "for {} - ".format(
                                          auth_type, self.iqn, err))
                    self.logger.error("Client.configure_auth) failed to set "
                                      "{} credentials for {}".format(
                                          auth_type, self.iqn))
                else:
                    self.change_count += 1

    def _add_lun(self, image, lun):
        """
        Add a given image to the client ACL
        :param image: rbd image name of the form pool/image (str)
        :param lun: rtslib lun object
        :return:
        """

        rc = 0
        # get the tpg lun to map this client to
        tpg_lun = lun['tpg_lun']

        # lunid allocated from the current config object setting, or if this is
        # a new device from the next free lun id 'position'
        if image in self.metadata['luns'].keys():
            lun_id = self.metadata['luns'][image]['lun_id']
        else:
            if image in self.lun_lookup:
                # this indicates a lun map for a group managed client
                lun_id = self.lun_lookup[image]
            else:
                lun_id = self.lun_id_list[0]  # pick lowest available lun ID

        self.logger.debug("(Client._add_lun) Adding {} to {} at "
                          "id {}".format(image, self.iqn, lun_id))

        try:
            m_lun = self.acl.mapped_lun(lun_id, tpg_lun=tpg_lun)
        except RTSLibError as err:
            self.logger.error("Client.add_lun RTSLibError for lun id {} -"
                              " {}".format(lun_id, err))
            rc = 12
        else:

            self.client_luns[image] = {
                "lun_id": lun_id,
                "mapped_lun": m_lun,
                "tpg_lun": tpg_lun
            }

            self.metadata['luns'][image] = {"lun_id": lun_id}
            self.lun_id_list.remove(lun_id)
            self.logger.info("(Client.add_lun) added image '{}' to "
                             "{}".format(image, self.iqn))
            self.change_count += 1

        return rc

    def _del_lun_map(self, image):
        """
        Delete a lun from the client's ACL
        :param image: rbd image name to remove
        :return:
        """

        lun = self.client_luns[image]['mapped_lun']
        try:
            lun.delete()
        except RTSLibError as err:
            self.error = True
            self.error_msg = err
        else:
            self.change_count += 1

            # the lun entry could have been deleted by another host, so before
            # we try and delete - make sure it's in our local copy of the
            # metadata!
            if image in self.metadata['luns']:
                del self.metadata['luns'][image]

    def delete(self):
        """
        Delete the client definition from LIO
        :return:
        """

        try:
            self.acl.delete()
            self.try_disable_auth(self.tpg)
            self.change_count += 1
            self.logger.info("(Client.delete) deleted NodeACL for "
                             "{}".format(self.iqn))
        except RTSLibError as err:
            self.error = True
            self.error_msg = "RTS NodeACL delete failure"
            self.logger.error("(Client.delete) failed to delete client {} "
                              "- error: {}".format(self.iqn, err))

    def exists(self):
        """
        This function determines whether this instances iqn is already defined
        to LIO
        :return: Boolean
        """

        r = lio_root.RTSRoot()
        client_list = [client.node_wwn for client in r.node_acls]
        return self.iqn in client_list

    def seed_config(self, config):
        """
        function to seed the config object with a new client definition
        """

        config.add_item("clients", self.iqn)
        config.update_item("clients", self.iqn, GWClient.seed_metadata)

        # persist the config update, and leave the connection to the ceph
        # object open since adding just the iqn is only the start of the
        # definition
        config.commit("retain")

    def manage(self, rqst_type, committer=None):
        """
        Manage the allocation or removal of this client
        :param rqst_type is either 'present' (try and create the nodeACL), or
        'absent' - delete the nodeACL
        :param committer is the host responsible for any commits to the
        configuration - this is not needed for Ansible management, but is used
        by the CLI->API->GWClient interaction
        """
        # Build a local object representing the rados configuration object
        config_object = Config(self.logger)
        if config_object.error:
            self.error = True
            self.error_msg = config_object.error_msg
            return

        # use current config to hold a copy of the current rados config
        # object (dict)
        self.current_config = config_object.config

        running_under_ansible = ansible_control()
        self.logger.debug("(GWClient.manage) running under ansible?"
                          " {}".format(running_under_ansible))

        if running_under_ansible:
            update_host = GWClient.get_update_host(self.current_config)
        else:
            update_host = committer

        self.logger.debug("GWClient.manage) update host to handle any config "
                          "update is {}".format(update_host))

        if rqst_type == "present":

            ###################################################################
            # Ensure the client exists in LIO                                 #
            ###################################################################

            # first look at the request to see if it matches the settings
            # already in the config object - if so this is just a rerun, or a
            # reboot so config object updates are not needed when we change
            # the LIO environment
            if self.iqn in self.current_config['clients'].keys():
                self.metadata = self.current_config['clients'][self.iqn]
                config_image_list = sorted(self.metadata['luns'].keys())

                #
                # Does the request match the current config?

                # extract the chap_str from the config object entry
                config_chap = CHAP(self.metadata['auth']['chap'])
                chap_str = config_chap.chap_str
                if config_chap.error:
                    self.error = True
                    self.error_msg = config_chap.error_msg
                    return

                if self.chap == chap_str and \
                   config_image_list == sorted(self.requested_images):
                    self.commit_enabled = False
            else:
                # requested iqn is not in the config object
                if running_under_ansible:
                    if update_host == gethostname().split('.')[0]:
                        self.seed_config(config_object)
                else:
                    # not ansible, so just run the command
                    self.seed_config(config_object)

                self.metadata = GWClient.seed_metadata

            self.logger.debug("(manage) config updates to be applied from "
                              "this host: {}".format(self.commit_enabled))

            client_exists = self.exists()
            self.define_client()
            if self.error:
                # unable to define the client!
                return

            if client_exists and self.metadata["group_name"]:
                # bypass setup_luns for existing clients that have an
                # associated host group
                pass
            else:
                # either the client didn't exist (new or boot time), or the
                # group_name is not defined so run setup_luns for this client
                bad_images = self.validate_images()
                if not bad_images:

                    self.setup_luns()
                    if self.error:
                        return
                else:
                    # request for images to map to this client that haven't
                    # been added to LIO yet!
                    self.error = True
                    self.error_msg = ("Non-existent images {} requested "
                                      "for {}".format(bad_images, self.iqn))
                    return

            # if '/' in self.chap:
            if self.chap == '':
                self.logger.warning("(main) client '{}' configured without"
                                    " security".format(self.iqn))

            self.configure_auth('chap', self.chap)
            if self.error:
                return

            # check the client object's change count, and update the config
            # object if this is the updating host
            if self.change_count > 0:

                if self.commit_enabled:

                    if update_host == gethostname().split('.')[0]:
                        # update the config object with this clients settings
                        self.logger.debug("Updating config object metadata "
                                          "for '{}'".format(self.iqn))
                        config_object.update_item("clients", self.iqn,
                                                  self.metadata)

                        # persist the config update
                        config_object.commit()

        else:
            ###################################################################
            # Remove the requested client from the config object and LIO      #
            ###################################################################
            if self.exists():
                self.define_client()  # grab the client and parent tpg objects
                self.delete()  # deletes from the local LIO instance
                if self.error:
                    return
                else:
                    # remove this client from the config

                    if update_host == gethostname().split('.')[0]:
                        self.logger.debug("Removing {} from the config "
                                          "object".format(self.iqn))
                        config_object.del_item("clients", self.iqn)
                        config_object.commit()

            else:
                # desired state is absent, but the client does not exist
                # in LIO - Nothing to do!
                self.logger.info("(main) client {} removal request, but it's"
                                 "not in LIO...skipping".format(self.iqn))

    def validate_images(self):
        """
        Confirm that the images listed are actually allocated to the tpg and
        can therefore be used by a client
        :return: a list of images that are NOT in the tpg ... should be empty!
        """
        bad_images = []
        tpg_lun_list = self.get_images(self.tpg).keys()
        self.logger.debug("tpg images: {}".format(tpg_lun_list))
        self.logger.debug("request images: {}".format(self.requested_images))
        for image in self.requested_images:
            if image not in tpg_lun_list:
                bad_images.append(image)

        return bad_images

    @staticmethod
    def get_update_host(config):
        """
        decide which gateway host should be responsible for any config object
        updates
        :param config: configuration dict from the rados pool
        :return: a suitable gateway host that is online
        """

        ptr = 0
        potential_hosts = [
            host_name for host_name in config["gateways"].keys()
            if isinstance(config["gateways"][host_name], dict)
        ]

        # Assume the 1st element from the list is OK for now
        # TODO check the potential hosts are online/available

        return potential_hosts[ptr]

    def get_images(self, rts_object):
        """
        Funtion to return a dict of luns mapped to either a node ACL or the
        TPG, based on the passed object type
        :param rts_object: rtslib object - either NodeACL or TPG
        :return: dict indexed by image name of LUN object attributes
        """

        luns_mapped = {}

        if isinstance(rts_object, NodeACL):
            # return a dict of images assigned to this client
            for m_lun in rts_object.mapped_luns:

                key = m_lun.tpg_lun.storage_object.name
                luns_mapped[key] = {
                    "lun_id": m_lun.mapped_lun,
                    "mapped_lun": m_lun,
                    "tpg_lun": m_lun.tpg_lun
                }

        elif isinstance(rts_object, TPG):
            # return a dict of *all* images available to this tpg
            for m_lun in rts_object.luns:

                key = m_lun.storage_object.name
                luns_mapped[key] = {
                    "lun_id": m_lun.lun,
                    "mapped_lun": None,
                    "tpg_lun": m_lun
                }
        return luns_mapped
示例#6
0
class Client(object):
    """
    This class holds a representation of a client connecting to LIO
    """

    supported_access_types = ['chap']

    def __init__(self, client_iqn, image_list, auth_type, credentials):
        """
        Instantiate an instance of an LIO client
        :param client_iqn: iscsi iqn string
        :param image_list: list of rbd images to attach to this client
        :param auth_type: authentication type - null or chap
        :param credentials: chap credentials in the format 'user/password'
        :return:
        """

        self.iqn = client_iqn
        self.requested_images = image_list
        self.auth_type = auth_type              # auth ... '' or chap
        self.credentials = credentials          # parameters for auth
        self.acl = None
        self.error = False
        self.error_msg = ''
        self.client_luns = {}
        self.tpg = None
        self.tpg_luns = {}
        self.lun_id_list = range(256)           # available LUN ids 0..255
        self.change_count = 0

    def setup_luns(self):
        """
        Add the requested LUNs to the node ACL definition. The image list defined for the
        client is compared to the current runtime settings, resulting in new images being
        added, or images removed.
        """

        self.client_luns = get_images(self.acl)
        for image_name in self.client_luns:
            lun_id = self.client_luns[image_name]['lun_id']
            self.lun_id_list.remove(lun_id)
            logger.debug("(Client.setup_luns) {} has id of {}".format(image_name, lun_id))

        self.tpg_luns = get_images(self.tpg)
        current_map = dict(self.client_luns)

        for image in self.requested_images:
            if image in self.client_luns:
                del current_map[image]
                continue
            else:
                rc = self._add_lun(image, self.tpg_luns[image])
                if rc != 0:
                    self.error = True
                    self.error_msg = "{} is missing from the tpg - unable to map".format(image)
                    logger.debug("(Client.setup) tpg luns {}".format(self.tpg_luns))
                    logger.error("(Client.setup) missing image '{}' from the tpg".format(image))
                    return

        # 'current_map' should be empty, if not the remaining images need to be removed
        # from the client
        if current_map:
            for image in current_map:
                self._del_lun_map(image)
                if self.error:
                    logger.error("(Client.setup) unable to delete {} from {}".format(self.iqn,
                                                                                     image))
                    return

    def define_client(self):
        """
        Establish the links for this object to the corresponding ACL and TPG objects from LIO
        :return:
        """

        r = lio_root.RTSRoot()

        # NB. this will check all tpg's for a matching iqn
        for client in r.node_acls:
            if client.node_wwn == self.iqn:
                self.acl = client
                self.tpg = client.parent_tpg
                logger.debug("(Client.define_client) - {} already defined".format(self.iqn))
                return

        # at this point the client does not exist, so create it
        # NB. The solution supports only a single tpg definition, so simply grabbing the
        # first tpg is fine. If multiple tpgs are required this will need more work
        self.tpg = r.tpgs.next()

        try:
            self.acl = NodeACL(self.tpg, self.iqn)
        except RTSLibError as err:
            logger.error("(Client.define_client) FAILED to define {}".format(self.iqn))
            logger.debug("(Client.define_client) failure msg {}".format(err))
            self.error = True
            self.error_msg = err
        else:
            self.change_count += 1
            logger.info("(Client.define_client) {} added successfully".format(self.iqn))

    def configure_auth(self):
        """
        Attempt to configure authentication for the client, given the credentials provided
        :return:
        """

        try:
            client_username, client_password = self.credentials.split('/')

            if self.acl.chap_userid == '' or self.acl.chap_userid != client_username:
                self.acl.chap_userid = client_username
                logger.info("(Client.configure_auth) chap user name changed for {}".format(self.iqn))
                self.change_count += 1
            if self.acl.chap_password == '' or self.acl.chap_password != client_password:
                self.acl.chap_password = client_password
                logger.info("(Client.configure_auth) chap password changed for {}".format(self.iqn))

        except RTSLibError as err:
            self.error = True
            self.error_msg = "Unable to (re)configure chap - ".format(err)
            logger.error("Client.configure_auth) failed to set credentials on node")

    def _add_lun(self, image, lun):
        """
        Add a given image to the client ACL
        :param image: rbd image name (str)
        :param lun: rtslib lun object
        :return:
        """

        rc = 0
        # get the tpg lun to map this client to
        tpg_lun = lun['tpg_lun']
        lun_id = self.lun_id_list[0]        # pick the lowest available lun ID
        logger.debug("(Client._add_lun) Adding {} to {} at id {}".format(image, self.iqn, lun_id))
        try:
            m_lun = self.acl.mapped_lun(lun_id, tpg_lun=tpg_lun)
            self.client_luns[image] = {"lun_id": lun_id,
                                       "mapped_lun": m_lun,
                                       "tpg_lun": tpg_lun}
            self.lun_id_list.remove(lun_id)
            logger.info("(Client.add_lun) added image '{}' to {}".format(image, self.iqn))
            self.change_count += 1

        except RTSLibError as err:
            logger.error("Client.add_lun RTSLibError for lun id {} - {}".format(lun_id, err))
            rc = 12

        return rc

    def _del_lun_map(self, image):
        """
        Delete a lun from the client's ACL
        :param image: rbd image name to remove
        :return:
        """

        lun = self.client_luns[image]['mapped_lun']
        try:
            lun.delete()
            self.change_count += 1
        except RTSLibError as err:
            self.error = True
            self.error_msg = err

    def delete(self):
        """
        Delete the client definition from LIO
        :return:
        """

        try:
            self.acl.delete()
            self.change_count += 1
            logger.info("(Client.delete) deleted NodeACL for {}".format(self.iqn))
        except RTSLibError as err:
            self.error = True
            self.error_msg = "RTS NodeACL delete failure"
            logger.error("(Client.delete) failed to delete client {} - error: {}".format(self.iqn,
                                                                                         err))

    def exists(self):
        """
        This function determines whether this instances iqn is already defined to LIO
        :return: Boolean
        """

        r = lio_root.RTSRoot()
        client_list = [client.node_wwn for client in r.node_acls]
        return self.iqn in client_list