inventory.{cli,api}: use only dictionaries
This commit is contained in:
@@ -272,11 +272,14 @@ def set_service_instance(
|
|||||||
inventory = load_inventory_json(base_path)
|
inventory = load_inventory_json(base_path)
|
||||||
target_type = get_args(get_type_hints(Service)[module_name])[1]
|
target_type = get_args(get_type_hints(Service)[module_name])[1]
|
||||||
|
|
||||||
module_instance_map: dict[str, Any] = getattr(inventory.services, module_name, {})
|
module_instance_map: dict[str, Any] = inventory.get("services", {}).get(
|
||||||
|
module_name, {}
|
||||||
|
)
|
||||||
|
|
||||||
module_instance_map[instance_name] = from_dict(target_type, config)
|
module_instance_map[instance_name] = from_dict(target_type, config)
|
||||||
|
|
||||||
setattr(inventory.services, module_name, module_instance_map)
|
inventory["services"] = inventory.get("services", {})
|
||||||
|
inventory["services"][module_name] = module_instance_map
|
||||||
|
|
||||||
set_inventory(
|
set_inventory(
|
||||||
inventory, base_path, f"Update {module_name} instance {instance_name}"
|
inventory, base_path, f"Update {module_name} instance {instance_name}"
|
||||||
|
|||||||
@@ -63,9 +63,11 @@ def show_clan_meta(uri: str | Path) -> Meta:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return Meta(
|
return Meta(
|
||||||
name=clan_meta.get("name"),
|
{
|
||||||
description=clan_meta.get("description", None),
|
"name": clan_meta.get("name"),
|
||||||
icon=icon_path,
|
"description": clan_meta.get("description"),
|
||||||
|
"icon": icon_path if icon_path else "",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -73,9 +75,9 @@ def show_command(args: argparse.Namespace) -> None:
|
|||||||
flake_path = args.flake.path
|
flake_path = args.flake.path
|
||||||
meta = show_clan_meta(flake_path)
|
meta = show_clan_meta(flake_path)
|
||||||
|
|
||||||
print(f"Name: {meta.name}")
|
print(f"Name: {meta.get("name")}")
|
||||||
print(f"Description: {meta.description or '-'}")
|
print(f"Description: {meta.get("description", '-')}")
|
||||||
print(f"Icon: {meta.icon or '-'}")
|
print(f"Icon: {meta.get("icon", '-')}")
|
||||||
|
|
||||||
|
|
||||||
def register_parser(parser: argparse.ArgumentParser) -> None:
|
def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from clan_cli.api import API
|
from clan_cli.api import API
|
||||||
from clan_cli.inventory import Meta, load_inventory_json, set_inventory
|
from clan_cli.inventory import Inventory, Meta, load_inventory_json, set_inventory
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -11,10 +11,10 @@ class UpdateOptions:
|
|||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def update_clan_meta(options: UpdateOptions) -> Meta:
|
def update_clan_meta(options: UpdateOptions) -> Inventory:
|
||||||
inventory = load_inventory_json(options.directory)
|
inventory = load_inventory_json(options.directory)
|
||||||
inventory.meta = options.meta
|
inventory["meta"] = options.meta
|
||||||
|
|
||||||
set_inventory(inventory, options.directory, "Update clan metadata")
|
set_inventory(inventory, options.directory, "Update clan metadata")
|
||||||
|
|
||||||
return inventory.meta
|
return inventory
|
||||||
|
|||||||
@@ -61,9 +61,7 @@ def get_inventory_path(flake_dir: str | Path, create: bool = True) -> Path:
|
|||||||
|
|
||||||
|
|
||||||
# Default inventory
|
# Default inventory
|
||||||
default_inventory = Inventory(
|
default_inventory: Inventory = {"meta": {"name": "New Clan"}}
|
||||||
meta=Meta(name="New Clan"), machines={}, services=Service()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
@@ -381,9 +379,7 @@ def patch_inventory_with(base_dir: Path, section: str, content: dict[str, Any])
|
|||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def set_inventory(
|
def set_inventory(inventory: Inventory, flake_dir: str | Path, message: str) -> None:
|
||||||
inventory: Inventory | dict[str, Any], flake_dir: str | Path, message: str
|
|
||||||
) -> 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
|
||||||
@@ -396,18 +392,11 @@ def set_inventory(
|
|||||||
filtered_modules = lambda m: {
|
filtered_modules = lambda m: {
|
||||||
key: value for key, value in m.items() if "/nix/store" not in value
|
key: value for key, value in m.items() if "/nix/store" not in value
|
||||||
}
|
}
|
||||||
if isinstance(inventory, dict):
|
modules = filtered_modules(inventory.get("modules", {})) # type: ignore
|
||||||
modules = filtered_modules(inventory.get("modules", {})) # type: ignore
|
inventory["modules"] = modules
|
||||||
inventory["modules"] = modules
|
|
||||||
else:
|
|
||||||
modules = filtered_modules(inventory.modules) # type: ignore
|
|
||||||
inventory.modules = modules
|
|
||||||
|
|
||||||
with inventory_file.open("w") as f:
|
with inventory_file.open("w") as f:
|
||||||
if isinstance(inventory, Inventory):
|
json.dump(inventory, f, indent=2)
|
||||||
json.dump(dataclass_to_dict(inventory), f, indent=2)
|
|
||||||
else:
|
|
||||||
json.dump(inventory, f, indent=2)
|
|
||||||
|
|
||||||
commit_file(inventory_file, Path(flake_dir), commit_message=message)
|
commit_file(inventory_file, Path(flake_dir), commit_message=message)
|
||||||
|
|
||||||
@@ -436,7 +425,7 @@ def merge_template_inventory(
|
|||||||
Merge the template inventory into the current inventory
|
Merge the template inventory into the current inventory
|
||||||
The template inventory is expected to be a subset of the current inventory
|
The template inventory is expected to be a subset of the current inventory
|
||||||
"""
|
"""
|
||||||
for service_name, instance in template_inventory.services.items():
|
for service_name, instance in template_inventory.get("services", {}).items():
|
||||||
if len(instance.keys()) > 0:
|
if len(instance.keys()) > 0:
|
||||||
msg = f"Service {service_name} in template inventory has multiple instances"
|
msg = f"Service {service_name} in template inventory has multiple instances"
|
||||||
description = (
|
description = (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# ruff: noqa: N806
|
# ruff: noqa: N806
|
||||||
# ruff: noqa: F401
|
# ruff: noqa: F401
|
||||||
# fmt: off
|
# fmt: off
|
||||||
from typing import Any, Literal, TypedDict, NotRequired
|
from typing import Any, Literal, NotRequired, TypedDict
|
||||||
|
|
||||||
|
|
||||||
class MachineDeploy(TypedDict):
|
class MachineDeploy(TypedDict):
|
||||||
@@ -33,4 +33,4 @@ class Inventory(TypedDict):
|
|||||||
meta: NotRequired[Meta]
|
meta: NotRequired[Meta]
|
||||||
modules: NotRequired[dict[str, str]]
|
modules: NotRequired[dict[str, str]]
|
||||||
services: NotRequired[dict[str, Service]]
|
services: NotRequired[dict[str, Service]]
|
||||||
tags: NotRequired[dict[str, list[str]]]
|
tags: NotRequired[dict[str, list[str]]]
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ from clan_cli.git import commit_file
|
|||||||
from clan_cli.inventory import Machine as InventoryMachine
|
from clan_cli.inventory import Machine as InventoryMachine
|
||||||
from clan_cli.inventory import (
|
from clan_cli.inventory import (
|
||||||
MachineDeploy,
|
MachineDeploy,
|
||||||
dataclass_to_dict,
|
|
||||||
load_inventory_json,
|
load_inventory_json,
|
||||||
merge_template_inventory,
|
merge_template_inventory,
|
||||||
set_inventory,
|
set_inventory,
|
||||||
@@ -64,15 +63,17 @@ def create_machine(opts: CreateOptions) -> None:
|
|||||||
clan_dir = opts.clan_dir.path
|
clan_dir = opts.clan_dir.path
|
||||||
|
|
||||||
log.debug(f"Importing machine '{opts.template_name}' from {opts.template_src}")
|
log.debug(f"Importing machine '{opts.template_name}' from {opts.template_src}")
|
||||||
|
machine_name = opts.machine.get("name")
|
||||||
if opts.template_name in list_nixos_machines(clan_dir) and not opts.machine.name:
|
if opts.template_name in list_nixos_machines(clan_dir) and not opts.machine.get(
|
||||||
|
"name"
|
||||||
|
):
|
||||||
msg = f"{opts.template_name} is already defined in {clan_dir}"
|
msg = f"{opts.template_name} is already defined in {clan_dir}"
|
||||||
description = (
|
description = (
|
||||||
"Please add the --rename option to import the machine with a different name"
|
"Please add the --rename option to import the machine with a different name"
|
||||||
)
|
)
|
||||||
raise ClanError(msg, description=description)
|
raise ClanError(msg, description=description)
|
||||||
|
|
||||||
machine_name = opts.template_name if not opts.machine.name else opts.machine.name
|
machine_name = machine_name if machine_name else opts.template_name
|
||||||
dst = clan_dir / "machines" / machine_name
|
dst = clan_dir / "machines" / machine_name
|
||||||
|
|
||||||
# TODO: Move this into nix code
|
# TODO: Move this into nix code
|
||||||
@@ -138,19 +139,24 @@ def create_machine(opts: CreateOptions) -> None:
|
|||||||
merge_template_inventory(inventory, template_inventory, machine_name)
|
merge_template_inventory(inventory, template_inventory, machine_name)
|
||||||
|
|
||||||
deploy = MachineDeploy()
|
deploy = MachineDeploy()
|
||||||
deploy.targetHost = opts.target_host
|
target_host = opts.target_host
|
||||||
|
if target_host:
|
||||||
|
deploy["targetHost"] = target_host
|
||||||
# TODO: We should allow the template to specify machine metadata if not defined by user
|
# TODO: We should allow the template to specify machine metadata if not defined by user
|
||||||
new_machine = InventoryMachine(
|
new_machine = InventoryMachine(
|
||||||
name=machine_name, deploy=deploy, tags=opts.machine.tags
|
name=machine_name, deploy=deploy, tags=opts.machine.get("tags", [])
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
not has_inventory
|
not has_inventory
|
||||||
and len(opts.machine.tags) == 0
|
and len(opts.machine.get("tags", [])) == 0
|
||||||
and new_machine.deploy.targetHost is None
|
and new_machine.get("deploy", {}).get("targetHost") is None
|
||||||
):
|
):
|
||||||
# no need to update inventory if there are no tags or target host
|
# no need to update inventory if there are no tags or target host
|
||||||
return
|
return
|
||||||
inventory.machines.update({new_machine.name: dataclass_to_dict(new_machine)})
|
|
||||||
|
inventory["machines"] = inventory.get("machines", {})
|
||||||
|
inventory["machines"][machine_name] = new_machine
|
||||||
|
|
||||||
set_inventory(inventory, clan_dir, "Imported machine from template")
|
set_inventory(inventory, clan_dir, "Imported machine from template")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from clan_cli.inventory import load_inventory_json, set_inventory
|
|||||||
def delete_machine(flake: FlakeId, name: str) -> None:
|
def delete_machine(flake: FlakeId, name: str) -> None:
|
||||||
inventory = load_inventory_json(flake.path)
|
inventory = load_inventory_json(flake.path)
|
||||||
|
|
||||||
machine = inventory.machines.pop(name, None)
|
machine = inventory.get("machines", {}).pop(name, None)
|
||||||
if machine is None:
|
if machine is None:
|
||||||
msg = f"Machine {name} does not exist"
|
msg = f"Machine {name} does not exist"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def set_machine(flake_url: Path, machine_name: str, machine: Machine) -> None:
|
|||||||
@API.register
|
@API.register
|
||||||
def list_inventory_machines(flake_url: str | Path) -> dict[str, Machine]:
|
def list_inventory_machines(flake_url: str | Path) -> dict[str, Machine]:
|
||||||
inventory = load_inventory_eval(flake_url)
|
inventory = load_inventory_eval(flake_url)
|
||||||
return inventory.machines
|
return inventory.get("machines", {})
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -61,7 +61,7 @@ def extract_header(c: str) -> str:
|
|||||||
@API.register
|
@API.register
|
||||||
def get_inventory_machine_details(flake_url: Path, machine_name: str) -> MachineDetails:
|
def get_inventory_machine_details(flake_url: Path, machine_name: str) -> MachineDetails:
|
||||||
inventory = load_inventory_eval(flake_url)
|
inventory = load_inventory_eval(flake_url)
|
||||||
machine = inventory.machines.get(machine_name)
|
machine = inventory.get("machines", {}).get(machine_name)
|
||||||
if machine is None:
|
if machine 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)
|
||||||
@@ -113,12 +113,12 @@ class ConnectionOptions:
|
|||||||
def check_machine_online(
|
def check_machine_online(
|
||||||
flake_url: str | Path, machine_name: str, opts: ConnectionOptions | None
|
flake_url: str | Path, machine_name: str, opts: ConnectionOptions | None
|
||||||
) -> Literal["Online", "Offline"]:
|
) -> Literal["Online", "Offline"]:
|
||||||
machine = load_inventory_eval(flake_url).machines.get(machine_name)
|
machine = load_inventory_eval(flake_url).get("machines", {}).get(machine_name)
|
||||||
if not machine:
|
if not machine:
|
||||||
msg = f"Machine {machine_name} not found in inventory"
|
msg = f"Machine {machine_name} not found in inventory"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
|
||||||
hostname = machine.deploy.targetHost
|
hostname = machine.get("deploy", {}).get("targetHost")
|
||||||
|
|
||||||
if not hostname:
|
if not hostname:
|
||||||
msg = f"Machine {machine_name} does not specify a targetHost"
|
msg = f"Machine {machine_name} does not specify a targetHost"
|
||||||
|
|||||||
@@ -96,15 +96,19 @@ def update_machines(base_path: str, machines: list[InventoryMachine]) -> None:
|
|||||||
|
|
||||||
# Convert InventoryMachine to Machine
|
# Convert InventoryMachine to Machine
|
||||||
for machine in machines:
|
for machine in machines:
|
||||||
|
name = machine.get("name")
|
||||||
|
if not name:
|
||||||
|
msg = "Machine name is not set"
|
||||||
|
raise ClanError(msg)
|
||||||
m = Machine(
|
m = Machine(
|
||||||
name=machine.name,
|
name,
|
||||||
flake=FlakeId(base_path),
|
flake=FlakeId(base_path),
|
||||||
)
|
)
|
||||||
if not machine.deploy.targetHost:
|
if not machine.get("deploy", {}).get("targetHost"):
|
||||||
msg = f"'TargetHost' is not set for machine '{machine.name}'"
|
msg = f"'TargetHost' is not set for machine '{name}'"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
# Copy targetHost to machine
|
# Copy targetHost to machine
|
||||||
m.override_target_host = machine.deploy.targetHost
|
m.override_target_host = machine.get("deploy", {}).get("targetHost")
|
||||||
# Would be nice to have?
|
# Would be nice to have?
|
||||||
# m.override_build_host = machine.deploy.buildHost
|
# m.override_build_host = machine.deploy.buildHost
|
||||||
group_machines.append(m)
|
group_machines.append(m)
|
||||||
|
|||||||
41
pkgs/clan-cli/tests/test_inventory.py
Normal file
41
pkgs/clan-cli/tests/test_inventory.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from clan_cli.inventory.classes import Inventory, Machine, Meta, Service
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_meta_minimal() -> None:
|
||||||
|
# Name is required
|
||||||
|
res = Meta(
|
||||||
|
{
|
||||||
|
"name": "foo",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert res == {"name": "foo"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_inventory_minimal() -> None:
|
||||||
|
# Meta is required
|
||||||
|
res = Inventory(
|
||||||
|
{
|
||||||
|
"meta": Meta(
|
||||||
|
{
|
||||||
|
"name": "foo",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert res == {"meta": {"name": "foo"}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_machine_minimal() -> None:
|
||||||
|
# Empty is valid
|
||||||
|
res = Machine({})
|
||||||
|
|
||||||
|
assert res == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_make_service_minimal() -> None:
|
||||||
|
# Empty is valid
|
||||||
|
res = Service({})
|
||||||
|
|
||||||
|
assert res == {}
|
||||||
@@ -72,7 +72,7 @@ def test_add_module_to_inventory(
|
|||||||
|
|
||||||
inventory = load_inventory_json(base_path)
|
inventory = load_inventory_json(base_path)
|
||||||
|
|
||||||
inventory.services = {
|
inventory["services"] = {
|
||||||
"borgbackup": {
|
"borgbackup": {
|
||||||
"borg1": {
|
"borg1": {
|
||||||
"meta": {"name": "borg1"},
|
"meta": {"name": "borg1"},
|
||||||
|
|||||||
Reference in New Issue
Block a user