@@ -97,6 +97,8 @@ Pachete de sistem instalate în timpul evoluției: |
||
| 97 | 97 |
|
| 98 | 98 |
- `ripgrep` pe jumper |
| 99 | 99 |
- `rsync` pe jumper |
| 100 |
+- `sqlite` pe jumper |
|
| 101 |
+- `perl-DBD-SQLite` pe jumper |
|
| 100 | 102 |
- `sqlite3` pe mazeri/GitPrep |
| 101 | 103 |
|
| 102 | 104 |
## 2026-06-06 - OTP Login Keeps a Password-Manager-Friendly Form Shape |
@@ -140,3 +142,22 @@ Scop: |
||
| 140 | 142 |
- testul live pe jumper să fie legat de un commit identificabil |
| 141 | 143 |
- GitPrep să păstreze istoria canonică pentru arhivare și recuperare |
| 142 | 144 |
- badge-ul de build și meta tag-ul `xdev-build` devin verificarea rapidă pentru ce rulează |
| 145 |
+ |
|
| 146 |
+## 2026-06-09 - SQLite Runtime Source of Truth |
|
| 147 |
+ |
|
| 148 |
+Observație: editările de hosts făcute în aplicația live puteau rămâne în working tree-ul de pe jumper sau puteau fi suprascrise/confuzionate la push-ul de cod. Modelul vechi descria simultan `config/hosts.yaml` ca registry editabil în git și ca dată operațională runtime, ceea ce nu era o sursă de adevăr clară. |
|
| 149 |
+ |
|
| 150 |
+Decizie: |
|
| 151 |
+ |
|
| 152 |
+- `var/host-manager.sqlite` devine sursa de adevăr runtime pentru registry și Work Orders |
|
| 153 |
+- `config/hosts.yaml` și `config/work-orders.yaml` rămân seed/snapshot/export compatibility files |
|
| 154 |
+- la prima pornire, aplicația seed-uiește documentele lipsă din YAML în SQLite |
|
| 155 |
+- download-urile `/download/hosts.yaml`, `/download/local-hosts.tsv` și `/download/monitoring.json` sunt randate din SQLite |
|
| 156 |
+- `config/local-hosts.tsv` rămâne manifest generat explicit pentru sync-ul DNS local |
|
| 157 |
+- push-urile de cod către jumper nu trebuie să înlocuiască baza runtime din `var/` |
|
| 158 |
+ |
|
| 159 |
+Scop: |
|
| 160 |
+ |
|
| 161 |
+- editările făcute în UI să nu se piardă la deploy/push de cod |
|
| 162 |
+- să existe o singură autoritate runtime |
|
| 163 |
+- YAML/TSV să rămână utile pentru export, review și bootstrap fără să fie storage-ul live |
|
@@ -12,15 +12,15 @@ Perl-ul livrat de distribuție este acceptat ca bază de runtime. Modulele Perl |
||
| 12 | 12 |
|
| 13 | 13 |
Pachetele CPAN nu se instalează direct pe host cu `cpan`, `cpanm` sau mecanisme similare. Dacă aplicația are nevoie de un modul CPAN, modulul trebuie făcut disponibil prin repo-ul local după audit. Este acceptabilă o versiune mai veche/stabilă din repo-ul local; nu urmărim neapărat ultima versiune upstream. |
| 14 | 14 |
|
| 15 |
-MVP-ul curent nu are dependențe CPAN externe. |
|
| 15 |
+MVP-ul curent nu are dependențe CPAN instalate direct pe host. Pentru store-ul runtime folosește `DBI`/`DBD::SQLite` disponibile din distribuție sau repo-ul local auditat, plus SQLite. |
|
| 16 | 16 |
|
| 17 | 17 |
## Rol |
| 18 | 18 |
|
| 19 |
-`config/hosts.yaml` este registrul editabil și trebuie menținut în git. Aplicația este complet în spatele autentificării OTP pentru orice date de registru, exporturi sau modificări. |
|
| 19 |
+`var/host-manager.sqlite` este sursa de adevăr runtime pentru registry și Work Orders. La prima pornire, aplicația seed-uiește baza din `config/hosts.yaml` și `config/work-orders.yaml` dacă documentele lipsesc din SQLite. Aplicația este complet în spatele autentificării OTP pentru orice date de registru, exporturi sau modificări. |
|
| 20 | 20 |
|
| 21 |
-Git rămâne mecanismul de audit, istoric și rollback. Aplicația nu înlocuiește repo-ul și nu devine o bază de date separată. |
|
| 21 |
+Git rămâne mecanismul pentru cod, seed-uri, exporturi și istoric manual. Aplicația nu mai scrie registry-ul live direct în working tree, ca editările făcute în UI să nu se piardă la push/deploy de cod. |
|
| 22 | 22 |
|
| 23 |
-Schimbările cu impact operațional care elimină nume sau schimbă semantica serviciilor locale se fac prin Work Order (WO), nu prin ștergere directă din UI. WO-ul rămâne în git, exprimă intenția operațională și trebuie dus până la capăt înainte să modifice registrul. |
|
| 23 |
+Schimbările cu impact operațional care elimină nume sau schimbă semantica serviciilor locale se fac prin Work Order (WO), nu prin ștergere directă din UI. WO-ul rămâne în store-ul runtime, exprimă intenția operațională și trebuie dus până la capăt înainte să modifice registrul. |
|
| 24 | 24 |
|
| 25 | 25 |
Endpoint-uri publice: |
| 26 | 26 |
|
@@ -111,9 +111,9 @@ Secretul nu se comite în repo. Dacă avem nevoie de integrare cu un manager de |
||
| 111 | 111 |
|
| 112 | 112 |
## Flux |
| 113 | 113 |
|
| 114 |
-1. Hosturile se editează în aplicație sau direct în `config/hosts.yaml`. |
|
| 114 |
+1. Hosturile se editează în aplicație; store-ul runtime este `var/host-manager.sqlite`. |
|
| 115 | 115 |
2. Operatorii autentificați pot descărca `/download/hosts.yaml`, `/download/local-hosts.tsv` sau `/download/monitoring.json`. |
| 116 |
-3. Pentru DNS local, butonul `Write local-hosts.tsv` regenerează `config/local-hosts.tsv`. |
|
| 116 |
+3. Pentru DNS local, butonul `Write local-hosts.tsv` regenerează `config/local-hosts.tsv` din SQLite. |
|
| 117 | 117 |
4. Sincronizarea efectivă către jumper și as01 rămâne: |
| 118 | 118 |
|
| 119 | 119 |
```bash |
@@ -122,7 +122,7 @@ Secretul nu se comite în repo. Dacă avem nevoie de integrare cu un manager de |
||
| 122 | 122 |
|
| 123 | 123 |
## Work Orders |
| 124 | 124 |
|
| 125 |
-`config/work-orders.yaml` păstrează operațiuni care trebuie executate și confirmate înainte să atingă registrul. |
|
| 125 |
+Work Orders sunt păstrate în SQLite. `config/work-orders.yaml` rămâne seed/snapshot compatibil pentru instalări noi și export manual. |
|
| 126 | 126 |
|
| 127 | 127 |
Un WO nu înseamnă că numele nu mai este în uz. Înseamnă doar că vrem să ajungem acolo. Pentru retragerea unui nume, checklist-ul trebuie să acopere pașii reali: ștergerea vhostului, înlocuirea certificatelor publice cu certificate locale, reîncărcarea serviciilor, testarea accesului și verificarea că nu mai există consumatori. |
| 128 | 128 |
|
@@ -136,12 +136,12 @@ Confirmarea unui WO: |
||
| 136 | 136 |
|
| 137 | 137 |
- cere tastarea exactă a ID-ului WO în interfață |
| 138 | 138 |
- este blocată dacă există pași de checklist nemarcați `done` |
| 139 |
-- elimină numele declarate din `config/hosts.yaml` |
|
| 139 |
+- elimină numele declarate din registry-ul SQLite |
|
| 140 | 140 |
- marchează WO-ul ca `confirmed` |
| 141 | 141 |
- regenerează `config/local-hosts.tsv` |
| 142 | 142 |
- nu rulează automat sync-ul către resolvere |
| 143 | 143 |
|
| 144 |
-După confirmare, operatorul verifică schimbarea în git și rulează explicit: |
|
| 144 |
+După confirmare, operatorul verifică exportul și rulează explicit: |
|
| 145 | 145 |
|
| 146 | 146 |
```bash |
| 147 | 147 |
./scripts/sync_local_hosts.sh --apply --verify |
@@ -151,7 +151,7 @@ Primul WO curent este pentru retragerea numelor locale `pmx.*`/`pbs.*` create is |
||
| 151 | 151 |
|
| 152 | 152 |
## Convenții de nume |
| 153 | 153 |
|
| 154 |
-`madagascar.xdev.ro` este domeniul implicit al rețelei interne. În `config/hosts.yaml` se declară doar numele canonice/FQDN-urile necesare. |
|
| 154 |
+`madagascar.xdev.ro` este domeniul implicit al rețelei interne. În registry se declară doar numele canonice/FQDN-urile necesare. |
|
| 155 | 155 |
|
| 156 | 156 |
Pentru orice nume `*.madagascar.xdev.ro`, aplicația derivă automat aliasul scurt prin eliminarea sufixului `.madagascar.xdev.ro`. |
| 157 | 157 |
|
@@ -164,7 +164,7 @@ Aliasurile derivate nu se declară separat în `hosts.yaml`. Ele apar în API, m |
||
| 164 | 164 |
|
| 165 | 165 |
## Git și managementul cheilor |
| 166 | 166 |
|
| 167 |
-Varianta obligatorie pentru servicii automate este să citească `config/hosts.yaml` din git, nu să depindă de HTTP neautentificat. Serviciile care sincronizează DNS, monitorizare sau inventare primesc chei dedicate, cu acces minim. |
|
| 167 |
+Varianta obligatorie pentru servicii automate este să consume exporturi generate și verificate, nu să depindă de HTTP neautentificat. Serviciile care sincronizează DNS, monitorizare sau inventare primesc chei dedicate, cu acces minim. |
|
| 168 | 168 |
|
| 169 | 169 |
Reguli: |
| 170 | 170 |
|
@@ -178,20 +178,16 @@ Reguli: |
||
| 178 | 178 |
Modelul recomandat: |
| 179 | 179 |
|
| 180 | 180 |
```text |
| 181 |
-git repo |
|
| 182 |
- config/hosts.yaml sursă versionată |
|
| 183 |
- config/local-hosts.tsv manifest generat/versionat pentru DNS local |
|
| 184 |
- config/work-orders.yaml operațiuni confirmabile/versionate |
|
| 185 |
- |
|
| 186 | 181 |
jumper |
| 187 |
- host-manager editează working tree cu OTP |
|
| 182 |
+ var/host-manager.sqlite sursă runtime pentru registry și Work Orders |
|
| 183 |
+ host-manager editează SQLite cu OTP |
|
| 188 | 184 |
sync_local_hosts.sh aplică DNS după review/verificare |
| 189 | 185 |
|
| 190 | 186 |
servicii consumatoare |
| 191 |
- git pull read-only citesc hosts.yaml/local-hosts.tsv |
|
| 187 |
+ export verificat citesc hosts.yaml/local-hosts.tsv/monitoring.json |
|
| 192 | 188 |
``` |
| 193 | 189 |
|
| 194 |
-Pentru etapa MVP, aplicația nu face commit/push automat. După o modificare, schimbarea rămâne vizibilă în working tree și se comite explicit după review. Automatizarea commit/push poate fi adăugată ulterior, dar numai cu cheie separată și reguli clare de semnare/audit. |
|
| 190 |
+Pentru etapa MVP, aplicația nu face commit/push automat. După o modificare, schimbarea rămâne în SQLite și poate fi exportată explicit pentru review/arhivare. Automatizarea commit/push pentru exporturi poate fi adăugată ulterior, dar numai cu cheie separată și reguli clare de semnare/audit. |
|
| 195 | 191 |
|
| 196 | 192 |
## Autoritate locală de certificate |
| 197 | 193 |
|
@@ -228,5 +224,5 @@ Reguli: |
||
| 228 | 224 |
## Limitări MVP |
| 229 | 225 |
|
| 230 | 226 |
- Parserul YAML acceptă schema strictă generată de aplicație, nu YAML arbitrar. |
| 231 |
-- Conflict engine-ul verifică doar consistența locală din `hosts.yaml`. |
|
| 227 |
+- Conflict engine-ul verifică doar consistența locală din registry-ul SQLite. |
|
| 232 | 228 |
- DHCP, mDNS, `hosts-local.yaml` și `madagascar.json` sunt încă surse pentru audit manual sau pentru următorul collector. |
@@ -26,11 +26,12 @@ Implementarea versionată este: |
||
| 26 | 26 |
|
| 27 | 27 |
| Fișier | Rol | |
| 28 | 28 |
|--------|-----| |
| 29 |
-| `config/hosts.yaml` | registrul versionat pentru hosturi și FQDN-uri canonice | |
|
| 29 |
+| `var/host-manager.sqlite` | sursa de adevăr runtime pentru registry și Work Orders | |
|
| 30 |
+| `config/hosts.yaml` | seed/snapshot export pentru hosturi și FQDN-uri canonice | |
|
| 30 | 31 |
| `config/local-hosts.tsv` | manifest DNS generat, cu aliasuri scurte derivate | |
| 31 | 32 |
| `scripts/sync_local_hosts.sh` | generează și sincronizează `/etc/hosts`, `cloaking-rules.txt` și `/ip dns static` | |
| 32 | 33 |
|
| 33 |
-`madagascar.xdev.ro` este domeniul implicit. Pentru orice nume `*.madagascar.xdev.ro`, aliasul scurt este derivat automat. De exemplu, `autonas01.madagascar.xdev.ro` publică și `autonas01`, iar `pmx.baobab.madagascar.xdev.ro` publică și `pmx.baobab`. Aliasurile derivate nu se declară separat în `config/hosts.yaml`. |
|
| 34 |
+`madagascar.xdev.ro` este domeniul implicit. Pentru orice nume `*.madagascar.xdev.ro`, aliasul scurt este derivat automat. De exemplu, `autonas01.madagascar.xdev.ro` publică și `autonas01`, iar `pmx.baobab.madagascar.xdev.ro` publică și `pmx.baobab`. Aliasurile derivate nu se declară separat în registry. |
|
| 34 | 35 |
|
| 35 | 36 |
## Ierarhia surselor |
| 36 | 37 |
|
@@ -38,10 +39,11 @@ Când inventarele se contrazic, ordinea de încredere este: |
||
| 38 | 39 |
|
| 39 | 40 |
1. DHCP lease/reservation pe router (`admin@192.168.2.1`) — autoritatea pentru alocarea IP-urilor pe LAN. Configurațiile statice locale nu au voie să mute un IP peste DHCP; cel mult semnalează o rezervare lipsă sau o intrare veche. |
| 40 | 41 |
2. `cluster/cluster-context/madagascar.json` — autoritatea pentru roluri, topologie și IP-uri de serviciu. Pentru nodurile Proxmox, DNS-ul de serviciu poate folosi interfața `thunderbridge` (`192.168.10.x`) chiar dacă management/WAN este `192.168.2.x`. |
| 41 |
-3. `config/local-hosts.tsv` — manifestul DNS local publicat pe jumper și as01. Acesta trebuie să fie derivat sau validat din DHCP plus topologia clusterului, nu folosit ca sursă primară de alocare IP. |
|
| 42 |
-4. `hosts-local.yaml` — inventar SSH: aliasuri, utilizatori, entrypoint-uri și căi de acces. IP-urile de aici sunt utile pentru audit, dar pot fi stale dacă DHCP spune altceva. |
|
| 43 |
-5. mDNS (`*.local`) — sursă observată de descoperire și validare. Confirmă prezența unui host sau propune aliasuri, dar nu creează automat intrări `madagascar.xdev.ro`. |
|
| 44 |
-6. DNS public — folosit doar pentru acces extern. Local, numele interne trebuie shadow-uite exact sau lăsate nerezolvate; wildcard-ul public nu este autoritate pentru LAN. |
|
| 42 |
+3. `var/host-manager.sqlite` — registry-ul operațional aprobat, editat prin Madagascar Local Authority. |
|
| 43 |
+4. `config/local-hosts.tsv` — manifestul DNS local publicat pe jumper și as01. Acesta trebuie să fie derivat sau validat din DHCP plus topologia clusterului, nu folosit ca sursă primară de alocare IP. |
|
| 44 |
+5. `hosts-local.yaml` — inventar SSH: aliasuri, utilizatori, entrypoint-uri și căi de acces. IP-urile de aici sunt utile pentru audit, dar pot fi stale dacă DHCP spune altceva. |
|
| 45 |
+6. mDNS (`*.local`) — sursă observată de descoperire și validare. Confirmă prezența unui host sau propune aliasuri, dar nu creează automat intrări `madagascar.xdev.ro`. |
|
| 46 |
+7. DNS public — folosit doar pentru acces extern. Local, numele interne trebuie shadow-uite exact sau lăsate nerezolvate; wildcard-ul public nu este autoritate pentru LAN. |
|
| 45 | 47 |
|
| 46 | 48 |
Reguli de împăcare: |
| 47 | 49 |
|
@@ -61,7 +63,7 @@ Locația implicită: |
||
| 61 | 63 |
var/mdns-observations.yaml |
| 62 | 64 |
``` |
| 63 | 65 |
|
| 64 |
-Regulă importantă: `config/hosts.yaml` este un output generat din surse, nu store-ul primar al listenerului. Listenerul mDNS nu modifică `config/hosts.yaml` și nu publică nimic direct în `config/local-hosts.tsv`. |
|
| 66 |
+Regulă importantă: listenerul mDNS nu modifică registry-ul SQLite, `config/hosts.yaml` sau `config/local-hosts.tsv`. Seed-ul mDNS rămâne observație separată până la review. |
|
| 65 | 67 |
|
| 66 | 68 |
Rulare manuală pentru test: |
| 67 | 69 |
|
@@ -102,9 +104,8 @@ Aplică și verifică: |
||
| 102 | 104 |
## Pași pentru un hostname nou |
| 103 | 105 |
|
| 104 | 106 |
```bash |
| 105 |
-# 1. Adaugă hostul în config/local-hosts.tsv |
|
| 106 |
-# Format: hosts_ip<TAB>dns_ip<TAB>name alias... |
|
| 107 |
-192.168.2.XXX 192.168.2.XXX host.madagascar.xdev.ro host |
|
| 107 |
+# 1. Adaugă hostul în Madagascar Local Authority. |
|
| 108 |
+# Aplicația păstrează registry-ul în var/host-manager.sqlite. |
|
| 108 | 109 |
|
| 109 | 110 |
# 2. Aplică pe ambele resolvere |
| 110 | 111 |
./scripts/sync_local_hosts.sh --apply --verify |
@@ -16,9 +16,10 @@ git@192.168.2.102:repositories/bogdan/LocalAuthority.git |
||
| 16 | 16 |
|
| 17 | 17 |
The runtime instance lives on jumper and remains the local source for operational registry data: |
| 18 | 18 |
|
| 19 |
-- `config/hosts.yaml` - git-versioned host registry |
|
| 19 |
+- `var/host-manager.sqlite` - runtime source of truth for host registry and Work Orders |
|
| 20 |
+- `config/hosts.yaml` - seed/snapshot export for host registry compatibility |
|
| 20 | 21 |
- `config/local-hosts.tsv` - DNS manifest exported for local resolvers |
| 21 |
-- `config/work-orders.yaml` - confirmable operational changes |
|
| 22 |
+- `config/work-orders.yaml` - seed/snapshot export for confirmable operational changes |
|
| 22 | 23 |
- `scripts/host_manager.pl` - Perl-only web app |
| 23 | 24 |
- `scripts/sync_local_hosts.sh` - local DNS sync to jumper and as01 |
| 24 | 25 |
- `scripts/ca_manager.sh` - local OpenSSL CA helper for host certificates |
@@ -35,7 +36,7 @@ Secrets live outside git in `/etc/xdev/host-manager.env`. |
||
| 35 | 36 |
|
| 36 | 37 |
The product name is **Madagascar Local Authority**. The technical service, Unix user, repository path, and environment files still use `host-manager`. |
| 37 | 38 |
|
| 38 |
-The web UI is OTP-protected for all registry data, downloads, exports, and writes. Automation should consume this repository through git with dedicated read-only keys, not through unauthenticated HTTP. |
|
| 39 |
+The web UI is OTP-protected for all registry data, downloads, exports, and writes. Automation should consume generated exports with dedicated read-only access, not unauthenticated HTTP. |
|
| 39 | 40 |
|
| 40 | 41 |
For agent/operator context, see: |
| 41 | 42 |
|
@@ -76,7 +77,7 @@ git push origin main |
||
| 76 | 77 |
tool, but the normal development loop is commit plus push: `jumper-runtime` for |
| 77 | 78 |
live testing, `origin`/GitPrep for archive and sharing. |
| 78 | 79 |
|
| 79 |
-`config/` is not deployed by default because `hosts.yaml`, `local-hosts.tsv`, and `work-orders.yaml` are operational data that may be changed on jumper by the application. Deploy config only when intentionally replacing runtime registry data: |
|
| 80 |
+`config/` is not deployed by default. The live source of truth is `var/host-manager.sqlite`; `hosts.yaml`, `local-hosts.tsv`, and `work-orders.yaml` are seed/snapshot/export files that should not replace runtime data during normal code pushes. Deploy config only when intentionally replacing seed/export files: |
|
| 80 | 81 |
|
| 81 | 82 |
```bash |
| 82 | 83 |
scripts/deploy_to_jumper.sh --include-config |
@@ -84,7 +85,7 @@ scripts/deploy_to_jumper.sh --include-config |
||
| 84 | 85 |
|
| 85 | 86 |
The default internal domain is `madagascar.xdev.ro`. Short aliases are derived automatically from FQDNs, so `autonas01.madagascar.xdev.ro` also publishes `autonas01` without declaring it separately. |
| 86 | 87 |
|
| 87 |
-Name removals with operational impact go through a Work Order. A WO records intent first; the operational checklist must be completed before confirmation can update `hosts.yaml`, mark the WO as confirmed, and regenerate `local-hosts.tsv`. Resolver sync remains an explicit operator step. |
|
| 88 |
+Name removals with operational impact go through a Work Order. A WO records intent first; the operational checklist must be completed before confirmation can update the SQLite registry, mark the WO as confirmed, and regenerate `local-hosts.tsv`. Resolver sync remains an explicit operator step. |
|
| 88 | 89 |
|
| 89 | 90 |
The local host CA stores private material outside git under `var/ca`. Initialize it on jumper with: |
| 90 | 91 |
|
@@ -18,8 +18,9 @@ Operational rules: |
||
| 18 | 18 |
- Push committed code to `jumper-runtime` for live testing on jumper. |
| 19 | 19 |
- Push committed code to `origin`/GitPrep for archival/canonical history. |
| 20 | 20 |
- `scripts/deploy_to_jumper.sh` is available for explicit rsync deploys, but the normal development loop is commit plus push. |
| 21 |
-- Do not deploy `config/` unless the user explicitly asks to replace runtime registry data. |
|
| 22 |
-- Treat `config/hosts.yaml`, `config/local-hosts.tsv`, and `config/work-orders.yaml` as operational data that may be changed by the live app. |
|
| 21 |
+- Runtime registry and Work Orders live in `var/host-manager.sqlite` on jumper. |
|
| 22 |
+- Do not deploy `config/` unless the user explicitly asks to replace seed/snapshot/export files. |
|
| 23 |
+- Treat `config/hosts.yaml`, `config/local-hosts.tsv`, and `config/work-orders.yaml` as compatibility exports/seeds, not the live source of truth. |
|
| 23 | 24 |
- Do not install npm, pip, or CPAN packages directly on hosts. Distribution packages are acceptable when needed. |
| 24 | 25 |
- Perl from the distribution and core/distribution modules are allowed. |
| 25 | 26 |
- CPAN modules are allowed only after requesting an audit and RPM packaging for the local audited repository. |
@@ -4,7 +4,8 @@ policy: |
||
| 4 | 4 |
ip_authority: "dhcp" |
| 5 | 5 |
topology_authority: "madagascar.json" |
| 6 | 6 |
dns_manifest: "config/local-hosts.tsv" |
| 7 |
- storage_authority: "git" |
|
| 7 |
+ storage_authority: "sqlite" |
|
| 8 |
+ runtime_database: "var/host-manager.sqlite" |
|
| 8 | 9 |
consumer_access: "read-only deploy keys" |
| 9 | 10 |
hosts: |
| 10 | 11 |
- id: "baobab" |
@@ -15,6 +15,8 @@ Instanța curentă este instalată pe jumper în `/usr/local/xdev-host-manager` |
||
| 15 | 15 |
Se folosesc doar pachete din distribuție: |
| 16 | 16 |
|
| 17 | 17 |
- `perl` |
| 18 |
+- `perl-DBI` / `perl-DBD-SQLite` dacă nu sunt deja disponibile |
|
| 19 |
+- `sqlite` |
|
| 18 | 20 |
- `nginx` |
| 19 | 21 |
|
| 20 | 22 |
Nu se instalează npm, pip sau CPAN direct pe host. |
@@ -31,6 +33,7 @@ sudo dnf install nginx |
||
| 31 | 33 |
/usr/local/xdev-host-manager |
| 32 | 34 |
config/hosts.yaml |
| 33 | 35 |
config/local-hosts.tsv |
| 36 |
+ var/host-manager.sqlite |
|
| 34 | 37 |
var/mdns-observations.yaml |
| 35 | 38 |
scripts/host_manager.pl |
| 36 | 39 |
scripts/mdns_host_seed.pl |
@@ -93,6 +96,10 @@ hosts.madagascar.xdev.ro -> 192.168.2.100 |
||
| 93 | 96 |
|
| 94 | 97 |
Nu se adaugă wildcard local. Doar acest nume exact trebuie publicat. |
| 95 | 98 |
|
| 99 |
+## Runtime store |
|
| 100 |
+ |
|
| 101 |
+`var/host-manager.sqlite` este sursa de adevăr pentru registry și Work Orders. La prima pornire, aplicația seed-uiește documentele lipsă din `config/hosts.yaml` și `config/work-orders.yaml`; ulterior push-urile de cod nu trebuie să înlocuiască baza runtime. |
|
| 102 |
+ |
|
| 96 | 103 |
## mDNS discovery |
| 97 | 104 |
|
| 98 |
-`host-manager-mdns` este un listener separat care observă mDNS și seeduiește `var/mdns-observations.yaml`. `config/hosts.yaml` rămâne output generat din surse și nu este modificat direct de listener. |
|
| 105 |
+`host-manager-mdns` este un listener separat care observă mDNS și seeduiește `var/mdns-observations.yaml`. Listenerul nu modifică registry-ul SQLite, `config/hosts.yaml` sau `config/local-hosts.tsv`. |
|
@@ -3,6 +3,7 @@ |
||
| 3 | 3 |
|
| 4 | 4 |
HOST_MANAGER_BIND=127.0.0.1 |
| 5 | 5 |
HOST_MANAGER_PORT=8088 |
| 6 |
+HOST_MANAGER_DB=/usr/local/xdev-host-manager/var/host-manager.sqlite |
|
| 6 | 7 |
HOST_MANAGER_DATA=/usr/local/xdev-host-manager/config/hosts.yaml |
| 7 | 8 |
HOST_MANAGER_LOCAL_HOSTS_TSV=/usr/local/xdev-host-manager/config/local-hosts.tsv |
| 8 | 9 |
|
@@ -7,6 +7,7 @@ use strict; |
||
| 7 | 7 |
use warnings; |
| 8 | 8 |
|
| 9 | 9 |
use Cwd qw(abs_path); |
| 10 |
+use DBI; |
|
| 10 | 11 |
use Digest::SHA qw(hmac_sha1 hmac_sha256_hex sha256_hex); |
| 11 | 12 |
use File::Basename qw(dirname); |
| 12 | 13 |
use File::Path qw(make_path); |
@@ -20,6 +21,7 @@ my $project_dir = dirname($script_dir); |
||
| 20 | 21 |
my %opt = ( |
| 21 | 22 |
bind => $ENV{HOST_MANAGER_BIND} || '127.0.0.1',
|
| 22 | 23 |
port => $ENV{HOST_MANAGER_PORT} || 8088,
|
| 24 |
+ db => $ENV{HOST_MANAGER_DB} || "$project_dir/var/host-manager.sqlite",
|
|
| 23 | 25 |
data => $ENV{HOST_MANAGER_DATA} || "$project_dir/config/hosts.yaml",
|
| 24 | 26 |
local_hosts_tsv => $ENV{HOST_MANAGER_LOCAL_HOSTS_TSV} || "$project_dir/config/local-hosts.tsv",
|
| 25 | 27 |
work_orders => $ENV{HOST_MANAGER_WORK_ORDERS} || "$project_dir/config/work-orders.yaml",
|
@@ -31,6 +33,8 @@ while (@ARGV) {
|
||
| 31 | 33 |
$opt{bind} = shift @ARGV;
|
| 32 | 34 |
} elsif ($arg eq '--port') {
|
| 33 | 35 |
$opt{port} = shift @ARGV;
|
| 36 |
+ } elsif ($arg eq '--db') {
|
|
| 37 |
+ $opt{db} = shift @ARGV;
|
|
| 34 | 38 |
} elsif ($arg eq '--data') {
|
| 35 | 39 |
$opt{data} = shift @ARGV;
|
| 36 | 40 |
} elsif ($arg eq '--local-hosts-tsv') {
|
@@ -57,7 +61,8 @@ my $server = IO::Socket::INET->new( |
||
| 57 | 61 |
) or die "Cannot listen on $opt{bind}:$opt{port}: $!\n";
|
| 58 | 62 |
|
| 59 | 63 |
print "host-manager listening on http://$opt{bind}:$opt{port}\n";
|
| 60 |
-print "data file: $opt{data}\n";
|
|
| 64 |
+print "database: $opt{db}\n";
|
|
| 65 |
+print "seed/export hosts file: $opt{data}\n";
|
|
| 61 | 66 |
print "OTP login: " . ($ENV{HOST_MANAGER_TOTP_SECRET} ? "enabled\n" : "disabled; set HOST_MANAGER_TOTP_SECRET\n");
|
| 62 | 67 |
|
| 63 | 68 |
while (my $client = $server->accept) {
|
@@ -78,11 +83,14 @@ Usage: perl scripts/host_manager.pl [--bind 127.0.0.1] [--port 8088] |
||
| 78 | 83 |
Environment: |
| 79 | 84 |
HOST_MANAGER_TOTP_SECRET Base32 TOTP secret required for write access. |
| 80 | 85 |
HOST_MANAGER_SESSION_SECRET Optional session signing secret. |
| 86 |
+ HOST_MANAGER_DB Defaults to var/host-manager.sqlite. |
|
| 81 | 87 |
HOST_MANAGER_DATA Defaults to config/hosts.yaml. |
| 82 | 88 |
HOST_MANAGER_LOCAL_HOSTS_TSV Defaults to config/local-hosts.tsv. |
| 83 | 89 |
HOST_MANAGER_WORK_ORDERS Defaults to config/work-orders.yaml. |
| 84 | 90 |
|
| 85 |
-The nginx vhost keeps registry, CA, work order and download endpoints behind OTP. |
|
| 91 |
+SQLite is the runtime source of truth. YAML files seed a new database and remain |
|
| 92 |
+download/export compatibility artifacts. The nginx vhost keeps registry, CA, |
|
| 93 |
+work order and download endpoints behind OTP. |
|
| 86 | 94 |
EOF |
| 87 | 95 |
} |
| 88 | 96 |
|
@@ -144,7 +152,8 @@ sub handle_client {
|
||
| 144 | 152 |
return send_json($client, 200, work_orders_payload(load_work_orders())); |
| 145 | 153 |
} |
| 146 | 154 |
if ($method eq 'GET' && $path eq '/download/hosts.yaml') {
|
| 147 |
- return send_file($client, $opt{data}, 'application/x-yaml; charset=utf-8', 'hosts.yaml');
|
|
| 155 |
+ my $registry = load_registry(); |
|
| 156 |
+ return send_download($client, 200, render_hosts_yaml($registry), 'application/x-yaml; charset=utf-8', 'hosts.yaml'); |
|
| 148 | 157 |
} |
| 149 | 158 |
if ($method eq 'GET' && $path eq '/download/local-hosts.tsv') {
|
| 150 | 159 |
my $registry = load_registry(); |
@@ -203,25 +212,25 @@ sub app_page_path {
|
||
| 203 | 212 |
} |
| 204 | 213 |
|
| 205 | 214 |
sub load_registry {
|
| 206 |
- return parse_hosts_yaml(read_file($opt{data}));
|
|
| 215 |
+ my $registry = parse_hosts_yaml(load_operational_doc('hosts_yaml', $opt{data}, default_hosts_yaml()));
|
|
| 216 |
+ normalize_registry_policy($registry); |
|
| 217 |
+ return $registry; |
|
| 207 | 218 |
} |
| 208 | 219 |
|
| 209 | 220 |
sub save_registry {
|
| 210 | 221 |
my ($registry) = @_; |
| 211 | 222 |
$registry->{updated_at} = iso_now();
|
| 212 |
- backup_file($opt{data});
|
|
| 213 |
- write_file($opt{data}, render_hosts_yaml($registry));
|
|
| 223 |
+ normalize_registry_policy($registry); |
|
| 224 |
+ save_operational_doc('hosts_yaml', render_hosts_yaml($registry));
|
|
| 214 | 225 |
} |
| 215 | 226 |
|
| 216 | 227 |
sub load_work_orders {
|
| 217 |
- return { version => 1, work_orders => [] } unless -f $opt{work_orders};
|
|
| 218 |
- return parse_work_orders_yaml(read_file($opt{work_orders}));
|
|
| 228 |
+ return parse_work_orders_yaml(load_operational_doc('work_orders_yaml', $opt{work_orders}, default_work_orders_yaml()));
|
|
| 219 | 229 |
} |
| 220 | 230 |
|
| 221 | 231 |
sub save_work_orders {
|
| 222 | 232 |
my ($orders) = @_; |
| 223 |
- backup_file($opt{work_orders});
|
|
| 224 |
- write_file($opt{work_orders}, render_work_orders_yaml($orders));
|
|
| 233 |
+ save_operational_doc('work_orders_yaml', render_work_orders_yaml($orders));
|
|
| 225 | 234 |
} |
| 226 | 235 |
|
| 227 | 236 |
sub work_orders_payload {
|
@@ -501,7 +510,7 @@ sub problem {
|
||
| 501 | 510 |
sub render_local_hosts_tsv {
|
| 502 | 511 |
my ($registry) = @_; |
| 503 | 512 |
my $out = "# Local DNS manifest for the madagascar network.\n"; |
| 504 |
- $out .= "# Generated by scripts/host_manager.pl from config/hosts.yaml.\n"; |
|
| 513 |
+ $out .= "# Generated by scripts/host_manager.pl from the runtime SQLite registry.\n"; |
|
| 505 | 514 |
$out .= "#\n"; |
| 506 | 515 |
$out .= "# Format:\n"; |
| 507 | 516 |
$out .= "# hosts_ip<TAB>dns_ip<TAB>name [aliases...]\n"; |
@@ -541,7 +550,7 @@ sub render_monitoring {
|
||
| 541 | 550 |
return {
|
| 542 | 551 |
version => $registry->{version},
|
| 543 | 552 |
generated_at => iso_now(), |
| 544 |
- source => 'config/hosts.yaml', |
|
| 553 |
+ source => $opt{db},
|
|
| 545 | 554 |
hosts => \@hosts, |
| 546 | 555 |
}; |
| 547 | 556 |
} |
@@ -1118,6 +1127,88 @@ sub backup_file {
|
||
| 1118 | 1127 |
write_file("$backup_dir/$name.$stamp.bak", read_file($path));
|
| 1119 | 1128 |
} |
| 1120 | 1129 |
|
| 1130 |
+my $db_handle; |
|
| 1131 |
+ |
|
| 1132 |
+sub dbh {
|
|
| 1133 |
+ return $db_handle if $db_handle; |
|
| 1134 |
+ ensure_parent_dir($opt{db});
|
|
| 1135 |
+ $db_handle = DBI->connect( |
|
| 1136 |
+ "dbi:SQLite:dbname=$opt{db}",
|
|
| 1137 |
+ '', |
|
| 1138 |
+ '', |
|
| 1139 |
+ {
|
|
| 1140 |
+ RaiseError => 1, |
|
| 1141 |
+ PrintError => 0, |
|
| 1142 |
+ AutoCommit => 1, |
|
| 1143 |
+ sqlite_unicode => 1, |
|
| 1144 |
+ }, |
|
| 1145 |
+ ) or die "Cannot open SQLite database $opt{db}\n";
|
|
| 1146 |
+ $db_handle->do('PRAGMA journal_mode = WAL');
|
|
| 1147 |
+ $db_handle->do('PRAGMA foreign_keys = ON');
|
|
| 1148 |
+ $db_handle->do(<<'SQL'); |
|
| 1149 |
+CREATE TABLE IF NOT EXISTS documents ( |
|
| 1150 |
+ name TEXT PRIMARY KEY, |
|
| 1151 |
+ content TEXT NOT NULL, |
|
| 1152 |
+ updated_at TEXT NOT NULL |
|
| 1153 |
+) |
|
| 1154 |
+SQL |
|
| 1155 |
+ return $db_handle; |
|
| 1156 |
+} |
|
| 1157 |
+ |
|
| 1158 |
+sub load_operational_doc {
|
|
| 1159 |
+ my ($name, $seed_path, $default_text) = @_; |
|
| 1160 |
+ my $dbh = dbh(); |
|
| 1161 |
+ my $row = $dbh->selectrow_hashref('SELECT content FROM documents WHERE name = ?', undef, $name);
|
|
| 1162 |
+ return $row->{content} if $row;
|
|
| 1163 |
+ |
|
| 1164 |
+ my $content = -f $seed_path ? read_file($seed_path) : $default_text; |
|
| 1165 |
+ save_operational_doc($name, $content); |
|
| 1166 |
+ return $content; |
|
| 1167 |
+} |
|
| 1168 |
+ |
|
| 1169 |
+sub save_operational_doc {
|
|
| 1170 |
+ my ($name, $content) = @_; |
|
| 1171 |
+ my $dbh = dbh(); |
|
| 1172 |
+ $dbh->do( |
|
| 1173 |
+ 'INSERT INTO documents (name, content, updated_at) VALUES (?, ?, ?) ' |
|
| 1174 |
+ . 'ON CONFLICT(name) DO UPDATE SET content = excluded.content, updated_at = excluded.updated_at', |
|
| 1175 |
+ undef, |
|
| 1176 |
+ $name, |
|
| 1177 |
+ $content, |
|
| 1178 |
+ iso_now(), |
|
| 1179 |
+ ); |
|
| 1180 |
+} |
|
| 1181 |
+ |
|
| 1182 |
+sub normalize_registry_policy {
|
|
| 1183 |
+ my ($registry) = @_; |
|
| 1184 |
+ $registry->{policy} ||= {};
|
|
| 1185 |
+ $registry->{policy}{storage_authority} = 'sqlite';
|
|
| 1186 |
+ $registry->{policy}{runtime_database} = $opt{db};
|
|
| 1187 |
+} |
|
| 1188 |
+ |
|
| 1189 |
+sub default_hosts_yaml {
|
|
| 1190 |
+ return <<'YAML'; |
|
| 1191 |
+version: 1 |
|
| 1192 |
+updated_at: "" |
|
| 1193 |
+policy: |
|
| 1194 |
+ storage_authority: "sqlite" |
|
| 1195 |
+hosts: |
|
| 1196 |
+YAML |
|
| 1197 |
+} |
|
| 1198 |
+ |
|
| 1199 |
+sub default_work_orders_yaml {
|
|
| 1200 |
+ return <<'YAML'; |
|
| 1201 |
+version: 1 |
|
| 1202 |
+work_orders: |
|
| 1203 |
+YAML |
|
| 1204 |
+} |
|
| 1205 |
+ |
|
| 1206 |
+sub ensure_parent_dir {
|
|
| 1207 |
+ my ($path) = @_; |
|
| 1208 |
+ my $dir = dirname($path); |
|
| 1209 |
+ make_path($dir) unless -d $dir; |
|
| 1210 |
+} |
|
| 1211 |
+ |
|
| 1121 | 1212 |
sub url_decode {
|
| 1122 | 1213 |
my ($value) = @_; |
| 1123 | 1214 |
$value = '' unless defined $value; |
@@ -2266,7 +2357,7 @@ sub app_html {
|
||
| 2266 | 2357 |
}); |
| 2267 | 2358 |
|
| 2268 | 2359 |
$('write-tsv').addEventListener('click', async () => {
|
| 2269 |
- if (!confirm('Write config/local-hosts.tsv from hosts.yaml?')) return;
|
|
| 2360 |
+ if (!confirm('Write config/local-hosts.tsv from the runtime registry?')) return;
|
|
| 2270 | 2361 |
try {
|
| 2271 | 2362 |
await api('/api/render/local-hosts-tsv', { method: 'POST' });
|
| 2272 | 2363 |
msg('local-hosts.tsv written');
|