コード例 #1
0
ファイル: lun.py プロジェクト: Fubo1/ceph-iscsi-config
class LUN(object):
    def __init__(self, logger, pool, image, size, allocating_host):
        self.logger = logger
        self.image = image
        self.pool = pool
        self.pool_id = 0
        self.size = size
        self.config_key = '{}.{}'.format(self.pool, self.image)

        # the allocating host could be fqdn or shortname - but the config
        # only uses shortname so it needs to be converted to shortname format
        self.allocating_host = allocating_host.split('.')[0]

        self.owner = ''  # gateway host that owns the preferred path for this LUN
        self.error = False
        self.error_msg = ''
        self.num_changes = 0
        self.dm_device = ''  # e.g. /dev/mapper/0-58f8b515f007c

        self.config = Config(logger)
        if self.config.error:
            self.error = self.config.error
            self.error_msg = self.config.error_msg
            return

        self._validate_request()

    def _validate_request(self):

        # Before we start make sure that the target host is actually defined to the config
        if self.allocating_host not in self.config.config['gateways'].keys():
            self.logger.critical(
                "Owning host is not valid, please provide a valid gateway name for this rbd image"
            )
            self.error = True
            self.error_msg = (
                "host name given for {} is not a valid gateway name, "
                "listed in the config".format(self.image))
        elif not rados_pool(pool=self.pool):
            # Could create the pool, but a fat finger moment in the config file would mean rbd images
            # get created and mapped, and then need correcting. Better to exit if the pool doesn't exist
            self.error = True
            self.error_msg = "Pool '{}' does not exist. Unable to continue".format(
                self.pool)

    @staticmethod
    def remove_dm_device(dm_path):
        dm_name = os.path.basename(dm_path)
        resp = shellcommand('multipath -f {}'.format(dm_name))

        return False if resp else True

    def remove_lun(self):

        this_host = gethostname().split('.')[0]
        self.logger.info("LUN deletion request received, rbd removal to be "
                         "performed by {}".format(self.allocating_host))

        # First ensure the LUN is not allocated to a client
        clients = self.config.config['clients']
        lun_in_use = False
        for iqn in clients:
            client_luns = clients[iqn]['luns'].keys()
            if self.config_key in client_luns:
                lun_in_use = True
                break

        if lun_in_use:
            # this will fail the ansible task for this lun/host
            self.error = True
            self.error_msg = "Unable to delete {} - allocated to {}".format(
                self.config_key, iqn)
            self.logger.warning(self.error_msg)
            return

        # Check that the LUN is in LIO - if not there is nothing to do for this request
        lun = self.lun_in_lio()
        if not lun:
            return

        # Now we know the request is for a LUN in LIO, and it's not masked to a client
        self.remove_dev_from_lio()
        if self.error:
            return

        rbd_image = RBDDev(self.image, '0G', self.pool)
        rbd_image.get_rbd_map()

        dm_path = LUN.dm_device_name_from_rbd_map(rbd_image.rbd_map)
        if LUN.remove_dm_device(dm_path):

            rbd_image.unmap_rbd()
            if rbd_image.error:
                self.error = True
                self.error_msg = "Unable to unmap {} from host".format(
                    self.config_key)
                self.logger.error(self.error_msg)
                return

            self.num_changes += 1

            if this_host == self.allocating_host:
                # by using the allocating host we ensure the delete is not
                # issue by several hosts when initiated through ansible
                rbd_image.delete_rbd()
                if rbd_image.error:
                    self.error = True
                    self.error_msg = "Unable to delete the underlying rbd image {}".format(
                        self.config_key)
                    return

                # remove the definition from the config object
                self.config.del_item('disks', self.config_key)
                self.config.commit()

        else:
            self.error = True
            self.error_msg = "Unable to remove dm device for {}".format(
                self.config_key)
            self.logger.error(self.error_msg)
            return

    def manage(self, desired_state):

        self.logger.debug("lun.manage request for {}, desired state {}".format(
            self.image, desired_state))

        if desired_state == 'present':

            self.allocate()

        elif desired_state == 'absent':

            self.remove_lun()

    def allocate(self):
        self.logger.debug(
            "LUN.allocate starting, getting a list of rbd devices")
        disk_list = RBDDev.rbd_list(pool=self.pool)
        self.logger.debug("rados pool '{}' contains the following - {}".format(
            self.pool, disk_list))
        this_host = gethostname().split('.')[0]
        self.logger.debug("Hostname Check - this host is {}, target host for "
                          "allocations is {}".format(this_host,
                                                     self.allocating_host))
        rbd_image = RBDDev(self.image, self.size, self.pool)
        self.pool_id = rbd_image.pool_id

        # if the image required isn't defined, create it!
        if self.image not in disk_list:
            # create the requested disk if this is the 'owning' host
            if this_host == self.allocating_host:  # is_this_host(target_host):

                rbd_image.create()

                if not rbd_image.error:
                    self.config.add_item('disks', self.config_key)
                    self.logger.info(
                        "(LUN.allocate) created {}/{} successfully".format(
                            self.pool, self.image))
                    self.num_changes += 1
                else:
                    self.error = True
                    self.error_msg = rbd_image.error_msg
                    return

            else:
                # the image isn't there, and this isn't the 'owning' host
                # so wait until the disk arrives
                waiting = 0
                while self.image not in disk_list:
                    sleep(settings.config.loop_delay)
                    disk_list = RBDDev.rbd_list(pool=self.pool)
                    waiting += settings.config.loop_delay
                    if waiting >= settings.config.time_out:
                        self.error = True
                        self.error_msg = "(LUN.allocate) timed out waiting for rbd to show up"
                        return
        else:
            # requested image is defined to ceph, so ensure it's in the config
            if self.config_key not in self.config.config['disks']:
                self.config.add_item('disks', self.config_key)

        # make sure the rbd is mapped. this will also ensure the
        # RBDDEV object will hold a valid dm_device attribute
        rbd_image.get_rbd_map()
        if rbd_image.map_needed:
            self.num_changes += 1

        self.logger.debug("Check the rbd image size matches the request")

        # if updates_made is not set, the disk pre-exists so on the owning host see if it needs to be resized
        if self.num_changes == 0 and this_host == self.allocating_host:  # is_this_host(target_host):

            # check the size, and update if needed
            rbd_image.rbd_size()
            if rbd_image.error:
                self.logger.critical(rbd_image.error_msg)
                self.error = True
                self.error_msg = rbd_image.error_msg
                return

            if rbd_image.changed:
                self.logger.info("rbd image {} resized to {}".format(
                    self.image, self.size))
                self.num_changes += 1
            else:
                self.logger.debug(
                    "rbd image {} size matches the configuration file request".
                    format(self.image))

        # for LIO mapping purposes, we use the device mapper device not the raw /dev/rbdX device
        # Using the dm device ensures that any connectivity issue doesn't result in stale device
        # structures in the kernel, since device-mapper will tidy those up
        self.dm_get_device(rbd_image.rbd_map)
        if self.dm_device is None:
            self.logger.critical(
                "Could not find dm multipath device for {}. Make sure the multipathd"
                " service is enabled, and confirm entry is in /dev/mapper/".
                format(self.image))
            self.error = True
            self.error_msg = "Could not find dm multipath device for {}".format(
                self.image)
            return

        # ensure the dm device size matches the request size
        if not self.dm_size_ok(rbd_image):
            self.error = True
            self.error_msg = "Unable to sync the dm device to the parent rbd size - {}".format(
                self.image)
            self.logger.critical(self.error_msg)
            return

        self.logger.debug("Begin processing LIO mapping requirement")

        self.logger.debug("(LUN.allocate) {} is mapped to {}.".format(
            self.image, self.dm_device))

        # check this rbd image is in the /etc/ceph/rbdmap file
        if rbd_image.rbdmap_entry():
            self.logger.debug(
                '(LUN.allocate) Entry added to /etc/ceph/rbdmap for {}/{}'.
                format(self.pool, self.image))
            self.num_changes += 1

        # now see if we need to add this rbd image to LIO
        lun = self.lun_in_lio()

        if not lun:

            # this image has not been defined to this hosts LIO, so check the config for the details and
            # if it's  missing define the wwn/alua_state and update the config
            if this_host == self.allocating_host:
                # first check to see if the device needs adding
                try:
                    wwn = self.config.config['disks'][self.config_key]['wwn']
                except KeyError:
                    wwn = ''

                if wwn == '':
                    # disk hasn't been defined to LIO yet, it' not been defined to the config yet
                    # and this is the allocating host
                    lun = self.add_dev_to_lio()
                    if self.error:
                        return

                    # lun is now in LIO, time for some housekeeping :P
                    wwn = lun._get_wwn()
                    self.owner = LUN.set_owner(self.config.config['gateways'])
                    self.logger.debug("Owner for {} will be {}".format(
                        self.image, self.owner))

                    disk_attr = {
                        "wwn": wwn,
                        "image": self.image,
                        "owner": self.owner,
                        "pool": self.pool,
                        "pool_id": rbd_image.pool_id,
                        "dm_device": self.dm_device
                    }

                    self.config.update_item('disks', self.config_key,
                                            disk_attr)

                    gateway_dict = self.config.config['gateways'][self.owner]
                    gateway_dict['active_luns'] += 1

                    self.config.update_item('gateways', self.owner,
                                            gateway_dict)

                    self.logger.debug(
                        "(LUN.allocate) registered '{}' with wwn '{}' with the"
                        " config object".format(self.image, wwn))
                    self.logger.info(
                        "(LUN.allocate) added '{}/{}' to LIO and config object"
                        .format(self.pool, self.image))

                else:
                    # config object already had wwn for this rbd image
                    lun = self.add_dev_to_lio(wwn)
                    if self.error:
                        return
                    self.logger.debug(
                        "(LUN.allocate) registered '{}' to LIO with wwn '{}' from "
                        "the config object".format(self.image, wwn))

                self.num_changes += 1

            else:
                # lun is not already in LIO, but this is not the owning node that defines the wwn
                # we need the wwn from the config (placed by the allocating host), so we wait!
                waiting = 0
                while waiting < settings.config.time_out:
                    self.config.refresh()
                    if self.config_key in self.config.config['disks']:
                        if 'wwn' in self.config.config['disks'][
                                self.config_key]:
                            if self.config.config['disks'][
                                    self.config_key]['wwn']:
                                wwn = self.config.config['disks'][
                                    self.config_key]['wwn']
                                break
                    sleep(settings.config.loop_delay)
                    waiting += settings.config.loop_delay
                    self.logger.debug(
                        "(LUN.allocate) waiting for config object to show {}"
                        " with it's wwn".format(self.image))

                if waiting >= settings.config.time_out:
                    self.error = True
                    self.error_msg = (
                        "(LUN.allocate) waited too long for the wwn information "
                        "on image {} to arrive".format(self.image))
                    return

                # At this point we have a wwn from the config for this rbd image, so just add to LIO
                lun = self.add_dev_to_lio(wwn)
                if self.error:
                    return

                self.logger.info(
                    "(LUN.allocate) added {} to LIO using wwn '{}'"
                    " defined by {}".format(self.image, wwn,
                                            self.allocating_host))

                self.num_changes += 1

        self.logger.debug("config meta data for this disk is {}".format(
            self.config.config['disks'][self.config_key]))

        # the owning host for an image is the only host that commits to the config
        if this_host == self.allocating_host and self.config.changed:

            self.logger.debug(
                "(LUN.allocate) Committing change(s) to the config object in pool {}"
                .format(self.pool))
            self.config.commit()
            self.error = self.config.error
            self.error_msg = self.config.error_msg

    def dm_get_device(self, map_device):
        """
        set the dm_device attribute based on the rbd map device entry
        :param map_device: /dev/rbdX
        :return: None
        """

        self.dm_device = LUN.dm_device_name_from_rbd_map(map_device)
        if self.dm_device is None:
            return

        if not LUN.dm_wait_for_device(self.dm_device):
            self.dm_device = None

    def dm_size_ok(self, rbd_object):
        """
        Check that the dm device matches the request. if the size request is lower than
        current size, just return since resizing down is not support and problematic
        for client filesystems anyway
        :return boolean indicating whether the size matches
        """

        target_bytes = convert_2_bytes(self.size)
        if rbd_object.size_bytes > target_bytes:
            return True

        tmr = 0
        size_ok = False
        rbd_size_ok = False
        dm_path_found = False

        # we have to wait for the rbd size to match, since the rbd could have been
        # resized on another gateway host when this is called from Ansible
        while tmr < settings.config.time_out:
            if rbd_object.size_bytes == target_bytes:
                rbd_size_ok = True
                break
            sleep(settings.config.loop_delay)
            tmr += settings.config.loop_delay

        # since the size matches underneath device mapper, now we ensure the size
        # matches with device mapper - if not issue a resize map request
        if rbd_size_ok:

            # find the dm-X device
            dm_devices = glob.glob('/sys/class/block/dm-*/')
            # convert the full dm_device path to just the name (last component of path
            dm_name = os.path.basename(self.dm_device)

            for dm_dev in dm_devices:
                if fread(os.path.join(dm_dev, 'dm/name')) == dm_name:
                    dm_path_found = True
                    break

            if dm_path_found:

                # size is in sectors, so read it and * 512 = bytes
                dm_size_bytes = int(fread(os.path.join(dm_dev, 'size'))) * 512
                if dm_size_bytes != target_bytes:

                    self.logger.info(
                        "Issuing a resize map for {}".format(dm_name))
                    response = shellcommand(
                        'multipathd resize map {}'.format(dm_name))

                    self.logger.debug("resize result : {}".format(response))
                    dm_size_bytes = int(fread(os.path.join(dm_dev,
                                                           'size'))) * 512

                    if response.lower().startswith(
                            'ok') and dm_size_bytes == target_bytes:
                        size_ok = True
                    else:
                        self.logger.critical(
                            "multipathd resize map for {} failed".format(
                                dm_name))
                else:
                    # size matches
                    size_ok = True
            else:
                self.logger.critical(
                    "Unable to locate a dm-X device for this rbd image - {}".
                    format(self.image))

        return size_ok

    def lun_in_lio(self):
        found_it = False
        rtsroot = root.RTSRoot()
        for stg_object in rtsroot.storage_objects:

            # First match on name, but then check the pool incase the same name exists in multiple pools
            if stg_object.name == self.config_key:

                found_it = True
                break

        return stg_object if found_it else None

    def add_dev_to_lio(self, in_wwn=None):
        """
        Add an rbd device to the LIO configuration
        :param in_wwn: optional wwn identifying the rbd image to clients - must match across gateways
        :return: LIO LUN object
        """

        self.logger.info(
            "(LUN.add_dev_to_lio) Adding image '{}' with path {} to LIO".
            format(self.image, self.dm_device))
        new_lun = None
        try:
            new_lun = BlockStorageObject(name=self.config_key,
                                         dev=self.dm_device,
                                         wwn=in_wwn)
        except RTSLibError as err:
            self.error = True
            self.error_msg = "failed to add {} to LIO - error({})".format(
                self.image, str(err))

        return new_lun

    def remove_dev_from_lio(self):
        lio_root = root.RTSRoot()

        # remove the device from all tpgs
        for t in lio_root.tpgs:
            for lun in t.luns:
                if lun.storage_object.name == self.config_key:
                    try:
                        lun.delete()
                    except RTSLibError as e:
                        self.error = True
                        self.error_msg = "Delete from LIO/TPG failed - {}".format(
                            e)
                        return
                    else:
                        break  # continue to the next tpg

        for stg_object in lio_root.storage_objects:
            if stg_object.name == self.config_key:

                alua_dir = os.path.join(stg_object.path, "alua")

                # remove the alua directories (future versions will handle this
                # natively within rtslib_fb
                for dirname in next(os.walk(alua_dir))[1]:
                    if dirname != "default_tg_pt_gp":
                        try:
                            alua_tpg = ALUATargetPortGroup(stg_object, dirname)
                            alua_tpg.delete()
                        except (RTSLibError, RTSLibNotInCFS) as err:
                            self.error = True
                            self.error_msg = "Delete of ALUA directories failed - {}".format(
                                err)
                            return

                try:
                    stg_object.delete()
                except RTSLibError as e:
                    self.error = True
                    self.error_msg = "Delete from LIO/backstores failed - {}".format(
                        e)
                    return

                break

    @staticmethod
    def set_owner(gateways):
        """
        Determine the gateway in the configuration with the lowest number of active LUNs. This
        gateway is then selected as the owner for the primary path of the current LUN being
        processed
        :param gateways: gateway dict returned from the RADOS configuration object
        :return: specific gateway hostname (str) that should provide the active path for the next LUN
        """

        # Gateways contains simple attributes and dicts. The dicts define the gateways settings, so
        # first we extract only the dicts within the main gateways dict
        gw_nodes = {
            key: gateways[key]
            for key in gateways if isinstance(gateways[key], dict)
        }
        gw_items = gw_nodes.items()

        # first entry is the lowest number of active_luns
        gw_items.sort(key=lambda x: (x[1]['active_luns']))

        # 1st tuple is gw with lowest active_luns, so return the 1st
        # element which is the hostname
        return gw_items[0][0]

    @staticmethod
    def dm_device_name_from_rbd_map(map_device):
        """
        take a mapped device name /dev/rbdX to determine the /dev/mapper/X
        equivalent by reading the devices attribute files in sysfs
        :param map_device: device path of the form /dev/rbdX
        :return: device mapper name for the rbd device /dev/mapper/<pool>-<image-id>
        """

        rbd_bus_id = map_device[8:]
        dm_uid = None

        # TODO - could fread encounter an IOerror?
        rbd_path = os.path.join('/sys/bus/rbd/devices', rbd_bus_id)
        if os.path.exists(rbd_path):
            pool_id = fread(os.path.join(rbd_path, "pool_id"))
            image_id = fread(os.path.join(rbd_path, "image_id"))
            current_snap = fread(os.path.join(rbd_path, "current_snap"))

            dm_uid = "/dev/mapper/{}-{}".format(pool_id, image_id)
            if current_snap != "-":
                dm_uid += "-{}".format(fread(os.path.join(rbd_path,
                                                          "snap_id")))

        return dm_uid

    @staticmethod
    def dm_wait_for_device(dm_device):
        """
        multipath may take a few seconds for the device to appear, so we
        need to wait until we see it - but use a timeout to abort if necessary
        :param dm_device: dm device name /dev/mapper/<pool>-<image_id>
        :return boolean representing when the device has been found
        """

        waiting = 0

        # wait for multipathd and udev to setup /dev node
        # /dev/mapper/<pool_id>-<rbd_image_id>
        # e.g. /dev/mapper/0-519d42ae8944a
        while os.path.exists(dm_device) is False:
            sleep(settings.config.loop_delay)
            waiting += settings.config.loop_delay
            if waiting >= settings.config.time_out:
                break

        return os.path.exists(dm_device)
