• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

xapi-project / xen-api / 13699792417

06 Mar 2025 01:27PM CUT coverage: 77.729%. Remained the same
13699792417

Pull #6335

github

web-flow
Merge 40833fb15 into 8ca83288c
Pull Request #6335: (docs) Describe the flows of setting NUMA node affinity in Xen by xenopsd

3354 of 4315 relevant lines covered (77.73%)

0.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.05
/python3/libexec/nbd_client_manager.py
1
#!/usr/bin/python3
2

3
"""
4
Provides functions and a CLI for safely connecting to and disconnecting from
5
NBD devices.
6
"""
7

8
import argparse
1✔
9
import fcntl
1✔
10
import json
1✔
11
import logging
1✔
12
import logging.handlers
1✔
13
import os
1✔
14
import re
1✔
15
import subprocess
1✔
16
import time
1✔
17
from datetime import datetime, timedelta
1✔
18

19
LOGGER = logging.getLogger("nbd_client_manager")
1✔
20
LOGGER.setLevel(logging.DEBUG)
1✔
21

22
LOCK_FILE = "/var/run/nonpersistent/nbd_client_manager"
1✔
23

24
# Don't wait more than 10 minutes for the NBD device
25
MAX_DEVICE_WAIT_MINUTES = 10
1✔
26

27
# According to https://github.com/thom311/libnl/blob/main/include/netlink/errno.h#L38
28
NLE_BUSY = 25
1✔
29

30
class InvalidNbdDevName(Exception):
1✔
31
    """
32
    The NBD device should be in this format: nbd{0-1000}
33
    If we cannot match this pattern, raise this exception
34
    """
35

36
class NbdConnStateTimeout(Exception):
1✔
37
    """
38
    If we cannot get the connection status of a nbd device,
39
    raise this exception.
40
    """
41

42
class NbdDeviceNotFound(Exception):
1✔
43
    """
44
    The NBD device file does not exist. Raised when there are no free NBD
45
    devices.
46
    """
47

48
    def __init__(self, nbd_device):
1✔
49
        super().__init__(
1✔
50
            "NBD device '{}' does not exist".format(nbd_device)
51
        )
52
        self.nbd_device = nbd_device
1✔
53

54
class FileLock: # pragma: no cover
55
    """Container for data relating to a file lock"""
56

57
    def __init__(self, path):
58
        self._path = path
59
        self._lock_file = None
60

61
    def _lock(self):
62
        """Acquire the lock"""
63
        flags = fcntl.LOCK_EX
64
        # pylint: disable=consider-using-with
65
        self._lock_file = open(self._path, "w+", encoding="utf8")
66
        fcntl.flock(self._lock_file, flags)
67

68
    def _unlock(self):
69
        """Unlock and remove the lock file"""
70
        if self._lock_file:
71
            fcntl.flock(self._lock_file, fcntl.LOCK_UN)
72
            self._lock_file.close()
73
            self._lock_file = None
74

75
    def __enter__(self):
76
        self._lock()
77

78
    def __exit__(self, *args):
79
        self._unlock()
80

81

82
FILE_LOCK = FileLock(path=LOCK_FILE)
1✔
83

84

85
def _call(cmd_args, raise_err=True, log_err=True):
1✔
86
    """
87
    [call cmd_args] executes [cmd_args] and returns the exit code.
88
    If [error] and exit code != 0, log and throws a CalledProcessError.
89
    """
90
    LOGGER.debug("Running cmd %s", cmd_args)
1✔
91
    # pylint: disable=consider-using-with
92
    proc = subprocess.Popen(
1✔
93
        cmd_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True,
94
        universal_newlines=True
95
    )
96

97
    _, stderr = proc.communicate()
1✔
98

99
    if proc.returncode != 0:
1✔
100
        if log_err:
1✔
101
            LOGGER.error(
1✔
102
                "%s exited with code %d: %s", " ".join(cmd_args), proc.returncode, stderr
103
            )
104

105
        if raise_err:
1✔
106
            raise subprocess.CalledProcessError(
1✔
107
                returncode=proc.returncode, cmd=cmd_args, output=stderr
108
            )
109

110
    return proc.returncode
1✔
111

112

113
def _is_nbd_device_connected(nbd_device):
1✔
114
    """
115
    Checks whether the specified nbd device is connected according to
116
    nbd-client.
117
    """
118
    # First check if the file exists, because "nbd-client -c" returns
119
    # 1 for a non-existent file.
120
    if not os.path.exists(nbd_device):
1✔
121
        raise NbdDeviceNotFound(nbd_device)
