diff --git a/infra/examples/junos-commit-check/README.md b/infra/examples/junos-commit-check/README.md
index 08273c4..c770e72 100644
--- a/infra/examples/junos-commit-check/README.md
+++ b/infra/examples/junos-commit-check/README.md
@@ -17,9 +17,10 @@ normalization and which do not.
## Methodology
-Single vJunos-router node. For each grammar rule that accepts `ip_prefix`
-or `ip_prefix_default_32`, we attempt to commit `192.168.1.111/24` (host
-bits set) via `commit check`. The device either rejects it or accepts it.
+Single vJunos-router node. For each grammar rule that accepts a prefix,
+we attempt to commit a prefix with host bits set via `commit check`. The
+device either rejects it or accepts it. IPv4 uses `192.168.1.111/24`;
+IPv6 uses `2001:db8::1/32`.
The `checks.yaml` uses two check types:
@@ -34,12 +35,12 @@ Each check loads config lines, runs `commit check`, and rolls back.
# On EC2 with containerlab + vJunos image:
sudo containerlab deploy -t topology.clab.yml
# Wait for health (~5 min)
-python3 run_commit_checks.py checks.yaml 172.20.20.2 admin "admin@123"
+python -m lab_builder validate topology.clab.yml --checks checks.yaml
```
## Results (vJunos 25.4R1.12)
-**Rejects host bits:**
+### IPv4: rejects host bits
- `routing-options static route`
- `routing-options aggregate route`
@@ -49,7 +50,7 @@ python3 run_commit_checks.py checks.yaml 172.20.20.2 admin "admin@123"
- `firewall filter X term T then next-ip`
- `policy-options condition X if-route-exists`
-**Accepts host bits:**
+### IPv4: accepts host bits
- `policy-options prefix-list`
- `policy-options policy-statement X from route-filter`
@@ -59,7 +60,30 @@ python3 run_commit_checks.py checks.yaml 172.20.20.2 admin "admin@123"
- `interfaces X unit Y family inet address`
- `interfaces X unit Y family inet address A vrrp-group N track route`
-**Not tested (requires vSRX):**
+### IPv6: rejects host bits
+
+- `routing-options rib inet6.0 static route`
+- `routing-options rib inet6.0 aggregate route`
+- `routing-options rib inet6.0 generate route`
+- `protocols ospf3 area X area-range`
+- `policy-options condition X if-route-exists` (table inet6.0)
+
+### IPv6: accepts host bits
+
+- `firewall family inet6 filter X term T from destination-address`
+- `firewall family inet6 filter X term T then next-ip6`
+- `policy-options prefix-list`
+- `policy-options policy-statement X from route-filter`
+- `interfaces X unit Y family inet6 address`
+- `protocols bgp group X allow`
+
+### Notable IPv4/IPv6 asymmetry
+
+Junos rejects host bits in IPv4 `firewall filter` (destination-address
+and next-ip) but accepts them in `firewall family inet6 filter`
+(destination-address and next-ip6).
+
+### Not tested (requires vSRX)
- `security nat` (pool address, match address, static-nat prefix)
- `security address-book`
diff --git a/infra/examples/junos-commit-check/checks.yaml b/infra/examples/junos-commit-check/checks.yaml
index ad035c4..03b0fbf 100644
--- a/infra/examples/junos-commit-check/checks.yaml
+++ b/infra/examples/junos-commit-check/checks.yaml
@@ -95,6 +95,85 @@ checks:
- "set policy-options condition TEST if-route-exists table inet.0"
description: "condition if-route-exists with valid prefix"
+ # --- Contexts where Junos REJECTS host bits (IPv6) ---
+
+ - type: commit_check_rejects
+ node: dut
+ config_lines:
+ - "set routing-options rib inet6.0 static route 2001:db8::1/32 reject"
+ description: "IPv6 static route with host bits set"
+
+ - type: commit_check_accepts
+ node: dut
+ config_lines:
+ - "set routing-options rib inet6.0 static route 2001:db8::/32 reject"
+ description: "IPv6 static route with valid prefix"
+
+ - type: commit_check_rejects
+ node: dut
+ config_lines:
+ - "set routing-options rib inet6.0 aggregate route 2001:db8::1/32"
+ description: "IPv6 aggregate route with host bits set"
+
+ - type: commit_check_accepts
+ node: dut
+ config_lines:
+ - "set routing-options rib inet6.0 aggregate route 2001:db8::/32"
+ description: "IPv6 aggregate route with valid prefix"
+
+ - type: commit_check_rejects
+ node: dut
+ config_lines:
+ - "set routing-options rib inet6.0 generate route 2001:db8::1/32"
+ description: "IPv6 generate route with host bits set"
+
+ - type: commit_check_accepts
+ node: dut
+ config_lines:
+ - "set routing-options rib inet6.0 generate route 2001:db8::/32"
+ description: "IPv6 generate route with valid prefix"
+
+ - type: commit_check_rejects
+ node: dut
+ config_lines:
+ - "set protocols ospf3 area 0.0.0.0 interface lo0.0"
+ - "set protocols ospf3 area 0.0.0.0 area-range 2001:db8::1/32"
+ description: "OSPFv3 area-range with host bits set"
+
+ - type: commit_check_accepts
+ node: dut
+ config_lines:
+ - "set protocols ospf3 area 0.0.0.0 interface lo0.0"
+ - "set protocols ospf3 area 0.0.0.0 area-range 2001:db8::/32"
+ description: "OSPFv3 area-range with valid prefix"
+
+ - type: commit_check_accepts
+ node: dut
+ config_lines:
+ - "set firewall family inet6 filter TEST term T from destination-address 2001:db8::1/32"
+ - "set firewall family inet6 filter TEST term T then accept"
+ description: "firewall inet6 destination-address with host bits (accepted, unlike IPv4)"
+
+ - type: commit_check_accepts
+ node: dut
+ config_lines:
+ - "set firewall family inet6 filter TEST term T then next-ip6 2001:db8::1/32"
+ description: "firewall next-ip6 with host bits (accepted, unlike IPv4)"
+
+ - type: commit_check_rejects
+ node: dut
+ config_lines:
+ - "set policy-options condition TEST if-route-exists 2001:db8::1/32"
+ - "set policy-options condition TEST if-route-exists table inet6.0"
+ description: "condition if-route-exists IPv6 with host bits set"
+
+ - type: commit_check_accepts
+ node: dut
+ config_lines:
+ - "set policy-options condition TEST if-route-exists 2001:db8::/32"
+ - "set policy-options condition TEST if-route-exists table inet6.0"
+ description: "condition if-route-exists IPv6 with valid prefix"
+
# --- Contexts where Junos ACCEPTS host bits ---
- type: commit_check_accepts
@@ -150,6 +229,34 @@ checks:
- "set interfaces ge-0/0/0 unit 0 family inet address 10.0.0.1/30 vrrp-group 1 track route 192.168.1.111/24 routing-instance default priority-cost 100"
description: "VRRP track route with host bits (accepted)"
+ # --- Contexts where Junos ACCEPTS host bits (IPv6) ---
+
+ - type: commit_check_accepts
+ node: dut
+ config_lines:
+ - "set policy-options prefix-list TEST 2001:db8::1/32"
+ description: "prefix-list with IPv6 host bits (accepted)"
+
+ - type: commit_check_accepts
+ node: dut
+ config_lines:
+ - "set policy-options policy-statement TEST term T from route-filter 2001:db8::1/32 exact"
+ description: "route-filter with IPv6 host bits (accepted)"
+
+ - type: commit_check_accepts
+ node: dut
+ config_lines:
+ - "set interfaces ge-0/0/0 unit 0 family inet6 address 2001:db8::1/32"
+ description: "interface inet6 address with host bits (accepted)"
+
+ - type: commit_check_accepts
+ node: dut
+ config_lines:
+ - "set protocols bgp group TEST type external"
+ - "set protocols bgp group TEST peer-as 65001"
+ - "set protocols bgp group TEST allow 2001:db8::1/32"
+ description: "BGP allow with IPv6 host bits (accepted)"
+
# TODO: Security hierarchy checks (require vSRX image)
# - security nat source pool
address
# - security nat source pool address to
diff --git a/snapshots/junos_commit_check/configs/accepts-bgp-allow-v6/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/accepts-bgp-allow-v6/show_configuration_|_display_set.txt
new file mode 100644
index 0000000..22f96a9
--- /dev/null
+++ b/snapshots/junos_commit_check/configs/accepts-bgp-allow-v6/show_configuration_|_display_set.txt
@@ -0,0 +1,5 @@
+set system host-name accepts-bgp-allow-v6
+set routing-options autonomous-system 65000
+set protocols bgp group TEST type external
+set protocols bgp group TEST peer-as 65001
+set protocols bgp group TEST allow 2001:db8::1/32
diff --git a/snapshots/junos_commit_check/configs/accepts-firewall6-address/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/accepts-firewall6-address/show_configuration_|_display_set.txt
new file mode 100644
index 0000000..e1b0710
--- /dev/null
+++ b/snapshots/junos_commit_check/configs/accepts-firewall6-address/show_configuration_|_display_set.txt
@@ -0,0 +1,3 @@
+set system host-name accepts-firewall6-address
+set firewall family inet6 filter TEST term T from destination-address 2001:db8::1/32
+set firewall family inet6 filter TEST term T then accept
diff --git a/snapshots/junos_commit_check/configs/accepts-firewall6-next-ip6/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/accepts-firewall6-next-ip6/show_configuration_|_display_set.txt
new file mode 100644
index 0000000..79bd1c2
--- /dev/null
+++ b/snapshots/junos_commit_check/configs/accepts-firewall6-next-ip6/show_configuration_|_display_set.txt
@@ -0,0 +1,2 @@
+set system host-name accepts-firewall6-next-ip6
+set firewall family inet6 filter TEST term T then next-ip6 2001:db8::1/32
diff --git a/snapshots/junos_commit_check/configs/accepts-interface-v6/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/accepts-interface-v6/show_configuration_|_display_set.txt
new file mode 100644
index 0000000..15a7afd
--- /dev/null
+++ b/snapshots/junos_commit_check/configs/accepts-interface-v6/show_configuration_|_display_set.txt
@@ -0,0 +1,2 @@
+set system host-name accepts-interface-v6
+set interfaces ge-0/0/0 unit 0 family inet6 address 2001:db8::1/32
diff --git a/snapshots/junos_commit_check/configs/accepts-prefix-list-v6/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/accepts-prefix-list-v6/show_configuration_|_display_set.txt
new file mode 100644
index 0000000..625a9b5
--- /dev/null
+++ b/snapshots/junos_commit_check/configs/accepts-prefix-list-v6/show_configuration_|_display_set.txt
@@ -0,0 +1,2 @@
+set system host-name accepts-prefix-list-v6
+set policy-options prefix-list TEST 2001:db8::1/32
diff --git a/snapshots/junos_commit_check/configs/accepts-route-filter-v6/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/accepts-route-filter-v6/show_configuration_|_display_set.txt
new file mode 100644
index 0000000..af127c8
--- /dev/null
+++ b/snapshots/junos_commit_check/configs/accepts-route-filter-v6/show_configuration_|_display_set.txt
@@ -0,0 +1,2 @@
+set system host-name accepts-route-filter-v6
+set policy-options policy-statement TEST term T from route-filter 2001:db8::1/32 exact
diff --git a/snapshots/junos_commit_check/configs/rejects-aggregate-v6/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/rejects-aggregate-v6/show_configuration_|_display_set.txt
new file mode 100644
index 0000000..3dc2445
--- /dev/null
+++ b/snapshots/junos_commit_check/configs/rejects-aggregate-v6/show_configuration_|_display_set.txt
@@ -0,0 +1,2 @@
+set system host-name rejects-aggregate-v6
+set routing-options rib inet6.0 aggregate route 2001:db8::1/32
diff --git a/snapshots/junos_commit_check/configs/rejects-condition-v6/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/rejects-condition-v6/show_configuration_|_display_set.txt
new file mode 100644
index 0000000..bfdc80d
--- /dev/null
+++ b/snapshots/junos_commit_check/configs/rejects-condition-v6/show_configuration_|_display_set.txt
@@ -0,0 +1,3 @@
+set system host-name rejects-condition-v6
+set policy-options condition TEST if-route-exists 2001:db8::1/32
+set policy-options condition TEST if-route-exists table inet6.0
diff --git a/snapshots/junos_commit_check/configs/rejects-generate-v6/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/rejects-generate-v6/show_configuration_|_display_set.txt
new file mode 100644
index 0000000..8b39dc8
--- /dev/null
+++ b/snapshots/junos_commit_check/configs/rejects-generate-v6/show_configuration_|_display_set.txt
@@ -0,0 +1,2 @@
+set system host-name rejects-generate-v6
+set routing-options rib inet6.0 generate route 2001:db8::1/32
diff --git a/snapshots/junos_commit_check/configs/rejects-ospf3-area-range/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/rejects-ospf3-area-range/show_configuration_|_display_set.txt
new file mode 100644
index 0000000..dd075db
--- /dev/null
+++ b/snapshots/junos_commit_check/configs/rejects-ospf3-area-range/show_configuration_|_display_set.txt
@@ -0,0 +1,3 @@
+set system host-name rejects-ospf3-area-range
+set protocols ospf3 area 0.0.0.0 interface lo0.0
+set protocols ospf3 area 0.0.0.0 area-range 2001:db8::1/32
diff --git a/snapshots/junos_commit_check/configs/rejects-static-v6/show_configuration_|_display_set.txt b/snapshots/junos_commit_check/configs/rejects-static-v6/show_configuration_|_display_set.txt
new file mode 100644
index 0000000..1218940
--- /dev/null
+++ b/snapshots/junos_commit_check/configs/rejects-static-v6/show_configuration_|_display_set.txt
@@ -0,0 +1,2 @@
+set system host-name rejects-static-v6
+set routing-options rib inet6.0 static route 2001:db8::1/32 reject
diff --git a/snapshots/junos_commit_check/show/host_nos.txt b/snapshots/junos_commit_check/show/host_nos.txt
index 41e3319..6d5edfc 100644
--- a/snapshots/junos_commit_check/show/host_nos.txt
+++ b/snapshots/junos_commit_check/show/host_nos.txt
@@ -6,10 +6,21 @@
"rejects-firewall-address": "junos",
"rejects-firewall-next-ip": "junos",
"rejects-condition": "junos",
+ "rejects-static-v6": "junos",
+ "rejects-aggregate-v6": "junos",
+ "rejects-generate-v6": "junos",
+ "rejects-ospf3-area-range": "junos",
+ "rejects-condition-v6": "junos",
"accepts-prefix-list": "junos",
"accepts-route-filter": "junos",
"accepts-snmp": "junos",
"accepts-bgp-allow": "junos",
"accepts-interface": "junos",
- "accepts-mpls-install": "junos"
+ "accepts-mpls-install": "junos",
+ "accepts-prefix-list-v6": "junos",
+ "accepts-route-filter-v6": "junos",
+ "accepts-interface-v6": "junos",
+ "accepts-bgp-allow-v6": "junos",
+ "accepts-firewall6-address": "junos",
+ "accepts-firewall6-next-ip6": "junos"
}
diff --git a/snapshots/junos_commit_check/validation/parse_warnings.yaml b/snapshots/junos_commit_check/validation/parse_warnings.yaml
index 3253df8..f1661b7 100644
--- a/snapshots/junos_commit_check/validation/parse_warnings.yaml
+++ b/snapshots/junos_commit_check/validation/parse_warnings.yaml
@@ -12,7 +12,7 @@
# prefix normalization checks (batfish/batfish#9928).
expects_fatal_warning:
- # Junos rejects unnormalized prefixes in these contexts
+ # Junos rejects unnormalized prefixes in these contexts (IPv4)
- host: rejects-static
contains: "192.168.1.111/24"
- host: rejects-aggregate
@@ -27,12 +27,30 @@ expects_fatal_warning:
contains: "192.168.1.111/24"
- host: rejects-condition
contains: "192.168.1.111/24"
+ # Junos rejects unnormalized prefixes in these contexts (IPv6)
+ - host: rejects-static-v6
+ contains: "2001:db8::1/32"
+ - host: rejects-aggregate-v6
+ contains: "2001:db8::1/32"
+ - host: rejects-generate-v6
+ contains: "2001:db8::1/32"
+ - host: rejects-ospf3-area-range
+ contains: "2001:db8::1/32"
+ - host: rejects-condition-v6
+ contains: "2001:db8::1/32"
expects_no_fatal_warning:
- # Junos accepts unnormalized prefixes in these contexts
+ # Junos accepts unnormalized prefixes in these contexts (IPv4)
- accepts-prefix-list
- accepts-route-filter
- accepts-snmp
- accepts-bgp-allow
- accepts-interface
- accepts-mpls-install
+ # Junos accepts unnormalized prefixes in these contexts (IPv6)
+ - accepts-prefix-list-v6
+ - accepts-route-filter-v6
+ - accepts-interface-v6
+ - accepts-bgp-allow-v6
+ - accepts-firewall6-address
+ - accepts-firewall6-next-ip6
diff --git a/snapshots/junos_commit_check/validation/sickbay.yaml b/snapshots/junos_commit_check/validation/sickbay.yaml
index 521b0d0..3497ab8 100644
--- a/snapshots/junos_commit_check/validation/sickbay.yaml
+++ b/snapshots/junos_commit_check/validation/sickbay.yaml
@@ -26,3 +26,29 @@ entries:
skip:
skip_type: dont_run
reason: "commit check lab: configs are parse-warning test fixtures, not full devices"
+ # IPv6 prefix normalization not yet implemented in Batfish
+ - hostname: rejects-static-v6
+ test_name: test_parse_warnings
+ skip:
+ skip_type: xfail
+ reason: "https://github.com/batfish/batfish/issues/9934"
+ - hostname: rejects-aggregate-v6
+ test_name: test_parse_warnings
+ skip:
+ skip_type: xfail
+ reason: "https://github.com/batfish/batfish/issues/9934"
+ - hostname: rejects-generate-v6
+ test_name: test_parse_warnings
+ skip:
+ skip_type: xfail
+ reason: "https://github.com/batfish/batfish/issues/9934"
+ - hostname: rejects-ospf3-area-range
+ test_name: test_parse_warnings
+ skip:
+ skip_type: xfail
+ reason: "https://github.com/batfish/batfish/issues/9934"
+ - hostname: rejects-condition-v6
+ test_name: test_parse_warnings
+ skip:
+ skip_type: xfail
+ reason: "https://github.com/batfish/batfish/issues/9934"
diff --git a/src/lab_builder/validate.py b/src/lab_builder/validate.py
index febd512..1361e3a 100644
--- a/src/lab_builder/validate.py
+++ b/src/lab_builder/validate.py
@@ -262,10 +262,15 @@ def _arista_check_bgp_peer(node: NodeInfo, neighbor: str) -> CheckResult:
# ---------------------------------------------------------------------------
-def _junos_commit_check(node: NodeInfo, config_lines: list[str]) -> tuple[bool, str]:
+def _junos_commit_check(
+ node: NodeInfo, config_lines: list[str]
+) -> tuple[bool | None, str]:
"""Load config lines on a Junos device and run 'commit check'.
- Returns (success, output) where success is True if commit check passes.
+ Returns (result, output) where result is:
+ True - commit check succeeded
+ False - commit check was rejected (definitive failure output)
+ None - indeterminate (exception, timeout, or empty output)
Always rolls back after the check so the device stays clean.
"""
from lab_builder.device import connect
@@ -278,19 +283,28 @@ def _junos_commit_check(node: NodeInfo, config_lines: list[str]) -> tuple[bool,
if not stripped or stripped.startswith("#"):
continue
conn.send_command_timing(stripped)
- output: str = conn.send_command_timing("commit check")
+ output: str = conn.send_command(
+ "commit check", expect_string=r"#", read_timeout=30
+ )
conn.send_command_timing("rollback 0")
conn.exit_config_mode()
- success = "configuration check succeeds" in output.lower()
- return success, output.strip()
+ stripped_output = output.strip()
+ if "configuration check succeeds" in output.lower():
+ return True, stripped_output
+ elif "error" in output.lower() or "failed" in output.lower():
+ return False, stripped_output
+ elif not stripped_output:
+ return None, "empty output from commit check"
+ else:
+ return None, f"indeterminate output: {stripped_output}"
except Exception as e:
try:
conn.send_command_timing("rollback 0")
conn.exit_config_mode()
except Exception:
pass
- return False, f"exception: {e}"
+ return None, f"exception: {e}"
finally:
conn.disconnect()
@@ -299,8 +313,15 @@ def _check_commit_rejects(
node: NodeInfo, config_lines: list[str], expected_error: str | None
) -> CheckResult:
"""Verify that Junos rejects the given config lines at commit check."""
- success, output = _junos_commit_check(node, config_lines)
- if success:
+ result, output = _junos_commit_check(node, config_lines)
+ if result is None:
+ return CheckResult(
+ "commit_check_rejects",
+ node.name,
+ False,
+ f"indeterminate result: {output}",
+ )
+ if result:
return CheckResult(
"commit_check_rejects",
node.name,
@@ -324,8 +345,15 @@ def _check_commit_rejects(
def _check_commit_accepts(node: NodeInfo, config_lines: list[str]) -> CheckResult:
"""Verify that Junos accepts the given config lines at commit check."""
- success, output = _junos_commit_check(node, config_lines)
- if success:
+ result, output = _junos_commit_check(node, config_lines)
+ if result is None:
+ return CheckResult(
+ "commit_check_accepts",
+ node.name,
+ False,
+ f"indeterminate result: {output}",
+ )
+ if result:
return CheckResult(
"commit_check_accepts",
node.name,