Merge pull request 'clan-cli: Refactor the API to use the Flake object' (#3531) from Qubasa/clan-core:replace_machine_name_with_machine_obj into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3531
This commit is contained in:
Mic92
2025-05-07 13:21:13 +00:00
31 changed files with 168 additions and 136 deletions

View File

@@ -107,7 +107,7 @@ def create_clan(opts: CreateOptions) -> CreateClanResponse:
response.flake_update = flake_update response.flake_update = flake_update
if opts.initial: if opts.initial:
init_inventory(str(opts.dest), init=opts.initial) init_inventory(Flake(str(opts.dest)), init=opts.initial)
return response return response

View File

@@ -3,8 +3,8 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from clan_cli.clan_dirs import machine_gcroot
from clan_cli.cmd import run from clan_cli.cmd import run
from clan_cli.dirs import machine_gcroot
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.flake import Flake from clan_cli.flake import Flake
from clan_cli.machines.list import list_nixos_machines from clan_cli.machines.list import list_nixos_machines

View File

@@ -2,20 +2,21 @@ from dataclasses import dataclass
from clan_lib.api import API from clan_lib.api import API
from clan_cli.flake import Flake
from clan_cli.inventory import Inventory, Meta, load_inventory_json, set_inventory from clan_cli.inventory import Inventory, Meta, load_inventory_json, set_inventory
@dataclass @dataclass
class UpdateOptions: class UpdateOptions:
directory: str flake: Flake
meta: Meta meta: Meta
@API.register @API.register
def update_clan_meta(options: UpdateOptions) -> Inventory: def update_clan_meta(options: UpdateOptions) -> Inventory:
inventory = load_inventory_json(options.directory) inventory = load_inventory_json(options.flake)
inventory["meta"] = options.meta inventory["meta"] = options.meta
set_inventory(inventory, options.directory, "Update clan metadata") set_inventory(inventory, options.flake, "Update clan metadata")
return inventory return inventory

View File

@@ -0,0 +1,43 @@
import logging
import urllib
from pathlib import Path
from clan_cli.flake import Flake
from .dirs import user_data_dir, user_gcroot_dir
log = logging.getLogger(__name__)
def clan_key_safe(flake_url: str) -> str:
"""
only embed the url in the path, not the clan name, as it would involve eval.
"""
quoted_url = urllib.parse.quote_plus(flake_url)
return f"{quoted_url}"
def machine_gcroot(flake_url: str) -> Path:
# Always build icon so that we can symlink it to the gcroot
gcroot_dir = user_gcroot_dir()
clan_gcroot = gcroot_dir / clan_key_safe(flake_url)
clan_gcroot.mkdir(parents=True, exist_ok=True)
return clan_gcroot
def vm_state_dir(flake_url: str, vm_name: str) -> Path:
clan_key = clan_key_safe(str(flake_url))
return user_data_dir() / "clan" / "vmstate" / clan_key / vm_name
def machines_dir(flake: Flake) -> Path:
if flake.is_local:
return flake.path / "machines"
store_path = flake.store_path
assert store_path is not None, "Invalid flake object"
return Path(store_path) / "machines"
def specific_machine_dir(flake: Flake, machine: str) -> Path:
return machines_dir(flake) / machine

View File

@@ -1,7 +1,6 @@
import logging import logging
import os import os
import sys import sys
import urllib
from enum import Enum from enum import Enum
from pathlib import Path from pathlib import Path
@@ -24,14 +23,6 @@ def find_git_repo_root() -> Path | None:
return find_toplevel([".git"]) return find_toplevel([".git"])
def clan_key_safe(flake_url: str) -> str:
"""
only embed the url in the path, not the clan name, as it would involve eval.
"""
quoted_url = urllib.parse.quote_plus(flake_url)
return f"{quoted_url}"
def find_toplevel(top_level_files: list[str]) -> Path | None: def find_toplevel(top_level_files: list[str]) -> Path | None:
"""Returns the path to the toplevel of the clan flake""" """Returns the path to the toplevel of the clan flake"""
for project_file in top_level_files: for project_file in top_level_files:
@@ -114,31 +105,10 @@ def user_gcroot_dir() -> Path:
return p return p
def machine_gcroot(flake_url: str) -> Path:
# Always build icon so that we can symlink it to the gcroot
gcroot_dir = user_gcroot_dir()
clan_gcroot = gcroot_dir / clan_key_safe(flake_url)
clan_gcroot.mkdir(parents=True, exist_ok=True)
return clan_gcroot
def user_history_file() -> Path: def user_history_file() -> Path:
return user_config_dir() / "clan" / "history" return user_config_dir() / "clan" / "history"
def vm_state_dir(flake_url: str, vm_name: str) -> Path:
clan_key = clan_key_safe(str(flake_url))
return user_data_dir() / "clan" / "vmstate" / clan_key / vm_name
def machines_dir(flake_dir: Path) -> Path:
return flake_dir / "machines"
def specific_machine_dir(flake_dir: Path, machine: str) -> Path:
return machines_dir(flake_dir) / machine
def module_root() -> Path: def module_root() -> Path:
return Path(__file__).parent return Path(__file__).parent

View File

@@ -1,7 +1,7 @@
import logging import logging
from pathlib import Path from pathlib import Path
from clan_cli.dirs import vm_state_dir from clan_cli.clan_dirs import vm_state_dir
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine

View File

@@ -2,7 +2,7 @@ import shutil
from pathlib import Path from pathlib import Path
from typing import override from typing import override
from clan_cli.dirs import vm_state_dir from clan_cli.clan_dirs import vm_state_dir
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from . import SecretStoreBase from . import SecretStoreBase

View File

@@ -23,6 +23,7 @@ from clan_lib.api import API, dataclass_to_dict, from_dict
from clan_cli.cmd import run_no_stdout from clan_cli.cmd import run_no_stdout
from clan_cli.errors import ClanCmdError, ClanError from clan_cli.errors import ClanCmdError, ClanError
from clan_cli.flake import Flake
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
@@ -49,11 +50,11 @@ __all__ = [
] ]
def get_inventory_path(flake_dir: str | Path) -> Path: def get_inventory_path(flake: Flake) -> Path:
""" """
Get the path to the inventory file in the flake directory Get the path to the inventory file in the flake directory
""" """
inventory_file = (Path(flake_dir) / "inventory.json").resolve() inventory_file = (flake.path / "inventory.json").resolve()
return inventory_file return inventory_file
@@ -61,7 +62,7 @@ def get_inventory_path(flake_dir: str | Path) -> Path:
default_inventory: Inventory = {"meta": {"name": "New Clan"}} default_inventory: Inventory = {"meta": {"name": "New Clan"}}
def load_inventory_eval(flake_dir: str | Path) -> Inventory: def load_inventory_eval(flake_dir: Flake) -> Inventory:
""" """
Loads the evaluated inventory. Loads the evaluated inventory.
After all merge operations with eventual nix code in buildClan. After all merge operations with eventual nix code in buildClan.
@@ -354,7 +355,7 @@ def determine_writeability(
return results return results
def get_inventory_current_priority(flake_dir: str | Path) -> dict: def get_inventory_current_priority(flake: Flake) -> dict:
""" """
Returns the current priority of the inventory values Returns the current priority of the inventory values
@@ -374,7 +375,7 @@ def get_inventory_current_priority(flake_dir: str | Path) -> dict:
""" """
cmd = nix_eval( cmd = nix_eval(
[ [
f"{flake_dir}#clanInternals.inventoryClass.introspection", f"{flake}#clanInternals.inventoryClass.introspection",
"--json", "--json",
] ]
) )
@@ -392,7 +393,7 @@ def get_inventory_current_priority(flake_dir: str | Path) -> dict:
@API.register @API.register
def load_inventory_json(flake_dir: str | Path) -> Inventory: def load_inventory_json(flake: Flake) -> Inventory:
""" """
Load the inventory FILE from the flake directory Load the inventory FILE from the flake directory
If no file is found, returns an empty dictionary If no file is found, returns an empty dictionary
@@ -402,7 +403,7 @@ def load_inventory_json(flake_dir: str | Path) -> Inventory:
Use load_inventory_eval instead Use load_inventory_eval instead
""" """
inventory_file = get_inventory_path(flake_dir) inventory_file = get_inventory_path(flake)
if not inventory_file.exists(): if not inventory_file.exists():
return {} return {}
@@ -472,14 +473,14 @@ def patch(d: dict[str, Any], path: str, content: Any) -> None:
@API.register @API.register
def patch_inventory_with(base_dir: Path, section: str, content: dict[str, Any]) -> None: def patch_inventory_with(flake: Flake, section: str, content: dict[str, Any]) -> None:
""" """
Pass only the section to update and the content to update with. Pass only the section to update and the content to update with.
Make sure you pass only attributes that you would like to persist. Make sure you pass only attributes that you would like to persist.
ATTENTION: Don't pass nix eval values unintentionally. ATTENTION: Don't pass nix eval values unintentionally.
""" """
inventory_file = get_inventory_path(base_dir) inventory_file = get_inventory_path(flake)
curr_inventory = {} curr_inventory = {}
if inventory_file.exists(): if inventory_file.exists():
@@ -491,7 +492,9 @@ def patch_inventory_with(base_dir: Path, section: str, content: dict[str, Any])
with inventory_file.open("w") as f: with inventory_file.open("w") as f:
json.dump(curr_inventory, f, indent=2) json.dump(curr_inventory, f, indent=2)
commit_file(inventory_file, base_dir, commit_message=f"inventory.{section}: Update") commit_file(
inventory_file, flake.path, commit_message=f"inventory.{section}: Update"
)
@dataclass @dataclass
@@ -503,16 +506,16 @@ class WriteInfo:
@API.register @API.register
def get_inventory_with_writeable_keys( def get_inventory_with_writeable_keys(
flake_dir: str | Path, flake: Flake,
) -> WriteInfo: ) -> WriteInfo:
""" """
Load the inventory and determine the writeable keys Load the inventory and determine the writeable keys
Performs 2 nix evaluations to get the current priority and the inventory Performs 2 nix evaluations to get the current priority and the inventory
""" """
current_priority = get_inventory_current_priority(flake_dir) current_priority = get_inventory_current_priority(flake)
data_eval: Inventory = load_inventory_eval(flake_dir) data_eval: Inventory = load_inventory_eval(flake)
data_disk: Inventory = load_inventory_json(flake_dir) data_disk: Inventory = load_inventory_json(flake)
writeables = determine_writeability( writeables = determine_writeability(
current_priority, dict(data_eval), dict(data_disk) current_priority, dict(data_eval), dict(data_disk)
@@ -524,14 +527,14 @@ def get_inventory_with_writeable_keys(
# TODO: remove this function in favor of a proper read/write API # TODO: remove this function in favor of a proper read/write API
@API.register @API.register
def set_inventory( def set_inventory(
inventory: Inventory, flake_dir: str | Path, message: str, commit: bool = True inventory: Inventory, flake: Flake, message: str, commit: bool = True
) -> None: ) -> None:
""" """
Write the inventory to the flake directory Write the inventory to the flake directory
and commit it to git with the given message and commit it to git with the given message
""" """
write_info = get_inventory_with_writeable_keys(flake_dir) write_info = get_inventory_with_writeable_keys(flake)
# Remove internals from the inventory # Remove internals from the inventory
inventory.pop("tags", None) # type: ignore inventory.pop("tags", None) # type: ignore
@@ -552,43 +555,43 @@ def set_inventory(
for delete_path in delete_set: for delete_path in delete_set:
delete_by_path(persisted, delete_path) delete_by_path(persisted, delete_path)
inventory_file = get_inventory_path(flake_dir) inventory_file = get_inventory_path(flake)
with inventory_file.open("w") as f: with inventory_file.open("w") as f:
json.dump(persisted, f, indent=2) json.dump(persisted, f, indent=2)
if commit: if commit:
commit_file(inventory_file, Path(flake_dir), commit_message=message) commit_file(inventory_file, flake.path, commit_message=message)
# TODO: wrap this in a proper persistence API # TODO: wrap this in a proper persistence API
def delete(directory: str | Path, delete_set: set[str]) -> None: def delete(flake: Flake, delete_set: set[str]) -> None:
""" """
Delete keys from the inventory Delete keys from the inventory
""" """
write_info = get_inventory_with_writeable_keys(directory) write_info = get_inventory_with_writeable_keys(flake)
data_disk = dict(write_info.data_disk) data_disk = dict(write_info.data_disk)
for delete_path in delete_set: for delete_path in delete_set:
delete_by_path(data_disk, delete_path) delete_by_path(data_disk, delete_path)
inventory_file = get_inventory_path(directory) inventory_file = get_inventory_path(flake)
with inventory_file.open("w") as f: with inventory_file.open("w") as f:
json.dump(data_disk, f, indent=2) json.dump(data_disk, f, indent=2)
commit_file( commit_file(
inventory_file, inventory_file,
Path(directory), flake.path,
commit_message=f"Delete inventory keys {delete_set}", commit_message=f"Delete inventory keys {delete_set}",
) )
def init_inventory(directory: str, init: Inventory | None = None) -> None: def init_inventory(flake: Flake, init: Inventory | None = None) -> None:
inventory = None inventory = None
# Try reading the current flake # Try reading the current flake
if init is None: if init is None:
with contextlib.suppress(ClanCmdError): with contextlib.suppress(ClanCmdError):
inventory = load_inventory_eval(directory) inventory = load_inventory_eval(flake)
if init is not None: if init is not None:
inventory = init inventory = init
@@ -596,9 +599,9 @@ def init_inventory(directory: str, init: Inventory | None = None) -> None:
# Write inventory.json file # Write inventory.json file
if inventory is not None: if inventory is not None:
# Persist creates a commit message for each change # Persist creates a commit message for each change
set_inventory(inventory, directory, "Init inventory") set_inventory(inventory, flake, "Init inventory")
@API.register @API.register
def get_inventory(base_path: str | Path) -> Inventory: def get_inventory(flake: Flake) -> Inventory:
return load_inventory_eval(base_path) return load_inventory_eval(flake)

View File

@@ -110,7 +110,7 @@ def create_machine(opts: CreateOptions, commit: bool = True) -> None:
new_machine["deploy"] = {"targetHost": target_host} new_machine["deploy"] = {"targetHost": target_host}
patch_inventory_with( patch_inventory_with(
clan_dir, f"machines.{machine_name}", dataclass_to_dict(new_machine) Flake(str(clan_dir)), f"machines.{machine_name}", dataclass_to_dict(new_machine)
) )
# Commit at the end in that order to avoid committing halve-baked machines # Commit at the end in that order to avoid committing halve-baked machines

View File

@@ -6,8 +6,8 @@ from pathlib import Path
from clan_lib.api import API from clan_lib.api import API
from clan_cli import Flake, inventory from clan_cli import Flake, inventory
from clan_cli.clan_dirs import specific_machine_dir
from clan_cli.completions import add_dynamic_completer, complete_machines from clan_cli.completions import add_dynamic_completer, complete_machines
from clan_cli.dirs import specific_machine_dir
from clan_cli.secrets.folders import sops_secrets_folder from clan_cli.secrets.folders import sops_secrets_folder
from clan_cli.secrets.machines import has_machine as secrets_has_machine from clan_cli.secrets.machines import has_machine as secrets_has_machine
from clan_cli.secrets.machines import remove_machine as secrets_machine_remove from clan_cli.secrets.machines import remove_machine as secrets_machine_remove
@@ -23,7 +23,7 @@ log = logging.getLogger(__name__)
@API.register @API.register
def delete_machine(flake: Flake, name: str) -> None: def delete_machine(flake: Flake, name: str) -> None:
try: try:
inventory.delete(str(flake.path), {f"machines.{name}"}) inventory.delete(flake, {f"machines.{name}"})
except KeyError as exc: except KeyError as exc:
# louis@(2025-03-09): test infrastructure does not seem to set the # louis@(2025-03-09): test infrastructure does not seem to set the
# inventory properly, but more importantly only one machine in my # inventory properly, but more importantly only one machine in my
@@ -35,7 +35,7 @@ def delete_machine(flake: Flake, name: str) -> None:
changed_paths: list[Path] = [] changed_paths: list[Path] = []
folder = specific_machine_dir(flake.path, name) folder = specific_machine_dir(flake, name)
if folder.exists(): if folder.exists():
changed_paths.append(folder) changed_paths.append(folder)
shutil.rmtree(folder) shutil.rmtree(folder)

View File

@@ -7,9 +7,9 @@ from pathlib import Path
from clan_lib.api import API from clan_lib.api import API
from clan_cli.clan_dirs import specific_machine_dir
from clan_cli.cmd import RunOpts, run_no_stdout from clan_cli.cmd import RunOpts, run_no_stdout
from clan_cli.completions import add_dynamic_completer, complete_machines from clan_cli.completions import add_dynamic_completer, complete_machines
from clan_cli.dirs import specific_machine_dir
from clan_cli.errors import ClanCmdError, ClanError from clan_cli.errors import ClanCmdError, ClanError
from clan_cli.flake import Flake from clan_cli.flake import Flake
from clan_cli.git import commit_file from clan_cli.git import commit_file
@@ -26,39 +26,39 @@ class HardwareConfig(Enum):
NIXOS_GENERATE_CONFIG = "nixos-generate-config" NIXOS_GENERATE_CONFIG = "nixos-generate-config"
NONE = "none" NONE = "none"
def config_path(self, clan_dir: Path, machine_name: str) -> Path: def config_path(self, flake: Flake, machine_name: str) -> Path:
machine_dir = specific_machine_dir(clan_dir, machine_name) machine_dir = specific_machine_dir(flake, machine_name)
if self == HardwareConfig.NIXOS_FACTER: if self == HardwareConfig.NIXOS_FACTER:
return machine_dir / "facter.json" return machine_dir / "facter.json"
return machine_dir / "hardware-configuration.nix" return machine_dir / "hardware-configuration.nix"
@classmethod @classmethod
def detect_type( def detect_type(
cls: type["HardwareConfig"], clan_dir: Path, machine_name: str cls: type["HardwareConfig"], flake: Flake, machine_name: str
) -> "HardwareConfig": ) -> "HardwareConfig":
hardware_config = HardwareConfig.NIXOS_GENERATE_CONFIG.config_path( hardware_config = HardwareConfig.NIXOS_GENERATE_CONFIG.config_path(
clan_dir, machine_name flake, machine_name
) )
if hardware_config.exists() and "throw" not in hardware_config.read_text(): if hardware_config.exists() and "throw" not in hardware_config.read_text():
return HardwareConfig.NIXOS_GENERATE_CONFIG return HardwareConfig.NIXOS_GENERATE_CONFIG
if HardwareConfig.NIXOS_FACTER.config_path(clan_dir, machine_name).exists(): if HardwareConfig.NIXOS_FACTER.config_path(flake, machine_name).exists():
return HardwareConfig.NIXOS_FACTER return HardwareConfig.NIXOS_FACTER
return HardwareConfig.NONE return HardwareConfig.NONE
@API.register @API.register
def show_machine_hardware_config(clan_dir: Path, machine_name: str) -> HardwareConfig: def show_machine_hardware_config(flake: Flake, machine_name: str) -> HardwareConfig:
""" """
Show hardware information for a machine returns None if none exist. Show hardware information for a machine returns None if none exist.
""" """
return HardwareConfig.detect_type(clan_dir, machine_name) return HardwareConfig.detect_type(flake, machine_name)
@API.register @API.register
def show_machine_hardware_platform(clan_dir: Path, machine_name: str) -> str | None: def show_machine_hardware_platform(flake: Flake, machine_name: str) -> str | None:
""" """
Show hardware information for a machine returns None if none exist. Show hardware information for a machine returns None if none exist.
""" """
@@ -66,7 +66,7 @@ def show_machine_hardware_platform(clan_dir: Path, machine_name: str) -> str | N
system = config["system"] system = config["system"]
cmd = nix_eval( cmd = nix_eval(
[ [
f"{clan_dir}#clanInternals.machines.{system}.{machine_name}", f"{flake}#clanInternals.machines.{system}.{machine_name}",
"--apply", "--apply",
"machine: { inherit (machine.pkgs) system; }", "machine: { inherit (machine.pkgs) system; }",
"--json", "--json",
@@ -103,7 +103,7 @@ def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareCon
override_target_host=opts.target_host, override_target_host=opts.target_host,
) )
hw_file = opts.backend.config_path(opts.flake.path, opts.machine) hw_file = opts.backend.config_path(opts.flake, opts.machine)
hw_file.parent.mkdir(parents=True, exist_ok=True) hw_file.parent.mkdir(parents=True, exist_ok=True)
if opts.backend == HardwareConfig.NIXOS_FACTER: if opts.backend == HardwareConfig.NIXOS_FACTER:
@@ -152,7 +152,7 @@ def generate_machine_hardware_info(opts: HardwareGenerateOptions) -> HardwareCon
f"machines/{opts.machine}/{hw_file.name}: update hardware configuration", f"machines/{opts.machine}/{hw_file.name}: update hardware configuration",
) )
try: try:
show_machine_hardware_platform(opts.flake.path, opts.machine) show_machine_hardware_platform(opts.flake, opts.machine)
if backup_file: if backup_file:
backup_file.unlink(missing_ok=True) backup_file.unlink(missing_ok=True)
except ClanCmdError as e: except ClanCmdError as e:

View File

@@ -113,7 +113,7 @@ def install_machine(opts: InstallOptions) -> None:
str(opts.update_hardware_config.value), str(opts.update_hardware_config.value),
str( str(
opts.update_hardware_config.config_path( opts.update_hardware_config.config_path(
machine.flake.path, machine.name machine.flake, machine.name
) )
), ),
] ]

View File

@@ -12,16 +12,18 @@ from clan_lib.api.disk import MachineDiskMatter
from clan_lib.api.modules import parse_frontmatter from clan_lib.api.modules import parse_frontmatter
from clan_lib.api.serde import dataclass_to_dict from clan_lib.api.serde import dataclass_to_dict
from clan_cli.clan_dirs import specific_machine_dir
from clan_cli.cmd import RunOpts, run from clan_cli.cmd import RunOpts, run
from clan_cli.completions import add_dynamic_completer, complete_tags from clan_cli.completions import add_dynamic_completer, complete_tags
from clan_cli.dirs import specific_machine_dir
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.flake import Flake
from clan_cli.inventory import ( from clan_cli.inventory import (
load_inventory_eval, load_inventory_eval,
patch_inventory_with, patch_inventory_with,
) )
from clan_cli.inventory.classes import Machine as InventoryMachine from clan_cli.inventory.classes import Machine as InventoryMachine
from clan_cli.machines.hardware import HardwareConfig from clan_cli.machines.hardware import HardwareConfig
from clan_cli.machines.machines import Machine
from clan_cli.nix import nix_eval from clan_cli.nix import nix_eval
from clan_cli.tags import list_nixos_machines_by_tags from clan_cli.tags import list_nixos_machines_by_tags
@@ -29,15 +31,13 @@ log = logging.getLogger(__name__)
@API.register @API.register
def set_machine(flake_url: Path, machine_name: str, machine: InventoryMachine) -> None: def set_machine(flake: Flake, machine_name: str, machine: InventoryMachine) -> None:
patch_inventory_with( patch_inventory_with(flake, f"machines.{machine_name}", dataclass_to_dict(machine))
flake_url, f"machines.{machine_name}", dataclass_to_dict(machine)
)
@API.register @API.register
def list_machines(flake_url: str | Path) -> dict[str, InventoryMachine]: def list_machines(flake: Flake) -> dict[str, InventoryMachine]:
inventory = load_inventory_eval(flake_url) inventory = load_inventory_eval(flake)
return inventory.get("machines", {}) return inventory.get("machines", {})
@@ -60,16 +60,16 @@ def extract_header(c: str) -> str:
@API.register @API.register
def get_machine_details(flake_url: Path, machine_name: str) -> MachineDetails: def get_machine_details(machine: Machine) -> MachineDetails:
inventory = load_inventory_eval(flake_url) inventory = load_inventory_eval(machine.flake)
machine = inventory.get("machines", {}).get(machine_name) machine_inv = inventory.get("machines", {}).get(machine.name)
if machine is None: if machine_inv is None:
msg = f"Machine {machine_name} not found in inventory" msg = f"Machine {machine.name} not found in inventory"
raise ClanError(msg) raise ClanError(msg)
hw_config = HardwareConfig.detect_type(flake_url, machine_name) hw_config = HardwareConfig.detect_type(machine.flake, machine.name)
machine_dir = specific_machine_dir(flake_url, machine_name) machine_dir = specific_machine_dir(machine.flake, machine.name)
disk_schema: MachineDiskMatter | None = None disk_schema: MachineDiskMatter | None = None
disk_path = machine_dir / "disko.nix" disk_path = machine_dir / "disko.nix"
if disk_path.exists(): if disk_path.exists():
@@ -80,7 +80,9 @@ def get_machine_details(flake_url: Path, machine_name: str) -> MachineDetails:
if data: if data:
disk_schema = data # type: ignore disk_schema = data # type: ignore
return MachineDetails(machine=machine, hw_config=hw_config, disk_schema=disk_schema) return MachineDetails(
machine=machine_inv, hw_config=hw_config, disk_schema=disk_schema
)
def list_nixos_machines(flake_url: str | Path) -> list[str]: def list_nixos_machines(flake_url: str | Path) -> list[str]:

View File

@@ -16,7 +16,7 @@
# (subdir / ".clan-flake").touch() # (subdir / ".clan-flake").touch()
# assert _get_clan_flake_toplevel() == subdir # assert _get_clan_flake_toplevel() == subdir
from clan_cli.dirs import clan_key_safe, vm_state_dir from clan_cli.clan_dirs import clan_key_safe, vm_state_dir
def test_clan_key_safe() -> None: def test_clan_key_safe() -> None:

View File

@@ -1,4 +1,5 @@
import pytest import pytest
from clan_cli.flake import Flake
from clan_cli.inventory import load_inventory_json from clan_cli.inventory import load_inventory_json
from clan_cli.secrets.folders import sops_machines_folder from clan_cli.secrets.folders import sops_machines_folder
from clan_cli.tests import fixtures_flakes from clan_cli.tests import fixtures_flakes
@@ -24,7 +25,7 @@ def test_machine_subcommands(
] ]
) )
inventory: dict = dict(load_inventory_json(str(test_flake_with_core.path))) inventory: dict = dict(load_inventory_json(Flake(str(test_flake_with_core.path))))
assert "machine1" in inventory["machines"] assert "machine1" in inventory["machines"]
assert "service" not in inventory assert "service" not in inventory
@@ -40,7 +41,7 @@ def test_machine_subcommands(
["machines", "delete", "--flake", str(test_flake_with_core.path), "machine1"] ["machines", "delete", "--flake", str(test_flake_with_core.path), "machine1"]
) )
inventory_2: dict = dict(load_inventory_json(str(test_flake_with_core.path))) inventory_2: dict = dict(load_inventory_json(Flake(str(test_flake_with_core.path))))
assert "machine1" not in inventory_2["machines"] assert "machine1" not in inventory_2["machines"]
assert "service" not in inventory_2 assert "service" not in inventory_2

