Virtuozzo QEMU development (svt-core@virtuozzo.com)
 help / color / mirror / Atom feed
From: "Denis V. Lunev" <den@openvz.org>
To: svt-core@virtuozzo.com
Cc: andrey.drobyshev@virtuozzo.com, den@openvz.org
Subject: [PATCH hci-8.0 v2 3/3] iotests/migrate-cpr-exec: cover chardevs with logfile= #VSTOR-148304
Date: Mon,  5 Oct 2026 11:26:35 +0200	[thread overview]
Message-ID: <20261005092635.3564485-4-den@openvz.org> (raw)
In-Reply-To: <20261005092635.3564485-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     | 154 ++++++++++++++++++
 tests/qemu-iotests/tests/migrate-cpr-exec.out |   4 +-
 2 files changed, 156 insertions(+), 2 deletions(-)

diff --git a/tests/qemu-iotests/tests/migrate-cpr-exec b/tests/qemu-iotests/tests/migrate-cpr-exec
index 51e6cbcd659..2b30929561c 100755
--- a/tests/qemu-iotests/tests/migrate-cpr-exec
+++ b/tests/qemu-iotests/tests/migrate-cpr-exec
@@ -288,6 +288,160 @@ 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'
+        files = set()
+        for fd in os.listdir(fd_dir):
+            try:
+                files.add(os.readlink(os.path.join(fd_dir, fd)))
+            except FileNotFoundError:
+                pass
+        return files
+
+    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_chardev_logfile_not_preserved(self) -> None:
+        """A log the source did not save is dropped, not reopened by path"""
+        log = self._log_path('unsaved.log')
+        arg = 'pty,id=char0'
+
+        def add_log():
+            self._replace_arg(arg, f'{arg},logfile={log}')
+
+        def post_cpr():
+            self.assertFalse(os.path.exists(log))
+
+        self.vm.add_args('-chardev', arg,
+                         '-device', 'isa-serial,chardev=char0')
+        self._test_cpr(pre_cpr_hook=add_log, post_cpr_hook=post_cpr)
+        self.assertIn("chardev 'char0': log file was not preserved, "
+                      "continuing without it", self.vm.get_log())
+
     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..cfa5c0d0e66 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 35 tests
 
 OK
-- 
2.53.0


  parent reply	other threads:[~2026-10-05  9:26 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-05  9:26 [PATCH hci-8.0 v2 0/3] chardev: fix qemu-update destroying VMs with a console log #VSTOR-148304 Denis V. Lunev
2026-10-05  9:26 ` [PATCH hci-8.0 v2 1/3] chardev: implement cpr_claim_fds() in the base class #VSTOR-148304 Denis V. Lunev
2026-10-05  9:28   ` no-reply
2026-10-05  9:26 ` [PATCH hci-8.0 v2 2/3] chardev: preserve the logfile fd across cpr-exec #VSTOR-148304 Denis V. Lunev
2026-10-05  9:30   ` no-reply
2026-10-05  9:26 ` Denis V. Lunev [this message]
2026-10-05  9:35   ` [PATCH hci-8.0 v2 3/3] iotests/migrate-cpr-exec: cover chardevs with logfile= #VSTOR-148304 no-reply
2026-10-05  9:47 ` [PATCH hci-8.0 v2 0/3] chardev: fix qemu-update destroying VMs with a console log #VSTOR-148304 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=20261005092635.3564485-4-den@openvz.org \
    --to=den@openvz.org \
    --cc=andrey.drobyshev@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