pmcf
Version:
Poor mans configuration management
532 lines (450 loc) • 12 kB
JavaScript
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { FileContentProvider } from "npm-pkgbuild";
import {
string_attribute,
string_attribute_writable,
string_collection_attribute_writable,
number_attribute_writable,
boolean_attribute_false
} from "pacc";
import { ServiceOwner, Base, addresses } from "pmcf";
import { networkAddressAttributes } from "./network-support.mjs";
import { addHook } from "./hooks.mjs";
import {
domainFromDominName,
domainName,
writeLines,
sectionLines,
asArray
} from "./utils.mjs";
import { addType } from "./types.mjs";
import { loadHooks } from "./hooks.mjs";
import { generateMachineInfo, generateKnownHosts } from "./host-utils.mjs";
import { NetworkInterfaceTypeDefinition } from "./network-interfaces/network-interface.mjs";
const HostTypeDefinition = {
name: "host",
priority: 0.5,
owners: ["owner", "network", "root"],
extends: Base.typeDefinition,
attributes: {
...networkAddressAttributes,
networkInterfaces: {
type: "network_interface",
collection: true,
writable: true
},
services: { type: "service", collection: true, writable: true },
aliases: string_collection_attribute_writable,
os: {
...string_attribute_writable,
values: ["osx", "windows", "linux"]
},
"machine-id": string_attribute_writable,
distribution: string_attribute_writable,
deployment: {
...string_attribute_writable,
values: ["production", "development"]
},
weight: number_attribute_writable,
serial: string_attribute_writable,
vendor: string_attribute_writable,
keymap: string_attribute_writable,
chassis: {
...string_attribute_writable,
values: [
"phone",
"tablet",
"router",
"gateway",
"desktop",
"notebook",
"server",
"monitor",
"camera",
"inverter",
"battery",
"virtual",
"dehumidifier"
]
},
architecture: {
...string_attribute_writable,
values: ["x86", "x86_64", "aarch64", "armv7"]
},
replaces: string_collection_attribute_writable,
depends: string_collection_attribute_writable,
provides: string_collection_attribute_writable,
extends: { type: "host", collection: true, writable: true },
model: string_attribute,
isModel: boolean_attribute_false
}
};
export class Host extends ServiceOwner {
_extends = [];
_aliases = new Set();
_networkInterfaces = new Map();
_provides = new Set();
_replaces = new Set();
_depends = new Set();
_os;
_distribution;
_deployment;
_chassis;
_vendor;
_architecture;
_serial;
_keymap;
static {
addType(this);
}
static get typeDefinition() {
return HostTypeDefinition;
}
read(data, type) {
super.read(data, type);
if (data?.extends) {
this.finalize(() => {
for (const host of this.extends) {
host.execFinalize();
this._applyExtends(host);
}
});
}
this.extra = data.extra;
}
_applyExtends(host) {
for (const service of host.services) {
//present.extends.push(service);
this.services = service.forOwner(this);
}
for (const [name, ni] of host.networkInterfaces) {
if (ni.isTemplate) {
} else {
let present = this._networkInterfaces.get(name);
if (!present) {
present = ni.forOwner(this);
this._networkInterfaces.set(name, present);
}
present.extends.push(ni);
}
}
}
_traverse(...args) {
if (super._traverse(...args)) {
for (const ni of this.networkInterfaces.values()) {
ni._traverse(...args);
}
return true;
}
return false;
}
set serial(value) {
this._serial = value;
}
get serial() {
return this.extendedProperty("_serial");
}
set deployment(value) {
this._deployment = value;
}
get deployment() {
return this.extendedProperty("_deployment");
}
set chassis(value) {
this._chassis = value;
}
get chassis() {
return this.extendedProperty("_chassis");
}
set vendor(value) {
this._vendor = value;
}
get vendor() {
return this.extendedProperty("_vendor");
}
set keymap(value) {
this._keymap = value;
}
get keymap() {
return this.extendedProperty("_keymap");
}
set architecture(value) {
this._architecture = value;
}
get architecture() {
return this.extendedProperty("_architecture");
}
get derivedPackaging() {
return this.extends.reduce((a, c) => a.union(c.packaging), new Set());
}
get isTemplate() {
return this.isModel || this.name?.match(/services\//); // TODO
}
get isModel() {
return this._vendor || this._chassis ? true : false;
}
get model() {
return this.extends.find(h => h.isModel);
}
set aliases(value) {
if (value instanceof Set) {
this._aliases = this._aliases.union(value);
} else {
this._aliases.add(value);
}
}
get aliases() {
return this.extends.reduce((a, c) => a.union(c.aliases), this._aliases);
}
set extends(value) {
this._extends.push(value);
}
get extends() {
return this._extends;
}
set provides(value) {
if (value instanceof Set) {
this._provides = this._provides.union(value);
} else {
this._provides.add(value);
}
}
get provides() {
return this.expand(
this.extends.reduce((a, c) => a.union(c.provides), this._provides)
);
}
set replaces(value) {
if (value instanceof Set) {
this._replaces = this._replaces.union(value);
} else {
this._replaces.add(value);
}
}
get replaces() {
return this.expand(
this.extends.reduce((a, c) => a.union(c.replaces), this._replaces)
);
}
set depends(value) {
if (value instanceof Set) {
this._depends = this._depends.union(value);
} else {
this._depends.add(value);
}
}
get depends() {
return this.expand(
this.extends.reduce((a, c) => a.union(c.depends), this._depends)
);
}
set os(value) {
this._os = value;
}
get os() {
return this.extendedProperty("_os");
}
set distribution(value) {
this._distribution = value;
}
get distribution() {
return this.extendedProperty("_distribution");
}
get modelName() {
return this.model?.hostName;
}
get hostName() {
const parts = this.name.split(/\//);
return parts[parts.length - 1];
}
get foreignDomainNames() {
return [...this.aliases].filter(n => n.split(".").length > 1);
}
get foreignDomains() {
return new Set(
[...this.aliases].map(n => domainFromDominName(n, this.domain))
);
}
get domains() {
return this.foreignDomains.union(this.localDomains);
}
get directDomainNames() {
return new Set(
[this.hostName, ...this.aliases].map(n => domainName(n, this.domain))
);
}
get domainNames() {
return new Set(
[
...[...this.networkInterfaces.values()].reduce(
(all, networkInterface) => all.union(networkInterface.domainNames),
this.directDomainNames
)
].map(n => domainName(n, this.domain))
);
}
get domainName() {
return domainName(this.hostName, this.domain);
}
*domainNamesIn(domain) {
for (const domainName of this.domainNames) {
if (domain === domainFromDominName(domainName)) {
yield domainName;
}
}
}
get clusters() {
const clusters = new Set();
for (const ni of this.networkInterfaces.values()) {
if (ni.cluster) {
clusters.add(ni.cluster);
}
}
return clusters;
}
get host() {
return this;
}
*hosts() {
yield this;
}
typeNamed(typeName, name) {
if (typeName === NetworkInterfaceTypeDefinition.name) {
const ni = this._networkInterfaces.get(name);
if (ni) {
return ni;
}
}
return super.typeNamed(typeName, name);
}
named(name) {
const ni = this._networkInterfaces.get(name);
if (ni) {
return ni;
}
return super.named(name);
}
get network() {
for (const ni of this.networkInterfaces.values()) {
if (ni._kind !== "loopback" && ni._network) {
return ni._network;
}
}
return super.network;
}
get networks() {
return new Set(
[...this.networkInterfaces.values()]
.filter(ni => ni._network)
.map(ni => ni._network)
);
}
get networkInterfaces() {
return this._networkInterfaces;
}
set networkInterfaces(networkInterface) {
this._networkInterfaces.set(networkInterface.name, networkInterface);
if (!this.isTemplate) {
networkInterface.network?.addObject(this);
}
}
*networkAddresses(filter) {
for (const networkInterface of this.networkInterfaces.values()) {
yield* networkInterface.networkAddresses(filter);
}
}
get address() {
return this.addresses[0];
}
get addresses() {
return addresses(this.networkAddresses());
}
get subnets() {
const sn = new Map();
for (const networkInterface of this.networkInterfaces.values()) {
for (const s of networkInterface.subnets()) {
sn.set(s.address, s);
}
}
return new Set(sn.values());
}
async publicKey(type = "ed25519") {
return readFile(join(this.directory, `ssh_host_${type}_key.pub`), "utf8");
}
async *preparePackages(dir) {
let packageData = {
dir,
sources: [
new FileContentProvider(
{ base: this.directory, pattern: "*.pub" },
{ destination: "/etc/ssh/", mode: 0o644 }
),
new FileContentProvider(
{ base: this.directory, pattern: "*_key" },
{ destination: "/etc/ssh/", mode: 0o600 }
),
new FileContentProvider(
{ base: this.directory, pattern: "credential.secret" },
{ destination: "/var/lib/systemd/", mode: 0o400 }
),
new FileContentProvider(dir + "/")
],
outputs: this.outputs,
properties: {
name: `${this.typeName}-${this.owner.name}-${this.name}`,
description: `${this.typeName} definitions for ${this.fullName}`,
access: "private",
dependencies: [
`${this.location.typeName}-${this.location.name}`,
...this.depends
],
provides: [...this.provides],
replaces: [`mf-${this.hostName}`, ...this.replaces],
requires: [],
backup: "root/.ssh/known_hosts",
hooks: await loadHooks(
{},
new URL("host.install", import.meta.url).pathname
)
}
};
for (const ni of this.networkInterfaces.values()) {
await ni.systemdDefinitions(packageData);
}
if (this.keymap) {
await writeLines(dir, "etc/vconsole.conf", `KEYMAP=${this.keymap}`);
}
await generateMachineInfo(this, packageData);
await generateKnownHosts(this.owner.hosts(), join(dir, "root", ".ssh"));
for (const service of this.services) {
if (service.systemdConfigs) {
for (const { serviceName, configFileName, content } of asArray(
service.systemdConfigs(this.name)
)) {
await writeLines(dir, configFileName, sectionLines(...content));
addHook(
packageData.properties.hooks,
"post_install",
`systemctl enable ${serviceName}`
);
}
}
}
yield packageData;
if (this.extra) {
packageData = {
dir,
sources: [new FileContentProvider(join(this.directory, "extra") + "/")],
outputs: this.outputs,
properties: {
name: `${this.typeName}-extra-${this.owner.name}-${this.name}`,
description: `additional files for ${this.fullName}`,
access: "private",
dependencies: [`${this.typeName}-${this.owner.name}-${this.name}`]
}
};
yield packageData;
}
}
}