View File

@@ -88,7 +88,7 @@ def test_add_module_to_inventory(
} }
} }
set_inventory(inventory, base_path, "Add borgbackup service") set_inventory(inventory, Flake(str(base_path)), "Add borgbackup service")
# cmd = ["facts", "generate", "--flake", str(test_flake_with_core.path), "machine1"] # cmd = ["facts", "generate", "--flake", str(test_flake_with_core.path), "machine1"]
cmd = [ cmd = [

View File

@@ -3,7 +3,7 @@ import shutil
from collections.abc import Iterable from collections.abc import Iterable
from pathlib import Path from pathlib import Path
from clan_cli.dirs import vm_state_dir from clan_cli.clan_dirs import vm_state_dir
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from clan_cli.ssh.host import Host from clan_cli.ssh.host import Host

View File

@@ -2,7 +2,7 @@ import shutil
from collections.abc import Iterable from collections.abc import Iterable
from pathlib import Path from pathlib import Path
from clan_cli.dirs import vm_state_dir from clan_cli.clan_dirs import vm_state_dir
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from clan_cli.ssh.host import Host from clan_cli.ssh.host import Host
from clan_cli.vars._types import StoreBase from clan_cli.vars._types import StoreBase

View File

@@ -12,9 +12,10 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from clan_cli.clan_dirs import vm_state_dir
from clan_cli.cmd import CmdOut, Log, RunOpts, handle_io, run from clan_cli.cmd import CmdOut, Log, RunOpts, handle_io, run
from clan_cli.completions import add_dynamic_completer, complete_machines from clan_cli.completions import add_dynamic_completer, complete_machines
from clan_cli.dirs import module_root, user_cache_dir, vm_state_dir from clan_cli.dirs import module_root, user_cache_dir
from clan_cli.errors import ClanCmdError, ClanError from clan_cli.errors import ClanCmdError, ClanError
from clan_cli.facts.generate import generate_facts from clan_cli.facts.generate import generate_facts
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine

View File

@@ -2,12 +2,12 @@ import json
import logging import logging
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path
from typing import Any, TypedDict from typing import Any, TypedDict
from uuid import uuid4 from uuid import uuid4
from clan_cli.dirs import TemplateType, clan_templates from clan_cli.dirs import TemplateType, clan_templates
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.flake import Flake
from clan_cli.git import commit_file from clan_cli.git import commit_file
from clan_cli.machines.hardware import HardwareConfig, show_machine_hardware_config from clan_cli.machines.hardware import HardwareConfig, show_machine_hardware_config
@@ -75,7 +75,7 @@ templates: dict[str, dict[str, Callable[[dict[str, Any]], Placeholder]]] = {
@API.register @API.register
def get_disk_schemas( def get_disk_schemas(
base_path: Path, machine_name: str | None = None flake: Flake, machine_name: str | None = None
) -> dict[str, DiskSchema]: ) -> dict[str, DiskSchema]:
""" """
Get the available disk schemas Get the available disk schemas
@@ -85,9 +85,7 @@ def get_disk_schemas(
hw_report = {} hw_report = {}
if machine_name is not None: if machine_name is not None:
hw_report_path = HardwareConfig.NIXOS_FACTER.config_path( hw_report_path = HardwareConfig.NIXOS_FACTER.config_path(flake, machine_name)
base_path, machine_name
)
if not hw_report_path.exists(): if not hw_report_path.exists():
msg = "Hardware configuration missing" msg = "Hardware configuration missing"
raise ClanError(msg) raise ClanError(msg)
@@ -132,7 +130,7 @@ class MachineDiskMatter(TypedDict):
@API.register @API.register
def set_machine_disk_schema( def set_machine_disk_schema(
base_path: Path, flake: Flake,
machine_name: str, machine_name: str,
schema_name: str, schema_name: str,
# Placeholders are used to fill in the disk schema # Placeholders are used to fill in the disk schema
@@ -144,8 +142,8 @@ def set_machine_disk_schema(
Set the disk placeholders of the template Set the disk placeholders of the template
""" """
# Assert the hw-config must exist before setting the disk # Assert the hw-config must exist before setting the disk
hw_config = show_machine_hardware_config(base_path, machine_name) hw_config = show_machine_hardware_config(flake, machine_name)
hw_config_path = hw_config.config_path(base_path, machine_name) hw_config_path = hw_config.config_path(flake, machine_name)
if not hw_config_path.exists(): if not hw_config_path.exists():
msg = "Hardware configuration must exist before applying disk schema" msg = "Hardware configuration must exist before applying disk schema"
@@ -162,7 +160,7 @@ def set_machine_disk_schema(
raise ClanError(msg) raise ClanError(msg)
# Check that the placeholders are valid # Check that the placeholders are valid
disk_schema = get_disk_schemas(base_path, machine_name)[schema_name] disk_schema = get_disk_schemas(flake, machine_name)[schema_name]
# check that all required placeholders are present # check that all required placeholders are present
for placeholder_name, schema_placeholder in disk_schema.placeholders.items(): for placeholder_name, schema_placeholder in disk_schema.placeholders.items():
if schema_placeholder.required and placeholder_name not in placeholders: if schema_placeholder.required and placeholder_name not in placeholders:
@@ -223,6 +221,6 @@ def set_machine_disk_schema(
commit_file( commit_file(
disko_file_path, disko_file_path,
base_path, flake.path,
commit_message=f"Set disk schema of machine: {machine_name} to {schema_name}", commit_message=f"Set disk schema of machine: {machine_name} to {schema_name}",
) )

View File

@@ -8,8 +8,8 @@ from typing import Any
import clan_cli.clan.create import clan_cli.clan.create
import pytest import pytest
from clan_cli.clan_dirs import specific_machine_dir
from clan_cli.cmd import RunOpts, run from clan_cli.cmd import RunOpts, run
from clan_cli.dirs import specific_machine_dir
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.flake import Flake from clan_cli.flake import Flake
from clan_cli.inventory import patch_inventory_with from clan_cli.inventory import patch_inventory_with
@@ -209,7 +209,7 @@ def test_clan_create_api(
# ===== CREATE BASE INVENTORY ====== # ===== CREATE BASE INVENTORY ======
inventory = create_base_inventory(ssh_keys) inventory = create_base_inventory(ssh_keys)
patch_inventory_with(dest_clan_dir, "services", inventory.services) patch_inventory_with(Flake(str(dest_clan_dir)), "services", inventory.services)
# Invalidate cache because of new inventory # Invalidate cache because of new inventory
clan_dir_flake.invalidate_cache() clan_dir_flake.invalidate_cache()
@@ -240,7 +240,7 @@ def test_clan_create_api(
facter_json = test_lib_root / "assets" / "facter.json" facter_json = test_lib_root / "assets" / "facter.json"
assert facter_json.exists(), f"Source facter file not found: {facter_json}" assert facter_json.exists(), f"Source facter file not found: {facter_json}"
dest_dir = specific_machine_dir(clan_dir_flake.path, machine.name) dest_dir = specific_machine_dir(Flake(str(clan_dir_flake.path)), machine.name)
# specific_machine_dir should create the directory, but ensure it exists just in case # specific_machine_dir should create the directory, but ensure it exists just in case
dest_dir.mkdir(parents=True, exist_ok=True) dest_dir.mkdir(parents=True, exist_ok=True)
@@ -254,7 +254,8 @@ def test_clan_create_api(
# ===== Create Disko Config ====== # ===== Create Disko Config ======
facter_path = ( facter_path = (
specific_machine_dir(clan_dir_flake.path, machine.name) / "facter.json" specific_machine_dir(Flake(str(clan_dir_flake.path)), machine.name)
/ "facter.json"
) )
with facter_path.open("r") as f: with facter_path.open("r") as f:
facter_report = json.load(f) facter_report = json.load(f)
@@ -264,9 +265,7 @@ def test_clan_create_api(
assert disk_devs is not None assert disk_devs is not None
placeholders = {"mainDisk": disk_devs[0]} placeholders = {"mainDisk": disk_devs[0]}
set_machine_disk_schema( set_machine_disk_schema(clan_dir_flake, machine.name, "single-disk", placeholders)
clan_dir_flake.path, machine.name, "single-disk", placeholders
)
clan_dir_flake.invalidate_cache() clan_dir_flake.invalidate_cache()
with pytest.raises(ClanError) as exc_info: with pytest.raises(ClanError) as exc_info:

View File

@@ -13,7 +13,7 @@ from typing import IO, ClassVar
import gi import gi
from clan_cli import vms from clan_cli import vms
from clan_cli.dirs import vm_state_dir from clan_cli.clan_dirs import vm_state_dir
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from clan_cli.vms.inspect import inspect_vm from clan_cli.vms.inspect import inspect_vm
from clan_cli.vms.qemu import QMPWrapper from clan_cli.vms.qemu import QMPWrapper

View File

@@ -13,9 +13,9 @@ export async function get_inventory(client: QueryClient, base_path: string) {
queryKey: [base_path, "inventory"], queryKey: [base_path, "inventory"],
queryFn: () => { queryFn: () => {
console.log("Refreshing inventory"); console.log("Refreshing inventory");
return callApi("get_inventory", { base_path }) as Promise< return callApi("get_inventory", {
ApiEnvelope<Inventory> flake: { identifier: base_path },
>; }) as Promise<ApiEnvelope<Inventory>>;
}, },
revalidateIfStale: true, revalidateIfStale: true,
staleTime: 60 * 1000, staleTime: 60 * 1000,

View File

@@ -6,7 +6,7 @@ export const instance_name = (machine_name: string) =>
export async function get_iwd_service(base_path: string, machine_name: string) { export async function get_iwd_service(base_path: string, machine_name: string) {
const r = await callApi("get_inventory", { const r = await callApi("get_inventory", {
base_path, flake: { identifier: base_path },
}); });
if (r.status == "error") { if (r.status == "error") {
return null; return null;

View File

@@ -43,7 +43,7 @@ export const tagsQuery = (uri: string | null) =>
if (!uri) return []; if (!uri) return [];
const response = await callApi("get_inventory", { const response = await callApi("get_inventory", {
base_path: uri, flake: { identifier: uri },
}); });
if (response.status === "error") { if (response.status === "error") {
toast.error("Failed to fetch data"); toast.error("Failed to fetch data");
@@ -64,7 +64,7 @@ export const machinesQuery = (uri: string | null) =>
if (!uri) return []; if (!uri) return [];
const response = await callApi("get_inventory", { const response = await callApi("get_inventory", {
base_path: uri, flake: { identifier: uri },
}); });
if (response.status === "error") { if (response.status === "error") {
toast.error("Failed to fetch data"); toast.error("Failed to fetch data");

View File

@@ -42,7 +42,7 @@ const EditClanForm = (props: EditClanFormProps) => {
(async () => { (async () => {
await callApi("update_clan_meta", { await callApi("update_clan_meta", {
options: { options: {
directory: props.directory, flake: { identifier: props.directory },
meta: values, meta: values,
}, },
}); });

View File

@@ -10,7 +10,9 @@ export function DiskView() {
const currUri = activeURI(); const currUri = activeURI();
if (currUri) { if (currUri) {
// Example of calling an API // Example of calling an API
const result = await callApi("get_inventory", { base_path: currUri }); const result = await callApi("get_inventory", {
flake: { identifier: currUri },
});
if (result.status === "error") throw new Error("Failed to fetch data"); if (result.status === "error") throw new Error("Failed to fetch data");
return result.data; return result.data;
} }

View File

@@ -115,7 +115,7 @@ const InstallMachine = (props: InstallMachineProps) => {
if (shouldRunDisk) { if (shouldRunDisk) {
setProgressText("Setting up disk ... (1/5)"); setProgressText("Setting up disk ... (1/5)");
const disk_response = await callApi("set_machine_disk_schema", { const disk_response = await callApi("set_machine_disk_schema", {
base_path: curr_uri, flake: { identifier: curr_uri },
machine_name: props.name, machine_name: props.name,
placeholders: diskValues.placeholders, placeholders: diskValues.placeholders,
schema_name: diskValues.schema, schema_name: diskValues.schema,
@@ -415,7 +415,9 @@ const MachineForm = (props: MachineDetailsProps) => {
} }
const machine_response = await callApi("set_machine", { const machine_response = await callApi("set_machine", {
flake_url: curr_uri, flake: {
identifier: curr_uri,
},
machine_name: props.initialData.machine.name || "My machine", machine_name: props.initialData.machine.name || "My machine",
machine: { machine: {
...values.machine, ...values.machine,
@@ -680,8 +682,12 @@ export const MachineDetails = () => {
const curr = activeURI(); const curr = activeURI();
if (curr) { if (curr) {
const result = await callApi("get_machine_details", { const result = await callApi("get_machine_details", {
flake_url: curr, machine: {
machine_name: params.id, flake: {
identifier: curr,
},
name: params.id,
},
}); });
if (result.status === "error") throw new Error("Failed to fetch data"); if (result.status === "error") throw new Error("Failed to fetch data");
return result.data; return result.data;

View File

@@ -37,7 +37,9 @@ export const DiskStep = (props: StepProps<DiskValues>) => {
queryKey: [props.dir, props.machine_id, "disk_schemas"], queryKey: [props.dir, props.machine_id, "disk_schemas"],
queryFn: async () => { queryFn: async () => {
const result = await callApi("get_disk_schemas", { const result = await callApi("get_disk_schemas", {
base_path: props.dir, flake: {
identifier: props.dir,
},
machine_name: props.machine_id, machine_name: props.machine_id,
}); });
if (result.status === "error") throw new Error("Failed to fetch data"); if (result.status === "error") throw new Error("Failed to fetch data");

View File

@@ -52,7 +52,9 @@ export const HWStep = (props: StepProps<HardwareValues>) => {
queryKey: [props.dir, props.machine_id, "hw_report"], queryKey: [props.dir, props.machine_id, "hw_report"],
queryFn: async () => { queryFn: async () => {
const result = await callApi("show_machine_hardware_config", { const result = await callApi("show_machine_hardware_config", {
clan_dir: props.dir, flake: {
identifier: props.dir,
},
machine_name: props.machine_id, machine_name: props.machine_id,
}); });
if (result.status === "error") throw new Error("Failed to fetch data"); if (result.status === "error") throw new Error("Failed to fetch data");

View File

@@ -39,7 +39,9 @@ export const MachineListView: Component = () => {
const uri = activeURI(); const uri = activeURI();
if (uri) { if (uri) {
const response = await callApi("list_machines", { const response = await callApi("list_machines", {
flake_url: uri, flake: {
identifier: uri,
},
}); });
if (response.status === "error") { if (response.status === "error") {
toast.error("Failed to fetch data"); toast.error("Failed to fetch data");