--- /dev/null
+# Copyright (c) 2013 - 2015 EMC Corporation.
+# 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.
+"""
+Driver for EMC ScaleIO based on ScaleIO remote CLI.
+"""
+
+import base64
+import json
+import os
+
+import eventlet
+from oslo_concurrency import processutils
+from oslo_config import cfg
+from oslo_log import log as logging
+from oslo_utils import units
+import requests
+import six
+import urllib
+
+from cinder import context
+from cinder import exception
+from cinder.i18n import _, _LI, _LW
+from cinder.image import image_utils
+from cinder import utils
+from cinder.volume import driver
+from cinder.volume.drivers.san import san
+from cinder.volume import volume_types
+
+CONF = cfg.CONF
+
+LOG = logging.getLogger(__name__)
+
+scaleio_opts = [
+ cfg.StrOpt('sio_rest_server_port',
+ default='443',
+ help='REST server port.'),
+ cfg.BoolOpt('sio_verify_server_certificate',
+ default=False,
+ help='Whether to verify server certificate.'),
+ cfg.StrOpt('sio_server_certificate_path',
+ default=None,
+ help='Server certificate path.'),
+ cfg.BoolOpt('sio_round_volume_capacity',
+ default=True,
+ help='Whether to round volume capacity.'),
+ cfg.BoolOpt('sio_force_delete',
+ default=False,
+ help='Whether to allow force delete.'),
+ cfg.BoolOpt('sio_unmap_volume_before_deletion',
+ default=False,
+ help='Whether to unmap volume before deletion.'),
+ cfg.StrOpt('sio_protection_domain_id',
+ default=None,
+ help='Protection domain id.'),
+ cfg.StrOpt('sio_protection_domain_name',
+ default=None,
+ help='Protection domain name.'),
+ cfg.StrOpt('sio_storage_pools',
+ default=None,
+ help='Storage pools.'),
+ cfg.StrOpt('sio_storage_pool_name',
+ default=None,
+ help='Storage pool name.'),
+ cfg.StrOpt('sio_storage_pool_id',
+ default=None,
+ help='Storage pool id.')
+]
+
+CONF.register_opts(scaleio_opts)
+
+STORAGE_POOL_NAME = 'sio:sp_name'
+STORAGE_POOL_ID = 'sio:sp_id'
+PROTECTION_DOMAIN_NAME = 'sio:pd_name'
+PROTECTION_DOMAIN_ID = 'sio:pd_id'
+PROVISIONING_KEY = 'sio:provisioning'
+IOPS_LIMIT_KEY = 'sio:iops_limit'
+BANDWIDTH_LIMIT = 'sio:bandwidth_limit'
+
+BLOCK_SIZE = 8
+OK_STATUS_CODE = 200
+VOLUME_NOT_FOUND_ERROR = 3
+VOLUME_NOT_MAPPED_ERROR = 84
+VOLUME_ALREADY_MAPPED_ERROR = 81
+
+
+class ScaleIODriver(driver.VolumeDriver):
+ """EMC ScaleIO Driver."""
+
+ VERSION = "2.0"
+
+ def __init__(self, *args, **kwargs):
+ super(ScaleIODriver, self).__init__(*args, **kwargs)
+
+ self.configuration.append_config_values(san.san_opts)
+ self.configuration.append_config_values(scaleio_opts)
+ self.server_ip = self.configuration.san_ip
+ self.server_port = self.configuration.sio_rest_server_port
+ self.server_username = self.configuration.san_login
+ self.server_password = self.configuration.san_password
+ self.server_token = None
+ self.verify_server_certificate = (
+ self.configuration.sio_verify_server_certificate)
+ self.server_certificate_path = None
+ if self.verify_server_certificate:
+ self.server_certificate_path = (
+ self.configuration.sio_server_certificate_path)
+ LOG.info(_LI(
+ "REST server IP: %(ip)s, port: %(port)s, username: %(user)s. "
+ "Verify server's certificate: %(verify_cert)s."),
+ {'ip': self.server_ip,
+ 'port': self.server_port,
+ 'user': self.server_username,
+ 'verify_cert': self.verify_server_certificate})
+
+ self.storage_pools = [e.strip() for e in
+ self.configuration.sio_storage_pools.split(',')]
+ self.storage_pool_name = self.configuration.sio_storage_pool_name
+ self.storage_pool_id = self.configuration.sio_storage_pool_id
+ if (self.storage_pool_name is None and self.storage_pool_id is None):
+ LOG.warning(_LW("No storage pool name or id was found."))
+ else:
+ LOG.info(_LI(
+ "Storage pools names: %(pools)s, "
+ "storage pool name: %(pool)s, pool id: %(pool_id)s."),
+ {'pools': self.storage_pools,
+ 'pool': self.storage_pool_name,
+ 'pool_id': self.storage_pool_id})
+
+ self.protection_domain_name = (
+ self.configuration.sio_protection_domain_name)
+ LOG.info(_LI(
+ "Protection domain name: %(domain_name)s."),
+ {'domain_name': self.protection_domain_name})
+ self.protection_domain_id = self.configuration.sio_protection_domain_id
+ LOG.info(_LI(
+ "Protection domain name: %(domain_id)s."),
+ {'domain_id': self.protection_domain_id})
+
+ def check_for_setup_error(self):
+ if (not self.protection_domain_name and
+ not self.protection_domain_id):
+ LOG.warning(_LW("No protection domain name or id "
+ "was specified in configuration."))
+
+ if self.protection_domain_name and self.protection_domain_id:
+ msg = _("Cannot specify both protection domain name "
+ "and protection domain id.")
+ raise exception.InvalidInput(reason=msg)
+
+ if not self.server_ip:
+ msg = _("REST server IP must by specified.")
+ raise exception.InvalidInput(reason=msg)
+
+ if not self.server_username:
+ msg = _("REST server username must by specified.")
+ raise exception.InvalidInput(reason=msg)
+
+ if not self.server_password:
+ msg = _("REST server password must by specified.")
+ raise exception.InvalidInput(reason=msg)
+
+ if not self.verify_server_certificate:
+ LOG.warning(_LW("Verify certificate is not set, using default of "
+ "False."))
+
+ if self.verify_server_certificate and not self.server_certificate_path:
+ msg = _("Path to REST server's certificate must be specified.")
+ raise exception.InvalidInput(reason=msg)
+
+ if self.storage_pool_name and self.storage_pool_id:
+ msg = _("Cannot specify both storage pool name and storage "
+ "pool id.")
+ raise exception.InvalidInput(reason=msg)
+
+ if not self.storage_pool_name and not self.storage_pool_id:
+ msg = _("Must specify storage pool name or id.")
+ raise exception.InvalidInput(reason=msg)
+
+ def _find_storage_pool_id_from_storage_type(self, storage_type):
+ # Default to what was configured in configuration file if not defined.
+ return storage_type.get(STORAGE_POOL_ID,
+ self.storage_pool_id)
+
+ def _find_storage_pool_name_from_storage_type(self, storage_type):
+ return storage_type.get(STORAGE_POOL_NAME,
+ self.storage_pool_name)
+
+ def _find_protection_domain_id_from_storage_type(self, storage_type):
+ # Default to what was configured in configuration file if not defined.
+ return storage_type.get(PROTECTION_DOMAIN_ID,
+ self.protection_domain_id)
+
+ def _find_protection_domain_name_from_storage_type(self, storage_type):
+ # Default to what was configured in configuration file if not defined.
+ return storage_type.get(PROTECTION_DOMAIN_NAME,
+ self.protection_domain_name)
+
+ def _find_provisioning_type(self, storage_type):
+ return storage_type.get(PROVISIONING_KEY)
+
+ def _find_iops_limit(self, storage_type):
+ return storage_type.get(IOPS_LIMIT_KEY)
+
+ def _find_bandwidth_limit(self, storage_type):
+ return storage_type.get(BANDWIDTH_LIMIT)
+
+ def id_to_base64(self, id):
+ # Base64 encode the id to get a volume name less than 32 characters due
+ # to ScaleIO limitation.
+ name = six.text_type(id).replace("-", "")
+ try:
+ name = base64.b16decode(name.upper())
+ except TypeError:
+ pass
+ encoded_name = base64.b64encode(name)
+ LOG.debug(
+ "Converted id %(id)s to scaleio name %(name)s.",
+ {'id': id, 'name': encoded_name})
+ return encoded_name
+
+ def create_volume(self, volume):
+ """Creates a scaleIO volume."""
+ self._check_volume_size(volume.size)
+
+ volname = self.id_to_base64(volume.id)
+
+ storage_type = self._get_volumetype_extraspecs(volume)
+ storage_pool_name = self._find_storage_pool_name_from_storage_type(
+ storage_type)
+ storage_pool_id = self._find_storage_pool_id_from_storage_type(
+ storage_type)
+ protection_domain_id = (
+ self._find_protection_domain_id_from_storage_type(storage_type))
+ protection_domain_name = (
+ self._find_protection_domain_name_from_storage_type(storage_type))
+ provisioning_type = self._find_provisioning_type(storage_type)
+
+ LOG.info(_LI(
+ "Volume type: %(volume_type)s, storage pool name: %(pool_name)s, "
+ "storage pool id: %(pool_id)s, protection domain id: "
+ "%(domain_id)s, protection domain name: %(domain_name)s."),
+ {'volume_type': storage_type,
+ 'pool_name': storage_pool_name,
+ 'pool_id': storage_pool_id,
+ 'domain_id': protection_domain_id,
+ 'domain_name': protection_domain_name})
+
+ verify_cert = self._get_verify_cert()
+
+ if storage_pool_name:
+ self.storage_pool_name = storage_pool_name
+ self.storage_pool_id = None
+ if storage_pool_id:
+ self.storage_pool_id = storage_pool_id
+ self.storage_pool_name = None
+ if protection_domain_name:
+ self.protection_domain_name = protection_domain_name
+ self.protection_domain_id = None
+ if protection_domain_id:
+ self.protection_domain_id = protection_domain_id
+ self.protection_domain_name = None
+
+ domain_id = self.protection_domain_id
+ if not domain_id:
+ if not self.protection_domain_name:
+ msg = _("Must specify protection domain name or"
+ " protection domain id.")
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ encoded_domain_name = urllib.quote(self.protection_domain_name, '')
+ req_vars = {'server_ip': self.server_ip,
+ 'server_port': self.server_port,
+ 'encoded_domain_name': encoded_domain_name}
+ request = ("https://%(server_ip)s:%(server_port)s"
+ "/api/types/Domain/instances/getByName::"
+ "%(encoded_domain_name)s") % req_vars
+ LOG.info(_LI("ScaleIO get domain id by name request: %s."),
+ request)
+ r = requests.get(
+ request,
+ auth=(
+ self.server_username,
+ self.server_token),
+ verify=verify_cert)
+ r = self._check_response(r, request)
+
+ domain_id = r.json()
+ if not domain_id:
+ msg = (_("Domain with name %s wasn't found.")
+ % self.protection_domain_name)
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+ if r.status_code != OK_STATUS_CODE and "errorCode" in domain_id:
+ msg = (_("Error getting domain id from name %(name)s: %(id)s.")
+ % {'name': self.protection_domain_name,
+ 'id': domain_id['message']})
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ LOG.info(_LI("Domain id is %s."), domain_id)
+ pool_name = self.storage_pool_name
+ pool_id = self.storage_pool_id
+ if pool_name:
+ encoded_domain_name = urllib.quote(pool_name, '')
+ req_vars = {'server_ip': self.server_ip,
+ 'server_port': self.server_port,
+ 'domain_id': domain_id,
+ 'encoded_domain_name': encoded_domain_name}
+ request = ("https://%(server_ip)s:%(server_port)s"
+ "/api/types/Pool/instances/getByName::"
+ "%(domain_id)s,%(encoded_domain_name)s") % req_vars
+ LOG.info(_LI("ScaleIO get pool id by name request: %s."), request)
+ r = requests.get(
+ request,
+ auth=(
+ self.server_username,
+ self.server_token),
+ verify=verify_cert)
+ pool_id = r.json()
+ if not pool_id:
+ msg = (_("Pool with name %(pool_name)s wasn't found in "
+ "domain %(domain_id)s.")
+ % {'pool_name': pool_name,
+ 'domain_id': domain_id})
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+ if r.status_code != OK_STATUS_CODE and "errorCode" in pool_id:
+ msg = (_("Error getting pool id from name %(pool_name)s: "
+ "%(err_msg)s.")
+ % {'pool_name': pool_name,
+ 'err_msg': pool_id['message']})
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ LOG.info(_LI("Pool id is %s."), pool_id)
+ if provisioning_type == 'thin':
+ provisioning = "ThinProvisioned"
+ # Default volume type is thick.
+ else:
+ provisioning = "ThickProvisioned"
+
+ # units.Mi = 1024 ** 2
+ volume_size_kb = volume.size * units.Mi
+ params = {'protectionDomainId': domain_id,
+ 'volumeSizeInKb': six.text_type(volume_size_kb),
+ 'name': volname,
+ 'volumeType': provisioning,
+ 'storagePoolId': pool_id}
+
+ LOG.info(_LI("Params for add volume request: %s."), params)
+ r = requests.post(
+ "https://" +
+ self.server_ip +
+ ":" +
+ self.server_port +
+ "/api/types/Volume/instances",
+ data=json.dumps(params),
+ headers=self._get_headers(),
+ auth=(
+ self.server_username,
+ self.server_token),
+ verify=verify_cert)
+ response = r.json()
+ LOG.info(_LI("Add volume response: %s"), response)
+
+ if r.status_code != OK_STATUS_CODE and "errorCode" in response:
+ msg = (_("Error creating volume: %s.") % response['message'])
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ LOG.info(_LI("Created volume %(volname)s, volume id %(volid)s."),
+ {'volname': volname, 'volid': volume.id})
+
+ def _check_volume_size(self, size):
+ if size % 8 != 0:
+ round_volume_capacity = (
+ self.configuration.sio_round_volume_capacity)
+ if not round_volume_capacity:
+ exception_msg = (_(
+ "Cannot create volume of size %s: not multiple of 8GB.") %
+ size)
+ LOG.error(exception_msg)
+ raise exception.VolumeBackendAPIException(data=exception_msg)
+
+ def create_snapshot(self, snapshot):
+ """Creates a scaleio snapshot."""
+ volname = self.id_to_base64(snapshot.volume_id)
+ snapname = self.id_to_base64(snapshot.id)
+ self._snapshot_volume(volname, snapname)
+
+ def _snapshot_volume(self, volname, snapname):
+ vol_id = self._get_volume_id(volname)
+ params = {
+ 'snapshotDefs': [{"volumeId": vol_id, "snapshotName": snapname}]}
+ req_vars = {'server_ip': self.server_ip,
+ 'server_port': self.server_port}
+ request = ("https://%(server_ip)s:%(server_port)s"
+ "/api/instances/System/action/snapshotVolumes") % req_vars
+ r = requests.post(
+ request,
+ data=json.dumps(params),
+ headers=self._get_headers(),
+ auth=(
+ self.server_username,
+ self.server_token),
+ verify=self._get_verify_cert())
+ r = self._check_response(r, request, False, params)
+ response = r.json()
+ LOG.info(_LI("snapshot volume response: %s."), response)
+ if r.status_code != OK_STATUS_CODE and "errorCode" in response:
+ msg = (_("Failed creating snapshot for volume %(volname)s: "
+ "%(response)s.") %
+ {'volname': volname,
+ 'response': response['message']})
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ def _check_response(self, response, request, is_get_request=True,
+ params=None):
+ if response.status_code == 401 or response.status_code == 403:
+ LOG.info(_LI("Token is invalid, going to re-login and get "
+ "a new one."))
+ login_request = (
+ "https://" + self.server_ip +
+ ":" + self.server_port + "/api/login")
+ verify_cert = self._get_verify_cert()
+ r = requests.get(
+ login_request,
+ auth=(
+ self.server_username,
+ self.server_password),
+ verify=verify_cert)
+ token = r.json()
+ self.server_token = token
+ # Repeat request with valid token.
+ LOG.info(_LI(
+ "Going to perform request again %s with valid token."),
+ request)
+ if is_get_request:
+ res = requests.get(request,
+ auth=(self.server_username,
+ self.server_token),
+ verify=verify_cert)
+ else:
+ res = requests.post(request,
+ data=json.dumps(params),
+ headers=self._get_headers(),
+ auth=(self.server_username,
+ self.server_token),
+ verify=verify_cert)
+ return res
+ return response
+
+ def create_volume_from_snapshot(self, volume, snapshot):
+ """Creates a volume from a snapshot."""
+ # We interchange 'volume' and 'snapshot' because in ScaleIO
+ # snapshot is a volume: once a snapshot is generated it
+ # becomes a new unmapped volume in the system and the user
+ # may manipulate it in the same manner as any other volume
+ # exposed by the system
+ volname = self.id_to_base64(snapshot.id)
+ snapname = self.id_to_base64(volume.id)
+ LOG.info(_LI(
+ "ScaleIO create volume from snapshot: snapshot %(snapname)s "
+ "to volume %(volname)s."),
+ {'volname': volname,
+ 'snapname': snapname})
+ self._snapshot_volume(volname, snapname)
+
+ def _get_volume_id(self, volname):
+ volname_encoded = urllib.quote(volname, '')
+ volname_double_encoded = urllib.quote(volname_encoded, '')
+ LOG.info(_LI("Volume name after double encoding is %s."),
+ volname_double_encoded)
+ req_vars = {'server_ip': self.server_ip,
+ 'server_port': self.server_port,
+ 'encoded': volname_double_encoded}
+ request = ("https://%(server_ip)s:%(server_port)s"
+ "/api/types/Volume/instances/getByName::"
+ "%(encoded)s") % req_vars
+ LOG.info(_LI("ScaleIO get volume id by name request: %s"), request)
+ r = requests.get(
+ request,
+ auth=(self.server_username,
+ self.server_token),
+ verify=self._get_verify_cert())
+ r = self._check_response(r, request)
+
+ vol_id = r.json()
+
+ if not vol_id:
+ msg = _("Volume with name %s wasn't found.") % volname
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+ if r.status_code != OK_STATUS_CODE and "errorCode" in vol_id:
+ msg = (_("Error getting volume id from name %(volname)s: %(err)s.")
+ % {'volname': volname,
+ 'err': vol_id['message']})
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ LOG.info(_LI("volume id is %s."), vol_id)
+ return vol_id
+
+ def _get_headers(self):
+ return {'content-type': 'application/json'}
+
+ def _get_verify_cert(self):
+ verify_cert = False
+ if self.verify_server_certificate:
+ verify_cert = self.server_certificate_path
+ return verify_cert
+
+ def extend_volume(self, volume, new_size):
+ """Extends the size of an existing available ScaleIO volume."""
+
+ self._check_volume_size(new_size)
+
+ volname = self.id_to_base64(volume.id)
+
+ LOG.info(_LI(
+ "ScaleIO extend volume: volume %(volname)s to size %(new_size)s."),
+ {'volname': volname,
+ 'new_size': new_size})
+
+ vol_id = self._get_volume_id(volname)
+ req_vars = {'server_ip': self.server_ip,
+ 'server_port': self.server_port,
+ 'vol_id': vol_id}
+ request = ("https://%(server_ip)s:%(server_port)s"
+ "/api/instances/Volume::%(vol_id)s"
+ "/action/setVolumeSize") % req_vars
+ LOG.info(_LI("Change volume capacity request: %s."), request)
+ volume_new_size = new_size
+ params = {'sizeInGB': six.text_type(volume_new_size)}
+ r = requests.post(
+ request,
+ data=json.dumps(params),
+ headers=self._get_headers(),
+ auth=(self.server_username,
+ self.server_token),
+ verify=self._get_verify_cert())
+ r = self._check_response(r, request, False, params)
+
+ if r.status_code != OK_STATUS_CODE:
+ response = r.json()
+ msg = (_("Error extending volume %(vol)s: %(err)s.")
+ % {'vol': volname,
+ 'err': response['message']})
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ def create_cloned_volume(self, volume, src_vref):
+ """Creates a cloned volume."""
+ volname = self.id_to_base64(src_vref.id)
+ snapname = self.id_to_base64(volume.id)
+ LOG.info(_LI(
+ "ScaleIO create cloned volume: source volume %(src)s to target "
+ "volume %(tgt)."),
+ {'src': volname,
+ 'tgt': snapname})
+ self._snapshot_volume(volname, snapname)
+
+ def delete_volume(self, volume):
+ """Deletes a self.logical volume"""
+ volname = self.id_to_base64(volume.id)
+ self._delete_volume(volname)
+
+ def _delete_volume(self, volname):
+ volname_encoded = urllib.quote(volname, '')
+ volname_double_encoded = urllib.quote(volname_encoded, '')
+ LOG.info(_LI("Volume name after double encoding is %s."),
+ volname_double_encoded)
+
+ verify_cert = self._get_verify_cert()
+
+ # convert volume name to id
+ req_vars = {'server_ip': self.server_ip,
+ 'server_port': self.server_port,
+ 'encoded': volname_double_encoded}
+ request = ("https://%(server_ip)s:%(server_port)s"
+ "/api/types/Volume/instances/getByName::"
+ "%(encoded)s") % req_vars
+ LOG.info(_LI("ScaleIO get volume id by name request: %s."), request)
+ r = requests.get(
+ request,
+ auth=(
+ self.server_username,
+ self.server_token),
+ verify=verify_cert)
+ r = self._check_response(r, request)
+ LOG.info(_LI("Get by name response: %s."), r.text)
+ vol_id = r.json()
+ LOG.info(_LI("ScaleIO volume id to delete is %s."), vol_id)
+
+ if r.status_code != OK_STATUS_CODE and "errorCode" in vol_id:
+ msg = (_("Error getting volume id from name %(vol)s: %(err)s.")
+ % {'vol': volname, 'err': vol_id['message']})
+ LOG.error(msg)
+
+ error_code = vol_id['errorCode']
+ if (error_code == VOLUME_NOT_FOUND_ERROR):
+ force_delete = self.configuration.sio_force_delete
+ if force_delete:
+ LOG.warning(_LW(
+ "Ignoring error in delete volume %s: volume not found "
+ "due to force delete settings."), volname)
+ return
+
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ unmap_before_delete = (
+ self.configuration.sio_unmap_volume_before_deletion)
+ # Ensure that the volume is not mapped to any SDC before deletion in
+ # case unmap_before_deletion is enabled.
+ if unmap_before_delete:
+ params = {'allSdcs': ''}
+ req_vars = {'server_ip': self.server_ip,
+ 'server_port': self.server_port,
+ 'vol_id': vol_id}
+ request = ("https://%(server_ip)s:%(server_port)s"
+ "/api/instances/Volume::%(vol_id)s"
+ "/action/removeMappedSdc") % req_vars
+ LOG.info(_LI(
+ "Trying to unmap volume from all sdcs before deletion: %s."),
+ request)
+ r = requests.post(
+ request,
+ data=json.dumps(params),
+ headers=self._get_headers(),
+ auth=(
+ self.server_username,
+ self.server_token),
+ verify=verify_cert)
+ r = self._check_response(r, request, False, params)
+ LOG.debug("Unmap volume response: %s.", r.text)
+
+ params = {'removeMode': 'ONLY_ME'}
+ r = requests.post(
+ "https://" +
+ self.server_ip +
+ ":" +
+ self.server_port +
+ "/api/instances/Volume::" +
+ six.text_type(vol_id) +
+ "/action/removeVolume",
+ data=json.dumps(params),
+ headers=self._get_headers(),
+ auth=(self.server_username,
+ self.server_token),
+ verify=verify_cert)
+ r = self._check_response(r, request, False, params)
+
+ if r.status_code != OK_STATUS_CODE:
+ response = r.json()
+ error_code = response['errorCode']
+ if error_code == 78:
+ force_delete = self.configuration.sio_orce_delete
+ if force_delete:
+ LOG.warning(_LW(
+ "Ignoring error in delete volume %s: volume not found "
+ "due to force delete settings."), vol_id)
+ else:
+ msg = (_("Error deleting volume %s: volume not found.") %
+ vol_id)
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+ else:
+ msg = (_("Error deleting volume %(vol)s: %(err)s.") %
+ {'vol': vol_id,
+ 'err': response['message']})
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ def delete_snapshot(self, snapshot):
+ """Deletes a ScaleIO snapshot."""
+ snapname = self.id_to_base64(snapshot.id)
+ LOG.info(_LI("ScaleIO delete snapshot."))
+ self._delete_volume(snapname)
+
+ def initialize_connection(self, volume, connector):
+ """Initializes the connection and returns connection info.
+
+ The scaleio driver returns a driver_volume_type of 'scaleio'.
+ """
+
+ LOG.debug("Connector is %s.", connector)
+ volname = self.id_to_base64(volume.id)
+ properties = {}
+
+ properties['scaleIO_volname'] = volname
+ properties['hostIP'] = connector['ip']
+ properties['serverIP'] = self.server_ip
+ properties['serverPort'] = self.server_port
+ properties['serverUsername'] = self.server_username
+ properties['serverPassword'] = self.server_password
+ properties['serverToken'] = self.server_token
+
+ storage_type = self._get_volumetype_extraspecs(volume)
+ LOG.info(_LI("Volume type is %s."), storage_type)
+ iops_limit = self._find_iops_limit(storage_type)
+ LOG.info(_LI("iops limit is: %s."), iops_limit)
+ bandwidth_limit = self._find_bandwidth_limit(storage_type)
+ LOG.info(_LI("Bandwidth limit is: %s."), bandwidth_limit)
+ properties['iopsLimit'] = iops_limit
+ properties['bandwidthLimit'] = bandwidth_limit
+
+ return {'driver_volume_type': 'scaleio',
+ 'data': properties}
+
+ def terminate_connection(self, volume, connector, **kwargs):
+ LOG.debug("scaleio driver terminate connection.")
+
+ def _update_volume_stats(self):
+ stats = {}
+
+ backend_name = self.configuration.safe_get('volume_backend_name')
+ stats['volume_backend_name'] = backend_name or 'scaleio'
+ stats['vendor_name'] = 'EMC'
+ stats['driver_version'] = self.VERSION
+ stats['storage_protocol'] = 'scaleio'
+ stats['total_capacity_gb'] = 'unknown'
+ stats['free_capacity_gb'] = 'unknown'
+ stats['reserved_percentage'] = 0
+ stats['QoS_support'] = False
+
+ pools = []
+
+ verify_cert = self._get_verify_cert()
+
+ max_free_capacity = 0
+ total_capacity = 0
+
+ for sp_name in self.storage_pools:
+ splitted_name = sp_name.split(':')
+ domain_name = splitted_name[0]
+ pool_name = splitted_name[1]
+ LOG.debug("domain name is %(domain)s, pool name is %(pool)s.",
+ {'domain': domain_name,
+ 'pool': pool_name})
+ # Get domain id from name.
+ encoded_domain_name = urllib.quote(domain_name, '')
+ req_vars = {'server_ip': self.server_ip,
+ 'server_port': self.server_port,
+ 'encoded_domain_name': encoded_domain_name}
+ request = ("https://%(server_ip)s:%(server_port)s"
+ "/api/types/Domain/instances/getByName::"
+ "%(encoded_domain_name)s") % req_vars
+ LOG.info(_LI("ScaleIO get domain id by name request: %s."),
+ request)
+ LOG.info(_LI("username: %(username)s, verify_cert: %(verify)s."),
+ {'username': self.server_username,
+ 'verify': verify_cert})
+ r = requests.get(
+ request,
+ auth=(
+ self.server_username,
+ self.server_token),
+ verify=verify_cert)
+ r = self._check_response(r, request)
+ LOG.info(_LI("Get domain by name response: %s"), r.text)
+ domain_id = r.json()
+ if not domain_id:
+ msg = (_("Domain with name %s wasn't found.")
+ % self.protection_domain_name)
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+ if r.status_code != OK_STATUS_CODE and "errorCode" in domain_id:
+ msg = (_("Error getting domain id from name %(name)s: "
+ "%(err)s.")
+ % {'name': self.protection_domain_name,
+ 'err': domain_id['message']})
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+ LOG.info(_LI("Domain id is %s."), domain_id)
+
+ # Get pool id from name.
+ encoded_pool_name = urllib.quote(pool_name, '')
+ req_vars = {'server_ip': self.server_ip,
+ 'server_port': self.server_port,
+ 'domain_id': domain_id,
+ 'encoded_pool_name': encoded_pool_name}
+ request = ("https://%(server_ip)s:%(server_port)s"
+ "/api/types/Pool/instances/getByName::"
+ "%(domain_id)s,%(encoded_pool_name)s") % req_vars
+ LOG.info(_LI("ScaleIO get pool id by name request: %s."), request)
+ r = requests.get(
+ request,
+ auth=(
+ self.server_username,
+ self.server_token),
+ verify=verify_cert)
+ pool_id = r.json()
+ if not pool_id:
+ msg = (_("Pool with name %(pool)s wasn't found in domain "
+ "%(domain)s.")
+ % {'pool': pool_name,
+ 'domain': domain_id})
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+ if r.status_code != OK_STATUS_CODE and "errorCode" in pool_id:
+ msg = (_("Error getting pool id from name %(pool)s: "
+ "%(err)s.")
+ % {'pool': pool_name,
+ 'err': pool_id['message']})
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+ LOG.info(_LI("Pool id is %s."), pool_id)
+ req_vars = {'server_ip': self.server_ip,
+ 'server_port': self.server_port}
+ request = ("https://%(server_ip)s:%(server_port)s"
+ "/api/types/StoragePool/instances/action/"
+ "querySelectedStatistics") % req_vars
+ params = {'ids': [pool_id], 'properties': [
+ "capacityInUseInKb", "capacityLimitInKb"]}
+ r = requests.post(
+ request,
+ data=json.dumps(params),
+ headers=self._get_headers(),
+ auth=(
+ self.server_username,
+ self.server_token),
+ verify=verify_cert)
+ response = r.json()
+ LOG.info(_LI("Query capacity stats response: %s."), response)
+ for res in response.itervalues():
+ capacityInUse = res['capacityInUseInKb']
+ capacityLimit = res['capacityLimitInKb']
+ total_capacity_gb = capacityLimit / units.Mi
+ used_capacity_gb = capacityInUse / units.Mi
+ free_capacity_gb = total_capacity_gb - used_capacity_gb
+ LOG.info(_LI(
+ "free capacity of pool %(pool)s is: %(free)s, "
+ "total capacity: %(total)s."),
+ {'pool': pool_name,
+ 'free': free_capacity_gb,
+ 'total': total_capacity_gb})
+ pool = {'pool_name': sp_name,
+ 'total_capacity_gb': total_capacity_gb,
+ 'free_capacity_gb': free_capacity_gb,
+ 'QoS_support': False,
+ 'reserved_percentage': 0
+ }
+
+ pools.append(pool)
+ if free_capacity_gb > max_free_capacity:
+ max_free_capacity = free_capacity_gb
+ total_capacity = total_capacity + total_capacity_gb
+
+ stats['volume_backend_name'] = backend_name or 'scaleio'
+ stats['vendor_name'] = 'EMC'
+ stats['driver_version'] = self.VERSION
+ stats['storage_protocol'] = 'scaleio'
+ # Use zero capacities here so we always use a pool.
+ stats['total_capacity_gb'] = total_capacity
+ stats['free_capacity_gb'] = max_free_capacity
+ LOG.info(_LI(
+ "Free capacity for backend is: %(free)s, total capacity: "
+ "%(total)s."),
+ {'free': max_free_capacity,
+ 'total': total_capacity})
+
+ stats['reserved_percentage'] = 0
+ stats['QoS_support'] = False
+ stats['pools'] = pools
+
+ LOG.info(_LI("Backend name is %s."), stats["volume_backend_name"])
+
+ self._stats = stats
+
+ def get_volume_stats(self, refresh=False):
+ """Get volume stats.
+
+ If 'refresh' is True, run update the stats first.
+ """
+ if refresh:
+ self._update_volume_stats()
+
+ return self._stats
+
+ def _get_volumetype_extraspecs(self, volume):
+ specs = {}
+ ctxt = context.get_admin_context()
+ type_id = volume['volume_type_id']
+ if type_id:
+ volume_type = volume_types.get_volume_type(ctxt, type_id)
+ specs = volume_type.get('extra_specs')
+ for key, value in specs.iteritems():
+ specs[key] = value
+
+ return specs
+
+ def find_volume_path(self, volume_id):
+
+ LOG.info(_LI("looking for volume %s."), volume_id)
+ # Look for the volume in /dev/disk/by-id directory.
+ disk_filename = ""
+ tries = 0
+ while not disk_filename:
+ if tries > self.configuration.num_volume_device_scan_tries:
+ msg = (_(
+ "scaleIO volume %s not found at expected path.")
+ % volume_id)
+ raise exception.VolumeBackendAPIException(msg)
+ by_id_path = "/dev/disk/by-id"
+ if not os.path.isdir(by_id_path):
+ LOG.warning(_LW(
+ "scaleIO volume %(vol)s not yet found (no directory "
+ "/dev/disk/by-id yet). Try number: %(tries)d."),
+ {'vol': volume_id,
+ 'tries': tries})
+ tries = tries + 1
+ eventlet.sleep(1)
+ continue
+ filenames = os.listdir(by_id_path)
+ LOG.info(_LI(
+ "Files found in path %(path)s: %(file)s."),
+ {'path': by_id_path,
+ 'file': filenames})
+ for filename in filenames:
+ if (filename.startswith("emc-vol") and
+ filename.endswith(volume_id)):
+ disk_filename = filename
+ if not disk_filename:
+ LOG.warning(_LW(
+ "scaleIO volume %(vol)s not yet found. "
+ "Try number: %(tries)d."),
+ {'vol': volume_id,
+ 'tries': tries})
+ tries = tries + 1
+ eventlet.sleep(1)
+
+ if (tries != 0):
+ LOG.info(_LI(
+ "Found scaleIO device %(file)s after %(tries)d retries "),
+ {'file': disk_filename,
+ 'tries': tries})
+ full_disk_name = by_id_path + "/" + disk_filename
+ LOG.info(_LI("Full disk name is %s."), full_disk_name)
+ return full_disk_name
+
+ def _get_client_id(
+ self, server_ip, server_username, server_password, sdc_ip):
+ req_vars = {'server_ip': server_ip,
+ 'server_port': self.server_port,
+ 'sdc_ip': sdc_ip}
+ request = ("https://%(server_ip)s:%(server_port)s"
+ "/api/types/Client/instances/getByIp::"
+ "%(sdc_ip)s/") % req_vars
+ LOG.info(_LI("ScaleIO get client id by ip request: %s."), request)
+ r = requests.get(
+ request,
+ auth=(
+ server_username,
+ self.server_token),
+ verify=self._get_verify_cert())
+ r = self._check_response(r, request)
+
+ sdc_id = r.json()
+ if not sdc_id:
+ msg = _("Client with ip %s wasn't found.") % sdc_ip
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+ if r.status_code != 200 and "errorCode" in sdc_id:
+ msg = (_("Error getting sdc id from ip %(ip)s: %(id)s.")
+ % {'ip': sdc_ip, 'id': sdc_id['message']})
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+ LOG.info(_LI("ScaleIO sdc id is %s."), sdc_id)
+ return sdc_id
+
+ def _sio_attach_volume(self, volume, sdc_ip):
+ # We need to make sure we even *have* a local path
+ LOG.info(_LI("ScaleIO attach volume in scaleio cinder driver."))
+ volname = self.id_to_base64(volume.id)
+
+ cmd = ['drv_cfg']
+ cmd += ["--query_guid"]
+
+ LOG.info(_LI("ScaleIO sdc query guid command: %s"), six.text_type(cmd))
+
+ try:
+ (out, err) = utils.execute(*cmd, run_as_root=True)
+ LOG.debug("Map volume %(cmd)s: stdout=%(out)s stderr=%(err)s",
+ {'cmd': cmd, 'out': out, 'err': err})
+ except processutils.ProcessExecutionError as e:
+ msg = _("Error querying sdc guid: %s.") % six.text_type(e.stderr)
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ guid = out
+ LOG.info(_LI("Current sdc guid: %s."), guid)
+
+ params = {'guid': guid}
+
+ volume_id = self._get_volume_id(volname)
+ req_vars = {'server_ip': self.server_ip,
+ 'server_port': self.server_port,
+ 'volume_id': volume_id}
+ request = ("https://%(server_ip)s:%(server_port)s"
+ "/api/instances/Volume::%(volume_id)s"
+ "/action/addMappedSdc") % req_vars
+ LOG.info(_LI("Map volume request: %s."), request)
+ r = requests.post(
+ request,
+ data=json.dumps(params),
+ headers=self._get_headers(),
+ auth=(
+ self.server_username,
+ self.server_token),
+ verify=self._get_verify_cert())
+ r = self._check_response(r, request, False)
+
+ if r.status_code != OK_STATUS_CODE:
+ response = r.json()
+ error_code = response['errorCode']
+ if (error_code == VOLUME_ALREADY_MAPPED_ERROR):
+ LOG.warning(_LW("Ignoring error mapping volume %s: "
+ "volume already mapped."), volname)
+ else:
+ msg = (_("Error mapping volume %(vol)s: %(err)s.")
+ % {'vol': volname,
+ 'err': response['message']})
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ formated_id = volume_id
+
+ return self.find_volume_path(formated_id)
+
+ def _sio_detach_volume(self, volume, sdc_ip):
+ LOG.info(_LI("ScaleIO detach volume in scaleio cinder driver."))
+ volname = self.id_to_base64(volume.id)
+
+ cmd = ['drv_cfg']
+ cmd += ["--query_guid"]
+
+ LOG.info(_LI("ScaleIO sdc query guid command: %s."), cmd)
+
+ try:
+ (out, err) = utils.execute(*cmd, run_as_root=True)
+ LOG.debug("Unmap volume %(cmd)s: stdout=%(out)s stderr=%(err)s",
+ {'cmd': cmd, 'out': out, 'err': err})
+
+ except processutils.ProcessExecutionError as e:
+ msg = _("Error querying sdc guid: %s.") % six.text_type(e.stderr)
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ guid = out
+ LOG.info(_LI("Current sdc guid: %s."), guid)
+
+ params = {'guid': guid}
+
+ volume_id = self._get_volume_id(volname)
+ req_vars = {'server_ip': self.server_ip,
+ 'server_port': self.server_port,
+ 'vol_id': volume_id}
+ request = ("https://%(server_ip)s:%(server_port)s"
+ "/api/instances/Volume::%(vol_id)s"
+ "/action/removeMappedSdc") % req_vars
+ LOG.info(_LI("Unmap volume request: %s."), request)
+ r = requests.post(
+ request,
+ data=json.dumps(params),
+ headers=self._get_headers(),
+ auth=(
+ self.server_username,
+ self.server_token),
+ verify=self._get_verify_cert())
+ r = self._check_response(r, request, False, params)
+
+ if r.status_code != OK_STATUS_CODE:
+ response = r.json()
+ error_code = response['errorCode']
+ if error_code == VOLUME_NOT_MAPPED_ERROR:
+ LOG.warning(_LW("Ignoring error unmapping volume %s: "
+ "volume not mapped."), volname)
+ else:
+ msg = (_("Error unmapping volume %(vol)s: %(err)s.")
+ % {'vol': volname,
+ 'err': response['message']})
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ def copy_image_to_volume(self, context, volume, image_service, image_id):
+ """Fetch the image from image_service and write it to the volume."""
+ LOG.info(_LI(
+ "ScaleIO copy_image_to_volume volume: %(vol)s image service: "
+ "%(service)s image id: %(id)s."),
+ {'vol': volume,
+ 'service': six.text_type(image_service),
+ 'id': six.text_type(image_id)})
+ properties = utils.brick_get_connector_properties()
+ sdc_ip = properties['ip']
+ LOG.debug("SDC ip is: %s", sdc_ip)
+
+ try:
+ image_utils.fetch_to_raw(context,
+ image_service,
+ image_id,
+ self._sio_attach_volume(volume, sdc_ip),
+ BLOCK_SIZE,
+ size=volume['size'])
+
+ finally:
+ self._sio_detach_volume(volume, sdc_ip)
+
+ def copy_volume_to_image(self, context, volume, image_service, image_meta):
+ """Copy the volume to the specified image."""
+ LOG.info(_LI(
+ "ScaleIO copy_volume_to_image volume: %(vol)s image service: "
+ "%(service)s image meta: %(meta)s."),
+ {'vol': volume,
+ 'service': six.text_type(image_service),
+ 'meta': six.text_type(image_meta)})
+ properties = utils.brick_get_connector_properties()
+ sdc_ip = properties['ip']
+ LOG.debug("SDC ip is: {0}".format(sdc_ip))
+ try:
+ image_utils.upload_volume(context,
+ image_service,
+ image_meta,
+ self._sio_attach_volume(volume, sdc_ip))
+ finally:
+ self._sio_detach_volume(volume, sdc_ip)
+
+ def ensure_export(self, context, volume):
+ """Driver entry point to get the export info for an existing volume."""
+ pass
+
+ def create_export(self, context, volume):
+ """Driver entry point to get the export info for a new volume."""
+ pass
+
+ def remove_export(self, context, volume):
+ """Driver entry point to remove an export for a volume."""
+ pass
+
+ def check_for_export(self, context, volume_id):
+ """Make sure volume is exported."""
+ pass