Inventory: init first implementation #1638
@ -3,7 +3,7 @@ let
|
|||||||
clanDir = config.clan.core.clanDir;
|
clanDir = config.clan.core.clanDir;
|
||||||
machineDir = clanDir + "/machines/";
|
machineDir = clanDir + "/machines/";
|
||||||
in
|
in
|
||||||
{
|
lib.warn "This module is deprecated use the service via the inventory interface instead." {
|
||||||
imports = [ ../borgbackup ];
|
imports = [ ../borgbackup ];
|
||||||
|
|
||||||
options.clan.borgbackup-static = {
|
options.clan.borgbackup-static = {
|
||||||
|
@ -27,8 +27,55 @@ let
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
# Each .nix file in the roles directory is a role
|
||||||
|
# TODO: Helper function to set available roles within module meta.
|
||||||
|
roles =
|
||||||
|
if builtins.pathExists ./roles then
|
||||||
|
lib.pipe ./roles [
|
||||||
|
builtins.readDir
|
||||||
|
(lib.filterAttrs (_n: v: v == "regular"))
|
||||||
|
lib.attrNames
|
||||||
|
(map (fileName: lib.removeSuffix ".nix" fileName))
|
||||||
|
]
|
||||||
|
else
|
||||||
|
null;
|
||||||
|
|
||||||
|
# TODO: make this an interface of every module
|
||||||
|
# Maybe load from readme.md
|
||||||
|
metaInfoOption = lib.mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
description = ''
|
||||||
|
Meta is used to retrieve information about this module.
|
||||||
|
|
||||||
|
- `availableRoles` is a list of roles that can be assigned via the inventory.
|
||||||
|
- `category` is used to group services in the clan marketplace.
|
||||||
|
- `description` is a short description of the service for the clan marketplace.
|
||||||
|
'';
|
||||||
|
default = {
|
||||||
|
description = "Borgbackup is a backup program. Optionally, it supports compression and authenticated encryption.";
|
||||||
|
availableRoles = roles;
|
||||||
|
category = "backup";
|
||||||
|
};
|
||||||
|
type = lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
description = lib.mkOption { type = lib.types.str; };
|
||||||
|
availableRoles = lib.mkOption { type = lib.types.nullOr (lib.types.listOf lib.types.str); };
|
||||||
|
category = lib.mkOption {
|
||||||
|
description = "A category for the service. This is used to group services in the clan ui";
|
||||||
|
type = lib.types.enum [
|
||||||
|
"backup"
|
||||||
|
"network"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|
||||||
|
options.clan.borgbackup.meta = metaInfoOption;
|
||||||
|
|
||||||
options.clan.borgbackup.destinations = lib.mkOption {
|
options.clan.borgbackup.destinations = lib.mkOption {
|
||||||
type = lib.types.attrsOf (
|
type = lib.types.attrsOf (
|
||||||
lib.types.submodule (
|
lib.types.submodule (
|
||||||
@ -76,7 +123,7 @@ in
|
|||||||
lib.nameValuePair "borgbackup-job-${dest.name}" {
|
lib.nameValuePair "borgbackup-job-${dest.name}" {
|
||||||
# since borgbackup mounts the system read-only, we need to run in a ExecStartPre script, so we can generate additional files.
|
# since borgbackup mounts the system read-only, we need to run in a ExecStartPre script, so we can generate additional files.
|
||||||
serviceConfig.ExecStartPre = [
|
serviceConfig.ExecStartPre = [
|
||||||
(''+${pkgs.writeShellScript "borgbackup-job-${dest.name}-pre-backup-commands" preBackupScript}'')
|
''+${pkgs.writeShellScript "borgbackup-job-${dest.name}-pre-backup-commands" preBackupScript}''
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
) cfg.destinations;
|
) cfg.destinations;
|
||||||
|
30
clanModules/borgbackup/roles/client.nix
Normal file
30
clanModules/borgbackup/roles/client.nix
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{ config, lib, ... }:
|
||||||
|
let
|
||||||
|
instances = config.clan.inventory.services.borgbackup;
|
||||||
|
# roles = { ${role_name} :: { machines :: [string] } }
|
||||||
|
allServers = lib.foldlAttrs (
|
||||||
|
acc: _instanceName: instanceConfig:
|
||||||
|
acc
|
||||||
|
++ (
|
||||||
|
if builtins.elem machineName instanceConfig.roles.client.machines then
|
||||||
|
instanceConfig.roles.server.machines
|
||||||
|
else
|
||||||
|
[ ]
|
||||||
|
)
|
||||||
|
) [ ] instances;
|
||||||
|
|
||||||
|
inherit (config.clan.core) machineName;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config.clan.borgbackup.destinations =
|
||||||
|
let
|
||||||
|
|
||||||
|
destinations = builtins.map (serverName: {
|
||||||
|
name = serverName;
|
||||||
|
value = {
|
||||||
|
repo = "borg@${serverName}:/var/lib/borgbackup/${machineName}";
|
||||||
|
};
|
||||||
|
}) allServers;
|
||||||
|
in
|
||||||
|
(builtins.listToAttrs destinations);
|
||||||
|
}
|
45
clanModules/borgbackup/roles/server.nix
Normal file
45
clanModules/borgbackup/roles/server.nix
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{ config, lib, ... }:
|
||||||
|
let
|
||||||
|
clanDir = config.clan.core.clanDir;
|
||||||
|
machineDir = clanDir + "/machines/";
|
||||||
|
inherit (config.clan.core) machineName;
|
||||||
|
|
||||||
|
instances = config.clan.inventory.services.borgbackup;
|
||||||
|
|
||||||
|
# roles = { ${role_name} :: { machines :: [string] } }
|
||||||
|
|
||||||
|
allClients = lib.foldlAttrs (
|
||||||
|
acc: _instanceName: instanceConfig:
|
||||||
|
acc
|
||||||
|
++ (
|
||||||
|
if (builtins.elem machineName instanceConfig.roles.server.machines) then
|
||||||
|
instanceConfig.roles.client.machines
|
||||||
|
else
|
||||||
|
[ ]
|
||||||
|
)
|
||||||
|
) [ ] instances;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config.services.borgbackup.repos =
|
||||||
|
let
|
||||||
|
borgbackupIpMachinePath = machines: machineDir + machines + "/facts/borgbackup.ssh.pub";
|
||||||
|
machinesMaybeKey = builtins.map (
|
||||||
|
machine:
|
||||||
|
let
|
||||||
|
fullPath = borgbackupIpMachinePath machine;
|
||||||
|
in
|
||||||
|
if builtins.pathExists fullPath then machine else null
|
||||||
|
) allClients;
|
||||||
|
|
||||||
|
machinesWithKey = lib.filter (x: x != null) machinesMaybeKey;
|
||||||
|
|
||||||
|
hosts = builtins.map (machine: {
|
||||||
|
name = machine;
|
||||||
|
value = {
|
||||||
|
path = "/var/lib/borgbackup/${machine}";
|
||||||
|
authorizedKeys = [ (builtins.readFile (borgbackupIpMachinePath machine)) ];
|
||||||
|
};
|
||||||
|
}) machinesWithKey;
|
||||||
|
in
|
||||||
|
if (builtins.listToAttrs hosts) != [ ] then builtins.listToAttrs hosts else { };
|
||||||
|
}
|
@ -49,7 +49,7 @@ nav:
|
|||||||
- Mesh VPN: getting-started/mesh-vpn.md
|
- Mesh VPN: getting-started/mesh-vpn.md
|
||||||
- Backup & Restore: getting-started/backups.md
|
- Backup & Restore: getting-started/backups.md
|
||||||
- Flake-parts: getting-started/flake-parts.md
|
- Flake-parts: getting-started/flake-parts.md
|
||||||
- Modules:
|
- Reference:
|
||||||
- Clan Modules:
|
- Clan Modules:
|
||||||
- reference/clanModules/borgbackup-static.md
|
- reference/clanModules/borgbackup-static.md
|
||||||
- reference/clanModules/borgbackup.md
|
- reference/clanModules/borgbackup.md
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
clanModulesFileInfo = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModules);
|
clanModulesFileInfo = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModules);
|
||||||
clanModulesReadmes = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModulesReadmes);
|
clanModulesReadmes = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModulesReadmes);
|
||||||
|
clanModulesMeta = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModulesMeta);
|
||||||
|
|
||||||
# Simply evaluated options (JSON)
|
# Simply evaluated options (JSON)
|
||||||
renderOptions =
|
renderOptions =
|
||||||
@ -54,6 +55,7 @@
|
|||||||
# A file that contains the links to all clanModule docs
|
# A file that contains the links to all clanModule docs
|
||||||
export CLAN_MODULES=${clanModulesFileInfo}
|
export CLAN_MODULES=${clanModulesFileInfo}
|
||||||
export CLAN_MODULES_READMES=${clanModulesReadmes}
|
export CLAN_MODULES_READMES=${clanModulesReadmes}
|
||||||
|
export CLAN_MODULES_META=${clanModulesMeta}
|
||||||
|
|
||||||
mkdir $out
|
mkdir $out
|
||||||
|
|
||||||
|
@ -43,11 +43,16 @@ let
|
|||||||
module_name: _module: self.lib.modules.getReadme module_name
|
module_name: _module: self.lib.modules.getReadme module_name
|
||||||
) clanModules;
|
) clanModules;
|
||||||
|
|
||||||
|
clanModulesMeta = builtins.mapAttrs (
|
||||||
|
module_name: _module:
|
||||||
|
(self.lib.evalClanModules [ module_name ]).config.clan.${module_name}.meta or { }
|
||||||
|
) clanModules;
|
||||||
|
|
||||||
# clanCore docs
|
# clanCore docs
|
||||||
clanCoreDocs = (evalDocs (getOptions [ ]).clan.core).optionsJSON;
|
clanCoreDocs = (evalDocs (getOptions [ ]).clan.core).optionsJSON;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
inherit clanModulesReadmes;
|
inherit clanModulesReadmes clanModulesMeta;
|
||||||
clanCore = clanCoreDocs;
|
clanCore = clanCoreDocs;
|
||||||
clanModules = clanModulesDocs;
|
clanModules = clanModulesDocs;
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,8 @@ from typing import Any
|
|||||||
CLAN_CORE = os.getenv("CLAN_CORE")
|
CLAN_CORE = os.getenv("CLAN_CORE")
|
||||||
CLAN_MODULES = os.environ.get("CLAN_MODULES")
|
CLAN_MODULES = os.environ.get("CLAN_MODULES")
|
||||||
CLAN_MODULES_READMES = os.environ.get("CLAN_MODULES_READMES")
|
CLAN_MODULES_READMES = os.environ.get("CLAN_MODULES_READMES")
|
||||||
|
CLAN_MODULES_META = os.environ.get("CLAN_MODULES_META")
|
||||||
|
|
||||||
|
|
||||||
OUT = os.environ.get("out")
|
OUT = os.environ.get("out")
|
||||||
|
|
||||||
@ -76,7 +78,9 @@ def render_option(name: str, option: dict[str, Any], level: int = 3) -> str:
|
|||||||
|
|
||||||
res = f"""
|
res = f"""
|
||||||
{"#" * level} {sanitize(name)}
|
{"#" * level} {sanitize(name)}
|
||||||
{"Readonly" if read_only else ""}
|
|
||||||
|
{"**Readonly**" if read_only else ""}
|
||||||
|
|
||||||
{option.get("description", "No description available.")}
|
{option.get("description", "No description available.")}
|
||||||
|
|
||||||
**Type**: `{option["type"]}`
|
**Type**: `{option["type"]}`
|
||||||
@ -188,6 +192,35 @@ def produce_clan_core_docs() -> None:
|
|||||||
of.write(output)
|
of.write(output)
|
||||||
|
|
||||||
|
|
||||||
|
def render_meta(meta: dict[str, Any], module_name: str) -> str:
|
||||||
|
roles = meta.get("availableRoles", None)
|
||||||
|
|
||||||
|
if roles:
|
||||||
|
roles_list = "\n".join([f" - `{r}`" for r in roles])
|
||||||
|
return f"""
|
||||||
|
???+ tip "Inventory (WIP)"
|
||||||
|
|
||||||
|
Predefined roles:
|
||||||
|
|
||||||
|
{roles_list}
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```{{.nix hl_lines="4"}}
|
||||||
|
buildClan {{
|
||||||
|
inventory.services = {{
|
||||||
|
{module_name}.instance_1 = {{
|
||||||
|
roles.{roles[0]}.machines = [ "sara_machine" ];
|
||||||
|
# ...
|
||||||
|
}};
|
||||||
|
}};
|
||||||
|
}}
|
||||||
|
```
|
||||||
|
|
||||||
|
"""
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def produce_clan_modules_docs() -> None:
|
def produce_clan_modules_docs() -> None:
|
||||||
if not CLAN_MODULES:
|
if not CLAN_MODULES:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@ -198,6 +231,11 @@ def produce_clan_modules_docs() -> None:
|
|||||||
f"Environment variables are not set correctly: $CLAN_MODULES_READMES={CLAN_MODULES_READMES}"
|
f"Environment variables are not set correctly: $CLAN_MODULES_READMES={CLAN_MODULES_READMES}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not CLAN_MODULES_META:
|
||||||
|
raise ValueError(
|
||||||
|
f"Environment variables are not set correctly: $CLAN_MODULES_META={CLAN_MODULES_META}"
|
||||||
|
)
|
||||||
|
|
||||||
if not OUT:
|
if not OUT:
|
||||||
raise ValueError(f"Environment variables are not set correctly: $out={OUT}")
|
raise ValueError(f"Environment variables are not set correctly: $out={OUT}")
|
||||||
|
|
||||||
@ -207,6 +245,10 @@ def produce_clan_modules_docs() -> None:
|
|||||||
with open(CLAN_MODULES_READMES) as readme:
|
with open(CLAN_MODULES_READMES) as readme:
|
||||||
readme_map: dict[str, str] = json.load(readme)
|
readme_map: dict[str, str] = json.load(readme)
|
||||||
|
|
||||||
|
with open(CLAN_MODULES_META) as f:
|
||||||
|
meta_map: dict[str, Any] = json.load(f)
|
||||||
|
print(meta_map)
|
||||||
|
|
||||||
# {'borgbackup': '/nix/store/hi17dwgy7963ddd4ijh81fv0c9sbh8sw-options.json', ... }
|
# {'borgbackup': '/nix/store/hi17dwgy7963ddd4ijh81fv0c9sbh8sw-options.json', ... }
|
||||||
for module_name, options_file in links.items():
|
for module_name, options_file in links.items():
|
||||||
with open(Path(options_file) / "share/doc/nixos/options.json") as f:
|
with open(Path(options_file) / "share/doc/nixos/options.json") as f:
|
||||||
@ -217,6 +259,11 @@ def produce_clan_modules_docs() -> None:
|
|||||||
if readme_map.get(module_name, None):
|
if readme_map.get(module_name, None):
|
||||||
output += f"{readme_map[module_name]}\n"
|
output += f"{readme_map[module_name]}\n"
|
||||||
|
|
||||||
|
# Add meta information:
|
||||||
|
# - Inventory implementation status
|
||||||
|
if meta_map.get(module_name, None):
|
||||||
|
output += render_meta(meta_map.get(module_name, {}), module_name)
|
||||||
|
|
||||||
output += module_usage(module_name)
|
output += module_usage(module_name)
|
||||||
|
|
||||||
output += options_head if len(options.items()) else ""
|
output += options_head if len(options.items()) else ""
|
||||||
|
@ -53,8 +53,6 @@
|
|||||||
./nixosModules/flake-module.nix
|
./nixosModules/flake-module.nix
|
||||||
./pkgs/flake-module.nix
|
./pkgs/flake-module.nix
|
||||||
./templates/flake-module.nix
|
./templates/flake-module.nix
|
||||||
|
|
||||||
./inventory/flake-module.nix
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -13,7 +13,6 @@ let
|
|||||||
inherit lib clan-core;
|
inherit lib clan-core;
|
||||||
inherit (inputs) nixpkgs;
|
inherit (inputs) nixpkgs;
|
||||||
};
|
};
|
||||||
|
|
||||||
cfg = config.clan;
|
cfg = config.clan;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@ -91,6 +90,9 @@ in
|
|||||||
clanInternals = lib.mkOption {
|
clanInternals = lib.mkOption {
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = {
|
options = {
|
||||||
|
inventory = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
|
||||||
|
inventoryFile = lib.mkOption { type = lib.types.unspecified; };
|
||||||
|
|
||||||
meta = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
|
meta = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
|
||||||
all-machines-json = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
|
all-machines-json = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
|
||||||
machines = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); };
|
machines = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); };
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
# Inventory
|
|
||||||
|
|
||||||
This part provides a specification for the inventory.
|
|
||||||
|
|
||||||
It is used for design phase and as validation helper.
|
|
||||||
|
|
||||||
> Cue is less verbose and easier to understand and maintain than json-schema.
|
|
||||||
> Json-schema, if needed can be easily generated on-the fly.
|
|
||||||
|
|
||||||
## Checking validity
|
|
||||||
|
|
||||||
Directly check a json against the schema
|
|
||||||
|
|
||||||
`cue vet inventory.json root.cue -d '#Root'`
|
|
||||||
|
|
||||||
## Json schema
|
|
||||||
|
|
||||||
Export the json-schema i.e. for usage in python / javascript / nix
|
|
||||||
|
|
||||||
`cue export --out openapi root.cue`
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
Comments are rendered as descriptions in the json schema.
|
|
||||||
|
|
||||||
```cue
|
|
||||||
// A name of the clan (primarily shown by the UI)
|
|
||||||
name: string
|
|
||||||
```
|
|
||||||
|
|
||||||
Cue open sets. In the following `foo = {...}` means that the key `foo` can contain any arbitrary json object.
|
|
||||||
|
|
||||||
```cue
|
|
||||||
foo: { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
Cue dynamic keys.
|
|
||||||
|
|
||||||
```cue
|
|
||||||
[string]: {
|
|
||||||
attr: string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This is the schema of
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"a": {
|
|
||||||
"attr": "foo"
|
|
||||||
},
|
|
||||||
"b": {
|
|
||||||
"attr": "bar"
|
|
||||||
}
|
|
||||||
// ... Indefinitely more dynamic keys of type "string"
|
|
||||||
}
|
|
||||||
```
|
|
@ -1,137 +0,0 @@
|
|||||||
{
|
|
||||||
description = "<Put your description here>";
|
|
||||||
|
|
||||||
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
|
|
||||||
|
|
||||||
outputs =
|
|
||||||
{ clan-core, ... }:
|
|
||||||
let
|
|
||||||
pkgs = clan-core.inputs.nixpkgs.legacyPackages.${system};
|
|
||||||
system = "x86_64-linux";
|
|
||||||
in
|
|
||||||
# Usage see: https://docs.clan.lol
|
|
||||||
# nice_flake_interface -> buildInventory() -> Inventory -> buildClanFromInventory() -> nixosConfigurations
|
|
||||||
# buildClanFromInventory = inventory: evalModules {
|
|
||||||
# extraAttrs = { inherit inventory; };
|
|
||||||
# # (attrNames inventory.machines)
|
|
||||||
# };
|
|
||||||
# clan =
|
|
||||||
# clan-core.lib.buildClanFromInventory [
|
|
||||||
# # Inventory 0 (loads the json file managed by the Python API)
|
|
||||||
# (builtins.fromJSON (builtins.readFile ./inventory.json))
|
|
||||||
# # ->
|
|
||||||
# # {
|
|
||||||
# # services."backups_1".autoIncludeMachines = true;
|
|
||||||
# # services."backups_1".module = "borgbackup";
|
|
||||||
# # ... etc.
|
|
||||||
# # }
|
|
||||||
# ]
|
|
||||||
# ++ (buildInventory {
|
|
||||||
# clanName = "nice_flake_interface";
|
|
||||||
# description = "A nice flake interface";
|
|
||||||
# icon = "assets/icon.png";
|
|
||||||
# machines = {
|
|
||||||
# jon = {
|
|
||||||
# # Just regular nixos/clan configuration ?
|
|
||||||
# # config = {
|
|
||||||
# # imports = [
|
|
||||||
# # ./modules/shared.nix
|
|
||||||
# # ./machines/jon/configuration.nix
|
|
||||||
# # ];
|
|
||||||
# # nixpkgs.hostPlatform = system;
|
|
||||||
# # # Set this for clan commands use ssh i.e. `clan machines update`
|
|
||||||
# # # If you change the hostname, you need to update this line to root@<new-hostname>
|
|
||||||
# # # This only works however if you have avahi running on your admin machine else use IP
|
|
||||||
# # clan.networking.targetHost = pkgs.lib.mkDefault "root@jon";
|
|
||||||
# # # ssh root@flash-installer.local lsblk --output NAME,ID-LINK,FSTYPE,SIZE,MOUNTPOINT
|
|
||||||
# # disko.devices.disk.main = {
|
|
||||||
# # device = "/dev/disk/by-id/__CHANGE_ME__";
|
|
||||||
# # };
|
|
||||||
# # # IMPORTANT! Add your SSH key here
|
|
||||||
# # # e.g. > cat ~/.ssh/id_ed25519.pub
|
|
||||||
# # users.users.root.openssh.authorizedKeys.keys = throw ''
|
|
||||||
# # Don't forget to add your SSH key here!
|
|
||||||
# # users.users.root.openssh.authorizedKeys.keys = [ "<YOUR SSH_KEY>" ]
|
|
||||||
# # '';
|
|
||||||
# # # Zerotier needs one controller to accept new nodes. Once accepted
|
|
||||||
# # # the controller can be offline and routing still works.
|
|
||||||
# # clan.networking.zerotier.controller.enable = true;
|
|
||||||
# # };
|
|
||||||
# };
|
|
||||||
# };
|
|
||||||
# })
|
|
||||||
# ++ [
|
|
||||||
# # Low level inventory overrides (comes at the end)
|
|
||||||
# {
|
|
||||||
# services."backups_2".autoIncludeMachines = true;
|
|
||||||
# services."backups_2".module = "borgbackup";
|
|
||||||
# }
|
|
||||||
# ];
|
|
||||||
# # buildClan :: [ Partial<Inventory> ] -> Inventory
|
|
||||||
# # foldl' (acc: v: lib.recursiveUpdate acc v) {} []
|
|
||||||
# inventory = [
|
|
||||||
# # import json
|
|
||||||
# {...}
|
|
||||||
# # power user flake
|
|
||||||
# {...}
|
|
||||||
# ]
|
|
||||||
# # With Module system
|
|
||||||
# # Pros: Easy to understand,
|
|
||||||
# # Cons: Verbose, hard to maintain
|
|
||||||
# # buildClan :: { modules = [ { config = Partial<Inventory>; options :: InventoryOptions; } } ]; } -> Inventory
|
|
||||||
# eval = lib.evalModules {
|
|
||||||
# modules = [
|
|
||||||
# {
|
|
||||||
# # Inventory Schema
|
|
||||||
# # Python validation
|
|
||||||
# options = {...}
|
|
||||||
# }
|
|
||||||
# {
|
|
||||||
# config = map lib.mkDefault
|
|
||||||
# (builtins.fromJSON (builtins.readFile ./inventory.json))
|
|
||||||
# }
|
|
||||||
# {
|
|
||||||
# # User provided
|
|
||||||
# config = {...}
|
|
||||||
# }
|
|
||||||
# # Later overrides.
|
|
||||||
# {
|
|
||||||
# lib.mkForce ...
|
|
||||||
# }
|
|
||||||
# ];
|
|
||||||
# }
|
|
||||||
# nixosConfigurations = lib.evalModules inventory;
|
|
||||||
# eval.config.inventory
|
|
||||||
# #
|
|
||||||
# eval.config.machines.jon#nixosConfig
|
|
||||||
# eval.config.machines.sara#nixosConfig
|
|
||||||
#
|
|
||||||
# {inventory, config, ...}:{
|
|
||||||
# hostname = config.machines.sara # Invalid
|
|
||||||
# hostname = inventory.machines.sara.hostname # Valid
|
|
||||||
# }
|
|
||||||
/*
|
|
||||||
# Type
|
|
||||||
|
|
||||||
buildInventory :: {
|
|
||||||
clanName :: string
|
|
||||||
machines :: {
|
|
||||||
${name} :: {
|
|
||||||
config :: {
|
|
||||||
# NixOS configuration
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
# ... More mapped inventory options
|
|
||||||
# i.e. shared config for all machines
|
|
||||||
} -> Inventory
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
# all machines managed by Clan
|
|
||||||
inherit (clan) nixosConfigurations clanInternals;
|
|
||||||
# add the Clan cli tool to the dev shell
|
|
||||||
devShells.${system}.default = pkgs.mkShell {
|
|
||||||
packages = [ clan-core.packages.${system}.clan-cli ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
package machines
|
|
||||||
|
|
||||||
|
|
||||||
#machine: machines: [string]: {
|
|
||||||
name: string,
|
|
||||||
description?: string,
|
|
||||||
icon?: string
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"machines": {
|
|
||||||
"camina_machine": {
|
|
||||||
"name": "camina"
|
|
||||||
},
|
|
||||||
"vyr_machine": {
|
|
||||||
"name": "vyr"
|
|
||||||
},
|
|
||||||
"vi_machine": {
|
|
||||||
"name": "vi"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"name": "kenjis clan"
|
|
||||||
},
|
|
||||||
"services": {
|
|
||||||
"backup": {
|
|
||||||
"meta": {
|
|
||||||
"name": "My backup"
|
|
||||||
},
|
|
||||||
"module": "borbackup-static",
|
|
||||||
"machineConfig": {
|
|
||||||
"vyr": {
|
|
||||||
"roles": ["server"]
|
|
||||||
},
|
|
||||||
"vi": {
|
|
||||||
"roles": ["client"]
|
|
||||||
},
|
|
||||||
"camina_machine": {
|
|
||||||
"roles": ["client"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"folders": ["/home", "/root", "/var", "/etc"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
{
|
|
||||||
"machines": {
|
|
||||||
"camina_machine": {
|
|
||||||
"name": "camina"
|
|
||||||
},
|
|
||||||
"vyr": {
|
|
||||||
"name": "vyr"
|
|
||||||
},
|
|
||||||
"vi": {
|
|
||||||
"name": "vi"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"name": "kenjis clan"
|
|
||||||
},
|
|
||||||
"services": {
|
|
||||||
"sync_files": {
|
|
||||||
"meta": {
|
|
||||||
"name": "My sync"
|
|
||||||
},
|
|
||||||
"module": "syncthing-static-peers",
|
|
||||||
"machineConfig": {
|
|
||||||
"vyr": {},
|
|
||||||
"vi": {},
|
|
||||||
"camina_machine": {}
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"folders": {
|
|
||||||
"test": {
|
|
||||||
"path": "~/data/docs",
|
|
||||||
"devices": ["camina", "vyr", "vi"]
|
|
||||||
},
|
|
||||||
"videos": {
|
|
||||||
"path": "~/data/videos",
|
|
||||||
"devices": ["camina", "vyr", "ezra"]
|
|
||||||
},
|
|
||||||
"playlist": {
|
|
||||||
"path": "~/data/playlist",
|
|
||||||
"devices": ["camina", "vyr", "ezra"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"machines": {
|
|
||||||
"camina_machine": {
|
|
||||||
"name": "camina"
|
|
||||||
},
|
|
||||||
"vyr_machine": {
|
|
||||||
"name": "vyr"
|
|
||||||
},
|
|
||||||
"vi_machine": {
|
|
||||||
"name": "vi"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"meta": {
|
|
||||||
"name": "kenjis clan"
|
|
||||||
},
|
|
||||||
"services": {
|
|
||||||
"backup": {
|
|
||||||
"meta": {
|
|
||||||
"name": "My backup"
|
|
||||||
},
|
|
||||||
"module": "borbackup-static",
|
|
||||||
"machineConfig": {
|
|
||||||
"vyr_machine": {
|
|
||||||
"roles": ["server"]
|
|
||||||
},
|
|
||||||
"vi_machine": {
|
|
||||||
"roles": ["peer"]
|
|
||||||
},
|
|
||||||
"camina_machine": {
|
|
||||||
"roles": ["peer"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"config": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -12,53 +12,83 @@
|
|||||||
# DEPRECATED: use meta.icon instead
|
# DEPRECATED: use meta.icon instead
|
||||||
clanIcon ? null, # A path to an icon to be used for the clan, should be the same for all machines
|
clanIcon ? null, # A path to an icon to be used for the clan, should be the same for all machines
|
||||||
meta ? { }, # A set containing clan meta: name :: string, icon :: string, description :: string
|
meta ? { }, # A set containing clan meta: name :: string, icon :: string, description :: string
|
||||||
pkgsForSystem ? (_system: null), # A map from arch to pkgs, if specified this nixpkgs will be only imported once for each system.
|
# A map from arch to pkgs, if specified this nixpkgs will be only imported once for each system.
|
||||||
# This improves performance, but all nipxkgs.* options will be ignored.
|
# This improves performance, but all nipxkgs.* options will be ignored.
|
||||||
|
pkgsForSystem ? (_system: null),
|
||||||
|
/*
|
||||||
|
Low level inventory configuration.
|
||||||
|
Overrides the services configuration.
|
||||||
|
*/
|
||||||
|
inventory ? { },
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
deprecationWarnings = [
|
# Internal inventory, this is the result of merging all potential inventory sources:
|
||||||
(lib.warnIf (
|
# - Default instances configured via 'services'
|
||||||
clanName != null
|
# - The inventory overrides
|
||||||
) "clanName is deprecated, please use meta.name instead. ${clanName}" null)
|
# - Machines that exist in inventory.machines
|
||||||
(lib.warnIf (clanIcon != null) "clanIcon is deprecated, please use meta.icon instead" null)
|
# - Machines explicitly configured via 'machines' argument
|
||||||
];
|
# - Machines that exist in the machines directory
|
||||||
|
# Checks on the module level:
|
||||||
|
# - Each service role must reference a valid machine after all machines are merged
|
||||||
|
mergedInventory =
|
||||||
|
(lib.evalModules {
|
||||||
|
modules = [
|
||||||
|
clan-core.lib.inventory.interface
|
||||||
|
{ inherit meta; }
|
||||||
|
(
|
||||||
|
if
|
||||||
|
builtins.pathExists "${directory}/inventory.json"
|
||||||
|
# Is recursively applied. Any explicit nix will override.
|
||||||
|
then
|
||||||
|
lib.mkDefault (builtins.fromJSON (builtins.readFile "${directory}/inventory.json"))
|
||||||
|
else
|
||||||
|
{ }
|
||||||
|
)
|
||||||
|
inventory
|
||||||
|
# Machines explicitly configured via 'machines' argument
|
||||||
|
{
|
||||||
|
# { ${name} :: meta // { name, tags } }
|
||||||
|
machines = lib.mapAttrs (
|
||||||
|
name: config:
|
||||||
|
(lib.attrByPath [
|
||||||
|
"clan"
|
||||||
|
"meta"
|
||||||
|
] { } config)
|
||||||
|
// {
|
||||||
|
# meta.name default is the attribute name of the machine
|
||||||
|
name = lib.mkDefault (
|
||||||
|
lib.attrByPath [
|
||||||
|
"clan"
|
||||||
|
"meta"
|
||||||
|
"name"
|
||||||
|
] name config
|
||||||
|
);
|
||||||
|
tags = lib.attrByPath [
|
||||||
|
"clan"
|
||||||
|
"tags"
|
||||||
|
] [ ] config;
|
||||||
|
}
|
||||||
|
) machines;
|
||||||
|
}
|
||||||
|
# Will be deprecated
|
||||||
|
{ machines = lib.mapAttrs (_n: _: lib.mkDefault { }) machinesDirs; }
|
||||||
|
|
||||||
|
# Deprecated interface
|
||||||
|
(if clanName != null then { meta.name = clanName; } else { })
|
||||||
|
(if clanIcon != null then { meta.icon = clanIcon; } else { })
|
||||||
|
];
|
||||||
|
}).config;
|
||||||
|
|
||||||
|
inherit (clan-core.lib.inventory) buildInventory;
|
||||||
|
|
||||||
|
# map from machine name to service configuration
|
||||||
|
# { ${machineName} :: Config }
|
||||||
|
serviceConfigs = buildInventory mergedInventory;
|
||||||
|
|
||||||
machinesDirs = lib.optionalAttrs (builtins.pathExists "${directory}/machines") (
|
machinesDirs = lib.optionalAttrs (builtins.pathExists "${directory}/machines") (
|
||||||
builtins.readDir (directory + /machines)
|
builtins.readDir (directory + /machines)
|
||||||
);
|
);
|
||||||
|
|
||||||
mergedMeta =
|
|
||||||
let
|
|
||||||
metaFromFile =
|
|
||||||
if (builtins.pathExists "${directory}/clan/meta.json") then
|
|
||||||
let
|
|
||||||
settings = builtins.fromJSON (builtins.readFile "${directory}/clan/meta.json");
|
|
||||||
in
|
|
||||||
settings
|
|
||||||
else
|
|
||||||
{ };
|
|
||||||
legacyMeta = lib.filterAttrs (_: v: v != null) {
|
|
||||||
name = clanName;
|
|
||||||
icon = clanIcon;
|
|
||||||
};
|
|
||||||
optionsMeta = lib.filterAttrs (_: v: v != null) meta;
|
|
||||||
|
|
||||||
warnings =
|
|
||||||
builtins.map (
|
|
||||||
name:
|
|
||||||
if
|
|
||||||
metaFromFile.${name} or null != optionsMeta.${name} or null && optionsMeta.${name} or null != null
|
|
||||||
then
|
|
||||||
lib.warn "meta.${name} is set in different places. (exlicit option meta.${name} overrides ${directory}/clan/meta.json)" null
|
|
||||||
else
|
|
||||||
null
|
|
||||||
) (builtins.attrNames metaFromFile)
|
|
||||||
++ [ (if (res.name or null == null) then (throw "meta.name should be set") else null) ];
|
|
||||||
res = metaFromFile // legacyMeta // optionsMeta;
|
|
||||||
in
|
|
||||||
# Print out warnings before returning the merged result
|
|
||||||
builtins.deepSeq warnings res;
|
|
||||||
|
|
||||||
machineSettings =
|
machineSettings =
|
||||||
machineName:
|
machineName:
|
||||||
# CLAN_MACHINE_SETTINGS_FILE allows to override the settings file temporarily
|
# CLAN_MACHINE_SETTINGS_FILE allows to override the settings file temporarily
|
||||||
@ -71,14 +101,16 @@ let
|
|||||||
builtins.fromJSON (builtins.readFile (directory + /machines/${machineName}/settings.json))
|
builtins.fromJSON (builtins.readFile (directory + /machines/${machineName}/settings.json))
|
||||||
);
|
);
|
||||||
|
|
||||||
# Read additional imports specified via a config option in settings.json
|
|
||||||
# This is not an infinite recursion, because the imports are discovered here
|
|
||||||
# before calling evalModules.
|
|
||||||
# It is still useful to have the imports as an option, as this allows for type
|
|
||||||
# checking and easy integration with the config frontend(s)
|
|
||||||
machineImports =
|
machineImports =
|
||||||
machineSettings: map (module: clan-core.clanModules.${module}) (machineSettings.clanImports or [ ]);
|
machineSettings: map (module: clan-core.clanModules.${module}) (machineSettings.clanImports or [ ]);
|
||||||
|
|
||||||
|
deprecationWarnings = [
|
||||||
|
(lib.warnIf (
|
||||||
|
clanName != null
|
||||||
|
) "clanName is deprecated, please use meta.name instead. ${clanName}" null)
|
||||||
|
(lib.warnIf (clanIcon != null) "clanIcon is deprecated, please use meta.icon instead" null)
|
||||||
|
];
|
||||||
|
|
||||||
# TODO: remove default system once we have a hardware-config mechanism
|
# TODO: remove default system once we have a hardware-config mechanism
|
||||||
nixosConfiguration =
|
nixosConfiguration =
|
||||||
{
|
{
|
||||||
@ -98,6 +130,9 @@ let
|
|||||||
clan-core.nixosModules.clanCore
|
clan-core.nixosModules.clanCore
|
||||||
extraConfig
|
extraConfig
|
||||||
(machines.${name} or { })
|
(machines.${name} or { })
|
||||||
|
# Inherit the inventory assertions ?
|
||||||
|
{ inherit (mergedInventory) assertions; }
|
||||||
|
{ imports = serviceConfigs.${name} or { }; }
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
# Settings
|
# Settings
|
||||||
@ -125,7 +160,7 @@ let
|
|||||||
} // specialArgs;
|
} // specialArgs;
|
||||||
};
|
};
|
||||||
|
|
||||||
allMachines = machinesDirs // machines;
|
allMachines = mergedInventory.machines or { };
|
||||||
|
|
||||||
supportedSystems = [
|
supportedSystems = [
|
||||||
"x86_64-linux"
|
"x86_64-linux"
|
||||||
@ -177,9 +212,10 @@ builtins.deepSeq deprecationWarnings {
|
|||||||
inherit nixosConfigurations;
|
inherit nixosConfigurations;
|
||||||
|
|
||||||
clanInternals = {
|
clanInternals = {
|
||||||
# Evaluated clan meta
|
meta = mergedInventory.meta;
|
||||||
# Merged /clan/meta.json with overrides from buildClan
|
inventory = mergedInventory;
|
||||||
meta = mergedMeta;
|
|
||||||
|
inventoryFile = "${directory}/inventory.json";
|
||||||
|
|
||||||
# machine specifics
|
# machine specifics
|
||||||
machines = configsPerSystem;
|
machines = configsPerSystem;
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
evalClanModules = import ./eval-clan-modules { inherit clan-core nixpkgs lib; };
|
evalClanModules = import ./eval-clan-modules { inherit clan-core nixpkgs lib; };
|
||||||
|
inventory = import ./inventory { inherit lib clan-core; };
|
||||||
jsonschema = import ./jsonschema { inherit lib; };
|
jsonschema = import ./jsonschema { inherit lib; };
|
||||||
modules = import ./description.nix { inherit clan-core lib; };
|
modules = import ./description.nix { inherit clan-core lib; };
|
||||||
buildClan = import ./build-clan { inherit clan-core lib nixpkgs; };
|
buildClan = import ./build-clan { inherit clan-core lib nixpkgs; };
|
||||||
|
@ -5,7 +5,10 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
imports = [ ./jsonschema/flake-module.nix ];
|
imports = [
|
||||||
|
./jsonschema/flake-module.nix
|
||||||
|
./inventory/flake-module.nix
|
||||||
|
];
|
||||||
flake.lib = import ./default.nix {
|
flake.lib = import ./default.nix {
|
||||||
inherit lib inputs;
|
inherit lib inputs;
|
||||||
inherit (inputs) nixpkgs;
|
inherit (inputs) nixpkgs;
|
||||||
|
90
lib/inventory/README.md
Normal file
90
lib/inventory/README.md
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# Inventory
|
||||||
|
|
||||||
|
The inventory is our concept for distributed services. Users can configure multiple machines with minimal effort.
|
||||||
|
|
||||||
|
- The inventory acts as a declarative source of truth for all machine configurations.
|
||||||
|
- Users can easily add or remove machines to/from services.
|
||||||
|
- Ensures that all machines and services are configured consistently, across multiple nixosConfigs.
|
||||||
|
- Defaults and predefined roles in our modules minimizes the need for manual configuration.
|
||||||
|
|
||||||
|
Open questions:
|
||||||
|
|
||||||
|
- [ ] How do we set default role, description and other metadata?
|
||||||
|
- It must be accessible from Python.
|
||||||
|
- It must set the value in the module system.
|
||||||
|
|
||||||
|
- [ ] Inventory might use assertions. Should each machine inherit the inventory assertions ?
|
||||||
|
|
||||||
|
- [ ] Is the service config interface the same as the module config interface ?
|
||||||
|
|
||||||
|
- [ ] As a user do I want to see borgbackup as the high level category?
|
||||||
|
|
||||||
|
|
||||||
|
Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
nixosConfig < machine_module < inventory
|
||||||
|
---------------------------------------------
|
||||||
|
nixos < borgbackup <- inventory <-> UI
|
||||||
|
|
||||||
|
creates the config Maps from high level services to the borgbackup clan module
|
||||||
|
for ONE machine Inventory is completely serializable.
|
||||||
|
UI can interact with the inventory to define machines, and services
|
||||||
|
Defining Users is out of scope for the first prototype.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Provides a specification for the inventory
|
||||||
|
|
||||||
|
It is used for design phase and as validation helper.
|
||||||
|
|
||||||
|
> Cue is less verbose and easier to understand and maintain than json-schema.
|
||||||
|
> Json-schema, if needed can be easily generated on-the fly.
|
||||||
|
|
||||||
|
## Checking validity
|
||||||
|
|
||||||
|
Directly check a json against the schema
|
||||||
|
|
||||||
|
`cue vet inventory.json root.cue -d '#Root'`
|
||||||
|
|
||||||
|
## Json schema
|
||||||
|
|
||||||
|
Export the json-schema i.e. for usage in python / javascript / nix
|
||||||
|
|
||||||
|
`cue export --out openapi root.cue`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Comments are rendered as descriptions in the json schema.
|
||||||
|
|
||||||
|
```cue
|
||||||
|
// A name of the clan (primarily shown by the UI)
|
||||||
|
name: string
|
||||||
|
```
|
||||||
|
|
||||||
|
Cue open sets. In the following `foo = {...}` means that the key `foo` can contain any arbitrary json object.
|
||||||
|
|
||||||
|
```cue
|
||||||
|
foo: { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
Cue dynamic keys.
|
||||||
|
|
||||||
|
```cue
|
||||||
|
[string]: {
|
||||||
|
attr: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the schema of
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"a": {
|
||||||
|
"attr": "foo"
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
"attr": "bar"
|
||||||
|
}
|
||||||
|
// ... Indefinitely more dynamic keys of type "string"
|
||||||
|
}
|
||||||
|
```
|
106
lib/inventory/build-inventory/default.nix
Normal file
106
lib/inventory/build-inventory/default.nix
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
# Generate partial NixOS configurations for every machine in the inventory
|
||||||
|
# This function is responsible for generating the module configuration for every machine in the inventory.
|
||||||
|
{ lib, clan-core }:
|
||||||
|
inventory:
|
||||||
|
let
|
||||||
|
machines = machinesFromInventory inventory;
|
||||||
|
|
||||||
|
resolveTags =
|
||||||
|
# Inventory, { machines :: [string], tags :: [string] }
|
||||||
|
inventory: members: {
|
||||||
|
machines =
|
||||||
|
members.machines or [ ]
|
||||||
|
++ (builtins.foldl' (
|
||||||
|
acc: tag:
|
||||||
|
let
|
||||||
|
tagMembers = builtins.attrNames (
|
||||||
|
lib.filterAttrs (_n: v: builtins.elem tag v.tags or [ ]) inventory.machines
|
||||||
|
);
|
||||||
|
in
|
||||||
|
# throw "Machine tag ${tag} not found. Not machine with: tag ${tagName} not in inventory.";
|
||||||
|
if tagMembers == [ ] then
|
||||||
|
throw "Machine tag ${tag} not found. Not machine with: tag ${tag} not in inventory."
|
||||||
|
else
|
||||||
|
acc ++ tagMembers
|
||||||
|
) [ ] members.tags or [ ]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Returns a NixOS configuration for every machine in the inventory.
|
||||||
|
|
||||||
|
machinesFromInventory :: Inventory -> { ${machine_name} :: NixOSConfiguration }
|
||||||
|
*/
|
||||||
|
machinesFromInventory =
|
||||||
|
inventory:
|
||||||
|
# For every machine in the inventory, build a NixOS configuration
|
||||||
|
# For each machine generate config, forEach service, if the machine is used.
|
||||||
|
builtins.mapAttrs (
|
||||||
|
machineName: _:
|
||||||
|
lib.foldlAttrs (
|
||||||
|
# [ Modules ], String, { ${instance_name} :: ServiceConfig }
|
||||||
|
acc: moduleName: serviceConfigs:
|
||||||
|
acc
|
||||||
|
# Collect service config
|
||||||
|
++ (lib.foldlAttrs (
|
||||||
|
# [ Modules ], String, ServiceConfig
|
||||||
|
acc2: instanceName: serviceConfig:
|
||||||
|
let
|
||||||
|
resolvedRoles = builtins.mapAttrs (
|
||||||
|
_roleName: members: resolveTags inventory members
|
||||||
|
) serviceConfig.roles;
|
||||||
|
|
||||||
|
isInService = builtins.any (members: builtins.elem machineName members.machines) (
|
||||||
|
builtins.attrValues resolvedRoles
|
||||||
|
);
|
||||||
|
|
||||||
|
# Inverse map of roles. Allows for easy lookup of roles for a given machine.
|
||||||
|
# { ${machine_name} :: [roles]
|
||||||
|
inverseRoles = lib.foldlAttrs (
|
||||||
|
acc: roleName:
|
||||||
|
{ machines }:
|
||||||
|
acc
|
||||||
|
// builtins.foldl' (
|
||||||
|
acc2: machineName: acc2 // { ${machineName} = (acc.${machineName} or [ ]) ++ [ roleName ]; }
|
||||||
|
) { } machines
|
||||||
|
) { } resolvedRoles;
|
||||||
|
|
||||||
|
machineServiceConfig = (serviceConfig.machines.${machineName} or { }).config or { };
|
||||||
|
globalConfig = serviceConfig.config or { };
|
||||||
|
|
||||||
|
# TODO: maybe optimize this dont lookup the role in inverse roles. Imports are not lazy
|
||||||
|
roleModules = builtins.map (
|
||||||
|
role:
|
||||||
|
let
|
||||||
|
path = "${clan-core.clanModules.${moduleName}}/roles/${role}.nix";
|
||||||
|
in
|
||||||
|
if builtins.pathExists path then
|
||||||
|
path
|
||||||
|
else
|
||||||
|
throw "Role doesnt have a module: ${role}. Path: ${path} not found."
|
||||||
|
) inverseRoles.${machineName} or [ ];
|
||||||
|
in
|
||||||
|
if isInService then
|
||||||
|
acc2
|
||||||
|
++ [
|
||||||
|
{
|
||||||
|
imports = [ clan-core.clanModules.${moduleName} ] ++ roleModules;
|
||||||
|
config.clan.${moduleName} = lib.mkMerge [
|
||||||
|
globalConfig
|
||||||
|
machineServiceConfig
|
||||||
|
];
|
||||||
|
}
|
||||||
|
{
|
||||||
|
config.clan.inventory.services.${moduleName}.${instanceName} = {
|
||||||
|
roles = resolvedRoles;
|
||||||
|
# TODO: Add inverseRoles to the service config if needed
|
||||||
|
# inherit inverseRoles;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]
|
||||||
|
else
|
||||||
|
acc2
|
||||||
|
) [ ] serviceConfigs)
|
||||||
|
) [ ] inventory.services
|
||||||
|
) inventory.machines;
|
||||||
|
in
|
||||||
|
machines
|
121
lib/inventory/build-inventory/interface.nix
Normal file
121
lib/inventory/build-inventory/interface.nix
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
{ config, lib, ... }:
|
||||||
|
let
|
||||||
|
t = lib.types;
|
||||||
|
|
||||||
|
metaOptions = {
|
||||||
|
name = lib.mkOption { type = t.str; };
|
||||||
|
description = lib.mkOption {
|
||||||
|
default = null;
|
||||||
|
type = t.nullOr t.str;
|
||||||
|
};
|
||||||
|
icon = lib.mkOption {
|
||||||
|
default = null;
|
||||||
|
type = t.nullOr t.str;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
machineRef = lib.mkOptionType {
|
||||||
|
name = "machineRef";
|
||||||
|
description = "Machine :: [${builtins.concatStringsSep " | " (builtins.attrNames config.machines)}]";
|
||||||
|
check = v: lib.isString v && builtins.elem v (builtins.attrNames config.machines);
|
||||||
|
merge = lib.mergeEqualOption;
|
||||||
|
};
|
||||||
|
|
||||||
|
allTags = lib.unique (
|
||||||
|
lib.foldlAttrs (
|
||||||
|
tags: _: m:
|
||||||
|
tags ++ m.tags or [ ]
|
||||||
|
) [ ] config.machines
|
||||||
|
);
|
||||||
|
|
||||||
|
tagRef = lib.mkOptionType {
|
||||||
|
name = "tagRef";
|
||||||
|
description = "Tags :: [${builtins.concatStringsSep " | " allTags}]";
|
||||||
|
check = v: lib.isString v && builtins.elem v allTags;
|
||||||
|
merge = lib.mergeEqualOption;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.assertions = lib.mkOption {
|
||||||
|
type = t.listOf t.unspecified;
|
||||||
|
internal = true;
|
||||||
|
default = [ ];
|
||||||
|
};
|
||||||
|
config.assertions = lib.foldlAttrs (
|
||||||
|
ass1: serviceName: c:
|
||||||
|
ass1
|
||||||
|
++ lib.foldlAttrs (
|
||||||
|
ass2: instanceName: instanceConfig:
|
||||||
|
let
|
||||||
|
serviceMachineNames = lib.attrNames instanceConfig.machines;
|
||||||
|
topLevelMachines = lib.attrNames config.machines;
|
||||||
|
# All machines must be defined in the top-level machines
|
||||||
|
assertions = builtins.map (m: {
|
||||||
|
assertion = builtins.elem m topLevelMachines;
|
||||||
|
message = "${serviceName}.${instanceName}.machines.${m}. Should be one of [ ${builtins.concatStringsSep " | " topLevelMachines} ]";
|
||||||
|
}) serviceMachineNames;
|
||||||
|
in
|
||||||
|
ass2 ++ assertions
|
||||||
|
) [ ] c
|
||||||
|
) [ ] config.services;
|
||||||
|
|
||||||
|
options.meta = metaOptions;
|
||||||
|
|
||||||
|
options.machines = lib.mkOption {
|
||||||
|
default = { };
|
||||||
|
type = t.attrsOf (
|
||||||
|
t.submodule {
|
||||||
|
options = {
|
||||||
|
inherit (metaOptions) name description icon;
|
||||||
|
tags = lib.mkOption {
|
||||||
|
default = [ ];
|
||||||
|
apply = lib.unique;
|
||||||
|
type = t.listOf t.str;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
options.services = lib.mkOption {
|
||||||
|
default = { };
|
||||||
|
type = t.attrsOf (
|
||||||
|
t.attrsOf (
|
||||||
|
t.submodule {
|
||||||
|
options.meta = metaOptions;
|
||||||
|
options.config = lib.mkOption {
|
||||||
|
default = { };
|
||||||
|
type = t.anything;
|
||||||
|
};
|
||||||
|
options.machines = lib.mkOption {
|
||||||
|
default = { };
|
||||||
|
type = t.attrsOf (
|
||||||
|
t.submodule {
|
||||||
|
options.config = lib.mkOption {
|
||||||
|
default = { };
|
||||||
|
type = t.anything;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
options.roles = lib.mkOption {
|
||||||
|
default = { };
|
||||||
|
type = t.attrsOf (
|
||||||
|
t.submodule {
|
||||||
|
options.machines = lib.mkOption {
|
||||||
|
default = [ ];
|
||||||
|
type = t.listOf machineRef;
|
||||||
|
};
|
||||||
|
options.tags = lib.mkOption {
|
||||||
|
default = [ ];
|
||||||
|
apply = lib.unique;
|
||||||
|
type = t.listOf tagRef;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
5
lib/inventory/default.nix
Normal file
5
lib/inventory/default.nix
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{ lib, clan-core }:
|
||||||
|
{
|
||||||
|
buildInventory = import ./build-inventory { inherit lib clan-core; };
|
||||||
|
interface = ./build-inventory/interface.nix;
|
||||||
|
}
|
38
lib/inventory/example.nix
Normal file
38
lib/inventory/example.nix
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{ self, ... }:
|
||||||
|
self.lib.buildClan {
|
||||||
|
# Name of the clan in the UI, should be unique
|
||||||
|
meta.name = "Inventory clan";
|
||||||
|
|
||||||
|
# Should usually point to the directory of flake.nix
|
||||||
|
directory = self;
|
||||||
|
|
||||||
|
inventory = {
|
||||||
|
services = {
|
||||||
|
borgbackup.instance_1 = {
|
||||||
|
roles.server.machines = [ "backup_server" ];
|
||||||
|
roles.client.tags = [ "backup" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# merged with
|
||||||
|
machines = {
|
||||||
|
"backup_server" = {
|
||||||
|
clan.tags = [ "all" ];
|
||||||
|
};
|
||||||
|
"client_1_machine" = {
|
||||||
|
clan.tags = [
|
||||||
|
"all"
|
||||||
|
"backup"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"client_2_machine" = {
|
||||||
|
clan.tags = [
|
||||||
|
"all"
|
||||||
|
"backup"
|
||||||
|
];
|
||||||
|
# Name of the machine in the UI
|
||||||
|
clan.meta.name = "camina";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
@ -13,6 +13,7 @@
|
|||||||
mkdir -p $out
|
mkdir -p $out
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
devShells.inventory-schema = pkgs.mkShell { inputsFrom = [ config.packages.inventory-schema ]; };
|
devShells.inventory-schema = pkgs.mkShell { inputsFrom = [ config.packages.inventory-schema ]; };
|
||||||
|
|
||||||
checks.inventory-schema-checks = pkgs.stdenv.mkDerivation {
|
checks.inventory-schema-checks = pkgs.stdenv.mkDerivation {
|
@ -1,8 +1,7 @@
|
|||||||
package inventory
|
package inventory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"clan.lol/inventory/services"
|
"clan.lol/inventory/schema"
|
||||||
"clan.lol/inventory/machines"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@jsonschema(schema="http://json-schema.org/schema#")
|
@jsonschema(schema="http://json-schema.org/schema#")
|
||||||
@ -16,9 +15,9 @@ import (
|
|||||||
icon?: string
|
icon?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// A map of services
|
// // A map of services
|
||||||
services.#service
|
schema.#service
|
||||||
|
|
||||||
// A map of machines
|
// // A map of machines
|
||||||
machines.#machine
|
schema.#machine
|
||||||
}
|
}
|
@ -1,22 +1,29 @@
|
|||||||
package services
|
package schema
|
||||||
|
|
||||||
#ServiceRole: "server" | "client" | "both"
|
#machine: machines: [string]: {
|
||||||
|
name: string,
|
||||||
|
description?: string,
|
||||||
|
icon?: string
|
||||||
|
tags: [...string]
|
||||||
|
}
|
||||||
|
|
||||||
#service: services: [string]: {
|
#role: string
|
||||||
|
|
||||||
|
#service: services: [string]: [string]: {
|
||||||
// Required meta fields
|
// Required meta fields
|
||||||
meta: {
|
meta: {
|
||||||
name: string,
|
name: string,
|
||||||
icon?: string
|
icon?: string
|
||||||
description?: string,
|
description?: string,
|
||||||
},
|
},
|
||||||
// Required module specifies the behavior of the service.
|
|
||||||
module: string,
|
|
||||||
|
|
||||||
// We moved the machine sepcific config to "machines".
|
// We moved the machine sepcific config to "machines".
|
||||||
// It may be moved back depending on what makes more sense in the future.
|
// It may be moved back depending on what makes more sense in the future.
|
||||||
machineConfig: {
|
roles: [#role]: {
|
||||||
|
machines: [...string],
|
||||||
|
tags: [...string],
|
||||||
|
}
|
||||||
|
machines: {
|
||||||
[string]: {
|
[string]: {
|
||||||
roles?: [ ...#ServiceRole ],
|
|
||||||
config?: {
|
config?: {
|
||||||
...
|
...
|
||||||
}
|
}
|
53
lib/inventory/src/tests/borgbackup.json
Normal file
53
lib/inventory/src/tests/borgbackup.json
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"machines": {
|
||||||
|
"camina_machine": {
|
||||||
|
"name": "camina",
|
||||||
|
"tags": ["laptop"]
|
||||||
|
},
|
||||||
|
"vyr_machine": {
|
||||||
|
"name": "vyr"
|
||||||
|
},
|
||||||
|
"vi_machine": {
|
||||||
|
"name": "vi",
|
||||||
|
"tags": ["laptop"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"name": "kenjis clan"
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"borgbackup": {
|
||||||
|
"instance_1": {
|
||||||
|
"meta": {
|
||||||
|
"name": "My backup"
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"server": {
|
||||||
|
"machines": ["vyr_machine"]
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"machines": ["vyr_machine"],
|
||||||
|
"tags": ["laptop"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"machines": {},
|
||||||
|
"config": {}
|
||||||
|
},
|
||||||
|
"instance_2": {
|
||||||
|
"meta": {
|
||||||
|
"name": "My backup"
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"server": {
|
||||||
|
"machines": ["vi_machine"]
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"machines": ["camina_machine"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"machines": {},
|
||||||
|
"config": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
lib/inventory/src/tests/syncthing.json
Normal file
47
lib/inventory/src/tests/syncthing.json
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"machines": {
|
||||||
|
"camina_machine": {
|
||||||
|
"name": "camina"
|
||||||
|
},
|
||||||
|
"vyr_machine": {
|
||||||
|
"name": "vyr"
|
||||||
|
},
|
||||||
|
"vi_machine": {
|
||||||
|
"name": "vi"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"name": "kenjis clan"
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"syncthing": {
|
||||||
|
"instance_1": {
|
||||||
|
"meta": {
|
||||||
|
"name": "My sync"
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"peer": {
|
||||||
|
"machines": ["vyr_machine", "vi_machine", "camina_machine"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"machines": {},
|
||||||
|
"config": {
|
||||||
|
"folders": {
|
||||||
|
"test": {
|
||||||
|
"path": "~/data/docs",
|
||||||
|
"devices": ["camina_machine", "vyr_machine", "vi_machine"]
|
||||||
|
},
|
||||||
|
"videos": {
|
||||||
|
"path": "~/data/videos",
|
||||||
|
"devices": ["camina_machine", "vyr_machine"]
|
||||||
|
},
|
||||||
|
"playlist": {
|
||||||
|
"path": "~/data/playlist",
|
||||||
|
"devices": ["camina_machine", "vi_machine"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
lib/inventory/src/tests/zerotier.json
Normal file
36
lib/inventory/src/tests/zerotier.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"machines": {
|
||||||
|
"camina_machine": {
|
||||||
|
"name": "camina"
|
||||||
|
},
|
||||||
|
"vyr_machine": {
|
||||||
|
"name": "vyr"
|
||||||
|
},
|
||||||
|
"vi_machine": {
|
||||||
|
"name": "vi"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"name": "kenjis clan"
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"zerotier": {
|
||||||
|
"instance_1": {
|
||||||
|
"meta": {
|
||||||
|
"name": "My Network"
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"controller": { "machines": ["vyr_machine"] },
|
||||||
|
"moon": { "machines": ["vyr_machine"] },
|
||||||
|
"peer": { "machines": ["vi_machine", "camina_machine"] }
|
||||||
|
},
|
||||||
|
"machines": {
|
||||||
|
"vyr_machine": {
|
||||||
|
"config": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,5 +14,8 @@
|
|||||||
./vm.nix
|
./vm.nix
|
||||||
./wayland-proxy-virtwl.nix
|
./wayland-proxy-virtwl.nix
|
||||||
./zerotier
|
./zerotier
|
||||||
|
# Inventory
|
||||||
|
./inventory/interface.nix
|
||||||
|
./meta/interface.nix
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
35
nixosModules/clanCore/inventory/interface.nix
Normal file
35
nixosModules/clanCore/inventory/interface.nix
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{ lib, ... }:
|
||||||
|
let
|
||||||
|
# {
|
||||||
|
# roles = {
|
||||||
|
# client = {
|
||||||
|
# machines = [
|
||||||
|
# "camina_machine"
|
||||||
|
# "vi_machine"
|
||||||
|
# ];
|
||||||
|
# };
|
||||||
|
# server = {
|
||||||
|
# machines = [ "vyr_machine" ];
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
# }
|
||||||
|
instanceOptions = lib.types.submodule {
|
||||||
|
options.roles = lib.mkOption { type = lib.types.attrsOf machinesList; };
|
||||||
|
};
|
||||||
|
|
||||||
|
# {
|
||||||
|
# machines = [
|
||||||
|
# "camina_machine"
|
||||||
|
# "vi_machine"
|
||||||
|
# "vyr_machine"
|
||||||
|
# ];
|
||||||
|
# }
|
||||||
|
machinesList = lib.types.submodule {
|
||||||
|
options.machines = lib.mkOption { type = lib.types.listOf lib.types.str; };
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.clan.inventory.services = lib.mkOption {
|
||||||
|
type = lib.types.attrsOf (lib.types.attrsOf instanceOptions);
|
||||||
|
};
|
||||||
|
}
|
10
nixosModules/clanCore/meta/interface.nix
Normal file
10
nixosModules/clanCore/meta/interface.nix
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{ lib, ... }:
|
||||||
|
let
|
||||||
|
optStr = lib.types.nullOr lib.types.str;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.clan.meta.name = lib.mkOption { type = lib.types.str; };
|
||||||
|
options.clan.meta.description = lib.mkOption { type = optStr; };
|
||||||
|
options.clan.meta.icon = lib.mkOption { type = optStr; };
|
||||||
|
options.clan.tags = lib.mkOption { type = lib.types.listOf lib.types.str; };
|
||||||
|
}
|
@ -11,7 +11,19 @@
|
|||||||
# Usage see: https://docs.clan.lol
|
# Usage see: https://docs.clan.lol
|
||||||
clan = clan-core.lib.buildClan {
|
clan = clan-core.lib.buildClan {
|
||||||
directory = self;
|
directory = self;
|
||||||
clanName = "__CHANGE_ME__"; # Ensure this is internet wide unique.
|
meta.name = "__CHANGE_ME__"; # Ensure this is internet wide unique.
|
||||||
|
|
||||||
|
# Distributed services, uncomment to enable.
|
||||||
|
# inventory = {
|
||||||
|
# services = {
|
||||||
|
# # This example configures a BorgBackup service
|
||||||
|
# # Check: https://docs.clan.lol/reference/clanModules which ones are available in Inventory
|
||||||
|
# borgbackup.instance_1 = {
|
||||||
|
# roles.server.machines = [ "jon" ];
|
||||||
|
# roles.client.machines = [ "sara" ];
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
|
||||||
# Prerequisite: boot into the installer
|
# Prerequisite: boot into the installer
|
||||||
# See: https://docs.clan.lol/getting-started/installer
|
# See: https://docs.clan.lol/getting-started/installer
|
||||||
|
Loading…
Reference in New Issue
Block a user