diff --git a/poetry.lock b/poetry.lock index cb5ce69..66f4f6b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -328,6 +328,21 @@ websocket-client = ">=0.32.0" [package.extras] ssh = ["paramiko (>=2.4.3)"] +[[package]] +name = "docker-stubs" +version = "0.1.0" +description = "Stubs package for the Python Docker API." +optional = false +python-versions = "^3.8" +files = [] +develop = false + +[package.source] +type = "git" +url = "https://github.com/rdozier-work/docker-stubs" +reference = "HEAD" +resolved_reference = "9de7906804ae912f1d644c97b617ac77e784fca8" + [[package]] name = "executing" version = "2.0.1" @@ -1092,4 +1107,4 @@ h11 = ">=0.9.0,<1" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "c74d1dcf762e5266d41c81b62bef444f065124fede9b3a34176cd9a6b93d9c2b" +content-hash = "0da96ff6654ff4c33b776829e34f78ba2725a0de30980ec5eaab86a8b7abbead" diff --git a/pyproject.toml b/pyproject.toml index 4a21428..ef4efa5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ black = "^23.10.1" snoop = "^0.4.3" pytest = "^7.4.3" mypy = "^1.6.1" +docker-stubs = {git = "https://github.com/rdozier-work/docker-stubs"} [tool.poetry.scripts] node_deployer = "node_deployer.__main__:main" diff --git a/src/node_deployer/autoignition.py b/src/node_deployer/autoignition.py index d40a776..247fd74 100644 --- a/src/node_deployer/autoignition.py +++ b/src/node_deployer/autoignition.py @@ -81,8 +81,11 @@ def convert_json_via_fuelignition( bytestream = io.BytesIO(b"".join(chunk for chunk in filestream)) bytestream.seek(0) tar = tarfile.open(fileobj=bytestream) + container_image = tar.extractfile(tar.getmembers()[0].name) + if container_image is None: + raise Exception("Failed to extract image from tarfile") with open(host_image_path, "wb+") as f: - f.write(tar.extractfile(tar.getmembers()[0].name).read()) + f.write(container_image.read()) def build_fuelignition() -> docker.models.images.Image: diff --git a/src/node_deployer/config.py b/src/node_deployer/config.py index 9df19b3..1983578 100644 --- a/src/node_deployer/config.py +++ b/src/node_deployer/config.py @@ -1,5 +1,6 @@ from pathlib import Path from types import SimpleNamespace +from typing import Union import docker import tomllib @@ -9,7 +10,7 @@ CLIENT = docker.from_env(version="auto") MAX_PORT: int = 65535 PROJECT_ROOT: Path = Path(__file__).parent.parent.parent.absolute() -type ConfigLabel = str | list[str] +ConfigLabel = Union[str, list[str]] # After PEP695 support: type ConfigLabel = str | list[str] class Config(SimpleNamespace): diff --git a/src/node_deployer/create_disk.py b/src/node_deployer/create_disk.py index 595ebc4..7ec2d2e 100644 --- a/src/node_deployer/create_disk.py +++ b/src/node_deployer/create_disk.py @@ -1,19 +1,20 @@ from fnmatch import fnmatch import ipaddress -from typing import Annotated +from typing import Annotated, Optional, Union from docker.types import Mount import typer -from typing import Tuple -from .config import config from .cli import cli_spinner +from .config import config from .create_img import create_img from .debug import debug_guard from .utils import ensure_build_dir -type IPAddress = ipaddress.IPv4Address | ipaddress.IPv6Address +# When PEP695 is supported this line should be: +# type IPAddress = ipaddress.IPv4Address | ipaddress.IPv6Address +IPAddress = Union[ipaddress.IPv4Address, ipaddress.IPv6Address] def filter_validation_response(response: str) -> str: @@ -60,18 +61,18 @@ def validation_result() -> str: ) if config.CLEANUP_IMAGES: image.remove(force=True) - return response + return response.decode() @cli_spinner(description="Validating ignition image", total=None) -def validate() -> Tuple[bool, str]: +def validate() -> tuple[bool, str]: """Validates the ignition image Returns: - Tuple[bool, str]: A tuple containing a boolean indicating whether + tuple[bool, str]: A tuple containing a boolean indicating whether the validation was successful and the response from the validation """ - response = validation_result().decode() + response = validation_result() response = filter_validation_response(response) return (not bool(response), response) @@ -96,7 +97,7 @@ def write_disk(disk: str) -> None: @ensure_build_dir def create_ignition_disk( disk: Annotated[ - str, + Optional[str], typer.Option( "--disk", "-d", @@ -114,7 +115,7 @@ def create_ignition_disk( ), ] = "node", password: Annotated[ - str, + Optional[str], typer.Option( "--password", "-p", @@ -125,7 +126,7 @@ def create_ignition_disk( ), ] = None, switch_ip: Annotated[ - IPAddress, + Optional[IPAddress], typer.Option( "--switch-ip", "-ip", @@ -146,7 +147,7 @@ def create_ignition_disk( ), ] = 4789, swarm_token: Annotated[ - str, + Optional[str], typer.Option( "--swarm-token", "-t", @@ -194,6 +195,10 @@ def create_ignition_disk( Raises: typer.Exit: Exit CLI if the ignition image is invalid """ + # Guard against the user specifying no disk + if disk is None: + raise typer.BadParameter("No disk specified") + create_img( hostname = hostname, password = password, diff --git a/src/node_deployer/create_img.py b/src/node_deployer/create_img.py index 44df6a6..a2f28d4 100644 --- a/src/node_deployer/create_img.py +++ b/src/node_deployer/create_img.py @@ -1,18 +1,19 @@ import ipaddress import json from pathlib import Path -from typing import Annotated +from typing import Annotated, Optional, Union import typer -from .config import config from .autoignition import json_to_img from .cli import cli_spinner +from .config import config from .debug import debug_guard from .utils import ensure_build_dir - -type IPAddress = ipaddress.IPv4Address | ipaddress.IPv6Address +# When PEP695 is supported this line should be: +# type IPAddress = ipaddress.IPv4Address | ipaddress.IPv6Address +IPAddress = Union[ipaddress.IPv4Address, ipaddress.IPv6Address] def load_template() -> dict: @@ -99,7 +100,7 @@ def create_img( ), ] = "node", password: Annotated[ - str, + Optional[str], typer.Option( "--password", "-p", @@ -110,7 +111,7 @@ def create_img( ), ] = None, switch_ip: Annotated[ - IPAddress, + Optional[IPAddress], typer.Option( "--switch-ip", "-ip", @@ -131,7 +132,7 @@ def create_img( ), ] = 4789, swarm_token: Annotated[ - str, + Optional[str], typer.Option( "--swarm-token", "-t", @@ -194,10 +195,13 @@ def create_img( } ) + # Guards against the user not specifying a password + if password is None: + raise typer.BadParameter("Password must be specified") + # Create ignition configuration - ignition_config = load_template() ignition_config = apply_ignition_settings( - ignition_config, + load_template(), hostname, password, swarm_config, diff --git a/src/node_deployer/node_deployer.py b/src/node_deployer/node_deployer.py index 63717ad..29b9c9a 100755 --- a/src/node_deployer/node_deployer.py +++ b/src/node_deployer/node_deployer.py @@ -1,12 +1,14 @@ +from typing import Any, Dict + import typer -from .config import config from .autoignition import json_to_img +from .config import config from .create_disk import create_ignition_disk from .create_img import create_img -cmd_params = { +cmd_params: Dict[Any, Any] = { "no_args_is_help": True, } @@ -15,9 +17,19 @@ app = typer.Typer( **cmd_params, ) -app.command(**cmd_params)(create_img) -app.command(**cmd_params)(create_ignition_disk) -app.command(**cmd_params)(json_to_img) +# Register commands +app.command( + help = str(create_ignition_disk.__doc__).split("Args:")[0].strip(), + **cmd_params +)(create_ignition_disk) +app.command( + help = str(create_img.__doc__).split("Args:")[0].strip(), + **cmd_params +)(create_img) +app.command( + help = str(json_to_img.__doc__).split("Args:")[0].strip(), + **cmd_params +)(json_to_img) if __name__ == "__main__": config.update_config("cli")