LINSTOR driver update for LINSTOR v0.9.12 with REST API

LINSTOR driver for Cinder now supports the latest
version of LINSTOR with REST API on the backend.

Change-Id: Icf04b1c515c766edc037ba6f4bfba5b370faebbe
This commit is contained in:
Woojay Poynter 2019-07-03 09:53:52 -07:00
parent 9dd5232ea5
commit 92b43f9c68
4 changed files with 945 additions and 762 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
# Copyright (c) 2014-2018 LINBIT HA Solutions GmbH # Copyright (c) 2014-2019 LINBIT HA Solutions GmbH
# All Rights Reserved. # All Rights Reserved.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -15,7 +15,7 @@
"""This driver connects Cinder to an installed LINSTOR instance. """This driver connects Cinder to an installed LINSTOR instance.
See https://docs.linbit.com/docs/users-guide-9.0/#ch-openstack See https://docs.linbit.com/docs/users-guide-9.0/#ch-openstack-linstor
for more details. for more details.
""" """
@ -34,11 +34,6 @@ from cinder import interface
from cinder.volume import configuration from cinder.volume import configuration
from cinder.volume import driver from cinder.volume import driver
try:
import google.protobuf.json_format as proto
except ImportError:
proto = None
try: try:
import linstor import linstor
lin_drv = linstor.Linstor lin_drv = linstor.Linstor
@ -69,7 +64,14 @@ linstor_opts = [
default=4096, default=4096,
help='Default Block size for Image restoration. ' help='Default Block size for Image restoration. '
'When using iSCSI transport, this option ' 'When using iSCSI transport, this option '
'specifies the block size'), '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', cfg.BoolOpt('linstor_controller_diskless',
default=True, default=True,
@ -84,14 +86,25 @@ CONF.register_opts(linstor_opts, group=configuration.SHARED_CONF_GROUP)
CINDER_UNKNOWN = 'unknown' CINDER_UNKNOWN = 'unknown'
DM_VN_PREFIX = 'CV_' DM_VN_PREFIX = 'CV_'
DM_SN_PREFIX = 'SN_' DM_SN_PREFIX = 'SN_'
LVM = 'Lvm' DISKLESS = 'DISKLESS'
LVMTHIN = 'LvmThin' LVM = 'LVM'
LVM_THIN = 'LVM_THIN'
ZFS = 'ZFS'
ZFS_THIN = 'ZFS_THIN'
class LinstorBaseDriver(driver.VolumeDriver): class LinstorBaseDriver(driver.VolumeDriver):
"""Cinder driver that uses Linstor for storage.""" """Cinder driver that uses LINSTOR for storage.
VERSION = '1.0.0' 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 # ThirdPartySystems wiki page
CI_WIKI_NAME = 'LINBIT_LINSTOR_CI' CI_WIKI_NAME = 'LINBIT_LINSTOR_CI'
@ -113,6 +126,8 @@ class LinstorBaseDriver(driver.VolumeDriver):
'linstor_default_blocksize') 'linstor_default_blocksize')
self.diskless = self.configuration.safe_get( self.diskless = self.configuration.safe_get(
'linstor_controller_diskless') 'linstor_controller_diskless')
self.ap_count = self.configuration.safe_get(
'linstor_autoplace_count')
self.default_backend_name = self.configuration.safe_get( self.default_backend_name = self.configuration.safe_get(
'volume_backend_name') 'volume_backend_name')
self.host_name = socket.gethostname() self.host_name = socket.gethostname()
@ -176,44 +191,36 @@ class LinstorBaseDriver(driver.VolumeDriver):
with lin_drv(self.default_uri) as lin: with lin_drv(self.default_uri) as lin:
if not lin.connected: if not lin.connected:
lin.connect() lin.connect()
api_reply = lin.resource_list()[0].__dict__['_rest_data']
response = proto.MessageToDict(lin.resource_list()[0].proto_msg) return api_reply
return response
def _get_api_resource_dfn_list(self): def _get_api_resource_dfn_list(self):
with lin_drv(self.default_uri) as lin: with lin_drv(self.default_uri) as lin:
if not lin.connected: if not lin.connected:
lin.connect() lin.connect()
api_reply = lin.resource_dfn_list()[0].__dict__['_rest_data']
return api_reply
response = proto.MessageToDict( def _get_api_node_list(self):
lin.resource_dfn_list()[0].proto_msg)
return response
def _get_api_nodes_list(self):
with lin_drv(self.default_uri) as lin: with lin_drv(self.default_uri) as lin:
if not lin.connected: if not lin.connected:
lin.connect() lin.connect()
api_reply = lin.node_list()[0].__dict__['_rest_data']
response = proto.MessageToDict(lin.node_list()[0].proto_msg) return api_reply
return response
def _get_api_storage_pool_dfn_list(self): def _get_api_storage_pool_dfn_list(self):
with lin_drv(self.default_uri) as lin: with lin_drv(self.default_uri) as lin:
if not lin.connected: if not lin.connected:
lin.connect() lin.connect()
api_reply = lin.storage_pool_dfn_list()[0].__dict__['_rest_data']
response = proto.MessageToDict( return api_reply
lin.storage_pool_dfn_list()[0].proto_msg)
return response
def _get_api_storage_pool_list(self): def _get_api_storage_pool_list(self):
with lin_drv(self.default_uri) as lin: with lin_drv(self.default_uri) as lin:
if not lin.connected: if not lin.connected:
lin.connect() lin.connect()
api_reply = lin.storage_pool_list()[0].__dict__['_rest_data']
response = proto.MessageToDict( return api_reply
lin.storage_pool_list()[0].proto_msg)
return response
def _get_api_volume_extend(self, rsc_target_name, new_size): def _get_api_volume_extend(self, rsc_target_name, new_size):
with lin_drv(self.default_uri) as lin: with lin_drv(self.default_uri) as lin:
@ -226,25 +233,15 @@ class LinstorBaseDriver(driver.VolumeDriver):
size=self._vol_size_to_linstor(new_size)) size=self._vol_size_to_linstor(new_size))
return vol_reply return vol_reply
def _api_snapshot_create(self, node_names, rsc_name, snapshot_name): def _api_snapshot_create(self, drbd_rsc_name, snapshot_name):
with lin_drv(self.default_uri) as lin: lin = linstor.Resource(drbd_rsc_name, uri=self.default_uri)
if not lin.connected: snap_reply = lin.snapshot_create(snapshot_name)
lin.connect() return snap_reply
snap_reply = lin.snapshot_create(node_names=node_names, def _api_snapshot_delete(self, drbd_rsc_name, snapshot_name):
rsc_name=rsc_name, lin = linstor.Resource(drbd_rsc_name, uri=self.default_uri)
snapshot_name=snapshot_name, snap_reply = lin.snapshot_delete(snapshot_name)
async_msg=False) return snap_reply
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): def _api_rsc_dfn_delete(self, drbd_rsc_name):
with lin_drv(self.default_uri) as lin: with lin_drv(self.default_uri) as lin:
@ -320,6 +317,18 @@ class LinstorBaseDriver(driver.VolumeDriver):
rsc_reply = lin.resource_create([new_rsc], async_msg=False) rsc_reply = lin.resource_create([new_rsc], async_msg=False)
return rsc_reply 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): def _api_rsc_delete(self, rsc_name, node_name):
with lin_drv(self.default_uri) as lin: with lin_drv(self.default_uri) as lin:
if not lin.connected: if not lin.connected:
@ -329,6 +338,36 @@ class LinstorBaseDriver(driver.VolumeDriver):
rsc_name=rsc_name) rsc_name=rsc_name)
return rsc_reply 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): def _api_volume_dfn_delete(self, rsc_name, volume_nr):
with lin_drv(self.default_uri) as lin: with lin_drv(self.default_uri) as lin:
if not lin.connected: if not lin.connected:
@ -353,29 +392,39 @@ class LinstorBaseDriver(driver.VolumeDriver):
return vol_reply return vol_reply
def _api_snapshot_resource_restore(self, def _api_snapshot_resource_restore(self,
nodes,
src_rsc_name, src_rsc_name,
src_snap_name, src_snap_name,
new_vol_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: with lin_drv(self.default_uri) as lin:
if not lin.connected: if not lin.connected:
lin.connect() lin.connect()
rsc_reply = lin.snapshot_resource_restore( aux_prop = {}
node_names=nodes, aux_prop["Aux/restore"] = "done"
from_resource=src_rsc_name, lin.volume_dfn_modify(
from_snapshot=src_snap_name, rsc_name=new_vol_name,
to_resource=new_vol_name) volume_nr=0,
return rsc_reply set_properties=aux_prop)
if new_rsc.name == new_vol_name:
return True
return False
def _get_rsc_path(self, rsc_name): def _get_rsc_path(self, rsc_name):
rsc_list_reply = self._get_api_resource_list() rsc_list_reply = self._get_api_resource_list()
for rsc in rsc_list_reply['resources']: if rsc_list_reply:
if rsc['name'] == rsc_name and rsc['nodeName'] == self.host_name: for rsc in rsc_list_reply:
for volume in rsc['vlms']: if (rsc["name"] == rsc_name and
if volume['vlmNr'] == 0: rsc["node_name"] == self.host_name):
return volume['devicePath'] for volume in rsc["volumes"]:
if volume["volume_number"] == 0:
return volume["device_path"]
def _get_local_path(self, volume): def _get_local_path(self, volume):
try: try:
@ -391,13 +440,11 @@ class LinstorBaseDriver(driver.VolumeDriver):
def _get_spd(self): def _get_spd(self):
# Storage Pool Definition List # Storage Pool Definition List
spd_list_reply = self._get_api_storage_pool_dfn_list() spd_list_reply = self._get_api_storage_pool_dfn_list()
spd_list = [] spd_list = []
for node in spd_list_reply['storPoolDfns']:
spd_item = {} if spd_list_reply:
spd_item['spd_uuid'] = node['uuid'] for spd in spd_list_reply:
spd_item['spd_name'] = node['storPoolName'] spd_list.append(spd["storage_pool_name"])
spd_list.append(spd_item)
return spd_list return spd_list
@ -405,49 +452,39 @@ class LinstorBaseDriver(driver.VolumeDriver):
# Fetch Storage Pool List # Fetch Storage Pool List
sp_list_reply = self._get_api_storage_pool_list() sp_list_reply = self._get_api_storage_pool_list()
# Fetch Resource Definition List
sp_list = []
# Separate the diskless nodes # Separate the diskless nodes
sp_diskless_list = [] sp_diskless_list = []
sp_list = []
node_count = 0 node_count = 0
if sp_list_reply: if sp_list_reply:
for node in sp_list_reply['storPools']: for node in sp_list_reply:
if node['storPoolName'] == self.default_pool: if node["storage_pool_name"] == self.default_pool:
sp_node = {} sp_node = {}
sp_node['node_uuid'] = node['nodeUuid'] sp_node["node_name"] = node["node_name"]
sp_node['node_name'] = node['nodeName'] sp_node["sp_uuid"] = node["uuid"]
sp_node['sp_uuid'] = node['storPoolUuid'] sp_node["sp_name"] = node["storage_pool_name"]
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']: if node["provider_kind"] == DISKLESS:
diskless = True diskless = True
sp_node['sp_free'] = -1.0 sp_node["sp_free"] = -1.0
sp_node['sp_cap'] = 0.0 sp_node["sp_cap"] = -1.0
sp_node["sp_allocated"] = 0.0
else: else:
diskless = False diskless = False
if 'freeSpace' in node: if "free_capacity" in node:
sp_node['sp_free'] = round( temp = float(node["free_capacity"]) / units.Mi
int(node['freeSpace']['freeCapacity']) / sp_node["sp_free"] = round(temp)
units.Mi, temp = float(node["total_capacity"]) / units.Mi
2) sp_node["sp_cap"] = round(temp)
sp_node['sp_cap'] = round(
int(node['freeSpace']['totalCapacity']) /
units.Mi,
2)
# Driver drivers = [LVM, LVM_THIN, ZFS, ZFS_THIN, DISKLESS]
if node['driver'] == "LvmDriver":
sp_node['driver_name'] = LVM # Driver selection
elif node['driver'] == "LvmThinDriver": if node["provider_kind"] in drivers:
sp_node['driver_name'] = LVMTHIN sp_node['driver_name'] = node["provider_kind"]
else: else:
sp_node['driver_name'] = node['driver'] sp_node['driver_name'] = str(node["provider_kind"])
if diskless: if diskless:
sp_diskless_list.append(sp_node) sp_diskless_list.append(sp_node)
@ -455,9 +492,9 @@ class LinstorBaseDriver(driver.VolumeDriver):
sp_list.append(sp_node) sp_list.append(sp_node)
node_count += 1 node_count += 1
# Add the diskless nodes to the end of the list # Add the diskless nodes to the end of the list
if sp_diskless_list: if sp_diskless_list:
sp_list.extend(sp_diskless_list) sp_list.extend(sp_diskless_list)
return sp_list return sp_list
@ -465,7 +502,7 @@ class LinstorBaseDriver(driver.VolumeDriver):
data = {} data = {}
data["volume_backend_name"] = self.default_backend_name data["volume_backend_name"] = self.default_backend_name
data["vendor_name"] = 'LINBIT' data["vendor_name"] = "LINBIT"
data["driver_version"] = self.VERSION data["driver_version"] = self.VERSION
data["pools"] = [] data["pools"] = []
@ -477,40 +514,51 @@ class LinstorBaseDriver(driver.VolumeDriver):
for rd in rd_list: for rd in rd_list:
num_vols += 1 num_vols += 1
allocated_sizes_gb = [] # allocated_sizes_gb = []
free_capacity_gb = [] free_gb = []
total_capacity_gb = [] total_gb = []
thin_enabled = False thin_enabled = False
# Free capacity for Local Node # Total & Free capacity for Local Node
single_pool = {} single_pool = {}
for sp in sp_data: for sp in sp_data:
if 'Diskless' not in sp['driver_name']: if "Diskless" not in sp["driver_name"]:
if 'LvmThin' in sp['driver_name']: thin_backends = [LVM_THIN, ZFS_THIN]
if sp["driver_name"] in thin_backends:
thin_enabled = True thin_enabled = True
if 'sp_cap' in sp: if "sp_cap" in sp:
if sp['sp_cap'] >= 0.0: if sp["sp_cap"] >= 0.0:
total_capacity_gb.append(sp['sp_cap']) total_gb.append(sp["sp_cap"])
if 'sp_free' in sp: if "sp_free" in sp:
if sp['sp_free'] >= 0.0: if sp["sp_free"] >= 0.0:
free_capacity_gb.append(sp['sp_free']) free_gb.append(sp["sp_free"])
sp_allocated_size_gb = 0
for vlm_uuid in sp['sp_vlms_uuid']: # Allocated capacity
for rd in rd_list: sp_allocated_size_gb = 0.0
if 'vlm_dfn_uuid' in rd: local_resources = []
if rd['vlm_dfn_uuid'] == vlm_uuid:
sp_allocated_size_gb += rd['rd_size'] reply = self._get_api_resource_list()
allocated_sizes_gb.append(sp_allocated_size_gb)
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["pool_name"] = data["volume_backend_name"]
single_pool["free_capacity_gb"] = min(free_capacity_gb) single_pool["free_capacity_gb"] = min(free_gb) if free_gb else 0
single_pool["total_capacity_gb"] = min(total_capacity_gb) single_pool["total_capacity_gb"] = min(total_gb) if total_gb else 0
single_pool['provisioned_capacity_gb'] = max(allocated_sizes_gb) single_pool["provisioned_capacity_gb"] = sp_allocated_size_gb
single_pool["reserved_percentage"] = ( single_pool["reserved_percentage"] = (
self.configuration.reserved_percentage) self.configuration.reserved_percentage)
single_pool['thin_provisioning_support'] = thin_enabled single_pool["thin_provisioning_support"] = thin_enabled
single_pool['thick_provisioning_support'] = not thin_enabled single_pool["thick_provisioning_support"] = not thin_enabled
single_pool['max_over_subscription_ratio'] = ( single_pool["max_over_subscription_ratio"] = (
self.configuration.max_over_subscription_ratio) self.configuration.max_over_subscription_ratio)
single_pool["location_info"] = self.default_uri single_pool["location_info"] = self.default_uri
single_pool["total_volumes"] = num_vols single_pool["total_volumes"] = num_vols
@ -518,7 +566,7 @@ class LinstorBaseDriver(driver.VolumeDriver):
single_pool["goodness_function"] = self.get_goodness_function() single_pool["goodness_function"] = self.get_goodness_function()
single_pool["QoS_support"] = False single_pool["QoS_support"] = False
single_pool["multiattach"] = False single_pool["multiattach"] = False
single_pool["backend_state"] = 'up' single_pool["backend_state"] = "up"
data["pools"].append(single_pool) data["pools"].append(single_pool)
@ -526,29 +574,16 @@ class LinstorBaseDriver(driver.VolumeDriver):
def _get_resource_definitions(self): def _get_resource_definitions(self):
rd_list_reply = self._get_api_resource_dfn_list()
rd_list = [] rd_list = []
rd_list_reply = self._get_api_resource_dfn_list() if rd_list_reply:
for node in rd_list_reply:
# Only if resource definition present
if 'rscDfns' in rd_list_reply:
for node in rd_list_reply['rscDfns']:
# Count only Cinder volumes # Count only Cinder volumes
if DM_VN_PREFIX in node['rscName']: if DM_VN_PREFIX in node['name']:
rd_node = {} rd_node = {}
rd_node['rd_uuid'] = node['rscDfnUuid'] rd_node["rd_uuid"] = node['uuid']
rd_node['rd_name'] = node['rscName'] rd_node["rd_name"] = node['name']
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) rd_list.append(rd_node)
return rd_list return rd_list
@ -558,46 +593,62 @@ class LinstorBaseDriver(driver.VolumeDriver):
However, it excludes diskless nodes. However, it excludes diskless nodes.
""" """
rsc_list_reply = self._get_api_resource_list()
rsc_list_reply = self._get_api_resource_list() # reply in dict
snap_list = [] snap_list = []
for rsc in rsc_list_reply['resources']:
if rsc['name'] != resource:
continue
# Diskless nodes are not available for snapshots if rsc_list_reply:
diskless = False for rsc in rsc_list_reply:
if 'rscFlags' in rsc: if rsc["name"] != resource:
if 'DISKLESS' in rsc['rscFlags']: continue
diskless = True
if not diskless: # Diskless nodes are not available for snapshots
snap_list.append(rsc['nodeName']) 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 return snap_list
def _get_linstor_nodes(self): def _get_diskless_nodes(self, resource):
# Returns all available DRBD nodes # Returns diskless nodes given a resource
node_list_reply = self._get_api_nodes_list() 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 = [] node_list = []
for node in node_list_reply['nodes']:
node_list.append(node['name']) if node_list_reply:
for node in node_list_reply:
node_list.append(node["name"])
return node_list return node_list
def _get_nodes(self): def _get_nodes(self):
# Get Node List # Returns all LINSTOR nodes in a dict list
node_list_reply = self._get_api_nodes_list() node_list_reply = self._get_api_node_list()
node_list = [] node_list = []
if node_list_reply: if node_list_reply:
for node in node_list_reply['nodes']: for node in node_list_reply:
node_item = {} node_item = {}
node_item['node_name'] = node['name'] node_item["node_name"] = node["name"]
node_item['node_uuid'] = node['uuid'] node_item["node_address"] = (
node_item['node_address'] = ( node["net_interfaces"][0]["address"])
node['netInterfaces'][0]['address'])
node_list.append(node_item) node_list.append(node_item)
return node_list return node_list
@ -622,83 +673,57 @@ class LinstorBaseDriver(driver.VolumeDriver):
# #
def create_snapshot(self, snapshot): def create_snapshot(self, snapshot):
snap_name = self._snapshot_name_from_cinder_snapshot(snapshot) snap_name = self._snapshot_name_from_cinder_snapshot(snapshot)
drbd_rsc_name = self._drbd_resource_name_from_cinder_snapshot(snapshot) 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, snap_reply = self._api_snapshot_create(drbd_rsc_name=rsc_name,
rsc_name=drbd_rsc_name,
snapshot_name=snap_name) snapshot_name=snap_name)
if not self._check_api_reply(snap_reply, noerror_only=True): if not snap_reply:
msg = 'ERROR creating a LINSTOR snapshot {}'.format(snap_name) msg = 'ERROR creating a LINSTOR snapshot {}'.format(snap_name)
LOG.error(msg) LOG.error(msg)
raise exception.VolumeBackendAPIException(msg) raise exception.VolumeBackendAPIException(msg)
def delete_snapshot(self, snapshot): def delete_snapshot(self, snapshot):
snap_name = self._snapshot_name_from_cinder_snapshot(snapshot) snapshot_name = self._snapshot_name_from_cinder_snapshot(snapshot)
drbd_rsc_name = self._drbd_resource_name_from_cinder_snapshot(snapshot) rsc_name = self._drbd_resource_name_from_cinder_snapshot(snapshot)
snap_reply = self._api_snapshot_delete(drbd_rsc_name, snap_name) snap_reply = self._api_snapshot_delete(rsc_name, snapshot_name)
if not self._check_api_reply(snap_reply, noerror_only=True): if not snap_reply:
msg = 'ERROR deleting a LINSTOR snapshot {}'.format(snap_name) msg = 'ERROR deleting a LINSTOR snapshot {}'.format(snapshot_name)
LOG.error(msg) LOG.error(msg)
raise exception.VolumeBackendAPIException(msg) raise exception.VolumeBackendAPIException(msg)
# Delete RD if no other RSC are found # Delete RD if no other RSC are found
if not self._get_snapshot_nodes(drbd_rsc_name): if not self._get_snapshot_nodes(rsc_name):
self._api_rsc_dfn_delete(drbd_rsc_name) self._api_rsc_dfn_delete(rsc_name)
def create_volume_from_snapshot(self, volume, snapshot): def create_volume_from_snapshot(self, volume, snapshot):
src_rsc_name = self._drbd_resource_name_from_cinder_snapshot(snapshot) src_rsc_name = self._drbd_resource_name_from_cinder_snapshot(snapshot)
src_snap_name = self._snapshot_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_vol_name = self._drbd_resource_name_from_cinder_volume(volume)
# New RD # If no autoplace, manually build a cluster list
rsc_reply = self._api_rsc_dfn_create(new_vol_name) if self.ap_count == 0:
diskless_nodes = []
nodes = []
for node in self._get_storage_pool():
if not self._check_api_reply(rsc_reply): if DISKLESS in node['driver_name']:
msg = _('Error on creating LINSTOR Resource Definition') diskless_nodes.append(node['node_name'])
LOG.error(msg) continue
raise exception.VolumeBackendAPIException(data=msg)
# New VD from Snap # Filter out controller node if it is diskless
reply = self._api_snapshot_volume_dfn_restore(src_rsc_name, if self.diskless and node['node_name'] == self.host_name:
src_snap_name, continue
new_vol_name) else:
if not self._check_api_reply(reply, noerror_only=True): nodes.append(node['node_name'])
msg = _('Error on restoring LINSTOR Volume Definition')
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
# Set StorPoolName property on VD reply = self._api_snapshot_resource_restore(src_rsc_name,
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, src_snap_name,
new_vol_name) new_vol_name)
if not self._check_api_reply(reply, noerror_only=True): if not reply:
msg = _('Error on restoring LINSTOR resources') msg = _('Error on restoring a LINSTOR volume')
LOG.error(msg) LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg) raise exception.VolumeBackendAPIException(data=msg)
@ -708,6 +733,13 @@ class LinstorBaseDriver(driver.VolumeDriver):
node_name=self.host_name, node_name=self.host_name,
diskless=self.diskless) 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 # Upsize if larger volume than original snapshot
src_rsc_size = int(snapshot['volume_size']) src_rsc_size = int(snapshot['volume_size'])
new_vol_size = int(volume['size']) new_vol_size = int(volume['size'])
@ -722,7 +754,7 @@ class LinstorBaseDriver(driver.VolumeDriver):
if not self._check_api_reply(reply, noerror_only=True): if not self._check_api_reply(reply, noerror_only=True):
# Delete failed volume # Delete failed volume
failed_volume = [] failed_volume = {}
failed_volume['id'] = volume['id'] failed_volume['id'] = volume['id']
self.delete_volume(failed_volume) self.delete_volume(failed_volume)
@ -731,9 +763,9 @@ class LinstorBaseDriver(driver.VolumeDriver):
raise exception.VolumeBackendAPIException(data=msg) raise exception.VolumeBackendAPIException(data=msg)
def create_volume(self, volume): def create_volume(self, volume):
# Check for Storage Pool List # Check for Storage Pool List
sp_data = self._get_storage_pool() sp_data = self._get_storage_pool()
rsc_size = 1
rsc_size = volume['size'] rsc_size = volume['size']
# No existing Storage Pools found # No existing Storage Pools found
@ -747,11 +779,11 @@ class LinstorBaseDriver(driver.VolumeDriver):
LOG.error(msg) LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg) raise exception.VolumeBackendAPIException(data=msg)
# Create Storage Pool (definition is implicit) # Create Storage Pool
spd_list = self._get_spd() spd_list = self._get_spd()
if spd_list: if spd_list:
spd_name = spd_list[0]['spd_name'] spd_name = spd_list[0]
for node in node_list: for node in node_list:
@ -766,12 +798,12 @@ class LinstorBaseDriver(driver.VolumeDriver):
storage_driver=node_driver, storage_driver=node_driver,
driver_pool_name=self.default_vg_name) driver_pool_name=self.default_vg_name)
if not self._check_api_reply(sp_reply): if not self._check_api_reply(sp_reply, noerror_only=True):
msg = _('Could not create a LINSTOR storage pool') msg = _('Could not create a LINSTOR storage pool')
LOG.error(msg) LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg) raise exception.VolumeBackendAPIException(data=msg)
# # Check for RD # Check for RD
# If Retyping from another volume, use parent/origin uuid # If Retyping from another volume, use parent/origin uuid
# as a name source # as a name source
if (volume['migration_status'] is not None and if (volume['migration_status'] is not None and
@ -785,8 +817,8 @@ class LinstorBaseDriver(driver.VolumeDriver):
# Create a New RD # Create a New RD
rsc_dfn_reply = self._api_rsc_dfn_create(rsc_name) rsc_dfn_reply = self._api_rsc_dfn_create(rsc_name)
if not self._check_api_reply(rsc_dfn_reply,
noerror_only=True): if not self._check_api_reply(rsc_dfn_reply, noerror_only=True):
msg = _("Error creating a LINSTOR resource definition") msg = _("Error creating a LINSTOR resource definition")
LOG.error(msg) LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg) raise exception.VolumeBackendAPIException(data=msg)
@ -795,8 +827,8 @@ class LinstorBaseDriver(driver.VolumeDriver):
vd_size = self._vol_size_to_linstor(rsc_size) vd_size = self._vol_size_to_linstor(rsc_size)
vd_reply = self._api_volume_dfn_create(rsc_name=rsc_name, vd_reply = self._api_volume_dfn_create(rsc_name=rsc_name,
size=int(vd_size)) size=int(vd_size))
if not self._check_api_reply(vd_reply,
noerror_only=True): if not self._check_api_reply(vd_reply, noerror_only=True):
msg = _("Error creating a LINSTOR volume definition") msg = _("Error creating a LINSTOR volume definition")
LOG.error(msg) LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg) raise exception.VolumeBackendAPIException(data=msg)
@ -804,26 +836,38 @@ class LinstorBaseDriver(driver.VolumeDriver):
# Create LINSTOR Resources # Create LINSTOR Resources
ctrl_in_sp = False ctrl_in_sp = False
for node in sp_data: for node in sp_data:
# Check if controller is in the pool # Check if controller is in the pool
if node['node_name'] == self.host_name: if node['node_name'] == self.host_name:
ctrl_in_sp = True ctrl_in_sp = True
# Create resources and, # Use autoplace to deploy if set
# Check only errors when creating diskless resources if self.ap_count:
if 'Diskless' in node['driver_name']: try:
diskless = True self._api_rsc_autoplace(rsc_name=rsc_name)
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): except Exception:
msg = _("Error creating a LINSTOR resource") msg = _("Error creating autoplaces LINSTOR resource(s)")
LOG.error(msg) LOG.error(msg)
raise exception.VolumeBackendAPIException(data=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 # If the controller is diskless and not in the pool, create a diskless
# resource on it # resource on it
if not ctrl_in_sp and self.diskless: if not ctrl_in_sp and self.diskless:
@ -832,7 +876,7 @@ class LinstorBaseDriver(driver.VolumeDriver):
diskless=True) diskless=True)
if not self._check_api_reply(rsc_reply, noerror_only=True): if not self._check_api_reply(rsc_reply, noerror_only=True):
msg = _("Error creating a LINSTOR resource") msg = _("Error creating a LINSTOR controller resource")
LOG.error(msg) LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg) raise exception.VolumeBackendAPIException(data=msg)
@ -841,32 +885,56 @@ class LinstorBaseDriver(driver.VolumeDriver):
def delete_volume(self, volume): def delete_volume(self, volume):
drbd_rsc_name = self._drbd_resource_name_from_cinder_volume(volume) drbd_rsc_name = self._drbd_resource_name_from_cinder_volume(volume)
rsc_list_reply = self._get_api_resource_list() 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 rsc_list_reply: # If autoplace was used, use Resource class
# Delete Resources if self.ap_count:
for rsc in rsc_list_reply['resources']:
if rsc['name'] != drbd_rsc_name:
continue
rsc_reply = self._api_rsc_delete( rsc_reply = self._api_rsc_auto_delete(drbd_rsc_name)
node_name=rsc['nodeName'], if not rsc_reply:
rsc_name=drbd_rsc_name) msg = _("Error deleting an autoplaced LINSTOR resource")
if not self._check_api_reply(rsc_reply, noerror_only=True): LOG.error(msg)
msg = _("Error deleting a LINSTOR resource") raise exception.VolumeBackendAPIException(data=msg)
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
# Delete VD # Delete all resources in a cluster manually if not autoplaced
vd_reply = self._api_volume_dfn_delete(drbd_rsc_name, 0) else:
if not vd_reply: if rsc_list_reply:
if not self._check_api_reply(vd_reply): # Remove diskless nodes first
msg = _("Error deleting a LINSTOR volume definition") if diskless_nodes:
LOG.error(msg) for node in diskless_nodes:
raise exception.VolumeBackendAPIException(data=msg) 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)
# Delete RD # Remove diskful nodes
# Will fail if snapshot exists but expected if diskful_nodes:
self._api_rsc_dfn_delete(drbd_rsc_name) 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 return True
@ -877,7 +945,7 @@ class LinstorBaseDriver(driver.VolumeDriver):
extend_reply = self._get_api_volume_extend(rsc_target_name, new_size) extend_reply = self._get_api_volume_extend(rsc_target_name, new_size)
if not self._check_api_reply(extend_reply, noerror_only=True): if not self._check_api_reply(extend_reply, noerror_only=True):
msg = _("ERROR Linstor Volume Extend") msg = _("ERROR extending a LINSTOR volume")
LOG.error(msg) LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg) raise exception.VolumeBackendAPIException(data=msg)
@ -895,11 +963,10 @@ class LinstorBaseDriver(driver.VolumeDriver):
self.delete_snapshot(snapshot) self.delete_snapshot(snapshot)
def copy_image_to_volume(self, context, volume, image_service, image_id): def copy_image_to_volume(self, context, volume, image_service, image_id):
# self.create_volume(volume) already called by Cinder, and works. # 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) full_rsc_name = self._drbd_resource_name_from_cinder_volume(volume)
# This creates a LINSTOR volume at the original size. # This creates a LINSTOR volume from the source image
image_utils.fetch_to_raw(context, image_utils.fetch_to_raw(context,
image_service, image_service,
image_id, image_id,
@ -923,14 +990,10 @@ class LinstorBaseDriver(driver.VolumeDriver):
return (False, None) return (False, None)
def check_for_setup_error(self): def check_for_setup_error(self):
msg = None msg = None
if linstor is None: if linstor is None:
msg = _('Linstor python package not found') msg = _('Linstor python package not found')
if proto is None:
msg = _('Protobuf python package not found')
if msg is not None: if msg is not None:
LOG.error(msg) LOG.error(msg)
raise exception.VolumeDriverException(message=msg) raise exception.VolumeDriverException(message=msg)
@ -954,7 +1017,7 @@ class LinstorBaseDriver(driver.VolumeDriver):
# Class with iSCSI interface methods # Class with iSCSI interface methods
@interface.volumedriver @interface.volumedriver
class LinstorIscsiDriver(LinstorBaseDriver): class LinstorIscsiDriver(LinstorBaseDriver):
"""Cinder iSCSI driver that uses Linstor for storage.""" """Cinder iSCSI driver that uses LINSTOR for storage."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(LinstorIscsiDriver, self).__init__(*args, **kwargs) super(LinstorIscsiDriver, self).__init__(*args, **kwargs)
@ -965,7 +1028,7 @@ class LinstorIscsiDriver(LinstorBaseDriver):
self.helper_driver = self.helper_name self.helper_driver = self.helper_name
self.target_driver = None self.target_driver = None
else: else:
self.helper_name = self.configuration.safe_get('target_helper') self.helper_name = self.configuration.safe_get('iscsi_helper')
self.helper_driver = self.target_mapping[self.helper_name] self.helper_driver = self.target_mapping[self.helper_name]
self.target_driver = importutils.import_object( self.target_driver = importutils.import_object(
self.helper_driver, self.helper_driver,
@ -1024,7 +1087,7 @@ class LinstorIscsiDriver(LinstorBaseDriver):
# Class with DRBD transport mode # Class with DRBD transport mode
@interface.volumedriver @interface.volumedriver
class LinstorDrbdDriver(LinstorBaseDriver): class LinstorDrbdDriver(LinstorBaseDriver):
"""Cinder DRBD driver that uses Linstor for storage.""" """Cinder DRBD driver that uses LINSTOR for storage."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(LinstorDrbdDriver, self).__init__(*args, **kwargs) super(LinstorDrbdDriver, self).__init__(*args, **kwargs)

View File

@ -23,9 +23,6 @@ pywbem>=0.7.0 # LGPLv2.1+
# IBM XIV # IBM XIV
pyxcli>=1.1.5 # Apache-2.0 pyxcli>=1.1.5 # Apache-2.0
# LINSTOR
protobuf>=3.6.1 # BSD
# RBD # RBD
rados # LGPLv2.1 rados # LGPLv2.1
rbd # LGPLv2.1 rbd # LGPLv2.1

View File

@ -0,0 +1,11 @@
---
upgrade:
- |
The LINSTOR driver for Cinder supports LINSTOR 0.9.12. The driver
supports LINSTOR backend using REST API.
The new driver adds 'linstor_autoplace_count' configuration option that
specifies the number of volume replicas.
features:
- |
The LINSTOR Driver for Cinder now supports LINSTOR 0.9.12.