def list_volume_infos(self, filters=None): """ Returns list of volumes in the system. Each item in the list will include the name and id of the volume :return: list of volume dictionaries """ if isinstance(filters, dict) and 'name_prefix' in filters: name_prefix = filters['name_prefix'] filter_func = lambda i: i['name'].startswith(name_prefix) else: filter_func = lambda i: True infos = [] r_uri = '/api/types/Volume/instances' req = self._get(r_uri) if req.status_code != 200: raise exceptions.Error('Error listing volumes: %s' % req.json().get('message')) volume_objects = req.json() for volume in volume_objects: if filter_func(volume): infos.append({'id': volume['id'], 'name': volume['name']}) return infos
def _get_pdid(self, protection_domain): """ Private method retrieves the ScaleIO protection domain ID. ScaleIO objects are assigned a unique ID that can be used to identify the object. :param protection_domain: Unique 32 character string name of Protection Domain :return: Protection domain ID """ if not protection_domain: raise ValueError( 'Invalid protection_domain parameter, protection_domain=%s' % protection_domain) # request uri to retrieve pd id r_uri = '/api/types/Domain/instances/getByName::' + \ utilities.encode_string(protection_domain, double=True) req = self._get(r_uri) if req.status_code != 200: raise exceptions.Error( 'Error retrieving ScaleIO protection domain ID for %s: %s' % (protection_domain, req.content)) return req.json()
def get_storage_pool_statistics(self, protection_domain, storage_pool, props): """ Returns the requested statistics on a storage pool. :param: protection_domain. Name of the protection domain :param: storage_pool. Name of the storage pool :param: props. Array of property names to query :return: dict of storage pool properties """ if not protection_domain: raise ValueError( 'Invalid protection_domain parameter, protection_domain=%s' % protection_domain) if not storage_pool: raise ValueError( 'Invalid storage_pool parameter, storage_pool=%s' % storage_pool) if not props: raise ValueError('No properties specified to query') pd_id = self._get_pdid(protection_domain) sp_id = self._get_spid(storage_pool, pd_id) params = {'ids': [sp_id], 'properties': props} r_uri = '/api/types/StoragePool/instances/action/querySelectedStatistics' req = self._post(r_uri, params=params) if req.status_code != 200: raise exceptions.Error('Error retrieving storage pool stats: %s' % req.json().get('message')) stats = req.json()[sp_id] return stats
def extend_volume(self, volume_id_or_name, volume_size_gb): """ Extend the volume size. Extend a volume in multiples of 8GB. Increases the capacity of a volume. You can increase (but not decrease) a volume capacity at any time, as long as there is enough capacity for the volume size to grow. :param volume_id_or_name: ScaleIO volume ID or volume name :param volume_size_gb: New volume size in GB :return: Nothing """ volume_id = self._validate_volume_id(volume_id_or_name) # extend requires size in GB, so we will convert and check size is # multiple of 8GB volume_size_gb = self._validate_size( volume_size_gb, utilities.UnitSize.GBYTE, utilities.UnitSize.GBYTE) params = {'sizeInGB': str(volume_size_gb)} r_uri = '/api/instances/Volume::' + volume_id + '/action/setVolumeSize' req = self._post(r_uri, params=params) if req.status_code == 200: LOG.debug('Extended volume size %s successfully new size ' 'is %s GB', volume_id, volume_size_gb) elif req.json().get('errorCode') == VOLUME_CANNOT_EXTEND: LOG.error('Volume %s extend error: %s', volume_id, (req.json().get('message'))) raise exceptions.SizeTooSmall( "Required size %s GB for volume '%s' is too small: %s" % (volume_size_gb, volume_id, req.json().get('message'))) else: raise exceptions.Error("Error extending volume '%s': %s" % (volume_id, req.json().get('message')))
def systempool_size(self): """ Return ScaleIO cluster storage statistics in kilobytes. This is the raw total system capacity. :return: Tuple (used, total, free) in kilobytes """ used_kb = 0 total_kb = 0 free_kb = 0 r_uri = '/api/types/System/instances/action/querySelectedStatistics' params = {'ids': [], 'properties': ['capacityInUseInKb', 'capacityLimitInKb']} req = self._post(r_uri, params=params) if req.status_code == 200: used_kb = req.json().get('capacityInUseInKb') total_kb = req.json().get('capacityLimitInKb') free_kb = (int(total_kb) - int(used_kb)) LOG.debug('Total=%sKB, Used=%sKB, Free=%sKB' % (total_kb, used_kb, free_kb)) else: raise exceptions.Error('Error retrieving cluster statistics: %s' % req.json().get('message')) return (used_kb, total_kb, free_kb)
def list_storage_pool_infos(self, protection_domain): """ Returns list of storage pools within a protection domain. Each item in the list will include the name and id of the storage pool :param: protection_domain. Name of the protection domain to query for pools :return: list of storage pool dictionaries, containing 'name' and 'id' """ if not protection_domain: raise ValueError( 'Invalid protection_domain parameter, protection_domain=%s' % protection_domain) pd_id = self._get_pdid(protection_domain) infos = [] r_uri = '/api/instances/ProtectionDomain::' + pd_id + '/relationships/StoragePool' req = self._get(r_uri) if req.status_code != 200: raise exceptions.Error('Error listing storage pools: %s' % req.json().get('message')) sp_objects = req.json() for sp in sp_objects: infos.append({'id': sp['id'], 'name': sp['name']}) return infos
def rename_volume(self, volume_id_or_name, new_volume_name): """ Rename an existing volume :param volume_id_or_name: ScaleIO volume ID or volume name :param new_volume_name: New volume name :return: Nothing """ volume_id = self._validate_volume_id(volume_id_or_name) if not new_volume_name: raise ValueError( 'Invalid new_volume_name parameter, volume_name=%s' % new_volume_name) params = {'newName': new_volume_name} r_uri = '/api/instances/Volume::' + volume_id + '/action/setVolumeName' req = self._post(r_uri, params=params) if req.status_code == 200: # success LOG.debug('Renamed volume %s successfully' % volume_id) elif req.json().get('errorCode') == VOLUME_ALREADY_EXISTS: raise exceptions.VolumeExists( "Volume name '%s' already exists, cannot rename '%s'" % (new_volume_name, volume_id)) else: raise exceptions.Error( "Error renaming volume '%s' to '%s': %s" % (volume_id, new_volume_name, req.json().get('message')))
def get_volumeid(self, volume_name): """ Return ScaleIO volume ID given a unique string volume name :param volume_name: Unique 32 character string name of the volume :return: ScaleIO ID of volume """ volume_id = None if not volume_name: raise ValueError( 'Invalid volume_name parameter, volume_name=%s' % volume_name) r_uri = '/api/types/Volume/instances/getByName::' + \ utilities.encode_string(volume_name, double=True) req = self._get(r_uri) if req.status_code == 200: volume_id = req.json() LOG.debug('Retrieved volume id %s successfully', volume_id) return volume_id elif req.json().get('errorCode') == RESOURCE_NOT_FOUND_ERROR: raise exceptions.VolumeNotFound("Volume name '%s' is not found" % volume_name) else: raise exceptions.Error( "Error resolving volume name '%s' to id: %s" % (volume_name, req.json().get('message')))
def get_pool_id(self, protection_domain, storage_pool): """ Returns the id of a specified storage pool. :param: protection_domain. Name of the protection domain to query :param: storage_pool. Name of the storage pool within the protection domain :return: id of the specified storage pool """ if not protection_domain: raise ValueError( 'Invalid protection_domain parameter, protection_domain=%s' % protection_domain) if not storage_pool: raise ValueError( 'Invalid storage_pool parameter, storage_pool=%s' % storage_pool) r_uri = '/api/types/StoragePool/instances/action/queryIdByKey' params = { 'name': storage_pool, 'protectionDomainName': protection_domain} req = self._post(r_uri, params=params) if req.status_code == 200: pool_id = req.json() else: raise exceptions.Error('Error retrieving Pool ID: %s' % req.json().get('message')) return pool_id
def _map_volume(self, volume_id, sdc_guid, allow_multi_map=True): """ Private method maps a volume to a SDC :param volume_id: ScaleIO volume ID :param sdc_guid: Unique SDC identifier supplied by drv_cfg utility :param allow_multi_map: Allow this volume to be mapped to another SDC. Actual for the first mapping operation only. :return: Nothing """ multi_map = str(allow_multi_map).lower() params = {'guid': sdc_guid, 'allowMultipleMappings': multi_map} r_uri = '/api/instances/Volume::' + volume_id + '/action/addMappedSdc' req = self._post(r_uri, params=params) if req.status_code == 200: # success LOG.debug('Mapped volume %s successfully', volume_id) elif req.json().get('errorCode') == VOLUME_ALREADY_MAPPED_ERROR: LOG.warn('Volume already mapped: %s', (req.json().get('message'))) raise exceptions.VolumeAlreadyMapped( "Volume '%s' is already mapped: %s" % (volume_id, req.json().get('message'))) else: raise exceptions.Error("Error mapping volume '%s': %s" % (volume_id, req.json().get('message')))
def storagepool_size(self, protection_domain, storage_pool): """ For a given single storage pool, return the used, total and free space in bytes that can be allocated for Volumes. Note this is not the total capacity of the system. :param protection_domain: Protection domain name :param storage_pool: Storage pool name :return: Tuple used_bytes, total_bytes, free_bytes """ if not protection_domain: raise ValueError( 'Invalid protection_domain parameter, protection_domain=%s' % protection_domain) if not storage_pool: raise ValueError( 'Invalid storage_pool parameter, storage_pool=%s' % storage_pool) used_bytes = 0 total_bytes = 0 free_bytes = 0 pd_id = self._get_pdid(protection_domain) sp_id = self._get_spid(storage_pool, pd_id) r_uri = ('/api/types/StoragePool/instances/action/' 'querySelectedStatistics') params = { 'ids': [sp_id], 'properties': [ 'capacityAvailableForVolumeAllocationInKb', 'capacityLimitInKb', 'spareCapacityInKb' ] } req = self._post(r_uri, params=params) if req.status_code == 200: stat = req.json()[sp_id] # Divide by two because ScaleIO creates a copy for each volume total_kb = (stat['capacityLimitInKb'] - stat['spareCapacityInKb']) / 2 # Free capacity for the storage pool free_kb = stat['capacityAvailableForVolumeAllocationInKb'] # Calculate used capacity used_kb = total_kb - free_kb # convert to bytes used_bytes = used_kb * 1024 total_bytes = total_kb * 1024 free_bytes = free_kb * 1024 else: raise exceptions.Error( 'Error retrieving storage pool statistics: %s' % req.json().get('message')) return (used_bytes, total_bytes, free_bytes)
def create_volume(self, volume_name, protection_domain, storage_pool, provisioning_type='thick', volume_size_gb=8): """ Add a volume. You can create a volume when the requested capacity is available. To start allocating volumes, the system requires that there be at least three SDS nodes. User-defined names are optional. You can define object names, according to the following rules: * Contain less than 32 characters * Contain only alphanumeric and punctuation characters * Be unique within the object type :param volume_name: Name of the volume you want to create :param protection_domain: Protection domain name :param storage_pool: Storage pool name :param provisioning_type: thick/ThickProvisioned or thin/ThinProvisioned :param volume_size_gb: The size of the volume in GB (must be multiple of 8GB) :return: Tuple containing the volume id and volume name created """ if not volume_name: raise ValueError( 'Invalid volume_name parameter, volume_name=%s' % volume_name) # get protection domain id for request store this for the duration of # the object pd_id = self._get_pdid(protection_domain) sp_id = self._get_spid(storage_pool, pd_id) # create requires size in KB, so we will convert and check size is # multiple of 8GB volume_size_kb = self._validate_size( volume_size_gb, utilities.UnitSize.GBYTE, utilities.UnitSize.KBYTE) # request payload containing volume create params params = {'protectionDomainId': pd_id, 'volumeSizeInKb': str(volume_size_kb), 'name': volume_name, 'volumeType': self._get_provisiontype(provisioning_type), 'storagePoolId': sp_id} r_uri = '/api/types/Volume/instances' req = self._post(r_uri, params=params) if req.status_code == 200: volume_id = req.json().get('id') LOG.debug('Created volume %s successfully', volume_id) elif req.json().get('errorCode') == VOLUME_ALREADY_EXISTS: raise exceptions.VolumeExists("Volume name '%s' already exists" % volume_name) else: raise exceptions.Error("Error creating volume '%s': %s" % (volume_name, req.json().get('message'))) return volume_id, volume_name
def list_volume_infos(self): infos = [] r_uri = '/api/types/Volume/instances' req = self._get(r_uri) if req.status_code != 200: raise exceptions.Error('Error listing volumes: %s' % req.json().get('message')) volume_objects = req.json() for volume in volume_objects: infos.append({'id': volume['id'], 'name': volume['name']}) return infos
def get_storage_pool_properties_from_id(self, storage_pool_id): """ Returns the properties of the specified storage pool. :param: storage_pool_id. ID of the storage pool :return: dict of storage pool properties """ r_uri = '/api/instances/StoragePool::' + storage_pool_id req = self._get(r_uri) if req.status_code != 200: raise exceptions.Error('Error retrieving storage pool props: %s' % req.json().get('message')) return req.json()
def get_protection_domain_properties_from_id(self, protection_domain_id): """ Returns the properties of the specified protection domain. :param: protection_domain_id. ID of the protection domain :return: dict of protection domain properties """ r_uri = '/api/instances/ProtectionDomain::' + protection_domain_id req = self._get(r_uri) if req.status_code != 200: raise exceptions.Error('Error retrieving protection domain props: %s' % req.json().get('message')) return req.json()
def get_configuration(self): """ Returns the system, configuration :return: dict of system properties """ r_uri = '/api/Configuration' req = self._get(r_uri) if req.status_code != 200: raise exceptions.Error('Error retrieving system properties: %s' % req.json().get('message')) props = req.json() return props
def is_volume_attached(self, volume_id_or_name, sdc_guid): """ Check if a volume is attached to a SDC :param volume_id_or_name: ScaleIO volume ID or volume name :sdc_guid GUID of SDC :return: True if the volume is attached to the SDC """ volume_object = self._volume(volume_id_or_name) params = {'ids': [si['sdcId'] for si in volume_object.mappedSdcInfo]} r_uri = '/api/types/Sdc/instances/action/queryBySelectedIds' req = self._post(r_uri, params=params) if req.status_code != 200: raise exceptions.Error('Error requesting SDC: %s' % req.json.get('message')) return any(sdc_guid == sdc['sdcGuid'] for sdc in req.json())
def get_scaleio_api_version(self): """ Returns ScaleIO REST API version. This may or may not match the version of ScaleIO, but will be in the form of n.n.n, matching the regex: '\d+\.\d+\.\d+' :return: String containing version number """ r_uri = '/api/version' req = self._get(r_uri) if req.status_code != 200: raise exceptions.Error('Error retrieving api version information: %s' % req.json().get('message')) version = req.text.replace('\"', '') return version
def list_protection_domain_infos(self): """ Returns list of protection domains in the system. Each item in the list will include the name and id of the protection domain :return: list of protection domain dictionaries, containing 'name' and 'id' """ infos = [] r_uri = '/api/types/ProtectionDomain/instances' req = self._get(r_uri) if req.status_code != 200: raise exceptions.Error('Error listing protection domains: %s' % req.json().get('message')) pd_objects = req.json() for pd in pd_objects: infos.append({'id': pd['id'], 'name': pd['name']}) return infos
def snapshot_volume(self, volume_id_or_name, snapshot_name): """ Snapshot an existing volume. The ScaleIO storage system enables you to take snapshots of existing volumes, up to 31 per volume. The snapshots are thinly provisioned and are extremely quick. Once a snapshot is generated, it becomes a new, unmapped volume in the system. :param volume_id_or_name: ScaleIO volume ID or volume name :param snapshot_name: Name of the snapshot :return: Tuple containing the volume id of snapshot and volume list """ volume_id = self._validate_volume_id(volume_id_or_name) if not snapshot_name: raise ValueError( 'Invalid snapshot snapshot_name parameter, snapshot_name=%s' % snapshot_name) snapshot_gid = volume_list = None params = { 'snapshotDefs': [{ 'volumeId': volume_id, 'snapshotName': snapshot_name }] } r_uri = '/api/instances/System/action/snapshotVolumes' req = self._post(r_uri, params=params) if req.status_code == 200: snapshot_gid = req.json().get('snapshotGroupId') volume_list = req.json().get('volumeIdList') elif req.json().get('errorCode') == VOLUME_ALREADY_EXISTS: raise exceptions.VolumeExists( "Volume name '%s' already exists, cannot make snapshot '%s'" % (snapshot_name, volume_id)) else: raise exceptions.Error( "Error making snapshot '%s' of volume '%s': %s" % (snapshot_name, volume_id, req.json().get('message'))) return snapshot_gid, volume_list
def get_volume_properties(self, volume_id_or_name): """ Returns properties of a ScaleIO volume :param volume_id_or_name: ScaleIO volume ID or volume name :return: Dict containing volume properties """ volume_id = self._validate_volume_id(volume_id_or_name) r_uri = '/api/instances/Volume::' + volume_id req = self._get(r_uri) if req.status_code == 200: # success LOG.debug('Retrieved volume object %s successfully', volume_id) elif req.json().get('errorCode') == VOLUME_NOT_FOUND_ERROR: raise exceptions.VolumeNotFound('Volume %s is not found' % volume_id) else: raise exceptions.Error("Error retrieving volume '%s': %s" % (volume_id, req.json().get('message'))) return req.json()
def snapshot_volume_from_defs(self, snapshot_defs): """ Snapshot existing volume(s) from a list of volume/snapshots. Enables the taking of snapshots of multiple existing volumes, up to 31 per volume. The snapshots are thinly provisioned and are extremely quick. Once a snapshot is generated, it becomes a new, unmapped volume in the system. :param snapshot_defs: List of maps of volumeID->snapshot_names :return: List of newly created snapshots """ if not snapshot_defs: raise ValueError( 'Invalid snapshot snapshot_defs parameter, None') params = {'snapshotDefs': snapshot_defs} r_uri = '/api/instances/System/action/snapshotVolumes' req = self._post(r_uri, params=params) if req.status_code != 200: raise exceptions.Error( "Error making snapshots from: %s" % snapshot_defs) return req.json()
def _get_spid(self, storage_pool, pd_id): """ Private method retrieves the ScaleIO storage pool ID. ScaleIO objects are assigned a unique ID that can be used to identify the object. :param storage_pool: Unique 32 character string name of Storage Pool :param pd_id: Protection domain id associated with storage pool :return: Storage pool id """ if not storage_pool: raise ValueError( 'Invalid storage_pool parameter, storage_pool=%s' % storage_pool) # request uri to retrieve sp id r_uri = '/api/types/Pool/instances/getByName::' + \ pd_id + ',' + utilities.encode_string(storage_pool, double=True) req = self._get(r_uri) if req.status_code != 200: raise exceptions.Error( 'Error retrieving ScaleIO storage pool ID for %s: %s' % (storage_pool, req.content)) return req.json()
def _unmap_volume(self, volume_id, sdc_guid=None, unmap_all=False): """ Private method unmaps a volume from one or all SDCs. :param volume_id: ScaleIO volume ID :param sdc_guid: Unique SDC identifier :param unmap_all: True, unmap from all SDCs, False only unmap from local SDC :return: Nothing """ if not unmap_all: if not sdc_guid: raise ValueError( 'sdc_guid must be specified or unmap_all must be True') else: LOG.debug('Using ScaleIO SDC client GUID %s for ' 'unmap operation.' % sdc_guid) if unmap_all: # unmap from all sdcs params = {'allSdcs': ''} else: # only unmap from local sdc params = {'guid': sdc_guid} r_uri = '/api/instances/Volume::' + \ volume_id + '/action/removeMappedSdc' req = self._post(r_uri, params=params) if req.status_code == 200: # success LOG.debug('Unmapped volume %s successfully', volume_id) elif req.json().get('errorCode') == VOLUME_NOT_MAPPED_ERROR: LOG.warn('Volume %s cannot be unmapped: %s', volume_id, req.json().get('message')) raise exceptions.VolumeNotMapped("Volume '%s' is not mapped" % volume_id) else: raise exceptions.Error("Error unmapping volume '%s': %s" % (volume_id, req.json().get('message')))
def _volume(self, volume_id_or_name): """ Return a ScaleIOVolume object :param volume_id_or_name: ScaleIO volume ID or volume name :return: ScaleIOVolume object or None if no valid volume found """ volume_id = self._validate_volume_id(volume_id_or_name) volume_obj = None r_uri = '/api/instances/Volume::' + volume_id req = self._get(r_uri) if req.status_code == 200: # success LOG.debug('Retrieved volume object %s successfully', volume_id) volume_obj = _ScaleIOVolume(req.json()) elif req.json().get('errorCode') == VOLUME_NOT_FOUND_ERROR: raise exceptions.VolumeNotFound('Volume %s is not found' % volume_id) else: raise exceptions.Error("Error retrieving volume '%s': %s" % (volume_id, req.json().get('message'))) return volume_obj
def delete_volume(self, volume_id_or_name, include_descendents=False, only_descendents=False, vtree=False, unmap_on_delete=False, force_delete=True): """ Delete a volume. This command removes a ScaleIO volume. Before removing a volume, you must ensure that it is not mapped to any SDCs. When removing a volume, you can remove the VTree as well (all related snapshots), the volume and its snapshots, or just the snapshots. Before removing a VTree, you must unmap all volumes in the VTree before removing them. Note: Removal of a volume erases all the data on the corresponding volume. :param volume_id_or_name: ScaleIO volume ID or volume name :param include_descendents: Remove volume along with any descendents :param only_descendents: Remove only the descendents of the volume :param vtree: Remove the entire VTREE :param unmap_on_delete: Unmap volume from all SDCs before deleting :param force_delete: Ignore if volume is already deleted :return: Nothing """ volume_id = self._validate_volume_id(volume_id_or_name) # the removeMode options are mutually exclusive if [include_descendents, only_descendents, vtree].count(True) > 1: raise ValueError( 'Only one removeMode flag can be specified ' '(include_descendants, only_descendants, vtree)') # default removeMode is ONLY_ME remove_mode = 'ONLY_ME' if (include_descendents): remove_mode = 'INCLUDING_DESCENDANTS' if (only_descendents): remove_mode = 'DESCENDANTS_ONLY' if (vtree): remove_mode = 'WHOLE_VTREE' params = {'removeMode': remove_mode} if unmap_on_delete: LOG.debug('Unmap before delete flag True, attempting ' 'to unmap volume %s from all sdcs before deletion', volume_id) try: self._unmap_volume(volume_id, unmap_all=True) except exceptions.VolumeNotMapped: pass r_uri = '/api/instances/Volume::' + volume_id + '/action/removeVolume' req = self._post(r_uri, params=params) if req.status_code == 200: LOG.debug('Removed volume %s successfully', volume_id) elif req.json().get('errorCode') == VOLUME_NOT_FOUND_ERROR: if not force_delete: LOG.warn('Error removing volume %s: %s', volume_id, (req.json().get('message'))) raise exceptions.VolumeNotFound("Volume '%s' is not found" % volume_id) else: raise exceptions.Error("Error removing volume '%s': %s" % (volume_id, req.json().get('message')))