362 lines
12 KiB
Python
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)
|