clan_cli: flake_name -> flake_dir

This commit is contained in:
lassulus
2023-11-15 14:28:40 +01:00
parent 7c50846f00
commit 1ea13646ea
35 changed files with 199 additions and 354 deletions

View File

@@ -1,6 +1,7 @@
import argparse import argparse
import logging import logging
import sys import sys
from pathlib import Path
from types import ModuleType from types import ModuleType
from typing import Any, Optional, Sequence from typing import Any, Optional, Sequence
@@ -57,7 +58,8 @@ def create_parser(prog: Optional[str] = None) -> argparse.ArgumentParser:
parser.add_argument( parser.add_argument(
"--flake", "--flake",
help="path to the flake where the clan resides in", help="path to the flake where the clan resides in",
default=None, default=get_clan_flake_toplevel(),
type=Path,
) )
subparsers = parser.add_subparsers() subparsers = parser.add_subparsers()
@@ -93,8 +95,6 @@ def create_parser(prog: Optional[str] = None) -> argparse.ArgumentParser:
if argcomplete: if argcomplete:
argcomplete.autocomplete(parser) argcomplete.autocomplete(parser)
if len(sys.argv) == 1:
parser.print_help()
return parser return parser
@@ -103,13 +103,13 @@ def main() -> None:
parser = create_parser() parser = create_parser()
args = parser.parse_args() args = parser.parse_args()
if len(sys.argv) == 1:
parser.print_help()
if args.debug: if args.debug:
setup_logging(logging.DEBUG) setup_logging(logging.DEBUG)
log.debug("Debug log activated") log.debug("Debug log activated")
if args.flake is None:
args.flake = get_clan_flake_toplevel()
if not hasattr(args, "func"): if not hasattr(args, "func"):
return return

View File

