python-3parclient 4.2.14__tar.gz → 4.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: python-3parclient
3
- Version: 4.2.14
4
- Summary: HPE Alletra 9000 and HPE Primera and HPE 3PAR REST Client
3
+ Version: 4.3
4
+ Summary: HPE Alletra MP, 9000, Primera and 3PAR REST Client
5
5
  Author-email: Raghavendra Tilay <raghavendra-uddhav.tilay@hpe.com>
6
6
  License-File: LICENSE
7
7
  Classifier: Development Status :: 5 - Production/Stable
@@ -0,0 +1,7 @@
1
+ 2026-02-20: 4.3
2
+ NVMe TCP changes in client.py
3
+ Airtel fixes in client.py & http.py
4
+
5
+ 2024-05-22: 4.2.14
6
+ Changes for Arcus R4 and NVMe host
7
+
@@ -8,11 +8,11 @@ packages = ["src/hpe3parclient"]
8
8
 
9
9
  [project]
10
10
  name = "python-3parclient"
11
- version = "4.2.14"
11
+ version = "4.3"
12
12
  authors = [
13
13
  { name="Raghavendra Tilay", email="raghavendra-uddhav.tilay@hpe.com" },
14
14
  ]
15
- description = "HPE Alletra 9000 and HPE Primera and HPE 3PAR REST Client"
15
+ description = "HPE Alletra MP, 9000, Primera and 3PAR REST Client"
16
16
  readme = "README.md"
