Coverage for manila/share/drivers/netapp/dataontap/client/client_cmode_rest.py: 90%

2855 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2026-02-18 22:19 +0000

1# Copyright (c) 2023 NetApp, Inc. All rights reserved. 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); you may 

4# not use this file except in compliance with the License. You may obtain 

5# a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

12# License for the specific language governing permissions and limitations 

13# under the License. 

14 

15import copy 

16from datetime import datetime 

17from http import client as http_client 

18import math 

19import re 

20import time 

21 

22from oslo_log import log 

23from oslo_utils import excutils 

24from oslo_utils import netutils 

25from oslo_utils import strutils 

26from oslo_utils import units 

27 

28from manila import exception 

29from manila.i18n import _ 

30from manila.share.drivers.netapp.dataontap.client import client_base 

31from manila.share.drivers.netapp.dataontap.client import client_cmode 

32from manila.share.drivers.netapp.dataontap.client import rest_api as netapp_api 

33from manila.share.drivers.netapp import utils as na_utils 

34from manila import utils 

35 

36LOG = log.getLogger(__name__) 

37DELETED_PREFIX = 'deleted_manila_' 

38DEFAULT_IPSPACE = 'Default' 

39CLUSTER_IPSPACES = ('Cluster', DEFAULT_IPSPACE) 

40DEFAULT_BROADCAST_DOMAIN = 'Default' 

41BROADCAST_DOMAIN_PREFIX = 'domain_' 

42DEFAULT_MAX_PAGE_LENGTH = 10000 

43CIFS_USER_GROUP_TYPE = 'windows' 

44SNAPSHOT_CLONE_OWNER = 'volume_clone' 

45CUTOVER_ACTION_MAP = { 

46 'defer': 'defer_on_failure', 

47 'abort': 'abort_on_failure', 

48 'force': 'force', 

49 'wait': 'wait', 

50} 

51DEFAULT_TIMEOUT = 15 

52DEFAULT_TCP_MAX_XFER_SIZE = 65536 

53DEFAULT_UDP_MAX_XFER_SIZE = 32768 

54DEFAULT_SECURITY_CERT_EXPIRE_DAYS = 365 

55 

56 

57class NetAppRestClient(object): 

58 

59 def __init__(self, **kwargs): 

60 

61 self.connection = netapp_api.RestNaServer( 

62 host=kwargs['hostname'], 

63 transport_type=kwargs['transport_type'], 

64 ssl_cert_path=kwargs['ssl_cert_path'], 

65 port=kwargs['port'], 

66 username=kwargs['username'], 

67 password=kwargs['password'], 

68 trace=kwargs.get('trace', False), 

69 api_trace_pattern=kwargs.get('api_trace_pattern', 

70 na_utils.API_TRACE_PATTERN), 

71 private_key_file=kwargs['private_key_file'], 

72 certificate_file=kwargs['certificate_file'], 

73 ca_certificate_file=kwargs['ca_certificate_file'], 

74 certificate_host_validation=kwargs['certificate_host_validation']) 

75 

76 self.async_rest_timeout = kwargs['async_rest_timeout'] 

77 

78 self.vserver = kwargs.get('vserver') 

79 

80 self.connection.set_vserver(self.vserver) 

81 

82 # NOTE(nahimsouza): Set this flag to False to ensure get_ontap_version 

83 # will be called without SVM tunneling. This is necessary because 

84 # requests with SVM scoped account can not be tunneled in REST API. 

85 self._have_cluster_creds = False 

86 ontap_version = self.get_ontap_version(cached=False) 

87 if ontap_version['version-tuple'] < (9, 12, 1): 87 ↛ 88line 87 didn't jump to line 88 because the condition on line 87 was never true

88 msg = _('This driver can communicate with ONTAP via REST APIs ' 

89 'exclusively only when paired with a NetApp ONTAP storage ' 

90 'system running release 9.12.1 or newer. ' 

91 'To use ZAPI and supported REST APIs instead, ' 

92 'set "netapp_use_legacy_client" to True.') 

93 raise exception.NetAppException(msg) 

94 self.connection.set_ontap_version(ontap_version) 

95 

96 # NOTE(nahimsouza): ZAPI Client is needed to implement the fallback 

97 # when a REST method is not supported. 

98 self.zapi_client = client_cmode.NetAppCmodeClient(**kwargs) 

99 

100 self._have_cluster_creds = self._check_for_cluster_credentials() 

101 

102 self._init_features() 

103 

104 def _init_features(self): 

105 """Initialize feature support map.""" 

106 self.features = client_base.Features() 

107 

108 # NOTE(felipe_rodrigues): REST client only runs with ONTAP 9.11.1 or 

109 # upper, so all features below are supported with this client. 

110 self.features.add_feature('SNAPMIRROR_V2', supported=True) 

111 self.features.add_feature('SYSTEM_METRICS', supported=True) 

112 self.features.add_feature('SYSTEM_CONSTITUENT_METRICS', 

113 supported=True) 

114 self.features.add_feature('BROADCAST_DOMAINS', supported=True) 

115 self.features.add_feature('IPSPACES', supported=True) 

116 self.features.add_feature('SUBNETS', supported=True) 

117 self.features.add_feature('CLUSTER_PEER_POLICY', supported=True) 

118 self.features.add_feature('ADVANCED_DISK_PARTITIONING', 

119 supported=True) 

120 self.features.add_feature('KERBEROS_VSERVER', supported=True) 

121 self.features.add_feature('FLEXVOL_ENCRYPTION', supported=True) 

122 self.features.add_feature('SVM_DR', supported=True) 

123 self.features.add_feature('ADAPTIVE_QOS', supported=True) 

124 self.features.add_feature('TRANSFER_LIMIT_NFS_CONFIG', 

125 supported=True) 

126 self.features.add_feature('CIFS_DC_ADD_SKIP_CHECK', 

127 supported=True) 

128 self.features.add_feature('LDAP_LDAP_SERVERS', 

129 supported=True) 

130 self.features.add_feature('FLEXGROUP', supported=True) 

131 self.features.add_feature('FLEXGROUP_FAN_OUT', supported=True) 

132 self.features.add_feature('SVM_MIGRATE', supported=True) 

133 self.features.add_feature('UNIFIED_AGGR', supported=True) 

134 

135 def __getattr__(self, name): 

136 """If method is not implemented for REST, try to call the ZAPI.""" 

137 LOG.debug("The %s call is not supported for REST, falling back to " 

138 "ZAPI.", name) 

139 # Don't use self.zapi_client to avoid reentrant call to __getattr__() 

140 zapi_client = object.__getattribute__(self, 'zapi_client') 

141 return getattr(zapi_client, name) 

142 

143 def _wait_job_result(self, job_url): 

144 

145 interval = 2 

146 retries = (self.async_rest_timeout / interval) 

147 

148 @utils.retry(netapp_api.NaRetryableError, interval=interval, 

149 retries=retries, backoff_rate=1) 

150 def _waiter(): 

151 response = self.send_request(job_url, 'get', 

152 enable_tunneling=False) 

153 

154 job_state = response.get('state') 

155 if job_state == 'success': 

156 return response 

157 elif job_state == 'failure': 

158 message = response['error']['message'] 

159 code = response['error']['code'] 

160 raise netapp_api.NaRetryableError(message=message, code=code) 

161 

162 msg_args = {'job': job_url, 'state': job_state} 

163 LOG.debug("Job %(job)s has not finished: %(state)s", msg_args) 

164 raise netapp_api.NaRetryableError(message='Job is running.') 

165 

166 try: 

167 return _waiter() 

168 except netapp_api.NaRetryableError: 

169 msg = _("Job %s did not reach the expected state. Retries " 

170 "exhausted. Aborting.") % job_url 

171 raise na_utils.NetAppDriverException(msg) 

172 

173 def send_request(self, action_url, method, body=None, query=None, 

174 enable_tunneling=True, 

175 max_page_length=DEFAULT_MAX_PAGE_LENGTH, 

176 wait_on_accepted=True): 

177 

178 """Sends REST request to ONTAP. 

179 

180 :param action_url: action URL for the request 

181 :param method: HTTP method for the request ('get', 'post', 'put', 

182 'delete' or 'patch') 

183 :param body: dict of arguments to be passed as request body 

184 :param query: dict of arguments to be passed as query string 

185 :param enable_tunneling: enable tunneling to the ONTAP host 

186 :param max_page_length: size of the page during pagination 

187 :param wait_on_accepted: if True, wait until the job finishes when 

188 HTTP code 202 (Accepted) is returned 

189 

190 :returns: parsed REST response 

191 """ 

192 

193 # NOTE(felipe_rodrigues): disable tunneling when running in SVM scoped 

194 # context, otherwise REST API fails. 

195 if not self._have_cluster_creds: 195 ↛ 196line 195 didn't jump to line 196 because the condition on line 195 was never true

196 enable_tunneling = False 

197 

198 response = None 

199 

200 if method == 'get': 

201 response = self.get_records( 

202 action_url, query, enable_tunneling, max_page_length) 

203 else: 

204 code, response = self.connection.invoke_successfully( 

205 action_url, method, body=body, query=query, 

206 enable_tunneling=enable_tunneling) 

207 

208 if code == http_client.ACCEPTED and wait_on_accepted: 

209 # get job URL and discard '/api' 

210 job_url = response['job']['_links']['self']['href'][4:] 

211 response = self._wait_job_result(job_url) 

212 

213 return response 

214 

215 def get_records(self, action_url, query=None, enable_tunneling=True, 

216 max_page_length=DEFAULT_MAX_PAGE_LENGTH): 

217 """Retrieves ONTAP resources using pagination REST request. 

218 

219 :param action_url: action URL for the request 

220 :param query: dict of arguments to be passed as query string 

221 :param enable_tunneling: enable tunneling to the ONTAP host 

222 :param max_page_length: size of the page during pagination 

223 

224 :returns: dict containing records and num_records 

225 """ 

226 

227 # NOTE(felipe_rodrigues): disable tunneling when running in SVM scoped 

228 # context, otherwise REST API fails. 

229 if not self._have_cluster_creds: 229 ↛ 230line 229 didn't jump to line 230 because the condition on line 229 was never true

230 enable_tunneling = False 

231 

232 # Initialize query variable if it is None 

233 query = query if query else {} 

234 query['max_records'] = max_page_length 

235 

236 _, response = self.connection.invoke_successfully( 

237 action_url, 'get', query=query, 

238 enable_tunneling=enable_tunneling) 

239 

240 # NOTE(nahimsouza): if all records are returned in the first call, 

241 # 'next_url' will be None. 

242 next_url = response.get('_links', {}).get('next', {}).get('href') 

243 next_url = next_url[4:] if next_url else None # discard '/api' 

244 

245 # Get remaining pages, saving data into first page 

246 while next_url: 

247 # NOTE(nahimsouza): clean the 'query', because the parameters are 

248 # already included in 'next_url'. 

249 _, next_response = self.connection.invoke_successfully( 

250 next_url, 'get', query=None, 

251 enable_tunneling=enable_tunneling) 

252 

253 response['num_records'] += next_response.get('num_records', 0) 

254 response['records'].extend(next_response.get('records')) 

255 

256 next_url = ( 

257 next_response.get('_links', {}).get('next', {}).get('href')) 

258 next_url = next_url[4:] if next_url else None # discard '/api' 

259 

260 return response 

261 

262 @na_utils.trace 

263 def get_ontap_version(self, cached=True): 

264 """Get the current Data ONTAP version.""" 

265 

266 if cached: 

267 return self.connection.get_ontap_version() 

268 

269 query = { 

270 'fields': 'version' 

271 } 

272 

273 try: 

274 response = self.send_request('/cluster/nodes', 'get', query=query, 

275 enable_tunneling=False) 

276 records = response.get('records')[0] 

277 

278 return { 

279 'version': records['version']['full'], 

280 'version-tuple': (records['version']['generation'], 

281 records['version']['major'], 

282 records['version']['minor']), 

283 } 

284 except netapp_api.api.NaApiError as e: 

285 if e.code != netapp_api.EREST_NOT_AUTHORIZED: 285 ↛ 286line 285 didn't jump to line 286 because the condition on line 285 was never true

286 raise 

287 

288 # NOTE(nahimsouza): SVM scoped account is not authorized to access 

289 # the /cluster/nodes endpoint, that's why we use /private/cli 

290 

291 response = self.send_request('/private/cli/version', 'get', 

292 query=query) 

293 # Response is formatted as: 

294 # 'NetApp Release 9.12.1: Wed Feb 01 01:10:18 UTC 2023' 

295 version_full = response['records'][0]['version']['full'] 

296 version_parsed = re.findall(r'\d+\.\d+\.\d+', version_full)[0] 

297 version_splited = version_parsed.split('.') 

298 return { 

299 'version': version_full, 

300 'version-tuple': (int(version_splited[0]), 

301 int(version_splited[1]), 

302 int(version_splited[2])), 

303 } 

304 

305 @na_utils.trace 

306 def get_job(self, job_uuid): 

307 """Get a job in ONTAP. 

308 

309 :param job_uuid: uuid of the job to be searched. 

310 """ 

311 action_url = f'/cluster/jobs/{job_uuid}' 

312 return self.send_request(action_url, 'get', enable_tunneling=False) 

313 

314 @na_utils.trace 

315 def _has_records(self, api_response): 

316 """Check if API response contains any records.""" 

317 if (not api_response['num_records'] or 

318 api_response['num_records'] == 0): 

319 return False 

320 else: 

321 return True 

322 

323 @na_utils.trace 

324 def get_licenses(self): 

325 """Get list of ONTAP licenses.""" 

326 try: 

327 result = self.send_request('/cluster/licensing/licenses', 'get') 

328 except netapp_api.api.NaApiError: 

329 with excutils.save_and_reraise_exception(): 

330 LOG.exception("Could not get list of ONTAP licenses.") 

331 

332 return sorted( 

333 [license['name'] for license in result.get('records', [])]) 

334 

335 @na_utils.trace 

336 def _get_security_key_manager_nve_support(self): 

337 """Determine whether the cluster platform supports Volume Encryption""" 

338 

339 query = {'fields': 'volume_encryption.*'} 

340 

341 try: 

342 response = self.send_request('/security/key-managers', 

343 'get', query=query) 

344 records = response.get('records', []) 

345 if records: 

346 if records[0]['volume_encryption']['supported']: 

347 return True 

348 except netapp_api.api.NaApiError as e: 

349 LOG.debug("NVE disabled due to error code: %s - %s", 

350 e.code, e.message) 

351 return False 

352 

353 LOG.debug("NVE disabled - Key management is not " 

354 "configured on the admin Vserver.") 

355 return False 

356 

357 @na_utils.trace 

358 def is_nve_supported(self): 

359 """Determine whether NVE is supported on this platform.""" 

360 

361 nodes = self.list_cluster_nodes() 

362 system_version = self.get_ontap_version() 

363 version = system_version.get('version') 

364 

365 # Not all platforms support this feature. NVE is not supported if the 

366 # version includes the substring '<1no-DARE>' (no Data At Rest 

367 # Encryption). 

368 if "<1no-DARE>" not in version: 

369 if nodes is not None: 369 ↛ 372line 369 didn't jump to line 372 because the condition on line 369 was always true

370 return self._get_security_key_manager_nve_support() 

371 else: 

372 LOG.warning('Cluster credentials are required in order to ' 

373 'determine whether NetApp Volume Encryption is ' 

374 'supported or not on this platform.') 

375 return False 

376 else: 

377 LOG.warning('NetApp Volume Encryption is not supported on this ' 

378 'ONTAP version: %(version)s. ', {'version': version}) 

379 return False 

380 

381 @na_utils.trace 

382 def check_for_cluster_credentials(self): 

383 """Check if credentials to connect to ONTAP from cached value.""" 

384 return self._have_cluster_creds 

385 

386 @na_utils.trace 

387 def _check_for_cluster_credentials(self): 

388 """Check if credentials to connect to ONTAP are defined correctly.""" 

389 try: 

390 self.list_cluster_nodes() 

391 # API succeeded, so definitely a cluster management LIF 

392 return True 

393 except netapp_api.api.NaApiError as e: 

394 if e.code == netapp_api.EREST_NOT_AUTHORIZED: 

395 LOG.debug('Not connected to cluster management LIF.') 

396 return False 

397 else: 

398 raise 

399 

400 @na_utils.trace 

401 def list_cluster_nodes(self): 

402 """Get all available cluster nodes.""" 

403 

404 result = self.send_request('/cluster/nodes', 'get') 

405 return [node['name'] for node in result.get('records', [])] 

406 

407 @na_utils.trace 

408 def _get_volume_by_args(self, vol_name=None, aggregate_name=None, 

409 vol_path=None, vserver=None, fields=None, 

410 is_root=None): 

411 """Get info from a single volume according to the args.""" 

412 

413 query = { 

414 'style': 'flex*', # Match both 'flexvol' and 'flexgroup' 

415 'error_state.is_inconsistent': 'false', 

416 'fields': 'name,style,svm.name,svm.uuid' 

417 } 

418 

419 if vol_name: 

420 query['name'] = vol_name 

421 if aggregate_name: 

422 query['aggregates.name'] = aggregate_name 

423 if vol_path: 

424 query['nas.path'] = vol_path 

425 if vserver: 

426 query['svm.name'] = vserver 

427 if fields: 

428 query['fields'] = fields 

429 if is_root is not None: 

430 query['is_svm_root'] = is_root 

431 

432 volumes_response = self.send_request( 

433 '/storage/volumes/', 'get', query=query) 

434 

435 records = volumes_response.get('records', []) 

436 if len(records) != 1: 

437 msg = _('Could not find unique share. Shares found: %(shares)s.') 

438 msg_args = {'shares': records} 

439 raise exception.NetAppException(message=msg % msg_args) 

440 

441 return records[0] 

442 

443 @na_utils.trace 

444 def restore_snapshot(self, volume_name, snapshot_name): 

445 """Reverts a volume to the specified snapshot.""" 

446 

447 volume = self._get_volume_by_args(vol_name=volume_name) 

448 uuid = volume['uuid'] 

449 

450 body = { 

451 'restore_to.snapshot.name': snapshot_name 

452 } 

453 

454 # Update volume 

455 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body) 

456 

457 @na_utils.trace 

458 def vserver_exists(self, vserver_name): 

459 """Checks if Vserver exists.""" 

460 LOG.debug('Checking if Vserver %s exists', vserver_name) 

461 

462 query = { 

463 'name': vserver_name 

464 } 

465 

466 try: 

467 result = self.send_request('/svm/svms', 'get', query=query, 

468 enable_tunneling=False) 

469 except netapp_api.api.NaApiError as e: 

470 if e.code == netapp_api.EREST_VSERVER_NOT_FOUND: 

471 return False 

472 else: 

473 raise 

474 return self._has_records(result) 

475 

476 @na_utils.trace 

477 def list_root_aggregates(self): 

478 """Get names of all aggregates that contain node root volumes.""" 

479 response = self.send_request('/private/cli/aggr', 'get', 

480 query={'root': 'true'}) 

481 return [aggr['aggregate'] for aggr in response['records']] 

482 

483 @na_utils.trace 

484 def list_non_root_aggregates(self): 

485 """Get names of all aggregates that don't contain node root volumes.""" 

486 

487 # NOTE(nahimsouza): According to REST API doc, only data aggregates are 

488 # returned by the /storage/aggregates endpoint, which means no System 

489 # owned root aggregate will be included in the output. Also, note that 

490 # this call does not work for users with SVM scoped account. 

491 response = self.send_request('/storage/aggregates', 'get') 

492 aggr_list = response['records'] 

493 

494 return [aggr['name'] for aggr in aggr_list] 

495 

496 @na_utils.trace 

497 def get_cluster_aggregate_capacities(self, aggregate_names): 

498 """Calculates capacity of one or more aggregates. 

499 

500 Returns dictionary of aggregate capacity metrics. 

501 'used' is the actual space consumed on the aggregate. 

502 'available' is the actual space remaining. 

503 'size' is the defined total aggregate size, such that 

504 used + available = total. 

505 """ 

506 if aggregate_names is not None and len(aggregate_names) == 0: 506 ↛ 507line 506 didn't jump to line 507 because the condition on line 506 was never true

507 return {} 

508 

509 fields = 'name,space' 

510 aggrs = self._get_aggregates(aggregate_names=aggregate_names, 

511 fields=fields) 

512 aggr_space_dict = dict() 

513 for aggr in aggrs: 

514 aggr_name = aggr['name'] 

515 aggr_space_attrs = aggr['space'] 

516 

517 aggr_space_dict[aggr_name] = { 

518 'available': 

519 int(aggr_space_attrs["block_storage"]["available"]), 

520 'total': 

521 int(aggr_space_attrs["block_storage"]["size"]), 

522 'used': 

523 int(aggr_space_attrs["block_storage"]["used"]), 

524 } 

525 return aggr_space_dict 

526 

527 @na_utils.trace 

528 def _get_aggregates(self, aggregate_names=None, fields=None): 

529 """Get a list of aggregates and their attributes. 

530 

531 :param aggregate_names: List of aggregate names. 

532 :param fields: List of fields to be retrieved from each aggregate. 

533 

534 :return: List of aggregates. 

535 """ 

536 

537 query = {} 

538 if aggregate_names: 538 ↛ 541line 538 didn't jump to line 541 because the condition on line 538 was always true

539 query['name'] = ','.join(aggregate_names) 

540 

541 if fields: 541 ↛ 542line 541 didn't jump to line 542 because the condition on line 541 was never true

542 query['fields'] = fields 

543 

544 # NOTE(nahimsouza): This endpoint returns only data aggregates. Also, 

545 # it does not work with SVM scoped account. 

546 response = self.send_request('/storage/aggregates', 'get', query=query) 

547 

548 if not self._has_records(response): 548 ↛ 549line 548 didn't jump to line 549 because the condition on line 548 was never true

549 return [] 

550 else: 

551 return response.get('records', []) 

552 

553 @na_utils.trace 

554 def get_aggregate(self, aggregate_name): 

555 """Get aggregate attributes needed for the storage service catalog.""" 

556 

557 if not aggregate_name: 

558 return {} 

559 

560 fields = ('name,block_storage.primary.raid_type,' 

561 'block_storage.storage_type,snaplock_type') 

562 

563 try: 

564 aggrs = self._get_aggregates(aggregate_names=[aggregate_name], 

565 fields=fields) 

566 except netapp_api.api.NaApiError: 

567 LOG.exception('Failed to get info for aggregate %s.', 

568 aggregate_name) 

569 return {} 

570 

571 if len(aggrs) == 0: 571 ↛ 572line 571 didn't jump to line 572 because the condition on line 571 was never true

572 return {} 

573 

574 aggr_attributes = aggrs[0] 

575 

576 aggregate = { 

577 'name': aggr_attributes['name'], 

578 'raid-type': 

579 aggr_attributes['block_storage']['primary']['raid_type'], 

580 'is-hybrid': 

581 aggr_attributes['block_storage']['storage_type'] == 'hybrid', 

582 'snaplock-type': aggr_attributes.get('snaplock_type'), 

583 'is-snaplock': False if (aggr_attributes.get('snaplock_type') 

584 == 'non_snaplock') else True 

585 } 

586 

587 return aggregate 

588 

589 @na_utils.trace 

590 def get_node_for_aggregate(self, aggregate_name): 

591 """Get home node for the specified aggregate. 

592 

593 This API could return None, most notably if it was sent 

594 to a Vserver LIF, so the caller must be able to handle that case. 

595 """ 

596 

597 if not aggregate_name: 

598 return None 

599 

600 fields = 'name,home_node.name' 

601 

602 try: 

603 aggrs = self._get_aggregates(aggregate_names=[aggregate_name], 

604 fields=fields) 

605 except netapp_api.api.NaApiError as e: 

606 if e.code == netapp_api.EREST_NOT_AUTHORIZED: 

607 LOG.debug("Could not get the home node of aggregate %s: " 

608 "command not authorized.", aggregate_name) 

609 return None 

610 else: 

611 raise 

612 

613 return aggrs[0]['home_node']['name'] if aggrs else None 

614 

615 @na_utils.trace 

616 def get_aggregate_disk_types(self, aggregate_name): 

617 """Get the disk type(s) of an aggregate.""" 

618 

619 disk_types = set() 

620 disk_types.update(self._get_aggregate_disk_types(aggregate_name)) 

621 

622 return list(disk_types) if disk_types else None 

623 

624 @na_utils.trace 

625 def _get_aggregate_disk_types(self, aggregate_name): 

626 """Get the disk type(s) of an aggregate (may be a list).""" 

627 

628 disk_types = set() 

629 

630 query = { 

631 'aggregates.name': aggregate_name, 

632 'fields': 'effective_type' 

633 } 

634 

635 try: 

636 response = self.send_request( 

637 '/storage/disks', 'get', query=query) 

638 except netapp_api.api.NaApiError: 

639 LOG.exception('Failed to get disk info for aggregate %s.', 

640 aggregate_name) 

641 

642 return disk_types 

643 

644 for storage_disk_info in response['records']: 

645 disk_types.add(storage_disk_info['effective_type']) 

646 

647 return disk_types 

648 

649 @na_utils.trace 

650 def volume_exists(self, volume_name): 

651 """Checks if volume exists.""" 

652 LOG.debug('Checking if volume %s exists', volume_name) 

653 

654 query = { 

655 'name': volume_name 

656 } 

657 

658 result = self.send_request( 

659 '/storage/volumes', 'get', query=query) 

660 return self._has_records(result) 

661 

662 @na_utils.trace 

663 def list_vserver_aggregates(self): 

664 """Returns a list of aggregates available to a vserver. 

665 

666 This must be called against a Vserver LIF. 

667 """ 

668 return list(self.get_vserver_aggregate_capacities().keys()) 

669 

670 @na_utils.trace 

671 def get_vserver_aggregate_capacities(self, aggregate_names=None): 

672 """Calculates capacity of one or more aggregates for a vserver. 

673 

674 Returns dictionary of aggregate capacity metrics. This must 

675 be called against a Vserver LIF. 

676 """ 

677 

678 if aggregate_names is not None and len(aggregate_names) == 0: 

679 return {} 

680 

681 query = { 

682 'fields': 'name,aggregates.name,aggregates.available_size' 

683 } 

684 response = self.send_request('/svm/svms', 'get', query=query) 

685 

686 if not response['records']: 

687 msg = _('Could not find information of vserver.') 

688 raise exception.NetAppException(message=msg) 

689 vserver = response['records'][0] 

690 

691 aggr_space_dict = dict() 

692 for aggr in vserver.get('aggregates', []): 

693 available_size = aggr.get('available_size') 

694 if available_size is None: 

695 # NOTE(felipe_rodrigues): available_size not returned means 

696 # the vserver does not have any aggregate assigned to it. REST 

697 # API returns all non root aggregates of the cluster to vserver 

698 # that does not have any, but without the space information. 

699 LOG.warning('No aggregates assigned to Vserver %s.', 

700 vserver['name']) 

701 return {} 

702 

703 aggr_name = aggr['name'] 

704 if aggregate_names is None or aggr_name in aggregate_names: 

705 aggr_space_dict[aggr['name']] = {'available': available_size} 

706 

707 if not aggr_space_dict: 

708 LOG.warning('No aggregates assigned to Vserver %s.', 

709 vserver['name']) 

710 return {} 

711 

712 LOG.debug('Found available Vserver aggregates: %s.', aggr_space_dict) 

713 return aggr_space_dict 

714 

715 @na_utils.trace 

716 def qos_policy_group_create(self, qos_policy_group_name, vserver, 

717 max_throughput=None, min_throughput=None): 

718 """Creates a QoS policy group.""" 

719 

720 body = { 

721 'name': qos_policy_group_name, 

722 'svm.name': vserver, 

723 } 

724 if max_throughput: 

725 value = max_throughput.lower() 

726 if 'iops' in max_throughput: 

727 value = value.replace('iops', '') 

728 value = int(value) 

729 body['fixed.max_throughput_iops'] = value 

730 else: 

731 value = value.replace('b/s', '') 

732 value = int(value) 

733 body['fixed.max_throughput_mbps'] = math.ceil(value / 

734 units.Mi) 

735 if min_throughput: 

736 value = min_throughput.lower() 

737 if 'iops' in min_throughput: 

738 value = value.replace('iops', '') 

739 value = int(value) 

740 body['fixed.min_throughput_iops'] = value 

741 else: 

742 value = value.replace('b/s', '') 

743 value = int(value) 

744 body['fixed.min_throughput_mbps'] = math.ceil(value / 

745 units.Mi) 

746 return self.send_request('/storage/qos/policies', 'post', 

747 body=body) 

748 

749 @na_utils.trace 

750 def list_network_interfaces(self): 

751 """Get the names of available LIFs.""" 

752 

753 query = { 

754 'fields': 'name' 

755 } 

756 

757 result = self.send_request('/network/ip/interfaces', 'get', 

758 query=query) 

759 

760 if self._has_records(result): 760 ↛ exitline 760 didn't return from function 'list_network_interfaces' because the condition on line 760 was always true

761 return [lif['name'] for lif in result.get('records', [])] 

762 

763 @na_utils.trace 

764 def get_network_interfaces(self, protocols=None): 

765 """Get available LIFs.""" 

766 

767 protocols = na_utils.convert_to_list(protocols) 

768 protocols = [f"data_{protocol.lower()}" for protocol in protocols] 

769 

770 if protocols: 

771 query = { 

772 'services': ','.join(protocols), 

773 'fields': 'ip.address,location.home_node.name,' 

774 'location.home_port.name,ip.netmask,' 

775 'services,svm.name,enabled' 

776 } 

777 else: 

778 query = { 

779 'fields': 'ip.address,location.home_node.name,' 

780 'location.home_port.name,ip.netmask,' 

781 'services,svm.name,enabled' 

782 } 

783 

784 result = self.send_request('/network/ip/interfaces', 'get', 

785 query=query) 

786 

787 interfaces = [] 

788 for lif_info in result.get('records', []): 

789 lif = { 

790 'administrative-status': ( 

791 'up' if lif_info['enabled'] else 'down'), 

792 'uuid': lif_info['uuid'], 

793 'address': lif_info['ip']['address'], 

794 'home-node': lif_info['location']['home_node']['name'], 

795 'home-port': lif_info['location']['home_port']['name'], 

796 'interface-name': lif_info['name'], 

797 'netmask': lif_info['ip']['netmask'], 

798 'role': lif_info['services'], 

799 'vserver': lif_info['svm']['name'], 

800 } 

801 interfaces.append(lif) 

802 return interfaces 

803 

804 @na_utils.trace 

805 def clear_nfs_export_policy_for_volume(self, volume_name): 

806 """Clear NFS export policy for volume, i.e. sets it to default.""" 

807 self.set_nfs_export_policy_for_volume(volume_name, 'default') 

808 

809 @na_utils.trace 

810 def set_nfs_export_policy_for_volume(self, volume_name, policy_name): 

811 """Set NFS the export policy for the specified volume.""" 

812 query = {"name": volume_name} 

813 body = {'nas.export_policy.name': policy_name} 

814 

815 try: 

816 self.send_request('/storage/volumes/', 'patch', query=query, 

817 body=body) 

818 except netapp_api.api.NaApiError as e: 

819 # NOTE(nahimsouza): Since this error is ignored in ZAPI, we are 

820 # replicating the behavior here. 

821 if e.code == netapp_api.EREST_CANNOT_MODITY_OFFLINE_VOLUME: 821 ↛ exitline 821 didn't return from function 'set_nfs_export_policy_for_volume' because the condition on line 821 was always true

822 LOG.debug('Cannot modify offline volume: %s', volume_name) 

823 return 

824 

825 @na_utils.trace 

826 def create_nfs_export_policy(self, policy_name): 

827 """Create an NFS export policy.""" 

828 body = {'name': policy_name} 

829 try: 

830 self.send_request('/protocols/nfs/export-policies', 'post', 

831 body=body) 

832 except netapp_api.api.NaApiError as e: 

