pmcf
Version:
Poor mans configuration management
481 lines (444 loc) • 12.4 kB
JavaScript
import { join } from "node:path";
import { FileContentProvider } from "npm-pkgbuild";
import { reverseArpa } from "ip-utilties";
import {
string_attribute,
number_attribute_writable,
boolean_attribute_writable_true
} from "pacc";
import {
Service,
sortDescendingByPriority,
ServiceTypeDefinition,
serviceEndpoints,
SUBNET_LOCALHOST_IPV4,
SUBNET_LOCALHOST_IPV6
} from "pmcf";
import { addType } from "../types.mjs";
import { writeLines } from "../utils.mjs";
const KeaServiceTypeDefinition = {
name: "kea",
specializationOf: ServiceTypeDefinition,
owners: ServiceTypeDefinition.owners,
extends: ServiceTypeDefinition,
priority: 0.1,
properties: {
"ddns-send-updates": {
...boolean_attribute_writable_true,
isCommonOption: true
},
"renew-timer": {
...number_attribute_writable,
isCommonOption: true,
default: 900
},
"rebind-timer": {
...number_attribute_writable,
isCommonOption: true,
default: 1800
},
"valid-lifetime": {
...number_attribute_writable,
mandatory: true,
isCommonOption: true,
default: 86400
},
"ddns-conflict-resolution-mode": {
...string_attribute,
writable: true,
isCommonOption: true
//values: ["check-exists-with-dhcid"]
}
},
service: {
extends: ["dhcp"],
services: {
"kea-ddns": {
endpoints: [
{
family: "IPv4",
kind: "loopback",
port: 53001,
protocol: "tcp",
tls: false
}
]
},
/*"kea-control-agent": {
endpoints: [
{
family: "IPv4",
port: 53002,
pathname: "/",
protocol: "tcp",
tls: false
}
]
},*/
"kea-ha-4": {
endpoints: [
{
family: "IPv4",
port: 53003,
pathname: "/",
protocol: "tcp",
tls: false
}
]
},
"kea-ha-6": {
endpoints: [
{
family: "IPv6",
port: 53004,
pathname: "/",
protocol: "tcp",
tls: false
}
]
},
"kea-control-dhcp4": {
endpoints: [
{
family: "unix",
path: "/run/kea/4-ctrl-socket"
}
]
},
"kea-control-dhcp6": {
endpoints: [
{
family: "unix",
path: "/run/kea/6-ctrl-socket"
}
]
},
"kea-control-ddns": {
endpoints: [
{
family: "unix",
path: "/run/kea/ddns-ctrl-socket"
}
]
}
}
}
};
const keaVersion = 3.0;
export const fetureHasHTTPEndpoints = keaVersion >= 3.0;
export class KeaService extends Service {
static {
addType(this);
}
static get typeDefinition() {
return KeaServiceTypeDefinition;
}
constructor(owner, data) {
super(owner, data);
this.read(data, KeaServiceTypeDefinition);
}
get type() {
return KeaServiceTypeDefinition.name;
}
async *preparePackages(dir) {
const ctrlAgentEndpoint = this.endpoint(
fetureHasHTTPEndpoints ? "kea-ha-4" : "kea-control-agent"
);
if (!ctrlAgentEndpoint) {
return;
}
const network = this.network;
const host = this.host;
const name = host.name;
console.log("kea", name, network.name);
const dnsServerEndpoints = serviceEndpoints(network, {
services: {
types: "dns",
priority: ">=300"
},
endpoints: endpoint => endpoint.networkInterface.kind !== "loopback"
});
const packageData = {
dir,
sources: [new FileContentProvider(dir + "/")],
outputs: this.outputs,
properties: {
name: `kea-${this.location.name}-${name}`,
description: `kea definitions for ${this.fullName}@${name}`,
access: "private",
dependencies: [`kea>=${keaVersion}`]
}
};
const peers = async family =>
(
await Array.fromAsync(
network.findServices({
type: "kea",
priority: ">=" + (this.priority < 100 ? this.priority : 100)
})
)
)
.sort(sortDescendingByPriority)
.map((dhcp, i) => {
const ctrlAgentEndpoint = dhcp.endpoint(
fetureHasHTTPEndpoints ? `kea-ha-${family}` : "kea-control-agent"
);
if (ctrlAgentEndpoint) {
return {
name: dhcp.host.name,
role: i === 0 ? "primary" : i > 1 ? "backup" : "standby",
url: ctrlAgentEndpoint.url,
"auto-failover": i <= 1
};
}
})
.filter(p => p != null);
const loggers = [
{
"output-options": [
{
output: "syslog"
}
],
severity: "INFO",
debuglevel: 0
}
];
const commonConfig = async family => {
const cfg = {
"interfaces-config": {
interfaces: listenInterfaces(`IPv${family}`)
},
"control-socket": toUnix(this.endpoint(`kea-control-dhcp${family}`)),
"lease-database": {
type: "memfile",
"lfc-interval": 3600
},
"multi-threading": {
"enable-multi-threading": true,
"thread-pool-size": 2,
"packet-queue-size": 4
},
"expired-leases-processing": {
"reclaim-timer-wait-time": 10,
"flush-reclaimed-timer-wait-time": 25,
"hold-reclaimed-time": 3600,
"max-reclaim-leases": 100,
"max-reclaim-time": 250,
"unwarned-reclaim-cycles": 5
},
"hooks-libraries": [
/*{
library: "/usr/lib/kea/hooks/libdhcp_ddns_tuning.so"
},*/
{
library: "/usr/lib/kea/hooks/libdhcp_lease_cmds.so"
},
{
library: "/usr/lib/kea/hooks/libdhcp_ha.so",
parameters: {
"high-availability": [
{
"this-server-name": name,
mode: "hot-standby",
"heartbeat-delay": 60000,
"max-response-delay": 60000,
"max-ack-delay": 10000,
/*
"multi-threading": {
"enable-multi-threading": true,
"http-dedicated-listener": true,
"http-listener-threads": 2,
"http-client-threads": 2
},*/
peers: await peers(family)
}
]
}
}
],
"dhcp-ddns": dhcpServerDdns,
loggers,
"option-data": [
{
name: family == 4 ? "domain-name-servers" : "dns-servers",
data: dnsServerEndpoints
.filter(endpoint => endpoint.family === `IPv${family}`)
.map(endpoint => endpoint.address)
.join(",")
},
{
name: "domain-search",
data: [...this.domains].join(",")
}
]
};
for (const [key] of Object.entries(
KeaServiceTypeDefinition.properties
).filter(
([key, attribute]) =>
attribute.isCommonOption && this[key] !== undefined
)) {
cfg[key] = this[key];
}
return cfg;
};
const toUnix = endpoint => {
return {
"socket-type": "unix",
"socket-name": endpoint?.path
};
};
/*const ctrlAgent = {
"Control-agent": {
"http-host": ctrlAgentEndpoint.hostname,
"http-port": ctrlAgentEndpoint.port,
"control-sockets": {
dhcp4: toUnix(this.endpoint("kea-control-dhcp4")),
dhcp6: toUnix(this.endpoint("kea-control-dhcp6")),
d2: toUnix(this.endpoint("kea-control-ddns"))
},
loggers
}
};*/
const dnsServersSlot = names =>
names.map(name => {
return {
name,
"dns-servers": dnsServerEndpoints
.filter(endpoint => endpoint.family === "IPv4")
.map(endpoint => {
return { "ip-address": endpoint.address };
})
};
});
const ddnsEndpoint = this.endpoint("kea-ddns");
const subnetPrefixes = new Set(
[...this.subnets]
.filter(s => s != SUBNET_LOCALHOST_IPV4 && s != SUBNET_LOCALHOST_IPV6)
.map(s => s.prefix)
);
const ddns = {
DhcpDdns: {
"ip-address": ddnsEndpoint.address,
port: ddnsEndpoint.port,
"control-socket": toUnix(
this.endpoint(e => e.type === "kea-control-ddns")
),
"tsig-keys": [],
"forward-ddns": {
"ddns-domains": dnsServersSlot([...this.domains])
},
"reverse-ddns": {
"ddns-domains": dnsServersSlot(
[...subnetPrefixes].map(prefix => reverseArpa(prefix))
)
},
loggers
}
};
const dhcpServerDdns = {
"enable-updates": true,
"server-ip": ddnsEndpoint.address,
"server-port": ddnsEndpoint.port,
"max-queue-size": 16,
"ncr-protocol": "UDP",
"ncr-format": "JSON"
};
const hwmap = new Map();
const hostNames = new Set();
for await (const { networkInterface } of network.networkAddresses()) {
if (networkInterface.hwaddr) {
if (!hostNames.has(networkInterface.hostName)) {
hwmap.set(networkInterface.hwaddr, networkInterface);
hostNames.add(networkInterface.hostName);
}
}
}
const reservations = (subnet, family) =>
[...hwmap]
.map(([k, networkInterface]) => {
let ip = {};
let addr = networkInterface.networkAddress(
n => n.family === `IPv${family}`
)?.address;
if (addr && subnet.matchesAddress(addr)) {
ip =
family == 6 ? { "ip-addresses": [addr] } : { "ip-address": addr };
}
return {
"hw-address": k,
...ip,
hostname: networkInterface.domainName,
"client-classes": ["SKIP_DDNS"]
};
})
.sort((a, b) => a.hostname.localeCompare(b.hostname));
const listenInterfaces = family =>
this.endpoints(
endpoint =>
endpoint.type === "dhcp" &&
endpoint.family === family &&
endpoint.networkInterface.kind !== "loopback" &&
endpoint.networkInterface.kind !== "wlan"
).map(
endpoint => `${endpoint.networkInterface.name}/${endpoint.address}`
);
const subnets = [...this.subnets].filter(
s => s !== SUBNET_LOCALHOST_IPV4 && s !== SUBNET_LOCALHOST_IPV6
);
const dhcp4 = {
Dhcp4: {
...(await commonConfig("4")),
subnet4: subnets
.filter(s => s.family === "IPv4")
.map((subnet, index) => {
return {
id: index + 1,
subnet: subnet.longAddress,
pools: subnet.dhcpPools.map(range => {
return { pool: range.join(" - ") };
}),
"option-data": [
{
name: "routers",
data: network.gateway.address
}
],
reservations: reservations(subnet, "4")
};
})
}
};
const dhcp6 = {
Dhcp6: {
...(await commonConfig("6")),
subnet6: subnets
.filter(s => s.family === "IPv6")
.map((subnet, index) => {
return {
id: index + 1,
subnet: subnet.longAddress,
pools: subnet.dhcpPools.map(range => {
return { pool: range.join(" - ") };
}),
reservations: reservations(subnet, "6")
};
})
}
};
for (const [name, data] of Object.entries({
"kea-dhcp-ddns": ddns,
"kea-dhcp4": dhcp4,
"kea-dhcp6": dhcp6
})) {
loggers[0].name = name;
await writeLines(
join(packageData.dir, "etc/kea"),
`${name}.conf`,
JSON.stringify(data, undefined, 2)
);
}
yield packageData;
}
}