Merge pull request 'add factsStore modules' (#839) from fact_store into main
This commit is contained in:
@@ -37,9 +37,9 @@ let
|
|||||||
in
|
in
|
||||||
(machineImports settings)
|
(machineImports settings)
|
||||||
++ [
|
++ [
|
||||||
(nixpkgs.lib.mkOverride 51 extraConfig)
|
|
||||||
settings
|
settings
|
||||||
clan-core.nixosModules.clanCore
|
clan-core.nixosModules.clanCore
|
||||||
|
extraConfig
|
||||||
(machines.${name} or { })
|
(machines.${name} or { })
|
||||||
({
|
({
|
||||||
clanCore.clanName = clanName;
|
clanCore.clanName = clanName;
|
||||||
|
|||||||
@@ -44,6 +44,13 @@
|
|||||||
the directory on the deployment server where secrets are uploaded
|
the directory on the deployment server where secrets are uploaded
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
factsModule = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = ''
|
||||||
|
the python import path to the facts module
|
||||||
|
'';
|
||||||
|
default = "clan_cli.facts.modules.in_repo";
|
||||||
|
};
|
||||||
secretsModule = lib.mkOption {
|
secretsModule = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = ''
|
description = ''
|
||||||
@@ -84,7 +91,7 @@
|
|||||||
# optimization for faster secret generate/upload and machines update
|
# optimization for faster secret generate/upload and machines update
|
||||||
config = {
|
config = {
|
||||||
system.clan.deployment.data = {
|
system.clan.deployment.data = {
|
||||||
inherit (config.system.clan) secretsModule secretsData;
|
inherit (config.system.clan) factsModule secretsModule secretsData;
|
||||||
inherit (config.clan.networking) targetHost buildHost;
|
inherit (config.clan.networking) targetHost buildHost;
|
||||||
inherit (config.clan.deployment) requireExplicitUpdate;
|
inherit (config.clan.deployment) requireExplicitUpdate;
|
||||||
inherit (config.clanCore) secretsUploadDirectory;
|
inherit (config.clanCore) secretsUploadDirectory;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
{ config, lib, pkgs, ... }:
|
||||||
{
|
{
|
||||||
options.clanCore.secretStore = lib.mkOption {
|
options.clanCore.secretStore = lib.mkOption {
|
||||||
type = lib.types.enum [ "sops" "password-store" "custom" ];
|
type = lib.types.enum [ "sops" "password-store" "vm" "custom" ];
|
||||||
default = "sops";
|
default = "sops";
|
||||||
description = ''
|
description = ''
|
||||||
method to store secrets
|
method to store secrets
|
||||||
@@ -150,5 +150,6 @@
|
|||||||
imports = [
|
imports = [
|
||||||
./sops.nix
|
./sops.nix
|
||||||
./password-store.nix
|
./password-store.nix
|
||||||
|
./vm.nix
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
10
nixosModules/clanCore/secrets/vm.nix
Normal file
10
nixosModules/clanCore/secrets/vm.nix
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{ config, lib, ... }:
|
||||||
|
{
|
||||||
|
config = lib.mkIf (config.clanCore.secretStore == "vm") {
|
||||||
|
clanCore.secretsDirectory = "/etc/secrets";
|
||||||
|
clanCore.secretsUploadDirectory = "/etc/secrets";
|
||||||
|
system.clan.secretsModule = "clan_cli.secrets.modules.vm";
|
||||||
|
system.clan.factsModule = "clan_cli.facts.modules.vm";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ in
|
|||||||
|
|
||||||
environment.systemPackages = [ config.clanCore.clanPkgs.zerotier-members ];
|
environment.systemPackages = [ config.clanCore.clanPkgs.zerotier-members ];
|
||||||
})
|
})
|
||||||
(lib.mkIf (config.clanCore.secretsUploadDirectory != null && !cfg.controller.enable && cfg.networkId != null) {
|
(lib.mkIf (!cfg.controller.enable && cfg.networkId != null) {
|
||||||
clanCore.secrets.zerotier = {
|
clanCore.secrets.zerotier = {
|
||||||
facts.zerotier-ip = { };
|
facts.zerotier-ip = { };
|
||||||
facts.zerotier-meshname = { };
|
facts.zerotier-meshname = { };
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
|||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from . import backups, config, flakes, flash, history, machines, secrets, vms
|
from . import backups, config, facts, flakes, flash, history, machines, secrets, vms
|
||||||
from .custom_logger import setup_logging
|
from .custom_logger import setup_logging
|
||||||
from .dirs import get_clan_flake_toplevel
|
from .dirs import get_clan_flake_toplevel
|
||||||
from .errors import ClanCmdError, ClanError
|
from .errors import ClanCmdError, ClanError
|
||||||
@@ -91,6 +91,9 @@ def create_parser(prog: str | None = None) -> argparse.ArgumentParser:
|
|||||||
parser_secrets = subparsers.add_parser("secrets", help="manage secrets")
|
parser_secrets = subparsers.add_parser("secrets", help="manage secrets")
|
||||||
secrets.register_parser(parser_secrets)
|
secrets.register_parser(parser_secrets)
|
||||||
|
|
||||||
|
parser_facts = subparsers.add_parser("facts", help="manage facts")
|
||||||
|
facts.register_parser(parser_facts)
|
||||||
|
|
||||||
parser_machine = subparsers.add_parser(
|
parser_machine = subparsers.add_parser(
|
||||||
"machines", help="Manage machines and their configuration"
|
"machines", help="Manage machines and their configuration"
|
||||||
)
|
)
|
||||||
|
|||||||
21
pkgs/clan-cli/clan_cli/facts/__init__.py
Normal file
21
pkgs/clan-cli/clan_cli/facts/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# !/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from .check import register_check_parser
|
||||||
|
from .list import register_list_parser
|
||||||
|
|
||||||
|
|
||||||
|
# takes a (sub)parser and configures it
|
||||||
|
def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
|
subparser = parser.add_subparsers(
|
||||||
|
title="command",
|
||||||
|
description="the command to run",
|
||||||
|
help="the command to run",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
check_parser = subparser.add_parser("check", help="check if facts are up to date")
|
||||||
|
register_check_parser(check_parser)
|
||||||
|
|
||||||
|
list_parser = subparser.add_parser("list", help="list all facts")
|
||||||
|
register_list_parser(list_parser)
|
||||||
38
pkgs/clan-cli/clan_cli/facts/check.py
Normal file
38
pkgs/clan-cli/clan_cli/facts/check.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import argparse
|
||||||
|
import importlib
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ..machines.machines import Machine
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def check_facts(machine: Machine) -> bool:
|
||||||
|
facts_module = importlib.import_module(machine.facts_module)
|
||||||
|
fact_store = facts_module.FactStore(machine=machine)
|
||||||
|
|
||||||
|
existing_facts = fact_store.get_all()
|
||||||
|
missing_facts = []
|
||||||
|
for service in machine.secrets_data:
|
||||||
|
for fact in machine.secrets_data[service]["facts"]:
|
||||||
|
if fact not in existing_facts.get(service, {}):
|
||||||
|
log.info(f"Fact {fact} for service {service} is missing")
|
||||||
|
missing_facts.append((service, fact))
|
||||||
|
|
||||||
|
if missing_facts:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_command(args: argparse.Namespace) -> None:
|
||||||
|
machine = Machine(name=args.machine, flake=args.flake)
|
||||||
|
if check_facts(machine):
|
||||||
|
print("All facts are present")
|
||||||
|
|
||||||
|
|
||||||
|
def register_check_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
|
parser.add_argument(
|
||||||
|
"machine",
|
||||||
|
help="The machine to check facts for",
|
||||||
|
)
|
||||||
|
parser.set_defaults(func=check_command)
|
||||||
36
pkgs/clan-cli/clan_cli/facts/list.py
Normal file
36
pkgs/clan-cli/clan_cli/facts/list.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import argparse
|
||||||
|
import importlib
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ..machines.machines import Machine
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_facts(machine: Machine) -> dict:
|
||||||
|
facts_module = importlib.import_module(machine.facts_module)
|
||||||
|
fact_store = facts_module.FactStore(machine=machine)
|
||||||
|
|
||||||
|
# for service in machine.secrets_data:
|
||||||
|
# facts[service] = {}
|
||||||
|
# for fact in machine.secrets_data[service]["facts"]:
|
||||||
|
# fact_content = fact_store.get(service, fact)
|
||||||
|
# if fact_content:
|
||||||
|
# facts[service][fact] = fact_content.decode()
|
||||||
|
# else:
|
||||||
|
# log.error(f"Fact {fact} for service {service} is missing")
|
||||||
|
return fact_store.get_all()
|
||||||
|
|
||||||
|
|
||||||
|
def get_command(args: argparse.Namespace) -> None:
|
||||||
|
machine = Machine(name=args.machine, flake=args.flake)
|
||||||
|
print(json.dumps(get_all_facts(machine), indent=4))
|
||||||
|
|
||||||
|
|
||||||
|
def register_list_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
|
parser.add_argument(
|
||||||
|
"machine",
|
||||||
|
help="The machine to print facts for",
|
||||||
|
)
|
||||||
|
parser.set_defaults(func=get_command)
|
||||||
0
pkgs/clan-cli/clan_cli/facts/modules/__init__.py
Normal file
0
pkgs/clan-cli/clan_cli/facts/modules/__init__.py
Normal file
47
pkgs/clan-cli/clan_cli/facts/modules/in_repo.py
Normal file
47
pkgs/clan-cli/clan_cli/facts/modules/in_repo.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from clan_cli.errors import ClanError
|
||||||
|
from clan_cli.machines.machines import Machine
|
||||||
|
|
||||||
|
|
||||||
|
class FactStore:
|
||||||
|
def __init__(self, machine: Machine) -> None:
|
||||||
|
self.machine = machine
|
||||||
|
self.works_remotely = False
|
||||||
|
|
||||||
|
def set(self, _service: str, name: str, value: bytes) -> Path | None:
|
||||||
|
if isinstance(self.machine.flake, Path):
|
||||||
|
fact_path = (
|
||||||
|
self.machine.flake / "machines" / self.machine.name / "facts" / name
|
||||||
|
)
|
||||||
|
fact_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fact_path.touch()
|
||||||
|
fact_path.write_bytes(value)
|
||||||
|
return fact_path
|
||||||
|
else:
|
||||||
|
raise ClanError(
|
||||||
|
f"in_flake fact storage is only supported for local flakes: {self.machine.flake}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def exists(self, _service: str, name: str) -> bool:
|
||||||
|
fact_path = (
|
||||||
|
self.machine.flake_dir / "machines" / self.machine.name / "facts" / name
|
||||||
|
)
|
||||||
|
return fact_path.exists()
|
||||||
|
|
||||||
|
# get a single fact
|
||||||
|
def get(self, _service: str, name: str) -> bytes:
|
||||||
|
fact_path = (
|
||||||
|
self.machine.flake_dir / "machines" / self.machine.name / "facts" / name
|
||||||
|
)
|
||||||
|
return fact_path.read_bytes()
|
||||||
|
|
||||||
|
# get all facts
|
||||||
|
def get_all(self) -> dict[str, dict[str, bytes]]:
|
||||||
|
facts_folder = self.machine.flake_dir / "machines" / self.machine.name / "facts"
|
||||||
|
facts: dict[str, dict[str, bytes]] = {}
|
||||||
|
facts["TODO"] = {}
|
||||||
|
if facts_folder.exists():
|
||||||
|
for fact_path in facts_folder.iterdir():
|
||||||
|
facts["TODO"][fact_path.name] = fact_path.read_bytes()
|
||||||
|
return facts
|
||||||
44
pkgs/clan-cli/clan_cli/facts/modules/vm.py
Normal file
44
pkgs/clan-cli/clan_cli/facts/modules/vm.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from clan_cli.dirs import vm_state_dir
|
||||||
|
from clan_cli.errors import ClanError
|
||||||
|
from clan_cli.machines.machines import Machine
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FactStore:
|
||||||
|
def __init__(self, machine: Machine) -> None:
|
||||||
|
self.machine = machine
|
||||||
|
self.works_remotely = False
|
||||||
|
self.dir = vm_state_dir(str(machine.flake), machine.name) / "facts"
|
||||||
|
log.debug(f"FactStore initialized with dir {self.dir}")
|
||||||
|
|
||||||
|
def exists(self, service: str, name: str) -> bool:
|
||||||
|
fact_path = self.dir / service / name
|
||||||
|
return fact_path.exists()
|
||||||
|
|
||||||
|
def set(self, service: str, name: str, value: bytes) -> Path | None:
|
||||||
|
fact_path = self.dir / service / name
|
||||||
|
fact_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fact_path.write_bytes(value)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# get a single fact
|
||||||
|
def get(self, service: str, name: str) -> bytes:
|
||||||
|
fact_path = self.dir / service / name
|
||||||
|
if fact_path.exists():
|
||||||
|
return fact_path.read_bytes()
|
||||||
|
raise ClanError(f"Fact {name} for service {service} not found")
|
||||||
|
|
||||||
|
# get all facts
|
||||||
|
def get_all(self) -> dict[str, dict[str, bytes]]:
|
||||||
|
facts: dict[str, dict[str, bytes]] = {}
|
||||||
|
if self.dir.exists():
|
||||||
|
for service in self.dir.iterdir():
|
||||||
|
facts[service.name] = {}
|
||||||
|
for fact in service.iterdir():
|
||||||
|
facts[service.name][fact.name] = fact.read_bytes()
|
||||||
|
|
||||||
|
return facts
|
||||||
@@ -96,6 +96,10 @@ class Machine:
|
|||||||
def secrets_module(self) -> str:
|
def secrets_module(self) -> str:
|
||||||
return self.deployment_info["secretsModule"]
|
return self.deployment_info["secretsModule"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def facts_module(self) -> str:
|
||||||
|
return self.deployment_info["factsModule"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def secrets_data(self) -> dict:
|
def secrets_data(self) -> dict:
|
||||||
if self.deployment_info["secretsData"]:
|
if self.deployment_info["secretsData"]:
|
||||||
@@ -151,6 +155,7 @@ class Machine:
|
|||||||
attr: str,
|
attr: str,
|
||||||
extra_config: None | dict = None,
|
extra_config: None | dict = None,
|
||||||
impure: bool = False,
|
impure: bool = False,
|
||||||
|
nix_options: list[str] = [],
|
||||||
) -> str | Path:
|
) -> str | Path:
|
||||||
"""
|
"""
|
||||||
Build the machine and return the path to the result
|
Build the machine and return the path to the result
|
||||||
@@ -184,17 +189,15 @@ class Machine:
|
|||||||
if extra_config is not None:
|
if extra_config is not None:
|
||||||
metadata = nix_metadata(self.flake_dir)
|
metadata = nix_metadata(self.flake_dir)
|
||||||
url = metadata["url"]
|
url = metadata["url"]
|
||||||
if "dirtyRev" in metadata:
|
if "dirtyRevision" in metadata:
|
||||||
if not impure:
|
# if not impure:
|
||||||
raise ClanError(
|
# raise ClanError(
|
||||||
"The machine has a dirty revision, and impure mode is not allowed"
|
# "The machine has a dirty revision, and impure mode is not allowed"
|
||||||
)
|
# )
|
||||||
else:
|
# else:
|
||||||
|
# args += ["--impure"]
|
||||||
args += ["--impure"]
|
args += ["--impure"]
|
||||||
|
|
||||||
if "dirtyRev" in nix_metadata(self.flake_dir):
|
|
||||||
dirty_rev = nix_metadata(self.flake_dir)["dirtyRevision"]
|
|
||||||
url = f"{url}?rev={dirty_rev}"
|
|
||||||
args += [
|
args += [
|
||||||
"--expr",
|
"--expr",
|
||||||
f"""
|
f"""
|
||||||
@@ -216,7 +219,8 @@ class Machine:
|
|||||||
else:
|
else:
|
||||||
flake = self.flake
|
flake = self.flake
|
||||||
args += [
|
args += [
|
||||||
f'{flake}#clanInternals.machines."{system}".{self.name}.{attr}'
|
f'{flake}#clanInternals.machines."{system}".{self.name}.{attr}',
|
||||||
|
*nix_options,
|
||||||
]
|
]
|
||||||
|
|
||||||
if method == "eval":
|
if method == "eval":
|
||||||
@@ -234,6 +238,7 @@ class Machine:
|
|||||||
refresh: bool = False,
|
refresh: bool = False,
|
||||||
extra_config: None | dict = None,
|
extra_config: None | dict = None,
|
||||||
impure: bool = False,
|
impure: bool = False,
|
||||||
|
nix_options: list[str] = [],
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
eval a nix attribute of the machine
|
eval a nix attribute of the machine
|
||||||
@@ -242,7 +247,7 @@ class Machine:
|
|||||||
if attr in self.eval_cache and not refresh and extra_config is None:
|
if attr in self.eval_cache and not refresh and extra_config is None:
|
||||||
return self.eval_cache[attr]
|
return self.eval_cache[attr]
|
||||||
|
|
||||||
output = self.nix("eval", attr, extra_config, impure)
|
output = self.nix("eval", attr, extra_config, impure, nix_options)
|
||||||
if isinstance(output, str):
|
if isinstance(output, str):
|
||||||
self.eval_cache[attr] = output
|
self.eval_cache[attr] = output
|
||||||
return output
|
return output
|
||||||
@@ -255,6 +260,7 @@ class Machine:
|
|||||||
refresh: bool = False,
|
refresh: bool = False,
|
||||||
extra_config: None | dict = None,
|
extra_config: None | dict = None,
|
||||||
impure: bool = False,
|
impure: bool = False,
|
||||||
|
nix_options: list[str] = [],
|
||||||
) -> Path:
|
) -> Path:
|
||||||
"""
|
"""
|
||||||
build a nix attribute of the machine
|
build a nix attribute of the machine
|
||||||
@@ -264,7 +270,7 @@ class Machine:
|
|||||||
if attr in self.build_cache and not refresh and extra_config is None:
|
if attr in self.build_cache and not refresh and extra_config is None:
|
||||||
return self.build_cache[attr]
|
return self.build_cache[attr]
|
||||||
|
|
||||||
output = self.nix("build", attr, extra_config, impure)
|
output = self.nix("build", attr, extra_config, impure, nix_options)
|
||||||
if isinstance(output, Path):
|
if isinstance(output, Path):
|
||||||
self.build_cache[attr] = output
|
self.build_cache[attr] = output
|
||||||
return output
|
return output
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ log = logging.getLogger(__name__)
|
|||||||
def check_secrets(machine: Machine) -> bool:
|
def check_secrets(machine: Machine) -> bool:
|
||||||
secrets_module = importlib.import_module(machine.secrets_module)
|
secrets_module = importlib.import_module(machine.secrets_module)
|
||||||
secret_store = secrets_module.SecretStore(machine=machine)
|
secret_store = secrets_module.SecretStore(machine=machine)
|
||||||
|
facts_module = importlib.import_module(machine.facts_module)
|
||||||
|
fact_store = facts_module.FactStore(machine=machine)
|
||||||
|
|
||||||
missing_secrets = []
|
missing_secrets = []
|
||||||
missing_facts = []
|
missing_facts = []
|
||||||
@@ -19,11 +21,13 @@ def check_secrets(machine: Machine) -> bool:
|
|||||||
log.info(f"Secret {secret} for service {service} is missing")
|
log.info(f"Secret {secret} for service {service} is missing")
|
||||||
missing_secrets.append((service, secret))
|
missing_secrets.append((service, secret))
|
||||||
|
|
||||||
for fact in machine.secrets_data[service]["facts"].values():
|
for fact in machine.secrets_data[service]["facts"]:
|
||||||
if not (machine.flake / fact).exists():
|
if not fact_store.exists(service, fact):
|
||||||
log.info(f"Fact {fact} for service {service} is missing")
|
log.info(f"Fact {fact} for service {service} is missing")
|
||||||
missing_facts.append((service, fact))
|
missing_facts.append((service, fact))
|
||||||
|
|
||||||
|
log.debug(f"missing_secrets: {missing_secrets}")
|
||||||
|
log.debug(f"missing_facts: {missing_facts}")
|
||||||
if missing_secrets or missing_facts:
|
if missing_secrets or missing_facts:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import argparse
|
|||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
@@ -21,11 +20,15 @@ def generate_secrets(machine: Machine) -> None:
|
|||||||
secrets_module = importlib.import_module(machine.secrets_module)
|
secrets_module = importlib.import_module(machine.secrets_module)
|
||||||
secret_store = secrets_module.SecretStore(machine=machine)
|
secret_store = secrets_module.SecretStore(machine=machine)
|
||||||
|
|
||||||
|
facts_module = importlib.import_module(machine.facts_module)
|
||||||
|
fact_store = facts_module.FactStore(machine=machine)
|
||||||
|
|
||||||
with TemporaryDirectory() as d:
|
with TemporaryDirectory() as d:
|
||||||
for service in machine.secrets_data:
|
for service in machine.secrets_data:
|
||||||
tmpdir = Path(d) / service
|
tmpdir = Path(d) / service
|
||||||
# check if all secrets exist and generate them if at least one is missing
|
# check if all secrets exist and generate them if at least one is missing
|
||||||
needs_regeneration = not check_secrets(machine)
|
needs_regeneration = not check_secrets(machine)
|
||||||
|
log.debug(f"{service} needs_regeneration: {needs_regeneration}")
|
||||||
if needs_regeneration:
|
if needs_regeneration:
|
||||||
if not isinstance(machine.flake, Path):
|
if not isinstance(machine.flake, Path):
|
||||||
msg = f"flake is not a Path: {machine.flake}"
|
msg = f"flake is not a Path: {machine.flake}"
|
||||||
@@ -78,16 +81,15 @@ def generate_secrets(machine: Machine) -> None:
|
|||||||
files_to_commit.append(secret_path)
|
files_to_commit.append(secret_path)
|
||||||
|
|
||||||
# store facts
|
# store facts
|
||||||
for name, fact_path in machine.secrets_data[service]["facts"].items():
|
for name in machine.secrets_data[service]["facts"]:
|
||||||
fact_file = facts_dir / name
|
fact_file = facts_dir / name
|
||||||
if not fact_file.is_file():
|
if not fact_file.is_file():
|
||||||
msg = f"did not generate a file for '{name}' when running the following command:\n"
|
msg = f"did not generate a file for '{name}' when running the following command:\n"
|
||||||
msg += machine.secrets_data[service]["generator"]
|
msg += machine.secrets_data[service]["generator"]
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
fact_path = machine.flake / fact_path
|
fact_file = fact_store.set(service, name, fact_file.read_bytes())
|
||||||
fact_path.parent.mkdir(parents=True, exist_ok=True)
|
if fact_file:
|
||||||
shutil.copyfile(fact_file, fact_path)
|
files_to_commit.append(fact_file)
|
||||||
files_to_commit.append(fact_path)
|
|
||||||
commit_files(
|
commit_files(
|
||||||
files_to_commit,
|
files_to_commit,
|
||||||
machine.flake_dir,
|
machine.flake_dir,
|
||||||
|
|||||||
31
pkgs/clan-cli/clan_cli/secrets/modules/vm.py
Normal file
31
pkgs/clan-cli/clan_cli/secrets/modules/vm.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from clan_cli.dirs import vm_state_dir
|
||||||
|
from clan_cli.machines.machines import Machine
|
||||||
|
|
||||||
|
|
||||||
|
class SecretStore:
|
||||||
|
def __init__(self, machine: Machine) -> None:
|
||||||
|
self.machine = machine
|
||||||
|
self.dir = vm_state_dir(str(machine.flake), machine.name) / "secrets"
|
||||||
|
self.dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def set(self, service: str, name: str, value: bytes) -> Path | None:
|
||||||
|
secret_file = self.dir / service / name
|
||||||
|
secret_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
secret_file.write_bytes(value)
|
||||||
|
return None # we manage the files outside of the git repo
|
||||||
|
|
||||||
|
def get(self, service: str, name: str) -> bytes:
|
||||||
|
secret_file = self.dir / service / name
|
||||||
|
return secret_file.read_bytes()
|
||||||
|
|
||||||
|
def exists(self, service: str, name: str) -> bool:
|
||||||
|
return (self.dir / service / name).exists()
|
||||||
|
|
||||||
|
def upload(self, output_dir: Path) -> None:
|
||||||
|
if os.path.exists(output_dir):
|
||||||
|
shutil.rmtree(output_dir)
|
||||||
|
shutil.copytree(self.dir, output_dir)
|
||||||
@@ -15,10 +15,10 @@ from pathlib import Path
|
|||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
from ..cmd import Log, run
|
from ..cmd import Log, run
|
||||||
from ..dirs import machine_gcroot, module_root, user_cache_dir, vm_state_dir
|
from ..dirs import module_root, user_cache_dir, vm_state_dir
|
||||||
from ..errors import ClanError
|
from ..errors import ClanError
|
||||||
from ..machines.machines import Machine
|
from ..machines.machines import Machine
|
||||||
from ..nix import nix_build, nix_config, nix_shell
|
from ..nix import nix_shell
|
||||||
from ..secrets.generate import generate_secrets
|
from ..secrets.generate import generate_secrets
|
||||||
from .inspect import VmConfig, inspect_vm
|
from .inspect import VmConfig, inspect_vm
|
||||||
|
|
||||||
@@ -153,26 +153,39 @@ def qemu_command(
|
|||||||
return QemuCommand(command, vsock_cid=vsock_cid)
|
return QemuCommand(command, vsock_cid=vsock_cid)
|
||||||
|
|
||||||
|
|
||||||
|
def facts_to_nixos_config(facts: dict[str, dict[str, bytes]]) -> dict:
|
||||||
|
nixos_config: dict = {}
|
||||||
|
nixos_config["clanCore"] = {}
|
||||||
|
nixos_config["clanCore"]["secrets"] = {}
|
||||||
|
for service, service_facts in facts.items():
|
||||||
|
nixos_config["clanCore"]["secrets"][service] = {}
|
||||||
|
nixos_config["clanCore"]["secrets"][service]["facts"] = {}
|
||||||
|
for fact, value in service_facts.items():
|
||||||
|
nixos_config["clanCore"]["secrets"][service]["facts"][fact] = {
|
||||||
|
"value": value.decode()
|
||||||
|
}
|
||||||
|
return nixos_config
|
||||||
|
|
||||||
|
|
||||||
# TODO move this to the Machines class
|
# TODO move this to the Machines class
|
||||||
def build_vm(
|
def build_vm(
|
||||||
machine: Machine, vm: VmConfig, nix_options: list[str] = []
|
machine: Machine, vm: VmConfig, tmpdir: Path, nix_options: list[str]
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
config = nix_config()
|
secrets_dir = get_secrets(machine, tmpdir)
|
||||||
system = config["system"]
|
|
||||||
|
|
||||||
clan_dir = machine.flake
|
facts_module = importlib.import_module(machine.facts_module)
|
||||||
cmd = nix_build(
|
fact_store = facts_module.FactStore(machine=machine)
|
||||||
[
|
facts = fact_store.get_all()
|
||||||
f'{clan_dir}#clanInternals.machines."{system}"."{machine.name}".config.system.clan.vm.create',
|
|
||||||
*nix_options,
|
nixos_config_file = machine.build_nix(
|
||||||
],
|
"config.system.clan.vm.create",
|
||||||
machine_gcroot(flake_url=str(vm.flake_url)) / f"vm-{machine.name}",
|
extra_config=facts_to_nixos_config(facts),
|
||||||
)
|
nix_options=nix_options,
|
||||||
proc = run(
|
|
||||||
cmd, log=Log.BOTH, error_msg=f"Could not build vm config for {machine.name}"
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
return json.loads(Path(proc.stdout.strip()).read_text())
|
vm_data = json.loads(Path(nixos_config_file).read_text())
|
||||||
|
vm_data["secrets_dir"] = str(secrets_dir)
|
||||||
|
return vm_data
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
raise ClanError(f"Failed to parse vm config: {e}")
|
raise ClanError(f"Failed to parse vm config: {e}")
|
||||||
|
|
||||||
@@ -182,16 +195,13 @@ def get_secrets(
|
|||||||
tmpdir: Path,
|
tmpdir: Path,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
secrets_dir = tmpdir / "secrets"
|
secrets_dir = tmpdir / "secrets"
|
||||||
secrets_dir.mkdir(exist_ok=True)
|
secrets_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
secrets_module = importlib.import_module(machine.secrets_module)
|
secrets_module = importlib.import_module(machine.secrets_module)
|
||||||
secret_store = secrets_module.SecretStore(machine=machine)
|
secret_store = secrets_module.SecretStore(machine=machine)
|
||||||
|
|
||||||
# Only generate secrets for local clans
|
# TODO Only generate secrets for local clans
|
||||||
if isinstance(machine.flake, Path) and machine.flake.is_dir():
|
|
||||||
generate_secrets(machine)
|
generate_secrets(machine)
|
||||||
else:
|
|
||||||
log.warning("won't generate secrets for non local clan")
|
|
||||||
|
|
||||||
secret_store.upload(secrets_dir)
|
secret_store.upload(secrets_dir)
|
||||||
return secrets_dir
|
return secrets_dir
|
||||||
@@ -302,20 +312,19 @@ def run_vm(vm: VmConfig, nix_options: list[str] = []) -> None:
|
|||||||
machine = Machine(vm.machine_name, vm.flake_url)
|
machine = Machine(vm.machine_name, vm.flake_url)
|
||||||
log.debug(f"Creating VM for {machine}")
|
log.debug(f"Creating VM for {machine}")
|
||||||
|
|
||||||
# TODO: We should get this from the vm argument
|
|
||||||
nixos_config = build_vm(machine, vm, nix_options)
|
|
||||||
|
|
||||||
# store the temporary rootfs inside XDG_CACHE_HOME on the host
|
# store the temporary rootfs inside XDG_CACHE_HOME on the host
|
||||||
# otherwise, when using /tmp, we risk running out of memory
|
# otherwise, when using /tmp, we risk running out of memory
|
||||||
cache = user_cache_dir() / "clan"
|
cache = user_cache_dir() / "clan"
|
||||||
cache.mkdir(exist_ok=True)
|
cache.mkdir(exist_ok=True)
|
||||||
with TemporaryDirectory(dir=cache) as cachedir, TemporaryDirectory() as sockets:
|
with TemporaryDirectory(dir=cache) as cachedir, TemporaryDirectory() as sockets:
|
||||||
tmpdir = Path(cachedir)
|
tmpdir = Path(cachedir)
|
||||||
|
|
||||||
|
# TODO: We should get this from the vm argument
|
||||||
|
nixos_config = build_vm(machine, vm, tmpdir, nix_options)
|
||||||
|
|
||||||
xchg_dir = tmpdir / "xchg"
|
xchg_dir = tmpdir / "xchg"
|
||||||
xchg_dir.mkdir(exist_ok=True)
|
xchg_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
secrets_dir = get_secrets(machine, tmpdir)
|
|
||||||
|
|
||||||
state_dir = vm_state_dir(str(vm.flake_url), machine.name)
|
state_dir = vm_state_dir(str(vm.flake_url), machine.name)
|
||||||
state_dir.mkdir(parents=True, exist_ok=True)
|
state_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
@@ -350,7 +359,7 @@ def run_vm(vm: VmConfig, nix_options: list[str] = []) -> None:
|
|||||||
vm,
|
vm,
|
||||||
nixos_config,
|
nixos_config,
|
||||||
xchg_dir=xchg_dir,
|
xchg_dir=xchg_dir,
|
||||||
secrets_dir=secrets_dir,
|
secrets_dir=Path(nixos_config["secrets_dir"]),
|
||||||
rootfs_img=rootfs_img,
|
rootfs_img=rootfs_img,
|
||||||
state_img=state_img,
|
state_img=state_img,
|
||||||
virtiofsd_socket=virtiofsd_socket,
|
virtiofsd_socket=virtiofsd_socket,
|
||||||
|
|||||||
Reference in New Issue
Block a user