Yadro Tatlin Unified FC driver

Added support of FC to Tatlin Unified driver

Implements: blueprint yadro-tatlin-unified-fc
Co-Authored-By: Sergey Karpenkov <s.karpenkov@yadro.com>

Change-Id: Ia8192854a7095354f2616d53d2c5b71580384dcb
This commit is contained in:
Vladislav Belogrudov 2023-07-06 12:54:45 +03:00
parent 25455c476f
commit 3f3112f796
10 changed files with 721 additions and 132 deletions

View File

@ -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):

View File

@ -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',

View File

@ -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)

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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_username>
chap_password=<chap_password>
or
.. code-block:: ini
[fc-1]
volume_driver=cinder.volume.drivers.yadro.tatlin_fc.TatlinFCVolumeDriver
san_ip=<management_ip>
san_login=<login>
san_password=<password>
tat_api_retry_count=<count>
api_port=<management_port>
pool_name=<cinder_volumes_pool>
export_ports=<port1>,<port2>
host_group=<name>
max_resource_count=<count>
``volume_driver``
Volume driver name.

View File

@ -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)

View File

@ -0,0 +1,4 @@
---
features:
- |
Yadro Tatlin Unified: Added initial version of the FC driver.