UNPKG

pmcf

Version:

Poor mans configuration management

337 lines (291 loc) 7.78 kB
import { isLocalhost } from "ip-utilties"; import { Base, Endpoint } from "pmcf"; import { addType } from "./types.mjs"; import { asArray } from "./utils.mjs"; import { networkAddressProperties } from "./network-support.mjs"; import { DNSRecord, dnsFullName, dnsFormatParameters, dnsMergeParameters } from "./dns-utils.mjs"; const ServiceTypes = { ntp: { endpoints: [{ protocol: "udp", port: 123, tls: false }] }, dns: { endpoints: [{ protocol: "udp", port: 53, tls: false }] }, ldap: { endpoints: [{ protocol: "tcp", port: 389, tls: false }] }, ldaps: { endpoints: [{ protocol: "tcp", port: 636, tls: true }] }, http: { endpoints: [{ protocol: "tcp", port: 80, tls: false }] }, https: { endpoints: [{ protocol: "tcp", port: 443, tls: true }], dnsRecord: { type: "HTTPS", parameters: { alpn: "h2" } } }, http3: { extends: ["https"], type: "https", endpoints: [{ protocol: "tcp", port: 443, tls: true }], dnsRecord: { type: "HTTPS", parameters: { "no-default-alpn": undefined, alpn: "h3" } } }, rtsp: { endpoints: [{ protocol: "tcp", port: 554, tls: false }] }, smtp: { endpoints: [{ protocol: "tcp", port: 25, tls: false }], dnsRecord: { type: "MX" } }, ssh: { endpoints: [{ protocol: "tcp", port: 22, tls: false }] }, imap: { endpoints: [{ protocol: "tcp", port: 143, tls: false }] }, imaps: { endpoints: [{ protocol: "tcp", port: 993, tls: true }] }, dhcp: { endpoints: [{ port: 547, protocol: "udp", tls: false }] }, "dhcpv6-client": { endpoints: [ { protocol: "tcp", port: 546, tls: false }, { protocol: "udp", port: 546, tls: false } ] }, "dhcpv6-server": { endpoints: [{ port: 547, tls: false }] }, smb: { endpoints: [{ protocol: "tcp", port: 445, tls: false }] }, timemachine: { type: "adisk", extends: ["smb"], endpoints: [{ protocol: "tcp", port: 445, tls: false }], dnsRecord: { type: "TXT", parameters: { sys: "waMa=0", adVF: "0x100", dk0: "adVN=MF-TM-999" // adVF: "0x82" } } } }; export const endpointProperties = { port: { type: "number", collection: false, writeable: true }, protocol: { type: "string", collection: false, writeable: true, values: ["tcp", "udp"] }, type: { type: "string", collection: false, writeable: true }, tls: { type: "boolean", collection: false, writeable: false, default: false } }; export const EndpointTypeDefinition = { name: "endpoint", owners: ["service"], priority: 0.4, specializations: {}, properties: endpointProperties }; export const ServiceTypeDefinition = { name: "service", owners: ["host", "cluster", "network_interface"], priority: 0.4, extends: Base.typeDefinition, specializations: {}, factoryFor(owner, value) { const type = value.type ?? value.name; const t = ServiceTypeDefinition.specializations[type]; if (t) { delete value.type; return t.clazz; } return Service; }, properties: { ...networkAddressProperties, ...endpointProperties, ipAddresses: { type: "string", collection: true, writeable: true }, alias: { type: "string", collection: false, writeable: true }, weight: { type: "number", collection: false, writeable: true, default: 1 }, systemd: { type: "string", collection: true, writeable: true } } }; export class Service extends Base { _alias; _weight; _type; _port; _ipAddresses; _systemd; _extends = []; static { addType(this); } static get typeDefinition() { return ServiceTypeDefinition; } constructor(owner, data) { super(owner, data); this.read(data, ServiceTypeDefinition); } set extends(value) { this._extends.push(value); } get extends() { return this._extends; } get network() { return this.host.network; } get host() { return this.owner; } get domainName() { return this.host.domainName; } get ipAddressOrDomainName() { return this.address ?? this.domainName; } get addresses() { return this._ipAddresses ?? this.owner.addresses; } get address() { return this._ipAddresses?.[0] ?? this.host.address; } set ipAddresses(value) { this._ipAddresses = value; } get networks() { return this.host.networks; } endpoints(filter) { const local = this._port === undefined ? { type: this.type } : { type: this.type, port: this._port }; const data = ServiceTypes[this.type]?.endpoints || [ { tls: false } ]; const result = [...this.host.networkAddresses()] .map(na => data.map( d => new Endpoint(this, na, { ...d, ...local }) ) ) .flat(); return filter ? result.filter(filter) : result; } set alias(value) { this._alias = value; } get alias() { return this.extendedProperty("_alias"); } set port(value) { this._port = value; } get port() { return this.endpoints()[0].port; } get protocol() { return this.endpoints()[0].protocol; } get tls() { return this.endpoints()[0].tls; } set weight(value) { this._weight = value; } get weight() { return this.extendedProperty("_weight") ?? this.owner.weight ?? 1; } set type(value) { this._type = value; } get type() { return this._type ?? this.name; } get systemdServices() { return this.extendedProperty("_systemd"); } dnsRecordsForDomainName(domainName, hasSVRRecords) { const records = []; if (this.priority <= 1 && this.alias) { records.push(DNSRecord(this.alias, "CNAME", dnsFullName(domainName))); } if (hasSVRRecords) { for (const ep of this.endpoints( e => e.protocol && e.networkInterface.kind !== "loopback" )) { records.push( DNSRecord( dnsFullName( `_${ServiceTypes[this.type]?.type ?? this.type}._${ ep.protocol }.${domainName}` ), "SRV", this.priority ?? 10, this.weight, ep.port, dnsFullName(this.domainName) ) ); } } const dnsRecord = ServiceTypes[this.type]?.dnsRecord; if (dnsRecord) { let parameters = dnsRecord.parameters; if (parameters) { for (const service of this.findServices()) { if (service !== this) { const r = ServiceTypes[service.type]?.dnsRecord; if (r?.type === dnsRecord.type) { parameters = dnsMergeParameters(parameters, r.parameters); } } } records.push( DNSRecord( dnsFullName(domainName), dnsRecord.type, this.priority ?? 10, ".", dnsFormatParameters(parameters) ) ); } else { records.push( DNSRecord("@", dnsRecord.type, this.priority, dnsFullName(domainName)) ); } } return records; } } export const sortByPriority = (a, b) => a.priority - b.priority; export function serviceAddresses( sources, filter, addressType = "addresses", addressFilter = a => !isLocalhost(a) ) { return asArray(sources) .map(ft => Array.from(ft.findServices(filter))) .flat() .sort(sortByPriority) .map(s => s[addressType]) .flat() .filter(addressFilter); } export function serviceEndpoints(sources, filter, endpointFilter) { return asArray(sources) .map(ft => Array.from(ft.findServices(filter))) .flat() .sort(sortByPriority) .map(service => service.endpoints(endpointFilter)) .flat(); }