def __init__(self, hostname, username, password, vserver, pool_name, cluster_id, allocation_unit=None, compute_instance_id=None): logger.debug('Init %s', self.__class__.__name__) self.cluster_id = cluster_id self.pool_name = pool_name self.vserver = vserver self.driver = CmodeISCSIDriver( hostname=hostname, username=username, password=password, vserver=vserver, api_minor_version=1, # TODO: Remove hard-coded values api_major_version=21, # TODO: Remove hard-coded values port=80, # TODO: Remove hard-coded values server_type='FILER', # TODO: Remove hard-coded values transport_type='HTTP' # TODO: Remove hard-coded values ) self._allocation_unit = allocation_unit or 1 self._compute_instance_id = compute_instance_id or socket.gethostname()
class NetAppBlockDeviceAPI(object): """ Common operations provided by all block device back-ends, exposed via synchronous methods. """ def __init__(self, hostname, username, password, vserver, pool_name, cluster_id, allocation_unit=None, compute_instance_id=None): logger.debug('Init %s', self.__class__.__name__) self.cluster_id = cluster_id self.pool_name = pool_name self.vserver = vserver self.driver = CmodeISCSIDriver( hostname=hostname, username=username, password=password, vserver=vserver, api_minor_version=1, # TODO: Remove hard-coded values api_major_version=21, # TODO: Remove hard-coded values port=80, # TODO: Remove hard-coded values server_type='FILER', # TODO: Remove hard-coded values transport_type='HTTP' # TODO: Remove hard-coded values ) self._allocation_unit = allocation_unit or 1 self._compute_instance_id = compute_instance_id or socket.gethostname() def _vol_f2n(self, volname): return str(volname).replace('-', '_') def _vol_n2f(self, s): return s.replace('_', '-') def _volume_exists(self, blockdevice_id): """ Check if volume exists. :param unicode blockdevice_id: Name of the volume to check :return: True if volume exists, otherwise False :rtype: bool """ logger.debug('Checking if volume %s exists', blockdevice_id) for volume in self.list_volumes(): if volume.blockdevice_id == blockdevice_id: logger.debug('Volume exists: %s', blockdevice_id) return True logger.debug('Volume does not exist: %s', blockdevice_id) return False def _volume_attached(self, blockdevice_id): """ Check if a volume is attached to an initiator group. :param unicode blockdevice_id: Name of the volume to check :return: True if volume is attached, otherwise False :rtype: bool """ logger.debug('Checking if volume %s is attached', blockdevice_id) for volume in self.list_volumes(): if volume.blockdevice_id == blockdevice_id: if volume.attached_to: logger.debug('Volume attached: %s', blockdevice_id) return True else: logger.debug('Volume is not attached: %s', blockdevice_id) return False logger.error('Bad path for volume: %s', blockdevice_id) def allocation_unit(self): """ The size, in bytes up to which :py:class:`~flocker.node._deploy.IDeployer` will round volume sizes before calling :py:meth:`create_volume`. :return: The size in bytes to round the volume size :rtype: int """ return self._allocation_unit def compute_instance_id(self): """ Get an identifier for this node. This will be compared against :py:attr:`flocker.node.agents.blockdevice.BlockDeviceVolume.attached_to` to determine which volumes are locally attached and it will be used with :py:meth:`attach_volume` to locally attach volumes. :return: A provider-specific node identifier which identifies the node where the method is run :rtype: unicode """ return self._compute_instance_id def create_volume(self, dataset_id, size): """ Create a new volume. When called by :py:class:`~flocker.node._deploy.IDeployer`, the supplied size will be rounded up to the nearest :py:meth:`allocation_unit`. :param str dataset_id: The Flocker dataset ID of the dataset on this volume :param int size: The size of the new volume in bytes :return: The created volume :rtype: :py:class:`~flocker.node.agents.blockdevice.BlockDeviceVolume` """ logger.debug('Creating volume %s of size %s', dataset_id, size) # This is to avoid needing a separate data store for mapping # dataset-ids to block-device-ids and back again. volume = _blockdevicevolume_from_dataset_id(dataset_id, size) self.driver.create_volume( self.pool_name, self._vol_f2n(volume.blockdevice_id), size) return volume 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 """ blockdevice_id_str = self._vol_f2n(blockdevice_id) logger.debug('Destroying volume %s', blockdevice_id) if not self._volume_exists(blockdevice_id=blockdevice_id): raise UnknownVolume(blockdevice_id) self.driver.delete_volume(self.pool_name, blockdevice_id_str) def list_volumes(self): """ List all the block devices available via the back end API. :return: The :py:class:`~flocker.node.agents.blockdevice.BlockDeviceVolume` volumes :rtype: list """ logger.debug('Getting a list of block device volumes') netapp_volumes = self.driver.volumes(self.vserver, pool_name=self.pool_name) block_device_volumes = [] for netapp_volume in netapp_volumes: logger.debug('Creating a BlockDeviceVolume object for %s', netapp_volume) try: # Create a BlockDeviceVolume, and append it to list. attached_to = None # TODO: Clean this up a bit if unicode(netapp_volume['attached_to']) != 'None': attached_to = unicode(netapp_volume['attached_to']) volume = _blockdevicevolume_from_blockdevice_id( blockdevice_id=unicode(self._vol_n2f( netapp_volume['blockdevice_id'])), size=int(netapp_volume['size']), attached_to=attached_to) block_device_volumes.append(volume) except Exception: # TODO: Too broad, make more specific logger.exception('Unexpected error while creating a ' 'BlockDeviceVolume for %s', netapp_volume) logger.debug('Found the following block device volumes: %s', block_device_volumes) return block_device_volumes def attach_volume(self, blockdevice_id, attach_to): """ Attach ``blockdevice_id`` to the node indicated by ``attach_to``. :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 ``attached_to`` attribute set to ``attach_to`` """ blockdevice_id_str = self._vol_f2n(blockdevice_id) logger.debug('Attaching volume %s to %s', blockdevice_id, attach_to) if not self._volume_exists(blockdevice_id=blockdevice_id): raise UnknownVolume(blockdevice_id) if self._volume_attached(blockdevice_id=blockdevice_id): raise AlreadyAttachedVolume(blockdevice_id) self.driver.attach_volume(pool_name=self.pool_name, volume_name=blockdevice_id_str, igroup_name=attach_to) # The volume is attached. Now extract the other properties of the # volume to create a BlockDeviceVolume. volumes = self.list_volumes() for volume in volumes: if volume.blockdevice_id == blockdevice_id: return _blockdevicevolume_from_blockdevice_id( blockdevice_id=volume.blockdevice_id, size=int(volume.size), attached_to=attach_to) raise UnknownVolume(blockdevice_id) 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. """ blockdevice_id_str = self._vol_f2n(blockdevice_id) logger.debug('Detaching volume %s', blockdevice_id) if self._volume_exists(blockdevice_id=blockdevice_id) is False: raise UnknownVolume(blockdevice_id) if self._volume_attached(blockdevice_id=blockdevice_id) is False: raise UnattachedVolume(blockdevice_id) igroup_name = None volumes = self.list_volumes() for volume in volumes: if volume.blockdevice_id == blockdevice_id: igroup_name = volume.attached_to self.driver.detach_volume(pool_name=self.pool_name, volume_name=blockdevice_id_str, igroup_name=igroup_name) # TODO: This method does a lot and can be broken down 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. """ logger.debug('Getting device path for %s', blockdevice_id) blockdevice_id_str = self._vol_f2n(blockdevice_id) if not self._volume_exists(blockdevice_id=blockdevice_id): raise UnknownVolume(blockdevice_id) if not self._volume_attached(blockdevice_id=blockdevice_id): raise UnattachedVolume(blockdevice_id) lun_id = self.driver._get_lun_id(blockdevice_id=blockdevice_id_str, pool_name=self.pool_name, vserver=self.vserver) logger.debug('%s has the LUN ID %s', blockdevice_id_str, lun_id) # Rescan the session to ensure we get the right results with "lsscsi". os.system("rescan-scsi-bus.sh -a -r") os.system("iscsiadm -m session --rescan > /dev/null") scsi_device = None output = check_output([b"/usr/bin/lsscsi"]) # Parse the result of "lsscsi" to get the desired path. for row in output.split('\n'): if re.search(r'NETAPP', row, re.I): if re.search(r'\d:\d:\d:' + str(lun_id), row, re.I): # TODO: There is a better way to parse this output device_name = re.findall(r'/\w+', row, re.I) if device_name: # /dev/sdb scsi_device = device_name[0] + device_name[1] multipath_devices = [] mapper_device = None output = check_output([b"/sbin/multipath", "-ll"]) for row in output.split('\n'): if re.search(r'NETAPP', row, re.I): device_name = re.findall(r'[/a-zA-Z0-9_-]+', row, re.I) if device_name: multipath_devices.append(device_name[0]) logger.debug('Device names: %s', ', '.join(device_name)) logger.debug('Multipath devices for %s = %s', blockdevice_id, multipath_devices) for i in multipath_devices: output = check_output([b"multipath", "-ll", "/dev/mapper/" + i]) for row in output.split('\n'): if re.search(r'\d:\d:\d:\d ' + scsi_device.split("/")[2], row, re.I): mapper_device = i logger.debug("mapper_device[3] == /dev/mapper/%s" % i) break if mapper_device: return FilePath("/dev/mapper/" + mapper_device) else: raise UnattachedVolume(blockdevice_id)