Merge pull request 'nixosModules/clanCore: support nix-darwin' (#3287) from nix-darwin into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3287
This commit is contained in:
Michael Hoang
2025-04-22 13:50:38 +00:00
20 changed files with 233 additions and 157 deletions

View File

@@ -1,6 +1,7 @@
{ pkgs, lib }: { pkgs, lib }:
let let
eval = lib.evalModules { eval = lib.evalModules {
class = "nixos";
modules = [ modules = [
./interface.nix ./interface.nix
]; ];

View File

@@ -9,30 +9,24 @@
... ...
}: }:
{ {
imports = [ imports = builtins.filter builtins.pathExists (
{ [
imports = builtins.filter builtins.pathExists ( "${directory}/machines/${name}/configuration.nix"
[ ]
"${directory}/machines/${name}/configuration.nix" ++ lib.optionals (_class == "nixos") [
] "${directory}/machines/${name}/hardware-configuration.nix"
++ lib.optionals (_class == "nixos") [ "${directory}/machines/${name}/disko.nix"
"${directory}/machines/${name}/hardware-configuration.nix" ]
"${directory}/machines/${name}/disko.nix" );
]
); clan.core.settings = {
} inherit (meta) name icon;
(lib.optionalAttrs (_class == "nixos") { inherit directory;
clan.core.settings = { machine = {
inherit (meta) name icon; inherit name;
inherit directory; };
machine = { };
inherit name;
}; # TODO: move into nixosModules
}; networking.hostName = lib.mkDefault name;
})
# TODO: move into nixos modules
({
networking.hostName = lib.mkDefault name;
})
];
} }

View File

@@ -221,17 +221,11 @@ in
# machine specifics # machine specifics
machines = configsPerSystem; machines = configsPerSystem;
all-machines-json = all-machines-json = lib.mapAttrs (
if !lib.hasAttrByPath [ "darwinModules" "clanCore" ] clan-core then system: configs:
lib.mapAttrs ( nixpkgs.legacyPackages.${system}.writers.writeJSON "machines.json" (
system: configs: lib.mapAttrs (_: m: m.config.system.clan.deployment.data) configs
nixpkgs.legacyPackages.${system}.writers.writeJSON "machines.json" ( )
lib.mapAttrs (_: m: m.config.system.clan.deployment.data) ( ) configsPerSystem;
lib.filterAttrs (_n: v: v.class == "nixos") configs
)
)
) configsPerSystem
else
throw "remove NixOS filter and support nix-darwin as well";
}; };
} }

View File

