From 89787b0e23d2f09985dfa8b8d5ef49bdaa63921a Mon Sep 17 00:00:00 2001 From: spektr Date: Sat, 31 Oct 2026 16:06:21 +0100 Subject: [PATCH] LLM-Server: IPv6 deaktiviert (host_baseline) + Swift-Repin und wget-4-Download nach Live-Befunden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Betreiberentscheidung 31.10.2026 (GNAI-002 Abschnitt 6): Der KI-Host wird kohärent IPv4-only betrieben. host_baseline verwaltet /etc/sysctl.d/99-ghostnet-disable-ipv6.conf (all+default), wendet es bei Änderung an und verifiziert den Laufzeitwert. Anlass: hängender get_url- Download durch AAAA-Antworten ohne nutzbare IPv6-Route (Messungen im Dokument; Pi-hole filtert AAAA nicht, der Host nutzt Pi-hole nicht). model_store nach Live-Befunden: Swift-Repin auf Repository-Revision 98a6100 (alte Revision entfernt, Datei laut Herausgeber-SHA256SUMS unverändert; GNAI-010 v0.5) und Download über wget -4 mit expliziter SHA-256-Verifikation vor dem atomaren Move. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- .../ai/GNAI-010-TECH_Llama-Serving.md | 7 ++- .../host/GNAI-002-INFRA_BaseSystem.md | 44 +++++++++++++++ .../ansible/roles/host_baseline/README.md | 8 +++ .../roles/host_baseline/defaults/main.yml | 9 +++ .../roles/host_baseline/tasks/main.yml | 47 ++++++++++++++++ .../llm/ansible/roles/model_store/README.md | 7 ++- .../roles/model_store/defaults/main.yml | 8 +-- .../ansible/roles/model_store/tasks/main.yml | 56 ++++++++++++++++--- 8 files changed, 169 insertions(+), 17 deletions(-) diff --git a/docs/platform/llm-server/ai/GNAI-010-TECH_Llama-Serving.md b/docs/platform/llm-server/ai/GNAI-010-TECH_Llama-Serving.md index 7924bdc..c993c58 100644 --- a/docs/platform/llm-server/ai/GNAI-010-TECH_Llama-Serving.md +++ b/docs/platform/llm-server/ai/GNAI-010-TECH_Llama-Serving.md @@ -21,6 +21,7 @@ | 2026-09-21 | 0.3 | spektr / Codex | je Modell ein festes Quadlet vorgesehen; Swift und Unsloth durch kontrollierten Stop/Start gegenseitig ausgeschlossen; Router-Modus zurückgestellt | | 2026-09-21 | 0.2 | spektr / Codex | Unsloth UD-Q6_K als zweites installiertes Modellprofil und kontrollierten Profilwechsel für A/B-Tests ergänzt | | 2026-09-21 | 0.1 | spektr / Codex | erstes technisches Profil für Swift Q6_K, 128k, Q8-KV, rootless Quadlet und gestufte MTP-Abnahme erstellt | +| 2026-10-31 | 0.5 | spektr / Mistral Vibe | Swift-Repository-Revision aktualisiert: `eb0e3a7…` wurde vom Herausgeber aus dem Repository entfernt (resolve liefert 404); Repin auf den aktuellen `main`-Commit `98a6100…`. Die Q6_K-Datei ist laut Herausgeber-SHA256SUMS unverändert (`7f4de8…`), der Vision-Projektor ebenfalls (`daa111…`); beide Prüfsummen bleiben unverändert und werden beim Download verifiziert. | --- @@ -80,11 +81,11 @@ Testing zur Recherche aufgelöst, aber nicht in ein freigegebenes Profil | Feld | Wert | | --- | --- | | Repository | `ukisai/Swift-Qwen3.8-27B-GGUF` | -| Repository-Revision | `eb0e3a7dc70643c2ab920f5133750d02c20849ff` | +| Repository-Revision | `98a6100719e3aafa186041089b71c7b48390dbe1` (am 31.10.2026 repinnt; die ursprüngliche Revision `eb0e3a7dc70643c2ab920f5133750d02c20849ff` wurde vom Herausgeber entfernt, die Datei-Inhalte sind laut Prüfsummen unverändert) | | Datei | `Swift-Qwen3.8-27B-Q6_K.gguf` | | veröffentlichte Größe | 22,9 GB | | SHA-256 | `7f4de8abd5446c08b0f975a1b38e43d02639f4ae9ecdd2ddfd5c5b0f612bda59` | -| Download | `https://huggingface.co/ukisai/Swift-Qwen3.8-27B-GGUF/resolve/eb0e3a7dc70643c2ab920f5133750d02c20849ff/Swift-Qwen3.8-27B-Q6_K.gguf` | +| Download | `https://huggingface.co/ukisai/Swift-Qwen3.8-27B-GGUF/resolve/98a6100719e3aafa186041089b71c7b48390dbe1/Swift-Qwen3.8-27B-Q6_K.gguf` | | API-Alias | `swift-qwen3.8-27b-q6k` | Die Prüfsumme stammt aus der vom Herausgeber veröffentlichten @@ -123,7 +124,7 @@ Für eine spätere Vision-Stufe ist vorgesehen: | Datei | `mmproj-Swift-Qwen3.8-27B-F16.gguf` | | veröffentlichte Größe | 928 MB | | SHA-256 | `daa1116c9422fa390cc8688495da0e91781f92841dfc3b31a378ff252571745a` | -| Download | `https://huggingface.co/ukisai/Swift-Qwen3.8-27B-GGUF/resolve/eb0e3a7dc70643c2ab920f5133750d02c20849ff/mmproj-Swift-Qwen3.8-27B-F16.gguf` | +| Download | `https://huggingface.co/ukisai/Swift-Qwen3.8-27B-GGUF/resolve/98a6100719e3aafa186041089b71c7b48390dbe1/mmproj-Swift-Qwen3.8-27B-F16.gguf` | Der Projektor wird erst heruntergeladen und mit `--mmproj` eingebunden, wenn die Vision-Teststufe ausdrücklich aktiviert wird. diff --git a/docs/platform/llm-server/host/GNAI-002-INFRA_BaseSystem.md b/docs/platform/llm-server/host/GNAI-002-INFRA_BaseSystem.md index e1b0c7a..519bd12 100644 --- a/docs/platform/llm-server/host/GNAI-002-INFRA_BaseSystem.md +++ b/docs/platform/llm-server/host/GNAI-002-INFRA_BaseSystem.md @@ -17,6 +17,7 @@ | 2026-09-18 | 1.3 | spektr | GPU-Abnahme auf nachweisbare Geräteerkennung begrenzt; Firmwarepaket präzisiert. | | 2026-09-19 | 1.4 | spektr | Getrennte SSH-Zugänge für `ghost` und `ghost-agent` als erfolgreich getestet übernommen. | | 2026-09-22 | 1.5 | Codex | In die hostbezogene Struktur `platform/ghostnet-ai/` verschoben und Geltungsbereich präzisiert. | +| 2026-10-31 | 1.6 | spektr / Mistral Vibe | Betreiberentscheidung IPv4-only: IPv6 wird auf dem Host über die `host_baseline`-Rolle systematisch deaktiviert (Abschnitt 6); Anlass war der hängende Modell-Download am 31.10.2026 durch den Halbdual-Stack-Zustand. | --- @@ -157,3 +158,46 @@ Vor der Übergabe an das KI-Team (Phase 2) muss das System folgende Punkte beste ## 5. Lesender Teilabgleich des bestehenden Hosts am 18.09.2026 Per SSH wurden Debian 13.6, Hostname `ghostnet-ki`, Intel Core i5-13600K, 62 GiB nutzbarer RAM und zwei AMD Navi-21-GPUs mit PCI-ID `1002:73bf` beobachtet. `lsblk` zeigt eine 1,8-TiB-NVMe mit EFI-, ext4-Root- und Swap-Partition; `df -h /` zeigte 1,8 TiB Dateisystemgröße. `lspci` bestätigt die Gerätefamilie, aber weder das genaue Kartenmodell noch ROCm-Funktion und ReBAR-Leistung. BIOS-Version, XMP, Firmwarestand und Restore wurden nicht geprüft. + +--- + +## 6. Betreiberentscheidung: IPv6 auf dem Host deaktiviert (31.10.2026) + +**Entscheidung (Betreiber, 31.10.2026):** Der KI-Host wird bewusst IPv4-only +betrieben. IPv6 wird über die `host_baseline`-Rolle systematisch deaktiviert +(`/etc/sysctl.d/99-ghostnet-disable-ipv6.conf` mit +`net.ipv6.conf.all.disable_ipv6 = 1` und +`net.ipv6.conf.default.disable_ipv6 = 1`). + +**Anlass:** Während der ersten Modell-Downloads am 31.10.2026 hing der +Ansible-`get_url`-Download auf dem Host minutenlang ohne TCP-Verbindung und +ohne Datenträgeraktivität, obwohl dieselbe URL vom Host aus mit `wget -4` in +Sekunden erreichbar war. Messungen dazu: + +| Prüfung | Ergebnis | +| --- | --- | +| Host-Netzwerk | nur link-lokales IPv6 auf `enp8s0`; keine globale Adresse, keine IPv6-Default-Route | +| Resolver des Hosts | Fritzbox `10.0.0.1` und `1.1.1.1` (der Host nutzt Pi-hole zu diesem Zeitpunkt nicht) | +| AAAA-Antworten | Fritzbox, 1.1.1.1 und Pi-hole `10.0.0.14` liefern alle AAAA-Records; AAAA wird nirgends gefiltert | + +Programme mit IPv6-Präferenz (wie Pythons `urllib`) laufen bei dieser +Inkonsistenz aus AAAA-Antworten ohne nutzbare IPv6-Route in Timeouts oder +Hänger; die 302-Weiterleitung der Hugging-Face-Downloads zum +CloudFront-CDN traf genau darauf. Die Download-Rolle `model_store` erzwingt +daher zusätzlich IPv4 (`wget -4`) und verifiziert die SHA-256 explizit. + +**Einordnung:** Die Entscheidung beendet diesen Halbdual-Stack-Zustand +zugunsten eines kohärenten IPv4-only-Betriebs. Sie erhält die dokumentierte +Sicherheitseigenschaft „keine IPv6-Exposition" (GNAI-007 Offenpunkt O7, +teilbestätigt am 31.10.2026) und entspricht dem bestehenden +LAN-Betriebsmodell. Eine echte Dual-Stack-Einführung (Fritzbox-Präfix, +Router Advertisements, Firewall-Regeln, neue Perimeter-Abnahme) bleibt ein +eigener Netzwerk-Sprint und ist für den KI-Stack nicht erforderlich. Ein +optionales Folgeziel ist die Umstellung des Host-Resolvers auf Pi-hole, um +Split-DNS konsistent auch auf dem Host zu nutzen; das ist ein separater +kleiner Schritt und kein Bestandteil dieser Entscheidung. + +**Umsetzung:** `llm-server/host/ansible/roles/host_baseline` verwaltet die +Drop-in-Datei, wendet sie bei Änderung über `sysctl --system` an und +verifiziert den Laufzeitwert `net.ipv6.conf.all.disable_ipv6 = 1`. Ein +zweiter Lauf muss `changed=0` melden. diff --git a/llm-server/host/ansible/roles/host_baseline/README.md b/llm-server/host/ansible/roles/host_baseline/README.md index 460e1ca..4d0edc3 100644 --- a/llm-server/host/ansible/roles/host_baseline/README.md +++ b/llm-server/host/ansible/roles/host_baseline/README.md @@ -10,6 +10,14 @@ Its first version deliberately has a narrow scope: - require at least 100 GiB free on the root filesystem; and - create root-owned parent directories and the future shared-data root. +Since 31.10.2026 the role also disables IPv6 on the host +(`host_baseline_disable_ipv6`, default `true`) via +`/etc/sysctl.d/99-ghostnet-disable-ipv6.conf`, applies it with +`sysctl --system` when changed and verifies the runtime value. Rationale: +the LAN has no usable global IPv6 path while all resolvers return AAAA +records, which made dual-stack-preferring applications hang on outbound +connections (operator decision, GNAI-002 section 6). + Environment-specific directory ownership is managed by `workload_accounts` so the shared baseline and workload identities do not compete over permissions. diff --git a/llm-server/host/ansible/roles/host_baseline/defaults/main.yml b/llm-server/host/ansible/roles/host_baseline/defaults/main.yml index abd4a35..2651967 100644 --- a/llm-server/host/ansible/roles/host_baseline/defaults/main.yml +++ b/llm-server/host/ansible/roles/host_baseline/defaults/main.yml @@ -21,3 +21,12 @@ host_baseline_directories: # (AGENTS.md) — with (root) alone, runas to other users would prompt. host_baseline_deploy_sudoers_path: /etc/sudoers.d/90-ansible-deploy host_baseline_deploy_sudoers_content: "ansible-deploy ALL=(ALL:ALL) NOPASSWD: ALL\n" + +# IPv6 is deliberately disabled on this host (operator decision 2026-10-31, +# GNAI-002 section 6): the LAN provides no usable global IPv6 path, but all +# available resolvers still return AAAA records. Dual-stack-preferring +# applications then hang on outbound connections (observed 2026-10-31 during +# the first model downloads). A coherent IPv4-only host keeps the documented +# "no IPv6 exposure" property (GNAI-007 open item O7) and makes name +# resolution and connect behaviour deterministic. +host_baseline_disable_ipv6: true diff --git a/llm-server/host/ansible/roles/host_baseline/tasks/main.yml b/llm-server/host/ansible/roles/host_baseline/tasks/main.yml index 8ce526c..99c1853 100644 --- a/llm-server/host/ansible/roles/host_baseline/tasks/main.yml +++ b/llm-server/host/ansible/roles/host_baseline/tasks/main.yml @@ -66,3 +66,50 @@ group: root mode: "0440" validate: /usr/sbin/visudo -cf %s + +- name: Configure IPv6 as disabled on the host + ansible.builtin.copy: + content: | + # Managed by the host_baseline role (GNAI-002 decision 2026-10-31). + net.ipv6.conf.all.disable_ipv6 = 1 + net.ipv6.conf.default.disable_ipv6 = 1 + dest: /etc/sysctl.d/99-ghostnet-disable-ipv6.conf + owner: root + group: root + mode: "0644" + when: host_baseline_disable_ipv6 + register: host_baseline_ipv6_sysctl_file + +- name: Apply the IPv6 sysctl drop-in + ansible.builtin.command: + argv: + - /usr/sbin/sysctl + - --system + changed_when: false + when: + - host_baseline_disable_ipv6 + - host_baseline_ipv6_sysctl_file is changed + - not ansible_check_mode + +- name: Read the runtime IPv6 state + ansible.builtin.command: + argv: + - /usr/sbin/sysctl + - -n + - net.ipv6.conf.all.disable_ipv6 + changed_when: false + check_mode: false + when: host_baseline_disable_ipv6 + register: host_baseline_ipv6_state + +- name: Verify IPv6 is disabled at runtime + ansible.builtin.assert: + that: + - host_baseline_ipv6_state.stdout | trim == "1" + fail_msg: >- + IPv6 is still enabled despite the sysctl drop-in; outbound + dual-stack connections may hang (GNAI-002 section 6). + success_msg: IPv6 is disabled on the host (net.ipv6.conf.all.disable_ipv6 = 1). + when: + - host_baseline_disable_ipv6 + - not ansible_check_mode diff --git a/llm-server/llm/ansible/roles/model_store/README.md b/llm-server/llm/ansible/roles/model_store/README.md index 196c141..f69e08c 100644 --- a/llm-server/llm/ansible/roles/model_store/README.md +++ b/llm-server/llm/ansible/roles/model_store/README.md @@ -23,8 +23,11 @@ profile directory read-only. - Manifest is validated (unique profiles, 64-hex checksums, HTTPS URLs). - Directories are `root:root 0755`, artifact files `root:root 0644`; a `SOURCE.yml` provenance record is rendered per directory. -- Downloads go to a hidden `.part` file with `get_url` checksum verification - and are moved into place only after the checksum passed (atomic). +- Downloads use `wget -4` into a hidden `.part` file, followed by explicit + SHA-256 verification; only after the checksum passed is the file moved + into place (atomic). IPv4 is forced because the host resolves AAAA first + but has no global IPv6 route (dual-stack CDN redirects would hang; + observed 31.10.2026). - An existing file with a mismatching checksum fails the run; it is never overwritten or used. - Check mode downloads nothing; already-present files are still verified. diff --git a/llm-server/llm/ansible/roles/model_store/defaults/main.yml b/llm-server/llm/ansible/roles/model_store/defaults/main.yml index c24b563..2c6a1ed 100644 --- a/llm-server/llm/ansible/roles/model_store/defaults/main.yml +++ b/llm-server/llm/ansible/roles/model_store/defaults/main.yml @@ -17,9 +17,9 @@ model_store_artifacts: directory: swift-qwen3.8-27b file: Swift-Qwen3.8-27B-Q6_K.gguf sha256: 7f4de8abd5446c08b0f975a1b38e43d02639f4ae9ecdd2ddfd5c5b0f612bda59 - url: https://huggingface.co/ukisai/Swift-Qwen3.8-27B-GGUF/resolve/eb0e3a7dc70643c2ab920f5133750d02c20849ff/Swift-Qwen3.8-27B-Q6_K.gguf + url: https://huggingface.co/ukisai/Swift-Qwen3.8-27B-GGUF/resolve/98a6100719e3aafa186041089b71c7b48390dbe1/Swift-Qwen3.8-27B-Q6_K.gguf repository: ukisai/Swift-Qwen3.8-27B-GGUF - revision: eb0e3a7dc70643c2ab920f5133750d02c20849ff + revision: 98a6100719e3aafa186041089b71c7b48390dbe1 origin: Swift efficiency finetune of Qwen3.8-27B (UkisAI), Q6_K license: >- Qwen base model Apache-2.0; Swift changes under Swift Open License 1.0 @@ -40,9 +40,9 @@ model_store_vision_artifact: directory: swift-qwen3.8-27b file: mmproj-Swift-Qwen3.8-27B-F16.gguf sha256: daa1116c9422fa390cc8688495da0e91781f92841dfc3b31a378ff252571745a - url: https://huggingface.co/ukisai/Swift-Qwen3.8-27B-GGUF/resolve/eb0e3a7dc70643c2ab920f5133750d02c20849ff/mmproj-Swift-Qwen3.8-27B-F16.gguf + url: https://huggingface.co/ukisai/Swift-Qwen3.8-27B-GGUF/resolve/98a6100719e3aafa186041089b71c7b48390dbe1/mmproj-Swift-Qwen3.8-27B-F16.gguf repository: ukisai/Swift-Qwen3.8-27B-GGUF - revision: eb0e3a7dc70643c2ab920f5133750d02c20849ff + revision: 98a6100719e3aafa186041089b71c7b48390dbe1 origin: Swift vision projector, F16 (vision stage only) license: >- Qwen base model Apache-2.0; Swift changes under Swift Open License 1.0. diff --git a/llm-server/llm/ansible/roles/model_store/tasks/main.yml b/llm-server/llm/ansible/roles/model_store/tasks/main.yml index de2185f..c002f98 100644 --- a/llm-server/llm/ansible/roles/model_store/tasks/main.yml +++ b/llm-server/llm/ansible/roles/model_store/tasks/main.yml @@ -121,15 +121,38 @@ - models - service +- name: Require the download utility + ansible.builtin.stat: + path: /usr/bin/wget + register: model_store_wget_tool + tags: + - models + - service + +- name: Verify the download utility is executable + ansible.builtin.assert: + that: + - model_store_wget_tool.stat.exists + - model_store_wget_tool.stat.executable + fail_msg: /usr/bin/wget is required to download model artifacts. + tags: + - models + - service + - name: Download missing model artifacts atomically - ansible.builtin.get_url: - url: "{{ item.item.url }}" - dest: "{{ model_store_root }}/{{ item.item.directory }}/.{{ item.item.file }}.part" - checksum: "sha256:{{ item.item.sha256 }}" - mode: "0644" - owner: root - group: root - timeout: 120 + # wget with forced IPv4: the host currently resolves AAAA first but has no + # global IPv6 route, which blackholes redirects to dual-stack CDNs + # (observed 31.10.2026 with the Hugging Face xet/CloudFront bridge). + # The SHA-256 is verified explicitly after the download, before the move. + ansible.builtin.command: + argv: + - /usr/bin/wget + - -4 + - --timeout=60 + - --tries=3 + - -O + - "{{ model_store_root }}/{{ item.item.directory }}/.{{ item.item.file }}.part" + - "{{ item.item.url }}" loop: "{{ model_store_present.results | selectattr('stat.exists', 'equalto', false) | list }}" loop_control: label: "{{ item.item.profile }}" @@ -138,6 +161,23 @@ tags: - models +- name: Verify the checksum of freshly downloaded artifacts + ansible.builtin.command: + argv: + - /usr/bin/sha256sum + - "{{ model_store_root }}/{{ item.item.directory }}/.{{ item.item.file }}.part" + changed_when: false + check_mode: false + loop: "{{ model_store_present.results | selectattr('stat.exists', 'equalto', false) | list }}" + loop_control: + label: "{{ item.item.profile }}" + when: + - not ansible_check_mode + register: model_store_fresh_checksums + failed_when: model_store_fresh_checksums.stdout.split()[0] != item.item.sha256 + tags: + - models + - name: Move verified downloads to their final model path ansible.builtin.command: argv: