コード例 #1
0
class IBMStorageBlockDeviceAPI(object):
    """
    A ``IBlockDeviceAPI`` and  ``IProfiledBlockDeviceAPI`` for IBM Storage.
    """

    @logme(LOG)
    def __init__(self, cluster_id, backend_client, driver_conf):
        """
        Initialize new instance of the IBM Storage Flocker driver.

        :param backend_client: IBMStorageAbsClient
        :param UUID cluster_id: The Flocker cluster ID
        :param driver_conf: dict with default resource to provision
            and default hostname for attach / detach operations.
        :raises MultipathCmdNotFound, RescanCmdNotFound:
                in case mandatory commands are missing
        """
        self._client = backend_client
        self._cluster_id = cluster_id
        self._storage_resource = driver_conf[CONF_PARAM_DEFAULT_SERVICE]
        self._instance_id = self._get_host(driver_conf)
        self._cluster_id_slug = uuid2slug(self._cluster_id)
        self._host_ops = HostActions(backend_client.con_info.debug_level)
        self._is_multipathing = self._host_ops.is_multipath_active()
        LOG.info(messages.DRIVER_INITIALIZATION.format(
            backend_type=self._client.backend_type,
            backend_ip=self._client.con_info.management_ip,
            username=self._client.con_info.credential['username'],
        ))

    @staticmethod
    def _get_host(driver_conf):
        hostname = driver_conf[CONF_PARAM_HOSTNAME] or \
            unicode(socket.gethostname())
        LOG.debug(messages.HOSTNAME_TO_BE_USE.format(hostname=hostname))
        return hostname

    def _get_volume(self, blockdevice_id):
        """
        Return BlockDeviceVolume if exists, else raise exception.

        :param unicode blockdevice_id: Name of the volume to check
        :raise: UnknownVolume - in case the volume does not exist
        :return: BlockDeviceVolume
        """
        return self._get_blockdevicevolume_by_vol(
            self._get_volume_object(blockdevice_id)
        )

    def _get_volume_object(self, blockdevice_id):
        """
        Return VolInfo if exist else raise exception.

        :param unicode blockdevice_id: Name of the volume to check
        :raise: UnknownVolume - in case volume not exist
        :return: BlockDeviceVolume
        """
        vol_objs = self._client.list_volumes(wwn=blockdevice_id)
        if not vol_objs:
            LOG.error("Volume does not exists: " + str(blockdevice_id))
            raise UnknownVolume(blockdevice_id)

        return vol_objs[0]

    def _volume_exist(self, blockdevice_id):
        """
        :param blockdevice_id:
        :return: Boolean
        """
        try:
            self._get_volume_object(blockdevice_id)
            return True
        except UnknownVolume:
            return False

    @logme(LOG, PREFIX)
    def compute_instance_id(self):
        """
        Get the backend-specific identifier for this node.

        This will be compared against ``BlockDeviceVolume.attached_to``
        to determine which volumes are locally attached and it will be used
        with ``attach_volume`` to locally attach volumes.

        :raise UnknownInstanceID: If we cannot determine the identifier
                                  of the node.
        :returns: A ``unicode`` object giving a provider-specific node
            identifier which identifies the node where the method is run.
        """
        return self._instance_id

    @logme(LOG, PREFIX)
    def allocation_unit(self):
        """
        The size in bytes up to which ``IDeployer`` will round volume
        sizes before calling ``IBlockDeviceAPI.create_volume``.

        :rtype: ``int``
        """
        return self._client.allocation_unit()

    @logme(LOG, PREFIX)
    def create_volume_with_profile(self, dataset_id, size, profile_name):
        """
        Create a new volume with the specified profile.

        When called by ``IDeployer``, the supplied size will be
        rounded up to the nearest ``IBlockDeviceAPI.allocation_unit()``.


        :param UUID dataset_id: The Flocker dataset ID of the dataset on this
            volume.
        :param int size: The size of the new volume in bytes.
        :param unicode profile_name: The name of the storage profile for this
            volume.

        :returns: A ``BlockDeviceVolume`` of the newly created volume.
        """
        volume_name = build_vol_name(dataset_id, self._cluster_id_slug)
        vol_obj = self._client.create_volume(
            vol=volume_name,
            resource=profile_name,
            size=size,
        )

        LOG.info(messages.DRIVER_OPERATION_VOL_CREATE_WITH_PROFILE.format(
            name=vol_obj.name,
            size=vol_obj.size,
            profile=profile_name,
            wwn=vol_obj.wwn))

        return _get_blockdevicevolume(dataset_id, vol_obj.wwn, vol_obj.size)

    @logme(LOG, PREFIX)
    def create_volume(self, dataset_id, size):
        """
        Create a new volume.

        :param UUID dataset_id: The Flocker dataset ID of the dataset on this
            volume.
        :param int size: The size of the new volume in bytes.
        :returns: A ``BlockDeviceVolume``.
        """
        default_profile = self._client.handle_default_profile(
            self._storage_resource)

        LOG.info(messages.DRIVER_OPERATION_VOL_CREATING.format(
            dataset_id=dataset_id,
            size=size,
            default_profile=default_profile,
        ))

        return self.create_volume_with_profile(dataset_id, size,
                                               default_profile)

    @logme(LOG, PREFIX)
    def destroy_volume(self, blockdevice_id):
        """
        Destroy an existing volume.

        :param unicode blockdevice_id: The unique identifier for the volume to
            destroy.
        :raises UnknownVolume: If the supplied ``blockdevice_id`` does not
            exist.
        :return: ``None``
        """
        # raise exception if not exist
        vol = self._get_volume_object(blockdevice_id)
        self._client.delete_volume(blockdevice_id)
        LOG.info(messages.DRIVER_OPERATION_VOL_DESTROY.format(
            volname=vol.name,
            wwn=blockdevice_id,
        ))

    @logme(LOG, PREFIX)
    def attach_volume(self, blockdevice_id, attach_to):
        """
        Attach ``blockdevice_id`` to ``host``.

        :param unicode blockdevice_id: The unique identifier for the block
            device being attached.
        :param unicode attach_to: An identifier like the one returned by the
            ``compute_instance_id`` method indicating the node to which to
            attach the volume.
        :raises UnknownVolume: If the supplied ``blockdevice_id`` does not
            exist.
        :raises AlreadyAttachedVolume: If the supplied ``blockdevice_id`` is
            already attached.
        :returns: A ``BlockDeviceVolume`` with a ``host`` attribute set to
            ``host``.
        """
        # Raises UnknownVolume
        volume = self._get_volume(blockdevice_id)

        # raises AlreadyAttachedVolume
        if volume.attached_to is not None:
            LOG.error("Could Not attach Volume {} is already attached".
                      format(str(blockdevice_id)))
            raise AlreadyAttachedVolume(blockdevice_id)

        # Try to map the volume
        self._client.map_volume(wwn=blockdevice_id, host=attach_to)

        attached_volume = volume.set(attached_to=attach_to)
        LOG.info(messages.DRIVER_OPERATION_VOL_ATTACH.format(
            blockdevice_id=blockdevice_id, attach_to=attach_to))

        # Rescan the OS to discover the attached volume
        LOG.info(messages.DRIVER_OPERATION_VOL_RESCAN_START_ATTACH.format(
            blockdevice_id=blockdevice_id))
        self._host_ops.rescan_scsi()

        return attached_volume

    @logme(LOG, PREFIX)
    def detach_volume(self, blockdevice_id):
        """
        Detach ``blockdevice_id`` from whatever host it is attached to.

        :param unicode blockdevice_id: The unique identifier for the block
            device being detached.

        :raises UnknownVolume: If the supplied ``blockdevice_id`` does not
            exist.
        :raises UnattachedVolume: If the supplied ``blockdevice_id`` is
            not attached to anything.
        :returns: ``None``
        """
        # raises UnknownVolume
        volume = self._get_volume(blockdevice_id)

        # raises UnattachedVolume
        if volume.attached_to is None:
            LOG.error(messages.CANNOT_DETACH_VOLUME_NOT_ATTACHED.
                      format(str(blockdevice_id)))
            raise UnattachedVolume(blockdevice_id)

        self._clean_up_device_before_unmap(blockdevice_id)
        self._client.unmap_volume(wwn=blockdevice_id, host=volume.attached_to)
        LOG.info(messages.DRIVER_OPERATION_VOL_DETTACH.format(
            blockdevice_id=blockdevice_id, attach_to=volume.attached_to))

        # Rescan the OS to clean the detached volume
        LOG.info(messages.DRIVER_OPERATION_VOL_RESCAN_START_ATTACH.format(
            blockdevice_id=blockdevice_id))
        self._host_ops.rescan_scsi()

    @logme(LOG)
    def _clean_up_device_before_unmap(self, blockdevice_id):
        """
        The function cleans the multipath device.
        Use this function before unmapping a device from the backend.
        If a device is unmapped before cleaning the related multipath device,
        this may result in a faulty path. See example below.

        example of faulty devices :
        ----------------------------
        200173800fdf510eb dm-0 IBM     ,2810XIV
        size=16G features='1 queue_if_no_path' hwhandler='0' wp=rw
        `-+- policy='round-robin 0' prio=0 status=active
          |- 3:0:0:3 sdf 8:80  active faulty running
          `- 4:0:0:3 sdg 8:96  active faulty running

        :param blockdevice_id:
        :return: None
        """

        if not self._is_multipathing:
            LOG.debug(messages.NO_NEED_TO_CLEAN_IF_NO_MULTIPATHING)
            return
        try:
            device_path = self.get_device_path(blockdevice_id)
        except UnknownVolume:
            LOG.debug(messages.NO_DEVICE_FOUND_FOR_WWN.format(
                wwn=blockdevice_id))
            return

        self._host_ops.clean_mp_device(device_path.path)

    def _is_cluster_volume(self, vol_name):
        """
        Check if the volume is part of the Flocker cluster
        :param vol_name
        :return Boolean
        """
        if vol_name.startswith(VOL_NAME_FLOCKER_PREFIX):
            cluster_id_slug = get_cluster_id_slug_from_vol_name(vol_name)
            if cluster_id_slug == self._cluster_id_slug:
                return True
        return False

    @logme(LOG, PREFIX)
    def list_volumes(self):
        """
        List all the block devices available via the back end API.

        Only volumes for this particular Flocker cluster should be included.

        :returns: A ``list`` of ``BlockDeviceVolume``s.
        """
        volumes = []

        vol_list = self._client.list_volumes(resource=self._storage_resource)
        map_dict = self._client.get_vols_mapping()
        host_dict = self._client.get_hosts()

        for vol in vol_list:
            if not self._is_cluster_volume(vol.name):
                continue
            host_id = map_dict.get(vol.wwn)  # vol can be mapped to one host.
            hostname = host_dict.get(host_id) if host_id else None
            if hostname:
                hostname = unicode(hostname)
            attach_to = hostname
            vol_dataset_id = get_dataset_id_from_vol_name(vol.name)

            block_device_volume = _get_blockdevicevolume(
                vol_dataset_id,
                vol.wwn,
                vol.size,
                attach_to)
            volumes.append(block_device_volume)
        return volumes

    def _get_blockdevicevolume_by_vol(self, vol_obj):
        """
        return BlockDeviceVolume from VolInfo.

        :param vol_obj: VolInfo
        :raise UnknownVolume:
        :return: BlockDeviceVolume
        """
        if not self._is_cluster_volume(vol_obj.name):
            raise UnknownVolume(unicode(vol_obj.wwn))

        host = self._client.get_vol_mapping(vol_obj.wwn)
        host = unicode(host) if host else None

        vol_dataset_id = get_dataset_id_from_vol_name(vol_obj.name)
        block_device_volume = _get_blockdevicevolume(
            vol_dataset_id,
            vol_obj.wwn,
            vol_obj.size,
            host)
        return block_device_volume

    @logme(LOG, PREFIX)
    def get_device_path(self, blockdevice_id):
        """
        Return the device path that has been allocated to the block device on
        the host to which it is currently attached.

        :param unicode blockdevice_id: The unique identifier for the block
            device.
        :raises UnknownVolume: If the supplied ``blockdevice_id`` does not
            exist.
        :raises UnattachedVolume: If the supplied ``blockdevice_id`` is
            not attached to a host.
        :returns: A ``FilePath`` for the device.
        """
        # raises UnknownVolume
        vol_info = self._get_volume_object(blockdevice_id)
        volume = self._get_blockdevicevolume_by_vol(vol_info)

        if volume.attached_to is None:
            LOG.error(messages.BLOCKDEVICE_NOT_ATTACHED_STOP_SEARCHING.
                      format(str(blockdevice_id)))
            raise UnattachedVolume(blockdevice_id)

        if self._is_multipathing:
            # Assume OS rescan was already triggered
            # TODO consider to rescan if device was not found
            return self._get_device_multipath(vol_info.name, blockdevice_id)
        else:
            raise Exception(messages.SUPPORT_ONLY_MULTIPATHING)

    @logme(LOG)
    def _get_device_multipath(self, vol_name, blockdevice_id):
        """
        :param vol_name: Volume name for logging
        :param blockdevice_id: which is the WWN of the volume
        :return: A ``FilePath`` for the multipath device.
        """
        try:
            device_path = self._host_ops.get_multipath_device(
                vol_wwn=blockdevice_id)
        except (host_actions.MultipathDeviceNotFound,
                host_actions.MultipathDeviceFilePathNotFound,
                host_actions.CalledProcessError) as e:
            LOG.error(messages.CANNOT_FIND_DEVICE_PATH.format(
                str(blockdevice_id), vol_name, e))

            raise UnattachedVolume(blockdevice_id)
        device_path_obj = FilePath(device_path)
        LOG.info(messages.DRIVER_OPERATION_GET_MULTIPATH_DEVICE.format(
            volname=vol_name, device_path=device_path_obj.path,
            cmd=self._host_ops.multipath_cmd_ll))

        return device_path_obj
コード例 #2
0
 def test_rescan(self, check_output_mock):
     check_output_mock.return_value = None
     hostops = HostActions()
     hostops.rescan_scsi()