# Copyright (c) 2022 NetApp, Inc. All rights reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import copy
from datetime import datetime
from datetime import timedelta
import math
from time import time

from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import units

from cinder import exception
from cinder.i18n import _
from cinder import utils
from cinder.volume.drivers.netapp.dataontap.client import api as netapp_api
from cinder.volume.drivers.netapp.dataontap.client import client_cmode
from cinder.volume.drivers.netapp import utils as na_utils
from cinder.volume import volume_utils

LOG = logging.getLogger(__name__)
DEFAULT_MAX_PAGE_LENGTH = 10000
ONTAP_SELECT_MODEL = 'FDvM300'
ONTAP_C190 = 'C190'
HTTP_ACCEPTED = 202
DELETED_PREFIX = 'deleted_cinder_'
DEFAULT_TIMEOUT = 15
REST_SYNC_TIMEOUT = 15

# Keys in this map are REST API's endpoints that the user shall have permission
# in order to enable extra specs reported to Cinder's scheduler.
# NOTE(sfernand): ONTAP does not retrieve volume efficiency information
# properly when using the pre-created "vsadmin" role (SVM scoped), causing
# dedup and compression extra specs to be reported as disabled despite its
# current configuration.
SSC_API_MAP = {
    '/storage/aggregates': [
        'netapp_raid_type',
    ],
    '/storage/disks': [
        'netapp_disk_type',
    ],
    '/snapmirror/relationships': [
        'netapp_mirrored',
    ],
    '/storage/volumes': [
        'netapp_flexvol_encryption'
        'netapp_dedup',
        'netapp_compression',
    ],
}


