pmcf
Version:
Poor mans configuration management
235 lines (204 loc) • 6.95 kB
JavaScript
import { join } from "node:path";
import { FileContentProvider } from "npm-pkgbuild";
import { number_attribute_writable } from "pacc";
import { Owner } from "./owner.mjs";
import { Host } from "./host.mjs";
import { serviceEndpoints } from "pmcf";
import { addType } from "./types.mjs";
import { writeLines } from "./utils.mjs";
const ClusterTypeDefinition = {
name: "cluster",
owners: [Owner.typeDefinition, "network", "location", "root"],
priority: 0.7,
extends: Host.typeDefinition,
attributes: {
routerId: number_attribute_writable,
masters: { type: "network_interface", collection: true, writable: true },
backups: { type: "network_interface", collection: true, writable: true },
members: { type: "network_interface", collection: true, writable: false },
checkInterval: number_attribute_writable
}
};
export class Cluster extends Host {
_masters = [];
_backups = [];
routerId = 100;
checkInterval = 60;
static {
addType(this);
}
static get typeDefinition() {
return ClusterTypeDefinition;
}
set masters(value) {
this._masters.push(value);
value.cluster = this;
}
get masters() {
return this._masters;
}
set backups(value) {
this._backups.push(value);
value.cluster = this;
}
get backups() {
return this._backups;
}
get members() {
return new Set(this.masters).union(new Set(this.backups));
}
async *preparePackages(stagingDir) {
for (const ni of [...this.owner.clusters()].reduce(
(all, cluster) => all.union(cluster.members),
new Set()
)) {
const host = ni.host;
const name = `keepalived-${host.location.name}-${host.name}`;
const packageStagingDir = join(stagingDir, name);
const result = {
sources: [
new FileContentProvider(packageStagingDir + "/")[
Symbol.asyncIterator
]()
],
outputs: host.outputs,
properties: {
name,
description: `${this.typeName} definitions for ${this.fullName}`,
access: "private",
dependencies: ["keepalived>=2.3.4"]
}
};
const cfg = [
"global_defs {",
" notification_email {",
" " + this.administratorEmail,
" }",
` smtp_server ${this.smtp.address()}`,
` notification_email_from keepalived@${host.domainName}`,
" enable_script_security",
" script_user root",
" max_auto_priority 20",
"}",
""
];
for (const cluster of [...this.owner.clusters()].sort((a, b) =>
a.name.localeCompare(b.name)
)) {
cfg.push(`vrrp_instance ${cluster.name} {`);
cfg.push(
` state ${cluster.masters.indexOf(ni) === 0 ? "MASTER" : "BACKUP"}`
);
cfg.push(` interface ${ni.name}`);
for (const na of cluster.networkAddresses(
na => na.networkInterface.kind !== "loopback"
)) {
cfg.push(
` ${
na.family === "IPv4"
? "virtual_ipaddress"
: "virtual_ipaddress_excluded"
} {`
);
cfg.push(
` ${na.cidrAddress} dev ${ni.name} label ${cluster.name}`
);
cfg.push(" }");
}
cfg.push(` virtual_router_id ${cluster.routerId}`);
let reducedPrio = cluster.masters.indexOf(ni);
if (reducedPrio < 0) {
reducedPrio = cluster.backups.indexOf(ni) + 5;
}
cfg.push(` priority ${host.priority - reducedPrio}`);
cfg.push(" smtp_alert");
cfg.push(" advert_int 5");
cfg.push(" authentication {");
cfg.push(" auth_type PASS");
cfg.push(" auth_pass pass1234");
cfg.push(" }");
cfg.push(
` notify_master "/usr/bin/systemctl start ${cluster.name}-master.target"`,
` notify_backup "/usr/bin/systemctl start ${cluster.name}-backup.target"`,
` notify_fault "/usr/bin/systemctl start ${cluster.name}-fault.target"`
);
cfg.push("}", "");
for (const endpoint of serviceEndpoints(cluster, {
services: { type: "http" },
endpoints: e => e.networkInterface.kind !== "loopback"
})) {
cfg.push(`virtual_server ${cluster.address} ${endpoint.port} {`);
cfg.push(` delay_loop ${cluster.checkInterval}`);
cfg.push(" lb_algo wlc");
cfg.push(" persistence_timeout 600");
cfg.push(` protocol ${endpoint.protocol.toUpperCase()}`);
for (const member of this.members) {
const memberService =
member.findService({ type: endpoint.type }) ||
member.host.findService({ type: endpoint.type }); // TODO
cfg.push(` real_server ${member.address} ${memberService.port} {`);
cfg.push(` weight ${memberService.weight}`);
switch (endpoint.type) {
case "dns":
cfg.push(` DNS_CHECK {`);
cfg.push(" type A");
cfg.push(" name google.com");
cfg.push(" }");
break;
case "smtp":
cfg.push(` SMTP_CHECK {`);
cfg.push(" }");
break;
default:
switch (endpoint.protocol) {
case "tcp":
cfg.push(` TCP_CHECK {`);
cfg.push(" connect_timeout 10");
cfg.push(" }");
break;
}
}
cfg.push(" }");
}
cfg.push("}", "");
break; // only one for now
}
await writeLines(
join(packageStagingDir, "/usr/lib/systemd/system"),
`${cluster.name}-master.target`,
[
"[Unit]",
`Description=master state of cluster ${cluster.name}`,
"PartOf=keepalived.service",
`Conflicts=${cluster.name}-fault.target`
]
);
await writeLines(
join(packageStagingDir, "/usr/lib/systemd/system"),
`${cluster.name}-backup.target`,
[
"[Unit]",
`Description=backup state of cluster ${cluster.name}`,
"PartOf=keepalived.service",
`Conflicts=${cluster.name}-fault.target`
]
);
await writeLines(
join(packageStagingDir, "/usr/lib/systemd/system"),
`${cluster.name}-fault.target`,
[
"[Unit]",
`Description=fault state of cluster ${cluster.name}`,
`Conflicts=${cluster.name}-master.target ${cluster.name}-backup.target`
]
);
}
await writeLines(
join(packageStagingDir, "etc/keepalived"),
"keepalived.conf",
cfg
);
yield result;
}
}
}