# 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, }