Better restructure, and improved debug

This commit is contained in:
Cian Hughes
2023-11-01 13:45:08 +00:00
parent f5816ee17e
commit cba4743035
16 changed files with 454 additions and 219 deletions

View File

@@ -1,24 +0,0 @@
from functools import wraps
import inspect
from typing import Callable
import typer # type: ignore
import config
def debug_guard(f: Callable) -> Callable:
if not config.DEBUG:
return f
try:
import snoop # type: ignore
except ImportError:
typer.echo("Debug mode requires the snoop package")
raise typer.Exit(1)
else:
snoop.install(
snoop="ss",
)
typer.echo(f"Debug mode enabled: {inspect.stack()[1].filename}")
wraps(f)(ss)(f) # noqa: F821 #* ss is installed in debug_mode

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env poetry run python
from autoignition import json_to_img
import config
from create_disk import create_ignition_disk
from create_img import create_img
import typer
app = typer.Typer(
help="A tool for creating ignition images for automated deployment to a swarm"
)
app.command()(create_img)
app.command()(create_ignition_disk)
app.command()(json_to_img)
if __name__ == "__main__":
config.update_config("cli")
app()

View File

View File

@@ -0,0 +1,31 @@
#!/usr/bin/env python
def main() -> None:
from . import config
config.update_config("cli")
from .node_deployer import app
app()
def debug():
from . import config
config.update_config("debug")
from .node_deployer import app
# Below, we set the default value of the debug flag
# for the base function of each command to True
def unwrap(f):
if hasattr(f, "__wrapped__"):
return unwrap(f.__wrapped__)
else:
return f
for c in app.registered_commands:
f = unwrap(c.callback)
defaults = list(f.__defaults__)
defaults[-1] = True
f.__defaults__ = tuple(defaults)
app()
if __name__ == "__main__":
main()

View File

@@ -5,16 +5,17 @@ import tarfile
import time
from typing import Annotated
from cli import cli_spinner
import config
from debug import debug_guard
import git
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
import typer
from utils import ensure_build_dir
from . import config
from .cli import cli_spinner
from .debug import debug_guard
from .utils import ensure_build_dir
def create_driver():
@@ -114,14 +115,39 @@ def build_fuelignition():
@cli_spinner(description="Converting json to img", total=None)
@ensure_build_dir
def json_to_img(
json_path: Path = Annotated[
json_path: Annotated[
Path,
typer.Option(help="The fuel-ignition json for configuring the disk image", prompt=True),
],
img_path: Path = Annotated[
typer.Option(
"--json-path", "-i",
help="The fuel-ignition json for configuring the disk image",
prompt=True,
exists=True,
dir_okay=False,
),
] = Path("fuelignition.json"),
img_path: Annotated[
Path,
typer.Option(help="The file to output the disk image to", prompt=True),
],
typer.Option(
"--img-path", "-o",
help="The file to output the disk image to",
prompt=True,
dir_okay=False,
writable=True,
readable=False,
),
] = Path("ignition.img"),
debug: Annotated[
bool,
typer.Option(
"--debug",
help="Enable debug mode",
is_eager=True,
is_flag=True,
flag_value=True,
expose_value=config.DEBUG,
hidden=not config.DEBUG,
)
] = False,
) -> None:
"""Takes a fuel-ignition json file and produces an ignition disk image file"""
selenium_container = None

View File

@@ -4,8 +4,8 @@ from typing import Callable
from rich.progress import Progress, SpinnerColumn, TextColumn
import config
from utils import Singleton
from . import config
from .utils import Singleton
class SingletonProgress(Progress, metaclass=Singleton):

View File

@@ -1,7 +1,8 @@
# flake8: noqa: F821
# type: ignore
#* This file sets a number of config constants by modifying its own globals
#* As a result, F821 is disabled as the intereter cannot be trusted to know
#* when F821 should be raised.
#* As a result, F821 and typing is disabled as the interpreter cannot be
#* trusted to know when F821 or UndefinedVeriable errors should be raised.
from pathlib import Path
@@ -10,7 +11,8 @@ import tomllib
CLIENT = docker.from_env(version="auto")
PROJECT_ROOT = Path(__file__).parent.parent.absolute()
MAX_PORT: int = 65535
PROJECT_ROOT: Path = Path(__file__).parent.parent.parent.absolute()
type ConfigLabel = str | list[str]
@@ -63,8 +65,9 @@ def update_config(config_label: ConfigLabel = "default") -> None:
apply_config(get_config(config_label))
def init() -> None:
globals().update(get_config())
def init(config) -> None:
globals().update(get_config(config))
update_config()
init()
init(config="default")

