From 3695ee6fb8c105a717886d973251cce9d297854c Mon Sep 17 00:00:00 2001 From: susmita-poddar Date: Wed, 13 Aug 2025 10:55:15 +0530 Subject: [PATCH 01/11] Making cinder layer thin and putting changes in client code - 1st cut --- hpe3parclient/client.py | 405 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 405 insertions(+) diff --git a/hpe3parclient/client.py b/hpe3parclient/client.py index ef5f31a..7b22a46 100644 --- a/hpe3parclient/client.py +++ b/hpe3parclient/client.py @@ -31,6 +31,9 @@ import time import uuid import logging +import ast + +from oslo_serialization import base64 try: # For Python 3.0 and later @@ -205,6 +208,40 @@ class HPE3ParClient(object): DEFAULT_NVME_PORT = 4420 DEFAULT_PORT_NQN = 'nqn.2014-08.org.nvmexpress.discovery' + hpe3par_valid_keys = ['cpg', 'snap_cpg', 'provisioning', 'persona', 'vvs', + 'flash_cache', 'compression', 'group_replication', + 'convert_to_base'] + + # Valid values for volume type extra specs + # The first value in the list is the default value + valid_prov_values = ['thin', 'full', 'dedup'] + + valid_persona_values = ['2 - Generic-ALUA', + '1 - Generic', + '3 - Generic-legacy', + '4 - HPUX-legacy', + '5 - AIX-legacy', + '6 - EGENERA', + '7 - ONTAP-legacy', + '8 - VMware', + '9 - OpenVMS', + '10 - HPUX', + '11 - WindowsServer'] + + + # v2 replication constants + SYNC = 1 + PERIODIC = 2 + EXTRA_SPEC_REP_MODE = "replication:mode" + EXTRA_SPEC_REP_SYNC_PERIOD = "replication:sync_period" + RC_ACTION_CHANGE_TO_PRIMARY = 7 + DEFAULT_REP_MODE = 'periodic' + DEFAULT_SYNC_PERIOD = 900 + RC_GROUP_STARTED = 3 + SYNC_STATUS_COMPLETED = 3 + FAILBACK_VALUE = 'default' + + def __init__(self, api_url, debug=False, secure=False, timeout=None, suppress_ssl_warnings=False): self.api_url = api_url @@ -5373,4 +5410,372 @@ def remove_vlun_nvme(self, vol_name_3par, hostname, host_nqn): " belongs to different host: %(vlun_host)s", {'lun': vlun['lun'], 'vlun_host': vlun.get('hostname', 'Unknown')}) + + + + def _check_license_enabled(self, systemVersion, valid_licenses, + license_to_check, capability): + """Check a license against valid licenses on the array.""" + major_minor = systemVersion.split('.') + if int(major_minor[0]) == 10 and int(major_minor[1]) >= 5: + return True + else: + if valid_licenses: + for license in valid_licenses: + if license_to_check in license.get('name'): + return True + logger.debug("'%(capability)s' requires a '%(license)s' " + "license which is not installed.", + {'capability': capability, + 'license': license_to_check}) + return False + + + @staticmethod + def _encode_name(name): + uuid_str = name.replace("-", "") + vol_uuid = uuid.UUID('urn:uuid:%s' % uuid_str) + vol_encoded = base64.encode_as_text(vol_uuid.bytes) + + # 3par doesn't allow +, nor / + vol_encoded = vol_encoded.replace('+', '.') + vol_encoded = vol_encoded.replace('/', '-') + # strip off the == as 3par doesn't like those. + vol_encoded = vol_encoded.replace('=', '') + return vol_encoded + + + def _get_3par_ums_name(self, snapshot_id): + ums_name = self._encode_name(snapshot_id) + return "ums-%s" % ums_name + + + def _get_3par_vvs_name(self, volume_id): + vvs_name = self._encode_name(volume_id) + return "vvs-%s" % vvs_name + + + def _get_3par_unm_name(self, volume_id): + unm_name = self._encode_name(volume_id) + return "unm-%s" % unm_name + + + def _get_existing_volume_ref_name_client(self, existing_ref, is_snapshot): + """Returns the volume name of an existing reference. + + Checks if an existing volume reference has a source-name or + source-id element. If source-name or source-id is not present an + error will be thrown. + """ + vol_name = None + if 'source-name' in existing_ref: + vol_name = existing_ref['source-name'] + elif 'source-id' in existing_ref: + if is_snapshot: + vol_name = self._get_3par_ums_name(existing_ref['source-id']) + else: + vol_name = self._get_3par_unm_name(existing_ref['source-id']) + else: + reason = "Reference must contain source-name or source-id." + raise exceptions.ClientException(reason) + return vol_name + + + def _get_3par_vol_comment_value(self, vol_comment, key): + comment_dict = dict(ast.literal_eval(vol_comment)) + if key in comment_dict: + return comment_dict[key] + return None + + + @staticmethod + def _add_name_id_to_comment(comment, volume): + name_id = volume.get('_name_id') + if name_id: + comment['_name_id'] = name_id + + + + def _get_3par_snap_name(self, snapshot_id, temp_snap=False): + snapshot_name = self._encode_name(snapshot_id) + if temp_snap: + # is this a temporary snapshot + # this is done during cloning + prefix = "tss-%s" + else: + prefix = "oss-%s" + return prefix % snapshot_name + + + + def is_volume_group_snap_type(self, volume_type): + consis_group_snap_type = False + if volume_type: + extra_specs = volume_type.get('extra_specs') + if 'consistent_group_snapshot_enabled' in extra_specs: + gsnap_val = extra_specs['consistent_group_snapshot_enabled'] + consis_group_snap_type = (gsnap_val == " True") + return consis_group_snap_type + + + def _is_volume_type_replicated(self, volume_type): + replicated_type = False + extra_specs = volume_type.get('extra_specs') + if extra_specs and 'replication_enabled' in extra_specs: + rep_val = extra_specs['replication_enabled'] + replicated_type = (rep_val == " True") + + return replicated_type + + + def _get_3par_rcg_name_of_group(self, group_id): + rcg_name = self._encode_name(group_id) + rcg = "rcg-%s" % rcg_name + return rcg[:22] + + + + def _get_3par_remote_rcg_name_of_group(self, group_id, provider_location): + return self._get_3par_rcg_name_of_group(group_id) + ".r" + ( + str(provider_location)) + + + def _get_keys_by_volume_type(self, volume_type): + hpe3par_keys = {} + specs = volume_type.get('extra_specs') + for key, value in specs.items(): + if ':' in key: + fields = key.split(':') + key = fields[1] + if key in self.hpe3par_valid_keys: + hpe3par_keys[key] = value + return hpe3par_keys + + + def _get_hpe3par_tiramisu_value(self, volume_type): + hpe3par_tiramisu = False + hpe3par_keys = self._get_keys_by_volume_type(volume_type) + if hpe3par_keys.get('group_replication'): + hpe3par_tiramisu = ( + hpe3par_keys['group_replication'] == " True") + + return hpe3par_tiramisu + + + def _get_key_value(self, hpe3par_keys, key, default=None): + if hpe3par_keys is not None and key in hpe3par_keys: + return hpe3par_keys[key] + else: + return default + + + def _get_boolean_key_value(self, hpe3par_keys, key, default=False): + value = self._get_key_value( + hpe3par_keys, key, default) + if isinstance(value, str): + if value.lower() == 'true': + value = True + else: + value = False + return value + + + def _get_3par_vol_comment(self, volume_name): + vol = self.getVolume(volume_name) + if 'comment' in vol: + return vol['comment'] + return None + + + def _get_remote_copy_mode_num(self, mode): + ret_mode = None + if mode == "sync": + ret_mode = self.SYNC + if mode == "periodic": + ret_mode = self.PERIODIC + return ret_mode + + + def _get_replication_mode_from_volume_type(self, volume_type): + # Default replication mode is PERIODIC + replication_mode_num = self.PERIODIC + extra_specs = volume_type.get("extra_specs") + if extra_specs: + replication_mode = extra_specs.get( + self.EXTRA_SPEC_REP_MODE, self.DEFAULT_REP_MODE) + + replication_mode_num = self._get_remote_copy_mode_num( + replication_mode) + + return replication_mode_num + + def _is_replication_mode_correct(self, mode, sync_num): + rep_flag = True + # Make sure replication_mode is set to either sync|periodic. + mode = self._get_remote_copy_mode_num(mode) + if not mode: + logger.error("Extra spec replication:mode must be set and must " + "be either 'sync' or 'periodic'.") + rep_flag = False + else: + # If replication:mode is periodic, replication_sync_period must be + # set between 300 - 31622400 seconds. + if mode == self.PERIODIC and ( + sync_num < 300 or sync_num > 31622400): + logger.error("Extra spec replication:sync_period must be " + "greater than 299 and less than 31622401 " + "seconds.") + rep_flag = False + return rep_flag + + + def _are_targets_in_their_natural_direction(self, rcg): + + targets = rcg['targets'] + for target in targets: + if target['roleReversed'] or ( + target['state'] != self.RC_GROUP_STARTED): + return False + + # Make sure all volumes are fully synced. + volumes = rcg['volumes'] + for volume in volumes: + remote_volumes = volume['remoteVolumes'] + for remote_volume in remote_volumes: + if remote_volume['syncStatus'] != ( + self.SYNC_STATUS_COMPLETED): + return False + return True + + + def _is_group_in_remote_copy_group(self, group): + rcg_name = self._get_3par_rcg_name_of_group(group.id) + try: + self.getRemoteCopyGroup(rcg_name) + return True + except exceptions.HTTPNotFound: + return False + + + def _get_cpg_from_cpg_map(self, cpg_map, target_cpg): + ret_target_cpg = None + cpg_pairs = cpg_map.split(' ') + for cpg_pair in cpg_pairs: + cpgs = cpg_pair.split(':') + cpg = cpgs[0] + dest_cpg = cpgs[1] + if cpg == target_cpg: + ret_target_cpg = dest_cpg + + return ret_target_cpg + + + def _generate_hpe3par_cpgs(self, cpg_map): + hpe3par_cpgs = [] + cpg_pairs = cpg_map.split(' ') + for cpg_pair in cpg_pairs: + cpgs = cpg_pair.split(':') + hpe3par_cpgs.append(cpgs[1]) + + return hpe3par_cpgs + + + def disable_replication_client(self, group, volumes): + """Disable replication for a group. + + :param group: the group object + :param volumes: the list of volumes + :returns: model_update, None + """ + + model_update = {} + if not group.is_replicated: + raise NotImplementedError() + + if not volumes: + # Return if empty group + return model_update + + try: + vvs_name = self._get_3par_vvs_name(group.id) + rcg_name = self._get_3par_rcg_name_of_group(group.id) + + # Check VV and RCG exist on 3par, + # if RCG exist then stop RCG + self.getVolumeSet(vvs_name) + self.stopRemoteCopy(rcg_name) + except exceptions.HTTPNotFound as ex: + # The remote-copy group does not exist or + # set does not exist. + raise ex + except Exception as ex: + logger.error("Error disabling replication on group %(group)s. " + "Exception received: %(e)s.", + {'group': group.id, 'e': ex}) + raise exceptions.ClientException(group_id=group.id) + + return model_update + + + + def enable_replication_client(self, group, volumes): + """Enable replication for a group. + + :param group: the group object + :param volumes: the list of volumes + :returns: model_update, None + """ + + model_update = {} + if not group.is_replicated: + raise NotImplementedError() + + if not volumes: + # Return if empty group + return model_update + + try: + vvs_name = self._get_3par_vvs_name(group.id) + rcg_name = self._get_3par_rcg_name_of_group(group.id) + + # Check VV and RCG exist on 3par, + # if RCG exist then start RCG + self.getVolumeSet(vvs_name) + self.startRemoteCopy(rcg_name) + except exceptions.HTTPNotFound as ex: + # The remote-copy group does not exist or + # set does not exist. + raise ex + except exceptions.HTTPForbidden as ex: + # The remote-copy group has already been started. + raise ex + except Exception as ex: + logger.error("Error enabling replication on group %(group)s. " + "Exception received: %(e)s.", + {'group': group.id, 'e': ex}) + raise exceptions.ClientException(group_id=group.id) + + return model_update + + + def _get_replication_sync_period_from_volume_type_client(self, volume_type): + # Default replication sync period is 900s + replication_sync_period = self.DEFAULT_SYNC_PERIOD + rep_mode = self.DEFAULT_REP_MODE + extra_specs = volume_type.get("extra_specs") + if extra_specs: + replication_sync_period = extra_specs.get( + self.EXTRA_SPEC_REP_SYNC_PERIOD, self.DEFAULT_SYNC_PERIOD) + + replication_sync_period = int(replication_sync_period) + if not self._is_replication_mode_correct(rep_mode, + replication_sync_period): + raise exceptions.ClientException() + + return replication_sync_period + + + + + From b8ed3f1bb8ea0d1ed8b7d6c23592655e88c62fc8 Mon Sep 17 00:00:00 2001 From: susmita-poddar Date: Wed, 20 Aug 2025 16:36:57 +0530 Subject: [PATCH 02/11] initialize_connection_iscsi - 1st cut --- hpe3parclient/client.py | 244 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 232 insertions(+), 12 deletions(-) diff --git a/hpe3parclient/client.py b/hpe3parclient/client.py index 7b22a46..1a94117 100644 --- a/hpe3parclient/client.py +++ b/hpe3parclient/client.py @@ -241,6 +241,8 @@ class HPE3ParClient(object): SYNC_STATUS_COMPLETED = 3 FAILBACK_VALUE = 'default' + DEFAULT_ISCSI_PORT = 3260 + def __init__(self, api_url, debug=False, secure=False, timeout=None, suppress_ssl_warnings=False): @@ -5540,18 +5542,6 @@ def _get_3par_remote_rcg_name_of_group(self, group_id, provider_location): str(provider_location)) - def _get_keys_by_volume_type(self, volume_type): - hpe3par_keys = {} - specs = volume_type.get('extra_specs') - for key, value in specs.items(): - if ':' in key: - fields = key.split(':') - key = fields[1] - if key in self.hpe3par_valid_keys: - hpe3par_keys[key] = value - return hpe3par_keys - - def _get_hpe3par_tiramisu_value(self, volume_type): hpe3par_tiramisu = False hpe3par_keys = self._get_keys_by_volume_type(volume_type) @@ -5774,8 +5764,238 @@ def _get_replication_sync_period_from_volume_type_client(self, volume_type): return replication_sync_period + + # v2 replication conversion + def _get_3par_rcg_name_client(self, volume, vol_name=None): + # if non-replicated volume is retyped or migrated to replicated vol, + # then rcg_name is different. Try to get that new rcg_name. + if volume['migration_status'] == 'success': + vol_details = self.getVolume(vol_name) + rcg_name = vol_details.get('rcopyGroup') + return rcg_name + else: + # by default, rcg_name is similar to volume name + rcg_name = self._encode_name(volume.get('_name_id') + or volume['id']) + rcg = "rcg-%s" % rcg_name + return rcg[:22] + + + def getStorageSystemIdName(self): + info = self.getStorageSystemInfo() + + return info['id'], info['name'] + + + def getWsApiVersionBuild(self): + info = self.getWsApiVersion() + + return info['build'] + + + def check_replication_flags_client(self, options): + required_flags = ['hpe3par_api_url', 'hpe3par_username', + 'hpe3par_password', 'san_ip', 'san_login', + 'san_password', 'backend_id', + 'replication_mode', 'cpg_map'] + + for flag in required_flags: + if not options.get(flag, None): + msg = (('%s is not set and is required for the replication ' + 'device to be valid.') % flag) + logger.error(msg) + raise exceptions.ClientException(desc=msg) + + + def initialize_iscsi_ports_client(self, ip_addr): + temp_iscsi_ip = {} + + if "." in ip_addr: + # v4 + ip = ip_addr.split(':') + if len(ip) == 1: + temp_iscsi_ip[ip_addr] = ( + {'ip_port': self.DEFAULT_ISCSI_PORT}) + elif len(ip) == 2: + temp_iscsi_ip[ip[0]] = {'ip_port': ip[1]} + elif ":" in ip_addr: + # v6 + if "]" in ip_addr: + ip = ip_addr.split(']:') + ip_addr_v6 = ip[0] + ip_addr_v6 = ip_addr_v6.strip('[') + port_v6 = ip[1] + temp_iscsi_ip[ip_addr_v6] = {'ip_port': port_v6} + else: + temp_iscsi_ip[ip_addr] = ( + {'ip_port': self.DEFAULT_ISCSI_PORT}) + else: + logger.warning("Invalid IP address format '%s'", ip_addr) + + return temp_iscsi_ip + + + def get_active_target_ports_client(self, remote_client=None): + if remote_client: + client_obj = remote_client + ports = client_obj.getPorts() + else: + client_obj = self + ports = client_obj.get_ports() + + target_ports = [] + for port in ports['members']: + if ( + port['mode'] == client_obj.PORT_MODE_TARGET and + port['linkState'] == client_obj.PORT_STATE_READY + ): + port['nsp'] = self.build_nsp(port['portPos']) + target_ports.append(port) + + return target_ports + def build_nsp(self, portPos): + return '%s:%s:%s' % (portPos['node'], + portPos['slot'], + portPos['cardPort']) + + + + def get_active_iscsi_target_ports_client(self, remote_client=None): + ports = self.get_active_target_ports_client(remote_client) + if remote_client: + client_obj = remote_client + else: + client_obj = self + + iscsi_ports = [] + for port in ports: + if port['protocol'] == client_obj.PORT_PROTO_ISCSI: + iscsi_ports.append(port) + + return iscsi_ports + def update_dicts_client(self, temp_iscsi_ip, iscsi_ip_list, iscsi_ports): + for port in iscsi_ports: + ip = port['IPAddr'] + if ip in temp_iscsi_ip: + self._update_dicts(temp_iscsi_ip, iscsi_ip_list, ip, port) + + if 'iSCSIVlans' in port: + for vip in port['iSCSIVlans']: + ip = vip['IPAddr'] + if ip in temp_iscsi_ip: + logger.debug("vlan ip: %(ip)s", {'ip': ip}) + self._update_dicts(temp_iscsi_ip, iscsi_ip_list, + ip, port) + + return iscsi_ip_list, temp_iscsi_ip + + + def _update_dicts(self, temp_iscsi_ip, iscsi_ip_list, ip, port): + ip_port = temp_iscsi_ip[ip]['ip_port'] + iscsi_ip_list[ip] = {'ip_port': ip_port, + 'nsp': port['nsp'], + 'iqn': port['iSCSIName']} + del temp_iscsi_ip[ip] + + + def getCPGDomain(self, cpg): + cpg_obj = self.getCPG(cpg) + if 'domain' in cpg_obj: + return cpg_obj['domain'] + + return None + + + def getVolumeWithCPG(self, volume_name, allowSnap=False): + vol = self.getVolume(volume_name) + # Search for 'userCPG' in the get volume REST API, + # if found return userCPG , else search for snapCPG attribute + # when allowSnap=True. For the cases where 3PAR REST call for + # get volume doesn't have either userCPG or snapCPG , + # take the default value of cpg from 'host' attribute from volume param + logger.debug("get volume response is: %s", vol) + if 'userCPG' in vol: + return vol['userCPG'] + elif allowSnap and 'snapCPG' in vol: + return vol['snapCPG'] + + + def _get_prioritized_host_on_3par_client(self, host, hosts, hostname): + # Check whether host with wwn/iqn of initiator present on 3par + if hosts and hosts['members'] and 'name' in hosts['members'][0]: + # Retrieving 'host' and 'hosts' from 3par using hostname + # and wwn/iqn respectively. Compare hostname of 'host' and 'hosts', + # if they do not match it means 3par has a pre-existing host + # with some other name. + if host['name'] != hosts['members'][0]['name']: + hostname = hosts['members'][0]['name'] + logger.info(("Prioritize the host retrieved from wwn/iqn " + "Hostname : %(hosts)s is used instead " + "of Hostname: %(host)s"), + {'hosts': hostname, + 'host': host['name']}) + host = self.getHost(hostname) + return host, hostname + + return host, hostname + + + def queryHostReturnHostname(self, iscsi_iqn): + hosts = self.queryHost(iqns=iscsi_iqn) + + if hosts and hosts['members'] and 'name' in hosts['members'][0]: + return hosts['members'][0]['name'] + else: + return None + + + def hostNameFromHost(self, host): + if host and 'name' in host: + return host['name'] + else: + return None + + def volumeNameForVLun(self, vlun): + return vlun['volumeName'] + + + def iscsi_ip_port(self, port): + if port and 'IPAddr' in port: + return port['IPAddr'] + else: + return None + + + def vlunPortPos(self, vlun): + return vlun['portPos'] + + + def create_vlun_info(self, location): + vlun_info = None + if location: + # The LUN id is returned as part of the location URI + vlun = location.split(',') + vlun_info = {'volume_name': vlun[0], + 'lun_id': int(vlun[1]), + 'host_name': vlun[2], + } + if len(vlun) > 3: + vlun_info['nsp'] = vlun[3] + + return vlun_info + + + def get_vlun_info(self, vlun): + return vlun['volumeName'], vlun['lun'], vlun['portPos'] + + + def get_iSCSIVlans_Port(self, port): + return 'iSCSIVlans' in port, port['iSCSIVlans'] + + def get_vlan_ip(self, vip): + return vip['IPAddr'] \ No newline at end of file From 5412070a3d7507bd7ade2cc5ed1757e4966bc0c1 Mon Sep 17 00:00:00 2001 From: susmita-poddar Date: Thu, 21 Aug 2025 15:58:52 +0530 Subject: [PATCH 03/11] initialize_connection_fc - 1st cut, initialize_connection_iscsi - 2nd cut --- hpe3parclient/client.py | 48 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/hpe3parclient/client.py b/hpe3parclient/client.py index 1a94117..9e891be 100644 --- a/hpe3parclient/client.py +++ b/hpe3parclient/client.py @@ -5945,8 +5945,11 @@ def _get_prioritized_host_on_3par_client(self, host, hosts, hostname): return host, hostname - def queryHostReturnHostname(self, iscsi_iqn): - hosts = self.queryHost(iqns=iscsi_iqn) + def queryHostReturnHostname(self, iscsi_iqn=None, wwns=None): + if iscsi_iqn is not None: + hosts = self.queryHost(iqns=iscsi_iqn) + elif wwns is not None: + hosts = self.queryHost(wwns=wwns) if hosts and hosts['members'] and 'name' in hosts['members'][0]: return hosts['members'][0]['name'] @@ -5998,4 +6001,43 @@ def get_iSCSIVlans_Port(self, port): return 'iSCSIVlans' in port, port['iSCSIVlans'] def get_vlan_ip(self, vip): - return vip['IPAddr'] \ No newline at end of file + return vip['IPAddr'] + + + def get_host_wwns(self, host): + host_wwns = [] + + if 'FCPaths' in host: + for path in host['FCPaths']: + wwn = path.get('wwn', None) + if wwn is not None: + host_wwns.append(wwn.lower()) + + return host_wwns + + + def create_mod_request(self, iscsi_iqn=None, wwn=None): + mod_request = {} + + if iscsi_iqn is not None: + mod_request = {'pathOperation': self.HOST_EDIT_ADD, + 'iSCSINames': [iscsi_iqn]} + elif wwn is not None: + mod_request = {'pathOperation': self.HOST_EDIT_ADD, + 'FCWWNs': wwn} + + return mod_request + + + def host_iscsi_info(self, host): + return 'iSCSIPaths' not in host, len(host['iSCSIPaths']), host['initiatorChapEnabled'] + + + def create_mod_host_chap_request(self, username, password): + mod_request = {'chapOperation': self.HOST_EDIT_ADD, + 'chapOperationMode': self.CHAP_INITIATOR, + 'chapName': username, + 'chapSecret': password} + + return mod_request + \ No newline at end of file From a7d9e8eb6359ce74b7d0a2f9c0f5996c06a51e86 Mon Sep 17 00:00:00 2001 From: susmita-poddar Date: Tue, 26 Aug 2025 13:01:39 +0530 Subject: [PATCH 04/11] create_volume - 1st cut --- hpe3parclient/client.py | 214 +++++++++++++++++++++++++++++++--------- 1 file changed, 170 insertions(+), 44 deletions(-) diff --git a/hpe3parclient/client.py b/hpe3parclient/client.py index 9e891be..b76d2a6 100644 --- a/hpe3parclient/client.py +++ b/hpe3parclient/client.py @@ -243,6 +243,22 @@ class HPE3ParClient(object): DEFAULT_ISCSI_PORT = 3260 + MIN_CLIENT_VERSION = '4.2.10' + DEDUP_API_VERSION = 30201120 + FLASH_CACHE_API_VERSION = 30201200 + COMPRESSION_API_VERSION = 30301215 + SRSTATLD_API_VERSION = 30201200 + REMOTE_COPY_API_VERSION = 30202290 + API_VERSION_2023 = 100000000 + + + # License values for reported capabilities + PRIORITY_OPT_LIC = "Priority Optimization" + THIN_PROV_LIC = "Thin Provisioning" + REMOTE_COPY_LIC = "Remote Copy" + SYSTEM_REPORTER_LIC = "System Reporter" + COMPRESSION_LIC = "Compression" + def __init__(self, api_url, debug=False, secure=False, timeout=None, suppress_ssl_warnings=False): @@ -5490,11 +5506,11 @@ def _get_3par_vol_comment_value(self, vol_comment, key): return None - @staticmethod + """ @staticmethod def _add_name_id_to_comment(comment, volume): name_id = volume.get('_name_id') if name_id: - comment['_name_id'] = name_id + comment['_name_id'] = name_id """ @@ -5510,14 +5526,14 @@ def _get_3par_snap_name(self, snapshot_id, temp_snap=False): - def is_volume_group_snap_type(self, volume_type): + """ def is_volume_group_snap_type(self, volume_type): consis_group_snap_type = False if volume_type: extra_specs = volume_type.get('extra_specs') if 'consistent_group_snapshot_enabled' in extra_specs: gsnap_val = extra_specs['consistent_group_snapshot_enabled'] consis_group_snap_type = (gsnap_val == " True") - return consis_group_snap_type + return consis_group_snap_type """ def _is_volume_type_replicated(self, volume_type): @@ -5542,16 +5558,6 @@ def _get_3par_remote_rcg_name_of_group(self, group_id, provider_location): str(provider_location)) - def _get_hpe3par_tiramisu_value(self, volume_type): - hpe3par_tiramisu = False - hpe3par_keys = self._get_keys_by_volume_type(volume_type) - if hpe3par_keys.get('group_replication'): - hpe3par_tiramisu = ( - hpe3par_keys['group_replication'] == " True") - - return hpe3par_tiramisu - - def _get_key_value(self, hpe3par_keys, key, default=None): if hpe3par_keys is not None and key in hpe3par_keys: return hpe3par_keys[key] @@ -5585,19 +5591,6 @@ def _get_remote_copy_mode_num(self, mode): ret_mode = self.PERIODIC return ret_mode - - def _get_replication_mode_from_volume_type(self, volume_type): - # Default replication mode is PERIODIC - replication_mode_num = self.PERIODIC - extra_specs = volume_type.get("extra_specs") - if extra_specs: - replication_mode = extra_specs.get( - self.EXTRA_SPEC_REP_MODE, self.DEFAULT_REP_MODE) - - replication_mode_num = self._get_remote_copy_mode_num( - replication_mode) - - return replication_mode_num def _is_replication_mode_correct(self, mode, sync_num): rep_flag = True @@ -5748,7 +5741,7 @@ def enable_replication_client(self, group, volumes): return model_update - def _get_replication_sync_period_from_volume_type_client(self, volume_type): + """ def _get_replication_sync_period_from_volume_type_client(self, volume_type): # Default replication sync period is 900s replication_sync_period = self.DEFAULT_SYNC_PERIOD rep_mode = self.DEFAULT_REP_MODE @@ -5762,24 +5755,13 @@ def _get_replication_sync_period_from_volume_type_client(self, volume_type): replication_sync_period): raise exceptions.ClientException() - return replication_sync_period + return replication_sync_period """ # v2 replication conversion - def _get_3par_rcg_name_client(self, volume, vol_name=None): - # if non-replicated volume is retyped or migrated to replicated vol, - # then rcg_name is different. Try to get that new rcg_name. - if volume['migration_status'] == 'success': - vol_details = self.getVolume(vol_name) - rcg_name = vol_details.get('rcopyGroup') - return rcg_name - else: - # by default, rcg_name is similar to volume name - rcg_name = self._encode_name(volume.get('_name_id') - or volume['id']) - rcg = "rcg-%s" % rcg_name - return rcg[:22] - + def _get_3par_rcg_name_client(self, vol_details): + rcg_name = vol_details.get('rcopyGroup') + return rcg_name def getStorageSystemIdName(self): info = self.getStorageSystemInfo() @@ -6040,4 +6022,148 @@ def create_mod_host_chap_request(self, username, password): 'chapSecret': password} return mod_request - \ No newline at end of file + + + def getStorageSystemVersionAndLicense(self, info): + systemVersion = info['systemVersion'] + + if 'licenseInfo' in info: + if 'licenses' in info['licenseInfo']: + valid_licenses = info['licenseInfo']['licenses'] + + return systemVersion, valid_licenses + + + def getRemoteCopyGroupVolumes(self, rcg_name): + rcg = self.getRemoteCopyGroup(rcg_name) + + return rcg['volumes'] + + + def modifyRemoteCopyGroupPayload(self, targets, replication_mode_num, snap_cpg, local_cpg): + rcg_targets = [] + + for target in targets: + if target['replication_mode'] == replication_mode_num: + cpg = self._get_cpg_from_cpg_map(target['cpg_map'], + local_cpg) + + + rcg_target = {'targetName': target['backend_id'], + 'remoteUserCPG': cpg, + 'remoteSnapCPG': cpg} + rcg_targets.append(rcg_target) + + optional = {'localSnapCPG': snap_cpg, + 'localUserCPG': local_cpg, + 'targets': rcg_targets} + + return optional + + + def modifyRemoteCopyGroupPayloadSyncTargets(self, targets, replication_mode_num, replication_sync_period): + sync_targets = [] + + for target in targets: + if target['replication_mode'] == replication_mode_num: + sync_target = {'targetName': target['backend_id'], + 'syncPeriod': replication_sync_period} + sync_targets.append(sync_target) + + opt = {'targets': sync_targets} + return opt + + + def add_vol_to_remote_copy_group_params(self, targets, replication_mode_num, vol_name): + rcg_targets = [] + + for target in targets: + if target['replication_mode'] == replication_mode_num: + rcg_target = {'targetName': target['backend_id'], + 'secVolumeName': vol_name} + rcg_targets.append(rcg_target) + + optional = {'volumeAutoCreation': True} + + return rcg_targets, optional + + + def getRemoteCopyVolumesAndSyncPeriod(self, rcg, replication_sync_period): + if len(rcg['volumes']) and 'syncPeriod' in rcg['targets'][0]: + if replication_sync_period != int(rcg['targets'][0]['syncPeriod']): + return True + else: + return False + else: + return False + + + def create_qos_rule(self, min_io, max_io, min_bw, max_bw, latency, priority): + qosRule = {} + + if min_io: + qosRule['ioMinGoal'] = int(min_io) + if max_io is None: + qosRule['ioMaxLimit'] = int(min_io) + if max_io: + qosRule['ioMaxLimit'] = int(max_io) + if min_io is None: + qosRule['ioMinGoal'] = int(max_io) + if min_bw: + qosRule['bwMinGoalKB'] = min_bw + if max_bw is None: + qosRule['bwMaxLimitKB'] = min_bw + if max_bw: + qosRule['bwMaxLimitKB'] = max_bw + if min_bw is None: + qosRule['bwMinGoalKB'] = max_bw + if latency: + # latency could be values like 2, 5, etc or + # small values like 0.1, 0.02, etc. + # we are converting to float so that 0.1 doesn't become 0 + latency = float(latency) + if latency >= 1: + # by default, latency in millisecs + qosRule['latencyGoal'] = int(latency) + else: + # latency < 1 Eg. 0.1, 0.02, etc + # convert latency to microsecs + qosRule['latencyGoaluSecs'] = int(latency * 1000) + if priority: + qosRule['priority'] = priority + + return qosRule + + + def createRemoteCopyGroupPayload(self, targets, replication_mode_num, local_cpg, snap_cpg, domain, version): + rcg_targets = [] + + for target in targets: + # Only add targets that match the volumes replication mode. + if target['replication_mode'] == replication_mode_num: + cpg = self._get_cpg_from_cpg_map(target['cpg_map'], + local_cpg) + rcg_target = {'targetName': target['backend_id'], + 'mode': replication_mode_num, + 'userCPG': cpg} + if version < self.API_VERSION_2023: + rcg_target['snapCPG'] = cpg + rcg_targets.append(rcg_target) + + optional = {'localUserCPG': local_cpg} + + if version < self.API_VERSION_2023: + optional['localSnapCPG'] = snap_cpg + + if domain: + optional["domain"] = domain + + return rcg_targets, optional + + + def modifyRemoteCopyGroupPayloadPpParams(self): + pp_params = {'targets': [ + {'policies': {'autoFailover': True, + 'pathManagement': True, + 'autoRecover': True}}]} + return pp_params From 15fda7fdc7c6c4a1c15984dd393a145ff375bbdd Mon Sep 17 00:00:00 2001 From: susmita-poddar Date: Thu, 28 Aug 2025 15:07:31 +0530 Subject: [PATCH 05/11] recheck whatever done till now --- hpe3parclient/client.py | 97 ++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 54 deletions(-) diff --git a/hpe3parclient/client.py b/hpe3parclient/client.py index b76d2a6..39ee91a 100644 --- a/hpe3parclient/client.py +++ b/hpe3parclient/client.py @@ -5639,20 +5639,7 @@ def _is_group_in_remote_copy_group(self, group): except exceptions.HTTPNotFound: return False - - def _get_cpg_from_cpg_map(self, cpg_map, target_cpg): - ret_target_cpg = None - cpg_pairs = cpg_map.split(' ') - for cpg_pair in cpg_pairs: - cpgs = cpg_pair.split(':') - cpg = cpgs[0] - dest_cpg = cpgs[1] - if cpg == target_cpg: - ret_target_cpg = dest_cpg - - return ret_target_cpg - def _generate_hpe3par_cpgs(self, cpg_map): hpe3par_cpgs = [] cpg_pairs = cpg_map.split(' ') @@ -5775,20 +5762,6 @@ def getWsApiVersionBuild(self): return info['build'] - def check_replication_flags_client(self, options): - required_flags = ['hpe3par_api_url', 'hpe3par_username', - 'hpe3par_password', 'san_ip', 'san_login', - 'san_password', 'backend_id', - 'replication_mode', 'cpg_map'] - - for flag in required_flags: - if not options.get(flag, None): - msg = (('%s is not set and is required for the replication ' - 'device to be valid.') % flag) - logger.error(msg) - raise exceptions.ClientException(desc=msg) - - def initialize_iscsi_ports_client(self, ip_addr): temp_iscsi_ip = {} @@ -5818,25 +5791,31 @@ def initialize_iscsi_ports_client(self, ip_addr): - def get_active_target_ports_client(self, remote_client=None): - if remote_client: - client_obj = remote_client - ports = client_obj.getPorts() - else: - client_obj = self - ports = client_obj.get_ports() - + def get_active_target_ports_client(self, ports): target_ports = [] + for port in ports['members']: if ( - port['mode'] == client_obj.PORT_MODE_TARGET and - port['linkState'] == client_obj.PORT_STATE_READY + port['mode'] == self.PORT_MODE_TARGET and + port['linkState'] == self.PORT_STATE_READY ): port['nsp'] = self.build_nsp(port['portPos']) target_ports.append(port) return target_ports + def get_active_protocol_ports(self, ports, iscsi_proto=False, fc_proto=False): + proto_ports = [] + if fc_proto: + for port in ports: + if port['protocol'] == self.PORT_PROTO_FC: + proto_ports.append(port) + elif iscsi_proto: + for port in ports: + if port['protocol'] == self.PORT_PROTO_ISCSI: + proto_ports.append(port) + + return proto_ports def build_nsp(self, portPos): return '%s:%s:%s' % (portPos['node'], @@ -5844,22 +5823,6 @@ def build_nsp(self, portPos): portPos['cardPort']) - - def get_active_iscsi_target_ports_client(self, remote_client=None): - ports = self.get_active_target_ports_client(remote_client) - if remote_client: - client_obj = remote_client - else: - client_obj = self - - iscsi_ports = [] - for port in ports: - if port['protocol'] == client_obj.PORT_PROTO_ISCSI: - iscsi_ports.append(port) - - return iscsi_ports - - def update_dicts_client(self, temp_iscsi_ip, iscsi_ip_list, iscsi_ports): for port in iscsi_ports: ip = port['IPAddr'] @@ -5998,7 +5961,7 @@ def get_host_wwns(self, host): return host_wwns - def create_mod_request(self, iscsi_iqn=None, wwn=None): + def create_modifyhost_request(self, iscsi_iqn=None, wwn=None): mod_request = {} if iscsi_iqn is not None: @@ -6167,3 +6130,29 @@ def modifyRemoteCopyGroupPayloadPpParams(self): 'pathManagement': True, 'autoRecover': True}}]} return pp_params + + + + def createHostOptional(self, domain, persona_id): + optional = { + 'domain': domain, + 'persona': persona_id + } + + return optional + + + def getPortsIqnOrWwnOrNqn(self, ports, iscsi_port=False, fc_port=False): + all_target_wwns = [] + all_target_iqns = [] + + if fc_port: + for port in ports: + all_target_wwns.append(port['portWWN']) + return all_target_wwns + + if iscsi_port: + for port in ports: + all_target_iqns.append(port['portIQN']) + return all_target_iqns + From 5a8559ceff946ddd3f313ab408b6666ba6ab9d3d Mon Sep 17 00:00:00 2001 From: susmita-poddar Date: Tue, 2 Sep 2025 18:12:02 +0530 Subject: [PATCH 06/11] other flows in order of priority - 1st cut --- hpe3parclient/client.py | 114 ++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 68 deletions(-) diff --git a/hpe3parclient/client.py b/hpe3parclient/client.py index 39ee91a..ae2c554 100644 --- a/hpe3parclient/client.py +++ b/hpe3parclient/client.py @@ -5524,28 +5524,7 @@ def _get_3par_snap_name(self, snapshot_id, temp_snap=False): prefix = "oss-%s" return prefix % snapshot_name - - - """ def is_volume_group_snap_type(self, volume_type): - consis_group_snap_type = False - if volume_type: - extra_specs = volume_type.get('extra_specs') - if 'consistent_group_snapshot_enabled' in extra_specs: - gsnap_val = extra_specs['consistent_group_snapshot_enabled'] - consis_group_snap_type = (gsnap_val == " True") - return consis_group_snap_type """ - - - def _is_volume_type_replicated(self, volume_type): - replicated_type = False - extra_specs = volume_type.get('extra_specs') - if extra_specs and 'replication_enabled' in extra_specs: - rep_val = extra_specs['replication_enabled'] - replicated_type = (rep_val == " True") - - return replicated_type - - + def _get_3par_rcg_name_of_group(self, group_id): rcg_name = self._encode_name(group_id) rcg = "rcg-%s" % rcg_name @@ -5565,17 +5544,6 @@ def _get_key_value(self, hpe3par_keys, key, default=None): return default - def _get_boolean_key_value(self, hpe3par_keys, key, default=False): - value = self._get_key_value( - hpe3par_keys, key, default) - if isinstance(value, str): - if value.lower() == 'true': - value = True - else: - value = False - return value - - def _get_3par_vol_comment(self, volume_name): vol = self.getVolume(volume_name) if 'comment' in vol: @@ -6003,20 +5971,7 @@ def getRemoteCopyGroupVolumes(self, rcg_name): return rcg['volumes'] - def modifyRemoteCopyGroupPayload(self, targets, replication_mode_num, snap_cpg, local_cpg): - rcg_targets = [] - - for target in targets: - if target['replication_mode'] == replication_mode_num: - cpg = self._get_cpg_from_cpg_map(target['cpg_map'], - local_cpg) - - - rcg_target = {'targetName': target['backend_id'], - 'remoteUserCPG': cpg, - 'remoteSnapCPG': cpg} - rcg_targets.append(rcg_target) - + def modifyRemoteCopyGroupOptional(self, rcg_targets, snap_cpg, local_cpg): optional = {'localSnapCPG': snap_cpg, 'localUserCPG': local_cpg, 'targets': rcg_targets} @@ -6024,6 +5979,14 @@ def modifyRemoteCopyGroupPayload(self, targets, replication_mode_num, snap_cpg, return optional + def modifyRemoteCopyGroupTarget(self, targetName, cpg): + rcg_target = {'targetName': targetName, + 'remoteUserCPG': cpg, + 'remoteSnapCPG': cpg} + + return rcg_target + + def modifyRemoteCopyGroupPayloadSyncTargets(self, targets, replication_mode_num, replication_sync_period): sync_targets = [] @@ -6098,41 +6061,35 @@ def create_qos_rule(self, min_io, max_io, min_bw, max_bw, latency, priority): return qosRule - def createRemoteCopyGroupPayload(self, targets, replication_mode_num, local_cpg, snap_cpg, domain, version): - rcg_targets = [] + def createRemoteCopyGroupTarget(self, apiVersion, targetName, replication_mode_num, replication_sync_period, cpg): + rcg_target = {'targetName': targetName, + 'mode': replication_mode_num, + 'userCPG': cpg} + if apiVersion < self.API_VERSION_2023: + rcg_target['snapCPG'] = cpg - for target in targets: - # Only add targets that match the volumes replication mode. - if target['replication_mode'] == replication_mode_num: - cpg = self._get_cpg_from_cpg_map(target['cpg_map'], - local_cpg) - rcg_target = {'targetName': target['backend_id'], - 'mode': replication_mode_num, - 'userCPG': cpg} - if version < self.API_VERSION_2023: - rcg_target['snapCPG'] = cpg - rcg_targets.append(rcg_target) + sync_target = {'targetName': targetName, + 'syncPeriod': replication_sync_period} + return rcg_target, sync_target + + def createRemoteCopyGroupOptional(self, apiVersion, snap_cpg, local_cpg, domain): optional = {'localUserCPG': local_cpg} - - if version < self.API_VERSION_2023: + if apiVersion < self.API_VERSION_2023: optional['localSnapCPG'] = snap_cpg - if domain: optional["domain"] = domain - return rcg_targets, optional - + return optional - def modifyRemoteCopyGroupPayloadPpParams(self): + def modifyRemoteCopyGroupPpParams(self): pp_params = {'targets': [ {'policies': {'autoFailover': True, 'pathManagement': True, 'autoRecover': True}}]} return pp_params - - + def createHostOptional(self, domain, persona_id): optional = { 'domain': domain, @@ -6155,4 +6112,25 @@ def getPortsIqnOrWwnOrNqn(self, ports, iscsi_port=False, fc_port=False): for port in ports: all_target_iqns.append(port['portIQN']) return all_target_iqns + + + + def copyVolumeOptional(self, wsapiVersion, snap_cpg=None, + tpvv=True, tdvv=False, compression=None, comment=None): + + optional = {'tpvv': tpvv, 'online': True} + + if snap_cpg is not None and wsapiVersion < self.API_VERSION_2023: + optional['snapCPG'] = snap_cpg + if wsapiVersion >= self.DEDUP_API_VERSION: + optional['tdvv'] = tdvv + + if (compression is not None and + wsapiVersion >= self.COMPRESSION_API_VERSION): + optional['compression'] = compression + + if comment: + optional['comment'] = comment + + return optional From 56479e540063302544beae0bf49955306836f5f3 Mon Sep 17 00:00:00 2001 From: susmita-poddar Date: Wed, 3 Sep 2025 10:17:25 +0530 Subject: [PATCH 07/11] other flows in order of priority - 2nd cut --- hpe3parclient/client.py | 133 ---------------------------------------- 1 file changed, 133 deletions(-) diff --git a/hpe3parclient/client.py b/hpe3parclient/client.py index ae2c554..4cd56ec 100644 --- a/hpe3parclient/client.py +++ b/hpe3parclient/client.py @@ -5478,26 +5478,6 @@ def _get_3par_unm_name(self, volume_id): return "unm-%s" % unm_name - def _get_existing_volume_ref_name_client(self, existing_ref, is_snapshot): - """Returns the volume name of an existing reference. - - Checks if an existing volume reference has a source-name or - source-id element. If source-name or source-id is not present an - error will be thrown. - """ - vol_name = None - if 'source-name' in existing_ref: - vol_name = existing_ref['source-name'] - elif 'source-id' in existing_ref: - if is_snapshot: - vol_name = self._get_3par_ums_name(existing_ref['source-id']) - else: - vol_name = self._get_3par_unm_name(existing_ref['source-id']) - else: - reason = "Reference must contain source-name or source-id." - raise exceptions.ClientException(reason) - return vol_name - def _get_3par_vol_comment_value(self, vol_comment, key): comment_dict = dict(ast.literal_eval(vol_comment)) @@ -5506,14 +5486,6 @@ def _get_3par_vol_comment_value(self, vol_comment, key): return None - """ @staticmethod - def _add_name_id_to_comment(comment, volume): - name_id = volume.get('_name_id') - if name_id: - comment['_name_id'] = name_id """ - - - def _get_3par_snap_name(self, snapshot_id, temp_snap=False): snapshot_name = self._encode_name(snapshot_id) if temp_snap: @@ -5607,111 +5579,6 @@ def _is_group_in_remote_copy_group(self, group): except exceptions.HTTPNotFound: return False - - def _generate_hpe3par_cpgs(self, cpg_map): - hpe3par_cpgs = [] - cpg_pairs = cpg_map.split(' ') - for cpg_pair in cpg_pairs: - cpgs = cpg_pair.split(':') - hpe3par_cpgs.append(cpgs[1]) - - return hpe3par_cpgs - - - def disable_replication_client(self, group, volumes): - """Disable replication for a group. - - :param group: the group object - :param volumes: the list of volumes - :returns: model_update, None - """ - - model_update = {} - if not group.is_replicated: - raise NotImplementedError() - - if not volumes: - # Return if empty group - return model_update - - try: - vvs_name = self._get_3par_vvs_name(group.id) - rcg_name = self._get_3par_rcg_name_of_group(group.id) - - # Check VV and RCG exist on 3par, - # if RCG exist then stop RCG - self.getVolumeSet(vvs_name) - self.stopRemoteCopy(rcg_name) - except exceptions.HTTPNotFound as ex: - # The remote-copy group does not exist or - # set does not exist. - raise ex - except Exception as ex: - logger.error("Error disabling replication on group %(group)s. " - "Exception received: %(e)s.", - {'group': group.id, 'e': ex}) - raise exceptions.ClientException(group_id=group.id) - - return model_update - - - - def enable_replication_client(self, group, volumes): - """Enable replication for a group. - - :param group: the group object - :param volumes: the list of volumes - :returns: model_update, None - """ - - model_update = {} - if not group.is_replicated: - raise NotImplementedError() - - if not volumes: - # Return if empty group - return model_update - - try: - vvs_name = self._get_3par_vvs_name(group.id) - rcg_name = self._get_3par_rcg_name_of_group(group.id) - - # Check VV and RCG exist on 3par, - # if RCG exist then start RCG - self.getVolumeSet(vvs_name) - self.startRemoteCopy(rcg_name) - except exceptions.HTTPNotFound as ex: - # The remote-copy group does not exist or - # set does not exist. - raise ex - except exceptions.HTTPForbidden as ex: - # The remote-copy group has already been started. - raise ex - except Exception as ex: - logger.error("Error enabling replication on group %(group)s. " - "Exception received: %(e)s.", - {'group': group.id, 'e': ex}) - raise exceptions.ClientException(group_id=group.id) - - return model_update - - - """ def _get_replication_sync_period_from_volume_type_client(self, volume_type): - # Default replication sync period is 900s - replication_sync_period = self.DEFAULT_SYNC_PERIOD - rep_mode = self.DEFAULT_REP_MODE - extra_specs = volume_type.get("extra_specs") - if extra_specs: - replication_sync_period = extra_specs.get( - self.EXTRA_SPEC_REP_SYNC_PERIOD, self.DEFAULT_SYNC_PERIOD) - - replication_sync_period = int(replication_sync_period) - if not self._is_replication_mode_correct(rep_mode, - replication_sync_period): - raise exceptions.ClientException() - - return replication_sync_period """ - # v2 replication conversion def _get_3par_rcg_name_client(self, vol_details): From 68024f212c7ce1bebe659f03b62e013c5fc25c43 Mon Sep 17 00:00:00 2001 From: susmita-poddar Date: Thu, 4 Sep 2025 15:40:04 +0530 Subject: [PATCH 08/11] Addressing RC1 --- hpe3parclient/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hpe3parclient/client.py b/hpe3parclient/client.py index 4cd56ec..1afad9d 100644 --- a/hpe3parclient/client.py +++ b/hpe3parclient/client.py @@ -5725,9 +5725,9 @@ def _get_prioritized_host_on_3par_client(self, host, hosts, hostname): return host, hostname - def queryHostReturnHostname(self, iscsi_iqn=None, wwns=None): - if iscsi_iqn is not None: - hosts = self.queryHost(iqns=iscsi_iqn) + def queryHostReturnHostname(self, iscsi_iqns=None, wwns=None): + if iscsi_iqns is not None: + hosts = self.queryHost(iqns=iscsi_iqns) elif wwns is not None: hosts = self.queryHost(wwns=wwns) From 1a5b012fd8c9508ac038d6289e21e153d1edecc6 Mon Sep 17 00:00:00 2001 From: susmita-poddar Date: Mon, 8 Sep 2025 10:18:37 +0530 Subject: [PATCH 09/11] Removing oslo import and related dependencies from client code --- hpe3parclient/client.py | 64 ----------------------------------------- 1 file changed, 64 deletions(-) diff --git a/hpe3parclient/client.py b/hpe3parclient/client.py index 1afad9d..a00a495 100644 --- a/hpe3parclient/client.py +++ b/hpe3parclient/client.py @@ -33,8 +33,6 @@ import logging import ast -from oslo_serialization import base64 - try: # For Python 3.0 and later from urllib.parse import quote @@ -5448,36 +5446,6 @@ def _check_license_enabled(self, systemVersion, valid_licenses, 'license': license_to_check}) return False - - @staticmethod - def _encode_name(name): - uuid_str = name.replace("-", "") - vol_uuid = uuid.UUID('urn:uuid:%s' % uuid_str) - vol_encoded = base64.encode_as_text(vol_uuid.bytes) - - # 3par doesn't allow +, nor / - vol_encoded = vol_encoded.replace('+', '.') - vol_encoded = vol_encoded.replace('/', '-') - # strip off the == as 3par doesn't like those. - vol_encoded = vol_encoded.replace('=', '') - return vol_encoded - - - def _get_3par_ums_name(self, snapshot_id): - ums_name = self._encode_name(snapshot_id) - return "ums-%s" % ums_name - - - def _get_3par_vvs_name(self, volume_id): - vvs_name = self._encode_name(volume_id) - return "vvs-%s" % vvs_name - - - def _get_3par_unm_name(self, volume_id): - unm_name = self._encode_name(volume_id) - return "unm-%s" % unm_name - - def _get_3par_vol_comment_value(self, vol_comment, key): comment_dict = dict(ast.literal_eval(vol_comment)) @@ -5486,29 +5454,6 @@ def _get_3par_vol_comment_value(self, vol_comment, key): return None - def _get_3par_snap_name(self, snapshot_id, temp_snap=False): - snapshot_name = self._encode_name(snapshot_id) - if temp_snap: - # is this a temporary snapshot - # this is done during cloning - prefix = "tss-%s" - else: - prefix = "oss-%s" - return prefix % snapshot_name - - - def _get_3par_rcg_name_of_group(self, group_id): - rcg_name = self._encode_name(group_id) - rcg = "rcg-%s" % rcg_name - return rcg[:22] - - - - def _get_3par_remote_rcg_name_of_group(self, group_id, provider_location): - return self._get_3par_rcg_name_of_group(group_id) + ".r" + ( - str(provider_location)) - - def _get_key_value(self, hpe3par_keys, key, default=None): if hpe3par_keys is not None and key in hpe3par_keys: return hpe3par_keys[key] @@ -5571,15 +5516,6 @@ def _are_targets_in_their_natural_direction(self, rcg): return True - def _is_group_in_remote_copy_group(self, group): - rcg_name = self._get_3par_rcg_name_of_group(group.id) - try: - self.getRemoteCopyGroup(rcg_name) - return True - except exceptions.HTTPNotFound: - return False - - # v2 replication conversion def _get_3par_rcg_name_client(self, vol_details): rcg_name = vol_details.get('rcopyGroup') From ca9aab164c69e67a477947702ca344b50d5c637b Mon Sep 17 00:00:00 2001 From: susmita-poddar Date: Mon, 8 Sep 2025 14:11:59 +0530 Subject: [PATCH 10/11] Addressing RC2 --- hpe3parclient/client.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/hpe3parclient/client.py b/hpe3parclient/client.py index a00a495..14d358d 100644 --- a/hpe3parclient/client.py +++ b/hpe3parclient/client.py @@ -5454,13 +5454,6 @@ def _get_3par_vol_comment_value(self, vol_comment, key): return None - def _get_key_value(self, hpe3par_keys, key, default=None): - if hpe3par_keys is not None and key in hpe3par_keys: - return hpe3par_keys[key] - else: - return default - - def _get_3par_vol_comment(self, volume_name): vol = self.getVolume(volume_name) if 'comment' in vol: @@ -5521,17 +5514,6 @@ def _get_3par_rcg_name_client(self, vol_details): rcg_name = vol_details.get('rcopyGroup') return rcg_name - def getStorageSystemIdName(self): - info = self.getStorageSystemInfo() - - return info['id'], info['name'] - - - def getWsApiVersionBuild(self): - info = self.getWsApiVersion() - - return info['build'] - def initialize_iscsi_ports_client(self, ip_addr): temp_iscsi_ip = {} From 3c4888d8f4f8c2b4d284e7bde708fd1cb7057843 Mon Sep 17 00:00:00 2001 From: susmita-poddar Date: Tue, 9 Sep 2025 14:39:13 +0530 Subject: [PATCH 11/11] Addressing RC3 --- hpe3parclient/client.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/hpe3parclient/client.py b/hpe3parclient/client.py index 14d358d..3fed5c4 100644 --- a/hpe3parclient/client.py +++ b/hpe3parclient/client.py @@ -5491,7 +5491,6 @@ def _is_replication_mode_correct(self, mode, sync_num): def _are_targets_in_their_natural_direction(self, rcg): - targets = rcg['targets'] for target in targets: if target['roleReversed'] or ( @@ -5601,14 +5600,6 @@ def _update_dicts(self, temp_iscsi_ip, iscsi_ip_list, ip, port): del temp_iscsi_ip[ip] - def getCPGDomain(self, cpg): - cpg_obj = self.getCPG(cpg) - if 'domain' in cpg_obj: - return cpg_obj['domain'] - - return None - - def getVolumeWithCPG(self, volume_name, allowSnap=False): vol = self.getVolume(volume_name) # Search for 'userCPG' in the get volume REST API, @@ -5655,16 +5646,6 @@ def queryHostReturnHostname(self, iscsi_iqns=None, wwns=None): return None - def hostNameFromHost(self, host): - if host and 'name' in host: - return host['name'] - else: - return None - - def volumeNameForVLun(self, vlun): - return vlun['volumeName'] - - def iscsi_ip_port(self, port): if port and 'IPAddr' in port: return port['IPAddr']