Files
bf-infra/inventory/build.py

145 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Kanonischer Doku-Generator für das BF-Inventar.
Wahrheit = CSV. Markdown wird daraus generiert.
- `build.py --seed` : (einmalig) bf.csv aus IPs.xlsx erzeugen (mit Transform/Deltas)
- `build.py` : bf.md aus bf.csv + bf-core-ports.md aus bf-core-ports.csv rendern
Künftige Änderungen: bf.csv editieren -> `python3 build.py` -> bf.md aktuell.
"""
import sys, os, re, csv, zipfile
import xml.etree.ElementTree as ET
HERE = os.path.dirname(os.path.abspath(__file__))
XLSX = os.path.join(HERE, 'IPs.xlsx')
BF_CSV = os.path.join(HERE, 'bf.csv')
BF_MD = os.path.join(HERE, 'bf.md')
PORTS_CSV = os.path.join(HERE, 'bf-core-ports.csv')
PORTS_MD = os.path.join(HERE, 'bf-core-ports.md')
IPRE = re.compile(r'^\d{1,3}(\.\d{1,3}){3}$')
MACRE = re.compile(r'^[0-9A-Fa-f]{1,2}(:[0-9A-Fa-f]{1,2}){5}$')
COLS = ['section', 'ip', 'hostname', 'mac', 'pve_ct', 'server_ip', 'server_mac', 'note']
def _strip(t): return re.sub(r'\{[^}]*\}', '', t)
def read_xlsx_sheet(path, sheet):
z = zipfile.ZipFile(path)
shared = []
if 'xl/sharedStrings.xml' in z.namelist():
for si in ET.fromstring(z.read('xl/sharedStrings.xml')):
shared.append(''.join(t.text or '' for t in si.iter() if _strip(t.tag) == 't'))
wb = ET.fromstring(z.read('xl/workbook.xml'))
rels = {r.get('Id'): r.get('Target') for r in ET.fromstring(z.read('xl/_rels/workbook.xml.rels'))}
tgt = None
for s in wb.iter():
if _strip(s.tag) == 'sheet' and s.get('name') == sheet:
rid = [v for k, v in s.attrib.items() if k.endswith('}id') or k == 'id'][0]
t = rels[rid]; tgt = t if t.startswith('xl/') else 'xl/' + t
ws = ET.fromstring(z.read(tgt))
def colnum(ref):
m = re.match(r'([A-Z]+)(\d+)', ref)
if not m: return 0, 0
c = 0
for ch in m.group(1): c = c * 26 + (ord(ch) - 64)
return c, int(m.group(2))
cells = {}; maxc = 0; maxr = 0
for c in ws.iter():
if _strip(c.tag) != 'c': continue
ref = c.get('r') or ''; t = c.get('t'); col, row = colnum(ref)
if not col: continue
vs = [x for x in c if _strip(x.tag) == 'v']; iss = [x for x in c if _strip(x.tag) == 'is']
val = ''
if t == 's' and vs:
try: val = shared[int(vs[0].text)]
except Exception: val = vs[0].text or ''
elif iss: val = ''.join(x.text or '' for x in iss[0].iter() if _strip(x.tag) == 't')
elif vs: val = vs[0].text or ''
if val != '':
cells.setdefault(row, {})[col] = val; maxc = max(maxc, col); maxr = max(maxr, row)
grid = [[cells.get(r, {}).get(c, '') for c in range(1, maxc + 1)] for r in range(1, maxr + 1)]
return grid
def seed_bf_csv():
grid = read_xlsx_sheet(XLSX, 'bf')
out = []
section = ''
for r in grid[1:]: # skip header row
nz = [x for x in r if x.strip()]
if not nz:
continue
# cols: A(0) empty | B(1) ip/section | C(2) host | D(3) mac | E(4) ct | F(5) sip | G(6) smac | H(7) note | I(8) note2
def g(i): return r[i].strip() if i < len(r) else ''
ipcell = g(1)
# section header row = single non-empty cell, not an IP
if len(nz) == 1 and not IPRE.match(nz[0].strip()):
section = nz[0].strip(); continue
ip = ipcell if IPRE.match(ipcell) else ''
host = g(2); mac = g(3) if MACRE.match(g(3)) else ''
ct = g(4); sip = g(5); smac = g(6)
notes = [g(7), g(8)]
# defensive: misplaced data -> note
if g(3) and not MACRE.match(g(3)): notes.append('mac?=' + g(3))
if ct and not ct.isdigit(): notes.append(ct); ct = ''
if sip and not IPRE.match(sip): notes.append(sip); sip = ''
if smac and not MACRE.match(smac): notes.append(smac); smac = ''
note = ' / '.join(n for n in notes if n.strip())
# --- deltas (User 2026-06-05) ---
if host == 'VPNjoernfuenfstueck':
continue # gekickt
if host == 'VPNchristoph':
note = 'Fallback bis CF One überall läuft; IP umziehbar/unwichtig'
out.append([section, ip, host, mac, ct, sip, smac, note])
with open(BF_CSV, 'w', newline='') as f:
w = csv.writer(f); w.writerow(COLS); w.writerows(out)
print(f'seed: wrote {BF_CSV} ({len(out)} rows)')
def render_bf_md():
with open(BF_CSV) as f:
rows = list(csv.DictReader(f))
lines = ['# Inventar — Standort BF (kanonisch)', '',
'> Generiert aus `bf.csv` via `build.py`. **Quelle der Wahrheit = bf.csv.** Seed: `IPs.xlsx`.',
'> ⚠️ Manche `pve_ct`/Einträge können Doku-Leichen sein (z.B. re-dockerisierte Container).', '']
cur = None
cols = ['ip', 'hostname', 'mac', 'pve_ct', 'server_ip', 'server_mac', 'note']
for row in rows:
if row['section'] != cur:
cur = row['section']
lines += ['', f'## {cur or "(ohne Sektion)"}', '',
'| ' + ' | '.join(cols) + ' |', '|' + '|'.join(['---'] * len(cols)) + '|']
if not any(row[c].strip() for c in cols):
continue
lines.append('| ' + ' | '.join(row[c].replace('|', '\\|') for c in cols) + ' |')
with open(BF_MD, 'w') as f:
f.write('\n'.join(lines) + '\n')
print(f'wrote {BF_MD} ({len(rows)} rows)')
def render_ports_md():
if not os.path.exists(PORTS_CSV):
return
with open(PORTS_CSV) as f:
rows = list(csv.DictReader(f))
cols = list(rows[0].keys()) if rows else []
lines = ['# Core-Switch ALL-SG8316M (sw1, 10.40.41.239) — Port-Map', '',
'> Ist-Belegung. Ports 4/8/15/16 **behalten** (4+8 interim bis neue Switches produktiv).',
'> Übrige Ports (1–3, 5–7, 9–14): noch nicht erfasst — gemeinsam ergänzen.', '',
'| ' + ' | '.join(cols) + ' |', '|' + '|'.join(['---'] * len(cols)) + '|']
for r in rows:
lines.append('| ' + ' | '.join((r[c] or '').replace('|', '\\|') for c in cols) + ' |')
with open(PORTS_MD, 'w') as f:
f.write('\n'.join(lines) + '\n')
print(f'wrote {PORTS_MD} ({len(rows)} ports)')
if __name__ == '__main__':
if '--seed' in sys.argv or not os.path.exists(BF_CSV):
seed_bf_csv()
render_bf_md()
render_ports_md()