diff --git a/.gitignore b/.gitignore index 75dbd7889..ba5b4a4a2 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,5 @@ pkgs/clan-app/ui/.fonts *.gif *.mp4 *.mkv + +.jj diff --git a/clanServices/zerotier/default.nix b/clanServices/zerotier/default.nix index 04e396b89..7c65f4a76 100644 --- a/clanServices/zerotier/default.nix +++ b/clanServices/zerotier/default.nix @@ -8,8 +8,25 @@ roles.peer = { perInstance = - { instanceName, roles, ... }: { + instanceName, + roles, + lib, + ... + }: + { + exports.networking = { + priority = lib.mkDefault 900; + # TODO add user space network support to clan-cli + module = "clan_lib.network.zerotier"; + peers = lib.mapAttrs (name: _machine: { + host.var = { + machine = name; + generator = "zerotier"; + file = "zerotier-ip"; + }; + }) roles.peer.machines; + }; nixosModule = { config, diff --git a/pkgs/clan-cli/clan_cli/vars/get.py b/pkgs/clan-cli/clan_cli/vars/get.py index c04e9cbe0..361a7b90e 100644 --- a/pkgs/clan-cli/clan_cli/vars/get.py +++ b/pkgs/clan-cli/clan_cli/vars/get.py @@ -29,10 +29,10 @@ def get_machine_var(machine: Machine, var_id: str) -> Var: if var.id.startswith(var_id): results.append(var) if len(results) == 0: - msg = f"No var found for search string: {var_id}" + msg = f"Couldn't find var: {var_id} for machine: {machine}" raise ClanError(msg) if len(results) > 1: - error = f"Found multiple vars for {var_id}:\n - " + "\n - ".join( + error = f"Found multiple vars in {machine} for {var_id}:\n - " + "\n - ".join( [str(var) for var in results], ) raise ClanError(error) diff --git a/pkgs/clan-cli/clan_lib/network/zerotier/__init__.py b/pkgs/clan-cli/clan_lib/network/zerotier/__init__.py new file mode 100644 index 000000000..2bdf8e352 --- /dev/null +++ b/pkgs/clan-cli/clan_lib/network/zerotier/__init__.py @@ -0,0 +1,43 @@ +import logging +import time +from collections.abc import Iterator +from contextlib import contextmanager +from dataclasses import dataclass + +from clan_lib.errors import ClanError +from clan_lib.network import Network, NetworkTechnologyBase, Peer +from clan_lib.network.zerotier.lib import check_zerotier_running +from clan_lib.ssh.remote import Remote + +log = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class NetworkTechnology(NetworkTechnologyBase): + def is_running(self) -> bool: + return check_zerotier_running() + + def ping(self, remote: Remote) -> None | float: + if self.is_running(): + try: + # Use the existing SSH reachability check + now = time.time() + remote.check_machine_ssh_reachable() + + return (time.time() - now) * 1000 + + except ClanError as e: + log.debug(f"Error checking peer {remote}: {e}") + return None + return None + + @contextmanager + def connection(self, network: Network) -> Iterator[Network]: + # TODO: Implement userspace ZeroTier service start/stop + yield network + + def remote(self, peer: Peer) -> "Remote": + return Remote( + address=peer.host, + command_prefix=peer.name, + ) diff --git a/pkgs/clan-cli/clan_lib/network/zerotier/lib.py b/pkgs/clan-cli/clan_lib/network/zerotier/lib.py new file mode 100644 index 000000000..c94653e5b --- /dev/null +++ b/pkgs/clan-cli/clan_lib/network/zerotier/lib.py @@ -0,0 +1,27 @@ +import contextlib +import json +import urllib.request +from typing import TypedDict + + +class ZeroTierConfig(TypedDict): + clock: int + online: bool + version: str + versionBuild: int + versionMajor: int + versionMinor: int + versionRev: int + + +def get_zerotier_health() -> ZeroTierConfig: + # Placeholder for actual ZeroTier running check + res = urllib.request.urlopen("http://localhost:9993/health") + return json.load(res) + + +def check_zerotier_running() -> bool: + with contextlib.suppress(urllib.error.URLError): + get_zerotier_health() + return True + return False