122
    cmd = ["nbd-client", "-check", nbd_device]
1✔
123
    returncode = _call(cmd, raise_err=False, log_err=False)
1✔
124
    if returncode == 0:
1✔
125
        return True
1✔
126
    if returncode == 1:
1✔
127
        return False
1✔
128
    raise subprocess.CalledProcessError(returncode=returncode, cmd=cmd)
×
129

130

131
def _find_unused_nbd_device():
1✔
132
    """
133
    Returns the path of the first /dev/nbdX device that is not
134
    connected according to nbd-client.
135
    Raises NbdDeviceNotFound if no devices are available.
136
    """
137
    for device_no in range(0, 1000):
1✔
138
        nbd_device = "/dev/nbd{}".format(device_no)
1✔
139
        if not _is_nbd_device_connected(nbd_device=nbd_device):
1✔
140
            return nbd_device
1✔
141

142
    # If there are 1000 nbd devices (unlikely) and all are connected
143
    raise NbdDeviceNotFound(nbd_device)  # pyright:ignore[reportPossiblyUnboundVariable]
144

145
def _wait_for_nbd_device(nbd_device, connected):
1✔
146
    deadline = datetime.now() + timedelta(minutes=MAX_DEVICE_WAIT_MINUTES)
1✔
147

148
    while _is_nbd_device_connected(nbd_device=nbd_device) != connected:
1✔
149
        if datetime.now() > deadline:
×
150
            raise NbdConnStateTimeout(
×
151
                "Timed out waiting for connection state of device %s to be %s"
152
                % (nbd_device, connected)
153
            )
154

155
        LOGGER.debug(
×
156
            "Connection status of NBD device %s not yet %s, waiting",
157
            nbd_device,
158
            connected,
159
        )
160
        time.sleep(0.1)
×
161

162

163
PERSISTENT_INFO_DIR = "/var/run/nonpersistent/nbd"
1✔
164

165

166
def _get_persistent_connect_info_filename(device):
1✔
167
    """
168
    Return the full path for the persistent file containing
169
    the connection details. This is based on the device
170
    name, so /dev/nbd0 -> /var/run/nonpersistent/nbd/0
171
    """
172
    matched = re.search("/dev/nbd([0-9]+)", device)
1✔
173
    if not matched:
1✔
174
        raise InvalidNbdDevName("Can not get the nbd number")
×
175
    number = matched.group(1)
1✔
176
    return PERSISTENT_INFO_DIR + "/" + number
1✔
177

178

179
def _persist_connect_info(device, path, exportname):
1✔
180
    if not os.path.exists(PERSISTENT_INFO_DIR):
1✔
181
        os.makedirs(PERSISTENT_INFO_DIR)
1✔
182
    filename = _get_persistent_connect_info_filename(device)
1✔
183
    with open(filename, "w", encoding="utf-8") as info_file:
1✔
184
        info_file.write(json.dumps({"path": path, "exportname": exportname}))
1✔
185

186

187
def _remove_persistent_connect_info(device):
1✔
188
    try:
1✔
189
        os.remove(_get_persistent_connect_info_filename(device))
1✔
190
    except OSError:
×
191
        pass
192

193

194
def connect_nbd(path, exportname):
1✔
195
    """Connects to a free NBD device using nbd-client and returns its path"""
196
    # We should not ask for too many nbds, as we might not have enough memory
197
    _call(["modprobe", "nbd", "nbds_max=24"])
1✔
198
    # Wait for systemd-udevd to process the udev rules
199
    _call(["udevadm", "settle", "--timeout=30"])
1✔
200
    retries = 0
1✔
201
    while True:
1✔
202
        try:
1✔
203
            with FILE_LOCK:
1✔
204
                nbd_device = _find_unused_nbd_device()
1✔
205
                cmd = [
1✔
206
                    "nbd-client",
207
                    "-unix",
208
                    path,
209
                    nbd_device,
210
                    "-timeout",
211
                    "60",
212
                    "-name",
213
                    exportname,
214
                ]
215
                ret = _call(cmd, raise_err=False, log_err=True)
1✔
216
                if NLE_BUSY == ret:
1✔
217
                    # Although _find_unused_nbd_device tell us the nbd devcie is
218
                    # not connected by other nbd-client, it may be opened and locked
219
                    # by other process like systemd-udev, raise NbdDeviceNotFound to retry
220
                    LOGGER.warning("Device %s is busy, will retry", nbd_device)
×
221
                    raise NbdDeviceNotFound(nbd_device)
