225 lines
6.8 KiB
Python
225 lines
6.8 KiB
Python
# -*- coding: utf-8 eval: (blacken-mode 1) -*-
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
#
|
|
# September 2 2021, Christian Hopps <chopps@labn.net>
|
|
#
|
|
# 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 .args import add_launch_args
|
|
from .base import get_event_loop
|
|
from .cleanup import cleanup_previous
|
|
from .cleanup import is_running_in_rundir
|
|
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")
|
|
add_launch_args(rap.add_argument)
|
|
|
|
# Move to munet.args?
|
|
rap.add_argument(
|
|
"-C",
|
|
"--cleanup",
|
|
action="store_true",
|
|
help="Remove the entire rundir (not just node subdirs) prior to running.",
|
|
)
|
|
# Move to munet.args?
|
|
rap.add_argument(
|
|
"--topology-only",
|
|
action="store_true",
|
|
help="Do not run any node commands",
|
|
)
|
|
rap.add_argument(
|
|
"--validate-only",
|
|
action="store_true",
|
|
help="Validate the config against the schema definition",
|
|
)
|
|
rap.add_argument("--unshare-inline", action="store_true", help=argparse.SUPPRESS)
|
|
|
|
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(
|
|
"--kill",
|
|
action="store_true",
|
|
help="Kill previous running processes using same rundir and exit",
|
|
)
|
|
eap.add_argument("--no-kill", action="store_true", help=argparse.SUPPRESS)
|
|
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"
|
|
rundir = os.path.abspath(rundir)
|
|
args.rundir = rundir
|
|
|
|
if args.kill:
|
|
logging.info("Killing any previous run using rundir: {rundir}")
|
|
cleanup_previous(args.rundir)
|
|
elif is_running_in_rundir(args.rundir):
|
|
logging.fatal(
|
|
"Munet processes using rundir: %s, use `--kill` to cleanup first", rundir
|
|
)
|
|
return 1
|
|
|
|
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)
|
|
|
|
if args.kill:
|
|
return 0
|
|
|
|
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
|
|
|
|
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)
|