1
1
"""NetApp NVMe driver with dynamic multi-SVM support."""
2
2
3
+ from collections .abc import Generator
3
4
from contextlib import contextmanager
5
+ from functools import cached_property
4
6
5
7
from cinder import context
6
8
from cinder import exception
7
9
from cinder import interface
8
10
from cinder .objects import volume_type as vol_type_obj
11
+ from cinder .volume import configuration
9
12
from cinder .volume import driver as volume_driver
10
13
from cinder .volume import volume_utils
11
14
from cinder .volume .drivers .netapp import options as na_opts
22
25
LOG = logging .getLogger (__name__ )
23
26
CONF = cfg .CONF
24
27
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
+
25
40
# Configuration options for dynamic NetApp driver
26
41
# Using cinder.volume.configuration approach for better abstraction
27
42
NETAPP_DYNAMIC_OPTS = [
32
47
na_opts .netapp_support_opts ,
33
48
na_opts .netapp_san_opts ,
34
49
na_opts .netapp_cluster_opts ,
50
+ netapp_dynamic_opts ,
35
51
]
36
52
37
53
@@ -732,8 +748,7 @@ def create_volume(self, volume):
732
748
if not password :
733
749
missing_conf .append ("netapp_password" )
734
750
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 } "
737
752
)
738
753
LOG .error (msg )
739
754
raise exception .VolumeBackendAPIException (data = msg )
@@ -823,7 +838,7 @@ def create_volume(self, volume):
823
838
# Update metadata and add to namespace table
824
839
metadata ["Volume" ] = flexvol_name
825
840
metadata ["Qtree" ] = None
826
- handle = f' { self .vserver } :{ metadata [" Path" ] } '
841
+ handle = f" { self .vserver } :{ metadata [' Path' ] } "
827
842
828
843
# Add to namespace table for tracking
829
844
# The original library maintains a namespace_table to
@@ -941,20 +956,98 @@ class NetappCinderDynamicDriver(volume_driver.BaseVD):
941
956
def __init__ (self , * args , ** kwargs ):
942
957
"""Initialize the driver and create library instance."""
943
958
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
+ )
945
997
946
998
@staticmethod
947
999
def get_driver_options ():
948
1000
"""All options this driver supports."""
949
1001
return [item for sublist in NETAPP_DYNAMIC_OPTS for item in sublist ]
950
1002
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
954
1045
955
1046
def check_for_setup_error (self ):
956
1047
"""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 ()
958
1051
959
1052
def _svmify_pool (self , pool : dict , svm_name : str , ** kwargs ) -> dict :
960
1053
"""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:
968
1061
pool_name = pool ["pool_name" ]
969
1062
pool ["pool_name" ] = f"{ svm_name } { _SVM_NAME_DELIM } { pool_name } "
970
1063
pool ["netapp_vserver" ] = svm_name
1064
+ prefix = self .configuration .safe_get ("netapp_vserver_prefix" )
1065
+ pool ["netapp_project_id" ] = svm_name .replace (prefix , "" )
971
1066
pool .update (kwargs )
972
1067
return pool
973
1068
974
1069
@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
984
1096
985
1097
def create_volume (self , volume ):
986
1098
"""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 )
988
1101
989
1102
def delete_volume (self , volume ):
990
1103
"""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 )
992
1106
993
1107
def create_snapshot (self , snapshot ):
994
1108
"""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 )
996
1111
997
1112
def delete_snapshot (self , snapshot ):
998
1113
"""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 )
1000
1116
1001
1117
def create_volume_from_snapshot (self , volume , snapshot ):
1002
1118
"""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 )
1004
1121
1005
1122
def create_cloned_volume (self , volume , src_vref ):
1006
1123
"""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 )
1008
1126
1009
1127
def extend_volume (self , volume , new_size ):
1010
1128
"""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 )
1012
1131
1013
1132
def initialize_connection (self , volume , connector ):
1014
1133
"""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 )
1016
1136
1017
1137
def terminate_connection (self , volume , connector , ** kwargs ):
1018
1138
"""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
1020
1169
1021
1170
def get_volume_stats (self , refresh = False ):
1022
1171
"""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
1024
1188
1025
1189
def create_export (self , context , volume , connector ):
1026
1190
"""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 )
1028
1193
1029
1194
def ensure_export (self , context , volume ):
1030
1195
"""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 )
1032
1198
1033
1199
def remove_export (self , context , volume ):
1034
1200
"""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