833 if e.code != netapp_api.EREST_DUPLICATE_ENTRY: 833 ↛ exitline 833 didn't return from function 'create_nfs_export_policy' because the condition on line 833 was always true

834 msg = _("Create NFS export policy %s fail.") 

835 LOG.debug(msg, policy_name) 

836 raise 

837 

838 @na_utils.trace 

839 def soft_delete_nfs_export_policy(self, policy_name): 

840 """Try to delete export policy or mark it to be deleted later.""" 

841 try: 

842 self.delete_nfs_export_policy(policy_name) 

843 except netapp_api.api.NaApiError: 

844 # NOTE(cknight): Policy deletion can fail if called too soon after 

845 # removing from a flexvol. So rename for later harvesting. 

846 LOG.warning("Fail to delete NFS export policy %s." 

847 "Export policy will be renamed instead.", policy_name) 

848 self.rename_nfs_export_policy(policy_name, 

849 DELETED_PREFIX + policy_name) 

850 

851 @na_utils.trace 

852 def rename_nfs_export_policy(self, policy_name, new_policy_name): 

853 """Rename NFS export policy.""" 

854 response = self.send_request( 

855 '/protocols/nfs/export-policies', 'get', 

856 query={'name': policy_name}) 

857 

858 if not self._has_records(response): 

859 msg = _('Could not rename policy %(policy_name)s. ' 

860 'Entry does not exist.') 

861 msg_args = {'policy_name': policy_name} 

862 raise exception.NetAppException(msg % msg_args) 

863 

864 uuid = response['records'][0]['id'] 

865 body = {'name': new_policy_name} 

866 self.send_request(f'/protocols/nfs/export-policies/{uuid}', 

867 'patch', body=body) 

868 

869 @na_utils.trace 

870 def get_volume_junction_path(self, volume_name, is_style_cifs=False): 

871 """Gets a volume junction path.""" 

872 query = { 

873 'name': volume_name, 

874 'fields': 'nas.path' 

875 } 

876 result = self.send_request('/storage/volumes/', 'get', query=query) 

877 return result['records'][0]['nas']['path'] 

878 

879 @na_utils.trace 

880 def get_volume_snapshot_attributes(self, volume_name): 

881 """Returns snapshot attributes""" 

882 volume = self._get_volume_by_args(vol_name=volume_name) 

883 vol_uuid = volume['uuid'] 

884 query = { 

885 'fields': 'snapshot_directory_access_enabled,snapshot_policy.name' 

886 } 

887 

888 result = self.send_request( 

889 f'/storage/volumes/{vol_uuid}', 'get', query=query) 

890 

891 snap_attributes = {} 

892 snap_attributes['snapshot-policy'] = result.get( 

893 'snapshot_policy', '').get('name') 

894 snap_attributes['snapdir-access-enabled'] = result.get( 

895 'snapshot_directory_access_enabled', 'false') 

896 return snap_attributes 

897 

898 @na_utils.trace 

899 def get_volume(self, volume_name): 

900 """Returns the volume with the specified name, if present.""" 

901 query = { 

902 'name': volume_name, 

903 'fields': 'aggregates.name,nas.path,name,svm.name,type,style,' 

904 'qos.policy.name,space.size,space.used,snaplock.type' 

905 } 

906 

907 result = self.send_request('/storage/volumes', 'get', query=query) 

908 

909 if not self._has_records(result): 

910 raise exception.StorageResourceNotFound(name=volume_name) 

911 elif result['num_records'] > 1: 

912 msg = _('Could not find unique volume %(vol)s.') 

913 msg_args = {'vol': volume_name} 

914 raise exception.NetAppException(msg % msg_args) 

915 

916 volume_infos = result['records'][0] 

917 aggregates = volume_infos.get('aggregates', []) 

918 

919 if len(aggregates) == 0: 

920 aggregate = '' 

921 aggregate_list = [] 

922 else: 

923 aggregate = aggregates[0]['name'] 

924 aggregate_list = [aggr['name'] for aggr in aggregates] 

925 

926 volume = { 

927 'aggregate': aggregate, 

928 'aggr-list': aggregate_list, 

929 'junction-path': volume_infos.get('nas', {}).get('path'), 

930 'name': volume_infos.get('name'), 

931 'owning-vserver-name': volume_infos.get('svm', {}).get('name'), 

932 'type': volume_infos.get('type'), 

933 'style': volume_infos.get('style'), 

934 'size': volume_infos.get('space', {}).get('size'), 

935 'size-used': volume_infos.get('space', {}).get('used'), 

936 'qos-policy-group-name': ( 

937 volume_infos.get('qos', {}).get('policy', {}).get('name')), 

938 'style-extended': volume_infos.get('style'), 

939 'snaplock-type': volume_infos.get('snaplock', {}).get('type'), 

940 } 

941 return volume 

942 

943 @na_utils.trace 

944 def cifs_share_exists(self, share_name): 

945 """Check that a CIFS share already exists.""" 

946 share_path = f'/{share_name}' 

947 query = { 

948 'name': share_name, 

949 'path': share_path, 

950 } 

951 result = self.send_request('/protocols/cifs/shares', 'get', 

952 query=query) 

953 return self._has_records(result) 

954 

955 @na_utils.trace 

956 def create_cifs_share(self, share_name, path): 

957 """Create a CIFS share.""" 

958 body = { 

959 'name': share_name, 

960 'path': path, 

961 'svm.name': self.vserver, 

962 } 

963 self.send_request('/protocols/cifs/shares', 'post', body=body) 

964 

965 @na_utils.trace 

966 def set_volume_security_style(self, volume_name, security_style='unix'): 

967 """Set volume security style""" 

968 

969 query = { 

970 'name': volume_name, 

971 } 

972 body = { 

973 'nas.security_style': security_style 

974 } 

975 self.send_request('/storage/volumes', 'patch', body=body, query=query) 

976 

977 @na_utils.trace 

978 def remove_cifs_share_access(self, share_name, user_name): 

979 """Remove CIFS share access.""" 

980 query = { 

981 'name': share_name, 

982 'fields': 'svm.uuid' 

983 } 

984 get_uuid = self.send_request('/protocols/cifs/shares', 'get', 

985 query=query) 

986 svm_uuid = get_uuid['records'][0]['svm']['uuid'] 

987 

988 self.send_request( 

989 f'/protocols/cifs/shares/{svm_uuid}/{share_name}' 

990 f'/acls/{user_name}/{CIFS_USER_GROUP_TYPE}', 'delete') 

991 

992 # TODO(caique): when ZAPI is dropped, this method should be removed and 

993 # the callers should start calling directly the "create_volume_async" 

994 @na_utils.trace 

995 def create_volume(self, aggregate_name, volume_name, size_gb, 

996 thin_provisioned=False, snapshot_policy=None, 

997 language=None, dedup_enabled=False, 

998 compression_enabled=False, max_files=None, 

999 snapshot_reserve=None, volume_type='rw', 

1000 qos_policy_group=None, adaptive_qos_policy_group=None, 

1001 encrypt=False, mount_point_name=None, 

1002 snaplock_type=None, **options): 

1003 """Creates a FlexVol volume synchronously.""" 

1004 

1005 # NOTE(nahimsouza): In REST API, both FlexVol and FlexGroup volumes are 

1006 # created asynchronously. However, we kept the synchronous process for 

1007 # FlexVols to replicate the behavior from ZAPI and avoid changes in the 

1008 # layers above. 

1009 self.create_volume_async( 

1010 [aggregate_name], volume_name, size_gb, is_flexgroup=False, 

1011 thin_provisioned=thin_provisioned, snapshot_policy=snapshot_policy, 

1012 language=language, max_files=max_files, 

1013 snapshot_reserve=snapshot_reserve, volume_type=volume_type, 

1014 qos_policy_group=qos_policy_group, encrypt=encrypt, 

1015 adaptive_qos_policy_group=adaptive_qos_policy_group, 

1016 mount_point_name=mount_point_name, snaplock_type=snaplock_type, 

1017 **options) 

1018 efficiency_policy = options.get('efficiency_policy', None) 

1019 self.update_volume_efficiency_attributes( 

1020 volume_name, dedup_enabled, compression_enabled, 

1021 efficiency_policy=efficiency_policy 

1022 ) 

1023 

1024 if max_files is not None: 1024 ↛ 1027line 1024 didn't jump to line 1027 because the condition on line 1024 was always true

1025 self.set_volume_max_files(volume_name, max_files) 

1026 

1027 if snaplock_type is not None: 1027 ↛ exitline 1027 didn't return from function 'create_volume' because the condition on line 1027 was always true

1028 self.set_snaplock_attributes(volume_name, **options) 

1029 

1030 @na_utils.trace 

1031 def create_volume_async(self, aggregate_list, volume_name, size_gb, 

1032 is_flexgroup=False, thin_provisioned=False, 

1033 snapshot_policy=None, 

1034 language=None, snapshot_reserve=None, 

1035 volume_type='rw', qos_policy_group=None, 

1036 encrypt=False, adaptive_qos_policy_group=None, 

1037 auto_provisioned=False, mount_point_name=None, 

1038 snaplock_type=None, **options): 

1039 """Creates FlexGroup/FlexVol volumes. 

1040 

1041 If the parameter `is_flexgroup` is False, the creation process is 

1042 made synchronously to replicate ZAPI behavior for FlexVol creation. 

1043 

1044 """ 

1045 

1046 body = { 

1047 'size': size_gb * units.Gi, 

1048 'name': volume_name, 

1049 } 

1050 

1051 body['style'] = 'flexgroup' if is_flexgroup else 'flexvol' 

1052 

1053 if aggregate_list and not auto_provisioned: 1053 ↛ 1056line 1053 didn't jump to line 1056 because the condition on line 1053 was always true

1054 body['aggregates'] = [{'name': aggr} for aggr in aggregate_list] 

1055 

1056 body.update(self._get_create_volume_body( 

1057 volume_name, thin_provisioned, snapshot_policy, language, 

1058 snapshot_reserve, volume_type, qos_policy_group, encrypt, 

1059 adaptive_qos_policy_group, mount_point_name, snaplock_type)) 

1060 

1061 # NOTE(nahimsouza): When a volume is not a FlexGroup, volume creation 

1062 # is made synchronously to replicate old ZAPI behavior. When ZAPI is 

1063 # deprecated, this can be changed to be made asynchronously. 

1064 wait_on_accepted = (not is_flexgroup) 

1065 result = self.send_request('/storage/volumes', 'post', body=body, 

1066 wait_on_accepted=wait_on_accepted) 

1067 

1068 job_info = { 

1069 'jobid': result.get('job', {}).get('uuid', {}), 

1070 # NOTE(caiquemello): remove error-code and error-message 

1071 # when zapi is dropped. 

1072 'error-code': '', 

1073 'error-message': '' 

1074 } 

1075 return job_info 

1076 

1077 @na_utils.trace 

1078 def _get_create_volume_body(self, volume_name, thin_provisioned, 

1079 snapshot_policy, language, snapshot_reserve, 

1080 volume_type, qos_policy_group, encrypt, 

1081 adaptive_qos_policy_group, 

1082 mount_point_name, snaplock_type): 

1083 """Builds the body to volume creation request.""" 

1084 

1085 body = { 

1086 'type': volume_type, 

1087 'guarantee.type': ('none' if thin_provisioned else 'volume'), 

1088 'svm.name': self.connection.get_vserver() 

1089 } 

1090 if volume_type != 'dp': 1090 ↛ 1093line 1090 didn't jump to line 1093 because the condition on line 1090 was always true

1091 mount_point_name = mount_point_name or volume_name 

1092 body['nas.path'] = f'/{mount_point_name}' 

1093 if snapshot_policy is not None: 1093 ↛ 1095line 1093 didn't jump to line 1095 because the condition on line 1093 was always true

1094 body['snapshot_policy.name'] = snapshot_policy 

1095 if language is not None: 1095 ↛ 1097line 1095 didn't jump to line 1097 because the condition on line 1095 was always true

1096 body['language'] = language 

1097 if snapshot_reserve is not None: 1097 ↛ 1099line 1097 didn't jump to line 1099 because the condition on line 1097 was always true

1098 body['space.snapshot.reserve_percent'] = str(snapshot_reserve) 

1099 if qos_policy_group is not None: 1099 ↛ 1101line 1099 didn't jump to line 1101 because the condition on line 1099 was always true

1100 body['qos.policy.name'] = qos_policy_group 

1101 if adaptive_qos_policy_group is not None: 1101 ↛ 1104line 1101 didn't jump to line 1104 because the condition on line 1101 was always true

1102 body['qos.policy.name'] = adaptive_qos_policy_group 

1103 

1104 if encrypt is True: 1104 ↛ 1111line 1104 didn't jump to line 1111 because the condition on line 1104 was always true

1105 if not self.features.FLEXVOL_ENCRYPTION: 1105 ↛ 1106line 1105 didn't jump to line 1106 because the condition on line 1105 was never true

1106 msg = 'Flexvol encryption is not supported on this backend.' 

1107 raise exception.NetAppException(msg) 

1108 else: 

1109 body['encryption.enabled'] = 'true' 

1110 else: 

1111 body['encryption.enabled'] = 'false' 

1112 

1113 if snaplock_type is not None: 1113 ↛ 1116line 1113 didn't jump to line 1116 because the condition on line 1113 was always true

1114 body['snaplock.type'] = snaplock_type 

1115 

1116 return body 

1117 

1118 @na_utils.trace 

1119 def get_job_state(self, job_id): 

1120 """Returns job state for a given job id.""" 

1121 query = { 

1122 'uuid': job_id, 

1123 'fields': 'state' 

1124 } 

1125 

1126 result = self.send_request('/cluster/jobs/', 'get', query=query, 

1127 enable_tunneling=False) 

1128 

1129 job_info = result.get('records', []) 

1130 

1131 if not self._has_records(result): 

1132 msg = _('Could not find job with ID %(id)s.') 

1133 msg_args = {'id': job_id} 

1134 raise exception.NetAppException(msg % msg_args) 

1135 elif len(job_info) > 1: 

1136 msg = _('Could not find unique job for ID %(id)s.') 

1137 msg_args = {'id': job_id} 

1138 raise exception.NetAppException(msg % msg_args) 

1139 

1140 return job_info[0]['state'] 

1141 

1142 @na_utils.trace 

1143 def get_volume_efficiency_status(self, volume_name): 

1144 """Get dedupe & compression status for a volume.""" 

1145 query = { 

1146 'efficiency.volume_path': f'/vol/{volume_name}', 

1147 'fields': 'efficiency.state,efficiency.compression' 

1148 } 

1149 dedupe = False 

1150 compression = False 

1151 try: 

1152 response = self.send_request('/storage/volumes', 'get', 

1153 query=query) 

1154 if self._has_records(response): 1154 ↛ 1162line 1154 didn't jump to line 1162 because the condition on line 1154 was always true

1155 efficiency = response['records'][0]['efficiency'] 

1156 dedupe = (efficiency['state'] == 'enabled') 

1157 compression = (efficiency['compression'] != 'none') 

1158 except netapp_api.api.NaApiError: 

1159 msg = _('Failed to get volume efficiency status for %s.') 

1160 LOG.error(msg, volume_name) 

1161 

1162 return { 

1163 'dedupe': dedupe, 

1164 'compression': compression, 

1165 } 

1166 

1167 @na_utils.trace 

1168 def update_volume_snapshot_policy(self, volume_name, snapshot_policy): 

1169 """Set snapshot policy for the specified volume.""" 

1170 volume = self._get_volume_by_args(vol_name=volume_name) 

1171 uuid = volume['uuid'] 

1172 

1173 body = { 

1174 'snapshot_policy.name': snapshot_policy 

1175 } 

1176 # update snapshot policy 

1177 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body) 

1178 

1179 @na_utils.trace 

1180 def update_volume_efficiency_attributes(self, volume_name, dedup_enabled, 

1181 compression_enabled, 

1182 is_flexgroup=False, 

1183 efficiency_policy=None): 

1184 """Update dedupe & compression attributes to match desired values.""" 

1185 

1186 efficiency_status = self.get_volume_efficiency_status(volume_name) 

1187 # cDOT compression requires dedup to be enabled 

1188 dedup_enabled = dedup_enabled or compression_enabled 

1189 # enable/disable compression if needed 

1190 if compression_enabled and not efficiency_status['compression']: 

1191 self.enable_compression_async(volume_name) 

1192 elif not compression_enabled and efficiency_status['compression']: 1192 ↛ 1195line 1192 didn't jump to line 1195 because the condition on line 1192 was always true

1193 self.disable_compression_async(volume_name) 

1194 # enable/disable dedup if needed 

1195 if dedup_enabled and not efficiency_status['dedupe']: 

1196 self.enable_dedupe_async(volume_name) 

1197 elif not dedup_enabled and efficiency_status['dedupe']: 1197 ↛ 1200line 1197 didn't jump to line 1200 because the condition on line 1197 was always true

1198 self.disable_dedupe_async(volume_name) 

1199 

1200 self.apply_volume_efficiency_policy( 

1201 volume_name, efficiency_policy=efficiency_policy) 

1202 

1203 @na_utils.trace 

1204 def enable_dedupe_async(self, volume_name): 

1205 """Enable deduplication on FlexVol/FlexGroup volume asynchronously.""" 

1206 

1207 volume = self._get_volume_by_args(vol_name=volume_name) 

1208 uuid = volume['uuid'] 

1209 

1210 body = { 

1211 'efficiency': {'dedupe': 'background'} 

1212 } 

1213 # update volume efficiency 

1214 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body) 

1215 

1216 @na_utils.trace 

1217 def disable_dedupe_async(self, volume_name): 

1218 """Disable deduplication on FlexVol/FlexGroup volume asynchronously.""" 

1219 

1220 volume = self._get_volume_by_args(vol_name=volume_name) 

1221 uuid = volume['uuid'] 

1222 

1223 body = { 

1224 'efficiency': {'dedupe': 'none'} 

1225 } 

1226 # update volume efficiency 

1227 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body) 

1228 

1229 @na_utils.trace 

1230 def enable_compression_async(self, volume_name): 

1231 """Enable compression on FlexVol/FlexGroup volume asynchronously.""" 

1232 volume = self._get_volume_by_args(vol_name=volume_name) 

1233 uuid = volume['uuid'] 

1234 

1235 body = { 

1236 'efficiency': {'compression': 'background'} 

1237 } 

1238 # update volume efficiency 

1239 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body) 

1240 

1241 @na_utils.trace 

1242 def disable_compression_async(self, volume_name): 

1243 """Disable compression on FlexVol/FlexGroup volume asynchronously.""" 

1244 

1245 volume = self._get_volume_by_args(vol_name=volume_name) 

1246 uuid = volume['uuid'] 

1247 

1248 body = { 

1249 'efficiency': {'compression': 'none'} 

1250 } 

1251 # update volume efficiency 

1252 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body) 

1253 

1254 @na_utils.trace 

1255 def apply_volume_efficiency_policy(self, volume_name, 

1256 efficiency_policy=None): 

1257 if efficiency_policy: 

1258 """Apply volume efficiency policy to FlexVol""" 

1259 volume = self._get_volume_by_args(vol_name=volume_name) 

1260 uuid = volume['uuid'] 

1261 

1262 body = { 

1263 'efficiency': {'policy': efficiency_policy} 

1264 } 

1265 

1266 # update volume efficiency policy only if policy_name is provided 

1267 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body) 

1268 

1269 @na_utils.trace 

1270 def set_volume_max_files(self, volume_name, max_files, 

1271 retry_allocated=False): 

1272 """Set share file limit.""" 

1273 

1274 try: 

1275 volume = self._get_volume_by_args(vol_name=volume_name) 

1276 uuid = volume['uuid'] 

1277 

1278 body = { 

1279 'files.maximum': int(max_files) 

1280 } 

1281 

1282 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body) 

1283 except netapp_api.api.NaApiError as e: 

1284 if e.code != netapp_api.EREST_CANNOT_MODITY_SPECIFIED_FIELD: 1284 ↛ 1285line 1284 didn't jump to line 1285 because the condition on line 1284 was never true

1285 return 

1286 if retry_allocated: 1286 ↛ 1303line 1286 didn't jump to line 1303 because the condition on line 1286 was always true

1287 alloc_files = self.get_volume_allocated_files(volume_name) 

1288 new_max_files = alloc_files['used'] 

1289 # no need to act if current max files are set to 

1290 # allocated files 

1291 if new_max_files == alloc_files['maximum']: 1291 ↛ 1292line 1291 didn't jump to line 1292 because the condition on line 1291 was never true

1292 return 

1293 msg = _('Set higher max files %(new_max_files)s ' 

1294 'on %(vol)s. The current allocated inodes ' 

1295 'are larger than requested %(max_files)s.') 

1296 msg_args = {'vol': volume_name, 

1297 'max_files': max_files, 

1298 'new_max_files': new_max_files} 

1299 LOG.info(msg, msg_args) 

1300 self.set_volume_max_files(volume_name, new_max_files, 

1301 retry_allocated=False) 

1302 else: 

1303 raise exception.NetAppException(message=e.message) 

1304 

1305 @na_utils.trace 

1306 def get_volume_allocated_files(self, volume_name): 

1307 """Get share allocated files.""" 

1308 

1309 try: 

1310 volume = self._get_volume_by_args(vol_name=volume_name) 

1311 uuid = volume['uuid'] 

1312 

1313 query = { 

1314 'fields': 'files.maximum,files.used' 

1315 } 

1316 response = self.send_request(f'/storage/volumes/{uuid}', 'get', 

1317 query=query) 

1318 if self._has_records(response): 

1319 return response['records'][0]['files'] 

1320 except netapp_api.api.NaApiError: 

1321 msg = _('Failed to get volume allocated files for %s.') 

1322 LOG.error(msg, volume_name) 

1323 return {'maximum': 0, 'used': 0} 

1324 

1325 @na_utils.trace 

1326 def set_volume_snapdir_access(self, volume_name, hide_snapdir): 

1327 """Set volume snapshot directory visibility.""" 

1328 

1329 try: 

1330 volume = self._get_volume_by_args(vol_name=volume_name) 

1331 except exception.NetAppException: 

1332 msg = _('Could not find volume %s to set snapdir access') 

1333 LOG.error(msg, volume_name) 

1334 raise exception.SnapshotResourceNotFound(name=volume_name) 

1335 

1336 uuid = volume['uuid'] 

1337 

1338 body = { 

1339 'snapshot_directory_access_enabled': str(not hide_snapdir).lower() 

1340 } 

1341 

1342 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body) 

1343 

1344 @na_utils.trace 

1345 def get_fpolicy_scopes(self, share_name, policy_name=None, 

1346 extensions_to_include=None, 

1347 extensions_to_exclude=None, shares_to_include=None): 

1348 """Retrieve fpolicy scopes. 

1349 

1350 :param policy_name: name of the policy associated with a scope. 

1351 :param share_name: name of the share associated with the fpolicy scope. 

1352 :param extensions_to_include: file extensions included for screening. 

1353 Values should be provided as comma separated list 

1354 :param extensions_to_exclude: file extensions excluded for screening. 

1355 Values should be provided as comma separated list 

1356 :param shares_to_include: list of shares to include for file access 

1357 monitoring. 

1358 :return: list of fpolicy scopes or empty list 

1359 """ 

1360 try: 

1361 volume = self._get_volume_by_args(vol_name=share_name) 

1362 svm_uuid = volume['svm']['uuid'] 

1363 

1364 except exception.NetAppException: 

1365 LOG.debug('Could not find fpolicy. Share not found: %s.', 

1366 share_name) 

1367 return [] 

1368 

1369 query = {} 

1370 if policy_name: 1370 ↛ 1373line 1370 didn't jump to line 1373 because the condition on line 1370 was always true

1371 query['name'] = policy_name 

1372 

1373 if shares_to_include: 1373 ↛ 1376line 1373 didn't jump to line 1376 because the condition on line 1373 was always true

1374 query['scope.include_shares'] = ','.join( 

1375 [str(share) for share in shares_to_include]) 

1376 if extensions_to_include: 1376 ↛ 1379line 1376 didn't jump to line 1379 because the condition on line 1376 was always true

1377 query['scope.include_extension'] = ','.join( 

1378 [str(ext_include) for ext_include in extensions_to_include]) 

1379 if extensions_to_exclude: 1379 ↛ 1383line 1379 didn't jump to line 1383 because the condition on line 1379 was always true

1380 query['scope.exclude_extension'] = ','.join( 

1381 [str(ext_exclude) for ext_exclude in extensions_to_exclude]) 

1382 

1383 result = self.send_request( 

1384 f'/protocols/fpolicy/{svm_uuid}/policies', 'get', query=query) 

1385 

1386 fpolicy_scopes = [] 

1387 if self._has_records(result): 1387 ↛ 1403line 1387 didn't jump to line 1403 because the condition on line 1387 was always true

1388 for fpolicy_scope_result in result['records']: 

1389 name = fpolicy_scope_result['name'] 

1390 policy_scope = fpolicy_scope_result.get('scope') 

1391 if policy_scope: 1391 ↛ 1388line 1391 didn't jump to line 1388 because the condition on line 1391 was always true

1392 ext_include = policy_scope.get('include_extension', []) 

1393 ext_exclude = policy_scope.get('exclude_extension', []) 

1394 shares_include = policy_scope.get('include_shares', []) 

1395 

1396 fpolicy_scopes.append({ 

1397 'policy-name': name, 

1398 'file-extensions-to-include': ext_include, 

1399 'file-extensions-to-exclude': ext_exclude, 

1400 'shares-to-include': shares_include, 

1401 }) 

1402 

1403 return fpolicy_scopes 

1404 

1405 @na_utils.trace 

1406 def get_fpolicy_policies_status(self, share_name, policy_name=None, 

1407 status='true'): 

1408 """Get fpolicy polices status currently configured in the vserver·""" 

1409 volume = self._get_volume_by_args(vol_name=share_name) 

1410 svm_uuid = volume['svm']['uuid'] 

1411 query = {} 

1412 if policy_name: 1412 ↛ 1416line 1412 didn't jump to line 1416 because the condition on line 1412 was always true

1413 query['name'] = policy_name 

1414 query['enabled'] = status 

1415 

1416 result = self.send_request( 

1417 f'/protocols/fpolicy/{svm_uuid}/policies', 'get', query=query) 

1418 

1419 fpolicy_status = [] 

1420 if self._has_records(result): 1420 ↛ 1431line 1420 didn't jump to line 1431 because the condition on line 1420 was always true

1421 for fpolicy_status_result in result['records']: 

1422 name = fpolicy_status_result['name'] 

1423 status = fpolicy_status_result.get('enabled', '') 

1424 seq = fpolicy_status_result.get('priority', '') 

1425 fpolicy_status.append({ 

1426 'policy-name': name, 

1427 'status': strutils.bool_from_string(status), 

1428 'sequence-number': int(seq) 

1429 }) 

1430 

1431 return fpolicy_status 

1432 

1433 @na_utils.trace 

1434 def get_fpolicy_policies(self, share_name, policy_name=None, 

1435 engine_name='native', event_names=[]): 

1436 """Retrieve one or more fpolicy policies. 

1437 

1438 :param policy_name: name of the policy to be retrieved 

1439 :param engine_name: name of the engine 

1440 :param share_name: name of the share associated with the fpolicy 

1441 policy. 

1442 :param event_names: list of event names that must be associated to the 

1443 fpolicy policy 

1444 :return: list of fpolicy policies or empty list 

1445 """ 

1446 volume = self._get_volume_by_args(vol_name=share_name) 

1447 svm_uuid = volume['svm']['uuid'] 

1448 query = {} 

1449 

1450 if policy_name: 1450 ↛ 1452line 1450 didn't jump to line 1452 because the condition on line 1450 was always true

1451 query['name'] = policy_name 

1452 if engine_name: 1452 ↛ 1454line 1452 didn't jump to line 1454 because the condition on line 1452 was always true

1453 query['engine.name'] = engine_name 

1454 if event_names: 1454 ↛ 1458line 1454 didn't jump to line 1458 because the condition on line 1454 was always true

1455 query['events'] = ','.join( 

1456 [str(events) for events in event_names]) 

1457 

1458 result = self.send_request( 

1459 f'/protocols/fpolicy/{svm_uuid}/policies', 'get', query=query) 

1460 

1461 fpolicy_policies = [] 

1462 if self._has_records(result): 1462 ↛ 1475line 1462 didn't jump to line 1475 because the condition on line 1462 was always true

1463 for fpolicy_policies_result in result['records']: 

1464 name = fpolicy_policies_result['name'] 

1465 engine = (fpolicy_policies_result.get( 

1466 'engine', {}).get('name', '')) 

1467 events = ([event['name'] for event in 

1468 fpolicy_policies_result.get('events', [])]) 

1469 fpolicy_policies.append({ 

1470 'policy-name': name, 

1471 'engine-name': engine, 

1472 'events': events 

1473 }) 

1474 

1475 return fpolicy_policies 

1476 

1477 @na_utils.trace 

1478 def get_fpolicy_events(self, share_name, event_name=None, protocol=None, 

1479 file_operations=None): 

1480 """Retrives a list of fpolicy events. 

1481 

1482 :param event_name: name of the fpolicy event 

1483 :param protocol: name of protocol. Possible values are: 'nfsv3', 

1484 'nfsv4' or 'cifs'. 

1485 :param file_operations: name of file operations to be monitored. Values 

1486 should be provided as list of strings. 

1487 :returns List of policy events or empty list 

1488 """ 

1489 volume = self._get_volume_by_args(vol_name=share_name) 

1490 svm_uuid = volume['svm']['uuid'] 

1491 query = {} 

1492 if event_name: 1492 ↛ 1494line 1492 didn't jump to line 1494 because the condition on line 1492 was always true

1493 query['name'] = event_name 

1494 if protocol: 1494 ↛ 1496line 1494 didn't jump to line 1496 because the condition on line 1494 was always true

1495 query['protocol'] = protocol 

1496 if file_operations: 1496 ↛ 1500line 1496 didn't jump to line 1500 because the condition on line 1496 was always true

1497 query['fields'] = (','.join([str(f'file_operations.{file_op}') 

1498 for file_op in file_operations])) 

1499 

1500 result = self.send_request( 

1501 f'/protocols/fpolicy/{svm_uuid}/events', 'get', query=query) 

1502 

1503 fpolicy_events = [] 

1504 if self._has_records(result): 1504 ↛ 1521line 1504 didn't jump to line 1521 because the condition on line 1504 was always true

1505 for fpolicy_events_result in result['records']: 

1506 name = fpolicy_events_result['name'] 

1507 proto = fpolicy_events_result.get('protocol', '') 

1508 

1509 file_operations = [] 

1510 operations = fpolicy_events_result.get('file_operations', {}) 

1511 for key, value in operations.items(): 

1512 if value: 1512 ↛ 1511line 1512 didn't jump to line 1511 because the condition on line 1512 was always true

1513 file_operations.append(key) 

1514 

1515 fpolicy_events.append({ 

1516 'event-name': name, 

1517 'protocol': proto, 

1518 'file-operations': file_operations 

1519 }) 

1520 

1521 return fpolicy_events 

1522 

1523 @na_utils.trace 

1524 def create_fpolicy_event(self, share_name, event_name, protocol, 

1525 file_operations): 

1526 """Creates a new fpolicy policy event. 

1527 

1528 :param event_name: name of the new fpolicy event 

1529 :param protocol: name of protocol for which event is created. Possible 

1530 values are: 'nfsv3', 'nfsv4' or 'cifs'. 

1531 :param file_operations: name of file operations to be monitored. Values 

1532 should be provided as list of strings. 

1533 :param share_name: name of share associated with the vserver where the 

1534 fpolicy event should be added. 

1535 """ 

1536 volume = self._get_volume_by_args(vol_name=share_name) 

1537 svm_uuid = volume['svm']['uuid'] 

1538 body = { 

1539 'name': event_name, 

1540 'protocol': protocol, 

1541 } 

1542 for file_op in file_operations: 

1543 body[f'file_operations.{file_op}'] = 'true' 

1544 

1545 self.send_request(f'/protocols/fpolicy/{svm_uuid}/events', 'post', 

1546 body=body) 

