cinder/cinder/volume/drivers/netapp/dataontap/client/client_base.py

443 lines
18 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.
#
# 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 sys
from oslo_log import log as logging
from oslo_utils import excutils
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 import utils as na_utils
from cinder.volume import volume_utils
LOG = logging.getLogger(__name__)
DELETED_PREFIX = 'deleted_cinder_'
MAX_SIZE_FOR_A_LUN = '17555678822400'
@six.add_metaclass(volume_utils.TraceWrapperMetaclass)
class Client(object):
def __init__(self, **kwargs):
host = kwargs['hostname']
username = kwargs['username']
password = kwargs['password']
api_trace_pattern = kwargs['api_trace_pattern']
self.connection = netapp_api.NaServer(
host=host,
transport_type=kwargs['transport_type'],
port=kwargs['port'],
username=username,
password=password,
api_trace_pattern=api_trace_pattern)
self.ssh_client = self._init_ssh_client(host, username, password)
def _init_ssh_client(self, host, username, password):
return netapp_api.SSHUtil(
host=host,
username=username,
password=password)
def _init_features(self):
"""Set up the repository of available Data ONTAP features."""
self.features = na_utils.Features()
def get_ontap_version(self, cached=True):
"""Gets the ONTAP version."""
if cached:
return self.connection.get_ontap_version()
ontap_version = netapp_api.NaElement("system-get-version")
result = self.connection.invoke_successfully(ontap_version, True)
version_tuple = result.get_child_by_name(
'version-tuple') or netapp_api.NaElement('none')
system_version_tuple = version_tuple.get_child_by_name(
'system-version-tuple') or netapp_api.NaElement('none')
generation = system_version_tuple.get_child_content("generation")
major = system_version_tuple.get_child_content("major")
return '%(generation)s.%(major)s' % {
'generation': generation,
'major': major}
def get_ontapi_version(self, cached=True):
"""Gets the supported ontapi version."""
if cached:
return self.connection.get_api_version()
ontapi_version = netapp_api.NaElement('system-get-ontapi-version')
res = self.connection.invoke_successfully(ontapi_version, False)
major = res.get_child_content('major-version')
minor = res.get_child_content('minor-version')
return major, minor
def _strip_xml_namespace(self, string):
if string.startswith('{') and '}' in string:
return string.split('}', 1)[1]
return string
def check_is_naelement(self, elem):
"""Checks if object is instance of NaElement."""
if not isinstance(elem, netapp_api.NaElement):
raise ValueError('Expects NaElement')
def create_lun(self, volume_name, lun_name, size, metadata,
qos_policy_group_name=None,
qos_policy_group_is_adaptive=False):
"""Issues API request for creating LUN on volume."""
self._validate_qos_policy_group(qos_policy_group_is_adaptive)
path = '/vol/%s/%s' % (volume_name, lun_name)
space_reservation = metadata['SpaceReserved']
initial_size = size
ontap_version = self.get_ontap_version()
# On older ONTAP versions the extend size is limited to its
# geometry on max_resize_size. In order to remove this
# limitation we create the LUN with its maximum possible size
# and then shrink to the requested size.
if ontap_version < '9.5':
initial_size = MAX_SIZE_FOR_A_LUN
# In order to create a LUN with its maximum size (16TB),
# the space_reservation needs to be disabled
space_reservation = 'false'
params = {'path': path, 'size': str(initial_size),
'ostype': metadata['OsType'],
'space-reservation-enabled': space_reservation}
version = self.get_ontapi_version()
if version >= (1, 110):
params['use-exact-size'] = 'true'
lun_create = netapp_api.NaElement.create_node_with_children(
'lun-create-by-size',
**params)
if qos_policy_group_name:
if qos_policy_group_is_adaptive:
lun_create.add_new_child(
'qos-adaptive-policy-group', qos_policy_group_name)
else:
lun_create.add_new_child(
'qos-policy-group', qos_policy_group_name)
try:
self.connection.invoke_successfully(lun_create, True)
except netapp_api.NaApiError as ex:
with excutils.save_and_reraise_exception():
LOG.error("Error provisioning volume %(lun_name)s on "
"%(volume_name)s. Details: %(ex)s",
{'lun_name': lun_name,
'volume_name': volume_name,
'ex': ex})
if ontap_version < '9.5':
self.do_direct_resize(path, six.text_type(size))
if metadata['SpaceReserved'] == 'true':
self.set_lun_space_reservation(path, True)
def set_lun_space_reservation(self, path, flag):
"""Sets the LUN space reservation on ONTAP."""
lun_modify_space_reservation = (
netapp_api.NaElement.create_node_with_children(
'lun-set-space-reservation-info', **{
'path': path,
'enable': str(flag)}))
self.connection.invoke_successfully(lun_modify_space_reservation, True)
def destroy_lun(self, path, force=True):
"""Destroys the LUN at the path."""
lun_destroy = netapp_api.NaElement.create_node_with_children(
'lun-destroy',
**{'path': path})
if force:
lun_destroy.add_new_child('force', 'true')
self.connection.invoke_successfully(lun_destroy, True)
seg = path.split("/")
LOG.debug("Destroyed LUN %s", seg[-1])
def map_lun(self, path, igroup_name, lun_id=None):
"""Maps LUN to the initiator and returns LUN id assigned."""
lun_map = netapp_api.NaElement.create_node_with_children(
'lun-map', **{'path': path,
'initiator-group': igroup_name})
if lun_id:
lun_map.add_new_child('lun-id', lun_id)
try:
result = self.connection.invoke_successfully(lun_map, True)
return result.get_child_content('lun-id-assigned')
except netapp_api.NaApiError as e:
code = e.code
message = e.message
LOG.warning('Error mapping LUN. Code :%(code)s, Message: '
'%(message)s', {'code': code, 'message': message})
raise
def unmap_lun(self, path, igroup_name):
"""Unmaps a LUN from given initiator."""
lun_unmap = netapp_api.NaElement.create_node_with_children(
'lun-unmap',
**{'path': path, 'initiator-group': igroup_name})
try:
self.connection.invoke_successfully(lun_unmap, True)
except netapp_api.NaApiError as e:
exc_info = sys.exc_info()
LOG.warning("Error unmapping LUN. Code :%(code)s, Message: "
"%(message)s", {'code': e.code,
'message': e.message})
# if the LUN is already unmapped
if e.code == '13115' or e.code == '9016':
pass
else:
six.reraise(*exc_info)
def create_igroup(self, igroup, igroup_type='iscsi', os_type='default'):
"""Creates igroup with specified args."""
igroup_create = netapp_api.NaElement.create_node_with_children(
'igroup-create',
**{'initiator-group-name': igroup,
'initiator-group-type': igroup_type,
'os-type': os_type})
self.connection.invoke_successfully(igroup_create, True)
def add_igroup_initiator(self, igroup, initiator):
"""Adds initiators to the specified igroup."""
igroup_add = netapp_api.NaElement.create_node_with_children(
'igroup-add',
**{'initiator-group-name': igroup,
'initiator': initiator})
self.connection.invoke_successfully(igroup_add, True)
def do_direct_resize(self, path, new_size_bytes, force=True):
"""Resize the LUN."""
seg = path.split("/")
LOG.info("Resizing LUN %s directly to new size.", seg[-1])
lun_resize = netapp_api.NaElement.create_node_with_children(
'lun-resize',
**{'path': path,
'size': new_size_bytes})
if force:
lun_resize.add_new_child('force', 'true')
self.connection.invoke_successfully(lun_resize, True)
def get_lun_geometry(self, path):
"""Gets the LUN geometry."""
geometry = {}
lun_geo = netapp_api.NaElement("lun-get-geometry")
lun_geo.add_new_child('path', path)
try:
result = self.connection.invoke_successfully(lun_geo, True)
geometry['size'] = result.get_child_content("size")
geometry['bytes_per_sector'] = result.get_child_content(
"bytes-per-sector")
geometry['sectors_per_track'] = result.get_child_content(
"sectors-per-track")
geometry['tracks_per_cylinder'] = result.get_child_content(
"tracks-per-cylinder")
geometry['cylinders'] = result.get_child_content("cylinders")
geometry['max_resize'] = result.get_child_content(
"max-resize-size")
except Exception as e:
LOG.error("LUN %(path)s geometry failed. Message - %(msg)s",
{'path': path, 'msg': six.text_type(e)})
return geometry
def get_volume_options(self, volume_name):
"""Get the value for the volume option."""
opts = []
vol_option_list = netapp_api.NaElement("volume-options-list-info")
vol_option_list.add_new_child('volume', volume_name)
result = self.connection.invoke_successfully(vol_option_list, True)
options = result.get_child_by_name("options")
if options:
opts = options.get_children()
return opts
def move_lun(self, path, new_path):
"""Moves the LUN at path to new path."""
seg = path.split("/")
new_seg = new_path.split("/")
LOG.debug("Moving LUN %(name)s to %(new_name)s.",
{'name': seg[-1], 'new_name': new_seg[-1]})
lun_move = netapp_api.NaElement("lun-move")
lun_move.add_new_child("path", path)
lun_move.add_new_child("new-path", new_path)
self.connection.invoke_successfully(lun_move, True)
def get_iscsi_target_details(self):
"""Gets the iSCSI target portal details."""
raise NotImplementedError()
def get_fc_target_wwpns(self):
"""Gets the FC target details."""
raise NotImplementedError()
def get_iscsi_service_details(self):
"""Returns iscsi iqn."""
raise NotImplementedError()
def check_iscsi_initiator_exists(self, iqn):
"""Returns True if initiator exists."""
raise NotImplementedError()
def set_iscsi_chap_authentication(self, iqn, username, password):
"""Provides NetApp host's CHAP credentials to the backend."""
raise NotImplementedError()
def get_lun_list(self):
"""Gets the list of LUNs on filer."""
raise NotImplementedError()
def get_igroup_by_initiators(self, initiator_list):
"""Get igroups exactly matching a set of initiators."""
raise NotImplementedError()
def _validate_qos_policy_group(self, is_adaptive, spec=None, is_nfs=False):
"""Raises an exception if the backend doesn't support the QoS spec."""
raise NotImplementedError()
def _has_luns_mapped_to_initiator(self, initiator):
"""Checks whether any LUNs are mapped to the given initiator."""
lun_list_api = netapp_api.NaElement('lun-initiator-list-map-info')
lun_list_api.add_new_child('initiator', initiator)
result = self.connection.invoke_successfully(lun_list_api, True)
lun_maps_container = result.get_child_by_name(
'lun-maps') or netapp_api.NaElement('none')
return len(lun_maps_container.get_children()) > 0
def has_luns_mapped_to_initiators(self, initiator_list):
"""Checks whether any LUNs are mapped to the given initiator(s)."""
for initiator in initiator_list:
if self._has_luns_mapped_to_initiator(initiator):
return True
return False
def get_lun_by_args(self, **args):
"""Retrieves LUNs with specified args."""
raise NotImplementedError()
def get_performance_counter_info(self, object_name, counter_name):
"""Gets info about one or more Data ONTAP performance counters."""
api_args = {'objectname': object_name}
result = self.connection.send_request('perf-object-counter-list-info',
api_args,
enable_tunneling=False)
counters = result.get_child_by_name(
'counters') or netapp_api.NaElement('None')
for counter in counters.get_children():
if counter.get_child_content('name') == counter_name:
labels = []
label_list = counter.get_child_by_name(
'labels') or netapp_api.NaElement('None')
for label in label_list.get_children():
labels.extend(label.get_content().split(','))
base_counter = counter.get_child_content('base-counter')
return {
'name': counter_name,
'labels': labels,
'base-counter': base_counter,
}
else:
raise exception.NotFound(_('Counter %s not found') % counter_name)
def delete_snapshot(self, volume_name, snapshot_name):
"""Deletes a volume snapshot."""
api_args = {'volume': volume_name, 'snapshot': snapshot_name}
self.connection.send_request('snapshot-delete', api_args)
def create_cg_snapshot(self, volume_names, snapshot_name):
"""Creates a consistency group snapshot out of one or more flexvols.
ONTAP requires an invocation of cg-start to first fence off the
flexvols to be included in the snapshot. If cg-start returns
success, a cg-commit must be executed to finalized the snapshot and
unfence the flexvols.
"""
cg_id = self._start_cg_snapshot(volume_names, snapshot_name)
if not cg_id:
msg = _('Could not start consistency group snapshot %s.')
raise exception.VolumeBackendAPIException(data=msg % snapshot_name)
self._commit_cg_snapshot(cg_id)
def _start_cg_snapshot(self, volume_names, snapshot_name):
snapshot_init = {
'snapshot': snapshot_name,
'timeout': 'relaxed',
'volumes': [
{'volume-name': volume_name} for volume_name in volume_names
],
}
result = self.connection.send_request('cg-start', snapshot_init)
return result.get_child_content('cg-id')
def _commit_cg_snapshot(self, cg_id):
snapshot_commit = {'cg-id': cg_id}
self.connection.send_request('cg-commit', snapshot_commit)
def get_snapshot(self, volume_name, snapshot_name):
"""Gets a single snapshot."""
raise NotImplementedError()
@utils.retry(exception.SnapshotIsBusy)
def wait_for_busy_snapshot(self, flexvol, snapshot_name):
"""Checks for and handles a busy snapshot.
If a snapshot is busy, for reasons other than cloning, an exception is
raised immediately. Otherwise, wait for a period of time for the clone
dependency to finish before giving up. If the snapshot is not busy then
no action is taken and the method exits.
"""
snapshot = self.get_snapshot(flexvol, snapshot_name)
if not snapshot['busy']:
LOG.debug("Backing consistency group snapshot %s available for "
"deletion.", snapshot_name)
return
else:
LOG.debug("Snapshot %(snap)s for vol %(vol)s is busy, waiting "
"for volume clone dependency to clear.",
{"snap": snapshot_name, "vol": flexvol})
raise exception.SnapshotIsBusy(snapshot_name=snapshot_name)
def mark_snapshot_for_deletion(self, volume, snapshot_name):
"""Mark snapshot for deletion by renaming snapshot."""
return self.rename_snapshot(
volume, snapshot_name, DELETED_PREFIX + snapshot_name)
def rename_snapshot(self, volume, current_name, new_name):
"""Renames a snapshot."""
api_args = {
'volume': volume,
'current-name': current_name,
'new-name': new_name,
}
return self.connection.send_request('snapshot-rename', api_args)