From 9a2e57ce4c0465a0a360b57afed6878afb59b513 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Sat, 18 Oct 2025 22:18:06 -0400 Subject: [PATCH] configure(SSOT): YAML + export CLI (Prometheus/Icinga/DNS/Ansible/NetBox) --- configure/configure.yaml | 15 ++++++++++ configure/export.mjs | 63 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 configure/configure.yaml create mode 100644 configure/export.mjs diff --git a/configure/configure.yaml b/configure/configure.yaml new file mode 100644 index 0000000..a79fb69 --- /dev/null +++ b/configure/configure.yaml @@ -0,0 +1,15 @@ +meta: + domain: "chezlepro.ca" + site_default: "dc1" + tenant_default: "clp" + tenant_id_map: { clp: 11 } + vni_formula: "VNI = tenant_id * 10000 + VLAN" + dns_defaults: { ttl: 300 } +inventory: + pve.infra.dc1.chezlepro.ca: + layer: 1 + layer_name: infra + role: proxmox + plan: VPN + site: dc1 + ip_internal: 10.0.0.10 diff --git a/configure/export.mjs b/configure/export.mjs new file mode 100644 index 0000000..4238eb6 --- /dev/null +++ b/configure/export.mjs @@ -0,0 +1,63 @@ +import fs from 'node:fs' +import path from 'node:path' +import YAML from 'js-yaml' + +const root = YAML.load(fs.readFileSync('configure/configure.yaml','utf8'))||{} +const inv = root.inventory||{} +const ttl = root?.meta?.dns_defaults?.ttl || 300 +const zone = root?.meta?.domain || 'example.local' + +const derive = (k,h) => { + const x={...h,fqdn:k} + if(!x.plan) x.plan = Number(x.layer)<=4?'VPN':'PKI' + if(!x.site) x.site = root?.meta?.site_default || 'dc1' + if(!x.tenant && x.plan==='PKI') x.tenant = root?.meta?.tenant_default || 'clp' + const vlan=Number(x.vlan), tid=(root?.meta?.tenant_id_map||{})[x.tenant||root?.meta?.tenant_default] + if((x.vni===null||x.vni===undefined||x.vni==='') && Number.isFinite(vlan) && tid) x.vni = tid*10000+vlan + x.monitoring=x.monitoring||{}; x.monitoring.prometheus=x.monitoring.prometheus||{} + if(!x.monitoring.prometheus.job){ const ln=(x.layer_name||'').toLowerCase(); x.monitoring.prometheus.job=ln||'node' } + x.dns=x.dns||{ ttl, records:[] } + return x +} + +const prom = () => { + const jobs={} + for(const [k,v] of Object.entries(inv)){ const h=derive(k,v); const j=h?.monitoring?.prometheus?.job||'node'; (jobs[j]??=[]).push(h.fqdn) } + return YAML.dump({ scrape_configs: Object.entries(jobs).map(([j,t])=>({job_name:j, static_configs:[{targets:t}]})) }) +} +const icinga = () => { + const out=[] + for(const [k,v] of Object.entries(inv)){ + const h=derive(k,v); const addr=h.plan==='VPN'?(h.ip_internal||''):(h.ip_federative||h.ip_internal||'') + const groups=(h.hostgroups||[]).join(','); const checks=(h.checks||[]).map(c=>` vars.checks+=[\"${c}\"]`).join("\n") + out.push(`object Host "${h.fqdn}" {\n import "generic-host"\n address = "${addr}"\n vars.hostgroups = [${groups?groups.split(',').map(s=>`"${s.trim()}"`).join(', '):''}]\n${checks}\n}`) + } + return out.join("\n\n") +} +const zonefile = () => { + const rec=[`; zone ${zone}`, `$TTL ${ttl}`] + for(const [k,v] of Object.entries(inv)){ + const h=derive(k,v) + if(h?.dns?.records?.length){ for(const r of h.dns.records){ if(!r?.type) continue; const name=k.replace(`.${zone}`,'@'); const val=(r.value||'').replace?.('{zone}', zone) ?? r.value; rec.push(`${name} ${r.ttl||ttl} IN ${r.type} ${val}`) } } + else { const ip=h.plan==='VPN'?h.ip_internal:(h.ip_federative||h.ip_internal); if(ip){ const name=k.replace(`.${zone}`,'@'); rec.push(`${name} ${ttl} IN A ${ip}`) } } + } + return rec.join("\n") +} +const ansible = () => { + const groups={all:[]} + for(const [k,v] of Object.entries(inv)){ const h=derive(k,v); groups.all.push(k); for(const g of [`layer_${h.layer||0}`, `plan_${(h.plan||'').toLowerCase()}`, (h.layer_name||'').toLowerCase(), ...((h?.ansible?.groups)||[])] ){ if(!g) continue; (groups[g]??=[]).push(k) } } + const lines=[]; for(const [g,hs] of Object.entries(groups)){ lines.push(`[${g}]`); hs.sort().forEach(n=>lines.push(n)); lines.push('') } ; return lines.join("\n") +} +const netbox = () => { + const rows=[["name","site","device_role","device_type","primary_ip4","tenant","tags"].join(",")] + for(const [k,v] of Object.entries(inv)){ const h=derive(k,v); const nb=h.netbox||{}; const ip=h.plan==='VPN'?(h.ip_internal||''):(h.ip_federative||h.ip_internal||''); const r=[k, nb.site||"DC1", nb.device_role||"Server", nb.device_type||"VM", ip, nb.tenant||"Chezlepro", (h.tags||[]).join('|')]; rows.push(r.map(x=>`"${String(x).replaceAll('"','""')}"`).join(",")) } + return rows.join("\n") +} + +fs.mkdirSync('out',{recursive:true}) +fs.writeFileSync('out/prometheus-targets.yaml', prom()) +fs.writeFileSync('out/icinga-hosts.conf', icinga()) +fs.writeFileSync('out/zonefile.zone', zonefile()) +fs.writeFileSync('out/inventory.ini', ansible()) +fs.writeFileSync('out/netbox.csv', netbox()) +console.log('✅ exports: ./out')