1547 

1548 @na_utils.trace 

1549 def delete_fpolicy_event(self, share_name, event_name): 

1550 """Deletes a fpolicy policy event. 

1551 

1552 :param event_name: name of the event to be deleted 

1553 :param share_name: name of share associated with the vserver where the 

1554 fpolicy event should be deleted. 

1555 """ 

1556 try: 

1557 volume = self._get_volume_by_args(vol_name=share_name) 

1558 svm_uuid = volume['svm']['uuid'] 

1559 except exception.NetAppException: 

1560 msg = _("FPolicy event %s not found.") 

1561 LOG.debug(msg, event_name) 

1562 return 

1563 try: 

1564 self.send_request( 

1565 f'/protocols/fpolicy/{svm_uuid}/events/{event_name}', 'delete') 

1566 except netapp_api.api.NaApiError as e: 

1567 if e.code == netapp_api.EREST_ENTRY_NOT_FOUND: 

1568 msg = _("FPolicy event %s not found.") 

1569 LOG.debug(msg, event_name) 

1570 else: 

1571 raise exception.NetAppException(message=e.message) 

1572 

1573 @na_utils.trace 

1574 def delete_fpolicy_policy(self, share_name, policy_name): 

1575 """Deletes a fpolicy policy. 

1576 

1577 :param policy_name: name of the policy to be deleted. 

1578 """ 

1579 try: 

1580 volume = self._get_volume_by_args(vol_name=share_name) 

1581 svm_uuid = volume['svm']['uuid'] 

1582 except exception.NetAppException: 

1583 msg = _("FPolicy policy %s not found.") 

1584 LOG.debug(msg, policy_name) 

1585 return 

1586 try: 

1587 self.send_request( 

1588 f'/protocols/fpolicy/{svm_uuid}/policies/{policy_name}', 

1589 'delete') 

1590 except netapp_api.api.NaApiError as e: 

1591 if e.code == netapp_api.EREST_ENTRY_NOT_FOUND: 

1592 msg = _("FPolicy policy %s not found.") 

1593 LOG.debug(msg, policy_name) 

1594 else: 

1595 raise exception.NetAppException(message=e.message) 

1596 

1597 @na_utils.trace 

1598 def enable_fpolicy_policy(self, share_name, policy_name, sequence_number): 

1599 """Enables a specific named policy. 

1600 

1601 :param policy_name: name of the policy to be enabled 

1602 :param share_name: name of the share associated with the vserver and 

1603 the fpolicy 

1604 :param sequence_number: policy sequence number 

1605 """ 

1606 volume = self._get_volume_by_args(vol_name=share_name) 

1607 svm_uuid = volume['svm']['uuid'] 

1608 body = { 

1609 'priority': sequence_number, 

1610 } 

1611 

1612 self.send_request( 

1613 f'/protocols/fpolicy/{svm_uuid}/policies/{policy_name}', 'patch', 

1614 body=body) 

1615 

1616 @na_utils.trace 

1617 def modify_fpolicy_scope(self, share_name, policy_name, 

1618 shares_to_include=[], extensions_to_include=None, 

1619 extensions_to_exclude=None): 

1620 """Modify an existing fpolicy scope. 

1621 

1622 :param policy_name: name of the policy associated to the scope. 

1623 :param share_name: name of the share associated with the fpolicy scope. 

1624 :param shares_to_include: list of shares to include for file access 

1625 monitoring. 

1626 :param extensions_to_include: file extensions included for screening. 

1627 Values should be provided as comma separated list 

1628 :param extensions_to_exclude: file extensions excluded for screening. 

1629 Values should be provided as comma separated list 

1630 """ 

1631 volume = self._get_volume_by_args(vol_name=share_name) 

1632 svm_uuid = volume['svm']['uuid'] 

1633 

1634 body = {} 

1635 if policy_name: 1635 ↛ 1638line 1635 didn't jump to line 1638 because the condition on line 1635 was always true

1636 body['name'] = policy_name 

1637 

1638 if shares_to_include: 1638 ↛ 1641line 1638 didn't jump to line 1641 because the condition on line 1638 was always true

1639 body['scope.include_shares'] = ','.join( 

1640 [str(share) for share in shares_to_include]) 

1641 if extensions_to_include: 1641 ↛ 1644line 1641 didn't jump to line 1644 because the condition on line 1641 was always true

1642 body['scope.include_extension'] = ','.join( 

1643 [str(ext_include) for ext_include in extensions_to_include]) 

1644 if extensions_to_exclude: 1644 ↛ 1648line 1644 didn't jump to line 1648 because the condition on line 1644 was always true

1645 body['scope.exclude_extension'] = ','.join( 

1646 [str(ext_exclude) for ext_exclude in extensions_to_exclude]) 

1647 

1648 self.send_request(f'/protocols/fpolicy/{svm_uuid}/policies/', 

1649 'patch', body=body) 

1650 

1651 @na_utils.trace 

1652 def create_fpolicy_policy_with_scope(self, fpolicy_name, share_name, 

1653 events, engine='native', 

1654 extensions_to_include=None, 

1655 extensions_to_exclude=None): 

1656 """Creates a fpolicy policy resource with scopes. 

1657 

1658 :param fpolicy_name: name of the fpolicy policy to be created. 

1659 :param share_name: name of the share to be associated with the new 

1660 scope. 

1661 :param events: list of event names for file access monitoring. 

1662 :param engine: name of the engine to be used. 

1663 :param extensions_to_include: file extensions included for screening. 

1664 Values should be provided as comma separated list 

1665 :param extensions_to_exclude: file extensions excluded for screening. 

1666 Values should be provided as comma separated list 

1667 """ 

1668 volume = self._get_volume_by_args(vol_name=share_name) 

1669 svm_uuid = volume['svm']['uuid'] 

1670 

1671 body = { 

1672 'name': fpolicy_name, 

1673 'events.name': events, 

1674 'engine.name': engine, 

1675 'scope.include_shares': [share_name] 

1676 } 

1677 

1678 if extensions_to_include: 1678 ↛ 1680line 1678 didn't jump to line 1680 because the condition on line 1678 was always true

1679 body['scope.include_extension'] = extensions_to_include.split(',') 

1680 if extensions_to_exclude: 1680 ↛ 1683line 1680 didn't jump to line 1683 because the condition on line 1680 was always true

1681 body['scope.exclude_extension'] = extensions_to_exclude.split(',') 

1682 

1683 self.send_request(f'/protocols/fpolicy/{svm_uuid}/policies', 'post', 

1684 body=body) 

1685 

1686 @na_utils.trace 

1687 def delete_nfs_export_policy(self, policy_name): 

1688 """Delete NFS export policy.""" 

1689 

1690 # Get policy id. 

1691 query = { 

1692 'name': policy_name, 

1693 } 

1694 response = self.send_request('/protocols/nfs/export-policies', 'get', 

1695 query=query) 

1696 if not response.get('records'): 

1697 return 

1698 policy_id = response.get('records')[0]['id'] 

1699 

1700 # Remove policy. 

1701 self.send_request(f'/protocols/nfs/export-policies/{policy_id}', 

1702 'delete') 

1703 

1704 @na_utils.trace 

1705 def remove_cifs_share(self, share_name): 

1706 """Remove CIFS share from the CIFS server.""" 

1707 

1708 # Get SVM UUID. 

1709 query = { 

1710 'name': self.vserver, 

1711 'fields': 'uuid' 

1712 } 

1713 res = self.send_request('/svm/svms', 'get', query=query) 

1714 if not res.get('records'): 

1715 msg = _('Vserver %s not found.') % self.vserver 

1716 raise exception.NetAppException(msg) 

1717 svm_id = res.get('records')[0]['uuid'] 

1718 

1719 # Remove CIFS share. 

1720 try: 

1721 self.send_request(f'/protocols/cifs/shares/{svm_id}/{share_name}', 

1722 'delete') 

1723 except netapp_api.api.NaApiError as e: 

1724 if e.code == netapp_api.EREST_ENTRY_NOT_FOUND: 

1725 return 

1726 raise 

1727 

1728 @na_utils.trace 

1729 def _unmount_volume(self, volume_name): 

1730 """Unmounts a volume.""" 

1731 # Get volume UUID. 

1732 volume = self._get_volume_by_args(vol_name=volume_name) 

1733 uuid = volume['uuid'] 

1734 

1735 # Unmount volume async operation. 

1736 body = {"nas": {"path": ""}} 

1737 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body) 

1738 

1739 @na_utils.trace 

1740 # TODO(felipe_rodrigues): remove the force parameter when ZAPI is dropped. 

1741 def unmount_volume(self, volume_name, force=False, wait_seconds=30): 

1742 """Unmounts a volume, retrying if a clone split is ongoing. 

1743 

1744 NOTE(cknight): While unlikely to happen in normal operation, any client 

1745 that tries to delete volumes immediately after creating volume clones 

1746 is likely to experience failures if cDOT isn't quite ready for the 

1747 delete. The volume unmount is the first operation in the delete 

1748 path that fails in this case, and there is no proactive check we can 

1749 use to reliably predict the failure. And there isn't a specific error 

1750 code from volume-unmount, so we have to check for a generic error code 

1751 plus certain language in the error code. It's ugly, but it works, and 

1752 it's better than hard-coding a fixed delay. 

1753 """ 

1754 

1755 # Do the unmount, handling split-related errors with retries. 

1756 retry_interval = 3 # seconds 

1757 for retry in range(int(wait_seconds / retry_interval)): 

1758 try: 

1759 self._unmount_volume(volume_name) 

1760 LOG.debug('Volume %s unmounted.', volume_name) 

1761 return 

1762 except netapp_api.api.NaApiError as e: 

1763 if (e.code == netapp_api.EREST_UNMOUNT_FAILED_LOCK 

1764 and 'job ID' in e.message): 

1765 msg = ('Could not unmount volume %(volume)s due to ' 

1766 'ongoing volume operation: %(exception)s') 

1767 msg_args = {'volume': volume_name, 'exception': e} 

1768 LOG.warning(msg, msg_args) 

1769 time.sleep(retry_interval) 

1770 continue 

1771 raise 

1772 

1773 msg = _('Failed to unmount volume %(volume)s after ' 

1774 'waiting for %(wait_seconds)s seconds.') 

1775 msg_args = {'volume': volume_name, 'wait_seconds': wait_seconds} 

1776 LOG.error(msg, msg_args) 

1777 raise exception.NetAppException(msg % msg_args) 

1778 

1779 @na_utils.trace 

1780 def get_clones_of_parent_volume(self, vserver, volume): 

1781 """get one or more clones of given parent volume""" 

1782 

1783 query = { 

1784 'clone.parent_svm.name': vserver, 

1785 'clone.parent_volume.name': volume, 

1786 'fields': 'name' 

1787 } 

1788 result = self.get_records('/storage/volumes', query=query) 

1789 clones = [] 

1790 records = result.get('records', []) 

1791 for record in records: 

1792 clones.append(record.get('name')) 

1793 return clones 

1794 

1795 @na_utils.trace 

1796 def online_volume(self, volume_name): 

1797 """Onlines a volume.""" 

1798 # Get volume UUID. 

1799 volume = self._get_volume_by_args(vol_name=volume_name) 

1800 uuid = volume['uuid'] 

1801 

1802 body = {'state': 'online'} 

1803 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body) 

1804 

1805 @na_utils.trace 

1806 def offline_volume(self, volume_name): 

1807 """Offlines a volume.""" 

1808 # Get volume UUID. 

1809 volume = self._get_volume_by_args(vol_name=volume_name) 

1810 uuid = volume['uuid'] 

1811 

1812 body = {'state': 'offline'} 

1813 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body) 

1814 

1815 @na_utils.trace 

1816 def rename_volume(self, volume_name, new_volume_name): 

1817 """Renames a volume.""" 

1818 volume = self._get_volume_by_args(vol_name=volume_name) 

1819 uuid = volume['uuid'] 

1820 

1821 body = {'name': new_volume_name} 

1822 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body) 

1823 msg = _('Soft-deleted/renamed volume %(volume)s to %(new_volume)s.') 

1824 msg_args = {'volume': volume_name, 'new_volume': new_volume_name} 

1825 LOG.debug(msg, msg_args) 

1826 

1827 @na_utils.trace 

1828 def soft_delete_volume(self, volume_name, 

1829 return_errors=False): 

1830 """Soft deletes a volume.""" 

1831 try: 

1832 # Get volume UUID. 

1833 volume = self._get_volume_by_args(vol_name=volume_name) 

1834 uuid = volume['uuid'] 

1835 

1836 # delete volume async operation. 

1837 self.send_request(f'/storage/volumes/{uuid}', 'delete') 

1838 except netapp_api.api.NaApiError as e: 

1839 if e.code == netapp_api.EREST_VOLDEL_NOT_ALLOW_BY_CLONE: 1839 ↛ 1845line 1839 didn't jump to line 1845 because the condition on line 1839 was always true

1840 if return_errors: 

1841 return 'del_not_allow_by_clone' 

1842 LOG.warning('Delete volume %s failed, renaming..', volume_name) 

1843 self.rename_volume(volume_name, DELETED_PREFIX + volume_name) 

1844 else: 

1845 if return_errors: 

1846 return 'error' 

1847 raise exception.NetAppException(message=e.message) 

1848 

1849 @na_utils.trace 

1850 def delete_volume(self, volume_name, return_errors=False): 

1851 """Deletes a volume.""" 

1852 return self.soft_delete_volume( 

1853 volume_name, 

1854 return_errors=return_errors) 

1855 

1856 @na_utils.trace 

1857 def get_deleted_volumes_to_prune(self): 

1858 """Returns a list of deleted volumes to prune.""" 

1859 

1860 query = { 

1861 'name': DELETED_PREFIX + '*', 

1862 'type': 'rw', 

1863 'fields': 'name,state,svm.name' 

1864 } 

1865 try: 

1866 result = self.get_records('/storage/volumes', query=query) 

1867 except netapp_api.api.NaApiError: 

1868 LOG.error("Failed to get deleted volumes to prune") 

1869 return [] 

1870 return result.get('records', []) 

1871 

1872 @na_utils.trace 

1873 def prune_deleted_volumes(self): 

1874 """Prunes deleted volumes.""" 

1875 LOG.debug('Checking for deleted volumes to prune.') 

1876 

1877 records = self.get_deleted_volumes_to_prune() 

1878 for record in records: 

1879 vol_name = record.get('name') 

1880 vol_state = record.get('state') 

1881 vserver = record.get('svm', {}).get('name') 

1882 

1883 client = copy.deepcopy(self) 

1884 client.set_vserver(vserver) 

1885 

1886 clones = self.get_clones_of_parent_volume( 

1887 vserver, vol_name) 

1888 if clones: 

1889 if vol_state == 'offline': 

1890 try: 

1891 client.online_volume(vol_name) 

1892 except Exception: 

1893 LOG.error("Volume online failed for " 

1894 "volume %s", vol_name) 

1895 

1896 for clone in clones: 

1897 try: 

1898 client.online_volume(clone) 

1899 except Exception: 

1900 LOG.error("Volume online failed for " 

1901 "volume %s", clone) 

1902 elif vol_state == 'online': 1902 ↛ 1878line 1902 didn't jump to line 1878 because the condition on line 1902 was always true

1903 for clone in clones: 

1904 try: 

1905 if client.volume_clone_split_status( 1905 ↛ 1903line 1905 didn't jump to line 1903 because the condition on line 1905 was always true

1906 clone) == ( 

1907 na_utils.CLONE_SPLIT_STATUS_UNKNOWN): 

1908 client.volume_clone_split_start(clone) 

1909 LOG.debug('Starting clone split for ' 

1910 'volume %s ', vol_name) 

1911 except Exception: 

1912 LOG.error("Volume clone split failed for " 

1913 "volume %s", clone) 

1914 else: 

1915 ret = client.delete_volume(vol_name, return_errors=True) 

1916 if ret in ('del_not_allow_by_clone', 'error'): 1916 ↛ 1917line 1916 didn't jump to line 1917 because the condition on line 1916 was never true

1917 LOG.error('Pruning soft-deleted ' 

1918 'volume %s failed', vol_name) 

1919 

1920 @na_utils.trace 

1921 def qos_policy_group_get(self, qos_policy_group_name): 

1922 """Checks if a QoS policy group exists.""" 

1923 

1924 query = { 

1925 'name': qos_policy_group_name, 

1926 'fields': 'name,object_count,fixed.max_throughput_iops,' 

1927 'fixed.max_throughput_mbps,svm.name,' 

1928 'fixed.min_throughput_iops,fixed.min_throughput_mbps', 

1929 } 

1930 try: 

1931 res = self.send_request('/storage/qos/policies', 'get', 

1932 query=query) 

1933 except netapp_api.api.NaApiError as e: 

1934 if e.code == netapp_api.EREST_NOT_AUTHORIZED: 1934 ↛ 1940line 1934 didn't jump to line 1940 because the condition on line 1934 was always true

1935 msg = _("Configured ONTAP login user cannot retrieve " 

1936 "QoS policies.") 

1937 LOG.error(msg) 

1938 raise exception.NetAppException(msg) 

1939 else: 

1940 raise 

1941 

1942 if not res.get('records'): 

1943 msg = _('QoS %s not found.') % qos_policy_group_name 

1944 raise exception.NetAppException(msg) 

1945 

1946 qos_policy_group_info = res.get('records')[0] 

1947 policy_info = { 

1948 'policy-group': qos_policy_group_info.get('name'), 

1949 'vserver': qos_policy_group_info.get('svm', {}).get('name'), 

1950 'num-workloads': int(qos_policy_group_info.get('object_count')), 

1951 } 

1952 

1953 max_iops = qos_policy_group_info.get('fixed', {}).get( 

1954 'max_throughput_iops') 

1955 max_mbps = qos_policy_group_info.get('fixed', {}).get( 

1956 'max_throughput_mbps') 

1957 

1958 if max_iops: 1958 ↛ 1959line 1958 didn't jump to line 1959 because the condition on line 1958 was never true

1959 policy_info['max-throughput'] = f'{max_iops}iops' 

1960 elif max_mbps: 1960 ↛ 1961line 1960 didn't jump to line 1961 because the condition on line 1960 was never true

1961 policy_info['max-throughput'] = f'{max_mbps * 1024 * 1024}b/s' 

1962 else: 

1963 policy_info['max-throughput'] = None 

1964 

1965 min_iops = qos_policy_group_info.get('fixed', {}).get( 

1966 'min_throughput_iops') 

1967 min_mbps = qos_policy_group_info.get('fixed', {}).get( 

1968 'min_throughput_mbps') 

1969 

1970 if min_iops: 1970 ↛ 1971line 1970 didn't jump to line 1971 because the condition on line 1970 was never true

1971 policy_info['min-throughput'] = f'{min_iops}iops' 

1972 elif min_mbps: 1972 ↛ 1973line 1972 didn't jump to line 1973 because the condition on line 1972 was never true

1973 policy_info['min-throughput'] = f'{min_mbps * 1024 * 1024}b/s' 

1974 else: 

1975 policy_info['min-throughput'] = None 

1976 

1977 return policy_info 

1978 

1979 @na_utils.trace 

1980 def qos_policy_group_exists(self, qos_policy_group_name): 

1981 """Checks if a QoS policy group exists.""" 

1982 try: 

1983 self.qos_policy_group_get(qos_policy_group_name) 

1984 except exception.NetAppException: 

1985 return False 

1986 return True 

1987 

1988 @na_utils.trace 

1989 def qos_policy_group_rename(self, qos_policy_group_name, new_name): 

1990 """Renames a QoS policy group.""" 

1991 if qos_policy_group_name == new_name: 

1992 return 

1993 # Get QoS UUID. 

1994 query = { 

1995 'name': qos_policy_group_name, 

1996 'fields': 'uuid', 

1997 } 

1998 res = self.send_request('/storage/qos/policies', 'get', query=query) 

1999 if not res.get('records'): 

2000 msg = _('QoS %s not found.') % qos_policy_group_name 

2001 raise exception.NetAppException(msg) 

2002 uuid = res.get('records')[0]['uuid'] 

2003 

2004 body = {"name": new_name} 

2005 self.send_request(f'/storage/qos/policies/{uuid}', 'patch', 

2006 body=body) 

2007 

2008 @na_utils.trace 

2009 def remove_unused_qos_policy_groups(self): 

2010 """Deletes all QoS policy groups that are marked for deletion.""" 

2011 # Get QoS policies. 

2012 query = { 

2013 'name': '%s*' % DELETED_PREFIX, 

2014 'fields': 'uuid,name', 

2015 } 

2016 res = self.send_request('/storage/qos/policies', 'get', query=query) 

2017 for qos in res.get('records'): 

2018 uuid = qos['uuid'] 

2019 try: 

2020 self.send_request(f'/storage/qos/policies/{uuid}', 'delete') 

2021 except netapp_api.api.NaApiError as ex: 

2022 msg = ('Could not delete QoS policy group %(qos_name)s. ' 

2023 'Details: %(ex)s') 

2024 msg_args = {'qos_name': qos['name'], 'ex': ex} 

2025 LOG.debug(msg, msg_args) 

2026 

2027 @na_utils.trace 

2028 def mark_qos_policy_group_for_deletion(self, qos_policy_group_name): 

2029 """Soft delete backing QoS policy group for a manila share.""" 

2030 # NOTE(gouthamr): ONTAP deletes storage objects asynchronously. As 

2031 # long as garbage collection hasn't occurred, assigned QoS policy may 

2032 # still be tagged "in use". So, we rename the QoS policy group using a 

2033 # specific pattern and later attempt on a best effort basis to 

2034 # delete any QoS policy groups matching that pattern. 

2035 

2036 if self.qos_policy_group_exists(qos_policy_group_name): 

2037 new_name = DELETED_PREFIX + qos_policy_group_name 

2038 try: 

2039 self.qos_policy_group_rename(qos_policy_group_name, new_name) 

2040 except netapp_api.api.NaApiError as ex: 

2041 msg = ('Rename failure in cleanup of cDOT QoS policy ' 

2042 'group %(name)s: %(ex)s') 

2043 msg_args = {'name': qos_policy_group_name, 'ex': ex} 

2044 LOG.warning(msg, msg_args) 

2045 # Attempt to delete any QoS policies named "deleted_manila-*". 

2046 self.remove_unused_qos_policy_groups() 

2047 

2048 @na_utils.trace 

2049 def qos_policy_group_modify(self, qos_policy_group_name, max_throughput): 

2050 """Modifies a QoS policy group.""" 

2051 

2052 query = { 

2053 'name': qos_policy_group_name, 

2054 } 

2055 body = {} 

2056 value = max_throughput.lower() 

2057 if 'iops' in value: 

2058 value = value.replace('iops', '') 

2059 value = int(value) 

2060 body['fixed.max_throughput_iops'] = value 

2061 body['fixed.max_throughput_mbps'] = 0 

2062 elif 'b/s' in value: 2062 ↛ 2068line 2062 didn't jump to line 2068 because the condition on line 2062 was always true

2063 value = value.replace('b/s', '') 

2064 value = int(value) 

2065 body['fixed.max_throughput_mbps'] = math.ceil(value / 

2066 units.Mi) 

2067 body['fixed.max_throughput_iops'] = 0 

2068 res = self.send_request('/storage/qos/policies', 'get', query=query) 

2069 if not res.get('records'): 

2070 msg = ('QoS %s not found.') % qos_policy_group_name 

2071 raise exception.NetAppException(msg) 

2072 uuid = res.get('records')[0]['uuid'] 

2073 self.send_request(f'/storage/qos/policies/{uuid}', 'patch', 

2074 body=body) 

2075 

2076 @na_utils.trace 

2077 def set_volume_size(self, volume_name, size_gb): 

2078 """Set volume size.""" 

2079 

2080 volume = self._get_volume_by_args(vol_name=volume_name) 

2081 uuid = volume['uuid'] 

2082 

2083 body = { 

2084 'space.size': int(size_gb) * units.Gi 

2085 } 

2086 

2087 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body) 

2088 

2089 @na_utils.trace 

2090 def set_volume_filesys_size_fixed(self, 

2091 volume_name, 

2092 filesys_size_fixed=False): 

2093 """Set volume file system size fixed to true/false.""" 

2094 volume = self._get_volume_by_args(vol_name=volume_name) 

2095 uuid = volume['uuid'] 

2096 body = { 

2097 'space.filesystem_size_fixed': filesys_size_fixed 

2098 } 

2099 self.send_request(f'/storage/volumes/{uuid}', 

2100 'patch', body=body) 

2101 

2102 @na_utils.trace 

2103 def create_snapshot(self, volume_name, snapshot_name, 

2104 snapmirror_label=None): 

2105 """Creates a volume snapshot.""" 

2106 

2107 volume = self._get_volume_by_args(vol_name=volume_name) 

2108 uuid = volume['uuid'] 

2109 body = { 

2110 'name': snapshot_name, 

2111 } 

2112 if snapmirror_label is not None: 2112 ↛ 2113line 2112 didn't jump to line 2113 because the condition on line 2112 was never true

2113 body['snapmirror_label'] = snapmirror_label 

2114 self.send_request(f'/storage/volumes/{uuid}/snapshots', 'post', 

2115 body=body) 

2116 

2117 @na_utils.trace 

2118 def is_flexgroup_supported(self): 

2119 return self.features.FLEXGROUP 

2120 

2121 @na_utils.trace 

2122 def is_flexgroup_volume(self, volume_name): 

2123 """Determines if the ONTAP volume is FlexGroup.""" 

2124 

2125 query = { 

2126 'name': volume_name, 

2127 'fields': 'style' 

2128 } 

2129 result = self.send_request('/storage/volumes/', 'get', query=query) 

2130 

2131 if not self._has_records(result): 

2132 raise exception.StorageResourceNotFound(name=volume_name) 

2133 

2134 vols = result.get('records', []) 

2135 if len(vols) > 1: 

2136 msg = _('More than one volume with volume name %(vol)s found.') 

2137 msg_args = {'vol': volume_name} 

2138 raise exception.NetAppException(msg % msg_args) 

2139 

2140 return na_utils.is_style_extended_flexgroup(vols[0]['style']) 

2141 

2142 @staticmethod 

2143 def _is_busy_snapshot(snapshot_owners): 

2144 """Checks if the owners means that the snapshot is busy. 

2145 

2146 Snapshot is busy when any of the owners doesn't end with 'dependent'. 

2147 """ 

2148 

2149 for owner in snapshot_owners: 

2150 if not owner.endswith('dependent'): 

2151 return True 

2152 

2153 return False 

2154 

2155 @na_utils.trace 

2156 def get_snapshot(self, volume_name, snapshot_name): 

2157 """Gets a single snapshot.""" 

2158 try: 

2159 volume = self._get_volume_by_args(vol_name=volume_name) 

2160 except exception.NetAppException: 

2161 msg = _('Could not find volume %s to get snapshot') 

2162 LOG.error(msg, volume_name) 

2163 raise exception.SnapshotResourceNotFound(name=snapshot_name) 

2164 

2165 uuid = volume['uuid'] 

2166 query = { 

2167 'name': snapshot_name, 

2168 'fields': 'name,volume,create_time,owners' 

2169 } 

2170 result = self.send_request(f'/storage/volumes/{uuid}/snapshots', 'get', 

2171 query=query) 

2172 

2173 if not self._has_records(result): 

2174 raise exception.SnapshotResourceNotFound(name=snapshot_name) 

2175 

2176 snapshots = result.get('records', []) 

2177 if len(snapshots) > 1: 

2178 msg = _('Could not find unique snapshot %(snap)s on ' 

2179 'volume %(vol)s.') 

2180 msg_args = {'snap': snapshot_name, 'vol': volume_name} 

2181 raise exception.NetAppException(msg % msg_args) 

2182 

2183 snapshot_info = snapshots[0] 

2184 # NOTE(felipe_rodrigues): even requesting the field owners, it is not 

2185 # sent back in case no owners. 

2186 owners = set(snapshot_info.get('owners', [])) 

2187 return { 

2188 'access-time': snapshot_info['create_time'], 

2189 'name': snapshot_info['name'], 

2190 'volume': snapshot_info['volume']['name'], 

2191 'owners': owners, 

2192 'busy': self._is_busy_snapshot(owners), 

2193 'locked_by_clone': SNAPSHOT_CLONE_OWNER in owners, 

2194 } 

2195 

2196 @na_utils.trace 

2197 def get_clone_children_for_snapshot(self, volume_name, snapshot_name): 

2198 """Returns volumes that are keeping a snapshot locked.""" 

2199 

2200 query = { 

2201 'clone.parent_snapshot.name': snapshot_name, 

2202 'clone.parent_volume.name': volume_name, 

2203 'fields': 'name' 

2204 } 

2205 result = self.get_records('/storage/volumes', query=query) 

2206 

2207 return [{'name': volume['name']} 

2208 for volume in result.get('records', [])] 

2209 

2210 @na_utils.trace 

2211 def volume_clone_split_start(self, volume_name): 

2212 """Begins splitting a clone from its parent.""" 

2213 

2214 volume = self._get_volume_by_args(vol_name=volume_name) 

2215 uuid = volume['uuid'] 

2216 body = { 

2217 'clone.split_initiated': 'true', 

2218 } 

2219 self.send_request(f'/storage/volumes/{uuid}', 'patch', 

2220 body=body, wait_on_accepted=False) 

2221 

2222 @na_utils.trace 

2223 def volume_clone_split_status(self, volume_name): 

2224 """Status of splitting a clone from its parent.""" 

2225 

2226 query = { 

2227 'name': volume_name, 

2228 'fields': 'clone.split_complete_percent' 

2229 } 

2230 

2231 response = self.send_request('/storage/volumes/', 'get', query=query) 

2232 if not self._has_records(response): 

2233 return na_utils.CLONE_SPLIT_STATUS_FINISHED 

2234 

2235 vol = response.get('records')[0] 

2236 percent = vol.get('clone.split_complete_percent') 

2237 try: 

2238 if int(percent) < 100: 

2239 return na_utils.CLONE_SPLIT_STATUS_ONGOING 

2240 if int(percent) == 100: 

2241 return na_utils.CLONE_SPLIT_STATUS_FINISHED 

2242 except (ValueError, TypeError) as e: 

2243 LOG.exception(f"unexpected error converting clone-split " 

2244 f"percentage '{percent}' to integer: {e}") 

2245 return na_utils.CLONE_SPLIT_STATUS_UNKNOWN 

2246 

2247 @na_utils.trace 

2248 def volume_clone_split_stop(self, volume_name): 

2249 """Stops splitting a clone from its parent.""" 

2250 

2251 volume = self._get_volume_by_args(vol_name=volume_name) 

2252 uuid = volume['uuid'] 

2253 body = { 

2254 'clone.split_initiated': 'false', 

2255 } 

2256 try: 

2257 self.send_request(f'/storage/volumes/{uuid}', 'patch', 

2258 body=body, wait_on_accepted=False) 

2259 except netapp_api.NaApiError as e: 

2260 if e.code in (netapp_api.EVOLUMEDOESNOTEXIST, 

2261 netapp_api.EVOLNOTCLONE, 

2262 netapp_api.EVOLOPNOTUNDERWAY): 

2263 return 

2264 raise 

2265 

2266 @na_utils.trace 

2267 def delete_snapshot(self, volume_name, snapshot_name, ignore_owners=False): 

2268 """Deletes a volume snapshot.""" 

2269 

2270 try: 

2271 volume = self._get_volume_by_args(vol_name=volume_name) 

2272 except exception.NetAppException: 

2273 msg = _('Could not find volume %s to delete snapshot') 

2274 LOG.warning(msg, volume_name) 

2275 return 

2276 uuid = volume['uuid'] 

2277 

2278 query = { 

2279 'name': snapshot_name, 

2280 'fields': 'uuid' 

2281 } 

2282 snapshot = self.send_request(f'/storage/volumes/{uuid}/snapshots', 

2283 'get', query=query) 

2284 if self._has_records(snapshot): 2284 ↛ exitline 2284 didn't return from function 'delete_snapshot' because the condition on line 2284 was always true