17
17
  classifiers = [
18
18
  "Development Status :: 5 - Production/Stable",
@@ -1,4 +1,4 @@
1
- # (c) Copyright 2012-2016 Hewlett Packard Enterprise Development LP
1
+ # (c) Copyright 2012-2025, 2026 Hewlett Packard Enterprise Development LP
2
2
  # All Rights Reserved.
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -202,6 +202,9 @@ class HPE3ParClient(object):
202
202
  RC_ACTION_CHANGE_TO_NATURUAL_DIRECTION = 10
203
203
  RC_ACTION_OVERRIDE_FAIL_SAFE = 11
204
204
 
205
+ DEFAULT_NVME_PORT = 4420
206
+ DEFAULT_PORT_NQN = 'nqn.2014-08.org.nvmexpress.discovery'
207
+
205
208
  def __init__(self, api_url, debug=False, secure=False, timeout=None,
206
209
  suppress_ssl_warnings=False):
207
210
  self.api_url = api_url
@@ -218,6 +221,9 @@ class HPE3ParClient(object):
218
221
 
219
222
  def is_primera_array(self):
220
223
  return self.primera_supported
224
+
225
+ def get_session_key(self):
226
+ return self.http.get_session_key()
221
227
 
222
228
  def setSSHOptions(self, ip, login, password, port=22,
223
229
  conn_timeout=None, privatekey=None,
@@ -250,22 +256,17 @@ class HPE3ParClient(object):
250
256
  :returns: Version dict
251
257
 
252
258
  """
253
- try:
254
- # remove everything down to host:port
255
- host_url = self.api_url.split('/api')
256
- self.http.set_url(host_url[0])
257
- # get the api version
258
- response, body = self.http.get('/api')
259
+ # remove everything down to host:port
260
+ host_url = self.api_url.split('/api')
261
+ # Build absolute URL and call without mutating http client's base
262
+ response, body = self.http.get(host_url[0] + '/api')
259
263
 
260
- api_version = body
261
- if (api_version['build'] >=
262
- self.HPE3PAR_WS_PRIMERA_MIN_BUILD_VERSION):
263
- self.primera_supported = True
264
+ api_version = body
265
+ if (api_version['build'] >=
266
+ self.HPE3PAR_WS_PRIMERA_MIN_BUILD_VERSION):
267
+ self.primera_supported = True
264
268
 
265
- return body
266
- finally:
267
- # reset the url
268
- self.http.set_url(self.api_url)
269
+ return body
269
270
 
270
271
  def debug_rest(self, flag):
271
272
  """This is useful for debugging requests to 3PAR.
@@ -1610,6 +1611,35 @@ class HPE3ParClient(object):
1610
1611
  response, body = self.http.get('/hosts/%s' % name)
1611
1612
  return body
1612
1613
 
1614
+ def getHostByNqn(self, nqn):
1615
+ """Get information about a Host by its NVMe Qualified Name (NQN).
1616
+
1617
+ :param nqn: The NQN of the Host to find
1618
+ :type nqn: str
1619
+
1620
+ :returns: host dict
1621
+ :raises: :class:`~hpe3parclient.exceptions.HTTPNotFound`
1622
+ - NON_EXISTENT_HOST - HOST doesn't exist
1623
+
1624
+ """
1625
+ body = self.getHosts()
1626
+ if 'members' not in body:
1627
+ return None
1628
+
1629
+ for host in body['members']:
1630
+ if 'NVMETCPPaths' not in host or not host['NVMETCPPaths']:
1631
+ continue
1632
+ nvme_paths = host['NVMETCPPaths']
1633
+ for path in nvme_paths:
1634
+ path_nqn = path.get('NQN')
1635
+ if path_nqn == nqn:
1636
+ return host
1637
+
1638
+ # If we reach here, no host with the given NQN was found
1639
+ logger.error("Host with NQN '%s' not found.", nqn)
1640
+ msg = "Host with NQN '%s' not found." % nqn
1641
+ raise exceptions.HTTPNotFound(error={'desc': msg})
1642
+
1613
1643
  def createHost(self, name, iscsiNames=None, FCWwns=None,
1614
1644
  nqn=None, optional=None):
1615
1645
  """Create a new Host entry.
@@ -1694,8 +1724,19 @@ class HPE3ParClient(object):
1694
1724
  if optional:
1695
1725
  info = self._mergeDict(info, optional)
1696
1726
 
1697
- response, body = self.http.post('/hosts', body=info)
1698
- return body
1727
+ try:
1728
+ response, body = self.http.post('/hosts', body=info)
1729
+ except Exception as ex:
1730
+ logger.error("Failed to create host: %s", str(ex))
1731
+ raise
1732
+ if response is not None and 'location' in response:
1733
+ location = response['location']
1734
+ logger.debug("Created host at: %s", location)
1735
+ hostObj = self.getHost(name)
1736
+ if hostObj is None:
1737
+ msg = "Host %s was created but not found." % name
1738
+ raise exceptions.HTTPNotFound(error={'desc': msg})
1739
+ return hostObj
1699
1740
 
1700
1741
  def modifyHost(self, name, mod_request):
1701
1742
  """Modify an existing Host entry.
@@ -2192,7 +2233,7 @@ class HPE3ParClient(object):
2192
2233
  response, body = self.http.get('/vluns')
2193
2234
  return body
2194
2235
 
2195
- def getVLUN(self, volumeName):
2236
+ def getVLUN(self, volumeName, allVluns=False):
2196
2237
  """Get information about a VLUN.
2197
2238
 
2198
2239
  :param volumeName: The volume name of the VLUN to find
@@ -2211,9 +2252,14 @@ class HPE3ParClient(object):
2211
2252
  response, body = self.http.get('/vluns?query=%s' %
2212
2253
  quote(query.encode("utf8")))
2213
2254
 
2214
- # Return the first VLUN found for the volume.
2215
- for vlun in body.get('members', []):
2216
- return vlun
2255
+ if allVluns:
2256
+ # Return all the VLUNs found for the volume.
2257
+ vluns = body.get('members', [])
2258
+ return vluns
2259
+ else:
2260
+ # Return the first VLUN found for the volume.
2261
+ for vlun in body.get('members', []):
2262
+ return vlun
2217
2263
  else:
2218
2264
  vluns = self.getVLUNs()
2219
2265
  if vluns:
@@ -5073,3 +5119,256 @@ class HPE3ParClient(object):
5073
5119
  # it means task cannot be cancelled,
5074
5120
  # because it is 'done' or already 'cancelled'
5075
5121
  pass
5122
+
5123
+ def getNvmePorts(self):
5124
+ all_ports = self.getPorts()
5125
+
5126
+ target_ports = []
5127
+ for port in all_ports['members']:
5128
+ if (
5129
+ port['mode'] == self.PORT_MODE_TARGET and
5130
+ port['linkState'] == self.PORT_STATE_READY
5131
+ ):
5132
+ port_pos = port['portPos']
5133
+ nsp = '%s:%s:%s' % (port_pos['node'], port_pos['slot'],
5134
+ port_pos['cardPort'])
5135
+ port['nsp'] = nsp
5136
+ target_ports.append(port)
5137
+
5138
+ nvme_ports = []
5139
+ for port in target_ports:
5140
+ if port['protocol'] == self.PORT_PROTO_NVME:
5141
+ nvme_ports.append(port)
5142
+
5143
+ logger.debug("nvme_ports: %(ports)s", {'ports': nvme_ports})
5144
+ return nvme_ports
5145
+
5146
+ def get_matched_array_ips_and_ports(self, client_conf):
5147
+ temp_nvme_ip = {}
5148
+ nvme_ip_list = {}
5149
+ conf_ips = client_conf['hpe3par_nvme_ips']
5150
+
5151
+ for ip_addr in conf_ips:
5152
+ # "ip"(given by user in cinder conf)
5153
+ # contains IP Address, NVMe port in <IP>:<PORT> format.
5154
+ ip = ip_addr.split(':')
5155
+ if len(ip) == 1:
5156
+ # "ip" doesn't contain NVMe port, use default NVMe port.
5157
+ temp_nvme_ip[ip_addr] = {'ip_port': self.DEFAULT_NVME_PORT}
5158
+ elif len(ip) == 2:
5159
+ #Valid format <IP>:<PORT>
5160
+ temp_nvme_ip[ip[0]] = {'ip_port': ip[1]}
5161
+ else:
5162
+ #Invalid format such as <IP>:<PORT>:<DATA>
5163
+ logger.warning("Invalid IP address format '%s'", ip_addr)
5164
+
5165
+ # get all the valid nvme ports from array
5166
+ # when found, add the valid nvme ip and port
5167
+ # to the nvme IP dictionary
5168
+ nvme_ports = self.getNvmePorts()
5169
+ if not len(nvme_ports):
5170
+ msg = 'Unable to obtain NVMe ports from storage system.'
5171
+ logger.error(msg)
5172
+ raise exceptions.HTTPNotFound(
5173
+ {'code': 'NON_EXISTENT_NVME_PORTS',
5174
+ 'desc': msg})
5175
+ for port in nvme_ports:
5176
+ ip = port['nodeWWN']
5177
+ if ip in temp_nvme_ip:
5178
+ ip_port = temp_nvme_ip[ip]['ip_port']
5179
+ nvme_ip_list[ip] = {'ip_port': ip_port,
5180
+ 'nsp': port['nsp']}
5181
+ del temp_nvme_ip[ip]
5182
+
5183
+ logger.debug("After mapping IPs from cinder to storage system:"
5184
+ "temp_nvme_ip: %(ips)s", {'ips': temp_nvme_ip})
5185
+ logger.debug("After mapping IPs from cinder to storage system:"
5186
+ "nvme_ip_list: %(ips)s", {'ips': nvme_ip_list})
5187
+
5188
+ # lets see if there are invalid nvme IPs left in the temp dict
5189
+ if len(temp_nvme_ip) > 0:
5190
+ logger.warning("Found invalid nvme IP address(s) in "
5191
+ "configuration option(s) hpe3par_nvme_ips '%s.'",
5192
+ (", ".join(temp_nvme_ip)))
5193
+
5194
+ if not len(nvme_ip_list):
5195
+ msg = 'At least one valid nvme IP address must be set.'
5196
+ logger.error(msg)
5197
+ raise exceptions.HTTPNotFound(
5198
+ {'code': 'NON_EXISTENT_NVME_IP',
5199
+ 'desc': msg})
5200
+
5201
+ ret_vals = (nvme_ip_list, nvme_ports)
5202
+ return ret_vals
5203
+
5204
+ def create_host_or_use_existing(self, hostname, iscsiNames=None,
5205
+ FCWwns=None, nqn=None, domain=None):
5206
+ try:
5207
+ host = self.getHost(hostname)
5208
+ logger.debug("Host is already present:%(name)s",
5209
+ {'name': hostname})
5210
+ return host
5211
+ except exceptions.HTTPNotFound:
5212
+ logger.debug("Host doesn't exist. Creating host with %(name)s",
5213
+ {'name': hostname})
5214
+ try:
5215
+ host = self.createHost(hostname, iscsiNames=iscsiNames,
5216
+ FCWwns=FCWwns, nqn=nqn,
5217
+ optional={'domain': domain})
5218
+ except Exception as ex:
5219
+ logger.error("Exception occurred while creating host %(name)s: %(ex)s",
5220
+ {'name': hostname, 'ex': str(ex)})
5221
+ raise
5222
+ return host
5223
+ except Exception as ex:
5224
+ logger.error("Exception occurred while creating host %(name)s: %(ex)s",
5225
+ {'name': hostname, 'ex': str(ex)})
5226
+ raise
5227
+
5228
+ def create_host_nvme(self, hostname, nqn=None, domain=None):
5229
+ host = self.create_host_or_use_existing(hostname, nqn=nqn, domain=domain)
5230
+ return host
5231
+
5232
+ def getNextLunId(self, hostname, host_type='nvme'):
5233
+ # lun id can be 0 through 16383 (1 to 256 for NVMe hosts)
5234
+ LIMIT = 16383
5235
+ if host_type=='nvme':
5236
+ LIMIT = 256
5237
+
5238
+ lun_id_max = 0
5239
+ try:
5240
+ host_vluns = self.getHostVLUNs(hostname)
5241
+ for vlun in host_vluns:
5242
+ lun_id_x = vlun['lun']
5243
+ if lun_id_x > lun_id_max:
5244
+ lun_id_max = lun_id_x
5245
+
5246
+ except exceptions.HTTPNotFound:
5247
+ # ignore, no existing VLUNs were found
5248
+ pass
5249
+
5250
+ lun_id_next = lun_id_max + 1
5251
+ if lun_id_next > LIMIT:
5252
+ msg = "Lun id exceeded limit '%d'" % LIMIT
5253
+ raise exceptions.HTTPNotFound(error={'desc': msg})
5254
+
5255
+ return lun_id_next
5256
+
5257
+ def getNqn(self, portPos=None):
5258
+ # in dev and QA array, all ports have same nqn below:
5259
+ # 'nqn.2014-08.org.nvmexpress.discovery'
5260
+ return self.DEFAULT_PORT_NQN
5261
+
5262
+ def build_portPos(self, nsp):
5263
+ arr = nsp.split(":")
5264
+ portPos = {}
5265
+ portPos['node'] = int(arr[0])
5266
+ portPos['slot'] = int(arr[1])
5267
+ portPos['cardPort'] = int(arr[2])
5268
+ return portPos
5269
+
5270
+ def find_existing_vluns(self, vol_name_3par, host):
5271
+ existing_vluns = []
5272
+ try:
5273
+ host_vluns = self.getHostVLUNs(host['name'])
5274
+ for vlun in host_vluns:
5275
+ if vlun['volumeName'] == vol_name_3par:
5276
+ existing_vluns.append(vlun)
5277
+ except exceptions.HTTPNotFound:
5278
+ # ignore, no existing VLUNs were found
5279
+ logger.debug("No existing VLUNs were found for host/volume "
5280
+ "combination: %(host)s, %(vol)s",
5281
+ {'host': host['name'],
5282
+ 'vol': vol_name_3par})
5283
+ return existing_vluns
5284
+
5285
+ def create_vlun_nvme(self, vol_name_3par, host, nvme_ips):
5286
+ """Create a VLUN for NVMe host.
5287
+ :param vol_name_3par: The name of the volume on 3PAR.
5288
+ :param host: The host object containing host information.
5289
+ :param nvme_ips: The NVMe IPs to use for the VLUN.
5290
+ :returns: A tuple containing a list of portals and target NQNs.
5291
+ :rtype: tuple
5292
+ """
5293
+
5294
+ # Collect all existing VLUNs for this volume/host combination.
5295
+ existing_vluns = self.find_existing_vluns(vol_name_3par, host)
5296
+ logger.debug("existing_vluns: %(ev)s", {'ev': existing_vluns})
5297
+ host_name = host['name']
5298
+ portals = []
5299
+ target_nqns = []
5300
+ lun_id = None
5301
+ # check for an already existing VLUN matching the
5302
+ # nsp for this nvme IP. If one is found, use it
5303
+ # instead of creating a new VLUN.
5304
+ if existing_vluns:
5305
+ for v in existing_vluns:
5306
+ lun_id = v['lun']
5307
+ logger.debug("vlun exists for host name: %(host)s" \
5308
+ " with lun: %(lun)s",
5309
+ {'host': host_name, 'lun': v['lun']})
5310
+ break
5311
+ else:
5312
+ logger.debug("creating vlun for host name: %(host)s",
5313
+ {'host': host_name})
5314
+ if lun_id is None:
5315
+ logger.debug("lun_id is None. calling getNextLunId")
5316
+ lun_id = self.getNextLunId(host_name)
5317
+ logger.debug("lun_id_next: %(id)s", {'id': lun_id})
5318
+ logger.debug("lun_id is %(id)s", {'id': lun_id})
5319
+ location = self.createVLUN(vol_name_3par, lun=lun_id,
5320
+ hostname=host_name)
5321
+ logger.debug("location: %(loc)s", {'loc': location})
5322
+ target_portal_ips = list(nvme_ips.keys())
5323
+ for nvme_ip in target_portal_ips:
5324
+ portals.append(
5325
+ (nvme_ip, nvme_ips[nvme_ip]['ip_port'], 'tcp')
5326
+ )
5327
+ #target_nqns.append(self.getNqn())
5328
+ vlun = self.getVLUN(vol_name_3par)
5329
+ nqn_of_vlun = vlun['Subsystem_NQN']
5330
+ logger.debug("nqn_of_vlun: %(nqn)s", {'nqn': nqn_of_vlun})
5331
+ target_nqns.append(nqn_of_vlun)
5332
+
5333
+ ret_vals = (portals, target_nqns)
5334
+ return ret_vals
5335
+
5336
+ def remove_vlun_nvme(self, vol_name_3par, hostname, host_nqn):
5337
+ vlunsData = self.getVLUN(vol_name_3par, True)
5338
+ print("vlunsData: ", vlunsData)
5339
+ if vlunsData == []:
5340
+ logger.error("No VLUN found for volume %(name)s on host %(host)s",
5341
+ {'name': vol_name_3par, 'host': hostname})
5342
+ return
5343
+
5344
+ # When deleteing VLUNs, you simply need to remove the template VLUN
5345
+ # and any active VLUNs will be automatically removed. The template
5346
+ # VLUN are marked as active: False
5347
+ vluns = []
5348
+ for vlun in vlunsData:
5349
+ if vol_name_3par in vlun['volumeName']:
5350
+ # template VLUNs are 'active' = False
5351
+ if not vlun['active']:
5352
+ vluns.append(vlun)
5353
+
5354
+ print("vluns: ", vluns)
5355
+ if not vluns:
5356
+ logger.warning("3PAR vlun for volume %(name)s not found on host "
5357
+ "%(host)s", {'name': vol_name_3par, 'host': hostname})
5358
+ return
5359
+
5360
+ for vlun in vluns:
5361
+ print(" -- vlun: ", vlun)
5362
+ # Check if this VLUN belongs to the specified hostname
5363
+ if vlun.get('hostname') == hostname:
5364
+ logger.debug("deleting vlun: %(lun)s", {'lun': vlun})
5365
+ print("deleting vlun")
5366
+ self.deleteVLUN(vol_name_3par, vlun['lun'],
5367
+ hostname)
5368
+ # port=vlun['portPos'])
5369
+ else:
5370
+ logger.debug("Skipping vlun: %(lun)s -"
5371
+ " belongs to different host: %(vlun_host)s",
5372
+ {'lun': vlun['lun'],
5373
+ 'vlun_host': vlun.get('hostname', 'Unknown')})
5374
+
@@ -72,6 +72,8 @@ class HTTPJSONRESTClient(object):
72
72
  if suppress_ssl_warnings:
73
73
  requests.packages.urllib3.disable_warnings()
74
74
 
75
+ HTTPJSONRESTClient._logger.debug("Initializing Session Key in __init__:")
76
+
75
77
  self.session_key = None
76
78
 
77
79
  # should be http://<Server:Port>/api/v1
@@ -86,6 +88,9 @@ class HTTPJSONRESTClient(object):
86
88
  # should be http://<Server:Port>/api/v1
87
89
  self.api_url = api_url.rstrip('/')
88
90
 
91
+ def get_session_key(self):
92
+ return self.session_key
93
+
89
94
  def set_debug_flag(self, flag):
90
95
  """
91
96
  This turns on/off http request/response debugging output to console
@@ -111,6 +116,8 @@ class HTTPJSONRESTClient(object):
111
116
 
112
117
  """
113
118
  # this prevens re-auth attempt if auth fails
119
+ HTTPJSONRESTClient._logger.debug("Session Key in authenticate: %s\n" %
120
+ (self.session_key))
114
121
  self.auth_try = 1
115
122
  self.session_key = None
116
123
 
@@ -136,9 +143,16 @@ class HTTPJSONRESTClient(object):
136
143
  This clears the authenticated session with the 3PAR server.
137
144
 
138
145
  """
139
- # delete the session on the 3Par
140
- self.delete('/credentials/%s' % self.session_key)
141
- self.session_key = None
146
+ try:
147
+ # delete the session on the 3Par
148
+ HTTPJSONRESTClient._logger.debug("Session Key in unauthenticate: %s\n" %
149
+ (self.session_key))
150
+ self.delete('/credentials/%s' % self.session_key)
151
+ self.session_key = None
152
+ except Exception as ex:
153
+ HTTPJSONRESTClient._logger.error("Error during unauthenticate: %s\n"
154
+ % (str(ex)))
155
+ raise ex
142
156
 
143
157
  def get_timings(self):
144
158
  """
@@ -275,7 +289,7 @@ class HTTPJSONRESTClient(object):
275
289
  self.delay = self.delay * self.backoff + 1
276
290
 
277
291
  # Raise exception, we have exhausted all retries.
278
- if self.tries is 0:
292
+ if self.tries == 0:
279
293
  raise ex
280
294
  except requests.exceptions.HTTPError as err:
281
295
  raise exceptions.HTTPError("HTTP Error: %s" % err)
@@ -292,8 +306,17 @@ class HTTPJSONRESTClient(object):
292
306
 
293
307
  return resp, body
294
308
 
309
+ def _build_full_url(self, url):
310
+ """Build full URL, supporting both absolute and relative URLs."""
311
+ if url.startswith('http://') or url.startswith('https://'):
312
+ return url.rstrip('/')
313
+ else:
314
+ return self.api_url + url
315
+
295
316
  def _time_request(self, url, method, **kwargs):
296
317
  start_time = time.time()
318
+ HTTPJSONRESTClient._logger.debug("url in _time_request: %s\n" %
319
+ (url))
297
320
  resp, body = self.request(url, method, **kwargs)
298
321
  self.times.append(("%s %s" % (method, url),
299
322
  start_time, time.time()))
@@ -301,11 +324,15 @@ class HTTPJSONRESTClient(object):
301
324
 
302
325
  def _do_reauth(self, url, method, ex, **kwargs):
303
326
  # print("_do_reauth called")
327
+ HTTPJSONRESTClient._logger.debug("session key in _do_reauth: %s\n" %
328
+ (self.session_key))
329
+ HTTPJSONRESTClient._logger.debug("auth_try in _do_reauth: %s\n" %
330
+ (self.auth_try))
304
331
  try:
305
332
  if self.auth_try != 1:
306
333
  self._reauth()
307
- resp, body = self._time_request(self.api_url + url, method,
308
- **kwargs)
334
+ full_url = self._build_full_url(url)
335
+ resp, body = self._time_request(full_url, method, **kwargs)
309
336
  return resp, body
310
337
  else:
311
338
  raise ex
@@ -317,8 +344,12 @@ class HTTPJSONRESTClient(object):
317
344
  # might be because the auth token expired, so try to
318
345
  # re-authenticate and try again. If it still fails, bail.
319
346
  try:
320
- resp, body = self._time_request(self.api_url + url, method,
321
- **kwargs)
347
+ HTTPJSONRESTClient._logger.debug("url in _cs_request: %s\n" %
348
+ (url))
349
+ full_url = self._build_full_url(url)
350
+ HTTPJSONRESTClient._logger.debug("full url in _cs_request: %s\n" %
351
+ (full_url))
352
+ resp, body = self._time_request(full_url, method, **kwargs)
322
353
  return resp, body
323
354
  except exceptions.HTTPUnauthorized as ex:
324
355
  # print("_CS_REQUEST HTTPUnauthorized")
@@ -1,2 +0,0 @@
1
- Changes for Arcus R4 and NVMe host
2
-
File without changes