@@ -57,8 +57,7 @@ def runforcli(
try: try:
res = asyncio.run(func(*args)) res = asyncio.run(func(*args))
for i in res.items(): for name, out in res.items():
name, out = i
if out.stderr: if out.stderr:
print(f"{name}: {out.stderr}", end="") print(f"{name}: {out.stderr}", end="")
if out.stdout: if out.stdout:

View File

@@ -1,20 +1,17 @@
import json import json
import subprocess import subprocess
from pathlib import Path
from typing import Optional from typing import Optional
from clan_cli.nix import nix_eval from clan_cli.nix import nix_eval
from .dirs import specific_flake_dir
from .types import FlakeName
def get_clan_module_names( def get_clan_module_names(
flake_name: FlakeName, flake_dir: Path,
) -> tuple[list[str], Optional[str]]: ) -> tuple[list[str], Optional[str]]:
""" """
Get the list of clan modules from the clan-core flake input Get the list of clan modules from the clan-core flake input
""" """
flake = specific_flake_dir(flake_name)
proc = subprocess.run( proc = subprocess.run(
nix_eval( nix_eval(
[ [
@@ -23,7 +20,7 @@ def get_clan_module_names(
"--expr", "--expr",
f""" f"""
let let
flake = builtins.getFlake (toString {flake}); flake = builtins.getFlake (toString {flake_dir});
in in
builtins.attrNames flake.inputs.clan-core.clanModules builtins.attrNames flake.inputs.clan-core.clanModules
""", """,
@@ -31,7 +28,7 @@ def get_clan_module_names(
), ),
capture_output=True, capture_output=True,
text=True, text=True,
cwd=flake, cwd=flake_dir,
) )
if proc.returncode != 0: if proc.returncode != 0:
return [], proc.stderr return [], proc.stderr

View File

@@ -10,11 +10,10 @@ import sys
from pathlib import Path from pathlib import Path
from typing import Any, Optional, Tuple, get_origin from typing import Any, Optional, Tuple, get_origin
from clan_cli.dirs import machine_settings_file, specific_flake_dir from clan_cli.dirs import machine_settings_file
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.git import commit_file from clan_cli.git import commit_file
from clan_cli.nix import nix_eval from clan_cli.nix import nix_eval
from clan_cli.types import FlakeName
script_dir = Path(__file__).parent script_dir = Path(__file__).parent
@@ -108,9 +107,9 @@ def cast(value: Any, type: Any, opt_description: str) -> Any:
def options_for_machine( def options_for_machine(
flake_name: FlakeName, machine_name: str, show_trace: bool = False flake_dir: Path, machine_name: str, show_trace: bool = False
) -> dict: ) -> dict:
clan_dir = specific_flake_dir(flake_name) clan_dir = flake_dir
flags = [] flags = []
if show_trace: if show_trace:
flags.append("--show-trace") flags.append("--show-trace")
@@ -131,9 +130,9 @@ def options_for_machine(
def read_machine_option_value( def read_machine_option_value(
flake_name: FlakeName, machine_name: str, option: str, show_trace: bool = False flake_dir: Path, machine_name: str, option: str, show_trace: bool = False
) -> str: ) -> str:
clan_dir = specific_flake_dir(flake_name) clan_dir = flake_dir
# use nix eval to read from .#nixosConfigurations.default.config.{option} # use nix eval to read from .#nixosConfigurations.default.config.{option}
# this will give us the evaluated config with the options attribute # this will give us the evaluated config with the options attribute
cmd = nix_eval( cmd = nix_eval(
@@ -177,12 +176,12 @@ def get_or_set_option(args: argparse.Namespace) -> None:
options = json.load(f) options = json.load(f)
# compute settings json file location # compute settings json file location
if args.settings_file is None: if args.settings_file is None:
settings_file = machine_settings_file(args.flake, args.machine) settings_file = machine_settings_file(Path(args.flake), args.machine)
else: else:
settings_file = args.settings_file settings_file = args.settings_file
# set the option with the given value # set the option with the given value
set_option( set_option(
flake_name=args.flake, flake_dir=Path(args.flake),
option=args.option, option=args.option,
value=args.value, value=args.value,
options=options, options=options,
@@ -248,7 +247,7 @@ def find_option(
def set_option( def set_option(
flake_name: FlakeName, flake_dir: Path,
option: str, option: str,
value: Any, value: Any,
options: dict, options: dict,
@@ -298,10 +297,10 @@ def set_option(
json.dump(new_config, f, indent=2) json.dump(new_config, f, indent=2)
print(file=f) # add newline at the end of the file to make git happy print(file=f) # add newline at the end of the file to make git happy
if settings_file.resolve().is_relative_to(specific_flake_dir(flake_name)): if settings_file.resolve().is_relative_to(flake_dir):
commit_file( commit_file(
settings_file, settings_file,
repo_dir=specific_flake_dir(flake_name), repo_dir=flake_dir,
commit_message=f"Set option {option_description}", commit_message=f"Set option {option_description}",
) )
@@ -360,11 +359,6 @@ def register_parser(
nargs="*", nargs="*",
help="option value to set (if omitted, the current value is printed)", help="option value to set (if omitted, the current value is printed)",
) )
parser.add_argument(
"flake",
type=str,
help="name of the flake to set machine options for",
)
def main(argv: Optional[list[str]] = None) -> None: def main(argv: Optional[list[str]] = None) -> None:

View File

@@ -2,6 +2,7 @@ import json
import os import os
import re import re
import subprocess import subprocess
from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import Optional from typing import Optional
@@ -10,18 +11,15 @@ from fastapi import HTTPException
from clan_cli.dirs import ( from clan_cli.dirs import (
machine_settings_file, machine_settings_file,
nixpkgs_source, nixpkgs_source,
specific_flake_dir,
specific_machine_dir, specific_machine_dir,
) )
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.git import commit_file from clan_cli.git import commit_file
from clan_cli.nix import nix_eval from clan_cli.nix import nix_eval
from ..types import FlakeName
def verify_machine_config( def verify_machine_config(
flake_name: FlakeName, flake_dir: Path,
machine_name: str, machine_name: str,
config: Optional[dict] = None, config: Optional[dict] = None,
) -> Optional[str]: ) -> Optional[str]:
@@ -30,8 +28,8 @@ def verify_machine_config(
Returns a tuple of (success, error_message) Returns a tuple of (success, error_message)
""" """
if config is None: if config is None:
config = config_for_machine(flake_name, machine_name) config = config_for_machine(flake_dir, machine_name)
flake = specific_flake_dir(flake_name) flake = flake_dir
with NamedTemporaryFile(mode="w", dir=flake) as clan_machine_settings_file: with NamedTemporaryFile(mode="w", dir=flake) as clan_machine_settings_file:
json.dump(config, clan_machine_settings_file, indent=2) json.dump(config, clan_machine_settings_file, indent=2)
clan_machine_settings_file.seek(0) clan_machine_settings_file.seek(0)
@@ -82,23 +80,21 @@ def verify_machine_config(
return None return None
def config_for_machine(flake_name: FlakeName, machine_name: str) -> dict: def config_for_machine(flake_dir: Path, machine_name: str) -> dict:
# read the config from a json file located at {flake}/machines/{machine_name}/settings.json # read the config from a json file located at {flake}/machines/{machine_name}/settings.json
if not specific_machine_dir(flake_name, machine_name).exists(): if not specific_machine_dir(flake_dir, machine_name).exists():
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
detail=f"Machine {machine_name} not found. Create the machine first`", detail=f"Machine {machine_name} not found. Create the machine first`",
) )
settings_path = machine_settings_file(flake_name, machine_name) settings_path = machine_settings_file(flake_dir, machine_name)
if not settings_path.exists(): if not settings_path.exists():
return {} return {}
with open(settings_path) as f: with open(settings_path) as f:
return json.load(f) return json.load(f)
def set_config_for_machine( def set_config_for_machine(flake_dir: Path, machine_name: str, config: dict) -> None:
flake_name: FlakeName, machine_name: str, config: dict
) -> None:
hostname_regex = r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)$" hostname_regex = r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)$"
if not re.match(hostname_regex, machine_name): if not re.match(hostname_regex, machine_name):
raise ClanError("Machine name must be a valid hostname") raise ClanError("Machine name must be a valid hostname")
@@ -111,11 +107,10 @@ def set_config_for_machine(
config["networking"]["hostName"] = machine_name config["networking"]["hostName"] = machine_name
# create machine folder if it doesn't exist # create machine folder if it doesn't exist
# write the config to a json file located at {flake}/machines/{machine_name}/settings.json # write the config to a json file located at {flake}/machines/{machine_name}/settings.json
settings_path = machine_settings_file(flake_name, machine_name) settings_path = machine_settings_file(flake_dir, machine_name)
settings_path.parent.mkdir(parents=True, exist_ok=True) settings_path.parent.mkdir(parents=True, exist_ok=True)
with open(settings_path, "w") as f: with open(settings_path, "w") as f:
json.dump(config, f, indent=2) json.dump(config, f)
repo_dir = specific_flake_dir(flake_name)
if repo_dir is not None: if flake_dir is not None:
commit_file(settings_path, repo_dir) commit_file(settings_path, flake_dir)

View File

@@ -10,22 +10,18 @@ from fastapi import HTTPException
from clan_cli.dirs import ( from clan_cli.dirs import (
nixpkgs_source, nixpkgs_source,
specific_flake_dir,
) )
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.nix import nix_eval from clan_cli.nix import nix_eval
from ..types import FlakeName
def machine_schema( def machine_schema(
flake_name: FlakeName, flake_dir: Path,
config: dict, config: dict,
clan_imports: Optional[list[str]] = None, clan_imports: Optional[list[str]] = None,
) -> dict: ) -> dict:
flake = specific_flake_dir(flake_name)
# use nix eval to lib.evalModules .#nixosConfigurations.<machine_name>.options.clan # use nix eval to lib.evalModules .#nixosConfigurations.<machine_name>.options.clan
with NamedTemporaryFile(mode="w", dir=flake) as clan_machine_settings_file: with NamedTemporaryFile(mode="w", dir=flake_dir) as clan_machine_settings_file:
env = os.environ.copy() env = os.environ.copy()
if clan_imports is not None: if clan_imports is not None:
config["clanImports"] = clan_imports config["clanImports"] = clan_imports
@@ -43,9 +39,8 @@ def machine_schema(
f""" f"""
let let
b = builtins; b = builtins;
# hardcoding system for now, not sure where to get it from system = b.currentSystem;
system = "x86_64-linux"; flake = b.getFlake (toString {flake_dir});
flake = b.getFlake (toString {flake});
clan-core = flake.inputs.clan-core; clan-core = flake.inputs.clan-core;
config = b.fromJSON (b.readFile (b.getEnv "CLAN_MACHINE_SETTINGS_FILE")); config = b.fromJSON (b.readFile (b.getEnv "CLAN_MACHINE_SETTINGS_FILE"));
modules_not_found = modules_not_found =
@@ -59,7 +54,7 @@ def machine_schema(
), ),
capture_output=True, capture_output=True,
text=True, text=True,
cwd=flake, cwd=flake_dir,
env=env, env=env,
) )
if proc.returncode != 0: if proc.returncode != 0:
@@ -86,9 +81,8 @@ def machine_schema(
"--expr", "--expr",
f""" f"""
let let
# hardcoding system for now, not sure where to get it from system = builtins.currentSystem;
system = "x86_64-linux"; flake = builtins.getFlake (toString {flake_dir});
flake = builtins.getFlake (toString {flake});
clan-core = flake.inputs.clan-core; clan-core = flake.inputs.clan-core;
nixpkgsSrc = flake.inputs.nixpkgs or {nixpkgs_source()}; nixpkgsSrc = flake.inputs.nixpkgs or {nixpkgs_source()};
lib = import (nixpkgsSrc + /lib); lib = import (nixpkgsSrc + /lib);
@@ -115,7 +109,7 @@ def machine_schema(
), ),
capture_output=True, capture_output=True,
text=True, text=True,
cwd=flake, cwd=flake_dir,
env=env, env=env,
) )
if proc.returncode != 0: if proc.returncode != 0:

View File

@@ -4,9 +4,6 @@ import sys
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from .errors import ClanError
from .types import FlakeName
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -15,10 +12,7 @@ def get_clan_flake_toplevel() -> Optional[Path]:
def find_git_repo_root() -> Optional[Path]: def find_git_repo_root() -> Optional[Path]:
try: return find_toplevel([".git"])
return find_toplevel([".git"])
except ClanError:
return None
def find_toplevel(top_level_files: list[str]) -> Optional[Path]: def find_toplevel(top_level_files: list[str]) -> Optional[Path]:
@@ -42,35 +36,16 @@ def user_config_dir() -> Path:
return Path(os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))) return Path(os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config")))
def clan_config_dir() -> Path: def machines_dir(flake_dir: Path) -> Path:
path = user_config_dir() / "clan" return flake_dir / "machines"
path.mkdir(parents=True, exist_ok=True)
return path.resolve()
def clan_flakes_dir() -> Path: def specific_machine_dir(flake_dir: Path, machine: str) -> Path:
path = clan_config_dir() / "flakes" return machines_dir(flake_dir) / machine
path.mkdir(parents=True, exist_ok=True)
return path.resolve()
def specific_flake_dir(flake_name: FlakeName) -> Path: def machine_settings_file(flake_dir: Path, machine: str) -> Path:
flake_dir = clan_flakes_dir() / flake_name return specific_machine_dir(flake_dir, machine) / "settings.json"
if not flake_dir.exists():
raise ClanError(f"Flake '{flake_name}' does not exist in {clan_flakes_dir()}")
return flake_dir
def machines_dir(flake_name: FlakeName) -> Path:
return specific_flake_dir(flake_name) / "machines"
def specific_machine_dir(flake_name: FlakeName, machine: str) -> Path:
return machines_dir(flake_name) / machine
def machine_settings_file(flake_name: FlakeName, machine: str) -> Path:
return specific_machine_dir(flake_name, machine) / "settings.json"
def module_root() -> Path: def module_root() -> Path:

View File

@@ -2,7 +2,6 @@
import argparse import argparse
from .create import register_create_parser from .create import register_create_parser
from .list_flakes import register_list_parser
# takes a (sub)parser and configures it # takes a (sub)parser and configures it
@@ -15,6 +14,3 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
) )
create_parser = subparser.add_parser("create", help="Create a clan flake") create_parser = subparser.add_parser("create", help="Create a clan flake")
register_create_parser(create_parser) register_create_parser(create_parser)
list_parser = subparser.add_parser("list", help="List clan flakes")
register_list_parser(list_parser)

View File

@@ -7,7 +7,6 @@ from pydantic import AnyUrl
from pydantic.tools import parse_obj_as from pydantic.tools import parse_obj_as
from ..async_cmd import CmdOut, run, runforcli from ..async_cmd import CmdOut, run, runforcli
from ..dirs import clan_flakes_dir
from ..errors import ClanError from ..errors import ClanError
from ..nix import nix_command, nix_shell from ..nix import nix_command, nix_shell
@@ -63,22 +62,16 @@ async def create_flake(directory: Path, url: AnyUrl) -> Dict[str, CmdOut]:
def create_flake_command(args: argparse.Namespace) -> None: def create_flake_command(args: argparse.Namespace) -> None:
flake_dir = clan_flakes_dir() / args.name runforcli(create_flake, args.path, args.url)
runforcli(create_flake, flake_dir, args.url)
# takes a (sub)parser and configures it # takes a (sub)parser and configures it
def register_create_parser(parser: argparse.ArgumentParser) -> None: def register_create_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"name",
type=str,
help="name for the flake",
)
parser.add_argument( parser.add_argument(
"--url", "--url",
type=str, type=str,
help="url for the flake", help="url for the flake",
default=DEFAULT_URL, default=DEFAULT_URL,
) )
# parser.add_argument("name", type=str, help="name of the flake") parser.add_argument("path", type=Path, help="Path to the flake", default=Path("."))
parser.set_defaults(func=create_flake_command) parser.set_defaults(func=create_flake_command)

View File

@@ -1,27 +0,0 @@
import argparse
import logging
import os
from ..dirs import clan_flakes_dir
log = logging.getLogger(__name__)
def list_flakes() -> list[str]:
path = clan_flakes_dir()
log.debug(f"Listing machines in {path}")
if not path.exists():
return []
objs: list[str] = []
for f in os.listdir(path):
objs.append(f)
return objs
def list_command(args: argparse.Namespace) -> None:
for flake in list_flakes():
print(flake)
def register_list_parser(parser: argparse.ArgumentParser) -> None:
parser.set_defaults(func=list_command)

View File

@@ -12,9 +12,4 @@ def create_command(args: argparse.Namespace) -> None:
def register_create_parser(parser: argparse.ArgumentParser) -> None: def register_create_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument("machine", type=str) parser.add_argument("machine", type=str)
parser.add_argument(
"flake",
type=str,
help="name of the flake to create machine for",
)
parser.set_defaults(func=create_command) parser.set_defaults(func=create_command)

View File

@@ -15,9 +15,4 @@ def delete_command(args: argparse.Namespace) -> None:
def register_delete_parser(parser: argparse.ArgumentParser) -> None: def register_delete_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument("host", type=str) parser.add_argument("host", type=str)
parser.add_argument(
"flake",
type=str,
help="name of the flake to create machine for",
)
parser.set_defaults(func=delete_command) parser.set_defaults(func=delete_command)

View File

@@ -1,10 +1,11 @@
from pathlib import Path
from ..dirs import specific_machine_dir from ..dirs import specific_machine_dir
from ..types import FlakeName
def machine_has_fact(flake_name: FlakeName, machine: str, fact: str) -> bool: def machine_has_fact(flake_dir: Path, machine: str, fact: str) -> bool:
return (specific_machine_dir(flake_name, machine) / "facts" / fact).exists() return (specific_machine_dir(flake_dir, machine) / "facts" / fact).exists()
def machine_get_fact(flake_name: FlakeName, machine: str, fact: str) -> str: def machine_get_fact(flake_dir: Path, machine: str, fact: str) -> str:
return (specific_machine_dir(flake_name, machine) / "facts" / fact).read_text() return (specific_machine_dir(flake_dir, machine) / "facts" / fact).read_text()

View File

@@ -3,14 +3,12 @@ import subprocess
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from ..dirs import specific_flake_dir
from ..machines.machines import Machine from ..machines.machines import Machine
from ..nix import nix_shell from ..nix import nix_shell
from ..secrets.generate import generate_secrets from ..secrets.generate import generate_secrets
from ..types import FlakeName
def install_nixos(machine: Machine, flake_name: FlakeName) -> None: def install_nixos(machine: Machine) -> None:
h = machine.host h = machine.host
target_host = f"{h.user or 'root'}@{h.host}" target_host = f"{h.user or 'root'}@{h.host}"
@@ -41,10 +39,10 @@ def install_nixos(machine: Machine, flake_name: FlakeName) -> None:
def install_command(args: argparse.Namespace) -> None: def install_command(args: argparse.Namespace) -> None:
machine = Machine(args.machine, flake_dir=specific_flake_dir(args.flake)) machine = Machine(args.machine, flake_dir=args.flake)
machine.deployment_address = args.target_host machine.deployment_address = args.target_host
install_nixos(machine, args.flake) install_nixos(machine)
def register_install_parser(parser: argparse.ArgumentParser) -> None: def register_install_parser(parser: argparse.ArgumentParser) -> None:
@@ -58,9 +56,4 @@ def register_install_parser(parser: argparse.ArgumentParser) -> None:
type=str, type=str,
help="ssh address to install to in the form of user@host:2222", help="ssh address to install to in the form of user@host:2222",
) )
parser.add_argument(
"flake",
type=str,
help="name of the flake to install machine from",
)
parser.set_defaults(func=install_command) parser.set_defaults(func=install_command)

View File

@@ -1,16 +1,16 @@
import argparse import argparse
import logging import logging
import os import os
from pathlib import Path
from ..dirs import machines_dir from ..dirs import machines_dir
from ..types import FlakeName
from .types import validate_hostname from .types import validate_hostname
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def list_machines(flake_name: FlakeName) -> list[str]: def list_machines(flake_dir: Path) -> list[str]:
path = machines_dir(flake_name) path = machines_dir(flake_dir)
log.debug(f"Listing machines in {path}") log.debug(f"Listing machines in {path}")
if not path.exists(): if not path.exists():
return [] return []
@@ -22,14 +22,9 @@ def list_machines(flake_name: FlakeName) -> list[str]:
def list_command(args: argparse.Namespace) -> None: def list_command(args: argparse.Namespace) -> None:
for machine in list_machines(args.flake): for machine in list_machines(Path(args.flake)):
print(machine) print(machine)
def register_list_parser(parser: argparse.ArgumentParser) -> None: def register_list_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"flake",
type=str,
help="name of the flake to create machine for",
)
parser.set_defaults(func=list_command) parser.set_defaults(func=list_command)

View File

@@ -1,27 +1,9 @@
import logging import logging
from pathlib import Path from pathlib import Path
from typing import NewType, Union from typing import Union
from pydantic import AnyUrl from pydantic import AnyUrl
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
FlakeName = NewType("FlakeName", str)
FlakeUrl = Union[AnyUrl, Path] FlakeUrl = Union[AnyUrl, Path]
def validate_path(base_dir: Path, value: Path) -> Path:
user_path = (base_dir / value).resolve()
# Check if the path is within the data directory
if not str(user_path).startswith(str(base_dir)):
if not str(user_path).startswith("/tmp/pytest"):
raise ValueError(
f"Destination out of bounds. Expected {user_path} to start with {base_dir}"
)
else:
log.warning(
f"Detected pytest tmpdir. Skipping path validation for {user_path}"
)
return user_path

View File

@@ -2,7 +2,6 @@ import argparse
import asyncio import asyncio
import json import json
import os import os
import re
import shlex import shlex
import sys import sys
import tempfile import tempfile
@@ -10,19 +9,11 @@ from pathlib import Path
from typing import Iterator from typing import Iterator
from uuid import UUID from uuid import UUID
from ..dirs import clan_flakes_dir, specific_flake_dir
from ..nix import nix_build, nix_config, nix_eval, nix_shell from ..nix import nix_build, nix_config, nix_eval, nix_shell
from ..task_manager import BaseTask, Command, create_task from ..task_manager import BaseTask, Command, create_task
from ..types import validate_path
from .inspect import VmConfig, inspect_vm from .inspect import VmConfig, inspect_vm
def is_flake_url(s: str) -> bool:
if re.match(r"^http.?://[a-zA-Z0-9.-]+/[a-zA-Z0-9.-]+", s) is not None:
return True
return False
class BuildVmTask(BaseTask): class BuildVmTask(BaseTask):
def __init__(self, uuid: UUID, vm: VmConfig, nix_options: list[str] = []) -> None: def __init__(self, uuid: UUID, vm: VmConfig, nix_options: list[str] = []) -> None:
super().__init__(uuid, num_cmds=7) super().__init__(uuid, num_cmds=7)
@@ -72,8 +63,7 @@ class BuildVmTask(BaseTask):
self.log.debug(f"Building VM for clan name: {clan_name}") self.log.debug(f"Building VM for clan name: {clan_name}")
flake_dir = clan_flakes_dir() / clan_name flake_dir = Path(self.vm.flake_url)
validate_path(clan_flakes_dir(), flake_dir)
flake_dir.mkdir(exist_ok=True) flake_dir.mkdir(exist_ok=True)
with tempfile.TemporaryDirectory() as tmpdir_: with tempfile.TemporaryDirectory() as tmpdir_:
@@ -93,7 +83,7 @@ class BuildVmTask(BaseTask):
env["SECRETS_DIR"] = str(secrets_dir) env["SECRETS_DIR"] = str(secrets_dir)
# Only generate secrets for local clans # Only generate secrets for local clans
if not is_flake_url(str(self.vm.flake_url)): if isinstance(self.vm.flake_url, Path) and self.vm.flake_url.is_dir():
cmd = next(cmds) cmd = next(cmds)
if Path(self.vm.flake_url).is_dir(): if Path(self.vm.flake_url).is_dir():
cmd.run( cmd.run(
@@ -200,9 +190,7 @@ def create_vm(vm: VmConfig, nix_options: list[str] = []) -> BuildVmTask:
def create_command(args: argparse.Namespace) -> None: def create_command(args: argparse.Namespace) -> None:
flake_url = args.flake flake_url = args.flake_url or args.flake
if not is_flake_url(str(args.flake)):
flake_url = specific_flake_dir(args.flake)
vm = asyncio.run(inspect_vm(flake_url=flake_url, flake_attr=args.machine)) vm = asyncio.run(inspect_vm(flake_url=flake_url, flake_attr=args.machine))
task = create_vm(vm, args.option) task = create_vm(vm, args.option)
@@ -212,4 +200,5 @@ def create_command(args: argparse.Namespace) -> None:
def register_create_parser(parser: argparse.ArgumentParser) -> None: def register_create_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument("machine", type=str, help="machine in the flake to create") parser.add_argument("machine", type=str, help="machine in the flake to create")
parser.add_argument("--flake_url", type=str, help="flake url")
parser.set_defaults(func=create_command) parser.set_defaults(func=create_command)

View File

@@ -6,7 +6,6 @@ from pathlib import Path
from pydantic import AnyUrl, BaseModel from pydantic import AnyUrl, BaseModel
from ..async_cmd import run from ..async_cmd import run
from ..dirs import specific_flake_dir
from ..nix import nix_config, nix_eval from ..nix import nix_config, nix_eval
@@ -34,7 +33,7 @@ async def inspect_vm(flake_url: AnyUrl | Path, flake_attr: str) -> VmConfig:
def inspect_command(args: argparse.Namespace) -> None: def inspect_command(args: argparse.Namespace) -> None:
clan_dir = specific_flake_dir(args.flake) clan_dir = Path(args.flake)
res = asyncio.run(inspect_vm(flake_url=clan_dir, flake_attr=args.machine)) res = asyncio.run(inspect_vm(flake_url=clan_dir, flake_attr=args.machine))
print("Cores:", res.cores) print("Cores:", res.cores)
print("Memory size:", res.memory_size) print("Memory size:", res.memory_size)
@@ -43,9 +42,4 @@ def inspect_command(args: argparse.Namespace) -> None:
def register_inspect_parser(parser: argparse.ArgumentParser) -> None: def register_inspect_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument("machine", type=str) parser.add_argument("machine", type=str)
parser.add_argument(
"flake",
type=str,
help="name of the flake to create machine for",
)
parser.set_defaults(func=inspect_command) parser.set_defaults(func=inspect_command)

View File

@@ -1,25 +1,13 @@
import logging import logging
from pathlib import Path
from typing import Any
from pydantic import AnyUrl, BaseModel, Extra, validator from pydantic import AnyUrl, BaseModel, Extra
from ..dirs import clan_flakes_dir
from ..flakes.create import DEFAULT_URL from ..flakes.create import DEFAULT_URL
from ..types import validate_path
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class ClanFlakePath(BaseModel): class FlakeCreateInput(BaseModel):
flake_name: Path
@validator("flake_name")
def check_flake_name(cls: Any, v: Path) -> Path: # noqa
return validate_path(clan_flakes_dir(), v)
class FlakeCreateInput(ClanFlakePath):
url: AnyUrl = DEFAULT_URL url: AnyUrl = DEFAULT_URL

View File

@@ -1,10 +1,10 @@
# Logging setup # Logging setup
import logging import logging
from pathlib import Path
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from clan_cli.clan_modules import get_clan_module_names from clan_cli.clan_modules import get_clan_module_names
from clan_cli.types import FlakeName
from ..api_outputs import ( from ..api_outputs import (
ClanModulesResponse, ClanModulesResponse,
@@ -15,9 +15,9 @@ log = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
@router.get("/api/{flake_name}/clan_modules", tags=[Tags.modules]) @router.get("/api/clan_modules", tags=[Tags.modules])
async def list_clan_modules(flake_name: FlakeName) -> ClanModulesResponse: async def list_clan_modules(flake_dir: Path) -> ClanModulesResponse:
module_names, error = get_clan_module_names(flake_name) module_names, error = get_clan_module_names(flake_dir)
if error is not None: if error is not None:
raise HTTPException(status_code=400, detail=error) raise HTTPException(status_code=400, detail=error)
return ClanModulesResponse(clan_modules=module_names) return ClanModulesResponse(clan_modules=module_names)

View File

@@ -13,12 +13,11 @@ from clan_cli.webui.api_outputs import (
FlakeAction, FlakeAction,
FlakeAttrResponse, FlakeAttrResponse,
FlakeCreateResponse, FlakeCreateResponse,
FlakeListResponse,
FlakeResponse, FlakeResponse,
) )
from ...async_cmd import run from ...async_cmd import run
from ...flakes import create, list_flakes from ...flakes import create
from ...nix import nix_command, nix_flake_show from ...nix import nix_command, nix_flake_show
from ..tags import Tags from ..tags import Tags
@@ -78,23 +77,17 @@ async def inspect_flake(
return FlakeResponse(content=content, actions=actions) return FlakeResponse(content=content, actions=actions)
@router.get("/api/flake/list", tags=[Tags.flake])
async def list_all_flakes() -> FlakeListResponse:
flakes = list_flakes.list_flakes()
return FlakeListResponse(flakes=flakes)
@router.post( @router.post(
"/api/flake/create", tags=[Tags.flake], status_code=status.HTTP_201_CREATED "/api/flake/create", tags=[Tags.flake], status_code=status.HTTP_201_CREATED
) )
async def create_flake( async def create_flake(
args: Annotated[FlakeCreateInput, Body()], flake_dir: Path, args: Annotated[FlakeCreateInput, Body()]
) -> FlakeCreateResponse: ) -> FlakeCreateResponse:
if args.flake_name.exists(): if flake_dir.exists():
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, status_code=status.HTTP_409_CONFLICT,
detail="Flake already exists", detail="Flake already exists",
) )
cmd_out = await create.create_flake(args.flake_name, args.url) cmd_out = await create.create_flake(flake_dir, args.url)
return FlakeCreateResponse(cmd_out=cmd_out) return FlakeCreateResponse(cmd_out=cmd_out)

View File

@@ -1,5 +1,6 @@
# Logging setup # Logging setup
import logging import logging
from pathlib import Path
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Body from fastapi import APIRouter, Body
@@ -15,7 +16,6 @@ from ...config.machine import (
) )
from ...config.schema import machine_schema from ...config.schema import machine_schema
from ...machines.list import list_machines as _list_machines from ...machines.list import list_machines as _list_machines
from ...types import FlakeName
from ..api_outputs import ( from ..api_outputs import (
ConfigResponse, ConfigResponse,
Machine, Machine,
@@ -31,66 +31,62 @@ log = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
@router.get("/api/{flake_name}/machines", tags=[Tags.machine]) @router.get("/api/machines", tags=[Tags.machine])
async def list_machines(flake_name: FlakeName) -> MachinesResponse: async def list_machines(flake_dir: Path) -> MachinesResponse:
machines = [] machines = []
for m in _list_machines(flake_name): for m in _list_machines(flake_dir):
machines.append(Machine(name=m, status=Status.UNKNOWN)) machines.append(Machine(name=m, status=Status.UNKNOWN))
return MachinesResponse(machines=machines) return MachinesResponse(machines=machines)
@router.get("/api/{flake_name}/machines/{name}", tags=[Tags.machine]) @router.get("/api/machines/{name}", tags=[Tags.machine])
async def get_machine(flake_name: FlakeName, name: str) -> MachineResponse: async def get_machine(flake_dir: Path, name: str) -> MachineResponse:
log.error("TODO") log.error("TODO")
return MachineResponse(machine=Machine(name=name, status=Status.UNKNOWN)) return MachineResponse(machine=Machine(name=name, status=Status.UNKNOWN))
@router.get("/api/{flake_name}/machines/{name}/config", tags=[Tags.machine]) @router.get("/api/machines/{name}/config", tags=[Tags.machine])
async def get_machine_config(flake_name: FlakeName, name: str) -> ConfigResponse: async def get_machine_config(flake_dir: Path, name: str) -> ConfigResponse:
config = config_for_machine(flake_name, name) config = config_for_machine(flake_dir, name)
return ConfigResponse(**config) return ConfigResponse(**config)
@router.put("/api/{flake_name}/machines/{name}/config", tags=[Tags.machine]) @router.put("/api/machines/{name}/config", tags=[Tags.machine])
async def put_machine( async def set_machine_config(
flake_name: FlakeName, name: str, config: Annotated[MachineConfig, Body()] flake_dir: Path, name: str, config: Annotated[MachineConfig, Body()]
) -> None: ) -> None:
"""
Set the config for a machine.
Creates the machine if it doesn't yet exist.
"""
conf = jsonable_encoder(config) conf = jsonable_encoder(config)
set_config_for_machine(flake_name, name, conf) set_config_for_machine(flake_dir, name, conf)
@router.put( @router.put(
"/api/{flake_name}/schema", "/api/schema",
tags=[Tags.machine], tags=[Tags.machine],
responses={400: {"model": MissingClanImports}}, responses={400: {"model": MissingClanImports}},
) )
async def get_machine_schema( async def get_machine_schema(
flake_name: FlakeName, config: Annotated[MachineConfig, Body()] flake_dir: Path, config: Annotated[dict, Body()]
) -> SchemaResponse: ) -> SchemaResponse:
schema = machine_schema(flake_name, config=dict(config)) schema = machine_schema(flake_dir, config=config)
return SchemaResponse(schema=schema) return SchemaResponse(schema=schema)
@router.get("/api/{flake_name}/machines/{name}/verify", tags=[Tags.machine]) @router.get("/api/machines/{name}/verify", tags=[Tags.machine])
async def get_verify_machine_config( async def get_verify_machine_config(
flake_name: FlakeName, name: str flake_dir: Path, name: str
) -> VerifyMachineResponse: ) -> VerifyMachineResponse:
error = verify_machine_config(flake_name, name) error = verify_machine_config(flake_dir, name)
success = error is None success = error is None
return VerifyMachineResponse(success=success, error=error) return VerifyMachineResponse(success=success, error=error)
@router.put("/api/{flake_name}/machines/{name}/verify", tags=[Tags.machine]) @router.put("/api/machines/{name}/verify", tags=[Tags.machine])
async def put_verify_machine_config( async def put_verify_machine_config(
flake_name: FlakeName, flake_dir: Path,
name: str, name: str,
config: Annotated[dict, Body()], config: Annotated[dict, Body()],
) -> VerifyMachineResponse: ) -> VerifyMachineResponse:
error = verify_machine_config(flake_name, name, config) error = verify_machine_config(flake_dir, name, config)
success = error is None success = error is None
return VerifyMachineResponse(success=success, error=error) return VerifyMachineResponse(success=success, error=error)

View File

@@ -13,7 +13,6 @@ from pydantic.tools import parse_obj_as
from root import CLAN_CORE from root import CLAN_CORE
from clan_cli.dirs import nixpkgs_source from clan_cli.dirs import nixpkgs_source
from clan_cli.types import FlakeName
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -36,14 +35,13 @@ def substitute(
class FlakeForTest(NamedTuple): class FlakeForTest(NamedTuple):
name: FlakeName
path: Path path: Path
def create_flake( def create_flake(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
temporary_home: Path, temporary_home: Path,
flake_name: FlakeName, flake_name: str,
clan_core_flake: Path | None = None, clan_core_flake: Path | None = None,
machines: list[str] = [], machines: list[str] = [],
remote: bool = False, remote: bool = False,
@@ -55,7 +53,7 @@ def create_flake(
template = Path(__file__).parent / flake_name template = Path(__file__).parent / flake_name
# copy the template to a new temporary location # copy the template to a new temporary location
flake = temporary_home / ".config/clan/flakes" / flake_name flake = temporary_home / flake_name
shutil.copytree(template, flake) shutil.copytree(template, flake)
# lookup the requested machines in ./test_machines and include them # lookup the requested machines in ./test_machines and include them
@@ -91,16 +89,16 @@ def create_flake(
if remote: if remote:
with tempfile.TemporaryDirectory(): with tempfile.TemporaryDirectory():
yield FlakeForTest(flake_name, flake) yield FlakeForTest(flake)
else: else:
yield FlakeForTest(flake_name, flake) yield FlakeForTest(flake)
@pytest.fixture @pytest.fixture
def test_flake( def test_flake(
monkeypatch: pytest.MonkeyPatch, temporary_home: Path monkeypatch: pytest.MonkeyPatch, temporary_home: Path
) -> Iterator[FlakeForTest]: ) -> Iterator[FlakeForTest]:
yield from create_flake(monkeypatch, temporary_home, FlakeName("test_flake")) yield from create_flake(monkeypatch, temporary_home, "test_flake")
@pytest.fixture @pytest.fixture
@@ -114,7 +112,7 @@ def test_flake_with_core(
yield from create_flake( yield from create_flake(
monkeypatch, monkeypatch,
temporary_home, temporary_home,
FlakeName("test_flake_with_core"), "test_flake_with_core",
CLAN_CORE, CLAN_CORE,
) )
@@ -140,6 +138,6 @@ def test_flake_with_core_and_pass(
yield from create_flake( yield from create_flake(
monkeypatch, monkeypatch,
temporary_home, temporary_home,
FlakeName("test_flake_with_core_and_pass"), "test_flake_with_core_and_pass",
CLAN_CORE, CLAN_CORE,
) )

View File

@@ -4,22 +4,17 @@ import shlex
from clan_cli import create_parser from clan_cli import create_parser
from clan_cli.custom_logger import get_caller from clan_cli.custom_logger import get_caller
from clan_cli.dirs import get_clan_flake_toplevel
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class Cli: class Cli:
def __init__(self) -> None:
self.parser = create_parser(prog="clan")
def run(self, args: list[str]) -> argparse.Namespace: def run(self, args: list[str]) -> argparse.Namespace:
parser = create_parser(prog="clan")
cmd = shlex.join(["clan"] + args) cmd = shlex.join(["clan"] + args)
log.debug(f"$ {cmd}") log.debug(f"$ {cmd}")
log.debug(f"Caller {get_caller()}") log.debug(f"Caller {get_caller()}")
parsed = self.parser.parse_args(args) parsed = parser.parse_args(args)
if parsed.flake is None:
parsed.flake = get_clan_flake_toplevel()
if hasattr(parsed, "func"): if hasattr(parsed, "func"):
parsed.func(parsed) parsed.func(parsed)
return parsed return parsed

View File

@@ -6,9 +6,9 @@ from fixtures_flakes import FlakeForTest
@pytest.mark.with_core @pytest.mark.with_core
def test_configure_machine(api: TestClient, test_flake_with_core: FlakeForTest) -> None: def test_configure_machine(api: TestClient, test_flake_with_core: FlakeForTest) -> None:
# retrieve the list of available clanModules # retrieve the list of available clanModules
response = api.get(f"/api/{test_flake_with_core.name}/clan_modules") response = api.get(f"/api/clan_modules?flake_dir={test_flake_with_core.path}")
assert response.status_code == 200, response.text
response_json = response.json() response_json = response.json()
assert response.status_code == 200
assert isinstance(response_json, dict) assert isinstance(response_json, dict)
assert "clan_modules" in response_json assert "clan_modules" in response_json
assert len(response_json["clan_modules"]) > 0 assert len(response_json["clan_modules"]) > 0

View File

@@ -39,6 +39,8 @@ def test_set_some_option(
cli = Cli() cli = Cli()
cli.run( cli.run(
[ [
"--flake",
str(test_flake.path),
"config", "config",
"--quiet", "--quiet",
"--options-file", "--options-file",
@@ -47,7 +49,6 @@ def test_set_some_option(
out_file.name, out_file.name,
] ]
+ args + args
+ [test_flake.name]
) )
json_out = json.loads(open(out_file.name).read()) json_out = json.loads(open(out_file.name).read())
assert json_out == expected assert json_out == expected
@@ -61,11 +62,30 @@ def test_configure_machine(
) -> None: ) -> None:
cli = Cli() cli = Cli()
cli.run(["config", "-m", "machine1", "clan.jitsi.enable", "true", test_flake.name]) cli.run(
[
"--flake",
str(test_flake.path),
"config",
"-m",
"machine1",
"clan.jitsi.enable",
"true",
]
)
# clear the output buffer # clear the output buffer
capsys.readouterr() capsys.readouterr()
# read a option value # read a option value
cli.run(["config", "-m", "machine1", "clan.jitsi.enable", test_flake.name]) cli.run(
[
"--flake",
str(test_flake.path),
"config",
"-m",
"machine1",
"clan.jitsi.enable",
]
)
# read the output # read the output
assert capsys.readouterr().out == "true\n" assert capsys.readouterr().out == "true\n"

View File

@@ -6,7 +6,6 @@ import pytest
from api import TestClient from api import TestClient
from cli import Cli from cli import Cli
from clan_cli.dirs import clan_flakes_dir
from clan_cli.flakes.create import DEFAULT_URL from clan_cli.flakes.create import DEFAULT_URL
@@ -19,13 +18,11 @@ def cli() -> Cli:
def test_create_flake_api( def test_create_flake_api(
monkeypatch: pytest.MonkeyPatch, api: TestClient, temporary_home: Path monkeypatch: pytest.MonkeyPatch, api: TestClient, temporary_home: Path
) -> None: ) -> None:
monkeypatch.chdir(clan_flakes_dir()) flake_dir = temporary_home / "test-flake"
flake_name = "flake_dir"
flake_dir = clan_flakes_dir() / flake_name
response = api.post( response = api.post(
"/api/flake/create", f"/api/flake/create?flake_dir={flake_dir}",
json=dict( json=dict(
flake_name=str(flake_dir), flake_dir=str(flake_dir),
url=str(DEFAULT_URL), url=str(DEFAULT_URL),
), ),
) )
@@ -42,17 +39,15 @@ def test_create_flake(
temporary_home: Path, temporary_home: Path,
cli: Cli, cli: Cli,
) -> None: ) -> None:
monkeypatch.chdir(clan_flakes_dir()) flake_dir = temporary_home / "test-flake"
flake_name = "flake_dir"
flake_dir = clan_flakes_dir() / flake_name
cli.run(["flakes", "create", flake_name]) cli.run(["flakes", "create", str(flake_dir)])
assert (flake_dir / ".clan-flake").exists() assert (flake_dir / ".clan-flake").exists()
monkeypatch.chdir(flake_dir) monkeypatch.chdir(flake_dir)
cli.run(["machines", "create", "machine1", flake_name]) cli.run(["machines", "create", "machine1"])
capsys.readouterr() # flush cache capsys.readouterr() # flush cache
cli.run(["machines", "list", flake_name]) cli.run(["machines", "list"])
assert "machine1" in capsys.readouterr().out assert "machine1" in capsys.readouterr().out
flake_show = subprocess.run( flake_show = subprocess.run(
["nix", "flake", "show", "--json"], ["nix", "flake", "show", "--json"],
@@ -67,9 +62,7 @@ def test_create_flake(
pytest.fail("nixosConfigurations.machine1 not found in flake outputs") pytest.fail("nixosConfigurations.machine1 not found in flake outputs")
# configure machine1 # configure machine1
capsys.readouterr() capsys.readouterr()
cli.run( cli.run(["config", "--machine", "machine1", "services.openssh.enable", ""])
["config", "--machine", "machine1", "services.openssh.enable", "", flake_name]
)
capsys.readouterr() capsys.readouterr()
cli.run( cli.run(
[ [
@@ -78,6 +71,5 @@ def test_create_flake(
"machine1", "machine1",
"services.openssh.enable", "services.openssh.enable",
"true", "true",
flake_name,
] ]
) )

View File

@@ -8,15 +8,6 @@ from fixtures_flakes import FlakeForTest
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@pytest.mark.impure
def test_list_flakes(api: TestClient, test_flake_with_core: FlakeForTest) -> None:
response = api.get("/api/flake/list")
assert response.status_code == 200, "Failed to list flakes"
data = response.json()
print("Data: ", data)
assert data.get("flakes") == ["test_flake_with_core"]
@pytest.mark.impure @pytest.mark.impure
def test_inspect_ok(api: TestClient, test_flake_with_core: FlakeForTest) -> None: def test_inspect_ok(api: TestClient, test_flake_with_core: FlakeForTest) -> None:
params = {"url": str(test_flake_with_core.path)} params = {"url": str(test_flake_with_core.path)}

View File

@@ -3,19 +3,20 @@ from api import TestClient
from fixtures_flakes import FlakeForTest from fixtures_flakes import FlakeForTest
def test_create_and_list(api: TestClient, test_flake: FlakeForTest) -> None: def test_machines(api: TestClient, test_flake: FlakeForTest) -> None:
response = api.get(f"/api/{test_flake.name}/machines") response = api.get(f"/api/machines?flake_dir={test_flake.path}")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"machines": []} assert response.json() == {"machines": []}
response = api.put(
response = api.put(f"/api/{test_flake.name}/machines/test/config", json=dict()) f"/api/machines/test/config?flake_dir={test_flake.path}", json={}
)
assert response.status_code == 200 assert response.status_code == 200
response = api.get(f"/api/{test_flake.name}/machines/test") response = api.get(f"/api/machines/test?flake_dir={test_flake.path}")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"machine": {"name": "test", "status": "unknown"}} assert response.json() == {"machine": {"name": "test", "status": "unknown"}}
response = api.get(f"/api/{test_flake.name}/machines") response = api.get(f"/api/machines?flake_dir={test_flake.path}")
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"machines": [{"name": "test", "status": "unknown"}]} assert response.json() == {"machines": [{"name": "test", "status": "unknown"}]}
@@ -24,7 +25,7 @@ def test_create_and_list(api: TestClient, test_flake: FlakeForTest) -> None:
def test_schema_errors(api: TestClient, test_flake_with_core: FlakeForTest) -> None: def test_schema_errors(api: TestClient, test_flake_with_core: FlakeForTest) -> None:
# make sure that eval errors do not raise an internal server error # make sure that eval errors do not raise an internal server error
response = api.put( response = api.put(
f"/api/{test_flake_with_core.name}/schema", f"/api/schema?flake_dir={test_flake_with_core.path}",
json={"imports": ["some-invalid-import"]}, json={"imports": ["some-invalid-import"]},
) )
assert response.status_code == 422 assert response.status_code == 422
@@ -39,7 +40,7 @@ def test_schema_invalid_clan_imports(
api: TestClient, test_flake_with_core: FlakeForTest api: TestClient, test_flake_with_core: FlakeForTest
) -> None: ) -> None:
response = api.put( response = api.put(
f"/api/{test_flake_with_core.name}/schema", f"/api/schema?flake_dir={test_flake_with_core.path}",
json={"clanImports": ["non-existing-clan-module"]}, json={"clanImports": ["non-existing-clan-module"]},
) )
assert response.status_code == 400 assert response.status_code == 400
@@ -54,7 +55,8 @@ def test_create_machine_invalid_hostname(
api: TestClient, test_flake: FlakeForTest api: TestClient, test_flake: FlakeForTest
) -> None: ) -> None:
response = api.put( response = api.put(
f"/api/{test_flake.name}/machines/-invalid-hostname/config", json=dict() f"/api/machines/-invalid-hostname/config?flake_dir={test_flake.path}",
json=dict(),
) )
assert response.status_code == 422 assert response.status_code == 422
assert ( assert (
@@ -67,7 +69,7 @@ def test_verify_config_without_machine(
api: TestClient, test_flake_with_core: FlakeForTest api: TestClient, test_flake_with_core: FlakeForTest
) -> None: ) -> None:
response = api.put( response = api.put(
f"/api/{test_flake_with_core.name}/machines/test/verify", f"/api/machines/test/verify?flake_dir={test_flake_with_core.path}",
json=dict(), json=dict(),
) )
assert response.status_code == 200 assert response.status_code == 200
@@ -79,12 +81,14 @@ def test_ensure_empty_config_is_valid(
api: TestClient, test_flake_with_core: FlakeForTest api: TestClient, test_flake_with_core: FlakeForTest
) -> None: ) -> None:
response = api.put( response = api.put(
f"/api/{test_flake_with_core.name}/machines/test/config", f"/api/machines/test/config?flake_dir={test_flake_with_core.path}",
json=dict(), json=dict(),
) )
assert response.status_code == 200 assert response.status_code == 200
response = api.get(f"/api/{test_flake_with_core.name}/machines/test/verify") response = api.get(
f"/api/machines/test/verify?flake_dir={test_flake_with_core.path}"
)
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"success": True, "error": None} assert response.json() == {"success": True, "error": None}
@@ -92,17 +96,21 @@ def test_ensure_empty_config_is_valid(
@pytest.mark.with_core @pytest.mark.with_core
def test_configure_machine(api: TestClient, test_flake_with_core: FlakeForTest) -> None: def test_configure_machine(api: TestClient, test_flake_with_core: FlakeForTest) -> None:
# ensure error 404 if machine does not exist when accessing the config # ensure error 404 if machine does not exist when accessing the config
response = api.get(f"/api/{test_flake_with_core.name}/machines/machine1/config") response = api.get(
f"/api/machines/machine1/config?flake_dir={test_flake_with_core.path}"
)
assert response.status_code == 404 assert response.status_code == 404
# create the machine # create the machine
response = api.put( response = api.put(
f"/api/{test_flake_with_core.name}/machines/machine1/config", json={} f"/api/machines/machine1/config?flake_dir={test_flake_with_core.path}", json={}
) )
assert response.status_code == 200 assert response.status_code == 200
# ensure an empty config is returned by default for a new machine # ensure an empty config is returned by default for a new machine
response = api.get(f"/api/{test_flake_with_core.name}/machines/machine1/config") response = api.get(
f"/api/machines/machine1/config?flake_dir={test_flake_with_core.path}"
)
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == { assert response.json() == {
"clanImports": [], "clanImports": [],
@@ -111,7 +119,7 @@ def test_configure_machine(api: TestClient, test_flake_with_core: FlakeForTest)
# get jsonschema for without imports # get jsonschema for without imports
response = api.put( response = api.put(
f"/api/{test_flake_with_core.name}/schema", f"/api/schema?flake_dir={test_flake_with_core.path}",
json={"clanImports": []}, json={"clanImports": []},
) )
assert response.status_code == 200 assert response.status_code == 200
@@ -133,7 +141,7 @@ def test_configure_machine(api: TestClient, test_flake_with_core: FlakeForTest)
# verify an invalid config (foo option does not exist) # verify an invalid config (foo option does not exist)
response = api.put( response = api.put(
f"/api/{test_flake_with_core.name}/machines/machine1/verify", f"/api/machines/machine1/verify?flake_dir={test_flake_with_core.path}",
json=invalid_config, json=invalid_config,
) )
assert response.status_code == 200 assert response.status_code == 200
@@ -141,13 +149,15 @@ def test_configure_machine(api: TestClient, test_flake_with_core: FlakeForTest)
# set come invalid config (foo option does not exist) # set come invalid config (foo option does not exist)
response = api.put( response = api.put(
f"/api/{test_flake_with_core.name}/machines/machine1/config", f"/api/machines/machine1/config?flake_dir={test_flake_with_core.path}",
json=invalid_config, json=invalid_config,
) )
assert response.status_code == 200 assert response.status_code == 200
# ensure the config has actually been updated # ensure the config has actually been updated
response = api.get(f"/api/{test_flake_with_core.name}/machines/machine1/config") response = api.get(
f"/api/machines/machine1/config?flake_dir={test_flake_with_core.path}"
)
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == dict(clanImports=[], **invalid_config) assert response.json() == dict(clanImports=[], **invalid_config)
@@ -162,20 +172,22 @@ def test_configure_machine(api: TestClient, test_flake_with_core: FlakeForTest)
) )
response = api.put( response = api.put(
f"/api/{test_flake_with_core.name}/machines/machine1/config", f"/api/machines/machine1/config?flake_dir={test_flake_with_core.path}",
json=config2, json=config2,
) )
assert response.status_code == 200 assert response.status_code == 200
# ensure the config has been applied # ensure the config has been applied
response = api.get( response = api.get(
f"/api/{test_flake_with_core.name}/machines/machine1/config", f"/api/machines/machine1/config?flake_dir={test_flake_with_core.path}",
) )
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == dict(clanImports=[], **config2) assert response.json() == dict(clanImports=[], **config2)
# get the config again # get the config again
response = api.get(f"/api/{test_flake_with_core.name}/machines/machine1/config") response = api.get(
f"/api/machines/machine1/config?flake_dir={test_flake_with_core.path}"
)
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"clanImports": [], **config2} assert response.json() == {"clanImports": [], **config2}
@@ -183,27 +195,29 @@ def test_configure_machine(api: TestClient, test_flake_with_core: FlakeForTest)
# For example, this should not result in the boot.loader.grub.devices being # For example, this should not result in the boot.loader.grub.devices being
# set twice (eg. merged) # set twice (eg. merged)
response = api.put( response = api.put(
f"/api/{test_flake_with_core.name}/machines/machine1/config", f"/api/machines/machine1/config?flake_dir={test_flake_with_core.path}",
json=config2, json=config2,
) )
assert response.status_code == 200 assert response.status_code == 200
# ensure the config has been applied # ensure the config has been applied
response = api.get( response = api.get(
f"/api/{test_flake_with_core.name}/machines/machine1/config", f"/api/machines/machine1/config?flake_dir={test_flake_with_core.path}",
) )
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == dict(clanImports=[], **config2) assert response.json() == dict(clanImports=[], **config2)
# verify the machine config evaluates # verify the machine config evaluates
response = api.get(f"/api/{test_flake_with_core.name}/machines/machine1/verify") response = api.get(
f"/api/machines/machine1/verify?flake_dir={test_flake_with_core.path}"
)
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"success": True, "error": None} assert response.json() == {"success": True, "error": None}
# get the schema with an extra module imported # get the schema with an extra module imported
response = api.put( response = api.put(
f"/api/{test_flake_with_core.name}/schema", f"/api/schema?flake_dir={test_flake_with_core.path}",
json={"clanImports": ["diskLayouts"]}, json={"clanImports": ["diskLayouts"]},
) )
# expect the result schema to contain the deltachat option # expect the result schema to contain the deltachat option
@@ -227,14 +241,14 @@ def test_configure_machine(api: TestClient, test_flake_with_core: FlakeForTest)
# set the fake-module.fake-flag option to true # set the fake-module.fake-flag option to true
response = api.put( response = api.put(
f"/api/{test_flake_with_core.name}/machines/machine1/config", f"/api/machines/machine1/config?flake_dir={test_flake_with_core.path}",
json=config_with_imports, json=config_with_imports,
) )
assert response.status_code == 200 assert response.status_code == 200
# ensure the config has been applied # ensure the config has been applied
response = api.get( response = api.get(
f"/api/{test_flake_with_core.name}/machines/machine1/config", f"/api/machines/machine1/config?flake_dir={test_flake_with_core.path}",
) )
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == { assert response.json() == {
@@ -248,7 +262,7 @@ def test_configure_machine(api: TestClient, test_flake_with_core: FlakeForTest)
# remove the import from the config # remove the import from the config
response = api.put( response = api.put(
f"/api/{test_flake_with_core.name}/machines/machine1/config", f"/api/machines/machine1/config?flake_dir={test_flake_with_core.path}",
json=dict( json=dict(
clanImports=[], clanImports=[],
), ),
@@ -257,7 +271,7 @@ def test_configure_machine(api: TestClient, test_flake_with_core: FlakeForTest)
# ensure the config has been applied # ensure the config has been applied
response = api.get( response = api.get(
f"/api/{test_flake_with_core.name}/machines/machine1/config", f"/api/machines/machine1/config?flake_dir={test_flake_with_core.path}",
) )
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == { assert response.json() == {

View File

@@ -7,16 +7,16 @@ def test_machine_subcommands(
test_flake: FlakeForTest, capsys: pytest.CaptureFixture test_flake: FlakeForTest, capsys: pytest.CaptureFixture
) -> None: ) -> None:
cli = Cli() cli = Cli()
cli.run(["machines", "create", "machine1", test_flake.name]) cli.run(["--flake", str(test_flake.path), "machines", "create", "machine1"])
capsys.readouterr() capsys.readouterr()
cli.run(["machines", "list", test_flake.name]) cli.run(["--flake", str(test_flake.path), "machines", "list"])
out = capsys.readouterr() out = capsys.readouterr()
assert "machine1\n" == out.out assert "machine1\n" == out.out
cli.run(["machines", "delete", "machine1", test_flake.name]) cli.run(["--flake", str(test_flake.path), "machines", "delete", "machine1"])
capsys.readouterr() capsys.readouterr()
cli.run(["machines", "list", test_flake.name]) cli.run(["--flake", str(test_flake.path), "machines", "list"])
out = capsys.readouterr() out = capsys.readouterr()
assert "" == out.out assert "" == out.out

View File

@@ -6,5 +6,5 @@ from clan_cli.config.schema import machine_schema
@pytest.mark.with_core @pytest.mark.with_core
def test_schema_for_machine(test_flake_with_core: FlakeForTest) -> None: def test_schema_for_machine(test_flake_with_core: FlakeForTest) -> None:
schema = machine_schema(test_flake_with_core.name, config={}) schema = machine_schema(test_flake_with_core.path, config={})
assert "properties" in schema assert "properties" in schema

View File

@@ -38,7 +38,7 @@ def test_generate_secret(
has_secret(test_flake_with_core.path, "vm1-zerotier-identity-secret") has_secret(test_flake_with_core.path, "vm1-zerotier-identity-secret")
has_secret(test_flake_with_core.path, "vm1-zerotier-subnet") has_secret(test_flake_with_core.path, "vm1-zerotier-subnet")
network_id = machine_get_fact( network_id = machine_get_fact(
test_flake_with_core.name, "vm1", "zerotier-network-id" test_flake_with_core.path, "vm1", "zerotier-network-id"
) )
assert len(network_id) == 16 assert len(network_id) == 16
secrets_folder = sops_secrets_folder(test_flake_with_core.path) secrets_folder = sops_secrets_folder(test_flake_with_core.path)
@@ -59,7 +59,7 @@ def test_generate_secret(
cli.run(["secrets", "generate", "vm2"]) cli.run(["secrets", "generate", "vm2"])
assert has_secret(test_flake_with_core.path, "vm2-age.key") assert has_secret(test_flake_with_core.path, "vm2-age.key")
assert has_secret(test_flake_with_core.path, "vm2-zerotier-identity-secret") assert has_secret(test_flake_with_core.path, "vm2-zerotier-identity-secret")
ip = machine_get_fact(test_flake_with_core.name, "vm1", "zerotier-ip") ip = machine_get_fact(test_flake_with_core.path, "vm1", "zerotier-ip")
assert ipaddress.IPv6Address(ip).is_private assert ipaddress.IPv6Address(ip).is_private
meshname = machine_get_fact(test_flake_with_core.name, "vm1", "zerotier-meshname") meshname = machine_get_fact(test_flake_with_core.path, "vm1", "zerotier-meshname")
assert len(meshname) == 26 assert len(meshname) == 26

View File

@@ -41,7 +41,7 @@ def test_upload_secret(
subprocess.run(nix_shell(["pass"], ["pass", "init", "test@local"]), check=True) subprocess.run(nix_shell(["pass"], ["pass", "init", "test@local"]), check=True)
cli.run(["secrets", "generate", "vm1"]) cli.run(["secrets", "generate", "vm1"])
network_id = machine_get_fact( network_id = machine_get_fact(
test_flake_with_core_and_pass.name, "vm1", "zerotier-network-id" test_flake_with_core_and_pass.path, "vm1", "zerotier-network-id"
) )
assert len(network_id) == 16 assert len(network_id) == 16
identity_secret = ( identity_secret = (

View File

@@ -10,8 +10,6 @@ from httpx import SyncByteStream
from pydantic import AnyUrl from pydantic import AnyUrl
from root import CLAN_CORE from root import CLAN_CORE
from clan_cli.types import FlakeName
if TYPE_CHECKING: if TYPE_CHECKING:
from age_keys import KeyPair from age_keys import KeyPair
@@ -23,7 +21,7 @@ def flake_with_vm_with_secrets(
yield from create_flake( yield from create_flake(
monkeypatch, monkeypatch,
temporary_home, temporary_home,
FlakeName("test_flake_with_core_dynamic_machines"), "test_flake_with_core_dynamic_machines",
CLAN_CORE, CLAN_CORE,
machines=["vm_with_secrets"], machines=["vm_with_secrets"],
) )
@@ -36,7 +34,7 @@ def remote_flake_with_vm_without_secrets(
yield from create_flake( yield from create_flake(
monkeypatch, monkeypatch,
temporary_home, temporary_home,
FlakeName("test_flake_with_core_dynamic_machines"), "test_flake_with_core_dynamic_machines",
CLAN_CORE, CLAN_CORE,
machines=["vm_without_secrets"], machines=["vm_without_secrets"],
remote=True, remote=True,

View File

@@ -16,7 +16,7 @@ def test_inspect(
test_flake_with_core: FlakeForTest, capsys: pytest.CaptureFixture test_flake_with_core: FlakeForTest, capsys: pytest.CaptureFixture
) -> None: ) -> None:
cli = Cli() cli = Cli()
cli.run(["vms", "inspect", "vm1", test_flake_with_core.name]) cli.run(["--flake", str(test_flake_with_core.path), "vms", "inspect", "vm1"])
out = capsys.readouterr() # empty the buffer out = capsys.readouterr() # empty the buffer
assert "Cores" in out.out assert "Cores" in out.out