2285 snapshot_uuid = snapshot['records'][0]['uuid'] 

2286 # NOTE(rfluisa): The CLI passthrough was used here, because the 

2287 # REST API endpoint used to delete snapshots does not an equivalent 

2288 # to the ignore_owners field 

2289 

2290 if ignore_owners: 

2291 query_cli = { 

2292 'vserver': self.vserver, 

2293 'volume': volume_name, 

2294 'snapshot': snapshot_name, 

2295 'ignore-owners': 'true' 

2296 } 

2297 self.send_request( 

2298 '/private/cli/snapshot', 'delete', query=query_cli) 

2299 else: 

2300 self.send_request( 

2301 f'/storage/volumes/{uuid}/snapshots/{snapshot_uuid}', 

2302 'delete') 

2303 

2304 @na_utils.trace 

2305 def rename_snapshot_and_split_clones(self, volume_name, snapshot_name): 

2306 """Renames volume snapshot & splits clones.""" 

2307 

2308 msg_args = {'snap': snapshot_name, 'vol': volume_name} 

2309 try: 

2310 self.rename_snapshot(volume_name, 

2311 snapshot_name, 

2312 DELETED_PREFIX + snapshot_name) 

2313 msg = _('Soft-deleted snapshot %(snap)s on volume %(vol)s.') 

2314 LOG.info(msg, msg_args) 

2315 except netapp_api.NaApiError as e: 

2316 if e.code == netapp_api.EREST_SNAPSHOT_NOT_FOUND: 

2317 msg = _('Snapshot %(snap)s on volume %(vol)s not found.') 

2318 LOG.debug(msg, msg_args) 

2319 return 

2320 else: 

2321 raise 

2322 

2323 # Snapshots are locked by clone(s), so split the clone(s) 

2324 snapshot_children = self.get_clone_children_for_snapshot( 

2325 volume_name, DELETED_PREFIX + snapshot_name) 

2326 for snapshot_child in snapshot_children: 

2327 self.volume_clone_split_start(snapshot_child['name']) 

2328 

2329 @na_utils.trace 

2330 def soft_delete_snapshot(self, volume_name, snapshot_name): 

2331 """Deletes a volume snapshot, or renames & splits if delete fails.""" 

2332 try: 

2333 self.delete_snapshot(volume_name, snapshot_name) 

2334 except netapp_api.NaApiError: 

2335 self.rename_snapshot_and_split_clones(volume_name, snapshot_name) 

2336 

2337 @na_utils.trace 

2338 def rename_snapshot(self, volume_name, snapshot_name, new_snapshot_name): 

2339 """Renames the snapshot.""" 

2340 

2341 volume = self._get_volume_by_args(vol_name=volume_name) 

2342 uuid = volume['uuid'] 

2343 query = { 

2344 'name': snapshot_name, 

2345 } 

2346 body = { 

2347 'name': new_snapshot_name, 

2348 } 

2349 self.send_request(f'/storage/volumes/{uuid}/snapshots', 'patch', 

2350 query=query, body=body) 

2351 

2352 @na_utils.trace 

2353 def _get_soft_deleted_snapshots(self): 

2354 """Returns non-busy, soft-deleted snapshots suitable for reaping.""" 

2355 

2356 query = { 

2357 'name': DELETED_PREFIX + '*', 

2358 'fields': 'uuid,volume,owners,svm.name' 

2359 } 

2360 result = self.get_records('/storage/volumes/*/snapshots', query=query) 

2361 

2362 snapshot_map = {} 

2363 for snapshot_info in result.get('records', []): 

2364 if self._is_busy_snapshot(snapshot_info['owners']): 

2365 continue 

2366 

2367 vserver = snapshot_info['svm']['name'] 

2368 snapshot_list = snapshot_map.get(vserver, []) 

2369 snapshot_list.append({ 

2370 'uuid': snapshot_info['uuid'], 

2371 'volume_uuid': snapshot_info['volume']['uuid'], 

2372 }) 

2373 snapshot_map[vserver] = snapshot_list 

2374 

2375 return snapshot_map 

2376 

2377 @na_utils.trace 

2378 def prune_deleted_snapshots(self): 

2379 """Deletes non-busy snapshots that were previously soft-deleted.""" 

2380 

2381 deleted_snapshots_map = self._get_soft_deleted_snapshots() 

2382 

2383 for vserver in deleted_snapshots_map: 

2384 client = copy.deepcopy(self) 

2385 client.set_vserver(vserver) 

2386 

2387 for snapshot in deleted_snapshots_map[vserver]: 

2388 try: 

2389 vol_uuid = snapshot['volume_uuid'] 

2390 snap_uuid = snapshot['uuid'] 

2391 self.send_request(f'/storage/volumes/{vol_uuid}/snapshots/' 

2392 f'{snap_uuid}', 'delete') 

2393 except netapp_api.api.NaApiError: 

2394 msg = _('Could not delete snapshot %(snap)s on ' 

2395 'volume %(volume)s.') 

2396 msg_args = { 

2397 'snap': snapshot['uuid'], 

2398 'volume': snapshot['volume_uuid'], 

2399 } 

2400 LOG.exception(msg, msg_args) 

2401 

2402 @na_utils.trace 

2403 def snapshot_exists(self, snapshot_name, volume_name): 

2404 """Checks if Snapshot exists for a specified volume.""" 

2405 LOG.debug('Checking if snapshot %(snapshot)s exists for ' 

2406 'volume %(volume)s', 

2407 {'snapshot': snapshot_name, 'volume': volume_name}) 

2408 

2409 volume = self._get_volume_by_args(vol_name=volume_name, 

2410 fields='uuid,state') 

2411 

2412 if volume['state'] == 'offline': 

2413 msg = _('Could not read information for snapshot %(name)s. ' 

2414 'Volume %(volume)s is offline.') 

2415 msg_args = { 

2416 'name': snapshot_name, 

2417 'volume': volume_name, 

2418 } 

2419 LOG.debug(msg, msg_args) 

2420 raise exception.SnapshotUnavailable(msg % msg_args) 

2421 

2422 query = {'name': snapshot_name} 

2423 vol_uuid = volume['uuid'] 

2424 result = self.send_request( 

2425 f'/storage/volumes/{vol_uuid}/snapshots/', 'get', query=query) 

2426 

2427 return self._has_records(result) 

2428 

2429 @na_utils.trace 

2430 def volume_has_luns(self, volume_name): 

2431 """Checks if volume has LUNs.""" 

2432 LOG.debug('Checking if volume %s has LUNs', volume_name) 

2433 

2434 query = { 

2435 'location.volume.name': volume_name, 

2436 } 

2437 

2438 response = self.send_request('/storage/luns/', 'get', query=query) 

2439 

2440 return self._has_records(response) 

2441 

2442 @na_utils.trace 

2443 def volume_has_junctioned_volumes(self, junction_path): 

2444 """Checks if volume has volumes mounted beneath its junction path.""" 

2445 if not junction_path: 

2446 return False 

2447 

2448 query = { 

2449 'nas.path': junction_path + '/*' 

2450 } 

2451 

2452 response = self.send_request('/storage/volumes/', 'get', query=query) 

2453 return self._has_records(response) 

2454 

2455 @na_utils.trace 

2456 def set_volume_name(self, volume_name, new_volume_name): 

2457 """Set volume name.""" 

2458 volume = self._get_volume_by_args(vol_name=volume_name) 

2459 uuid = volume['uuid'] 

2460 

2461 body = { 

2462 'name': new_volume_name 

2463 } 

2464 

2465 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body) 

2466 

2467 @na_utils.trace 

2468 def mount_volume(self, volume_name, junction_path=None): 

2469 """Mounts a volume on a junction path.""" 

2470 volume = self._get_volume_by_args(vol_name=volume_name) 

2471 uuid = volume['uuid'] 

2472 

2473 body = { 

2474 'nas.path': (junction_path if junction_path 

2475 else '/%s' % volume_name) 

2476 } 

2477 

2478 try: 

2479 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body) 

2480 except netapp_api.api.NaApiError as e: 

2481 # NOTE(rfluisa): This verification was added to keep the error code 

2482 # compatible with the one that was returned by ZAPI 

2483 if e.code == netapp_api.EREST_SNAPMIRROR_INITIALIZING: 2483 ↛ 2486line 2483 didn't jump to line 2486 because the condition on line 2483 was always true

2484 raise netapp_api.api.NaApiError(message=e.message, 

2485 code=netapp_api.api.EAPIERROR) 

2486 raise 

2487 

2488 @na_utils.trace 

2489 def get_volume_at_junction_path(self, junction_path): 

2490 """Returns the volume with the specified junction path, if present.""" 

2491 if not junction_path: 

2492 return None 

2493 

2494 query = { 

2495 'nas.path': junction_path, 

2496 'fields': 'name', 

2497 } 

2498 

2499 response = self.send_request('/storage/volumes/', 'get', query=query) 

2500 

2501 if not self._has_records(response): 2501 ↛ 2502line 2501 didn't jump to line 2502 because the condition on line 2501 was never true

2502 return None 

2503 

2504 vol = response.get('records')[0] 

2505 

2506 volume = { 

2507 'name': vol.get('name'), 

2508 } 

2509 return volume 

2510 

2511 @na_utils.trace 

2512 def get_aggregate_for_volume(self, volume_name): 

2513 """Get the name of the aggregate containing a volume.""" 

2514 

2515 query = { 

2516 'name': volume_name, 

2517 'fields': 'aggregates', 

2518 } 

2519 

2520 res = self.send_request('/storage/volumes/', 'get', query=query) 

2521 

2522 aggregate = res.get('aggregates') 

2523 

2524 if not aggregate: 

2525 msg = _('Could not find aggregate for volume %s.') 

2526 raise exception.NetAppException(msg % volume_name) 

2527 

2528 aggregate_size = len(res.get('aggregates')) 

2529 

2530 if aggregate_size > 1: 2530 ↛ 2533line 2530 didn't jump to line 2533 because the condition on line 2530 was always true

2531 aggregate = [aggr.get('name') for aggr in res.get('aggregates')] 

2532 

2533 return aggregate 

2534 

2535 @na_utils.trace 

2536 def get_volume_to_manage(self, aggregate_name, volume_name): 

2537 """Get existing volume info to be managed.""" 

2538 

2539 query = { 

2540 'name': volume_name, 

2541 'fields': 'name,aggregates.name,nas.path,name,type,style,' 

2542 'svm.name,qos.policy.name,space.size', 

2543 'aggregates.name': aggregate_name 

2544 } 

2545 

2546 response = self.send_request('/storage/volumes', 'get', query=query) 

2547 if not self._has_records(response): 2547 ↛ 2548line 2547 didn't jump to line 2548 because the condition on line 2547 was never true

2548 return None 

2549 

2550 res = response.get('records', [])[0] 

2551 aggregate = '' 

2552 aggr_list = [] 

2553 aggregate_size = len(res.get('aggregates', [])) 

2554 if aggregate_size == 1: 2554 ↛ 2557line 2554 didn't jump to line 2557 because the condition on line 2554 was always true

2555 aggregate = res.get('aggregates', [])[0].get('name', '') 

2556 else: 

2557 aggr_list = [aggr.get('name') for aggr in res.get('aggregates')] 

2558 

2559 volume = { 

2560 'aggregate': aggregate, 

2561 'aggr-list': aggr_list, 

2562 'junction-path': res.get('nas', {}).get('path', ''), 

2563 'name': res.get('name'), 

2564 'type': res.get('type'), 

2565 # NOTE(caiquemello): REST no longer uses flex or infinitevol as 

2566 # styles. In onder to keep compatibility style is set to 'flex'. 

2567 'style': 'flex', 

2568 'owning-vserver-name': res.get('svm', {}).get('name', ''), 

2569 'size': res.get('space', {}).get('size', 0), 

2570 'qos-policy-group-name': ( 

2571 res.get('qos', {}).get('policy', {}).get('name', '')) 

2572 } 

2573 

2574 return volume 

2575 

2576 @na_utils.trace 

2577 def _parse_timestamp(self, time_str): 

2578 """Parse timestamp string into a number.""" 

2579 

2580 try: 

2581 dt = datetime.fromisoformat(time_str) 

2582 return dt.timestamp() 

2583 except Exception: 

2584 LOG.debug("Failed to parse timestamp: %s", time_str) 

2585 raise 

2586 

2587 @na_utils.trace 

2588 def _get_snapmirrors(self, source_path=None, dest_path=None, 

2589 source_vserver=None, source_volume=None, 

2590 dest_vserver=None, dest_volume=None, 

2591 list_destinations_only=False, 

2592 enable_tunneling=True, 

2593 desired_attributes=None): 

2594 """Get a list of snapmirrors.""" 

2595 

2596 fields = ['state', 'source.svm.name', 'source.path', 

2597 'destination.svm.name', 'destination.path', 

2598 'transfer.end_time', 'uuid', 'policy.type', 

2599 'transfer_schedule.name', 'transfer.state', 

2600 'last_transfer_type', 'transfer.bytes_transferred', 

2601 'healthy'] 

2602 

2603 query = {} 

2604 query['fields'] = ','.join(fields) 

2605 

2606 if source_path: 

2607 query['source.path'] = source_path 

2608 else: 

2609 query_src_vol = source_volume if source_volume else '*' 

2610 query_src_vserver = source_vserver if source_vserver else '*' 

2611 query['source.path'] = query_src_vserver + ':' + query_src_vol 

2612 

2613 if dest_path: 

2614 query['destination.path'] = dest_path 

2615 else: 

2616 query_dst_vol = dest_volume if dest_volume else '*' 

2617 query_dst_vserver = dest_vserver if dest_vserver else '*' 

2618 query['destination.path'] = query_dst_vserver + ':' + query_dst_vol 

2619 

2620 if list_destinations_only: 2620 ↛ 2621line 2620 didn't jump to line 2621 because the condition on line 2620 was never true

2621 query['list_destinations_only'] = 'true' 

2622 

2623 response = self.send_request( 

2624 '/snapmirror/relationships', 'get', query=query, 

2625 enable_tunneling=enable_tunneling) 

2626 

2627 snapmirrors = [] 

2628 for record in response.get('records', []): 

2629 snapmirrors.append({ 

2630 'relationship-status': ( 

2631 'idle' 

2632 if record.get('state') == 'snapmirrored' 

2633 else record.get('state')), 

2634 'transferring-state': record.get('transfer', {}).get('state'), 

2635 'mirror-state': record.get('state'), 

2636 'schedule': ( 

2637 record['transfer_schedule']['name'] 

2638 if record.get('transfer_schedule') 

2639 else None), 

2640 'source-vserver': record['source']['svm']['name'], 

2641 'source-volume': (record['source']['path'].split(':')[1] if 

2642 record.get('source') else None), 

2643 'destination-vserver': record['destination']['svm']['name'], 

2644 'destination-volume': ( 

2645 record['destination']['path'].split(':')[1] 

2646 if record.get('destination') else None), 

2647 'last-transfer-end-timestamp': 

2648 (self._parse_timestamp(record['transfer']['end_time']) if 

2649 record.get('transfer', {}).get('end_time') else 0), 

2650 'uuid': record['uuid'], 

2651 'policy-type': record.get('policy', {}).get('type'), 

2652 'is-healthy': ( 

2653 'true' 

2654 if record.get('healthy', {}) is True else 'false'), 

2655 'last-transfer-type': record.get('last_transfer_type', None), 

2656 'last-transfer-size': record.get('transfer', 

2657 {}).get('bytes_transferred'), 

2658 

2659 }) 

2660 

2661 return snapmirrors 

2662 

2663 @na_utils.trace 

2664 def get_snapmirrors_svm(self, source_vserver=None, dest_vserver=None, 

2665 desired_attributes=None): 

2666 """Get all snapmirrors from specified SVMs source/destination.""" 

2667 source_path = source_vserver + ':*' if source_vserver else None 

2668 dest_path = dest_vserver + ':*' if dest_vserver else None 

2669 return self.get_snapmirrors(source_path=source_path, 

2670 dest_path=dest_path, 

2671 desired_attributes=desired_attributes) 

2672 

2673 @na_utils.trace 

2674 def get_snapmirrors(self, source_path=None, dest_path=None, 

2675 source_vserver=None, dest_vserver=None, 

2676 source_volume=None, dest_volume=None, 

2677 desired_attributes=None, enable_tunneling=None, 

2678 list_destinations_only=None): 

2679 """Gets one or more SnapMirror relationships. 

2680 

2681 Either the source or destination info may be omitted. 

2682 Desired attributes exists only to keep consistency with ZAPI client 

2683 signature and has no effect in the output. 

2684 """ 

2685 

2686 snapmirrors = self._get_snapmirrors( 

2687 source_path=source_path, 

2688 dest_path=dest_path, 

2689 source_vserver=source_vserver, 

2690 source_volume=source_volume, 

2691 dest_vserver=dest_vserver, 

2692 dest_volume=dest_volume, 

2693 enable_tunneling=enable_tunneling, 

2694 list_destinations_only=list_destinations_only) 

2695 

2696 return snapmirrors 

2697 

2698 @na_utils.trace 

2699 def volume_has_snapmirror_relationships(self, volume): 

2700 """Return True if snapmirror relationships exist for a given volume. 

2701 

2702 If we have snapmirror control plane license, we can verify whether 

2703 the given volume is part of any snapmirror relationships. 

2704 """ 

2705 try: 

2706 # Check if volume is a source snapmirror volume 

2707 snapmirrors = self.get_snapmirrors( 

2708 source_vserver=volume['owning-vserver-name'], 

2709 source_volume=volume['name']) 

2710 

2711 # Check if volume is a destination snapmirror volume 

2712 if not snapmirrors: 

2713 snapmirrors = self.get_snapmirrors( 

2714 dest_vserver=volume['owning-vserver-name'], 

2715 dest_volume=volume['name']) 

2716 

2717 has_snapmirrors = len(snapmirrors) > 0 

2718 except netapp_api.api.NaApiError: 

2719 msg = ("Could not determine if volume %s is part of " 

2720 "existing snapmirror relationships.") 

2721 LOG.exception(msg, volume['name']) 

2722 has_snapmirrors = False 

2723 

2724 return has_snapmirrors 

2725 

2726 @na_utils.trace 

2727 def modify_volume(self, aggregate_name, volume_name, 

2728 thin_provisioned=False, snapshot_policy=None, 

2729 language=None, dedup_enabled=False, 

2730 compression_enabled=False, max_files=None, 

2731 qos_policy_group=None, hide_snapdir=None, 

2732 autosize_attributes=None, 

2733 adaptive_qos_policy_group=None, **options): 

2734 """Update backend volume for a share as necessary. 

2735 

2736 :param aggregate_name: either a list or a string. List for aggregate 

2737 names where the FlexGroup resides, while a string for the aggregate 

2738 name where FlexVol volume is. 

2739 :param volume_name: name of the modified volume. 

2740 :param thin_provisioned: volume is thin. 

2741 :param snapshot_policy: policy of volume snapshot. 

2742 :param language: language of the volume. 

2743 :param dedup_enabled: is the deduplication enabled for the volume. 

2744 :param compression_enabled: is the compression enabled for the volume. 

2745 :param max_files: number of maximum files in the volume. 

2746 :param qos_policy_group: name of the QoS policy. 

2747 :param hide_snapdir: hide snapshot directory. 

2748 :param autosize_attributes: autosize for the volume. 

2749 :param adaptive_qos_policy_group: name of the adaptive QoS policy. 

2750 """ 

2751 

2752 body = { 

2753 'guarantee': {'type': 'none' if thin_provisioned else 'volume'} 

2754 } 

2755 if autosize_attributes: 

2756 reset_val = str(autosize_attributes.get('reset', False)).lower() 

2757 if reset_val == 'true': 2757 ↛ 2759line 2757 didn't jump to line 2759 because the condition on line 2757 was never true

2758 # Handle autosize reset 

2759 vserver = autosize_attributes.get('vserver') or self.vserver 

2760 if volume_name and vserver: 

2761 self.reset_volume_autosize(volume_name, vserver) 

2762 else: 

2763 # Build autosize attributes 

2764 autosize = self._build_autosize_attributes(autosize_attributes) 

2765 if autosize: 2765 ↛ 2768line 2765 didn't jump to line 2768 because the condition on line 2765 was always true

2766 body['autosize'] = autosize 

2767 

2768 if language: 

2769 body['language'] = language 

2770 

2771 if max_files: 

2772 body['files'] = {'maximum': max_files} 

2773 

2774 if snapshot_policy: 

2775 body['snapshot_policy'] = {'name': snapshot_policy} 

2776 

2777 qos_policy_name = qos_policy_group or adaptive_qos_policy_group 

2778 if qos_policy_name: 

2779 body['qos'] = {'policy': {'name': qos_policy_name}} 

2780 

2781 if hide_snapdir in (True, False): 

2782 # Value of hide_snapdir needs to be inverted for ZAPI parameter 

2783 body['snapshot_directory_access_enabled'] = ( 

2784 str(not hide_snapdir).lower()) 

2785 

2786 aggregates = None 

2787 if isinstance(aggregate_name, list): 

2788 is_flexgroup = True 

2789 aggregates = ','.join(aggregate_name) 

2790 else: 

2791 is_flexgroup = False 

2792 aggregates = aggregate_name 

2793 

2794 volume = self._get_volume_by_args(vol_name=volume_name, 

2795 aggregate_name=aggregates) 

2796 

2797 self.send_request('/storage/volumes/' + volume['uuid'], 

2798 'patch', body=body) 

2799 # Extract efficiency_policy from provisioning_options 

2800 efficiency_policy = options.get('efficiency_policy', None) 

2801 # Efficiency options must be handled separately 

2802 self.update_volume_efficiency_attributes( 

2803 volume_name, dedup_enabled, compression_enabled, 

2804 is_flexgroup=is_flexgroup, efficiency_policy=efficiency_policy 

2805 ) 

2806 if self._is_snaplock_enabled_volume(volume_name): 2806 ↛ exitline 2806 didn't return from function 'modify_volume' because the condition on line 2806 was always true

2807 self.set_snaplock_attributes(volume_name, **options) 

2808 

2809 @na_utils.trace 

2810 def _build_autosize_attributes(self, autosize_attributes): 

2811 """Build autosize attributes dict from autosize_attributes.""" 

2812 src = autosize_attributes 

2813 

2814 # Build autosize dict directly 

2815 autosize_key_map = { 

2816 'grow-threshold-percent': 'grow_threshold', 

2817 'shrink-threshold-percent': 'shrink_threshold', 

2818 'maximum-size': 'maximum', 

2819 'minimum-size': 'minimum', 

2820 } 

2821 

2822 autosize = { 

2823 dest_key: src[src_key] 

2824 for src_key, dest_key in autosize_key_map.items() 

2825 if src_key in src 

2826 } 

2827 

2828 # Add mode if present 

2829 if 'mode' in src: 

2830 autosize['mode'] = src['mode'] 

2831 

2832 return autosize if autosize else None 

2833 

2834 @na_utils.trace 

2835 def reset_volume_autosize(self, volume_name, vserver_name): 

2836 """Resets volume autosize configuration to default values. 

2837 

2838 This method resets the autosize configuration for a FlexVol volume 

2839 back to its default settings. The autosize feature automatically 

2840 grows or shrinks a volume based on the amount of used space. 

2841 """ 

2842 query = { 

2843 "vserver": vserver_name, 

2844 "volume": volume_name 

2845 } 

2846 body = { 

2847 "autosize-reset": "true" 

2848 } 

2849 

2850 try: 

2851 self.send_request('/private/cli/volume', 'patch', 

2852 query=query, body=body) 

2853 except netapp_api.api.NaApiError as e: 

2854 LOG.error('Failed to reset volume autosize for %s. Error: %s. ' 

2855 'Code: %s', volume_name, e.message, e.code) 

2856 raise 

2857 

2858 @na_utils.trace 

2859 def start_volume_move(self, volume_name, vserver, destination_aggregate, 

2860 cutover_action='wait', encrypt_destination=None): 

2861 """Moves a FlexVol across Vserver aggregates. 

2862 

2863 Requires cluster-scoped credentials. 

2864 """ 

2865 self._send_volume_move_request( 

2866 volume_name, vserver, 

2867 destination_aggregate, 

2868 cutover_action=cutover_action, 

2869 encrypt_destination=encrypt_destination) 

2870 

2871 @na_utils.trace 

2872 def check_volume_move(self, volume_name, vserver, destination_aggregate, 

2873 encrypt_destination=None): 

2874 """Moves a FlexVol across Vserver aggregates. 

2875 

2876 Requires cluster-scoped credentials. 

2877 """ 

2878 self._send_volume_move_request( 

2879 volume_name, 

2880 vserver, 

2881 destination_aggregate, 

2882 validation_only=True, 

2883 encrypt_destination=encrypt_destination) 

2884 

2885 @na_utils.trace 

2886 def _send_volume_move_request(self, volume_name, vserver, 

2887 destination_aggregate, 

2888 cutover_action='wait', 

2889 validation_only=False, 

2890 encrypt_destination=None): 

2891 """Send request to check if vol move is possible, or start it. 

2892 

2893 :param volume_name: Name of the FlexVol to be moved. 

2894 :param destination_aggregate: Name of the destination aggregate 

2895 :param cutover_action: can have one of [cutover_wait]. 'cutover_wait' 

2896 to go into cutover manually. 

2897 :param validation_only: If set to True, only validates if the volume 

2898 move is possible, does not trigger data copy. 

2899 :param encrypt_destination: If set to True, it encrypts the Flexvol 

2900 after the volume move is complete. 

2901 """ 

2902 body = { 

2903 'movement.destination_aggregate.name': destination_aggregate, 

2904 } 

2905 # NOTE(caiquemello): In REST 'cutover_action'was deprecated. Now the 

2906 # equivalant behavior is represented by 'movement.state'. The 

2907 # equivalent in ZAPI for 'defer_on_failure' is the default value 

2908 # for 'movement.state' in REST. So, there is no need to set 'defer' in 

2909 # the body. Remove this behavior when ZAPI is removed. 

2910 if cutover_action != 'defer': 2910 ↛ 2913line 2910 didn't jump to line 2913 because the condition on line 2910 was always true

2911 body['movement.state'] = CUTOVER_ACTION_MAP[cutover_action] 

2912 

2913 query = { 

2914 'name': volume_name, 

2915 } 

2916 

2917 if encrypt_destination is True: 2917 ↛ 2918line 2917 didn't jump to line 2918 because the condition on line 2917 was never true

2918 body['encryption.enabled'] = 'true' 

2919 elif encrypt_destination is False: 2919 ↛ 2922line 2919 didn't jump to line 2922 because the condition on line 2919 was always true

2920 body['encryption.enabled'] = 'false' 

2921 

2922 if validation_only: 2922 ↛ 2925line 2922 didn't jump to line 2925 because the condition on line 2922 was always true

2923 body['validate_only'] = 'true' 

2924 

2925 self.send_request('/storage/volumes/', 'patch', query=query, body=body, 

2926 wait_on_accepted=False) 

2927 

2928 @na_utils.trace 

2929 def get_nfs_export_policy_for_volume(self, volume_name): 

2930 """Get the actual export policy for a share.""" 

2931 

2932 query = { 

2933 'name': volume_name, 

2934 'fields': 'nas.export_policy.name' 

2935 } 

2936 

2937 response = self.send_request('/storage/volumes/', 'get', query=query) 

2938 

2939 if not self._has_records(response): 

2940 msg = _('Could not find export policy for volume %s.') 

2941 raise exception.NetAppException(msg % volume_name) 

2942 

2943 volume = response['records'][0] 

2944 return volume['nas']['export_policy']['name'] 

2945 

2946 @na_utils.trace 

2947 def get_unique_export_policy_id(self, policy_name): 

2948 """Get export policy uuid for a given policy name""" 

2949 

2950 get_uuid = self.send_request( 

2951 '/protocols/nfs/export-policies', 'get', 

2952 query={'name': policy_name}) 

2953 

2954 if not self._has_records(get_uuid): 

2955 msg = _('Could not find export policy with name %s.') 

2956 raise exception.NetAppException(msg % policy_name) 

2957 

2958 uuid = get_uuid['records'][0]['id'] 

2959 return uuid 

2960 

2961 @na_utils.trace 

2962 def _get_nfs_export_rule_indices(self, policy_name, client_match): 

2963 """Get index of the rule within the export policy.""" 

2964 

2965 uuid = self.get_unique_export_policy_id(policy_name) 

2966 

2967 query = { 

2968 'clients.match': client_match, 

2969 'fields': 'clients.match,index' 

2970 } 

2971 

2972 response = self.send_request( 

2973 f'/protocols/nfs/export-policies/{uuid}/rules', 

2974 'get', query=query) 

2975 

2976 rules = response['records'] 

2977 indices = [rule['index'] for rule in rules] 

2978 indices.sort() 

2979 return [str(i) for i in indices] 

2980 

2981 @na_utils.trace 

2982 def _add_nfs_export_rule(self, policy_name, client_match, readonly, 

2983 auth_methods): 

2984 """Add rule to NFS export policy.""" 

2985 uuid = self.get_unique_export_policy_id(policy_name) 

2986 body = { 

2987 'clients': [{'match': client_match}], 

2988 'ro_rule': [], 

2989 'rw_rule': [], 

2990 'superuser': [] 

2991 } 

2992 

2993 for am in auth_methods: 

2994 body['ro_rule'].append(am) 

2995 body['rw_rule'].append(am) 

2996 body['superuser'].append(am) 

2997 if readonly: 2997 ↛ 2999line 2997 didn't jump to line 2999 because the condition on line 2997 was never true

2998 # readonly, overwrite with auth method 'never' 

2999 body['rw_rule'] = ['never'] 

3000 

3001 self.send_request(f'/protocols/nfs/export-policies/{uuid}/rules', 

3002 'post', body=body) 

3003 

3004 @na_utils.trace 

3005 def _update_nfs_export_rule(self, policy_name, client_match, readonly, 

3006 rule_index, auth_methods): 

3007 """Update rule of NFS export policy.""" 

3008 uuid = self.get_unique_export_policy_id(policy_name) 

3009 body = { 

3010 'client_match': client_match, 

3011 'ro_rule': [], 

3012 'rw_rule': [], 

3013 'superuser': [] 

3014 } 

3015 

3016 for am in auth_methods: 

3017 body['ro_rule'].append(am) 

3018 body['rw_rule'].append(am) 

3019 body['superuser'].append(am) 

3020 if readonly: 3020 ↛ 3022line 3020 didn't jump to line 3022 because the condition on line 3020 was never true

3021 # readonly, overwrite with auth method 'never' 

3022 body['rw_rule'] = ['never'] 

3023 

3024 self.send_request( 

3025 f'/protocols/nfs/export-policies/{uuid}/rules/{rule_index}', 

3026 'patch', body=body) 

3027 

3028 @na_utils.trace 

3029 def _remove_nfs_export_rules(self, policy_name, rule_indices): 

3030 """Remove rule from NFS export policy.""" 

3031 uuid = self.get_unique_export_policy_id(policy_name) 

3032 for index in rule_indices: 

3033 body = { 

3034 'index': index 

3035 } 

3036 try: 

3037 self.send_request( 

3038 f'/protocols/nfs/export-policies/{uuid}/rules/{index}', 

3039 'delete', body=body) 

3040 except netapp_api.api.NaApiError as e: 

3041 if e.code != netapp_api.EREST_ENTRY_NOT_FOUND: 3041 ↛ 3032line 3041 didn't jump to line 3032 because the condition on line 3041 was always true

3042 msg = _("Fail to delete export rule %s.") 

3043 LOG.debug(msg, policy_name) 

3044 raise 

3045 

3046 @na_utils.trace 

3047 def get_cifs_share_access(self, share_name): 

3048 """Get CIFS share access rules.""" 

3049 query = { 

3050 'name': share_name, 

3051 } 

3052 get_uuid = self.send_request('/protocols/cifs/shares', 'get', 

3053 query=query) 

3054 svm_uuid = get_uuid['records'][0]['svm']['uuid'] 

