# -*- coding: utf-8 eval: (blacken-mode 1) -*- # SPDX-License-Identifier: GPL-2.0-or-later # # September 2 2021, Christian Hopps # # Copyright 2021, LabN Consulting, L.L.C. # """The main function for standalone operation.""" import argparse import asyncio import logging import logging.config import os import subprocess import sys from . import cli from . import parser from .base import get_event_loop from .cleanup import cleanup_previous from .compat import PytestConfig logger = None async def forever(): while True: await asyncio.sleep(3600) async def run_and_wait(args, unet): tasks = [] if not args.topology_only: # add the cmd.wait()s returned from unet.run() tasks += await unet.run() if sys.stdin.isatty() and not args.no_cli: # Run an interactive CLI task = cli.async_cli(unet) else: if args.no_wait: logger.info("Waiting for all node cmd to complete") task = asyncio.gather(*tasks, return_exceptions=True) else: logger.info("Waiting on signal to exit") task = asyncio.create_task(forever()) task = asyncio.gather(task, *tasks, return_exceptions=True) try: await task finally: # Basically we are canceling tasks from unet.run() which are just async calls to # node.cmd_p.wait() so we've stopped waiting for them to complete, but not # actually canceld/killed the cmd_p process. for task in tasks: task.cancel() async def async_main(args, config): status = 3 # Setup the namespaces and network addressing. unet = await parser.async_build_topology( config, rundir=args.rundir, args=args, pytestconfig=PytestConfig(args) ) logger.info("Topology up: rundir: %s", unet.rundir) try: status = await run_and_wait(args, unet) except KeyboardInterrupt: logger.info("Exiting, received KeyboardInterrupt in async_main") except asyncio.CancelledError as ex: logger.info("task canceled error: %s cleaning up", ex) except Exception as error: logger.info("Exiting, unexpected exception %s", error, exc_info=True) else: logger.info("Exiting normally") logger.debug("main: async deleting") try: await unet.async_delete() except KeyboardInterrupt: status = 2 logger.warning("Received KeyboardInterrupt while cleaning up.") except Exception as error: status = 2 logger.info("Deleting, unexpected exception %s", error, exc_info=True) return status def main(*args): ap = argparse.ArgumentParser(args) cap = ap.add_argument_group(title="Config", description="config related options") cap.add_argument("-c", "--config", help="config file (yaml, toml, json, ...)") cap.add_argument( "-d", "--rundir", help="runtime directory for tempfiles, logs, etc" ) cap.add_argument( "--kinds-config", help="kinds config file, overrides default search (yaml, toml, json, ...)", ) cap.add_argument( "--project-root", help="directory to stop searching for kinds config at" ) rap = ap.add_argument_group(title="Runtime", description="runtime related options") rap.add_argument( "-C", "--cleanup", action="store_true", help="Remove the entire rundir (not just node subdirs) prior to running.", ) rap.add_argument( "--gdb", metavar="NODE-LIST", help="comma-sep list of hosts to run gdb on" ) rap.add_argument( "--gdb-breakpoints", metavar="BREAKPOINT-LIST", help="comma-sep list of breakpoints to set", ) rap.add_argument( "--host", action="store_true", help="no isolation for top namespace, bridges exposed to default namespace", ) rap.add_argument( "--pcap", metavar="TARGET-LIST", help="comma-sep list of capture targets (NETWORK or NODE:IFNAME)", ) rap.add_argument( "--shell", metavar="NODE-LIST", help="comma-sep list of nodes to open shells on" ) rap.add_argument( "--stderr", metavar="NODE-LIST", help="comma-sep list of nodes to open windows viewing stderr", ) rap.add_argument( "--stdout", metavar="NODE-LIST", help="comma-sep list of nodes to open windows viewing stdout", ) rap.add_argument( "--topology-only", action="store_true", help="Do not run any node commands", ) rap.add_argument("--unshare-inline", action="store_true", help=argparse.SUPPRESS) rap.add_argument( "--validate-only", action="store_true", help="Validate the config against the schema definition", ) rap.add_argument("-v", "--verbose", action="store_true", help="be verbose") rap.add_argument( "-V", "--version", action="store_true", help="print the verison number and exit" ) eap = ap.add_argument_group(title="Uncommon", description="uncommonly used options") eap.add_argument("--log-config", help="logging config file (yaml, toml, json, ...)") eap.add_argument( "--no-kill", action="store_true", help="Do not kill previous running processes", ) eap.add_argument( "--no-cli", action="store_true", help="Do not run the interactive CLI" ) eap.add_argument("--no-wait", action="store_true", help="Exit after commands") args = ap.parse_args() if args.version: from importlib import metadata # pylint: disable=C0415 print(metadata.version("munet")) sys.exit(0) rundir = args.rundir if args.rundir else "/tmp/munet" args.rundir = rundir if args.cleanup: if os.path.exists(rundir): if not os.path.exists(f"{rundir}/config.json"): logging.critical( 'unsafe: won\'t clean up rundir "%s" as ' "previous config.json not present", rundir, ) sys.exit(1) else: subprocess.run(["/usr/bin/rm", "-rf", rundir], check=True) subprocess.run(f"mkdir -p {rundir} && chmod 755 {rundir}", check=True, shell=True) os.environ["MUNET_RUNDIR"] = rundir parser.setup_logging(args) global logger # pylint: disable=W0603 logger = logging.getLogger("munet") config = parser.get_config(args.config) logger.info("Loaded config from %s", config["config_pathname"]) if not config["topology"]["nodes"]: logger.critical("No nodes defined in config file") return 1 if not args.no_kill: cleanup_previous() loop = None status = 4 try: parser.validate_config(config, logger, args) if args.validate_only: return 0 # Executes the cmd for each node. loop = get_event_loop() status = loop.run_until_complete(async_main(args, config)) except KeyboardInterrupt: logger.info("Exiting, received KeyboardInterrupt in main") except Exception as error: logger.info("Exiting, unexpected exception %s", error, exc_info=True) finally: if loop: loop.close() return status if __name__ == "__main__": exit_status = main() sys.exit(exit_status)