Adicionar src/renatoxsr-py/scripts/composectl.py
This commit is contained in:
@@ -0,0 +1,353 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# vim: set: ft=python ts=4 sw=4 et :
|
||||||
|
# python3 << '#EOF'
|
||||||
|
|
||||||
|
try:
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
from collections import namedtuple
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
|
import datetime as dt
|
||||||
|
from enum import Enum, IntEnum, StrEnum, property as enum_property
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from typing import TextIO, NamedTuple
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
args = [e.msg]
|
||||||
|
msg = ["[%s]"]
|
||||||
|
if e.name and e.path:
|
||||||
|
args.append(e.name)
|
||||||
|
args.append(e.path)
|
||||||
|
msg.append("('%s' : '%s')")
|
||||||
|
else:
|
||||||
|
msg.append("('%s')")
|
||||||
|
if e.name:
|
||||||
|
args.append(e.name)
|
||||||
|
if e.path:
|
||||||
|
args.append(e.path)
|
||||||
|
|
||||||
|
#logging.basicConfig(level=logging.DEBUG)
|
||||||
|
#logger = logging.getLogger(__name__)
|
||||||
|
#logger.critical(msg, *args)
|
||||||
|
print(msg % tuple(args))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
#print("import success")
|
||||||
|
|
||||||
|
# ***
|
||||||
|
|
||||||
|
# Globals and constants
|
||||||
|
|
||||||
|
# ANSI CODES
|
||||||
|
def CSI(code) -> str:
|
||||||
|
return "\033[" + str(code) + "m"
|
||||||
|
|
||||||
|
COLOR_RESET = CSI(0)
|
||||||
|
|
||||||
|
class COLOR(IntEnum):
|
||||||
|
BLACK = 0 #\033[40m\033[90m\033[100m
|
||||||
|
RED = 1 #\033[41m\033[91m\033[101m
|
||||||
|
GREEN = 2 #\033[42m\033[92m\033[102m
|
||||||
|
YELLOW= 3 #\033[43m\033[93m\033[103m
|
||||||
|
BLUE = 4 #\033[44m\033[94m\033[104m
|
||||||
|
MAGENTA = 5 #\033[45m\033[95m\033[105m
|
||||||
|
CYAN = 6 #\033[46m\033[96m\033[106m
|
||||||
|
WHITE = 7 #\033[47m\033[97m\033[107m
|
||||||
|
FG = 30
|
||||||
|
BG = 40
|
||||||
|
|
||||||
|
@enum_property
|
||||||
|
def reset(cls):
|
||||||
|
return CSI(0)
|
||||||
|
|
||||||
|
@enum_property
|
||||||
|
def fg(self):
|
||||||
|
return CSI(COLOR.FG + self)
|
||||||
|
|
||||||
|
@enum_property
|
||||||
|
def bg(self):
|
||||||
|
return CSI(COLOR.BG + self)
|
||||||
|
|
||||||
|
|
||||||
|
# ***
|
||||||
|
|
||||||
|
# Global Setup
|
||||||
|
|
||||||
|
# Setup logger
|
||||||
|
#logging.basicConfig(level=logging.DEBUG, format=LOGFMT)
|
||||||
|
#for handler in logging.root.handlers[:]:
|
||||||
|
# logging.root.removeHandler(handler)
|
||||||
|
# handler.close()
|
||||||
|
|
||||||
|
# Log utils
|
||||||
|
LEVEL_COLOR: dict[int, str] = {
|
||||||
|
logging.DEBUG: COLOR.CYAN.fg,
|
||||||
|
logging.INFO: COLOR.MAGENTA.fg,
|
||||||
|
logging.WARNING: COLOR.YELLOW.fg,
|
||||||
|
logging.ERROR: COLOR.RED.fg,
|
||||||
|
logging.CRITICAL: COLOR.RED.bg + COLOR.WHITE.fg,
|
||||||
|
}
|
||||||
|
|
||||||
|
LEVEL_NAME: dict[int, str] = {
|
||||||
|
logging.DEBUG: "DEBUG",
|
||||||
|
logging.INFO: "INFO",
|
||||||
|
logging.WARNING: "WARN",
|
||||||
|
logging.ERROR: "ERR",
|
||||||
|
logging.CRITICAL: "CRIT",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LevelColorFormatter(logging.Formatter):
|
||||||
|
def format(self, record):
|
||||||
|
LOGFMT = ("[%(name)s][%(filename)s" +
|
||||||
|
#":%(funcName)s" +
|
||||||
|
"(%(lineno)04d)]" +
|
||||||
|
"[%(relativeCreated)d]%(levelname)5s: %(message)s")
|
||||||
|
record.relativeCreatedSeconds = record.relativeCreated / 1000.0
|
||||||
|
record.levelname = LEVEL_NAME.get(record.levelno, "UNSET")
|
||||||
|
formatter = logging.Formatter(f"{LEVEL_COLOR[record.levelno]}" +
|
||||||
|
LOGFMT.replace("%(relativeCreated)d","%(relativeCreatedSeconds).03f")
|
||||||
|
+ COLOR_RESET)
|
||||||
|
return formatter.format(record)
|
||||||
|
|
||||||
|
|
||||||
|
# ***
|
||||||
|
# Get module logger
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
# Console Logger
|
||||||
|
stderr_handler = logging.StreamHandler(stream=sys.stderr)
|
||||||
|
stderr_handler.setFormatter(LevelColorFormatter())
|
||||||
|
stderr_handler.setLevel(logging.DEBUG)
|
||||||
|
logger.addHandler(stderr_handler)
|
||||||
|
#print(logger.handlers)
|
||||||
|
|
||||||
|
# ***
|
||||||
|
|
||||||
|
|
||||||
|
MIN_VERSION = NamedTuple("MIN_VERSION", [
|
||||||
|
("major", int),
|
||||||
|
("minor", int),
|
||||||
|
("micro", int),
|
||||||
|
("releaselevel", str),
|
||||||
|
("serial", int)])(
|
||||||
|
major=3,
|
||||||
|
minor=11,
|
||||||
|
micro=None,
|
||||||
|
releaselevel=None,
|
||||||
|
serial=None)
|
||||||
|
|
||||||
|
python_requires = f">={MIN_VERSION.major}{MIN_VERSION.minor}"
|
||||||
|
|
||||||
|
# Check python version
|
||||||
|
def check_python_version(version_info, min_version):
|
||||||
|
logger.debug(version_info)
|
||||||
|
#print(version_info)
|
||||||
|
if version_info < min_version:
|
||||||
|
logger.critical("Insufficient Python version: %d.%d. (needs at least: %d.%d)",
|
||||||
|
version_info[0],
|
||||||
|
version_info[1],
|
||||||
|
min_version[0],
|
||||||
|
min_version[1])
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Run check
|
||||||
|
check_python_version(sys.version_info, MIN_VERSION)
|
||||||
|
|
||||||
|
# Package version
|
||||||
|
def set_package_version(file):
|
||||||
|
mtime = Path(file).stat().st_mtime
|
||||||
|
version = dt.datetime.fromtimestamp(mtime).strftime("v%Y-%m-%d.%H-%M-%S.%f")
|
||||||
|
logger.debug("%s:%s", file, version)
|
||||||
|
return version
|
||||||
|
|
||||||
|
# Set version
|
||||||
|
__version__ = set_package_version(__file__)
|
||||||
|
|
||||||
|
# Global Functions
|
||||||
|
|
||||||
|
def cmd(compose_cmd: str | Path, compose_file: str | Path, *args, **kwargs):
|
||||||
|
if isinstance(compose_cmd, str):
|
||||||
|
compose_bin, compose_args = compose_cmd.split(" ", maxsplit=1)
|
||||||
|
bin_path = shutil.which(compose_bin)
|
||||||
|
logger.debug("BIN: %s", bin_path)
|
||||||
|
full_cmd = [bin_path]
|
||||||
|
if " " in compose_args:
|
||||||
|
full_cmd.extend(compose_args.split(" "))
|
||||||
|
else:
|
||||||
|
full_cmd.append(compose_args)
|
||||||
|
if not compose_file:
|
||||||
|
compose_file = "compose.yml"
|
||||||
|
compose_fpath = Path(compose_file).resolve()
|
||||||
|
if not compose_fpath.exists():
|
||||||
|
logger.error("Compose file does not exist: '%s'", compose_file)
|
||||||
|
return None
|
||||||
|
full_cmd.extend(["-f", compose_fpath])
|
||||||
|
for a in args:
|
||||||
|
if isinstance(a, str):
|
||||||
|
full_cmd.append(a)
|
||||||
|
elif isinstance(a, Mapping):
|
||||||
|
for k,v in a.items():
|
||||||
|
if " " in k:
|
||||||
|
logger.error("Invalid key name: %s", k)
|
||||||
|
continue
|
||||||
|
if len(k) > 1:
|
||||||
|
full_cmd.append(f"--{k}")
|
||||||
|
else:
|
||||||
|
full_cmd.append(f"-{k}")
|
||||||
|
full_cmd.append(str(v))
|
||||||
|
elif isinstance(a, Sequence):
|
||||||
|
full_cmd.extend(a)
|
||||||
|
else:
|
||||||
|
full_cmd.append(a)
|
||||||
|
logger.debug("CMD: %s", repr(full_cmd))
|
||||||
|
return full_cmd
|
||||||
|
|
||||||
|
# def log_wait(i, td, outs, errs):
|
||||||
|
# #if outs is None and errs is None:
|
||||||
|
# logger.debug("[%d] %d.%03d", i, td.seconds, td.microseconds % 1000)
|
||||||
|
# if outs is not None:
|
||||||
|
# sys.stdout.write(
|
||||||
|
# COLOR.GREEN.fg +
|
||||||
|
# (outs.decode(encoding="utf-8") if isinstance(outs, bytes) else outs)
|
||||||
|
# + COLOR_RESET)
|
||||||
|
# sys.stdout.flush()
|
||||||
|
# if errs is not None:
|
||||||
|
# sys.stderr.write(
|
||||||
|
# COLOR.RED.fg +
|
||||||
|
# (errs.decode(encoding="utf-8") if isinstance(errs, bytes) else errs)
|
||||||
|
# + COLOR_RESET)
|
||||||
|
# sys.stderr.flush()
|
||||||
|
|
||||||
|
|
||||||
|
async def runcmd(cmd, name: str = None):
|
||||||
|
cmd_exec = cmd[0]
|
||||||
|
cmd_args = cmd[1:]
|
||||||
|
logger.debug("EXEC: '%s'", cmd_exec)
|
||||||
|
logger.debug("ARGS: ['%s']", "', '".join([str(c) for c in cmd_args]))
|
||||||
|
start = dt.datetime.now()
|
||||||
|
i = 0
|
||||||
|
async with asyncio.TaskGroup() as tg:
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
cmd_exec, *cmd_args,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE)
|
||||||
|
|
||||||
|
tg.create_task(read_stream(proc.stdout, sys.stdout,
|
||||||
|
name=(name or "STDOUT"),
|
||||||
|
color=COLOR.GREEN.fg,
|
||||||
|
start_time=start))
|
||||||
|
tg.create_task(read_stream(proc.stderr, sys.stderr,
|
||||||
|
name=(name or "STDERR"),
|
||||||
|
color=COLOR.RED.fg,
|
||||||
|
start_time=start))
|
||||||
|
return_code = await proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
async def read_stream(
|
||||||
|
stream_from: TextIO,
|
||||||
|
stream_to: TextIO,
|
||||||
|
name: str=None,
|
||||||
|
color: str = None,
|
||||||
|
start_time: dt.datetime = None,
|
||||||
|
):
|
||||||
|
while True:
|
||||||
|
line = await stream_from.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
if start_time:
|
||||||
|
td = dt.datetime.now() - start_time
|
||||||
|
delta = "%d.%03d " % (td.seconds, td.microseconds % 1000)
|
||||||
|
stream_to.write(
|
||||||
|
(color or "") +
|
||||||
|
(f"[{name}] " if name else "") +
|
||||||
|
(delta if start_time else "") +
|
||||||
|
line.decode("raw_unicode_escape") + COLOR_RESET)
|
||||||
|
stream_to.flush()
|
||||||
|
|
||||||
|
|
||||||
|
# Configured Actions
|
||||||
|
class ACTION(Enum):
|
||||||
|
UP = ("--progress=plain", "up", "-d", "--remove-orphans")
|
||||||
|
UPDB = ("--progress=plain", "up","-d","--build", "--pull", "always", "--remove-orphans")
|
||||||
|
DOWN = ("--progress=plain", "down","--remove-orphans")
|
||||||
|
LOGS = ("--progress=plain", "logs",)
|
||||||
|
LOGSF = ("--progress=plain", "logs","-f")
|
||||||
|
LS = ("--progress=plain", "ls",)
|
||||||
|
BUILD = ("--progress=plain", "build", "--pull", "--push")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logger.debug(f"CWD:\t{os.getcwd()}")
|
||||||
|
p = argparse.ArgumentParser()
|
||||||
|
p.add_argument("--root", type=Path, default="/srv/containers")
|
||||||
|
#p.add_argument("--prefix", default="", help="Prefix of compose files")
|
||||||
|
#p.add_argument("--suffix", default="", help="Suffix to find compose files, before .yml extension")
|
||||||
|
p.add_argument("--subdir", default="", help="Subdir to find containers")
|
||||||
|
p.add_argument("--name", default="*", help="Name of folder to find compose.yml file")
|
||||||
|
p.add_argument("--cmd", default="docker compose", help="compose command")
|
||||||
|
p.add_argument("--file", default="compose.yml")
|
||||||
|
p.add_argument("action", choices=[a.name.lower() for a in ACTION])
|
||||||
|
args = p.parse_args()
|
||||||
|
logger.debug("Args:")
|
||||||
|
for k,v in vars(args).items():
|
||||||
|
logger.debug(" - %s:\t%s", k, repr(v))
|
||||||
|
action = ACTION[args.action.upper()].value
|
||||||
|
logger.debug("Action:\t('%s')", "', '".join(action))
|
||||||
|
try:
|
||||||
|
logger.debug("Starting runner context")
|
||||||
|
with asyncio.Runner() as runner:
|
||||||
|
i = 0
|
||||||
|
root_path = args.root.joinpath(args.subdir)
|
||||||
|
glob_pattern = args.name+"/*"+args.file
|
||||||
|
logger.debug("Globbing '%s' in '%s'", glob_pattern, root_path)
|
||||||
|
for path in root_path.glob(glob_pattern):
|
||||||
|
i += 1
|
||||||
|
logger.info("[%02d] File: '%s'", i, str(path))
|
||||||
|
runner.run(
|
||||||
|
runcmd(
|
||||||
|
cmd(args.cmd, path, action),
|
||||||
|
name=path.parent.name))
|
||||||
|
logger.info("Found %d matches in '%s' (pattern: %s", i, root_path, glob_pattern)
|
||||||
|
except asyncio.CancelledError as e:
|
||||||
|
logger.critical("Cancel task: %s (%s)", str(e), path)
|
||||||
|
asyncio.sleep(0.5)
|
||||||
|
except RuntimeError as e:
|
||||||
|
logger.critical(str(e))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
sys.exit(main())
|
||||||
|
except KeyboardInterrupt as sigint:
|
||||||
|
logger.critical("[KeyboardInterrupt] %s", str(sigint))
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
if loop.is_running():
|
||||||
|
all_tasks = asyncio.all_tasks(loop)
|
||||||
|
for task in all_tasks:
|
||||||
|
task.cancel()
|
||||||
|
loop.run_until_complete(
|
||||||
|
asyncio.gather(
|
||||||
|
*all_tasks,
|
||||||
|
#return_exceptions=True
|
||||||
|
return_exceptions=False
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error during task gathering: %s", str(e))
|
||||||
|
finally:
|
||||||
|
logger.info("Exit.")
|
||||||
|
sys.exit(1)
|
||||||
|
except RuntimeError as e:
|
||||||
|
logger.critical("[RuntimeError] %s", str(e))
|
||||||
|
#sys.exit(1)
|
||||||
|
|
||||||
|
#EOF
|
||||||
Reference in New Issue
Block a user