def test_get_multipath_device_not_exit_device(self, ospathexist, check_output_mock): check_output_mock.return_value = MULTIPATH_OUTPUT ospathexist.return_value = True hostops = HostActions() with self.assertRaises(MultipathDeviceNotFound): hostops.get_multipath_device('fake-vol-wwn')
def test_get_multipath_device_redhat_exist_device(self, ospathexist, check_output_mock): check_output_mock.return_value = REDHAT_MULTIPATH_OUTPUT ospathexist.return_value = True hostops = HostActions() self.assertEqual(hostops.get_multipath_device(REDHAT_MULTIPATH_WWN), '{}/{}'.format(PREFIX_DEVICE_PATH, REDHAT_MULTIPATH_MPATH))
def test_get_multipath_device_exist2_device(self, ospathexist, check_output_mock): check_output_mock.return_value = MULTIPATH_OUTPUT2 ospathexist.return_value = True hostops = HostActions() self.assertEqual(hostops.get_multipath_device(MULTIPATH_OUTPUT_WWN2), '{}/{}'.format(PREFIX_DEVICE_PATH, WWN_PREFIX + MULTIPATH_OUTPUT_WWN2))
def test_veryfy_debug_level(self): # check the default HostActions() self.assertEqual( host_actions.LOG.level, getattr(host_actions.logging, DEFAULT_DEBUG_LEVEL)) # check with specific level HostActions("ERROR") self.assertEqual( host_actions.LOG.level, getattr(host_actions.logging, "ERROR"))
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'], ))
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
def test_no_multipath_exist(self, find_executable): find_executable.side_effect = [None, 'rescan', 'iscsiadm', None] with self.assertRaises(MultipathCmdNotFound): HostActions()
def test_no_rescaan_cmd_exist(self, find_executable): find_executable.side_effect = [None, None] with self.assertRaises(RescanCmdNotFound): HostActions()
def test_rescan(self, check_output_mock): check_output_mock.return_value = None hostops = HostActions() hostops.rescan_scsi()