How to use:
1. prepare test image (any distribution with working network), and
modify IMAGE variable in script
You'll need to setup static ip in the image, for example in Ubuntu:
cat /etc/netplan/01-netcfg.yaml
network:
version: 2
renderer: networkd
ethernets:
ens1:
dhcp4: no
addresses: [10.0.1.2/24]
gateway4: 10.0.1.1
nameservers:
addresses: [8.8.8.8, 8.8.4.4]
2. run script with sudo (to manipulate with TAPs)
cd python
sudo ./tap-migration-stand.py --qemu-binary path/to/qemu-system-x86_64 \
--mode (new | cpr)
mode=new means current series, mode=cpr means
"[RFC V2 0/8] Live update: tap and vhost"
3. run in separate terminal something like
while true; do gvncviewer :0; gvncviewer :1; sleep 0.5; done
to monitor our vms (on live update, vnc port will change every time)
4. run ping processes, for example
in host: ping -f 10.0.1.2 -i 0.002
in vm: ping -f 10.0.1.1 -i 0
5. in running script, run command "u" (without quotes), which means
live-update.
6. you may also pass a counter, like "u 1000", to update 1000 times,
which helps to catch racy bugs.
Signed-off-by: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
---
python/qemu/machine/machine.py | 37 ++--
python/tap-migration-stand.py | 348 +++++++++++++++++++++++++++++++++
2 files changed, 369 insertions(+), 16 deletions(-)
create mode 100755 python/tap-migration-stand.py
diff --git a/python/qemu/machine/machine.py b/python/qemu/machine/machine.py
index ebb58d5b68..fed54f768d 100644
--- a/python/qemu/machine/machine.py
+++ b/python/qemu/machine/machine.py
@@ -360,6 +360,7 @@ def _pre_launch(self) -> None:
# _post_shutdown()!
# pylint: disable=consider-using-with
self._qemu_log_path = os.path.join(self.log_dir, self._name + ".log")
+ print(f"Open log file: {self._qemu_log_path}")
self._qemu_log_file = open(self._qemu_log_path, 'wb')
self._iolog = None
@@ -435,17 +436,17 @@ def _post_shutdown(self) -> None:
self._user_killed = False
self._launched = False
- def launch(self) -> None:
+ def launch(self, do_launch=True, do_post_launch=True) -> None:
"""
Launch the VM and make sure we cleanup and expose the
command line/output in case of exception
"""
- if self._launched:
+ if self._launched and do_launch:
raise QEMUMachineError('VM already launched')
try:
- self._launch()
+ self._launch(do_launch, do_post_launch)
except BaseException as exc:
# We may have launched the process but it may
# have exited before we could connect via QMP.
@@ -468,23 +469,26 @@ def launch(self) -> None:
# that exception. However, we still want to clean up.
raise
- def _launch(self) -> None:
+ def _launch(self, do_launch=True, do_post_launch=True) -> None:
"""
Launch the VM and establish a QMP connection
"""
- self._pre_launch()
- LOG.debug('VM launch command: %r', ' '.join(self._qemu_full_args))
+ if do_launch:
+ self._pre_launch()
+ print('VM launch command: %r', ' '.join(self._qemu_full_args))
- # Cleaning up of this subprocess is guaranteed by _do_shutdown.
- # pylint: disable=consider-using-with
- self._popen = subprocess.Popen(self._qemu_full_args,
- stdin=subprocess.DEVNULL,
- stdout=self._qemu_log_file,
- stderr=subprocess.STDOUT,
- shell=False,
- close_fds=False)
- self._launched = True
- self._post_launch()
+ # Cleaning up of this subprocess is guaranteed by _do_shutdown.
+ # pylint: disable=consider-using-with
+ self._popen = subprocess.Popen(self._qemu_full_args,
+ stdin=subprocess.DEVNULL,
+ stdout=self._qemu_log_file,
+ stderr=subprocess.STDOUT,
+ shell=False,
+ close_fds=False)
+ self._launched = True
+
+ if do_post_launch:
+ self._post_launch()
def _close_qmp_connection(self) -> None:
"""
@@ -732,6 +736,7 @@ def cmd(self, cmd: str,
conv_keys = True
qmp_args = self._qmp_args(conv_keys, args)
+ print(cmd, qmp_args)
ret = self._qmp.cmd(cmd, **qmp_args)
if cmd == 'quit':
self._quit_issued = True
diff --git a/python/tap-migration-stand.py b/python/tap-migration-stand.py
new file mode 100755
index 0000000000..24e0e58e40
--- /dev/null
+++ b/python/tap-migration-stand.py
@@ -0,0 +1,348 @@
+#!/usr/bin/env python3
+import argparse
+import subprocess
+import time
+from enum import Enum
+from typing import Tuple
+
+from qemu.machine import QEMUMachine
+
+
+IMAGE = "/home/vsementsov/work/vms/newfocal.raw"
+
+
+def run(cmd: str, check: bool = True) -> None:
+ subprocess.run(cmd, check=check, shell=True)
+
+
+def del_tap(tap: str) -> None:
+ run(f"sudo ip tuntap del {tap} mode tap multi_queue", check=False)
+
+
+def init_tap(tap: str) -> None:
+ run(f"sudo ip tuntap add dev {tap} mode tap multi_queue")
+ run(f"sudo ip link set dev {tap} address e6:1d:44:b5:03:5d")
+ run(f"sudo ip addr add 10.0.1.1/24 dev {tap}")
+ run(f"sudo ip link set {tap} up")
+
+
+class MigrationFailed(Exception):
+ pass
+
+
+class MyVM(QEMUMachine):
+ class Mode(Enum):
+ CPR = "cpr"
+ CPR_NO_TAP = "cpr-no-tap"
+ NO_TAP = "no-tap"
+ OPEN_SAME_TAP = "open-same-tap"
+ OPEN_NEW_TAP = "open-new-tap"
+ NEW = "new"
+
+ def __init__(
+ self,
+ binary: str,
+ mode: Mode,
+ incoming: bool = False,
+ ind: int = 0,
+ vhost: bool = False,
+ ):
+ assert ind in (0, 1)
+ self.tap_name = f"tap{ind}" if mode == MyVM.Mode.OPEN_NEW_TAP else "tap0"
+ self.cpr = mode in (MyVM.Mode.CPR, MyVM.Mode.CPR_NO_TAP)
+ self.no_tap = mode in (MyVM.Mode.NO_TAP, MyVM.Mode.CPR_NO_TAP)
+ self.mode = mode
+ self.ind = ind
+ self.qemu_binary = binary
+ self.vhost = vhost
+ self.fds = None
+ auxshare_str = "-machine aux-ram-share=on" if self.cpr else ""
+
+ additional_args = []
+ if incoming:
+ additional_args = ["-incoming", "defer"]
+ if self.cpr:
+ additional_args += [
+ "-incoming",
+ '{"channel-type": "cpr","addr": '
+ '{ "transport": "socket","type": "unix", "path": "/tmp/cpr.sock"}}',
+ ]
+
+ new_traces = "-trace tap_*" if mode == MyVM.Mode.NEW else ""
+
+ super().__init__(
+ binary=binary,
+ log_dir="/tmp/logdir/",
+ name=f"mytest{ind}",
+ args=f"""
+ -device pxb-pcie,bus_nr=128,bus=pcie.0,id=pcie.1
+ -device pcie-root-port,id=s0,slot=0,bus=pcie.1
+ -device pcie-root-port,id=s1,slot=1,bus=pcie.1
+ -device pcie-root-port,id=s2,slot=2,bus=pcie.1
+
+ -hda {IMAGE}
+ -m 4G -enable-kvm -M q35 -vnc :{ind} -nodefaults -vga std
+ -qmp stdio
+ -msg timestamp
+ -S
+ -trace migrate_*
+ -trace migration_cleanup
+ -trace migration_cancel
+ -trace handle_qmp_command
+ -trace monitor_qmp_respond
+ {new_traces}
+ -object memory-backend-file,id=ram0,size=4G,mem-path=/dev/shm/ram0,share=on
+ -machine memory-backend=ram0 {auxshare_str}
+ """.split()
+ + additional_args,
+ )
+
+ def add_tap_netdev(self, tap, vhost: bool, local_incoming: bool = False):
+ args = {
+ "id": "netdev.1",
+ "vhost": vhost,
+ "vhostforce": vhost,
+ "type": "tap",
+ "ifname": tap,
+ "script": "no",
+ "downscript": "no",
+ "queues": 4,
+ }
+
+ if self.cpr:
+ args["cpr"] = True
+ elif local_incoming:
+ args["local-incoming"] = True
+
+ self.cmd("netdev_add", args)
+
+ self.cmd(
+ "device_add",
+ driver="virtio-net-pci",
+ romfile="",
+ id="vnet.1",
+ netdev="netdev.1",
+ mq=True,
+ vectors=18,
+ bus="s1",
+ mac="d6:0d:75:f8:0f:b7",
+ disable_legacy="off",
+ )
+
+ def setup_network_first_time(self):
+ if self.no_tap:
+ return
+
+ del_tap("tap0")
+ del_tap("tap1")
+ assert self.tap_name == "tap0"
+ init_tap("tap0")
+
+ self.add_tap_netdev(self.tap_name, self.vhost)
+
+ def setup_network_incoming(self, fds=None):
+ if self.no_tap:
+ return
+
+ if self.mode == MyVM.Mode.OPEN_NEW_TAP:
+ run(f"sudo ip tuntap add dev {self.tap_name} mode tap multi_queue")
+ run(f"sudo ip link set {self.tap_name} up")
+ tap = self.tap_name
+ else:
+ tap = "tap0"
+
+ self.add_tap_netdev(
+ tap, self.vhost, local_incoming=(self.mode == MyVM.Mode.NEW)
+ )
+
+ def pre_start_network_switch(self):
+ assert self.mode == MyVM.Mode.OPEN_NEW_TAP
+
+ a = time.time()
+ prev_tap = f"tap{1 - self.ind}"
+ run(f"sudo ip link set {self.tap_name} down")
+ run(f"sudo ip link set {prev_tap} down")
+ run(f"sudo ip addr delete 10.0.1.1/24 dev {prev_tap}")
+ run(f"sudo ip link set dev {self.tap_name} address e6:1d:44:b5:03:5d")
+ run(f"sudo ip addr add 10.0.1.1/24 dev {self.tap_name}")
+ run(f"sudo ip link set {self.tap_name} up")
+ b = time.time()
+ print("network switch:", b - a)
+
+ def wait_migration_complete(self) -> bool:
+ while True:
+ event = self.event_wait("MIGRATION", timeout=1000)
+ print("source:", event)
+ assert event
+ if event["data"]["status"] == "completed":
+ return True
+ if event["data"]["status"] == "failed":
+ print("MIGRATION FAILED!")
+ print(self.cmd("query-migrate"))
+ return False
+
+ def mig_cap(self):
+ if self.cpr:
+ self.cmd("migrate-set-parameters", {"mode": "cpr-transfer"})
+ cap_list = ["events", "x-ignore-shared"]
+ if self.mode == MyVM.Mode.NEW:
+ cap_list.append("local-tap")
+ caps = [{"capability": c, "state": True} for c in cap_list]
+ self.cmd("migrate-set-capabilities", {"capabilities": caps})
+
+ def migrate(self):
+ self.mig_cap()
+ if self.cpr:
+ self.cmd(
+ "migrate",
+ {
+ "channels": [
+ {
+ "channel-type": "main",
+ "addr": {
+ "transport": "socket",
+ "type": "unix",
+ "path": "/tmp/migr.sock",
+ },
+ },
+ {
+ "channel-type": "cpr",
+ "addr": {
+ "transport": "socket",
+ "type": "unix",
+ "path": "/tmp/cpr.sock",
+ },
+ },
+ ]
+ },
+ )
+ else:
+ self.cmd("migrate", uri="unix:/tmp/migr.sock")
+
+ def live_update(self) -> Tuple["MyVM", float]:
+ ind = 1 - self.ind
+ target = MyVM(
+ binary=self.qemu_binary,
+ ind=ind,
+ mode=self.mode,
+ incoming=True,
+ vhost=self.vhost,
+ )
+
+ if self.cpr:
+ print("launch target (cpr)")
+ target.launch(do_post_launch=False)
+ time.sleep(1)
+
+ print("call migrate on source, will pass fds")
+ self.migrate()
+
+ print("vm:", self.cmd("query-status"), self.cmd("query-migrate"))
+ print("post launch and qmp connect to target..")
+ target.launch(do_launch=False)
+ else:
+ print("launch target (usual)")
+ target.launch()
+
+ target.setup_network_incoming(self.fds)
+
+ target.mig_cap()
+
+ if self.cpr:
+ freeze_start = time.time()
+ target.cmd("migrate-incoming", {"uri": "unix:/tmp/migr.sock"})
+ else:
+ target.cmd("migrate-incoming", {"uri": "unix:/tmp/migr.sock"})
+ freeze_start = time.time()
+ self.migrate()
+
+ print("wait migration on source")
+ if not self.wait_migration_complete():
+ target.shutdown()
+ raise MigrationFailed
+
+ print("wait source STOP")
+ stop_event = self.event_wait("STOP", timeout=1000)
+ assert stop_event
+ print(stop_event)
+
+ print("wait migration on target")
+ assert target.wait_migration_complete()
+
+ result = self.qmp("query-status")
+ assert result["return"]["status"] == "postmigrate"
+
+ result = target.qmp("query-status")
+ assert result["return"]["status"] == "paused"
+
+ if self.mode == MyVM.Mode.OPEN_NEW_TAP:
+ target.pre_start_network_switch()
+
+ print("target CONT")
+ target.qmp("cont")
+ freeze_end = time.time()
+
+ freeze_time = freeze_end - freeze_start
+ print("freeze-time: ", freeze_time)
+
+ self.shutdown()
+
+ if self.mode == MyVM.Mode.OPEN_NEW_TAP:
+ del_tap(self.tap_name)
+
+ print(target.cmd("query-version"))
+ print(target.cmd("query-status"))
+ return target, freeze_time
+
+
+def main():
+ # cleanup previous test runs
+ run("rm -rf /tmp/logdir", check=False)
+ run("mkdir /tmp/logdir", check=False)
+ run("killall qemu-system-x86_64", check=False)
+
+ p = argparse.ArgumentParser()
+ p.add_argument("--qemu-binary", required=True)
+ p.add_argument("--vhost", action="store_true")
+ p.add_argument("--mode", choices=[e.value for e in MyVM.Mode], required=True)
+ args = p.parse_args()
+
+ print("vhost:", args.vhost)
+ print("mode:", args.mode)
+
+ vm = MyVM(binary=args.qemu_binary, mode=MyVM.Mode(args.mode), vhost=args.vhost)
+ vm.launch()
+ vm.setup_network_first_time()
+ vm.cmd("cont")
+
+ while True:
+ cmd = input().strip()
+ if cmd == "q":
+ break
+
+ if cmd == "s":
+ print(vm.cmd("query-status"))
+ vm.cmd("cont")
+ continue
+
+ if cmd.startswith("u"):
+ spl = cmd.split()
+ assert len(spl) <= 2
+ num = int(cmd.split(maxsplit=1)[1]) if len(spl) == 2 else 1
+ total_freeze_time = 0
+ try:
+ for i in range(num):
+ vm, freeze_time = vm.live_update()
+ print("DONE:", i)
+ total_freeze_time += freeze_time
+ except MigrationFailed:
+ continue
+
+ print(f"avg freeze-time: {total_freeze_time / num}")
+ continue
+
+ vm.shutdown()
+
+
+if __name__ == "__main__":
+ main()
--
2.48.1
© 2016 - 2025 Red Hat, Inc.