refactor: replace eval_nix/build_nix with machine.select()

- Remove nix(), eval_nix(), and build_nix() methods from Machine class
- Add select() method that handles machine-specific attribute prefixes
- Update all usages to use machine.select() directly
- Handle Path conversion and tmp_store logic at call sites
- This simplifies the Machine API and prepares for deployment.json removal
This commit is contained in:
lassulus
2025-06-29 16:35:19 +02:00
parent f9f7d65e94
commit cc923d5638
11 changed files with 44 additions and 60 deletions

View File

@@ -992,7 +992,7 @@ def test_dynamic_invalidation(
# before generating, dependent generator validation should be empty; see bogus hardware-configuration.nix above # before generating, dependent generator validation should be empty; see bogus hardware-configuration.nix above
# we have to avoid `*.files.value` in this initial select because the generators haven't been run yet # we have to avoid `*.files.value` in this initial select because the generators haven't been run yet
# Generators 0: The initial generators before any 'vars generate' # Generators 0: The initial generators before any 'vars generate'
generators_0 = machine.eval_nix(f"{gen_prefix}.*.{{validationHash}}") generators_0 = machine.select(f"{gen_prefix}.*.{{validationHash}}")
assert generators_0["dependent_generator"]["validationHash"] is None assert generators_0["dependent_generator"]["validationHash"] is None
# generate both my_generator and (the dependent) dependent_generator # generate both my_generator and (the dependent) dependent_generator
@@ -1001,7 +1001,7 @@ def test_dynamic_invalidation(
# after generating once, dependent generator validation should be set # after generating once, dependent generator validation should be set
# Generators_1: The generators after the first 'vars generate' # Generators_1: The generators after the first 'vars generate'
generators_1 = machine.eval_nix(gen_prefix) generators_1 = machine.select(gen_prefix)
assert generators_1["dependent_generator"]["validationHash"] is not None assert generators_1["dependent_generator"]["validationHash"] is not None
# @tangential: after generating once, neither generator should want to run again because `clan vars generate` should have re-evaluated the dependent generator's validationHash after executing the parent generator but before executing the dependent generator # @tangential: after generating once, neither generator should want to run again because `clan vars generate` should have re-evaluated the dependent generator's validationHash after executing the parent generator but before executing the dependent generator
@@ -1014,7 +1014,7 @@ def test_dynamic_invalidation(
cli.run(["vars", "generate", "--flake", str(flake.path), machine.name]) cli.run(["vars", "generate", "--flake", str(flake.path), machine.name])
clan_flake.invalidate_cache() clan_flake.invalidate_cache()
# Generators_2: The generators after the second 'vars generate' # Generators_2: The generators after the second 'vars generate'
generators_2 = machine.eval_nix(gen_prefix) generators_2 = machine.select(gen_prefix)
assert ( assert (
generators_1["dependent_generator"]["validationHash"] generators_1["dependent_generator"]["validationHash"]
== generators_2["dependent_generator"]["validationHash"] == generators_2["dependent_generator"]["validationHash"]

View File

@@ -72,14 +72,20 @@ class Generator:
def final_script(self) -> Path: def final_script(self) -> Path:
assert self._machine is not None assert self._machine is not None
final_script = self._machine.build_nix( from clan_lib.nix import nix_test_store
f'config.clan.core.vars.generators."{self.name}".finalScript'
output = Path(
self._machine.select(
f'config.clan.core.vars.generators."{self.name}".finalScript'
)
) )
return final_script if tmp_store := nix_test_store():
output = tmp_store.joinpath(*output.parts[1:])
return output
def validation(self) -> str | None: def validation(self) -> str | None:
assert self._machine is not None assert self._machine is not None
return self._machine.eval_nix( return self._machine.select(
f'config.clan.core.vars.generators."{self.name}".validationHash' f'config.clan.core.vars.generators."{self.name}".validationHash'
) )

View File

@@ -33,7 +33,7 @@ class SecretStore(StoreBase):
@property @property
def _store_backend(self) -> str: def _store_backend(self) -> str:
backend = self.machine.eval_nix("config.clan.core.vars.settings.passBackend") backend = self.machine.select("config.clan.core.vars.settings.passBackend")
return backend return backend
@property @property

View File

@@ -55,7 +55,7 @@ class VmConfig:
def inspect_vm(machine: Machine) -> VmConfig: def inspect_vm(machine: Machine) -> VmConfig:
data = machine.eval_nix("config.clan.core.vm.inspect") data = machine.select("config.clan.core.vm.inspect")
# HACK! # HACK!
data["flake_url"] = dataclasses.asdict(machine.flake) data["flake_url"] = dataclasses.asdict(machine.flake)
return VmConfig.from_json(data) return VmConfig.from_json(data)

View File

@@ -49,16 +49,24 @@ def facts_to_nixos_config(facts: dict[str, dict[str, bytes]]) -> dict:
# TODO move this to the Machines class # TODO move this to the Machines class
def build_vm(machine: Machine, tmpdir: Path) -> dict[str, str]: def build_vm(
machine: Machine, tmpdir: Path, nix_options: list[str] | None = None
) -> dict[str, str]:
# TODO pass prompt here for the GTK gui # TODO pass prompt here for the GTK gui
if nix_options is None:
nix_options = []
secrets_dir = get_secrets(machine, tmpdir) secrets_dir = get_secrets(machine, tmpdir)
public_facts = machine.public_facts_store.get_all() from clan_lib.nix import nix_test_store
nixos_config_file = machine.build_nix( output = Path(
"config.system.clan.vm.create", extra_config=facts_to_nixos_config(public_facts) machine.select(
"config.system.clan.vm.create",
)
) )
if tmp_store := nix_test_store():
output = tmp_store.joinpath(*output.parts[1:])
nixos_config_file = output
try: try:
vm_data = json.loads(Path(nixos_config_file).read_text()) vm_data = json.loads(Path(nixos_config_file).read_text())
vm_data["secrets_dir"] = str(secrets_dir) vm_data["secrets_dir"] = str(secrets_dir)

View File

@@ -4,7 +4,7 @@ from clan_lib.machines.machines import Machine
def create_backup(machine: Machine, provider: str | None = None) -> None: def create_backup(machine: Machine, provider: str | None = None) -> None:
machine.info(f"creating backup for {machine.name}") machine.info(f"creating backup for {machine.name}")
backup_scripts = machine.eval_nix("config.clan.core.backups") backup_scripts = machine.select("config.clan.core.backups")
host = machine.target_host() host = machine.target_host()
if provider is None: if provider is None:
if not backup_scripts["providers"]: if not backup_scripts["providers"]:

View File

@@ -15,7 +15,7 @@ class Backup:
def list_provider(machine: Machine, host: Remote, provider: str) -> list[Backup]: def list_provider(machine: Machine, host: Remote, provider: str) -> list[Backup]:
results = [] results = []
backup_metadata = machine.eval_nix("config.clan.core.backups") backup_metadata = machine.select("config.clan.core.backups")
list_command = backup_metadata["providers"][provider]["list"] list_command = backup_metadata["providers"][provider]["list"]
proc = host.run( proc = host.run(
[list_command], [list_command],
@@ -41,7 +41,7 @@ def list_provider(machine: Machine, host: Remote, provider: str) -> list[Backup]
def list_backups(machine: Machine, provider: str | None = None) -> list[Backup]: def list_backups(machine: Machine, provider: str | None = None) -> list[Backup]:
backup_metadata = machine.eval_nix("config.clan.core.backups") backup_metadata = machine.select("config.clan.core.backups")
results = [] results = []
with machine.target_host().ssh_control_master() as host: with machine.target_host().ssh_control_master() as host:
if provider is None: if provider is None:

View File

@@ -7,8 +7,8 @@ from clan_lib.ssh.remote import Remote
def restore_service( def restore_service(
machine: Machine, host: Remote, name: str, provider: str, service: str machine: Machine, host: Remote, name: str, provider: str, service: str
) -> None: ) -> None:
backup_metadata = machine.eval_nix("config.clan.core.backups") backup_metadata = machine.select("config.clan.core.backups")
backup_folders = machine.eval_nix("config.clan.core.state") backup_folders = machine.select("config.clan.core.state")
if service not in backup_folders: if service not in backup_folders:
msg = f"Service {service} not found in configuration. Available services are: {', '.join(backup_folders.keys())}" msg = f"Service {service} not found in configuration. Available services are: {', '.join(backup_folders.keys())}"
@@ -60,7 +60,7 @@ def restore_backup(
errors = [] errors = []
with machine.target_host().ssh_control_master() as host: with machine.target_host().ssh_control_master() as host:
if service is None: if service is None:
backup_folders = machine.eval_nix("config.clan.core.state") backup_folders = machine.select("config.clan.core.state")
for _service in backup_folders: for _service in backup_folders:
try: try:
restore_service(machine, host, name, provider, _service) restore_service(machine, host, name, provider, _service)

View File

@@ -81,9 +81,10 @@ class Machine:
@property @property
def deployment(self) -> dict: def deployment(self) -> dict:
deployment = json.loads( output = Path(self.select("config.system.clan.deployment.file"))
self.build_nix("config.system.clan.deployment.file").read_text() if tmp_store := nix_test_store():
) output = tmp_store.joinpath(*output.parts[1:])
deployment = json.loads(output.read_text())
return deployment return deployment
@cached_property @cached_property
@@ -159,13 +160,13 @@ class Machine:
return None return None
def nix( def select(
self, self,
attr: str, attr: str,
) -> Any: ) -> Any:
""" """
Build the machine and return the path to the result Select a nix attribute of the machine
accepts a secret store and a facts store # TODO @attr: the attribute to get
""" """
config = nix_config() config = nix_config()
@@ -175,36 +176,6 @@ class Machine:
f'clanInternals.machines."{system}"."{self.name}".{attr}' f'clanInternals.machines."{system}"."{self.name}".{attr}'
) )
def eval_nix(self, attr: str, extra_config: None | dict = None) -> Any:
"""
eval a nix attribute of the machine
@attr: the attribute to get
"""
if extra_config:
log.warning("extra_config in eval_nix is no longer supported")
return self.nix(attr)
def build_nix(self, attr: str, extra_config: None | dict = None) -> Path:
"""
build a nix attribute of the machine
@attr: the attribute to get
"""
if extra_config:
log.warning("extra_config in build_nix is no longer supported")
output = self.nix(attr)
output = Path(output)
if tmp_store := nix_test_store():
output = tmp_store.joinpath(*output.parts[1:])
assert output.exists(), f"The output {output} doesn't exist"
if isinstance(output, Path):
return output
msg = "build_nix returned not a Path"
raise ClanError(msg)
@dataclass(frozen=True) @dataclass(frozen=True)
class RemoteSource: class RemoteSource:
@@ -229,7 +200,7 @@ def get_host(
machine.debug( 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" 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"
) )
host_str = machine.eval_nix(f'config.clan.core.networking."{field}"') host_str = machine.select(f'config.clan.core.networking."{field}"')
source = "nix_machine" source = "nix_machine"
if not host_str: if not host_str:

View File

@@ -282,5 +282,5 @@ def test_clan_create_api(
clan_dir_flake.invalidate_cache() clan_dir_flake.invalidate_cache()
with pytest.raises(ClanError) as exc_info: with pytest.raises(ClanError) as exc_info:
machine.build_nix("config.system.build.toplevel") Path(machine.select("config.system.build.toplevel"))
assert "nixos-system-test-clan" in str(exc_info.value) assert "nixos-system-test-clan" in str(exc_info.value)

View File

@@ -63,8 +63,7 @@ class TestMachine(Machine):
def flake_dir(self) -> Path: def flake_dir(self) -> Path:
return self.test_dir return self.test_dir
@override def select(self, attr: str) -> Any:
def nix(self, attr: str) -> Any:
""" """
Build the machine and return the path to the result Build the machine and return the path to the result
accepts a secret store and a facts store # TODO accepts a secret store and a facts store # TODO