Files
lgcosta 5a7382185b
build / build (clang, ) (push) Failing after 6s
build / build (gcc, ) (push) Failing after 6s
build / build (gcc, --enable-io-failure-emulation) (push) Failing after 6s
build / build (gcc, --enable-warnings) (push) Failing after 5s
Add a libvirt/KVM test environment and fix the gcc-12 warning it found
The VM harness (tests/vm/) builds and exercises the tools on real Debian and
Alpine guests, driving the serial console (no sshd/cloud-init dependency).

Results on Debian 12 (glibc 2.36, gcc 12.2): 12/12 checks pass, 0 warnings
with -Wall -Wextra, including a real 128M -> 256M resize and the rollback
save/restore/reject paths.

Results on Alpine 3.20 (musl 1.2.5): the build cannot start. configure stops
at "reiserfsprogs requires a method to add printf modifiers" because
reiserfscore/prints.c depends on glibc's register_printf_specifier(3) and
<printf.h>, which musl does not provide. This is pre-existing and out of
scope here; measured at 341 use sites across 20+ files, all routed through
reiserfs_warning()/reiserfs_info(). Documented in tests/vm/RESULTS.md.

Fix the one real defect the harness surfaced:

fsck/semantic_check.c: check_check_regular_file() is called with NULL for
new_ih (line 466), so the block that would fill *new_ih is unreachable and
check_semantic_pass() never returns RELOCATED through that path. The
"case RELOCATED:" arm still reads relocated_ih.ih_key, which gcc 12.2
reports as "may be used uninitialized" (-Wmaybe-uninitialized); gcc 16 does
not, because it proves the path dead. Zero-initialise the struct and say why.

Verified: 0 warnings on gcc 12.2 after the change, and the Debian battery
stays at 12/12.
2026-11-12 00:11:31 -03:00

126 lines
3.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Roda um comando no guest atraves do console serial do libvirt.
O console serial e o unico canal garantido nas imagens cloud minimas: nao
depende de sshd, de cloud-init nem de rede. Este script sincroniza com o
prompt, autentica (root/rfstest) e executa o comando, lendo a saida do pty
em blocos pequenos para evitar a sobreposicao de eco que ocorre em comandos
longos.
Uso:
guestcmd.py <dominio> '<comando>'
guestcmd.py <dominio> -f <arquivo> # executa um script local
"""
import base64
import os
import re
import select
import subprocess
import sys
import time
USER = "root"
PASSWORD = "rfstest"
def console_pty(domain):
xml = subprocess.run(["virsh", "dumpxml", domain],
capture_output=True, text=True).stdout
m = (re.search(r"<console[^>]*>\s*<source path='([^']+)'", xml)
or re.search(r"<serial[^>]*>\s*<source path='([^']+)'", xml))
if not m:
sys.exit(f"console serial nao encontrada para {domain}")
return m.group(1)
class Console:
def __init__(self, path):
self.fd = os.open(path, os.O_RDWR | os.O_NOCTTY)
def send(self, s):
os.write(self.fd, s.encode())
def drain(self, seconds):
buf = b""
end = time.time() + seconds
while time.time() < end:
r, _, _ = select.select([self.fd], [], [], 0.3)
if not r:
continue
try:
chunk = os.read(self.fd, 65536)
except OSError:
break
if not chunk:
break
buf += chunk
return buf
def login(self):
self.send("\n")
time.sleep(1.0)
snap = self.drain(2.5).decode(errors="replace").lower()
if "login:" not in snap:
return # ja autenticado
self.send(USER + "\n")
self.drain(3.0)
self.send(PASSWORD + "\n")
time.sleep(2.0)
self.drain(2.0)
def run(self, command, timeout=120.0):
"""Executa e devolve a saida, sincronizando por marcador no pty."""
out_path = "/tmp/.guestcmd.out"
self.send(f"rm -f {out_path}; ( {command} ) > {out_path} 2>&1; "
f"echo __GC_DONE__\n")
deadline = time.time() + timeout
while time.time() < deadline:
if b"__GC_DONE__" in self.drain(2.0):
break
else:
return "(timeout)\n"
# tamanho em bytes
self.send(f"wc -c < {out_path}\n")
time.sleep(1.2)
raw = self.drain(2.0).decode(errors="replace")
nums = re.findall(r"\d+", raw)
size = int(nums[-1]) if nums else 0
# ler em blocos base64 (evita eco/sobreposicao)
data = b""
while len(data) < size:
self.send(f"dd if={out_path} bs=1 skip={len(data)} count=150 "
f"2>/dev/null | base64 -w0; echo\n")
time.sleep(0.8)
enc = self.drain(2.0).decode(errors="replace")
token = "".join(re.findall(r"[A-Za-z0-9+/=]{4,}", enc))
if not token:
break
try:
data += base64.b64decode(token + "=" * (-len(token) % 4))
except Exception:
break
return data.decode(errors="replace")
def main():
if len(sys.argv) < 3:
sys.exit(__doc__)
domain = sys.argv[1]
console = Console(console_pty(domain))
console.login()
if sys.argv[2] == "-f":
with open(sys.argv[3]) as fh:
script = fh.read().replace("'", "'\\''")
command = f"sh -c '{script}'"
else:
command = sys.argv[2]
sys.stdout.write(console.run(command))
sys.stdout.write("\n")
if __name__ == "__main__":
main()