Cinder RSD Driver
Add a new Cinder Driver for Intel RSD with NVMeoF support: - report stats - create volume - delete volume - attach volume - detach volume - create snapshot - delete snapshot - create volume from snapshot - clone volume - extend volume - copy volume to image (reuse the base-class impl) - copy image to volume (reuse the base-class impl) - host-assisted volume migration (reuse the manager code) - unit tests - release-note - documentation Change-Id: Ie83dab4858729353865268231352033934dafdc2 Implements: blueprint nvme-volume-driver
This commit is contained in:
parent
3c29c3846e
commit
b92b80241c
1276
cinder/tests/unit/volume/drivers/test_rsd.py
Normal file
1276
cinder/tests/unit/volume/drivers/test_rsd.py
Normal file
File diff suppressed because it is too large
Load Diff
723
cinder/volume/drivers/rsd.py
Normal file
723
cinder/volume/drivers/rsd.py
Normal file
@ -0,0 +1,723 @@
|
|||||||
|
# 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 RackScale Design.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import units
|
||||||
|
|
||||||
|
from cinder import exception
|
||||||
|
from cinder.i18n import _
|
||||||
|
from cinder import interface
|
||||||
|
from cinder import utils
|
||||||
|
from cinder.volume import driver
|
||||||
|
|
||||||
|
from distutils import version
|
||||||
|
|
||||||
|
try:
|
||||||
|
from rsd_lib import RSDLib
|
||||||
|
from sushy import exceptions as sushy_exceptions
|
||||||
|
except ImportError:
|
||||||
|
# Used for tests, when no rsd-lib is installed
|
||||||
|
RSDLib = None
|
||||||
|
sushy_exceptions = None
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
RSD_OPTS = [
|
||||||
|
cfg.StrOpt('podm_url',
|
||||||
|
default='',
|
||||||
|
help='URL of PODM service'),
|
||||||
|
cfg.StrOpt('podm_username',
|
||||||
|
default='',
|
||||||
|
help='Username of PODM service'),
|
||||||
|
cfg.StrOpt('podm_password',
|
||||||
|
default='',
|
||||||
|
help='Password of PODM service',
|
||||||
|
secret=True),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class RSDRetryableException(exception.VolumeDriverException):
|
||||||
|
message = _("RSD retryable exception: %(reason)s")
|
||||||
|
|
||||||
|
|
||||||
|
def get_volume_metadata(volume):
|
||||||
|
metadata = volume.get('volume_metadata')
|
||||||
|
if metadata:
|
||||||
|
ret = {data['key']: data['value'] for data in metadata}
|
||||||
|
else:
|
||||||
|
ret = volume.get('metadata', {})
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class RSDClient(object):
|
||||||
|
def __init__(self, rsdlib):
|
||||||
|
self.rsdlib = rsdlib
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def initialize(cls, url, username, password, verify):
|
||||||
|
if not RSDLib:
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=(_("RSDLib is not available, please install rsd-lib.")))
|
||||||
|
|
||||||
|
try:
|
||||||
|
rsdlib = RSDLib(url, username, password, verify=verify).factory()
|
||||||
|
except Exception:
|
||||||
|
# error credentials may throw unexpected exception
|
||||||
|
LOG.exception("Cannot connect to RSD PODM")
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=_("initialize: Cannot connect to RSD PODM."))
|
||||||
|
|
||||||
|
rsd_lib_version = version.LooseVersion(rsdlib._rsd_api_version)
|
||||||
|
if rsd_lib_version < version.LooseVersion("2.4.0"):
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=(_("initialize: Unsupported rsd_lib version: "
|
||||||
|
"%(current)s < %(expected)s.")
|
||||||
|
% {'current': rsdlib._rsd_api_version,
|
||||||
|
'expected': "2.4.0"}))
|
||||||
|
|
||||||
|
LOG.info("initialize: Connected to %s at version %s.",
|
||||||
|
url, rsdlib._rsd_api_version)
|
||||||
|
return cls(rsdlib)
|
||||||
|
|
||||||
|
def _get_storage(self, storage_url):
|
||||||
|
ss_url = "/".join(storage_url.split("/", 5)[:5])
|
||||||
|
storage_service = self.rsdlib.get_storage_service(ss_url)
|
||||||
|
return storage_service
|
||||||
|
|
||||||
|
def _get_storages(self, filter_nvme=True):
|
||||||
|
ret = []
|
||||||
|
for storage in (self.rsdlib
|
||||||
|
.get_storage_service_collection().get_members()):
|
||||||
|
if filter_nvme:
|
||||||
|
drives = storage.drives.get_members()
|
||||||
|
if drives and (any(map(lambda drive:
|
||||||
|
False if not drive.protocol
|
||||||
|
else 'nvme' in drive.protocol.lower(),
|
||||||
|
drives))):
|
||||||
|
ret.append(storage)
|
||||||
|
else:
|
||||||
|
ret.append(storage)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def _get_node(self, node_url):
|
||||||
|
return self.rsdlib.get_node(node_url)
|
||||||
|
|
||||||
|
def _get_volume(self, volume_url):
|
||||||
|
ss = self._get_storage(volume_url)
|
||||||
|
volume = ss.volumes.get_member(volume_url)
|
||||||
|
return volume
|
||||||
|
|
||||||
|
def _get_providing_pool(self, volume):
|
||||||
|
len_cs = len(volume.capacity_sources)
|
||||||
|
if len_cs != 1:
|
||||||
|
raise exception.ValidationError(
|
||||||
|
detail=(_("Volume %(vol)s has %(len_cs)d capacity_sources!")
|
||||||
|
% {'vol': volume.path,
|
||||||
|
'len_cs': len_cs}))
|
||||||
|
len_pp = len(volume.capacity_sources[0].providing_pools)
|
||||||
|
if len_pp != 1:
|
||||||
|
raise exception.ValidationError(
|
||||||
|
detail=(_("Volume %(vol)s has %(len_pp)d providing_pools!")
|
||||||
|
% {'vol': volume.path,
|
||||||
|
'len_pp': len_pp}))
|
||||||
|
return volume.capacity_sources[0].providing_pools[0]
|
||||||
|
|
||||||
|
def _create_vol_or_snap(self,
|
||||||
|
storage,
|
||||||
|
size_in_bytes,
|
||||||
|
pool_url=None,
|
||||||
|
source_snap=None,
|
||||||
|
source_vol=None):
|
||||||
|
capacity_sources = None
|
||||||
|
if pool_url:
|
||||||
|
capacity_sources = [{
|
||||||
|
"ProvidingPools": [{
|
||||||
|
"@odata.id": pool_url
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
|
||||||
|
replica_infos = None
|
||||||
|
if source_snap:
|
||||||
|
replica_infos = [{
|
||||||
|
"ReplicaType": "Clone",
|
||||||
|
"Replica": {"@odata.id": source_snap}
|
||||||
|
}]
|
||||||
|
if source_vol:
|
||||||
|
raise exception.InvalidInput(
|
||||||
|
reason=(_("Cannot specify both source_snap=%(snap)s and "
|
||||||
|
"source_vol=%(vol)s!")
|
||||||
|
% {'snap': source_snap,
|
||||||
|
'vol': source_vol}))
|
||||||
|
elif source_vol:
|
||||||
|
replica_infos = [{
|
||||||
|
"ReplicaType": "Snapshot",
|
||||||
|
"Replica": {"@odata.id": source_vol}
|
||||||
|
}]
|
||||||
|
|
||||||
|
LOG.debug("Creating... with size_byte=%s, "
|
||||||
|
"capacity_sources=%s, replica_infos=%s",
|
||||||
|
size_in_bytes, capacity_sources, replica_infos)
|
||||||
|
volume_url = storage.volumes.create_volume(
|
||||||
|
size_in_bytes,
|
||||||
|
capacity_sources=capacity_sources,
|
||||||
|
replica_infos=replica_infos)
|
||||||
|
LOG.debug("Created volume_url=%s", volume_url)
|
||||||
|
return volume_url
|
||||||
|
|
||||||
|
def create_volume(self, size_in_gb):
|
||||||
|
size_in_bytes = size_in_gb * units.Gi
|
||||||
|
try:
|
||||||
|
for storage in self._get_storages():
|
||||||
|
try:
|
||||||
|
volume_url = self._create_vol_or_snap(
|
||||||
|
storage, size_in_bytes)
|
||||||
|
LOG.info("RSD volume %s created, with size %s GiB",
|
||||||
|
volume_url, size_in_gb)
|
||||||
|
return volume_url
|
||||||
|
# NOTE(Yingxin): Currently, we capture sushy_exception to
|
||||||
|
# identify that volume creation is failed at RSD backend.
|
||||||
|
except (sushy_exceptions.HTTPError,
|
||||||
|
sushy_exceptions.ConnectionError) as e:
|
||||||
|
LOG.warning("skipped storage %s for creation error %s",
|
||||||
|
storage.path, e)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception("Create volume failed")
|
||||||
|
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=(_('Unable to create new volume with %d GiB') % size_in_gb))
|
||||||
|
|
||||||
|
def create_snap(self, volume_url):
|
||||||
|
try:
|
||||||
|
ss = self._get_storage(volume_url)
|
||||||
|
volume = self._get_volume(volume_url)
|
||||||
|
pool_url = self._get_providing_pool(volume)
|
||||||
|
snap_url = self._create_vol_or_snap(
|
||||||
|
ss, volume.capacity_bytes,
|
||||||
|
pool_url=pool_url,
|
||||||
|
source_vol=volume_url)
|
||||||
|
LOG.info("RSD snapshot %s created, from volume %s",
|
||||||
|
snap_url, volume_url)
|
||||||
|
return snap_url
|
||||||
|
except Exception:
|
||||||
|
LOG.exception("Create snapshot failed")
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=(_('Unable to create snapshot from volume %s')
|
||||||
|
% volume_url))
|
||||||
|
|
||||||
|
def create_volume_from_snap(self, snap_url, size_in_gb=None):
|
||||||
|
try:
|
||||||
|
ss = self._get_storage(snap_url)
|
||||||
|
snap = self._get_volume(snap_url)
|
||||||
|
if not size_in_gb:
|
||||||
|
size_in_bytes = snap.capacity_bytes
|
||||||
|
else:
|
||||||
|
size_in_bytes = size_in_gb * units.Gi
|
||||||
|
pool_url = self._get_providing_pool(snap)
|
||||||
|
volume_url = self._create_vol_or_snap(
|
||||||
|
ss, size_in_bytes,
|
||||||
|
pool_url=pool_url,
|
||||||
|
source_snap=snap_url)
|
||||||
|
LOG.info("RSD volume %s created, from snap %s, "
|
||||||
|
"with size %s GiB.",
|
||||||
|
volume_url, snap_url,
|
||||||
|
size_in_bytes / units.Gi)
|
||||||
|
return volume_url
|
||||||
|
except Exception:
|
||||||
|
LOG.exception("Create volume from snapshot failed")
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=(_('Unable to create volume from snapshot %s')
|
||||||
|
% snap_url))
|
||||||
|
|
||||||
|
def clone_volume(self, volume_url, size_in_gb=None):
|
||||||
|
try:
|
||||||
|
ss = self._get_storage(volume_url)
|
||||||
|
origin_volume = self._get_volume(volume_url)
|
||||||
|
pool_url = self._get_providing_pool(origin_volume)
|
||||||
|
snap_url = self._create_vol_or_snap(
|
||||||
|
ss, origin_volume.capacity_bytes,
|
||||||
|
pool_url=pool_url,
|
||||||
|
source_vol=volume_url)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception("Clone volume failed (create snapshot phase)")
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=(_('Unable to create volume from volume %s, snapshot '
|
||||||
|
'creation failed.')
|
||||||
|
% volume_url))
|
||||||
|
try:
|
||||||
|
if not size_in_gb:
|
||||||
|
size_in_bytes = origin_volume.capacity_bytes
|
||||||
|
else:
|
||||||
|
size_in_bytes = size_in_gb * units.Gi
|
||||||
|
new_vol_url = self._create_vol_or_snap(
|
||||||
|
ss, size_in_bytes,
|
||||||
|
pool_url=pool_url,
|
||||||
|
source_snap=snap_url)
|
||||||
|
LOG.info("RSD volume %s created, from volume %s and snap %s, "
|
||||||
|
"with size %s GiB.",
|
||||||
|
new_vol_url, volume_url, snap_url,
|
||||||
|
size_in_bytes / units.Gi)
|
||||||
|
return new_vol_url, snap_url
|
||||||
|
except Exception:
|
||||||
|
LOG.exception("Clone volume failed (clone volume phase)")
|
||||||
|
try:
|
||||||
|
self.delete_vol_or_snap(snap_url)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception("Clone volume failed (undo snapshot)")
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=(_('Unable to delete the temp snapshot %(snap)s, '
|
||||||
|
'during a failure to clone volume %(vol)s.')
|
||||||
|
% {'snap': snap_url,
|
||||||
|
'vol': volume_url}))
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=(_('Unable to create volume from volume %s, volume '
|
||||||
|
'creation failed.')
|
||||||
|
% volume_url))
|
||||||
|
|
||||||
|
def extend_volume(self, volume_url, size_in_gb):
|
||||||
|
size_in_bytes = size_in_gb * units.Gi
|
||||||
|
try:
|
||||||
|
volume = self._get_volume(volume_url)
|
||||||
|
volume.resize(size_in_bytes)
|
||||||
|
LOG.info("RSD volume %s resized to %s Bytes",
|
||||||
|
volume.path, size_in_bytes)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception("Extend volume failed")
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=(_('Unable to extend volume %s.') % volume_url))
|
||||||
|
|
||||||
|
def delete_vol_or_snap(self, volume_url,
|
||||||
|
volume_name='', ignore_non_exist=False):
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
volume = self._get_volume(volume_url)
|
||||||
|
except sushy_exceptions.ResourceNotFoundError:
|
||||||
|
if ignore_non_exist:
|
||||||
|
LOG.warning("Deleted non existent vol/snap %s", volume_url)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
if volume.links.endpoints:
|
||||||
|
LOG.warning("Delete vol/snap failed, attached: %s", volume_url)
|
||||||
|
raise exception.VolumeIsBusy(_("Volume is already attached"),
|
||||||
|
volume_name=volume_name)
|
||||||
|
volume.delete()
|
||||||
|
except sushy_exceptions.BadRequestError as e:
|
||||||
|
try:
|
||||||
|
msg = e.body['@Message.ExtendedInfo'][0]['Message']
|
||||||
|
if (msg == "Cannot delete source snapshot volume when "
|
||||||
|
"other clone volumes are based on this snapshot."):
|
||||||
|
LOG.warning("Delete vol/snap failed, has-deps: %s",
|
||||||
|
volume_url)
|
||||||
|
raise exception.SnapshotIsBusy(snapshot_name=volume_name)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception("Delete vol/snap failed")
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=(_('Unable to delete volume %s.') % volume_url))
|
||||||
|
except Exception:
|
||||||
|
LOG.exception("Delete vol/snap failed")
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=(_('Unable to delete volume %s.') % volume_url))
|
||||||
|
LOG.info("RSD volume deleted: %s", volume_url)
|
||||||
|
|
||||||
|
def get_node_url_by_uuid(self, uuid):
|
||||||
|
uuid = uuid.upper()
|
||||||
|
try:
|
||||||
|
nodes = self.rsdlib.get_node_collection().get_members()
|
||||||
|
for node in nodes:
|
||||||
|
node_system = None
|
||||||
|
if node:
|
||||||
|
node_system = self.rsdlib.get_system(
|
||||||
|
node.links.computer_system)
|
||||||
|
if (node and
|
||||||
|
node_system and
|
||||||
|
node_system.uuid and
|
||||||
|
node_system.uuid.upper() == uuid):
|
||||||
|
return node.path
|
||||||
|
except Exception:
|
||||||
|
LOG.exception("Get node url failed")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_stats(self):
|
||||||
|
free_capacity_gb = 0
|
||||||
|
total_capacity_gb = 0
|
||||||
|
allocated_capacity_gb = 0
|
||||||
|
total_volumes = 0
|
||||||
|
try:
|
||||||
|
storages = self._get_storages()
|
||||||
|
for storage in storages:
|
||||||
|
for pool in storage.storage_pools.get_members():
|
||||||
|
total_capacity_gb += (
|
||||||
|
float(pool.capacity.allocated_bytes or 0) / units.Gi)
|
||||||
|
allocated_capacity_gb += (
|
||||||
|
float(pool.capacity.consumed_bytes or 0) / units.Gi)
|
||||||
|
total_volumes += len(storage.volumes.members_identities)
|
||||||
|
free_capacity_gb = total_capacity_gb - allocated_capacity_gb
|
||||||
|
LOG.info("Got RSD stats: free_gb:%s, total_gb:%s, "
|
||||||
|
"allocated_gb:%s, volumes:%s",
|
||||||
|
free_capacity_gb,
|
||||||
|
total_capacity_gb,
|
||||||
|
allocated_capacity_gb,
|
||||||
|
total_volumes)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception("Get stats failed")
|
||||||
|
|
||||||
|
return (free_capacity_gb,
|
||||||
|
total_capacity_gb,
|
||||||
|
allocated_capacity_gb,
|
||||||
|
total_volumes)
|
||||||
|
|
||||||
|
def _get_nqn_endpoints(self, endpoint_urls):
|
||||||
|
ret = []
|
||||||
|
for endpoint_url in endpoint_urls:
|
||||||
|
endpoint_json = (
|
||||||
|
json.loads(self.rsdlib._conn.get(endpoint_url).text))
|
||||||
|
for ident in endpoint_json["Identifiers"]:
|
||||||
|
if ident["DurableNameFormat"] == "NQN":
|
||||||
|
nqn = ident["DurableName"]
|
||||||
|
ret.append((nqn, endpoint_json))
|
||||||
|
break
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@utils.retry(RSDRetryableException,
|
||||||
|
interval=4,
|
||||||
|
retries=5,
|
||||||
|
backoff_rate=2)
|
||||||
|
def attach_volume_to_node(self, volume_url, node_url):
|
||||||
|
LOG.info('Trying attach from node %s to volume %s',
|
||||||
|
node_url, volume_url)
|
||||||
|
try:
|
||||||
|
volume = self._get_volume(volume_url)
|
||||||
|
node = self._get_node(node_url)
|
||||||
|
if len(volume.links.endpoints) > 0:
|
||||||
|
raise exception.ValidationError(
|
||||||
|
detail=(_("Volume %s already attached") % volume_url))
|
||||||
|
|
||||||
|
node.attach_endpoint(volume.path)
|
||||||
|
except sushy_exceptions.InvalidParameterValueError as e:
|
||||||
|
LOG.exception("Attach volume failed (not allowable)")
|
||||||
|
raise RSDRetryableException(
|
||||||
|
reason=(_("Not allowed to attach from "
|
||||||
|
"%(node)s to %(volume)s.")
|
||||||
|
% {'node': node_url,
|
||||||
|
'volume': volume_url}))
|
||||||
|
except Exception:
|
||||||
|
LOG.exception("Attach volume failed (attach phase)")
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=(_("Attach failed from %(node)s to %(volume)s.")
|
||||||
|
% {'node': node_url,
|
||||||
|
'volume': volume_url}))
|
||||||
|
try:
|
||||||
|
volume.refresh()
|
||||||
|
node.refresh()
|
||||||
|
|
||||||
|
v_endpoints = volume.links.endpoints
|
||||||
|
v_endpoints = self._get_nqn_endpoints(v_endpoints)
|
||||||
|
if len(v_endpoints) != 1:
|
||||||
|
raise exception.ValidationError(
|
||||||
|
detail=(_("Attach volume error: %d target nqns")
|
||||||
|
% len(v_endpoints)))
|
||||||
|
target_nqn, v_endpoint = v_endpoints[0]
|
||||||
|
ip_transports = v_endpoint["IPTransportDetails"]
|
||||||
|
if len(ip_transports) != 1:
|
||||||
|
raise exception.ValidationError(
|
||||||
|
detail=(_("Attach volume error: %d target ips")
|
||||||
|
% len(ip_transports)))
|
||||||
|
ip_transport = ip_transports[0]
|
||||||
|
target_ip = ip_transport["IPv4Address"]["Address"]
|
||||||
|
target_port = ip_transport["Port"]
|
||||||
|
|
||||||
|
node_system = self.rsdlib.get_system(node.links.computer_system)
|
||||||
|
n_endpoints = tuple(
|
||||||
|
val["@odata.id"]
|
||||||
|
for val in node_system.json["Links"]["Endpoints"])
|
||||||
|
n_endpoints = self._get_nqn_endpoints(n_endpoints)
|
||||||
|
if len(n_endpoints) == 0:
|
||||||
|
raise exception.ValidationError(
|
||||||
|
detail=(_("Attach volume error: %d host nqns")
|
||||||
|
% len(n_endpoints)))
|
||||||
|
host_nqn, v_endpoint = n_endpoints[0]
|
||||||
|
|
||||||
|
LOG.info('Attachment successful: Retrieved target IP %s, '
|
||||||
|
'target Port %s, target NQN %s and initiator NQN %s',
|
||||||
|
target_ip, target_port, target_nqn, host_nqn)
|
||||||
|
return (target_ip, target_port, target_nqn, host_nqn)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception("Attach volume failed (post-attach)")
|
||||||
|
try:
|
||||||
|
node.refresh()
|
||||||
|
node.detach_endpoint(volume.path)
|
||||||
|
LOG.info('Detached from node %s to volume %s',
|
||||||
|
node_url, volume_url)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception("Attach volume failed (undo attach)")
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=(_("Undo-attach failed from %(node)s to %(volume)s.")
|
||||||
|
% {'node': node_url,
|
||||||
|
'volume': volume_url}))
|
||||||
|
if isinstance(e, exception.ValidationError):
|
||||||
|
raise RSDRetryableException(
|
||||||
|
reason=(_("Validation error during post-attach from "
|
||||||
|
"%(node)s to %(volume)s.")
|
||||||
|
% {'node': node_url,
|
||||||
|
'volume': volume_url}))
|
||||||
|
else:
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=(_("Post-attach failed from %(node)s to %(volume)s.")
|
||||||
|
% {'node': node_url,
|
||||||
|
'volume': volume_url}))
|
||||||
|
|
||||||
|
def detach_volume_from_node(self, volume_url, node_url):
|
||||||
|
LOG.info('Trying detach from node %s for volume %s',
|
||||||
|
node_url, volume_url)
|
||||||
|
try:
|
||||||
|
volume = self._get_volume(volume_url)
|
||||||
|
node = self._get_node(node_url)
|
||||||
|
node.detach_endpoint(volume.path)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception("Detach volume failed")
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=(_("Detach failed from %(node)s for %(volume)s.")
|
||||||
|
% {'node': node_url,
|
||||||
|
'volume': volume_url}))
|
||||||
|
|
||||||
|
def detach_all_node_connections_for_volume(self, volume_url):
|
||||||
|
try:
|
||||||
|
volume = self._get_volume(volume_url)
|
||||||
|
nodes = self.rsdlib.get_node_collection().get_members()
|
||||||
|
for node in nodes:
|
||||||
|
if node:
|
||||||
|
if volume.path in node.get_allowed_detach_endpoints():
|
||||||
|
node.detach_endpoint(volume.path)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception("Detach failed for volume from all host "
|
||||||
|
"connections")
|
||||||
|
raise exception.VolumeBackendAPIException(
|
||||||
|
data=(_("Detach failed for %(volume)s from all host "
|
||||||
|
"connections.")
|
||||||
|
% {'volume': volume_url}))
|
||||||
|
|
||||||
|
|
||||||
|
@interface.volumedriver
|
||||||
|
class RSDDriver(driver.VolumeDriver):
|
||||||
|
"""Openstack driver to perform NVMe-oF volume management in RSD Solution
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
Version History:
|
||||||
|
1.0.0: Initial driver
|
||||||
|
"""
|
||||||
|
|
||||||
|
VERSION = '1.0.0'
|
||||||
|
CI_WIKI_NAME = 'INTEL-RSD-CI'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(RSDDriver, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.configuration.append_config_values(RSD_OPTS)
|
||||||
|
self.rsdClient = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_driver_options():
|
||||||
|
return RSD_OPTS
|
||||||
|
|
||||||
|
@utils.trace
|
||||||
|
def do_setup(self, context):
|
||||||
|
self.rsdClient = RSDClient.initialize(
|
||||||
|
self.configuration.podm_url,
|
||||||
|
self.configuration.podm_username,
|
||||||
|
self.configuration.podm_password,
|
||||||
|
self.configuration.suppress_requests_ssl_warnings)
|
||||||
|
|
||||||
|
def check_for_setup_error(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@utils.trace
|
||||||
|
def create_volume(self, volume):
|
||||||
|
size_in_gb = int(volume['size'])
|
||||||
|
volume_url = self.rsdClient.create_volume(size_in_gb)
|
||||||
|
return {'provider_location': volume_url}
|
||||||
|
|
||||||
|
@utils.trace
|
||||||
|
def delete_volume(self, volume):
|
||||||
|
volume_url = volume['provider_location']
|
||||||
|
if not volume_url:
|
||||||
|
return
|
||||||
|
self.rsdClient.delete_vol_or_snap(volume_url,
|
||||||
|
volume_name=volume.name,
|
||||||
|
ignore_non_exist=True)
|
||||||
|
provider_snap_url = volume.metadata.get("rsd_provider_snap")
|
||||||
|
if provider_snap_url:
|
||||||
|
self.rsdClient.delete_vol_or_snap(provider_snap_url,
|
||||||
|
volume_name=volume.name,
|
||||||
|
ignore_non_exist=True)
|
||||||
|
|
||||||
|
def _update_volume_stats(self):
|
||||||
|
backend_name = (
|
||||||
|
self.configuration.safe_get('volume_backend_name') or 'RSD')
|
||||||
|
|
||||||
|
ret = self.rsdClient.get_stats()
|
||||||
|
(free_capacity_gb,
|
||||||
|
total_capacity_gb,
|
||||||
|
allocated_capacity_gb,
|
||||||
|
total_volumes) = ret
|
||||||
|
|
||||||
|
spool = {}
|
||||||
|
spool['pool_name'] = backend_name
|
||||||
|
spool['total_capacity_gb'] = total_capacity_gb
|
||||||
|
spool['free_capacity_gb'] = free_capacity_gb
|
||||||
|
spool['allocated_capacity_gb'] = allocated_capacity_gb
|
||||||
|
spool['thin_provisioning_support'] = True
|
||||||
|
spool['thick_provisioning_support'] = True
|
||||||
|
spool['multiattach'] = False
|
||||||
|
|
||||||
|
self._stats['volume_backend_name'] = backend_name
|
||||||
|
self._stats['vendor_name'] = 'Intel'
|
||||||
|
self._stats['driver_version'] = self.VERSION
|
||||||
|
self._stats['storage_protocol'] = 'nvmeof'
|
||||||
|
# SinglePool
|
||||||
|
self._stats['pools'] = [spool]
|
||||||
|
|
||||||
|
@utils.trace
|
||||||
|
def get_volume_stats(self, refresh=False):
|
||||||
|
if refresh:
|
||||||
|
self._update_volume_stats()
|
||||||
|
return self._stats
|
||||||
|
|
||||||
|
@utils.trace
|
||||||
|
def initialize_connection(self, volume, connector, **kwargs):
|
||||||
|
uuid = connector.get("system uuid")
|
||||||
|
if not uuid:
|
||||||
|
msg = _("initialize_connection error: no uuid available!")
|
||||||
|
LOG.exception(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(msg)
|
||||||
|
node_url = self.rsdClient.get_node_url_by_uuid(uuid)
|
||||||
|
if not node_url:
|
||||||
|
msg = (_("initialize_connection error: no node_url from uuid %s!")
|
||||||
|
% uuid)
|
||||||
|
LOG.exception(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(msg)
|
||||||
|
|
||||||
|
volume_url = volume['provider_location']
|
||||||
|
target_ip, target_port, target_nqn, initiator_nqn = (
|
||||||
|
self.rsdClient.attach_volume_to_node(volume_url, node_url))
|
||||||
|
conn_info = {
|
||||||
|
'driver_volume_type': 'nvmeof',
|
||||||
|
'data': {
|
||||||
|
'transport_type': 'rdma',
|
||||||
|
'host_nqn': initiator_nqn,
|
||||||
|
'nqn': target_nqn,
|
||||||
|
'target_port': target_port,
|
||||||
|
'target_portal': target_ip,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return conn_info
|
||||||
|
|
||||||
|
@utils.trace
|
||||||
|
def terminate_connection(self, volume, connector, **kwargs):
|
||||||
|
if connector is None:
|
||||||
|
# None connector means force-detach
|
||||||
|
volume_url = volume['provider_location']
|
||||||
|
self.rsdClient.detach_all_node_connections_for_volume(volume_url)
|
||||||
|
return
|
||||||
|
|
||||||
|
uuid = connector.get("system uuid")
|
||||||
|
if not uuid:
|
||||||
|
msg = _("terminate_connection error: no uuid available!")
|
||||||
|
LOG.exception(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(msg)
|
||||||
|
node_url = self.rsdClient.get_node_url_by_uuid(uuid)
|
||||||
|
if not node_url:
|
||||||
|
msg = (_("terminate_connection error: no node_url from uuid %s!")
|
||||||
|
% uuid)
|
||||||
|
LOG.exception(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(msg)
|
||||||
|
|
||||||
|
volume_url = volume['provider_location']
|
||||||
|
self.rsdClient.detach_volume_from_node(volume_url, node_url)
|
||||||
|
|
||||||
|
def ensure_export(self, context, volume):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_export(self, context, volume, connector):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def remove_export(self, context, volume):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@utils.trace
|
||||||
|
def create_volume_from_snapshot(self, volume, snapshot):
|
||||||
|
snap_url = snapshot.provider_location
|
||||||
|
old_size_in_gb = snapshot.volume_size
|
||||||
|
size_in_gb = volume.size
|
||||||
|
volume_url = self.rsdClient.create_volume_from_snap(snap_url)
|
||||||
|
if size_in_gb != old_size_in_gb:
|
||||||
|
try:
|
||||||
|
self.rsdClient.extend_volume(volume_url, size_in_gb)
|
||||||
|
except Exception:
|
||||||
|
self.rsdClient.delete_vol_or_snap(volume_url,
|
||||||
|
volume_name=volume.name)
|
||||||
|
raise
|
||||||
|
return {'provider_location': volume_url}
|
||||||
|
|
||||||
|
@utils.trace
|
||||||
|
def create_snapshot(self, snapshot):
|
||||||
|
volume_url = snapshot.volume.provider_location
|
||||||
|
snap_url = self.rsdClient.create_snap(volume_url)
|
||||||
|
snapshot.provider_location = snap_url
|
||||||
|
snapshot.save()
|
||||||
|
|
||||||
|
@utils.trace
|
||||||
|
def delete_snapshot(self, snapshot):
|
||||||
|
snap_url = snapshot.provider_location
|
||||||
|
if not snap_url:
|
||||||
|
return
|
||||||
|
self.rsdClient.delete_vol_or_snap(snap_url,
|
||||||
|
volume_name=snapshot.name,
|
||||||
|
ignore_non_exist=True)
|
||||||
|
|
||||||
|
@utils.trace
|
||||||
|
def extend_volume(self, volume, new_size):
|
||||||
|
volume_url = volume.provider_location
|
||||||
|
self.rsdClient.extend_volume(volume_url, new_size)
|
||||||
|
|
||||||
|
def clone_image(self, context, volume,
|
||||||
|
image_location, image_meta,
|
||||||
|
image_service):
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
@utils.trace
|
||||||
|
def create_cloned_volume(self, volume, src_vref):
|
||||||
|
volume_url = src_vref.provider_location
|
||||||
|
old_size_in_gb = src_vref.size
|
||||||
|
size_in_gb = volume.size
|
||||||
|
new_vol_url, provider_snap_url = self.rsdClient.clone_volume(
|
||||||
|
volume_url)
|
||||||
|
metadata = get_volume_metadata(volume)
|
||||||
|
metadata["rsd_provider_snap"] = provider_snap_url
|
||||||
|
if size_in_gb != old_size_in_gb:
|
||||||
|
try:
|
||||||
|
self.rsdClient.extend_volume(new_vol_url, size_in_gb)
|
||||||
|
except Exception:
|
||||||
|
self.rsdClient.delete_vol_or_snap(new_vol_url,
|
||||||
|
volume_name=volume.name)
|
||||||
|
self.rsdClient.delete_vol_or_snap(provider_snap_url,
|
||||||
|
volume_name=volume.name)
|
||||||
|
raise
|
||||||
|
return {'provider_location': new_vol_url,
|
||||||
|
'metadata': metadata}
|
@ -0,0 +1,52 @@
|
|||||||
|
====================================
|
||||||
|
Intel Rack Scale Design (RSD) driver
|
||||||
|
====================================
|
||||||
|
|
||||||
|
The Intel Rack Scale Design volume driver is a block storage driver providing
|
||||||
|
NVMe-oF support for RSD storage.
|
||||||
|
|
||||||
|
System requirements
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
To use the RSD driver, the following requirements are needed:
|
||||||
|
|
||||||
|
* The driver only supports RSD API at version 2.4 or later.
|
||||||
|
* The driver requires rsd-lib.
|
||||||
|
* ``cinder-volume`` should be running on one of the composed node in RSD, and
|
||||||
|
have access to the PODM url.
|
||||||
|
* All the ``nova-compute`` services should be running on the composed nodes in
|
||||||
|
RSD.
|
||||||
|
* All the ``cinder-volume`` and ``nova-compute`` nodes should have installed
|
||||||
|
``dmidecode`` and the latest ``nvme-cli`` with connect/disconnect
|
||||||
|
subcommands.
|
||||||
|
|
||||||
|
Supported operations
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
* Create, delete volumes.
|
||||||
|
* Attach, detach volumes.
|
||||||
|
* Copy an image to a volume.
|
||||||
|
* Copy a volume to an image.
|
||||||
|
* Create, delete snapshots.
|
||||||
|
* Create a volume from a snapshot.
|
||||||
|
* Clone a volume.
|
||||||
|
* Extend a volume.
|
||||||
|
* Get volume statistics.
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
On ``cinder-volume`` nodes, using the following configurations in your
|
||||||
|
``/etc/cinder/cinder.conf``:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
volume_driver = cinder.volume.drivers.rsd.RSDDriver
|
||||||
|
|
||||||
|
The following table contains the configuration options supported by the
|
||||||
|
RSD driver:
|
||||||
|
|
||||||
|
.. config-table::
|
||||||
|
:config-target: RSD
|
||||||
|
|
||||||
|
cinder.volume.drivers.rsd
|
@ -44,3 +44,6 @@ infi.dtypes.iqn # PSF
|
|||||||
|
|
||||||
# Storpool
|
# Storpool
|
||||||
storpool # Apache-2.0
|
storpool # Apache-2.0
|
||||||
|
|
||||||
|
# RSD Driver
|
||||||
|
rsd-lib # Apache-2.0
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Added a new Cinder driver for RackScale Design NVMe-oF storage solution.
|
Loading…
Reference in New Issue
Block a user