Merge pull request 'import nixos-facter by default' (#2178) from nixos-facter into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/2178
This commit is contained in:
Mic92
2024-10-09 10:43:17 +00:00
37 changed files with 246 additions and 888 deletions

View File

@@ -10,7 +10,6 @@
{ {
pkgs, pkgs,
lib, lib,
config,
... ...
}: }:
let let
@@ -30,6 +29,7 @@
clan.core.networking.targetHost = "machine"; clan.core.networking.targetHost = "machine";
networking.hostName = "machine"; networking.hostName = "machine";
services.openssh.settings.UseDns = false; services.openssh.settings.UseDns = false;
nixpkgs.hostPlatform = "x86_64-linux";
programs.ssh.knownHosts = { programs.ssh.knownHosts = {
machine.hostNames = [ "machine" ]; machine.hostNames = [ "machine" ];

View File

@@ -20,13 +20,13 @@
environment.etc."install-successful".text = "ok"; environment.etc."install-successful".text = "ok";
nixpkgs.hostPlatform = "x86_64-linux";
boot.consoleLogLevel = lib.mkForce 100; boot.consoleLogLevel = lib.mkForce 100;
boot.kernelParams = [ "boot.shell_on_fail" ]; boot.kernelParams = [ "boot.shell_on_fail" ];
}; };
}; };
perSystem = perSystem =
{ {
nodes,
pkgs, pkgs,
lib, lib,
... ...

View File

@@ -18,6 +18,7 @@ in
# Dummy file system # Dummy file system
fileSystems."/".device = "/dev/null"; fileSystems."/".device = "/dev/null";
boot.loader.grub.device = "/dev/null"; boot.loader.grub.device = "/dev/null";
nixpkgs.hostPlatform = "x86_64-linux";
imports = [ imports = [
documentationModule documentationModule
]; ];

View File

@@ -12,8 +12,7 @@
}, },
"description": "A nice thing", "description": "A nice thing",
"icon": "./path/to/icon.png", "icon": "./path/to/icon.png",
"tags": ["1", "2", "3"], "tags": ["1", "2", "3"]
"system": "x86_64-linux"
} }
}, },
"services": { "services": {

View File

@@ -26,36 +26,10 @@ let
} }
); );
machineSettings =
machineName:
let
warn = lib.warn ''
The use of ./machines/<machine>/settings.json is deprecated.
If your settings.json is empty, you can safely remove it.
!!! Consider using the inventory system. !!!
File: ${directory + /machines/${machineName}/settings.json}
If there are still features missing in the inventory system, please open an issue on the clan-core repository.
'';
in
# 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
warn (builtins.fromJSON (builtins.readFile (builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE")))
else
lib.optionalAttrs (builtins.pathExists "${directory}/machines/${machineName}/settings.json") (
warn (builtins.fromJSON (builtins.readFile (directory + /machines/${machineName}/settings.json)))
);
machineImports =
machineSettings: map (module: clan-core.clanModules.${module}) (machineSettings.clanImports or [ ]);
# TODO: remove default system once we have a hardware-config mechanism # TODO: remove default system once we have a hardware-config mechanism
nixosConfiguration = nixosConfiguration =
{ {
system ? "x86_64-linux", system ? null,
name, name,
pkgs ? null, pkgs ? null,
extraConfig ? { }, extraConfig ? { },
@@ -63,40 +37,16 @@ let
nixpkgs.lib.nixosSystem { nixpkgs.lib.nixosSystem {
modules = modules =
let let
settings = machineSettings name;
facterJson = "${directory}/machines/${name}/facter.json";
hwConfig = "${directory}/machines/${name}/hardware-configuration.nix"; hwConfig = "${directory}/machines/${name}/hardware-configuration.nix";
facterModules = lib.optionals (builtins.pathExists facterJson) [
clan-core.inputs.nixos-facter-modules.nixosModules.facter
{ config.facter.reportPath = facterJson; }
];
in in
(machineImports settings) [
++ facterModules
++ [
{ {
# Autoinclude configuration.nix and hardware-configuration.nix # Autoinclude configuration.nix and hardware-configuration.nix
imports = builtins.filter builtins.pathExists [ imports = builtins.filter builtins.pathExists [
"${directory}/machines/${name}/configuration.nix" "${directory}/machines/${name}/configuration.nix"
hwConfig hwConfig
]; ];
config.warnings =
lib.optionals
(builtins.all builtins.pathExists [
hwConfig
facterJson
])
[
''
Duplicate hardware facts: '${hwConfig}' and '${facterJson}' exist.
Using both is not recommended.
It is recommended to use the hardware facts from '${facterJson}', please remove '${hwConfig}'.
''
];
} }
settings
clan-core.nixosModules.clanCore clan-core.nixosModules.clanCore
extraConfig extraConfig
(machines.${name} or { }) (machines.${name} or { })
@@ -115,7 +65,7 @@ let
# Machine specific settings # Machine specific settings
clan.core.machineName = name; clan.core.machineName = name;
networking.hostName = lib.mkDefault name; networking.hostName = lib.mkDefault name;
nixpkgs.hostPlatform = lib.mkDefault system; nixpkgs.hostPlatform = lib.mkIf (system != null) (lib.mkDefault system);
# speeds up nix commands by using the nixpkgs from the host system (especially useful in VMs) # speeds up nix commands by using the nixpkgs from the host system (especially useful in VMs)
nix.registry.nixpkgs.to = { nix.registry.nixpkgs.to = {
@@ -132,38 +82,7 @@ let
} // specialArgs; } // specialArgs;
}; };
# TODO: Will be deprecated allMachines = inventory.machines or { } // machines;
# We must migrate the tests, that create a settings.json to add a machine.
##################################################
testMachines =
lib.mapAttrs
(name: _: {
inherit name;
system = (machineSettings name).nixpkgs.hostSystem or null;
})
(
lib.filterAttrs (
machineName: _:
if builtins.pathExists "${directory}/machines/${machineName}/settings.json" then
lib.warn ''
The use of ./machines/<machine>/settings.json is deprecated.
If your settings.json is empty, you can safely remove it.
!!! Consider using the inventory system. !!!
File: ${directory + /machines/${machineName}/settings.json}
If there are still features missing in the inventory system, please open an issue on the clan-core repository.
'' true
else
false
) machinesDirs
);
machinesDirs = lib.optionalAttrs (builtins.pathExists "${directory}/machines") (
builtins.readDir (directory + /machines)
);
##################################################
allMachines = inventory.machines or { } // machines // testMachines;
supportedSystems = [ supportedSystems = [
"x86_64-linux" "x86_64-linux"

View File

@@ -146,6 +146,7 @@ in
{ foo, ... }: { foo, ... }:
{ {
networking.hostName = foo; networking.hostName = foo;
nixpkgs.hostPlatform = "x86_64-linux";
}; };
}; };
in in

View File

@@ -154,9 +154,6 @@ let
) [ ] inventory.services ) [ ] inventory.services
# Append each machine config # Append each machine config
++ [ ++ [
(lib.optionalAttrs (machineConfig.system or null != null) {
config.nixpkgs.hostPlatform = machineConfig.system;
})
(lib.optionalAttrs (machineConfig.deploy.targetHost or null != null) { (lib.optionalAttrs (machineConfig.deploy.targetHost or null != null) {
config.clan.core.networking.targetHost = machineConfig.deploy.targetHost; config.clan.core.networking.targetHost = machineConfig.deploy.targetHost;
}) })

View File

@@ -142,10 +142,6 @@ in
apply = lib.unique; apply = lib.unique;
type = types.listOf types.str; type = types.listOf types.str;
}; };
system = lib.mkOption {
default = null;
type = types.nullOr types.str;
};
deploy.targetHost = lib.mkOption { deploy.targetHost = lib.mkOption {
description = "Configuration for the deployment of the machine"; description = "Configuration for the deployment of the machine";
default = null; default = null;

View File

@@ -92,9 +92,9 @@ in
not_used_machine = builtins.length configs.not_used_machine; not_used_machine = builtins.length configs.not_used_machine;
}; };
expected = { expected = {
client_1_machine = 6; client_1_machine = 5;
client_2_machine = 6; client_2_machine = 5;
not_used_machine = 3; not_used_machine = 2;
}; };
}; };

View File

@@ -0,0 +1,4 @@
{
"version": 1,
"system": "x86_64-linux"
}

View File

@@ -9,6 +9,7 @@
./meta/interface.nix ./meta/interface.nix
./metadata.nix ./metadata.nix
./networking.nix ./networking.nix
./nixos-facter.nix
./nix-settings.nix ./nix-settings.nix
./options.nix ./options.nix
./outputs.nix ./outputs.nix

View File

@@ -0,0 +1,24 @@
{ lib, config, ... }:
let
directory = config.clan.core.clanDir;
inherit (config.clan.core) machineName;
facterJson = "${directory}/machines/${machineName}/facter.json";
hwConfig = "${directory}/machines/${machineName}/hardware-configuration.nix";
in
{
facter.reportPath = lib.mkIf (builtins.pathExists facterJson) facterJson;
warnings =
lib.optionals
(builtins.all builtins.pathExists [
hwConfig
facterJson
])
[
''
Duplicate hardware facts: '${hwConfig}' and '${facterJson}' exist.
Using both is not recommended.
It is recommended to use the hardware facts from '${facterJson}', please remove '${hwConfig}'.
''
];
}

View File

@@ -10,6 +10,7 @@
]; ];
clanCore.imports = [ clanCore.imports = [
inputs.sops-nix.nixosModules.sops inputs.sops-nix.nixosModules.sops
inputs.nixos-facter-modules.nixosModules.facter
inputs.disko.nixosModules.default inputs.disko.nixosModules.default
./clanCore ./clanCore
( (

View File

@@ -1,306 +0,0 @@
# !/usr/bin/env python3
import argparse
import json
import logging
import re
from pathlib import Path
from typing import Any, get_origin
from clan_cli.cmd import run
from clan_cli.dirs import machine_settings_file
from clan_cli.errors import ClanError
from clan_cli.git import commit_file
from clan_cli.nix import nix_eval
script_dir = Path(__file__).parent
log = logging.getLogger(__name__)
# nixos option type description to python type
def map_type(nix_type: str) -> Any:
if nix_type == "boolean":
return bool
if nix_type in [
"integer",
"signed integer",
"16 bit unsigned integer; between 0 and 65535 (both inclusive)",
]:
return int
if nix_type.startswith("string"):
return str
if nix_type.startswith("null or "):
subtype = nix_type.removeprefix("null or ")
return map_type(subtype) | None
if nix_type.startswith("attribute set of"):
subtype = nix_type.removeprefix("attribute set of ")
return dict[str, map_type(subtype)] # type: ignore
if nix_type.startswith("list of"):
subtype = nix_type.removeprefix("list of ")
return list[map_type(subtype)] # type: ignore
msg = f"Unknown type {nix_type}"
raise ClanError(msg)
# merge two dicts recursively
def merge(a: dict, b: dict, path: list[str] | None = None) -> dict:
a = a.copy()
if path is None:
path = []
for key in b:
if key in a:
if isinstance(a[key], dict) and isinstance(b[key], dict):
merge(a[key], b[key], [*path, str(key)])
elif isinstance(a[key], list) and isinstance(b[key], list):
a[key].extend(b[key])
elif a[key] != b[key]:
a[key] = b[key]
else:
a[key] = b[key]
return a
# A container inheriting from list, but overriding __contains__ to return True
# for all values.
# This is used to allow any value for the "choices" field of argparse
class AllContainer(list):
def __contains__(self, item: Any) -> bool:
return True
# value is always a list, as the arg parser cannot know the type upfront
# and therefore always allows multiple arguments.
def cast(value: Any, input_type: Any, opt_description: str) -> Any:
try:
# handle bools
if isinstance(input_type, bool):
if value[0] in ["true", "True", "yes", "y", "1"]:
return True
if value[0] in ["false", "False", "no", "n", "0"]:
return False
msg = f"Invalid value {value} for boolean"
raise ClanError(msg)
# handle lists
if get_origin(input_type) is list:
subtype = input_type.__args__[0]
return [cast([x], subtype, opt_description) for x in value]
# handle dicts
if get_origin(input_type) is dict:
if not isinstance(value, dict):
msg = f"Cannot set {opt_description} directly. Specify a suboption like {opt_description}.<name>"
raise ClanError(msg)
subtype = input_type.__args__[1]
return {k: cast(v, subtype, opt_description) for k, v in value.items()}
if str(input_type) == "str | None":
if value[0] in ["null", "None"]:
return None
return value[0]
if len(value) > 1:
msg = f"Too many values for {opt_description}"
raise ClanError(msg)
return input_type(value[0])
except ValueError as e:
msg = f"Invalid type for option {opt_description} (expected {input_type.__name__})"
raise ClanError(msg) from e
def options_for_machine(
flake_dir: Path, machine_name: str, show_trace: bool = False
) -> dict:
clan_dir = flake_dir
flags = []
if show_trace:
flags.append("--show-trace")
flags.append(
f"{clan_dir}#nixosConfigurations.{machine_name}.config.clan.core.optionsNix"
)
cmd = nix_eval(flags=flags)
proc = run(
cmd,
error_msg=f"Failed to read options for machine {machine_name}",
)
return json.loads(proc.stdout)
def read_machine_option_value(
flake_dir: Path, machine_name: str, option: str, show_trace: bool = False
) -> str:
clan_dir = flake_dir
# use nix eval to read from .#nixosConfigurations.default.config.{option}
# this will give us the evaluated config with the options attribute
cmd = nix_eval(
flags=[
"--show-trace",
f"{clan_dir}#nixosConfigurations.{machine_name}.config.{option}",
],
)
proc = run(cmd, error_msg=f"Failed to read option {option}")
value = json.loads(proc.stdout)
# print the value so that the output can be copied and fed as an input.
# for example a list should be displayed as space separated values surrounded by quotes.
if isinstance(value, list):
out = " ".join([json.dumps(x) for x in value])
elif isinstance(value, dict):
out = json.dumps(value, indent=2)
else:
out = json.dumps(value, indent=2)
return out
def get_option(args: argparse.Namespace) -> None:
print(
read_machine_option_value(
args.flake, args.machine, args.option, args.show_trace
)
)
# Currently writing is disabled
def get_or_set_option(args: argparse.Namespace) -> None:
if args.value == []:
print(
read_machine_option_value(
args.flake, args.machine, args.option, args.show_trace
)
)
else:
# load options
if args.options_file is None:
options = options_for_machine(
args.flake, machine_name=args.machine, show_trace=args.show_trace
)
else:
with args.options_file.open() as f:
options = json.load(f)
# compute settings json file location
if args.settings_file is None:
settings_file = machine_settings_file(args.flake.path, args.machine)
else:
settings_file = args.settings_file
# set the option with the given value
set_option(
flake_dir=args.flake.path,
option=args.option,
value=args.value,
options=options,
settings_file=settings_file,
option_description=args.option,
show_trace=args.show_trace,
)
if not args.quiet:
new_value = read_machine_option_value(args.flake, args.machine, args.option)
print(f"New Value for {args.option}:")
print(new_value)
def find_option(
option: str, value: Any, options: dict, option_description: str | None = None
) -> tuple[str, Any]:
"""
The option path specified by the user doesn't have to match exactly to an
entry in the options.json file. Examples
Example 1:
$ clan config services.openssh.settings.SomeSetting 42
This is a freeform option that does not appear in the options.json
The actual option is `services.openssh.settings`
And the value must be wrapped: {"SomeSettings": 42}
Example 2:
$ clan config users.users.my-user.name my-name
The actual option is `users.users.<name>.name`
"""
# option description is used for error messages
if option_description is None:
option_description = option
option_path = option.split(".")
# fuzzy search the option paths, so when
# specified option path: "foo.bar.baz.bum"
# available option path: "foo.<name>.baz.<name>"
# we can still find the option
first = option_path[0]
regex = rf"({first}|<name>)"
for elem in option_path[1:]:
regex += rf"\.({elem}|<name>)"
for opt in options:
if re.match(regex, opt):
return opt, value
# if the regex search did not find the option, start stripping the last
# element of the option path and find matching parent option
# (see examples above for why this is needed)
if len(option_path) == 1:
msg = f"Option {option_description} not found"
raise ClanError(msg)
option_path_parent = option_path[:-1]
attr_prefix = option_path[-1]
return find_option(
option=".".join(option_path_parent),
value={attr_prefix: value},
options=options,
option_description=option_description,
)
def set_option(
flake_dir: Path,
option: str,
value: Any,
options: dict,
settings_file: Path,
option_description: str = "",
show_trace: bool = False,
) -> None:
option_path_orig = option.split(".")
# returns for example:
# option: "users.users.<name>.name"
# value: "my-name"
option, value = find_option(
option=option,
value=value,
options=options,
option_description=option_description,
)
option_path = option.split(".")
option_path_store = option_path_orig[: len(option_path)]
target_type = map_type(options[option]["type"])
casted = cast(value, target_type, option)
# construct a nested dict from the option path and set the value
result: dict[str, Any] = {}
current = result
for part in option_path_store[:-1]:
current[part] = {}
current = current[part]
current[option_path_store[-1]] = value
current[option_path_store[-1]] = casted
# check if there is an existing config file
if settings_file.exists():
with settings_file.open() as f:
current_config = json.load(f)
else:
current_config = {}
# merge and save the new config file
new_config = merge(current_config, result)
settings_file.parent.mkdir(parents=True, exist_ok=True)
with settings_file.open("w") as f:
json.dump(new_config, f, indent=2)
print(file=f) # add newline at the end of the file to make git happy
if settings_file.resolve().is_relative_to(flake_dir):
commit_file(
settings_file,
repo_dir=flake_dir,
commit_message=f"Set option {option_description}",
)

View File

@@ -1,109 +0,0 @@
import json
import os
import re
from pathlib import Path
from tempfile import NamedTemporaryFile
from clan_cli.cmd import Log, run
from clan_cli.dirs import machine_settings_file, nixpkgs_source, specific_machine_dir
from clan_cli.errors import ClanError, ClanHttpError
from clan_cli.git import commit_file
from clan_cli.nix import nix_eval
def verify_machine_config(
flake_dir: Path,
machine_name: str,
config: dict | None = None,
) -> str | None:
"""
Verify that the machine evaluates successfully
Returns None, in case of success, or a String containing the error_message
"""
if config is None:
config = config_for_machine(flake_dir, machine_name)
flake = flake_dir
with NamedTemporaryFile(mode="w", dir=flake) as clan_machine_settings_file:
json.dump(config, clan_machine_settings_file, indent=2)
clan_machine_settings_file.seek(0)
env = os.environ.copy()
env["CLAN_MACHINE_SETTINGS_FILE"] = clan_machine_settings_file.name
cmd = nix_eval(
flags=[
"--show-trace",
"--impure", # needed to access CLAN_MACHINE_SETTINGS_FILE
"--expr",
f"""
let
# hardcoding system for now, not sure where to get it from
system = "x86_64-linux";
flake = builtins.getFlake (toString {flake});
clan-core = flake.inputs.clan-core;
nixpkgsSrc = flake.inputs.nixpkgs or {nixpkgs_source()};
lib = import (nixpkgsSrc + /lib);
pkgs = import nixpkgsSrc {{ inherit system; }};
config = lib.importJSON (builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE");
fakeMachine = pkgs.nixos {{
imports =
[
clan-core.nixosModules.clanCore
# potentially the config might affect submodule options,
# therefore we need to import it
config
{{clan.core.clanDir = {flake};}}
]
# add all clan modules specified via clanImports
++ (map (name: clan-core.clanModules.${{name}}) config.clanImports or []);
}};
in
fakeMachine.config.system.build.vm.outPath
""",
],
)
proc = run(
cmd,
cwd=flake,
env=env,
log=Log.BOTH,
)
if proc.returncode != 0:
return proc.stderr
return None
def config_for_machine(flake_dir: Path, machine_name: str) -> dict:
# read the config from a json file located at {flake}/machines/{machine_name}/settings.json
if not specific_machine_dir(flake_dir, machine_name).exists():
raise ClanHttpError(
msg=f"Machine {machine_name} not found. Create the machine first`",
status_code=404,
)
settings_path = machine_settings_file(flake_dir, machine_name)
if not settings_path.exists():
return {}
with settings_path.open() as f:
return json.load(f)
def set_config_for_machine(flake_dir: Path, machine_name: str, config: dict) -> None:
hostname_regex = r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)$"
if not re.match(hostname_regex, machine_name):
msg = "Machine name must be a valid hostname"
raise ClanError(msg)
if "networking" in config and "hostName" in config["networking"]:
if machine_name != config["networking"]["hostName"]:
raise ClanHttpError(
msg="Machine name does not match the 'networking.hostName' setting in the config",
status_code=400,
)
config["networking"]["hostName"] = machine_name
# create machine folder if it doesn't exist
# write the config to a json file located at {flake}/machines/{machine_name}/settings.json
settings_path = machine_settings_file(flake_dir, machine_name)
settings_path.parent.mkdir(parents=True, exist_ok=True)
with settings_path.open("w") as f:
json.dump(config, f)
if flake_dir is not None:
commit_file(settings_path, flake_dir)

View File

@@ -1,111 +0,0 @@
import json
from pathlib import Path
from typing import Any
from clan_cli.cmd import run
from clan_cli.errors import ClanError
from clan_cli.nix import nix_eval
script_dir = Path(__file__).parent
type_map: dict[str, type] = {
"array": list,
"boolean": bool,
"integer": int,
"number": float,
"string": str,
}
def schema_from_module_file(
file: str | Path = f"{script_dir}/jsonschema/example-schema.json",
) -> dict[str, Any]:
absolute_path = Path(file).absolute()
# define a nix expression that loads the given module file using lib.evalModules
nix_expr = f"""
let
lib = import <nixpkgs/lib>;
slib = import {script_dir}/jsonschema {{inherit lib;}};
in
slib.parseModule {absolute_path}
"""
# run the nix expression and parse the output as json
cmd = nix_eval(["--expr", nix_expr])
proc = run(cmd)
return json.loads(proc.stdout)
def subtype_from_schema(schema: dict[str, Any]) -> type:
if schema["type"] == "object":
if "additionalProperties" in schema:
sub_type = subtype_from_schema(schema["additionalProperties"])
return dict[str, sub_type] # type: ignore
if "properties" in schema:
msg = "Nested dicts are not supported"
raise ClanError(msg)
msg = "Unknown object type"
raise ClanError(msg)
if schema["type"] == "array":
if "items" not in schema:
msg = "Untyped arrays are not supported"
raise ClanError(msg)
sub_type = subtype_from_schema(schema["items"])
return list[sub_type] # type: ignore
return type_map[schema["type"]]
def type_from_schema_path(
schema: dict[str, Any],
path: list[str],
full_path: list[str] | None = None,
) -> type:
if full_path is None:
full_path = path
if len(path) == 0:
return subtype_from_schema(schema)
if schema["type"] == "object":
if "properties" in schema:
subtype = type_from_schema_path(schema["properties"][path[0]], path[1:])
return subtype
if "additionalProperties" in schema:
subtype = type_from_schema_path(schema["additionalProperties"], path[1:])
return subtype
msg = f"Unknown type for path {path}"
raise ClanError(msg)
msg = f"Unknown type for path {path}"
raise ClanError(msg)
def options_types_from_schema(schema: dict[str, Any]) -> dict[str, type]:
result: dict[str, type] = {}
for name, value in schema.get("properties", {}).items():
assert isinstance(value, dict)
type_ = value["type"]
if type_ == "object":
# handle additionalProperties
if "additionalProperties" in value:
sub_type = value["additionalProperties"].get("type")
if sub_type not in type_map:
msg = f"Unsupported object type {sub_type} (field {name})"
raise ClanError(msg)
result[f"{name}.<name>"] = type_map[sub_type]
continue
# handle properties
sub_result = options_types_from_schema(value)
for sub_name, sub_type in sub_result.items():
result[f"{name}.{sub_name}"] = sub_type
continue
if type_ == "array":
if "items" not in value:
msg = f"Untyped arrays are not supported (field: {name})"
raise ClanError(msg)
sub_type = value["items"].get("type")
if sub_type not in type_map:
msg = f"Unsupported list type {sub_type} (field {name})"
raise ClanError(msg)
sub_type_: type = type_map[sub_type]
result[name] = list[sub_type_] # type: ignore
continue
result[name] = type_map[type_]
return result

View File

@@ -115,10 +115,6 @@ def specific_machine_dir(flake_dir: Path, machine: str) -> Path:
return machines_dir(flake_dir) / machine return machines_dir(flake_dir) / machine
def machine_settings_file(flake_dir: Path, machine: str) -> Path:
return specific_machine_dir(flake_dir, machine) / "settings.json"
def module_root() -> Path: def module_root() -> Path:
return Path(__file__).parent return Path(__file__).parent

View File

@@ -19,7 +19,6 @@ class Machine:
name: str name: str
description: None | str = field(default = None) description: None | str = field(default = None)
icon: None | str = field(default = None) icon: None | str = field(default = None)
system: None | str = field(default = None)
tags: list[str] = field(default_factory = list) tags: list[str] = field(default_factory = list)

View File

@@ -33,13 +33,11 @@
)); ));
}; };
flakeLockFile = builtins.toFile "clan-core-flake.lock" (builtins.toJSON flakeLockVendoredDeps); flakeLockFile = builtins.toFile "clan-core-flake.lock" (builtins.toJSON flakeLockVendoredDeps);
clanCoreWithVendoredDeps = clanCoreWithVendoredDeps = pkgs.runCommand "clan-core-with-vendored-deps" { } ''
lib.trace flakeLockFile pkgs.runCommand "clan-core-with-vendored-deps" { } cp -r ${self} $out
'' chmod +w -R $out
cp -r ${self} $out cp ${flakeLockFile} $out/flake.lock
chmod +w -R $out '';
cp ${flakeLockFile} $out/flake.lock
'';
in in
{ {
devShells.clan-cli = pkgs.callPackage ./shell.nix { devShells.clan-cli = pkgs.callPackage ./shell.nix {

View File

@@ -15,6 +15,7 @@ pytest_plugins = [
"host_group", "host_group",
"fixtures_flakes", "fixtures_flakes",
"stdout", "stdout",
"nix_config",
] ]

View File

@@ -0,0 +1,29 @@
This is a revocation certificate for the OpenPGP key:
pub rsa1024 2024-09-29 [SCEAR]
9A9B2741C8062D3D3DF1302D8B049E262A5CA255
uid Root Superuser <test@local>
A revocation certificate is a kind of "kill switch" to publicly
declare that a key shall not anymore be used. It is not possible
to retract such a revocation certificate once it has been published.
Use it to revoke this key in case of a compromise or loss of
the secret key. However, if the secret key is still accessible,
it is better to generate a new revocation certificate and give
a reason for the revocation. For details see the description of
of the gpg command "--generate-revocation" in the GnuPG manual.
To avoid an accidental use of this file, a colon has been inserted
before the 5 dashes below. Remove this colon with a text editor
before importing and publishing this revocation certificate.
:-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: This is a revocation certificate
iLYEIAEIACAWIQSamydByAYtPT3xMC2LBJ4mKlyiVQUCZvl/cAIdAAAKCRCLBJ4m
KlyiVUWOA/9rDw6tSSw7Gh3vlaLZXSQvkftO3x9cJwePn6JPmM2nWLDcaOj+/Cd0
guyakYt7Fsxa6fqcv5sYV50bPRqAnfOWbR7jRl4DF6pSYNCHPlkWuLghdYsBOBo2
1MG/J+155aclsB8JQez1eGMe8KcpcJBcrYuZTAMekMGPrfyr9SwDUg==
=V2Jo
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -0,0 +1,15 @@
Created: 20240929T162520
Key: (private-key (rsa (n #00B1BF3E8A8CEA6A68439F67CDCAF5616B50D99A9F88
6D9E879D3FE990854E9ADFC35D7D26DBC5BC1800B3FF7B814F4623C1DFC34CAB4D326C
3E269C6059D567B5144659B3C895B52B428BA7B74CC2FA130D06C689C45B8FF8DA1D7C
7A578C99C0F221189D6BE045AE2EC8D2389423994BA0D650A2EDD2B7664642BFBF9691
495F#)(e #010001#)(d #57605C65AE94F39EF293136BB23842DE06DE19A90FDF573D
723B3F5D5872C626767AE831687B0116498E326AABABE51E61C9564FC3ABCCBC322737
DB137E191EB3B012B9C142290050EBD8ADD40BC68CCB577521E3A76DFD668BC6E584C7
0DD3B6CE545CC392B1D893EFB959BE3BD0EB7DF73A1F7AFBD9693353BA4FD3C05AED#)
(p #00C169E9E1DF8F39E7B2140FD52723FC5D10CCFC62D8A0876D39641AB00441345C
FC239EF8551B5F39CE850EF2DD79B98D70D57AD933648C86B7DD536B1B3AD6CB#)(q
#00EB43872BDDA397AC02A32E7CB0061ACB26A30497031D24FA793DE9EE4EFBACB1A4
6BF1444DE47CB63A6E254F2E4928BB0BB1F5C51C5247EEA8FF2D84BE25F13D#)(u
#00CEBE9717B5F7B59393065F884ACCA692F64545F492E50DF9070ACA9FBDA8A1EC03
906FDB9C112A97FADBB273E69548C6B17E6BE3BB664B9D02FB2100EF19AF7D#)))

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,6 @@
Key-Type: 1
Key-Length: 1024
Name-Real: Root Superuser
Name-Email: test@local
Expire-Date: 0
%no-protection

View File

@@ -0,0 +1 @@
test@local

View File

@@ -55,14 +55,33 @@ def set_machine_settings(
machine_name: str, machine_name: str,
machine_settings: dict, machine_settings: dict,
) -> None: ) -> None:
settings_path = flake / "machines" / machine_name / "settings.json" config_path = flake / "machines" / machine_name / "configuration.json"
settings_path.parent.mkdir(parents=True, exist_ok=True) config_path.write_text(json.dumps(machine_settings, indent=2))
settings_path.write_text(json.dumps(machine_settings, indent=2))
def init_git(monkeypatch: pytest.MonkeyPatch, flake: Path) -> None:
monkeypatch.setenv("GIT_AUTHOR_NAME", "clan-tool")
monkeypatch.setenv("GIT_AUTHOR_EMAIL", "clan@example.com")
monkeypatch.setenv("GIT_COMMITTER_NAME", "clan-tool")
monkeypatch.setenv("GIT_COMMITTER_EMAIL", "clan@example.com")
# TODO: Find out why test_vms_api.py fails in nix build
# but works in pytest when this bottom line is commented out
sp.run(
["git", "config", "--global", "init.defaultBranch", "main"],
cwd=flake,
check=True,
)
sp.run(["git", "init"], cwd=flake, check=True)
sp.run(["git", "add", "."], cwd=flake, check=True)
sp.run(["git", "commit", "-a", "-m", "Initial commit"], cwd=flake, check=True)
def generate_flake( def generate_flake(
temporary_home: Path, temporary_home: Path,
flake_template: Path, flake_template: Path,
monkeypatch: pytest.MonkeyPatch,
substitutions: dict[str, str] | None = None, substitutions: dict[str, str] | None = None,
# define the machines directly including their config # define the machines directly including their config
machine_configs: dict[str, dict] | None = None, machine_configs: dict[str, dict] | None = None,
@@ -90,13 +109,12 @@ def generate_flake(
inventory = {} inventory = {}
if machine_configs is None: if machine_configs is None:
machine_configs = {} machine_configs = {}
if substitutions is None: substitutions = {
substitutions = { "__CHANGE_ME__": "_test_vm_persistence",
"__CHANGE_ME__": "_test_vm_persistence", "git+https://git.clan.lol/clan/clan-core": "path://" + str(CLAN_CORE),
"git+https://git.clan.lol/clan/clan-core": "path://" + str(CLAN_CORE), "https://git.clan.lol/clan/clan-core/archive/main.tar.gz": "path://"
"https://git.clan.lol/clan/clan-core/archive/main.tar.gz": "path://" + str(CLAN_CORE),
+ str(CLAN_CORE), }
}
flake = temporary_home / "flake" flake = temporary_home / "flake"
shutil.copytree(flake_template, flake) shutil.copytree(flake_template, flake)
sp.run(["chmod", "+w", "-R", str(flake)], check=True) sp.run(["chmod", "+w", "-R", str(flake)], check=True)
@@ -121,6 +139,11 @@ def generate_flake(
# generate machines from machineConfigs # generate machines from machineConfigs
for machine_name, machine_config in machine_configs.items(): for machine_name, machine_config in machine_configs.items():
configuration_nix = flake / "machines" / machine_name / "configuration.nix"
configuration_nix.parent.mkdir(parents=True, exist_ok=True)
configuration_nix.write_text("""
{ imports = [ (builtins.fromJSON (builtins.readFile ./configuration.json)) ]; }
""")
set_machine_settings(flake, machine_name, machine_config) set_machine_settings(flake, machine_name, machine_config)
if "/tmp" not in str(os.environ.get("HOME")): if "/tmp" not in str(os.environ.get("HOME")):
@@ -135,17 +158,15 @@ def generate_flake(
cwd=flake, cwd=flake,
check=True, check=True,
) )
sp.run(["git", "init"], cwd=flake, check=True) init_git(monkeypatch, flake)
sp.run(["git", "add", "."], cwd=flake, check=True)
sp.run(["git", "config", "user.name", "clan-tool"], cwd=flake, check=True)
sp.run(["git", "config", "user.email", "clan@example.com"], cwd=flake, check=True)
sp.run(["git", "commit", "-a", "-m", "Initial commit"], cwd=flake, check=True)
return FlakeForTest(flake) return FlakeForTest(flake)
def create_flake( def create_flake(
temporary_home: Path, temporary_home: Path,
flake_template: str | Path, flake_template: str | Path,
monkeypatch: pytest.MonkeyPatch,
clan_core_flake: Path | None = None, clan_core_flake: Path | None = None,
# names referring to pre-defined machines from ../machines # names referring to pre-defined machines from ../machines
machines: list[str] | None = None, machines: list[str] | None = None,
@@ -198,18 +219,7 @@ def create_flake(
f"!! $HOME does not point to a temp directory!! HOME={os.environ['HOME']}" f"!! $HOME does not point to a temp directory!! HOME={os.environ['HOME']}"
) )
# TODO: Find out why test_vms_api.py fails in nix build init_git(monkeypatch, flake)
# but works in pytest when this bottom line is commented out
sp.run(
["git", "config", "--global", "init.defaultBranch", "main"],
cwd=flake,
check=True,
)
sp.run(["git", "init"], cwd=flake, check=True)
sp.run(["git", "add", "."], cwd=flake, check=True)
sp.run(["git", "config", "user.name", "clan-tool"], cwd=flake, check=True)
sp.run(["git", "config", "user.email", "clan@example.com"], cwd=flake, check=True)
sp.run(["git", "commit", "-a", "-m", "Initial commit"], cwd=flake, check=True)
if remote: if remote:
with tempfile.TemporaryDirectory(prefix="flake-"): with tempfile.TemporaryDirectory(prefix="flake-"):
@@ -222,7 +232,11 @@ def create_flake(
def test_flake( def test_flake(
monkeypatch: pytest.MonkeyPatch, temporary_home: Path monkeypatch: pytest.MonkeyPatch, temporary_home: Path
) -> Iterator[FlakeForTest]: ) -> Iterator[FlakeForTest]:
yield from create_flake(temporary_home, "test_flake") yield from create_flake(
temporary_home=temporary_home,
flake_template="test_flake",
monkeypatch=monkeypatch,
)
# check that git diff on ./sops is empty # check that git diff on ./sops is empty
if (temporary_home / "test_flake" / "sops").exists(): if (temporary_home / "test_flake" / "sops").exists():
git_proc = sp.run( git_proc = sp.run(
@@ -244,9 +258,10 @@ def test_flake_with_core(
msg = "clan-core flake not found. This test requires the clan-core flake to be present" msg = "clan-core flake not found. This test requires the clan-core flake to be present"
raise FixtureError(msg) raise FixtureError(msg)
yield from create_flake( yield from create_flake(
temporary_home, temporary_home=temporary_home,
"test_flake_with_core", flake_template="test_flake_with_core",
CLAN_CORE, clan_core_flake=CLAN_CORE,
monkeypatch=monkeypatch,
) )
@@ -276,9 +291,10 @@ def test_flake_with_core_and_pass(
msg = "clan-core flake not found. This test requires the clan-core flake to be present" msg = "clan-core flake not found. This test requires the clan-core flake to be present"
raise FixtureError(msg) raise FixtureError(msg)
yield from create_flake( yield from create_flake(
temporary_home, temporary_home=temporary_home,
"test_flake_with_core_and_pass", flake_template="test_flake_with_core_and_pass",
CLAN_CORE, clan_core_flake=CLAN_CORE,
monkeypatch=monkeypatch,
) )
@@ -290,7 +306,8 @@ def test_flake_minimal(
msg = "clan-core flake not found. This test requires the clan-core flake to be present" msg = "clan-core flake not found. This test requires the clan-core flake to be present"
raise FixtureError(msg) raise FixtureError(msg)
yield from create_flake( yield from create_flake(
temporary_home, temporary_home=temporary_home,
CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
CLAN_CORE, monkeypatch=monkeypatch,
clan_core_flake=CLAN_CORE,
) )

View File

@@ -0,0 +1,24 @@
import json
import subprocess
from dataclasses import dataclass
import pytest
@dataclass
class ConfigItem:
aliases: list[str]
defaultValue: bool # noqa: N815
description: str
documentDefault: bool # noqa: N815
experimentalFeature: str # noqa: N815
value: str | bool | list[str] | dict[str, str]
@pytest.fixture(scope="session")
def nix_config() -> dict[str, ConfigItem]:
proc = subprocess.run(
["nix", "show-config", "--json"], check=True, stdout=subprocess.PIPE
)
data = json.loads(proc.stdout)
return {name: ConfigItem(**c) for name, c in data.items()}

View File

@@ -1,171 +0,0 @@
from pathlib import Path
import pytest
from clan_cli import config
from clan_cli.config import parsing
from clan_cli.errors import ClanError
example_options = f"{Path(config.__file__).parent}/jsonschema/options.json"
def test_walk_jsonschema_all_types() -> None:
schema = {
"type": "object",
"properties": {
"array": {
"type": "array",
"items": {
"type": "string",
},
},
"boolean": {"type": "boolean"},
"integer": {"type": "integer"},
"number": {"type": "number"},
"string": {"type": "string"},
},
}
expected = {
"array": list[str],
"boolean": bool,
"integer": int,
"number": float,
"string": str,
}
assert config.parsing.options_types_from_schema(schema) == expected
def test_walk_jsonschema_nested() -> None:
schema = {
"type": "object",
"properties": {
"name": {
"type": "object",
"properties": {
"first": {"type": "string"},
"last": {"type": "string"},
},
},
"age": {"type": "integer"},
},
}
expected = {
"age": int,
"name.first": str,
"name.last": str,
}
assert config.parsing.options_types_from_schema(schema) == expected
# test walk_jsonschema with dynamic attributes (e.g. "additionalProperties")
def test_walk_jsonschema_dynamic_attrs() -> None:
schema = {
"type": "object",
"properties": {
"age": {"type": "integer"},
"users": {
"type": "object",
"additionalProperties": {"type": "string"},
},
},
}
expected = {
"age": int,
"users.<name>": str, # <name> is a placeholder for any string
}
assert config.parsing.options_types_from_schema(schema) == expected
def test_type_from_schema_path_simple() -> None:
schema = {
"type": "boolean",
}
assert parsing.type_from_schema_path(schema, []) is bool
def test_type_from_schema_path_nested() -> None:
schema = {
"type": "object",
"properties": {
"name": {
"type": "object",
"properties": {
"first": {"type": "string"},
"last": {"type": "string"},
},
},
"age": {"type": "integer"},
},
}
assert parsing.type_from_schema_path(schema, ["age"]) is int
assert parsing.type_from_schema_path(schema, ["name", "first"]) is str
def test_type_from_schema_path_dynamic_attrs() -> None:
schema = {
"type": "object",
"properties": {
"age": {"type": "integer"},
"users": {
"type": "object",
"additionalProperties": {"type": "string"},
},
},
}
assert parsing.type_from_schema_path(schema, ["age"]) is int
assert parsing.type_from_schema_path(schema, ["users", "foo"]) is str
def test_map_type() -> None:
with pytest.raises(ClanError):
config.map_type("foo")
assert config.map_type("string") is str
assert config.map_type("integer") is int
assert config.map_type("boolean") is bool
assert config.map_type("attribute set of string") == dict[str, str]
assert config.map_type("attribute set of integer") == dict[str, int]
assert config.map_type("null or string") == str | None
# test the cast function with simple types
def test_cast() -> None:
assert (
config.cast(value=["true"], input_type=bool, opt_description="foo-option")
is True
)
assert (
config.cast(value=["null"], input_type=str | None, opt_description="foo-option")
is None
)
assert (
config.cast(value=["bar"], input_type=str | None, opt_description="foo-option")
== "bar"
)
@pytest.mark.parametrize(
("option", "value", "options", "expected"),
[
("foo.bar", ["baz"], {"foo.bar": {"type": "str"}}, ("foo.bar", ["baz"])),
("foo.bar", ["baz"], {"foo": {"type": "attrs"}}, ("foo", {"bar": ["baz"]})),
(
"users.users.my-user.name",
["my-name"],
{"users.users.<name>.name": {"type": "str"}},
("users.users.<name>.name", ["my-name"]),
),
(
"foo.bar.baz.bum",
["val"],
{"foo.<name>.baz": {"type": "attrs"}},
("foo.<name>.baz", {"bum": ["val"]}),
),
(
"userIds.DavHau",
["42"],
{"userIds": {"type": "attrs"}},
("userIds", {"DavHau": ["42"]}),
),
],
)
def test_find_option(option: str, value: list, options: dict, expected: tuple) -> None:
assert config.find_option(option, value, options) == expected

View File

@@ -12,23 +12,11 @@
inputs = inputs' // { inputs = inputs' // {
clan-core = fake-clan-core; 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
{ }
);
machineImports = map (module: fake-clan-core.clanModules.${module}) (
machineSettings.clanImports or [ ]
);
in in
{ {
nixosConfigurations.machine1 = inputs.nixpkgs.lib.nixosSystem { nixosConfigurations.machine1 = inputs.nixpkgs.lib.nixosSystem {
modules = machineImports ++ [ modules = [
./nixosModules/machine1.nix ./nixosModules/machine1.nix
machineSettings
( (
{ {
lib, lib,

View File

@@ -1,4 +1,5 @@
import json import json
import subprocess
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import pytest import pytest
@@ -55,12 +56,19 @@ def test_add_module_to_inventory(
) )
opts = CreateOptions( opts = CreateOptions(
clan_dir=FlakeId(str(base_path)), clan_dir=FlakeId(str(base_path)),
machine=Machine( machine=Machine(name="machine1", tags=[], deploy=MachineDeploy()),
name="machine1", tags=[], system="x86_64-linux", deploy=MachineDeploy()
),
) )
create_machine(opts) create_machine(opts)
(test_flake_with_core.path / "machines" / "machine1" / "facter.json").write_text(
json.dumps(
{
"version": 1,
"system": "x86_64-linux",
}
)
)
subprocess.run(["git", "add", "."], cwd=test_flake_with_core.path)
inventory = load_inventory_json(base_path) inventory = load_inventory_json(base_path)

View File

@@ -1,5 +1,5 @@
import json import json
import subprocess import shutil
from dataclasses import dataclass from dataclasses import dataclass
from io import StringIO from io import StringIO
from pathlib import Path from pathlib import Path
@@ -9,7 +9,7 @@ from age_keys import SopsSetup
from clan_cli.clan_uri import FlakeId from clan_cli.clan_uri import FlakeId
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from clan_cli.machines.machines import Machine from clan_cli.machines.machines import Machine
from clan_cli.nix import nix_eval, nix_shell, run from clan_cli.nix import nix_eval, run
from clan_cli.vars.check import check_vars from clan_cli.vars.check import check_vars
from clan_cli.vars.generate import generate_vars_for_machine from clan_cli.vars.generate import generate_vars_for_machine
from clan_cli.vars.list import stringify_all_vars from clan_cli.vars.list import stringify_all_vars
@@ -83,12 +83,14 @@ def test_generate_public_var(
temporary_home: Path, temporary_home: Path,
) -> None: ) -> None:
config = nested_dict() config = nested_dict()
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"] my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
my_generator["files"]["my_value"]["secret"] = False my_generator["files"]["my_value"]["secret"] = False
my_generator["script"] = "echo hello > $out/my_value" my_generator["script"] = "echo hello > $out/my_value"
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"my_machine": config}, machine_configs={"my_machine": config},
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
@@ -122,12 +124,14 @@ def test_generate_secret_var_sops(
sops_setup: SopsSetup, sops_setup: SopsSetup,
) -> None: ) -> None:
config = nested_dict() config = nested_dict()
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"] my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
my_generator["files"]["my_secret"]["secret"] = True my_generator["files"]["my_secret"]["secret"] = True
my_generator["script"] = "echo hello > $out/my_secret" my_generator["script"] = "echo hello > $out/my_secret"
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"my_machine": config}, machine_configs={"my_machine": config},
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
@@ -162,6 +166,7 @@ def test_generate_secret_var_sops_with_default_group(
sops_setup: SopsSetup, sops_setup: SopsSetup,
) -> None: ) -> None:
config = nested_dict() config = nested_dict()
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
config["clan"]["core"]["sops"]["defaultGroups"] = ["my_group"] config["clan"]["core"]["sops"]["defaultGroups"] = ["my_group"]
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"] my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
my_generator["files"]["my_secret"]["secret"] = True my_generator["files"]["my_secret"]["secret"] = True
@@ -169,6 +174,7 @@ def test_generate_secret_var_sops_with_default_group(
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"my_machine": config}, machine_configs={"my_machine": config},
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
@@ -193,6 +199,7 @@ def test_generated_shared_secret_sops(
sops_setup: SopsSetup, sops_setup: SopsSetup,
) -> None: ) -> None:
m1_config = nested_dict() m1_config = nested_dict()
m1_config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
shared_generator = m1_config["clan"]["core"]["vars"]["generators"][ shared_generator = m1_config["clan"]["core"]["vars"]["generators"][
"my_shared_generator" "my_shared_generator"
] ]
@@ -200,12 +207,14 @@ def test_generated_shared_secret_sops(
shared_generator["files"]["my_shared_secret"]["secret"] = True shared_generator["files"]["my_shared_secret"]["secret"] = True
shared_generator["script"] = "echo hello > $out/my_shared_secret" shared_generator["script"] = "echo hello > $out/my_shared_secret"
m2_config = nested_dict() m2_config = nested_dict()
m2_config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
m2_config["clan"]["core"]["vars"]["generators"]["my_shared_generator"] = ( m2_config["clan"]["core"]["vars"]["generators"]["my_shared_generator"] = (
shared_generator.copy() shared_generator.copy()
) )
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"machine1": m1_config, "machine2": m2_config}, machine_configs={"machine1": m1_config, "machine2": m2_config},
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
@@ -233,8 +242,10 @@ def test_generated_shared_secret_sops(
def test_generate_secret_var_password_store( def test_generate_secret_var_password_store(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
temporary_home: Path, temporary_home: Path,
test_root: Path,
) -> None: ) -> None:
config = nested_dict() config = nested_dict()
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
config["clan"]["core"]["vars"]["settings"]["secretStore"] = "password-store" config["clan"]["core"]["vars"]["settings"]["secretStore"] = "password-store"
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"] my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
my_generator["files"]["my_secret"]["secret"] = True my_generator["files"]["my_secret"]["secret"] = True
@@ -248,33 +259,18 @@ def test_generate_secret_var_password_store(
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"my_machine": config}, machine_configs={"my_machine": config},
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
gnupghome = temporary_home / "gpg" gnupghome = temporary_home / "gpg"
gnupghome.mkdir(mode=0o700) shutil.copytree(test_root / "data" / "gnupg-home", gnupghome)
monkeypatch.setenv("GNUPGHOME", str(gnupghome)) monkeypatch.setenv("GNUPGHOME", str(gnupghome))
password_store_dir = temporary_home / "pass"
shutil.copytree(test_root / "data" / "password-store", password_store_dir)
monkeypatch.setenv("PASSWORD_STORE_DIR", str(temporary_home / "pass")) monkeypatch.setenv("PASSWORD_STORE_DIR", str(temporary_home / "pass"))
gpg_key_spec = temporary_home / "gpg_key_spec"
gpg_key_spec.write_text(
"""
Key-Type: 1
Key-Length: 1024
Name-Real: Root Superuser
Name-Email: test@local
Expire-Date: 0
%no-protection
"""
)
subprocess.run(
nix_shell(
["nixpkgs#gnupg"], ["gpg", "--batch", "--gen-key", str(gpg_key_spec)]
),
check=True,
)
subprocess.run(
nix_shell(["nixpkgs#pass"], ["pass", "init", "test@local"]), check=True
)
machine = Machine(name="my_machine", flake=FlakeId(str(flake.path))) machine = Machine(name="my_machine", flake=FlakeId(str(flake.path)))
assert not check_vars(machine) assert not check_vars(machine)
cli.run(["vars", "generate", "--flake", str(flake.path), "my_machine"]) cli.run(["vars", "generate", "--flake", str(flake.path), "my_machine"])
@@ -318,6 +314,7 @@ def test_generate_secret_for_multiple_machines(
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"machine1": machine1_config, "machine2": machine2_config}, machine_configs={"machine1": machine1_config, "machine2": machine2_config},
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
@@ -363,6 +360,7 @@ def test_dependant_generators(
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"my_machine": config}, machine_configs={"my_machine": config},
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
@@ -402,6 +400,7 @@ def test_prompt(
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"my_machine": config}, machine_configs={"my_machine": config},
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
@@ -421,6 +420,7 @@ def test_share_flag(
sops_setup: SopsSetup, sops_setup: SopsSetup,
) -> None: ) -> None:
config = nested_dict() config = nested_dict()
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
shared_generator = config["clan"]["core"]["vars"]["generators"]["shared_generator"] shared_generator = config["clan"]["core"]["vars"]["generators"]["shared_generator"]
shared_generator["share"] = True shared_generator["share"] = True
shared_generator["files"]["my_secret"]["secret"] = True shared_generator["files"]["my_secret"]["secret"] = True
@@ -440,6 +440,7 @@ def test_share_flag(
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"my_machine": config}, machine_configs={"my_machine": config},
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
@@ -490,6 +491,7 @@ def test_prompt_create_file(
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"my_machine": config}, machine_configs={"my_machine": config},
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
@@ -518,6 +520,7 @@ def test_api_get_prompts(
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"my_machine": config}, machine_configs={"my_machine": config},
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
@@ -546,6 +549,7 @@ def test_api_set_prompts(
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"my_machine": config}, machine_configs={"my_machine": config},
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
@@ -592,6 +596,7 @@ def test_commit_message(
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"my_machine": config}, machine_configs={"my_machine": config},
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
@@ -641,6 +646,7 @@ def test_default_value(
temporary_home: Path, temporary_home: Path,
) -> None: ) -> None:
config = nested_dict() config = nested_dict()
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"] my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
my_generator["files"]["my_value"]["secret"] = False my_generator["files"]["my_value"]["secret"] = False
my_generator["files"]["my_value"]["value"]["_type"] = "override" my_generator["files"]["my_value"]["value"]["_type"] = "override"
@@ -650,6 +656,7 @@ def test_default_value(
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"my_machine": config}, machine_configs={"my_machine": config},
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
@@ -683,6 +690,7 @@ def test_stdout_of_generate(
sops_setup: SopsSetup, sops_setup: SopsSetup,
) -> None: ) -> None:
config = nested_dict() config = nested_dict()
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"] my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
my_generator["files"]["my_value"]["secret"] = False my_generator["files"]["my_value"]["secret"] = False
my_generator["script"] = "echo -n hello > $out/my_value" my_generator["script"] = "echo -n hello > $out/my_value"
@@ -694,6 +702,7 @@ def test_stdout_of_generate(
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"my_machine": config}, machine_configs={"my_machine": config},
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
@@ -761,6 +770,7 @@ def test_migration_skip(
sops_setup: SopsSetup, sops_setup: SopsSetup,
) -> None: ) -> None:
config = nested_dict() config = nested_dict()
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
my_service = config["clan"]["core"]["facts"]["services"]["my_service"] my_service = config["clan"]["core"]["facts"]["services"]["my_service"]
my_service["secret"]["my_value"] = {} my_service["secret"]["my_value"] = {}
my_service["generator"]["script"] = "echo -n hello > $secrets/my_value" my_service["generator"]["script"] = "echo -n hello > $secrets/my_value"
@@ -772,6 +782,7 @@ def test_migration_skip(
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"my_machine": config}, machine_configs={"my_machine": config},
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
@@ -792,6 +803,7 @@ def test_migration(
sops_setup: SopsSetup, sops_setup: SopsSetup,
) -> None: ) -> None:
config = nested_dict() config = nested_dict()
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
my_service = config["clan"]["core"]["facts"]["services"]["my_service"] my_service = config["clan"]["core"]["facts"]["services"]["my_service"]
my_service["public"]["my_value"] = {} my_service["public"]["my_value"] = {}
my_service["generator"]["script"] = "echo -n hello > $facts/my_value" my_service["generator"]["script"] = "echo -n hello > $facts/my_value"
@@ -802,6 +814,7 @@ def test_migration(
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"my_machine": config}, machine_configs={"my_machine": config},
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)
@@ -822,6 +835,7 @@ def test_fails_when_files_are_left_from_other_backend(
sops_setup: SopsSetup, sops_setup: SopsSetup,
) -> None: ) -> None:
config = nested_dict() config = nested_dict()
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
my_secret_generator = config["clan"]["core"]["vars"]["generators"][ my_secret_generator = config["clan"]["core"]["vars"]["generators"][
"my_secret_generator" "my_secret_generator"
] ]
@@ -835,6 +849,7 @@ def test_fails_when_files_are_left_from_other_backend(
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"my_machine": config}, machine_configs={"my_machine": config},
) )
monkeypatch.chdir(flake.path) monkeypatch.chdir(flake.path)

View File

@@ -12,6 +12,7 @@ from clan_cli.vms.run import inspect_vm, spawn_vm
from fixtures_flakes import generate_flake from fixtures_flakes import generate_flake
from helpers import cli from helpers import cli
from helpers.nixos_config import nested_dict from helpers.nixos_config import nested_dict
from nix_config import ConfigItem
from root import CLAN_CORE from root import CLAN_CORE
@@ -19,10 +20,12 @@ from root import CLAN_CORE
def test_vm_deployment( def test_vm_deployment(
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
temporary_home: Path, temporary_home: Path,
nix_config: dict[str, ConfigItem],
sops_setup: SopsSetup, sops_setup: SopsSetup,
) -> None: ) -> None:
# machine 1 # machine 1
machine1_config = nested_dict() machine1_config = nested_dict()
machine1_config["nixpkgs"]["hostPlatform"] = nix_config["system"].value
machine1_config["clan"]["virtualisation"]["graphics"] = False machine1_config["clan"]["virtualisation"]["graphics"] = False
machine1_config["services"]["getty"]["autologinUser"] = "root" machine1_config["services"]["getty"]["autologinUser"] = "root"
machine1_config["services"]["openssh"]["enable"] = True machine1_config["services"]["openssh"]["enable"] = True
@@ -48,6 +51,7 @@ def test_vm_deployment(
""" """
# machine 2 # machine 2
machine2_config = nested_dict() machine2_config = nested_dict()
machine2_config["nixpkgs"]["hostPlatform"] = nix_config["system"].value
machine2_config["clan"]["virtualisation"]["graphics"] = False machine2_config["clan"]["virtualisation"]["graphics"] = False
machine2_config["services"]["getty"]["autologinUser"] = "root" machine2_config["services"]["getty"]["autologinUser"] = "root"
machine2_config["services"]["openssh"]["enable"] = True machine2_config["services"]["openssh"]["enable"] = True
@@ -62,6 +66,7 @@ def test_vm_deployment(
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs={"m1_machine": machine1_config, "m2_machine": machine2_config}, machine_configs={"m1_machine": machine1_config, "m2_machine": machine2_config},
) )

View File

@@ -83,6 +83,7 @@ def test_vm_persistence(
flake = generate_flake( flake = generate_flake(
temporary_home, temporary_home,
flake_template=CLAN_CORE / "templates" / "minimal", flake_template=CLAN_CORE / "templates" / "minimal",
monkeypatch=monkeypatch,
machine_configs=config, machine_configs=config,
) )

View File

@@ -23,7 +23,7 @@
modulename: _: jsonLib.parseOptions (optionsFromModule modulename) { } modulename: _: jsonLib.parseOptions (optionsFromModule modulename) { }
) clanModules; ) clanModules;
clanModuleFunctionSchemas = lib.mapAttrsFlatten ( clanModuleFunctionSchemas = lib.attrsets.mapAttrsToList (
modulename: _: modulename: _:
(self.lib.modules.getFrontmatter modulename) (self.lib.modules.getFrontmatter modulename)
// { // {

View File

@@ -7,10 +7,19 @@
initialized = inputs.nixpkgs.legacyPackages.x86_64-linux.runCommand "minimal-clan-flake" { } '' initialized = inputs.nixpkgs.legacyPackages.x86_64-linux.runCommand "minimal-clan-flake" { } ''
mkdir $out mkdir $out
cp -r ${path}/* $out cp -r ${path}/* $out
mkdir -p $out/machines/foo mkdir -p $out/machines/testmachine
# TODO: Instead create a machine by calling the API, this wont break in future tests and is much closer to what the user performs # TODO: Instead create a machine by calling the API, this wont break in future tests and is much closer to what the user performs
echo '{ "nixpkgs": { "hostPlatform": "x86_64-linux" } }' > $out/machines/foo/settings.json cat > $out/machines/testmachine/hardware-configuration.nix << EOF
{ lib, ... }: {
nixpkgs.hostPlatform = "x86_64-linux";
system.stateVersion = lib.version;
documentation.enable = false;
users.users.root.initialPassword = "fnord23";
boot.loader.grub.devices = lib.mkForce [ "/dev/sda" ];
fileSystems."/".device = lib.mkDefault "/dev/sda";
}
EOF
''; '';
evaled = (import "${initialized}/flake.nix").outputs { evaled = (import "${initialized}/flake.nix").outputs {
self = evaled // { self = evaled // {
@@ -22,7 +31,7 @@
{ {
type = "derivation"; type = "derivation";
name = "minimal-clan-flake-check"; name = "minimal-clan-flake-check";
inherit (evaled.nixosConfigurations.foo.config.system.build.vm) drvPath outPath; inherit (evaled.nixosConfigurations.testmachine.config.system.build.toplevel) drvPath outPath;
}; };
}; };
} }