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:
@@ -10,7 +10,6 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -30,6 +29,7 @@
|
||||
clan.core.networking.targetHost = "machine";
|
||||
networking.hostName = "machine";
|
||||
services.openssh.settings.UseDns = false;
|
||||
nixpkgs.hostPlatform = "x86_64-linux";
|
||||
|
||||
programs.ssh.knownHosts = {
|
||||
machine.hostNames = [ "machine" ];
|
||||
|
||||
@@ -20,13 +20,13 @@
|
||||
|
||||
environment.etc."install-successful".text = "ok";
|
||||
|
||||
nixpkgs.hostPlatform = "x86_64-linux";
|
||||
boot.consoleLogLevel = lib.mkForce 100;
|
||||
boot.kernelParams = [ "boot.shell_on_fail" ];
|
||||
};
|
||||
};
|
||||
perSystem =
|
||||
{
|
||||
nodes,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
|
||||
@@ -18,6 +18,7 @@ in
|
||||
# Dummy file system
|
||||
fileSystems."/".device = "/dev/null";
|
||||
boot.loader.grub.device = "/dev/null";
|
||||
nixpkgs.hostPlatform = "x86_64-linux";
|
||||
imports = [
|
||||
documentationModule
|
||||
];
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
},
|
||||
"description": "A nice thing",
|
||||
"icon": "./path/to/icon.png",
|
||||
"tags": ["1", "2", "3"],
|
||||
"system": "x86_64-linux"
|
||||
"tags": ["1", "2", "3"]
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -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
|
||||
nixosConfiguration =
|
||||
{
|
||||
system ? "x86_64-linux",
|
||||
system ? null,
|
||||
name,
|
||||
pkgs ? null,
|
||||
extraConfig ? { },
|
||||
@@ -63,40 +37,16 @@ let
|
||||
nixpkgs.lib.nixosSystem {
|
||||
modules =
|
||||
let
|
||||
settings = machineSettings name;
|
||||
facterJson = "${directory}/machines/${name}/facter.json";
|
||||
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
|
||||
(machineImports settings)
|
||||
++ facterModules
|
||||
++ [
|
||||
[
|
||||
{
|
||||
# Autoinclude configuration.nix and hardware-configuration.nix
|
||||
imports = builtins.filter builtins.pathExists [
|
||||
"${directory}/machines/${name}/configuration.nix"
|
||||
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
|
||||
extraConfig
|
||||
(machines.${name} or { })
|
||||
@@ -115,7 +65,7 @@ let
|
||||
# Machine specific settings
|
||||
clan.core.machineName = 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)
|
||||
nix.registry.nixpkgs.to = {
|
||||
@@ -132,38 +82,7 @@ let
|
||||
} // specialArgs;
|
||||
};
|
||||
|
||||
# TODO: Will be deprecated
|
||||
# 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;
|
||||
allMachines = inventory.machines or { } // machines;
|
||||
|
||||
supportedSystems = [
|
||||
"x86_64-linux"
|
||||
|
||||
@@ -146,6 +146,7 @@ in
|
||||
{ foo, ... }:
|
||||
{
|
||||
networking.hostName = foo;
|
||||
nixpkgs.hostPlatform = "x86_64-linux";
|
||||
};
|
||||
};
|
||||
in
|
||||
|
||||
@@ -154,9 +154,6 @@ let
|
||||
) [ ] inventory.services
|
||||
# Append each machine config
|
||||
++ [
|
||||
(lib.optionalAttrs (machineConfig.system or null != null) {
|
||||
config.nixpkgs.hostPlatform = machineConfig.system;
|
||||
})
|
||||
(lib.optionalAttrs (machineConfig.deploy.targetHost or null != null) {
|
||||
config.clan.core.networking.targetHost = machineConfig.deploy.targetHost;
|
||||
})
|
||||
|
||||
@@ -142,10 +142,6 @@ in
|
||||
apply = lib.unique;
|
||||
type = types.listOf types.str;
|
||||
};
|
||||
system = lib.mkOption {
|
||||
default = null;
|
||||
type = types.nullOr types.str;
|
||||
};
|
||||
deploy.targetHost = lib.mkOption {
|
||||
description = "Configuration for the deployment of the machine";
|
||||
default = null;
|
||||
|
||||
@@ -92,9 +92,9 @@ in
|
||||
not_used_machine = builtins.length configs.not_used_machine;
|
||||
};
|
||||
expected = {
|
||||
client_1_machine = 6;
|
||||
client_2_machine = 6;
|
||||
not_used_machine = 3;
|
||||
client_1_machine = 5;
|
||||
client_2_machine = 5;
|
||||
not_used_machine = 2;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
4
machines/test-inventory-machine/facter.json
Normal file
4
machines/test-inventory-machine/facter.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"version": 1,
|
||||
"system": "x86_64-linux"
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
./meta/interface.nix
|
||||
./metadata.nix
|
||||
./networking.nix
|
||||
./nixos-facter.nix
|
||||
./nix-settings.nix
|
||||
./options.nix
|
||||
./outputs.nix
|
||||
|
||||
24
nixosModules/clanCore/nixos-facter.nix
Normal file
24
nixosModules/clanCore/nixos-facter.nix
Normal 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}'.
|
||||
''
|
||||
];
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
];
|
||||
clanCore.imports = [
|
||||
inputs.sops-nix.nixosModules.sops
|
||||
inputs.nixos-facter-modules.nixosModules.facter
|
||||
inputs.disko.nixosModules.default
|
||||
./clanCore
|
||||
(
|
||||
|
||||
@@ -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}",
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -115,10 +115,6 @@ def specific_machine_dir(flake_dir: Path, machine: str) -> Path:
|
||||
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:
|
||||
return Path(__file__).parent
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ class Machine:
|
||||
name: str
|
||||
description: None | str = field(default = None)
|
||||
icon: None | str = field(default = None)
|
||||
system: None | str = field(default = None)
|
||||
tags: list[str] = field(default_factory = list)
|
||||
|
||||
|
||||
|
||||
@@ -33,13 +33,11 @@
|
||||
));
|
||||
};
|
||||
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
|
||||
'';
|
||||
clanCoreWithVendoredDeps = 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 {
|
||||
|
||||
@@ -15,6 +15,7 @@ pytest_plugins = [
|
||||
"host_group",
|
||||
"fixtures_flakes",
|
||||
"stdout",
|
||||
"nix_config",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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-----
|
||||
@@ -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#)))
|
||||
BIN
pkgs/clan-cli/tests/data/gnupg-home/pubring.kbx
Normal file
BIN
pkgs/clan-cli/tests/data/gnupg-home/pubring.kbx
Normal file
Binary file not shown.
BIN
pkgs/clan-cli/tests/data/gnupg-home/random_seed
Normal file
BIN
pkgs/clan-cli/tests/data/gnupg-home/random_seed
Normal file
Binary file not shown.
BIN
pkgs/clan-cli/tests/data/gnupg-home/trustdb.gpg
Normal file
BIN
pkgs/clan-cli/tests/data/gnupg-home/trustdb.gpg
Normal file
Binary file not shown.
6
pkgs/clan-cli/tests/data/gnupg.conf
Normal file
6
pkgs/clan-cli/tests/data/gnupg.conf
Normal file
@@ -0,0 +1,6 @@
|
||||
Key-Type: 1
|
||||
Key-Length: 1024
|
||||
Name-Real: Root Superuser
|
||||
Name-Email: test@local
|
||||
Expire-Date: 0
|
||||
%no-protection
|
||||
1
pkgs/clan-cli/tests/data/password-store/.gpg-id
Normal file
1
pkgs/clan-cli/tests/data/password-store/.gpg-id
Normal file
@@ -0,0 +1 @@
|
||||
test@local
|
||||
@@ -55,14 +55,33 @@ def set_machine_settings(
|
||||
machine_name: str,
|
||||
machine_settings: dict,
|
||||
) -> None:
|
||||
settings_path = flake / "machines" / machine_name / "settings.json"
|
||||
settings_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
settings_path.write_text(json.dumps(machine_settings, indent=2))
|
||||
config_path = flake / "machines" / machine_name / "configuration.json"
|
||||
config_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(
|
||||
temporary_home: Path,
|
||||
flake_template: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
substitutions: dict[str, str] | None = None,
|
||||
# define the machines directly including their config
|
||||
machine_configs: dict[str, dict] | None = None,
|
||||
@@ -90,13 +109,12 @@ def generate_flake(
|
||||
inventory = {}
|
||||
if machine_configs is None:
|
||||
machine_configs = {}
|
||||
if substitutions is None:
|
||||
substitutions = {
|
||||
"__CHANGE_ME__": "_test_vm_persistence",
|
||||
"git+https://git.clan.lol/clan/clan-core": "path://" + str(CLAN_CORE),
|
||||
"https://git.clan.lol/clan/clan-core/archive/main.tar.gz": "path://"
|
||||
+ str(CLAN_CORE),
|
||||
}
|
||||
substitutions = {
|
||||
"__CHANGE_ME__": "_test_vm_persistence",
|
||||
"git+https://git.clan.lol/clan/clan-core": "path://" + str(CLAN_CORE),
|
||||
"https://git.clan.lol/clan/clan-core/archive/main.tar.gz": "path://"
|
||||
+ str(CLAN_CORE),
|
||||
}
|
||||
flake = temporary_home / "flake"
|
||||
shutil.copytree(flake_template, flake)
|
||||
sp.run(["chmod", "+w", "-R", str(flake)], check=True)
|
||||
@@ -121,6 +139,11 @@ def generate_flake(
|
||||
|
||||
# generate machines from machineConfigs
|
||||
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)
|
||||
|
||||
if "/tmp" not in str(os.environ.get("HOME")):
|
||||
@@ -135,17 +158,15 @@ def generate_flake(
|
||||
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)
|
||||
init_git(monkeypatch, flake)
|
||||
|
||||
return FlakeForTest(flake)
|
||||
|
||||
|
||||
def create_flake(
|
||||
temporary_home: Path,
|
||||
flake_template: str | Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
clan_core_flake: Path | None = None,
|
||||
# names referring to pre-defined machines from ../machines
|
||||
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']}"
|
||||
)
|
||||
|
||||
# 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", "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)
|
||||
init_git(monkeypatch, flake)
|
||||
|
||||
if remote:
|
||||
with tempfile.TemporaryDirectory(prefix="flake-"):
|
||||
@@ -222,7 +232,11 @@ def create_flake(
|
||||
def test_flake(
|
||||
monkeypatch: pytest.MonkeyPatch, temporary_home: Path
|
||||
) -> 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
|
||||
if (temporary_home / "test_flake" / "sops").exists():
|
||||
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"
|
||||
raise FixtureError(msg)
|
||||
yield from create_flake(
|
||||
temporary_home,
|
||||
"test_flake_with_core",
|
||||
CLAN_CORE,
|
||||
temporary_home=temporary_home,
|
||||
flake_template="test_flake_with_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"
|
||||
raise FixtureError(msg)
|
||||
yield from create_flake(
|
||||
temporary_home,
|
||||
"test_flake_with_core_and_pass",
|
||||
CLAN_CORE,
|
||||
temporary_home=temporary_home,
|
||||
flake_template="test_flake_with_core_and_pass",
|
||||
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"
|
||||
raise FixtureError(msg)
|
||||
yield from create_flake(
|
||||
temporary_home,
|
||||
CLAN_CORE / "templates" / "minimal",
|
||||
CLAN_CORE,
|
||||
temporary_home=temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
clan_core_flake=CLAN_CORE,
|
||||
)
|
||||
|
||||
24
pkgs/clan-cli/tests/nix_config.py
Normal file
24
pkgs/clan-cli/tests/nix_config.py
Normal 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()}
|
||||
@@ -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
|
||||
@@ -12,23 +12,11 @@
|
||||
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
|
||||
{ }
|
||||
);
|
||||
machineImports = map (module: fake-clan-core.clanModules.${module}) (
|
||||
machineSettings.clanImports or [ ]
|
||||
);
|
||||
in
|
||||
{
|
||||
nixosConfigurations.machine1 = inputs.nixpkgs.lib.nixosSystem {
|
||||
modules = machineImports ++ [
|
||||
modules = [
|
||||
./nixosModules/machine1.nix
|
||||
machineSettings
|
||||
(
|
||||
{
|
||||
lib,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import subprocess
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
@@ -55,12 +56,19 @@ def test_add_module_to_inventory(
|
||||
)
|
||||
opts = CreateOptions(
|
||||
clan_dir=FlakeId(str(base_path)),
|
||||
machine=Machine(
|
||||
name="machine1", tags=[], system="x86_64-linux", deploy=MachineDeploy()
|
||||
),
|
||||
machine=Machine(name="machine1", tags=[], deploy=MachineDeploy()),
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
import subprocess
|
||||
import shutil
|
||||
from dataclasses import dataclass
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
@@ -9,7 +9,7 @@ from age_keys import SopsSetup
|
||||
from clan_cli.clan_uri import FlakeId
|
||||
from clan_cli.errors import ClanError
|
||||
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.generate import generate_vars_for_machine
|
||||
from clan_cli.vars.list import stringify_all_vars
|
||||
@@ -83,12 +83,14 @@ def test_generate_public_var(
|
||||
temporary_home: Path,
|
||||
) -> None:
|
||||
config = nested_dict()
|
||||
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
|
||||
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
|
||||
my_generator["files"]["my_value"]["secret"] = False
|
||||
my_generator["script"] = "echo hello > $out/my_value"
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"my_machine": config},
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
@@ -122,12 +124,14 @@ def test_generate_secret_var_sops(
|
||||
sops_setup: SopsSetup,
|
||||
) -> None:
|
||||
config = nested_dict()
|
||||
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
|
||||
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
|
||||
my_generator["files"]["my_secret"]["secret"] = True
|
||||
my_generator["script"] = "echo hello > $out/my_secret"
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"my_machine": config},
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
@@ -162,6 +166,7 @@ def test_generate_secret_var_sops_with_default_group(
|
||||
sops_setup: SopsSetup,
|
||||
) -> None:
|
||||
config = nested_dict()
|
||||
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
|
||||
config["clan"]["core"]["sops"]["defaultGroups"] = ["my_group"]
|
||||
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
|
||||
my_generator["files"]["my_secret"]["secret"] = True
|
||||
@@ -169,6 +174,7 @@ def test_generate_secret_var_sops_with_default_group(
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"my_machine": config},
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
@@ -193,6 +199,7 @@ def test_generated_shared_secret_sops(
|
||||
sops_setup: SopsSetup,
|
||||
) -> None:
|
||||
m1_config = nested_dict()
|
||||
m1_config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
|
||||
shared_generator = m1_config["clan"]["core"]["vars"]["generators"][
|
||||
"my_shared_generator"
|
||||
]
|
||||
@@ -200,12 +207,14 @@ def test_generated_shared_secret_sops(
|
||||
shared_generator["files"]["my_shared_secret"]["secret"] = True
|
||||
shared_generator["script"] = "echo hello > $out/my_shared_secret"
|
||||
m2_config = nested_dict()
|
||||
m2_config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
|
||||
m2_config["clan"]["core"]["vars"]["generators"]["my_shared_generator"] = (
|
||||
shared_generator.copy()
|
||||
)
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"machine1": m1_config, "machine2": m2_config},
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
@@ -233,8 +242,10 @@ def test_generated_shared_secret_sops(
|
||||
def test_generate_secret_var_password_store(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
temporary_home: Path,
|
||||
test_root: Path,
|
||||
) -> None:
|
||||
config = nested_dict()
|
||||
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
|
||||
config["clan"]["core"]["vars"]["settings"]["secretStore"] = "password-store"
|
||||
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
|
||||
my_generator["files"]["my_secret"]["secret"] = True
|
||||
@@ -248,33 +259,18 @@ def test_generate_secret_var_password_store(
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"my_machine": config},
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
gnupghome = temporary_home / "gpg"
|
||||
gnupghome.mkdir(mode=0o700)
|
||||
shutil.copytree(test_root / "data" / "gnupg-home", 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"))
|
||||
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)))
|
||||
assert not check_vars(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(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"machine1": machine1_config, "machine2": machine2_config},
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
@@ -363,6 +360,7 @@ def test_dependant_generators(
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"my_machine": config},
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
@@ -402,6 +400,7 @@ def test_prompt(
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"my_machine": config},
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
@@ -421,6 +420,7 @@ def test_share_flag(
|
||||
sops_setup: SopsSetup,
|
||||
) -> None:
|
||||
config = nested_dict()
|
||||
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
|
||||
shared_generator = config["clan"]["core"]["vars"]["generators"]["shared_generator"]
|
||||
shared_generator["share"] = True
|
||||
shared_generator["files"]["my_secret"]["secret"] = True
|
||||
@@ -440,6 +440,7 @@ def test_share_flag(
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"my_machine": config},
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
@@ -490,6 +491,7 @@ def test_prompt_create_file(
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"my_machine": config},
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
@@ -518,6 +520,7 @@ def test_api_get_prompts(
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"my_machine": config},
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
@@ -546,6 +549,7 @@ def test_api_set_prompts(
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"my_machine": config},
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
@@ -592,6 +596,7 @@ def test_commit_message(
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"my_machine": config},
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
@@ -641,6 +646,7 @@ def test_default_value(
|
||||
temporary_home: Path,
|
||||
) -> None:
|
||||
config = nested_dict()
|
||||
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
|
||||
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
|
||||
my_generator["files"]["my_value"]["secret"] = False
|
||||
my_generator["files"]["my_value"]["value"]["_type"] = "override"
|
||||
@@ -650,6 +656,7 @@ def test_default_value(
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"my_machine": config},
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
@@ -683,6 +690,7 @@ def test_stdout_of_generate(
|
||||
sops_setup: SopsSetup,
|
||||
) -> None:
|
||||
config = nested_dict()
|
||||
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
|
||||
my_generator = config["clan"]["core"]["vars"]["generators"]["my_generator"]
|
||||
my_generator["files"]["my_value"]["secret"] = False
|
||||
my_generator["script"] = "echo -n hello > $out/my_value"
|
||||
@@ -694,6 +702,7 @@ def test_stdout_of_generate(
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"my_machine": config},
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
@@ -761,6 +770,7 @@ def test_migration_skip(
|
||||
sops_setup: SopsSetup,
|
||||
) -> None:
|
||||
config = nested_dict()
|
||||
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
|
||||
my_service = config["clan"]["core"]["facts"]["services"]["my_service"]
|
||||
my_service["secret"]["my_value"] = {}
|
||||
my_service["generator"]["script"] = "echo -n hello > $secrets/my_value"
|
||||
@@ -772,6 +782,7 @@ def test_migration_skip(
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"my_machine": config},
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
@@ -792,6 +803,7 @@ def test_migration(
|
||||
sops_setup: SopsSetup,
|
||||
) -> None:
|
||||
config = nested_dict()
|
||||
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
|
||||
my_service = config["clan"]["core"]["facts"]["services"]["my_service"]
|
||||
my_service["public"]["my_value"] = {}
|
||||
my_service["generator"]["script"] = "echo -n hello > $facts/my_value"
|
||||
@@ -802,6 +814,7 @@ def test_migration(
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"my_machine": config},
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
@@ -822,6 +835,7 @@ def test_fails_when_files_are_left_from_other_backend(
|
||||
sops_setup: SopsSetup,
|
||||
) -> None:
|
||||
config = nested_dict()
|
||||
config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
|
||||
my_secret_generator = config["clan"]["core"]["vars"]["generators"][
|
||||
"my_secret_generator"
|
||||
]
|
||||
@@ -835,6 +849,7 @@ def test_fails_when_files_are_left_from_other_backend(
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"my_machine": config},
|
||||
)
|
||||
monkeypatch.chdir(flake.path)
|
||||
|
||||
@@ -12,6 +12,7 @@ from clan_cli.vms.run import inspect_vm, spawn_vm
|
||||
from fixtures_flakes import generate_flake
|
||||
from helpers import cli
|
||||
from helpers.nixos_config import nested_dict
|
||||
from nix_config import ConfigItem
|
||||
from root import CLAN_CORE
|
||||
|
||||
|
||||
@@ -19,10 +20,12 @@ from root import CLAN_CORE
|
||||
def test_vm_deployment(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
temporary_home: Path,
|
||||
nix_config: dict[str, ConfigItem],
|
||||
sops_setup: SopsSetup,
|
||||
) -> None:
|
||||
# machine 1
|
||||
machine1_config = nested_dict()
|
||||
machine1_config["nixpkgs"]["hostPlatform"] = nix_config["system"].value
|
||||
machine1_config["clan"]["virtualisation"]["graphics"] = False
|
||||
machine1_config["services"]["getty"]["autologinUser"] = "root"
|
||||
machine1_config["services"]["openssh"]["enable"] = True
|
||||
@@ -48,6 +51,7 @@ def test_vm_deployment(
|
||||
"""
|
||||
# machine 2
|
||||
machine2_config = nested_dict()
|
||||
machine2_config["nixpkgs"]["hostPlatform"] = nix_config["system"].value
|
||||
machine2_config["clan"]["virtualisation"]["graphics"] = False
|
||||
machine2_config["services"]["getty"]["autologinUser"] = "root"
|
||||
machine2_config["services"]["openssh"]["enable"] = True
|
||||
@@ -62,6 +66,7 @@ def test_vm_deployment(
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs={"m1_machine": machine1_config, "m2_machine": machine2_config},
|
||||
)
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ def test_vm_persistence(
|
||||
flake = generate_flake(
|
||||
temporary_home,
|
||||
flake_template=CLAN_CORE / "templates" / "minimal",
|
||||
monkeypatch=monkeypatch,
|
||||
machine_configs=config,
|
||||
)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
modulename: _: jsonLib.parseOptions (optionsFromModule modulename) { }
|
||||
) clanModules;
|
||||
|
||||
clanModuleFunctionSchemas = lib.mapAttrsFlatten (
|
||||
clanModuleFunctionSchemas = lib.attrsets.mapAttrsToList (
|
||||
modulename: _:
|
||||
(self.lib.modules.getFrontmatter modulename)
|
||||
// {
|
||||
|
||||
@@ -7,10 +7,19 @@
|
||||
initialized = inputs.nixpkgs.legacyPackages.x86_64-linux.runCommand "minimal-clan-flake" { } ''
|
||||
mkdir $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
|
||||
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 {
|
||||
self = evaled // {
|
||||
@@ -22,7 +31,7 @@
|
||||
{
|
||||
type = "derivation";
|
||||
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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user