blob: a1f9ef7366f6d822677909067469a6d913b3a9f7 [file] [log] [blame]
Biser Milanov65a1a6f2023-06-02 11:52:03 +03001From eeea4310317d10a6f730777e1e0b1cb73cb004d4 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 Milanov65a1a6f2023-06-02 11:52:03 +030016 os_brick/initiator/connectors/storpool.py | 81 ++++++++--
Biser Milanov109150a2023-05-26 12:05:31 +030017 .../initiator/connectors/test_storpool.py | 151 +++++++++++-------
Biser Milanov65a1a6f2023-06-02 11:52:03 +030018 2 files changed, 165 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 Milanov65a1a6f2023-06-02 11:52:03 +030021index e1bd55c..72d2acf 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)
43@@ -46,6 +51,35 @@ class StorPoolConnector(base.BaseLinuxConnector):
44 else:
45 self._attach = None
46
47+ def _detach_retry(self, sp_ourid, volume):
48+ """Retries attempt to handle LUKS tests-related failures:"
49+ busy: volume ... open at ...
50+ """
51+ count = 10
52+ while True:
53+ try:
54+ force = count == 0
55+ self._attach.api().volumesReassignWait(
56+ {
57+ "reassign": [{
58+ "volume": volume,
59+ "detach": [sp_ourid],
60+ "force": force,
61+ }]
62+ }
63+ )
64+ break
65+ except spapi.ApiError as exc:
66+ if (
67+ exc.name in ("busy", "invalidParam")
68+ and "is open at" in exc.desc
69+ ):
70+ assert count > 0
71+ time.sleep(0.2)
72+ count -= 1
73+ else:
74+ raise
75+
76 @staticmethod
77 def get_connector_properties(root_helper, *args, **kwargs):
78 """The StorPool connector properties."""
79@@ -76,16 +110,29 @@ class StorPoolConnector(base.BaseLinuxConnector):
Biser Milanov109150a2023-05-26 12:05:31 +030080 if mode is None or mode not in ('rw', 'ro'):
81 raise exception.BrickException(
82 'Invalid access_mode specified in the connection data.')
83- req_id = 'brick-%s-%s' % (client_id, volume_id)
84- self._attach.add(req_id, {
85- 'volume': volume,
86- 'type': 'brick',
87- 'id': req_id,
88- 'rights': 1 if mode == 'ro' else 2,
89- 'volsnap': False
90- })
91- self._attach.sync(req_id, None)
92- return {'type': 'block', 'path': '/dev/storpool/' + volume}
93+ try:
94+ sp_ourid = self._attach.config()["SP_OURID"]
95+ except KeyError:
96+ raise exception.BrickException(
97+ 'SP_OURID missing, cannot connect volume %s' % volume_id)
98+
99+ try:
100+ self._attach.api().volumesReassignWait(
101+ {"reassign": [{"volume": volume, mode: [sp_ourid]}]})
102+ except Exception as exc:
103+ raise exception.BrickException(
104+ 'Communication with the StorPool API '
105+ 'failed: %s' % (exc)) from exc
106+
107+ try:
108+ volume_info = self._attach.api().volumeInfo(volume)
109+ except Exception as exc:
110+ raise exception.BrickException(
111+ 'Communication with the StorPool API '
112+ 'failed: %s' % (exc)) from exc
113+
Biser Milanov7795e092023-05-30 13:24:09 +0300114+ sp_global_id = volume_info.globalId
Biser Milanov109150a2023-05-26 12:05:31 +0300115+ return {'type': 'block', 'path': '/dev/storpool-byid/' + sp_global_id}
116
117 @utils.connect_volume_undo_prepare_result(unlink_after=True)
118 def disconnect_volume(self, connection_properties, device_info,
Biser Milanov65a1a6f2023-06-02 11:52:03 +0300119@@ -124,9 +171,17 @@ class StorPoolConnector(base.BaseLinuxConnector):
Biser Milanov109150a2023-05-26 12:05:31 +0300120 raise exception.BrickException(
121 'Invalid StorPool connection data, no volume ID specified.')
122 volume = self._attach.volumeName(volume_id)
123- req_id = 'brick-%s-%s' % (client_id, volume_id)
124- self._attach.sync(req_id, volume)
125- self._attach.remove(req_id)
126+ try:
127+ sp_ourid = self._attach.config()["SP_OURID"]
128+ except KeyError:
129+ raise exception.BrickException(
130+ 'SP_OURID missing, cannot disconnect volume %s' % volume)
131+ try:
Biser Milanov65a1a6f2023-06-02 11:52:03 +0300132+ self._detach_retry(sp_ourid, volume)
Biser Milanov109150a2023-05-26 12:05:31 +0300133+ except Exception as exc:
134+ raise exception.BrickException(
135+ 'Communication with the StorPool API '
136+ 'failed: %s' % (exc)) from exc
137
138 def get_search_path(self):
139 return '/dev/storpool'
140diff --git a/os_brick/tests/initiator/connectors/test_storpool.py b/os_brick/tests/initiator/connectors/test_storpool.py
141index 614deba..9cc1076 100644
142--- a/os_brick/tests/initiator/connectors/test_storpool.py
143+++ b/os_brick/tests/initiator/connectors/test_storpool.py
144@@ -27,44 +27,13 @@ def volumeNameExt(vid):
145
146 class MockStorPoolADB(object):
147 def __init__(self, log):
148- self.requests = {}
149- self.attached = {}
150+ pass
151
152 def api(self):
153 pass
154
155- def add(self, req_id, req):
156- if req_id in self.requests:
157- raise Exception('Duplicate MockStorPool request added')
158- self.requests[req_id] = req
159-
160- def remove(self, req_id):
161- req = self.requests.get(req_id, None)
162- if req is None:
163- raise Exception('Unknown MockStorPool request removed')
164- elif req['volume'] in self.attached:
165- raise Exception('Removing attached MockStorPool volume')
166- del self.requests[req_id]
167-
168- def sync(self, req_id, detached):
169- req = self.requests.get(req_id, None)
170- if req is None:
171- raise Exception('Unknown MockStorPool request synced')
172- volume = req.get('volume', None)
173- if volume is None:
174- raise Exception('MockStorPool request without volume')
175-
176- if detached is None:
177- if volume in self.attached:
178- raise Exception('Duplicate MockStorPool request synced')
179- self.attached[volume] = req
180- else:
181- if volume != detached:
182- raise Exception(
183- 'Mismatched volumes on a MockStorPool request removal')
184- elif detached not in self.attached:
185- raise Exception('MockStorPool request not attached yet')
186- del self.attached[detached]
187+ def config(self):
188+ return {"SP_OURID": 1}
189
190 def volumeName(self, vid):
191 return volumeNameExt(vid)
192@@ -101,6 +70,7 @@ class StorPoolConnectorTestCase(test_connector.ConnectorTestCase):
193 'client_id': 1,
194 'access_mode': 'rw',
195 }
196+ self.fakeGlobalId = 'OneNiceGlobalId'
197 self.fakeConnection = None
198 self.fakeSize = 1024 * 1024 * 1024
199
200@@ -109,30 +79,56 @@ class StorPoolConnectorTestCase(test_connector.ConnectorTestCase):
201 self.adb = self.connector._attach
202
203 def test_connect_volume(self):
204- self.assertNotIn(self.volumeName(self.fakeProp['volume']),
205- self.adb.attached)
206- conn = self.connector.connect_volume(self.fakeProp)
207- self.assertIn('type', conn)
208- self.assertIn('path', conn)
209- self.assertIn(self.volumeName(self.fakeProp['volume']),
210- self.adb.attached)
211-
212- self.assertEqual(self.connector.get_search_path(), '/dev/storpool')
213- paths = self.connector.get_volume_paths(self.fakeProp)
214- self.assertEqual(len(paths), 1)
215- self.assertEqual(paths[0],
216- "/dev/storpool/" +
217- self.volumeName(self.fakeProp['volume']))
218- self.fakeConnection = conn
219+ volume_name = volumeNameExt(self.fakeProp['volume'])
220+ api = mock.MagicMock(spec=['volumesReassignWait', 'volumeInfo'])
221+ api.volumesReassignWait = mock.MagicMock(spec=['__call__'])
222+ api.volumeInfo = mock.Mock(
223+ return_value={'data': {'globalId': self.fakeGlobalId}})
224+
225+ with mock.patch.object(
226+ self.adb, attribute='api', spec=['__call__']
227+ ) as fake_api:
228+ fake_api.return_value = api
229+
230+ conn = self.connector.connect_volume(self.fakeProp)
231+ self.assertIn('type', conn)
232+ self.assertIn('path', conn)
233+ self.assertEqual(conn['path'],
234+ '/dev/storpool-byid/' + self.fakeGlobalId)
235+ self.assertEqual(len(api.volumesReassignWait.mock_calls), 1)
236+ self.assertEqual(api.volumesReassignWait.mock_calls[0], mock.call(
237+ {'reassign': [{'volume': 'os--volume--sp-vol-1', 'rw': [1]}]}))
238+ self.assertEqual(len(api.volumeInfo.mock_calls), 1)
239+ self.assertEqual(api.volumeInfo.mock_calls[0],
240+ mock.call(volume_name))
241+
242+ self.assertEqual(self.connector.get_search_path(), '/dev/storpool')
243+
244+ paths = self.connector.get_volume_paths(self.fakeProp)
245+ self.assertEqual(len(paths), 1)
246+ self.assertEqual(paths[0],
247+ "/dev/storpool/" +
248+ self.volumeName(self.fakeProp['volume']))
249+ self.fakeConnection = conn
250
251 def test_disconnect_volume(self):
252 if self.fakeConnection is None:
253 self.test_connect_volume()
254- self.assertIn(self.volumeName(self.fakeProp['volume']),
255- self.adb.attached)
256- self.connector.disconnect_volume(self.fakeProp, None)
257- self.assertNotIn(self.volumeName(self.fakeProp['volume']),
258- self.adb.attached)
259+
260+ api = mock.MagicMock(spec=['volumesReassignWait'])
261+ api.volumesReassignWait = mock.MagicMock(spec=['__call__'])
262+
263+ with mock.patch.object(
264+ self.adb, attribute='api', spec=['__call__']
265+ ) as fake_api:
266+ fake_api.return_value = api
267+ reassign_wait_data = {'reassign': [
268+ {'volume': volumeNameExt(self.fakeProp['volume']),
269+ 'detach': [1]}]}
270+
271+ self.connector.disconnect_volume(self.fakeProp, None)
272+ self.assertEqual(api.volumesReassignWait.mock_calls[0],
273+ (mock.call(reassign_wait_data)))
274
275 def test_connect_exceptions(self):
276 """Raise exceptions on missing connection information"""
277@@ -146,6 +142,53 @@ class StorPoolConnectorTestCase(test_connector.ConnectorTestCase):
278 self.assertRaises(exception.BrickException,
279 self.connector.disconnect_volume, c, None)
280
281+ def test_sp_ourid_exceptions(self):
282+ """Raise exceptions on missing SP_OURID"""
283+ with mock.patch.object(self.connector._attach, 'config')\
284+ as fake_config:
285+ fake_config.return_value = {}
286+
287+ self.assertRaises(exception.BrickException,
288+ self.connector.connect_volume, self.fakeProp)
289+
290+ self.assertRaises(exception.BrickException,
291+ self.connector.disconnect_volume, self.fakeProp,
292+ None)
293+
294+ def test_sp_api_exceptions(self):
295+ """Handle SP API exceptions"""
296+ api = mock.MagicMock(spec=['volumesReassignWait', 'volumeInfo'])
297+ api.volumesReassignWait = mock.MagicMock(spec=['__call__'])
298+ api.volumesReassignWait.side_effect = Exception()
299+ api.volumeInfo = mock.MagicMock(spec=['__call__'])
300+
301+ with mock.patch.object(
302+ self.adb, attribute='api', spec=['__call__']
303+ ) as fake_api:
304+ fake_api.return_value = api
305+
306+ self.assertRaises(exception.BrickException,
307+ self.connector.connect_volume, self.fakeProp)
308+
309+ self.assertRaises(exception.BrickException,
310+ self.connector.disconnect_volume, self.fakeProp,
311+ None)
312+
313+ api.volumesReassignWait.side_effect = ""
314+ api.volumeInfo = Exception()
315+
316+ with mock.patch.object(
317+ self.adb, attribute='api', spec=['__call__']
318+ ) as fake_api:
319+ fake_api.return_value = api
320+
321+ self.assertRaises(exception.BrickException,
322+ self.connector.connect_volume, self.fakeProp)
323+
324+ self.assertRaises(exception.BrickException,
325+ self.connector.disconnect_volume, self.fakeProp,
326+ None)
327+
328 def test_extend_volume(self):
329 if self.fakeConnection is None:
330 self.test_connect_volume()
331--
3322.25.1
333