222

223
                if 0 != ret:
1✔
224
                    raise subprocess.CalledProcessError(returncode=ret, cmd=cmd)
×
225

226
                _wait_for_nbd_device(nbd_device=nbd_device, connected=True)
1✔
227
                _persist_connect_info(nbd_device, path, exportname)
1✔
228
                nbd = (
1✔
229
                    nbd_device[len("/dev/") :]
230
                    if nbd_device.startswith("/dev/")
231
                    else nbd_device
232
                )
233
                with open("/sys/block/" + nbd + "/queue/scheduler",
1✔
234
                          "w", encoding="utf-8") as fd:
235
                    fd.write("none")
1✔
236
                # Set the NBD queue size to the same as the qcow2 cluster size
237
                with open("/sys/block/" + nbd + "/queue/max_sectors_kb",
1✔
238
                          "w", encoding="utf-8") as fd:
239
                    fd.write("512")
1✔
240
                with open("/sys/block/" + nbd + "/queue/nr_requests",
1✔
241
                          "w", encoding="utf-8") as fd:
242
                    fd.write("8")
1✔
243

244
            return nbd_device
1✔
245
        except NbdDeviceNotFound as exn:
×
246
            LOGGER.warning("Failed to find free nbd device: %s", exn)
×
247
            retries = retries + 1
×
248
            if retries == 1:
×
249
                # We sleep for a shorter period first, in case an nbd device
250
                # will become available soon (e.g. during PV Linux guest bootstorm):
251
                time.sleep(10)
×
252
            elif retries < 30:
×
253
                time.sleep(60)
×
254
            else:
255
                raise exn
×
256

257

258
def disconnect_nbd_device(nbd_device):
1✔
259
    """
260
    Disconnects the given device using nbd-client.
261
    This function is idempotent: calling it on an already disconnected device
262
    does nothing.
263
    """
264
    try:
1✔
265
        if _is_nbd_device_connected(nbd_device=nbd_device):
1✔
266
            _remove_persistent_connect_info(nbd_device)
1✔
267
            cmd = ["nbd-client", "-disconnect", nbd_device]
1✔
268
            _call(cmd)
1✔
269
            _wait_for_nbd_device(nbd_device=nbd_device, connected=False)
1✔
270
    except NbdDeviceNotFound:
1✔
271
        # Device gone, no-op
272
        pass
273

274

275
def _connect_cli(args):
1✔
276
    device = connect_nbd(path=args.path, exportname=args.exportname)
×
277
    print(device)
×
278

279

280
def _disconnect_cli(args):
1✔
281
    disconnect_nbd_device(nbd_device=args.device)
×
282

283
# The main function is covered by manual test and XenRT test
284
# Exclude it from unit test coverage
285
def _main(): # pragma: no cover
286
    # Configure the root logger to log into syslog
287
    # (Specifically, into /var/log/user.log)
288
    syslog_handler = logging.handlers.SysLogHandler(
289
        address="/dev/log", facility=logging.handlers.SysLogHandler.LOG_USER
290
    )
291
    # Ensure the program name is included in the log messages:
292
    formatter = logging.Formatter("%(name)s: [%(levelname)s] %(message)s")
293
    syslog_handler.setFormatter(formatter)
294
    logging.getLogger().addHandler(syslog_handler)
295

296
    try:
297
        parser = argparse.ArgumentParser(
298
            description="Connect to and disconnect from an NBD device"
299
        )
300

301
        subparsers = parser.add_subparsers(dest="command_name")
302

303
        parser_connect = subparsers.add_parser(
304
            "connect", help="Connect to a free NBD device and return its path"
305
        )
306
        parser_connect.add_argument(
307
            "--path",
308
            required=True,
309
            help="The path of the Unix domain socket of the NBD server",
310
        )
311
        parser_connect.add_argument(
312
            "--exportname",
313
            required=True,
314
            help="The export name of the device to connect to",
315
        )
316
        parser_connect.set_defaults(func=_connect_cli)
317

318
        parser_disconnect = subparsers.add_parser(
319
            "disconnect", help="Disconnect from the given NBD device"
320
        )
321
        parser_disconnect.add_argument(
322
            "--device", required=True, help="The path of the NBD device to disconnect"
323
        )
324
        parser_disconnect.set_defaults(func=_disconnect_cli)
325

326
        args = parser.parse_args()
327
        args.func(args)
328
    except Exception as exn:
329
        LOGGER.exception(exn)
330
        raise
331

332

333
if __name__ == "__main__":
1✔
334
    _main()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc