clan-cli: init machine from inventory

This commit is contained in:
Qubasa
2025-06-07 00:51:53 +02:00
parent fca772a4f4
commit 68b7c22faf
3 changed files with 121 additions and 32 deletions

View File

@@ -194,7 +194,7 @@ def install_command(args: argparse.Namespace) -> None:
if not args.yes: if not args.yes:
ask = input( ask = input(
f"Install {args.machine} to {machine.target_host_address}? [y/N] " f"Install {args.machine} to {machine.target_host().target}? [y/N] "
) )
if ask != "y": if ask != "y":
return None return None

View File

@@ -21,7 +21,8 @@ class ConnectionOptions:
def check_machine_online( def check_machine_online(
machine: Machine, opts: ConnectionOptions | None = None machine: Machine, opts: ConnectionOptions | None = None
) -> Literal["Online", "Offline"]: ) -> Literal["Online", "Offline"]:
hostname = machine.target_host_address hostname = machine.target_host().target
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"
raise ClanError(msg) raise ClanError(msg)

View File

@@ -5,16 +5,19 @@ import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import cached_property from functools import cached_property
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any, Literal
from clan_cli.facts import public_modules as facts_public_modules from clan_cli.facts import public_modules as facts_public_modules
from clan_cli.facts import secret_modules as facts_secret_modules from clan_cli.facts import secret_modules as facts_secret_modules
from clan_cli.ssh.host_key import HostKeyCheck from clan_cli.ssh.host_key import HostKeyCheck
from clan_cli.vars._types import StoreBase from clan_cli.vars._types import StoreBase
from clan_lib.api import API
from clan_lib.errors import ClanCmdError, ClanError from clan_lib.errors import ClanCmdError, ClanError
from clan_lib.flake import Flake from clan_lib.flake import Flake
from clan_lib.nix import nix_config, nix_test_store from clan_lib.nix import nix_config, nix_test_store
from clan_lib.nix_models.clan import InventoryMachine
from clan_lib.persist.inventory_store import InventoryStore
from clan_lib.ssh.remote import Remote from clan_lib.ssh.remote import Remote
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -34,6 +37,18 @@ class Machine:
private_key: Path | None = None private_key: Path | None = None
host_key_check: HostKeyCheck = HostKeyCheck.STRICT host_key_check: HostKeyCheck = HostKeyCheck.STRICT
def get_inv_machine(self) -> "InventoryMachine":
inventory_store = InventoryStore(flake=self.flake)
inv = inventory_store.read()
inv_machine = inv.get("machines", {}).get(self.name, None)
if inv_machine is None:
msg = f"Machine '{self.name}' not found in clan"
raise ClanError(msg, description="Check your inventory configuration.")
return inv_machine
def get_id(self) -> str: def get_id(self) -> str:
return f"{self.flake}#{self.name}" return f"{self.flake}#{self.name}"
@@ -54,6 +69,10 @@ class Machine:
kwargs.update({"extra": {"command_prefix": self.name}}) kwargs.update({"extra": {"command_prefix": self.name}})
log.info(msg, *args, **kwargs) log.info(msg, *args, **kwargs)
def warn(self, msg: str, *args: Any, **kwargs: Any) -> None:
kwargs.update({"extra": {"command_prefix": self.name}})
log.warning(msg, *args, **kwargs)
def error(self, msg: str, *args: Any, **kwargs: Any) -> None: def error(self, msg: str, *args: Any, **kwargs: Any) -> None:
kwargs.update({"extra": {"command_prefix": self.name}}) kwargs.update({"extra": {"command_prefix": self.name}})
log.error(msg, *args, **kwargs) log.error(msg, *args, **kwargs)
@@ -83,17 +102,6 @@ class Machine:
) )
return deployment return deployment
@property
def target_host_address(self) -> str:
val = self.override_target_host or self.deployment.get("targetHost")
if val is None:
msg = f"'targetHost' is not set for machine '{self.name}'"
raise ClanError(
msg,
description="See https://docs.clan.lol/guides/getting-started/deploy/#setting-the-target-host for more information.",
)
return val
@cached_property @cached_property
def secret_facts_store(self) -> facts_secret_modules.SecretStoreBase: def secret_facts_store(self) -> facts_secret_modules.SecretStoreBase:
module = importlib.import_module(self.deployment["facts"]["secretModule"]) module = importlib.import_module(self.deployment["facts"]["secretModule"])
@@ -144,30 +152,40 @@ class Machine:
return self.flake.path return self.flake.path
def target_host(self) -> Remote: def target_host(self) -> Remote:
if self.override_target_host:
return Remote.from_deployment_address( return Remote.from_deployment_address(
machine_name=self.name, machine_name=self.name,
address=self.target_host_address, address=self.override_target_host,
host_key_check=self.host_key_check, host_key_check=self.host_key_check,
private_key=self.private_key, private_key=self.private_key,
) )
remote = get_target_host(self.name, self.flake)
if remote is None:
msg = f"'targetHost' is not set for machine '{self.name}'"
raise ClanError(
msg,
description="See https://docs.clan.lol/guides/getting-started/deploy/#setting-the-target-host for more information.",
)
return remote.data
def build_host(self) -> Remote | None: def build_host(self) -> Remote | None:
""" """
The host where the machine is built and deployed from. The host where the machine is built and deployed from.
Can be the same as the target host. Can be the same as the target host.
""" """
address = self.override_build_host or self.deployment.get("buildHost")
if address is None: if self.override_build_host:
return None return Remote.from_deployment_address(
# enable ssh agent forwarding to allow the build host to access the target host
host = Remote.from_deployment_address(
machine_name=self.name, machine_name=self.name,
address=address, address=self.override_build_host,
host_key_check=self.host_key_check, host_key_check=self.host_key_check,
forward_agent=True,
private_key=self.private_key, private_key=self.private_key,
) )
return host
remote = get_build_host(self)
return remote.data if remote else None
def nix( def nix(
self, self,
@@ -234,3 +252,73 @@ class Machine:
return output return output
msg = "build_nix returned not a Path" msg = "build_nix returned not a Path"
raise ClanError(msg) raise ClanError(msg)
@dataclass(frozen=True)
class RemoteSource:
data: Remote
source: Literal["inventory", "nix_machine"]
@API.register
def get_target_host(name: str, flake: Flake) -> RemoteSource | None:
"""
Get the build host for a machine.
"""
machine = Machine(name=name, flake=flake)
inv_machine = machine.get_inv_machine()
source = "inventory"
target_host_str = inv_machine.get("deploy", {}).get("targetHost")
if target_host_str is None:
machine.info(
"'targetHost' is not set in inventory, falling back to slow Nix config"
)
target_host_str = machine.eval_nix("config.clan.core.networking.targetHost")
source = "nix_machine"
if not target_host_str:
return None
return RemoteSource(
data=Remote.from_deployment_address(
machine_name=machine.name,
address=target_host_str,
host_key_check=machine.host_key_check,
private_key=machine.private_key,
),
source=source,
)
@API.register
def get_build_host(name: str, flake: Flake) -> RemoteSource | None:
"""
Get the build host for a machine.
"""
machine = Machine(name=name, flake=flake)
inv_machine = machine.get_inv_machine()
source = "inventory"
target_host_str = inv_machine.get("deploy", {}).get("buildHost")
if target_host_str is None:
machine.info(
"'buildHost' is not set in inventory, falling back to slow Nix config"
)
target_host_str = machine.eval_nix("config.clan.core.networking.buildHost")
source = "nix_machine"
if not target_host_str:
return None
return RemoteSource(
data=Remote.from_deployment_address(
machine_name=machine.name,
address=target_host_str,
host_key_check=machine.host_key_check,
private_key=machine.private_key,
),
source=source,
)