Merge pull request 'Clan_lib: add filtering by tag to list API' (#4197) from cli-fixup into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4197
This commit is contained in:
hsjobeki
2025-07-04 11:53:43 +00:00
8 changed files with 69 additions and 30 deletions

View File

@@ -1,4 +1,5 @@
from dataclasses import dataclass
from typing import TypedDict
from clan_lib.api import API
from clan_lib.errors import ClanError
@@ -10,15 +11,44 @@ from clan_lib.persist.inventory_store import InventoryStore
from clan_lib.persist.util import set_value_by_path
class MachineFilter(TypedDict):
tags: list[str]
class ListOptions(TypedDict):
filter: MachineFilter
@API.register
def list_machines(flake: Flake) -> dict[str, InventoryMachine]:
def list_machines(
flake: Flake, opts: ListOptions | None = None
) -> dict[str, InventoryMachine]:
"""
List machines in the inventory for the UI.
List machines of a clan
Usage Example:
machines = list_machines(flake, {"filter": {"tags": ["foo" "bar"]}})
lists only machines that include both "foo" AND "bar"
"""
inventory_store = InventoryStore(flake=flake)
inventory = inventory_store.read()
machines = inventory.get("machines", {})
if opts and opts.get("filter"):
filtered_machines = {}
filter_tags = opts.get("filter", {}).get("tags", [])
for machine_name, machine in machines.items():
machine_tags = machine.get("tags", [])
if all(ft in machine_tags for ft in filter_tags):
filtered_machines[machine_name] = machine
return filtered_machines
return machines

View File

@@ -16,35 +16,39 @@ from clan_lib.nix_models.clan import InventoryMachine
log = logging.getLogger(__name__)
def convert_inventory_to_machines(
flake: Flake, machines: dict[str, InventoryMachine]
) -> dict[str, Machine]:
return {
name: Machine.from_inventory(name, flake, inventory_machine)
for name, inventory_machine in machines.items()
}
def list_full_machines(flake: Flake) -> dict[str, Machine]:
"""
Like `list_machines`, but returns a full 'machine' instance for each machine.
"""
machines = list_machines(flake)
res: dict[str, Machine] = {}
for name in machines:
machine = Machine(name=name, flake=flake)
res[machine.name] = machine
return res
return convert_inventory_to_machines(flake, machines)
def query_machines_by_tags(flake: Flake, tags: list[str]) -> dict[str, Machine]:
def query_machines_by_tags(
flake: Flake, tags: list[str]
) -> dict[str, InventoryMachine]:
"""
Query machines by their respective tags, if multiple tags are specified
then only machines that have those respective tags specified will be listed.
It is an intersection of the tags and machines.
"""
machines = list_full_machines(flake)
machines = list_machines(flake)
filtered_machines = {}
for machine in machines.values():
inv_machine = get_machine(machine.flake, machine.name)
machine_tags = inv_machine.get("tags", [])
for machine_name, machine in machines.items():
machine_tags = machine.get("tags", [])
if all(tag in machine_tags for tag in tags):
filtered_machines[machine.name] = machine
filtered_machines[machine_name] = machine
return filtered_machines

View File

@@ -29,6 +29,15 @@ class Machine:
name: str
flake: Flake
@classmethod
def from_inventory(
cls,
name: str,
flake: Flake,
_inventory_machine: InventoryMachine,
) -> "Machine":
return cls(name=name, flake=flake)
def get_inv_machine(self) -> "InventoryMachine":
return get_machine(self.flake, self.name)
@@ -166,7 +175,7 @@ class Machine:
@dataclass(frozen=True)
class RemoteSource:
data: Remote
source: Literal["inventory", "nix_machine"]
source: Literal["inventory", "machine"]
@API.register
@@ -179,15 +188,15 @@ def get_host(
machine = Machine(name=name, flake=flake)
inv_machine = machine.get_inv_machine()
source: Literal["inventory", "nix_machine"] = "inventory"
source: Literal["inventory", "machine"] = "inventory"
host_str = inv_machine.get("deploy", {}).get(field)
if host_str is None:
machine.debug(
f"'{field}' is not set in inventory, falling back to slower Nix config, set it either through the Nix or json interface to improve performance"
machine.warn(
f"'{field}' is not set in `inventory.machines.${name}.deploy.targetHost` - falling back to _slower_ nixos option: `clan.core.networking.targetHost`"
)
host_str = machine.select(f'config.clan.core.networking."{field}"')
source = "nix_machine"
source = "machine"
if not host_str:
return None