Add the "use storpool's api for attach/detach" patch.

Change-Id: Ic24fe4f0df773c9e2d128bb296d293bfb9a93436
diff --git a/patches/openstack/os-brick/attach-globalid.patch b/patches/openstack/os-brick/attach-globalid.patch
new file mode 100644
index 0000000..6e67a7d
--- /dev/null
+++ b/patches/openstack/os-brick/attach-globalid.patch
@@ -0,0 +1,279 @@
+From 82be031a554d293307900236ed8f4c1737452891 Mon Sep 17 00:00:00 2001
+From: Biser Milanov <biser.milanov@storpool.com>
+Date: Tue, 23 May 2023 17:55:09 +0300
+Subject: [PATCH] storpool.py: Use StorPool's API for Attach/Detach
+
+Drop the use of the node-scoped file that helped manage volumes on that
+node.
+
+Volumes are now attached and detached by calling the StorPool API
+directly.
+
+Additionally refactor tests to reflect this.
+
+Change-Id: I0fabcde1dc249b9b09f99ee0b7d759432972314a
+---
+ os_brick/initiator/connectors/storpool.py     |  48 ++++--
+ .../initiator/connectors/test_storpool.py     | 151 +++++++++++-------
+ 2 files changed, 132 insertions(+), 67 deletions(-)
+
+diff --git a/os_brick/initiator/connectors/storpool.py b/os_brick/initiator/connectors/storpool.py
+index e1bd55c..940b78f 100644
+--- a/os_brick/initiator/connectors/storpool.py
++++ b/os_brick/initiator/connectors/storpool.py
+@@ -76,16 +76,29 @@ class StorPoolConnector(base.BaseLinuxConnector):
+         if mode is None or mode not in ('rw', 'ro'):
+             raise exception.BrickException(
+                 'Invalid access_mode specified in the connection data.')
+-        req_id = 'brick-%s-%s' % (client_id, volume_id)
+-        self._attach.add(req_id, {
+-            'volume': volume,
+-            'type': 'brick',
+-            'id': req_id,
+-            'rights': 1 if mode == 'ro' else 2,
+-            'volsnap': False
+-        })
+-        self._attach.sync(req_id, None)
+-        return {'type': 'block', 'path': '/dev/storpool/' + volume}
++        try:
++            sp_ourid = self._attach.config()["SP_OURID"]
++        except KeyError:
++            raise exception.BrickException(
++                'SP_OURID missing, cannot connect volume %s' % volume_id)
++
++        try:
++            self._attach.api().volumesReassignWait(
++                {"reassign": [{"volume": volume, mode: [sp_ourid]}]})
++        except Exception as exc:
++            raise exception.BrickException(
++                'Communication with the StorPool API '
++                'failed: %s' % (exc)) from exc
++
++        try:
++            volume_info = self._attach.api().volumeInfo(volume)
++        except Exception as exc:
++            raise exception.BrickException(
++                'Communication with the StorPool API '
++                'failed: %s' % (exc)) from exc
++
++        sp_global_id = volume_info["data"]["globalId"]
++        return {'type': 'block', 'path': '/dev/storpool-byid/' + sp_global_id}
+ 
+     @utils.connect_volume_undo_prepare_result(unlink_after=True)
+     def disconnect_volume(self, connection_properties, device_info,
+@@ -124,9 +137,18 @@ class StorPoolConnector(base.BaseLinuxConnector):
+             raise exception.BrickException(
+                 'Invalid StorPool connection data, no volume ID specified.')
+         volume = self._attach.volumeName(volume_id)
+-        req_id = 'brick-%s-%s' % (client_id, volume_id)
+-        self._attach.sync(req_id, volume)
+-        self._attach.remove(req_id)
++        try:
++            sp_ourid = self._attach.config()["SP_OURID"]
++        except KeyError:
++            raise exception.BrickException(
++                'SP_OURID missing, cannot disconnect volume %s' % volume)
++        try:
++            self._attach.api().volumesReassignWait(
++                {"reassign": [{"volume": volume, "detach": [sp_ourid]}]})
++        except Exception as exc:
++            raise exception.BrickException(
++                'Communication with the StorPool API '
++                'failed: %s' % (exc)) from exc
+ 
+     def get_search_path(self):
+         return '/dev/storpool'
+diff --git a/os_brick/tests/initiator/connectors/test_storpool.py b/os_brick/tests/initiator/connectors/test_storpool.py
+index 614deba..9cc1076 100644
+--- a/os_brick/tests/initiator/connectors/test_storpool.py
++++ b/os_brick/tests/initiator/connectors/test_storpool.py
+@@ -27,44 +27,13 @@ def volumeNameExt(vid):
+ 
+ class MockStorPoolADB(object):
+     def __init__(self, log):
+-        self.requests = {}
+-        self.attached = {}
++        pass
+ 
+     def api(self):
+         pass
+ 
+-    def add(self, req_id, req):
+-        if req_id in self.requests:
+-            raise Exception('Duplicate MockStorPool request added')
+-        self.requests[req_id] = req
+-
+-    def remove(self, req_id):
+-        req = self.requests.get(req_id, None)
+-        if req is None:
+-            raise Exception('Unknown MockStorPool request removed')
+-        elif req['volume'] in self.attached:
+-            raise Exception('Removing attached MockStorPool volume')
+-        del self.requests[req_id]
+-
+-    def sync(self, req_id, detached):
+-        req = self.requests.get(req_id, None)
+-        if req is None:
+-            raise Exception('Unknown MockStorPool request synced')
+-        volume = req.get('volume', None)
+-        if volume is None:
+-            raise Exception('MockStorPool request without volume')
+-
+-        if detached is None:
+-            if volume in self.attached:
+-                raise Exception('Duplicate MockStorPool request synced')
+-            self.attached[volume] = req
+-        else:
+-            if volume != detached:
+-                raise Exception(
+-                    'Mismatched volumes on a MockStorPool request removal')
+-            elif detached not in self.attached:
+-                raise Exception('MockStorPool request not attached yet')
+-            del self.attached[detached]
++    def config(self):
++        return {"SP_OURID": 1}
+ 
+     def volumeName(self, vid):
+         return volumeNameExt(vid)
+@@ -101,6 +70,7 @@ class StorPoolConnectorTestCase(test_connector.ConnectorTestCase):
+             'client_id': 1,
+             'access_mode': 'rw',
+         }
++        self.fakeGlobalId = 'OneNiceGlobalId'
+         self.fakeConnection = None
+         self.fakeSize = 1024 * 1024 * 1024
+ 
+@@ -109,30 +79,56 @@ class StorPoolConnectorTestCase(test_connector.ConnectorTestCase):
+         self.adb = self.connector._attach
+ 
+     def test_connect_volume(self):
+-        self.assertNotIn(self.volumeName(self.fakeProp['volume']),
+-                         self.adb.attached)
+-        conn = self.connector.connect_volume(self.fakeProp)
+-        self.assertIn('type', conn)
+-        self.assertIn('path', conn)
+-        self.assertIn(self.volumeName(self.fakeProp['volume']),
+-                      self.adb.attached)
+-
+-        self.assertEqual(self.connector.get_search_path(), '/dev/storpool')
+-        paths = self.connector.get_volume_paths(self.fakeProp)
+-        self.assertEqual(len(paths), 1)
+-        self.assertEqual(paths[0],
+-                         "/dev/storpool/" +
+-                         self.volumeName(self.fakeProp['volume']))
+-        self.fakeConnection = conn
++        volume_name = volumeNameExt(self.fakeProp['volume'])
++        api = mock.MagicMock(spec=['volumesReassignWait', 'volumeInfo'])
++        api.volumesReassignWait = mock.MagicMock(spec=['__call__'])
++        api.volumeInfo = mock.Mock(
++            return_value={'data': {'globalId': self.fakeGlobalId}})
++
++        with mock.patch.object(
++                self.adb, attribute='api', spec=['__call__']
++        ) as fake_api:
++            fake_api.return_value = api
++
++            conn = self.connector.connect_volume(self.fakeProp)
++            self.assertIn('type', conn)
++            self.assertIn('path', conn)
++            self.assertEqual(conn['path'],
++                             '/dev/storpool-byid/' + self.fakeGlobalId)
++            self.assertEqual(len(api.volumesReassignWait.mock_calls), 1)
++            self.assertEqual(api.volumesReassignWait.mock_calls[0], mock.call(
++                {'reassign': [{'volume': 'os--volume--sp-vol-1', 'rw': [1]}]}))
++            self.assertEqual(len(api.volumeInfo.mock_calls), 1)
++            self.assertEqual(api.volumeInfo.mock_calls[0],
++                             mock.call(volume_name))
++
++            self.assertEqual(self.connector.get_search_path(), '/dev/storpool')
++
++            paths = self.connector.get_volume_paths(self.fakeProp)
++            self.assertEqual(len(paths), 1)
++            self.assertEqual(paths[0],
++                             "/dev/storpool/" +
++                             self.volumeName(self.fakeProp['volume']))
++            self.fakeConnection = conn
+ 
+     def test_disconnect_volume(self):
+         if self.fakeConnection is None:
+             self.test_connect_volume()
+-        self.assertIn(self.volumeName(self.fakeProp['volume']),
+-                      self.adb.attached)
+-        self.connector.disconnect_volume(self.fakeProp, None)
+-        self.assertNotIn(self.volumeName(self.fakeProp['volume']),
+-                         self.adb.attached)
++
++        api = mock.MagicMock(spec=['volumesReassignWait'])
++        api.volumesReassignWait = mock.MagicMock(spec=['__call__'])
++
++        with mock.patch.object(
++                self.adb, attribute='api', spec=['__call__']
++        ) as fake_api:
++            fake_api.return_value = api
++            reassign_wait_data = {'reassign': [
++                {'volume': volumeNameExt(self.fakeProp['volume']),
++                 'detach': [1]}]}
++
++            self.connector.disconnect_volume(self.fakeProp, None)
++            self.assertEqual(api.volumesReassignWait.mock_calls[0],
++                             (mock.call(reassign_wait_data)))
+ 
+     def test_connect_exceptions(self):
+         """Raise exceptions on missing connection information"""
+@@ -146,6 +142,53 @@ class StorPoolConnectorTestCase(test_connector.ConnectorTestCase):
+                 self.assertRaises(exception.BrickException,
+                                   self.connector.disconnect_volume, c, None)
+ 
++    def test_sp_ourid_exceptions(self):
++        """Raise exceptions on missing SP_OURID"""
++        with mock.patch.object(self.connector._attach, 'config')\
++                as fake_config:
++            fake_config.return_value = {}
++
++            self.assertRaises(exception.BrickException,
++                              self.connector.connect_volume, self.fakeProp)
++
++            self.assertRaises(exception.BrickException,
++                              self.connector.disconnect_volume, self.fakeProp,
++                              None)
++
++    def test_sp_api_exceptions(self):
++        """Handle SP API exceptions"""
++        api = mock.MagicMock(spec=['volumesReassignWait', 'volumeInfo'])
++        api.volumesReassignWait = mock.MagicMock(spec=['__call__'])
++        api.volumesReassignWait.side_effect = Exception()
++        api.volumeInfo = mock.MagicMock(spec=['__call__'])
++
++        with mock.patch.object(
++                self.adb, attribute='api', spec=['__call__']
++        ) as fake_api:
++            fake_api.return_value = api
++
++            self.assertRaises(exception.BrickException,
++                              self.connector.connect_volume, self.fakeProp)
++
++            self.assertRaises(exception.BrickException,
++                              self.connector.disconnect_volume, self.fakeProp,
++                              None)
++
++        api.volumesReassignWait.side_effect = ""
++        api.volumeInfo = Exception()
++
++        with mock.patch.object(
++                self.adb, attribute='api', spec=['__call__']
++        ) as fake_api:
++            fake_api.return_value = api
++
++            self.assertRaises(exception.BrickException,
++                              self.connector.connect_volume, self.fakeProp)
++
++            self.assertRaises(exception.BrickException,
++                              self.connector.disconnect_volume, self.fakeProp,
++                              None)
++
+     def test_extend_volume(self):
+         if self.fakeConnection is None:
+             self.test_connect_volume()
+-- 
+2.25.1
+
diff --git a/patches/series.experimental b/patches/series.experimental
index 6c1928d..fe59680 100644
--- a/patches/series.experimental
+++ b/patches/series.experimental
@@ -11,3 +11,4 @@
 openstack/cinder/sep-sp-iscsi.patch
 openstack/cinderlib/storpool-test-20190910.patch
 openstack/devstack/eatmydata.patch
+openstack/os-brick/attach-globalid.patch