Skip to content

Commit 0d43955

Browse files
cardoenidzrai
andcommitted
feat(cinder-understack): add support for multi-SVM via multi libraries
The upstream driver has a 1:1 relationship with a NetApp NVMe library to the NVMe driver. This instead creates an instance of the minimal NVMe storage library wrapper with its own configuration so we can continue to use the upstream code on a per SVM basis where its library is 1:1 with a SVM while still managing them all via one driver. The driver then managers the delegation to the appropriate SVM based on the SVM name that is set on the volume type. SVMs are expected to exist with the leading prefix of os- on the NetApp and will be auto-detected and configured within the driver. Further work needs to occur to periodically add newly discovered SVMs and to clean up after SVMs that have been removed. Co-Authored-by: Nidhi Rai <[email protected]>
1 parent bbb4e4d commit 0d43955

File tree

2 files changed

+231
-48
lines changed

2 files changed

+231
-48
lines changed

python/cinder-understack/cinder_understack/dynamic_netapp_driver.py

Lines changed: 197 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
"""NetApp NVMe driver with dynamic multi-SVM support."""
22

3+
from collections.abc import Generator
34
from contextlib import contextmanager
5+
from functools import cached_property
46

57
from cinder import context
68
from cinder import exception
79
from cinder import interface
810
from cinder.objects import volume_type as vol_type_obj
11+
from cinder.volume import configuration
912
from cinder.volume import driver as volume_driver
1013
from cinder.volume import volume_utils
1114
from cinder.volume.drivers.netapp import options as na_opts
@@ -22,6 +25,18 @@
2225
LOG = logging.getLogger(__name__)
2326
CONF = cfg.CONF
2427

28+
# Dynamic SVM options - our custom configuration group
29+
netapp_dynamic_opts = [
30+
cfg.StrOpt(
31+
"netapp_vserver_prefix",
32+
default="os-",
33+
help="Prefix to use when constructing SVM/vserver names from tenant IDs. "
34+
"The SVM name will be formed as <prefix><tenant_id>. This allows "
35+
"the driver to dynamically select different SVMs based on the "
36+
"volume's project/tenant ID instead of being confined to one SVM.",
37+
),
38+
]
39+
2540
# Configuration options for dynamic NetApp driver
2641
# Using cinder.volume.configuration approach for better abstraction
2742
NETAPP_DYNAMIC_OPTS = [
@@ -32,6 +47,7 @@
3247
na_opts.netapp_support_opts,
3348
na_opts.netapp_san_opts,
3449
na_opts.netapp_cluster_opts,
50+
netapp_dynamic_opts,
3551
]
3652

3753

@@ -732,8 +748,7 @@ def create_volume(self, volume):
732748
if not password:
733749
missing_conf.append("netapp_password")
734750
msg = (
735-
f"Missing required NetApp configuration in cinder.conf: "
736-
f"{missing_conf}"
751+
f"Missing required NetApp configuration in cinder.conf: {missing_conf}"
737752
)
738753
LOG.error(msg)
739754
raise exception.VolumeBackendAPIException(data=msg)
@@ -823,7 +838,7 @@ def create_volume(self, volume):
823838
# Update metadata and add to namespace table
824839
metadata["Volume"] = flexvol_name
825840
metadata["Qtree"] = None
826-
handle = f'{self.vserver}:{metadata["Path"]}'
841+
handle = f"{self.vserver}:{metadata['Path']}"
827842

