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"