From 3f3112f7965ea41b24b17d305b37e42ab6ea0bff Mon Sep 17 00:00:00 2001 From: Vladislav Belogrudov Date: Thu, 6 Jul 2023 12:54:45 +0300 Subject: [PATCH] Yadro Tatlin Unified FC driver Added support of FC to Tatlin Unified driver Implements: blueprint yadro-tatlin-unified-fc Co-Authored-By: Sergey Karpenkov Change-Id: Ia8192854a7095354f2616d53d2c5b71580384dcb --- .../drivers/yadro/test_tatlin_client.py | 191 ++++++--- .../drivers/yadro/test_tatlin_common.py | 3 +- .../volume/drivers/yadro/test_tatlin_fc.py | 366 ++++++++++++++++++ .../volume/drivers/yadro/test_tatlin_iscsi.py | 2 +- cinder/volume/drivers/yadro/tatlin_common.py | 55 ++- cinder/volume/drivers/yadro/tatlin_fc.py | 124 ++++++ cinder/volume/drivers/yadro/tatlin_iscsi.py | 86 ++-- .../drivers/yadro-tatlin-volume-driver.rst | 20 +- doc/source/reference/support-matrix.ini | 2 +- ...ro-tatlin-unified-fc-b6e1225ad99c6304.yaml | 4 + 10 files changed, 721 insertions(+), 132 deletions(-) create mode 100644 cinder/tests/unit/volume/drivers/yadro/test_tatlin_fc.py create mode 100644 cinder/volume/drivers/yadro/tatlin_fc.py create mode 100644 releasenotes/notes/bp-yadro-tatlin-unified-fc-b6e1225ad99c6304.yaml diff --git a/cinder/tests/unit/volume/drivers/yadro/test_tatlin_client.py b/cinder/tests/unit/volume/drivers/yadro/test_tatlin_client.py index 511bac1cba2..89cdbeef893 100644 --- a/cinder/tests/unit/volume/drivers/yadro/test_tatlin_client.py +++ b/cinder/tests/unit/volume/drivers/yadro/test_tatlin_client.py @@ -35,39 +35,55 @@ from cinder.volume.drivers.yadro.tatlin_client import TatlinClientV25 from cinder.volume.drivers.yadro.tatlin_exception import TatlinAPIException -RES_PORTS_RESP = [ - { - "port": "fc20", - "port_status": "healthy", - "port_status_desc": "resource is available", - "running": [ - "sp-0", - "sp-1" - ], - "wwn": [ - "10:00:14:52:90:00:03:10", - "10:00:14:52:90:00:03:90" - ], - "lun": "scsi-lun-fc20-5", - "volume": "pty-vol-0d9627cb-c52e-49f1-878c-57c9bc3010c9", - "lun_index": "5" - } -] +VOL_ID = 'cinder-volume-id' + +LUN_ID = 75 + +HOST_ID = 'host-id' + +HOST_ID_2 = 'host-id-2' + +HOST_GROUP_ID = 'group-id' + +HOST_GROUP_NAME = 'cinder-group' + +HOST_IQN = 'iqn.1994-05.com.redhat:12345' + +POOL_NAME = 'cinder-pool-name' + +POOL_ID = 'cinder-pool-id' ALL_HOSTS_RESP = [ { - "version": "d6a2d310d9adb16f0d24d5352b5c4837", - "id": "5e37d335-8fff-4aee-840a-34749301a16a", - "name": "victoria-fc", + "version": "c7216b2e14c8edc718e1664178f75777", + "id": HOST_ID_2, + "name": "cinder-host-2", "port_type": "fc", - "initiators": [ - "21:00:34:80:0d:6b:aa:e3", - "21:00:34:80:0d:6b:aa:e2" - ], - "tags": [], - "comment": "", - "auth": {} - } + "initiators": ["21:00:34:80:0d:74:17:30", "21:00:34:80:0d:74:17:31"], + }, + { + "version": "216d08e98f8d4a695b6632fc3c79b1cc", + "id": HOST_ID, + "name": "cinder-host-1", + "port_type": "fc", + "initiators": ['21:00:00:24:ff:7f:35:b7', '21:00:00:24:ff:7f:35:b6'], + }, + { + "version": "301fc82d355a691248b1e1dd8164f5e5", + "id": HOST_ID, + "name": "cinder-host-1", + "port_type": "iscsi", + "initiators": [HOST_IQN], + "auth": {"auth_type": "none"}, + }, + { + "version": "401fc82d355a691248b1e1dd8164f5e5", + "id": HOST_ID_2, + "name": "cinder-host-2", + "port_type": "iscsi", + "initiators": ["iqn.1994-05.com.redhat:5daf702e9655"], + "auth": {"auth_type": "none"}, + }, ] RES_MAPPING_RESP = [ @@ -75,19 +91,45 @@ RES_MAPPING_RESP = [ "resource_id": "62bbb941-ba4a-4101-927d-e527ce5ee011", "host_id": "5e37d335-8fff-4aee-840a-34749301a16a", "mapped_lun_id": 1 - } + }, + { + "resource_id": VOL_ID, + "host_id": HOST_ID, + "mapped_lun_id": LUN_ID + }, + { + "resource_id": "62bbb941-ba4a-4101-927d-e527ce5ee011", + "host_id": "5e37d335-8fff-4aee-840a-34749301a16a", + "mapped_lun_id": 1 + }, +] + +RES_MAPPING_RESP2 = [ + { + "resource_id": "62bbb941-ba4a-4101-927d-e527ce5ee011", + "host_id": "5e37d335-8fff-4aee-840a-34749301a16a", + "mapped_lun_id": 1 + }, + { + "resource_id": "62bbb941-ba4a-4101-927d-e527ce5ee011", + "host_id": "5e37d335-8fff-4aee-840a-34749301a16a", + "mapped_lun_id": 1 + }, ] POOL_LIST_RESPONCE = [ { - "id": "7e259486-deb8-4d11-8cb0-e2c5874aaa5e", - "name": "cinder-pool", + "id": POOL_ID, + "name": POOL_NAME, + "status": "ready" + }, + { + "id": "123", + "name": "some-name", "status": "ready" } ] -VOL_ID = 'cinder-volume-id' - ERROR_VOLUME = [ { "ptyId": "f28ee814-22ed-4bb0-8b6a-f7ce9075034a", @@ -132,7 +174,7 @@ RESOURCE_INFORMATION = { "lbaFormat": "4kn", "volume_id": "pty-vol-62bbb941-ba4a-4101-927d-e527ce5ee011", "wwid": "naa.614529011650000c4000800000000004", - "lun_id": "4", + "lun_id": LUN_ID, "cached": "true", "rCacheMode": "enabled", "wCacheMode": "enabled", @@ -152,7 +194,7 @@ RESOURCE_INFORMATION = { ], "lun": "scsi-lun-fc21-4", "volume": "pty-vol-62bbb941-ba4a-4101-927d-e527ce5ee011", - "lun_index": "4" + "lun_index": LUN_ID }, { "port": "fc20", @@ -169,7 +211,7 @@ RESOURCE_INFORMATION = { ], "lun": "scsi-lun-fc20-4", "volume": "pty-vol-62bbb941-ba4a-4101-927d-e527ce5ee011", - "lun_index": "4" + "lun_index": LUN_ID } ], "volume_path": "/dev/mapper/dmc-89382c6c-7cf9-4ff8-bdbb-f438d20c960a", @@ -179,13 +221,44 @@ RESOURCE_INFORMATION = { } } +VOL_PORTS_RESP = [ + { + "port": "p01", + "port_status": "healthy", + "running": ["sp-0", "sp-1"], + "wwn": ["iqn.2017-01.com.yadro:tatlin:sn.09082200a51002"], + "lun_index": LUN_ID, + }, + { + "port": "p11", + "port_status": "healthy", + "running": ["sp-0", "sp-1"], + "wwn": ["iqn.2017-01.com.yadro:tatlin:sn.09082200a51002"], + "lun_index": LUN_ID, + }, + { + "port": "p10", + "port_status": "healthy", + "running": ["sp-0", "sp-1"], + "wwn": ["iqn.2017-01.com.yadro:tatlin:sn.09082200a51002"], + "lun_index": LUN_ID, + }, + { + "port": "p00", + "port_status": "healthy", + "running": ["sp-0", "sp-1"], + "wwn": ["iqn.2017-01.com.yadro:tatlin:sn.09082200a51002"], + "lun_index": LUN_ID + }, +] + ALL_HOST_GROUP_RESP = [ { "version": "20c28d21549fb7ec5777637f72f50043", - "id": "314b5546-45da-4c8f-a24c-b615265fbc32", - "name": "cinder-group", + "id": HOST_GROUP_ID, + "name": HOST_GROUP_NAME, "host_ids": [ - "5e37d335-8fff-4aee-840a-34749301a16a" + HOST_ID, ], "tags": None, "comment": "" @@ -413,18 +486,34 @@ class TatlinClientTest(TestCase): def test_get_host_group_id_success(self, send_request): send_request.return_value = MockResponse( ALL_HOST_GROUP_RESP, codes.ok) - self.assertEqual(self.client.get_host_group_id('cinder-group'), - '314b5546-45da-4c8f-a24c-b615265fbc32') + self.assertEqual(self.client.get_host_group_id(HOST_GROUP_NAME), + HOST_GROUP_ID) - @mock.patch.object(TatlinClientCommon, - 'is_volume_exists', - return_value=True) @mock.patch.object(TatlinAccessAPI, 'send_request') - def test_get_resource_ports_array(self, send_request, *args): - send_request.return_value = MockResponse(RES_PORTS_RESP, codes.ok) + def test_get_volume_ports(self, send_request): + send_request.return_value = MockResponse( + VOL_PORTS_RESP, requests.codes.ok) + self.assertEqual(VOL_PORTS_RESP, self.client.get_volume_ports(VOL_ID)) - self.assertListEqual(self.client.get_resource_ports_array(VOL_ID), - ["fc20"]) + @mock.patch.object(TatlinAccessAPI, 'send_request') + def test_get_volume_ports_negative(self, send_request): + send_request.return_value = MockResponse( + {}, requests.codes.internal_server_error) + self.assertRaises(VolumeBackendAPIException, + self.client.get_volume_ports, + VOL_ID) + + @mock.patch.object(TatlinClientCommon, 'get_volume_ports') + def test_get_resource_ports_array_empty(self, vol_ports): + vol_ports.return_value = [] + self.assertListEqual([], self.client.get_resource_ports_array(VOL_ID)) + + @mock.patch.object(TatlinClientCommon, 'get_volume_ports') + def test_get_resource_ports_array(self, vol_ports): + vol_ports.return_value = VOL_PORTS_RESP + self.assertListEqual( + ['p00', 'p01', 'p10', 'p11'], + sorted(self.client.get_resource_ports_array(VOL_ID))) @mock.patch.object(TatlinAccessAPI, 'send_request') def test_get_resource_mapping_negative(self, send_request): @@ -436,8 +525,8 @@ class TatlinClientTest(TestCase): @mock.patch.object(TatlinAccessAPI, 'send_request') def test_get_pool_id_by_name(self, send_request, *args): send_request.return_value = MockResponse(POOL_LIST_RESPONCE, codes.ok) - self.assertEqual(self.client.get_pool_id_by_name('cinder-pool'), - '7e259486-deb8-4d11-8cb0-e2c5874aaa5e') + self.assertEqual(self.client.get_pool_id_by_name(POOL_NAME), + POOL_ID) @mock.patch.object(TatlinAccessAPI, 'send_request') def test_get_all_hosts(self, send_request): diff --git a/cinder/tests/unit/volume/drivers/yadro/test_tatlin_common.py b/cinder/tests/unit/volume/drivers/yadro/test_tatlin_common.py index d288412ac21..ae1c17de210 100644 --- a/cinder/tests/unit/volume/drivers/yadro/test_tatlin_common.py +++ b/cinder/tests/unit/volume/drivers/yadro/test_tatlin_common.py @@ -391,7 +391,7 @@ class TatlinCommonVolumeDriverTest(TestCase): @mock.patch.object(TatlinClientCommon, 'is_volume_ready', return_value=True) - def test_wait_volume_reay_success(self, is_ready): + def test_wait_volume_ready_success(self, is_ready): self.driver.wait_volume_ready(DummyVolume('cinder_volume')) @mock.patch.object(TatlinCommonVolumeDriver, '_update_qos') @@ -505,7 +505,6 @@ class TatlinCommonVolumeDriverTest(TestCase): '62bbb941-ba4a-4101-927d-e527ce5ee011', '') @mock.patch.object(TatlinCommonVolumeDriver, '_update_qos') - @mock.patch.object(TatlinCommonVolumeDriver, 'wait_volume_online') @mock.patch.object(TatlinClientCommon, 'add_vol_to_host') @mock.patch.object(TatlinClientCommon, 'is_volume_exists', diff --git a/cinder/tests/unit/volume/drivers/yadro/test_tatlin_fc.py b/cinder/tests/unit/volume/drivers/yadro/test_tatlin_fc.py new file mode 100644 index 00000000000..a4b6ac9a572 --- /dev/null +++ b/cinder/tests/unit/volume/drivers/yadro/test_tatlin_fc.py @@ -0,0 +1,366 @@ +# Copyright (C) 2021-2022 YADRO. +# All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import unittest +from unittest import mock + +from cinder import exception +from cinder.tests.unit.volume.drivers.yadro import test_tatlin_client as tc +from cinder.volume import configuration +from cinder.volume.drivers.yadro import tatlin_client +from cinder.volume.drivers.yadro import tatlin_common +from cinder.volume.drivers.yadro import tatlin_fc +from cinder.volume.drivers.yadro import tatlin_utils + + +FC_PORTS_RESP = [ + { + "id": "fc-sp-0-1000145290000320", + "meta": {"tatlin-node": "sp-0", "type": "fc", "port-type": "active"}, + "params": {"ifname": "fc40", "wwpn": "10:00:14:52:90:00:03:20"} + }, + { + "id": "fc-sp-0-1000145290000321", + "meta": {"tatlin-node": "sp-0", "type": "fc", "port-type": "active"}, + "params": {"ifname": "fc41", "wwpn": "10:00:14:52:90:00:03:21"} + }, + { + "id": "fc-sp-0-1000145290000310", + "meta": {"tatlin-node": "sp-0", "type": "fc", "port-type": "active"}, + "params": {"ifname": "fc20", "wwpn": "10:00:14:52:90:00:03:10"} + }, + { + "id": "fc-sp-0-1000145290000311", + "meta": {"tatlin-node": "sp-0", "type": "fc", "port-type": "active"}, + "params": {"ifname": "fc21", "wwpn": "10:00:14:52:90:00:03:11"} + }, + { + "id": "fc-sp-1-1000145290000390", + "meta": {"tatlin-node": "sp-1", "type": "fc", "port-type": "active"}, + "params": {"ifname": "fc20", "wwpn": "10:00:14:52:90:00:03:90"} + }, + { + "id": "fc-sp-1-1000145290000391", + "meta": {"tatlin-node": "sp-1", "type": "fc", "port-type": "active"}, + "params": {"ifname": "fc21", "wwpn": "10:00:14:52:90:00:03:91"} + }, + { + "id": "fc-sp-1-10001452900003a0", + "meta": {"tatlin-node": "sp-1", "type": "fc", "port-type": "active"}, + "params": {"ifname": "fc40", "wwpn": "10:00:14:52:90:00:03:a0"} + }, + { + "id": "fc-sp-1-10001452900003a1", + "meta": {"tatlin-node": "sp-1", "type": "fc", "port-type": "active"}, + "params": {"ifname": "fc41", "wwpn": "10:00:14:52:90:00:03:a1"} + }, +] + +FC_PORTS_PORTALS = { + 'fc21': ['10:00:14:52:90:00:03:11', '10:00:14:52:90:00:03:91'], + 'fc20': ['10:00:14:52:90:00:03:10', '10:00:14:52:90:00:03:90'], +} + +FC_TARGET_WWNS = [ + '1000145290000390', + '1000145290000311', + '1000145290000310', + '1000145290000391', +] + +FC_VOL_PORTS_RESP = [ + { + "port": "fc21", + "port_status": "healthy", + "running": ["sp-0", "sp-1"], + "wwn": ["10:00:14:52:90:00:03:11", "10:00:14:52:90:00:03:91"], + "lun_index": tc.LUN_ID, + }, + { + "port": "fc20", + "port_status": "healthy", + "running": ["sp-0", "sp-1"], + "wwn": ["10:00:14:52:90:00:03:10", "10:00:14:52:90:00:03:90"], + "lun_index": tc.LUN_ID, + }, + { + "port": "fc40", + "port_status": "healthy", + "running": ["sp-0", "sp-1"], + "wwn": ["10:00:14:52:90:00:03:09", "10:00:14:52:90:00:03:89"], + "lun_index": tc.LUN_ID, + }, +] + +HOST_WWNS = [ + '21000024ff7f35b7', + '21000024ff7f35b6', +] + +INITIATOR_TARGET_MAP = { + '21000024ff7f35b7': FC_TARGET_WWNS, + '21000024ff7f35b6': FC_TARGET_WWNS, +} + +FC_CONNECTOR = {'wwpns': HOST_WWNS, 'host': 'myhost'} + +FC_CONNECTOR_2 = {'wwpns': ['123', '456'], 'host': 'myhost'} + +VOLUME_DATA = { + 'discard': False, + 'target_discovered': True, + 'target_lun': tc.LUN_ID, + 'target_wwn': [ + '10:00:14:52:90:00:03:11', + '10:00:14:52:90:00:03:91', + '10:00:14:52:90:00:03:10', + '10:00:14:52:90:00:03:90', + ], + 'initiator_target_map': INITIATOR_TARGET_MAP, +} + + +def get_fake_tatlin_config(): + config = configuration.Configuration( + tatlin_common.tatlin_opts, + configuration.SHARED_CONF_GROUP) + config.san_ip = '127.0.0.1' + config.san_password = 'pwd' + config.san_login = 'admin' + config.pool_name = tc.POOL_NAME + config.host_group = 'cinder-group' + config.tat_api_retry_count = 1 + config.wait_interval = 1 + config.wait_retry_count = 3 + config.chap_username = 'chap_user' + config.chap_password = 'chap_passwd' + config.state_path = '/tmp' + config.export_ports = 'fc20,fc21' + return config + + +class TatlinFCVolumeDriverTest(unittest.TestCase): + @mock.patch.object(tatlin_utils.TatlinVolumeConnections, + 'create_store') + @mock.patch.object(tatlin_client.TatlinAccessAPI, + '_authenticate_access') + def setUp(self, auth_access, create_store): + access_api = tatlin_client.TatlinAccessAPI( + '127.0.0.1', '443', 'user', 'passwd', False) + access_api._authenticate_access = mock.MagicMock() + self.client = tatlin_client.TatlinClientCommon( + access_api, api_retry_count=1, wait_interval=1, wait_retry_count=1) + mock.patch.object(tatlin_client.TatlinAccessAPI, + '_authenticate_access') + self.driver = tatlin_fc.TatlinFCVolumeDriver( + configuration=get_fake_tatlin_config()) + self.driver._get_tatlin_client = mock.MagicMock() + self.driver._get_tatlin_client.return_value = self.client + self.driver.do_setup(None) + + @mock.patch.object(tatlin_fc.fczm_utils, 'add_fc_zone') + @mock.patch.object(tatlin_common.TatlinCommonVolumeDriver, + '_is_cinder_host_connection') + @mock.patch.object(tatlin_fc.TatlinFCVolumeDriver, + '_create_volume_data') + @mock.patch.object(tatlin_common.TatlinCommonVolumeDriver, + '_find_mapped_lun') + @mock.patch.object(tatlin_common.TatlinCommonVolumeDriver, + 'add_volume_to_host') + @mock.patch.object(tatlin_fc.TatlinFCVolumeDriver, + 'find_current_host') + def test_initialize_connection(self, + find_current_host, + add_volume_to_host, + find_mapped_lun, + create_volume_data, + is_cinder_connection, + add_fc_zone): + find_current_host.return_value = tc.HOST_ID + find_mapped_lun.return_value = tc.LUN_ID + is_cinder_connection.return_value = False + create_volume_data.return_value = VOLUME_DATA + volume = tc.DummyVolume(tc.VOL_ID) + connector = FC_CONNECTOR + data = self.driver.initialize_connection(volume, FC_CONNECTOR) + self.assertDictEqual( + data, + {'driver_volume_type': 'fibre_channel', 'data': VOLUME_DATA} + ) + find_current_host.assert_called_once() + add_volume_to_host.assert_called_once_with(volume, tc.HOST_ID) + is_cinder_connection.assert_called_once_with(connector) + create_volume_data.assert_called_once_with(volume, connector) + add_fc_zone.assert_called_once_with(data) + + @mock.patch.object(tatlin_fc.TatlinFCVolumeDriver, + '_create_volume_data') + @mock.patch.object(tatlin_common.TatlinCommonVolumeDriver, + 'add_volume_to_host') + @mock.patch.object(tatlin_common.TatlinCommonVolumeDriver, + '_find_mapped_lun') + @mock.patch.object(tatlin_fc.TatlinFCVolumeDriver, + 'find_current_host') + @mock.patch.object(tatlin_utils.TatlinVolumeConnections, + 'increment') + @mock.patch.object(tatlin_common.TatlinCommonVolumeDriver, + '_is_cinder_host_connection') + def test_initialize_connection_cinder_attachement(self, + is_cinder_connection, + increment, *args): + is_cinder_connection.return_value = True + volume = tc.DummyVolume(tc.VOL_ID) + self.driver.initialize_connection(volume, FC_CONNECTOR) + is_cinder_connection.assert_called_once_with(FC_CONNECTOR) + increment.assert_called_once_with(tc.VOL_ID) + + @mock.patch.object(tatlin_client.TatlinClientCommon, + 'get_port_portal') + def test_get_ports_portals(self, get_port_portal): + get_port_portal.return_value = FC_PORTS_RESP + pp = self.driver._get_ports_portals() + self.assertDictEqual(pp, FC_PORTS_PORTALS) + + @mock.patch.object(tatlin_client.TatlinClientCommon, + 'get_all_hosts') + def test_find_current_host(self, get_all_hosts): + get_all_hosts.return_value = tc.ALL_HOSTS_RESP + host_id = self.driver.find_current_host(FC_CONNECTOR) + self.assertEqual(host_id, tc.HOST_ID) + + @mock.patch.object(tatlin_client.TatlinClientCommon, + 'get_all_hosts') + def test_find_current_host_not_found(self, + get_all_hosts): + get_all_hosts.return_value = tc.ALL_HOSTS_RESP + self.assertRaises(exception.VolumeBackendAPIException, + self.driver.find_current_host, FC_CONNECTOR_2) + + @mock.patch.object(tatlin_fc.TatlinFCVolumeDriver, + '_build_initiator_target_map') + @mock.patch.object(tatlin_fc.TatlinFCVolumeDriver, + '_get_ports_portals') + @mock.patch.object(tatlin_client.TatlinClientCommon, + 'get_volume_ports') + @mock.patch.object(tatlin_common.TatlinCommonVolumeDriver, + '_find_mapped_lun') + def test_create_volume_data(self, + find_lun, + volume_ports, + ports_portals, + build_map): + find_lun.return_value = tc.LUN_ID + volume_ports.return_value = FC_VOL_PORTS_RESP + ports_portals.return_value = FC_PORTS_PORTALS + build_map.return_value = INITIATOR_TARGET_MAP + volume = tc.DummyVolume(tc.VOL_ID) + connector = FC_CONNECTOR + data = self.driver._create_volume_data(volume, connector) + self.assertEqual(data['target_lun'], tc.LUN_ID) + self.assertEqual(sorted(data['target_wwn']), sorted(FC_TARGET_WWNS)) + self.assertDictEqual(data['initiator_target_map'], + INITIATOR_TARGET_MAP) + + @mock.patch.object(tatlin_fc.fczm_utils, 'remove_fc_zone') + @mock.patch.object(tatlin_client.TatlinClientCommon, + 'get_resource_mapping') + @mock.patch.object(tatlin_fc.TatlinFCVolumeDriver, + '_create_volume_data') + @mock.patch.object(tatlin_fc.TatlinFCVolumeDriver, + 'find_current_host') + @mock.patch.object(tatlin_common.TatlinCommonVolumeDriver, + '_is_cinder_host_connection') + @mock.patch.object(tatlin_common.TatlinCommonVolumeDriver, + 'remove_volume_from_host') + def test_terminate_connection(self, + remove_host, + is_cinder, + find_host, + create_data, + resource_mapping, + remove_fc_zone): + is_cinder.return_value = True + find_host.return_value = tc.HOST_ID + resource_mapping.return_value = tc.RES_MAPPING_RESP + volume = tc.DummyVolume(tc.VOL_ID) + connector = FC_CONNECTOR + self.driver.terminate_connection(volume, connector) + remove_host.assert_called_once_with(volume, tc.HOST_ID) + remove_fc_zone.assert_not_called() + + @mock.patch.object(tatlin_fc.fczm_utils, 'remove_fc_zone') + @mock.patch.object(tatlin_client.TatlinClientCommon, + 'get_resource_mapping') + @mock.patch.object(tatlin_fc.TatlinFCVolumeDriver, + '_create_volume_data') + @mock.patch.object(tatlin_fc.TatlinFCVolumeDriver, + 'find_current_host') + @mock.patch.object(tatlin_common.TatlinCommonVolumeDriver, + '_is_cinder_host_connection') + @mock.patch.object(tatlin_common.TatlinCommonVolumeDriver, + 'remove_volume_from_host') + def test_terminate_connection_with_zone_removal(self, + remove_host, + is_cinder, + find_host, + create_data, + resource_mapping, + remove_fc_zone): + is_cinder.return_value = True + find_host.return_value = tc.HOST_ID_2 + resource_mapping.side_effect = [ + tc.RES_MAPPING_RESP, + tc.RES_MAPPING_RESP2, + ] + create_data.return_value = VOLUME_DATA + volume = tc.DummyVolume(tc.VOL_ID) + connector = FC_CONNECTOR + self.driver.terminate_connection(volume, connector) + remove_host.assert_called_once_with(volume, tc.HOST_ID_2) + remove_fc_zone.assert_called_once_with({ + 'driver_volume_type': 'fibre_channel', + 'data': VOLUME_DATA, + }) + + def test_build_initiator_target_map(self): + self.driver._lookup_service = None + connector = FC_CONNECTOR + targets = FC_TARGET_WWNS + itmap = self.driver._build_initiator_target_map(targets, connector) + self.assertListEqual(sorted(itmap.keys()), + sorted(INITIATOR_TARGET_MAP.keys())) + for initiator in itmap: + self.assertListEqual(sorted(itmap[initiator]), + sorted(INITIATOR_TARGET_MAP[initiator])) + + def test_build_initiator_target_map_with_lookup(self): + lookup_service = mock.MagicMock() + lookup_service.get_device_mapping_from_network.return_value = { + 'san-1': { + 'initiator_port_wwn_list': HOST_WWNS, + 'target_port_wwn_list': FC_TARGET_WWNS, + }, + } + self.driver._lookup_service = lookup_service + connector = FC_CONNECTOR + targets = FC_TARGET_WWNS + itmap = self.driver._build_initiator_target_map(targets, connector) + self.assertListEqual(sorted(itmap.keys()), + sorted(INITIATOR_TARGET_MAP.keys())) + for initiator in itmap: + self.assertListEqual(sorted(itmap[initiator]), + sorted(INITIATOR_TARGET_MAP[initiator])) + lookup_service.get_device_mapping_from_network.assert_called_once_with( + connector['wwpns'], targets) diff --git a/cinder/tests/unit/volume/drivers/yadro/test_tatlin_iscsi.py b/cinder/tests/unit/volume/drivers/yadro/test_tatlin_iscsi.py index 2bf0fb8f56e..cd941e8e102 100644 --- a/cinder/tests/unit/volume/drivers/yadro/test_tatlin_iscsi.py +++ b/cinder/tests/unit/volume/drivers/yadro/test_tatlin_iscsi.py @@ -312,7 +312,7 @@ class TatlinISCSIVolumeDriverTest(TestCase): (MockResponse(ISCSI_HOST_INFO, 200)), ] self.assertEqual(self.driver.find_current_host( - 'iqn.1994-05.com.redhat:4e5d7ab85a4c'), + {'initiator': 'iqn.1994-05.com.redhat:4e5d7ab85a4c'}), '5e37d335-8fff-4aee-840a-34749301a16a') @mock.patch.object(TatlinAccessAPI, 'send_request') diff --git a/cinder/volume/drivers/yadro/tatlin_common.py b/cinder/volume/drivers/yadro/tatlin_common.py index 80c815d65eb..ac45cdf20f3 100644 --- a/cinder/volume/drivers/yadro/tatlin_common.py +++ b/cinder/volume/drivers/yadro/tatlin_common.py @@ -35,6 +35,7 @@ from cinder.volume import qos_specs from cinder.volume import volume_types from cinder.volume import volume_utils from cinder.volume.volume_utils import brick_get_connector_properties +from cinder.zonemanager import utils as fczm_utils LOG = logging.getLogger(__name__) @@ -372,32 +373,50 @@ class TatlinCommonVolumeDriver(driver.VolumeDriver, object): ignore_errors=ignore_errors) _do_detach_volume() + @volume_utils.trace + def initialize_connection(self, volume, connector): + @utils.synchronized("tatlin-volume-connections-%s" % volume.name_id) + def _initialize_connection(): + LOG.debug('Init %s with connector %s', volume.name_id, connector) + current_host = self.find_current_host(connector) + self.add_volume_to_host(volume, current_host) + if self._is_cinder_host_connection(connector): + self._connections.increment(volume.name_id) + connection_info = self._create_connection_info(volume, connector) + fczm_utils.add_fc_zone(connection_info) + return connection_info + return _initialize_connection() + @volume_utils.trace def terminate_connection(self, volume, connector, **kwargs): @utils.synchronized("tatlin-volume-connections-%s" % volume.name_id) def _terminate_connection(): LOG.debug('Terminate connection for %s with connector %s', volume.name_id, connector) + connection_info = self._create_connection_info(volume, connector) if not connector: - return + self.remove_volume_from_all_hosts(volume) + return connection_info if self._is_cinder_host_connection(connector): connections = self._connections.decrement(volume.name_id) if connections > 0: LOG.debug('Not terminating connection: ' 'volume %s, existing connections: %s', volume.name_id, connections) - return + return connection_info hostname = connector['host'] if self._is_nova_multiattached(volume, hostname): LOG.debug('Volume %s is attached on host %s to multiple VMs.' ' Not terminating connection', volume.name_id, hostname) - return - - host = self.find_current_host(connector['initiator']) - LOG.debug('Terminate connection volume %s removing from host %s', - volume.name_id, host) - self.remove_volume_from_host(volume, host) + return connection_info + host_id = self.find_current_host(connector) + self.remove_volume_from_host(volume, host_id) + resources = [r for r in self.tatlin_api.get_resource_mapping() + if r.get('host_id', '') == host_id] + if not resources: + fczm_utils.remove_fc_zone(connection_info) + return connection_info _terminate_connection() def _is_cinder_host_connection(self, connector): @@ -673,6 +692,13 @@ class TatlinCommonVolumeDriver(driver.VolumeDriver, object): def remove_volume_from_host(self, volume, host_id): self.tatlin_api.remove_vol_from_host(volume.name_id, host_id) + def remove_volume_from_all_hosts(self, volume): + mappings = self.tatlin_api.get_resource_mapping() + hosts = [m['host_id'] for m in mappings + if 'resource_id' in m and m['resource_id'] == volume.name_id] + for host_id in hosts: + self.tatlin_api.remove_vol_from_host(volume.name_id, host_id) + def _is_port_assigned(self, volume_id, port): LOG.debug('VOLUME %s: Checking port %s ', volume_id, port) cur_ports = self.tatlin_api.get_resource_ports_array(volume_id) @@ -684,20 +710,19 @@ class TatlinCommonVolumeDriver(driver.VolumeDriver, object): def _get_ports_portals(self): return {} - def _find_mapped_lun(self, volume_id, iqn): - host_id = self.find_current_host(iqn) + def _create_connection_info(self, volume, connector): + return {} + + def _find_mapped_lun(self, volume_id, connector): + host_id = self.find_current_host(connector) result = self.tatlin_api.get_resource_mapping() for r in result: if 'host_id' in r: if r['resource_id'] == volume_id and r['host_id'] == host_id: - LOG.debug('Current mapped lun record %s volume_id: %s ' - 'host_id: is %s', r, volume_id, host_id) return r['mapped_lun_id'] - mess = (_('Unable to get mapped lun for volume %s on host %s') % (volume_id, host_id)) LOG.error(mess) - raise exception.VolumeBackendAPIException(message=mess) @staticmethod @@ -727,7 +752,7 @@ class TatlinCommonVolumeDriver(driver.VolumeDriver, object): wait_interval=self._wait_interval, wait_retry_count=self._wait_retry_count) - def find_current_host(self, wwn): + def find_current_host(self, connector): return '' @property diff --git a/cinder/volume/drivers/yadro/tatlin_fc.py b/cinder/volume/drivers/yadro/tatlin_fc.py new file mode 100644 index 00000000000..f80d6081f60 --- /dev/null +++ b/cinder/volume/drivers/yadro/tatlin_fc.py @@ -0,0 +1,124 @@ +# Copyright, 2023, KNS Group LLC (YADRO) +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log as logging + +from cinder.common import constants +from cinder import exception +from cinder.i18n import _ +from cinder import interface +from cinder.volume import driver +from cinder.volume.drivers.yadro import tatlin_common +from cinder.zonemanager import utils as fczm_utils + +LOG = logging.getLogger(__name__) + + +@interface.volumedriver +class TatlinFCVolumeDriver(tatlin_common.TatlinCommonVolumeDriver, + driver.FibreChannelDriver): + """ACCESS Tatlin FC Driver. + + Executes commands relating to FC. + Supports creation of volumes. + + .. code-block:: none + + API version history: + + 1.0 - Initial version. + """ + + VERSION = '1.0' + + SUPPORTS_ACTIVE_ACTIVE = True + + # ThirdPartySystems wiki + CI_WIKI_NAME = "Yadro_Tatlin_Unified_CI" + + def __init__(self, *args, **kwargs): + super(TatlinFCVolumeDriver, self).__init__(*args, **kwargs) + self.backend_name = self.configuration.safe_get( + 'volume_backend_name') or 'TatlinFC' + self.DRIVER_VOLUME_TYPE = constants.FC + self._lookup_service = fczm_utils.create_lookup_service() + + def _create_connection_info(self, volume, connector): + info = { + 'driver_volume_type': constants.FC_VARIANT_1, + 'data': self._create_volume_data(volume, connector) + } + return info + + def _get_ports_portals(self): + result = self.tatlin_api.get_port_portal("fc") + ports = {} + for p in result: + iface = p['params']['ifname'] + if self._export_ports and iface not in self._export_ports: + continue + ports.setdefault(iface, []).append(p['params']['wwpn']) + return ports + + def _create_volume_data(self, volume, connector): + if connector is None: + return {} + lun_id = self._find_mapped_lun(volume.name_id, connector) + volume_ports = self.tatlin_api.get_volume_ports(volume.name_id) + ports_portals = self._get_ports_portals() + data = { + 'target_discovered': True, + 'target_lun': lun_id, + 'discard': False, + } + target_wwns = [] + for port in volume_ports: + wwpns = ports_portals.get(port['port']) + if not wwpns: + continue + target_wwns += [w.replace(':', '') for w in wwpns] + + data['target_wwn'] = target_wwns + data['initiator_target_map'] = self._build_initiator_target_map( + target_wwns, connector) + return data + + def find_current_host(self, connector): + wwns = connector['wwpns'] + LOG.debug('Try to find host id for %s', wwns) + result = self.tatlin_api.get_all_hosts() + for h in result: + for wwn in h['initiators']: + if wwn.replace(':', '') in wwns: + LOG.debug('Current host is %s', h['id']) + return h['id'] + message = _('Unable to get host information for wwns: %s') % str(wwns) + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) + + def _build_initiator_target_map(self, target_wwns, connector): + result = {} + + if self._lookup_service: + mapping = self._lookup_service.get_device_mapping_from_network( + connector['wwpns'], target_wwns) + for fabric in mapping.values(): + for initiator in fabric['initiator_port_wwn_list']: + result.setdefault(initiator, []).extend( + fabric['target_port_wwn_list']) + result = {i: list(set(t)) for i, t in result.items()} + else: + result = dict.fromkeys(connector['wwpns'], target_wwns) + + return result diff --git a/cinder/volume/drivers/yadro/tatlin_iscsi.py b/cinder/volume/drivers/yadro/tatlin_iscsi.py index a8d29e4b247..5b57c24f1ed 100644 --- a/cinder/volume/drivers/yadro/tatlin_iscsi.py +++ b/cinder/volume/drivers/yadro/tatlin_iscsi.py @@ -18,11 +18,9 @@ from oslo_log import log as logging from cinder import exception from cinder.i18n import _ from cinder import interface -from cinder import utils from cinder.volume import driver from cinder.volume.drivers.yadro.tatlin_common import TatlinCommonVolumeDriver from cinder.volume.drivers.yadro.tatlin_exception import TatlinAPIException -from cinder.volume import volume_utils LOG = logging.getLogger(__name__) @@ -39,9 +37,10 @@ class TatlinISCSIVolumeDriver(TatlinCommonVolumeDriver, driver.ISCSIDriver): API version history: 1.0 - Initial version. + 1.1 - Common code sharing with FC driver """ - VERSION = "1.0" + VERSION = "1.1" SUPPORTS_ACTIVE_ACTIVE = True @@ -56,31 +55,12 @@ class TatlinISCSIVolumeDriver(TatlinCommonVolumeDriver, driver.ISCSIDriver): 'volume_backend_name') or 'TatlinISCSI') self.DRIVER_VOLUME_TYPE = 'iSCSI' - @volume_utils.trace - def initialize_connection(self, volume, connector): - @utils.synchronized("tatlin-volume-connections-%s" % volume.name_id) - def _initialize_connection(): - LOG.debug('Init %s with connector %s', volume.name_id, connector) - eth_ports = self._get_ports_portals() - current_host = self.find_current_host(connector['initiator']) - self.add_volume_to_host(volume, current_host) - mapped_lun = self._find_mapped_lun( - volume.name_id, connector['initiator']) - port_result = self.tatlin_api.get_volume_ports(volume.name_id) - - result = { - 'driver_volume_type': 'iscsi', - 'data': self._create_volume_data(port_result, eth_ports, - mapped_lun) - } - - if self._is_cinder_host_connection(connector): - self._connections.increment(volume.name_id) - - LOG.debug('Current connection info %s', result) - return result - - return _initialize_connection() + def _create_connection_info(self, volume, connector): + info = { + 'driver_volume_type': 'iscsi', + 'data': self._create_volume_data(volume, connector) + } + return info def _get_ports_portals(self): try: @@ -105,70 +85,56 @@ class TatlinISCSIVolumeDriver(TatlinCommonVolumeDriver, driver.ISCSIDriver): return ports - def _create_volume_data(self, port_inf, eth_ports, lun_id): + def _create_volume_data(self, volume, connector): + if connector is None: + return {} + eth_ports = self._get_ports_portals() + lun_id = self._find_mapped_lun(volume.name_id, connector) + vol_ports = self.tatlin_api.get_volume_ports(volume.name_id) res = {'target_discovered': True, 'target_lun': lun_id} - - tatlin_version = self.tatlin_api.get_tatlin_version() - - if tatlin_version > (2, 3): - if self._auth_method == 'CHAP': - res['auth_method'] = 'CHAP' - res['auth_username'] = self._chap_username - res['auth_password'] = self._chap_password - else: - cred = self.tatlin_api.get_iscsi_cred() + if self._auth_method == 'CHAP': res['auth_method'] = 'CHAP' - res['auth_username'] = cred['userid'] - res['auth_password'] = cred['password'] - + res['auth_username'] = self._chap_username + res['auth_password'] = self._chap_password target_luns = [] target_iqns = [] target_portals = [] - LOG.debug('Port data: %s', port_inf) - for port in port_inf: + for port in vol_ports: if port['port'] not in eth_ports.keys(): continue - ips = eth_ports[port['port']] target_portals += ips - luns = [lun_id for _ in ips] target_luns += luns - if 'running' in port: target_iqns += port['wwn'] * len(port['running']) else: target_iqns += port['wwn'] - if not target_portals or not target_iqns or not target_luns: message = (_('Not enough connection data, ' 'luns: %s, portals: %s, iqns: %s') % target_luns, target_portals, target_iqns) LOG.error(message) raise exception.VolumeBackendAPIException(message=message) - res['target_lun'] = target_luns[0] res['target_luns'] = target_luns res['target_iqn'] = target_iqns[0] res['target_iqns'] = target_iqns res['target_portal'] = target_portals[0] res['target_portals'] = target_portals - LOG.debug("Volume data = %s", res) return res - def find_current_host(self, wwn): - LOG.debug('Try to find host id for %s', wwn) - + def find_current_host(self, connector): + iqn = connector['initiator'] + LOG.debug('Try to find host id for %s', iqn) gr_id = self.tatlin_api.get_host_group_id(self._host_group) - group_info = self.tatlin_api.get_host_group_info(gr_id) LOG.debug('Group info for %s is %s', self._host_group, group_info) for host_id in group_info['host_ids']: - if wwn in self.tatlin_api.get_host_info(host_id)['initiators']: - LOG.debug('Found host %s for initiator %s', host_id, wwn) + if iqn in self.tatlin_api.get_host_info(host_id)['initiators']: + LOG.debug('Found host %s for initiator %s', host_id, iqn) return host_id - - mess = _('Unable to find host for initiator %s' % wwn) - LOG.error(mess) - raise exception.VolumeBackendAPIException(message=mess) + message = _('Unable to find host for initiator %s' % iqn) + LOG.error(message) + raise exception.VolumeBackendAPIException(message=message) diff --git a/doc/source/configuration/block-storage/drivers/yadro-tatlin-volume-driver.rst b/doc/source/configuration/block-storage/drivers/yadro-tatlin-volume-driver.rst index 087da75c701..d442d69f767 100644 --- a/doc/source/configuration/block-storage/drivers/yadro-tatlin-volume-driver.rst +++ b/doc/source/configuration/block-storage/drivers/yadro-tatlin-volume-driver.rst @@ -2,7 +2,7 @@ YADRO Cinder Driver ============================ -YADRO Cinder driver provides iSCSI support for +YADRO Cinder driver provides iSCSI and FC support for TATLIN.UNIFIED storages. @@ -49,7 +49,7 @@ details about each setting, see the user's guide of the storage system. #. Ports - Setup data ETH ports you want to export volumes to. + Setup Ethernet or FC ports you want to export volumes to. #. Hosts @@ -87,6 +87,22 @@ Add the following configuration to ``/etc/cinder/cinder.conf``: chap_username= chap_password= +or + +.. code-block:: ini + + [fc-1] + volume_driver=cinder.volume.drivers.yadro.tatlin_fc.TatlinFCVolumeDriver + san_ip= + san_login= + san_password= + tat_api_retry_count= + api_port= + pool_name= + export_ports=, + host_group= + max_resource_count= + ``volume_driver`` Volume driver name. diff --git a/doc/source/reference/support-matrix.ini b/doc/source/reference/support-matrix.ini index dc4da1bcbff..cd578ffce89 100644 --- a/doc/source/reference/support-matrix.ini +++ b/doc/source/reference/support-matrix.ini @@ -223,7 +223,7 @@ title=Windows iSCSI Driver title=Windows SMB Driver [driver.yadro] -title=Yadro Tatlin Unified Driver (iSCSI) +title=Yadro Tatlin Unified Driver (iSCSI, FC) [driver.zadara] title=Zadara Storage Driver (iSCSI, NFS) diff --git a/releasenotes/notes/bp-yadro-tatlin-unified-fc-b6e1225ad99c6304.yaml b/releasenotes/notes/bp-yadro-tatlin-unified-fc-b6e1225ad99c6304.yaml new file mode 100644 index 00000000000..2a8d3c694d2 --- /dev/null +++ b/releasenotes/notes/bp-yadro-tatlin-unified-fc-b6e1225ad99c6304.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Yadro Tatlin Unified: Added initial version of the FC driver.