mirror_frr/tests/topotests/lib/micronet_compat.py
Christian Hopps 20b31fffc1 tests: deal with parallel exit of process we are reaping
Signed-off-by: Christian Hopps <chopps@labn.net>
2021-09-06 16:54:36 -04:00

267 lines
7.5 KiB
Python

# -*- coding: utf-8 eval: (blacken-mode 1) -*-
#
# July 11 2021, Christian Hopps <chopps@labn.net>
#
# Copyright (c) 2021, LabN Consulting, L.L.C
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; see the file COPYING; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
import glob
import logging
import os
import signal
import time
from lib.micronet import LinuxNamespace, Micronet
from lib.micronet_cli import cli
def get_pids_with_env(has_var, has_val=None):
result = {}
for pidenv in glob.iglob("/proc/*/environ"):
pid = pidenv.split("/")[2]
try:
with open(pidenv, "rb") as rfb:
envlist = [
x.decode("utf-8").split("=", 1) for x in rfb.read().split(b"\0")
]
envlist = [[x[0], ""] if len(x) == 1 else x for x in envlist]
envdict = dict(envlist)
if has_var not in envdict:
continue
if has_val is None:
result[pid] = envdict
elif envdict[has_var] == str(has_val):
result[pid] = envdict
except Exception:
# E.g., process exited and files are gone
pass
return result
def _kill_piddict(pids_by_upid, sig):
for upid, pids in pids_by_upid:
logging.info(
"Sending %s to (%s) of micronet pid %s", sig, ", ".join(pids), upid
)
for pid in pids:
try:
os.kill(int(pid), sig)
except Exception:
pass
def _get_our_pids():
ourpid = str(os.getpid())
piddict = get_pids_with_env("MICRONET_PID", ourpid)
pids = [x for x in piddict if x != ourpid]
if pids:
return {ourpid: pids}
return {}
def _get_other_pids():
piddict = get_pids_with_env("MICRONET_PID")
unet_pids = {d["MICRONET_PID"] for d in piddict.values()}
pids_by_upid = {p: set() for p in unet_pids}
for pid, envdict in piddict.items():
pids_by_upid[envdict["MICRONET_PID"]].add(pid)
# Filter out any child pid sets whos micronet pid is still running
return {x: y for x, y in pids_by_upid.items() if x not in y}
def _get_pids_by_upid(ours):
if ours:
return _get_our_pids()
return _get_other_pids()
def _cleanup_pids(ours):
pids_by_upid = _get_pids_by_upid(ours).items()
if not pids_by_upid:
return
_kill_piddict(pids_by_upid, signal.SIGTERM)
# Give them 5 second to exit cleanly
logging.info("Waiting up to 5s to allow for clean exit of abandon'd pids")
for _ in range(0, 5):
pids_by_upid = _get_pids_by_upid(ours).items()
if not pids_by_upid:
return
time.sleep(1)
pids_by_upid = _get_pids_by_upid(ours).items()
_kill_piddict(pids_by_upid, signal.SIGKILL)
def cleanup_current():
"""Attempt to cleanup preview runs.
Currently this only scans for old processes.
"""
logging.info("reaping current micronet processes")
_cleanup_pids(True)
def cleanup_previous():
"""Attempt to cleanup preview runs.
Currently this only scans for old processes.
"""
logging.info("reaping past micronet processes")
_cleanup_pids(False)
class Node(LinuxNamespace):
"""Node (mininet compat)."""
def __init__(self, name, **kwargs):
"""
Create a Node.
"""
self.params = kwargs
if "private_mounts" in kwargs:
private_mounts = kwargs["private_mounts"]
else:
private_mounts = kwargs.get("privateDirs", [])
logger = kwargs.get("logger")
super(Node, self).__init__(name, logger=logger, private_mounts=private_mounts)
def cmd(self, cmd, **kwargs):
"""Execute a command, joins stdout, stderr, ignores exit status."""
return super(Node, self).cmd_legacy(cmd, **kwargs)
def config(self, lo="up", **params):
"""Called by Micronet when topology is built (but not started)."""
# mininet brings up loopback here.
del params
del lo
def intfNames(self):
return self.intfs
def terminate(self):
return
class Topo(object): # pylint: disable=R0205
def __init__(self, *args, **kwargs):
raise Exception("Remove Me")
class Mininet(Micronet):
"""
Mininet using Micronet.
"""
g_mnet_inst = None
def __init__(self, controller=None):
"""
Create a Micronet.
"""
assert not controller
if Mininet.g_mnet_inst is not None:
Mininet.g_mnet_inst.stop()
Mininet.g_mnet_inst = self
self.configured_hosts = set()
self.host_params = {}
self.prefix_len = 8
# SNMPd used to require this, which was set int he mininet shell
# that all commands executed from. This is goofy default so let's not
# do it if we don't have to. The snmpd.conf files have been updated
# to set permissions to root:frr 770 to make this unneeded in that case
# os.umask(0)
super(Mininet, self).__init__()
self.logger.debug("%s: Creating", self)
def __str__(self):
return "Mininet()"
def configure_hosts(self):
"""
Configure hosts once the topology has been built.
This function can be called multiple times if routers are added to the topology
later.
"""
if not self.hosts:
return
self.logger.debug("Configuring hosts: %s", self.hosts.keys())
for name in sorted(self.hosts.keys()):
if name in self.configured_hosts:
continue
host = self.hosts[name]
first_intf = host.intfs[0] if host.intfs else None
params = self.host_params[name]
if first_intf and "ip" in params:
ip = params["ip"]
i = ip.find("/")
if i == -1:
plen = self.prefix_len
else:
plen = int(ip[i + 1 :])
ip = ip[:i]
host.cmd_raises("ip addr add {}/{} dev {}".format(ip, plen, first_intf))
if "defaultRoute" in params:
host.cmd_raises(
"ip route add default {}".format(params["defaultRoute"])
)
host.config()
self.configured_hosts.add(name)
def add_host(self, name, cls=Node, **kwargs):
"""Add a host to micronet."""
self.host_params[name] = kwargs
super(Mininet, self).add_host(name, cls=cls, **kwargs)
def start(self):
"""Start the micronet topology."""
self.logger.debug("%s: Starting (no-op).", self)
def stop(self):
"""Stop the mininet topology (deletes)."""
self.logger.debug("%s: Stopping (deleting).", self)
self.delete()
self.logger.debug("%s: Stopped (deleted).", self)
if Mininet.g_mnet_inst == self:
Mininet.g_mnet_inst = None
def cli(self):
cli(self)