
When calling ONTAP API the block_count is sent as a float which isn't accepted by the API because it will have a trailing '.0'. So it's needed to cast it to int before send it to the API as string. Change-Id: Ia1f9b4717506a227af42f4f534278e0d9ab14dad Closes-Bug: #1818506
2246 lines
86 KiB
Python
2246 lines
86 KiB
Python
# 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.connection.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.connection.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.connection.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):
|
|
# ONTAP handles only 128 MB per call as of v9.1
|
|
bc_limit = 2 ** 18 # 2^18 blocks * 512 bytes/block = 128 MB
|
|
z_calls = int(math.ceil(block_count / float(bc_limit)))
|
|
zbc = block_count
|
|
if z_calls == 0:
|
|
z_calls = 1
|
|
for _call in range(0, z_calls):
|
|
if zbc > bc_limit:
|
|
block_count = bc_limit
|
|
zbc -= bc_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(int(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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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': 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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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.connection.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
|