3055 query = {'fields': 'user_or_group,permission'} 

3056 result = self.send_request( 

3057 f'/protocols/cifs/shares/{svm_uuid}/{share_name}/acls', 

3058 'get', query=query) 

3059 

3060 rules = {} 

3061 for records in result["records"]: 

3062 user_or_group = records['user_or_group'] 

3063 permission = records['permission'] 

3064 rules[user_or_group] = permission 

3065 

3066 return rules 

3067 

3068 @na_utils.trace 

3069 def add_cifs_share_access(self, share_name, user_name, readonly): 

3070 """Add CIFS share access rules.""" 

3071 query = { 

3072 'name': share_name 

3073 } 

3074 

3075 get_uuid = self.send_request('/protocols/cifs/shares', 'get', 

3076 query=query) 

3077 svm_uuid = get_uuid['records'][0]['svm']['uuid'] 

3078 

3079 body = { 

3080 'permission': 'read' if readonly else 'full_control', 

3081 'user_or_group': user_name, 

3082 } 

3083 

3084 self.send_request( 

3085 f'/protocols/cifs/shares/{svm_uuid}/{share_name}/acls', 

3086 'post', body=body) 

3087 

3088 @na_utils.trace 

3089 def modify_cifs_share_access(self, share_name, user_name, readonly): 

3090 """Modify CIFS share access rules.""" 

3091 query = { 

3092 'name': share_name 

3093 } 

3094 

3095 get_uuid = self.send_request('/protocols/cifs/shares', 'get', 

3096 query=query) 

3097 svm_uuid = get_uuid['records'][0]['svm']['uuid'] 

3098 

3099 body = { 

3100 'permission': 'read' if readonly else 'full_control', 

3101 } 

3102 

3103 self.send_request( 

3104 f'/protocols/cifs/shares/{svm_uuid}/{share_name}' 

3105 f'/acls/{user_name}/{CIFS_USER_GROUP_TYPE}', 'patch', body=body) 

3106 

3107 @na_utils.trace 

3108 def check_snaprestore_license(self): 

3109 """Check SnapRestore license for SVM scoped user.""" 

3110 try: 

3111 body = { 

3112 'restore_to.snapshot.name': '' 

3113 } 

3114 query = { 

3115 # NOTE(felipe_rodrigues): Acting over all volumes to prevent 

3116 # entry not found error. So, the error comes either by license 

3117 # not installed or snapshot not specified. 

3118 'name': '*' 

3119 } 

3120 self.send_request('/storage/volumes', 'patch', body=body, 

3121 query=query) 

3122 except netapp_api.api.NaApiError as e: 

3123 LOG.debug('Fake restore snapshot request failed: %s', e) 

3124 if e.code == netapp_api.EREST_LICENSE_NOT_INSTALLED: 

3125 return False 

3126 elif e.code == netapp_api.EREST_SNAPSHOT_NOT_SPECIFIED: 3126 ↛ 3130line 3126 didn't jump to line 3130 because the condition on line 3126 was always true

3127 return True 

3128 else: 

3129 # unexpected error. 

3130 raise e 

3131 

3132 # since it passed an empty snapshot, it should never get here. 

3133 msg = _("Caught an unexpected behavior: the fake restore to " 

3134 "snapshot request using all volumes and empty string " 

3135 "snapshot as argument has not failed.") 

3136 LOG.exception(msg) 

3137 raise exception.NetAppException(msg) 

3138 

3139 @na_utils.trace 

3140 def trigger_volume_move_cutover(self, volume_name, vserver, force=True): 

3141 """Triggers the cut-over for a volume in data motion.""" 

3142 query = { 

3143 'name': volume_name 

3144 } 

3145 body = { 

3146 'movement.state': 'cutover' 

3147 } 

3148 self.send_request('/storage/volumes/', 'patch', 

3149 query=query, body=body) 

3150 

3151 @na_utils.trace 

3152 def abort_volume_move(self, volume_name, vserver): 

3153 """Abort volume move operation.""" 

3154 volume = self._get_volume_by_args(vol_name=volume_name) 

3155 vol_uuid = volume['uuid'] 

3156 self.send_request(f'/storage/volumes/{vol_uuid}', 'patch') 

3157 

3158 @na_utils.trace 

3159 def get_volume_move_status(self, volume_name, vserver): 

3160 """Gets the current state of a volume move operation.""" 

3161 

3162 fields = 'movement.percent_complete,movement.state' 

3163 

3164 query = { 

3165 'name': volume_name, 

3166 'svm.name': vserver, 

3167 'fields': fields 

3168 } 

3169 

3170 result = self.send_request('/storage/volumes/', 'get', query=query) 

3171 

3172 if not self._has_records(result): 

3173 msg = ("Volume %(vol)s in Vserver %(server)s is not part of any " 

3174 "data motion operations.") 

3175 msg_args = {'vol': volume_name, 'server': vserver} 

3176 raise exception.NetAppException(msg % msg_args) 

3177 

3178 volume_move_info = result.get('records')[0] 

3179 volume_movement = volume_move_info['movement'] 

3180 

3181 status_info = { 

3182 'percent-complete': volume_movement.get('percent_complete', 0), 

3183 'estimated-completion-time': '', 

3184 'state': volume_movement['state'], 

3185 'details': '', 

3186 'cutover-action': '', 

3187 'phase': volume_movement['state'], 

3188 } 

3189 

3190 return status_info 

3191 

3192 @na_utils.trace 

3193 def list_snapmirror_snapshots(self, volume_name, newer_than=None): 

3194 """Gets SnapMirror snapshots on a volume.""" 

3195 

3196 volume = self._get_volume_by_args(vol_name=volume_name) 

3197 uuid = volume['uuid'] 

3198 

3199 query = { 

3200 'owners': 'snapmirror_dependent', 

3201 } 

3202 

3203 if newer_than: 3203 ↛ 3204line 3203 didn't jump to line 3204 because the condition on line 3203 was never true

3204 query['create_time'] = '>' + newer_than 

3205 

3206 response = self.send_request( 

3207 f'/storage/volumes/{uuid}/snapshots/', 

3208 'get', query=query) 

3209 

3210 return [snapshot_info['name'] 

3211 for snapshot_info in response['records']] 

3212 

3213 @na_utils.trace 

3214 def abort_snapmirror_vol(self, source_vserver, source_volume, 

3215 dest_vserver, dest_volume, 

3216 clear_checkpoint=False): 

3217 """Stops ongoing transfers for a SnapMirror relationship.""" 

3218 self._abort_snapmirror(source_vserver=source_vserver, 

3219 dest_vserver=dest_vserver, 

3220 source_volume=source_volume, 

3221 dest_volume=dest_volume, 

3222 clear_checkpoint=clear_checkpoint) 

3223 

3224 @na_utils.trace 

3225 def _abort_snapmirror(self, source_path=None, dest_path=None, 

3226 source_vserver=None, dest_vserver=None, 

3227 source_volume=None, dest_volume=None, 

3228 clear_checkpoint=False): 

3229 """Stops ongoing transfers for a SnapMirror relationship.""" 

3230 

3231 snapmirror = self.get_snapmirrors( 

3232 source_path=source_path, 

3233 dest_path=dest_path, 

3234 source_vserver=source_vserver, 

3235 source_volume=source_volume, 

3236 dest_vserver=dest_vserver, 

3237 dest_volume=dest_volume) 

3238 if snapmirror: 3238 ↛ exitline 3238 didn't return from function '_abort_snapmirror' because the condition on line 3238 was always true

3239 snapmirror_uuid = snapmirror[0]['uuid'] 

3240 

3241 query = {'state': 'transferring'} 

3242 transfers = self.send_request('/snapmirror/relationships/' + 

3243 snapmirror_uuid + '/transfers/', 

3244 'get', query=query) 

3245 

3246 if not transfers.get('records'): 3246 ↛ 3247line 3246 didn't jump to line 3247 because the condition on line 3246 was never true

3247 raise netapp_api.api.NaApiError( 

3248 code=netapp_api.EREST_ENTRY_NOT_FOUND) 

3249 

3250 body = {'state': 'hard_aborted' if clear_checkpoint else 'aborted'} 

3251 

3252 for transfer in transfers['records']: 

3253 transfer_uuid = transfer['uuid'] 

3254 self.send_request('/snapmirror/relationships/' + 

3255 snapmirror_uuid + '/transfers/' + 

3256 transfer_uuid, 'patch', body=body) 

3257 

3258 @na_utils.trace 

3259 def delete_snapmirror_vol(self, source_vserver, source_volume, 

3260 dest_vserver, dest_volume): 

3261 """Destroys a SnapMirror relationship between volumes.""" 

3262 self._delete_snapmirror(source_vserver=source_vserver, 

3263 dest_vserver=dest_vserver, 

3264 source_volume=source_volume, 

3265 dest_volume=dest_volume) 

3266 

3267 @na_utils.trace 

3268 def _delete_snapmirror(self, source_vserver=None, source_volume=None, 

3269 dest_vserver=None, dest_volume=None): 

3270 """Deletes an SnapMirror relationship on destination.""" 

3271 query_uuid = {} 

3272 query_uuid['source.path'] = source_vserver + ':' + source_volume 

3273 query_uuid['destination.path'] = (dest_vserver + ':' + 

3274 dest_volume) 

3275 query_uuid['fields'] = 'uuid' 

3276 

3277 response = self.send_request('/snapmirror/relationships/', 'get', 

3278 query=query_uuid) 

3279 

3280 records = response.get('records') 

3281 

3282 if records: 

3283 # 'destination_only' deletes the snapmirror on destination 

3284 # but does not release it on source. 

3285 query_delete = {"destination_only": "true"} 

3286 

3287 snapmirror_uuid = records[0].get('uuid') 

3288 self.send_request('/snapmirror/relationships/' + 

3289 snapmirror_uuid, 'delete', 

3290 query=query_delete) 

3291 

3292 @na_utils.trace 

3293 def get_snapmirror_destinations(self, source_path=None, dest_path=None, 

3294 source_vserver=None, source_volume=None, 

3295 dest_vserver=None, dest_volume=None, 

3296 desired_attributes=None, 

3297 enable_tunneling=None): 

3298 """Gets one or more SnapMirror at source endpoint.""" 

3299 

3300 snapmirrors = self.get_snapmirrors( 

3301 source_path=source_path, 

3302 dest_path=dest_path, 

3303 source_vserver=source_vserver, 

3304 source_volume=source_volume, 

3305 dest_vserver=dest_vserver, 

3306 dest_volume=dest_volume, 

3307 # NOTE (nahimsouza): From ONTAP 9.12.1 the snapmirror destinations 

3308 # can only be retrieved with no tunneling. 

3309 enable_tunneling=False, 

3310 list_destinations_only=True) 

3311 

3312 return snapmirrors 

3313 

3314 @na_utils.trace 

3315 def release_snapmirror_vol(self, source_vserver, source_volume, 

3316 dest_vserver, dest_volume, 

3317 relationship_info_only=False): 

3318 """Removes a SnapMirror relationship on the source endpoint.""" 

3319 snapmirror_destinations_list = self.get_snapmirror_destinations( 

3320 source_vserver=source_vserver, 

3321 dest_vserver=dest_vserver, 

3322 source_volume=source_volume, 

3323 dest_volume=dest_volume, 

3324 desired_attributes=['relationship-id']) 

3325 

3326 if len(snapmirror_destinations_list) > 1: 3326 ↛ 3327line 3326 didn't jump to line 3327 because the condition on line 3326 was never true

3327 msg = ("Expected snapmirror relationship to be unique. " 

3328 "List returned: %s." % snapmirror_destinations_list) 

3329 raise exception.NetAppException(msg) 

3330 

3331 query = {} 

3332 if relationship_info_only: 3332 ↛ 3333line 3332 didn't jump to line 3333 because the condition on line 3332 was never true

3333 query["source_info_only"] = 'true' 

3334 else: 

3335 query["source_only"] = 'true' 

3336 

3337 if len(snapmirror_destinations_list) == 1: 3337 ↛ exitline 3337 didn't return from function 'release_snapmirror_vol' because the condition on line 3337 was always true

3338 uuid = snapmirror_destinations_list[0].get("uuid") 

3339 self.send_request(f'/snapmirror/relationships/{uuid}', 'delete', 

3340 query=query) 

3341 

3342 @na_utils.trace 

3343 def disable_fpolicy_policy(self, policy_name): 

3344 """Disables a specific policy. 

3345 

3346 :param policy_name: name of the policy to be disabled 

3347 """ 

3348 # Get SVM UUID. 

3349 query = { 

3350 'name': self.vserver, 

3351 'fields': 'uuid' 

3352 } 

3353 res = self.send_request('/svm/svms', 'get', query=query, 

3354 enable_tunneling=False) 

3355 if not res.get('records'): 

3356 msg = _('Vserver %s not found.') % self.vserver 

3357 raise exception.NetAppException(msg) 

3358 svm_id = res.get('records')[0]['uuid'] 

3359 try: 

3360 self.send_request(f'/protocols/fpolicy/{svm_id}/policies' 

3361 f'/{policy_name}', 'patch') 

3362 except netapp_api.api.NaApiError as e: 

3363 if (e.code in [netapp_api.EREST_POLICY_ALREADY_DISABLED, 3363 ↛ 3366line 3363 didn't jump to line 3366 because the condition on line 3363 was never true

3364 netapp_api.EREST_FPOLICY_MODIF_POLICY_DISABLED, 

3365 netapp_api.EREST_ENTRY_NOT_FOUND]): 

3366 msg = _("FPolicy policy %s not found or already disabled.") 

3367 LOG.debug(msg, policy_name) 

3368 else: 

3369 raise exception.NetAppException(message=e.message) 

3370 

3371 @na_utils.trace 

3372 def delete_fpolicy_scope(self, policy_name): 

3373 """Delete fpolicy scope 

3374 

3375 This method is not implemented since the REST API design does not allow 

3376 for the deletion of the scope only. When deleting a fpolicy policy, the 

3377 scope will be deleted along with it. 

3378 """ 

3379 pass 

3380 

3381 @na_utils.trace 

3382 def create_snapmirror_vol(self, source_vserver, source_volume, 

3383 destination_vserver, destination_volume, 

3384 relationship_type, schedule=None, 

3385 policy=na_utils.MIRROR_ALL_SNAP_POLICY): 

3386 """Creates a SnapMirror relationship between volumes.""" 

3387 self._create_snapmirror(source_vserver, destination_vserver, 

3388 source_volume=source_volume, 

3389 destination_volume=destination_volume, 

3390 schedule=schedule, policy=policy, 

3391 relationship_type=relationship_type) 

3392 

3393 @na_utils.trace 

3394 def _create_snapmirror(self, source_vserver, destination_vserver, 

3395 source_volume=None, destination_volume=None, 

3396 schedule=None, policy=None, 

3397 relationship_type=na_utils.DATA_PROTECTION_TYPE, 

3398 identity_preserve=None, max_transfer_rate=None): 

3399 """Creates a SnapMirror relationship.""" 

3400 

3401 # NOTE(nahimsouza): Extended Data Protection (XDP) SnapMirror 

3402 # relationships are the only relationship types that are supported 

3403 # through the REST API. The arg relationship_type was kept due to 

3404 # compatibility with ZAPI implementation. 

3405 

3406 # NOTE(nahimsouza): The argument identity_preserve is always None 

3407 # and it is not available on REST API. It was kept in the signature 

3408 # due to compatilbity with ZAPI implementation. 

3409 

3410 # TODO(nahimsouza): Tests what happens if volume is None. This happens 

3411 # when a snapmirror from SVM is created. 

3412 body = { 

3413 'source': { 

3414 'path': source_vserver + ':' + source_volume 

3415 }, 

3416 'destination': { 

3417 'path': destination_vserver + ':' + destination_volume 

3418 } 

3419 } 

3420 

3421 if schedule: 3421 ↛ 3422line 3421 didn't jump to line 3422 because the condition on line 3421 was never true

3422 body['transfer_schedule.name'] = schedule 

3423 

3424 if policy: 

3425 body['policy.name'] = policy 

3426 

3427 if max_transfer_rate is not None: 3427 ↛ 3428line 3427 didn't jump to line 3428 because the condition on line 3427 was never true

3428 body['throttle'] = max_transfer_rate 

3429 

3430 try: 

3431 self.send_request('/snapmirror/relationships/', 'post', body=body) 

3432 except netapp_api.api.NaApiError as e: 

3433 if e.code != netapp_api.EREST_ERELATION_EXISTS: 

3434 LOG.debug('Failed to create snapmirror. Error: %s. Code: %s', 

3435 e.message, e.code) 

3436 raise 

3437 

3438 def _set_snapmirror_state(self, state, source_path, destination_path, 

3439 source_vserver, source_volume, 

3440 destination_vserver, destination_volume, 

3441 wait_result=True, schedule=None): 

3442 """Change the snapmirror state between two volumes.""" 

3443 

3444 snapmirror = self.get_snapmirrors(source_path=source_path, 

3445 dest_path=destination_path, 

3446 source_vserver=source_vserver, 

3447 source_volume=source_volume, 

3448 dest_vserver=destination_vserver, 

3449 dest_volume=destination_volume) 

3450 

3451 if not snapmirror: 

3452 msg = _('Failed to get information about relationship between ' 

3453 'source %(src_vserver)s:%(src_volume)s and ' 

3454 'destination %(dst_vserver)s:%(dst_volume)s.') % { 

3455 'src_vserver': source_vserver, 

3456 'src_volume': source_volume, 

3457 'dst_vserver': destination_vserver, 

3458 'dst_volume': destination_volume} 

3459 

3460 raise na_utils.NetAppDriverException(msg) 

3461 

3462 uuid = snapmirror[0]['uuid'] 

3463 body = {} 

3464 if state: 3464 ↛ 3466line 3464 didn't jump to line 3466 because the condition on line 3464 was always true

3465 body.update({'state': state}) 

3466 if schedule: 3466 ↛ 3467line 3466 didn't jump to line 3467 because the condition on line 3466 was never true

3467 body.update({"transfer_schedule": {'name': schedule}}) 

3468 

3469 result = self.send_request(f'/snapmirror/relationships/{uuid}', 

3470 'patch', body=body, 

3471 wait_on_accepted=wait_result) 

3472 

3473 job = result['job'] 

3474 job_info = { 

3475 'operation-id': None, 

3476 'status': None, 

3477 'jobid': job.get('uuid'), 

3478 'error-code': None, 

3479 'error-message': None, 

3480 'relationship-uuid': uuid, 

3481 } 

3482 

3483 return job_info 

3484 

3485 @na_utils.trace 

3486 def initialize_snapmirror_vol(self, source_vserver, source_volume, 

3487 dest_vserver, dest_volume, 

3488 source_snapshot=None, 

3489 transfer_priority=None): 

3490 """Initializes a SnapMirror relationship between volumes.""" 

3491 return self._initialize_snapmirror( 

3492 source_vserver=source_vserver, dest_vserver=dest_vserver, 

3493 source_volume=source_volume, dest_volume=dest_volume, 

3494 source_snapshot=source_snapshot, 

3495 transfer_priority=transfer_priority) 

3496 

3497 @na_utils.trace 

3498 def _initialize_snapmirror(self, source_path=None, dest_path=None, 

3499 source_vserver=None, dest_vserver=None, 

3500 source_volume=None, dest_volume=None, 

3501 source_snapshot=None, transfer_priority=None): 

3502 """Initializes a SnapMirror relationship.""" 

3503 

3504 # NOTE(nahimsouza): The args source_snapshot and transfer_priority are 

3505 # always None and they are not available on REST API, they were 

3506 # kept in the signature due to compatilbity with ZAPI implementation. 

3507 

3508 return self._set_snapmirror_state( 

3509 'snapmirrored', source_path, dest_path, 

3510 source_vserver, source_volume, 

3511 dest_vserver, dest_volume, wait_result=False) 

3512 

3513 @na_utils.trace 

3514 def modify_snapmirror_vol(self, source_vserver, source_volume, 

3515 dest_vserver, dest_volume, 

3516 schedule=None, policy=None, tries=None, 

3517 max_transfer_rate=None): 

3518 """Modifies a SnapMirror relationship between volumes.""" 

3519 return self._modify_snapmirror( 

3520 source_vserver=source_vserver, dest_vserver=dest_vserver, 

3521 source_volume=source_volume, dest_volume=dest_volume, 

3522 schedule=schedule) 

3523 

3524 @na_utils.trace 

3525 def _modify_snapmirror(self, source_path=None, dest_path=None, 

3526 source_vserver=None, dest_vserver=None, 

3527 source_volume=None, dest_volume=None, 

3528 schedule=None): 

3529 """Modifies a SnapMirror relationship.""" 

3530 return self._set_snapmirror_state( 

3531 None, source_path, dest_path, 

3532 source_vserver, source_volume, 

3533 dest_vserver, dest_volume, wait_result=False, 

3534 schedule=schedule) 

3535 

3536 @na_utils.trace 

3537 def create_volume_clone(self, volume_name, parent_volume_name, 

3538 parent_snapshot_name=None, 

3539 qos_policy_group=None, 

3540 adaptive_qos_policy_group=None, 

3541 mount_point_name=None, 

3542 **options): 

3543 """Create volume clone in the same aggregate as parent volume.""" 

3544 

3545 body = { 

3546 'name': volume_name, 

3547 'clone.parent_volume.name': parent_volume_name, 

3548 'clone.parent_snapshot.name': parent_snapshot_name, 

3549 'nas.path': '/%s' % (mount_point_name or volume_name), 

3550 'clone.is_flexclone': 'true', 

3551 'svm.name': self.connection.get_vserver(), 

3552 } 

3553 

3554 self.send_request('/storage/volumes', 'post', body=body) 

3555 

3556 # NOTE(nahimsouza): QoS policy can not be set during the cloning 

3557 # process, so we need to make a separate request. 

3558 if qos_policy_group is not None: 

3559 volume = self._get_volume_by_args(vol_name=volume_name) 

3560 uuid = volume['uuid'] 

3561 body = { 

3562 'qos.policy.name': qos_policy_group, 

3563 } 

3564 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body) 

3565 

3566 if adaptive_qos_policy_group is not None: 

3567 self.set_qos_adaptive_policy_group_for_volume( 

3568 volume_name, adaptive_qos_policy_group) 

3569 

3570 @na_utils.trace 

3571 def quiesce_snapmirror_vol(self, source_vserver, source_volume, 

3572 dest_vserver, dest_volume): 

3573 """Disables future transfers to a SnapMirror destination.""" 

3574 self._quiesce_snapmirror(source_vserver=source_vserver, 

3575 dest_vserver=dest_vserver, 

3576 source_volume=source_volume, 

3577 dest_volume=dest_volume) 

3578 

3579 @na_utils.trace 

3580 def _quiesce_snapmirror(self, source_path=None, dest_path=None, 

3581 source_vserver=None, dest_vserver=None, 

3582 source_volume=None, dest_volume=None): 

3583 """Disables future transfers to a SnapMirror destination.""" 

3584 

3585 snapmirror = self.get_snapmirrors( 

3586 source_path=source_path, 

3587 dest_path=dest_path, 

3588 source_vserver=source_vserver, 

3589 source_volume=source_volume, 

3590 dest_vserver=dest_vserver, 

3591 dest_volume=dest_volume) 

3592 

3593 if snapmirror: 3593 ↛ exitline 3593 didn't return from function '_quiesce_snapmirror' because the condition on line 3593 was always true

3594 uuid = snapmirror[0]['uuid'] 

3595 body = {'state': 'paused'} 

3596 

3597 self.send_request(f'/snapmirror/relationships/{uuid}', 'patch', 

3598 body=body) 

3599 

3600 @na_utils.trace 

3601 def break_snapmirror_vol(self, source_vserver, source_volume, 

3602 dest_vserver, dest_volume): 

3603 """Breaks a data protection SnapMirror relationship.""" 

3604 self._break_snapmirror(source_vserver=source_vserver, 

3605 dest_vserver=dest_vserver, 

3606 source_volume=source_volume, 

3607 dest_volume=dest_volume) 

3608 

3609 @na_utils.trace 

3610 def _break_snapmirror(self, source_path=None, dest_path=None, 

3611 source_vserver=None, dest_vserver=None, 

3612 source_volume=None, dest_volume=None): 

3613 """Breaks a data protection SnapMirror relationship.""" 

3614 

3615 interval = 2 

3616 retries = (10 / interval) 

3617 

3618 @utils.retry(netapp_api.NaRetryableError, interval=interval, 

3619 retries=retries, backoff_rate=1) 

3620 def _waiter(): 

3621 snapmirror = self.get_snapmirrors( 

3622 source_path=source_path, 

3623 dest_path=dest_path, 

3624 source_vserver=source_vserver, 

3625 source_volume=source_volume, 

3626 dest_vserver=dest_vserver, 

3627 dest_volume=dest_volume) 

3628 

3629 snapmirror_state = snapmirror[0].get('transferring-state') 

3630 if snapmirror_state == 'success': 

3631 uuid = snapmirror[0]['uuid'] 

3632 body = {'state': 'broken_off'} 

3633 self.send_request(f'/snapmirror/relationships/{uuid}', 'patch', 

3634 body=body) 

3635 return 

3636 else: 

3637 message = 'Waiting for transfer state to be SUCCESS.' 

3638 code = '' 

3639 raise netapp_api.NaRetryableError(message=message, code=code) 

3640 

3641 try: 

3642 return _waiter() 

3643 except netapp_api.NaRetryableError: 

3644 msg = _("Transfer state did not reach the expected state. Retries " 

3645 "exhausted. Aborting.") 

3646 raise na_utils.NetAppDriverException(msg) 

3647 

3648 @na_utils.trace 

3649 def resume_snapmirror_vol(self, source_vserver, source_volume, 

3650 dest_vserver, dest_volume): 

3651 """Resume a SnapMirror relationship if it is quiesced.""" 

3652 self._resume_snapmirror(source_vserver=source_vserver, 

3653 dest_vserver=dest_vserver, 

3654 source_volume=source_volume, 

3655 dest_volume=dest_volume) 

3656 

3657 @na_utils.trace 

3658 def resync_snapmirror_vol(self, source_vserver, source_volume, 

3659 dest_vserver, dest_volume): 

3660 """Resync a SnapMirror relationship between volumes.""" 

3661 self._resync_snapmirror(source_vserver=source_vserver, 

3662 dest_vserver=dest_vserver, 

3663 source_volume=source_volume, 

3664 dest_volume=dest_volume) 

3665 

3666 @na_utils.trace 

3667 def _resume_snapmirror(self, source_path=None, dest_path=None, 

3668 source_vserver=None, dest_vserver=None, 

3669 source_volume=None, dest_volume=None): 

3670 """Resume a SnapMirror relationship if it is quiesced.""" 

3671 response = self.get_snapmirrors(source_path=source_path, 

3672 dest_path=dest_path, 

3673 source_vserver=source_vserver, 

3674 dest_vserver=dest_vserver, 

3675 source_volume=source_volume, 

3676 dest_volume=dest_volume) 

3677 

3678 if not response: 3678 ↛ 3681line 3678 didn't jump to line 3681 because the condition on line 3678 was never true

3679 # NOTE(nahimsouza): As ZAPI returns this error code, it was kept 

3680 # to avoid changes in the layer above. 

3681 raise netapp_api.api.NaApiError( 

3682 code=netapp_api.api.EOBJECTNOTFOUND) 

3683 

3684 snapmirror_uuid = response[0]['uuid'] 

3685 snapmirror_policy = response[0]['policy-type'] 

3686 

3687 body_resync = {} 

3688 if snapmirror_policy == 'async': 

3689 body_resync['state'] = 'snapmirrored' 

3690 elif snapmirror_policy == 'sync': 3690 ↛ 3693line 3690 didn't jump to line 3693 because the condition on line 3690 was always true

3691 body_resync['state'] = 'in_sync' 

3692 

3693 self.send_request('/snapmirror/relationships/' + 

3694 snapmirror_uuid, 'patch', 

3695 body=body_resync, wait_on_accepted=False) 

3696 

3697 @na_utils.trace 

3698 def _resync_snapmirror(self, source_path=None, dest_path=None, 

3699 source_vserver=None, dest_vserver=None, 

3700 source_volume=None, dest_volume=None): 

3701 """Resync a SnapMirror relationship.""" 

3702 # We reuse the resume operation for resync since both are handled in 

3703 # the same way in the REST API, by setting the snapmirror relationship 

3704 # to the snapmirrored state. 

3705 self._resume_snapmirror(source_path, dest_path, 

3706 source_vserver, dest_vserver, 

3707 source_volume, dest_volume) 

3708 

3709 @na_utils.trace 

3710 def add_nfs_export_rule(self, policy_name, client_match, readonly, 

3711 auth_methods): 

3712 """Add rule to NFS export policy.""" 

3713 rule_indices = self._get_nfs_export_rule_indices(policy_name, 

3714 client_match) 

3715 if not rule_indices: 

3716 self._add_nfs_export_rule(policy_name, client_match, readonly, 

3717 auth_methods) 

3718 else: 

3719 # Update first rule and delete the rest 

3720 self._update_nfs_export_rule( 

3721 policy_name, client_match, readonly, rule_indices.pop(0), 

3722 auth_methods) 

3723 self._remove_nfs_export_rules(policy_name, rule_indices) 

3724 

3725 @na_utils.trace 

3726 def set_qos_policy_group_for_volume(self, volume_name, 

3727 qos_policy_group_name): 

3728 """Set QoS policy group for volume.""" 

3729 volume = self._get_volume_by_args(vol_name=volume_name) 

3730 uuid = volume['uuid'] 

3731 

3732 body = { 

3733 'qos.policy.name': qos_policy_group_name 

3734 } 

3735 

3736 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body) 

3737 

3738 @na_utils.trace 

3739 def update_snapmirror_vol(self, source_vserver, source_volume, 

3740 dest_vserver, dest_volume): 

3741 """Schedules a snapmirror update between volumes.""" 

3742 self._update_snapmirror(source_vserver=source_vserver, 

3743 dest_vserver=dest_vserver, 

3744 source_volume=source_volume, 

3745 dest_volume=dest_volume) 

3746 

3747 @na_utils.trace 

3748 def _update_snapmirror(self, source_path=None, dest_path=None, 

3749 source_vserver=None, dest_vserver=None, 

3750 source_volume=None, dest_volume=None): 

3751 """Update a snapmirror relationship asynchronously.""" 

3752 snapmirrors = self.get_snapmirrors(source_path=source_path, 

3753 dest_path=dest_path, 

3754 source_vserver=source_vserver, 

3755 dest_vserver=dest_vserver, 

3756 source_volume=source_volume, 

3757 dest_volume=dest_volume) 

3758 

3759 if not snapmirrors: 

3760 msg = _('Failed to get snapmirror relationship information') 

3761 raise na_utils.NetAppDriverException(msg) 

3762 

3763 snapmirror_uuid = snapmirrors[0]['uuid'] 

3764 

3765 # NOTE(nahimsouza): A POST with an empty body starts the update 

3766 # snapmirror operation. 

3767 try: 

3768 self.send_request('/snapmirror/relationships/' + 

3769 snapmirror_uuid + '/transfers/', 'post', 

3770 wait_on_accepted=False) 

3771 except netapp_api.api.NaApiError as e: 

3772 transfer_in_progress = 'Another transfer is in progress' 

3773 

3774 if (e.code == netapp_api.EREST_SNAPMIRROR_NOT_INITIALIZED and 

3775 transfer_in_progress in e.message): 

3776 # NOTE (nahimsouza): Raise this message to keep compatibility 

3777 # with ZAPI and avoid change the driver layer. 

3778 raise netapp_api.api.NaApiError(message='not initialized', 

3779 code=netapp_api.api.EAPIERROR) 

3780 

3781 if not (e.code == netapp_api.EREST_UPDATE_SNAPMIRROR_FAILED 3781 ↛ exitline 3781 didn't return from function '_update_snapmirror' because the condition on line 3781 was always true

3782 and transfer_in_progress in e.message): 

3783 raise 

3784 

3785 @na_utils.trace 

3786 def get_cluster_name(self): 