View File

@@ -1,14 +1,18 @@
from fnmatch import fnmatch
import ipaddress
from typing import Annotated
from docker.types import Mount
import typer
from cli import cli_spinner
import config
from create_img import create_img
from debug import debug_guard
from docker.types import Mount
from utils import ensure_build_dir
from . import config
from .cli import cli_spinner
from .create_img import create_img
from .debug import debug_guard
from .utils import ensure_build_dir
type IPAddress = ipaddress.IPv4Address | ipaddress.IPv6Address
def filter_validation_response(response: str) -> str:
@@ -66,35 +70,86 @@ def write_disk(disk: str) -> None:
@cli_spinner(description="Creating ignition initialisation disk", total=None)
@ensure_build_dir
def create_ignition_disk(
disk: str = Annotated[str, typer.Option(help="Path to the disk to write to", prompt=True)],
hostname: str = Annotated[str, typer.Option(help="Hostname for the new node", prompt=True)],
password: str = Annotated[
disk: Annotated[
str,
typer.Option(
"--disk",
"-d",
help="Path to the disk to write to",
prompt=True,
),
] = None,
hostname: Annotated[
str,
typer.Option(
"--hostname",
"-h",
help="Hostname for the new node",
prompt=True,
),
] = "node",
password: Annotated[
str,
typer.Option(
"--password",
"-p",
help="Password for the root user on the new node",
prompt=True,
confirmation_prompt=True,
hide_input=True,
),
],
switch_ip_address: str = Annotated[
str, typer.Option(help="IP address of the switch to connect to", prompt=True)
],
switch_port: int = Annotated[
int, typer.Option(help="Port on the switch to connect to", prompt=True)
],
swarm_token: str = Annotated[
str, typer.Option(help="Swarm token for connecting to the swarm", prompt=True)
],
] = None,
switch_ip: Annotated[
IPAddress,
typer.Option(
"--switch-ip",
"-ip",
help="IP address of the switch to connect to",
prompt=True,
parser=ipaddress.ip_address,
),
] = None,
switch_port: Annotated[
int,
typer.Option(
"--switch-port",
"-sp",
help="Port on the switch to connect to",
prompt=True,
min=1,
max=config.MAX_PORT,
),
] = 4789,
swarm_token: Annotated[
str,
typer.Option(
"--swarm-token",
"-t",
help="Swarm token for connecting to the swarm",
prompt=True,
),
] = None,
debug: Annotated[
bool,
typer.Option(
"--debug",
help="Enable debug mode",
is_eager=True,
is_flag=True,
flag_value=True,
hidden=not config.DEBUG,
)
] = False,
) -> None:
"""Writes an ignition image to the specified disk for easy deployment of new nodes to the swarm""" # noqa
create_img(
hostname,
password,
switch_ip_address,
switch_port,
swarm_token,
config.BUILD_DIR / "ignition.img",
hostname = hostname,
password = password,
switch_ip = switch_ip,
switch_port = switch_port,
swarm_token = swarm_token,
img_path = config.BUILD_DIR / "ignition.img",
debug = debug,
)
valid, response = validate()
if not valid:

View File

