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