Capture redacted guest serial while waiting for shutdown
This commit is contained in:
+26
-1
@@ -575,10 +575,35 @@ class VM:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
def wait_poweroff(self, timeout):
|
def wait_poweroff(self, timeout):
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
serial_open = True
|
||||||
|
while self.process.poll() is None:
|
||||||
|
remaining = deadline - time.monotonic()
|
||||||
|
if remaining <= 0:
|
||||||
|
raise SmokeError('Guest failed to power off in time')
|
||||||
|
if serial_open:
|
||||||
try:
|
try:
|
||||||
rc = self.process.wait(timeout=timeout)
|
self.console.read(min(remaining, 0.25))
|
||||||
|
except SmokeError as exc:
|
||||||
|
if str(exc) != 'Guest console closed':
|
||||||
|
raise
|
||||||
|
serial_open = False
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.process.wait(timeout=remaining)
|
||||||
except subprocess.TimeoutExpired as exc:
|
except subprocess.TimeoutExpired as exc:
|
||||||
raise SmokeError('Guest failed to power off in time') from exc
|
raise SmokeError('Guest failed to power off in time') from exc
|
||||||
|
# Capture bytes queued immediately before exit, even if poll observed
|
||||||
|
# the child exiting before the first serial read.
|
||||||
|
while serial_open:
|
||||||
|
try:
|
||||||
|
if not self.console.read(0):
|
||||||
|
break
|
||||||
|
except SmokeError as exc:
|
||||||
|
if str(exc) != 'Guest console closed':
|
||||||
|
raise
|
||||||
|
serial_open = False
|
||||||
|
rc = self.process.returncode
|
||||||
if rc:
|
if rc:
|
||||||
raise SmokeError(f'Test QEMU failed during poweroff ({rc})')
|
raise SmokeError(f'Test QEMU failed during poweroff ({rc})')
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ from pathlib import Path
|
|||||||
import socket
|
import socket
|
||||||
import select
|
import select
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
import unittest
|
import unittest
|
||||||
@@ -182,6 +184,46 @@ class SmokeTests(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(self.mod.SmokeError, 'Installer.*status 1'):
|
with self.assertRaisesRegex(self.mod.SmokeError, 'Installer.*status 1'):
|
||||||
console.finish_installer('testtoken', timeout=0.05)
|
console.finish_installer('testtoken', timeout=0.05)
|
||||||
|
|
||||||
|
def shutdown_vm(self, program):
|
||||||
|
one, two = socket.socketpair()
|
||||||
|
vm = self.mod.VM.__new__(self.mod.VM)
|
||||||
|
vm.console = self.mod.Console(one, self.root / 'shutdown.log', ['split-secret'])
|
||||||
|
self.addCleanup(vm.console.close)
|
||||||
|
code = ('import socket,sys,time; s=socket.socket(fileno=int(sys.argv[1])); ' + program)
|
||||||
|
vm.process = subprocess.Popen([sys.executable, '-c', code, str(two.fileno())],
|
||||||
|
pass_fds=(two.fileno(),), stdin=subprocess.DEVNULL)
|
||||||
|
two.close()
|
||||||
|
def reap():
|
||||||
|
if vm.process.poll() is None:
|
||||||
|
vm.process.terminate()
|
||||||
|
vm.process.wait(timeout=2)
|
||||||
|
self.addCleanup(reap)
|
||||||
|
return vm
|
||||||
|
|
||||||
|
def test_poweroff_drains_and_redacts_serial_until_owned_process_exits(self):
|
||||||
|
vm = self.shutdown_vm("s.sendall(b'Stopping split-'); time.sleep(.02); "
|
||||||
|
"s.sendall(b'secret service\\nPower down\\n'); s.close()")
|
||||||
|
vm.wait_poweroff(1)
|
||||||
|
output = (self.root / 'shutdown.log').read_bytes()
|
||||||
|
self.assertIn(b'Stopping [REDACTED] service\nPower down\n', output)
|
||||||
|
self.assertNotIn(b'split-secret', output)
|
||||||
|
|
||||||
|
def test_poweroff_preserves_failure_cause_and_original_timeout(self):
|
||||||
|
for program, error in (("s.sendall(b'Stop job stuck\\n'); s.close(); time.sleep(1)",
|
||||||
|
'Guest failed to power off in time'),
|
||||||
|
("s.sendall(b'QEMU shutdown failed\\n'); s.close(); sys.exit(7)",
|
||||||
|
'Test QEMU failed during poweroff')):
|
||||||
|
with self.subTest(program=program):
|
||||||
|
vm = self.shutdown_vm(program)
|
||||||
|
started = time.monotonic()
|
||||||
|
with self.assertRaisesRegex(self.mod.SmokeError, error):
|
||||||
|
vm.wait_poweroff(.15)
|
||||||
|
self.assertLess(time.monotonic() - started, .5)
|
||||||
|
output = (self.root / 'shutdown.log').read_bytes()
|
||||||
|
self.assertIn(b'Stop job stuck' if 'time.sleep' in program else b'QEMU shutdown failed', output)
|
||||||
|
if 'time.sleep' in program:
|
||||||
|
self.assertIsNone(vm.process.poll(), 'timeout must not kill or falsely pass the owned process')
|
||||||
|
|
||||||
def test_frame_real_shell_roundtrip_does_not_accept_sentinel_in_echo(self):
|
def test_frame_real_shell_roundtrip_does_not_accept_sentinel_in_echo(self):
|
||||||
one, two = socket.socketpair()
|
one, two = socket.socketpair()
|
||||||
self.addCleanup(one.close)
|
self.addCleanup(one.close)
|
||||||
|
|||||||
Reference in New Issue
Block a user