mirror of
https://git.proxmox.com/git/mirror_frr
synced 2025-07-27 11:44:16 +00:00
tests: update munet to 0.15.4
- add readline and waitline functions for use with popen objects - other non-topotest (munet native) run changes - vm/qemu support booting cloud images (rocky, ubuntu, debian) - native topology init commands Signed-off-by: Christian Hopps <chopps@labn.net>
This commit is contained in:
parent
a962ff7833
commit
3366056bce
@ -332,6 +332,10 @@ class Commander: # pylint: disable=R0904
|
|||||||
self.last = None
|
self.last = None
|
||||||
self.exec_paths = {}
|
self.exec_paths = {}
|
||||||
|
|
||||||
|
# For running commands one time only (deals with asyncio)
|
||||||
|
self.cmd_once_done = {}
|
||||||
|
self.cmd_once_locks = {}
|
||||||
|
|
||||||
if not logger:
|
if not logger:
|
||||||
logname = f"munet.{self.__class__.__name__.lower()}.{name}"
|
logname = f"munet.{self.__class__.__name__.lower()}.{name}"
|
||||||
self.logger = logging.getLogger(logname)
|
self.logger = logging.getLogger(logname)
|
||||||
@ -1189,7 +1193,7 @@ class Commander: # pylint: disable=R0904
|
|||||||
return stdout
|
return stdout
|
||||||
|
|
||||||
# Run a command in a new window (gnome-terminal, screen, tmux, xterm)
|
# Run a command in a new window (gnome-terminal, screen, tmux, xterm)
|
||||||
def run_in_window(
|
def run_in_window( # pylint: disable=too-many-positional-arguments
|
||||||
self,
|
self,
|
||||||
cmd,
|
cmd,
|
||||||
wait_for=False,
|
wait_for=False,
|
||||||
@ -1205,7 +1209,7 @@ class Commander: # pylint: disable=R0904
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
cmd: string to execute.
|
cmd: string to execute.
|
||||||
wait_for: True to wait for exit from command or `str` as channel neme to
|
wait_for: True to wait for exit from command or `str` as channel name to
|
||||||
signal on exit, otherwise False
|
signal on exit, otherwise False
|
||||||
background: Do not change focus to new window.
|
background: Do not change focus to new window.
|
||||||
title: Title for new pane (tmux) or window (xterm).
|
title: Title for new pane (tmux) or window (xterm).
|
||||||
@ -1405,6 +1409,26 @@ class Commander: # pylint: disable=R0904
|
|||||||
|
|
||||||
return pane_info
|
return pane_info
|
||||||
|
|
||||||
|
async def async_cmd_raises_once(self, cmd, **kwargs):
|
||||||
|
if cmd in self.cmd_once_done:
|
||||||
|
return self.cmd_once_done[cmd]
|
||||||
|
|
||||||
|
if cmd not in self.cmd_once_locks:
|
||||||
|
self.cmd_once_locks[cmd] = asyncio.Lock()
|
||||||
|
|
||||||
|
async with self.cmd_once_locks[cmd]:
|
||||||
|
if cmd not in self.cmd_once_done:
|
||||||
|
self.logger.info("Running command once: %s", cmd)
|
||||||
|
self.cmd_once_done[cmd] = await commander.async_cmd_raises(
|
||||||
|
cmd, **kwargs
|
||||||
|
)
|
||||||
|
return self.cmd_once_done[cmd]
|
||||||
|
|
||||||
|
def cmd_raises_once(self, cmd, **kwargs):
|
||||||
|
if cmd not in self.cmd_once_done:
|
||||||
|
self.cmd_once_done[cmd] = commander.cmd_raises(cmd, **kwargs)
|
||||||
|
return self.cmd_once_done[cmd]
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
"""Calls self.async_delete within an exec loop."""
|
"""Calls self.async_delete within an exec loop."""
|
||||||
asyncio.run(self.async_delete())
|
asyncio.run(self.async_delete())
|
||||||
|
@ -117,6 +117,12 @@
|
|||||||
"bios": {
|
"bios": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"cloud-init": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"cloud-init-disk": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"disk": {
|
"disk": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -129,7 +135,7 @@
|
|||||||
"initial-cmd": {
|
"initial-cmd": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"kerenel": {
|
"kernel": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"initrd": {
|
"initrd": {
|
||||||
@ -373,6 +379,12 @@
|
|||||||
"networks-autonumber": {
|
"networks-autonumber": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"initial-setup-cmd": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"initial-setup-host-cmd": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"networks": {
|
"networks": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
@ -452,6 +464,12 @@
|
|||||||
"bios": {
|
"bios": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"cloud-init": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"cloud-init-disk": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"disk": {
|
"disk": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -464,7 +482,7 @@
|
|||||||
"initial-cmd": {
|
"initial-cmd": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"kerenel": {
|
"kernel": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"initrd": {
|
"initrd": {
|
||||||
|
@ -180,7 +180,7 @@ class TestCase:
|
|||||||
|
|
||||||
# sum_hfmt = "{:5.5s} {:4.4s} {:>6.6s} {}"
|
# sum_hfmt = "{:5.5s} {:4.4s} {:>6.6s} {}"
|
||||||
# sum_dfmt = "{:5s} {:4.4s} {:^6.6s} {}"
|
# sum_dfmt = "{:5s} {:4.4s} {:^6.6s} {}"
|
||||||
sum_fmt = "%-8.8s %4.4s %{}s %6s %s"
|
sum_fmt = "%-10s %4.4s %{}s %6s %s"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -24,6 +24,13 @@ import time
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# We only want to require yaml for the gen cloud image feature
|
||||||
|
import yaml
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
from . import cli
|
from . import cli
|
||||||
from .base import BaseMunet
|
from .base import BaseMunet
|
||||||
from .base import Bridge
|
from .base import Bridge
|
||||||
@ -749,9 +756,11 @@ class L3NodeMixin(NodeMixin):
|
|||||||
# Disable IPv6
|
# Disable IPv6
|
||||||
self.cmd_raises("sysctl -w net.ipv6.conf.all.autoconf=0")
|
self.cmd_raises("sysctl -w net.ipv6.conf.all.autoconf=0")
|
||||||
self.cmd_raises("sysctl -w net.ipv6.conf.all.disable_ipv6=1")
|
self.cmd_raises("sysctl -w net.ipv6.conf.all.disable_ipv6=1")
|
||||||
|
self.cmd_raises("sysctl -w net.ipv6.conf.all.forwarding=0")
|
||||||
else:
|
else:
|
||||||
self.cmd_raises("sysctl -w net.ipv6.conf.all.autoconf=1")
|
self.cmd_raises("sysctl -w net.ipv6.conf.all.autoconf=1")
|
||||||
self.cmd_raises("sysctl -w net.ipv6.conf.all.disable_ipv6=0")
|
self.cmd_raises("sysctl -w net.ipv6.conf.all.disable_ipv6=0")
|
||||||
|
self.cmd_raises("sysctl -w net.ipv6.conf.all.forwarding=1")
|
||||||
|
|
||||||
self.next_p2p_network = ipaddress.ip_network(f"10.254.{self.id}.0/31")
|
self.next_p2p_network = ipaddress.ip_network(f"10.254.{self.id}.0/31")
|
||||||
self.next_p2p_network6 = ipaddress.ip_network(f"fcff:ffff:{self.id:02x}::/127")
|
self.next_p2p_network6 = ipaddress.ip_network(f"fcff:ffff:{self.id:02x}::/127")
|
||||||
@ -2265,6 +2274,164 @@ class L3QemuVM(L3NodeMixin, LinuxNamespace):
|
|||||||
tid = self.cpu_thread_map[i]
|
tid = self.cpu_thread_map[i]
|
||||||
self.cmd_raises_nsonly(f"taskset -cp {aff} {tid}")
|
self.cmd_raises_nsonly(f"taskset -cp {aff} {tid}")
|
||||||
|
|
||||||
|
def _gen_network_config(self):
|
||||||
|
intfs = sorted(self.intfs)
|
||||||
|
if not intfs:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
self.logger.debug("Generating cloud-init interface config")
|
||||||
|
config = {}
|
||||||
|
config["version"] = 2
|
||||||
|
enets = config["ethernets"] = {}
|
||||||
|
|
||||||
|
for ifname in sorted(self.intfs):
|
||||||
|
self.logger.debug("Interface %s", ifname)
|
||||||
|
conn = find_with_kv(self.config["connections"], "name", ifname)
|
||||||
|
|
||||||
|
index = self.config["connections"].index(conn)
|
||||||
|
to = conn["to"]
|
||||||
|
switch = self.unet.switches.get(to)
|
||||||
|
mtu = conn.get("mtu")
|
||||||
|
if not mtu and switch:
|
||||||
|
mtu = switch.config.get("mtu")
|
||||||
|
|
||||||
|
devaddr = conn.get("physical", "")
|
||||||
|
# Eventually we should get the MAC from /sys
|
||||||
|
if not devaddr:
|
||||||
|
mac = self.tapmacs.get(ifname, f"02:aa:aa:aa:{index:02x}:{self.id:02x}")
|
||||||
|
nic = {
|
||||||
|
"match": {"macaddress": str(mac)},
|
||||||
|
"set-name": ifname,
|
||||||
|
}
|
||||||
|
if mtu:
|
||||||
|
nic["mtu"] = str(mtu)
|
||||||
|
enets[f"nic-{ifname}"] = nic
|
||||||
|
|
||||||
|
ifaddr4 = self.get_intf_addr(ifname, ipv6=False)
|
||||||
|
ifaddr6 = self.get_intf_addr(ifname, ipv6=True)
|
||||||
|
if not ifaddr4 and not ifaddr6:
|
||||||
|
continue
|
||||||
|
net = {
|
||||||
|
"dhcp4": False,
|
||||||
|
"dhcp6": False,
|
||||||
|
"accept-ra": False,
|
||||||
|
"addresses": [],
|
||||||
|
}
|
||||||
|
if ifaddr4:
|
||||||
|
net["addresses"].append(str(ifaddr4))
|
||||||
|
if ifaddr6:
|
||||||
|
net["addresses"].append(str(ifaddr6))
|
||||||
|
if switch and hasattr(switch, "is_nat") and switch.is_nat:
|
||||||
|
net["nameservers"] = {"addresses": []}
|
||||||
|
nameservers = net["nameservers"]["addresses"]
|
||||||
|
if hasattr(switch, "ip6_address"):
|
||||||
|
net["gateway6"] = str(switch.ip6_address)
|
||||||
|
nameservers.append("2001:4860:4860::8888")
|
||||||
|
if switch.ip_address:
|
||||||
|
net["gateway4"] = str(switch.ip_address)
|
||||||
|
nameservers.append("8.8.8.8")
|
||||||
|
enets[ifname] = net
|
||||||
|
|
||||||
|
return yaml.safe_dump(config)
|
||||||
|
|
||||||
|
def _gen_cloud_init(self):
|
||||||
|
qc = self.qemu_config
|
||||||
|
cc = qc.get("console", {})
|
||||||
|
cipath = self.rundir.joinpath("cloud-init.img")
|
||||||
|
|
||||||
|
geniso = get_exec_path_host("genisoimage")
|
||||||
|
if not geniso:
|
||||||
|
mfbin = get_exec_path_host("mkfs.vfat")
|
||||||
|
mcbin = get_exec_path_host("mcopy")
|
||||||
|
assert (
|
||||||
|
mfbin and mcbin
|
||||||
|
), "genisoimage or mkfs.vfat,mcopy needed to gen cloud-init disk"
|
||||||
|
|
||||||
|
#
|
||||||
|
# cloud-init: meta-data
|
||||||
|
#
|
||||||
|
mdata = f"""
|
||||||
|
instance-id: "munet-{self.id}"
|
||||||
|
local-hostname: "{self.name}"
|
||||||
|
"""
|
||||||
|
#
|
||||||
|
# cloud-init: user-data
|
||||||
|
#
|
||||||
|
ssh_auth_s = ""
|
||||||
|
if bool(self.ssh_keyfile):
|
||||||
|
pubkey = commander.cmd_raises(f"ssh-keygen -y -f {self.ssh_keyfile}")
|
||||||
|
assert pubkey, f"Can't extract public key from {self.ssh_keyfile}"
|
||||||
|
pubkey = pubkey.strip()
|
||||||
|
ssh_auth_s = f'ssh_authorized_keys: ["{pubkey}"]'
|
||||||
|
|
||||||
|
user = cc.get("user", "root")
|
||||||
|
password = cc.get("password", "admin")
|
||||||
|
if user != "root":
|
||||||
|
root_password = "admin"
|
||||||
|
else:
|
||||||
|
root_password = password
|
||||||
|
|
||||||
|
udata = f"""#cloud-config
|
||||||
|
disable_root: 0
|
||||||
|
ssh_pwauth: 1
|
||||||
|
hostname: {self.name}
|
||||||
|
runcmd:
|
||||||
|
- systemctl enable serial-getty@ttyS1.service
|
||||||
|
- systemctl start serial-getty@ttyS1.service
|
||||||
|
- systemctl enable serial-getty@ttyS2.service
|
||||||
|
- systemctl start serial-getty@ttyS2.service
|
||||||
|
- systemctl enable serial-getty@hvc0.service
|
||||||
|
- systemctl start serial-getty@hvc0.service
|
||||||
|
- systemctl enable serial-getty@hvc1.service
|
||||||
|
- systemctl start serial-getty@hvc1.service
|
||||||
|
users:
|
||||||
|
- name: root
|
||||||
|
lock_passwd: false
|
||||||
|
plain_text_passwd: "{root_password}"
|
||||||
|
{ssh_auth_s}
|
||||||
|
"""
|
||||||
|
if user != "root":
|
||||||
|
udata += """
|
||||||
|
- name: {user}
|
||||||
|
lock_passwd: false
|
||||||
|
plain_text_passwd: "{password}"
|
||||||
|
{ssh_auth_s}
|
||||||
|
"""
|
||||||
|
#
|
||||||
|
# cloud-init: network-config
|
||||||
|
#
|
||||||
|
ndata = self._gen_network_config()
|
||||||
|
|
||||||
|
#
|
||||||
|
# Generate cloud-init files
|
||||||
|
#
|
||||||
|
cidir = self.rundir.joinpath("ci-data")
|
||||||
|
commander.cmd_raises(f"mkdir -p {cidir}")
|
||||||
|
|
||||||
|
with open(cidir.joinpath("meta-data"), "w+", encoding="utf-8") as f:
|
||||||
|
f.write(mdata)
|
||||||
|
with open(cidir.joinpath("user-data"), "w+", encoding="utf-8") as f:
|
||||||
|
f.write(udata)
|
||||||
|
files = "meta-data user-data"
|
||||||
|
if ndata:
|
||||||
|
files += " network-config"
|
||||||
|
with open(cidir.joinpath("network-config"), "w+", encoding="utf-8") as f:
|
||||||
|
f.write(ndata)
|
||||||
|
if geniso:
|
||||||
|
commander.cmd_raises(
|
||||||
|
f"cd {cidir} && "
|
||||||
|
f'genisoimage -output "{cipath}" -volid cidata'
|
||||||
|
f" -joliet -rock {files}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
commander.cmd_raises(f'cd {cidir} && mkfs.vfat -n cidata "{cipath}"')
|
||||||
|
commander.cmd_raises(f'cd {cidir} && mcopy -oi "{cipath}" {files}')
|
||||||
|
|
||||||
|
#
|
||||||
|
# Generate cloud-init disk
|
||||||
|
#
|
||||||
|
return cipath
|
||||||
|
|
||||||
async def launch(self):
|
async def launch(self):
|
||||||
"""Launch qemu."""
|
"""Launch qemu."""
|
||||||
self.logger.info("%s: Launch Qemu", self)
|
self.logger.info("%s: Launch Qemu", self)
|
||||||
@ -2367,11 +2534,21 @@ class L3QemuVM(L3NodeMixin, LinuxNamespace):
|
|||||||
diskpath = os.path.join(self.unet.config_dirname, diskpath)
|
diskpath = os.path.join(self.unet.config_dirname, diskpath)
|
||||||
|
|
||||||
if dtpl and (not disk or not os.path.exists(diskpath)):
|
if dtpl and (not disk or not os.path.exists(diskpath)):
|
||||||
|
basename = os.path.basename(dtpl)
|
||||||
|
confdir = self.unet.config_dirname
|
||||||
|
if re.match("(https|http|ftp|tftp):.*", dtpl):
|
||||||
|
await self.unet.async_cmd_raises_once(
|
||||||
|
f"cd {confdir} && (test -e {basename} || curl -fLO {dtpl})"
|
||||||
|
)
|
||||||
|
dtplpath = os.path.join(confdir, basename)
|
||||||
|
|
||||||
if not disk:
|
if not disk:
|
||||||
disk = qc["disk"] = f"{self.name}-{os.path.basename(dtpl)}"
|
disk = qc["disk"] = f"{self.name}-{basename}"
|
||||||
diskpath = os.path.join(self.rundir, disk)
|
diskpath = os.path.join(self.rundir, disk)
|
||||||
|
|
||||||
if self.path_exists(diskpath):
|
if self.path_exists(diskpath):
|
||||||
logging.debug("Disk '%s' file exists, using.", diskpath)
|
logging.debug("Disk '%s' file exists, using.", diskpath)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if dtplpath[0] != "/":
|
if dtplpath[0] != "/":
|
||||||
dtplpath = os.path.join(self.unet.config_dirname, dtpl)
|
dtplpath = os.path.join(self.unet.config_dirname, dtpl)
|
||||||
@ -2392,11 +2569,15 @@ class L3QemuVM(L3NodeMixin, LinuxNamespace):
|
|||||||
args.extend(["-device", "ahci,id=ahci"])
|
args.extend(["-device", "ahci,id=ahci"])
|
||||||
args.extend(["-device", "ide-hd,bus=ahci.0,drive=sata-disk0"])
|
args.extend(["-device", "ide-hd,bus=ahci.0,drive=sata-disk0"])
|
||||||
|
|
||||||
cidiskpath = qc.get("cloud-init-disk")
|
if qc.get("cloud-init"):
|
||||||
if cidiskpath:
|
cidiskpath = qc.get("cloud-init-disk")
|
||||||
if cidiskpath[0] != "/":
|
if cidiskpath:
|
||||||
cidiskpath = os.path.join(self.unet.config_dirname, cidiskpath)
|
if cidiskpath[0] != "/":
|
||||||
args.extend(["-drive", f"file={cidiskpath},if=virtio,format=qcow2"])
|
cidiskpath = os.path.join(self.unet.config_dirname, cidiskpath)
|
||||||
|
else:
|
||||||
|
cidiskpath = self._gen_cloud_init()
|
||||||
|
diskfmt = "qcow2" if str(cidiskpath).endswith("qcow2") else "raw"
|
||||||
|
args.extend(["-drive", f"file={cidiskpath},if=virtio,format={diskfmt}"])
|
||||||
|
|
||||||
# args.extend(["-display", "vnc=0.0.0.0:40"])
|
# args.extend(["-display", "vnc=0.0.0.0:40"])
|
||||||
|
|
||||||
@ -2488,7 +2669,7 @@ class L3QemuVM(L3NodeMixin, LinuxNamespace):
|
|||||||
if use_cmdcon:
|
if use_cmdcon:
|
||||||
confiles.append("_cmdcon")
|
confiles.append("_cmdcon")
|
||||||
|
|
||||||
password = cc.get("password", "")
|
password = cc.get("password", "admin")
|
||||||
if self.disk_created:
|
if self.disk_created:
|
||||||
password = cc.get("initial-password", password)
|
password = cc.get("initial-password", password)
|
||||||
|
|
||||||
@ -2764,9 +2945,11 @@ ff02::2\tip6-allrouters
|
|||||||
# Disable IPv6
|
# Disable IPv6
|
||||||
self.cmd_raises("sysctl -w net.ipv6.conf.all.autoconf=0")
|
self.cmd_raises("sysctl -w net.ipv6.conf.all.autoconf=0")
|
||||||
self.cmd_raises("sysctl -w net.ipv6.conf.all.disable_ipv6=1")
|
self.cmd_raises("sysctl -w net.ipv6.conf.all.disable_ipv6=1")
|
||||||
|
self.cmd_raises("sysctl -w net.ipv6.conf.all.forwarding=0")
|
||||||
else:
|
else:
|
||||||
self.cmd_raises("sysctl -w net.ipv6.conf.all.autoconf=1")
|
self.cmd_raises("sysctl -w net.ipv6.conf.all.autoconf=1")
|
||||||
self.cmd_raises("sysctl -w net.ipv6.conf.all.disable_ipv6=0")
|
self.cmd_raises("sysctl -w net.ipv6.conf.all.disable_ipv6=0")
|
||||||
|
self.cmd_raises("sysctl -w net.ipv6.conf.all.forwarding=1")
|
||||||
|
|
||||||
# we really need overlay, but overlay-layers (used by overlay-images)
|
# we really need overlay, but overlay-layers (used by overlay-images)
|
||||||
# counts on things being present in overlay so this temp stuff doesn't work.
|
# counts on things being present in overlay so this temp stuff doesn't work.
|
||||||
@ -2774,6 +2957,24 @@ ff02::2\tip6-allrouters
|
|||||||
# # Let's hide podman details
|
# # Let's hide podman details
|
||||||
# self.tmpfs_mount("/var/lib/containers/storage/overlay-containers")
|
# self.tmpfs_mount("/var/lib/containers/storage/overlay-containers")
|
||||||
|
|
||||||
|
def run_init_cmds(unet, key, on_host):
|
||||||
|
cmds = unet.topoconf.get(key, "")
|
||||||
|
cmds = cmds.replace("%CONFIGDIR%", str(unet.config_dirname))
|
||||||
|
cmds = cmds.replace("%RUNDIR%", str(unet.rundir))
|
||||||
|
cmds = cmds.strip()
|
||||||
|
if not cmds:
|
||||||
|
return
|
||||||
|
|
||||||
|
cmds += "\n"
|
||||||
|
c = commander if on_host else unet
|
||||||
|
o = c.cmd_raises(cmds)
|
||||||
|
self.logger.debug(
|
||||||
|
"run_init_cmds (on-host: %s): %s", on_host, cmd_error(0, o, "")
|
||||||
|
)
|
||||||
|
|
||||||
|
run_init_cmds(self, "initial-setup-host-cmd", True)
|
||||||
|
run_init_cmds(self, "initial-setup-cmd", False)
|
||||||
|
|
||||||
shellopt = self.cfgopt.getoption("--shell")
|
shellopt = self.cfgopt.getoption("--shell")
|
||||||
shellopt = shellopt if shellopt else ""
|
shellopt = shellopt if shellopt else ""
|
||||||
if shellopt == "all" or "." in shellopt.split(","):
|
if shellopt == "all" or "." in shellopt.split(","):
|
||||||
@ -3061,7 +3262,8 @@ done"""
|
|||||||
if not rc:
|
if not rc:
|
||||||
continue
|
continue
|
||||||
logging.info("Pulling missing image %s", image)
|
logging.info("Pulling missing image %s", image)
|
||||||
aw = self.rootcmd.async_cmd_raises(f"podman pull {image}")
|
|
||||||
|
aw = self.rootcmd.async_cmd_raises_once(f"podman pull {image}")
|
||||||
tasks.append(asyncio.create_task(aw))
|
tasks.append(asyncio.create_task(aw))
|
||||||
if not tasks:
|
if not tasks:
|
||||||
return
|
return
|
||||||
|
@ -8,12 +8,17 @@
|
|||||||
"""Utility functions useful when using munet testing functionailty in pytest."""
|
"""Utility functions useful when using munet testing functionailty in pytest."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
|
import fcntl
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import select
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from ..base import BaseMunet
|
from ..base import BaseMunet
|
||||||
|
from ..base import Timeout
|
||||||
from ..cli import async_cli
|
from ..cli import async_cli
|
||||||
|
|
||||||
|
|
||||||
@ -23,6 +28,7 @@ from ..cli import async_cli
|
|||||||
|
|
||||||
|
|
||||||
async def async_pause_test(desc=""):
|
async def async_pause_test(desc=""):
|
||||||
|
"""Pause the running of a test offering options for CLI or PDB."""
|
||||||
isatty = sys.stdout.isatty()
|
isatty = sys.stdout.isatty()
|
||||||
if not isatty:
|
if not isatty:
|
||||||
desc = f" for {desc}" if desc else ""
|
desc = f" for {desc}" if desc else ""
|
||||||
@ -49,11 +55,12 @@ async def async_pause_test(desc=""):
|
|||||||
|
|
||||||
|
|
||||||
def pause_test(desc=""):
|
def pause_test(desc=""):
|
||||||
|
"""Pause the running of a test offering options for CLI or PDB."""
|
||||||
asyncio.run(async_pause_test(desc))
|
asyncio.run(async_pause_test(desc))
|
||||||
|
|
||||||
|
|
||||||
def retry(retry_timeout, initial_wait=0, retry_sleep=2, expected=True):
|
def retry(retry_timeout, initial_wait=0, retry_sleep=2, expected=True):
|
||||||
"""decorator: retry while functions return is not None or raises an exception.
|
"""Retry decorated function until it returns None, raises an exception, or timeout.
|
||||||
|
|
||||||
* `retry_timeout`: Retry for at least this many seconds; after waiting
|
* `retry_timeout`: Retry for at least this many seconds; after waiting
|
||||||
initial_wait seconds
|
initial_wait seconds
|
||||||
@ -116,3 +123,91 @@ def retry(retry_timeout, initial_wait=0, retry_sleep=2, expected=True):
|
|||||||
return func_retry
|
return func_retry
|
||||||
|
|
||||||
return _retry
|
return _retry
|
||||||
|
|
||||||
|
|
||||||
|
def readline(f, timeout=None):
|
||||||
|
"""Read a line or timeout.
|
||||||
|
|
||||||
|
This function will take over the file object, the file object should not be used
|
||||||
|
outside of calling this function once you begin.
|
||||||
|
|
||||||
|
Return: A line, remaining buffer if EOF (subsequent calls will return ""), or None
|
||||||
|
for timeout.
|
||||||
|
"""
|
||||||
|
fd = f.fileno()
|
||||||
|
if not hasattr(f, "munet_non_block_set"):
|
||||||
|
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||||
|
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
||||||
|
f.munet_non_block_set = True
|
||||||
|
f.munet_lines = []
|
||||||
|
f.munet_buf = ""
|
||||||
|
|
||||||
|
if f.munet_lines:
|
||||||
|
return f.munet_lines.pop(0)
|
||||||
|
|
||||||
|
timeout = Timeout(timeout)
|
||||||
|
remaining = timeout.remaining()
|
||||||
|
while remaining > 0:
|
||||||
|
ready, _, _ = select.select([fd], [], [], remaining)
|
||||||
|
if not ready:
|
||||||
|
return None
|
||||||
|
|
||||||
|
c = f.read()
|
||||||
|
if c is None:
|
||||||
|
logging.error("munet readline: unexpected None during read")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not c:
|
||||||
|
logging.debug("munet readline: got eof")
|
||||||
|
c = f.munet_buf
|
||||||
|
f.munet_buf = ""
|
||||||
|
return c
|
||||||
|
|
||||||
|
f.munet_buf += c
|
||||||
|
while "\n" in f.munet_buf:
|
||||||
|
a, f.munet_buf = f.munet_buf.split("\n", 1)
|
||||||
|
f.munet_lines.append(a + "\n")
|
||||||
|
|
||||||
|
if f.munet_lines:
|
||||||
|
return f.munet_lines.pop(0)
|
||||||
|
|
||||||
|
remaining = timeout.remaining()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def waitline(f, regex, timeout=120):
|
||||||
|
"""Match a regex within lines from a file with a timeout.
|
||||||
|
|
||||||
|
This function will take over the file object (by calling `readline` above), the file
|
||||||
|
object should not be used outside of calling these functions once you begin.
|
||||||
|
|
||||||
|
Return: the match object or None.
|
||||||
|
"""
|
||||||
|
timeo = Timeout(timeout)
|
||||||
|
while not timeo.is_expired():
|
||||||
|
line = readline(f, timeo.remaining())
|
||||||
|
if line is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
if line == "":
|
||||||
|
logging.warning("waitline: got eof while matching '%s'", regex)
|
||||||
|
return None
|
||||||
|
|
||||||
|
assert line[-1] == "\n"
|
||||||
|
line = line[:-1]
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
logging.debug("waitline: searching: '%s' for '%s'", line, regex)
|
||||||
|
m = re.search(regex, line)
|
||||||
|
if m:
|
||||||
|
logging.debug("waitline: matched '%s'", m.group(0))
|
||||||
|
return m
|
||||||
|
|
||||||
|
logging.warning(
|
||||||
|
"Timeout while getting output matching '%s' within %ss (actual %ss)",
|
||||||
|
regex,
|
||||||
|
timeout,
|
||||||
|
timeo.elapsed(),
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
Loading…
Reference in New Issue
Block a user