• 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

16.92
/python3/libexec/usb_reset.py
1
#!/usr/bin/env python3
2
#
3
# Copyright (C) Citrix Systems Inc.
4
#
5
# This program is free software; you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License as published
7
# by the Free Software Foundation; version 2.1 only. #
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU Lesser General Public License for more details.
12
#
13
# You should have received a copy of the GNU Lesser General Public License
14
# along with this program; if not, write to the Free Software Foundation, Inc.,
15
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
16
#
17
# attach
18
# ./usb_reset.py attach device -d dom-id -p pid [-r]
19
# ./usb_reset.py attach 2-2 -d 12 -p 4130
20
# ./usb_reset.py attach 2-2 -d 12 -p 4130 -r
21
# 1. reset device
22
# if without -r, do step 2~4
23
# 2. if it's the first USB device to pass-through
24
#      a) bind mount /dev /sys in chroot directory (/var/xen/qemu/root-<domid>)
25
#      b) create new cgroup devices:/qemu-<domid>,
26
#      c) blacklist all and add default device whitelist,
27
#      d) join current qemu process to this cgroup
28
# 3. save device uid/gid to /var/run/nonpersistent/usb/<device>
29
# 4. set device file uid/gid to (qemu_base + dom-id)
30
# 5. add current device to whitelist
31
#
32
# detach
33
# ./usb_reset.py detach device -d dom-id
34
# ./usb_reset.py detach 2-2 -d 12
35
# 1. restore device file uid/gid from /var/run/nonpersistent/usb/<device>
36
# 2. remove current device from whitelist
37
#
38
# cleanup
39
# ./usb_reset.py cleanup -d dom-id
40
# ./usb_reset.py cleanup -d 12
41
# 1.remove the cgroup if one has been created.
42
# 2.umount /dev, /sys from chroot directory if they are mounted.
43

44
import argparse
1✔
45
import ctypes
1✔
46
import ctypes.util
1✔
47
import errno
1✔
48
import fcntl
1✔
49
import grp
1✔
50
import xcp.logger as log  # pytype: disable=import-error
1✔
51
import logging
1✔
52
import os
1✔
53
import pwd
1✔
54
import re
1✔
55
from stat import S_ISCHR, S_ISBLK
1✔
56

57

58
def parse_arg():
1✔
59
    parser = argparse.ArgumentParser(
60
        description="script to attach, detach, cleanup for USB pass-through")
61
    subparsers = parser.add_subparsers(help="command", dest="command")
×
62

63
    attach = subparsers.add_parser("attach", help="attach a USB device")
×
64
    attach.add_argument("device", help="the target usb device")
×
65
    attach.add_argument("-d", dest="domid", type=int, required=True,
×
66
                        help="specify the domid of the VM")
67
    attach.add_argument("-p", dest="pid", type=int, required=True,
×
68
                        help="the process id of QEMU")
69
    attach.add_argument("-r", dest="reset_only", action="store_true",
×
70
                        help="reset device only, for privileged mode")
71

72
    detach = subparsers.add_parser("detach", help="detach a USB device")
×
73
    detach.add_argument("device", help="the target usb device")
×
74
    detach.add_argument("-d", dest="domid", type=int, required=True,
×
75
                        help="specify the domid of the VM")
76

77
    cleanup = subparsers.add_parser("cleanup", help="clean up chroot directory")
×
78
    cleanup.add_argument("-d", dest="domid", type=int, required=True,
×
79
                         help="specify the domid of the VM")
80

81
    return parser.parse_args()
×
82

83

84
def get_root_dir(domid):
1✔
85
    return "/var/xen/qemu/root-{}".format(domid)
×
86

87

88
def get_cg_dir(domid):
1✔
89
    return "/sys/fs/cgroup/devices/qemu-{}".format(domid)
×
90

91

92
def get_ids_path(device):
1✔
93
    usb_dir = "/var/run/nonpersistent/usb"
×
94
    try:
×
95
        os.makedirs(usb_dir)
×
96
    except OSError as e:
×
97
        if e.errno != errno.EEXIST:
×
98
            raise
×
99

100
    return os.path.join(usb_dir, device)
×
101

102

103
def save_device_ids(device):
1✔
104
    path = dev_path(device)
×
105

106
    try:
×
107
        stat = os.stat(path)
×
108
        ids_info = "{} {}".format(stat.st_uid, stat.st_gid)
×
109
    except OSError as e:
×
110
        log.error("Failed to stat {}: {}".format(path, str(e)))
×
111
        exit(1)
×
112

113
    try:
×
114
        with open(get_ids_path(device), "w") as f:
×
115
            f.write(ids_info)
×
116
    except IOError as e:
×
117
        log.error("Failed to save device ids {}: {}".format(path, str(e)))
×
118
        exit(1)
×
119

120

121
def load_device_ids(device):
1✔
122
    ids_path = get_ids_path(device)
×
123
    try:
×
124
        with open(ids_path) as f:
×
125
            uid, gid = list(map(int, f.readline().split()))
×
126
    except (IOError, ValueError) as e:
×
127
        log.error("Failed to load device ids: {}".format(str(e)))
×
128