3787 """Gets cluster name.""" 

3788 

3789 result = self.send_request('/cluster', 'get', enable_tunneling=False) 

3790 return result.get('name') 

3791 

3792 @na_utils.trace 

3793 def check_volume_clone_split_completed(self, volume_name): 

3794 """Check if volume clone split operation already finished.""" 

3795 volume = self._get_volume_by_args(vol_name=volume_name, 

3796 fields='clone.is_flexclone') 

3797 

3798 return volume['clone']['is_flexclone'] is False 

3799 

3800 @na_utils.trace 

3801 def rehost_volume(self, volume_name, vserver, destination_vserver): 

3802 """Rehosts a volume from one Vserver into another Vserver. 

3803 

3804 :param volume_name: Name of the FlexVol to be rehosted. 

3805 :param vserver: Source Vserver name to which target volume belongs. 

3806 :param destination_vserver: Destination Vserver name where target 

3807 volume must reside after successful volume rehost operation. 

3808 """ 

3809 # TODO(raffaelacunha): As soon NetApp REST API supports "volume_rehost" 

3810 # the current endpoint (using CLI passthrough) must be replaced. 

3811 body = { 

3812 "vserver": vserver, 

3813 "volume": volume_name, 

3814 "destination_vserver": destination_vserver 

3815 } 

3816 self.send_request('/private/cli/volume/rehost', 'post', body=body) 

3817 

3818 @na_utils.trace 

3819 def get_net_options(self): 

3820 """Retrives the IPv6 support.""" 

3821 

3822 return { 

3823 'ipv6-enabled': True, 

3824 } 

3825 

3826 @na_utils.trace 

3827 def set_qos_adaptive_policy_group_for_volume(self, volume_name, 

3828 qos_policy_group_name): 

3829 """Set QoS adaptive policy group for volume.""" 

3830 

3831 # NOTE(renanpiranguinho): For REST API, adaptive QoS is set the same 

3832 # way as normal QoS. 

3833 self.set_qos_policy_group_for_volume(volume_name, 

3834 qos_policy_group_name) 

3835 

3836 def get_performance_counter_info(self, object_name, counter_name): 

3837 """Gets info about one or more Data ONTAP performance counters.""" 

3838 

3839 # NOTE(nahimsouza): This conversion is nedeed because different names 

3840 # are used in ZAPI and we want to avoid changes in the driver for now. 

3841 rest_counter_names = { 

3842 'domain_busy': 'domain_busy_percent', 

3843 'processor_elapsed_time': 'elapsed_time', 

3844 'avg_processor_busy': 'average_processor_busy_percent', 

3845 } 

3846 

3847 rest_counter_name = counter_name 

3848 if counter_name in rest_counter_names: 

3849 rest_counter_name = rest_counter_names[counter_name] 

3850 

3851 # Get counter table info 

3852 query = { 

3853 'counter_schemas.name': rest_counter_name, 

3854 'fields': 'counter_schemas.*' 

3855 } 

3856 

3857 try: 

3858 table = self.send_request( 

3859 f'/cluster/counter/tables/{object_name}', 

3860 'get', query=query) 

3861 

3862 name = counter_name # use the original name (ZAPI compatible) 

3863 base_counter = table['counter_schemas'][0]['denominator']['name'] 

3864 

3865 query = { 

3866 'counters.name': rest_counter_name, 

3867 'fields': 'counters.*' 

3868 } 

3869 

3870 response = self.send_request( 

3871 f'/cluster/counter/tables/{object_name}/rows', 

3872 'get', query=query, enable_tunneling=False) 

3873 

3874 table_rows = response.get('records', []) 

3875 labels = [] 

3876 if len(table_rows) != 0: 

3877 labels = table_rows[0]['counters'][0].get('labels', []) 

3878 

3879 # NOTE(nahimsouza): Values have a different format on REST API 

3880 # and we want to keep compatibility with ZAPI for a while 

3881 if object_name == 'wafl' and counter_name == 'cp_phase_times': 

3882 # discard the prefix 'cp_' 

3883 labels = [label[3:] for label in labels] 

3884 

3885 return { 

3886 'name': name, 

3887 'labels': labels, 

3888 'base-counter': base_counter, 

3889 } 

3890 except netapp_api.api.NaApiError: 

3891 raise exception.NotFound(_('Counter %s not found') % counter_name) 

3892 

3893 def get_performance_instance_uuids(self, object_name, node_name): 

3894 """Get UUIDs of performance instances for a cluster node.""" 

3895 

3896 query = { 

3897 'id': node_name + ':*', 

3898 } 

3899 

3900 response = self.send_request( 

3901 f'/cluster/counter/tables/{object_name}/rows', 

3902 'get', query=query, enable_tunneling=False) 

3903 

3904 records = response.get('records', []) 

3905 

3906 uuids = [] 

3907 for record in records: 

3908 uuids.append(record['id']) 

3909 

3910 return uuids 

3911 

3912 def get_performance_counters(self, object_name, instance_uuids, 

3913 counter_names): 

3914 """Gets more cDOT performance counters.""" 

3915 

3916 # NOTE(nahimsouza): This conversion is nedeed because different names 

3917 # are used in ZAPI and we want to avoid changes in the driver for now. 

3918 rest_counter_names = { 

3919 'domain_busy': 'domain_busy_percent', 

3920 'processor_elapsed_time': 'elapsed_time', 

3921 'avg_processor_busy': 'average_processor_busy_percent', 

3922 } 

3923 

3924 zapi_counter_names = { 

3925 'domain_busy_percent': 'domain_busy', 

3926 'elapsed_time': 'processor_elapsed_time', 

3927 'average_processor_busy_percent': 'avg_processor_busy', 

3928 } 

3929 

3930 for i in range(len(counter_names)): 

3931 if counter_names[i] in rest_counter_names: 3931 ↛ 3930line 3931 didn't jump to line 3930 because the condition on line 3931 was always true

3932 counter_names[i] = rest_counter_names[counter_names[i]] 

3933 

3934 query = { 

3935 'id': '|'.join(instance_uuids), 

3936 'counters.name': '|'.join(counter_names), 

3937 'fields': 'id,counter_table.name,counters.*', 

3938 } 

3939 

3940 response = self.send_request( 

3941 f'/cluster/counter/tables/{object_name}/rows', 

3942 'get', query=query) 

3943 

3944 counter_data = [] 

3945 for record in response.get('records', []): 

3946 for counter in record['counters']: 

3947 

3948 counter_name = counter['name'] 

3949 

3950 # Reverts the name conversion 

3951 if counter_name in zapi_counter_names: 3951 ↛ 3954line 3951 didn't jump to line 3954 because the condition on line 3951 was always true

3952 counter_name = zapi_counter_names[counter_name] 

3953 

3954 counter_value = '' 

3955 if counter.get('value'): 

3956 counter_value = counter.get('value') 

3957 elif counter.get('values'): 3957 ↛ 3963line 3957 didn't jump to line 3963 because the condition on line 3957 was always true

3958 # NOTE(nahimsouza): Conversion made to keep compatibility 

3959 # with old ZAPI format 

3960 values = counter.get('values') 

3961 counter_value = ','.join([str(v) for v in values]) 

3962 

3963 counter_data.append({ 

3964 'instance-name': record['counter_table']['name'], 

3965 'instance-uuid': record['id'], 

3966 'node-name': record['id'].split(':')[0], 

3967 'timestamp': int(time.time()), 

3968 counter_name: counter_value, 

3969 }) 

3970 

3971 return counter_data 

3972 

3973 @na_utils.trace 

3974 def _list_vservers(self): 

3975 """Get the names of vservers present""" 

3976 query = { 

3977 'fields': 'name', 

3978 } 

3979 response = self.send_request('/svm/svms', 'get', query=query, 

3980 enable_tunneling=False) 

3981 

3982 return [svm['name'] for svm in response.get('records', [])] 

3983 

3984 @na_utils.trace 

3985 def _get_ems_log_destination_vserver(self): 

3986 """Returns the best vserver destination for EMS messages.""" 

3987 

3988 # NOTE(nahimsouza): Differently from ZAPI, only 'data' SVMs can be 

3989 # managed by the SVM REST APIs - that's why the vserver type is not 

3990 # specified. 

3991 vservers = self._list_vservers() 

3992 

3993 if vservers: 

3994 return vservers[0] 

3995 

3996 raise exception.NotFound("No Vserver found to receive EMS messages.") 

3997 

3998 @na_utils.trace 

3999 def send_ems_log_message(self, message_dict): 

4000 """Sends a message to the Data ONTAP EMS log.""" 

4001 

4002 body = { 

4003 'computer_name': message_dict['computer-name'], 

4004 'event_source': message_dict['event-source'], 

4005 'app_version': message_dict['app-version'], 

4006 'category': message_dict['category'], 

4007 'severity': 'notice', 

4008 'autosupport_required': message_dict['auto-support'] == 'true', 

4009 'event_id': message_dict['event-id'], 

4010 'event_description': message_dict['event-description'], 

4011 } 

4012 

4013 bkp_connection = copy.copy(self.connection) 

4014 bkp_timeout = self.connection.get_timeout() 

4015 bkp_vserver = self.vserver 

4016 

4017 self.connection.set_timeout(25) 

4018 try: 

4019 # TODO(nahimsouza): Vserver is being set to replicate the ZAPI 

4020 # behavior, but need to check if this could be removed in REST API 

4021 self.connection.set_vserver( 

4022 self._get_ems_log_destination_vserver()) 

4023 self.send_request('/support/ems/application-logs', 

4024 'post', body=body) 

4025 LOG.debug('EMS executed successfully.') 

4026 except netapp_api.api.NaApiError as e: 

4027 LOG.warning('Failed to invoke EMS. %s', e) 

4028 finally: 

4029 # Restores the data 

4030 timeout = ( 

4031 bkp_timeout if bkp_timeout is not None else DEFAULT_TIMEOUT) 

4032 self.connection = copy.copy(bkp_connection) 

4033 self.connection.set_timeout(timeout) 

4034 self.connection.set_vserver(bkp_vserver) 

4035 

4036 @na_utils.trace 

4037 def _get_deleted_nfs_export_policies(self): 

4038 """Get soft deleted NFS export policies.""" 

4039 query = { 

4040 'name': DELETED_PREFIX + '*', 

4041 'fields': 'name,svm.name', 

4042 } 

4043 

4044 response = self.send_request('/protocols/nfs/export-policies', 

4045 'get', query=query) 

4046 

4047 policy_map = {} 

4048 for record in response['records']: 

4049 vserver = record['svm']['name'] 

4050 policies = policy_map.get(vserver, []) 

4051 policies.append(record['name']) 

4052 policy_map[vserver] = policies 

4053 

4054 return policy_map 

4055 

4056 @na_utils.trace 

4057 def prune_deleted_nfs_export_policies(self): 

4058 """Delete export policies that were marked for deletion.""" 

4059 deleted_policy_map = self._get_deleted_nfs_export_policies() 

4060 for vserver in deleted_policy_map: 

4061 client = copy.copy(self) 

4062 client.connection = copy.copy(self.connection) 

4063 client.connection.set_vserver(vserver) 

4064 for policy in deleted_policy_map[vserver]: 

4065 try: 

4066 client.delete_nfs_export_policy(policy) 

4067 except netapp_api.api.NaApiError: 

4068 LOG.debug('Could not delete export policy %s.', policy) 

4069 

4070 @na_utils.trace 

4071 def get_nfs_config_default(self, desired_args=None): 

4072 """Gets the default NFS config with the desired params""" 

4073 

4074 query = {'fields': 'transport.*'} 

4075 

4076 if self.vserver: 4076 ↛ 4077line 4076 didn't jump to line 4077 because the condition on line 4076 was never true

4077 query['svm.name'] = self.vserver 

4078 

4079 response = self.send_request('/protocols/nfs/services/', 

4080 'get', query=query) 

4081 

4082 # NOTE(nahimsouza): Default values to replicate ZAPI behavior when 

4083 # response is empty. Also, REST API does not have an equivalent to 

4084 # 'udp-max-xfer-size', so the default is always returned. 

4085 nfs_info = { 

4086 'tcp-max-xfer-size': str(DEFAULT_TCP_MAX_XFER_SIZE), 

4087 'udp-max-xfer-size': str(DEFAULT_UDP_MAX_XFER_SIZE), 

4088 } 

4089 records = response.get('records', []) 

4090 if records: 4090 ↛ 4094line 4090 didn't jump to line 4094 because the condition on line 4090 was always true

4091 nfs_info['tcp-max-xfer-size'] = ( 

4092 str(records[0]['transport']['tcp_max_transfer_size'])) 

4093 

4094 return nfs_info 

4095 

4096 @na_utils.trace 

4097 def create_kerberos_realm(self, security_service): 

4098 """Creates Kerberos realm on cluster.""" 

4099 

4100 body = { 

4101 'comment': '', 

4102 'kdc.ip': security_service['server'], 

4103 'kdc.port': '88', 

4104 'kdc.vendor': 'other', 

4105 'name': security_service['domain'].upper(), 

4106 } 

4107 try: 

4108 self.send_request('/protocols/nfs/kerberos/realms', 'post', 

4109 body=body) 

4110 except netapp_api.api.NaApiError as e: 

4111 if e.code == netapp_api.EREST_DUPLICATE_ENTRY: 

4112 LOG.debug('Kerberos realm config already exists.') 

4113 else: 

4114 msg = _('Failed to create Kerberos realm. %s') 

4115 raise exception.NetAppException(msg % e.message) 

4116 

4117 @na_utils.trace 

4118 def configure_kerberos(self, security_service, vserver_name): 

4119 """Configures Kerberos for NFS on Vserver.""" 

4120 

4121 self.configure_dns(security_service, vserver_name=vserver_name) 

4122 spn = self._get_kerberos_service_principal_name( 

4123 security_service, vserver_name) 

4124 

4125 lifs = self.get_network_interfaces() 

4126 

4127 if not lifs: 

4128 msg = _("Cannot set up Kerberos. There are no LIFs configured.") 

4129 raise exception.NetAppException(msg) 

4130 

4131 for lif in lifs: 

4132 body = { 

4133 'password': security_service['password'], 

4134 'user': security_service['user'], 

4135 'interface.name': lif['interface-name'], 

4136 'enabled': True, 

4137 'spn': spn 

4138 } 

4139 

4140 interface_uuid = lif['uuid'] 

4141 

4142 self.send_request( 

4143 f'/protocols/nfs/kerberos/interfaces/{interface_uuid}', 

4144 'patch', body=body) 

4145 

4146 @na_utils.trace 

4147 def _get_kerberos_service_principal_name(self, security_service, 

4148 vserver_name): 

4149 """Build Kerberos service principal name.""" 

4150 return ('nfs/' + vserver_name.replace('_', '-') + '.' + 

4151 security_service['domain'] + '@' + 

4152 security_service['domain'].upper()) 

4153 

4154 @na_utils.trace 

4155 def _get_cifs_server_name(self, vserver_name): 

4156 """Build CIFS server name.""" 

4157 # 'cifs-server' is CIFS Server NetBIOS Name, max length is 15. 

4158 # Should be unique within each domain (data['domain']). 

4159 # Cut to 15 char with begin and end, attempt to make valid DNS hostname 

4160 cifs_server = (vserver_name[0:8] + 

4161 '-' + 

4162 vserver_name[-6:]).replace('_', '-').upper() 

4163 return cifs_server 

4164 

4165 @na_utils.trace 

4166 def configure_ldap(self, security_service, timeout=30, vserver_name=None): 

4167 """Configures LDAP on Vserver.""" 

4168 self._create_ldap_client(security_service, vserver_name=vserver_name) 

4169 

4170 @na_utils.trace 

4171 def configure_active_directory(self, security_service, 

4172 vserver_name, aes_encryption): 

4173 """Configures AD on Vserver.""" 

4174 self.configure_dns(security_service, vserver_name=vserver_name) 

4175 self.configure_cifs_aes_encryption(vserver_name, aes_encryption) 

4176 self.set_preferred_dc(security_service, vserver_name) 

4177 

4178 cifs_server = self._get_cifs_server_name(vserver_name) 

4179 

4180 body = { 

4181 'ad_domain.user': security_service['user'], 

4182 'ad_domain.password': security_service['password'], 

4183 'force': 'true', 

4184 'name': cifs_server, 

4185 'ad_domain.fqdn': security_service['domain'], 

4186 } 

4187 

4188 if security_service['ou'] is not None: 4188 ↛ 4191line 4188 didn't jump to line 4191 because the condition on line 4188 was always true

4189 body['ad_domain.organizational_unit'] = security_service['ou'] 

4190 

4191 try: 

4192 LOG.debug("Trying to setup CIFS server with data: %s", body) 

4193 self.send_request('/protocols/cifs/services', 'post', body=body) 

4194 except netapp_api.api.NaApiError as e: 

4195 credential_msg = "could not authenticate" 

4196 privilege_msg = "insufficient access" 

4197 if (e.code == netapp_api.api.EAPIERROR and ( 

4198 credential_msg in e.message.lower() or 

4199 privilege_msg in e.message.lower())): 

4200 auth_msg = _("Failed to create CIFS server entry. " 

4201 "Please double check your user credentials " 

4202 "or privileges. %s") 

4203 raise exception.SecurityServiceFailedAuth(auth_msg % e.message) 

4204 msg = _("Failed to create CIFS server entry. %s") 

4205 raise exception.NetAppException(msg % e.message) 

4206 

4207 @na_utils.trace 

4208 def _get_unique_svm_by_name(self, vserver_name=None): 

4209 """Get the specified SVM UUID.""" 

4210 query = { 

4211 'name': vserver_name if vserver_name else self.vserver, 

4212 'fields': 'uuid' 

4213 } 

4214 response = self.send_request('/svm/svms', 'get', query=query) 

4215 if not response.get('records'): 

4216 msg = ('Vserver %s not found.') % self.vserver 

4217 raise exception.NetAppException(msg) 

4218 svm_uuid = response['records'][0]['uuid'] 

4219 return svm_uuid 

4220 

4221 @na_utils.trace 

4222 def get_dns_config(self, vserver_name=None): 

4223 """Read DNS servers and domains currently configured in the vserver·""" 

4224 svm_uuid = self._get_unique_svm_by_name(vserver_name) 

4225 try: 

4226 result = self.send_request(f'/name-services/dns/{svm_uuid}', 'get') 

4227 except netapp_api.api.NaApiError as e: 

4228 if e.code == netapp_api.EREST_ENTRY_NOT_FOUND: 4228 ↛ 4229line 4228 didn't jump to line 4229 because the condition on line 4228 was never true

4229 return {} 

4230 msg = ("Failed to retrieve DNS configuration. %s") 

4231 raise exception.NetAppException(msg % e.message) 

4232 

4233 dns_config = {} 

4234 dns_info = result.get('dynamic_dns', {}) 

4235 

4236 dns_config['dns-state'] = dns_info.get('enabled', '') 

4237 dns_config['domains'] = result.get('domains', []) 

4238 dns_config['dns-ips'] = result.get('servers', []) 

4239 

4240 return dns_config 

4241 

4242 @na_utils.trace 

4243 def configure_dns(self, security_service, vserver_name=None): 

4244 """Configure DNS address and servers for a vserver.""" 

4245 body = { 

4246 'domains': [], 

4247 'servers': [] 

4248 } 

4249 # NOTE(dviroel): Read the current dns configuration and merge with the 

4250 # new one. This scenario is expected when 2 security services provide 

4251 # a DNS configuration, like 'active_directory' and 'ldap'. 

4252 current_dns_config = self.get_dns_config(vserver_name=vserver_name) 

4253 domains = set(current_dns_config.get('domains', [])) 

4254 dns_ips = set(current_dns_config.get('dns-ips', [])) 

4255 

4256 svm_uuid = self._get_unique_svm_by_name(vserver_name) 

4257 

4258 domains.add(security_service['domain']) 

4259 for domain in domains: 

4260 body['domains'].append(domain) 

4261 

4262 for dns_ip in security_service['dns_ip'].split(','): 

4263 dns_ips.add(dns_ip.strip()) 

4264 body['servers'] = [] 

4265 for dns_ip in sorted(dns_ips): 

4266 body['servers'].append(dns_ip) 

4267 

4268 try: 

4269 if current_dns_config: 

4270 self.send_request(f'/name-services/dns/{svm_uuid}', 

4271 'patch', body=body) 

4272 else: 

4273 self.send_request('/name-services/dns', 'post', body=body) 

4274 except netapp_api.api.NaApiError as e: 

4275 msg = _("Failed to configure DNS. %s") 

4276 raise exception.NetAppException(msg % e.message) 

4277 

4278 @na_utils.trace 

4279 def setup_security_services(self, security_services, vserver_client, 

4280 vserver_name, aes_encryption, timeout=30): 

4281 """Setup SVM security services.""" 

4282 body = { 

4283 'nsswitch.namemap': ['ldap', 'files'], 

4284 'nsswitch.group': ['ldap', 'files'], 

4285 'nsswitch.netgroup': ['ldap', 'files'], 

4286 'nsswitch.passwd': ['ldap', 'files'], 

4287 } 

4288 

4289 svm_uuid = self._get_unique_svm_by_name(vserver_name) 

4290 

4291 self.send_request(f'/svm/svms/{svm_uuid}', 'patch', body=body) 

4292 

4293 for security_service in security_services: 

4294 if security_service['type'].lower() == 'ldap': 

4295 vserver_client.configure_ldap(security_service, 

4296 timeout=timeout, 

4297 vserver_name=vserver_name) 

4298 

4299 elif security_service['type'].lower() == 'active_directory': 

4300 vserver_client.configure_active_directory(security_service, 

4301 vserver_name, 

4302 aes_encryption) 

4303 vserver_client.configure_cifs_options(security_service) 

4304 

4305 elif security_service['type'].lower() == 'kerberos': 4305 ↛ 4311line 4305 didn't jump to line 4311 because the condition on line 4305 was always true

4306 vserver_client.create_kerberos_realm(security_service) 

4307 vserver_client.configure_kerberos(security_service, 

4308 vserver_name) 

4309 

4310 else: 

4311 msg = _('Unsupported security service type %s for ' 

4312 'Data ONTAP driver') 

4313 raise exception.NetAppException(msg % security_service['type']) 

4314 

4315 @na_utils.trace 

4316 def _create_ldap_client(self, security_service, vserver_name=None): 

4317 ad_domain = security_service.get('domain') 

4318 ldap_servers = security_service.get('server') 

4319 bind_dn = security_service.get('user') 

4320 ldap_schema = 'RFC-2307' 

4321 

4322 if ad_domain: 

4323 if ldap_servers: 

4324 msg = _("LDAP client cannot be configured with both 'server' " 

4325 "and 'domain' parameters. Use 'server' for Linux/Unix " 

4326 "LDAP servers or 'domain' for Active Directory LDAP " 

4327 "servers.") 

4328 LOG.exception(msg) 

4329 raise exception.NetAppException(msg) 

4330 # RFC2307bis, for MS Active Directory LDAP server 

4331 ldap_schema = 'MS-AD-BIS' 

4332 bind_dn = (security_service.get('user') + '@' + ad_domain) 

4333 else: 

4334 if not ldap_servers: 

4335 msg = _("LDAP client cannot be configured without 'server' " 

4336 "or 'domain' parameters. Use 'server' for Linux/Unix " 

4337 "LDAP servers or 'domain' for Active Directory LDAP " 

4338 "server.") 

4339 LOG.exception(msg) 

4340 raise exception.NetAppException(msg) 

4341 

4342 if security_service.get('dns_ip'): 4342 ↛ 4345line 4342 didn't jump to line 4345 because the condition on line 4342 was always true

4343 self.configure_dns(security_service) 

4344 

4345 body = { 

4346 'port': '389', 

4347 'schema': ldap_schema, 

4348 'bind_dn': bind_dn, 

4349 'bind_password': security_service.get('password'), 

4350 'svm.name': vserver_name 

4351 } 

4352 

4353 if security_service.get('ou'): 4353 ↛ 4355line 4353 didn't jump to line 4355 because the condition on line 4353 was always true

4354 body['base_dn'] = security_service['ou'] 

4355 if ad_domain: 

4356 # Active Directory LDAP server 

4357 body['ad_domain'] = ad_domain 

4358 else: 

4359 body['servers'] = [] 

4360 for server in ldap_servers.split(','): 

4361 body['servers'].append(server.strip()) 

4362 

4363 self.send_request('/name-services/ldap', 'post', 

4364 body=body) 

4365 

4366 @na_utils.trace 

4367 def modify_ldap(self, new_security_service, current_security_service): 

4368 """Modifies LDAP client on a Vserver.""" 

4369 ad_domain = new_security_service.get('domain') 

4370 ldap_servers = new_security_service.get('server') 

4371 bind_dn = new_security_service.get('user') 

4372 ldap_schema = 'RFC-2307' 

4373 svm_uuid = self._get_unique_svm_by_name(self.vserver) 

4374 

4375 if ad_domain: 

4376 if ldap_servers: 

4377 msg = _("LDAP client cannot be configured with both 'server' " 

4378 "and 'domain' parameters. Use 'server' for Linux/Unix " 

4379 "LDAP servers or 'domain' for Active Directory LDAP " 

4380 "servers.") 

4381 LOG.exception(msg) 

4382 raise exception.NetAppException(msg) 

4383 # RFC2307bis, for MS Active Directory LDAP server 

4384 ldap_schema = 'MS-AD-BIS' 

4385 bind_dn = (new_security_service.get('user') + '@' + ad_domain) 

4386 else: 

4387 if not ldap_servers: 

4388 msg = _("LDAP client cannot be configured without 'server' " 

4389 "or 'domain' parameters. Use 'server' for Linux/Unix " 

4390 "LDAP servers or 'domain' for Active Directory LDAP " 

4391 "server.") 

4392 LOG.exception(msg) 

4393 raise exception.NetAppException(msg) 

4394 

4395 body = { 

4396 'port': '389', 

4397 'schema': ldap_schema, 

4398 'bind_dn': bind_dn, 

4399 'bind_password': new_security_service.get('password') 

4400 } 

4401 

4402 if new_security_service.get('ou'): 4402 ↛ 4404line 4402 didn't jump to line 4404 because the condition on line 4402 was always true

4403 body['base_dn'] = new_security_service['ou'] 

4404 if ad_domain: 

4405 # Active Directory LDAP server 

4406 body['ad_domain'] = ad_domain 

4407 else: 

4408 body['servers'] = [] 

4409 for server in ldap_servers.split(','): 

4410 body['servers'].append(server.strip()) 

4411 

4412 self.send_request(f'/name-services/ldap/{svm_uuid}', 'patch', 

4413 body=body) 

4414 

4415 @na_utils.trace 

4416 def update_kerberos_realm(self, security_service): 

4417 """Update Kerberos realm info. Only KDC IP can be changed.""" 

4418 realm_name = security_service['domain'] 

4419 svm_uuid = self._get_unique_svm_by_name(self.vserver) 

4420 

4421 body = { 

4422 'kdc-ip': security_service['server'], 

4423 } 

4424 

4425 try: 

4426 self.send_request( 

4427 f'/protocols/nfs/kerberos/realms/{svm_uuid}/{realm_name}', 

4428 'patch', body=body) 

4429 except netapp_api.api.NaApiError as e: 

4430 msg = _('Failed to update Kerberos realm. %s') 

4431 raise exception.NetAppException(msg % e.message) 

4432 

4433 @na_utils.trace 

4434 def update_dns_configuration(self, dns_ips, domains): 

4435 """Overrides DNS configuration with the specified IPs and domains.""" 

4436 current_dns_config = self.get_dns_config(vserver_name=self.vserver) 

4437 body = { 

4438 'domains': [], 

4439 'servers': [], 

4440 } 

4441 for domain in domains: 

4442 body['domains'].append(domain) 

4443 

4444 for dns_ip in dns_ips: 

4445 body['servers'].append(dns_ip) 

4446 

4447 empty_dns_config = (not body['domains'] and 

4448 not body['servers']) 

4449 

4450 svm_uuid = self._get_unique_svm_by_name(self.vserver) 

4451 

4452 if current_dns_config: 

4453 endpoint, operation, body = ( 

4454 (f'/name-services/dns/{svm_uuid}', 

4455 'delete', {}) if empty_dns_config 

4456 else (f'/name-services/dns/{svm_uuid}', 'patch', body)) 

4457 else: 

4458 endpoint, operation, body = '/name-services/dns', 'post', body 

4459 

4460 try: 

4461 self.send_request(endpoint, operation, body) 

4462 except netapp_api.api.NaApiError as e: 

4463 msg = ("Failed to update DNS configuration. %s") 

4464 raise exception.NetAppException(msg % e.message) 

4465 

4466 @na_utils.trace 

4467 def remove_preferred_dcs(self, security_service, svm_uuid): 

4468 """Drops all preferred DCs at once.""" 

4469 

4470 query = { 

4471 'fqdn': security_service['domain'], 

4472 } 

4473 

4474 records = self.send_request(f'/protocols/cifs/domains/{svm_uuid}/' 

4475 f'preferred-domain-controllers/', 'get') 

4476 

4477 fqdn = records.get('fqdn') 

4478 server_ip = records.get('server_ip') 

4479 

4480 try: 

4481 self.send_request( 

4482 f'/protocols/cifs/domains/{svm_uuid}/' 

4483 f'preferred-domain-controllers/{fqdn}/{server_ip}', 

4484 'delete', query=query) 

4485 except netapp_api.api.NaApiError as e: 

4486 msg = _("Failed to unset preferred DCs. %s") 

4487 raise exception.NetAppException(msg % e.message) 

4488 

4489 @na_utils.trace 

4490 def modify_active_directory_security_service( 

4491 self, vserver_name, differring_keys, new_security_service, 

4492 current_security_service): 

4493 """Modify Active Directory security service.""" 

4494 

4495 svm_uuid = self._get_unique_svm_by_name(vserver_name) 

4496 new_username = new_security_service['user'] 

4497 

4498 records = self.send_request( 

4499 f'/protocols/cifs/local-users/{svm_uuid}', 'get') 

4500 sid = records.get('sid') 

4501 if 'password' in differring_keys: 

4502 query = { 

4503 'password': new_security_service['password'] 

4504 } 

4505 try: 

4506 self.send_request( 

4507 f'/protocols/cifs/local-users/{svm_uuid}/{sid}', 

4508 'patch', query=query 

4509 ) 

4510 except netapp_api.api.NaApiError as e: 

4511 msg = _("Failed to modify existing CIFS server password. %s") 

4512 raise exception.NetAppException(msg % e.message) 

4513 

4514 if 'user' in differring_keys: 4514 ↛ 4527line 4514 didn't jump to line 4527 because the condition on line 4514 was always true

4515 query = { 

4516 'name': new_username 

4517 } 

4518 try: 

4519 self.send_request( 

4520 f'/protocols/cifs/local-users/{svm_uuid}/{sid}', 

4521 'patch', query=query 

4522 ) 

4523 except netapp_api.api.NaApiError as e: 

4524 msg = _("Failed to modify existing CIFS server user-name. %s") 

4525 raise exception.NetAppException(msg % e.message) 

4526 

4527 if 'server' in differring_keys: 4527 ↛ exitline 4527 didn't return from function 'modify_active_directory_security_service' because the condition on line 4527 was always true

4528 if current_security_service['server'] is not None: 4528 ↛ 4531line 4528 didn't jump to line 4531 because the condition on line 4528 was always true

4529 self.remove_preferred_dcs(current_security_service, svm_uuid) 

4530 

4531 if new_security_service['server'] is not None: 4531 ↛ exitline 4531 didn't return from function 'modify_active_directory_security_service' because the condition on line 4531 was always true

4532 self.set_preferred_dc(new_security_service, svm_uuid) 

4533 

4534 @na_utils.trace 

4535 def configure_cifs_aes_encryption(self, vserver_name, aes_encryption): 

4536 try: 

4537 svm_uuid = self._get_unique_svm_by_name(vserver_name) 

