Browse Source

Merge "FC Allow for multipath volumes with different LUNs" into stable/queens

changes/32/607632/1 2.3.4
Zuul 3 years ago
committed by Gerrit Code Review
parent
commit
d4a600748b
  1. 109
      os_brick/initiator/connectors/fibre_channel.py
  2. 66
      os_brick/initiator/linuxfc.py
  3. 185
      os_brick/tests/initiator/connectors/test_fibre_channel.py
  4. 87
      os_brick/tests/initiator/test_linuxfc.py

109
os_brick/initiator/connectors/fibre_channel.py

@ -20,6 +20,7 @@ from oslo_service import loopingcall
import six
from os_brick import exception
from os_brick.i18n import _
from os_brick import initiator
from os_brick.initiator.connectors import base
from os_brick.initiator import linuxfc
@ -70,12 +71,70 @@ class FibreChannelConnector(base.BaseLinuxConnector):
"""Where do we look for FC based volumes."""
return '/dev/disk/by-path'
def _get_possible_volume_paths(self, connection_properties, hbas):
ports = connection_properties['target_wwn']
possible_devs = self._get_possible_devices(hbas, ports)
def _add_targets_to_connection_properties(self, connection_properties):
target_wwn = connection_properties.get('target_wwn')
target_wwns = connection_properties.get('target_wwns')
if target_wwns:
wwns = target_wwns
elif isinstance(target_wwn, list):
wwns = target_wwn
elif isinstance(target_wwn, six.string_types):
wwns = [target_wwn]
else:
wwns = []
target_lun = connection_properties.get('target_lun', 0)
target_luns = connection_properties.get('target_luns')
if target_luns:
luns = target_luns
elif isinstance(target_lun, int):
luns = [target_lun]
else:
luns = []
if len(luns) == len(wwns):
# Handles single wwwn + lun or multiple, potentially
# different wwns or luns
targets = list(zip(wwns, luns))
elif len(luns) == 1 and len(wwns) > 1:
# For the case of multiple wwns, but a single lun (old path)
targets = []
for wwn in wwns:
targets.append((wwn, luns[0]))
else:
# Something is wrong, this shouldn't happen
msg = _("Unable to find potential volume paths for FC device "
"with lun: %(lun)s and wwn: %(wwn)s") % {
"lun": target_lun, "wwn": target_wwn}
LOG.error(msg)
raise exception.VolumePathsNotFound(msg)
connection_properties['targets'] = targets
wwpn_lun_map = dict()
for wwpn, lun in targets:
wwpn_lun_map[wwpn] = lun
# If there is an initiator_target_map we can update it too
if 'initiator_target_map' in connection_properties:
itmap = connection_properties['initiator_target_map']
new_itmap = dict()
for init_wwpn in itmap:
target_wwpns = itmap[init_wwpn]
init_targets = []
for target_wwpn in target_wwpns:
if target_wwpn in wwpn_lun_map:
init_targets.append((target_wwpn,
wwpn_lun_map[target_wwpn]))
new_itmap[init_wwpn] = init_targets
connection_properties['initiator_target_lun_map'] = new_itmap
return connection_properties
lun = connection_properties.get('target_lun', 0)
host_paths = self._get_host_devices(possible_devs, lun)
def _get_possible_volume_paths(self, connection_properties, hbas):
targets = connection_properties['targets']
possible_devs = self._get_possible_devices(hbas, targets)
host_paths = self._get_host_devices(possible_devs)
return host_paths
def get_volume_paths(self, connection_properties):
@ -100,6 +159,9 @@ class FibreChannelConnector(base.BaseLinuxConnector):
Try and update the local kernel's size information
for an FC volume.
"""
connection_properties = self._add_targets_to_connection_properties(
connection_properties)
volume_paths = self.get_volume_paths(connection_properties)
if volume_paths:
return self._linuxscsi.extend_volume(volume_paths)
@ -126,6 +188,9 @@ class FibreChannelConnector(base.BaseLinuxConnector):
LOG.debug("execute = %s", self._execute)
device_info = {'type': 'block'}
connection_properties = self._add_targets_to_connection_properties(
connection_properties)
hbas = self._linuxfc.get_fc_hbas_info()
host_devices = self._get_possible_volume_paths(
connection_properties, hbas)
@ -194,9 +259,14 @@ class FibreChannelConnector(base.BaseLinuxConnector):
LOG.debug("connect_volume returning %s", device_info)
return device_info
def _get_host_devices(self, possible_devs, lun):
def _get_host_devices(self, possible_devs):
"""Compute the device paths on the system with an id, wwn, and lun
:param possible_devs: list of (pci_id, wwn, lun) tuples
:return: list of device paths on the system based on the possible_devs
"""
host_devices = []
for pci_num, target_wwn in possible_devs:
for pci_num, target_wwn, lun in possible_devs:
host_device = "/dev/disk/by-path/pci-%s-fc-%s-lun-%s" % (
pci_num,
target_wwn,
@ -204,14 +274,13 @@ class FibreChannelConnector(base.BaseLinuxConnector):
host_devices.append(host_device)
return host_devices
def _get_possible_devices(self, hbas, wwnports):
def _get_possible_devices(self, hbas, targets):
"""Compute the possible fibre channel device options.
:param hbas: available hba devices.
:param wwnports: possible wwn addresses. Can either be string
or list of strings.
:param targets: tuple of possible wwn addresses and lun combinations.
:returns: list of (pci_id, wwn) tuples
:returns: list of (pci_id, wwn, lun) tuples
Given one or more wwn (mac addresses for fibre channel) ports
do the matrix math to figure out a set of pci device, wwn
@ -219,23 +288,13 @@ class FibreChannelConnector(base.BaseLinuxConnector):
provides a search space for the device connection.
"""
# the wwn (think mac addresses for fiber channel devices) can
# either be a single value or a list. Normalize it to a list
# for further operations.
wwns = []
if isinstance(wwnports, list):
for wwn in wwnports:
wwns.append(str(wwn))
elif isinstance(wwnports, six.string_types):
wwns.append(str(wwnports))
raw_devices = []
for hba in hbas:
pci_num = self._get_pci_num(hba)
if pci_num is not None:
for wwn in wwns:
for wwn, lun in targets:
target_wwn = "0x%s" % wwn.lower()
raw_devices.append((pci_num, target_wwn))
raw_devices.append((pci_num, target_wwn, lun))
return raw_devices
@utils.trace
@ -257,6 +316,10 @@ class FibreChannelConnector(base.BaseLinuxConnector):
devices = []
wwn = None
connection_properties = self._add_targets_to_connection_properties(
connection_properties)
volume_paths = self.get_volume_paths(connection_properties)
mpath_path = None
for path in volume_paths:

66
os_brick/initiator/linuxfc.py

@ -19,7 +19,6 @@ import os
from oslo_concurrency import processutils as putils
from oslo_log import log as logging
import six
from os_brick.initiator import linuxscsi
@ -42,20 +41,18 @@ class LinuxFibreChannel(linuxscsi.LinuxSCSI):
single WWNN for all ports, so caller should expect us to return either
explicit channel and targets or wild cards if we cannot determine them.
:returns: List of lists with [c, t] entries, the channel and target
The connection properties will need to have "target" values defined in
it which are expected to be tuples of (wwpn, lun).
:returns: List of lists with [c, t, l] entries, the channel and target
may be '-' wildcards if unable to determine them.
"""
# We want the target's WWPNs, so we use the initiator_target_map if
# present for this hba or default to target_wwns if not present.
wwpns = conn_props['target_wwn']
targets = conn_props['targets']
if 'initiator_target_map' in conn_props:
wwpns = conn_props['initiator_target_map'].get(hba['port_name'],
wwpns)
# If it's not a string then it's an iterable (most likely a list),
# so we need to create a BRE for the grep query.
if not isinstance(wwpns, six.string_types):
wwpns = '\|'.join(wwpns)
targets = conn_props['initiator_target_lun_map'].get(
hba['port_name'], targets)
# Leave only the number from the host_device field (ie: host6)
host_device = hba['host_device']
@ -63,33 +60,36 @@ class LinuxFibreChannel(linuxscsi.LinuxSCSI):
host_device = host_device[4:]
path = '/sys/class/fc_transport/target%s:' % host_device
# Since we'll run the command in a shell ensure BRE are being used
cmd = 'grep -Gil "%(wwpns)s" %(path)s*/port_name' % {'wwpns': wwpns,
'path': path}
try:
# We need to run command in shell to expand the * glob
out, _err = self._execute(cmd, shell=True)
return [line.split('/')[4].split(':')[1:]
for line in out.split('\n') if line.startswith(path)]
except Exception as exc:
LOG.debug('Could not get HBA channel and SCSI target ID, path: '
'%(path)s*, reason: %(reason)s', {'path': path,
'reason': exc})
return [['-', '-']]
ctls = []
for wwpn, lun in targets:
cmd = 'grep -Gil "%(wwpns)s" %(path)s*/port_name' % {'wwpns': wwpn,
'path': path}
try:
# We need to run command in shell to expand the * glob
out, _err = self._execute(cmd, shell=True)
ctls += [line.split('/')[4].split(':')[1:] + [lun]
for line in out.split('\n') if line.startswith(path)]
except Exception as exc:
LOG.debug('Could not get HBA channel and SCSI target ID, path:'
' %(path)s*, reason: %(reason)s', {'path': path,
'reason': exc})
# If we didn't find any paths just give back wildcards for
# the channel and target ids.
ctls.append(['-', '-', lun])
return ctls
def rescan_hosts(self, hbas, connection_properties):
LOG.debug('Rescaning HBAs %(hbas)s with connection properties '
'%(conn_props)s', {'hbas': hbas,
'conn_props': connection_properties})
target_lun = connection_properties['target_lun']
get_cts = self._get_hba_channel_scsi_target
get_ctsl = self._get_hba_channel_scsi_target
# Use initiator_target_map provided by backend as HBA exclussion map
ports = connection_properties.get('initiator_target_map')
# Use initiator_target_map provided by backend as HBA exclusion map
ports = connection_properties.get('initiator_target_lun_map')
if ports:
hbas = [hba for hba in hbas if hba['port_name'] in ports]
LOG.debug('Using initiator target map to exclude HBAs')
process = [(hba, get_cts(hba, connection_properties))
process = [(hba, get_ctsl(hba, connection_properties))
for hba in hbas]
# With no target map we'll check if target implements single WWNN for
@ -100,20 +100,20 @@ class LinuxFibreChannel(linuxscsi.LinuxSCSI):
no_info = []
for hba in hbas:
cts = get_cts(hba, connection_properties)
ctls = get_ctsl(hba, connection_properties)
found_info = True
for hba_channel, target_id in cts:
for hba_channel, target_id, target_lun in ctls:
if hba_channel == '-' or target_id == '-':
found_info = False
target_list = with_info if found_info else no_info
target_list.append((hba, cts))
target_list.append((hba, ctls))
process = with_info or no_info
msg = "implements" if with_info else "doesn't implement"
LOG.debug('FC target %s single WWNN for all ports.', msg)
for hba, cts in process:
for hba_channel, target_id in cts:
for hba, ctls in process:
for hba_channel, target_id, target_lun in ctls:
LOG.debug('Scanning host %(host)s (wwnn: %(wwnn)s, c: '
'%(channel)s, t: %(target)s, l: %(lun)s)',
{'host': hba['host_device'],

185
os_brick/tests/initiator/connectors/test_fibre_channel.py

@ -11,6 +11,7 @@
# 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 ddt
import mock
import os
@ -67,13 +68,13 @@ class FibreChannelConnectorTestCase(test_connector.ConnectorTestCase):
'device_path': hbas[0]['ClassDevicePath']}]
return info
def fibrechan_connection(self, volume, location, wwn):
def fibrechan_connection(self, volume, location, wwn, lun=1):
return {'driver_volume_type': 'fibrechan',
'data': {
'volume_id': volume['id'],
'target_portal': location,
'target_wwn': wwn,
'target_lun': 1,
'target_lun': lun,
}}
@mock.patch.object(linuxfc.LinuxFibreChannel, 'get_fc_hbas')
@ -125,8 +126,10 @@ class FibreChannelConnectorTestCase(test_connector.ConnectorTestCase):
location = '10.0.2.15:3260'
wwn = '1234567890123456'
connection_info = self.fibrechan_connection(vol, location, wwn)
volume_paths = self.connector.get_volume_paths(
connection_info['data'])
conn_data = self.connector._add_targets_to_connection_properties(
connection_info['data']
)
volume_paths = self.connector.get_volume_paths(conn_data)
expected = ['/dev/disk/by-path/pci-0000:05:00.2'
'-fc-0x1234567890123456-lun-1']
@ -169,10 +172,15 @@ class FibreChannelConnectorTestCase(test_connector.ConnectorTestCase):
name = 'volume-00000001'
vol = {'id': 1, 'name': name}
# Should work for string, unicode, and list
wwns = ['1234567890123456', six.text_type('1234567890123456'),
['1234567890123456', '1234567890123457']]
for wwn in wwns:
connection_info = self.fibrechan_connection(vol, location, wwn)
wwns_luns = [
('1234567890123456', 1),
(six.text_type('1234567890123456'), 1),
(['1234567890123456', '1234567890123457'], 1),
(['1234567890123456', '1234567890123457'], 1),
]
for wwn, lun in wwns_luns:
connection_info = self.fibrechan_connection(vol, location,
wwn, lun)
dev_info = self.connector.connect_volume(connection_info['data'])
exp_wwn = wwn[0] if isinstance(wwn, list) else wwn
dev_str = ('/dev/disk/by-path/pci-0000:05:00.2-fc-0x%s-lun-1' %
@ -188,13 +196,13 @@ class FibreChannelConnectorTestCase(test_connector.ConnectorTestCase):
# Should not work for anything other than string, unicode, and list
connection_info = self.fibrechan_connection(vol, location, 123)
self.assertRaises(exception.NoFibreChannelHostsFound,
self.assertRaises(exception.VolumePathsNotFound,
self.connector.connect_volume,
connection_info['data'])
get_fc_hbas_mock.side_effect = [[]]
get_fc_hbas_info_mock.side_effect = [[]]
self.assertRaises(exception.NoFibreChannelHostsFound,
self.assertRaises(exception.VolumePathsNotFound,
self.connector.connect_volume,
connection_info['data'])
@ -470,3 +478,160 @@ class FibreChannelConnectorTestCase(test_connector.ConnectorTestCase):
was_multipath)
remove_mock.assert_called_once_with('/dev/sda',
flush=flush_mock.return_value)
@ddt.data(
{
"target_info": {
"target_lun": 1,
"target_wwn": '1234567890123456',
},
"expected_targets": [
('1234567890123456', 1)
]
},
{
"target_info": {
"target_lun": 1,
"target_wwn": ['1234567890123456', '1234567890123457'],
},
"expected_targets": [
('1234567890123456', 1),
('1234567890123457', 1),
]
},
{
"target_info": {
"target_luns": [1, 1],
"target_wwn": ['1234567890123456', '1234567890123457'],
},
"expected_targets": [
('1234567890123456', 1),
('1234567890123457', 1),
]
},
{
"target_info": {
"target_luns": [1, 2],
"target_wwn": ['1234567890123456', '1234567890123457'],
},
"expected_targets": [
('1234567890123456', 1),
('1234567890123457', 2),
]
},
{
"target_info": {
"target_luns": [1, 1],
"target_wwns": ['1234567890123456', '1234567890123457'],
},
"expected_targets": [
('1234567890123456', 1),
('1234567890123457', 1),
]
},
{
"target_info": {
"target_lun": 7,
"target_luns": [1, 1],
"target_wwn": 'foo',
"target_wwns": ['1234567890123456', '1234567890123457'],
},
"expected_targets": [
('1234567890123456', 1),
('1234567890123457', 1),
]
},
# Add the zone map in now
{
"target_info": {
"target_lun": 1,
"target_wwn": '1234567890123456',
},
"expected_targets": [
('1234567890123456', 1)
],
"itmap": {
'0004567890123456': ['1234567890123456']
},
"expected_map": {
'0004567890123456': [('1234567890123456', 1)]
}
},
{
"target_info": {
"target_lun": 1,
"target_wwn": ['1234567890123456', '1234567890123457'],
},
"expected_targets": [
('1234567890123456', 1),
('1234567890123457', 1),
],
"itmap": {
'0004567890123456': ['1234567890123456',
'1234567890123457']
},
"expected_map": {
'0004567890123456': [('1234567890123456', 1),
('1234567890123457', 1)]
}
},
{
"target_info": {
"target_luns": [1, 2],
"target_wwn": ['1234567890123456', '1234567890123457'],
},
"expected_targets": [
('1234567890123456', 1),
('1234567890123457', 2),
],
"itmap": {
'0004567890123456': ['1234567890123456'],
'1004567890123456': ['1234567890123457'],
},
"expected_map": {
'0004567890123456': [('1234567890123456', 1)],
'1004567890123456': [('1234567890123457', 2)],
}
},
{
"target_info": {
"target_luns": [1, 2],
"target_wwn": ['1234567890123456', '1234567890123457'],
},
"expected_targets": [
('1234567890123456', 1),
('1234567890123457', 2),
],
"itmap": {
'0004567890123456': ['1234567890123456',
'1234567890123457']
},
"expected_map": {
'0004567890123456': [('1234567890123456', 1),
('1234567890123457', 2)]
}
},
)
@ddt.unpack
def test__add_targets_to_connection_properties(self, target_info,
expected_targets,
itmap=None,
expected_map=None):
volume = {'id': 'fake_uuid'}
wwn = '1234567890123456'
conn = self.fibrechan_connection(volume, "10.0.2.15:3260", wwn)
conn['data'].update(target_info)
if itmap:
conn['data']['initiator_target_map'] = itmap
connection_info = self.connector._add_targets_to_connection_properties(
conn['data'])
self.assertIn('targets', connection_info)
self.assertEqual(expected_targets, connection_info['targets'])
if itmap:
self.assertIn('initiator_target_lun_map', connection_info)
self.assertEqual(expected_map,
connection_info['initiator_target_lun_map'])

87
os_brick/tests/initiator/test_linuxfc.py

@ -50,9 +50,17 @@ class LinuxFCTestCase(base.TestCase):
connection_properties = {
'initiator_target_map': {'50014380186af83c': ['514f0c50023f6c00'],
'50014380186af83e': ['514f0c50023f6c01']},
'initiator_target_lun_map': {
'50014380186af83c': [('514f0c50023f6c00', 1)],
'50014380186af83e': [('514f0c50023f6c01', 1)]
},
'target_discovered': False,
'target_lun': 1,
'target_wwn': ['514f0c50023f6c00', '514f0c50023f6c01']
'target_wwn': ['514f0c50023f6c00', '514f0c50023f6c01'],
'targets': [
('514f0c50023f6c00', 1),
('514f0c50023f6c01', 1),
]
}
hbas = [
@ -69,6 +77,7 @@ class LinuxFCTestCase(base.TestCase):
]
if not zone_manager:
del connection_properties['initiator_target_map']
del connection_properties['initiator_target_lun_map']
return hbas, connection_properties
def test__get_hba_channel_scsi_target_single_wwpn(self):
@ -76,6 +85,7 @@ class LinuxFCTestCase(base.TestCase):
'')
hbas, con_props = self.__get_rescan_info()
con_props['target_wwn'] = con_props['target_wwn'][0]
con_props['targets'] = con_props['targets'][0:1]
with mock.patch.object(self.lfc, '_execute',
return_value=execute_results) as execute_mock:
res = self.lfc._get_hba_channel_scsi_target(hbas[0], con_props)
@ -83,22 +93,56 @@ class LinuxFCTestCase(base.TestCase):
'grep -Gil "514f0c50023f6c00" '
'/sys/class/fc_transport/target6:*/port_name',
shell=True)
expected = [['0', '1']]
expected = [['0', '1', 1]]
self.assertListEqual(expected, res)
def test__get_hba_channel_scsi_target_multiple_wwpn(self):
execute_results = ('/sys/class/fc_transport/target6:0:1/port_name\n'
'/sys/class/fc_transport/target6:0:2/port_name\n',
'')
execute_results = [
['/sys/class/fc_transport/target6:0:1/port_name\n', ''],
['/sys/class/fc_transport/target6:0:2/port_name\n', ''],
]
hbas, con_props = self.__get_rescan_info()
with mock.patch.object(self.lfc, '_execute',
return_value=execute_results) as execute_mock:
side_effect=execute_results) as execute_mock:
res = self.lfc._get_hba_channel_scsi_target(hbas[0], con_props)
execute_mock.assert_called_once_with(
'grep -Gil "514f0c50023f6c00\|514f0c50023f6c01" '
'/sys/class/fc_transport/target6:*/port_name',
shell=True)
expected = [['0', '1'], ['0', '2']]
expected_cmds = [
mock.call('grep -Gil "514f0c50023f6c00" '
'/sys/class/fc_transport/target6:*/port_name',
shell=True),
mock.call('grep -Gil "514f0c50023f6c01" '
'/sys/class/fc_transport/target6:*/port_name',
shell=True),
]
execute_mock.assert_has_calls(expected_cmds)
expected = [['0', '1', 1], ['0', '2', 1]]
self.assertListEqual(expected, res)
def test__get_hba_channel_scsi_target_multiple_wwpn_and_luns(self):
execute_results = [
['/sys/class/fc_transport/target6:0:1/port_name\n', ''],
['/sys/class/fc_transport/target6:0:2/port_name\n', ''],
]
hbas, con_props = self.__get_rescan_info()
con_props['target_lun'] = [1, 7]
con_props['targets'] = [
('514f0c50023f6c00', 1),
('514f0c50023f6c01', 7),
]
with mock.patch.object(self.lfc, '_execute',
side_effect=execute_results) as execute_mock:
res = self.lfc._get_hba_channel_scsi_target(hbas[0], con_props)
expected_cmds = [
mock.call('grep -Gil "514f0c50023f6c00" '
'/sys/class/fc_transport/target6:*/port_name',
shell=True),
mock.call('grep -Gil "514f0c50023f6c01" '
'/sys/class/fc_transport/target6:*/port_name',
shell=True),
]
execute_mock.assert_has_calls(expected_cmds)
expected = [['0', '1', 1], ['0', '2', 7]]
self.assertListEqual(expected, res)
def test__get_hba_channel_scsi_target_zone_manager(self):
@ -112,7 +156,7 @@ class LinuxFCTestCase(base.TestCase):
'grep -Gil "514f0c50023f6c00" '
'/sys/class/fc_transport/target6:*/port_name',
shell=True)
expected = [['0', '1']]
expected = [['0', '1', 1]]
self.assertListEqual(expected, res)
def test__get_hba_channel_scsi_target_not_found(self):
@ -135,11 +179,11 @@ class LinuxFCTestCase(base.TestCase):
'grep -Gil "514f0c50023f6c00" '
'/sys/class/fc_transport/target6:*/port_name',
shell=True)
self.assertEqual([['-', '-']], res)
self.assertEqual(res, [['-', '-', 1]])
def test_rescan_hosts_initiator_map(self):
"""Test FC rescan with initiator map and not every HBA connected."""
get_chan_results = [[['2', '3'], ['4', '5']], [['6', '7']]]
get_chan_results = [[['2', '3', 1], ['4', '5', 1]], [['6', '7', 1]]]
hbas, con_props = self.__get_rescan_info(zone_manager=True)
@ -177,12 +221,14 @@ class LinuxFCTestCase(base.TestCase):
def test_rescan_hosts_single_wwnn(self):
"""Test FC rescan with no initiator map and single WWNN for ports."""
get_chan_results = [[['2', '3'], ['4', '5']],
[['6', '7']], [['-', '-']]]
get_chan_results = [
[['2', '3', 1], ['4', '5', 1]],
[['6', '7', 1]],
[['-', '-', 1]],
]
hbas, con_props = self.__get_rescan_info(zone_manager=False)
hbas, con_props = self.__get_rescan_info(zone_manager=True)
# Remove the initiator map
con_props.pop('initiator_target_map')
# This HBA is the one that is not included in the single WWNN.
hbas.append({'device_path': ('/sys/devices/pci0000:00/0000:00:02.0/'
'0000:04:00.2/host8/fc_host/host8'),
@ -216,10 +262,11 @@ class LinuxFCTestCase(base.TestCase):
def test_rescan_hosts_wildcard(self):
"""Test when we don't have initiator map or target is single WWNN."""
get_chan_results = [[['-', '-']], [['-', '-']]]
get_chan_results = [[['-', '-', 1]], [['-', '-', 1]]]
hbas, con_props = self.__get_rescan_info(zone_manager=True)
# Remove the initiator map
con_props.pop('initiator_target_map')
con_props.pop('initiator_target_lun_map')
with mock.patch.object(self.lfc, '_get_hba_channel_scsi_target',
side_effect=get_chan_results), \
mock.patch.object(self.lfc, '_execute',

Loading…
Cancel
Save