diff --git a/pkgs/clan-app/ui/src/components/machine-list-item/index.tsx b/pkgs/clan-app/ui/src/components/machine-list-item/index.tsx index c523285f3..2cf8dc85e 100644 --- a/pkgs/clan-app/ui/src/components/machine-list-item/index.tsx +++ b/pkgs/clan-app/ui/src/components/machine-list-item/index.tsx @@ -9,7 +9,7 @@ import { Typography } from "../Typography"; import "./css/index.css"; import { useClanContext } from "@/src/contexts/clan"; -type MachineDetails = SuccessQuery<"list_inv_machines">["data"][string]; +type MachineDetails = SuccessQuery<"list_machines">["data"][string]; interface MachineListItemProps { name: string; diff --git a/pkgs/clan-app/ui/src/routes/machines/create.tsx b/pkgs/clan-app/ui/src/routes/machines/create.tsx index 5eaa6ae06..54032f368 100644 --- a/pkgs/clan-app/ui/src/routes/machines/create.tsx +++ b/pkgs/clan-app/ui/src/routes/machines/create.tsx @@ -63,7 +63,7 @@ export function CreateMachine() { reset(formStore); await queryClient.invalidateQueries({ - queryKey: [active_dir, "list_inv_machines"], + queryKey: [active_dir, "list_machines"], }); navigate("/machines"); diff --git a/pkgs/clan-app/ui/src/routes/machines/details.tsx b/pkgs/clan-app/ui/src/routes/machines/details.tsx index 97bc7f217..60aa78dfe 100644 --- a/pkgs/clan-app/ui/src/routes/machines/details.tsx +++ b/pkgs/clan-app/ui/src/routes/machines/details.tsx @@ -411,7 +411,7 @@ const MachineForm = (props: MachineDetailsProps) => { return; } - const machine_response = await callApi("update_machine", { + const machine_response = await callApi("set_machine", { machine: { name: props.initialData.machine.name || "My machine", flake: { diff --git a/pkgs/clan-app/ui/src/routes/machines/list.tsx b/pkgs/clan-app/ui/src/routes/machines/list.tsx index c792a0110..6a25aac4b 100644 --- a/pkgs/clan-app/ui/src/routes/machines/list.tsx +++ b/pkgs/clan-app/ui/src/routes/machines/list.tsx @@ -10,7 +10,7 @@ import { makePersisted } from "@solid-primitives/storage"; import { useClanContext } from "@/src/contexts/clan"; type MachinesModel = Extract< - OperationResponse<"list_inv_machines">, + OperationResponse<"list_machines">, { status: "success" } >["data"]; @@ -25,14 +25,14 @@ export const MachineListView: Component = () => { const { activeClanURI } = useClanContext(); const inventoryQuery = useQuery(() => ({ - queryKey: [activeClanURI(), "list_inv_machines"], + queryKey: [activeClanURI(), "list_machines"], placeholderData: {}, enabled: !!activeClanURI(), queryFn: async () => { console.log("fetching inventory", activeClanURI()); const uri = activeClanURI(); if (uri) { - const response = await callApi("list_inv_machines", { + const response = await callApi("list_machines", { flake: { identifier: uri, }, @@ -60,7 +60,7 @@ export const MachineListView: Component = () => { await queryClient.invalidateQueries({ // Invalidates the cache for of all types of machine list at once - queryKey: [clanURI, "list_inv_machines"], + queryKey: [clanURI, "list_machines"], }); }; diff --git a/pkgs/clan-cli/clan_cli/__init__.py b/pkgs/clan-cli/clan_cli/__init__.py index f70401827..56104f2d4 100644 --- a/pkgs/clan-cli/clan_cli/__init__.py +++ b/pkgs/clan-cli/clan_cli/__init__.py @@ -5,15 +5,6 @@ import sys from pathlib import Path from types import ModuleType -# These imports are unused, but necessary for @API.register to run once. -from clan_lib.api import directory, disk, mdns_discovery, modules - -from .arg_actions import AppendOptionAction -from .clan import show, update - -# API endpoints that are not used in the cli. -__all__ = ["directory", "disk", "mdns_discovery", "modules", "update"] - from clan_lib.custom_logger import setup_logging from clan_lib.dirs import get_clan_flake_toplevel_or_env from clan_lib.errors import ClanError @@ -27,6 +18,8 @@ from . import ( state, vms, ) +from .arg_actions import AppendOptionAction +from .clan import show from .facts import cli as facts from .flash import cli as flash_cli from .hyperlink import help_hyperlink diff --git a/pkgs/clan-cli/clan_cli/clan/inspect.py b/pkgs/clan-cli/clan_cli/clan/inspect.py index 51ffffb19..89ffc13b3 100644 --- a/pkgs/clan-cli/clan_cli/clan/inspect.py +++ b/pkgs/clan-cli/clan_cli/clan/inspect.py @@ -16,7 +16,7 @@ from clan_lib.nix import ( nix_metadata, ) -from clan_cli.machines.list import list_machines +from clan_cli.machines.list import list_full_machines from clan_cli.vms.inspect import VmConfig, inspect_vm @@ -58,7 +58,7 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig: system = config["system"] # Check if the machine exists - machines: dict[str, Machine] = list_machines(Flake(str(flake_url))) + machines: dict[str, Machine] = list_full_machines(Flake(str(flake_url))) if machine_name not in machines: msg = f"Machine {machine_name} not found in {flake_url}. Available machines: {', '.join(machines)}" raise ClanError(msg) diff --git a/pkgs/clan-cli/clan_cli/clan/show.py b/pkgs/clan-cli/clan_cli/clan/show.py index f182d396b..0af6b599e 100644 --- a/pkgs/clan-cli/clan_cli/clan/show.py +++ b/pkgs/clan-cli/clan_cli/clan/show.py @@ -1,80 +1,11 @@ import argparse -import json import logging -from pathlib import Path -from urllib.parse import urlparse -from clan_lib.api import API -from clan_lib.cmd import run -from clan_lib.errors import ClanCmdError, ClanError -from clan_lib.flake import Flake -from clan_lib.nix import nix_eval -from clan_lib.nix_models.clan import InventoryMeta as Meta +from clan_lib.clan.get import show_clan_meta log = logging.getLogger(__name__) -@API.register -def show_clan_meta(flake: Flake) -> Meta: - if flake.is_local and not flake.path.exists(): - msg = f"Path {flake} does not exist" - raise ClanError(msg, description="clan directory does not exist") - cmd = nix_eval( - [ - f"{flake}#clanInternals.inventory.meta", - "--json", - ] - ) - res = "{}" - - try: - proc = run(cmd) - res = proc.stdout.strip() - except ClanCmdError as e: - msg = "Evaluation failed on meta attribute" - raise ClanError( - msg, - location=f"show_clan {flake}", - description=str(e.cmd), - ) from e - - clan_meta = json.loads(res) - - # Check if icon is a URL such as http:// or https:// - # Check if icon is an relative path - # All other schemas such as file://, ftp:// are not yet supported. - icon_path: str | None = None - if meta_icon := clan_meta.get("icon"): - scheme = urlparse(meta_icon).scheme - if scheme in ["http", "https"]: - icon_path = meta_icon - elif scheme in [""]: - if Path(meta_icon).is_absolute(): - msg = "Invalid absolute path" - raise ClanError( - msg, - location=f"show_clan {flake}", - description="Icon path must be a URL or a relative path", - ) - - icon_path = str((flake.path / meta_icon).resolve()) - else: - msg = "Invalid schema" - raise ClanError( - msg, - location=f"show_clan {flake}", - description="Icon path must be a URL or a relative path", - ) - - return Meta( - { - "name": clan_meta.get("name"), - "description": clan_meta.get("description"), - "icon": icon_path if icon_path else "", - } - ) - - def show_command(args: argparse.Namespace) -> None: flake_path = args.flake.path meta = show_clan_meta(flake_path) diff --git a/pkgs/clan-cli/clan_cli/facts/generate.py b/pkgs/clan-cli/clan_cli/facts/generate.py index b1eb038d2..4751fe56a 100644 --- a/pkgs/clan-cli/clan_cli/facts/generate.py +++ b/pkgs/clan-cli/clan_cli/facts/generate.py @@ -18,7 +18,7 @@ from clan_cli.completions import ( complete_machines, complete_services_for_machine, ) -from clan_cli.machines.list import list_machines +from clan_cli.machines.list import list_full_machines from .check import check_secrets from .public_modules import FactStoreBase @@ -227,7 +227,7 @@ def generate_command(args: argparse.Namespace) -> None: msg = "Could not find clan flake toplevel directory" raise ClanError(msg) - machines: list[Machine] = list(list_machines(args.flake).values()) + machines: list[Machine] = list(list_full_machines(args.flake).values()) if len(args.machines) > 0: machines = list( filter( diff --git a/pkgs/clan-cli/clan_cli/machines/create.py b/pkgs/clan-cli/clan_cli/machines/create.py index 9aa8359f8..3155b2d6a 100644 --- a/pkgs/clan-cli/clan_cli/machines/create.py +++ b/pkgs/clan-cli/clan_cli/machines/create.py @@ -21,7 +21,7 @@ from clan_lib.templates import ( ) from clan_cli.completions import add_dynamic_completer, complete_tags -from clan_cli.machines.list import list_machines +from clan_cli.machines.list import list_full_machines log = logging.getLogger(__name__) @@ -71,7 +71,7 @@ def create_machine( log.info(f"Found template '{template.name}' in '{template.input_variant}'") machine_name = opts.machine.get("name") - if opts.template_name in list_machines( + if opts.template_name in list_full_machines( Flake(str(clan_dir)) ) and not opts.machine.get("name"): msg = f"{opts.template_name} is already defined in {clan_dir}" diff --git a/pkgs/clan-cli/clan_cli/machines/list.py b/pkgs/clan-cli/clan_cli/machines/list.py index 7c26c1b80..582dd6f0d 100644 --- a/pkgs/clan-cli/clan_cli/machines/list.py +++ b/pkgs/clan-cli/clan_cli/machines/list.py @@ -9,10 +9,9 @@ from clan_lib.api.modules import parse_frontmatter from clan_lib.dirs import specific_machine_dir from clan_lib.errors import ClanError from clan_lib.flake import Flake -from clan_lib.machines.actions import get_machine +from clan_lib.machines.actions import get_machine, list_machines from clan_lib.machines.machines import Machine from clan_lib.nix_models.clan import InventoryMachine -from clan_lib.persist.inventory_store import InventoryStore from clan_cli.completions import add_dynamic_completer, complete_tags from clan_cli.machines.hardware import HardwareConfig @@ -20,29 +19,20 @@ from clan_cli.machines.hardware import HardwareConfig log = logging.getLogger(__name__) -@API.register -def list_inv_machines(flake: Flake) -> dict[str, InventoryMachine]: - """ - List machines in the inventory for the UI. - """ - inventory_store = InventoryStore(flake=flake) - inventory = inventory_store.read() - - res = inventory.get("machines", {}) - return res - - -def list_machines( +def list_full_machines( flake: Flake, nix_options: list[str] | None = None ) -> dict[str, Machine]: - inventory_store = InventoryStore(flake=flake) - inventory = inventory_store.read() - res = {} + """ + Like `list_machines`, but returns a full 'machine' instance for each machine. + """ + machines = list_machines(flake) + + res: dict[str, Machine] = {} if nix_options is None: nix_options = [] - for inv_machine in inventory.get("machines", {}).values(): + for inv_machine in machines.values(): name = inv_machine.get("name") # Technically, this should not happen, but we are defensive here. if name is None: @@ -65,7 +55,7 @@ def query_machines_by_tags(flake: Flake, tags: list[str]) -> dict[str, Machine]: then only machines that have those respective tags specified will be listed. It is an intersection of the tags and machines. """ - machines = list_machines(flake) + machines = list_full_machines(flake) filtered_machines = {} for machine in machines.values(): @@ -125,7 +115,7 @@ def list_command(args: argparse.Namespace) -> None: for name in query_machines_by_tags(flake, args.tags): print(name) else: - for name in list_machines(flake): + for name in list_full_machines(flake): print(name) diff --git a/pkgs/clan-cli/clan_cli/machines/update.py b/pkgs/clan-cli/clan_cli/machines/update.py index 06b341f37..baa0a34e4 100644 --- a/pkgs/clan-cli/clan_cli/machines/update.py +++ b/pkgs/clan-cli/clan_cli/machines/update.py @@ -22,7 +22,7 @@ from clan_cli.completions import ( ) from clan_cli.facts.generate import generate_facts from clan_cli.facts.upload import upload_secrets -from clan_cli.machines.list import list_machines +from clan_cli.machines.list import list_full_machines from clan_cli.vars.generate import generate_vars from clan_cli.vars.upload import upload_secret_vars @@ -225,7 +225,7 @@ def update_command(args: argparse.Namespace) -> None: machines: list[Machine] = [] # if no machines are passed, we will update all machines selected_machines = ( - args.machines if args.machines else list_machines(args.flake).keys() + args.machines if args.machines else list_full_machines(args.flake).keys() ) if args.target_host is not None and len(args.machines) > 1: diff --git a/pkgs/clan-cli/clan_cli/vars/generate.py b/pkgs/clan-cli/clan_cli/vars/generate.py index 1356f42a7..efc2a59dc 100644 --- a/pkgs/clan-cli/clan_cli/vars/generate.py +++ b/pkgs/clan-cli/clan_cli/vars/generate.py @@ -14,7 +14,7 @@ from clan_cli.completions import ( complete_machines, complete_services_for_machine, ) -from clan_cli.machines.list import list_machines +from clan_cli.machines.list import list_full_machines from clan_cli.vars._types import StoreBase from clan_cli.vars.migration import check_can_migrate, migrate_files from clan_lib.api import API @@ -511,7 +511,7 @@ def generate_command(args: argparse.Namespace) -> None: msg = "Could not find clan flake toplevel directory" raise ClanError(msg) - machines: list[Machine] = list(list_machines(args.flake, args.option).values()) + machines: list[Machine] = list(list_full_machines(args.flake, args.option).values()) if len(args.machines) > 0: machines = list( diff --git a/pkgs/clan-cli/clan_lib/clan/get.py b/pkgs/clan-cli/clan_lib/clan/get.py new file mode 100644 index 000000000..9f56c124d --- /dev/null +++ b/pkgs/clan-cli/clan_lib/clan/get.py @@ -0,0 +1,71 @@ +import json +from pathlib import Path +from urllib.parse import urlparse + +from clan_lib.api import API +from clan_lib.cmd import run +from clan_lib.errors import ClanCmdError, ClanError +from clan_lib.flake import Flake +from clan_lib.nix import nix_eval +from clan_lib.nix_models.clan import InventoryMeta as Meta + + +@API.register +def show_clan_meta(flake: Flake) -> Meta: + if flake.is_local and not flake.path.exists(): + msg = f"Path {flake} does not exist" + raise ClanError(msg, description="clan directory does not exist") + cmd = nix_eval( + [ + f"{flake}#clanInternals.inventory.meta", + "--json", + ] + ) + res = "{}" + + try: + proc = run(cmd) + res = proc.stdout.strip() + except ClanCmdError as e: + msg = "Evaluation failed on meta attribute" + raise ClanError( + msg, + location=f"show_clan {flake}", + description=str(e.cmd), + ) from e + + clan_meta = json.loads(res) + + # Check if icon is a URL such as http:// or https:// + # Check if icon is an relative path + # All other schemas such as file://, ftp:// are not yet supported. + icon_path: str | None = None + if meta_icon := clan_meta.get("icon"): + scheme = urlparse(meta_icon).scheme + if scheme in ["http", "https"]: + icon_path = meta_icon + elif scheme in [""]: + if Path(meta_icon).is_absolute(): + msg = "Invalid absolute path" + raise ClanError( + msg, + location=f"show_clan {flake}", + description="Icon path must be a URL or a relative path", + ) + + icon_path = str((flake.path / meta_icon).resolve()) + else: + msg = "Invalid schema" + raise ClanError( + msg, + location=f"show_clan {flake}", + description="Icon path must be a URL or a relative path", + ) + + return Meta( + { + "name": clan_meta.get("name"), + "description": clan_meta.get("description"), + "icon": icon_path if icon_path else "", + } + ) diff --git a/pkgs/clan-cli/clan_cli/clan/update.py b/pkgs/clan-cli/clan_lib/clan/update.py similarity index 100% rename from pkgs/clan-cli/clan_cli/clan/update.py rename to pkgs/clan-cli/clan_lib/clan/update.py diff --git a/pkgs/clan-cli/clan_lib/machines/actions.py b/pkgs/clan-cli/clan_lib/machines/actions.py index dc00dba7e..d9879dcfb 100644 --- a/pkgs/clan-cli/clan_lib/machines/actions.py +++ b/pkgs/clan-cli/clan_lib/machines/actions.py @@ -1,7 +1,8 @@ +from dataclasses import dataclass + from clan_lib.api import API from clan_lib.errors import ClanError from clan_lib.flake.flake import Flake -from clan_lib.machines.machines import Machine from clan_lib.nix_models.clan import ( InventoryMachine, ) @@ -9,6 +10,18 @@ from clan_lib.persist.inventory_store import InventoryStore from clan_lib.persist.util import set_value_by_path +@API.register +def list_machines(flake: Flake) -> dict[str, InventoryMachine]: + """ + List machines in the inventory for the UI. + """ + inventory_store = InventoryStore(flake=flake) + inventory = inventory_store.read() + + machines = inventory.get("machines", {}) + return machines + + @API.register def get_machine(flake: Flake, name: str) -> InventoryMachine: inventory_store = InventoryStore(flake=flake) @@ -22,9 +35,21 @@ def get_machine(flake: Flake, name: str) -> InventoryMachine: return InventoryMachine(**machine_inv) +# TODO: remove this machine, once the Machine class is refactored +# We added this now, to allow for dispatching actions. To require only 'name' and 'flake' of a machine. +@dataclass(frozen=True) +class MachineID: + name: str + flake: Flake + + @API.register -def update_machine(machine: Machine, update: InventoryMachine) -> None: +def set_machine(machine: MachineID, update: InventoryMachine) -> None: + """ + Update the machine information in the inventory. + """ assert machine.name == update.get("name", machine.name), "Machine name mismatch" + inventory_store = InventoryStore(flake=machine.flake) inventory = inventory_store.read()