4538 body = { 

4539 'security.advertised_kdc_encryptions': ( 

4540 ['aes-128', 'aes-256'] if aes_encryption else 

4541 ['des', 'rc4']), 

4542 } 

4543 

4544 self.send_request( 

4545 f'/protocols/cifs/services/{svm_uuid}', 'patch', body=body) 

4546 except netapp_api.api.NaApiError as e: 

4547 msg = _("Failed to set aes encryption. %s") 

4548 raise exception.NetAppException(msg % e.message) 

4549 

4550 @na_utils.trace 

4551 def set_preferred_dc(self, security_service, vserver_name): 

4552 """Set preferred domain controller.""" 

4553 

4554 # server is optional 

4555 if not security_service['server']: 

4556 return 

4557 

4558 query = { 

4559 'server_ip': [], 

4560 'fqdn': security_service['domain'], 

4561 'skip_config_validation': 'false', 

4562 } 

4563 

4564 for dc_ip in security_service['server'].split(','): 

4565 query['server_ip'].append(dc_ip.strip()) 

4566 

4567 svm_uuid = self._get_unique_svm_by_name(vserver_name) 

4568 

4569 try: 

4570 self.send_request( 

4571 f'/protocols/cifs/domains/{svm_uuid}' 

4572 '/preferred-domain-controllers', 

4573 'post', query=query) 

4574 except netapp_api.api.NaApiError as e: 

4575 msg = _("Failed to set preferred DC. %s") 

4576 raise exception.NetAppException(msg % e.message) 

4577 

4578 @na_utils.trace 

4579 def create_vserver_peer(self, vserver_name, peer_vserver_name, 

4580 peer_cluster_name=None): 

4581 """Creates a Vserver peer relationship for SnapMirrors.""" 

4582 body = { 

4583 'svm.name': vserver_name, 

4584 'peer.svm.name': peer_vserver_name, 

4585 'applications': ['snapmirror'] 

4586 } 

4587 if peer_cluster_name: 

4588 body['peer.cluster.name'] = peer_cluster_name 

4589 

4590 self.send_request('/svm/peers', 'post', body=body, 

4591 enable_tunneling=False) 

4592 

4593 @na_utils.trace 

4594 def _get_svm_peer_uuid(self, vserver_name, peer_vserver_name): 

4595 """Get UUID of SVM peer.""" 

4596 query = { 

4597 'svm.name': vserver_name, 

4598 'peer.svm.name': peer_vserver_name, 

4599 'fields': 'uuid' 

4600 } 

4601 res = self.send_request('/svm/peers', 'get', query=query) 

4602 if not res.get('records'): 

4603 msg = ('Vserver peer not found.') 

4604 raise exception.NetAppException(msg) 

4605 peer_uuid = res.get('records')[0]['uuid'] 

4606 return peer_uuid 

4607 

4608 @na_utils.trace 

4609 def accept_vserver_peer(self, vserver_name, peer_vserver_name): 

4610 """Accepts a pending Vserver peer relationship.""" 

4611 uuid = self._get_svm_peer_uuid(vserver_name, peer_vserver_name) 

4612 body = {'state': 'peered'} 

4613 self.send_request(f'/svm/peers/{uuid}', 'patch', body=body, 

4614 enable_tunneling=False) 

4615 

4616 @na_utils.trace 

4617 def get_vserver_peers(self, vserver_name=None, peer_vserver_name=None): 

4618 """Gets one or more Vserver peer relationships.""" 

4619 

4620 query = {} 

4621 if peer_vserver_name: 4621 ↛ 4623line 4621 didn't jump to line 4623 because the condition on line 4621 was always true

4622 query['name'] = peer_vserver_name 

4623 if vserver_name: 4623 ↛ 4626line 4623 didn't jump to line 4626 because the condition on line 4623 was always true

4624 query['svm.name'] = vserver_name 

4625 

4626 query['fields'] = 'uuid,svm.name,peer.svm.name,state,peer.cluster.name' 

4627 

4628 result = self.send_request('/svm/peers', 'get', query=query) 

4629 if not self._has_records(result): 

4630 return [] 

4631 

4632 vserver_peers = [] 

4633 for vserver_peer in result['records']: 

4634 vserver_peer_info = { 

4635 'uuid': vserver_peer['uuid'], 

4636 'vserver': vserver_peer['svm']['name'], 

4637 'peer-vserver': vserver_peer['peer']['svm']['name'], 

4638 'peer-state': vserver_peer['state'], 

4639 'peer-cluster': vserver_peer['peer']['cluster']['name'], 

4640 } 

4641 vserver_peers.append(vserver_peer_info) 

4642 

4643 return vserver_peers 

4644 

4645 @na_utils.trace 

4646 def delete_vserver_peer(self, vserver_name, peer_vserver_name): 

4647 """Deletes a Vserver peer relationship.""" 

4648 

4649 vserver_peer = self.get_vserver_peers(vserver_name, peer_vserver_name) 

4650 uuid = vserver_peer[0].get('uuid') 

4651 self.send_request(f'/svm/peers/{uuid}', 'delete', 

4652 enable_tunneling=False) 

4653 

4654 @na_utils.trace 

4655 def create_vserver(self, vserver_name, root_volume_aggregate_name, 

4656 root_volume_name, aggregate_names, ipspace_name, 

4657 security_cert_expire_days, delete_retention_hours, 

4658 logical_space_reporting): 

4659 """Creates new vserver and assigns aggregates.""" 

4660 

4661 # NOTE(nahimsouza): root_volume_aggregate_name and root_volume_name 

4662 # were kept due to compatibility issues, but they are not used in 

4663 # the vserver creation by REST API 

4664 self._create_vserver( 

4665 vserver_name, aggregate_names, ipspace_name, 

4666 delete_retention_hours, name_server_switch=['files'], 

4667 logical_space_reporting=logical_space_reporting) 

4668 self._modify_security_cert(vserver_name, security_cert_expire_days) 

4669 

4670 @na_utils.trace 

4671 def create_vserver_dp_destination(self, vserver_name, aggregate_names, 

4672 ipspace_name, delete_retention_hours): 

4673 """Creates new 'dp_destination' vserver and assigns aggregates.""" 

4674 self._create_vserver( 

4675 vserver_name, aggregate_names, ipspace_name, 

4676 delete_retention_hours, subtype='dp_destination') 

4677 

4678 @na_utils.trace 

4679 def _create_vserver(self, vserver_name, aggregate_names, ipspace_name, 

4680 delete_retention_hours, 

4681 name_server_switch=None, subtype=None, 

4682 logical_space_reporting=False): 

4683 """Creates new vserver and assigns aggregates.""" 

4684 body = { 

4685 'name': vserver_name, 

4686 } 

4687 

4688 if name_server_switch: 4688 ↛ 4691line 4688 didn't jump to line 4691 because the condition on line 4688 was always true

4689 body['nsswitch.namemap'] = name_server_switch 

4690 

4691 if subtype: 4691 ↛ 4694line 4691 didn't jump to line 4694 because the condition on line 4691 was always true

4692 body['subtype'] = subtype 

4693 

4694 if ipspace_name: 4694 ↛ 4697line 4694 didn't jump to line 4697 because the condition on line 4694 was always true

4695 body['ipspace.name'] = ipspace_name 

4696 

4697 body['aggregates'] = [] 

4698 for aggr_name in aggregate_names: 

4699 body['aggregates'].append({'name': aggr_name}) 

4700 

4701 body['is_space_reporting_logical'] = ( 

4702 'true' if logical_space_reporting else 'false') 

4703 body['is_space_enforcement_logical'] = ( 

4704 'true' if logical_space_reporting else 'false') 

4705 

4706 self.send_request('/svm/svms', 'post', body=body) 

4707 

4708 try: 

4709 svm_uuid = self._get_unique_svm_by_name(vserver_name) 

4710 body = { 

4711 'retention_period': delete_retention_hours 

4712 } 

4713 self.send_request(f'/svm/svms/{svm_uuid}', 'patch', 

4714 body=body) 

4715 except netapp_api.api.NaApiError: 

4716 LOG.warning('Failed to modify retention period for vserver ' 

4717 '%(server)s.', {'server': vserver_name}) 

4718 

4719 @na_utils.trace 

4720 def create_barbican_kms_config_for_specified_vserver(self, vserver_name, 

4721 config_name, key_id, 

4722 keystone_url, 

4723 app_cred_id, 

4724 app_cred_secret): 

4725 """Creates a Barbican KMS configuration for the specified vserver.""" 

4726 

4727 body = { 

4728 'svm.name': vserver_name, 

4729 'configuration.name': config_name, 

4730 'key_id': key_id, 

4731 'keystone_url': keystone_url, 

4732 'application_cred_id': app_cred_id, 

4733 'application_cred_secret': app_cred_secret, 

4734 } 

4735 

4736 self.send_request('/security/barbican-kms', 'post', body=body) 

4737 

4738 @na_utils.trace 

4739 def get_key_store_config_uuid(self, config_name): 

4740 """Retrieves keystore configuration uuid for the specified config name. 

4741 

4742 """ 

4743 

4744 query = { 

4745 'configuration.name': config_name 

4746 } 

4747 

4748 response = self.send_request('/security/key-stores', 

4749 'get', query=query) 

4750 

4751 if not response.get('records'): 

4752 return None 

4753 

4754 return response.get('records')[0]['configuration']['uuid'] 

4755 

4756 @na_utils.trace 

4757 def enable_key_store_config(self, config_uuid): 

4758 """Enables a keystore configuration""" 

4759 

4760 body = { 

4761 "enabled": True 

4762 } 

4763 

4764 # Update key-store 

4765 self.send_request(f'/security/key-stores/{config_uuid}', 'patch', 

4766 body=body) 

4767 

4768 @na_utils.trace 

4769 def _modify_security_cert(self, vserver_name, security_cert_expire_days): 

4770 """Create new security certificate with given expire days.""" 

4771 

4772 # Do not modify security certificate if specified expire days are 

4773 # equal to default security certificate expire days i.e. 365. 

4774 if security_cert_expire_days == DEFAULT_SECURITY_CERT_EXPIRE_DAYS: 4774 ↛ 4775line 4774 didn't jump to line 4775 because the condition on line 4774 was never true

4775 return 

4776 

4777 query = { 

4778 'common-name': vserver_name, 

4779 'ca': vserver_name, 

4780 'type': 'server', 

4781 'svm.name': vserver_name, 

4782 } 

4783 result = self.send_request('/security/certificates', 

4784 'get', query=query) 

4785 old_certificate_info_list = result.get('records', []) 

4786 if not old_certificate_info_list: 4786 ↛ 4787line 4786 didn't jump to line 4787 because the condition on line 4786 was never true

4787 LOG.warning("Unable to retrieve certificate-info for vserver " 

4788 "%(server)s'. Cannot set the certificate expiry to " 

4789 "%s(conf)s. ", {'server': vserver_name, 

4790 'conf': security_cert_expire_days}) 

4791 return 

4792 

4793 body = { 

4794 'common-name': vserver_name, 

4795 'type': 'server', 

4796 'svm.name': vserver_name, 

4797 'expiry_time': f'P{security_cert_expire_days}DT', 

4798 } 

4799 query = { 

4800 'return_records': 'true' 

4801 } 

4802 result = self.send_request('/security/certificates', 

4803 'post', body=body, query=query) 

4804 new_certificate_info_list = result.get('records', []) 

4805 if not new_certificate_info_list: 4805 ↛ 4806line 4805 didn't jump to line 4806 because the condition on line 4805 was never true

4806 LOG.warning('Failed to create new security certificate for ' 

4807 'vserver %(server)s.', {'server': vserver_name}) 

4808 return 

4809 

4810 for certificate_info in new_certificate_info_list: 

4811 cert_uuid = certificate_info.get('uuid', None) 

4812 svm = certificate_info.get('svm', []) 

4813 svm_uuid = svm.get('uuid', None) 

4814 if not svm_uuid or not cert_uuid: 4814 ↛ 4815line 4814 didn't jump to line 4815 because the condition on line 4814 was never true

4815 continue 

4816 

4817 try: 

4818 body = { 

4819 'certificate': { 

4820 'uuid': cert_uuid, 

4821 }, 

4822 'client_enabled': 'false', 

4823 } 

4824 self.send_request(f'/svm/svms/{svm_uuid}', 'patch', 

4825 body=body) 

4826 except netapp_api.api.NaApiError: 

4827 LOG.debug('Failed to modify SSL for vserver ' 

4828 '%(server)s.', {'server': vserver_name}) 

4829 

4830 # Delete all old security certificates 

4831 for certificate_info in old_certificate_info_list: 

4832 uuid = certificate_info.get('uuid', None) 

4833 try: 

4834 self.send_request(f'/security/certificates/{uuid}', 'delete') 

4835 except netapp_api.api.NaApiError: 

4836 LOG.error("Failed to delete security certificate for vserver " 

4837 "%s.", vserver_name) 

4838 

4839 @na_utils.trace 

4840 def list_node_data_ports(self, node): 

4841 """List data ports from node.""" 

4842 ports = self.get_node_data_ports(node) 

4843 return [port.get('port') for port in ports] 

4844 

4845 @na_utils.trace 

4846 def _sort_data_ports_by_speed(self, ports): 

4847 """Sort ports by speed.""" 

4848 

4849 def sort_key(port): 

4850 value = port.get('speed') 

4851 if not (value and isinstance(value, str)): 

4852 return 0 

4853 elif value.isdigit(): 

4854 return int(value) 

4855 elif value == 'auto': 

4856 return 3 

4857 elif value == 'undef': 

4858 return 2 

4859 else: 

4860 return 1 

4861 

4862 return sorted(ports, key=sort_key, reverse=True) 

4863 

4864 @na_utils.trace 

4865 def get_node_data_ports(self, node): 

4866 """Get applicable data ports on the node.""" 

4867 query = { 

4868 'node.name': node, 

4869 'state': 'up', 

4870 'type': 'physical', 

4871 'broadcast_domain.name': 'Default', 

4872 'fields': 'node.name,speed,name' 

4873 } 

4874 

4875 result = self.send_request('/network/ethernet/ports', 'get', 

4876 query=query) 

4877 

4878 net_port_info_list = result.get('records', []) 

4879 

4880 ports = [] 

4881 if net_port_info_list: 4881 ↛ 4910line 4881 didn't jump to line 4910 because the condition on line 4881 was always true

4882 

4883 # NOTE(pulluri): This query selects the ports that are 

4884 # being exclusively used for data management 

4885 query_interfaces = { 

4886 'service_policy.name': '!default-management', 

4887 'services': 'data_*', 

4888 'fields': 'location.port.name' 

4889 } 

4890 response = self.send_request('/network/ip/interfaces', 'get', 

4891 query=query_interfaces, 

4892 enable_tunneling=False) 

4893 

4894 data_ports = set( 

4895 [record['location']['port']['name'] 

4896 for record in response.get('records', [])] 

4897 ) 

4898 

4899 for port_info in net_port_info_list: 

4900 if port_info['name'] in data_ports: 

4901 port = { 

4902 'node': port_info['node']['name'], 

4903 'port': port_info['name'], 

4904 'speed': port_info['speed'], 

4905 } 

4906 ports.append(port) 

4907 

4908 ports = self._sort_data_ports_by_speed(ports) 

4909 

4910 return ports 

4911 

4912 @na_utils.trace 

4913 def get_ipspace_name_for_vlan_port(self, vlan_node, vlan_port, vlan_id): 

4914 """Gets IPSpace name for specified VLAN""" 

4915 

4916 port = vlan_port if not vlan_id else '%(port)s-%(id)s' % { 

4917 'port': vlan_port, 

4918 'id': vlan_id, 

4919 } 

4920 query = { 

4921 'name': port, 

4922 'node.name': vlan_node, 

4923 'fields': 'broadcast_domain.ipspace.name' 

4924 } 

4925 result = self.send_request('/network/ethernet/ports/', 'get', 

4926 query=query) 

4927 

4928 records = result.get('records', []) 

4929 if not records: 4929 ↛ 4930line 4929 didn't jump to line 4930 because the condition on line 4929 was never true

4930 return None 

4931 ipspace_name = records[0]['broadcast_domain']['ipspace']['name'] 

4932 

4933 return ipspace_name 

4934 

4935 @na_utils.trace 

4936 def create_ipspace(self, ipspace_name): 

4937 """Creates an IPspace.""" 

4938 body = {'name': ipspace_name} 

4939 self.send_request('/network/ipspaces', 'post', body=body) 

4940 

4941 @na_utils.trace 

4942 def create_port_and_broadcast_domain(self, node, port, vlan, mtu, ipspace): 

4943 """Create port and broadcast domain, if they don't exist.""" 

4944 home_port_name = port 

4945 if vlan: 4945 ↛ 4949line 4945 didn't jump to line 4949 because the condition on line 4945 was always true

4946 self._create_vlan(node, port, vlan) 

4947 home_port_name = '%(port)s-%(tag)s' % {'port': port, 'tag': vlan} 

4948 

4949 self._ensure_broadcast_domain_for_port( 

4950 node, home_port_name, mtu, ipspace=ipspace) 

4951 

4952 return home_port_name 

4953 

4954 @na_utils.trace 

4955 def _create_vlan(self, node, port, vlan): 

4956 """Create VLAN port if it does not exist.""" 

4957 try: 

4958 body = { 

4959 'vlan.base_port.name': port, 

4960 'node.name': node, 

4961 'vlan.tag': vlan, 

4962 'type': 'vlan' 

4963 } 

4964 self.send_request('/network/ethernet/ports', 'post', body=body) 

4965 except netapp_api.api.NaApiError as e: 

4966 if e.code == netapp_api.EREST_DUPLICATE_ENTRY: 

4967 LOG.debug('VLAN %(vlan)s already exists on port %(port)s', 

4968 {'vlan': vlan, 'port': port}) 

4969 else: 

4970 msg = _('Failed to create VLAN %(vlan)s on ' 

4971 'port %(port)s. %(err_msg)s') 

4972 msg_args = {'vlan': vlan, 'port': port, 'err_msg': e.message} 

4973 raise exception.NetAppException(msg % msg_args) 

4974 

4975 @na_utils.trace 

4976 def _ensure_broadcast_domain_for_port(self, node, port, mtu, 

4977 ipspace=DEFAULT_IPSPACE): 

4978 """Ensure a port is in a broadcast domain. Create one if necessary. 

4979 

4980 If the IPspace:domain pair match for the given port, which commonly 

4981 happens in multi-node clusters, then there isn't anything to do. 

4982 Otherwise, we can assume the IPspace is correct and extant by this 

4983 point, so the remaining task is to remove the port from any domain it 

4984 is already in, create the domain for the IPspace if it doesn't exist, 

4985 and add the port to this domain. 

4986 """ 

4987 

4988 # Derive the broadcast domain name from the IPspace name since they 

4989 # need to be 1-1 and the default for both is the same name, 'Default'. 

4990 domain = re.sub(r'ipspace', 'domain', ipspace) 

4991 

4992 port_info = self._get_broadcast_domain_for_port(node, port) 

4993 

4994 # Port already in desired ipspace and broadcast domain. 

4995 if (port_info['ipspace'] == ipspace 

4996 and port_info['broadcast-domain'] == domain): 

4997 self._modify_broadcast_domain(domain, ipspace, mtu) 

4998 return 

4999 

5000 # If desired broadcast domain doesn't exist, create it. 

5001 if not self._broadcast_domain_exists(domain, ipspace): 

5002 self._create_broadcast_domain(domain, ipspace, mtu) 

5003 else: 

5004 self._modify_broadcast_domain(domain, ipspace, mtu) 

5005 

5006 # Move the port into the broadcast domain where it is needed. 

5007 self._add_port_to_broadcast_domain(node, port, domain, ipspace) 

5008 

5009 @na_utils.trace 

5010 def _get_broadcast_domain_for_port(self, node, port): 

5011 """Get broadcast domain for a specific port.""" 

5012 query = { 

5013 'node.name': node, 

5014 'name': port, 

5015 'fields': 'broadcast_domain.name,broadcast_domain.ipspace.name' 

5016 } 

5017 result = self.send_request( 

5018 '/network/ethernet/ports', 'get', query=query) 

5019 

5020 net_port_info_list = result.get('records', []) 

5021 port_info = net_port_info_list[0] 

5022 if not port_info: 

5023 msg = _('Could not find port %(port)s on node %(node)s.') 

5024 msg_args = {'port': port, 'node': node} 

5025 raise exception.NetAppException(msg % msg_args) 

5026 

5027 broadcast_domain = port_info.get('broadcast_domain', {}) 

5028 broadcast_domain_name = broadcast_domain.get('name') 

5029 ipspace_name = broadcast_domain.get('ipspace', {}).get('name') 

5030 port = { 

5031 'broadcast-domain': broadcast_domain_name, 

5032 'ipspace': ipspace_name 

5033 } 

5034 return port 

5035 

5036 @na_utils.trace 

5037 def _create_broadcast_domain(self, domain, ipspace, mtu): 

5038 """Create a broadcast domain.""" 

5039 body = { 

5040 'ipspace.name': ipspace, 

5041 'name': domain, 

5042 'mtu': mtu, 

5043 } 

5044 self.send_request( 

5045 '/network/ethernet/broadcast-domains', 'post', body=body) 

5046 

5047 @na_utils.trace 

5048 def _modify_broadcast_domain(self, domain, ipspace, mtu): 

5049 """Modify a broadcast domain.""" 

5050 query = { 

5051 'name': domain 

5052 } 

5053 

5054 body = { 

5055 'ipspace.name': ipspace, 

5056 'mtu': mtu, 

5057 } 

5058 self.send_request( 

5059 '/network/ethernet/broadcast-domains', 'patch', body=body, 

5060 query=query) 

5061 

5062 @na_utils.trace 

5063 def _delete_port_by_ipspace_and_broadcast_domain(self, port, 

5064 domain, ipspace): 

5065 query = { 

5066 'broadcast_domain.ipspace.name': ipspace, 

5067 'broadcast_domain.name': domain, 

5068 'name': port 

5069 } 

5070 self.send_request('/network/ethernet/ports/', 'delete', query=query) 

5071 

5072 @na_utils.trace 

5073 def _broadcast_domain_exists(self, domain, ipspace): 

5074 """Check if a broadcast domain exists.""" 

5075 query = { 

5076 'ipspace.name': ipspace, 

5077 'name': domain, 

5078 } 

5079 result = self.send_request( 

5080 '/network/ethernet/broadcast-domains', 

5081 'get', query=query) 

5082 return self._has_records(result) 

5083 

5084 @na_utils.trace 

5085 def _add_port_to_broadcast_domain(self, node, port, domain, ipspace): 

5086 """Set a broadcast domain for a given port.""" 

5087 try: 

5088 query = { 

5089 'name': port, 

5090 'node.name': node, 

5091 } 

5092 body = { 

5093 'broadcast_domain.ipspace.name': ipspace, 

5094 'broadcast_domain.name': domain, 

5095 } 

5096 self.send_request('/network/ethernet/ports/', 'patch', 

5097 query=query, body=body) 

5098 except netapp_api.api.NaApiError as e: 

5099 if e.code == netapp_api.EREST_FAIL_ADD_PORT_BROADCAST: 

5100 LOG.debug('Port %(port)s already exists in broadcast domain ' 

5101 '%(domain)s', {'port': port, 'domain': domain}) 

5102 else: 

5103 msg = _('Failed to add port %(port)s to broadcast domain ' 

5104 '%(domain)s. %(err_msg)s') 

5105 msg_args = { 

5106 'port': port, 

5107 'domain': domain, 

5108 'err_msg': e.message, 

5109 } 

5110 raise exception.NetAppException(msg % msg_args) 

5111 

5112 @na_utils.trace 

5113 def update_showmount(self, showmount): 

5114 """Update show mount for vserver. """ 

5115 # Get SVM UUID. 

5116 query = { 

5117 'name': self.vserver, 

5118 'fields': 'uuid' 

5119 } 

5120 res = self.send_request('/svm/svms', 'get', query=query) 

5121 if not res.get('records'): 5121 ↛ 5122line 5121 didn't jump to line 5122 because the condition on line 5121 was never true

5122 msg = _('Vserver %s not found.') % self.vserver 

5123 raise exception.NetAppException(msg) 

5124 svm_id = res.get('records')[0]['uuid'] 

5125 

5126 body = { 

5127 'showmount_enabled': showmount, 

5128 } 

5129 self.send_request(f'/protocols/nfs/services/{svm_id}', 'patch', 

5130 body=body) 

5131 

5132 @na_utils.trace 

5133 def update_pnfs(self, pnfs): 

5134 """Update pNFS for vserver. """ 

5135 # Get SVM UUID. 

5136 query = { 

5137 'name': self.vserver, 

5138 'fields': 'uuid' 

5139 } 

5140 res = self.send_request('/svm/svms', 'get', query=query) 

5141 if not res.get('records'): 

5142 msg = _('Vserver %s not found.') % self.vserver 

5143 raise exception.NetAppException(msg) 

5144 svm_id = res.get('records')[0]['uuid'] 

5145 

5146 body = { 

5147 'protocol.v41_features.pnfs_enabled': pnfs, 

5148 } 

5149 self.send_request(f'/protocols/nfs/services/{svm_id}', 'patch', 

5150 body=body) 

5151 

5152 @na_utils.trace 

5153 def enable_nfs(self, versions, nfs_config=None): 

5154 """Enables NFS on Vserver.""" 

5155 svm_id = self._get_unique_svm_by_name() 

5156 

5157 body = { 

5158 'svm.uuid': svm_id, 

5159 'enabled': 'true', 

5160 } 

5161 self.send_request('/protocols/nfs/services/', 'post', 

5162 body=body) 

5163 

5164 self._enable_nfs_protocols(versions, svm_id) 

5165 

5166 if nfs_config: 

5167 self._configure_nfs(nfs_config, svm_id) 

5168 

5169 self._create_default_nfs_export_rules() 

5170 

5171 @na_utils.trace 

5172 def _enable_nfs_protocols(self, versions, svm_id): 

5173 """Set the enabled NFS protocol versions.""" 

5174 nfs3 = 'true' if 'nfs3' in versions else 'false' 

5175 nfs40 = 'true' if 'nfs4.0' in versions else 'false' 

5176 nfs41 = 'true' if 'nfs4.1' in versions else 'false' 

5177 

5178 body = { 

5179 'protocol.v3_enabled': nfs3, 

5180 'protocol.v40_enabled': nfs40, 

5181 'protocol.v41_enabled': nfs41, 

5182 'showmount_enabled': 'true', 

5183 'windows.v3_ms_dos_client_enabled': 'true', 

5184 'protocol.v3_features.connection_drop': 'false', 

5185 'protocol.v3_features.ejukebox_enabled': 'false', 

5186 } 

5187 self.send_request(f'/protocols/nfs/services/{svm_id}', 'patch', 

5188 body=body) 

5189 

5190 @na_utils.trace 

5191 def _create_default_nfs_export_rules(self): 

5192 """Create the default export rule for the NFS service.""" 

5193 

5194 body = { 

5195 'clients': [{'match': '0.0.0.0/0'}], 

5196 'ro_rule': [ 

5197 'any', 

5198 ], 

5199 'rw_rule': [ 

5200 'never' 

5201 ], 

5202 } 

5203 

5204 uuid = self.get_unique_export_policy_id('default') 

5205 

5206 self.send_request(f'/protocols/nfs/export-policies/{uuid}/rules', 

5207 "post", body=body) 

5208 body['clients'] = [{'match': '::/0'}] 

5209 self.send_request(f'/protocols/nfs/export-policies/{uuid}/rules', 

5210 "post", body=body) 

5211 

5212 @na_utils.trace 

5213 def _configure_nfs(self, nfs_config, svm_id): 

5214 """Sets the nfs configuraton""" 

5215 

5216 if ('udp-max-xfer-size' in nfs_config and 

5217 (nfs_config['udp-max-xfer-size'] 

5218 != str(DEFAULT_UDP_MAX_XFER_SIZE))): 

5219 

5220 msg = _('Failed to configure NFS. REST API does not support ' 

5221 'setting udp-max-xfer-size default value %(default)s ' 

5222 'is not equal to actual value %(actual)s') 

5223 msg_args = { 

5224 'default': DEFAULT_UDP_MAX_XFER_SIZE, 

5225 'actual': nfs_config['udp-max-xfer-size'], 

5226 } 

5227 raise exception.NetAppException(msg % msg_args) 

5228 

5229 nfs_config_value = int(nfs_config['tcp-max-xfer-size']) 

5230 body = { 

5231 'transport.tcp_max_transfer_size': nfs_config_value 

5232 } 

5233 self.send_request(f'/protocols/nfs/services/{svm_id}', 'patch', 

5234 body=body) 

5235 

5236 @na_utils.trace 

5237 def create_network_interface(self, ip, netmask, node, port, 

5238 vserver_name, lif_name): 

5239 """Creates LIF on VLAN port.""" 

5240 LOG.debug('Creating LIF %(lif)s for Vserver %(vserver)s ' 

5241 'node/port %(node)s:%(port)s.', 

5242 {'lif': lif_name, 'vserver': vserver_name, 'node': node, 

5243 'port': port}) 

5244 

5245 query = { 

5246 'name': 'default-data-files', 

5247 'svm.name': vserver_name, 

5248 'fields': 'uuid,name,services,svm.name' 

5249 } 

5250 

5251 result = self.send_request('/network/ip/service-policies/', 'get', 

5252 query=query) 

5253 

5254 if result.get('records'): 5254 ↛ 5270line 5254 didn't jump to line 5270 because the condition on line 5254 was always true

5255 policy = result['records'][0] 

5256 

5257 # NOTE(nahimsouza): Workaround to add services in the policy 

5258 # in the case ONTAP does not create it automatically 

5259 if 'data_nfs' not in policy['services']: 5259 ↛ 5261line 5259 didn't jump to line 5261 because the condition on line 5259 was always true

5260 policy['services'].append('data_nfs') 

5261 if 'data_cifs' not in policy['services']: 5261 ↛ 5264line 5261 didn't jump to line 5264 because the condition on line 5261 was always true

5262 policy['services'].append('data_cifs') 

5263 

5264 uuid = policy['uuid'] 

5265 body = {'services': policy['services']} 

5266 self.send_request( 

5267 f'/network/ip/service-policies/{uuid}', 'patch', 

5268 body=body) 

5269 

5270 body = { 

5271 'ip.address': ip, 

5272 'ip.netmask': netmask, 

5273 'enabled': 'true', 

5274 'service_policy.name': 'default-data-files', 

5275 'location.home_node.name': node, 

5276 'location.home_port.name': port, 

5277 'name': lif_name, 

5278 'svm.name': vserver_name, 

5279 } 

5280 self.send_request('/network/ip/interfaces', 'post', body=body) 

5281 

5282 @na_utils.trace 

5283 def network_interface_exists(self, vserver_name, node, port, ip, netmask, 

5284 vlan=None, home_port=None): 

5285 """Checks if LIF exists.""" 

5286 if not home_port: 5286 ↛ 5289line 5286 didn't jump to line 5289 because the condition on line 5286 was always true

5287 home_port = port if not vlan else f'{port}-{vlan}' 

5288 

5289 query = { 

5290 'ip.address': ip, 

5291 'location.home_node.name': node, 

5292 'location.home_port.name': home_port, 

5293 'ip.netmask': netmask, 

5294 'svm.name': vserver_name, 

5295 'fields': 'name', 

5296 } 

5297 result = self.send_request('/network/ip/interfaces', 

5298 'get', query=query) 

5299 return self._has_records(result) 

5300 

5301 @na_utils.trace 

5302 def create_route(self, gateway, destination=None): 

5303 """Create a network route.""" 

5304 if not gateway: 

5305 return 

5306 

5307 address = None 

5308 netmask = None 

5309 if not destination: 

5310 if netutils.is_valid_ipv6(gateway): 5310 ↛ 5311line 5310 didn't jump to line 5311 because the condition on line 5310 was never true

5311 destination = '::/0' 

5312 else: 

5313 destination = '0.0.0.0/0' 

5314 

5315 if '/' in destination: 5315 ↛ 5318line 5315 didn't jump to line 5318 because the condition on line 5315 was always true

