功能:扩展U盘兼容并优化插拔响应和导出负载
This commit is contained in:
1 parent
b2de05c6a6
commit
bb5f554820
9 files changed
+589
-73
No files matched your search
+49
-7
@@ -54,7 +54,14 @@ def peripherals(qmp):
|
||||
return {x["name"] for x in qmp.call("qom-list", {"path": "/machine/peripheral"})}
|
||||
|
||||
|
||||
def attach(qmp, vm, port, bus, vendor, product):
|
||||
def storage_filter(vendor=None, product=None):
|
||||
"""Allow only USB Mass Storage, optionally narrowed to one VID/PID."""
|
||||
if vendor is None or product is None:
|
||||
return "0x08:-1:-1:-1:1|-1:-1:-1:-1:0"
|
||||
return f"0x08:0x{vendor}:0x{product}:-1:1|-1:-1:-1:-1:0"
|
||||
|
||||
|
||||
def attach(qmp, vm, port, bus, vendor=None, product=None):
|
||||
channel, device = ids(vm)
|
||||
channels = {x["label"]: x for x in qmp.call("query-chardev")}
|
||||
if channel in channels or device in peripherals(qmp):
|
||||
@@ -70,7 +77,7 @@ def attach(qmp, vm, port, bus, vendor, product):
|
||||
qmp.call("device_add", {
|
||||
"driver": "usb-redir", "id": device, "chardev": channel, "bus": bus,
|
||||
# QEMU uses ':' within rules; spice-client uses ',' instead.
|
||||
"filter": f"0x08:0x{vendor}:0x{product}:-1:1|-1:-1:-1:-1:0",
|
||||
"filter": storage_filter(vendor, product),
|
||||
})
|
||||
except Exception:
|
||||
# Roll back only the channel created by this invocation.
|
||||
@@ -102,28 +109,63 @@ def status(qmp, vm):
|
||||
}
|
||||
|
||||
|
||||
def ensure(qmp, vm, port, bus, vendor=None, product=None):
|
||||
"""Attach the runtime endpoint if the VM is running and it is absent."""
|
||||
channel, device = ids(vm)
|
||||
if device not in peripherals(qmp):
|
||||
attach(qmp, vm, port, bus, vendor, product)
|
||||
|
||||
|
||||
def watch(vm, port, bus, vendor=None, product=None, interval=3):
|
||||
"""Keep the runtime endpoint attached across VM restarts."""
|
||||
while True:
|
||||
try:
|
||||
qmp = Qmp(vm)
|
||||
try:
|
||||
ensure(qmp, vm, port, bus, vendor, product)
|
||||
finally:
|
||||
qmp.close()
|
||||
except (OSError, RuntimeError) as exc:
|
||||
print(f"waiting for VM/QMP: {exc}", flush=True)
|
||||
time.sleep(interval)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("action", choices=["status", "attach", "detach"])
|
||||
parser.add_argument("action", choices=["status", "attach", "detach", "watch"])
|
||||
parser.add_argument("--vm", type=int, required=True)
|
||||
parser.add_argument("--port", type=int, default=40001)
|
||||
parser.add_argument("--bus", default="ehci-2.0")
|
||||
parser.add_argument("--vendor")
|
||||
parser.add_argument("--product")
|
||||
parser.add_argument("--any-storage", action="store_true",
|
||||
help="Allow any pure USB Mass Storage device")
|
||||
parser.add_argument("--guest-ejected", action="store_true",
|
||||
help="Confirm the guest has safely ejected the USB device before detach")
|
||||
args = parser.parse_args()
|
||||
if args.vm < 100 or not 1 <= args.port <= 65535:
|
||||
parser.error("invalid VM ID or local tunnel port")
|
||||
if args.action == "attach" and not all(
|
||||
re.fullmatch(r"[0-9a-fA-F]{4}", x or "") for x in [args.vendor, args.product]):
|
||||
parser.error("attach requires an exact four-digit --vendor and --product")
|
||||
if args.action in ("attach", "watch"):
|
||||
exact = all(re.fullmatch(r"[0-9a-fA-F]{4}", x or "") for x in [args.vendor, args.product])
|
||||
if args.any_storage and (args.vendor or args.product):
|
||||
parser.error("--any-storage cannot be combined with --vendor/--product")
|
||||
if not args.any_storage and not exact:
|
||||
parser.error("attach/watch requires --any-storage or exact --vendor and --product")
|
||||
elif args.any_storage or args.vendor or args.product:
|
||||
parser.error("--vendor/--product/--any-storage are only valid for attach/watch")
|
||||
if args.action == "watch":
|
||||
watch(args.vm, args.port, args.bus,
|
||||
None if args.any_storage else args.vendor.lower(),
|
||||
None if args.any_storage else args.product.lower())
|
||||
return
|
||||
if args.action == "detach" and not args.guest_ejected:
|
||||
parser.error("safely eject in the guest first, then pass --guest-ejected")
|
||||
qmp = Qmp(args.vm)
|
||||
try:
|
||||
if args.action == "attach":
|
||||
attach(qmp, args.vm, args.port, args.bus, args.vendor.lower(), args.product.lower())
|
||||
attach(qmp, args.vm, args.port, args.bus,
|
||||
None if args.any_storage else args.vendor.lower(),
|
||||
None if args.any_storage else args.product.lower())
|
||||
elif args.action == "detach":
|
||||
detach(qmp, args.vm)
|
||||
print(json.dumps(status(qmp, args.vm), ensure_ascii=False, indent=2))
|
||||
|
||||
Reference in new issue
Block a user