Showing 9 changed files with 104 additions and 17 deletions
+15 -1
.doc/development-logs/dns.md
@@ -13,8 +13,22 @@ Resolverele interne sunt:
13 13
 - jumper: `192.168.2.100`
14 14
 - as01: `192.168.2.2`
15 15
 
16
-Sync-ul ramane explicit:
16
+## 2026-06-10 - Prompt Resolver Publishing
17
+
18
+Incident: un vhost nou, `git.madagascar.xdev.ro`, exista in SQLite si in
19
+manifestul generat, dar nu fusese aplicat pe resolvere. Pe macOS, o incercare
20
+de rezolvare esuata poate ramane cache-uita agresiv, deci publicarea DNS trebuie
21
+sa urmeze rapid dupa editarea registry-ului.
22
+
23
+Decizie:
24
+
25
+- modificarile DNS facute prin aplicatie regenereaza `config/local-hosts.tsv`
26
+- aplicatia atinge `var/dns-publish.trigger`
27
+- `host-manager-dns-publish.path` porneste `host-manager-dns-publish.service`
28
+- serviciul oneshot ruleaza sync-ul privilegiat existent:
17 29
 
18 30
 ```bash
19 31
 ./scripts/sync_local_hosts.sh --apply --verify
20 32
 ```
33
+
34
+Sync-ul manual ramane disponibil pentru interventii operationale si audit.
+6 -5
.doc/host-manager.md
@@ -114,8 +114,8 @@ Secretul nu se comite în repo. Dacă avem nevoie de integrare cu un manager de
114 114
 
115 115
 1. Hosturile se editează în aplicație; store-ul runtime este `var/host-manager.sqlite`.
116 116
 2. Operatorii autentificați pot descărca `/download/hosts.yaml`, `/download/local-hosts.tsv` sau `/download/monitoring.json`.
117
-3. Pentru DNS local, butonul `Write local-hosts.tsv` regenerează `config/local-hosts.tsv` din SQLite.
118
-4. Sincronizarea efectivă către jumper și as01 rămâne:
117
+3. Pentru DNS local, schimbările făcute în aplicație regenerează `config/local-hosts.tsv` din SQLite.
118
+4. Aplicația atinge `var/dns-publish.trigger`, iar pe jumper `host-manager-dns-publish.path` pornește prompt:
119 119
 
120 120
 ```bash
121 121
 ./scripts/sync_local_hosts.sh --apply --verify
@@ -140,12 +140,13 @@ Confirmarea unui WO:
140 140
 - elimină numele declarate din registry-ul SQLite
141 141
 - marchează WO-ul ca `confirmed`
142 142
 - regenerează `config/local-hosts.tsv`
143
-- nu rulează automat sync-ul către resolvere
143
+- declanșează publicarea către resolverele locale prin `host-manager-dns-publish.path`
144 144
 
145
-După confirmare, operatorul verifică exportul și rulează explicit:
145
+După confirmare, operatorul poate verifica manual resolverele cu:
146 146
 
147 147
 ```bash
148
-./scripts/sync_local_hosts.sh --apply --verify
148
+dig @192.168.2.100 nume.madagascar.xdev.ro +short
149
+dig @192.168.2.2   nume.madagascar.xdev.ro +short
149 150
 ```
150 151
 
151 152
 Primul WO curent este pentru retragerea numelor locale `pmx.*`/`pbs.*` create istoric pentru vhosturi nginx cu certificate Let's Encrypt. Odată cu CA-ul local, aceste nume nu mai trebuie să existe ca vhosturi separate pentru interfețele Proxmox/PBS, dar rămân publicate până când checklist-ul operațional este complet și WO-ul este confirmat.
+2 -1
.doc/local-hosts.md
@@ -126,7 +126,8 @@ Aplică și verifică:
126 126
 # 1. Adaugă hostul în Madagascar Local Authority.
127 127
 # Aplicația păstrează registry-ul în var/host-manager.sqlite.
128 128
 
129
-# 2. Aplică pe ambele resolvere
129
+# 2. Publicarea către resolvere este declanșată automat pe jumper.
130
+# Pentru intervenție manuală sau re-verificare:
130 131
 ./scripts/sync_local_hosts.sh --apply --verify
131 132
 
132 133
 # 3. Verificare manuală, dacă e nevoie
+17 -0
deploy/jumper/README.md
@@ -42,6 +42,8 @@ sudo dnf install nginx
42 42
 /etc/xdev/host-manager.env
43 43
 /etc/systemd/system/host-manager.service
44 44
 /etc/systemd/system/host-manager-mdns.service
45
+/etc/systemd/system/host-manager-dns-publish.path
46
+/etc/systemd/system/host-manager-dns-publish.service
45 47
 /etc/nginx/conf.d/madagascar.xdev.ro.conf
46 48
 ```
