# Copyright (c) 2014 Alex Meade.  All rights reserved.
# Copyright (c) 2014 Clinton Knight.  All rights reserved.
# Copyright (c) 2015 Tom Barron.  All rights reserved.
# Copyright (c) 2016 Mike Rooney. All rights reserved.
# Copyright (c) 2017 Jose Porrua. 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
import math
import re

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

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_base
from cinder.volume.drivers.netapp import utils as na_utils

from oslo_utils import strutils


LOG = logging.getLogger(__name__)
DEFAULT_MAX_PAGE_LENGTH = 50


@six.add_metaclass(utils.TraceWrapperMetaclass)
class Client(client_base.Client):

    def __init__(self, **kwargs):
        super(Client, self).__init__(**kwargs)
        self.vserver = kwargs.get('vserver', None)
        self.connection.set_vserver(self.vserver)

        # Default values to run first api
        self.connection.set_api_version(1, 15)
        (major, minor) = self.get_ontapi_version(cached=False)
        self.connection.set_api_version(major, minor)
        self._init_features()

    def _init_features(self):
        super(Client, self)._init_features()

        ontapi_version = self.get_ontapi_version()   # major, minor

        ontapi_1_20 = ontapi_version >= (1, 20)
        ontapi_1_2x = (1, 20) <= ontapi_version < (1, 30)
        ontapi_1_30 = ontapi_version >= (1, 30)
        ontapi_1_100 = ontapi_version >= (1, 100)
        ontapi_1_1xx = (1, 100) <= ontapi_version < (1, 200)

        self.features.add_feature('SNAPMIRROR_V2', supported=ontapi_1_20)
        self.features.add_feature('USER_CAPABILITY_LIST',
                                  supported=ontapi_1_20)
        self.features.add_feature('SYSTEM_METRICS', supported=ontapi_1_2x)
        self.features.add_feature('CLONE_SPLIT_STATUS', supported=ontapi_1_30)
        self.features.add_feature('FAST_CLONE_DELETE', supported=ontapi_1_30)
        self.features.add_feature('SYSTEM_CONSTITUENT_METRICS',
                                  supported=ontapi_1_30)
        self.features.add_feature('ADVANCED_DISK_PARTITIONING',
                                  supported=ontapi_1_30)
        self.features.add_feature('BACKUP_CLONE_PARAM', supported=ontapi_1_100)
        self.features.add_feature('CLUSTER_PEER_POLICY', supported=ontapi_1_30)
        self.features.add_feature('FLEXVOL_ENCRYPTION', supported=ontapi_1_1xx)

        LOG.info('Reported ONTAPI Version: %(major)s.%(minor)s',
                 {'major': ontapi_version[0], 'minor': ontapi_version[1]})

    def _invoke_vserver_api(self, na_element, vserver):
        server = copy.copy(self.connection)
        server.set_vserver(vserver)
        result = server.invoke_successfully(na_element, True)
        return result

    def _has_records(self, api_result_element):
        num_records = api_result_element.get_child_content('num-records')
        return bool(num_records and '0' != num_records)

    def _get_record_count(self, api_result_element):
        try:
            return int(api_result_element.get_child_content('num-records'))
        except TypeError:
            msg = _('Missing record count for NetApp iterator API invocation.')
            raise exception.NetAppDriverException(msg)

    def set_vserver(self, vserver):
        self.vserver = vserver
        self.connection.set_vserver(vserver)

    def send_iter_request(self, api_name, api_args=None, enable_tunneling=True,
                          max_page_length=DEFAULT_MAX_PAGE_LENGTH):
        """Invoke an iterator-style getter API."""

        if not api_args:
            api_args = {}

        api_args['max-records'] = max_page_length

        # Get first page
        result = self.send_request(
            api_name, api_args, enable_tunneling=enable_tunneling)

        # Most commonly, we can just return here if there is no more data
        next_tag = result.get_child_content('next-tag')
        if not next_tag:
            return result

        # Ensure pagination data is valid and prepare to store remaining pages
        num_records = self._get_record_count(result)
        attributes_list = result.get_child_by_name('attributes-list')
        if not attributes_list:
            msg = _('Missing attributes list for API %s.') % api_name
            raise exception.NetAppDriverException(msg)

        # Get remaining pages, saving data into first page
        while next_tag is not None:
            next_api_args = copy.deepcopy(api_args)
            next_api_args['tag'] = next_tag
            next_result = self.send_request(
                api_name, next_api_args, enable_tunneling=enable_tunneling)

            next_attributes_list = next_result.get_child_by_name(
                'attributes-list') or netapp_api.NaElement('none')

            for record in next_attributes_list.get_children():
                attributes_list.add_child_elem(record)

            num_records += self._get_record_count(next_result)
            next_tag = next_result.get_child_content('next-tag')

        result.get_child_by_name('num-records').set_content(
            six.text_type(num_records))
        result.get_child_by_name('next-tag').set_content('')
        return result

    def list_vservers(self, vserver_type='data'):
        """Get the names of vservers present, optionally filtered by type."""
        query = {
            'vserver-info': {
                'vserver-type': vserver_type,
            }
        } if vserver_type else None

        api_args = {
            'desired-attributes': {
                'vserver-info': {
                    'vserver-name': None,
                },
            },
        }
        if query:
            api_args['query'] = query

        result = self.send_iter_request('vserver-get-iter', api_args,
                                        enable_tunneling=False)
        vserver_info_list = result.get_child_by_name(
            'attributes-list') or netapp_api.NaElement('none')
        return [vserver_info.get_child_content('vserver-name')
                for vserver_info in vserver_info_list.get_children()]

    def _get_ems_log_destination_vserver(self):
        """Returns the best vserver destination for EMS messages."""
        major, minor = self.get_ontapi_version(cached=True)

        if (major > 1) or (major == 1 and minor > 15):
            # Prefer admin Vserver (requires cluster credentials).
            admin_vservers = self.list_vservers(vserver_type='admin')
            if admin_vservers:
                return admin_vservers[0]

            # Fall back to data Vserver.
            data_vservers = self.list_vservers(vserver_type='data')
            if data_vservers:
                return data_vservers[0]

        # If older API version, or no other Vservers found, use node Vserver.
        node_vservers = self.list_vservers(vserver_type='node')
        if node_vservers:
            return node_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."""

        # NOTE(cknight): Cannot use deepcopy on the connection context
        node_client = copy.copy(self)
        node_client.connection = copy.copy(self.connection)
        node_client.connection.set_timeout(25)

        try:
            node_client.set_vserver(self._get_ems_log_destination_vserver())
            node_client.send_request('ems-autosupport-log', message_dict)
            LOG.debug('EMS executed successfully.')
        except netapp_api.NaApiError as e:
            LOG.warning('Failed to invoke EMS. %s', e)

    def get_iscsi_target_details(self):
        """Gets the iSCSI target portal details."""
        iscsi_if_iter = netapp_api.NaElement('iscsi-interface-get-iter')
        result = self.connection.invoke_successfully(iscsi_if_iter, True)
        tgt_list = []
        num_records = result.get_child_content('num-records')
        if num_records and int(num_records) >= 1:
            attr_list = result.get_child_by_name('attributes-list')
            iscsi_if_list = attr_list.get_children()
            for iscsi_if in iscsi_if_list:
                d = dict()
                d['address'] = iscsi_if.get_child_content('ip-address')
                d['port'] = iscsi_if.get_child_content('ip-port')
                d['tpgroup-tag'] = iscsi_if.get_child_content('tpgroup-tag')
                d['interface-enabled'] = iscsi_if.get_child_content(
                    'is-interface-enabled')
                tgt_list.append(d)
        return tgt_list

    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 check_iscsi_initiator_exists(self, iqn):
        """Returns True if initiator exists."""
        initiator_exists = True
        try:
            auth_list = netapp_api.NaElement('iscsi-initiator-get-auth')
            auth_list.add_new_child('initiator', iqn)
            self.connection.invoke_successfully(auth_list, True)
        except netapp_api.NaApiError:
            initiator_exists = False

        return initiator_exists

    def get_fc_target_wwpns(self):
        """Gets the FC target details."""
        wwpns = []
        port_name_list_api = netapp_api.NaElement('fcp-port-name-get-iter')
        port_name_list_api.add_new_child('max-records', '100')
        result = self.connection.invoke_successfully(port_name_list_api, True)
        num_records = result.get_child_content('num-records')
        if num_records and int(num_records) >= 1:
            for port_name_info in result.get_child_by_name(
                    'attributes-list').get_children():

                if port_name_info.get_child_content('is-used') != 'true':
                    continue

                wwpn = port_name_info.get_child_content('port-name').lower()
                wwpns.append(wwpn)

        return wwpns

    def get_iscsi_service_details(self):
        """Returns iscsi iqn."""
        iscsi_service_iter = netapp_api.NaElement('iscsi-service-get-iter')
        result = self.connection.invoke_successfully(iscsi_service_iter, True)
        if result.get_child_content('num-records') and\
                int(result.get_child_content('num-records')) >= 1:
            attr_list = result.get_child_by_name('attributes-list')
            iscsi_service = attr_list.get_child_by_name('iscsi-service-info')
            return iscsi_service.get_child_content('node-name')
        LOG.debug('No iSCSI service found for vserver %s', self.vserver)
        return None

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

        Gets the LUNs from cluster with vserver.
        """

        luns = []
        tag = None
        while True:
            api = netapp_api.NaElement('lun-get-iter')
            api.add_new_child('max-records', '100')
            if tag:
                api.add_new_child('tag', tag, True)
            lun_info = netapp_api.NaElement('lun-info')
            lun_info.add_new_child('vserver', self.vserver)
            query = netapp_api.NaElement('query')
            query.add_child_elem(lun_info)
            api.add_child_elem(query)
            result = self.connection.invoke_successfully(api, True)
            if result.get_child_by_name('num-records') and\
                    int(result.get_child_content('num-records')) >= 1:
                attr_list = result.get_child_by_name('attributes-list')
                luns.extend(attr_list.get_children())
            tag = result.get_child_content('next-tag')
            if tag is None:
                break
        return luns

    def get_lun_map(self, path):
        """Gets the LUN map by LUN path."""
        tag = None
        map_list = []
        while True:
            lun_map_iter = netapp_api.NaElement('lun-map-get-iter')
            lun_map_iter.add_new_child('max-records', '100')
            if tag:
                lun_map_iter.add_new_child('tag', tag, True)
            query = netapp_api.NaElement('query')
            lun_map_iter.add_child_elem(query)
            query.add_node_with_children('lun-map-info', **{'path': path})
            result = self.connection.invoke_successfully(lun_map_iter, True)
            tag = result.get_child_content('next-tag')
            if result.get_child_content('num-records') and \
                    int(result.get_child_content('num-records')) >= 1:
                attr_list = result.get_child_by_name('attributes-list')
                lun_maps = attr_list.get_children()
                for lun_map in lun_maps:
                    lun_m = dict()
                    lun_m['initiator-group'] = lun_map.get_child_content(
                        'initiator-group')
                    lun_m['lun-id'] = lun_map.get_child_content('lun-id')
                    lun_m['vserver'] = lun_map.get_child_content('vserver')
                    map_list.append(lun_m)
            if tag is None:
                break
        return map_list

    def _get_igroup_by_initiator_query(self, initiator, tag):
        igroup_get_iter = netapp_api.NaElement('igroup-get-iter')
        igroup_get_iter.add_new_child('max-records', '100')
        if tag:
            igroup_get_iter.add_new_child('tag', tag, True)

        query = netapp_api.NaElement('query')
        igroup_info = netapp_api.NaElement('initiator-group-info')
        query.add_child_elem(igroup_info)
        igroup_info.add_new_child('vserver', self.vserver)
        initiators = netapp_api.NaElement('initiators')
        igroup_info.add_child_elem(initiators)
        igroup_get_iter.add_child_elem(query)
        initiators.add_node_with_children(
            'initiator-info', **{'initiator-name': initiator})

        # limit results to just the attributes of interest
        desired_attrs = netapp_api.NaElement('desired-attributes')
        desired_igroup_info = netapp_api.NaElement('initiator-group-info')
        desired_igroup_info.add_node_with_children(
            'initiators', **{'initiator-info': None})
        desired_igroup_info.add_new_child('vserver', None)
        desired_igroup_info.add_new_child('initiator-group-name', None)
        desired_igroup_info.add_new_child('initiator-group-type', None)
        desired_igroup_info.add_new_child('initiator-group-os-type', None)
        desired_attrs.add_child_elem(desired_igroup_info)
        igroup_get_iter.add_child_elem(desired_attrs)

        return igroup_get_iter

    def get_igroup_by_initiators(self, initiator_list):
        """Get igroups exactly matching a set of initiators."""
        tag = None
        igroup_list = []
        if not initiator_list:
            return igroup_list

        initiator_set = set(initiator_list)

        while True:
            # C-mode getter APIs can't do an 'and' query, so match the first
            # initiator (which will greatly narrow the search results) and
            # filter the rest in this method.
            query = self._get_igroup_by_initiator_query(initiator_list[0], tag)
            result = self.connection.invoke_successfully(query, True)

            tag = result.get_child_content('next-tag')
            num_records = result.get_child_content('num-records')
            if num_records and int(num_records) >= 1:

                for igroup_info in result.get_child_by_name(
                        'attributes-list').get_children():

                    initiator_set_for_igroup = set()
                    for initiator_info in igroup_info.get_child_by_name(
                            'initiators').get_children():

                        initiator_set_for_igroup.add(
                            initiator_info.get_child_content('initiator-name'))

                    if initiator_set == initiator_set_for_igroup:
                        igroup = {'initiator-group-os-type':
                                  igroup_info.get_child_content(
                                      'initiator-group-os-type'),
                                  'initiator-group-type':
                                  igroup_info.get_child_content(
                                      'initiator-group-type'),
                                  'initiator-group-name':
                                  igroup_info.get_child_content(
                                      'initiator-group-name')}
                        igroup_list.append(igroup)

            if tag is None:
                break

        return igroup_list

    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):
        # zAPI can only handle 2^24 blocks per range
        bc_limit = 2 ** 24  # 8GB
        # zAPI can only handle 32 block ranges per call
        br_limit = 32
        z_limit = br_limit * bc_limit  # 256 GB
        z_calls = int(math.ceil(block_count / float(z_limit)))
        zbc = block_count
        if z_calls == 0:
            z_calls = 1
        for _call in range(0, z_calls):
            if zbc > z_limit:
                block_count = z_limit
                zbc -= z_limit
            else:
                block_count = zbc

            zapi_args = {
                'volume': volume,
                'source-path': name,
                'destination-path': new_name,
                'space-reserve': space_reserved,
            }
            if source_snapshot:
                zapi_args['snapshot-name'] = source_snapshot
            if is_snapshot and self.features.BACKUP_CLONE_PARAM:
                zapi_args['is-backup'] = 'true'
            clone_create = netapp_api.NaElement.create_node_with_children(
                'clone-create', **zapi_args)
            if qos_policy_group_name is not None:
                clone_create.add_new_child('qos-policy-group-name',
                                           qos_policy_group_name)
            if block_count > 0:
                block_ranges = netapp_api.NaElement("block-ranges")
                segments = int(math.ceil(block_count / float(bc_limit)))
                bc = block_count
                for _segment in range(0, segments):
                    if bc > bc_limit:
                        block_count = bc_limit
                        bc -= bc_limit
                    else:
                        block_count = bc
                    block_range =\
                        netapp_api.NaElement.create_node_with_children(
                            'block-range',
                            **{'source-block-number':
                               six.text_type(src_block),
                               'destination-block-number':
                               six.text_type(dest_block),
                               'block-count':
                               six.text_type(block_count)})
                    block_ranges.add_child_elem(block_range)
                    src_block += int(block_count)
                    dest_block += int(block_count)
                clone_create.add_child_elem(block_ranges)
            self.connection.invoke_successfully(clone_create, True)

    def get_lun_by_args(self, **args):
        """Retrieves LUN with specified args."""
        lun_iter = netapp_api.NaElement('lun-get-iter')
        lun_iter.add_new_child('max-records', '100')
        query = netapp_api.NaElement('query')
        lun_iter.add_child_elem(query)
        query.add_node_with_children('lun-info', **args)
        luns = self.connection.invoke_successfully(lun_iter, True)
        attr_list = luns.get_child_by_name('attributes-list')
        if not attr_list:
            return []
        return attr_list.get_children()

    def file_assign_qos(self, flex_vol, qos_policy_group_name, file_path):
        """Assigns the named QoS policy-group to a file."""
        api_args = {
            'volume': flex_vol,
            'qos-policy-group-name': qos_policy_group_name,
            'file': file_path,
            'vserver': self.vserver,
        }
        return self.send_request('file-assign-qos', api_args, False)

    def provision_qos_policy_group(self, qos_policy_group_info):
        """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 is not None:
            return

        spec = qos_policy_group_info.get('spec')
        if spec is not None:
            if not self.qos_policy_group_exists(spec['policy_name']):
                self.qos_policy_group_create(spec['policy_name'],
                                             spec['max_throughput'])
            else:
                self.qos_policy_group_modify(spec['policy_name'],
                                             spec['max_throughput'])

    def qos_policy_group_exists(self, qos_policy_group_name):
        """Checks if a QOS policy group exists."""
        api_args = {
            'query': {
                'qos-policy-group-info': {
                    'policy-group': qos_policy_group_name,
                },
            },
            'desired-attributes': {
                'qos-policy-group-info': {
                    'policy-group': None,
                },
            },
        }
        result = self.send_request('qos-policy-group-get-iter',
                                   api_args,
                                   False)
        return self._has_records(result)

    def qos_policy_group_create(self, qos_policy_group_name, max_throughput):
        """Creates a QOS policy group."""
        api_args = {
            'policy-group': qos_policy_group_name,
            'max-throughput': max_throughput,
            'vserver': self.vserver,
        }
        return self.send_request('qos-policy-group-create', api_args, False)

    def qos_policy_group_modify(self, qos_policy_group_name, max_throughput):
        """Modifies a QOS policy group."""
        api_args = {
            'policy-group': qos_policy_group_name,
            'max-throughput': max_throughput,
        }
        return self.send_request('qos-policy-group-modify', api_args, False)

    def qos_policy_group_delete(self, qos_policy_group_name):
        """Attempts to delete a QOS policy group."""
        api_args = {'policy-group': qos_policy_group_name}
        return self.send_request('qos-policy-group-delete', api_args, False)

    def qos_policy_group_rename(self, qos_policy_group_name, new_name):
        """Renames a QOS policy group."""
        api_args = {
            'policy-group-name': qos_policy_group_name,
            'new-name': new_name,
        }
        return self.send_request('qos-policy-group-rename', api_args, False)

    def mark_qos_policy_group_for_deletion(self, qos_policy_group_info):
        """Do (soft) delete of backing QOS policy group for 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 is not None:
            current_name = spec['policy_name']
            new_name = client_base.DELETED_PREFIX + current_name
            try:
                self.qos_policy_group_rename(current_name, new_name)
            except netapp_api.NaApiError as ex:
                LOG.warning('Rename failure in cleanup of cDOT QOS policy '
                            'group %(name)s: %(ex)s',
                            {'name': current_name, 'ex': ex})

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

    def remove_unused_qos_policy_groups(self):
        """Deletes all QOS policy groups that are marked for deletion."""
        api_args = {
            'query': {
                'qos-policy-group-info': {
                    'policy-group': '%s*' % client_base.DELETED_PREFIX,
                    'vserver': self.vserver,
                }
            },
            'max-records': 3500,
            'continue-on-failure': 'true',
            'return-success-list': 'false',
            'return-failure-list': 'false',
        }

        try:
            self.send_request('qos-policy-group-delete-iter', api_args, False)
        except netapp_api.NaApiError as ex:
            msg = 'Could not delete QOS policy groups. Details: %(ex)s'
            msg_args = {'ex': ex}
            LOG.debug(msg, msg_args)

    def set_lun_qos_policy_group(self, path, qos_policy_group):
        """Sets qos_policy_group on a LUN."""
        api_args = {
            'path': path,
            'qos-policy-group': qos_policy_group,
        }
        return self.send_request('lun-set-qos-policy-group', api_args)

    def get_if_info_by_ip(self, ip):
        """Gets the network interface info by ip."""
        net_if_iter = netapp_api.NaElement('net-interface-get-iter')
        net_if_iter.add_new_child('max-records', '10')
        query = netapp_api.NaElement('query')
        net_if_iter.add_child_elem(query)
        query.add_node_with_children(
            'net-interface-info',
            **{'address': na_utils.resolve_hostname(ip)})
        result = self.connection.invoke_successfully(net_if_iter, True)
        num_records = result.get_child_content('num-records')
        if num_records and int(num_records) >= 1:
            attr_list = result.get_child_by_name('attributes-list')
            return attr_list.get_children()
        raise exception.NotFound(
            _('No interface found on cluster for ip %s') % ip)

    def get_vol_by_junc_vserver(self, vserver, junction):
        """Gets the volume by junction path and vserver."""
        vol_iter = netapp_api.NaElement('volume-get-iter')
        vol_iter.add_new_child('max-records', '10')
        query = netapp_api.NaElement('query')
        vol_iter.add_child_elem(query)
        vol_attrs = netapp_api.NaElement('volume-attributes')
        query.add_child_elem(vol_attrs)
        vol_attrs.add_node_with_children(
            'volume-id-attributes',
            **{'junction-path': junction,
               'owning-vserver-name': vserver})
        des_attrs = netapp_api.NaElement('desired-attributes')
        des_attrs.add_node_with_children('volume-attributes',
                                         **{'volume-id-attributes': None})
        vol_iter.add_child_elem(des_attrs)
        result = self._invoke_vserver_api(vol_iter, vserver)
        num_records = result.get_child_content('num-records')
        if num_records and int(num_records) >= 1:
            attr_list = result.get_child_by_name('attributes-list')
            vols = attr_list.get_children()
            vol_id = vols[0].get_child_by_name('volume-id-attributes')
            return vol_id.get_child_content('name')
        msg_fmt = {'vserver': vserver, 'junction': junction}
        raise exception.NotFound(_("No volume on cluster with vserver "
                                   "%(vserver)s and junction path "
                                   "%(junction)s ") % msg_fmt)

    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 with params volume %(volume)s, src %(src_path)s, "
                  "dest %(dest_path)s, vserver %(vserver)s,"
                  "source_snapshot %(source_snapshot)s",
                  {'volume': flex_vol, 'src_path': src_path,
                   'dest_path': dest_path, 'vserver': vserver,
                   'source_snapshot': source_snapshot})
        zapi_args = {
            'volume': flex_vol,
            'source-path': src_path,
            'destination-path': dest_path,
        }
        if is_snapshot and self.features.BACKUP_CLONE_PARAM:
            zapi_args['is-backup'] = 'true'
        if source_snapshot:
            zapi_args['snapshot-name'] = source_snapshot
        clone_create = netapp_api.NaElement.create_node_with_children(
            'clone-create', **zapi_args)
        major, minor = self.connection.get_api_version()
        if major == 1 and minor >= 20 and dest_exists:
            clone_create.add_new_child('destination-exists', 'true')
        self._invoke_vserver_api(clone_create, vserver)

    def get_file_usage(self, path, vserver):
        """Gets the file unique bytes."""
        LOG.debug('Getting file usage for %s', path)
        file_use = netapp_api.NaElement.create_node_with_children(
            'file-usage-get', **{'path': path})
        res = self._invoke_vserver_api(file_use, vserver)
        unique_bytes = res.get_child_content('unique-bytes')
        LOG.debug('file-usage for path %(path)s is %(bytes)s',
                  {'path': path, 'bytes': unique_bytes})
        return unique_bytes

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

        Returns True if the specified cluster API exists and may be called by
        the current user. The API is *called* on Data ONTAP versions prior to
        8.2, while versions starting with 8.2 utilize an API designed for
        this purpose.
        """

        if not self.features.USER_CAPABILITY_LIST:
            return self._check_cluster_api_legacy(api)
        else:
            return self._check_cluster_api(object_name, operation_name, api)

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

        Returns True if the specified cluster API exists and may be called by
        the current user.  This method assumes Data ONTAP 8.2 or higher.
        """

        api_args = {
            'query': {
                'capability-info': {
                    'object-name': object_name,
                    'operation-list': {
                        'operation-info': {
                            'name': operation_name,
                        },
                    },
                },
            },
            'desired-attributes': {
                'capability-info': {
                    'operation-list': {
                        'operation-info': {
                            'api-name': None,
                        },
                    },
                },
            },
        }
        result = self.send_request(
            'system-user-capability-get-iter', api_args, False)

        if not self._has_records(result):
            return False

        capability_info_list = result.get_child_by_name(
            'attributes-list') or netapp_api.NaElement('none')

        for capability_info in capability_info_list.get_children():

            operation_list = capability_info.get_child_by_name(
                'operation-list') or netapp_api.NaElement('none')

            for operation_info in operation_list.get_children():
                api_name = operation_info.get_child_content('api-name') or ''
                api_names = api_name.split(',')
                if api in api_names:
                    return True

        return False

    def _check_cluster_api_legacy(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.  This method should only be used for Data ONTAP 8.1,
        and only getter APIs may be tested because the API is actually called
        to perform the check.
        """

        if not re.match(".*-get$|.*-get-iter$|.*-list-info$", api):
            raise ValueError(_('Non-getter API passed to API test method.'))

        try:
            self.send_request(api, enable_tunneling=False)
        except netapp_api.NaApiError as ex:
            if ex.code in (netapp_api.EAPIPRIVILEGE, netapp_api.EAPINOTFOUND):
                return False

        return True

    def list_cluster_nodes(self):
        """Get all available cluster nodes."""

        api_args = {
            'desired-attributes': {
                'node-details-info': {
                    'node': None,
                },
            },
        }
        result = self.send_iter_request('system-node-get-iter', api_args)
        nodes_info_list = result.get_child_by_name(
            'attributes-list') or netapp_api.NaElement('none')
        return [node_info.get_child_content('node') for node_info
                in nodes_info_list.get_children()]

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

        net_interface_get_iter_args = {
            'query': {
                'net-interface-info': {
                    'operational-status': 'up'
                }
            },
            'desired-attributes': {
                'net-interface-info': {
                    'address': None,
                }
            }
        }
        result = self.send_iter_request('net-interface-get-iter',
                                        net_interface_get_iter_args)

        lif_info_list = result.get_child_by_name(
            'attributes-list') or netapp_api.NaElement('none')

        return [lif_info.get_child_content('address') for lif_info in
                lif_info_list.get_children()]

    def get_flexvol_capacity(self, flexvol_path=None, flexvol_name=None):
        """Gets total capacity and free capacity, in bytes, of the flexvol."""

        volume_id_attributes = {}
        if flexvol_path:
            volume_id_attributes['junction-path'] = flexvol_path
        if flexvol_name:
            volume_id_attributes['name'] = flexvol_name

        api_args = {
            'query': {
                'volume-attributes': {
                    'volume-id-attributes': volume_id_attributes,
                }
            },
            'desired-attributes': {
                'volume-attributes': {
                    'volume-space-attributes': {
                        'size-available': None,
                        'size-total': None,
                    }
                }
            },
        }

        result = self.send_iter_request('volume-get-iter', api_args)
        if self._get_record_count(result) != 1:
            msg = _('Volume %s not found.')
            msg_args = flexvol_path or flexvol_name
            raise exception.NetAppDriverException(msg % msg_args)

        attributes_list = result.get_child_by_name('attributes-list')
        volume_attributes = attributes_list.get_child_by_name(
            'volume-attributes')
        volume_space_attributes = volume_attributes.get_child_by_name(
            'volume-space-attributes')

        size_available = float(
            volume_space_attributes.get_child_content('size-available'))
        size_total = float(
            volume_space_attributes.get_child_content('size-total'))

        return {
            'size-total': size_total,
            'size-available': size_available,
        }

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

        api_args = {
            'query': {
                'volume-attributes': {
                    'volume-id-attributes': {
                        'type': 'rw',
                        'style': 'flex',
                    },
                    'volume-state-attributes': {
                        'is-vserver-root': 'false',
                        'is-inconsistent': 'false',
                        'is-invalid': 'false',
                        'state': 'online',
                    },
                },
            },
            'desired-attributes': {
                'volume-attributes': {
                    'volume-id-attributes': {
                        'name': None,
                    },
                },
            },
        }
        result = self.send_iter_request('volume-get-iter', api_args)
        if not self._has_records(result):
            return []

        volumes = []

        attributes_list = result.get_child_by_name(
            'attributes-list') or netapp_api.NaElement('none')

        for volume_attributes in attributes_list.get_children():

            volume_id_attributes = volume_attributes.get_child_by_name(
                'volume-id-attributes') or netapp_api.NaElement('none')

            volumes.append(volume_id_attributes.get_child_content('name'))

        return volumes

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

        volume_id_attributes = {'type': 'rw', 'style': 'flex'}
        if flexvol_path:
            volume_id_attributes['junction-path'] = flexvol_path
        if flexvol_name:
            volume_id_attributes['name'] = flexvol_name

        api_args = {
            'query': {
                'volume-attributes': {
                    'volume-id-attributes': volume_id_attributes,
                    'volume-state-attributes': {
                        'is-vserver-root': 'false',
                        'is-inconsistent': 'false',
                        'is-invalid': 'false',
                        'state': 'online',
                    },
                },
            },
            'desired-attributes': {
                'volume-attributes': {
                    'volume-id-attributes': {
                        'name': None,
                        'owning-vserver-name': None,
                        'junction-path': None,
                        'containing-aggregate-name': None,
                        'type': None,
                    },
                    'volume-mirror-attributes': {
                        'is-data-protection-mirror': None,
                        'is-replica-volume': None,
                    },
                    'volume-space-attributes': {
                        'is-space-guarantee-enabled': None,
                        'space-guarantee': None,
                        'percentage-snapshot-reserve': None,
                        'size': None,
                    },
                    'volume-qos-attributes': {
                        'policy-group-name': None,
                    },
                    'volume-snapshot-attributes': {
                        'snapshot-policy': None,
                    },
                    'volume-language-attributes': {
                        'language-code': None,
                    },
                },
            },
        }
        result = self.send_iter_request('volume-get-iter', api_args)

        if self._get_record_count(result) != 1:
            msg = _('Could not find unique volume %(vol)s.')
            msg_args = {'vol': flexvol_name}
            raise exception.VolumeBackendAPIException(data=msg % msg_args)

        attributes_list = result.get_child_by_name(
            'attributes-list') or netapp_api.NaElement('none')

        volume_attributes = attributes_list.get_child_by_name(
            'volume-attributes') or netapp_api.NaElement('none')

        volume_id_attributes = volume_attributes.get_child_by_name(
            'volume-id-attributes') or netapp_api.NaElement('none')
        volume_space_attributes = volume_attributes.get_child_by_name(
            'volume-space-attributes') or netapp_api.NaElement('none')
        volume_qos_attributes = volume_attributes.get_child_by_name(
            'volume-qos-attributes') or netapp_api.NaElement('none')
        volume_snapshot_attributes = volume_attributes.get_child_by_name(
            'volume-snapshot-attributes') or netapp_api.NaElement('none')
        volume_language_attributes = volume_attributes.get_child_by_name(
            'volume-language-attributes') or netapp_api.NaElement('none')

        volume = {
            'name': volume_id_attributes.get_child_content('name'),
            'vserver': volume_id_attributes.get_child_content(
                'owning-vserver-name'),
            'junction-path': volume_id_attributes.get_child_content(
                'junction-path'),
            'aggregate': volume_id_attributes.get_child_content(
                'containing-aggregate-name'),
            'type': volume_id_attributes.get_child_content('type'),
            'space-guarantee-enabled': strutils.bool_from_string(
                volume_space_attributes.get_child_content(
                    'is-space-guarantee-enabled')),
            'space-guarantee': volume_space_attributes.get_child_content(
                'space-guarantee'),
            'percentage-snapshot-reserve': (
                volume_space_attributes.get_child_content(
                    'percentage-snapshot-reserve')),
            'size': volume_space_attributes.get_child_content('size'),
            'qos-policy-group': volume_qos_attributes.get_child_content(
                'policy-group-name'),
            'snapshot-policy': volume_snapshot_attributes.get_child_content(
                'snapshot-policy'),
            'language': volume_language_attributes.get_child_content(
                'language-code'),
        }

        return volume

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

        api_args = {
            'query': {
                'sis-status-info': {
                    'path': '/vol/%s' % flexvol_name,
                },
            },
            'desired-attributes': {
                'sis-status-info': {
                    'state': None,
                    'is-compression-enabled': None,
                    'logical-data-size': None,
                    'logical-data-limit': None,
                },
            },
        }

        no_dedupe_response = {
            'compression': False,
            'dedupe': False,
            'logical-data-size': 0,
            'logical-data-limit': 1,
        }

        try:
            result = self.send_iter_request('sis-get-iter', api_args)
        except netapp_api.NaApiError:
            LOG.exception('Failed to get dedupe info for volume %s.',
                          flexvol_name)
            return no_dedupe_response

        if self._get_record_count(result) != 1:
            return no_dedupe_response

        attributes_list = result.get_child_by_name(
            'attributes-list') or netapp_api.NaElement('none')

        sis_status_info = attributes_list.get_child_by_name(
            'sis-status-info') or netapp_api.NaElement('none')

        logical_data_size = sis_status_info.get_child_content(
            'logical-data-size') or 0
        logical_data_limit = sis_status_info.get_child_content(
            'logical-data-limit') or 1

        sis = {
            'compression': strutils.bool_from_string(
                sis_status_info.get_child_content('is-compression-enabled')),
            'dedupe': na_utils.to_bool(
                sis_status_info.get_child_content('state')),
            'logical-data-size': int(logical_data_size),
            'logical-data-limit': int(logical_data_limit),
        }

        return sis

    def get_flexvol_dedupe_used_percent(self, flexvol_name):
        """Determine how close a flexvol is to its shared block limit."""

        # Note(cknight): The value returned by this method is computed from
        # values returned by two different APIs, one of which was new in
        # Data ONTAP 8.3.
        if not self.features.CLONE_SPLIT_STATUS:
            return 0.0

        dedupe_info = self.get_flexvol_dedupe_info(flexvol_name)
        clone_split_info = self.get_clone_split_info(flexvol_name)

        total_dedupe_blocks = (dedupe_info.get('logical-data-size') +
                               clone_split_info.get('unsplit-size'))
        dedupe_used_percent = (100.0 * float(total_dedupe_blocks) /
                               dedupe_info.get('logical-data-limit'))
        return dedupe_used_percent

    def get_clone_split_info(self, flexvol_name):
        """Get the status of unsplit file/LUN clones in a flexvol."""

        try:
            result = self.send_request('clone-split-status',
                                       {'volume-name': flexvol_name})
        except netapp_api.NaApiError:
            LOG.exception('Failed to get clone split info for volume %s.',
                          flexvol_name)
            return {'unsplit-size': 0, 'unsplit-clone-count': 0}

        clone_split_info = result.get_child_by_name(
            'clone-split-info') or netapp_api.NaElement('none')

        unsplit_size = clone_split_info.get_child_content('unsplit-size') or 0
        unsplit_clone_count = clone_split_info.get_child_content(
            'unsplit-clone-count') or 0

        return {
            'unsplit-size': int(unsplit_size),
            'unsplit-clone-count': int(unsplit_clone_count),
        }

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

        api_args = {
            'query': {
                'snapmirror-info': {
                    'source-vserver': vserver_name,
                    'source-volume': flexvol_name,
                    'mirror-state': 'snapmirrored',
                    'relationship-type': 'data_protection',
                },
            },
            'desired-attributes': {
                'snapmirror-info': None,
            },
        }

        try:
            result = self.send_iter_request('snapmirror-get-iter', api_args)
        except netapp_api.NaApiError:
            LOG.exception('Failed to get SnapMirror info for volume %s.',
                          flexvol_name)
            return False

        if not self._has_records(result):
            return False

        return True

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

        if not self.features.FLEXVOL_ENCRYPTION:
            return False

        api_args = {
            'query': {
                'volume-attributes': {
                    'encrypt': 'true',
                    'volume-id-attributes': {
                        'name': flexvol_name,
                        'owning-vserver-name': vserver_name,
                    },
                },
            },
            'desired-attributes': {
                'volume-attributes': {
                    'encrypt': None,
                },
            },
        }

        try:
            result = self.send_iter_request('volume-get-iter', api_args)
        except netapp_api.NaApiError:
            LOG.exception('Failed to get Encryption info for volume %s.',
                          flexvol_name)
            return False

        if not self._has_records(result):
            return False

        return True

    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 volume."""
        api_args = {
            'containing-aggr-name': aggregate_name,
            'size': six.text_type(size_gb) + 'g',
            'volume': flexvol_name,
            'volume-type': volume_type,
        }
        if volume_type == 'dp':
            snapshot_policy = None
        else:
            api_args['junction-path'] = '/%s' % flexvol_name
        if snapshot_policy is not None:
            api_args['snapshot-policy'] = snapshot_policy
        if space_guarantee_type:
            api_args['space-reserve'] = space_guarantee_type
        if language is not None:
            api_args['language-code'] = language
        if snapshot_reserve is not None:
            api_args['percentage-snapshot-reserve'] = six.text_type(
                snapshot_reserve)
        self.send_request('volume-create', api_args)

        # cDOT compression requires that deduplication be enabled.
        if dedupe_enabled or compression_enabled:
            self.enable_flexvol_dedupe(flexvol_name)
        if compression_enabled:
            self.enable_flexvol_compression(flexvol_name)

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

        api_args = {
            'query': {
                'volume-attributes': {
                    'volume-id-attributes': {
                        'name': volume_name,
                    },
                },
            },
            'desired-attributes': {
                'volume-attributes': {
                    'volume-id-attributes': {
                        'name': None,
                    },
                },
            },
        }
        result = self.send_iter_request('volume-get-iter', api_args)
        return self._has_records(result)

    def rename_flexvol(self, orig_flexvol_name, new_flexvol_name):
        """Set flexvol name."""
        api_args = {
            'volume': orig_flexvol_name,
            'new-volume-name': new_flexvol_name,
        }
        self.send_request('volume-rename', api_args)

    def mount_flexvol(self, flexvol_name, junction_path=None):
        """Mounts a volume on a junction path."""
        api_args = {
            'volume-name': flexvol_name,
            'junction-path': (junction_path if junction_path
                              else '/%s' % flexvol_name)
        }
        self.send_request('volume-mount', api_args)

    def enable_flexvol_dedupe(self, flexvol_name):
        """Enable deduplication on volume."""
        api_args = {'path': '/vol/%s' % flexvol_name}
        self.send_request('sis-enable', api_args)

    def disable_flexvol_dedupe(self, flexvol_name):
        """Disable deduplication on volume."""
        api_args = {'path': '/vol/%s' % flexvol_name}
        self.send_request('sis-disable', api_args)

    def enable_flexvol_compression(self, flexvol_name):
        """Enable compression on volume."""
        api_args = {
            'path': '/vol/%s' % flexvol_name,
            'enable-compression': 'true'
        }
        self.send_request('sis-set-config', api_args)

    def disable_flexvol_compression(self, flexvol_name):
        """Disable compression on volume."""
        api_args = {
            'path': '/vol/%s' % flexvol_name,
            'enable-compression': 'false'
        }
        self.send_request('sis-set-config', api_args)

    @utils.trace_method
    def delete_file(self, path_to_file):
        """Delete file at path."""

        api_args = {
            'path': path_to_file,
        }
        # Use fast clone deletion engine if it is supported.
        if self.features.FAST_CLONE_DELETE:
            api_args['is-clone-file'] = 'true'
        self.send_request('file-delete-file', api_args, True)

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

        query = {
            'aggr-attributes': {
                'aggregate-name': '|'.join(aggregate_names),
            }
        } if aggregate_names else None

        api_args = {}
        if query:
            api_args['query'] = query
        if desired_attributes:
            api_args['desired-attributes'] = desired_attributes

        result = self.send_request('aggr-get-iter',
                                   api_args,
                                   enable_tunneling=False)
        if not self._has_records(result):
            return []
        else:
            return result.get_child_by_name('attributes-list').get_children()

    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

        desired_attributes = {
            'aggr-attributes': {
                'aggregate-name': None,
                'aggr-ownership-attributes': {
                    'home-name': None,
                },
            },
        }

        try:
            aggrs = self._get_aggregates(aggregate_names=[aggregate_name],
                                         desired_attributes=desired_attributes)
        except netapp_api.NaApiError as e:
            if e.code == netapp_api.EAPINOTFOUND:
                return None
            else:
                raise

        if len(aggrs) < 1:
            return None

        aggr_ownership_attrs = aggrs[0].get_child_by_name(
            'aggr-ownership-attributes') or netapp_api.NaElement('none')
        return aggr_ownership_attrs.get_child_content('home-name')

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

        if not aggregate_name:
            return {}

        desired_attributes = {
            'aggr-attributes': {
                'aggregate-name': None,
                'aggr-raid-attributes': {
                    'raid-type': None,
                    'is-hybrid': None,
                },
            },
        }

        try:
            aggrs = self._get_aggregates(aggregate_names=[aggregate_name],
                                         desired_attributes=desired_attributes)
        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]
        aggr_raid_attrs = aggr_attributes.get_child_by_name(
            'aggr-raid-attributes') or netapp_api.NaElement('none')

        aggregate = {
            'name': aggr_attributes.get_child_content('aggregate-name'),
            'raid-type': aggr_raid_attrs.get_child_content('raid-type'),
            'is-hybrid': strutils.bool_from_string(
                aggr_raid_attrs.get_child_content('is-hybrid')),
        }

        return aggregate

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

        disk_types = set()
        disk_types.update(self._get_aggregate_disk_types(aggregate_name))
        if self.features.ADVANCED_DISK_PARTITIONING:
            disk_types.update(self._get_aggregate_disk_types(aggregate_name,
                                                             shared=True))

        return list(disk_types) if disk_types else None

    def _get_aggregate_disk_types(self, aggregate_name, shared=False):
        """Get the disk type(s) of an aggregate (may be a list)."""

        disk_types = set()

        if shared:
            disk_raid_info = {
                'disk-shared-info': {
                    'aggregate-list': {
                        'shared-aggregate-info': {
                            'aggregate-name': aggregate_name,
                        },
                    },
                },
            }
        else:
            disk_raid_info = {
                'disk-aggregate-info': {
                    'aggregate-name': aggregate_name,
                },
            }

        api_args = {
            'query': {
                'storage-disk-info': {
                    'disk-raid-info': disk_raid_info,
                },
            },
            'desired-attributes': {
                'storage-disk-info': {
                    'disk-raid-info': {
                        'effective-disk-type': None,
                    },
                },
            },
        }

        try:
            result = self.send_iter_request(
                'storage-disk-get-iter', api_args, enable_tunneling=False)
        except netapp_api.NaApiError:
            LOG.exception('Failed to get disk info for aggregate %s.',
                          aggregate_name)
            return disk_types

        attributes_list = result.get_child_by_name(
            'attributes-list') or netapp_api.NaElement('none')

        for storage_disk_info in attributes_list.get_children():

            disk_raid_info = storage_disk_info.get_child_by_name(
                'disk-raid-info') or netapp_api.NaElement('none')
            disk_type = disk_raid_info.get_child_content(
                'effective-disk-type')
            if disk_type:
                disk_types.add(disk_type)

        return disk_types

    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."""

        desired_attributes = {
            'aggr-attributes': {
                'aggr-space-attributes': {
                    'percent-used-capacity': None,
                    'size-available': None,
                    'size-total': None,
                },
            },
        }

        try:
            aggrs = self._get_aggregates(aggregate_names=[aggregate_name],
                                         desired_attributes=desired_attributes)
        except netapp_api.NaApiError as e:
            if e.code == netapp_api.EAPINOTFOUND:
                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 {}

        if len(aggrs) < 1:
            return {}

        aggr_attributes = aggrs[0]
        aggr_space_attributes = aggr_attributes.get_child_by_name(
            'aggr-space-attributes') or netapp_api.NaElement('none')

        percent_used = int(aggr_space_attributes.get_child_content(
            'percent-used-capacity'))
        size_available = float(aggr_space_attributes.get_child_content(
            'size-available'))
        size_total = float(
            aggr_space_attributes.get_child_content('size-total'))

        return {
            'percent-used': percent_used,
            'size-available': size_available,
            'size-total': size_total,
        }

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

        api_args = {
            'objectname': object_name,
            'query': {
                'instance-info': {
                    'uuid': node_name + ':*',
                }
            }
        }

        result = self.send_request('perf-object-instance-list-info-iter',
                                   api_args,
                                   enable_tunneling=False)

        uuids = []

        instances = result.get_child_by_name(
            'attributes-list') or netapp_api.NaElement('None')

        for instance_info in instances.get_children():
            uuids.append(instance_info.get_child_content('uuid'))

        return uuids

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

        api_args = {
            'objectname': object_name,
            'instance-uuids': [
                {'instance-uuid': instance_uuid}
                for instance_uuid in instance_uuids
            ],
            'counters': [
                {'counter': counter} for counter in counter_names
            ],
        }

        result = self.send_request('perf-object-get-instances',
                                   api_args,
                                   enable_tunneling=False)

        counter_data = []

        timestamp = result.get_child_content('timestamp')

        instances = result.get_child_by_name(
            'instances') or netapp_api.NaElement('None')
        for instance in instances.get_children():

            instance_name = instance.get_child_content('name')
            instance_uuid = instance.get_child_content('uuid')
            node_name = instance_uuid.split(':')[0]

            counters = instance.get_child_by_name(
                'counters') or netapp_api.NaElement('None')
            for counter in counters.get_children():

                counter_name = counter.get_child_content('name')
                counter_value = counter.get_child_content('value')

                counter_data.append({
                    'instance-name': instance_name,
                    'instance-uuid': instance_uuid,
                    'node-name': node_name,
                    'timestamp': timestamp,
                    counter_name: counter_value,
                })

        return counter_data

    def get_snapshots_marked_for_deletion(self):
        """Get a list of snapshots marked for deletion."""

        api_args = {
            'query': {
                'snapshot-info': {
                    'name': client_base.DELETED_PREFIX + '*',
                    'vserver': self.vserver,
                    'busy': 'false',
                },
            },
            'desired-attributes': {
                'snapshot-info': {
                    'name': None,
                    'volume': None,
                    'snapshot-instance-uuid': None,
                }
            },
        }

        result = self.send_request('snapshot-get-iter', api_args)

        snapshots = []

        attributes = result.get_child_by_name(
            'attributes-list') or netapp_api.NaElement('none')
        snapshot_info_list = attributes.get_children()
        for snapshot_info in snapshot_info_list:
            snapshot_name = snapshot_info.get_child_content('name')
            snapshot_id = snapshot_info.get_child_content(
                'snapshot-instance-uuid')
            snapshot_volume = snapshot_info.get_child_content('volume')

            snapshots.append({
                'name': snapshot_name,
                'instance_id': snapshot_id,
                'volume_name': snapshot_volume,
            })

        return snapshots

    def get_snapshot(self, volume_name, snapshot_name):
        """Gets a single snapshot."""
        api_args = {
            'query': {
                'snapshot-info': {
                    'name': snapshot_name,
                    'volume': volume_name,
                },
            },
            'desired-attributes': {
                'snapshot-info': {
                    'name': None,
                    'volume': None,
                    'busy': None,
                    'snapshot-owners-list': {
                        'snapshot-owner': None,
                    }
                },
            },
        }
        result = self.send_request('snapshot-get-iter', api_args)

        self._handle_get_snapshot_return_failure(result, snapshot_name)

        attributes_list = result.get_child_by_name(
            'attributes-list') or netapp_api.NaElement('none')
        snapshot_info_list = attributes_list.get_children()

        self._handle_snapshot_not_found(result, snapshot_info_list,
                                        snapshot_name, volume_name)

        snapshot_info = snapshot_info_list[0]
        snapshot = {
            'name': snapshot_info.get_child_content('name'),
            'volume': snapshot_info.get_child_content('volume'),
            'busy': strutils.bool_from_string(
                snapshot_info.get_child_content('busy')),
        }

        snapshot_owners_list = snapshot_info.get_child_by_name(
            'snapshot-owners-list') or netapp_api.NaElement('none')
        snapshot_owners = set([
            snapshot_owner.get_child_content('owner')
            for snapshot_owner in snapshot_owners_list.get_children()])
        snapshot['owners'] = snapshot_owners

        return snapshot

    def _handle_get_snapshot_return_failure(self, result, snapshot_name):
        error_record_list = result.get_child_by_name(
            'volume-errors') or netapp_api.NaElement('none')
        errors = error_record_list.get_children()

        if errors:
            error = errors[0]
            error_code = error.get_child_content('errno')
            error_reason = error.get_child_content('reason')
            msg = _('Could not read information for snapshot %(name)s. '
                    'Code: %(code)s. Reason: %(reason)s')
            msg_args = {
                'name': snapshot_name,
                'code': error_code,
                'reason': error_reason,
            }
            if error_code == netapp_api.ESNAPSHOTNOTALLOWED:
                raise exception.SnapshotUnavailable(data=msg % msg_args)
            else:
                raise exception.VolumeBackendAPIException(data=msg % msg_args)

    def _handle_snapshot_not_found(self, result, snapshot_info_list,
                                   snapshot_name, volume_name):
        if not self._has_records(result):
            raise exception.SnapshotNotFound(snapshot_id=snapshot_name)
        elif len(snapshot_info_list) > 1:
            msg = _('Could not find unique snapshot %(snap)s on '
                    'volume %(vol)s.')
            msg_args = {'snap': snapshot_name, 'vol': volume_name}
            raise exception.VolumeBackendAPIException(data=msg % msg_args)

    def create_cluster_peer(self, addresses, username=None, password=None,
                            passphrase=None):
        """Creates a cluster peer relationship."""

        api_args = {
            'peer-addresses': [
                {'remote-inet-address': address} for address in addresses
            ],
        }
        if username:
            api_args['user-name'] = username
        if password:
            api_args['password'] = password
        if passphrase:
            api_args['passphrase'] = passphrase

        self.send_request('cluster-peer-create', api_args)

    def get_cluster_peers(self, remote_cluster_name=None):
        """Gets one or more cluster peer relationships."""

        api_args = {}
        if remote_cluster_name:
            api_args['query'] = {
                'cluster-peer-info': {
                    'remote-cluster-name': remote_cluster_name,
                }
            }

        result = self.send_iter_request('cluster-peer-get-iter', api_args)
        if not self._has_records(result):
            return []

        cluster_peers = []

        for cluster_peer_info in result.get_child_by_name(
                'attributes-list').get_children():

            cluster_peer = {
                'active-addresses': [],
                'peer-addresses': []
            }

            active_addresses = cluster_peer_info.get_child_by_name(
                'active-addresses') or netapp_api.NaElement('none')
            for address in active_addresses.get_children():
                cluster_peer['active-addresses'].append(address.get_content())

            peer_addresses = cluster_peer_info.get_child_by_name(
                'peer-addresses') or netapp_api.NaElement('none')
            for address in peer_addresses.get_children():
                cluster_peer['peer-addresses'].append(address.get_content())

            cluster_peer['availability'] = cluster_peer_info.get_child_content(
                'availability')
            cluster_peer['cluster-name'] = cluster_peer_info.get_child_content(
                'cluster-name')
            cluster_peer['cluster-uuid'] = cluster_peer_info.get_child_content(
                'cluster-uuid')
            cluster_peer['remote-cluster-name'] = (
                cluster_peer_info.get_child_content('remote-cluster-name'))
            cluster_peer['serial-number'] = (
                cluster_peer_info.get_child_content('serial-number'))
            cluster_peer['timeout'] = cluster_peer_info.get_child_content(
                'timeout')

            cluster_peers.append(cluster_peer)

        return cluster_peers

    def delete_cluster_peer(self, cluster_name):
        """Deletes a cluster peer relationship."""

        api_args = {'cluster-name': cluster_name}
        self.send_request('cluster-peer-delete', api_args)

    def get_cluster_peer_policy(self):
        """Gets the cluster peering policy configuration."""

        if not self.features.CLUSTER_PEER_POLICY:
            return {}

        result = self.send_request('cluster-peer-policy-get')

        attributes = result.get_child_by_name(
            'attributes') or netapp_api.NaElement('none')
        cluster_peer_policy = attributes.get_child_by_name(
            'cluster-peer-policy') or netapp_api.NaElement('none')

        policy = {
            'is-unauthenticated-access-permitted':
            cluster_peer_policy.get_child_content(
                'is-unauthenticated-access-permitted'),
            'passphrase-minimum-length':
            cluster_peer_policy.get_child_content(
                'passphrase-minimum-length'),
        }

        if policy['is-unauthenticated-access-permitted'] is not None:
            policy['is-unauthenticated-access-permitted'] = (
                strutils.bool_from_string(
                    policy['is-unauthenticated-access-permitted']))
        if policy['passphrase-minimum-length'] is not None:
            policy['passphrase-minimum-length'] = int(
                policy['passphrase-minimum-length'])

        return policy

    def set_cluster_peer_policy(self, is_unauthenticated_access_permitted=None,
                                passphrase_minimum_length=None):
        """Modifies the cluster peering policy configuration."""

        if not self.features.CLUSTER_PEER_POLICY:
            return

        if (is_unauthenticated_access_permitted is None and
                passphrase_minimum_length is None):
            return

        api_args = {}
        if is_unauthenticated_access_permitted is not None:
            api_args['is-unauthenticated-access-permitted'] = (
                'true' if strutils.bool_from_string(
                    is_unauthenticated_access_permitted) else 'false')
        if passphrase_minimum_length is not None:
            api_args['passphrase-minlength'] = six.text_type(
                passphrase_minimum_length)

        self.send_request('cluster-peer-policy-modify', api_args)

    def create_vserver_peer(self, vserver_name, peer_vserver_name):
        """Creates a Vserver peer relationship for SnapMirrors."""
        api_args = {
            'vserver': vserver_name,
            'peer-vserver': peer_vserver_name,
            'applications': [
                {'vserver-peer-application': 'snapmirror'},
            ],
        }
        self.send_request('vserver-peer-create', api_args)

    def delete_vserver_peer(self, vserver_name, peer_vserver_name):
        """Deletes a Vserver peer relationship."""

        api_args = {'vserver': vserver_name, 'peer-vserver': peer_vserver_name}
        self.send_request('vserver-peer-delete', api_args)

    def accept_vserver_peer(self, vserver_name, peer_vserver_name):
        """Accepts a pending Vserver peer relationship."""

        api_args = {'vserver': vserver_name, 'peer-vserver': peer_vserver_name}
        self.send_request('vserver-peer-accept', api_args)

    def get_vserver_peers(self, vserver_name=None, peer_vserver_name=None):
        """Gets one or more Vserver peer relationships."""

        api_args = None
        if vserver_name or peer_vserver_name:
            api_args = {'query': {'vserver-peer-info': {}}}
            if vserver_name:
                api_args['query']['vserver-peer-info']['vserver'] = (
                    vserver_name)
            if peer_vserver_name:
                api_args['query']['vserver-peer-info']['peer-vserver'] = (
                    peer_vserver_name)

        result = self.send_iter_request('vserver-peer-get-iter', api_args)
        if not self._has_records(result):
            return []

        vserver_peers = []

        for vserver_peer_info in result.get_child_by_name(
                'attributes-list').get_children():

            vserver_peer = {
                'vserver': vserver_peer_info.get_child_content('vserver'),
                'peer-vserver':
                vserver_peer_info.get_child_content('peer-vserver'),
                'peer-state':
                vserver_peer_info.get_child_content('peer-state'),
                'peer-cluster':
                vserver_peer_info.get_child_content('peer-cluster'),
            }
            vserver_peers.append(vserver_peer)

        return vserver_peers

    def _ensure_snapmirror_v2(self):
        """Verify support for SnapMirror control plane v2."""
        if not self.features.SNAPMIRROR_V2:
            msg = _('SnapMirror features require Data ONTAP 8.2 or later.')
            raise exception.NetAppDriverException(msg)

    def create_snapmirror(self, source_vserver, source_volume,
                          destination_vserver, destination_volume,
                          schedule=None, policy=None,
                          relationship_type='data_protection'):
        """Creates a SnapMirror relationship (cDOT 8.2 or later only)."""
        self._ensure_snapmirror_v2()

        api_args = {
            'source-volume': source_volume,
            'source-vserver': source_vserver,
            'destination-volume': destination_volume,
            'destination-vserver': destination_vserver,
            'relationship-type': relationship_type,
        }
        if schedule:
            api_args['schedule'] = schedule
        if policy:
            api_args['policy'] = policy

        try:
            self.send_request('snapmirror-create', api_args)
        except netapp_api.NaApiError as e:
            if e.code != netapp_api.ERELATION_EXISTS:
                raise

    def initialize_snapmirror(self, source_vserver, source_volume,
                              destination_vserver, destination_volume,
                              source_snapshot=None, transfer_priority=None):
        """Initializes a SnapMirror relationship (cDOT 8.2 or later only)."""
        self._ensure_snapmirror_v2()

        api_args = {
            'source-volume': source_volume,
            'source-vserver': source_vserver,
            'destination-volume': destination_volume,
            'destination-vserver': destination_vserver,
        }
        if source_snapshot:
            api_args['source-snapshot'] = source_snapshot
        if transfer_priority:
            api_args['transfer-priority'] = transfer_priority

        result = self.send_request('snapmirror-initialize', api_args)

        result_info = {}
        result_info['operation-id'] = result.get_child_content(
            'result-operation-id')
        result_info['status'] = result.get_child_content('result-status')
        result_info['jobid'] = result.get_child_content('result-jobid')
        result_info['error-code'] = result.get_child_content(
            'result-error-code')
        result_info['error-message'] = result.get_child_content(
            'result-error-message')

        return result_info

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

        api_args = {
            'query': {
                'snapmirror-destination-info': {
                    'source-volume': source_volume,
                    'source-vserver': source_vserver,
                    'destination-volume': destination_volume,
                    'destination-vserver': destination_vserver,
                    'relationship-info-only': ('true' if relationship_info_only
                                               else 'false'),
                }
            }
        }
        self.send_request('snapmirror-release-iter', api_args)

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

        api_args = {
            'source-volume': source_volume,
            'source-vserver': source_vserver,
            'destination-volume': destination_volume,
            'destination-vserver': destination_vserver,
        }
        self.send_request('snapmirror-quiesce', api_args)

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

        api_args = {
            'source-volume': source_volume,
            'source-vserver': source_vserver,
            'destination-volume': destination_volume,
            'destination-vserver': destination_vserver,
            'clear-checkpoint': 'true' if clear_checkpoint else 'false',
        }
        try:
            self.send_request('snapmirror-abort', api_args)
        except netapp_api.NaApiError as e:
            if e.code != netapp_api.ENOTRANSFER_IN_PROGRESS:
                raise

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

        api_args = {
            'source-volume': source_volume,
            'source-vserver': source_vserver,
            'destination-volume': destination_volume,
            'destination-vserver': destination_vserver,
        }
        self.send_request('snapmirror-break', api_args)

    def modify_snapmirror(self, source_vserver, source_volume,
                          destination_vserver, destination_volume,
                          schedule=None, policy=None, tries=None,
                          max_transfer_rate=None):
        """Modifies a SnapMirror relationship."""
        self._ensure_snapmirror_v2()

        api_args = {
            'source-volume': source_volume,
            'source-vserver': source_vserver,
            'destination-volume': destination_volume,
            'destination-vserver': destination_vserver,
        }
        if schedule:
            api_args['schedule'] = schedule
        if policy:
            api_args['policy'] = policy
        if tries is not None:
            api_args['tries'] = tries
        if max_transfer_rate is not None:
            api_args['max-transfer-rate'] = max_transfer_rate

        self.send_request('snapmirror-modify', api_args)

    def delete_snapmirror(self, source_vserver, source_volume,
                          destination_vserver, destination_volume):
        """Destroys a SnapMirror relationship."""
        self._ensure_snapmirror_v2()

        api_args = {
            'query': {
                'snapmirror-info': {
                    'source-volume': source_volume,
                    'source-vserver': source_vserver,
                    'destination-volume': destination_volume,
                    'destination-vserver': destination_vserver,
                }
            }
        }
        self.send_request('snapmirror-destroy-iter', api_args)

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

        api_args = {
            'source-volume': source_volume,
            'source-vserver': source_vserver,
            'destination-volume': destination_volume,
            'destination-vserver': destination_vserver,
        }
        try:
            self.send_request('snapmirror-update', api_args)
        except netapp_api.NaApiError as e:
            if (e.code != netapp_api.ETRANSFER_IN_PROGRESS and
                    e.code != netapp_api.EANOTHER_OP_ACTIVE):
                raise

    def resume_snapmirror(self, source_vserver, source_volume,
                          destination_vserver, destination_volume):
        """Resume a SnapMirror relationship if it is quiesced."""
        self._ensure_snapmirror_v2()

        api_args = {
            'source-volume': source_volume,
            'source-vserver': source_vserver,
            'destination-volume': destination_volume,
            'destination-vserver': destination_vserver,
        }
        try:
            self.send_request('snapmirror-resume', api_args)
        except netapp_api.NaApiError as e:
            if e.code != netapp_api.ERELATION_NOT_QUIESCED:
                raise

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

        api_args = {
            'source-volume': source_volume,
            'source-vserver': source_vserver,
            'destination-volume': destination_volume,
            'destination-vserver': destination_vserver,
        }
        self.send_request('snapmirror-resync', api_args)

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

        query = None
        if (source_vserver or source_volume or destination_vserver or
                destination_volume):
            query = {'snapmirror-info': {}}
            if source_volume:
                query['snapmirror-info']['source-volume'] = source_volume
            if destination_volume:
                query['snapmirror-info']['destination-volume'] = (
                    destination_volume)
            if source_vserver:
                query['snapmirror-info']['source-vserver'] = source_vserver
            if destination_vserver:
                query['snapmirror-info']['destination-vserver'] = (
                    destination_vserver)

        api_args = {}
        if query:
            api_args['query'] = query
        if desired_attributes:
            api_args['desired-attributes'] = desired_attributes

        result = self.send_iter_request('snapmirror-get-iter', api_args)
        if not self._has_records(result):
            return []
        else:
            return result.get_child_by_name('attributes-list').get_children()

    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 should be a flat list of attribute names.
        """
        self._ensure_snapmirror_v2()

        if desired_attributes is not None:
            desired_attributes = {
                'snapmirror-info': {attr: None for attr in desired_attributes},
            }

        result = self._get_snapmirrors(
            source_vserver=source_vserver,
            source_volume=source_volume,
            destination_vserver=destination_vserver,
            destination_volume=destination_volume,
            desired_attributes=desired_attributes)

        snapmirrors = []

        for snapmirror_info in result:
            snapmirror = {}
            for child in snapmirror_info.get_children():
                name = self._strip_xml_namespace(child.get_name())
                snapmirror[name] = child.get_content()
            snapmirrors.append(snapmirror)

        return snapmirrors

    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)),
        }

        return provisioning_opts
