A cpr-transfer target takes the vsock device over in post_load(). Make
sure the source gets it back when the target goes away afterwards.
Three cases, in the first two the target is made to fail the load by
lacking a device the source has:
- the target exits on the failure (default exit-on-error); the source
is stopped beforehand, so it stays paused until we 'cont' it once
the target is gone;
- the target stays alive (exit-on-error=false) and the source resumes
on its own (return-path capability);
- the target loads the state while the source is stopped, stays
paused and is quit normally; the source is resumed instead.
In all cases a host<->guest transfer must then work on the source.
The transfer helpers get a payload size parameter, since the plain
checks don't need the 512M of the stress cases, and the target start
code is moved into a helper shared with the existing cpr-transfer test.
Signed-off-by: Andrey Drobyshev <[email protected]>
---
tests/functional/x86_64/test_vhost_vsock_cpr.py | 178 +++++++++++++++++++-----
1 file changed, 140 insertions(+), 38 deletions(-)
diff --git a/tests/functional/x86_64/test_vhost_vsock_cpr.py
b/tests/functional/x86_64/test_vhost_vsock_cpr.py
index 035307fc82f..3ac0a80c5ed 100755
--- a/tests/functional/x86_64/test_vhost_vsock_cpr.py
+++ b/tests/functional/x86_64/test_vhost_vsock_cpr.py
@@ -24,6 +24,7 @@
VSOCK_PORT = 5000
TRANSFER_SIZE = 512 * 1024 * 1024 # file size for host<->guest transfer
+CHECK_SIZE = 16 * 1024 * 1024 # enough to just check that vsock works
CPR_INTERVAL = 0.2 # interval between CPR ops
VHOST_SET_OWNER = 0xAF01 # _IO(VHOST_VIRTIO, 0x01)
@@ -71,11 +72,6 @@ def setUp(self):
'qcow2', '-b', self.ASSET_DISKIMAGE.fetch(),
'-F', 'qcow2', self.disk])
- self.payload = self.scratch_file('payload')
- with open(self.payload, 'wb') as f:
- for _ in range(TRANSFER_SIZE >> 20):
- f.write(os.urandom(1 << 20))
-
self.sock_dir = self.socket_dir().name
self.cpr_count = 0
@@ -121,12 +117,16 @@ def attempt():
return None
return self.wait_for('QMP to come up', attempt)
- def wait_migration(self, qmp):
- def completed():
+ def wait_migration(self, qmp, expect='completed'):
+ def finished():
status = qmp.cmd('query-migrate').get('status')
- self.assertNotEqual(status, 'failed', 'migration failed')
- return status == 'completed'
- self.wait_for('migration completion', completed)
+ if status in ('completed', 'failed'):
+ if expect:
+ self.assertEqual(status, expect,
+ 'unexpected migration result')
+ return True
+ return False
+ self.wait_for('migration completion', finished)
def wait_running(self, qmp_path):
# Make sure we're dealing with the new QEMU after CPR
@@ -139,13 +139,37 @@ def wait_running(self, qmp_path):
except QMP_GONE:
pass
- def boot(self):
+ def boot(self, *extra_args):
self.guest = self.vm
- self.qmp_path = self.prepare_vm(self.guest, 'source')
+ self.qmp_path = self.prepare_vm(self.guest, 'source', *extra_args)
self.guest.launch()
self.wait_for_console_pattern('login:')
exec_command_and_wait_for_pattern(self, 'root', 'root@localhost:~#')
+ def start_target(self, name, main_incoming=None):
+ """
+ Start a cpr-transfer target listening on the CPR channel. Return
+ the VM, its QMP socket path and the channels for 'migrate'.
+ """
+ mig_sock = os.path.join(self.sock_dir, f'{name}-mig.sock')
+ cpr_sock = os.path.join(self.sock_dir, f'{name}-cpr.sock')
+ channels = [
+ {'channel-type': 'main',
+ 'addr': {'transport': 'socket', 'type': 'unix',
+ 'path': mig_sock}},
+ {'channel-type': 'cpr',
+ 'addr': {'transport': 'socket', 'type': 'unix',
+ 'path': cpr_sock}}]
+
+ target = self.get_vm(name=name)
+ qmp_path = self.prepare_vm(
+ target, name, '-incoming', main_incoming or f'unix:{mig_sock}',
+ '-incoming', 'cpr,addr.transport=socket,addr.type=unix,'
+ f'addr.path={cpr_sock}')
+ target.launch()
+ self.wait_for('the CPR socket', lambda: os.path.exists(cpr_sock))
+ return target, qmp_path, channels
+
def transfer(self, guest_cmd, host_cmd, cpr):
"""
Run a socat transfer between the guest and the host, and keep
@@ -160,21 +184,26 @@ def transfer(self, guest_cmd, host_cmd, cpr):
exec_command_and_wait_for_pattern(self, 'wait %1; echo RC_$?',
'RC_0', vm=self.guest)
- def transfer_both_ways(self, cpr):
+ def transfer_both_ways(self, cpr, size=TRANSFER_SIZE):
"""
- Send the payload to a file on the guest's disk, get it back into
- another file on the host, then compare.
+ Send a random payload to a file on the guest's disk, get it back
+ into another file on the host, then compare.
"""
+ payload = self.scratch_file('payload')
+ with open(payload, 'wb') as f:
+ for _ in range(size >> 20):
+ f.write(os.urandom(1 << 20))
+
echo = self.scratch_file('echo')
connect = f'VSOCK-CONNECT:{GUEST_CID}:{VSOCK_PORT},retry=10'
self.transfer(f'socat -u VSOCK-LISTEN:{VSOCK_PORT} CREATE:/root/data',
- ['socat', '-u', f'FILE:{self.payload}', connect], cpr)
+ ['socat', '-u', f'FILE:{payload}', connect], cpr)
self.transfer(f'socat -u FILE:/root/data VSOCK-LISTEN:{VSOCK_PORT}',
['socat', '-u', connect, f'CREATE:{echo}'], cpr)
self.log.info('%d CPR migrations performed', self.cpr_count)
- self.assertTrue(filecmp.cmp(self.payload, echo, shallow=False),
+ self.assertTrue(filecmp.cmp(payload, echo, shallow=False),
'the data came back different')
def test_cpr_transfer(self):
@@ -184,29 +213,11 @@ def test_cpr_transfer(self):
it and shutting down the source
"""
def cpr():
- n = self.cpr_count
- mig_sock = os.path.join(self.sock_dir, f'mig{n}.sock')
- cpr_sock = os.path.join(self.sock_dir, f'cpr{n}.sock')
- target = self.get_vm(name=f'target{n}')
- target_qmp_path = self.prepare_vm(
- target, f'target{n}',
- '-incoming', f'unix:{mig_sock}',
- '-incoming', 'cpr,addr.transport=socket,addr.type=unix,'
- f'addr.path={cpr_sock}')
-
- # The target listens on the CPR channel: wait for it to be there
- target.launch()
- self.wait_for('the CPR socket', lambda: os.path.exists(cpr_sock))
-
+ target, target_qmp_path, channels = self.start_target(
+ f'target{self.cpr_count}')
with self.qmp_connect(self.qmp_path) as qmp:
qmp.cmd('migrate-set-parameters', mode='cpr-transfer')
- qmp.cmd('migrate', channels=[
- {'channel-type': 'main',
- 'addr': {'transport': 'socket', 'type': 'unix',
- 'path': mig_sock}},
- {'channel-type': 'cpr',
- 'addr': {'transport': 'socket', 'type': 'unix',
- 'path': cpr_sock}}])
+ qmp.cmd('migrate', channels=channels)
self.wait_migration(qmp)
self.wait_running(target_qmp_path)
@@ -217,6 +228,97 @@ def cpr():
self.boot()
self.transfer_both_ways(cpr)
+ def test_cpr_transfer_dead_target(self):
+ """
+ A cpr-transfer target which exits on a load error after having
+ taken the vsock device over must release it on exit, so that the
+ source can be resumed afterwards. The target lacks a device the
+ source has. The source is stopped beforehand, so it stays paused
+ on failure until we 'cont' it.
+ """
+ self.boot('-device', 'virtio-rng-pci')
+ target, _, channels = self.start_target('target')
+
+ with self.qmp_connect(self.qmp_path) as qmp:
+ qmp.cmd('stop')
+ qmp.cmd('migrate-set-parameters', mode='cpr-transfer')
+ qmp.cmd('migrate', channels=channels)
+ self.wait_migration(qmp, expect=None)
+ self.wait_for('the target to exit',
+ lambda: not target.is_running())
+ self.assertEqual(target.exitcode(), 1,
+ 'the target did not exit on the load error')
+ qmp.cmd('cont')
+
+ self.wait_running(self.qmp_path)
+ self.transfer_both_ways(lambda: None, size=CHECK_SIZE)
+
+ def test_cpr_transfer_target_quit(self):
+ """
+ A cpr-transfer target which has loaded the state but hasn't been
+ started yet may quit normally, and the source resume instead.
+ The target doesn't unrealize its devices on quit, so it must
+ release the vsock device on exit.
+ """
+ self.boot()
+ target, target_qmp_path, channels = self.start_target(
+ 'target', main_incoming='defer')
+
+ with self.qmp_connect(self.qmp_path) as qmp:
+ qmp.cmd('stop')
+ qmp.cmd('migrate-set-parameters', mode='cpr-transfer')
+ qmp.cmd('migrate', channels=channels)
+
+ target_qmp = self.qmp_connect(target_qmp_path)
+ target_qmp.cmd('migrate-incoming', channels=channels[:1])
+ self.wait_migration(qmp)
+ self.wait_migration(target_qmp)
+ self.assertEqual(target_qmp.cmd('query-status')['status'],
+ 'paused')
+ try:
+ target_qmp.cmd('quit')
+ target_qmp.close()
+ except QMP_GONE:
+ pass
+
+ self.wait_for('the target to quit',
+ lambda: not target.is_running())
+ self.assertEqual(target.exitcode(), 0, 'the target did not quit')
+ qmp.cmd('cont')
+
+ self.wait_running(self.qmp_path)
+ self.transfer_both_ways(lambda: None, size=CHECK_SIZE)
+
+ def test_cpr_transfer_failed_target(self):
+ """
+ A cpr-transfer target which fails to load the state after having
+ taken the vsock device over must give it back to the source.
+ The target lacks a device the source has, stays alive on failure
+ (exit-on-error=false), and the source resumes on its own
+ (return-path). Then vsock must still work on the source.
+ """
+ self.boot('-device', 'virtio-rng-pci')
+ target, target_qmp_path, channels = self.start_target(
+ 'target', main_incoming='defer')
+
+ with self.qmp_connect(self.qmp_path) as qmp:
+ qmp.cmd('migrate-set-parameters', mode='cpr-transfer')
+ qmp.cmd('migrate-set-capabilities', capabilities=[
+ {'capability': 'return-path', 'state': True}])
+ qmp.cmd('migrate', channels=channels)
+
+ # The target opens its monitor once it has read the CPR state
+ with self.qmp_connect(target_qmp_path) as target_qmp:
+ target_qmp.cmd('migrate-incoming', channels=channels[:1],
+ **{'exit-on-error': False})
+ self.wait_migration(qmp, expect='failed')
+ self.wait_migration(target_qmp, expect='failed')
+
+ # The source resumes by itself, and vsock must work on it
+ self.wait_running(self.qmp_path)
+ self.transfer_both_ways(lambda: None, size=CHECK_SIZE)
+ target.shutdown()
+
def test_cpr_exec(self):
"""
Perform consecutive cpr-exec migration during vsock file transfer,
--
2.47.1