コード例 #2
0
ファイル: lun.py プロジェクト: deathowl/ceph-iscsi-config
class LUN(object):
    def __init__(self, logger, pool, image, size, allocating_host):
        self.logger = logger
        self.image = image
        self.pool = pool
        self.pool_id = 0
        self.size = size
        self.size_bytes = convert_2_bytes(size)
        self.config_key = '{}.{}'.format(self.pool, self.image)
        self.controls = {}

        # the allocating host could be fqdn or shortname - but the config
        # only uses shortname so it needs to be converted to shortname format
        self.allocating_host = allocating_host.split('.')[0]

        self.owner = ''  # gateway that owns the preferred path for this LUN
        self.error = False
        self.error_msg = ''
        self.num_changes = 0

        self.config = Config(logger)
        if self.config.error:
            self.error = self.config.error
            self.error_msg = self.config.error_msg
            return

        self._validate_request()
        if self.config_key in self.config.config['disks']:
            self.controls = self.config.config['disks'][self.config_key].get(
                'controls', {}).copy()

    def _get_max_data_area_mb(self):
        max_data_area_mb = self.controls.get('max_data_area_mb', None)
        if max_data_area_mb is None:
            return settings.config.max_data_area_mb
        return max_data_area_mb

    def _set_max_data_area_mb(self, value):
        if value is None or str(value) == str(
                settings.config.max_data_area_mb):
            self.controls.pop('max_data_area_mb', None)
        else:
            self.controls['max_data_area_mb'] = value

    max_data_area_mb = property(_get_max_data_area_mb,
                                _set_max_data_area_mb,
                                doc="get/set kernel data area (MiB)")

    def _validate_request(self):

        # Before we start make sure that the target host is actually
        # defined to the config
        if self.allocating_host not in self.config.config['gateways'].keys():
            self.logger.critical("Owning host is not valid, please provide a "
                                 "valid gateway name for this rbd image")
            self.error = True
            self.error_msg = ("host name given for {} is not a valid gateway"
                              " name, listed in the config".format(self.image))

        elif not rados_pool(pool=self.pool):
            # Could create the pool, but a fat finger moment in the config
            # file would mean rbd images get created and mapped, and then need
            # correcting. Better to exit if the pool doesn't exist
            self.error = True
            self.error_msg = ("Pool '{}' does not exist. Unable to "
                              "continue".format(self.pool))

    def remove_lun(self):

        this_host = gethostname().split('.')[0]
        self.logger.info("LUN deletion request received, rbd removal to be "
                         "performed by {}".format(self.allocating_host))

        # First ensure the LUN is not allocated to a client
        clients = self.config.config['clients']
        lun_in_use = False
        for iqn in clients:
            client_luns = clients[iqn]['luns'].keys()
            if self.config_key in client_luns:
                lun_in_use = True
                break

        if lun_in_use:
            # this will fail the ansible task for this lun/host
            self.error = True
            self.error_msg = ("Unable to delete {} - allocated to "
                              "{}".format(self.config_key, iqn))

            self.logger.warning(self.error_msg)
            return

        # Check that the LUN is in LIO - if not there is nothing to do for
        # this request
        lun = self.lio_stg_object()
        if not lun:
            return

        # Now we know the request is for a LUN in LIO, and it's not masked
        # to a client
        self.remove_dev_from_lio()
        if self.error:
            return

        rbd_image = RBDDev(self.image, '0G', self.pool)

        if this_host == self.allocating_host:
            # by using the allocating host we ensure the delete is not
            # issue by several hosts when initiated through ansible
            rbd_image.delete()
            if rbd_image.error:
                self.error = True
                self.error_msg = ("Unable to delete the underlying rbd "
                                  "image {}".format(self.config_key))
                return

            # determine which host was the path owner
            disk_owner = self.config.config['disks'][self.config_key]['owner']

            #
            # remove the definition from the config object
            self.config.del_item('disks', self.config_key)

            # update the active_luns count for gateway that owned this
            # lun
            gw_metadata = self.config.config['gateways'][disk_owner]
            if gw_metadata['active_luns'] > 0:
                gw_metadata['active_luns'] -= 1

                self.config.update_item('gateways', disk_owner, gw_metadata)

            self.config.commit()

    def manage(self, desired_state):

        self.logger.debug("LUN.manage request for {}, desired state "
                          "{}".format(self.image, desired_state))

        if desired_state == 'present':

            self.allocate()

        elif desired_state == 'absent':

            self.remove_lun()

    def allocate(self):
        self.logger.debug("LUN.allocate starting, listing rbd devices")
        disk_list = RBDDev.rbd_list(pool=self.pool)
        self.logger.debug("rados pool '{}' contains the following - "
                          "{}".format(self.pool, disk_list))

        this_host = gethostname().split('.')[0]
        self.logger.debug("Hostname Check - this host is {}, target host for "
                          "allocations is {}".format(this_host,
                                                     self.allocating_host))

        rbd_image = RBDDev(self.image, self.size, self.pool)
        self.pool_id = rbd_image.pool_id

        # if the image required isn't defined, create it!
        if self.image not in disk_list:
            # create the requested disk if this is the 'owning' host
            if this_host == self.allocating_host:

                rbd_image.create()

                if not rbd_image.error:
                    self.config.add_item('disks', self.config_key)
                    self.logger.info("(LUN.allocate) created {}/{} "
                                     "successfully".format(
                                         self.pool, self.image))
                    self.num_changes += 1
                else:
                    self.error = True
                    self.error_msg = rbd_image.error_msg
                    return

            else:
                # the image isn't there, and this isn't the 'owning' host
                # so wait until the disk arrives
                waiting = 0
                while self.image not in disk_list:
                    sleep(settings.config.loop_delay)
                    disk_list = RBDDev.rbd_list(pool=self.pool)
                    waiting += settings.config.loop_delay
                    if waiting >= settings.config.time_out:
                        self.error = True
                        self.error_msg = ("(LUN.allocate) timed out waiting "
                                          "for rbd to show up")
                        return
        else:
            # requested image is already defined to ceph

            if rbd_image.valid:
                # rbd image is OK to use, so ensure it's in the config
                # object
                if self.config_key not in self.config.config['disks']:
                    self.config.add_item('disks', self.config_key)

            else:
                # rbd image is not valid for export, so abort
                self.error = True
                self.error_msg = ("(LUN.allocate) rbd '{}' is not compatible "
                                  "with LIO\nOnly image features {} are"
                                  " supported".format(
                                      self.image,
                                      ','.join(RBDDev.rbd_feature_list)))
                self.logger.error(self.error_msg)
                return

        self.logger.debug("Check the rbd image size matches the request")

        # if updates_made is not set, the disk pre-exists so on the owning
        # host see if it needs to be resized
        if self.num_changes == 0 and this_host == self.allocating_host:

            # check the size, and update if needed
            rbd_image.rbd_size()
            if rbd_image.error:
                self.logger.critical(rbd_image.error_msg)
                self.error = True
                self.error_msg = rbd_image.error_msg
                return

            if rbd_image.changed:
                self.logger.info("rbd image {} resized "
                                 "to {}".format(self.config_key, self.size))
                self.num_changes += 1
            else:
                self.logger.debug("rbd image {} size matches the configuration"
                                  " file request".format(self.config_key))

        self.logger.debug("Begin processing LIO mapping")

        # now see if we need to add this rbd image to LIO
        so = self.lio_stg_object()
        if not so:

            # this image has not been defined to this hosts LIO, so check the
            # config for the details and if it's  missing define the
            # wwn/alua_state and update the config
            if this_host == self.allocating_host:
                # first check to see if the device needs adding
                try:
                    wwn = self.config.config['disks'][self.config_key]['wwn']
                except KeyError:
                    wwn = ''

                if wwn == '':
                    # disk hasn't been defined to LIO yet, it' not been defined
                    # to the config yet and this is the allocating host
                    lun = self.add_dev_to_lio()
                    if self.error:
                        return

                    # lun is now in LIO, time for some housekeeping :P
                    wwn = lun._get_wwn()
                    self.owner = LUN.set_owner(self.config.config['gateways'])
                    self.logger.debug("{} owner will be {}".format(
                        self.image, self.owner))

                    disk_attr = {
                        "wwn": wwn,
                        "image": self.image,
                        "owner": self.owner,
                        "pool": self.pool,
                        "pool_id": rbd_image.pool_id,
                        "controls": self.controls
                    }

                    self.config.update_item('disks', self.config_key,
                                            disk_attr)

                    gateway_dict = self.config.config['gateways'][self.owner]
                    gateway_dict['active_luns'] += 1

                    self.config.update_item('gateways', self.owner,
                                            gateway_dict)

                    self.logger.debug("(LUN.allocate) registered '{}' with "
                                      "wwn '{}' with the config "
                                      "object".format(self.image, wwn))
                    self.logger.info("(LUN.allocate) added '{}/{}' to LIO and"
                                     " config object".format(
                                         self.pool, self.image))

                else:
                    # config object already had wwn for this rbd image
                    self.add_dev_to_lio(wwn)
                    if self.error:
                        return

                    # delete/update on-disk attributes
                    disk_attr = self.config.config['disks'][self.config_key]
                    if self.controls != disk_attr.get('controls', {}):
                        disk_attr['controls'] = self.controls
                        self.config.update_item('disks', self.config_key,
                                                disk_attr)

                    self.logger.debug("(LUN.allocate) registered '{}' to LIO "
                                      "with wwn '{}' from the config "
                                      "object".format(self.image, wwn))

                self.num_changes += 1

            else:
                # lun is not already in LIO, but this is not the owning node
                # that defines the wwn we need the wwn from the config
                # (placed by the allocating host), so we wait!
                waiting = 0
                while waiting < settings.config.time_out:
                    self.config.refresh()
                    if self.config_key in self.config.config['disks']:
                        if 'wwn' in self.config.config['disks'][
                                self.config_key]:
                            if self.config.config['disks'][
                                    self.config_key]['wwn']:
                                wwn = self.config.config['disks'][
                                    self.config_key]['wwn']
                                break
                    sleep(settings.config.loop_delay)
                    waiting += settings.config.loop_delay
                    self.logger.debug(
                        "(LUN.allocate) waiting for config object"
                        " to show {} with it's wwn".format(self.image))

                if waiting >= settings.config.time_out:
                    self.error = True
                    self.error_msg = ("(LUN.allocate) waited too long for the "
                                      "wwn information on image {} to "
                                      "arrive".format(self.image))
                    return

                # At this point we have a wwn from the config for this rbd
                # image, so just add to LIO
                self.add_dev_to_lio(wwn)
                if self.error:
                    return

                self.logger.info("(LUN.allocate) added {} to LIO using wwn "
                                 "'{}' defined by {}".format(
                                     self.image, wwn, self.allocating_host))

                self.num_changes += 1

        else:
            # lun exists in LIO, check the size is correct
            if not self.lio_size_ok(rbd_image, so):
                self.error = True
                self.error_msg = "Unable to sync the rbd device size with LIO"
                self.logger.critical(self.error_msg)
                return

        self.logger.debug("config meta data for this disk is "
                          "{}".format(
                              self.config.config['disks'][self.config_key]))

        # the owning host for an image is the only host that commits to the
        # config
        if this_host == self.allocating_host and self.config.changed:

            self.logger.debug("(LUN.allocate) Committing change(s) to the "
                              "config object in pool {}".format(self.pool))
            self.config.commit()
            self.error = self.config.error
            self.error_msg = self.config.error_msg

    def lio_size_ok(self, rbd_object, stg_object):
        """
        Check that the SO in LIO matches the current size of the rbd. if the
        size requested < current size, just return. Downsizing an rbd is not
        supported by this code and problematic for client filesystems anyway!
        :return boolean indicating whether the size matches
        """

        tmr = 0
        size_ok = False
        rbd_size_ok = False
        # dm_path_found = False

        # We have to wait for the rbd size to match, since the rbd could have
        # been resized on another gateway host
        while tmr < settings.config.time_out:
            if self.size_bytes <= rbd_object.current_size:
                rbd_size_ok = True
                break
            sleep(settings.config.loop_delay)
            tmr += settings.config.loop_delay

        # we have the right size for the rbd - check that LIO dev size matches
        if rbd_size_ok:

            # If the LIO size is not right, poke it with the new value
            if stg_object.size < self.size_bytes:
                self.logger.info("Resizing {} in LIO "
                                 "to {}".format(self.config_key,
                                                self.size_bytes))

                stg_object.set_attribute("dev_size", self.size_bytes)

                size_ok = stg_object.size == self.size_bytes

            else:
                size_ok = True

        return size_ok

    def lio_stg_object(self):
        found_it = False
        rtsroot = root.RTSRoot()
        for stg_object in rtsroot.storage_objects:

            # First match on name, but then check the pool incase the same
            # name exists in multiple pools
            if stg_object.name == self.config_key:

                found_it = True
                break

        return stg_object if found_it else None

    def add_dev_to_lio(self, in_wwn=None):
        """
        Add an rbd device to the LIO configuration
        :param in_wwn: optional wwn identifying the rbd image to clients
        (must match across gateways)
        :return: LIO LUN object
        """
        self.logger.info("(LUN.add_dev_to_lio) Adding image "
                         "'{}' to LIO".format(self.config_key))

        # extract control parameter overrides (if any) or use default
        controls = self.controls.copy()
        for k in ['max_data_area_mb']:
            if controls.get(k, None) is None:
                controls[k] = getattr(settings.config, k, None)

        control_string = gen_control_string(controls)
        if control_string:
            self.logger.debug("control=\"{}\"".format(control_string))

        new_lun = None
        try:
            # config string = rbd identifier / config_key (pool/image) /
            # optional osd timeout
            cfgstring = "rbd/{}/{};osd_op_timeout={}".format(
                self.pool, self.image, settings.config.osd_op_timeout)

            new_lun = UserBackedStorageObject(name=self.config_key,
                                              config=cfgstring,
                                              size=self.size_bytes,
                                              wwn=in_wwn,
                                              control=control_string)
        except RTSLibError as err:
            self.error = True
            self.error_msg = ("failed to add {} to LIO - "
                              "error({})".format(self.config_key, str(err)))
            self.logger.error(self.error_msg)
            return None

        try:
            new_lun.set_attribute("cmd_time_out", 0)
            new_lun.set_attribute("qfull_time_out",
                                  settings.config.qfull_timeout)
        except RTSLibError as err:
            self.error = True
            self.error_msg = ("Could not set LIO device attribute "
                              "cmd_time_out/qfull_time_out for device: {}. "
                              "Kernel not supported. - "
                              "error({})".format(self.config_key, str(err)))
            self.logger.error(self.error_msg)
            new_lun.delete()
            return None

        self.logger.info("(LUN.add_dev_to_lio) Successfully added {}"
                         " to LIO".format(self.config_key))

        return new_lun

    def remove_dev_from_lio(self):
        lio_root = root.RTSRoot()

        # remove the device from all tpgs
        for t in lio_root.tpgs:
            for lun in t.luns:
                if lun.storage_object.name == self.config_key:
                    try:
                        lun.delete()
                    except RTSLibError as e:
                        self.error = True
                        self.error_msg = ("Delete from LIO/TPG failed - "
                                          "{}".format(e))
                        return
                    else:
                        break  # continue to the next tpg

        for stg_object in lio_root.storage_objects:
            if stg_object.name == self.config_key:

                # alua_dir = os.path.join(stg_object.path, "alua")

                # # remove the alua directories (future versions will handle this
                # # natively within rtslib_fb
                # for dirname in next(os.walk(alua_dir))[1]:
                #     if dirname != "default_tg_pt_gp":
                #         try:
                #             alua_tpg = ALUATargetPortGroup(stg_object, dirname)
                #             alua_tpg.delete()
                #         except (RTSLibError, RTSLibNotInCFS) as err:
                #             self.error = True
                #             self.error_msg = ("Delete of ALUA dirs failed - "
                #                               "{}".format(err))
                #             return

                try:
                    stg_object.delete()
                except RTSLibError as e:
                    self.error = True
                    self.error_msg = ("Delete from LIO/backstores failed - "
                                      "{}".format(e))
                    return

                break

    @staticmethod
    def set_owner(gateways):
        """
        Determine the gateway in the configuration with the lowest number of
        active LUNs. This gateway is then selected as the owner for the
        primary path of the current LUN being processed
        :param gateways: gateway dict returned from the RADOS configuration
               object
        :return: specific gateway hostname (str) that should provide the
               active path for the next LUN
        """

        # Gateways contains simple attributes and dicts. The dicts define the
        # gateways settings, so first we extract only the dicts within the
        # main gateways dict
        gw_nodes = {
            key: gateways[key]
            for key in gateways if isinstance(gateways[key], dict)
        }
        gw_items = gw_nodes.items()

        # first entry is the lowest number of active_luns
        gw_items.sort(key=lambda x: (x[1]['active_luns']))

        # 1st tuple is gw with lowest active_luns, so return the 1st
        # element which is the hostname
        return gw_items[0][0]