1
0
forked from clan/clan-core

make machine class now a dataclass

This commit is contained in:
Jörg Thalheim 2024-07-02 14:06:31 +02:00
parent cc583dd79e
commit b4698528ef
23 changed files with 125 additions and 128 deletions

View File

@ -2,6 +2,7 @@ import argparse
import json import json
import logging import logging
from ..clan_uri import FlakeId
from ..completions import ( from ..completions import (
add_dynamic_completer, add_dynamic_completer,
complete_backup_providers_for_machine, complete_backup_providers_for_machine,
@ -40,7 +41,7 @@ def create_backup(machine: Machine, provider: str | None = None) -> None:
def create_command(args: argparse.Namespace) -> None: def create_command(args: argparse.Namespace) -> None:
if args.flake is None: if args.flake is None:
raise ClanError("Could not find clan flake toplevel directory") raise ClanError("Could not find clan flake toplevel directory")
machine = Machine(name=args.machine, flake=args.flake) machine = Machine(name=args.machine, flake=FlakeId(args.flake))
create_backup(machine=machine, provider=args.provider) create_backup(machine=machine, provider=args.provider)

View File

@ -3,6 +3,7 @@ import json
import subprocess import subprocess
from dataclasses import dataclass from dataclasses import dataclass
from ..clan_uri import FlakeId
from ..completions import ( from ..completions import (
add_dynamic_completer, add_dynamic_completer,
complete_backup_providers_for_machine, complete_backup_providers_for_machine,
@ -55,7 +56,7 @@ def list_backups(machine: Machine, provider: str | None = None) -> list[Backup]:
def list_command(args: argparse.Namespace) -> None: def list_command(args: argparse.Namespace) -> None:
if args.flake is None: if args.flake is None:
raise ClanError("Could not find clan flake toplevel directory") raise ClanError("Could not find clan flake toplevel directory")
machine = Machine(name=args.machine, flake=args.flake) machine = Machine(name=args.machine, flake=FlakeId(args.flake))
backups = list_backups(machine=machine, provider=args.provider) backups = list_backups(machine=machine, provider=args.provider)
for backup in backups: for backup in backups:
print(backup.name) print(backup.name)

View File

@ -2,6 +2,7 @@ import argparse
import json import json
import subprocess import subprocess
from ..clan_uri import FlakeId
from ..completions import ( from ..completions import (
add_dynamic_completer, add_dynamic_completer,
complete_backup_providers_for_machine, complete_backup_providers_for_machine,
@ -86,7 +87,7 @@ def restore_backup(
def restore_command(args: argparse.Namespace) -> None: def restore_command(args: argparse.Namespace) -> None:
if args.flake is None: if args.flake is None:
raise ClanError("Could not find clan flake toplevel directory") raise ClanError("Could not find clan flake toplevel directory")
machine = Machine(name=args.machine, flake=args.flake) machine = Machine(name=args.machine, flake=FlakeId(args.flake))
restore_backup( restore_backup(
machine=machine, machine=machine,
provider=args.provider, provider=args.provider,

View File

@ -2,6 +2,7 @@ import argparse
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from ..clan_uri import FlakeId
from ..cmd import run from ..cmd import run
from ..dirs import machine_gcroot from ..dirs import machine_gcroot
from ..errors import ClanError from ..errors import ClanError
@ -45,7 +46,7 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig:
f"Machine {machine_name} not found in {flake_url}. Available machines: {', '.join(machines)}" f"Machine {machine_name} not found in {flake_url}. Available machines: {', '.join(machines)}"
) )
machine = Machine(machine_name, flake_url) machine = Machine(machine_name, FlakeId(flake_url))
vm = inspect_vm(machine) vm = inspect_vm(machine)
# Make symlink to gcroots from vm.machine_icon # Make symlink to gcroots from vm.machine_icon
@ -102,16 +103,16 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig:
@dataclass @dataclass
class InspectOptions: class InspectOptions:
machine: str machine: str
flake: Path flake: FlakeId
def inspect_command(args: argparse.Namespace) -> None: def inspect_command(args: argparse.Namespace) -> None:
inspect_options = InspectOptions( inspect_options = InspectOptions(
machine=args.machine, machine=args.machine,
flake=args.flake or Path.cwd(), flake=FlakeId(args.flake or Path.cwd()),
) )
res = inspect_flake( res = inspect_flake(
flake_url=inspect_options.flake, machine_name=inspect_options.machine flake_url=str(inspect_options.flake), machine_name=inspect_options.machine
) )
print("Clan name:", res.clan_name) print("Clan name:", res.clan_name)
print("Icon:", res.icon) print("Icon:", res.icon)

View File

@ -9,6 +9,7 @@ from .errors import ClanError
@dataclass @dataclass
class FlakeId: class FlakeId:
# FIXME: this is such a footgun if you accidnetally pass a string
_value: str | Path _value: str | Path
def __str__(self) -> str: def __str__(self) -> str:
@ -26,9 +27,6 @@ class FlakeId:
assert isinstance(self._value, str) assert isinstance(self._value, str)
return self._value return self._value
def __repr__(self) -> str:
return f"ClanUrl({self._value})"
def is_local(self) -> bool: def is_local(self) -> bool:
return isinstance(self._value, Path) return isinstance(self._value, Path)
@ -37,8 +35,9 @@ class FlakeId:
# Define the ClanURI class # Define the ClanURI class
@dataclass
class ClanURI: class ClanURI:
flake_id: FlakeId flake: FlakeId
machine_name: str machine_name: str
# Initialize the class with a clan:// URI # Initialize the class with a clan:// URI
@ -62,7 +61,7 @@ class ClanURI:
clean_comps = components._replace(query=components.query, fragment="") clean_comps = components._replace(query=components.query, fragment="")
# Parse the URL into a ClanUrl object # Parse the URL into a ClanUrl object
self.flake_id = self._parse_url(clean_comps) self.flake = self._parse_url(clean_comps)
self.machine_name = "defaultVM" self.machine_name = "defaultVM"
if components.fragment: if components.fragment:
self.machine_name = components.fragment self.machine_name = components.fragment
@ -85,7 +84,7 @@ class ClanURI:
return flake_id return flake_id
def get_url(self) -> str: def get_url(self) -> str:
return str(self.flake_id) return str(self.flake)
@classmethod @classmethod
def from_str( def from_str(
@ -103,6 +102,3 @@ class ClanURI:
clan_uri += f"#{machine_name}" clan_uri += f"#{machine_name}"
return cls(clan_uri) return cls(clan_uri)
def __repr__(self) -> str:
return f"ClanURI({self})"

View File

@ -208,8 +208,7 @@ def complete_secrets(
""" """
Provides completion functionality for clan secrets Provides completion functionality for clan secrets
""" """
from pathlib import Path from .clan_uri import FlakeId
from .secrets.secrets import ListSecretsOptions, list_secrets from .secrets.secrets import ListSecretsOptions, list_secrets
if (clan_dir_result := clan_dir(None)) is not None: if (clan_dir_result := clan_dir(None)) is not None:
@ -218,11 +217,11 @@ def complete_secrets(
flake = "." flake = "."
options = ListSecretsOptions( options = ListSecretsOptions(
flake=Path(flake), flake=FlakeId(flake),
pattern=None, pattern=None,
) )
secrets = list_secrets(options.flake, options.pattern) secrets = list_secrets(options.flake.path, options.pattern)
secrets_dict = {name: "secret" for name in secrets} secrets_dict = {name: "secret" for name in secrets}
return secrets_dict return secrets_dict

View File

@ -57,10 +57,13 @@ class ClanError(Exception):
self.description = description self.description = description
self.location = location or "Unknown location" self.location = location or "Unknown location"
self.msg = msg or "" self.msg = msg or ""
exception_msg = ""
if location:
exception_msg += f"{location}: \n"
exception_msg += self.msg
if self.description: if self.description:
exception_msg = f"{self.location}: \n{self.msg} - {self.description}" exception_msg = f" - {self.description}"
else:
exception_msg = f"{self.location}: \n{self.msg}"
super().__init__(exception_msg) super().__init__(exception_msg)

View File

@ -2,6 +2,7 @@ import argparse
import importlib import importlib
import logging import logging
from ..clan_uri import FlakeId
from ..completions import add_dynamic_completer, complete_machines from ..completions import add_dynamic_completer, complete_machines
from ..machines.machines import Machine from ..machines.machines import Machine
@ -49,7 +50,7 @@ def check_secrets(machine: Machine, service: None | str = None) -> bool:
def check_command(args: argparse.Namespace) -> None: def check_command(args: argparse.Namespace) -> None:
machine = Machine( machine = Machine(
name=args.machine, name=args.machine,
flake=args.flake, flake=FlakeId(args.flake),
) )
check_secrets(machine, service=args.service) check_secrets(machine, service=args.service)

View File

@ -3,6 +3,7 @@ import importlib
import json import json
import logging import logging
from ..clan_uri import FlakeId
from ..completions import add_dynamic_completer, complete_machines from ..completions import add_dynamic_completer, complete_machines
from ..machines.machines import Machine from ..machines.machines import Machine
@ -26,7 +27,7 @@ def get_all_facts(machine: Machine) -> dict:
def get_command(args: argparse.Namespace) -> None: def get_command(args: argparse.Namespace) -> None:
machine = Machine(name=args.machine, flake=args.flake) machine = Machine(name=args.machine, flake=FlakeId(args.flake))
# the raw_facts are bytestrings making them not json serializable # the raw_facts are bytestrings making them not json serializable
raw_facts = get_all_facts(machine) raw_facts = get_all_facts(machine)

View File

@ -12,9 +12,13 @@ class FactStore(FactStoreBase):
self.works_remotely = False self.works_remotely = False
def set(self, service: str, name: str, value: bytes) -> Path | None: def set(self, service: str, name: str, value: bytes) -> Path | None:
if isinstance(self.machine.flake, Path): if self.machine.flake.is_local():
fact_path = ( fact_path = (
self.machine.flake / "machines" / self.machine.name / "facts" / name self.machine.flake.path
/ "machines"
/ self.machine.name
/ "facts"
/ name
) )
fact_path.parent.mkdir(parents=True, exist_ok=True) fact_path.parent.mkdir(parents=True, exist_ok=True)
fact_path.touch() fact_path.touch()

View File

@ -4,6 +4,7 @@ import logging
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from ..clan_uri import FlakeId
from ..cmd import Log, run from ..cmd import Log, run
from ..completions import add_dynamic_completer, complete_machines from ..completions import add_dynamic_completer, complete_machines
from ..machines.machines import Machine from ..machines.machines import Machine
@ -44,7 +45,7 @@ def upload_secrets(machine: Machine) -> None:
def upload_command(args: argparse.Namespace) -> None: def upload_command(args: argparse.Namespace) -> None:
machine = Machine(name=args.machine, flake=args.flake) machine = Machine(name=args.machine, flake=FlakeId(args.flake))
upload_secrets(machine) upload_secrets(machine)

View File

@ -11,6 +11,7 @@ from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Any from typing import Any
from .clan_uri import FlakeId
from .cmd import Log, run from .cmd import Log, run
from .completions import add_dynamic_completer, complete_machines from .completions import add_dynamic_completer, complete_machines
from .errors import ClanError from .errors import ClanError
@ -87,7 +88,7 @@ def flash_machine(
@dataclass @dataclass
class FlashOptions: class FlashOptions:
flake: Path flake: FlakeId
machine: str machine: str
disks: dict[str, str] disks: dict[str, str]
ssh_keys_path: list[Path] ssh_keys_path: list[Path]
@ -119,7 +120,7 @@ class AppendDiskAction(argparse.Action):
def flash_command(args: argparse.Namespace) -> None: def flash_command(args: argparse.Namespace) -> None:
opts = FlashOptions( opts = FlashOptions(
flake=args.flake, flake=FlakeId(args.flake),
machine=args.machine, machine=args.machine,
disks=args.disk, disks=args.disk,
ssh_keys_path=args.ssh_pubkey, ssh_keys_path=args.ssh_pubkey,

View File

@ -7,6 +7,7 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from ..clan_uri import FlakeId
from ..cmd import Log, run from ..cmd import Log, run
from ..completions import add_dynamic_completer, complete_machines from ..completions import add_dynamic_completer, complete_machines
from ..facts.generate import generate_facts from ..facts.generate import generate_facts
@ -90,7 +91,7 @@ def install_nixos(
@dataclass @dataclass
class InstallOptions: class InstallOptions:
flake: Path flake: FlakeId
machine: str machine: str
target_host: str target_host: str
kexec: str | None kexec: str | None
@ -123,7 +124,7 @@ def install_command(args: argparse.Namespace) -> None:
password = None password = None
opts = InstallOptions( opts = InstallOptions(
flake=args.flake, flake=FlakeId(args.flake),
machine=args.machine, machine=args.machine,
target_host=target_host, target_host=target_host,
kexec=args.kexec, kexec=args.kexec,

View File

@ -1,6 +1,7 @@
import json import json
from pathlib import Path from pathlib import Path
from ..clan_uri import FlakeId
from ..cmd import run from ..cmd import run
from ..nix import nix_build, nix_config from ..nix import nix_build, nix_config
from .machines import Machine from .machines import Machine
@ -21,8 +22,8 @@ def get_all_machines(flake_dir: Path, nix_options: list[str]) -> list[Machine]:
machines.append( machines.append(
Machine( Machine(
name=name, name=name,
flake=flake_dir, flake=FlakeId(flake_dir),
deployment_info=machine_data, cached_deployment=machine_data,
nix_options=nix_options, nix_options=nix_options,
) )
) )
@ -34,5 +35,7 @@ def get_selected_machines(
) -> list[Machine]: ) -> list[Machine]:
machines = [] machines = []
for name in machine_names: for name in machine_names:
machines.append(Machine(name=name, flake=flake_dir, nix_options=nix_options)) machines.append(
Machine(name=name, flake=FlakeId(flake_dir), nix_options=nix_options)
)
return machines return machines

View File

@ -1,10 +1,11 @@
import json import json
import logging import logging
from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import Any from typing import Any
from clan_cli.clan_uri import ClanURI, FlakeId from clan_cli.clan_uri import FlakeId
from ..cmd import run_no_stdout from ..cmd import run_no_stdout
from ..errors import ClanError from ..errors import ClanError
@ -14,63 +15,41 @@ from ..ssh import Host, parse_deployment_address
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@dataclass
class Machine: class Machine:
name: str name: str
flake: FlakeId flake: FlakeId
nix_options: list[str] nix_options: list[str] = field(default_factory=list)
eval_cache: dict[str, str] cached_deployment: None | dict = None
build_cache: dict[str, Path]
_flake_path: Path | None
_deployment_info: None | dict
def __init__( _eval_cache: dict[str, str] = field(default_factory=dict)
self, _build_cache: dict[str, Path] = field(default_factory=dict)
name: str,
flake: Path | str,
deployment_info: dict | None = None,
nix_options: list[str] = [],
) -> None:
"""
Creates a Machine
@name: the name of the machine
@clan_dir: the directory of the clan, optional, if not set it will be determined from the current working directory
@machine_json: can be optionally used to skip evaluation of the machine, location of the json file with machine data
"""
uri = ClanURI.from_str(str(flake), name)
self.flake_id = uri.flake_id
self.name: str = uri.machine_name
self.eval_cache: dict[str, str] = {}
self.build_cache: dict[str, Path] = {}
self._flake_path: Path | None = None
self._deployment_info: None | dict = deployment_info
self.nix_options = nix_options
def flush_caches(self) -> None: def flush_caches(self) -> None:
self._deployment_info = None self.cached_deployment = None
self._flake_path = None self._build_cache.clear()
self.build_cache.clear() self._eval_cache.clear()
self.eval_cache.clear()
def __str__(self) -> str: def __str__(self) -> str:
return f"Machine(name={self.name}, flake={self.flake_id})" return f"Machine(name={self.name}, flake={self.flake})"
def __repr__(self) -> str: def __repr__(self) -> str:
return str(self) return str(self)
@property @property
def deployment_info(self) -> dict: def deployment(self) -> dict:
if self._deployment_info is not None: if self.cached_deployment is not None:
return self._deployment_info return self.cached_deployment
self._deployment_info = json.loads( deployment = json.loads(
self.build_nix("config.system.clan.deployment.file").read_text() self.build_nix("config.system.clan.deployment.file").read_text()
) )
return self._deployment_info self.cached_deployment = deployment
return deployment
@property @property
def target_host_address(self) -> str: def target_host_address(self) -> str:
# deploymentAddress is deprecated. # deploymentAddress is deprecated.
val = self.deployment_info.get("targetHost") or self.deployment_info.get( val = self.deployment.get("targetHost") or self.deployment.get(
"deploymentAddress" "deploymentAddress"
) )
if val is None: if val is None:
@ -80,40 +59,34 @@ class Machine:
@target_host_address.setter @target_host_address.setter
def target_host_address(self, value: str) -> None: def target_host_address(self, value: str) -> None:
self.deployment_info["targetHost"] = value self.deployment["targetHost"] = value
@property @property
def secret_facts_module(self) -> str: def secret_facts_module(self) -> str:
return self.deployment_info["facts"]["secretModule"] return self.deployment["facts"]["secretModule"]
@property @property
def public_facts_module(self) -> str: def public_facts_module(self) -> str:
return self.deployment_info["facts"]["publicModule"] return self.deployment["facts"]["publicModule"]
@property @property
def facts_data(self) -> dict[str, dict[str, Any]]: def facts_data(self) -> dict[str, dict[str, Any]]:
if self.deployment_info["facts"]["services"]: if self.deployment["facts"]["services"]:
return self.deployment_info["facts"]["services"] return self.deployment["facts"]["services"]
return {} return {}
@property @property
def secrets_upload_directory(self) -> str: def secrets_upload_directory(self) -> str:
return self.deployment_info["facts"]["secretUploadDirectory"] return self.deployment["facts"]["secretUploadDirectory"]
@property @property
def flake_dir(self) -> Path: def flake_dir(self) -> Path:
if self._flake_path: if self.flake.is_local():
return self._flake_path return self.flake.path
elif self.flake.is_remote():
if self.flake_id.is_local(): return Path(nix_metadata(self.flake.url)["path"])
self._flake_path = self.flake_id.path
elif self.flake_id.is_remote():
self._flake_path = Path(nix_metadata(self.flake_id.url)["path"])
else: else:
raise ClanError(f"Unsupported flake url: {self.flake_id}") raise ClanError(f"Unsupported flake url: {self.flake}")
assert self._flake_path is not None
return self._flake_path
@property @property
def target_host(self) -> Host: def target_host(self) -> Host:
@ -127,7 +100,7 @@ class Machine:
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.
""" """
build_host = self.deployment_info.get("buildHost") build_host = self.deployment.get("buildHost")
if build_host is None: if build_host is None:
return self.target_host return self.target_host
# enable ssh agent forwarding to allow the build host to access the target host # enable ssh agent forwarding to allow the build host to access the target host
@ -230,12 +203,12 @@ class Machine:
eval a nix attribute of the machine eval a nix attribute of the machine
@attr: the attribute to get @attr: the attribute to get
""" """
if attr in self.eval_cache and not refresh and extra_config is None: if attr in self._eval_cache and not refresh and extra_config is None:
return self.eval_cache[attr] return self._eval_cache[attr]
output = self.nix("eval", attr, extra_config, impure, nix_options) output = self.nix("eval", attr, extra_config, impure, nix_options)
if isinstance(output, str): if isinstance(output, str):
self.eval_cache[attr] = output self._eval_cache[attr] = output
return output return output
else: else:
raise ClanError("eval_nix returned not a string") raise ClanError("eval_nix returned not a string")
@ -253,12 +226,12 @@ class Machine:
@attr: the attribute to get @attr: the attribute to get
""" """
if attr in self.build_cache and not refresh and extra_config is None: if attr in self._build_cache and not refresh and extra_config is None:
return self.build_cache[attr] return self._build_cache[attr]
output = self.nix("build", attr, extra_config, impure, nix_options) output = self.nix("build", attr, extra_config, impure, nix_options)
if isinstance(output, Path): if isinstance(output, Path):
self.build_cache[attr] = output self._build_cache[attr] = output
return output return output
else: else:
raise ClanError("build_nix returned not a Path") raise ClanError("build_nix returned not a Path")

View File

@ -5,6 +5,7 @@ import os
import shlex import shlex
import sys import sys
from ..clan_uri import FlakeId
from ..cmd import run from ..cmd import run
from ..completions import add_dynamic_completer, complete_machines from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError from ..errors import ClanError
@ -80,7 +81,7 @@ def upload_sources(
) )
def deploy_nixos(machines: MachineGroup) -> None: def deploy_machine(machines: MachineGroup) -> None:
""" """
Deploy to all hosts in parallel Deploy to all hosts in parallel
""" """
@ -137,7 +138,7 @@ def update(args: argparse.Namespace) -> None:
machines = [] machines = []
if len(args.machines) == 1 and args.target_host is not None: if len(args.machines) == 1 and args.target_host is not None:
machine = Machine( machine = Machine(
name=args.machines[0], flake=args.flake, nix_options=args.option name=args.machines[0], flake=FlakeId(args.flake), nix_options=args.option
) )
machine.target_host_address = args.target_host machine.target_host_address = args.target_host
machines.append(machine) machines.append(machine)
@ -149,7 +150,7 @@ def update(args: argparse.Namespace) -> None:
if len(args.machines) == 0: if len(args.machines) == 0:
ignored_machines = [] ignored_machines = []
for machine in get_all_machines(args.flake, args.option): for machine in get_all_machines(args.flake, args.option):
if machine.deployment_info.get("requireExplicitUpdate", False): if machine.deployment.get("requireExplicitUpdate", False):
continue continue
try: try:
machine.build_host machine.build_host
@ -170,7 +171,7 @@ def update(args: argparse.Namespace) -> None:
else: else:
machines = get_selected_machines(args.flake, args.option, args.machines) machines = get_selected_machines(args.flake, args.option, args.machines)
deploy_nixos(MachineGroup(machines)) deploy_machine(MachineGroup(machines))
def register_update_parser(parser: argparse.ArgumentParser) -> None: def register_update_parser(parser: argparse.ArgumentParser) -> None:
@ -190,4 +191,9 @@ def register_update_parser(parser: argparse.ArgumentParser) -> None:
type=str, type=str,
help="address of the machine to update, in the format of user@host:1234", help="address of the machine to update, in the format of user@host:1234",
) )
parser.add_argument(
"--darwin",
type=str,
help="Hack to deploy darwin machines. This will be removed in the future when we have full darwin integration.",
)
parser.set_defaults(func=update) parser.set_defaults(func=update)

View File

@ -9,6 +9,7 @@ from pathlib import Path
from typing import IO from typing import IO
from .. import tty from .. import tty
from ..clan_uri import FlakeId
from ..completions import ( from ..completions import (
add_dynamic_completer, add_dynamic_completer,
complete_groups, complete_groups,
@ -263,16 +264,16 @@ def list_secrets(flake_dir: Path, pattern: str | None = None) -> list[str]:
@dataclass @dataclass
class ListSecretsOptions: class ListSecretsOptions:
flake: Path flake: FlakeId
pattern: str | None pattern: str | None
def list_command(args: argparse.Namespace) -> None: def list_command(args: argparse.Namespace) -> None:
options = ListSecretsOptions( options = ListSecretsOptions(
flake=args.flake, flake=FlakeId(args.flake),
pattern=args.pattern, pattern=args.pattern,
) )
lst = list_secrets(options.flake, options.pattern) lst = list_secrets(options.flake.path, options.pattern)
if len(lst) > 0: if len(lst) > 0:
print("\n".join(lst)) print("\n".join(lst))

View File

@ -3,6 +3,7 @@ import json
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from ..clan_uri import FlakeId
from ..completions import add_dynamic_completer, complete_machines from ..completions import add_dynamic_completer, complete_machines
from ..machines.machines import Machine from ..machines.machines import Machine
@ -12,7 +13,7 @@ class VmConfig:
machine_name: str machine_name: str
machine_icon: Path machine_icon: Path
machine_description: str machine_description: str
flake_url: str | Path flake_url: FlakeId
clan_name: str clan_name: str
cores: int cores: int
@ -23,19 +24,19 @@ class VmConfig:
def inspect_vm(machine: Machine) -> VmConfig: def inspect_vm(machine: Machine) -> VmConfig:
data = json.loads(machine.eval_nix("config.clan.core.vm.inspect")) data = json.loads(machine.eval_nix("config.clan.core.vm.inspect"))
return VmConfig(flake_url=str(machine.flake), **data) return VmConfig(flake_url=machine.flake, **data)
@dataclass @dataclass
class InspectOptions: class InspectOptions:
machine: str machine: str
flake: Path flake: FlakeId
def inspect_command(args: argparse.Namespace) -> None: def inspect_command(args: argparse.Namespace) -> None:
inspect_options = InspectOptions( inspect_options = InspectOptions(
machine=args.machine, machine=args.machine,
flake=args.flake or Path.cwd(), flake=FlakeId(args.flake or Path.cwd()),
) )
machine = Machine(inspect_options.machine, inspect_options.flake) machine = Machine(inspect_options.machine, inspect_options.flake)

View File

@ -7,6 +7,7 @@ from contextlib import ExitStack
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from ..clan_uri import FlakeId
from ..cmd import Log, run from ..cmd import Log, run
from ..completions import add_dynamic_completer, complete_machines from ..completions import add_dynamic_completer, complete_machines
from ..dirs import module_root, user_cache_dir, vm_state_dir from ..dirs import module_root, user_cache_dir, vm_state_dir
@ -198,7 +199,7 @@ def run_command(
option: list[str] = [], option: list[str] = [],
**kwargs: dict[str, str], **kwargs: dict[str, str],
) -> None: ) -> None:
machine_obj: Machine = Machine(machine, flake) machine_obj: Machine = Machine(machine, FlakeId(flake))
vm: VmConfig = inspect_vm(machine=machine_obj) vm: VmConfig = inspect_vm(machine=machine_obj)

View File

@ -21,13 +21,13 @@ def test_get_url() -> None:
def test_local_uri() -> None: def test_local_uri() -> None:
# Create a ClanURI object from a local URI # Create a ClanURI object from a local URI
uri = ClanURI("clan://file:///home/user/Downloads") uri = ClanURI("clan://file:///home/user/Downloads")
assert uri.flake_id.path == Path("/home/user/Downloads") assert uri.flake.path == Path("/home/user/Downloads")
def test_is_remote() -> None: def test_is_remote() -> None:
# Create a ClanURI object from a remote URI # Create a ClanURI object from a remote URI
uri = ClanURI("clan://https://example.com") uri = ClanURI("clan://https://example.com")
assert uri.flake_id.url == "https://example.com" assert uri.flake.url == "https://example.com"
def test_direct_local_path() -> None: def test_direct_local_path() -> None:
@ -47,35 +47,35 @@ def test_remote_with_clanparams() -> None:
uri = ClanURI("clan://https://example.com") uri = ClanURI("clan://https://example.com")
assert uri.machine_name == "defaultVM" assert uri.machine_name == "defaultVM"
assert uri.flake_id.url == "https://example.com" assert uri.flake.url == "https://example.com"
def test_from_str_remote() -> None: def test_from_str_remote() -> None:
uri = ClanURI.from_str(url="https://example.com", machine_name="myVM") uri = ClanURI.from_str(url="https://example.com", machine_name="myVM")
assert uri.get_url() == "https://example.com" assert uri.get_url() == "https://example.com"
assert uri.machine_name == "myVM" assert uri.machine_name == "myVM"
assert uri.flake_id.url == "https://example.com" assert uri.flake.url == "https://example.com"
def test_from_str_local() -> None: def test_from_str_local() -> None:
uri = ClanURI.from_str(url="~/Projects/democlan", machine_name="myVM") uri = ClanURI.from_str(url="~/Projects/democlan", machine_name="myVM")
assert uri.get_url().endswith("/Projects/democlan") assert uri.get_url().endswith("/Projects/democlan")
assert uri.machine_name == "myVM" assert uri.machine_name == "myVM"
assert uri.flake_id.is_local() assert uri.flake.is_local()
assert str(uri.flake_id).endswith("/Projects/democlan") # type: ignore assert str(uri.flake).endswith("/Projects/democlan") # type: ignore
def test_from_str_local_no_machine() -> None: def test_from_str_local_no_machine() -> None:
uri = ClanURI.from_str("~/Projects/democlan") uri = ClanURI.from_str("~/Projects/democlan")
assert uri.get_url().endswith("/Projects/democlan") assert uri.get_url().endswith("/Projects/democlan")
assert uri.machine_name == "defaultVM" assert uri.machine_name == "defaultVM"
assert uri.flake_id.is_local() assert uri.flake.is_local()
assert str(uri.flake_id).endswith("/Projects/democlan") # type: ignore assert str(uri.flake).endswith("/Projects/democlan") # type: ignore
def test_from_str_local_no_machine2() -> None: def test_from_str_local_no_machine2() -> None:
uri = ClanURI.from_str("~/Projects/democlan#syncthing-peer1") uri = ClanURI.from_str("~/Projects/democlan#syncthing-peer1")
assert uri.get_url().endswith("/Projects/democlan") assert uri.get_url().endswith("/Projects/democlan")
assert uri.machine_name == "syncthing-peer1" assert uri.machine_name == "syncthing-peer1"
assert uri.flake_id.is_local() assert uri.flake.is_local()
assert str(uri.flake_id).endswith("/Projects/democlan") # type: ignore assert str(uri.flake).endswith("/Projects/democlan") # type: ignore

View File

@ -6,7 +6,6 @@ from cli import Cli
from fixtures_flakes import FlakeForTest from fixtures_flakes import FlakeForTest
from pytest import CaptureFixture from pytest import CaptureFixture
from clan_cli.clan_uri import ClanURI
from clan_cli.dirs import user_history_file from clan_cli.dirs import user_history_file
from clan_cli.history.add import HistoryEntry from clan_cli.history.add import HistoryEntry
@ -19,11 +18,10 @@ def test_history_add(
test_flake_with_core: FlakeForTest, test_flake_with_core: FlakeForTest,
) -> None: ) -> None:
cli = Cli() cli = Cli()
uri = ClanURI.from_str(str(test_flake_with_core.path), "vm1")
cmd = [ cmd = [
"history", "history",
"add", "add",
str(uri), f"clan://{test_flake_with_core.path}#vm1",
] ]
cli.run(cmd) cli.run(cmd)
@ -39,7 +37,6 @@ def test_history_list(
test_flake_with_core: FlakeForTest, test_flake_with_core: FlakeForTest,
) -> None: ) -> None:
cli = Cli() cli = Cli()
uri = ClanURI.from_str(str(test_flake_with_core.path), "vm1")
cmd = [ cmd = [
"history", "history",
"list", "list",
@ -48,6 +45,6 @@ def test_history_list(
cli.run(cmd) cli.run(cmd)
assert str(test_flake_with_core.path) not in capsys.readouterr().out assert str(test_flake_with_core.path) not in capsys.readouterr().out
cli.run(["history", "add", str(uri)]) cli.run(["history", "add", f"clan://{test_flake_with_core.path}#vm1"])
cli.run(cmd) cli.run(cmd)
assert str(test_flake_with_core.path) in capsys.readouterr().out assert str(test_flake_with_core.path) in capsys.readouterr().out

View File

@ -6,6 +6,7 @@ from cli import Cli
from fixtures_flakes import FlakeForTest from fixtures_flakes import FlakeForTest
from validator import is_valid_age_key, is_valid_ssh_key from validator import is_valid_age_key, is_valid_ssh_key
from clan_cli.clan_uri import FlakeId
from clan_cli.facts.secret_modules.sops import SecretStore from clan_cli.facts.secret_modules.sops import SecretStore
from clan_cli.machines.facts import machine_get_fact from clan_cli.machines.facts import machine_get_fact
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
@ -48,7 +49,7 @@ def test_generate_secret(
) )
cmd = ["facts", "generate", "--flake", str(test_flake_with_core.path), "vm1"] cmd = ["facts", "generate", "--flake", str(test_flake_with_core.path), "vm1"]
cli.run(cmd) cli.run(cmd)
store1 = SecretStore(Machine(name="vm1", flake=test_flake_with_core.path)) store1 = SecretStore(Machine(name="vm1", flake=FlakeId(test_flake_with_core.path)))
assert store1.exists("", "age.key") assert store1.exists("", "age.key")
assert store1.exists("", "zerotier-identity-secret") assert store1.exists("", "zerotier-identity-secret")
@ -76,7 +77,7 @@ def test_generate_secret(
secrets_folder / "vm1-zerotier-identity-secret" / "machines" / "vm1" secrets_folder / "vm1-zerotier-identity-secret" / "machines" / "vm1"
).exists() ).exists()
store2 = SecretStore(Machine(name="vm2", flake=test_flake_with_core.path)) store2 = SecretStore(Machine(name="vm2", flake=FlakeId(test_flake_with_core.path)))
assert store2.exists("", "password") assert store2.exists("", "password")
assert store2.exists("", "password-hash") assert store2.exists("", "password-hash")

View File

@ -6,6 +6,7 @@ from cli import Cli
from fixtures_flakes import FlakeForTest from fixtures_flakes import FlakeForTest
from validator import is_valid_ssh_key from validator import is_valid_ssh_key
from clan_cli.clan_uri import FlakeId
from clan_cli.facts.secret_modules.password_store import SecretStore from clan_cli.facts.secret_modules.password_store import SecretStore
from clan_cli.machines.facts import machine_get_fact from clan_cli.machines.facts import machine_get_fact
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
@ -48,7 +49,9 @@ def test_upload_secret(
) )
cli.run(["facts", "generate", "vm1"]) cli.run(["facts", "generate", "vm1"])
store = SecretStore(Machine(name="vm1", flake=test_flake_with_core_and_pass.path)) store = SecretStore(
Machine(name="vm1", flake=FlakeId(test_flake_with_core_and_pass.path))
)
network_id = machine_get_fact( network_id = machine_get_fact(
test_flake_with_core_and_pass.path, "vm1", "zerotier-network-id" test_flake_with_core_and_pass.path, "vm1", "zerotier-network-id"