mirror of
https://github.com/oddlama/nix-config.git
synced 2025-10-10 14:50:40 +02:00
feat(topology): implement nixos-extra-modules wireguard extractor
This commit is contained in:
parent
b20376f2e4
commit
65890181e9
12 changed files with 418 additions and 77 deletions
18
topology/nixos/extractors/services.nix
Normal file
18
topology/nixos/extractors/services.nix
Normal 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";
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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 = [];
|
||||
# #};
|
||||
#};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -17,6 +17,9 @@ in {
|
|||
# Allow simple alias to set/get attributes of this node
|
||||
(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:
|
||||
import ../options/${x} (
|
||||
module:
|
||||
|
@ -48,49 +51,4 @@ in {
|
|||
# Ensure a node exists for this host
|
||||
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
|
||||
# }
|
||||
#];
|
||||
}
|
||||
|
|
|
@ -17,8 +17,8 @@ in
|
|||
default = {};
|
||||
type = types.attrsOf (types.submodule (submod: {
|
||||
options = {
|
||||
name = mkOption {
|
||||
description = "The name of this disk";
|
||||
id = mkOption {
|
||||
description = "The id of this disk";
|
||||
default = submod.config._module.args.name;
|
||||
readOnly = true;
|
||||
type = types.str;
|
||||
|
|
|
@ -18,8 +18,8 @@ in
|
|||
default = {};
|
||||
type = types.attrsOf (types.submodule (submod: {
|
||||
options = {
|
||||
name = mkOption {
|
||||
description = "The name of this firewall rule";
|
||||
id = mkOption {
|
||||
description = "The id of this firewall rule";
|
||||
type = types.str;
|
||||
readOnly = true;
|
||||
default = submod.config._module.args.name;
|
||||
|
|
|
@ -5,6 +5,9 @@ f: {
|
|||
}: let
|
||||
inherit
|
||||
(lib)
|
||||
attrValues
|
||||
flatten
|
||||
flip
|
||||
mkOption
|
||||
types
|
||||
;
|
||||
|
@ -18,36 +21,83 @@ in
|
|||
default = {};
|
||||
type = types.attrsOf (types.submodule (submod: {
|
||||
options = {
|
||||
name = mkOption {
|
||||
description = "The name of this interface";
|
||||
id = mkOption {
|
||||
description = "The id of this interface";
|
||||
type = types.str;
|
||||
readOnly = true;
|
||||
default = submod.config._module.args.name;
|
||||
};
|
||||
|
||||
virtual = mkOption {
|
||||
description = "Whether this is a virtual interface.";
|
||||
type = types.bool;
|
||||
};
|
||||
|
||||
mac = mkOption {
|
||||
description = "The MAC address of this interface, if known.";
|
||||
default = null;
|
||||
type = types.nullOr types.str;
|
||||
};
|
||||
|
||||
addresses = mkOption {
|
||||
description = "The configured address(es), or a descriptive string (like DHCP).";
|
||||
type = types.listOf types.str;
|
||||
};
|
||||
#addresses = mkOption {
|
||||
# description = "The configured address(es), or a descriptive string (like DHCP).";
|
||||
# type = types.listOf types.str;
|
||||
#};
|
||||
|
||||
#gateway = mkOption {
|
||||
# description = "The configured gateway, if any";
|
||||
# type = types.nullOr types.str;
|
||||
# default = null;
|
||||
#};
|
||||
|
||||
network = mkOption {
|
||||
description = ''
|
||||
The global name of the attached/spanned network.
|
||||
If this is given, this interface can be shown in the network graph.
|
||||
'';
|
||||
description = "The id of the network to which this interface belongs, if any.";
|
||||
default = null;
|
||||
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}";
|
||||
}
|
||||
)
|
||||
)
|
||||
));
|
||||
};
|
||||
}
|
||||
|
|
57
topology/options/networks.nix
Normal file
57
topology/options/networks.nix
Normal 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)
|
||||
};
|
||||
}));
|
||||
};
|
||||
}
|
|
@ -5,6 +5,7 @@ f: {
|
|||
}: let
|
||||
inherit
|
||||
(lib)
|
||||
literalExpression
|
||||
mkOption
|
||||
types
|
||||
;
|
||||
|
@ -16,17 +17,28 @@ in
|
|||
'';
|
||||
type = types.attrsOf (types.submodule (nodeSubmod: {
|
||||
options = {
|
||||
name = mkOption {
|
||||
description = "The name of this node";
|
||||
id = mkOption {
|
||||
description = "The id of this node";
|
||||
default = nodeSubmod.config._module.args.name;
|
||||
readOnly = true;
|
||||
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 {
|
||||
description = "TODO";
|
||||
default = "normal";
|
||||
type = types.enum ["normal" "microvm" "nixos-container"];
|
||||
type = types.enum ["nixos" "microvm" "nixos-container"];
|
||||
};
|
||||
|
||||
parent = mkOption {
|
||||
|
|
56
topology/options/services.nix
Normal file
56
topology/options/services.nix
Normal 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;
|
||||
};
|
||||
};
|
||||
}));
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
|
@ -7,6 +7,8 @@
|
|||
(lib)
|
||||
attrNames
|
||||
concatLists
|
||||
concatStringsSep
|
||||
filter
|
||||
filterAttrs
|
||||
flip
|
||||
getAttrFromPath
|
||||
|
@ -54,6 +56,35 @@ in {
|
|||
readOnly = true;
|
||||
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
|
||||
|
@ -68,10 +99,14 @@ in {
|
|||
))
|
||||
);
|
||||
in {
|
||||
output =
|
||||
mkIf (config.renderer != null)
|
||||
(mkDefault config.renderers.${config.renderer}.output);
|
||||
output = let
|
||||
failedAssertions = map (x: x.message) (filter (x: !x.assertion) config.assertions);
|
||||
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"];
|
||||
networks = aggregate ["networks"];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,25 +6,53 @@
|
|||
}: let
|
||||
inherit
|
||||
(lib)
|
||||
attrValues
|
||||
concatLines
|
||||
mapAttrsToList
|
||||
;
|
||||
|
||||
toD2 = _nodeName: node: ''
|
||||
${node.name}: |md
|
||||
# ${node.name}
|
||||
#toD2 = _nodeName: node: ''
|
||||
# ${node.id}: |md
|
||||
# # ${node.id}
|
||||
|
||||
## Disks:
|
||||
${concatLines (mapAttrsToList (_: v: "- ${v.name}") node.disks)}
|
||||
# ## Disks:
|
||||
# ${concatLines (mapAttrsToList (_: v: "- ${v.id}") node.disks)}
|
||||
|
||||
## Interfaces:
|
||||
${concatLines (mapAttrsToList (_: v: "- ${v.name}, mac ${toString v.mac}, addrs ${toString v.addresses}, network ${toString v.network}") node.interfaces)}
|
||||
# ## Interfaces:
|
||||
# ${concatLines (mapAttrsToList (_: v: "- ${v.id}, mac ${toString v.mac}, addrs ${toString v.addresses}, network ${toString v.network}") node.interfaces)}
|
||||
|
||||
## Firewall Zones:
|
||||
${concatLines (mapAttrsToList (_: v: "- ${v.name}, mac ${toString v.mac}, addrs ${toString v.addresses}, network ${toString v.network}") node.firewallRules)}
|
||||
# ## Firewall Zones:
|
||||
# ${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
|
||||
pkgs.writeText "network.d2" ''
|
||||
${concatLines (mapAttrsToList toD2 config.nodes)}
|
||||
${concatLines (map netToD2 (attrValues config.networks))}
|
||||
${concatLines (map nodeToD2 (attrValues config.nodes))}
|
||||
''
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue