cinder/cinder/volume/drivers/linstordrv.py

1160 lines
41 KiB
Python

# Copyright (c) 2014-2019 LINBIT HA Solutions GmbH
# 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.
"""This driver connects Cinder to an installed LINSTOR instance.
See https://docs.linbit.com/docs/users-guide-9.0/#ch-openstack-linstor
for more details.
"""
import socket
import uuid
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import importutils
from oslo_utils import units
from cinder import exception
from cinder.i18n import _
from cinder.image import image_utils
from cinder import interface
from cinder.volume import configuration
from cinder.volume import driver
try:
import linstor
lin_drv = linstor.Linstor
except ImportError:
linstor = None
lin_drv = None
# To override these values, update cinder.conf in /etc/cinder/
linstor_opts = [
cfg.StrOpt('linstor_default_volume_group_name',
default='drbd-vg',
help='Default Volume Group name for LINSTOR. '
'Not Cinder Volume.'),
cfg.StrOpt('linstor_default_uri',
default='linstor://localhost',
help='Default storage URI for LINSTOR.'),
cfg.StrOpt('linstor_default_storage_pool_name',
default='DfltStorPool',
help='Default Storage Pool name for LINSTOR.'),
cfg.FloatOpt('linstor_volume_downsize_factor',
default=4096,
help='Default volume downscale size in KiB = 4 MiB.'),
cfg.IntOpt('linstor_default_blocksize',
default=4096,
help='Default Block size for Image restoration. '
'When using iSCSI transport, this option '
'specifies the block size.'),
cfg.IntOpt('linstor_autoplace_count',
default=0,
help='Autoplace replication count on volume deployment. '
'0 = Full cluster replication without autoplace, '
'1 = Single node deployment without replication, '
'2 or greater = Replicated deployment with autoplace.'),
cfg.BoolOpt('linstor_controller_diskless',
default=True,
help='True means Cinder node is a diskless LINSTOR node.')
]
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CONF.register_opts(linstor_opts, group=configuration.SHARED_CONF_GROUP)
CINDER_UNKNOWN = 'unknown'
DM_VN_PREFIX = 'CV_'
DM_SN_PREFIX = 'SN_'
DISKLESS = 'DISKLESS'
LVM = 'LVM'
LVM_THIN = 'LVM_THIN'
ZFS = 'ZFS'
ZFS_THIN = 'ZFS_THIN'
class LinstorBaseDriver(driver.VolumeDriver):
"""Cinder driver that uses LINSTOR for storage.
Version history:
.. code-block:: none
1.0.0 - Initial driver
1.0.1 - Added support for LINSTOR 0.9.12
"""
VERSION = '1.0.1'
# ThirdPartySystems wiki page
CI_WIKI_NAME = 'LINBIT_LINSTOR_CI'
def __init__(self, *args, **kwargs):
super(LinstorBaseDriver, self).__init__(*args, **kwargs)
LOG.debug('START: Base Init Linstor')
self.configuration.append_config_values(linstor_opts)
self.default_pool = self.configuration.safe_get(
'linstor_default_storage_pool_name')
self.default_uri = self.configuration.safe_get(
'linstor_default_uri')
self.default_downsize_factor = self.configuration.safe_get(
'linstor_volume_downsize_factor')
self.default_vg_name = self.configuration.safe_get(
'linstor_default_volume_group_name')
self.default_blocksize = self.configuration.safe_get(
'linstor_default_blocksize')
self.diskless = self.configuration.safe_get(
'linstor_controller_diskless')
self.ap_count = self.configuration.safe_get(
'linstor_autoplace_count')
self.default_backend_name = self.configuration.safe_get(
'volume_backend_name')
self.host_name = socket.gethostname()
@staticmethod
def get_driver_options():
return linstor_opts
def _ping(self):
with lin_drv(self.default_uri) as lin:
return lin.ping()
def _clean_uuid(self):
"""Returns a UUID string, WITHOUT braces."""
# Some uuid library versions put braces around the result.
# We don't want them, just a plain [0-9a-f-]+ string.
uuid_str = str(uuid.uuid4())
uuid_str = uuid_str.replace("{", "")
uuid_str = uuid_str.replace("}", "")
return uuid_str
# LINSTOR works in kiB units; Cinder uses GiB.
def _vol_size_to_linstor(self, size):
return int(size * units.Mi - self.default_downsize_factor)
def _vol_size_to_cinder(self, size):
return int(size / units.Mi)
def _is_clean_volume_name(self, name, prefix):
try:
if (name.startswith(CONF.volume_name_template % "") and
uuid.UUID(name[7:]) is not None):
return prefix + name[7:]
except ValueError:
return None
try:
if uuid.UUID(name) is not None:
return prefix + name
except ValueError:
return None
def _snapshot_name_from_cinder_snapshot(self, snapshot):
sn_name = self._is_clean_volume_name(snapshot['id'], DM_SN_PREFIX)
return sn_name
def _cinder_volume_name_from_drbd_resource(self, rsc_name):
cinder_volume_name = rsc_name.split(DM_VN_PREFIX)[1]
return cinder_volume_name
def _drbd_resource_name_from_cinder_snapshot(self, snapshot):
drbd_resource_name = '{}{}'.format(DM_VN_PREFIX,
snapshot['volume_id'])
return drbd_resource_name
def _drbd_resource_name_from_cinder_volume(self, volume):
drbd_resource_name = '{}{}'.format(DM_VN_PREFIX, volume['id'])
return drbd_resource_name
def _get_api_resource_list(self):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
api_reply = lin.resource_list()[0].__dict__['_rest_data']
return api_reply
def _get_api_resource_dfn_list(self):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
api_reply = lin.resource_dfn_list()[0].__dict__['_rest_data']
return api_reply
def _get_api_node_list(self):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
api_reply = lin.node_list()[0].__dict__['_rest_data']
return api_reply
def _get_api_storage_pool_dfn_list(self):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
api_reply = lin.storage_pool_dfn_list()[0].__dict__['_rest_data']
return api_reply
def _get_api_storage_pool_list(self):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
api_reply = lin.storage_pool_list()[0].__dict__['_rest_data']
return api_reply
def _get_api_volume_extend(self, rsc_target_name, new_size):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
vol_reply = lin.volume_dfn_modify(
rsc_name=rsc_target_name,
volume_nr=0,
size=self._vol_size_to_linstor(new_size))
return vol_reply
def _api_snapshot_create(self, drbd_rsc_name, snapshot_name):
lin = linstor.Resource(drbd_rsc_name, uri=self.default_uri)
snap_reply = lin.snapshot_create(snapshot_name)
return snap_reply
def _api_snapshot_delete(self, drbd_rsc_name, snapshot_name):
lin = linstor.Resource(drbd_rsc_name, uri=self.default_uri)
snap_reply = lin.snapshot_delete(snapshot_name)
return snap_reply
def _api_rsc_dfn_delete(self, drbd_rsc_name):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
snap_reply = lin.resource_dfn_delete(drbd_rsc_name)
return snap_reply
def _api_storage_pool_create(self,
node_name,
storage_pool_name,
storage_driver,
driver_pool_name):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
sp_reply = lin.storage_pool_create(
node_name=node_name,
storage_pool_name=storage_pool_name,
storage_driver=storage_driver,
driver_pool_name=driver_pool_name)
return sp_reply
def _api_rsc_dfn_create(self, rsc_name):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
rsc_dfn_reply = lin.resource_dfn_create(rsc_name)
return rsc_dfn_reply
def _api_volume_dfn_create(self, rsc_name, size):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
vol_dfn_reply = lin.volume_dfn_create(
rsc_name=rsc_name,
storage_pool=self.default_pool,
size=size)
return vol_dfn_reply
def _api_volume_dfn_set_sp(self, rsc_target_name):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
snap_reply = lin.volume_dfn_modify(
rsc_name=rsc_target_name,
volume_nr=0,
set_properties={
'StorPoolName': self.default_pool
})
return snap_reply
def _api_rsc_create(self, rsc_name, node_name, diskless=False):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
if diskless:
storage_pool = None
else:
storage_pool = self.default_pool
new_rsc = linstor.ResourceData(rsc_name=rsc_name,
node_name=node_name,
storage_pool=storage_pool,
diskless=diskless)
rsc_reply = lin.resource_create([new_rsc], async_msg=False)
return rsc_reply
def _api_rsc_autoplace(self, rsc_name):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
new_rsc = linstor.Resource(name=rsc_name, uri=self.default_uri)
new_rsc.placement.redundancy = self.ap_count
new_rsc.placement.storage_pool = self.default_pool
rsc_reply = new_rsc.autoplace()
return rsc_reply
def _api_rsc_delete(self, rsc_name, node_name):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
rsc_reply = lin.resource_delete(node_name=node_name,
rsc_name=rsc_name)
return rsc_reply
def _api_rsc_auto_delete(self, rsc_name):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
rsc = linstor.Resource(str(rsc_name), self.default_uri)
return rsc.delete()
def _api_rsc_is_diskless(self, rsc_name):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
rsc = linstor.Resource(str(rsc_name))
return rsc.is_diskless(self.host_name)
def _api_rsc_size(self, rsc_name):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
rsc = linstor.Resource(str(rsc_name))
if len(rsc.volumes):
if "size" in rsc.volumes:
return rsc.volumes[0].size
else:
return 0
else:
return 0
def _api_volume_dfn_delete(self, rsc_name, volume_nr):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
rsc_reply = lin.volume_dfn_delete(rsc_name=rsc_name,
volume_nr=volume_nr)
return rsc_reply
def _api_snapshot_volume_dfn_restore(self,
src_rsc_name,
src_snap_name,
new_vol_name):
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
vol_reply = lin.snapshot_volume_definition_restore(
from_resource=src_rsc_name,
from_snapshot=src_snap_name,
to_resource=new_vol_name)
return vol_reply
def _api_snapshot_resource_restore(self,
src_rsc_name,
src_snap_name,
new_vol_name):
lin = linstor.Resource(src_rsc_name, uri=self.default_uri)
new_rsc = lin.restore_from_snapshot(src_snap_name, new_vol_name)
# Adds an aux/property KV for synchronous return from snapshot restore
with lin_drv(self.default_uri) as lin:
if not lin.connected:
lin.connect()
aux_prop = {}
aux_prop["Aux/restore"] = "done"
lin.volume_dfn_modify(
rsc_name=new_vol_name,
volume_nr=0,
set_properties=aux_prop)
if new_rsc.name == new_vol_name:
return True
return False
def _get_rsc_path(self, rsc_name):
rsc_list_reply = self._get_api_resource_list()
if rsc_list_reply:
for rsc in rsc_list_reply:
if (rsc["name"] == rsc_name and
rsc["node_name"] == self.host_name):
for volume in rsc["volumes"]:
if volume["volume_number"] == 0:
return volume["device_path"]
def _get_local_path(self, volume):
try:
full_rsc_name = (
self._drbd_resource_name_from_cinder_volume(volume))
return self._get_rsc_path(full_rsc_name)
except Exception:
message = _('Local Volume not found.')
raise exception.VolumeBackendAPIException(data=message)
def _get_spd(self):
# Storage Pool Definition List
spd_list_reply = self._get_api_storage_pool_dfn_list()
spd_list = []
if spd_list_reply:
for spd in spd_list_reply:
spd_list.append(spd["storage_pool_name"])
return spd_list
def _get_storage_pool(self):
# Fetch Storage Pool List
sp_list_reply = self._get_api_storage_pool_list()
# Separate the diskless nodes
sp_diskless_list = []
sp_list = []
node_count = 0
if sp_list_reply:
for node in sp_list_reply:
if node["storage_pool_name"] == self.default_pool:
sp_node = {}
sp_node["node_name"] = node["node_name"]
sp_node["sp_uuid"] = node["uuid"]
sp_node["sp_name"] = node["storage_pool_name"]
if node["provider_kind"] == DISKLESS:
diskless = True
sp_node["sp_free"] = -1.0
sp_node["sp_cap"] = -1.0
sp_node["sp_allocated"] = 0.0
else:
diskless = False
if "free_capacity" in node:
temp = float(node["free_capacity"]) / units.Mi
sp_node["sp_free"] = round(temp)
temp = float(node["total_capacity"]) / units.Mi
sp_node["sp_cap"] = round(temp)
drivers = [LVM, LVM_THIN, ZFS, ZFS_THIN, DISKLESS]
# Driver selection
if node["provider_kind"] in drivers:
sp_node['driver_name'] = node["provider_kind"]
else:
sp_node['driver_name'] = str(node["provider_kind"])
if diskless:
sp_diskless_list.append(sp_node)
else:
sp_list.append(sp_node)
node_count += 1
# Add the diskless nodes to the end of the list
if sp_diskless_list:
sp_list.extend(sp_diskless_list)
return sp_list
def _get_volume_stats(self):
data = {}
data["volume_backend_name"] = self.default_backend_name
data["vendor_name"] = "LINBIT"
data["driver_version"] = self.VERSION
data["pools"] = []
sp_data = self._get_storage_pool()
rd_list = self._get_resource_definitions()
# Total volumes and capacity
num_vols = 0
for rd in rd_list:
num_vols += 1
# allocated_sizes_gb = []
free_gb = []
total_gb = []
thin_enabled = False
# Total & Free capacity for Local Node
single_pool = {}
for sp in sp_data:
if "Diskless" not in sp["driver_name"]:
thin_backends = [LVM_THIN, ZFS_THIN]
if sp["driver_name"] in thin_backends:
thin_enabled = True
if "sp_cap" in sp:
if sp["sp_cap"] >= 0.0:
total_gb.append(sp["sp_cap"])
if "sp_free" in sp:
if sp["sp_free"] >= 0.0:
free_gb.append(sp["sp_free"])
# Allocated capacity
sp_allocated_size_gb = 0.0
local_resources = []
reply = self._get_api_resource_list()
if reply:
for rsc in reply:
if rsc["node_name"] == self.host_name:
local_resources.append(rsc["name"])
for rsc_name in local_resources:
if not self._api_rsc_is_diskless(rsc_name):
rsc_size = self._api_rsc_size(rsc_name)
sp_allocated_size_gb += round(
int(rsc_size) / units.Gi, 2)
single_pool["pool_name"] = data["volume_backend_name"]
single_pool["free_capacity_gb"] = min(free_gb) if free_gb else 0
single_pool["total_capacity_gb"] = min(total_gb) if total_gb else 0
single_pool["provisioned_capacity_gb"] = sp_allocated_size_gb
single_pool["reserved_percentage"] = (
self.configuration.reserved_percentage)
single_pool["thin_provisioning_support"] = thin_enabled
single_pool["thick_provisioning_support"] = not thin_enabled
single_pool["max_over_subscription_ratio"] = (
self.configuration.max_over_subscription_ratio)
single_pool["location_info"] = self.default_uri
single_pool["total_volumes"] = num_vols
single_pool["filter_function"] = self.get_filter_function()
single_pool["goodness_function"] = self.get_goodness_function()
single_pool["QoS_support"] = False
single_pool["multiattach"] = False
single_pool["backend_state"] = "up"
data["pools"].append(single_pool)
return data
def _get_resource_definitions(self):
rd_list_reply = self._get_api_resource_dfn_list()
rd_list = []
if rd_list_reply:
for node in rd_list_reply:
# Count only Cinder volumes
if DM_VN_PREFIX in node['name']:
rd_node = {}
rd_node["rd_uuid"] = node['uuid']
rd_node["rd_name"] = node['name']
rd_list.append(rd_node)
return rd_list
def _get_snapshot_nodes(self, resource):
"""Returns all available resource nodes for snapshot.
However, it excludes diskless nodes.
"""
rsc_list_reply = self._get_api_resource_list()
snap_list = []
if rsc_list_reply:
for rsc in rsc_list_reply:
if rsc["name"] != resource:
continue
# Diskless nodes are not available for snapshots
diskless = False
if "flags" in rsc:
if 'DISKLESS' in rsc["flags"]:
diskless = True
if not diskless:
snap_list.append(rsc["node_name"])
return snap_list
def _get_diskless_nodes(self, resource):
# Returns diskless nodes given a resource
rsc_list_reply = self._get_api_resource_list()
diskless_list = []
if rsc_list_reply:
for rsc in rsc_list_reply:
if rsc["name"] != resource:
continue
if "flags" in rsc:
if DISKLESS in rsc["flags"]:
diskless_list.append(rsc["node_name"])
return diskless_list
def _get_linstor_nodes(self):
# Returns all available LINSTOR nodes
node_list_reply = self._get_api_node_list()
node_list = []
if node_list_reply:
for node in node_list_reply:
node_list.append(node["name"])
return node_list
def _get_nodes(self):
# Returns all LINSTOR nodes in a dict list
node_list_reply = self._get_api_node_list()
node_list = []
if node_list_reply:
for node in node_list_reply:
node_item = {}
node_item["node_name"] = node["name"]
node_item["node_address"] = (
node["net_interfaces"][0]["address"])
node_list.append(node_item)
return node_list
def _check_api_reply(self, api_response, noerror_only=False):
if noerror_only:
# Checks if none of the replies has an error
return lin_drv.all_api_responses_no_error(api_response)
else:
# Check if all replies are success
return lin_drv.all_api_responses_success(api_response)
def _copy_vol_to_image(self, context, image_service, image_meta, rsc_path,
store_id=None):
return image_utils.upload_volume(context,
image_service,
image_meta,
rsc_path,
store_id=store_id)
#
# Snapshot
#
def create_snapshot(self, snapshot):
snap_name = self._snapshot_name_from_cinder_snapshot(snapshot)
rsc_name = self._drbd_resource_name_from_cinder_snapshot(snapshot)
snap_reply = self._api_snapshot_create(drbd_rsc_name=rsc_name,
snapshot_name=snap_name)
if not snap_reply:
msg = 'ERROR creating a LINSTOR snapshot {}'.format(snap_name)
LOG.error(msg)
raise exception.VolumeBackendAPIException(msg)
def delete_snapshot(self, snapshot):
snapshot_name = self._snapshot_name_from_cinder_snapshot(snapshot)
rsc_name = self._drbd_resource_name_from_cinder_snapshot(snapshot)
snap_reply = self._api_snapshot_delete(rsc_name, snapshot_name)
if not snap_reply:
msg = 'ERROR deleting a LINSTOR snapshot {}'.format(snapshot_name)
LOG.error(msg)
raise exception.VolumeBackendAPIException(msg)
# Delete RD if no other RSC are found
if not self._get_snapshot_nodes(rsc_name):
self._api_rsc_dfn_delete(rsc_name)
def create_volume_from_snapshot(self, volume, snapshot):
src_rsc_name = self._drbd_resource_name_from_cinder_snapshot(snapshot)
src_snap_name = self._snapshot_name_from_cinder_snapshot(snapshot)
new_vol_name = self._drbd_resource_name_from_cinder_volume(volume)
# If no autoplace, manually build a cluster list
if self.ap_count == 0:
diskless_nodes = []
nodes = []
for node in self._get_storage_pool():
if DISKLESS in node['driver_name']:
diskless_nodes.append(node['node_name'])
continue
# Filter out controller node if it is diskless
if self.diskless and node['node_name'] == self.host_name:
continue
else:
nodes.append(node['node_name'])
reply = self._api_snapshot_resource_restore(src_rsc_name,
src_snap_name,
new_vol_name)
if not reply:
msg = _('Error on restoring a LINSTOR volume')
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
# Manually add the controller node as a resource if diskless
if self.diskless:
reply = self._api_rsc_create(rsc_name=new_vol_name,
node_name=self.host_name,
diskless=self.diskless)
# Add any other diskless nodes only if not autoplaced
if self.ap_count == 0 and diskless_nodes:
for node in diskless_nodes:
self._api_rsc_create(rsc_name=new_vol_name,
node_name=node,
diskless=True)
# Upsize if larger volume than original snapshot
src_rsc_size = int(snapshot['volume_size'])
new_vol_size = int(volume['size'])
if new_vol_size > src_rsc_size:
upsize_target_name = self._is_clean_volume_name(volume['id'],
DM_VN_PREFIX)
reply = self._get_api_volume_extend(
rsc_target_name=upsize_target_name,
new_size=new_vol_size)
if not self._check_api_reply(reply, noerror_only=True):
# Delete failed volume
failed_volume = {}
failed_volume['id'] = volume['id']
self.delete_volume(failed_volume)
msg = _('Error on extending LINSTOR resource size')
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
def create_volume(self, volume):
# Check for Storage Pool List
sp_data = self._get_storage_pool()
rsc_size = volume['size']
# No existing Storage Pools found
if not sp_data:
# Check for Nodes
node_list = self._get_nodes()
if not node_list:
msg = _('No LINSTOR resource nodes available / configured')
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
# Create Storage Pool
spd_list = self._get_spd()
if spd_list:
spd_name = spd_list[0]
for node in node_list:
node_driver = None
for sp in sp_data:
if sp['node_name'] == node['node_name']:
node_driver = sp['driver_name']
sp_reply = self._api_storage_pool_create(
node_name=node['node_name'],
storage_pool_name=spd_name,
storage_driver=node_driver,
driver_pool_name=self.default_vg_name)
if not self._check_api_reply(sp_reply, noerror_only=True):
msg = _('Could not create a LINSTOR storage pool')
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
# Check for RD
# If Retyping from another volume, use parent/origin uuid
# as a name source
if (volume['migration_status'] is not None and
str(volume['migration_status']).find('success') == -1):
src_name = str(volume['migration_status']).split(':')[1]
rsc_name = self._is_clean_volume_name(str(src_name),
DM_VN_PREFIX)
else:
rsc_name = self._is_clean_volume_name(volume['id'],
DM_VN_PREFIX)
# Create a New RD
rsc_dfn_reply = self._api_rsc_dfn_create(rsc_name)
if not self._check_api_reply(rsc_dfn_reply, noerror_only=True):
msg = _("Error creating a LINSTOR resource definition")
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
# Create a New VD
vd_size = self._vol_size_to_linstor(rsc_size)
vd_reply = self._api_volume_dfn_create(rsc_name=rsc_name,
size=int(vd_size))
if not self._check_api_reply(vd_reply, noerror_only=True):
msg = _("Error creating a LINSTOR volume definition")
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
# Create LINSTOR Resources
ctrl_in_sp = False
for node in sp_data:
# Check if controller is in the pool
if node['node_name'] == self.host_name:
ctrl_in_sp = True
# Use autoplace to deploy if set
if self.ap_count:
try:
self._api_rsc_autoplace(rsc_name=rsc_name)
except Exception:
msg = _("Error creating autoplaces LINSTOR resource(s)")
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
# Otherwise deploy across the entire cluster
else:
for node in sp_data:
# Deploy resource on each node
if DISKLESS in node['driver_name']:
diskless = True
else:
diskless = False
rsc_reply = self._api_rsc_create(rsc_name=rsc_name,
node_name=node['node_name'],
diskless=diskless)
if not self._check_api_reply(rsc_reply, noerror_only=True):
msg = _("Error creating a LINSTOR resource")
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
# If the controller is diskless and not in the pool, create a diskless
# resource on it
if not ctrl_in_sp and self.diskless:
rsc_reply = self._api_rsc_create(rsc_name=rsc_name,
node_name=self.host_name,
diskless=True)
if not self._check_api_reply(rsc_reply, noerror_only=True):
msg = _("Error creating a LINSTOR controller resource")
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
return {}
def delete_volume(self, volume):
drbd_rsc_name = self._drbd_resource_name_from_cinder_volume(volume)
rsc_list_reply = self._get_api_resource_list()
diskful_nodes = self._get_snapshot_nodes(drbd_rsc_name)
diskless_nodes = self._get_diskless_nodes(drbd_rsc_name)
# If autoplace was used, use Resource class
if self.ap_count:
rsc_reply = self._api_rsc_auto_delete(drbd_rsc_name)
if not rsc_reply:
msg = _("Error deleting an autoplaced LINSTOR resource")
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
# Delete all resources in a cluster manually if not autoplaced
else:
if rsc_list_reply:
# Remove diskless nodes first
if diskless_nodes:
for node in diskless_nodes:
rsc_reply = self._api_rsc_delete(
node_name=node,
rsc_name=drbd_rsc_name)
if not self._check_api_reply(rsc_reply,
noerror_only=True):
msg = _("Error deleting a diskless LINSTOR rsc")
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
# Remove diskful nodes
if diskful_nodes:
for node in diskful_nodes:
rsc_reply = self._api_rsc_delete(
node_name=node,
rsc_name=drbd_rsc_name)
if not self._check_api_reply(rsc_reply,
noerror_only=True):
msg = _("Error deleting a LINSTOR resource")
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
# Delete VD
vd_reply = self._api_volume_dfn_delete(drbd_rsc_name, 0)
if not vd_reply:
if not self._check_api_reply(vd_reply):
msg = _("Error deleting a LINSTOR volume definition")
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
# Delete RD
# Will fail if snapshot exists but expected
self._api_rsc_dfn_delete(drbd_rsc_name)
return True
def extend_volume(self, volume, new_size):
rsc_target_name = self._is_clean_volume_name(volume['id'],
DM_VN_PREFIX)
extend_reply = self._get_api_volume_extend(rsc_target_name, new_size)
if not self._check_api_reply(extend_reply, noerror_only=True):
msg = _("ERROR extending a LINSTOR volume")
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
def create_cloned_volume(self, volume, src_vref):
temp_id = self._clean_uuid()
snapshot = {}
snapshot['id'] = temp_id
snapshot['volume_id'] = src_vref['id']
snapshot['volume_size'] = src_vref['size']
self.create_snapshot(snapshot)
self.create_volume_from_snapshot(volume, snapshot)
self.delete_snapshot(snapshot)
def copy_image_to_volume(self, context, volume, image_service, image_id):
# self.create_volume(volume) already called by Cinder, and works
full_rsc_name = self._drbd_resource_name_from_cinder_volume(volume)
# This creates a LINSTOR volume from the source image
image_utils.fetch_to_raw(context,
image_service,
image_id,
str(self._get_rsc_path(full_rsc_name)),
self.default_blocksize,
size=volume['size'])
return {}
def copy_volume_to_image(self, context, volume, image_service, image_meta):
full_rsc_name = self._drbd_resource_name_from_cinder_volume(volume)
rsc_path = str(self._get_rsc_path(full_rsc_name))
# retrieve store information from extra-specs
store_id = volume.volume_type.extra_specs.get('image_service:store_id')
self._copy_vol_to_image(context,
image_service,
image_meta,
rsc_path,
store_id=store_id)
return {}
# Not supported currently
def migrate_volume(self, ctxt, volume, host, thin=False, mirror_count=0):
return (False, None)
def check_for_setup_error(self):
msg = None
if linstor is None:
msg = _('Linstor python package not found')
if msg is not None:
LOG.error(msg)
raise exception.VolumeDriverException(message=msg)
def create_export(self, context, volume, connector):
pass
def ensure_export(self, context, volume):
pass
def initialize_connection(self, volume, connector, **kwargs):
pass
def remove_export(self, context, volume):
pass
def terminate_connection(self, volume, connector, **kwargs):
pass
# Class with iSCSI interface methods
@interface.volumedriver
class LinstorIscsiDriver(LinstorBaseDriver):
"""Cinder iSCSI driver that uses LINSTOR for storage."""
def __init__(self, *args, **kwargs):
super(LinstorIscsiDriver, self).__init__(*args, **kwargs)
# iSCSI target_helper
if 'h_name' in kwargs:
self.helper_name = kwargs.get('h_name')
self.helper_driver = self.helper_name
self.target_driver = None
else:
self.helper_name = self.configuration.safe_get('iscsi_helper')
self.helper_driver = self.target_mapping[self.helper_name]
self.target_driver = importutils.import_object(
self.helper_driver,
configuration=self.configuration,
db=self.db,
executor=self._execute)
LOG.info('START: LINSTOR DRBD driver {}'.format(self.helper_name))
def get_volume_stats(self, refresh=False):
data = self._get_volume_stats()
data["storage_protocol"] = 'iSCSI'
data["pools"][0]["location_info"] = (
'LinstorIscsiDriver:' + data["pools"][0]["location_info"])
return data
def ensure_export(self, context, volume):
volume_path = self._get_local_path(volume)
return self.target_driver.ensure_export(
context,
volume,
volume_path)
def create_export(self, context, volume, connector):
volume_path = self._get_local_path(volume)
export_info = self.target_driver.create_export(
context,
volume,
volume_path)
return {'provider_location': export_info['location'],
'provider_auth': export_info['auth'], }
def remove_export(self, context, volume):
return self.target_driver.remove_export(context, volume)
def initialize_connection(self, volume, connector, **kwargs):
return self.target_driver.initialize_connection(volume, connector)
def validate_connector(self, connector):
return self.target_driver.validate_connector(connector)
def terminate_connection(self, volume, connector, **kwargs):
return self.target_driver.terminate_connection(volume,
connector,
**kwargs)
# Class with DRBD transport mode
@interface.volumedriver
class LinstorDrbdDriver(LinstorBaseDriver):
"""Cinder DRBD driver that uses LINSTOR for storage."""
def __init__(self, *args, **kwargs):
super(LinstorDrbdDriver, self).__init__(*args, **kwargs)
def _return_drbd_config(self, volume):
full_rsc_name = self._drbd_resource_name_from_cinder_volume(volume)
rsc_path = self._get_rsc_path(full_rsc_name)
return {
'driver_volume_type': 'local',
'data': {
"device_path": str(rsc_path)
}
}
def _node_in_sp(self, node_name):
for pool in self._get_storage_pool():
if pool['node_name'] == node_name:
return True
return False
def get_volume_stats(self, refresh=False):
data = self._get_volume_stats()
data["storage_protocol"] = 'DRBD'
data["pools"][0]["location_info"] = 'LinstorDrbdDriver:{}'.format(
data["pools"][0]["location_info"])
return data
def initialize_connection(self, volume, connector, **kwargs):
node_name = connector['host']
if not self._node_in_sp(connector['host']):
full_rsc_name = self._drbd_resource_name_from_cinder_volume(volume)
rsc_reply = self._api_rsc_create(rsc_name=full_rsc_name,
node_name=node_name,
diskless=True)
if not self._check_api_reply(rsc_reply, noerror_only=True):
msg = _('Error on creating LINSTOR Resource')
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
return self._return_drbd_config(volume)
def terminate_connection(self, volume, connector, **kwargs):
if connector:
node_name = connector['host']
if not self._node_in_sp(connector['host']):
rsc_name = self._drbd_resource_name_from_cinder_volume(volume)
rsc_reply = self._api_rsc_delete(rsc_name=rsc_name,
node_name=node_name)
if not self._check_api_reply(rsc_reply, noerror_only=True):
msg = _('Error on deleting LINSTOR Resource')
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
def create_export(self, context, volume, connector):
return self._return_drbd_config(volume)
def ensure_export(self, context, volume):
return self._return_drbd_config(volume)
def remove_export(self, context, volume):
pass