class RestClient(object, metaclass=volume_utils.TraceWrapperMetaclass):

    def __init__(self, **kwargs):

        host = kwargs['hostname']
        username = kwargs['username']
        password = kwargs['password']
        api_trace_pattern = kwargs['api_trace_pattern']
        private_key_file = kwargs['private_key_file']
        certificate_file = kwargs['certificate_file']
        ca_certificate_file = kwargs['ca_certificate_file']
        certificate_host_validation = kwargs['certificate_host_validation']
        is_disaggregated = kwargs.get('is_disaggregated', False)
        ssl_cert_path = kwargs['ssl_cert_path']

        if private_key_file and certificate_file and ca_certificate_file:
            self.connection = netapp_api.RestNaServer(
                host=host,
                transport_type='https',
                ssl_cert_path=ssl_cert_path,
                port=kwargs['port'],
                private_key_file=private_key_file,
                certificate_file=certificate_file,
                ca_certificate_file=ca_certificate_file,
                certificate_host_validation=certificate_host_validation,
                api_trace_pattern=api_trace_pattern)
        elif private_key_file and certificate_file:
            self.connection = netapp_api.RestNaServer(
                host=host,
                transport_type='https',
                ssl_cert_path=ssl_cert_path,
                port=kwargs['port'],
                private_key_file=private_key_file,
                certificate_file=certificate_file,
                certificate_host_validation=certificate_host_validation,
                api_trace_pattern=api_trace_pattern)
        else:
            self.connection = netapp_api.RestNaServer(
                host=host,
                transport_type=kwargs['transport_type'],
                ssl_cert_path=ssl_cert_path,
                port=kwargs['port'],
                username=username,
                password=password,
                api_trace_pattern=api_trace_pattern)

        self.async_rest_timeout = kwargs.get('async_rest_timeout', 60)

        self.vserver = kwargs.get('vserver')
        self.connection.set_vserver(self.vserver)

        ontap_version = self.get_ontap_version(cached=False)
        if ontap_version < (9, 11, 1):
            msg = _('REST Client can be used only with ONTAP 9.11.1 or upper.')
            raise na_utils.NetAppDriverException(msg)
        self.connection.set_ontap_version(ontap_version)

        self.ssh_client = self._init_ssh_client(host, username, password)

        # NOTE(nahimsouza): ZAPI Client is needed to implement the fallback
        # when a REST method is not supported.
        if not is_disaggregated:
            self.zapi_client = client_cmode.Client(**kwargs)

        self._init_features()

    def _init_ssh_client(self, host, username, password):
        return netapp_api.SSHUtil(
            host=host,
            username=username,
            password=password)

    def _init_features(self):
        self.features = na_utils.Features()

        generation, major, minor = self.get_ontap_version()
        ontap_version = (generation, major)

        ontap_9_0 = ontap_version >= (9, 0)
        ontap_9_4 = ontap_version >= (9, 4)
        ontap_9_5 = ontap_version >= (9, 5)
        ontap_9_6 = ontap_version >= (9, 6)
        ontap_9_8 = ontap_version >= (9, 8)
        ontap_9_14 = ontap_version >= (9, 14)

        nodes_info = self._get_cluster_nodes_info()
        for node in nodes_info:
            qos_min_block = False
            qos_min_nfs = False
            if node['model'] == ONTAP_SELECT_MODEL:
                qos_min_block = node['is_all_flash_select'] and ontap_9_6
                qos_min_nfs = qos_min_block
            elif ONTAP_C190 in node['model']:
                qos_min_block = node['is_all_flash'] and ontap_9_6
                qos_min_nfs = qos_min_block
            else:
                qos_min_block = node['is_all_flash'] and ontap_9_0
                qos_min_nfs = node['is_all_flash'] and ontap_9_0

            qos_name = na_utils.qos_min_feature_name(True, node['name'])
            self.features.add_feature(qos_name, supported=qos_min_nfs)
            qos_name = na_utils.qos_min_feature_name(False, node['name'])
            self.features.add_feature(qos_name, supported=qos_min_block)

        self.features.add_feature('SNAPMIRROR_V2', supported=ontap_9_0)
        self.features.add_feature('USER_CAPABILITY_LIST',
                                  supported=ontap_9_0)
        self.features.add_feature('SYSTEM_METRICS', supported=ontap_9_0)
        self.features.add_feature('CLONE_SPLIT_STATUS', supported=ontap_9_0)
        self.features.add_feature('FAST_CLONE_DELETE', supported=ontap_9_0)
        self.features.add_feature('SYSTEM_CONSTITUENT_METRICS',
                                  supported=ontap_9_0)
        self.features.add_feature('ADVANCED_DISK_PARTITIONING',
                                  supported=ontap_9_0)
        self.features.add_feature('BACKUP_CLONE_PARAM', supported=ontap_9_0)
        self.features.add_feature('CLUSTER_PEER_POLICY', supported=ontap_9_0)
        self.features.add_feature('FLEXVOL_ENCRYPTION', supported=ontap_9_0)
        self.features.add_feature('FLEXGROUP', supported=ontap_9_8)
        # Flex group file clone is supported for ONTAP 9.14 and above versions
        # so updating this from 9.9 to 9.14.
        self.features.add_feature('FLEXGROUP_CLONE_FILE',
                                  supported=ontap_9_14)

        self.features.add_feature('ADAPTIVE_QOS', supported=ontap_9_4)
        self.features.add_feature('ADAPTIVE_QOS_BLOCK_SIZE',
                                  supported=ontap_9_5)
        self.features.add_feature('ADAPTIVE_QOS_EXPECTED_IOPS_ALLOCATION',
                                  supported=ontap_9_5)

        LOG.info('ONTAP Version: %(generation)s.%(major)s.%(minor)s',
                 {'generation': ontap_version[0], 'major': ontap_version[1],
                  'minor': minor})

    def __getattr__(self, name):
        """If method is not implemented for REST, try to call the ZAPI."""
        LOG.debug("The %s call is not supported for REST, falling back to "
                  "ZAPI.", name)
        # Don't use self.zapi_client to avoid reentrant call to __getattr__()
        zapi_client = object.__getattribute__(self, 'zapi_client')
        return getattr(zapi_client, name)

    def _wait_job_result(self, job_url):
        """Waits for a job to finish."""

        interval = 2
        retries = (self.async_rest_timeout / interval)

        @utils.retry(netapp_api.NaRetryableError, interval=interval,
                     retries=retries, backoff_rate=1)
        def _waiter():
            response = self.send_request(job_url, 'get',
                                         enable_tunneling=False)

            job_state = response.get('state')
            if job_state == 'success':
                return response
            elif job_state == 'failure':
                message = response['error']['message']
                code = response['error']['code']
                raise netapp_api.NaApiError(message=message, code=code)

            msg_args = {'job': job_url, 'state': job_state}
            LOG.debug("Job %(job)s has not finished: %(state)s", msg_args)
            raise netapp_api.NaRetryableError(message='Job is running.')

        try:
            return _waiter()
        except netapp_api.NaRetryableError:
            msg = _("Job %s did not reach the expected state. Retries "
                    "exhausted. Aborting.") % job_url
            raise na_utils.NetAppDriverException(msg)

    def send_request(self, action_url, method, body=None, query=None,
                     enable_tunneling=True,
                     max_page_length=DEFAULT_MAX_PAGE_LENGTH,
                     wait_on_accepted=True):

        """Sends REST request to ONTAP.

        :param action_url: action URL for the request
        :param method: HTTP method for the request ('get', 'post', 'put',
            'delete' or 'patch')
        :param body: dict of arguments to be passed as request body
        :param query: dict of arguments to be passed as query string
        :param enable_tunneling: enable tunneling to the ONTAP host
        :param max_page_length: size of the page during pagination
        :param wait_on_accepted: if True, wait until the job finishes when
            HTTP code 202 (Accepted) is returned

        :returns: parsed REST response
        """

        response = None

        if method == 'get':
            response = self.get_records(
                action_url, query, enable_tunneling, max_page_length)
        else:
            code, response = self.connection.invoke_successfully(
                action_url, method, body=body, query=query,
                enable_tunneling=enable_tunneling)

            if code == HTTP_ACCEPTED and wait_on_accepted:
                # get job URL and discard '/api'
                job_url = response['job']['_links']['self']['href'][4:]
                response = self._wait_job_result(job_url)

        return response

    def get_records(self, action_url, query=None, enable_tunneling=True,
                    max_page_length=DEFAULT_MAX_PAGE_LENGTH):
        """Retrieves ONTAP resources using pagination REST request.

        :param action_url: action URL for the request
        :param query: dict of arguments to be passed as query string
        :param enable_tunneling: enable tunneling to the ONTAP host
        :param max_page_length: size of the page during pagination

        :returns: dict containing records and num_records
        """

        # Initialize query variable if it is None
        query = query if query else {}
        query['max_records'] = max_page_length

        _, response = self.connection.invoke_successfully(
            action_url, 'get', query=query,
            enable_tunneling=enable_tunneling)

        # NOTE(nahimsouza): if all records are returned in the first call,
        # 'next_url' will be None.
        next_url = response.get('_links', {}).get('next', {}).get('href')
        next_url = next_url[4:] if next_url else None  # discard '/api'

        # Get remaining pages, saving data into first page
        while next_url:
            # NOTE(nahimsouza): clean the 'query', because the parameters are
            # already included in 'next_url'.
            _, next_response = self.connection.invoke_successfully(
                next_url, 'get', query=None,
                enable_tunneling=enable_tunneling)

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

            next_url = (
                next_response.get('_links', {}).get('next', {}).get('href'))
            next_url = next_url[4:] if next_url else None  # discard '/api'

        return response

    def get_ontap_version(self, cached=True):
        """Gets the ONTAP version as tuple."""

        if cached:
            return self.connection.get_ontap_version()

        query = {
            'fields': 'version'
        }

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

        version = (response['version']['generation'],
                   response['version']['major'],
                   response['version']['minor'])

        return version

    def check_api_permissions(self):
        """Check which APIs that support SSC functionality are available."""

        inaccessible_apis = []
        invalid_extra_specs = []

        for api, extra_specs in SSC_API_MAP.items():
            if not self.check_cluster_api(api):
                inaccessible_apis.append(api)
                invalid_extra_specs.extend(extra_specs)

        if inaccessible_apis:
            if '/storage/volumes' in inaccessible_apis:
                msg = _('User not permitted to query Data ONTAP volumes.')
                raise exception.VolumeBackendAPIException(data=msg)
            else:
                LOG.warning('The configured user account does not have '
                            'sufficient privileges to use all needed '
                            'APIs. The following extra specs will fail '
                            'or be ignored: %s.', invalid_extra_specs)

        return invalid_extra_specs

    def check_cluster_api(self, api):
        """Checks the availability of a cluster API.

        Returns True if the specified cluster API exists and may be called by
        the current user.
        """
        try:
            # No need to return any records here since we just want to know if
            # the user is allowed to make the request. A "Permission Denied"
            # error code is expected in case user does not have the necessary
            # permissions.
            self.send_request('%s?return_records=false' % api, 'get',
                              enable_tunneling=False)
        except netapp_api.NaApiError as ex:
            # NOTE(nahimsouza): This function only returns false in case user
            # is not authorized. If other error is returned, it must be
            # handled in the function call that uses the same endpoint.
            if ex.code == netapp_api.REST_UNAUTHORIZED:
                return False

        return True

    def _get_cluster_nodes_info(self):
        """Return a list of models of the nodes in the cluster."""
        query_args = {'fields': 'model,'
                                'name,'
                                'is_all_flash_optimized,'
                                'is_all_flash_select_optimized'}

        nodes = []
        try:
            result = self.send_request('/cluster/nodes', 'get',
                                       query=query_args,
                                       enable_tunneling=False)

            for record in result['records']:
                node_model = record.get('model')
                node_name = record.get('name')

                if node_model is None:
                    LOG.warning(
                        'No model information available for node %s', node_name
                    )
                    # Default model to empty string
                    node_model = ""

                node = {
                    'model': node_model,
                    'name': node_name,
                    'is_all_flash':
                        record['is_all_flash_optimized'],
                    'is_all_flash_select':
                        record['is_all_flash_select_optimized']
                }
                nodes.append(node)
        except netapp_api.NaApiError as e:
            if e.code == netapp_api.REST_UNAUTHORIZED:
                LOG.debug('Cluster nodes can only be collected with '
                          'cluster scoped credentials.')
            else:
                LOG.exception('Failed to get the cluster nodes.')

        return nodes

    def list_flexvols(self):
        """Returns the names of the flexvols on the controller."""

        query = {
            'type': 'rw',
            'style': 'flex*',  # Match both 'flexvol' and 'flexgroup'
            'is_svm_root': 'false',
            'error_state.is_inconsistent': 'false',
            'state': 'online',
            'fields': 'name'
        }

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

        records = response.get('records', [])
        volumes = [volume['name'] for volume in records]

        return volumes

    def _get_unique_volume(self, records):
        """Get the unique FlexVol or FlexGroup volume from a volume list."""
        if len(records) != 1:
            msg = _('Could not find unique volume. Volumes found: %(vol)s.')
            msg_args = {'vol': records}
            raise exception.VolumeBackendAPIException(data=msg % msg_args)

        return records[0]

    def _get_volume_by_args(self, vol_name=None, vol_path=None,
                            vserver=None, fields=None):
        """Get info from a single volume according to the args."""

        query = {
            'type': 'rw',
            'style': 'flex*',  # Match both 'flexvol' and 'flexgroup'
            'is_svm_root': 'false',
            'error_state.is_inconsistent': 'false',
            'state': 'online',
            'fields': 'name,style'
        }

        if vol_name:
            query['name'] = vol_name
        if vol_path:
            query['nas.path'] = vol_path
        if vserver:
            query['svm.name'] = vserver
        if fields:
            query['fields'] = fields

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

        records = volumes_response.get('records', [])
        volume = self._get_unique_volume(records)
        return volume

    def get_flexvol(self, flexvol_path=None, flexvol_name=None):
        """Get flexvol attributes needed for the storage service catalog."""

        fields = ('aggregates.name,name,svm.name,nas.path,'
                  'type,guarantee.honored,guarantee.type,'
                  'space.snapshot.reserve_percent,space.size,'
                  'qos.policy.name,snapshot_policy,language,style')
        unique_volume = self._get_volume_by_args(
            vol_name=flexvol_name, vol_path=flexvol_path, fields=fields)

        aggregate = None
        if unique_volume['style'] == 'flexvol':
            # flexvol has only 1 aggregate
            aggregate = [unique_volume['aggregates'][0]['name']]
        else:
            aggregate = [aggr["name"]
                         for aggr in unique_volume.get('aggregates', [])]

        qos_policy_group = (
            unique_volume.get('qos', {}).get('policy', {}).get('name'))

        volume = {
            'name': unique_volume['name'],
            'vserver': unique_volume['svm']['name'],
            'junction-path': unique_volume.get('nas', {}).get('path'),
            'aggregate': aggregate,
            'type': unique_volume['type'],
            'space-guarantee-enabled': unique_volume['guarantee']['honored'],
            'space-guarantee': unique_volume['guarantee']['type'],
            'percentage-snapshot-reserve':
                str(unique_volume['space']['snapshot']['reserve_percent']),
            'size': str(unique_volume['space']['size']),
            'qos-policy-group': qos_policy_group,
            'snapshot-policy': unique_volume['snapshot_policy']['name'],
            'language': unique_volume['language'],
            'style-extended': unique_volume['style'],
        }

        return volume

    def is_flexvol_mirrored(self, flexvol_name, vserver_name):
        """Check if flexvol is a SnapMirror source."""

        query = {
            'source.path': vserver_name + ':' + flexvol_name,
            'state': 'snapmirrored',
            'return_records': 'false',
        }

        try:
            response = self.send_request('/snapmirror/relationships/',
                                         'get', query=query)
            return response['num_records'] > 0
        except netapp_api.NaApiError:
            LOG.exception('Failed to get SnapMirror info for volume %s.',
                          flexvol_name)

        return False

    def is_flexvol_encrypted(self, flexvol_name, vserver_name):
        """Check if a flexvol is encrypted."""

        if not self.features.FLEXVOL_ENCRYPTION:
            return False

        query = {
            'encryption.enabled': 'true',
            'name': flexvol_name,
            'svm.name': vserver_name,
            'return_records': 'false',
        }

        try:
            response = self.send_request(
                '/storage/volumes/', 'get', query=query)
            return response['num_records'] > 0
        except netapp_api.NaApiError:
            LOG.exception('Failed to get Encryption info for volume %s.',
                          flexvol_name)

        return False

    def get_aggregate_disk_types(self, aggregate_name):
        """Get the disk type(s) of an aggregate."""
        disk_types = self._get_aggregate_disk_types(aggregate_name)
        return list(disk_types) if disk_types else None

    def _get_aggregate_disk_types(self, aggregate_name):
        """Get the disk type(s) of an aggregate"""

        disk_types = set()

        query = {
            'aggregates.name': aggregate_name,
            'fields': 'effective_type'
        }

        try:
            response = self.send_request(
                '/storage/disks', 'get', query=query, enable_tunneling=False)
        except netapp_api.NaApiError:
            LOG.exception('Failed to get disk info for aggregate %s.',
                          aggregate_name)
            return disk_types

        for storage_disk_info in response['records']:
            disk_types.add(storage_disk_info['effective_type'])

        return disk_types

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

        query = {}
        if aggregate_names:
            query['name'] = ','.join(aggregate_names)

        if fields:
            query['fields'] = fields

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

        return response['records']

    def get_aggregate(self, aggregate_name):
        """Get aggregate attributes needed for the storage service catalog."""

        if not aggregate_name:
            return {}

        fields = ('name,block_storage.primary.raid_type,'
                  'block_storage.storage_type,home_node.name')

        try:
            aggrs = self._get_aggregates(aggregate_names=[aggregate_name],
                                         fields=fields)
        except netapp_api.NaApiError:
            LOG.exception('Failed to get info for aggregate %s.',
                          aggregate_name)
            return {}

        if len(aggrs) < 1:
            return {}

        aggr_attributes = aggrs[0]

        aggregate = {
            'name': aggr_attributes['name'],
            'raid-type':
                aggr_attributes['block_storage']['primary']['raid_type'],
            'is-hybrid':
                aggr_attributes['block_storage']['storage_type'] == 'hybrid',
            'node-name': aggr_attributes['home_node']['name'],
        }

        return aggregate

    def is_qos_min_supported(self, is_nfs, node_name):
        """Check if the node supports QoS minimum."""
        if node_name is None:
            # whether no access to node name (SVM account or error), the QoS
            # min support is dropped.
            return False

        qos_min_name = na_utils.qos_min_feature_name(is_nfs, node_name)
        return getattr(self.features, qos_min_name, False).__bool__()

    def get_flexvol_dedupe_info(self, flexvol_name):
        """Get dedupe attributes needed for the storage service catalog."""

        query = {
            'efficiency.volume_path': '/vol/%s' % flexvol_name,
            'fields': 'efficiency.state,efficiency.compression'
        }

        # Set default values for the case there is no response.
        no_dedupe_response = {
            'compression': False,
            'dedupe': False,
            'logical-data-size': 0,
            'logical-data-limit': 1,
        }

        try:
            response = self.send_request('/storage/volumes',
                                         'get', query=query)
        except netapp_api.NaApiError:
            LOG.exception('Failed to get dedupe info for volume %s.',
                          flexvol_name)
            return no_dedupe_response

        if response["num_records"] != 1:
            return no_dedupe_response

        state = response["records"][0]["efficiency"]["state"]
        compression = response["records"][0]["efficiency"]["compression"]

        # TODO(nahimsouza): as soon as REST API supports the fields
        # 'logical-data-size and 'logical-data-limit', we should include
        # them in the query and set them correctly.
        # NOTE(nahimsouza): these fields are only used by the client function
        # `get_flexvol_dedupe_used_percent`, since the function is not
        # implemented on REST yet, the below hard-coded fields are not
        # affecting the driver in anyway.
        logical_data_size = 0
        logical_data_limit = 1

        dedupe_info = {
            'compression': False if compression == "none" else True,
            'dedupe': False if state == "disabled" else True,
            'logical-data-size': logical_data_size,
            'logical-data-limit': logical_data_limit,
        }

        return dedupe_info

    def get_lun_list(self):
        """Gets the list of LUNs on filer.

        Gets the LUNs from cluster with vserver.
        """

        query = {
            'svm.name': self.vserver,
            'fields': 'svm.name,location.volume.name,space.size,'
                      'location.qtree.name,name,os_type,'
                      'space.scsi_thin_provisioning_support_enabled,'
                      'space.guarantee.requested,uuid'
        }

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

        if response['num_records'] == '0':
            return []

        lun_list = []
        for lun in response['records']:
            lun_info = {}
            lun_info['Vserver'] = lun['svm']['name']
            lun_info['Volume'] = lun['location']['volume']['name']
            lun_info['Size'] = lun['space']['size']
            lun_info['Qtree'] = \
                lun['location'].get('qtree', {}).get('name', '')
            lun_info['Path'] = lun['name']
            lun_info['OsType'] = lun['os_type']
            lun_info['SpaceReserved'] = lun['space']['guarantee']['requested']
            lun_info['SpaceAllocated'] = \
                lun['space']['scsi_thin_provisioning_support_enabled']
            lun_info['UUID'] = lun['uuid']

            lun_list.append(lun_info)

        return lun_list

    def get_lun_by_args(self, **lun_info_args):
        """Retrieves LUN with specified args."""

        query = {
            'fields': 'svm.name,location.volume.name,space.size,'
                      'location.qtree.name,name,os_type,'
                      'space.scsi_thin_provisioning_support_enabled,'
                      'space.guarantee.requested,uuid'
        }

        if lun_info_args:
            if 'vserver' in lun_info_args:
                query['svm.name'] = lun_info_args['vserver']
            if 'path' in lun_info_args:
                query['name'] = lun_info_args['path']
            if 'uuid' in lun_info_args:
                query['uuid'] = lun_info_args['uuid']

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

        if response['num_records'] == '0':
            return []

        lun_list = []
        for lun in response['records']:
            lun_info = {}
            lun_info['Vserver'] = lun['svm']['name']
            lun_info['Volume'] = lun['location']['volume']['name']
            lun_info['Size'] = lun['space']['size']
            lun_info['Qtree'] = \
                lun['location'].get('qtree', {}).get('name', '')
            lun_info['Path'] = lun['name']
            lun_info['OsType'] = lun['os_type']
            lun_info['SpaceReserved'] = lun['space']['guarantee']['requested']
            lun_info['SpaceAllocated'] = \
                lun['space']['scsi_thin_provisioning_support_enabled']
            lun_info['UUID'] = lun['uuid']

            # NOTE(nahimsouza): Currently, ONTAP REST API does not have the
            # 'block-size' in the response. By default, we are setting its
            # value to 512, since traditional block size advertised by hard
            # disks is 512 bytes.
            lun_info['BlockSize'] = 512

            lun_list.append(lun_info)

        return lun_list

    def get_lun_sizes_by_volume(self, volume_name):
        """"Gets the list of LUNs and their sizes from a given volume name"""

        query = {
            'location.volume.name': volume_name,
            'svm.name': self.vserver,
            'fields': 'space.size,name'
        }

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

        if response['num_records'] == '0':
            return []

        luns = []
        for lun_info in response['records']:
            luns.append({
                'path': lun_info.get('name', ''),
                'size': float(lun_info.get('space', {}).get('size', 0))
            })
        return luns

    def get_file_sizes_by_dir(self, dir_path):
        """Gets the list of files and their sizes from a given directory."""

        # 'dir_path' will always be a FlexVol name
        volume = self._get_volume_by_args(vol_name=dir_path)

        query = {
            'type': 'file',
            'fields': 'size,name'
        }

        vol_uuid = volume['uuid']
        try:
            response = self.send_request(
                f'/storage/volumes/{vol_uuid}/files',
                'get', query=query)
        except netapp_api.NaApiError as e:
            if e.code == netapp_api.REST_NO_SUCH_FILE:
                return []
            else:
                raise e

        files = []
        for file_info in response['records']:
            files.append({
                'name': file_info.get('name', ''),
                'file-size': float(file_info.get('size', 0))
            })
        return files

    def get_volume_state(self, junction_path=None, name=None):
        """Returns volume state for a given name or junction path."""

        query_args = {}

        if name:
            query_args['name'] = name
        if junction_path:
            query_args['nas.path'] = junction_path

        query_args['fields'] = 'state'

        response = self.send_request('/storage/volumes/',
                                     'get', query=query_args)
        try:
            records = response.get('records', [])
            unique_volume = self._get_unique_volume(records)
        except exception.VolumeBackendAPIException:
            return None

        return unique_volume['state']

    def delete_snapshot(self, volume_name, snapshot_name):
        """Deletes a volume snapshot."""
        volume = self._get_volume_by_args(vol_name=volume_name)
        self.send_request(
            f'/storage/volumes/{volume["uuid"]}/snapshots'
            f'?name={snapshot_name}', 'delete')

    def get_operational_lif_addresses(self):
        """Gets the IP addresses of operational LIFs on the vserver."""

        query = {
            'state': 'up',
            'fields': 'ip.address',
        }

        response = self.send_request(
            '/network/ip/interfaces/', 'get', query=query)

        return [lif_info['ip']['address']
                for lif_info in response['records']]

    def _list_vservers(self):
        """Get the names of vservers present"""
        query = {
            'fields': 'name',
        }
        response = self.send_request('/svm/svms', 'get', query=query,
                                     enable_tunneling=False)

        return [svm['name'] for svm in response.get('records', [])]

    def _get_ems_log_destination_vserver(self):
        """Returns the best vserver destination for EMS messages."""

        # NOTE(nahimsouza): Differently from ZAPI, only 'data' SVMs can be
        # managed by the SVM REST APIs - that's why the vserver type is not
        # specified.
        vservers = self._list_vservers()

        if vservers:
            return vservers[0]

        raise exception.NotFound("No Vserver found to receive EMS messages.")

    def send_ems_log_message(self, message_dict):
        """Sends a message to the Data ONTAP EMS log."""

        body = {
            'computer_name': message_dict['computer-name'],
            'event_source': message_dict['event-source'],
            'app_version': message_dict['app-version'],
            'category': message_dict['category'],
            'severity': 'notice',
            'autosupport_required': message_dict['auto-support'] == 'true',
            'event_id': message_dict['event-id'],
            'event_description': message_dict['event-description'],
        }

        bkp_connection = copy.copy(self.connection)
        bkp_timeout = self.connection.get_timeout()
        bkp_vserver = self.vserver

        self.connection.set_timeout(25)
        try:
            # TODO(nahimsouza): Vserver is being set to replicate the ZAPI
            # behavior, but need to check if this could be removed in REST API
            self.connection.set_vserver(
                self._get_ems_log_destination_vserver())
            self.send_request('/support/ems/application-logs',
                              'post', body=body)
            LOG.debug('EMS executed successfully.')
        except netapp_api.NaApiError as e:
            LOG.warning('Failed to invoke EMS. %s', e)
        finally:
            # Restores the data
            timeout = (
                bkp_timeout if bkp_timeout is not None else DEFAULT_TIMEOUT)
            self.connection.set_timeout(timeout)
            self.connection = copy.copy(bkp_connection)
            self.connection.set_vserver(bkp_vserver)

    def get_performance_counter_info(self, object_name, counter_name):
        """Gets info about one or more Data ONTAP performance counters."""

        # NOTE(nahimsouza): This conversion is nedeed because different names
        # are used in ZAPI and we want to avoid changes in the driver for now.
        rest_counter_names = {
            'domain_busy': 'domain_busy_percent',
            'processor_elapsed_time': 'elapsed_time',
            'avg_processor_busy': 'average_processor_busy_percent',
        }

        rest_counter_name = counter_name
        if counter_name in rest_counter_names:
            rest_counter_name = rest_counter_names[counter_name]

        # Get counter table info
        query = {
            'counter_schemas.name': rest_counter_name,
            'fields': 'counter_schemas.*'
        }

        try:
            table = self.send_request(
                f'/cluster/counter/tables/{object_name}',
                'get', query=query, enable_tunneling=False)

            name = counter_name  # use the original name (ZAPI compatible)
            base_counter = table['counter_schemas'][0]['denominator']['name']

            query = {
                'counters.name': rest_counter_name,
                'fields': 'counters.*'
            }

            response = self.send_request(
                f'/cluster/counter/tables/{object_name}/rows',
                'get', query=query, enable_tunneling=False)

            table_rows = response.get('records', [])
            labels = []
            if len(table_rows) != 0:
                labels = table_rows[0]['counters'][0].get('labels', [])

                # NOTE(nahimsouza): Values have a different format on REST API
                # and we want to keep compatibility with ZAPI for a while
                if object_name == 'wafl' and counter_name == 'cp_phase_times':
                    # discard the prefix 'cp_'
                    labels = [label[3:] for label in labels]

            return {
                'name': name,
                'labels': labels,
                'base-counter': base_counter,
            }
        except netapp_api.NaApiError:
            raise exception.NotFound(_('Counter %s not found') % counter_name)

    def get_performance_instance_uuids(self, object_name, node_name):
        """Get UUIDs of performance instances for a cluster node."""

        query = {
            'id': node_name + ':*',
        }

        response = self.send_request(
            f'/cluster/counter/tables/{object_name}/rows',
            'get', query=query, enable_tunneling=False)

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

        uuids = []
        for record in records:
            uuids.append(record['id'])

        return uuids

    def get_performance_counters(self, object_name, instance_uuids,
                                 counter_names):
        """Gets more cDOT performance counters."""

        # NOTE(nahimsouza): This conversion is nedeed because different names
        # are used in ZAPI and we want to avoid changes in the driver for now.
        rest_counter_names = {
            'domain_busy': 'domain_busy_percent',
            'processor_elapsed_time': 'elapsed_time',
            'avg_processor_busy': 'average_processor_busy_percent',
        }

        zapi_counter_names = {
            'domain_busy_percent': 'domain_busy',
            'elapsed_time': 'processor_elapsed_time',
            'average_processor_busy_percent': 'avg_processor_busy',
        }

        for i in range(len(counter_names)):
            if counter_names[i] in rest_counter_names:
                counter_names[i] = rest_counter_names[counter_names[i]]

        query = {
            'id': '|'.join(instance_uuids),
            'counters.name': '|'.join(counter_names),
            'fields': 'id,counter_table.name,counters.*',
        }

        response = self.send_request(
            f'/cluster/counter/tables/{object_name}/rows',
            'get', query=query, enable_tunneling=False)

        counter_data = []
        for record in response.get('records', []):
            for counter in record['counters']:

                counter_name = counter['name']

                # Reverts the name conversion
                if counter_name in zapi_counter_names:
                    counter_name = zapi_counter_names[counter_name]

                counter_value = ''
                if counter.get('value'):
                    counter_value = counter.get('value')
                elif counter.get('values'):
                    # NOTE(nahimsouza): Conversion made to keep compatibility
                    # with old ZAPI format
                    values = counter.get('values')
                    counter_value = ','.join([str(v) for v in values])

                counter_data.append({
                    'instance-name': record['counter_table']['name'],
                    'instance-uuid': record['id'],
                    'node-name': record['id'].split(':')[0],
                    'timestamp': int(time()),
                    counter_name: counter_value,
                })

        return counter_data

    def get_aggregate_capacities(self, aggregate_names):
        """Gets capacity info for multiple aggregates."""

        if not isinstance(aggregate_names, list):
            return {}

        aggregates = {}
        for aggregate_name in aggregate_names:
            aggregates[aggregate_name] = self._get_aggregate_capacity(
                aggregate_name)

        return aggregates

    def _get_aggregate_capacity(self, aggregate_name):
        """Gets capacity info for an aggregate."""

        fields = ('space.block_storage.available,space.block_storage.size,'
                  'space.block_storage.used')

        try:
            aggrs = self._get_aggregates(aggregate_names=[aggregate_name],
                                         fields=fields)

            result = {}
            if len(aggrs) > 0:
                aggr = aggrs[0]

                available = float(aggr['space']['block_storage']['available'])
                total = float(aggr['space']['block_storage']['size'])
                used = float(aggr['space']['block_storage']['used'])
                percent_used = int((used * 100) // total)

                result = {
                    'percent-used': percent_used,
                    'size-available': available,
                    'size-total': total,
                }

            return result
        except netapp_api.NaApiError as e:
            if (e.code == netapp_api.REST_API_NOT_FOUND or
                    e.code == netapp_api.REST_UNAUTHORIZED):
                LOG.debug('Aggregate capacity can only be collected with '
                          'cluster scoped credentials.')
            else:
                LOG.exception('Failed to get info for aggregate %s.',
                              aggregate_name)
            return {}

    def get_node_for_aggregate(self, aggregate_name):
        """Get home node for the specified aggregate.

        This API could return None, most notably if it was sent
        to a Vserver LIF, so the caller must be able to handle that case.
        """

        if not aggregate_name:
            return None

        fields = 'home_node.name'
        try:
            aggrs = self._get_aggregates(aggregate_names=[aggregate_name],
                                         fields=fields)
            node = None
            if len(aggrs) > 0:
                aggr = aggrs[0]
                node = aggr['home_node']['name']

            return node
        except netapp_api.NaApiError as e:
            if e.code == netapp_api.REST_API_NOT_FOUND:
                return None
            else:
                raise e

    def provision_qos_policy_group(self, qos_policy_group_info,
                                   qos_min_support):
        """Create QoS policy group on the backend if appropriate."""
        if qos_policy_group_info is None:
            return

        # Legacy QoS uses externally provisioned QoS policy group,
        # so we don't need to create one on the backend.
        legacy = qos_policy_group_info.get('legacy')
        if legacy:
            return

        spec = qos_policy_group_info.get('spec')

        if not spec:
            return

        is_adaptive = na_utils.is_qos_policy_group_spec_adaptive(
            qos_policy_group_info)
        self._validate_qos_policy_group(is_adaptive, spec=spec,
                                        qos_min_support=qos_min_support)

        qos_policy_group = self._get_qos_first_policy_group_by_name(
            spec['policy_name'])

        if not qos_policy_group:
            self._create_qos_policy_group(spec, is_adaptive)
        else:
            self._modify_qos_policy_group(spec, is_adaptive,
                                          qos_policy_group)

    def _get_qos_first_policy_group_by_name(self, qos_policy_group_name):
        records = self._get_qos_policy_group_by_name(qos_policy_group_name)
        if len(records) == 0:
            return None

        return records[0]

    def _get_qos_policy_group_by_name(self, qos_policy_group_name):
        query = {'name': qos_policy_group_name}

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

        records = response.get('records')
        if not records:
            return []

        return records

    def _qos_spec_to_api_args(self, spec, is_adaptive, vserver=None):
        """Convert a QoS spec to REST args."""
        rest_args = {}
        if is_adaptive:
            rest_args['adaptive'] = {}
            if spec.get('absolute_min_iops'):
                rest_args['adaptive']['absolute_min_iops'] = (
                    self._sanitize_qos_spec_value(
                        spec.get('absolute_min_iops')))
            if spec.get('expected_iops'):
                rest_args['adaptive']['expected_iops'] = (
                    self._sanitize_qos_spec_value(spec.get('expected_iops')))
            if spec.get('expected_iops_allocation'):
                rest_args['adaptive']['expected_iops_allocation'] = (
                    spec.get('expected_iops_allocation'))
            if spec.get('peak_iops'):
                rest_args['adaptive']['peak_iops'] = (
                    self._sanitize_qos_spec_value(spec.get('peak_iops')))
            if spec.get('peak_iops_allocation'):
                rest_args['adaptive']['peak_iops_allocation'] = (
                    spec.get('peak_iops_allocation'))
            if spec.get('block_size'):
                rest_args['adaptive']['block_size'] = (
                    spec.get('block_size'))
        else:
            rest_args['fixed'] = {}
            qos_max = spec.get('max_throughput')
            if qos_max and 'iops' in qos_max:
                rest_args['fixed']['max_throughput_iops'] = (
                    self._sanitize_qos_spec_value(qos_max))
            elif qos_max:
                # Convert from B/s to MB/s
                value = math.ceil(
                    self._sanitize_qos_spec_value(qos_max) / (10**6))
                rest_args['fixed']['max_throughput_mbps'] = value

            qos_min = spec.get('min_throughput')
            if qos_min and 'iops' in qos_min:
                rest_args['fixed']['min_throughput_iops'] = (
                    self._sanitize_qos_spec_value(qos_min))

        if spec.get('policy_name'):
            rest_args['name'] = spec.get('policy_name')
        if spec.get('return_record'):
            rest_args['return_records'] = spec.get('return_record')

        if vserver:
            rest_args['svm'] = {}
            rest_args['svm']['name'] = vserver

        return rest_args

    def _sanitize_qos_spec_value(self, value):
        value = value.lower()
        value = value.replace('iops', '').replace('b/s', '')
        value = int(value)
        return value

    def _create_qos_policy_group(self, spec, is_adaptive):
        """Creates a QoS policy group."""
        body = self._qos_spec_to_api_args(
            spec, is_adaptive, vserver=self.vserver)

        self.send_request('/storage/qos/policies/', 'post', body=body,
                          enable_tunneling=False)

    def _modify_qos_policy_group(self, spec, is_adaptive, qos_policy_group):
        """Modifies a QoS policy group."""
        body = self._qos_spec_to_api_args(spec, is_adaptive)
        if qos_policy_group['name'] == body['name']:
            body.pop('name')

        self.send_request(
            f'/storage/qos/policies/{qos_policy_group["uuid"]}', 'patch',
            body=body, enable_tunneling=False)

    def get_vol_by_junc_vserver(self, vserver, junction):
        """Gets the volume by junction path and vserver."""
        volume = self._get_volume_by_args(vol_path=junction, vserver=vserver)
        return volume['name']

    def file_assign_qos(self, flex_vol, qos_policy_group_name,
                        qos_policy_group_is_adaptive, file_path):
        """Assigns the named QoS policy-group to a file."""
        volume = self._get_volume_by_args(flex_vol)
        body = {
            'qos_policy.name': qos_policy_group_name
        }

        self.send_request(
            f'/storage/volumes/{volume["uuid"]}/files/{file_path}',
            'patch', body=body, enable_tunneling=False)

    def mark_qos_policy_group_for_deletion(self, qos_policy_group_info,
                                           is_adaptive=False):
        """Soft delete a QoS policy group backing a cinder volume."""
        if qos_policy_group_info is None:
            return

        spec = qos_policy_group_info.get('spec')

        # For cDOT we want to delete the QoS policy group that we created for
        # this cinder volume.  Because the QoS policy may still be "in use"
        # after the zapi call to delete the volume itself returns successfully,
        # we instead rename the QoS policy group using a specific pattern and
        # later attempt on a best effort basis to delete any QoS policy groups
        # matching that pattern.
        if spec:
            current_name = spec['policy_name']
            new_name = DELETED_PREFIX + current_name
            try:
                self._rename_qos_policy_group(current_name, new_name)
            except netapp_api.NaApiError as ex:
                LOG.warning('Rename failure in cleanup of cDOT QoS policy '
                            'group %(current_name)s: %(ex)s',
                            {'current_name': current_name, 'ex': ex})

        # Attempt to delete any QoS policies named "delete-openstack-*".
        self.remove_unused_qos_policy_groups()

    def delete_file(self, path_to_file):
        """Delete file at path."""
        LOG.debug('Deleting file: %s', path_to_file)

        volume_name = path_to_file.split('/')[2]
        relative_path = '/'.join(path_to_file.split('/')[3:])
        volume = self._get_volume_by_args(volume_name)

        # Path requires "%2E" to represent "." and "%2F" to represent "/".
        relative_path = relative_path.replace('.', '%2E').replace('/', '%2F')

        self.send_request(f'/storage/volumes/{volume["uuid"]}'
                          + f'/files/{relative_path}', 'delete')

    def _rename_qos_policy_group(self, qos_policy_group_name, new_name):
        """Renames a QoS policy group."""
        body = {'name': new_name}
        query = {'name': qos_policy_group_name}
        self.send_request('/storage/qos/policies/', 'patch', body=body,
                          query=query, enable_tunneling=False)

    def remove_unused_qos_policy_groups(self):
        """Deletes all QoS policy groups that are marked for deletion."""
        query = {'name': f'{DELETED_PREFIX}*'}
        self.send_request('/storage/qos/policies', 'delete', query=query)

    def create_lun(self, volume_name, lun_name, size, metadata,
                   qos_policy_group_name=None,
                   qos_policy_group_is_adaptive=False):
        """Issues API request for creating LUN on volume."""
        self._validate_qos_policy_group(qos_policy_group_is_adaptive)

        path = f'/vol/{volume_name}/{lun_name}'
        space_reservation = metadata['SpaceReserved']
        space_allocation = metadata['SpaceAllocated']
        initial_size = size

        body = {
            'name': path,
            'space.size': str(initial_size),
            'os_type': metadata['OsType'],
            'space.guarantee.requested': space_reservation,
            'space.scsi_thin_provisioning_support_enabled': space_allocation
        }

        if qos_policy_group_name:
            body['qos_policy.name'] = qos_policy_group_name

        try:
            self.send_request('/storage/luns', 'post', body=body)
        except netapp_api.NaApiError as ex:
            with excutils.save_and_reraise_exception():
                LOG.error('Error provisioning volume %(lun_name)s on '
                          '%(volume_name)s. Details: %(ex)s',
                          {
                              'lun_name': lun_name,
                              'volume_name': volume_name,
                              'ex': ex,
                          })

    def do_direct_resize(self, path, new_size_bytes, force=True):
        """Resize the LUN."""
        seg = path.split("/")
        LOG.info('Resizing LUN %s directly to new size.', seg[-1])

        body = {'name': path, 'space.size': new_size_bytes}

        self._lun_update_by_path(path, body)

    def _get_lun_by_path(self, path, fields=None):
        query = {'name': path}

        if fields:
            query['fields'] = fields

        response = self.send_request('/storage/luns', 'get', query=query)
        records = response.get('records', [])

        return records

    def _get_first_lun_by_path(self, path, fields=None):
        records = self._get_lun_by_path(path, fields=fields)
        if len(records) == 0:
            return None

        return records[0]

    def _lun_update_by_path(self, path, body):
        """Update the LUN."""
        lun = self._get_first_lun_by_path(path)

        if not lun:
            raise netapp_api.NaApiError(code=netapp_api.EOBJECTNOTFOUND)

        self.send_request(f'/storage/luns/{lun["uuid"]}', 'patch', body=body)

    def _validate_qos_policy_group(self, is_adaptive, spec=None,
                                   qos_min_support=False):
        if is_adaptive and not self.features.ADAPTIVE_QOS:
            msg = _("Adaptive QoS feature requires ONTAP 9.4 or later.")
            raise na_utils.NetAppDriverException(msg)

        if not spec:
            return

        if 'min_throughput' in spec and not qos_min_support:
            msg = 'min_throughput is not supported by this back end.'
            raise na_utils.NetAppDriverException(msg)

    def get_if_info_by_ip(self, ip):
        """Gets the network interface info by ip."""
        query_args = {}
        query_args['ip.address'] = volume_utils.resolve_hostname(ip)
        query_args['fields'] = 'svm'

        result = self.send_request('/network/ip/interfaces/', 'get',
                                   query=query_args, enable_tunneling=False)
        num_records = result['num_records']
        records = result.get('records', [])

        if num_records == 0:
            raise exception.NotFound(
                _('No interface found on cluster for ip %s') % ip)

        return [{'vserver': item['svm']['name']} for item in records]

    def get_igroup_by_initiators(self, initiator_list):
        """Get igroups exactly matching a set of initiators."""

        igroup_list = []
        if not initiator_list:
            return igroup_list

        query = {
            'svm.name': self.vserver,
            'initiators.name': ','.join(initiator_list),
            'fields': 'name,protocol,os_type'
        }

        response = self.send_request('/protocols/san/igroups',
                                     'get', query=query)
        records = response.get('records', [])
        for igroup_item in records:
            igroup = {'initiator-group-os-type': igroup_item['os_type'],
                      'initiator-group-type': igroup_item['protocol'],
                      'initiator-group-name': igroup_item['name']}
            igroup_list.append(igroup)

        return igroup_list

    def add_igroup_initiator(self, igroup, initiator):
        """Adds initiators to the specified igroup."""
        query_initiator_uuid = {
            'name': igroup,
            'fields': 'uuid'
        }

        response_initiator_uuid = self.send_request(
            '/protocols/san/igroups/', 'get', query=query_initiator_uuid)

        response = response_initiator_uuid.get('records', [])
        if len(response) < 1:
            msg = _('Could not find igroup initiator.')
            raise exception.VolumeBackendAPIException(data=msg)

        igroup_uuid = response[0]['uuid']

        body = {
            'name': initiator
        }

        self.send_request('/protocols/san/igroups/' +
                          igroup_uuid + '/initiators',
                          'post', body=body)

    def create_igroup(self, igroup, igroup_type='iscsi', os_type='default'):
        """Creates igroup with specified args."""
        body = {
            'name': igroup,
            'protocol': igroup_type,
            'os_type': os_type,
        }
        self.send_request('/protocols/san/igroups', 'post', body=body)

    def map_lun(self, path, igroup_name, lun_id=None):
        """Maps LUN to the initiator and returns LUN id assigned."""

        body_post = {
            'lun.name': path,
            'igroup.name': igroup_name,
        }

        if lun_id is not None:
            body_post['logical_unit_number'] = lun_id

        try:
            result = self.send_request('/protocols/san/lun-maps', 'post',
                                       body=body_post,
                                       query={'return_records': 'true'})
            records = result.get('records')
            lun_id_assigned = records[0].get('logical_unit_number')
            return lun_id_assigned
        except netapp_api.NaApiError as e:
            code = e.code
            message = e.message
            LOG.warning('Error mapping LUN. Code :%(code)s, Message: '
                        '%(message)s', {'code': code, 'message': message})
            raise

    def get_lun_map(self, path):
        """Gets the LUN map by LUN path."""
        map_list = []

        query = {
            'lun.name': path,
            'fields': 'igroup.name,logical_unit_number,svm.name',
        }

        response = self.send_request('/protocols/san/lun-maps',
                                     'get',
                                     query=query)
        num_records = response.get('num_records')
        records = response.get('records', None)
        if records is None or num_records is None:
            return map_list

        for element in records:
            map_lun = {}
            map_lun['initiator-group'] = element['igroup']['name']
            map_lun['lun-id'] = element['logical_unit_number']
            map_lun['vserver'] = element['svm']['name']
            map_list.append(map_lun)

        return map_list

    def get_fc_target_wwpns(self):
        """Gets the FC target details."""
        wwpns = []
        query = {
            'fields': 'wwpn'
        }
        response = self.send_request('/network/fc/interfaces',
                                     'get', query=query)

        records = response.get('records')
        for record in records:
            wwpn = record.get('wwpn').lower()
            wwpns.append(wwpn)

        return wwpns

    def unmap_lun(self, path, igroup_name):
        """Unmaps a LUN from given initiator."""

        # get lun amd igroup uuids
        query_uuid = {
            'igroup.name': igroup_name,
            'lun.name': path,
            'fields': 'lun.uuid,igroup.uuid'
        }

        response_uuid = self.send_request(
            '/protocols/san/lun-maps', 'get', query=query_uuid)

        if response_uuid['num_records'] > 0:
            lun_uuid = response_uuid['records'][0]['lun']['uuid']
            igroup_uuid = response_uuid['records'][0]['igroup']['uuid']

            try:
                self.send_request(
                    f'/protocols/san/lun-maps/{lun_uuid}/{igroup_uuid}',
                    'delete')
            except netapp_api.NaApiError as e:
                LOG.warning("Error unmapping LUN. Code: %(code)s, Message: "
                            "%(message)s", {'code': e.code,
                                            'message': e.message})
                # if the LUN is already unmapped
                if e.code == netapp_api.REST_NO_SUCH_LUN_MAP:
                    pass
                else:
                    raise e
        else:
            # Input is invalid or LUN may already be unmapped
            LOG.warning("Error unmapping LUN. Invalid input.")

    def has_luns_mapped_to_initiators(self, initiator_list):
        """Checks whether any LUNs are mapped to the given initiator(s)."""
        query = {
            'initiators.name': ','.join(initiator_list),
            'fields': 'lun_maps'
        }

        response = self.send_request('/protocols/san/igroups',
                                     'get', query=query)

        records = response.get('records', [])
        if len(records) > 0:
            for record in records:
                lun_maps = record.get('lun_maps', [])
                if len(lun_maps) > 0:
                    return True

        return False

    def get_iscsi_service_details(self):
        """Returns iscsi iqn."""
        query = {
            'fields': 'target.name'
        }
        response = self.send_request(
            '/protocols/san/iscsi/services', 'get', query=query)
        records = response.get('records')
        if records:
            return records[0]['target']['name']

        LOG.debug('No iSCSI service found for vserver %s', self.vserver)
        return None

    def check_iscsi_initiator_exists(self, iqn):
        """Returns True if initiator exists."""
        endpoint_url = '/protocols/san/iscsi/credentials'
        initiator_exists = True
        try:
            query = {
                'initiator': iqn,
            }
            response = self.send_request(endpoint_url, 'get', query=query)
            records = response.get('records')
            if not records:
                initiator_exists = False

        except netapp_api.NaApiError:
            initiator_exists = False

        return initiator_exists

    def set_iscsi_chap_authentication(self, iqn, username, password):
        """Provides NetApp host's CHAP credentials to the backend."""
        initiator_exists = self.check_iscsi_initiator_exists(iqn)

        command_template = ('iscsi security %(mode)s -vserver %(vserver)s '
                            '-initiator-name %(iqn)s -auth-type CHAP '
                            '-user-name %(username)s')

        if initiator_exists:
            LOG.debug('Updating CHAP authentication for %(iqn)s.',
                      {'iqn': iqn})
            command = command_template % {
                'mode': 'modify',
                'vserver': self.vserver,
                'iqn': iqn,
                'username': username,
            }
        else:
            LOG.debug('Adding initiator %(iqn)s with CHAP authentication.',
                      {'iqn': iqn})
            command = command_template % {
                'mode': 'create',
                'vserver': self.vserver,
                'iqn': iqn,
                'username': username,
            }

        try:
            with self.ssh_client.ssh_connect_semaphore:
                ssh_pool = self.ssh_client.ssh_pool
                with ssh_pool.item() as ssh:
                    self.ssh_client.execute_command_with_prompt(ssh,
                                                                command,
                                                                'Password:',
                                                                password)
        except Exception as e:
            msg = _('Failed to set CHAP authentication for target IQN %(iqn)s.'
                    ' Details: %(ex)s') % {
                'iqn': iqn,
                'ex': e,
            }
            LOG.error(msg)
            raise exception.VolumeBackendAPIException(data=msg)

    def get_iscsi_target_details(self):
        """Gets the iSCSI target portal details."""
        query = {
            'services': 'data_iscsi',
            'fields': 'ip.address,enabled'
        }

        response = self.send_request('/network/ip/interfaces',
                                     'get', query=query)

        target_list = []
        records = response.get('records', [])
        for record in records:
            details = dict()
            details['address'] = record['ip']['address']
            details['tpgroup-tag'] = None
            details['interface-enabled'] = record['enabled']
            # NOTE(nahimsouza): from ONTAP documentation:
            # ONTAP does not support changing the port number for iSCSI.
            # Port number 3260 is registered as part of the iSCSI specification
            # and cannot be used by any other application or service.
            details['port'] = 3260
            target_list.append(details)

        return target_list

    def move_lun(self, path, new_path):
        """Moves the LUN at path to new path."""
        seg = path.split("/")
        new_seg = new_path.split("/")
        LOG.debug("Moving LUN %(name)s to %(new_name)s.",
                  {'name': seg[-1], 'new_name': new_seg[-1]})
        query = {
            'svm.name': self.vserver,
            'name': path
        }
        body = {
            'name': new_path,
        }
        self.send_request('/storage/luns/', 'patch', query=query, body=body)

    def clone_file(self, flex_vol, src_path, dest_path, vserver,
                   dest_exists=False, source_snapshot=None, is_snapshot=False):
        """Clones file on vserver."""
        LOG.debug('Cloning file - volume %(flex_vol)s, src %(src_path)s, '
                  'dest %(dest_path)s, vserver %(vserver)s,'
                  'source_snapshot %(source_snapshot)s',
                  {
                      'flex_vol': flex_vol,
                      'src_path': src_path,
                      'dest_path': dest_path,
                      'vserver': vserver,
                      'source_snapshot': source_snapshot,
                  })

        volume = self._get_volume_by_args(flex_vol)
        body = {
            'volume': {
                'uuid': volume['uuid'],
                'name': volume['name']
            },
            'source_path': src_path,
            'destination_path': dest_path,
        }
        if is_snapshot and self.features.BACKUP_CLONE_PARAM:
            body['is_backup'] = True

        if dest_exists:
            body['overwrite_destination'] = True

        self.send_request('/storage/file/clone', 'post', body=body)

    def clone_lun(self, volume, name, new_name, space_reserved='true',
                  qos_policy_group_name=None, src_block=0, dest_block=0,
                  block_count=0, source_snapshot=None, is_snapshot=False,
                  qos_policy_group_is_adaptive=False):
        """Clones lun on vserver."""
        LOG.debug('Cloning lun - volume: %(volume)s, name: %(name)s, '
                  'new_name: %(new_name)s, space_reserved: %(space_reserved)s,'
                  ' qos_policy_group_name: %(qos_policy_group_name)s',
                  {
                      'volume': volume,
                      'name': name,
                      'new_name': new_name,
                      'space_reserved': space_reserved,
                      'qos_policy_group_name': qos_policy_group_name,
                  })

        # NOTE(nahimsouza): some parameters are not available on REST API,
        # but they are in the header just to keep compatilbility with ZAPI:
        # src_block, dest_block, block_count, is_snapshot

        self._validate_qos_policy_group(qos_policy_group_is_adaptive)

        source_path = f'/vol/{volume}'
        if source_snapshot:
            source_path += f'/.snapshot/{source_snapshot}'
        source_path += f'/{name}'
        body = {
            'svm': {
                'name': self.vserver
            },
            'name': f'/vol/{volume}/{new_name}',
            'clone': {
                'source': {
                    'name': source_path,
                }
            },
            'space': {
                'guarantee': {
                    'requested': space_reserved == 'true',
                }
            }
        }

        if qos_policy_group_name:
            body['qos_policy'] = {'name': qos_policy_group_name}

        self.send_request('/storage/luns', 'post', body=body)

    def destroy_lun(self, path, force=True):
        """Destroys the LUN at the path."""
        query = {}
        query['name'] = path
        query['svm'] = self.vserver

        if force:
            query['allow_delete_while_mapped'] = 'true'

        self.send_request('/storage/luns/', 'delete', query=query)

    def get_flexvol_capacity(self, flexvol_path=None, flexvol_name=None):
        """Gets total capacity and free capacity, in bytes, of the flexvol."""
        fields = 'name,space.available,space.afs_total'
        try:
            volume = self._get_volume_by_args(
                vol_name=flexvol_name, vol_path=flexvol_path, fields=fields)
            capacity = {
                'size-total': float(volume['space']['afs_total']),
                'size-available': float(volume['space']['available']),
            }
            return capacity
        except exception.VolumeBackendAPIException:
            msg = _('Volume %s not found.')
            msg_args = flexvol_path or flexvol_name
            raise na_utils.NetAppDriverException(msg % msg_args)

    def get_provisioning_options_from_flexvol(self, flexvol_name):
        """Get a dict of provisioning options matching existing flexvol."""

        flexvol_info = self.get_flexvol(flexvol_name=flexvol_name)
        dedupe_info = self.get_flexvol_dedupe_info(flexvol_name)

        provisioning_opts = {
            'aggregate': flexvol_info['aggregate'],
            # space-guarantee can be 'none', 'file', 'volume'
            'space_guarantee_type': flexvol_info.get('space-guarantee'),
            'snapshot_policy': flexvol_info['snapshot-policy'],
            'language': flexvol_info['language'],
            'dedupe_enabled': dedupe_info['dedupe'],
            'compression_enabled': dedupe_info['compression'],
            'snapshot_reserve': flexvol_info['percentage-snapshot-reserve'],
            'volume_type': flexvol_info['type'],
            'size': int(math.ceil(float(flexvol_info['size']) / units.Gi)),
            'is_flexgroup': flexvol_info['style-extended'] == 'flexgroup',
        }

        return provisioning_opts

    def flexvol_exists(self, volume_name):
        """Checks if a flexvol exists on the storage array."""
        LOG.debug('Checking if volume %s exists', volume_name)

        query = {
            'name': volume_name,
            'return_records': 'false'
        }

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

        return response['num_records'] > 0

    def create_volume_async(self, name, aggregate_list, size_gb,
                            space_guarantee_type=None, snapshot_policy=None,
                            language=None, dedupe_enabled=False,
                            compression_enabled=False, snapshot_reserve=None,
                            volume_type='rw'):
        """Creates a volume asynchronously."""

        body = {
            'name': name,
            'size': size_gb * units.Gi,
            'type': volume_type,
        }

        if isinstance(aggregate_list, list):
            body['style'] = 'flexgroup'
            body['aggregates'] = [{'name': aggr} for aggr in aggregate_list]
        else:
            body['style'] = 'flexvol'
            body['aggregates'] = [{'name': aggregate_list}]

        if volume_type == 'dp':
            snapshot_policy = None
        else:
            body['nas'] = {'path': '/%s' % name}

        if snapshot_policy is not None:
            body['snapshot_policy'] = {'name': snapshot_policy}

        if space_guarantee_type:
            body['guarantee'] = {'type': space_guarantee_type}

        if language is not None:
            body['language'] = language

        if snapshot_reserve is not None:
            body['space'] = {
                'snapshot': {
                    'reserve_percent': str(snapshot_reserve)
                }
            }

        # cDOT compression requires that deduplication be enabled.
        if dedupe_enabled or compression_enabled:
            body['efficiency'] = {'dedupe': 'background'}

        if compression_enabled:
            body['efficiency']['compression'] = 'background'

        response = self.send_request('/storage/volumes/', 'post', body=body,
                                     wait_on_accepted=False)

        job_info = {
            'status': None,
            'jobid': response["job"]["uuid"],
            'error-code': None,
            'error-message': None,
        }

        return job_info

    def create_flexvol(self, flexvol_name, aggregate_name, size_gb,
                       space_guarantee_type=None, snapshot_policy=None,
                       language=None, dedupe_enabled=False,
                       compression_enabled=False, snapshot_reserve=None,
                       volume_type='rw'):
        """Creates a flexvol asynchronously and return the job info."""

        return self.create_volume_async(
            flexvol_name, aggregate_name, size_gb,
            space_guarantee_type=space_guarantee_type,
            snapshot_policy=snapshot_policy, language=language,
            dedupe_enabled=dedupe_enabled,
            compression_enabled=compression_enabled,
            snapshot_reserve=snapshot_reserve, volume_type=volume_type)

    def enable_volume_dedupe_async(self, volume_name):
        """Enable deduplication on FlexVol/FlexGroup volume asynchronously."""

        query = {
            'name': volume_name,
            'fields': 'uuid,style',
        }
        body = {
            'efficiency': {'dedupe': 'background'}
        }
        self.send_request('/storage/volumes/', 'patch', body=body, query=query,
                          wait_on_accepted=False)

    def enable_volume_compression_async(self, volume_name):
        """Enable compression on FlexVol/FlexGroup volume asynchronously."""
        query = {
            'name': volume_name
        }
        body = {
            'efficiency': {'compression': 'background'}
        }
        self.send_request('/storage/volumes/', 'patch', body=body, query=query,
                          wait_on_accepted=False)

    def _parse_lagtime(self, time_str):
        """Parse lagtime string (ISO 8601) into a number of seconds."""

        fmt_str = 'PT'
        if 'H' in time_str:
            fmt_str += '%HH'
        if 'M' in time_str:
            fmt_str += '%MM'
        if 'S' in time_str:
            fmt_str += '%SS'

        t = None
        try:
            t = datetime.strptime(time_str, fmt_str)
        except Exception:
            LOG.debug("Failed to parse lagtime: %s", time_str)
            raise

        # convert to timedelta to get the total seconds
        td = timedelta(hours=t.hour, minutes=t.minute, seconds=t.second)
        return td.total_seconds()

    def _get_snapmirrors(self, source_vserver=None, source_volume=None,
                         destination_vserver=None, destination_volume=None):

        fields = ['state', 'source.svm.name', 'source.path',
                  'destination.svm.name', 'destination.path', 'transfer.state',
                  'transfer.end_time', 'lag_time', 'healthy', 'uuid']

        query = {}
        query['fields'] = '{}'.format(','.join(f for f in fields))

        query_src_vol = source_volume if source_volume else '*'
        query_src_vserver = source_vserver if source_vserver else '*'
        query['source.path'] = query_src_vserver + ':' + query_src_vol

        query_dst_vol = destination_volume if destination_volume else '*'
        query_dst_vserver = destination_vserver if destination_vserver else '*'
        query['destination.path'] = query_dst_vserver + ':' + query_dst_vol

        response = self.send_request(
            '/snapmirror/relationships', 'get', query=query)

        snapmirrors = []
        for record in response.get('records', []):
            snapmirrors.append({
                'relationship-status': (
                    'idle'
                    if record.get('state') == 'snapmirrored'
                    else record.get('state')),
                'transferring-state': record.get('transfer', {}).get('state'),
                'mirror-state': record['state'],
                'source-vserver': record['source']['svm']['name'],
                'source-volume': (record['source']['path'].split(':')[1] if
                                  record.get('source') else None),
                'destination-vserver': record['destination']['svm']['name'],
                'destination-volume': (
                    record['destination']['path'].split(':')[1]
                    if record.get('destination') else None),
                'last-transfer-end-timestamp':
                    (record['transfer']['end_time'] if
                     record.get('transfer', {}).get('end_time') else None),
                'lag-time': (self._parse_lagtime(record['lag_time']) if
                             record.get('lag_time') else None),
                'is-healthy': record['healthy'],
                'uuid': record['uuid']
            })

        return snapmirrors

    def get_snapmirrors(self, source_vserver, source_volume,
                        destination_vserver, destination_volume,
                        desired_attributes=None):
        """Gets one or more SnapMirror relationships.

        Either the source or destination info may be omitted.
        Desired attributes exists only to keep consistent with ZAPI client
        signature and has no effect in the output.
        """

        snapmirrors = self._get_snapmirrors(
            source_vserver=source_vserver,
            source_volume=source_volume,
            destination_vserver=destination_vserver,
            destination_volume=destination_volume)

        return snapmirrors

    def create_ontap_consistency_group(self, source_vserver, source_volume,
                                       source_cg):
        """Creates a ontap consistency group"""

        body = {
            'svm': {
                'name': source_vserver
            },
            'name': source_cg,
            'volumes': [{
                'name': source_volume,
                "provisioning_options": {"action": "add"}
            }]
        }

        try:
            self.send_request('/application/consistency-groups/', 'post',
                              body=body)
        except netapp_api.NaApiError as e:
            if e.code != netapp_api.REST_ERELATION_EXISTS:
                raise e

    def create_snapmirror(self, source_vserver, source_volume,
                          destination_vserver, destination_volume,
                          source_cg=None,
                          destination_cg=None,
                          schedule=None, policy=None,
                          relationship_type='data_protection'):
        """Creates a SnapMirror relationship.

        The schedule and relationship type is kept to avoid breaking
        the API used by data_motion, but are not used on the REST API.

        The schedule is part of the policy associated the relationship and the
        relationship_type will be ignored because XDP is the only type
        supported through REST API.
        """
        if source_cg is not None:
            body = {
                'source': {
                    'path': source_vserver + ':/cg/' + source_cg,
                    'consistency_group_volumes': [{'name': source_volume}]
                },
                'destination': {
                    'path': destination_vserver + ':/cg/' + destination_cg,
                    'consistency_group_volumes':
                        [{'name': destination_volume}]
                },
                'state': 'in_sync'
            }
        else:
            body = {
                'source': {
                    'path': source_vserver + ':' + source_volume
                },
                'destination': {
                    'path': destination_vserver + ':' + destination_volume
                }
            }

        if policy:
            body['policy'] = {'name': policy}

        try:
            self.send_request('/snapmirror/relationships/', 'post', body=body)
        except netapp_api.NaApiError as e:
            if e.code != netapp_api.REST_ERELATION_EXISTS:
                raise e

    def _set_snapmirror_state(self, state, source_vserver, source_volume,
                              destination_vserver, destination_volume,
                              wait_result=True):
        """Change the snapmirror state between two volumes."""

        snapmirror = self.get_snapmirrors(source_vserver, source_volume,
                                          destination_vserver,
                                          destination_volume)

        if not snapmirror:
            msg = _('Failed to get information about relationship between '
                    'source %(src_vserver)s:%(src_volume)s and '
                    'destination %(dst_vserver)s:%(dst_volume)s.') % {
                'src_vserver': source_vserver,
                'src_volume': source_volume,
                'dst_vserver': destination_vserver,
                'dst_volume': destination_volume}
            raise na_utils.NetAppDriverException(msg)

        uuid = snapmirror[0]['uuid']
        body = {'state': state}
        result = self.send_request('/snapmirror/relationships/' + uuid,
                                   'patch', body=body,
                                   wait_on_accepted=wait_result)

        job_info = {
            'operation-id': None,
            'status': None,
            'jobid': result.get('job', {}).get('uuid'),
            'error-code': None,
            'error-message': None,
            'relationship-uuid': uuid,
        }

        return job_info

    def initialize_snapmirror(self, source_vserver, source_volume,
                              destination_vserver, destination_volume,
                              active_sync_policy=False,
                              source_snapshot=None, transfer_priority=None):
        """Initializes a SnapMirror relationship."""

        # TODO: Trigger a geometry exception to be caught by data_motion.
        # This error is raised when using ZAPI with different volume component
        # numbers, but in REST, the job must be checked sometimes before that
        # error occurs.
        state = 'snapmirrored'
        if active_sync_policy:
            state = 'in_sync'
        return self._set_snapmirror_state(
            state, source_vserver, source_volume,
            destination_vserver, destination_volume, wait_result=False)

    def abort_snapmirror(self, source_vserver, source_volume,
                         destination_vserver, destination_volume,
                         clear_checkpoint=False):
        """Stops ongoing transfers for a SnapMirror relationship."""

        snapmirror = self.get_snapmirrors(source_vserver, source_volume,
                                          destination_vserver,
                                          destination_volume)
        if not snapmirror:
            msg = _('Failed to get information about relationship between '
                    'source %(src_vserver)s:%(src_volume)s and '
                    'destination %(dst_vserver)s:%(dst_volume)s.') % {
                'src_vserver': source_vserver,
                'src_volume': source_volume,
                'dst_vserver': destination_vserver,
                'dst_volume': destination_volume}
            raise na_utils.NetAppDriverException(msg)

        snapmirror_uuid = snapmirror[0]['uuid']

        query = {'state': 'transferring'}
        transfers = self.send_request('/snapmirror/relationships/' +
                                      snapmirror_uuid + '/transfers/', 'get',
                                      query=query)

        if not transfers.get('records'):
            raise netapp_api.NaApiError(
                code=netapp_api.ENOTRANSFER_IN_PROGRESS)

        body = {'state': 'hard_aborted' if clear_checkpoint else 'aborted'}

        for transfer in transfers['records']:
            transfer_uuid = transfer['uuid']
            self.send_request('/snapmirror/relationships/' +
                              snapmirror_uuid + '/transfers/' +
                              transfer_uuid, 'patch', body=body)

    def delete_snapmirror(self, source_vserver, source_volume,
                          destination_vserver, destination_volume):

        """Deletes an SnapMirror relationship on destination."""

        query_uuid = {}
        query_uuid['source.path'] = source_vserver + ':' + source_volume
        query_uuid['destination.path'] = (destination_vserver + ':' +
                                          destination_volume)
        query_uuid['fields'] = 'uuid'

        response = self.send_request('/snapmirror/relationships/', 'get',
                                     query=query_uuid)

        records = response.get('records')
        if not records:
            raise netapp_api.NaApiError(code=netapp_api.EOBJECTNOTFOUND)

        # 'destination_only' deletes the snapmirror on destination but does not
        # release it on source.
        query_delete = {"destination_only": "true"}

        snapmirror_uuid = records[0].get('uuid')
        self.send_request('/snapmirror/relationships/' +
                          snapmirror_uuid, 'delete',
                          query=query_delete)

    def resume_snapmirror(self, source_vserver, source_volume,
                          destination_vserver, destination_volume):

        """Resume a SnapMirror relationship."""

        query_uuid = {}
        query_uuid['source.path'] = source_vserver + ':' + source_volume
        query_uuid['destination.path'] = (destination_vserver + ':' +
                                          destination_volume)
        query_uuid['fields'] = 'uuid,policy.type'

        response_snapmirrors = self.send_request('/snapmirror/relationships/',
                                                 'get', query=query_uuid)

        records = response_snapmirrors.get('records')
        if not records:
            raise netapp_api.NaApiError(code=netapp_api.EOBJECTNOTFOUND)

        snapmirror_uuid = records[0]['uuid']
        snapmirror_policy = records[0]['policy']['type']

        body_resync = {}
        if snapmirror_policy == 'async':
            body_resync['state'] = 'snapmirrored'
        elif snapmirror_policy == 'sync':
            body_resync['state'] = 'in_sync'

        self.send_request('/snapmirror/relationships/' +
                          snapmirror_uuid, 'patch',
                          body=body_resync)

    def release_snapmirror(self, source_vserver, source_volume,
                           destination_vserver, destination_volume,
                           relationship_info_only=False):
        """Removes a SnapMirror relationship on the source endpoint."""

        query_uuid = {}
        query_uuid['list_destinations_only'] = 'true'
        query_uuid['source.path'] = source_vserver + ':' + source_volume
        query_uuid['destination.path'] = (destination_vserver + ':' +
                                          destination_volume)
        query_uuid['fields'] = 'uuid'

        response_snapmirrors = self.send_request('/snapmirror/relationships/',
                                                 'get', query=query_uuid)

        records = response_snapmirrors.get('records')
        if not records:
            raise netapp_api.NaApiError(code=netapp_api.EOBJECTNOTFOUND)

        query_release = {}
        if relationship_info_only:
            # release without removing related snapshots
            query_release['source_info_only'] = 'true'
        else:
            # release and removing all related snapshots
            query_release['source_only'] = 'true'

        snapmirror_uuid = records[0].get('uuid')
        self.send_request('/snapmirror/relationships/' +
                          snapmirror_uuid, 'delete',
                          query=query_release)

    def resync_snapmirror(self, source_vserver, source_volume,
                          destination_vserver, destination_volume):
        """Resync a SnapMirror relationship."""

        # We reuse the resume operation for resync since both are handled in
        # the same way in the REST API, by setting the snapmirror relationship
        # to the snapmirrored state.
        self.resume_snapmirror(source_vserver,
                               source_volume,
                               destination_vserver,
                               destination_volume)

    def quiesce_snapmirror(self, source_vserver, source_volume,
                           destination_vserver, destination_volume):
        """Disables future transfers to a SnapMirror destination."""

        return self._set_snapmirror_state(
            'paused', source_vserver, source_volume,
            destination_vserver, destination_volume)

    def break_snapmirror(self, source_vserver, source_volume,
                         destination_vserver, destination_volume):
        """Breaks a data protection SnapMirror relationship."""

        interval = 2
        retries = (10 / interval)

        @utils.retry(netapp_api.NaRetryableError, interval=interval,
                     retries=retries, backoff_rate=1)
        def _waiter():
            snapmirror = self.get_snapmirrors(
                source_vserver=source_vserver,
                source_volume=source_volume,
                destination_vserver=destination_vserver,
                destination_volume=destination_volume)

            snapmirror_state = None
            if snapmirror:
                snapmirror_state = snapmirror[0].get('transferring-state')

            if snapmirror_state == 'success':
                uuid = snapmirror[0]['uuid']
                body = {'state': 'broken_off'}
                self.send_request(f'/snapmirror/relationships/{uuid}', 'patch',
                                  body=body)
                return
            else:
                message = 'Waiting for transfer state to be SUCCESS.'
                code = ''
                raise netapp_api.NaRetryableError(message=message, code=code)

        try:
            return _waiter()
        except netapp_api.NaRetryableError:
            msg = _("Transfer state did not reach the expected state. Retries "
                    "exhausted. Aborting.")
            raise na_utils.NetAppDriverException(msg)

    def update_snapmirror(self, source_vserver, source_volume,
                          destination_vserver, destination_volume):
        """Schedules a SnapMirror update."""

        snapmirror = self.get_snapmirrors(source_vserver, source_volume,
                                          destination_vserver,
                                          destination_volume)
        if not snapmirror:
            msg = _('Failed to get information about relationship between '
                    'source %(src_vserver)s:%(src_volume)s and '
                    'destination %(dst_vserver)s:%(dst_volume)s.') % {
                'src_vserver': source_vserver,
                'src_volume': source_volume,
                'dst_vserver': destination_vserver,
                'dst_volume': destination_volume}

            raise na_utils.NetAppDriverException(msg)

        snapmirror_uuid = snapmirror[0]['uuid']

        # NOTE(nahimsouza): A POST with an empty body starts the update
        # snapmirror operation.
        try:
            self.send_request('/snapmirror/relationships/' +
                              snapmirror_uuid + '/transfers/', 'post',
                              wait_on_accepted=False)
        except netapp_api.NaApiError as e:
            if (e.code != netapp_api.REST_UPDATE_SNAPMIRROR_FAILED):
                LOG.warning('Unexpected failure during snapmirror update.'
                            'Code: %(code)s, Message: %(message)s',
                            {'code': e.code, 'message': e.message})
            raise

    def mount_flexvol(self, flexvol_name, junction_path=None):
        """Mounts a volume on a junction path."""
        query = {'name': flexvol_name}
        body = {'nas.path': (
            junction_path if junction_path else '/%s' % flexvol_name)}
        self.send_request('/storage/volumes', 'patch', query=query, body=body)

    def get_cluster_name(self):
        """Gets cluster name."""
        query = {'fields': 'name'}

        response = self.send_request('/cluster', 'get', query=query,
                                     enable_tunneling=False)

        return response['name']

    def get_vserver_peers(self, vserver_name=None, peer_vserver_name=None):
        """Gets one or more Vserver peer relationships."""
        query = {
            'fields': 'svm.name,state,peer.svm.name,peer.cluster.name,'
                      'applications'
        }

        if peer_vserver_name:
            query['name'] = peer_vserver_name
        if vserver_name:
            query['svm.name'] = vserver_name

        response = self.send_request('/svm/peers', 'get', query=query,
                                     enable_tunneling=False)
        records = response.get('records', [])

        vserver_peers = []
        for vserver_info in records:
            vserver_peer = {
                'vserver': vserver_info['svm']['name'],
                'peer-vserver': vserver_info['peer']['svm']['name'],
                'peer-state': vserver_info['state'],
                'peer-cluster': vserver_info['peer']['cluster']['name'],
                'applications': vserver_info['applications'],
            }
            vserver_peers.append(vserver_peer)

        return vserver_peers

    def create_vserver_peer(self, vserver_name, peer_vserver_name,
                            vserver_peer_application=None):
        """Creates a Vserver peer relationship."""
        # default peering application to `snapmirror` if none is specified.
        if not vserver_peer_application:
            vserver_peer_application = ['snapmirror']

        body = {
            'svm.name': vserver_name,
            'name': peer_vserver_name,
            'applications': vserver_peer_application
        }

        self.send_request('/svm/peers', 'post', body=body,
                          enable_tunneling=False)

    def start_lun_move(self, lun_name, dest_ontap_volume,
                       src_ontap_volume=None, dest_lun_name=None):
        """Starts a lun move operation between ONTAP volumes."""
        if dest_lun_name is None:
            dest_lun_name = lun_name
        if src_ontap_volume is None:
            src_ontap_volume = dest_ontap_volume

        src_path = f'/vol/{src_ontap_volume}/{lun_name}'
        dest_path = f'/vol/{dest_ontap_volume}/{dest_lun_name}'
        body = {'name': dest_path}
        self._lun_update_by_path(src_path, body)

        return dest_path

    def get_lun_move_status(self, dest_path):
        """Get lun move job status from a given dest_path."""
        lun = self._get_first_lun_by_path(
            dest_path, fields='movement.progress')

        if not lun:
            return None

        move_progress = lun['movement']['progress']
        move_status = {
            'job-status': move_progress['state'],
            'last-failure-reason': (move_progress
                                    .get('failure', {})
                                    .get('message', None))
        }

        return move_status

    def start_lun_copy(self, lun_name, dest_ontap_volume, dest_vserver,
                       src_ontap_volume=None, src_vserver=None,
                       dest_lun_name=None):
        """Starts a lun copy operation between ONTAP volumes."""
        if src_ontap_volume is None:
            src_ontap_volume = dest_ontap_volume
        if src_vserver is None:
            src_vserver = dest_vserver
        if dest_lun_name is None:
            dest_lun_name = lun_name

        src_path = f'/vol/{src_ontap_volume}/{lun_name}'
        dest_path = f'/vol/{dest_ontap_volume}/{dest_lun_name}'

        body = {
            'name': dest_path,
            'copy.source.name': src_path,
            'svm.name': dest_vserver
        }

        self.send_request('/storage/luns', 'post', body=body,
                          enable_tunneling=False)

        return dest_path

    def get_lun_copy_status(self, dest_path):
        """Get lun copy job status from a given dest_path."""
        lun = self._get_first_lun_by_path(
            dest_path, fields='copy.source.progress')

        if not lun:
            return None

        copy_progress = lun['copy']['source']['progress']
        copy_status = {
            'job-status': copy_progress['state'],
            'last-failure-reason': (copy_progress
                                    .get('failure', {})
                                    .get('message', None))
        }

        return copy_status

    def cancel_lun_copy(self, dest_path):
        """Cancel an in-progress lun copy by deleting the lun."""
        query = {
            'name': dest_path,
            'svm.name': self.vserver
        }

        try:
            self.send_request('/storage/luns/', 'delete', query=query)
        except netapp_api.NaApiError as e:
            msg = (_('Could not cancel lun copy by deleting lun at %s. %s'))
            raise na_utils.NetAppDriverException(msg % (dest_path, e))

    def start_file_copy(self, file_name, dest_ontap_volume,
                        src_ontap_volume=None,
                        dest_file_name=None):
        """Starts a file copy operation between ONTAP volumes."""
        if src_ontap_volume is None:
            src_ontap_volume = dest_ontap_volume
        if dest_file_name is None:
            dest_file_name = file_name

        source_vol = self._get_volume_by_args(src_ontap_volume)

        dest_vol = source_vol
        if dest_ontap_volume != src_ontap_volume:
            dest_vol = self._get_volume_by_args(dest_ontap_volume)

        body = {
            'files_to_copy': [
                {
                    'source': {
                        'path': f'{src_ontap_volume}/{file_name}',
                        'volume': {
                            'uuid': source_vol['uuid']
                        }
                    },
                    'destination': {
                        'path': f'{dest_ontap_volume}/{dest_file_name}',
                        'volume': {
                            'uuid': dest_vol['uuid']
                        }
                    }
                }
            ]
        }

        result = self.send_request('/storage/file/copy', 'post', body=body,
                                   enable_tunneling=False)
        return result['job']['uuid']

    def get_file_copy_status(self, job_uuid):
        """Get file copy job status from a given job's UUID."""
        # TODO(rfluisa): Select only the fields that are needed here.
        query = {}
        query['fields'] = '*'

        result = self.send_request(
            f'/cluster/jobs/{job_uuid}', 'get', query=query,
            enable_tunneling=False)

        if not result or not result.get('state', None):
            return None

        state = result.get('state')
        if state == 'success':
            state = 'complete'
        elif state == 'failure':
            state = 'destroyed'

        copy_status = {
            'job-status': state,
            'last-failure-reason': result.get('error', {}).get('message', None)
        }

        return copy_status

    def rename_file(self, orig_file_name, new_file_name):
        """Rename a volume file."""
        LOG.debug("Renaming the file %(original)s to %(new)s.",
                  {'original': orig_file_name, 'new': new_file_name})

        unique_volume = self._get_volume_by_args(
            vol_name=orig_file_name.split('/')[2])

        # Get the relative path
        orig_file_name = '/'.join(orig_file_name.split('/')[3:])
        new_file_name = '/'.join(new_file_name.split('/')[3:])

        # Path requires "%2E" to represent "." and "%2F" to represent "/".
        orig_file_name = orig_file_name.replace('.', '%2E').replace('/', '%2F')
        new_file_name = new_file_name.replace('.', '%2E').replace('/', '%2F')

        body = {'path': new_file_name}

        self.send_request(
            f'/storage/volumes/{unique_volume["uuid"]}/files/{orig_file_name}',
            'patch', body=body)

    def get_namespace_list(self):
        """Gets the list of namespaces on filer.

        Gets the namespaces from cluster with vserver.
        """

        query = {
            'svm.name': self.vserver,
            'fields': 'svm.name,location.volume.name,space.size,'
                      'location.qtree.name,name,os_type,'
                      'space.guarantee.requested,uuid'
        }

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

        namespace_list = []
        for namespace in response.get('records', []):
            namespace_info = {}
            namespace_info['Vserver'] = namespace['svm']['name']
            namespace_info['Volume'] = namespace['location']['volume']['name']
            namespace_info['Size'] = namespace['space']['size']
            namespace_info['Qtree'] = (
                namespace['location'].get('qtree', {}).get('name', ''))
            namespace_info['Path'] = namespace['name']
            namespace_info['OsType'] = namespace['os_type']
            namespace_info['SpaceReserved'] = (
                namespace['space']['guarantee']['requested'])
            namespace_info['UUID'] = namespace['uuid']

            namespace_list.append(namespace_info)

        return namespace_list

    def create_namespace(self, volume_name, namespace_name, size, metadata):
        """Issues API request for creating namespace on volume."""

        path = f'/vol/{volume_name}/{namespace_name}'
        initial_size = size

        body = {
            'name': path,
            'space.size': str(initial_size),
            'os_type': metadata['OsType'],
        }

        try:
            self.send_request('/storage/namespaces', 'post', body=body)
        except netapp_api.NaApiError as ex:
            with excutils.save_and_reraise_exception():
                LOG.error('Error provisioning volume %(namespace_name)s on '
                          '%(volume_name)s. Details: %(ex)s',
                          {
                              'namespace_name': namespace_name,
                              'volume_name': volume_name,
                              'ex': ex,
                          })

    def destroy_namespace(self, path, force=True):
        """Destroys the namespace at the path."""
        query = {
            'name': path,
            'svm': self.vserver
        }

        if force:
            query['allow_delete_while_mapped'] = 'true'

        self.send_request('/storage/namespaces', 'delete', query=query)

    def clone_namespace(self, volume, name, new_name):
        """Clones namespace on vserver."""
        LOG.debug('Cloning namespace - volume: %(volume)s, name: %(name)s, '
                  'new_name: %(new_name)s',
                  {
                      'volume': volume,
                      'name': name,
                      'new_name': new_name,
                  })

        source_path = f'/vol/{volume}/{name}'
        body = {
            'svm': {
                'name': self.vserver
            },
            'name': f'/vol/{volume}/{new_name}',
            'clone': {
                'source': {
                    'name': source_path,
                }
            }
        }
        self.send_request('/storage/namespaces', 'post', body=body)

    def get_namespace_by_args(self, **namespace_info_args):
        """Retrieves namespace with specified args."""

        query = {
            'fields': 'svm.name,location.volume.name,space.size,'
                      'location.qtree.name,name,os_type,'
                      'space.guarantee.requested,uuid,space.block_size'
        }

        if namespace_info_args:
            if 'vserver' in namespace_info_args:
                query['svm.name'] = namespace_info_args['vserver']
            if 'path' in namespace_info_args:
                query['name'] = namespace_info_args['path']
            if 'uuid' in namespace_info_args:
                query['uuid'] = namespace_info_args['uuid']

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

        namespace_list = []
        for namespace in response.get('records', []):
            namespace_info = {}
            namespace_info['Vserver'] = namespace['svm']['name']
            namespace_info['Volume'] = namespace['location']['volume']['name']
            namespace_info['Size'] = namespace['space']['size']
            namespace_info['Qtree'] = (
                namespace['location'].get('qtree', {}).get('name', ''))
            namespace_info['Path'] = namespace['name']
            namespace_info['OsType'] = namespace['os_type']
            namespace_info['SpaceReserved'] = (
                namespace['space']['guarantee']['requested'])
            namespace_info['UUID'] = namespace['uuid']
            namespace_info['BlockSize'] = namespace['space']['block_size']

            namespace_list.append(namespace_info)

        return namespace_list

    def namespace_resize(self, path, new_size_bytes):
        """Resize the namespace."""
        seg = path.split("/")
        LOG.info('Resizing namespace %s to new size.', seg[-1])

        body = {'space.size': new_size_bytes}
        query = {'name': path}
        self.send_request('/storage/namespaces', 'patch', body=body,
                          query=query)

    def get_namespace_sizes_by_volume(self, volume_name):
        """"Gets the list of namespace and their sizes from a given volume."""

        query = {
            'location.volume.name': volume_name,
            'svm.name': self.vserver,
            'fields': 'space.size,name'
        }
        response = self.send_request('/storage/namespaces', 'get', query=query)

        namespaces = []
        for namespace_info in response.get('records', []):
            namespaces.append({
                'path': namespace_info.get('name', ''),
                'size': float(namespace_info.get('space', {}).get('size', 0))
            })

        return namespaces

    def get_subsystem_by_host(self, host_nqn):
        """Get subsystem exactly matching the initiator host."""
        query = {
            'svm.name': self.vserver,
            'hosts.nqn': host_nqn,
            'fields': 'name,os_type',
            'name': f'{na_utils.OPENSTACK_PREFIX}*',
        }
        response = self.send_request('/protocols/nvme/subsystems', 'get',
                                     query=query)

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

        return [{'name': subsystem['name'], 'os_type': subsystem['os_type']}
                for subsystem in records]

    def get_subsystem_by_path(self, path):
        """Get subsystem by its namespace path."""
        query = {
            'svm.name': self.vserver,
            'subsystem_maps.namespace.name': path,
            'fields': 'name,os_type',
            'name': f'{na_utils.OPENSTACK_PREFIX}*',
        }
        response = self.send_request('/protocols/nvme/subsystems', 'get',
                                     query=query)

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

        return [{'name': subsystem['name'], 'os_type': subsystem['os_type']}
                for subsystem in records]

    def create_subsystem(self, subsystem_name, os_type, host_nqn):
        """Creates subsystem with specified args."""
        body = {
            'svm.name': self.vserver,
            'name': subsystem_name,
            'os_type': os_type,
            'hosts': [{'nqn': host_nqn}]
        }
        self.send_request('/protocols/nvme/subsystems', 'post', body=body)

    def get_namespace_map(self, path):
        """Gets the namespace map using its path."""
        query = {
            'namespace.name': path,
            'fields': 'subsystem.name,namespace.uuid,svm.name,subsystem.uuid',
        }
        response = self.send_request('/protocols/nvme/subsystem-maps',
                                     'get',
                                     query=query)

        records = response.get('records', [])
        map_list = []
        for map in records:
            map_subsystem = {}
            map_subsystem['subsystem'] = map['subsystem']['name']
            map_subsystem['subsystem_uuid'] = map['subsystem']['uuid']
            map_subsystem['uuid'] = map['namespace']['uuid']
            map_subsystem['vserver'] = map['svm']['name']

            map_list.append(map_subsystem)

        return map_list

    def map_namespace(self, path, subsystem_name):
        """Maps namespace to the host nqn and returns namespace uuid."""

        body_post = {
            'namespace.name': path,
            'subsystem.name': subsystem_name
        }
        try:
            result = self.send_request('/protocols/nvme/subsystem-maps',
                                       'post',
                                       body=body_post,
                                       query={'return_records': 'true'})
            records = result.get('records')
            namespace_uuid = records[0]['namespace']['uuid']
            return namespace_uuid
        except netapp_api.NaApiError as e:
            code = e.code
            message = e.message
            LOG.warning('Error mapping namespace. Code :%(code)s, Message: '
                        '%(message)s', {'code': code, 'message': message})
            raise

    def get_nvme_subsystem_nqn(self, subsystem):
        """Returns target subsystem nqn."""
        query = {
            'fields': 'target_nqn',
            'name': subsystem,
            'svm.name': self.vserver
        }
        response = self.send_request(
            '/protocols/nvme/subsystems', 'get', query=query)

        records = response.get('records', [])
        if records:
            return records[0]['target_nqn']

        LOG.debug('No %(subsystem)s NVMe subsystem found for vserver '
                  '%(vserver)s',
                  {'subsystem': subsystem, 'vserver': self.vserver})
        return None

    def get_nvme_target_portals(self):
        """Gets the NVMe target portal details."""
        query = {
            'services': 'data_nvme_tcp',
            'fields': 'ip.address',
            'enabled': 'true',
        }

        response = self.send_request('/network/ip/interfaces', 'get',
                                     query=query)

        interfaces = response.get('records', [])
        return [record['ip']['address'] for record in interfaces]

    def unmap_namespace(self, path, subsystem):
        """Unmaps a namespace from given subsystem."""

        query = {
            'subsystem.name': subsystem,
            'namespace.name': path
        }
        self.send_request('/protocols/nvme/subsystem-maps', 'delete',
                          query=query)

    def unmap_host_with_subsystem(self, host_nqn, subsystem_uuid):
        """Unmaps a host from given subsystem.

        In multiattach and live migration scenarios,it is possible that the
        host is attached to single namespace from different subsystems and
        repeated unmapping to subsystem to host is possible. Errors are
        logged but not propagated. Calling code will proceed even if
        unmapping fails.
        """
        url = f'/protocols/nvme/subsystems/{subsystem_uuid}/hosts/{host_nqn}'
        try:
            self.send_request(url, 'delete')
        except netapp_api.NaApiError as e:
            LOG.warning(
                "Failed to unmap host from subsystem. "
                "Host NQN: %(host_nqn)s, Subsystem UUID: %(subsystem_uuid)s, "
                "Error Code: %(code)s, Error Message: %(message)s",
                {'host_nqn': host_nqn, 'subsystem_uuid': subsystem_uuid,
                 'code': e.code, 'message': e.message}
            )

    def map_host_with_subsystem(self, host_nqn, subsystem_uuid):
        """Add host nqn to the subsystem"""

        body_post = {
            'nqn': host_nqn,
        }
        try:
            self.send_request(
                f'/protocols/nvme/subsystems/{subsystem_uuid}/hosts',
                'post',
                body=body_post
            )
        except netapp_api.NaApiError as e:
            code = e.code
            message = e.message
            if e.code == netapp_api.REST_HOST_ALREADY_MAPPED_TO_SUBSYSTEM:
                LOG.info(
                    'Host %(host_nqn)s is already mapped to subsystem '
                    '%(subsystem_uuid)s ', {'host_nqn': host_nqn,
                                            'subsystem_uuid': subsystem_uuid
                                            }
                )
            else:
                LOG.error(
                    'Error mapping host to subsystem. Code :'
                    '%(code)s, Message: %(message)s',
                    {'code': code, 'message': message}
                )
                raise
