mirror of
https://git.proxmox.com/git/mirror_frr
synced 2025-07-17 15:46:26 +00:00

- 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>
214 lines
6.6 KiB
Python
214 lines
6.6 KiB
Python
# -*- coding: utf-8 eval: (blacken-mode 1) -*-
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
#
|
|
# April 22 2022, Christian Hopps <chopps@gmail.com>
|
|
#
|
|
# Copyright (c) 2022, LabN Consulting, L.L.C
|
|
#
|
|
"""Utility functions useful when using munet testing functionailty in pytest."""
|
|
import asyncio
|
|
import datetime
|
|
import fcntl
|
|
import functools
|
|
import logging
|
|
import os
|
|
import re
|
|
import select
|
|
import sys
|
|
import time
|
|
|
|
from ..base import BaseMunet
|
|
from ..base import Timeout
|
|
from ..cli import async_cli
|
|
|
|
|
|
# =================
|
|
# Utility Functions
|
|
# =================
|
|
|
|
|
|
async def async_pause_test(desc=""):
|
|
"""Pause the running of a test offering options for CLI or PDB."""
|
|
isatty = sys.stdout.isatty()
|
|
if not isatty:
|
|
desc = f" for {desc}" if desc else ""
|
|
logging.info("NO PAUSE on non-tty terminal%s", desc)
|
|
return
|
|
|
|
while True:
|
|
if desc:
|
|
print(f"\n== PAUSING: {desc} ==")
|
|
try:
|
|
user = input('PAUSED, "cli" for CLI, "pdb" to debug, "Enter" to continue: ')
|
|
except EOFError:
|
|
print("^D...continuing")
|
|
break
|
|
user = user.strip()
|
|
if user == "cli":
|
|
await async_cli(BaseMunet.g_unet)
|
|
elif user == "pdb":
|
|
breakpoint() # pylint: disable=W1515
|
|
elif user:
|
|
print(f'Unrecognized input: "{user}"')
|
|
else:
|
|
break
|
|
|
|
|
|
def pause_test(desc=""):
|
|
"""Pause the running of a test offering options for CLI or PDB."""
|
|
asyncio.run(async_pause_test(desc))
|
|
|
|
|
|
def retry(retry_timeout, initial_wait=0, retry_sleep=2, expected=True):
|
|
"""Retry decorated function until it returns None, raises an exception, or timeout.
|
|
|
|
* `retry_timeout`: Retry for at least this many seconds; after waiting
|
|
initial_wait seconds
|
|
* `initial_wait`: Sleeps for this many seconds before first executing function
|
|
* `retry_sleep`: The time to sleep between retries.
|
|
* `expected`: if False then the return logic is inverted, except for exceptions,
|
|
(i.e., a non None ends the retry loop, and returns that value)
|
|
"""
|
|
|
|
def _retry(func):
|
|
@functools.wraps(func)
|
|
def func_retry(*args, **kwargs):
|
|
# Allow the wrapped function's args to override the fixtures
|
|
_retry_sleep = float(kwargs.pop("retry_sleep", retry_sleep))
|
|
_retry_timeout = kwargs.pop("retry_timeout", retry_timeout)
|
|
_expected = kwargs.pop("expected", expected)
|
|
_initial_wait = kwargs.pop("initial_wait", initial_wait)
|
|
retry_until = datetime.datetime.now() + datetime.timedelta(
|
|
seconds=_retry_timeout + _initial_wait
|
|
)
|
|
|
|
if initial_wait > 0:
|
|
logging.info("Waiting for [%s]s as initial delay", initial_wait)
|
|
time.sleep(initial_wait)
|
|
|
|
while True:
|
|
seconds_left = (retry_until - datetime.datetime.now()).total_seconds()
|
|
try:
|
|
try:
|
|
ret = func(*args, seconds_left=seconds_left, **kwargs)
|
|
except TypeError as error:
|
|
if "seconds_left" not in str(error):
|
|
raise
|
|
ret = func(*args, **kwargs)
|
|
|
|
logging.debug("Function returned %s", ret)
|
|
|
|
positive_result = ret is None
|
|
if _expected == positive_result:
|
|
logging.debug("Function succeeds")
|
|
return ret
|
|
except Exception as error:
|
|
logging.info('Function raised exception: "%s"', error)
|
|
ret = error
|
|
|
|
if seconds_left < 0:
|
|
logging.info("Retry timeout of %ds reached", _retry_timeout)
|
|
if isinstance(ret, Exception):
|
|
raise ret
|
|
return ret
|
|
|
|
logging.info(
|
|
"Sleeping %ds until next retry with %.1f retry time left",
|
|
_retry_sleep,
|
|
seconds_left,
|
|
)
|
|
time.sleep(_retry_sleep)
|
|
|
|
func_retry._original = func # pylint: disable=W0212
|
|
return func_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
|