Skip to content

Commit 77c88a0

Browse files
authored
Merge pull request #1175 from rackerlabs/discover_new_svm
feat: Add task to looping calls to discover new SVMs every 5 minutes
2 parents aa7a82a + b792ec8 commit 77c88a0

File tree

2 files changed

+175
-0
lines changed

2 files changed

+175
-0
lines changed

python/cinder-understack/cinder_understack/dynamic_netapp_driver.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from contextlib import contextmanager
55
from functools import cached_property
66

7+
from cinder import context
78
from cinder import exception
89
from cinder import interface
910
from cinder.volume import configuration
@@ -19,6 +20,7 @@
1920
from cinder.volume.drivers.netapp.dataontap.utils import capabilities
2021
from oslo_config import cfg
2122
from oslo_log import log as logging
23+
from oslo_service import loopingcall
2224

2325
LOG = logging.getLogger(__name__)
2426
CONF = cfg.CONF
@@ -33,6 +35,13 @@
3335
"the driver to dynamically select different SVMs based on the "
3436
"volume's project/tenant ID instead of being confined to one SVM.",
3537
),
38+
cfg.IntOpt(
39+
"netapp_svm_discovery_interval",
40+
default=300,
41+
help="In seconds for SVM discovery. The driver will "
42+
"periodically scan the NetApp cluster for new SVMs matching the "
43+
"configured prefix.",
44+
),
3645
]
3746

3847
# Configuration options for dynamic NetApp driver
@@ -144,6 +153,8 @@ def __init__(self, *args, **kwargs):
144153
self._libraries = {}
145154
# aggregated stats
146155
self._stats = self._empty_volume_stats()
156+
# looping call placeholder
157+
self._looping_call = None
147158

148159
def _create_svm_lib(self, svm_name: str) -> NetAppMinimalLibrary:
149160
# we create a configuration object per SVM library to
@@ -222,12 +233,76 @@ def do_setup(self, ctxt):
222233
svm_lib.do_setup(ctxt)
223234
self._libraries[svm_name] = svm_lib
224235

236+
def _remove_svm_lib(self, svm_name: str, svm_lib: NetAppMinimalLibrary):
237+
"""Remove resources for a given SVM library."""
238+
# TODO: Need to free up resources here.
239+
LOG.info("Removing resources for SVM library %s", svm_name)
240+
# Stop any looping calls if they exist
241+
if svm_lib._looping_call and hasattr(svm_lib, "loopingcalls"):
242+
LOG.info("Stopping looping call for SVM library %s", svm_name)
243+
svm_lib.loopingcalls.stop_tasks()
244+
# Adding None coz it has a reference to the looping call
245+
svm_lib.looping_call = None
246+
# There are other attributes which are in svm_lib even after
247+
# Stopping the looping.
248+
249+
def _refresh_svm_libraries(self):
250+
return self._actual_refresh_svm_libraries(context.get_admin_context())
251+
252+
def _actual_refresh_svm_libraries(self, ctxt):
253+
"""Refresh the SVM libraries."""
254+
LOG.info("Start refreshing SVM libraries")
255+
# Print all current library keys
256+
existing_libs = set(self._libraries.keys())
257+
LOG.info("Existing library keys: %s", existing_libs)
258+
# Get the current SVMs from cluster
259+
current_svms = set(self._get_svms())
260+
LOG.info("Current SVMs detected from cluster: %s", current_svms)
261+
# Remove libraries for SVMs that no longer exist
262+
stale_svms = existing_libs - current_svms
263+
for svm_name in stale_svms:
264+
LOG.info("Removing stale NVMe library for SVM: %s", svm_name)
265+
# TODO : stop looping calls, free resources.
266+
svm_lib = self._libraries.get(svm_name)
267+
self._remove_svm_lib(svm_name, svm_lib)
268+
del self._libraries[svm_name]
269+
270+
# Add new SVM libraries
271+
new_svms = current_svms - existing_libs
272+
for svm_name in new_svms:
273+
LOG.info("Creating NVMe library for new SVM: %s", svm_name)
274+
try:
275+
lib = self._create_svm_lib(svm_name)
276+
# Call do_setup to initialize the library
277+
lib.do_setup(ctxt)
278+
lib.check_for_setup_error()
279+
LOG.info("Library creation success for SVM: %s", svm_name)
280+
self._libraries[svm_name] = lib
281+
except Exception:
282+
LOG.exception(
283+
"Failed to create library for SVM %s",
284+
svm_name,
285+
)
286+
LOG.info("Final libraries loaded: %s", list(self._libraries.keys()))
287+
225288
def check_for_setup_error(self):
226289
"""Check for setup errors."""
227290
for svm_name, svm_lib in self._libraries.items():
228291
LOG.info("Checking NVMe library for errors for SVM %s", svm_name)
229292
svm_lib.check_for_setup_error()
230293

294+
# looping call to refresh SVM libraries
295+
if not self._looping_call:
296+
interval = self.configuration.safe_get("netapp_svm_discovery_interval")
297+
if interval and interval > 0:
298+
self._looping_call = loopingcall.FixedIntervalLoopingCall(
299+
self._refresh_svm_libraries
300+
)
301+
# removed initial_delay the first call run after full interval .
302+
self._looping_call.start(interval=interval)
303+
else:
304+
LOG.info("SVM discovery timer disabled (interval=%s)", interval)
305+
231306
def _svmify_pool(self, pool: dict, svm_name: str, **kwargs) -> dict:
232307
"""Applies SVM info to a pool so we can target it and track it."""
233308
# We need to prefix our pool_name, which is 1:1 with the FlexVol

