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:
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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],
|
|
||||||
# )
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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"}}}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user