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
« 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.
15import copy
16from datetime import datetime
17from http import client as http_client
18import math
19import re
20import time
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
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
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
57class NetAppRestClient(object):
59 def __init__(self, **kwargs):
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'])
76 self.async_rest_timeout = kwargs['async_rest_timeout']
78 self.vserver = kwargs.get('vserver')
80 self.connection.set_vserver(self.vserver)
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)
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)
100 self._have_cluster_creds = self._check_for_cluster_credentials()
102 self._init_features()
104 def _init_features(self):
105 """Initialize feature support map."""
106 self.features = client_base.Features()
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)
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)
143 def _wait_job_result(self, job_url):
145 interval = 2
146 retries = (self.async_rest_timeout / interval)
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)
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)
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.')
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)
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):
178 """Sends REST request to ONTAP.
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
190 :returns: parsed REST response
191 """
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
198 response = None
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)
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)
213 return response
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.
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
224 :returns: dict containing records and num_records
225 """
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
232 # Initialize query variable if it is None
233 query = query if query else {}
234 query['max_records'] = max_page_length
236 _, response = self.connection.invoke_successfully(
237 action_url, 'get', query=query,
238 enable_tunneling=enable_tunneling)
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'
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)
253 response['num_records'] += next_response.get('num_records', 0)
254 response['records'].extend(next_response.get('records'))
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'
260 return response
262 @na_utils.trace
263 def get_ontap_version(self, cached=True):
264 """Get the current Data ONTAP version."""
266 if cached:
267 return self.connection.get_ontap_version()
269 query = {
270 'fields': 'version'
271 }
273 try:
274 response = self.send_request('/cluster/nodes', 'get', query=query,
275 enable_tunneling=False)
276 records = response.get('records')[0]
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
288 # NOTE(nahimsouza): SVM scoped account is not authorized to access
289 # the /cluster/nodes endpoint, that's why we use /private/cli
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 }
305 @na_utils.trace
306 def get_job(self, job_uuid):
307 """Get a job in ONTAP.
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)
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
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.")
332 return sorted(
333 [license['name'] for license in result.get('records', [])])
335 @na_utils.trace
336 def _get_security_key_manager_nve_support(self):
337 """Determine whether the cluster platform supports Volume Encryption"""
339 query = {'fields': 'volume_encryption.*'}
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
353 LOG.debug("NVE disabled - Key management is not "
354 "configured on the admin Vserver.")
355 return False
357 @na_utils.trace
358 def is_nve_supported(self):
359 """Determine whether NVE is supported on this platform."""
361 nodes = self.list_cluster_nodes()
362 system_version = self.get_ontap_version()
363 version = system_version.get('version')
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
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
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
400 @na_utils.trace
401 def list_cluster_nodes(self):
402 """Get all available cluster nodes."""
404 result = self.send_request('/cluster/nodes', 'get')
405 return [node['name'] for node in result.get('records', [])]
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."""
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 }
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
432 volumes_response = self.send_request(
433 '/storage/volumes/', 'get', query=query)
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)
441 return records[0]
443 @na_utils.trace
444 def restore_snapshot(self, volume_name, snapshot_name):
445 """Reverts a volume to the specified snapshot."""
447 volume = self._get_volume_by_args(vol_name=volume_name)
448 uuid = volume['uuid']
450 body = {
451 'restore_to.snapshot.name': snapshot_name
452 }
454 # Update volume
455 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body)
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)
462 query = {
463 'name': vserver_name
464 }
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)
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']]
483 @na_utils.trace
484 def list_non_root_aggregates(self):
485 """Get names of all aggregates that don't contain node root volumes."""
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']
494 return [aggr['name'] for aggr in aggr_list]
496 @na_utils.trace
497 def get_cluster_aggregate_capacities(self, aggregate_names):
498 """Calculates capacity of one or more aggregates.
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 {}
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']
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
527 @na_utils.trace
528 def _get_aggregates(self, aggregate_names=None, fields=None):
529 """Get a list of aggregates and their attributes.
531 :param aggregate_names: List of aggregate names.
532 :param fields: List of fields to be retrieved from each aggregate.
534 :return: List of aggregates.
535 """
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)
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
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)
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', [])
553 @na_utils.trace
554 def get_aggregate(self, aggregate_name):
555 """Get aggregate attributes needed for the storage service catalog."""
557 if not aggregate_name:
558 return {}
560 fields = ('name,block_storage.primary.raid_type,'
561 'block_storage.storage_type,snaplock_type')
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 {}
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 {}
574 aggr_attributes = aggrs[0]
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 }
587 return aggregate
589 @na_utils.trace
590 def get_node_for_aggregate(self, aggregate_name):
591 """Get home node for the specified aggregate.
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 """
597 if not aggregate_name:
598 return None
600 fields = 'name,home_node.name'
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
613 return aggrs[0]['home_node']['name'] if aggrs else None
615 @na_utils.trace
616 def get_aggregate_disk_types(self, aggregate_name):
617 """Get the disk type(s) of an aggregate."""
619 disk_types = set()
620 disk_types.update(self._get_aggregate_disk_types(aggregate_name))
622 return list(disk_types) if disk_types else None
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)."""
628 disk_types = set()
630 query = {
631 'aggregates.name': aggregate_name,
632 'fields': 'effective_type'
633 }
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)
642 return disk_types
644 for storage_disk_info in response['records']:
645 disk_types.add(storage_disk_info['effective_type'])
647 return disk_types
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)
654 query = {
655 'name': volume_name
656 }
658 result = self.send_request(
659 '/storage/volumes', 'get', query=query)
660 return self._has_records(result)
662 @na_utils.trace
663 def list_vserver_aggregates(self):
664 """Returns a list of aggregates available to a vserver.
666 This must be called against a Vserver LIF.
667 """
668 return list(self.get_vserver_aggregate_capacities().keys())
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.
674 Returns dictionary of aggregate capacity metrics. This must
675 be called against a Vserver LIF.
676 """
678 if aggregate_names is not None and len(aggregate_names) == 0:
679 return {}
681 query = {
682 'fields': 'name,aggregates.name,aggregates.available_size'
683 }
684 response = self.send_request('/svm/svms', 'get', query=query)
686 if not response['records']:
687 msg = _('Could not find information of vserver.')
688 raise exception.NetAppException(message=msg)
689 vserver = response['records'][0]
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 {}
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}
707 if not aggr_space_dict:
708 LOG.warning('No aggregates assigned to Vserver %s.',
709 vserver['name'])
710 return {}
712 LOG.debug('Found available Vserver aggregates: %s.', aggr_space_dict)
713 return aggr_space_dict
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."""
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)
749 @na_utils.trace
750 def list_network_interfaces(self):
751 """Get the names of available LIFs."""
753 query = {
754 'fields': 'name'
755 }
757 result = self.send_request('/network/ip/interfaces', 'get',
758 query=query)
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', [])]
763 @na_utils.trace
764 def get_network_interfaces(self, protocols=None):
765 """Get available LIFs."""
767 protocols = na_utils.convert_to_list(protocols)
768 protocols = [f"data_{protocol.lower()}" for protocol in protocols]
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 }
784 result = self.send_request('/network/ip/interfaces', 'get',
785 query=query)
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
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')
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}
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
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
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)
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})
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)
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)
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']
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 }
888 result = self.send_request(
889 f'/storage/volumes/{vol_uuid}', 'get', query=query)
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
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 }
907 result = self.send_request('/storage/volumes', 'get', query=query)
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)
916 volume_infos = result['records'][0]
917 aggregates = volume_infos.get('aggregates', [])
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]
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
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)
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)
965 @na_utils.trace
966 def set_volume_security_style(self, volume_name, security_style='unix'):
967 """Set volume security style"""
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)
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']
988 self.send_request(
989 f'/protocols/cifs/shares/{svm_uuid}/{share_name}'
990 f'/acls/{user_name}/{CIFS_USER_GROUP_TYPE}', 'delete')
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."""
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 )
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)
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)
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.
1041 If the parameter `is_flexgroup` is False, the creation process is
1042 made synchronously to replicate ZAPI behavior for FlexVol creation.
1044 """
1046 body = {
1047 'size': size_gb * units.Gi,
1048 'name': volume_name,
1049 }
1051 body['style'] = 'flexgroup' if is_flexgroup else 'flexvol'
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]
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))
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)
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
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."""
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
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'
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
1116 return body
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 }
1126 result = self.send_request('/cluster/jobs/', 'get', query=query,
1127 enable_tunneling=False)
1129 job_info = result.get('records', [])
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)
1140 return job_info[0]['state']
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)
1162 return {
1163 'dedupe': dedupe,
1164 'compression': compression,
1165 }
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']
1173 body = {
1174 'snapshot_policy.name': snapshot_policy
1175 }
1176 # update snapshot policy
1177 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body)
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."""
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)
1200 self.apply_volume_efficiency_policy(
1201 volume_name, efficiency_policy=efficiency_policy)
1203 @na_utils.trace
1204 def enable_dedupe_async(self, volume_name):
1205 """Enable deduplication on FlexVol/FlexGroup volume asynchronously."""
1207 volume = self._get_volume_by_args(vol_name=volume_name)
1208 uuid = volume['uuid']
1210 body = {
1211 'efficiency': {'dedupe': 'background'}
1212 }
1213 # update volume efficiency
1214 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body)
1216 @na_utils.trace
1217 def disable_dedupe_async(self, volume_name):
1218 """Disable deduplication on FlexVol/FlexGroup volume asynchronously."""
1220 volume = self._get_volume_by_args(vol_name=volume_name)
1221 uuid = volume['uuid']
1223 body = {
1224 'efficiency': {'dedupe': 'none'}
1225 }
1226 # update volume efficiency
1227 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body)
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']
1235 body = {
1236 'efficiency': {'compression': 'background'}
1237 }
1238 # update volume efficiency
1239 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body)
1241 @na_utils.trace
1242 def disable_compression_async(self, volume_name):
1243 """Disable compression on FlexVol/FlexGroup volume asynchronously."""
1245 volume = self._get_volume_by_args(vol_name=volume_name)
1246 uuid = volume['uuid']
1248 body = {
1249 'efficiency': {'compression': 'none'}
1250 }
1251 # update volume efficiency
1252 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body)
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']
1262 body = {
1263 'efficiency': {'policy': efficiency_policy}
1264 }
1266 # update volume efficiency policy only if policy_name is provided
1267 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body)
1269 @na_utils.trace
1270 def set_volume_max_files(self, volume_name, max_files,
1271 retry_allocated=False):
1272 """Set share file limit."""
1274 try:
1275 volume = self._get_volume_by_args(vol_name=volume_name)
1276 uuid = volume['uuid']
1278 body = {
1279 'files.maximum': int(max_files)
1280 }
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)
1305 @na_utils.trace
1306 def get_volume_allocated_files(self, volume_name):
1307 """Get share allocated files."""
1309 try:
1310 volume = self._get_volume_by_args(vol_name=volume_name)
1311 uuid = volume['uuid']
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}
1325 @na_utils.trace
1326 def set_volume_snapdir_access(self, volume_name, hide_snapdir):
1327 """Set volume snapshot directory visibility."""
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)
1336 uuid = volume['uuid']
1338 body = {
1339 'snapshot_directory_access_enabled': str(not hide_snapdir).lower()
1340 }
1342 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body)
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.
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']
1364 except exception.NetAppException:
1365 LOG.debug('Could not find fpolicy. Share not found: %s.',
1366 share_name)
1367 return []
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
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])
1383 result = self.send_request(
1384 f'/protocols/fpolicy/{svm_uuid}/policies', 'get', query=query)
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', [])
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 })
1403 return fpolicy_scopes
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
1416 result = self.send_request(
1417 f'/protocols/fpolicy/{svm_uuid}/policies', 'get', query=query)
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 })
1431 return fpolicy_status
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.
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 = {}
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])
1458 result = self.send_request(
1459 f'/protocols/fpolicy/{svm_uuid}/policies', 'get', query=query)
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 })
1475 return fpolicy_policies
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.
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]))
1500 result = self.send_request(
1501 f'/protocols/fpolicy/{svm_uuid}/events', 'get', query=query)
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', '')
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)
1515 fpolicy_events.append({
1516 'event-name': name,
1517 'protocol': proto,
1518 'file-operations': file_operations
1519 })
1521 return fpolicy_events
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.
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'
1545 self.send_request(f'/protocols/fpolicy/{svm_uuid}/events', 'post',
1546 body=body)
1548 @na_utils.trace
1549 def delete_fpolicy_event(self, share_name, event_name):
1550 """Deletes a fpolicy policy event.
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)
1573 @na_utils.trace
1574 def delete_fpolicy_policy(self, share_name, policy_name):
1575 """Deletes a fpolicy policy.
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)
1597 @na_utils.trace
1598 def enable_fpolicy_policy(self, share_name, policy_name, sequence_number):
1599 """Enables a specific named policy.
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 }
1612 self.send_request(
1613 f'/protocols/fpolicy/{svm_uuid}/policies/{policy_name}', 'patch',
1614 body=body)
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.
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']
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
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])
1648 self.send_request(f'/protocols/fpolicy/{svm_uuid}/policies/',
1649 'patch', body=body)
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.
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']
1671 body = {
1672 'name': fpolicy_name,
1673 'events.name': events,
1674 'engine.name': engine,
1675 'scope.include_shares': [share_name]
1676 }
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(',')
1683 self.send_request(f'/protocols/fpolicy/{svm_uuid}/policies', 'post',
1684 body=body)
1686 @na_utils.trace
1687 def delete_nfs_export_policy(self, policy_name):
1688 """Delete NFS export policy."""
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']
1700 # Remove policy.
1701 self.send_request(f'/protocols/nfs/export-policies/{policy_id}',
1702 'delete')
1704 @na_utils.trace
1705 def remove_cifs_share(self, share_name):
1706 """Remove CIFS share from the CIFS server."""
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']
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
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']
1735 # Unmount volume async operation.
1736 body = {"nas": {"path": ""}}
1737 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body)
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.
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 """
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
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)
1779 @na_utils.trace
1780 def get_clones_of_parent_volume(self, vserver, volume):
1781 """get one or more clones of given parent volume"""
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
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']
1802 body = {'state': 'online'}
1803 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body)
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']
1812 body = {'state': 'offline'}
1813 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body)
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']
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)
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']
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)
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)
1856 @na_utils.trace
1857 def get_deleted_volumes_to_prune(self):
1858 """Returns a list of deleted volumes to prune."""
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', [])
1872 @na_utils.trace
1873 def prune_deleted_volumes(self):
1874 """Prunes deleted volumes."""
1875 LOG.debug('Checking for deleted volumes to prune.')
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')
1883 client = copy.deepcopy(self)
1884 client.set_vserver(vserver)
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)
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)
1920 @na_utils.trace
1921 def qos_policy_group_get(self, qos_policy_group_name):
1922 """Checks if a QoS policy group exists."""
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
1942 if not res.get('records'):
1943 msg = _('QoS %s not found.') % qos_policy_group_name
1944 raise exception.NetAppException(msg)
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 }
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')
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
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')
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
1977 return policy_info
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
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']
2004 body = {"name": new_name}
2005 self.send_request(f'/storage/qos/policies/{uuid}', 'patch',
2006 body=body)
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)
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.
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()
2048 @na_utils.trace
2049 def qos_policy_group_modify(self, qos_policy_group_name, max_throughput):
2050 """Modifies a QoS policy group."""
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)
2076 @na_utils.trace
2077 def set_volume_size(self, volume_name, size_gb):
2078 """Set volume size."""
2080 volume = self._get_volume_by_args(vol_name=volume_name)
2081 uuid = volume['uuid']
2083 body = {
2084 'space.size': int(size_gb) * units.Gi
2085 }
2087 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body)
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)
2102 @na_utils.trace
2103 def create_snapshot(self, volume_name, snapshot_name,
2104 snapmirror_label=None):
2105 """Creates a volume snapshot."""
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)
2117 @na_utils.trace
2118 def is_flexgroup_supported(self):
2119 return self.features.FLEXGROUP
2121 @na_utils.trace
2122 def is_flexgroup_volume(self, volume_name):
2123 """Determines if the ONTAP volume is FlexGroup."""
2125 query = {
2126 'name': volume_name,
2127 'fields': 'style'
2128 }
2129 result = self.send_request('/storage/volumes/', 'get', query=query)
2131 if not self._has_records(result):
2132 raise exception.StorageResourceNotFound(name=volume_name)
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)
2140 return na_utils.is_style_extended_flexgroup(vols[0]['style'])
2142 @staticmethod
2143 def _is_busy_snapshot(snapshot_owners):
2144 """Checks if the owners means that the snapshot is busy.
2146 Snapshot is busy when any of the owners doesn't end with 'dependent'.
2147 """
2149 for owner in snapshot_owners:
2150 if not owner.endswith('dependent'):
2151 return True
2153 return False
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)
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)
2173 if not self._has_records(result):
2174 raise exception.SnapshotResourceNotFound(name=snapshot_name)
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)
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 }
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."""
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)
2207 return [{'name': volume['name']}
2208 for volume in result.get('records', [])]
2210 @na_utils.trace
2211 def volume_clone_split_start(self, volume_name):
2212 """Begins splitting a clone from its parent."""
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)
2222 @na_utils.trace
2223 def volume_clone_split_status(self, volume_name):
2224 """Status of splitting a clone from its parent."""
2226 query = {
2227 'name': volume_name,
2228 'fields': 'clone.split_complete_percent'
2229 }
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
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
2247 @na_utils.trace
2248 def volume_clone_split_stop(self, volume_name):
2249 """Stops splitting a clone from its parent."""
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
2266 @na_utils.trace
2267 def delete_snapshot(self, volume_name, snapshot_name, ignore_owners=False):
2268 """Deletes a volume snapshot."""
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']
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
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')
2304 @na_utils.trace
2305 def rename_snapshot_and_split_clones(self, volume_name, snapshot_name):
2306 """Renames volume snapshot & splits clones."""
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
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'])
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)
2337 @na_utils.trace
2338 def rename_snapshot(self, volume_name, snapshot_name, new_snapshot_name):
2339 """Renames the snapshot."""
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)
2352 @na_utils.trace
2353 def _get_soft_deleted_snapshots(self):
2354 """Returns non-busy, soft-deleted snapshots suitable for reaping."""
2356 query = {
2357 'name': DELETED_PREFIX + '*',
2358 'fields': 'uuid,volume,owners,svm.name'
2359 }
2360 result = self.get_records('/storage/volumes/*/snapshots', query=query)
2362 snapshot_map = {}
2363 for snapshot_info in result.get('records', []):
2364 if self._is_busy_snapshot(snapshot_info['owners']):
2365 continue
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
2375 return snapshot_map
2377 @na_utils.trace
2378 def prune_deleted_snapshots(self):
2379 """Deletes non-busy snapshots that were previously soft-deleted."""
2381 deleted_snapshots_map = self._get_soft_deleted_snapshots()
2383 for vserver in deleted_snapshots_map:
2384 client = copy.deepcopy(self)
2385 client.set_vserver(vserver)
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)
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})
2409 volume = self._get_volume_by_args(vol_name=volume_name,
2410 fields='uuid,state')
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)
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)
2427 return self._has_records(result)
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)
2434 query = {
2435 'location.volume.name': volume_name,
2436 }
2438 response = self.send_request('/storage/luns/', 'get', query=query)
2440 return self._has_records(response)
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
2448 query = {
2449 'nas.path': junction_path + '/*'
2450 }
2452 response = self.send_request('/storage/volumes/', 'get', query=query)
2453 return self._has_records(response)
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']
2461 body = {
2462 'name': new_volume_name
2463 }
2465 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body)
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']
2473 body = {
2474 'nas.path': (junction_path if junction_path
2475 else '/%s' % volume_name)
2476 }
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
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
2494 query = {
2495 'nas.path': junction_path,
2496 'fields': 'name',
2497 }
2499 response = self.send_request('/storage/volumes/', 'get', query=query)
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
2504 vol = response.get('records')[0]
2506 volume = {
2507 'name': vol.get('name'),
2508 }
2509 return volume
2511 @na_utils.trace
2512 def get_aggregate_for_volume(self, volume_name):
2513 """Get the name of the aggregate containing a volume."""
2515 query = {
2516 'name': volume_name,
2517 'fields': 'aggregates',
2518 }
2520 res = self.send_request('/storage/volumes/', 'get', query=query)
2522 aggregate = res.get('aggregates')
2524 if not aggregate:
2525 msg = _('Could not find aggregate for volume %s.')
2526 raise exception.NetAppException(msg % volume_name)
2528 aggregate_size = len(res.get('aggregates'))
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')]
2533 return aggregate
2535 @na_utils.trace
2536 def get_volume_to_manage(self, aggregate_name, volume_name):
2537 """Get existing volume info to be managed."""
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 }
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
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')]
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 }
2574 return volume
2576 @na_utils.trace
2577 def _parse_timestamp(self, time_str):
2578 """Parse timestamp string into a number."""
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
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."""
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']
2603 query = {}
2604 query['fields'] = ','.join(fields)
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
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
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'
2623 response = self.send_request(
2624 '/snapmirror/relationships', 'get', query=query,
2625 enable_tunneling=enable_tunneling)
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'),
2659 })
2661 return snapmirrors
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)
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.
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 """
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)
2696 return snapmirrors
2698 @na_utils.trace
2699 def volume_has_snapmirror_relationships(self, volume):
2700 """Return True if snapmirror relationships exist for a given volume.
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'])
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'])
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
2724 return has_snapmirrors
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.
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 """
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
2768 if language:
2769 body['language'] = language
2771 if max_files:
2772 body['files'] = {'maximum': max_files}
2774 if snapshot_policy:
2775 body['snapshot_policy'] = {'name': snapshot_policy}
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}}
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())
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
2794 volume = self._get_volume_by_args(vol_name=volume_name,
2795 aggregate_name=aggregates)
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)
2809 @na_utils.trace
2810 def _build_autosize_attributes(self, autosize_attributes):
2811 """Build autosize attributes dict from autosize_attributes."""
2812 src = autosize_attributes
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 }
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 }
2828 # Add mode if present
2829 if 'mode' in src:
2830 autosize['mode'] = src['mode']
2832 return autosize if autosize else None
2834 @na_utils.trace
2835 def reset_volume_autosize(self, volume_name, vserver_name):
2836 """Resets volume autosize configuration to default values.
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 }
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
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.
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)
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.
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)
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.
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]
2913 query = {
2914 'name': volume_name,
2915 }
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'
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'
2925 self.send_request('/storage/volumes/', 'patch', query=query, body=body,
2926 wait_on_accepted=False)
2928 @na_utils.trace
2929 def get_nfs_export_policy_for_volume(self, volume_name):
2930 """Get the actual export policy for a share."""
2932 query = {
2933 'name': volume_name,
2934 'fields': 'nas.export_policy.name'
2935 }
2937 response = self.send_request('/storage/volumes/', 'get', query=query)
2939 if not self._has_records(response):
2940 msg = _('Could not find export policy for volume %s.')
2941 raise exception.NetAppException(msg % volume_name)
2943 volume = response['records'][0]
2944 return volume['nas']['export_policy']['name']
2946 @na_utils.trace
2947 def get_unique_export_policy_id(self, policy_name):
2948 """Get export policy uuid for a given policy name"""
2950 get_uuid = self.send_request(
2951 '/protocols/nfs/export-policies', 'get',
2952 query={'name': policy_name})
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)
2958 uuid = get_uuid['records'][0]['id']
2959 return uuid
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."""
2965 uuid = self.get_unique_export_policy_id(policy_name)
2967 query = {
2968 'clients.match': client_match,
2969 'fields': 'clients.match,index'
2970 }
2972 response = self.send_request(
2973 f'/protocols/nfs/export-policies/{uuid}/rules',
2974 'get', query=query)
2976 rules = response['records']
2977 indices = [rule['index'] for rule in rules]
2978 indices.sort()
2979 return [str(i) for i in indices]
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 }
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']
3001 self.send_request(f'/protocols/nfs/export-policies/{uuid}/rules',
3002 'post', body=body)
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 }
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']
3024 self.send_request(
3025 f'/protocols/nfs/export-policies/{uuid}/rules/{rule_index}',
3026 'patch', body=body)
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
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)
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
3066 return rules
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 }
3075 get_uuid = self.send_request('/protocols/cifs/shares', 'get',
3076 query=query)
3077 svm_uuid = get_uuid['records'][0]['svm']['uuid']
3079 body = {
3080 'permission': 'read' if readonly else 'full_control',
3081 'user_or_group': user_name,
3082 }
3084 self.send_request(
3085 f'/protocols/cifs/shares/{svm_uuid}/{share_name}/acls',
3086 'post', body=body)
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 }
3095 get_uuid = self.send_request('/protocols/cifs/shares', 'get',
3096 query=query)
3097 svm_uuid = get_uuid['records'][0]['svm']['uuid']
3099 body = {
3100 'permission': 'read' if readonly else 'full_control',
3101 }
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)
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
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)
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)
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')
3158 @na_utils.trace
3159 def get_volume_move_status(self, volume_name, vserver):
3160 """Gets the current state of a volume move operation."""
3162 fields = 'movement.percent_complete,movement.state'
3164 query = {
3165 'name': volume_name,
3166 'svm.name': vserver,
3167 'fields': fields
3168 }
3170 result = self.send_request('/storage/volumes/', 'get', query=query)
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)
3178 volume_move_info = result.get('records')[0]
3179 volume_movement = volume_move_info['movement']
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 }
3190 return status_info
3192 @na_utils.trace
3193 def list_snapmirror_snapshots(self, volume_name, newer_than=None):
3194 """Gets SnapMirror snapshots on a volume."""
3196 volume = self._get_volume_by_args(vol_name=volume_name)
3197 uuid = volume['uuid']
3199 query = {
3200 'owners': 'snapmirror_dependent',
3201 }
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
3206 response = self.send_request(
3207 f'/storage/volumes/{uuid}/snapshots/',
3208 'get', query=query)
3210 return [snapshot_info['name']
3211 for snapshot_info in response['records']]
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)
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."""
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']
3241 query = {'state': 'transferring'}
3242 transfers = self.send_request('/snapmirror/relationships/' +
3243 snapmirror_uuid + '/transfers/',
3244 'get', query=query)
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)
3250 body = {'state': 'hard_aborted' if clear_checkpoint else 'aborted'}
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)
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)
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'
3277 response = self.send_request('/snapmirror/relationships/', 'get',
3278 query=query_uuid)
3280 records = response.get('records')
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"}
3287 snapmirror_uuid = records[0].get('uuid')
3288 self.send_request('/snapmirror/relationships/' +
3289 snapmirror_uuid, 'delete',
3290 query=query_delete)
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."""
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)
3312 return snapmirrors
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'])
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)
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'
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)
3342 @na_utils.trace
3343 def disable_fpolicy_policy(self, policy_name):
3344 """Disables a specific policy.
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)
3371 @na_utils.trace
3372 def delete_fpolicy_scope(self, policy_name):
3373 """Delete fpolicy scope
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
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)
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."""
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.
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.
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 }
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
3424 if policy:
3425 body['policy.name'] = policy
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
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
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."""
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)
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}
3460 raise na_utils.NetAppDriverException(msg)
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}})
3469 result = self.send_request(f'/snapmirror/relationships/{uuid}',
3470 'patch', body=body,
3471 wait_on_accepted=wait_result)
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 }
3483 return job_info
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)
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."""
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.
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)
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)
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)
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."""
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 }
3554 self.send_request('/storage/volumes', 'post', body=body)
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)
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)
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)
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."""
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)
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'}
3597 self.send_request(f'/snapmirror/relationships/{uuid}', 'patch',
3598 body=body)
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)
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."""
3615 interval = 2
3616 retries = (10 / interval)
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)
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)
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)
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)
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)
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)
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)
3684 snapmirror_uuid = response[0]['uuid']
3685 snapmirror_policy = response[0]['policy-type']
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'
3693 self.send_request('/snapmirror/relationships/' +
3694 snapmirror_uuid, 'patch',
3695 body=body_resync, wait_on_accepted=False)
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)
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)
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']
3732 body = {
3733 'qos.policy.name': qos_policy_group_name
3734 }
3736 self.send_request(f'/storage/volumes/{uuid}', 'patch', body=body)
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)
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)
3759 if not snapmirrors:
3760 msg = _('Failed to get snapmirror relationship information')
3761 raise na_utils.NetAppDriverException(msg)
3763 snapmirror_uuid = snapmirrors[0]['uuid']
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'
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)
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
3785 @na_utils.trace
3786 def get_cluster_name(self):
3787 """Gets cluster name."""
3789 result = self.send_request('/cluster', 'get', enable_tunneling=False)
3790 return result.get('name')
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')
3798 return volume['clone']['is_flexclone'] is False
3800 @na_utils.trace
3801 def rehost_volume(self, volume_name, vserver, destination_vserver):
3802 """Rehosts a volume from one Vserver into another Vserver.
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)
3818 @na_utils.trace
3819 def get_net_options(self):
3820 """Retrives the IPv6 support."""
3822 return {
3823 'ipv6-enabled': True,
3824 }
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."""
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)
3836 def get_performance_counter_info(self, object_name, counter_name):
3837 """Gets info about one or more Data ONTAP performance counters."""
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 }
3847 rest_counter_name = counter_name
3848 if counter_name in rest_counter_names:
3849 rest_counter_name = rest_counter_names[counter_name]
3851 # Get counter table info
3852 query = {
3853 'counter_schemas.name': rest_counter_name,
3854 'fields': 'counter_schemas.*'
3855 }
3857 try:
3858 table = self.send_request(
3859 f'/cluster/counter/tables/{object_name}',
3860 'get', query=query)
3862 name = counter_name # use the original name (ZAPI compatible)
3863 base_counter = table['counter_schemas'][0]['denominator']['name']
3865 query = {
3866 'counters.name': rest_counter_name,
3867 'fields': 'counters.*'
3868 }
3870 response = self.send_request(
3871 f'/cluster/counter/tables/{object_name}/rows',
3872 'get', query=query, enable_tunneling=False)
3874 table_rows = response.get('records', [])
3875 labels = []
3876 if len(table_rows) != 0:
3877 labels = table_rows[0]['counters'][0].get('labels', [])
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]
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)
3893 def get_performance_instance_uuids(self, object_name, node_name):
3894 """Get UUIDs of performance instances for a cluster node."""
3896 query = {
3897 'id': node_name + ':*',
3898 }
3900 response = self.send_request(
3901 f'/cluster/counter/tables/{object_name}/rows',
3902 'get', query=query, enable_tunneling=False)
3904 records = response.get('records', [])
3906 uuids = []
3907 for record in records:
3908 uuids.append(record['id'])
3910 return uuids
3912 def get_performance_counters(self, object_name, instance_uuids,
3913 counter_names):
3914 """Gets more cDOT performance counters."""
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 }
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 }
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]]
3934 query = {
3935 'id': '|'.join(instance_uuids),
3936 'counters.name': '|'.join(counter_names),
3937 'fields': 'id,counter_table.name,counters.*',
3938 }
3940 response = self.send_request(
3941 f'/cluster/counter/tables/{object_name}/rows',
3942 'get', query=query)
3944 counter_data = []
3945 for record in response.get('records', []):
3946 for counter in record['counters']:
3948 counter_name = counter['name']
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]
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])
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 })
3971 return counter_data
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)
3982 return [svm['name'] for svm in response.get('records', [])]
3984 @na_utils.trace
3985 def _get_ems_log_destination_vserver(self):
3986 """Returns the best vserver destination for EMS messages."""
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()
3993 if vservers:
3994 return vservers[0]
3996 raise exception.NotFound("No Vserver found to receive EMS messages.")
3998 @na_utils.trace
3999 def send_ems_log_message(self, message_dict):
4000 """Sends a message to the Data ONTAP EMS log."""
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 }
4013 bkp_connection = copy.copy(self.connection)
4014 bkp_timeout = self.connection.get_timeout()
4015 bkp_vserver = self.vserver
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)
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 }
4044 response = self.send_request('/protocols/nfs/export-policies',
4045 'get', query=query)
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
4054 return policy_map
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)
4070 @na_utils.trace
4071 def get_nfs_config_default(self, desired_args=None):
4072 """Gets the default NFS config with the desired params"""
4074 query = {'fields': 'transport.*'}
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
4079 response = self.send_request('/protocols/nfs/services/',
4080 'get', query=query)
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']))
4094 return nfs_info
4096 @na_utils.trace
4097 def create_kerberos_realm(self, security_service):
4098 """Creates Kerberos realm on cluster."""
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)
4117 @na_utils.trace
4118 def configure_kerberos(self, security_service, vserver_name):
4119 """Configures Kerberos for NFS on Vserver."""
4121 self.configure_dns(security_service, vserver_name=vserver_name)
4122 spn = self._get_kerberos_service_principal_name(
4123 security_service, vserver_name)
4125 lifs = self.get_network_interfaces()
4127 if not lifs:
4128 msg = _("Cannot set up Kerberos. There are no LIFs configured.")
4129 raise exception.NetAppException(msg)
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 }
4140 interface_uuid = lif['uuid']
4142 self.send_request(
4143 f'/protocols/nfs/kerberos/interfaces/{interface_uuid}',
4144 'patch', body=body)
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())
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
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)
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)
4178 cifs_server = self._get_cifs_server_name(vserver_name)
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 }
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']
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)
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
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)
4233 dns_config = {}
4234 dns_info = result.get('dynamic_dns', {})
4236 dns_config['dns-state'] = dns_info.get('enabled', '')
4237 dns_config['domains'] = result.get('domains', [])
4238 dns_config['dns-ips'] = result.get('servers', [])
4240 return dns_config
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', []))
4256 svm_uuid = self._get_unique_svm_by_name(vserver_name)
4258 domains.add(security_service['domain'])
4259 for domain in domains:
4260 body['domains'].append(domain)
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)
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)
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 }
4289 svm_uuid = self._get_unique_svm_by_name(vserver_name)
4291 self.send_request(f'/svm/svms/{svm_uuid}', 'patch', body=body)
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)
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)
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)
4310 else:
4311 msg = _('Unsupported security service type %s for '
4312 'Data ONTAP driver')
4313 raise exception.NetAppException(msg % security_service['type'])
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'
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)
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)
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 }
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())
4363 self.send_request('/name-services/ldap', 'post',
4364 body=body)
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)
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)
4395 body = {
4396 'port': '389',
4397 'schema': ldap_schema,
4398 'bind_dn': bind_dn,
4399 'bind_password': new_security_service.get('password')
4400 }
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())
4412 self.send_request(f'/name-services/ldap/{svm_uuid}', 'patch',
4413 body=body)
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)
4421 body = {
4422 'kdc-ip': security_service['server'],
4423 }
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)
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)
4444 for dns_ip in dns_ips:
4445 body['servers'].append(dns_ip)
4447 empty_dns_config = (not body['domains'] and
4448 not body['servers'])
4450 svm_uuid = self._get_unique_svm_by_name(self.vserver)
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
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)
4466 @na_utils.trace
4467 def remove_preferred_dcs(self, security_service, svm_uuid):
4468 """Drops all preferred DCs at once."""
4470 query = {
4471 'fqdn': security_service['domain'],
4472 }
4474 records = self.send_request(f'/protocols/cifs/domains/{svm_uuid}/'
4475 f'preferred-domain-controllers/', 'get')
4477 fqdn = records.get('fqdn')
4478 server_ip = records.get('server_ip')
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)
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."""
4495 svm_uuid = self._get_unique_svm_by_name(vserver_name)
4496 new_username = new_security_service['user']
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)
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)
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)
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)
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 }
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)
4550 @na_utils.trace
4551 def set_preferred_dc(self, security_service, vserver_name):
4552 """Set preferred domain controller."""
4554 # server is optional
4555 if not security_service['server']:
4556 return
4558 query = {
4559 'server_ip': [],
4560 'fqdn': security_service['domain'],
4561 'skip_config_validation': 'false',
4562 }
4564 for dc_ip in security_service['server'].split(','):
4565 query['server_ip'].append(dc_ip.strip())
4567 svm_uuid = self._get_unique_svm_by_name(vserver_name)
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)
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
4590 self.send_request('/svm/peers', 'post', body=body,
4591 enable_tunneling=False)
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
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)
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."""
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
4626 query['fields'] = 'uuid,svm.name,peer.svm.name,state,peer.cluster.name'
4628 result = self.send_request('/svm/peers', 'get', query=query)
4629 if not self._has_records(result):
4630 return []
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)
4643 return vserver_peers
4645 @na_utils.trace
4646 def delete_vserver_peer(self, vserver_name, peer_vserver_name):
4647 """Deletes a Vserver peer relationship."""
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)
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."""
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)
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')
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 }
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
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
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
4697 body['aggregates'] = []
4698 for aggr_name in aggregate_names:
4699 body['aggregates'].append({'name': aggr_name})
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')
4706 self.send_request('/svm/svms', 'post', body=body)
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})
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."""
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 }
4736 self.send_request('/security/barbican-kms', 'post', body=body)
4738 @na_utils.trace
4739 def get_key_store_config_uuid(self, config_name):
4740 """Retrieves keystore configuration uuid for the specified config name.
4742 """
4744 query = {
4745 'configuration.name': config_name
4746 }
4748 response = self.send_request('/security/key-stores',
4749 'get', query=query)
4751 if not response.get('records'):
4752 return None
4754 return response.get('records')[0]['configuration']['uuid']
4756 @na_utils.trace
4757 def enable_key_store_config(self, config_uuid):
4758 """Enables a keystore configuration"""
4760 body = {
4761 "enabled": True
4762 }
4764 # Update key-store
4765 self.send_request(f'/security/key-stores/{config_uuid}', 'patch',
4766 body=body)
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."""
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
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
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
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
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})
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)
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]
4845 @na_utils.trace
4846 def _sort_data_ports_by_speed(self, ports):
4847 """Sort ports by speed."""
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
4862 return sorted(ports, key=sort_key, reverse=True)
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 }
4875 result = self.send_request('/network/ethernet/ports', 'get',
4876 query=query)
4878 net_port_info_list = result.get('records', [])
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
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)
4894 data_ports = set(
4895 [record['location']['port']['name']
4896 for record in response.get('records', [])]
4897 )
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)
4908 ports = self._sort_data_ports_by_speed(ports)
4910 return ports
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"""
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)
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']
4933 return ipspace_name
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)
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}
4949 self._ensure_broadcast_domain_for_port(
4950 node, home_port_name, mtu, ipspace=ipspace)
4952 return home_port_name
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)
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.
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 """
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)
4992 port_info = self._get_broadcast_domain_for_port(node, port)
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
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)
5006 # Move the port into the broadcast domain where it is needed.
5007 self._add_port_to_broadcast_domain(node, port, domain, ipspace)
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)
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)
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
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)
5047 @na_utils.trace
5048 def _modify_broadcast_domain(self, domain, ipspace, mtu):
5049 """Modify a broadcast domain."""
5050 query = {
5051 'name': domain
5052 }
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)
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)
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)
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)
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']
5126 body = {
5127 'showmount_enabled': showmount,
5128 }
5129 self.send_request(f'/protocols/nfs/services/{svm_id}', 'patch',
5130 body=body)
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']
5146 body = {
5147 'protocol.v41_features.pnfs_enabled': pnfs,
5148 }
5149 self.send_request(f'/protocols/nfs/services/{svm_id}', 'patch',
5150 body=body)
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()
5157 body = {
5158 'svm.uuid': svm_id,
5159 'enabled': 'true',
5160 }
5161 self.send_request('/protocols/nfs/services/', 'post',
5162 body=body)
5164 self._enable_nfs_protocols(versions, svm_id)
5166 if nfs_config:
5167 self._configure_nfs(nfs_config, svm_id)
5169 self._create_default_nfs_export_rules()
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'
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)
5190 @na_utils.trace
5191 def _create_default_nfs_export_rules(self):
5192 """Create the default export rule for the NFS service."""
5194 body = {
5195 'clients': [{'match': '0.0.0.0/0'}],
5196 'ro_rule': [
5197 'any',
5198 ],
5199 'rw_rule': [
5200 'never'
5201 ],
5202 }
5204 uuid = self.get_unique_export_policy_id('default')
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)
5212 @na_utils.trace
5213 def _configure_nfs(self, nfs_config, svm_id):
5214 """Sets the nfs configuraton"""
5216 if ('udp-max-xfer-size' in nfs_config and
5217 (nfs_config['udp-max-xfer-size']
5218 != str(DEFAULT_UDP_MAX_XFER_SIZE))):
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)
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)
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})
5245 query = {
5246 'name': 'default-data-files',
5247 'svm.name': vserver_name,
5248 'fields': 'uuid,name,services,svm.name'
5249 }
5251 result = self.send_request('/network/ip/service-policies/', 'get',
5252 query=query)
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]
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')
5264 uuid = policy['uuid']
5265 body = {'services': policy['services']}
5266 self.send_request(
5267 f'/network/ip/service-policies/{uuid}', 'patch',
5268 body=body)
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)
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}'
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)
5301 @na_utils.trace
5302 def create_route(self, gateway, destination=None):
5303 """Create a network route."""
5304 if not gateway:
5305 return
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'
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
5320 body = {
5321 'destination.address': address,
5322 'gateway': gateway,
5323 }
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
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)
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 }
5352 svm_uuid = self._get_unique_svm_by_name(vserver_name)
5353 self.send_request(f'/svm/svms/{svm_uuid}', 'patch', body=body)
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)
5360 query = {
5361 'name': vserver_name,
5362 'fields': 'state,subtype'
5363 }
5365 response = self.send_request('/svm/svms', 'get', query=query)
5366 if not response.get('records'):
5367 return
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
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"""
5382 query = {'fields': 'transport.*'}
5383 query['svm.name'] = vserver
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 }
5390 response = self.send_request('/protocols/nfs/services/',
5391 'get', query=query)
5392 records = response.get('records', [])
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']))
5398 return nfs_info
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 }
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)
5414 if self._has_records(response):
5415 return response['records'][0].get('ipspace', {}).get('name')
5417 return None
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')
5430 policy_name = []
5431 for record in records:
5432 policy_name.append(record.get('name'))
5433 return policy_name
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"""
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
5460 @na_utils.trace
5461 def delete_snapmirror_policy(self, policy_name):
5462 """Deletes a SnapMirror policy."""
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
5478 @na_utils.trace
5479 def delete_vserver(self, vserver_name, vserver_client,
5480 security_services=None):
5481 """Deletes a Vserver.
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)
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()
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)
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)
5514 if security_services and not is_dp_destination:
5515 self._terminate_vserver_services(vserver_name, vserver_client,
5516 security_services)
5518 self.send_request(f'/svm/svms/{svm_uuid}', 'delete')
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']
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)
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 }
5540 body_force = {
5541 'ad_domain.password': service['password'],
5542 'ad_domain.user': service['user'],
5543 'force': True
5544 }
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)
5561 @na_utils.trace
5562 def disable_kerberos(self, security_service):
5563 """Disable Kerberos in all Vserver LIFs."""
5565 lifs = self.get_network_interfaces()
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 }
5577 interface_uuid = lif['uuid']
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)
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']
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)
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 }
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)
5640 @na_utils.trace
5641 def get_degraded_ports(self, broadcast_domains, ipspace_name):
5642 """Get degraded ports for broadcast domains and an ipspace."""
5644 valid_domains = self._get_valid_broadcast_domains(broadcast_domains)
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 }
5654 result = self.send_request('/network/ethernet/ports', 'get',
5655 query=query)
5657 net_port_info_list = result.get('records', [])
5659 ports = []
5660 for port_info in net_port_info_list:
5661 ports.append(f"{port_info['node']['name']}:"
5662 f"{port_info['name']}")
5664 return ports
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
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.
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 }
5707 if dest_ipspace:
5708 ipspace_data = {
5709 "ipspace": {
5710 "name": dest_ipspace,
5711 }
5712 }
5713 body["destination"].update(ipspace_data)
5715 return self.send_request('/svm/migrations', 'post', body=body,
5716 wait_on_accepted=False)
5718 @na_utils.trace
5719 def get_migration_check_job_state(self, job_id):
5720 """Get the job state of a share server migration.
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)
5742 @na_utils.trace
5743 def svm_migrate_complete(self, migration_id):
5744 """Send a request to complete the SVM migration.
5746 :param migration_id: the id of the migration provided by the storage.
5747 """
5748 body = {
5749 "action": "cutover"
5750 }
5752 return self.send_request(
5753 f'/svm/migrations/{migration_id}', 'patch', body=body,
5754 wait_on_accepted=False)
5756 @na_utils.trace
5757 def svm_migrate_cancel(self, migration_id):
5758 """Send a request to cancel the SVM migration.
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)
5765 @na_utils.trace
5766 def svm_migration_get(self, migration_id):
5767 """Send a request to get the progress of the SVM migration.
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')
5773 @na_utils.trace
5774 def svm_migrate_pause(self, migration_id):
5775 """Send a request to pause a migration.
5777 :param migration_id: the id of the migration provided by the storage.
5778 """
5779 body = {
5780 "action": "pause"
5781 }
5783 return self.send_request(
5784 f'/svm/migrations/{migration_id}', 'patch', body=body,
5785 wait_on_accepted=False)
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)
5792 query = {
5793 'svm.name': vserver_name,
5794 'name': interface_name
5795 }
5796 self.send_request('/network/ip/interfaces', 'delete', query=query)
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)
5811 @na_utils.trace
5812 def get_ipspaces(self, ipspace_name=None, vserver_name=None):
5813 """Gets one or more IPSpaces."""
5815 query = {
5816 'name': ipspace_name
5817 }
5818 result = self.send_request('/network/ipspaces', 'get',
5819 query=query)
5821 if not self._has_records(result):
5822 return []
5824 ipspace_info = result.get('records')[0]
5826 query = {
5827 'broadcast_domain.ipspace.name': ipspace_name
5828 }
5829 ports = self.send_request('/network/ethernet/ports',
5830 'get', query=query)
5832 query = {
5833 'ipspace.name': ipspace_name
5834 }
5835 vservers = self.send_request('/svm/svms',
5836 'get', query=query)
5838 br_domains = self.send_request('/network/ethernet/broadcast-domains',
5839 'get', query=query)
5841 ipspace = {
5842 'ports': [],
5843 'vservers': [],
5844 'broadcast-domains': [],
5845 }
5847 for port in ports.get('records'):
5848 ipspace['ports'].append(port.get('name'))
5850 for vserver in vservers.get('records'):
5851 ipspace['vservers'].append(vserver.get('name'))
5853 for broadcast in br_domains.get('records'):
5854 ipspace['broadcast-domains'].append(broadcast.get('name'))
5856 ipspace['ipspace'] = ipspace_info.get('name')
5858 ipspace['uuid'] = ipspace_info.get('uuid')
5860 return ipspace
5862 @na_utils.trace
5863 def _delete_port_and_broadcast_domain(self, domain, ipspace):
5864 """Delete a broadcast domain and its ports."""
5866 ipspace_name = ipspace['ipspace']
5867 ports = ipspace['ports']
5869 for port in ports:
5870 self._delete_port_by_ipspace_and_broadcast_domain(
5871 port,
5872 domain,
5873 ipspace_name)
5875 query = {
5876 'name': domain,
5877 'ipspace.name': ipspace_name
5878 }
5880 self.send_request('/network/ethernet/broadcast-domains', 'delete',
5881 query=query)
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
5890 for broadcast_domain_name in ipspace['broadcast-domains']:
5891 self._delete_port_and_broadcast_domain(broadcast_domain_name,
5892 ipspace)
5894 @na_utils.trace
5895 def delete_ipspace(self, ipspace_name):
5896 """Deletes an IPspace
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
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
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
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
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
5934 return True
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"""
5940 query = {
5941 'svm.name': svm_name,
5942 'fields': 'size'
5943 }
5945 response = self.send_request('/storage/volumes/', 'get', query=query)
5947 svm_volumes = response.get('records', [])
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
5959 # Convert Bytes to GBs.
5960 return (total_volumes_size / 1024**3)
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
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 }
6000 if newer_than:
6001 query['create_time'] = '>' + newer_than
6003 response = self.send_request(
6004 f'/storage/volumes/{uuid}/snapshots/',
6005 'get', query=query)
6007 return [snapshot_info['name']
6008 for snapshot_info in response['records']]
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
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')
6046 body[na_api_attr] = utils.convert_time_duration_to_iso_format(
6047 options.get(share_type_attr))
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
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)
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")
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')
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']
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