Skip to content

Commit 6afa0f4

Browse files
authored
fix(ISCSi): fix race condition on ISCSI operations (#131)
When creating or updating VDIs on an LVMoISCSI SR, a race condition could occur between concurrent iscsiadm calls, causing the SM daemon to receive "No records found" from iscsiadm and raise an SR 202 error. The root cause is twofold: 1. doexec_locked() did not use a try/finally block, meaning that if an exception occurred during iscsiadm execution, the lock was never released, blocking all subsequent iSCSI operations indefinitely. 2. restart_daemon() was deleting iscsi/nodes without holding the iscsiadm lock, creating a window where get_node_records() could be called while the node database was being wiped, returning an empty result and raising a fatal exception. This issue was particularly visible on environments with a high number of LUNs and multipath connections Signed-off-by: Goulven Riou <goulven.riou@vates.tech>
1 parent 285450d commit 6afa0f4

3 files changed

Lines changed: 89 additions & 37 deletions

File tree

drivers/LVMoISCSISR.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import util
2929
import scsiutil
3030
import lvutil
31+
import lock
3132
import time
3233
import os
3334
import sys
@@ -37,8 +38,8 @@
3738
import iscsilib
3839
import glob
3940
import copy
40-
import scsiutil
4141
import xml.dom.minidom
42+
from iscsilib import iscsi_lock, iscsi_try_lock
4243

4344
CAPABILITIES = ["SR_PROBE", "SR_UPDATE", "SR_METADATA", "SR_TRIM", "SR_CACHING",
4445
"VDI_CREATE", "VDI_DELETE", "VDI_ATTACH", "VDI_DETACH",
@@ -547,16 +548,22 @@ def probe(self) -> str:
547548
@override
548549
def check_sr(self, sr_uuid) -> None:
549550
"""Hook to check SR health"""
550-
pbdref = util.find_my_pbd(self.session, self.host_ref, self.sr_ref)
551-
if pbdref:
552-
other_config = self.session.xenapi.PBD.get_other_config(pbdref)
553-
if util.sessions_less_than_targets(other_config, self.dconf):
554-
self.create_iscsi_sessions(sr_uuid)
555-
for iscsi in self.iscsiSRs:
556-
try:
557-
iscsi.attach(sr_uuid)
558-
except xs_errors.SROSError:
559-
util.SMlog("Failed to attach iSCSI target")
551+
with iscsi_try_lock() as acquired:
552+
if not acquired:
553+
util.SMlog("check_sr: iSCSI operation in progress, skipping")
554+
return
555+
pbd_ref = util.find_my_pbd(self.session, self.host_ref, self.sr_ref)
556+
if not pbd_ref:
557+
return
558+
other_config = self.session.xenapi.PBD.get_other_config(pbd_ref)
559+
if not util.sessions_less_than_targets(other_config, self.dconf):
560+
return
561+
self.create_iscsi_sessions(sr_uuid)
562+
for iscsi in self.iscsiSRs:
563+
try:
564+
iscsi.attach(sr_uuid)
565+
except xs_errors.SROSError:
566+
util.SMlog("check_sr: Failed to attach iSCSI target")
560567

561568
@override
562569
def vdi(self, uuid) -> VDI.VDI:

drivers/iscsilib.py

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@
2626
import lock
2727
import glob
2828
import tempfile
29+
import contextlib
2930
from configparser import RawConfigParser
3031
import io
32+
from functools import wraps
33+
from typing import Iterator
34+
3135

3236
# The 3.x kernel brings with it some iSCSI path changes in sysfs
3337
_KERNEL_VERSION = os.uname()[2]
@@ -49,17 +53,47 @@
4953
_ISCSI_DB_PATH = '/var/lib/iscsi'
5054

5155

56+
@contextlib.contextmanager
57+
def iscsi_lock() -> Iterator[None]:
58+
"""Context manager to serialize iscsiadm calls. Blocks until lock is available."""
59+
iscsiadm_lock = lock.Lock(lock.LOCK_TYPE_ISCSIADM_RUNNING, 'iscsiadm')
60+
iscsiadm_lock.acquire()
61+
try:
62+
yield
63+
finally:
64+
iscsiadm_lock.release()
65+
66+
67+
@contextlib.contextmanager
68+
def iscsi_try_lock() -> Iterator[bool]:
69+
"""
70+
Context manager to serialize iscsiadm calls.
71+
Yields True if the lock was acquired, False immediately if unavailable.
72+
"""
73+
iscsiadm_lock = lock.Lock(lock.LOCK_TYPE_ISCSIADM_RUNNING, 'iscsiadm')
74+
acquired = iscsiadm_lock.acquireNoblock()
75+
try:
76+
yield acquired
77+
finally:
78+
if acquired:
79+
iscsiadm_lock.release()
80+
81+
82+
def with_iscsi_lock(fn):
83+
"""Decorator to run a function whilst holding the iSCSI lock."""
84+
@wraps(fn)
85+
def wrapper(*args, **kwargs):
86+
with iscsi_lock():
87+
return fn(*args, **kwargs)
88+
return wrapper
89+
90+
5291
def doexec_locked(cmd):
5392
"""Executes via util.doexec the command specified whilst holding lock"""
54-
_lock = None
5593
if os.path.basename(cmd[0]) == 'iscsiadm':
56-
_lock = lock.Lock(lock.LOCK_TYPE_ISCSIADM_RUNNING, 'iscsiadm')
57-
_lock.acquire()
58-
# util.SMlog("%s" % cmd)
59-
(rc, stdout, stderr) = util.doexec(cmd)
60-
if _lock is not None and _lock.held():
61-
_lock.release()
62-
return (rc, stdout, stderr)
94+
with iscsi_lock():
95+
return util.doexec(cmd)
96+
return util.doexec(cmd)
6397

6498

6599
def noexn_on_failure(cmd):
@@ -182,8 +216,10 @@ def discovery(target, port, chapuser, chappass, targetIQN="any",
182216
def get_node_records(targetIQN="any"):
183217
"""Return the node records that the iscsi daemon already knows about"""
184218
cmd = ["iscsiadm", "-m", "node"]
185-
failuremessage = "Failed to obtain node records from iscsi daemon"
186-
(stdout, stderr) = exn_on_failure(cmd, failuremessage)
219+
(rc, stdout, stderr) = doexec_locked(cmd)
220+
if rc != 0:
221+
util.SMlog(f"get_node_records: iscsiadm rc={rc} stderr={stderr}")
222+
return []
187223
return parse_node_output(stdout, targetIQN)
188224

189225

@@ -389,17 +425,13 @@ def stop_daemon():
389425
exn_on_failure(cmd, failuremessage)
390426

391427

428+
@with_iscsi_lock
392429
def restart_daemon():
393430
stop_daemon()
394-
if os.path.exists(os.path.join(_ISCSI_DB_PATH, 'nodes')):
395-
try:
396-
shutil.rmtree(os.path.join(_ISCSI_DB_PATH, 'nodes'))
397-
except:
398-
pass
399-
try:
400-
shutil.rmtree(os.path.join(_ISCSI_DB_PATH, 'send_targets'))
401-
except:
402-
pass
431+
with contextlib.suppress(OSError):
432+
shutil.rmtree(os.path.join(_ISCSI_DB_PATH, 'nodes'))
433+
with contextlib.suppress(OSError):
434+
shutil.rmtree(os.path.join(_ISCSI_DB_PATH, 'send_targets'))
403435
cmd = ["/usr/bin/systemctl", "start", "iscsid.service"]
404436
failuremessage = "Failed to start iscsi daemon"
405437
exn_on_failure(cmd, failuremessage)

tests/test_iscsilib.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import iscsilib
22
import unittest.mock as mock
33
import unittest
4-
4+
import lock
55

66
TEST_IQN = 'iqn.2003-01.com.bla:00.ecd28.mo121'
77

@@ -34,17 +34,30 @@ def test_restore_rootdisk_nodes(self, doexec, get_rootdisk_iqns):
3434
@mock.patch('iscsilib.stop_daemon', mock.Mock())
3535
@mock.patch('iscsilib.exn_on_failure', mock.Mock())
3636
@mock.patch('util.doexec', mock.Mock())
37+
@mock.patch('lock.flock.WriteLock')
38+
@mock.patch('lock.Lock._mkdirs', mock.Mock())
3739
@mock.patch('os.path.exists')
3840
@mock.patch('shutil.rmtree')
39-
def test_restart_daemon(self, rmtree, exists):
41+
def test_restart_daemon(self, rmtree, exists, mock_write_lock):
4042
exists.return_value = True
41-
42-
iscsilib.restart_daemon()
43-
43+
saved_instances = lock.Lock.INSTANCES.copy()
44+
saved_base_instances = lock.Lock.BASE_INSTANCES.copy()
45+
46+
def restore_lock_state():
47+
lock.Lock.INSTANCES.clear()
48+
lock.Lock.INSTANCES.update(saved_instances)
49+
lock.Lock.BASE_INSTANCES.clear()
50+
lock.Lock.BASE_INSTANCES.update(saved_base_instances)
51+
52+
self.addCleanup(restore_lock_state)
53+
lock.Lock.INSTANCES.clear()
54+
lock.Lock.BASE_INSTANCES.clear()
55+
with mock.patch('builtins.open', mock.mock_open(), create=True) as mock_open:
56+
mock_open.return_value.fileno.return_value = 0
57+
iscsilib.restart_daemon()
4458
rmtree.assert_has_calls([mock.call('/var/lib/iscsi/nodes'),
4559
mock.call('/var/lib/iscsi/send_targets')])
46-
47-
60+
4861
@mock.patch('util.doexec', mock.Mock())
4962
@mock.patch('iscsilib.exn_on_failure')
5063
@mock.patch('iscsilib.tempfile', autospec=True)

0 commit comments

Comments
 (0)