@@ -3,19 +3,20 @@ import json
from pathlib import Path
from typing import Annotated
from autoignition import json_to_img
from cli import cli_spinner
import config
from debug import debug_guard
import typer
from utils import ensure_build_dir
from . import config
from .autoignition import json_to_img
from .cli import cli_spinner
from .debug import debug_guard
from .utils import ensure_build_dir
MAX_PORT: int = 65535
type IPAddress = ipaddress.IPv4Address | ipaddress.IPv6Address
def load_template() -> dict:
with open(config.SRC_DIR / "templates" / "fuelignition.json", "r") as f:
with open(config.SRC_DIR / "templates/fuelignition.json", "r") as f:
out = json.load(f)
return out
@@ -31,8 +32,8 @@ def apply_ignition_settings(
ignition_config["login"]["users"][0]["passwd"] = password
# Add files that will define a service to ensure that the node joins the swarm
with open(config.SRC_DIR / "templates" / "join_swarm.sh", "r") as f1, open(
config.SRC_DIR / "templates" / "join_swarm.service", "r"
with open(config.SRC_DIR / "templates/join_swarm.sh", "r") as f1, open(
config.SRC_DIR / "templates/join_swarm.service", "r"
) as f2:
swarm_script, swarm_service = f1.read(), f2.read()
@@ -72,47 +73,82 @@ def apply_ignition_settings(
@cli_spinner(description="Creating ignition image", total=None)
@ensure_build_dir
def create_img(
hostname: str = Annotated[str, typer.Option(help="Hostname for the new node", prompt=True)],
password: str = Annotated[
hostname: Annotated[
str,
typer.Option(
"--hostname",
"-h",
help="Hostname for the new node",
prompt=True,
),
] = "node",
password: Annotated[
str,
typer.Option(
"--password",
"-p",
help="Password for the root user on the new node",
prompt=True,
confirmation_prompt=True,
hide_input=True,
),
],
switch_ip_address: str = Annotated[
str, typer.Option(help="IP address of the switch to connect to", prompt=True)
],
switch_port: int = Annotated[
int, typer.Option(
] = None,
switch_ip: Annotated[
IPAddress,
typer.Option(
"--switch-ip",
"-ip",
help="IP address of the switch to connect to",
prompt=True,
parser=ipaddress.ip_address,
),
] = None,
switch_port: Annotated[
int,
typer.Option(
"--switch-port",
"-sp",
help="Port on the switch to connect to",
prompt=True,
min=1,
max=MAX_PORT,
)
],
swarm_token: str = Annotated[
str, typer.Option(help="Swarm token for connecting to the swarm", prompt=True)
],
img_path: Path = Annotated[
max=config.MAX_PORT,
),
] = 4789,
swarm_token: Annotated[
str,
typer.Option(
"--swarm-token",
"-t",
help="Swarm token for connecting to the swarm",
prompt=True,
),
] = None,
img_path: Annotated[
Path,
typer.Option(
help="Path to the JSON file to be converted to an img",
default=Path.cwd() / "ignition.img",
"--img-path",
"-o",
help="Path to which the ignition image should be written",
dir_okay=False,
),
],
] = Path("ignition.img"),
debug: Annotated[
bool,
typer.Option(
"--debug",
help="Enable debug mode",
is_eager=True,
is_flag=True,
flag_value=True,
hidden=not config.DEBUG,
),
] = False,
) -> None:
"""Creates an ignition image for deploying a new node to the swarm"""
switch_ip_address = ipaddress.ip_address(switch_ip_address)
if switch_port > MAX_PORT:
raise ValueError(f"Port must be less than {MAX_PORT}")
# get swarm configuration as JSON
swarm_config = json.dumps(
{
"SWITCH_IP_ADDRESS": str(switch_ip_address),
"SWITCH_IP_ADDRESS": str(switch_ip),
"SWITCH_PORT": switch_port,
"SWARM_TOKEN": swarm_token,
}
@@ -132,7 +168,11 @@ def create_img(
json.dump(ignition_config, f, indent=4)
# convert ignition configuration to image
json_to_img(config.BUILD_DIR / "fuelignition.json", img_path)
json_to_img(
json_path=config.BUILD_DIR / "fuelignition.json",
img_path=img_path,
debug=debug,
)
if __name__ == "__main__":

View File

@@ -0,0 +1,39 @@
from functools import wraps
import inspect
from typing import Callable
import typer
from . import config
# def merge(f1: Callable) -> Callable:
# https://docs.python.org/3/library/functools.html#functools.update_wrapper
# wraps, but it combines the signatures of the two functions
# This will allow us to add/remove the `debug` arg depending on config context
def debug_guard(f: Callable) -> Callable:
if not config.DEBUG:
return f
try:
import snoop # type: ignore
except ImportError:
typer.echo("Debug mode requires the dev group to be installed")
raise typer.Exit(1)
else:
snoop.install(**config.snoop["install"])
@wraps(f)
def debug_mode(
*args,
**kwargs,
) -> Callable:
typer.echo(f"Debug mode enabled: {inspect.stack()[1].filename}")
if kwargs.get("debug", False):
# Snoop depth is set to compensate for wrapper stack frames
return snoop.snoop(**config.snoop["snoop"])(f)(*args, **kwargs) # noqa: F821 #* ss is installed in debug_mode
else:
return f(*args, **kwargs)
return debug_mode

View File

@@ -0,0 +1,24 @@
import typer
from . import config
from .autoignition import json_to_img
from .create_disk import create_ignition_disk
from .create_img import create_img
cmd_params = {
"no_args_is_help": True,
}
app = typer.Typer(
help="A tool for creating ignition images for automated deployment to a swarm",
**cmd_params,
)
app.command(**cmd_params)(create_img)
app.command(**cmd_params)(create_ignition_disk)
app.command(**cmd_params)(json_to_img)
if __name__ == "__main__":
config.update_config("cli")
app()

View File

@@ -2,7 +2,7 @@ from functools import wraps
from pathlib import Path
from typing import Callable
import config
from . import config
def ensure_build_dir(f: Callable) -> Callable: