2
0
mirror of https://github.com/xcat2/xcat-core.git synced 2026-10-22 23:58:13 +00:00
Files
xcat-core/xCAT-test/provtest/src/provtest_lib/proc.py
T
Daniel Hilst 7ea4bbdfb4 test(provtest): add provtest, a wire-level provision chain client
provtest asks the management node the questions a booting node asks and
asserts on the answers. DNS, TFTP and HTTP are driven by dig, curl and
tftp, so the result is what a real client sees rather than what this
tool's own protocol parser believed. The xcatd stages use sockets: they
must choose their source address, because xcatd names a client by the
reverse lookup of the address a connection arrived from.

Scenarios are declarative .conf files sharing dhcptest's assertion
grammar, exit codes and TAP output. Nothing here reads the xCAT database
or runs an xCAT command; every expected value arrives via --set.

validate and list run offline, in a checkout, with no root.
2026-09-11 07:55:59 -03:00

93 lines
2.8 KiB
Python

"""The one place a child process is started.
DNS, HTTP and TFTP are driven by the clients an operator would reach for --
`dig`, `curl`, `tftp` -- rather than by a hand-rolled implementation of each
protocol. What is under test is whether a real client gets a usable answer,
so using the real client removes a whole class of "the test's own parser was
wrong" from the result.
Confining the spawning to this module is what makes that safe. Output is read
from a pipe with a timeout and a hard kill, never through a shell, and every
argument list is built as a list, so nothing a `.conf` supplies can be
interpreted as a command.
"""
import os
import shutil
import subprocess
from .errors import UnsupportedError
class Completed(object):
"""What a client program did: exit status and the two streams, as bytes."""
__slots__ = ("argv", "rc", "out", "err", "timed_out")
def __init__(self, argv, rc, out=b"", err=b"", timed_out=False):
self.argv = list(argv)
self.rc = rc
self.out = out
self.err = err
self.timed_out = timed_out
@property
def text(self):
return self.out.decode("utf-8", "replace")
@property
def errtext(self):
return self.err.decode("utf-8", "replace")
def command(self):
return " ".join(self.argv)
def __repr__(self):
return "Completed(%r, rc=%r)" % (self.command(), self.rc)
def which(program):
"""The path to a client program, or None."""
return shutil.which(program)
def require(program, why):
"""The path to a client program, or a skip-the-host error saying why."""
path = which(program)
if path is None:
raise UnsupportedError(
"%s is not installed, so %s cannot be tested" % (program, why))
return path
def run(argv, timeout=10.0, stdin=None):
"""Run a client program and collect what it said.
A program that outlives its timeout is killed and reported as timed out
rather than raising, because "the server did not answer" is a result the
scenario may be asserting on.
"""
env = dict(os.environ)
# A localised client would report its errors in a language the parsers
# here do not read.
env["LC_ALL"] = "C"
env["LANG"] = "C"
try:
child = subprocess.Popen(
list(argv),
stdin=subprocess.PIPE if stdin is not None else subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
)
except OSError as exc:
return Completed(argv, 127, b"", str(exc).encode())
try:
out, err = child.communicate(input=stdin, timeout=timeout)
return Completed(argv, child.returncode, out, err)
except subprocess.TimeoutExpired:
child.kill()
out, err = child.communicate()
return Completed(argv, 124, out, err, timed_out=True)