129
    try:
×
130
        os.remove(ids_path)
×
131
    except OSError as e:
×
132
        # ignore and continue
133
        log.warning("Failed to remove device ids: {}".format(str(e)))
×
134

135
    return uid, gid  # pyright: ignore[reportPossiblyUnboundVariable] # pragma: no cover
136

137

138
# throw IOError, ValueError
139
def read_int(path):
1✔
140
    with open(path) as f:
×
141
        return int(f.readline())
×
142

143

144
def dev_path(device):
1✔
145
    # check device node pattern
146
    # example: "4-1", "1-2.1.2"
147
    pat = re.compile(r"\d+-\d+(\.\d+)*$")
×
148
    if pat.match(device) is None:
×
149
        log.error("Unexpected device node: {}".format(device))
×
150
        exit(1)
×
151
    try:
×
152
        bus = read_int("/sys/bus/usb/devices/{}/busnum".format(device))
×
153
        dev = read_int("/sys/bus/usb/devices/{}/devnum".format(device))
×
154
        return "/dev/bus/usb/{0:03d}/{1:03d}".format(bus, dev)
×
155
    except (IOError, ValueError) as e:
×
156
        log.error("Failed to get device path {}: {}".format(device, str(e)))
×
157
        exit(1)
×
158

159

160
def get_ctl(path, mode):  # type:(str, str) -> str
1✔
161
    """get the string to control device access for cgroup
162
    :param path: the device file path
163
    :param mode: either "r" or "rw"
164
    :return: the string to control device access
165
    """
166
    try:
×
167
        st = os.stat(path)
×
168
    except OSError as e:
×
169
        log.error("Failed to get stat of {}: {}".format(path, str(e)))
×
170
        raise
×
171

172
    t = ""
×
173
    if S_ISBLK(st.st_mode):
×
174
        t = "b"
×
175
    elif S_ISCHR(st.st_mode):
×
176
        t = "c"
×
177
    if t and mode in ("r", "rw"):
×
178
        return "{} {}:{} {}".format(t, os.major(st.st_rdev), os.minor(
×
179
            st.st_rdev), mode)
180
    raise RuntimeError("Failed to get control string of {}".format(path))
×
181

182

183
def _device_ctl(path, domid, allow):
1✔
184
    cg_dir = get_cg_dir(domid)
×
185
    file_name = "/devices.allow" if allow else "/devices.deny"
×
186
    try:
×
187
        with open(cg_dir + file_name, "w") as f:
×
188
            f.write(get_ctl(path, "rw"))
×
189
    except (IOError, OSError, RuntimeError) as e:
×
190
        log.error("Failed to {} {}: {}".format(
×
191
            "allow" if allow else "deny", path, str(e)))
192
        exit(1)
×
193

194

195
def allow_device(path, domid):
1✔
196
    _device_ctl(path, domid, True)
×
197

198

199
def deny_device(path, domid):
1✔
200
    _device_ctl(path, domid, False)
×
201

202

203
def setup_cgroup(domid, pid):  # type:(str, str) -> None
1✔
204
    """
205
    Associate the given process id (pid) with the given Linux kernel control group
206
    and limit it's device access to only /dev/null.
207

208
    :param domid (str): The control group ID string (passed on from the command line)
209
    :param pid (str): The process ID string (passed on from the command line)
210

211
    If the control group directory does not exist yet, the control group is created.
212

213
    - The pid goes into the file "tasks" to associate the process with the cgroup.
214
    - Deny device access by default by writing "a" to devices.deny.
215
    - Grant read-write access to /dev/null, writing it's device IDs to devices.allow.
216

217
    If any error occur during the setup process, the error is logged and
218
    the program exits with a status code of 1.
219
    """
220
    cg_dir = get_cg_dir(domid)
×
221

222
    try:
×
223
        os.mkdir(cg_dir, 0o755)
×
224
    except OSError as e:
×
225
        if e.errno != errno.EEXIST:
×
226
            log.error("Failed to create cgroup: {}".format(cg_dir))
×
227
            exit(1)
×
228

229
    try:
×
230
        # unbuffered write to ensure each one is flushed immediately
231
        # to the kernel's control group filesystem:
232
        #
233
        # The order of writes is likely not important, but the writes
234
        # may have to be a single write() system call for the entire string.
235
        #
236
        # Using the unbuffered Raw IO mode, we know the write was done
237
        # in exactly this way by the write function call itself, not later.
238
        #
239
        # With small writes like this , splitting them because of overflowing the
240
        # buffer is not expected to happen. To stay safe and keep using unbuffered I/O
241
        # We have to migrate to binary mode in python3,as python3 supports unbuffered 
242
        # raw I/O in binary mode.
243
        #
244
        with open(cg_dir + "/tasks", "wb", 0) as tasks, \
×
245
                open(cg_dir + "/devices.deny", "wb", 0) as deny, \
246
                open(cg_dir + "/devices.allow", "wb", 0) as allow:
247

248
            # deny all
249
            deny.write(b"a")
×
250

251
            # To write bytes, we've to encode the strings to bytes below:
252

253
            # grant rw access to /dev/null by default
