v0.5.42-beta - netwerk toevoegen: twee blokkades weg
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m3s
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m3s
Gemeld met twee screenshots: een foutmelding dat de IP-range buiten het subnet
lag terwijl 10.0.20.240/28 gewoon binnen 10.0.20.0/24 valt, en een geval zonder
melding waarbij aanmaken toch niet lukte. Twee losse oorzaken.
1. Verouderd beoordelingsantwoord
De live beoordeling gaat af bij elke toetsaanslag. Tijdens het typen van
/28 kom je langs /2, en 10.0.20.240/2 normaliseert naar 0.0.0.0/2 - buiten
het subnet. Kwam dat antwoord na het goede binnen, dan bleef die fout staan
en bleef de knop uit.
- Volgnummer op de verzoeken; alleen het antwoord bij de laatste vraag telt.
- Knop niet meer uitgeschakeld op clientstatus; de server beslist bij het
opslaan, zodat een verouderde beoordeling nooit blokkeert.
2. CIDR met hostbits
10.0.20.200/28 leest Python met strict=False als 10.0.20.192/28, maar Docker
weigert het met "has host bits set". We valideerden soepel en stuurden de
ruwe tekst door, dus de interface keurde iets goed dat bij het aanmaken
alsnog strandde.
- normalize() zet subnet en ip_range om naar canonieke vorm voor ze naar
Docker gaan en voor ze worden opgeslagen.
- Waarschuwing die toont wat er van de invoer gemaakt is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
This commit is contained in:
parent
923c8dabee
commit
cbb232d63f
6 changed files with 144 additions and 10 deletions
31
CHANGELOG.md
31
CHANGELOG.md
|
|
@ -1,3 +1,34 @@
|
|||
# v0.5.42-beta — Netwerk toevoegen: twee blokkades weg
|
||||
|
||||
Een netwerk aanmaken lukte niet, met wisselend gedrag: soms een foutmelding dat
|
||||
de IP-range buiten het subnet lag terwijl dat aantoonbaar niet zo was, soms geen
|
||||
melding maar toch geen resultaat. Het bleken twee losse oorzaken.
|
||||
|
||||
## De foutmelding die niet klopte
|
||||
|
||||
De beoordeling gaat af bij elke toetsaanslag. Tijdens het intypen van `/28` ga
|
||||
je langs `/2`, en `10.0.20.240/2` normaliseert naar `0.0.0.0/2` — inderdaad
|
||||
buiten je subnet. Kwam dat antwoord ná het antwoord voor de volledige waarde
|
||||
binnen, dan bleef die foutmelding staan en bleef de knop uitgeschakeld.
|
||||
|
||||
Er zit nu een volgnummer op: alleen het antwoord bij de laatste vraag telt. En
|
||||
de knop wordt niet meer uitgeschakeld op de clientstatus — bij het opslaan
|
||||
beslist de server, zodat een verouderde beoordeling je nooit kan blokkeren.
|
||||
|
||||
## De range die Docker weigerde
|
||||
|
||||
`10.0.20.200/28` heeft hostbits gezet. Python leest dat soepel als
|
||||
`10.0.20.192/28`, maar Docker is streng en antwoordt *"has host bits set"*. Wij
|
||||
valideerden soepel en stuurden vervolgens de ruwe tekst door, dus de interface
|
||||
keurde iets goed dat bij het aanmaken alsnog strandde.
|
||||
|
||||
Subnet en IP-range worden nu omgezet naar hun canonieke vorm voordat ze naar
|
||||
Docker gaan én voordat ze worden opgeslagen, met een waarschuwing die laat zien
|
||||
wat er van je invoer gemaakt is. `.200/28` is trouwens een logische invoer — je
|
||||
bedoelt "vanaf .200" — maar een /28 begint nu eenmaal op een veelvoud van 16.
|
||||
|
||||
---
|
||||
|
||||
# v0.5.41-beta — Netwerk toevoegen liep vast tijdens het typen
|
||||
|
||||
Een netwerk toevoegen gaf een JSON-parsefout in de browser.
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.5.41-beta
|
||||
0.5.42-beta
|
||||
|
|
|
|||
|
|
@ -1419,12 +1419,15 @@ def api_networks_review():
|
|||
@app.route("/api/networks", methods=["POST"])
|
||||
def api_networks_add():
|
||||
d = request.json or {}
|
||||
net = {"name": (d.get("name") or "").strip(),
|
||||
"driver": d.get("driver") or "macvlan",
|
||||
"parent": (d.get("parent") or "").strip(),
|
||||
"subnet": (d.get("subnet") or "").strip(),
|
||||
"gateway": (d.get("gateway") or "").strip(),
|
||||
"ip_range": (d.get("ip_range") or "").strip()}
|
||||
# Normaliseren vóór opslaan: wat we bewaren moet gelijk zijn aan wat Docker
|
||||
# heeft gekregen, anders wijkt de config af van het echte netwerk.
|
||||
net = networks.normalize({
|
||||
"name": (d.get("name") or "").strip(),
|
||||
"driver": d.get("driver") or "macvlan",
|
||||
"parent": (d.get("parent") or "").strip(),
|
||||
"subnet": (d.get("subnet") or "").strip(),
|
||||
"gateway": (d.get("gateway") or "").strip(),
|
||||
"ip_range": (d.get("ip_range") or "").strip()})
|
||||
probleem = networks.validate(net)
|
||||
if probleem:
|
||||
return jsonify(ok=False, msg=probleem), 400
|
||||
|
|
|
|||
|
|
@ -290,6 +290,18 @@ def review(net: dict) -> dict:
|
|||
waarschuwingen: list[str] = []
|
||||
info: dict = {}
|
||||
|
||||
ingevoerd_range = (net.get("ip_range") or "").strip()
|
||||
ingevoerd_subnet = (net.get("subnet") or "").strip()
|
||||
net = normalize(net)
|
||||
for label, ingevoerd, genormaliseerd in (
|
||||
("subnet", ingevoerd_subnet, net.get("subnet")),
|
||||
("IP-range", ingevoerd_range, net.get("ip_range"))):
|
||||
if ingevoerd and genormaliseerd and ingevoerd != genormaliseerd:
|
||||
waarschuwingen.append(
|
||||
f"Het {label} {ingevoerd} is niet uitgelijnd op een netwerkgrens "
|
||||
f"en wordt gelezen als {genormaliseerd}. Docker weigert de "
|
||||
"oorspronkelijke notatie, dus Server Up slaat de omgezette vorm op.")
|
||||
|
||||
blokkerend = validate(net)
|
||||
if blokkerend:
|
||||
fouten.append(blokkerend)
|
||||
|
|
@ -378,8 +390,30 @@ def _bruikbaar_bereik(net, gateway: str = ""):
|
|||
return eerste, laatste, max(aantal, 0)
|
||||
|
||||
|
||||
def normalize(net: dict) -> dict:
|
||||
"""Zet subnet en ip_range om naar hun canonieke vorm.
|
||||
|
||||
`10.0.20.200/28` is voor Python met strict=False gewoon `10.0.20.192/28`,
|
||||
maar Docker weigert het met "has host bits set". Zonder deze omzetting komt
|
||||
een adres dat de gebruiker logisch invult (het eerste adres dat hij wil
|
||||
uitdelen) niet door `docker network create`.
|
||||
"""
|
||||
schoon = dict(net)
|
||||
for sleutel in ("subnet", "ip_range"):
|
||||
waarde = (schoon.get(sleutel) or "").strip()
|
||||
if not waarde:
|
||||
continue
|
||||
try:
|
||||
schoon[sleutel] = str(ipaddress.ip_network(waarde, strict=False))
|
||||
except ValueError:
|
||||
schoon[sleutel] = waarde # laat validate() de fout melden
|
||||
schoon["gateway"] = (schoon.get("gateway") or "").strip()
|
||||
return schoon
|
||||
|
||||
|
||||
def create(net: dict) -> tuple[bool, str]:
|
||||
"""Maak het netwerk aan bij de Docker-daemon (idempotent)."""
|
||||
net = normalize(net)
|
||||
probleem = validate(net)
|
||||
if probleem:
|
||||
return False, probleem
|
||||
|
|
|
|||
|
|
@ -991,7 +991,7 @@ tailwind.config = {
|
|||
</div>
|
||||
<div class="modal-foot">
|
||||
<button class="btn" @click="modal=null" x-text="t('cancel')">Annuleren</button>
|
||||
<button class="btn btn-primary" @click="addNetwork()" :disabled="netReview.ok === false">
|
||||
<button class="btn btn-primary" @click="addNetwork()">
|
||||
<span class="mdi mdi-plus"></span> <span x-text="t('add')">Aanmaken</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -1308,7 +1308,7 @@ function app() {
|
|||
|
||||
// Macvlan/ipvlan-netwerken waarmee stacks een eigen IP krijgen
|
||||
netList: [], netIfaces: [], netDetected: [], netDetectError: '',
|
||||
netReview: {}, netReviewTimer: null,
|
||||
netReview: {}, netReviewTimer: null, netReviewSeq: 0,
|
||||
newNet: { name: '', driver: 'macvlan', parent: '', subnet: '', gateway: '', ip_range: '' },
|
||||
|
||||
docker: { running: 0, containers: 0, images: 0 },
|
||||
|
|
@ -1768,7 +1768,13 @@ function app() {
|
|||
if (!this.newNet.subnet || !this.newNet.parent || !this.newNet.name) {
|
||||
this.netReview = {}; return;
|
||||
}
|
||||
this.netReview = await this.rpc('/api/networks/review', this.newNet);
|
||||
// Volgnummer tegen antwoorden die te laat binnenkomen. Tijdens het
|
||||
// typen van '/28' ga je langs '/2', en dat is een geldig maar veel te
|
||||
// groot netwerk buiten je subnet. Kwam dat antwoord ná het goede
|
||||
// binnen, dan bleef die foutmelding staan en bleef de knop uit.
|
||||
const volgnummer = ++this.netReviewSeq;
|
||||
const antwoord = await this.rpc('/api/networks/review', this.newNet);
|
||||
if (volgnummer === this.netReviewSeq) this.netReview = antwoord;
|
||||
}, 350);
|
||||
},
|
||||
// Vul automatisch het eerstvolgende vrije adres in als je een netwerk kiest.
|
||||
|
|
@ -1777,6 +1783,8 @@ function app() {
|
|||
this.install.ip = n ? (n.next_ip || '') : '';
|
||||
},
|
||||
async addNetwork() {
|
||||
// Eerst de laatste stand laten beoordelen, zodat een fout uit een
|
||||
// afgebroken review niet blijft hangen.
|
||||
const d = await this.rpc('/api/networks', this.newNet);
|
||||
this.toast(d.msg || this.t(d.ok ? 'add' : 'error'), d.ok ? 'ok' : 'er');
|
||||
if (d.ok) {
|
||||
|
|
|
|||
|
|
@ -186,6 +186,64 @@ def test_bruikbaar_bereik_negeert_gateway_buiten_het_bereik():
|
|||
assert networks._bruikbaar_bereik(net, "192.168.1.1")[2] == 14
|
||||
|
||||
|
||||
# ── Normalisatie ─────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.parametrize("ingevoerd,verwacht", [
|
||||
("10.0.20.200/28", "10.0.20.192/28"), # hostbits gezet
|
||||
("10.0.20.240/28", "10.0.20.240/28"), # al uitgelijnd
|
||||
("192.168.1.5/24", "192.168.1.0/24"),
|
||||
])
|
||||
def test_normalize_lijnt_de_range_uit(ingevoerd, verwacht):
|
||||
"""Docker weigert een CIDR met hostbits ('has host bits set'), terwijl wij
|
||||
hem soepel inlezen. Zonder omzetting faalt `docker network create` op iets
|
||||
dat de interface wél accepteerde."""
|
||||
n = networks.normalize({**BASIS, "ip_range": ingevoerd})
|
||||
assert n["ip_range"] == verwacht
|
||||
|
||||
|
||||
def test_normalize_laat_onzin_staan_voor_validate():
|
||||
n = networks.normalize({**BASIS, "ip_range": "geen-cidr"})
|
||||
assert n["ip_range"] == "geen-cidr"
|
||||
|
||||
|
||||
def test_normalize_raakt_de_rest_niet_aan():
|
||||
n = networks.normalize(BASIS)
|
||||
assert n["name"] == "lan" and n["driver"] == "macvlan" and n["parent"] == "eth0"
|
||||
|
||||
|
||||
def test_review_waarschuwt_bij_niet_uitgelijnde_range():
|
||||
r = networks.review({**BASIS, "ip_range": "10.0.20.200/28",
|
||||
"subnet": "10.0.20.0/24", "gateway": "10.0.20.1"})
|
||||
assert r["ok"] is True
|
||||
assert any("10.0.20.192/28" in w for w in r["warnings"])
|
||||
|
||||
|
||||
def test_niet_uitgelijnde_range_uit_het_formulier_wordt_opgeslagen_als_canoniek(client, monkeypatch):
|
||||
"""Wat we bewaren moet gelijk zijn aan wat Docker heeft gekregen."""
|
||||
from conftest import login
|
||||
from core import networks as nw
|
||||
csrf = login(client)
|
||||
monkeypatch.setattr(nw, "exists", lambda naam: False)
|
||||
monkeypatch.setattr(nw, "create", lambda net: (True, "ok"))
|
||||
client.post("/api/networks",
|
||||
json={**BASIS, "subnet": "10.0.20.0/24", "gateway": "10.0.20.1",
|
||||
"ip_range": "10.0.20.200/28"},
|
||||
headers={"X-CSRF-Token": csrf})
|
||||
opgeslagen = client.get("/api/networks").get_json()["networks"]
|
||||
assert opgeslagen[0]["ip_range"] == "10.0.20.192/28"
|
||||
|
||||
|
||||
def test_scherm_uit_de_melding_valt_niet_ten_onrechte_buiten_het_subnet():
|
||||
"""De combinatie uit de foutmelding van de gebruiker: 10.0.20.240/28 in
|
||||
10.0.20.0/24 is geldig en mag geen fout geven."""
|
||||
r = networks.review({"name": "lan", "driver": "macvlan", "parent": "ens18",
|
||||
"subnet": "10.0.20.0/24", "gateway": "10.0.20.1",
|
||||
"ip_range": "10.0.20.240/28"})
|
||||
assert r["ok"] is True
|
||||
assert r["errors"] == []
|
||||
assert r["info"]["count"] == 14
|
||||
|
||||
|
||||
def test_review_geeft_blokkerende_validatiefout_door():
|
||||
r = networks.review({**BASIS, "subnet": "onzin"})
|
||||
assert r["ok"] is False
|
||||
|
|
|
|||
Loading…
Reference in a new issue