python-3parclient-4.2.12/0000755000000000000000000000000014106677657015144 5ustar rootroot00000000000000python-3parclient-4.2.12/hpe3parclient/0000755000000000000000000000000014106677657017705 5ustar rootroot00000000000000python-3parclient-4.2.12/hpe3parclient/__init__.py0000644000000000000000000000215514106434774022010 0ustar rootroot00000000000000# (c) Copyright 2012-2016 Hewlett Packard Enterprise Development LP # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ HPE 3PAR Client. :Author: Walter A. Boring IV :Author: Kurt Martin :Copyright: Copyright 2012-2016 Hewlett Packard Enterprise Development LP :License: Apache v2.0 """ version_tuple = (4, 2, 12) def get_version_string(): """Current version of HPE3PARClient.""" if isinstance(version_tuple[-1], str): return '.'.join(map(str, version_tuple[:-1])) + version_tuple[-1] return '.'.join(map(str, version_tuple)) version = get_version_string() python-3parclient-4.2.12/hpe3parclient/client.py0000644000000000000000000067601614106434774021544 0ustar rootroot00000000000000# (c) Copyright 2012-2016 Hewlett Packard Enterprise Development LP # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ HPE 3PAR REST Client. .. module: client .. moduleauthor: Walter A. Boring IV .. moduleauthor: Kurt Martin :Author: Walter A. Boring IV :Description: This is the 3PAR Client that talks to 3PAR's REST WSAPI Service. It provides the ability to provision 3PAR volumes, VLUNs, CPGs. This version also supports running actions on the 3PAR that use SSH. This client requires and works with 3PAR InForm 3.1.3 MU1 firmware """ import copy import re import time import uuid import logging try: # For Python 3.0 and later from urllib.parse import quote except ImportError: # Fall back to Python 2's urllib2 from urllib2 import quote from hpe3parclient import exceptions, http, ssh from hpe3parclient import showport_parser logger = logging.getLogger(__name__) class HPE3ParClient(object): """ The 3PAR REST API Client. :param api_url: The url to the WSAPI service on 3PAR ie. http://<3par server>:8080/api/v1 :type api_url: str """ CHAP_INITIATOR = 1 CHAP_TARGET = 2 PORT_MODE_TARGET = 2 PORT_MODE_INITIATOR = 3 PORT_MODE_PEER = 4 PORT_TYPE_HOST = 1 PORT_TYPE_DISK = 2 PORT_TYPE_FREE = 3 PORT_TYPE_IPORT = 4 PORT_TYPE_RCFC = 5 PORT_TYPE_PEER = 6 PORT_TYPE_RCIP = 7 PORT_TYPE_ISCSI = 8 PORT_TYPE_CNA = 9 PORT_PROTO_FC = 1 PORT_PROTO_ISCSI = 2 PORT_PROTO_FCOE = 3 PORT_PROTO_IP = 4 PORT_PROTO_SAS = 5 PORT_STATE_READY = 4 PORT_STATE_SYNC = 5 PORT_STATE_OFFLINE = 10 SET_MEM_ADD = 1 SET_MEM_REMOVE = 2 SET_RESYNC_PHYSICAL_COPY = 3 SET_STOP_PHYSICAL_COPY = 4 STOP_PHYSICAL_COPY = 1 RESYNC_PHYSICAL_COPY = 2 GROW_VOLUME = 3 PROMOTE_VIRTUAL_COPY = 4 VIRTUAL_COPY = 3 TUNE_VOLUME = 6 TPVV = 1 FPVV = 2 TDVV = 3 CONVERT_TO_DECO = 4 TARGET_TYPE_VVSET = 1 TARGET_TYPE_SYS = 2 PRIORITY_LOW = 1 PRIORITY_NORMAL = 2 PRIORITY_HIGH = 3 TASK_TYPE_VV_COPY = 1 TASK_TYPE_PHYS_COPY_RESYNC = 2 TASK_TYPE_MOVE_REGIONS = 3 TASK_TYPE_PROMOTE_SV = 4 TASK_TYPE_REMOTE_COPY_SYNC = 5 TASK_TYPE_REMOTE_COPY_REVERSE = 6 TASK_TYPE_REMOTE_COPY_FAILOVER = 7 TASK_TYPE_REMOTE_COPY_RECOVER = 8 TASK_TYPE_REMOTE_COPY_RESTORE = 9 TASK_TYPE_COMPACT_CPG = 10 TASK_TYPE_COMPACT_IDS = 11 TASK_TYPE_SNAPSHOT_ACCOUNTING = 12 TASK_TYPE_CHECK_VV = 13 TASK_TYPE_SCHEDULED_TASK = 14 TASK_TYPE_SYSTEM_TASK = 15 TASK_TYPE_BACKGROUND_TASK = 16 TASK_TYPE_IMPORT_VV = 17 TASK_TYPE_ONLINE_COPY = 18 TASK_TYPE_CONVERT_VV = 19 TASK_DONE = 1 TASK_ACTIVE = 2 TASK_CANCELLED = 3 TASK_FAILED = 4 # build contains major minor mj=3 min=01 main=03 build=230 # When updating these, make sure desc is appropriate for error messages # and make sure the version overrides in file_client are still OK. HPE3PAR_WS_MIN_BUILD_VERSION = 30103230 HPE3PAR_WS_MIN_BUILD_VERSION_DESC = '3.1.3 MU1' HPE3PAR_WS_PRIMERA_MIN_BUILD_VERSION = 40000128 HPE3PAR_WS_PRIMERA_MIN_BUILD_VERSIONDESC = '4.2.0' # Minimum build version needed for VLUN query support. HPE3PAR_WS_MIN_BUILD_VERSION_VLUN_QUERY = 30201292 HPE3PAR_WS_MIN_BUILD_VERSION_VLUN_QUERY_DESC = '3.2.1 MU2' WSAPI_MIN_VERSION_COMPRESSION_SUPPORT = '1.6.0' VLUN_TYPE_EMPTY = 1 VLUN_TYPE_PORT = 2 VLUN_TYPE_HOST = 3 VLUN_TYPE_MATCHED_SET = 4 VLUN_TYPE_HOST_SET = 5 VLUN_MULTIPATH_UNKNOWN = 1 VLUN_MULTIPATH_ROUND_ROBIN = 2 VLUN_MULTIPATH_FAILOVER = 3 CPG_RAID_R0 = 1 # RAID 0 CPG_RAID_R1 = 2 # RAID 1 CPG_RAID_R5 = 3 # RAID 5 CPG_RAID_R6 = 4 # RAID 6 CPG_HA_PORT = 1 # Support failure of a port. CPG_HA_CAGE = 2 # Support failure of a drive cage. CPG_HA_MAG = 3 # Support failure of a drive magazine. # Lowest numbered available chunklets, where transfer rate is the fastest. CPG_CHUNKLET_POS_PREF_FIRST = 1 # Highest numbered available chunklets, where transfer rate is the slowest. CPG_CHUNKLET_POS_PREF_LAST = 2 CPG_DISK_TYPE_FC = 1 # Fibre Channel CPG_DISK_TYPE_NL = 2 # Near Line CPG_DISK_TYPE_SSD = 3 # SSD HOST_EDIT_ADD = 1 HOST_EDIT_REMOVE = 2 HOST_PERSONA_GENERIC = 1 HOST_PERSONA_GENERIC_ALUA = 2 HOST_PERSONA_GENERIC_LEGACY = 3 HOST_PERSONA_HPUX_LEGACY = 4 HOST_PERSONA_AIX_LEGACY = 5 HOST_PERSONA_EGENERA = 6 HOST_PERSONA_ONTAP_LEGACY = 7 HOST_PERSONA_VMWARE = 8 HOST_PERSONA_OPENVMS = 9 HOST_PERSONA_HPUX = 10 HOST_PERSONA_WINDOWS_SERVER = 11 CHAP_OPERATION_MODE_INITIATOR = 1 CHAP_OPERATION_MODE_TARGET = 2 FLASH_CACHE_ENABLED = 1 FLASH_CACHE_DISABLED = 2 RC_ACTION_CHANGE_DIRECTION = 6 RC_ACTION_CHANGE_TO_PRIMARY = 7 RC_ACTION_MIGRATE_GROUP = 8 RC_ACTION_CHANGE_TO_SECONDARY = 9 RC_ACTION_CHANGE_TO_NATURUAL_DIRECTION = 10 RC_ACTION_OVERRIDE_FAIL_SAFE = 11 def __init__(self, api_url, debug=False, secure=False, timeout=None, suppress_ssl_warnings=False): self.api_url = api_url self.http = http.HTTPJSONRESTClient( self.api_url, secure=secure, timeout=timeout, suppress_ssl_warnings=suppress_ssl_warnings) api_version = None self.ssh = None self.vlun_query_supported = False self.primera_supported = False self.compression_supported = False self.debug_rest(debug) try: api_version = self.getWsApiVersion() except exceptions as ex: ex_desc = ex.get_description() if (ex_desc and ("Unable to find the server at" in ex_desc or "Only absolute URIs are allowed" in ex_desc)): raise exceptions.HTTPBadRequest(ex_desc) if (ex_desc and "SSL Certificate Verification Failed" in ex_desc): raise exceptions.SSLCertFailed() else: msg = ('Error: \'%s\' - Error communicating with the 3PAR WS. ' 'Check proxy settings. If error persists, either the ' '3PAR WS is not running or the version of the WS is ' 'not supported.') % ex_desc raise exceptions.UnsupportedVersion(msg) # Note the build contains major, minor, maintenance and build # e.g. 30102422 is 3 01 02 422 # therefore all we need to compare is the build if (api_version is None or api_version['build'] < self.HPE3PAR_WS_MIN_BUILD_VERSION): raise exceptions.UnsupportedVersion( 'Invalid 3PAR WS API, requires version, %s' % self.HPE3PAR_WS_MIN_BUILD_VERSION_DESC) # Check for VLUN query support. if (api_version['build'] >= self.HPE3PAR_WS_MIN_BUILD_VERSION_VLUN_QUERY): self.vlun_query_supported = True if (api_version['build'] >= self.HPE3PAR_WS_PRIMERA_MIN_BUILD_VERSION): self.primera_supported = True current_wsapi_version = '{}.{}.{}'.format(api_version.get('major'), api_version.get('minor'), api_version.get('revision')) if current_wsapi_version >= self.WSAPI_MIN_VERSION_COMPRESSION_SUPPORT: self.compression_supported = True def is_primera_array(self): return self.primera_supported def setSSHOptions(self, ip, login, password, port=22, conn_timeout=None, privatekey=None, **kwargs): """Set SSH Options for ssh calls. This is used to set the SSH credentials for calls that use SSH instead of REST HTTP. """ self.ssh = ssh.HPE3PARSSHClient(ip, login, password, port, conn_timeout, privatekey, **kwargs) def _run(self, cmd): if self.ssh is None: raise exceptions.SSHException('SSH is not initialized. Initialize' ' it by calling "setSSHOptions".') else: self.ssh.open() return self.ssh.run(cmd) def getWsApiVersion(self): """Get the 3PAR WS API version. :returns: Version dict """ try: # remove everything down to host:port host_url = self.api_url.split('/api') self.http.set_url(host_url[0]) # get the api version response, body = self.http.get('/api') return body finally: # reset the url self.http.set_url(self.api_url) def debug_rest(self, flag): """This is useful for debugging requests to 3PAR. :param flag: set to True to enable debugging :type flag: bool """ self.http.set_debug_flag(flag) if self.ssh: self.ssh.set_debug_flag(flag) def login(self, username, password, optional=None): """This authenticates against the 3PAR wsapi server and creates a session. :param username: The username :type username: str :param password: The Password :type password: str :returns: None """ self.http.authenticate(username, password, optional) def logout(self): """This destroys the session and logs out from the 3PAR server. The SSH connection to the 3PAR server is also closed. :returns: None """ self.http.unauthenticate() if self.ssh: self.ssh.close() def getStorageSystemInfo(self): """Get the Storage System Information :returns: Dictionary of Storage System Info """ response, body = self.http.get('/system') return body def getWSAPIConfigurationInfo(self): """Get the WSAPI Configuration Information. :returns: Dictionary of WSAPI configurations """ response, body = self.http.get('/wsapiconfiguration') return body def getOverallSystemCapacity(self): """Get the overall system capacity for the 3PAR server. :returns: Dictionary of system capacity information .. code-block:: python capacity = { "allCapacity": { # Overall system capacity # includes FC, NL, SSD # device types "totalMiB": 20054016, # Total system capacity # in MiB "allocated": { # Allocated space info "totalAllocatedMiB": 12535808, # Total allocated # capacity "volumes": { # Volume capacity info "totalVolumesMiB": 10919936, # Total capacity # allocated to volumes "nonCPGsMiB": 0, # Total non-CPG capacity "nonCPGUserMiB": 0, # The capacity allocated # to non-CPG user space "nonCPGSnapshotMiB": 0, # The capacity allocated # to non-CPG snapshot # volumes "nonCPGAdminMiB": 0, # The capacity allocated # to non-CPG # administrative volumes "CPGsMiB": 10919936, # Total capacity # allocated to CPGs "CPGUserMiB": 7205538, # User CPG space "CPGUserUsedMiB": 7092550, # The CPG allocated to # user space that is # in use "CPGUserUnusedMiB": 112988, # The CPG allocated to # user space that is not # in use "CPGSnapshotMiB": 2411870, # Snapshot CPG space "CPGSnapshotUsedMiB": 210256, # CPG allocated to # snapshot that is in use "CPGSnapshotUnusedMiB": 2201614, # CPG allocated to # snapshot space that is # not in use "CPGAdminMiB": 1302528, # Administrative volume # CPG space "CPGAdminUsedMiB": 115200, # The CPG allocated to # administrative space # that is in use "CPGAdminUnusedMiB": 1187328, # The CPG allocated to # administrative space # that is not in use "unmappedMiB": 0 # Allocated volume space # that is unmapped }, "system": { # System capacity info "totalSystemMiB": 1615872, # System space capacity "internalMiB": 780288, # The system capacity # allocated to internal # resources "spareMiB": 835584, # Total spare capacity "spareUsedMiB": 0, # The system capacity # allocated to spare resources # in use "spareUnusedMiB": 835584 # The system capacity # allocated to spare resources # that are unused } }, "freeMiB": 7518208, # Free capacity "freeInitializedMiB": 7518208, # Free initialized capacity "freeUninitializedMiB": 0, # Free uninitialized capacity "unavailableCapacityMiB": 0, # Unavailable capacity in MiB "failedCapacityMiB": 0 # Failed capacity in MiB }, "FCCapacity": { # System capacity from FC devices only ... # Same structure as above }, "NLCapacity": { # System capacity from NL devices only ... # Same structure as above }, "SSDCapacity": { # System capacity from SSD devices only ... # Same structure as above } } """ response, body = self.http.get('/capacity') return body # Volume methods def getVolumes(self): """Get the list of Volumes :returns: list of Volumes """ response, body = self.http.get('/volumes') return body def getVolume(self, name): """Get information about a volume. :param name: The name of the volume to find :type name: str :returns: volume :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - volume doesn't exist """ response, body = self.http.get('/volumes/%s' % name) return body def createVolume(self, name, cpgName, sizeMiB, optional=None): """Create a new volume. For the primera array there is support for only thin and DECO volume. To create DECO volume 'tdvv' and 'compression' both must be True. If only one of them is specified, it results in HTTPBadRequest. :param name: the name of the volume :type name: str :param cpgName: the name of the destination CPG :type cpgName: str :param sizeMiB: size in MiB for the volume :type sizeMiB: int :param optional: dict of other optional items :type optional: dict .. code-block:: python optional = { 'id': 12, # Volume ID. If not specified, next # available is chosen 'comment': 'some comment', # Additional information up to 511 # characters 'policies: { # Specifies VV policies 'staleSS': False, # True allows stale snapshots. 'oneHost': True, # True constrains volume export to # single host or host cluster 'zeroDetect': True, # True requests Storage System to # scan for zeros in incoming write # data 'system': False, # True special volume used by system # False is normal user volume 'caching': True}, # Read-only. True indicates write & # read caching & read ahead enabled 'snapCPG': 'CPG name', # CPG Used for snapshots 'ssSpcAllocWarningPct': 12, # Snapshot space allocation warning 'ssSpcAllocLimitPct': 22, # Snapshot space allocation limit 'tpvv': True, # True: Create TPVV # False (default) Create FPVV 'usrSpcAllocWarningPct': 22, # Enable user space allocation # warning 'usrSpcAllocLimitPct': 22, # User space allocation limit 'expirationHours': 256, # Relative time from now to expire # volume (max 43,800 hours) 'retentionHours': 256 # Relative time from now to retain # volume (max 43,800 hours) } :returns: List of Volumes :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT - Invalid Parameter :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - TOO_LARGE - Volume size above limit :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - NO_SPACE - Not Enough space is available :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - PERM_DENIED - Permission denied :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - EXISTENT_SV - Volume Exists already """ info = {'name': name, 'cpg': cpgName, 'sizeMiB': sizeMiB} # For primera array there is no compression and tdvv keys # removing tdvv, compression and # replacing compression+tdvv with reduce key for DECO if not optional and self.primera_supported: optional = {'tpvv': True} if optional: if self.primera_supported: for key in ['tpvv', 'compression', 'tdvv']: option = optional.get(key) if option and option not in [True, False]: # raising exception for junk compression input ex_desc = "39 - invalid input: wrong type for key "\ "[%s]. Valid values are [True, False]" % key raise exceptions.HTTPBadRequest(ex_desc) if optional.get('compression') is True: combination = ['tdvv', 'compression'] len_diff = len(set(combination) - set(optional.keys())) msg = "invalid input: For compressed and deduplicated "\ "volumes both 'compression' and " \ "'tdvv' must be specified as true" if len_diff == 1: raise exceptions.HTTPBadRequest(msg) if optional.get('tdvv') is True \ and optional.get('compression') is True: optional['reduce'] = True if optional.get('tdvv') is False \ and optional.get('compression') is True: raise exceptions.HTTPBadRequest(msg) else: msg = "invalid input: For compressed and deduplicated "\ "volumes both 'compression' and "\ "'tdvv' must be specified as true" if optional.get('tdvv') is False \ and optional.get('compression') is False: optional['reduce'] = False if optional.get('tdvv') is True \ and optional.get('compression') is False: raise exceptions.HTTPBadRequest(msg) if 'compression' in optional: optional.pop('compression') if 'tdvv' in optional: optional.pop('tdvv') info = self._mergeDict(info, optional) logger.debug("Parameters passed for create volume %s" % info) try: response, body = self.http.post('/volumes', body=info) return body except exceptions.HTTPBadRequest as ex: if self.primera_supported: ex_desc = 'invalid input: one of the parameters is required' ex_code = ex.get_code() # INV_INPUT_ONE_REQUIRED => 78 if ex_code == 78 and \ ex.get_description() == ex_desc and \ ex.get_ref() == 'tpvv,reduce': new_ex_desc = "invalid input: Either tpvv must be true "\ "OR for compressed and deduplicated "\ "volumes both 'compression' and 'tdvv' "\ "must be specified as true" raise exceptions.HTTPBadRequest(new_ex_desc) raise ex def deleteVolume(self, name): """Delete a volume. :param name: the name of the volume :type name: str :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - The volume does not exist :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - PERM_DENIED - Permission denied :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RETAINED - Volume retention time has not expired :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - HAS_RO_CHILD - Volume has read-only child :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - HAS_CHILD - The volume has a child volume :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - IN_USE - The volume is in use by VV set, VLUN, etc """ response, body = self.http.delete('/volumes/%s' % name) return body def modifyVolume(self, name, volumeMods, appType=None): """Modify a volume. :param name: the name of the volume :type name: str :param volumeMods: dictionary of volume attributes to change :type volumeMods: dict .. code-block:: python volumeMods = { 'newName': 'newName', # New volume name 'comment': 'some comment', # New volume comment 'snapCPG': 'CPG name', # Snapshot CPG name 'policies: { # Specifies VV policies 'staleSS': False, # True allows stale snapshots. 'oneHost': True, # True constrains volume export to # single host or host cluster 'zeroDetect': True, # True requests Storage System to # scan for zeros in incoming write # data 'system': False, # True special volume used by system # False is normal user volume 'caching': True}, # Read-only. True indicates write & # read caching & read ahead enabled 'ssSpcAllocWarningPct': 12, # Snapshot space allocation warning 'ssSpcAllocLimitPct': 22, # Snapshot space allocation limit 'tpvv': True, # True: Create TPVV # False: (default) Create FPVV 'usrSpcAllocWarningPct': 22, # Enable user space allocation # warning 'usrSpcAllocLimitPct': 22, # User space allocation limit 'userCPG': 'User CPG name', # User CPG name 'expirationHours': 256, # Relative time from now to expire # volume (max 43,800 hours) 'retentionHours': 256, # Relative time from now to retain # volume (max 43,800 hours) 'rmSsSpcAllocWarning': False, # True removes snapshot space # allocation warning. # False sets it when value > 0 'rmUsrSpcAllocWarwaning': False, # True removes user space # allocation warning. # False sets it when value > 0 'rmExpTime': False, # True resets expiration time to 0. # False sets it when value > 0 'rmSsSpcAllocLimit': False, # True removes snapshot space # allocation limit. # False sets it when value > 0 'rmUsrSpcAllocLimit': False # True removes user space # allocation limit. # False sets it when value > 0 } :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_WARN_GT_LIMIT - Allocation warning level is higher than the limit. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_USR_ALRT_NON_TPVV - User space allocation alerts are valid only with a TPVV. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_RETAIN_GT_EXPIRE - Retention time is greater than expiration time. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_VV_POLICY - Invalid policy specification (for example, caching or system is set to true). :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EXCEEDS_LENGTH - Invalid input: string length exceeds limit. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_TIME - Invalid time specified. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_MODIFY_USR_CPG_TPVV - usr_cpg cannot be modified on a TPVV. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - UNLICENSED_FEATURE - Retention time cannot be modified on a system without the Virtual Lock license. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - CPG_NOT_IN_SAME_DOMAIN - Snap CPG is not in the same domain as the user CPG. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_PEER_VOLUME - Cannot modify a peer volume. :raises: :class:`~hpe3parclient.exceptions.HTTPInternalServerError` - INT_SERV_ERR - Metadata of the VV is corrupted. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_SYS_VOLUME - Cannot modify retention time on a system volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_INTERNAL_VOLUME - Cannot modify an internal volume :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_VOLUME_NOT_DEFINED_ALL_NODES - Cannot modify a volume until the volume is defined on all volumes. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INVALID_OPERATION_VV_ONLINE_COPY_IN_PROGRESS - Cannot modify a volume when an online copy for that volume is in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INVALID_OPERATION_VV_VOLUME_CONV_IN_PROGRESS - Cannot modify a volume in the middle of a conversion operation. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INVALID_OPERATION_VV_SNAPSPACE_NOT_MOVED_TO_CPG - Snapshot space of a volume needs to be moved to a CPG before the user space. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_VOLUME_ACCOUNTING_IN_PROGRESS - The volume cannot be renamed until snapshot accounting has finished. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_ZERO_DETECT_TPVV - The zero_detect policy can be used only on TPVVs. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_CPG_ON_SNAPSHOT - CPG cannot be assigned to a snapshot. """ response = self.http.put('/volumes/%s' % name, body=volumeMods) if appType is not None: if 'newName' in volumeMods and volumeMods['newName']: name = volumeMods['newName'] try: self.setVolumeMetaData(name, 'hpe_ecosystem_product', appType) except Exception: pass return response def growVolume(self, name, amount): """Grow an existing volume by 'amount' Mebibytes. :param name: the name of the volume :type name: str :param amount: the additional size in MiB to add, rounded up to the next chunklet size (e.g. 256 or 1000 MiB) :type amount: int :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_NOT_IN_SAME_DOMAIN - The volume is not in the same domain. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - The volume does not exist. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_UNSUPPORTED_VV_TYPE - Invalid operation: Cannot grow this type of volume. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_TUNE_IN_PROGRESS - Invalid operation: Volume tuning is in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EXCEEDS_LENGTH - Invalid input: String length exceeds limit. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_VV_GROW_SIZE - Invalid grow size. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_NEW_SIZE_EXCEEDS_CPG_LIMIT - New volume size exceeds CPG limit :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_INTERNAL_VOLUME - This operation is not allowed on an internal volume. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_VOLUME_CONV_IN_PROGRESS - Invalid operation: VV conversion is in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_VOLUME_COPY_IN_PROGRESS - Invalid operation: online copy is in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_CLEANUP_IN_PROGRESS - Internal volume cleanup is in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IS_BEING_REMOVED - The volume is being removed. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IN_INCONSISTENT_STATE - The volume has an internal consistency error. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_SIZE_CANNOT_REDUCE - New volume size is smaller than the current size. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_NEW_SIZE_EXCEEDS_LIMITS - New volume size exceeds the limit. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_SA_SD_SPACE_REMOVED - Invalid operation: Volume SA/SD space is being removed. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_IS_BUSY - Invalid operation: Volume is currently busy. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_NOT_STARTED - Volume is not started. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_IS_PCOPY - Invalid operation: Volume is a physical copy. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_NOT_IN_NORMAL_STATE - Volume state is not normal :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_PROMOTE_IN_PROGRESS - Invalid operation: Volume promotion is in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_PARENT_OF_PCOPY - Invalid operation: Volume is the parent of physical copy. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - NO_SPACE - Insufficent space for requested operation. """ info = {'action': self.GROW_VOLUME, 'sizeMiB': int(amount)} response, body = self.http.put('/volumes/%s' % name, body=info) return body def promoteVirtualCopy(self, snapshot, optional=None): """Revert a volume to snapshot. :param snapshot: the snapshot name :type snapshot: str :param optional: Dictionary of optional params :type optional: dict .. code-block:: python optional = { 'online': False, # should execute promote # operation on online volume? 'allowRemoteCopyParent': 'False', # allow promote operation if # volume is in remote copy # volume group? 'priority': 1 # taskPriorityEnum (does not # apply to online copy) } :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_NOT_STARTED - Volume is not started. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IS_BEING_REMOVED - The volume is being removed. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IN_STALE_STATE - The volume is in a stale state. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_CANNOT_STOP_ONLINE_PROMOTE - The online promote cannot be stopped. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_BASE_VOLUME - The volume is a base volume. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_PCOPY_IN_PROGRESS - The destination volume has a physical copy in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_PARENT_PCOPY_IN_PROGRESS - The parent is involved in a physical copy. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_TUNE_IN_PROGRESS - Volume tuning is in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_IN_REMOTE_COPY - The volume is involved in Remote Copy. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_PARENT_VV_EXPORTED - Parent volume is exported. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_EXPORTED - Parent volume is exported. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_PROMOTE_TARGET_NOT_BASE_VV - The promote target is not a base volume. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_PARENT_SIZE_HAS_INCREASED - The parent volume size has increased. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_PARAM_CONFLICT - Parameters cannot be present at the same time. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_IS_BUSY - Volume is currently busy. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_PROMOTE_IN_PROGRESS - Volume promotion is in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_PROMOTE_IS_NOT_IN_PROGRESS - Volume promotion is not in progress. """ info = {'action': self.PROMOTE_VIRTUAL_COPY} if optional: info = self._mergeDict(info, optional) response, body = self.http.put('/volumes/%s' % snapshot, body=info) return body def copyVolume(self, src_name, dest_name, dest_cpg, optional=None): """Copy/Clone a volume. :param src_name: the source volume name :type src_name: str :param dest_name: the destination volume name :type dest_name: str :param dest_cpg: the destination CPG :type dest_cpg: str :param optional: Dictionary of optional params :type optional: dict .. code-block:: python optional = { 'online': False, # should physical copy be # performed online? 'tpvv': False, # use thin provisioned space # for destination # (online copy only) 'snapCPG': 'OpenStack_SnapCPG', # snapshot CPG for the # destination # (online copy only) 'saveSnapshot': False, # save the snapshot of the # source volume 'priority': 1 # taskPriorityEnum (does not # apply to online copy) } :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Invalid VV name or CPG name. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_CPG - The CPG does not exists. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - CPG_NOT_IN SAME_DOMAIN - The CPG is not in the current domain. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - The volume does not exist :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_NOT_IN_SAME_DOMAIN - The volume is not in the same domain. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_BAD_ENUM_VALUE - The priority value in not in the valid range(1-3). :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - EXISTENT_VOLUME - The volume already exists. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_SYS_VOLUME - The operation is not allowed on a system volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_NON_BASE_VOLUME - The destination volume is not a base volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_IN_REMOTE_COPY - The destination volume is involved in a remote copy. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_EXPORTED - The volume is exported. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_COPY_TO_SELF - The destination volume is the same as the parent. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_READONLY_SNAPSHOT - The parent volume is a read-only snapshot. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_COPY_TO_BASE - The destination volume is the base volume of a parent volume. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_VOLUME_CONV_IN_PROGRESS - The volume is in a conversion operation. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_NO_SNAPSHOT_ALLOWED - The parent volume must allow snapshots. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_ONLINE_COPY_IN_PROGRESS - The volume is the target of an online copy. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_CLEANUP_IN_PROGRESS - Cleanup of internal volume for the volume is in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_CIRCULAR_COPY - The parent volume is a copy of the destination volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_PEER_VOLUME - The operation is not allowed on a peer volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_INTERNAL_VOLUME - The operation is not allowed on an internal volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IS_BEING_REMOVED - The volume is being removed. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_NOT_IN_NORMAL_STATE - The volume is not in the normal state. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IN_INCONSISTENT_STATE - The volume has an internal consistency error. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_PCOPY_IN_PROGRESS - The destination volume has a physical copy in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_FAILED_ONLINE_COPY - Online copying of the destination volume has failed. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_COPY_PARENT_TOO_BIG - The size of the parent volume is larger than the size of the destination volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_NO_PARENT - The volume has no physical parent. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - IN_USE - The resynchronization snapshot is in a stale state. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IN_STALE_STATE - The volume is in a stale state. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VVCOPY - Physical copy not found. """ # Virtual volume sets are not supported with the -online option parameters = {'destVolume': dest_name, 'destCPG': dest_cpg} # For online copy, there has to be tpvv/tdvv(Non primera array) # and tpvv/compression(primera array) has to be passed from caller side # For offline copy, parameters tpvv/tdvv/compression are invalid, # has to be taken care by caller side if optional: if self.primera_supported: for key in ['tpvv', 'compression', 'tdvv']: option = optional.get(key) if option and option not in [True, False]: # raising exception for junk compression input ex_desc = "39 - invalid input: wrong type for key " \ "[%s]. Valid values are [True, False]" % key raise exceptions.HTTPBadRequest(ex_desc) if optional.get('compression') is True: combination = ['tdvv', 'compression'] len_diff = len(set(combination) - set(optional.keys())) msg = "invalid input: For compressed and deduplicated "\ "volumes both 'compression' and " \ "'tdvv' must be specified as true" if len_diff == 1: raise exceptions.HTTPBadRequest(msg) if optional.get('tdvv') is True \ and optional.get('compression') is True: optional['reduce'] = True if optional.get('tdvv') is False \ and optional.get('compression') is True: raise exceptions.HTTPBadRequest(msg) else: msg = "invalid input: For compressed and deduplicated "\ "volumes both 'compression' and "\ "'tdvv' must be specified as true" if optional.get('tdvv') is False \ and optional.get('compression') is False: optional['reduce'] = False if optional.get('tdvv') is True \ and optional.get('compression') is False: raise exceptions.HTTPBadRequest(msg) if 'compression' in optional: optional.pop('compression') if 'tdvv' in optional: optional.pop('tdvv') parameters = self._mergeDict(parameters, optional) if 'online' not in parameters or not parameters['online']: # 3Par won't allow destCPG to be set if it's not an online copy. parameters.pop('destCPG', None) info = {'action': 'createPhysicalCopy', 'parameters': parameters} logger.debug("Parameters passed for copy volume %s" % info) try: response, body = self.http.post('/volumes/%s' % src_name, body=info) return body except exceptions.HTTPBadRequest as ex: if self.primera_supported: ex_desc = 'invalid input: one of the parameters is required' ex_code = ex.get_code() # INV_INPUT_ONE_REQUIRED => 78 if ex_code == 78 and \ ex.get_description() == ex_desc and \ ex.get_ref() == 'tpvv,reduce': new_ex_desc = "invalid input: Either tpvv must be true "\ "OR for compressed and deduplicated "\ "volumes both 'compression' and 'tdvv' "\ "must be specified as true." raise exceptions.HTTPBadRequest(new_ex_desc) raise ex def isOnlinePhysicalCopy(self, name): """Is the volume being created by process of online copy? :param name: the name of the volume :type name: str """ task = self._findTask(name, active=True) if task is None: return False else: return True def stopOnlinePhysicalCopy(self, name): """Stopping a online physical copy operation. :param name: the name of the volume :type name: str """ # first we have to find the active copy task = self._findTask(name) task_id = None if task is None: # couldn't find the task self.deleteVolume(name) msg = "Couldn't find the copy task for '%s'" % name raise exceptions.HTTPNotFound(error={'desc': msg}) else: task_id = task[0] # now stop the copy if task_id is not None: self._cancelTask(task_id) else: self.deleteVolume(name) msg = "Couldn't find the copy task for '%s'" % name raise exceptions.HTTPNotFound(error={'desc': msg}) # we have to make sure the task is cancelled # before moving on. This can sometimes take a while. ready = False while not ready: time.sleep(1) task = self._findTask(name, True) if task is None: ready = True # now cleanup the dead snapshots vol = self.getVolume(name) if vol: if 'copyOf' in vol: snap1 = self.getVolume(vol['copyOf']) snap2 = self.getVolume(snap1['copyOf']) self.deleteVolume(name) if 'copyOf' in vol: self.deleteVolume(snap1['name']) self.deleteVolume(snap2['name']) def getAllTasks(self): """Get the list of all Tasks :returns: list of all Tasks """ response, body = self.http.get('/tasks') return body def getTask(self, taskId): """Get the status of a task. :param taskId: the task id :type taskId: int :returns: the status of the task :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_BELOW_RANGE - Bad Request Task ID must be a positive value. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EXCEEDS_RANGE - Bad Request Task ID is too large. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_TASK - Task with the specified task ID does not exist. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_WRONG_TYPE - Task ID is not an integer. """ response, body = self.http.get('/tasks/%s' % taskId) return body def _findTask(self, name, active=True): uri = '/tasks' response, body = self.http.get(uri) task_type = {1: 'vv_copy', 2: 'phys_copy_resync', 3: 'move_regions', 4: 'promote_sv', 5: 'remote_copy_sync', 6: 'remote_copy_reverse', 7: 'remote_copy_failover', 8: 'remote_copy_recover', 18: 'online_vv_copy'} status = {1: 'done', 2: 'active', 3: 'cancelled', 4: 'failed'} priority = {1: 'high', 2: 'med', 3: 'low'} for task_obj in body['members']: if(task_obj['name'] == name): if(active and task_obj['status'] != 2): # if active flag is True, but status of task is not True # then it means task got completed/cancelled/failed return None task_details = [] task_details.append(task_obj['id']) value = task_obj['type'] if value in task_type: type_str = task_type[value] else: type_str = 'n/a' task_details.append(type_str) task_details.append(task_obj['name']) value = task_obj['status'] task_details.append(status[value]) # Phase and Step feilds are not found task_details.append('---') task_details.append('---') task_details.append(task_obj['startTime']) task_details.append(task_obj['finishTime']) if('priority' in task_obj): value = task_obj['priority'] task_details.append(priority[value]) else: task_details.append('n/a') task_details.append(task_obj['user']) return task_details return None def _convert_cli_output_to_collection_like_wsapi(self, cli_output): return HPE3ParClient.convert_cli_output_to_wsapi_format(cli_output) def getPatches(self, history=True): """Get all the patches currently affecting the system. :param history: Specify the history of all patches and updates applied to the system. :returns: dict with total and members (see convert_cli_output_to_collection_like_wsapi()) """ cmd = ['showpatch'] if history: cmd.append('-hist') return self._convert_cli_output_to_collection_like_wsapi( self._run(cmd)) def getPatch(self, patch_id): """Get details on a specified patch ID if it has been applied to the system. :param patch_id: The ID of the patch. :returns: list of str (raw lines of CLI output as strings) """ return self._run(['showpatch', '-d', patch_id]) def stopOfflinePhysicalCopy(self, name): """Stopping a offline physical copy operation. :param name: the name of the volume :type name: str :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Invalid VV name or CPG name. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_CPG - The CPG does not exists. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - CPG_NOT_IN SAME_DOMAIN - The CPG is not in the current domain. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - The volume does not exist :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_NOT_IN_SAME_DOMAIN - The volume is not in the same domain. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_BAD_ENUM_VALUE - The priority value in not in the valid range(1-3). :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - EXISTENT_VOLUME - The volume already exists. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_SYS_VOLUME - The operation is not allowed on a system volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_NON_BASE_VOLUME - The destination volume is not a base volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_IN_REMOTE_COPY - The destination volume is involved in a remote copy. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_EXPORTED - The volume is exported. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_COPY_TO_SELF - The destination volume is the same as the parent. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_READONLY_SNAPSHOT - The parent volume is a read-only snapshot. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_COPY_TO_BASE - The destination volume is the base volume of a parent volume. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_VOLUME_CONV_IN_PROGRESS - The volume is in a conversion operation. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_NO_SNAPSHOT_ALLOWED - The parent volume must allow snapshots. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_ONLINE_COPY_IN_PROGRESS - The volume is the target of an online copy. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_CLEANUP_IN_PROGRESS - Cleanup of internal volume for the volume is in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_CIRCULAR_COPY - The parent volume is a copy of the destination volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_PEER_VOLUME - The operation is not allowed on a peer volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_INTERNAL_VOLUME - The operation is not allowed on an internal volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IS_BEING_REMOVED - The volume is being removed. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_NOT_IN_NORMAL_STATE - The volume is not in the normal state. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IN_INCONSISTENT_STATE - The volume has an internal consistency error. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_PCOPY_IN_PROGRESS - The destination volume has a physical copy in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_FAILED_ONLINE_COPY - Online copying of the destination volume has failed. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_COPY_PARENT_TOO_BIG - The size of the parent volume is larger than the size of the destination volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_NO_PARENT - The volume has no physical parent. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - IN_USE - The resynchronization snapshot is in a stale state. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IN_STALE_STATE - The volume is in a stale state. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VVCOPY - Physical copy not found. """ info = {'action': self.STOP_PHYSICAL_COPY} response, body = self.http.put('/volumes/%s' % name, body=info) return body def createSnapshot(self, name, copyOfName, optional=None): """Create a snapshot of an existing Volume. :param name: Name of the Snapshot :type name: str :param copyOfName: The volume you want to snapshot :type copyOfName: str :param optional: Dictionary of optional params :type optional: dict .. code-block:: python optional = { 'id': 12, # Specifies the ID of the volume, # next by default 'comment': "some comment", 'readOnly': True, # Read Only 'expirationHours': 36, # time from now to expire 'retentionHours': 12 # time from now to expire } :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - The volume does not exist :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - PERM_DENIED - Permission denied """ parameters = {'name': name} if optional: parameters = self._mergeDict(parameters, optional) info = {'action': 'createSnapshot', 'parameters': parameters} response, body = self.http.post('/volumes/%s' % copyOfName, body=info) return body # Host Set methods def findHostSet(self, name): """ Find the Host Set name for a host. :param name: the host name :type name: str """ host_set_name = None # If ssh isn't available search all host sets for this host if self.ssh is None: host_sets = self.getHostSets() if host_sets is not None and 'members' in host_sets: for host_set in host_sets['members']: if 'setmembers' in host_set: for host_name in host_set['setmembers']: if host_name == name: return host_set['name'] # Using ssh we can ask for the host set for this host else: cmd = ['showhostset', '-host', name] out = self._run(cmd) host_set_name = None if out and len(out) > 1: info = out[1].split(",") host_set_name = info[1] return host_set_name def getHostSets(self): """ Get information about every Host Set on the 3Par array :returns: list of Host Sets """ response, body = self.http.get('/hostsets') return body def getHostSet(self, name): """ Get information about a Host Set :param name: The name of the Host Set to find :type name: str :returns: host set dict :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_SET - The set does not exist """ response, body = self.http.get('/hostsets/%s' % name) return body def createHostSet(self, name, domain=None, comment=None, setmembers=None): """ This creates a new host set :param name: the host set to create :type set_name: str :param domain: the domain where the set lives :type domain: str :param comment: a comment for the host set :type comment: str :param setmembers: the hosts to add to the host set, the existence of the host will not be checked :type setmembers: list of str :returns: id of host set created :rtype: str :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - EXISTENT_SET - The set already exits. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - MEMBER_IN_DOMAINSET - The host is in a domain set. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - MEMBER_IN_SET - The object is already part of the set. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - MEMBER_NOT_IN_SAME_DOMAIN - Objects must be in the same domain to perform this operation. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_HOST - The host does not exists. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_DUP_NAME - Invalid input (duplicate name). """ info = {'name': name} if domain: info['domain'] = domain if comment: info['comment'] = comment if setmembers: members = {'setmembers': setmembers} info = self._mergeDict(info, members) response, body = self.http.post('/hostsets', body=info) if response is not None and 'location' in response: host_set_id = response['location'].rsplit( '/api/v1/hostsets/', 1)[-1] return host_set_id else: return None def deleteHostSet(self, name): """ This removes a host set. :param name: the host set to remove :type name: str :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_SET - The set does not exists. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - EXPORTED_VLUN - The host set has exported VLUNs. """ self.http.delete('/hostsets/%s' % name) def modifyHostSet(self, name, action=None, newName=None, comment=None, setmembers=None): """ This modifies a host set by adding or removing a hosts from the set. It's action is based on the enums SET_MEM_ADD or SET_MEM_REMOVE. :param name: the host set name :type name: str :param action: add or remove host(s) from the set :type action: enum :param newName: new name of set :type newName: str :param comment: new comment for the set :type comment: str :param setmembers: the host(s) to add to the set, the existence of the host(s) will not be checked :type setmembers: list str :returns: headers - dict of HTTP Response headers. Upon successful modification of a host set HTTP code 200 OK is returned and the URI of the updated host set will be returned in the location portion of the headers. :returns: body - the body of the response. None if successful. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - EXISTENT_SET - The set already exits. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_SET - The set does not exists. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - MEMBER_IN_DOMAINSET - The host is in a domain set. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - MEMBER_IN_SET - The object is already part of the set. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - MEMBER_NOT_IN_SET - The object is not part of the set. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - MEMBER_NOT_IN_SAME_DOMAIN - Objects must be in the same domain to perform this operation. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_DUP_NAME - Invalid input (duplicate name). :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_PARAM_CONFLICT - Invalid input (parameters cannot be present at the same time). :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Invalid contains one or more illegal characters. """ info = {} if action: info['action'] = action if newName: info['newName'] = newName if comment: info['comment'] = comment if setmembers: members = {'setmembers': setmembers} info = self._mergeDict(info, members) response = self.http.put('/hostsets/%s' % name, body=info) return response def addHostToHostSet(self, set_name, name): """ This adds a host to a host set. :param set_name: the host set name :type set_name: str :param name: the host name to add :type name: str :returns: headers - dict of HTTP Response headers. Upon successful modification of a host set HTTP code 200 OK is returned and the URI of the updated host set will be returned in the location portion of the headers. :returns: body - the body of the response. None if successful. """ return self.modifyHostSet(set_name, action=self.SET_MEM_ADD, setmembers=[name]) def removeHostFromHostSet(self, set_name, name): """ Remove a host from a host set. :param set_name: the host set name :type set_name: str :param name: the host name to remove :type name: str :returns: headers - dict of HTTP Response headers. Upon successful modification of a host set HTTP code 200 OK is returned and the URI of the updated host set will be returned in the location portion of the headers. :returns: body - the body of the response. None if successful. """ return self.modifyHostSet(set_name, action=self.SET_MEM_REMOVE, setmembers=[name]) def removeHostFromItsHostSet(self, name): """ Remove a host from its host set if it is a member of one. :param name: the host name to remove :type name: str :returns: None if host has no host set, else (headers, body) :returns: headers - dict of HTTP Response headers. Upon successful modification of a host set HTTP code 200 OK is returned and the URI of the updated host set will be returned in the location portion of the headers. :returns: body - the body of the response. None if successful. """ host_set_name = self.findHostSet(name) if host_set_name is None: return None return self.removeHostFromHostSet(host_set_name, name) def getHosts(self): """Get information about every Host on the 3Par array. :returns: list of Hosts """ response, body = self.http.get('/hosts') return body def getHost(self, name): """Get information about a Host. :param name: The name of the Host to find :type name: str :returns: host dict :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_HOST - HOST doesn't exist """ response, body = self.http.get('/hosts/%s' % name) return body def createHost(self, name, iscsiNames=None, FCWwns=None, optional=None): """Create a new Host entry. :param name: The name of the host :type name: str :param iscsiNames: Array if iscsi iqns :type name: array :param FCWwns: Array if Fibre Channel World Wide Names :type name: array :param optional: The optional stuff :type optional: dict .. code-block:: python optional = { 'persona': 1, # ID of the persona to assign # to the host. # 3.1.3 default: Generic-ALUA # 3.1.2 default: General 'domain': 'myDomain', # Create the host in the # specified domain, or default # domain if unspecified. 'forceTearDown': False, # If True, force to tear down # low-priority VLUN exports. 'descriptors': {'location': 'earth', # The host's location 'IPAddr': '10.10.10.10', # The host's IP address 'os': 'linux', # The operating system running # on the host. 'model': 'ex', # The host's model 'contact': 'Smith', # The host's owner and contact 'comment': "Joe's box"} # Additional host information } :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - PERM_DENIED - Permission denied :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_MISSING_REQUIRED - Name not specified. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_PARAM_CONFLICT - FCWWNs and iSCSINames are both specified. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EXCEEDS_LENGTH - Host name, domain name, or iSCSI name is too long. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EMPTY_STR - Input string (for domain name, iSCSI name, etc.) is empty. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Any error from host-name or domain-name parsing. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_TOO_MANY_WWN_OR_iSCSI - More than 1024 WWNs or iSCSI names are specified. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_WRONG_TYPE - The length of WWN is not 16. WWN specification contains non-hexadecimal digit. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - EXISTENT_PATH - host WWN/iSCSI name already used by another host :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - EXISTENT_HOST - host name is already used. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - NO_SPACE - No space to create host. """ info = {'name': name} if iscsiNames: iscsi = {'iSCSINames': iscsiNames} info = self._mergeDict(info, iscsi) if FCWwns: fc = {'FCWWNs': FCWwns} info = self._mergeDict(info, fc) if optional: info = self._mergeDict(info, optional) response, body = self.http.post('/hosts', body=info) return body def modifyHost(self, name, mod_request): """Modify an existing Host entry. :param name: The name of the host :type name: str :param mod_request: Objects for Host Modification Request :type mod_request: dict .. code-block:: python mod_request = { 'newName': 'myNewName', # New name of the host 'pathOperation': 1, # If adding, adds the WWN or # iSCSI name to the existing # host. 'FCWWNs': [], # One or more WWN to set for # the host. 'iSCSINames': [], # One or more iSCSI names to # set for the host. 'forcePathRemoval': False, # If True, remove SSN(s) or # iSCSI(s) even if there are # VLUNs exported to host 'persona': 1, # ID of the persona to modify # the host's persona to. 'descriptors': {'location': 'earth', # The host's location 'IPAddr': '10.10.10.10', # The host's IP address 'os': 'linux', # The operating system running # on the host. 'model': 'ex', # The host's model 'contact': 'Smith', # The host's owner and contact 'comment': 'Joes box'} # Additional host information 'chapOperation': HOST_EDIT_ADD, # Add or remove 'chapOperationMode': CHAP_INITIATOR, # Initator or target 'chapName': 'MyChapName', # The chap name 'chapSecret': 'xyz', # The chap secret for the host # or the target 'chapSecretHex': False, # If True, the chapSecret is # treated as Hex. 'chapRemoveTargetOnly': True # If True, then remove target # chap only } :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT - Missing host name. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_PARAM_CONFLICT - Both iSCSINames & FCWWNs are specified. (lot of other possibilities) :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ONE_REQUIRED - iSCSINames or FCWwns missing. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ONE_REQUIRED - No path operation specified. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_BAD_ENUM_VALUE - Invalid enum value. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_MISSING_REQUIRED - Required fields missing. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EXCEEDS_LENGTH - Host descriptor argument length, new host name, or iSCSI name is too long. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Error parsing host or iSCSI name. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - EXISTENT_HOST - New host name is already used. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_HOST - Host to be modified does not exist. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_TOO_MANY_WWN_OR_iSCSI - More than 1024 WWNs or iSCSI names are specified. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_WRONG_TYPE - Input value is of the wrong type. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - EXISTENT_PATH - WWN or iSCSI name is already claimed by other host. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_BAD_LENGTH - CHAP hex secret length is not 16 bytes, or chap ASCII secret length is not 12 to 16 characters. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NO_INITIATOR_CHAP - Setting target CHAP without initiator CHAP. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_CHAP - Remove non-existing CHAP. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - NON_UNIQUE_CHAP_SECRET - CHAP secret is not unique. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - EXPORTED_VLUN - Setting persona with active export; remove a host path on an active export. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - NON_EXISTENT_PATH - Remove a non-existing path. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - LUN_HOSTPERSONA_CONFLICT - LUN number and persona capability conflict. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_DUP_PATH - Duplicate path specified. """ response = self.http.put('/hosts/%s' % name, body=mod_request) return response def deleteHost(self, name): """Delete a Host. :param name: Host Name :type name: str :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_HOST - HOST Not Found :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - IN_USE - The HOST Cannot be removed because it's in use. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - PERM_DENIED - Permission denied """ response, body = self.http.delete('/hosts/%s' % name) def findHost(self, iqn=None, wwn=None): """Find a host from an iSCSI initiator or FC WWN. :param iqn: lookup based on iSCSI initiator :type iqn: str :param wwn: lookup based on WWN :type wwn: str """ # for now there is no search in the REST API # so we can do a create looking for a specific # error. If we don't get that error, we nuke the # fake host. def _hostname(): # create a safe, random hostname that won't # create a collision when findHost is called # in parallel, before the temp host is removed. uuid_str = str(uuid.uuid4()).replace("-", "")[:20] return uuid_str cmd = ['createhost'] # create a random hostname hostname = _hostname() if iqn: cmd.append('-iscsi') cmd.append(hostname) if iqn: cmd.append(iqn) else: cmd.append(wwn) result = self._run(cmd) test = ' '.join(result) search_str = "already used by host " if search_str in test: # host exists, return name used by 3par hostname_3par = self._get_next_word(test, search_str) return hostname_3par else: # host creation worked...so we need to remove it. # this means we didn't find an existing host that # is using the iqn or wwn. self.deleteHost(hostname) return None def queryHost(self, iqns=None, wwns=None): """Find a host from an iSCSI initiator or FC WWN. :param iqns: lookup based on iSCSI initiator list :type iqns: list :param wwns: lookup based on WWN list :type wwns: list :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT - Invalid URI syntax. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_HOST - HOST Not Found :raises: :class:`~hpe3parclient.exceptions.HTTPInternalServerError` - INTERNAL_SERVER_ERR - Internal server error. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Host name contains invalid character. """ wwnsQuery = '' if wwns: tmpQuery = [] for wwn in wwns: tmpQuery.append('wwn==%s' % wwn) wwnsQuery = ('FCPaths[%s]' % ' OR '.join(tmpQuery)) iqnsQuery = '' if iqns: tmpQuery = [] for iqn in iqns: tmpQuery.append('name==%s' % iqn) iqnsQuery = ('iSCSIPaths[%s]' % ' OR '.join(tmpQuery)) query = '' if wwnsQuery and iqnsQuery: query = ('%(wwns)s OR %(iqns)s' % ({'wwns': wwnsQuery, 'iqns': iqnsQuery})) elif wwnsQuery: query = wwnsQuery elif iqnsQuery: query = iqnsQuery query = '"%s"' % query response, body = self.http.get('/hosts?query=%s' % quote(query.encode("utf8"))) return body def getHostVLUNs(self, hostName): """Get all of the VLUNs on a specific Host. :param hostName: Host name :type hostNane: str :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_HOST - HOST Not Found """ # calling getHost to see if the host exists and raise not found # exception if it's not found. self.getHost(hostName) vluns = [] # Check if the WSAPI supports VLUN querying. If it is supported # request only the VLUNs that are associated with the host. if self.vlun_query_supported: query = '"hostname EQ %s"' % hostName response, body = self.http.get('/vluns?query=%s' % quote(query.encode("utf8"))) for vlun in body.get('members', []): vluns.append(vlun) else: allVLUNs = self.getVLUNs() if allVLUNs: for vlun in allVLUNs['members']: if 'hostname' in vlun and vlun['hostname'] == hostName: vluns.append(vlun) if len(vluns) < 1: raise exceptions.HTTPNotFound( {'code': 'NON_EXISTENT_VLUNS', 'desc': "No VLUNs for host '%s' found" % hostName}) return vluns # PORT Methods def getPorts(self): """Get the list of ports on the 3PAR. :returns: list of Ports """ response, body = self.http.get('/ports') # if any of the ports are iSCSI ports and # are vlan tagged (as shown by showport -iscsivlans), then # the port information is merged with the WSAPI # returned port information. if self.ssh is not None: if any([port['protocol'] == self.PORT_PROTO_ISCSI and 'iSCSIPortInfo' in port and port['iSCSIPortInfo']['vlan'] == 1 for port in body['members']]): iscsi_vlan_data = self._run(['showport', '-iscsivlans']) port_parser = showport_parser.ShowportParser() iscsi_ports = port_parser.parseShowport(iscsi_vlan_data) expanded_ports = self._cloneISCSIPorts(body, iscsi_ports) for cli_port in expanded_ports: for wsapi_port in body[u'members']: if wsapi_port['portPos']['node'] == \ cli_port['portPos']['node'] \ and wsapi_port['portPos']['slot'] == \ cli_port['portPos']['slot'] \ and wsapi_port['portPos']['cardPort'] == \ cli_port['portPos']['cardPort']: port_parser._merge_dict(wsapi_port, cli_port) body['total'] = len(body['members']) return body def _getProtocolPorts(self, protocol, state=None): return_ports = [] ports = self.getPorts() if ports: for port in ports['members']: if port['protocol'] == protocol: if state is None: return_ports.append(port) elif port['linkState'] == state: return_ports.append(port) return return_ports def _cloneISCSIPorts(self, real_ports, vlan_ports): cloned_ports = [] for port in vlan_ports: matching_ports = [ x for x in real_ports['members'] if (x['protocol'] == self.PORT_PROTO_ISCSI and x['iSCSIPortInfo']['vlan'] == 1 and x['portPos'] == port['portPos']) ] # should only be one if len(matching_ports) > 1: err = ("Found {} matching ports for vlan tagged iSCSI port " "{}. There should only be one.") raise exceptions.\ NoUniqueMatch(err.format(len(matching_ports), port)) if len(matching_ports) == 1: new_port = copy.deepcopy(matching_ports[0]) new_port.update(port) cloned_ports.append(new_port) return cloned_ports def getFCPorts(self, state=None): """Get a list of Fibre Channel Ports. :returns: list of Fibre Channel Ports """ return self._getProtocolPorts(1, state) def getiSCSIPorts(self, state=None): """Get a list of iSCSI Ports. :returns: list of iSCSI Ports """ return self._getProtocolPorts(2, state) def getIPPorts(self, state=None): """Get a list of IP Ports. :returns: list of IP Ports """ return self._getProtocolPorts(4, state) # CPG methods def getCPGs(self): """Get entire list of CPGs. :returns: list of cpgs """ response, body = self.http.get('/cpgs') return body def getCPG(self, name): """Get information about a CPG. :param name: The name of the CPG to find :type name: str :returns: cpg dict :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_CPG - CPG doesn't exist """ response, body = self.http.get('/cpgs/%s' % name) return body def getCPGAvailableSpace(self, name): """Get available space information about a CPG. :param name: The name of the CPG to find :type name: str :returns: Available space dict .. code-block:: python info = { "rawFreeMiB": 1000000, # Raw free capacity in MiB "usableFreeMiB": 5000 # LD free capacity in MiB } :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_CPG - CPG Not Found """ info = {'cpg': name} response, body = self.http.post('/spacereporter', body=info) return body def createCPG(self, name, optional=None): """Create a CPG. :param name: CPG Name :type name: str :param optional: Optional parameters :type optional: dict .. code-block:: python optional = { 'growthIncrementMiB': 100, # Growth increment in MiB for # each auto-grown operation 'growthLimitMiB': 1024, # Auto-grow operation is limited # to specified storage amount 'usedLDWarningAlertMiB': 200, # Threshold to trigger warning # of used logical disk space 'domain': 'MyDomain', # Name of the domain object 'LDLayout': { 'RAIDType': 1, # Disk Raid Type 'setSize': 100, # Size in number of chunklets 'HA': 0, # Layout supports failure of # one port pair (1), # one cage (2), # or one magazine (3) 'chunkletPosPref': 2, # Chunklet location perference # characteristics. # Lowest Number/Fastest transfer # = 1 # Higher Number/Slower transfer # = 2 'diskPatterns': []} # Patterns for candidate disks } :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT Invalid URI Syntax. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - NON_EXISTENT_DOMAIN - Domain doesn't exist. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - NO_SPACE - Not Enough space is available. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - BAD_CPG_PATTERN A Pattern in a CPG specifies illegal values. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - PERM_DENIED - Permission denied :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - EXISTENT_CPG - CPG Exists already """ info = {'name': name} if optional: if self.primera_supported: for key, value in dict(optional).items(): if key == 'LDLayout': ldlayout = value for keys, val in dict(ldlayout).items(): if keys == 'setSize' or \ (keys == 'RAIDType' and ldlayout.get('RAIDType') == 1): ldlayout.pop(keys) info = self._mergeDict(info, optional) response, body = self.http.post('/cpgs', body=info) return body def deleteCPG(self, name): """Delete a CPG. :param name: CPG Name :type name: str :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_CPG - CPG Not Found :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - IN_USE - The CPG Cannot be removed because it's in use. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - PERM_DENIED - Permission denied """ response, body = self.http.delete('/cpgs/%s' % name) # VLUN methods # # Virtual-LUN, or VLUN, is a pairing between a virtual volume and a # logical unit number (LUN), expressed as either a VLUN template or # an active VLUN. # A VLUN template sets up an association between a virtual volume and a # LUN-host, LUN-port, or LUN-host-port combination by establishing the # export rule or the manner in which the Volume is exported. def getVLUNs(self): """Get VLUNs. :returns: Array of VLUNs """ response, body = self.http.get('/vluns') return body def getVLUN(self, volumeName): """Get information about a VLUN. :param volumeName: The volume name of the VLUN to find :type name: str :returns: VLUN :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VLUN - VLUN doesn't exist """ # Check if the WSAPI supports VLUN querying. If it is supported # request only the VLUNs that are associated with the volume. if self.vlun_query_supported: query = '"volumeName EQ %s"' % volumeName response, body = self.http.get('/vluns?query=%s' % quote(query.encode("utf8"))) # Return the first VLUN found for the volume. for vlun in body.get('members', []): return vlun else: vluns = self.getVLUNs() if vluns: for vlun in vluns['members']: if vlun['volumeName'] == volumeName: return vlun raise exceptions.HTTPNotFound({'code': 'NON_EXISTENT_VLUN', 'desc': "VLUN '%s' was not found" % volumeName}) def createVLUN(self, volumeName, lun=None, hostname=None, portPos=None, noVcn=None, overrideLowerPriority=None, auto=False): """Create a new VLUN. When creating a VLUN, the volumeName is required. The lun member is not required if auto is set to True. Either hostname or portPos (or both in the case of matched sets) is also required. The noVcn and overrideLowerPriority members are optional. :param volumeName: Name of the volume to be exported :type volumeName: str :param lun: The new LUN id :type lun: int :param hostname: Name of the host which the volume is to be exported. :type hostname: str :param portPos: 'portPos' (dict) - System port of VLUN exported to. It includes node number, slot number, and card port number :type portPos: dict .. code-block:: python portPos = {'node': 1, # System node (0-7) 'slot': 2, # PCI bus slot in the node (0-5) 'port': 1} # Port number on the FC card (0-4) :param noVcn: A VLUN change notification (VCN) not be issued after export (-novcn). Default: False. :type noVcn: bool :param overrideLowerPriority: Existing lower priority VLUNs will be overridden (-ovrd). Use only if hostname member exists. Default: False. :type overrideLowerPriority: bool :returns: the location of the VLUN """ info = {'volumeName': volumeName} if lun is not None: info['lun'] = lun if hostname: info['hostname'] = hostname if portPos: info['portPos'] = portPos if noVcn: info['noVcn'] = noVcn if overrideLowerPriority: info['overrideLowerPriority'] = overrideLowerPriority if auto: info['autoLun'] = True info['maxAutoLun'] = 0 info['lun'] = 0 headers, body = self.http.post('/vluns', body=info) if headers: location = headers['location'].replace('/api/v1/vluns/', '') return location else: return None def deleteVLUN(self, volumeName, lunID, hostname=None, port=None): """Delete a VLUN. :param volumeName: the volume name of the VLUN :type name: str :param lunID: The LUN ID :type lunID: int :param hostname: Name of the host which the volume is exported. For VLUN of port type,the value is empty :type hostname: str :param port: Specifies the system port of the VLUN export. It includes the system node number, PCI bus slot number, and card port number on the FC card in the format :: :type port: dict .. code-block:: python port = {'node': 1, # System node (0-7) 'slot': 2, # PCI bus slot in the node (0-5) 'port': 1} # Port number on the FC card (0-4) :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_MISSING_REQUIRED - Incomplete VLUN info. Missing volumeName or lun, or both hostname and port. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_PORT_SELECTION - Specified port is invalid. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EXCEEDS_RANGE - The LUN specified exceeds expected range. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_HOST - The host does not exist :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VLUN - The VLUN does not exist :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_PORT - The port does not exist :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - PERM_DENIED - Permission denied """ vlun = "%s,%s" % (volumeName, lunID) if hostname: vlun += ",%s" % hostname else: if port: vlun += "," if port: vlun += ",%s:%s:%s" % (port['node'], port['slot'], port['cardPort']) response, body = self.http.delete('/vluns/%s' % vlun) # VolumeSet methods def findVolumeSet(self, name): """ Find the first Volume Set that contains a target volume. If a volume set other than the first one found is desired use findAllVolumeSets and search the results. :param name: the volume name :type name: str :returns: The name of the first volume set that contains the target volume, otherwise None. """ vvset_names = self.findAllVolumeSets(name) vvset_name = None if vvset_names: vvset_name = vvset_names[0]['name'] return vvset_name def findAllVolumeSets(self, name): """ Return a list of every Volume Set the given volume is a part of. The list can contain zero, one, or multiple items. :param name: the volume name :type name: str :returns: a list of Volume Set dicts .. code-block:: python vvset_names = [{ 'name': "volume_set_1", # The name of the volume set 'comment': 'Samplet VVSet', # The volume set's comment 'domain': 'my_domain', # The volume set's domain 'setmembers': ['V1', 'V2'] # List of strings containing # the volumes that are members # of this volume set }, ... ] :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IN_INCONSISTENT_STATE - Internal inconsistency error in vol :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IS_BEING_REMOVED - The volume is being removed :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOLUME - The volume does not exists :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_SYS_VOLUME - Illegal op on system vol :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_INTERNAL_VOLUME - Illegal op on internal vol """ vvset_names = [] volume_sets = self.getVolumeSets() for volume_set in volume_sets['members']: if 'setmembers' in volume_set and name in volume_set['setmembers']: vvset_names.append(volume_set) return vvset_names def getVolumeSets(self): """ Get Volume Sets :returns: Array of Volume Sets """ response, body = self.http.get('/volumesets') return body def getVolumeSet(self, name): """ Get information about a Volume Set :param name: The name of the Volume Set to find :type name: str :returns: Volume Set :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_SET - The set doesn't exist """ response, body = self.http.get('/volumesets/%s' % name) return body def createVolumeSet(self, name, domain=None, comment=None, setmembers=None): """ This creates a new volume set :param name: the volume set to create :type set_name: str :param domain: the domain where the set lives :type domain: str :param comment: the comment for on the vv set :type comment: str :param setmembers: the vv to add to the set, the existence of the vv will not be checked :type setmembers: array :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - EXISTENT_SET - The set already exits. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - MEMBER_IN_DOMAINSET - The host is in a domain set. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - MEMBER_IN_SET - The object is already part of the set. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - MEMBER_NOT_IN_SAME_DOMAIN - Objects must be in the same domain to perform this operation. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IN_INCONSISTENT_STATE - The volume has an internal inconsistency error. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IS_BEING_REMOVED - The volume is being removed. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOLUME - The volume does not exists. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_HOST - The host does not exists. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_SYS_VOLUME - The operation is not allowed on a system volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_INTERNAL_VOLUME - The operation is not allowed on an internal volume. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_DUP_NAME - Invalid input (duplicate name). """ info = {'name': name} if domain: info['domain'] = domain if comment: info['comment'] = comment if setmembers: members = {'setmembers': setmembers} info = self._mergeDict(info, members) response, body = self.http.post('/volumesets', body=info) def deleteVolumeSet(self, name): """ This removes a volume set. You must clear all QOS rules before a volume set can be deleted. :param name: the volume set to remove :type name: str :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_SET - The set does not exists. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - EXPORTED_VLUN - The host set has exported VLUNs. The VV set was exported. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - VVSET_QOS_TARGET - The object is already part of the set. """ response, body = self.http.delete('/volumesets/%s' % name) def modifyVolumeSet(self, name, action=None, newName=None, comment=None, flashCachePolicy=None, setmembers=None): """ This modifies a volume set by adding or remove a volume from the volume set. It's actions is based on the enums SET_MEM_ADD or SET_MEM_REMOVE. :param action: add or remove volume from the set :type action: enum :param name: the volume set name :type name: str :param newName: new name of set :type newName: str :param comment: the comment for on the vv set :type comment: str :param flashCachePolicy: the flash-cache policy for the vv set :type comment: FLASH_CACHED_ENABLED or FLASH_CACHE_DISABLED :param setmembers: the vv to add to the set, the existence of the vv will not be checked :type setmembers: array :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - EXISTENT_SET - The set already exits. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_SET - The set does not exists. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - MEMBER_IN_DOMAINSET - The host is in a domain set. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - MEMBER_IN_SET - The object is already part of the set. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - MEMBER_NOT_IN_SET - The object is not part of the set. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - MEMBER_NOT_IN_SAME_DOMAIN - Objects must be in the same domain to perform this operation. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IN_INCONSISTENT_STATE - The volume has an internal inconsistency error. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IS_BEING_REMOVED - The volume is being removed. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOLUME - The volume does not exists. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_SYS_VOLUME - The operation is not allowed on a system volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_INTERNAL_VOLUME - The operation is not allowed on an internal volume. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_DUP_NAME - Invalid input (duplicate name). :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_PARAM_CONFLICT - Invalid input (parameters cannot be present at the same time). :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Invalid contains one or more illegal characters. """ info = {} if action: info['action'] = action if newName: info['newName'] = newName if comment: info['comment'] = comment if flashCachePolicy: info['flashCachePolicy'] = flashCachePolicy if setmembers: members = {'setmembers': setmembers} info = self._mergeDict(info, members) response = self.http.put('/volumesets/%s' % name, body=info) return response # QoS Priority Optimization methods def addVolumeToVolumeSet(self, set_name, name): """ This adds a volume to a volume set :param set_name: the volume set name :type set_name: str :param name: the volume name to add :type name: str """ return self.modifyVolumeSet(set_name, action=self.SET_MEM_ADD, setmembers=[name]) def removeVolumeFromVolumeSet(self, set_name, name): """ Remove a volume from a volume set :param set_name: the volume set name :type set_name: str :param name: the volume name to add :type name: str """ return self.modifyVolumeSet(set_name, action=self.SET_MEM_REMOVE, setmembers=[name]) def createSnapshotOfVolumeSet(self, name, copyOfName, optional=None): """Create a snapshot of an existing Volume Set. :param name: Name of the Snapshot. The vvname pattern is described in "VV Name Patterns" in the HPE 3PAR Command Line Interface Reference, which is available at the following website: http://www.hp.com/go/storage/docs :type name: str :param copyOfName: The volume set you want to snapshot :type copyOfName: str :param optional: Dictionary of optional params :type optional: dict .. code-block:: python optional = { 'id': 12, # Specifies ID of the volume set # set, next by default 'comment': "some comment", 'readOnly': True, # Read Only 'expirationHours': 36, # time from now to expire 'retentionHours': 12 # time from now to expire } :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INVALID_INPUT_VV_PATTERN - Invalid volume pattern specified :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_SET - The set does not exist :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - EMPTY_SET - The set is empty :raises: :class:`~hpe3parclient.exceptions.HTTPServiceUnavailable` - VV_LIMIT_REACHED - Maximum number of volumes reached :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - The storage volume does not exist :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IS_BEING_REMOVED - The volume is being removed :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_READONLY_TO_READONLY_SNAP - Creating a read-only copy from a read-only volume is not permitted :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - NO_SNAP_CPG - No snapshot CPG has been configured for the volume :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_DUP_NAME - Invalid input (duplicate name). :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_SNAP_PARENT_SAME_BASE - Two parent snapshots share thesame base volume :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_ONLINE_COPY_IN_PROGRESS - Invalid operation. Online copyis in progress :raises: :class:`~hpe3parclient.exceptions.HTTPServiceUnavailable` - VV_ID_LIMIT_REACHED - Max number of volumeIDs has been reached :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOLUME - The volume does not exists :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IN_STALE_STATE - The volume is in a stale state. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_NOT_STARTED - Volume is not started :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_UNAVAILABLE - The volume is not accessible :raises: :class:`~hpe3parclient.exceptions.HTTPServiceUnavailable` - SNAPSHOT_LIMIT_REACHED - Max number of snapshots has been reached :raises: :class:`~hpe3parclient.exceptions.HTTPServiceUnavailable` - CPG_ALLOCATION_WARNING_REACHED - The CPG has reached the allocation warning :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_VOLUME_CONV_IN_PROGRESS - Invalid operation: VV conversion is in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_CLEANUP_IN_PROGRESS - Internal volume cleanup is in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_PEER_VOLUME - Cannot modify a peer volume. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_ONLINE_COPY_IN_PROGRESS - The volume is the target of an online copy. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_INTERNAL_VOLUME - Illegal op on internal vol :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - EXISTENT_ID - An ID exists :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_NOT_IN_NORMAL_STATE - Volume state is not normal :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IN_INCONSISTENT_STATE - Internal inconsistency error in vol :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_RETAIN_GT_EXPIRE - Retention time is greater than expiration time. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_TIME - Invalid time specified. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_SNAPSHOT_NOT_SAME_TYPE - Some snapshots in the volume set are read-only, some are read-write """ parameters = {'name': name} if optional: parameters = self._mergeDict(parameters, optional) info = {'action': 'createSnapshot', 'parameters': parameters} response, body = self.http.post('/volumesets/%s' % copyOfName, body=info) return body # QoS Priority Optimization methods def setQOSRule(self, set_name, max_io=None, max_bw=None): """ Set a QOS Rule on a volume set :param set_name: the volume set name for the rule. :type set_name: str :param max_io: the maximum IOPS value :type max_io: int :param max_bw: The maximum Bandwidth :type max_bw: """ cmd = ['setqos'] if max_io is not None: cmd.extend(['-io', '%s' % max_io]) if max_bw is not None: cmd.extend(['-bw', '%sM' % max_bw]) cmd.append('vvset:' + set_name) result = self._run(cmd) if result: msg = result[0] else: msg = None if msg: if 'no matching QoS target found' in msg: raise exceptions.HTTPNotFound(error={'desc': msg}) else: raise exceptions.SetQOSRuleException(message=msg) def queryQoSRules(self): """ Get QoS Rules :returns: Array of QoS Rules """ response, body = self.http.get('/qos') return body def queryQoSRule(self, targetName, targetType='vvset'): """ Query a QoS rule :param targetType: target type is vvset or sys :type targetType: str :param targetName: the name of the target. When targetType is sys, target name must be sys:all_others. :type targetName: str :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_QOS_RULE - QoS rule does not exist. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Illegal character in the input. """ response, body = self.http.get('/qos/%(targetType)s:%(targetName)s' % {'targetType': targetType, 'targetName': targetName}) return body def createQoSRules(self, targetName, qosRules, target_type=TARGET_TYPE_VVSET): """ Create QOS rules The QoS rule can be applied to VV sets. By using sys:all_others, you can apply the rule to all volumes in the system for which no QoS rule has been defined. ioMinGoal and ioMaxLimit must be used together to set I/O limits. Similarly, bwMinGoalKB and bwMaxLimitKB must be used together. If ioMaxLimitOP is set to 2 (no limit), ioMinGoalOP must also be to set to 2 (zero), and vice versa. They cannot be set to 'none' individually. Similarly, if bwMaxLimitOP is set to 2 (no limit), then bwMinGoalOP must also be set to 2. If ioMaxLimitOP is set to 1 (no limit), ioMinGoalOP must also be to set to 1 (zero) and vice versa. Similarly, if bwMaxLimitOP is set to 1 (zero), then bwMinGoalOP must also be set to 1. The ioMinGoalOP and ioMaxLimitOP fields take precedence over the ioMinGoal and ioMaxLimit fields. The bwMinGoalOP and bwMaxLimitOP fields take precedence over the bwMinGoalKB and bwMaxLimitKB fields :param target_type: Type of QoS target, either enum TARGET_TYPE_VVS or TARGET_TYPE_SYS. :type target_type: enum :param targetName: the name of the target object on which the QoS rule will be created. :type targetName: str :param qosRules: QoS options :type qosRules: dict .. code-block:: python qosRules = { 'priority': 2, # priority enum 'bwMinGoalKB': 1024, # bandwidth rate minimum goal in # kilobytes per second 'bwMaxLimitKB': 1024, # bandwidth rate maximum limit in # kilobytes per second 'ioMinGoal': 10000, # I/O-per-second minimum goal 'ioMaxLimit': 2000000, # I/0-per-second maximum limit 'enable': True, # QoS rule for target enabled? 'bwMinGoalOP': 1, # zero none operation enum, when set to # 1, bandwidth minimum goal is 0 # when set to 2, the bandwidth mimumum # goal is none (NoLimit) 'bwMaxLimitOP': 1, # zero none operation enum, when set to # 1, bandwidth maximum limit is 0 # when set to 2, the bandwidth maximum # limit is none (NoLimit) 'ioMinGoalOP': 1, # zero none operation enum, when set to # 1, I/O minimum goal is 0 # when set to 2, the I/O minimum goal is # none (NoLimit) 'ioMaxLimitOP': 1, # zero none operation enum, when set to # 1, I/O maximum limit is 0 # when set to 2, the I/O maximum limit # is none (NoLimit) 'latencyGoal': 5000, # Latency goal in milliseconds 'defaultLatency': False # Use latencyGoal or defaultLatency? } :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EXCEEDS_RANGE - Invalid input: number exceeds expected range. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_QOS_RULE - QoS rule does not exists. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Illegal character in the input. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - EXISTENT_QOS_RULE - QoS rule already exists. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_MIN_GOAL_GRT_MAX_LIMIT - I/O-per-second maximum limit should be greater than the minimum goal. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_BW_MIN_GOAL_GRT_MAX_LIMIT - Bandwidth maximum limit should be greater than the mimimum goal. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_BELOW_RANGE - I/O-per-second limit is below range. Bandwidth limit is below range. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - UNLICENSED_FEATURE - The system is not licensed for QoS. """ info = {'name': targetName, 'type': target_type} info = self._mergeDict(info, qosRules) response, body = self.http.post('/qos', body=info) return body def modifyQoSRules(self, targetName, qosRules, targetType='vvset'): """ Modify an existing QOS rules The QoS rule can be applied to VV sets. By using sys:all_others, you can apply the rule to all volumes in the system for which no QoS rule has been defined. ioMinGoal and ioMaxLimit must be used together to set I/O limits. Similarly, bwMinGoalKB and bwMaxLimitKB must be used together. If ioMaxLimitOP is set to 2 (no limit), ioMinGoalOP must also be to set to 2 (zero), and vice versa. They cannot be set to 'none' individually. Similarly, if bwMaxLimitOP is set to 2 (no limit), then bwMinGoalOP must also be set to 2. If ioMaxLimitOP is set to 1 (no limit), ioMinGoalOP must also be to set to 1 (zero) and vice versa. Similarly, if bwMaxLimitOP is set to 1 (zero), then bwMinGoalOP must also be set to 1. The ioMinGoalOP and ioMaxLimitOP fields take precedence over the ioMinGoal and ioMaxLimit fields. The bwMinGoalOP and bwMaxLimitOP fields take precedence over the bwMinGoalKB and bwMaxLimitKB fields :param targetName: the name of the target object on which the QoS rule will be created. :type targetName: str :param targetType: Type of QoS target, either vvset or sys :type targetType: str :param qosRules: QoS options :type qosRules: dict .. code-block:: python qosRules = { 'priority': 2, # priority enum 'bwMinGoalKB': 1024, # bandwidth rate minimum goal in # kilobytes per second 'bwMaxLimitKB': 1024, # bandwidth rate maximum limit in # kilobytes per second 'ioMinGoal': 10000, # I/O-per-second minimum goal. 'ioMaxLimit': 2000000, # I/0-per-second maximum limit 'enable': True, # QoS rule for target enabled? 'bwMinGoalOP': 1, # zero none operation enum, when set to # 1, bandwidth minimum goal is 0 # when set to 2, the bandwidth minimum # goal is none (NoLimit) 'bwMaxLimitOP': 1, # zero none operation enum, when set to # 1, bandwidth maximum limit is 0 # when set to 2, the bandwidth maximum # limit is none (NoLimit) 'ioMinGoalOP': 1, # zero none operation enum, when set to # 1, I/O minimum goal minimum goal is 0 # when set to 2, the I/O minimum goal is # none (NoLimit) 'ioMaxLimitOP': 1, # zero none operation enum, when set to # 1, I/O maximum limit is 0 # when set to 2, the I/O maximum limit # is none (NoLimit) 'latencyGoal': 5000, # Latency goal in milliseconds 'defaultLatency': False # Use latencyGoal or defaultLatency? } :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` INV_INPUT_EXCEEDS_RANGE - Invalid input: number exceeds expected range. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` NON_EXISTENT_QOS_RULE - QoS rule does not exists. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` INV_INPUT_ILLEGAL_CHAR - Illegal character in the input. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` EXISTENT_QOS_RULE - QoS rule already exists. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` INV_INPUT_IO_MIN_GOAL_GRT_MAX_LIMIT - I/O-per-second maximum limit should be greater than the minimum goal. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` INV_INPUT_BW_MIN_GOAL_GRT_MAX_LIMIT - Bandwidth maximum limit should be greater than the minimum goal. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` INV_INPUT_BELOW_RANGE - I/O-per-second limit is below range. Bandwidth limit is below range. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` UNLICENSED_FEATURE - The system is not licensed for QoS. """ response = self.http.put('/qos/%(targetType)s:%(targetName)s' % {'targetType': targetType, 'targetName': targetName}, body=qosRules) return response def deleteQoSRules(self, targetName, targetType='vvset'): """Clear and Delete QoS rules. :param targetType: target type is vvset or sys :type targetType: str :param targetName: the name of the target. When targetType is sys, target name must be sys:all_others. :type targetName: str :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_QOS_RULE - QoS rule does not exist. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Illegal character in the input """ response, body = self.http.delete( '/qos/%(targetType)s:%(targetName)s' % {'targetType': targetType, 'targetName': targetName}) return body def setVolumeMetaData(self, name, key, value): """This is used to set a key/value pair metadata into a volume. If the key already exists on the volume the value will be updated. :param name: the volume name :type name: str :param key: the metadata key name :type key: str :param value: the metadata value :type value: str :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EXCEEDS_LENGTH - Invalid input: string length exceeds limit. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_MISSING_REQUIRED - Required fields missing :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_UNREC_NAME - Unrecognized name :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Illegal character in input :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - The volume does not exist """ key_exists = False info = { 'key': key, 'value': value } try: response, body = self.http.post('/volumes/%s/objectKeyValues' % name, body=info) except exceptions.HTTPConflict: key_exists = True except Exception: raise if key_exists: info = { 'value': value } response, body = self.http.put( '/volumes/%(name)s/objectKeyValues/%(key)s' % {'name': name, 'key': key}, body=info) return response def getVolumeMetaData(self, name, key): """This is used to get a key/value pair metadata from a volume. :param name: the volume name :type name: str :param key: the metadata key name :type key: str :returns: dict with the requested key's data. .. code-block:: python data = { # time of creation in seconds format 'creationTimeSec': 1406074222 # the date/time the key was added 'date_added': 'Mon Jul 14 16:09:36 PDT 2014', 'value': 'data' # the value associated with the key 'key': 'key_name' # the key name # time of creation in date format 'creationTime8601': '2014-07-22T17:10:22-07:00' } :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Illegal character in input :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - The volume does not exist :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_OBJECT_KEY - Object key does not exist """ response, body = self.http.get( '/volumes/%(name)s/objectKeyValues/%(key)s' % {'name': name, 'key': key}) return body def getAllVolumeMetaData(self, name): """This is used to get all key/value pair metadata from a volume. :param name: the volume name :type name: str :returns: dict with all keys and associated data. .. code-block:: python keys = { 'total': 2, 'members': [ { # time of creation in seconds format 'creationTimeSec': 1406074222 # the date/time the key was added 'date_added': 'Mon Jul 14 16:09:36 PDT 2014', 'value': 'data' # the value associated with the key 'key': 'key_name' # the key name # time of creation in date format 'creationTime8601': '2014-07-22T17:10:22-07:00' }, { # time of creation in seconds format 'creationTimeSec': 1406074222 # the date/time the key was added 'date_added': 'Mon Jul 14 16:09:36 PDT 2014', 'value': 'data' # the value associated with the key 'key': 'key_name_2' # the key name # time of creation in date format 'creationTime8601': '2014-07-22T17:10:22-07:00' } ] } :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - The volume does not exist """ response, body = self.http.get('/volumes/%s/objectKeyValues' % name) return body def removeVolumeMetaData(self, name, key): """This is used to remove a metadata key/value pair from a volume. :param name: the volume name :type name: str :param key: the metadata key name :type key: str :returns: None :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Illegal character in input :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - The volume does not exist :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_OBJECT_KEY - Object key does not exist """ response, body = self.http.delete( '/volumes/%(name)s/objectKeyValues/%(key)s' % {'name': name, 'key': key}) return body def findVolumeMetaData(self, name, key, value): """Determines whether a volume contains a specific key/value pair. :param name: the volume name :type name: str :param key: the metadata key name :type key: str :param value: the metadata value :type value: str :returns: bool """ try: contents = self.getVolumeMetaData(name, key) if contents['value'] == value: return True except Exception: pass return False def getRemoteCopyInfo(self): """ Querying Overall Remote-Copy Information :returns: Overall Remote Copy Information """ response, body = self.http.get('/remotecopy') return body def getRemoteCopyGroups(self): """ Returns information on all Remote Copy Groups :returns: list of Remote Copy Groups """ response, body = self.http.get('/remotecopygroups') return body def getRemoteCopyGroup(self, name): """ Returns information on one Remote Copy Group :param name: the remote copy group name :type name: str :returns: Remote Copy Group """ response, body = self.http.get('/remotecopygroups/%s' % name) return body def getRemoteCopyGroupVolumes(self, remoteCopyGroupName): """ Returns information on all volumes in a Remote Copy Groups :param remoteCopyGroupName: the remote copy group name :type name: str :returns: list of volumes in a Remote Copy Groups """ response, body = self.http.get( '/remotecopygroups/%s/volumes' % (remoteCopyGroupName) ) return body def getRemoteCopyGroupVolume(self, remoteCopyGroupName, volumeName): """ Returns information on one volume of a Remote Copy Group :param remoteCopyGroupName: the remote copy group name :type name: str :param volumeName: the remote copy group name :type name: str :returns: RemoteVolume """ response, body = self.http.get( '/remotecopygroups/%s/volumes/%s' % (remoteCopyGroupName, volumeName) ) return body def createRemoteCopyGroup(self, name, targets, optional=None): """ Creates a remote copy group :param name: the remote copy group name :type name: str :param targets: Specifies the attributes of the target of the remote-copy group. :type targets: list :param optional: dict of other optional items :type optional: dict .. code-block:: python targets = [ { "targetName": "name", # Target name associated with # the remote-copy group to be # created "mode": 2, # Specifies the volume group # mode. # 1 - The remote-copy group mode # is synchronous. # 2 - The remote-copy group mode # is periodic. # 3 - The remote-copy group mode # is periodic. # 4 - Remote-copy group mode is # asynchronous. "userCPG": "SOME_CPG", # Specifies the user CPG # that will be used for # volumes that are # autocreated on the # target. "snapCPG": "SOME_SNAP_CPG" # Specifies the snap CPG # that will be used for # volumes that are # autocreated on the # target. } ] optional = { "localSnapCPG" : "SNAP_CPG", # Specifies the local snap # CPG that will be used for # volumes that are autocreated. "localUserCPG" : "SOME_CPG", # Specifies the local user # CPG that will be used for # volumes that are autocreated. "domain" : "some-domain" # Specifies the attributes of # the target of the # remote-copy group. } :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Invalid character in the remote-copy group or volume name. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - EXISTENT_RCOPY_GROUP - The remote-copy group already exists. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - RCOPY_GROUP_TOO_MANY_TARGETS - Too many remote-copy group targets have been specified. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_BAD_ENUM_VALUE - The mode is not valid. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - RCOPY_GROUP_TARGET_NOT_UNIQUE - The remote-copy group target is not unique. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_IS_NOT_READY - The remote-copy configuration is not ready for commands. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_MODE_NOT_SUPPORTED - The remote-copy group mode is not supported. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - RCOPY_GROUP_MAX_GROUP_REACHED_PERIODIC - The maximum number of remote-copy groups in periodic mode has been reached. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - RCOPY_GROUP_MAX_GROUP_REACHED_PERIODIC - The maximum number of remote-copy groups in periodic mode has been reached. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_SECONDARY_GROUP_MORE_THAN_ONE_BACKUP_TARGET - Secondary groups should have only one target that is not a backup. :raises: :class:`~hpe3parclient.exceptions.HTTPServiceUnavailable` - RCOPY_GROUP_MORE_THAN_ONE_SYNC_TARGET - Remote-copy groups can have no more than one synchronous-mode target. :raises: :class:`~hpe3parclient.exceptions.HTTPServiceUnavailable` - RCOPY_GROUP_MORE_THAN_ONE_PERIODIC_TARGET - Remote-copy groups can have no more than one periodic-mode target. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_ONE_TO_ONE_CONFIG_FOR_MIXED_MODE - Mixed mode is supported in a 1-to-1 remote-copy configuration. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_INV_TARGET - The specified target is not a target of the remote-copy group. :raises: :class:`~hpe3parclient.exceptions.HTTPNotImplemented` - RCOPY_TARGET_IN_PEER_PERSISTENCE_SYNC_GROUP_ONLY - The remote-copy target is configured with peer persistence; only synchronous groups can be added. :raises: :class:`~hpe3parclient.exceptions.HTTPNotImplemented` - RCOPY_TARGET_MODE_NOT_SUPPORTED - The remote-copy target mode is not supported. :raises: :class:`~hpe3parclient.exceptions.HTTPNotImplemented` - RCOPY_TARGET_MULTI_TARGET_NOT_SUPPORTED - The remote-copy target was created in an earlier version of the HP 3PAR OS that does not support multiple targets. :raises: :class:`~hpe3parclient.exceptions.HTTPNotImplemented` - RCOPY_TARGET_VOL_AUTO_CREATION_NOT_SUPPORTED - The remote-copy target is in an older version of the HP 3PAR OS that does not support autocreation of :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - RCOPY_GROUP_MIXED_MODES_ON_ONE_TARGET - Remote-copy groups with different modes on a single target are not supported. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_CPG - The CPG does not exists. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - CPG_NOT_IN_SAME_DOMAIN - Snap CPG is not in the same domain as the user CPG. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - NON_EXISTENT_DOMAIN - Domain doesn't exist. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_HAS_NO_CPG - No CPG has been defined for the remote-copy group on the target. :raises: :class:`~hpe3parclient.exceptions.HTTPServiceUnavailable` - RCOPY_MAX_SYNC_TARGET_REACHED - The maximum number of remote-copy synchronous targets has been reached. :raises: :class:`~hpe3parclient.exceptions.HTTPServiceUnavailable` - RCOPY_MAX_PERIODIC_TARGET_REACHED - The maximum number of remote-copy periodic targets has been reached. """ parameters = {'name': name, 'targets': targets} if optional: parameters = self._mergeDict(parameters, optional) response, body = self.http.post('/remotecopygroups', body=parameters) return body def removeRemoteCopyGroup(self, name, keep_snap=False): """ Deletes a remote copy group :param name: the remote copy group name :type name: str :param keep_snap: used to retain the local volume resynchronization snapshot. NOTE: to retain the snapshot pass 'true' to keep_snap :type keep_snap: bool :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_RCOPY_GROUP - The remote-copy group does not exist. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_STARTED - The remote-copy group has already been started. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_IS_BUSY - The remote-copy group is currently busy; retry later. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_TARGET_IS_NOT_READY - The remote-copy group target is not ready. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_OPERATION_ONLY_ON_PRIMARY_SIDE - The operation should be performed only on the primary side. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_RENAME_RESYNC_SNAPSHOT_FAILED - Renaming of the remote-copy group resynchronization snapshot failed. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_IN_FAILOVER_STATE - The remote-copy group is in failover state; both the source system and the target system are in the primary state. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - RCOPY_GROUP_TARGET_VOLUME_MISMATCH - Secondary group on target system has a mismatched volume configuration. """ if keep_snap: snap_query = 'true' else: snap_query = 'false' response, body = self.http.delete( '/remotecopygroups/%(name)s?keepSnap=%(snap_query)s' % {'name': name, 'snap_query': snap_query}) return body def modifyRemoteCopyGroup(self, name, optional=None): """ Modifies a remote copy group :param name: the remote copy group name :type name: str :param optional: dict of other optional items :type optional: dict .. code-block:: python optional = { "localUserCPG": "CPG", # Specifies the local user # CPG that will be used for # autocreated volumes. "localSnapCPG": "SNAP_CPG", # Specifies the local snap # CPG that will be used for # autocreated volumes. "targets": targets, # Specifies the attributes of # the remote-copy group # target. "unsetUserCPG": False, # If True, this option # unsets the localUserCPG and # remoteUserCPG of the # remote-copy group. "unsetSnapCPG": Flase # If True, this option # unsets the localSnapCPG and # remoteSnapCPG of the # remote-copy group. } targets = [ { "targetName": "name", # Specifies the target name # associated with the # remote-copy group to be # created. "remoteUserCPG": "CPG", # Specifies the user CPG # on the target that will be # used for autocreated # volumes. "remoteSnapCPG": "SNAP_CPG", # Specifies the snap CPG # on the target that will be # used for autocreated # volumes. "syncPeriod": 300, # Specifies that asynchronous # periodic remote-copy groups # should be synchronized # periodically to the # . # Range is 300 - 31622400 # seconds (1 year). "rmSyncPeriod": False, # If True, this option # resets the syncPeriod # time to 0 (zero). # If False, the # syncPeriod value is 0 # (zero), then Ignore. # If False, and the # syncPeriod value is # positive, then then the # synchronizaiton period # is set. "mode": 2, # Volume group mode. Can be # either synchronous or # periodic. # 1 - The remote-copy group # mode is synchronous. # 2 - The remote-copy group # mode is periodic. # 3 - The remote-copy group # mode is periodic. # 4 - Remote-copy group mode # is asynchronous. "snapFrequency": 300, # Async mode only. Specifies # the interval in seconds at # which Remote Copy takes # coordinated snapshots. Range # is 300-31622400 seconds # (1 year). "rmSnapFrequency": False, # If True, this option resets # the snapFrequency time # rmSnapFrequency to 0 (zero). # If False and the # snapFrequency value is 0 # (zero), then Ignore. If # False, and the snapFrequency # value is positive, sets the # snapFrequency value. "policies": policies # The policy assigned to # the remote-copy group. } ] policies = { "autoRecover": False, # If the remote copy is stopped # as a result of links going # down, the remote-copy group # can be automatically # restarted after the links # come back up. "overPeriodAlert": False, # If synchronization of an # asynchronous periodic # remote-copy group takes # longer to complete than its # synchronization period, an # alert is generated. "autoFailover": False, # Automatic failover on a # remote-copy group. "pathManagement": False # Automatic failover on a # remote-copy group. } :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_RCOPY_GROUP - The remote-copy group does not exist. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_OPERATION_ONLY_ON_PRIMARY_SIDE - The operation should be performed only on the primary side. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_IS_NOT_PERIODIC - Target in group is not periodic. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_INV_POLICY_FOR_PERIODIC_GROUP - Invalid policy for a periodic group. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_INV_POLICY_FOR_SYNC_GROUP - Invalid policy for a synchronous target. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_CPG - The CPG does not exists. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_INV_TARGET - The specified target is not a target of the remote-copy group. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - CPG_NOT_IN_SAME_DOMAIN - Snap CPG is not in the same domain as the user CPG. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_BELOW_RANGE - The minimum allowable period is 300 seconds. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EXCEEDS_RANGE - Invalid input: the period is too long. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_STARTED - The remote-copy group has already been started. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_INV_OPERATION_ON_MULTIPLE_TARGETS - The operation is not supported on multiple targets. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - RCOPY_GROUP_TARGET_NOT_UNIQUE - The remote-copy group target is not unique. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_INV_TARGET_NUMBER - The wrong number of targets is specified for the remote-copy group. """ parameters = {} if optional: parameters = self._mergeDict(parameters, optional) response, body = self.http.put('/remotecopygroups/%s' % name, body=parameters) return body def addVolumeToRemoteCopyGroup(self, name, volumeName, targets, optional=None, useHttpPost=False): """ Adds a volume to a remote copy group :param name: Name of the remote copy group :type name: string :param volumeName: Specifies the name of the existing virtual volume to be admitted to an existing remote-copy group. :type volumeName: string :param targets: Specifies the attributes of the target of the remote-copy group. :type targets: list :param optional: dict of other optional items :type optional: dict .. code-block:: python targets = [ { "targetName": "name", # The target name # associated with this # group. "secVolumeName": "sec_vol_name" # Specifies the name of # the secondary volume # on the target system. } ] optional = { "snapshotName": "snapshot_name", # The optional read-only # snapshotName is a # starting snapshot when # the group is started # without performing a # full resynchronization. # Instead, for # synchronized groups, # the volume # synchronizes deltas # between this # snapshotName and # the base volume. For # periodic groups, the # volume synchronizes # deltas between this # snapshotName and a # snapshot of the base. "volumeAutoCreation": False, # If set to true, the # secondary volumes # should be created # automatically on the # target using the CPG # associated with the # remote-copy group on # that target. "skipInitialSync": False # If set to true, the # volume should skip the # initial sync. This is # for the admission of # volumes that have # been pre-synced with # the target volume. } :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_RCOPY_GROUP - The remote-copy group does not exist. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - volume doesn't exist :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_SNAPSHOT - The specified snapshot does not exist. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_SNAPSHOT_IS_RW - The specified snapshot can only be read-only. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_VOL_IS_RO - The volume to be admitted to the remote-copy group cannot be read-only. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_HAS_NO_CPG - No CPG has been defined for the remote-copy group on the target. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - RCOPY_GROUP_EXISTENT_VOL - The specified volume is already in the remote-copy group. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - RCOPY_GROUP_EXISTENT_VOL_ON_TARGET - The specified secondary volume to be automatically created already exists on the target. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_INV_TARGET - The specified target is not a target of the remote-copy group. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_VOL_SIZE_NOT_MATCH - The size of the volume added to the remote-copy group does not match the size of the volume on the target. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - RCOPY_GROUP_NON_EXISTENT_VOL_ON_TARGET - The specified secondary volume does not exist on the target. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_VOL_NO_SNAPSHOT_SPACE - The volume to be admitted into the remote-copy group requires that snapshot space be allocated. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_TARGET_VOL_NO_SNAPSHOT_SPACE - The specified secondary volumes on the target require snapshot space. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_VOL_IS_PHYSICAL_COPY - A physical copy cannot be added to a remote-copy group :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_MAX_VOL_REACHED_PERIODIC - The number of periodic-mode volumes on the system has reached the limit. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_MAX_VOL_REACHED_SYNC - The number of synchronous-mode volumes on the system has reached the limit. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_MAX_VOL_REACHED - The number of volumes on the system has reached the limit. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_IS_NOT_READY - The remote-copy configuration is not ready :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_VOL_INTERNAL_CONSISTENCY_ERR - The volume to be admitted into the remote-copy group has an internal consistency error. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_IS_BEING_REMOVED - The volume to be admitted into the remote-copy group is being removed. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUPSNAPSHOT_PARENT_MISMATCH - The names of the snapshot and its parent do not match. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_TARGET_VOL_EXPORTED - Secondary volumes cannot be admitted when they are exported. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_VOL_IS_PEER_PROVISIONED - A peer-provisioned volume cannot be admitted into a remote-copy group. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_VOL_ONLINE_CONVERSION - Online volume conversions do not support remote copy. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_VOL_ONLINE_PROMOTE - Online volume promotes do not support remote copy. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_VOL_ONLINE_COPY - Online volume copies do not support remote copy. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_VOL_CLEAN_UP - Cleanup of internal volume is in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_VOL_IS_INTERNAL - Internal volumes cannot be admitted into a remote-copy group. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_VOL_NOT_IN_SAME_DOMAIN - The remote-copy group has a different domain than the volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_STARTED - The remote-copy group has already been started. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_IS_BUSY - The remote-copy group is currently busy; retry later. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_VOL_IN_OTHER_GROUP - The volume is already in another remote-copy group. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_INV_TARGET_NUMBER - The wrong number of targets is specified for the remote-copy group. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_INV_TARGET - The specified target is not a target of the remote-copy group. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_NOT_SUPPORT_VOL_ID - The target for the remote-copy group does not support volume IDs. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_IS_SELF_MIRRORED - The target is self-mirrored. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_TARGET_VOL_IS_RO - The remote-copy target volume cannot be read-only. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_OPERATION_ONLY_ON_PRIMARY_SIDE - The operation should be performed only on the primary side. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_TARGET_IS_NOT_READY - The remote-copy group target is not ready. :raises: :class:`~hpe3parclient.exceptions.HTTPNotImplemented` - RCOPY_UNSUPPORTED_TARGET_VERSION - The target HP 3PAR OS version is not supported. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_MULTIPLE_VOL_IN_SAME_FAMILY - A remote-copy group cannot contain multiple volumes in the same family tree. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_MULTIPLE_RW_SNAPSHOT_IN_SAME_FAMILY - Only one read/write snapshot in the same family can be added to a remote-copy group. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_SYNC_SNAPSHOT_IN_MULTIPLE_TARGET - A synchronization snapshot cannot be set with multiple targets. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_ADD_VOL_FAILED - Failed to add volume to the remote-copy group. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_ADD_VOL_FAILED_PARTIAL - Adding volume to remote-copy group succeeded on some targets. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_SET_AUTO_CREATED - The set was created automatically Members cannot be added or removed. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_SECONDARY_DOES_NOT_MATCH_PRIMARY - The remote-copy group is in the failover state. Both systems are in the primary state. """ if not useHttpPost: parameters = {'action': 1, 'volumeName': volumeName, 'targets': targets} if optional: parameters = self._mergeDict(parameters, optional) response, body = self.http.put('/remotecopygroups/%s' % name, body=parameters) else: parameters = {'volumeName': volumeName, 'targets': targets} if optional: parameters = self._mergeDict(parameters, optional) response, body = self.http.post( '/remotecopygroups/%s/volumes' % name, body=parameters ) return body def removeVolumeFromRemoteCopyGroup(self, name, volumeName, optional=None, removeFromTarget=False, useHttpDelete=True): """ Removes a volume from a remote copy group :param name: Name of the remote copy group :type name: string :param volumeName: Specifies the name of the existing virtual volume to be removed from an existing remote-copy group. :type volumeName: string :param optional: dict of other optional items :type optional: dict .. code-block:: python optional = { "keepSnap": False # If true, the resynchronization # snapshot of the local volume is # retained. } :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_RCOPY_GROUP - The remote-copy group does not exist. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - volume doesn't exist :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_IS_NOT_READY - The remote-copy configuration is not ready for commands. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_STARTED - The remote-copy group has already been started. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_IS_BUSY - The remote-copy group is currently busy; retry later. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - RCOPY_GROUP_VOL_NOT_IN_GROUP - The volume is not in the remote-copy group. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_RENAME_RESYNC_SNAPSHOT_FAILED - Renaming of the remote-copy group resynchronization snapshot failed. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - RCOPY_GROUP_CREATED_MIRROR_CONFIG_OFF - The remote-copy group was created when the configuration mirroring policy was turned off on the target. However, this policy is now turned on. In order to dismiss a volume from the remote-copy group, the configuration mirroring policy must be turned off. Retry after turning the policy off. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_OPERATION_ONLY_ON_PRIMARY_SIDE - The operation should be performed only on the primary side. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_TARGET_IS_NOT_READY - The remote-copy group target is not ready. """ # Now this feature is supported in the WSAPI. if not useHttpDelete: # Retaining this code (ssh) for backward compatibility only. if removeFromTarget: if optional: keep_snap = optional.get('keepSnap', False) else: keep_snap = False if keep_snap: cmd = ['dismissrcopyvv', '-f', '-keepsnap', '-removevv', volumeName, name] else: cmd = ['dismissrcopyvv', '-f', '-removevv', volumeName, name] self._run(cmd) else: parameters = {'action': 2, 'volumeName': volumeName} if optional: parameters = self._mergeDict(parameters, optional) response, body = self.http.put('/remotecopygroups/%s' % name, body=parameters) return body else: option = None if optional and optional.get('keepSnap') and removeFromTarget: raise Exception("keepSnap and removeFromTarget cannot be both\ true while removing the volume from remote copy group") elif optional and optional.get('keepSnap'): option = 'keepSnap' elif removeFromTarget: option = 'removeSecondaryVolume' delete_url = '/remotecopygroups/%s/volumes/%s' % (name, volumeName) if option: delete_url += '?%s=true' % option response, body = self.http.delete(delete_url) return body def startRemoteCopy(self, name, optional=None): """ Starts a remote copy :param name: Name of the remote copy group :type name: string :param optional: dict of other optional items :type optional: dict .. code-block:: python # All the volumes in the group must be specified. While specifying # the pair, the starting snapshot is optional. If it is not # specified, a full resynchronization of the volume is performed. startingSnapshots = [ { "volumeName": "vol_name", # Volume name "snapshotName": "snap_name" # Snapshot name } ] optional = { "skipInitialSync": False, # If True, the volume # should skip the initial # synchronization and # sets the volumes to # a synchronized state. "targetName": "target_name", # The target name associated # with this group. "startingSnapshots": startingSnapshots } :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_RCOPY_GROUP - The remote-copy group does not exist. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_INV_TARGET - The specified target is not a target of the remote-copy group. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_STARTED - The remote-copy group has already been started. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - RCOPY_GROUP_EMPTY - The remote-copy group must contain volumes before being started. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_OPERATION_ONLY_ON_PRIMARY_SIDE - The operation should be performed only on the primary side. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - RCOPY_TARGET_NOT_SPECIFIED - A target must be specified to complete this operation. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - RCOPY_GROUP_NOT_ALL_VOLUMES_SPECIFIED - All the volumes in the remote-copy group must be specified to complete this operation. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - RCOPY_GROUP_EXISTENT_VOL_WWN_ON_TARGET - Secondary volume WWN already exists on the target. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - RCOPY_GROUP_VOLUME_ALREADY_SYNCED - Volume is already synchronized. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - RCOPY_GROUP_INCORRECT_SNAPSHOT_OR_VOLUME_SPECIFIED - An incorrect starting snapshot or volume was specified, or the snapshot or volume does not exist. """ parameters = {'action': 3} if optional: parameters = self._mergeDict(parameters, optional) response, body = self.http.put('/remotecopygroups/%s' % name, body=parameters) return body def stopRemoteCopy(self, name, optional=None): """ Stops a remote copy :param name: Name of the remote copy group :type name: string :param optional: dict of other optional items :type optional: dict .. code-block:: python optional = { "noSnapshot": False, # If true, this option turns # off creation of snapshots # in synchronous and # periodic modes, and # deletes the current # synchronization snapshots. "targetName": "target_name" # The target name associated # with this group } :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_RCOPY_GROUP - The remote-copy group does not exist. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_TARGET_IS_NOT_READY - The remote-copy group target is not ready. """ parameters = {'action': 4} if optional: parameters = self._mergeDict(parameters, optional) response, body = self.http.put('/remotecopygroups/%s' % name, body=parameters) return body def synchronizeRemoteCopyGroup(self, name, optional=None): """ Synchronizing a remote copy group :param name: Name of the remote copy group :type name: string :param optional: dict of other optional items :type optional: dict .. code-block:: python optional = { "noResyncSnapshot": False, # If true, does not save # the resynchronization # snapshot. Applicable # only to remote-copy # groups in asychronous # periodic mode. "targetName": "target_name", # The target name # assoicated with the # remote-copy group. "fullSync": False # If true, this option # forces a full # synchronization of the # remote-copy group, even # if the volumes are # already synchronized. # This option, which # applies only to volume # groups in synchronous # mode, can be used to # resynchronize volumes # that have become # inconsistent. } :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_RCOPY_GROUP - The remote-copy group does not exist. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_OPERATION_ONLY_ON_PRIMARY_SIDE - The operation should be performed only on the primary side. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - UNLICENSED_FEATURE - The system is not licensed for this feature. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_INV_TARGET - The specified target is not a target of the remote-copy group. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_TARGET_IS_NOT_READY - The remote-copy group target is not ready. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_INVOLVED_IN_SYNCHRONIZATION - The remote-copy group is already involved in synchronization. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_STARTED - The remote-copy group has already been started. """ parameters = {'action': 5} if optional: parameters = self._mergeDict(parameters, optional) response, body = self.http.put('/remotecopygroups/%s' % name, body=parameters) return body def recoverRemoteCopyGroupFromDisaster(self, name, action, optional=None): """ Recovers a remote copy group from a disaster :param name: Name of the remote copy group :type name: string :param action: Specifies the action to be taken on the specified group. The action may be any of values 6 through 11: * RC_ACTION_CHANGE_DIRECTION - Changes the current direction of the remote-copy groups. * RC_ACTION_CHANGE_TO_PRIMARY - Changes the secondary groups to primary groups on the active system. * RC_ACTION_MIGRATE_GROUP - Migrates the remote-copy group from the primary system to the secondary system without impacting I/O. * RC_ACTION_CHANGE_TO_SECONDARY - Changes the primary remote-copy group on the backup system to the secondary remote-copy group. * RC_ACTION_CHANGE_TO_NATURUAL_DIRECTION - Changes all remote-copy groups to their natural direction and starts them. * RC_ACTION_OVERRIDE_FAIL_SAFE - Overrides the failsafe state that is applied to the remote-copy group. :type action: int :param optional: dict of other optional items :type optional: dict .. code-block:: python optional = { "targetName": "target_name", # The target name # associated with this # group. "skipStart": False, # If true, groups are not # started after role reversal # is completed. Valid for # only FAILOVER, RECOVER, # and RESTORE operations. "skipSync": False, # If true, the groups are # not synchronized after # role reversal is # completed. Valid only for # FAILOVER, RECOVER, and # RESTORE operations. "discardNewData": False, # If true and the group # has multiple targets, # don't check other targets # of the group to see if # newer data should be # pushed from them. # Valid only for FAILOVER # operation. "skipPromote": False, # If true, the snapshots of # the groups that are # switched from secondary # to primary are not # promoted to the base # volume. Valid only for # FAILOVER and REVERSE # operations. "noSnapshot": False, # If true, the snapshots # are not taken of the # groups that are switched # from secondary to # primary. Valid for # FAILOVER, REVERSE, and # RESTORE operations. "stopGroups": False, # If true, the groups are # stopped before performing # the reverse operation. # Valid only for REVERSE # operation. "localGroupsDirection": False # If true, the group's # direction is changed only # on the system where the # operation is run. Valid # only for REVERSE operation } :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_RCOPY_GROUP - The remote-copy group does not exist. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - UNLICENSED_FEATURE - System is not licensed for this feature. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - RCOPY_GROUP_INV_TARGET - Specified target is not in remote copy group. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_INPUT_MISSING_REQUIRED - Group has multiple targets. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_RCOPY_GROUP_ROLE_CONFLICT - Group is not in correct role for this operation. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_INV_OPERATION_ON_MULTIPLE_TARGETS - The operation is not supported on multiple targets. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_NOT_STOPPED - Remote copy group is not stopped. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_RCOPY_GROUP_ROLE_CONFLICT - Group is not in correct role for this operation. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_NOT_STARTED - Remote copy not started. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_INPUT_PARAM_CONFLICT - Parameters cannot be present at the same time. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_PROMOTE_IN_PROGRESS - Volume promotion is in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_IS_BUSY - Remote copy group is currently busy. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_STARTED - Remote copy group has already been started. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_EMPTY - Remote copy group does not contain any volumes. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_OPERATION_ONLY_ON_PRIMARY_SIDE - Operation should only be issued on primary side. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - RCOPY_GROUP_OPERATION_ONLY_ON_SECONDARY_SIDE - Operation should only be issued on secondary side. """ parameters = {'action': action} if optional: parameters = self._mergeDict(parameters, optional) response, body = self.http.post('/remotecopygroups/%s' % name, body=parameters) return body def toggleRemoteCopyConfigMirror(self, target, mirror_config=True): """ Used to toggle config mirroring policies on a target device. :param target: The 3PAR target name to enable/disable config mirroring. :type target: string :param mirror_config: Specifies whether to enable or disable config mirroring. :type mirror_config: bool """ obj = {'mirrorConfig': mirror_config} info = {'policies': obj} try: self.http.put('/remotecopytargets/%s' % target, body=info) except exceptions.HTTPBadRequest as ex: pass def getVolumeSnapshots(self, name, live_test=True): """ Shows all snapshots associated with a given volume. :param name: The volume name :type name: str :returns: List of snapshot names """ uri = '/volumes?query="copyOf EQ %s"' % (name) response, body = self.http.get(uri) if live_test: snapshots = [] for volume in body['members']: if 'copyOf' in volume: if (volume['copyOf'] == name and volume['copyType'] == self.VIRTUAL_COPY): snapshots.append(volume['name']) return snapshots else: snapshots = [] for volume in body['members']: if re.match('SNAP', volume['name']): snapshots.append(volume['name']) return snapshots def _mergeDict(self, dict1, dict2): """ Safely merge 2 dictionaries together :param dict1: The first dictionary :type dict1: dict :param dict2: The second dictionary :type dict2: dict :returns: dict :raises Exception: dict1, dict2 is not a dictionary """ if type(dict1) is not dict: raise Exception("dict1 is not a dictionary") if type(dict2) is not dict: raise Exception("dict2 is not a dictionary") dict3 = dict1.copy() dict3.update(dict2) return dict3 def _get_next_word(self, s, search_string): """Return the next word. Search 's' for 'search_string', if found return the word preceding 'search_string' from 's'. """ word = re.search(search_string.strip(' ') + ' ([^ ]*)', s) return word.groups()[0].strip(' ') def getCPGStatData(self, name, interval='daily', history='7d'): """ Requests CPG performance data at a sampling rate (interval) for a given length of time to sample (history) :param name: a valid CPG name :type name: str :param interval: hourly, or daily :type interval: str :param history: xm for x minutes, xh for x hours, or xd for x days (e.g. 30m, 1.5h, 7d) :type history: str :returns: dict :raises: :class:`~hpe3parclient.exceptions.SrstatldException` - srstatld gives invalid output """ if interval not in ['daily', 'hourly']: raise exceptions.ClientException("Input interval not valid") if not re.compile("(\d*\.\d+|\d+)[mhd]").match(history): raise exceptions.ClientException("Input history not valid") cmd = ['srstatld', '-cpg', name, '-' + interval, '-btsecs', '-' + history] output = self._run(cmd) if not isinstance(output, list): raise exceptions.SrstatldException("srstatld output not a list") elif len(output) < 4: raise exceptions.SrstatldException("srstatld output list too " "short") elif len(output[-1].split(',')) < 16: raise exceptions.SrstatldException("srstatld output last line " "invalid") else: return self._format_srstatld_output(output) def _format_srstatld_output(self, out): """ Formats the output of the 3PAR CLI command srstatld Takes the total read/write value when possible :param out: the output of srstatld :type out: list :returns: dict """ line = out[-1].split(',') formatted = { 'throughput': float(line[4]), 'bandwidth': float(line[7]), 'latency': float(line[10]), 'io_size': float(line[13]), 'queue_length': float(line[14]), 'avg_busy_perc': float(line[15]) } return formatted def getSnapshotsOfVolume(self, snapcpgName, volName): """Gets list of snapshots of a volume. :param snapcpgName: The name of the CPG in which the volume snapshot(s) are present :type snapcpgName: str :param volName: The volume name for which the list of snapshots needs to be retrieved :type volName: str :returns: list of snapshots of volName """ uri = '/volumes?query="snapCPG EQ %s"' % (snapcpgName) response, body = self.http.get(uri) snapshots = [] for volume in body['members']: if 'copyOf' in volume: if (volume['copyOf'] == volName and volume['copyType'] == self.VIRTUAL_COPY): snapshots.append(volume['name']) return snapshots def getFlashCache(self): """Get information about flash cache on the 3Par array. :returns: list of Hosts """ response, body = self.http.get('/flashcache') return body def createFlashCache(self, sizeInGib, mode): """Creates a new FlashCache :param sizeInGib: Specifies the node pair size of the Flash Cache on the system. :type: int :param: mode : Simulator: 1 Real: 2 (default) :type: int :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - NO_SPACE - Not enough space is available for the operation. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_EXCEEDS_RANGE - A JSON input object contains a name-value pair with a numeric value that exceeds the expected range. Flash Cache exceeds the expected range. The HTTP ref member contains the name. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - EXISTENT_FLASH_CACHE - The Flash Cache already exists. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - FLASH_CACHE_NOT_SUPPORTED - Flash Cache is not supported. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_FLASH_CACHE_SIZE - Invalid Flash Cache size. The size must be a multiple of 16 G. """ flash_cache = {'sizeGiB': sizeInGib} if mode is not None: mode = {'mode': mode} flash_cache = self._mergeDict(flash_cache, mode) info = {'flashCache': flash_cache} response, body = self.http.post('/', body=info) return body def deleteFlashCache(self): """Deletes an existing Flash Cache :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - FLASH_CACHE_IS_BEING_REMOVED - Unable to delete the Flash Cache, the Flash Cache is being removed. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - FLASH_CACHE_NOT_SUPPORTED - Flash Cache is not supported on this system. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_FLASH_CACHE - The Flash Cache does not exist. """ self.http.delete('/flashcache') def resyncPhysicalCopy(self, volume_name): """Resynchronizes a physical copy. :param name - The name of the volume :type - string """ info = {'action': self.RESYNC_PHYSICAL_COPY} response = self.http.put("/volumes/%s" % (volume_name), body=info) return response[1] def admitRemoteCopyLinks( self, targetName, source_port, target_port_wwn_or_ip): """Adding remote copy link from soure to target. :param targetName - The name of target system :type - string :source_port - Source ethernet/Fibre channel port :type- string :target_port_wwn_or_ip- Target system's peer port WWN/IP :type- string """ source_target_port_pair = source_port + ':' + target_port_wwn_or_ip cmd = ['admitrcopylink', targetName, source_target_port_pair] response = self._run(cmd) if response != []: raise exceptions.SSHException(response) return response def dismissRemoteCopyLinks( self, targetName, source_port, target_port_wwn_or_ip): """Dismiss remote copy link from soure to target. :param targetName - The name of target system :type - string :source_port - Source ethernet/Fibre channel port :type- string :target_port_wwn_or_ip- Target system's peer port WWN/IP :type- string """ source_target_port_pair = source_port + ':' + target_port_wwn_or_ip cmd = ['dismissrcopylink', targetName, source_target_port_pair] response = self._run(cmd) if response != []: raise exceptions.SSHException(response) return response def startrCopy(self): """Starting remote copy service :param No """ cmd = ['startrcopy'] response = self._run(cmd) if response != []: raise exceptions.SSHException(response) return response def rcopyServiceExists(self): """Checking remote copy service status. :returns: True if remote copy service status is 'Started' : False if remote copy service status is 'Stopped' """ cmd = ['showrcopy'] response = self._run(cmd) rcopyservice_status = False if 'Started' in response[2]: rcopyservice_status = True return rcopyservice_status def getRemoteCopyLink(self, link_name): """ Querying specific remote copy link :returns: Specific remote copy link info """ response, body = self.http.get('/remotecopylinks/%s' % link_name) return body def rcopyLinkExists(self, targetName, local_port, target_system_peer_port): """Checking remote copy link from soure to target. :param targetName - The name of target system :type - string :source_port - Source ethernet/Fibre channel port :type- string :target_port_wwn_or_ip- Target system's peer port WWN/IP :type- string :returns: True if remote copy link exists : False if remote copy link doesn't exist """ cmd = ['showrcopy', 'links'] response = self._run(cmd) for item in response: if item.startswith(targetName): link_info = item.split(',') if link_info[0] == targetName and \ link_info[1] == local_port and \ link_info[2] == target_system_peer_port: return True return False def admitRemoteCopyTarget(self, targetName, mode, remote_copy_group_name, optional=None): """Adding target to remote copy group :param targetName - The name of target system :type - string :mode - synchronization mode :type - string :remote_copy_group_name :type - string :optional :type - dict .. code-block:: python optional = { "volumePairs": [{ "sourceVolumeName": "source_name", # The target volume # name associated with # this group. "targetVolumeName": "target_name" # The target volume # name associated with # this group. }] } """ cmd = ['admitrcopytarget', targetName, mode, remote_copy_group_name] if optional: volumePairs = optional.get('volumePairs') if volumePairs is not None: for volumePair in volumePairs: source_target_pair = \ volumePair['sourceVolumeName'] + ':' + \ volumePair['targetVolumeName'] cmd.append(source_target_pair) response = self._run(cmd) err_resp = self.check_response_for_admittarget(response, targetName) if err_resp: err = (("Admit remote copy target failed Error is\ '%(err_resp)s' ") % {'err_resp': err_resp}) raise exceptions.SSHException(err) return response def dismissRemoteCopyTarget(self, targetName, remote_copy_group_name): """Removing target from remote copy group :param targetName - The name of target system :type - string :remote_copy_group_name :type - string """ option = '-f' cmd = ['dismissrcopytarget', option, targetName, remote_copy_group_name] response = self._run(cmd) for message in response: if "has been dismissed from group" in message: return response raise exceptions.SSHException(response) def targetInRemoteCopyGroupExists( self, target_name, remote_copy_group_name): """Determines whether target is present in remote copy group. :param name: target_name :type name: str :remote_copy_group_name :type key: str :returns: bool """ try: contents = self.getRemoteCopyGroup(remote_copy_group_name) for item in contents['targets']: if item['target'] == target_name: return True except Exception: pass return False def remoteCopyGroupStatusCheck( self, remote_copy_group_name): """ Determines whether all volumes syncStatus is synced or not when remote copy group status is started. If all volumes syncStatus is 'synced' then it will return true else false :param remote_copy_group_name - Remote copy group name :type remote_copy_group_name: str :return: True: If remote copy group is started and all : volume syncStatus is 'synced' i.e. 3 : False: If remote copy group is started and some : volume status is not 'synced'. """ response = self.getRemoteCopyGroup(remote_copy_group_name) for target in response['targets']: if target['state'] != 3: return False for volume in response['volumes']: for each_target_volume in volume['remoteVolumes']: if each_target_volume['syncStatus'] != 3: return False return True def check_response_for_admittarget(self, resp, targetName): """ Checks whether command response having valid output or not if output is invalid then return that response. """ for r in resp: if 'error' in str.lower(r) or 'invalid' in str.lower(r) \ or 'must specify a mapping' in str.lower(r) \ or 'not exist' in str.lower(r) \ or 'no target' in str.lower(r) \ or 'group contains' in str.lower(r) \ or 'Target is already in this group.' in str(r) \ or 'could not locate an indicated volume.' in str(r) \ or 'Target system %s could not be contacted' % targetName \ in str(r) \ or 'Target %s could not get info on secondary target' \ % targetName in str(r) \ or 'Target %s is not up and ready' % targetName in str(r) \ or 'A group may have only a single synchronous target.' \ in str(r) or \ 'cannot have groups with more than one ' \ 'synchronization mode' \ in str.lower(r): return r def check_response(self, resp): for r in resp: if 'error' in str.lower(r) or 'invalid' in str.lower(r): err_resp = r.strip() return err_resp def createSchedule(self, schedule_name, task, taskfreq): """Create Schedule for volume snapshot. :param schedule_name - The name of the schedule :type - string :param task - command to for which schedule is created :type - string :param taskfreq - frequency of schedule :type - string """ cmd = ['createsched'] cmd.append("\"" + task + "\"") if '@' not in taskfreq: cmd.append("\"" + taskfreq + "\"") else: cmd.append(taskfreq) cmd.append(schedule_name) try: resp = self._run(cmd) err_resp = self.check_response(resp) if err_resp: raise exceptions.SSHException(err_resp) else: for r in resp: if str.lower('The schedule format is \ or by @hourly @daily @monthly @weekly @monthly \ @yearly') in str.lower(r): raise exceptions.SSHException(r.strip()) except exceptions.SSHException as ex: raise exceptions.SSHException(ex) def deleteSchedule(self, schedule_name): """Delete Schedule :param schedule_name - The name of the schedule to delete :type - string """ cmd = ['removesched', '-f', schedule_name] try: resp = self._run(cmd) err_resp = self.check_response(resp) if err_resp: err = (("Delete snapschedule failed Error is\ '%(err_resp)s' ") % {'err_resp': err_resp}) raise exceptions.SSHException(reason=err) except exceptions.SSHException as ex: raise exceptions.SSHException(reason=ex) def getSchedule(self, schedule_name): """Get Schedule :param schedule_name - The name of the schedule to get information :type - string """ cmd = ['showsched ', schedule_name] try: result = self._run(cmd) for r in result: if 'No scheduled tasks ' in r: msg = "Couldn't find the schedule '%s'" % schedule_name raise exceptions.SSHNotFoundException(msg) except exceptions.SSHNotFoundException as ex: raise exceptions.SSHNotFoundException(ex) return result def modifySchedule(self, name, schedule_opt): """Modify Schedule. :param name - The name of the schedule :type - string :param schedule_opt - :type schedule_opt - dictionary of option to be modified .. code-block:: python mod_request = { 'newName': 'myNewName', # New name of the schedule 'taskFrequency': '0 * * * *' # String containing cron or # @monthly, @hourly, @daily, # @yearly and @weekly. } """ cmd = ['setsched'] if 'newName' in schedule_opt: cmd.append('-name') cmd.append(schedule_opt['newName']) if 'taskFrequency' in schedule_opt: cmd.append('-s') if '@' not in schedule_opt['taskFrequency']: cmd.append("\"" + schedule_opt['taskFrequency'] + "\"") else: cmd.append(schedule_opt['taskFrequency']) cmd.append(name) try: resp = self._run(cmd) err_resp = self.check_response(resp) if err_resp: raise exceptions.SSHException(err_resp) else: for r in resp: if str.lower('The schedule format is \ or by @hourly @daily @monthly @weekly @monthly \ @yearly') in str.lower(r): raise exceptions.SSHException(r.strip()) except exceptions.SSHException as ex: raise exceptions.SSHException(ex) def suspendSchedule(self, schedule_name): """Suspend Schedule :param schedule_name - The name of the schedule to get information :type - string """ cmd = ['setsched', '-suspend', schedule_name] try: resp = self._run(cmd) err_resp = self.check_response(resp) if err_resp: err = (("Schedule suspend failed Error is\ '%(err_resp)s' ") % {'err_resp': err_resp}) raise exceptions.SSHException(reason=err) except exceptions.SSHException as ex: raise exceptions.SSHException(reason=ex) def resumeSchedule(self, schedule_name): """Resume Schedule :param schedule_name - The name of the schedule to get information :type - string """ cmd = ['setsched', '-resume', schedule_name] try: resp = self._run(cmd) err_resp = self.check_response(resp) if err_resp: err = (("Schedule resume failed Error is\ '%(err_resp)s' ") % {'err_resp': err_resp}) raise exceptions.SSHException(reason=err) except exceptions.SSHException as ex: raise exceptions.SSHException(reason=ex) def remoteCopyGroupStatusStartedCheck( self, remote_copy_group_name): """ Checks whether remote copy group status is started or not :param remote_copy_group_name - Remote copy group name :type remote_copy_group_name: str :return: True: If remote copy group is in started : state i.e. 3 : False: If remote copy group is not in started : state """ response = self.getRemoteCopyGroup(remote_copy_group_name) status_started_counter = 0 for target in response['targets']: if target['state'] == 3: status_started_counter += 1 if status_started_counter == len(response['targets']): return True else: return False def remoteCopyGroupStatusStoppedCheck( self, remote_copy_group_name): """ Checks whether remote copy group status is stopped or not :param remote_copy_group_name - Remote copy group name :type remote_copy_group_name: str :return: True: If remote copy group is in stopped : state i.e. 5 : False: If remote copy group is not in started : state """ response = self.getRemoteCopyGroup(remote_copy_group_name) status_stopped_counter = 0 for target in response['targets']: if target['state'] == 5: status_stopped_counter += 1 if status_stopped_counter == len(response['targets']): return True else: return False def getScheduleStatus(self, schedule_name): """ Checks schedule status active/suspended and returns it. :param schedule_name - Schedule name :type schedule_name: str :return: active/suspended """ result = self.getSchedule(schedule_name) for r in result: if 'suspended' in r: return 'suspended' elif 'active' in r: return 'active' msg = "Couldn't find the schedule '%s' status" % schedule_name raise exceptions.SSHException(reason=msg) @staticmethod def convert_cli_output_to_wsapi_format(cli_output): """Convert CLI output into a response that looks the WS-API would. Use the first line as coma-separated headers. Build dictionaries for the remaining lines using the headers as keys. Return a dictionary with total and members (the dictionaries). If there isn't enough data for headers and data then total is 0 and members is an empty list. If you need more validity checking, you might want to do it before this generic routine. It does minimal checking. :param cli_output: The result from the CLI (i.e. from ssh.run(cmd)). The first row is headers. Following rows are data. :type cli_output: list .. code-block:: python # Example 1: Typical CLI output with header row and data rows. cli_output = [ 'InstallTime,Id,Package,Version', '2013-08-21 18:06:45 PDT,MU2,Complete,3.1.2.422', '2013-10-10 15:20:05 PDT,MU3,Complete,3.1.2.484', '2014-01-30 11:34:20 PST,DEVEL,Complete,3.1.3.170', '2014-03-26 13:59:42 PDT,GA,Complete,3.1.3.202', '2014-06-06 14:46:56 PDT,MU1,Complete,3.1.3.230' ] # Example 2: Example CLI output for an empty result. cli_output = ['No patch is applied to the system.'] :returns: dict with total and members. members is list of dicts using header for keys and data for values. :rtype: dict .. code-block:: python # Example 1: Converted to total and members list of dictionaries. ret = { 'total': 5, 'members': [ { 'Package': 'Complete', 'Version': '3.1.2.422', 'InstallTime': '2013-08-21 18:06:45 PDT', 'Id': 'MU2' }, { 'Package': 'Complete', 'Version': '3.1.2.484', 'InstallTime': '2013-10-10 15:20:05 PDT', 'Id': 'MU3' }, { 'Package': 'Complete', 'Version': '3.1.3.170', 'InstallTime': '2014-01-30 11:34:20 PST', 'Id': 'DEVEL' }, { 'Package': 'Complete', 'Version': '3.1.3.202', 'InstallTime': '2014-03-26 13:59:42 PDT', 'Id': 'GA' }, { 'Package': 'Complete', 'Version': '3.1.3.230', 'InstallTime': '2014-06-06 14:46:56 PDT', 'Id': 'MU1' } ] } # Example 2: No data rows, so zero members. ret = {'total': 0, 'members': []} """ members = [] if cli_output and len(cli_output) >= 2: for index, line in enumerate(cli_output): if index == 0: headers = line.split(',') else: split = line.split(',') member = {} for i, header in enumerate(headers): try: member[header] = split[i] except IndexError: member[header] = None members.append(member) return {'total': len(members), 'members': members} @staticmethod def _getSshClient(ip, login, password, port=22, conn_timeout=None, privatekey=None, **kwargs): ssh_client = ssh.HPE3PARSSHClient(ip, login, password, port, conn_timeout, privatekey, **kwargs) return ssh_client @staticmethod def getPortNumber(ip, login, password, port=22, conn_timeout=None, privatekey=None, **kwargs): """Get port number from showwsapi output :param 3PAR credentials :return: HTTPS_Port column value """ try: ssh_client = HPE3ParClient._getSshClient(ip, login, password, port, conn_timeout, privatekey, **kwargs) if ssh_client is None: raise exceptions.SSHException("SSH is not initialized.\ Initialize it by calling 'setSSHOptions'.") ssh_client.open() cli_output = ssh_client.run(['showwsapi']) wsapi_dict = HPE3ParClient.convert_cli_output_to_wsapi_format( cli_output) return wsapi_dict['members'][0]['HTTPS_Port'] finally: if ssh_client: ssh_client.close() def tuneVolume(self, volName, tune_operation, optional=None): """Tune a volume. :param name: the name of the volume :type name: str :param name: tune_operation 1 for USR_CPG 2 for SNP_CPG :type name: int :param optional: dictionary of volume attributes to change :type optional: dict .. code-block:: python optional = { 'action': 6, # For tuneVolume operation 'userCPG': 'User CPG name', # Required if tuneOperation is 1 'snapCPG': 'Snap CPG name', # Required if tuneOperation is 2 'conversionOperation': 1, # For TPVV 1, For FPVV 2, For TDVV # 3, for CONVERT_TO_DECO 4 'compression': False, # compression is not supported for # FPVV } :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - CPG_NOT_IN_SAME_DOMAIN - Snap CPG is not in the same domain as the user CPG. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - INV_INPUT_ILLEGAL_CHAR - Invalid VV name or CPG name. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_INPUT_VV_IS_FPVV - The volume is already fully provisioned. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_INPUT_VV_IS_TDVV - The volume is already deduplicated. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_INPUT_VV_IS_TPVV - The volume is already thinly provisioned. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_UNSUPPORTED_VV_TYPE - Invalid operation: Cannot grow this type of volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_MODIFY_USR_CPG_TDVV - Cannot change USR CPG of a TDVV to a different CPG.. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_NON_BASE_VOLUME - The destination volume is not a base volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_SYS_VOLUME - The volume is a system volume. This operation is not allowed on a system volume. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_CLEANUP_IN_PROGRESS - Internal volume cleanup is in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_INTERNAL_VOLUME - Cannot modify an internal volume :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_VOLUME_CONV_IN_PROGRESS - Invalid operation: VV conversion is in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_NOT_IN_NORMAL_STATE - Volume state is not normal :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_OPERATION_VV_PEER_VOLUME - Cannot modify a peer volume. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_TASK_CANCEL_IN_PROGRESS - Invalid operation: A task involving the volume is being canceled.. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_PROMOTE_IN_PROGRESS - Invalid operation: Volume promotion is in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPConflict` - INV_OPERATION_VV_TUNE_IN_PROGRESS - Invalid operation: Volume tuning is in progress. :raises: :class:`~hpe3parclient.exceptions.HTTPBadRequest` - NO_SPACE - Not Enough space is available :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - NODE_DOWN - The node is down. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_CPG - The CPG does not exists. :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound` - NON_EXISTENT_VOL - volume doesn't exist :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IN_INCONSISTENT_STATE - The volume has an internal consistency error. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_IS_BEING_REMOVED - The volume is being removed. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_NEEDS_TO_BE_CHECKED - The volume needs to be checked. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - VV_NOT_STARTED - Volume is not started. :raises: :class:`~hpe3parclient.exceptions.HTTPForbidden` - INV_INPUT_VV_IS_FPVV - A fully provisioned volume cannot be compressed. """ info = {'action': self.TUNE_VOLUME, 'tuneOperation': tune_operation} if optional is not None and not self.compression_supported: if 'compression' in optional.keys() \ and optional.get('compression') is False: del optional['compression'] if optional: if self.primera_supported: if optional.get('compression') is True: if optional.get('conversionOperation') == self.TDVV: optional['conversionOperation'] = self.CONVERT_TO_DECO optional.pop('compression') else: raise exceptions.HTTPBadRequest("invalid input: On\ primera array, with 'compression' set to true 'tdvv' must be true") else: if optional.get('conversionOperation') == self.TDVV: raise exceptions.HTTPBadRequest("invalid input: On\ primera array, for compression and deduplicated volume 'tdvv' should be true\ and 'compression' must be specified as true") elif optional.get('conversionOperation') == self.TPVV: if 'compression' in optional.keys(): optional.pop('compression') elif optional.get('conversionOperation') ==\ self.CONVERT_TO_DECO: if 'compression' in optional.keys(): optional.pop('compression') elif optional.get('conversionOperation') == self.FPVV: raise exceptions.HTTPBadRequest("invalid input:\ On primera array 'fpvv' is not supported") info = self._mergeDict(info, optional) response, body = self.http.put( '/volumes/%s' % volName, body=info) return body def _cancelTask(self, taskId): info = {'action': 1} try: self.http.put('/tasks/%s' % taskId, body=info) except exceptions.HTTPBadRequest as ex: # it means task cannot be cancelled, # because it is 'done' or already 'cancelled' pass python-3parclient-4.2.12/hpe3parclient/exceptions.py0000644000000000000000000003007314106434774022432 0ustar rootroot00000000000000# vim: tabstop=4 shiftwidth=4 softtabstop=4 # # (c) Copyright 2012-2015 Hewlett Packard Enterprise Development LP # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Exceptions for the client .. module: exceptions :Author: Walter A. Boring IV :Description: This contains the HTTP exceptions that can come back from the REST calls to 3PAR """ import logging # Python 3+ override try: basestring except NameError: basestring = str LOG = logging.getLogger(__name__) class UnsupportedVersion(Exception): """ Indicates that the user is trying to use an unsupported version of the API """ pass class CommandError(Exception): pass class AuthorizationFailure(Exception): pass class NoUniqueMatch(Exception): pass class ClientException(Exception): """ The base exception class for all exceptions this library raises. :param error: The error array :type error: array """ _error_code = None _error_desc = None _error_ref = None _debug1 = None _debug2 = None def __init__(self, error=None): super(ClientException, self).__init__() if not error: return if isinstance(error, basestring): # instead of KeyError below, take it and make it the _error_desc. self._error_desc = error else: if 'code' in error: self._error_code = error['code'] if 'desc' in error: self._error_desc = error['desc'] if 'ref' in error: self._error_ref = error['ref'] if 'debug1' in error: self._debug1 = error['debug1'] if 'debug2' in error: self._debug2 = error['debug2'] def get_code(self): return self._error_code def get_description(self): return self._error_desc def get_ref(self): return self._error_ref def __str__(self): formatted_string = self.message if self.http_status: formatted_string += " (HTTP %s)" % self.http_status if self._error_code: formatted_string += " %s" % self._error_code if self._error_desc: formatted_string += " - %s" % self._error_desc if self._error_ref: formatted_string += " - %s" % self._error_ref if self._debug1: formatted_string += " (1: '%s')" % self._debug1 if self._debug2: formatted_string += " (2: '%s')" % self._debug2 return formatted_string # SSL Errors class SSLCertFailed(ClientException): """ The SSL certificate from the server could not be verified """ http_status = "" message = "SSL Certificate Verification Failed" # Python Requests Errors class RequestException(ClientException): """ There was an ambiguous exception that occurred in Requests """ pass class ConnectionError(ClientException): """ There was an error connecting to the server """ pass class HTTPError(ClientException): """ An HTTP error occurred """ pass class URLRequired(ClientException): """ A valid URL is required to make a request """ pass class TooManyRedirects(ClientException): """ Too many redirects """ pass class Timeout(ClientException): """ The request timed out """ pass # 400 Errors class HTTPBadRequest(ClientException): """ HTTP 400 - Bad request: you sent some malformed data. """ http_status = 400 message = "Bad request" class HTTPUnauthorized(ClientException): """ HTTP 401 - Unauthorized: bad credentials. """ http_status = 401 message = "Unauthorized" class HTTPForbidden(ClientException): """ HTTP 403 - Forbidden: your credentials don't give you access to this resource. """ http_status = 403 message = "Forbidden" class HTTPNotFound(ClientException): """ HTTP 404 - Not found """ http_status = 404 message = "Not found" class HTTPMethodNotAllowed(ClientException): """ HTTP 405 - Method not Allowed """ http_status = 405 message = "Method Not Allowed" class HTTPNotAcceptable(ClientException): """ HTTP 406 - Method not Acceptable """ http_status = 406 message = "Method Not Acceptable" class HTTPProxyAuthRequired(ClientException): """ HTTP 407 - The client must first authenticate itself with the proxy. """ http_status = 407 message = "Proxy Authentication Required" class HTTPRequestTimeout(ClientException): """ HTTP 408 - The server timed out waiting for the request. """ http_status = 408 message = "Request Timeout" class HTTPConflict(ClientException): """ HTTP 409 - Conflict: A Conflict happened on the server """ http_status = 409 message = "Conflict" class HTTPGone(ClientException): """ HTTP 410 - Indicates that the resource requested is no longer available and will not be available again. """ http_status = 410 message = "Gone" class HTTPLengthRequired(ClientException): """ HTTP 411 - The request did not specify the length of its content, which is required by the requested resource. """ http_status = 411 message = "Length Required" class HTTPPreconditionFailed(ClientException): """ HTTP 412 - The server does not meet one of the preconditions that the requester put on the request. """ http_status = 412 message = "Over limit" class HTTPRequestEntityTooLarge(ClientException): """ HTTP 413 - The request is larger than the server is willing or able to process """ http_status = 413 message = "Request Entity Too Large" class HTTPRequestURITooLong(ClientException): """ HTTP 414 - The URI provided was too long for the server to process. """ http_status = 414 message = "Request URI Too Large" class HTTPUnsupportedMediaType(ClientException): """ HTTP 415 - The request entity has a media type which the server or resource does not support. """ http_status = 415 message = "Unsupported Media Type" class HTTPRequestedRangeNotSatisfiable(ClientException): """ HTTP 416 - The client has asked for a portion of the file, but the server cannot supply that portion. """ http_status = 416 message = "Requested Range Not Satisfiable" class HTTPExpectationFailed(ClientException): """ HTTP 417 - The server cannot meet the requirements of the Expect request-header field. """ http_status = 417 message = "Expectation Failed" class HTTPTeaPot(ClientException): """ HTTP 418 - I'm a Tea Pot """ http_status = 418 message = "I'm A Teapot. (RFC 2324)" # 500 Errors class HTTPInternalServerError(ClientException): """ HTTP 500 - Internal Server Error: an internal error occured. """ http_status = 500 message = "Internal Server Error" class HTTPNotImplemented(ClientException): """ HTTP 501 - Not Implemented: the server does not support this operation. """ http_status = 501 message = "Not Implemented" class HTTPBadGateway(ClientException): """ HTTP 502 - The server was acting as a gateway or proxy and received an invalid response from the upstream server. """ http_status = 502 message = "Bad Gateway" class HTTPServiceUnavailable(ClientException): """ HTTP 503 - The server is currently unavailable """ http_status = 503 message = "Service Unavailable" class HTTPGatewayTimeout(ClientException): """ HTTP 504 - The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. """ http_status = 504 message = "Gateway Timeout" class HTTPVersionNotSupported(ClientException): """ HTTP 505 - The server does not support the HTTP protocol version used in the request. """ http_status = 505 message = "Version Not Supported" # In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() # so we can do this: # _code_map = dict((c.http_status, c) # for c in ClientException.__subclasses__()) # # Instead, we have to hardcode it: _code_map = dict((c.http_status, c) for c in [HTTPBadRequest, HTTPUnauthorized, HTTPForbidden, HTTPNotFound, HTTPMethodNotAllowed, HTTPNotAcceptable, HTTPProxyAuthRequired, HTTPRequestTimeout, HTTPConflict, HTTPGone, HTTPLengthRequired, HTTPPreconditionFailed, HTTPRequestEntityTooLarge, HTTPRequestURITooLong, HTTPUnsupportedMediaType, HTTPRequestedRangeNotSatisfiable, HTTPExpectationFailed, HTTPTeaPot, HTTPNotImplemented, HTTPBadGateway, HTTPServiceUnavailable, HTTPGatewayTimeout, HTTPVersionNotSupported, HTTPInternalServerError]) def from_response(response, body): """ Return an instance of an ClientException or subclass based on a Python Requests response. Usage:: resp, body = http.request(...) if resp.status != 200: raise exception_from_response(resp, body) """ cls = _code_map.get(response.status, ClientException) return cls(body) class SSHException(Exception): """This is the basis for the SSH Exceptions.""" code = 500 message = "An unknown exception occurred." def __init__(self, message=None, **kwargs): self.kwargs = kwargs if 'code' not in self.kwargs: try: self.kwargs['code'] = self.code except AttributeError: pass if not message: try: message = self.message % kwargs except Exception: # kwargs doesn't match a variable in the message # log the issue and the kwargs LOG.exception('Exception in string format operation') for name, value in list(kwargs.items()): LOG.error("%s: %s" % (name, value)) # at least get the core message out if something happened message = self.message self.msg = message super(SSHException, self).__init__(message) class SSHInjectionThreat(SSHException): message = "SSH command injection detected: %(command)s" class GrowVolumeException(SSHException): message = "SSH grow volume failed: %(command)s" class CopyVolumeException(SSHException): message = "SSH copy volume failed: %(command)s" class SetQOSRuleException(SSHException): message = "SSH set QOS rule failed: %(command)s" class SrstatldException(SSHException): message = "SSH command failed: %(command)s" class SSHNotFoundException(SSHException): message = "SSH command failed: %(command)s" class ProcessExecutionError(Exception): def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None, description=None): self.exit_code = exit_code self.stderr = stderr self.stdout = stdout self.cmd = cmd self.description = description if description is None: description = "Unexpected error while running command." if exit_code is None: exit_code = '-' message = ("%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % (description, cmd, exit_code, stdout, stderr)) super(ProcessExecutionError, self).__init__(message) python-3parclient-4.2.12/hpe3parclient/file_client.py0000644000000000000000000021277414106434774022540 0ustar rootroot00000000000000# (c) Copyright 2015 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ HPE 3PAR File Persona Client .. module: file_client .. moduleauthor: Mark Sturdevant :Author: Mark Sturdevant :Description: Client for 3PAR File Persona. This module provides a client for File Persona functionality. The File Persona client requires 3PAR InForm 3.2.1 (MU3) with File Persona capability. This client extends the regular 3PAR client. """ import logging from functools import wraps from hpe3parclient import client from hpe3parclient import tcl_parser TCL = tcl_parser.HPE3ParTclParser() LOG = logging.getLogger(__name__) # Commands that require -f (non-interactive) flag register with _force_me FORCE_ME = {} # Commands that require -d (get details) flag register with _get_details GET_DETAILS = {} # Commands that require the protocol arg before any option flags register here PROTOCOL_FIRST = {} class HPE3ParFilePersonaClient(client.HPE3ParClient): """ The 3PAR File Persona Client. The File Persona client requires 3PAR InForm 3.2.1 (MU3) with File Persona capability :param api_url: The url to the WSAPI service on 3PAR ie. http://<3par server>:8080/api/v1 :type api_url: str """ # File Persona minimum WSAPI overrides minimum for non-File Persona. HPE3PAR_WS_MIN_BUILD_VERSION = 30201256 HPE3PAR_WS_MIN_BUILD_VERSION_DESC = '3.2.1 (MU3)' def __init__(self, api_url, secure=False): super(self.__class__, self).__init__(api_url, secure=secure) self.interfaces = None @staticmethod def _build_command(command, *args, **kwargs): """Build a SSH/CLI command based on command + args + kwargs. A built cmd will look like: command [OPTIONS] [SPECIFIERS] It is returned as an array of strings to be passed to ssh.run. command can be a string or list of strings. If it is a string, it will be split(). This allows commands with flags like 'doit -v' to work. Passing in ['doit','-v'] has the same result. Options are taken from kwargs. The CLI option will be a hyphen followed by the kwarg key. E.g., -key. If the value is None, then the option is skipped. If the value is a boolean, then False options are ignored and True options are set as flags. E.g., '-debug' with no value after it. Note: a _quoted_ 'true' or 'false' is treated as a string (below) and this is important to allow so that we can also do '-enabled true'. Options with non-boolean values will be used like '-key value' (of course returned as '-key', 'value' in the array). Since embedded spaces in a value are not allowed by the injection checker without quoting (and quoting inside strings is not natural) kwargs with key named 'comment' will automatically have their value quoted. :returns: list of strings """ # Command is a string or a list like ['doit','-f'] if isinstance(command, str): # If a string, split it into a list. cmd = command.split() else: cmd = command # Some commands require the protocol {smb|nfs} before the option flags. # To use this abnormal pattern those methods register in # PROTOCOL_FIRST. if cmd[0] in PROTOCOL_FIRST: # Add the protocol (args[1]) to the command and eat it. protocol = args[1] cmd.append(protocol) new_args = [args[0]] new_args.extend(args[2:]) args = new_args # Some commands require the -f flag so that they are non-interactive. # Those methods register in FORCE_ME. if cmd[0] in FORCE_ME: cmd.append('-f') # -f is forced to True (we are non-interactive). # If we find it in a kwarg, eat it. force = kwargs.pop('f', True) if not force: # If anyone thought they could override this. Log it. LOG.info( "Ignoring f=False. Always non-interactive for %s." % cmd[0]) # Some commands require the -d flag so that we get details (because # our parser might expect them). # Those methods register in GET_DETAILS. if cmd[0] in GET_DETAILS: cmd.append('-d') # 'd' is in signature for completeness, but is forced to True # Eat it. details = kwargs.pop('d', True) if not details: # If anyone thought they could override this. Log it. LOG.info( "Ignoring d=False. Always getting details for %s." % cmd[0]) # Add the options if len(kwargs) > 0: for k, v in list(kwargs.items()): if isinstance(v, bool): # Boolean, just add a flag if true. No value. if v: cmd.append('-%s' % k) elif v: # Non-Boolean. Add -opt value, if not None. cmd.append("-%s" % k) if k == 'comment': # Quoting needed for comments (spaces) cmd.append('"%s"' % v) else: cmd.append(v) # Add the specifiers if len(args) > 1: cmd.extend(args[1:]) return cmd @staticmethod def _build_command_and_run_with_ssh(cmd, *args, **kwargs): """Build CLI command from cmd, args, kwargs and run it with ssh.""" client = args[0] # first arg is the client itself command = HPE3ParFilePersonaClient._build_command(cmd, *args, **kwargs) client.ssh.open() return client.ssh.run(command, multi_line_stripper=True) def _run_with_cli(func): """Decorator to build command from method signature and run SSH/CLI. The command is built from the method name, args and kwargs. The results of the CLI command are returned. The actual results of the original method are not used, but the method will be ran so that prints, logging can be used. """ @wraps(func) def wrapper(*args, **kwargs): func(*args, **kwargs) return HPE3ParFilePersonaClient._build_command_and_run_with_ssh( func.__name__, *args, **kwargs) return wrapper def _get_details(func): """Decorator for a command/method that needs the -d flag. Our parser might only recognize data with details, so always set d=True in these cases. """ @wraps(func) def wrapper(*args, **kwargs): GET_DETAILS[func.__name__] = True func(*args, **kwargs) return wrapper def _force_me(func): """Decorator for a command/method that needs the -f flag. When used, this decorator should be below the @_run_with_cli decorator. """ @wraps(func) def wrapper(*args, **kwargs): FORCE_ME[func.__name__] = True func(*args, **kwargs) return wrapper def _protocol_first(func): """Decorator for a command/method that needs protocol arg before flags. When used, this decorator should be below the @_run_with_cli decorator. """ @wraps(func) def wrapper(*args, **kwargs): PROTOCOL_FIRST[func.__name__] = True func(*args, **kwargs) return wrapper def _set_key_value(self, dictionary, key, value): """Set value for key in dictionary with some special key treatment.""" if key == 'comment' and isinstance(value, list): # Special treatment for comments. They are the one thing that # we don't want ['split', 'into', 'a', 'nice', 'list']. # Put them back together. dictionary[key] = ' '.join(value) elif key == 'vfsip' and value: # Expand the sub-fsips like in getfsip. interface = self.gettpdinterface()['getfsipInd'] dictionary[key] = [] for vfsip in value: dictionary[key].append(self._create_member(interface, vfsip)) else: dictionary[key] = value def _wrap_tpd_interface(func): """Take gettpdinterface results and set interfaces.""" def get_interface_keys(interface): keys = [] key_pos_pairs = interface[1] for pair in key_pos_pairs: key = pair[0] split_key = key.split(',') if len(split_key) == 1: keys.append(key) else: current_key = keys[-1] sub_key = split_key[1] if isinstance(current_key, str): # If this is current_key,sub_key, then # make current_key (current_key, [sub_key]) current_key = (current_key, [sub_key]) else: # Already (current_key, [sub_key,...] # Append the new sub_key to list. current_key[1].append(sub_key) keys[-1] = current_key return keys @wraps(func) def wrapper(*args, **kwargs): client = args[0] # first arg is the client itself cached = client.interfaces if cached is not None: return cached result = func(*args, **kwargs) # Since there are many, we ignore interfaces not in this list. # This reduces memory use (and also allows our fake server to work) supported_commands = [ 'getfsharenfsInd', 'getfsharesmbInd', 'getfsipInd', 'getfsnapInd', 'getfsnapcleanInd', 'getfsquotaInd', 'getfspoolInd', 'getfssystemInd', 'getfstoreInd', 'getvfsInd', ] tpd_interfaces = TCL.parse_tcl(result[0]) interfaces = {} for interface in tpd_interfaces: interface_name = interface[0] if interface_name in supported_commands: interfaces[interface_name] = get_interface_keys(interface) # Cache the interfaces client.interfaces = interfaces return interfaces return wrapper def _parse_members(self, keys, data): members = [] if data: if len(keys) > 1 and isinstance(data[0], list): members = [] # list of members (list of lists) for values in data: member = dict(list(zip(keys, values))) members.append(member) else: # 1 member (list of values) members = dict(list(zip(keys, data))) return members def _create_member(self, interface, values): member = {} for index, item in enumerate(values): if isinstance(interface[index], str): key = interface[index] self._set_key_value(member, key, item) else: header, keys = interface[index] sub_list = self._parse_members(keys, item) member[header] = sub_list return member def _wrap_tcl(func): """Turn TCL output into Python dicts.""" @wraps(func) def wrapper(*args, **kwargs): client = args[0] # first arg is the client itself command = func.__name__ result = func(*args, **kwargs) if not result: return { 'message': None, 'total': 0, 'members': [] } # Combine output lines into one string result_str = ''.join(result) # Non-TCL output is just a message (probably an error message) if not result_str.startswith('{'): return { 'message': result_str, 'total': 0, 'members': [] } single = False # Get interface for command (some special cases) if command == 'getfpg': # Interface uses old "fspool" name interface_id = 'getfspoolInd' elif command == 'getfs': # Get the whole thing with getfssystemInd single = True interface_id = 'getfssystemInd' elif command == 'getfshare' and args[1] == 'nfs': interface_id = 'getfsharenfsInd' elif command == 'getfshare' and args[1] == 'smb': interface_id = 'getfsharesmbInd' else: # Normal case should be cmd + 'Ind' interface_id = '%sInd' % command interface = client.gettpdinterface()[interface_id] parsed_result = TCL.parse_tcl(result_str) # getfsip has extra level of nesting. if command == 'getfsip': parsed_result = parsed_result[0] # getfsnapclean added a special 'not running' message # Filter out that garbage. if command == 'getfsnapclean': not_running = ['No', 'reclamation', 'task', 'running'] parsed_result = [x for x in parsed_result if x[0:4] != not_running] if single: # Single member (need to add to list) members = [client._create_member(interface, parsed_result)] else: members = [] for values in parsed_result: members.append(client._create_member(interface, values)) return {'members': members, 'total': len(members), 'message': None} return wrapper @_wrap_tpd_interface @_run_with_cli def gettpdinterface(self): """Get and parse TPD interfaces (and set for re-use). The output is filtered to only include interfaces used by this client. :return: Dictionary of TPD interfaces """ @_wrap_tcl @_run_with_cli def getfs(self): """Show information on File Services cluster. The getfs command displays information on File Services nodes. :return: dict with message, total and members .. code-block:: python result = { 'message': None, # Error message, if any. 'total': 0 # Number of members returned. 'members': [], # List containing dict of FS settings. } """ @_run_with_cli def createfpg(self, cpgname, fpgname, size, comment=None, node=None, full=False, wait=False): """Create a file provisioning group. The createfpg command creates a file provisioning group of the given name and size within the specified cpg. For this command MB = 1048576 bytes, GB = 1024MB, and TB = 1024GB. :param cpgname: The CPG where the VVs associated with the file provisioning group will be created :param fpgname: The name of the file provisioning group to be created :param size: The size of the file provisioning group to be created. The specified size must be between 1T and 32T. A suffix (with no whitespace before the suffix) will modify the units to GB (g or G suffix) or TB (t or T suffix). :param comment: Specifies the textual description of the file provisioning group. :param node: Bind the created file provisioning group to the specified node. :param full: Create the file provisioning group using fully provisioned volumes. :param wait: Wait until the associated task is completed before proceeding. This option will produce verbose task information. :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli def growfpg(self, fpgname, size): """Grow a file provisioning group. The growfpg command grows a file provisioning group of the given name by the size specified, within the CPG associated with the base file provisioning group. For each grow undertaken, at least one additional VV of name .n is created. :param fpgname: The name of the filesystem to be grown. :param size: The size of the filesystem to be grown. :return: List of strings. Lines of output from the CLI command. """ @_wrap_tcl @_run_with_cli def getfpg(self, *fpgs): """Show file provisioning group information The getfpg command displays information on file provisioning groups :param fpgs: Limit output to the specified file provisioning group. :return: dict with message, total and members .. code-block:: python result = { 'message': None, # Error message, if any. 'total': 0 # Number of members returned. 'members': [], # List containing dict of FPGs } """ @_run_with_cli @_force_me def setfpg(self, fpgname, comment=None, rmcomment=False, activate=False, deactivate=False, primarynode=None, failover=False, forced=False): """Modify the properties of a File Provisioning Group. The setfpg command allows the user to enable and disable various properties associated with a File Provisioning Group. Access to all domains is required to run this command. The -primarynode and -failover options are mutually exclusive. When assigning primary nodes, the secondary node will be implicit as a couplet pair [0,1] [2,3] [4,5] [6,7]. This action will fail if the graceful failover is not possible. The -failover and -primarynode options will result in temporary unavailability of the Virtual File Servers associated with the File Provisioning Group being migrated, and also the unavailability of any associated shares. An implicit -deactivate and -activate process is undertaken during a migration to the alternate node. :param fpgname: The name of the file provisioning group to be modified. :param comment: Specifies any addition textual information. :param rmcomment: Clears the comment string. :param activate: Makes the File Provisioning Group available. :param deactivate: Makes the File Provisioning Group unavailable. :param primarynode: Specifies the primary node to which the File Provisioning Group will be assigned. Appropriate values are defined as those on which file services has been enabled. :param failover: Specifies that the File Provisioning Group should be failed over to its alternate node. If it has previously failed over to the secondary, this will cause it to fail back to the primary node. Will fail if a graceful failover is not possible. :param forced: In the event of failure to failover, this will attempt a forced failover. :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli @_force_me def removefpg(self, *fpgname, **kwargs): r"""Remove a file provisioning group The removefpg command removes a file provisioning group and its underlying components from the system. It is necessary to remove any shares on the file provisioning group before removing the file provisioning group itself. :param fpgname: fpgname is the name of the file provisioning group(s) to be removed. This specifier can be repeated to remove multiple file provisioning groups. When used with pat=True, specifies a glob-style pattern. This specifier can be repeated to remove multiple file provisioning groups. :param \**kwargs: See below. :kwargs: * **forget** -- Removes the specified file provisioning group which is involved in Remote DR, keeping the virtual volume intact. * **wait** -- Wait until the associated task is completed before proceeding. This option will produce verbose task information. * **pat** -- The fpgname parameter is a glob-style pattern. :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli def createvfs(self, ipaddr, subnet, vfsname, nocert=False, certfile=None, certdata=None, comment=None, bgrace=None, igrace=None, fpg=None, cpg=None, size=None, node=None, vlan=None, wait=False): """Create a Virtual File Server. createvfs creates a Virtual File Server. It can optionally create the File Provisioning Group to which the VFS will belong. If an fpg is created, it will be given the same name as the VFS. Both names must be available for creation for the command to succeed. Either -fpg or the parameters to create a File Provisioning Group must be specified in order to create a VFS. This command will spawn a task and return the taskid. Grace times are specified in minutes. Certificates must be in PEM format, containing both public and private keys. Only one of the following certificate options can be specified: nocert, certfile, certdata. :param ipaddr: The IP address to which the VFS should be assigned :param subnet: The subnet for the IP Address. :param vfsname: The name of the VFS to be created. :param nocert: Do not create a self signed certificate associated with the VFS. :param certfile: Use the certificate data contained in this file. :param certdata: Use the certificate data contained in this string. :param comment: Specifies any additional textual information. :param bgrace: The block grace time in minutes for quotas within the VFS. :param igrace: The inode grace time in minutes for quotas within the VFS. :param fpg: The name of the File Provisioning Group in which the VFS should be created. :param cpg: The CPG in which the File Provisioning Group should be created. :param size: The size of the File Provisioning Group to be created. :param node: The node to which the File Provisioning Group should be assigned. :param vlan: The VLAN ID associated with the VFSIP. :param wait: Wait until the associated task is completed before proceeding. This option will produce verbose task information. :return: List of strings. Lines of output from the CLI command. """ @_wrap_tcl @_run_with_cli @_get_details def getvfs(self, fpg=None, vfs=None): """Display Virtual File Server information. The getvfs command displays information on Virtual File Servers. VFS name is not globally unique, and the same VFS name may be in use in multiple File Provisioning Groups. If no filter options are provided the system will traverse all File Provisioning Groups and display all associated VFSs. :param fpg: Limit the display to VFSs contained within the File Provisioning Group. :param vfs: Limit the display to the specified VFS name. :return: dict with message, total and members .. code-block:: python result = { 'message': None, # Error message, if any. 'total': 0 # Number of members returned. 'members': [], # List containing dict of VFSs. } """ @_run_with_cli def setvfs(self, vfs, fpg=None, certfile=None, certdata=None, certgen=False, rmcert=None, comment=None, bgrace=None, igrace=None): """Modify a Virtual File Server. Allows modification of the specified Virtual File Server Only one of the following certificate options can be specified: certfile, certdata, certgen, rmcert. Certificates must be in PEM format, containing both public and private keys. Grace times are specified in minutes. :param vfs: The name of the VFS to be modified. :param fpg: The name of the File Provisioning Group to which the VFS belongs. :param certfile: Use the certificate data contained in this file. :param certdata: Use the certificate data contained in this string. :param certgen: Generates and sets a certificate for the VFS. :param rmcert: Remove the named certificate from the VFS. :param comment: Specifies any additional textual information. :param bgrace: Specifies the block grace time for quotas within the VFS. :param igrace: Specifies the inode grace time for quotas within the VFS. :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli @_force_me def removevfs(self, vfs, fpg=None): """Remove a Virtual File Server. The removevfs command removes a Virtual File Server and its underlying components from the system. :param vfs: The name of the VFS to be removed. :param fpg: fpg is the name of the File Provisioning Group containing the VFS :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli def createfsip(self, ipaddr, subnet, vfs, vlantag=None, fpg=None): """Assigns an IP address to a Virtual File Server. :param ipaddr: Specifies the IP address to be assign to the Virtual File Server. :param subnet: Specifies the subnet mask to be used. :param vfs: Specifies the Virtual File Server to which the IP address will be assigned. :param vlantag: Specifies the VLAN Tag to be used. :param fpg: Specifies the file provisioning group in which the Virtual File Server was created. :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli @_force_me def setfsip(self, vfs, id, vlantag=None, ip=None, subnet=None, fpg=None): """Modifies the network config of a Virtual File Server. :param vfs: Specifies the Virtual File Server which is to have its network config modified. :param id: Specifies the ID for the network config. :param vlantag: Specifies the VLAN Tag to be used. :param ip: Specifies the new IP address. :param subnet: Specifies the new subnet mask. :param fpg: Specifies the File Provisioning Group in which the Virtual File Server was created. :return: List of strings. Lines of output from the CLI command. """ @_wrap_tcl @_run_with_cli def getfsip(self, vfs, fpg=None): """Shows the network config of a Virtual File Server. :param vfs: Specifies the Virtual File Server which is to have its network config modified. :param fpg: Specifies the File Provisioning Group in which the Virtual File Server was created. :return: dict with message, total and members .. code-block:: python result = { 'message': None, # Error message, if any. 'total': 0 # Number of members returned. 'members': [], # List containing dict of FSIPs. } """ @_run_with_cli @_force_me def removefsip(self, vfs, id, fpg=None): """Removes the network config of a Virtual File Server. :param vfs: Specifies the Virtual File Server which is to have its network config removed. :param id: Specifies the ID for the network config. :param fpg: Specifies the File Provisioning Group in which the Virtual File Server was created. :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli def createfsgroup(self, groupname, gid=None, memberlist=None): """Create a local group account associated with file services. The -gid option can have any value between 1000 and 65535. To access an SMB share, specify the group as "LOCAL_CLUSTER\". :param groupname: Specifies the local group name using up to 31 characters. Valid characters are alphanumeric characters, periods, dashes (except first character), and underscores. :param gid: Specifies the group ID to be used. :param memberlist: User members of the group. :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli @_force_me def setfsgroup(self, groupname, memberlist=None): """Modify a local group account associated with file services. memberlist specifies user members of the group. It is a set of comma separated strings (memberlist=''). If has a prefix (for example, +user1): \+ add to the existing user list. Users in must not be in the existing list. \- remove from the existing list. Users in must be already in the existing list. If specified, the prefix will be applied to the entire list. If has no prefix, will be used as the new user list. :param groupname: Specifies the local group name using up to 31 characters. Valid characters are alphanumeric characters, periods, dashes (except first character), and underscores. :param memberlist: Specifies user members of the group. It is a set of comma separated strings. :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli @_force_me def removefsgroup(self, groupname): """Remove a local group account associated with file services. :param groupname: Specifies the local group name using up to 31 characters. Valid characters are alphanumeric characters, periods, dashes (except first character), and underscores. :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli def createfsuser(self, username, passwd='default', primarygroup=None, enable=None, uid=None, grplist=None): """Create a local user account associated with file services. If not specified -uid will be given a default value. The -uid option can have any value between 1000 and 65535. If the -enabled option is not supplied the user will be enabled by default. Valid values are strings 'false' or 'true' (default). These values are strings -- not Python booleans. To access an SMB share, specify the user as "LOCAL_CLUSTER\". :param username: Specifies the local user name using up to 31 characters. Valid characters are alphanumeric characters, periods, dashes (except first character), and underscores. :param passwd: Specifies the user's password. :param primarygroup: Specifies the user's primary group. :param enable: Specifies the user is enabled or disabled on creation. Valid values are strings 'false' or 'true' (default). These values are strings -- not Python booleans. :param uid: Specifies the user ID to be used. :param grplist: Specifies a list of additional groups the user is to be a member. :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli @_force_me def setfsuser(self, username, passwd=None, primarygroup=None, enable=None, grplist=None): """Modify a local user account associated with file services. Valid values for enabled are strings 'false' or 'true' (or None). These values are strings -- not Python booleans. grplist specifies a list of additional groups which the user is to be a member. It is a set of comma separated strings (grplist=''). If has a prefix (for example, +group1): \+ add to the existing group list. Groups in must not be in the existing list. \- remove from the existing list. Groups in must be already in the existing list. If specified, the prefix will be applied to the entire list. If has no prefix, will be used as the new group list. :param username: Specifies the local user name using up to 31 characters. Valid characters are alphanumeric characters, periods, dashes (except first character), and underscores. :param passwd: Specifies the user's password. :param primarygroup: Specifies the user's primary group. :param enable: Specifies if the user is enabled or not. :param grplist: Specifies a list of additional groups which the user is to be a member. It is a set of comma separated strings. :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli @_force_me def removefsuser(self, username): """Remove a local user account associated with file services. :param username: Specifies the local user name using up to 31 characters. Valid characters are alphanumeric characters, periods, dashes (except first character), and underscores. :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli def createfstore(self, vfs, fstore, comment=None, fpg=None): """Create a file store. The createfstore command creates a new fstore with the specified name for the specified storage pool and the virtual file system. :param vfs: Specifies the name of the virtual file system. :param fstore: Specifies the name of the file store to be created. :param comment: Specifies the textual description of the fstore. :param fpg: Specifies the name of the file provisioning group. :return: List of strings. Lines of output from the CLI command. """ @_wrap_tcl @_run_with_cli def getfstore(self, fpg=None, vfs=None, fstore=None): """Display File Store information. The showfstore command displays information on the file stores. To specify VFS or fstore filters, the parent components must be specified. :param fpg: Limit the display to virtual file servers contained within the file provisioning group. :param vfs: Limit the display to the specified virtual file server. :param fstore: Limit the display to the specified file store. :return: dict with message, total and members .. code-block:: python result = { 'message': None, # Error message, if any. 'total': 0 # Number of members returned. 'members': [], # List containing dict of fstores. } """ @_run_with_cli def setfstore(self, vfs, fstore, comment=None, fpg=None): """Modify a File Store. :param vfs: The name of the containing Virtual File Server. :param fstore: The name of the fstore to be modified. :param comment: Specifies any addition textual information. :param fpg: The name of the parent File Provisioning Group. :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli @_force_me def removefstore(self, vfs, fstore, fpg=None): """Remove a File Store The removefstore command removes a File store and its underlying components from the system :param vfs: The name of the containing Virtual File Server. :param fstore: The name of the fstore to be removed. :param fpg: The name of the parent File Provisioning Group. :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli @_protocol_first @_force_me def createfshare(self, protocol, vfs, sharename, fpg=None, fstore=None, sharedir=None, comment=None, abe=None, allowip=None, denyip=None, allowperm=None, denyperm=None, cache=None, ca=None, options=None, clientip=None, ssl=None, urlpath=None): """Create a file share. The createfshare command creates file shares for supported protocols. PROTOCOLS smb Creates an SMB file share. nfs Creates an NFS file share. obj Creates an Object file share. OPTIONS The following parameters are for all protocols: fpg fstore sharedir comment The following options are specific to each subcommand: smb abe {true|false} allowip denyip allowperm denyperm cache {off|manual|optimized|auto} ca {true|false} nfs options clientip obj ssl {true|false} urlpath The file provisioning group and its underneath virtual file server must be created before creating file shares. For SMB permissions, the same user cannot be specified with the same permission in both "allowperm" and "denyperm". To access an SMB share: for users configured locally, specify "LOCAL_CLUSTER\", for users configured on Active Directory, specify "\" or "\", for users configured on the LDAP server, specify "\". For NFS shares, it is not allowed to create two shares which have identical clients (i.e. specified by -clientip) and share directory (i.e. specified by -sharedir). If you create NFS shares without specifying different -clientip and -sharedir options, the second "createfshare" will fail. To create Object share, the virtual file server specified by must have an associated IP address. :param protocol: The protocol {'nfs'|'smb'|'obj'} :param vfs: The virtual file server under which the file store, if it does not exist, and the share will be created. :param sharename: The share name to be created. :param fpg: Specifies the file provisioning group that belongs. If this is not specified, the command will find out the file provisioning group based on the specified . However, if exists under multiple file provisioning groups, -fpg must be specified. :param fstore: Specifies the file store under which the share will be created. If this is not specified, the command uses the as the file store name. The file store will be created if it does not exist. :param sharedir: Specifies the directory path to share. It can be a full path starting from "/", or a relative path under the file store. If this is not specified, the share created will be rooted at the file store. If option is specified, option -fstore must be specified. :param comment: Specifies any comments or additional information for the share. The comment can be up to 255 characters long. Unprintable characters are not allowed. :param abe: Access Based Enumeration. Specifies if users can see only the files and directories to which they have been allowed access on the shares. The default is 'false'. Valid values are 'true', 'false' or None. The parameter is a Python string -- not a boolean. :param allowip: Specifies client IP addresses that are allowed access to the share. Use commas to separate the IP addresses. The default is "", which allows all IP addresses (i.e. empty means all are allowed). :param denyip: Specifies client IP addresses that are denied access to the share. Use commas to separate the IP addresses. The default is "", which denies none of IP addresses (i.e. empty means none is denied). :param allowperm: Specifies the permission that a user/group is allowed to access the share. must be specified in the format of: ":,:,...". can be a user or group name. must be "fullcontrol", "read", or "change". "Everyone" is a special user for all users and groups. If the user is configured locally using "createfsuser", use to specify the user (for example, -allowperm user1:fullcontrol). If the user is configured on Active Directory, use "setfs ad" to join Active Directory domain with if it has not been done, and use "\\" or "\\" to specify the user (for example, -allowperm example.com\\aduser:fullcontrol). The can be found by running "showfs -ad". If the user is configured on the LDAP server, use "setfs ldap" to create LDAP configuration with if it has not been done, and use "\\" to specify the user (for example, -allowperm ldaphost\\ldapuser:read). If not specified, no default permissions will be allowed for the new shares, which sets the same default as a Windows Server 2012 R2 server would. This is to avoid a system administrator inadvertently allowing any non explicitly specified user to be able to access the SMB share. :param denyperm: Specifies the permission that a user/group is denied to access the share. must be specified in the format of: ":,:,...". can be a user or group name. must be "fullcontrol", "read", or "change". "Everyone" is a special user for all users and groups. If the user is configured locally using "createfsuser", use to specify the user (for example, -denyperm user1:fullcontrol). If the user is configured on Active Directory, use "setfs ad" to join Active Directory domain with if it has not been done, and use "\\" or "\\" to specify the user (for example, -denyperm example.com\\aduser:fullcontrol). The can be found by running "showfs -ad". If the user is configured on the LDAP server, use "setfs ldap" to create LDAP configuration with if it has not been done, and use "\\" to specify the user (for example, -denyperm ldaphost\\ldapuser:read). :param cache: Specifies client-side caching for offline files. Valid values are: "off": The client must not cache any files from this share. The share is configured to disallow caching. "manual": The client must allow only manual caching for the files open from this share. "optimized": The client may cache every file that it opens from this share. Also, the client may satisfy the file requests from its local cache. The share is configured to allow automatic caching of programs and documents. "auto": The client may cache every file that it opens from this share. The share is configured to allow automatic caching of documents. If this is not specified, the default is "manual". :param ca: Continuous Availability. Specifies if SMB3 continuous availability features should be enabled for this share. If not specified, the default is 'true'. Valid values are 'true', 'false' or None. The parameter is a Python string -- not a boolean. :param options: Specifies options to use for the share to be created. Standard NFS export options except "no_subtree_check" are supported. Do not enter option "fsid", which is provided. If not specified, the following options will be automatically set: sync, auth_nlm, wdelay, sec=sys, no_all_squash, crossmnt, secure, subtree_check, hide, root_squash, ro. See linux exports(5) man page for detailed information. :param clientip: Specifies the clients that can access the share. The NFS client can be specified by the name (for example, sys1.hpe.com), the name with a wildcard (for example, *.hpe.com), or by its IP address. Use comma to separate the IP addresses. If this is not specified, the default is "*". :param ssl: Specifies if SSL is enabled. The default is false. :param urlpath: Specifies the URL that clients will use to access the share. If this is not specified, the command uses as . :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli @_protocol_first def setfshare(self, protocol, vfs, sharename, fpg=None, fstore=None, comment=None, abe=None, allowip=None, denyip=None, allowperm=None, denyperm=None, cache=None, ca=None, options=None, clientip=None, ssl=None): """Set/modify file share properties. The setfshare command modifies file share properties for supported protocols. For setting SMB permissions, the same user cannot be specified with the same permission in both "allowperm" and "denyperm". PROTOCOLS smb Sets file share options for SMB. nfs Sets file share options for NFS. obj Sets file share options for Object. OPTIONS The following options are for all protocols: fpg fstore comment The following options are specific to each protocol: smb -abe {true|false} -allowip [+|-] -denyip [+|-] -allowperm [+|-|=] -denyperm [+|-|=] -cache {off|manual|optimized|auto} -ca {true|false} nfs -options -clientip [+|-] obj -ssl {true|false} :param protocol: The protocol {'nfs'|'smb'|'obj'} :param vfs: Specifies the virtual file server that the share to be modified belongs. :param sharename: Specifies the name of the share to be modified. :param fpg: Specifies the file provisioning group that belongs. If this is not specified, the command will find out the file provisioning group based on the specified . However, if exists under multiple file provisioning groups, -fpg must be specified. :param fstore: Specifies the file store that the share to be modified belongs. If this is not specified, the will be used as the file store name to identify the share. :param comment: Specifies any comments or additional information for the share. The comment can be up to 255 characters long. Unprintable characters are not allowed. :param abe: Access Based Enumeration. Specifies if users can see only the files and directories to which they have been allowed access on the shares. :param allowip: Specifies client IP addresses that are allowed access to the share. Use commas to separate the IP addresses. If has a prefix (for example: +1.1.1.0,2.2.2.0): \+ add to the existing allowed list. The IP addresses in must not be in the existing allowed list. \- remove from the existing allowed list. The IP addresses in must be already in the existing allowed list. If specified, the prefix will be applied to the entire . If has no prefix, will be used as the new allowed list. :param denyip: Specifies client IP addresses that are denied access to the share. Use commas to separate the IP addresses. If has a prefix (for example: +1.1.1.0,2.2.2.0): \+ add to the existing denied list. The IP addresses in must not be in the existing denied list. \- remove from the existing denied list. The IP addresses in must already be in the existing denied list. If specified, the prefix will be applied to the entire . If has no prefix, will be used as the new denied list. :param allowperm: Specifies the permissions that users/groups are allowed to access the share. must be specified in the format of: ":,:,...". The can be a user or group name specified using the same format as described in createfshare. must be "fullcontrol", "read", or "change". If has a prefix (for example: +Everyone:read): \+ add to the existing allowed list. Users/groups in must not be in the existing allowed list. \- remove from the existing allowed list. Users/groups in must be already in the existing allowed list. = modify the existing allowed list with . Users/groups in must be already in the existing allowed list. If specified, the prefix will be applied to the entire . If has no prefix, will be used as the new allowed list. :param denyperm: Specifies the permissions that users/groups are denied to access the share. must be specified in the format of: ":,:,...". The can be a user or group name specified using the same format as described in createfshare. must be "fullcontrol", "read", or "change". If has a prefix (for example, +Everyone:read): \+ add to the existing denied list. Users/groups in must not be in the existing denied list. \- remove from the existing denied list. Users/groups in must be already in the existing denied list. = modify the existing denied list with . Users/groups set in must be already in the existing denied list. If specified, the prefix will be applied to the entire . If has no prefix, will be used as the new denied list. :param cache: Specifies client-side caching for offline files. Valid values are: "off": The client must not cache any files from this share. The share is configured to disallow caching. "manual": The client must allow only manual caching for the files open from this share. "optimized": The client may cache every file that it opens from this share. Also, the client may satisfy the file requests from its local cache. The share is configured to allow automatic caching of programs and documents. "auto": The client may cache every file that it opens from this share. The share is configured to allow automatic caching of documents. :param ca: Continuous Availability. Specifies if SMB3 continuous availability features should be enabled for this share. If not specified, the default is 'true'. Valid values are 'true', 'false' or None. The parameter is a Python string -- not a boolean. :param options: Specifies the new options to use for the share. This completely overwrites the options you set previously. Standard NFS export options except "no_subtree_check" are supported. Do not enter option "fsid", which is provided. If not specified, the following options will be automatically set: sync, auth_nlm, wdelay, sec=sys, no_all_squash, crossmnt, secure, subtree_check, hide, root_squash, ro. See linux exports(5) man page for detailed information on valid options. :param clientip: Specifies the clients that can access the share. The NFS client can be specified by the name (for example, sys1.hpe.com), the name with a wildcard (for example, *.hpe.com), or by its IP address. Use comma to separate the IP addresses. If has a prefix (for example, +1.1.1.0,2.2.2.0): \+ add to the existing list. IP addresses in must not be in the existing list. \- remove from the existing list. IP addresses in must be already in the existing list. If specified, the prefix will be applied to the entire . If has no prefix, will be used as the new list. :param ssl: Specifies to enable or disable SSL. :return: List of strings. Lines of output from the CLI command. """ @_wrap_tcl @_run_with_cli @_protocol_first def getfshare(self, protocol, *sharename, **kwargs): r"""Show file shares information. The getfshare command displays file share information for supported protocols. PROTOCOLS smb Displays file shares information for SMB. nfs Displays file shares information for NFS. obj Displays file shares information for Object. :param protocol: The protocol {'nfs'|'smb'|'obj'} :param sharename: Displays only shares with names matching the specified or one of glob-style patterns. :param \**kwargs: See below. :kwargs: * **fpg** -- Specifies the file provisioning group name. This limits the share output to those shares associated with the specified file provisioning group. * **vfs** -- Specifies the virtual file server name. This limits the share output to those shares associated with the specified virtual file server. If this option is specified, but -fpg is not specified, the command will find out the file provisioning group based on . However, if exists under multiple file provisioning groups, -fpg must be specified. * **fstore** -- Specifies the file store name. This limits the share output to only those shares associated with the specified file store. If this is specified, option -vfs must be specified. * **pat** -- Specifies the file share names using the glob-style pattern. Shares which have the name matching any of the specified glob-style patterns will be displayed. The -pat option can specify a list of patterns, and it must be used if specifier is used. :return: dict with message, total and members .. code-block:: python result = { 'message': None, # Error message, if any. 'total': 0 # Number of members returned. 'members': [], # List containing dict of fshares. } """ @_run_with_cli @_protocol_first @_force_me def removefshare(self, protocol, vfs, sharename, fpg=None, fstore=None): """Remove a file share from File Services cluster. PROTOCOLS smb Removes an SMB file share. nfs Removes an NFS file share. obj Removes an Object file share. :param protocol: The protocol {'nfs'|'smb'|'obj'} :param vfs: Specifies the virtual file server name. :param sharename: The name of the share to be removed. :param fpg: Specifies the file provisioning group that belongs. If this is not specified, the command will find out the file provisioning group based on the specified . However, if exists under multiple file provisioning groups, -fpg must be specified. :param fstore: Specifies the file store that the file share to be removed belongs. If this is not specified, the will be used as . :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli @_force_me def createfsnap(self, vfs, fstore, tag, retain=None, fpg=None): """Create a snapshot for File Services. If option -retain is specified and the file store already has the maximum number of snapshots taken, the oldest snapshot will be deleted first before the new snapshot is created. If the command fails to create the new snapshot, the deleted snapshot will not be restored. :param vfs: Specifies the name of the virtual file server. :param fstore: Specifies the name of the file store that the snapshot will be taken. This is the path relative to . :param tag: Specifies the suffix to be appended to the timestamp of snapshot creation time in ISO 8601 date and time format, which will become the name of the created file store snapshot (for example: if "snapshot1" is being used as , the snapshot name will be 2013-12-17T215020_snapshot1). The name can be used as the value of option -snapname to display or remove a snapshot. :param retain: Number of snapshots to retain with the specified tag. Snapshots exceeding the count will be deleted, oldest first. The valid range of is from 1 to 1024. :param fpg: Specifies the file provisioning group that belongs. If this is not specified, the command will find out the file provisioning group based on the specified . However, if exists under multiple file provisioning groups, -fpg must be specified. :return: List of strings. Lines of output from the CLI command. """ @_wrap_tcl @_run_with_cli def getfsnap(self, *snapname, **kwargs): r"""Show snapshot information for File Services. :param snapname: Displays only snapshots with names matching the specified or one of glob-style patterns. :param \**kwargs: See below. :kwargs: * **fpg** -- Specifies the file provisioning group name. This option limits the snapshot output to those associated snapshots with the specified file provisioning group. * **vfs** -- Specifies the virtual file server name. This option limits the snapshot output to those snapshots associated with the specified virtual file server. * **fstore** -- Specifies the file store name. This option limits the snapshot output to only those snapshots associated with the specified file store. * **pat** -- Specifies the snapshot names using glob-style patterns. Snapshots which have the name matching any of the specified glob-style patterns will be displayed. Patterns can be repeated using a comma-separated list. The -pat option must be used if specifier is used. :return: dict with message, total and members .. code-block:: python result = { 'message': None, # Error message, if any. 'total': 0 # Number of members returned. 'members': [], # List containing dict of fsnaps. } """ @_run_with_cli @_force_me def removefsnap(self, vfs, fstore, snapname=None, fpg=None): """Remove file store snapshots from File Services. :param vfs: Specifies the virtual file server name. :param fstore: Specifies the file store name. :param snapname: Specifies the name of the snapshot to be removed. If this is not specified, all snapshots of the file store specified by will be removed. :param fpg: Specifies the file provisioning group that belongs. If this is not specified, the command will find out the file provisioning group based on the specified . However, if exists under multiple file provisioning groups, -fpg must be specified. :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli def startfsnapclean(self, fpgname, resume=False, reclaimStrategy=None): """Start or resume an on-demand reclamation task. :param fpgname: Specifies the name of the file provisioning group. :param resume: Specifies a paused reclamation task needs to be resumed. :param reclaimStrategy: Specifies the strategy to be used while reclaiming snap space. 'maxspeed': Suggests optimize for speedy reclamation. 'maxspace': Suggests optimize to reclaim maximum space. :return: List of strings. Lines of output from the CLI command. """ @_wrap_tcl @_run_with_cli def getfsnapclean(self, fpgname): """List details of snapshot reclamation tasks. The showfsnapclean command displays the details of an on-demand snapshot reclamation task active on a file provisioning group. :param fpgname: Specifies the name of the file provisioning group. :return: dict with message, total and members .. code-block:: python result = { 'message': None, # Error message, if any. 'total': 0 # Number of members returned. 'members': [], # List containing dict of fsnapcleans. } """ @_run_with_cli def stopfsnapclean(self, fpgname, pause=False): """Stop or pause an on-demand reclamation task. The stopfsnapclean command stops or pauses an on-demand reclamation task on a file provisioning group. There can be only one reclamation task running on a file provisioning group. If we pause reclamation task, it will still be counted. If the task is not running, the following output is displayed: No reclamation task running on Storage Pool samplepool (Server error: 400) :param fpgname: Specifies the name of the file provisioning group. :param pause: Specifies to pause a reclamation task. :return: List of strings. Lines of output from the CLI command. """ @_run_with_cli def setfsquota(self, vfsname, fpg=None, username=None, groupname=None, fstore=None, scapacity=None, hcapacity=None, sfile=None, hfile=None, clear=False, archive=False, restore=None): """Set the quotas for a specific virtual file server. :param vfsname: Specifies the name of the virtual file server associated with the quotas. :param fpg: Specifies the name of the file provisioning group hosting the virtual file server. :param username: The username of the quotas to be modified. If the user is configured on Active Directory, use "setfs ad" to join Active Directory domain with if it has not been done, and use "\\" or "\\" to specify the user (for example, -username example.com\aduser). The "" is Active Directory NetBIOS name, which can be found by running "showfs -ad". If the user is configured on the LDAP server, use "setfs ldap" to create LDAP configuration with if it has not been done, and use "\\" to specify the user (for example, -username ldaphost\\ldapuser). The "" is the LDAP server NetBIOS name, which can be found by running "showfs -ldap". :param groupname: The groupname of the quotas to be modified. If the group is configured on Active Directory, use "setfs ad" to join Active Directory domain with if it has not been done, and use "\\" or "\\" to specify the user (for example, -groupname example.com\adgroup). The is Active Directory NetBIOS name, which can be found by running "showfs -ad". If the group is configured on the LDAP server, use "setfs ldap" to create LDAP configuration with if it has not been done, and use "\\" to specify the user (for example, -groupname ldaphost\\ldapgroup). :param fstore: The path to the fstore to which you wish to apply quotas. :param scapacity: An integer value in MB for the soft capacity storage quota. :param hcapacity: An integer value in MB for the hard capacity storage quota. :param sfile: An integer limit of the number of files for the soft file quota. :param hfile: An integer limit of the number of files for the hard file quota. :param clear: Clears the quotas of the specified object. :param archive: Stores the quota information associated with the VFS in a file. :param restore: Applies the quota information stored in the file to the VFS. :return: List of strings. Lines of output from the CLI command. """ @_wrap_tcl @_run_with_cli def getfsquota(self, username=None, groupname=None, fstore=None, vfs=None, fpg=None): """Show the quotas for File Services. :param username: The user name of the quotas to be displayed. :param groupname: The group name of the quotas to be displayed. :param fstore: The file store of the quotas to be displayed. :param vfs: Specifies the name of the virtual file server associated with the quotas. :param fpg: Specifies the name of the file provisioning group hosting the virtual file server. :return: dict with message, total and members .. code-block:: python result = { 'message': None, # Error message, if any. 'total': 0 # Number of members returned. 'members': [], # List containing dict of fsquotas. } """ python-3parclient-4.2.12/hpe3parclient/http.py0000644000000000000000000003477314106434774021243 0ustar rootroot00000000000000# vim: tabstop=4 shiftwidth=4 softtabstop=4 # # (c) Copyright 2012-2015 Hewlett Packard Enterprise Development LP # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ HTTPJSONRESTClient. .. module: http :Author: Walter A. Boring IV :Description: This is the HTTP Client that is used to make the actual calls. It includes the authentication that knows the cookie name for 3PAR. """ import logging import requests import time import ast try: import json except ImportError: import simplejson as json from hpe3parclient import exceptions class HTTPJSONRESTClient(object): """ An HTTP REST Client that sends and recieves JSON data as the body of the HTTP request. :param api_url: The url to the WSAPI service on 3PAR ie. http://<3par server>:8080 :type api_url: str :param secure: Validate SSL cert? Default will not validate :type secure: bool :param http_log_debug: Turns on http log debugging. Default will not log :type http_log_debug: bool :param suppress_ssl_warnings: Suppresses log warning messages if True. Default will not suppress warnings. :type suppress_ssl_warnings: bool """ USER_AGENT = 'python-3parclient' SESSION_COOKIE_NAME = 'X-Hp3Par-Wsapi-Sessionkey' http_log_debug = False _logger = logging.getLogger(__name__) # Retry constants retry_exceptions = (exceptions.HTTPServiceUnavailable, requests.exceptions.ConnectionError) tries = 5 delay = 0 backoff = 2 def __init__(self, api_url, secure=False, http_log_debug=False, suppress_ssl_warnings=False, timeout=None): if suppress_ssl_warnings: requests.packages.urllib3.disable_warnings() self.session_key = None # should be http:///api/v1 self.set_url(api_url) self.set_debug_flag(http_log_debug) self.times = [] # [("item", starttime, endtime), ...] self.secure = secure self.timeout = timeout def set_url(self, api_url): # should be http:///api/v1 self.api_url = api_url.rstrip('/') def set_debug_flag(self, flag): """ This turns on/off http request/response debugging output to console :param flag: Set to True to enable debugging output :type flag: bool """ if not HTTPJSONRESTClient.http_log_debug and flag: ch = logging.StreamHandler() HTTPJSONRESTClient._logger.setLevel(logging.DEBUG) HTTPJSONRESTClient._logger.addHandler(ch) HTTPJSONRESTClient.http_log_debug = True def authenticate(self, user, password, optional=None): """ This tries to create an authenticated session with the 3PAR server :param user: The username :type user: str :param password: Password :type password: str """ # this prevens re-auth attempt if auth fails self.auth_try = 1 self.session_key = None info = {'user': user, 'password': password} self._auth_optional = None if optional: self._auth_optional = optional info.update(optional) resp, body = self.post('/credentials', body=info) if body and 'key' in body: self.session_key = body['key'] self.auth_try = 0 self.user = user self.password = password def _reauth(self): self.authenticate(self.user, self.password, self._auth_optional) def unauthenticate(self): """ This clears the authenticated session with the 3PAR server. """ # delete the session on the 3Par self.delete('/credentials/%s' % self.session_key) self.session_key = None def get_timings(self): """ Ths gives an array of the request timings since last reset_timings call """ return self.times def reset_timings(self): """ This resets the request/response timings array """ self.times = [] def _http_log_req(self, args, kwargs): if not self.http_log_debug: return string_parts = ['curl -i'] for element in args: if element in ('GET', 'POST'): string_parts.append(' -X %s' % element) else: string_parts.append(' %s' % element) for element in kwargs['headers']: header = ' -H "%s: %s"' % (element, kwargs['headers'][element]) string_parts.append(header) HTTPJSONRESTClient._logger.debug("\nREQ: %s\n" % "".join(string_parts)) if 'body' in kwargs: if 'password' in kwargs['body']: body_dict = ast.literal_eval(kwargs['body']) body_dict['password'] = "********" HTTPJSONRESTClient._logger.debug("REQ BODY: %s\n" % (kwargs['body'])) def _http_log_resp(self, resp, body): if not self.http_log_debug: return # Replace commas with newlines to break the debug into new lines, # making it easier to read HTTPJSONRESTClient._logger.debug("RESP:%s\n", str(resp).replace("',", "'\n")) HTTPJSONRESTClient._logger.debug("RESP BODY:%s\n", body) def request(self, *args, **kwargs): """ This makes an HTTP Request to the 3Par server. You should use get, post, delete instead. """ if self.session_key and self.auth_try != 1: kwargs.setdefault('headers', {})[self.SESSION_COOKIE_NAME] = \ self.session_key kwargs.setdefault('headers', kwargs.get('headers', {})) kwargs['headers']['User-Agent'] = self.USER_AGENT kwargs['headers']['Accept'] = 'application/json' if 'body' in kwargs: kwargs['headers']['Content-Type'] = 'application/json' kwargs['body'] = json.dumps(kwargs['body']) payload = kwargs['body'] else: payload = None # args[0] contains the URL, args[1] contains the HTTP verb/method http_url = args[0] http_method = args[1] self._http_log_req(args, kwargs) r = None resp = None body = None while r is None and self.tries > 0: try: # Check to see if the request is being retried. If it is, we # want to delay. if self.delay: time.sleep(self.delay) if self.timeout: r = requests.request(http_method, http_url, data=payload, headers=kwargs['headers'], verify=self.secure, timeout=self.timeout) else: r = requests.request(http_method, http_url, data=payload, headers=kwargs['headers'], verify=self.secure) resp = r.headers body = r.text if isinstance(body, bytes): body = body.decode('utf-8') # resp['status'], status['content-location'], and resp.status # need to be manually set as Python Requests doesn't provide # them automatically. resp['status'] = str(r.status_code) resp.status = r.status_code if 'location' not in resp: resp['content-location'] = r.url r.close() self._http_log_resp(resp, body) # Try and convert the body response to an object # This assumes the body of the reply is JSON if body: try: body = json.loads(body) except ValueError: pass else: body = None if resp.status >= 400: if body and 'message' in body: body['desc'] = body['message'] raise exceptions.from_response(resp, body) except requests.exceptions.SSLError as err: HTTPJSONRESTClient._logger.error( "SSL certificate verification failed: (%s). You must have " "a valid SSL certificate or disable SSL " "verification.", err) raise exceptions.SSLCertFailed("SSL Certificate Verification " "Failed.") except self.retry_exceptions as ex: # If we catch an exception where we want to retry, we need to # decrement the retry count prepare to try again. r = None self.tries -= 1 self.delay = self.delay * self.backoff + 1 # Raise exception, we have exhausted all retries. if self.tries is 0: raise ex except requests.exceptions.HTTPError as err: raise exceptions.HTTPError("HTTP Error: %s" % err) except requests.exceptions.URLRequired as err: raise exceptions.URLRequired("URL Required: %s" % err) except requests.exceptions.TooManyRedirects as err: raise exceptions.TooManyRedirects( "Too Many Redirects: %s" % err) except requests.exceptions.Timeout as err: raise exceptions.Timeout("Timeout: %s" % err) except requests.exceptions.RequestException as err: raise exceptions.RequestException( "Request Exception: %s" % err) return resp, body def _time_request(self, url, method, **kwargs): start_time = time.time() resp, body = self.request(url, method, **kwargs) self.times.append(("%s %s" % (method, url), start_time, time.time())) return resp, body def _do_reauth(self, url, method, ex, **kwargs): # print("_do_reauth called") try: if self.auth_try != 1: self._reauth() resp, body = self._time_request(self.api_url + url, method, **kwargs) return resp, body else: raise ex except exceptions.HTTPUnauthorized: raise ex def _cs_request(self, url, method, **kwargs): # Perform the request once. If we get a 401 back then it # might be because the auth token expired, so try to # re-authenticate and try again. If it still fails, bail. try: resp, body = self._time_request(self.api_url + url, method, **kwargs) return resp, body except exceptions.HTTPUnauthorized as ex: # print("_CS_REQUEST HTTPUnauthorized") resp, body = self._do_reauth(url, method, ex, **kwargs) return resp, body except exceptions.HTTPForbidden as ex: # print("_CS_REQUEST HTTPForbidden") resp, body = self._do_reauth(url, method, ex, **kwargs) return resp, body def get(self, url, **kwargs): """ Make an HTTP GET request to the server. .. code-block:: python #example call try { headers, body = http.get('/volumes') } except exceptions.HTTPUnauthorized as ex: print "Not logged in" } :param url: The relative url from the 3PAR api_url :type url: str :returns: headers - dict of HTTP Response headers :returns: body - the body of the response. If the body was JSON, it will be an object """ return self._cs_request(url, 'GET', **kwargs) def post(self, url, **kwargs): """ Make an HTTP POST request to the server. .. code-block:: python #example call try { info = {'name': 'new volume name', 'cpg': 'MyCPG', 'sizeMiB': 300} headers, body = http.post('/volumes', body=info) } except exceptions.HTTPUnauthorized as ex: print "Not logged in" } :param url: The relative url from the 3PAR api_url :type url: str :returns: headers - dict of HTTP Response headers :returns: body - the body of the response. If the body was JSON, it will be an object """ return self._cs_request(url, 'POST', **kwargs) def put(self, url, **kwargs): """ Make an HTTP PUT request to the server. .. code-block:: python #example call try { info = {'name': 'something'} headers, body = http.put('/volumes', body=info) } except exceptions.HTTPUnauthorized as ex: print "Not logged in" } :param url: The relative url from the 3PAR api_url :type url: str :returns: headers - dict of HTTP Response headers :returns: body - the body of the response. If the body was JSON, it will be an object """ return self._cs_request(url, 'PUT', **kwargs) def delete(self, url, **kwargs): """ Make an HTTP DELETE request to the server. .. code-block:: python #example call try { headers, body = http.delete('/volumes/%s' % name) } except exceptions.HTTPUnauthorized as ex: print "Not logged in" } :param url: The relative url from the 3PAR api_url :type url: str :returns: headers - dict of HTTP Response headers :returns: body - the body of the response. If the body was JSON, it will be an object """ return self._cs_request(url, 'DELETE', **kwargs) python-3parclient-4.2.12/hpe3parclient/showport_parser.py0000644000000000000000000001233014106434774023506 0ustar rootroot00000000000000# (c) Copyright 2015 Hewlett Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections """ Parser for 3PAR showport commands. :Author: Derek Chadwell :Description: Parser to create port objects similar to the json objects return by WSAPI commands. This functionality fills gaps in iscsi port data reported by WSAPI commands, namely iSCSI ports with vlan tags. """ class ShowportParser: """ Parses the following showport commands on an HP 3par array showport showport -iscsi showport -iscsivlan """ def __init__(self): self.parser_methods_by_header = { 'N:S:P': self._parsePortLocation, 'VLAN': self._parseVlan, 'IPAddr': self._parseIPAddr, 'Gateway': self._parseGateway, 'MTU': self._parseMtu, 'TPGT': self._parseTpgt, 'STGT': self._parseSTGT, 'iSNS_Addr': self._parseIsnsAddr, 'iSNS_Port': self._parseIsnsPort, 'Netmask/PrefixLen': self._parseNetmask, } def parseShowport(self, port_show_output): """Parses the showports output from HP3Parclient.ssh.run([cmd]) Returns: an array of port-like dictionaries similar to what you get from the wsapi GET /ports endpoint. NOTE: There are several pieces that showports doesn't give you that don't exist in this output. """ new_ports = [] # the last two lines are just a dashed line # and the number of entries returned. We don't want those port_show_output = port_show_output[0:-2] if not port_show_output: return new_ports # The first array in the # ports output list is the headers headers = port_show_output.pop(0).split(',') # then parse each line and create a port-like # dictionary from it for line in port_show_output: new_port = {} entries = line.split(',') for i, entry in enumerate(entries): parser = self.parser_methods_by_header[headers[i]] self._merge_dict(new_port, parser(entry)) new_ports.append(new_port) return new_ports def _parsePortLocation(self, nps): """Parse N:S:P data into a dictionary with key "portPost" "portPos":{ "node":0, "slot":0, "cardPort":1 }, """ nps_array = nps.split(':') port_pos = {'portPos': { 'node': int(nps_array[0]), 'slot': int(nps_array[1]), 'cardPort': int(nps_array[2]) } } return port_pos def _parseVlan(self, vlan): """the vlan key/value pair as part of the iSCSIPortInfo dict""" return {'iSCSIPortInfo': {'vlan': vlan}} def _parseIPAddr(self, address): """the IP key/value pair as part of the iSCSIPortInfo dict""" return {'IPAddr': address, 'iSCSIPortInfo': {'IPAddr': address}} def _parseGateway(self, gw): """the gw key/value pair as part of the iSCSIPortInfo dict""" return {'iSCSIPortInfo': {'gateway': gw}} def _parseMtu(self, mtu): """the mtu key/value pair as part of the iSCSIPortInfo dict""" return {'iSCSIPortInfo': {'mtu': int(mtu)}} def _parseTpgt(self, tpgt): """the tpgt key/value pair as part of the iSCSIPortInfo dict""" return {'iSCSIPortInfo': {'tpgt': int(tpgt)}} def _parseSTGT(self, stgt): """the stgt key/value pair as part of the iSCSIPortInfo dict""" return {'iSCSIPortInfo': {'stgt': int(stgt)}} def _parseIsnsAddr(self, addr): """the isns key/value pair as part of the iSCSIPortInfo dict""" return {'iSCSIPortInfo': {'iSNSAddr': addr}} def _parseIsnsPort(self, port): """the isns key/value pair as part of the iSCSIPortInfo dict""" return {'iSCSIPortInfo': {'iSNSPort': int(port)}} def _parseNetmask(self, mask): """the network key/value pair as part of the iSCSIPortInfo dict""" return {'iSCSIPortInfo': {'netmask': mask}} def _merge_dict(self, d1, d2): """ Modifies d1 in-place to contain values from d2. If any value in d1 is a dictionary (or dict-like), *and* the corresponding value in d2 is also a dictionary, then merge them in-place. """ for k, v2 in d2.items(): v1 = d1.get(k) # returns None if v1 has no value for this key if (isinstance(v1, collections.Mapping) and isinstance(v2, collections.Mapping)): self._merge_dict(v1, v2) else: d1[k] = v2 python-3parclient-4.2.12/hpe3parclient/ssh.py0000644000000000000000000003430714106434774021052 0ustar rootroot00000000000000# (c) Copyright 2014-2015 Hewlett Packard Enterprise Development LP # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ HPE 3PAR SSH Client .. module: ssh :Author: Walter A. Boring IV :Description: This is the SSH Client that is used to make calls to the 3PAR where an existing REST API doesn't exist. """ import logging import os import paramiko from random import randint import re from eventlet import greenthread from hpe3parclient import exceptions # Python 3+ override try: basestring python3 = False except NameError: basestring = str python3 = True # Commands that require Tpd::rtpd prefix tpd_commands = [ 'createfstore', 'getfstore', 'getfsquota', 'getfshare' ] class HPE3PARSSHClient(object): """This class is used to execute SSH commands on a 3PAR.""" log_debug = False _logger = logging.getLogger(__name__) _logger.setLevel(logging.INFO) def __init__(self, ip, login, password, port=22, conn_timeout=None, privatekey=None, **kwargs): self.san_ip = ip self.san_ssh_port = port self.ssh_conn_timeout = conn_timeout self.san_login = login self.san_password = password self.san_privatekey = privatekey self._create_ssh(**kwargs) def _create_ssh(self, **kwargs): try: ssh = paramiko.SSHClient() known_hosts_file = kwargs.get('known_hosts_file', None) if known_hosts_file is None: ssh.load_system_host_keys() else: # Make sure we can open the file for appending first. # This is needed to create the file when we run CI tests with # no existing key file. open(known_hosts_file, 'a').close() ssh.load_host_keys(known_hosts_file) missing_key_policy = kwargs.get('missing_key_policy', None) if missing_key_policy is None: missing_key_policy = paramiko.AutoAddPolicy() elif isinstance(missing_key_policy, basestring): # To make it configurable, allow string to be mapped to object. if missing_key_policy == paramiko.AutoAddPolicy().__class__.\ __name__: missing_key_policy = paramiko.AutoAddPolicy() elif missing_key_policy == paramiko.RejectPolicy().__class__.\ __name__: missing_key_policy = paramiko.RejectPolicy() elif missing_key_policy == paramiko.WarningPolicy().__class__.\ __name__: missing_key_policy = paramiko.WarningPolicy() else: raise exceptions.SSHException( "Invalid missing_key_policy: %s" % missing_key_policy ) ssh.set_missing_host_key_policy(missing_key_policy) self.ssh = ssh except Exception as e: msg = "Error connecting via ssh: %s" % e self._logger.error(msg) raise paramiko.SSHException(msg) def _connect(self, ssh): if self.san_password: ssh.connect(self.san_ip, port=self.san_ssh_port, username=self.san_login, password=self.san_password, timeout=self.ssh_conn_timeout) elif self.san_privatekey: pkfile = os.path.expanduser(self.san_privatekey) privatekey = paramiko.RSAKey.from_private_key_file(pkfile) ssh.connect(self.san_ip, port=self.san_ssh_port, username=self.san_login, pkey=privatekey, timeout=self.ssh_conn_timeout) else: msg = "Specify a password or private_key" raise exceptions.SSHException(msg) def open(self): """Opens a new SSH connection if the transport layer is missing. This can be called if an active SSH connection is open already. """ # Create a new SSH connection if the transport layer is missing. if self.ssh: transport_active = False if self.ssh.get_transport(): transport_active = self.ssh.get_transport().is_active() if not transport_active: try: self._connect(self.ssh) except Exception as e: msg = "Error connecting via ssh: %s" % e self._logger.error(msg) raise paramiko.SSHException(msg) def close(self): if self.ssh: self.ssh.close() def set_debug_flag(self, flag): """ This turns on ssh debugging output to console :param flag: Set to True to enable debugging output :type flag: bool """ if not HPE3PARSSHClient.log_debug and flag: ch = logging.StreamHandler() self._logger.setLevel(logging.DEBUG) self._logger.addHandler(ch) HPE3PARSSHClient.log_debug = True @staticmethod def sanitize_cert(output_list): if isinstance(output_list, list): output = ''.join(output_list) else: output = output_list try: begin_cert_str = '-BEGIN CERTIFICATE-' begin_cert_pos = output.index(begin_cert_str) pre = ''.join((output[:begin_cert_pos], begin_cert_str, 'sanitized')) try: end_cert_str = '-END CERTIFICATE-' end_cert_pos = output.index(end_cert_str) return pre if begin_cert_pos >= end_cert_pos else ''.join( (pre, output[end_cert_pos:])) except ValueError: return pre except ValueError: return output @staticmethod def raise_stripper_error(reason, output): msg = "Multi-line stripper failed: %s" % reason HPE3PARSSHClient._logger.error(msg) HPE3PARSSHClient._logger.debug("Output: %s" % HPE3PARSSHClient.sanitize_cert(output)) raise exceptions.SSHException(msg) @staticmethod def strip_input_from_output(cmd, output): """The input commands are echoed in the output. Strip that. The legacy way of doing this expected a fixed number of before and after lines. With Unity many commands are being broken into multiple lines, so the stripper needs to adjust. This new stripper attempts to recognize the input commands and prompt in the output so that it knows what it is stripping (or else it raises an exception). """ # Keep output lines after the 'exit'. # 'exit' is the last of the stdin. for i, line in enumerate(output): if line == 'exit': output = output[i + 1:] break else: reason = "Did not find 'exit' in output." HPE3PARSSHClient.raise_stripper_error(reason, output) if not output: reason = "Did not find any output after 'exit'." HPE3PARSSHClient.raise_stripper_error(reason, output) # The next line is prompt plus setclienv command. # Use this to get the prompt string. prompt_pct = output[0].find('% setclienv csvtable 1') if prompt_pct < 0: reason = "Did not find '% setclienv csvtable 1' in output." HPE3PARSSHClient.raise_stripper_error(reason, output) prompt = output[0][0:prompt_pct + 1] del output[0] # Next find the prompt plus the command. # It might be broken into multiple lines, so loop and # append until we find the whole prompt plus command. command_string = ' '.join(cmd) if re.match('|'.join(tpd_commands), command_string): escp_command_string = command_string.replace('"', '\\"') command_string = "Tpd::rtpd " + '"' + escp_command_string + '"' seek = ' '.join((prompt, command_string)) found = '' for i, line in enumerate(output): found = ''.join((found, line.rstrip('\r\n'))) if found == seek: # Found the whole thing. Use the rest as output now. output = output[i + 1:] break else: HPE3PARSSHClient._logger.debug("Command: %s" % command_string) reason = "Did not find match for command in output" HPE3PARSSHClient.raise_stripper_error(reason, output) # Always strip the last 2 return output[:len(output) - 2] def run(self, cmd, multi_line_stripper=False): """Runs a CLI command over SSH, without doing any result parsing.""" self._logger.debug("SSH CMD = %s " % cmd) (stdout, stderr) = self._run_ssh(cmd, False) # we have to strip out the input and exit lines if python3: tmp = stdout.decode().split("\r\n") else: tmp = stdout.split("\r\n") # default is old stripper -- to avoid breaking things, for now if multi_line_stripper: out = self.strip_input_from_output(cmd, tmp) self._logger.debug("OUT = %s" % self.sanitize_cert(out)) else: out = tmp[5:len(tmp) - 2] self._logger.debug("OUT = %s" % out) return out def _ssh_execute(self, cmd, check_exit_code=True): """We have to do this in order to get CSV output from the CLI command. We first have to issue a command to tell the CLI that we want the output to be formatted in CSV, then we issue the real command. """ if re.match('|'.join(tpd_commands), cmd): cmd = 'Tpd::rtpd "' + cmd.replace('"', '\\"') + '"' self._logger.debug('Running cmd (SSH): %s', cmd) channel = self.ssh.invoke_shell() stdin_stream = channel.makefile('wb') stdout_stream = channel.makefile('rb') stderr_stream = channel.makefile('rb') stdin_stream.write('''setclienv csvtable 1 %s exit ''' % cmd) # stdin.write('process_input would go here') # stdin.flush() # NOTE(justinsb): This seems suspicious... # ...other SSH clients have buffering issues with this approach stdout = stdout_stream.read() stderr = stderr_stream.read() stdin_stream.close() stdout_stream.close() stderr_stream.close() exit_status = channel.recv_exit_status() # exit_status == -1 if no exit code was returned if exit_status != -1: self._logger.debug('Result was %s' % exit_status) if check_exit_code and exit_status != 0: msg = "command %s failed" % cmd self._logger.error(msg) raise exceptions.ProcessExecutionError(exit_code=exit_status, stdout=stdout, stderr=stderr, cmd=cmd) channel.close() return (stdout, stderr) def _run_ssh(self, cmd_list, check_exit=True, attempts=2): self.check_ssh_injection(cmd_list) command = ' '. join(cmd_list) try: total_attempts = attempts while attempts > 0: attempts -= 1 try: return self._ssh_execute(command, check_exit_code=check_exit) except Exception as e: self._logger.error(e) if attempts > 0: greenthread.sleep(randint(20, 500) / 100.0) if not self.ssh.get_transport().is_alive(): self._create_ssh() msg = ("SSH Command failed after '%(total_attempts)r' " "attempts : '%(command)s'" % {'total_attempts': total_attempts, 'command': command}) self._logger.error(msg) raise exceptions.SSHException(message=msg) except Exception: self._logger.error("Error running ssh command: %s" % command) raise def check_ssh_injection(self, cmd_list): ssh_injection_pattern = ['`', '$', '|', '||', ';', '&', '&&', '>', '>>', '<'] # Check whether injection attacks exist for arg in cmd_list: arg = arg.strip() # Check for matching quotes on the ends is_quoted = re.match('^(?P[\'"])(?P.*)(?P=quote)$', arg) if is_quoted: # Check for unescaped quotes within the quoted argument quoted = is_quoted.group('quoted') if quoted: if (re.match('[\'"]', quoted) or re.search('[^\\\\][\'"]', quoted)): raise exceptions.SSHInjectionThreat( command=str(cmd_list)) else: # We only allow spaces within quoted arguments, and that # is the only special character allowed within quotes if len(arg.split()) > 1: raise exceptions.SSHInjectionThreat(command=str(cmd_list)) # Second, check whether danger character in command. So the shell # special operator must be a single argument. for c in ssh_injection_pattern: if arg == c: continue result = arg.find(c) if not result == -1: if result == 0 or not arg[result - 1] == '\\': raise exceptions.SSHInjectionThreat(command=cmd_list) python-3parclient-4.2.12/hpe3parclient/tcl_parser.py0000644000000000000000000000453114106434774022407 0ustar rootroot00000000000000# (c) Copyright 2015 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ TCL parser for 3PAR gettpdinterface and get output. .. module: file_client .. moduleauthor: Mark Sturdevant :Author: Mark Sturdevant :Description: TCL parser for 3PAR gettpdinterface and get output. This module parses TCL strings and returns python structures. """ MAX_LEVELS = 10 class HPE3ParTclParser(object): """The 3PAR TCL Parser.""" @staticmethod def parse_tcl(tcl): token = '' result = [] lists = [[]] * MAX_LEVELS level = -1 for c in tcl: if c == '{': level += 1 if level > MAX_LEVELS: # For deeper nesting, just capture as string token += c else: token = '' for l in range(0, level + 1): lists[level] = [] elif c == '}': if token != '' and level <= MAX_LEVELS: lists[level].append(token) token = '' if level > MAX_LEVELS: # For deeper nesting, just capture as string token += c elif level > 0: lists[level - 1].append(lists[level]) lists[level] = [] else: result.append(lists[level]) lists[level] = [] level -= 1 elif c == ' ': if level > MAX_LEVELS: # For deeper nesting, just capture as string token += c elif token != '': lists[level].append(token) token = '' else: token += c return result python-3parclient-4.2.12/python_3parclient.egg-info/0000755000000000000000000000000014106677657022303 5ustar rootroot00000000000000python-3parclient-4.2.12/python_3parclient.egg-info/PKG-INFO0000644000000000000000000001713014106677656023401 0ustar rootroot00000000000000Metadata-Version: 2.1 Name: python-3parclient Version: 4.2.12 Summary: HPE Alletra 9000 and HPE Primera and HPE 3PAR HTTP REST Client Home-page: http://packages.python.org/python-3parclient Author: Walter A. Boring IV Author-email: walter.boring@hpe.com Maintainer: Walter A. Boring IV License: Apache License, Version 2.0 Keywords: hpe,3par,rest Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Environment :: Web Environment Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Topic :: Internet :: WWW/HTTP Requires: paramiko Requires: eventlet Requires: requests Provides: hpe3parclient License-File: LICENSE.txt .. image:: https://img.shields.io/pypi/v/python-3parclient.svg :target: https://pypi.python.org/pypi/python-3parclient :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/python-3parclient.svg :target: https://pypi.python.org/pypi/python-3parclient :alt: Downloads HPE Alletra 9000 and HPE Primera and HPE 3PAR REST Client ========================================================= This is a Client library that can talk to the HPE Alletra 9000 and Primera and 3PAR Storage array. The HPE Alletra 9000 and Primera and 3PAR storage array has a REST web service interface and a command line interface. This client library implements a simple interface for talking with either interface, as needed. The python Requests library is used to communicate with the REST interface. The python paramiko library is used to communicate with the command line interface over an SSH connection. This is the new location for the rebranded HP 3PAR Rest Client and will be where all future releases are made. It was previously located on PyPi at: https://pypi.python.org/pypi/hp3parclient The GitHub repository for the old HP 3PAR Rest Client is located at: https://github.com/hpe-storage/python-3parclient/tree/3.x The HP 3PAR Rest Client (hp3parclient) is now considered deprecated. Requirements ============ This branch requires 3.1.3 version MU1 or later of the HPE 3PAR firmware. This branch requires 4.3.1 version of the HPE Primera firmware. This branch requires 9.3.0 version of the HPE Alletra 9000 firmware. File Persona capabilities require HPE 3PAR firmware 3.2.1 Build 46 or later. Capabilities ============ * Create Volume * Delete Volume * Get all Volumes * Get a Volume * Modify a Volume * Copy a Volume * Create a Volume Snapshot * Create CPG * Delete CPG * Get all CPGs * Get a CPG * Get a CPG's Available Space * Create a VLUN * Delete a VLUN * Get all VLUNs * Get a VLUN * Create a Host * Delete a Host * Get all Hosts * Get a Host * Get VLUNs for a Host * Find a Host * Find a Host Set for a Host * Get all Host Sets * Get a Host Set * Create a Host Set * Delete a Host Set * Modify a Host Set * Get all Ports * Get iSCSI Ports * Get FC Ports * Get IP Ports * Set Volume Metadata * Get Volume Metadata * Get All Volume Metadata * Find Volume Metadata * Remove Volume Metadata * Create a Volume Set * Delete a Volume Set * Modify a Volume Set * Get a Volume Set * Get all Volume Sets * Find one Volume Set containing a specified Volume * Find all Volume Sets containing a specified Volume * Create a QOS Rule * Modify a QOS Rule * Delete a QOS Rule * Set a QOS Rule * Query a QOS Rule * Query all QOS Rules * Get a Task * Get all Tasks * Get a Patch * Get all Patches * Get WSAPI Version * Get WSAPI Configuration Info * Get Storage System Info * Get Overall System Capacity * Stop Online Physical Copy * Query Online Physical Copy Status * Stop Offline Physical Copy * Resync Physical Copy * Query Remote Copy Info * Query a Remote Copy Group * Query all Remote Copy Groups * Create a Remote Copy Group * Delete a Remote Copy Group * Modify a Remote Copy Group * Add a Volume to a Remote Copy Group * Remove a Volume from a Remote Copy Group * Start Remote Copy on a Remote Copy Group * Stop Remote Copy on a Remote Copy Group * Synchronize a Remote Copy Group * Recover a Remote Copy Group from a Disaster * Enable/Disable Config Mirroring on a Remote Copy Target * Get Remote Copy Group Volumes * Get Remote Copy Group Volume * Admit Remote Copy Link * Dismiss Remote Copy Link * Start Remote Copy * Remote Copy Service Exists Check * Get Remote Copy Link * Remote Copy Link Exists Check * Admit Remote Copy Target * Dismiss Remote Copy Target * Target In Remote Copy Group Exists Check * Remote Copy Group Status Check * Remote Copy Group Status Started Check * Remote Copy Group Status Stopped Check * Create Schedule * Delete Schedule * Get Schedule * Modify Schedule * Suspend Schedule * Resume Schedule * Get Schedule Status * Promote Virtual Copy * Get a Flash Cache * Create a Flash Cache * Delete a Flash Cache File Persona Capabilities ========================= * Get File Services Info * Create a File Provisioning Group * Grow a File Provisioning Group * Get File Provisioning Group Info * Modify a File Provisioning Group * Remove a File Provisioning Group * Create a Virtual File Server * Get Virtual File Server Info * Modify a Virtual File Server * Remove a Virtual File Server * Assign an IP Address to a Virtual File Server * Get the Network Config of a Virtual File Server * Modify the Network Config of a Virtual File Server * Remove the Network Config of a Virtual File Server * Create a File Services User Group * Modify a File Services User Group * Remove a File Services User Group * Create a File Services User * Modify a File Services User * Remove a File Services User * Create a File Store * Get File Store Info * Modify a File Store * Remove a File Store * Create a File Share * Get File Share Info * Modify a File Share * Remove a File Share * Create a File Store Snapshot * Get File Store Snapshot Info * Remove a File Store Snapshot * Reclaim Space from Deleted File Store Snapshots * Get File Store Snapshot Reclamation Info * Stop or Pause a File Store Snapshot Reclamation Task * Set File Services Quotas * Get Files Services Quota Info Installation ============ To install from source:: $ sudo pip install . To install from http://pypi.org:: $ sudo pip install python-3parclient Unit Tests ========== To run all unit tests:: $ tox -e py27 To run a specific test:: $ tox -e py27 -- test/file.py:class_name.test_method_name To run all unit tests with code coverage:: $ tox -e cover The output of the coverage tests will be placed into the ``coverage`` dir. Folders ======= * docs -- contains the documentation. * hpe3parclient -- the actual client.py library * test -- unit tests * samples -- some sample uses Documentation ============= To build the documentation:: $ tox -e docs To view the built documentation point your browser to:: docs/html/index.html Running Simulators ================== The unit tests should automatically start/stop the simulators. To start them manually use the following commands. To stop them, use 'kill'. Starting them manually before running unit tests also allows you to watch the debug output. * WSAPI:: $ python test/HPE3ParMockServer_flask.py -port 5001 -user -password -debug * SSH:: $ python test/HPE3ParMockServer_ssh.py [port] Building wheel dist =================== This client now supports building via the new python WHEELS standard. Take a look at http://pythonwheels.com * building:: $ python setup.py bdist_wheel * building and uploading:: $ python setup.py sdist bdist_wheel upload python-3parclient-4.2.12/python_3parclient.egg-info/SOURCES.txt0000644000000000000000000000210014106677656024157 0ustar rootroot00000000000000LICENSE.txt README.rst setup.cfg setup.py hpe3parclient/__init__.py hpe3parclient/client.py hpe3parclient/exceptions.py hpe3parclient/file_client.py hpe3parclient/http.py hpe3parclient/showport_parser.py hpe3parclient/ssh.py hpe3parclient/tcl_parser.py python_3parclient.egg-info/PKG-INFO python_3parclient.egg-info/SOURCES.txt python_3parclient.egg-info/dependency_links.txt python_3parclient.egg-info/requires.txt python_3parclient.egg-info/top_level.txt test/HPE3ParClient_base.py test/HPE3ParMockServer_flask.py test/HPE3ParMockServer_ssh.py test/__init__.py test/test_HPE3ParClient_CPG.py test/test_HPE3ParClient_Exception.py test/test_HPE3ParClient_FilePersona.py test/test_HPE3ParClient_FilePersona_Mock.py test/test_HPE3ParClient_HostSet.py test/test_HPE3ParClient_MockSSH.py test/test_HPE3ParClient_ShowportParser.py test/test_HPE3ParClient_Stats.py test/test_HPE3ParClient_VLUN.py test/test_HPE3ParClient_host.py test/test_HPE3ParClient_ports.py test/test_HPE3ParClient_retry.py test/test_HPE3ParClient_system.py test/test_HPE3ParClient_volume.py test/test_HTTPJSONRESTClient.pypython-3parclient-4.2.12/python_3parclient.egg-info/dependency_links.txt0000644000000000000000000000000114106677656026350 0ustar rootroot00000000000000 python-3parclient-4.2.12/python_3parclient.egg-info/requires.txt0000644000000000000000000000003314106677656024676 0ustar rootroot00000000000000paramiko eventlet requests python-3parclient-4.2.12/python_3parclient.egg-info/top_level.txt0000644000000000000000000000002314106677656025027 0ustar rootroot00000000000000hpe3parclient test python-3parclient-4.2.12/test/0000755000000000000000000000000014106677657016123 5ustar rootroot00000000000000python-3parclient-4.2.12/test/HPE3ParClient_base.py0000644000000000000000000002171014106434774021760 0ustar rootroot00000000000000# (c) Copyright 2015 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Test base class of 3PAR Client.""" import os import sys import unittest import subprocess import time import inspect from pytest_testconfig import config import datetime from functools import wraps from hpe3parclient import client, file_client TIME = datetime.datetime.now().strftime('%H%M%S') try: # For Python 3.0 and later from urllib.parse import urlparse except ImportError: # Fall back to Python 2's urllib2 from urlparse import urlparse class HPE3ParClientBaseTestCase(unittest.TestCase): user = config['TEST']['user'] password = config['TEST']['pass'] flask_url = config['TEST']['flask_url'] url_3par = config['TEST']['3par_url'] debug = config['TEST']['debug'].lower() == 'true' unitTest = config['TEST']['unit'].lower() == 'true' port = None remote_copy = config['TEST']['run_remote_copy'].lower() == 'true' run_remote_copy = remote_copy and not unitTest if run_remote_copy: secondary_user = config['TEST_REMOTE_COPY']['user'] secondary_password = config['TEST_REMOTE_COPY']['pass'] secondary_url_3par = config['TEST_REMOTE_COPY']['3par_url'] secondary_target_name = config['TEST_REMOTE_COPY']['target_name'] ssh_port = None if 'ssh_port' in config['TEST']: ssh_port = int(config['TEST']['ssh_port']) elif unitTest: ssh_port = 2200 else: ssh_port = 22 # Don't setup SSH unless needed. It slows things down. withSSH = False if 'domain' in config['TEST']: DOMAIN = config['TEST']['domain'] else: DOMAIN = 'UNIT_TEST_DOMAIN' if 'cpg_ldlayout_ha' in config['TEST']: CPG_LDLAYOUT_HA = int(config['TEST']['cpg_ldlayout_ha']) if 'disk_type' in config['TEST']: DISK_TYPE = int(config['TEST']['disk_type']) CPG_OPTIONS = {'domain': DOMAIN, 'LDLayout': {'HA': CPG_LDLAYOUT_HA, 'diskPatterns': [{'diskType': DISK_TYPE}]}} else: CPG_OPTIONS = {'domain': DOMAIN, 'LDLayout': {'HA': CPG_LDLAYOUT_HA}} else: CPG_LDLAYOUT_HA = None CPG_OPTIONS = {'domain': DOMAIN} if 'known_hosts_file' in config['TEST']: known_hosts_file = config['TEST']['known_hosts_file'] else: known_hosts_file = None if 'missing_key_policy' in config['TEST']: missing_key_policy = config['TEST']['missing_key_policy'] else: missing_key_policy = None def setUp(self, withSSH=False, withFilePersona=False): self.withSSH = withSSH self.withFilePersona = withFilePersona cwd = os.path.dirname(os.path.abspath( inspect.getfile(inspect.currentframe()))) if self.unitTest: self.printHeader('Using flask ' + self.flask_url) parsed_url = urlparse(self.flask_url) userArg = '-user=%s' % self.user passwordArg = '-password=%s' % self.password portArg = '-port=%s' % parsed_url.port script = 'HPE3ParMockServer_flask.py' path = "%s/%s" % (cwd, script) try: self.mockServer = subprocess.Popen([sys.executable, path, userArg, passwordArg, portArg], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE ) except Exception: pass time.sleep(1) if self.withFilePersona: self.cl = file_client.HPE3ParFilePersonaClient(self.flask_url) else: self.cl = client.HPE3ParClient(self.flask_url) if self.withSSH: self.printHeader('Using paramiko SSH server on port %s' % self.ssh_port) ssh_script = 'HPE3ParMockServer_ssh.py' ssh_path = "%s/%s" % (cwd, ssh_script) self.mockSshServer = subprocess.Popen([sys.executable, ssh_path, str(self.ssh_port)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) time.sleep(1) else: if withFilePersona: self.printHeader('Using 3PAR %s with File Persona' % self.url_3par) self.cl = file_client.HPE3ParFilePersonaClient(self.url_3par) else: self.printHeader('Using 3PAR ' + self.url_3par) self.cl = client.HPE3ParClient(self.url_3par) if self.withSSH: # This seems to slow down the test cases, so only use this when # requested if self.unitTest: # The mock SSH server can be accessed at 0.0.0.0. ip = '0.0.0.0' else: parsed_3par_url = urlparse(self.url_3par) ip = parsed_3par_url.hostname.split(':').pop() try: # Now that we don't do keep-alive, the conn_timeout needs to # be set high enough to avoid sometimes slow response in # the File Persona tests. self.cl.setSSHOptions( ip, self.user, self.password, port=self.ssh_port, conn_timeout=500, known_hosts_file=self.known_hosts_file, missing_key_policy=self.missing_key_policy) except Exception as ex: print(ex) self.fail("failed to start ssh client") # Setup remote copy target if self.run_remote_copy: parsed_3par_url = urlparse(self.secondary_url_3par) ip = parsed_3par_url.hostname.split(':').pop() self.secondary_cl = client.HPE3ParClient(self.secondary_url_3par) try: self.secondary_cl.setSSHOptions( ip, self.secondary_user, self.secondary_password, port=self.ssh_port, conn_timeout=500, known_hosts_file=self.known_hosts_file, missing_key_policy=self.missing_key_policy) except Exception as ex: print(ex) self.fail("failed to start ssh client") self.secondary_cl.login(self.secondary_user, self.secondary_password) if self.debug: self.cl.debug_rest(True) self.cl.login(self.user, self.password) if not self.port: ports = self.cl.getPorts() ports = [p for p in ports['members'] if p['linkState'] == 4 and # Ready ('device' not in p or not p['device']) and p['mode'] == self.cl.PORT_MODE_TARGET] self.port = ports[0]['portPos'] def tearDown(self): self.cl.logout() if self.run_remote_copy: self.secondary_cl.logout() if self.unitTest: self.mockServer.kill() if self.withSSH: self.mockSshServer.kill() def print_header_and_footer(func): """Decorator to print header and footer for unit tests.""" @wraps(func) def wrapper(*args, **kwargs): test = args[0] test.printHeader(unittest.TestCase.id(test)) result = func(*args, **kwargs) test.printFooter(unittest.TestCase.id(test)) return result return wrapper def printHeader(self, name): print("\n##Start testing '%s'" % name) def printFooter(self, name): print("##Completed testing '%s\n" % name) def findInDict(self, dic, key, value): for i in dic: if key in i and i[key] == value: return True python-3parclient-4.2.12/test/HPE3ParMockServer_flask.py0000644000000000000000000024074614106434774023024 0ustar rootroot00000000000000import flask import re import pprint import json import random import string import argparse import uuid from time import gmtime, strftime from werkzeug.exceptions import default_exceptions from werkzeug.exceptions import HTTPException # 3PAR error code constants INV_USER_PASS = 5 INV_INPUT = 12 EXISTENT_CPG = 14 NON_EXISTENT_CPG = 15 EXISTENT_HOST = 16 NON_EXISTENT_HOST = 17 NON_EXISTENT_VLUN = 19 EXISTENT_VOL = 22 NON_EXISTENT_VOL = 23 EXPORTED_VLUN = 26 TOO_LARGE = 28 NON_EXISTENT_DOMAIN = 38 INV_INPUT_WRONG_TYPE = 39 INV_INPUT_MISSING_REQUIRED = 40 INV_INPUT_EXCEEDS_RANGE = 43 INV_INPUT_PARAM_CONFLICT = 44 INV_INPUT_EMPTY_STR = 45 INV_INPUT_BAD_ENUM_VALUE = 46 INV_INPUT_PORT_SPECIFICATION = 55 INV_INPUT_EXCEEDS_LENGTH = 57 EXISTENT_ID = 59 INV_INPUT_ILLEGAL_CHAR = 69 EXISTENT_PATH = 73 NON_EXISTENT_SET = 77 HOST_IN_SET = 77 INV_INPUT_ONE_REQUIRED = 78 NON_EXISTENT_PATH = 80 NON_EXISTENT_QOS_RULE = 100 EXISTENT_SET = 101 EXISTENT_QOS_RULE = 114 INV_INPUT_BELOW_RANGE = 115 INV_INPUT_QOS_TARGET_OBJECT = 117 INV_OPERATION_VV_IN_REMOTE_COPY = 120 NON_EXISTENT_TASK = 145 INV_INPUT_VV_GROW_SIZE = 152 VV_NEW_SIZE_EXCEED_CPG_LIMIT = 153 NON_EXISTENT_OBJECT_KEY = 180 EXISTENT_OBJECT_KEY = 181 NON_EXISTENT_RCOPY_GROUP = 187 EXISTENT_RCOPY_GROUP = 237 # Remote Copy Actions ADMIT_VV = 1 DISMISS_VV = 2 START_GROUP = 3 STOP_GROUP = 4 SYNC_GROUP = 5 FAILOVER_GROUP = 7 # Remote Copy States RCOPY_STARTED = 3 RCOPY_STOPPED = 5 parser = argparse.ArgumentParser() parser.add_argument("-debug", help="Turn on http debugging", default=False, action="store_true") parser.add_argument("-user", help="User name") parser.add_argument("-password", help="User password") parser.add_argument("-port", help="Port to listen on", type=int, default=5000) args = parser.parse_args() user_name = args.user user_pass = args.password debugRequests = False if "debug" in args and args.debug: debugRequests = True # __all__ = ['make_json_app'] def id_generator(size=6, chars=string.ascii_uppercase + string.digits): return ''.join(random.choice(chars) for x in range(size)) def make_json_app(import_name, **kwargs): """ Create a JSON-oriented Flask app. All error responses that you don't specifically manage yourself will have application/json content type, and will contain JSON like this (just an example): { "message": "405: Method Not Allowed" } """ def make_json_error(ex): pprint.pprint(ex) # pprint.pprint(ex.code) response = flask.jsonify(message=str(ex)) # response = jsonify(ex) response.status_code = (ex.code if isinstance(ex, HTTPException) else 500) pprint.pprint(response) return response app = flask.Flask(import_name, **kwargs) # app.debug = True app.secret_key = id_generator(24) for code in list(default_exceptions.keys()): app.errorhandler(code)(make_json_error) return app app = make_json_app(__name__) session_key = id_generator(24) def debugRequest(request): if debugRequests: print("\n") pprint.pprint(request) pprint.pprint(request.headers) pprint.pprint(request.data) def throw_error(http_code, error_code=None, desc=None, debug1=None, debug2=None): if error_code: info = {'code': error_code, 'desc': desc} if debug1: info['debug1'] = debug1 if debug2: info['debug2'] = debug2 flask.abort(flask.Response(json.dumps(info), status=http_code)) else: flask.abort(http_code) @app.route('/') def index(): debugRequest(flask.request) if 'username' in flask.session: return 'Logged in as %s' % flask.escape(flask.session['username']) flask.abort(401) @app.route('/api/v1/throwerror') def errtest(): debugRequest(flask.request) throw_error(405, 123, 'testing throwing an error', 'debug1 message', 'debug2 message') @app.errorhandler(404) def not_found(error): debugRequest(flask.request) return flask.Response("%s has not been implemented" % flask.request.path, status=501) @app.route('/api/v1/credentials', methods=['GET', 'POST']) def credentials(): debugRequest(flask.request) if flask.request.method == 'GET': return 'GET credentials called' elif flask.request.method == 'POST': data = json.loads(flask.request.data.decode('utf-8')) if data['user'] == user_name and data['password'] == user_pass: # do something good here try: resp = flask.make_response(json.dumps({'key': session_key}), 201) resp.headers['Location'] = ('/api/v1/credentials/%s' % session_key) flask.session['username'] = data['user'] flask.session['password'] = data['password'] flask.session['session_key'] = session_key return resp except Exception as ex: pprint.pprint(ex) else: # authentication failed! throw_error(403, INV_USER_PASS, "invalid username or password") @app.route('/api/v1/credentials/', methods=['DELETE']) def logout_credentials(session_key): debugRequest(flask.request) flask.session.clear() return 'DELETE credentials called' # CPG @app.route('/api/v1/cpgs', methods=['POST']) def create_cpgs(): debugRequest(flask.request) data = json.loads(flask.request.data.decode('utf-8')) valid_keys = {'name': None, 'growthIncrementMB': None, 'growthLimitMB': None, 'usedLDWarningAlertMB': None, 'domain': None, 'LDLayout': None} valid_LDLayout_keys = {'RAIDType': None, 'setSize': None, 'HA': None, 'chuckletPosRef': None, 'diskPatterns': None} for key in list(data.keys()): if key not in list(valid_keys.keys()): throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % key) elif 'LDLayout' in list(data.keys()): layout = data['LDLayout'] for subkey in list(layout.keys()): if subkey not in valid_LDLayout_keys: throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % subkey) if 'domain' in data and data['domain'] == 'BAD_DOMAIN': throw_error(404, NON_EXISTENT_DOMAIN, "Non-existing domain specified.") for cpg in cpgs['members']: if data['name'] == cpg['name']: throw_error(409, EXISTENT_CPG, "CPG '%s' already exist." % data['name']) cpgs['members'].append(data) cpgs['total'] = cpgs['total'] + 1 return flask.make_response("", 200) @app.route('/api/v1/cpgs', methods=['GET']) def get_cpgs(): debugRequest(flask.request) # should get it from global cpgs resp = flask.make_response(json.dumps(cpgs), 200) return resp @app.route('/api/v1/cpgs/', methods=['GET']) def get_cpg(cpg_name): debugRequest(flask.request) for cpg in cpgs['members']: if cpg['name'] == cpg_name: resp = flask.make_response(json.dumps(cpg), 200) return resp throw_error(404, NON_EXISTENT_CPG, "CPG '%s' doesn't exist" % cpg_name) @app.route('/api/v1/spacereporter', methods=['POST']) def get_cpg_available_space(): debugRequest(flask.request) data = json.loads(flask.request.data.decode('utf-8')) for cpg in cpgs['members']: if cpg['name'] == data['cpg']: fake_cpg_info = { "rawFreeMiB": 7630848, "usableFreeMiB": 3815424 } resp = flask.make_response(json.dumps(fake_cpg_info), 200) return resp throw_error(404, NON_EXISTENT_CPG, "CPG '%s' doesn't exist" % data['cpg']) @app.route('/api/v1/cpgs/', methods=['DELETE']) def delete_cpg(cpg_name): debugRequest(flask.request) for cpg in cpgs['members']: if cpg['name'] == cpg_name: cpgs['members'].remove(cpg) return flask.make_response("", 200) throw_error(404, NON_EXISTENT_CPG, "CPG '%s' doesn't exist" % cpg_name) # Host Set def get_host_set_for_host(name): for host_set in host_sets['members']: for host_name in host_set['setmembers']: if host_name == name: return host_set['name'] return None @app.route('/api/v1/hostsets', methods=['GET']) def get_host_sets(): debugRequest(flask.request) resp = flask.make_response(json.dumps(host_sets), 200) return resp @app.route('/api/v1/hostsets', methods=['POST']) def create_host_set(): debugRequest(flask.request) data = json.loads(flask.request.data.decode('utf-8')) valid_keys = {'name': None, 'comment': None, 'domain': None, 'setmembers': None} for key in list(data.keys()): if key not in list(valid_keys.keys()): throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % key) if 'name' in list(data.keys()): for host_set in host_sets['members']: if host_set['name'] == data['name']: throw_error(409, EXISTENT_SET, 'Set exists') if len(data['name']) > 31: throw_error(400, INV_INPUT_EXCEEDS_LENGTH, 'invalid input: string length exceeds limit') else: throw_error(400, INV_INPUT, 'No host set name provided.') host_sets['members'].append(data) resp = flask.make_response( "", 201, {'location': '/api/v1/hostsets/' + data['name']}) return resp @app.route('/api/v1/hostsets/', methods=['GET']) def get_host_set(host_set_name): debugRequest(flask.request) charset = {'!', '@', '#', '$', '%', '&', '^'} for char in charset: if char in host_set_name: throw_error(400, INV_INPUT_ILLEGAL_CHAR, 'illegal character in input') for host_set in host_sets['members']: if host_set['name'] == host_set_name: resp = flask.make_response(json.dumps(host_set), 200) return resp throw_error(404, NON_EXISTENT_SET, "host set doesn't exist") @app.route('/api/v1/hostsets/', methods=['PUT']) def modify_host_set(host_set_name): debugRequest(flask.request) if len(host_set_name) > 31: throw_error(400, INV_INPUT_EXCEEDS_LENGTH, 'invalid input: string length exceeds limit') data = json.loads(flask.request.data.decode('utf-8')) if 'newName' in data: if len(data['newName']) > 32: throw_error(400, INV_INPUT_EXCEEDS_LENGTH, 'host set name is too long.') if 'setmembers' in data: throw_error(400, INV_INPUT_PARAM_CONFLICT, "invalid input: parameters cannot be present at the" " same time") for host_set in host_sets['members']: if host_set['name'] == host_set_name: if 'newName' in data: host_set['name'] = data['newName'] if 'comment' in data: host_set['comment'] = data['comment'] if 'setmembers' in data and 'action' in data: members = data['setmembers'] for member in members: get_host(member) if 1 == data['action']: # 1 is memAdd - Adds a member to the set if 'setmembers' not in host_set: host_set['setmembers'] = [] if member not in host_set['setmembers']: host_set['setmembers'].extend(members) else: throw_error(409, HOST_IN_SET, "The object is already part of the set") elif 2 == data['action']: # 2 is memRemove- Removes a member from the set for member in members: host_set['setmembers'].remove(member) else: throw_error(400, INV_INPUT_BAD_ENUM_VALUE, desc='invalid input: bad enum value - action') resp = flask.make_response(json.dumps(host_set), 200) return resp throw_error(404, NON_EXISTENT_SET, "host set doesn't exist") @app.route('/api/v1/hostsets/', methods=['DELETE']) def delete_host_set(host_set_name): debugRequest(flask.request) for host_set in host_sets['members']: if host_set['name'] == host_set_name: host_sets['members'].remove(host_set) return flask.make_response("", 200) throw_error(404, NON_EXISTENT_SET, "The host set '%s' does not exists." % host_set_name) # Host @app.route('/api/v1/hosts', methods=['POST']) def create_hosts(): debugRequest(flask.request) data = json.loads(flask.request.data.decode('utf-8')) valid_members = ['FCWWNs', 'descriptors', 'domain', 'iSCSINames', 'id', 'name'] for member_key in list(data.keys()): if member_key not in valid_members: throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % member_key) if data['name'] is None: throw_error(400, INV_INPUT_MISSING_REQUIRED, 'Name not specified.') elif len(data['name']) > 31: throw_error(400, INV_INPUT_EXCEEDS_LENGTH, 'Host name is too long.') elif 'domain' in data and len(data['domain']) > 31: throw_error(400, INV_INPUT_EXCEEDS_LENGTH, 'Domain name is too long.') elif 'domain' in data and data['domain'] == '': throw_error(400, INV_INPUT_EMPTY_STR, 'Input string (for domain, iSCSI etc.) is empty.') charset = {'!', '@', '#', '$', '%', '&', '^'} for char in charset: if char in data['name']: throw_error(400, INV_INPUT_ILLEGAL_CHAR, 'Error parsing host-name or domain-name') elif 'domain' in data and char in data['domain']: throw_error(400, INV_INPUT_ILLEGAL_CHAR, 'Error parsing host-name or domain-name') if 'FCWWNs' in list(data.keys()): if 'iSCSINames' in list(data.keys()): throw_error(400, INV_INPUT_PARAM_CONFLICT, 'FCWWNS and iSCSINames are both specified.') if 'FCWWNs' in list(data.keys()): fc = data['FCWWNs'] for wwn in fc: if len(wwn.replace(':', '')) != 16: throw_error(400, INV_INPUT_WRONG_TYPE, 'Length of WWN is not 16.') if 'FCWWNs' in data: for host in hosts['members']: if 'FCWWNs' in host: for fc_path in data['FCWWNs']: if fc_path in host['FCWWNs']: throw_error(409, EXISTENT_PATH, 'WWN already claimed by other host.') if 'iSCSINames' in data: for host in hosts: if 'iSCSINames' in host: for iqn in data['iSCSINames']: if iqn in host['iSCSINames']: throw_error(409, EXISTENT_PATH, 'iSCSI name already claimed by other' ' host.') for host in hosts['members']: if data['name'] == host['name']: throw_error(409, EXISTENT_HOST, "HOST '%s' already exist." % data['name']) hosts['members'].append(data) hosts['total'] = hosts['total'] + 1 resp = flask.make_response("", 201) return resp @app.route('/api/v1/hosts/', methods=['PUT']) def modify_host(host_name): debugRequest(flask.request) data = json.loads(flask.request.data.decode('utf-8')) if host_name == 'None': throw_error(404, INV_INPUT, 'Missing host name.') if 'FCWWNs' in list(data.keys()): if 'iSCSINames' in list(data.keys()): throw_error(400, INV_INPUT_PARAM_CONFLICT, 'FCWWNS and iSCSINames are both specified.') elif 'pathOperation' not in list(data.keys()): throw_error(400, INV_INPUT_ONE_REQUIRED, 'pathOperation is missing and WWN is specified.') if 'iSCSINames' in list(data.keys()): if 'pathOperation' not in list(data.keys()): throw_error(400, INV_INPUT_ONE_REQUIRED, 'pathOperation is missing and iSCSI Name is' ' specified.') if 'newName' in list(data.keys()): charset = {'!', '@', '#', '$', '%', '&', '^'} for char in charset: if char in data['newName']: throw_error(400, INV_INPUT_ILLEGAL_CHAR, 'Error parsing host-name or domain-name') if len(data['newName']) > 32: throw_error(400, INV_INPUT_EXCEEDS_LENGTH, 'New host name is too long.') for host in hosts['members']: if host['name'] == data['newName']: throw_error(409, EXISTENT_HOST, 'New host name is already used.') if 'pathOperation' in list(data.keys()): if 'iSCSINames' in list(data.keys()): for host in hosts['members']: if host['name'] == host_name: if data['pathOperation'] == 1: for host in hosts['members']: if 'iSCSINames' in list(host.keys()): for path in data['iSCSINames']: for h_paths in host['iSCSINames']: if path == h_paths: throw_error(409, EXISTENT_PATH, 'iSCSI name is already' ' claimed by other ' 'host.') for path in data['iSCSINames']: host['iSCSINames'].append(path) resp = flask.make_response(json.dumps(host), 200) return resp elif data['pathOperation'] == 2: for path in data['iSCSINames']: for h_paths in host['iSCSINames']: if path == h_paths: host['iSCSINames'].remove(h_paths) resp = flask.make_response( json.dumps(host), 200) return resp throw_error(404, NON_EXISTENT_PATH, 'Removing a non-existent path.') else: throw_error(400, INV_INPUT_BAD_ENUM_VALUE, 'pathOperation: Invalid enum value.') throw_error(404, NON_EXISTENT_HOST, 'Host to be modified does not exist.') elif 'FCWWNs' in list(data.keys()): for host in hosts['members']: if host['name'] == host_name: if data['pathOperation'] == 1: for host in hosts['members']: if 'FCWWNs' in list(host.keys()): for path in data['FCWWNs']: for h_paths in host['FCWWNs']: if path == h_paths: throw_error(409, EXISTENT_PATH, 'WWN is already ' 'claimed by other ' 'host.') for path in data['FCWWNs']: host['FCWWNs'].append(path) resp = flask.make_response(json.dumps(host), 200) return resp elif data['pathOperation'] == 2: for path in data['FCWWNs']: for h_paths in host['FCWWNs']: if path == h_paths: host['FCWWNs'].remove(h_paths) resp = flask.make_response( json.dumps(host), 200) return resp throw_error(404, NON_EXISTENT_PATH, 'Removing a non-existent path.') else: throw_error(400, INV_INPUT_BAD_ENUM_VALUE, 'pathOperation: Invalid enum value.') throw_error(404, NON_EXISTENT_HOST, 'Host to be modified does not exist.') else: throw_error(400, INV_INPUT_ONE_REQUIRED, 'pathOperation specified and no WWNs or iSCSNames' ' specified.') for host in hosts['members']: if host['name'] == host_name: for member_key in list(data.keys()): if member_key == 'newName': host['name'] = data['newName'] else: host[member_key] = data[member_key] resp = flask.make_response(json.dumps(host), 200) return resp throw_error(404, NON_EXISTENT_HOST, 'Host to be modified does not exist.') @app.route('/api/v1/hosts/', methods=['DELETE']) def delete_host(host_name): debugRequest(flask.request) # Can't delete a host with VLUN if len(get_vluns_for_host(host_name)) > 0: throw_error(409, EXPORTED_VLUN, "has exported VLUN") # Can't delete a host in a host set if get_host_set_for_host(host_name) is not None: throw_error(409, HOST_IN_SET, "host is a member of a set") for host in hosts['members']: if host['name'] == host_name: hosts['members'].remove(host) return flask.make_response("", 200) throw_error(404, NON_EXISTENT_HOST, "The host '%s' doesn't exist" % host_name) @app.route('/api/v1/hosts', methods=['GET']) def get_hosts(): debugRequest(flask.request) query = flask.request.args.get('query') matched_hosts = [] if query is not None: parsed_query = _parse_query(query) for host in hosts['members']: pprint.pprint(host) if 'FCWWNs' in host: pprint.pprint(host['FCWWNs']) for hostwwn in host['FCWWNs']: if hostwwn.replace(':', '') in parsed_query['wwns']: matched_hosts.append(host) break elif 'iSCSINames' in host: pprint.pprint(host['iSCSINames']) for iqn in host['iSCSINames']: if iqn in parsed_query['iqns']: matched_hosts.append(host) break result = {'total': len(matched_hosts), 'members': matched_hosts} resp = flask.make_response(json.dumps(result), 200) else: resp = flask.make_response(json.dumps(hosts), 200) return resp def _parse_query(query): wwns = re.findall("wwn==([0-9A-Z]*)", query) iqns = re.findall("name==([\w.:-]*)", query) parsed_query = {"wwns": wwns, "iqns": iqns} return parsed_query @app.route('/api/v1/hosts/', methods=['GET']) def get_host(host_name): debugRequest(flask.request) charset = {'!', '@', '#', '$', '%', '&', '^'} for char in charset: if char in host_name: throw_error(400, INV_INPUT_ILLEGAL_CHAR, 'Host name contains invalid character.') if host_name == 'InvalidURI': throw_error(400, INV_INPUT, 'Invalid URI Syntax.') for host in hosts['members']: if host['name'] == host_name: if 'iSCSINames' in list(host.keys()): iscsi_paths = [] for path in host['iSCSINames']: iscsi_paths.append({'name': path}) host['iSCSIPaths'] = iscsi_paths elif 'FCWWNs' in list(host.keys()): fc_paths = [] for path in host['FCWWNs']: fc_paths.append({'wwn': path.replace(':', '')}) host['FCPaths'] = fc_paths resp = flask.make_response(json.dumps(host), 200) return resp throw_error(404, NON_EXISTENT_HOST, "host does not exist") # Port @app.route('/api/v1/ports', methods=['GET']) def get_ports(): debugRequest(flask.request) resp = flask.make_response(json.dumps(ports), 200) return resp # VLUN @app.route('/api/v1/vluns', methods=['POST']) def create_vluns(): debugRequest(flask.request) data = json.loads(flask.request.data.decode('utf-8')) valid_keys = {'volumeName': None, 'lun': 0, 'hostname': None, 'portPos': None, 'noVcn': False, 'overrideLowerPriority': False} valid_port_keys = {'node': 1, 'slot': 1, 'cardPort': 0} # do some fake errors here depending on data for key in list(data.keys()): if key not in list(valid_keys.keys()): throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % key) elif 'portPos' in list(data.keys()): portP = data['portPos'] for subkey in list(portP.keys()): if subkey not in valid_port_keys: throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % subkey) if 'lun' in data: if data['lun'] > 16384: throw_error(400, TOO_LARGE, 'LUN is greater than 16384.') else: throw_error(400, INV_INPUT, 'Missing LUN.') if 'volumeName' not in data: throw_error(400, INV_INPUT_MISSING_REQUIRED, 'Missing volumeName.') else: for volume in volumes['members']: if volume['name'] == data['volumeName']: vluns['members'].append(data) resp = flask.make_response(json.dumps(vluns), 201) resp.headers['location'] = '/api/v1/vluns/' return resp throw_error(404, NON_EXISTENT_VOL, 'Specified volume does not exist.') @app.route('/api/v1/vluns/', methods=['DELETE']) def delete_vluns(vlun_str): # is like volumeName,lun,host,node:slot:port debugRequest(flask.request) params = vlun_str.split(',') for vlun in vluns['members']: if vlun['volumeName'] == params[0] and vlun['lun'] == int(params[1]): if len(params) == 4: if str(params[2]) != vlun['hostname']: throw_error(404, NON_EXISTENT_HOST, "The host '%s' doesn't exist" % params[2]) print(vlun['portPos']) port = getPort(vlun['portPos']) if not port == params[3]: throw_error(400, INV_INPUT_PORT_SPECIFICATION, "Specified port is invalid %s" % params[3]) elif len(params) == 3: if ':' in params[2]: port = getPort(vlun['portPos']) if not port == params[2]: throw_error(400, INV_INPUT_PORT_SPECIFICATION, "Specified port is invalid %s" % params[2]) else: if str(params[2]) != vlun['hostname']: throw_error(404, NON_EXISTENT_HOST, "The host '%s' doesn't exist" % params[2]) vluns['members'].remove(vlun) return flask.make_response(json.dumps(params), 200) throw_error(404, NON_EXISTENT_VLUN, "The volume '%s' doesn't exist" % vluns) def getPort(portPos): port = "%s:%s:%s" % (portPos['node'], portPos['slot'], portPos['cardPort']) print(port) return port @app.route('/api/v1/vluns', methods=['GET']) def get_vluns(): debugRequest(flask.request) resp = flask.make_response(json.dumps(vluns), 200) return resp def get_vluns_for_host(host_name): ret = [] for vlun in vluns['members']: if vlun['hostname'] == host_name: ret.append(vlun) return ret # VOLUMES & SNAPSHOTS @app.route('/api/v1/volumes/', methods=['POST']) def create_snapshot(volume_name): debugRequest(flask.request) data = json.loads(flask.request.data.decode('utf-8')) # is this for an online copy? onlineCopy = False valid_keys = {'action': None, 'parameters': None} valid_parm_keys = {'name': None, 'destVolume': None, 'destCPG': None, 'id': None, 'comment': None, 'online': None, 'readOnly': None, 'expirationHours': None, 'retentionHours': None} # do some fake errors here depending on data for key in list(data.keys()): if key not in list(valid_keys.keys()): throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % key) elif 'parameters' in list(data.keys()): parm = data['parameters'] for subkey in list(parm.keys()): if subkey not in valid_parm_keys: throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % subkey) if 'action' in data and data['action'] == 'createPhysicalCopy': valid_offline_param_keys = {'online': None, 'destVolume': None, 'saveSnapshot': None, 'priority': None} valid_online_param_keys = {'online': None, 'destCPG': None, 'tpvv': None, 'tdvv': None, 'snapCPG': None, 'saveSnapshot': None, 'priority': None, 'reduce': None} params = data['parameters'] if 'online' in params and params['online']: # we are checking online copy onlineCopy = True for subkey in params.keys(): if subkey not in valid_online_param_keys: throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % subkey) else: # we are checking offline copy for subkey in params.keys(): if subkey not in valid_offline_param_keys: throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % subkey) for volume in volumes['members']: if volume['name'] == volume_name: if data['action'] == "createPhysicalCopy": new_name = data['parameters'].get('destVolume') if not onlineCopy: # we have to have the destination volume for offline copies found = False for vol in volumes['members']: if vol['name'] == new_name: found = True break if not found: throw_error(404, NON_EXISTENT_VOL, "volume does not exist") else: new_name = data['parameters'].get('name') volumes['members'].append({'name': new_name}) resp = flask.make_response(json.dumps(volume), 200) return resp throw_error(404, NON_EXISTENT_VOL, "volume doesn't exist") @app.route('/api/v1/volumesets/', methods=['POST']) def create_volumeset_snapshot(volumeset_name): debugRequest(flask.request) data = json.loads(flask.request.data.decode('utf-8')) valid_keys = {'action': None, 'parameters': None} valid_parm_keys = {'name': None, 'destVolume': None, 'destCPG': None, 'id': None, 'comment': None, 'online': None, 'readOnly': None, 'expirationHours': None, 'retentionHours': None} # do some fake errors here depending on data for key in list(data.keys()): if key not in list(valid_keys.keys()): throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % key) elif 'parameters' in list(data.keys()): parm = data['parameters'] for subkey in list(parm.keys()): if subkey not in valid_parm_keys: throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % subkey) vvset_snap_name = data['parameters']['name'] snap_base = vvset_snap_name.split("@count@")[0] for vset in volume_sets['members']: setmembers = vset.get('setmembers', None) if vset['name'] == volumeset_name and setmembers: for i, member in enumerate(setmembers): vol_name = snap_base + str(i) volumes['members'].append({'name': vol_name, 'copyOf': member}) if data['action'] == "createPhysicalCopy": new_name = data['parameters'].get('destVolume') else: new_name = data['parameters'].get('name') volume_sets['members'].append({'name': new_name}) resp = flask.make_response(json.dumps(vset), 200) return resp throw_error(404, NON_EXISTENT_SET, "volume set doesn't exist") @app.route('/api/v1/volumes', methods=['POST']) def create_volumes(): debugRequest(flask.request) data = json.loads(flask.request.data.decode('utf-8')) valid_keys = {'name': None, 'cpg': None, 'sizeMiB': None, 'id': None, 'comment': None, 'policies': None, 'snapCPG': None, 'ssSpcAllocWarningPct': None, 'ssSpcAllocLimitPct': None, 'tpvv': None, 'usrSpcAllocWarningPct': None, 'usrSpcAllocLimitPct': None, 'isCopy': None, 'copyOfName': None, 'copyRO': None, 'expirationHours': None, 'retentionHours': None, 'reduce': None} for key in list(data.keys()): if key not in list(valid_keys.keys()): throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % key) if 'name' in list(data.keys()): for vol in volumes['members']: if vol['name'] == data['name']: throw_error(409, EXISTENT_VOL, 'The volume already exists.') if len(data['name']) > 31: throw_error(400, INV_INPUT_EXCEEDS_LENGTH, 'Invalid Input: String length exceeds limit : Name') else: throw_error(400, INV_INPUT, 'No volume name provided.') if 'sizeMiB' in list(data.keys()): if data['sizeMiB'] < 256: throw_error(400, INV_INPUT_EXCEEDS_RANGE, 'Minimum volume size is 256 MiB') elif data['sizeMiB'] > 16777216: throw_error(400, TOO_LARGE, 'Volume size is above architectural limit : 16TiB') if 'tpvv' in list(data.keys()): if data['tpvv'] not in [True, False, None]: throw_error(400, INV_INPUT_WRONG_TYPE, 'Invalid input:wrong type for value - tpvv') if 'id' in list(data.keys()): for vol in volumes['members']: if vol['id'] == data['id']: throw_error(409, EXISTENT_ID, 'Specified volume ID already exists.') volumes['members'].append(data) return flask.make_response("", 200) @app.route('/api/v1/volumes/', methods=['DELETE']) def delete_volumes(volume_name): debugRequest(flask.request) for volume in volumes['members']: if volume['name'] == volume_name: volumes['members'].remove(volume) return flask.make_response("", 200) throw_error(404, NON_EXISTENT_VOL, "The volume '%s' does not exists." % volume_name) @app.route('/api/v1/volumes', methods=['GET']) def get_volumes(): debugRequest(flask.request) resp = flask.make_response(json.dumps(volumes), 200) return resp @app.route('/api/v1/volumes/', methods=['GET']) def get_volume(volume_name): debugRequest(flask.request) charset = {'!', '@', '#', '$', '%', '&', '^'} for char in charset: if char in volume_name: throw_error(400, INV_INPUT_ILLEGAL_CHAR, 'Invalid character for volume name.') for volume in volumes['members']: if volume['name'] == volume_name: resp = flask.make_response(json.dumps(volume), 200) return resp throw_error(404, NON_EXISTENT_VOL, "volume doesn't exist") @app.route('/api/v1/volumes/', methods=['PUT']) def modify_volume(volume_name): debugRequest(flask.request) if volume_name not in [volume['name'] for volume in volumes['members']]: throw_error(404, NON_EXISTENT_VOL, "The volume does not exist") for volume in volumes['members']: if volume['name'] == volume_name: break data = json.loads(flask.request.data.decode('utf-8')) if 'online' in data and 'priority' in data: throw_error(409, INV_INPUT_PARAM_CONFLICT, "invalid input: parameters cannot be present at the" " same time") if len(remote_copy_groups['members']) != 0: if data.get('action') == 4: if data.get('allowRemoteCopyParent') is not True: throw_error(403, INV_OPERATION_VV_IN_REMOTE_COPY, "Volume is involved in remote copy") if data.get('action') == 4: task['taskid'] = '12345' resp = flask.make_response(json.dumps(task), 200) return resp if data.get('action') == 6: valid_keys = {'action': None, 'tuneOperation': None, 'userCPG': None, 'snapCPG': None, 'conversionOperation': None, 'keepVV': None, 'compression': None} for key in list(data.keys()): if key not in list(valid_keys.keys()): throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % key) if 'conversionOperation' in list(data.keys()): if data['conversionOperation'] not in [1, 2, 3, 4]: throw_error(400, INV_INPUT_WRONG_TYPE, "Invalid input:wrong type for value" " - conversionOperation") if 'compression' in list(data.keys()): if data['compression'] not in [True, False, None]: throw_error(400, INV_INPUT_WRONG_TYPE, "Invalid input:wrong type for value" " - compression") if 'tuneOperation' in list(data.keys()): if data['tuneOperation'] not in [1, 2]: throw_error(400, INV_INPUT_WRONG_TYPE, "Invalid input:wrong type for value" " - tuneOperation") if 'keepVV' in list(data.keys()) and len(data['keepVV']) > 31: throw_error(400, INV_INPUT_EXCEEDS_LENGTH, 'Invalid Input: String length exceeds limit : keepVV') conversion_operation = data.get('conversionOperation') if conversion_operation == 1: volume_type = 'tpvv' elif conversion_operation == 2: volume_type = 'fpvv' elif conversion_operation == 3: volume_type = 'tdvv' if conversion_operation == 4: if (volume.get('tdvv') is None or volume.get('tdvv') is False) and \ (volume.get('compression') is None or volume.get('compression') is False): volume['tdvv'] = True volume['compression'] = True else: if volume.get(volume_type) is None or \ volume.get(volume_type) is False: volume[volume_type] = True resp = flask.make_response(json.dumps(volume), 200) return resp _grow_volume(volume, data) # do volume renames last if 'newName' in data: volume['name'] = data['newName'] resp = flask.make_response(json.dumps(volume), 200) return resp def _grow_volume(volume, data): # Only grow if there is a need if 'sizeMiB' in data: size = data['sizeMiB'] if size <= 0: throw_error(400, INV_INPUT_VV_GROW_SIZE, 'Invalid grow size') cur_size = volume['sizeMiB'] new_size = cur_size + size if new_size > 16777216: throw_error(403, VV_NEW_SIZE_EXCEED_CPG_LIMIT, 'New volume size exceeds CPG limit.') volume['sizeMiB'] = new_size @app.route('/api/v1/remotecopygroups', methods=['GET']) def get_remote_copy_groups(): debugRequest(flask.request) resp = flask.make_response(json.dumps(remote_copy_groups), 200) return resp @app.route('/api/v1/remotecopygroups/', methods=['GET']) def get_remote_copy_group(rcg_name): debugRequest(flask.request) for rcg in remote_copy_groups['members']: if rcg['name'] == rcg_name: resp = flask.make_response(json.dumps(rcg), 200) return resp throw_error(404, NON_EXISTENT_RCOPY_GROUP, "remote copy group doesn't exist") @app.route('/api/v1/remotecopygroups', methods=['POST']) def create_remote_copy_group(): debugRequest(flask.request) data = json.loads(flask.request.data.decode('utf-8')) valid_keys = {'name': None, 'targets': None, 'targetName': None, 'mode': None, 'userCPG': None, 'snapCPG': None, 'localSnapCPG': None, 'localUserCPG': None, 'domain': None} for key in list(data.keys()): if key not in list(valid_keys.keys()): throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % key) if 'name' in list(data.keys()): for rcg in remote_copy_groups['members']: if rcg['name'] == data['name']: throw_error(409, EXISTENT_RCOPY_GROUP, "Remote copy group exists.") if len(data['name']) > 25: throw_error(400, INV_INPUT_EXCEEDS_LENGTH, 'Invalid Input: String length exceeds limit : Name') else: throw_error(400, INV_INPUT, 'No remote copy group provided.') data['volumes'] = [] remote_copy_groups['members'].append(data) return flask.make_response("", 200) @app.route('/api/v1/remotecopygroups/', methods=['DELETE']) def delete_remote_copy_group(rcg_name): debugRequest(flask.request) for rcg in remote_copy_groups['members']: if rcg['name'] == rcg_name: remote_copy_groups['members'].remove(rcg) return flask.make_response("", 200) throw_error(404, NON_EXISTENT_RCOPY_GROUP, "The remote copy group '%s' does not exist." % rcg_name) @app.route('/api/v1/remotecopygroups/', methods=['PUT']) def modify_remote_copy_group(rcg_name): debugRequest(flask.request) data = json.loads(flask.request.data.decode('utf-8')) valid_keys = {'targets': None, 'targetName': None, 'mode': None, 'userCPG': None, 'snapCPG': None, 'localSnapCPG': None, 'localUserCPG': None, 'domain': None, 'unsetUserCPG': None, 'unsetSnapCPG': None, '': None, 'remoteUserCPG': None, 'remoteSnapCPG': None, 'syncPeriod': None, 'rmSyncPeriod': None, 'snapFrequency': None, 'rmSnapFrequency': None, 'policies': None, 'autoRecover': None, 'overPeriodAlert': None, 'autoFailover': None, 'pathManagement': None, 'secVolumeName': None, 'snapshotName': None, 'volumeAutoCreation': None, 'skipInitialSync': None, 'volumeName': None, 'action': None} for key in list(data.keys()): if key not in list(valid_keys.keys()): throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % key) action = data.get('action') for rcg in remote_copy_groups['members']: if rcg['name'] == rcg_name: # We are modifying values for a remote copy group if not action: for k, v in data.items(): rcg[k] = v resp = flask.make_response(json.dumps(rcg), 200) # We are adding a volume to a remote copy group elif action == ADMIT_VV: vol_found = False for vol in volumes['members']: if data['volumeName'] == vol['name']: vol_found = True rcg['volumes'].append(vol) if not vol_found: throw_error(404, NON_EXISTENT_VOL, "volume doesn't exist") resp = flask.make_response(json.dumps(rcg), 200) # We are removing a volume to a remote copy group elif action == DISMISS_VV: for vol in rcg['volumes']: if data['volumeName'] == vol['name']: rcg['volumes'].remove(vol) resp = flask.make_response(json.dumps(rcg), 200) # We are starting remote copy on a group elif action == START_GROUP: targets = rcg['targets'] for target in targets: target['state'] = RCOPY_STARTED resp = flask.make_response(json.dumps(rcg), 200) # We are stopping remote copy on a group elif action == STOP_GROUP: targets = rcg['targets'] for target in targets: target['state'] = RCOPY_STOPPED resp = flask.make_response(json.dumps(rcg), 200) # We are synchronizing the remote copy group elif action == SYNC_GROUP: targets = rcg['targets'] sync_time = strftime("%Y-%m-%d %H:%M:%S", gmtime()) for target in targets: target['groupLastSyncTime'] = sync_time resp = flask.make_response(json.dumps(rcg), 200) return resp throw_error(404, NON_EXISTENT_RCOPY_GROUP, "remote copy group doesn't exist") @app.route('/api/v1/remotecopygroups/', methods=['POST']) def recover_remote_copy_group(rcg_name): debugRequest(flask.request) data = json.loads(flask.request.data.decode('utf-8')) valid_keys = {'targets': None, 'targetName': None, 'skipStart': None, 'skipSync': None, 'discardNewData': None, 'skipPromote': None, 'noSnapshot': None, 'stopGroups': None, 'localGroupsDirection': None, 'action': None} for key in list(data.keys()): if key not in list(valid_keys.keys()): throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % key) action = data.get('action') for rcg in remote_copy_groups['members']: if rcg['name'] == rcg_name: # We are failing over a remote copy group if action == FAILOVER_GROUP: rcg['roleReversed'] = True resp = flask.make_response(json.dumps(rcg), 200) return resp throw_error(404, NON_EXISTENT_RCOPY_GROUP, "remote copy group doesn't exist") @app.route('/api/v1/volumesets', methods=['GET']) def get_volume_sets(): debugRequest(flask.request) resp = flask.make_response(json.dumps(volume_sets), 200) return resp @app.route('/api/v1/volumesets', methods=['POST']) def create_volume_set(): debugRequest(flask.request) data = json.loads(flask.request.data.decode('utf-8')) valid_keys = {'name': None, 'comment': None, 'domain': None, 'setmembers': None} for key in list(data.keys()): if key not in list(valid_keys.keys()): throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % key) if 'name' in list(data.keys()): for vset in volume_sets['members']: if vset['name'] == data['name']: throw_error(409, EXISTENT_SET, "Set exists") if len(data['name']) > 31: throw_error(400, INV_INPUT_EXCEEDS_LENGTH, 'Invalid Input: String length exceeds limit : Name') else: throw_error(400, INV_INPUT, 'No volume set name provided.') volume_sets['members'].append(data) return flask.make_response("", 200) @app.route('/api/v1/volumesets/', methods=['GET']) def get_volume_set(volume_set_name): debugRequest(flask.request) charset = {'!', '@', '#', '$', '%', '&', '^'} for char in charset: if char in volume_set_name: throw_error(400, INV_INPUT_ILLEGAL_CHAR, 'Invalid character for volume set name.') for vset in volume_sets['members']: if vset['name'] == volume_set_name: resp = flask.make_response(json.dumps(vset), 200) return resp throw_error(404, NON_EXISTENT_SET, "volume set doesn't exist") @app.route('/api/v1/volumesets/', methods=['PUT']) def modify_volume_set(volume_set_name): debugRequest(flask.request) data = json.loads(flask.request.data.decode('utf-8')) for vset in volume_sets['members']: if vset['name'] == volume_set_name: if 'newName' in data: vset['name'] = data['newName'] if 'comment' in data: vset['comment'] = data['comment'] if 'flashCachePolicy' in data: vset['flashCachePolicy'] = data['flashCachePolicy'] if 'setmembers' in data and 'action' in data: members = data['setmembers'] if 1 == data['action']: # 1 is memAdd - Adds a member to the set if 'setmembers' not in vset: vset['setmembers'] = [] vset['setmembers'].extend(members) elif 2 == data['action']: # 2 is memRemove- Removes a member from the set for member in members: vset['setmembers'].remove(member) else: throw_error(400, INV_INPUT_BAD_ENUM_VALUE, desc='invalid input: bad enum value - action') resp = flask.make_response(json.dumps(vset), 200) return resp throw_error(404, NON_EXISTENT_SET, "volume set doesn't exist") @app.route('/api/v1/volumesets/', methods=['DELETE']) def delete_volume_set(volume_set_name): debugRequest(flask.request) for vset in volume_sets['members']: if vset['name'] == volume_set_name: volume_sets['members'].remove(vset) if 'qos' in vset: try: _delete_qos_db(vset['qos']) except Exception as ex: print(vars(ex)) return flask.make_response("", 200) throw_error(404, NON_EXISTENT_SET, "The volume set '%s' does not exists." % volume_set_name) def _validate_qos_input(data): valid_keys = {'name': None, 'type': None, 'priority': None, 'bwMinGoalKB': None, 'bwMaxLimitKB': None, 'ioMinGoal': None, 'ioMaxLimit': None, 'enable': None, 'bwMinGoalOP': None, 'bwMaxLimitOP': None, 'ioMinGoalOP': None, 'ioMaxLimitOP': None, 'latencyGoal': None, 'defaultLatency': None} for key in list(data.keys()): if key not in list(valid_keys.keys()): throw_error(400, INV_INPUT, "Invalid Parameter '%s'" % key) @app.route('/api/v1/qos', methods=['GET']) def query_all_qos(): debugRequest(flask.request) return flask.make_response(json.dumps(qos_db), 200) @app.route('/api/v1/qos/:', methods=['GET']) def query_qos(target_type, target_name): debugRequest(flask.request) qos = _get_qos_db(target_name) return flask.make_response(json.dumps(qos)) def _get_qos_db(name): for qos in qos_db['members']: if qos['name'] == name: return qos throw_error(404, NON_EXISTENT_QOS_RULE, "non-existent QoS rule") def debug_qos(title): if debugRequest: print(title) pprint.pprint(qos_db) def _add_qos_db(qos): debug_qos("_add_qos_db start") qos['id'] = uuid.uuid1().urn qos_db['members'].append(qos) qos_db['total'] = len(qos_db['members']) debug_qos("_add_qos_db end") return qos['id'] def _modify_qos_db(qos_id, new_qos): debug_qos("_modify_qos_db start") for qos in qos_db['members']: if qos['id'] == qos_id: qos.update(new_qos) debug_qos("_modify_qos_db end") return debug_qos("_modify_qos_db end error") throw_error(404, NON_EXISTENT_QOS_RULE, "non-existent QoS rule") def _delete_qos_db(qos_id): debug_qos("_delete_qos_db start") for qos in qos_db['members']: if qos['id'] == qos_id: qos_db['members'].remove(qos) debug_qos("_delete_qos_db end") @app.route('/api/v1/qos', methods=['POST']) def create_qos(): debugRequest(flask.request) qos = json.loads(flask.request.data.decode('utf-8')) if 'name' not in qos: throw_error(404, INV_INPUT, "Missing required parameter 'name'") if 'type' not in qos: throw_error(404, INV_INPUT, "Missing required parameter 'type'") elif qos['type'] != 1: throw_error(404, INV_INPUT, "Flask currently only supports type = 1 (VVSET). " "Type unsuppored: %s" % qos['type']) _validate_qos_input(qos) for vset in volume_sets['members']: if vset['name'] == qos['name']: if 'qos' in vset: throw_error(400, EXISTENT_QOS_RULE, "QoS rule exists") else: qos_id = _add_qos_db(qos) vset['qos'] = qos_id return flask.make_response("", 201) throw_error(404, INV_INPUT_QOS_TARGET_OBJECT, "Invalid QOS target object") @app.route('/api/v1/qos/:', methods=['PUT']) def modify_qos(target_type, name): debugRequest(flask.request) qos = json.loads(flask.request.data.decode('utf-8')) _validate_qos_input(qos) for vset in volume_sets['members']: if vset['name'] == name: if 'qos' not in vset: throw_error(404, NON_EXISTENT_QOS_RULE, "non-existent QoS rule") else: _modify_qos_db(vset['qos'], qos) return flask.make_response("", 200) throw_error(404, INV_INPUT_QOS_TARGET_OBJECT, "Invalid QOS target object") @app.route('/api/v1/qos/:', methods=['DELETE']) def delete_qos(target_type, target_name): debugRequest(flask.request) for vset in volume_sets['members']: if vset['name'] == target_name: if 'qos' not in vset: throw_error(404, NON_EXISTENT_SET, "QoS rule does not exists") else: _delete_qos_db(vset['qos']) return flask.make_response("", 200) throw_error(404, INV_INPUT_QOS_TARGET_OBJECT, "Invalid QOS target object") @app.route('/api/v1/wsapiconfiguration', methods=['GET']) def get_wsapi_configuration(): debugRequest(flask.request) # TODO: these are copied from the pdf config = {"httpState": "Enabled", "httpPort": 8008, "httpsState": "Enabled", "httpsPort": 8080, "version": "1.3", "sessionsInUse": 0, "systemResourceUsage": 144} return flask.make_response(json.dumps(config)) @app.route('/api/v1/system', methods=['GET']) def get_system(): debugRequest(flask.request) system_info = {"id": 12345, "name": "Flask", "systemVersion": "3.2.1.46", "IPv4Addr": "10.10.10.10", "model": "HP_3PAR 7400", "serialNumber": "1234567", "totalNodes": 2, "masterNode": 0, "onlineNodes": [0, 1], "clusterNodes": [0, 1], "chunkletSizeMiB": 1024, "totalCapacityMiB": 35549184.0, "allocatedCapacityMiB": 4318208.0, "freeCapacityMiB": 31230976.0, "failedCapacityMiB": 0.0, "location": "Flask Test Virtual", "owner": "Flask Owner", "contact": "flask@flask.com", "comment": "flask test env", "timeZone": "America/Los_Angeles"} resp = flask.make_response(json.dumps(system_info), 200) return resp @app.route('/api/v1/capacity', methods=['GET']) def get_overall_capacity(): debugRequest(flask.request) capacity_info = { "allCapacity": { "totalMiB": 20054016, "allocated": { "totalAllocatedMiB": 12535808, "volumes": { "totalVolumesMiB": 10919936, "nonCPGsMiB": 0, "nonCPGUserMiB": 0, "nonCPGSnapshotMiB": 0, "nonCPGAdminMiB": 0, "CPGsMiB": 10919936, "CPGUserMiB": 7205538, "CPGUserUsedMiB": 7092550, "CPGUserUnusedMiB": 112988, "CPGSnapshotMiB": 2411870, "CPGSnapshotUsedMiB": 210256, "CPGSnapshotUnusedMiB": 2201614, "CPGAdminMiB": 1302528, "CPGAdminUsedMiB": 115200, "CPGAdminUnusedMiB": 1187328, "unmappedMiB": 0 }, "system": { "totalSystemMiB": 1615872, "internalMiB": 780288, "spareMiB": 835584, "spareUsedMiB": 0, "spareUnusedMiB": 835584 } }, "freeMiB": 7518208, "freeInitializedMiB": 7518208, "freeUninitializedMiB": 0, "unavailableCapacityMiB": 0, "failedCapacityMiB": 0 }, "FCCapacity": { "totalMiB": 20054016, "allocated": { "totalAllocatedMiB": 12535808, "volumes": { "totalVolumesMiB": 10919936, "nonCPGsMiB": 0, "nonCPGUserMiB": 0, "nonCPGSnapshotMiB": 0, "nonCPGAdminMiB": 0, "CPGsMiB": 10919936, "CPGUserMiB": 7205538, "CPGUserUsedMiB": 7092550, "CPGUserUnusedMiB": 112988, "CPGSnapshotMiB": 2411870, "CPGSnapshotUsedMiB": 210256, "CPGSnapshotUnusedMiB": 2201614, "CPGAdminMiB": 1302528, "CPGAdminUsedMiB": 115200, "CPGAdminUnusedMiB": 1187328, "unmappedMiB": 0 }, "system": { "totalSystemMiB": 1615872, "internalMiB": 780288, "spareMiB": 835584, "spareUsedMiB": 0, "spareUnusedMiB": 835584 } }, "freeMiB": 7518208, "freeInitializedMiB": 7518208, "freeUninitializedMiB": 0, "unavailableCapacityMiB": 0, "failedCapacityMiB": 0 }, "NLCapacity": { "totalMiB": 0, "allocated": { "totalAllocatedMiB": 0, "volumes": { "totalVolumesMiB": 0, "nonCPGsMiB": 0, "nonCPGUserMiB": 0, "nonCPGSnapshotMiB": 0, "nonCPGAdminMiB": 0, "CPGsMiB": 0, "CPGUserMiB": 0, "CPGUserUsedMiB": 0, "CPGUserUnusedMiB": 0, "CPGSnapshotMiB": 0, "CPGSnapshotUsedMiB": 0, "CPGSnapshotUnusedMiB": 0, "CPGAdminMiB": 0, "CPGAdminUsedMiB": 0, "CPGAdminUnusedMiB": 0, "unmappedMiB": 0 }, "system": { "totalSystemMiB": 0, "internalMiB": 0, "spareMiB": 0, "spareUsedMiB": 0, "spareUnusedMiB": 0 } }, "freeMiB": 0, "freeInitializedMiB": 0, "freeUninitializedMiB": 0, "unavailableCapacityMiB": 0, "failedCapacityMiB": 0 }, "SSDCapacity": { "totalMiB": 0, "allocated": { "totalAllocatedMiB": 0, "volumes": { "totalVolumesMiB": 0, "nonCPGsMiB": 0, "nonCPGUserMiB": 0, "nonCPGSnapshotMiB": 0, "nonCPGAdminMiB": 0, "CPGsMiB": 0, "CPGUserMiB": 0, "CPGUserUsedMiB": 0, "CPGUserUnusedMiB": 0, "CPGSnapshotMiB": 0, "CPGSnapshotUsedMiB": 0, "CPGSnapshotUnusedMiB": 0, "CPGAdminMiB": 0, "CPGAdminUsedMiB": 0, "CPGAdminUnusedMiB": 0, "unmappedMiB": 0 }, "system": { "totalSystemMiB": 0, "internalMiB": 0, "spareMiB": 0, "spareUsedMiB": 0, "spareUnusedMiB": 0 } }, "freeMiB": 0, "freeInitializedMiB": 0, "freeUninitializedMiB": 0, "unavailableCapacityMiB": 0, "failedCapacityMiB": 0 }, } resp = flask.make_response(json.dumps(capacity_info), 200) return resp @app.route('/api', methods=['GET']) def get_version(): debugRequest(flask.request) version = {'major': 1, 'minor': 3, 'build': 30201256} resp = flask.make_response(json.dumps(version), 200) return resp @app.route('/api/v1/tasks/', methods=['GET']) def get_task(task_id): debugRequest(flask.request) try: task_id = int(task_id) except ValueError: throw_error(400, INV_INPUT_WRONG_TYPE, "Task ID is not an integer") if task_id <= 0: throw_error(400, INV_INPUT_BELOW_RANGE, "Task ID must be a positive value") if task_id > 65535: throw_error(400, INV_INPUT_EXCEEDS_RANGE, "Task ID is too large") for task in tasks['members']: if task['id'] == task_id: return flask.make_response(json.dumps(task), 200) throw_error(404, NON_EXISTENT_TASK, "Task not found: '%s'" % task_id) @app.route('/api/v1/tasks', methods=['GET']) def get_tasks(): debugRequest(flask.request) resp = flask.make_response(json.dumps(tasks), 200) return resp @app.route('/api/v1/volumes//objectKeyValues', methods=['POST']) def create_key_value_pair(volume_name): debugRequest(flask.request) kv_pair = json.loads(flask.request.data.decode('utf-8')) key = kv_pair['key'] value = kv_pair['value'] if key is None: throw_error(400, INV_INPUT_MISSING_REQUIRED, 'key not specified.') if value is None: throw_error(400, INV_INPUT_MISSING_REQUIRED, 'value not specified.') if len(key) > 31 or len(value) > 31: throw_error(400, INV_INPUT_EXCEEDS_LENGTH, 'invalid input: string length exceeds limit') charset = {'!', '@', '#', '$', '%', '&', '^'} for char in charset: if char in key or char in value: throw_error(400, INV_INPUT_ILLEGAL_CHAR, 'Error parsing key or value') vol_exists = False for member in volumes['members']: if member['name'] == volume_name: vol_exists = True if 'metadata' not in member: member['metadata'] = { 'total': 0, 'members': [] } for kv_pair in member['metadata']['members']: if kv_pair['key'] == key: throw_error(409, EXISTENT_OBJECT_KEY, "Key '%s' already exist." % key) member['metadata']['members'].append({'key': key, 'value': value}) member['metadata']['total'] += 1 break if not vol_exists: throw_error(404, NON_EXISTENT_VOL, "volume doesn't exist") return flask.make_response("Created", 201) @app.route('/api/v1/volumes//objectKeyValues/', methods=['PUT']) def update_key_value_pair(volume_name, key): debugRequest(flask.request) body = json.loads(flask.request.data.decode('utf-8')) value = body['value'] if key is None: throw_error(400, INV_INPUT_MISSING_REQUIRED, 'key not specified.') if value is None: throw_error(400, INV_INPUT_MISSING_REQUIRED, 'value not specified.') if len(key) > 31 or len(value) > 31: throw_error(400, INV_INPUT_EXCEEDS_LENGTH, 'invalid input: string length exceeds limit') charset = {'!', '@', '#', '$', '%', '&', '^'} for char in charset: if char in key or char in value: throw_error(400, INV_INPUT_ILLEGAL_CHAR, 'Error parsing key or value') vol_exists = False for member in volumes['members']: if member['name'] == volume_name: vol_exists = True if 'metadata' not in member: throw_error(404, NON_EXISTENT_OBJECT_KEY, "Key '%s' does not exist." % key) keyFound = False for kv_pair in member['metadata']['members']: if kv_pair['key'] == key: kv_pair['value'] = value keyFound = True if not keyFound: throw_error(404, NON_EXISTENT_OBJECT_KEY, "Key '%s' does not exist." % key) break if not vol_exists: throw_error(404, NON_EXISTENT_VOL, "volume doesn't exist") return flask.make_response("OK", 200) @app.route('/api/v1/volumes//objectKeyValues/', methods=['GET']) def get_key_value_pair(volume_name, key): debugRequest(flask.request) if len(key) > 31: throw_error(400, INV_INPUT_EXCEEDS_LENGTH, 'invalid input: string length exceeds limit') charset = {'!', '@', '#', '$', '%', '&', '^'} for char in charset: if char in key: throw_error(400, INV_INPUT_ILLEGAL_CHAR, 'Error parsing key or value') vol_exists = False resp = None for member in volumes['members']: if member['name'] == volume_name: vol_exists = True if 'metadata' not in member: throw_error(404, NON_EXISTENT_OBJECT_KEY, "Key '%s' does not exist." % key) keyFound = False for kv_pair in member['metadata']['members']: if kv_pair['key'] == key: resp = flask.make_response(json.dumps(kv_pair), 200) keyFound = True break if not keyFound: throw_error(404, NON_EXISTENT_OBJECT_KEY, "Key '%s' does not exist." % key) break if not vol_exists: throw_error(404, NON_EXISTENT_VOL, "volume doesn't exist") return resp @app.route('/api/v1/volumes//objectKeyValues', methods=['GET']) def get_all_key_value_pairs(volume_name): debugRequest(flask.request) vol_exists = False resp = None for member in volumes['members']: if member['name'] == volume_name: vol_exists = True if 'metadata' not in member: member['metadata'] = {'total': 0, 'members': []} resp = flask.make_response(json.dumps(member['metadata']), 200) break if not vol_exists: throw_error(404, NON_EXISTENT_VOL, "volume doesn't exist") return resp @app.route('/api/v1/volumes//objectKeyValues/', methods=['DELETE']) def remove_key_value_pair(volume_name, key): debugRequest(flask.request) if key is None: throw_error(404, NON_EXISTENT_OBJECT_KEY, "Key '%s' does not exist." % key) if len(key) > 31: throw_error(404, NON_EXISTENT_OBJECT_KEY, "Key '%s' does not exist." % key) charset = {'!', '@', '#', '$', '%', '&', '^'} for char in charset: if char in key: throw_error(400, INV_INPUT_ILLEGAL_CHAR, 'Error parsing key or value') vol_exists = False for member in volumes['members']: if member['name'] == volume_name: vol_exists = True if 'metadata' not in member: throw_error(404, NON_EXISTENT_OBJECT_KEY, "Key '%s' does not exist." % key) keyFound = False for i, kv_pair in enumerate(member['metadata']['members']): if kv_pair['key'] == key: member['metadata']['members'].pop(i) keyFound = True if not keyFound: throw_error(404, NON_EXISTENT_OBJECT_KEY, "Key '%s' does not exist." % key) break if not vol_exists: throw_error(404, NON_EXISTENT_VOL, "volume doesn't exist") return flask.make_response("OK", 200) if __name__ == "__main__": # fake 2 CPGs global cpgs cpgs = {'members': [{'SAGrowth': {'LDLayout': {'diskPatterns': [{'diskType': 1}]}, 'incrementMiB': 8192}, 'SAUsage': {'rawTotalMiB': 24576, 'rawUsedMiB': 768, 'totalMiB': 8192, 'usedMiB': 256}, 'SDGrowth': {'LDLayout': {'diskPatterns': [{'diskType': 1}]}, 'incrementMiB': 16384, 'limitMiB': 256000, 'warningMiB': 204800}, 'SDUsage': {'rawTotalMiB': 32768, 'rawUsedMiB': 2048, 'totalMiB': 16384, 'usedMiB': 1024}, 'UsrUsage': {'rawTotalMiB': 239616, 'rawUsedMiB': 229376, 'totalMiB': 119808, 'usedMiB': 114688}, 'additionalStates': [], 'degradedStates': [], 'domain': 'UNIT_TEST', 'failedStates': [], 'id': 0, 'name': 'UnitTestCPG', 'numFPVVs': 12, 'numTPVVs': 0, 'state': 1, 'uuid': 'f9b018cc-7cb6-4358-a0bf-93243f853d96'}, {'SAGrowth': {'LDLayout': {'diskPatterns': [{'diskType': 1}]}, 'incrementMiB': 8192}, 'SAUsage': {'rawTotalMiB': 24576, 'rawUsedMiB': 768, 'totalMiB': 8192, 'usedMiB': 256}, 'SDGrowth': {'LDLayout': {'diskPatterns': [{'diskType': 1}]}, 'incrementMiB': 16384, 'limitMiB': 256000, 'warningMiB': 204800}, 'SDUsage': {'rawTotalMiB': 32768, 'rawUsedMiB': 2048, 'totalMiB': 16384, 'usedMiB': 1024}, 'UsrUsage': {'rawTotalMiB': 239616, 'rawUsedMiB': 229376, 'totalMiB': 119808, 'usedMiB': 114688}, 'additionalStates': [], 'degradedStates': [], 'domain': 'UNIT_TEST', 'failedStates': [], 'id': 0, 'name': 'UnitTestCPG2', 'numFPVVs': 12, 'numTPVVs': 0, 'state': 1, 'uuid': 'f9b018cc-7cb6-4358-a0bf-93243f853d97'}], 'total': 2} # fake volumes global volumes volumes = {'members': [{'additionalStates': [], 'adminSpace': {'freeMiB': 0, 'rawReservedMiB': 384, 'reservedMiB': 128, 'usedMiB': 128}, 'baseId': 1, 'copyType': 1, 'creationTime8601': '2012-09-24T15:12:13-07:00', 'creationTimeSec': 1348524733, 'degradedStates': [], 'domain': 'UNIT_TEST', 'failedStates': [], 'id': 91, 'name': 'UnitTestVolume', 'policies': {'caching': True, 'oneHost': False, 'staleSS': True, 'system': False, 'zeroDetect': False}, 'provisioningType': 1, 'readOnly': False, 'sizeMiB': 102400, 'snapCPG': 'UnitTestCPG', 'snapshotSpace': {'freeMiB': 0, 'rawReservedMiB': 1024, 'reservedMiB': 512, 'usedMiB': 512}, 'ssSpcAllocLimitPct': 0, 'ssSpcAllocWarningPct': 95, 'state': 1, 'userCPG': 'UnitTestCPG', 'userSpace': {'freeMiB': 0, 'rawReservedMiB': 204800, 'reservedMiB': 102400, 'usedMiB': 102400}, 'usrSpcAllocLimitPct': 0, 'usrSpcAllocWarningPct': 0, 'uuid': '8bc9394e-f87a-4c1a-8777-11cba75af94c', 'wwn': '50002AC00001383D'}, {'additionalStates': [], 'adminSpace': {'freeMiB': 0, 'rawReservedMiB': 384, 'reservedMiB': 128, 'usedMiB': 128}, 'baseId': 41, 'comment': 'test volume', 'copyType': 1, 'creationTime8601': '2012-09-27T14:11:56-07:00', 'creationTimeSec': 1348780316, 'degradedStates': [], 'domain': 'UNIT_TEST', 'failedStates': [], 'id': 92, 'name': 'UnitTestVolume2', 'policies': {'caching': True, 'oneHost': False, 'staleSS': True, 'system': False, 'zeroDetect': False}, 'provisioningType': 1, 'readOnly': False, 'sizeMiB': 10240, 'snapCPG': 'UnitTestCPG', 'snapshotSpace': {'freeMiB': 0, 'rawReservedMiB': 1024, 'reservedMiB': 512, 'usedMiB': 512}, 'ssSpcAllocLimitPct': 0, 'ssSpcAllocWarningPct': 0, 'state': 1, 'userCPG': 'UnitTestCPG', 'userSpace': {'freeMiB': 0, 'rawReservedMiB': 20480, 'reservedMiB': 10240, 'usedMiB': 10240}, 'usrSpcAllocLimitPct': 0, 'usrSpcAllocWarningPct': 0, 'uuid': '6d5542b2-f06a-4788-879e-853ad0a3be42', 'wwn': '50002AC00029383D'}], 'total': 26} # fake ports global ports ports = {'members': [{'linkState': 4, 'mode': 2, 'nodeWwn': None, 'portPos': {'cardPort': 1, 'node': 1, 'slot': 7}, 'portWwn': '2C27D75375D5', 'protocol': 2, 'type': 7}, {'linkState': 4, 'mode': 2, 'nodeWwn': None, 'portPos': {'cardPort': 2, 'node': 2, 'slot': 8}, 'portWwn': '2C27D75375D6', 'protocol': 2, 'type': 7}, {'linkState': 4, 'mode': 2, 'nodeWwn': None, 'portPos': {'cardPort': 3, 'node': 3, 'slot': 5}, 'portWwn': '2C27D75375D7', 'protocol': 1, 'type': 7}, {'linkState': 4, 'mode': 2, 'nodeWwn': None, 'portPos': {'cardPort': 4, 'node': 4, 'slot': 6}, 'portWwn': '2C27D75375D8', 'protocol': 1, 'type': 7}, {'portPos': {'node': 0, 'slot': 3, 'cardPort': 1}, 'protocol': 4, 'linkState': 10, 'label': 'RCIP0', 'device': [], 'mode': 4, 'HWAddr': 'B4B52FA76931', 'type': 7}, {'portPos': {'node': 1, 'slot': 3, 'cardPort': 1}, 'protocol': 4, 'linkState': 10, 'label': 'RCIP1', 'device': [], 'mode': 4, 'HWAddr': 'B4B52FA768B1', 'type': 7}], 'total': 6} # fake host sets global host_sets host_sets = {'members': [], 'total': 0} # fake hosts global hosts hosts = {'members': [{'FCWWNs': [], 'descriptors': None, 'domain': 'UNIT_TEST', 'iSCSINames': [{'driverVersion': '1.0', 'firmwareVersion': '1.0', 'hostSpeed': 100, 'ipAddr': '10.10.221.59', 'model': 'TestModel', 'name': 'iqnTestName', 'portPos': {'cardPort': 1, 'node': 1, 'slot': 8}, 'vendor': 'HP'}], 'id': 11, 'name': 'UnitTestHost'}, {'FCWWNs': [], 'descriptors': None, 'domain': 'UNIT_TEST', 'iSCSINames': [{'driverVersion': '1.0', 'firmwareVersion': '1.0', 'hostSpeed': 100, 'ipAddr': '10.10.221.58', 'model': 'TestMode2', 'name': 'iqnTestName2', 'portPos': {'cardPort': 1, 'node': 1, 'slot': 8}, 'vendor': 'HP'}], 'id': 12, 'name': 'UnitTestHost2'}], 'total': 2} # fake create vluns global vluns vluns = {'members': [{'active': True, 'failedPathInterval': 0, 'failedPathPol': 1, 'hostname': 'UnitTestHost', 'lun': 31, 'multipathing': 1, 'portPos': {'cardPort': 1, 'node': 1, 'slot': 2}, 'remoteName': '100010604B0174F1', 'type': 4, 'volumeName': 'UnitTestVolume', 'volumeWWN': '50002AC00001383D'}, {'active': False, 'failedPathInterval': 0, 'failedPathPol': 1, 'hostname': 'UnitTestHost2', 'lun': 32, 'multipathing': 1, 'portPos': {'cardPort': 2, 'node': 2, 'slot': 3}, 'type': 3, 'volumeName': 'UnitTestVolume2', 'volumeWWN': '50002AC00029383D'}], 'total': 2} global volume_sets volume_sets = {'members': [], 'total': 0} global remote_copy_groups remote_copy_groups = {'members': [], 'total': 0} global target_remote_copy_groups target_remote_copy_groups = {'members': [], 'total': 0} global qos_db qos_db = {'members': [], 'total': 0} global task task = {"taskid": ''} global tasks tasks = {"total": 2, "members": [{"id": 8933, "type": 15, "name": "check_slow_disk", "status": 1, "startTime": "2014-02-06 13:07:03 PST", "finishTime": "2014-02-06 14:03:04 PST", "priority": -1, "user": "3parsvc"}, {"id": 8934, "type": 15, "name": "remove_expired_vvs", "status": 1, "startTime": "2014-02-06 13:27:03 PST", "finishTime": "2014-02-06 13:27:03 PST", "priority": -1, "user": "3parsvc"}]} app.run(port=args.port, debug=debugRequests) python-3parclient-4.2.12/test/HPE3ParMockServer_ssh.py0000644000000000000000000007304214106434774022512 0ustar rootroot00000000000000# (c) Copyright 2015-2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Test SSH server.""" import argparse import logging import os import shlex import socket import sys import threading import paramiko paramiko.util.log_to_file('paramiko_server.log') class CliParseException(Exception): pass class CliArgumentParser(argparse.ArgumentParser): def error(self, message): usage = super(CliArgumentParser, self).format_help() full_message = "%s\r\n%s" % (message, usage) raise CliParseException(full_message) def parse_args(self, *args): return super(CliArgumentParser, self).parse_args(args[1:]) class Cli(object): def __init__(self): self.log_name = 'paramiko.3PARCLI' self.logger = paramiko.util.get_logger(self.log_name) self.fpgs = {} self.vfss = {} def do_cli_other(self, *args): msg = 'FAIL! Mock SSH CLI does not know how to "%s".' % ' '.join(args) self.logger.log(logging.ERROR, msg) return msg def do_cli_exit(self, *args): self.logger.log(logging.INFO, "quiting... g'bye") return '' def do_cli_quit(self, *args): self.logger.log(logging.INFO, "quiting... g'bye") return '' def do_cli_setclienv(self, *args): command_string = ' '.join(args) self.logger.log(logging.INFO, command_string) return None def do_cli_createhost(self, *args): str = 'Host ' + args[1] + ' already used by host ' return str def do_cli_admitrcopylink(self, *args): return [] def do_cli_dismissrcopylink(self, *args): return [] def do_cli_startrcopy(self, *args): return [] def do_cli_admitrcopytarget(self, *args): return [] def do_cli_dismissrcopytarget(self, *args): return [] def do_cli_createsched(self, *args): return [] def do_cli_removesched(self, *args): return [] def do_cli_setsched(self, *args): return [] def do_cli_removefpg(self, *args): parser = CliArgumentParser(prog=args[0]) parser.add_argument( '-f', default=False, action="store_true", help="Specifies that the command is forced. If this option is " "not used, the command requires confirmation before " "proceeding with its operation.") parser.add_argument( '-forget', help="Removes the specified file provisioning group which is " "involved in Remote DR, keeping the virtual volume intact.") parser.add_argument( '-wait', default=False, action="store_true", help="Wait until the associated task is completed before " "proceeding. This option will produce verbose task " "information.") parser.add_argument( "fpgname", help="fpgname is the name of the file provisioning group to be " "removed. This specifier can be repeated to remove multiple " "common provisioning groups." "" "OR" "" "Specifies a glob-style pattern. This specifier can be " "repeated to remove multiple common provisioning groups. " "If this specifier is not used, the specifier must " "be used. See help on sub,globpat for more information.") opts = parser.parse_args(*args) fpg = self.fpgs.pop(opts.fpgname) if fpg is None: return "File Provisioning Group: %s did not exist." % fpg else: return "File Provisioning Group: %s removed." % fpg def do_cli_createfpg(self, *args): parser = CliArgumentParser(prog=args[0]) parser.add_argument( '-comment', help="Specifies the textual description of the " "file provisioning group.") parser.add_argument( '-full', default=False, action="store_true", help="Specifies the textual description of the " "file provisioning group.") parser.add_argument( '-node', help="Bind the created file provisioning group to the specified " "node.") parser.add_argument( '-recover', help="Recovers the specified file provisioning group which is " "involved in Remote DR and that was removed using the " "-forget option.") parser.add_argument( '-wait', default=False, action="store_true", help="Wait until the associated task is completed before " "proceeding. This option will produce verbose task " "information.") parser.add_argument( "cpgname", help="The CPG where the VVs associated with the file provisioning " "group will be created") parser.add_argument( "fpgname", help="The name of the file provisioning group to be created") parser.add_argument( "size", help=""" The size of the file provisioning group to be created. The specified size must be between 1T and 32T. A suffix (with no whitespace before the suffix) will modify the units to TB (t or T suffix). """) self.logger.log( logging.WARNING, "createfpg with argparser args %s" % ','.join(args)) opts = parser.parse_args(*args) # Don't know what CPGs are in flask, yet, so just # use hard-coded value for test case. cpg = opts.cpgname if cpg and cpg == 'thiscpgdoesnotexist': return 'Error: Invalid CPG name: %s\r' % cpg # ... and use hard-coded value for domain test case. if cpg and cpg.startswith('UT3_'): return '%s belongs to domain HARDCODED which cannot be used for ' \ 'File Services.\r' % cpg # Validate size. size = opts.size units = size[-1] if units.upper() != 'T': return 'The suffix, %s, for size is invalid.\r' % units if opts.fpgname in self.fpgs: return "Error: FPG %s already exists\r" % opts.fpgname self.fpgs[opts.fpgname] = opts return ('File Provisioning Group %s created.\n' 'File Provisioning Group %s activated.' % (opts.fpgname, opts.fpgname)) def do_cli_createvfs(self, *args): parser = CliArgumentParser(prog=args[0]) parser.add_argument( '-comment', help="Specifies any additional textual information.") parser.add_argument( '-bgrace', default='3600', help="The block grace time in seconds for quotas within the VFS.") parser.add_argument( '-igrace', default='3600', help="The inode grace time in seconds for quotas within the VFS.") parser.add_argument( '-fpg', help="The name of an existing File Provisioning Group in which " "the VFS should be created.") parser.add_argument( '-cpg', help="The CPG in which the File Provisioning Group should be " "created.") parser.add_argument( '-size', help="The size of the File Provisioning Group to be created.") parser.add_argument( '-node', help="The node to which the File Provisioning Group should be " "assigned. Can only be used when creating the FPG with the " "-cpg option.") parser.add_argument( '-vlan', help="The VLAN ID associated with the VFSIP.") parser.add_argument( '-wait', default=False, action="store_true", help="Wait until the associated task is completed before " "proceeding. This option will produce verbose task " "information.") parser.add_argument( "ipaddr", help="The IP address to which the VFS should be assigned") parser.add_argument( "subnet", help="The subnet for the IP Address.") parser.add_argument( "vfsname", help="The name of the VFS to be created.") self.logger.log( logging.INFO, "createvfs with argparser args %s" % ','.join(args)) opts = parser.parse_args(*args) bgrace = -1 try: bgrace = int(opts.bgrace) except Exception: pass if bgrace < 1 or bgrace > 2147483647: return 'bgrace value should be between 1 and 2147483647\r' igrace = -1 try: igrace = int(opts.igrace) except Exception: pass if igrace < 1 or igrace > 2147483647: return 'igrace value should be between 1 and 2147483647\r' if opts.vfsname in self.vfss: return 'VFS "%s" already exists within FPG %s\r' % (opts.vfsname, opts.fpg) self.vfss[opts.vfsname] = opts return 'Created VFS "%s" on FPG %s.' % (opts.vfsname, opts.fpg) def do_cli_showfpg(self, *args): usage = """ SYNTAX showfpg [options] [] OPTIONS -d Detailed output. """ details = False fpg_name = None for arg in args[1:]: if arg == '-d': if details: return "Option -d already specified" else: details = True elif arg[0] == '-': return "showfpg: Invalid option %s\r\n\r\n%s" % (arg, usage) elif not fpg_name: fpg_name = arg if fpg_name: if fpg_name in self.fpgs: return [ "Header", "------", self.fpgs[fpg_name] # TODO: formatting ] else: return "File Provisioning Group: %s not found" % fpg_name else: if self.fpgs: ret = [ "Header", "------", "foo" ] # for fpg in self.fpgs: # ret = '\n'.join((ret, fpg)) return ret else: return "No File Provisioning Groups found." def do_cli_gettpdinterface(self, *args): tpdinterface = open('./test/tpdinterface/tpdinterface.tcl', 'r') tcl = tpdinterface.read() self.logger.log( logging.ERROR, tcl) tpdinterface.close() return tcl def do_cli_getfs(self, *args): ret = \ '{' \ '{0 - Yes running Yes Yes 1.0.0.5-20140730 0:2:1,0:2:2 1 1500} '\ '{1 - Yes running No Yes 1.0.0.5-20140730 1:2:1,1:2:2 1 1500} ' \ '{2 - No Unknown No No - - - -} ' \ '{3 - No Unknown No No - - - -}' \ '} ' \ '{' \ '{0 unityUserAddr5a5c7103-252a-413a-ae81-49688ea7ece0 ' \ '10.50.158.1 255.255.0.0 0} ' \ '{1 unityUserAddrcd512782-1e2e-4a1d-be71-3b568b65594d ' \ '10.50.158.2 255.255.0.0 0}' \ '} ' \ '{unityUserGWAddr291e4af6-925f-4f9d-b4eb-305a4b21fad5 10.50.0.1} '\ '{10.50.0.5 csim.rose.hpe.com} ' \ '{{defaultProfile 80 443 true 5 58 8192 8192}} ' \ '{} {} {{ActiveDirectory Local}} ' \ '{false ' \ '{' \ '0.centos.pool.ntp.org ' \ '1.centos.pool.ntp.org ' \ '2.centos.pool.ntp.org}}' return ret def do_cli_getfpg(self, *args): filtered_fpgs = [] if len(args) > 1: for fpg in args[1:]: if not fpg in self.fpgs: return 'File Provisioning Group: %s not found\r' % fpg else: filtered_fpgs.append(self.fpgs[fpg]) else: filtered_fpgs = list(self.fpgs.values()) # Place-holders for most values set outside of loop. # Param based on the create params (or generated) inside the loop. fpg_dict = { 'uuid': '49ec3e7e-d7af-4e73-9536-79065483164f', 'generation': '1', 'hosts': '{node1fs node0fs}', 'createTime': '1424473547353', 'number': '5', 'availCapacityKiB': '1073031792', 'freeCapacityKiB': '1073031792', 'capacityKiB': '1073741824', 'fFree': '2216786133', 'filesUsed': '36', 'domains_name': 'd457e1ab-9d92-4355-a639-51cefbb93068', 'domains_owner': '1', 'domains_filesets': 'fileset1', 'domains_volumes': '200', 'domains_hosts': '{1 0}', 'domains_ipfsType': 'ADE', 'segments_number': '1', 'segments_unavailable': 'false', 'segments_readOnly': 'false', 'segments_ipfsType': 'ADE', 'segments_domain': 'd457e1ab-9d92-4355-a639-51cefbb93068', 'segments_fileset': 'fileset1', 'segments_availCapacityKiB': '1073031792', 'segments_freeCapacityKiB': '1073031792', 'segments_capacityKiB': '1073741824', 'segments_fFree': '2216786133', 'segments_files': '2216786169', 'volumes_name': '{}', 'volumes_lunUuid': '200', 'volumes_hosts': '{1 0}', 'volumes_capacityInMb': '1048576', 'mountStates': 'ACTIVATED', 'primaryNode': '1', 'alternateNode': '0', 'currentNode': '1', 'comment': '{}', 'overallStateInt': '1', 'compId': '10465140418516302068', 'usedCapacityKiB': '710032', 'freezeState': 'NOT_FROZEN', 'isolationState': 'ACCESSIBLE', } fpg_list = [] for fpg in filtered_fpgs: fpgname = fpg.fpgname fpg_dict['fpgname'] = fpgname fpg_dict['mountpath'] = ''.join(('/', fpgname)) fpg_dict['vvs'] = '.'.join((fpgname, '1')) fpg_dict['defaultCpg'] = fpg.cpgname fpg_dict['domains_fsname'] = fpgname fpg_dict['segments_fsname'] = fpgname fpg_tcl_format = ( '{' '%(fpgname)s ' '%(uuid)s ' '%(generation)s ' '%(mountpath)s ' '%(hosts)s ' '%(createTime)s ' '%(number)s ' '%(availCapacityKiB)s ' '%(freeCapacityKiB)s ' '%(capacityKiB)s ' '%(fFree)s ' '%(filesUsed)s ' '%(vvs)s ' '%(defaultCpg)s ' '{{' '%(domains_name)s ' '%(domains_owner)s ' '%(domains_fsname)s ' '%(domains_filesets)s ' '%(domains_volumes)s ' '%(domains_hosts)s ' '%(domains_ipfsType)s ' '}}' '{{' '%(segments_fsname)s ' '%(segments_number)s ' '%(segments_unavailable)s ' '%(segments_readOnly)s ' '%(segments_ipfsType)s ' '%(segments_domain)s ' '%(segments_fileset)s ' '%(segments_availCapacityKiB)s ' '%(segments_freeCapacityKiB)s ' '%(segments_capacityKiB)s ' '%(segments_fFree)s ' '%(segments_files)s ' '}}' '{{' '%(volumes_name)s ' '%(volumes_lunUuid)s ' '%(volumes_hosts)s ' '%(volumes_capacityInMb)s ' '}}' '%(mountStates)s ' '%(primaryNode)s ' '%(alternateNode)s ' '%(currentNode)s ' '%(comment)s ' '%(overallStateInt)s ' '%(compId)s ' '%(usedCapacityKiB)s ' '%(freezeState)s ' '%(isolationState)s ' '}' ) fpg_list.append(fpg_tcl_format % fpg_dict) return ' '.join(fpg_list) if fpg_list else ( 'No File Provisioning Groups found.') def do_cli_getvfs(self, *args): parser = CliArgumentParser(prog=args[0]) parser.add_argument( '-d', default=False, action="store_true", help="Detailed output.") parser.add_argument( '-fpg', help="Limit the display to VFSs contained within the " "File Provisioning Group.") parser.add_argument( '-vfs', help="Limit the display to the specified VFS name.") self.logger.log( logging.INFO, "getvfs with argparser args %s" % ','.join(args)) opts = parser.parse_args(*args) filtered_vfss = [] if opts.fpg or opts.vfs: for vfs in list(self.vfss.values()): if opts.vfs and vfs.vfsname != opts.vfs: continue if opts.fpg and vfs.fpg != opts.fpg: continue filtered_vfss.append(vfs) else: filtered_vfss = list(self.vfss.values()) # Place-holders for most values set outside of loop. # Param based on the create params (or generated) inside the loop. vfs_dict = { 'uuid': '49ec3e7e-d7af-4e73-9536-79065483164f', 'comment': '{}', 'overallStateInt': '1', 'compId': '10465140418516302068', } vfs_list = [] for vfs in filtered_vfss: vfsname = vfs.vfsname vfs_dict['vfsname'] = vfsname vfs_dict['fspname'] = vfs.fpg vfs_dict['vfsip'] = 'todo' vfs_dict['certs'] = 'todo' vfs_dict['bgrace'] = vfs.bgrace vfs_dict['igrace'] = vfs.igrace vfs_dict['uuid'] = 'todo' vfs_tcl_format = ( '{' '%(vfsname)s ' '%(fspname)s ' '%(vfsip)s ' '%(overallStateInt)s ' '%(comment)s ' '%(certs)s ' '%(bgrace)s ' '%(igrace)s ' '%(uuid)s ' '%(compId)s ' '}' ) vfs_list.append(vfs_tcl_format % vfs_dict) return ' '.join(vfs_list) if vfs_list else '' def do_cli_removevfs(self, *args): parser = CliArgumentParser(prog=args[0]) parser.add_argument( '-f', default=False, action="store_true", help="Specifies that the command is forced.") parser.add_argument( '-fpg', help="Name of the File Provisioning Group containing the VFS.") parser.add_argument( "vfs", help="The name of the VFS to be removed.") self.logger.log( logging.INFO, "removevfs with argparser args %s" % ','.join(args)) opts = parser.parse_args(*args) if opts.fpg and not opts.fpg in self.fpgs: return 'File Provisioning Group: %s not found\r' % opts.fpg for vfs in list(self.vfss.values()): if vfs.vfsname == opts.vfs and ( opts.fpg is None or vfs.fpg == opts.fpg): self.vfss.pop(vfs.vfsname) return "deleted VFS" else: return ('Virtual file server %s was not found in any existing ' 'file provisioning group.\r' % opts.vfs) def do_cli_getfsip(self, *args): parser = CliArgumentParser(prog=args[0]) parser.add_argument( '-fpg', help="Specifies the File Provisioning Group in which the " "Virtual File Server was created.") parser.add_argument( 'vfs', help="Specifies the Virtual File Server which is to have its " "network config modified.") self.logger.log( logging.INFO, "getfsip with argparser args %s" % ','.join(args)) opts = parser.parse_args(*args) if opts.fpg and not opts.fpg in self.fpgs: return 'File Provisioning Group: %s not found\r' % opts.fpg fsips = [] for vfs in list(self.vfss.values()): if opts.vfs and vfs.vfsname != opts.vfs: continue if opts.fpg and vfs.fpg != opts.fpg: continue fsips.append({ 'id': '012345679abcdef', 'vfs': vfs.vfsname, 'fpg': vfs.fpg, 'ipaddr': vfs.ipaddr, 'subnet': vfs.subnet, 'vlan': vfs.vlan or '0', 'type': 'user', }) if not fsips: return 'Invalid VFS %s\r' % opts.vfs fsip_list = [] for fsip in fsips: tcl_format = ( '{' '%(id)s ' '%(fpg)s ' '%(vfs)s ' '%(vlan)s ' '%(subnet)s ' '%(ipaddr)s ' '%(type)s' '}' ) fsip_list.append(tcl_format % fsip) if fsip_list: return '{%s}' % ' '.join(fsip_list) else: return 'No FSIPS found.' def do_cli_showpatch(self, *args): self.logger.log(logging.ERROR, "TEST SHOWPATCH") print(args) if len(args) > 1: self.logger.log(logging.ERROR, "TEST SHOWPATCH len %s" % len(args)) self.logger.log(logging.ERROR, "TEST SHOWPATCH 1) %s" % args[1]) if args[1] == '-hist': return "patch history not faked yet" elif args[1] == '-d': patch_id = args[2] return "Patch " + patch_id + " not recognized." return "showpatch needs more arg checking and implementing" def do_cli_showvv(self, *args): # NOTE(aorourke): Only the pattern matching (-p) for volumes whose # copyof column matches the volumes name (-copyof) is supported in # the mocked version of showvv. parser = CliArgumentParser(prog=args[0]) parser.add_argument( '-p', default=False, action="store_true", help="Pattern for matching VVs to show.") parser.add_argument( '-copyof', default=True, action="store_true", help="Show only VVs whose CopyOf column matches one more of the " "vvname_or_patterns.") parser.add_argument( "name", help="The name of the VV to show.") self.logger.log( logging.INFO, "showvv with argparser args %s" % ','.join(args)) opts = parser.parse_args(*args) if not opts.p or not opts.copyof or not opts.name: return "no vv listed" if "VOLUME1_UNIT_TEST" in opts.name: cli_out = """ ,,,,,,,,--Rsvd(MB)---,,,-(MB)-\r\n Id,Name,Prov,Type,CopyOf,BsId,Rd,-Detailed_State-,Adm,Snp,Usr,VSize\r\n 123,SNAP_UNIT_TEST1,snp,vcopy,myVol,123,RO,-,-,-,-,-\r\n 124,SNAP_UNIT_TEST2,vcopy,myVol,124,RO,-,-,-,-,-\r\n --------------------------------\r\n 2,total\r\n """ else: cli_out = "no vv listed" return cli_out def do_cli_setqos(self, *args): parser = CliArgumentParser(prog=args[0]) parser.add_argument( '-pri', default='normal', help='Set the QoS scheduling priority of the QoS rule (of this ' 'target object).The default priority is "normal".') parser.add_argument( '-io', help='Sets the I/O issue count Min goal and Max limit for QoS ' 'throttling. If only is given, sets both I/O issue ' 'count rate Min goal and Max limit to the given value. If "none" ' 'is specified, there is no limit on I/O issue count. Note even ' 'when there is no limit for I/O issue count, I/O-bandwidth-count ' 'based throttling (-bw) can still dynamically put a limit on it.') parser.add_argument( '-bw', help='Sets the I/O issue bandwidth rate Min goal and Max limit ' 'for QoS throttling. If only is given, sets both I/O ' 'issue bandwidth rate Min goal and Max limit to the given value. ' 'If "none" is specified, there is no limit on I/O issue bandwidth ' 'rate. Note even when there is no limit for I/O issue count, ' 'I/O-bandwidth-rate based throttling (-io) can still dynamically ' 'put a limit on it. The default unit is byte. The integer can ' 'optionally be followed with k or K to indicate a multiple of ' '1000, m or M to indicate a multiple of 1,000,000, or g or G to ' 'indicate a multiple of 1,000,000,000.') parser.add_argument( "name", help="The target object name of QoS setting.") self.logger.log( logging.INFO, "setqos with argparser args %s" % ','.join(args)) opts = parser.parse_args(*args) if ":" not in opts.name: return "Invalid QoS rule patterns" if not (opts.io or opts.bw): return ''' setqos: At least one option must be specified SYNTAX setqos [options] [{{vvset|domain}:{|}|sys:all_others}]... ''' if "VSET_" in opts.name: return '' else: return "no matching QoS target found" def process_command(self, cmd): self.logger.log(logging.INFO, cmd) if cmd is None: print("returnNone") return '' args = shlex.split(cmd) if args: method = getattr(self, 'do_cli_' + args[0], self.do_cli_other) try: return method(*args) except Exception as cmd_exception: return str(cmd_exception) else: return '' class ParamikoServer(paramiko.ServerInterface): def __init__(self): self.event = threading.Event() def check_channel_request(self, kind, chanid): return paramiko.OPEN_SUCCEEDED def check_auth_none(self, username): return paramiko.AUTH_SUCCESSFUL def check_auth_password(self, username, password): # if (username == '3paradm') and (password == '3pardata'): # return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_SUCCESSFUL def check_auth_publickey(self, username, key): return paramiko.AUTH_SUCCESSFUL def get_allowed_auths(self, username): return 'password,publickey,none' def check_channel_shell_request(self, c): self.event.set() return True def check_channel_pty_request(self, c, term, width, height, pixelwidth, pixelheight, modes): return True if __name__ == "__main__": if len(sys.argv) > 1: port = int(sys.argv[1]) else: port = 2200 key_file = os.path.expanduser('~/.ssh/id_rsa') host_key = paramiko.RSAKey(filename=key_file) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(('', int(port))) s.listen(60) print("Listening for SSH client connections...") connection, address = s.accept() transport = None channel = None try: transport = paramiko.Transport(connection) transport.load_server_moduli() transport.add_server_key(host_key) server = ParamikoServer() transport.start_server(server=server) cliProcessor = Cli() while True: channel = transport.accept(60) if channel is None: print("Failed to get SSH channel.") sys.exit(1) print("Connected") server.event.wait(10) if not server.event.isSet(): print("No shell set") sys.exit(1) fio = channel.makefile('rU') commands = [] command = None while not (command == 'exit' or command == 'quit'): command = fio.readline().strip('\r\n') commands.append(command) to_send = '\r\n'.join(commands) channel.send(to_send) output = [''] prompt = "FAKE-3PAR-CLI cli% " for cmd in commands: output.append('%s%s' % (prompt, cmd)) result = cliProcessor.process_command(cmd) if result is not None: output.append(result) output_to_send = '\r\n'.join(output) channel.send(output_to_send) channel.close() print("Disconnected") finally: if channel: channel.close() if transport: try: transport.close() print("transport closed") except Exception as e: print("transport close exception %s" % e) pass python-3parclient-4.2.12/test/__init__.py0000644000000000000000000000000014106434774020211 0ustar rootroot00000000000000python-3parclient-4.2.12/test/test_HPE3ParClient_CPG.py0000644000000000000000000001453114106434774022521 0ustar rootroot00000000000000# (c) Copyright 2015 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Test class of 3PAR Client handling CPG.""" from test import HPE3ParClient_base as hpe3parbase from hpe3parclient import exceptions DOMAIN = 'UNIT_TEST_DOMAIN' CPG_NAME1 = 'CPG1_UNIT_TEST' + hpe3parbase.TIME CPG_NAME2 = 'CPG2_UNIT_TEST' + hpe3parbase.TIME class HPE3ParClientCPGTestCase(hpe3parbase.HPE3ParClientBaseTestCase): def setUp(self): super(HPE3ParClientCPGTestCase, self).setUp() def tearDown(self): try: self.cl.deleteCPG(CPG_NAME1) except Exception: pass try: self.cl.deleteCPG(CPG_NAME2) except Exception: pass # very last, tear down base class super(HPE3ParClientCPGTestCase, self).tearDown() def test_1_create_CPG(self): self.printHeader('create_CPG') # add one optional = self.CPG_OPTIONS name = CPG_NAME1 self.cl.createCPG(name, optional) # check cpg1 = self.cl.getCPG(name) self.assertIsNotNone(cpg1) cpgName = cpg1['name'] self.assertEqual(name, cpgName) # add another name = CPG_NAME2 optional2 = optional.copy() if self.CPG_LDLAYOUT_HA is None: more_optional = {'LDLayout': {'RAIDType': 2}} else: if self.DISK_TYPE is None: more_optional = {'LDLayout': {'RAIDType': 2, 'HA': self.CPG_LDLAYOUT_HA}} else: more_optional = {'LDLayout': {'RAIDType': 2, 'HA': self.CPG_LDLAYOUT_HA, 'diskPatterns': [{'diskType': self.DISK_TYPE}]}} optional2.update(more_optional) self.cl.createCPG(name, optional2) # check cpg2 = self.cl.getCPG(name) self.assertIsNotNone(cpg2) cpgName = cpg2['name'] self.assertEqual(name, cpgName) self.printFooter('create_CPG') def test_1_create_CPG_badDomain(self): self.printHeader('create_CPG_badDomain') if self.DISK_TYPE is None: optional = {'domain': 'BAD_DOMAIN'} else: optional = {'LDLayout': {'diskPatterns': [{ 'diskType': self.DISK_TYPE}]}, 'domain': 'BAD_DOMAIN'} self.assertRaises(exceptions.HTTPNotFound, self.cl.createCPG, CPG_NAME1, optional) self.printFooter('create_CPG_badDomain') def test_1_create_CPG_dup(self): self.printHeader('create_CPG_dup') optional = self.CPG_OPTIONS name = CPG_NAME1 self.cl.createCPG(name, optional) self.assertRaises(exceptions.HTTPConflict, self.cl.createCPG, CPG_NAME1, optional) self.printFooter('create_CPG_dup') def test_1_create_CPG_badParams(self): self.printHeader('create_CPG_badParams') optional = {'domainBad': 'UNIT_TEST'} self.assertRaises(exceptions.HTTPBadRequest, self.cl.createCPG, CPG_NAME1, optional) self.printFooter('create_CPG_badParams') def test_1_create_CPG_badParams2(self): self.printHeader('create_CPG_badParams2') optional = {'domain': 'UNIT_TEST'} more_optional = {'LDLayout': {'RAIDBadType': 1}} optional.update(more_optional) self.assertRaises(exceptions.HTTPBadRequest, self.cl.createCPG, CPG_NAME1, optional) self.printFooter('create_CPG_badParams2') def test_2_get_CPG_bad(self): self.printHeader('get_CPG_bad') self.assertRaises(exceptions.HTTPNotFound, self.cl.getCPG, 'BadName') self.printFooter('get_CPG_bad') def test_2_get_CPGs(self): self.printHeader('get_CPGs') optional = self.CPG_OPTIONS name = CPG_NAME1 self.cl.createCPG(name, optional) cpgs = self.cl.getCPGs() self.assertGreater(len(cpgs), 0, 'getCPGs failed with no CPGs') self.assertTrue(self.findInDict(cpgs['members'], 'name', CPG_NAME1)) self.printFooter('get_CPGs') def test_3_delete_CPG_nonExist(self): self.printHeader('delete_CPG_nonExist') self.assertRaises(exceptions.HTTPNotFound, self.cl.deleteCPG, 'NonExistCPG') self.printFooter('delete_CPG_nonExist') def test_3_delete_CPGs(self): self.printHeader('delete_CPGs') # add one optional = self.CPG_OPTIONS self.cl.createCPG(CPG_NAME1, optional) cpg = self.cl.getCPG(CPG_NAME1) self.assertTrue(cpg['name'], CPG_NAME1) cpgs = self.cl.getCPGs() if cpgs and cpgs['total'] > 0: for cpg in cpgs['members']: if cpg['name'] == CPG_NAME1: # pprint.pprint("Deleting CPG %s " % cpg['name']) self.cl.deleteCPG(cpg['name']) # check self.assertRaises(exceptions.HTTPNotFound, self.cl.getCPG, CPG_NAME1) self.printFooter('delete_CPGs') def test_4_get_cpg_available_space(self): self.printHeader('get_cpg_available_space') optional = self.CPG_OPTIONS name = CPG_NAME1 self.cl.createCPG(name, optional) cpg1 = self.cl.getCPGAvailableSpace(name) self.assertIsNotNone(cpg1) self.printFooter('get_cpg_available_space') def test_4_get_cpg_available_space_bad_cpg(self): self.printHeader('get_cpg_available_space_bad_cpg') self.assertRaises( exceptions.HTTPNotFound, self.cl.getCPGAvailableSpace, 'BadName') self.printFooter('get_cpg_available_space_bad_cpg') # testing # suite = unittest.TestLoader().loadTestsFromTestCase(HPE3ParClientCPGTestCase) # unittest.TextTestRunner(verbosity=2).run(suite) python-3parclient-4.2.12/test/test_HPE3ParClient_Exception.py0000644000000000000000000000432714106434774024050 0ustar rootroot00000000000000# (c) Copyright 2015 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Test class of 3PAR Client handling exceptions.""" from test import HPE3ParClient_base as hpe3parbase from hpe3parclient import exceptions class HPE3ParClientExceptionTestCase(hpe3parbase.HPE3ParClientBaseTestCase): def setUp(self): super(HPE3ParClientExceptionTestCase, self).setUp() def tearDown(self): super(HPE3ParClientExceptionTestCase, self).tearDown() def test_from_response_string_format(self): self.printHeader('from_response') # Fake response representing an internal server error. class FakeResponse(object): status = 500 fake_response = FakeResponse() output = exceptions.from_response(fake_response, {}).__str__() self.assertEqual('Internal Server Error (HTTP 500)', output) self.printFooter('from_response') def test_client_exception_string_format(self): self.printHeader('client_exception') fake_error = {'code': 999, 'desc': 'Fake Description', 'ref': 'Fake Ref', 'debug1': 'Fake Debug 1', 'debug2': 'Fake Debug 2', } # Create a fake exception and check that the output is # converted properly. client_ex = exceptions.ClientException(error=fake_error) client_ex.message = "Fake Error" client_ex.http_status = 500 output = client_ex.__str__() self.assertEqual("Fake Error (HTTP 500) 999 - Fake Description - " "Fake Ref (1: 'Fake Debug 1') (2: 'Fake Debug 2')", output) self.printFooter('client_exception') python-3parclient-4.2.12/test/test_HPE3ParClient_FilePersona.py0000644000000000000000000012170614106434774024322 0ustar rootroot00000000000000# (c) Copyright 2015 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Test class of 3Par Client handling File Persona API.""" import pprint import re import time import unittest from pytest_testconfig import config from functools import wraps from test import HPE3ParClient_base as hpe3parbase cpgs_to_delete = [] fpgs_to_delete = [] vfss_to_delete = [] fstores_to_delete = [] fshares_to_delete = [] SKIP_MSG = "Skipping test because skip_file_persona=true in config." def is_live_test(): return config['TEST']['unit'].lower() == 'false' def skip_file_persona(): return config['TEST']['skip_file_persona'].lower() == 'true' class HPE3ParFilePersonaClientTestCase(hpe3parbase.HPE3ParClientBaseTestCase): interfaces = None DEBUG = config['TEST']['debug'].lower() == 'true' def debug_print(self, obj, **kwargs): if self.DEBUG: print(pprint.pformat(obj, **kwargs)) def setUp(self, withSSH=True, withFilePersona=True): self.withSSH = withSSH self.withFilePersona = withFilePersona super(HPE3ParFilePersonaClientTestCase, self).setUp( withSSH=self.withSSH, withFilePersona=self.withFilePersona) # Only get the tpdinterface once and reuse it for all the tests. if self.interfaces is None: self.interfaces = self.cl.gettpdinterface() save_interface = open('test/tpdinterface/interface.save', 'w') for k, v in list(self.interfaces.items()): save_interface.write(' {%s {' % k) for header in v: if isinstance(header, str): save_interface.write(' {%s 0}' % header) else: h, sub = header save_interface.write(' {%s 0}' % h) for s in sub: save_interface.write(' {%s,%s 0}' % (h, s)) save_interface.write('}}') save_interface.close() else: self.cl.interfaces = HPE3ParFilePersonaClientTestCase.interfaces def tearDown(self): """Clean-up -- without fail -- more than humanly possible.""" # Start by removing and cleaning fsnaps so other things can be deleted. for fpgname, vfsname, fstore in fstores_to_delete: try: self.cl.removefsnap(vfsname, fstore, fpg=fpgname) self.cl.startfsnapclean(fpgname, reclaimStrategy='maxspeed') # TODO: get smart about cleaning snapshots time.sleep(5) except Exception as e: print(e) pass for fpgname, vfsname, fstore, share, protocol in fshares_to_delete: try: self.cl.removefshare(protocol, vfsname, share, fstore=fstore, fpg=fpgname) except Exception as e: print(e) pass del fshares_to_delete[:] for fpgname, vfsname, fstore in fstores_to_delete: try: self.cl.removefstore(vfsname, fstore, fpg=fpgname) except Exception as e: print(e) pass del fstores_to_delete[:] for fpgname, vfsname in vfss_to_delete: try: self.cl.removevfs(vfsname, fpg=fpgname) except Exception as e: print(e) pass del vfss_to_delete[:] for fpgname in fpgs_to_delete: try: self.cl.removefpg(fpgname, wait=True) except Exception as e: print(e) pass del fpgs_to_delete[:] for cpgname in cpgs_to_delete: try: self.cl.deleteCPG(cpgname) except Exception as e: print(e) pass del cpgs_to_delete[:] # very last, tear down base class super(HPE3ParFilePersonaClientTestCase, self).tearDown() def print_header_and_footer(func): """Print header and footer for unit tests.""" @wraps(func) def wrapper(*args, **kwargs): test = args[0] test.printHeader(unittest.TestCase.id(test)) start_time = time.time() result = func(*args, **kwargs) elapsed_time = time.time() - start_time print("Elapsed Time: %ss" % elapsed_time) test.printFooter(unittest.TestCase.id(test)) return result return wrapper def find_expected_in_result(self, expected, result): for line in result: if re.search(expected, line): break else: print("Did NOT find expected error. Expected: %s" % expected) print(pprint.pformat(result)) self.fail("Did NOT find expected error. Expected: %s" % expected) def find_expected_key_val_in_members(self, expected, members): for member in members: if expected in list(member.items()): break else: self.fail("Did NOT find expected error. Expected: %s" % expected) def get_fpg_count(self): return self.cl.getfpg()['total'] def get_vfs_count(self, fpg=None): return self.cl.getvfs(fpg=fpg)['total'] def validate_getfs_members(self, members): for member in members: self.assertEqual([], member['ad']) self.assertIsInstance(member['auth'], dict) self.assertIsInstance(member['auth']['order'], list) for auth in member['auth']['order']: self.assertIn(auth, ['Ldap', 'ActiveDirectory', 'Local']) self.assertIsInstance(member['dns'], dict) self.assertIsInstance(member['dns']['addresses'], str) self.assertIsInstance(member['dns']['suffixes'], str) self.assertIsInstance(member['gwaddress'], dict) self.assertIsInstance(member['gwaddress']['address'], str) self.assertIsInstance(member['gwaddress']['nsCuid'], str) self.assertIsInstance(member['httpobj'], list) for httpobj in member['httpobj']: self.assertIn(httpobj['keepAlive'], ('true', 'false')) self.assertIsInstance(int(httpobj['keepAliveTimeout']), int) self.assertIsInstance(int(httpobj['maxClients']), int) self.assertIsInstance(int(httpobj['ports']), int) self.assertIsInstance(httpobj['profileName'], str) self.assertIsInstance(int(httpobj['rBlockSize']), int) self.assertIsInstance(int(httpobj['sslPorts']), int) self.assertIsInstance(int(httpobj['wBlockSize']), int) self.assertEqual([], member['ldap']) self.assertEqual(4, len(member['node'])) for node in member['node']: self.assertIn(node['activeNode'], ('Yes', 'No')) self.assertIn(node['bondMode'], ('1', '6', '-')) self.assertIn(node['fsvcList'], ('Yes', 'No')) self.assertIn(node['fsvcState'], ('running', 'Unknown')) self.assertIn(node['inCluster'], ('Yes', 'No')) self.assertIsInstance(node['mtu'], str) self.assertIn(node['nodeId'], ('0', '1', '2', '3')) self.assertIsInstance(node['nodeName'], str) self.assertIn(node['nspList'], ('0:2:1,0:2:2', '1:2:1,1:2:2', '-')) self.assertIsInstance(node['version'], str) for node_ip in member['nodeIp']: self.assertIsInstance(node_ip['address'], str) self.assertIsInstance(int(node_ip['nodeId']), int) self.assertIsInstance(node_ip['nsCuid'], str) self.assertIsInstance(node_ip['subnet'], str) self.assertIsInstance(node_ip['vlantag'], str) def validate_getfpg_members(self, members): for member in members: self.assertIsNotNone(member['CompId']) self.assertIsInstance(int(member['alternateNode']), int) self.assertIsInstance(int(member['availCapacityKiB']), int) self.assertIsInstance(int(member['capacityKiB']), int) self.assertIsInstance(member['comment'], str) self.assertIsInstance(int(member['createTime']), int) self.assertIsInstance(int(member['currentNode']), int) self.assertIn('defaultCpg', member) self.assertIsInstance(member['domains'], list) for domain in member['domains']: self.assertIsInstance(domain['filesets'], str) self.assertIsInstance(domain['fsname'], str) # Domain hosts typically look like ['node1fs', 'node0fs'] self.assertIsInstance(domain['hosts'], list) for host in domain['hosts']: self.assertIsInstance(host, str) self.assertIsInstance(domain['ipfsType'], str) self.assertIsInstance(domain['name'], str) self.assertIsInstance(int(domain['owner']), int) # volumes is ID or list of IDs if isinstance(domain['volumes'], list): for vol_id in domain['volumes']: self.assertIsInstance(int(vol_id), int) else: self.assertIsInstance(int(domain['volumes']), int) self.assertIsInstance(int(member['fFree']), int) self.assertIsInstance(int(member['filesUsed']), int) self.assertIsInstance(int(member['freeCapacityKiB']), int) self.assertIn(member['freezeState'], ['NOT_FROZEN', 'UNKNOWN']) fpgname = member['fsname'] self.assertIsInstance(fpgname, str) self.assertIsInstance(int(member['generation']), int) self.assertIsInstance(member['hosts'], list) self.assertIn(member['isolationState'], ['ACCESSIBLE', 'UNKNOWN']) self.assertIn(member['mountStates'], ['ACTIVATED', 'DEACTIVATED', 'MOUNTING', 'UNMOUNTING', ]) self.assertIsInstance(member['mountpath'], str) self.assertTrue(member['mountpath'].startswith('/')) self.assertIsInstance(int(member['number']), int) self.assertIsInstance(int(member['overallStateInt']), int) self.assertIsInstance(int(member['primaryNode']), int) self.assertIsInstance(member['segments'], list) for segment in member['segments']: self.assertIsInstance(int(segment['availCapacityKiB']), int) self.assertIsInstance(int(segment['capacityKiB']), int) self.assertIsInstance(segment['domain'], str) self.assertIsInstance(int(segment['fFree']), int) self.assertIsInstance(int(segment['files']), int) self.assertIsInstance(segment['fileset'], str) self.assertIsInstance(int(segment['freeCapacityKiB']), int) self.assertIsInstance(segment['fsname'], str) self.assertIsInstance(segment['ipfsType'], str) self.assertIsInstance(int(segment['number']), int) self.assertIn(segment['readOnly'], ['true', 'false']) self.assertIn(segment['unavailable'], ['true', 'false']) self.assertIsInstance(int(member['usedCapacityKiB']), int) self.assertIsInstance(member['uuid'], str) self.assertIsInstance(member['volumes'], list) for volume in member['volumes']: self.assertIsInstance(int(volume['capacityInMb']), int) # Volume hosts should be something like ['0', '1'] self.assertIsInstance(volume['hosts'], list) for host in volume['hosts']: self.assertIsInstance(int(host), int) self.assertIsInstance(int(volume['lunUuid']), int) self.assertFalse(volume['name']) # Name is always empty def validate_vv_name(fpg_name, vv_name): """Expect vv name to look like 'fpg_name.#'.""" self.assertIsInstance(vv_name, str) vv_split = vv_name.split('.') self.assertEqual(fpg_name, vv_split[0]) self.assertIsInstance(int(vv_split[1]), int) if isinstance(member['vvs'], list): for vv in member['vvs']: validate_vv_name(fpgname, vv) else: vv = member['vvs'] self.assertIsInstance(vv, str) validate_vv_name(fpgname, vv) def validate_getvfs_members(self, members): for member in members: self.assertIsInstance(member['CompId'], str) self.assertIsInstance(int(member['bgrace']), int) self.assertIsInstance(member['certs'], str) self.assertIsInstance(member['comment'], str) self.assertIsInstance(member['fspname'], str) self.assertIsInstance(int(member['igrace']), int) self.assertIsInstance(int(member['overallStateInt']), int) self.assertIsInstance(member['uuid'], str) self.assertIsInstance(member['vfsip'], str) self.assertIsInstance(member['vfsname'], str) def validate_fs(self, expected_count=None): result = self.cl.getfs() self.debug_print("DEBUG: getfs result...") self.debug_print(result) total = result['total'] message = result['message'] members = result['members'] # Validate contents if total == 0: self.assertEqual([], members) else: self.assertIsNone(message) self.validate_getfs_members(members) # Compare against expected count if expected_count is not None: self.assertEqual(expected_count, total) def validate_fpg(self, fpgname=None, no_fpgname=None, expected_count=None): result = self.cl.getfpg() self.debug_print("DEBUG: getfpg result...") self.debug_print(result) total = result['total'] message = result['message'] members = result['members'] # Validate contents if total == 0: self.assertEqual([], members) self.assertIn(message, ('No File Provisioning Groups found.', None)) else: self.assertIsNone(message) self.validate_getfpg_members(members) # Compare against expected count if expected_count is not None: self.assertEqual(expected_count, total) # Look for expected if fpgname: for member in result['members']: if member['fsname'] == fpgname: break else: self.fail('Did NOT find expected FPG %s' % fpgname) # Look for expected _not_existing_ if no_fpgname: for member in result['members']: if member['fsname'] == no_fpgname: self.fail('Found unexpected FPG %s.' % fpgname) def validate_vfs(self, fpgname=None, vfsname=None, no_vfsname=None, expected_count=None): result = self.cl.getvfs(fpg=fpgname) self.debug_print("DEBUG: getvfs result...") self.debug_print(result) total = result['total'] message = result['message'] # Validate contents if fpgname is not None: success_message = None not_found_message = 'Invalid VFS %s\r' % vfsname self.assertIn(message, (success_message, not_found_message)) elif total == 0: self.assertEqual('', message) else: self.assertIsNone(message) # Compare against expected count if expected_count is not None: self.assertEqual(expected_count, total) # Look for expected if vfsname: for member in result['members']: if member['vfsname'] == vfsname and ( fpgname is None or member['fspname'] == fpgname): break else: self.fail('Did NOT find expected VFS %s' % vfsname) # Look for expected _not_existing_ if no_vfsname: for member in result['members']: if member['vfsname'] == no_vfsname and ( fpgname is None or member['fspname'] == fpgname): self.fail('Found unexpected VFS %s.' % no_vfsname) @unittest.skipIf(is_live_test(), "Skip on real array which may have exiting VFSs.") @print_header_and_footer def test_getvfs_empty(self): self.validate_vfs(expected_count=0) @unittest.skipIf(is_live_test() and skip_file_persona(), SKIP_MSG) @print_header_and_footer def test_getfs(self): self.validate_fs(expected_count=1) @unittest.skipIf(is_live_test(), "Skip on real array which may have exiting VFSs.") @print_header_and_footer def test_getfpg_empty(self): self.validate_fpg(expected_count=0) @unittest.skipIf(is_live_test() and skip_file_persona(), SKIP_MSG) @print_header_and_footer def test_getfpg_bogus(self): result = self.cl.getfpg('bogus1', 'bogus2', 'bogus3') expected_message = 'File Provisioning Group: bogus1 not found\r' self.assertEqual(expected_message, result['message']) self.assertEqual(0, result['total']) self.assertEqual([], result['members']) @unittest.skipIf(is_live_test() and skip_file_persona(), SKIP_MSG) @print_header_and_footer def test_createfpg_bogus_cpg(self): fpg_count = self.get_fpg_count() test_prefix = 'UT1_' fpgname = test_prefix + "FPG_" + hpe3parbase.TIME fpgs_to_delete.append(fpgname) bogus_cpgname = 'thiscpgdoesnotexist' result = self.cl.createfpg(bogus_cpgname, fpgname, '1X') self.assertEqual( 'Error: Invalid CPG name: %s\r' % bogus_cpgname, result[0]) self.validate_fpg(expected_count=fpg_count) @unittest.skipIf(is_live_test() and skip_file_persona(), SKIP_MSG) @print_header_and_footer def test_createfpg_bad_size(self): test_prefix = 'UT2_' fpg_count = self.get_fpg_count() fpgname = test_prefix + "FPG_" + hpe3parbase.TIME fpgs_to_delete.append(fpgname) # Create a CPG for the test cpgname = test_prefix + "CPG_" + hpe3parbase.TIME cpgs_to_delete.append(cpgname) optional = self.CPG_OPTIONS.copy() optional.pop('domain', None) # File Persona doesn't allow a domain self.cl.createCPG(cpgname, optional) result = self.cl.createfpg(cpgname, fpgname, '1X') self.assertEqual( 'The suffix, X, for size is invalid.\r', result[0]) self.validate_fpg(expected_count=fpg_count) @unittest.skipIf(is_live_test() and skip_file_persona(), SKIP_MSG) @print_header_and_footer def test_createfpg_in_domain_err(self): fpg_count = self.get_fpg_count() test_prefix = 'UT3_' fpgname = test_prefix + "FPG_" + hpe3parbase.TIME fpgs_to_delete.append(fpgname) # Create a CPG for the test cpgname = test_prefix + "CPG_" + hpe3parbase.TIME cpgs_to_delete.append(cpgname) optional = self.CPG_OPTIONS self.cl.createCPG(cpgname, optional) result = self.cl.createfpg(cpgname, fpgname, '1T', wait=True) expected = 'belongs to domain.*which cannot be used for File Services.' self.find_expected_in_result(expected, result) self.validate_fpg(expected_count=fpg_count) @unittest.skipIf(is_live_test() and skip_file_persona(), SKIP_MSG) @print_header_and_footer def test_createfpg_twice_and_remove(self): fpg_count = self.get_fpg_count() test_prefix = 'UT4_' fpgname = test_prefix + "FPG_" + hpe3parbase.TIME fpgs_to_delete.append(fpgname) # Create a CPG for the test cpgname = test_prefix + "CPG_" + hpe3parbase.TIME cpgs_to_delete.append(cpgname) optional = self.CPG_OPTIONS.copy() optional.pop('domain', None) # File Persona doesn't allow domain self.cl.createCPG(cpgname, optional) # Create FPG once to test createfpg result = self.cl.createfpg(cpgname, fpgname, '1T', wait=True) expected = 'File Provisioning Group *%s created.' % fpgname self.find_expected_in_result(expected, result) expected = 'File Provisioning Group *%s activated.' % fpgname self.find_expected_in_result(expected, result) self.validate_fpg(fpgname=fpgname, expected_count=fpg_count + 1) # Create same FPG again to test createfpg already exists error result = self.cl.createfpg(cpgname, fpgname, '1T', wait=True) expected = ('Error: FPG %s already exists\r' % fpgname) self.assertEqual(expected, result[0]) self.validate_fpg(fpgname=fpgname, expected_count=fpg_count + 1) # Test removefpg self.cl.removefpg(fpgname, wait=True) self.validate_fpg(no_fpgname=fpgname, expected_count=fpg_count) def get_or_create_fpg(self, test_prefix): fpgname = config['TEST'].get('fpg') if fpgname is not None: return fpgname fpgname = test_prefix + "FPG_" + hpe3parbase.TIME fpgs_to_delete.append(fpgname) # Create a CPG for the test cpgname = test_prefix + "CPG_" + hpe3parbase.TIME cpgs_to_delete.append(cpgname) optional = self.CPG_OPTIONS.copy() optional.pop('domain', None) # File Persona doesn't allow a domain self.cl.createCPG(cpgname, optional) # Create FPG result = self.cl.createfpg(cpgname, fpgname, '1T', wait=True) expected = 'File Provisioning Group *%s created.' % fpgname self.find_expected_in_result(expected, result) expected = 'File Provisioning Group *%s activated.' % fpgname self.find_expected_in_result(expected, result) self.validate_fpg(fpgname=fpgname) return fpgname def get_or_create_vfs(self, test_prefix, fpgname): vfsname = config['TEST'].get('vfs') if vfsname is not None: return vfsname vfsname = test_prefix + "VFS_" + hpe3parbase.TIME bgrace = '11' igrace = '22' comment = 'this is a test comment' vfss_to_delete.append((fpgname, vfsname)) result = self.cl.createvfs('127.0.0.2', '255.255.0.0', vfsname, fpg=fpgname, bgrace=bgrace, igrace=igrace, comment=comment, wait=True) expected = 'Created VFS "%s" on FPG %s.' % (vfsname, fpgname) self.find_expected_in_result(expected, result) self.validate_vfs(fpgname=fpgname, vfsname=vfsname) return vfsname @unittest.skipIf(is_live_test() and skip_file_persona(), SKIP_MSG) @print_header_and_footer def test_createvfs_bogus_bgrace(self): test_prefix = 'UT6_' fpgname = self.get_or_create_fpg(test_prefix) vfsname = self.get_or_create_vfs(test_prefix, fpgname) result = self.cl.createvfs('127.0.0.2', '255.255.255.0', vfsname, fpg=fpgname, bgrace='bogus', igrace='60', wait=True) self.assertEqual('bgrace value should be between 1 and 2147483647\r', result[0]) @unittest.skipIf(is_live_test() and skip_file_persona(), SKIP_MSG) @print_header_and_footer def test_createvfs_bogus_igrace(self): test_prefix = 'UT6_' fpgname = self.get_or_create_fpg(test_prefix) vfsname = self.get_or_create_vfs(test_prefix, fpgname) result = self.cl.createvfs('127.0.0.2', '255.255.255.0', vfsname, fpg=fpgname, bgrace='60', igrace='bogus', wait=True) self.assertEqual('igrace value should be between 1 and 2147483647\r', result[0]) def get_fsips(self, fpgname, vfsname): """Test FSIPS after VFS is created.""" result = self.cl.getfsip(vfsname, fpg=fpgname) self.debug_print(result) self.assertEqual(None, result['message']) self.assertEqual(1, result['total']) member = result['members'][0] self.assertEqual(fpgname, member['fspool']) self.assertEqual(vfsname, member['vfs']) self.assertEqual('user', member['networkName']) self.assertEqual('0', member['vlanTag']) uid_match = '^[0-f]*$' self.assertIsNotNone(re.match(uid_match, member['policyID'])) ip_addr_ish = r'[0-9.]*$' self.assertIsNotNone(re.match(ip_addr_ish, member['address']), '%s does not look like an IP Addr.' % member['address']) self.assertIsNotNone(re.match(ip_addr_ish, member['prefixLen']), '%s does not look like an IP Addr.' % member['prefixLen']) result = self.cl.getfsip(vfsname, fpg='bogus') self.debug_print(result) expected = { 'message': 'File Provisioning Group: bogus not found\r', 'total': 0, 'members': [] } self.assertEqual(expected, result) result = self.cl.getfsip('bogus', fpg=fpgname) self.debug_print(result) expected = { 'message': 'Invalid VFS bogus\r', 'total': 0, 'members': [] } self.assertEqual(expected, result) def validate_fstores(self, result): self.assertIsNone(result['message']) self.assertEqual(len(result['members']), result['total']) for member in result['members']: self.assertTrue(member['CompId'].isdigit()) self.assertIsInstance(member['comment'], str) self.assertIsInstance(member['fspoolName'], str) self.assertIsInstance(member['fstoreName'], str) self.assertTrue(member['overallStateInt'].isdigit()) uid_match = '^[\-0-f]*$' self.assertIsNotNone(re.match(uid_match, member['uuid'])) self.assertIsInstance(member['vfsName'], str) def validate_fstore(self, result, fpgname, vfsname, fstore, comment): self.assertIsNone(result['message']) self.assertEqual(1, result['total']) member = result['members'][0] self.assertTrue(member['CompId'].isdigit()) self.assertEqual(comment, member['comment']) self.assertEqual(fpgname, member['fspoolName']) self.assertEqual(fstore, member['fstoreName']) self.assertTrue(member['overallStateInt'].isdigit()) uid_match = '^[\-0-f]*$' self.assertIsNotNone(re.match(uid_match, member['uuid'])) self.assertEqual(vfsname, member['vfsName']) def crud_fstore(self, fpgname, vfsname, fstore): fstores_to_delete.append((fpgname, vfsname, fstore)) comment = "This is the CRUD test fstore." result = self.cl.createfstore(vfsname, fstore, fpg=fpgname, comment=comment) self.assertEqual([], result) result = self.cl.getfstore(fpg=fpgname, vfs=vfsname, fstore=fstore) self.validate_fstore(result, fpgname, vfsname, fstore, comment) new_comment = "new comment" result = self.cl.setfstore(vfsname, fstore, fpg=fpgname, comment=new_comment) self.assertEqual([], result) result = self.cl.getfstore(fpg=fpgname, vfs=vfsname, fstore=fstore) self.validate_fstore(result, fpgname, vfsname, fstore, new_comment) result = self.cl.getfstore() self.validate_fstores(result) pre_remove_total = result['total'] result = self.cl.removefstore(vfsname, fstore, fpg=fpgname) self.assertEqual(['%s removed' % fstore], result) result = self.cl.getfstore(fpg=fpgname, vfs=vfsname, fstore=fstore) self.assertGreater(pre_remove_total, result['total']) def create_share(self, protocol, fpgname, vfsname, share_name, comment): fstores_to_delete.append((fpgname, vfsname, share_name)) fshares_to_delete.append((fpgname, vfsname, share_name, share_name, protocol)) result = self.cl.createfshare(protocol, vfsname, share_name, fpg=fpgname, fstore=share_name, comment=comment) self.assertEqual([], result) def create_fsnap(self, fpgname, vfsname, fstore, tag): # Test error messages with bogus names result = self.cl.createfsnap('bogus', fstore, tag, fpg=fpgname) self.assertEqual(['Virtual Server bogus does not exist on FPG %s\r' % fpgname], result) result = self.cl.createfsnap(vfsname, 'bogus', tag, fpg=fpgname) self.assertEqual(['File Store bogus does not exist on FPG %s\r' % fpgname], result) result = self.cl.createfsnap(vfsname, fstore, tag, fpg='bogus') self.assertEqual(['FPG bogus not found\r'], result) result = self.cl.getfsnap('bogus', fpg=fpgname, vfs=vfsname, fstore=fstore, pat=True) self.assertEqual({'members': [], 'message': None, 'total': 0}, result) result = self.cl.getfsnap('bogus', fpg=fpgname, vfs=vfsname, fstore=fstore) expected = { 'members': [], 'message': 'SnapShot bogus does not exist on FPG %s path ' '%s/%s\r' % (fpgname, vfsname, fstore), 'total': 0} self.assertEqual(expected, result) result = self.cl.createfsnap(vfsname, fstore, tag, fpg=fpgname, retain=0) self.assertTrue(result[0].endswith('_%s' % tag)) result = self.cl.getfsnap('*%s' % tag, fpg=fpgname, vfs=vfsname, fstore=fstore, pat=True) member = result['members'][0] self.assertTrue(member['CompId'].isdigit()) self.assertTrue(member['createTime'].isdigit()) self.assertEqual(fpgname, member['fspName']) self.assertEqual(fstore, member['fstoreName']) snapname = member['snapName'] self.assertTrue(snapname.endswith('_%s' % tag)) self.assertEqual(vfsname, member['vfsName']) self.assertEqual(1, result['total']) self.assertIsNone(result['message']) # Test get by name instead of pattern result2 = self.cl.getfsnap(snapname, fpg=fpgname, vfs=vfsname, fstore=fstore) # For some reason, when -pat is not used the result does not include # a CompId. Otherwise the results should be identical (same fsnap). self.debug_print(result) self.debug_print(result2) del member['CompId'] # For some reason this is not in result2. self.assertEqual(result, result2) # Should be same result result = self.cl.createfsnap(vfsname, fstore, tag, fpg=fpgname) self.assertTrue(result[0].endswith('_%s' % tag)) result = self.cl.removefsnap(vfsname, fstore, fpg=fpgname, snapname=snapname) self.assertEqual([], result) result = self.cl.removefsnap(vfsname, fstore, fpg=fpgname) self.assertEqual([], result) success = [] running = ['Reclamation already running on %s\r' % fpgname] expected_in = (success, running) # After first one expect 'running', but to avoid timing issues in # the test results accept either success or running. result = self.cl.startfsnapclean(fpgname, reclaimStrategy='maxspeed') self.assertIn(result, expected_in) result = self.cl.startfsnapclean(fpgname, reclaimStrategy='maxspeed') self.assertIn(result, expected_in) result = self.cl.getfsnapclean(fpgname) self.debug_print('GETFSNAPCLEAN:') self.debug_print(result) self.assertIsNone(result['message']) self.assertLess(0, result['total']) for member in result['members']: self.assertTrue(member['avgFileSizeKb'].isdigit()) self.assertTrue(member['endTime'].isdigit()) self.assertIn(member['exitStatus'], ['OK', 'N/A']) self.assertIn(member['logLevel'], ['INFO', 'N/A']) self.assertTrue(member['numDentriesReclaimed'].isdigit()) self.assertTrue(member['numDentriesScanned'].isdigit()) self.assertTrue(member['numErrors'].isdigit()) self.assertTrue(member['numInodesSkipped'].isdigit()) self.assertTrue(member['spaceRecoveredCumulative'].isdigit()) self.assertTrue(member['startTime'].isdigit()) self.assertIn(member['strategy'].upper(), ('MAXSPACE', 'MAXSPEED')) uid_match = '^[0-f]*$' self.assertIsNotNone(re.match(uid_match, member['taskId'])) self.assertIn(member['taskState'], ('RUNNING', 'COMPLETED', 'STOPPED', 'UNKNOWN')) self.assertIn(member['verboseMode'], ['false', 'NA']) result = self.cl.stopfsnapclean(fpgname) self.assertEqual([], result) result = self.cl.startfsnapclean(fpgname, resume=True) self.assertEqual(['No reclamation task running on FPG %s\r' % fpgname], result) def remove_fstore(self, fpgname, vfsname, fstore): self.cl.removefsnap(vfsname, fstore, fpg=fpgname) result = self.cl.startfsnapclean(fpgname, reclaimStrategy='maxspeed') success = [] running = ['Reclamation already running on %s\r' % fpgname] expected_in = (success, running) self.assertIn(result, expected_in) result = self.cl.removefstore(vfsname, fstore, fpg=fpgname) self.assertEqual(['%s removed' % fstore], result) def remove_share(self, protocol, fpgname, vfsname, share_name): result = self.cl.removefshare(protocol, vfsname, share_name, fpg=fpgname, fstore=share_name) self.assertEqual([], result) result = self.cl.removefshare(protocol, vfsname, share_name, fpg=fpgname, fstore=share_name) if protocol == 'nfs': expected = ['%s Delete Export failed with error: ' 'share %s does not exist\r' % (protocol.upper(), share_name)] self.assertEqual(expected, result) else: expected_prefix = 'Failure on Delete Share: %s:' % share_name len_prefix = len(expected_prefix) self.assertEqual(expected_prefix, result[0][0:len_prefix]) # Remove with bogus filestore result = self.cl.removefshare(protocol, vfsname, share_name, fpg=fpgname, fstore='bogus') if protocol == 'nfs': expected = [ '%s Delete Export failed with error: ' 'File Store bogus was not found\r' % protocol.upper()] else: expected = ['Could not find Store=bogus\r'] self.assertEqual(expected, result) @unittest.skipIf(skip_file_persona(), SKIP_MSG) @print_header_and_footer def test_create_and_remove_shares(self): test_prefix = 'UT5_' fpgname = config['TEST'].get('fpg', None) if fpgname is None: fpgname = self.get_or_create_fpg(test_prefix) # Create VFS vfsname = self.get_or_create_vfs(test_prefix, fpgname) # Try creating it again result = self.cl.createvfs('127.0.0.2', '255.255.255.0', vfsname, fpg=fpgname, wait=True) expected = ('VFS "%s" already exists within FPG %s\r' % (vfsname, fpgname)) self.assertEqual(expected, result[0]) self.validate_vfs(vfsname=vfsname, fpgname=fpgname, expected_count=1) # Get the VFS and validate the original settings. result = self.cl.getvfs(fpg=fpgname, vfs=vfsname) self.assertIn('bgrace', result['members'][0]) self.assertIn('igrace', result['members'][0]) self.assertIn('comment', result['members'][0]) # Test FSIPS while we have a VFS # (unfortunately FSIPS might not be ready yet) if result['members'][0]['overallStateInt'] == '1': self.get_fsips(fpgname, vfsname) # CRUD test fstore using this VFS fstore = test_prefix + "CRUD_FSTORE_" + hpe3parbase.TIME self.crud_fstore(fpgname, vfsname, fstore) # NFS SHARES and FSTORE protocol = 'nfs' share_name = "UT_test_share_%s" % protocol comment = 'OpenStack Manila fshare %s' % share_name self.create_share(protocol, fpgname, vfsname, share_name, comment) result = self.cl.getfshare(protocol, share_name, fpg=fpgname, vfs=vfsname, fstore=share_name) self.debug_print(result) self.assertEqual(None, result['message']) self.assertEqual(1, result['total']) member = result['members'][0] self.assertEqual(share_name, member['fstoreName']) self.assertEqual(fpgname, member['fspName']) self.assertEqual(comment, member['comment']) self.assertIsInstance(int(member['overallStateInt']), int) self.assertIsInstance(member['options'], list) self.assertEqual('*', member['clients']) self.assertTrue(member['CompId'].isdigit()) self.remove_share(protocol, fpgname, vfsname, share_name) # SMB SHARES and FSTORE protocol = 'smb' share_name = "UT_test_share_%s" % protocol fstore = share_name comment = 'OpenStack Manila fshare %s' % share_name self.create_share(protocol, fpgname, vfsname, share_name, comment) result = self.cl.getfshare(protocol, share_name, fpg=fpgname, vfs=vfsname, fstore=share_name) self.debug_print(result) self.assertEqual(None, result['message']) self.assertEqual(1, result['total']) member = result['members'][0] self.assertEqual(fstore, member['fstoreName']) self.assertEqual(fpgname, member['fspName']) self.assertEqual(vfsname, member['vfsName']) self.assertEqual(comment, member['comment']) self.assertIsInstance(int(member['overallStateInt']), int) self.assertTrue(member['CompId'].isdigit()) self.assertEqual('false', member['abe']) self.assertIsInstance(member['allowIP'], list) self.assertIsInstance(member['denyIP'], list) self.assertEqual([], member['allowPerm']) self.assertEqual([], member['denyPerm']) self.assertEqual('true', member['ca']) self.assertEqual('manual', member['cache']) self.assertEqual([], member['shareDir']) self.assertEqual(share_name, member['shareName']) self.assertEqual('---', member['uuid']) # SNAPSHOTS (need a share to use) # test creates and cleans tag = test_prefix + "TAG_" + hpe3parbase.TIME self.create_fsnap(fpgname, vfsname, fstore, tag) # SHARE REMOVAL -- includes tests/asserts self.remove_share(protocol, fpgname, vfsname, share_name) # FSTORE REMOVAL -- includes tests/asserts and fsnap remove/clean self.remove_fstore(fpgname, vfsname, fstore) @unittest.skipIf(is_live_test() and skip_file_persona(), SKIP_MSG) @print_header_and_footer def test_removevfs_bogus(self): self.assertRaises(AttributeError, self.cl.removevfs, None) result = self.cl.removevfs('bogus') vfs_not_found = ('Virtual file server bogus was not found in any ' 'existing file provisioning group.\r') self.assertEqual(vfs_not_found, result[0]) self.assertRaises(AttributeError, self.cl.removevfs, None, fpg='bogus') result = self.cl.removevfs('bogus', fpg='bogus') fpg_not_found = 'File Provisioning Group: bogus not found\r' self.assertEqual(fpg_not_found, result[0]) # testing # suite = unittest.TestLoader(). # loadTestsFromTestCase(HPE3ParFilePersonaClientTestCase) # unittest.TextTestRunner(verbosity=2).run(suite) python-3parclient-4.2.12/test/test_HPE3ParClient_FilePersona_Mock.py0000644000000000000000000003450214106434774025270 0ustar rootroot00000000000000# (c) Copyright 2015 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Test class of 3PAR Client handling File Persona API.""" import mock import pprint from pytest_testconfig import config from test import HPE3ParClient_base as hpe3parbase from hpe3parclient import exceptions from hpe3parclient import file_client from hpe3parclient import ssh class HPE3ParFilePersonaClientMockTestCase(hpe3parbase .HPE3ParClientBaseTestCase): interfaces = None DEBUG = config['TEST']['debug'].lower() == 'true' def debug_print(self, obj, **kwargs): if self.DEBUG: print(pprint.pformat(obj, **kwargs)) def setUp(self, **kwargs): version = (file_client.HPE3ParFilePersonaClient .HPE3PAR_WS_MIN_BUILD_VERSION) mock_version = mock.Mock() mock_version.return_value = {'build': version} with mock.patch('hpe3parclient.client.HPE3ParClient.getWsApiVersion', mock_version): self.cl = file_client.HPE3ParFilePersonaClient('anyurl') self.cl.ssh = mock.Mock() self.cl.http = mock.Mock() self.cl.ssh.run = mock.Mock() self.cl.ssh.run.return_value = 'anystring' def tearDown(self): pass class ArgMatcher(object): """Test args vs. expected. Options order can vary.""" def __init__(self, f, cmd, options, specifiers): self.assertEqual = f self.cmd = cmd self.options = options self.specifiers = specifiers def __eq__(self, actual): # Command has to be first. Allow string or list ['doit','nfs']. if isinstance(self.cmd, str): self.cmd = [self.cmd] for c in self.cmd: self.assertEqual(c, actual[0]) del actual[0] # Specifiers have to be last. if self.specifiers: num_specs = len(self.specifiers) self.assertEqual(self.specifiers, actual[-num_specs:]) actual = actual[0:-num_specs] # Options can be in any order. Some are flags. Some are pairs. if self.options: for option in self.options: if isinstance(option, str): actual.remove(option) else: first = actual.index(option[0]) self.assertEqual(option[1], actual[first + 1]) del actual[first + 1] del actual[first] self.assertEqual(actual, []) else: # No options should match and empty actual. self.assertEqual(self.options, actual) return True def test_cli_from_sig_varargs(self): """Use mock and removefpg to test cli_from sig with varargs and kwargs.""" self.cl.removefpg() self.cl.ssh.run.assert_called_with(['removefpg', '-f'], multi_line_stripper=True) self.cl.removefpg("foo") self.cl.ssh.run.assert_called_with(['removefpg', '-f', 'foo'], multi_line_stripper=True) self.cl.removefpg("foo", "bar") self.cl.ssh.run.assert_called_with(['removefpg', '-f', 'foo', 'bar'], multi_line_stripper=True) self.cl.removefpg("foo", "bar", f=False) # f=False needs to be ignored self.cl.ssh.run.assert_called_with( self.ArgMatcher(self.assertEqual, 'removefpg', ['-f'], ['foo', 'bar']), multi_line_stripper=True) self.cl.removefpg("foo", "bar", forget="4gotten", wait=True) self.cl.ssh.run.assert_called_with( self.ArgMatcher(self.assertEqual, 'removefpg', ['-f', '-wait', ('-forget', '4gotten')], ['foo', 'bar']), multi_line_stripper=True) # what if string 'True' is used. That is not a boolean! self.cl.removefpg("foo", "bar", forget='True', wait=True) self.cl.ssh.run.assert_called_with( self.ArgMatcher(self.assertEqual, 'removefpg', ['-f', '-wait', ('-forget', 'True')], ['foo', 'bar']), multi_line_stripper=True) # keyword=None is skipped # keyword=False (boolean) is skipped self.cl.removefpg("foo", "bar", forget=None, wait=False) self.cl.ssh.run.assert_called_with(['removefpg', '-f', 'foo', 'bar'], multi_line_stripper=True) def test_build_cmd_from_str_or_list(self): """Test that build_cmd works with list or string.""" result1 = self.cl._build_command('test -foo') self.assertEqual(['test', '-foo'], result1) result2 = self.cl._build_command(['test', '-foo']) self.assertEqual(['test', '-foo'], result2) def test_get_details(self): """Test that get_details cannot be overridden by an arg.""" test_function_name = 'testdetails' file_client.GET_DETAILS[test_function_name] = True result = self.cl._build_command(test_function_name, d=False) self.assertEqual([test_function_name, '-d'], result) def test_removefpg_mock(self): """Use mock to test removefpg -f.""" self.cl.removefpg() self.cl.ssh.run.assert_called_with( ['removefpg', '-f'], multi_line_stripper=True) self.cl.removefpg('testfpg') self.cl.ssh.run.assert_called_with( ['removefpg', '-f', 'testfpg'], multi_line_stripper=True) def test_createfstore_mock(self): """Use mock to test createfstore.""" self.assertRaises(TypeError, self.cl.createfstore) self.cl.createfstore('testvfs', 'testfstore') self.cl.ssh.run.assert_called_with(['createfstore', 'testvfs', 'testfstore'], multi_line_stripper=True) self.cl.createfstore('testvfs', 'testfstore', fpg='testfpg', comment='test comment') self.cl.ssh.run.assert_called_with( self.ArgMatcher(self.assertEqual, 'createfstore', [('-comment', '"test comment"'), ('-fpg', 'testfpg')], ['testvfs', 'testfstore']), multi_line_stripper=True) def test_createfshare_mock(self): """Use mock to test createfshare with protocol first.""" self.assertRaises(TypeError, self.cl.createfshare) self.cl.createfshare('nfs', 'testvfs', 'testfshare') self.cl.ssh.run.assert_called_with(['createfshare', 'nfs', '-f', 'testvfs', 'testfshare'], multi_line_stripper=True) self.cl.createfshare('smb', 'testvfs', 'testfshare') self.cl.ssh.run.assert_called_with(['createfshare', 'smb', '-f', 'testvfs', 'testfshare'], multi_line_stripper=True) self.cl.createfshare('nfs', 'testvfs', 'testfstore', fpg='testfpg', fstore='testfstore', sharedir='testsharedir', comment='test comment') self.cl.ssh.run.assert_called_with(self.ArgMatcher( self.assertEqual, ['createfshare', 'nfs'], ['-f', ('-fpg', 'testfpg'), ('-fstore', 'testfstore'), ('-sharedir', 'testsharedir'), ('-comment', '"test comment"')], # Comments get quoted ['testvfs', 'testfstore']), multi_line_stripper=True) def test_createfshare_mock_smb_ca(self): """Use mock to test createfshare smb -ca argument.""" self.cl.createfshare('smb', 'testvfs', 'testfshare', ca=None) self.cl.ssh.run.assert_called_with(['createfshare', 'smb', '-f', 'testvfs', 'testfshare'], multi_line_stripper=True) self.cl.createfshare('smb', 'testvfs', 'testfshare', ca='true') self.cl.ssh.run.assert_called_with(self.ArgMatcher( self.assertEqual, ['createfshare', 'smb'], ['-f', ('-ca', 'true')], ['testvfs', 'testfshare']), multi_line_stripper=True) self.cl.createfshare('smb', 'testvfs', 'testfshare', ca='false') self.cl.ssh.run.assert_called_with(self.ArgMatcher( self.assertEqual, ['createfshare', 'smb'], ['-f', ('-ca', 'false')], ['testvfs', 'testfshare']), multi_line_stripper=True) def test_setfshare_mock_smb_ca(self): """Use mock to test setfshare smb -ca argument.""" self.cl.setfshare('smb', 'testvfs', 'testfshare', ca=None) self.cl.ssh.run.assert_called_with(['setfshare', 'smb', 'testvfs', 'testfshare'], multi_line_stripper=True) self.cl.setfshare('smb', 'testvfs', 'testfshare', ca='true') self.cl.ssh.run.assert_called_with(['setfshare', 'smb', '-ca', 'true', 'testvfs', 'testfshare'], multi_line_stripper=True) self.cl.setfshare('smb', 'testvfs', 'testfshare', ca='false') self.cl.ssh.run.assert_called_with(['setfshare', 'smb', '-ca', 'false', 'testvfs', 'testfshare'], multi_line_stripper=True) def test_strip_input_from_output(self): cmd = [ 'createvfs', '-fpg', 'marktestfpg', '-wait', '127.0.0.2', '255.255.255.0', 'UT5_VFS_150651' ] out = [ 'setclienv csvtable 1', 'createvfs -fpg marktestfpg -wait 127.0.0.2 255.255.255.0 ' 'UT5_VFS_150651', 'exit', 'CSIM-EOS08_1611165 cli% setclienv csvtable 1\r', 'CSIM-EOS08_1611165 cli% createvfs -fpg marktestfpg -wait ' '127.0.0.2 255.255.255.\r', '0 UT5_VFS_150651\r', 'VFS UT5_VFS_150651 already exists within FPG marktestfpg\r', 'CSIM-EOS08_1611165 cli% exit\r', '' ] expected = [ 'VFS UT5_VFS_150651 already exists within FPG marktestfpg\r'] actual = ssh.HPE3PARSSHClient.strip_input_from_output(cmd, out) self.assertEqual(expected, actual) def test_strip_input_from_output_no_exit(self): cmd = [ 'createvfs', '-fpg', 'marktestfpg', '-wait', '127.0.0.2', '255.255.255.0', 'UT5_VFS_150651' ] out = [ 'setclienv csvtable 1', 'createvfs -fpg marktestfpg -wait 127.0.0.2 255.255.255.0 ' 'UT5_VFS_150651', 'XXXt', # Don't match 'CSIM-EOS08_1611165 cli% setclienv csvtable 1\r', 'CSIM-EOS08_1611165 cli% createvfs -fpg marktestfpg -wait ' '127.0.0.2 255.255.255.\r', '0 UT5_VFS_150651\r', 'VFS UT5_VFS_150651 already exists within FPG marktestfpg\r', 'CSIM-EOS08_1611165 cli% exit\r', '' ] self.assertRaises(exceptions.SSHException, ssh.HPE3PARSSHClient.strip_input_from_output, cmd, out) def test_strip_input_from_output_no_setclienv(self): cmd = [ 'createvfs', '-fpg', 'marktestfpg', '-wait', '127.0.0.2', '255.255.255.0', 'UT5_VFS_150651' ] out = [ 'setclienv csvtable 1', 'createvfs -fpg marktestfpg -wait 127.0.0.2 255.255.255.0 ' 'UT5_VFS_150651', 'exit', 'CSIM-EOS08_1611165 cli% setcliXXX csvtable 1\r', # Don't match 'CSIM-EOS08_1611165 cli% createvfs -fpg marktestfpg -wait ' '127.0.0.2 255.255.255.\r', '0 UT5_VFS_150651\r', 'VFS UT5_VFS_150651 already exists within FPG marktestfpg\r', 'CSIM-EOS08_1611165 cli% exit\r', '' ] self.assertRaises(exceptions.SSHException, ssh.HPE3PARSSHClient.strip_input_from_output, cmd, out) def test_strip_input_from_output_no_cmd_match(self): cmd = [ 'createvfs', '-fpg', 'marktestfpg', '-wait', '127.0.0.2', '255.255.255.0', 'UT5_VFS_150651' ] out = [ 'setclienv csvtable 1', 'createvfs -fpg marktestfpg -wait 127.0.0.2 255.255.255.0 ' 'UT5_VFS_150651', 'exit', 'CSIM-EOS08_1611165 cli% setclienv csvtable 1\r', 'CSIM-EOS08_1611165 cli% createvfs -fpg marktestfpg -wait ' '127.0.0.2 255.255.255.\r', '0 UT5_VFS_XXXXXX\r', # Don't match 'VFS UT5_VFS_150651 already exists within FPG marktestfpg\r', 'CSIM-EOS08_1611165 cli% exit\r', '' ] self.assertRaises(exceptions.SSHException, ssh.HPE3PARSSHClient.strip_input_from_output, cmd, out) # testing # suite = unittest.TestLoader(). # loadTestsFromTestCase(HPE3ParFilePersonaClientTestCase) # unittest.TextTestRunner(verbosity=2).run(suite) python-3parclient-4.2.12/test/test_HPE3ParClient_HostSet.py0000644000000000000000000004046214106434774023503 0ustar rootroot00000000000000# (c) Copyright 2012-2015 Hewlett Packard Enterprise Development LP # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Test class of 3PAR Client handling of Host Sets.""" import unittest from test import HPE3ParClient_base import random from hpe3parclient import exceptions VOLUME_SIZE = 512 EXPORTED_VLUN = 26 HOST_IN_SET = 77 INV_INPUT_PARAM_CONFLICT = 44 LUN_1 = random.randint(1, 10) LUN_2 = random.randint(1, 10) # Ensure LUN1 and LUN2 are distinct. while LUN_1 == LUN_2: LUN_2 = random.randint(1, 10) host_sets_to_delete = [] hosts_to_delete = [] cpgs_to_delete = [] volumes_to_delete = [] vluns_to_delete = [] # Additional test names declared in test cases to account for variation # in desired name format and aid in manual cleanup. class HPE3ParClientHostSetTestCase(HPE3ParClient_base .HPE3ParClientBaseTestCase): def setUp(self, withSSH=False): super(HPE3ParClientHostSetTestCase, self).setUp(withSSH=False) # noinspection PyBroadException def tearDown(self): """Clean-up -- without fail -- more than humanly possible.""" for vlun in vluns_to_delete: try: self.cl.deleteVLUN(*vlun) except Exception: pass del vluns_to_delete[:] for volume_name in volumes_to_delete: try: self.cl.deleteVolume(volume_name) except Exception: pass del volumes_to_delete[:] for cpg_name in cpgs_to_delete: try: self.cl.deleteCPG(cpg_name) except Exception: pass del cpgs_to_delete[:] for host_name in hosts_to_delete: try: self.cl.removeHostFromItsHostSet(host_name) except Exception: pass try: self.cl.deleteHost(host_name) except Exception: pass del hosts_to_delete[:] for host_set_name in host_sets_to_delete: try: self.cl.deleteHostSet(host_set_name) except Exception: pass del host_sets_to_delete[:] # very last, tear down base class super(HPE3ParClientHostSetTestCase, self).tearDown() def test_crud_host_without_host_set(self): """CRUD test for attach/detach VLUN to host w/o a host set.""" self.printHeader("crud_host_without_host_set") test_prefix = 'UT1_' # # CREATE # # Create Host host_name = test_prefix + "HOST_" + HPE3ParClient_base.TIME hosts_to_delete.append(host_name) optional = {'domain': self.DOMAIN} self.cl.createHost(host_name, optional=optional) # Create CPG cpg_name = test_prefix + "CPG_" + HPE3ParClient_base.TIME cpgs_to_delete.append(cpg_name) optional = self.CPG_OPTIONS self.cl.createCPG(cpg_name, optional) # Create Volumes volume_name1 = test_prefix + "VOL1_" + HPE3ParClient_base.TIME volume_name2 = test_prefix + "VOL2_" + HPE3ParClient_base.TIME volumes_to_delete.extend([volume_name1, volume_name2]) self.cl.createVolume(volume_name1, cpg_name, VOLUME_SIZE) self.cl.createVolume(volume_name2, cpg_name, VOLUME_SIZE) # Create VLUNs vlun1 = [volume_name1, LUN_1, host_name, self.port] vlun2 = [volume_name2, LUN_2, host_name, self.port] vluns_to_delete.extend([vlun1, vlun2]) self.cl.createVLUN(*vlun1) self.cl.createVLUN(*vlun2) # # READ # host = self.cl.getHost(host_name) self.assertEqual(host['name'], host_name) cpg = self.cl.getCPG(cpg_name) self.assertEqual(cpg['name'], cpg_name) volume = self.cl.getVolume(volume_name1) self.assertEqual(volume['name'], volume_name1) volume = self.cl.getVolume(volume_name2) self.assertEqual(volume['name'], volume_name2) host_vluns = self.cl.getHostVLUNs(host_name) self.assertIn(volume_name1, [vlun['volumeName'] for vlun in host_vluns]) self.assertIn(volume_name2, [vlun['volumeName'] for vlun in host_vluns]) vlun = self.cl.getVLUN(volume_name1) self.assertEqual(vlun['volumeName'], volume_name1) vlun = self.cl.getVLUN(volume_name2) self.assertEqual(vlun['volumeName'], volume_name2) # # DELETE # # Try to delete everything in this test as part of the successful test. # The tearDown() also tries to cleanup in case of test failure. self.cl.deleteVLUN(*vlun1) # Make sure that we cannot delete the host while there is still a vlun with self.assertRaises(exceptions.HTTPConflict) as cm: self.cl.deleteHost(host_name) e = cm.exception self.assertEqual(e.get_code(), 26) self.assertEqual(e.get_description(), "has exported VLUN") self.cl.deleteVLUN(*vlun2) # Now we can delete the host self.cl.deleteHost(host_name) # Should be able to clean-up these, too self.cl.deleteVolume(volume_name1) self.cl.deleteVolume(volume_name2) self.cl.deleteCPG(cpg_name) self.printFooter("crud_host_without_host_set") def test_crud_host_with_host_set(self): """CRUD test for attach/detach VLUN to host in host set.""" self.printHeader("crud_host_with_host_set") test_prefix = 'UT2_' # # CREATE # # Create Host host_name = test_prefix + "HOST_" + HPE3ParClient_base.TIME hosts_to_delete.append(host_name) optional = {'domain': self.DOMAIN} self.cl.createHost(host_name, optional=optional) # Create Host Set host_set_name = test_prefix + "HOST_SET_" + HPE3ParClient_base.TIME host_sets_to_delete.append(host_set_name) host_set_id = self.cl.createHostSet( host_set_name, self.DOMAIN, unittest.TestCase.shortDescription(self), [host_name]) # Adding same host again to test add host in host set try: self.cl.addHostToHostSet(host_set_id, host_name) except exceptions.HTTPConflict: pass self.assertEqual(host_set_name, host_set_id) # Create CPG cpg_name = test_prefix + "CPG_" + HPE3ParClient_base.TIME cpgs_to_delete.append(cpg_name) optional = self.CPG_OPTIONS self.cl.createCPG(cpg_name, optional) # Create Volumes volume_name1 = test_prefix + "VOL1_" + HPE3ParClient_base.TIME volume_name2 = test_prefix + "VOL2_" + HPE3ParClient_base.TIME volumes_to_delete.extend([volume_name1, volume_name2]) self.cl.createVolume(volume_name1, cpg_name, VOLUME_SIZE) self.cl.createVolume(volume_name2, cpg_name, VOLUME_SIZE) # Create VLUNs vlun1 = [volume_name1, LUN_1, host_name, self.port] vlun2 = [volume_name2, LUN_2, host_name, self.port] vluns_to_delete.extend([vlun1, vlun2]) self.cl.createVLUN(*vlun1) self.cl.createVLUN(*vlun2) # # READ # host = self.cl.getHost(host_name) self.assertEqual(host['name'], host_name) host_sets = self.cl.getHostSets() host_set = self.cl.getHostSet(host_set_name) found_host_set = self.cl.findHostSet(host_name) self.assertIsNotNone(host_sets) self.assertIsNotNone(host_set) self.assertIsNotNone(found_host_set) self.assertEqual(host_set['name'], found_host_set) self.assertIn(host_set, host_sets['members']) cpg = self.cl.getCPG(cpg_name) self.assertEqual(cpg['name'], cpg_name) volume = self.cl.getVolume(volume_name1) self.assertEqual(volume['name'], volume_name1) volume = self.cl.getVolume(volume_name2) self.assertEqual(volume['name'], volume_name2) host_vluns = self.cl.getHostVLUNs(host_name) self.assertIn(volume_name1, [vlun['volumeName'] for vlun in host_vluns]) self.assertIn(volume_name2, [vlun['volumeName'] for vlun in host_vluns]) vlun = self.cl.getVLUN(volume_name1) self.assertEqual(vlun['volumeName'], volume_name1) vlun = self.cl.getVLUN(volume_name2) self.assertEqual(vlun['volumeName'], volume_name2) # # DELETE # # Try to delete everything in this test as part of the successful test. # The tearDown() also tries to cleanup in case of test failure. self.cl.deleteVLUN(*vlun1) # Make sure that we cannot delete the host while there is still a vlun with self.assertRaises(exceptions.HTTPConflict) as cm: self.cl.deleteHost(host_name) e = cm.exception self.assertEqual(e.get_code(), EXPORTED_VLUN) self.assertEqual(e.get_description(), "has exported VLUN") self.cl.deleteVLUN(*vlun2) # Make sure that we cannot delete the host while it is in a host set with self.assertRaises(exceptions.HTTPConflict) as cm: self.cl.deleteHost(host_name) e = cm.exception self.assertEqual(e.get_code(), HOST_IN_SET) self.assertEqual(e.get_description(), "host is a member of a set") self.cl.removeHostFromItsHostSet(host_name) # Now we can delete the host self.cl.deleteHost(host_name) # Should be able to clean-up these too self.cl.deleteHostSet(host_set_name) self.cl.deleteVolume(volume_name1) self.cl.deleteVolume(volume_name2) self.cl.deleteCPG(cpg_name) self.printFooter("crud_host_with_host_set") def test_host_set_name_too_long(self): """Host set name too long.""" self.printHeader("host_set_name_too_long") test_prefix = 'UT_' # name too long host_set_name = (test_prefix + "HOST_SET_NAME_IS_TOOOOOOOOOO_LONG_" + HPE3ParClient_base.TIME) host_sets_to_delete.append(host_set_name) pre_count = len(self.cl.getHostSets()['members']) with self.assertRaises(exceptions.HTTPBadRequest) as cm: self.cl.createHostSet(host_set_name) e = cm.exception self.assertEqual( e.get_description(), "invalid input: string length exceeds limit") post_count = len(self.cl.getHostSets()['members']) self.assertEqual(pre_count, post_count) self.assertRaises( exceptions.HTTPNotFound, self.cl.getHostSet, host_set_name ) with self.assertRaises(exceptions.HTTPBadRequest) as cm: self.cl.modifyHostSet(host_set_name, comment="not gonna happen") e = cm.exception self.assertEqual( e.get_description(), "invalid input: string length exceeds limit") self.assertRaises( exceptions.HTTPNotFound, self.cl.deleteHostSet, host_set_name ) self.printFooter("host_set_name_too_long") def test_host_set_name_invalid(self): """Host set name with invalid characters.""" self.printHeader("host_set_name_invalid") # name has invalid characters host_set_name = "HostSet-!nval!d" with self.assertRaises(exceptions.HTTPBadRequest) as cm: self.cl.getHostSet(host_set_name) e = cm.exception self.assertEqual(e.get_description(), "illegal character in input") self.printFooter("host_set_name_invalid") def test_duplicate_host_set_name(self): """Host set name already exists.""" self.printHeader("duplicate_host_set_name") test_prefix = 'UT3_' # create same one twice host_set_name = test_prefix + "HS_X_" + HPE3ParClient_base.TIME host_sets_to_delete.append(host_set_name) pre_count = len(self.cl.getHostSets()['members']) original_comment = "original comment" host_set_id = self.cl.createHostSet(host_set_name, comment=original_comment) self.assertEqual(host_set_name, host_set_id) post_count = len(self.cl.getHostSets()['members']) self.assertEqual(pre_count + 1, post_count) pre_count = post_count self.assertRaises( exceptions.HTTPConflict, self.cl.createHostSet, host_set_name ) post_count = len(self.cl.getHostSets()['members']) self.assertEqual(pre_count, post_count) self.printFooter("duplicate_host_set_name") def test_modify_param_conflict(self): """Test modify of host sets parameter conflict.""" self.printHeader("modify_param_conflict") test_prefix = 'UT4_' host_set_name1 = test_prefix + "HS1_" + HPE3ParClient_base.TIME host_sets_to_delete.append(host_set_name1) self.cl.createHostSet(host_set_name1) host_set_name2 = test_prefix + "HS2_" + HPE3ParClient_base.TIME host_sets_to_delete.append(host_set_name2) new_comment = "new comment" new_member = "bogushost" with self.assertRaises(exceptions.HTTPBadRequest) as cm: self.cl.modifyHostSet(host_set_name1, 1, host_set_name2, new_comment, setmembers=[new_member]) e = cm.exception self.assertEqual(e.get_description(), "invalid input: parameters cannot be present" " at the same time") self.assertEqual(e.get_code(), INV_INPUT_PARAM_CONFLICT) self.printFooter("modify_param_conflict") def test_bogus_host(self): """Modify of host set with bogus host.""" self.printHeader("bogus_host") test_prefix = 'UT5_' host_set_name1 = test_prefix + "HS1_" + HPE3ParClient_base.TIME host_sets_to_delete.append(host_set_name1) self.cl.createHostSet(host_set_name1) new_member = "bogushost" with self.assertRaises(exceptions.HTTPNotFound) as cm: self.cl.modifyHostSet(host_set_name1, 1, setmembers=[new_member]) e = cm.exception self.assertEqual(e.get_description(), "host does not exist") self.assertEqual(e.get_code(), 17) self.printFooter("bogus_host") def test_modify(self): """Test modify of host sets.""" self.printHeader("modify") test_prefix = 'UT6_' host_set_name1 = test_prefix + "HS1_" + HPE3ParClient_base.TIME host_sets_to_delete.append(host_set_name1) self.cl.createHostSet(host_set_name1) host_set_name2 = test_prefix + "HS2_" + HPE3ParClient_base.TIME host_sets_to_delete.append(host_set_name2) new_comment = "new comment" self.cl.modifyHostSet(host_set_name1, newName=host_set_name2) self.cl.modifyHostSet(host_set_name2, comment=new_comment) created_host1 = test_prefix + "HOST1_" + HPE3ParClient_base.TIME hosts_to_delete.append(created_host1) self.cl.createHost(created_host1) self.cl.modifyHostSet(host_set_name2, 1, setmembers=[created_host1]) self.assertRaises( exceptions.HTTPNotFound, self.cl.getHostSet, host_set_name1 ) host2 = self.cl.getHostSet(host_set_name2) self.assertEqual(host2['name'], host_set_name2) self.assertEqual(host2['comment'], new_comment) self.assertEqual(host2['setmembers'], [created_host1]) created_host2 = test_prefix + "HOST2_" + HPE3ParClient_base.TIME hosts_to_delete.append(created_host2) self.cl.createHost(created_host2) self.cl.modifyHostSet(host_set_name2, 1, setmembers=[created_host2]) host2 = self.cl.getHostSet(host_set_name2) self.assertEqual(host2['setmembers'], [created_host1, created_host2]) self.printFooter("modify") python-3parclient-4.2.12/test/test_HPE3ParClient_MockSSH.py0000644000000000000000000002600114106434774023352 0ustar rootroot00000000000000# (c) Copyright 2012-2015 Hewlett Packard Enterprise Development LP # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import mock import paramiko import unittest from test import HPE3ParClient_base from hpe3parclient import exceptions from hpe3parclient import ssh from hpe3parclient import client # Python 3+ override try: basestring except NameError: basestring = str user = "u" password = "p" ip = "10.10.22.241" api_url = "http://10.10.22.241:8008/api/v1" class HPE3ParClientMockSSHTestCase(HPE3ParClient_base .HPE3ParClientBaseTestCase): def mock_paramiko(self, known_hosts_file, missing_key_policy): """Verify that these params get into paramiko.""" mock_lhk = mock.Mock() mock_lshk = mock.Mock() mock_smhkp = mock.Mock() mock_smhkp.side_effect = Exception("Let's end this here") with mock.patch('paramiko.client.SSHClient.load_system_host_keys', mock_lshk, create=True): with mock.patch('paramiko.client.SSHClient.load_host_keys', mock_lhk, create=True): with mock.patch('paramiko.client.SSHClient.' 'set_missing_host_key_policy', mock_smhkp, create=True): try: self.cl.setSSHOptions( ip, user, password, known_hosts_file=known_hosts_file, missing_key_policy=missing_key_policy) except paramiko.SSHException as e: if 'Invalid missing_key_policy' in str(e): raise e except Exception: pass if known_hosts_file is None: mock_lhk.assert_not_called() mock_lshk.assert_called_with() else: mock_lhk.assert_called_with(known_hosts_file) mock_lshk.assert_not_called() actual = mock_smhkp.call_args[0][0].__class__.__name__ if missing_key_policy is None: # If missing, it should be called with our # default which is an AutoAddPolicy expected = paramiko.AutoAddPolicy().__class__.__name__ elif isinstance(missing_key_policy, basestring): expected = missing_key_policy else: expected = missing_key_policy.__class__.__name__ self.assertEqual(actual, expected) def do_mock_create_ssh(self, known_hosts_file, missing_key_policy): """Verify that params are getting forwarded to _create_ssh().""" mock_ssh = mock.Mock() with mock.patch('hpe3parclient.ssh.HPE3PARSSHClient._create_ssh', mock_ssh, create=True): self.cl.setSSHOptions(ip, user, password, known_hosts_file=known_hosts_file, missing_key_policy=missing_key_policy) mock_ssh.assert_called_with(missing_key_policy=missing_key_policy, known_hosts_file=known_hosts_file) # Create a mocked ssh object for the client so that it can be # "closed" during a logout. self.cl.ssh = mock.MagicMock() @mock.patch('hpe3parclient.ssh.HPE3PARSSHClient') def do_mock_ssh(self, known_hosts_file, missing_key_policy, mock_ssh_client): """Verify that params are getting forwarded to HPE3PARSSHClient.""" self.cl.setSSHOptions(ip, user, password, known_hosts_file=known_hosts_file, missing_key_policy=missing_key_policy) mock_ssh_client.assert_called_with( ip, user, password, 22, None, None, missing_key_policy=missing_key_policy, known_hosts_file=known_hosts_file) def base(self, known_hosts_file, missing_key_policy): self.printHeader("%s : known_hosts_file=%s missing_key_policy=%s" % (unittest.TestCase.id(self), known_hosts_file, missing_key_policy)) self.do_mock_ssh(known_hosts_file, missing_key_policy) self.do_mock_create_ssh(known_hosts_file, missing_key_policy) self.mock_paramiko(known_hosts_file, missing_key_policy) self.printFooter(unittest.TestCase.id(self)) def test_auto_add_policy(self): known_hosts_file = "test_bogus_known_hosts_file" missing_key_policy = "AutoAddPolicy" self.base(known_hosts_file, missing_key_policy) def test_warning_policy(self): known_hosts_file = "test_bogus_known_hosts_file" missing_key_policy = "WarningPolicy" self.base(known_hosts_file, missing_key_policy) def test_reject_policy(self): known_hosts_file = "test_bogus_known_hosts_file" missing_key_policy = "RejectPolicy" self.base(known_hosts_file, missing_key_policy) def test_known_hosts_file_is_none(self): known_hosts_file = None missing_key_policy = paramiko.RejectPolicy() self.base(known_hosts_file, missing_key_policy) def test_both_settings_are_none(self): known_hosts_file = None missing_key_policy = None self.base(known_hosts_file, missing_key_policy) def test_bogus_missing_key_policy(self): known_hosts_file = None missing_key_policy = "bogus" self.assertRaises(paramiko.SSHException, self.base, known_hosts_file, missing_key_policy) def test_create_ssh_except(self): """Make sure that SSH exceptions are not quietly eaten.""" self.cl.setSSHOptions(ip, user, password, known_hosts_file=None, missing_key_policy=paramiko.AutoAddPolicy) self.cl.ssh.ssh = mock.Mock() self.cl.ssh.ssh.invoke_shell.side_effect = Exception('boom') cmd = ['fake'] self.assertRaises(exceptions.SSHException, self.cl.ssh._run_ssh, cmd) self.cl.ssh.ssh.assert_has_calls( [ mock.call.get_transport(), mock.call.get_transport().is_alive(), mock.call.invoke_shell(), mock.call.get_transport(), mock.call.get_transport().is_alive(), ] ) def test_sanitize_cert(self): # no begin cert input = 'foo -END CERTIFICATE- no begin' expected = input out = ssh.HPE3PARSSHClient.sanitize_cert(input) self.assertEqual(expected, out) # pre, begin, middle, end, post input = 'head -BEGIN CERTIFICATE-1234-END CERTIFICATE- tail' expected = 'head -BEGIN CERTIFICATE-sanitized-END CERTIFICATE- tail' out = ssh.HPE3PARSSHClient.sanitize_cert(input) self.assertEqual(expected, out) # end before begin input = 'head -END CERTIFICATE-1234-BEGIN CERTIFICATE- tail' expected = 'head -END CERTIFICATE-1234-BEGIN CERTIFICATE-sanitized' out = ssh.HPE3PARSSHClient.sanitize_cert(input) self.assertEqual(expected, out) # no end input = 'head -BEGIN CERTIFICATE-1234-END CEXXXXXXXTE- tail' expected = 'head -BEGIN CERTIFICATE-sanitized' out = ssh.HPE3PARSSHClient.sanitize_cert(input) self.assertEqual(expected, out) # test with a list input = ['head -BEGIN CERTIFICATE-----1234', 'ss09f87sdf987sf97sfsds0f7sf97s89', '6789-----END CERTIFICATE- tail'] expected = 'head -BEGIN CERTIFICATE-sanitized-END CERTIFICATE- tail' out = ssh.HPE3PARSSHClient.sanitize_cert(input) self.assertEqual(expected, out) def test_strip_input_from_output(self): cmd = ['foo', '-v'] # nothing after exit output = ['exit'] self.assertRaises(exceptions.SSHException, ssh.HPE3PARSSHClient.strip_input_from_output, cmd, output) # no exit output = ['line1', 'line2', 'line3'] self.assertRaises(exceptions.SSHException, ssh.HPE3PARSSHClient.strip_input_from_output, cmd, output) # no setclienv csv output = [cmd, 'exit', 'out'] self.assertRaises(exceptions.SSHException, ssh.HPE3PARSSHClient.strip_input_from_output, cmd, output) # command not in output after exit output = [cmd, 'exit', 'PROMPT% setclienv csvtable 1'] self.assertRaises(exceptions.SSHException, ssh.HPE3PARSSHClient.strip_input_from_output, cmd, output) # success output = [cmd, 'setclienv csvtable 1', 'exit', 'PROMPT% setclienv csvtable 1', 'PROMPT% foo -v', 'out1', 'out2', 'out3', '------', 'totals'] result = ssh.HPE3PARSSHClient.strip_input_from_output(cmd, output) self.assertEqual(['out1', 'out2', 'out3'], result) @mock.patch('hpe3parclient.client.ssh.HPE3PARSSHClient', spec=True) def test_verify_get_port(self, mock_ssh_client): known_hosts_file = "test_bogus_known_hosts_file" missing_key_policy = "AutoAddPolicy" cli_output = ["-Service-,-State-,HTTPS_Port,-Version-," "------------------API_URL-------------------", "Enabled,Active,443,1.7.0," "https://vp2-157.in.rdlabs.hpecorp.net/api/v1"] with mock.patch.object(client.HPE3ParClient, "_getSshClient") as mock_get_ssh_client: mock_get_ssh_client.return_value = mock_ssh_client mock_ssh_client.open.return_value = True mock_ssh_client.run.return_value = cli_output result = client.HPE3ParClient.getPortNumber( ip, user, password, 22, None, None, known_hosts_file=known_hosts_file, missing_key_policy=missing_key_policy) self.assertEqual('443', result) python-3parclient-4.2.12/test/test_HPE3ParClient_ShowportParser.py0000644000000000000000000004672314106434774025122 0ustar rootroot00000000000000# (c) Copyright 2015 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Test class of 3PAR Client handling parsing of showport commands.""" from test import HPE3ParClient_base as hp3parbase # from hpe3parclient import client # from hpe3parclient.client import ShowportParser from hpe3parclient import showport_parser class HP3ParClientShowportTestCase(hp3parbase.HPE3ParClientBaseTestCase): def setUp(self): super(HP3ParClientShowportTestCase, self).setUp() def tearDown(self): super(HP3ParClientShowportTestCase, self).tearDown() def test_parse_showport_iscsivlans(self): parsed_ports = showport_parser.\ ShowportParser().parseShowport(ports_iscsivlan) if parsed_ports != parsed_ports_key: err_msg = 'parsed ports does not match key output: {}' return self.fail(err_msg.format(parsed_ports)) return def test_parse_showport_empty(self): ports = \ ['N:S:P,VLAN,IPAddr,Netmask/PrefixLen,Gateway,' 'MTU,TPGT,STGT,iSNS_Addr,iSNS_Port', '-----------------------------------------' '----------------------------------------------', '0,,,,,,,,,'] parsed_ports = showport_parser.ShowportParser().parseShowport(ports) if len(parsed_ports) != 0: err_msg = 'Parsed ports should be empty but contains data: {}' return self.fail(err_msg.format(parsed_ports)) return def test_clone_ports(self): parsed_ports = showport_parser.\ ShowportParser().parseShowport(ports_iscsivlan) expanded_ports = self.cl._cloneISCSIPorts(real_ports, parsed_ports) if expanded_ports != expanded_ports_key: err_msg = 'combined ports output does not match test key: {}' return self.fail(err_msg.format(expanded_ports)) return global ports_iscsivlan ports_iscsivlan = \ ['N:S:P,VLAN,IPAddr,Netmask/PrefixLen,Gateway,MTU,' 'TPGT,STGT,iSNS_Addr,iSNS_Port', '0:2:1,101,172.20.0.150,255.255.255.0,' '172.20.0.1,9000,1024,1024,0.0.0.0,3205', '0:2:2,102,172.20.1.150,255.255.255.0,' '172.20.1.1,9000,1025,1025,0.0.0.0,3205', '1:2:1,101,172.20.0.151,255.255.255.0,' '172.20.0.1,9000,1026,1026,0.0.0.0,3205', '1:2:2,102,172.20.1.151,255.255.255.0,' '172.20.1.1,9000,1027,1027,0.0.0.0,3205', '---------------------------------------' '------------------------------------------------', '4,,,,,,,,,'] global real_ports real_ports = { u'total': 14, u'members': [ { u'portWWN': u'20010002AC01C533', u'protocol': 1, u'partnerPos': { u'node': 1, u'slot': 0, u'cardPort': 1 }, u'linkState': 5, u'failoverState': 1, u'mode': 2, u'device': [ ], u'nodeWWN': u'2FF70002AC01C533', u'type': 3, u'portPos': { u'node': 0, u'slot': 0, u'cardPort': 1 } }, { u'portWWN': u'20020002AC01C533', u'protocol': 1, u'partnerPos': { u'node': 1, u'slot': 0, u'cardPort': 2 }, u'linkState': 5, u'failoverState': 1, u'mode': 2, u'device': [ ], u'nodeWWN': u'2FF70002AC01C533', u'type': 3, u'portPos': { u'node': 0, u'slot': 0, u'cardPort': 2 } }, { u'portWWN': u'50002AC01101C533', u'protocol': 5, u'linkState': 4, u'label': u'DP-1', u'mode': 3, u'device': [ u'cage0', u'cage1', u'cage2' ], u'nodeWWN': u'50002ACFF701C533', u'type': 2, u'portPos': { u'node': 0, u'slot': 1, u'cardPort': 1 } }, { u'portWWN': u'50002AC01201C533', u'protocol': 5, u'linkState': 4, u'label': u'DP-2', u'mode': 3, u'device': [ u'cage3', u'cage4', u'cage5' ], u'nodeWWN': u'50002ACFF701C533', u'type': 2, u'portPos': { u'node': 0, u'slot': 1, u'cardPort': 2 } }, { u'portPos': { u'node': 0, u'slot': 2, u'cardPort': 1 }, u'protocol': 2, u'iSCSIPortInfo': { u'iSNSAddr': u'0.0.0.0', u'vlan': 1, u'IPAddr': u'0.0.0.0', u'rate': u'10Gbps', u'mtu': 9000, u'stgt': 21, u'netmask': u'0.0.0.0', u'iSCSIName': u'iqn.2000-05.com.3pardata:20210002ac01c533', u'tpgt': 21, u'iSNSPort': 3205, u'gateway': u'172.20.0.1' }, u'partnerPos': { u'node': 1, u'slot': 2, u'cardPort': 1 }, u'IPAddr': u'0.0.0.0', u'linkState': 4, u'device': [ ], u'iSCSIName': u'iqn.2000-05.com.3pardata:20210002ac01c533', u'failoverState': 1, u'mode': 2, u'HWAddr': u'1402EC613AFA', u'type': 8 }, { u'portPos': { u'node': 0, u'slot': 2, u'cardPort': 2 }, u'protocol': 2, u'iSCSIPortInfo': { u'iSNSAddr': u'0.0.0.0', u'vlan': 1, u'IPAddr': u'0.0.0.0', u'rate': u'10Gbps', u'mtu': 1500, u'stgt': 22, u'netmask': u'0.0.0.0', u'iSCSIName': u'iqn.2000-05.com.3pardata:20220002ac01c533', u'tpgt': 22, u'iSNSPort': 3205, u'gateway': u'0.0.0.0' }, u'partnerPos': { u'node': 1, u'slot': 2, u'cardPort': 2 }, u'IPAddr': u'0.0.0.0', u'linkState': 4, u'device': [ ], u'iSCSIName': u'iqn.2000-05.com.3pardata:20220002ac01c533', u'failoverState': 1, u'mode': 2, u'HWAddr': u'1402EC613AF2', u'type': 8 }, { u'portPos': { u'node': 0, u'slot': 3, u'cardPort': 1 }, u'protocol': 4, u'linkState': 10, u'label': u'IP0', u'device': [ ], u'mode': 4, u'HWAddr': u'941882447BDD', u'type': 3 }, { u'portWWN': u'21010002AC01C533', u'protocol': 1, u'partnerPos': { u'node': 0, u'slot': 0, u'cardPort': 1 }, u'linkState': 5, u'failoverState': 1, u'mode': 2, u'device': [ ], u'nodeWWN': u'2FF70002AC01C533', u'type': 3, u'portPos': { u'node': 1, u'slot': 0, u'cardPort': 1 } }, { u'portWWN': u'21020002AC01C533', u'protocol': 1, u'partnerPos': { u'node': 0, u'slot': 0, u'cardPort': 2 }, u'linkState': 5, u'failoverState': 1, u'mode': 2, u'device': [ ], u'nodeWWN': u'2FF70002AC01C533', u'type': 3, u'portPos': { u'node': 1, u'slot': 0, u'cardPort': 2 } }, { u'portWWN': u'50002AC11101C533', u'protocol': 5, u'linkState': 4, u'label': u'DP-1', u'mode': 3, u'device': [ u'cage0', u'cage1', u'cage2' ], u'nodeWWN': u'50002ACFF701C533', u'type': 2, u'portPos': { u'node': 1, u'slot': 1, u'cardPort': 1 } }, { u'portWWN': u'50002AC11201C533', u'protocol': 5, u'linkState': 4, u'label': u'DP-2', u'mode': 3, u'device': [ u'cage3', u'cage4', u'cage5' ], u'nodeWWN': u'50002ACFF701C533', u'type': 2, u'portPos': { u'node': 1, u'slot': 1, u'cardPort': 2 } }, { u'portPos': { u'node': 1, u'slot': 2, u'cardPort': 1 }, u'protocol': 2, u'iSCSIPortInfo': { u'iSNSAddr': u'0.0.0.0', u'vlan': 1, u'IPAddr': u'0.0.0.0', u'rate': u'10Gbps', u'mtu': 1500, u'stgt': 121, u'netmask': u'0.0.0.0', u'iSCSIName': u'iqn.2000-05.com.3pardata:21210002ac01c533', u'tpgt': 121, u'iSNSPort': 3205, u'gateway': u'0.0.0.0' }, u'partnerPos': { u'node': 0, u'slot': 2, u'cardPort': 1 }, u'IPAddr': u'0.0.0.0', u'linkState': 4, u'device': [ ], u'iSCSIName': u'iqn.2000-05.com.3pardata:21210002ac01c533', u'failoverState': 1, u'mode': 2, u'HWAddr': u'1402EC613B0A', u'type': 8 }, { u'portPos': { u'node': 1, u'slot': 2, u'cardPort': 2 }, u'protocol': 2, u'iSCSIPortInfo': { u'iSNSAddr': u'0.0.0.0', u'vlan': 1, u'IPAddr': u'0.0.0.0', u'rate': u'10Gbps', u'mtu': 1500, u'stgt': 122, u'netmask': u'0.0.0.0', u'iSCSIName': u'iqn.2000-05.com.3pardata:21220002ac01c533', u'tpgt': 122, u'iSNSPort': 3205, u'gateway': u'0.0.0.0' }, u'partnerPos': { u'node': 0, u'slot': 2, u'cardPort': 2 }, u'IPAddr': u'0.0.0.0', u'linkState': 4, u'device': [ ], u'iSCSIName': u'iqn.2000-05.com.3pardata:21220002ac01c533', u'failoverState': 1, u'mode': 2, u'HWAddr': u'1402EC613B02', u'type': 8 }, { u'portPos': { u'node': 1, u'slot': 3, u'cardPort': 1 }, u'protocol': 4, u'linkState': 10, u'label': u'IP1', u'device': [ ], u'mode': 4, u'HWAddr': u'941882447CED', u'type': 3 } ] } global parsed_ports_key parsed_ports_key = [ { 'IPAddr': '172.20.0.150', 'portPos': { 'node': 0, 'slot': 2, 'cardPort': 1 }, 'iSCSIPortInfo': { 'iSNSAddr': '0.0.0.0', 'vlan': '101', 'IPAddr': '172.20.0.150', 'mtu': 9000, 'stgt': 1024, 'netmask': '255.255.255.0', 'tpgt': 1024, 'iSNSPort': 3205, 'gateway': '172.20.0.1' } }, { 'IPAddr': '172.20.1.150', 'portPos': { 'node': 0, 'slot': 2, 'cardPort': 2 }, 'iSCSIPortInfo': { 'iSNSAddr': '0.0.0.0', 'vlan': '102', 'IPAddr': '172.20.1.150', 'mtu': 9000, 'stgt': 1025, 'netmask': '255.255.255.0', 'tpgt': 1025, 'iSNSPort': 3205, 'gateway': '172.20.1.1' } }, { 'IPAddr': '172.20.0.151', 'portPos': { 'node': 1, 'slot': 2, 'cardPort': 1 }, 'iSCSIPortInfo': { 'iSNSAddr': '0.0.0.0', 'vlan': '101', 'IPAddr': '172.20.0.151', 'mtu': 9000, 'stgt': 1026, 'netmask': '255.255.255.0', 'tpgt': 1026, 'iSNSPort': 3205, 'gateway': '172.20.0.1' } }, { 'IPAddr': '172.20.1.151', 'portPos': { 'node': 1, 'slot': 2, 'cardPort': 2 }, 'iSCSIPortInfo': { 'iSNSAddr': '0.0.0.0', 'vlan': '102', 'IPAddr': '172.20.1.151', 'mtu': 9000, 'stgt': 1027, 'netmask': '255.255.255.0', 'tpgt': 1027, 'iSNSPort': 3205, 'gateway': '172.20.1.1' } } ] global expanded_ports_key expanded_ports_key = [ { u'portPos': { 'node': 0, 'cardPort': 1, 'slot': 2 }, u'device': [ ], u'linkState': 4, u'partnerPos': { u'node': 1, u'cardPort': 1, u'slot': 2 }, u'iSCSIPortInfo': { 'vlan': '101', 'gateway': '172.20.0.1', 'iSNSPort': 3205, 'mtu': 9000, 'IPAddr': '172.20.0.150', 'stgt': 1024, 'netmask': '255.255.255.0', 'tpgt': 1024, 'iSNSAddr': '0.0.0.0' }, u'type': 8, u'protocol': 2, u'failoverState': 1, u'IPAddr': '172.20.0.150', u'iSCSIName': u'iqn.2000-05.com.3pardata:20210002ac01c533', u'HWAddr': u'1402EC613AFA', u'mode': 2 }, { u'portPos': { 'node': 0, 'cardPort': 2, 'slot': 2 }, u'device': [ ], u'linkState': 4, u'partnerPos': { u'node': 1, u'cardPort': 2, u'slot': 2 }, u'iSCSIPortInfo': { 'vlan': '102', 'gateway': '172.20.1.1', 'iSNSPort': 3205, 'mtu': 9000, 'IPAddr': '172.20.1.150', 'stgt': 1025, 'netmask': '255.255.255.0', 'tpgt': 1025, 'iSNSAddr': '0.0.0.0' }, u'type': 8, u'protocol': 2, u'failoverState': 1, u'IPAddr': '172.20.1.150', u'iSCSIName': u'iqn.2000-05.com.3pardata:20220002ac01c533', u'HWAddr': u'1402EC613AF2', u'mode': 2 }, { u'portPos': { 'node': 1, 'cardPort': 1, 'slot': 2 }, u'device': [ ], u'linkState': 4, u'partnerPos': { u'node': 0, u'cardPort': 1, u'slot': 2 }, u'iSCSIPortInfo': { 'vlan': '101', 'gateway': '172.20.0.1', 'iSNSPort': 3205, 'mtu': 9000, 'IPAddr': '172.20.0.151', 'stgt': 1026, 'netmask': '255.255.255.0', 'tpgt': 1026, 'iSNSAddr': '0.0.0.0' }, u'type': 8, u'protocol': 2, u'failoverState': 1, u'IPAddr': '172.20.0.151', u'iSCSIName': u'iqn.2000-05.com.3pardata:21210002ac01c533', u'HWAddr': u'1402EC613B0A', u'mode': 2 }, { u'portPos': { 'node': 1, 'cardPort': 2, 'slot': 2 }, u'device': [ ], u'linkState': 4, u'partnerPos': { u'node': 0, u'cardPort': 2, u'slot': 2 }, u'iSCSIPortInfo': { 'vlan': '102', 'gateway': '172.20.1.1', 'iSNSPort': 3205, 'mtu': 9000, 'IPAddr': '172.20.1.151', 'stgt': 1027, 'netmask': '255.255.255.0', 'tpgt': 1027, 'iSNSAddr': '0.0.0.0' }, u'type': 8, u'protocol': 2, u'failoverState': 1, u'IPAddr': '172.20.1.151', u'iSCSIName': u'iqn.2000-05.com.3pardata:21220002ac01c533', u'HWAddr': u'1402EC613B02', u'mode': 2 } ] python-3parclient-4.2.12/test/test_HPE3ParClient_Stats.py0000644000000000000000000000531514106434774023206 0ustar rootroot00000000000000# (c) Copyright 2015 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Test class of 3PAR Client handling Stats.""" import mock from test import HPE3ParClient_base from hpe3parclient import exceptions class HPE3ParClientStatsTestCase(HPE3ParClient_base.HPE3ParClientBaseTestCase): def setUp(self): super(HPE3ParClientStatsTestCase, self).setUp(withSSH=True) def tearDown(self): # very last, tear down base class super(HPE3ParClientStatsTestCase, self).tearDown() def test_getCPGStatData(self): self.printHeader('getCPGStatData') # Find a cpg name cpgs = self.cl.getCPGs() cpg = cpgs['members'][0] name = cpg['name'] self.assertTrue(self.findInDict(cpgs['members'], 'name', name)) # Make srstatld give valid output self.cl._run = mock.Mock(return_value=[ ',,---IO/s----,,,-KBytes/s--,,,---Svct ms----,,,-IOSz KBytes-,,,,', 'Time,Secs,Rd,Wr,Tot,Rd,Wr,Tot,Rd,Wr,Tot,Rd,Wr,Tot,QLen,AvgBusy%', '2015-07-02 01:45:00 PDT,1435826700,1.3,6.2,7.5,4.6,89.3,93.9,' '0.63,10.18,8.54,3.6,14.3,12.5,0,0.0', '2015-07-02 13:40:00 PDT,1435869600,1.3,6.2,7.5,4.6,88.6,93.1,' '0.34,10.34,8.63,3.6,14.3,12.4,0,0.0', '----------------------------------------------------------------' '-----------------------------------------', ',144,1.3,6.7,8.0,4.7,95.6,100.4,0.59,10.24,8.69,3.7,14.2,12.5,' '0.9,0.1', ]) # Get result interval = 'daily' history = '7d' result = self.cl.getCPGStatData(name, interval, history) # Check result self.assertIsNotNone(result) self.assertTrue(type(result) is dict) # Make srstatld give invalid output self.cl._run = mock.Mock(return_value=['']) # Expect exception self.assertRaises(exceptions.SrstatldException, self.cl.getCPGStatData, name, interval, history) self.printFooter('getCPGStatData') # testing # suite = unittest.TestLoader().loadTestsFromTestCase(HPE3ParClientCPGTestCase) # unittest.TextTestRunner(verbosity=2).run(suite) python-3parclient-4.2.12/test/test_HPE3ParClient_VLUN.py0000644000000000000000000003505314106434774022676 0ustar rootroot00000000000000# (c) Copyright 2015 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Test class of 3PAR Client handling VLUN.""" from test import HPE3ParClient_base as hpe3parbase import random import mock import unittest from pytest_testconfig import config from hpe3parclient import client from hpe3parclient import exceptions try: from urllib.parse import quote except ImportError: from urllib2 import quote CPG_NAME1 = 'CPG1_VLUN_UNIT_TEST' + hpe3parbase.TIME CPG_NAME2 = 'CPG2_VLUN_UNIT_TEST' + hpe3parbase.TIME VOLUME_NAME1 = 'VOLUME1_VLUN_UNIT_TEST' + hpe3parbase.TIME VOLUME_NAME2 = 'VOLUME2_VLUN_UNIT_TEST' + hpe3parbase.TIME DOMAIN = 'UNIT_TEST_DOMAIN' HOST_NAME1 = 'HOST1_VLUN_UNIT_TEST' + hpe3parbase.TIME HOST_NAME2 = 'HOST2_VLUN_UNIT_TEST' + hpe3parbase.TIME LUN_0 = 0 LUN_1 = random.randint(1, 10) LUN_2 = random.randint(1, 10) # Ensure LUN1 and LUN2 are distinct. while LUN_2 == LUN_1: LUN_2 = random.randint(1, 10) class HPE3ParClientVLUNTestCase(hpe3parbase.HPE3ParClientBaseTestCase): def setUp(self): super(HPE3ParClientVLUNTestCase, self).setUp() try: optional = self.CPG_OPTIONS self.cl.createCPG(CPG_NAME1, optional) except Exception: pass try: optional = self.CPG_OPTIONS self.cl.createCPG(CPG_NAME2, optional) except Exception: pass try: self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024) except Exception: pass try: self.cl.createVolume(VOLUME_NAME2, CPG_NAME2, 1024) except Exception: pass try: optional = {'domain': self.DOMAIN} self.cl.createHost(HOST_NAME1, None, None, optional) except Exception: pass try: optional = {'domain': self.DOMAIN} self.cl.createHost(HOST_NAME2, None, None, optional) except Exception: pass def tearDown(self): try: self.cl.deleteVLUN(VOLUME_NAME1, LUN_1, HOST_NAME1, self.port) except Exception: pass try: self.cl.deleteVLUN(VOLUME_NAME1, LUN_1, HOST_NAME2) except Exception: pass try: self.cl.deleteVLUN(VOLUME_NAME2, LUN_2, HOST_NAME2) except Exception: pass try: self.cl.deleteVLUN(VOLUME_NAME2, LUN_2, HOST_NAME2, self.port) except Exception: pass try: self.cl.deleteVolume(VOLUME_NAME1) except Exception: pass try: self.cl.deleteVolume(VOLUME_NAME2) except Exception: pass try: self.cl.deleteCPG(CPG_NAME1) except Exception: pass try: self.cl.deleteCPG(CPG_NAME2) except Exception: pass try: self.cl.deleteHost(HOST_NAME1) except Exception: pass try: self.cl.deleteHost(HOST_NAME2) except Exception: pass # very last, tear down base class super(HPE3ParClientVLUNTestCase, self).tearDown() def test_1_create_VLUN(self): self.printHeader('create_VLUN') # add one noVcn = False overrideObjectivePriority = True self.cl.createVLUN(VOLUME_NAME1, LUN_1, HOST_NAME1, self.port, noVcn, overrideObjectivePriority) # check vlun1 = self.cl.getVLUN(VOLUME_NAME1) self.assertIsNotNone(vlun1) volName = vlun1['volumeName'] self.assertEqual(VOLUME_NAME1, volName) # add another self.cl.createVLUN(VOLUME_NAME2, LUN_2, HOST_NAME2) # check vlun2 = self.cl.getVLUN(VOLUME_NAME2) self.assertIsNotNone(vlun2) volName = vlun2['volumeName'] self.assertEqual(VOLUME_NAME2, volName) self.printFooter('create_VLUN') def test_1_create_VLUN_tooLarge(self): self.printHeader('create_VLUN_tooLarge') lun = 100000 self.assertRaises(exceptions.HTTPBadRequest, self.cl.createVLUN, VOLUME_NAME1, lun, HOST_NAME1, self.port) self.printFooter('create_VLUN_tooLarge') def test_1_create_VLUN_volulmeNonExist(self): self.printHeader('create_VLUN_volumeNonExist') self.assertRaises(exceptions.HTTPNotFound, self.cl.createVLUN, 'Some_Volume', LUN_1, HOST_NAME1, self.port) self.printFooter('create_VLUN_volumeNonExist') def test_1_create_VLUN_badParams(self): self.printHeader('create_VLUN_badParams') portPos = {'badNode': 1, 'cardPort': 1, 'slot': 2} self.assertRaises(exceptions.HTTPBadRequest, self.cl.createVLUN, VOLUME_NAME1, LUN_1, HOST_NAME1, portPos) self.printFooter('create_VLUN_badParams') def test_1_create_VLUN_with_id_zero(self): self.printHeader('create_VLUN_with_id_zero') self.cl.createVLUN(VOLUME_NAME1, LUN_0, HOST_NAME1) vlun0 = self.cl.getVLUN(VOLUME_NAME1) self.assertIsNotNone(vlun0) lun = vlun0['lun'] self.assertEqual(LUN_0, lun) volName = vlun0['volumeName'] self.assertEqual(VOLUME_NAME1, volName) self.printFooter('create_VLUN_with_id_zero') def test_2_get_VLUN_bad(self): self.printHeader('get_VLUN_bad') self.assertRaises(exceptions.HTTPNotFound, self.cl.getVLUN, 'badName') self.printFooter('get_VLUN_bad') def test_2_get_VLUNs(self): self.printHeader('get_VLUNs') # add 2 noVcn = False overrideLowerPriority = True self.cl.createVLUN(VOLUME_NAME1, LUN_1, HOST_NAME1, self.port, noVcn, overrideLowerPriority) self.cl.createVLUN(VOLUME_NAME2, LUN_2, HOST_NAME2) # get all vluns = self.cl.getVLUNs() v1 = self.cl.getVLUN(VOLUME_NAME1) v2 = self.cl.getVLUN(VOLUME_NAME2) self.assertTrue(self.findInDict(vluns['members'], 'lun', v1['lun'])) self.assertTrue(self.findInDict(vluns['members'], 'volumeName', v1['volumeName'])) self.assertTrue(self.findInDict(vluns['members'], 'lun', v2['lun'])) self.assertTrue(self.findInDict(vluns['members'], 'volumeName', v2['volumeName'])) self.printFooter('get_VLUNs') def test_3_delete_VLUN_volumeNonExist(self): self.printHeader('delete_VLUN_volumeNonExist') self.cl.createVLUN(VOLUME_NAME1, LUN_1, HOST_NAME1, self.port) self.cl.getVLUN(VOLUME_NAME1) self.assertRaises(exceptions.HTTPNotFound, self.cl.deleteVLUN, 'UnitTestVolume', LUN_1, HOST_NAME1, self.port) self.printFooter('delete_VLUN_volumeNonExist') def test_3_delete_VLUN_hostNonExist(self): self.printHeader('delete_VLUN_hostNonExist') self.cl.createVLUN(VOLUME_NAME1, LUN_1, HOST_NAME1, self.port) self.cl.getVLUN(VOLUME_NAME1) self.assertRaises(exceptions.HTTPNotFound, self.cl.deleteVLUN, VOLUME_NAME1, LUN_1, 'BoggusHost', self.port) self.printFooter('delete_VLUN_hostNonExist') def test_3_delete_VLUN_portNonExist(self): self.printHeader('delete_VLUN_portNonExist') self.cl.createVLUN(VOLUME_NAME2, LUN_2, HOST_NAME2, self.port) self.cl.getVLUN(VOLUME_NAME2) port = {'node': 8, 'cardPort': 8, 'slot': 8} self.assertRaises( exceptions.HTTPBadRequest, self.cl.deleteVLUN, VOLUME_NAME2, LUN_2, HOST_NAME2, port ) self.printFooter("delete_VLUN_portNonExist") def test_3_delete_VLUNs(self): self.printHeader('delete_VLUNs') self.cl.createVLUN(VOLUME_NAME1, LUN_1, HOST_NAME1, self.port) self.cl.getVLUN(VOLUME_NAME1) self.cl.deleteVLUN(VOLUME_NAME1, LUN_1, HOST_NAME1, self.port) self.assertRaises( exceptions.HTTPNotFound, self.cl.deleteVLUN, VOLUME_NAME1, LUN_1, HOST_NAME1, self.port ) self.printFooter('delete_VLUNs') def test_4_get_host_VLUNs(self): self.printHeader('get_host_vluns') self.cl.createVLUN(VOLUME_NAME2, LUN_2, HOST_NAME2) self.cl.createVLUN(VOLUME_NAME1, LUN_1, HOST_NAME2) host_vluns = self.cl.getHostVLUNs(HOST_NAME2) self.assertIn(VOLUME_NAME1, [vlun['volumeName'] for vlun in host_vluns]) self.assertIn(VOLUME_NAME2, [vlun['volumeName'] for vlun in host_vluns]) self.assertIn(LUN_1, [vlun['lun'] for vlun in host_vluns]) self.assertIn(LUN_2, [vlun['lun'] for vlun in host_vluns]) self.printFooter('get_host_vluns') def test_4_get_host_VLUNs_unknown_host(self): self.printHeader('get_host_vluns_unknown_host') self.assertRaises( exceptions.HTTPNotFound, self.cl.getHostVLUNs, 'bogusHost' ) self.printFooter('get_host_vluns_unknown_host') @unittest.skipIf(config['TEST']['unit'].lower() == 'false', "only works with flask server") @mock.patch('hpe3parclient.client.HPE3ParClient.getWsApiVersion') def test_5_get_VLUN_no_query_support(self, mock_version): self.printHeader('get_VLUN_no_query_support') # Mock the version number to a version that does not support # VLUN querying and then remake the client. version = (client.HPE3ParClient .HPE3PAR_WS_MIN_BUILD_VERSION_VLUN_QUERY - 1) mock_version.return_value = {'build': version} self.cl = client.HPE3ParClient(self.flask_url) # Mock the HTTP GET function to track what the call to it was. self.cl.http.get = mock.Mock() self.cl.http.get.return_value = ( {}, {'members': [{'volumeName': VOLUME_NAME1}]} ) self.cl.createVLUN(VOLUME_NAME1, LUN_1, HOST_NAME1) self.cl.getVLUN(VOLUME_NAME1) # Check for the request that happens when VLUN querying is unsupported. self.cl.http.get.assert_has_calls([mock.call('/vluns')]) self.printFooter('get_VLUN_no_query_support') @unittest.skipIf(config['TEST']['unit'].lower() == 'false', "only works with flask server") @mock.patch('hpe3parclient.client.HPE3ParClient.getWsApiVersion') def test_5_get_host_VLUNs_no_query_support(self, mock_version): self.printHeader('get_host_VLUNs_no_query_support') # Mock the version number to a version that does not support # VLUN querying and then remake the client. version = (client.HPE3ParClient .HPE3PAR_WS_MIN_BUILD_VERSION_VLUN_QUERY - 1) mock_version.return_value = {'build': version} self.cl = client.HPE3ParClient(self.flask_url) # Mock the HTTP GET function to track what the call to it was. self.cl.http.get = mock.Mock() self.cl.http.get.return_value = ( {}, {'members': [{'hostname': HOST_NAME1}]} ) self.cl.createVLUN(VOLUME_NAME1, LUN_1, HOST_NAME1) self.cl.getHostVLUNs(HOST_NAME1) # Check for the request that happens when VLUN querying is unsupported. self.cl.http.get.assert_has_calls([mock.call('/vluns')]) self.printFooter('get_host_VLUNs_no_query_support') @unittest.skipIf(config['TEST']['unit'].lower() == 'false', "only works with flask server") @mock.patch('hpe3parclient.client.HPE3ParClient.getWsApiVersion') def test_5_get_VLUN_query_support(self, mock_version): self.printHeader('get_VLUN_query_support') # Mock the version number to a version that supports # VLUN querying and then remake the client. version = client.HPE3ParClient.HPE3PAR_WS_MIN_BUILD_VERSION_VLUN_QUERY mock_version.return_value = {'build': version} self.cl = client.HPE3ParClient(self.flask_url) # Mock the HTTP GET function to track what the call to it was. self.cl.http.get = mock.Mock() self.cl.http.get.return_value = ( {}, {'members': [{'volumeName': VOLUME_NAME1, 'active': True}]} ) self.cl.createVLUN(VOLUME_NAME1, LUN_1, HOST_NAME1) self.cl.getVLUN(VOLUME_NAME1) # Check for the request that happens when VLUN querying is supported. query = '"volumeName EQ %s"' % VOLUME_NAME1 expected_query = '/vluns?query=%s' % quote(query.encode("utf-8")) self.cl.http.get.assert_has_calls([mock.call(expected_query)]) self.printFooter('get_VLUN_query_support') @unittest.skipIf(config['TEST']['unit'].lower() == 'false', "only works with flask server") @mock.patch('hpe3parclient.client.HPE3ParClient.getWsApiVersion') def test_5_get_host_VLUNs_query_support(self, mock_version): self.printHeader('get_host_VLUNs_query_support') # Mock the version number to a version that supports # VLUN querying and then remake the client. version = client.HPE3ParClient.HPE3PAR_WS_MIN_BUILD_VERSION_VLUN_QUERY mock_version.return_value = {'build': version} self.cl = client.HPE3ParClient(self.flask_url) # Mock the HTTP GET function to track what the call to it was. self.cl.http.get = mock.Mock() self.cl.http.get.return_value = ( {}, {'members': [{'hostname': HOST_NAME1, 'active': True}]} ) self.cl.createVLUN(VOLUME_NAME1, LUN_1, HOST_NAME1) self.cl.getHostVLUNs(HOST_NAME1) # Check for the request that happens when VLUN querying is supported. query = '"hostname EQ %s"' % HOST_NAME1 expected_query = '/vluns?query=%s' % quote(query.encode("utf-8")) self.cl.http.get.assert_has_calls([mock.call(expected_query)]) self.printFooter('get_host_VLUNs_query_support') # testing # suite = unittest.TestLoader().loadTestsFromTestCase( # HPE3ParClientVLUNTestCase) # unittest.TextTestRunner(verbosity=2).run(suite) python-3parclient-4.2.12/test/test_HPE3ParClient_host.py0000644000000000000000000005044414106434774023070 0ustar rootroot00000000000000# (c) Copyright 2015 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Test class of 3PAR Client handling Host.""" from test import HPE3ParClient_base as hpe3parbase from hpe3parclient import exceptions # Insert colons into time string to match WWN format. TIME2 = "" for i in range(6): if i % 2 == 0: TIME2 += ":" + hpe3parbase.TIME[i] else: TIME2 += hpe3parbase.TIME[i] DOMAIN = 'UNIT_TEST_DOMAIN' HOST_NAME1 = 'HOST1_UNIT_TEST' + hpe3parbase.TIME HOST_NAME2 = 'HOST2_UNIT_TEST' + hpe3parbase.TIME WWN1 = "00:00:00:00:00" + TIME2 WWN2 = "11:11:11:11:11" + TIME2 IQN1 = 'iqn.1993-08.org.debian:01:00000' + hpe3parbase.TIME IQN2 = 'iqn.bogus.org.debian:01:0000' + hpe3parbase.TIME class HPE3ParClientHostTestCase(hpe3parbase.HPE3ParClientBaseTestCase): def setUp(self): super(HPE3ParClientHostTestCase, self).setUp(withSSH=True) def tearDown(self): try: self.cl.deleteHost(HOST_NAME1) except Exception: pass try: self.cl.deleteHost(HOST_NAME2) except Exception: pass # very last, tear down base class super(HPE3ParClientHostTestCase, self).tearDown() def test_1_create_host_badParams(self): self.printHeader('create_host_badParams') name = 'UnitTestHostBadParams' optional = {'iSCSIPaths': 'foo bar'} self.assertRaises(exceptions.HTTPBadRequest, self.cl.createHost, name, None, None, optional) self.printFooter('create_host_badParams') def test_1_create_host_no_name(self): self.printHeader('create_host_no_name') optional = {'domain': 'default'} self.assertRaises(exceptions.HTTPBadRequest, self.cl.createHost, None, None, None, optional) self.printFooter('create_host_no_name') def test_1_create_host_exceed_length(self): self.printHeader('create_host_exceed_length') optional = {'domain': 'ThisDomainNameIsWayTooLongToMakeAnySense'} self.assertRaises(exceptions.HTTPBadRequest, self.cl.createHost, HOST_NAME1, None, None, optional) self.printFooter('create_host_exceed_length') def test_1_create_host_empty_domain(self): self.printHeader('create_host_empty_domain') optional = {'domain': ''} self.assertRaises(exceptions.HTTPBadRequest, self.cl.createHost, HOST_NAME1, None, None, optional) self.printFooter('create_host_empty_domain') def test_1_create_host_illegal_string(self): self.printHeader('create_host_illegal_string') optional = {'domain': 'doma&n'} self.assertRaises(exceptions.HTTPBadRequest, self.cl.createHost, HOST_NAME1, None, None, optional) self.printFooter('create_host_illegal_string') def test_1_create_host_param_conflict(self): self.printHeader('create_host_param_conflict') optional = {'domain': DOMAIN} fc = [WWN1, WWN2] iscsi = [IQN1, IQN2] self.assertRaises(exceptions.HTTPBadRequest, self.cl.createHost, HOST_NAME1, iscsi, fc, optional) self.printFooter('create_host_param_conflict') def test_1_create_host_wrong_type(self): self.printHeader('create_host_wrong_type') optional = {'domain': self.DOMAIN} fc = ['00:00:00:00:00:00:00'] self.assertRaises(exceptions.HTTPBadRequest, self.cl.createHost, HOST_NAME1, None, fc, optional) self.printFooter('create_host_wrong_type') def test_1_create_host_existent_path(self): self.printHeader('create_host_existent_path') optional = {'domain': DOMAIN} fc = [WWN1, WWN2] self.cl.createHost(HOST_NAME1, None, fc, optional) self.assertRaises(exceptions.HTTPConflict, self.cl.createHost, HOST_NAME2, None, fc, optional) self.printFooter('create_host_existent_path') def test_1_create_host_duplicate(self): self.printHeader('create_host_duplicate') optional = {'domain': self.DOMAIN} self.cl.createHost(HOST_NAME1, None, None, optional) self.assertRaises(exceptions.HTTPConflict, self.cl.createHost, HOST_NAME1, None, None, optional) self.printFooter('create_host_duplicate') def test_1_create_host(self): self.printHeader('create_host') # add one optional = {'domain': DOMAIN} fc = [WWN1, WWN2] self.cl.createHost(HOST_NAME1, None, fc, optional) # check host1 = self.cl.getHost(HOST_NAME1) self.assertIsNotNone(host1) name1 = host1['name'] self.assertEqual(HOST_NAME1, name1) # add another iscsi = [IQN1, IQN2] self.cl.createHost(HOST_NAME2, iscsi, None, optional) # check host2 = self.cl.getHost(HOST_NAME2) self.assertIsNotNone(host2) name3 = host2['name'] self.assertEqual(HOST_NAME2, name3) self.printFooter('create_host') def test_1_create_host_no_optional(self): self.printHeader('create_host_no_optional') # add one fc = [WWN1, WWN2] self.cl.createHost(HOST_NAME1, None, fc) # check host1 = self.cl.getHost(HOST_NAME1) self.assertIsNotNone(host1) name1 = host1['name'] self.assertEqual(HOST_NAME1, name1) self.printFooter('create_host_no_optional') def test_2_delete_host_nonExist(self): self.printHeader("delete_host_non_exist") self.assertRaises(exceptions.HTTPNotFound, self.cl.deleteHost, "UnitTestNonExistHost") self.printFooter("delete_host_non_exist") def test_2_delete_host(self): self.printHeader("delete_host") optional = {'domain': DOMAIN} fc = [WWN1, WWN2] self.cl.createHost(HOST_NAME1, None, fc, optional) # check host1 = self.cl.getHost(HOST_NAME1) self.assertIsNotNone(host1) hosts = self.cl.getHosts() if hosts and hosts['total'] > 0: for host in hosts['members']: if 'name' in host and host['name'] == HOST_NAME1: self.cl.deleteHost(host['name']) self.assertRaises(exceptions.HTTPNotFound, self.cl.getHost, HOST_NAME1) self.printFooter("delete_host") def test_3_get_host_bad(self): self.printHeader("get_host_bad") self.assertRaises(exceptions.HTTPNotFound, self.cl.getHost, "BadHostName") self.printFooter("get_host_bad") def test_3_get_host_illegal(self): self.printHeader("get_host_illegal") self.assertRaises(exceptions.HTTPBadRequest, self.cl.getHost, "B&dHostName") self.printFooter("get_host_illegal") def test_3_get_hosts(self): self.printHeader("get_hosts") optional = {'domain': DOMAIN} fc = [WWN1, WWN2] self.cl.createHost(HOST_NAME1, None, fc, optional) iscsi = [IQN1, IQN2] self.cl.createHost(HOST_NAME2, iscsi, None, optional) hosts = self.cl.getHosts() self.assertGreaterEqual(hosts['total'], 2) host_names = [host['name'] for host in hosts['members'] if 'name' in host] self.assertIn(HOST_NAME1, host_names) self.assertIn(HOST_NAME2, host_names) def test_3_get_host(self): self.printHeader("get_host") self.assertRaises(exceptions.HTTPNotFound, self.cl.getHost, HOST_NAME1) optional = {'domain': DOMAIN} fc = [WWN1, WWN2] self.cl.createHost(HOST_NAME1, None, fc, optional) host1 = self.cl.getHost(HOST_NAME1) self.assertEqual(host1['name'], HOST_NAME1) self.printFooter('get_host') def test_4_modify_host(self): self.printHeader('modify_host') self.assertRaises(exceptions.HTTPNotFound, self.cl.getHost, HOST_NAME1) self.assertRaises(exceptions.HTTPNotFound, self.cl.getHost, HOST_NAME2) optional = {'domain': DOMAIN} fc = [WWN1, WWN2] self.cl.createHost(HOST_NAME1, None, fc, optional) # validate host was created host1 = self.cl.getHost(HOST_NAME1) self.assertEqual(host1['name'], HOST_NAME1) # change host name mod_request = {'newName': HOST_NAME2} self.cl.modifyHost(HOST_NAME1, mod_request) # validate host name was changed host2 = self.cl.getHost(HOST_NAME2) self.assertEqual(host2['name'], HOST_NAME2) # host 1 name should be history self.assertRaises(exceptions.HTTPNotFound, self.cl.getHost, HOST_NAME1) self.printFooter('modfiy_host') def test_4_modify_host_no_name(self): self.printHeader('modify_host_no_name') mod_request = {'newName': HOST_NAME1} self.assertRaises( exceptions.HTTPNotFound, self.cl.modifyHost, None, mod_request ) self.printFooter('modify_host_no_name') def test_4_modify_host_param_conflict(self): self.printHeader('modify_host_param_conflict') fc = [WWN1, WWN2] iscsi = [IQN1, IQN2] mod_request = {'newName': HOST_NAME1, 'FCWWNs': fc, 'iSCSINames': iscsi} self.assertRaises( exceptions.HTTPBadRequest, self.cl.modifyHost, HOST_NAME2, mod_request ) self.printFooter('modify_host_param_conflict') def test_4_modify_host_illegal_char(self): self.printHeader('modify_host_illegal_char') mod_request = {'newName': 'New#O$TN@ME'} self.assertRaises( exceptions.HTTPBadRequest, self.cl.modifyHost, HOST_NAME2, mod_request ) self.printFooter('modify_host_illegal_char') def test_4_modify_host_pathOperation_missing1(self): self.printHeader('modify_host_pathOperation_missing1') fc = [WWN1, WWN2] mod_request = {'FCWWNs': fc} self.assertRaises( exceptions.HTTPBadRequest, self.cl.modifyHost, HOST_NAME1, mod_request ) self.printFooter('modify_host_pathOperation_missing1') def test_4_modify_host_pathOperation_missing2(self): self.printHeader('modify_host_pathOperation_missing2') iscsi = [IQN1, IQN2] mod_request = {'iSCSINames': iscsi} self.assertRaises( exceptions.HTTPBadRequest, self.cl.modifyHost, HOST_NAME1, mod_request ) self.printFooter('modify_host_pathOperation_missing2') def test_4_modify_host_pathOperationOnly(self): self.printHeader('modify_host_pathOperationOnly') mod_request = {'pathOperation': 1} self.assertRaises( exceptions.HTTPBadRequest, self.cl.modifyHost, HOST_NAME2, mod_request ) self.printFooter('modify_host_pathOperationOnly') def test_4_modify_host_too_long(self): self.printHeader('modify_host_too_long') optional = {'domain': DOMAIN} fc = [WWN1, WWN2] self.cl.createHost(HOST_NAME1, None, fc, optional) mod_request = {'newName': 'ThisHostNameIsWayTooLongToMakeAnyRealSense' 'AndIsDeliberatelySo'} self.assertRaises( exceptions.HTTPBadRequest, self.cl.modifyHost, HOST_NAME1, mod_request ) self.printFooter('modify_host_too_long') def test_4_modify_host_dup_newName(self): self.printHeader('modify_host_dup_newName') optional = {'domain': DOMAIN} fc = [WWN1, WWN2] self.cl.createHost(HOST_NAME1, None, fc, optional) iscsi = [IQN1, IQN2] self.cl.createHost(HOST_NAME2, iscsi, None, optional) mod_request = {'newName': HOST_NAME1} self.assertRaises( exceptions.HTTPConflict, self.cl.modifyHost, HOST_NAME2, mod_request ) self.printFooter('modify_host_dup_newName') def test_4_modify_host_nonExist(self): self.printHeader('modify_host_nonExist') mod_request = {'newName': HOST_NAME2} self.assertRaises( exceptions.HTTPNotFound, self.cl.modifyHost, HOST_NAME1, mod_request ) self.printFooter('modify_host_nonExist') def test_4_modify_host_existent_path(self): self.printHeader('modify_host_existent_path') optional = {'domain': DOMAIN} fc = [WWN1, WWN2] iscsi = [IQN1, IQN2] self.cl.createHost(HOST_NAME1, None, fc, optional) self.cl.createHost(HOST_NAME2, iscsi, None, optional) mod_request = {'pathOperation': 1, 'iSCSINames': iscsi} self.assertRaises( exceptions.HTTPConflict, self.cl.modifyHost, HOST_NAME1, mod_request ) self.printFooter('modify_host_existent_path') def test_4_modify_host_nonExistent_path_iSCSI(self): self.printHeader('modify_host_nonExistent_path_iSCSI') optional = {'domain': DOMAIN} iscsi = [IQN1] self.cl.createHost(HOST_NAME1, iscsi, None, optional) iscsi2 = [IQN2] mod_request = {'pathOperation': 2, 'iSCSINames': iscsi2} self.assertRaises( exceptions.HTTPNotFound, self.cl.modifyHost, HOST_NAME1, mod_request ) self.printFooter('modify_host_nonExistent_path_iSCSI') def test_4_modify_host_nonExistent_path_fc(self): self.printHeader('modify_host_nonExistent_path_fc') optional = {'domain': DOMAIN} fc = [WWN1] self.cl.createHost(HOST_NAME1, None, fc, optional) fc2 = [WWN2] mod_request = {'pathOperation': 2, 'FCWWNs': fc2} self.assertRaises( exceptions.HTTPNotFound, self.cl.modifyHost, HOST_NAME1, mod_request ) self.printFooter('modify_host_nonExistent_path_fc') def test_4_modify_host_add_fc(self): self.printHeader('modify_host_fc') optional = {'domain': DOMAIN} fc = [WWN1] self.cl.createHost(HOST_NAME1, None, fc, optional) fc2 = [WWN2] mod_request = {'pathOperation': 1, 'FCWWNs': fc2} self.cl.modifyHost(HOST_NAME1, mod_request) newHost = self.cl.getHost(HOST_NAME1) fc_paths = newHost['FCPaths'] for path in fc_paths: if path['wwn'] == WWN2.replace(':', ''): self.printFooter('modify_host_add_fc') return self.fail('Failed to add FCWWN') def test_4_modify_host_remove_fc(self): self.printHeader('modify_host_remove_fc') optional = {'domain': DOMAIN} fc = [WWN1] self.cl.createHost(HOST_NAME1, None, fc, optional) mod_request = {'pathOperation': 2, 'FCWWNs': fc} self.cl.modifyHost(HOST_NAME1, mod_request) newHost = self.cl.getHost(HOST_NAME1) fc_paths = newHost['FCPaths'] for path in fc_paths: if path['wwn'] == WWN1.replace(':', ''): self.fail('Failed to remove FCWWN') return self.printFooter('modify_host_remove_fc') def test_4_modify_host_add_iscsi(self): self.printHeader('modify_host_add_iscsi') optional = {'domain': DOMAIN} iscsi = [IQN1] self.cl.createHost(HOST_NAME1, iscsi, None, optional) iscsi2 = [IQN2] mod_request = {'pathOperation': 1, 'iSCSINames': iscsi2} self.cl.modifyHost(HOST_NAME1, mod_request) newHost = self.cl.getHost(HOST_NAME1) iscsi_paths = newHost['iSCSIPaths'] for path in iscsi_paths: print(path) if path['name'] == IQN2: self.printFooter('modify_host_add_iscsi') return self.fail('Failed to add iSCSI') def test_4_modify_host_remove_iscsi(self): self.printHeader('modify_host_remove_iscsi') optional = {'domain': DOMAIN} iscsi = [IQN1] self.cl.createHost(HOST_NAME1, iscsi, None, optional) mod_request = {'pathOperation': 2, 'iSCSINames': iscsi} self.cl.modifyHost(HOST_NAME1, mod_request) newHost = self.cl.getHost(HOST_NAME1) iscsi_paths = newHost['iSCSIPaths'] for path in iscsi_paths: if path['name'] == IQN2: self.fail('Failed to remove iSCSI') return self.printFooter('modify_host_remove_iscsi') def test_5_query_host_iqn(self): self.printHeader('query_host_iqn') optional = {'domain': DOMAIN} iscsi = [IQN1] self.cl.createHost(HOST_NAME1, iscsi, None, optional) hosts = self.cl.queryHost(iqns=[iscsi.pop()]) self.assertIsNotNone(hosts) self.assertEqual(1, hosts['total']) self.assertEqual(hosts['members'].pop()['name'], HOST_NAME1) self.printFooter('query_host_iqn') def test_5_query_host_iqn2(self): # TODO test multiple iqns in one query pass def test_5_query_host_wwn(self): self.printHeader('query_host_wwn') optional = {'domain': DOMAIN} fc = [WWN1] self.cl.createHost(HOST_NAME1, None, fc, optional) hosts = self.cl.queryHost(wwns=[fc.pop().replace(':', '')]) self.assertIsNotNone(hosts) self.assertEqual(1, hosts['total']) self.assertEqual(hosts['members'].pop()['name'], HOST_NAME1) self.printFooter('query_host_wwn') def test_5_query_host_wwn2(self): # TODO test multiple wwns in one query pass def test_5_query_host_iqn_and_wwn(self): self.printHeader('query_host_iqn_and_wwn') optional = {'domain': DOMAIN} iscsi = [IQN1] self.cl.createHost(HOST_NAME1, iscsi, None, optional) fc = [WWN1] self.cl.createHost(HOST_NAME2, None, fc, optional) hosts = self.cl.queryHost(iqns=[IQN1], wwns=[WWN1.replace(':', '')]) self.assertIsNotNone(hosts) self.assertEqual(2, hosts['total']) self.assertIn(HOST_NAME1, [host['name'] for host in hosts['members']]) self.assertIn(HOST_NAME2, [host['name'] for host in hosts['members']]) self.printFooter('query_host_iqn_and_wwn') def test_6_find_host(self): self.printHeader('find_host_wwn') hosts = self.cl.findHost(wwn=WWN1) if hosts is not None: # host found self.assertIsNotNone(hosts) else: # host not found self.assertIsNone(hosts) self.printFooter('find_host_wwn') python-3parclient-4.2.12/test/test_HPE3ParClient_ports.py0000644000000000000000000001237614106434774023264 0ustar rootroot00000000000000# (c) Copyright 2015 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Test class of 3PAR Client handling Ports.""" from test import HPE3ParClient_base as hpe3parbase iscsi_ports = [{ 'IPAddr': '192.168.1.1', 'portPos': { 'node': 0, 'slot': 2, 'cardPort': 1 }, 'iSCSIPortInfo': { 'iSNSAddr': '0.0.0.0', 'vlan': '100', 'IPAddr': '192.168.1.1', 'mtu': 1500, 'stgt': 21, 'netmask': '255.255.192.0', 'tpgt': 1024, 'iSNSPort': 3205, 'gateway': '0.0.0.0' } }] body = { u'total': 14, u'members': [{ u'portPos': { u'node': 0, u'slot': 2, u'cardPort': 1 }, u'protocol': 2, u'iSCSIPortInfo': { u'iSNSAddr': u'0.0.0.0', u'vlan': 1, u'IPAddr': u'0.0.0.0', u'rate': u'10Gbps', u'mtu': 1500, u'stgt': 21, u'netmask': u'0.0.0.0', u'iSCSIName': u'iqn.2000-05.com.3pardata:20210002ac01db31', u'tpgt': 21, u'iSNSPort': 3205, u'gateway': u'0.0.0.0' }, u'partnerPos': { u'node': 1, u'slot': 2, u'cardPort': 1 }, u'IPAddr': u'0.0.0.0', u'linkState': 4, u'device': [ ], u'iSCSIName': u'iqn.2000-05.com.3pardata:20210002ac01db31', u'failoverState': 1, u'mode': 2, u'HWAddr': u'70106FCE921A', u'type': 8, u'iSCSIVlans': [ { u'iSNSAddr': u'0.0.0.0', u'IPAddr': u'192.168.1.1', u'mtu': 1500, u'stgt': 21, u'netmask': u'255.255.192.0', u'tpgt': 1024, u'iSNSPort': 3205, u'gateway': u'0.0.0.0', u'vlanTag': 100 } ] }] } user = "u" password = "p" ip = "10.10.22.241" class HPE3ParClientPortTestCase(hpe3parbase.HPE3ParClientBaseTestCase): def setUp(self): super(HPE3ParClientPortTestCase, self).setUp() def tearDown(self): super(HPE3ParClientPortTestCase, self).tearDown() def test_get_ports_all(self): self.printHeader('get_ports_all') ports = self.cl.getPorts() if ports: if len(ports['members']) == ports['total']: self.printFooter('get_ports_all') return else: self.fail('Number of ports in invalid.') else: self.fail('Cannot retrieve ports.') def test_cloned_iscsi_ports(self): self.printHeader('get_vlan_tagged_iscsi_port_info') iscsi_port = self.cl._cloneISCSIPorts(body, iscsi_ports) if iscsi_port: if iscsi_port[0]['iSCSIPortInfo']['IPAddr'] ==\ iscsi_ports[0]['IPAddr']: self.printFooter('get_vlan_tagged_iscsi_port_info') return else: self.fail('iSCSIVlan IPAddr is not found') else: self.fail('Cannot retrieve ports') def test_get_ports_ssh(self): self.printHeader('get_ports_cli') self.cl.setSSHOptions(ip, user, password) ports = self.cl.getPorts() if ports: if len(ports['members']) == ports['total']: self.printFooter('get_ports_cli') return else: self.fail('Number of ports is invalid.') else: self.fail('Cannot retrieve ports.') def test_get_ports_fc(self): self.printHeader('get_ports_fc') fc_ports = self.cl.getFCPorts(4) print(fc_ports) if fc_ports: for port in fc_ports: if port['protocol'] != 1: self.fail('Non-FC ports detected.') self.printFooter('get_ports_fc') return else: self.fail('Cannot retrieve FC ports.') def test_get_ports_iscsi(self): self.printHeader('get_ports_iscsi') iscsi = self.cl.getiSCSIPorts(4) if iscsi: for port in iscsi: if port['protocol'] != 2: self.fail('Non-iSCSI ports detected.') self.printFooter('get_ports_iscsi') return else: self.fail('Cannot retrieve iSCSI Ports.') def test_get_ports_ip(self): self.printHeader('get_ports_ip') ip = self.cl.getIPPorts() if ip: for port in ip: if port['protocol'] != 4: self.fail('non-ip ports detected.') self.printFooter('get_ports_ip') else: self.fail('cannot retrieve ip ports.') python-3parclient-4.2.12/test/test_HPE3ParClient_retry.py0000644000000000000000000000654214106434774023260 0ustar rootroot00000000000000# (c) Copyright 2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Test class of 3PAR Client handling WSAPI retries.""" import importlib import mock import requests from test import HPE3ParClient_base as hpe3parbase from hpe3parclient import exceptions class HPE3ParClientRetryTestCase(hpe3parbase.HPE3ParClientBaseTestCase): def setUp(self): super(HPE3ParClientRetryTestCase, self).setUp() def tearDown(self): # NOTE(aorourke): We mock out the requests library's request method in # order to force exceptions so we can test retry attempts. By doing # this, we completely destroy the functionaility of requests. # Therefore, after every unit test we run, we need to reimport the # library to restore proper functionality or all future tests will # fail. In Python 2.7 we must use the built in reload() method while # in Python 3.4 we must use importlib.reload(). try: reload(requests) except NameError: importlib.reload(requests) super(HPE3ParClientRetryTestCase, self).tearDown() def test_retry_exhaust_all_attempts_service_unavailable(self): http = self.cl.http # There should be 5 tries before anything is called. self.assertEqual(http.tries, 5) # The requests object needs to raise an exception in order for us # to test the retry functionality. requests.request = mock.Mock() requests.request.side_effect = exceptions.HTTPServiceUnavailable( "Maximum number of WSAPI connections reached.") # This will take ~30 seconds to fail. self.assertRaises( exceptions.HTTPServiceUnavailable, http.get, '/volumes') # There should be 0 tries left after the call. self.assertEqual(http.tries, 0) def test_retry_exhaust_all_attempts_connection_error(self): http = self.cl.http # There should be 5 tries before anything is called. self.assertEqual(http.tries, 5) # The requests object needs to raise an exception in order for us # to test the retry functionality. requests.request = mock.Mock() requests.request.side_effect = requests.exceptions.ConnectionError( "There was a connection error.") # This will take ~30 seconds to fail. self.assertRaises( requests.exceptions.ConnectionError, http.get, '/volumes') # There should be 0 tries left after the call. self.assertEqual(http.tries, 0) def test_no_retry(self): http = self.cl.http # There should be 5 tries before anything is called. self.assertEqual(http.tries, 5) http.get('/volumes') # There should be 5 tries left after the call. self.assertEqual(http.tries, 5) python-3parclient-4.2.12/test/test_HPE3ParClient_system.py0000644000000000000000000001144614106434774023436 0ustar rootroot00000000000000# (c) Copyright 2015 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Test class of 3PAR Client System Level APIS.""" from pytest_testconfig import config import unittest from test import HPE3ParClient_base as hpe3parbase from hpe3parclient import exceptions class HPE3ParClientSystemTestCase(hpe3parbase.HPE3ParClientBaseTestCase): def setUp(self): super(HPE3ParClientSystemTestCase, self).setUp(withSSH=True) def tearDown(self): # very last, tear down base class super(HPE3ParClientSystemTestCase, self).tearDown() def test_get_patch(self): """This can work with or without a patch, but you need to manually enter a valid one or use the bogus one. """ self.printHeader('get_patch') # actual patch you might be able to test with somewhere: # patch_id = 'P16' # # bogus patch name that should consistently be not recognized: patch_id = 'P16-BOGUS' result = self.cl.getPatch(patch_id) self.assertIsNotNone(result) if len(result) > 1: # found patch test results self.assertGreater(len(result), 1) self.assertTrue("Patch detail info for " + patch_id in result[0]) else: # bogus/not-found patch test results self.assertEqual(len(result), 1) self.assertTrue("Patch " + patch_id + " not recognized" in result[0]) self.printFooter('get_patch') @unittest.skipIf(config['TEST']['unit'].lower() == 'true', "only works with real array") def test_get_patches(self): """This test includes history (not just patches), so it should always have results. """ self.printHeader('get_patches') result = self.cl.getPatches() self.assertIsNotNone(result) self.assertGreater(result['total'], 0) self.assertGreater(len(result['members']), 0) self.printFooter('get_patches') @unittest.skipIf(config['TEST']['unit'].lower() == 'true', "only works with real array") def test_get_patches_no_hist(self): """This test expects to find no patches installed (typical in our test environment). """ self.printHeader('get_patches') result = self.cl.getPatches(history=False) self.assertIsNotNone(result) self.assertEqual(result['total'], 0) self.assertEqual(len(result['members']), 0) self.printFooter('get_patches') def test_getStorageSystemInfo(self): self.printHeader('getStorageSystemInfo') info = self.cl.getStorageSystemInfo() self.assertIsNotNone(info) self.printFooter('getStorageSystemInfo') def test_getWSAPIConfigurationInfo(self): self.printHeader('getWSAPIConfigurationInfo') info = self.cl.getWSAPIConfigurationInfo() self.assertIsNotNone(info) self.printFooter('getWSAPIConfigurationInfo') def test_query_task(self): self.printHeader("query_task") tasks = self.cl.getAllTasks() self.assertIsNotNone(tasks) self.assertGreater(tasks['total'], 0) first_task = tasks['members'].pop() self.assertIsNotNone(first_task) task = self.cl.getTask(first_task['id']) self.assertEqual(first_task, task) self.printFooter("query_task") def test_query_task_negative(self): self.printHeader("query_task_negative") self.assertRaises( exceptions.HTTPBadRequest, self.cl.getTask, -1 ) self.printFooter("query_task_negative") def test_query_task_non_int(self): self.printHeader("query_task_non_int") self.assertRaises( exceptions.HTTPBadRequest, self.cl.getTask, "nonIntTask" ) self.printFooter("query_task_non_int") def test_get_overall_system_capacity(self): self.printHeader("get_overall_system_capacity") capacity = self.cl.getOverallSystemCapacity() self.assertIsNotNone(capacity) self.printFooter("get_overall_system_capacity") # testing # suite = unittest.TestLoader(). # loadTestsFromTestCase(HPE3ParClientSystemTestCase) # unittest.TextTestRunner(verbosity=2).run(suite) python-3parclient-4.2.12/test/test_HPE3ParClient_volume.py0000644000000000000000000031670014106434774023422 0ustar rootroot00000000000000# (c) Copyright 2015-2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Test class of 3PAR Client handling volume & snapshot.""" import time import unittest from pytest_testconfig import config from test import HPE3ParClient_base as hpe3parbase from hpe3parclient import exceptions CPG_NAME1 = 'CPG1_UNIT_TEST' + hpe3parbase.TIME CPG_NAME2 = 'CPG2_UNIT_TEST' + hpe3parbase.TIME VOLUME_NAME1 = 'VOLUME1_UNIT_TEST' + hpe3parbase.TIME VOLUME_NAME2 = 'VOLUME2_UNIT_TEST' + hpe3parbase.TIME VOLUME_NAME3 = 'VOLUME3_UNIT_TEST' + hpe3parbase.TIME SNAP_NAME1 = 'SNAP_UNIT_TEST1' + hpe3parbase.TIME SNAP_NAME2 = 'SNAP_UNIT_TEST2' + hpe3parbase.TIME DOMAIN = 'UNIT_TEST_DOMAIN' VOLUME_SET_NAME1 = 'VOLUME_SET1_UNIT_TEST' + hpe3parbase.TIME VOLUME_SET_NAME2 = 'VOLUME_SET2_UNIT_TEST' + hpe3parbase.TIME VOLUME_SET_NAME3 = 'VOLUME_SET3_UNIT_TEST' + hpe3parbase.TIME VOLUME_SET_NAME4 = 'VSET_' + hpe3parbase.TIME SIZE = 512 REMOTE_COPY_GROUP_NAME1 = 'RCG1_UNIT_TEST' + hpe3parbase.TIME REMOTE_COPY_GROUP_NAME2 = 'RCG2_UNIT_TEST' + hpe3parbase.TIME REMOTE_COPY_TARGETS = [{"targetName": "testTarget", "mode": 2, "roleReversed": False, "groupLastSyncTime": None}] RC_VOLUME_NAME = 'RC_VOLUME1_UNIT_TEST' + hpe3parbase.TIME SCHEDULE_NAME1 = 'SCHEDULE_NAME1' + hpe3parbase.TIME SCHEDULE_NAME2 = 'SCHEDULE_NAME2' + hpe3parbase.TIME SKIP_RCOPY_MESSAGE = ("Only works with flask server.") SKIP_FLASK_RCOPY_MESSAGE = ("Remote copy is not configured to be tested " "on live arrays.") TARGET_NAME = 'testtarget' SOURCE_PORT = '1:1:1' TARGET_PORT = '10.10.10.1' RCOPY_STARTED = 3 RCOPY_STOPPED = 5 FAILOVER_GROUP = 7 RESTORE_GROUP = 10 MODE = 'sync' TPVV = 1 FPVV = 2 TDVV = 3 CONVERT_TO_DECO = 4 INVALID_PROVISIONING_TYPE = 5 USR_CPG = 1 INVALID_CPG = 3 VOLUME_PAIR_LIST = {'volumePairs': [{'sourceVolumeName': 'primary_vol1', 'targetVolumeName': 'secondary_vol1'}, {'sourceVolumeName': 'primary_vol2', 'targetVolumeName': 'secondary_vol2'}]} def is_live_test(): return config['TEST']['unit'].lower() == 'false' def no_remote_copy(): unit_test = config['TEST']['unit'].lower() == 'false' remote_copy = config['TEST']['run_remote_copy'].lower() == 'true' run_remote_copy = not remote_copy or not unit_test return run_remote_copy class HPE3ParClientVolumeTestCase(hpe3parbase.HPE3ParClientBaseTestCase): def setUp(self): super(HPE3ParClientVolumeTestCase, self).setUp(withSSH=True) optional = self.CPG_OPTIONS try: self.cl.createCPG(CPG_NAME1, optional) except Exception: pass try: self.cl.createCPG(CPG_NAME2, optional) except Exception: pass try: self.secondary_cl.createCPG(CPG_NAME1, optional) except Exception: pass try: self.secondary_cl.createCPG(CPG_NAME2, optional) except Exception: pass def tearDown(self): try: self.cl.deleteVolume(SNAP_NAME1) except Exception: pass try: self.cl.deleteVolume(SNAP_NAME2) except Exception: pass try: self.cl.deleteVolumeSet(VOLUME_SET_NAME1) except Exception: pass try: self.cl.deleteVolumeSet(VOLUME_SET_NAME2) except Exception: pass try: self.cl.deleteVolumeSet(VOLUME_SET_NAME3) except Exception: pass try: self.cl.deleteVolume(VOLUME_NAME1) except Exception: pass try: self.cl.deleteVolume(VOLUME_NAME2) except Exception: pass try: self.cl.deleteVolume(VOLUME_NAME3) except Exception: pass try: self.cl.deleteCPG(CPG_NAME1) except Exception: pass try: self.cl.deleteCPG(CPG_NAME2) except Exception: pass try: self.cl.stopRemoteCopy(REMOTE_COPY_GROUP_NAME1) except Exception: pass try: self.cl.removeVolumeFromRemoteCopyGroup( REMOTE_COPY_GROUP_NAME1, RC_VOLUME_NAME, removeFromTarget=True, useHttpDelete=False) except Exception: pass try: self.cl.removeRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) except Exception: pass try: self.cl.deleteVolume(RC_VOLUME_NAME) except Exception: pass try: self.secondary_cl.deleteVolume(RC_VOLUME_NAME) except Exception: pass try: self.secondary_cl.deleteCPG(CPG_NAME1) except Exception: pass try: self.secondary_cl.deleteCPG(CPG_NAME2) except Exception: pass super(HPE3ParClientVolumeTestCase, self).tearDown() def test_1_create_volume(self): self.printHeader('create_volume') # add one optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) # check vol1 = self.cl.getVolume(VOLUME_NAME1) self.assertIsNotNone(vol1) volName = vol1['name'] self.assertEqual(VOLUME_NAME1, volName) # add another optional = {'comment': 'test volume2', 'tpvv': True} self.cl.createVolume(VOLUME_NAME2, CPG_NAME2, SIZE, optional) # check vol2 = self.cl.getVolume(VOLUME_NAME2) self.assertIsNotNone(vol2) volName = vol2['name'] comment = vol2['comment'] self.assertEqual(VOLUME_NAME2, volName) self.assertEqual("test volume2", comment) self.printFooter('create_volume') def test_1_create_volume_badParams(self): self.printHeader('create_volume_badParams') name = VOLUME_NAME1 cpgName = CPG_NAME1 optional = {'id': 4, 'comment': 'test volume', 'badPram': True} self.assertRaises( exceptions.HTTPBadRequest, self.cl.createVolume, name, cpgName, SIZE, optional) self.printFooter('create_volume_badParams') def test_1_create_volume_duplicate_name(self): self.printHeader('create_volume_duplicate_name') # add one and check optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) self.assertRaises( exceptions.HTTPConflict, self.cl.createVolume, VOLUME_NAME1, CPG_NAME2, SIZE, optional ) self.printFooter('create_volume_duplicate_name') def test_1_create_volume_tooLarge(self): self.printHeader('create_volume_tooLarge') optional = {'id': 3, 'comment': 'test volume', 'tpvv': True} self.assertRaises( exceptions.HTTPBadRequest, self.cl.createVolume, VOLUME_NAME1, CPG_NAME1, 16777218, optional ) self.printFooter('create_volume_tooLarge') def test_1_create_volume_duplicate_ID(self): self.printHeader('create_volume_duplicate_ID') optional = {'id': 10000, 'comment': 'first volume'} optional2 = {'id': 10000, 'comment': 'volume with duplicate ID'} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) self.assertRaises( exceptions.HTTPConflict, self.cl.createVolume, VOLUME_NAME2, CPG_NAME2, SIZE, optional2 ) self.printFooter('create_volume_duplicate_ID') def test_1_create_volume_longName(self): self.printHeader('create_volume_longName') optional = {'id': 5} LongName = ('ThisVolumeNameIsWayTooLongToMakeAnySenseAndIs' 'DeliberatelySo') self.assertRaises( exceptions.HTTPBadRequest, self.cl.createVolume, LongName, CPG_NAME1, SIZE, optional ) self.printFooter('create_volume_longName') def test_2_get_volume_bad(self): self.printHeader('get_volume_bad') self.assertRaises( exceptions.HTTPNotFound, self.cl.getVolume, 'NoSuchVolume' ) self.printFooter('get_volume_bad') def test_2_get_volumes(self): self.printHeader('get_volumes') self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE) self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, SIZE) vol1 = self.cl.getVolume(VOLUME_NAME1) vol2 = self.cl.getVolume(VOLUME_NAME2) vols = self.cl.getVolumes() self.assertTrue(self.findInDict(vols['members'], 'name', vol1['name'])) self.assertTrue(self.findInDict(vols['members'], 'name', vol2['name'])) self.printFooter('get_volumes') def test_3_delete_volume_nonExist(self): self.printHeader('delete_volume_nonExist') self.assertRaises( exceptions.HTTPNotFound, self.cl.deleteVolume, VOLUME_NAME1 ) self.printFooter('delete_volume_nonExist') def test_3_delete_volumes(self): self.printHeader('delete_volumes') optional = {'comment': 'Made by flask.'} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) self.cl.getVolume(VOLUME_NAME1) optional = {'comment': 'Made by flask.'} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, SIZE, optional) self.cl.getVolume(VOLUME_NAME2) self.cl.deleteVolume(VOLUME_NAME1) self.assertRaises( exceptions.HTTPNotFound, self.cl.getVolume, VOLUME_NAME1 ) self.cl.deleteVolume(VOLUME_NAME2) self.assertRaises( exceptions.HTTPNotFound, self.cl.getVolume, VOLUME_NAME2 ) self.printFooter('delete_volumes') def test_4_create_snapshot_no_optional(self): self.printHeader('create_snapshot_no_optional') optional = {'snapCPG': CPG_NAME1} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) # add one self.cl.createSnapshot(SNAP_NAME1, VOLUME_NAME1) # no API to get and check self.cl.deleteVolume(SNAP_NAME1) self.printFooter('create_snapshot_no_optional') def test_4_create_snapshot(self): self.printHeader('create_snapshot') optional = {'snapCPG': CPG_NAME1} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) # add one optional = {'expirationHours': 300} self.cl.createSnapshot(SNAP_NAME1, VOLUME_NAME1, optional) # no API to get and check self.cl.deleteVolume(SNAP_NAME1) self.printFooter('create_snapshot') def test_4_create_snapshot_badParams(self): self.printHeader('create_snapshot_badParams') # add one optional = {'snapCPG': CPG_NAME1} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) optional = {'Bad': True, 'expirationHours': 300} self.assertRaises( exceptions.HTTPBadRequest, self.cl.createSnapshot, SNAP_NAME1, VOLUME_NAME1, optional ) self.printFooter('create_snapshot_badParams') def test_4_create_snapshot_nonExistVolume(self): self.printHeader('create_snapshot_nonExistVolume') # add one name = 'UnitTestSnapshot' volName = 'NonExistVolume' optional = {'id': 1, 'comment': 'test snapshot', 'readOnly': True, 'expirationHours': 300} self.assertRaises( exceptions.HTTPNotFound, self.cl.createSnapshot, name, volName, optional ) self.printFooter('create_snapshot_nonExistVolume') def test_5_grow_volume(self): self.printHeader('grow_volume') # add one optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) # grow it result = self.cl.growVolume(VOLUME_NAME1, 1) result = self.cl.getVolume(VOLUME_NAME1) size_after = result['sizeMiB'] self.assertGreater(size_after, SIZE) self.printFooter('grow_volume') def test_5_grow_volume_with_float_value(self): self.printHeader('grow_volume_with_float_value') # add one optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) # grow it result = self.cl.growVolume(VOLUME_NAME1, 1.0) result = self.cl.getVolume(VOLUME_NAME1) size_after = result['sizeMiB'] self.assertGreater(size_after, SIZE) self.printFooter('grow_volume_with_float_value') def test_5_grow_volume_bad(self): self.printHeader('grow_volume_bad') # add one optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) # shrink it # 3par is returning 409 instead of 400 self.assertRaises( (exceptions.HTTPBadRequest, exceptions.HTTPConflict), self.cl.growVolume, VOLUME_NAME1, -1 ) self.printFooter('grow_volume_bad') def test_6_copy_volume(self): self.printHeader('copy_volume') # TODO: Add support for ssh/stopPhysical copy in mock mode if self.unitTest: self.printFooter('copy_volume') return # add one optional = {'comment': 'test volume', 'tpvv': True, 'snapCPG': CPG_NAME1} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) # copy it optional = {'online': True} self.cl.copyVolume(VOLUME_NAME1, VOLUME_NAME2, CPG_NAME1, optional) self.cl.getVolume(VOLUME_NAME2) self.cl.stopOnlinePhysicalCopy(VOLUME_NAME2) self.assertRaises( exceptions.HTTPNotFound, self.cl.getVolume, VOLUME_NAME2 ) self.printFooter('copy_volume') def test_6_copy_volume_invalid_volume(self): self.printHeader('copy_volume') # TODO: Add support for ssh/stopPhysical copy in mock mode if self.unitTest: self.printFooter('copy_volume') return self.assertRaises( exceptions.HTTPNotFound, self.cl.stopOnlinePhysicalCopy, "fake-volume" ) self.printFooter('copy_volume') def test_7_copy_volume_failure(self): self.printHeader('copy_volume_failure') # add one optional = {'comment': 'test volume', 'tpvv': True, 'snapCPG': CPG_NAME1} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, SIZE, optional) optional = {'online': False, 'tpvv': True} self.assertRaises( exceptions.HTTPBadRequest, self.cl.copyVolume, VOLUME_NAME1, VOLUME_NAME2, CPG_NAME1, optional) optional = {'online': False, 'tpdd': True} self.assertRaises( exceptions.HTTPBadRequest, self.cl.copyVolume, VOLUME_NAME1, VOLUME_NAME2, CPG_NAME1, optional) # destCPG isn't allowed to go to the 3PAR during an # offline copy. The client strips it out, so this should pass optional = {'online': False, 'destCPG': 'test'} self.cl.copyVolume(VOLUME_NAME1, VOLUME_NAME2, CPG_NAME1, optional) self.cl.getVolume(VOLUME_NAME2) self.cl.deleteVolume(VOLUME_NAME2) self.cl.deleteVolume(VOLUME_NAME1) self.printFooter('copy_volume_failure') def test_7_create_volume_set(self): self.printHeader('create_volume_set') self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=self.DOMAIN, comment="Unit test volume set 1") resp = self.cl.getVolumeSet(VOLUME_SET_NAME1) print(resp) self.printFooter('create_volume_set') def test_7_create_volume_set_with_volumes(self): self.printHeader('create_volume_set') optional = {'comment': 'test volume 1', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) optional = {'comment': 'test volume 2', 'tpvv': True} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, SIZE, optional) members = [VOLUME_NAME1, VOLUME_NAME2] self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=self.DOMAIN, comment="Unit test volume set 1", setmembers=members) resp = self.cl.getVolumeSet(VOLUME_SET_NAME1) self.assertIsNotNone(resp) resp_members = resp['setmembers'] self.assertIn(VOLUME_NAME1, resp_members) self.assertIn(VOLUME_NAME2, resp_members) self.printFooter('create_volume_set') def test_7_create_volume_set_dup(self): self.printHeader('create_volume_set_dup') self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=self.DOMAIN, comment="Unit test volume set 1") # create it again self.assertRaises( exceptions.HTTPConflict, self.cl.createVolumeSet, VOLUME_SET_NAME1, domain=self.DOMAIN, comment="Unit test volume set 1" ) self.printFooter('create_volume_set_dup') def test_7_add_remove_volume_in_volume_set(self): self.printHeader('add_remove_volume_in_volume_set') optional = {'comment': 'test volume 1', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=self.DOMAIN, comment="Unit test volume set 1", setmembers=None) # Add volume to volume set self.cl.addVolumeToVolumeSet(VOLUME_SET_NAME1, VOLUME_NAME1) resp = self.cl.getVolumeSet(VOLUME_SET_NAME1) self.assertIsNotNone(resp) resp_members = resp['setmembers'] self.assertIn(VOLUME_NAME1, resp_members) # Remove volume from volume set self.cl.removeVolumeFromVolumeSet(VOLUME_SET_NAME1, VOLUME_NAME1) # Check that None is returned if no volume sets are found. result = self.cl.findVolumeSet(VOLUME_NAME1) self.assertIsNone(result) self.printFooter('add_remove_volume_in_volume_set') def test_8_get_volume_sets(self): self.printHeader('get_volume_sets') self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=self.DOMAIN, comment="Unit test volume set 1") self.cl.createVolumeSet(VOLUME_SET_NAME2, domain=self.DOMAIN) sets = self.cl.getVolumeSets() self.assertIsNotNone(sets) set_names = [vset['name'] for vset in sets['members']] self.assertIn(VOLUME_SET_NAME1, set_names) self.assertIn(VOLUME_SET_NAME2, set_names) self.printFooter('get_volume_sets') def test_8_find_all_volume_sets(self): self.printHeader('find_all_volume_sets') optional = {'comment': 'test volume 1', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) optional = {'comment': 'test volume 2', 'tpvv': True} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, 1024, optional) optional = {'comment': 'test volume 3', 'tpvv': True} self.cl.createVolume(VOLUME_NAME3, CPG_NAME1, 1024, optional) self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=self.DOMAIN, comment="Unit test volume set 1") self.cl.createVolumeSet(VOLUME_SET_NAME2, domain=self.DOMAIN, comment="Unit test volume set 2", setmembers=[VOLUME_NAME1]) self.cl.createVolumeSet(VOLUME_SET_NAME3, domain=self.DOMAIN, comment="Unit test volume set 3", setmembers=[VOLUME_NAME1, VOLUME_NAME2]) sets = self.cl.findAllVolumeSets(VOLUME_NAME1) self.assertIsNotNone(sets) set_names = [vset['name'] for vset in sets] self.assertIn(VOLUME_SET_NAME2, set_names) self.assertIn(VOLUME_SET_NAME3, set_names) self.assertNotIn(VOLUME_SET_NAME1, set_names) sets = self.cl.findAllVolumeSets(VOLUME_NAME3) expected = [] self.assertEqual(sets, expected) self.printFooter('find_all_volume_sets') def test_8_find_volume_set(self): self.printHeader('find_volume_set') optional = {'comment': 'test volume 1', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) optional = {'comment': 'test volume 2', 'tpvv': True} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, 1024, optional) optional = {'comment': 'test volume 3', 'tpvv': True} self.cl.createVolume(VOLUME_NAME3, CPG_NAME1, 1024, optional) self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=self.DOMAIN, comment="Unit test volume set 1") self.cl.createVolumeSet(VOLUME_SET_NAME2, domain=self.DOMAIN, comment="Unit test volume set 2", setmembers=[VOLUME_NAME1]) self.cl.createVolumeSet(VOLUME_SET_NAME3, domain=self.DOMAIN, comment="Unit test volume set 3", setmembers=[VOLUME_NAME1, VOLUME_NAME2]) result = self.cl.findVolumeSet(VOLUME_NAME1) self.assertEqual(result, VOLUME_SET_NAME2) # Check that None is returned if no volume sets are found. result = self.cl.findVolumeSet(VOLUME_NAME3) self.assertIsNone(result) self.printFooter('find_volumet_set') def test_9_del_volume_set_empty(self): self.printHeader('del_volume_set_empty') self.cl.createVolumeSet(VOLUME_SET_NAME2, domain=self.DOMAIN) self.cl.deleteVolumeSet(VOLUME_SET_NAME2) self.printFooter('del_volume_set_empty') def test_9_del_volume_set_with_volumes(self): self.printHeader('delete_volume_set_with_volumes') optional = {'comment': 'test volume 1', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) optional = {'comment': 'test volume 2', 'tpvv': True} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, SIZE, optional) members = [VOLUME_NAME1, VOLUME_NAME2] self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=self.DOMAIN, comment="Unit test volume set 1", setmembers=members) self.cl.deleteVolumeSet(VOLUME_SET_NAME1) self.printFooter('delete_volume_set_with_volumes') def test_10_modify_volume_set_change_name(self): self.printHeader('modify_volume_set_change_name') self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=self.DOMAIN, comment="First") self.cl.modifyVolumeSet(VOLUME_SET_NAME1, newName=VOLUME_SET_NAME2) vset = self.cl.getVolumeSet(VOLUME_SET_NAME2) self.assertEqual("First", vset['comment']) self.printFooter('modify_volume_set_change_name') def test_10_modify_volume_set_change_comment(self): self.printHeader('modify_volume_set_change_comment') self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=self.DOMAIN, comment="First") self.cl.modifyVolumeSet(VOLUME_SET_NAME1, comment="Second") vset = self.cl.getVolumeSet(VOLUME_SET_NAME1) self.assertEqual("Second", vset['comment']) self.printFooter('modify_volume_set_change_comment') def test_10_modify_volume_set_change_flash_cache(self): self.printHeader('modify_volume_set_change_flash_cache') try: self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=self.DOMAIN, comment="First") self.cl.modifyVolumeSet( VOLUME_SET_NAME1, flashCachePolicy=self.cl.FLASH_CACHE_ENABLED) vset = self.cl.getVolumeSet(VOLUME_SET_NAME1) self.assertEqual(self.cl.FLASH_CACHE_ENABLED, vset['flashCachePolicy']) self.cl.modifyVolumeSet( VOLUME_SET_NAME1, flashCachePolicy=self.cl.FLASH_CACHE_DISABLED) vset = self.cl.getVolumeSet(VOLUME_SET_NAME1) self.assertEqual(self.cl.FLASH_CACHE_DISABLED, vset['flashCachePolicy']) except exceptions.HTTPBadRequest: # means we are on a server that doesn't support FlashCachePolicy # pre 3.2.1 MU2 pass except exceptions.HTTPNotFound as e: # Pass if server doesn't have flash cache # Not found (HTTP 404) 285 - Flash cache does not exist if e.get_code() == 285: pass else: raise self.printFooter('modify_volume_set_change_flash_cache') def test_10_modify_volume_set_add_members_to_empty(self): self.printHeader('modify_volume_set_add_members_to_empty') optional = {'comment': 'test volume 1', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) optional = {'comment': 'test volume 2', 'tpvv': True} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, SIZE, optional) self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=self.DOMAIN, comment="Unit test volume set 1") members = [VOLUME_NAME1, VOLUME_NAME2] self.cl.modifyVolumeSet(VOLUME_SET_NAME1, self.cl.SET_MEM_ADD, setmembers=members) resp = self.cl.getVolumeSet(VOLUME_SET_NAME1) print(resp) self.assertTrue(VOLUME_NAME1 in resp['setmembers']) self.assertTrue(VOLUME_NAME2 in resp['setmembers']) self.printFooter('modify_volume_set_add_members_to_empty') def test_10_modify_volume_set_add_members(self): self.printHeader('modify_volume_set_add_members') optional = {'comment': 'test volume 1', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) optional = {'comment': 'test volume 2', 'tpvv': True} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, SIZE, optional) members = [VOLUME_NAME1] self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=self.DOMAIN, setmembers=members, comment="Unit test volume set 1") members = [VOLUME_NAME2] self.cl.modifyVolumeSet(VOLUME_SET_NAME1, self.cl.SET_MEM_ADD, setmembers=members) resp = self.cl.getVolumeSet(VOLUME_SET_NAME1) print(resp) self.assertTrue(VOLUME_NAME1 in resp['setmembers']) self.assertTrue(VOLUME_NAME2 in resp['setmembers']) self.printFooter('modify_volume_set_add_members') def test_10_modify_volume_set_del_members(self): self.printHeader('modify_volume_del_members') optional = {'comment': 'test volume 1', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) optional = {'comment': 'test volume 2', 'tpvv': True} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, SIZE, optional) members = [VOLUME_NAME1, VOLUME_NAME2] self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=self.DOMAIN, comment="Unit test volume set 1", setmembers=members) members = [VOLUME_NAME1] self.cl.modifyVolumeSet(VOLUME_SET_NAME1, action=self.cl.SET_MEM_REMOVE, setmembers=members) resp = self.cl.getVolumeSet(VOLUME_SET_NAME1) self.assertIsNotNone(resp) resp_members = resp['setmembers'] self.assertNotIn(VOLUME_NAME1, resp_members) self.assertIn(VOLUME_NAME2, resp_members) self.printFooter('modify_volume_del_members') def _create_vv_sets(self): optional = {'comment': 'test volume 1', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) optional = {'comment': 'test volume 2', 'tpvv': True} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, SIZE, optional) members = [VOLUME_NAME1, VOLUME_NAME2] self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=self.DOMAIN, comment="Unit test volume set 1", setmembers=members) def test_11_add_qos(self): self.printHeader('add_qos') self._create_vv_sets() qos = {'bwMinGoalKB': 1024, 'bwMaxLimitKB': 1024} self.cl.createQoSRules(VOLUME_SET_NAME1, qos) rule = self.cl.queryQoSRule(VOLUME_SET_NAME1) self.assertIsNotNone(rule) self.assertEqual(rule['bwMinGoalKB'], qos['bwMinGoalKB']) self.assertEqual(rule['bwMaxLimitKB'], qos['bwMaxLimitKB']) self.printFooter('add_qos') def test_12_modify_qos(self): self.printHeader('modify_qos') self._create_vv_sets() qos_before = {'bwMinGoalKB': 1024, 'bwMaxLimitKB': 1024} qos_after = {'bwMinGoalKB': 1024, 'bwMaxLimitKB': 2048} self.cl.createQoSRules(VOLUME_SET_NAME1, qos_before) self.cl.modifyQoSRules(VOLUME_SET_NAME1, qos_after) rule = self.cl.queryQoSRule(VOLUME_SET_NAME1) self.assertIsNotNone(rule) self.assertEqual(rule['bwMinGoalKB'], qos_after['bwMinGoalKB']) self.assertEqual(rule['bwMaxLimitKB'], qos_after['bwMaxLimitKB']) self.printFooter('modify_qos') def test_13_delete_qos(self): self.printHeader('delete_qos') self._create_vv_sets() self.cl.createVolumeSet(VOLUME_SET_NAME2) qos1 = {'bwMinGoalKB': 1024, 'bwMaxLimitKB': 1024} qos2 = {'bwMinGoalKB': 512, 'bwMaxLimitKB': 2048} self.cl.createQoSRules(VOLUME_SET_NAME1, qos1) self.cl.createQoSRules(VOLUME_SET_NAME2, qos2) all_qos = self.cl.queryQoSRules() self.assertGreaterEqual(all_qos['total'], 2) self.assertIn(VOLUME_SET_NAME1, [qos['name'] for qos in all_qos['members']]) self.assertIn(VOLUME_SET_NAME2, [qos['name'] for qos in all_qos['members']]) self.cl.deleteQoSRules(VOLUME_SET_NAME1) all_qos = self.cl.queryQoSRules() self.assertIsNotNone(all_qos) self.assertNotIn(VOLUME_SET_NAME1, [qos['name'] for qos in all_qos['members']]) self.assertIn(VOLUME_SET_NAME2, [qos['name'] for qos in all_qos['members']]) self.printFooter('delete_qos') def test_14_modify_volume_rename(self): self.printHeader('modify volume') # add one optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) volumeMod = {'newName': VOLUME_NAME2} self.cl.modifyVolume(VOLUME_NAME1, volumeMod) vol2 = self.cl.getVolume(VOLUME_NAME2) self.assertIsNotNone(vol2) self.assertEqual(vol2['comment'], optional['comment']) self.printFooter('modify volume') def test_15_set_volume_metadata(self): self.printHeader('set volume metadata') optional = {'comment': 'test volume', 'tpvv': True} expected_value = 'test_val' self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) self.cl.setVolumeMetaData(VOLUME_NAME1, 'test_key', expected_value) result = self.cl.getVolumeMetaData(VOLUME_NAME1, 'test_key') self.assertEqual(result['value'], expected_value) self.printFooter('set volume metadata') def test_15_set_bad_volume_metadata(self): self.printHeader('set bad volume metadata') self.assertRaises(exceptions.HTTPNotFound, self.cl.setVolumeMetaData, 'Fake_Volume', 'test_key', 'test_val') self.printFooter('set bad volume metadata') def test_15_set_volume_metadata_existing_key(self): self.printHeader('set volume metadata existing key') optional = {'comment': 'test volume', 'tpvv': True} expected = 'new_test_val' self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) self.cl.setVolumeMetaData(VOLUME_NAME1, 'test_key', 'test_val') self.cl.setVolumeMetaData(VOLUME_NAME1, 'test_key', 'new_test_val') contents = self.cl.getVolumeMetaData(VOLUME_NAME1, 'test_key') self.assertEqual(contents['value'], expected) self.printFooter('set volume metadata existing key') def test_15_set_volume_metadata_invalid_length(self): self.printHeader('set volume metadata invalid length') optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) # Some backends have a key limit of 31 characters while and other # are larger self.assertRaises(exceptions.HTTPBadRequest, self.cl.setVolumeMetaData, VOLUME_NAME1, 'this_key_will_cause_an_exception ' 'x' * 256, 'test_val') self.printFooter('set volume metadata invalid length') def test_15_set_volume_metadata_invalid_data(self): self.printHeader('set volume metadata invalid data') optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) self.assertRaises(exceptions.HTTPBadRequest, self.cl.setVolumeMetaData, VOLUME_NAME1, None, 'test_val') self.printFooter('set volume metadata invalid data') def test_16_get_volume_metadata(self): self.printHeader('get volume metadata') optional = {'comment': 'test volume', 'tpvv': True} expected_value = 'test_val' self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) self.cl.setVolumeMetaData(VOLUME_NAME1, 'test_key', expected_value) result = self.cl.getVolumeMetaData(VOLUME_NAME1, 'test_key') self.assertEqual(expected_value, result['value']) self.printFooter('get volume metadata') def test_16_get_volume_metadata_missing_volume(self): self.printHeader('get volume metadata missing volume') self.assertRaises(exceptions.HTTPNotFound, self.cl.getVolumeMetaData, 'Fake_Volume', 'bad_key') self.printFooter('get volume metadata missing volume') def test_16_get_volume_metadata_missing_key(self): self.printHeader('get volume metadata missing key') optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) self.assertRaises(exceptions.HTTPNotFound, self.cl.getVolumeMetaData, VOLUME_NAME1, 'bad_key') self.printFooter('get volume metadata missing key') def test_16_get_volume_metadata_invalid_input(self): self.printHeader('get volume metadata invalid input') optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) self.assertRaises(exceptions.HTTPBadRequest, self.cl.getVolumeMetaData, VOLUME_NAME1, '&') self.printFooter('get volume metadata invalid input') def test_17_get_all_volume_metadata(self): self.printHeader('get all volume metadata') # Keys present in metadata optional = {'comment': 'test volume', 'tpvv': True} expected_value_1 = 'test_val' expected_value_2 = 'test_val2' self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) self.cl.setVolumeMetaData(VOLUME_NAME1, 'test_key_1', expected_value_1) self.cl.setVolumeMetaData(VOLUME_NAME1, 'test_key_2', expected_value_2) result = self.cl.getAllVolumeMetaData(VOLUME_NAME1) # Key- Value pairs are unordered for member in result['members']: if member['key'] == 'test_key_1': self.assertEqual(expected_value_1, member['value']) elif member['key'] == 'test_key_2': self.assertEqual(expected_value_2, member['value']) else: raise Exception("Unexpected member %s" % member) # No keys present in metadata optional = {'comment': 'test volume', 'tpvv': True} expected_value = {'total': 0, 'members': []} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, 1024, optional) result = self.cl.getAllVolumeMetaData(VOLUME_NAME2) self.assertEqual(expected_value, result) self.printFooter('get all volume metadata') def test_17_get_all_volume_metadata_missing_volume(self): self.printHeader('get all volume metadata missing volume') self.assertRaises(exceptions.HTTPNotFound, self.cl.getAllVolumeMetaData, 'Fake_Volume') self.printFooter('get all volume metadata missing volume') def test_18_remove_volume_metadata(self): self.printHeader('remove volume metadata') optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) self.cl.setVolumeMetaData(VOLUME_NAME1, 'test_key', 'test_val') self.cl.removeVolumeMetaData(VOLUME_NAME1, 'test_key') self.assertRaises(exceptions.HTTPNotFound, self.cl.getVolumeMetaData, VOLUME_NAME1, 'test_key') self.printFooter('remove volume metadata') def test_18_remove_volume_metadata_missing_volume(self): self.printHeader('remove volume metadata missing volume') self.assertRaises(exceptions.HTTPNotFound, self.cl.removeVolumeMetaData, 'Fake_Volume', 'test_key') self.printFooter('remove volume metadata missing volume') def test_18_remove_volume_metadata_missing_key(self): self.printHeader('remove volume metadata missing key') optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) self.assertRaises(exceptions.HTTPNotFound, self.cl.removeVolumeMetaData, VOLUME_NAME1, 'test_key') self.printFooter('remove volume metadata missing key') def test_18_remove_volume_metadata_invalid_input(self): self.printHeader('remove volume metadata invalid input') optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) self.assertRaises(exceptions.HTTPBadRequest, self.cl.removeVolumeMetaData, VOLUME_NAME1, '&') self.printFooter('remove volume metadata invalid input') def test_19_find_volume_metadata(self): self.printHeader('find volume metadata') # Volume should be found optional = {'comment': 'test volume', 'tpvv': True} expected = True self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) self.cl.setVolumeMetaData(VOLUME_NAME1, 'test_key', 'test_val') result = self.cl.findVolumeMetaData(VOLUME_NAME1, 'test_key', 'test_val') self.assertEqual(result, expected) # Volume should not be found optional = {'comment': 'test volume', 'tpvv': True} expected = False result = self.cl.findVolumeMetaData(VOLUME_NAME1, 'bad_key', 'test_val') self.assertEqual(result, expected) self.printFooter('find volume metadata') def test_19_find_volume_metadata_missing_volume(self): self.printHeader('find volume metadata missing volume') expected = False result = self.cl.findVolumeMetaData('Fake_Volume', 'test_key', 'test_val') self.assertEqual(result, expected) self.printFooter('find volume metadata missing volume') def test_20_create_vvset_snapshot_no_optional(self): self.printHeader('create_vvset_snapshot_no_optional') # create volume to add to volume set optional = {'snapCPG': CPG_NAME1} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) # create volume set and add a volume to it self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=DOMAIN, setmembers=[VOLUME_NAME1]) # @count@ is needed by 3PAR to create volume set snapshots. will # create SNAP_NAME1-0 format self.cl.createSnapshotOfVolumeSet(SNAP_NAME1 + "-@count@", VOLUME_SET_NAME1) # assert snapshot was created snap = SNAP_NAME1 + "-0" snapshot = self.cl.getVolume(snap) self.assertEqual(VOLUME_NAME1, snapshot['copyOf']) # cleanup volume snapshot and volume set self.cl.deleteVolume(snap) self.cl.deleteVolumeSet(VOLUME_SET_NAME1) self.printFooter('create_vvset_snapshot_no_optional') def test_20_create_vvset_snapshot(self): self.printHeader('create_vvset_snapshot') # create volume to add to volume set optional = {'snapCPG': CPG_NAME1} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) # create volume set and add a volume to it self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=DOMAIN, setmembers=[VOLUME_NAME1]) # @count@ is needed by 3PAR to create volume set snapshots. will # create SNAP_NAME1-0 format optional = {'expirationHours': 300} self.cl.createSnapshotOfVolumeSet(SNAP_NAME1 + "-@count@", VOLUME_SET_NAME1, optional) # assert snapshot was created snap = SNAP_NAME1 + "-0" snapshot = self.cl.getVolume(snap) self.assertEqual(VOLUME_NAME1, snapshot['copyOf']) # cleanup volume snapshot and volume set self.cl.deleteVolume(snap) self.cl.deleteVolumeSet(VOLUME_SET_NAME1) self.printFooter('create_vvset_snapshot') def test_20_create_vvset_snapshot_badParams(self): self.printHeader('create_vvset_snapshot_badParams') # add one self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=DOMAIN) optional = {'Bad': True, 'expirationHours': 300} self.assertRaises( exceptions.HTTPBadRequest, self.cl.createSnapshotOfVolumeSet, SNAP_NAME1, VOLUME_SET_NAME1, optional ) self.printFooter('create_vvset_snapshot_badParams') def test_20_create_vvset_snapshot_nonExistVolumeSet(self): self.printHeader('create_vvset_snapshot_nonExistVolume') # add one name = 'UnitTestVvsetSnapshot' volSetName = 'NonExistVolumeSet' optional = {'comment': 'test vvset snapshot', 'readOnly': True, 'expirationHours': 300} self.assertRaises( exceptions.HTTPNotFound, self.cl.createSnapshotOfVolumeSet, name, volSetName, optional ) self.printFooter('create_vvset_snapshot_nonExistVolume') def test_20_create_vvset_emptyVolumeSet(self): self.printHeader('test_20_create_vvset_emptyVolumeSet') name = 'UnitTestVvsetSnapshot' self.cl.createVolumeSet(VOLUME_SET_NAME1, domain=DOMAIN) self.assertRaises( exceptions.HTTPNotFound, self.cl.createSnapshotOfVolumeSet, name, VOLUME_SET_NAME1 ) self.cl.deleteVolumeSet(VOLUME_SET_NAME1) self.printFooter('test_20_create_vvset_emptyVolumeSet') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test_21_create_remote_copy_group(self): self.printHeader('create_remote_copy_group') # Create empty remote copy group self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, REMOTE_COPY_TARGETS, optional={"domain": DOMAIN}) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) self.printFooter('create_remote_copy_group') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test_21_delete_remote_copy_group(self): self.printHeader('delete_remote_copy_group') # Create empty remote copy group self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, REMOTE_COPY_TARGETS, optional={"domain": DOMAIN}) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Delete remote copy group self.cl.removeRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertRaises( exceptions.HTTPNotFound, self.cl.getRemoteCopyGroup, REMOTE_COPY_GROUP_NAME1 ) self.printFooter('create_delete_copy_group') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test_21_modify_remote_copy_group(self): self.printHeader('modify_remote_copy_group') # Create empty remote copy group self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, REMOTE_COPY_TARGETS, optional={"domain": DOMAIN}) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) REMOTE_COPY_TARGETS[0]['syncPeriod'] = 300 self.cl.modifyRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, {'targets': REMOTE_COPY_TARGETS}) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] self.assertEqual(300, targets[0]['syncPeriod']) self.printFooter('modify_remote_copy_group') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test_21_add_volume_to_remote_copy_group(self): self.printHeader('add_volume_to_remote_copy_group') # Create empty remote copy group self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, REMOTE_COPY_TARGETS, optional={"domain": DOMAIN}) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Create volume optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(RC_VOLUME_NAME, CPG_NAME1, SIZE, optional) # Add volume to remote copy group self.cl.addVolumeToRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, RC_VOLUME_NAME, REMOTE_COPY_TARGETS) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) volumes = resp['volumes'] self.assertEqual(RC_VOLUME_NAME, volumes[0]['name']) self.printFooter('add_volume_to_remote_copy_group') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test_21_add_volume_to_remote_copy_group_nonExistVolume(self): self.printHeader('add_volume_to_remote_copy_group_nonExistVolume') # Create empty remote copy group self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, REMOTE_COPY_TARGETS, optional={"domain": DOMAIN}) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Add non existent volume to remote copy group self.assertRaises( exceptions.HTTPNotFound, self.cl.addVolumeToRemoteCopyGroup, REMOTE_COPY_GROUP_NAME1, 'BAD_VOLUME_NAME', REMOTE_COPY_TARGETS ) self.printFooter('add_volume_to_remote_copy_group_nonExistVolume') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test_21_remove_volume_from_remote_copy_group(self): self.printHeader('remove_volume_from_remote_copy_group') # Create empty remote copy group self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, REMOTE_COPY_TARGETS, optional={"domain": DOMAIN}) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Create volume optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(RC_VOLUME_NAME, CPG_NAME1, SIZE, optional) # Add volume to remote copy group self.cl.addVolumeToRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, RC_VOLUME_NAME, REMOTE_COPY_TARGETS) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) volumes = resp['volumes'] self.assertEqual(RC_VOLUME_NAME, volumes[0]['name']) # Remove volume from remote copy group self.cl.removeVolumeFromRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, RC_VOLUME_NAME, useHttpDelete=False) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) volumes = resp['volumes'] self.assertEqual([], volumes) self.printFooter('remove_volume_from_remote_copy_group') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test_21_start_remote_copy(self): self.printHeader('start_remote_copy') # Create empty remote copy group self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, REMOTE_COPY_TARGETS, optional={"domain": DOMAIN}) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Create volume optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(RC_VOLUME_NAME, CPG_NAME1, SIZE, optional) # Add volume to remote copy group self.cl.addVolumeToRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, RC_VOLUME_NAME, REMOTE_COPY_TARGETS) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) volumes = resp['volumes'] self.assertEqual(RC_VOLUME_NAME, volumes[0]['name']) # Start remote copy for the group self.cl.startRemoteCopy(REMOTE_COPY_GROUP_NAME1) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] self.assertEqual(RCOPY_STARTED, targets[0]['state']) self.printFooter('start_remote_copy') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test_21_stop_remote_copy(self): self.printHeader('stop_remote_copy') # Create empty remote copy group self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, REMOTE_COPY_TARGETS, optional={"domain": DOMAIN}) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Create volume optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(RC_VOLUME_NAME, CPG_NAME1, SIZE, optional) # Add volume to remote copy group self.cl.addVolumeToRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, RC_VOLUME_NAME, REMOTE_COPY_TARGETS) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) volumes = resp['volumes'] self.assertEqual(RC_VOLUME_NAME, volumes[0]['name']) # Start remote copy for the group self.cl.startRemoteCopy(REMOTE_COPY_GROUP_NAME1) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] self.assertEqual(RCOPY_STARTED, targets[0]['state']) # Stop remote copy for the group self.cl.stopRemoteCopy(REMOTE_COPY_GROUP_NAME1) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] self.assertEqual(RCOPY_STOPPED, targets[0]['state']) self.printFooter('stop_remote_copy') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test_21_synchronize_remote_copy_group(self): self.printHeader('synchronize_remote_copy_group') # Create empty remote copy group self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, REMOTE_COPY_TARGETS, optional={"domain": DOMAIN}) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Create volume optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(RC_VOLUME_NAME, CPG_NAME1, SIZE, optional) # Add volume to remote copy group self.cl.addVolumeToRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, RC_VOLUME_NAME, REMOTE_COPY_TARGETS) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) volumes = resp['volumes'] self.assertEqual(RC_VOLUME_NAME, volumes[0]['name']) # Start remote copy for the group self.cl.startRemoteCopy(REMOTE_COPY_GROUP_NAME1) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] self.assertEqual(RCOPY_STARTED, targets[0]['state']) # Synchronize the remote copy group self.cl.synchronizeRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] assert targets[0]['groupLastSyncTime'] is not None self.printFooter('synchronize_remote_copy_group') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test_21_failover_remote_copy_group(self): self.printHeader('failover_remote_copy_group') # Create empty remote copy group self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, REMOTE_COPY_TARGETS, optional={"domain": DOMAIN}) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Create volume optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(RC_VOLUME_NAME, CPG_NAME1, SIZE, optional) # Add volume to remote copy group self.cl.addVolumeToRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, RC_VOLUME_NAME, REMOTE_COPY_TARGETS) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) volumes = resp['volumes'] self.assertEqual(RC_VOLUME_NAME, volumes[0]['name']) # Start remote copy for the group self.cl.startRemoteCopy(REMOTE_COPY_GROUP_NAME1) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] self.assertEqual(RCOPY_STARTED, targets[0]['state']) # Failover remote copy group self.cl.recoverRemoteCopyGroupFromDisaster(REMOTE_COPY_GROUP_NAME1, FAILOVER_GROUP) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(True, resp['roleReversed']) self.printFooter('failover_remote_copy_group') @unittest.skipIf(no_remote_copy(), SKIP_FLASK_RCOPY_MESSAGE) def test_22_create_remote_copy_group(self): self.printHeader('create_remote_copy_group') # Create empty remote copy group targets = [{"targetName": self.secondary_target_name, "mode": 2, "userCPG": CPG_NAME1, "snapCPG": CPG_NAME1}] optional = {'localSnapCPG': CPG_NAME1, 'localUserCPG': CPG_NAME1, 'domain': DOMAIN} self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, targets, optional=optional) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Check remote copy group on the secondary array info = self.cl.getStorageSystemInfo() client_id = str(info['id']) target_rcg_name = REMOTE_COPY_GROUP_NAME1 + ".r" + client_id resp = self.secondary_cl.getRemoteCopyGroup(target_rcg_name) self.assertEqual(target_rcg_name, resp['name']) @unittest.skipIf(no_remote_copy(), SKIP_FLASK_RCOPY_MESSAGE) def test_22_delete_remote_copy_group(self): self.printHeader('delete_remote_copy_group') # Create empty remote copy group targets = [{"targetName": self.secondary_target_name, "mode": 2, "userCPG": CPG_NAME1, "snapCPG": CPG_NAME1}] optional = {'localSnapCPG': CPG_NAME1, 'localUserCPG': CPG_NAME1, 'domain': DOMAIN} self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, targets, optional=optional) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Check remote copy group on the secondary array info = self.cl.getStorageSystemInfo() client_id = str(info['id']) target_rcg_name = REMOTE_COPY_GROUP_NAME1 + ".r" + client_id resp = self.secondary_cl.getRemoteCopyGroup(target_rcg_name) self.assertEqual(target_rcg_name, resp['name']) # Delete remote copy group self.cl.removeRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertRaises( exceptions.HTTPNotFound, self.cl.getRemoteCopyGroup, REMOTE_COPY_GROUP_NAME1 ) # Check remote copy does not exist on target array self.assertRaises( exceptions.HTTPNotFound, self.secondary_cl.getRemoteCopyGroup, REMOTE_COPY_GROUP_NAME1 ) self.printFooter('create_delete_copy_group') @unittest.skipIf(no_remote_copy(), SKIP_FLASK_RCOPY_MESSAGE) def test_22_modify_remote_copy_group(self): self.printHeader('modify_remote_copy_group') # Create empty remote copy group targets = [{"targetName": self.secondary_target_name, "mode": 2, "userCPG": CPG_NAME1, "snapCPG": CPG_NAME1}] optional = {'localSnapCPG': CPG_NAME1, 'localUserCPG': CPG_NAME1, 'domain': DOMAIN} self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, targets, optional=optional) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Check remote copy group on the secondary array info = self.cl.getStorageSystemInfo() client_id = str(info['id']) target_rcg_name = REMOTE_COPY_GROUP_NAME1 + ".r" + client_id resp = self.secondary_cl.getRemoteCopyGroup(target_rcg_name) self.assertEqual(target_rcg_name, resp['name']) # Modify the remote copy group targets = [{'targetName': self.secondary_target_name, 'syncPeriod': 300}] self.cl.modifyRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, {'targets': targets}) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] self.assertEqual(300, targets[0]['syncPeriod']) # Check modification on target array resp = self.secondary_cl.getRemoteCopyGroup(target_rcg_name) targets = resp['targets'] self.assertEqual(300, targets[0]['syncPeriod']) self.printFooter('modify_remote_copy_group') @unittest.skipIf(no_remote_copy(), SKIP_FLASK_RCOPY_MESSAGE) def test_22_add_volume_to_remote_copy_group(self): self.printHeader('add_volume_to_remote_copy_group') # Create empty remote copy group targets = [{"targetName": self.secondary_target_name, "mode": 2, "userCPG": CPG_NAME1, "snapCPG": CPG_NAME1}] optional = {'localSnapCPG': CPG_NAME1, 'localUserCPG': CPG_NAME1, 'domain': DOMAIN} self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, targets, optional=optional) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Check remote copy group on the secondary array info = self.cl.getStorageSystemInfo() client_id = str(info['id']) target_rcg_name = REMOTE_COPY_GROUP_NAME1 + ".r" + client_id resp = self.secondary_cl.getRemoteCopyGroup(target_rcg_name) self.assertEqual(target_rcg_name, resp['name']) # Create volume optional = {'comment': 'test volume', 'tpvv': True, 'snapCPG': CPG_NAME1} self.cl.createVolume(RC_VOLUME_NAME, CPG_NAME1, SIZE, optional) # Add volume to remote copy group targets = [{'targetName': self.secondary_target_name, 'secVolumeName': RC_VOLUME_NAME}] optional = {'volumeAutoCreation': True} self.cl.addVolumeToRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, RC_VOLUME_NAME, targets, optional=optional) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) volumes = resp['volumes'] self.assertEqual(RC_VOLUME_NAME, volumes[0]['localVolumeName']) remote_volumes = volumes[0]['remoteVolumes'] self.assertEqual(RC_VOLUME_NAME, remote_volumes[0]['remoteVolumeName']) self.printFooter('add_volume_to_remote_copy_group') @unittest.skipIf(no_remote_copy(), SKIP_FLASK_RCOPY_MESSAGE) def test_22_add_volume_to_remote_copy_group_nonExistVolume(self): self.printHeader('add_volume_to_remote_copy_group_nonExistVolume') # Create empty remote copy group targets = [{"targetName": self.secondary_target_name, "mode": 2, "userCPG": CPG_NAME1, "snapCPG": CPG_NAME1}] optional = {'localSnapCPG': CPG_NAME1, 'localUserCPG': CPG_NAME1, 'domain': DOMAIN} self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, targets, optional=optional) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Check remote copy group on the secondary array info = self.cl.getStorageSystemInfo() client_id = str(info['id']) target_rcg_name = REMOTE_COPY_GROUP_NAME1 + ".r" + client_id resp = self.secondary_cl.getRemoteCopyGroup(target_rcg_name) self.assertEqual(target_rcg_name, resp['name']) # Add non existent volume to remote copy group targets = [{'targetName': self.secondary_target_name, 'secVolumeName': RC_VOLUME_NAME}] self.assertRaises( exceptions.HTTPNotFound, self.cl.addVolumeToRemoteCopyGroup, REMOTE_COPY_GROUP_NAME1, 'BAD_VOLUME_NAME', targets ) self.printFooter('add_volume_to_remote_copy_group_nonExistVolume') @unittest.skipIf(no_remote_copy(), SKIP_FLASK_RCOPY_MESSAGE) def test_22_remove_volume_from_remote_copy_group(self): self.printHeader('remove_volume_from_remote_copy_group') # Create empty remote copy group targets = [{"targetName": self.secondary_target_name, "mode": 2, "userCPG": CPG_NAME1, "snapCPG": CPG_NAME1}] optional = {'localSnapCPG': CPG_NAME1, 'localUserCPG': CPG_NAME1, 'domain': DOMAIN} self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, targets, optional=optional) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Create volume optional = {'comment': 'test volume', 'tpvv': True, 'snapCPG': CPG_NAME1} self.cl.createVolume(RC_VOLUME_NAME, CPG_NAME1, SIZE, optional) # Add volume to remote copy group targets = [{'targetName': self.secondary_target_name, 'secVolumeName': RC_VOLUME_NAME}] optional = {'volumeAutoCreation': True} self.cl.addVolumeToRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, RC_VOLUME_NAME, targets, optional=optional) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) volumes = resp['volumes'] self.assertEqual(RC_VOLUME_NAME, volumes[0]['localVolumeName']) remote_volumes = volumes[0]['remoteVolumes'] self.assertEqual(RC_VOLUME_NAME, remote_volumes[0]['remoteVolumeName']) # Remove volume from remote copy group self.cl.removeVolumeFromRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, RC_VOLUME_NAME, removeFromTarget=True, useHttpDelete=False) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) volumes = resp['volumes'] self.assertEqual([], volumes) # Check volume does not exist on target array self.assertRaises( exceptions.HTTPNotFound, self.secondary_cl.getVolume, RC_VOLUME_NAME ) self.printFooter('remove_volume_from_remote_copy_group') @unittest.skipIf(no_remote_copy(), SKIP_FLASK_RCOPY_MESSAGE) def test_22_start_remote_copy(self): self.printHeader('start_remote_copy') # Create empty remote copy group targets = [{"targetName": self.secondary_target_name, "mode": 2, "userCPG": CPG_NAME1, "snapCPG": CPG_NAME1}] optional = {'localSnapCPG': CPG_NAME1, 'localUserCPG': CPG_NAME1, 'domain': DOMAIN} self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, targets, optional=optional) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Create volume optional = {'comment': 'test volume', 'tpvv': True, 'snapCPG': CPG_NAME1} self.cl.createVolume(RC_VOLUME_NAME, CPG_NAME1, SIZE, optional) # Add volume to remote copy group targets = [{'targetName': self.secondary_target_name, 'secVolumeName': RC_VOLUME_NAME}] optional = {'volumeAutoCreation': True} self.cl.addVolumeToRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, RC_VOLUME_NAME, targets, optional=optional) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) volumes = resp['volumes'] self.assertEqual(RC_VOLUME_NAME, volumes[0]['localVolumeName']) remote_volumes = volumes[0]['remoteVolumes'] self.assertEqual(RC_VOLUME_NAME, remote_volumes[0]['remoteVolumeName']) # Start remote copy for the group self.cl.startRemoteCopy(REMOTE_COPY_GROUP_NAME1) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] self.assertEqual(RCOPY_STARTED, targets[0]['state']) self.printFooter('start_remote_copy') @unittest.skipIf(no_remote_copy(), SKIP_FLASK_RCOPY_MESSAGE) def test_22_stop_remote_copy(self): self.printHeader('start_remote_copy') # Create empty remote copy group targets = [{"targetName": self.secondary_target_name, "mode": 2, "userCPG": CPG_NAME1, "snapCPG": CPG_NAME1}] optional = {'localSnapCPG': CPG_NAME1, 'localUserCPG': CPG_NAME1, 'domain': DOMAIN} self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, targets, optional=optional) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Create volume optional = {'comment': 'test volume', 'tpvv': True, 'snapCPG': CPG_NAME1} self.cl.createVolume(RC_VOLUME_NAME, CPG_NAME1, SIZE, optional) # Add volume to remote copy group targets = [{'targetName': self.secondary_target_name, 'secVolumeName': RC_VOLUME_NAME}] optional = {'volumeAutoCreation': True} self.cl.addVolumeToRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, RC_VOLUME_NAME, targets, optional=optional) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) volumes = resp['volumes'] self.assertEqual(RC_VOLUME_NAME, volumes[0]['localVolumeName']) remote_volumes = volumes[0]['remoteVolumes'] self.assertEqual(RC_VOLUME_NAME, remote_volumes[0]['remoteVolumeName']) # Start remote copy for the group self.cl.startRemoteCopy(REMOTE_COPY_GROUP_NAME1) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] self.assertEqual(RCOPY_STARTED, targets[0]['state']) # Stop remote copy for the group self.cl.stopRemoteCopy(REMOTE_COPY_GROUP_NAME1) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] self.assertEqual(RCOPY_STOPPED, targets[0]['state']) self.printFooter('start_remote_copy') @unittest.skipIf(no_remote_copy(), SKIP_FLASK_RCOPY_MESSAGE) def test_22_synchronize_remote_copy_group(self): self.printHeader('synchronize_remote_copy_group') # Create empty remote copy group targets = [{"targetName": self.secondary_target_name, "mode": 2, "userCPG": CPG_NAME1, "snapCPG": CPG_NAME1}] optional = {'localSnapCPG': CPG_NAME1, 'localUserCPG': CPG_NAME1, 'domain': DOMAIN} self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, targets, optional=optional) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Create volume optional = {'comment': 'test volume', 'tpvv': True, 'snapCPG': CPG_NAME1} self.cl.createVolume(RC_VOLUME_NAME, CPG_NAME1, SIZE, optional) # Add volume to remote copy group targets = [{'targetName': self.secondary_target_name, 'secVolumeName': RC_VOLUME_NAME}] optional = {'volumeAutoCreation': True} self.cl.addVolumeToRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, RC_VOLUME_NAME, targets, optional=optional) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) volumes = resp['volumes'] self.assertEqual(RC_VOLUME_NAME, volumes[0]['localVolumeName']) remote_volumes = volumes[0]['remoteVolumes'] self.assertEqual(RC_VOLUME_NAME, remote_volumes[0]['remoteVolumeName']) # Start remote copy for the group self.cl.startRemoteCopy(REMOTE_COPY_GROUP_NAME1) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] self.assertEqual(RCOPY_STARTED, targets[0]['state']) # Synchronize the remote copy group self.cl.synchronizeRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] assert targets[0]['groupLastSyncTime'] is not None self.printFooter('synchronize_remote_copy_group') @unittest.skipIf(no_remote_copy(), SKIP_FLASK_RCOPY_MESSAGE) def test_22_failover_remote_copy_group(self): self.printHeader('failover_remote_copy_group') # Create empty remote copy group targets = [{"targetName": self.secondary_target_name, "mode": 2, "userCPG": CPG_NAME1, "snapCPG": CPG_NAME1}] optional = {'localSnapCPG': CPG_NAME1, 'localUserCPG': CPG_NAME1, 'domain': DOMAIN} self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, targets, optional=optional) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Create volume optional = {'comment': 'test volume', 'tpvv': True, 'snapCPG': CPG_NAME1} self.cl.createVolume(RC_VOLUME_NAME, CPG_NAME1, SIZE, optional) # Add volume to remote copy group targets = [{'targetName': self.secondary_target_name, 'secVolumeName': RC_VOLUME_NAME}] optional = {'volumeAutoCreation': True} self.cl.addVolumeToRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, RC_VOLUME_NAME, targets, optional=optional) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) volumes = resp['volumes'] self.assertEqual(RC_VOLUME_NAME, volumes[0]['localVolumeName']) remote_volumes = volumes[0]['remoteVolumes'] self.assertEqual(RC_VOLUME_NAME, remote_volumes[0]['remoteVolumeName']) # Start remote copy for the group self.cl.startRemoteCopy(REMOTE_COPY_GROUP_NAME1) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] self.assertEqual(RCOPY_STARTED, targets[0]['state']) # Stop remote copy for the group self.cl.stopRemoteCopy(REMOTE_COPY_GROUP_NAME1) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] self.assertEqual(RCOPY_STOPPED, targets[0]['state']) # Failover remote copy group info = self.cl.getStorageSystemInfo() client_id = str(info['id']) target_rcg_name = REMOTE_COPY_GROUP_NAME1 + ".r" + client_id self.secondary_cl.recoverRemoteCopyGroupFromDisaster( target_rcg_name, FAILOVER_GROUP) resp = self.secondary_cl.getRemoteCopyGroup(target_rcg_name) targets = resp['targets'] self.assertEqual(True, targets[0]['roleReversed']) # Restore the remote copy group self.secondary_cl.recoverRemoteCopyGroupFromDisaster( target_rcg_name, RESTORE_GROUP) # Let the restore take affect time.sleep(10) self.printFooter('failover_remote_copy_group') def test_23_get_volume_snapshots(self): # Create volume and snaphot it optional = {'snapCPG': CPG_NAME1} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) self.cl.createSnapshot(SNAP_NAME1, VOLUME_NAME1) self.cl.createSnapshot(SNAP_NAME2, VOLUME_NAME1) # Get the volumes snapshots live_test = is_live_test() snapshots = self.cl.getVolumeSnapshots(VOLUME_NAME1, live_test) # If the volume has snapshots, their names will be returned as # a list self.assertEqual([SNAP_NAME1, SNAP_NAME2], snapshots) # Test where volume does not exist snapshots = self.cl.getVolumeSnapshots("BAD_VOL") # An empty list is returned if the volume does not exist self.assertEqual([], snapshots) def test_24_set_qos(self): self.printHeader('set_qos') self.cl.createVolumeSet(VOLUME_SET_NAME4, comment="Unit test volume set 4") self.assertRaises( exceptions.SetQOSRuleException, self.cl.setQOSRule, VOLUME_SET_NAME4) max_io = 300 max_bw = 1024 self.cl.setQOSRule(VOLUME_SET_NAME4, max_io, max_bw) self.cl.setQOSRule(VOLUME_SET_NAME4, max_io) self.cl.setQOSRule(VOLUME_SET_NAME4, max_bw) self.printFooter('set_qos') def test_25_promote_virtual_copy(self): self.printHeader('promote_virtual_copy') optional = {'snapCPG': CPG_NAME1} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) # add one self.cl.createSnapshot(SNAP_NAME1, VOLUME_NAME1) # no API to get and check resp = self.cl.promoteVirtualCopy(SNAP_NAME1) self.assertIsNotNone(resp['taskid']) self.printFooter('promote_virtual_copy') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test_25_promote_virtual_copy_on_replicated_volume(self): self.printHeader('promote_virtual_copy_on_replicated_volume') # Create empty remote copy group self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, REMOTE_COPY_TARGETS, optional={"domain": DOMAIN}) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Create volume optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(RC_VOLUME_NAME, CPG_NAME1, SIZE, optional) # Add volume to remote copy group self.cl.addVolumeToRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, RC_VOLUME_NAME, REMOTE_COPY_TARGETS) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) volumes = resp['volumes'] self.assertEqual(RC_VOLUME_NAME, volumes[0]['name']) self.cl.createSnapshot(SNAP_NAME1, RC_VOLUME_NAME) # Stop remote copy for the group self.cl.stopRemoteCopy(REMOTE_COPY_GROUP_NAME1) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] self.assertEqual(RCOPY_STOPPED, targets[0]['state']) optional = {'allowRemoteCopyParent': True} resp = self.cl.promoteVirtualCopy(SNAP_NAME1, optional) self.assertIsNotNone(resp['taskid']) # Start remote copy for the group self.cl.startRemoteCopy(REMOTE_COPY_GROUP_NAME1) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] self.assertEqual(RCOPY_STARTED, targets[0]['state']) self.printFooter('promote_virtual_copy_on_replicated_volume') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test_25_promote_virtual_copy_with_bad_param(self): self.printHeader('promote_virtual_copy_with_bad_param') self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE) # add one self.cl.createSnapshot(SNAP_NAME1, VOLUME_NAME1) # no API to get and check optional = {'online': True, 'allowRemoteCopyParent': True, 'priority': 1} self.assertRaises( exceptions.HTTPConflict, self.cl.promoteVirtualCopy, SNAP_NAME1, optional ) self.printFooter('promote_virtual_copy_with_bad_param') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test_25_promote_vcopy_on_rep_vol_with_bad_param(self): self.printHeader('promote_vcopy_on_rep_vol_with_bad_param') # Create empty remote copy group self.cl.createRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, REMOTE_COPY_TARGETS, optional={"domain": DOMAIN}) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) self.assertEqual(REMOTE_COPY_GROUP_NAME1, resp['name']) # Create volume optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(RC_VOLUME_NAME, CPG_NAME1, SIZE, optional) # Add volume to remote copy group self.cl.addVolumeToRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1, RC_VOLUME_NAME, REMOTE_COPY_TARGETS) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) volumes = resp['volumes'] self.assertEqual(RC_VOLUME_NAME, volumes[0]['name']) self.cl.createSnapshot(SNAP_NAME1, RC_VOLUME_NAME) # Stop remote copy for the group self.cl.stopRemoteCopy(REMOTE_COPY_GROUP_NAME1) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] self.assertEqual(RCOPY_STOPPED, targets[0]['state']) self.assertRaises( exceptions.HTTPForbidden, self.cl.promoteVirtualCopy, SNAP_NAME1, optional ) # Start remote copy for the group self.cl.startRemoteCopy(REMOTE_COPY_GROUP_NAME1) resp = self.cl.getRemoteCopyGroup(REMOTE_COPY_GROUP_NAME1) targets = resp['targets'] self.assertEqual(RCOPY_STARTED, targets[0]['state']) self.printFooter('promote_vcopy_on_rep_vol_with_bad_param') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test26_admit_rcopy_link(self): self.printHeader('admit_rcopy_link_test') res = self.cl.admitRemoteCopyLinks(TARGET_NAME, SOURCE_PORT, TARGET_PORT) self.assertEqual(res, []) self.printFooter('admit_rcopy_link_test') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test27_dismiss_rcopy_link(self): self.printHeader('dismiss_rcopy_link_test') res = self.cl.dismissRemoteCopyLinks(TARGET_NAME, SOURCE_PORT, TARGET_PORT) self.assertEqual(res, []) self.printFooter('dismiss_rcopy_link_test') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test28_start_rcopy(self): self.printHeader('start_rcopy_test') res = self.cl.startrCopy() self.assertEqual(res, []) self.printFooter('start_rcopy_test') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test29_admit_rcopy_target(self): self.printHeader('admit_rcopy_target_test') res = self.cl.admitRemoteCopyTarget(TARGET_NAME, MODE, REMOTE_COPY_GROUP_NAME1) self.assertEqual(res, []) self.printFooter('admit_rcopy_target_test') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test30_admit_rcopy_target(self): self.printHeader('admit_rcopy_target_test') res = self.cl.admitRemoteCopyTarget(TARGET_NAME, MODE, REMOTE_COPY_GROUP_NAME1, VOLUME_PAIR_LIST) self.assertEqual(res, []) self.printFooter('admit_rcopy_target_test') # TODO: Fix this later # @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) # def test31_dismiss_rcopy_target(self): # self.printHeader('dismiss_rcopy_target_test') # res = self.cl.dismissRemoteCopyTarget(TARGET_NAME, # REMOTE_COPY_GROUP_NAME1) # self.assertEqual(res, []) # self.printFooter('dismiss_rcopy_target_test') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test32_create_schedule(self): self.printHeader('create_schedule_test') cmd = "createsv -ro snap-" + VOLUME_NAME1 + " " + VOLUME_NAME1 res = self.cl.createSchedule(SCHEDULE_NAME1, cmd, 'hourly') self.assertEqual(res, None) self.printFooter('create_schedule_test') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test33_delete_schedule(self): self.printHeader('delete_schedule_test') res = self.cl.deleteSchedule(SCHEDULE_NAME1) self.assertEqual(res, None) self.printFooter('delete_schedule_test') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test34_modify_schedule(self): self.printHeader('modify_schedule_test') res = self.cl.modifySchedule(SCHEDULE_NAME1, {'newName': SCHEDULE_NAME2}) self.assertEqual(res, None) self.printFooter('modify_schedule_test') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test35_suspend_schedule(self): self.printHeader('suspend_schedule_test') res = self.cl.suspendSchedule(SCHEDULE_NAME1) self.assertEqual(res, None) self.printFooter('suspend_schedule_test') @unittest.skipIf(is_live_test(), SKIP_RCOPY_MESSAGE) def test36_resume_schedule(self): self.printHeader('resume_schedule_test') res = self.cl.resumeSchedule(SCHEDULE_NAME1) self.assertEqual(res, None) self.printFooter('resume_schedule_test') def test37_create_volume_with_primera_support_with_no_option(self): self.printHeader('create_volume') self.cl.primera_supported = True # add volume with no options specified, # it should create bydefault tpvv volume self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE) # check vol1 = self.cl.getVolume(VOLUME_NAME1) self.assertIsNotNone(vol1) volName = vol1['name'] self.assertEqual(VOLUME_NAME1, volName) self.printFooter('create_volume') def test38_create_volume_with_primera_support_with_option(self): self.printHeader('create_volume') self.cl.primera_supported = True # add one optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) # check vol1 = self.cl.getVolume(VOLUME_NAME1) self.assertIsNotNone(vol1) volName = vol1['name'] self.assertEqual(VOLUME_NAME1, volName) # add another compressed volume optional = {'comment': 'test volume2', 'compression': True, 'tdvv': True} self.cl.createVolume(VOLUME_NAME2, CPG_NAME2, 16384, optional) # check vol2 = self.cl.getVolume(VOLUME_NAME2) self.assertIsNotNone(vol2) volName = vol2['name'] comment = vol2['comment'] reduced = vol2['reduce'] self.assertEqual(VOLUME_NAME2, volName) self.assertEqual("test volume2", comment) self.assertEqual(True, reduced) def test38_create_volume_with_primera_support_with_option_None(self): self.printHeader('create_volume') self.cl.primera_supported = True # add one optional = {'comment': 'test volume', 'tpvv': None, 'compression': True, 'tdvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 16384, optional) # check vol1 = self.cl.getVolume(VOLUME_NAME1) self.assertIsNotNone(vol1) volName = vol1['name'] reduced = vol1['reduce'] comment = vol1['comment'] self.assertEqual(VOLUME_NAME1, volName) self.assertEqual("test volume", comment) self.assertEqual(True, reduced) # add another one optional = {'comment': 'test volume2', 'tpvv': True, 'compression': None, 'tdvv': None} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, SIZE, optional) # check vol2 = self.cl.getVolume(VOLUME_NAME2) self.assertIsNotNone(vol2) volName = vol2['name'] comment = vol2['comment'] self.assertEqual(VOLUME_NAME2, volName) self.assertEqual("test volume2", comment) self.printFooter('create_volume') def test_39_create_volume_badParams(self): self.printHeader('create_volume_badParams') self.cl.merlin_supported = True optional = {'comment': 'test volume', 'tpvv': "junk"} self.assertRaises( exceptions.HTTPBadRequest, self.cl.createVolume, VOLUME_NAME1, CPG_NAME1, SIZE, optional) self.printFooter('create_volume_badParams') def test_40_create_volume_badParams(self): self.printHeader('create_volume_badParams') self.cl.primera_supported = True optional = {'comment': 'test volume', 'compression': "junk", 'tdvv': "junk"} self.assertRaises( exceptions.HTTPBadRequest, self.cl.createVolume, VOLUME_NAME1, CPG_NAME1, SIZE, optional) self.printFooter('create_volume_badParams') def test_41_create_volume_junk_values(self): self.printHeader('create_volume_junkParams') self.cl.primera_supported = True optional = {'comment': 'test volume', 'tpvv': "junk", 'compression': "junk"} self.assertRaises( exceptions.HTTPBadRequest, self.cl.createVolume, VOLUME_NAME1, CPG_NAME1, SIZE, optional) self.printFooter('create_volume_junkParams') def test_42_create_volume_junk_compression(self): self.printHeader('create_volume_junkParams') self.cl.primera_supported = True optional = {'comment': 'test volume', 'tpvv': None, 'compression': "junk"} self.assertRaises( exceptions.HTTPBadRequest, self.cl.createVolume, VOLUME_NAME1, CPG_NAME1, SIZE, optional) self.printFooter('create_volume_junkParams') def test_43_create_volume_parameter_absent(self): self.printHeader('create_volume_noParams') self.cl.primera_supported = True optional = {'comment': 'test volume', 'compression': False} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) # check vol1 = self.cl.getVolume(VOLUME_NAME1) self.assertIsNotNone(vol1) volName = vol1['name'] comment = vol1['comment'] self.assertEqual(VOLUME_NAME1, volName) self.assertEqual("test volume", comment) # add another one optional = {'comment': 'test volume2', 'tpvv': False} self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, SIZE, optional) # check vol2 = self.cl.getVolume(VOLUME_NAME2) self.assertIsNotNone(vol2) volName = vol2['name'] comment = vol2['comment'] self.assertEqual(VOLUME_NAME2, volName) self.assertEqual("test volume2", comment) self.printFooter('create_volume_noParams') def test_44_offline_copy_volume_primera_support(self): self.printHeader('copy_volume') self.cl.primera_supported = True # add one optional = {'comment': 'test volume', 'tpvv': True, 'snapCPG': CPG_NAME1} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, 1024, optional) self.cl.createVolume(VOLUME_NAME2, CPG_NAME1, 1024, optional) # copy it optional1 = {'online': False} self.cl.copyVolume(VOLUME_NAME1, VOLUME_NAME2, CPG_NAME1, optional1) vol2 = self.cl.getVolume(VOLUME_NAME2) volName = vol2['name'] self.assertEqual(VOLUME_NAME2, volName) self.printFooter('copy_volume') def test_45_online_copy_volume_primera_support(self): self.printHeader('copy_volume') self.cl.primera_supported = True # TODO: Add support for ssh/stopPhysical copy in mock mode if self.unitTest: self.printFooter('copy_volume') return # add one optional = {'comment': 'test volume', 'tpvv': True, 'snapCPG': CPG_NAME1} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) # copy it # for online copy we need to specify the tpvv/reduce for merlin optional = {'online': True, 'tpvv': True} self.cl.copyVolume(VOLUME_NAME1, VOLUME_NAME2, CPG_NAME1, optional) vol2 = self.cl.getVolume(VOLUME_NAME2) volName = vol2['name'] self.assertEqual(VOLUME_NAME2, volName) self.printFooter('copy_volume') def test_46_copy_volume_interrupted_primera_support(self): self.printHeader('copy_volume') self.cl.primera_supported = True # TODO: Add support for ssh/stopPhysical copy in mock mode if self.unitTest: self.printFooter('copy_volume') return # add one optional = {'comment': 'test volume', 'tpvv': True, 'snapCPG': CPG_NAME1} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) # copy it optional = {'online': True, 'tpvv': True} self.cl.copyVolume(VOLUME_NAME1, VOLUME_NAME2, CPG_NAME1, optional) self.cl.getVolume(VOLUME_NAME2) self.cl.stopOnlinePhysicalCopy(VOLUME_NAME2) self.assertRaises( exceptions.HTTPNotFound, self.cl.getVolume, VOLUME_NAME2 ) self.printFooter('copy_volume') def test_47_create_default_volume(self): self.printHeader('create_volume') self.cl.primera_supported = True # add one optional = {'comment': 'test volume', 'tpvv': True, 'compression': False} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) # check vol1 = self.cl.getVolume(VOLUME_NAME1) self.assertIsNotNone(vol1) volName = vol1['name'] self.assertEqual(VOLUME_NAME1, volName) def test_48_tune_volume_to_dedup_compressed_on_primera(self): self.printHeader('convert_to_deco') self.cl.primera_supported = True self.cl.compression_supported = True optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) usr_cpg = USR_CPG optional = {'userCPG': "UserCPG", 'conversionOperation': CONVERT_TO_DECO, 'keepVV': "keep_vv", 'compression': False} self.cl.tuneVolume(VOLUME_NAME1, usr_cpg, optional) vol2 = self.cl.getVolume(VOLUME_NAME1) self.assertEqual(vol2['tdvv'], True) self.assertEqual(vol2['compression'], True) self.printFooter('convert_to_deco') def test_49_tune_volume_to_full_on_primera(self): self.printHeader('convert_to_full') self.cl.primera_supported = True self.cl.compression_supported = True optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) usr_cpg = USR_CPG optional = {'userCPG': "UserCPG", 'conversionOperation': FPVV, 'keepVV': "keep_vv", 'compression': False} self.assertRaises( exceptions.HTTPBadRequest, self.cl.tuneVolume, VOLUME_NAME1, usr_cpg, optional) self.printFooter('convert_to_full') def test_50_tune_volume_to_dedup_on_primera(self): self.printHeader('convert_to_dedup') self.cl.primera_supported = True self.cl.compression_supported = True optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) usr_cpg = USR_CPG optional = {'userCPG': "UserCPG", 'conversionOperation': TDVV, 'keepVV': "keep_vv", 'compression': False} self.assertRaises( exceptions.HTTPBadRequest, self.cl.tuneVolume, VOLUME_NAME1, usr_cpg, optional) self.printFooter('convert_to_dedup') def test_51_tune_volume_to_thin_compressed_on_primera(self): self.printHeader('convert_to_thin_compressed') self.cl.primera_supported = True self.cl.compression_supported = True optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) usr_cpg = USR_CPG optional = {'userCPG': "UserCPG", 'conversionOperation': TPVV, 'keepVV': "keep_vv", 'compression': True} self.assertRaises( exceptions.HTTPBadRequest, self.cl.tuneVolume, VOLUME_NAME1, usr_cpg, optional) self.printFooter('convert_to_thin_compressed') def test_52_tune_volume_with_bad_parameter_primera(self): self.printHeader('tune_volume_with_bad_param') self.cl.primera_supported = True self.cl.compression_supported = True optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) usr_cpg = USR_CPG optional = {'xyz': "UserCPG", 'conversionOperation': TPVV, 'keepVV': "keep_vv", 'compression': True} self.assertRaises( exceptions.HTTPBadRequest, self.cl.tuneVolume, VOLUME_NAME1, usr_cpg, optional) self.printFooter('tune_volume_with_bad_param') def test_53_tune_volume_with_invalid_conversion_operation(self): self.printHeader('tune_volume_with_invalid_conversion_operation') self.cl.primera_supported = True self.cl.compression_supported = True optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) usr_cpg = USR_CPG optional = {'userCPG': "UserCPG", 'conversionOperation': INVALID_PROVISIONING_TYPE, 'keepVV': "keep_vv", 'compression': True} self.assertRaises( exceptions.HTTPBadRequest, self.cl.tuneVolume, VOLUME_NAME1, usr_cpg, optional) self.printFooter('tune_volume_with_invalid_conversion_operation') def test_54_tune_volume_with_invalid_compression_value(self): self.printHeader('tune_volume_with_invalid_compression_value') self.cl.primera_supported = True self.cl.compression_supported = True optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) usr_cpg = USR_CPG optional = {'userCPG': "UserCPG", 'conversionOperation': FPVV, 'keepVV': "keep_vv", 'compression': "xyz"} self.assertRaises( exceptions.HTTPBadRequest, self.cl.tuneVolume, VOLUME_NAME1, usr_cpg, optional) self.printFooter('tune_volume_with_invalid_compression_value') def test_55_tune_volume_with_invalid_usercpg_value(self): self.printHeader('tune_volume_with_invalid_usercpg_value') self.cl.primera_supported = True self.cl.compression_supported = True optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) usr_cpg = INVALID_CPG optional = {'userCPG': "UserCPG", 'conversionOperation': TPVV, 'keepVV': "keep_vv", 'compression': False} self.assertRaises( exceptions.HTTPBadRequest, self.cl.tuneVolume, VOLUME_NAME1, usr_cpg, optional) self.printFooter('tune_volume_with_invalid_usercpg_value') def test_56_tune_volume_with_exceeded_length_of_keepvv(self): self.printHeader('tune_volume_with_exceeded_length_of_keepvv') self.cl.primera_supported = True self.cl.compression_supported = True optional = {'comment': 'test volume', 'tpvv': True} self.cl.createVolume(VOLUME_NAME1, CPG_NAME1, SIZE, optional) usr_cpg = USR_CPG optional = {'userCPG': "UserCPG", 'conversionOperation': TPVV, 'keepVV': "asdfjslfjsldkjfasdjlksjdflsdjakjsdlkfjsdjdsdlf", 'compression': False} self.assertRaises( exceptions.HTTPBadRequest, self.cl.tuneVolume, VOLUME_NAME1, usr_cpg, optional) self.printFooter('tune_volume_with_exceeded_length_of_keepvv') # testing # suite = unittest.TestLoader(). # loadTestsFromTestCase(HPE3ParClientVolumeTestCase) # unittest.TextTestRunner(verbosity=2).run(suite) python-3parclient-4.2.12/test/test_HTTPJSONRESTClient.py0000644000000000000000000001134514106434774022635 0ustar rootroot00000000000000# (c) Copyright 2015 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Test class of 3PAR Client handling HTTPJSONRESTClient.""" import unittest import mock import requests from hpe3parclient import exceptions from hpe3parclient import http class HTTPJSONRESTClientTestCase(unittest.TestCase): http = None def setUp(self): fake_url = 'http://fake-url:0000' self.http = http.HTTPJSONRESTClient(fake_url, secure=False, http_log_debug=True, suppress_ssl_warnings=False, timeout=None) def tearDown(self): self.http = None def test_cs_request(self): url = "fake-url" method = 'GET' # Test for HTTPUnauthorized self.http._time_request = mock.Mock() ex = exceptions.HTTPUnauthorized() self.http._time_request.side_effect = ex self.http._do_reauth = mock.Mock() resp = 'fake_response' body = 'fake_body' self.http._do_reauth.return_value = (resp, body) self.http._cs_request(url, method) self.http._time_request.assert_called() self.http._do_reauth.assert_called_with(url, method, ex) # Test for HTTPForbidden ex = exceptions.HTTPForbidden() self.http._time_request.side_effect = ex self.http._cs_request(url, method) self.http._time_request.assert_called() self.http._do_reauth.assert_called_with(url, method, ex) def test_do_reauth_exception(self): url = "fake-url" method = 'GET' ex = exceptions.HTTPUnauthorized self.http.auth_try = 2 self.http._reauth = mock.Mock() self.http._time_request = mock.Mock() self.http._time_request.side_effect = ex self.assertRaises(ex, self.http._do_reauth, url, method, ex) self.http._reauth.assert_called() def test_do_reauth_with_auth_try_condition_false(self): ex = exceptions.HTTPUnauthorized url = "fake-url" method = 'GET' self.http.auth_try = 1 self.assertRaises(ex, self.http._do_reauth, url, method, ex) def test_request(self): self.http._http_log_req = mock.Mock() self.http.timeout = 10 retest = mock.Mock() http_method = 'fake this' http_url = 'http://fake-url:0000' with mock.patch('requests.request', retest, create=True): # Test timeout exception retest.side_effect = requests.exceptions.Timeout self.assertRaises(exceptions.Timeout, self.http.request, http_url, http_method) # Test too many redirects exception retest.side_effect = requests.exceptions.TooManyRedirects self.assertRaises(exceptions.TooManyRedirects, self.http.request, http_url, http_method) # Test HTTP Error exception retest.side_effect = requests.exceptions.HTTPError self.assertRaises(exceptions.HTTPError, self.http.request, http_url, http_method) # Test URL required exception retest.side_effect = requests.exceptions.URLRequired self.assertRaises(exceptions.URLRequired, self.http.request, http_url, http_method) # Test request exception retest.side_effect = requests.exceptions.RequestException self.assertRaises(exceptions.RequestException, self.http.request, http_url, http_method) # Test requests exception retest.side_effect = requests.exceptions.SSLError self.assertRaisesRegexp(exceptions.SSLCertFailed, "failed") self.assertEqual(self.http.timeout, 10) # Test retry exception retest.side_effect = requests.exceptions.ConnectionError self.assertRaises(requests.exceptions.ConnectionError, self.http.request, http_url, http_method) python-3parclient-4.2.12/LICENSE.txt0000644000000000000000000002613614106434774016766 0ustar rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. python-3parclient-4.2.12/README.rst0000644000000000000000000001522214106676444016626 0ustar rootroot00000000000000.. image:: https://img.shields.io/pypi/v/python-3parclient.svg :target: https://pypi.python.org/pypi/python-3parclient :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/python-3parclient.svg :target: https://pypi.python.org/pypi/python-3parclient :alt: Downloads HPE Alletra 9000 and HPE Primera and HPE 3PAR REST Client ========================================================= This is a Client library that can talk to the HPE Alletra 9000 and Primera and 3PAR Storage array. The HPE Alletra 9000 and Primera and 3PAR storage array has a REST web service interface and a command line interface. This client library implements a simple interface for talking with either interface, as needed. The python Requests library is used to communicate with the REST interface. The python paramiko library is used to communicate with the command line interface over an SSH connection. This is the new location for the rebranded HP 3PAR Rest Client and will be where all future releases are made. It was previously located on PyPi at: https://pypi.python.org/pypi/hp3parclient The GitHub repository for the old HP 3PAR Rest Client is located at: https://github.com/hpe-storage/python-3parclient/tree/3.x The HP 3PAR Rest Client (hp3parclient) is now considered deprecated. Requirements ============ This branch requires 3.1.3 version MU1 or later of the HPE 3PAR firmware. This branch requires 4.3.1 version of the HPE Primera firmware. This branch requires 9.3.0 version of the HPE Alletra 9000 firmware. File Persona capabilities require HPE 3PAR firmware 3.2.1 Build 46 or later. Capabilities ============ * Create Volume * Delete Volume * Get all Volumes * Get a Volume * Modify a Volume * Copy a Volume * Create a Volume Snapshot * Create CPG * Delete CPG * Get all CPGs * Get a CPG * Get a CPG's Available Space * Create a VLUN * Delete a VLUN * Get all VLUNs * Get a VLUN * Create a Host * Delete a Host * Get all Hosts * Get a Host * Get VLUNs for a Host * Find a Host * Find a Host Set for a Host * Get all Host Sets * Get a Host Set * Create a Host Set * Delete a Host Set * Modify a Host Set * Get all Ports * Get iSCSI Ports * Get FC Ports * Get IP Ports * Set Volume Metadata * Get Volume Metadata * Get All Volume Metadata * Find Volume Metadata * Remove Volume Metadata * Create a Volume Set * Delete a Volume Set * Modify a Volume Set * Get a Volume Set * Get all Volume Sets * Find one Volume Set containing a specified Volume * Find all Volume Sets containing a specified Volume * Create a QOS Rule * Modify a QOS Rule * Delete a QOS Rule * Set a QOS Rule * Query a QOS Rule * Query all QOS Rules * Get a Task * Get all Tasks * Get a Patch * Get all Patches * Get WSAPI Version * Get WSAPI Configuration Info * Get Storage System Info * Get Overall System Capacity * Stop Online Physical Copy * Query Online Physical Copy Status * Stop Offline Physical Copy * Resync Physical Copy * Query Remote Copy Info * Query a Remote Copy Group * Query all Remote Copy Groups * Create a Remote Copy Group * Delete a Remote Copy Group * Modify a Remote Copy Group * Add a Volume to a Remote Copy Group * Remove a Volume from a Remote Copy Group * Start Remote Copy on a Remote Copy Group * Stop Remote Copy on a Remote Copy Group * Synchronize a Remote Copy Group * Recover a Remote Copy Group from a Disaster * Enable/Disable Config Mirroring on a Remote Copy Target * Get Remote Copy Group Volumes * Get Remote Copy Group Volume * Admit Remote Copy Link * Dismiss Remote Copy Link * Start Remote Copy * Remote Copy Service Exists Check * Get Remote Copy Link * Remote Copy Link Exists Check * Admit Remote Copy Target * Dismiss Remote Copy Target * Target In Remote Copy Group Exists Check * Remote Copy Group Status Check * Remote Copy Group Status Started Check * Remote Copy Group Status Stopped Check * Create Schedule * Delete Schedule * Get Schedule * Modify Schedule * Suspend Schedule * Resume Schedule * Get Schedule Status * Promote Virtual Copy * Get a Flash Cache * Create a Flash Cache * Delete a Flash Cache File Persona Capabilities ========================= * Get File Services Info * Create a File Provisioning Group * Grow a File Provisioning Group * Get File Provisioning Group Info * Modify a File Provisioning Group * Remove a File Provisioning Group * Create a Virtual File Server * Get Virtual File Server Info * Modify a Virtual File Server * Remove a Virtual File Server * Assign an IP Address to a Virtual File Server * Get the Network Config of a Virtual File Server * Modify the Network Config of a Virtual File Server * Remove the Network Config of a Virtual File Server * Create a File Services User Group * Modify a File Services User Group * Remove a File Services User Group * Create a File Services User * Modify a File Services User * Remove a File Services User * Create a File Store * Get File Store Info * Modify a File Store * Remove a File Store * Create a File Share * Get File Share Info * Modify a File Share * Remove a File Share * Create a File Store Snapshot * Get File Store Snapshot Info * Remove a File Store Snapshot * Reclaim Space from Deleted File Store Snapshots * Get File Store Snapshot Reclamation Info * Stop or Pause a File Store Snapshot Reclamation Task * Set File Services Quotas * Get Files Services Quota Info Installation ============ To install from source:: $ sudo pip install . To install from http://pypi.org:: $ sudo pip install python-3parclient Unit Tests ========== To run all unit tests:: $ tox -e py27 To run a specific test:: $ tox -e py27 -- test/file.py:class_name.test_method_name To run all unit tests with code coverage:: $ tox -e cover The output of the coverage tests will be placed into the ``coverage`` dir. Folders ======= * docs -- contains the documentation. * hpe3parclient -- the actual client.py library * test -- unit tests * samples -- some sample uses Documentation ============= To build the documentation:: $ tox -e docs To view the built documentation point your browser to:: docs/html/index.html Running Simulators ================== The unit tests should automatically start/stop the simulators. To start them manually use the following commands. To stop them, use 'kill'. Starting them manually before running unit tests also allows you to watch the debug output. * WSAPI:: $ python test/HPE3ParMockServer_flask.py -port 5001 -user -password -debug * SSH:: $ python test/HPE3ParMockServer_ssh.py [port] Building wheel dist =================== This client now supports building via the new python WHEELS standard. Take a look at http://pythonwheels.com * building:: $ python setup.py bdist_wheel * building and uploading:: $ python setup.py sdist bdist_wheel upload python-3parclient-4.2.12/setup.cfg0000644000000000000000000000054714106677657016773 0ustar rootroot00000000000000[aliases] test = pytest [tool:pytest] addopts = --verbose --tc-file=config.ini --ignore-glob=samples/* python_files = test_*.py [tool:pytest:cover] addopts = --verbose --tc-file=config.ini --ignore-glob=samples/* --workers=3 python_files = test_*.py [bdist_wheel] universal = 1 [metadata] license_file = LICENSE.txt [egg_info] tag_build = tag_date = 0 python-3parclient-4.2.12/setup.py0000644000000000000000000000255414106676444016655 0ustar rootroot00000000000000import hpe3parclient try: from setuptools import setup, find_packages except ImportError: from distutils.core import setup, find_packages with open('README.rst') as f: readme = f.read() setup( name='python-3parclient', version=hpe3parclient.version, description="HPE Alletra 9000 and HPE Primera and HPE 3PAR HTTP REST Client", long_description=readme, author="Walter A. Boring IV", author_email="walter.boring@hpe.com", maintainer="Walter A. Boring IV", keywords=["hpe", "3par", "rest"], requires=['paramiko', 'eventlet', 'requests'], install_requires=['paramiko', 'eventlet', 'requests'], tests_require=["pytest", "pytest-runner", "pytest-testconfig", "flask", "werkzeug", "requests", "pytest-cov"], license="Apache License, Version 2.0", packages=find_packages(), provides=['hpe3parclient'], url="http://packages.python.org/python-3parclient", classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Environment :: Web Environment', 'Programming Language :: Python', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Topic :: Internet :: WWW/HTTP', ] ) python-3parclient-4.2.12/PKG-INFO0000644000000000000000000001713014106677657016243 0ustar rootroot00000000000000Metadata-Version: 2.1 Name: python-3parclient Version: 4.2.12 Summary: HPE Alletra 9000 and HPE Primera and HPE 3PAR HTTP REST Client Home-page: http://packages.python.org/python-3parclient Author: Walter A. Boring IV Author-email: walter.boring@hpe.com Maintainer: Walter A. Boring IV License: Apache License, Version 2.0 Keywords: hpe,3par,rest Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Environment :: Web Environment Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Topic :: Internet :: WWW/HTTP Requires: paramiko Requires: eventlet Requires: requests Provides: hpe3parclient License-File: LICENSE.txt .. image:: https://img.shields.io/pypi/v/python-3parclient.svg :target: https://pypi.python.org/pypi/python-3parclient :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/python-3parclient.svg :target: https://pypi.python.org/pypi/python-3parclient :alt: Downloads HPE Alletra 9000 and HPE Primera and HPE 3PAR REST Client ========================================================= This is a Client library that can talk to the HPE Alletra 9000 and Primera and 3PAR Storage array. The HPE Alletra 9000 and Primera and 3PAR storage array has a REST web service interface and a command line interface. This client library implements a simple interface for talking with either interface, as needed. The python Requests library is used to communicate with the REST interface. The python paramiko library is used to communicate with the command line interface over an SSH connection. This is the new location for the rebranded HP 3PAR Rest Client and will be where all future releases are made. It was previously located on PyPi at: https://pypi.python.org/pypi/hp3parclient The GitHub repository for the old HP 3PAR Rest Client is located at: https://github.com/hpe-storage/python-3parclient/tree/3.x The HP 3PAR Rest Client (hp3parclient) is now considered deprecated. Requirements ============ This branch requires 3.1.3 version MU1 or later of the HPE 3PAR firmware. This branch requires 4.3.1 version of the HPE Primera firmware. This branch requires 9.3.0 version of the HPE Alletra 9000 firmware. File Persona capabilities require HPE 3PAR firmware 3.2.1 Build 46 or later. Capabilities ============ * Create Volume * Delete Volume * Get all Volumes * Get a Volume * Modify a Volume * Copy a Volume * Create a Volume Snapshot * Create CPG * Delete CPG * Get all CPGs * Get a CPG * Get a CPG's Available Space * Create a VLUN * Delete a VLUN * Get all VLUNs * Get a VLUN * Create a Host * Delete a Host * Get all Hosts * Get a Host * Get VLUNs for a Host * Find a Host * Find a Host Set for a Host * Get all Host Sets * Get a Host Set * Create a Host Set * Delete a Host Set * Modify a Host Set * Get all Ports * Get iSCSI Ports * Get FC Ports * Get IP Ports * Set Volume Metadata * Get Volume Metadata * Get All Volume Metadata * Find Volume Metadata * Remove Volume Metadata * Create a Volume Set * Delete a Volume Set * Modify a Volume Set * Get a Volume Set * Get all Volume Sets * Find one Volume Set containing a specified Volume * Find all Volume Sets containing a specified Volume * Create a QOS Rule * Modify a QOS Rule * Delete a QOS Rule * Set a QOS Rule * Query a QOS Rule * Query all QOS Rules * Get a Task * Get all Tasks * Get a Patch * Get all Patches * Get WSAPI Version * Get WSAPI Configuration Info * Get Storage System Info * Get Overall System Capacity * Stop Online Physical Copy * Query Online Physical Copy Status * Stop Offline Physical Copy * Resync Physical Copy * Query Remote Copy Info * Query a Remote Copy Group * Query all Remote Copy Groups * Create a Remote Copy Group * Delete a Remote Copy Group * Modify a Remote Copy Group * Add a Volume to a Remote Copy Group * Remove a Volume from a Remote Copy Group * Start Remote Copy on a Remote Copy Group * Stop Remote Copy on a Remote Copy Group * Synchronize a Remote Copy Group * Recover a Remote Copy Group from a Disaster * Enable/Disable Config Mirroring on a Remote Copy Target * Get Remote Copy Group Volumes * Get Remote Copy Group Volume * Admit Remote Copy Link * Dismiss Remote Copy Link * Start Remote Copy * Remote Copy Service Exists Check * Get Remote Copy Link * Remote Copy Link Exists Check * Admit Remote Copy Target * Dismiss Remote Copy Target * Target In Remote Copy Group Exists Check * Remote Copy Group Status Check * Remote Copy Group Status Started Check * Remote Copy Group Status Stopped Check * Create Schedule * Delete Schedule * Get Schedule * Modify Schedule * Suspend Schedule * Resume Schedule * Get Schedule Status * Promote Virtual Copy * Get a Flash Cache * Create a Flash Cache * Delete a Flash Cache File Persona Capabilities ========================= * Get File Services Info * Create a File Provisioning Group * Grow a File Provisioning Group * Get File Provisioning Group Info * Modify a File Provisioning Group * Remove a File Provisioning Group * Create a Virtual File Server * Get Virtual File Server Info * Modify a Virtual File Server * Remove a Virtual File Server * Assign an IP Address to a Virtual File Server * Get the Network Config of a Virtual File Server * Modify the Network Config of a Virtual File Server * Remove the Network Config of a Virtual File Server * Create a File Services User Group * Modify a File Services User Group * Remove a File Services User Group * Create a File Services User * Modify a File Services User * Remove a File Services User * Create a File Store * Get File Store Info * Modify a File Store * Remove a File Store * Create a File Share * Get File Share Info * Modify a File Share * Remove a File Share * Create a File Store Snapshot * Get File Store Snapshot Info * Remove a File Store Snapshot * Reclaim Space from Deleted File Store Snapshots * Get File Store Snapshot Reclamation Info * Stop or Pause a File Store Snapshot Reclamation Task * Set File Services Quotas * Get Files Services Quota Info Installation ============ To install from source:: $ sudo pip install . To install from http://pypi.org:: $ sudo pip install python-3parclient Unit Tests ========== To run all unit tests:: $ tox -e py27 To run a specific test:: $ tox -e py27 -- test/file.py:class_name.test_method_name To run all unit tests with code coverage:: $ tox -e cover The output of the coverage tests will be placed into the ``coverage`` dir. Folders ======= * docs -- contains the documentation. * hpe3parclient -- the actual client.py library * test -- unit tests * samples -- some sample uses Documentation ============= To build the documentation:: $ tox -e docs To view the built documentation point your browser to:: docs/html/index.html Running Simulators ================== The unit tests should automatically start/stop the simulators. To start them manually use the following commands. To stop them, use 'kill'. Starting them manually before running unit tests also allows you to watch the debug output. * WSAPI:: $ python test/HPE3ParMockServer_flask.py -port 5001 -user -password -debug * SSH:: $ python test/HPE3ParMockServer_ssh.py [port] Building wheel dist =================== This client now supports building via the new python WHEELS standard. Take a look at http://pythonwheels.com * building:: $ python setup.py bdist_wheel * building and uploading:: $ python setup.py sdist bdist_wheel upload