From: "Denis V. Lunev" <den@openvz.org>
To: svt-core@virtuozzo.com
Cc: andrey.drobyshev@virtuozzo.com, den@virtuozzo.com
Subject: [PATCH 3/3] iotests/migrate-cpr-exec: cover chardevs with logfile= #VSTOR-148304
Date: Fri, 2 Oct 2026 18:48:53 +0200 [thread overview]
Message-ID: <20261002164853.2130337-4-den@openvz.org> (raw)
In-Reply-To: <20261002164853.2130337-1-den@openvz.org>
From: Denis V. Lunev <den@openvz.org>
Run a console pty with its log in an fdset, a guest agent socket and a
second QMP monitor, each with a log, through cpr-exec with a command
line whose logfile paths cannot be created, as libvirt's is. Cover
chardev-change to a new log for pty and socket, and a failed
chardev-change, whose rollback must keep the old log preserved.
Signed-off-by: Denis V. Lunev <den@openvz.org>
---
tests/qemu-iotests/tests/migrate-cpr-exec | 132 ++++++++++++++++++
tests/qemu-iotests/tests/migrate-cpr-exec.out | 4 +-
2 files changed, 134 insertions(+), 2 deletions(-)
diff --git a/tests/qemu-iotests/tests/migrate-cpr-exec b/tests/qemu-iotests/tests/migrate-cpr-exec
index 51e6cbcd659..e8a6c502c84 100755
--- a/tests/qemu-iotests/tests/migrate-cpr-exec
+++ b/tests/qemu-iotests/tests/migrate-cpr-exec
@@ -288,6 +288,138 @@ class TestCprExec(iotests.QMPTestCase):
'-device', 'isa-serial,chardev=char0')
self._test_cpr(pre_cpr_hook=change_socket)
+ BAD_LOG = os.path.join(iotests.test_dir, 'nonexistent', 'chr.log')
+
+ def _serial_write(self, data: str) -> None:
+ for c in data:
+ self.vm.qtest(f'outb 0x3f8 {ord(c)}')
+
+ def _open_files(self) -> set:
+ fd_dir = f'/proc/{self.vm.get_pid()}/fd'
+ return {os.readlink(os.path.join(fd_dir, fd))
+ for fd in os.listdir(fd_dir)}
+
+ def _replace_arg(self, old: str, new: str) -> None:
+ args = list(self.vm._qemu_full_args)
+ args[args.index(old)] = new
+ self.vm._qemu_full_args = tuple(args)
+
+ def _read(self, path: str) -> str:
+ with open(path, encoding='utf-8') as f:
+ return f.read()
+
+ def _log_path(self, name: str) -> str:
+ path = os.path.join(iotests.test_dir, name)
+ self.addCleanup(lambda: os.path.exists(path) and os.remove(path))
+ return path
+
+ def test_chardev_logfile(self) -> None:
+ """Log fds survive cpr-exec; the new logfile= paths are never opened"""
+ console_log = self._log_path('console.log')
+ agent_log = self._log_path('agent.log')
+ mon_log = self._log_path('mon.log')
+ agent_sock = iotests.file_path('agent.sock')
+ mon_sock = iotests.file_path('mon.sock')
+
+ console = 'pty,id=charserial0,logfile=/dev/fdset/1,logappend=on'
+ agent = (f'socket,id=charchannel0,path={agent_sock},server=on,'
+ f'wait=off,logfile={agent_log}')
+ mon = (f'socket,id=charmonitor1,path={mon_sock},server=on,'
+ f'wait=off,logfile={mon_log}')
+
+ def pre_cpr():
+ self._serial_write('before\n')
+ for c in (console, agent, mon):
+ self._replace_arg(c, c.replace(c[c.index('logfile='):],
+ f'logfile={self.BAD_LOG}'))
+ args = list(self.vm._qemu_full_args)
+ i = args.index('-add-fd')
+ self.vm._qemu_full_args = tuple(args[:i] + args[i + 2:])
+
+ def post_cpr():
+ self._serial_write('after\n')
+ self.assertEqual(self._read(console_log), 'before\nafter\n')
+ self.assertLessEqual({agent_log, mon_log}, self._open_files())
+ self.assertFalse(os.path.exists(os.path.dirname(self.BAD_LOG)))
+
+ fd = os.open(console_log, os.O_WRONLY | os.O_CREAT | os.O_APPEND)
+ try:
+ self.vm.add_fd(fd, 1, 'console')
+ self.vm.add_args(
+ '-chardev', console,
+ '-device', 'isa-serial,chardev=charserial0',
+ '-chardev', agent, '-device', 'virtio-serial',
+ '-device', 'virtserialport,chardev=charchannel0,'
+ 'name=org.qemu.guest_agent.0',
+ '-chardev', mon, '-mon', 'chardev=charmonitor1,mode=control')
+ self._test_cpr(pre_cpr_hook=pre_cpr, post_cpr_hook=post_cpr)
+ finally:
+ os.close(fd)
+
+ def _test_chardev_change_logfile(self, backend: str, data_new: dict,
+ opts_old: str) -> None:
+ """cpr-exec after chardev-change preserves the new log, not the old"""
+ log_old = self._log_path('chr-old.log')
+ log_new = self._log_path('chr-new.log')
+ arg = f'{backend},id=char0,{opts_old}logfile={log_old}'
+
+ def change():
+ self._serial_write('before\n')
+ res = self.vm.qmp('chardev-change', id='char0',
+ backend={'type': backend,
+ 'data': {**data_new,
+ 'logfile': log_new}})
+ assert 'return' in res, res
+ self._replace_arg(arg, f'{backend},id=char0,{opts_old}'
+ f'logfile={self.BAD_LOG}')
+
+ def post_cpr():
+ self._serial_write('after\n')
+ self.assertEqual(self._read(log_old), 'before\n')
+ self.assertEqual(self._read(log_new), 'after\n')
+ self.assertNotIn(log_old, self._open_files())
+
+ self.vm.add_args('-chardev', arg,
+ '-device', 'isa-serial,chardev=char0')
+ self._test_cpr(pre_cpr_hook=change, post_cpr_hook=post_cpr)
+
+ def test_chardev_change_logfile_pty(self) -> None:
+ self._test_chardev_change_logfile('pty', {}, '')
+
+ def test_chardev_change_logfile_socket(self) -> None:
+ sock1 = iotests.file_path('chr-log-sock1')
+ sock2 = iotests.file_path('chr-log-sock2')
+ self._test_chardev_change_logfile(
+ 'socket',
+ {'addr': {'type': 'unix', 'data': {'path': sock2}},
+ 'server': True, 'wait': False},
+ f'path={sock1},server=on,wait=off,')
+
+ def test_chardev_change_logfile_rollback(self) -> None:
+ """A rolled back chardev-change keeps the old log preserved"""
+ log = self._log_path('chr.log')
+ log_new = self._log_path('chr-new.log')
+ link = self._log_path('pty-rollback.link')
+ open(link, 'w').close()
+ arg = f'pty,id=char0,logfile={log}'
+
+ def failed_change():
+ self._serial_write('before\n')
+ res = self.vm.qmp('chardev-change', id='char0',
+ backend={'type': 'pty',
+ 'data': {'path': link,
+ 'logfile': log_new}})
+ assert 'error' in res, res
+ self._replace_arg(arg, f'pty,id=char0,logfile={self.BAD_LOG}')
+
+ def post_cpr():
+ self._serial_write('after\n')
+ self.assertEqual(self._read(log), 'before\nafter\n')
+
+ self.vm.add_args('-chardev', arg,
+ '-device', 'isa-serial,chardev=char0')
+ self._test_cpr(pre_cpr_hook=failed_change, post_cpr_hook=post_cpr)
+
def test_nbd_attached_hd(self) -> None:
nbd_img = os.path.join(iotests.test_dir, 'nbd_disk')
iotests.qemu_img_create('-f', 'qcow2', nbd_img, '64M')
diff --git a/tests/qemu-iotests/tests/migrate-cpr-exec.out b/tests/qemu-iotests/tests/migrate-cpr-exec.out
index 5ce2f9a2ed9..88bf7fa73a5 100644
--- a/tests/qemu-iotests/tests/migrate-cpr-exec.out
+++ b/tests/qemu-iotests/tests/migrate-cpr-exec.out
@@ -1,5 +1,5 @@
-..............................
+..................................
----------------------------------------------------------------------
-Ran 30 tests
+Ran 34 tests
OK
--
2.53.0
next prev parent reply other threads:[~2026-10-02 16:49 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-02 16:48 [PATCH 0/3] chardev: fix qemu-update destroying VMs with a console log #VSTOR-148304 Denis V. Lunev
2026-10-02 16:48 ` [PATCH 1/3] chardev: implement cpr_claim_fds() in the base class #VSTOR-148304 Denis V. Lunev
2026-10-02 16:50 ` no-reply
2026-10-05 9:05 ` Andrey Drobyshev
2026-10-02 16:48 ` [PATCH 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304 Denis V. Lunev
2026-10-02 16:54 ` no-reply
2026-10-05 9:05 ` Andrey Drobyshev
2026-10-02 16:48 ` Denis V. Lunev [this message]
2026-10-02 16:55 ` [PATCH 3/3] iotests/migrate-cpr-exec: cover chardevs with logfile= #VSTOR-148304 no-reply
2026-10-05 9:05 ` Andrey Drobyshev
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261002164853.2130337-4-den@openvz.org \
--to=den@openvz.org \
--cc=andrey.drobyshev@virtuozzo.com \
--cc=den@virtuozzo.com \
--cc=svt-core@virtuozzo.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox