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
|
# v0.5.41-beta — Netwerk toevoegen liep vast tijdens het typen
|
||||||
|
|
||||||
Een netwerk toevoegen gaf een JSON-parsefout in de browser.
|
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"])
|
@app.route("/api/networks", methods=["POST"])
|
||||||
def api_networks_add():
|
def api_networks_add():
|
||||||
d = request.json or {}
|
d = request.json or {}
|
||||||
net = {"name": (d.get("name") or "").strip(),
|
# Normaliseren vóór opslaan: wat we bewaren moet gelijk zijn aan wat Docker
|
||||||
"driver": d.get("driver") or "macvlan",
|
# heeft gekregen, anders wijkt de config af van het echte netwerk.
|
||||||
"parent": (d.get("parent") or "").strip(),
|
net = networks.normalize({
|
||||||
"subnet": (d.get("subnet") or "").strip(),
|
"name": (d.get("name") or "").strip(),
|
||||||
"gateway": (d.get("gateway") or "").strip(),
|
"driver": d.get("driver") or "macvlan",
|
||||||
"ip_range": (d.get("ip_range") or "").strip()}
|
"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)
|
probleem = networks.validate(net)
|
||||||
if probleem:
|
if probleem:
|
||||||
return jsonify(ok=False, msg=probleem), 400
|
return jsonify(ok=False, msg=probleem), 400
|
||||||
|
|
|
||||||
|
|
@ -290,6 +290,18 @@ def review(net: dict) -> dict:
|
||||||
waarschuwingen: list[str] = []
|
waarschuwingen: list[str] = []
|
||||||
info: dict = {}
|
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)
|
blokkerend = validate(net)
|
||||||
if blokkerend:
|
if blokkerend:
|
||||||
fouten.append(blokkerend)
|
fouten.append(blokkerend)
|
||||||
|
|
@ -378,8 +390,30 @@ def _bruikbaar_bereik(net, gateway: str = ""):
|
||||||
return eerste, laatste, max(aantal, 0)
|
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]:
|
def create(net: dict) -> tuple[bool, str]:
|
||||||
"""Maak het netwerk aan bij de Docker-daemon (idempotent)."""
|
"""Maak het netwerk aan bij de Docker-daemon (idempotent)."""
|
||||||
|
net = normalize(net)
|
||||||
probleem = validate(net)
|
probleem = validate(net)
|
||||||
if probleem:
|
if probleem:
|
||||||
return False, probleem
|
return False, probleem
|
||||||
|
|
|
||||||
|
|
@ -991,7 +991,7 @@ tailwind.config = {
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-foot">
|
<div class="modal-foot">
|
||||||
<button class="btn" @click="modal=null" x-text="t('cancel')">Annuleren</button>
|
<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>
|
<span class="mdi mdi-plus"></span> <span x-text="t('add')">Aanmaken</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1308,7 +1308,7 @@ function app() {
|
||||||
|
|
||||||
// Macvlan/ipvlan-netwerken waarmee stacks een eigen IP krijgen
|
// Macvlan/ipvlan-netwerken waarmee stacks een eigen IP krijgen
|
||||||
netList: [], netIfaces: [], netDetected: [], netDetectError: '',
|
netList: [], netIfaces: [], netDetected: [], netDetectError: '',
|
||||||
netReview: {}, netReviewTimer: null,
|
netReview: {}, netReviewTimer: null, netReviewSeq: 0,
|
||||||
newNet: { name: '', driver: 'macvlan', parent: '', subnet: '', gateway: '', ip_range: '' },
|
newNet: { name: '', driver: 'macvlan', parent: '', subnet: '', gateway: '', ip_range: '' },
|
||||||
|
|
||||||
docker: { running: 0, containers: 0, images: 0 },
|
docker: { running: 0, containers: 0, images: 0 },
|
||||||
|
|
@ -1768,7 +1768,13 @@ function app() {
|
||||||
if (!this.newNet.subnet || !this.newNet.parent || !this.newNet.name) {
|
if (!this.newNet.subnet || !this.newNet.parent || !this.newNet.name) {
|
||||||
this.netReview = {}; return;
|
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);
|
}, 350);
|
||||||
},
|
},
|
||||||
// Vul automatisch het eerstvolgende vrije adres in als je een netwerk kiest.
|
// 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 || '') : '';
|
this.install.ip = n ? (n.next_ip || '') : '';
|
||||||
},
|
},
|
||||||
async addNetwork() {
|
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);
|
const d = await this.rpc('/api/networks', this.newNet);
|
||||||
this.toast(d.msg || this.t(d.ok ? 'add' : 'error'), d.ok ? 'ok' : 'er');
|
this.toast(d.msg || this.t(d.ok ? 'add' : 'error'), d.ok ? 'ok' : 'er');
|
||||||
if (d.ok) {
|
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
|
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():
|
def test_review_geeft_blokkerende_validatiefout_door():
|
||||||
r = networks.review({**BASIS, "subnet": "onzin"})
|
r = networks.review({**BASIS, "subnet": "onzin"})
|
||||||
assert r["ok"] is False
|
assert r["ok"] is False
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue