@@ -1,6 +1,6 @@ |
||
| 1 | 1 |
# SQLite Database |
| 2 | 2 |
|
| 3 |
-Madagascar Local Authority folosește SQLite ca store runtime pentru registry și Work Orders. |
|
| 3 |
+Madagascar Local Authority folosește SQLite ca sursă de adevăr runtime pentru hosturi, aliasuri, vhosturi, Work Orders, workeri de date și certificate. |
|
| 4 | 4 |
|
| 5 | 5 |
Locația implicită în checkout: |
| 6 | 6 |
|
@@ -20,191 +20,377 @@ Path-ul poate fi schimbat cu: |
||
| 20 | 20 |
HOST_MANAGER_DB=/path/to/host-manager.sqlite |
| 21 | 21 |
``` |
| 22 | 22 |
|
| 23 |
-## Rol |
|
| 23 |
+## Principii |
|
| 24 | 24 |
|
| 25 |
-SQLite este sursa de adevăr runtime. Fișierele din `config/` nu mai sunt store-ul live: |
|
| 25 |
+Hosturile sunt identificate prin FQDN complet, nu prin short name. Exemplu: `gw.local` și `gw.remote` sunt identități diferite. Coloana compatibilă `legacy_id` păstrează ID-ul scurt folosit de UI-ul curent, dar cheia reală este `hosts.fqdn`. |
|
| 26 | 26 |
|
| 27 |
-- `config/hosts.yaml` seed-uiește documentul `hosts_yaml` dacă baza este nouă. |
|
| 28 |
-- `config/work-orders.yaml` seed-uiește documentul `work_orders_yaml` dacă baza este nouă. |
|
| 29 |
-- `config/local-hosts.tsv` este manifest generat explicit din registry-ul SQLite. |
|
| 30 |
-- `/download/hosts.yaml`, `/download/local-hosts.tsv` și `/download/monitoring.json` sunt randate din SQLite. |
|
| 27 |
+Schema evită să transforme `hosts` într-un tabel cu prea multe coloane. Datele specializate sunt în tabele separate: |
|
| 31 | 28 |
|
| 32 |
-Aplicația nu face commit/push automat după modificări. Exportul și arhivarea rămân pași operaționali separați. |
|
| 29 |
+- aliasuri: `host_aliases` |
|
| 30 |
+- roluri: `host_roles` |
|
| 31 |
+- surse: `host_sources` |
|
| 32 |
+- flaguri: `host_flags` |
|
| 33 |
+- SSH: `host_ssh` |
|
| 34 |
+- vhosturi mutabile: `vhosts` |
|
| 35 |
+- certificate: `certificates`, `certificate_dns_names` |
|
| 36 |
+- workeri și observații: `data_workers`, `dhcp_leases`, `mdns_observations` |
|
| 33 | 37 |
|
| 34 |
-## Schema curentă |
|
| 38 |
+`documents` rămâne doar tabel legacy pentru migrarea din modelul vechi document-store. Aplicația nu îl mai folosește ca sursă de adevăr. |
|
| 35 | 39 |
|
| 36 |
-Schema este creată automat de `scripts/host_manager.pl` la prima citire/scriere a store-ului. |
|
| 40 |
+## Schema Version |
|
| 41 |
+ |
|
| 42 |
+Schema curentă este versiunea `2`. |
|
| 43 |
+ |
|
| 44 |
+```sql |
|
| 45 |
+schema_meta('schema_version') = '2'
|
|
| 46 |
+``` |
|
| 47 |
+ |
|
| 48 |
+`schema_meta` păstrează și metadate runtime precum `registry_updated_at`. |
|
| 49 |
+ |
|
| 50 |
+## Catalog |
|
| 51 |
+ |
|
| 52 |
+| Tabel | Rol | |
|
| 53 |
+|-------|-----| |
|
| 54 |
+| `schema_meta` | metadate de schemă/runtime | |
|
| 55 |
+| `documents` | document-store legacy pentru migrare | |
|
| 56 |
+| `hosts` | hosturi canonice, identificate prin FQDN | |
|
| 57 |
+| `host_aliases` | aliasuri păstrate inclusiv după retragere | |
|
| 58 |
+| `host_roles` | roluri active/retrase per host | |
|
| 59 |
+| `host_sources` | surse active/retrase per host | |
|
| 60 |
+| `host_flags` | flaguri extensibile per host | |
|
| 61 |
+| `host_ssh` | profile SSH per host | |
|
| 62 |
+| `vhosts` | vhosturi mutabile între hosturi | |
|
| 63 |
+| `data_workers` | workeri/surse care colectează date | |
|
| 64 |
+| `dhcp_leases` | date observate din DHCP lease/reservation | |
|
| 65 |
+| `mdns_observations` | date observate din mDNS | |
|
| 66 |
+| `certificates` | certificate emise de CA locală | |
|
| 67 |
+| `certificate_dns_names` | SAN DNS names pentru certificate | |
|
| 68 |
+| `work_orders` | Work Orders | |
|
| 69 |
+| `work_order_checklist` | checklist items pentru Work Orders | |
|
| 70 |
+| `work_order_actions` | acțiuni confirmabile pentru Work Orders | |
|
| 71 |
+ |
|
| 72 |
+## Tabele |
|
| 73 |
+ |
|
| 74 |
+### `hosts` |
|
| 37 | 75 |
|
| 38 | 76 |
```sql |
| 39 |
-CREATE TABLE IF NOT EXISTS documents ( |
|
| 40 |
- name TEXT PRIMARY KEY, |
|
| 41 |
- content TEXT NOT NULL, |
|
| 77 |
+CREATE TABLE hosts ( |
|
| 78 |
+ fqdn TEXT PRIMARY KEY, |
|
| 79 |
+ legacy_id TEXT NOT NULL UNIQUE, |
|
| 80 |
+ status TEXT NOT NULL DEFAULT 'active', |
|
| 81 |
+ hosts_ip TEXT NOT NULL DEFAULT '', |
|
| 82 |
+ dns_ip TEXT NOT NULL DEFAULT '', |
|
| 83 |
+ monitoring TEXT NOT NULL DEFAULT 'pending', |
|
| 84 |
+ notes TEXT NOT NULL DEFAULT '', |
|
| 85 |
+ created_at TEXT NOT NULL, |
|
| 42 | 86 |
updated_at TEXT NOT NULL |
| 43 | 87 |
); |
| 44 | 88 |
``` |
| 45 | 89 |
|
| 46 |
-### Catalog |
|
| 90 |
+Chei și indexuri: |
|
| 47 | 91 |
|
| 48 |
-| Obiect | Tip | Rol | |
|
| 49 |
-|--------|-----|-----| |
|
| 50 |
-| `documents` | table | Document-store runtime pentru registry și Work Orders | |
|
| 51 |
-| `sqlite_autoindex_documents_1` | index intern SQLite | Indexul automat creat pentru `PRIMARY KEY (name)` | |
|
| 92 |
+- PK: `hosts(fqdn)` |
|
| 93 |
+- UNIQUE: `hosts(legacy_id)` |
|
| 52 | 94 |
|
| 53 |
-Nu există alte tabele, view-uri, trigger-e sau indexuri definite de aplicație în schema curentă. |
|
| 95 |
+### `host_aliases` |
|
| 54 | 96 |
|
| 55 |
-### Tabel: `documents` |
|
| 56 |
- |
|
| 57 |
-| Coloană | Tip declarat | Null | Default | Cheie | Descriere | |
|
| 58 |
-|---------|--------------|------|---------|-------|-----------| |
|
| 59 |
-| `name` | `TEXT` | `NOT NULL` implicit prin PK | none | `PRIMARY KEY` | Identificatorul documentului operațional. Valorile cunoscute sunt `hosts_yaml` și `work_orders_yaml`. | |
|
| 60 |
-| `content` | `TEXT` | `NOT NULL` | none | none | Payload-ul documentului, serializat ca YAML strict compatibil cu parserul aplicației. | |
|
| 61 |
-| `updated_at` | `TEXT` | `NOT NULL` | none | none | Timestamp ISO UTC setat de aplicație la insert/update. | |
|
| 97 |
+```sql |
|
| 98 |
+CREATE TABLE host_aliases ( |
|
| 99 |
+ alias_name TEXT NOT NULL, |
|
| 100 |
+ host_fqdn TEXT NOT NULL REFERENCES hosts(fqdn) |
|
| 101 |
+ ON UPDATE CASCADE ON DELETE RESTRICT, |
|
| 102 |
+ alias_kind TEXT NOT NULL DEFAULT 'declared', |
|
| 103 |
+ status TEXT NOT NULL DEFAULT 'active', |
|
| 104 |
+ is_dns_published INTEGER NOT NULL DEFAULT 1, |
|
| 105 |
+ created_at TEXT NOT NULL, |
|
| 106 |
+ retired_at TEXT, |
|
| 107 |
+ notes TEXT NOT NULL DEFAULT '', |
|
| 108 |
+ PRIMARY KEY (alias_name, host_fqdn) |
|
| 109 |
+); |
|
| 110 |
+``` |
|
| 62 | 111 |
|
| 63 |
-### Chei |
|
| 112 |
+Indexuri: |
|
| 64 | 113 |
|
| 65 |
-| Tabel | Cheie | Coloane | Observații | |
|
| 66 |
-|-------|------|---------|------------| |
|
| 67 |
-| `documents` | Primary key | `name` | Garantează un singur rând pentru fiecare document operațional. | |
|
| 114 |
+```sql |
|
| 115 |
+CREATE UNIQUE INDEX idx_host_aliases_active_name |
|
| 116 |
+ON host_aliases(alias_name) |
|
| 117 |
+WHERE status = 'active'; |
|
| 68 | 118 |
|
| 69 |
-SQLite implementează cheia primară textuală prin indexul intern `sqlite_autoindex_documents_1`. |
|
| 119 |
+CREATE INDEX idx_host_aliases_host_status |
|
| 120 |
+ON host_aliases(host_fqdn, status); |
|
| 121 |
+``` |
|
| 70 | 122 |
|
| 71 |
-### Indexuri |
|
| 123 |
+Reguli: |
|
| 72 | 124 |
|
| 73 |
-| Index | Tabel | Coloane | Unic | Creat de | Rol | |
|
| 74 |
-|-------|-------|---------|------|----------|-----| |
|
| 75 |
-| `sqlite_autoindex_documents_1` | `documents` | `name` | da | SQLite | Lookup rapid pentru `SELECT ... WHERE name = ?` și enforcement pentru primary key. | |
|
| 125 |
+- aliasurile retrase nu se șterg; se setează `status = 'retired'` |
|
| 126 |
+- un alias activ poate aparține unui singur host |
|
| 127 |
+- aliasurile scurte derivate, cum ar fi `baobab`, sunt păstrate cu `alias_kind = 'derived'` |
|
| 128 |
+- short aliases derivate din vhosturi, cum ar fi `pmx.baobab`, sunt păstrate cu `alias_kind = 'derived-vhost'` |
|
| 76 | 129 |
|
| 77 |
-Nu există indexuri explicite definite de aplicație. Nu există index pe `updated_at`, pentru că aplicația nu face query-uri de tip listare istorică sau filtrare temporală. |
|
| 130 |
+### `vhosts` |
|
| 78 | 131 |
|
| 79 |
-### Relații |
|
| 132 |
+```sql |
|
| 133 |
+CREATE TABLE vhosts ( |
|
| 134 |
+ vhost_fqdn TEXT PRIMARY KEY, |
|
| 135 |
+ host_fqdn TEXT NOT NULL REFERENCES hosts(fqdn) |
|
| 136 |
+ ON UPDATE CASCADE ON DELETE RESTRICT, |
|
| 137 |
+ status TEXT NOT NULL DEFAULT 'active', |
|
| 138 |
+ service_name TEXT NOT NULL DEFAULT '', |
|
| 139 |
+ upstream_url TEXT NOT NULL DEFAULT '', |
|
| 140 |
+ tls_mode TEXT NOT NULL DEFAULT 'local-ca', |
|
| 141 |
+ certificate_id TEXT REFERENCES certificates(certificate_id) |
|
| 142 |
+ ON UPDATE CASCADE ON DELETE SET NULL, |
|
| 143 |
+ notes TEXT NOT NULL DEFAULT '', |
|
| 144 |
+ created_at TEXT NOT NULL, |
|
| 145 |
+ updated_at TEXT NOT NULL |
|
| 146 |
+); |
|
| 147 |
+``` |
|
| 80 | 148 |
|
| 81 |
-Nu există foreign keys între tabele, pentru că schema curentă are o singură tabelă. |
|
| 149 |
+Indexuri: |
|
| 82 | 150 |
|
| 83 |
-Relațiile de business sunt în interiorul YAML-ului din `content`, nu modelate relational. De exemplu: |
|
| 151 |
+```sql |
|
| 152 |
+CREATE INDEX idx_vhosts_host_status |
|
| 153 |
+ON vhosts(host_fqdn, status); |
|
| 154 |
+``` |
|
| 84 | 155 |
|
| 85 |
-- hosturile, numele, rolurile și sursele sunt structuri YAML în documentul `hosts_yaml` |
|
| 86 |
-- checklist-ul și acțiunile unui Work Order sunt structuri YAML în documentul `work_orders_yaml` |
|
| 87 |
-- acțiunile Work Order referă `host_id` textual în YAML, nu printr-un `FOREIGN KEY` |
|
| 156 |
+Un vhost se mută de pe un host pe altul prin update pe `vhosts.host_fqdn`. Vhosturile retrase rămân în tabel cu `status = 'retired'`. |
|
| 88 | 157 |
|
| 89 |
-`PRAGMA foreign_keys = ON` este activat ca pregătire pentru o schemă viitoare cu tabele relaționale, dar în schema curentă nu are efect practic. |
|
| 158 |
+### `host_roles`, `host_sources`, `host_flags`, `host_ssh` |
|
| 90 | 159 |
|
| 91 |
-### Constrângeri |
|
| 160 |
+```sql |
|
| 161 |
+CREATE TABLE host_roles ( |
|
| 162 |
+ host_fqdn TEXT NOT NULL REFERENCES hosts(fqdn) |
|
| 163 |
+ ON UPDATE CASCADE ON DELETE RESTRICT, |
|
| 164 |
+ role TEXT NOT NULL, |
|
| 165 |
+ status TEXT NOT NULL DEFAULT 'active', |
|
| 166 |
+ created_at TEXT NOT NULL, |
|
| 167 |
+ retired_at TEXT, |
|
| 168 |
+ PRIMARY KEY (host_fqdn, role) |
|
| 169 |
+); |
|
| 92 | 170 |
|
| 93 |
-| Tabel | Constrângere | Efect | |
|
| 94 |
-|-------|--------------|-------| |
|
| 95 |
-| `documents` | `PRIMARY KEY (name)` | Nu permite două documente cu același `name`. | |
|
| 96 |
-| `documents` | `content TEXT NOT NULL` | Nu permite documente fără payload. | |
|
| 97 |
-| `documents` | `updated_at TEXT NOT NULL` | Nu permite rânduri fără timestamp de modificare. | |
|
| 171 |
+CREATE TABLE host_sources ( |
|
| 172 |
+ host_fqdn TEXT NOT NULL REFERENCES hosts(fqdn) |
|
| 173 |
+ ON UPDATE CASCADE ON DELETE RESTRICT, |
|
| 174 |
+ source TEXT NOT NULL, |
|
| 175 |
+ status TEXT NOT NULL DEFAULT 'active', |
|
| 176 |
+ created_at TEXT NOT NULL, |
|
| 177 |
+ retired_at TEXT, |
|
| 178 |
+ PRIMARY KEY (host_fqdn, source) |
|
| 179 |
+); |
|
| 98 | 180 |
|
| 99 |
-Nu există `CHECK` constraints pentru valorile permise în `name`, formatul YAML sau formatul timestamp-ului. Aceste validări sunt făcute în codul Perl prin parserul strict YAML și prin fluxurile API. |
|
| 181 |
+CREATE TABLE host_flags ( |
|
| 182 |
+ host_fqdn TEXT NOT NULL REFERENCES hosts(fqdn) |
|
| 183 |
+ ON UPDATE CASCADE ON DELETE RESTRICT, |
|
| 184 |
+ flag TEXT NOT NULL, |
|
| 185 |
+ value TEXT NOT NULL DEFAULT '1', |
|
| 186 |
+ created_at TEXT NOT NULL, |
|
| 187 |
+ updated_at TEXT NOT NULL, |
|
| 188 |
+ PRIMARY KEY (host_fqdn, flag) |
|
| 189 |
+); |
|
| 100 | 190 |
|
| 101 |
-### Operațiuni SQL folosite de aplicație |
|
| 191 |
+CREATE TABLE host_ssh ( |
|
| 192 |
+ host_fqdn TEXT NOT NULL REFERENCES hosts(fqdn) |
|
| 193 |
+ ON UPDATE CASCADE ON DELETE RESTRICT, |
|
| 194 |
+ profile_name TEXT NOT NULL DEFAULT 'default', |
|
| 195 |
+ username TEXT NOT NULL DEFAULT '', |
|
| 196 |
+ port INTEGER NOT NULL DEFAULT 22, |
|
| 197 |
+ identity_file TEXT NOT NULL DEFAULT '', |
|
| 198 |
+ address TEXT NOT NULL DEFAULT '', |
|
| 199 |
+ local_forward_host TEXT NOT NULL DEFAULT '', |
|
| 200 |
+ local_forward_port INTEGER, |
|
| 201 |
+ remote_forward_host TEXT NOT NULL DEFAULT '', |
|
| 202 |
+ remote_forward_port INTEGER, |
|
| 203 |
+ notes TEXT NOT NULL DEFAULT '', |
|
| 204 |
+ created_at TEXT NOT NULL, |
|
| 205 |
+ updated_at TEXT NOT NULL, |
|
| 206 |
+ PRIMARY KEY (host_fqdn, profile_name) |
|
| 207 |
+); |
|
| 208 |
+``` |
|
| 102 | 209 |
|
| 103 |
-Citire document: |
|
| 210 |
+### `data_workers` |
|
| 104 | 211 |
|
| 105 | 212 |
```sql |
| 106 |
-SELECT content FROM documents WHERE name = ?; |
|
| 213 |
+CREATE TABLE data_workers ( |
|
| 214 |
+ worker_id TEXT PRIMARY KEY, |
|
| 215 |
+ worker_type TEXT NOT NULL, |
|
| 216 |
+ name TEXT NOT NULL DEFAULT '', |
|
| 217 |
+ status TEXT NOT NULL DEFAULT 'active', |
|
| 218 |
+ source TEXT NOT NULL DEFAULT '', |
|
| 219 |
+ last_run_at TEXT, |
|
| 220 |
+ notes TEXT NOT NULL DEFAULT '', |
|
| 221 |
+ created_at TEXT NOT NULL, |
|
| 222 |
+ updated_at TEXT NOT NULL |
|
| 223 |
+); |
|
| 107 | 224 |
``` |
| 108 | 225 |
|
| 109 |
-Insert/update document: |
|
| 226 |
+Indexuri: |
|
| 110 | 227 |
|
| 111 | 228 |
```sql |
| 112 |
-INSERT INTO documents (name, content, updated_at) |
|
| 113 |
-VALUES (?, ?, ?) |
|
| 114 |
-ON CONFLICT(name) DO UPDATE |
|
| 115 |
-SET content = excluded.content, |
|
| 116 |
- updated_at = excluded.updated_at; |
|
| 229 |
+CREATE INDEX idx_data_workers_type_status |
|
| 230 |
+ON data_workers(worker_type, status); |
|
| 117 | 231 |
``` |
| 118 | 232 |
|
| 119 |
-Creare schemă: |
|
| 233 |
+Seed implicit: |
|
| 234 |
+ |
|
| 235 |
+- `dhcp-router`, type `dhcp` |
|
| 236 |
+- `mdns-listener`, type `mdns` |
|
| 237 |
+ |
|
| 238 |
+### `dhcp_leases` |
|
| 120 | 239 |
|
| 121 | 240 |
```sql |
| 122 |
-CREATE TABLE IF NOT EXISTS documents ( |
|
| 123 |
- name TEXT PRIMARY KEY, |
|
| 124 |
- content TEXT NOT NULL, |
|
| 125 |
- updated_at TEXT NOT NULL |
|
| 241 |
+CREATE TABLE dhcp_leases ( |
|
| 242 |
+ lease_key TEXT PRIMARY KEY, |
|
| 243 |
+ worker_id TEXT NOT NULL REFERENCES data_workers(worker_id) |
|
| 244 |
+ ON UPDATE CASCADE ON DELETE RESTRICT, |
|
| 245 |
+ host_fqdn TEXT REFERENCES hosts(fqdn) |
|
| 246 |
+ ON UPDATE CASCADE ON DELETE SET NULL, |
|
| 247 |
+ observed_name TEXT NOT NULL DEFAULT '', |
|
| 248 |
+ ip_address TEXT NOT NULL, |
|
| 249 |
+ mac_address TEXT NOT NULL DEFAULT '', |
|
| 250 |
+ lease_state TEXT NOT NULL DEFAULT '', |
|
| 251 |
+ first_seen TEXT NOT NULL, |
|
| 252 |
+ last_seen TEXT NOT NULL, |
|
| 253 |
+ raw TEXT NOT NULL DEFAULT '' |
|
| 126 | 254 |
); |
| 127 | 255 |
``` |
| 128 | 256 |
|
| 129 |
-Setări aplicate la conectare: |
|
| 257 |
+Indexuri: |
|
| 130 | 258 |
|
| 131 | 259 |
```sql |
| 132 |
-PRAGMA journal_mode = WAL; |
|
| 133 |
-PRAGMA foreign_keys = ON; |
|
| 260 |
+CREATE INDEX idx_dhcp_leases_ip ON dhcp_leases(ip_address); |
|
| 261 |
+CREATE INDEX idx_dhcp_leases_mac ON dhcp_leases(mac_address); |
|
| 262 |
+CREATE INDEX idx_dhcp_leases_worker_last_seen |
|
| 263 |
+ON dhcp_leases(worker_id, last_seen); |
|
| 134 | 264 |
``` |
| 135 | 265 |
|
| 136 |
-### Versiune schemă |
|
| 137 |
- |
|
| 138 |
-Nu există încă tabelă `schema_migrations` sau `schema_version`. Schema este implicit versiunea 1 și este definită de blocul `CREATE TABLE IF NOT EXISTS documents` din `scripts/host_manager.pl`. |
|
| 139 |
- |
|
| 140 |
-În schema curentă există o singură tabelă generică, `documents`. Conținutul operațional rămâne serializat în formatul YAML strict deja folosit de aplicație. Alegerea este deliberată pentru migrarea minimă: sursa de adevăr se mută din working tree în SQLite fără să schimbăm încă parserul, UI-ul sau formatul exporturilor. |
|
| 141 |
- |
|
| 142 |
-## Documente |
|
| 143 |
- |
|
| 144 |
-### `hosts_yaml` |
|
| 266 |
+### `mdns_observations` |
|
| 145 | 267 |
|
| 146 |
-Conține registry-ul de hosturi în formatul `config/hosts.yaml`. |
|
| 147 |
- |
|
| 148 |
-Este citit de: |
|
| 268 |
+```sql |
|
| 269 |
+CREATE TABLE mdns_observations ( |
|
| 270 |
+ observation_key TEXT PRIMARY KEY, |
|
| 271 |
+ worker_id TEXT NOT NULL REFERENCES data_workers(worker_id) |
|
| 272 |
+ ON UPDATE CASCADE ON DELETE RESTRICT, |
|
| 273 |
+ host_fqdn TEXT REFERENCES hosts(fqdn) |
|
| 274 |
+ ON UPDATE CASCADE ON DELETE SET NULL, |
|
| 275 |
+ observed_name TEXT NOT NULL, |
|
| 276 |
+ ip_address TEXT NOT NULL, |
|
| 277 |
+ rr_type TEXT NOT NULL DEFAULT 'A', |
|
| 278 |
+ ttl INTEGER NOT NULL DEFAULT 0, |
|
| 279 |
+ first_seen TEXT NOT NULL, |
|
| 280 |
+ last_seen TEXT NOT NULL, |
|
| 281 |
+ seen_count INTEGER NOT NULL DEFAULT 1, |
|
| 282 |
+ last_peer TEXT NOT NULL DEFAULT '', |
|
| 283 |
+ raw TEXT NOT NULL DEFAULT '' |
|
| 284 |
+); |
|
| 285 |
+``` |
|
| 149 | 286 |
|
| 150 |
-- `GET /api/hosts` |
|
| 151 |
-- `GET /download/hosts.yaml` |
|
| 152 |
-- `GET /download/local-hosts.tsv` |
|
| 153 |
-- `GET /download/monitoring.json` |
|
| 154 |
-- `POST /api/render/local-hosts-tsv` |
|
| 287 |
+Indexuri: |
|
| 155 | 288 |
|
| 156 |
-Este scris de: |
|
| 289 |
+```sql |
|
| 290 |
+CREATE INDEX idx_mdns_observations_name ON mdns_observations(observed_name); |
|
| 291 |
+CREATE INDEX idx_mdns_observations_ip ON mdns_observations(ip_address); |
|
| 292 |
+CREATE INDEX idx_mdns_observations_worker_last_seen |
|
| 293 |
+ON mdns_observations(worker_id, last_seen); |
|
| 294 |
+``` |
|
| 157 | 295 |
|
| 158 |
-- `POST /api/hosts/upsert` |
|
| 159 |
-- `POST /api/hosts/delete` |
|
| 160 |
-- `POST /api/work-orders/confirm`, când o acțiune modifică registry-ul |
|
| 296 |
+### `certificates` și `certificate_dns_names` |
|
| 161 | 297 |
|
| 162 |
-Aplicația normalizează la salvare: |
|
| 298 |
+```sql |
|
| 299 |
+CREATE TABLE certificates ( |
|
| 300 |
+ certificate_id TEXT PRIMARY KEY, |
|
| 301 |
+ host_fqdn TEXT REFERENCES hosts(fqdn) |
|
| 302 |
+ ON UPDATE CASCADE ON DELETE SET NULL, |
|
| 303 |
+ common_name TEXT NOT NULL DEFAULT '', |
|
| 304 |
+ subject TEXT NOT NULL DEFAULT '', |
|
| 305 |
+ issuer TEXT NOT NULL DEFAULT '', |
|
| 306 |
+ serial TEXT UNIQUE, |
|
| 307 |
+ status TEXT NOT NULL DEFAULT 'issued', |
|
| 308 |
+ not_before TEXT NOT NULL DEFAULT '', |
|
| 309 |
+ not_after TEXT NOT NULL DEFAULT '', |
|
| 310 |
+ fingerprint_sha256 TEXT UNIQUE, |
|
| 311 |
+ cert_path TEXT NOT NULL DEFAULT '', |
|
| 312 |
+ csr_path TEXT NOT NULL DEFAULT '', |
|
| 313 |
+ created_at TEXT NOT NULL, |
|
| 314 |
+ updated_at TEXT NOT NULL, |
|
| 315 |
+ notes TEXT NOT NULL DEFAULT '' |
|
| 316 |
+); |
|
| 163 | 317 |
|
| 164 |
-```yaml |
|
| 165 |
-policy: |
|
| 166 |
- storage_authority: "sqlite" |
|
| 167 |
- runtime_database: "<HOST_MANAGER_DB>" |
|
| 318 |
+CREATE TABLE certificate_dns_names ( |
|
| 319 |
+ certificate_id TEXT NOT NULL REFERENCES certificates(certificate_id) |
|
| 320 |
+ ON UPDATE CASCADE ON DELETE CASCADE, |
|
| 321 |
+ dns_name TEXT NOT NULL, |
|
| 322 |
+ PRIMARY KEY (certificate_id, dns_name) |
|
| 323 |
+); |
|
| 168 | 324 |
``` |
| 169 | 325 |
|
| 170 |
-### `work_orders_yaml` |
|
| 326 |
+Indexuri: |
|
| 171 | 327 |
|
| 172 |
-Conține Work Orders în formatul `config/work-orders.yaml`. |
|
| 328 |
+```sql |
|
| 329 |
+CREATE INDEX idx_certificate_dns_names_dns_name |
|
| 330 |
+ON certificate_dns_names(dns_name); |
|
| 331 |
+``` |
|
| 173 | 332 |
|
| 174 |
-Este citit de: |
|
| 333 |
+Certificatele emise sunt sincronizate când aplicația citește lista CA prin `ca_manager.sh list-json`. |
|
| 175 | 334 |
|
| 176 |
-- `GET /api/work-orders` |
|
| 177 |
-- `POST /api/work-orders/checklist` |
|
| 178 |
-- `POST /api/work-orders/confirm` |
|
| 335 |
+### Work Orders |
|
| 179 | 336 |
|
| 180 |
-Este scris de: |
|
| 337 |
+```sql |
|
| 338 |
+CREATE TABLE work_orders ( |
|
| 339 |
+ id TEXT PRIMARY KEY, |
|
| 340 |
+ status TEXT NOT NULL DEFAULT 'pending', |
|
| 341 |
+ title TEXT NOT NULL DEFAULT '', |
|
| 342 |
+ reason TEXT NOT NULL DEFAULT '', |
|
| 343 |
+ created_at TEXT NOT NULL, |
|
| 344 |
+ confirmed_at TEXT NOT NULL DEFAULT '', |
|
| 345 |
+ result TEXT NOT NULL DEFAULT '', |
|
| 346 |
+ updated_at TEXT NOT NULL |
|
| 347 |
+); |
|
| 181 | 348 |
|
| 182 |
-- `POST /api/work-orders/checklist` |
|
| 183 |
-- `POST /api/work-orders/confirm` |
|
| 349 |
+CREATE TABLE work_order_checklist ( |
|
| 350 |
+ work_order_id TEXT NOT NULL REFERENCES work_orders(id) |
|
| 351 |
+ ON UPDATE CASCADE ON DELETE CASCADE, |
|
| 352 |
+ item_id TEXT NOT NULL, |
|
| 353 |
+ text TEXT NOT NULL DEFAULT '', |
|
| 354 |
+ status TEXT NOT NULL DEFAULT 'pending', |
|
| 355 |
+ owner TEXT NOT NULL DEFAULT '', |
|
| 356 |
+ notes TEXT NOT NULL DEFAULT '', |
|
| 357 |
+ updated_at TEXT NOT NULL DEFAULT '', |
|
| 358 |
+ PRIMARY KEY (work_order_id, item_id) |
|
| 359 |
+); |
|
| 184 | 360 |
|
| 185 |
-## Seed inițial |
|
| 361 |
+CREATE TABLE work_order_actions ( |
|
| 362 |
+ work_order_id TEXT NOT NULL REFERENCES work_orders(id) |
|
| 363 |
+ ON UPDATE CASCADE ON DELETE CASCADE, |
|
| 364 |
+ position INTEGER NOT NULL, |
|
| 365 |
+ type TEXT NOT NULL, |
|
| 366 |
+ host_fqdn TEXT REFERENCES hosts(fqdn) |
|
| 367 |
+ ON UPDATE CASCADE ON DELETE SET NULL, |
|
| 368 |
+ host_legacy_id TEXT NOT NULL DEFAULT '', |
|
| 369 |
+ name TEXT NOT NULL DEFAULT '', |
|
| 370 |
+ payload TEXT NOT NULL DEFAULT '', |
|
| 371 |
+ PRIMARY KEY (work_order_id, position) |
|
| 372 |
+); |
|
| 373 |
+``` |
|
| 186 | 374 |
|
| 187 |
-La prima accesare a unui document: |
|
| 375 |
+## Migrare și Seed |
|
| 188 | 376 |
|
| 189 |
-1. Aplicația caută rândul în `documents`. |
|
| 190 |
-2. Dacă rândul există, conținutul din SQLite câștigă. |
|
| 191 |
-3. Dacă rândul lipsește, aplicația citește fișierul seed din `config/`. |
|
| 192 |
-4. Dacă fișierul seed lipsește, aplicația creează un document gol valid. |
|
| 193 |
-5. Documentul este inserat în SQLite și de atunci SQLite devine autoritatea. |
|
| 377 |
+La prima inițializare a unei baze fără rânduri în `hosts`, aplicația seed-uiește din: |
|
| 194 | 378 |
|
| 195 |
-Important: după seed, modificările ulterioare ale fișierelor `config/hosts.yaml` sau `config/work-orders.yaml` nu schimbă automat baza existentă. |
|
| 379 |
+1. `documents.name = 'hosts_yaml'`, dacă există din vechiul model |
|
| 380 |
+2. `config/hosts.yaml`, dacă documentul legacy lipsește |
|
| 381 |
+3. document gol valid, dacă lipsește și seed-ul |
|
| 196 | 382 |
|
| 197 |
-## Fișiere SQLite |
|
| 383 |
+Work Orders se seed-uiesc similar din `documents.name = 'work_orders_yaml'` sau `config/work-orders.yaml`. |
|
| 198 | 384 |
|
| 199 |
-Cu WAL activ, runtime-ul poate avea trei fișiere: |
|
| 385 |
+Seed-ul curent produce: |
|
| 200 | 386 |
|
| 201 |
-```text |
|
| 202 |
-var/host-manager.sqlite |
|
| 203 |
-var/host-manager.sqlite-wal |
|
| 204 |
-var/host-manager.sqlite-shm |
|
| 205 |
-``` |
|
| 387 |
+- 11 hosturi cunoscute |
|
| 388 |
+- aliasuri scurte derivate pentru hosturi și vhosturi |
|
| 389 |
+- vhosturile `hosts.*`, `pmx.*` și `pbs.*` |
|
| 390 |
+- workerii `dhcp-router` și `mdns-listener` |
|
| 391 |
+- Work Order-ul existent pentru retragerea numelor legacy |
|
| 206 | 392 |
|
| 207 |
-Toate trei aparțin store-ului runtime. Nu se comit în git și nu se înlocuiesc prin deploy de cod. |
|
| 393 |
+`config/local-hosts.tsv` rămâne manifest generat explicit din tabelele runtime. |
|
| 208 | 394 |
|
| 209 | 395 |
## Inspecție |
| 210 | 396 |
|
@@ -212,45 +398,37 @@ Pe jumper: |
||
| 212 | 398 |
|
| 213 | 399 |
```bash |
| 214 | 400 |
cd /usr/local/xdev-host-manager |
| 215 |
-sqlite3 var/host-manager.sqlite '.schema' |
|
| 216 | 401 |
sqlite3 var/host-manager.sqlite '.tables' |
| 217 |
-sqlite3 var/host-manager.sqlite 'pragma table_info(documents);' |
|
| 218 |
-sqlite3 var/host-manager.sqlite 'pragma index_list(documents);' |
|
| 219 |
-sqlite3 var/host-manager.sqlite 'pragma index_info(sqlite_autoindex_documents_1);' |
|
| 220 |
-sqlite3 var/host-manager.sqlite 'pragma foreign_key_list(documents);' |
|
| 221 |
-sqlite3 var/host-manager.sqlite 'select name, updated_at, length(content) from documents order by name;' |
|
| 222 |
-sqlite3 var/host-manager.sqlite 'select content from documents where name = "hosts_yaml";' |
|
| 223 |
-sqlite3 var/host-manager.sqlite 'select content from documents where name = "work_orders_yaml";' |
|
| 402 |
+sqlite3 var/host-manager.sqlite '.schema hosts' |
|
| 403 |
+sqlite3 var/host-manager.sqlite '.schema host_aliases' |
|
| 404 |
+sqlite3 var/host-manager.sqlite '.schema vhosts' |
|
| 405 |
+sqlite3 var/host-manager.sqlite 'pragma foreign_key_list(vhosts);' |
|
| 406 |
+sqlite3 var/host-manager.sqlite 'pragma index_list(host_aliases);' |
|
| 407 |
+sqlite3 var/host-manager.sqlite 'select fqdn, legacy_id, status, dns_ip from hosts order by legacy_id;' |
|
| 408 |
+sqlite3 var/host-manager.sqlite 'select alias_name, host_fqdn, alias_kind, status from host_aliases order by alias_name;' |
|
| 409 |
+sqlite3 var/host-manager.sqlite 'select vhost_fqdn, host_fqdn, status from vhosts order by vhost_fqdn;' |
|
| 224 | 410 |
``` |
| 225 | 411 |
|
| 226 |
-Pentru export YAML curent: |
|
| 227 |
- |
|
| 228 |
-```bash |
|
| 229 |
-curl -fsS -b cookies.txt https://hosts.madagascar.xdev.ro/download/hosts.yaml |
|
| 230 |
-``` |
|
| 231 |
- |
|
| 232 |
-Endpoint-urile de download sunt OTP-protected; exemplul presupune o sesiune validă. |
|
| 233 |
- |
|
| 234 | 412 |
## Backup |
| 235 | 413 |
|
| 236 |
-Backup recomandat, cu serviciul oprit sau prin API-ul SQLite: |
|
| 414 |
+Backup recomandat: |
|
| 237 | 415 |
|
| 238 | 416 |
```bash |
| 239 | 417 |
cd /usr/local/xdev-host-manager |
| 240 | 418 |
sqlite3 var/host-manager.sqlite ".backup 'backups/host-manager/host-manager.sqlite.$(date +%Y%m%d_%H%M%S).bak'" |
| 241 | 419 |
``` |
| 242 | 420 |
|
| 243 |
-Pentru o copie brută, include și fișierele WAL/SHM sau oprește serviciul înainte: |
|
| 421 |
+Cu WAL activ, o copie brută trebuie să trateze fișierele ca set coerent: |
|
| 244 | 422 |
|
| 245 |
-```bash |
|
| 246 |
-sudo systemctl stop host-manager |
|
| 247 |
-sudo cp -a var/host-manager.sqlite* backups/host-manager/ |
|
| 248 |
-sudo systemctl start host-manager |
|
| 423 |
+```text |
|
| 424 |
+var/host-manager.sqlite |
|
| 425 |
+var/host-manager.sqlite-wal |
|
| 426 |
+var/host-manager.sqlite-shm |
|
| 249 | 427 |
``` |
| 250 | 428 |
|
| 251 | 429 |
## Restore |
| 252 | 430 |
|
| 253 |
-Restore-ul înlocuiește sursa de adevăr runtime și trebuie făcut explicit: |
|
| 431 |
+Restore-ul înlocuiește sursa de adevăr runtime: |
|
| 254 | 432 |
|
| 255 | 433 |
```bash |
| 256 | 434 |
sudo systemctl stop host-manager |
@@ -259,19 +437,3 @@ sudo chown host-manager:host-manager var/host-manager.sqlite |
||
| 259 | 437 |
sudo systemctl start host-manager |
| 260 | 438 |
curl -fsS http://127.0.0.1:8088/healthz >/dev/null |
| 261 | 439 |
``` |
| 262 |
- |
|
| 263 |
-Dacă se restaurează o copie brută, tratează `host-manager.sqlite`, `host-manager.sqlite-wal` și `host-manager.sqlite-shm` ca set coerent. |
|
| 264 |
- |
|
| 265 |
-## Evoluție probabilă |
|
| 266 |
- |
|
| 267 |
-Schema generică `documents` este potrivită pentru tranziția curentă. Dacă aplicația are nevoie de query-uri structurale, validări SQL sau merge/audit mai granular, următorul pas ar trebui să fie o migrare versionată către tabele dedicate, de exemplu: |
|
| 268 |
- |
|
| 269 |
-- `hosts` |
|
| 270 |
-- `host_names` |
|
| 271 |
-- `host_roles` |
|
| 272 |
-- `host_sources` |
|
| 273 |
-- `work_orders` |
|
| 274 |
-- `work_order_checklist` |
|
| 275 |
-- `work_order_actions` |
|
| 276 |
- |
|
| 277 |
-Până atunci, contractul stabil este numele documentelor din `documents` și formatele YAML randate de aplicație. |
|
@@ -161,3 +161,24 @@ Scop: |
||
| 161 | 161 |
- editările făcute în UI să nu se piardă la deploy/push de cod |
| 162 | 162 |
- să existe o singură autoritate runtime |
| 163 | 163 |
- YAML/TSV să rămână utile pentru export, review și bootstrap fără să fie storage-ul live |
| 164 |
+ |
|
| 165 |
+## 2026-06-09 - Relational Runtime Schema |
|
| 166 |
+ |
|
| 167 |
+Observație: primul pas SQLite mutase sursa de adevăr din git în baza de date, dar o păstra ca document YAML într-o tabelă generică. Asta rezolva pierderea editărilor la push, dar nu oferea o bază de date operațională reală. |
|
| 168 |
+ |
|
| 169 |
+Decizie: |
|
| 170 |
+ |
|
| 171 |
+- `hosts.fqdn` devine cheia reală pentru hosturi, ca numele complete să evite coliziuni între domenii |
|
| 172 |
+- `legacy_id` rămâne doar compatibilitate pentru UI/API-ul curent |
|
| 173 |
+- aliasurile sunt păstrate în `host_aliases`, inclusiv după retragere |
|
| 174 |
+- vhosturile sunt în `vhosts` și pot fi mutate prin schimbarea `host_fqdn` |
|
| 175 |
+- rolurile, sursele, flagurile și SSH sunt în tabele separate, nu coloane adăugate continuu în `hosts` |
|
| 176 |
+- workerii de date au `data_workers`, `dhcp_leases` și `mdns_observations` |
|
| 177 |
+- certificatele emise au `certificates` și `certificate_dns_names` |
|
| 178 |
+- `documents` rămâne doar tabel legacy pentru migrare din modelul vechi document-store |
|
| 179 |
+ |
|
| 180 |
+Scop: |
|
| 181 |
+ |
|
| 182 |
+- schema să poată susține inventar, DNS, vhosturi, observații externe și certificate fără să piardă istoric operațional |
|
| 183 |
+- aliasurile/vhosturile retrase să rămână audibile în baza de date |
|
| 184 |
+- structura să fie extensibilă fără să supraîncarce tabelul `hosts` |
|
@@ -16,7 +16,7 @@ MVP-ul curent nu are dependențe CPAN instalate direct pe host. Pentru store-ul |
||
| 16 | 16 |
|
| 17 | 17 |
## Rol |
| 18 | 18 |
|
| 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. |
|
| 19 |
+`var/host-manager.sqlite` este sursa de adevăr runtime pentru hosturi, aliasuri, vhosturi, Work Orders, workeri de date și certificate emise. La prima pornire, aplicația seed-uiește tabelele din `config/hosts.yaml` și `config/work-orders.yaml` dacă baza nu are încă rânduri operaționale. Aplicația este complet în spatele autentificării OTP pentru orice date de registru, exporturi sau modificări. |
|
| 20 | 20 |
|
| 21 | 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 |
|
@@ -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 registry se declară doar numele canonice/FQDN-urile necesare. |
|
| 154 |
+`madagascar.xdev.ro` este domeniul implicit al rețelei interne. Hosturile sunt identificate în baza de date prin FQDN complet, iar UI-ul păstrează temporar `id` ca identificator compatibil. |
|
| 155 | 155 |
|
| 156 | 156 |
Pentru orice nume `*.madagascar.xdev.ro`, aplicația derivă automat aliasul scurt prin eliminarea sufixului `.madagascar.xdev.ro`. |
| 157 | 157 |
|
@@ -160,7 +160,7 @@ Exemple: |
||
| 160 | 160 |
- `autonas01.madagascar.xdev.ro` produce automat aliasul `autonas01` |
| 161 | 161 |
- `pmx.baobab.madagascar.xdev.ro` produce automat aliasul `pmx.baobab` |
| 162 | 162 |
|
| 163 |
-Aliasurile derivate nu se declară separat în `hosts.yaml`. Ele apar în API, monitoring export și `local-hosts.tsv` ca nume efective. |
|
| 163 |
+Aliasurile derivate nu se declară separat în `hosts.yaml`. Ele sunt păstrate în tabelul `host_aliases` și apar în API, monitoring export și `local-hosts.tsv` ca nume efective. |
|
| 164 | 164 |
|
| 165 | 165 |
## Git și managementul cheilor |
| 166 | 166 |
|
@@ -179,7 +179,7 @@ Modelul recomandat: |
||
| 179 | 179 |
|
| 180 | 180 |
```text |
| 181 | 181 |
jumper |
| 182 |
- var/host-manager.sqlite sursă runtime pentru registry și Work Orders |
|
| 182 |
+ var/host-manager.sqlite sursă runtime relațională pentru registry și Work Orders |
|
| 183 | 183 |
host-manager editează SQLite cu OTP |
| 184 | 184 |
sync_local_hosts.sh aplică DNS după review/verificare |
| 185 | 185 |
|
@@ -4,7 +4,7 @@ policy: |
||
| 4 | 4 |
ip_authority: "dhcp" |
| 5 | 5 |
topology_authority: "madagascar.json" |
| 6 | 6 |
dns_manifest: "config/local-hosts.tsv" |
| 7 |
- storage_authority: "sqlite" |
|
| 7 |
+ storage_authority: "sqlite-relational" |
|
| 8 | 8 |
runtime_database: "var/host-manager.sqlite" |
| 9 | 9 |
consumer_access: "read-only deploy keys" |
| 10 | 10 |
hosts: |
@@ -212,7 +212,7 @@ sub app_page_path {
|
||
| 212 | 212 |
} |
| 213 | 213 |
|
| 214 | 214 |
sub load_registry {
|
| 215 |
- my $registry = parse_hosts_yaml(load_operational_doc('hosts_yaml', $opt{data}, default_hosts_yaml()));
|
|
| 215 |
+ my $registry = load_registry_from_db(); |
|
| 216 | 216 |
normalize_registry_policy($registry); |
| 217 | 217 |
return $registry; |
| 218 | 218 |
} |
@@ -221,16 +221,16 @@ sub save_registry {
|
||
| 221 | 221 |
my ($registry) = @_; |
| 222 | 222 |
$registry->{updated_at} = iso_now();
|
| 223 | 223 |
normalize_registry_policy($registry); |
| 224 |
- save_operational_doc('hosts_yaml', render_hosts_yaml($registry));
|
|
| 224 |
+ save_registry_to_db($registry); |
|
| 225 | 225 |
} |
| 226 | 226 |
|
| 227 | 227 |
sub load_work_orders {
|
| 228 |
- return parse_work_orders_yaml(load_operational_doc('work_orders_yaml', $opt{work_orders}, default_work_orders_yaml()));
|
|
| 228 |
+ return load_work_orders_from_db(); |
|
| 229 | 229 |
} |
| 230 | 230 |
|
| 231 | 231 |
sub save_work_orders {
|
| 232 | 232 |
my ($orders) = @_; |
| 233 |
- save_operational_doc('work_orders_yaml', render_work_orders_yaml($orders));
|
|
| 233 |
+ save_work_orders_to_db($orders); |
|
| 234 | 234 |
} |
| 235 | 235 |
|
| 236 | 236 |
sub work_orders_payload {
|
@@ -582,7 +582,80 @@ sub ca_manager_json {
|
||
| 582 | 582 |
local $/; |
| 583 | 583 |
my $out = <$fh>; |
| 584 | 584 |
close $fh or die "CA manager failed\n"; |
| 585 |
- return $out || '{}';
|
|
| 585 |
+ $out ||= $command eq 'list-json' ? '[]' : '{}';
|
|
| 586 |
+ sync_certificates_from_json($out) if $command eq 'list-json'; |
|
| 587 |
+ return $out; |
|
| 588 |
+} |
|
| 589 |
+ |
|
| 590 |
+sub sync_certificates_from_json {
|
|
| 591 |
+ my ($json) = @_; |
|
| 592 |
+ my $certs = eval { json_decode($json || '[]') };
|
|
| 593 |
+ return if $@ || ref($certs) ne 'ARRAY'; |
|
| 594 |
+ my $dbh = dbh(); |
|
| 595 |
+ my $now = iso_now(); |
|
| 596 |
+ with_transaction($dbh, sub {
|
|
| 597 |
+ for my $cert (@$certs) {
|
|
| 598 |
+ next unless ref($cert) eq 'HASH'; |
|
| 599 |
+ my $name = clean_id($cert->{name} || $cert->{serial} || $cert->{fingerprint_sha256} || '');
|
|
| 600 |
+ next unless $name; |
|
| 601 |
+ my @dns_names = map { normalize_dns_name($_) } @{ $cert->{dns_names} || [] };
|
|
| 602 |
+ my $host_fqdn = infer_certificate_host_fqdn($dbh, \@dns_names); |
|
| 603 |
+ my $cert_path = ca_issued_cert_path($name); |
|
| 604 |
+ my $csr_path = ca_dir() . "/csr/$name.csr.pem"; |
|
| 605 |
+ my $serial = clean_scalar($cert->{serial} || '');
|
|
| 606 |
+ my $fingerprint = clean_scalar($cert->{fingerprint_sha256} || '');
|
|
| 607 |
+ $dbh->do( |
|
| 608 |
+ 'INSERT INTO certificates (certificate_id, host_fqdn, common_name, subject, issuer, serial, status, not_before, not_after, fingerprint_sha256, cert_path, csr_path, created_at, updated_at, notes) ' |
|
| 609 |
+ . "VALUES (?, ?, ?, ?, ?, ?, 'issued', ?, ?, ?, ?, ?, ?, ?, '') " |
|
| 610 |
+ . 'ON CONFLICT(certificate_id) DO UPDATE SET host_fqdn = excluded.host_fqdn, common_name = excluded.common_name, ' |
|
| 611 |
+ . 'subject = excluded.subject, issuer = excluded.issuer, serial = excluded.serial, status = excluded.status, ' |
|
| 612 |
+ . 'not_before = excluded.not_before, not_after = excluded.not_after, fingerprint_sha256 = excluded.fingerprint_sha256, ' |
|
| 613 |
+ . 'cert_path = excluded.cert_path, csr_path = excluded.csr_path, updated_at = excluded.updated_at', |
|
| 614 |
+ undef, |
|
| 615 |
+ $name, |
|
| 616 |
+ $host_fqdn || undef, |
|
| 617 |
+ $dns_names[0] || '', |
|
| 618 |
+ clean_scalar($cert->{subject} || ''),
|
|
| 619 |
+ clean_scalar($cert->{issuer} || ''),
|
|
| 620 |
+ length($serial) ? $serial : undef, |
|
| 621 |
+ clean_scalar($cert->{not_before} || ''),
|
|
| 622 |
+ clean_scalar($cert->{not_after} || ''),
|
|
| 623 |
+ length($fingerprint) ? $fingerprint : undef, |
|
| 624 |
+ $cert_path, |
|
| 625 |
+ $csr_path, |
|
| 626 |
+ $now, |
|
| 627 |
+ $now, |
|
| 628 |
+ ); |
|
| 629 |
+ $dbh->do('DELETE FROM certificate_dns_names WHERE certificate_id = ?', undef, $name);
|
|
| 630 |
+ for my $dns_name (@dns_names) {
|
|
| 631 |
+ next unless length $dns_name; |
|
| 632 |
+ $dbh->do( |
|
| 633 |
+ 'INSERT OR IGNORE INTO certificate_dns_names (certificate_id, dns_name) VALUES (?, ?)', |
|
| 634 |
+ undef, |
|
| 635 |
+ $name, |
|
| 636 |
+ $dns_name, |
|
| 637 |
+ ); |
|
| 638 |
+ } |
|
| 639 |
+ } |
|
| 640 |
+ }); |
|
| 641 |
+} |
|
| 642 |
+ |
|
| 643 |
+sub infer_certificate_host_fqdn {
|
|
| 644 |
+ my ($dbh, $dns_names) = @_; |
|
| 645 |
+ for my $name (@$dns_names) {
|
|
| 646 |
+ my ($fqdn) = $dbh->selectrow_array('SELECT fqdn FROM hosts WHERE fqdn = ?', undef, $name);
|
|
| 647 |
+ return $fqdn if $fqdn; |
|
| 648 |
+ } |
|
| 649 |
+ for my $name (@$dns_names) {
|
|
| 650 |
+ my ($fqdn) = $dbh->selectrow_array('SELECT host_fqdn FROM host_aliases WHERE alias_name = ? AND status = ?', undef, $name, 'active');
|
|
| 651 |
+ return $fqdn if $fqdn; |
|
| 652 |
+ ($fqdn) = $dbh->selectrow_array('SELECT host_fqdn FROM vhosts WHERE vhost_fqdn = ? AND status = ?', undef, $name, 'active');
|
|
| 653 |
+ return $fqdn if $fqdn; |
|
| 654 |
+ } |
|
| 655 |
+ for my $name (@$dns_names) {
|
|
| 656 |
+ return $name if $name =~ /\./; |
|
| 657 |
+ } |
|
| 658 |
+ return ''; |
|
| 586 | 659 |
} |
| 587 | 660 |
|
| 588 | 661 |
sub parse_hosts_yaml {
|
@@ -1128,6 +1201,7 @@ sub backup_file {
|
||
| 1128 | 1201 |
} |
| 1129 | 1202 |
|
| 1130 | 1203 |
my $db_handle; |
| 1204 |
+my $db_seeded = 0; |
|
| 1131 | 1205 |
|
| 1132 | 1206 |
sub dbh {
|
| 1133 | 1207 |
return $db_handle if $db_handle; |
@@ -1145,44 +1219,791 @@ sub dbh {
|
||
| 1145 | 1219 |
) or die "Cannot open SQLite database $opt{db}\n";
|
| 1146 | 1220 |
$db_handle->do('PRAGMA journal_mode = WAL');
|
| 1147 | 1221 |
$db_handle->do('PRAGMA foreign_keys = ON');
|
| 1148 |
- $db_handle->do(<<'SQL'); |
|
| 1222 |
+ create_database_schema($db_handle); |
|
| 1223 |
+ seed_database($db_handle) unless $db_seeded++; |
|
| 1224 |
+ return $db_handle; |
|
| 1225 |
+} |
|
| 1226 |
+ |
|
| 1227 |
+sub create_database_schema {
|
|
| 1228 |
+ my ($dbh) = @_; |
|
| 1229 |
+ $dbh->do(<<'SQL'); |
|
| 1230 |
+CREATE TABLE IF NOT EXISTS schema_meta ( |
|
| 1231 |
+ key TEXT PRIMARY KEY, |
|
| 1232 |
+ value TEXT NOT NULL, |
|
| 1233 |
+ updated_at TEXT NOT NULL |
|
| 1234 |
+) |
|
| 1235 |
+SQL |
|
| 1236 |
+ $dbh->do(<<'SQL'); |
|
| 1149 | 1237 |
CREATE TABLE IF NOT EXISTS documents ( |
| 1150 | 1238 |
name TEXT PRIMARY KEY, |
| 1151 | 1239 |
content TEXT NOT NULL, |
| 1152 | 1240 |
updated_at TEXT NOT NULL |
| 1153 | 1241 |
) |
| 1154 | 1242 |
SQL |
| 1155 |
- return $db_handle; |
|
| 1243 |
+ $dbh->do( |
|
| 1244 |
+ 'INSERT INTO schema_meta (key, value, updated_at) VALUES (?, ?, ?) ' |
|
| 1245 |
+ . 'ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at', |
|
| 1246 |
+ undef, 'schema_version', '2', iso_now() |
|
| 1247 |
+ ); |
|
| 1248 |
+ $dbh->do(<<'SQL'); |
|
| 1249 |
+CREATE TABLE IF NOT EXISTS hosts ( |
|
| 1250 |
+ fqdn TEXT PRIMARY KEY, |
|
| 1251 |
+ legacy_id TEXT NOT NULL UNIQUE, |
|
| 1252 |
+ status TEXT NOT NULL DEFAULT 'active', |
|
| 1253 |
+ hosts_ip TEXT NOT NULL DEFAULT '', |
|
| 1254 |
+ dns_ip TEXT NOT NULL DEFAULT '', |
|
| 1255 |
+ monitoring TEXT NOT NULL DEFAULT 'pending', |
|
| 1256 |
+ notes TEXT NOT NULL DEFAULT '', |
|
| 1257 |
+ created_at TEXT NOT NULL, |
|
| 1258 |
+ updated_at TEXT NOT NULL |
|
| 1259 |
+) |
|
| 1260 |
+SQL |
|
| 1261 |
+ $dbh->do(<<'SQL'); |
|
| 1262 |
+CREATE TABLE IF NOT EXISTS host_aliases ( |
|
| 1263 |
+ alias_name TEXT NOT NULL, |
|
| 1264 |
+ host_fqdn TEXT NOT NULL, |
|
| 1265 |
+ alias_kind TEXT NOT NULL DEFAULT 'declared', |
|
| 1266 |
+ status TEXT NOT NULL DEFAULT 'active', |
|
| 1267 |
+ is_dns_published INTEGER NOT NULL DEFAULT 1, |
|
| 1268 |
+ created_at TEXT NOT NULL, |
|
| 1269 |
+ retired_at TEXT, |
|
| 1270 |
+ notes TEXT NOT NULL DEFAULT '', |
|
| 1271 |
+ PRIMARY KEY (alias_name, host_fqdn), |
|
| 1272 |
+ FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE RESTRICT |
|
| 1273 |
+) |
|
| 1274 |
+SQL |
|
| 1275 |
+ $dbh->do(<<'SQL'); |
|
| 1276 |
+CREATE UNIQUE INDEX IF NOT EXISTS idx_host_aliases_active_name |
|
| 1277 |
+ON host_aliases(alias_name) |
|
| 1278 |
+WHERE status = 'active' |
|
| 1279 |
+SQL |
|
| 1280 |
+ $dbh->do(<<'SQL'); |
|
| 1281 |
+CREATE INDEX IF NOT EXISTS idx_host_aliases_host_status |
|
| 1282 |
+ON host_aliases(host_fqdn, status) |
|
| 1283 |
+SQL |
|
| 1284 |
+ $dbh->do(<<'SQL'); |
|
| 1285 |
+CREATE TABLE IF NOT EXISTS host_roles ( |
|
| 1286 |
+ host_fqdn TEXT NOT NULL, |
|
| 1287 |
+ role TEXT NOT NULL, |
|
| 1288 |
+ status TEXT NOT NULL DEFAULT 'active', |
|
| 1289 |
+ created_at TEXT NOT NULL, |
|
| 1290 |
+ retired_at TEXT, |
|
| 1291 |
+ PRIMARY KEY (host_fqdn, role), |
|
| 1292 |
+ FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE RESTRICT |
|
| 1293 |
+) |
|
| 1294 |
+SQL |
|
| 1295 |
+ $dbh->do(<<'SQL'); |
|
| 1296 |
+CREATE TABLE IF NOT EXISTS host_sources ( |
|
| 1297 |
+ host_fqdn TEXT NOT NULL, |
|
| 1298 |
+ source TEXT NOT NULL, |
|
| 1299 |
+ status TEXT NOT NULL DEFAULT 'active', |
|
| 1300 |
+ created_at TEXT NOT NULL, |
|
| 1301 |
+ retired_at TEXT, |
|
| 1302 |
+ PRIMARY KEY (host_fqdn, source), |
|
| 1303 |
+ FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE RESTRICT |
|
| 1304 |
+) |
|
| 1305 |
+SQL |
|
| 1306 |
+ $dbh->do(<<'SQL'); |
|
| 1307 |
+CREATE TABLE IF NOT EXISTS host_flags ( |
|
| 1308 |
+ host_fqdn TEXT NOT NULL, |
|
| 1309 |
+ flag TEXT NOT NULL, |
|
| 1310 |
+ value TEXT NOT NULL DEFAULT '1', |
|
| 1311 |
+ created_at TEXT NOT NULL, |
|
| 1312 |
+ updated_at TEXT NOT NULL, |
|
| 1313 |
+ PRIMARY KEY (host_fqdn, flag), |
|
| 1314 |
+ FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE RESTRICT |
|
| 1315 |
+) |
|
| 1316 |
+SQL |
|
| 1317 |
+ $dbh->do(<<'SQL'); |
|
| 1318 |
+CREATE TABLE IF NOT EXISTS host_ssh ( |
|
| 1319 |
+ host_fqdn TEXT NOT NULL, |
|
| 1320 |
+ profile_name TEXT NOT NULL DEFAULT 'default', |
|
| 1321 |
+ username TEXT NOT NULL DEFAULT '', |
|
| 1322 |
+ port INTEGER NOT NULL DEFAULT 22, |
|
| 1323 |
+ identity_file TEXT NOT NULL DEFAULT '', |
|
| 1324 |
+ address TEXT NOT NULL DEFAULT '', |
|
| 1325 |
+ local_forward_host TEXT NOT NULL DEFAULT '', |
|
| 1326 |
+ local_forward_port INTEGER, |
|
| 1327 |
+ remote_forward_host TEXT NOT NULL DEFAULT '', |
|
| 1328 |
+ remote_forward_port INTEGER, |
|
| 1329 |
+ notes TEXT NOT NULL DEFAULT '', |
|
| 1330 |
+ created_at TEXT NOT NULL, |
|
| 1331 |
+ updated_at TEXT NOT NULL, |
|
| 1332 |
+ PRIMARY KEY (host_fqdn, profile_name), |
|
| 1333 |
+ FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE RESTRICT |
|
| 1334 |
+) |
|
| 1335 |
+SQL |
|
| 1336 |
+ $dbh->do(<<'SQL'); |
|
| 1337 |
+CREATE TABLE IF NOT EXISTS certificates ( |
|
| 1338 |
+ certificate_id TEXT PRIMARY KEY, |
|
| 1339 |
+ host_fqdn TEXT, |
|
| 1340 |
+ common_name TEXT NOT NULL DEFAULT '', |
|
| 1341 |
+ subject TEXT NOT NULL DEFAULT '', |
|
| 1342 |
+ issuer TEXT NOT NULL DEFAULT '', |
|
| 1343 |
+ serial TEXT UNIQUE, |
|
| 1344 |
+ status TEXT NOT NULL DEFAULT 'issued', |
|
| 1345 |
+ not_before TEXT NOT NULL DEFAULT '', |
|
| 1346 |
+ not_after TEXT NOT NULL DEFAULT '', |
|
| 1347 |
+ fingerprint_sha256 TEXT UNIQUE, |
|
| 1348 |
+ cert_path TEXT NOT NULL DEFAULT '', |
|
| 1349 |
+ csr_path TEXT NOT NULL DEFAULT '', |
|
| 1350 |
+ created_at TEXT NOT NULL, |
|
| 1351 |
+ updated_at TEXT NOT NULL, |
|
| 1352 |
+ notes TEXT NOT NULL DEFAULT '', |
|
| 1353 |
+ FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE SET NULL |
|
| 1354 |
+) |
|
| 1355 |
+SQL |
|
| 1356 |
+ $dbh->do(<<'SQL'); |
|
| 1357 |
+CREATE TABLE IF NOT EXISTS certificate_dns_names ( |
|
| 1358 |
+ certificate_id TEXT NOT NULL, |
|
| 1359 |
+ dns_name TEXT NOT NULL, |
|
| 1360 |
+ PRIMARY KEY (certificate_id, dns_name), |
|
| 1361 |
+ FOREIGN KEY (certificate_id) REFERENCES certificates(certificate_id) ON UPDATE CASCADE ON DELETE CASCADE |
|
| 1362 |
+) |
|
| 1363 |
+SQL |
|
| 1364 |
+ $dbh->do(<<'SQL'); |
|
| 1365 |
+CREATE INDEX IF NOT EXISTS idx_certificate_dns_names_dns_name |
|
| 1366 |
+ON certificate_dns_names(dns_name) |
|
| 1367 |
+SQL |
|
| 1368 |
+ $dbh->do(<<'SQL'); |
|
| 1369 |
+CREATE TABLE IF NOT EXISTS vhosts ( |
|
| 1370 |
+ vhost_fqdn TEXT PRIMARY KEY, |
|
| 1371 |
+ host_fqdn TEXT NOT NULL, |
|
| 1372 |
+ status TEXT NOT NULL DEFAULT 'active', |
|
| 1373 |
+ service_name TEXT NOT NULL DEFAULT '', |
|
| 1374 |
+ upstream_url TEXT NOT NULL DEFAULT '', |
|
| 1375 |
+ tls_mode TEXT NOT NULL DEFAULT 'local-ca', |
|
| 1376 |
+ certificate_id TEXT, |
|
| 1377 |
+ notes TEXT NOT NULL DEFAULT '', |
|
| 1378 |
+ created_at TEXT NOT NULL, |
|
| 1379 |
+ updated_at TEXT NOT NULL, |
|
| 1380 |
+ FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE RESTRICT, |
|
| 1381 |
+ FOREIGN KEY (certificate_id) REFERENCES certificates(certificate_id) ON UPDATE CASCADE ON DELETE SET NULL |
|
| 1382 |
+) |
|
| 1383 |
+SQL |
|
| 1384 |
+ $dbh->do(<<'SQL'); |
|
| 1385 |
+CREATE INDEX IF NOT EXISTS idx_vhosts_host_status |
|
| 1386 |
+ON vhosts(host_fqdn, status) |
|
| 1387 |
+SQL |
|
| 1388 |
+ $dbh->do(<<'SQL'); |
|
| 1389 |
+CREATE TABLE IF NOT EXISTS data_workers ( |
|
| 1390 |
+ worker_id TEXT PRIMARY KEY, |
|
| 1391 |
+ worker_type TEXT NOT NULL, |
|
| 1392 |
+ name TEXT NOT NULL DEFAULT '', |
|
| 1393 |
+ status TEXT NOT NULL DEFAULT 'active', |
|
| 1394 |
+ source TEXT NOT NULL DEFAULT '', |
|
| 1395 |
+ last_run_at TEXT, |
|
| 1396 |
+ notes TEXT NOT NULL DEFAULT '', |
|
| 1397 |
+ created_at TEXT NOT NULL, |
|
| 1398 |
+ updated_at TEXT NOT NULL |
|
| 1399 |
+) |
|
| 1400 |
+SQL |
|
| 1401 |
+ $dbh->do(<<'SQL'); |
|
| 1402 |
+CREATE INDEX IF NOT EXISTS idx_data_workers_type_status |
|
| 1403 |
+ON data_workers(worker_type, status) |
|
| 1404 |
+SQL |
|
| 1405 |
+ $dbh->do(<<'SQL'); |
|
| 1406 |
+CREATE TABLE IF NOT EXISTS dhcp_leases ( |
|
| 1407 |
+ lease_key TEXT PRIMARY KEY, |
|
| 1408 |
+ worker_id TEXT NOT NULL, |
|
| 1409 |
+ host_fqdn TEXT, |
|
| 1410 |
+ observed_name TEXT NOT NULL DEFAULT '', |
|
| 1411 |
+ ip_address TEXT NOT NULL, |
|
| 1412 |
+ mac_address TEXT NOT NULL DEFAULT '', |
|
| 1413 |
+ lease_state TEXT NOT NULL DEFAULT '', |
|
| 1414 |
+ first_seen TEXT NOT NULL, |
|
| 1415 |
+ last_seen TEXT NOT NULL, |
|
| 1416 |
+ raw TEXT NOT NULL DEFAULT '', |
|
| 1417 |
+ FOREIGN KEY (worker_id) REFERENCES data_workers(worker_id) ON UPDATE CASCADE ON DELETE RESTRICT, |
|
| 1418 |
+ FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE SET NULL |
|
| 1419 |
+) |
|
| 1420 |
+SQL |
|
| 1421 |
+ $dbh->do('CREATE INDEX IF NOT EXISTS idx_dhcp_leases_ip ON dhcp_leases(ip_address)');
|
|
| 1422 |
+ $dbh->do('CREATE INDEX IF NOT EXISTS idx_dhcp_leases_mac ON dhcp_leases(mac_address)');
|
|
| 1423 |
+ $dbh->do('CREATE INDEX IF NOT EXISTS idx_dhcp_leases_worker_last_seen ON dhcp_leases(worker_id, last_seen)');
|
|
| 1424 |
+ $dbh->do(<<'SQL'); |
|
| 1425 |
+CREATE TABLE IF NOT EXISTS mdns_observations ( |
|
| 1426 |
+ observation_key TEXT PRIMARY KEY, |
|
| 1427 |
+ worker_id TEXT NOT NULL, |
|
| 1428 |
+ host_fqdn TEXT, |
|
| 1429 |
+ observed_name TEXT NOT NULL, |
|
| 1430 |
+ ip_address TEXT NOT NULL, |
|
| 1431 |
+ rr_type TEXT NOT NULL DEFAULT 'A', |
|
| 1432 |
+ ttl INTEGER NOT NULL DEFAULT 0, |
|
| 1433 |
+ first_seen TEXT NOT NULL, |
|
| 1434 |
+ last_seen TEXT NOT NULL, |
|
| 1435 |
+ seen_count INTEGER NOT NULL DEFAULT 1, |
|
| 1436 |
+ last_peer TEXT NOT NULL DEFAULT '', |
|
| 1437 |
+ raw TEXT NOT NULL DEFAULT '', |
|
| 1438 |
+ FOREIGN KEY (worker_id) REFERENCES data_workers(worker_id) ON UPDATE CASCADE ON DELETE RESTRICT, |
|
| 1439 |
+ FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE SET NULL |
|
| 1440 |
+) |
|
| 1441 |
+SQL |
|
| 1442 |
+ $dbh->do('CREATE INDEX IF NOT EXISTS idx_mdns_observations_name ON mdns_observations(observed_name)');
|
|
| 1443 |
+ $dbh->do('CREATE INDEX IF NOT EXISTS idx_mdns_observations_ip ON mdns_observations(ip_address)');
|
|
| 1444 |
+ $dbh->do('CREATE INDEX IF NOT EXISTS idx_mdns_observations_worker_last_seen ON mdns_observations(worker_id, last_seen)');
|
|
| 1445 |
+ $dbh->do(<<'SQL'); |
|
| 1446 |
+CREATE TABLE IF NOT EXISTS work_orders ( |
|
| 1447 |
+ id TEXT PRIMARY KEY, |
|
| 1448 |
+ status TEXT NOT NULL DEFAULT 'pending', |
|
| 1449 |
+ title TEXT NOT NULL DEFAULT '', |
|
| 1450 |
+ reason TEXT NOT NULL DEFAULT '', |
|
| 1451 |
+ created_at TEXT NOT NULL, |
|
| 1452 |
+ confirmed_at TEXT NOT NULL DEFAULT '', |
|
| 1453 |
+ result TEXT NOT NULL DEFAULT '', |
|
| 1454 |
+ updated_at TEXT NOT NULL |
|
| 1455 |
+) |
|
| 1456 |
+SQL |
|
| 1457 |
+ $dbh->do(<<'SQL'); |
|
| 1458 |
+CREATE TABLE IF NOT EXISTS work_order_checklist ( |
|
| 1459 |
+ work_order_id TEXT NOT NULL, |
|
| 1460 |
+ item_id TEXT NOT NULL, |
|
| 1461 |
+ text TEXT NOT NULL DEFAULT '', |
|
| 1462 |
+ status TEXT NOT NULL DEFAULT 'pending', |
|
| 1463 |
+ owner TEXT NOT NULL DEFAULT '', |
|
| 1464 |
+ notes TEXT NOT NULL DEFAULT '', |
|
| 1465 |
+ updated_at TEXT NOT NULL DEFAULT '', |
|
| 1466 |
+ PRIMARY KEY (work_order_id, item_id), |
|
| 1467 |
+ FOREIGN KEY (work_order_id) REFERENCES work_orders(id) ON UPDATE CASCADE ON DELETE CASCADE |
|
| 1468 |
+) |
|
| 1469 |
+SQL |
|
| 1470 |
+ $dbh->do(<<'SQL'); |
|
| 1471 |
+CREATE TABLE IF NOT EXISTS work_order_actions ( |
|
| 1472 |
+ work_order_id TEXT NOT NULL, |
|
| 1473 |
+ position INTEGER NOT NULL, |
|
| 1474 |
+ type TEXT NOT NULL, |
|
| 1475 |
+ host_fqdn TEXT, |
|
| 1476 |
+ host_legacy_id TEXT NOT NULL DEFAULT '', |
|
| 1477 |
+ name TEXT NOT NULL DEFAULT '', |
|
| 1478 |
+ payload TEXT NOT NULL DEFAULT '', |
|
| 1479 |
+ PRIMARY KEY (work_order_id, position), |
|
| 1480 |
+ FOREIGN KEY (work_order_id) REFERENCES work_orders(id) ON UPDATE CASCADE ON DELETE CASCADE, |
|
| 1481 |
+ FOREIGN KEY (host_fqdn) REFERENCES hosts(fqdn) ON UPDATE CASCADE ON DELETE SET NULL |
|
| 1482 |
+) |
|
| 1483 |
+SQL |
|
| 1156 | 1484 |
} |
| 1157 | 1485 |
|
| 1158 |
-sub load_operational_doc {
|
|
| 1159 |
- my ($name, $seed_path, $default_text) = @_; |
|
| 1160 |
- my $dbh = dbh(); |
|
| 1486 |
+sub seed_database {
|
|
| 1487 |
+ my ($dbh) = @_; |
|
| 1488 |
+ seed_default_workers($dbh); |
|
| 1489 |
+ |
|
| 1490 |
+ if (!db_scalar($dbh, 'SELECT COUNT(*) FROM hosts')) {
|
|
| 1491 |
+ my $registry = parse_hosts_yaml(legacy_document_text($dbh, 'hosts_yaml', $opt{data}, default_hosts_yaml()));
|
|
| 1492 |
+ normalize_registry_policy($registry); |
|
| 1493 |
+ with_transaction($dbh, sub {
|
|
| 1494 |
+ import_registry_to_db($dbh, $registry, 0); |
|
| 1495 |
+ }); |
|
| 1496 |
+ } |
|
| 1497 |
+ |
|
| 1498 |
+ if (!db_scalar($dbh, 'SELECT COUNT(*) FROM work_orders')) {
|
|
| 1499 |
+ my $orders = parse_work_orders_yaml(legacy_document_text($dbh, 'work_orders_yaml', $opt{work_orders}, default_work_orders_yaml()));
|
|
| 1500 |
+ with_transaction($dbh, sub {
|
|
| 1501 |
+ import_work_orders_to_db($dbh, $orders); |
|
| 1502 |
+ }); |
|
| 1503 |
+ } |
|
| 1504 |
+ |
|
| 1505 |
+ seed_mdns_observations_from_yaml($dbh); |
|
| 1506 |
+} |
|
| 1507 |
+ |
|
| 1508 |
+sub with_transaction {
|
|
| 1509 |
+ my ($dbh, $code) = @_; |
|
| 1510 |
+ return $code->() unless $dbh->{AutoCommit};
|
|
| 1511 |
+ $dbh->begin_work; |
|
| 1512 |
+ my $ok = eval {
|
|
| 1513 |
+ $code->(); |
|
| 1514 |
+ 1; |
|
| 1515 |
+ }; |
|
| 1516 |
+ if (!$ok) {
|
|
| 1517 |
+ my $err = $@ || 'transaction failed'; |
|
| 1518 |
+ eval { $dbh->rollback };
|
|
| 1519 |
+ die $err; |
|
| 1520 |
+ } |
|
| 1521 |
+ $dbh->commit; |
|
| 1522 |
+} |
|
| 1523 |
+ |
|
| 1524 |
+sub db_scalar {
|
|
| 1525 |
+ my ($dbh, $sql, @bind) = @_; |
|
| 1526 |
+ my ($value) = $dbh->selectrow_array($sql, undef, @bind); |
|
| 1527 |
+ return $value || 0; |
|
| 1528 |
+} |
|
| 1529 |
+ |
|
| 1530 |
+sub legacy_document_text {
|
|
| 1531 |
+ my ($dbh, $name, $seed_path, $default_text) = @_; |
|
| 1161 | 1532 |
my $row = $dbh->selectrow_hashref('SELECT content FROM documents WHERE name = ?', undef, $name);
|
| 1162 |
- return $row->{content} if $row;
|
|
| 1533 |
+ return $row->{content} if $row && defined $row->{content};
|
|
| 1534 |
+ return read_file($seed_path) if -f $seed_path; |
|
| 1535 |
+ return $default_text; |
|
| 1536 |
+} |
|
| 1537 |
+ |
|
| 1538 |
+sub load_registry_from_db {
|
|
| 1539 |
+ my $dbh = dbh(); |
|
| 1540 |
+ my $registry = {
|
|
| 1541 |
+ version => 1, |
|
| 1542 |
+ updated_at => db_scalar($dbh, 'SELECT value FROM schema_meta WHERE key = ?', 'registry_updated_at') || '', |
|
| 1543 |
+ policy => {},
|
|
| 1544 |
+ hosts => [], |
|
| 1545 |
+ }; |
|
| 1546 |
+ |
|
| 1547 |
+ my $sth = $dbh->prepare('SELECT * FROM hosts ORDER BY legacy_id');
|
|
| 1548 |
+ $sth->execute; |
|
| 1549 |
+ while (my $row = $sth->fetchrow_hashref) {
|
|
| 1550 |
+ my $fqdn = $row->{fqdn};
|
|
| 1551 |
+ push @{ $registry->{hosts} }, {
|
|
| 1552 |
+ id => $row->{legacy_id},
|
|
| 1553 |
+ status => $row->{status},
|
|
| 1554 |
+ hosts_ip => $row->{hosts_ip},
|
|
| 1555 |
+ dns_ip => $row->{dns_ip},
|
|
| 1556 |
+ names => [ active_names_for_host($dbh, $fqdn) ], |
|
| 1557 |
+ roles => [ active_values_for_host($dbh, 'host_roles', 'role', $fqdn) ], |
|
| 1558 |
+ sources => [ active_values_for_host($dbh, 'host_sources', 'source', $fqdn) ], |
|
| 1559 |
+ monitoring => $row->{monitoring},
|
|
| 1560 |
+ notes => $row->{notes},
|
|
| 1561 |
+ }; |
|
| 1562 |
+ } |
|
| 1563 |
+ |
|
| 1564 |
+ return $registry; |
|
| 1565 |
+} |
|
| 1566 |
+ |
|
| 1567 |
+sub save_registry_to_db {
|
|
| 1568 |
+ my ($registry) = @_; |
|
| 1569 |
+ my $dbh = dbh(); |
|
| 1570 |
+ with_transaction($dbh, sub {
|
|
| 1571 |
+ import_registry_to_db($dbh, $registry, 1); |
|
| 1572 |
+ set_schema_meta($dbh, 'registry_updated_at', $registry->{updated_at} || iso_now());
|
|
| 1573 |
+ }); |
|
| 1574 |
+} |
|
| 1575 |
+ |
|
| 1576 |
+sub import_registry_to_db {
|
|
| 1577 |
+ my ($dbh, $registry, $retire_missing) = @_; |
|
| 1578 |
+ my %seen; |
|
| 1579 |
+ for my $host (@{ $registry->{hosts} || [] }) {
|
|
| 1580 |
+ my $fqdn = upsert_host_to_db($dbh, $host); |
|
| 1581 |
+ $seen{$fqdn} = 1 if $fqdn;
|
|
| 1582 |
+ } |
|
| 1583 |
+ |
|
| 1584 |
+ return unless $retire_missing; |
|
| 1585 |
+ my $sth = $dbh->prepare('SELECT fqdn FROM hosts WHERE status <> ?');
|
|
| 1586 |
+ $sth->execute('retired');
|
|
| 1587 |
+ while (my ($fqdn) = $sth->fetchrow_array) {
|
|
| 1588 |
+ next if $seen{$fqdn};
|
|
| 1589 |
+ retire_host_in_db($dbh, $fqdn); |
|
| 1590 |
+ } |
|
| 1591 |
+} |
|
| 1163 | 1592 |
|
| 1164 |
- my $content = -f $seed_path ? read_file($seed_path) : $default_text; |
|
| 1165 |
- save_operational_doc($name, $content); |
|
| 1166 |
- return $content; |
|
| 1593 |
+sub upsert_host_to_db {
|
|
| 1594 |
+ my ($dbh, $host) = @_; |
|
| 1595 |
+ my $now = iso_now(); |
|
| 1596 |
+ my $fqdn = canonical_host_fqdn($host); |
|
| 1597 |
+ return '' unless $fqdn; |
|
| 1598 |
+ my $legacy_id = clean_id($host->{id} || legacy_id_from_fqdn($fqdn));
|
|
| 1599 |
+ my $status = clean_scalar($host->{status} || 'active');
|
|
| 1600 |
+ my $hosts_ip = clean_scalar($host->{hosts_ip} || '');
|
|
| 1601 |
+ my $dns_ip = clean_scalar($host->{dns_ip} || '');
|
|
| 1602 |
+ my $monitoring = clean_scalar($host->{monitoring} || 'pending');
|
|
| 1603 |
+ my $notes = clean_scalar($host->{notes} || '');
|
|
| 1604 |
+ |
|
| 1605 |
+ $dbh->do( |
|
| 1606 |
+ 'INSERT INTO hosts (fqdn, legacy_id, status, hosts_ip, dns_ip, monitoring, notes, created_at, updated_at) ' |
|
| 1607 |
+ . 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ' |
|
| 1608 |
+ . 'ON CONFLICT(fqdn) DO UPDATE SET legacy_id = excluded.legacy_id, status = excluded.status, ' |
|
| 1609 |
+ . 'hosts_ip = excluded.hosts_ip, dns_ip = excluded.dns_ip, monitoring = excluded.monitoring, ' |
|
| 1610 |
+ . 'notes = excluded.notes, updated_at = excluded.updated_at', |
|
| 1611 |
+ undef, |
|
| 1612 |
+ $fqdn, $legacy_id, $status, $hosts_ip, $dns_ip, $monitoring, $notes, $now, $now, |
|
| 1613 |
+ ); |
|
| 1614 |
+ |
|
| 1615 |
+ sync_host_values($dbh, 'host_roles', 'role', $fqdn, [ clean_list($host->{roles}) ]);
|
|
| 1616 |
+ sync_host_values($dbh, 'host_sources', 'source', $fqdn, [ clean_list($host->{sources}) ]);
|
|
| 1617 |
+ sync_host_names($dbh, $fqdn, [ clean_list($host->{names}) ]);
|
|
| 1618 |
+ return $fqdn; |
|
| 1619 |
+} |
|
| 1620 |
+ |
|
| 1621 |
+sub sync_host_values {
|
|
| 1622 |
+ my ($dbh, $table, $column, $fqdn, $values) = @_; |
|
| 1623 |
+ my $now = iso_now(); |
|
| 1624 |
+ my %active = map { $_ => 1 } @$values;
|
|
| 1625 |
+ for my $value (@$values) {
|
|
| 1626 |
+ $dbh->do( |
|
| 1627 |
+ "INSERT INTO $table (host_fqdn, $column, status, created_at, retired_at) VALUES (?, ?, 'active', ?, '') " |
|
| 1628 |
+ . "ON CONFLICT(host_fqdn, $column) DO UPDATE SET status = 'active', retired_at = ''", |
|
| 1629 |
+ undef, |
|
| 1630 |
+ $fqdn, $value, $now, |
|
| 1631 |
+ ); |
|
| 1632 |
+ } |
|
| 1633 |
+ |
|
| 1634 |
+ my $sth = $dbh->prepare("SELECT $column FROM $table WHERE host_fqdn = ? AND status = 'active'");
|
|
| 1635 |
+ $sth->execute($fqdn); |
|
| 1636 |
+ while (my ($value) = $sth->fetchrow_array) {
|
|
| 1637 |
+ next if $active{$value};
|
|
| 1638 |
+ $dbh->do("UPDATE $table SET status = 'retired', retired_at = ? WHERE host_fqdn = ? AND $column = ?", undef, $now, $fqdn, $value);
|
|
| 1639 |
+ } |
|
| 1640 |
+} |
|
| 1641 |
+ |
|
| 1642 |
+sub sync_host_names {
|
|
| 1643 |
+ my ($dbh, $fqdn, $names) = @_; |
|
| 1644 |
+ my $now = iso_now(); |
|
| 1645 |
+ my (%aliases, %vhosts); |
|
| 1646 |
+ if (my $short = short_alias_for_fqdn($fqdn)) {
|
|
| 1647 |
+ $aliases{$short} = 1;
|
|
| 1648 |
+ upsert_alias_to_db($dbh, $fqdn, $short, 'derived', $now); |
|
| 1649 |
+ } |
|
| 1650 |
+ for my $name (@$names) {
|
|
| 1651 |
+ $name = normalize_dns_name($name); |
|
| 1652 |
+ next unless length $name; |
|
| 1653 |
+ next if $name eq $fqdn; |
|
| 1654 |
+ if (name_is_vhost($name)) {
|
|
| 1655 |
+ $vhosts{$name} = 1;
|
|
| 1656 |
+ upsert_vhost_to_db($dbh, $fqdn, $name, $now); |
|
| 1657 |
+ if (my $short = short_alias_for_fqdn($name)) {
|
|
| 1658 |
+ $aliases{$short} = 1;
|
|
| 1659 |
+ upsert_alias_to_db($dbh, $fqdn, $short, 'derived-vhost', $now); |
|
| 1660 |
+ } |
|
| 1661 |
+ } else {
|
|
| 1662 |
+ $aliases{$name} = 1;
|
|
| 1663 |
+ upsert_alias_to_db($dbh, $fqdn, $name, 'declared', $now); |
|
| 1664 |
+ if (my $short = short_alias_for_fqdn($name)) {
|
|
| 1665 |
+ $aliases{$short} = 1;
|
|
| 1666 |
+ upsert_alias_to_db($dbh, $fqdn, $short, 'derived', $now); |
|
| 1667 |
+ } |
|
| 1668 |
+ } |
|
| 1669 |
+ } |
|
| 1670 |
+ |
|
| 1671 |
+ retire_missing_names($dbh, 'host_aliases', 'alias_name', $fqdn, \%aliases, $now); |
|
| 1672 |
+ retire_missing_names($dbh, 'vhosts', 'vhost_fqdn', $fqdn, \%vhosts, $now); |
|
| 1673 |
+} |
|
| 1674 |
+ |
|
| 1675 |
+sub upsert_alias_to_db {
|
|
| 1676 |
+ my ($dbh, $fqdn, $alias, $kind, $now) = @_; |
|
| 1677 |
+ $dbh->do( |
|
| 1678 |
+ 'INSERT INTO host_aliases (alias_name, host_fqdn, alias_kind, status, is_dns_published, created_at, retired_at, notes) ' |
|
| 1679 |
+ . "VALUES (?, ?, ?, 'active', 1, ?, '', '') " |
|
| 1680 |
+ . "ON CONFLICT(alias_name, host_fqdn) DO UPDATE SET alias_kind = excluded.alias_kind, status = 'active', is_dns_published = 1, retired_at = ''", |
|
| 1681 |
+ undef, |
|
| 1682 |
+ $alias, $fqdn, $kind, $now, |
|
| 1683 |
+ ); |
|
| 1684 |
+} |
|
| 1685 |
+ |
|
| 1686 |
+sub upsert_vhost_to_db {
|
|
| 1687 |
+ my ($dbh, $fqdn, $vhost, $now) = @_; |
|
| 1688 |
+ my $service = vhost_service_name($vhost); |
|
| 1689 |
+ $dbh->do( |
|
| 1690 |
+ 'INSERT INTO vhosts (vhost_fqdn, host_fqdn, status, service_name, upstream_url, tls_mode, certificate_id, notes, created_at, updated_at) ' |
|
| 1691 |
+ . "VALUES (?, ?, 'active', ?, '', 'local-ca', NULL, '', ?, ?) " |
|
| 1692 |
+ . "ON CONFLICT(vhost_fqdn) DO UPDATE SET host_fqdn = excluded.host_fqdn, status = 'active', " |
|
| 1693 |
+ . 'service_name = excluded.service_name, updated_at = excluded.updated_at', |
|
| 1694 |
+ undef, |
|
| 1695 |
+ $vhost, $fqdn, $service, $now, $now, |
|
| 1696 |
+ ); |
|
| 1697 |
+} |
|
| 1698 |
+ |
|
| 1699 |
+sub retire_missing_names {
|
|
| 1700 |
+ my ($dbh, $table, $name_column, $fqdn, $active, $now) = @_; |
|
| 1701 |
+ my $sth = $dbh->prepare("SELECT $name_column FROM $table WHERE host_fqdn = ? AND status = 'active'");
|
|
| 1702 |
+ $sth->execute($fqdn); |
|
| 1703 |
+ while (my ($name) = $sth->fetchrow_array) {
|
|
| 1704 |
+ next if $active->{$name};
|
|
| 1705 |
+ if ($table eq 'host_aliases') {
|
|
| 1706 |
+ $dbh->do( |
|
| 1707 |
+ "UPDATE host_aliases SET status = 'retired', is_dns_published = 0, retired_at = ? WHERE host_fqdn = ? AND alias_name = ?", |
|
| 1708 |
+ undef, $now, $fqdn, $name, |
|
| 1709 |
+ ); |
|
| 1710 |
+ } else {
|
|
| 1711 |
+ $dbh->do( |
|
| 1712 |
+ "UPDATE vhosts SET status = 'retired', updated_at = ? WHERE host_fqdn = ? AND vhost_fqdn = ?", |
|
| 1713 |
+ undef, $now, $fqdn, $name, |
|
| 1714 |
+ ); |
|
| 1715 |
+ } |
|
| 1716 |
+ } |
|
| 1717 |
+} |
|
| 1718 |
+ |
|
| 1719 |
+sub retire_host_in_db {
|
|
| 1720 |
+ my ($dbh, $fqdn) = @_; |
|
| 1721 |
+ my $now = iso_now(); |
|
| 1722 |
+ $dbh->do("UPDATE hosts SET status = 'retired', updated_at = ? WHERE fqdn = ?", undef, $now, $fqdn);
|
|
| 1723 |
+ $dbh->do("UPDATE host_aliases SET status = 'retired', is_dns_published = 0, retired_at = ? WHERE host_fqdn = ? AND status = 'active'", undef, $now, $fqdn);
|
|
| 1724 |
+ $dbh->do("UPDATE vhosts SET status = 'retired', updated_at = ? WHERE host_fqdn = ? AND status = 'active'", undef, $now, $fqdn);
|
|
| 1725 |
+ $dbh->do("UPDATE host_roles SET status = 'retired', retired_at = ? WHERE host_fqdn = ? AND status = 'active'", undef, $now, $fqdn);
|
|
| 1726 |
+ $dbh->do("UPDATE host_sources SET status = 'retired', retired_at = ? WHERE host_fqdn = ? AND status = 'active'", undef, $now, $fqdn);
|
|
| 1167 | 1727 |
} |
| 1168 | 1728 |
|
| 1169 |
-sub save_operational_doc {
|
|
| 1170 |
- my ($name, $content) = @_; |
|
| 1729 |
+sub active_names_for_host {
|
|
| 1730 |
+ my ($dbh, $fqdn) = @_; |
|
| 1731 |
+ my @names = ($fqdn); |
|
| 1732 |
+ my $aliases = $dbh->prepare("SELECT alias_name FROM host_aliases WHERE host_fqdn = ? AND status = 'active' AND is_dns_published = 1 AND alias_kind NOT LIKE 'derived%' ORDER BY alias_name");
|
|
| 1733 |
+ $aliases->execute($fqdn); |
|
| 1734 |
+ while (my ($name) = $aliases->fetchrow_array) {
|
|
| 1735 |
+ push @names, $name; |
|
| 1736 |
+ } |
|
| 1737 |
+ my $vhosts = $dbh->prepare("SELECT vhost_fqdn FROM vhosts WHERE host_fqdn = ? AND status = 'active' ORDER BY vhost_fqdn");
|
|
| 1738 |
+ $vhosts->execute($fqdn); |
|
| 1739 |
+ while (my ($name) = $vhosts->fetchrow_array) {
|
|
| 1740 |
+ push @names, $name; |
|
| 1741 |
+ } |
|
| 1742 |
+ return unique_preserve(@names); |
|
| 1743 |
+} |
|
| 1744 |
+ |
|
| 1745 |
+sub active_values_for_host {
|
|
| 1746 |
+ my ($dbh, $table, $column, $fqdn) = @_; |
|
| 1747 |
+ my @values; |
|
| 1748 |
+ my $sth = $dbh->prepare("SELECT $column FROM $table WHERE host_fqdn = ? AND status = 'active' ORDER BY $column");
|
|
| 1749 |
+ $sth->execute($fqdn); |
|
| 1750 |
+ while (my ($value) = $sth->fetchrow_array) {
|
|
| 1751 |
+ push @values, $value; |
|
| 1752 |
+ } |
|
| 1753 |
+ return @values; |
|
| 1754 |
+} |
|
| 1755 |
+ |
|
| 1756 |
+sub load_work_orders_from_db {
|
|
| 1171 | 1757 |
my $dbh = dbh(); |
| 1758 |
+ my $orders = { version => 1, work_orders => [] };
|
|
| 1759 |
+ my $sth = $dbh->prepare('SELECT * FROM work_orders ORDER BY id');
|
|
| 1760 |
+ $sth->execute; |
|
| 1761 |
+ while (my $row = $sth->fetchrow_hashref) {
|
|
| 1762 |
+ my $wo = {
|
|
| 1763 |
+ id => $row->{id},
|
|
| 1764 |
+ status => $row->{status},
|
|
| 1765 |
+ title => $row->{title},
|
|
| 1766 |
+ reason => $row->{reason},
|
|
| 1767 |
+ created_at => $row->{created_at},
|
|
| 1768 |
+ checklist => [], |
|
| 1769 |
+ actions => [], |
|
| 1770 |
+ }; |
|
| 1771 |
+ $wo->{confirmed_at} = $row->{confirmed_at} if length($row->{confirmed_at} || '');
|
|
| 1772 |
+ $wo->{result} = $row->{result} if length($row->{result} || '');
|
|
| 1773 |
+ |
|
| 1774 |
+ my $items = $dbh->prepare('SELECT * FROM work_order_checklist WHERE work_order_id = ? ORDER BY item_id');
|
|
| 1775 |
+ $items->execute($row->{id});
|
|
| 1776 |
+ while (my $item = $items->fetchrow_hashref) {
|
|
| 1777 |
+ my %copy = ( |
|
| 1778 |
+ id => $item->{item_id},
|
|
| 1779 |
+ text => $item->{text},
|
|
| 1780 |
+ status => $item->{status},
|
|
| 1781 |
+ ); |
|
| 1782 |
+ for my $key (qw(owner notes updated_at)) {
|
|
| 1783 |
+ $copy{$key} = $item->{$key} if length($item->{$key} || '');
|
|
| 1784 |
+ } |
|
| 1785 |
+ push @{ $wo->{checklist} }, \%copy;
|
|
| 1786 |
+ } |
|
| 1787 |
+ |
|
| 1788 |
+ my $actions = $dbh->prepare('SELECT * FROM work_order_actions WHERE work_order_id = ? ORDER BY position');
|
|
| 1789 |
+ $actions->execute($row->{id});
|
|
| 1790 |
+ while (my $action = $actions->fetchrow_hashref) {
|
|
| 1791 |
+ my %copy = ( type => $action->{type} );
|
|
| 1792 |
+ $copy{host_id} = $action->{host_legacy_id} if length($action->{host_legacy_id} || '');
|
|
| 1793 |
+ $copy{name} = $action->{name} if length($action->{name} || '');
|
|
| 1794 |
+ push @{ $wo->{actions} }, \%copy;
|
|
| 1795 |
+ } |
|
| 1796 |
+ |
|
| 1797 |
+ push @{ $orders->{work_orders} }, $wo;
|
|
| 1798 |
+ } |
|
| 1799 |
+ return $orders; |
|
| 1800 |
+} |
|
| 1801 |
+ |
|
| 1802 |
+sub save_work_orders_to_db {
|
|
| 1803 |
+ my ($orders) = @_; |
|
| 1804 |
+ my $dbh = dbh(); |
|
| 1805 |
+ with_transaction($dbh, sub {
|
|
| 1806 |
+ import_work_orders_to_db($dbh, $orders); |
|
| 1807 |
+ }); |
|
| 1808 |
+} |
|
| 1809 |
+ |
|
| 1810 |
+sub import_work_orders_to_db {
|
|
| 1811 |
+ my ($dbh, $orders) = @_; |
|
| 1812 |
+ my $now = iso_now(); |
|
| 1813 |
+ my %seen; |
|
| 1814 |
+ for my $wo (@{ $orders->{work_orders} || [] }) {
|
|
| 1815 |
+ my $id = clean_scalar($wo->{id} || '');
|
|
| 1816 |
+ next unless $id; |
|
| 1817 |
+ $seen{$id} = 1;
|
|
| 1818 |
+ $dbh->do( |
|
| 1819 |
+ 'INSERT INTO work_orders (id, status, title, reason, created_at, confirmed_at, result, updated_at) ' |
|
| 1820 |
+ . 'VALUES (?, ?, ?, ?, ?, ?, ?, ?) ' |
|
| 1821 |
+ . 'ON CONFLICT(id) DO UPDATE SET status = excluded.status, title = excluded.title, reason = excluded.reason, ' |
|
| 1822 |
+ . 'created_at = excluded.created_at, confirmed_at = excluded.confirmed_at, result = excluded.result, updated_at = excluded.updated_at', |
|
| 1823 |
+ undef, |
|
| 1824 |
+ $id, |
|
| 1825 |
+ clean_scalar($wo->{status} || 'pending'),
|
|
| 1826 |
+ clean_scalar($wo->{title} || ''),
|
|
| 1827 |
+ clean_scalar($wo->{reason} || ''),
|
|
| 1828 |
+ clean_scalar($wo->{created_at} || $now),
|
|
| 1829 |
+ clean_scalar($wo->{confirmed_at} || ''),
|
|
| 1830 |
+ clean_scalar($wo->{result} || ''),
|
|
| 1831 |
+ $now, |
|
| 1832 |
+ ); |
|
| 1833 |
+ $dbh->do('DELETE FROM work_order_checklist WHERE work_order_id = ?', undef, $id);
|
|
| 1834 |
+ for my $item (@{ $wo->{checklist} || [] }) {
|
|
| 1835 |
+ $dbh->do( |
|
| 1836 |
+ 'INSERT INTO work_order_checklist (work_order_id, item_id, text, status, owner, notes, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)', |
|
| 1837 |
+ undef, |
|
| 1838 |
+ $id, |
|
| 1839 |
+ clean_scalar($item->{id} || ''),
|
|
| 1840 |
+ clean_scalar($item->{text} || ''),
|
|
| 1841 |
+ clean_scalar($item->{status} || 'pending'),
|
|
| 1842 |
+ clean_scalar($item->{owner} || ''),
|
|
| 1843 |
+ clean_scalar($item->{notes} || ''),
|
|
| 1844 |
+ clean_scalar($item->{updated_at} || ''),
|
|
| 1845 |
+ ); |
|
| 1846 |
+ } |
|
| 1847 |
+ $dbh->do('DELETE FROM work_order_actions WHERE work_order_id = ?', undef, $id);
|
|
| 1848 |
+ my $position = 0; |
|
| 1849 |
+ for my $action (@{ $wo->{actions} || [] }) {
|
|
| 1850 |
+ my $legacy_id = clean_id($action->{host_id} || '');
|
|
| 1851 |
+ my $host_fqdn = fqdn_for_legacy_id($dbh, $legacy_id); |
|
| 1852 |
+ $dbh->do( |
|
| 1853 |
+ 'INSERT INTO work_order_actions (work_order_id, position, type, host_fqdn, host_legacy_id, name, payload) VALUES (?, ?, ?, ?, ?, ?, ?)', |
|
| 1854 |
+ undef, |
|
| 1855 |
+ $id, |
|
| 1856 |
+ $position++, |
|
| 1857 |
+ clean_scalar($action->{type} || ''),
|
|
| 1858 |
+ $host_fqdn || undef, |
|
| 1859 |
+ $legacy_id, |
|
| 1860 |
+ normalize_dns_name($action->{name} || ''),
|
|
| 1861 |
+ '', |
|
| 1862 |
+ ); |
|
| 1863 |
+ } |
|
| 1864 |
+ } |
|
| 1865 |
+} |
|
| 1866 |
+ |
|
| 1867 |
+sub seed_default_workers {
|
|
| 1868 |
+ my ($dbh) = @_; |
|
| 1869 |
+ my $now = iso_now(); |
|
| 1870 |
+ my @workers = ( |
|
| 1871 |
+ [ 'dhcp-router', 'dhcp', 'Router DHCP leases', 'admin@192.168.2.1', 'DHCP lease/reservation collector source.' ], |
|
| 1872 |
+ [ 'mdns-listener', 'mdns', 'mDNS listener', 'var/mdns-observations.yaml', 'mDNS observation collector source.' ], |
|
| 1873 |
+ ); |
|
| 1874 |
+ for my $worker (@workers) {
|
|
| 1875 |
+ $dbh->do( |
|
| 1876 |
+ 'INSERT INTO data_workers (worker_id, worker_type, name, status, source, last_run_at, notes, created_at, updated_at) ' |
|
| 1877 |
+ . "VALUES (?, ?, ?, 'active', ?, NULL, ?, ?, ?) " |
|
| 1878 |
+ . 'ON CONFLICT(worker_id) DO UPDATE SET worker_type = excluded.worker_type, name = excluded.name, ' |
|
| 1879 |
+ . 'status = excluded.status, source = excluded.source, notes = excluded.notes, updated_at = excluded.updated_at', |
|
| 1880 |
+ undef, |
|
| 1881 |
+ @$worker, |
|
| 1882 |
+ $now, |
|
| 1883 |
+ $now, |
|
| 1884 |
+ ); |
|
| 1885 |
+ } |
|
| 1886 |
+} |
|
| 1887 |
+ |
|
| 1888 |
+sub seed_mdns_observations_from_yaml {
|
|
| 1889 |
+ my ($dbh) = @_; |
|
| 1890 |
+ return if db_scalar($dbh, 'SELECT COUNT(*) FROM mdns_observations'); |
|
| 1891 |
+ my $path = "$project_dir/var/mdns-observations.yaml"; |
|
| 1892 |
+ return unless -f $path; |
|
| 1893 |
+ my $db = parse_mdns_observations_yaml(read_file($path)); |
|
| 1894 |
+ with_transaction($dbh, sub {
|
|
| 1895 |
+ for my $observation (@{ $db->{observations} || [] }) {
|
|
| 1896 |
+ $dbh->do( |
|
| 1897 |
+ 'INSERT INTO mdns_observations (observation_key, worker_id, host_fqdn, observed_name, ip_address, rr_type, ttl, first_seen, last_seen, seen_count, last_peer, raw) ' |
|
| 1898 |
+ . "VALUES (?, 'mdns-listener', NULL, ?, ?, 'A', ?, ?, ?, ?, ?, '') " |
|
| 1899 |
+ . 'ON CONFLICT(observation_key) DO UPDATE SET observed_name = excluded.observed_name, ip_address = excluded.ip_address, ' |
|
| 1900 |
+ . 'ttl = excluded.ttl, last_seen = excluded.last_seen, seen_count = excluded.seen_count, last_peer = excluded.last_peer', |
|
| 1901 |
+ undef, |
|
| 1902 |
+ clean_scalar($observation->{key} || "$observation->{name}|$observation->{ip}"),
|
|
| 1903 |
+ clean_scalar($observation->{name} || ''),
|
|
| 1904 |
+ clean_scalar($observation->{ip} || ''),
|
|
| 1905 |
+ int($observation->{ttl} || 0),
|
|
| 1906 |
+ clean_scalar($observation->{first_seen} || iso_now()),
|
|
| 1907 |
+ clean_scalar($observation->{last_seen} || iso_now()),
|
|
| 1908 |
+ int($observation->{seen_count} || 1),
|
|
| 1909 |
+ clean_scalar($observation->{last_peer} || ''),
|
|
| 1910 |
+ ); |
|
| 1911 |
+ } |
|
| 1912 |
+ }); |
|
| 1913 |
+} |
|
| 1914 |
+ |
|
| 1915 |
+sub parse_mdns_observations_yaml {
|
|
| 1916 |
+ my ($text) = @_; |
|
| 1917 |
+ my %db = ( observations => [] ); |
|
| 1918 |
+ my ($section, $current); |
|
| 1919 |
+ for my $line (split /\n/, $text || '') {
|
|
| 1920 |
+ next if $line =~ /^\s*$/ || $line =~ /^\s*#/; |
|
| 1921 |
+ if ($line =~ /^observations:\s*$/) {
|
|
| 1922 |
+ $section = 'observations'; |
|
| 1923 |
+ } elsif (($section || '') eq 'observations' && $line =~ /^ - key:\s*(.+)$/) {
|
|
| 1924 |
+ $current = { key => yaml_unquote($1) };
|
|
| 1925 |
+ push @{ $db{observations} }, $current;
|
|
| 1926 |
+ } elsif ($current && $line =~ /^ ([A-Za-z0-9_]+):\s*(.*)$/) {
|
|
| 1927 |
+ $current->{$1} = yaml_unquote($2);
|
|
| 1928 |
+ } |
|
| 1929 |
+ } |
|
| 1930 |
+ return \%db; |
|
| 1931 |
+} |
|
| 1932 |
+ |
|
| 1933 |
+sub set_schema_meta {
|
|
| 1934 |
+ my ($dbh, $key, $value) = @_; |
|
| 1172 | 1935 |
$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', |
|
| 1936 |
+ 'INSERT INTO schema_meta (key, value, updated_at) VALUES (?, ?, ?) ' |
|
| 1937 |
+ . 'ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at', |
|
| 1175 | 1938 |
undef, |
| 1176 |
- $name, |
|
| 1177 |
- $content, |
|
| 1939 |
+ $key, |
|
| 1940 |
+ defined $value ? $value : '', |
|
| 1178 | 1941 |
iso_now(), |
| 1179 | 1942 |
); |
| 1180 | 1943 |
} |
| 1181 | 1944 |
|
| 1945 |
+sub fqdn_for_legacy_id {
|
|
| 1946 |
+ my ($dbh, $legacy_id) = @_; |
|
| 1947 |
+ return '' unless length($legacy_id || ''); |
|
| 1948 |
+ my ($fqdn) = $dbh->selectrow_array('SELECT fqdn FROM hosts WHERE legacy_id = ?', undef, $legacy_id);
|
|
| 1949 |
+ return $fqdn || ''; |
|
| 1950 |
+} |
|
| 1951 |
+ |
|
| 1952 |
+sub canonical_host_fqdn {
|
|
| 1953 |
+ my ($host) = @_; |
|
| 1954 |
+ my @names = map { normalize_dns_name($_) } @{ $host->{names} || [] };
|
|
| 1955 |
+ for my $name (@names) {
|
|
| 1956 |
+ return $name if $name =~ /\.madagascar\.xdev\.ro\z/ && !name_is_vhost($name); |
|
| 1957 |
+ } |
|
| 1958 |
+ for my $name (@names) {
|
|
| 1959 |
+ return $name if $name =~ /\./ && !name_is_vhost($name); |
|
| 1960 |
+ } |
|
| 1961 |
+ for my $name (@names) {
|
|
| 1962 |
+ return $name if $name =~ /\./; |
|
| 1963 |
+ } |
|
| 1964 |
+ my $id = clean_id($host->{id} || '');
|
|
| 1965 |
+ return $id ? "$id.madagascar.xdev.ro" : ''; |
|
| 1966 |
+} |
|
| 1967 |
+ |
|
| 1968 |
+sub legacy_id_from_fqdn {
|
|
| 1969 |
+ my ($fqdn) = @_; |
|
| 1970 |
+ $fqdn = normalize_dns_name($fqdn); |
|
| 1971 |
+ $fqdn =~ s/\.madagascar\.xdev\.ro\z//; |
|
| 1972 |
+ $fqdn =~ s/\..*\z//; |
|
| 1973 |
+ return clean_id($fqdn); |
|
| 1974 |
+} |
|
| 1975 |
+ |
|
| 1976 |
+sub normalize_dns_name {
|
|
| 1977 |
+ my ($name) = @_; |
|
| 1978 |
+ $name = lc clean_scalar($name || ''); |
|
| 1979 |
+ $name =~ s/\.\z//; |
|
| 1980 |
+ return $name; |
|
| 1981 |
+} |
|
| 1982 |
+ |
|
| 1983 |
+sub name_is_vhost {
|
|
| 1984 |
+ my ($name) = @_; |
|
| 1985 |
+ $name = normalize_dns_name($name); |
|
| 1986 |
+ return $name =~ /\A(?:pmx|pbs|hosts)\./ ? 1 : 0; |
|
| 1987 |
+} |
|
| 1988 |
+ |
|
| 1989 |
+sub vhost_service_name {
|
|
| 1990 |
+ my ($name) = @_; |
|
| 1991 |
+ $name = normalize_dns_name($name); |
|
| 1992 |
+ return $1 if $name =~ /\A([a-z0-9-]+)\./; |
|
| 1993 |
+ return ''; |
|
| 1994 |
+} |
|
| 1995 |
+ |
|
| 1996 |
+sub short_alias_for_fqdn {
|
|
| 1997 |
+ my ($name) = @_; |
|
| 1998 |
+ $name = normalize_dns_name($name); |
|
| 1999 |
+ return $1 if $name =~ /\A(.+)\.madagascar\.xdev\.ro\z/; |
|
| 2000 |
+ return ''; |
|
| 2001 |
+} |
|
| 2002 |
+ |
|
| 1182 | 2003 |
sub normalize_registry_policy {
|
| 1183 | 2004 |
my ($registry) = @_; |
| 1184 | 2005 |
$registry->{policy} ||= {};
|
| 1185 |
- $registry->{policy}{storage_authority} = 'sqlite';
|
|
| 2006 |
+ $registry->{policy}{storage_authority} = 'sqlite-relational';
|
|
| 1186 | 2007 |
$registry->{policy}{runtime_database} = $opt{db};
|
| 1187 | 2008 |
} |
| 1188 | 2009 |
|
@@ -1191,7 +2012,7 @@ sub default_hosts_yaml {
|
||
| 1191 | 2012 |
version: 1 |
| 1192 | 2013 |
updated_at: "" |
| 1193 | 2014 |
policy: |
| 1194 |
- storage_authority: "sqlite" |
|
| 2015 |
+ storage_authority: "sqlite-relational" |
|
| 1195 | 2016 |
hosts: |
| 1196 | 2017 |
YAML |
| 1197 | 2018 |
} |