From b30f83866f6c261f02160b4d8da449e1b6824f9a Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 27 Jul 2026 00:13:53 +0200 Subject: [PATCH] v0.5.43-beta - van-tot omrekenen naar een CIDR-blok "Ik wil van 10.0.20.200 tot .254" bestaat niet als CIDR-notatie: 55 adressen, terwijl Docker alleen uitgelijnde blokken van een macht van twee accepteert. Dat is niet iets wat een gebruiker zelf hoort uit te rekenen. - Nieuwe range_options(): vertaalt een gewenst begin- en eindadres naar twee bruikbare blokken. "Ruim" is het kleinste blok dat de hele wens omvat, "krap" het grootste blok dat er volledig binnen valt. Met per optie hoeveel adressen erbij komen of ontbreken. - Endpoint POST /api/networks/range en een uitklapbare van-tot-hulp onder het IP-range-veld; klikken op een optie vult het veld. - Voor 10.0.20.200-254 levert dat 10.0.20.192/26 op: 62 adressen, .193 t/m .254. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb --- CHANGELOG.md | 18 +++++++++ VERSION | 2 +- server-up/app.py | 8 ++++ server-up/core/networks.py | 68 ++++++++++++++++++++++++++++++++++ server-up/templates/index.html | 49 ++++++++++++++++++++++++ server-up/translations/en.json | 9 ++++- server-up/translations/nl.json | 9 ++++- tests/test_network_detect.py | 59 +++++++++++++++++++++++++++++ 8 files changed, 219 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9af3d2..9266b76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +# v0.5.43-beta — Van-tot omrekenen naar een CIDR-blok + +"Ik wil van 10.0.20.200 tot .254" is een volstrekt redelijke wens, maar bestaat +niet als CIDR-notatie: dat zijn 55 adressen, en Docker accepteert alleen +uitgelijnde blokken van 2, 4, 8, 16 … adressen. + +Onder het IP-range-veld zit nu een **van-tot-hulp**. Vul de eerste en laatste +gewenste adressen in en Server Up rekent uit welke blokken in de buurt komen: + +* **dekt alles** — het kleinste blok dat je hele wens omvat (pakt er onderaan + wat bij) +* **past binnen** — het grootste blok dat volledig binnen je wens valt + +Bij t/m levert dat op: 62 adressen, t/m +. Klikken vult het veld in. + +--- + # v0.5.42-beta — Netwerk toevoegen: twee blokkades weg Een netwerk aanmaken lukte niet, met wisselend gedrag: soms een foutmelding dat diff --git a/VERSION b/VERSION index 5b27ae9..42de7df 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.42-beta +0.5.43-beta diff --git a/server-up/app.py b/server-up/app.py index 5b2af01..2c9aaab 100644 --- a/server-up/app.py +++ b/server-up/app.py @@ -1402,6 +1402,14 @@ def api_networks(): drivers=list(networks.DRIVERS)) +@app.route("/api/networks/range", methods=["POST"]) +def api_networks_range(): + """Zet "van adres X tot adres Y" om in bruikbare CIDR-blokken.""" + d = request.json or {} + return jsonify(networks.range_options( + d.get("first", ""), d.get("last", ""), d.get("subnet", ""))) + + @app.route("/api/networks/review", methods=["POST"]) def api_networks_review(): """Beoordeel een definitie zonder iets aan te maken (live in het formulier).""" diff --git a/server-up/core/networks.py b/server-up/core/networks.py index 295dbea..5d5f640 100644 --- a/server-up/core/networks.py +++ b/server-up/core/networks.py @@ -390,6 +390,74 @@ def _bruikbaar_bereik(net, gateway: str = ""): return eerste, laatste, max(aantal, 0) +def range_options(eerste: str, laatste: str, subnet: str = "") -> dict: + """Vertaal "ik wil van X tot Y" naar bruikbare CIDR-blokken. + + Docker accepteert één `--ip-range`, en dat moet een uitgelijnd blok zijn met + een macht van twee als grootte. Een wens als `.200 t/m .254` (55 adressen) + past daar niet in. Deze functie geeft daarom twee bruikbare antwoorden: + + * **ruim** — het kleinste blok dat je hele wens omvat; pakt er wat adressen + onderaan bij + * **krap** — het grootste blok dat volledig bínnen je wens valt; nooit te + veel, maar mogelijk minder dan gevraagd + + Bijna altijd is "ruim" wat je wilt, mits je DHCP-bereik eronder blijft. + """ + try: + f = ipaddress.ip_address(eerste.strip()) + l = ipaddress.ip_address(laatste.strip()) + except (ValueError, AttributeError): + return {"ok": False, "msg": "Vul twee geldige IP-adressen in.", "options": []} + if f.version != 4 or l.version != 4: + return {"ok": False, "msg": "Alleen IPv4 wordt ondersteund.", "options": []} + if int(l) < int(f): + f, l = l, f + + net = None + if subnet: + try: + net = ipaddress.ip_network(subnet, strict=False) + if f not in net or l not in net: + return {"ok": False, "options": [], + "msg": f"Die adressen liggen niet allebei binnen {net}."} + except ValueError: + net = None + + gewenst = int(l) - int(f) + 1 + opties = [] + + # Ruim: opklimmen tot beide adressen erin passen. + omvattend = ipaddress.ip_network(f"{f}/32") + for _ in range(32): + if l in omvattend: + break + omvattend = omvattend.supernet() + if l in omvattend and (net is None or omvattend.subnet_of(net)): + opties.append(_range_optie("ruim", omvattend, gewenst)) + + # Krap: het grootste blok uit de exacte opdeling van het bereik. + try: + stukken = list(ipaddress.summarize_address_range(f, l)) + except Exception: + stukken = [] + if stukken: + grootste = max(stukken, key=lambda n: n.num_addresses) + if not opties or str(grootste) != opties[0]["cidr"]: + opties.append(_range_optie("krap", grootste, gewenst)) + + return {"ok": bool(opties), "options": opties, "wanted": gewenst, + "msg": "" if opties else "Geen passend blok gevonden."} + + +def _range_optie(soort: str, blok, gewenst: int) -> dict: + eerste, laatste, aantal = _bruikbaar_bereik(blok) + return {"kind": soort, "cidr": str(blok), "first": str(eerste), + "last": str(laatste), "count": aantal, + "extra": max(blok.num_addresses - gewenst, 0), + "short": max(gewenst - blok.num_addresses, 0)} + + def normalize(net: dict) -> dict: """Zet subnet en ip_range om naar hun canonieke vorm. diff --git a/server-up/templates/index.html b/server-up/templates/index.html index 128f630..2699b42 100644 --- a/server-up/templates/index.html +++ b/server-up/templates/index.html @@ -972,6 +972,41 @@ tailwind.config = {

+ + +
+ + + Ik wil een bepaald bereik + +
+
+ van + + tot + +
+

+

+ +
+
@@ -1309,6 +1344,7 @@ function app() { // Macvlan/ipvlan-netwerken waarmee stacks een eigen IP krijgen netList: [], netIfaces: [], netDetected: [], netDetectError: '', netReview: {}, netReviewTimer: null, netReviewSeq: 0, + rangeCalc: { first: '', last: '', options: [], msg: '' }, rangeCalcTimer: null, newNet: { name: '', driver: 'macvlan', parent: '', subnet: '', gateway: '', ip_range: '' }, docker: { running: 0, containers: 0, images: 0 }, @@ -1760,6 +1796,19 @@ function app() { } this.reviewNet(); }, + // 'Van adres X tot Y' omrekenen naar een CIDR-blok dat Docker accepteert. + calcRange() { + if (this.rangeCalcTimer) clearTimeout(this.rangeCalcTimer); + this.rangeCalcTimer = setTimeout(async () => { + const { first, last } = this.rangeCalc; + if (!first || !last) { this.rangeCalc.options = []; this.rangeCalc.msg = ''; return; } + const d = await this.rpc('/api/networks/range', + { first, last, subnet: this.newNet.subnet }); + this.rangeCalc.options = d.options || []; + this.rangeCalc.msg = d.ok ? '' : (d.msg || ''); + }, 350); + }, + // Live meelezen terwijl je typt; even wachten zodat niet elke toetsaanslag // een verzoek stuurt. reviewNet() { diff --git a/server-up/translations/en.json b/server-up/translations/en.json index cf452b9..03e39ef 100644 --- a/server-up/translations/en.json +++ b/server-up/translations/en.json @@ -232,5 +232,12 @@ "tip_example_title": "Example with a router at 192.168.1.1", "tip_example": "Subnet 192.168.1.0/24 ← same as your router\nGateway 192.168.1.1 ← your router\nIP range 192.168.1.240/28 ← .240 through .255\n\nSet your router's DHCP pool to 192.168.1.20 – 192.168.1.239 so that\nthose last sixteen addresses are reserved for your containers.", "suggest_range": "Suggest a free block", - "range_result": "{count} addresses available: {first} through {last}" + "range_result": "{count} addresses available: {first} through {last}", + "range_helper": "I want a specific range", + "range_helper_hint": "Docker works with blocks of 2, 4, 8, 16 … addresses starting on a fixed boundary. A wish like '.200 to .254' does not exist as a notation; the closest blocks are listed below.", + "from": "from", + "to": "to", + "range_ruim": "covers all", + "range_krap": "fits inside", + "range_count": "{count} addresses" } diff --git a/server-up/translations/nl.json b/server-up/translations/nl.json index 3dd3651..3601e93 100644 --- a/server-up/translations/nl.json +++ b/server-up/translations/nl.json @@ -232,5 +232,12 @@ "tip_example_title": "Voorbeeld bij een router op 192.168.1.1", "tip_example": "Subnet 192.168.1.0/24 ← hetzelfde als je router\nGateway 192.168.1.1 ← je router\nIP-range 192.168.1.240/28 ← .240 t/m .255\n\nStel je DHCP-bereik in je router in op 192.168.1.20 – 192.168.1.239,\nzodat die laatste zestien adressen alleen voor je containers zijn.", "suggest_range": "Stel een vrij blok voor", - "range_result": "{count} adressen beschikbaar: {first} tot en met {last}" + "range_result": "{count} adressen beschikbaar: {first} tot en met {last}", + "range_helper": "Ik wil een bepaald bereik", + "range_helper_hint": "Docker werkt met blokken van 2, 4, 8, 16 … adressen die op een vaste grens beginnen. Een wens als '.200 tot .254' bestaat niet als notatie; hieronder staan de dichtstbijzijnde blokken.", + "from": "van", + "to": "tot", + "range_ruim": "dekt alles", + "range_krap": "past binnen", + "range_count": "{count} adressen" } diff --git a/tests/test_network_detect.py b/tests/test_network_detect.py index d44e256..8e38041 100644 --- a/tests/test_network_detect.py +++ b/tests/test_network_detect.py @@ -290,3 +290,62 @@ def test_detectiefout_wordt_doorgegeven(client, monkeypatch): d = client.get("/api/networks").get_json() assert d["detect_error"] == "docker niet bereikbaar" assert d["detected"] == [] + + +# ── Van-tot omrekenen ──────────────────────────────────────────────────────── + +def test_range_options_voor_200_tot_254(): + """De vraag uit de praktijk: 55 adressen, wat geen geldig CIDR-blok is.""" + r = networks.range_options("10.0.20.200", "10.0.20.254", "10.0.20.0/24") + assert r["ok"] is True + ruim = next(o for o in r["options"] if o["kind"] == "ruim") + assert ruim["cidr"] == "10.0.20.192/26" + assert ruim["count"] == 62 + assert (ruim["first"], ruim["last"]) == ("10.0.20.193", "10.0.20.254") + krap = next(o for o in r["options"] if o["kind"] == "krap") + assert krap["cidr"] == "10.0.20.208/28" + + +def test_range_options_exact_passend_blok(): + """Valt de wens toevallig samen met een blok, dan is er maar één antwoord.""" + r = networks.range_options("10.0.20.240", "10.0.20.255", "10.0.20.0/24") + assert [o["cidr"] for o in r["options"]] == ["10.0.20.240/28"] + + +def test_range_options_draait_omgekeerde_invoer_om(): + r = networks.range_options("10.0.20.254", "10.0.20.200") + assert r["ok"] is True + assert r["options"][0]["cidr"] == "10.0.20.192/26" + + +def test_range_options_buiten_het_subnet(): + r = networks.range_options("192.168.9.10", "192.168.9.20", "10.0.20.0/24") + assert r["ok"] is False + assert "niet allebei" in r["msg"] + + +@pytest.mark.parametrize("eerste,laatste", [ + ("geen-ip", "10.0.20.254"), ("10.0.20.200", ""), ("", ""), +]) +def test_range_options_ongeldige_invoer(eerste, laatste): + r = networks.range_options(eerste, laatste) + assert r["ok"] is False + assert r["options"] == [] + + +def test_range_options_meldt_hoeveel_er_bij_komt(): + r = networks.range_options("10.0.20.200", "10.0.20.254", "10.0.20.0/24") + ruim = next(o for o in r["options"] if o["kind"] == "ruim") + assert ruim["extra"] == 9 # 64 adressen in het blok, 55 gevraagd + + +def test_range_endpoint(client): + from conftest import login + csrf = login(client) + r = client.post("/api/networks/range", + json={"first": "10.0.20.200", "last": "10.0.20.254", + "subnet": "10.0.20.0/24"}, + headers={"X-CSRF-Token": csrf}) + d = r.get_json() + assert d["ok"] is True + assert d["options"][0]["cidr"] == "10.0.20.192/26"