47 49
 
@@ -55,6 +57,8 @@ sudo install -d -o host-manager -g host-manager /usr/local/xdev-host-manager
55 57
 sudo install -d -m 0750 /etc/xdev
56 58
 sudo install -m 0644 deploy/jumper/host-manager.service /etc/systemd/system/host-manager.service
57 59
 sudo install -m 0644 deploy/jumper/host-manager-mdns.service /etc/systemd/system/host-manager-mdns.service
60
+sudo install -m 0644 deploy/jumper/host-manager-dns-publish.path /etc/systemd/system/host-manager-dns-publish.path
61
+sudo install -m 0644 deploy/jumper/host-manager-dns-publish.service /etc/systemd/system/host-manager-dns-publish.service
58 62
 sudo install -m 0644 deploy/jumper/nginx-host-manager.conf /etc/nginx/conf.d/madagascar.xdev.ro.conf
59 63
 ```
60 64
 
@@ -73,6 +77,7 @@ Validare:
73 77
 sudo systemctl daemon-reload
74 78
 sudo systemctl enable --now host-manager
75 79
 sudo systemctl enable --now host-manager-mdns
80
+sudo systemctl enable --now host-manager-dns-publish.path
76 81
 sudo nginx -t
77 82
 sudo systemctl reload nginx
78 83
 curl -fsS http://127.0.0.1:8088/healthz
@@ -97,6 +102,18 @@ madagascar.xdev.ro -> jumper.madagascar.xdev.ro
97 102
 
98 103
 Nu se adaugă wildcard local. Doar acest nume exact trebuie publicat.
99 104
 
105
+Schimbările DNS făcute prin aplicație regenerează `config/local-hosts.tsv` și
106
+ating `var/dns-publish.trigger`. Pe jumper,
107
+`host-manager-dns-publish.path` pornește imediat
108
+`host-manager-dns-publish.service`, care rulează:
109
+
110
+```bash
111
+/usr/local/xdev-host-manager/scripts/sync_local_hosts.sh --apply --verify
112
+```
113
+
114
+Serviciul oneshot rulează ca root prin systemd, deoarece publicarea atinge
115
+`/etc/hosts`, `dnscrypt-proxy`, `systemd-resolved` și DNS-ul static de pe as01.
116
+
100 117
 ## Runtime store
101 118
 
102 119
 `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.
+10 -0
deploy/jumper/host-manager-dns-publish.path
@@ -0,0 +1,10 @@
1
+[Unit]
2
+Description=Watch Madagascar Local Authority DNS publish trigger
3
+After=host-manager.service
4
+
5
+[Path]
6
+PathChanged=/usr/local/xdev-host-manager/var/dns-publish.trigger
7
+Unit=host-manager-dns-publish.service
8
+
9
+[Install]
10
+WantedBy=multi-user.target
+9 -0
deploy/jumper/host-manager-dns-publish.service
@@ -0,0 +1,9 @@
1
+[Unit]
2
+Description=Publish Madagascar local DNS records to resolvers
3
+After=network-online.target host-manager.service
4
+Wants=network-online.target
5
+
6
+[Service]
7
+Type=oneshot
8
+WorkingDirectory=/usr/local/xdev-host-manager
9
+ExecStart=/usr/local/xdev-host-manager/scripts/sync_local_hosts.sh --apply --verify
+1 -0
deploy/jumper/host-manager.env.example
@@ -6,6 +6,7 @@ HOST_MANAGER_PORT=8088
6 6
 HOST_MANAGER_DB=/usr/local/xdev-host-manager/var/host-manager.sqlite
7 7
 HOST_MANAGER_DATA=/usr/local/xdev-host-manager/config/hosts.yaml
8 8
 HOST_MANAGER_LOCAL_HOSTS_TSV=/usr/local/xdev-host-manager/config/local-hosts.tsv
9
+HOST_MANAGER_DNS_PUBLISH_TRIGGER=/usr/local/xdev-host-manager/var/dns-publish.trigger
9 10
 
10 11
 # Base32 TOTP secret. Required for write access.
11 12
 HOST_MANAGER_TOTP_SECRET=CHANGE_ME_BASE32
+3 -0
scripts/deploy_to_jumper.sh
@@ -118,6 +118,9 @@ printf 'revision=%s\nbranch=%s\ndirty=%s\ndeployed_at=%s\n' \
118 118
 ssh "$TARGET_HOST" "cd '$TARGET_DIR' && perl -c scripts/host_manager.pl >/dev/null && perl -c scripts/mdns_host_seed.pl >/dev/null"
119 119
 
120 120
 if [[ "$RESTART" -eq 1 ]]; then
121
+    ssh "$TARGET_HOST" "sudo -n install -m 0644 '$TARGET_DIR/deploy/jumper/host-manager-dns-publish.path' /etc/systemd/system/host-manager-dns-publish.path"
122
+    ssh "$TARGET_HOST" "sudo -n install -m 0644 '$TARGET_DIR/deploy/jumper/host-manager-dns-publish.service' /etc/systemd/system/host-manager-dns-publish.service"
123
+    ssh "$TARGET_HOST" "sudo -n systemctl daemon-reload && sudo -n systemctl enable --now host-manager-dns-publish.path >/dev/null"
121 124
     ssh "$TARGET_HOST" "sudo -n systemctl restart host-manager"
122 125
 fi
123 126
 
+41 -10
scripts/host_manager.pl
@@ -24,6 +24,7 @@ my %opt = (
24 24
     db => $ENV{HOST_MANAGER_DB} || "$project_dir/var/host-manager.sqlite",
25 25
     data => $ENV{HOST_MANAGER_DATA} || "$project_dir/config/hosts.yaml",
26 26
     local_hosts_tsv => $ENV{HOST_MANAGER_LOCAL_HOSTS_TSV} || "$project_dir/config/local-hosts.tsv",
27
+    dns_publish_trigger => $ENV{HOST_MANAGER_DNS_PUBLISH_TRIGGER} || "$project_dir/var/dns-publish.trigger",
27 28
     work_orders => $ENV{HOST_MANAGER_WORK_ORDERS} || "$project_dir/config/work-orders.yaml",
28 29
 );
29 30
 my $print_local_hosts_tsv = 0;
@@ -95,6 +96,8 @@ Environment:
95 96
   HOST_MANAGER_DB               Defaults to var/host-manager.sqlite.
96 97
   HOST_MANAGER_DATA             Defaults to config/hosts.yaml.
97 98
   HOST_MANAGER_LOCAL_HOSTS_TSV  Defaults to config/local-hosts.tsv.
99
+  HOST_MANAGER_DNS_PUBLISH_TRIGGER
100
+                                  Defaults to var/dns-publish.trigger.
98 101
   HOST_MANAGER_WORK_ORDERS      Defaults to config/work-orders.yaml.
99 102
   --print-local-hosts-tsv       Print the runtime DNS manifest and exit.
100 103
 
@@ -257,10 +260,8 @@ sub handle_client {
257 260
         }
258 261
         if ($path eq '/api/render/local-hosts-tsv') {
259 262
             my $registry = load_registry();
260
-            my $content = render_local_hosts_tsv($registry);
261
-            backup_file($opt{local_hosts_tsv});
262
-            write_file($opt{local_hosts_tsv}, $content);
263
-            return send_json($client, 200, { ok => json_bool(1), file => $opt{local_hosts_tsv} });
263
+            my $publish = publish_dns_change($registry, 'manual-render');
264
+            return send_json($client, 200, { ok => json_bool(1), file => $opt{local_hosts_tsv}, dns_publish => $publish });
264 265
         }
265 266
     }
266 267
 
@@ -283,6 +284,7 @@ sub save_registry {
283 284
     $registry->{updated_at} = iso_now();
284 285
     normalize_registry_policy($registry);
285 286
     save_registry_to_db($registry);
287
+    return publish_dns_change($registry, 'registry-save');
286 288
 }
287 289
 
288 290
 sub load_work_orders {
@@ -488,16 +490,15 @@ sub confirm_work_order {
488 490
     $work_order->{confirmed_at} = iso_now();
489 491
     $work_order->{result} = scalar(@$results) . ' action(s) applied';
490 492
 
491
-    save_registry($registry);
493
+    my $publish = save_registry($registry);
492 494
     save_work_orders($orders);
493
-    backup_file($opt{local_hosts_tsv});
494
-    write_file($opt{local_hosts_tsv}, render_local_hosts_tsv($registry));
495 495
 
496 496
     return send_json($client, 200, {
497 497
         ok => json_bool(1),
498 498
         work_order => $work_order,
499 499
         results => $results,
500 500
         local_hosts_tsv => $opt{local_hosts_tsv},
501
+        dns_publish => $publish,
501 502
     });
502 503
 }
503 504
 
@@ -835,7 +836,8 @@ sub reassign_vhost {
835 836
         my $err = $@ || 'vhost_reassign_failed';
836 837
         return send_json($client, 409, { error => 'vhost_reassign_failed', detail => clean_scalar($err) });
837 838
     }
838
-    return send_json($client, 200, { ok => json_bool(1), vhost_fqdn => $vhost, host_fqdn => $target_fqdn, previous_host_fqdn => $current_fqdn });
839
+    my $publish = publish_dns_change(load_registry(), 'vhost-reassign');
840
+    return send_json($client, 200, { ok => json_bool(1), vhost_fqdn => $vhost, host_fqdn => $target_fqdn, previous_host_fqdn => $current_fqdn, dns_publish => $publish });
839 841
 }
840 842
 
841 843
 sub upsert_vhost {
@@ -873,7 +875,8 @@ sub upsert_vhost {
873 875
         my $err = $@ || 'vhost_upsert_failed';
874 876
         return send_json($client, 409, { error => 'vhost_upsert_failed', detail => clean_scalar($err) });
875 877
     }
876
-    return send_json($client, 200, { ok => json_bool(1), vhost_fqdn => $vhost, host_fqdn => $target_fqdn, previous_host_fqdn => $current_fqdn || '' });
878
+    my $publish = publish_dns_change(load_registry(), 'vhost-upsert');
879
+    return send_json($client, 200, { ok => json_bool(1), vhost_fqdn => $vhost, host_fqdn => $target_fqdn, previous_host_fqdn => $current_fqdn || '', dns_publish => $publish });
877 880
 }
878 881
 
879 882
 sub delete_vhost {
@@ -911,7 +914,8 @@ sub delete_vhost {
911 914
         my $err = $@ || 'vhost_delete_failed';
912 915
         return send_json($client, 409, { error => 'vhost_delete_failed', detail => clean_scalar($err) });
913 916
     }
914
-    return send_json($client, 200, { ok => json_bool(1), vhost_fqdn => $vhost, previous_host_fqdn => $current_fqdn });
917
+    my $publish = publish_dns_change(load_registry(), 'vhost-delete');
918
+    return send_json($client, 200, { ok => json_bool(1), vhost_fqdn => $vhost, previous_host_fqdn => $current_fqdn, dns_publish => $publish });
915 919
 }
916 920
 
917 921
 sub set_host_certificate {
@@ -2163,6 +2167,33 @@ sub backup_file {
2163 2167
     write_file("$backup_dir/$name.$stamp.bak", read_file($path));
2164 2168
 }
2165 2169
 
2170
+sub publish_dns_change {
2171
+    my ($registry, $reason) = @_;
2172
+    $reason = clean_scalar($reason || 'registry-change');
2173
+
2174
+    backup_file($opt{local_hosts_tsv});
2175
+    write_file($opt{local_hosts_tsv}, render_local_hosts_tsv($registry));
2176
+
2177
+    my $trigger = $opt{dns_publish_trigger} || '';
2178
+    return {
2179
+        queued => json_bool(0),
2180
+        file => $opt{local_hosts_tsv},
2181
+        reason => $reason,
2182
+    } unless length $trigger;
2183
+
2184
+    ensure_parent_dir($trigger);
2185
+    open my $fh, '>>', $trigger or die "Cannot write DNS publish trigger $trigger: $!";
2186
+    print {$fh} iso_now() . "\t$reason\n";
2187
+    close $fh or die "Cannot close DNS publish trigger $trigger: $!";
2188
+
2189
+    return {
2190
+        queued => json_bool(1),
2191
+        file => $opt{local_hosts_tsv},
2192
+        trigger => $trigger,
2193
+        reason => $reason,
2194
+    };
2195
+}
2196
+
2166 2197
 my $db_handle;
2167 2198
 my $db_seeded = 0;
2168 2199