LLM-Server: IPv6 deaktiviert (host_baseline) + Swift-Repin und wget-4-Download nach Live-Befunden

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 <vibe@mistral.ai>
This commit is contained in:
2026-11-01 17:32:17 +01:00
parent d53b2400c9
commit 89787b0e23
8 changed files with 169 additions and 17 deletions
@@ -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.
@@ -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.
@@ -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.
@@ -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
@@ -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
@@ -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.
@@ -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.
@@ -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: