cinder/cinder/volume/drivers/datera/datera_common.py

362 lines
12 KiB
Python

# Copyright 2020 Datera
# 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 functools
import random
import re
import string
import time
import types
import uuid
from glanceclient import exc as glance_exc
from oslo_log import log as logging
from oslo_utils import importutils
from cinder import context
from cinder import exception
from cinder.i18n import _
from cinder.image import glance
from cinder.volume import qos_specs
from cinder.volume import volume_types
LOG = logging.getLogger(__name__)
dfs_sdk = importutils.try_import('dfs_sdk')
OS_PREFIX = "OS"
UNMANAGE_PREFIX = "UNMANAGED"
# Taken from this SO post :
# http://stackoverflow.com/a/18516125
# Using old-style string formatting because of the nature of the regex
# conflicting with new-style curly braces
UUID4_STR_RE = ("%s.*([a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab]"
"[a-f0-9]{3}-?[a-f0-9]{12})")
UUID4_RE = re.compile(UUID4_STR_RE % OS_PREFIX)
SNAP_RE = re.compile(r"\d{10,}\.\d+")
# Recursive dict to assemble basic url structure for the most common
# API URL endpoints. Most others are constructed from these
DEFAULT_SI_SLEEP = 1
DEFAULT_SI_SLEEP_API_2 = 5
DEFAULT_SNAP_SLEEP = 1
API_VERSIONS = ["2.1", "2.2"]
API_TIMEOUT = 20
VALID_CHARS = set(string.ascii_letters + string.digits + "-_.")
class DateraAPIException(exception.VolumeBackendAPIException):
message = _("Bad response from Datera API")
def get_name(resource):
dn = resource.get('display_name')
cid = resource.get('id')
if dn:
dn = filter_chars(dn)
# Check to ensure the name is short enough to fit. Prioritize
# the prefix and Cinder ID, strip all invalid characters
nl = len(OS_PREFIX) + len(dn) + len(cid) + 2
if nl >= 64:
dn = dn[:-(nl - 63)]
return "-".join((OS_PREFIX, dn, cid))
return "-".join((OS_PREFIX, cid))
def get_unmanaged(name):
return "-".join((UNMANAGE_PREFIX, name))
def filter_chars(s):
if s:
return ''.join([c for c in s if c in VALID_CHARS])
return s
def lookup(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
obj = args[0]
name = "_" + func.__name__ + "_" + obj.apiv.replace(".", "_")
LOG.debug("Trying method: %s", name)
call_id = uuid.uuid4()
if obj.do_profile:
LOG.debug("Profiling method: %s, id %s", name, call_id)
t1 = time.time()
obj.thread_local.trace_id = call_id
result = getattr(obj, name)(*args[1:], **kwargs)
if obj.do_profile:
t2 = time.time()
timedelta = round(t2 - t1, 3)
LOG.debug("Profile for method %s, id %s: %ss",
name, call_id, timedelta)
return result
return wrapper
def _parse_vol_ref(ref):
if ref.count(":") not in (2, 3):
raise exception.ManageExistingInvalidReference(
_("existing_ref argument must be of this format: "
"tenant:app_inst_name:storage_inst_name:vol_name or "
"app_inst_name:storage_inst_name:vol_name"))
try:
(tenant, app_inst_name, storage_inst_name,
vol_name) = ref.split(":")
if tenant == "root":
tenant = None
except (TypeError, ValueError):
app_inst_name, storage_inst_name, vol_name = ref.split(
":")
tenant = None
return app_inst_name, storage_inst_name, vol_name, tenant
def _check_snap_ref(ref):
if not SNAP_RE.match(ref):
raise exception.ManageExistingInvalidReference(
_("existing_ref argument must be of this format: "
"1234567890.12345678"))
return True
def _get_size(app_inst):
"""Helper method for getting the size of a backend object
If app_inst is provided, we'll just parse the dict to get
the size instead of making a separate http request
"""
if 'data' in app_inst:
app_inst = app_inst['data']
sis = app_inst['storage_instances']
found_si = sis[0]
found_vol = found_si['volumes'][0]
return found_vol['size']
def _get_volume_type_obj(driver, resource):
type_id = resource.get('volume_type_id', None)
# Handle case of volume with no type. We still want the
# specified defaults from above
if type_id:
ctxt = context.get_admin_context()
volume_type = volume_types.get_volume_type(ctxt, type_id)
else:
volume_type = None
return volume_type
def _get_policies_for_resource(driver, resource):
volume_type = driver._get_volume_type_obj(resource)
return driver._get_policies_for_volume_type(volume_type)
def _get_policies_for_volume_type(driver, volume_type):
"""Get extra_specs and qos_specs of a volume_type.
This fetches the scoped keys from the volume type. Anything set from
qos_specs will override key/values set from extra_specs.
"""
# Handle case of volume with no type. We still want the
# specified defaults from above
if volume_type:
specs = volume_type.get('extra_specs', {})
else:
specs = {}
# Set defaults:
policies = {k.lstrip('DF:'): str(v['default']) for (k, v)
in driver._init_vendor_properties()[0].items()}
if volume_type:
qos_specs_id = volume_type.get('qos_specs_id')
if qos_specs_id is not None:
ctxt = context.get_admin_context()
qos_kvs = qos_specs.get_qos_specs(ctxt, qos_specs_id)['specs']
if qos_kvs:
specs.update(qos_kvs)
# Populate updated value
for key, value in specs.items():
if ':' in key:
fields = key.split(':')
key = fields[1]
policies[key] = value
# Cast everything except booleans int that can be cast
for k, v in policies.items():
# Handle String Boolean case
if v == 'True' or v == 'False':
policies[k] = policies[k] == 'True'
continue
# Int cast
try:
policies[k] = int(v)
except ValueError:
pass
return policies
def _image_accessible(driver, context, volume, image_meta):
# Determine if image is accessible by current project
pid = volume.get('project_id', '')
public = False
visibility = image_meta.get('visibility', None)
LOG.debug("Image %(image)s visibility: %(vis)s",
{"image": image_meta['id'], "vis": visibility})
if visibility and visibility in ['public', 'community']:
public = True
elif visibility and visibility in ['shared', 'private']:
# Do membership check. Newton and before didn't have a 'shared'
# visibility option, so we have to do this check for 'private'
# as well
gclient = glance.get_default_image_service()
members = []
# list_members is only available in Rocky+
try:
members = gclient.list_members(context, image_meta['id'])
except AttributeError:
# This is the fallback method for the same query
try:
members = gclient._client.call(context,
'list',
controller='image_members',
image_id=image_meta['id'])
except glance_exc.HTTPForbidden as e:
LOG.warning(e)
except glance_exc.HTTPForbidden as e:
LOG.warning(e)
members = list(members)
LOG.debug("Shared image %(image)s members: %(members)s",
{"image": image_meta['id'], "members": members})
for member in members:
if (member['member_id'] == pid and
member['status'] == 'accepted'):
public = True
break
if image_meta.get('is_public', False):
public = True
else:
if image_meta.get('owner', '') == pid:
public = True
if not public:
LOG.warning("Requested image is not "
"accessible by current Project.")
return public
def _format_tenant(tenant):
if tenant == "all" or (tenant and ('/root' in tenant or 'root' in tenant)):
return '/root'
elif tenant and ('/root' not in tenant and 'root' not in tenant):
return "/" + "/".join(('root', tenant)).strip('/')
return tenant
def get_ip_pool(policies):
ip_pool = policies['ip_pool']
if ',' in ip_pool:
ip_pools = ip_pool.split(',')
ip_pool = random.choice(ip_pools)
return ip_pool
def create_tenant(driver, project_id):
if driver.tenant_id.lower() == 'map':
name = get_name({'id': project_id})
elif driver.tenant_id:
name = driver.tenant_id.replace('root', '').strip('/')
else:
name = 'root'
if name:
try:
driver.api.tenants.create(name=name)
except dfs_sdk.exceptions.ApiConflictError:
LOG.debug("Tenant {} already exists".format(name))
return _format_tenant(name)
def get_tenant(driver, project_id):
if driver.tenant_id.lower() == 'map':
return _format_tenant(get_name({'id': project_id}))
elif not driver.tenant_id:
return _format_tenant('root')
return _format_tenant(driver.tenant_id)
def cvol_to_ai(driver, resource, tenant=None):
if not tenant:
tenant = get_tenant(driver, resource['project_id'])
try:
# api.tenants.get needs a non '/'-prefixed tenant id
driver.api.tenants.get(tenant.strip('/'))
except dfs_sdk.exceptions.ApiNotFoundError:
create_tenant(driver, resource['project_id'])
cid = resource.get('id', None)
if not cid:
raise ValueError('Unsure what id key to use for object', resource)
ais = driver.api.app_instances.list(
filter='match(name,.*{}.*)'.format(cid),
tenant=tenant)
if not ais:
raise exception.VolumeNotFound(volume_id=cid)
return ais[0]
def cvol_to_dvol(driver, resource, tenant=None):
if not tenant:
tenant = get_tenant(driver, resource['project_id'])
ai = cvol_to_ai(driver, resource, tenant=tenant)
si = ai.storage_instances.list(tenant=tenant)[0]
vol = si.volumes.list(tenant=tenant)[0]
return vol
def _version_to_int(ver):
# Using a factor of 100 per digit so up to 100 versions are supported
# per major/minor/patch/subpatch digit in this calculation
# Example:
# In [2]: _version_to_int("3.3.0.0")
# Out[2]: 303000000
# In [3]: _version_to_int("2.2.7.1")
# Out[3]: 202070100
VERSION_DIGITS = 4
factor = pow(10, VERSION_DIGITS * 2)
div = pow(10, 2)
val = 0
for c in ver.split("."):
val += int(int(c) * factor)
factor /= div
return val
def dat_version_gte(version_a, version_b):
return _version_to_int(version_a) >= _version_to_int(version_b)
def register_driver(driver):
for func in [_get_volume_type_obj,
_get_policies_for_resource,
_get_policies_for_volume_type,
_image_accessible,
get_tenant,
create_tenant,
cvol_to_ai,
cvol_to_dvol]:
f = types.MethodType(func, driver)
setattr(driver, func.__name__, f)