
LINSTOR is a storage orchestrator for DRBD. Change-Id: Ic3a4f85c10a2432d4128fce08604e9868722e8f7
1089 lines
39 KiB
Python
1089 lines
39 KiB
Python
# Copyright (c) 2014-2018 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
|
|
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 google.protobuf.json_format as proto
|
|
except ImportError:
|
|
proto = None
|
|
|
|
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.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_'
|
|
LVM = 'Lvm'
|
|
LVMTHIN = 'LvmThin'
|
|
|
|
|
|
class LinstorBaseDriver(driver.VolumeDriver):
|
|
"""Cinder driver that uses Linstor for storage."""
|
|
|
|
VERSION = '1.0.0'
|
|
|
|
# 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.default_backend_name = self.configuration.safe_get(
|
|
'volume_backend_name')
|
|
self.host_name = socket.gethostname()
|
|
|
|
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()
|
|
|
|
response = proto.MessageToDict(lin.resource_list()[0].proto_msg)
|
|
return response
|
|
|
|
def _get_api_resource_dfn_list(self):
|
|
with lin_drv(self.default_uri) as lin:
|
|
if not lin.connected:
|
|
lin.connect()
|
|
|
|
response = proto.MessageToDict(
|
|
lin.resource_dfn_list()[0].proto_msg)
|
|
return response
|
|
|
|
def _get_api_nodes_list(self):
|
|
with lin_drv(self.default_uri) as lin:
|
|
if not lin.connected:
|
|
lin.connect()
|
|
|
|
response = proto.MessageToDict(lin.node_list()[0].proto_msg)
|
|
return response
|
|
|
|
def _get_api_storage_pool_dfn_list(self):
|
|
with lin_drv(self.default_uri) as lin:
|
|
if not lin.connected:
|
|
lin.connect()
|
|
|
|
response = proto.MessageToDict(
|
|
lin.storage_pool_dfn_list()[0].proto_msg)
|
|
return response
|
|
|
|
def _get_api_storage_pool_list(self):
|
|
with lin_drv(self.default_uri) as lin:
|
|
if not lin.connected:
|
|
lin.connect()
|
|
|
|
response = proto.MessageToDict(
|
|
lin.storage_pool_list()[0].proto_msg)
|
|
return response
|
|
|
|
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, node_names, rsc_name, snapshot_name):
|
|
with lin_drv(self.default_uri) as lin:
|
|
if not lin.connected:
|
|
lin.connect()
|
|
|
|
snap_reply = lin.snapshot_create(node_names=node_names,
|
|
rsc_name=rsc_name,
|
|
snapshot_name=snapshot_name,
|
|
async_msg=False)
|
|
return snap_reply
|
|
|
|
def _api_snapshot_delete(self, drbd_rsc_name, snap_name):
|
|
with lin_drv(self.default_uri) as lin:
|
|
if not lin.connected:
|
|
lin.connect()
|
|
|
|
snap_reply = lin.snapshot_delete(rsc_name=drbd_rsc_name,
|
|
snapshot_name=snap_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_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_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,
|
|
nodes,
|
|
src_rsc_name,
|
|
src_snap_name,
|
|
new_vol_name):
|
|
with lin_drv(self.default_uri) as lin:
|
|
if not lin.connected:
|
|
lin.connect()
|
|
|
|
rsc_reply = lin.snapshot_resource_restore(
|
|
node_names=nodes,
|
|
from_resource=src_rsc_name,
|
|
from_snapshot=src_snap_name,
|
|
to_resource=new_vol_name)
|
|
return rsc_reply
|
|
|
|
def _get_rsc_path(self, rsc_name):
|
|
rsc_list_reply = self._get_api_resource_list()
|
|
|
|
for rsc in rsc_list_reply['resources']:
|
|
if rsc['name'] == rsc_name and rsc['nodeName'] == self.host_name:
|
|
for volume in rsc['vlms']:
|
|
if volume['vlmNr'] == 0:
|
|
return volume['backingDisk']
|
|
|
|
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 = []
|
|
for node in spd_list_reply['storPoolDfns']:
|
|
spd_item = {}
|
|
spd_item['spd_uuid'] = node['uuid']
|
|
spd_item['spd_name'] = node['storPoolName']
|
|
spd_list.append(spd_item)
|
|
|
|
return spd_list
|
|
|
|
def _get_storage_pool(self):
|
|
# Fetch Storage Pool List
|
|
sp_list_reply = self._get_api_storage_pool_list()
|
|
|
|
# Fetch Resource Definition List
|
|
sp_list = []
|
|
|
|
# Separate the diskless nodes
|
|
sp_diskless_list = []
|
|
node_count = 0
|
|
|
|
if sp_list_reply:
|
|
for node in sp_list_reply['storPools']:
|
|
if node['storPoolName'] == self.default_pool:
|
|
sp_node = {}
|
|
sp_node['node_uuid'] = node['nodeUuid']
|
|
sp_node['node_name'] = node['nodeName']
|
|
sp_node['sp_uuid'] = node['storPoolUuid']
|
|
sp_node['sp_name'] = node['storPoolName']
|
|
sp_node['sp_vlms_uuid'] = []
|
|
if 'vlms' in node:
|
|
for vlm in node['vlms']:
|
|
sp_node['sp_vlms_uuid'].append(vlm['vlmDfnUuid'])
|
|
|
|
if 'Diskless' in node['driver']:
|
|
diskless = True
|
|
sp_node['sp_free'] = -1.0
|
|
sp_node['sp_cap'] = 0.0
|
|
else:
|
|
diskless = False
|
|
if 'freeSpace' in node:
|
|
sp_node['sp_free'] = round(
|
|
int(node['freeSpace']['freeCapacity']) /
|
|
units.Mi,
|
|
2)
|
|
sp_node['sp_cap'] = round(
|
|
int(node['freeSpace']['totalCapacity']) /
|
|
units.Mi,
|
|
2)
|
|
|
|
# Driver
|
|
if node['driver'] == "LvmDriver":
|
|
sp_node['driver_name'] = LVM
|
|
elif node['driver'] == "LvmThinDriver":
|
|
sp_node['driver_name'] = LVMTHIN
|
|
else:
|
|
sp_node['driver_name'] = node['driver']
|
|
|
|
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_capacity_gb = []
|
|
total_capacity_gb = []
|
|
thin_enabled = False
|
|
|
|
# Free capacity for Local Node
|
|
single_pool = {}
|
|
for sp in sp_data:
|
|
if 'Diskless' not in sp['driver_name']:
|
|
if 'LvmThin' in sp['driver_name']:
|
|
thin_enabled = True
|
|
if 'sp_cap' in sp:
|
|
if sp['sp_cap'] >= 0.0:
|
|
total_capacity_gb.append(sp['sp_cap'])
|
|
if 'sp_free' in sp:
|
|
if sp['sp_free'] >= 0.0:
|
|
free_capacity_gb.append(sp['sp_free'])
|
|
sp_allocated_size_gb = 0
|
|
for vlm_uuid in sp['sp_vlms_uuid']:
|
|
for rd in rd_list:
|
|
if 'vlm_dfn_uuid' in rd:
|
|
if rd['vlm_dfn_uuid'] == vlm_uuid:
|
|
sp_allocated_size_gb += rd['rd_size']
|
|
allocated_sizes_gb.append(sp_allocated_size_gb)
|
|
|
|
single_pool["pool_name"] = data["volume_backend_name"]
|
|
single_pool["free_capacity_gb"] = min(free_capacity_gb)
|
|
single_pool["total_capacity_gb"] = min(total_capacity_gb)
|
|
single_pool['provisioned_capacity_gb'] = max(allocated_sizes_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 = []
|
|
|
|
rd_list_reply = self._get_api_resource_dfn_list()
|
|
|
|
# Only if resource definition present
|
|
if 'rscDfns' in rd_list_reply:
|
|
for node in rd_list_reply['rscDfns']:
|
|
|
|
# Count only Cinder volumes
|
|
if DM_VN_PREFIX in node['rscName']:
|
|
rd_node = {}
|
|
rd_node['rd_uuid'] = node['rscDfnUuid']
|
|
rd_node['rd_name'] = node['rscName']
|
|
rd_node['rd_port'] = node['rscDfnPort']
|
|
|
|
if 'vlmDfns' in node:
|
|
for vol in node['vlmDfns']:
|
|
if vol['vlmNr'] == 0:
|
|
rd_node['vlm_dfn_uuid'] = vol['vlmDfnUuid']
|
|
rd_node['rd_size'] = round(
|
|
float(vol['vlmSize']) / units.Mi, 2)
|
|
break
|
|
|
|
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() # reply in dict
|
|
|
|
snap_list = []
|
|
for rsc in rsc_list_reply['resources']:
|
|
if rsc['name'] != resource:
|
|
continue
|
|
|
|
# Diskless nodes are not available for snapshots
|
|
diskless = False
|
|
if 'rscFlags' in rsc:
|
|
if 'DISKLESS' in rsc['rscFlags']:
|
|
diskless = True
|
|
if not diskless:
|
|
snap_list.append(rsc['nodeName'])
|
|
|
|
return snap_list
|
|
|
|
def _get_linstor_nodes(self):
|
|
# Returns all available DRBD nodes
|
|
node_list_reply = self._get_api_nodes_list()
|
|
|
|
node_list = []
|
|
for node in node_list_reply['nodes']:
|
|
node_list.append(node['name'])
|
|
|
|
return node_list
|
|
|
|
def _get_nodes(self):
|
|
# Get Node List
|
|
node_list_reply = self._get_api_nodes_list()
|
|
|
|
node_list = []
|
|
if node_list_reply:
|
|
for node in node_list_reply['nodes']:
|
|
node_item = {}
|
|
node_item['node_name'] = node['name']
|
|
node_item['node_uuid'] = node['uuid']
|
|
node_item['node_address'] = (
|
|
node['netInterfaces'][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):
|
|
|
|
return image_utils.upload_volume(context,
|
|
image_service,
|
|
image_meta,
|
|
rsc_path)
|
|
|
|
#
|
|
# Snapshot
|
|
#
|
|
def create_snapshot(self, snapshot):
|
|
snap_name = self._snapshot_name_from_cinder_snapshot(snapshot)
|
|
drbd_rsc_name = self._drbd_resource_name_from_cinder_snapshot(snapshot)
|
|
node_names = self._get_snapshot_nodes(drbd_rsc_name)
|
|
|
|
snap_reply = self._api_snapshot_create(node_names=node_names,
|
|
rsc_name=drbd_rsc_name,
|
|
snapshot_name=snap_name)
|
|
|
|
if not self._check_api_reply(snap_reply, noerror_only=True):
|
|
msg = 'ERROR creating a LINSTOR snapshot {}'.format(snap_name)
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(msg)
|
|
|
|
def delete_snapshot(self, snapshot):
|
|
snap_name = self._snapshot_name_from_cinder_snapshot(snapshot)
|
|
drbd_rsc_name = self._drbd_resource_name_from_cinder_snapshot(snapshot)
|
|
|
|
snap_reply = self._api_snapshot_delete(drbd_rsc_name, snap_name)
|
|
|
|
if not self._check_api_reply(snap_reply, noerror_only=True):
|
|
msg = 'ERROR deleting a LINSTOR snapshot {}'.format(snap_name)
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(msg)
|
|
|
|
# Delete RD if no other RSC are found
|
|
if not self._get_snapshot_nodes(drbd_rsc_name):
|
|
self._api_rsc_dfn_delete(drbd_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)
|
|
|
|
# New RD
|
|
rsc_reply = self._api_rsc_dfn_create(new_vol_name)
|
|
|
|
if not self._check_api_reply(rsc_reply):
|
|
msg = _('Error on creating LINSTOR Resource Definition')
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
# New VD from Snap
|
|
reply = self._api_snapshot_volume_dfn_restore(src_rsc_name,
|
|
src_snap_name,
|
|
new_vol_name)
|
|
if not self._check_api_reply(reply, noerror_only=True):
|
|
msg = _('Error on restoring LINSTOR Volume Definition')
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
# Set StorPoolName property on VD
|
|
reply = self._api_volume_dfn_set_sp(new_vol_name)
|
|
if not self._check_api_reply(reply):
|
|
msg = _('Error on restoring LINSTOR Volume StorPoolName property')
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
# New RSC from Snap
|
|
# Assumes restoring to all the nodes containing the storage pool
|
|
# unless diskless
|
|
nodes = []
|
|
for node in self._get_storage_pool():
|
|
|
|
if 'Diskless' in node['driver_name']:
|
|
continue
|
|
|
|
# Filter out controller node if LINSTOR 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(nodes,
|
|
src_rsc_name,
|
|
src_snap_name,
|
|
new_vol_name)
|
|
if not self._check_api_reply(reply, noerror_only=True):
|
|
msg = _('Error on restoring LINSTOR resources')
|
|
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)
|
|
|
|
# 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 = 1
|
|
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 (definition is implicit)
|
|
spd_list = self._get_spd()
|
|
|
|
if spd_list:
|
|
spd_name = spd_list[0]['spd_name']
|
|
|
|
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):
|
|
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
|
|
|
|
# Create resources and,
|
|
# Check only errors when creating diskless resources
|
|
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 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()
|
|
|
|
if rsc_list_reply:
|
|
# Delete Resources
|
|
for rsc in rsc_list_reply['resources']:
|
|
if rsc['name'] != drbd_rsc_name:
|
|
continue
|
|
|
|
rsc_reply = self._api_rsc_delete(
|
|
node_name=rsc['nodeName'],
|
|
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 Linstor Volume Extend")
|
|
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.
|
|
# Need to check return values
|
|
full_rsc_name = self._drbd_resource_name_from_cinder_volume(volume)
|
|
|
|
# This creates a LINSTOR volume at the original size.
|
|
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))
|
|
|
|
self._copy_vol_to_image(context,
|
|
image_service,
|
|
image_meta,
|
|
rsc_path)
|
|
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 proto is None:
|
|
msg = _('Protobuf 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
|