254
            allow.write(get_ctl("/dev/null", "rw").encode())
×
255

256
            tasks.write(str(pid).encode())
×
257

258
    except (IOError, OSError, RuntimeError) as e:
×
259
        log.error("Failed to setup cgroup: {}".format(str(e)))
×
260
        exit(1)
×
261

262

263
def mount(source, target, fs, flags=0):
1✔
264
    if ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True
1✔
265
                   ).mount(source.encode(), target.encode(), fs.encode(), flags, None) < 0:
266
        log.error("Failed to mount {} ({}) to {} with flags {}: {}".
×
267
                  format(source, fs, target, flags,
268
                         os.strerror(ctypes.get_errno())))
269
        exit(1)
×
270

271

272
def umount(target):
1✔
273
    if ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True
1✔
274
                   ).umount(target.encode()) < 0:
275
        # log and continue
276
        log.error("Failed to umount {}: {}".
×
277
                  format(target, os.strerror(ctypes.get_errno())))
278

279

280
def attach(device, domid, pid, reset_only):
1✔
281
    path = dev_path(device)
×
282

283
    # reset device
284
    try:
×
285
        with open(path, "w") as f:
×
286
            # USBDEVFS_RESET _IO('U', 20)
287
            USBDEVFS_RESET = (ord('U') << 8) | 20
×
288
            fcntl.ioctl(f.fileno(), USBDEVFS_RESET, 0)
×
289
    except IOError as e:
×
290
        # log and continue
291
        log.error("Failed to reset {}: {}".format(path, str(e)))
×
292

293
    if reset_only:
×
294
        return
×
295

296
    save_device_ids(device)
×
297

298
    # set device file uid/gid
299
    try:
×
300
        os.chown(path, pwd.getpwnam("qemu_base").pw_uid + domid,
×
301
                 grp.getgrnam("qemu_base").gr_gid + domid)
302
    except OSError as e:
×
303
        log.error("Failed to chown device file {}: {}".format(path, str(e)))
×
304
        exit(1)
×
305

306
    root_dir = get_root_dir(domid)
×
307
    dev_dir = root_dir + "/dev"
×
308
    if not os.path.isdir(root_dir) or not os.path.isdir(dev_dir):
×
309
        log.error("Error: The chroot or dev directory doesn't exist")
×
310
        exit(1)
×
311

312
    if not os.path.isdir(dev_dir + "/bus"):
×
313
        # first USB device to pass-through
314
        MS_BIND = 4096  # mount flags, from fs.h
×
315
        mount("/dev", dev_dir, "", MS_BIND)
×
316
        setup_cgroup(domid, pid)
×
317

318
    sys_dir = root_dir + "/sys"
×
319
    # sys_dir could already be mounted because of PCI pass-through
320
    if not os.path.isdir(sys_dir):
×
321
        try:
×
322
            os.mkdir(sys_dir, 0o755)
×
323
        except OSError:
×
324
            log.error("Failed to create sys dir in chroot")
×
325
            exit(1)
×
326
    if not os.path.isdir(sys_dir + "/devices"):
×
327
        mount("/sys", sys_dir, "sysfs")
×
328

329
    # add device to cgroup allow list
330
    allow_device(path, domid)
×
331

332

333
def detach(device, domid):
1✔
334
    path = dev_path(device)
×
335
    uid, gid = load_device_ids(device)
×
336

337
    # restore uid, gid of the device file.
338
    try:
×
339
        os.chown(path, uid, gid)
×
340
    except OSError as e:
×
341
        log.error("Failed to chown device file {}: {}".format(path, str(e)))
×
342
        exit(1)
×
343

344
    # remove device from cgroup allow list
345
    deny_device(path, domid)
×
346

347

348
def cleanup(domid):
1✔
349
    # remove the cgroup if one has been created.
350
    if os.path.isdir(get_cg_dir(domid)):
×
351
        try:
×
352
            os.rmdir(get_cg_dir(domid))
×
353
        except OSError as e:
×
354
            # log and continue
355
            log.error("Failed to remove cgroup qemu-{}: {}"
×
356
                      .format(domid, str(e)))
357

358
    # umount /dev, /sys from chroot directory if they are mounted.
359
    root_dir = get_root_dir(domid)
×
360
    dev_dir = root_dir + "/dev"
×
361
    sys_dir = root_dir + "/sys"
×
362
    if os.path.isdir(dev_dir + "/bus"):
×
363
        umount(dev_dir)
×
364
    if os.path.isdir(sys_dir + "/devices"):
×
365
        umount(sys_dir)
×
366

367

368
if __name__ == "__main__":
1✔
369
    log.logToSyslog(level=logging.DEBUG)
×
370

371
    arg = parse_arg()
×
372

373
    if "attach" == arg.command:
×
374
        attach(arg.device, arg.domid, arg.pid, arg.reset_only)
×
375
    elif "detach" == arg.command:
×
376
        detach(arg.device, arg.domid)
×
377
    elif "cleanup" == arg.command:
×
378
        cleanup(arg.domid)
×
379
    else:
380
        log.error("Unexpected command: {}".format(arg.command))
×
381
        exit(1)
×
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