1
1
Fork 1
mirror of https://github.com/oddlama/nix-config.git synced 2025-10-10 23:00:39 +02:00

feat(topology): implement nixos-extra-modules wireguard extractor

This commit is contained in:
oddlama 2024-03-16 02:57:04 +01:00
parent b20376f2e4
commit 65890181e9
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
12 changed files with 418 additions and 77 deletions

View file

@ -0,0 +1,18 @@
{
config,
lib,
pkgs,
...
}: let
inherit
(lib)
mkIf
;
in {
topology.self.services = {
vaultwarden = mkIf config.services.vaultwarden.enable {
name = "Vaultwarden";
icon = "${pkgs.vaultwarden.webvault}/share/vaultwarden/vault/images/safari-pinned-tab.svg";
};
};
}

View file

@ -1,2 +1,47 @@
{ {
config,
lib,
...
}: let
inherit
(lib)
concatLists
flip
mkDefault
mkIf
mkMerge
optional
;
in {
#config = mkIf config.systemd.network.enable {
# topology.interfaces = mkMerge (
# # Create interfaces based on systemd.network.netdevs
# concatLists (
# flip mapAttrsToList config.systemd.network.netdevs (
# _unit: netdev:
# optional (netdev ? netdevConfig.Name) {
# ${netdev.netdevConfig.Name} = {
# physical = mkDefault false;
# };
# }
# )
# )
# # Add interface configuration based on systemd.network.networks
# #++ concatLists (
# # flip mapAttrsToList config.systemd.network.networks (
# # _unit: network:
# # optional (network ? matchConfig.Name) {
# # ${network.networkConfig.Name} = {
# # };
# # }
# # )
# #)
# );
# #self.interfaces = {
# #};
# #networks.somenet = {
# # connections = [];
# #};
#};
} }

View file

@ -1,2 +1,84 @@
{ {
config,
lib,
inputs ? {},
...
}: let
inherit
(lib)
flip
mapAttrsToList
mkDefault
mkIf
mkMerge
filter
optionals
;
headOrNull = xs:
if xs == []
then null
else builtins.head xs;
networkId = wgName: "wireguard-${wgName}";
in {
config = mkIf (config ? wireguard) {
# Create networks (this will be duplicated by each node,
# but it doesn't matter and will be merged anyway)
topology.networks = mkMerge (
flip mapAttrsToList config.wireguard (
wgName: _: let
inherit (lib.wireguard inputs wgName) networkCidrs;
in {
${networkId wgName} = {
name = mkDefault "Wireguard network '${wgName}'";
cidrv4 = headOrNull (filter lib.net.ip.isv4 networkCidrs);
cidrv6 = headOrNull (filter lib.net.ip.isv6 networkCidrs);
};
}
)
);
# Assign network and physical connections to related interfaces
topology.self.interfaces = mkMerge (
flip mapAttrsToList config.wireguard (
wgName: wgCfg: let
inherit
(lib.wireguard inputs wgName)
participatingClientNodes
participatingServerNodes
wgCfgOf
;
isServer = wgCfg.server.host != null;
filterSelf = filter (x: x != config.node.name);
# All nodes that use our node as the via into the wireguard network
ourClientNodes =
optionals isServer
(filter (n: (wgCfgOf n).client.via == config.node.name) participatingClientNodes);
# The list of peers that are "physically" connected in the wireguard network,
# meaning they communicate directly with each other.
connectedPeers =
if isServer
then
# Other servers in the same network
filterSelf participatingServerNodes
# Our clients
++ ourClientNodes
else [wgCfg.client.via];
in {
${wgCfg.linkName} = {
network = networkId wgName;
virtual = true;
physicalConnections = flip map connectedPeers (peer: {
node = peer;
interface = (wgCfgOf peer).linkName;
});
};
}
)
);
};
} }

View file

