blob: 71834bee92c6df9f7b7b686960a8991ed3dab3a2 [file] [log] [blame]
Biser Milanov0b188622023-06-02 17:20:09 +03001From 315448f05b310d4e4b2bad084ddcdc888e0c2cd0 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 Milanov0b188622023-06-02 17:20:09 +030016 os_brick/initiator/connectors/storpool.py | 84 +++++--
17 .../initiator/connectors/test_storpool.py | 219 +++++++++++++-----
18 2 files changed, 236 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 Milanov0b188622023-06-02 17:20:09 +0300144index 614deba..b539e2b 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 Milanov0b188622023-06-02 17:20:09 +0300147@@ -25,46 +25,35 @@ def volumeNameExt(vid):
148 return 'os--volume--{id}'.format(id=vid)
Biser Milanov109150a2023-05-26 12:05:31 +0300149
Biser Milanov0b188622023-06-02 17:20:09 +0300150
151+def faulty_api(req):
152+ faulty_api.real_fn(req)
153+ if faulty_api.fail_count > 0:
154+ faulty_api.fail_count -= 1
155+ raise MockApiError("busy",
156+ "'os--volume--sp-vol-1' is open at client 19")
157+
158+
159+class MockApiError(Exception):
160+ def __init__(self, name, desc):
161+ super(MockApiError, self).__init__()
162+ self.name = name
163+ self.desc = desc
164+
165+
166+class MockVolumeInfo(object):
167+ def __init__(self, global_id):
168+ self.globalId = global_id
169+
170+
Biser Milanov109150a2023-05-26 12:05:31 +0300171 class MockStorPoolADB(object):
172 def __init__(self, log):
173- self.requests = {}
174- self.attached = {}
175+ pass
176
177 def api(self):
178 pass
179
180- def add(self, req_id, req):
181- if req_id in self.requests:
182- raise Exception('Duplicate MockStorPool request added')
183- self.requests[req_id] = req
184-
185- def remove(self, req_id):
186- req = self.requests.get(req_id, None)
187- if req is None:
188- raise Exception('Unknown MockStorPool request removed')
189- elif req['volume'] in self.attached:
190- raise Exception('Removing attached MockStorPool volume')
191- del self.requests[req_id]
192-
193- def sync(self, req_id, detached):
194- req = self.requests.get(req_id, None)
195- if req is None:
196- raise Exception('Unknown MockStorPool request synced')
197- volume = req.get('volume', None)
198- if volume is None:
199- raise Exception('MockStorPool request without volume')
200-
201- if detached is None:
202- if volume in self.attached:
203- raise Exception('Duplicate MockStorPool request synced')
204- self.attached[volume] = req
205- else:
206- if volume != detached:
207- raise Exception(
208- 'Mismatched volumes on a MockStorPool request removal')
209- elif detached not in self.attached:
210- raise Exception('MockStorPool request not attached yet')
211- del self.attached[detached]
212+ def config(self):
213+ return {"SP_OURID": 1}
214
215 def volumeName(self, vid):
216 return volumeNameExt(vid)
Biser Milanov0b188622023-06-02 17:20:09 +0300217@@ -74,6 +63,10 @@ spopenstack = mock.Mock()
218 spopenstack.AttachDB = MockStorPoolADB
219 connector.spopenstack = spopenstack
220
221+spapi = mock.Mock()
222+spapi.ApiError = MockApiError
223+connector.spapi = spapi
224+
225
226 class StorPoolConnectorTestCase(test_connector.ConnectorTestCase):
227 def volumeName(self, vid):
228@@ -101,6 +94,8 @@ class StorPoolConnectorTestCase(test_connector.ConnectorTestCase):
Biser Milanov109150a2023-05-26 12:05:31 +0300229 'client_id': 1,
230 'access_mode': 'rw',
231 }
232+ self.fakeGlobalId = 'OneNiceGlobalId'
Biser Milanov0b188622023-06-02 17:20:09 +0300233+ self.api_calls_retry_max = 10
Biser Milanov109150a2023-05-26 12:05:31 +0300234 self.fakeConnection = None
235 self.fakeSize = 1024 * 1024 * 1024
236
Biser Milanov0b188622023-06-02 17:20:09 +0300237@@ -109,30 +104,56 @@ class StorPoolConnectorTestCase(test_connector.ConnectorTestCase):
Biser Milanov109150a2023-05-26 12:05:31 +0300238 self.adb = self.connector._attach
239
240 def test_connect_volume(self):
241- self.assertNotIn(self.volumeName(self.fakeProp['volume']),
242- self.adb.attached)
243- conn = self.connector.connect_volume(self.fakeProp)
244- self.assertIn('type', conn)
245- self.assertIn('path', conn)
246- self.assertIn(self.volumeName(self.fakeProp['volume']),
247- self.adb.attached)
248-
249- self.assertEqual(self.connector.get_search_path(), '/dev/storpool')
250- paths = self.connector.get_volume_paths(self.fakeProp)
251- self.assertEqual(len(paths), 1)
252- self.assertEqual(paths[0],
253- "/dev/storpool/" +
254- self.volumeName(self.fakeProp['volume']))
255- self.fakeConnection = conn
256+ volume_name = volumeNameExt(self.fakeProp['volume'])
257+ api = mock.MagicMock(spec=['volumesReassignWait', 'volumeInfo'])
258+ api.volumesReassignWait = mock.MagicMock(spec=['__call__'])
Biser Milanov0b188622023-06-02 17:20:09 +0300259+ volume_info = MockVolumeInfo(self.fakeGlobalId)
260+ api.volumeInfo = mock.Mock(return_value=volume_info)
Biser Milanov109150a2023-05-26 12:05:31 +0300261+
262+ with mock.patch.object(
263+ self.adb, attribute='api', spec=['__call__']
264+ ) as fake_api:
265+ fake_api.return_value = api
266+
267+ conn = self.connector.connect_volume(self.fakeProp)
268+ self.assertIn('type', conn)
269+ self.assertIn('path', conn)
270+ self.assertEqual(conn['path'],
271+ '/dev/storpool-byid/' + self.fakeGlobalId)
272+ self.assertEqual(len(api.volumesReassignWait.mock_calls), 1)
273+ self.assertEqual(api.volumesReassignWait.mock_calls[0], mock.call(
274+ {'reassign': [{'volume': 'os--volume--sp-vol-1', 'rw': [1]}]}))
275+ self.assertEqual(len(api.volumeInfo.mock_calls), 1)
276+ self.assertEqual(api.volumeInfo.mock_calls[0],
277+ mock.call(volume_name))
278+
279+ self.assertEqual(self.connector.get_search_path(), '/dev/storpool')
280+
281+ paths = self.connector.get_volume_paths(self.fakeProp)
282+ self.assertEqual(len(paths), 1)
283+ self.assertEqual(paths[0],
284+ "/dev/storpool/" +
285+ self.volumeName(self.fakeProp['volume']))
286+ self.fakeConnection = conn
287
288 def test_disconnect_volume(self):
289 if self.fakeConnection is None:
290 self.test_connect_volume()
291- self.assertIn(self.volumeName(self.fakeProp['volume']),
292- self.adb.attached)
293- self.connector.disconnect_volume(self.fakeProp, None)
294- self.assertNotIn(self.volumeName(self.fakeProp['volume']),
295- self.adb.attached)
296+
297+ api = mock.MagicMock(spec=['volumesReassignWait'])
298+ api.volumesReassignWait = mock.MagicMock(spec=['__call__'])
299+
300+ with mock.patch.object(
301+ self.adb, attribute='api', spec=['__call__']
302+ ) as fake_api:
303+ fake_api.return_value = api
304+ reassign_wait_data = {'reassign': [
305+ {'volume': volumeNameExt(self.fakeProp['volume']),
Biser Milanov0b188622023-06-02 17:20:09 +0300306+ 'detach': [1], 'force': False}]}
Biser Milanov109150a2023-05-26 12:05:31 +0300307+
308+ self.connector.disconnect_volume(self.fakeProp, None)
309+ self.assertEqual(api.volumesReassignWait.mock_calls[0],
310+ (mock.call(reassign_wait_data)))
311
312 def test_connect_exceptions(self):
313 """Raise exceptions on missing connection information"""
Biser Milanov0b188622023-06-02 17:20:09 +0300314@@ -146,6 +167,96 @@ class StorPoolConnectorTestCase(test_connector.ConnectorTestCase):
Biser Milanov109150a2023-05-26 12:05:31 +0300315 self.assertRaises(exception.BrickException,
316 self.connector.disconnect_volume, c, None)
317
318+ def test_sp_ourid_exceptions(self):
319+ """Raise exceptions on missing SP_OURID"""
320+ with mock.patch.object(self.connector._attach, 'config')\
321+ as fake_config:
322+ fake_config.return_value = {}
323+
324+ self.assertRaises(exception.BrickException,
325+ self.connector.connect_volume, self.fakeProp)
326+
327+ self.assertRaises(exception.BrickException,
328+ self.connector.disconnect_volume, self.fakeProp,
329+ None)
330+
331+ def test_sp_api_exceptions(self):
332+ """Handle SP API exceptions"""
333+ api = mock.MagicMock(spec=['volumesReassignWait', 'volumeInfo'])
334+ api.volumesReassignWait = mock.MagicMock(spec=['__call__'])
Biser Milanov0b188622023-06-02 17:20:09 +0300335+ # The generic exception should bypass the SP API exception handling
Biser Milanov109150a2023-05-26 12:05:31 +0300336+ api.volumesReassignWait.side_effect = Exception()
337+ api.volumeInfo = mock.MagicMock(spec=['__call__'])
338+
339+ with mock.patch.object(
340+ self.adb, attribute='api', spec=['__call__']
341+ ) as fake_api:
342+ fake_api.return_value = api
343+
344+ self.assertRaises(exception.BrickException,
345+ self.connector.connect_volume, self.fakeProp)
346+
347+ self.assertRaises(exception.BrickException,
348+ self.connector.disconnect_volume, self.fakeProp,
349+ None)
350+
351+ api.volumesReassignWait.side_effect = ""
352+ api.volumeInfo = Exception()
353+
354+ with mock.patch.object(
355+ self.adb, attribute='api', spec=['__call__']
356+ ) as fake_api:
357+ fake_api.return_value = api
358+
359+ self.assertRaises(exception.BrickException,
360+ self.connector.connect_volume, self.fakeProp)
361+
362+ self.assertRaises(exception.BrickException,
363+ self.connector.disconnect_volume, self.fakeProp,
364+ None)
365+
Biser Milanov0b188622023-06-02 17:20:09 +0300366+ # Test the retry logic
367+ faulty_api.fail_count = self.api_calls_retry_max - 1
368+ faulty_api.real_fn = mock.MagicMock(spec=['__call__'])
369+ api.volumesReassignWait = faulty_api
370+ api.volumeInfo = mock.MagicMock(spec=['__call__'])
371+
372+ with mock.patch.object(
373+ self.adb, attribute='api', spec=['__call__']
374+ ) as fake_api:
375+ fake_api.return_value = api
376+ reassign_wait_data = {'reassign': [
377+ {'volume': volumeNameExt(self.fakeProp['volume']),
378+ 'detach': [1], 'force': False}]}
379+
380+ self.connector.disconnect_volume(self.fakeProp, None)
381+ self.assertEqual(self.api_calls_retry_max,
382+ len(faulty_api.real_fn.mock_calls))
383+ for mock_call in faulty_api.real_fn.mock_calls:
384+ self.assertEqual(mock_call, mock.call(reassign_wait_data))
385+
386+ faulty_api.fail_count = self.api_calls_retry_max
387+ faulty_api.real_fn = mock.MagicMock(spec=['__call__'])
388+ api.volumesReassignWait = faulty_api
389+ api.volumeInfo = mock.MagicMock(spec=['__call__'])
390+
391+ with mock.patch.object(
392+ self.adb, attribute='api', spec=['__call__']
393+ ) as fake_api:
394+ fake_api.return_value = api
395+ reassign_wait_data = {'reassign': [
396+ {'volume': volumeNameExt(self.fakeProp['volume']),
397+ 'detach': [1], 'force': False}]}
398+
399+ self.connector.disconnect_volume(self.fakeProp, None)
400+ self.assertEqual(self.api_calls_retry_max + 1,
401+ len(faulty_api.real_fn.mock_calls))
402+ for mock_call in faulty_api.real_fn.mock_calls[:-1]:
403+ self.assertEqual(mock_call, mock.call(reassign_wait_data))
404+ reassign_wait_data['reassign'][0]['force'] = True
405+ self.assertEqual(faulty_api.real_fn.mock_calls[-1],
406+ mock.call(reassign_wait_data))
407+
Biser Milanov109150a2023-05-26 12:05:31 +0300408 def test_extend_volume(self):
409 if self.fakeConnection is None:
410 self.test_connect_volume()
411--
4122.25.1
413