@@ -28,6 +28,7 @@ let
}: }:
let let
evaled = lib.evalModules { evaled = lib.evalModules {
class = "nixos";
modules = [ modules = [
(baseModule { inherit pkgs; }) (baseModule { inherit pkgs; })
{ {
@@ -83,6 +84,7 @@ let
name = role; name = role;
value = value =
(lib.evalModules { (lib.evalModules {
class = "nixos";
modules = [ modules = [
(baseModule { inherit pkgs; }) (baseModule { inherit pkgs; })
clan-core.nixosModules.clanCore clan-core.nixosModules.clanCore

View File

@@ -1,23 +1,26 @@
{ _class, lib, ... }:
{ {
imports = [ imports =
./backups.nix [
./defaults.nix ./backups.nix
./facts ./defaults.nix
./inventory ./facts
./meta/interface.nix ./inventory
./metadata.nix ./meta/interface.nix
./networking.nix ./metadata.nix
./nixos-facter.nix ./networking.nix
./nix-settings.nix ./nix-settings.nix
./options.nix ./options.nix
./outputs.nix ./outputs.nix
./schema.nix ./schema.nix
./sops.nix ./sops.nix
./vars ./vars
./vm.nix ]
./wayland-proxy-virtwl.nix ++ lib.optionals (_class == "nixos") [
./zerotier ./nixos-facter.nix
./zfs.nix ./vm.nix
]; ./wayland-proxy-virtwl.nix
./zerotier
./zfs.nix
];
} }

View File

@@ -1,10 +1,27 @@
{ {
_class,
lib, lib,
config, config,
pkgs, pkgs,
... ...
}: }:
{ {
imports = lib.optional (_class == "nixos") (
lib.mkIf config.clan.core.enableRecommendedDefaults {
# Use systemd during boot as well except:
# - systems with raids as this currently require manual configuration: https://github.com/NixOS/nixpkgs/issues/210210
# - for containers we currently rely on the `stage-2` init script that sets up our /etc
boot.initrd.systemd.enable = lib.mkDefault (!config.boot.swraid.enable && !config.boot.isContainer);
# Don't install the /lib/ld-linux.so.2 stub. This saves one instance of nixpkgs.
environment.ldso32 = null;
environment.systemPackages = [
pkgs.nixos-facter # for `clan machines update-hardware-config --backend nixos-facter`
];
}
);
options.clan.core.enableRecommendedDefaults = lib.mkOption { options.clan.core.enableRecommendedDefaults = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
description = '' description = ''
@@ -20,11 +37,6 @@
}; };
config = lib.mkIf config.clan.core.enableRecommendedDefaults { config = lib.mkIf config.clan.core.enableRecommendedDefaults {
# Use systemd during boot as well except:
# - systems with raids as this currently require manual configuration: https://github.com/NixOS/nixpkgs/issues/210210
# - for containers we currently rely on the `stage-2` init script that sets up our /etc
boot.initrd.systemd.enable = lib.mkDefault (!config.boot.swraid.enable && !config.boot.isContainer);
# This disables the HTML manual and `nixos-help` command but leaves # This disables the HTML manual and `nixos-help` command but leaves
# `man configuration.nix` # `man configuration.nix`
documentation.doc.enable = lib.mkDefault false; documentation.doc.enable = lib.mkDefault false;
@@ -32,9 +44,6 @@
# Work around for https://github.com/NixOS/nixpkgs/issues/124215 # Work around for https://github.com/NixOS/nixpkgs/issues/124215
documentation.info.enable = lib.mkDefault false; documentation.info.enable = lib.mkDefault false;
# Don't install the /lib/ld-linux.so.2 stub. This saves one instance of nixpkgs.
environment.ldso32 = null;
environment.systemPackages = [ environment.systemPackages = [
# essential debugging tools for networked services # essential debugging tools for networked services
pkgs.dnsutils pkgs.dnsutils
@@ -43,8 +52,6 @@
pkgs.jq pkgs.jq
pkgs.htop pkgs.htop
pkgs.nixos-facter # for `clan machines update-hardware-config --backend nixos-facter`
pkgs.gitMinimal pkgs.gitMinimal
]; ];
}; };

View File

@@ -1,4 +1,9 @@
{ config, lib, ... }: {
_class,
config,
lib,
...
}:
{ {
options.clan.core = { options.clan.core = {
networking = { networking = {
@@ -96,23 +101,25 @@
] ]
) )
]; ];
config = lib.mkIf config.clan.core.enableRecommendedDefaults { config = lib.optionalAttrs (_class == "nixos") (
# conflicts with systemd-resolved lib.mkIf config.clan.core.enableRecommendedDefaults {
networking.useHostResolvConf = false; # conflicts with systemd-resolved
networking.useHostResolvConf = false;
# Allow PMTU / DHCP # Allow PMTU / DHCP
networking.firewall.allowPing = true; networking.firewall.allowPing = true;
# The notion of "online" is a broken concept # The notion of "online" is a broken concept
# https://github.com/systemd/systemd/blob/e1b45a756f71deac8c1aa9a008bd0dab47f64777/NEWS#L13 # https://github.com/systemd/systemd/blob/e1b45a756f71deac8c1aa9a008bd0dab47f64777/NEWS#L13
systemd.services.NetworkManager-wait-online.enable = false; systemd.services.NetworkManager-wait-online.enable = false;
systemd.network.wait-online.enable = false; systemd.network.wait-online.enable = false;
systemd.network.networks."99-ethernet-default-dhcp".networkConfig.MulticastDNS = lib.mkDefault true; systemd.network.networks."99-ethernet-default-dhcp".networkConfig.MulticastDNS = lib.mkDefault true;
systemd.network.networks."99-wireless-client-dhcp".networkConfig.MulticastDNS = lib.mkDefault true; systemd.network.networks."99-wireless-client-dhcp".networkConfig.MulticastDNS = lib.mkDefault true;
networking.firewall.allowedUDPPorts = [ 5353 ]; # Multicast DNS networking.firewall.allowedUDPPorts = [ 5353 ]; # Multicast DNS
# Use networkd instead of the pile of shell scripts # Use networkd instead of the pile of shell scripts
networking.useNetworkd = lib.mkDefault true; networking.useNetworkd = lib.mkDefault true;
}; }
);
} }

View File

@@ -1,27 +1,38 @@
{ lib, config, ... }: {
_class,
lib,
config,
...
}:
# Taken from: # Taken from:
# https://github.com/nix-community/srvos/blob/main/nixos/common/nix.nix # https://github.com/nix-community/srvos/blob/main/nixos/common/nix.nix
lib.mkIf config.clan.core.enableRecommendedDefaults { {
# Fallback quickly if substituters are not available. imports = lib.optional (_class == "nixos") (
nix.settings.connect-timeout = 5; lib.mkIf config.clan.core.enableRecommendedDefaults {
nix.daemonCPUSchedPolicy = lib.mkDefault "batch";
nix.daemonIOSchedClass = lib.mkDefault "idle";
nix.daemonIOSchedPriority = lib.mkDefault 7;
}
);
# Enable flakes config = lib.mkIf config.clan.core.enableRecommendedDefaults {
nix.settings.experimental-features = [ # Fallback quickly if substituters are not available.
"nix-command" nix.settings.connect-timeout = 5;
"flakes"
];
# The default at 10 is rarely enough. # Enable flakes
nix.settings.log-lines = lib.mkDefault 25; nix.settings.experimental-features = [
"nix-command"
"flakes"
];
# Avoid disk full issues # The default at 10 is rarely enough.
nix.settings.max-free = lib.mkDefault (3000 * 1024 * 1024); nix.settings.log-lines = lib.mkDefault 25;
nix.settings.min-free = lib.mkDefault (512 * 1024 * 1024);
nix.daemonCPUSchedPolicy = lib.mkDefault "batch"; # Avoid disk full issues
nix.daemonIOSchedClass = lib.mkDefault "idle"; nix.settings.max-free = lib.mkDefault (3000 * 1024 * 1024);
nix.daemonIOSchedPriority = lib.mkDefault 7; nix.settings.min-free = lib.mkDefault (512 * 1024 * 1024);
# Avoid copying unnecessary stuff over SSH # Avoid copying unnecessary stuff over SSH
nix.settings.builders-use-substitutes = true; nix.settings.builders-use-substitutes = true;
};
} }

View File

@@ -6,6 +6,7 @@ let
hwConfig = "${directory}/machines/${machineName}/hardware-configuration.nix"; hwConfig = "${directory}/machines/${machineName}/hardware-configuration.nix";
in in
{ {
_class = "nixos";
facter.reportPath = lib.mkIf (builtins.pathExists facterJson) facterJson; facter.reportPath = lib.mkIf (builtins.pathExists facterJson) facterJson;
warnings = warnings =
lib.optionals lib.optionals

View File

@@ -1,4 +1,5 @@
{ {
_class,
lib, lib,
config, config,
pkgs, pkgs,
@@ -9,18 +10,22 @@ let
submodule = submodule =
module: module:
submoduleWith { submoduleWith {
class = _class;
specialArgs.pkgs = pkgs; specialArgs.pkgs = pkgs;
modules = [ module ]; modules = [ module ];
}; };
in in
{ {
imports = [ imports =
./public/in_repo.nix [
./secret/fs.nix ./public/in_repo.nix
./secret/password-store.nix ./secret/fs.nix
./secret/sops ./secret/sops
./secret/vm.nix ./secret/vm.nix
]; ]
++ lib.optionals (_class == "nixos") [
./secret/password-store.nix
];
options.clan.core.vars = lib.mkOption { options.clan.core.vars = lib.mkOption {
description = '' description = ''
Generated Variables Generated Variables

View File

@@ -1,4 +1,5 @@
{ {
_class,
lib, lib,
config, config,
pkgs, pkgs,
@@ -271,7 +272,8 @@ in
}; };
group = lib.mkOption { group = lib.mkOption {
description = "The group name or id that will own the file."; description = "The group name or id that will own the file.";
default = "root"; default = if _class == "darwin" then "wheel" else "root";
defaultText = lib.literalExpression ''if _class == "darwin" then "wheel" else "root"'';
}; };
mode = lib.mkOption { mode = lib.mkOption {
type = lib.types.strMatching "^[0-7]{3}$"; type = lib.types.strMatching "^[0-7]{3}$";

View File

@@ -52,6 +52,8 @@ let
in in
{ {
_class = "nixos";
options.clan.vars.password-store = { options.clan.vars.password-store = {
secretLocation = lib.mkOption { secretLocation = lib.mkOption {
type = lib.types.path; type = lib.types.path;

View File

@@ -122,6 +122,8 @@ let
vmConfig = extendModules { modules = [ vmModule ]; }; vmConfig = extendModules { modules = [ vmModule ]; };
in in
{ {
_class = "nixos";
options = { options = {
clan.virtualisation = { clan.virtualisation = {
cores = lib.mkOption { cores = lib.mkOption {

View File

@@ -5,6 +5,7 @@
... ...
}: }:
{ {
_class = "nixos";
options = { options = {
# maybe upstream this? # maybe upstream this?
services.wayland-proxy-virtwl = { services.wayland-proxy-virtwl = {

View File

@@ -1,5 +1,7 @@
{ lib, config, ... }: { lib, config, ... }:
{ {
_class = "nixos";
# Use the same default hostID as the NixOS install ISO and nixos-anywhere. # Use the same default hostID as the NixOS install ISO and nixos-anywhere.
# This allows us to import zfs pool without using a force import. # This allows us to import zfs pool without using a force import.
# ZFS has this as a safety mechanism for networked block storage (ISCSI), but # ZFS has this as a safety mechanism for networked block storage (ISCSI), but

View File

@@ -1,25 +1,37 @@
{ inputs, self, ... }: { inputs, self, ... }:
let
clanCore =
{
_class,
pkgs,
lib,
...
}:
{
imports =
[
./clanCore
inputs.sops-nix."${_class}Modules".sops
]
++ lib.optionals (_class == "nixos") [
inputs.nixos-facter-modules.nixosModules.facter
inputs.disko.nixosModules.default
inputs.data-mesher.nixosModules.data-mesher
];
config = {
clan.core.clanPkgs = lib.mkDefault self.packages.${pkgs.hostPlatform.system};
};
};
in
{ {
flake.nixosModules = { flake.nixosModules.hidden-ssh-announce = ./hidden-ssh-announce.nix;
hidden-ssh-announce.imports = [ ./hidden-ssh-announce.nix ]; flake.nixosModules.bcachefs = ./bcachefs.nix;
bcachefs.imports = [ ./bcachefs.nix ]; flake.nixosModules.installer.imports = [
installer.imports = [ ./installer
./installer self.nixosModules.hidden-ssh-announce
self.nixosModules.hidden-ssh-announce self.nixosModules.bcachefs
self.nixosModules.bcachefs ];
];
clanCore.imports = [ flake.nixosModules.clanCore = clanCore;
inputs.sops-nix.nixosModules.sops flake.darwinModules.clanCore = clanCore;
inputs.nixos-facter-modules.nixosModules.facter
inputs.disko.nixosModules.default
inputs.data-mesher.nixosModules.data-mesher
./clanCore
(
{ pkgs, lib, ... }:
{
clan.core.clanPkgs = lib.mkDefault self.packages.${pkgs.hostPlatform.system};
}
)
];
};
} }

View File

@@ -7,11 +7,12 @@ from functools import cached_property
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal from typing import TYPE_CHECKING, Any, Literal
from clan_cli.cmd import Log, RunOpts, run_no_stdout
from clan_cli.errors import ClanCmdError, ClanError from clan_cli.errors import ClanCmdError, ClanError
from clan_cli.facts import public_modules as facts_public_modules from clan_cli.facts import public_modules as facts_public_modules
from clan_cli.facts import secret_modules as facts_secret_modules from clan_cli.facts import secret_modules as facts_secret_modules
from clan_cli.flake import Flake from clan_cli.flake import Flake
from clan_cli.nix import nix_config, nix_test_store from clan_cli.nix import nix_config, nix_eval, nix_test_store
from clan_cli.ssh.host import Host from clan_cli.ssh.host import Host
from clan_cli.ssh.host_key import HostKeyCheck from clan_cli.ssh.host_key import HostKeyCheck
from clan_cli.ssh.parse import parse_deployment_address from clan_cli.ssh.parse import parse_deployment_address
@@ -196,6 +197,27 @@ class Machine:
meta={"machine": self, "target_host": self.target_host}, meta={"machine": self, "target_host": self.target_host},
) )
@cached_property
def deploy_as_root(self) -> bool:
if self._class_ == "nixos":
return True
# Currently nix-darwin HEAD requires you to deploy as a non-root user
# however there is a soon to be merged PR that requires deployment
# as root to match NixOS: https://github.com/nix-darwin/nix-darwin/pull/1341
return json.loads(
run_no_stdout(
nix_eval(
[
f"{self.flake}#darwinConfigurations.{self.name}.options.system",
"--apply",
"system: system ? primaryUser",
]
),
RunOpts(log=Log.NONE),
).stdout.strip()
)
def nix( def nix(
self, self,
attr: str, attr: str,

View File

@@ -138,7 +138,6 @@ def deploy_machines(machines: list[Machine]) -> None:
nix_options = [ nix_options = [
"--show-trace", "--show-trace",
"--fast",
"--option", "--option",
"keep-going", "keep-going",
"true", "true",
@@ -146,30 +145,37 @@ def deploy_machines(machines: list[Machine]) -> None:
"accept-flake-config", "accept-flake-config",
"true", "true",
"-L", "-L",
"--build-host",
"",
*machine.nix_options, *machine.nix_options,
"--flake", "--flake",
f"{path}#{machine.name}", f"{path}#{machine.name}",
] ]
switch_cmd = ["nixos-rebuild", "switch", *nix_options] become_root = machine.deploy_as_root
test_cmd = ["nixos-rebuild", "test", *nix_options]
target_host: Host | None = host.meta.get("target_host") if machine._class_ == "nixos":
if target_host: nix_options += [
switch_cmd.extend(["--target-host", target_host.target]) "--fast",
test_cmd.extend(["--target-host", target_host.target]) "--build-host",
"",
]
if (target_host and target_host.user != "root") or host.user != "root": target_host: Host | None = host.meta.get("target_host")
switch_cmd.extend(["--use-remote-sudo"]) if target_host:
test_cmd.extend(["--use-remote-sudo"]) become_root = False
nix_options += ["--target-host", target_host.target]
if target_host.user != "root":
nix_options += ["--use-remote-sudo"]
switch_cmd = [f"{machine._class_}-rebuild", "switch", *nix_options]
test_cmd = [f"{machine._class_}-rebuild", "test", *nix_options]
env = host.nix_ssh_env(None) env = host.nix_ssh_env(None)
ret = host.run( ret = host.run(
switch_cmd, switch_cmd,
RunOpts(check=False, msg_color=MsgColor(stderr=AnsiColor.DEFAULT)), RunOpts(check=False, msg_color=MsgColor(stderr=AnsiColor.DEFAULT)),
extra_env=env, extra_env=env,
become_root=become_root,
) )
# Last output line (config store path) is printed to stdout instead of stderr # Last output line (config store path) is printed to stdout instead of stderr
@@ -192,11 +198,16 @@ def deploy_machines(machines: list[Machine]) -> None:
test_cmd if is_mobile else switch_cmd, test_cmd if is_mobile else switch_cmd,
RunOpts(msg_color=MsgColor(stderr=AnsiColor.DEFAULT)), RunOpts(msg_color=MsgColor(stderr=AnsiColor.DEFAULT)),
extra_env=env, extra_env=env,
become_root=True, become_root=become_root,
) )
with AsyncRuntime() as runtime: with AsyncRuntime() as runtime:
for machine in machines: for machine in machines:
if machine._class_ == "darwin":
if not machine.deploy_as_root and machine.target_host.user == "root":
msg = f"'TargetHost' should be set to a non-root user for deploying to nix-darwin on machine '{machine.name}'"
raise ClanError(msg)
machine.info(f"Updating {machine.name}") machine.info(f"Updating {machine.name}")
runtime.async_run( runtime.async_run(
AsyncOpts( AsyncOpts(
@@ -231,8 +242,6 @@ def update_command(args: argparse.Namespace) -> None:
if len(args.machines) == 0: if len(args.machines) == 0:
ignored_machines = [] ignored_machines = []
for machine in get_all_machines(args.flake, args.option): for machine in get_all_machines(args.flake, args.option):
if machine._class_ == "darwin":
continue
if machine.deployment.get("requireExplicitUpdate", False): if machine.deployment.get("requireExplicitUpdate", False):
continue continue
try: try:
@@ -260,11 +269,6 @@ def update_command(args: argparse.Namespace) -> None:
machine.override_build_host = args.build_host machine.override_build_host = args.build_host
machine.host_key_check = HostKeyCheck.from_str(args.host_key_check) machine.host_key_check = HostKeyCheck.from_str(args.host_key_check)
for machine in machines:
if machine._class_ == "darwin":
machine.error("Updating macOS machines is not yet supported")
sys.exit(1)
config = nix_config() config = nix_config()
system = config["system"] system = config["system"]
machine_names = [machine.name for machine in machines] machine_names = [machine.name for machine in machines]

View File

@@ -38,13 +38,15 @@ class Host:
def __post_init__(self) -> None: def __post_init__(self) -> None:
if not self.command_prefix: if not self.command_prefix:
self.command_prefix = self.host self.command_prefix = self.host
if not self.user:
self.user = "root"
def __str__(self) -> str: def __str__(self) -> str:
return self.target return self.target
@property @property
def target(self) -> str: def target(self) -> str:
return f"{self.user or 'root'}@{self.host}" return f"{self.user}@{self.host}"
@classmethod @classmethod
def from_host(cls, host: "Host") -> "Host": def from_host(cls, host: "Host") -> "Host":

View File

@@ -114,7 +114,9 @@ def test_parse_deployment_address(
assert result.host == expected_host assert result.host == expected_host
assert result.port == expected_port assert result.port == expected_port
assert result.user == expected_user assert result.user == expected_user or (
expected_user == "" and result.user == "root"
)
assert result.ssh_options == expected_options assert result.ssh_options == expected_options