828843
# Add to namespace table for tracking
829844
# The original library maintains a namespace_table to
@@ -941,20 +956,98 @@ class NetappCinderDynamicDriver(volume_driver.BaseVD):
941956
def __init__(self, *args, **kwargs):
942957
"""Initialize the driver and create library instance."""
943958
super().__init__(*args, **kwargs)
944-
self.library = NetappDynamicLibrary(self.DRIVER_NAME, "NVMe", **kwargs)
959+
self.configuration.append_config_values(self.__class__.get_driver_options())
960+
# save the arguments supplied
961+
self._init_kwargs = kwargs
962+
# but we don't need the configuration
963+
del self._init_kwargs["configuration"]
964+
# child libraries
965+
self._libraries = {}
966+
# aggregated stats
967+
self._stats = self._empty_volume_stats()
968+
969+
def _create_svm_lib(self, svm_name: str) -> NetAppMinimalLibrary:
970+
# we create a configuration object per SVM library to
971+
# provide the SVM name to the SVM library
972+
child_grp = f"{self.configuration.config_group}_{svm_name}"
973+
child_cfg = configuration.Configuration(
974+
volume_driver.volume_opts,
975+
config_group=child_grp,
976+
)
977+
# register the options
978+
child_cfg.append_config_values(self.__class__.get_driver_options())
979+
# we need to copy the configs so get the base group
980+
for opt in self.__class__.get_driver_options():
981+
try:
982+
val = getattr(self.configuration, opt.name)
983+
CONF.set_override(opt.name, val, group=child_grp)
984+
except cfg.NoSuchOptError:
985+
# this exception occurs if the option isn't set at all
986+
# which means we don't need to set an override
987+
pass
988+
989+
# now set the SVM name
990+
CONF.set_override("netapp_vserver", svm_name, group=child_grp)
991+
# now set the backend configuration name
992+
CONF.set_override("volume_backend_name", child_grp, group=child_grp)
993+
# return an instance of the library scoped to one SVM
994+
return NetAppMinimalLibrary(
995+
self.DRIVER_NAME, "NVMe", configuration=child_cfg, **self._init_kwargs
996+
)
945997

946998
@staticmethod
947999
def get_driver_options():
9481000
"""All options this driver supports."""
9491001
return [item for sublist in NETAPP_DYNAMIC_OPTS for item in sublist]
9501002

951-
def do_setup(self, context):
952-
"""Setup the driver."""
953-
self.library.do_setup(context)
1003+
@cached_property
1004+
def cluster(self) -> RestNaServer:
1005+
return RestNaServer(
1006+
transport_type=self.configuration.netapp_transport_type,
1007+
ssl_cert_path=self.configuration.netapp_ssl_cert_path,
1008+
username=self.configuration.netapp_login,
1009+
password=self.configuration.netapp_password,
1010+
hostname=self.configuration.netapp_server_hostname,
1011+
port=self.configuration.netapp_server_port,
1012+
vserver=None,
1013+
trace=volume_utils.TRACE_API,
1014+
api_trace_pattern=self.configuration.netapp_api_trace_pattern,
1015+
async_rest_timeout=self.configuration.netapp_async_rest_timeout,
1016+
)
1017+
1018+
def _get_svms(self):
1019+
prefix = self.configuration.safe_get("netapp_vserver_prefix")
1020+
svm_filter = {
1021+
"state": "running",
1022+
"nvme.enabled": "true",
1023+
"name": f"{prefix}*",
1024+
"fields": "name,uuid",
1025+
}
1026+
ret = self.cluster.get_records(
1027+
"svm/svms", query=svm_filter, enable_tunneling=False
1028+
)
1029+
return [rec["name"] for rec in ret["records"]]
1030+
1031+
def do_setup(self, ctxt):
1032+
"""Setup the driver.
1033+
1034+
Connected to the NetApp with cluster credentials to find the SVMs.
1035+
"""
1036+
for svm_name in self._get_svms():
1037+
if svm_name in self._libraries:
1038+
LOG.info("SVMe library already exists for SVM %s, skipping", svm_name)
1039+
continue
1040+
1041+
LOG.info("Creating NVMe library instance for SVM %s", svm_name)
1042+
svm_lib = self._create_svm_lib(svm_name)
1043+
svm_lib.do_setup(ctxt)
1044+
self._libraries[svm_name] = svm_lib
9541045

