From 94caea382fa4a85bec3a395df33bfb265bf572e4 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Wed, 6 Mar 2024 13:11:49 +0100 Subject: [PATCH 01/63] fix: typos --- clanModules/syncthing.nix | 2 +- docs/admins/secrets-management.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/clanModules/syncthing.nix b/clanModules/syncthing.nix index 8b70bc06..fc7c4bc5 100644 --- a/clanModules/syncthing.nix +++ b/clanModules/syncthing.nix @@ -62,7 +62,7 @@ } ]; - # Activates inofify compatibilty on syncthing + # Activates inofify compatibility on syncthing boot.kernel.sysctl."fs.inotify.max_user_watches" = lib.mkDefault 524288; services.syncthing = { diff --git a/docs/admins/secrets-management.md b/docs/admins/secrets-management.md index 658abb3e..dad7aa5f 100644 --- a/docs/admins/secrets-management.md +++ b/docs/admins/secrets-management.md @@ -160,7 +160,7 @@ examples. `clan secrets` stores each secrets in a single file, whereas [sops](https://github.com/Mic92/sops-nix) commonly allows to put all secrets in a yaml or json documents. -If you already happend to use sops-nix, you can migrate by using the `clan secrets import-sops` command by importing these documents: +If you already happened to use sops-nix, you can migrate by using the `clan secrets import-sops` command by importing these documents: ```shellSession % clan secrets import-sops --prefix matchbox- --group admins --machine matchbox nixos/matchbox/secrets/secrets.yaml From 93c868a3b7067f11885afab589c05e141efe6fbf Mon Sep 17 00:00:00 2001 From: Qubasa Date: Thu, 7 Mar 2024 02:24:36 +0700 Subject: [PATCH 02/63] clan_cli: Rewrite ClanURI --- pkgs/clan-cli/clan_cli/clan_uri.py | 185 ++++++++++------------- pkgs/clan-cli/clan_cli/history/update.py | 6 +- pkgs/clan-cli/tests/test_clan_uri.py | 114 +++----------- 3 files changed, 107 insertions(+), 198 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/clan_uri.py b/pkgs/clan-cli/clan_cli/clan_uri.py index 184f9c9f..032a4922 100644 --- a/pkgs/clan-cli/clan_cli/clan_uri.py +++ b/pkgs/clan-cli/clan_cli/clan_uri.py @@ -5,13 +5,13 @@ import urllib.request from dataclasses import dataclass from enum import Enum, member from pathlib import Path -from typing import Any, Self +from typing import Any from .errors import ClanError # Define an enum with different members that have different values -class ClanScheme(Enum): +class ClanUrl(Enum): # Use the dataclass decorator to add fields and methods to the members @member @dataclass @@ -33,137 +33,116 @@ class ClanScheme(Enum): # Parameters defined here will be DELETED from the nested uri # so make sure there are no conflicts with other webservices @dataclass -class ClanParameters: - flake_attr: str = "defaultVM" +class MachineParams: + dummy_opt: str = "dummy" + + +@dataclass +class MachineData: + name: str = "defaultVM" + params: MachineParams = dataclasses.field(default_factory=MachineParams) # Define the ClanURI class class ClanURI: + _orig_uri: str + _nested_uri: str + _components: urllib.parse.ParseResult + url: ClanUrl + machines: list[MachineData] + # Initialize the class with a clan:// URI def __init__(self, uri: str) -> None: + self.machines = [] + # users might copy whitespace along with the uri uri = uri.strip() - self._full_uri = uri + self._orig_uri = uri # Check if the URI starts with clan:// # If it does, remove the clan:// prefix if uri.startswith("clan://"): self._nested_uri = uri[7:] else: - raise ClanError(f"Invalid scheme: expected clan://, got {uri}") + raise ClanError(f"Invalid uri: expected clan://, got {uri}") # Parse the URI into components - # scheme://netloc/path;parameters?query#fragment + # url://netloc/path;parameters?query#fragment self._components = urllib.parse.urlparse(self._nested_uri) - # Parse the query string into a dictionary - query = urllib.parse.parse_qs(self._components.query) + # Replace the query string in the components with the new query string + clean_comps = self._components._replace( + query=self._components.query, fragment="" + ) - # Create a new dictionary with only the parameters we want - # example: https://example.com?flake_attr=myVM&password=1234 - # becomes: https://example.com?password=1234 - # clan_params = {"flake_attr": "myVM"} - # query = {"password": ["1234"]} - clan_params: dict[str, str] = {} - for field in dataclasses.fields(ClanParameters): - if field.name in query: - values = query[field.name] + # Parse the URL into a ClanUrl object + self.url = self._parse_url(clean_comps) + + # Parse the fragment into a list of machine queries + # Then parse every machine query into a MachineParameters object + machine_frags = list( + filter(lambda x: len(x) > 0, self._components.fragment.split("#")) + ) + for machine_frag in machine_frags: + machine = self._parse_machine_query(machine_frag) + self.machines.append(machine) + + # If there are no machine fragments, add a default machine + if len(machine_frags) == 0: + self.machines.append(MachineData()) + + def _parse_url(self, comps: urllib.parse.ParseResult) -> ClanUrl: + comb = ( + comps.scheme, + comps.netloc, + comps.path, + comps.params, + comps.query, + comps.fragment, + ) + match comb: + case ("file", "", path, "", "", _) | ("", "", path, "", "", _): # type: ignore + url = ClanUrl.LOCAL.value(Path(path).expanduser().resolve()) # type: ignore + case _: + url = ClanUrl.REMOTE.value(comps.geturl()) # type: ignore + + return url + + def _parse_machine_query(self, machine_frag: str) -> MachineData: + comp = urllib.parse.urlparse(machine_frag) + query = urllib.parse.parse_qs(comp.query) + machine_name = comp.path + + machine_params: dict[str, Any] = {} + for dfield in dataclasses.fields(MachineParams): + if dfield.name in query: + values = query[dfield.name] if len(values) > 1: - raise ClanError(f"Multiple values for parameter: {field.name}") - clan_params[field.name] = values[0] + raise ClanError(f"Multiple values for parameter: {dfield.name}") + machine_params[dfield.name] = values[0] # Remove the field from the query dictionary # clan uri and nested uri share one namespace for query parameters # we need to make sure there are no conflicts - del query[field.name] - # Reencode the query dictionary into a query string - real_query = urllib.parse.urlencode(query, doseq=True) + del query[dfield.name] + params = MachineParams(**machine_params) + machine = MachineData(name=machine_name, params=params) + return machine - # If the fragment contains a #, use the part after the # as the flake_attr - # on multiple #, use the first one - if self._components.fragment != "": - clan_params["flake_attr"] = self._components.fragment.split("#")[0] + def get_orig_uri(self) -> str: + return self._orig_uri - # Replace the query string in the components with the new query string - self._components = self._components._replace(query=real_query, fragment="") - - # Create a ClanParameters object from the clan_params dictionary - self.params = ClanParameters(**clan_params) - - comb = ( - self._components.scheme, - self._components.netloc, - self._components.path, - self._components.params, - self._components.query, - self._components.fragment, - ) - match comb: - case ("file", "", path, "", "", "") | ("", "", path, "", "", _): # type: ignore - self.scheme = ClanScheme.LOCAL.value(Path(path).expanduser().resolve()) # type: ignore - case _: - self.scheme = ClanScheme.REMOTE.value(self._components.geturl()) # type: ignore - - def get_internal(self) -> str: - match self.scheme: - case ClanScheme.LOCAL.value(path): + def get_url(self) -> str: + match self.url: + case ClanUrl.LOCAL.value(path): return str(path) - case ClanScheme.REMOTE.value(url): + case ClanUrl.REMOTE.value(url): return url case _: - raise ClanError(f"Unsupported uri components: {self.scheme}") - - def get_full_uri(self) -> str: - return self._full_uri - - def get_id(self) -> str: - return f"{self.get_internal()}#{self.params.flake_attr}" - - @classmethod - def from_path( - cls, # noqa - path: Path, - flake_attr: str | None = None, - params: dict[str, Any] | ClanParameters | None = None, - ) -> Self: - return cls.from_str(str(path), flake_attr=flake_attr, params=params) - - @classmethod - def from_str( - cls, # noqa - url: str, - flake_attr: str | None = None, - params: dict[str, Any] | ClanParameters | None = None, - ) -> Self: - if flake_attr is not None and params is not None: - raise ClanError("flake_attr and params are mutually exclusive") - - prefix = "clan://" - if url.startswith(prefix): - url = url[len(prefix) :] - - if params is None and flake_attr is None: - return cls(f"clan://{url}") - - comp = urllib.parse.urlparse(url) - query = urllib.parse.parse_qs(comp.query) - - if isinstance(params, dict): - query.update(params) - elif isinstance(params, ClanParameters): - query.update(params.__dict__) - elif flake_attr is not None: - query["flake_attr"] = [flake_attr] - else: - raise ClanError(f"Unsupported params type: {type(params)}") - - new_query = urllib.parse.urlencode(query, doseq=True) - comp = comp._replace(query=new_query) - new_url = urllib.parse.urlunparse(comp) - return cls(f"clan://{new_url}") + raise ClanError(f"Unsupported uri components: {self.url}") def __str__(self) -> str: - return self.get_full_uri() + return self.get_orig_uri() def __repr__(self) -> str: - return f"ClanURI({self.get_full_uri()})" + return f"ClanURI({self})" diff --git a/pkgs/clan-cli/clan_cli/history/update.py b/pkgs/clan-cli/clan_cli/history/update.py index 9b176dea..9b13cbe3 100644 --- a/pkgs/clan-cli/clan_cli/history/update.py +++ b/pkgs/clan-cli/clan_cli/history/update.py @@ -4,7 +4,7 @@ import datetime from clan_cli.flakes.inspect import inspect_flake -from ..clan_uri import ClanParameters, ClanURI +from ..clan_uri import ClanURI, MachineParams from ..errors import ClanCmdError from ..locked_open import write_history_file from ..nix import nix_metadata @@ -28,9 +28,9 @@ def update_history() -> list[HistoryEntry]: ) uri = ClanURI.from_str( url=str(entry.flake.flake_url), - params=ClanParameters(entry.flake.flake_attr), + params=MachineParams(machine_name=entry.flake.flake_attr), ) - flake = inspect_flake(uri.get_internal(), uri.params.flake_attr) + flake = inspect_flake(uri.get_url(), uri.machines[0].name) flake.flake_url = str(flake.flake_url) entry = HistoryEntry( flake=flake, last_used=datetime.datetime.now().isoformat() diff --git a/pkgs/clan-cli/tests/test_clan_uri.py b/pkgs/clan-cli/tests/test_clan_uri.py index 64cc4632..6cfb0402 100644 --- a/pkgs/clan-cli/tests/test_clan_uri.py +++ b/pkgs/clan-cli/tests/test_clan_uri.py @@ -1,28 +1,28 @@ from pathlib import Path -from clan_cli.clan_uri import ClanParameters, ClanScheme, ClanURI +from clan_cli.clan_uri import ClanURI, ClanUrl -def test_get_internal() -> None: +def test_get_url() -> None: # Create a ClanURI object from a remote URI with parameters - uri = ClanURI("clan://https://example.com?flake_attr=myVM&password=1234") - assert uri.get_internal() == "https://example.com?password=1234" + uri = ClanURI("clan://https://example.com?password=1234#myVM") + assert uri.get_url() == "https://example.com?password=1234" uri = ClanURI("clan://~/Downloads") - assert uri.get_internal().endswith("/Downloads") + assert uri.get_url().endswith("/Downloads") uri = ClanURI("clan:///home/user/Downloads") - assert uri.get_internal() == "/home/user/Downloads" + assert uri.get_url() == "/home/user/Downloads" uri = ClanURI("clan://file:///home/user/Downloads") - assert uri.get_internal() == "/home/user/Downloads" + assert uri.get_url() == "/home/user/Downloads" def test_local_uri() -> None: # Create a ClanURI object from a local URI uri = ClanURI("clan://file:///home/user/Downloads") - match uri.scheme: - case ClanScheme.LOCAL.value(path): + match uri.url: + case ClanUrl.LOCAL.value(path): assert path == Path("/home/user/Downloads") # type: ignore case _: assert False @@ -32,8 +32,8 @@ def test_is_remote() -> None: # Create a ClanURI object from a remote URI uri = ClanURI("clan://https://example.com") - match uri.scheme: - case ClanScheme.REMOTE.value(url): + match uri.url: + case ClanUrl.REMOTE.value(url): assert url == "https://example.com" # type: ignore case _: assert False @@ -42,104 +42,34 @@ def test_is_remote() -> None: def test_direct_local_path() -> None: # Create a ClanURI object from a remote URI uri = ClanURI("clan://~/Downloads") - assert uri.get_internal().endswith("/Downloads") + assert uri.get_url().endswith("/Downloads") def test_direct_local_path2() -> None: # Create a ClanURI object from a remote URI uri = ClanURI("clan:///home/user/Downloads") - assert uri.get_internal() == "/home/user/Downloads" + assert uri.get_url() == "/home/user/Downloads" def test_remote_with_clanparams() -> None: # Create a ClanURI object from a remote URI with parameters uri = ClanURI("clan://https://example.com") - assert uri.params.flake_attr == "defaultVM" + assert uri.machines[0].name == "defaultVM" - match uri.scheme: - case ClanScheme.REMOTE.value(url): + match uri.url: + case ClanUrl.REMOTE.value(url): assert url == "https://example.com" # type: ignore case _: assert False -def test_from_path_with_custom() -> None: - # Create a ClanURI object from a remote URI with parameters - uri_str = Path("/home/user/Downloads") - params = ClanParameters(flake_attr="myVM") - uri = ClanURI.from_path(uri_str, params=params) - assert uri.params.flake_attr == "myVM" - - match uri.scheme: - case ClanScheme.LOCAL.value(path): - assert path == Path("/home/user/Downloads") # type: ignore - case _: - assert False - - -def test_from_path_with_default() -> None: - # Create a ClanURI object from a remote URI with parameters - uri_str = Path("/home/user/Downloads") - params = ClanParameters() - uri = ClanURI.from_path(uri_str, params=params) - assert uri.params.flake_attr == "defaultVM" - - match uri.scheme: - case ClanScheme.LOCAL.value(path): - assert path == Path("/home/user/Downloads") # type: ignore - case _: - assert False - - -def test_from_str() -> None: - # Create a ClanURI object from a remote URI with parameters - uri_str = "https://example.com?password=asdasd&test=1234" - params = ClanParameters(flake_attr="myVM") - uri = ClanURI.from_str(url=uri_str, params=params) - assert uri.params.flake_attr == "myVM" - - match uri.scheme: - case ClanScheme.REMOTE.value(url): - assert url == "https://example.com?password=asdasd&test=1234" # type: ignore - case _: - assert False - - uri = ClanURI.from_str(url=uri_str, params={"flake_attr": "myVM"}) - assert uri.params.flake_attr == "myVM" - - uri = ClanURI.from_str(uri_str, "myVM") - assert uri.params.flake_attr == "myVM" - - uri_str = "~/Downloads/democlan" - params = ClanParameters(flake_attr="myVM") - uri = ClanURI.from_str(url=uri_str, params=params) - assert uri.params.flake_attr == "myVM" - assert uri.get_internal().endswith("/Downloads/democlan") - - uri_str = "~/Downloads/democlan" - uri = ClanURI.from_str(url=uri_str) - assert uri.params.flake_attr == "defaultVM" - assert uri.get_internal().endswith("/Downloads/democlan") - - uri_str = "clan://~/Downloads/democlan" - uri = ClanURI.from_str(url=uri_str) - assert uri.params.flake_attr == "defaultVM" - assert uri.get_internal().endswith("/Downloads/democlan") - - def test_remote_with_all_params() -> None: - # Create a ClanURI object from a remote URI with parameters - uri = ClanURI("clan://https://example.com?flake_attr=myVM&password=1234") - assert uri.params.flake_attr == "myVM" - - match uri.scheme: - case ClanScheme.REMOTE.value(url): - assert url == "https://example.com?password=1234" # type: ignore + uri = ClanURI("clan://https://example.com?password=12345#myVM#secondVM") + assert uri.machines[0].name == "myVM" + assert uri.machines[1].name == "secondVM" + match uri.url: + case ClanUrl.REMOTE.value(url): + assert url == "https://example.com?password=12345" # type: ignore case _: assert False - - -def test_with_hashtag() -> None: - uri = ClanURI("clan://https://example.com?flake_attr=thirdVM#myVM#secondVM") - assert uri.params.flake_attr == "myVM" From 442e5b45badbcee36dce0b5b8e488af43ff18f8b Mon Sep 17 00:00:00 2001 From: Qubasa Date: Thu, 7 Mar 2024 19:04:48 +0700 Subject: [PATCH 03/63] clan_cli: Simplify ClanURI --- pkgs/clan-cli/clan_cli/clan_uri.py | 54 +++++++++++----- pkgs/clan-cli/clan_cli/history/add.py | 12 ++-- pkgs/clan-cli/clan_cli/history/update.py | 6 +- pkgs/clan-cli/tests/test_clan_uri.py | 61 +++++++++++++++++-- pkgs/clan-cli/tests/test_history_cli.py | 8 +-- .../clan_vm_manager/components/vmobj.py | 6 +- .../clan_vm_manager/singletons/use_join.py | 6 +- .../clan_vm_manager/singletons/use_vms.py | 9 +-- .../clan_vm_manager/views/list.py | 8 +-- 9 files changed, 123 insertions(+), 47 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/clan_uri.py b/pkgs/clan-cli/clan_cli/clan_uri.py index 032a4922..20d53e4c 100644 --- a/pkgs/clan-cli/clan_cli/clan_uri.py +++ b/pkgs/clan-cli/clan_cli/clan_uri.py @@ -19,7 +19,10 @@ class ClanUrl(Enum): url: str # The url field holds the HTTP URL def __str__(self) -> str: - return f"REMOTE({self.url})" # The __str__ method returns a custom string representation + return f"{self.url}" # The __str__ method returns a custom string representation + + def __repr__(self) -> str: + return f"ClanUrl.REMOTE({self.url})" @member @dataclass @@ -27,7 +30,10 @@ class ClanUrl(Enum): path: Path # The path field holds the local path def __str__(self) -> str: - return f"LOCAL({self.path})" # The __str__ method returns a custom string representation + return f"{self.path}" # The __str__ method returns a custom string representation + + def __repr__(self) -> str: + return f"ClanUrl.LOCAL({self.path})" # Parameters defined here will be DELETED from the nested uri @@ -39,9 +45,13 @@ class MachineParams: @dataclass class MachineData: + url: ClanUrl name: str = "defaultVM" params: MachineParams = dataclasses.field(default_factory=MachineParams) + def get_id(self) -> str: + return f"{self.url}#{self.name}" + # Define the ClanURI class class ClanURI: @@ -49,11 +59,11 @@ class ClanURI: _nested_uri: str _components: urllib.parse.ParseResult url: ClanUrl - machines: list[MachineData] + _machines: list[MachineData] # Initialize the class with a clan:// URI def __init__(self, uri: str) -> None: - self.machines = [] + self._machines = [] # users might copy whitespace along with the uri uri = uri.strip() @@ -85,11 +95,12 @@ class ClanURI: ) for machine_frag in machine_frags: machine = self._parse_machine_query(machine_frag) - self.machines.append(machine) + self._machines.append(machine) # If there are no machine fragments, add a default machine if len(machine_frags) == 0: - self.machines.append(MachineData()) + default_machine = MachineData(url=self.url) + self._machines.append(default_machine) def _parse_url(self, comps: urllib.parse.ParseResult) -> ClanUrl: comb = ( @@ -126,20 +137,35 @@ class ClanURI: # we need to make sure there are no conflicts del query[dfield.name] params = MachineParams(**machine_params) - machine = MachineData(name=machine_name, params=params) + machine = MachineData(url=self.url, name=machine_name, params=params) return machine + @property + def machine(self) -> MachineData: + return self._machines[0] + def get_orig_uri(self) -> str: return self._orig_uri def get_url(self) -> str: - match self.url: - case ClanUrl.LOCAL.value(path): - return str(path) - case ClanUrl.REMOTE.value(url): - return url - case _: - raise ClanError(f"Unsupported uri components: {self.url}") + return str(self.url) + + @classmethod + def from_str( + cls, # noqa + url: str, + machine_name: str | None = None, + ) -> "ClanURI": + clan_uri = "" + if not url.startswith("clan://"): + clan_uri += "clan://" + + clan_uri += url + + if machine_name: + clan_uri += f"#{machine_name}" + + return cls(clan_uri) def __str__(self) -> str: return self.get_orig_uri() diff --git a/pkgs/clan-cli/clan_cli/history/add.py b/pkgs/clan-cli/clan_cli/history/add.py index 16bb9698..3b4b21dd 100644 --- a/pkgs/clan-cli/clan_cli/history/add.py +++ b/pkgs/clan-cli/clan_cli/history/add.py @@ -79,8 +79,8 @@ def new_history_entry(url: str, machine: str) -> HistoryEntry: def add_all_to_history(uri: ClanURI) -> list[HistoryEntry]: history = list_history() new_entries: list[HistoryEntry] = [] - for machine in list_machines(uri.get_internal()): - new_entry = _add_maschine_to_history_list(uri.get_internal(), machine, history) + for machine in list_machines(uri.get_url()): + new_entry = _add_maschine_to_history_list(uri.get_url(), machine, history) new_entries.append(new_entry) write_history_file(history) return new_entries @@ -89,9 +89,7 @@ def add_all_to_history(uri: ClanURI) -> list[HistoryEntry]: def add_history(uri: ClanURI) -> HistoryEntry: user_history_file().parent.mkdir(parents=True, exist_ok=True) history = list_history() - new_entry = _add_maschine_to_history_list( - uri.get_internal(), uri.params.flake_attr, history - ) + new_entry = _add_maschine_to_history_list(uri.get_url(), uri.machine.name, history) write_history_file(history) return new_entry @@ -121,9 +119,7 @@ def add_history_command(args: argparse.Namespace) -> None: # takes a (sub)parser and configures it def register_add_parser(parser: argparse.ArgumentParser) -> None: - parser.add_argument( - "uri", type=ClanURI.from_str, help="Path to the flake", default="." - ) + parser.add_argument("uri", type=ClanURI, help="Path to the flake", default=".") parser.add_argument( "--all", help="Add all machines", default=False, action="store_true" ) diff --git a/pkgs/clan-cli/clan_cli/history/update.py b/pkgs/clan-cli/clan_cli/history/update.py index 9b13cbe3..12ecf24e 100644 --- a/pkgs/clan-cli/clan_cli/history/update.py +++ b/pkgs/clan-cli/clan_cli/history/update.py @@ -4,7 +4,7 @@ import datetime from clan_cli.flakes.inspect import inspect_flake -from ..clan_uri import ClanURI, MachineParams +from ..clan_uri import ClanURI from ..errors import ClanCmdError from ..locked_open import write_history_file from ..nix import nix_metadata @@ -28,9 +28,9 @@ def update_history() -> list[HistoryEntry]: ) uri = ClanURI.from_str( url=str(entry.flake.flake_url), - params=MachineParams(machine_name=entry.flake.flake_attr), + machine_name=entry.flake.flake_attr, ) - flake = inspect_flake(uri.get_url(), uri.machines[0].name) + flake = inspect_flake(uri.get_url(), uri.machine.name) flake.flake_url = str(flake.flake_url) entry = HistoryEntry( flake=flake, last_used=datetime.datetime.now().isoformat() diff --git a/pkgs/clan-cli/tests/test_clan_uri.py b/pkgs/clan-cli/tests/test_clan_uri.py index 6cfb0402..f8c09cb7 100644 --- a/pkgs/clan-cli/tests/test_clan_uri.py +++ b/pkgs/clan-cli/tests/test_clan_uri.py @@ -55,7 +55,7 @@ def test_remote_with_clanparams() -> None: # Create a ClanURI object from a remote URI with parameters uri = ClanURI("clan://https://example.com") - assert uri.machines[0].name == "defaultVM" + assert uri.machine.name == "defaultVM" match uri.url: case ClanUrl.REMOTE.value(url): @@ -65,11 +65,64 @@ def test_remote_with_clanparams() -> None: def test_remote_with_all_params() -> None: - uri = ClanURI("clan://https://example.com?password=12345#myVM#secondVM") - assert uri.machines[0].name == "myVM" - assert uri.machines[1].name == "secondVM" + uri = ClanURI("clan://https://example.com?password=12345#myVM#secondVM?dummy_opt=1") + assert uri.machine.name == "myVM" + assert uri._machines[1].name == "secondVM" + assert uri._machines[1].params.dummy_opt == "1" match uri.url: case ClanUrl.REMOTE.value(url): assert url == "https://example.com?password=12345" # type: ignore case _: assert False + + +def test_from_str_remote() -> None: + uri = ClanURI.from_str(url="https://example.com", machine_name="myVM") + assert uri.get_url() == "https://example.com" + assert uri.get_orig_uri() == "clan://https://example.com#myVM" + assert uri.machine.name == "myVM" + assert len(uri._machines) == 1 + match uri.url: + case ClanUrl.REMOTE.value(url): + assert url == "https://example.com" # type: ignore + case _: + assert False + + +def test_from_str_local() -> None: + uri = ClanURI.from_str(url="~/Projects/democlan", machine_name="myVM") + assert uri.get_url().endswith("/Projects/democlan") + assert uri.get_orig_uri() == "clan://~/Projects/democlan#myVM" + assert uri.machine.name == "myVM" + assert len(uri._machines) == 1 + match uri.url: + case ClanUrl.LOCAL.value(path): + assert str(path).endswith("/Projects/democlan") # type: ignore + case _: + assert False + + +def test_from_str_local_no_machine() -> None: + uri = ClanURI.from_str("~/Projects/democlan") + assert uri.get_url().endswith("/Projects/democlan") + assert uri.get_orig_uri() == "clan://~/Projects/democlan" + assert uri.machine.name == "defaultVM" + assert len(uri._machines) == 1 + match uri.url: + case ClanUrl.LOCAL.value(path): + assert str(path).endswith("/Projects/democlan") # type: ignore + case _: + assert False + + +def test_from_str_local_no_machine2() -> None: + uri = ClanURI.from_str("~/Projects/democlan#syncthing-peer1") + assert uri.get_url().endswith("/Projects/democlan") + assert uri.get_orig_uri() == "clan://~/Projects/democlan#syncthing-peer1" + assert uri.machine.name == "syncthing-peer1" + assert len(uri._machines) == 1 + match uri.url: + case ClanUrl.LOCAL.value(path): + assert str(path).endswith("/Projects/democlan") # type: ignore + case _: + assert False diff --git a/pkgs/clan-cli/tests/test_history_cli.py b/pkgs/clan-cli/tests/test_history_cli.py index d5eb34bf..7da808ed 100644 --- a/pkgs/clan-cli/tests/test_history_cli.py +++ b/pkgs/clan-cli/tests/test_history_cli.py @@ -6,7 +6,7 @@ from cli import Cli from fixtures_flakes import FlakeForTest from pytest import CaptureFixture -from clan_cli.clan_uri import ClanParameters, ClanURI +from clan_cli.clan_uri import ClanURI from clan_cli.dirs import user_history_file from clan_cli.history.add import HistoryEntry @@ -19,8 +19,7 @@ def test_history_add( test_flake_with_core: FlakeForTest, ) -> None: cli = Cli() - params = ClanParameters(flake_attr="vm1") - uri = ClanURI.from_path(test_flake_with_core.path, params=params) + uri = ClanURI.from_str(str(test_flake_with_core.path), "vm1") cmd = [ "history", "add", @@ -40,8 +39,7 @@ def test_history_list( test_flake_with_core: FlakeForTest, ) -> None: cli = Cli() - params = ClanParameters(flake_attr="vm1") - uri = ClanURI.from_path(test_flake_with_core.path, params=params) + uri = ClanURI.from_str(str(test_flake_with_core.path), "vm1") cmd = [ "history", "list", diff --git a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py index a713d643..8b350d17 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py @@ -13,7 +13,7 @@ from typing import IO, ClassVar import gi from clan_cli import vms -from clan_cli.clan_uri import ClanScheme, ClanURI +from clan_cli.clan_uri import ClanURI, ClanUrl from clan_cli.history.add import HistoryEntry from clan_cli.machines.machines import Machine @@ -116,12 +116,12 @@ class VMObject(GObject.Object): url=self.data.flake.flake_url, flake_attr=self.data.flake.flake_attr ) match uri.scheme: - case ClanScheme.LOCAL.value(path): + case ClanUrl.LOCAL.value(path): self.machine = Machine( name=self.data.flake.flake_attr, flake=path, # type: ignore ) - case ClanScheme.REMOTE.value(url): + case ClanUrl.REMOTE.value(url): self.machine = Machine( name=self.data.flake.flake_attr, flake=url, # type: ignore diff --git a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_join.py b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_join.py index b8d9963d..3794f6a4 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_join.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_join.py @@ -62,8 +62,8 @@ class JoinList: cls._instance = cls.__new__(cls) cls.list_store = Gio.ListStore.new(JoinValue) - # Rerendering the join list every time an item changes in the clan_store ClanStore.use().register_on_deep_change(cls._instance._rerender_join_list) + return cls._instance def _rerender_join_list( @@ -83,7 +83,9 @@ class JoinList: """ value = JoinValue(uri) - if value.url.get_id() in [item.url.get_id() for item in self.list_store]: + if value.url.machine.get_id() in [ + item.url.machine.get_id() for item in self.list_store + ]: log.info(f"Join request already exists: {value.url}. Ignoring.") return diff --git a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py index 8ac60d60..50072c59 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py @@ -57,7 +57,7 @@ class ClanStore: store: "GKVStore", position: int, removed: int, added: int ) -> None: if added > 0: - store.register_on_change(on_vmstore_change) + store.values()[position].register_on_change(on_vmstore_change) callback(store, position, removed, added) self.clan_store.register_on_change(on_clanstore_change) @@ -111,10 +111,11 @@ class ClanStore: del self.clan_store[vm.data.flake.flake_url][vm.data.flake.flake_attr] def get_vm(self, uri: ClanURI) -> None | VMObject: - clan = self.clan_store.get(uri.get_internal()) - if clan is None: + vm_store = self.clan_store.get(str(uri.url)) + if vm_store is None: return None - return clan.get(uri.params.flake_attr, None) + machine = vm_store.get(uri.machine.name, None) + return machine def get_running_vms(self) -> list[VMObject]: return [ diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py index f5c4e535..0775f40b 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py @@ -190,8 +190,8 @@ class ClanList(Gtk.Box): log.debug("Rendering join row for %s", join_val.url) row = Adw.ActionRow() - row.set_title(join_val.url.params.flake_attr) - row.set_subtitle(join_val.url.get_internal()) + row.set_title(join_val.url.machine.name) + row.set_subtitle(str(join_val.url)) row.add_css_class("trust") vm = ClanStore.use().get_vm(join_val.url) @@ -204,7 +204,7 @@ class ClanList(Gtk.Box): ) avatar = Adw.Avatar() - avatar.set_text(str(join_val.url.params.flake_attr)) + avatar.set_text(str(join_val.url.machine.name)) avatar.set_show_initials(True) avatar.set_size(50) row.add_prefix(avatar) @@ -229,7 +229,7 @@ class ClanList(Gtk.Box): def on_join_request(self, source: Any, url: str) -> None: log.debug("Join request: %s", url) - clan_uri = ClanURI.from_str(url) + clan_uri = ClanURI(url) JoinList.use().push(clan_uri, self.on_after_join) def on_after_join(self, source: JoinValue) -> None: From a17eb3e8a3bc4b072225d6804fcfb0ba0fa6ead7 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Thu, 7 Mar 2024 19:09:01 +0700 Subject: [PATCH 04/63] clan_vm_manager: Fix broken vm start --- pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py index 8b350d17..972257c8 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py @@ -113,9 +113,9 @@ class VMObject(GObject.Object): @contextmanager def _create_machine(self) -> Generator[Machine, None, None]: uri = ClanURI.from_str( - url=self.data.flake.flake_url, flake_attr=self.data.flake.flake_attr + url=self.data.flake.flake_url, machine_name=self.data.flake.flake_attr ) - match uri.scheme: + match uri.url: case ClanUrl.LOCAL.value(path): self.machine = Machine( name=self.data.flake.flake_attr, From ab2defa9e43c7e71eb1fe27c3a0cb3ac704b9263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 6 Mar 2024 11:27:26 +0100 Subject: [PATCH 05/63] add confirmation prompt when installing --- checks/installation/flake-module.nix | 2 +- pkgs/clan-cli/clan_cli/machines/install.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/checks/installation/flake-module.nix b/checks/installation/flake-module.nix index 0768ad4f..75ba9997 100644 --- a/checks/installation/flake-module.nix +++ b/checks/installation/flake-module.nix @@ -107,7 +107,7 @@ in client.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../lib/ssh/privkey} /root/.ssh/id_ed25519") client.wait_until_succeeds("ssh -o StrictHostKeyChecking=accept-new -v root@target hostname") - client.succeed("clan --debug --flake ${../..} machines install test_install_machine root@target >&2") + client.succeed("clan --debug --flake ${../..} machines install --yes test_install_machine root@target >&2") try: target.shutdown() except BrokenPipeError: diff --git a/pkgs/clan-cli/clan_cli/machines/install.py b/pkgs/clan-cli/clan_cli/machines/install.py index 701303fe..82566adf 100644 --- a/pkgs/clan-cli/clan_cli/machines/install.py +++ b/pkgs/clan-cli/clan_cli/machines/install.py @@ -63,6 +63,7 @@ class InstallOptions: machine: str target_host: str kexec: str | None + confirm: bool def install_command(args: argparse.Namespace) -> None: @@ -71,10 +72,16 @@ def install_command(args: argparse.Namespace) -> None: machine=args.machine, target_host=args.target_host, kexec=args.kexec, + confirm=not args.yes, ) machine = Machine(opts.machine, flake=opts.flake) machine.target_host_address = opts.target_host + if opts.confirm: + ask = input(f"Install {machine.name} to {opts.target_host}? [y/N] ") + if ask != "y": + return + install_nixos(machine, kexec=opts.kexec) @@ -84,6 +91,12 @@ def register_install_parser(parser: argparse.ArgumentParser) -> None: type=str, help="use another kexec tarball to bootstrap NixOS", ) + parser.add_argument( + "--yes", + action="store_true", + help="do not ask for confirmation", + default=False, + ) parser.add_argument( "machine", type=str, From dd73406a9239b3533a79a5094347ef6c221e5505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 6 Mar 2024 16:41:26 +0100 Subject: [PATCH 06/63] installer: switch to systemd-boot grub is not able to boot from the disks that we flash for weird reasons. Since BIOS-boot is on life-support, we may as well just use systemd-boot. --- nixosModules/installer/default.nix | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/nixosModules/installer/default.nix b/nixosModules/installer/default.nix index 379fbcd5..5d00cd85 100644 --- a/nixosModules/installer/default.nix +++ b/nixosModules/installer/default.nix @@ -48,8 +48,13 @@ cat /var/shared/qrcode.utf8 fi ''; - boot.loader.grub.efiInstallAsRemovable = true; - boot.loader.grub.efiSupport = true; + + boot.loader.systemd-boot.enable = true; + + # Grub doesn't find devices for both BIOS and UEFI? + + #boot.loader.grub.efiInstallAsRemovable = true; + #boot.loader.grub.efiSupport = true; disko.devices = { disk = { stick = { @@ -59,10 +64,10 @@ content = { type = "gpt"; partitions = { - boot = { - size = "1M"; - type = "EF02"; # for grub MBR - }; + #boot = { + # size = "1M"; + # type = "EF02"; # for grub MBR + #}; ESP = { size = "100M"; type = "EF00"; From f599243cbd76c69b6fc8710cb4e3b0f59f59a01c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 7 Mar 2024 13:30:53 +0100 Subject: [PATCH 07/63] add flash command --- checks/flake-module.nix | 1 + checks/flash/flake-module.nix | 46 ++++++++++++ flake.lock | 6 +- pkgs/clan-cli/clan_cli/flash.py | 119 +++++++++++++++++++++++++++----- 4 files changed, 150 insertions(+), 22 deletions(-) create mode 100644 checks/flash/flake-module.nix diff --git a/checks/flake-module.nix b/checks/flake-module.nix index 22a5e66d..dcdb0090 100644 --- a/checks/flake-module.nix +++ b/checks/flake-module.nix @@ -3,6 +3,7 @@ ./impure/flake-module.nix ./backups/flake-module.nix ./installation/flake-module.nix + ./flash/flake-module.nix ]; perSystem = { pkgs, lib, self', ... }: { checks = diff --git a/checks/flash/flake-module.nix b/checks/flash/flake-module.nix new file mode 100644 index 00000000..3d729fcd --- /dev/null +++ b/checks/flash/flake-module.nix @@ -0,0 +1,46 @@ +{ self, ... }: +{ + perSystem = { nodes, pkgs, lib, ... }: + let + dependencies = [ + self + pkgs.stdenv.drvPath + self.clanInternals.machines.${pkgs.hostPlatform.system}.test_install_machine.config.system.build.toplevel + self.clanInternals.machines.${pkgs.hostPlatform.system}.test_install_machine.config.system.build.diskoScript + self.clanInternals.machines.${pkgs.hostPlatform.system}.test_install_machine.config.system.clan.deployment.file + self.inputs.nixpkgs.legacyPackages.${pkgs.hostPlatform.system}.disko + ] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs); + closureInfo = pkgs.closureInfo { rootPaths = dependencies; }; + in + { + checks = pkgs.lib.mkIf (pkgs.stdenv.isLinux) { + flash = + (import ../lib/test-base.nix) + { + name = "flash"; + nodes.target = { + virtualisation.emptyDiskImages = [ 4096 ]; + virtualisation.memorySize = 3000; + environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ]; + environment.etc."install-closure".source = "${closureInfo}/store-paths"; + + nix.settings = { + substituters = lib.mkForce [ ]; + hashed-mirrors = null; + connect-timeout = lib.mkForce 3; + flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}''; + experimental-features = [ + "nix-command" + "flakes" + ]; + }; + }; + testScript = '' + start_all() + machine.succeed("clan --flake ${../..} flash --debug --yes --disk main /dev/vdb test_install_machine") + ''; + } + { inherit pkgs self; }; + }; + }; +} diff --git a/flake.lock b/flake.lock index a34c6dd2..30440e79 100644 --- a/flake.lock +++ b/flake.lock @@ -78,11 +78,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1708847675, - "narHash": "sha256-RUZ7KEs/a4EzRELYDGnRB6i7M1Izii3JD/LyzH0c6Tg=", + "lastModified": 1709764733, + "narHash": "sha256-GptBnEUy8IcRrnd8X5WBJPDXG7M4bjj8OG4Wjg8dCDs=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "2a34566b67bef34c551f204063faeecc444ae9da", + "rev": "edf9f14255a7ac20f8da7b70609e980a964fca7a", "type": "github" }, "original": { diff --git a/pkgs/clan-cli/clan_cli/flash.py b/pkgs/clan-cli/clan_cli/flash.py index e99edb76..d15df36b 100644 --- a/pkgs/clan-cli/clan_cli/flash.py +++ b/pkgs/clan-cli/clan_cli/flash.py @@ -1,51 +1,110 @@ import argparse import importlib import logging +import os +import shlex +import shutil +from collections.abc import Sequence from dataclasses import dataclass from pathlib import Path from tempfile import TemporaryDirectory +from typing import Any +from .cmd import Log, run +from .errors import ClanError from .machines.machines import Machine -from .secrets.generate import generate_secrets +from .nix import nix_shell +from .secrets.modules import SecretStoreBase log = logging.getLogger(__name__) -def flash_machine(machine: Machine, device: str | None = None) -> None: +def flash_machine( + machine: Machine, disks: dict[str, str], dry_run: bool, debug: bool +) -> None: secrets_module = importlib.import_module(machine.secrets_module) - secret_store = secrets_module.SecretStore(machine=machine) - - generate_secrets(machine) - + secret_store: SecretStoreBase = secrets_module.SecretStore(machine=machine) with TemporaryDirectory() as tmpdir_: tmpdir = Path(tmpdir_) - upload_dir_ = machine.secrets_upload_directory + upload_dir = machine.secrets_upload_directory - if upload_dir_.startswith("/"): - upload_dir_ = upload_dir_[1:] - upload_dir = tmpdir / upload_dir_ - upload_dir.mkdir(parents=True) - secret_store.upload(upload_dir) + if upload_dir.startswith("/"): + local_dir = tmpdir / upload_dir[1:] + else: + local_dir = tmpdir / upload_dir - fs_image = machine.build_nix("config.system.clan.iso") - print(fs_image) + local_dir.mkdir(parents=True) + secret_store.upload(local_dir) + disko_install = [] + + if os.geteuid() != 0: + if shutil.which("sudo") is None: + raise ClanError( + "sudo is required to run disko-install as a non-root user" + ) + disko_install.append("sudo") + + disko_install.append("disko-install") + if dry_run: + disko_install.append("--dry-run") + if debug: + disko_install.append("--debug") + for name, device in disks.items(): + disko_install.extend(["--disk", name, device]) + + disko_install.extend(["--extra-files", str(local_dir), upload_dir]) + disko_install.extend(["--flake", str(machine.flake) + "#" + machine.name]) + + cmd = nix_shell( + ["nixpkgs#disko"], + disko_install, + ) + print("$", " ".join(map(shlex.quote, cmd))) + run(cmd, log=Log.BOTH, error_msg=f"Failed to flash {machine}") @dataclass class FlashOptions: flake: Path machine: str - device: str | None + disks: dict[str, str] + dry_run: bool + confirm: bool + debug: bool + + +class AppendDiskAction(argparse.Action): + def __init__(self, option_strings: str, dest: str, **kwargs: Any) -> None: + super().__init__(option_strings, dest, **kwargs) + + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: str | Sequence[str] | None, + option_string: str | None = None, + ) -> None: + disks = getattr(namespace, self.dest) + assert isinstance(values, list), "values must be a list" + disks[values[0]] = values[1] def flash_command(args: argparse.Namespace) -> None: opts = FlashOptions( flake=args.flake, machine=args.machine, - device=args.device, + disks=args.disk, + dry_run=args.dry_run, + confirm=not args.yes, + debug=args.debug, ) machine = Machine(opts.machine, flake=opts.flake) - flash_machine(machine, device=opts.device) + if opts.confirm and not opts.dry_run: + disk_str = ", ".join(f"{name}={device}" for name, device in opts.disks.items()) + ask = input(f"Install {machine.name} to {disk_str}? [y/N] ") + if ask != "y": + return + flash_machine(machine, disks=opts.disks, dry_run=opts.dry_run, debug=opts.debug) def register_parser(parser: argparse.ArgumentParser) -> None: @@ -55,8 +114,30 @@ def register_parser(parser: argparse.ArgumentParser) -> None: help="machine to install", ) parser.add_argument( - "--device", + "--disk", type=str, - help="device to flash the system to", + nargs=2, + metavar=("name", "value"), + action=AppendDiskAction, + help="device to flash to", + default={}, + ) + parser.add_argument( + "--yes", + action="store_true", + help="do not ask for confirmation", + default=False, + ) + parser.add_argument( + "--dry-run", + help="Only build the system, don't flash it", + default=False, + action="store_true", + ) + parser.add_argument( + "--debug", + help="Print debug information", + default=False, + action="store_true", ) parser.set_defaults(func=flash_command) From 93afd06bcbff457dbccbf664dd537754dc8134bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 7 Mar 2024 13:43:13 +0100 Subject: [PATCH 08/63] fix install test --- checks/installation/flake-module.nix | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/checks/installation/flake-module.nix b/checks/installation/flake-module.nix index 75ba9997..8a402b1b 100644 --- a/checks/installation/flake-module.nix +++ b/checks/installation/flake-module.nix @@ -21,12 +21,6 @@ in (modulesPath + "/testing/test-instrumentation.nix") # we need these 2 modules always to be able to run the tests (modulesPath + "/profiles/qemu-guest.nix") ]; - fileSystems."/nix/store" = lib.mkForce { - device = "nix-store"; - fsType = "9p"; - neededForBoot = true; - options = [ "trans=virtio" "version=9p2000.L" "cache=loose" ]; - }; clan.diskLayouts.singleDiskExt4.device = "/dev/vdb"; environment.etc."install-successful".text = "ok"; @@ -92,16 +86,16 @@ in testScript = '' def create_test_machine(oldmachine=None, args={}): # taken from + startCommand = "${pkgs.qemu_test}/bin/qemu-kvm" + startCommand += " -cpu max -m 1024 -virtfs local,path=/nix/store,security_model=none,mount_tag=nix-store" + startCommand += f' -drive file={oldmachine.state_dir}/empty0.qcow2,id=drive1,if=none,index=1,werror=report' + startCommand += ' -device virtio-blk-pci,drive=drive1' machine = create_machine({ - "qemuFlags": - '-cpu max -m 1024 -virtfs local,path=/nix/store,security_model=none,mount_tag=nix-store,' - f' -drive file={oldmachine.state_dir}/empty0.qcow2,id=drive1,if=none,index=1,werror=report' - f' -device virtio-blk-pci,drive=drive1', + "startCommand": startCommand, } | args) driver.machines.append(machine) return machine - start_all() client.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../lib/ssh/privkey} /root/.ssh/id_ed25519") From 26dd962799552c5312b38cf9c56440ac316c9edf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 7 Mar 2024 13:53:00 +0100 Subject: [PATCH 09/63] treefmt --- pkgs/clan-cli/clan_cli/vms/run.py | 11 ++++++----- pkgs/clan-cli/qemu/qmp.py | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/vms/run.py b/pkgs/clan-cli/clan_cli/vms/run.py index b4751cb4..6b35f98d 100644 --- a/pkgs/clan-cli/clan_cli/vms/run.py +++ b/pkgs/clan-cli/clan_cli/vms/run.py @@ -174,12 +174,13 @@ def run_vm( if vm.graphics and not vm.waypipe: packages.append("nixpkgs#virt-viewer") remote_viewer_mimetypes = module_root() / "vms" / "mimetypes" - env[ - "XDG_DATA_DIRS" - ] = f"{remote_viewer_mimetypes}:{env.get('XDG_DATA_DIRS', '')}" + env["XDG_DATA_DIRS"] = ( + f"{remote_viewer_mimetypes}:{env.get('XDG_DATA_DIRS', '')}" + ) - with start_waypipe(qemu_cmd.vsock_cid, f"[{vm.machine_name}] "), start_virtiofsd( - virtiofsd_socket + with ( + start_waypipe(qemu_cmd.vsock_cid, f"[{vm.machine_name}] "), + start_virtiofsd(virtiofsd_socket), ): run( nix_shell(packages, qemu_cmd.args), diff --git a/pkgs/clan-cli/qemu/qmp.py b/pkgs/clan-cli/qemu/qmp.py index 6dbef7c6..0e878f6c 100644 --- a/pkgs/clan-cli/qemu/qmp.py +++ b/pkgs/clan-cli/qemu/qmp.py @@ -1,6 +1,6 @@ # mypy: ignore-errors -""" QEMU Monitor Protocol Python class """ +"""QEMU Monitor Protocol Python class""" # Copyright (C) 2009, 2010 Red Hat Inc. # # Authors: From 3cc97ebc569fa29b0c78fcb1ca91b6625b949efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 7 Mar 2024 14:03:41 +0100 Subject: [PATCH 10/63] fix container tests --- checks/lib/container-driver/test_driver/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/checks/lib/container-driver/test_driver/__init__.py b/checks/lib/container-driver/test_driver/__init__.py index 95c16eb0..be9673a3 100644 --- a/checks/lib/container-driver/test_driver/__init__.py +++ b/checks/lib/container-driver/test_driver/__init__.py @@ -258,7 +258,7 @@ class Driver: self.machines = [] for container in containers: - name_match = re.match(r".*-nixos-system-(.+)-\d.+", container.name) + name_match = re.match(r".*-nixos-system-(.+)-(.+)", container.name) if not name_match: raise ValueError(f"Unable to extract hostname from {container.name}") name = name_match.group(1) From 4dfe4ecfa616cba679e68e966a7293220fc77df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Thu, 7 Mar 2024 17:24:57 +0100 Subject: [PATCH 11/63] fix building installer iso --- pkgs/installer/flake-module.nix | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkgs/installer/flake-module.nix b/pkgs/installer/flake-module.nix index 0e985378..e9d8c875 100644 --- a/pkgs/installer/flake-module.nix +++ b/pkgs/installer/flake-module.nix @@ -12,7 +12,12 @@ let nixpkgs.pkgs = self.inputs.nixpkgs.legacyPackages.x86_64-linux; }; - installer = lib.nixosSystem { modules = [ installerModule ]; }; + installer = lib.nixosSystem { + modules = [ + installerModule + { disko.memSize = 4096; } # FIXME: otherwise the image builder goes OOM + ]; + }; clan = self.lib.buildClan { clanName = "clan-core"; From 068f89e453246f4383758295adb56c44e333ca7f Mon Sep 17 00:00:00 2001 From: Qubasa Date: Fri, 8 Mar 2024 15:32:12 +0700 Subject: [PATCH 12/63] clan_vm_manager: Rewrite of Machine Class Part 1 --- pkgs/clan-cli/clan_cli/clan_uri.py | 12 ++--- pkgs/clan-cli/clan_cli/flakes/inspect.py | 1 - pkgs/clan-cli/clan_cli/machines/machines.py | 59 ++++++++++++--------- 3 files changed, 40 insertions(+), 32 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/clan_uri.py b/pkgs/clan-cli/clan_cli/clan_uri.py index 20d53e4c..a23eac1b 100644 --- a/pkgs/clan-cli/clan_cli/clan_uri.py +++ b/pkgs/clan-cli/clan_cli/clan_uri.py @@ -16,24 +16,24 @@ class ClanUrl(Enum): @member @dataclass class REMOTE: - url: str # The url field holds the HTTP URL + value: str # The url field holds the HTTP URL def __str__(self) -> str: - return f"{self.url}" # The __str__ method returns a custom string representation + return f"{self.value}" # The __str__ method returns a custom string representation def __repr__(self) -> str: - return f"ClanUrl.REMOTE({self.url})" + return f"ClanUrl.REMOTE({self.value})" @member @dataclass class LOCAL: - path: Path # The path field holds the local path + value: Path # The path field holds the local path def __str__(self) -> str: - return f"{self.path}" # The __str__ method returns a custom string representation + return f"{self.value}" # The __str__ method returns a custom string representation def __repr__(self) -> str: - return f"ClanUrl.LOCAL({self.path})" + return f"ClanUrl.LOCAL({self.value})" # Parameters defined here will be DELETED from the nested uri diff --git a/pkgs/clan-cli/clan_cli/flakes/inspect.py b/pkgs/clan-cli/clan_cli/flakes/inspect.py index 9897ed82..8d9f4e2e 100644 --- a/pkgs/clan-cli/clan_cli/flakes/inspect.py +++ b/pkgs/clan-cli/clan_cli/flakes/inspect.py @@ -86,7 +86,6 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig: # Get the flake metadata meta = nix_metadata(flake_url) - return FlakeConfig( vm=vm, flake_url=flake_url, diff --git a/pkgs/clan-cli/clan_cli/machines/machines.py b/pkgs/clan-cli/clan_cli/machines/machines.py index 8acc5b4a..32c9dee0 100644 --- a/pkgs/clan-cli/clan_cli/machines/machines.py +++ b/pkgs/clan-cli/clan_cli/machines/machines.py @@ -6,6 +6,7 @@ from pathlib import Path from tempfile import NamedTemporaryFile from typing import Any +from clan_cli.clan_uri import ClanURI, ClanUrl, MachineData from clan_cli.dirs import vm_state_dir from qemu.qmp import QEMUMonitorProtocol @@ -17,7 +18,7 @@ from ..ssh import Host, parse_deployment_address log = logging.getLogger(__name__) -class VMAttr: +class QMPWrapper: def __init__(self, state_dir: Path) -> None: # These sockets here are just symlinks to the real sockets which # are created by the run.py file. The reason being that we run into @@ -45,6 +46,7 @@ class Machine: name: str, flake: Path | str, deployment_info: dict | None = None, + machine: MachineData | None = None, ) -> None: """ Creates a Machine @@ -52,20 +54,26 @@ class 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 """ - self.name: str = name - self.flake: str | Path = flake + if machine is None: + uri = ClanURI.from_str(str(flake), name) + machine = uri.machine + self.flake = str(machine.url.value) + self.name = machine.name + self.data = machine + else: + self.data = machine self.eval_cache: dict[str, str] = {} self.build_cache: dict[str, Path] = {} - + self._flake_path: Path | None = None self._deployment_info: None | dict[str, str] = deployment_info - state_dir = vm_state_dir(flake_url=str(self.flake), vm_name=self.name) + state_dir = vm_state_dir(flake_url=str(self.data.url), vm_name=self.data.name) - self.vm: VMAttr = VMAttr(state_dir) + self.vm: QMPWrapper = QMPWrapper(state_dir) def __str__(self) -> str: - return f"Machine(name={self.name}, flake={self.flake})" + return f"Machine(name={self.data.name}, flake={self.data.url})" def __repr__(self) -> str: return str(self) @@ -86,7 +94,7 @@ class Machine: "deploymentAddress" ) if val is None: - msg = f"the 'clan.networking.targetHost' nixos option is not set for machine '{self.name}'" + msg = f"the 'clan.networking.targetHost' nixos option is not set for machine '{self.data.name}'" raise ClanError(msg) return val @@ -109,7 +117,7 @@ class Machine: return json.loads(Path(self.deployment_info["secretsData"]).read_text()) except json.JSONDecodeError as e: raise ClanError( - f"Failed to parse secretsData for machine {self.name} as json" + f"Failed to parse secretsData for machine {self.data.name} as json" ) from e return {} @@ -119,19 +127,22 @@ class Machine: @property def flake_dir(self) -> Path: - if isinstance(self.flake, Path): - return self.flake + if self._flake_path: + return self._flake_path - if hasattr(self, "flake_path"): - return Path(self.flake_path) + match self.data.url: + case ClanUrl.LOCAL.value(path): + self._flake_path = path + case ClanUrl.REMOTE.value(url): + self._flake_path = Path(nix_metadata(url)["path"]) - self.flake_path: str = nix_metadata(self.flake)["path"] - return Path(self.flake_path) + assert self._flake_path is not None + return self._flake_path @property def target_host(self) -> Host: return parse_deployment_address( - self.name, self.target_host_address, meta={"machine": self} + self.data.name, self.target_host_address, meta={"machine": self} ) @property @@ -145,7 +156,7 @@ class Machine: return self.target_host # enable ssh agent forwarding to allow the build host to access the target host return parse_deployment_address( - self.name, + self.data.name, build_host, forward_agent=True, meta={"machine": self, "target_host": self.target_host}, @@ -204,7 +215,7 @@ class Machine: args += [ "--expr", f""" - ((builtins.getFlake "{url}").clanInternals.machinesFunc."{system}"."{self.name}" {{ + ((builtins.getFlake "{url}").clanInternals.machinesFunc."{system}"."{self.data.name}" {{ extraConfig = builtins.fromJSON (builtins.readFile (builtins.fetchTree {{ type = "file"; url = if (builtins.compareVersions builtins.nixVersion "2.19") == -1 then "{file_info["path"]}" else "file:{file_info["path"]}"; @@ -214,15 +225,13 @@ class Machine: """, ] else: - if isinstance(self.flake, Path): - if (self.flake / ".git").exists(): - flake = f"git+file://{self.flake}" - else: - flake = f"path:{self.flake}" + if (self.flake_dir / ".git").exists(): + flake = f"git+file://{self.flake_dir}" else: - flake = self.flake + flake = f"path:{self.flake_dir}" + args += [ - f'{flake}#clanInternals.machines."{system}".{self.name}.{attr}', + f'{flake}#clanInternals.machines."{system}".{self.data.name}.{attr}', *nix_options, ] From e4896814f20c4c5473091c94ce908886d247675b Mon Sep 17 00:00:00 2001 From: Qubasa Date: Fri, 8 Mar 2024 22:01:54 +0700 Subject: [PATCH 13/63] clan_cli: Add TimeTable class to cmd.py. Fix bugs in Machine rewrite --- pkgs/clan-cli/clan_cli/cmd.py | 45 ++++++++++++++++++++- pkgs/clan-cli/clan_cli/machines/machines.py | 17 ++++++-- pkgs/clan-cli/clan_cli/vms/inspect.py | 2 +- 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/cmd.py b/pkgs/clan-cli/clan_cli/cmd.py index 932d8477..09f71ffb 100644 --- a/pkgs/clan-cli/clan_cli/cmd.py +++ b/pkgs/clan-cli/clan_cli/cmd.py @@ -4,7 +4,8 @@ import select import shlex import subprocess import sys -from datetime import datetime +import weakref +from datetime import datetime, timedelta from enum import Enum from pathlib import Path from typing import IO, Any @@ -58,6 +59,45 @@ def handle_output(process: subprocess.Popen, log: Log) -> tuple[str, str]: return stdout_buf.decode("utf-8"), stderr_buf.decode("utf-8") +class TimeTable: + """ + This class is used to store the time taken by each command + and print it at the end of the program if env PERF=1 is set. + """ + + def __init__(self) -> None: + self.table: dict[str, timedelta] = {} + weakref.finalize(self, self.table_print) + + def table_print(self) -> None: + if os.getenv("PERF") != "1": + return + print("======== CMD TIMETABLE ========") + + # Sort the table by time in descending order + sorted_table = sorted( + self.table.items(), key=lambda item: item[1], reverse=True + ) + + for k, v in sorted_table: + # Check if timedelta is greater than 1 second + if v.total_seconds() > 1: + # Print in red + print(f"\033[91mTook {v}s\033[0m for command: '{k}'") + else: + # Print in default color + print(f"Took {v} for command: '{k}'") + + def add(self, cmd: str, time: timedelta) -> None: + if cmd in self.table: + self.table[cmd] += time + else: + self.table[cmd] = time + + +TIME_TABLE = TimeTable() + + def run( cmd: list[str], *, @@ -83,7 +123,8 @@ def run( rc = process.wait() tend = datetime.now() - glog.debug(f"Command took {tend - tstart}s to run") + global TIME_TABLE + TIME_TABLE.add(shlex.join(cmd), tend - tstart) # Wait for the subprocess to finish cmd_out = CmdOut( diff --git a/pkgs/clan-cli/clan_cli/machines/machines.py b/pkgs/clan-cli/clan_cli/machines/machines.py index 32c9dee0..629e256a 100644 --- a/pkgs/clan-cli/clan_cli/machines/machines.py +++ b/pkgs/clan-cli/clan_cli/machines/machines.py @@ -41,6 +41,15 @@ class QMPWrapper: class Machine: + flake: str | Path + name: str + data: MachineData + eval_cache: dict[str, str] + build_cache: dict[str, Path] + _flake_path: Path | None + _deployment_info: None | dict[str, str] + vm: QMPWrapper + def __init__( self, name: str, @@ -57,11 +66,11 @@ class Machine: if machine is None: uri = ClanURI.from_str(str(flake), name) machine = uri.machine - self.flake = str(machine.url.value) - self.name = machine.name - self.data = machine + self.flake: str | Path = machine.url.value + self.name: str = machine.name + self.data: MachineData = machine else: - self.data = machine + self.data: MachineData = machine self.eval_cache: dict[str, str] = {} self.build_cache: dict[str, Path] = {} diff --git a/pkgs/clan-cli/clan_cli/vms/inspect.py b/pkgs/clan-cli/clan_cli/vms/inspect.py index cb70b758..a09b4361 100644 --- a/pkgs/clan-cli/clan_cli/vms/inspect.py +++ b/pkgs/clan-cli/clan_cli/vms/inspect.py @@ -22,7 +22,7 @@ class VmConfig: def inspect_vm(machine: Machine) -> VmConfig: data = json.loads(machine.eval_nix("config.clanCore.vm.inspect")) - return VmConfig(flake_url=machine.flake, **data) + return VmConfig(flake_url=str(machine.flake), **data) @dataclass From f4f31763741af126814326e9111226a9a9f8e067 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Fri, 8 Mar 2024 23:23:18 +0700 Subject: [PATCH 14/63] clan-vm-manager: Fix ClanUrl not pickable --- pkgs/clan-cli/clan_cli/clan_uri.py | 55 ++++++++++--------- pkgs/clan-cli/clan_cli/history/add.py | 7 --- pkgs/clan-cli/clan_cli/jsonrpc.py | 15 +++++ pkgs/clan-cli/clan_cli/locked_open.py | 12 +--- pkgs/clan-cli/clan_cli/machines/machines.py | 13 +++-- .../clan_vm_manager/components/vmobj.py | 23 ++++---- 6 files changed, 65 insertions(+), 60 deletions(-) create mode 100644 pkgs/clan-cli/clan_cli/jsonrpc.py diff --git a/pkgs/clan-cli/clan_cli/clan_uri.py b/pkgs/clan-cli/clan_cli/clan_uri.py index a23eac1b..203c8a6d 100644 --- a/pkgs/clan-cli/clan_cli/clan_uri.py +++ b/pkgs/clan-cli/clan_cli/clan_uri.py @@ -3,37 +3,29 @@ import dataclasses import urllib.parse import urllib.request from dataclasses import dataclass -from enum import Enum, member from pathlib import Path from typing import Any from .errors import ClanError -# Define an enum with different members that have different values -class ClanUrl(Enum): - # Use the dataclass decorator to add fields and methods to the members - @member - @dataclass - class REMOTE: - value: str # The url field holds the HTTP URL +@dataclass +class ClanUrl: + value: str | Path - def __str__(self) -> str: - return f"{self.value}" # The __str__ method returns a custom string representation + def __str__(self) -> str: + return ( + f"{self.value}" # The __str__ method returns a custom string representation + ) - def __repr__(self) -> str: - return f"ClanUrl.REMOTE({self.value})" + def __repr__(self) -> str: + return f"ClanUrl({self.value})" - @member - @dataclass - class LOCAL: - value: Path # The path field holds the local path + def is_local(self) -> bool: + return isinstance(self.value, Path) - def __str__(self) -> str: - return f"{self.value}" # The __str__ method returns a custom string representation - - def __repr__(self) -> str: - return f"ClanUrl.LOCAL({self.value})" + def is_remote(self) -> bool: + return isinstance(self.value, str) # Parameters defined here will be DELETED from the nested uri @@ -56,7 +48,6 @@ class MachineData: # Define the ClanURI class class ClanURI: _orig_uri: str - _nested_uri: str _components: urllib.parse.ParseResult url: ClanUrl _machines: list[MachineData] @@ -72,13 +63,13 @@ class ClanURI: # Check if the URI starts with clan:// # If it does, remove the clan:// prefix if uri.startswith("clan://"): - self._nested_uri = uri[7:] + nested_uri = uri[7:] else: raise ClanError(f"Invalid uri: expected clan://, got {uri}") # Parse the URI into components # url://netloc/path;parameters?query#fragment - self._components = urllib.parse.urlparse(self._nested_uri) + self._components = urllib.parse.urlparse(nested_uri) # Replace the query string in the components with the new query string clean_comps = self._components._replace( @@ -113,9 +104,9 @@ class ClanURI: ) match comb: case ("file", "", path, "", "", _) | ("", "", path, "", "", _): # type: ignore - url = ClanUrl.LOCAL.value(Path(path).expanduser().resolve()) # type: ignore + url = ClanUrl(Path(path).expanduser().resolve()) case _: - url = ClanUrl.REMOTE.value(comps.geturl()) # type: ignore + url = ClanUrl(comps.geturl()) return url @@ -150,6 +141,18 @@ class ClanURI: def get_url(self) -> str: return str(self.url) + def to_json(self) -> dict[str, Any]: + return { + "_orig_uri": self._orig_uri, + "url": str(self.url), + "machines": [dataclasses.asdict(m) for m in self._machines], + } + + def from_json(self, data: dict[str, Any]) -> None: + self._orig_uri = data["_orig_uri"] + self.url = data["url"] + self._machines = [MachineData(**m) for m in data["machines"]] + @classmethod def from_str( cls, # noqa diff --git a/pkgs/clan-cli/clan_cli/history/add.py b/pkgs/clan-cli/clan_cli/history/add.py index 3b4b21dd..f3b5ab7a 100644 --- a/pkgs/clan-cli/clan_cli/history/add.py +++ b/pkgs/clan-cli/clan_cli/history/add.py @@ -17,13 +17,6 @@ from ..locked_open import read_history_file, write_history_file log = logging.getLogger(__name__) -class EnhancedJSONEncoder(json.JSONEncoder): - def default(self, o: Any) -> Any: - if dataclasses.is_dataclass(o): - return dataclasses.asdict(o) - return super().default(o) - - @dataclasses.dataclass class HistoryEntry: last_used: str diff --git a/pkgs/clan-cli/clan_cli/jsonrpc.py b/pkgs/clan-cli/clan_cli/jsonrpc.py new file mode 100644 index 00000000..0d779ee4 --- /dev/null +++ b/pkgs/clan-cli/clan_cli/jsonrpc.py @@ -0,0 +1,15 @@ +import dataclasses +import json +from typing import Any + + +class ClanJSONEncoder(json.JSONEncoder): + def default(self, o: Any) -> Any: + # Check if the object has a to_json method + if hasattr(o, "to_json") and callable(o.to_json): + return o.to_json() + # Check if the object is a dataclass + elif dataclasses.is_dataclass(o): + return dataclasses.asdict(o) + # Otherwise, use the default serialization + return super().default(o) diff --git a/pkgs/clan-cli/clan_cli/locked_open.py b/pkgs/clan-cli/clan_cli/locked_open.py index b8ea03b9..a049d0ca 100644 --- a/pkgs/clan-cli/clan_cli/locked_open.py +++ b/pkgs/clan-cli/clan_cli/locked_open.py @@ -1,4 +1,3 @@ -import dataclasses import fcntl import json from collections.abc import Generator @@ -6,16 +5,11 @@ from contextlib import contextmanager from pathlib import Path from typing import Any +from clan_cli.jsonrpc import ClanJSONEncoder + from .dirs import user_history_file -class EnhancedJSONEncoder(json.JSONEncoder): - def default(self, o: Any) -> Any: - if dataclasses.is_dataclass(o): - return dataclasses.asdict(o) - return super().default(o) - - @contextmanager def _locked_open(filename: str | Path, mode: str = "r") -> Generator: """ @@ -29,7 +23,7 @@ def _locked_open(filename: str | Path, mode: str = "r") -> Generator: def write_history_file(data: Any) -> None: with _locked_open(user_history_file(), "w+") as f: - f.write(json.dumps(data, cls=EnhancedJSONEncoder, indent=4)) + f.write(json.dumps(data, cls=ClanJSONEncoder, indent=4)) def read_history_file() -> list[dict]: diff --git a/pkgs/clan-cli/clan_cli/machines/machines.py b/pkgs/clan-cli/clan_cli/machines/machines.py index 629e256a..7b572dc8 100644 --- a/pkgs/clan-cli/clan_cli/machines/machines.py +++ b/pkgs/clan-cli/clan_cli/machines/machines.py @@ -6,7 +6,7 @@ from pathlib import Path from tempfile import NamedTemporaryFile from typing import Any -from clan_cli.clan_uri import ClanURI, ClanUrl, MachineData +from clan_cli.clan_uri import ClanURI, MachineData from clan_cli.dirs import vm_state_dir from qemu.qmp import QEMUMonitorProtocol @@ -139,11 +139,12 @@ class Machine: if self._flake_path: return self._flake_path - match self.data.url: - case ClanUrl.LOCAL.value(path): - self._flake_path = path - case ClanUrl.REMOTE.value(url): - self._flake_path = Path(nix_metadata(url)["path"]) + if self.data.url.is_local(): + self._flake_path = Path(str(self.data.url)) + elif self.data.url.is_remote(): + self._flake_path = Path(nix_metadata(str(self.data.url))["path"]) + else: + raise ClanError(f"Unsupported flake url: {self.data.url}") assert self._flake_path is not None return self._flake_path diff --git a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py index 972257c8..7b47cdaa 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py @@ -13,7 +13,7 @@ from typing import IO, ClassVar import gi from clan_cli import vms -from clan_cli.clan_uri import ClanURI, ClanUrl +from clan_cli.clan_uri import ClanURI from clan_cli.history.add import HistoryEntry from clan_cli.machines.machines import Machine @@ -115,17 +115,16 @@ class VMObject(GObject.Object): uri = ClanURI.from_str( url=self.data.flake.flake_url, machine_name=self.data.flake.flake_attr ) - match uri.url: - case ClanUrl.LOCAL.value(path): - self.machine = Machine( - name=self.data.flake.flake_attr, - flake=path, # type: ignore - ) - case ClanUrl.REMOTE.value(url): - self.machine = Machine( - name=self.data.flake.flake_attr, - flake=url, # type: ignore - ) + if uri.url.is_local(): + self.machine = Machine( + name=self.data.flake.flake_attr, + flake=Path(str(uri.url)), + ) + if uri.url.is_remote(): + self.machine = Machine( + name=self.data.flake.flake_attr, + flake=str(uri.url), + ) yield self.machine self.machine = None From 372e212c0c9b7e8031369643151477cc4cd8b349 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Fri, 8 Mar 2024 23:47:27 +0700 Subject: [PATCH 15/63] clan_cli: Renamed ClanUrl to FlakeId --- pkgs/clan-cli/clan_cli/clan_uri.py | 58 +++++++++---------- pkgs/clan-cli/clan_cli/machines/machines.py | 16 ++--- pkgs/clan-cli/tests/test_clan_uri.py | 55 ++++-------------- .../clan_vm_manager/components/vmobj.py | 8 +-- .../clan_vm_manager/singletons/use_vms.py | 2 +- 5 files changed, 52 insertions(+), 87 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/clan_uri.py b/pkgs/clan-cli/clan_cli/clan_uri.py index 203c8a6d..9cb387cb 100644 --- a/pkgs/clan-cli/clan_cli/clan_uri.py +++ b/pkgs/clan-cli/clan_cli/clan_uri.py @@ -10,22 +10,30 @@ from .errors import ClanError @dataclass -class ClanUrl: - value: str | Path +class FlakeId: + _value: str | Path def __str__(self) -> str: - return ( - f"{self.value}" # The __str__ method returns a custom string representation - ) + return f"{self._value}" # The __str__ method returns a custom string representation + + @property + def path(self) -> Path: + assert isinstance(self._value, Path) + return self._value + + @property + def url(self) -> str: + assert isinstance(self._value, str) + return self._value def __repr__(self) -> str: - return f"ClanUrl({self.value})" + return f"ClanUrl({self._value})" def is_local(self) -> bool: - return isinstance(self.value, Path) + return isinstance(self._value, Path) def is_remote(self) -> bool: - return isinstance(self.value, str) + return isinstance(self._value, str) # Parameters defined here will be DELETED from the nested uri @@ -37,19 +45,19 @@ class MachineParams: @dataclass class MachineData: - url: ClanUrl + flake_id: FlakeId name: str = "defaultVM" params: MachineParams = dataclasses.field(default_factory=MachineParams) def get_id(self) -> str: - return f"{self.url}#{self.name}" + return f"{self.flake_id}#{self.name}" # Define the ClanURI class class ClanURI: _orig_uri: str _components: urllib.parse.ParseResult - url: ClanUrl + flake_id: FlakeId _machines: list[MachineData] # Initialize the class with a clan:// URI @@ -77,7 +85,7 @@ class ClanURI: ) # Parse the URL into a ClanUrl object - self.url = self._parse_url(clean_comps) + self.flake_id = self._parse_url(clean_comps) # Parse the fragment into a list of machine queries # Then parse every machine query into a MachineParameters object @@ -90,10 +98,10 @@ class ClanURI: # If there are no machine fragments, add a default machine if len(machine_frags) == 0: - default_machine = MachineData(url=self.url) + default_machine = MachineData(flake_id=self.flake_id) self._machines.append(default_machine) - def _parse_url(self, comps: urllib.parse.ParseResult) -> ClanUrl: + def _parse_url(self, comps: urllib.parse.ParseResult) -> FlakeId: comb = ( comps.scheme, comps.netloc, @@ -104,11 +112,11 @@ class ClanURI: ) match comb: case ("file", "", path, "", "", _) | ("", "", path, "", "", _): # type: ignore - url = ClanUrl(Path(path).expanduser().resolve()) + flake_id = FlakeId(Path(path).expanduser().resolve()) case _: - url = ClanUrl(comps.geturl()) + flake_id = FlakeId(comps.geturl()) - return url + return flake_id def _parse_machine_query(self, machine_frag: str) -> MachineData: comp = urllib.parse.urlparse(machine_frag) @@ -128,7 +136,7 @@ class ClanURI: # we need to make sure there are no conflicts del query[dfield.name] params = MachineParams(**machine_params) - machine = MachineData(url=self.url, name=machine_name, params=params) + machine = MachineData(flake_id=self.flake_id, name=machine_name, params=params) return machine @property @@ -139,19 +147,7 @@ class ClanURI: return self._orig_uri def get_url(self) -> str: - return str(self.url) - - def to_json(self) -> dict[str, Any]: - return { - "_orig_uri": self._orig_uri, - "url": str(self.url), - "machines": [dataclasses.asdict(m) for m in self._machines], - } - - def from_json(self, data: dict[str, Any]) -> None: - self._orig_uri = data["_orig_uri"] - self.url = data["url"] - self._machines = [MachineData(**m) for m in data["machines"]] + return str(self.flake_id) @classmethod def from_str( diff --git a/pkgs/clan-cli/clan_cli/machines/machines.py b/pkgs/clan-cli/clan_cli/machines/machines.py index 7b572dc8..72e4b9f9 100644 --- a/pkgs/clan-cli/clan_cli/machines/machines.py +++ b/pkgs/clan-cli/clan_cli/machines/machines.py @@ -66,7 +66,7 @@ class Machine: if machine is None: uri = ClanURI.from_str(str(flake), name) machine = uri.machine - self.flake: str | Path = machine.url.value + self.flake: str | Path = machine.flake_id._value self.name: str = machine.name self.data: MachineData = machine else: @@ -77,12 +77,12 @@ class Machine: self._flake_path: Path | None = None self._deployment_info: None | dict[str, str] = deployment_info - state_dir = vm_state_dir(flake_url=str(self.data.url), vm_name=self.data.name) + state_dir = vm_state_dir(flake_url=str(self.flake), vm_name=self.data.name) self.vm: QMPWrapper = QMPWrapper(state_dir) def __str__(self) -> str: - return f"Machine(name={self.data.name}, flake={self.data.url})" + return f"Machine(name={self.data.name}, flake={self.data.flake_id})" def __repr__(self) -> str: return str(self) @@ -139,12 +139,12 @@ class Machine: if self._flake_path: return self._flake_path - if self.data.url.is_local(): - self._flake_path = Path(str(self.data.url)) - elif self.data.url.is_remote(): - self._flake_path = Path(nix_metadata(str(self.data.url))["path"]) + if self.data.flake_id.is_local(): + self._flake_path = self.data.flake_id.path + elif self.data.flake_id.is_remote(): + self._flake_path = Path(nix_metadata(self.data.flake_id.url)["path"]) else: - raise ClanError(f"Unsupported flake url: {self.data.url}") + raise ClanError(f"Unsupported flake url: {self.data.flake_id}") assert self._flake_path is not None return self._flake_path diff --git a/pkgs/clan-cli/tests/test_clan_uri.py b/pkgs/clan-cli/tests/test_clan_uri.py index f8c09cb7..efddafa6 100644 --- a/pkgs/clan-cli/tests/test_clan_uri.py +++ b/pkgs/clan-cli/tests/test_clan_uri.py @@ -1,6 +1,6 @@ from pathlib import Path -from clan_cli.clan_uri import ClanURI, ClanUrl +from clan_cli.clan_uri import ClanURI def test_get_url() -> None: @@ -21,22 +21,13 @@ def test_get_url() -> None: def test_local_uri() -> None: # Create a ClanURI object from a local URI uri = ClanURI("clan://file:///home/user/Downloads") - match uri.url: - case ClanUrl.LOCAL.value(path): - assert path == Path("/home/user/Downloads") # type: ignore - case _: - assert False + assert uri.flake_id.path == Path("/home/user/Downloads") def test_is_remote() -> None: # Create a ClanURI object from a remote URI uri = ClanURI("clan://https://example.com") - - match uri.url: - case ClanUrl.REMOTE.value(url): - assert url == "https://example.com" # type: ignore - case _: - assert False + assert uri.flake_id.url == "https://example.com" def test_direct_local_path() -> None: @@ -56,12 +47,7 @@ def test_remote_with_clanparams() -> None: uri = ClanURI("clan://https://example.com") assert uri.machine.name == "defaultVM" - - match uri.url: - case ClanUrl.REMOTE.value(url): - assert url == "https://example.com" # type: ignore - case _: - assert False + assert uri.flake_id.url == "https://example.com" def test_remote_with_all_params() -> None: @@ -69,11 +55,7 @@ def test_remote_with_all_params() -> None: assert uri.machine.name == "myVM" assert uri._machines[1].name == "secondVM" assert uri._machines[1].params.dummy_opt == "1" - match uri.url: - case ClanUrl.REMOTE.value(url): - assert url == "https://example.com?password=12345" # type: ignore - case _: - assert False + assert uri.flake_id.url == "https://example.com?password=12345" def test_from_str_remote() -> None: @@ -82,11 +64,7 @@ def test_from_str_remote() -> None: assert uri.get_orig_uri() == "clan://https://example.com#myVM" assert uri.machine.name == "myVM" assert len(uri._machines) == 1 - match uri.url: - case ClanUrl.REMOTE.value(url): - assert url == "https://example.com" # type: ignore - case _: - assert False + assert uri.flake_id.url == "https://example.com" def test_from_str_local() -> None: @@ -95,11 +73,8 @@ def test_from_str_local() -> None: assert uri.get_orig_uri() == "clan://~/Projects/democlan#myVM" assert uri.machine.name == "myVM" assert len(uri._machines) == 1 - match uri.url: - case ClanUrl.LOCAL.value(path): - assert str(path).endswith("/Projects/democlan") # type: ignore - case _: - assert False + assert uri.flake_id.is_local() + assert str(uri.flake_id).endswith("/Projects/democlan") # type: ignore def test_from_str_local_no_machine() -> None: @@ -108,11 +83,8 @@ def test_from_str_local_no_machine() -> None: assert uri.get_orig_uri() == "clan://~/Projects/democlan" assert uri.machine.name == "defaultVM" assert len(uri._machines) == 1 - match uri.url: - case ClanUrl.LOCAL.value(path): - assert str(path).endswith("/Projects/democlan") # type: ignore - case _: - assert False + assert uri.flake_id.is_local() + assert str(uri.flake_id).endswith("/Projects/democlan") # type: ignore def test_from_str_local_no_machine2() -> None: @@ -121,8 +93,5 @@ def test_from_str_local_no_machine2() -> None: assert uri.get_orig_uri() == "clan://~/Projects/democlan#syncthing-peer1" assert uri.machine.name == "syncthing-peer1" assert len(uri._machines) == 1 - match uri.url: - case ClanUrl.LOCAL.value(path): - assert str(path).endswith("/Projects/democlan") # type: ignore - case _: - assert False + assert uri.flake_id.is_local() + assert str(uri.flake_id).endswith("/Projects/democlan") # type: ignore diff --git a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py index 7b47cdaa..02a7ada3 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py @@ -115,15 +115,15 @@ class VMObject(GObject.Object): uri = ClanURI.from_str( url=self.data.flake.flake_url, machine_name=self.data.flake.flake_attr ) - if uri.url.is_local(): + if uri.flake_id.is_local(): self.machine = Machine( name=self.data.flake.flake_attr, - flake=Path(str(uri.url)), + flake=uri.flake_id.path, ) - if uri.url.is_remote(): + if uri.flake_id.is_remote(): self.machine = Machine( name=self.data.flake.flake_attr, - flake=str(uri.url), + flake=uri.flake_id.url, ) yield self.machine self.machine = None diff --git a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py index 50072c59..71854844 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py @@ -111,7 +111,7 @@ class ClanStore: del self.clan_store[vm.data.flake.flake_url][vm.data.flake.flake_attr] def get_vm(self, uri: ClanURI) -> None | VMObject: - vm_store = self.clan_store.get(str(uri.url)) + vm_store = self.clan_store.get(str(uri.flake_id)) if vm_store is None: return None machine = vm_store.get(uri.machine.name, None) From 11cfc49d2778ed92b1a13519fd5b2857f3ad8d47 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sat, 9 Mar 2024 10:58:12 +0100 Subject: [PATCH 16/63] docs: improve readme for better onboarding --- pkgs/clan-vm-manager/README.md | 87 +++++++++++++++------ pkgs/clan-vm-manager/screenshots/image.png | Bin 0 -> 72795 bytes 2 files changed, 62 insertions(+), 25 deletions(-) create mode 100644 pkgs/clan-vm-manager/screenshots/image.png diff --git a/pkgs/clan-vm-manager/README.md b/pkgs/clan-vm-manager/README.md index df504a6a..ed97f28a 100644 --- a/pkgs/clan-vm-manager/README.md +++ b/pkgs/clan-vm-manager/README.md @@ -1,34 +1,28 @@ -## Developing GTK4 Applications +# Clan VM Manager +Provides users with the simple functionality to manage their locally registered clans. + +![app-preview](screenshots/image.png) + +## Available commands + +Run this application -## Demos -Adw has a demo application showing all widgets. You can run it by executing: ```bash -adwaita-1-demo -``` -GTK4 has a demo application showing all widgets. You can run it by executing: -```bash -gtk4-widget-factory +./bin/clan-vm-manager ``` -To find available icons execute: +Join a new clan + ```bash -gtk4-icon-browser +./bin/clan-vm-manager [clan-uri] ``` +For more available commands see the developer section below. +## Developing this Application -## Links -- [Adw PyGobject Reference](http://lazka.github.io/pgi-docs/index.html#Adw-1) -- [GTK4 PyGobject Reference](http://lazka.github.io/pgi-docs/index.html#Gtk-4.0) -- [Adw Widget Gallery](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/widget-gallery.html) -- [Python + GTK3 Tutorial](https://python-gtk-3-tutorial.readthedocs.io/en/latest/textview.html) - - - -## Debugging Style and Layout - -You can append `--debug` flag to enable debug logging printed into the console. +### Debugging Style and Layout ```bash # Enable the GTK debugger @@ -38,8 +32,51 @@ gsettings set org.gtk.Settings.Debug enable-inspector-keybinding true GTK_DEBUG=interactive ./bin/clan-vm-manager --debug ``` -## Profiling -To activate profiling execute: -``` +Appending `--debug` flag enables debug logging printed into the console. + +### Profiling + +To activate profiling you can run + +```bash PERF=1 ./bin/clan-vm-manager -``` \ No newline at end of file +``` + +### Library Components + +> Note: +> +> we recognized bugs when starting some cli-commands through the integrated vs-code terminal. +> If encountering issues make sure to run commands in a regular os-shell. + +lib-Adw has a demo application showing all widgets. You can run it by executing + +```bash +adwaita-1-demo +``` + +GTK4 has a demo application showing all widgets. You can run it by executing + +```bash +gtk4-widget-factory +``` + +To find available icons execute + +```bash +gtk4-icon-browser +``` + +### Links + +Here are some important documentation links related to the Clan VM Manager: + +- [Adw PyGobject Reference](http://lazka.github.io/pgi-docs/index.html#Adw-1): This link provides the PyGObject reference documentation for the Adw library, which is used in the Clan VM Manager. It contains detailed information about the Adw widgets and their usage. + +- [GTK4 PyGobject Reference](http://lazka.github.io/pgi-docs/index.html#Gtk-4.0): This link provides the PyGObject reference documentation for GTK4, the toolkit used for building the user interface of the Clan VM Manager. It includes information about GTK4 widgets, signals, and other features. + +- [Adw Widget Gallery](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/widget-gallery.html): This link showcases a widget gallery for Adw, allowing you to see the available widgets and their visual appearance. It can be helpful for designing the user interface of the Clan VM Manager. + +- [Python + GTK3 Tutorial](https://python-gtk-3-tutorial.readthedocs.io/en/latest/textview.html): Although the Clan VM Manager uses GTK4, this tutorial for GTK3 can still be useful as it covers the basics of building GTK-based applications with Python. It includes examples and explanations for various GTK widgets, including text views. + +- [GNOME Human Interface Guidelines](https://developer.gnome.org/hig/): This link provides the GNOME Human Interface Guidelines, which offer design and usability recommendations for creating GNOME applications. It covers topics such as layout, navigation, and interaction patterns. diff --git a/pkgs/clan-vm-manager/screenshots/image.png b/pkgs/clan-vm-manager/screenshots/image.png new file mode 100644 index 0000000000000000000000000000000000000000..a6f7f4a2995ac80772309f970ba7ae385ed5559e GIT binary patch literal 72795 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 Date: Sat, 9 Mar 2024 11:25:11 +0100 Subject: [PATCH 17/63] docs: improve start command --- pkgs/clan-vm-manager/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pkgs/clan-vm-manager/README.md b/pkgs/clan-vm-manager/README.md index ed97f28a..02a1366c 100644 --- a/pkgs/clan-vm-manager/README.md +++ b/pkgs/clan-vm-manager/README.md @@ -12,12 +12,18 @@ Run this application ./bin/clan-vm-manager ``` -Join a new clan +Join the default machine of a clan ```bash ./bin/clan-vm-manager [clan-uri] ``` +Join a specific machine of a clan + +```bash +./bin/clan-vm-manager [clan-uri]#[machine] +``` + For more available commands see the developer section below. ## Developing this Application From 01351ff5a149dcefc964e50cb8655f1b697d7e93 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Sat, 9 Mar 2024 23:15:32 +0700 Subject: [PATCH 18/63] clan-vm-manager: Add library for mypy pygobject types --- pkgs/clan-vm-manager/clan_vm_manager/app.py | 8 ++-- .../clan_vm_manager/components/gkvstore.py | 8 ++-- .../clan_vm_manager/components/trayicon.py | 9 +++-- .../clan_vm_manager/components/vmobj.py | 37 ++++++++++--------- .../clan_vm_manager/singletons/use_join.py | 8 ++-- .../clan_vm_manager/views/details.py | 12 ++++-- .../clan_vm_manager/views/list.py | 16 ++++++-- .../clan_vm_manager/windows/main_window.py | 1 + pkgs/clan-vm-manager/default.nix | 5 ++- pkgs/clan-vm-manager/pyproject.toml | 6 +-- pkgs/clan-vm-manager/shell.nix | 2 +- 11 files changed, 68 insertions(+), 44 deletions(-) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/app.py b/pkgs/clan-vm-manager/clan_vm_manager/app.py index 11d61e1f..a1ce1230 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/app.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/app.py @@ -33,10 +33,8 @@ class MainApplication(Adw.Application): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__( - *args, application_id="org.clan.vm-manager", flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, - **kwargs, ) self.add_main_option( @@ -48,7 +46,7 @@ class MainApplication(Adw.Application): None, ) - self.window: Adw.ApplicationWindow | None = None + self.window: "MainWindow" | None = None self.connect("activate", self.on_activate) self.connect("shutdown", self.on_shutdown) @@ -113,8 +111,10 @@ class MainApplication(Adw.Application): log.debug(f"Style css path: {resource_path}") css_provider = Gtk.CssProvider() css_provider.load_from_path(str(resource_path)) + display = Gdk.Display.get_default() + assert display is not None Gtk.StyleContext.add_provider_for_display( - Gdk.Display.get_default(), + display, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, ) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/components/gkvstore.py b/pkgs/clan-vm-manager/clan_vm_manager/components/gkvstore.py index 247e8ca9..d4795f63 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/components/gkvstore.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/components/gkvstore.py @@ -134,8 +134,8 @@ class GKVStore(GObject.GObject, Gio.ListModel, Generic[K, V]): def do_get_item(self, position: int) -> V | None: return self.get_item(position) - def get_item_type(self) -> GObject.GType: - return self.gtype.__gtype__ + def get_item_type(self) -> Any: + return self.gtype.__gtype__ # type: ignore[attr-defined] def do_get_item_type(self) -> GObject.GType: return self.get_item_type() @@ -187,10 +187,10 @@ class GKVStore(GObject.GObject, Gio.ListModel, Generic[K, V]): return len(self._items) # O(1) operation - def __getitem__(self, key: K) -> V: + def __getitem__(self, key: K) -> V: # type: ignore[override] return self._items[key] - def __contains__(self, key: K) -> bool: + def __contains__(self, key: K) -> bool: # type: ignore[override] return key in self._items def __str__(self) -> str: diff --git a/pkgs/clan-vm-manager/clan_vm_manager/components/trayicon.py b/pkgs/clan-vm-manager/clan_vm_manager/components/trayicon.py index 88caf424..89c900af 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/components/trayicon.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/components/trayicon.py @@ -24,6 +24,9 @@ import sys from collections.abc import Callable from typing import Any, ClassVar +import gi + +gi.require_version("Gtk", "4.0") from gi.repository import GdkPixbuf, Gio, GLib, Gtk @@ -168,7 +171,7 @@ class ImplUnavailableError(Exception): class BaseImplementation: - def __init__(self, application: Gtk.Application) -> None: + def __init__(self, application: Any) -> None: self.application = application self.menu_items: dict[int, Any] = {} self.menu_item_id: int = 1 @@ -1090,8 +1093,8 @@ class Win32Implementation(BaseImplementation): class TrayIcon: - def __init__(self, application: Gtk.Application) -> None: - self.application: Gtk.Application = application + def __init__(self, application: Gio.Application) -> None: + self.application: Gio.Application = application self.available: bool = True self.implementation: Any = None diff --git a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py index 02a7ada3..0c885b1a 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py @@ -32,13 +32,6 @@ class VMObject(GObject.Object): "vm_status_changed": (GObject.SignalFlags.RUN_FIRST, None, []) } - def _vm_status_changed_task(self) -> bool: - self.emit("vm_status_changed") - return GLib.SOURCE_REMOVE - - def update(self, data: HistoryEntry) -> None: - self.data = data - def __init__( self, icon: Path, @@ -47,16 +40,20 @@ class VMObject(GObject.Object): super().__init__() # Store the data from the history entry - self.data = data + self.data: HistoryEntry = data # Create a process object to store the VM process - self.vm_process = MPProcess("vm_dummy", mp.Process(), Path("./dummy")) - self.build_process = MPProcess("build_dummy", mp.Process(), Path("./dummy")) + self.vm_process: MPProcess = MPProcess( + "vm_dummy", mp.Process(), Path("./dummy") + ) + self.build_process: MPProcess = MPProcess( + "build_dummy", mp.Process(), Path("./dummy") + ) self._start_thread: threading.Thread = threading.Thread() self.machine: Machine | None = None # Watcher to stop the VM - self.KILL_TIMEOUT = 20 # seconds + self.KILL_TIMEOUT: int = 20 # seconds self._stop_thread: threading.Thread = threading.Thread() # Build progress bar vars @@ -66,7 +63,7 @@ class VMObject(GObject.Object): self.prog_bar_id: int = 0 # Create a temporary directory to store the logs - self.log_dir = tempfile.TemporaryDirectory( + self.log_dir: tempfile.TemporaryDirectory = tempfile.TemporaryDirectory( prefix="clan_vm-", suffix=f"-{self.data.flake.flake_attr}" ) self._logs_id: int = 0 @@ -75,14 +72,21 @@ class VMObject(GObject.Object): # To be able to set the switch state programmatically # we need to store the handler id returned by the connect method # and block the signal while we change the state. This is cursed. - self.switch = Gtk.Switch() + self.switch: Gtk.Switch = Gtk.Switch() self.switch_handler_id: int = self.switch.connect( "notify::active", self._on_switch_toggle ) self.connect("vm_status_changed", self._on_vm_status_changed) # Make sure the VM is killed when the reference to this object is dropped - self._finalizer = weakref.finalize(self, self._kill_ref_drop) + self._finalizer: weakref.finalize = weakref.finalize(self, self._kill_ref_drop) + + def _vm_status_changed_task(self) -> bool: + self.emit("vm_status_changed") + return GLib.SOURCE_REMOVE + + def update(self, data: HistoryEntry) -> None: + self.data = data def _on_vm_status_changed(self, source: "VMObject") -> None: self.switch.set_state(self.is_running() and not self.is_building()) @@ -93,9 +97,8 @@ class VMObject(GObject.Object): exit_build = self.build_process.proc.exitcode exitc = exit_vm or exit_build if not self.is_running() and exitc != 0: - self.switch.handler_block(self.switch_handler_id) - self.switch.set_active(False) - self.switch.handler_unblock(self.switch_handler_id) + with self.switch.handler_block(self.switch_handler_id): + self.switch.set_active(False) log.error(f"VM exited with error. Exitcode: {exitc}") def _on_switch_toggle(self, switch: Gtk.Switch, user_state: bool) -> None: diff --git a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_join.py b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_join.py index 3794f6a4..b52b41ef 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_join.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_join.py @@ -1,7 +1,7 @@ import logging import threading from collections.abc import Callable -from typing import Any, ClassVar +from typing import Any, ClassVar, cast import gi from clan_cli.clan_uri import ClanURI @@ -31,8 +31,8 @@ class JoinValue(GObject.Object): def __init__(self, url: ClanURI) -> None: super().__init__() - self.url = url - self.entry = None + self.url: ClanURI = url + self.entry: HistoryEntry | None = None def __join(self) -> None: new_entry = add_history(self.url) @@ -84,7 +84,7 @@ class JoinList: value = JoinValue(uri) if value.url.machine.get_id() in [ - item.url.machine.get_id() for item in self.list_store + cast(JoinValue, item).url.machine.get_id() for item in self.list_store ]: log.info(f"Join request already exists: {value.url}. Ignoring.") return diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/details.py b/pkgs/clan-vm-manager/clan_vm_manager/views/details.py index 58f5b595..c9ec2f93 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/views/details.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/details.py @@ -1,16 +1,19 @@ import os from collections.abc import Callable from functools import partial -from typing import Any, Literal +from typing import Any, Literal, TypeVar import gi gi.require_version("Adw", "1") from gi.repository import Adw, Gio, GObject, Gtk +# Define a TypeVar that is bound to GObject.Object +ListItem = TypeVar("ListItem", bound=GObject.Object) + def create_details_list( - model: Gio.ListStore, render_row: Callable[[Gtk.ListBox, GObject], Gtk.Widget] + model: Gio.ListStore, render_row: Callable[[Gtk.ListBox, ListItem], Gtk.Widget] ) -> Gtk.ListBox: boxed_list = Gtk.ListBox() boxed_list.set_selection_mode(Gtk.SelectionMode.NONE) @@ -49,7 +52,10 @@ class Details(Gtk.Box): def render_entry_row( self, boxed_list: Gtk.ListBox, item: PreferencesValue ) -> Gtk.Widget: - row = Adw.SpinRow.new_with_range(0, os.cpu_count(), 1) + cores: int | None = os.cpu_count() + fcores = float(cores) if cores else 1.0 + + row = Adw.SpinRow.new_with_range(0, fcores, 1) row.set_value(item.data) return row diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py index 0775f40b..438e2452 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py @@ -1,7 +1,7 @@ import logging from collections.abc import Callable from functools import partial -from typing import Any +from typing import Any, TypeVar import gi from clan_cli import history @@ -17,9 +17,13 @@ from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk log = logging.getLogger(__name__) +ListItem = TypeVar("ListItem", bound=GObject.Object) +CustomStore = TypeVar("CustomStore", bound=Gio.ListModel) + def create_boxed_list( - model: Gio.ListStore, render_row: Callable[[Gtk.ListBox, GObject], Gtk.Widget] + model: CustomStore, + render_row: Callable[[Gtk.ListBox, ListItem], Gtk.Widget], ) -> Gtk.ListBox: boxed_list = Gtk.ListBox() boxed_list.set_selection_mode(Gtk.SelectionMode.NONE) @@ -47,8 +51,9 @@ class ClanList(Gtk.Box): def __init__(self, config: ClanConfig) -> None: super().__init__(orientation=Gtk.Orientation.VERTICAL) - self.app = Gio.Application.get_default() - self.app.connect("join_request", self.on_join_request) + app = Gio.Application.get_default() + assert app is not None + app.connect("join_request", self.on_join_request) self.log_label: Gtk.Label = Gtk.Label() self.__init_machines = history.add.list_history() @@ -78,6 +83,7 @@ class ClanList(Gtk.Box): add_action = Gio.SimpleAction.new("add", GLib.VariantType.new("s")) add_action.connect("activate", self.on_add) app = Gio.Application.get_default() + assert app is not None app.add_action(add_action) menu_model = Gio.Menu() @@ -158,6 +164,7 @@ class ClanList(Gtk.Box): open_action = Gio.SimpleAction.new("edit", GLib.VariantType.new("s")) open_action.connect("activate", self.on_edit) app = Gio.Application.get_default() + assert app is not None app.add_action(open_action) menu_model = Gio.Menu() menu_model.append("Edit", f"app.edit::{vm.get_id()}") @@ -199,6 +206,7 @@ class ClanList(Gtk.Box): # Can't do this here because clan store is empty at this point if vm is not None: sub = row.get_subtitle() + assert sub is not None row.set_subtitle( sub + "\nClan already exists. Joining again will update it" ) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py index d78fc81a..895acc80 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py @@ -32,6 +32,7 @@ class MainWindow(Adw.ApplicationWindow): view.add_top_bar(header) app = Gio.Application.get_default() + assert app is not None self.tray_icon: TrayIcon = TrayIcon(app) # Initialize all ClanStore diff --git a/pkgs/clan-vm-manager/default.nix b/pkgs/clan-vm-manager/default.nix index 0395fec0..a5d2c15d 100644 --- a/pkgs/clan-vm-manager/default.nix +++ b/pkgs/clan-vm-manager/default.nix @@ -6,6 +6,7 @@ , wrapGAppsHook , gtk4 , gnome +, pygobject-stubs , gobject-introspection , clan-cli , makeDesktopItem @@ -41,7 +42,9 @@ python3.pkgs.buildPythonApplication { ]; buildInputs = [ gtk4 libadwaita gnome.adwaita-icon-theme ]; - propagatedBuildInputs = [ pygobject3 clan-cli ]; + + # We need to propagate the build inputs to nix fmt / treefmt + propagatedBuildInputs = [ pygobject3 clan-cli pygobject-stubs ]; # also re-expose dependencies so we test them in CI passthru = { diff --git a/pkgs/clan-vm-manager/pyproject.toml b/pkgs/clan-vm-manager/pyproject.toml index 6f8a2f6f..2eeacbac 100644 --- a/pkgs/clan-vm-manager/pyproject.toml +++ b/pkgs/clan-vm-manager/pyproject.toml @@ -22,9 +22,9 @@ disallow_untyped_calls = true disallow_untyped_defs = true no_implicit_optional = true -[[tool.mypy.overrides]] -module = "gi.*" -ignore_missing_imports = true +# [[tool.mypy.overrides]] +# module = "gi.*" +# ignore_missing_imports = true [[tool.mypy.overrides]] module = "clan_cli.*" diff --git a/pkgs/clan-vm-manager/shell.nix b/pkgs/clan-vm-manager/shell.nix index 07d355fa..1fc0bddd 100644 --- a/pkgs/clan-vm-manager/shell.nix +++ b/pkgs/clan-vm-manager/shell.nix @@ -24,7 +24,7 @@ mkShell ( python3Packages.ipdb gtk4.dev libadwaita.devdoc # has the demo called 'adwaita-1-demo' - ] ++ clan-vm-manager.nativeBuildInputs; + ] ++ clan-vm-manager.nativeBuildInputs ++ clan-vm-manager.propagatedBuildInputs; PYTHONBREAKPOINT = "ipdb.set_trace"; From 5c8343d943f737b4ca125ac948c759bddaa30128 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Sat, 9 Mar 2024 23:17:00 +0700 Subject: [PATCH 19/63] clan-vm-manager: Remove mypy ignore clan_cli types --- pkgs/clan-vm-manager/pyproject.toml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pkgs/clan-vm-manager/pyproject.toml b/pkgs/clan-vm-manager/pyproject.toml index 2eeacbac..b2061a55 100644 --- a/pkgs/clan-vm-manager/pyproject.toml +++ b/pkgs/clan-vm-manager/pyproject.toml @@ -22,14 +22,6 @@ disallow_untyped_calls = true disallow_untyped_defs = true no_implicit_optional = true -# [[tool.mypy.overrides]] -# module = "gi.*" -# ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = "clan_cli.*" -ignore_missing_imports = true - [tool.ruff] target-version = "py311" line-length = 88 From 14900a702b9a2eaf22266c09d72ad84788878ecb Mon Sep 17 00:00:00 2001 From: Qubasa Date: Sat, 9 Mar 2024 23:51:59 +0700 Subject: [PATCH 20/63] clan-vm-manager: Readd ignore for clan_cli because nix fmt fails in CI. But why \? --- pkgs/clan-vm-manager/pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkgs/clan-vm-manager/pyproject.toml b/pkgs/clan-vm-manager/pyproject.toml index b2061a55..8016c21e 100644 --- a/pkgs/clan-vm-manager/pyproject.toml +++ b/pkgs/clan-vm-manager/pyproject.toml @@ -22,6 +22,10 @@ disallow_untyped_calls = true disallow_untyped_defs = true no_implicit_optional = true +[[tool.mypy.overrides]] +module = "clan_cli.*" +ignore_missing_imports = true + [tool.ruff] target-version = "py311" line-length = 88 From 167f7f4eb3e4311077070a6ff846157cb77df1a8 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Sun, 10 Mar 2024 15:18:18 +0700 Subject: [PATCH 21/63] clan-cli: Add py.typed to make mypy work when used as library in clan-vm-manager --- pkgs/clan-cli/clan_cli/py.typed | 0 pkgs/clan-cli/pyproject.toml | 2 +- pkgs/clan-vm-manager/pyproject.toml | 3 --- 3 files changed, 1 insertion(+), 4 deletions(-) create mode 100644 pkgs/clan-cli/clan_cli/py.typed diff --git a/pkgs/clan-cli/clan_cli/py.typed b/pkgs/clan-cli/clan_cli/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/pkgs/clan-cli/pyproject.toml b/pkgs/clan-cli/pyproject.toml index 7800c659..e9d42d09 100644 --- a/pkgs/clan-cli/pyproject.toml +++ b/pkgs/clan-cli/pyproject.toml @@ -12,7 +12,7 @@ scripts = { clan = "clan_cli:main" } exclude = ["clan_cli.nixpkgs*", "result"] [tool.setuptools.package-data] -clan_cli = ["config/jsonschema/*", "webui/assets/**/*", "vms/mimetypes/**/*"] +clan_cli = ["py.typed", "config/jsonschema/*", "webui/assets/**/*", "vms/mimetypes/**/*"] [tool.pytest.ini_options] testpaths = "tests" diff --git a/pkgs/clan-vm-manager/pyproject.toml b/pkgs/clan-vm-manager/pyproject.toml index 8016c21e..73efaefa 100644 --- a/pkgs/clan-vm-manager/pyproject.toml +++ b/pkgs/clan-vm-manager/pyproject.toml @@ -22,9 +22,6 @@ disallow_untyped_calls = true disallow_untyped_defs = true no_implicit_optional = true -[[tool.mypy.overrides]] -module = "clan_cli.*" -ignore_missing_imports = true [tool.ruff] target-version = "py311" From 129a1516f6b5810899cb4724865ee62d25b6a5de Mon Sep 17 00:00:00 2001 From: Qubasa Date: Sun, 10 Mar 2024 16:06:03 +0700 Subject: [PATCH 22/63] clan-cli: Readd mypy ignore clan_cli because of treefmt complaining --- formatter.nix | 1 + pkgs/clan-cli/pyproject.toml | 1 - pkgs/clan-vm-manager/pyproject.toml | 3 +++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/formatter.nix b/formatter.nix index 2b7b6d0a..852c8cd7 100644 --- a/formatter.nix +++ b/formatter.nix @@ -15,6 +15,7 @@ treefmt.programs.mypy.directories = { "pkgs/clan-cli".extraPythonPackages = self'.packages.clan-cli.pytestDependencies; "pkgs/clan-vm-manager".extraPythonPackages = self'.packages.clan-vm-manager.propagatedBuildInputs; + # "pkgs/clan-vm-manager".options = ["--verbose"]; }; treefmt.settings.formatter.nix = { diff --git a/pkgs/clan-cli/pyproject.toml b/pkgs/clan-cli/pyproject.toml index e9d42d09..9193d90a 100644 --- a/pkgs/clan-cli/pyproject.toml +++ b/pkgs/clan-cli/pyproject.toml @@ -29,7 +29,6 @@ warn_redundant_casts = true disallow_untyped_calls = true disallow_untyped_defs = true no_implicit_optional = true -disable_error_code = ["has-type"] exclude = "clan_cli.nixpkgs" [[tool.mypy.overrides]] diff --git a/pkgs/clan-vm-manager/pyproject.toml b/pkgs/clan-vm-manager/pyproject.toml index 73efaefa..8016c21e 100644 --- a/pkgs/clan-vm-manager/pyproject.toml +++ b/pkgs/clan-vm-manager/pyproject.toml @@ -22,6 +22,9 @@ disallow_untyped_calls = true disallow_untyped_defs = true no_implicit_optional = true +[[tool.mypy.overrides]] +module = "clan_cli.*" +ignore_missing_imports = true [tool.ruff] target-version = "py311" From ee8fa1da0ac1752e2a92449b308bb1efe90d799c Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sat, 9 Mar 2024 16:45:54 +0100 Subject: [PATCH 23/63] vm-manager: add toast overlay to main window --- pkgs/clan-vm-manager/README.md | 6 +++ .../clan_vm_manager/assets/style.css | 1 + .../clan_vm_manager/singletons/toast.py | 54 +++++++++++++++++++ .../clan_vm_manager/views/list.py | 22 ++++++-- .../clan_vm_manager/windows/main_window.py | 8 ++- 5 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py diff --git a/pkgs/clan-vm-manager/README.md b/pkgs/clan-vm-manager/README.md index 02a1366c..8a31c44e 100644 --- a/pkgs/clan-vm-manager/README.md +++ b/pkgs/clan-vm-manager/README.md @@ -86,3 +86,9 @@ Here are some important documentation links related to the Clan VM Manager: - [Python + GTK3 Tutorial](https://python-gtk-3-tutorial.readthedocs.io/en/latest/textview.html): Although the Clan VM Manager uses GTK4, this tutorial for GTK3 can still be useful as it covers the basics of building GTK-based applications with Python. It includes examples and explanations for various GTK widgets, including text views. - [GNOME Human Interface Guidelines](https://developer.gnome.org/hig/): This link provides the GNOME Human Interface Guidelines, which offer design and usability recommendations for creating GNOME applications. It covers topics such as layout, navigation, and interaction patterns. + +## Error handling + +> Error dialogs should be avoided where possible, since they are disruptive. +> +> For simple non-critical errors, toasts can be a good alternative. \ No newline at end of file diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css b/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css index 1284730e..5799ba2a 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css +++ b/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css @@ -17,6 +17,7 @@ avatar { } .join-list { + margin-top: 1px; margin-left: 2px; margin-right: 2px; diff --git a/pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py b/pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py new file mode 100644 index 00000000..41acfc61 --- /dev/null +++ b/pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py @@ -0,0 +1,54 @@ +import logging +from typing import Any + +import gi + +gi.require_version("Gtk", "4.0") +gi.require_version("Adw", "1") + +from gi.repository import Adw + +log = logging.getLogger(__name__) + +class ToastOverlay: + """ + The ToastOverlay is a class that manages the display of toasts + It should be used as a singleton in your application to prevent duplicate toasts + Usage + """ + # For some reason, the adw toast overlay cannot be subclassed + # Thats why it is added as a class property + overlay: Adw.ToastOverlay + active_toasts: set[str] + + _instance: "None | ToastOverlay" = None + + def __init__(self) -> None: + raise RuntimeError("Call use() instead") + + @classmethod + def use(cls: Any) -> "ToastOverlay": + if cls._instance is None: + cls._instance = cls.__new__(cls) + cls.overlay = Adw.ToastOverlay() + cls.active_toasts = set() + + return cls._instance + + def add_toast_unique(self, toast: Adw.Toast, key: str) -> None: + if key not in self.active_toasts: + self.active_toasts.add(key) + self.overlay.add_toast(toast) + toast.connect("dismissed", lambda toast: self.active_toasts.remove(key)) + + +class ErrorToast: + toast: Adw.Toast + + def __init__(self, message: str): + super().__init__() + self.toast = Adw.Toast.new(f"Error: {message}") + self.toast.set_priority(Adw.ToastPriority.HIGH) + + + \ No newline at end of file diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py index 438e2452..c89cf878 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py @@ -9,6 +9,7 @@ from clan_cli.clan_uri import ClanURI from clan_vm_manager.components.interfaces import ClanConfig from clan_vm_manager.components.vmobj import VMObject +from clan_vm_manager.singletons.toast import ErrorToast, ToastOverlay from clan_vm_manager.singletons.use_join import JoinList, JoinValue from clan_vm_manager.singletons.use_vms import ClanStore, VMStore @@ -86,7 +87,7 @@ class ClanList(Gtk.Box): assert app is not None app.add_action(add_action) - menu_model = Gio.Menu() + # menu_model = Gio.Menu() # TODO: Make this lazy, blocks UI startup for too long # for vm in machines.list.list_machines(flake_url=vm.data.flake.flake_url): # if vm not in vm_store: @@ -95,10 +96,18 @@ class ClanList(Gtk.Box): box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) box.set_valign(Gtk.Align.CENTER) - add_button = Gtk.MenuButton() - add_button.set_has_frame(False) - add_button.set_menu_model(menu_model) - add_button.set_label("Add machine") + + add_button = Gtk.Button() + add_button_content = Adw.ButtonContent.new() + add_button_content.set_label("Add machine") + add_button_content.set_icon_name("list-add-symbolic") + add_button.add_css_class("flat") + add_button.set_child(add_button_content) + + + # add_button.set_has_frame(False) + # add_button.set_menu_model(menu_model) + # add_button.set_label("Add machine") box.append(add_button) grp.set_header_suffix(box) @@ -207,6 +216,9 @@ class ClanList(Gtk.Box): if vm is not None: sub = row.get_subtitle() assert sub is not None + + ToastOverlay.use().add_toast_unique(ErrorToast("Already exists. Joining again will update it").toast, "warning.duplicate.join") + row.set_subtitle( sub + "\nClan already exists. Joining again will update it" ) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py index 895acc80..a7b2cda3 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py @@ -5,6 +5,7 @@ import gi from clan_cli.history.list import list_history from clan_vm_manager.components.interfaces import ClanConfig +from clan_vm_manager.singletons.toast import ToastOverlay from clan_vm_manager.singletons.use_views import ViewStack from clan_vm_manager.singletons.use_vms import ClanStore from clan_vm_manager.views.details import Details @@ -24,9 +25,12 @@ class MainWindow(Adw.ApplicationWindow): super().__init__() self.set_title("cLAN Manager") self.set_default_size(980, 650) - + + overlay = ToastOverlay.use().overlay view = Adw.ToolbarView() - self.set_content(view) + overlay.set_child(view) + + self.set_content(overlay) header = Adw.HeaderBar() view.add_top_bar(header) From 4687c816ab76ec39566cb35b7f3fe6582a2c4809 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sun, 10 Mar 2024 13:18:01 +0100 Subject: [PATCH 24/63] clan-vm-manager: add log view --- .../clan_vm_manager/assets/style.css | 7 ++ .../clan_vm_manager/singletons/toast.py | 21 +++++- .../clan_vm_manager/views/list.py | 9 ++- .../clan_vm_manager/views/logs.py | 69 +++++++++++++++++++ .../clan_vm_manager/windows/main_window.py | 4 +- 5 files changed, 101 insertions(+), 9 deletions(-) create mode 100644 pkgs/clan-vm-manager/clan_vm_manager/views/logs.py diff --git a/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css b/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css index 5799ba2a..c179744d 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css +++ b/pkgs/clan-vm-manager/clan_vm_manager/assets/style.css @@ -57,3 +57,10 @@ avatar { searchbar { margin-bottom: 25px; } + + +.log-view { + margin-top: 12px; + font-family: monospace; + padding: 8px; +} diff --git a/pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py b/pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py index 41acfc61..941fc5d9 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py @@ -8,16 +8,21 @@ gi.require_version("Adw", "1") from gi.repository import Adw +from clan_vm_manager.singletons.use_views import ViewStack +from clan_vm_manager.views.logs import Logs + log = logging.getLogger(__name__) + class ToastOverlay: """ The ToastOverlay is a class that manages the display of toasts It should be used as a singleton in your application to prevent duplicate toasts Usage """ + # For some reason, the adw toast overlay cannot be subclassed - # Thats why it is added as a class property + # Thats why it is added as a class property overlay: Adw.ToastOverlay active_toasts: set[str] @@ -45,10 +50,20 @@ class ToastOverlay: class ErrorToast: toast: Adw.Toast - def __init__(self, message: str): + def __init__(self, message: str) -> None: super().__init__() self.toast = Adw.Toast.new(f"Error: {message}") self.toast.set_priority(Adw.ToastPriority.HIGH) + self.toast.set_button_label("details") - \ No newline at end of file + views = ViewStack.use().view + + # we cannot check this type, python is not smart enough + logs_view: Logs = views.get_child_by_name("logs") # type: ignore + logs_view.set_message(message) + + self.toast.connect( + "button-clicked", + lambda _: views.set_visible_child_name("logs"), + ) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py index c89cf878..c9e30da0 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py @@ -4,7 +4,6 @@ from functools import partial from typing import Any, TypeVar import gi -from clan_cli import history from clan_cli.clan_uri import ClanURI from clan_vm_manager.components.interfaces import ClanConfig @@ -57,7 +56,6 @@ class ClanList(Gtk.Box): app.connect("join_request", self.on_join_request) self.log_label: Gtk.Label = Gtk.Label() - self.__init_machines = history.add.list_history() # Add join list self.join_boxed_list = create_boxed_list( @@ -96,7 +94,6 @@ class ClanList(Gtk.Box): box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) box.set_valign(Gtk.Align.CENTER) - add_button = Gtk.Button() add_button_content = Adw.ButtonContent.new() add_button_content.set_label("Add machine") @@ -104,7 +101,6 @@ class ClanList(Gtk.Box): add_button.add_css_class("flat") add_button.set_child(add_button_content) - # add_button.set_has_frame(False) # add_button.set_menu_model(menu_model) # add_button.set_label("Add machine") @@ -217,7 +213,10 @@ class ClanList(Gtk.Box): sub = row.get_subtitle() assert sub is not None - ToastOverlay.use().add_toast_unique(ErrorToast("Already exists. Joining again will update it").toast, "warning.duplicate.join") + ToastOverlay.use().add_toast_unique( + ErrorToast("Already exists. Joining again will update it").toast, + "warning.duplicate.join", + ) row.set_subtitle( sub + "\nClan already exists. Joining again will update it" diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/logs.py b/pkgs/clan-vm-manager/clan_vm_manager/views/logs.py new file mode 100644 index 00000000..2374ba7f --- /dev/null +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/logs.py @@ -0,0 +1,69 @@ +import logging + +import gi + +gi.require_version("Adw", "1") +from gi.repository import Adw, Gio, Gtk + +from clan_vm_manager.singletons.use_views import ViewStack + +log = logging.getLogger(__name__) + + +class Logs(Gtk.Box): + """ + Simple log view + This includes a banner and a text view and a button to close the log and navigate back to the overview + """ + + def __init__(self) -> None: + super().__init__(orientation=Gtk.Orientation.VERTICAL) + + app = Gio.Application.get_default() + assert app is not None + + self.banner = Adw.Banner.new("Error details") + self.banner.set_revealed(True) + + close_button = Gtk.Button() + button_content = Adw.ButtonContent.new() + button_content.set_label("Back") + button_content.set_icon_name("go-previous-symbolic") + close_button.add_css_class("flat") + close_button.set_child(button_content) + close_button.connect( + "clicked", + lambda _: ViewStack.use().view.set_visible_child_name("list"), + ) + + self.close_button = close_button + + self.text_view = Gtk.TextView() + self.text_view.set_editable(False) + self.text_view.set_wrap_mode(Gtk.WrapMode.WORD) + self.text_view.add_css_class("log-view") + + self.append(self.close_button) + self.append(self.banner) + self.append(self.text_view) + + def set_message(self, message: str) -> None: + """ + Set the log message. This will delete any previous message + """ + buffer = self.text_view.get_buffer() + buffer.set_text(message) + + mark = buffer.create_mark(None, buffer.get_end_iter(), False) # type: ignore + self.text_view.scroll_to_mark(mark, 0.05, True, 0.0, 1.0) + + def append_message(self, message: str) -> None: + """ + Append to the end of a potentially existent log message + """ + buffer = self.text_view.get_buffer() + end_iter = buffer.get_end_iter() + buffer.insert(end_iter, message) # type: ignore + + mark = buffer.create_mark(None, buffer.get_end_iter(), False) # type: ignore + self.text_view.scroll_to_mark(mark, 0.05, True, 0.0, 1.0) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py index a7b2cda3..77bd4e87 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py @@ -10,6 +10,7 @@ from clan_vm_manager.singletons.use_views import ViewStack from clan_vm_manager.singletons.use_vms import ClanStore from clan_vm_manager.views.details import Details from clan_vm_manager.views.list import ClanList +from clan_vm_manager.views.logs import Logs gi.require_version("Adw", "1") @@ -25,7 +26,7 @@ class MainWindow(Adw.ApplicationWindow): super().__init__() self.set_title("cLAN Manager") self.set_default_size(980, 650) - + overlay = ToastOverlay.use().overlay view = Adw.ToolbarView() overlay.set_child(view) @@ -52,6 +53,7 @@ class MainWindow(Adw.ApplicationWindow): stack_view.add_named(scroll, "list") stack_view.add_named(Details(), "details") + stack_view.add_named(Logs(), "logs") stack_view.set_visible_child_name(config.initial_view) From b1897530c83eb49c0af395329cf9c1d4d417770b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 8 Mar 2024 11:12:34 +0100 Subject: [PATCH 25/63] clan.borgbackup: drop enable option --- checks/backups/flake-module.nix | 5 +--- checks/borgbackup/default.nix | 5 +--- clanModules/borgbackup.nix | 53 +++++++++++++++++---------------- 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/checks/backups/flake-module.nix b/checks/backups/flake-module.nix index 4ebecbfd..b838dc71 100644 --- a/checks/backups/flake-module.nix +++ b/checks/backups/flake-module.nix @@ -75,10 +75,7 @@ in }; system.extraDependencies = dependencies; clanCore.state.test-backups.folders = [ "/var/test-backups" ]; - clan.borgbackup = { - enable = true; - destinations.test_backup_server.repo = "borg@server:."; - }; + clan.borgbackup.destinations.test_backup_server.repo = "borg@server:."; }; }; perSystem = { nodes, pkgs, ... }: { diff --git a/checks/borgbackup/default.nix b/checks/borgbackup/default.nix index 5c466cc1..6dd30e44 100644 --- a/checks/borgbackup/default.nix +++ b/checks/borgbackup/default.nix @@ -36,10 +36,7 @@ }; clanCore.secretStore = "vm"; - clan.borgbackup = { - enable = true; - destinations.test.repo = "borg@localhost:."; - }; + clan.borgbackup.destinations.test.repo = "borg@localhost:."; } ]; }; diff --git a/clanModules/borgbackup.nix b/clanModules/borgbackup.nix index 06358e6f..90a2b086 100644 --- a/clanModules/borgbackup.nix +++ b/clanModules/borgbackup.nix @@ -3,34 +3,35 @@ let cfg = config.clan.borgbackup; in { - options.clan.borgbackup = { - enable = lib.mkEnableOption "backups with borgbackup"; - destinations = lib.mkOption { - type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: { - options = { - name = lib.mkOption { - type = lib.types.str; - default = name; - description = "the name of the backup job"; - }; - repo = lib.mkOption { - type = lib.types.str; - description = "the borgbackup repository to backup to"; - }; - rsh = lib.mkOption { - type = lib.types.str; - default = "ssh -i ${config.clanCore.secrets.borgbackup.secrets."borgbackup.ssh".path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"; - description = "the rsh to use for the backup"; - }; - + options.clan.borgbackup.destinations = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: { + options = { + name = lib.mkOption { + type = lib.types.str; + default = name; + description = "the name of the backup job"; }; - })); - description = '' - destinations where the machine should be backuped to - ''; - }; + repo = lib.mkOption { + type = lib.types.str; + description = "the borgbackup repository to backup to"; + }; + rsh = lib.mkOption { + type = lib.types.str; + default = "ssh -i ${config.clanCore.secrets.borgbackup.secrets."borgbackup.ssh".path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"; + description = "the rsh to use for the backup"; + }; + + }; + })); + default = { }; + description = '' + destinations where the machine should be backuped to + ''; }; - config = lib.mkIf cfg.enable { + + imports = [ (lib.mkRemovedOptionModule [ "clan" "borgbackup" "enable" ] "Just define clan.borgbackup.destinations to enable it") ]; + + config = lib.mkIf (cfg.destinations != [ ]) { services.borgbackup.jobs = lib.mapAttrs (_: dest: { paths = lib.flatten (map (state: state.folders) (lib.attrValues config.clanCore.state)); From 349d3b379c2914e4d0372048669b137a3d2d1bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 12 Mar 2024 13:06:12 +0100 Subject: [PATCH 26/63] update flake --- flake.lock | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/flake.lock b/flake.lock index 30440e79..1c25ea82 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1709632354, - "narHash": "sha256-jxRHwqrtNze51WKFKvxlQ8Inf62UNRl5cFqEQ2V96vE=", + "lastModified": 1710169806, + "narHash": "sha256-HeWFrRuHpnAiPmIr26OKl2g142HuGerwoO/XtW53pcI=", "owner": "nix-community", "repo": "disko", - "rev": "0d11aa8d6431326e10b8656420f91085c3bd0b12", + "rev": "fe064a639319ed61cdf12b8f6eded9523abcc498", "type": "github" }, "original": { @@ -27,11 +27,11 @@ ] }, "locked": { - "lastModified": 1706830856, - "narHash": "sha256-a0NYyp+h9hlb7ddVz4LUn1vT/PLwqfrWYcHMvFB1xYg=", + "lastModified": 1709336216, + "narHash": "sha256-Dt/wOWeW6Sqm11Yh+2+t0dfEWxoMxGBvv3JpIocFl9E=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "b253292d9c0a5ead9bc98c4e9a26c6312e27d69f", + "rev": "f7b3c975cf067e56e7cda6cb098ebe3fb4d74ca2", "type": "github" }, "original": { @@ -42,11 +42,11 @@ }, "nixlib": { "locked": { - "lastModified": 1708217146, - "narHash": "sha256-nGfEv7k78slqIR5E0zzWSx214d/4/ZPKDkObLJqVLVw=", + "lastModified": 1709426687, + "narHash": "sha256-jLBZmwXf0WYHzLkmEMq33bqhX55YtT5edvluFr0RcSA=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "e623008d8a46517470e6365505f1a3ce171fa46a", + "rev": "7873d84a89ae6e4841528ff7f5697ddcb5bdfe6c", "type": "github" }, "original": { @@ -63,11 +63,11 @@ ] }, "locked": { - "lastModified": 1708563055, - "narHash": "sha256-FaojUZNu+YPFi3eCI7mL4kxPKQ51DoySa7mqmllUOuc=", + "lastModified": 1710164763, + "narHash": "sha256-6p7yebSjzrL8qK4Q0gx2RnsxaudGUQcgkSxFG/J265Y=", "owner": "nix-community", "repo": "nixos-generators", - "rev": "f4631dee1a0fd56c0db89860e83e3588a28c7631", + "rev": "1d9c8cd24eba7942955f92fdcefba5a6a7543bc6", "type": "github" }, "original": { @@ -78,11 +78,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1709764733, - "narHash": "sha256-GptBnEUy8IcRrnd8X5WBJPDXG7M4bjj8OG4Wjg8dCDs=", + "lastModified": 1710213926, + "narHash": "sha256-D6wdwb289veivPoRV5/+IZaUG/XrdJPHpbR08cA5og0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "edf9f14255a7ac20f8da7b70609e980a964fca7a", + "rev": "e4e2121b151e492fd15d4bdb034e793738fdc120", "type": "github" }, "original": { @@ -110,11 +110,11 @@ "nixpkgs-stable": [] }, "locked": { - "lastModified": 1708830076, - "narHash": "sha256-Cjh2xdjxC6S6nW6Whr2dxSeh8vjodzhTmQdI4zPJ4RA=", + "lastModified": 1710195194, + "narHash": "sha256-KFxCJp0T6TJOz1IOKlpRdpsCr9xsvlVuWY/VCiAFnTE=", "owner": "Mic92", "repo": "sops-nix", - "rev": "2874fbbe4a65bd2484b0ad757d27a16107f6bc17", + "rev": "e52d8117b330f690382f1d16d81ae43daeb4b880", "type": "github" }, "original": { @@ -130,11 +130,11 @@ ] }, "locked": { - "lastModified": 1708897213, - "narHash": "sha256-QECZB+Hgz/2F/8lWvHNk05N6NU/rD9bWzuNn6Cv8oUk=", + "lastModified": 1710088047, + "narHash": "sha256-eSqKs6ZCsX9xJyNYLeMDMrxzIDsYtaWClfZCOp0ok6Y=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "e497a9ddecff769c2a7cbab51e1ed7a8501e7a3a", + "rev": "720322c5352d7b7bd2cb3601a9176b0e91d1de7d", "type": "github" }, "original": { From 823b5e67ed00be50b6f017ba9e2150acb766c85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 12 Mar 2024 13:17:04 +0100 Subject: [PATCH 27/63] fix backup not beeing activated --- clanModules/borgbackup.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clanModules/borgbackup.nix b/clanModules/borgbackup.nix index 90a2b086..d7ded090 100644 --- a/clanModules/borgbackup.nix +++ b/clanModules/borgbackup.nix @@ -31,7 +31,7 @@ in imports = [ (lib.mkRemovedOptionModule [ "clan" "borgbackup" "enable" ] "Just define clan.borgbackup.destinations to enable it") ]; - config = lib.mkIf (cfg.destinations != [ ]) { + config = lib.mkIf (cfg.destinations != { }) { services.borgbackup.jobs = lib.mapAttrs (_: dest: { paths = lib.flatten (map (state: state.folders) (lib.attrValues config.clanCore.state)); From 5d5f5040137b8ccdfecff013c4a9b3c18678ff8e Mon Sep 17 00:00:00 2001 From: a-kenji Date: Tue, 12 Mar 2024 16:30:20 +0100 Subject: [PATCH 28/63] enable: spice-vdagent if xserver is enable --- nixosModules/clanCore/vm.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nixosModules/clanCore/vm.nix b/nixosModules/clanCore/vm.nix index 89a23e9f..be1d429c 100644 --- a/nixosModules/clanCore/vm.nix +++ b/nixosModules/clanCore/vm.nix @@ -24,6 +24,9 @@ let services.acpid.handlers.power.event = "button/power.*"; services.acpid.handlers.power.action = "poweroff"; + # only works on x11 + services.spice-vdagentd.enable = config.services.xserver.enable; + boot.initrd.systemd.enable = true; # currently needed for system.etc.overlay.enable From 934cf6e57a6ed8df04339f9fd70a7ba0bc4cb565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 12 Mar 2024 16:49:18 +0100 Subject: [PATCH 29/63] mypy: fix clan-cli import in vm-manager --- formatter.nix | 1 - pkgs/clan-vm-manager/default.nix | 6 +++++- pkgs/clan-vm-manager/pyproject.toml | 4 ---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/formatter.nix b/formatter.nix index 852c8cd7..2b7b6d0a 100644 --- a/formatter.nix +++ b/formatter.nix @@ -15,7 +15,6 @@ treefmt.programs.mypy.directories = { "pkgs/clan-cli".extraPythonPackages = self'.packages.clan-cli.pytestDependencies; "pkgs/clan-vm-manager".extraPythonPackages = self'.packages.clan-vm-manager.propagatedBuildInputs; - # "pkgs/clan-vm-manager".options = ["--verbose"]; }; treefmt.settings.formatter.nix = { diff --git a/pkgs/clan-vm-manager/default.nix b/pkgs/clan-vm-manager/default.nix index a5d2c15d..13fb2bbe 100644 --- a/pkgs/clan-vm-manager/default.nix +++ b/pkgs/clan-vm-manager/default.nix @@ -44,7 +44,11 @@ python3.pkgs.buildPythonApplication { buildInputs = [ gtk4 libadwaita gnome.adwaita-icon-theme ]; # We need to propagate the build inputs to nix fmt / treefmt - propagatedBuildInputs = [ pygobject3 clan-cli pygobject-stubs ]; + propagatedBuildInputs = [ + (python3.pkgs.toPythonModule clan-cli) + pygobject3 + pygobject-stubs + ]; # also re-expose dependencies so we test them in CI passthru = { diff --git a/pkgs/clan-vm-manager/pyproject.toml b/pkgs/clan-vm-manager/pyproject.toml index 8016c21e..b2061a55 100644 --- a/pkgs/clan-vm-manager/pyproject.toml +++ b/pkgs/clan-vm-manager/pyproject.toml @@ -22,10 +22,6 @@ disallow_untyped_calls = true disallow_untyped_defs = true no_implicit_optional = true -[[tool.mypy.overrides]] -module = "clan_cli.*" -ignore_missing_imports = true - [tool.ruff] target-version = "py311" line-length = 88 From 38190adfb196042f33221b57e423ef0d8ea0bc1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 12 Mar 2024 17:09:26 +0100 Subject: [PATCH 30/63] workaround gitea bug --- .gitea/workflows/{checks.yaml => build.yaml} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename .gitea/workflows/{checks.yaml => build.yaml} (91%) diff --git a/.gitea/workflows/checks.yaml b/.gitea/workflows/build.yaml similarity index 91% rename from .gitea/workflows/checks.yaml rename to .gitea/workflows/build.yaml index 7804a165..7fa8d337 100644 --- a/.gitea/workflows/checks.yaml +++ b/.gitea/workflows/build.yaml @@ -5,17 +5,17 @@ on: branches: - main jobs: - checks: + test: runs-on: nix steps: - uses: actions/checkout@v3 - run: nix run --refresh github:Mic92/nix-fast-build -- --no-nom --eval-workers 20 - check-links: + test-links: runs-on: nix steps: - uses: actions/checkout@v3 - run: nix run --refresh --inputs-from .# nixpkgs#lychee . - checks-impure: + test-impure: runs-on: nix steps: - uses: actions/checkout@v3 From b5433beef9d7a5794b0c1910ee772223970719e1 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Tue, 12 Mar 2024 16:43:31 +0100 Subject: [PATCH 31/63] clan-modules: add vm-user module --- clanModules/flake-module.nix | 2 ++ clanModules/graphical.nix | 4 ++++ clanModules/vm-user.nix | 20 ++++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 clanModules/graphical.nix create mode 100644 clanModules/vm-user.nix diff --git a/clanModules/flake-module.nix b/clanModules/flake-module.nix index d618a39a..221e10ff 100644 --- a/clanModules/flake-module.nix +++ b/clanModules/flake-module.nix @@ -11,6 +11,8 @@ moonlight = ./moonlight.nix; sunshine = ./sunshine.nix; syncthing = ./syncthing.nix; + vm-user = ./vm-user.nix; + graphical = ./graphical.nix; xfce = ./xfce.nix; zt-tcp-relay = ./zt-tcp-relay.nix; localsend = ./localsend.nix; diff --git a/clanModules/graphical.nix b/clanModules/graphical.nix new file mode 100644 index 00000000..5f8a0fd5 --- /dev/null +++ b/clanModules/graphical.nix @@ -0,0 +1,4 @@ +_: +{ + fonts.enableDefaultPackages = true; +} diff --git a/clanModules/vm-user.nix b/clanModules/vm-user.nix new file mode 100644 index 00000000..28e93535 --- /dev/null +++ b/clanModules/vm-user.nix @@ -0,0 +1,20 @@ +{ + security = { + sudo.wheelNeedsPassword = false; + polkit.enable = true; + rtkit.enable = true; + }; + + users.users.user = { + isNormalUser = true; + createHome = true; + uid = 1000; + initialHashedPassword = ""; + extraGroups = [ + "wheel" + "video" + "render" + ]; + shell = "/run/current-system/sw/bin/bash"; + }; +} From 0c688a0919e09b4f5c0e6c8565a1d6f53a24a68d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 12 Mar 2024 17:23:12 +0100 Subject: [PATCH 32/63] Revert "workaround gitea bug" This reverts commit 38190adfb196042f33221b57e423ef0d8ea0bc1e. --- .gitea/workflows/{build.yaml => checks.yaml} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename .gitea/workflows/{build.yaml => checks.yaml} (91%) diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/checks.yaml similarity index 91% rename from .gitea/workflows/build.yaml rename to .gitea/workflows/checks.yaml index 7fa8d337..7804a165 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/checks.yaml @@ -5,17 +5,17 @@ on: branches: - main jobs: - test: + checks: runs-on: nix steps: - uses: actions/checkout@v3 - run: nix run --refresh github:Mic92/nix-fast-build -- --no-nom --eval-workers 20 - test-links: + check-links: runs-on: nix steps: - uses: actions/checkout@v3 - run: nix run --refresh --inputs-from .# nixpkgs#lychee . - test-impure: + checks-impure: runs-on: nix steps: - uses: actions/checkout@v3 From 4e5d0518474ba2a6037074105be07f8ae8dd5d6d Mon Sep 17 00:00:00 2001 From: Qubasa Date: Tue, 12 Mar 2024 23:19:20 +0700 Subject: [PATCH 33/63] clan_vm_manager: Fix mypy errors for clan_cli types --- .../clan_vm_manager/components/vmobj.py | 3 ++- .../clan_vm_manager/singletons/use_join.py | 1 + .../clan_vm_manager/singletons/use_vms.py | 10 +++++----- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py index 0c885b1a..5caf7b02 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py @@ -116,7 +116,7 @@ class VMObject(GObject.Object): @contextmanager def _create_machine(self) -> Generator[Machine, None, None]: uri = ClanURI.from_str( - url=self.data.flake.flake_url, machine_name=self.data.flake.flake_attr + url=str(self.data.flake.flake_url), machine_name=self.data.flake.flake_attr ) if uri.flake_id.is_local(): self.machine = Machine( @@ -128,6 +128,7 @@ class VMObject(GObject.Object): name=self.data.flake.flake_attr, flake=uri.flake_id.url, ) + assert self.machine is not None yield self.machine self.machine = None diff --git a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_join.py b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_join.py index b52b41ef..6e71d166 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_join.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_join.py @@ -97,6 +97,7 @@ class JoinList: def _on_join_finished(self, source: JoinValue) -> None: log.info(f"Join finished: {source.url}") self.discard(source) + assert source.entry is not None ClanStore.use().push_history_entry(source.entry) def discard(self, value: JoinValue) -> None: diff --git a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py index 71854844..8a725482 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py @@ -73,18 +73,18 @@ class ClanStore: def push_history_entry(self, entry: HistoryEntry) -> None: # TODO: We shouldn't do this here but in the list view if entry.flake.icon is None: - icon = assets.loc / "placeholder.jpeg" + icon: Path = assets.loc / "placeholder.jpeg" else: - icon = entry.flake.icon + icon = Path(entry.flake.icon) vm = VMObject( - icon=Path(icon), + icon=icon, data=entry, ) self.push(vm) def push(self, vm: VMObject) -> None: - url = vm.data.flake.flake_url + url = str(vm.data.flake.flake_url) # Only write to the store if the Clan is not already in it # Every write to the KVStore rerenders bound widgets to the clan_store @@ -108,7 +108,7 @@ class ClanStore: vm_store.append(vm) def remove(self, vm: VMObject) -> None: - del self.clan_store[vm.data.flake.flake_url][vm.data.flake.flake_attr] + del self.clan_store[str(vm.data.flake.flake_url)][vm.data.flake.flake_attr] def get_vm(self, uri: ClanURI) -> None | VMObject: vm_store = self.clan_store.get(str(uri.flake_id)) From c4642ad041c197d1cbc11e21b0be958cf2405740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Tue, 12 Mar 2024 17:34:04 +0100 Subject: [PATCH 34/63] reduce eval worker --- .gitea/workflows/checks.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/checks.yaml b/.gitea/workflows/checks.yaml index 7804a165..1ad90486 100644 --- a/.gitea/workflows/checks.yaml +++ b/.gitea/workflows/checks.yaml @@ -9,7 +9,7 @@ jobs: runs-on: nix steps: - uses: actions/checkout@v3 - - run: nix run --refresh github:Mic92/nix-fast-build -- --no-nom --eval-workers 20 + - run: nix run --refresh github:Mic92/nix-fast-build -- --no-nom --eval-workers 10 check-links: runs-on: nix steps: From 4044e42e5825220a6e8a45c1e6990211adba2dba Mon Sep 17 00:00:00 2001 From: a-kenji Date: Tue, 12 Mar 2024 17:29:08 +0100 Subject: [PATCH 35/63] fix: typo --- pkgs/clan-cli/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index 2b256e8c..c2361515 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -178,7 +178,7 @@ python3.pkgs.buildPythonApplication { <(${argcomplete}/bin/register-python-argcomplete --shell fish clan) ''; # Don't leak python packages into a devshell. - # It can be very confusing if you `nix run` than load the cli from the devshell instead. + # It can be very confusing if you `nix run` then load the cli from the devshell instead. postFixup = '' rm $out/nix-support/propagated-build-inputs ''; From c15d762dc76968f7d675b6ff833f7b06bea09727 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Tue, 12 Mar 2024 19:36:11 +0100 Subject: [PATCH 36/63] clan-modules: add xfce-vm module A specific module for vm's that don't (yet) support the waypipe module. --- clanModules/flake-module.nix | 1 + clanModules/xfce-vm.nix | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 clanModules/xfce-vm.nix diff --git a/clanModules/flake-module.nix b/clanModules/flake-module.nix index 221e10ff..e6ad9109 100644 --- a/clanModules/flake-module.nix +++ b/clanModules/flake-module.nix @@ -14,6 +14,7 @@ vm-user = ./vm-user.nix; graphical = ./graphical.nix; xfce = ./xfce.nix; + xfce-vm = ./xfce-vm.nix; zt-tcp-relay = ./zt-tcp-relay.nix; localsend = ./localsend.nix; waypipe = ./waypipe.nix; diff --git a/clanModules/xfce-vm.nix b/clanModules/xfce-vm.nix new file mode 100644 index 00000000..665dfa3c --- /dev/null +++ b/clanModules/xfce-vm.nix @@ -0,0 +1,15 @@ +{ config }: { + imports = [ + config.clanCore.clanModules.vm-user + config.clanCore.clanModules.graphical + ]; + + services.xserver = { + enable = true; + displayManager.autoLogin.enable = true; + displayManager.autoLogin.user = "user"; + desktopManager.xfce.enable = true; + desktopManager.xfce.enableScreensaver = false; + xkb.layout = "us"; + }; +} From df1729a84119dfaafcde186aaa1970b073dd33ec Mon Sep 17 00:00:00 2001 From: a-kenji Date: Tue, 12 Mar 2024 19:53:11 +0100 Subject: [PATCH 37/63] vm: improve xfce and vm-user module --- clanModules/xfce-vm.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/clanModules/xfce-vm.nix b/clanModules/xfce-vm.nix index 665dfa3c..7eadd7f4 100644 --- a/clanModules/xfce-vm.nix +++ b/clanModules/xfce-vm.nix @@ -1,7 +1,7 @@ -{ config }: { +{ imports = [ - config.clanCore.clanModules.vm-user - config.clanCore.clanModules.graphical + ./vm-user.nix + ./graphical.nix ]; services.xserver = { From 8a3250b1c98623c01c3a8d4a03bd700a6bc8cbfe Mon Sep 17 00:00:00 2001 From: a-kenji Date: Tue, 12 Mar 2024 20:53:04 +0100 Subject: [PATCH 38/63] sunshine: improve module --- clanModules/sunshine.nix | 226 +++++++++++++++++++++------------------ 1 file changed, 122 insertions(+), 104 deletions(-) diff --git a/clanModules/sunshine.nix b/clanModules/sunshine.nix index 6558e47d..1086883b 100644 --- a/clanModules/sunshine.nix +++ b/clanModules/sunshine.nix @@ -1,109 +1,127 @@ -{ pkgs, config, ... }: -{ - networking.firewall = { - allowedTCPPorts = [ - 47984 - 47989 - 47990 - 48010 - ]; - - allowedUDPPorts = [ - 47998 - 47999 - 48000 - 48002 - 48010 - ]; - }; - - networking.firewall.allowedTCPPortRanges = [ - { - from = 47984; - to = 48010; - } - ]; - networking.firewall.allowedUDPPortRanges = [ - { - from = 47998; - to = 48010; - } - ]; - - environment.systemPackages = [ - pkgs.sunshine - pkgs.avahi - # Convenience script, until we find a better UX - (pkgs.writers.writeDashBin "sun" '' - ${pkgs.sunshine}/bin/sunshine -1 ${ - pkgs.writeText "sunshine.conf" '' - address_family = both - '' - } "$@" - '') - # Create a dummy account, for easier setup, - # don't use this account in actual production yet. - (pkgs.writers.writeDashBin "init-sun" '' - ${pkgs.sunshine}/bin/sunshine \ - --creds "sun" "sun" - '') - ]; - - # Required to simulate input - boot.kernelModules = [ "uinput" ]; - security.rtkit.enable = true; - - # services.udev.extraRules = '' - # KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess" - # ''; - - services.udev.extraRules = '' - KERNEL=="uinput", GROUP="input", MODE="0660" OPTIONS+="static_node=uinput" +{ pkgs, config, options, ... }: +let + cfg = options.services.sunshine; + sunshineConfiguration = pkgs.writeText "sunshine.conf" '' + address_family = both + channels = 5 + pkey = /var/lib/sunshine/sunshine.key + cert = /var/lib/sunshine/sunshine.cert + file_state = /var/lib/sunshine/state.json + file_apps = /var/lib/sunshine/apps.json + credentials_file = /var/lib/sunshine/credentials.json ''; - - security.wrappers.sunshine = { - owner = "root"; - group = "root"; - capabilities = "cap_sys_admin+p"; - source = "${pkgs.sunshine}/bin/sunshine"; +in +{ + options.services.sunshine = { + enable = pkgs.lib.mkEnableOption "Sunshine self-hosted game stream host for Moonlight"; }; - systemd.user.services.sunshine = { - description = "sunshine"; - wantedBy = [ "graphical-session.target" ]; - environment = { - DISPLAY = ":0"; - }; - serviceConfig = { - ExecStart = "${config.security.wrapperDir}/sunshine"; - }; - }; + config = pkgs.lib.mkMerge [ + (pkgs.lib.mkIf cfg.enable + { + networking.firewall = { + allowedTCPPorts = [ + 47984 + 47989 + 47990 + 48010 + ]; - # xdg.configFile."sunshine/apps.json".text = builtins.toJSON { - # env = "/run/current-system/sw/bin"; - # apps = [ - # { - # name = "Steam"; - # output = "steam.txt"; - # detached = [ - # "${pkgs.util-linux}/bin/setsid ${pkgs.steam}/bin/steam steam://open/bigpicture" - # ]; - # image-path = "steam.png"; - # } - # ]; - # }; + allowedUDPPorts = [ + 47998 + 47999 + 48000 + 48002 + 48010 + ]; + }; + networking.firewall.allowedTCPPortRanges = [ + { + from = 47984; + to = 48010; + } + ]; + networking.firewall.allowedUDPPortRanges = [ + { + from = 47998; + to = 48010; + } + ]; - services = { - avahi = { - enable = true; - reflector = true; - nssmdns = true; - publish = { - enable = true; - addresses = true; - userServices = true; - workstation = true; - }; - }; - }; -} + environment.systemPackages = [ + pkgs.sunshine + (pkgs.writers.writeDashBin "sun" '' + ${pkgs.sunshine}/bin/sunshine -1 ${ + pkgs.writeText "sunshine.conf" '' + address_family = both + '' + } "$@" + '') + # Create a dummy account, for easier setup, + # don't use this account in actual production yet. + (pkgs.writers.writeDashBin "init-sun" '' + ${pkgs.sunshine}/bin/sunshine \ + --creds "sun" "sun" + '') + ]; + + # Required to simulate input + hardware.uinput.enable = true; + boot.kernelModules = [ "uinput" ]; + # services.udev.extraRules = '' + # KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess" + # ''; + services.udev.extraRules = '' + KERNEL=="uinput", GROUP="input", MODE="0660" OPTIONS+="static_node=uinput" + ''; + hardware.opengl.driSupport32Bit = true; + hardware.opengl.enable = true; + + security = { + rtkit.enable = true; + wrappers.sunshine = { + owner = "root"; + group = "root"; + capabilities = "cap_sys_admin+p"; + source = "${pkgs.sunshine}/bin/sunshine"; + }; + }; + + + systemd.tmpfiles.rules = [ + "d '/var/lib/sunshine' 0770 'user' 'users' - -" + ]; + + + systemd.user.services.sunshine = { + enable = true; + description = "Sunshine self-hosted game stream host for Moonlight"; + startLimitBurst = 5; + startLimitIntervalSec = 500; + script = "/run/current-system/sw/bin/env /run/wrappers/bin/sunshine ${sunshineConfiguration}"; + serviceConfig = { + Restart = "on-failure"; + RestartSec = "5s"; + ReadWritePaths = [ + "/var/lib/sunshine" + ]; + }; + wantedBy = [ "graphical-session.target" ]; + }; + } + ) + ] +# xdg.configFile."sunshine/apps.json".text = builtins.toJSON { +# env = "/run/current-system/sw/bin"; +# apps = [ +# { +# name = "Steam"; +# output = "steam.txt"; +# detached = [ +# "${pkgs.util-linux}/bin/setsid ${pkgs.steam}/bin/steam steam://open/bigpicture" +# ]; +# image-path = "steam.png"; +# } +# ]; +# }; +# } From 71cd46b0e9c02d80dd1884369561c9ab2d4777b4 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Tue, 12 Mar 2024 22:14:47 +0100 Subject: [PATCH 39/63] sunshine: add apps, improve uaccess rules --- clanModules/sunshine.nix | 243 +++++++++++++++++++++------------------ 1 file changed, 134 insertions(+), 109 deletions(-) diff --git a/clanModules/sunshine.nix b/clanModules/sunshine.nix index 1086883b..e9d2d08e 100644 --- a/clanModules/sunshine.nix +++ b/clanModules/sunshine.nix @@ -1,13 +1,41 @@ -{ pkgs, config, options, ... }: +{ pkgs, options, ... }: let - cfg = options.services.sunshine; + apps = pkgs.writeText "apps.json" (builtins.toJSON + { + env = { + PATH = "$(PATH):$(HOME)/.local/bin"; + }; + apps = [ + { + name = "Desktop"; + image-path = "desktop.png"; + } + { + name = "Low Res Desktop"; + image-path = "desktop.png"; + prep-cmd = [ + { + do = "xrandr --output HDMI-1 --mode 1920x1080"; + undo = "xrandr --output HDMI-1 --mode 1920x1200"; + } + ]; + } + { + name = "Steam Big Picture"; + detached = [ + "setsid steam steam://open/bigpicture" + ]; + image-path = "steam.png"; + } + ]; + }); sunshineConfiguration = pkgs.writeText "sunshine.conf" '' address_family = both channels = 5 pkey = /var/lib/sunshine/sunshine.key cert = /var/lib/sunshine/sunshine.cert file_state = /var/lib/sunshine/state.json - file_apps = /var/lib/sunshine/apps.json + file_apps = ${apps} credentials_file = /var/lib/sunshine/credentials.json ''; in @@ -16,112 +44,109 @@ in enable = pkgs.lib.mkEnableOption "Sunshine self-hosted game stream host for Moonlight"; }; - config = pkgs.lib.mkMerge [ - (pkgs.lib.mkIf cfg.enable - { - networking.firewall = { - allowedTCPPorts = [ - 47984 - 47989 - 47990 - 48010 - ]; + imports = [ + { + networking.firewall = { + allowedTCPPorts = [ + 47984 + 47989 + 47990 + 48010 + ]; - allowedUDPPorts = [ - 47998 - 47999 - 48000 - 48002 - 48010 + allowedUDPPorts = [ + 47998 + 47999 + 48000 + 48002 + 48010 + ]; + }; + networking.firewall.allowedTCPPortRanges = [ + { + from = 47984; + to = 48010; + } + ]; + networking.firewall.allowedUDPPortRanges = [ + { + from = 47998; + to = 48010; + } + ]; + + environment.systemPackages = [ + pkgs.sunshine + (pkgs.writers.writeDashBin "sun" '' + ${pkgs.sunshine}/bin/sunshine -1 ${ + pkgs.writeText "sunshine.conf" '' + address_family = both + '' + } "$@" + '') + # Create a dummy account, for easier setup, + # don't use this account in actual production yet. + (pkgs.writers.writeDashBin "init-sun" '' + ${pkgs.sunshine}/bin/sunshine \ + --creds "sun" "sun" + '') + ]; + + # Required to simulate input + hardware.uinput.enable = true; + boot.kernelModules = [ "uinput" ]; + + services.udev.extraRules = '' + KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess" + ''; + + hardware.opengl.driSupport32Bit = true; + hardware.opengl.enable = true; + + security = { + rtkit.enable = true; + wrappers.sunshine = { + owner = "root"; + group = "root"; + capabilities = "cap_sys_admin+p"; + source = "${pkgs.sunshine}/bin/sunshine"; + }; + }; + + + systemd.tmpfiles.rules = [ + "d '/var/lib/sunshine' 0770 'user' 'users' - -" + ]; + + + systemd.user.services.sunshine = { + enable = true; + description = "Sunshine self-hosted game stream host for Moonlight"; + startLimitBurst = 5; + startLimitIntervalSec = 500; + script = "/run/current-system/sw/bin/env /run/wrappers/bin/sunshine ${sunshineConfiguration}"; + serviceConfig = { + Restart = "on-failure"; + RestartSec = "5s"; + ReadWritePaths = [ + "/var/lib/sunshine" ]; }; - networking.firewall.allowedTCPPortRanges = [ - { - from = 47984; - to = 48010; - } - ]; - networking.firewall.allowedUDPPortRanges = [ - { - from = 47998; - to = 48010; - } - ]; - - environment.systemPackages = [ - pkgs.sunshine - (pkgs.writers.writeDashBin "sun" '' - ${pkgs.sunshine}/bin/sunshine -1 ${ - pkgs.writeText "sunshine.conf" '' - address_family = both - '' - } "$@" - '') - # Create a dummy account, for easier setup, - # don't use this account in actual production yet. - (pkgs.writers.writeDashBin "init-sun" '' - ${pkgs.sunshine}/bin/sunshine \ - --creds "sun" "sun" - '') - ]; - - # Required to simulate input - hardware.uinput.enable = true; - boot.kernelModules = [ "uinput" ]; - # services.udev.extraRules = '' - # KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess" - # ''; - services.udev.extraRules = '' - KERNEL=="uinput", GROUP="input", MODE="0660" OPTIONS+="static_node=uinput" - ''; - hardware.opengl.driSupport32Bit = true; - hardware.opengl.enable = true; - - security = { - rtkit.enable = true; - wrappers.sunshine = { - owner = "root"; - group = "root"; - capabilities = "cap_sys_admin+p"; - source = "${pkgs.sunshine}/bin/sunshine"; - }; - }; - - - systemd.tmpfiles.rules = [ - "d '/var/lib/sunshine' 0770 'user' 'users' - -" - ]; - - - systemd.user.services.sunshine = { - enable = true; - description = "Sunshine self-hosted game stream host for Moonlight"; - startLimitBurst = 5; - startLimitIntervalSec = 500; - script = "/run/current-system/sw/bin/env /run/wrappers/bin/sunshine ${sunshineConfiguration}"; - serviceConfig = { - Restart = "on-failure"; - RestartSec = "5s"; - ReadWritePaths = [ - "/var/lib/sunshine" - ]; - }; - wantedBy = [ "graphical-session.target" ]; - }; - } - ) - ] -# xdg.configFile."sunshine/apps.json".text = builtins.toJSON { -# env = "/run/current-system/sw/bin"; -# apps = [ -# { -# name = "Steam"; -# output = "steam.txt"; -# detached = [ -# "${pkgs.util-linux}/bin/setsid ${pkgs.steam}/bin/steam steam://open/bigpicture" -# ]; -# image-path = "steam.png"; -# } -# ]; -# }; -# } + wantedBy = [ "graphical-session.target" ]; + }; + } + ]; + # xdg.configFile."sunshine/apps.json".text = builtins.toJSON { + # env = "/run/current-system/sw/bin"; + # apps = [ + # { + # name = "Steam"; + # output = "steam.txt"; + # detached = [ + # "${pkgs.util-linux}/bin/setsid ${pkgs.steam}/bin/steam steam://open/bigpicture" + # ]; + # image-path = "steam.png"; + # } + # ]; + # }; +} From bcf26682c3fb499beb2c0f0f2f34fa5a4dc88dd4 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Tue, 12 Mar 2024 23:01:02 +0100 Subject: [PATCH 40/63] sunshine: add path --- clanModules/sunshine.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clanModules/sunshine.nix b/clanModules/sunshine.nix index e9d2d08e..f764f7eb 100644 --- a/clanModules/sunshine.nix +++ b/clanModules/sunshine.nix @@ -3,7 +3,7 @@ let apps = pkgs.writeText "apps.json" (builtins.toJSON { env = { - PATH = "$(PATH):$(HOME)/.local/bin"; + PATH = "$(PATH):$(HOME)/.local/bin:/run/current-system/sw/bin"; }; apps = [ { From 8ab6fcd4c00487816de7a8d7f3732418f27bf932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 13 Mar 2024 08:38:20 +0100 Subject: [PATCH 41/63] add sshd module --- clanModules/flake-module.nix | 1 + clanModules/sshd.nix | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 clanModules/sshd.nix diff --git a/clanModules/flake-module.nix b/clanModules/flake-module.nix index e6ad9109..0b379c35 100644 --- a/clanModules/flake-module.nix +++ b/clanModules/flake-module.nix @@ -11,6 +11,7 @@ moonlight = ./moonlight.nix; sunshine = ./sunshine.nix; syncthing = ./syncthing.nix; + sshd = ./sshd.nix; vm-user = ./vm-user.nix; graphical = ./graphical.nix; xfce = ./xfce.nix; diff --git a/clanModules/sshd.nix b/clanModules/sshd.nix new file mode 100644 index 00000000..ee4d09b9 --- /dev/null +++ b/clanModules/sshd.nix @@ -0,0 +1,18 @@ +{ config, pkgs, ... }: { + services.openssh.enable = true; + + services.openssh.hostKeys = [{ + path = config.clanCore.secrets.borgbackup.secrets."ssh.id_ed25519".path; + type = "ed25519"; + }]; + + clanCore.secrets.openssh = { + secrets."ssh.id_ed25519" = { }; + facts."ssh.id_ed25519.pub" = { }; + generator.path = [ pkgs.coreutils pkgs.openssh ]; + generator.script = '' + ssh-keygen -t ed25519 -N "" -f $secrets/ssh.id_ed25519 + mv $secrets/ssh.id_ed25519.pub $facts/ssh.id_ed25519.pub + ''; + }; +} From c2e43a4e65e45cee20f5926b40152e0f1c1e232d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 13 Mar 2024 09:18:09 +0100 Subject: [PATCH 42/63] allow fact-only secrets --- nixosModules/clanCore/secrets/default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nixosModules/clanCore/secrets/default.nix b/nixosModules/clanCore/secrets/default.nix index fae55c3a..94a0191b 100644 --- a/nixosModules/clanCore/secrets/default.nix +++ b/nixosModules/clanCore/secrets/default.nix @@ -100,6 +100,7 @@ config' = config; in lib.mkOption { + default = { }; type = lib.types.attrsOf (lib.types.submodule ({ config, name, ... }: { options = { name = lib.mkOption { From a9fc8de2d0231bc82e0a790f7d71cdfd8a50ef1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 13 Mar 2024 10:06:10 +0100 Subject: [PATCH 43/63] allow multi-line interactive secrets --- pkgs/clan-cli/clan_cli/secrets/generate.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/pkgs/clan-cli/clan_cli/secrets/generate.py b/pkgs/clan-cli/clan_cli/secrets/generate.py index d87f8d99..13721559 100644 --- a/pkgs/clan-cli/clan_cli/secrets/generate.py +++ b/pkgs/clan-cli/clan_cli/secrets/generate.py @@ -2,6 +2,7 @@ import argparse import importlib import logging import os +import subprocess from collections.abc import Callable from pathlib import Path from tempfile import TemporaryDirectory @@ -19,6 +20,15 @@ from .modules import SecretStoreBase log = logging.getLogger(__name__) +def read_multiline_input(prompt: str = "Finish with Ctrl-D") -> str: + """ + Read multi-line input from stdin. + """ + print(prompt, flush=True) + proc = subprocess.run(["cat"], stdout=subprocess.PIPE, text=True) + return proc.stdout + + def generate_service_secrets( machine: Machine, service: str, @@ -128,7 +138,12 @@ def generate_secrets( fact_store = facts_module.FactStore(machine=machine) if prompt is None: - prompt = lambda text: input(f"{text}: ") + + def prompt_func(text: str) -> str: + print(f"{text}: ") + return read_multiline_input() + + prompt = prompt_func with TemporaryDirectory() as tmp: tmpdir = Path(tmp) From a9dbd92ff331ebc84150bc859c8db074296402e5 Mon Sep 17 00:00:00 2001 From: DavHau Date: Wed, 13 Mar 2024 18:51:50 +0700 Subject: [PATCH 44/63] merge-after-ci: set labels correctly --- pkgs/merge-after-ci/merge-after-ci.py | 4 ++-- pkgs/tea-create-pr/script.sh | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pkgs/merge-after-ci/merge-after-ci.py b/pkgs/merge-after-ci/merge-after-ci.py index 759cca5a..1de06c7e 100644 --- a/pkgs/merge-after-ci/merge-after-ci.py +++ b/pkgs/merge-after-ci/merge-after-ci.py @@ -17,8 +17,8 @@ subprocess.run( "origin", "main", "--assignees", - "clan-bot", - *([*args.reviewers] if args.reviewers else []), + ",".join(["clan-bot", *args.reviewers]), + *(["--labels", "needs-review"] if not args.no_review else []), *args.args, ] ) diff --git a/pkgs/tea-create-pr/script.sh b/pkgs/tea-create-pr/script.sh index a22ae5dd..8216c027 100644 --- a/pkgs/tea-create-pr/script.sh +++ b/pkgs/tea-create-pr/script.sh @@ -30,5 +30,4 @@ tea pr create \ --description "$rest" \ --head "$tempRemoteBranch" \ --base "$targetBranch" \ - --labels "needs-review" \ "$@" From 59cb2b2a29b8de175d20d9c552d4fb763838a72e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 13 Mar 2024 14:26:39 +0100 Subject: [PATCH 45/63] fix openssh secrets --- clanModules/sshd.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clanModules/sshd.nix b/clanModules/sshd.nix index ee4d09b9..0112a2ff 100644 --- a/clanModules/sshd.nix +++ b/clanModules/sshd.nix @@ -2,7 +2,7 @@ services.openssh.enable = true; services.openssh.hostKeys = [{ - path = config.clanCore.secrets.borgbackup.secrets."ssh.id_ed25519".path; + path = config.clanCore.secrets.openssh.secrets."ssh.id_ed25519".path; type = "ed25519"; }]; From a6d52a669d18358c36029965ca4a5010582b9507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Wed, 13 Mar 2024 14:26:39 +0100 Subject: [PATCH 46/63] fix openssh secrets change facts path to be the full path sshd: fixup store path --- clanModules/sshd.nix | 2 +- nixosModules/clanCore/secrets/default.nix | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/clanModules/sshd.nix b/clanModules/sshd.nix index 0112a2ff..939fd327 100644 --- a/clanModules/sshd.nix +++ b/clanModules/sshd.nix @@ -2,7 +2,7 @@ services.openssh.enable = true; services.openssh.hostKeys = [{ - path = config.clanCore.secrets.openssh.secrets."ssh.id_ed25519".path; + path = builtins.storePath config.clanCore.secrets.openssh.secrets."ssh.id_ed25519".path; type = "ed25519"; }]; diff --git a/nixosModules/clanCore/secrets/default.nix b/nixosModules/clanCore/secrets/default.nix index 94a0191b..395acdc7 100644 --- a/nixosModules/clanCore/secrets/default.nix +++ b/nixosModules/clanCore/secrets/default.nix @@ -147,14 +147,14 @@ description = '' path to a fact which is generated by the generator ''; - default = "machines/${config.clanCore.machineName}/facts/${fact.config._module.args.name}"; + default = "${config.clanCore.clanDir}/machines/${config.clanCore.machineName}/facts/${fact.config._module.args.name}"; }; value = lib.mkOption { defaultText = lib.literalExpression "\${config.clanCore.clanDir}/\${fact.config.path}"; type = lib.types.nullOr lib.types.str; default = - if builtins.pathExists "${config.clanCore.clanDir}/${fact.config.path}" then - lib.strings.removeSuffix "\n" (builtins.readFile "${config.clanCore.clanDir}/${fact.config.path}") + if builtins.pathExists fact.config.path then + lib.strings.fileContents fact.config.path else null; }; From 7537af3943fbfbc607ba6f8b623bf7d5b0663182 Mon Sep 17 00:00:00 2001 From: DavHau Date: Thu, 14 Mar 2024 12:46:17 +0700 Subject: [PATCH 47/63] merge-after-ci: fix bug --- pkgs/merge-after-ci/merge-after-ci.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/merge-after-ci/merge-after-ci.py b/pkgs/merge-after-ci/merge-after-ci.py index 1de06c7e..02cf8687 100644 --- a/pkgs/merge-after-ci/merge-after-ci.py +++ b/pkgs/merge-after-ci/merge-after-ci.py @@ -2,7 +2,7 @@ import argparse import subprocess parser = argparse.ArgumentParser() -parser.add_argument("--reviewers", nargs="*") +parser.add_argument("--reviewers", nargs="*", default=[]) parser.add_argument("--no-review", action="store_true") parser.add_argument("args", nargs="*") args = parser.parse_args() From f4b8133037896f31f1283ecfc870d925ac8cfb14 Mon Sep 17 00:00:00 2001 From: DavHau Date: Thu, 14 Mar 2024 17:19:08 +0700 Subject: [PATCH 48/63] dev-shell: make python shell load fast - Add caching for editable installs - Remove sleep statement in GUI code --- devShell-python.nix | 46 ++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/devShell-python.nix b/devShell-python.nix index d5ac4680..4745b09e 100644 --- a/devShell-python.nix +++ b/devShell-python.nix @@ -49,9 +49,9 @@ ## PYTHON - tmp_path=$(realpath ./.direnv) + tmp_path="$(realpath ./.direnv/python)" repo_root=$(realpath .) - mkdir -p "$tmp_path/python/${pythonWithDeps.sitePackages}" + mkdir -p "$tmp_path/${pythonWithDeps.sitePackages}" # local dependencies localPackages=( @@ -59,28 +59,41 @@ $repo_root/pkgs/clan-vm-manager ) - # Install the package in editable mode - # This allows executing `clan` from within the dev-shell using the current - # version of the code and its dependencies. - # TODO: this is slow. get rid of pip or add better caching - echo "==== Installing local python packages in editable mode ====" + # Install executable wrappers for local python packages scripts + # This is done by utilizing `pip install --editable` + # As a result, executables like `clan` can be executed from within the dev-shell + # while using the current version of the code and its dependencies. for package in "''${localPackages[@]}"; do - ${pythonWithDeps}/bin/pip install \ - --quiet \ - --disable-pip-version-check \ - --no-index \ - --no-build-isolation \ - --prefix "$tmp_path/python" \ - --editable "$package" + pname=$(basename "$package") + if + [ ! -e "$tmp_path/meta/$pname/pyproject.toml" ] \ + || [ ! -e "$package/pyproject.toml" ] \ + || ! cmp -s "$tmp_path/meta/$pname/pyproject.toml" "$package/pyproject.toml" + then + echo "==== Installing local python package $pname in editable mode ====" + mkdir -p "$tmp_path/meta/$pname" + cp $package/pyproject.toml $tmp_path/meta/$pname/pyproject.toml + ${python3.pkgs.pip}/bin/pip install \ + --quiet \ + --disable-pip-version-check \ + --no-index \ + --no-build-isolation \ + --prefix "$tmp_path" \ + --editable "$package" + fi done - export PATH="$tmp_path/python/bin:$PATH" - export PYTHONPATH="''${PYTHONPATH:+$PYTHONPATH:}$tmp_path/python/${pythonWithDeps.sitePackages}" + export PATH="$tmp_path/bin:$PATH" + export PYTHONPATH="''${PYTHONPATH:+$PYTHONPATH:}$tmp_path/${pythonWithDeps.sitePackages}" for package in "''${localPackages[@]}"; do export PYTHONPATH="$package:$PYTHONPATH" done + + + ## GUI + if ! command -v xdg-mime &> /dev/null; then echo "Warning: 'xdg-mime' is not available. The desktop file cannot be installed." fi @@ -93,7 +106,6 @@ UI_BIN="clan-vm-manager" cp -f $DESKTOP_SRC $DESKTOP_DST - sleep 2 sed -i "s|Exec=.*clan-vm-manager|Exec=$UI_BIN|" $DESKTOP_DST xdg-mime default $DESKTOP_FILE_NAME x-scheme-handler/clan echo "==== Validating desktop file installation ====" From b44cbf5c7672bd28bfd7b6e90caa0f4ac9491319 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sun, 17 Mar 2024 14:08:39 +0100 Subject: [PATCH 49/63] clan-vm-manager: connect log view to build state of machines --- .../clan_vm_manager/components/vmobj.py | 33 +++++++- .../clan_vm_manager/singletons/toast.py | 77 +++++++++++++++++-- .../clan_vm_manager/singletons/use_vms.py | 52 +++++++++++-- .../clan_vm_manager/views/list.py | 64 ++++++++++++++- .../clan_vm_manager/views/logs.py | 20 ++--- .../clan_vm_manager/windows/main_window.py | 14 ++-- 6 files changed, 225 insertions(+), 35 deletions(-) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py index 5caf7b02..aa613e1e 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/components/vmobj.py @@ -5,7 +5,7 @@ import tempfile import threading import time import weakref -from collections.abc import Generator +from collections.abc import Callable, Generator from contextlib import contextmanager from datetime import datetime from pathlib import Path @@ -21,7 +21,7 @@ from clan_vm_manager.components.executor import MPProcess, spawn gi.require_version("GObject", "2.0") gi.require_version("Gtk", "4.0") -from gi.repository import GLib, GObject, Gtk +from gi.repository import Gio, GLib, GObject, Gtk log = logging.getLogger(__name__) @@ -29,19 +29,23 @@ log = logging.getLogger(__name__) class VMObject(GObject.Object): # Define a custom signal with the name "vm_stopped" and a string argument for the message __gsignals__: ClassVar = { - "vm_status_changed": (GObject.SignalFlags.RUN_FIRST, None, []) + "vm_status_changed": (GObject.SignalFlags.RUN_FIRST, None, []), + "vm_build_notify": (GObject.SignalFlags.RUN_FIRST, None, [bool, bool]), } def __init__( self, icon: Path, data: HistoryEntry, + build_log_cb: Callable[[Gio.File], None], ) -> None: super().__init__() # Store the data from the history entry self.data: HistoryEntry = data + self.build_log_cb = build_log_cb + # Create a process object to store the VM process self.vm_process: MPProcess = MPProcess( "vm_dummy", mp.Process(), Path("./dummy") @@ -89,6 +93,9 @@ class VMObject(GObject.Object): self.data = data def _on_vm_status_changed(self, source: "VMObject") -> None: + # Signal may be emited multiple times + self.emit("vm_build_notify", self.is_building(), self.is_running()) + self.switch.set_state(self.is_running() and not self.is_building()) if self.switch.get_sensitive() is False and not self.is_building(): self.switch.set_sensitive(True) @@ -154,6 +161,14 @@ class VMObject(GObject.Object): machine=machine, tmpdir=log_dir, ) + + gfile = Gio.File.new_for_path(str(log_dir / "build.log")) + # Gio documentation: + # Obtains a file monitor for the given file. + # If no file notification mechanism exists, then regular polling of the file is used. + g_monitor = gfile.monitor_file(Gio.FileMonitorFlags.NONE, None) + g_monitor.connect("changed", self.on_logs_changed) + GLib.idle_add(self._vm_status_changed_task) self.switch.set_sensitive(True) # Start the logs watcher @@ -206,6 +221,18 @@ class VMObject(GObject.Object): log.debug(f"VM {self.get_id()} has stopped") GLib.idle_add(self._vm_status_changed_task) + def on_logs_changed( + self, + monitor: Gio.FileMonitor, + file: Gio.File, + other_file: Gio.File, + event_type: Gio.FileMonitorEvent, + ) -> None: + if event_type == Gio.FileMonitorEvent.CHANGES_DONE_HINT: + # File was changed and the changes were written to disk + # wire up the callback for setting the logs + self.build_log_cb(file) + def start(self) -> None: if self.is_running(): log.warn("VM is already running. Ignoring start request") diff --git a/pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py b/pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py index 941fc5d9..13d5843c 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/singletons/toast.py @@ -1,4 +1,5 @@ import logging +from collections.abc import Callable from typing import Any import gi @@ -50,20 +51,86 @@ class ToastOverlay: class ErrorToast: toast: Adw.Toast - def __init__(self, message: str) -> None: + def __init__( + self, message: str, persistent: bool = False, details: str = "" + ) -> None: super().__init__() - self.toast = Adw.Toast.new(f"Error: {message}") - self.toast.set_priority(Adw.ToastPriority.HIGH) + self.toast = Adw.Toast.new( + f"""❌ Error {message}""" + ) + self.toast.set_use_markup(True) - self.toast.set_button_label("details") + self.toast.set_priority(Adw.ToastPriority.HIGH) + self.toast.set_button_label("Show more") + + if persistent: + self.toast.set_timeout(0) views = ViewStack.use().view # we cannot check this type, python is not smart enough logs_view: Logs = views.get_child_by_name("logs") # type: ignore - logs_view.set_message(message) + logs_view.set_message(details) self.toast.connect( "button-clicked", lambda _: views.set_visible_child_name("logs"), ) + + +class WarningToast: + toast: Adw.Toast + + def __init__(self, message: str, persistent: bool = False) -> None: + super().__init__() + self.toast = Adw.Toast.new( + f"⚠ Warning {message}" + ) + self.toast.set_use_markup(True) + + self.toast.set_priority(Adw.ToastPriority.NORMAL) + + if persistent: + self.toast.set_timeout(0) + + +class SuccessToast: + toast: Adw.Toast + + def __init__(self, message: str, persistent: bool = False) -> None: + super().__init__() + self.toast = Adw.Toast.new(f" {message}") + self.toast.set_use_markup(True) + + self.toast.set_priority(Adw.ToastPriority.NORMAL) + + if persistent: + self.toast.set_timeout(0) + + +class LogToast: + toast: Adw.Toast + + def __init__( + self, + message: str, + on_button_click: Callable[[], None], + button_label: str = "More", + persistent: bool = False, + ) -> None: + super().__init__() + self.toast = Adw.Toast.new( + f"""Logs are avilable {message}""" + ) + self.toast.set_use_markup(True) + + self.toast.set_priority(Adw.ToastPriority.NORMAL) + + if persistent: + self.toast.set_timeout(0) + + self.toast.set_button_label(button_label) + self.toast.connect( + "button-clicked", + lambda _: on_button_click(), + ) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py index 8a725482..f6464d8f 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py @@ -10,10 +10,12 @@ from clan_cli.history.add import HistoryEntry from clan_vm_manager import assets from clan_vm_manager.components.gkvstore import GKVStore from clan_vm_manager.components.vmobj import VMObject +from clan_vm_manager.singletons.use_views import ViewStack +from clan_vm_manager.views.logs import Logs gi.require_version("GObject", "2.0") gi.require_version("Gtk", "4.0") -from gi.repository import GLib +from gi.repository import Gio, GLib log = logging.getLogger(__name__) @@ -27,6 +29,10 @@ class ClanStore: _instance: "None | ClanStore" = None _clan_store: GKVStore[str, VMStore] + # set the vm that is outputting logs + # build logs are automatically streamed to the logs-view + _logging_vm: VMObject | None = None + # Make sure the VMS class is used as a singleton def __init__(self) -> None: raise RuntimeError("Call use() instead") @@ -41,6 +47,13 @@ class ClanStore: return cls._instance + def set_logging_vm(self, ident: str) -> VMObject | None: + vm = self.get_vm(ClanURI(f"clan://{ident}")) + if vm is not None: + self._logging_vm = vm + + return self._logging_vm + def register_on_deep_change( self, callback: Callable[[GKVStore, int, int, int], None] ) -> None: @@ -77,12 +90,41 @@ class ClanStore: else: icon = Path(entry.flake.icon) - vm = VMObject( - icon=icon, - data=entry, - ) + def log_details(gfile: Gio.File) -> None: + self.log_details(vm, gfile) + + vm = VMObject(icon=icon, data=entry, build_log_cb=log_details) self.push(vm) + def log_details(self, vm: VMObject, gfile: Gio.File) -> None: + views = ViewStack.use().view + logs_view: Logs = views.get_child_by_name("logs") # type: ignore + + def file_read_callback( + source_object: Gio.File, result: Gio.AsyncResult, _user_data: Any + ) -> None: + try: + # Finish the asynchronous read operation + res = source_object.load_contents_finish(result) + _success, contents, _etag_out = res + + # Convert the byte array to a string and print it + logs_view.set_message(contents.decode("utf-8")) + except Exception as e: + print(f"Error reading file: {e}") + + # only one vm can output logs at a time + if vm == self._logging_vm: + gfile.load_contents_async(None, file_read_callback, None) + else: + log.warning( + "Cannot log details of VM that is not the current logging VM.", + vm, + self._logging_vm, + ) + + # we cannot check this type, python is not smart enough + def push(self, vm: VMObject) -> None: url = str(vm.data.flake.flake_url) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py index c9e30da0..a1979dd3 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py @@ -8,9 +8,15 @@ from clan_cli.clan_uri import ClanURI from clan_vm_manager.components.interfaces import ClanConfig from clan_vm_manager.components.vmobj import VMObject -from clan_vm_manager.singletons.toast import ErrorToast, ToastOverlay +from clan_vm_manager.singletons.toast import ( + LogToast, + ToastOverlay, + WarningToast, +) from clan_vm_manager.singletons.use_join import JoinList, JoinValue +from clan_vm_manager.singletons.use_views import ViewStack from clan_vm_manager.singletons.use_vms import ClanStore, VMStore +from clan_vm_manager.views.logs import Logs gi.require_version("Adw", "1") from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk @@ -168,14 +174,42 @@ class ClanList(Gtk.Box): ## Drop down menu open_action = Gio.SimpleAction.new("edit", GLib.VariantType.new("s")) open_action.connect("activate", self.on_edit) + + build_logs_action = Gio.SimpleAction.new("logs", GLib.VariantType.new("s")) + build_logs_action.connect("activate", self.on_show_build_logs) + build_logs_action.set_enabled(False) + app = Gio.Application.get_default() assert app is not None + app.add_action(open_action) + app.add_action(build_logs_action) + + # set a callback function for conditionally enabling the build_logs action + def on_vm_build_notify( + vm: VMObject, is_building: bool, is_running: bool + ) -> None: + build_logs_action.set_enabled(is_building or is_running) + app.add_action(build_logs_action) + if is_building: + ToastOverlay.use().add_toast_unique( + LogToast( + """Build process running ...""", + on_button_click=lambda: self.show_vm_build_logs(vm.get_id()), + ).toast, + f"info.build.running.{vm}", + ) + + vm.connect("vm_build_notify", on_vm_build_notify) + menu_model = Gio.Menu() menu_model.append("Edit", f"app.edit::{vm.get_id()}") + menu_model.append("Show Logs", f"app.logs::{vm.get_id()}") + pref_button = Gtk.MenuButton() pref_button.set_icon_name("open-menu-symbolic") pref_button.set_menu_model(menu_model) + button_box.append(pref_button) ## VM switch button @@ -190,9 +224,31 @@ class ClanList(Gtk.Box): def on_edit(self, source: Any, parameter: Any) -> None: target = parameter.get_string() - print("Editing settings for machine", target) + def on_show_build_logs(self, _: Any, parameter: Any) -> None: + target = parameter.get_string() + self.show_vm_build_logs(target) + + def show_vm_build_logs(self, target: str) -> None: + vm = ClanStore.use().set_logging_vm(target) + if vm is None: + raise ValueError(f"VM {target} not found") + + views = ViewStack.use().view + # Reset the logs view + logs: Logs = views.get_child_by_name("logs") # type: ignore + + if logs is None: + raise ValueError("Logs view not found") + + name = vm.machine.name if vm.machine else "Unknown" + + logs.set_title(f"""📄 {name}""") + logs.set_message("Loading ...") + + views.set_visible_child_name("logs") + def render_join_row( self, boxed_list: Gtk.ListBox, join_val: JoinValue ) -> Gtk.Widget: @@ -214,7 +270,9 @@ class ClanList(Gtk.Box): assert sub is not None ToastOverlay.use().add_toast_unique( - ErrorToast("Already exists. Joining again will update it").toast, + WarningToast( + f"""{join_val.url.machine.name!s} Already exists. Joining again will update it""" + ).toast, "warning.duplicate.join", ) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/logs.py b/pkgs/clan-vm-manager/clan_vm_manager/views/logs.py index 2374ba7f..f7fb804f 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/views/logs.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/logs.py @@ -22,31 +22,27 @@ class Logs(Gtk.Box): app = Gio.Application.get_default() assert app is not None - self.banner = Adw.Banner.new("Error details") + self.banner = Adw.Banner.new("") + self.banner.set_use_markup(True) self.banner.set_revealed(True) + self.banner.set_button_label("Close") - close_button = Gtk.Button() - button_content = Adw.ButtonContent.new() - button_content.set_label("Back") - button_content.set_icon_name("go-previous-symbolic") - close_button.add_css_class("flat") - close_button.set_child(button_content) - close_button.connect( - "clicked", + self.banner.connect( + "button-clicked", lambda _: ViewStack.use().view.set_visible_child_name("list"), ) - self.close_button = close_button - self.text_view = Gtk.TextView() self.text_view.set_editable(False) self.text_view.set_wrap_mode(Gtk.WrapMode.WORD) self.text_view.add_css_class("log-view") - self.append(self.close_button) self.append(self.banner) self.append(self.text_view) + def set_title(self, title: str) -> None: + self.banner.set_title(title) + def set_message(self, message: str) -> None: """ Set the log message. This will delete any previous message diff --git a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py index 77bd4e87..88702732 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py @@ -46,22 +46,22 @@ class MainWindow(Adw.ApplicationWindow): # Initialize all views stack_view = ViewStack.use().view + clamp = Adw.Clamp() + clamp.set_child(stack_view) + clamp.set_maximum_size(1000) + scroll = Gtk.ScrolledWindow() scroll.set_propagate_natural_height(True) scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - scroll.set_child(ClanList(config)) + scroll.set_child(clamp) - stack_view.add_named(scroll, "list") + stack_view.add_named(ClanList(config), "list") stack_view.add_named(Details(), "details") stack_view.add_named(Logs(), "logs") stack_view.set_visible_child_name(config.initial_view) - clamp = Adw.Clamp() - clamp.set_child(stack_view) - clamp.set_maximum_size(1000) - - view.set_content(clamp) + view.set_content(scroll) self.connect("destroy", self.on_destroy) From e4f4680206ba65c55122e89c50c0c16b78308665 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Sun, 17 Mar 2024 14:57:23 +0100 Subject: [PATCH 50/63] clan-vm-manager: init log view with current state of log --- .../clan_vm_manager/singletons/use_vms.py | 6 +----- pkgs/clan-vm-manager/clan_vm_manager/views/list.py | 14 +++++++++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py index f6464d8f..220cd96e 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/singletons/use_vms.py @@ -117,11 +117,7 @@ class ClanStore: if vm == self._logging_vm: gfile.load_contents_async(None, file_read_callback, None) else: - log.warning( - "Cannot log details of VM that is not the current logging VM.", - vm, - self._logging_vm, - ) + log.info("Log details of VM hidden, vm is not current logging VM.") # we cannot check this type, python is not smart enough diff --git a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py index a1979dd3..59377c73 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/views/list.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/views/list.py @@ -1,3 +1,4 @@ +import base64 import logging from collections.abc import Callable from functools import partial @@ -175,7 +176,12 @@ class ClanList(Gtk.Box): open_action = Gio.SimpleAction.new("edit", GLib.VariantType.new("s")) open_action.connect("activate", self.on_edit) - build_logs_action = Gio.SimpleAction.new("logs", GLib.VariantType.new("s")) + action_id = base64.b64encode(vm.get_id().encode("utf-8")).decode("utf-8") + + build_logs_action = Gio.SimpleAction.new( + f"logs.{action_id}", GLib.VariantType.new("s") + ) + build_logs_action.connect("activate", self.on_show_build_logs) build_logs_action.set_enabled(False) @@ -204,7 +210,7 @@ class ClanList(Gtk.Box): menu_model = Gio.Menu() menu_model.append("Edit", f"app.edit::{vm.get_id()}") - menu_model.append("Show Logs", f"app.logs::{vm.get_id()}") + menu_model.append("Show Logs", f"app.logs.{action_id}::{vm.get_id()}") pref_button = Gtk.MenuButton() pref_button.set_icon_name("open-menu-symbolic") @@ -245,7 +251,9 @@ class ClanList(Gtk.Box): name = vm.machine.name if vm.machine else "Unknown" logs.set_title(f"""📄 {name}""") - logs.set_message("Loading ...") + # initial message. Streaming happens automatically when the file is changed by the build process + with open(vm.build_process.out_file) as f: + logs.set_message(f.read()) views.set_visible_child_name("logs") From 377302ff6c1ca52cfc51313c75377222465d7f3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 15 Mar 2024 11:44:18 +0100 Subject: [PATCH 51/63] change facts path to be reachable as a store path --- nixosModules/clanCore/secrets/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixosModules/clanCore/secrets/default.nix b/nixosModules/clanCore/secrets/default.nix index 395acdc7..93fee0cc 100644 --- a/nixosModules/clanCore/secrets/default.nix +++ b/nixosModules/clanCore/secrets/default.nix @@ -147,7 +147,7 @@ description = '' path to a fact which is generated by the generator ''; - default = "${config.clanCore.clanDir}/machines/${config.clanCore.machineName}/facts/${fact.config._module.args.name}"; + default = config.clanCore.clanDir + "/machines/${config.clanCore.machineName}/facts/${fact.config._module.args.name}"; }; value = lib.mkOption { defaultText = lib.literalExpression "\${config.clanCore.clanDir}/\${fact.config.path}"; From a6c3e15aca20a3ab7bf02e97cb6cff59293ffb83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 15 Mar 2024 11:46:27 +0100 Subject: [PATCH 52/63] don't use impure builtins.storePath --- clanModules/sshd.nix | 2 +- nixosModules/clanCore/secrets/default.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/clanModules/sshd.nix b/clanModules/sshd.nix index 939fd327..0112a2ff 100644 --- a/clanModules/sshd.nix +++ b/clanModules/sshd.nix @@ -2,7 +2,7 @@ services.openssh.enable = true; services.openssh.hostKeys = [{ - path = builtins.storePath config.clanCore.secrets.openssh.secrets."ssh.id_ed25519".path; + path = config.clanCore.secrets.openssh.secrets."ssh.id_ed25519".path; type = "ed25519"; }]; diff --git a/nixosModules/clanCore/secrets/default.nix b/nixosModules/clanCore/secrets/default.nix index 93fee0cc..18371359 100644 --- a/nixosModules/clanCore/secrets/default.nix +++ b/nixosModules/clanCore/secrets/default.nix @@ -143,7 +143,7 @@ default = fact.config._module.args.name; }; path = lib.mkOption { - type = lib.types.str; + type = lib.types.path; description = '' path to a fact which is generated by the generator ''; From c15043c4f169da02b9fd9740fe006e3aae9bb874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Fri, 15 Mar 2024 14:06:50 +0000 Subject: [PATCH 53/63] fix evaluation of backup module --- .gitignore | 1 - machines/test_backup_client/facts/borgbackup.ssh.pub | 1 + pkgs/installer/flake-module.nix | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 machines/test_backup_client/facts/borgbackup.ssh.pub diff --git a/.gitignore b/.gitignore index 45b863ff..fed75a61 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ example_clan result* /pkgs/clan-cli/clan_cli/nixpkgs /pkgs/clan-cli/clan_cli/webui/assets -/machines nixos.qcow2 **/*.glade~ diff --git a/machines/test_backup_client/facts/borgbackup.ssh.pub b/machines/test_backup_client/facts/borgbackup.ssh.pub new file mode 100644 index 00000000..c305404c --- /dev/null +++ b/machines/test_backup_client/facts/borgbackup.ssh.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBIbwIVnLy+uoDZ6uK/OCc1QK46SIGeC3mVc85dqLYQw lass@ignavia diff --git a/pkgs/installer/flake-module.nix b/pkgs/installer/flake-module.nix index e9d8c875..8327591e 100644 --- a/pkgs/installer/flake-module.nix +++ b/pkgs/installer/flake-module.nix @@ -29,7 +29,7 @@ in flake.packages.x86_64-linux.install-iso = self.inputs.disko.lib.makeDiskImages { nixosConfig = installer; }; - flake.nixosConfigurations = clan.nixosConfigurations; + flake.nixosConfigurations = { inherit (clan.nixosConfigurations) installer; }; flake.clanInternals = clan.clanInternals; flake.apps.x86_64-linux.install-vm.program = installer.config.formats.vm.outPath; flake.apps.x86_64-linux.install-vm-nogui.program = installer.config.formats.vm-nogui.outPath; From d7939e3cba927af113fd1ae18861093992515cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Mar 2024 18:31:16 +0100 Subject: [PATCH 54/63] add nix to devShell It's important for some tests that package manager used inside NixOS vms is the same as outside --- devShell.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/devShell.nix b/devShell.nix index c3f6eba7..5e5a914d 100644 --- a/devShell.nix +++ b/devShell.nix @@ -26,6 +26,7 @@ packages = [ select-shell pkgs.tea + pkgs.nix self'.packages.tea-create-pr self'.packages.merge-after-ci self'.packages.pending-reviews From 2dcdcd98e94b7f4092e18cca154265c6fbcef082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Mar 2024 19:14:24 +0100 Subject: [PATCH 55/63] installer: also match qemu and serial consoles for prompting qrcode --- nixosModules/installer/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixosModules/installer/default.nix b/nixosModules/installer/default.nix index 5d00cd85..94de49c0 100644 --- a/nixosModules/installer/default.nix +++ b/nixosModules/installer/default.nix @@ -37,7 +37,7 @@ }; services.getty.autologinUser = lib.mkForce "root"; programs.bash.interactiveShellInit = '' - if [ "$(tty)" = "/dev/tty1" ]; then + if [[ "$(tty)" =~ /dev/(tty1|hvc0|ttyS0)$ ]]; then echo -n 'waiting for tor to generate the hidden service' until test -e /var/shared/qrcode.utf8; do echo -n .; sleep 1; done echo From 77c0e6b31abb8f29fe533a90640865630992b681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Mar 2024 19:38:50 +0100 Subject: [PATCH 56/63] make installer nixos module stand-alone --- nixosModules/flake-module.nix | 6 +++++- pkgs/installer/flake-module.nix | 2 -- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/nixosModules/flake-module.nix b/nixosModules/flake-module.nix index 4fb480ea..9d75e27e 100644 --- a/nixosModules/flake-module.nix +++ b/nixosModules/flake-module.nix @@ -1,7 +1,11 @@ { inputs, self, ... }: { flake.nixosModules = { hidden-ssh-announce.imports = [ ./hidden-ssh-announce.nix ]; - installer.imports = [ ./installer ]; + installer.imports = [ + ./installer + self.nixosModules.hidden-ssh-announce + inputs.disko.nixosModules.disko + ]; clanCore.imports = [ inputs.sops-nix.nixosModules.sops ./clanCore diff --git a/pkgs/installer/flake-module.nix b/pkgs/installer/flake-module.nix index 8327591e..74ffaec2 100644 --- a/pkgs/installer/flake-module.nix +++ b/pkgs/installer/flake-module.nix @@ -3,9 +3,7 @@ let installerModule = { config, pkgs, ... }: { imports = [ self.nixosModules.installer - self.nixosModules.hidden-ssh-announce self.inputs.nixos-generators.nixosModules.all-formats - self.inputs.disko.nixosModules.disko ]; system.stateVersion = config.system.nixos.version; From 916e4dff84de5505abf253848426e28cdc958b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Mar 2024 19:44:49 +0100 Subject: [PATCH 57/63] change from nixpkgs-fmt to rfc style formatter --- formatter.nix | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/formatter.nix b/formatter.nix index 2b7b6d0a..4a0a23b4 100644 --- a/formatter.nix +++ b/formatter.nix @@ -7,8 +7,6 @@ ]; perSystem = { self', pkgs, ... }: { treefmt.projectRootFile = "flake.nix"; - treefmt.flakeCheck = true; - treefmt.flakeFormatter = true; treefmt.programs.shellcheck.enable = true; treefmt.programs.mypy.enable = true; @@ -25,7 +23,7 @@ # First deadnix ${lib.getExe pkgs.deadnix} --edit "$@" # Then nixpkgs-fmt - ${lib.getExe pkgs.nixpkgs-fmt} "$@" + ${lib.getExe pkgs.nixfmt-rfc-style} "$@" '' "--" # this argument is ignored by bash ]; From e296a3019d9f2662350f2fd3276c3ea625911cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Mar 2024 19:48:49 +0100 Subject: [PATCH 58/63] re-format with nixfmt --- checks/backups/flake-module.nix | 145 ++++----- checks/borgbackup/default.nix | 89 +++--- checks/container/default.nix | 31 +- checks/deltachat/default.nix | 49 +-- checks/flake-module.nix | 87 +++--- checks/flash/flake-module.nix | 57 ++-- checks/impure/flake-module.nix | 34 ++- checks/installation/flake-module.nix | 159 +++++----- checks/lib/container-driver/module.nix | 142 +++++---- checks/lib/container-driver/package.nix | 14 +- checks/lib/container-test.nix | 52 ++-- checks/lib/test-base.nix | 5 +- checks/schemas.nix | 51 ++-- checks/secrets/default.nix | 22 +- checks/wayland-proxy-virtwl/default.nix | 52 ++-- checks/zt-tcp-relay/default.nix | 41 +-- clanModules/borgbackup.nix | 116 ++++--- clanModules/deltachat.nix | 7 +- clanModules/diskLayouts.nix | 1 - clanModules/flake-module.nix | 3 +- clanModules/graphical.nix | 5 +- clanModules/localsend.nix | 9 +- clanModules/moonlight.nix | 3 +- clanModules/sshd.nix | 18 +- clanModules/sunshine.nix | 29 +- clanModules/syncthing.nix | 67 ++--- clanModules/waypipe.nix | 14 +- clanModules/zt-tcp-relay.nix | 12 +- devShell-python.nix | 13 +- devShell.nix | 16 +- flake.nix | 70 ++--- formatter.nix | 88 +++--- lib/build-clan/default.nix | 164 +++++----- lib/default.nix | 7 +- lib/flake-module.nix | 16 +- lib/jsonschema/default.nix | 283 ++++++++++-------- lib/jsonschema/example-interface.nix | 13 +- lib/jsonschema/flake-module.nix | 46 +-- lib/jsonschema/test.nix | 5 +- lib/jsonschema/test_parseOption.nix | 134 ++++++--- lib/jsonschema/test_parseOptions.nix | 19 +- nixosModules/clanCore/backups.nix | 79 ++--- nixosModules/clanCore/imports.nix | 5 +- nixosModules/clanCore/manual.nix | 5 +- nixosModules/clanCore/metadata.nix | 3 +- nixosModules/clanCore/networking.nix | 35 ++- nixosModules/clanCore/options.nix | 8 +- nixosModules/clanCore/outputs.nix | 12 +- nixosModules/clanCore/packages.nix | 3 +- nixosModules/clanCore/secrets/default.nix | 228 +++++++------- .../clanCore/secrets/password-store.nix | 1 - nixosModules/clanCore/secrets/sops.nix | 50 ++-- nixosModules/clanCore/secrets/vm.nix | 1 - nixosModules/clanCore/state.nix | 60 ++-- nixosModules/clanCore/vm.nix | 128 +++++--- .../clanCore/wayland-proxy-virtwl.nix | 7 +- nixosModules/clanCore/zerotier/default.nix | 84 ++++-- nixosModules/flake-module.nix | 12 +- nixosModules/hidden-ssh-announce.nix | 22 +- nixosModules/installer/default.nix | 28 +- nixosModules/iso/default.nix | 104 ++++--- pkgs/clan-cli/default.nix | 172 ++++++----- pkgs/clan-cli/flake-module.nix | 60 ++-- pkgs/clan-cli/shell.nix | 18 +- pkgs/clan-cli/tests/machines/vm1/default.nix | 3 +- .../machines/vm_with_secrets/default.nix | 3 +- .../machines/vm_without_secrets/default.nix | 3 +- .../clan-cli/tests/test_flake/fake-module.nix | 5 +- pkgs/clan-cli/tests/test_flake/flake.nix | 46 +-- .../test_flake/nixosModules/machine1.nix | 3 +- .../tests/test_flake_with_core/flake.nix | 57 ++-- .../test_flake_with_core_and_pass/flake.nix | 35 ++- .../flake.nix | 7 +- pkgs/clan-vm-manager/default.nix | 37 +-- pkgs/clan-vm-manager/flake-module.nix | 23 +- pkgs/clan-vm-manager/shell.nix | 39 ++- pkgs/flake-module.nix | 65 ++-- pkgs/go-ssb/default.nix | 13 +- pkgs/installer/flake-module.nix | 22 +- pkgs/merge-after-ci/default.nix | 28 +- pkgs/pending-reviews/default.nix | 7 +- pkgs/tea-create-pr/default.nix | 13 +- pkgs/wayland-proxy-virtwl/default.nix | 27 +- pkgs/zerotier-members/default.nix | 6 +- pkgs/zt-tcp-relay/default.nix | 7 +- templates/flake-module.nix | 3 +- templates/new-clan/flake.nix | 7 +- 87 files changed, 2122 insertions(+), 1650 deletions(-) diff --git a/checks/backups/flake-module.nix b/checks/backups/flake-module.nix index b838dc71..8ff597af 100644 --- a/checks/backups/flake-module.nix +++ b/checks/backups/flake-module.nix @@ -14,21 +14,27 @@ let }; in { - flake.nixosConfigurations = { inherit (clan.nixosConfigurations) test_backup_client; }; + flake.nixosConfigurations = { + inherit (clan.nixosConfigurations) test_backup_client; + }; flake.clanInternals = clan.clanInternals; flake.nixosModules = { - test_backup_server = { ... }: { - imports = [ - self.clanModules.borgbackup - ]; - services.sshd.enable = true; - services.borgbackup.repos.testrepo = { - authorizedKeys = [ - (builtins.readFile ../lib/ssh/pubkey) - ]; + test_backup_server = + { ... }: + { + imports = [ self.clanModules.borgbackup ]; + services.sshd.enable = true; + services.borgbackup.repos.testrepo = { + authorizedKeys = [ (builtins.readFile ../lib/ssh/pubkey) ]; + }; }; - }; - test_backup_client = { pkgs, lib, config, ... }: + test_backup_client = + { + pkgs, + lib, + config, + ... + }: let dependencies = [ self @@ -38,14 +44,10 @@ in closureInfo = pkgs.closureInfo { rootPaths = dependencies; }; in { - imports = [ - self.clanModules.borgbackup - ]; + imports = [ self.clanModules.borgbackup ]; networking.hostName = "client"; services.sshd.enable = true; - users.users.root.openssh.authorizedKeys.keyFiles = [ - ../lib/ssh/pubkey - ]; + users.users.root.openssh.authorizedKeys.keyFiles = [ ../lib/ssh/pubkey ]; systemd.tmpfiles.settings."vmsecrets" = { "/etc/secrets/borgbackup.ssh" = { @@ -78,65 +80,64 @@ in clan.borgbackup.destinations.test_backup_server.repo = "borg@server:."; }; }; - perSystem = { nodes, pkgs, ... }: { - checks = pkgs.lib.mkIf (pkgs.stdenv.isLinux) { - test-backups = - (import ../lib/test-base.nix) - { - name = "test-backups"; - nodes.server = { - imports = [ - self.nixosModules.test_backup_server - self.nixosModules.clanCore - { - clanCore.machineName = "server"; - clanCore.clanDir = ../..; - } - ]; - }; - nodes.client = { - imports = [ - self.nixosModules.test_backup_client - self.nixosModules.clanCore - { - clanCore.machineName = "client"; - clanCore.clanDir = ../..; - } - ]; - }; + perSystem = + { nodes, pkgs, ... }: + { + checks = pkgs.lib.mkIf (pkgs.stdenv.isLinux) { + test-backups = (import ../lib/test-base.nix) { + name = "test-backups"; + nodes.server = { + imports = [ + self.nixosModules.test_backup_server + self.nixosModules.clanCore + { + clanCore.machineName = "server"; + clanCore.clanDir = ../..; + } + ]; + }; + nodes.client = { + imports = [ + self.nixosModules.test_backup_client + self.nixosModules.clanCore + { + clanCore.machineName = "client"; + clanCore.clanDir = ../..; + } + ]; + }; - testScript = '' - import json - start_all() + testScript = '' + import json + start_all() - # setup - client.succeed("mkdir -m 700 /root/.ssh") - client.succeed( - "cat ${../lib/ssh/privkey} > /root/.ssh/id_ed25519" - ) - client.succeed("chmod 600 /root/.ssh/id_ed25519") - client.wait_for_unit("sshd", timeout=30) - client.succeed("ssh -o StrictHostKeyChecking=accept-new root@client hostname") + # setup + client.succeed("mkdir -m 700 /root/.ssh") + client.succeed( + "cat ${../lib/ssh/privkey} > /root/.ssh/id_ed25519" + ) + client.succeed("chmod 600 /root/.ssh/id_ed25519") + client.wait_for_unit("sshd", timeout=30) + client.succeed("ssh -o StrictHostKeyChecking=accept-new root@client hostname") - # dummy data - client.succeed("mkdir /var/test-backups") - client.succeed("echo testing > /var/test-backups/somefile") + # dummy data + client.succeed("mkdir /var/test-backups") + client.succeed("echo testing > /var/test-backups/somefile") - # create - client.succeed("clan --debug --flake ${../..} backups create test_backup_client") - client.wait_until_succeeds("! systemctl is-active borgbackup-job-test_backup_server") + # create + client.succeed("clan --debug --flake ${../..} backups create test_backup_client") + client.wait_until_succeeds("! systemctl is-active borgbackup-job-test_backup_server") - # list - backup_id = json.loads(client.succeed("borg-job-test_backup_server list --json"))["archives"][0]["archive"] - assert(backup_id in client.succeed("clan --debug --flake ${../..} backups list test_backup_client")) + # list + backup_id = json.loads(client.succeed("borg-job-test_backup_server list --json"))["archives"][0]["archive"] + assert(backup_id in client.succeed("clan --debug --flake ${../..} backups list test_backup_client")) - # restore - client.succeed("rm -f /var/test-backups/somefile") - client.succeed(f"clan --debug --flake ${../..} backups restore test_backup_client borgbackup {backup_id}") - assert(client.succeed("cat /var/test-backups/somefile").strip() == "testing") - ''; - } - { inherit pkgs self; }; + # restore + client.succeed("rm -f /var/test-backups/somefile") + client.succeed(f"clan --debug --flake ${../..} backups restore test_backup_client borgbackup {backup_id}") + assert(client.succeed("cat /var/test-backups/somefile").strip() == "testing") + ''; + } { inherit pkgs self; }; + }; }; - }; } diff --git a/checks/borgbackup/default.nix b/checks/borgbackup/default.nix index 6dd30e44..ba0008ad 100644 --- a/checks/borgbackup/default.nix +++ b/checks/borgbackup/default.nix @@ -1,48 +1,51 @@ -(import ../lib/test-base.nix) ({ ... }: { - name = "borgbackup"; +(import ../lib/test-base.nix) ( + { ... }: + { + name = "borgbackup"; - nodes.machine = { self, pkgs, ... }: { - imports = [ - self.clanModules.borgbackup - self.nixosModules.clanCore + nodes.machine = + { self, pkgs, ... }: { - services.openssh.enable = true; - services.borgbackup.repos.testrepo = { - authorizedKeys = [ - (builtins.readFile ../lib/ssh/pubkey) - ]; - }; - } - { - clanCore.machineName = "machine"; - clanCore.clanDir = ./.; - clanCore.state.testState.folders = [ "/etc/state" ]; - environment.etc.state.text = "hello world"; - systemd.tmpfiles.settings."vmsecrets" = { - "/etc/secrets/borgbackup.ssh" = { - C.argument = "${../lib/ssh/privkey}"; - z = { - mode = "0400"; - user = "root"; + imports = [ + self.clanModules.borgbackup + self.nixosModules.clanCore + { + services.openssh.enable = true; + services.borgbackup.repos.testrepo = { + authorizedKeys = [ (builtins.readFile ../lib/ssh/pubkey) ]; }; - }; - "/etc/secrets/borgbackup.repokey" = { - C.argument = builtins.toString (pkgs.writeText "repokey" "repokey12345"); - z = { - mode = "0400"; - user = "root"; + } + { + clanCore.machineName = "machine"; + clanCore.clanDir = ./.; + clanCore.state.testState.folders = [ "/etc/state" ]; + environment.etc.state.text = "hello world"; + systemd.tmpfiles.settings."vmsecrets" = { + "/etc/secrets/borgbackup.ssh" = { + C.argument = "${../lib/ssh/privkey}"; + z = { + mode = "0400"; + user = "root"; + }; + }; + "/etc/secrets/borgbackup.repokey" = { + C.argument = builtins.toString (pkgs.writeText "repokey" "repokey12345"); + z = { + mode = "0400"; + user = "root"; + }; + }; }; - }; - }; - clanCore.secretStore = "vm"; + clanCore.secretStore = "vm"; - clan.borgbackup.destinations.test.repo = "borg@localhost:."; - } - ]; - }; - testScript = '' - start_all() - machine.systemctl("start --wait borgbackup-job-test.service") - assert "machine-test" in machine.succeed("BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes /run/current-system/sw/bin/borg-job-test list") - ''; -}) + clan.borgbackup.destinations.test.repo = "borg@localhost:."; + } + ]; + }; + testScript = '' + start_all() + machine.systemctl("start --wait borgbackup-job-test.service") + assert "machine-test" in machine.succeed("BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes /run/current-system/sw/bin/borg-job-test list") + ''; + } +) diff --git a/checks/container/default.nix b/checks/container/default.nix index 37128b22..61c75a0e 100644 --- a/checks/container/default.nix +++ b/checks/container/default.nix @@ -1,14 +1,19 @@ -(import ../lib/container-test.nix) ({ ... }: { - name = "secrets"; +(import ../lib/container-test.nix) ( + { ... }: + { + name = "secrets"; - nodes.machine = { ... }: { - networking.hostName = "machine"; - services.openssh.enable = true; - services.openssh.startWhenNeeded = false; - }; - testScript = '' - start_all() - machine.succeed("systemctl status sshd") - machine.wait_for_unit("sshd") - ''; -}) + nodes.machine = + { ... }: + { + networking.hostName = "machine"; + services.openssh.enable = true; + services.openssh.startWhenNeeded = false; + }; + testScript = '' + start_all() + machine.succeed("systemctl status sshd") + machine.wait_for_unit("sshd") + ''; + } +) diff --git a/checks/deltachat/default.nix b/checks/deltachat/default.nix index 0fcf20b1..aa6e43ff 100644 --- a/checks/deltachat/default.nix +++ b/checks/deltachat/default.nix @@ -1,24 +1,29 @@ -(import ../lib/container-test.nix) ({ pkgs, ... }: { - name = "secrets"; +(import ../lib/container-test.nix) ( + { pkgs, ... }: + { + name = "secrets"; - nodes.machine = { self, ... }: { - imports = [ - self.clanModules.deltachat - self.nixosModules.clanCore + nodes.machine = + { self, ... }: { - clanCore.machineName = "machine"; - clanCore.clanDir = ./.; - } - ]; - }; - testScript = '' - start_all() - machine.wait_for_unit("maddy") - # imap - machine.succeed("${pkgs.netcat}/bin/nc -z -v ::1 143") - # smtp submission - machine.succeed("${pkgs.netcat}/bin/nc -z -v ::1 587") - # smtp - machine.succeed("${pkgs.netcat}/bin/nc -z -v ::1 25") - ''; -}) + imports = [ + self.clanModules.deltachat + self.nixosModules.clanCore + { + clanCore.machineName = "machine"; + clanCore.clanDir = ./.; + } + ]; + }; + testScript = '' + start_all() + machine.wait_for_unit("maddy") + # imap + machine.succeed("${pkgs.netcat}/bin/nc -z -v ::1 143") + # smtp submission + machine.succeed("${pkgs.netcat}/bin/nc -z -v ::1 587") + # smtp + machine.succeed("${pkgs.netcat}/bin/nc -z -v ::1 25") + ''; + } +) diff --git a/checks/flake-module.nix b/checks/flake-module.nix index dcdb0090..b792a1f9 100644 --- a/checks/flake-module.nix +++ b/checks/flake-module.nix @@ -1,41 +1,20 @@ -{ self, ... }: { +{ self, ... }: +{ imports = [ ./impure/flake-module.nix ./backups/flake-module.nix ./installation/flake-module.nix ./flash/flake-module.nix ]; - perSystem = { pkgs, lib, self', ... }: { - checks = - let - nixosTestArgs = { - # reference to nixpkgs for the current system - inherit pkgs; - # this gives us a reference to our flake but also all flake inputs - inherit self; - }; - nixosTests = lib.optionalAttrs (pkgs.stdenv.isLinux) { - # import our test - secrets = import ./secrets nixosTestArgs; - container = import ./container nixosTestArgs; - deltachat = import ./deltachat nixosTestArgs; - zt-tcp-relay = import ./zt-tcp-relay nixosTestArgs; - borgbackup = import ./borgbackup nixosTestArgs; - syncthing = import ./syncthing nixosTestArgs; - wayland-proxy-virtwl = import ./wayland-proxy-virtwl nixosTestArgs; - }; - schemaTests = pkgs.callPackages ./schemas.nix { - inherit self; - }; - - flakeOutputs = lib.mapAttrs' (name: config: lib.nameValuePair "nixos-${name}" config.config.system.build.toplevel) self.nixosConfigurations - // lib.mapAttrs' (n: lib.nameValuePair "package-${n}") self'.packages - // lib.mapAttrs' (n: lib.nameValuePair "devShell-${n}") self'.devShells - // lib.mapAttrs' (name: config: lib.nameValuePair "home-manager-${name}" config.activation-script) (self'.legacyPackages.homeConfigurations or { }); - in - nixosTests // schemaTests // flakeOutputs; - legacyPackages = { - nixosTests = + perSystem = + { + pkgs, + lib, + self', + ... + }: + { + checks = let nixosTestArgs = { # reference to nixpkgs for the current system @@ -43,12 +22,44 @@ # this gives us a reference to our flake but also all flake inputs inherit self; }; + nixosTests = lib.optionalAttrs (pkgs.stdenv.isLinux) { + # import our test + secrets = import ./secrets nixosTestArgs; + container = import ./container nixosTestArgs; + deltachat = import ./deltachat nixosTestArgs; + zt-tcp-relay = import ./zt-tcp-relay nixosTestArgs; + borgbackup = import ./borgbackup nixosTestArgs; + syncthing = import ./syncthing nixosTestArgs; + wayland-proxy-virtwl = import ./wayland-proxy-virtwl nixosTestArgs; + }; + schemaTests = pkgs.callPackages ./schemas.nix { inherit self; }; + + flakeOutputs = + lib.mapAttrs' ( + name: config: lib.nameValuePair "nixos-${name}" config.config.system.build.toplevel + ) self.nixosConfigurations + // lib.mapAttrs' (n: lib.nameValuePair "package-${n}") self'.packages + // lib.mapAttrs' (n: lib.nameValuePair "devShell-${n}") self'.devShells + // lib.mapAttrs' (name: config: lib.nameValuePair "home-manager-${name}" config.activation-script) ( + self'.legacyPackages.homeConfigurations or { } + ); in - lib.optionalAttrs (pkgs.stdenv.isLinux) { - # import our test - secrets = import ./secrets nixosTestArgs; - container = import ./container nixosTestArgs; - }; + nixosTests // schemaTests // flakeOutputs; + legacyPackages = { + nixosTests = + let + nixosTestArgs = { + # reference to nixpkgs for the current system + inherit pkgs; + # this gives us a reference to our flake but also all flake inputs + inherit self; + }; + in + lib.optionalAttrs (pkgs.stdenv.isLinux) { + # import our test + secrets = import ./secrets nixosTestArgs; + container = import ./container nixosTestArgs; + }; + }; }; - }; } diff --git a/checks/flash/flake-module.nix b/checks/flash/flake-module.nix index 3d729fcd..f43c4357 100644 --- a/checks/flash/flake-module.nix +++ b/checks/flash/flake-module.nix @@ -1,6 +1,12 @@ { self, ... }: { - perSystem = { nodes, pkgs, lib, ... }: + perSystem = + { + nodes, + pkgs, + lib, + ... + }: let dependencies = [ self @@ -14,33 +20,30 @@ in { checks = pkgs.lib.mkIf (pkgs.stdenv.isLinux) { - flash = - (import ../lib/test-base.nix) - { - name = "flash"; - nodes.target = { - virtualisation.emptyDiskImages = [ 4096 ]; - virtualisation.memorySize = 3000; - environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ]; - environment.etc."install-closure".source = "${closureInfo}/store-paths"; + flash = (import ../lib/test-base.nix) { + name = "flash"; + nodes.target = { + virtualisation.emptyDiskImages = [ 4096 ]; + virtualisation.memorySize = 3000; + environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ]; + environment.etc."install-closure".source = "${closureInfo}/store-paths"; - nix.settings = { - substituters = lib.mkForce [ ]; - hashed-mirrors = null; - connect-timeout = lib.mkForce 3; - flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}''; - experimental-features = [ - "nix-command" - "flakes" - ]; - }; - }; - testScript = '' - start_all() - machine.succeed("clan --flake ${../..} flash --debug --yes --disk main /dev/vdb test_install_machine") - ''; - } - { inherit pkgs self; }; + nix.settings = { + substituters = lib.mkForce [ ]; + hashed-mirrors = null; + connect-timeout = lib.mkForce 3; + flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}''; + experimental-features = [ + "nix-command" + "flakes" + ]; + }; + }; + testScript = '' + start_all() + machine.succeed("clan --flake ${../..} flash --debug --yes --disk main /dev/vdb test_install_machine") + ''; + } { inherit pkgs self; }; }; }; } diff --git a/checks/impure/flake-module.nix b/checks/impure/flake-module.nix index faeca31e..a918c6c2 100644 --- a/checks/impure/flake-module.nix +++ b/checks/impure/flake-module.nix @@ -1,18 +1,22 @@ { - perSystem = { pkgs, lib, ... }: { - # a script that executes all other checks - packages.impure-checks = pkgs.writeShellScriptBin "impure-checks" '' - #!${pkgs.bash}/bin/bash - set -euo pipefail + perSystem = + { pkgs, lib, ... }: + { + # a script that executes all other checks + packages.impure-checks = pkgs.writeShellScriptBin "impure-checks" '' + #!${pkgs.bash}/bin/bash + set -euo pipefail - export PATH="${lib.makeBinPath [ - pkgs.gitMinimal - pkgs.nix - pkgs.rsync # needed to have rsync installed on the dummy ssh server - ]}" - ROOT=$(git rev-parse --show-toplevel) - cd "$ROOT/pkgs/clan-cli" - nix develop "$ROOT#clan-cli" -c bash -c "TMPDIR=/tmp python -m pytest -s -m impure ./tests $@" - ''; - }; + export PATH="${ + lib.makeBinPath [ + pkgs.gitMinimal + pkgs.nix + pkgs.rsync # needed to have rsync installed on the dummy ssh server + ] + }" + ROOT=$(git rev-parse --show-toplevel) + cd "$ROOT/pkgs/clan-cli" + nix develop "$ROOT#clan-cli" -c bash -c "TMPDIR=/tmp python -m pytest -s -m impure ./tests $@" + ''; + }; } diff --git a/checks/installation/flake-module.nix b/checks/installation/flake-module.nix index 8a402b1b..522da566 100644 --- a/checks/installation/flake-module.nix +++ b/checks/installation/flake-module.nix @@ -12,26 +12,34 @@ let }; in { - flake.nixosConfigurations = { inherit (clan.nixosConfigurations) test_install_machine; }; + flake.nixosConfigurations = { + inherit (clan.nixosConfigurations) test_install_machine; + }; flake.clanInternals = clan.clanInternals; flake.nixosModules = { - test_install_machine = { lib, modulesPath, ... }: { - imports = [ - self.clanModules.diskLayouts - (modulesPath + "/testing/test-instrumentation.nix") # we need these 2 modules always to be able to run the tests - (modulesPath + "/profiles/qemu-guest.nix") - ]; - clan.diskLayouts.singleDiskExt4.device = "/dev/vdb"; + test_install_machine = + { lib, modulesPath, ... }: + { + imports = [ + self.clanModules.diskLayouts + (modulesPath + "/testing/test-instrumentation.nix") # we need these 2 modules always to be able to run the tests + (modulesPath + "/profiles/qemu-guest.nix") + ]; + clan.diskLayouts.singleDiskExt4.device = "/dev/vdb"; - environment.etc."install-successful".text = "ok"; + environment.etc."install-successful".text = "ok"; - boot.consoleLogLevel = lib.mkForce 100; - boot.kernelParams = [ - "boot.shell_on_fail" - ]; - }; + boot.consoleLogLevel = lib.mkForce 100; + boot.kernelParams = [ "boot.shell_on_fail" ]; + }; }; - perSystem = { nodes, pkgs, lib, ... }: + perSystem = + { + nodes, + pkgs, + lib, + ... + }: let dependencies = [ self @@ -45,74 +53,69 @@ in in { checks = pkgs.lib.mkIf (pkgs.stdenv.isLinux) { - test-installation = - (import ../lib/test-base.nix) - { - name = "test-installation"; - nodes.target = { - services.openssh.enable = true; - users.users.root.openssh.authorizedKeys.keyFiles = [ - ../lib/ssh/pubkey - ]; - system.nixos.variant_id = "installer"; - virtualisation.emptyDiskImages = [ 4096 ]; - nix.settings = { - substituters = lib.mkForce [ ]; - hashed-mirrors = null; - connect-timeout = lib.mkForce 3; - flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}''; - experimental-features = [ - "nix-command" - "flakes" - ]; - }; - }; - nodes.client = { - environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ]; - environment.etc."install-closure".source = "${closureInfo}/store-paths"; - virtualisation.memorySize = 2048; - nix.settings = { - substituters = lib.mkForce [ ]; - hashed-mirrors = null; - connect-timeout = lib.mkForce 3; - flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}''; - experimental-features = [ - "nix-command" - "flakes" - ]; - }; - system.extraDependencies = dependencies; - }; + test-installation = (import ../lib/test-base.nix) { + name = "test-installation"; + nodes.target = { + services.openssh.enable = true; + users.users.root.openssh.authorizedKeys.keyFiles = [ ../lib/ssh/pubkey ]; + system.nixos.variant_id = "installer"; + virtualisation.emptyDiskImages = [ 4096 ]; + nix.settings = { + substituters = lib.mkForce [ ]; + hashed-mirrors = null; + connect-timeout = lib.mkForce 3; + flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}''; + experimental-features = [ + "nix-command" + "flakes" + ]; + }; + }; + nodes.client = { + environment.systemPackages = [ self.packages.${pkgs.system}.clan-cli ]; + environment.etc."install-closure".source = "${closureInfo}/store-paths"; + virtualisation.memorySize = 2048; + nix.settings = { + substituters = lib.mkForce [ ]; + hashed-mirrors = null; + connect-timeout = lib.mkForce 3; + flake-registry = pkgs.writeText "flake-registry" ''{"flakes":[],"version":2}''; + experimental-features = [ + "nix-command" + "flakes" + ]; + }; + system.extraDependencies = dependencies; + }; - testScript = '' - def create_test_machine(oldmachine=None, args={}): # taken from - startCommand = "${pkgs.qemu_test}/bin/qemu-kvm" - startCommand += " -cpu max -m 1024 -virtfs local,path=/nix/store,security_model=none,mount_tag=nix-store" - startCommand += f' -drive file={oldmachine.state_dir}/empty0.qcow2,id=drive1,if=none,index=1,werror=report' - startCommand += ' -device virtio-blk-pci,drive=drive1' - machine = create_machine({ - "startCommand": startCommand, - } | args) - driver.machines.append(machine) - return machine + testScript = '' + def create_test_machine(oldmachine=None, args={}): # taken from + startCommand = "${pkgs.qemu_test}/bin/qemu-kvm" + startCommand += " -cpu max -m 1024 -virtfs local,path=/nix/store,security_model=none,mount_tag=nix-store" + startCommand += f' -drive file={oldmachine.state_dir}/empty0.qcow2,id=drive1,if=none,index=1,werror=report' + startCommand += ' -device virtio-blk-pci,drive=drive1' + machine = create_machine({ + "startCommand": startCommand, + } | args) + driver.machines.append(machine) + return machine - start_all() + start_all() - client.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../lib/ssh/privkey} /root/.ssh/id_ed25519") - client.wait_until_succeeds("ssh -o StrictHostKeyChecking=accept-new -v root@target hostname") + client.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../lib/ssh/privkey} /root/.ssh/id_ed25519") + client.wait_until_succeeds("ssh -o StrictHostKeyChecking=accept-new -v root@target hostname") - client.succeed("clan --debug --flake ${../..} machines install --yes test_install_machine root@target >&2") - try: - target.shutdown() - except BrokenPipeError: - # qemu has already exited - pass + client.succeed("clan --debug --flake ${../..} machines install --yes test_install_machine root@target >&2") + try: + target.shutdown() + except BrokenPipeError: + # qemu has already exited + pass - new_machine = create_test_machine(oldmachine=target, args={ "name": "new_machine" }) - assert(new_machine.succeed("cat /etc/install-successful").strip() == "ok") - ''; - } - { inherit pkgs self; }; + new_machine = create_test_machine(oldmachine=target, args={ "name": "new_machine" }) + assert(new_machine.succeed("cat /etc/install-successful").strip() == "ok") + ''; + } { inherit pkgs self; }; }; }; } diff --git a/checks/lib/container-driver/module.nix b/checks/lib/container-driver/module.nix index 10adac32..ca4e72e7 100644 --- a/checks/lib/container-driver/module.nix +++ b/checks/lib/container-driver/module.nix @@ -1,17 +1,23 @@ -{ hostPkgs, lib, config, ... }: +{ + hostPkgs, + lib, + config, + ... +}: let testDriver = hostPkgs.python3.pkgs.callPackage ./package.nix { inherit (config) extraPythonPackages; inherit (hostPkgs.pkgs) util-linux systemd; }; containers = map (m: m.system.build.toplevel) (lib.attrValues config.nodes); - pythonizeName = name: + pythonizeName = + name: let head = lib.substring 0 1 name; tail = lib.substring 1 (-1) name; in - (if builtins.match "[A-z_]" head == null then "_" else head) + - lib.stringAsChars (c: if builtins.match "[A-z0-9_]" c == null then "_" else c) tail; + (if builtins.match "[A-z_]" head == null then "_" else head) + + lib.stringAsChars (c: if builtins.match "[A-z0-9_]" c == null then "_" else c) tail; nodeHostNames = let nodesList = map (c: c.system.name) (lib.attrValues config.nodes); @@ -21,68 +27,72 @@ let pythonizedNames = map pythonizeName nodeHostNames; in { - driver = lib.mkForce (hostPkgs.runCommand "nixos-test-driver-${config.name}" - { - nativeBuildInputs = [ - hostPkgs.makeWrapper - ] ++ lib.optionals (!config.skipTypeCheck) [ hostPkgs.mypy ]; - buildInputs = [ testDriver ]; - testScript = config.testScriptString; - preferLocalBuild = true; - passthru = config.passthru; - meta = config.meta // { - mainProgram = "nixos-test-driver"; + driver = lib.mkForce ( + hostPkgs.runCommand "nixos-test-driver-${config.name}" + { + nativeBuildInputs = [ + hostPkgs.makeWrapper + ] ++ lib.optionals (!config.skipTypeCheck) [ hostPkgs.mypy ]; + buildInputs = [ testDriver ]; + testScript = config.testScriptString; + preferLocalBuild = true; + passthru = config.passthru; + meta = config.meta // { + mainProgram = "nixos-test-driver"; + }; + } + '' + mkdir -p $out/bin + + containers=(${toString containers}) + + ${lib.optionalString (!config.skipTypeCheck) '' + # prepend type hints so the test script can be type checked with mypy + cat "${./test-script-prepend.py}" >> testScriptWithTypes + echo "${builtins.toString machineNames}" >> testScriptWithTypes + echo -n "$testScript" >> testScriptWithTypes + + echo "Running type check (enable/disable: config.skipTypeCheck)" + echo "See https://nixos.org/manual/nixos/stable/#test-opt-skipTypeCheck" + + mypy --no-implicit-optional \ + --pretty \ + --no-color-output \ + testScriptWithTypes + ''} + + echo -n "$testScript" >> $out/test-script + + ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-test-driver + + wrapProgram $out/bin/nixos-test-driver \ + ${lib.concatStringsSep " " (map (name: "--add-flags '--container ${name}'") containers)} \ + --add-flags "--test-script '$out/test-script'" + '' + ); + + test = lib.mkForce ( + lib.lazyDerivation { + # lazyDerivation improves performance when only passthru items and/or meta are used. + derivation = hostPkgs.stdenv.mkDerivation { + name = "vm-test-run-${config.name}"; + + requiredSystemFeatures = [ "uid-range" ]; + + buildCommand = '' + mkdir -p $out + + # effectively mute the XMLLogger + export LOGFILE=/dev/null + + ${config.driver}/bin/nixos-test-driver -o $out + ''; + + passthru = config.passthru; + + meta = config.meta; }; + inherit (config) passthru meta; } - '' - mkdir -p $out/bin - - containers=(${toString containers}) - - ${lib.optionalString (!config.skipTypeCheck) '' - # prepend type hints so the test script can be type checked with mypy - cat "${./test-script-prepend.py}" >> testScriptWithTypes - echo "${builtins.toString machineNames}" >> testScriptWithTypes - echo -n "$testScript" >> testScriptWithTypes - - echo "Running type check (enable/disable: config.skipTypeCheck)" - echo "See https://nixos.org/manual/nixos/stable/#test-opt-skipTypeCheck" - - mypy --no-implicit-optional \ - --pretty \ - --no-color-output \ - testScriptWithTypes - ''} - - echo -n "$testScript" >> $out/test-script - - ln -s ${testDriver}/bin/nixos-test-driver $out/bin/nixos-test-driver - - wrapProgram $out/bin/nixos-test-driver \ - ${lib.concatStringsSep " " (map (name: "--add-flags '--container ${name}'") containers)} \ - --add-flags "--test-script '$out/test-script'" - ''); - - test = lib.mkForce (lib.lazyDerivation { - # lazyDerivation improves performance when only passthru items and/or meta are used. - derivation = hostPkgs.stdenv.mkDerivation { - name = "vm-test-run-${config.name}"; - - requiredSystemFeatures = [ "uid-range" ]; - - buildCommand = '' - mkdir -p $out - - # effectively mute the XMLLogger - export LOGFILE=/dev/null - - ${config.driver}/bin/nixos-test-driver -o $out - ''; - - passthru = config.passthru; - - meta = config.meta; - }; - inherit (config) passthru meta; - }); + ); } diff --git a/checks/lib/container-driver/package.nix b/checks/lib/container-driver/package.nix index 872ec154..cbb47e2e 100644 --- a/checks/lib/container-driver/package.nix +++ b/checks/lib/container-driver/package.nix @@ -1,8 +1,18 @@ -{ extraPythonPackages, python3Packages, buildPythonApplication, setuptools, util-linux, systemd }: +{ + extraPythonPackages, + python3Packages, + buildPythonApplication, + setuptools, + util-linux, + systemd, +}: buildPythonApplication { pname = "test-driver"; version = "0.0.1"; - propagatedBuildInputs = [ util-linux systemd ] ++ extraPythonPackages python3Packages; + propagatedBuildInputs = [ + util-linux + systemd + ] ++ extraPythonPackages python3Packages; nativeBuildInputs = [ setuptools ]; format = "pyproject"; src = ./.; diff --git a/checks/lib/container-test.nix b/checks/lib/container-test.nix index 3753167a..2c0b19b2 100644 --- a/checks/lib/container-test.nix +++ b/checks/lib/container-test.nix @@ -1,33 +1,33 @@ test: -{ pkgs -, self -, ... -}: +{ pkgs, self, ... }: let inherit (pkgs) lib; nixos-lib = import (pkgs.path + "/nixos/lib") { }; in -(nixos-lib.runTest ({ hostPkgs, ... }: { - hostPkgs = pkgs; - # speed-up evaluation - defaults = { - documentation.enable = lib.mkDefault false; - boot.isContainer = true; +(nixos-lib.runTest ( + { hostPkgs, ... }: + { + hostPkgs = pkgs; + # speed-up evaluation + defaults = { + documentation.enable = lib.mkDefault false; + boot.isContainer = true; - # undo qemu stuff - system.build.initialRamdisk = ""; - virtualisation.sharedDirectories = lib.mkForce { }; - networking.useDHCP = false; + # undo qemu stuff + system.build.initialRamdisk = ""; + virtualisation.sharedDirectories = lib.mkForce { }; + networking.useDHCP = false; - # we have not private networking so far - networking.interfaces = lib.mkForce { }; - #networking.primaryIPAddress = lib.mkForce null; - systemd.services.backdoor.enable = false; - }; - # to accept external dependencies such as disko - node.specialArgs.self = self; - imports = [ - test - ./container-driver/module.nix - ]; -})).config.result + # we have not private networking so far + networking.interfaces = lib.mkForce { }; + #networking.primaryIPAddress = lib.mkForce null; + systemd.services.backdoor.enable = false; + }; + # to accept external dependencies such as disko + node.specialArgs.self = self; + imports = [ + test + ./container-driver/module.nix + ]; + } +)).config.result diff --git a/checks/lib/test-base.nix b/checks/lib/test-base.nix index 998ffba9..1c88b9ea 100644 --- a/checks/lib/test-base.nix +++ b/checks/lib/test-base.nix @@ -1,8 +1,5 @@ test: -{ pkgs -, self -, ... -}: +{ pkgs, self, ... }: let inherit (pkgs) lib; nixos-lib = import (pkgs.path + "/nixos/lib") { }; diff --git a/checks/schemas.nix b/checks/schemas.nix index c12e1d8f..5307cc35 100644 --- a/checks/schemas.nix +++ b/checks/schemas.nix @@ -1,35 +1,48 @@ -{ self, runCommand, check-jsonschema, pkgs, lib, ... }: +{ + self, + runCommand, + check-jsonschema, + pkgs, + lib, + ... +}: let clanModules.clanCore = self.nixosModules.clanCore; baseModule = { - imports = - (import (pkgs.path + "/nixos/modules/module-list.nix")) - ++ [{ + imports = (import (pkgs.path + "/nixos/modules/module-list.nix")) ++ [ + { nixpkgs.hostPlatform = "x86_64-linux"; clanCore.clanName = "dummy"; - }]; + } + ]; }; - optionsFromModule = module: + optionsFromModule = + module: let evaled = lib.evalModules { - modules = [ module baseModule ]; + modules = [ + module + baseModule + ]; }; in evaled.options.clan; - clanModuleSchemas = lib.mapAttrs (_: module: self.lib.jsonschema.parseOptions (optionsFromModule module)) clanModules; + clanModuleSchemas = lib.mapAttrs ( + _: module: self.lib.jsonschema.parseOptions (optionsFromModule module) + ) clanModules; - mkTest = name: schema: runCommand "schema-${name}" { } '' - ${check-jsonschema}/bin/check-jsonschema \ - --check-metaschema ${builtins.toFile "schema-${name}" (builtins.toJSON schema)} - touch $out - ''; + mkTest = + name: schema: + runCommand "schema-${name}" { } '' + ${check-jsonschema}/bin/check-jsonschema \ + --check-metaschema ${builtins.toFile "schema-${name}" (builtins.toJSON schema)} + touch $out + ''; in -lib.mapAttrs' - (name: schema: { - name = "schema-${name}"; - value = mkTest name schema; - }) - clanModuleSchemas +lib.mapAttrs' (name: schema: { + name = "schema-${name}"; + value = mkTest name schema; +}) clanModuleSchemas diff --git a/checks/secrets/default.nix b/checks/secrets/default.nix index 8f050bf7..97b9a7e4 100644 --- a/checks/secrets/default.nix +++ b/checks/secrets/default.nix @@ -1,19 +1,19 @@ (import ../lib/test-base.nix) { name = "secrets"; - nodes.machine = { self, config, ... }: { - imports = [ - (self.nixosModules.clanCore) - ]; - environment.etc."secret".source = config.sops.secrets.secret.path; - environment.etc."group-secret".source = config.sops.secrets.group-secret.path; - sops.age.keyFile = ./key.age; + nodes.machine = + { self, config, ... }: + { + imports = [ (self.nixosModules.clanCore) ]; + environment.etc."secret".source = config.sops.secrets.secret.path; + environment.etc."group-secret".source = config.sops.secrets.group-secret.path; + sops.age.keyFile = ./key.age; - clanCore.clanDir = "${./.}"; - clanCore.machineName = "machine"; + clanCore.clanDir = "${./.}"; + clanCore.machineName = "machine"; - networking.hostName = "machine"; - }; + networking.hostName = "machine"; + }; testScript = '' machine.succeed("cat /etc/secret >&2") machine.succeed("cat /etc/group-secret >&2") diff --git a/checks/wayland-proxy-virtwl/default.nix b/checks/wayland-proxy-virtwl/default.nix index d678bcb8..4bfa2df7 100644 --- a/checks/wayland-proxy-virtwl/default.nix +++ b/checks/wayland-proxy-virtwl/default.nix @@ -1,25 +1,35 @@ -import ../lib/test-base.nix ({ config, pkgs, lib, ... }: { - name = "wayland-proxy-virtwl"; +import ../lib/test-base.nix ( + { + config, + pkgs, + lib, + ... + }: + { + name = "wayland-proxy-virtwl"; - nodes.machine = { self, ... }: { - imports = [ - self.nixosModules.clanCore + nodes.machine = + { self, ... }: { - clanCore.machineName = "machine"; - clanCore.clanDir = ./.; - } - ]; - services.wayland-proxy-virtwl.enable = true; + imports = [ + self.nixosModules.clanCore + { + clanCore.machineName = "machine"; + clanCore.clanDir = ./.; + } + ]; + services.wayland-proxy-virtwl.enable = true; - virtualisation.qemu.options = [ - "-vga none -device virtio-gpu-rutabaga,cross-domain=on,hostmem=4G,wsi=headless" - ]; + virtualisation.qemu.options = [ + "-vga none -device virtio-gpu-rutabaga,cross-domain=on,hostmem=4G,wsi=headless" + ]; - virtualisation.qemu.package = lib.mkForce pkgs.qemu_kvm; - }; - testScript = '' - start_all() - # use machinectl - machine.succeed("machinectl shell .host ${config.nodes.machine.systemd.package}/bin/systemctl --user start wayland-proxy-virtwl >&2") - ''; -}) + virtualisation.qemu.package = lib.mkForce pkgs.qemu_kvm; + }; + testScript = '' + start_all() + # use machinectl + machine.succeed("machinectl shell .host ${config.nodes.machine.systemd.package}/bin/systemctl --user start wayland-proxy-virtwl >&2") + ''; + } +) diff --git a/checks/zt-tcp-relay/default.nix b/checks/zt-tcp-relay/default.nix index a4fcea4a..b30e4c5f 100644 --- a/checks/zt-tcp-relay/default.nix +++ b/checks/zt-tcp-relay/default.nix @@ -1,20 +1,25 @@ -(import ../lib/container-test.nix) ({ pkgs, ... }: { - name = "zt-tcp-relay"; +(import ../lib/container-test.nix) ( + { pkgs, ... }: + { + name = "zt-tcp-relay"; - nodes.machine = { self, ... }: { - imports = [ - self.nixosModules.clanCore - self.clanModules.zt-tcp-relay + nodes.machine = + { self, ... }: { - clanCore.machineName = "machine"; - clanCore.clanDir = ./.; - } - ]; - }; - testScript = '' - start_all() - machine.wait_for_unit("zt-tcp-relay.service") - out = machine.succeed("${pkgs.netcat}/bin/nc -z -v localhost 4443") - print(out) - ''; -}) + imports = [ + self.nixosModules.clanCore + self.clanModules.zt-tcp-relay + { + clanCore.machineName = "machine"; + clanCore.clanDir = ./.; + } + ]; + }; + testScript = '' + start_all() + machine.wait_for_unit("zt-tcp-relay.service") + out = machine.succeed("${pkgs.netcat}/bin/nc -z -v localhost 4443") + print(out) + ''; + } +) diff --git a/clanModules/borgbackup.nix b/clanModules/borgbackup.nix index d7ded090..b6d4d99d 100644 --- a/clanModules/borgbackup.nix +++ b/clanModules/borgbackup.nix @@ -1,69 +1,88 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.clan.borgbackup; in { options.clan.borgbackup.destinations = lib.mkOption { - type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: { - options = { - name = lib.mkOption { - type = lib.types.str; - default = name; - description = "the name of the backup job"; - }; - repo = lib.mkOption { - type = lib.types.str; - description = "the borgbackup repository to backup to"; - }; - rsh = lib.mkOption { - type = lib.types.str; - default = "ssh -i ${config.clanCore.secrets.borgbackup.secrets."borgbackup.ssh".path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"; - description = "the rsh to use for the backup"; - }; - - }; - })); + type = lib.types.attrsOf ( + lib.types.submodule ( + { name, ... }: + { + options = { + name = lib.mkOption { + type = lib.types.str; + default = name; + description = "the name of the backup job"; + }; + repo = lib.mkOption { + type = lib.types.str; + description = "the borgbackup repository to backup to"; + }; + rsh = lib.mkOption { + type = lib.types.str; + default = "ssh -i ${ + config.clanCore.secrets.borgbackup.secrets."borgbackup.ssh".path + } -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"; + description = "the rsh to use for the backup"; + }; + }; + } + ) + ); default = { }; description = '' destinations where the machine should be backuped to ''; }; - imports = [ (lib.mkRemovedOptionModule [ "clan" "borgbackup" "enable" ] "Just define clan.borgbackup.destinations to enable it") ]; + imports = [ + (lib.mkRemovedOptionModule [ + "clan" + "borgbackup" + "enable" + ] "Just define clan.borgbackup.destinations to enable it") + ]; config = lib.mkIf (cfg.destinations != { }) { - services.borgbackup.jobs = lib.mapAttrs - (_: dest: { - paths = lib.flatten (map (state: state.folders) (lib.attrValues config.clanCore.state)); - exclude = [ "*.pyc" ]; - repo = dest.repo; - environment.BORG_RSH = dest.rsh; - compression = "auto,zstd"; - startAt = "*-*-* 01:00:00"; - persistentTimer = true; - preHook = '' - set -x - ''; + services.borgbackup.jobs = lib.mapAttrs (_: dest: { + paths = lib.flatten (map (state: state.folders) (lib.attrValues config.clanCore.state)); + exclude = [ "*.pyc" ]; + repo = dest.repo; + environment.BORG_RSH = dest.rsh; + compression = "auto,zstd"; + startAt = "*-*-* 01:00:00"; + persistentTimer = true; + preHook = '' + set -x + ''; - encryption = { - mode = "repokey"; - passCommand = "cat ${config.clanCore.secrets.borgbackup.secrets."borgbackup.repokey".path}"; - }; + encryption = { + mode = "repokey"; + passCommand = "cat ${config.clanCore.secrets.borgbackup.secrets."borgbackup.repokey".path}"; + }; - prune.keep = { - within = "1d"; # Keep all archives from the last day - daily = 7; - weekly = 4; - monthly = 0; - }; - }) - cfg.destinations; + prune.keep = { + within = "1d"; # Keep all archives from the last day + daily = 7; + weekly = 4; + monthly = 0; + }; + }) cfg.destinations; clanCore.secrets.borgbackup = { facts."borgbackup.ssh.pub" = { }; secrets."borgbackup.ssh" = { }; secrets."borgbackup.repokey" = { }; - generator.path = [ pkgs.openssh pkgs.coreutils pkgs.xkcdpass ]; + generator.path = [ + pkgs.openssh + pkgs.coreutils + pkgs.xkcdpass + ]; generator.script = '' ssh-keygen -t ed25519 -N "" -f "$secrets"/borgbackup.ssh mv "$secrets"/borgbackup.ssh.pub "$facts"/borgbackup.ssh.pub @@ -75,8 +94,9 @@ in # TODO list needs to run locally or on the remote machine list = '' # we need yes here to skip the changed url verification - ${lib.concatMapStringsSep "\n" (dest: ''yes y | borg-job-${dest.name} list --json | jq -r '. + {"job-name": "${dest.name}"}' '') - (lib.attrValues cfg.destinations)} + ${lib.concatMapStringsSep "\n" ( + dest: ''yes y | borg-job-${dest.name} list --json | jq -r '. + {"job-name": "${dest.name}"}' '' + ) (lib.attrValues cfg.destinations)} ''; create = '' ${lib.concatMapStringsSep "\n" (dest: '' diff --git a/clanModules/deltachat.nix b/clanModules/deltachat.nix index 771068a1..489ffaa3 100644 --- a/clanModules/deltachat.nix +++ b/clanModules/deltachat.nix @@ -1,4 +1,5 @@ -{ config, pkgs, ... }: { +{ config, pkgs, ... }: +{ networking.firewall.interfaces."zt+".allowedTCPPorts = [ 25 ]; # smtp with other hosts environment.systemPackages = [ pkgs.deltachat-desktop ]; @@ -134,9 +135,7 @@ storage &local_mailboxes } ''; - ensureAccounts = [ - "user@${domain}" - ]; + ensureAccounts = [ "user@${domain}" ]; ensureCredentials = { "user@${domain}".passwordFile = pkgs.writeText "dummy" "foobar"; }; diff --git a/clanModules/diskLayouts.nix b/clanModules/diskLayouts.nix index 0bbc45e0..097ff2fa 100644 --- a/clanModules/diskLayouts.nix +++ b/clanModules/diskLayouts.nix @@ -41,4 +41,3 @@ }; }; } - diff --git a/clanModules/flake-module.nix b/clanModules/flake-module.nix index 0b379c35..d2895e70 100644 --- a/clanModules/flake-module.nix +++ b/clanModules/flake-module.nix @@ -1,4 +1,5 @@ -{ inputs, ... }: { +{ inputs, ... }: +{ flake.clanModules = { diskLayouts = { imports = [ diff --git a/clanModules/graphical.nix b/clanModules/graphical.nix index 5f8a0fd5..e272d8b8 100644 --- a/clanModules/graphical.nix +++ b/clanModules/graphical.nix @@ -1,4 +1 @@ -_: -{ - fonts.enableDefaultPackages = true; -} +_: { fonts.enableDefaultPackages = true; } diff --git a/clanModules/localsend.nix b/clanModules/localsend.nix index b5b6210e..3172b8dd 100644 --- a/clanModules/localsend.nix +++ b/clanModules/localsend.nix @@ -1,7 +1,8 @@ -{ config -, pkgs -, lib -, ... +{ + config, + pkgs, + lib, + ... }: { # Integration can be improved, if the following issues get implemented: diff --git a/clanModules/moonlight.nix b/clanModules/moonlight.nix index 67267fb6..feb589df 100644 --- a/clanModules/moonlight.nix +++ b/clanModules/moonlight.nix @@ -1,4 +1,5 @@ -{ pkgs, ... }: { +{ pkgs, ... }: +{ hardware.opengl.enable = true; environment.systemPackages = [ pkgs.moonlight-qt ]; } diff --git a/clanModules/sshd.nix b/clanModules/sshd.nix index 0112a2ff..df38078d 100644 --- a/clanModules/sshd.nix +++ b/clanModules/sshd.nix @@ -1,15 +1,21 @@ -{ config, pkgs, ... }: { +{ config, pkgs, ... }: +{ services.openssh.enable = true; - services.openssh.hostKeys = [{ - path = config.clanCore.secrets.openssh.secrets."ssh.id_ed25519".path; - type = "ed25519"; - }]; + services.openssh.hostKeys = [ + { + path = config.clanCore.secrets.openssh.secrets."ssh.id_ed25519".path; + type = "ed25519"; + } + ]; clanCore.secrets.openssh = { secrets."ssh.id_ed25519" = { }; facts."ssh.id_ed25519.pub" = { }; - generator.path = [ pkgs.coreutils pkgs.openssh ]; + generator.path = [ + pkgs.coreutils + pkgs.openssh + ]; generator.script = '' ssh-keygen -t ed25519 -N "" -f $secrets/ssh.id_ed25519 mv $secrets/ssh.id_ed25519.pub $facts/ssh.id_ed25519.pub diff --git a/clanModules/sunshine.nix b/clanModules/sunshine.nix index f764f7eb..c2f01365 100644 --- a/clanModules/sunshine.nix +++ b/clanModules/sunshine.nix @@ -1,7 +1,7 @@ { pkgs, options, ... }: let - apps = pkgs.writeText "apps.json" (builtins.toJSON - { + apps = pkgs.writeText "apps.json" ( + builtins.toJSON { env = { PATH = "$(PATH):$(HOME)/.local/bin:/run/current-system/sw/bin"; }; @@ -22,13 +22,12 @@ let } { name = "Steam Big Picture"; - detached = [ - "setsid steam steam://open/bigpicture" - ]; + detached = [ "setsid steam steam://open/bigpicture" ]; image-path = "steam.png"; } ]; - }); + } + ); sunshineConfiguration = pkgs.writeText "sunshine.conf" '' address_family = both channels = 5 @@ -78,11 +77,9 @@ in environment.systemPackages = [ pkgs.sunshine (pkgs.writers.writeDashBin "sun" '' - ${pkgs.sunshine}/bin/sunshine -1 ${ - pkgs.writeText "sunshine.conf" '' - address_family = both - '' - } "$@" + ${pkgs.sunshine}/bin/sunshine -1 ${pkgs.writeText "sunshine.conf" '' + address_family = both + ''} "$@" '') # Create a dummy account, for easier setup, # don't use this account in actual production yet. @@ -113,11 +110,7 @@ in }; }; - - systemd.tmpfiles.rules = [ - "d '/var/lib/sunshine' 0770 'user' 'users' - -" - ]; - + systemd.tmpfiles.rules = [ "d '/var/lib/sunshine' 0770 'user' 'users' - -" ]; systemd.user.services.sunshine = { enable = true; @@ -128,9 +121,7 @@ in serviceConfig = { Restart = "on-failure"; RestartSec = "5s"; - ReadWritePaths = [ - "/var/lib/sunshine" - ]; + ReadWritePaths = [ "/var/lib/sunshine" ]; }; wantedBy = [ "graphical-session.target" ]; }; diff --git a/clanModules/syncthing.nix b/clanModules/syncthing.nix index fc7c4bc5..bf73dcf0 100644 --- a/clanModules/syncthing.nix +++ b/clanModules/syncthing.nix @@ -1,7 +1,8 @@ -{ config -, pkgs -, lib -, ... +{ + config, + pkgs, + lib, + ... }: { options.clan.syncthing = { @@ -53,9 +54,9 @@ assertions = [ { - assertion = - lib.all (attr: builtins.hasAttr attr config.services.syncthing.settings.folders) - config.clan.syncthing.autoShares; + assertion = lib.all ( + attr: builtins.hasAttr attr config.services.syncthing.settings.folders + ) config.clan.syncthing.autoShares; message = '' Syncthing: If you want to AutoShare a folder, you need to have it configured on the sharing device. ''; @@ -80,12 +81,8 @@ group = "syncthing"; - key = - lib.mkDefault - config.clan.secrets.syncthing.secrets."syncthing.key".path or null; - cert = - lib.mkDefault - config.clan.secrets.syncthing.secrets."syncthing.cert".path or null; + key = lib.mkDefault config.clan.secrets.syncthing.secrets."syncthing.key".path or null; + cert = lib.mkDefault config.clan.secrets.syncthing.secrets."syncthing.cert".path or null; settings = { options = { @@ -127,47 +124,33 @@ set -x # query pending deviceID's APIKEY=$(cat ${apiKey}) - PENDING=$(${ - lib.getExe pkgs.curl - } -X GET -H "X-API-Key: $APIKEY" ${baseAddress}${getPendingDevices}) + PENDING=$(${lib.getExe pkgs.curl} -X GET -H "X-API-Key: $APIKEY" ${baseAddress}${getPendingDevices}) PENDING=$(echo $PENDING | ${lib.getExe pkgs.jq} keys[]) # accept pending deviceID's for ID in $PENDING;do - ${ - lib.getExe pkgs.curl - } -X POST -d "{\"deviceId\": $ID}" -H "Content-Type: application/json" -H "X-API-Key: $APIKEY" ${baseAddress}${postNewDevice} + ${lib.getExe pkgs.curl} -X POST -d "{\"deviceId\": $ID}" -H "Content-Type: application/json" -H "X-API-Key: $APIKEY" ${baseAddress}${postNewDevice} # get all shared folders by their ID for folder in ${builtins.toString config.clan.syncthing.autoShares}; do - SHARED_IDS=$(${ - lib.getExe pkgs.curl - } -X GET -H "X-API-Key: $APIKEY" ${baseAddress}${SharedFolderById}"$folder" | ${ - lib.getExe pkgs.jq - } ."devices") - PATCHED_IDS=$(echo $SHARED_IDS | ${ - lib.getExe pkgs.jq - } ".+= [{\"deviceID\": $ID, \"introducedBy\": \"\", \"encryptionPassword\": \"\"}]") - ${ - lib.getExe pkgs.curl - } -X PATCH -d "{\"devices\": $PATCHED_IDS}" -H "X-API-Key: $APIKEY" ${baseAddress}${SharedFolderById}"$folder" + SHARED_IDS=$(${lib.getExe pkgs.curl} -X GET -H "X-API-Key: $APIKEY" ${baseAddress}${SharedFolderById}"$folder" | ${lib.getExe pkgs.jq} ."devices") + PATCHED_IDS=$(echo $SHARED_IDS | ${lib.getExe pkgs.jq} ".+= [{\"deviceID\": $ID, \"introducedBy\": \"\", \"encryptionPassword\": \"\"}]") + ${lib.getExe pkgs.curl} -X PATCH -d "{\"devices\": $PATCHED_IDS}" -H "X-API-Key: $APIKEY" ${baseAddress}${SharedFolderById}"$folder" done done ''; }; - systemd.timers.syncthing-auto-accept = - lib.mkIf config.clan.syncthing.autoAcceptDevices - { - description = "Syncthing Auto Accept"; + systemd.timers.syncthing-auto-accept = lib.mkIf config.clan.syncthing.autoAcceptDevices { + description = "Syncthing Auto Accept"; - wantedBy = [ "syncthing-auto-accept.service" ]; + wantedBy = [ "syncthing-auto-accept.service" ]; - timerConfig = { - OnActiveSec = lib.mkDefault 60; - OnUnitActiveSec = lib.mkDefault 60; - }; - }; + timerConfig = { + OnActiveSec = lib.mkDefault 60; + OnUnitActiveSec = lib.mkDefault 60; + }; + }; systemd.services.syncthing-init-api-key = let @@ -182,9 +165,7 @@ set -efu pipefail APIKEY=$(cat ${apiKey}) - ${ - lib.getExe pkgs.gnused - } -i "s/.*<\/apikey>/$APIKEY<\/apikey>/" /var/lib/syncthing/config.xml + ${lib.getExe pkgs.gnused} -i "s/.*<\/apikey>/$APIKEY<\/apikey>/" /var/lib/syncthing/config.xml # sudo systemctl restart syncthing.service systemctl restart syncthing.service ''; diff --git a/clanModules/waypipe.nix b/clanModules/waypipe.nix index b9d7d76e..322e8d61 100644 --- a/clanModules/waypipe.nix +++ b/clanModules/waypipe.nix @@ -1,7 +1,8 @@ -{ pkgs -, lib -, config -, ... +{ + pkgs, + lib, + config, + ... }: { options.clan.services.waypipe = { @@ -49,7 +50,10 @@ isNormalUser = true; uid = 1000; password = ""; - extraGroups = [ "wheel" "video" ]; + extraGroups = [ + "wheel" + "video" + ]; shell = "/run/current-system/sw/bin/bash"; }; diff --git a/clanModules/zt-tcp-relay.nix b/clanModules/zt-tcp-relay.nix index d48d550f..7ad8f438 100644 --- a/clanModules/zt-tcp-relay.nix +++ b/clanModules/zt-tcp-relay.nix @@ -1,4 +1,10 @@ -{ pkgs, lib, config, ... }: { +{ + pkgs, + lib, + config, + ... +}: +{ options.clan.zt-tcp-relay = { port = lib.mkOption { type = lib.types.port; @@ -13,7 +19,9 @@ wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; serviceConfig = { - ExecStart = "${pkgs.callPackage ../pkgs/zt-tcp-relay {}}/bin/zt-tcp-relay --listen [::]:${builtins.toString config.clan.zt-tcp-relay.port}"; + ExecStart = "${ + pkgs.callPackage ../pkgs/zt-tcp-relay { } + }/bin/zt-tcp-relay --listen [::]:${builtins.toString config.clan.zt-tcp-relay.port}"; Restart = "always"; RestartSec = "5"; dynamicUsers = true; diff --git a/devShell-python.nix b/devShell-python.nix index 4745b09e..149cdffc 100644 --- a/devShell-python.nix +++ b/devShell-python.nix @@ -1,9 +1,10 @@ { perSystem = - { pkgs - , self' - , lib - , ... + { + pkgs, + self', + lib, + ... }: let python3 = pkgs.python3; @@ -20,9 +21,7 @@ ps.pygobject3 ] ); - linuxOnlyPackages = lib.optionals pkgs.stdenv.isLinux [ - pkgs.xdg-utils - ]; + linuxOnlyPackages = lib.optionals pkgs.stdenv.isLinux [ pkgs.xdg-utils ]; in { devShells.python = pkgs.mkShell { diff --git a/devShell.nix b/devShell.nix index 5e5a914d..1815a4f7 100644 --- a/devShell.nix +++ b/devShell.nix @@ -1,9 +1,10 @@ { perSystem = - { pkgs - , self' - , config - , ... + { + pkgs, + self', + config, + ... }: let writers = pkgs.callPackage ./pkgs/builders/script-writers.nix { }; @@ -16,10 +17,9 @@ # A python program to switch between dev-shells # usage: select-shell shell-name # the currently enabled dev-shell gets stored in ./.direnv/selected-shell - select-shell = writers.writePython3Bin "select-shell" - { - flakeIgnore = [ "E501" ]; - } ./pkgs/scripts/select-shell.py; + select-shell = writers.writePython3Bin "select-shell" { + flakeIgnore = [ "E501" ]; + } ./pkgs/scripts/select-shell.py; in { devShells.default = pkgs.mkShell { diff --git a/flake.nix b/flake.nix index 14c799a7..eb482c23 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,9 @@ description = "clan.lol base operating system"; nixConfig.extra-substituters = [ "https://cache.clan.lol" ]; - nixConfig.extra-trusted-public-keys = [ "cache.clan.lol-1:3KztgSAB5R1M+Dz7vzkBGzXdodizbgLXGXKXlcQLA28=" ]; + nixConfig.extra-trusted-public-keys = [ + "cache.clan.lol-1:3KztgSAB5R1M+Dz7vzkBGzXdodizbgLXGXKXlcQLA28=" + ]; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable-small"; @@ -20,44 +22,42 @@ treefmt-nix.inputs.nixpkgs.follows = "nixpkgs"; }; - outputs = inputs @ { flake-parts, ... }: - flake-parts.lib.mkFlake { inherit inputs; } ({ lib, ... }: { - systems = [ - "x86_64-linux" - "aarch64-linux" - "aarch64-darwin" - ]; - imports = [ - ./checks/flake-module.nix - ./devShell.nix - ./devShell-python.nix - ./formatter.nix - ./templates/flake-module.nix - ./clanModules/flake-module.nix + outputs = + inputs@{ flake-parts, ... }: + flake-parts.lib.mkFlake { inherit inputs; } ( + { lib, ... }: + { + systems = [ + "x86_64-linux" + "aarch64-linux" + "aarch64-darwin" + ]; + imports = [ + ./checks/flake-module.nix + ./devShell.nix + ./devShell-python.nix + ./formatter.nix + ./templates/flake-module.nix + ./clanModules/flake-module.nix - ./pkgs/flake-module.nix + ./pkgs/flake-module.nix - ./lib/flake-module.nix - ./nixosModules/flake-module.nix - { - options.flake = flake-parts.lib.mkSubmoduleOptions { - clanInternals = lib.mkOption { - type = lib.types.submodule { - options = { - all-machines-json = lib.mkOption { - type = lib.types.attrsOf lib.types.str; - }; - machines = lib.mkOption { - type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); - }; - machinesFunc = lib.mkOption { - type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); + ./lib/flake-module.nix + ./nixosModules/flake-module.nix + { + options.flake = flake-parts.lib.mkSubmoduleOptions { + clanInternals = lib.mkOption { + type = lib.types.submodule { + options = { + all-machines-json = lib.mkOption { type = lib.types.attrsOf lib.types.str; }; + machines = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); }; + machinesFunc = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); }; }; }; }; }; - }; - } - ]; - }); + } + ]; + } + ); } diff --git a/formatter.nix b/formatter.nix index 4a0a23b4..f035b487 100644 --- a/formatter.nix +++ b/formatter.nix @@ -1,49 +1,47 @@ -{ lib -, inputs -, ... -}: { - imports = [ - inputs.treefmt-nix.flakeModule - ]; - perSystem = { self', pkgs, ... }: { - treefmt.projectRootFile = "flake.nix"; - treefmt.programs.shellcheck.enable = true; +{ lib, inputs, ... }: +{ + imports = [ inputs.treefmt-nix.flakeModule ]; + perSystem = + { self', pkgs, ... }: + { + treefmt.projectRootFile = "flake.nix"; + treefmt.programs.shellcheck.enable = true; - treefmt.programs.mypy.enable = true; - treefmt.programs.mypy.directories = { - "pkgs/clan-cli".extraPythonPackages = self'.packages.clan-cli.pytestDependencies; - "pkgs/clan-vm-manager".extraPythonPackages = self'.packages.clan-vm-manager.propagatedBuildInputs; - }; + treefmt.programs.mypy.enable = true; + treefmt.programs.mypy.directories = { + "pkgs/clan-cli".extraPythonPackages = self'.packages.clan-cli.pytestDependencies; + "pkgs/clan-vm-manager".extraPythonPackages = self'.packages.clan-vm-manager.propagatedBuildInputs; + }; - treefmt.settings.formatter.nix = { - command = "sh"; - options = [ - "-eucx" - '' - # First deadnix - ${lib.getExe pkgs.deadnix} --edit "$@" - # Then nixpkgs-fmt - ${lib.getExe pkgs.nixfmt-rfc-style} "$@" - '' - "--" # this argument is ignored by bash - ]; - includes = [ "*.nix" ]; - excludes = [ - # Was copied from nixpkgs. Keep diff minimal to simplify upstreaming. - "pkgs/builders/script-writers.nix" - ]; + treefmt.settings.formatter.nix = { + command = "sh"; + options = [ + "-eucx" + '' + # First deadnix + ${lib.getExe pkgs.deadnix} --edit "$@" + # Then nixpkgs-fmt + ${lib.getExe pkgs.nixfmt-rfc-style} "$@" + '' + "--" # this argument is ignored by bash + ]; + includes = [ "*.nix" ]; + excludes = [ + # Was copied from nixpkgs. Keep diff minimal to simplify upstreaming. + "pkgs/builders/script-writers.nix" + ]; + }; + treefmt.settings.formatter.python = { + command = "sh"; + options = [ + "-eucx" + '' + ${lib.getExe pkgs.ruff} --fix "$@" + ${lib.getExe pkgs.ruff} format "$@" + '' + "--" # this argument is ignored by bash + ]; + includes = [ "*.py" ]; + }; }; - treefmt.settings.formatter.python = { - command = "sh"; - options = [ - "-eucx" - '' - ${lib.getExe pkgs.ruff} --fix "$@" - ${lib.getExe pkgs.ruff} format "$@" - '' - "--" # this argument is ignored by bash - ]; - includes = [ "*.py" ]; - }; - }; } diff --git a/lib/build-clan/default.nix b/lib/build-clan/default.nix index eaccc25e..aa8f2ed2 100644 --- a/lib/build-clan/default.nix +++ b/lib/build-clan/default.nix @@ -1,66 +1,80 @@ -{ clan-core, nixpkgs, lib }: -{ directory # The directory containing the machines subdirectory -, specialArgs ? { } # Extra arguments to pass to nixosSystem i.e. useful to make self available -, machines ? { } # allows to include machine-specific modules i.e. machines.${name} = { ... } -, clanName # Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to. -, clanIcon ? null # A path to an icon to be used for the clan, should be the same for all machines -, pkgsForSystem ? (_system: null) # A map from arch to pkgs, if specified this nixpkgs will be only imported once for each system. - # This improves performance, but all nipxkgs.* options will be ignored. +{ + clan-core, + nixpkgs, + lib, +}: +{ + directory, # The directory containing the machines subdirectory + specialArgs ? { }, # Extra arguments to pass to nixosSystem i.e. useful to make self available + machines ? { }, # allows to include machine-specific modules i.e. machines.${name} = { ... } + clanName, # Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to. + clanIcon ? null, # A path to an icon to be used for the clan, should be the same for all machines + pkgsForSystem ? (_system: null), # A map from arch to pkgs, if specified this nixpkgs will be only imported once for each system. +# This improves performance, but all nipxkgs.* options will be ignored. }: let - machinesDirs = lib.optionalAttrs (builtins.pathExists "${directory}/machines") (builtins.readDir (directory + /machines)); + machinesDirs = lib.optionalAttrs (builtins.pathExists "${directory}/machines") ( + builtins.readDir (directory + /machines) + ); - machineSettings = machineName: + machineSettings = + machineName: # CLAN_MACHINE_SETTINGS_FILE allows to override the settings file temporarily # This is useful for doing a dry-run before writing changes into the settings.json # Using CLAN_MACHINE_SETTINGS_FILE requires passing --impure to nix eval - if builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE" != "" - then builtins.fromJSON (builtins.readFile (builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE")) + if builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE" != "" then + builtins.fromJSON (builtins.readFile (builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE")) else - lib.optionalAttrs (builtins.pathExists "${directory}/machines/${machineName}/settings.json") - (builtins.fromJSON - (builtins.readFile (directory + /machines/${machineName}/settings.json))); + lib.optionalAttrs (builtins.pathExists "${directory}/machines/${machineName}/settings.json") ( + builtins.fromJSON (builtins.readFile (directory + /machines/${machineName}/settings.json)) + ); # Read additional imports specified via a config option in settings.json # This is not an infinite recursion, because the imports are discovered here # before calling evalModules. # It is still useful to have the imports as an option, as this allows for type # checking and easy integration with the config frontend(s) - machineImports = machineSettings: - map - (module: clan-core.clanModules.${module}) - (machineSettings.clanImports or [ ]); + machineImports = + machineSettings: map (module: clan-core.clanModules.${module}) (machineSettings.clanImports or [ ]); # TODO: remove default system once we have a hardware-config mechanism - nixosConfiguration = { system ? "x86_64-linux", name, pkgs ? null, extraConfig ? { } }: nixpkgs.lib.nixosSystem { - modules = - let - settings = machineSettings name; - in - (machineImports settings) - ++ [ - settings - clan-core.nixosModules.clanCore - extraConfig - (machines.${name} or { }) - ({ - clanCore.clanName = clanName; - clanCore.clanIcon = clanIcon; - clanCore.clanDir = directory; - clanCore.machineName = name; - nixpkgs.hostPlatform = lib.mkDefault system; + nixosConfiguration = + { + system ? "x86_64-linux", + name, + pkgs ? null, + extraConfig ? { }, + }: + nixpkgs.lib.nixosSystem { + modules = + let + settings = machineSettings name; + in + (machineImports settings) + ++ [ + settings + clan-core.nixosModules.clanCore + extraConfig + (machines.${name} or { }) + ( + { + clanCore.clanName = clanName; + clanCore.clanIcon = clanIcon; + clanCore.clanDir = directory; + clanCore.machineName = name; + nixpkgs.hostPlatform = lib.mkDefault system; - # speeds up nix commands by using the nixpkgs from the host system (especially useful in VMs) - nix.registry.nixpkgs.to = { - type = "path"; - path = lib.mkDefault nixpkgs; - }; - } // lib.optionalAttrs (pkgs != null) { - nixpkgs.pkgs = lib.mkForce pkgs; - }) - ]; - inherit specialArgs; - }; + # speeds up nix commands by using the nixpkgs from the host system (especially useful in VMs) + nix.registry.nixpkgs.to = { + type = "path"; + path = lib.mkDefault nixpkgs; + }; + } + // lib.optionalAttrs (pkgs != null) { nixpkgs.pkgs = lib.mkForce pkgs; } + ) + ]; + inherit specialArgs; + }; allMachines = machinesDirs // machines; @@ -77,27 +91,38 @@ let # This instantiates nixos for each system that we support: # configPerSystem = ..nixosConfiguration # We need this to build nixos secret generators for each system - configsPerSystem = builtins.listToAttrs - (builtins.map - (system: lib.nameValuePair system - (lib.mapAttrs - (name: _: nixosConfiguration { + configsPerSystem = builtins.listToAttrs ( + builtins.map ( + system: + lib.nameValuePair system ( + lib.mapAttrs ( + name: _: + nixosConfiguration { inherit name system; pkgs = pkgsForSystem system; - }) - allMachines)) - supportedSystems); + } + ) allMachines + ) + ) supportedSystems + ); - configsFuncPerSystem = builtins.listToAttrs - (builtins.map - (system: lib.nameValuePair system - (lib.mapAttrs - (name: _: args: nixosConfiguration (args // { - inherit name system; - pkgs = pkgsForSystem system; - })) - allMachines)) - supportedSystems); + configsFuncPerSystem = builtins.listToAttrs ( + builtins.map ( + system: + lib.nameValuePair system ( + lib.mapAttrs ( + name: _: args: + nixosConfiguration ( + args + // { + inherit name system; + pkgs = pkgsForSystem system; + } + ) + ) allMachines + ) + ) supportedSystems + ); in { inherit nixosConfigurations; @@ -105,8 +130,11 @@ in clanInternals = { machines = configsPerSystem; machinesFunc = configsFuncPerSystem; - all-machines-json = lib.mapAttrs - (system: configs: nixpkgs.legacyPackages.${system}.writers.writeJSON "machines.json" (lib.mapAttrs (_: m: m.config.system.clan.deployment.data) configs)) - configsPerSystem; + all-machines-json = lib.mapAttrs ( + system: configs: + nixpkgs.legacyPackages.${system}.writers.writeJSON "machines.json" ( + lib.mapAttrs (_: m: m.config.system.clan.deployment.data) configs + ) + ) configsPerSystem; }; } diff --git a/lib/default.nix b/lib/default.nix index 856a4dff..58e95e79 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -1,4 +1,9 @@ -{ lib, clan-core, nixpkgs, ... }: +{ + lib, + clan-core, + nixpkgs, + ... +}: { jsonschema = import ./jsonschema { inherit lib; }; diff --git a/lib/flake-module.nix b/lib/flake-module.nix index 089da6c9..48437bb1 100644 --- a/lib/flake-module.nix +++ b/lib/flake-module.nix @@ -1,11 +1,11 @@ -{ lib -, inputs -, self -, ... -}: { - imports = [ - ./jsonschema/flake-module.nix - ]; +{ + lib, + inputs, + self, + ... +}: +{ + imports = [ ./jsonschema/flake-module.nix ]; flake.lib = import ./default.nix { inherit lib; inherit (inputs) nixpkgs; diff --git a/lib/jsonschema/default.nix b/lib/jsonschema/default.nix index 53dbb9e0..5ecda439 100644 --- a/lib/jsonschema/default.nix +++ b/lib/jsonschema/default.nix @@ -1,243 +1,290 @@ -{ lib ? import -, excludedTypes ? [ +{ + lib ? import , + excludedTypes ? [ "functionTo" "package" - ] + ], }: let # remove _module attribute from options clean = opts: builtins.removeAttrs opts [ "_module" ]; # throw error if option type is not supported - notSupported = option: lib.trace option throw '' - option type '${option.type.name}' ('${option.type.description}') not supported by jsonschema converter - location: ${lib.concatStringsSep "." option.loc} - ''; + notSupported = + option: + lib.trace option throw '' + option type '${option.type.name}' ('${option.type.description}') not supported by jsonschema converter + location: ${lib.concatStringsSep "." option.loc} + ''; isExcludedOption = option: (lib.elem (option.type.name or null) excludedTypes); - filterExcluded = lib.filter (opt: ! isExcludedOption opt); + filterExcluded = lib.filter (opt: !isExcludedOption opt); - filterExcludedAttrs = lib.filterAttrs (_name: opt: ! isExcludedOption opt); - - allBasicTypes = - [ "boolean" "integer" "number" "string" "array" "object" "null" ]; + filterExcludedAttrs = lib.filterAttrs (_name: opt: !isExcludedOption opt); + allBasicTypes = [ + "boolean" + "integer" + "number" + "string" + "array" + "object" + "null" + ]; in rec { # parses a nixos module to a jsonschema - parseModule = module: + parseModule = + module: let - evaled = lib.evalModules { - modules = [ module ]; - }; + evaled = lib.evalModules { modules = [ module ]; }; in parseOptions evaled.options; # parses a set of evaluated nixos options to a jsonschema - parseOptions = options': + parseOptions = + options': let options = filterExcludedAttrs (clean options'); # parse options to jsonschema properties properties = lib.mapAttrs (_name: option: parseOption option) options; # TODO: figure out how to handle if prop.anyOf is used - isRequired = prop: ! (prop ? default || prop.type or null == "object"); + isRequired = prop: !(prop ? default || prop.type or null == "object"); requiredProps = lib.filterAttrs (_: prop: isRequired prop) properties; - required = lib.optionalAttrs (requiredProps != { }) { - required = lib.attrNames requiredProps; - }; + required = lib.optionalAttrs (requiredProps != { }) { required = lib.attrNames requiredProps; }; in # return jsonschema - required // { + required + // { type = "object"; inherit properties; }; # parses and evaluated nixos option to a jsonschema property definition - parseOption = option: + parseOption = + option: let - default = lib.optionalAttrs (option ? default) { - inherit (option) default; - }; + default = lib.optionalAttrs (option ? default) { inherit (option) default; }; description = lib.optionalAttrs (option ? description) { description = option.description.text or option.description; }; in # either type - # TODO: if all nested optiosn are excluded, the parent sould be excluded too - if option.type.name or null == "either" + # TODO: if all nested optiosn are excluded, the parent sould be excluded too + if + option.type.name or null == "either" # return jsonschema property definition for either then let optionsList' = [ - { type = option.type.nestedTypes.left; _type = "option"; loc = option.loc; } - { type = option.type.nestedTypes.right; _type = "option"; loc = option.loc; } + { + type = option.type.nestedTypes.left; + _type = "option"; + loc = option.loc; + } + { + type = option.type.nestedTypes.right; + _type = "option"; + loc = option.loc; + } ]; optionsList = filterExcluded optionsList'; in - default // description // { - anyOf = map parseOption optionsList; - } + default // description // { anyOf = map parseOption optionsList; } # handle nested options (not a submodule) - else if ! option ? _type - then parseOptions option + else if !option ? _type then + parseOptions option # throw if not an option - else if option._type != "option" && option._type != "option-type" - then throw "parseOption: not an option" + else if option._type != "option" && option._type != "option-type" then + throw "parseOption: not an option" # parse nullOr - else if option.type.name == "nullOr" + else if + option.type.name == "nullOr" # return jsonschema property definition for nullOr then let - nestedOption = - { type = option.type.nestedTypes.elemType; _type = "option"; loc = option.loc; }; + nestedOption = { + type = option.type.nestedTypes.elemType; + _type = "option"; + loc = option.loc; + }; in - default // description // { - anyOf = - [{ type = "null"; }] - ++ ( - lib.optional (! isExcludedOption nestedOption) - (parseOption nestedOption) - ); + default + // description + // { + anyOf = [ + { type = "null"; } + ] ++ (lib.optional (!isExcludedOption nestedOption) (parseOption nestedOption)); } # parse bool - else if option.type.name == "bool" + else if + option.type.name == "bool" # return jsonschema property definition for bool - then default // description // { - type = "boolean"; - } + then + default // description // { type = "boolean"; } # parse float - else if option.type.name == "float" + else if + option.type.name == "float" # return jsonschema property definition for float - then default // description // { - type = "number"; - } + then + default // description // { type = "number"; } # parse int - else if (option.type.name == "int" || option.type.name == "positiveInt") + else if + (option.type.name == "int" || option.type.name == "positiveInt") # return jsonschema property definition for int - then default // description // { - type = "integer"; - } + then + default // description // { type = "integer"; } # parse string - else if option.type.name == "str" + else if + option.type.name == "str" # return jsonschema property definition for string - then default // description // { - type = "string"; - } + then + default // description // { type = "string"; } # parse string - else if option.type.name == "path" + else if + option.type.name == "path" # return jsonschema property definition for path - then default // description // { - type = "string"; - } + then + default // description // { type = "string"; } # parse anything - else if option.type.name == "anything" + else if + option.type.name == "anything" # return jsonschema property definition for anything - then default // description // { - type = allBasicTypes; - } + then + default // description // { type = allBasicTypes; } # parse unspecified - else if option.type.name == "unspecified" + else if + option.type.name == "unspecified" # return jsonschema property definition for unspecified - then default // description // { - type = allBasicTypes; - } + then + default // description // { type = allBasicTypes; } # parse raw - else if option.type.name == "raw" + else if + option.type.name == "raw" # return jsonschema property definition for raw - then default // description // { - type = allBasicTypes; - } + then + default // description // { type = allBasicTypes; } # parse enum - else if option.type.name == "enum" + else if + option.type.name == "enum" # return jsonschema property definition for enum - then default // description // { - enum = option.type.functor.payload; - } + then + default // description // { enum = option.type.functor.payload; } # parse listOf submodule - else if option.type.name == "listOf" && option.type.functor.wrapped.name == "submodule" + else if + option.type.name == "listOf" && option.type.functor.wrapped.name == "submodule" # return jsonschema property definition for listOf submodule - then default // description // { - type = "array"; - items = parseOptions (option.type.functor.wrapped.getSubOptions option.loc); - } + then + default + // description + // { + type = "array"; + items = parseOptions (option.type.functor.wrapped.getSubOptions option.loc); + } # parse list - else if (option.type.name == "listOf") + else if + (option.type.name == "listOf") # return jsonschema property definition for list then let - nestedOption = { type = option.type.functor.wrapped; _type = "option"; loc = option.loc; }; + nestedOption = { + type = option.type.functor.wrapped; + _type = "option"; + loc = option.loc; + }; in - default // description // { + default + // description + // { type = "array"; } - // (lib.optionalAttrs (! isExcludedOption nestedOption) { - items = parseOption nestedOption; - }) + // (lib.optionalAttrs (!isExcludedOption nestedOption) { items = parseOption nestedOption; }) # parse list of unspecified else if - (option.type.name == "listOf") - && (option.type.functor.wrapped.name == "unspecified") + (option.type.name == "listOf") && (option.type.functor.wrapped.name == "unspecified") # return jsonschema property definition for list - then default // description // { - type = "array"; - } + then + default // description // { type = "array"; } # parse attrsOf submodule - else if option.type.name == "attrsOf" && option.type.nestedTypes.elemType.name == "submodule" + else if + option.type.name == "attrsOf" && option.type.nestedTypes.elemType.name == "submodule" # return jsonschema property definition for attrsOf submodule - then default // description // { - type = "object"; - additionalProperties = parseOptions (option.type.nestedTypes.elemType.getSubOptions option.loc); - } + then + default + // description + // { + type = "object"; + additionalProperties = parseOptions (option.type.nestedTypes.elemType.getSubOptions option.loc); + } # parse attrs - else if option.type.name == "attrs" + else if + option.type.name == "attrs" # return jsonschema property definition for attrs - then default // description // { - type = "object"; - additionalProperties = true; - } + then + default + // description + // { + type = "object"; + additionalProperties = true; + } # parse attrsOf # TODO: if nested option is excluded, the parent sould be excluded too - else if option.type.name == "attrsOf" || option.type.name == "lazyAttrsOf" + else if + option.type.name == "attrsOf" || option.type.name == "lazyAttrsOf" # return jsonschema property definition for attrs then let - nestedOption = { type = option.type.nestedTypes.elemType; _type = "option"; loc = option.loc; }; + nestedOption = { + type = option.type.nestedTypes.elemType; + _type = "option"; + loc = option.loc; + }; in - default // description // { + default + // description + // { type = "object"; additionalProperties = - if ! isExcludedOption nestedOption - then parseOption { type = option.type.nestedTypes.elemType; _type = "option"; loc = option.loc; } - else false; + if !isExcludedOption nestedOption then + parseOption { + type = option.type.nestedTypes.elemType; + _type = "option"; + loc = option.loc; + } + else + false; } # parse submodule - else if option.type.name == "submodule" + else if + option.type.name == "submodule" # return jsonschema property definition for submodule # then (lib.attrNames (option.type.getSubOptions option.loc).opt) - then parseOptions (option.type.getSubOptions option.loc) + then + parseOptions (option.type.getSubOptions option.loc) # throw error if option type is not supported - else notSupported option; + else + notSupported option; } diff --git a/lib/jsonschema/example-interface.nix b/lib/jsonschema/example-interface.nix index 4a8cfe70..97fe4145 100644 --- a/lib/jsonschema/example-interface.nix +++ b/lib/jsonschema/example-interface.nix @@ -1,7 +1,6 @@ -/* - An example nixos module declaring an interface. -*/ -{ lib, ... }: { +# An example nixos module declaring an interface. +{ lib, ... }: +{ options = { # str name = lib.mkOption { @@ -44,7 +43,11 @@ # list of str kernelModules = lib.mkOption { type = lib.types.listOf lib.types.str; - default = [ "nvme" "xhci_pci" "ahci" ]; + default = [ + "nvme" + "xhci_pci" + "ahci" + ]; description = "A list of enabled kernel modules"; }; }; diff --git a/lib/jsonschema/flake-module.nix b/lib/jsonschema/flake-module.nix index 09e792dd..db133d60 100644 --- a/lib/jsonschema/flake-module.nix +++ b/lib/jsonschema/flake-module.nix @@ -1,29 +1,31 @@ { - perSystem = { pkgs, ... }: { - checks = { + perSystem = + { pkgs, ... }: + { + checks = { - # check if the `clan config` example jsonschema and data is valid - lib-jsonschema-example-valid = pkgs.runCommand "lib-jsonschema-example-valid" { } '' - echo "Checking that example-schema.json is valid" - ${pkgs.check-jsonschema}/bin/check-jsonschema \ - --check-metaschema ${./.}/example-schema.json + # check if the `clan config` example jsonschema and data is valid + lib-jsonschema-example-valid = pkgs.runCommand "lib-jsonschema-example-valid" { } '' + echo "Checking that example-schema.json is valid" + ${pkgs.check-jsonschema}/bin/check-jsonschema \ + --check-metaschema ${./.}/example-schema.json - echo "Checking that example-data.json is valid according to example-schema.json" - ${pkgs.check-jsonschema}/bin/check-jsonschema \ - --schemafile ${./.}/example-schema.json \ - ${./.}/example-data.json + echo "Checking that example-data.json is valid according to example-schema.json" + ${pkgs.check-jsonschema}/bin/check-jsonschema \ + --schemafile ${./.}/example-schema.json \ + ${./.}/example-data.json - touch $out - ''; + touch $out + ''; - # check if the `clan config` nix jsonschema converter unit tests succeed - lib-jsonschema-nix-unit-tests = pkgs.runCommand "lib-jsonschema-nix-unit-tests" { } '' - export NIX_PATH=nixpkgs=${pkgs.path} - ${pkgs.nix-unit}/bin/nix-unit \ - ${./.}/test.nix \ - --eval-store $(realpath .) - touch $out - ''; + # check if the `clan config` nix jsonschema converter unit tests succeed + lib-jsonschema-nix-unit-tests = pkgs.runCommand "lib-jsonschema-nix-unit-tests" { } '' + export NIX_PATH=nixpkgs=${pkgs.path} + ${pkgs.nix-unit}/bin/nix-unit \ + ${./.}/test.nix \ + --eval-store $(realpath .) + touch $out + ''; + }; }; - }; } diff --git a/lib/jsonschema/test.nix b/lib/jsonschema/test.nix index 34e05274..adf8ab00 100644 --- a/lib/jsonschema/test.nix +++ b/lib/jsonschema/test.nix @@ -1,6 +1,7 @@ # run these tests via `nix-unit ./test.nix` -{ lib ? (import { }).lib -, slib ? import ./. { inherit lib; } +{ + lib ? (import { }).lib, + slib ? import ./. { inherit lib; }, }: { parseOption = import ./test_parseOption.nix { inherit lib slib; }; diff --git a/lib/jsonschema/test_parseOption.nix b/lib/jsonschema/test_parseOption.nix index a20b329e..8471ed43 100644 --- a/lib/jsonschema/test_parseOption.nix +++ b/lib/jsonschema/test_parseOption.nix @@ -1,21 +1,25 @@ # tests for the nixos options to jsonschema converter # run these tests via `nix-unit ./test.nix` -{ lib ? (import { }).lib -, slib ? import ./. { inherit lib; } +{ + lib ? (import { }).lib, + slib ? import ./. { inherit lib; }, }: let description = "Test Description"; - evalType = type: default: + evalType = + type: default: let evaledConfig = lib.evalModules { - modules = [{ - options.opt = lib.mkOption { - inherit type; - inherit default; - inherit description; - }; - }]; + modules = [ + { + options.opt = lib.mkOption { + inherit type; + inherit default; + inherit description; + }; + } + ]; }; in evaledConfig.options.opt; @@ -25,11 +29,7 @@ in testNoDefaultNoDescription = let evaledConfig = lib.evalModules { - modules = [{ - options.opt = lib.mkOption { - type = lib.types.bool; - }; - }]; + modules = [ { options.opt = lib.mkOption { type = lib.types.bool; }; } ]; }; in { @@ -42,15 +42,17 @@ in testDescriptionIsAttrs = let evaledConfig = lib.evalModules { - modules = [{ - options.opt = lib.mkOption { - type = lib.types.bool; - description = { - _type = "mdDoc"; - text = description; + modules = [ + { + options.opt = lib.mkOption { + type = lib.types.bool; + description = { + _type = "mdDoc"; + text = description; + }; }; - }; - }]; + } + ]; }; in { @@ -112,7 +114,11 @@ in testEnum = let default = "foo"; - values = [ "foo" "bar" "baz" ]; + values = [ + "foo" + "bar" + "baz" + ]; in { expr = slib.parseOption (evalType (lib.types.enum values) default); @@ -124,7 +130,11 @@ in testListOfInt = let - default = [ 1 2 3 ]; + default = [ + 1 + 2 + 3 + ]; in { expr = slib.parseOption (evalType (lib.types.listOf lib.types.int) default); @@ -139,14 +149,26 @@ in testListOfUnspecified = let - default = [ 1 2 3 ]; + default = [ + 1 + 2 + 3 + ]; in { expr = slib.parseOption (evalType (lib.types.listOf lib.types.unspecified) default); expected = { type = "array"; items = { - type = [ "boolean" "integer" "number" "string" "array" "object" "null" ]; + type = [ + "boolean" + "integer" + "number" + "string" + "array" + "object" + "null" + ]; }; inherit default description; }; @@ -154,7 +176,11 @@ in testAttrs = let - default = { foo = 1; bar = 2; baz = 3; }; + default = { + foo = 1; + bar = 2; + baz = 3; + }; in { expr = slib.parseOption (evalType (lib.types.attrs) default); @@ -167,7 +193,11 @@ in testAttrsOfInt = let - default = { foo = 1; bar = 2; baz = 3; }; + default = { + foo = 1; + bar = 2; + baz = 3; + }; in { expr = slib.parseOption (evalType (lib.types.attrsOf lib.types.int) default); @@ -182,7 +212,11 @@ in testLazyAttrsOfInt = let - default = { foo = 1; bar = 2; baz = 3; }; + default = { + foo = 1; + bar = 2; + baz = 3; + }; in { expr = slib.parseOption (evalType (lib.types.lazyAttrsOf lib.types.int) default); @@ -286,7 +320,10 @@ in inherit description; }; }; - default = { foo.opt = false; bar.opt = true; }; + default = { + foo.opt = false; + bar.opt = true; + }; in { expr = slib.parseOption (evalType (lib.types.attrsOf (lib.types.submodule subModule)) default); @@ -315,7 +352,10 @@ in inherit description; }; }; - default = [{ opt = false; } { opt = true; }]; + default = [ + { opt = false; } + { opt = true; } + ]; in { expr = slib.parseOption (evalType (lib.types.listOf (lib.types.submodule subModule)) default); @@ -358,7 +398,15 @@ in expr = slib.parseOption (evalType lib.types.anything default); expected = { inherit default description; - type = [ "boolean" "integer" "number" "string" "array" "object" "null" ]; + type = [ + "boolean" + "integer" + "number" + "string" + "array" + "object" + "null" + ]; }; }; @@ -370,7 +418,15 @@ in expr = slib.parseOption (evalType lib.types.unspecified default); expected = { inherit default description; - type = [ "boolean" "integer" "number" "string" "array" "object" "null" ]; + type = [ + "boolean" + "integer" + "number" + "string" + "array" + "object" + "null" + ]; }; }; @@ -382,7 +438,15 @@ in expr = slib.parseOption (evalType lib.types.raw default); expected = { inherit default description; - type = [ "boolean" "integer" "number" "string" "array" "object" "null" ]; + type = [ + "boolean" + "integer" + "number" + "string" + "array" + "object" + "null" + ]; }; }; } diff --git a/lib/jsonschema/test_parseOptions.nix b/lib/jsonschema/test_parseOptions.nix index c4564a7e..1faf6e3b 100644 --- a/lib/jsonschema/test_parseOptions.nix +++ b/lib/jsonschema/test_parseOptions.nix @@ -1,14 +1,13 @@ # tests for the nixos options to jsonschema converter # run these tests via `nix-unit ./test.nix` -{ lib ? (import { }).lib -, slib ? import ./. { inherit lib; } +{ + lib ? (import { }).lib, + slib ? import ./. { inherit lib; }, }: let evaledOptions = let - evaledConfig = lib.evalModules { - modules = [ ./example-interface.nix ]; - }; + evaledConfig = lib.evalModules { modules = [ ./example-interface.nix ]; }; in evaledConfig.options; in @@ -21,11 +20,7 @@ in testParseNestedOptions = let evaled = lib.evalModules { - modules = [{ - options.foo.bar = lib.mkOption { - type = lib.types.bool; - }; - }]; + modules = [ { options.foo.bar = lib.mkOption { type = lib.types.bool; }; } ]; }; in { @@ -34,7 +29,9 @@ in properties = { foo = { properties = { - bar = { type = "boolean"; }; + bar = { + type = "boolean"; + }; }; required = [ "bar" ]; type = "object"; diff --git a/nixosModules/clanCore/backups.nix b/nixosModules/clanCore/backups.nix index 44d6f4fe..06a43744 100644 --- a/nixosModules/clanCore/backups.nix +++ b/nixosModules/clanCore/backups.nix @@ -1,45 +1,48 @@ { lib, ... }: { - imports = [ - ./state.nix - ]; + imports = [ ./state.nix ]; options.clanCore.backups = { providers = lib.mkOption { - type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: { - options = { - name = lib.mkOption { - type = lib.types.str; - default = name; - description = '' - Name of the backup provider - ''; - }; - list = lib.mkOption { - type = lib.types.str; - description = '' - script to list backups - ''; - }; - restore = lib.mkOption { - type = lib.types.str; - description = '' - script to restore a backup - should take an optional service name as argument - gets ARCHIVE_ID, LOCATION, JOB and FOLDERS as environment variables - ARCHIVE_ID is the id of the backup - LOCATION is the remote identifier of the backup - JOB is the job name of the backup - FOLDERS is a colon separated list of folders to restore - ''; - }; - create = lib.mkOption { - type = lib.types.str; - description = '' - script to start a backup - ''; - }; - }; - })); + type = lib.types.attrsOf ( + lib.types.submodule ( + { name, ... }: + { + options = { + name = lib.mkOption { + type = lib.types.str; + default = name; + description = '' + Name of the backup provider + ''; + }; + list = lib.mkOption { + type = lib.types.str; + description = '' + script to list backups + ''; + }; + restore = lib.mkOption { + type = lib.types.str; + description = '' + script to restore a backup + should take an optional service name as argument + gets ARCHIVE_ID, LOCATION, JOB and FOLDERS as environment variables + ARCHIVE_ID is the id of the backup + LOCATION is the remote identifier of the backup + JOB is the job name of the backup + FOLDERS is a colon separated list of folders to restore + ''; + }; + create = lib.mkOption { + type = lib.types.str; + description = '' + script to start a backup + ''; + }; + }; + } + ) + ); default = { }; description = '' Configured backup providers which are used by this machine diff --git a/nixosModules/clanCore/imports.nix b/nixosModules/clanCore/imports.nix index e89eaf0f..959378eb 100644 --- a/nixosModules/clanCore/imports.nix +++ b/nixosModules/clanCore/imports.nix @@ -1,6 +1,5 @@ -{ lib -, ... -}: { +{ lib, ... }: +{ /* Declaring imports inside the module system does not trigger an infinite recursion in this case because buildClan generates the imports from the diff --git a/nixosModules/clanCore/manual.nix b/nixosModules/clanCore/manual.nix index f6312138..e5b4f9a2 100644 --- a/nixosModules/clanCore/manual.nix +++ b/nixosModules/clanCore/manual.nix @@ -1 +1,4 @@ -{ pkgs, ... }: { documentation.nixos.enable = pkgs.lib.mkDefault false; } +{ pkgs, ... }: +{ + documentation.nixos.enable = pkgs.lib.mkDefault false; +} diff --git a/nixosModules/clanCore/metadata.nix b/nixosModules/clanCore/metadata.nix index 391f32b8..d0bdf145 100644 --- a/nixosModules/clanCore/metadata.nix +++ b/nixosModules/clanCore/metadata.nix @@ -1,4 +1,5 @@ -{ lib, pkgs, ... }: { +{ lib, pkgs, ... }: +{ options.clanCore = { clanName = lib.mkOption { type = lib.types.str; diff --git a/nixosModules/clanCore/networking.nix b/nixosModules/clanCore/networking.nix index affed63f..a82a7469 100644 --- a/nixosModules/clanCore/networking.nix +++ b/nixosModules/clanCore/networking.nix @@ -49,7 +49,18 @@ }; imports = [ - (lib.mkRenamedOptionModule [ "clan" "networking" "deploymentAddress" ] [ "clan" "networking" "targetHost" ]) + (lib.mkRenamedOptionModule + [ + "clan" + "networking" + "deploymentAddress" + ] + [ + "clan" + "networking" + "targetHost" + ] + ) ]; config = { # conflicts with systemd-resolved @@ -64,16 +75,18 @@ systemd.network.wait-online.enable = false; # Provide a default network configuration but don't compete with network-manager or dhcpcd - systemd.network.networks."50-uplink" = lib.mkIf (!(config.networking.networkmanager.enable || config.networking.dhcpcd.enable)) { - matchConfig.Type = "ether"; - networkConfig = { - DHCP = "yes"; - LLDP = "yes"; - LLMNR = "yes"; - MulticastDNS = "yes"; - IPv6AcceptRA = "yes"; - }; - }; + systemd.network.networks."50-uplink" = + lib.mkIf (!(config.networking.networkmanager.enable || config.networking.dhcpcd.enable)) + { + matchConfig.Type = "ether"; + networkConfig = { + DHCP = "yes"; + LLDP = "yes"; + LLMNR = "yes"; + MulticastDNS = "yes"; + IPv6AcceptRA = "yes"; + }; + }; # Use networkd instead of the pile of shell scripts networking.useNetworkd = lib.mkDefault true; diff --git a/nixosModules/clanCore/options.nix b/nixosModules/clanCore/options.nix index b2077999..68824b4b 100644 --- a/nixosModules/clanCore/options.nix +++ b/nixosModules/clanCore/options.nix @@ -1,4 +1,10 @@ -{ pkgs, options, lib, ... }: { +{ + pkgs, + options, + lib, + ... +}: +{ options.clanCore.optionsNix = lib.mkOption { type = lib.types.raw; internal = true; diff --git a/nixosModules/clanCore/outputs.nix b/nixosModules/clanCore/outputs.nix index 75d2c063..63435724 100644 --- a/nixosModules/clanCore/outputs.nix +++ b/nixosModules/clanCore/outputs.nix @@ -1,4 +1,10 @@ -{ config, lib, pkgs, ... }: { +{ + config, + lib, + pkgs, + ... +}: +{ # TODO: factor these out into a separate interface.nix. # Also think about moving these options out of `system.clan`. # Maybe we should not re-use the already polluted confg.system namespace @@ -90,6 +96,8 @@ inherit (config.clan.deployment) requireExplicitUpdate; inherit (config.clanCore) secretsUploadDirectory; }; - system.clan.deployment.file = pkgs.writeText "deployment.json" (builtins.toJSON config.system.clan.deployment.data); + system.clan.deployment.file = pkgs.writeText "deployment.json" ( + builtins.toJSON config.system.clan.deployment.data + ); }; } diff --git a/nixosModules/clanCore/packages.nix b/nixosModules/clanCore/packages.nix index 3481b3ec..7771874e 100644 --- a/nixosModules/clanCore/packages.nix +++ b/nixosModules/clanCore/packages.nix @@ -1,4 +1,5 @@ -{ pkgs, ... }: { +{ pkgs, ... }: +{ # essential debugging tools for networked services environment.systemPackages = [ pkgs.dnsutils diff --git a/nixosModules/clanCore/secrets/default.nix b/nixosModules/clanCore/secrets/default.nix index 18371359..9f896b8c 100644 --- a/nixosModules/clanCore/secrets/default.nix +++ b/nixosModules/clanCore/secrets/default.nix @@ -1,7 +1,17 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: { options.clanCore.secretStore = lib.mkOption { - type = lib.types.enum [ "sops" "password-store" "vm" "custom" ]; + type = lib.types.enum [ + "sops" + "password-store" + "vm" + "custom" + ]; default = "sops"; description = '' method to store secrets @@ -34,8 +44,8 @@ options.clanCore.secrets = lib.mkOption { default = { }; - type = lib.types.attrsOf - (lib.types.submodule (service: { + type = lib.types.attrsOf ( + lib.types.submodule (service: { options = { name = lib.mkOption { type = lib.types.str; @@ -45,55 +55,60 @@ ''; }; generator = lib.mkOption { - type = lib.types.submodule ({ config, ... }: { - options = { - path = lib.mkOption { - type = lib.types.listOf (lib.types.either lib.types.path lib.types.package); - default = [ ]; - description = '' - Extra paths to add to the PATH environment variable when running the generator. - ''; - }; - prompt = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - description = '' - prompt text to ask for a value. - This value will be passed to the script as the environment variable $prompt_value. - ''; - }; - script = lib.mkOption { - type = lib.types.str; - description = '' - Script to generate the secret. - The script will be called with the following variables: - - facts: path to a directory where facts can be stored - - secrets: path to a directory where secrets can be stored - The script is expected to generate all secrets and facts defined in the module. - ''; - }; - finalScript = lib.mkOption { - type = lib.types.str; - readOnly = true; - internal = true; - default = '' - set -eu -o pipefail + type = lib.types.submodule ( + { config, ... }: + { + options = { + path = lib.mkOption { + type = lib.types.listOf (lib.types.either lib.types.path lib.types.package); + default = [ ]; + description = '' + Extra paths to add to the PATH environment variable when running the generator. + ''; + }; + prompt = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + prompt text to ask for a value. + This value will be passed to the script as the environment variable $prompt_value. + ''; + }; + script = lib.mkOption { + type = lib.types.str; + description = '' + Script to generate the secret. + The script will be called with the following variables: + - facts: path to a directory where facts can be stored + - secrets: path to a directory where secrets can be stored + The script is expected to generate all secrets and facts defined in the module. + ''; + }; + finalScript = lib.mkOption { + type = lib.types.str; + readOnly = true; + internal = true; + default = '' + set -eu -o pipefail - export PATH="${lib.makeBinPath config.path}:${pkgs.coreutils}/bin" + export PATH="${lib.makeBinPath config.path}:${pkgs.coreutils}/bin" - # prepare sandbox user - mkdir -p /etc - cp ${pkgs.runCommand "fake-etc" {} '' - export PATH="${pkgs.coreutils}/bin" - mkdir -p $out - cp /etc/* $out/ - ''}/* /etc/ + # prepare sandbox user + mkdir -p /etc + cp ${ + pkgs.runCommand "fake-etc" { } '' + export PATH="${pkgs.coreutils}/bin" + mkdir -p $out + cp /etc/* $out/ + '' + }/* /etc/ - ${config.script} - ''; + ${config.script} + ''; + }; }; - }; - }); + } + ); }; secrets = let @@ -101,68 +116,77 @@ in lib.mkOption { default = { }; - type = lib.types.attrsOf (lib.types.submodule ({ config, name, ... }: { - options = { - name = lib.mkOption { - type = lib.types.str; - description = '' - name of the secret - ''; - default = name; - }; - path = lib.mkOption { - type = lib.types.str; - description = '' - path to a secret which is generated by the generator - ''; - default = "${config'.clanCore.secretsDirectory}/${config'.clanCore.secretsPrefix}${config.name}"; - }; - } // lib.optionalAttrs (config'.clanCore.secretStore == "sops") { - groups = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = config'.clanCore.sops.defaultGroups; - description = '' - Groups to decrypt the secret for. By default we always use the user's key. - ''; - }; - }; - })); + type = lib.types.attrsOf ( + lib.types.submodule ( + { config, name, ... }: + { + options = + { + name = lib.mkOption { + type = lib.types.str; + description = '' + name of the secret + ''; + default = name; + }; + path = lib.mkOption { + type = lib.types.str; + description = '' + path to a secret which is generated by the generator + ''; + default = "${config'.clanCore.secretsDirectory}/${config'.clanCore.secretsPrefix}${config.name}"; + }; + } + // lib.optionalAttrs (config'.clanCore.secretStore == "sops") { + groups = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = config'.clanCore.sops.defaultGroups; + description = '' + Groups to decrypt the secret for. By default we always use the user's key. + ''; + }; + }; + } + ) + ); description = '' path where the secret is located in the filesystem ''; }; facts = lib.mkOption { default = { }; - type = lib.types.attrsOf (lib.types.submodule (fact: { - options = { - name = lib.mkOption { - type = lib.types.str; - description = '' - name of the fact - ''; - default = fact.config._module.args.name; + type = lib.types.attrsOf ( + lib.types.submodule (fact: { + options = { + name = lib.mkOption { + type = lib.types.str; + description = '' + name of the fact + ''; + default = fact.config._module.args.name; + }; + path = lib.mkOption { + type = lib.types.path; + description = '' + path to a fact which is generated by the generator + ''; + default = + config.clanCore.clanDir + + "/machines/${config.clanCore.machineName}/facts/${fact.config._module.args.name}"; + }; + value = lib.mkOption { + defaultText = lib.literalExpression "\${config.clanCore.clanDir}/\${fact.config.path}"; + type = lib.types.nullOr lib.types.str; + default = + if builtins.pathExists fact.config.path then lib.strings.fileContents fact.config.path else null; + }; }; - path = lib.mkOption { - type = lib.types.path; - description = '' - path to a fact which is generated by the generator - ''; - default = config.clanCore.clanDir + "/machines/${config.clanCore.machineName}/facts/${fact.config._module.args.name}"; - }; - value = lib.mkOption { - defaultText = lib.literalExpression "\${config.clanCore.clanDir}/\${fact.config.path}"; - type = lib.types.nullOr lib.types.str; - default = - if builtins.pathExists fact.config.path then - lib.strings.fileContents fact.config.path - else - null; - }; - }; - })); + }) + ); }; }; - })); + }) + ); }; imports = [ ./sops.nix diff --git a/nixosModules/clanCore/secrets/password-store.nix b/nixosModules/clanCore/secrets/password-store.nix index 529a6c24..b3bf615d 100644 --- a/nixosModules/clanCore/secrets/password-store.nix +++ b/nixosModules/clanCore/secrets/password-store.nix @@ -13,4 +13,3 @@ system.clan.secretsModule = "clan_cli.secrets.modules.password_store"; }; } - diff --git a/nixosModules/clanCore/secrets/sops.nix b/nixosModules/clanCore/secrets/sops.nix index a5f627a5..d242c83a 100644 --- a/nixosModules/clanCore/secrets/sops.nix +++ b/nixosModules/clanCore/secrets/sops.nix @@ -1,22 +1,33 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let secretsDir = config.clanCore.clanDir + "/sops/secrets"; groupsDir = config.clanCore.clanDir + "/sops/groups"; - # My symlink is in the nixos module detected as a directory also it works in the repl. Is this because of pure evaluation? - containsSymlink = path: - builtins.pathExists path && (builtins.readFileType path == "directory" || builtins.readFileType path == "symlink"); + containsSymlink = + path: + builtins.pathExists path + && (builtins.readFileType path == "directory" || builtins.readFileType path == "symlink"); - containsMachine = parent: name: type: + containsMachine = + parent: name: type: type == "directory" && containsSymlink "${parent}/${name}/machines/${config.clanCore.machineName}"; - containsMachineOrGroups = name: type: - (containsMachine secretsDir name type) || lib.any (group: type == "directory" && containsSymlink "${secretsDir}/${name}/groups/${group}") groups; + containsMachineOrGroups = + name: type: + (containsMachine secretsDir name type) + || lib.any ( + group: type == "directory" && containsSymlink "${secretsDir}/${name}/groups/${group}" + ) groups; - filterDir = filter: dir: - lib.optionalAttrs (builtins.pathExists dir) - (lib.filterAttrs filter (builtins.readDir dir)); + filterDir = + filter: dir: + lib.optionalAttrs (builtins.pathExists dir) (lib.filterAttrs filter (builtins.readDir dir)); groups = builtins.attrNames (filterDir (containsMachine groupsDir) groupsDir); secrets = filterDir containsMachineOrGroups secretsDir; @@ -34,17 +45,18 @@ in clanCore.secretsDirectory = "/run/secrets"; clanCore.secretsPrefix = config.clanCore.machineName + "-"; system.clan.secretsModule = "clan_cli.secrets.modules.sops"; - sops.secrets = builtins.mapAttrs - (name: _: { - sopsFile = config.clanCore.clanDir + "/sops/secrets/${name}/secret"; - format = "binary"; - }) - secrets; + sops.secrets = builtins.mapAttrs (name: _: { + sopsFile = config.clanCore.clanDir + "/sops/secrets/${name}/secret"; + format = "binary"; + }) secrets; # To get proper error messages about missing secrets we need a dummy secret file that is always present - sops.defaultSopsFile = lib.mkIf config.sops.validateSopsFiles (lib.mkDefault (builtins.toString (pkgs.writeText "dummy.yaml" ""))); + sops.defaultSopsFile = lib.mkIf config.sops.validateSopsFiles ( + lib.mkDefault (builtins.toString (pkgs.writeText "dummy.yaml" "")) + ); - sops.age.keyFile = lib.mkIf (builtins.pathExists (config.clanCore.clanDir + "/sops/secrets/${config.clanCore.machineName}-age.key/secret")) - (lib.mkDefault "/var/lib/sops-nix/key.txt"); + sops.age.keyFile = lib.mkIf (builtins.pathExists ( + config.clanCore.clanDir + "/sops/secrets/${config.clanCore.machineName}-age.key/secret" + )) (lib.mkDefault "/var/lib/sops-nix/key.txt"); clanCore.secretsUploadDirectory = lib.mkDefault "/var/lib/sops-nix"; }; } diff --git a/nixosModules/clanCore/secrets/vm.nix b/nixosModules/clanCore/secrets/vm.nix index ce071dd2..1622c5cf 100644 --- a/nixosModules/clanCore/secrets/vm.nix +++ b/nixosModules/clanCore/secrets/vm.nix @@ -7,4 +7,3 @@ system.clan.factsModule = "clan_cli.facts.modules.vm"; }; } - diff --git a/nixosModules/clanCore/state.nix b/nixosModules/clanCore/state.nix index f2ecf659..50bc80a1 100644 --- a/nixosModules/clanCore/state.nix +++ b/nixosModules/clanCore/state.nix @@ -1,41 +1,43 @@ { lib, ... }: { # defaults - config.clanCore.state.HOME.folders = [ - "/home" - ]; + config.clanCore.state.HOME.folders = [ "/home" ]; # interface options.clanCore.state = lib.mkOption { default = { }; - type = lib.types.attrsOf - (lib.types.submodule ({ ... }: { - options = { - folders = lib.mkOption { - type = lib.types.listOf lib.types.str; - description = '' - Folder where state resides in - ''; - }; - preRestoreScript = lib.mkOption { - type = lib.types.str; - default = ":"; - description = '' - script to run before restoring the state dir from a backup + type = lib.types.attrsOf ( + lib.types.submodule ( + { ... }: + { + options = { + folders = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = '' + Folder where state resides in + ''; + }; + preRestoreScript = lib.mkOption { + type = lib.types.str; + default = ":"; + description = '' + script to run before restoring the state dir from a backup - Utilize this to stop services which currently access these folders - ''; - }; - postRestoreScript = lib.mkOption { - type = lib.types.str; - default = ":"; - description = '' - script to restore the service after the state dir was restored from a backup + Utilize this to stop services which currently access these folders + ''; + }; + postRestoreScript = lib.mkOption { + type = lib.types.str; + default = ":"; + description = '' + script to restore the service after the state dir was restored from a backup - Utilize this to start services which were previously stopped - ''; + Utilize this to start services which were previously stopped + ''; + }; }; - }; - })); + } + ) + ); }; } diff --git a/nixosModules/clanCore/vm.nix b/nixosModules/clanCore/vm.nix index be1d429c..62d98d21 100644 --- a/nixosModules/clanCore/vm.nix +++ b/nixosModules/clanCore/vm.nix @@ -1,12 +1,15 @@ -{ lib, config, pkgs, options, extendModules, modulesPath, ... }: +{ + lib, + config, + pkgs, + options, + extendModules, + modulesPath, + ... +}: let # Flatten the list of state folders into a single list - stateFolders = lib.flatten ( - lib.mapAttrsToList - (_item: attrs: attrs.folders) - config.clanCore.state - ); - + stateFolders = lib.flatten (lib.mapAttrsToList (_item: attrs: attrs.folders) config.clanCore.state); vmModule = { imports = [ @@ -32,7 +35,10 @@ let # currently needed for system.etc.overlay.enable boot.kernelPackages = pkgs.linuxPackages_latest; - boot.initrd.systemd.storePaths = [ pkgs.util-linux pkgs.e2fsprogs ]; + boot.initrd.systemd.storePaths = [ + pkgs.util-linux + pkgs.e2fsprogs + ]; boot.initrd.systemd.emergencyAccess = true; # sysusers is faster than nixos's perl scripts @@ -43,50 +49,72 @@ let boot.initrd.kernelModules = [ "virtiofs" ]; virtualisation.writableStore = false; - virtualisation.fileSystems = lib.mkForce ({ - "/nix/store" = { - device = "nix-store"; - options = [ "x-systemd.requires=systemd-modules-load.service" "ro" ]; - fsType = "virtiofs"; - }; + virtualisation.fileSystems = lib.mkForce ( + { + "/nix/store" = { + device = "nix-store"; + options = [ + "x-systemd.requires=systemd-modules-load.service" + "ro" + ]; + fsType = "virtiofs"; + }; - "/" = { - device = "/dev/vda"; - fsType = "ext4"; - options = [ "defaults" "x-systemd.makefs" "nobarrier" "noatime" "nodiratime" "data=writeback" "discard" ]; - }; + "/" = { + device = "/dev/vda"; + fsType = "ext4"; + options = [ + "defaults" + "x-systemd.makefs" + "nobarrier" + "noatime" + "nodiratime" + "data=writeback" + "discard" + ]; + }; - "/vmstate" = { - device = "/dev/vdb"; - options = [ "x-systemd.makefs" "noatime" "nodiratime" "discard" ]; - noCheck = true; - fsType = "ext4"; - }; + "/vmstate" = { + device = "/dev/vdb"; + options = [ + "x-systemd.makefs" + "noatime" + "nodiratime" + "discard" + ]; + noCheck = true; + fsType = "ext4"; + }; - ${config.clanCore.secretsUploadDirectory} = { - device = "secrets"; - fsType = "9p"; - neededForBoot = true; - options = [ "trans=virtio" "version=9p2000.L" "cache=loose" ]; - }; - - } // lib.listToAttrs (map - (folder: - lib.nameValuePair folder { - device = "/vmstate${folder}"; - fsType = "none"; - options = [ "bind" ]; - }) - stateFolders)); + ${config.clanCore.secretsUploadDirectory} = { + device = "secrets"; + fsType = "9p"; + neededForBoot = true; + options = [ + "trans=virtio" + "version=9p2000.L" + "cache=loose" + ]; + }; + } + // lib.listToAttrs ( + map ( + folder: + lib.nameValuePair folder { + device = "/vmstate${folder}"; + fsType = "none"; + options = [ "bind" ]; + } + ) stateFolders + ) + ); }; # We cannot simply merge the VM config into the current system config, because # it is not necessarily a VM. # Instead we use extendModules to create a second instance of the current # system configuration, and then merge the VM config into that. - vmConfig = extendModules { - modules = [ vmModule ]; - }; + vmConfig = extendModules { modules = [ vmModule ]; }; in { options = { @@ -210,12 +238,14 @@ in }; # for clan vm create system.clan.vm = { - create = pkgs.writeText "vm.json" (builtins.toJSON { - initrd = "${vmConfig.config.system.build.initialRamdisk}/${vmConfig.config.system.boot.loader.initrdFile}"; - toplevel = vmConfig.config.system.build.toplevel; - regInfo = (pkgs.closureInfo { rootPaths = vmConfig.config.virtualisation.additionalPaths; }); - inherit (config.clan.virtualisation) memorySize cores graphics; - }); + create = pkgs.writeText "vm.json" ( + builtins.toJSON { + initrd = "${vmConfig.config.system.build.initialRamdisk}/${vmConfig.config.system.boot.loader.initrdFile}"; + toplevel = vmConfig.config.system.build.toplevel; + regInfo = (pkgs.closureInfo { rootPaths = vmConfig.config.virtualisation.additionalPaths; }); + inherit (config.clan.virtualisation) memorySize cores graphics; + } + ); }; virtualisation = lib.optionalAttrs (options.virtualisation ? cores) { diff --git a/nixosModules/clanCore/wayland-proxy-virtwl.nix b/nixosModules/clanCore/wayland-proxy-virtwl.nix index d44d4753..0c52fa6a 100644 --- a/nixosModules/clanCore/wayland-proxy-virtwl.nix +++ b/nixosModules/clanCore/wayland-proxy-virtwl.nix @@ -1,4 +1,9 @@ -{ pkgs, config, lib, ... }: +{ + pkgs, + config, + lib, + ... +}: { options = { # maybe upstream this? diff --git a/nixosModules/clanCore/zerotier/default.nix b/nixosModules/clanCore/zerotier/default.nix index 719635aa..495394d2 100644 --- a/nixosModules/clanCore/zerotier/default.nix +++ b/nixosModules/clanCore/zerotier/default.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.clan.networking.zerotier; facts = config.clanCore.secrets.zerotier.facts or { }; @@ -76,16 +81,18 @@ in }; settings = lib.mkOption { description = lib.mdDoc "override the network config in /var/lib/zerotier/bla/$network.json"; - type = lib.types.submodule { - freeformType = (pkgs.formats.json { }).type; - }; + type = lib.types.submodule { freeformType = (pkgs.formats.json { }).type; }; }; }; config = lib.mkMerge [ ({ # Override license so that we can build zerotierone without # having to re-import nixpkgs. - services.zerotierone.package = lib.mkDefault (pkgs.zerotierone.overrideAttrs (_old: { meta = { }; })); + services.zerotierone.package = lib.mkDefault ( + pkgs.zerotierone.overrideAttrs (_old: { + meta = { }; + }) + ); }) (lib.mkIf ((facts.zerotier-ip.value or null) != null) { environment.etc."zerotier/ip".text = facts.zerotier-ip.value; @@ -104,29 +111,33 @@ in systemd.services.zerotierone.serviceConfig.ExecStartPre = [ "+${pkgs.writeShellScript "init-zerotier" '' - cp ${config.clanCore.secrets.zerotier.secrets.zerotier-identity-secret.path} /var/lib/zerotier-one/identity.secret - zerotier-idtool getpublic /var/lib/zerotier-one/identity.secret > /var/lib/zerotier-one/identity.public + cp ${config.clanCore.secrets.zerotier.secrets.zerotier-identity-secret.path} /var/lib/zerotier-one/identity.secret + zerotier-idtool getpublic /var/lib/zerotier-one/identity.secret > /var/lib/zerotier-one/identity.public - ${lib.optionalString (cfg.controller.enable) '' - mkdir -p /var/lib/zerotier-one/controller.d/network - ln -sfT ${pkgs.writeText "net.json" (builtins.toJSON cfg.settings)} /var/lib/zerotier-one/controller.d/network/${cfg.networkId}.json - ''} - ${lib.optionalString (cfg.moon.stableEndpoints != []) '' - if [[ ! -f /var/lib/zerotier-one/moon.json ]]; then - zerotier-idtool initmoon /var/lib/zerotier-one/identity.public > /var/lib/zerotier-one/moon.json - fi - ${genMoonScript}/bin/genmoon /var/lib/zerotier-one/moon.json ${builtins.toFile "moon.json" (builtins.toJSON cfg.moon.stableEndpoints)} /var/lib/zerotier-one/moons.d - ''} + ${lib.optionalString (cfg.controller.enable) '' + mkdir -p /var/lib/zerotier-one/controller.d/network + ln -sfT ${pkgs.writeText "net.json" (builtins.toJSON cfg.settings)} /var/lib/zerotier-one/controller.d/network/${cfg.networkId}.json + ''} + ${lib.optionalString (cfg.moon.stableEndpoints != [ ]) '' + if [[ ! -f /var/lib/zerotier-one/moon.json ]]; then + zerotier-idtool initmoon /var/lib/zerotier-one/identity.public > /var/lib/zerotier-one/moon.json + fi + ${genMoonScript}/bin/genmoon /var/lib/zerotier-one/moon.json ${builtins.toFile "moon.json" (builtins.toJSON cfg.moon.stableEndpoints)} /var/lib/zerotier-one/moons.d + ''} - # cleanup old networks - if [[ -d /var/lib/zerotier-one/networks.d ]]; then - find /var/lib/zerotier-one/networks.d \ - -type f \ - -name "*.conf" \ - -not \( ${lib.concatMapStringsSep " -o " (netId: ''-name "${netId}.conf"'') config.services.zerotierone.joinNetworks} \) \ - -delete - fi - ''}" + # cleanup old networks + if [[ -d /var/lib/zerotier-one/networks.d ]]; then + find /var/lib/zerotier-one/networks.d \ + -type f \ + -name "*.conf" \ + -not \( ${ + lib.concatMapStringsSep " -o " ( + netId: ''-name "${netId}.conf"'' + ) config.services.zerotierone.joinNetworks + } \) \ + -delete + fi + ''}" ]; systemd.services.zerotierone.serviceConfig.ExecStartPost = [ "+${pkgs.writeShellScript "configure-interface" '' @@ -145,7 +156,7 @@ in ${lib.concatMapStringsSep "\n" (moon: '' zerotier-cli orbit ${moon} ${moon} '') cfg.moon.orbitMoons} - ''}" + ''}" ]; networking.firewall.interfaces."zt+".allowedTCPPorts = [ 5353 ]; # mdns @@ -172,7 +183,11 @@ in facts.zerotier-ip = { }; facts.zerotier-network-id = { }; secrets.zerotier-identity-secret = { }; - generator.path = [ config.services.zerotierone.package pkgs.fakeroot pkgs.python3 ]; + generator.path = [ + config.services.zerotierone.package + pkgs.fakeroot + pkgs.python3 + ]; generator.script = '' python3 ${./generate.py} --mode network \ --ip "$facts/zerotier-ip" \ @@ -188,7 +203,10 @@ in clanCore.secrets.zerotier = { facts.zerotier-ip = { }; secrets.zerotier-identity-secret = { }; - generator.path = [ config.services.zerotierone.package pkgs.python3 ]; + generator.path = [ + config.services.zerotierone.package + pkgs.python3 + ]; generator.script = '' python3 ${./generate.py} --mode identity \ --ip "$facts/zerotier-ip" \ @@ -200,9 +218,7 @@ in (lib.mkIf (cfg.controller.enable && (facts.zerotier-network-id.value or null) != null) { clan.networking.zerotier.networkId = facts.zerotier-network-id.value; clan.networking.zerotier.settings = { - authTokens = [ - null - ]; + authTokens = [ null ]; authorizationEndpoint = ""; capabilities = [ ]; clientId = ""; @@ -242,7 +258,9 @@ in environment.etc."zerotier/network-id".text = facts.zerotier-network-id.value; systemd.services.zerotierone.serviceConfig.ExecStartPost = [ "+${pkgs.writeShellScript "whitelist-controller" '' - ${config.clanCore.clanPkgs.zerotier-members}/bin/zerotier-members allow ${builtins.substring 0 10 cfg.networkId} + ${config.clanCore.clanPkgs.zerotier-members}/bin/zerotier-members allow ${ + builtins.substring 0 10 cfg.networkId + } ''}" ]; }) diff --git a/nixosModules/flake-module.nix b/nixosModules/flake-module.nix index 9d75e27e..e6012694 100644 --- a/nixosModules/flake-module.nix +++ b/nixosModules/flake-module.nix @@ -1,4 +1,5 @@ -{ inputs, self, ... }: { +{ inputs, self, ... }: +{ flake.nixosModules = { hidden-ssh-announce.imports = [ ./hidden-ssh-announce.nix ]; installer.imports = [ @@ -10,9 +11,12 @@ inputs.sops-nix.nixosModules.sops ./clanCore ./iso - ({ pkgs, lib, ... }: { - clanCore.clanPkgs = lib.mkDefault self.packages.${pkgs.hostPlatform.system}; - }) + ( + { pkgs, lib, ... }: + { + clanCore.clanPkgs = lib.mkDefault self.packages.${pkgs.hostPlatform.system}; + } + ) ]; }; } diff --git a/nixosModules/hidden-ssh-announce.nix b/nixosModules/hidden-ssh-announce.nix index cca89a77..41ff88c8 100644 --- a/nixosModules/hidden-ssh-announce.nix +++ b/nixosModules/hidden-ssh-announce.nix @@ -1,8 +1,10 @@ -{ config -, lib -, pkgs -, ... -}: { +{ + config, + lib, + pkgs, + ... +}: +{ options.hidden-ssh-announce = { enable = lib.mkEnableOption "hidden-ssh-announce"; script = lib.mkOption { @@ -32,8 +34,14 @@ }; systemd.services.hidden-ssh-announce = { description = "announce hidden ssh"; - after = [ "tor.service" "network-online.target" ]; - wants = [ "tor.service" "network-online.target" ]; + after = [ + "tor.service" + "network-online.target" + ]; + wants = [ + "tor.service" + "network-online.target" + ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { # ${pkgs.tor}/bin/torify diff --git a/nixosModules/installer/default.nix b/nixosModules/installer/default.nix index 94de49c0..842e5072 100644 --- a/nixosModules/installer/default.nix +++ b/nixosModules/installer/default.nix @@ -1,11 +1,11 @@ -{ lib -, pkgs -, modulesPath -, ... -}: { - systemd.tmpfiles.rules = [ - "d /var/shared 0777 root root - -" - ]; +{ + lib, + pkgs, + modulesPath, + ... +}: +{ + systemd.tmpfiles.rules = [ "d /var/shared 0777 root root - -" ]; imports = [ (modulesPath + "/profiles/installation-device.nix") (modulesPath + "/profiles/all-hardware.nix") @@ -21,7 +21,17 @@ enable = true; script = pkgs.writeShellScript "write-hostname" '' set -efu - export PATH=${lib.makeBinPath (with pkgs; [ iproute2 coreutils jq qrencode ])} + export PATH=${ + lib.makeBinPath ( + with pkgs; + [ + iproute2 + coreutils + jq + qrencode + ] + ) + } mkdir -p /var/shared echo "$1" > /var/shared/onion-hostname diff --git a/nixosModules/iso/default.nix b/nixosModules/iso/default.nix index 85f48102..db3afe56 100644 --- a/nixosModules/iso/default.nix +++ b/nixosModules/iso/default.nix @@ -1,4 +1,10 @@ -{ config, extendModules, lib, pkgs, ... }: +{ + config, + extendModules, + lib, + pkgs, + ... +}: let # Generates a fileSystems entry for bind mounting a given state folder path # It binds directories from /var/clanstate/{some-path} to /{some-path}. @@ -13,54 +19,47 @@ let }; # Flatten the list of state folders into a single list - stateFolders = lib.flatten ( - lib.mapAttrsToList - (_item: attrs: attrs.folders) - config.clanCore.state - ); + stateFolders = lib.flatten (lib.mapAttrsToList (_item: attrs: attrs.folders) config.clanCore.state); # A module setting up bind mounts for all state folders stateMounts = { - fileSystems = - lib.listToAttrs - (map mkBindMount stateFolders); + fileSystems = lib.listToAttrs (map mkBindMount stateFolders); }; - isoModule = { config, ... }: { - imports = [ - stateMounts - ]; - options.clan.iso.disko = lib.mkOption { - type = lib.types.submodule { - freeformType = (pkgs.formats.json { }).type; - }; - default = { - disk = { - iso = { - type = "disk"; - imageSize = "10G"; # TODO add auto image size in disko - content = { - type = "gpt"; - partitions = { - boot = { - size = "1M"; - type = "EF02"; # for grub MBR - }; - ESP = { - size = "100M"; - type = "EF00"; - content = { - type = "filesystem"; - format = "vfat"; - mountpoint = "/boot"; + isoModule = + { config, ... }: + { + imports = [ stateMounts ]; + options.clan.iso.disko = lib.mkOption { + type = lib.types.submodule { freeformType = (pkgs.formats.json { }).type; }; + default = { + disk = { + iso = { + type = "disk"; + imageSize = "10G"; # TODO add auto image size in disko + content = { + type = "gpt"; + partitions = { + boot = { + size = "1M"; + type = "EF02"; # for grub MBR }; - }; - root = { - size = "100%"; - content = { - type = "filesystem"; - format = "ext4"; - mountpoint = "/"; + ESP = { + size = "100M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; }; }; }; @@ -68,19 +67,16 @@ let }; }; }; + config = { + disko.devices = lib.mkOverride 51 config.clan.iso.disko; + boot.loader.grub.enable = true; + boot.loader.grub.efiSupport = true; + boot.loader.grub.device = lib.mkForce "/dev/vda"; + boot.loader.grub.efiInstallAsRemovable = true; + }; }; - config = { - disko.devices = lib.mkOverride 51 config.clan.iso.disko; - boot.loader.grub.enable = true; - boot.loader.grub.efiSupport = true; - boot.loader.grub.device = lib.mkForce "/dev/vda"; - boot.loader.grub.efiInstallAsRemovable = true; - }; - }; - isoConfig = extendModules { - modules = [ isoModule ]; - }; + isoConfig = extendModules { modules = [ isoModule ]; }; in { config = { diff --git a/pkgs/clan-cli/default.nix b/pkgs/clan-cli/default.nix index c2361515..fa5252f1 100644 --- a/pkgs/clan-cli/default.nix +++ b/pkgs/clan-cli/default.nix @@ -1,36 +1,37 @@ -{ age -, lib -, argcomplete -, installShellFiles -, nix -, openssh -, pytest -, pytest-cov -, pytest-xdist -, pytest-subprocess -, pytest-timeout -, remote-pdb -, ipdb -, python3 -, runCommand -, setuptools -, sops -, stdenv -, wheel -, fakeroot -, rsync -, bash -, sshpass -, zbar -, tor -, git -, nixpkgs -, qemu -, gnupg -, e2fsprogs -, mypy -, rope -, clan-core-path +{ + age, + lib, + argcomplete, + installShellFiles, + nix, + openssh, + pytest, + pytest-cov, + pytest-xdist, + pytest-subprocess, + pytest-timeout, + remote-pdb, + ipdb, + python3, + runCommand, + setuptools, + sops, + stdenv, + wheel, + fakeroot, + rsync, + bash, + sshpass, + zbar, + tor, + git, + nixpkgs, + qemu, + gnupg, + e2fsprogs, + mypy, + rope, + clan-core-path, }: let @@ -38,19 +39,22 @@ let argcomplete # optional dependency: if not enabled, shell completion will not work ]; - pytestDependencies = runtimeDependencies ++ dependencies ++ [ - pytest - pytest-cov - pytest-subprocess - pytest-xdist - pytest-timeout - remote-pdb - ipdb - openssh - git - gnupg - stdenv.cc - ]; + pytestDependencies = + runtimeDependencies + ++ dependencies + ++ [ + pytest + pytest-cov + pytest-subprocess + pytest-xdist + pytest-timeout + remote-pdb + ipdb + openssh + git + gnupg + stdenv.cc + ]; # Optional dependencies for clan cli, we re-expose them here to make sure they all build. runtimeDependencies = [ @@ -70,7 +74,9 @@ let e2fsprogs ]; - runtimeDependenciesAsSet = builtins.listToAttrs (builtins.map (p: lib.nameValuePair (lib.getName p.name) p) runtimeDependencies); + runtimeDependenciesAsSet = builtins.listToAttrs ( + builtins.map (p: lib.nameValuePair (lib.getName p.name) p) runtimeDependencies + ); checkPython = python3.withPackages (_ps: pytestDependencies); @@ -121,42 +127,48 @@ python3.pkgs.buildPythonApplication { propagatedBuildInputs = dependencies; # also re-expose dependencies so we test them in CI - passthru.tests = (lib.mapAttrs' (n: lib.nameValuePair "clan-dep-${n}") runtimeDependenciesAsSet) // rec { - clan-pytest-without-core = runCommand "clan-pytest-without-core" { nativeBuildInputs = [ checkPython ] ++ pytestDependencies; } '' - cp -r ${source} ./src - chmod +w -R ./src - cd ./src + passthru.tests = + (lib.mapAttrs' (n: lib.nameValuePair "clan-dep-${n}") runtimeDependenciesAsSet) + // rec { + clan-pytest-without-core = + runCommand "clan-pytest-without-core" { nativeBuildInputs = [ checkPython ] ++ pytestDependencies; } + '' + cp -r ${source} ./src + chmod +w -R ./src + cd ./src - export NIX_STATE_DIR=$TMPDIR/nix IN_NIX_SANDBOX=1 - ${checkPython}/bin/python -m pytest -m "not impure and not with_core" ./tests - touch $out - ''; - # separate the tests that can never be cached - clan-pytest-with-core = runCommand "clan-pytest-with-core" { nativeBuildInputs = [ checkPython ] ++ pytestDependencies; } '' - cp -r ${source} ./src - chmod +w -R ./src - cd ./src + export NIX_STATE_DIR=$TMPDIR/nix IN_NIX_SANDBOX=1 + ${checkPython}/bin/python -m pytest -m "not impure and not with_core" ./tests + touch $out + ''; + # separate the tests that can never be cached + clan-pytest-with-core = + runCommand "clan-pytest-with-core" { nativeBuildInputs = [ checkPython ] ++ pytestDependencies; } + '' + cp -r ${source} ./src + chmod +w -R ./src + cd ./src - export CLAN_CORE=${clan-core-path} - export NIX_STATE_DIR=$TMPDIR/nix IN_NIX_SANDBOX=1 - ${checkPython}/bin/python -m pytest -m "not impure and with_core" ./tests - touch $out - ''; + export CLAN_CORE=${clan-core-path} + export NIX_STATE_DIR=$TMPDIR/nix IN_NIX_SANDBOX=1 + ${checkPython}/bin/python -m pytest -m "not impure and with_core" ./tests + touch $out + ''; - clan-pytest = runCommand "clan-pytest" { } '' - echo ${clan-pytest-without-core} - echo ${clan-pytest-with-core} - touch $out - ''; - check-for-breakpoints = runCommand "breakpoints" { } '' - if grep --include \*.py -Rq "breakpoint()" ${source}; then - echo "breakpoint() found in ${source}:" - grep --include \*.py -Rn "breakpoint()" ${source} - exit 1 - fi - touch $out - ''; - }; + clan-pytest = runCommand "clan-pytest" { } '' + echo ${clan-pytest-without-core} + echo ${clan-pytest-with-core} + touch $out + ''; + check-for-breakpoints = runCommand "breakpoints" { } '' + if grep --include \*.py -Rq "breakpoint()" ${source}; then + echo "breakpoint() found in ${source}:" + grep --include \*.py -Rn "breakpoint()" ${source} + exit 1 + fi + touch $out + ''; + }; passthru.nixpkgs = nixpkgs'; passthru.checkPython = checkPython; diff --git a/pkgs/clan-cli/flake-module.nix b/pkgs/clan-cli/flake-module.nix index cd1ae1bb..ba8cb7f0 100644 --- a/pkgs/clan-cli/flake-module.nix +++ b/pkgs/clan-cli/flake-module.nix @@ -1,37 +1,44 @@ -{ inputs, self, lib, ... }: { - perSystem = { self', pkgs, ... }: + inputs, + self, + lib, + ... +}: +{ + perSystem = + { self', pkgs, ... }: let flakeLock = lib.importJSON (self + /flake.lock); flakeInputs = (builtins.removeAttrs inputs [ "self" ]); flakeLockVendoredDeps = flakeLock // { - nodes = flakeLock.nodes // ( - lib.flip lib.mapAttrs flakeInputs (name: _: flakeLock.nodes.${name} // { - locked = { - inherit (flakeLock.nodes.${name}.locked) narHash; - lastModified = - # lol, nixpkgs has a different timestamp on the fs??? - if name == "nixpkgs" - then 0 - else 1; - path = "${inputs.${name}}"; - type = "path"; - }; - }) - ); + nodes = + flakeLock.nodes + // (lib.flip lib.mapAttrs flakeInputs ( + name: _: + flakeLock.nodes.${name} + // { + locked = { + inherit (flakeLock.nodes.${name}.locked) narHash; + lastModified = + # lol, nixpkgs has a different timestamp on the fs??? + if name == "nixpkgs" then 0 else 1; + path = "${inputs.${name}}"; + type = "path"; + }; + } + )); }; - flakeLockFile = builtins.toFile "clan-core-flake.lock" - (builtins.toJSON flakeLockVendoredDeps); - clanCoreWithVendoredDeps = lib.trace flakeLockFile pkgs.runCommand "clan-core-with-vendored-deps" { } '' - cp -r ${self} $out - chmod +w -R $out - cp ${flakeLockFile} $out/flake.lock - ''; + flakeLockFile = builtins.toFile "clan-core-flake.lock" (builtins.toJSON flakeLockVendoredDeps); + clanCoreWithVendoredDeps = + lib.trace flakeLockFile pkgs.runCommand "clan-core-with-vendored-deps" { } + '' + cp -r ${self} $out + chmod +w -R $out + cp ${flakeLockFile} $out/flake.lock + ''; in { - devShells.clan-cli = pkgs.callPackage ./shell.nix { - inherit (self'.packages) clan-cli; - }; + devShells.clan-cli = pkgs.callPackage ./shell.nix { inherit (self'.packages) clan-cli; }; packages = { clan-cli = pkgs.python3.pkgs.callPackage ./default.nix { inherit (inputs) nixpkgs; @@ -42,5 +49,4 @@ checks = self'.packages.clan-cli.tests; }; - } diff --git a/pkgs/clan-cli/shell.nix b/pkgs/clan-cli/shell.nix index ecc33574..e239355c 100644 --- a/pkgs/clan-cli/shell.nix +++ b/pkgs/clan-cli/shell.nix @@ -1,16 +1,20 @@ -{ nix-unit, clan-cli, system, mkShell, writeScriptBin, openssh, ruff, python3 }: +{ + nix-unit, + clan-cli, + system, + mkShell, + writeScriptBin, + openssh, + ruff, + python3, +}: let checkScript = writeScriptBin "check" '' nix build .#checks.${system}.{treefmt,clan-pytest} -L "$@" ''; pythonWithDeps = python3.withPackages ( - ps: - clan-cli.propagatedBuildInputs - ++ clan-cli.devDependencies - ++ [ - ps.pip - ] + ps: clan-cli.propagatedBuildInputs ++ clan-cli.devDependencies ++ [ ps.pip ] ); in mkShell { diff --git a/pkgs/clan-cli/tests/machines/vm1/default.nix b/pkgs/clan-cli/tests/machines/vm1/default.nix index 8ae4d04f..2f0f4cee 100644 --- a/pkgs/clan-cli/tests/machines/vm1/default.nix +++ b/pkgs/clan-cli/tests/machines/vm1/default.nix @@ -1,4 +1,5 @@ -{ lib, ... }: { +{ lib, ... }: +{ clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__"; system.stateVersion = lib.version; sops.age.keyFile = "__CLAN_SOPS_KEY_PATH__"; diff --git a/pkgs/clan-cli/tests/machines/vm_with_secrets/default.nix b/pkgs/clan-cli/tests/machines/vm_with_secrets/default.nix index 8ae4d04f..2f0f4cee 100644 --- a/pkgs/clan-cli/tests/machines/vm_with_secrets/default.nix +++ b/pkgs/clan-cli/tests/machines/vm_with_secrets/default.nix @@ -1,4 +1,5 @@ -{ lib, ... }: { +{ lib, ... }: +{ clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__"; system.stateVersion = lib.version; sops.age.keyFile = "__CLAN_SOPS_KEY_PATH__"; diff --git a/pkgs/clan-cli/tests/machines/vm_without_secrets/default.nix b/pkgs/clan-cli/tests/machines/vm_without_secrets/default.nix index db5b9eed..2d02b56a 100644 --- a/pkgs/clan-cli/tests/machines/vm_without_secrets/default.nix +++ b/pkgs/clan-cli/tests/machines/vm_without_secrets/default.nix @@ -1,4 +1,5 @@ -{ lib, ... }: { +{ lib, ... }: +{ clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__"; system.stateVersion = lib.version; clan.virtualisation.graphics = false; diff --git a/pkgs/clan-cli/tests/test_flake/fake-module.nix b/pkgs/clan-cli/tests/test_flake/fake-module.nix index b3c6580d..d00d0662 100644 --- a/pkgs/clan-cli/tests/test_flake/fake-module.nix +++ b/pkgs/clan-cli/tests/test_flake/fake-module.nix @@ -1,6 +1,5 @@ -{ lib -, ... -}: { +{ lib, ... }: +{ options.clan.fake-module.fake-flag = lib.mkOption { type = lib.types.bool; default = false; diff --git a/pkgs/clan-cli/tests/test_flake/flake.nix b/pkgs/clan-cli/tests/test_flake/flake.nix index 3f335a4e..ec93e145 100644 --- a/pkgs/clan-cli/tests/test_flake/flake.nix +++ b/pkgs/clan-cli/tests/test_flake/flake.nix @@ -2,32 +2,41 @@ # this placeholder is replaced by the path to nixpkgs inputs.nixpkgs.url = "__NIXPKGS__"; - outputs = inputs': + outputs = + inputs': let # fake clan-core input fake-clan-core = { clanModules.fake-module = ./fake-module.nix; }; - inputs = inputs' // { clan-core = fake-clan-core; }; + inputs = inputs' // { + clan-core = fake-clan-core; + }; machineSettings = ( - if builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE" != "" - then builtins.fromJSON (builtins.readFile (builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE")) - else if builtins.pathExists ./machines/machine1/settings.json - then builtins.fromJSON (builtins.readFile ./machines/machine1/settings.json) - else { } + if builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE" != "" then + builtins.fromJSON (builtins.readFile (builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE")) + else if builtins.pathExists ./machines/machine1/settings.json then + builtins.fromJSON (builtins.readFile ./machines/machine1/settings.json) + else + { } + ); + machineImports = map (module: fake-clan-core.clanModules.${module}) ( + machineSettings.clanImports or [ ] ); - machineImports = - map - (module: fake-clan-core.clanModules.${module}) - (machineSettings.clanImports or [ ]); in { nixosConfigurations.machine1 = inputs.nixpkgs.lib.nixosSystem { - modules = - machineImports ++ [ - ./nixosModules/machine1.nix - machineSettings - ({ lib, options, pkgs, ... }: { + modules = machineImports ++ [ + ./nixosModules/machine1.nix + machineSettings + ( + { + lib, + options, + pkgs, + ... + }: + { config = { nixpkgs.hostPlatform = "x86_64-linux"; # speed up by not instantiating nixpkgs twice and disable documentation @@ -51,8 +60,9 @@ The buildClan function will automatically import these modules for the current machine. ''; }; - }) - ]; + } + ) + ]; }; }; } diff --git a/pkgs/clan-cli/tests/test_flake/nixosModules/machine1.nix b/pkgs/clan-cli/tests/test_flake/nixosModules/machine1.nix index 8bf312e3..26371f09 100644 --- a/pkgs/clan-cli/tests/test_flake/nixosModules/machine1.nix +++ b/pkgs/clan-cli/tests/test_flake/nixosModules/machine1.nix @@ -1,4 +1,5 @@ -{ lib, ... }: { +{ lib, ... }: +{ options.clan.jitsi.enable = lib.mkOption { type = lib.types.bool; default = false; diff --git a/pkgs/clan-cli/tests/test_flake_with_core/flake.nix b/pkgs/clan-cli/tests/test_flake_with_core/flake.nix index 3870cb6c..2b1f4d0e 100644 --- a/pkgs/clan-cli/tests/test_flake_with_core/flake.nix +++ b/pkgs/clan-cli/tests/test_flake_with_core/flake.nix @@ -5,40 +5,45 @@ # this placeholder is replaced by the path to nixpkgs inputs.clan-core.url = "__CLAN_CORE__"; - outputs = { self, clan-core }: + outputs = + { self, clan-core }: let clan = clan-core.lib.buildClan { directory = self; clanName = "test_flake_with_core"; machines = { - vm1 = { lib, ... }: { - clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__"; - system.stateVersion = lib.version; - sops.age.keyFile = "__CLAN_SOPS_KEY_PATH__"; - clanCore.secretsUploadDirectory = "__CLAN_SOPS_KEY_DIR__"; - clanCore.sops.defaultGroups = [ "admins" ]; - clan.virtualisation.graphics = false; + vm1 = + { lib, ... }: + { + clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__"; + system.stateVersion = lib.version; + sops.age.keyFile = "__CLAN_SOPS_KEY_PATH__"; + clanCore.secretsUploadDirectory = "__CLAN_SOPS_KEY_DIR__"; + clanCore.sops.defaultGroups = [ "admins" ]; + clan.virtualisation.graphics = false; - clan.networking.zerotier.controller.enable = true; - networking.useDHCP = false; + clan.networking.zerotier.controller.enable = true; + networking.useDHCP = false; - systemd.services.shutdown-after-boot = { - enable = true; - wantedBy = [ "multi-user.target" ]; - after = [ "multi-user.target" ]; - script = '' - #!/usr/bin/env bash - shutdown -h now - ''; + systemd.services.shutdown-after-boot = { + enable = true; + wantedBy = [ "multi-user.target" ]; + after = [ "multi-user.target" ]; + script = '' + #!/usr/bin/env bash + shutdown -h now + ''; + }; + }; + vm2 = + { lib, ... }: + { + clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__"; + system.stateVersion = lib.version; + sops.age.keyFile = "__CLAN_SOPS_KEY_PATH__"; + clanCore.secretsUploadDirectory = "__CLAN_SOPS_KEY_DIR__"; + clan.networking.zerotier.networkId = "82b44b162ec6c013"; }; - }; - vm2 = { lib, ... }: { - clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__"; - system.stateVersion = lib.version; - sops.age.keyFile = "__CLAN_SOPS_KEY_PATH__"; - clanCore.secretsUploadDirectory = "__CLAN_SOPS_KEY_DIR__"; - clan.networking.zerotier.networkId = "82b44b162ec6c013"; - }; }; }; in diff --git a/pkgs/clan-cli/tests/test_flake_with_core_and_pass/flake.nix b/pkgs/clan-cli/tests/test_flake_with_core_and_pass/flake.nix index a7187d54..d9ca403b 100644 --- a/pkgs/clan-cli/tests/test_flake_with_core_and_pass/flake.nix +++ b/pkgs/clan-cli/tests/test_flake_with_core_and_pass/flake.nix @@ -5,30 +5,33 @@ # this placeholder is replaced by the path to clan-core inputs.clan-core.url = "__CLAN_CORE__"; - outputs = { self, clan-core }: + outputs = + { self, clan-core }: let clan = clan-core.lib.buildClan { directory = self; clanName = "test_flake_with_core_and_pass"; machines = { - vm1 = { lib, ... }: { - clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__"; - system.stateVersion = lib.version; - clanCore.secretStore = "password-store"; - clanCore.secretsUploadDirectory = lib.mkForce "__CLAN_SOPS_KEY_DIR__/secrets"; + vm1 = + { lib, ... }: + { + clan.networking.targetHost = "__CLAN_TARGET_ADDRESS__"; + system.stateVersion = lib.version; + clanCore.secretStore = "password-store"; + clanCore.secretsUploadDirectory = lib.mkForce "__CLAN_SOPS_KEY_DIR__/secrets"; - clan.networking.zerotier.controller.enable = true; + clan.networking.zerotier.controller.enable = true; - systemd.services.shutdown-after-boot = { - enable = true; - wantedBy = [ "multi-user.target" ]; - after = [ "multi-user.target" ]; - script = '' - #!/usr/bin/env bash - shutdown -h now - ''; + systemd.services.shutdown-after-boot = { + enable = true; + wantedBy = [ "multi-user.target" ]; + after = [ "multi-user.target" ]; + script = '' + #!/usr/bin/env bash + shutdown -h now + ''; + }; }; - }; }; }; in diff --git a/pkgs/clan-cli/tests/test_flake_with_core_dynamic_machines/flake.nix b/pkgs/clan-cli/tests/test_flake_with_core_dynamic_machines/flake.nix index 01cc5746..e10e244d 100644 --- a/pkgs/clan-cli/tests/test_flake_with_core_dynamic_machines/flake.nix +++ b/pkgs/clan-cli/tests/test_flake_with_core_dynamic_machines/flake.nix @@ -5,7 +5,8 @@ # this placeholder is replaced by the path to nixpkgs inputs.clan-core.url = "__CLAN_CORE__"; - outputs = { self, clan-core }: + outputs = + { self, clan-core }: let clan = clan-core.lib.buildClan { directory = self; @@ -14,9 +15,7 @@ let machineModules = builtins.readDir (self + "/machines"); in - builtins.mapAttrs - (name: _type: import (self + "/machines/${name}")) - machineModules; + builtins.mapAttrs (name: _type: import (self + "/machines/${name}")) machineModules; }; in { diff --git a/pkgs/clan-vm-manager/default.nix b/pkgs/clan-vm-manager/default.nix index 13fb2bbe..45662133 100644 --- a/pkgs/clan-vm-manager/default.nix +++ b/pkgs/clan-vm-manager/default.nix @@ -1,16 +1,17 @@ -{ python3 -, runCommand -, setuptools -, copyDesktopItems -, pygobject3 -, wrapGAppsHook -, gtk4 -, gnome -, pygobject-stubs -, gobject-introspection -, clan-cli -, makeDesktopItem -, libadwaita +{ + python3, + runCommand, + setuptools, + copyDesktopItems, + pygobject3, + wrapGAppsHook, + gtk4, + gnome, + pygobject-stubs, + gobject-introspection, + clan-cli, + makeDesktopItem, + libadwaita, }: let source = ./.; @@ -41,7 +42,11 @@ python3.pkgs.buildPythonApplication { gobject-introspection ]; - buildInputs = [ gtk4 libadwaita gnome.adwaita-icon-theme ]; + buildInputs = [ + gtk4 + libadwaita + gnome.adwaita-icon-theme + ]; # We need to propagate the build inputs to nix fmt / treefmt propagatedBuildInputs = [ @@ -73,7 +78,5 @@ python3.pkgs.buildPythonApplication { checkPhase = '' PYTHONPATH= $out/bin/clan-vm-manager --help ''; - desktopItems = [ - desktop-file - ]; + desktopItems = [ desktop-file ]; } diff --git a/pkgs/clan-vm-manager/flake-module.nix b/pkgs/clan-vm-manager/flake-module.nix index 7264c130..06de8e23 100644 --- a/pkgs/clan-vm-manager/flake-module.nix +++ b/pkgs/clan-vm-manager/flake-module.nix @@ -1,12 +1,15 @@ -{ ... }: { - perSystem = { config, pkgs, ... }: { - devShells.clan-vm-manager = pkgs.callPackage ./shell.nix { - inherit (config.packages) clan-cli clan-vm-manager; - }; - packages.clan-vm-manager = pkgs.python3.pkgs.callPackage ./default.nix { - inherit (config.packages) clan-cli; - }; +{ ... }: +{ + perSystem = + { config, pkgs, ... }: + { + devShells.clan-vm-manager = pkgs.callPackage ./shell.nix { + inherit (config.packages) clan-cli clan-vm-manager; + }; + packages.clan-vm-manager = pkgs.python3.pkgs.callPackage ./default.nix { + inherit (config.packages) clan-cli; + }; - checks = config.packages.clan-vm-manager.tests; - }; + checks = config.packages.clan-vm-manager.tests; + }; } diff --git a/pkgs/clan-vm-manager/shell.nix b/pkgs/clan-vm-manager/shell.nix index 1fc0bddd..36094422 100644 --- a/pkgs/clan-vm-manager/shell.nix +++ b/pkgs/clan-vm-manager/shell.nix @@ -1,11 +1,37 @@ -{ lib, runCommand, makeWrapper, stdenv, clan-vm-manager, gdb, gtk4, libadwaita, clan-cli, mkShell, ruff, desktop-file-utils, xdg-utils, mypy, python3, python3Packages }: +{ + lib, + runCommand, + makeWrapper, + stdenv, + clan-vm-manager, + gdb, + gtk4, + libadwaita, + clan-cli, + mkShell, + ruff, + desktop-file-utils, + xdg-utils, + mypy, + python3, + python3Packages, +}: mkShell ( let - pygdb = runCommand "pygdb" { buildInputs = [ gdb python3 makeWrapper ]; } '' - mkdir -p "$out/bin" - makeWrapper "${gdb}/bin/gdb" "$out/bin/pygdb" \ - --add-flags '-ex "source ${python3}/share/gdb/libpython.py"' - ''; + pygdb = + runCommand "pygdb" + { + buildInputs = [ + gdb + python3 + makeWrapper + ]; + } + '' + mkdir -p "$out/bin" + makeWrapper "${gdb}/bin/gdb" "$out/bin/pygdb" \ + --add-flags '-ex "source ${python3}/share/gdb/libpython.py"' + ''; in { inherit (clan-vm-manager) propagatedBuildInputs buildInputs; @@ -15,7 +41,6 @@ mkShell ( pygdb ]; - # To debug clan-vm-manger execute pygdb --args python ./bin/clan-vm-manager nativeBuildInputs = [ ruff diff --git a/pkgs/flake-module.nix b/pkgs/flake-module.nix index 771b4ee2..d446ed95 100644 --- a/pkgs/flake-module.nix +++ b/pkgs/flake-module.nix @@ -1,38 +1,45 @@ -{ ... }: { +{ ... }: +{ imports = [ ./clan-cli/flake-module.nix ./clan-vm-manager/flake-module.nix ./installer/flake-module.nix ]; - perSystem = { pkgs, config, lib, ... }: { - packages = { - tea-create-pr = pkgs.callPackage ./tea-create-pr { }; - zerotier-members = pkgs.callPackage ./zerotier-members { }; - zt-tcp-relay = pkgs.callPackage ./zt-tcp-relay { }; - merge-after-ci = pkgs.callPackage ./merge-after-ci { - inherit (config.packages) tea-create-pr; - }; - pending-reviews = pkgs.callPackage ./pending-reviews { }; - } // lib.optionalAttrs pkgs.stdenv.isLinux { - wayland-proxy-virtwl = pkgs.callPackage ./wayland-proxy-virtwl { }; - waypipe = pkgs.waypipe.overrideAttrs - (_old: { - # https://gitlab.freedesktop.org/mstoeckl/waypipe - src = pkgs.fetchFromGitLab { - domain = "gitlab.freedesktop.org"; - owner = "mstoeckl"; - repo = "waypipe"; - rev = "4e4ff3bc1943cf7f6aeb56b06c060f40578d3570"; - hash = "sha256-dxz4AmeJAweffyPCayvykworQNntHtHeq6PXMXWsM5k="; - }; - }); - # halalify zerotierone - zerotierone = pkgs.zerotierone.overrideAttrs (_old: { - meta = _old.meta // { - license = lib.licenses.apsl20; + perSystem = + { + pkgs, + config, + lib, + ... + }: + { + packages = + { + tea-create-pr = pkgs.callPackage ./tea-create-pr { }; + zerotier-members = pkgs.callPackage ./zerotier-members { }; + zt-tcp-relay = pkgs.callPackage ./zt-tcp-relay { }; + merge-after-ci = pkgs.callPackage ./merge-after-ci { inherit (config.packages) tea-create-pr; }; + pending-reviews = pkgs.callPackage ./pending-reviews { }; + } + // lib.optionalAttrs pkgs.stdenv.isLinux { + wayland-proxy-virtwl = pkgs.callPackage ./wayland-proxy-virtwl { }; + waypipe = pkgs.waypipe.overrideAttrs (_old: { + # https://gitlab.freedesktop.org/mstoeckl/waypipe + src = pkgs.fetchFromGitLab { + domain = "gitlab.freedesktop.org"; + owner = "mstoeckl"; + repo = "waypipe"; + rev = "4e4ff3bc1943cf7f6aeb56b06c060f40578d3570"; + hash = "sha256-dxz4AmeJAweffyPCayvykworQNntHtHeq6PXMXWsM5k="; + }; + }); + # halalify zerotierone + zerotierone = pkgs.zerotierone.overrideAttrs (_old: { + meta = _old.meta // { + license = lib.licenses.apsl20; + }; + }); }; - }); }; - }; } diff --git a/pkgs/go-ssb/default.nix b/pkgs/go-ssb/default.nix index a650cbea..a974d11e 100644 --- a/pkgs/go-ssb/default.nix +++ b/pkgs/go-ssb/default.nix @@ -1,7 +1,7 @@ -{ lib -, buildGoModule -, fetchFromGitHub -, +{ + lib, + buildGoModule, + fetchFromGitHub, }: buildGoModule rec { pname = "go-ssb"; @@ -17,7 +17,10 @@ buildGoModule rec { vendorHash = "sha256-ZytuWFre7Cz6Qt01tLQoPEuNzDIyoC938OkdIrU8nZo="; - ldflags = [ "-s" "-w" ]; + ldflags = [ + "-s" + "-w" + ]; # take very long doCheck = false; diff --git a/pkgs/installer/flake-module.nix b/pkgs/installer/flake-module.nix index 74ffaec2..fea1d77b 100644 --- a/pkgs/installer/flake-module.nix +++ b/pkgs/installer/flake-module.nix @@ -1,14 +1,16 @@ { self, lib, ... }: let - installerModule = { config, pkgs, ... }: { - imports = [ - self.nixosModules.installer - self.inputs.nixos-generators.nixosModules.all-formats - ]; + installerModule = + { config, pkgs, ... }: + { + imports = [ + self.nixosModules.installer + self.inputs.nixos-generators.nixosModules.all-formats + ]; - system.stateVersion = config.system.nixos.version; - nixpkgs.pkgs = self.inputs.nixpkgs.legacyPackages.x86_64-linux; - }; + system.stateVersion = config.system.nixos.version; + nixpkgs.pkgs = self.inputs.nixpkgs.legacyPackages.x86_64-linux; + }; installer = lib.nixosSystem { modules = [ @@ -27,7 +29,9 @@ in flake.packages.x86_64-linux.install-iso = self.inputs.disko.lib.makeDiskImages { nixosConfig = installer; }; - flake.nixosConfigurations = { inherit (clan.nixosConfigurations) installer; }; + flake.nixosConfigurations = { + inherit (clan.nixosConfigurations) installer; + }; flake.clanInternals = clan.clanInternals; flake.apps.x86_64-linux.install-vm.program = installer.config.formats.vm.outPath; flake.apps.x86_64-linux.install-vm-nogui.program = installer.config.formats.vm-nogui.outPath; diff --git a/pkgs/merge-after-ci/default.nix b/pkgs/merge-after-ci/default.nix index 28195dd8..732c720d 100644 --- a/pkgs/merge-after-ci/default.nix +++ b/pkgs/merge-after-ci/default.nix @@ -1,19 +1,19 @@ -{ bash -, callPackage -, coreutils -, git -, lib -, nix -, openssh -, tea -, tea-create-pr -, ... +{ + bash, + callPackage, + coreutils, + git, + lib, + nix, + openssh, + tea, + tea-create-pr, + ... }: let writers = callPackage ../builders/script-writers.nix { }; in -writers.writePython3Bin "merge-after-ci" -{ +writers.writePython3Bin "merge-after-ci" { makeWrapperArgs = [ "--prefix" "PATH" @@ -28,6 +28,4 @@ writers.writePython3Bin "merge-after-ci" tea-create-pr ]) ]; -} - ./merge-after-ci.py - +} ./merge-after-ci.py diff --git a/pkgs/pending-reviews/default.nix b/pkgs/pending-reviews/default.nix index dee60317..7aa4cafe 100644 --- a/pkgs/pending-reviews/default.nix +++ b/pkgs/pending-reviews/default.nix @@ -1,6 +1,7 @@ -{ writeShellApplication -, bash -, curl +{ + writeShellApplication, + bash, + curl, }: writeShellApplication { name = "pending-reviews"; diff --git a/pkgs/tea-create-pr/default.nix b/pkgs/tea-create-pr/default.nix index 15ac802a..a22b9a28 100644 --- a/pkgs/tea-create-pr/default.nix +++ b/pkgs/tea-create-pr/default.nix @@ -1,9 +1,10 @@ -{ writeShellApplication -, bash -, coreutils -, git -, tea -, openssh +{ + writeShellApplication, + bash, + coreutils, + git, + tea, + openssh, }: writeShellApplication { name = "tea-create-pr"; diff --git a/pkgs/wayland-proxy-virtwl/default.nix b/pkgs/wayland-proxy-virtwl/default.nix index b4303cd2..0f2f9863 100644 --- a/pkgs/wayland-proxy-virtwl/default.nix +++ b/pkgs/wayland-proxy-virtwl/default.nix @@ -1,4 +1,9 @@ -{ wayland-proxy-virtwl, fetchFromGitHub, libdrm, ocaml-ng }: +{ + wayland-proxy-virtwl, + fetchFromGitHub, + libdrm, + ocaml-ng, +}: let ocaml-wayland = ocaml-ng.ocamlPackages_5_0.wayland.overrideAttrs (_old: { src = fetchFromGitHub { @@ -16,13 +21,15 @@ wayland-proxy-virtwl.overrideAttrs (_old: { rev = "652fca9d4e006a2bdeba920dfaf53190c5373a7d"; hash = "sha256-VgpqxjHgueK9eQSX987PF0KvscpzkScOzFkW3haYCOw="; }; - buildInputs = [ libdrm ] ++ (with ocaml-ng.ocamlPackages_5_0; [ - ocaml-wayland - dune-configurator - eio_main - ppx_cstruct - cmdliner - logs - ppx_cstruct - ]); + buildInputs = + [ libdrm ] + ++ (with ocaml-ng.ocamlPackages_5_0; [ + ocaml-wayland + dune-configurator + eio_main + ppx_cstruct + cmdliner + logs + ppx_cstruct + ]); }) diff --git a/pkgs/zerotier-members/default.nix b/pkgs/zerotier-members/default.nix index 450b0793..55015914 100644 --- a/pkgs/zerotier-members/default.nix +++ b/pkgs/zerotier-members/default.nix @@ -1,4 +1,8 @@ -{ stdenv, python3, lib }: +{ + stdenv, + python3, + lib, +}: stdenv.mkDerivation { name = "zerotier-members"; diff --git a/pkgs/zt-tcp-relay/default.nix b/pkgs/zt-tcp-relay/default.nix index 3bdd313e..1dd6aa36 100644 --- a/pkgs/zt-tcp-relay/default.nix +++ b/pkgs/zt-tcp-relay/default.nix @@ -1,6 +1,7 @@ -{ lib -, rustPlatform -, fetchFromGitHub +{ + lib, + rustPlatform, + fetchFromGitHub, }: rustPlatform.buildRustPackage { diff --git a/templates/flake-module.nix b/templates/flake-module.nix index d44882f3..b8fe8e9f 100644 --- a/templates/flake-module.nix +++ b/templates/flake-module.nix @@ -1,4 +1,5 @@ -{ self, ... }: { +{ self, ... }: +{ flake.templates = { new-clan = { description = "Initialize a new clan flake"; diff --git a/templates/new-clan/flake.nix b/templates/new-clan/flake.nix index 38acce83..2f34b17c 100644 --- a/templates/new-clan/flake.nix +++ b/templates/new-clan/flake.nix @@ -3,7 +3,8 @@ inputs.clan-core.url = "git+https://git.clan.lol/clan/clan-core"; - outputs = { self, clan-core, ... }: + outputs = + { self, clan-core, ... }: let system = "x86_64-linux"; pkgs = clan-core.inputs.nixpkgs.legacyPackages.${system}; @@ -17,9 +18,7 @@ inherit (clan) nixosConfigurations clanInternals; # add the cLAN cli tool to the dev shell devShells.${system}.default = pkgs.mkShell { - packages = [ - clan-core.packages.${system}.clan-cli - ]; + packages = [ clan-core.packages.${system}.clan-cli ]; }; }; } From dd0ad2683b2d8617b376d8b37a5edb99822681b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Mar 2024 21:32:23 +0100 Subject: [PATCH 59/63] drop secret store logging from install command --- pkgs/clan-cli/clan_cli/machines/install.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pkgs/clan-cli/clan_cli/machines/install.py b/pkgs/clan-cli/clan_cli/machines/install.py index 82566adf..8f9cbb97 100644 --- a/pkgs/clan-cli/clan_cli/machines/install.py +++ b/pkgs/clan-cli/clan_cli/machines/install.py @@ -16,7 +16,6 @@ log = logging.getLogger(__name__) def install_nixos(machine: Machine, kexec: str | None = None) -> None: secrets_module = importlib.import_module(machine.secrets_module) log.info(f"installing {machine.name}") - log.info(f"using secret store: {secrets_module.SecretStore}") secret_store = secrets_module.SecretStore(machine=machine) h = machine.target_host From 9f25f472982b72c1d17371c54832b2b7f1c3e86b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Mar 2024 21:35:44 +0100 Subject: [PATCH 60/63] allow to debug nixos-anywhere --- pkgs/clan-cli/clan_cli/machines/install.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/machines/install.py b/pkgs/clan-cli/clan_cli/machines/install.py index 8f9cbb97..73682bb7 100644 --- a/pkgs/clan-cli/clan_cli/machines/install.py +++ b/pkgs/clan-cli/clan_cli/machines/install.py @@ -13,7 +13,9 @@ from ..secrets.generate import generate_secrets log = logging.getLogger(__name__) -def install_nixos(machine: Machine, kexec: str | None = None) -> None: +def install_nixos( + machine: Machine, kexec: str | None = None, debug: bool = False +) -> None: secrets_module = importlib.import_module(machine.secrets_module) log.info(f"installing {machine.name}") secret_store = secrets_module.SecretStore(machine=machine) @@ -45,6 +47,8 @@ def install_nixos(machine: Machine, kexec: str | None = None) -> None: ] if kexec: cmd += ["--kexec", kexec] + if debug: + cmd.append("--debug") cmd.append(target_host) run( @@ -63,6 +67,7 @@ class InstallOptions: target_host: str kexec: str | None confirm: bool + debug: bool def install_command(args: argparse.Namespace) -> None: @@ -72,6 +77,7 @@ def install_command(args: argparse.Namespace) -> None: target_host=args.target_host, kexec=args.kexec, confirm=not args.yes, + debug=args.debug, ) machine = Machine(opts.machine, flake=opts.flake) machine.target_host_address = opts.target_host @@ -81,7 +87,7 @@ def install_command(args: argparse.Namespace) -> None: if ask != "y": return - install_nixos(machine, kexec=opts.kexec) + install_nixos(machine, kexec=opts.kexec, debug=opts.debug) def register_install_parser(parser: argparse.ArgumentParser) -> None: @@ -96,6 +102,12 @@ def register_install_parser(parser: argparse.ArgumentParser) -> None: help="do not ask for confirmation", default=False, ) + parser.add_argument( + "--debug", + action="store_true", + help="print debug information", + default=False, + ) parser.add_argument( "machine", type=str, From 5ff36a2cd892e92711742246336482e7d2d6f044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sun, 17 Mar 2024 21:43:01 +0100 Subject: [PATCH 61/63] nixos-install: also respect port --- pkgs/clan-cli/clan_cli/machines/install.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkgs/clan-cli/clan_cli/machines/install.py b/pkgs/clan-cli/clan_cli/machines/install.py index 73682bb7..44b64dcf 100644 --- a/pkgs/clan-cli/clan_cli/machines/install.py +++ b/pkgs/clan-cli/clan_cli/machines/install.py @@ -45,6 +45,8 @@ def install_nixos( "--extra-files", str(tmpdir), ] + if machine.target_host.port: + cmd += ["--ssh-port", str(machine.target_host.port)] if kexec: cmd += ["--kexec", kexec] if debug: From 9300ecbfe2fd5b2dd0563223e14cf7b3de991f71 Mon Sep 17 00:00:00 2001 From: Clan Merge Bot Date: Mon, 18 Mar 2024 00:00:14 +0000 Subject: [PATCH 62/63] update flake lock - 2024-03-18T00:00+00:00 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'disko': 'github:nix-community/disko/fe064a639319ed61cdf12b8f6eded9523abcc498' (2024-03-11) → 'github:nix-community/disko/21d89b333ca300bef82c928c856d48b94a9f997c' (2024-03-14) • Updated input 'nixos-generators': 'github:nix-community/nixos-generators/1d9c8cd24eba7942955f92fdcefba5a6a7543bc6' (2024-03-11) → 'github:nix-community/nixos-generators/efd4e38532b5abfaa5c9fc95c5a913157dc20ccb' (2024-03-14) • Updated input 'nixos-generators/nixlib': 'github:nix-community/nixpkgs.lib/7873d84a89ae6e4841528ff7f5697ddcb5bdfe6c' (2024-03-03) → 'github:nix-community/nixpkgs.lib/630ebdc047ca96d8126e16bb664c7730dc52f6e6' (2024-03-10) • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/e4e2121b151e492fd15d4bdb034e793738fdc120' (2024-03-12) → 'github:NixOS/nixpkgs/f471be9644f3ab2f3cb868de1787ab70a537b0e7' (2024-03-17) • Updated input 'sops-nix': 'github:Mic92/sops-nix/e52d8117b330f690382f1d16d81ae43daeb4b880' (2024-03-11) → 'github:Mic92/sops-nix/83b68a0e8c94b72cdd0a6e547a14ca7eb1c03616' (2024-03-17) • Updated input 'treefmt-nix': 'github:numtide/treefmt-nix/720322c5352d7b7bd2cb3601a9176b0e91d1de7d' (2024-03-10) → 'github:numtide/treefmt-nix/35791f76524086ab4b785a33e4abbedfda64bd22' (2024-03-12) --- flake.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/flake.lock b/flake.lock index 1c25ea82..773c4c17 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1710169806, - "narHash": "sha256-HeWFrRuHpnAiPmIr26OKl2g142HuGerwoO/XtW53pcI=", + "lastModified": 1710427903, + "narHash": "sha256-sV0Q5ndvfjK9JfCg/QM/HX/fcittohvtq8dD62isxdM=", "owner": "nix-community", "repo": "disko", - "rev": "fe064a639319ed61cdf12b8f6eded9523abcc498", + "rev": "21d89b333ca300bef82c928c856d48b94a9f997c", "type": "github" }, "original": { @@ -42,11 +42,11 @@ }, "nixlib": { "locked": { - "lastModified": 1709426687, - "narHash": "sha256-jLBZmwXf0WYHzLkmEMq33bqhX55YtT5edvluFr0RcSA=", + "lastModified": 1710031547, + "narHash": "sha256-pkUg3hOKuGWMGF9WEMPPN/G4pqqdbNGJQ54yhyQYDVY=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "7873d84a89ae6e4841528ff7f5697ddcb5bdfe6c", + "rev": "630ebdc047ca96d8126e16bb664c7730dc52f6e6", "type": "github" }, "original": { @@ -63,11 +63,11 @@ ] }, "locked": { - "lastModified": 1710164763, - "narHash": "sha256-6p7yebSjzrL8qK4Q0gx2RnsxaudGUQcgkSxFG/J265Y=", + "lastModified": 1710398463, + "narHash": "sha256-fQlYanU84E8uwBpcoTCcLCwU8cqn0eQ7nwTcrWfSngc=", "owner": "nix-community", "repo": "nixos-generators", - "rev": "1d9c8cd24eba7942955f92fdcefba5a6a7543bc6", + "rev": "efd4e38532b5abfaa5c9fc95c5a913157dc20ccb", "type": "github" }, "original": { @@ -78,11 +78,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1710213926, - "narHash": "sha256-D6wdwb289veivPoRV5/+IZaUG/XrdJPHpbR08cA5og0=", + "lastModified": 1710672219, + "narHash": "sha256-Bp3Jsq1Jn8q4EesBlcOVNwnEipNpzYs73kvR3+3EUC4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e4e2121b151e492fd15d4bdb034e793738fdc120", + "rev": "f471be9644f3ab2f3cb868de1787ab70a537b0e7", "type": "github" }, "original": { @@ -110,11 +110,11 @@ "nixpkgs-stable": [] }, "locked": { - "lastModified": 1710195194, - "narHash": "sha256-KFxCJp0T6TJOz1IOKlpRdpsCr9xsvlVuWY/VCiAFnTE=", + "lastModified": 1710644594, + "narHash": "sha256-RquCuzxfy4Nr8DPbdp3D/AsbYep21JgQzG8aMH9jJ4A=", "owner": "Mic92", "repo": "sops-nix", - "rev": "e52d8117b330f690382f1d16d81ae43daeb4b880", + "rev": "83b68a0e8c94b72cdd0a6e547a14ca7eb1c03616", "type": "github" }, "original": { @@ -130,11 +130,11 @@ ] }, "locked": { - "lastModified": 1710088047, - "narHash": "sha256-eSqKs6ZCsX9xJyNYLeMDMrxzIDsYtaWClfZCOp0ok6Y=", + "lastModified": 1710278050, + "narHash": "sha256-Oc6BP7soXqb8itlHI8UKkdf3V9GeJpa1S39SR5+HJys=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "720322c5352d7b7bd2cb3601a9176b0e91d1de7d", + "rev": "35791f76524086ab4b785a33e4abbedfda64bd22", "type": "github" }, "original": { From 580010581c0beb9eef50131e8cdaf01e9ca95f08 Mon Sep 17 00:00:00 2001 From: DavHau Date: Tue, 19 Mar 2024 13:00:59 +0700 Subject: [PATCH 63/63] devshell: remove dependency on clan-cli derivation The devShell depended on clan-cli due to it being included as a dependency in the treefmt config. This is not optimal because this makes the devshell rebuild unnecessary often and also lead to build failures of the dev-shell if the clan-cli code is in a broken state (git rebasing, or during development etc.) --- devShell-python.nix | 8 +++----- formatter.nix | 3 ++- pkgs/clan-vm-manager/default.nix | 12 +++++++++--- pkgs/clan-vm-manager/pyproject.toml | 4 ++++ 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/devShell-python.nix b/devShell-python.nix index 149cdffc..066c4473 100644 --- a/devShell-python.nix +++ b/devShell-python.nix @@ -15,11 +15,9 @@ ps: clan-cli.propagatedBuildInputs ++ clan-cli.devDependencies - ++ [ - ps.pip - # clan-vm-manager deps - ps.pygobject3 - ] + ++ [ ps.pip ] + ++ [ clan-vm-manager.externalPythonDeps ] + # clan-vm-manager deps ); linuxOnlyPackages = lib.optionals pkgs.stdenv.isLinux [ pkgs.xdg-utils ]; in diff --git a/formatter.nix b/formatter.nix index f035b487..4aeacde3 100644 --- a/formatter.nix +++ b/formatter.nix @@ -10,7 +10,8 @@ treefmt.programs.mypy.enable = true; treefmt.programs.mypy.directories = { "pkgs/clan-cli".extraPythonPackages = self'.packages.clan-cli.pytestDependencies; - "pkgs/clan-vm-manager".extraPythonPackages = self'.packages.clan-vm-manager.propagatedBuildInputs; + "pkgs/clan-vm-manager".extraPythonPackages = + self'.packages.clan-vm-manager.externalPythonDeps ++ self'.packages.clan-cli.pytestDependencies; }; treefmt.settings.formatter.nix = { diff --git a/pkgs/clan-vm-manager/default.nix b/pkgs/clan-vm-manager/default.nix index 45662133..71ab3d60 100644 --- a/pkgs/clan-vm-manager/default.nix +++ b/pkgs/clan-vm-manager/default.nix @@ -24,7 +24,7 @@ let mimeTypes = [ "x-scheme-handler/clan" ]; }; in -python3.pkgs.buildPythonApplication { +python3.pkgs.buildPythonApplication rec { name = "clan-vm-manager"; src = source; format = "pyproject"; @@ -51,13 +51,19 @@ python3.pkgs.buildPythonApplication { # We need to propagate the build inputs to nix fmt / treefmt propagatedBuildInputs = [ (python3.pkgs.toPythonModule clan-cli) - pygobject3 - pygobject-stubs + passthru.externalPythonDeps ]; # also re-expose dependencies so we test them in CI passthru = { inherit desktop-file; + # Keep external dependencies in a separate lists to refer to thm elsewhere + # This helps avoiding issues like dev-shells accidentally depending on + # nix derivations of local packages. + externalPythonDeps = [ + pygobject3 + pygobject-stubs + ]; tests = { clan-vm-manager-no-breakpoints = runCommand "clan-vm-manager-no-breakpoints" { } '' if grep --include \*.py -Rq "breakpoint()" ${source}; then diff --git a/pkgs/clan-vm-manager/pyproject.toml b/pkgs/clan-vm-manager/pyproject.toml index b2061a55..8016c21e 100644 --- a/pkgs/clan-vm-manager/pyproject.toml +++ b/pkgs/clan-vm-manager/pyproject.toml @@ -22,6 +22,10 @@ disallow_untyped_calls = true disallow_untyped_defs = true no_implicit_optional = true +[[tool.mypy.overrides]] +module = "clan_cli.*" +ignore_missing_imports = true + [tool.ruff] target-version = "py311" line-length = 88