Merge pull request 'refactor(list/machines): use InventoryStore to interact with data"' (#3645) from hsjobeki/clan-core:persistence-1 into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3645
This commit is contained in:
hsjobeki
2025-05-14 15:03:48 +00:00
7 changed files with 37 additions and 88 deletions

View File

@@ -6,13 +6,13 @@ from pathlib import Path
from types import ModuleType from types import ModuleType
# These imports are unused, but necessary for @API.register to run once. # These imports are unused, but necessary for @API.register to run once.
from clan_lib.api import directory, disk, iwd, mdns_discovery, modules from clan_lib.api import directory, disk, mdns_discovery, modules
from .arg_actions import AppendOptionAction from .arg_actions import AppendOptionAction
from .clan import show, update from .clan import show, update
# API endpoints that are not used in the cli. # API endpoints that are not used in the cli.
__all__ = ["directory", "disk", "iwd", "mdns_discovery", "modules", "update"] __all__ = ["directory", "disk", "mdns_discovery", "modules", "update"]
from . import ( from . import (
backups, backups,

View File

@@ -19,10 +19,10 @@ from clan_lib.api import API
from clan_lib.nix_models.inventory import Inventory from clan_lib.nix_models.inventory import Inventory
from clan_lib.persist.inventory_store import WriteInfo from clan_lib.persist.inventory_store import WriteInfo
from clan_lib.persist.util import ( from clan_lib.persist.util import (
apply_patch,
calc_patches, calc_patches,
delete_by_path, delete_by_path,
determine_writeability, determine_writeability,
patch,
) )
from clan_cli.cmd import run from clan_cli.cmd import run
@@ -150,7 +150,7 @@ def patch_inventory_with(flake: Flake, section: str, content: dict[str, Any]) ->
with inventory_file.open("r") as f: with inventory_file.open("r") as f:
curr_inventory = json.load(f) curr_inventory = json.load(f)
patch(curr_inventory, section, content) apply_patch(curr_inventory, section, content)
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)
@@ -206,7 +206,7 @@ def set_inventory(
persisted = dict(write_info.data_disk) persisted = dict(write_info.data_disk)
for patch_path, data in patchset.items(): for patch_path, data in patchset.items():
patch(persisted, patch_path, data) apply_patch(persisted, patch_path, data)
for delete_path in delete_set: for delete_path in delete_set:
delete_by_path(persisted, delete_path) delete_by_path(persisted, delete_path)

View File

@@ -10,18 +10,15 @@ from typing import Literal
from clan_lib.api import API from clan_lib.api import API
from clan_lib.api.disk import MachineDiskMatter 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.nix_models.inventory import Machine as InventoryMachine from clan_lib.nix_models.inventory import Machine as InventoryMachine
from clan_lib.persist.inventory_store import InventoryStore
from clan_lib.persist.util import apply_patch
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.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 (
load_inventory_eval,
patch_inventory_with,
)
from clan_cli.machines.hardware import HardwareConfig from clan_cli.machines.hardware import HardwareConfig
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from clan_cli.nix import nix_eval from clan_cli.nix import nix_eval
@@ -32,12 +29,18 @@ log = logging.getLogger(__name__)
@API.register @API.register
def set_machine(flake: Flake, machine_name: str, machine: InventoryMachine) -> None: def set_machine(flake: Flake, machine_name: str, machine: InventoryMachine) -> None:
patch_inventory_with(flake, f"machines.{machine_name}", dataclass_to_dict(machine)) inventory_store = InventoryStore(flake=flake)
inventory = inventory_store.read()
apply_patch(inventory, f"machines.{machine_name}", machine)
inventory_store.write(
inventory, message=f"Update information about machine {machine_name}"
)
@API.register @API.register
def list_machines(flake: Flake) -> dict[str, InventoryMachine]: def list_machines(flake: Flake) -> dict[str, InventoryMachine]:
inventory = load_inventory_eval(flake) inventory_store = InventoryStore(flake=flake)
inventory = inventory_store.read()
return inventory.get("machines", {}) return inventory.get("machines", {})
@@ -61,7 +64,8 @@ def extract_header(c: str) -> str:
@API.register @API.register
def get_machine_details(machine: Machine) -> MachineDetails: def get_machine_details(machine: Machine) -> MachineDetails:
inventory = load_inventory_eval(machine.flake) inventory_store = InventoryStore(flake=machine.flake)
inventory = inventory_store.read()
machine_inv = inventory.get("machines", {}).get(machine.name) machine_inv = inventory.get("machines", {}).get(machine.name)
if machine_inv 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"

View File

@@ -1,67 +0,0 @@
from dataclasses import dataclass
def instance_name(machine_name: str) -> str:
return f"{machine_name}_wifi_0_"
@dataclass
class NetworkConfig:
ssid: str
password: str
# @API.register
# def set_iwd_service_for_machine(
# base_url: str, machine_name: str, networks: dict[str, NetworkConfig]
# ) -> None:
# """
# Set the admin service of a clan
# Every machine is by default part of the admin service via the 'all' tag
# """
# _instance_name = instance_name(machine_name)
# inventory = load_inventory_eval(base_url)
# instance = ServiceIwd(
# meta=ServiceMeta(name="wifi_0"),
# roles=ServiceIwdRole(
# default=ServiceIwdRoleDefault(
# machines=[machine_name],
# )
# ),
# config=IwdConfig(
# networks={k: IwdConfigNetwork(v.ssid) for k, v in networks.items()}
# ),
# )
# inventory.services.iwd[_instance_name] = instance
# save_inventory(
# inventory,
# base_url,
# f"Set iwd service: '{_instance_name}'",
# )
# pubkey = maybe_get_public_key()
# if not pubkey:
# # TODO: do this automatically
# # pubkey = generate_key()
# raise ClanError(msg="No public key found. Please initialize your key.")
# registered_key = maybe_get_user_or_machine(Path(base_url), pubkey)
# if not registered_key:
# # TODO: do this automatically
# # username = os.getlogin()
# # add_user(Path(base_url), username, pubkey, force=False)
# raise ClanError(msg="Your public key is not registered for use with this clan.")
# password_dict = {f"iwd.{net.ssid}": net.password for net in networks.values()}
# for net in networks.values():
# generate_facts(
# service=f"iwd.{net.ssid}",
# machines=[Machine(machine_name, FlakeId(base_url))],
# regenerate=True,
# # Just returns the password
# prompt=lambda service, _msg: password_dict[service],
# )

View File

@@ -8,10 +8,10 @@ from clan_cli.git import commit_file
from clan_lib.nix_models.inventory import Inventory from clan_lib.nix_models.inventory import Inventory
from .util import ( from .util import (
apply_patch,
calc_patches, calc_patches,
delete_by_path, delete_by_path,
determine_writeability, determine_writeability,
patch,
) )
@@ -102,6 +102,15 @@ class InventoryStore:
return WriteInfo(writeables, data_eval, data_disk) return WriteInfo(writeables, data_eval, data_disk)
def read(self) -> Inventory:
"""
Accessor to the merged inventory
Side Effects:
Runs 'nix eval' through the '_flake' member of this class
"""
return self._load_merged_inventory()
def delete(self, delete_set: set[str]) -> None: def delete(self, delete_set: set[str]) -> None:
""" """
Delete keys from the inventory Delete keys from the inventory
@@ -144,7 +153,7 @@ class InventoryStore:
persisted = dict(write_info.data_disk) persisted = dict(write_info.data_disk)
for patch_path, data in patchset.items(): for patch_path, data in patchset.items():
patch(persisted, patch_path, data) apply_patch(persisted, patch_path, data)
for delete_path in delete_set: for delete_path in delete_set:
delete_by_path(persisted, delete_path) delete_by_path(persisted, delete_path)

View File

@@ -307,7 +307,10 @@ def delete_by_path(d: dict[str, Any], path: str) -> Any:
return {last_key: value} return {last_key: value}
def patch(d: dict[str, Any], path: str, content: Any) -> None: type DictLike = dict[str, Any] | Any
def apply_patch(d: DictLike, path: str, content: Any) -> None:
""" """
Update the value at a specific dot-separated path in a nested dictionary. Update the value at a specific dot-separated path in a nested dictionary.

View File

@@ -5,10 +5,10 @@ import pytest
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_lib.persist.util import ( from clan_lib.persist.util import (
apply_patch,
calc_patches, calc_patches,
delete_by_path, delete_by_path,
determine_writeability, determine_writeability,
patch,
unmerge_lists, unmerge_lists,
) )
@@ -17,7 +17,7 @@ from clan_lib.persist.util import (
def test_patch_nested() -> None: def test_patch_nested() -> None:
orig = {"a": 1, "b": {"a": 2.1, "b": 2.2}, "c": 3} orig = {"a": 1, "b": {"a": 2.1, "b": 2.2}, "c": 3}
patch(orig, "b.b", "foo") apply_patch(orig, "b.b", "foo")
# Should only update the nested value # Should only update the nested value
assert orig == {"a": 1, "b": {"a": 2.1, "b": "foo"}, "c": 3} assert orig == {"a": 1, "b": {"a": 2.1, "b": "foo"}, "c": 3}
@@ -28,7 +28,7 @@ def test_patch_nested_dict() -> None:
# This should update the whole "b" dict # This should update the whole "b" dict
# Which also removes all other keys # Which also removes all other keys
patch(orig, "b", {"b": "foo"}) apply_patch(orig, "b", {"b": "foo"})
# Should only update the nested value # Should only update the nested value
assert orig == {"a": 1, "b": {"b": "foo"}, "c": 3} assert orig == {"a": 1, "b": {"b": "foo"}, "c": 3}
@@ -37,13 +37,13 @@ def test_patch_nested_dict() -> None:
def test_create_missing_paths() -> None: def test_create_missing_paths() -> None:
orig = {"a": 1} orig = {"a": 1}
patch(orig, "b.c", "foo") apply_patch(orig, "b.c", "foo")
# Should only update the nested value # Should only update the nested value
assert orig == {"a": 1, "b": {"c": "foo"}} assert orig == {"a": 1, "b": {"c": "foo"}}
orig = {} orig = {}
patch(orig, "a.b.c", "foo") apply_patch(orig, "a.b.c", "foo")
assert orig == {"a": {"b": {"c": "foo"}}} assert orig == {"a": {"b": {"c": "foo"}}}