blob: 7220b3f31496648e0ce3e641088f3d3064d67c47 [file] [log] [blame]
Biser Milanov8cd1aa92023-06-06 16:58:06 +03001From 1b717bfdcf398792b30980ac94c3770f4acbbe51 Mon Sep 17 00:00:00 2001
Biser Milanov109150a2023-05-26 12:05:31 +03002From: Biser Milanov <biser.milanov@storpool.com>
3Date: Tue, 23 May 2023 17:55:09 +0300
4Subject: [PATCH] storpool.py: Use StorPool's API for Attach/Detach
5
6Drop the use of the node-scoped file that helped manage volumes on that
7node.
8
9Volumes are now attached and detached by calling the StorPool API
10directly.
11
12Additionally refactor tests to reflect this.
13
14Change-Id: I0fabcde1dc249b9b09f99ee0b7d759432972314a
15---
Biser Milanov8cd1aa92023-06-06 16:58:06 +030016 os_brick/initiator/connectors/storpool.py | 84 ++++++-
17 .../initiator/connectors/test_storpool.py | 229 +++++++++++++-----
18 2 files changed, 246 insertions(+), 67 deletions(-)
Biser Milanov109150a2023-05-26 12:05:31 +030019
20diff --git a/os_brick/initiator/connectors/storpool.py b/os_brick/initiator/connectors/storpool.py
Biser Milanov0b188622023-06-02 17:20:09 +030021index e1bd55c..9752ad9 100644
Biser Milanov109150a2023-05-26 12:05:31 +030022--- a/os_brick/initiator/connectors/storpool.py
23+++ b/os_brick/initiator/connectors/storpool.py
Biser Milanov65a1a6f2023-06-02 11:52:03 +030024@@ -26,6 +26,7 @@ from os_brick import utils
25 LOG = logging.getLogger(__name__)
26
27 spopenstack = importutils.try_import('storpool.spopenstack')
28+spapi = importutils.try_import('storpool.spapi')
29
30
31 class StorPoolConnector(base.BaseLinuxConnector):
32@@ -37,6 +38,10 @@ class StorPoolConnector(base.BaseLinuxConnector):
33 super(StorPoolConnector, self).__init__(root_helper, driver=driver,
34 *args, **kwargs)
35
36+ if spapi is None:
37+ raise exception.BrickException(
38+ 'Could not import the StorPool API bindings')
39+
40 if spopenstack is not None:
41 try:
42 self._attach = spopenstack.AttachDB(log=LOG)
Biser Milanov0b188622023-06-02 17:20:09 +030043@@ -46,6 +51,38 @@ class StorPoolConnector(base.BaseLinuxConnector):
Biser Milanov65a1a6f2023-06-02 11:52:03 +030044 else:
45 self._attach = None
46
47+ def _detach_retry(self, sp_ourid, volume):
Biser Milanov0b188622023-06-02 17:20:09 +030048+ """Retry detaching.
49+
50+ Retries attempt to handle LUKS tests-related failures:
Biser Milanov65a1a6f2023-06-02 11:52:03 +030051+ busy: volume ... open at ...
52+ """
Biser Milanov0b188622023-06-02 17:20:09 +030053+
Biser Milanov65a1a6f2023-06-02 11:52:03 +030054+ count = 10
55+ while True:
56+ try:
57+ force = count == 0
58+ self._attach.api().volumesReassignWait(
59+ {
60+ "reassign": [{
61+ "volume": volume,
62+ "detach": [sp_ourid],
63+ "force": force,
64+ }]
65+ }
66+ )
67+ break
68+ except spapi.ApiError as exc:
69+ if (
70+ exc.name in ("busy", "invalidParam")
71+ and "is open at" in exc.desc
72+ ):
73+ assert count > 0
74+ time.sleep(0.2)
75+ count -= 1
76+ else:
77+ raise
78+
79 @staticmethod
80 def get_connector_properties(root_helper, *args, **kwargs):
81 """The StorPool connector properties."""
Biser Milanov0b188622023-06-02 17:20:09 +030082@@ -76,16 +113,29 @@ class StorPoolConnector(base.BaseLinuxConnector):
Biser Milanov109150a2023-05-26 12:05:31 +030083 if mode is None or mode not in ('rw', 'ro'):
84 raise exception.BrickException(
85 'Invalid access_mode specified in the connection data.')
86- req_id = 'brick-%s-%s' % (client_id, volume_id)
87- self._attach.add(req_id, {
88- 'volume': volume,
89- 'type': 'brick',
90- 'id': req_id,
91- 'rights': 1 if mode == 'ro' else 2,
92- 'volsnap': False
93- })
94- self._attach.sync(req_id, None)
95- return {'type': 'block', 'path': '/dev/storpool/' + volume}
96+ try:
97+ sp_ourid = self._attach.config()["SP_OURID"]
98+ except KeyError:
99+ raise exception.BrickException(
100+ 'SP_OURID missing, cannot connect volume %s' % volume_id)
101+
102+ try:
103+ self._attach.api().volumesReassignWait(
104+ {"reassign": [{"volume": volume, mode: [sp_ourid]}]})
105+ except Exception as exc:
106+ raise exception.BrickException(
107+ 'Communication with the StorPool API '
108+ 'failed: %s' % (exc)) from exc
109+
110+ try:
111+ volume_info = self._attach.api().volumeInfo(volume)
112+ except Exception as exc:
113+ raise exception.BrickException(
114+ 'Communication with the StorPool API '
115+ 'failed: %s' % (exc)) from exc
116+
Biser Milanov7795e092023-05-30 13:24:09 +0300117+ sp_global_id = volume_info.globalId
Biser Milanov109150a2023-05-26 12:05:31 +0300118+ return {'type': 'block', 'path': '/dev/storpool-byid/' + sp_global_id}
119
120 @utils.connect_volume_undo_prepare_result(unlink_after=True)
121 def disconnect_volume(self, connection_properties, device_info,
Biser Milanov0b188622023-06-02 17:20:09 +0300122@@ -124,9 +174,17 @@ class StorPoolConnector(base.BaseLinuxConnector):
Biser Milanov109150a2023-05-26 12:05:31 +0300123 raise exception.BrickException(
124 'Invalid StorPool connection data, no volume ID specified.')
125 volume = self._attach.volumeName(volume_id)
126- req_id = 'brick-%s-%s' % (client_id, volume_id)
127- self._attach.sync(req_id, volume)
128- self._attach.remove(req_id)
129+ try:
130+ sp_ourid = self._attach.config()["SP_OURID"]
131+ except KeyError:
132+ raise exception.BrickException(
133+ 'SP_OURID missing, cannot disconnect volume %s' % volume)
134+ try:
Biser Milanov65a1a6f2023-06-02 11:52:03 +0300135+ self._detach_retry(sp_ourid, volume)
Biser Milanov109150a2023-05-26 12:05:31 +0300136+ except Exception as exc:
137+ raise exception.BrickException(
138+ 'Communication with the StorPool API '
139+ 'failed: %s' % (exc)) from exc
140
141 def get_search_path(self):
142 return '/dev/storpool'
143diff --git a/os_brick/tests/initiator/connectors/test_storpool.py b/os_brick/tests/initiator/connectors/test_storpool.py
Biser Milanov8cd1aa92023-06-06 16:58:06 +0300144index 614deba..86cf216 100644
Biser Milanov109150a2023-05-26 12:05:31 +0300145--- a/os_brick/tests/initiator/connectors/test_storpool.py
146+++ b/os_brick/tests/initiator/connectors/test_storpool.py
Biser Milanov8cd1aa92023-06-06 16:58:06 +0300147@@ -13,6 +13,7 @@
148 # License for the specific language governing permissions and limitations
149 # under the License.
150
151+import copy
152 from unittest import mock
153
154
155@@ -25,46 +26,35 @@ def volumeNameExt(vid):
Biser Milanov0b188622023-06-02 17:20:09 +0300156 return 'os--volume--{id}'.format(id=vid)
Biser Milanov109150a2023-05-26 12:05:31 +0300157
Biser Milanov0b188622023-06-02 17:20:09 +0300158
159+def faulty_api(req):
160+ faulty_api.real_fn(req)
161+ if faulty_api.fail_count > 0:
162+ faulty_api.fail_count -= 1
163+ raise MockApiError("busy",
164+ "'os--volume--sp-vol-1' is open at client 19")
165+
166+
167+class MockApiError(Exception):
168+ def __init__(self, name, desc):
169+ super(MockApiError, self).__init__()
170+ self.name = name
171+ self.desc = desc
172+
173+
174+class MockVolumeInfo(object):
175+ def __init__(self, global_id):
176+ self.globalId = global_id
177+
178+
Biser Milanov109150a2023-05-26 12:05:31 +0300179 class MockStorPoolADB(object):
180 def __init__(self, log):
181- self.requests = {}
182- self.attached = {}
183+ pass
184
185 def api(self):
186 pass
187
188- def add(self, req_id, req):
189- if req_id in self.requests:
190- raise Exception('Duplicate MockStorPool request added')
191- self.requests[req_id] = req
192-
193- def remove(self, req_id):
194- req = self.requests.get(req_id, None)
195- if req is None:
196- raise Exception('Unknown MockStorPool request removed')
197- elif req['volume'] in self.attached:
198- raise Exception('Removing attached MockStorPool volume')
199- del self.requests[req_id]
200-
201- def sync(self, req_id, detached):
202- req = self.requests.get(req_id, None)
203- if req is None:
204- raise Exception('Unknown MockStorPool request synced')
205- volume = req.get('volume', None)
206- if volume is None:
207- raise Exception('MockStorPool request without volume')
208-
209- if detached is None:
210- if volume in self.attached:
211- raise Exception('Duplicate MockStorPool request synced')
212- self.attached[volume] = req
213- else:
214- if volume != detached:
215- raise Exception(
216- 'Mismatched volumes on a MockStorPool request removal')
217- elif detached not in self.attached:
218- raise Exception('MockStorPool request not attached yet')
219- del self.attached[detached]
220+ def config(self):
221+ return {"SP_OURID": 1}
222
223 def volumeName(self, vid):
224 return volumeNameExt(vid)
Biser Milanov8cd1aa92023-06-06 16:58:06 +0300225@@ -74,6 +64,10 @@ spopenstack = mock.Mock()
Biser Milanov0b188622023-06-02 17:20:09 +0300226 spopenstack.AttachDB = MockStorPoolADB
227 connector.spopenstack = spopenstack
228
229+spapi = mock.Mock()
230+spapi.ApiError = MockApiError
231+connector.spapi = spapi
232+
233
234 class StorPoolConnectorTestCase(test_connector.ConnectorTestCase):
235 def volumeName(self, vid):
Biser Milanov8cd1aa92023-06-06 16:58:06 +0300236@@ -101,38 +95,66 @@ class StorPoolConnectorTestCase(test_connector.ConnectorTestCase):
Biser Milanov109150a2023-05-26 12:05:31 +0300237 'client_id': 1,
238 'access_mode': 'rw',
239 }
240+ self.fakeGlobalId = 'OneNiceGlobalId'
Biser Milanov0b188622023-06-02 17:20:09 +0300241+ self.api_calls_retry_max = 10
Biser Milanov109150a2023-05-26 12:05:31 +0300242 self.fakeConnection = None
243 self.fakeSize = 1024 * 1024 * 1024
Biser Milanov8cd1aa92023-06-06 16:58:06 +0300244+ self.reassign_wait_data = {'reassign': [
245+ {'volume': volumeNameExt(self.fakeProp['volume']),
246+ 'detach': [1], 'force': False}]}
Biser Milanov109150a2023-05-26 12:05:31 +0300247
Biser Milanov8cd1aa92023-06-06 16:58:06 +0300248 self.connector = connector.StorPoolConnector(
249 None, execute=self.execute)
Biser Milanov109150a2023-05-26 12:05:31 +0300250 self.adb = self.connector._attach
251
252 def test_connect_volume(self):
253- self.assertNotIn(self.volumeName(self.fakeProp['volume']),
254- self.adb.attached)
255- conn = self.connector.connect_volume(self.fakeProp)
256- self.assertIn('type', conn)
257- self.assertIn('path', conn)
258- self.assertIn(self.volumeName(self.fakeProp['volume']),
259- self.adb.attached)
260-
261- self.assertEqual(self.connector.get_search_path(), '/dev/storpool')
262- paths = self.connector.get_volume_paths(self.fakeProp)
263- self.assertEqual(len(paths), 1)
264- self.assertEqual(paths[0],
265- "/dev/storpool/" +
266- self.volumeName(self.fakeProp['volume']))
267- self.fakeConnection = conn
268+ volume_name = volumeNameExt(self.fakeProp['volume'])
269+ api = mock.MagicMock(spec=['volumesReassignWait', 'volumeInfo'])
270+ api.volumesReassignWait = mock.MagicMock(spec=['__call__'])
Biser Milanov0b188622023-06-02 17:20:09 +0300271+ volume_info = MockVolumeInfo(self.fakeGlobalId)
272+ api.volumeInfo = mock.Mock(return_value=volume_info)
Biser Milanov109150a2023-05-26 12:05:31 +0300273+
274+ with mock.patch.object(
275+ self.adb, attribute='api', spec=['__call__']
276+ ) as fake_api:
277+ fake_api.return_value = api
278+
279+ conn = self.connector.connect_volume(self.fakeProp)
280+ self.assertIn('type', conn)
281+ self.assertIn('path', conn)
282+ self.assertEqual(conn['path'],
283+ '/dev/storpool-byid/' + self.fakeGlobalId)
284+ self.assertEqual(len(api.volumesReassignWait.mock_calls), 1)
285+ self.assertEqual(api.volumesReassignWait.mock_calls[0], mock.call(
286+ {'reassign': [{'volume': 'os--volume--sp-vol-1', 'rw': [1]}]}))
287+ self.assertEqual(len(api.volumeInfo.mock_calls), 1)
288+ self.assertEqual(api.volumeInfo.mock_calls[0],
289+ mock.call(volume_name))
290+
291+ self.assertEqual(self.connector.get_search_path(), '/dev/storpool')
292+
293+ paths = self.connector.get_volume_paths(self.fakeProp)
294+ self.assertEqual(len(paths), 1)
295+ self.assertEqual(paths[0],
296+ "/dev/storpool/" +
297+ self.volumeName(self.fakeProp['volume']))
298+ self.fakeConnection = conn
299
300 def test_disconnect_volume(self):
301 if self.fakeConnection is None:
302 self.test_connect_volume()
303- self.assertIn(self.volumeName(self.fakeProp['volume']),
304- self.adb.attached)
305- self.connector.disconnect_volume(self.fakeProp, None)
306- self.assertNotIn(self.volumeName(self.fakeProp['volume']),
307- self.adb.attached)
308+
309+ api = mock.MagicMock(spec=['volumesReassignWait'])
310+ api.volumesReassignWait = mock.MagicMock(spec=['__call__'])
311+
312+ with mock.patch.object(
313+ self.adb, attribute='api', spec=['__call__']
314+ ) as fake_api:
315+ fake_api.return_value = api
Biser Milanov109150a2023-05-26 12:05:31 +0300316+
317+ self.connector.disconnect_volume(self.fakeProp, None)
318+ self.assertEqual(api.volumesReassignWait.mock_calls[0],
Biser Milanov8cd1aa92023-06-06 16:58:06 +0300319+ (mock.call(self.reassign_wait_data)))
Biser Milanov109150a2023-05-26 12:05:31 +0300320
321 def test_connect_exceptions(self):
322 """Raise exceptions on missing connection information"""
Biser Milanov8cd1aa92023-06-06 16:58:06 +0300323@@ -146,6 +168,105 @@ class StorPoolConnectorTestCase(test_connector.ConnectorTestCase):
Biser Milanov109150a2023-05-26 12:05:31 +0300324 self.assertRaises(exception.BrickException,
325 self.connector.disconnect_volume, c, None)
326
327+ def test_sp_ourid_exceptions(self):
328+ """Raise exceptions on missing SP_OURID"""
329+ with mock.patch.object(self.connector._attach, 'config')\
330+ as fake_config:
331+ fake_config.return_value = {}
332+
333+ self.assertRaises(exception.BrickException,
334+ self.connector.connect_volume, self.fakeProp)
335+
336+ self.assertRaises(exception.BrickException,
337+ self.connector.disconnect_volume, self.fakeProp,
338+ None)
339+
340+ def test_sp_api_exceptions(self):
341+ """Handle SP API exceptions"""
342+ api = mock.MagicMock(spec=['volumesReassignWait', 'volumeInfo'])
343+ api.volumesReassignWait = mock.MagicMock(spec=['__call__'])
Biser Milanov0b188622023-06-02 17:20:09 +0300344+ # The generic exception should bypass the SP API exception handling
Biser Milanov109150a2023-05-26 12:05:31 +0300345+ api.volumesReassignWait.side_effect = Exception()
346+ api.volumeInfo = mock.MagicMock(spec=['__call__'])
347+
348+ with mock.patch.object(
349+ self.adb, attribute='api', spec=['__call__']
350+ ) as fake_api:
351+ fake_api.return_value = api
352+
353+ self.assertRaises(exception.BrickException,
354+ self.connector.connect_volume, self.fakeProp)
355+
356+ self.assertRaises(exception.BrickException,
357+ self.connector.disconnect_volume, self.fakeProp,
358+ None)
359+
360+ api.volumesReassignWait.side_effect = ""
361+ api.volumeInfo = Exception()
362+
363+ with mock.patch.object(
364+ self.adb, attribute='api', spec=['__call__']
365+ ) as fake_api:
366+ fake_api.return_value = api
367+
368+ self.assertRaises(exception.BrickException,
369+ self.connector.connect_volume, self.fakeProp)
370+
371+ self.assertRaises(exception.BrickException,
372+ self.connector.disconnect_volume, self.fakeProp,
373+ None)
374+
Biser Milanov0b188622023-06-02 17:20:09 +0300375+ # Test the retry logic
Biser Milanov8cd1aa92023-06-06 16:58:06 +0300376+ def init_mock_api(retries):
377+ faulty_api.fail_count = retries
378+ faulty_api.real_fn = mock.MagicMock(spec=['__call__'])
379+ api.volumesReassignWait = faulty_api
380+ api.volumeInfo = mock.MagicMock(spec=['__call__'])
Biser Milanov0b188622023-06-02 17:20:09 +0300381+
Biser Milanov8cd1aa92023-06-06 16:58:06 +0300382+ init_mock_api(self.api_calls_retry_max - 1)
Biser Milanov0b188622023-06-02 17:20:09 +0300383+ with mock.patch.object(
384+ self.adb, attribute='api', spec=['__call__']
385+ ) as fake_api:
386+ fake_api.return_value = api
Biser Milanov0b188622023-06-02 17:20:09 +0300387+
388+ self.connector.disconnect_volume(self.fakeProp, None)
389+ self.assertEqual(self.api_calls_retry_max,
390+ len(faulty_api.real_fn.mock_calls))
391+ for mock_call in faulty_api.real_fn.mock_calls:
Biser Milanov8cd1aa92023-06-06 16:58:06 +0300392+ self.assertEqual(mock_call, mock.call(self.reassign_wait_data))
Biser Milanov0b188622023-06-02 17:20:09 +0300393+
Biser Milanov8cd1aa92023-06-06 16:58:06 +0300394+ init_mock_api(self.api_calls_retry_max)
Biser Milanov0b188622023-06-02 17:20:09 +0300395+ with mock.patch.object(
396+ self.adb, attribute='api', spec=['__call__']
397+ ) as fake_api:
398+ fake_api.return_value = api
Biser Milanov8cd1aa92023-06-06 16:58:06 +0300399+ rwd = copy.deepcopy(self.reassign_wait_data)
Biser Milanov0b188622023-06-02 17:20:09 +0300400+
401+ self.connector.disconnect_volume(self.fakeProp, None)
402+ self.assertEqual(self.api_calls_retry_max + 1,
403+ len(faulty_api.real_fn.mock_calls))
404+ for mock_call in faulty_api.real_fn.mock_calls[:-1]:
Biser Milanov8cd1aa92023-06-06 16:58:06 +0300405+ self.assertEqual(mock_call, mock.call(rwd))
406+ rwd['reassign'][0]['force'] = True
407+ self.assertEqual(faulty_api.real_fn.mock_calls[-1], mock.call(rwd))
408+
409+ init_mock_api(self.api_calls_retry_max + 1)
410+ with mock.patch.object(
411+ self.adb, attribute='api', spec=['__call__']
412+ ) as fake_api:
413+ fake_api.return_value = api
414+ rwd = copy.deepcopy(self.reassign_wait_data)
415+
416+ self.assertRaises(exception.BrickException,
417+ self.connector.disconnect_volume, self.fakeProp,
418+ None)
419+ self.assertEqual(self.api_calls_retry_max + 1,
420+ len(faulty_api.real_fn.mock_calls))
421+ for mock_call in faulty_api.real_fn.mock_calls[:-1]:
422+ self.assertEqual(mock_call, mock.call(rwd))
423+ rwd['reassign'][0]['force'] = True
424+ self.assertEqual(faulty_api.real_fn.mock_calls[-1], mock.call(rwd))
Biser Milanov0b188622023-06-02 17:20:09 +0300425+
Biser Milanov109150a2023-05-26 12:05:31 +0300426 def test_extend_volume(self):
427 if self.fakeConnection is None:
428 self.test_connect_volume()
429--
4302.25.1
431