9551046
def check_for_setup_error(self):
9561047
"""Check for setup errors."""
957-
self.library.check_for_setup_error()
1048+
for svm_name, svm_lib in self._libraries.items():
1049+
LOG.info("Checking NVMe library for errors for SVM %s", svm_name)
1050+
svm_lib.check_for_setup_error()
9581051

9591052
def _svmify_pool(self, pool: dict, svm_name: str, **kwargs) -> dict:
9601053
"""Applies SVM info to a pool so we can target it and track it."""
@@ -968,68 +1061,142 @@ def _svmify_pool(self, pool: dict, svm_name: str, **kwargs) -> dict:
9681061
pool_name = pool["pool_name"]
9691062
pool["pool_name"] = f"{svm_name}{_SVM_NAME_DELIM}{pool_name}"
9701063
pool["netapp_vserver"] = svm_name
1064+
prefix = self.configuration.safe_get("netapp_vserver_prefix")
1065+
pool["netapp_project_id"] = svm_name.replace(prefix, "")
9711066
pool.update(kwargs)
9721067
return pool
9731068

9741069
@contextmanager
975-
def _fix_volume_pool_name(self, svm_name: str, volume):
976-
"""Strips out the SVM name from the volume host and restores."""
977-
original_host = volume.get("host")
978-
if not original_host:
979-
yield original_host
980-
else:
981-
volume["host"] = original_host.replace(f"{svm_name}{_SVM_NAME_DELIM}", "")
982-
yield volume
983-
volume["host"] = original_host
1070+
def _volume_to_library(self, volume) -> Generator[NetAppMinimalLibrary]:
1071+
"""From a volume find the specific NVMe library to use."""
1072+
# save this to restore it in the end
1073+
original_host = volume["host"]
1074+
# svm plus pool_name
1075+
svm_pool_name = volume_utils.extract_host(original_host, level="pool")
1076+
svm_name = svm_pool_name.split(_SVM_NAME_DELIM)[0]
1077+
try:
1078+
lib = self._libraries[svm_name]
1079+
except KeyError:
1080+
LOG.error("No such SVM %s instantiated", svm_name)
1081+
raise exception.DriverNotInitialized() from None
1082+
1083+
if lib.vserver != svm_name:
1084+
LOG.error(
1085+
"NVMe library vserver %s mismatch with volume.host SVM %s",
1086+
lib.vserver,
1087+
svm_name,
1088+
)
1089+
raise exception.InvalidInput(
1090+
reason="NVMe library vserver mismatch with volume.host"
1091+
)
1092+
1093+
volume["host"] = original_host.replace(f"{svm_name}{_SVM_NAME_DELIM}", "")
1094+
yield lib
1095+
volume["host"] = original_host
9841096

9851097
def create_volume(self, volume):
9861098
"""Create a volume."""
987-
return self.library.create_volume(volume)
1099+
with self._volume_to_library(volume) as lib:
1100+
return lib.create_volume(volume)
9881101

9891102
def delete_volume(self, volume):
9901103
"""Delete a volume."""
991-
return self.library.delete_volume(volume)
1104+
with self._volume_to_library(volume) as lib:
1105+
return lib.delete_volume(volume)
9921106

9931107
def create_snapshot(self, snapshot):
9941108
"""Create a snapshot."""
995-
return self.library.create_snapshot(snapshot)
1109+
with self._volume_to_library(snapshot.volume) as lib:
1110+
return lib.create_snapshot(snapshot)
9961111

9971112
def delete_snapshot(self, snapshot):
9981113
"""Delete a snapshot."""
999-
return self.library.delete_snapshot(snapshot)
1114+
with self._volume_to_library(snapshot.volume) as lib:
1115+
return lib.delete_snapshot(snapshot)
10001116

10011117
def create_volume_from_snapshot(self, volume, snapshot):
10021118
"""Create a volume from a snapshot."""
1003-
return self.library.create_volume_from_snapshot(volume, snapshot)
1119+
with self._volume_to_library(volume) as lib:
1120+
return lib.create_volume_from_snapshot(volume, snapshot)
10041121

