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

xapi-project / xen-api / 10317302373

09 Aug 2024 09:54AM UTC coverage: 77.536% (+29.9%) from 47.605%
10317302373

Pull #5896

github

web-flow
Merge pull request #5925 from liulinC/private/linl/py3

CP-49148: Clean py2 compatible code
Pull Request #5896: Python3 update feature merge

1428 of 1538 new or added lines in 29 files covered. (92.85%)

3386 of 4367 relevant lines covered (77.54%)

0.78 hits per line

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

82.57
/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

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

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

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

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

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

55
    def __init__(self, path):
56
        self._path = path
57
        self._lock_file = None
58

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

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

73
    def __enter__(self):
74
        self._lock()
75

76
    def __exit__(self, *args):
77
        self._unlock()
78

79

80
FILE_LOCK = FileLock(path=LOCK_FILE)
1✔
81

82

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

95
    _, stderr = proc.communicate()
1✔
96

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

102
        raise subprocess.CalledProcessError(
1✔
103
            returncode=proc.returncode, cmd=cmd_args, output=stderr
104
        )
105

106
    return proc.returncode
1✔
107

108

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

126

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

138
    # If there are 1000 nbd devices (unlikely) and all are connected
139
    raise NbdDeviceNotFound(nbd_device)  # pyright:ignore[reportPossiblyUnboundVariable]
140

141
def _wait_for_nbd_device(nbd_device, connected):
1✔
142
    deadline = datetime.now() + timedelta(minutes=MAX_DEVICE_WAIT_MINUTES)
1✔
143

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

151
        LOGGER.debug(
×
152
            "Connection status of NBD device %s not yet %s, waiting",
153
            nbd_device,
154
            connected,
155
        )
156
        time.sleep(0.1)
×
157

158

159
PERSISTENT_INFO_DIR = "/var/run/nonpersistent/nbd"
1✔
160

161

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

174

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

182

183
def _remove_persistent_connect_info(device):
1✔
184
    try:
1✔
185
        os.remove(_get_persistent_connect_info_filename(device))
1✔
186
    except OSError:
×
187
        pass
188

189

190
def connect_nbd(path, exportname):
1✔
191
    """Connects to a free NBD device using nbd-client and returns its path"""
192
    # We should not ask for too many nbds, as we might not have enough memory
193
    _call(["modprobe", "nbd", "nbds_max=24"])
1✔
194
    retries = 0
1✔
195
    while True:
1✔
196
        try:
1✔
197
            with FILE_LOCK:
1✔
198
                nbd_device = _find_unused_nbd_device()
1✔
199
                cmd = [
1✔
200
                    "nbd-client",
201
                    "-unix",
202
                    path,
203
                    nbd_device,
204
                    "-timeout",
205
                    "60",
206
                    "-name",
207
                    exportname,
208
                ]
209
                _call(cmd)
1✔
210
                _wait_for_nbd_device(nbd_device=nbd_device, connected=True)
1✔
211
                _persist_connect_info(nbd_device, path, exportname)
1✔
212
                nbd = (
1✔
213
                    nbd_device[len("/dev/") :]
214
                    if nbd_device.startswith("/dev/")
215
                    else nbd_device
216
                )
217
                with open("/sys/block/" + nbd + "/queue/scheduler",
1✔
218
                          "w", encoding="utf-8") as fd:
219
                    fd.write("none")
1✔
220
                # Set the NBD queue size to the same as the qcow2 cluster size
221
                with open("/sys/block/" + nbd + "/queue/max_sectors_kb",
1✔
222
                          "w", encoding="utf-8") as fd:
223
                    fd.write("512")
1✔
224
                with open("/sys/block/" + nbd + "/queue/nr_requests",
1✔
225
                          "w", encoding="utf-8") as fd:
226
                    fd.write("8")
1✔
227

228
            return nbd_device
1✔
229
        except NbdDeviceNotFound as exn:
×
NEW
230
            LOGGER.warning("Failed to find free nbd device: %s", exn)
×
231
            retries = retries + 1
×
232
            if retries == 1:
×
233
                # We sleep for a shorter period first, in case an nbd device
234
                # will become available soon (e.g. during PV Linux guest bootstorm):
235
                time.sleep(10)
×
236
            elif retries < 30:
×
237
                time.sleep(60)
×
238
            else:
239
                raise exn
×
240

241

242
def disconnect_nbd_device(nbd_device):
1✔
243
    """
244
    Disconnects the given device using nbd-client.
245
    This function is idempotent: calling it on an already disconnected device
246
    does nothing.
247
    """
248
    try:
1✔
249
        if _is_nbd_device_connected(nbd_device=nbd_device):
1✔
250
            _remove_persistent_connect_info(nbd_device)
1✔
251
            cmd = ["nbd-client", "-disconnect", nbd_device]
1✔
252
            _call(cmd)
1✔
253
            _wait_for_nbd_device(nbd_device=nbd_device, connected=False)
1✔
254
    except NbdDeviceNotFound:
1✔
255
        # Device gone, no-op
256
        pass
257

258

259
def _connect_cli(args):
1✔
260
    device = connect_nbd(path=args.path, exportname=args.exportname)
×
NEW
261
    print(device)
×
262

263

264
def _disconnect_cli(args):
1✔
265
    disconnect_nbd_device(nbd_device=args.device)
×
266

267
# The main function is covered by manual test and XenRT test
268
# Exclude it from unit test coverage
269
def _main(): # pragma: no cover
270
    # Configure the root logger to log into syslog
271
    # (Specifically, into /var/log/user.log)
272
    syslog_handler = logging.handlers.SysLogHandler(
273
        address="/dev/log", facility=logging.handlers.SysLogHandler.LOG_USER
274
    )
275
    # Ensure the program name is included in the log messages:
276
    formatter = logging.Formatter("%(name)s: [%(levelname)s] %(message)s")
277
    syslog_handler.setFormatter(formatter)
278
    logging.getLogger().addHandler(syslog_handler)
279

280
    try:
281
        parser = argparse.ArgumentParser(
282
            description="Connect to and disconnect from an NBD device"
283
        )
284

285
        subparsers = parser.add_subparsers(dest="command_name")
286

287
        parser_connect = subparsers.add_parser(
288
            "connect", help="Connect to a free NBD device and return its path"
289
        )
290
        parser_connect.add_argument(
291
            "--path",
292
            required=True,
293
            help="The path of the Unix domain socket of the NBD server",
294
        )
295
        parser_connect.add_argument(
296
            "--exportname",
297
            required=True,
298
            help="The export name of the device to connect to",
299
        )
300
        parser_connect.set_defaults(func=_connect_cli)
301

302
        parser_disconnect = subparsers.add_parser(
303
            "disconnect", help="Disconnect from the given NBD device"
304
        )
305
        parser_disconnect.add_argument(
306
            "--device", required=True, help="The path of the NBD device to disconnect"
307
        )
308
        parser_disconnect.set_defaults(func=_disconnect_cli)
309

310
        args = parser.parse_args()
311
        args.func(args)
312
    except Exception as exn:
313
        LOGGER.exception(exn)
314
        raise
315

316

317
if __name__ == "__main__":
1✔
318
    _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