5316 address, netmask = destination.split('/') 

5317 else: 

5318 address = destination 

5319 

5320 body = { 

5321 'destination.address': address, 

5322 'gateway': gateway, 

5323 } 

5324 

5325 if netmask: 5325 ↛ 5328line 5325 didn't jump to line 5328 because the condition on line 5325 was always true

5326 body['destination.netmask'] = netmask 

5327 

5328 try: 

5329 self.send_request('/network/ip/routes', 'post', body=body) 

5330 except netapp_api.api.NaApiError as e: 

5331 if (e.code == netapp_api.EREST_DUPLICATE_ROUTE): 

5332 LOG.debug('Route to %(destination)s via gateway %(gateway)s ' 

5333 'exists.', 

5334 {'destination': destination, 'gateway': gateway}) 

5335 else: 

5336 msg = _('Failed to create a route to %(destination)s via ' 

5337 'gateway %(gateway)s: %(err_msg)s') 

5338 msg_args = { 

5339 'destination': destination, 

5340 'gateway': gateway, 

5341 'err_msg': e.message, 

5342 } 

5343 raise exception.NetAppException(msg % msg_args) 

5344 

5345 @na_utils.trace 

5346 def rename_vserver(self, vserver_name, new_vserver_name): 

5347 """Rename a vserver.""" 

5348 body = { 

5349 'name': new_vserver_name 

5350 } 

5351 

5352 svm_uuid = self._get_unique_svm_by_name(vserver_name) 

5353 self.send_request(f'/svm/svms/{svm_uuid}', 'patch', body=body) 

5354 

5355 @na_utils.trace 

5356 def get_vserver_info(self, vserver_name): 

5357 """Retrieves Vserver info.""" 

5358 LOG.debug('Retrieving Vserver %s information.', vserver_name) 

5359 

5360 query = { 

5361 'name': vserver_name, 

5362 'fields': 'state,subtype' 

5363 } 

5364 

5365 response = self.send_request('/svm/svms', 'get', query=query) 

5366 if not response.get('records'): 

5367 return 

5368 

5369 vserver = response['records'][0] 

5370 vserver_info = { 

5371 'name': vserver_name, 

5372 'subtype': vserver['subtype'], 

5373 'operational_state': vserver['state'], 

5374 'state': vserver['state'], 

5375 } 

5376 return vserver_info 

5377 

5378 @na_utils.trace 

5379 def get_nfs_config(self, desired_args, vserver): 

5380 """Gets the NFS config of the given vserver with the desired params""" 

5381 

5382 query = {'fields': 'transport.*'} 

5383 query['svm.name'] = vserver 

5384 

5385 nfs_info = { 

5386 'tcp-max-xfer-size': str(DEFAULT_TCP_MAX_XFER_SIZE), 

5387 'udp-max-xfer-size': str(DEFAULT_UDP_MAX_XFER_SIZE) 

5388 } 

5389 

5390 response = self.send_request('/protocols/nfs/services/', 

5391 'get', query=query) 

5392 records = response.get('records', []) 

5393 

5394 if records: 5394 ↛ 5398line 5394 didn't jump to line 5398 because the condition on line 5394 was always true

5395 nfs_info['tcp-max-xfer-size'] = ( 

5396 str(records[0]['transport']['tcp_max_transfer_size'])) 

5397 

5398 return nfs_info 

5399 

5400 @na_utils.trace 

5401 def get_vserver_ipspace(self, vserver_name): 

5402 """Get the IPspace of the vserver, or None if not supported.""" 

5403 query = { 

5404 'name': vserver_name, 

5405 'fields': 'ipspace.name' 

5406 } 

5407 

5408 try: 

5409 response = self.send_request('/svm/svms', 'get', query=query) 

5410 except netapp_api.api.NaApiError: 

5411 msg = _('Could not determine IPspace for Vserver %s.') 

5412 raise exception.NetAppException(msg % vserver_name) 

5413 

5414 if self._has_records(response): 

5415 return response['records'][0].get('ipspace', {}).get('name') 

5416 

5417 return None 

5418 

5419 @na_utils.trace 

5420 def get_snapmirror_policies(self, vserver_name): 

5421 """Get all SnapMirror policies associated to a vServer.""" 

5422 query = { 

5423 'svm.name': vserver_name, 

5424 'fields': 'name' 

5425 } 

5426 response = self.send_request( 

5427 '/snapmirror/policies', 'get', query=query) 

5428 records = response.get('records') 

5429 

5430 policy_name = [] 

5431 for record in records: 

5432 policy_name.append(record.get('name')) 

5433 return policy_name 

5434 

5435 @na_utils.trace 

5436 def create_snapmirror_policy(self, policy_name, 

5437 policy_type='async', 

5438 discard_network_info=True, 

5439 preserve_snapshots=True, 

5440 snapmirror_label='all_source_snapshots', 

5441 keep=1): 

5442 """Create SnapMirror Policy""" 

5443 

5444 if policy_type == "vault": 

5445 body = {"name": policy_name, "type": "async", 

5446 "create_snapshot_on_source": False} 

5447 else: 

5448 body = {"name": policy_name, "type": policy_type} 

5449 if discard_network_info: 5449 ↛ 5450line 5449 didn't jump to line 5450 because the condition on line 5449 was never true

5450 body["exclude_network_config"] = {'svmdr-config-obj': 'network'} 

5451 if preserve_snapshots: 

5452 body["retention"] = [{"label": snapmirror_label, "count": keep}] 

5453 try: 

5454 self.send_request('/snapmirror/policies/', 'post', body=body) 

5455 except netapp_api.api.NaApiError as e: 

5456 LOG.debug('Failed to create SnapMirror policy. ' 

5457 'Error: %s. Code: %s', e.message, e.code) 

5458 raise 

5459 

5460 @na_utils.trace 

5461 def delete_snapmirror_policy(self, policy_name): 

5462 """Deletes a SnapMirror policy.""" 

5463 

5464 query = { 

5465 'name': policy_name, 

5466 'fields': 'uuid,name' 

5467 } 

5468 response = self.send_request('/snapmirror/policies', 

5469 'get', query=query) 

5470 if self._has_records(response): 

5471 uuid = response['records'][0]['uuid'] 

5472 try: 

5473 self.send_request(f'/snapmirror/policies/{uuid}', 'delete') 

5474 except netapp_api.api.NaApiError as e: 

5475 if e.code != netapp_api.EREST_ENTRY_NOT_FOUND: 5475 ↛ exitline 5475 didn't return from function 'delete_snapmirror_policy' because the condition on line 5475 was always true

5476 raise 

5477 

5478 @na_utils.trace 

5479 def delete_vserver(self, vserver_name, vserver_client, 

5480 security_services=None): 

5481 """Deletes a Vserver. 

5482 

5483 Checks if Vserver exists and does not have active shares. 

5484 Offlines and destroys root volumes. Deletes Vserver. 

5485 """ 

5486 vserver_info = self.get_vserver_info(vserver_name) 

5487 if vserver_info is None: 

5488 LOG.error("Vserver %s does not exist.", vserver_name) 

5489 return 

5490 svm_uuid = self._get_unique_svm_by_name(vserver_name) 

5491 

5492 is_dp_destination = vserver_info.get('subtype') == 'dp_destination' 

5493 root_volume_name = self.get_vserver_root_volume_name(vserver_name) 

5494 volumes_count = vserver_client.get_vserver_volume_count() 

5495 

5496 # NOTE(dviroel): 'dp_destination' vservers don't allow to delete its 

5497 # root volume. We can just call vserver-destroy directly. 

5498 if volumes_count == 1 and not is_dp_destination: 

5499 try: 

5500 vserver_client.offline_volume(root_volume_name) 

5501 except netapp_api.api.NaApiError as e: 

5502 if e.code == netapp_api.EREST_ENTRY_NOT_FOUND: 

5503 LOG.error("Cannot delete Vserver %s. " 

5504 "Failed to put volumes offline. " 

5505 "Entry doesn't exist.", vserver_name) 

5506 else: 

5507 raise 

5508 vserver_client.delete_volume(root_volume_name) 

5509 

5510 elif volumes_count > 1: 5510 ↛ 5514line 5510 didn't jump to line 5514 because the condition on line 5510 was always true

5511 msg = _("Cannot delete Vserver. Vserver %s has shares.") 

5512 raise exception.NetAppException(msg % vserver_name) 

5513 

5514 if security_services and not is_dp_destination: 

5515 self._terminate_vserver_services(vserver_name, vserver_client, 

5516 security_services) 

5517 

5518 self.send_request(f'/svm/svms/{svm_uuid}', 'delete') 

5519 

5520 @na_utils.trace 

5521 def get_vserver_volume_count(self): 

5522 """Get number of volumes in SVM.""" 

5523 query = {'return_records': 'false'} 

5524 response = self.send_request('/storage/volumes', 'get', query=query) 

5525 return response['num_records'] 

5526 

5527 @na_utils.trace 

5528 def _terminate_vserver_services(self, vserver_name, vserver_client, 

5529 security_services): 

5530 """Terminate SVM security services.""" 

5531 svm_uuid = self._get_unique_svm_by_name(vserver_name) 

5532 

5533 for service in security_services: 

5534 if service['type'].lower() == 'active_directory': 

5535 body = { 

5536 'ad_domain.password': service['password'], 

5537 'ad_domain.user': service['user'], 

5538 } 

5539 

5540 body_force = { 

5541 'ad_domain.password': service['password'], 

5542 'ad_domain.user': service['user'], 

5543 'force': True 

5544 } 

5545 

5546 try: 

5547 vserver_client.send_request( 

5548 f'/protocols/cifs/services/{svm_uuid}', 'delete', 

5549 body=body) 

5550 except netapp_api.api.NaApiError as e: 

5551 if e.code == netapp_api.EREST_ENTRY_NOT_FOUND: 

5552 LOG.error('CIFS server does not exist for ' 

5553 'Vserver %s.', vserver_name) 

5554 else: 

5555 vserver_client.send_request( 

5556 f'/protocols/cifs/services/{svm_uuid}', 'delete', 

5557 body=body_force) 

5558 elif service['type'].lower() == 'kerberos': 5558 ↛ 5533line 5558 didn't jump to line 5533 because the condition on line 5558 was always true

5559 vserver_client.disable_kerberos(service) 

5560 

5561 @na_utils.trace 

5562 def disable_kerberos(self, security_service): 

5563 """Disable Kerberos in all Vserver LIFs.""" 

5564 

5565 lifs = self.get_network_interfaces() 

5566 

5567 # NOTE(dviroel): If the Vserver has no LIFs, there are no Kerberos 

5568 # to be disabled. 

5569 for lif in lifs: 

5570 body = { 

5571 'password': security_service['password'], 

5572 'user': security_service['user'], 

5573 'interface.name': lif['interface-name'], 

5574 'enabled': False 

5575 } 

5576 

5577 interface_uuid = lif['uuid'] 

5578 

5579 try: 

5580 self.send_request( 

5581 f'/protocols/nfs/kerberos/interfaces/{interface_uuid}', 

5582 'patch', body=body) 

5583 except netapp_api.api.NaApiError as e: 

5584 disabled_msg = ( 

5585 "Kerberos is already enabled/disabled on this LIF") 

5586 if (e.code == netapp_api.EREST_KERBEROS_IS_ENABLED_DISABLED and 5586 ↛ 5590line 5586 didn't jump to line 5590 because the condition on line 5586 was never true

5587 disabled_msg in e.message): 

5588 # NOTE(dviroel): do not raise an error for 'Kerberos is 

5589 # already disabled in this LIF'. 

5590 continue 

5591 msg = ("Failed to disable Kerberos: %s.") 

5592 raise exception.NetAppException(msg % e.message) 

5593 

5594 @na_utils.trace 

5595 def get_vserver_root_volume_name(self, vserver_name): 

5596 """Get the root volume name of the vserver.""" 

5597 unique_volume = self._get_volume_by_args(vserver=vserver_name, 

5598 is_root=True) 

5599 return unique_volume['name'] 

5600 

5601 @na_utils.trace 

5602 def ipspace_has_data_vservers(self, ipspace_name): 

5603 """Check whether an IPspace has any data Vservers assigned to it.""" 

5604 query = {'ipspace.name': ipspace_name} 

5605 result = self.send_request('/svm/svms', 'get', query=query) 

5606 return self._has_records(result) 

5607 

5608 @na_utils.trace 

5609 def delete_vlan(self, node, port, vlan): 

5610 """Delete VLAN port if not in use.""" 

5611 query = { 

5612 'vlan.base_port.name': port, 

5613 'node.name': node, 

5614 'vlan.tag': vlan, 

5615 } 

5616 

5617 try: 

5618 self.send_request('/network/ethernet/ports/', 'delete', 

5619 query=query) 

5620 except netapp_api.api.NaApiError as e: 

5621 if e.code == netapp_api.EREST_ENTRY_NOT_FOUND: 

5622 LOG.debug('VLAN %(vlan)s on port %(port)s node %(node)s ' 

5623 'was not found') 

5624 elif (e.code == netapp_api.EREST_INTERFACE_BOUND or 

5625 e.code == netapp_api.EREST_PORT_IN_USE): 

5626 LOG.debug('VLAN %(vlan)s on port %(port)s node %(node)s ' 

5627 'still used by LIF and cannot be deleted.', 

5628 {'vlan': vlan, 'port': port, 'node': node}) 

5629 else: 

5630 msg = _('Failed to delete VLAN %(vlan)s on ' 

5631 'port %(port)s node %(node)s: %(err_msg)s') 

5632 msg_args = { 

5633 'vlan': vlan, 

5634 'port': port, 

5635 'node': node, 

5636 'err_msg': e.message 

5637 } 

5638 raise exception.NetAppException(msg % msg_args) 

5639 

5640 @na_utils.trace 

5641 def get_degraded_ports(self, broadcast_domains, ipspace_name): 

5642 """Get degraded ports for broadcast domains and an ipspace.""" 

5643 

5644 valid_domains = self._get_valid_broadcast_domains(broadcast_domains) 

5645 

5646 query = { 

5647 'broadcast_domain.name': '|'.join(valid_domains), 

5648 'broadcast_domain.ipspace.name': ipspace_name, 

5649 'state': 'degraded', 

5650 'type': 'vlan', 

5651 'fields': 'node.name,name' 

5652 } 

5653 

5654 result = self.send_request('/network/ethernet/ports', 'get', 

5655 query=query) 

5656 

5657 net_port_info_list = result.get('records', []) 

5658 

5659 ports = [] 

5660 for port_info in net_port_info_list: 

5661 ports.append(f"{port_info['node']['name']}:" 

5662 f"{port_info['name']}") 

5663 

5664 return ports 

5665 

5666 @na_utils.trace 

5667 def _get_valid_broadcast_domains(_self, broadcast_domains): 

5668 valid_domains = [] 

5669 for broadcast_domain in broadcast_domains: 

5670 if ( 

5671 broadcast_domain == 'OpenStack' 

5672 or broadcast_domain == DEFAULT_BROADCAST_DOMAIN 

5673 or broadcast_domain.startswith(BROADCAST_DOMAIN_PREFIX) 

5674 ): 

5675 valid_domains.append(broadcast_domain) 

5676 return valid_domains 

5677 

5678 @na_utils.trace 

5679 def svm_migration_start( 

5680 self, source_cluster_name, source_share_server_name, 

5681 dest_aggregates, dest_ipspace=None, check_only=False): 

5682 """Send a request to start the SVM migration in the backend. 

5683 

5684 :param source_cluster_name: the name of the source cluster. 

5685 :param source_share_server_name: the name of the source server. 

5686 :param dest_aggregates: the aggregates where volumes will be placed in 

5687 the migration. 

5688 :param dest_ipspace: created IPspace for the migration. 

5689 :param check_only: If the call will only check the feasibility. 

5690 deleted after the cutover or not. 

5691 """ 

5692 body = { 

5693 "auto_cutover": False, 

5694 "auto_source_cleanup": True, 

5695 "check_only": check_only, 

5696 "source": { 

5697 "cluster": {"name": source_cluster_name}, 

5698 "svm": {"name": source_share_server_name}, 

5699 }, 

5700 "destination": { 

5701 "volume_placement": { 

5702 "aggregates": dest_aggregates, 

5703 }, 

5704 }, 

5705 } 

5706 

5707 if dest_ipspace: 

5708 ipspace_data = { 

5709 "ipspace": { 

5710 "name": dest_ipspace, 

5711 } 

5712 } 

5713 body["destination"].update(ipspace_data) 

5714 

5715 return self.send_request('/svm/migrations', 'post', body=body, 

5716 wait_on_accepted=False) 

5717 

5718 @na_utils.trace 

5719 def get_migration_check_job_state(self, job_id): 

5720 """Get the job state of a share server migration. 

5721 

5722 :param job_id: id of the job to be searched. 

5723 """ 

5724 try: 

5725 job = self.get_job(job_id) 

5726 return job 

5727 except netapp_api.api.NaApiError as e: 

5728 if e.code == netapp_api.EREST_NFS_V4_0_ENABLED_MIGRATION_FAILURE: 

5729 msg = _( 

5730 'NFS v4.0 is not supported while migrating vservers.') 

5731 LOG.error(msg) 

5732 raise exception.NetAppException(message=e.message) 

5733 if e.code == netapp_api.EREST_VSERVER_MIGRATION_TO_NON_AFF_CLUSTER: 

5734 msg = _('Both source and destination clusters must be AFF ' 

5735 'systems.') 

5736 LOG.error(msg) 

5737 raise exception.NetAppException(message=e.message) 

5738 msg = (_('Failed to check migration support. Reason: ' 

5739 '%s' % e.message)) 

5740 raise exception.NetAppException(msg) 

5741 

5742 @na_utils.trace 

5743 def svm_migrate_complete(self, migration_id): 

5744 """Send a request to complete the SVM migration. 

5745 

5746 :param migration_id: the id of the migration provided by the storage. 

5747 """ 

5748 body = { 

5749 "action": "cutover" 

5750 } 

5751 

5752 return self.send_request( 

5753 f'/svm/migrations/{migration_id}', 'patch', body=body, 

5754 wait_on_accepted=False) 

5755 

5756 @na_utils.trace 

5757 def svm_migrate_cancel(self, migration_id): 

5758 """Send a request to cancel the SVM migration. 

5759 

5760 :param migration_id: the id of the migration provided by the storage. 

5761 """ 

5762 return self.send_request(f'/svm/migrations/{migration_id}', 'delete', 

5763 wait_on_accepted=False) 

5764 

5765 @na_utils.trace 

5766 def svm_migration_get(self, migration_id): 

5767 """Send a request to get the progress of the SVM migration. 

5768 

5769 :param migration_id: the id of the migration provided by the storage. 

5770 """ 

5771 return self.send_request(f'/svm/migrations/{migration_id}', 'get') 

5772 

5773 @na_utils.trace 

5774 def svm_migrate_pause(self, migration_id): 

5775 """Send a request to pause a migration. 

5776 

5777 :param migration_id: the id of the migration provided by the storage. 

5778 """ 

5779 body = { 

5780 "action": "pause" 

5781 } 

5782 

5783 return self.send_request( 

5784 f'/svm/migrations/{migration_id}', 'patch', body=body, 

5785 wait_on_accepted=False) 

5786 

5787 @na_utils.trace 

5788 def delete_network_interface(self, vserver_name, interface_name): 

5789 """Delete the LIF, disabling it before.""" 

5790 self.disable_network_interface(vserver_name, interface_name) 

5791 

5792 query = { 

5793 'svm.name': vserver_name, 

5794 'name': interface_name 

5795 } 

5796 self.send_request('/network/ip/interfaces', 'delete', query=query) 

5797 

5798 @na_utils.trace 

5799 def disable_network_interface(self, vserver_name, interface_name): 

5800 """Disable the LIF.""" 

5801 body = { 

5802 'enabled': 'false' 

5803 } 

5804 query = { 

5805 'svm.name': vserver_name, 

5806 'name': interface_name 

5807 } 

5808 self.send_request('/network/ip/interfaces', 'patch', body=body, 

5809 query=query) 

5810 

5811 @na_utils.trace 

5812 def get_ipspaces(self, ipspace_name=None, vserver_name=None): 

5813 """Gets one or more IPSpaces.""" 

5814 

5815 query = { 

5816 'name': ipspace_name 

5817 } 

5818 result = self.send_request('/network/ipspaces', 'get', 

5819 query=query) 

5820 

5821 if not self._has_records(result): 

5822 return [] 

5823 

5824 ipspace_info = result.get('records')[0] 

5825 

5826 query = { 

5827 'broadcast_domain.ipspace.name': ipspace_name 

5828 } 

5829 ports = self.send_request('/network/ethernet/ports', 

5830 'get', query=query) 

5831 

5832 query = { 

5833 'ipspace.name': ipspace_name 

5834 } 

5835 vservers = self.send_request('/svm/svms', 

5836 'get', query=query) 

5837 

5838 br_domains = self.send_request('/network/ethernet/broadcast-domains', 

5839 'get', query=query) 

5840 

5841 ipspace = { 

5842 'ports': [], 

5843 'vservers': [], 

5844 'broadcast-domains': [], 

5845 } 

5846 

5847 for port in ports.get('records'): 

5848 ipspace['ports'].append(port.get('name')) 

5849 

5850 for vserver in vservers.get('records'): 

5851 ipspace['vservers'].append(vserver.get('name')) 

5852 

5853 for broadcast in br_domains.get('records'): 

5854 ipspace['broadcast-domains'].append(broadcast.get('name')) 

5855 

5856 ipspace['ipspace'] = ipspace_info.get('name') 

5857 

5858 ipspace['uuid'] = ipspace_info.get('uuid') 

5859 

5860 return ipspace 

5861 

5862 @na_utils.trace 

5863 def _delete_port_and_broadcast_domain(self, domain, ipspace): 

5864 """Delete a broadcast domain and its ports.""" 

5865 

5866 ipspace_name = ipspace['ipspace'] 

5867 ports = ipspace['ports'] 

5868 

5869 for port in ports: 

5870 self._delete_port_by_ipspace_and_broadcast_domain( 

5871 port, 

5872 domain, 

5873 ipspace_name) 

5874 

5875 query = { 

5876 'name': domain, 

5877 'ipspace.name': ipspace_name 

5878 } 

5879 

5880 self.send_request('/network/ethernet/broadcast-domains', 'delete', 

5881 query=query) 

5882 

5883 @na_utils.trace 

5884 def _delete_port_and_broadcast_domains_for_ipspace(self, ipspace_name): 

5885 """Deletes all broadcast domains in an IPspace.""" 

5886 ipspace = self.get_ipspaces(ipspace_name) 

5887 if not ipspace: 

5888 return 

5889 

5890 for broadcast_domain_name in ipspace['broadcast-domains']: 

5891 self._delete_port_and_broadcast_domain(broadcast_domain_name, 

5892 ipspace) 

5893 

5894 @na_utils.trace 

5895 def delete_ipspace(self, ipspace_name): 

5896 """Deletes an IPspace 

5897 

5898 Returns: 

5899 True if ipspace was deleted, 

5900 False if validation or error prevented deletion 

5901 """ 

5902 if not self.features.IPSPACES: 5902 ↛ 5903line 5902 didn't jump to line 5903 because the condition on line 5902 was never true

5903 return False 

5904 

5905 if not ipspace_name: 5905 ↛ 5906line 5905 didn't jump to line 5906 because the condition on line 5905 was never true

5906 return False 

5907 

5908 if ( 5908 ↛ 5912line 5908 didn't jump to line 5912 because the condition on line 5908 was never true

5909 ipspace_name in CLUSTER_IPSPACES 

5910 or self.ipspace_has_data_vservers(ipspace_name) 

5911 ): 

5912 LOG.debug('IPspace %(ipspace)s not deleted: still in use.', 

5913 {'ipspace': ipspace_name}) 

5914 return False 

5915 

5916 try: 

5917 self._delete_port_and_broadcast_domains_for_ipspace(ipspace_name) 

5918 except netapp_api.NaApiError as e: 

5919 msg = _('Broadcast Domains of IPspace %s not deleted. ' 

5920 'Reason: %s') % (ipspace_name, e) 

5921 LOG.warning(msg) 

5922 return False 

5923 

5924 query = { 

5925 'name': ipspace_name 

5926 } 

5927 try: 

5928 self.send_request('/network/ipspaces', 'delete', query=query) 

5929 except netapp_api.NaApiError as e: 

5930 msg = _('IPspace %s not deleted. Reason: %s') % (ipspace_name, e) 

5931 LOG.warning(msg) 

5932 return False 

5933 

5934 return True 

5935 

5936 @na_utils.trace 

5937 def get_svm_volumes_total_size(self, svm_name): 

5938 """Gets volumes sizes sum (GB) from all volumes in SVM by svm_name""" 

5939 

5940 query = { 

5941 'svm.name': svm_name, 

5942 'fields': 'size' 

5943 } 

5944 

5945 response = self.send_request('/storage/volumes/', 'get', query=query) 

5946 

5947 svm_volumes = response.get('records', []) 

5948 

5949 if len(svm_volumes) > 0: 5949 ↛ 5957line 5949 didn't jump to line 5957 because the condition on line 5949 was always true

5950 total_volumes_size = 0 

5951 for volume in svm_volumes: 

5952 # Root volumes are not taking account because they are part of 

5953 # SVM creation. 

5954 if volume['name'] != 'root': 5954 ↛ 5951line 5954 didn't jump to line 5951 because the condition on line 5954 was always true

5955 total_volumes_size = total_volumes_size + volume['size'] 

5956 else: 

5957 return 0 

5958 

5959 # Convert Bytes to GBs. 

5960 return (total_volumes_size / 1024**3) 

5961 

5962 def snapmirror_restore_vol(self, source_path=None, dest_path=None, 

5963 source_vserver=None, dest_vserver=None, 

5964 source_volume=None, dest_volume=None, 

5965 des_cluster=None, source_snapshot=None): 

5966 """Restore snapshot copy from destination volume to source volume""" 

5967 snapmirror_info = self.get_snapmirrors(dest_path, source_path) 

5968 if not snapmirror_info: 5968 ↛ 5969line 5968 didn't jump to line 5969 because the condition on line 5968 was never true

5969 msg = _("There is no relationship between source " 

5970 "'%(source_path)s' and destination cluster" 

5971 " '%(des_path)s'") 

5972 msg_args = {'source_path': source_path, 

5973 'des_path': dest_path, 

5974 } 

5975 raise exception.NetAppException(msg % msg_args) 

5976 uuid = snapmirror_info[0].get('uuid') 

5977 body = {"destination": {"path": dest_path, 

5978 "cluster": {"name": des_cluster}}, 

5979 "source_snapshot": source_snapshot} 

5980 try: 

5981 self.send_request(f"/snapmirror/relationships/{uuid}/restore", 

5982 'post', body=body) 

5983 except netapp_api.api.NaApiError as e: 

5984 LOG.debug('Snapmirror restore has failed. Error: %s. Code: %s', 

5985 e.message, e.code) 

5986 raise 

5987 

5988 @na_utils.trace 

5989 def list_volume_snapshots(self, volume_name, snapmirror_label=None, 

5990 newer_than=None): 

5991 """Gets list of snapshots of volume.""" 

5992 volume = self._get_volume_by_args(vol_name=volume_name) 

5993 uuid = volume['uuid'] 

5994 query = {} 

5995 if snapmirror_label: 

5996 query = { 

5997 'snapmirror_label': snapmirror_label, 

5998 } 

5999 

6000 if newer_than: 

6001 query['create_time'] = '>' + newer_than 

6002 

6003 response = self.send_request( 

6004 f'/storage/volumes/{uuid}/snapshots/', 

6005 'get', query=query) 

6006 

6007 return [snapshot_info['name'] 

6008 for snapshot_info in response['records']] 

6009 

6010 @na_utils.trace 

6011 def is_snaplock_compliance_clock_configured(self, node_name): 

6012 """Get the SnapLock compliance clock is configured for each node""" 

6013 node_uuid = self._get_cluster_node_uuid(node_name) 

6014 response = self.send_request( 

6015 f'/storage/snaplock/compliance-clocks/{node_uuid}', 

6016 'get' 

6017 ) 

6018 clock_fmt_value = response.get('time') 

6019 if clock_fmt_value is None: 

6020 return False 

6021 return True 

6022 

6023 @na_utils.trace 

6024 def set_snaplock_attributes(self, volume_name, **options): 

6025 """Set the retention period for SnapLock enabled volume""" 

6026 body = {} 

6027 snaplock_attribute_mapping = { 

6028 'snaplock_autocommit_period': 'snaplock.autocommit_period', 

6029 'snaplock_min_retention_period': 'snaplock.retention.minimum', 

6030 'snaplock_max_retention_period': 'snaplock.retention.maximum', 

6031 'snaplock_default_retention_period': 'snaplock.retention.default', 

6032 } 

6033 for share_type_attr, na_api_attr in snaplock_attribute_mapping.items(): 

6034 if options.get(share_type_attr): 

6035 if share_type_attr == 'snaplock_default_retention_period': 

6036 default_retention_period = options.get( 

6037 'snaplock_default_retention_period' 

6038 ) 

6039 if default_retention_period == "max": 

6040 options[share_type_attr] =\ 

6041 options.get('snaplock_max_retention_period') 

6042 elif default_retention_period == "min": 

6043 options[share_type_attr] = \ 

6044 options.get('snaplock_min_retention_period') 

6045 

6046 body[na_api_attr] = utils.convert_time_duration_to_iso_format( 

6047 options.get(share_type_attr)) 

6048 

6049 if all(value is None for value in body.values()): 

6050 LOG.debug("All SnapLock attributes are None, not" 

6051 " updating SnapLock attributes") 

6052 return 

6053 

6054 volume = self._get_volume_by_args(vol_name=volume_name) 

6055 uuid = volume['uuid'] 

6056 self.send_request(f'/storage/volumes/{uuid}', 

6057 'patch', body=body) 

6058 

6059 @na_utils.trace 

6060 def _is_snaplock_enabled_volume(self, volume_name): 

6061 """Get whether volume is SnapLock enabled or disabled""" 

6062 vol_attr = self.get_volume(volume_name) 

6063 return vol_attr.get('snaplock-type') in ("compliance", "enterprise") 

6064 

6065 @na_utils.trace 

6066 def _get_cluster_node_uuid(self, node_name): 

6067 query = { 

6068 'name': node_name 

6069 } 

6070 response = self.send_request('/cluster/nodes', 

6071 'get', query=query) 

6072 return response.get('records')[0].get('uuid') 

6073 

6074 @na_utils.trace 

6075 def get_storage_failover_partner(self, node_name): 

6076 """Get the partner node of HA pair""" 

6077 node_uuid = self._get_cluster_node_uuid(node_name) 

6078 node_details = self.send_request(f'/cluster/nodes/{node_uuid}', 'get') 

6079 return node_details['ha']['partners'][0]['name'] 

6080 

6081 @na_utils.trace 

6082 def get_migratable_data_lif_for_node(self, node): 

6083 """Get available LIFs that can be migrated to another node.""" 

6084 protocols = ['data_nfs', 'data_cifs'] 

6085 query = { 

6086 'services': '|'.join(protocols), 

6087 'location.home_node.name': node, 

6088 'fields': 'name', 

6089 } 

6090 result = self.send_request('/network/ip/interfaces', 'get', 

6091 query=query) 

6092 migratable_lif = [] 

6093 if self._has_records(result): 6093 ↛ 6102line 6093 didn't jump to line 6102 because the condition on line 6093 was always true

6094 result = result.get('records', []) 

6095 for lif in result: 

6096 lif_result = self.send_request( 

6097 f'/network/ip/interfaces/{lif.get("uuid")}', 'get' 

6098 ) 

6099 failover_policy = lif_result['location']['failover'] 

6100 if failover_policy in ('default', 'sfo_partners_only'): 6100 ↛ 6095line 6100 didn't jump to line 6095 because the condition on line 6100 was always true

6101 migratable_lif.append(lif["name"]) 

6102 return migratable_lif