cinder/cinder/volume/drivers/netapp/utils.py

515 lines
18 KiB
Python

# Copyright (c) 2012 NetApp, Inc.
# Copyright (c) 2012 OpenStack Foundation
# 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.
"""
Utilities for NetApp drivers.
This module contains common utilities to be used by one or more
NetApp drivers to achieve the desired functionality.
"""
import base64
import binascii
import copy
import decimal
import platform
import socket
import uuid
from oslo.concurrency import processutils as putils
from oslo.utils import timeutils
import six
from cinder import context
from cinder import exception
from cinder.i18n import _
from cinder.openstack.common import log as logging
from cinder import utils
from cinder import version
from cinder.volume.drivers.netapp.api import NaApiError
from cinder.volume.drivers.netapp.api import NaElement
from cinder.volume.drivers.netapp.api import NaErrors
from cinder.volume.drivers.netapp.api import NaServer
from cinder.volume import volume_types
LOG = logging.getLogger(__name__)
OBSOLETE_SSC_SPECS = {'netapp:raid_type': 'netapp_raid_type',
'netapp:disk_type': 'netapp_disk_type'}
DEPRECATED_SSC_SPECS = {'netapp_unmirrored': 'netapp_mirrored',
'netapp_nodedup': 'netapp_dedup',
'netapp_nocompression': 'netapp_compression',
'netapp_thick_provisioned': 'netapp_thin_provisioned'}
def provide_ems(requester, server, netapp_backend, app_version,
server_type="cluster"):
"""Provide ems with volume stats for the requester.
:param server_type: cluster or 7mode.
"""
def _create_ems(netapp_backend, app_version, server_type):
"""Create ems api request."""
ems_log = NaElement('ems-autosupport-log')
host = socket.getfqdn() or 'Cinder_node'
if server_type == "cluster":
dest = "cluster node"
else:
dest = "7 mode controller"
ems_log.add_new_child('computer-name', host)
ems_log.add_new_child('event-id', '0')
ems_log.add_new_child('event-source',
'Cinder driver %s' % netapp_backend)
ems_log.add_new_child('app-version', app_version)
ems_log.add_new_child('category', 'provisioning')
ems_log.add_new_child('event-description',
'OpenStack Cinder connected to %s' % dest)
ems_log.add_new_child('log-level', '6')
ems_log.add_new_child('auto-support', 'false')
return ems_log
def _create_vs_get():
"""Create vs_get api request."""
vs_get = NaElement('vserver-get-iter')
vs_get.add_new_child('max-records', '1')
query = NaElement('query')
query.add_node_with_children('vserver-info',
**{'vserver-type': 'node'})
vs_get.add_child_elem(query)
desired = NaElement('desired-attributes')
desired.add_node_with_children(
'vserver-info', **{'vserver-name': '', 'vserver-type': ''})
vs_get.add_child_elem(desired)
return vs_get
def _get_cluster_node(na_server):
"""Get the cluster node for ems."""
na_server.set_vserver(None)
vs_get = _create_vs_get()
res = na_server.invoke_successfully(vs_get)
if (res.get_child_content('num-records') and
int(res.get_child_content('num-records')) > 0):
attr_list = res.get_child_by_name('attributes-list')
vs_info = attr_list.get_child_by_name('vserver-info')
vs_name = vs_info.get_child_content('vserver-name')
return vs_name
return None
do_ems = True
if hasattr(requester, 'last_ems'):
sec_limit = 3559
if not (timeutils.is_older_than(requester.last_ems, sec_limit)):
do_ems = False
if do_ems:
na_server = copy.copy(server)
na_server.set_timeout(25)
ems = _create_ems(netapp_backend, app_version, server_type)
try:
if server_type == "cluster":
api_version = na_server.get_api_version()
if api_version:
major, minor = api_version
else:
raise NaApiError(code='Not found',
message='No api version found')
if major == 1 and minor > 15:
node = getattr(requester, 'vserver', None)
else:
node = _get_cluster_node(na_server)
if node is None:
raise NaApiError(code='Not found',
message='No vserver found')
na_server.set_vserver(node)
else:
na_server.set_vfiler(None)
na_server.invoke_successfully(ems, True)
LOG.debug("ems executed successfully.")
except NaApiError as e:
LOG.warn(_("Failed to invoke ems. Message : %s") % e)
finally:
requester.last_ems = timeutils.utcnow()
def validate_instantiation(**kwargs):
"""Checks if a driver is instantiated other than by the unified driver.
Helps check direct instantiation of netapp drivers.
Call this function in every netapp block driver constructor.
"""
if kwargs and kwargs.get('netapp_mode') == 'proxy':
return
LOG.warn(_("It is not the recommended way to use drivers by NetApp. "
"Please use NetAppDriver to achieve the functionality."))
def invoke_api(na_server, api_name, api_family='cm', query=None,
des_result=None, additional_elems=None,
is_iter=False, records=0, tag=None,
timeout=0, tunnel=None):
"""Invokes any given api call to a NetApp server.
:param na_server: na_server instance
:param api_name: api name string
:param api_family: cm or 7m
:param query: api query as dict
:param des_result: desired result as dict
:param additional_elems: dict other than query and des_result
:param is_iter: is iterator api
:param records: limit for records, 0 for infinite
:param timeout: timeout seconds
:param tunnel: tunnel entity, vserver or vfiler name
"""
record_step = 50
if not (na_server or isinstance(na_server, NaServer)):
msg = _("Requires an NaServer instance.")
raise exception.InvalidInput(reason=msg)
server = copy.copy(na_server)
if api_family == 'cm':
server.set_vserver(tunnel)
else:
server.set_vfiler(tunnel)
if timeout > 0:
server.set_timeout(timeout)
iter_records = 0
cond = True
while cond:
na_element = create_api_request(
api_name, query, des_result, additional_elems,
is_iter, record_step, tag)
result = server.invoke_successfully(na_element, True)
if is_iter:
if records > 0:
iter_records = iter_records + record_step
if iter_records >= records:
cond = False
tag_el = result.get_child_by_name('next-tag')
tag = tag_el.get_content() if tag_el else None
if not tag:
cond = False
else:
cond = False
yield result
def create_api_request(api_name, query=None, des_result=None,
additional_elems=None, is_iter=False,
record_step=50, tag=None):
"""Creates a NetApp api request.
:param api_name: api name string
:param query: api query as dict
:param des_result: desired result as dict
:param additional_elems: dict other than query and des_result
:param is_iter: is iterator api
:param record_step: records at a time for iter api
:param tag: next tag for iter api
"""
api_el = NaElement(api_name)
if query:
query_el = NaElement('query')
query_el.translate_struct(query)
api_el.add_child_elem(query_el)
if des_result:
res_el = NaElement('desired-attributes')
res_el.translate_struct(des_result)
api_el.add_child_elem(res_el)
if additional_elems:
api_el.translate_struct(additional_elems)
if is_iter:
api_el.add_new_child('max-records', str(record_step))
if tag:
api_el.add_new_child('tag', tag, True)
return api_el
def to_bool(val):
"""Converts true, yes, y, 1 to True, False otherwise."""
if val:
strg = str(val).lower()
if (strg == 'true' or strg == 'y'
or strg == 'yes' or strg == 'enabled'
or strg == '1'):
return True
else:
return False
else:
return False
@utils.synchronized("safe_set_attr")
def set_safe_attr(instance, attr, val):
"""Sets the attribute in a thread safe manner.
Returns if new val was set on attribute.
If attr already had the value then False.
"""
if not instance or not attr:
return False
old_val = getattr(instance, attr, None)
if val is None and old_val is None:
return False
elif val == old_val:
return False
else:
setattr(instance, attr, val)
return True
def get_volume_extra_specs(volume):
"""Provides extra specs associated with volume."""
ctxt = context.get_admin_context()
type_id = volume.get('volume_type_id')
specs = None
if type_id is not None:
volume_type = volume_types.get_volume_type(ctxt, type_id)
specs = volume_type.get('extra_specs')
return specs
def check_apis_on_cluster(na_server, api_list=None):
"""Checks api availability and permissions on cluster.
Checks api availability and permissions for executing user.
Returns a list of failed apis.
"""
api_list = api_list or []
failed_apis = []
if api_list:
api_version = na_server.get_api_version()
if api_version:
major, minor = api_version
if major == 1 and minor < 20:
for api_name in api_list:
na_el = NaElement(api_name)
try:
na_server.invoke_successfully(na_el)
except Exception as e:
if isinstance(e, NaApiError):
if (e.code == NaErrors['API_NOT_FOUND'].code or
e.code ==
NaErrors['INSUFFICIENT_PRIVS'].code):
failed_apis.append(api_name)
elif major == 1 and minor >= 20:
failed_apis = copy.copy(api_list)
result = invoke_api(
na_server,
api_name='system-user-capability-get-iter',
api_family='cm',
additional_elems=None,
is_iter=True)
for res in result:
attr_list = res.get_child_by_name('attributes-list')
if attr_list:
capabilities = attr_list.get_children()
for capability in capabilities:
op_list = capability.get_child_by_name(
'operation-list')
if op_list:
ops = op_list.get_children()
for op in ops:
apis = op.get_child_content('api-name')
if apis:
api_list = apis.split(',')
for api_name in api_list:
if (api_name and
api_name.strip()
in failed_apis):
failed_apis.remove(api_name)
else:
continue
else:
msg = _("Unsupported Clustered Data ONTAP version.")
raise exception.VolumeBackendAPIException(data=msg)
else:
msg = _("Api version could not be determined.")
raise exception.VolumeBackendAPIException(data=msg)
return failed_apis
def resolve_hostname(hostname):
"""Resolves host name to IP address."""
res = socket.getaddrinfo(hostname, None)[0]
family, socktype, proto, canonname, sockaddr = res
return sockaddr[0]
def encode_hex_to_base32(hex_string):
"""Encodes hex to base32 bit as per RFC4648."""
bin_form = binascii.unhexlify(hex_string)
return base64.b32encode(bin_form)
def decode_base32_to_hex(base32_string):
"""Decodes base32 string to hex string."""
bin_form = base64.b32decode(base32_string)
return binascii.hexlify(bin_form)
def convert_uuid_to_es_fmt(uuid_str):
"""Converts uuid to e-series compatible name format."""
uuid_base32 = encode_hex_to_base32(uuid.UUID(str(uuid_str)).hex)
return uuid_base32.strip('=')
def convert_es_fmt_to_uuid(es_label):
"""Converts e-series name format to uuid."""
es_label_b32 = es_label.ljust(32, '=')
return uuid.UUID(binascii.hexlify(base64.b32decode(es_label_b32)))
def round_down(value, precision):
return float(decimal.Decimal(six.text_type(value)).quantize(
decimal.Decimal(precision), rounding=decimal.ROUND_DOWN))
def log_extra_spec_warnings(extra_specs):
for spec in (set(extra_specs.keys() if extra_specs else []) &
set(OBSOLETE_SSC_SPECS.keys())):
msg = _('Extra spec %(old)s is obsolete. Use %(new)s instead.')
args = {'old': spec, 'new': OBSOLETE_SSC_SPECS[spec]}
LOG.warn(msg % args)
for spec in (set(extra_specs.keys() if extra_specs else []) &
set(DEPRECATED_SSC_SPECS.keys())):
msg = _('Extra spec %(old)s is deprecated. Use %(new)s instead.')
args = {'old': spec, 'new': DEPRECATED_SSC_SPECS[spec]}
LOG.warn(msg % args)
class OpenStackInfo(object):
"""OS/distribution, release, and version.
NetApp uses these fields as content for EMS log entry.
"""
PACKAGE_NAME = 'python-cinder'
def __init__(self):
self._version = 'unknown version'
self._release = 'unknown release'
self._vendor = 'unknown vendor'
self._platform = 'unknown platform'
def _update_version_from_version_string(self):
try:
self._version = version.version_info.version_string()
except Exception:
pass
def _update_release_from_release_string(self):
try:
self._release = version.version_info.release_string()
except Exception:
pass
def _update_platform(self):
try:
self._platform = platform.platform()
except Exception:
pass
@staticmethod
def _get_version_info_version():
return version.version_info.version
@staticmethod
def _get_version_info_release():
return version.version_info.release
def _update_info_from_version_info(self):
try:
ver = self._get_version_info_version()
if ver:
self._version = ver
except Exception:
pass
try:
rel = self._get_version_info_release()
if rel:
self._release = rel
except Exception:
pass
# RDO, RHEL-OSP, Mirantis on Redhat, SUSE
def _update_info_from_rpm(self):
LOG.debug('Trying rpm command.')
try:
out, err = putils.execute("rpm", "-qa", "--queryformat",
"'%{version}\t%{release}\t%{vendor}'",
self.PACKAGE_NAME)
if not out:
LOG.info(_('No rpm info found for %(pkg)s package.') % {
'pkg': self.PACKAGE_NAME})
return False
parts = out.split()
self._version = parts[0]
self._release = parts[1]
self._vendor = ' '.join(parts[2::])
return True
except Exception as e:
LOG.info(_('Could not run rpm command: %(msg)s.') % {
'msg': e})
return False
# ubuntu, mirantis on ubuntu
def _update_info_from_dpkg(self):
LOG.debug('Trying dpkg-query command.')
try:
_vendor = None
out, err = putils.execute("dpkg-query", "-W", "-f='${Version}'",
self.PACKAGE_NAME)
if not out:
LOG.info(_('No dpkg-query info found for %(pkg)s package.') % {
'pkg': self.PACKAGE_NAME})
return False
# debian format: [epoch:]upstream_version[-debian_revision]
deb_version = out
# in case epoch or revision is missing, copy entire string
_release = deb_version
if ':' in deb_version:
deb_epoch, upstream_version = deb_version.split(':')
_release = upstream_version
if '-' in deb_version:
deb_revision = deb_version.split('-')[1]
_vendor = deb_revision
self._release = _release
if _vendor:
self._vendor = _vendor
return True
except Exception as e:
LOG.info(_('Could not run dpkg-query command: %(msg)s.') % {
'msg': e})
return False
def _update_openstack_info(self):
self._update_version_from_version_string()
self._update_release_from_release_string()
self._update_platform()
# some distributions override with more meaningful information
self._update_info_from_version_info()
# see if we have still more targeted info from rpm or apt
found_package = self._update_info_from_rpm()
if not found_package:
self._update_info_from_dpkg()
def info(self):
self._update_openstack_info()
return '%(version)s|%(release)s|%(vendor)s|%(platform)s' % {
'version': self._version, 'release': self._release,
'vendor': self._vendor, 'platform': self._platform}