586 lines
14 KiB
Python
586 lines
14 KiB
Python
# Better case
|
|
import logging
|
|
import os
|
|
import sys
|
|
from enum import Enum
|
|
from pprint import pformat, pprint
|
|
from types import SimpleNamespace
|
|
|
|
import coloredlogs
|
|
import typer
|
|
from typing_extensions import Annotated
|
|
|
|
from iam.app import App
|
|
from iam.framework import (empty, get_app_logger, iterate_any, open_yaml,
|
|
prune, to_csv, to_json, to_yaml)
|
|
from rich import box
|
|
from rich.console import Console
|
|
from rich.pretty import pprint
|
|
from rich.prompt import Prompt
|
|
from rich.table import Table
|
|
|
|
from . import exceptions as error
|
|
# from .cli_views import OutputFormat #, view_list, view_show
|
|
from .cli_views import ViewItem, ViewList
|
|
|
|
IamException = error.IamException
|
|
|
|
|
|
# Global vars
|
|
# ======================
|
|
|
|
|
|
# See: https://docs.python.org/3.8/library/logging.html#logging-levels
|
|
DEFAULT_LOG_LEVEL = 30 # warning
|
|
|
|
# Get default ident
|
|
DEBUG = os.environ.get("IAM_DEBUG", "False")
|
|
DEFAULT_IDENT = os.environ.get("IAM_IDENT", os.environ.get("SHELL_IDENT", ""))
|
|
DEFAULT_CONFIG = os.environ.get("IAM_CONFIG", None)
|
|
BOX_STYLE = box.MINIMAL
|
|
TABLE_MIN_WIDTH = 80
|
|
|
|
table_box_params = {
|
|
"box": box.ASCII2,
|
|
"min_width": 60,
|
|
}
|
|
|
|
|
|
# Init app
|
|
# ======================
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
console = Console()
|
|
item_view = ViewItem(output=console.print)
|
|
list_view = ViewList(output=console.print)
|
|
|
|
|
|
# General CLI
|
|
# ======================
|
|
cli = typer.Typer(rich_markup_mode="rich")
|
|
|
|
|
|
@cli.callback()
|
|
def cli_callback(
|
|
ctx: typer.Context,
|
|
verbose: Annotated[int, typer.Option("--verbose", "-v", count=True, max=3)] = 0,
|
|
config: Annotated[
|
|
str, typer.Option("--config", "-C", help="Configuration directory")
|
|
] = DEFAULT_CONFIG,
|
|
fmt: Annotated[
|
|
list_view.formats_enum, typer.Option("--format", "-F", help="Output format")
|
|
] = list_view.default_format,
|
|
ident: str = DEFAULT_IDENT,
|
|
):
|
|
# Calculate log level
|
|
log_level = DEFAULT_LOG_LEVEL - verbose * 10
|
|
log_level = log_level if log_level > 0 else 10
|
|
|
|
# Define loggers
|
|
loggers = {
|
|
"iam": {"level": log_level},
|
|
"iam.cli": {"level": "INFO"},
|
|
}
|
|
|
|
# Instanciate logger
|
|
get_app_logger(loggers=loggers, level=log_level, colors=True)
|
|
|
|
cli_config = SimpleNamespace(
|
|
log_level=log_level,
|
|
fmt=fmt,
|
|
ident=ident,
|
|
)
|
|
render_config = {
|
|
"fmt": fmt,
|
|
"table_settings": table_box_params,
|
|
}
|
|
|
|
render_item = lambda *args, conf={}: item_view.render(
|
|
*args, conf={**render_config, **conf}
|
|
)
|
|
render_list = lambda *args, conf={}: list_view.render(
|
|
*args, conf={**render_config, **conf}
|
|
)
|
|
|
|
# Instanciate app
|
|
logger.info(f"Current ident: {ident}")
|
|
ctx.obj = SimpleNamespace(
|
|
app=App(config_path=config, ident=ident),
|
|
cli=cli_config,
|
|
render_item=render_item,
|
|
render_list=render_list,
|
|
)
|
|
|
|
|
|
@cli.command("dump")
|
|
def main_dump(ctx: typer.Context):
|
|
"Dump ident configuration"
|
|
|
|
ctx.obj.app.user_dump()
|
|
|
|
|
|
@cli.command("help")
|
|
def main_dump(ctx: typer.Context):
|
|
"Dump ident configuration"
|
|
|
|
# pprint(ctx.parent.__dict__)
|
|
|
|
# pprint (cli.__dict__)
|
|
# pprint (dir(cli))
|
|
# # help(cli.__class__)
|
|
|
|
print("commands")
|
|
for item in cli.registered_commands:
|
|
pprint(item.__dict__)
|
|
|
|
# get_help(ctx)
|
|
|
|
# print ("OUTTT")
|
|
# pprint ((cli.registered_commands[0]))
|
|
# pprint ((cli.registered_commands[0].__dict__))
|
|
|
|
|
|
@cli.command("list")
|
|
def main_list(ctx: typer.Context):
|
|
"List all items"
|
|
|
|
ctx.invoke(kind_list, ctx)
|
|
ctx.invoke(res_list, ctx)
|
|
ctx.invoke(svc_list, ctx)
|
|
|
|
|
|
# Resource Kinds management
|
|
# ======================
|
|
# cli_kind = typer.Typer()
|
|
|
|
|
|
class KindCmd(str, Enum):
|
|
"Kinds sub commands"
|
|
LS = "list"
|
|
SHOW = "show"
|
|
|
|
|
|
@cli.command(
|
|
"kind",
|
|
hidden=True,
|
|
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
)
|
|
def cli_kind(ctx: typer.Context, cmd: KindCmd):
|
|
"""Command sources"""
|
|
prefix = "kind_"
|
|
func = globals()[f"{prefix}{cmd.value}"]
|
|
return ctx.invoke(func, ctx)
|
|
|
|
|
|
@cli.command("kind list", rich_help_panel="Resources Kind")
|
|
def kind_list(
|
|
ctx: typer.Context,
|
|
name: Annotated[str, typer.Argument()] = "",
|
|
):
|
|
"List resource kinds"
|
|
|
|
# Fetch data
|
|
filter = name
|
|
resources_kind = ctx.obj.app.catalog.resources_kind
|
|
data = []
|
|
for name, item in sorted(resources_kind.items(), key=lambda key: key):
|
|
if filter and not name.startswith(filter):
|
|
continue
|
|
data.append((name, item.desc))
|
|
|
|
# Render data
|
|
columns = ["name", "desc"]
|
|
conf = {
|
|
"table_title": "Resources kinds listing",
|
|
}
|
|
ctx.obj.render_list(data, columns, conf=conf)
|
|
|
|
|
|
@cli.command("kind show", rich_help_panel="Resources Kind")
|
|
def kind_show(
|
|
ctx: typer.Context,
|
|
name: Annotated[str, typer.Argument()] = "",
|
|
):
|
|
"Show resource kinds"
|
|
|
|
# Fetch data
|
|
columns = ["name", "desc", "input", "needs", "remap"]
|
|
filter = name
|
|
resources_kind = ctx.obj.app.catalog.resources_kind
|
|
for name, item in sorted(resources_kind.items(), key=lambda key: key):
|
|
if filter and not name.startswith(filter):
|
|
continue
|
|
|
|
data = [
|
|
item.name,
|
|
item.desc,
|
|
f"{to_yaml(item.input).strip()}",
|
|
f"{to_yaml(item.needs)}".strip(),
|
|
f"{to_yaml(item.remap)}".strip(),
|
|
]
|
|
|
|
# Render data
|
|
conf = {
|
|
"table_title": f"Resources kind: {data[0]}",
|
|
}
|
|
ctx.obj.render_item(data, columns, fmt=fmt, conf=conf)
|
|
|
|
|
|
# Resource management
|
|
# ======================
|
|
# cli_res = typer.Typer()
|
|
|
|
|
|
class ResCmds(str, Enum):
|
|
"Resources sub commands"
|
|
LS = "list"
|
|
SHOW = "show"
|
|
|
|
|
|
@cli.command(
|
|
"res",
|
|
hidden=True,
|
|
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
)
|
|
def cli_res(ctx: typer.Context, cmd: ResCmds):
|
|
"""Command sources"""
|
|
pprint(ctx.__dict__)
|
|
prefix = "res_"
|
|
func = globals()[f"{prefix}{cmd.value}"]
|
|
ctx.invoke(res_list, ctx)
|
|
# ctx.invoke(func, ctx)
|
|
|
|
|
|
@cli.command("res list", rich_help_panel="Resources")
|
|
def res_list(
|
|
ctx: typer.Context,
|
|
name: Annotated[str, typer.Argument()] = "",
|
|
):
|
|
"List resource kinds"
|
|
|
|
# Fetch data
|
|
filter = name
|
|
resources = ctx.obj.app.catalog.resources
|
|
data = []
|
|
for name, res in sorted(resources.items(), key=lambda key: key):
|
|
if filter and not name.startswith(filter):
|
|
continue
|
|
data.append(
|
|
(
|
|
res.get_kind(),
|
|
res.get_name(),
|
|
res.desc,
|
|
)
|
|
)
|
|
|
|
# Render data
|
|
columns = ["kind", "name", "desc"]
|
|
conf = {
|
|
"table_title": "Resources listing",
|
|
}
|
|
ctx.obj.render_list(data, columns, conf=conf)
|
|
|
|
|
|
@cli.command("res show", rich_help_panel="Resources")
|
|
def res_show(
|
|
ctx: typer.Context,
|
|
name: Annotated[str, typer.Argument()] = "",
|
|
all: bool = False,
|
|
):
|
|
"Show resource"
|
|
|
|
# Fetch data
|
|
columns = ["name", "uses", "input"]
|
|
columns_all = columns + ["active", "deps", "missing", "loop_max", "loop", "need"]
|
|
filter = name
|
|
resources = ctx.obj.app.catalog.resources.select("startswith", name)
|
|
for name, item in sorted(resources.items(), key=lambda key: key):
|
|
if filter and not name.startswith(filter):
|
|
continue
|
|
|
|
if not all and not item.is_active():
|
|
continue
|
|
|
|
data = [
|
|
name,
|
|
to_yaml(item.uses).strip(),
|
|
to_yaml(item.input).strip(),
|
|
]
|
|
if all:
|
|
data.extend(
|
|
[
|
|
item.is_active(),
|
|
",".join([x.name for x in item.resources_deps]),
|
|
",".join(item.resources_missing),
|
|
item.loop_limit,
|
|
to_yaml(item.loop).strip(),
|
|
to_yaml(item.needs).strip(),
|
|
]
|
|
)
|
|
|
|
# Render data
|
|
ctx.obj.render_item(
|
|
data,
|
|
columns_all if all else columns,
|
|
conf={
|
|
"table_title": f"Resources kind: {data[0]}",
|
|
},
|
|
)
|
|
|
|
|
|
# Services management
|
|
# ======================
|
|
|
|
|
|
class ServiceCmds(str, Enum):
|
|
"Serviceources sub commands"
|
|
LS = "list"
|
|
SHOW = "show"
|
|
|
|
|
|
@cli.command(
|
|
"svc",
|
|
hidden=True,
|
|
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
)
|
|
def cli_svc(ctx: typer.Context, cmd: ServiceCmds):
|
|
"""Command sources"""
|
|
prefix = "svc_"
|
|
func = globals()[f"{prefix}{cmd.value}"]
|
|
return ctx.invoke(func, ctx)
|
|
|
|
|
|
@cli.command("svc list", rich_help_panel="Services")
|
|
def svc_list(
|
|
ctx: typer.Context, name: Annotated[str, typer.Argument()] = "", all: bool = False
|
|
):
|
|
# filter: str = ""):
|
|
"List services"
|
|
|
|
columns = ["name", "desc"]
|
|
filter = name
|
|
services = ctx.obj.app.catalog.services
|
|
data = []
|
|
for name, res in sorted(services.items(), key=lambda key: key[1].name):
|
|
if filter and not name.startswith(filter):
|
|
continue
|
|
|
|
# if not all and not res.is_active():
|
|
# continue
|
|
|
|
data.append(
|
|
(
|
|
# res.get_kind(),
|
|
# res.get_name(),
|
|
res.name,
|
|
res.desc,
|
|
)
|
|
)
|
|
|
|
# Render data
|
|
ctx.obj.render_list(
|
|
data,
|
|
columns,
|
|
conf={
|
|
"table_title": f"Services listing",
|
|
},
|
|
)
|
|
|
|
|
|
@cli.command("svc show", rich_help_panel="Services")
|
|
def svc_show(
|
|
ctx: typer.Context,
|
|
name: Annotated[str, typer.Argument()] = "",
|
|
all: bool = False,
|
|
):
|
|
"Show service"
|
|
|
|
columns = [
|
|
"name",
|
|
"desc",
|
|
"enabled",
|
|
"input",
|
|
"commands",
|
|
"required_svc",
|
|
"resource_lookup",
|
|
"rest_matches" "dump",
|
|
]
|
|
columns_all = columns + ["active", "deps", "missing", "loop_max", "loop", "need"]
|
|
|
|
output = {}
|
|
services = ctx.obj.app.catalog.services.select("startswith", name)
|
|
for name, svc in services.items():
|
|
assert name == svc.name
|
|
|
|
data = [
|
|
svc.name,
|
|
svc.desc,
|
|
svc.enabled,
|
|
svc.input,
|
|
to_yaml(svc.list_cmds()).strip(),
|
|
svc.required_services,
|
|
svc.resources_lookup,
|
|
svc.resources_matches,
|
|
svc.dump_item(),
|
|
]
|
|
data = [str(item) for item in data]
|
|
|
|
ctx.obj.render_item(
|
|
data,
|
|
columns_all if all else columns,
|
|
conf={
|
|
"table_title": f"Service show: {data[0]}",
|
|
},
|
|
)
|
|
|
|
|
|
@cli.command("svc commands", rich_help_panel="Services")
|
|
def svc_commands(ctx: typer.Context):
|
|
"List services commands"
|
|
|
|
ret = {
|
|
"shell": {},
|
|
"cmds": {},
|
|
}
|
|
for svc_name, svc in ctx.obj.app.list_services().items():
|
|
cmds = svc.list_cmds()
|
|
for source in ["shell", "cmds"]:
|
|
items = cmds[source]
|
|
target = ret[source]
|
|
for cmd_name, conf in items.items():
|
|
target[cmd_name] = conf
|
|
|
|
pprint(ret, expand_all=True)
|
|
|
|
|
|
@cli.command("svc run", rich_help_panel="Services")
|
|
def svc_run(ctx: typer.Context, name: str, command: str):
|
|
"Run service command"
|
|
|
|
ret = ctx.obj.app.services.get(name, resolve=True)
|
|
tmp = ret.run_cmd(command)
|
|
pprint(tmp)
|
|
|
|
|
|
# Shell management
|
|
# ======================
|
|
|
|
|
|
class ShellCmds(str, Enum):
|
|
"Shellources sub commands"
|
|
ENABLE = "enable"
|
|
DISABLE = "disable"
|
|
ORDER = "order"
|
|
IDENTS = "idents"
|
|
|
|
|
|
@cli.command(
|
|
"shell",
|
|
hidden=True,
|
|
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
)
|
|
def cli_shell(ctx: typer.Context, cmd: ShellCmds):
|
|
"""Command sources"""
|
|
prefix = "shell_"
|
|
func = globals()[f"{prefix}{cmd.value}"]
|
|
return ctx.forward(func)
|
|
# return ctx.invoke(func, ctx)
|
|
|
|
|
|
@cli.command("shell idents", rich_help_panel="Shell")
|
|
def shell_idents(ctx: typer.Context, filter: str = ""):
|
|
"List idents"
|
|
|
|
ret = ctx.obj.app.get_idents()
|
|
|
|
print(" ".join(ret))
|
|
|
|
# pprint(ret, expand_all=True)
|
|
|
|
|
|
@cli.command("shell order", rich_help_panel="Shell")
|
|
def shell_order(ctx: typer.Context, reverse: bool = False):
|
|
"List services by loading order"
|
|
|
|
table = Table(title="Identity services", **table_box_params)
|
|
table.add_column("Order", style="magenta")
|
|
table.add_column("Service", style="cyan") # , justify="right", , no_wrap=True)
|
|
|
|
# services , _ = ctx.obj.app.catalog.resolve_service_order(reverse=reverse)
|
|
services, _ = ctx.obj.app.catalog.services.get_loading_order(reverse=reverse)
|
|
for index, name in enumerate(services):
|
|
table.add_row(str(index), name)
|
|
|
|
console.print(table)
|
|
|
|
|
|
@cli.command("shell enable", rich_help_panel="Shell")
|
|
def shell_enable(ctx: typer.Context, ident):
|
|
"Enable shell"
|
|
|
|
app = ctx.obj.app
|
|
|
|
# Disable existing
|
|
# logger.info(f"Disabling ident: {app.ident_name}")
|
|
ret1 = app.shell_disable()
|
|
print("-- %< --" * 8)
|
|
print(ret1)
|
|
print("-- %< --" * 8)
|
|
|
|
# Enable new
|
|
# logger.info(f"Enabling ident: {ident}")
|
|
app.init_ident(ident)
|
|
ret2 = app.shell_enable()
|
|
|
|
# Output
|
|
print("-- %< --" * 8)
|
|
print(ret2)
|
|
print("-- %< --" * 8)
|
|
|
|
|
|
@cli.command("shell disable", rich_help_panel="Shell")
|
|
def shell_disable(ctx: typer.Context):
|
|
"Disable shell"
|
|
|
|
ret = ctx.obj.app.shell_disable()
|
|
|
|
print("-- %< --" * 8)
|
|
print(ret)
|
|
print("-- %< --" * 8)
|
|
|
|
|
|
# Merge all subcommands
|
|
# ======================
|
|
|
|
# cli.add_typer(cli_shell, name="shell", help="Manage shell")
|
|
# cli.add_typer(cli_res, name="res", help="Manage resources")
|
|
# cli.add_typer(cli_svc, name="svc", help="Manage services")
|
|
# cli.add_typer(cli_kind, name="kind", help="Manage resource kind")
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
def run():
|
|
"Run cli"
|
|
|
|
if DEBUG.lower() in ["true", "y", "1", "t"]:
|
|
cli()
|
|
else:
|
|
try:
|
|
cli()
|
|
except IamException as err:
|
|
_msg = f"Iam exited with {type(err).__name__} error: {err}"
|
|
logger.error(_msg)
|
|
sys.exit(1)
|
|
|
|
# traceback.print_stack()
|
|
# logger.warning
|
|
|
|
|
|
# Cli bootsrapping
|
|
# ======================
|
|
if __name__ == "__main__":
|
|
run()
|