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"