@ -17,6 +17,9 @@ in {
# Allow simple alias to set/get attributes of this node # Allow simple alias to set/get attributes of this node
(mkAliasOptionModule ["topology" "self"] ["topology" "nodes" config.topology.id]) (mkAliasOptionModule ["topology" "self"] ["topology" "nodes" config.topology.id])
] ]
# Include extractors
++ map (x: ./extractors/${x}) (attrNames (builtins.readDir ./extractors))
# Include common topology options
++ flip map (attrNames (builtins.readDir ../options)) (x: ++ flip map (attrNames (builtins.readDir ../options)) (x:
import ../options/${x} ( import ../options/${x} (
module: module:
@ -48,49 +51,4 @@ in {
# Ensure a node exists for this host # Ensure a node exists for this host
nodes.${config.topology.id} = {}; nodes.${config.topology.id} = {};
}; };
#config.topology = mkMerge [
# {
# ################### TODO user config! #################
# id = config.node.name;
# ################### END user config #################
# guests =
# flip mapAttrsToList (config.microvm.vms or {})
# (_: vmCfg: vmCfg.config.config.topology.id);
# # TODO: container
# disks =
# flip mapAttrs (config.disko.devices.disk or {})
# (_: _: {});
# # TODO: zfs pools from disko / fileSystems
# # TODO: microvm shares
# # TODO: container shares
# # TODO: OCI containers shares
# interfaces = let
# isNetwork = netDef: (netDef.matchConfig != {}) && (netDef.address != [] || netDef.DHCP != null);
# macsByName = mapAttrs' (flip nameValuePair) (config.networking.renameInterfacesByMac or {});
# netNameFor = netName: netDef:
# netDef.matchConfig.Name
# or (
# if netDef ? matchConfig.MACAddress && macsByName ? ${netDef.matchConfig.MACAddress}
# then macsByName.${netDef.matchConfig.MACAddress}
# else lib.trace "Could not derive network name for systemd network ${netName} on host ${config.node.name}, using unit name as fallback." netName
# );
# netMACFor = netDef: netDef.matchConfig.MACAddress or null;
# networks = filterAttrs (_: isNetwork) (config.systemd.network.networks or {});
# in
# flip mapAttrs' networks (netName: netDef:
# nameValuePair (netNameFor netName netDef) {
# mac = netMACFor netDef;
# addresses =
# if netDef.address != []
# then netDef.address
# else ["DHCP"];
# });
# # TODO: for each nftable zone show open ports
# }
#];
} }

View file

@ -17,8 +17,8 @@ in
default = {}; default = {};
type = types.attrsOf (types.submodule (submod: { type = types.attrsOf (types.submodule (submod: {
options = { options = {
name = mkOption { id = mkOption {
description = "The name of this disk"; description = "The id of this disk";
default = submod.config._module.args.name; default = submod.config._module.args.name;
readOnly = true; readOnly = true;
type = types.str; type = types.str;

View file

@ -18,8 +18,8 @@ in
default = {}; default = {};
type = types.attrsOf (types.submodule (submod: { type = types.attrsOf (types.submodule (submod: {
options = { options = {
name = mkOption { id = mkOption {
description = "The name of this firewall rule"; description = "The id of this firewall rule";
type = types.str; type = types.str;
readOnly = true; readOnly = true;
default = submod.config._module.args.name; default = submod.config._module.args.name;

View file

@ -5,6 +5,9 @@ f: {
}: let }: let
inherit inherit
(lib) (lib)
attrValues
flatten
flip
mkOption mkOption
types types
; ;
@ -18,36 +21,83 @@ in
default = {}; default = {};
type = types.attrsOf (types.submodule (submod: { type = types.attrsOf (types.submodule (submod: {
options = { options = {
name = mkOption { id = mkOption {
description = "The name of this interface"; description = "The id of this interface";
type = types.str; type = types.str;
readOnly = true; readOnly = true;
default = submod.config._module.args.name; default = submod.config._module.args.name;
}; };
virtual = mkOption {
description = "Whether this is a virtual interface.";
type = types.bool;
};
mac = mkOption { mac = mkOption {
description = "The MAC address of this interface, if known."; description = "The MAC address of this interface, if known.";
default = null; default = null;
type = types.nullOr types.str; type = types.nullOr types.str;
}; };
addresses = mkOption { #addresses = mkOption {
description = "The configured address(es), or a descriptive string (like DHCP)."; # description = "The configured address(es), or a descriptive string (like DHCP).";
type = types.listOf types.str; # type = types.listOf types.str;
}; #};
#gateway = mkOption {
# description = "The configured gateway, if any";
# type = types.nullOr types.str;
# default = null;
#};
network = mkOption { network = mkOption {
description = '' description = "The id of the network to which this interface belongs, if any.";
The global name of the attached/spanned network.
If this is given, this interface can be shown in the network graph.
'';
default = null; default = null;
type = types.nullOr types.str; type = types.nullOr types.str;
}; };
physicalConnections = mkOption {
description = "A list of other node interfaces to which this node is physically connected.";
default = [];
type = types.listOf (types.submodule {
options = {
node = mkOption {
description = "The other node id.";
type = types.str;
};
interface = mkOption {
description = "The other node's interface id.";
type = types.str;
};
};
});
};
}; };
})); }));
}; };
}; };
}); });
}; };
config = {
assertions = flatten (flip map (attrValues config.nodes) (
node:
flip map (attrValues node.interfaces) (
interface:
[
{
assertion = config.networks ? ${interface.network};
message = "topology: nodes.${node.id}.interfaces.${interface.id} refers to an unknown network '${interface.network}'";
}
]
++ flip map interface.physicalConnections (
physicalConnection: {
assertion = config.nodes ? ${physicalConnection.node} && config.nodes.${physicalConnection.node}.interfaces ? ${physicalConnection.interface};
message = "topology: nodes.${node.id}.interfaces.${interface.id}.physicalConnections refers to an unknown node/interface nodes.${physicalConnection.node}.interfaces.${physicalConnection.interface}";
}
)
)
));
};
} }

View file

@ -0,0 +1,57 @@
f: {
lib,
config,
...
}: let
inherit
(lib)
mkOption
types
;
in
f {
options.networks = mkOption {
default = {};
description = ''
'';
type = types.attrsOf (types.submodule (networkSubmod: {
options = {
id = mkOption {
description = "The id of this network";
default = networkSubmod.config._module.args.name;
readOnly = true;
type = types.str;
};
name = mkOption {
description = "The name of this network";
type = types.str;
default = "Unnamed network '${networkSubmod.config.id}'";
};
color = mkOption {
description = "The color of this network";
default = "random";
type = types.either (types.strMatching "^#[0-9a-f]{6}$") (types.enum ["random"]);
};
cidrv4 = mkOption {
description = "The CIDRv4 address space of this network or null if it doesn't use ipv4";
default = null;
#type = types.nullOr types.net.cidrv4;
type = types.nullOr types.str;
};
cidrv6 = mkOption {
description = "The CIDRv6 address space of this network or null if it doesn't use ipv6";
default = null;
#type = types.nullOr types.net.cidrv6;
type = types.nullOr types.str;
};
# FIXME: vlan ids
# FIXME: nat to [other networks] (happening on node XY)
};
}));
};
}

View file

@ -5,6 +5,7 @@ f: {
}: let }: let
inherit inherit
(lib) (lib)
literalExpression
mkOption mkOption
types types
; ;
@ -16,17 +17,28 @@ in
''; '';
type = types.attrsOf (types.submodule (nodeSubmod: { type = types.attrsOf (types.submodule (nodeSubmod: {
options = { options = {
name = mkOption { id = mkOption {
description = "The name of this node"; description = "The id of this node";
default = nodeSubmod.config._module.args.name; default = nodeSubmod.config._module.args.name;
readOnly = true; readOnly = true;
type = types.str; type = types.str;
}; };
name = mkOption {
description = "The name of this node";
type = types.str;
default = nodeSubmod.config.id;
defaultText = literalExpression ''"<name>"'';
};
# FIXME: TODO emoji / icon
# FIXME: TODO hardware description "Odroid H3"
# FIXME: TODO hardware image
# FIXME: TODO are these good types? how about nixos vs router vs ...
type = mkOption { type = mkOption {
description = "TODO"; description = "TODO";
default = "normal"; type = types.enum ["nixos" "microvm" "nixos-container"];
type = types.enum ["normal" "microvm" "nixos-container"];
}; };
parent = mkOption { parent = mkOption {

View file

@ -0,0 +1,56 @@
f: {
lib,
config,
...
}: let
inherit
(lib)
mkOption
types
;
in
f {
options.nodes = mkOption {
type = types.attrsOf (types.submodule {
options = {
services = mkOption {
description = "TODO";
default = {};
type = types.attrsOf (types.submodule (submod: {
options = {
id = mkOption {
description = "The id of this service";
type = types.str;
readOnly = true;
default = submod.config._module.args.name;
};
name = mkOption {
description = "The name of this service";
type = types.str;
};
icon = mkOption {
description = "The icon for this service";
type = types.nullOr types.path;
default = null;
};
url = mkOption {
description = "The URL under which the service is reachable, if any.";
type = types.nullOr types.str;
default = null;
};
listenAddresses = mkOption {
description = "The addresses on which this service listens.";
type = types.nullOr types.str;
default = null;
};
};
}));
};
};
});
};
}

View file

@ -7,6 +7,8 @@
(lib) (lib)
attrNames attrNames
concatLists concatLists
concatStringsSep
filter
filterAttrs filterAttrs
flip flip
getAttrFromPath getAttrFromPath
@ -54,6 +56,35 @@ in {
readOnly = true; readOnly = true;
defaultText = literalExpression ''config.renderers.${config.renderer}.output''; defaultText = literalExpression ''config.renderers.${config.renderer}.output'';
}; };
assertions = mkOption {
internal = true;
default = [];
example = [
{
assertion = false;
message = "you can't enable this for that reason";
}
];
description = lib.mdDoc ''
This option allows modules to express conditions that must
hold for the evaluation of the topology configuration to
succeed, along with associated error messages for the user.
'';
type = types.listOf (types.submodule {
options = {
assertion = mkOption {
description = "The thing to assert.";
type = types.bool;
};
message = mkOption {
description = "The error message.";
type = types.str;
};
};
});
};
}; };
config = let config = let
@ -68,10 +99,14 @@ in {
)) ))
); );
in { in {
output = output = let
mkIf (config.renderer != null) failedAssertions = map (x: x.message) (filter (x: !x.assertion) config.assertions);
(mkDefault config.renderers.${config.renderer}.output); in
if failedAssertions != []
then throw "\nFailed assertions:\n${concatStringsSep "\n" (map (x: "- ${x}") failedAssertions)}"
else mkIf (config.renderer != null) (mkDefault config.renderers.${config.renderer}.output);
nodes = aggregate ["nodes"]; nodes = aggregate ["nodes"];
networks = aggregate ["networks"];
}; };
} }

View file

@ -6,25 +6,53 @@
}: let }: let
inherit inherit
(lib) (lib)
attrValues
concatLines concatLines
mapAttrsToList
; ;
toD2 = _nodeName: node: '' #toD2 = _nodeName: node: ''
${node.name}: |md # ${node.id}: |md
# ${node.name} # # ${node.id}
## Disks: # ## Disks:
${concatLines (mapAttrsToList (_: v: "- ${v.name}") node.disks)} # ${concatLines (mapAttrsToList (_: v: "- ${v.id}") node.disks)}
## Interfaces: # ## Interfaces:
${concatLines (mapAttrsToList (_: v: "- ${v.name}, mac ${toString v.mac}, addrs ${toString v.addresses}, network ${toString v.network}") node.interfaces)} # ${concatLines (mapAttrsToList (_: v: "- ${v.id}, mac ${toString v.mac}, addrs ${toString v.addresses}, network ${toString v.network}") node.interfaces)}
## Firewall Zones: # ## Firewall Zones:
${concatLines (mapAttrsToList (_: v: "- ${v.name}, mac ${toString v.mac}, addrs ${toString v.addresses}, network ${toString v.network}") node.firewallRules)} # ${concatLines (mapAttrsToList (_: v: "- ${v.id}, mac ${toString v.mac}, addrs ${toString v.addresses}, network ${toString v.network}") node.firewallRules)}
# ## Services:
# ${concatLines (mapAttrsToList (_: v: "- ${v.id}, name ${toString v.name}, icon ${toString v.icon}, url ${toString v.url}") node.services)}
# |
#'';
netToD2 = net: ''
${net.id}: |md
# ${net.name}
${net.cidrv4}
${net.cidrv6}
| |
''; '';
nodeInterfaceToD2 = node: interface: ''
${node.id}.${interface.id}: |md
## ${interface.id}
|
${node.id}.${interface.id} -> ${interface.network}
'';
nodeToD2 = node: ''
${node.id}: |md
# ${node.name}
|
${concatLines (map (nodeInterfaceToD2 node) (attrValues node.interfaces))}
'';
in in
pkgs.writeText "network.d2" '' pkgs.writeText "network.d2" ''
${concatLines (mapAttrsToList toD2 config.nodes)} ${concatLines (map netToD2 (attrValues config.networks))}
${concatLines (map nodeToD2 (attrValues config.nodes))}
'' ''