10051122
def create_cloned_volume(self, volume, src_vref):
10061123
"""Create a cloned volume."""
1007-
return self.library.create_cloned_volume(volume, src_vref)
1124+
with self._volume_to_library(volume) as lib:
1125+
return lib.create_cloned_volume(volume, src_vref)
10081126

10091127
def extend_volume(self, volume, new_size):
10101128
"""Extend a volume."""
1011-
return self.library.extend_volume(volume, new_size)
1129+
with self._volume_to_library(volume) as lib:
1130+
return lib.extend_volume(volume, new_size)
10121131

10131132
def initialize_connection(self, volume, connector):
10141133
"""Initialize connection to volume."""
1015-
return self.library.initialize_connection(volume, connector)
1134+
with self._volume_to_library(volume) as lib:
1135+
return lib.initialize_connection(volume, connector)
10161136

10171137
def terminate_connection(self, volume, connector, **kwargs):
10181138
"""Terminate connection to volume."""
1019-
return self.library.terminate_connection(volume, connector, **kwargs)
1139+
with self._volume_to_library(volume) as lib:
1140+
return lib.terminate_connection(volume, connector, **kwargs)
1141+
1142+
def get_filter_function(self):
1143+
"""Prefixes any filter function with our SVM matching."""
1144+
base_filter = super().get_filter_function()
1145+
svm_filter = "(capabilities.netapp_project_id == volume.project_id)"
1146+
if base_filter:
1147+
return f"{svm_filter} and {base_filter}"
1148+
else:
1149+
return svm_filter
1150+
1151+
def _empty_volume_stats(self):
1152+
data = {}
1153+
data["volume_backend_name"] = (
1154+
self.configuration.safe_get("volume_backend_name") or self.DRIVER_NAME
1155+
)
1156+
data["vendor_name"] = "NetApp"
1157+
data["driver_version"] = self.VERSION
1158+
data["storage_protocol"] = "NVMe"
1159+
data["sparse_copy_volume"] = True
1160+
data["replication_enabled"] = False
1161+
# each SVM is going to have different limits
1162+
data["total_capacity_gb"] = "unknown"
1163+
data["free_capacity_gb"] = "unknown"
1164+
# ensure we filter our pools by SVM
1165+
data["filter_function"] = self.get_filter_function()
1166+
data["goodness_function"] = self.get_goodness_function()
1167+
data["pools"] = []
1168+
return data
10201169

10211170
def get_volume_stats(self, refresh=False):
10221171
"""Get volume stats."""
1023-
return self.library.get_volume_stats(refresh)
1172+
if refresh:
1173+
data = self._empty_volume_stats()
1174+
for svm_name, svm_lib in self._libraries.items():
1175+
LOG.info("Get Volume Stats for SVM %s", svm_name)
1176+
ret = svm_lib.get_volume_stats(refresh)
1177+
LOG.info("Adding SVM data to pools for SVM %s", svm_name)
1178+
data["pools"].extend(
1179+
[
1180+
self._svmify_pool(
1181+
pool, svm_name, filter_function=data["filter_function"]
1182+
)
1183+
for pool in ret["pools"]
1184+
]
1185+
)
1186+
self._stats = data
1187+
return self._stats
10241188

10251189
def create_export(self, context, volume, connector):
10261190
"""Create export for volume."""
1027-
return self.library.create_export(context, volume, connector)
1191+
with self._volume_to_library(volume) as lib:
1192+
return lib.create_export(context, volume, connector)
10281193

10291194
def ensure_export(self, context, volume):
10301195
"""Ensure export for volume."""
1031-
return self.library.ensure_export(context, volume)
1196+
with self._volume_to_library(volume) as lib:
1197+
return lib.ensure_export(context, volume)
10321198

10331199
def remove_export(self, context, volume):
10341200
"""Remove export for volume."""
1035-
return self.library.remove_export(context, volume)
1201+
with self._volume_to_library(volume) as lib:
1202+
return lib.remove_export(context, volume)

0 commit comments

Comments
 (0)