1a5de5d4bd
This patch adds a CI_WIKI_NAME to each driver object. The value is the exact name of the ThirdPartySystems wiki page. This allows us to create an automated tool to associated jobs to drivers and track their CI reporting status correctly. This patch also updates the generate_driver_list.py script to output the driver list as a python list of dicts that can be directly consumed. Change-Id: I0ec5f705e91f680a731648cf50738ea219565f70
600 lines
21 KiB
Python
600 lines
21 KiB
Python
# Copyright 2013-2015 Blockbridge Networks, LLC.
|
|
#
|
|
# 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.
|
|
"""
|
|
Blockbridge EPS iSCSI Volume Driver
|
|
"""
|
|
|
|
import base64
|
|
import socket
|
|
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_serialization import jsonutils
|
|
from oslo_utils import units
|
|
import six
|
|
from six.moves import http_client
|
|
from six.moves import urllib
|
|
|
|
from cinder import context
|
|
from cinder import exception
|
|
from cinder.i18n import _
|
|
from cinder import interface
|
|
from cinder.volume import driver
|
|
from cinder.volume import utils as volume_utils
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
blockbridge_opts = [
|
|
cfg.StrOpt("blockbridge_api_host",
|
|
help="IP address/hostname of Blockbridge API."),
|
|
cfg.IntOpt("blockbridge_api_port",
|
|
help="Override HTTPS port to connect to Blockbridge "
|
|
"API server."),
|
|
cfg.StrOpt("blockbridge_auth_scheme",
|
|
default='token',
|
|
choices=['token', 'password'],
|
|
help="Blockbridge API authentication scheme (token "
|
|
"or password)"),
|
|
cfg.StrOpt("blockbridge_auth_token",
|
|
help="Blockbridge API token (for auth scheme 'token')",
|
|
secret=True),
|
|
cfg.StrOpt("blockbridge_auth_user",
|
|
help="Blockbridge API user (for auth scheme 'password')"),
|
|
cfg.StrOpt("blockbridge_auth_password",
|
|
help="Blockbridge API password (for auth scheme 'password')",
|
|
secret=True),
|
|
cfg.DictOpt("blockbridge_pools",
|
|
default={'OpenStack': '+openstack'},
|
|
help="Defines the set of exposed pools and their associated "
|
|
"backend query strings"),
|
|
cfg.StrOpt("blockbridge_default_pool",
|
|
help="Default pool name if unspecified."),
|
|
]
|
|
|
|
CONF = cfg.CONF
|
|
CONF.register_opts(blockbridge_opts)
|
|
|
|
|
|
class BlockbridgeAPIClient(object):
|
|
_api_cfg = None
|
|
|
|
def __init__(self, configuration=None):
|
|
self.configuration = configuration
|
|
|
|
def _get_api_cfg(self):
|
|
if self._api_cfg:
|
|
# return cached configuration
|
|
return self._api_cfg
|
|
|
|
if self.configuration.blockbridge_auth_scheme == 'password':
|
|
user = self.configuration.safe_get('blockbridge_auth_user')
|
|
pw = self.configuration.safe_get('blockbridge_auth_password')
|
|
creds = "%s:%s" % (user, pw)
|
|
if six.PY3:
|
|
creds = creds.encode('utf-8')
|
|
b64_creds = base64.encodestring(creds).decode('ascii')
|
|
else:
|
|
b64_creds = base64.encodestring(creds)
|
|
authz = "Basic %s" % b64_creds.replace("\n", "")
|
|
elif self.configuration.blockbridge_auth_scheme == 'token':
|
|
token = self.configuration.blockbridge_auth_token or ''
|
|
authz = "Bearer %s" % token
|
|
|
|
# set and return cached api cfg
|
|
self._api_cfg = {
|
|
'host': self.configuration.blockbridge_api_host,
|
|
'port': self.configuration.blockbridge_api_port,
|
|
'base_url': '/api/cinder',
|
|
'default_headers': {
|
|
'User-Agent': ("cinder-volume/%s" %
|
|
BlockbridgeISCSIDriver.VERSION),
|
|
'Accept': 'application/vnd.blockbridge-3+json',
|
|
'Authorization': authz,
|
|
},
|
|
}
|
|
|
|
return self._api_cfg
|
|
|
|
def submit(self, rel_url, method='GET', params=None, user_id=None,
|
|
project_id=None, req_id=None, action=None, **kwargs):
|
|
"""Submit a request to the configured API endpoint."""
|
|
|
|
cfg = self._get_api_cfg()
|
|
if cfg is None:
|
|
msg = _("Failed to determine blockbridge API configuration")
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
# alter the url appropriately if an action is requested
|
|
if action:
|
|
rel_url += "/actions/%s" % action
|
|
|
|
headers = cfg['default_headers'].copy()
|
|
url = cfg['base_url'] + rel_url
|
|
body = None
|
|
|
|
# include user, project and req-id, if supplied
|
|
tsk_ctx = []
|
|
if user_id and project_id:
|
|
tsk_ctx.append("ext_auth=keystone/%s/%s" % (project_id, user_id))
|
|
if req_id:
|
|
tsk_ctx.append("id=%s", req_id)
|
|
|
|
if tsk_ctx:
|
|
headers['X-Blockbridge-Task'] = ','.join(tsk_ctx)
|
|
|
|
# encode params based on request method
|
|
if method in ['GET', 'DELETE']:
|
|
# For GET method add parameters to the URL
|
|
if params:
|
|
url += '?' + urllib.parse.urlencode(params)
|
|
elif method in ['POST', 'PUT', 'PATCH']:
|
|
body = jsonutils.dumps(params)
|
|
headers['Content-Type'] = 'application/json'
|
|
else:
|
|
raise exception.UnknownCmd(cmd=method)
|
|
|
|
# connect and execute the request
|
|
connection = http_client.HTTPSConnection(cfg['host'], cfg['port'])
|
|
connection.request(method, url, body, headers)
|
|
response = connection.getresponse()
|
|
|
|
# read response data
|
|
rsp_body = response.read()
|
|
rsp_data = jsonutils.loads(rsp_body)
|
|
|
|
connection.close()
|
|
|
|
code = response.status
|
|
if code in [200, 201, 202, 204]:
|
|
pass
|
|
elif code == 401:
|
|
raise exception.NotAuthorized(_("Invalid credentials"))
|
|
elif code == 403:
|
|
raise exception.NotAuthorized(_("Insufficient privileges"))
|
|
else:
|
|
raise exception.VolumeBackendAPIException(data=rsp_data['message'])
|
|
|
|
return rsp_data
|
|
|
|
|
|
@interface.volumedriver
|
|
class BlockbridgeISCSIDriver(driver.ISCSIDriver):
|
|
"""Manages volumes hosted on Blockbridge EPS."""
|
|
|
|
VERSION = '1.3.0'
|
|
|
|
# ThirdPartySystems wiki page
|
|
CI_WIKI_NAME = "Blockbridge_EPS_CI"
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(BlockbridgeISCSIDriver, self).__init__(*args, **kwargs)
|
|
|
|
self.client = kwargs.get('client', None) or (
|
|
BlockbridgeAPIClient(configuration=self.configuration))
|
|
|
|
self.configuration.append_config_values(blockbridge_opts)
|
|
self.hostname = socket.gethostname()
|
|
|
|
def do_setup(self, context):
|
|
"""Set up the Blockbridge volume driver."""
|
|
pass
|
|
|
|
def check_for_setup_error(self):
|
|
"""Verify configuration is valid."""
|
|
|
|
# ensure the host is configured
|
|
if self.configuration.safe_get('blockbridge_api_host') is None:
|
|
raise exception.InvalidInput(
|
|
reason=_("Blockbridge api host not configured"))
|
|
|
|
# ensure the auth scheme is valid and has the necessary configuration.
|
|
auth_scheme = self.configuration.safe_get("blockbridge_auth_scheme")
|
|
|
|
if auth_scheme == 'password':
|
|
auth_user = self.configuration.safe_get('blockbridge_auth_user')
|
|
auth_pw = self.configuration.safe_get('blockbridge_auth_password')
|
|
if auth_user is None:
|
|
raise exception.InvalidInput(
|
|
reason=_("Blockbridge user not configured (required for "
|
|
"auth scheme 'password')"))
|
|
if auth_pw is None:
|
|
raise exception.InvalidInput(
|
|
reason=_("Blockbridge password not configured (required "
|
|
"for auth scheme 'password')"))
|
|
elif auth_scheme == 'token':
|
|
token = self.configuration.safe_get('blockbridge_auth_token')
|
|
if token is None:
|
|
raise exception.InvalidInput(
|
|
reason=_("Blockbridge token not configured (required "
|
|
"for auth scheme 'token')"))
|
|
else:
|
|
raise exception.InvalidInput(
|
|
reason=(_("Blockbridge configured with invalid auth scheme "
|
|
"'%(auth_scheme)s'") % {'auth_scheme': auth_scheme}))
|
|
|
|
# ensure at least one pool is defined
|
|
pools = self.configuration.safe_get('blockbridge_pools')
|
|
if pools is None:
|
|
raise exception.InvalidInput(
|
|
reason=_("Blockbridge pools not configured"))
|
|
|
|
default_pool = self.configuration.safe_get('blockbridge_default_pool')
|
|
if default_pool and default_pool not in pools:
|
|
raise exception.InvalidInput(
|
|
reason=_("Blockbridge default pool does not exist"))
|
|
|
|
def _vol_api_submit(self, vol_id, **kwargs):
|
|
vol_id = urllib.parse.quote(vol_id, '')
|
|
rel_url = "/volumes/%s" % vol_id
|
|
|
|
return self.client.submit(rel_url, **kwargs)
|
|
|
|
def _create_volume(self, vol_id, params, **kwargs):
|
|
"""Execute a backend volume create operation."""
|
|
|
|
self._vol_api_submit(vol_id, method='PUT', params=params, **kwargs)
|
|
|
|
def _delete_volume(self, vol_id, **kwargs):
|
|
"""Execute a backend volume delete operation."""
|
|
|
|
self._vol_api_submit(vol_id, method='DELETE', **kwargs)
|
|
|
|
def _extend_volume(self, vol_id, capacity, **kwargs):
|
|
"""Execute a backend volume grow operation."""
|
|
|
|
params = kwargs.get('params', {})
|
|
params['capacity'] = capacity
|
|
|
|
self._vol_api_submit(vol_id, method='POST', action='grow',
|
|
params=params, **kwargs)
|
|
|
|
def _snap_api_submit(self, vol_id, snap_id, **kwargs):
|
|
vol_id = urllib.parse.quote(vol_id, '')
|
|
snap_id = urllib.parse.quote(snap_id, '')
|
|
rel_url = "/volumes/%s/snapshots/%s" % (vol_id, snap_id)
|
|
|
|
return self.client.submit(rel_url, **kwargs)
|
|
|
|
def _create_snapshot(self, vol_id, snap_id, params, **kwargs):
|
|
"""Execute a backend snapshot create operation."""
|
|
|
|
self._snap_api_submit(vol_id, snap_id, method='PUT',
|
|
params=params, **kwargs)
|
|
|
|
def _delete_snapshot(self, vol_id, snap_id, **kwargs):
|
|
"""Execute a backend snapshot delete operation."""
|
|
|
|
return self._snap_api_submit(vol_id, snap_id, method='DELETE',
|
|
**kwargs)
|
|
|
|
def _export_api_submit(self, vol_id, ini_name, **kwargs):
|
|
vol_id = urllib.parse.quote(vol_id, '')
|
|
ini_name = urllib.parse.quote(ini_name, '')
|
|
rel_url = "/volumes/%s/exports/%s" % (vol_id, ini_name)
|
|
|
|
return self.client.submit(rel_url, **kwargs)
|
|
|
|
def _create_export(self, vol_id, ini_name, params, **kwargs):
|
|
"""Execute a backend volume export operation."""
|
|
|
|
return self._export_api_submit(vol_id, ini_name, method='PUT',
|
|
params=params, **kwargs)
|
|
|
|
def _delete_export(self, vol_id, ini_name, **kwargs):
|
|
"""Remove a previously created volume export."""
|
|
|
|
self._export_api_submit(vol_id, ini_name, method='DELETE',
|
|
**kwargs)
|
|
|
|
def _get_pool_stats(self, pool, query, **kwargs):
|
|
"""Retrieve pool statistics and capabilities."""
|
|
|
|
pq = {
|
|
'pool': pool,
|
|
'query': query,
|
|
}
|
|
pq.update(kwargs)
|
|
|
|
return self.client.submit('/status', params=pq)
|
|
|
|
def _get_dbref_name(self, ref):
|
|
display_name = ref.get('display_name')
|
|
if not display_name:
|
|
return ref.get('name')
|
|
return display_name
|
|
|
|
def _get_query_string(self, ctxt, volume):
|
|
pools = self.configuration.blockbridge_pools
|
|
default_pool = self.configuration.blockbridge_default_pool
|
|
explicit_pool = volume_utils.extract_host(volume['host'], 'pool')
|
|
|
|
pool_name = explicit_pool or default_pool
|
|
if pool_name:
|
|
return pools[pool_name]
|
|
else:
|
|
# no pool specified or defaulted -- just pick whatever comes out of
|
|
# the dictionary first.
|
|
return list(pools.values())[0]
|
|
|
|
def create_volume(self, volume):
|
|
"""Create a volume on a Blockbridge EPS backend.
|
|
|
|
:param volume: volume reference
|
|
"""
|
|
|
|
ctxt = context.get_admin_context()
|
|
create_params = {
|
|
'name': self._get_dbref_name(volume),
|
|
'query': self._get_query_string(ctxt, volume),
|
|
'capacity': int(volume['size'] * units.Gi),
|
|
}
|
|
|
|
LOG.debug("Provisioning %(capacity)s byte volume "
|
|
"with query '%(query)s'", create_params, resource=volume)
|
|
|
|
return self._create_volume(volume['id'],
|
|
create_params,
|
|
user_id=volume['user_id'],
|
|
project_id=volume['project_id'])
|
|
|
|
def create_cloned_volume(self, volume, src_vref):
|
|
"""Creates a clone of the specified volume."""
|
|
|
|
create_params = {
|
|
'name': self._get_dbref_name(volume),
|
|
'capacity': int(volume['size'] * units.Gi),
|
|
'src': {
|
|
'volume_id': src_vref['id'],
|
|
},
|
|
}
|
|
|
|
LOG.debug("Cloning source volume %(id)s", src_vref, resource=volume)
|
|
|
|
return self._create_volume(volume['id'],
|
|
create_params,
|
|
user_id=volume['user_id'],
|
|
project_id=volume['project_id'])
|
|
|
|
def delete_volume(self, volume):
|
|
"""Remove an existing volume.
|
|
|
|
:param volume: volume reference
|
|
"""
|
|
|
|
LOG.debug("Removing volume %(id)s", volume, resource=volume)
|
|
|
|
return self._delete_volume(volume['id'],
|
|
user_id=volume['user_id'],
|
|
project_id=volume['project_id'])
|
|
|
|
def create_snapshot(self, snapshot):
|
|
"""Create snapshot of existing volume.
|
|
|
|
:param snapshot: shapshot reference
|
|
"""
|
|
|
|
create_params = {
|
|
'name': self._get_dbref_name(snapshot),
|
|
}
|
|
|
|
LOG.debug("Creating snapshot of volume %(volume_id)s", snapshot,
|
|
resource=snapshot)
|
|
|
|
return self._create_snapshot(snapshot['volume_id'],
|
|
snapshot['id'],
|
|
create_params,
|
|
user_id=snapshot['user_id'],
|
|
project_id=snapshot['project_id'])
|
|
|
|
def create_volume_from_snapshot(self, volume, snapshot):
|
|
"""Create new volume from existing snapshot.
|
|
|
|
:param volume: reference of volume to be created
|
|
:param snapshot: reference of source snapshot
|
|
"""
|
|
|
|
create_params = {
|
|
'name': self._get_dbref_name(volume),
|
|
'capacity': int(volume['size'] * units.Gi),
|
|
'src': {
|
|
'volume_id': snapshot['volume_id'],
|
|
'snapshot_id': snapshot['id'],
|
|
},
|
|
}
|
|
|
|
LOG.debug("Creating volume from snapshot %(id)s", snapshot,
|
|
resource=volume)
|
|
|
|
return self._create_volume(volume['id'],
|
|
create_params,
|
|
user_id=volume['user_id'],
|
|
project_id=volume['project_id'])
|
|
|
|
def delete_snapshot(self, snapshot):
|
|
"""Delete volume's snapshot.
|
|
|
|
:param snapshot: shapshot reference
|
|
"""
|
|
|
|
LOG.debug("Deleting snapshot of volume %(volume_id)s", snapshot,
|
|
resource=snapshot)
|
|
|
|
self._delete_snapshot(snapshot['volume_id'],
|
|
snapshot['id'],
|
|
user_id=snapshot['user_id'],
|
|
project_id=snapshot['project_id'])
|
|
|
|
def create_export(self, _ctx, volume, connector):
|
|
"""Do nothing: target created during instance attachment."""
|
|
pass
|
|
|
|
def ensure_export(self, _ctx, volume):
|
|
"""Do nothing: target created during instance attachment."""
|
|
pass
|
|
|
|
def remove_export(self, _ctx, volume):
|
|
"""Do nothing: target created during instance attachment."""
|
|
pass
|
|
|
|
def initialize_connection(self, volume, connector, **kwargs):
|
|
"""Attach volume to initiator/host.
|
|
|
|
Creates a profile for the initiator, and adds the new profile to the
|
|
target ACL.
|
|
|
|
"""
|
|
|
|
# generate a CHAP secret here -- there is no way to retrieve an
|
|
# existing CHAP secret over the Blockbridge API, so it must be
|
|
# supplied by the volume driver.
|
|
export_params = {
|
|
'chap_user': (
|
|
kwargs.get('user', volume_utils.generate_username(16))),
|
|
'chap_secret': (
|
|
kwargs.get('password', volume_utils.generate_password(32))),
|
|
}
|
|
|
|
LOG.debug("Configuring export for %(initiator)s", connector,
|
|
resource=volume)
|
|
|
|
rsp = self._create_export(volume['id'],
|
|
connector['initiator'],
|
|
export_params,
|
|
user_id=volume['user_id'],
|
|
project_id=volume['project_id'])
|
|
|
|
# combine locally generated chap credentials with target iqn/lun to
|
|
# present the attach properties.
|
|
target_portal = "%s:%s" % (rsp['target_ip'], rsp['target_port'])
|
|
|
|
properties = {
|
|
'target_discovered': False,
|
|
'target_portal': target_portal,
|
|
'target_iqn': rsp['target_iqn'],
|
|
'target_lun': rsp['target_lun'],
|
|
'volume_id': volume['id'],
|
|
'auth_method': 'CHAP',
|
|
'auth_username': rsp['initiator_login'],
|
|
'auth_password': export_params['chap_secret'],
|
|
}
|
|
|
|
LOG.debug("Attach properties: %(properties)s",
|
|
{'properties': properties})
|
|
|
|
return {
|
|
'driver_volume_type': 'iscsi',
|
|
'data': properties,
|
|
}
|
|
|
|
def terminate_connection(self, volume, connector, **kwargs):
|
|
"""Detach volume from the initiator.
|
|
|
|
Removes initiator profile entry from target ACL.
|
|
|
|
"""
|
|
|
|
LOG.debug("Unconfiguring export for %(initiator)s", connector,
|
|
resource=volume)
|
|
|
|
self._delete_export(volume['id'],
|
|
connector['initiator'],
|
|
user_id=volume['user_id'],
|
|
project_id=volume['project_id'])
|
|
|
|
def extend_volume(self, volume, new_size):
|
|
"""Extend an existing volume."""
|
|
|
|
capacity = new_size * units.Gi
|
|
|
|
LOG.debug("Extending volume to %(capacity)s bytes",
|
|
{'capacity': capacity}, resource=volume)
|
|
|
|
self._extend_volume(volume['id'],
|
|
int(new_size * units.Gi),
|
|
user_id=volume['user_id'],
|
|
project_id=volume['project_id'])
|
|
|
|
def get_volume_stats(self, refresh=False):
|
|
if refresh:
|
|
self._update_volume_stats()
|
|
return self._stats
|
|
|
|
def _update_volume_stats(self):
|
|
if self.configuration:
|
|
cfg_name = self.configuration.safe_get('volume_backend_name')
|
|
backend_name = cfg_name or self.__class__.__name__
|
|
|
|
driver_cfg = {
|
|
'hostname': self.hostname,
|
|
'version': self.VERSION,
|
|
'backend_name': backend_name,
|
|
}
|
|
|
|
filter_function = self.get_filter_function()
|
|
goodness_function = self.get_goodness_function()
|
|
pools = []
|
|
|
|
LOG.debug("Updating volume driver statistics",
|
|
resource={'type': 'driver', 'id': backend_name})
|
|
|
|
for pool_name, query in self.configuration.blockbridge_pools.items():
|
|
stats = self._get_pool_stats(pool_name, query, **driver_cfg)
|
|
|
|
system_serial = stats.get('system_serial', 'unknown')
|
|
free_capacity = stats.get('free_capacity', None)
|
|
total_capacity = stats.get('total_capacity', None)
|
|
provisioned_capacity = stats.get('provisioned_capacity', None)
|
|
|
|
if free_capacity is None:
|
|
free_capacity = 'unknown'
|
|
else:
|
|
free_capacity = int(free_capacity / units.Gi)
|
|
|
|
if total_capacity is None:
|
|
total_capacity = 'unknown'
|
|
else:
|
|
total_capacity = int(total_capacity / units.Gi)
|
|
|
|
pool = {
|
|
'pool_name': pool_name,
|
|
'location_info': ('BlockbridgeDriver:%(sys_id)s:%(pool)s' %
|
|
{'sys_id': system_serial,
|
|
'pool': pool_name}),
|
|
'max_over_subscription_ratio': (
|
|
self.configuration.safe_get('max_over_subscription_ratio')
|
|
),
|
|
'free_capacity_gb': free_capacity,
|
|
'total_capacity_gb': total_capacity,
|
|
'reserved_percentage': 0,
|
|
'thin_provisioning_support': True,
|
|
'filter_function': filter_function,
|
|
'goodness_function': goodness_function,
|
|
}
|
|
|
|
if provisioned_capacity is not None:
|
|
pool['provisioned_capacity_gb'] = int(
|
|
provisioned_capacity / units.Gi
|
|
)
|
|
|
|
pools.append(pool)
|
|
|
|
self._stats = {
|
|
'volume_backend_name': backend_name,
|
|
'vendor_name': 'Blockbridge',
|
|
'driver_version': self.VERSION,
|
|
'storage_protocol': 'iSCSI',
|
|
'pools': pools,
|
|
}
|