python/cinder-understack/cinder_understack/tests/test_dynamic_netapp_driver.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,103 @@ def test_get_volume_stats_calls_library(self, mock_get_volume_stats):
106106
"""Test that get_volume_stats delegates to library."""
107107
self.driver.get_volume_stats(refresh=True)
108108
mock_get_volume_stats.assert_called_with(True)
109+
110+
# Mocked check_for_setup_errort to avoid below error .
111+
# NetAppDriverException: No pools are available for provisioning volumes.
112+
@mock.patch(
113+
"cinder_understack.dynamic_netapp_driver.loopingcall.FixedIntervalLoopingCall"
114+
)
115+
@mock.patch.object(NetAppNVMeStorageLibrary, "check_for_setup_error")
116+
def test_looping_call_starts_once(self, mock_check_setup, mock_looping_call_class):
117+
"""Test that looping call starts correctly and only once."""
118+
# To avoid config errors mocking the SVM lib setup check
119+
mock_check_setup.return_value = None
120+
121+
# Mock instance for FixedIntervalLoopingCall
122+
mock_looping_instance = mock.Mock()
123+
mock_looping_call_class.return_value = mock_looping_instance
124+
125+
# Clear the looping call so that it can be start
126+
self.driver._looping_call = None
127+
128+
# Trigger setup error check ( to start the loop)
129+
self.driver.check_for_setup_error()
130+
131+
# Verify loop initialized and started
132+
mock_looping_call_class.assert_called_once_with(
133+
self.driver._refresh_svm_libraries
134+
)
135+
# Todo: Use constants for interval and initial_delay
136+
mock_looping_instance.start.assert_called_once_with(interval=300)
137+
138+
# Test second call should not start loop again
139+
mock_looping_instance.reset_mock()
140+
self.driver.check_for_setup_error()
141+
mock_looping_instance.start.assert_not_called()
142+
143+
@mock.patch.object(
144+
dynamic_netapp_driver.NetappCinderDynamicDriver, "_create_svm_lib"
145+
)
146+
@mock.patch.object(dynamic_netapp_driver.NetappCinderDynamicDriver, "_get_svms")
147+
def test_refresh_svm_libraries_adds_and_removes_svms(
148+
self, mock_get_svms, mock_create_svm_lib
149+
):
150+
"""Test _refresh_svm_libraries add new SVMs and removes stale ones."""
151+
# Existing SVMs (before refresh called)
152+
expected_svm = f"os-{self.project_id}"
153+
154+
self.driver._libraries = {
155+
"os-old-svm": mock.Mock(vserver="os-old-svm"),
156+
expected_svm: mock.Mock(vserver=expected_svm),
157+
}
158+
159+
self.driver._get_svms = mock_get_svms
160+
# Returned by _get_svms (after refresh)
161+
mock_get_svms.return_value = [expected_svm, "os-new-svm"]
162+
163+
# make the created lib look like the real thing
164+
mock_lib_instance = mock.create_autospec(
165+
dynamic_netapp_driver.NetAppMinimalLibrary, instance=True
166+
)
167+
168+
# Mock the new lib instance created
169+
mock_lib_instance.vserver = "os-new-svm"
170+
mock_create_svm_lib.return_value = mock_lib_instance
171+
172+
# Trigger refresh
173+
self.driver._context = self.ctxt
174+
175+
self.driver._actual_refresh_svm_libraries(self.ctxt)
176+
177+
# Check stale SVM was removed
178+
self.assertNotIn("os-old-svm", self.driver._libraries)
179+
180+
# Check SVM was retained
181+
self.assertIn(expected_svm, self.driver._libraries)
182+
183+
# Check new SVM was added
184+
self.assertIn("os-new-svm", self.driver._libraries)
185+
186+
# New SVM lib should've been created and setup
187+
mock_create_svm_lib.assert_called_once_with("os-new-svm")
188+
mock_lib_instance.do_setup.assert_called_once_with(self.ctxt)
189+
mock_lib_instance.check_for_setup_error.assert_called_once()
190+
191+
@mock.patch.object(
192+
dynamic_netapp_driver.NetappCinderDynamicDriver, "_create_svm_lib"
193+
)
194+
@mock.patch.object(dynamic_netapp_driver.NetappCinderDynamicDriver, "_get_svms")
195+
def test_refresh_svm_libraries_handles_lib_creation_failure(
196+
self, mock_get_svms, mock_create_svm_lib
197+
):
198+
"""Ensure that failure in lib creation is caught and logged, not raised."""
199+
mock_get_svms.return_value = ["os-new-failing-svm"]
200+
mock_create_svm_lib.side_effect = Exception("Simulated failure")
201+
202+
self.driver._libraries = {}
203+
204+
# Should not raise exception
205+
self.driver._actual_refresh_svm_libraries(mock.Mock())
206+
207+
# The failing SVM should not be added to self._libraries
208+
self.assertNotIn("os-new-failing-svm", self.driver._libraries)

0 commit comments

Comments
 (0)