mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68e7167902 | ||
|
|
25a26fe7c6 | ||
|
|
6c8bd25cee | ||
|
|
ea300137ef | ||
|
|
44e9c87376 | ||
|
|
e708f21444 | ||
|
|
3508b8d0bb | ||
|
|
eec867bd5f | ||
|
|
daa0bd81e8 | ||
|
|
4ccc70aeee | ||
|
|
923443b620 | ||
|
|
9ad51c31bf | ||
|
|
6272c9c46c | ||
|
|
4750f96774 | ||
|
|
81bc143883 | ||
|
|
58b3fb30be | ||
|
|
6dfc305f0b | ||
|
|
8ca3d3a3ff | ||
|
|
7ae4de3a17 |
@@ -39,7 +39,7 @@ env:
|
||||
jobs:
|
||||
test:
|
||||
name: Regression Test ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.name || inputs.name }} ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.target || inputs.target }}
|
||||
runs-on: [ self-hosted, regression ]
|
||||
runs-on: [self-hosted, regression]
|
||||
steps:
|
||||
- name: Checkout infix repo
|
||||
uses: actions/checkout@v4
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
fi
|
||||
make test
|
||||
|
||||
- name: Publish Test Result for ${{ env.TARGET }}
|
||||
- name: Publish Test Result for ${{ env.TARGET }}
|
||||
# Ensure this runs even if Regression Test fails
|
||||
if: always()
|
||||
run: cat $TEST_PATH/.log/last/result-gh.md >> $GITHUB_STEP_SUMMARY
|
||||
@@ -96,15 +96,10 @@ jobs:
|
||||
# Ensure this runs even if Regression Test fails
|
||||
if: always()
|
||||
run: |
|
||||
asciidoctor-pdf \
|
||||
--theme $TEST_PATH/9pm/report/theme.yml \
|
||||
-a pdf-fontsdir=$TEST_PATH/9pm/report/fonts \
|
||||
$TEST_PATH/.log/last/report.adoc \
|
||||
-o $TEST_PATH/.log/last/report.pdf
|
||||
make test-dir="$(pwd)/$TEST_PATH" test-report
|
||||
|
||||
- name: Upload Test Report as Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-report
|
||||
path: ${{ env.TEST_PATH }}/.log/last/report.pdf
|
||||
|
||||
path: output/images/test-report.pdf
|
||||
|
||||
@@ -9,6 +9,10 @@ on:
|
||||
- ci-work
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-x86_64:
|
||||
if: startsWith(github.repository, 'kernelkit/')
|
||||
@@ -33,6 +37,6 @@ jobs:
|
||||
name: "infix"
|
||||
|
||||
test-publish-x86_64:
|
||||
if: startsWith(github.repository, 'kernelkit/')
|
||||
if: ${{ github.repository_owner == 'kernelkit' && github.ref_name == 'main' }}
|
||||
needs: test-run-x86_64
|
||||
uses: ./.github/workflows/publish.yml
|
||||
|
||||
@@ -83,7 +83,7 @@ genboot()
|
||||
{
|
||||
if [ -d "$bootdata" ]; then
|
||||
bootimg=$(cat <<EOF
|
||||
image efi-part.vfat {
|
||||
image $BINARIES_DIR/efi-part.vfat {
|
||||
size = $bootsize
|
||||
vfat {
|
||||
file EFI {
|
||||
@@ -98,7 +98,7 @@ EOF
|
||||
offset = $bootoffs
|
||||
partition-type-uuid = U
|
||||
bootable = true
|
||||
image = efi-part.vfat
|
||||
image = $BINARIES_DIR/efi-part.vfat
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
Regular → Executable
@@ -3,6 +3,18 @@ Change Log
|
||||
|
||||
All notable changes to the project are documented in this file.
|
||||
|
||||
[v25.08.1][] - 2025-10-03
|
||||
-------------------------
|
||||
|
||||
### Changes
|
||||
- N/A
|
||||
|
||||
### Fixes
|
||||
- Fix #1150: `show-legacy` wrapper permissions
|
||||
- Fix #1155: `show ospf` commands regression
|
||||
- Fix #1169: Expected OSPF neighbors not shown in `sysrepocfg` when the
|
||||
system has at least one non-OSPF interface
|
||||
|
||||
[v25.08.0][] - 2025-09-01
|
||||
-------------------------
|
||||
|
||||
@@ -1613,6 +1625,7 @@ Supported YANG models in addition to those used by sysrepo and netopeer:
|
||||
|
||||
[buildroot]: https://buildroot.org/
|
||||
[UNRELEASED]: https://github.com/kernelkit/infix/compare/v25.08.0...HEAD
|
||||
[v25.08.1]: https://github.com/kernelkit/infix/compare/v25.08.0...v26.08.1
|
||||
[v25.08.0]: https://github.com/kernelkit/infix/compare/v25.06.1...v26.08.0
|
||||
[v25.06.0]: https://github.com/kernelkit/infix/compare/v25.05.1...v26.06.0
|
||||
[v25.05.1]: https://github.com/kernelkit/infix/compare/v25.05.0...v25.05.1
|
||||
|
||||
@@ -338,17 +338,17 @@
|
||||
</PARAM>
|
||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||
if [ -z "$KLISH_PARAM_name" ]; then
|
||||
doas vtysh -c "show-legacy ip ospf" |pager
|
||||
doas vtysh -c "show ip ospf" |pager
|
||||
elif [ "$KLISH_PARAM_name" == "neighbor" ];then
|
||||
doas vtysh -c "show-legacy ip ospf neighbor" |pager
|
||||
doas vtysh -c "show ip ospf neighbor" |pager
|
||||
elif [ "$KLISH_PARAM_name" == "interfaces" ];then
|
||||
doas vtysh -c "show-legacy ip ospf interface" |pager
|
||||
doas vtysh -c "show ip ospf interface" |pager
|
||||
elif [ "$KLISH_PARAM_name" == "routes" ];then
|
||||
doas vtysh -c "show-legacy ip ospf route" |pager
|
||||
doas vtysh -c "show ip ospf route" |pager
|
||||
elif [ "$KLISH_PARAM_name" == "database" ];then
|
||||
doas vtysh -c "show-legacy ip ospf database" |pager
|
||||
doas vtysh -c "show ip ospf database" |pager
|
||||
elif [ "$KLISH_PARAM_name" == "bfd" ];then
|
||||
doas vtysh -c "show-legacy bfd peers" |pager
|
||||
doas vtysh -c "show bfd peers" |pager
|
||||
fi
|
||||
</ACTION>
|
||||
</COMMAND>
|
||||
|
||||
@@ -40,6 +40,11 @@ def main():
|
||||
for ifname, iface in interfaces["interfaces"].items():
|
||||
iface["name"] = ifname
|
||||
iface["neighbors"] = []
|
||||
|
||||
# Skip interfaces that don't have OSPF enabled or area configured
|
||||
if not iface.get("ospfEnabled", False) or not iface.get("area"):
|
||||
continue
|
||||
|
||||
for area_id in ospf["areas"]:
|
||||
area_type=""
|
||||
|
||||
|
||||
@@ -6,12 +6,26 @@
|
||||
infamy:
|
||||
specification: False
|
||||
|
||||
- case: meta/bootorder.py
|
||||
infamy:
|
||||
specification: False
|
||||
|
||||
- case: meta/check-version.py
|
||||
infamy:
|
||||
specification: False
|
||||
|
||||
|
||||
- name: Misc tests
|
||||
suite: misc/misc.yaml
|
||||
|
||||
- name: ietf-system
|
||||
suite: ietf_system/ietf_system.yaml
|
||||
|
||||
# Upgrade may leave wrong boot order
|
||||
- case: meta/bootorder.py
|
||||
infamy:
|
||||
specification: False
|
||||
|
||||
- name: ietf-syslog
|
||||
suite: ietf_syslog/ietf_syslog.yaml
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
=== OSPF Basic
|
||||
==== Description
|
||||
Very basic OSPF test just test that OSPF sends HELLO packets between the DUTs
|
||||
and that they exchange routes, ending with a simple connectivity check.
|
||||
Verifies basic OSPF functionality by configuring two routers (R1 and R2)
|
||||
with OSPF on their interconnecting link. The test ensures OSPF
|
||||
neighbors are established, routes are exchanged between the routers, and
|
||||
end-to-end connectivity is achieved.
|
||||
|
||||
An end-device (HOST) is connected to R2 on an interface without OSPF enabled.
|
||||
This verifies that OSPF status information remains accessible when a router
|
||||
has non-OSPF interfaces.
|
||||
|
||||
==== Topology
|
||||
ifdef::topdoc[]
|
||||
@@ -19,6 +25,7 @@ endif::topdoc[]
|
||||
. Set up topology and attach to target DUTs
|
||||
. Configure targets
|
||||
. Wait for OSPF routes
|
||||
. Verify R2 OSPF neighbors with non-OSPF interface
|
||||
. Test connectivity from PC:data to 192.168.200.1
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OSPF Basic
|
||||
"""OSPF Basic
|
||||
|
||||
Verifies basic OSPF functionality by configuring two routers (R1 and R2)
|
||||
with OSPF on their interconnecting link. The test ensures OSPF
|
||||
neighbors are established, routes are exchanged between the routers, and
|
||||
end-to-end connectivity is achieved.
|
||||
|
||||
An end-device (HOST) is connected to R2 on an interface without OSPF enabled.
|
||||
This verifies that OSPF status information remains accessible when a router
|
||||
has non-OSPF interfaces.
|
||||
|
||||
Very basic OSPF test just test that OSPF sends HELLO packets between the DUTs
|
||||
and that they exchange routes, ending with a simple connectivity check.
|
||||
"""
|
||||
|
||||
# TODO: Remove HOST node once Infamy supports unconnected ports in topologies
|
||||
|
||||
import infamy
|
||||
import infamy.route as route
|
||||
from infamy.util import until, parallel
|
||||
|
||||
|
||||
def config_target1(target, data, link):
|
||||
target.put_config_dict("ietf-interfaces", {
|
||||
"interfaces": {
|
||||
@@ -30,8 +39,8 @@ def config_target1(target, data, link):
|
||||
"ipv4": {
|
||||
"forwarding": True,
|
||||
"address": [{
|
||||
"ip": "192.168.50.1",
|
||||
"prefix-length": 24
|
||||
"ip": "192.168.50.1",
|
||||
"prefix-length": 24
|
||||
}]
|
||||
}
|
||||
},
|
||||
@@ -40,8 +49,8 @@ def config_target1(target, data, link):
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": "192.168.100.1",
|
||||
"prefix-length": 32
|
||||
"ip": "192.168.100.1",
|
||||
"prefix-length": 32
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -56,38 +65,34 @@ def config_target1(target, data, link):
|
||||
target.put_config_dict("ietf-routing", {
|
||||
"routing": {
|
||||
"control-plane-protocols": {
|
||||
"control-plane-protocol": [
|
||||
{
|
||||
"type": "infix-routing:static",
|
||||
"name": "default",
|
||||
"static-routes": {
|
||||
"ipv4": {
|
||||
"route": [{
|
||||
"destination-prefix": "192.168.33.1/32",
|
||||
"next-hop": {
|
||||
"special-next-hop": "blackhole"
|
||||
}
|
||||
}]
|
||||
}
|
||||
"control-plane-protocol": [{
|
||||
"type": "infix-routing:static",
|
||||
"name": "default",
|
||||
"static-routes": {
|
||||
"ipv4": {
|
||||
"route": [{
|
||||
"destination-prefix": "192.168.33.1/32",
|
||||
"next-hop": {
|
||||
"special-next-hop": "blackhole"
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
}
|
||||
}, {
|
||||
"type": "infix-routing:ospfv2",
|
||||
"name": "default",
|
||||
"ospf": {
|
||||
"redistribute": {
|
||||
"redistribute": [{
|
||||
"protocol": "static"
|
||||
},
|
||||
{
|
||||
}, {
|
||||
"protocol": "connected"
|
||||
}]
|
||||
},
|
||||
"areas": {
|
||||
"area": [{
|
||||
"area-id": "0.0.0.0",
|
||||
"interfaces":
|
||||
{
|
||||
"interfaces": {
|
||||
"interface": [{
|
||||
"enabled": True,
|
||||
"name": link,
|
||||
@@ -103,35 +108,43 @@ def config_target1(target, data, link):
|
||||
}
|
||||
})
|
||||
|
||||
def config_target2(target, link):
|
||||
|
||||
def config_target2(target, link, data):
|
||||
target.put_config_dict("ietf-interfaces", {
|
||||
"interfaces": {
|
||||
"interface": [
|
||||
{
|
||||
"name": link,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"forwarding": True,
|
||||
"address": [{
|
||||
"ip": "192.168.50.2",
|
||||
"prefix-length": 24
|
||||
}]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "lo",
|
||||
"enabled": True,
|
||||
"forwarding": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": "192.168.200.1",
|
||||
"prefix-length": 32
|
||||
}]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
"interfaces": {
|
||||
"interface": [{
|
||||
"name": link,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"forwarding": True,
|
||||
"address": [{
|
||||
"ip": "192.168.50.2",
|
||||
"prefix-length": 24
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
"name": data,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"forwarding": True,
|
||||
"address": [{
|
||||
"ip": "192.168.60.1",
|
||||
"prefix-length": 24
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
"name": "lo",
|
||||
"enabled": True,
|
||||
"forwarding": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": "192.168.200.1",
|
||||
"prefix-length": 32
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
target.put_config_dict("ietf-system", {
|
||||
"system": {
|
||||
@@ -141,8 +154,7 @@ def config_target2(target, link):
|
||||
target.put_config_dict("ietf-routing", {
|
||||
"routing": {
|
||||
"control-plane-protocols": {
|
||||
"control-plane-protocol": [
|
||||
{
|
||||
"control-plane-protocol": [{
|
||||
"type": "infix-routing:ospfv2",
|
||||
"name": "default",
|
||||
"ospf": {
|
||||
@@ -154,7 +166,7 @@ def config_target2(target, link):
|
||||
"areas": {
|
||||
"area": [{
|
||||
"area-id": "0.0.0.0",
|
||||
"interfaces":{
|
||||
"interfaces": {
|
||||
"interface": [{
|
||||
"enabled": True,
|
||||
"name": link,
|
||||
@@ -165,31 +177,61 @@ def config_target2(target, link):
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
def config_host(target, link):
|
||||
target.put_config_dict("ietf-interfaces", {
|
||||
"interfaces": {
|
||||
"interface": [{
|
||||
"name": link,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": "192.168.60.2",
|
||||
"prefix-length": 24
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
target.put_config_dict("ietf-system", {
|
||||
"system": {
|
||||
"hostname": "HOST"
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Set up topology and attach to target DUTs"):
|
||||
env = infamy.Env()
|
||||
R1 = env.attach("R1", "mgmt")
|
||||
R2 = env.attach("R2", "mgmt")
|
||||
HOST = env.attach("HOST", "mgmt")
|
||||
|
||||
with test.step("Configure targets"):
|
||||
_, R1data = env.ltop.xlate("R1", "data")
|
||||
_, R2link = env.ltop.xlate("R2", "link")
|
||||
_, R1link= env.ltop.xlate("R1", "link")
|
||||
_, R1link = env.ltop.xlate("R1", "link")
|
||||
_, R2data = env.ltop.xlate("R2", "data")
|
||||
_, HOSTlink = env.ltop.xlate("HOST", "link")
|
||||
|
||||
parallel(config_target1(R1, R1data, R1link),
|
||||
config_target2(R2, R2link))
|
||||
config_target2(R2, R2link, R2data),
|
||||
config_host(HOST, HOSTlink))
|
||||
with test.step("Wait for OSPF routes"):
|
||||
print("Waiting for OSPF routes..")
|
||||
until(lambda: route.ipv4_route_exist(R1, "192.168.200.1/32", proto="ietf-ospf:ospfv2"), attempts=200)
|
||||
until(lambda: route.ipv4_route_exist(R2, "192.168.100.1/32", proto="ietf-ospf:ospfv2"), attempts=200)
|
||||
until(lambda: route.ipv4_route_exist(R2, "192.168.33.1/32", proto="ietf-ospf:ospfv2"), attempts=200)
|
||||
|
||||
with test.step("Verify R2 OSPF neighbors with non-OSPF interface"):
|
||||
# Regression test for #1169
|
||||
assert route.ospf_has_neighbors(R2)
|
||||
|
||||
with test.step("Test connectivity from PC:data to 192.168.200.1"):
|
||||
_, hport0 = env.ltop.xlate("PC", "data")
|
||||
with infamy.IsolatedMacVlan(hport0) as ns0:
|
||||
|
||||
@@ -8,26 +8,34 @@ graph "2x2" {
|
||||
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||
|
||||
PC [
|
||||
label="PC | { <mgmt1> mgmt1 | <data> data | <mgmt2> mgmt2 }",
|
||||
pos="20,80!",
|
||||
label="PC | { <mgmt1> mgmt1 | <data> data | <mgmt2> mgmt2 | <mgmt3> mgmt3 }",
|
||||
pos="20,30!",
|
||||
requires="controller",
|
||||
];
|
||||
|
||||
R1 [
|
||||
label="{ <mgmt> mgmt | <data> data | <link> link} | R1 \n 192.168.100.1/32 \n(lo)",
|
||||
pos="250,80!",
|
||||
pos="160,60!",
|
||||
|
||||
requires="infix",
|
||||
];
|
||||
R2 [
|
||||
label="{ <link> link | <mgmt> mgmt | <data> data } | R2 \n 192.168.200.1/32 \n(lo)",
|
||||
pos="250,30!",
|
||||
pos="160,30!",
|
||||
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
HOST [
|
||||
label="{ <link> link | <mgmt> mgmt } | HOST \n end-device",
|
||||
pos="153,0!",
|
||||
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
PC:mgmt1 -- R1:mgmt [requires="mgmt", color="lightgray"]
|
||||
PC:mgmt2 -- R2:mgmt [requires="mgmt", color="lightgray"]
|
||||
PC:data -- R1:data [color="black", headlabel="192.168.10.1/24", taillabel="192.168.10.2/24", fontcolor="black"]
|
||||
PC:mgmt3 -- HOST:mgmt [requires="mgmt", color="lightgray"]
|
||||
PC:data -- R1:data [color="black", headlabel="192.168.10.1/24", taillabel="192.168.10.2/24", labeldistance=6, fontcolor="black"]
|
||||
R1:link -- R2:link [headlabel="192.168.50.2/24", taillabel="192.168.50.1/24", labeldistance=1, fontcolor="black", color="black"]
|
||||
R2:data -- HOST:link [headlabel="192.168.60.2/24", taillabel="192.168.60.1/24", labeldistance=1, fontcolor="black", color="black"]
|
||||
}
|
||||
|
||||
@@ -3,74 +3,99 @@
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Title: 2x2 Pages: 1 -->
|
||||
<svg width="713pt" height="198pt"
|
||||
viewBox="0.00 0.00 713.06 198.01" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 194.01)">
|
||||
<svg width="720pt" height="306pt"
|
||||
viewBox="0.00 0.00 720.00 306.08" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 302.52)">
|
||||
<title>2x2</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-194.01 709.06,-194.01 709.06,4 -4,4"/>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-302.52 717.06,-302.52 717.06,4 -4,4"/>
|
||||
<!-- PC -->
|
||||
<g id="node1" class="node">
|
||||
<title>PC</title>
|
||||
<polygon fill="none" stroke="black" points="0,-120.51 0,-189.51 91,-189.51 91,-120.51 0,-120.51"/>
|
||||
<text text-anchor="middle" x="16.5" y="-151.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">PC</text>
|
||||
<polyline fill="none" stroke="black" points="33,-120.51 33,-189.51 "/>
|
||||
<polygon fill="none" stroke="black" points="0,-97.51 0,-189.51 91,-189.51 91,-97.51 0,-97.51"/>
|
||||
<text text-anchor="middle" x="16.5" y="-139.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">PC</text>
|
||||
<polyline fill="none" stroke="black" points="33,-97.51 33,-189.51 "/>
|
||||
<text text-anchor="middle" x="62" y="-174.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
|
||||
<polyline fill="none" stroke="black" points="33,-166.51 91,-166.51 "/>
|
||||
<text text-anchor="middle" x="62" y="-151.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
|
||||
<polyline fill="none" stroke="black" points="33,-143.51 91,-143.51 "/>
|
||||
<text text-anchor="middle" x="62" y="-128.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
|
||||
<polyline fill="none" stroke="black" points="33,-120.51 91,-120.51 "/>
|
||||
<text text-anchor="middle" x="62" y="-105.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt3</text>
|
||||
</g>
|
||||
<!-- R1 -->
|
||||
<g id="node2" class="node">
|
||||
<title>R1</title>
|
||||
<polygon fill="none" stroke="black" points="490.06,-120.51 490.06,-189.51 705.06,-189.51 705.06,-120.51 490.06,-120.51"/>
|
||||
<text text-anchor="middle" x="515.06" y="-174.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="490.06,-166.51 540.06,-166.51 "/>
|
||||
<text text-anchor="middle" x="515.06" y="-151.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
|
||||
<polyline fill="none" stroke="black" points="490.06,-143.51 540.06,-143.51 "/>
|
||||
<text text-anchor="middle" x="515.06" y="-128.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
|
||||
<polyline fill="none" stroke="black" points="540.06,-120.51 540.06,-189.51 "/>
|
||||
<text text-anchor="middle" x="622.56" y="-166.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">R1 </text>
|
||||
<text text-anchor="middle" x="622.56" y="-151.31" font-family="DejaVu Sans Mono, Book" font-size="14.00"> 192.168.100.1/32 </text>
|
||||
<text text-anchor="middle" x="622.56" y="-136.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">(lo)</text>
|
||||
<polygon fill="none" stroke="black" points="498.06,-229.02 498.06,-298.02 713.06,-298.02 713.06,-229.02 498.06,-229.02"/>
|
||||
<text text-anchor="middle" x="523.06" y="-282.82" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="498.06,-275.02 548.06,-275.02 "/>
|
||||
<text text-anchor="middle" x="523.06" y="-259.82" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
|
||||
<polyline fill="none" stroke="black" points="498.06,-252.02 548.06,-252.02 "/>
|
||||
<text text-anchor="middle" x="523.06" y="-236.82" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
|
||||
<polyline fill="none" stroke="black" points="548.06,-229.02 548.06,-298.02 "/>
|
||||
<text text-anchor="middle" x="630.56" y="-274.82" font-family="DejaVu Sans Mono, Book" font-size="14.00">R1 </text>
|
||||
<text text-anchor="middle" x="630.56" y="-259.82" font-family="DejaVu Sans Mono, Book" font-size="14.00"> 192.168.100.1/32 </text>
|
||||
<text text-anchor="middle" x="630.56" y="-244.82" font-family="DejaVu Sans Mono, Book" font-size="14.00">(lo)</text>
|
||||
</g>
|
||||
<!-- PC--R1 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>PC:mgmt1--R1:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-178.01C91.5,-178.01 489.56,-178.01 489.56,-178.01"/>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-178.51C91.5,-178.51 497.56,-286.52 497.56,-286.52"/>
|
||||
</g>
|
||||
<!-- PC--R1 -->
|
||||
<g id="edge3" class="edge">
|
||||
<g id="edge4" class="edge">
|
||||
<title>PC:data--R1:data</title>
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M91.5,-155.01C91.5,-155.01 489.56,-155.01 489.56,-155.01"/>
|
||||
<text text-anchor="middle" x="430.56" y="-158.81" font-family="DejaVu Serif, Book" font-size="14.00">192.168.10.1/24</text>
|
||||
<text text-anchor="middle" x="150.5" y="-158.81" font-family="DejaVu Serif, Book" font-size="14.00">192.168.10.2/24</text>
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M91.5,-155.51C91.5,-155.51 497.56,-263.52 497.56,-263.52"/>
|
||||
<text text-anchor="middle" x="438.49" y="-270.35" font-family="DejaVu Serif, Book" font-size="14.00">192.168.10.1/24</text>
|
||||
<text text-anchor="middle" x="150.57" y="-141.29" font-family="DejaVu Serif, Book" font-size="14.00">192.168.10.2/24</text>
|
||||
</g>
|
||||
<!-- R2 -->
|
||||
<g id="node3" class="node">
|
||||
<title>R2</title>
|
||||
<polygon fill="none" stroke="black" points="490.06,-0.5 490.06,-69.5 705.06,-69.5 705.06,-0.5 490.06,-0.5"/>
|
||||
<text text-anchor="middle" x="515.06" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
|
||||
<polyline fill="none" stroke="black" points="490.06,-46.5 540.06,-46.5 "/>
|
||||
<text text-anchor="middle" x="515.06" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="490.06,-23.5 540.06,-23.5 "/>
|
||||
<text text-anchor="middle" x="515.06" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
|
||||
<polyline fill="none" stroke="black" points="540.06,-0.5 540.06,-69.5 "/>
|
||||
<text text-anchor="middle" x="622.56" y="-46.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">R2 </text>
|
||||
<text text-anchor="middle" x="622.56" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00"> 192.168.200.1/32 </text>
|
||||
<text text-anchor="middle" x="622.56" y="-16.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">(lo)</text>
|
||||
<polygon fill="none" stroke="black" points="498.06,-109.01 498.06,-178.01 713.06,-178.01 713.06,-109.01 498.06,-109.01"/>
|
||||
<text text-anchor="middle" x="523.06" y="-162.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
|
||||
<polyline fill="none" stroke="black" points="498.06,-155.01 548.06,-155.01 "/>
|
||||
<text text-anchor="middle" x="523.06" y="-139.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="498.06,-132.01 548.06,-132.01 "/>
|
||||
<text text-anchor="middle" x="523.06" y="-116.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
|
||||
<polyline fill="none" stroke="black" points="548.06,-109.01 548.06,-178.01 "/>
|
||||
<text text-anchor="middle" x="630.56" y="-154.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">R2 </text>
|
||||
<text text-anchor="middle" x="630.56" y="-139.81" font-family="DejaVu Sans Mono, Book" font-size="14.00"> 192.168.200.1/32 </text>
|
||||
<text text-anchor="middle" x="630.56" y="-124.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">(lo)</text>
|
||||
</g>
|
||||
<!-- PC--R2 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>PC:mgmt2--R2:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-132.01C91.5,-132.01 489.56,-35 489.56,-35"/>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-131.51C91.5,-131.51 497.56,-143.51 497.56,-143.51"/>
|
||||
</g>
|
||||
<!-- HOST -->
|
||||
<g id="node4" class="node">
|
||||
<title>HOST</title>
|
||||
<polygon fill="none" stroke="black" points="499.05,-0.5 499.05,-46.5 656.05,-46.5 656.05,-0.5 499.05,-0.5"/>
|
||||
<text text-anchor="middle" x="524.05" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
|
||||
<polyline fill="none" stroke="black" points="499.05,-23.5 549.05,-23.5 "/>
|
||||
<text text-anchor="middle" x="524.05" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="549.05,-0.5 549.05,-46.5 "/>
|
||||
<text text-anchor="middle" x="602.55" y="-27.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">HOST </text>
|
||||
<text text-anchor="middle" x="602.55" y="-12.3" font-family="DejaVu Sans Mono, Book" font-size="14.00"> end-device</text>
|
||||
</g>
|
||||
<!-- PC--HOST -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>PC:mgmt3--HOST:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M91.5,-108.51C91.5,-108.51 498.55,-11.5 498.55,-11.5"/>
|
||||
</g>
|
||||
<!-- R1--R2 -->
|
||||
<g id="edge4" class="edge">
|
||||
<g id="edge5" class="edge">
|
||||
<title>R1:link--R2:link</title>
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M514.56,-120.01C514.56,-120.01 514.56,-70 514.56,-70"/>
|
||||
<text text-anchor="middle" x="518.78" y="-75.36" font-family="DejaVu Serif, Book" font-size="14.00">192.168.50.2/24</text>
|
||||
<text text-anchor="middle" x="510.33" y="-107.25" font-family="DejaVu Serif, Book" font-size="14.00">192.168.50.1/24</text>
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M522.56,-228.52C522.56,-228.52 522.56,-178.51 522.56,-178.51"/>
|
||||
<text text-anchor="middle" x="526.78" y="-183.88" font-family="DejaVu Serif, Book" font-size="14.00">192.168.50.2/24</text>
|
||||
<text text-anchor="middle" x="518.33" y="-215.76" font-family="DejaVu Serif, Book" font-size="14.00">192.168.50.1/24</text>
|
||||
</g>
|
||||
<!-- R2--HOST -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>R2:data--HOST:link</title>
|
||||
<path fill="none" stroke="black" stroke-width="2" d="M522.56,-108.51C522.56,-108.51 523.55,-46.5 523.55,-46.5"/>
|
||||
<text text-anchor="middle" x="527.63" y="-51.93" font-family="DejaVu Serif, Book" font-size="14.00">192.168.60.2/24</text>
|
||||
<text text-anchor="middle" x="518.48" y="-95.68" font-family="DejaVu Serif, Book" font-size="14.00">192.168.60.1/24</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 6.5 KiB |
@@ -26,7 +26,6 @@ endif::topdoc[]
|
||||
. Set hostname to 'container-host'
|
||||
. Create enabled container from bundled OCI image
|
||||
. Verify container has started
|
||||
. Let container settle
|
||||
. Disable container
|
||||
. Verify container has stopped
|
||||
. Re-enable container
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
=== Container environment variables
|
||||
==== Description
|
||||
Verify that environment variables can be set in container configuration
|
||||
and are available inside the running container. Tests the 'env' list
|
||||
functionality by:
|
||||
and are available inside the running container.
|
||||
|
||||
1. Creating a container with multiple environment variables
|
||||
2. Using a custom script to extract env vars and serve them via HTTP
|
||||
3. Fetching the served content to verify environment variables are set correctly
|
||||
|
||||
Uses the nftables container image with custom rc.local script.
|
||||
1 Set up a container config with multiple environment variables
|
||||
2. Serve variables back to host using a CGI script in container
|
||||
3. Verify served content against environment variables
|
||||
|
||||
==== Topology
|
||||
ifdef::topdoc[]
|
||||
@@ -25,11 +22,10 @@ endif::topdoc[]
|
||||
==== Test sequence
|
||||
. Set up topology and attach to target DUT
|
||||
. Configure data interface with static IPv4
|
||||
. Create container with environment variables
|
||||
. Set up container with environment variables
|
||||
. Verify container has started
|
||||
. Verify environment variables are available via HTTP
|
||||
. Verify basic connectivity to data interface
|
||||
. Verify environment variables in HTTP response
|
||||
. Verify environment variables in CGI response
|
||||
|
||||
|
||||
<<<
|
||||
|
||||
@@ -3,27 +3,32 @@
|
||||
Container environment variables
|
||||
|
||||
Verify that environment variables can be set in container configuration
|
||||
and are available inside the running container. Tests the 'env' list
|
||||
functionality by:
|
||||
and are available inside the running container.
|
||||
|
||||
1. Creating a container with multiple environment variables
|
||||
2. Using a custom script to extract env vars and serve them via HTTP
|
||||
3. Fetching the served content to verify environment variables are set correctly
|
||||
|
||||
Uses the nftables container image with custom rc.local script.
|
||||
1 Set up a container config with multiple environment variables
|
||||
2. Serve variables back to host using a CGI script in container
|
||||
3. Verify served content against environment variables
|
||||
"""
|
||||
import infamy
|
||||
from infamy.util import until, to_binary
|
||||
|
||||
|
||||
with infamy.Test() as test:
|
||||
ENV_VARS = [
|
||||
{"key": "TEST_VAR", "value": "hello-world"},
|
||||
{"key": "APP_PORT", "value": "8080"},
|
||||
{"key": "DEBUG_MODE", "value": "true"},
|
||||
{"key": "PATH_WITH_SPACES", "value": "/path with spaces/test"}
|
||||
]
|
||||
NAME = "web-env"
|
||||
DUTIP = "10.0.0.2"
|
||||
OURIP = "10.0.0.1"
|
||||
url = infamy.Furl(f"http://{DUTIP}:8080/cgi-bin/env.cgi")
|
||||
|
||||
with test.step("Set up topology and attach to target DUT"):
|
||||
env = infamy.Env()
|
||||
target = env.attach("target", "mgmt")
|
||||
_, hport = env.ltop.xlate("host", "data")
|
||||
|
||||
if not target.has_model("infix-containers"):
|
||||
test.skip()
|
||||
@@ -44,44 +49,33 @@ with infamy.Test() as test:
|
||||
}
|
||||
})
|
||||
|
||||
with test.step("Create container with environment variables"):
|
||||
script = to_binary("""#!/bin/sh
|
||||
# Create HTTP response with environment variables
|
||||
printf "HTTP/1.1 200 OK\\r\\n" > /var/www/response.txt
|
||||
printf "Content-Type: text/plain\\r\\n" >> /var/www/response.txt
|
||||
printf "Connection: close\\r\\n\\r\\n" >> /var/www/response.txt
|
||||
with test.step("Set up container with environment variables"):
|
||||
cgi = [
|
||||
'#!/bin/sh',
|
||||
'# CGI script to output environment variables',
|
||||
'echo "Content-Type: text/plain"',
|
||||
'echo ""'
|
||||
]
|
||||
|
||||
# Add environment variables using printf to control encoding
|
||||
printf "TEST_VAR=\\"%s\\"\\n" "$TEST_VAR" >> /var/www/response.txt
|
||||
printf "APP_PORT=%s\\n" "$APP_PORT" >> /var/www/response.txt
|
||||
printf "DEBUG_MODE=\\"%s\\"\\n" "$DEBUG_MODE" >> /var/www/response.txt
|
||||
printf "PATH_WITH_SPACES=\\"%s\\"\\n" "$PATH_WITH_SPACES" >> /var/www/response.txt
|
||||
|
||||
while true; do
|
||||
nc -l -p 8080 < /var/www/response.txt 2>>/var/www/debug.log || sleep 1
|
||||
done
|
||||
""")
|
||||
for var in ENV_VARS:
|
||||
cgi.append(f'echo "{var["key"]}=${var["key"]}"')
|
||||
|
||||
target.put_config_dict("infix-containers", {
|
||||
"containers": {
|
||||
"container": [
|
||||
{
|
||||
"name": f"{NAME}",
|
||||
"image": f"oci-archive:{infamy.Container.NFTABLES_IMAGE}",
|
||||
"env": [
|
||||
{"key": "TEST_VAR", "value": "hello-world"},
|
||||
{"key": "APP_PORT", "value": "8080"},
|
||||
{"key": "DEBUG_MODE", "value": "true"},
|
||||
{"key": "PATH_WITH_SPACES", "value": "/path with spaces/test"}
|
||||
],
|
||||
"image": f"oci-archive:{infamy.Container.HTTPD_IMAGE}",
|
||||
"command": "/usr/sbin/httpd -f -v -p 8080",
|
||||
"env": ENV_VARS,
|
||||
"network": {
|
||||
"host": True
|
||||
},
|
||||
"mount": [
|
||||
{
|
||||
"name": "rc.local",
|
||||
"content": script,
|
||||
"target": "/etc/rc.local",
|
||||
"name": "env.cgi",
|
||||
"content": to_binary('\n'.join(cgi) + '\n'),
|
||||
"target": "/var/www/cgi-bin/env.cgi",
|
||||
"mode": "0755"
|
||||
}
|
||||
],
|
||||
@@ -98,20 +92,17 @@ done
|
||||
c = infamy.Container(target)
|
||||
until(lambda: c.running(NAME), attempts=60)
|
||||
|
||||
with test.step("Verify environment variables are available via HTTP"):
|
||||
_, hport = env.ltop.xlate("host", "data")
|
||||
url = infamy.Furl(f"http://{DUTIP}:8080/env.html")
|
||||
with infamy.IsolatedMacVlan(hport) as ns:
|
||||
ns.addip(OURIP)
|
||||
|
||||
with infamy.IsolatedMacVlan(hport) as ns:
|
||||
ns.addip(OURIP)
|
||||
with test.step("Verify basic connectivity to data interface"):
|
||||
ns.must_reach(DUTIP)
|
||||
|
||||
with test.step("Verify basic connectivity to data interface"):
|
||||
ns.must_reach(DUTIP)
|
||||
with test.step("Verify environment variables in CGI response"):
|
||||
expected_strings = []
|
||||
for var in ENV_VARS:
|
||||
expected_strings.append(f'{var["key"]}={var["value"]}')
|
||||
|
||||
with test.step("Verify environment variables in HTTP response"):
|
||||
until(lambda: url.nscheck(ns, "TEST_VAR=\"hello-world\""), attempts=10)
|
||||
until(lambda: url.nscheck(ns, "APP_PORT=8080"), attempts=10)
|
||||
until(lambda: url.nscheck(ns, "DEBUG_MODE=\"true\""), attempts=10)
|
||||
until(lambda: url.nscheck(ns, "PATH_WITH_SPACES=\"/path with spaces/test\""), attempts=10)
|
||||
until(lambda: url.nscheck(ns, expected_strings))
|
||||
|
||||
test.succeed()
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
- name: container_enabled
|
||||
case: container_enabled/test.py
|
||||
|
||||
# Disabled for v25.08, new/unstable
|
||||
# - name: container_environment
|
||||
# case: container_environment/test.py
|
||||
- name: container_environment
|
||||
case: container_environment/test.py
|
||||
|
||||
- name: container_bridge
|
||||
case: container_bridge/test.py
|
||||
|
||||
@@ -2,6 +2,5 @@
|
||||
- name: mdns_enable_disable
|
||||
case: mdns_enable_disable/test.py
|
||||
|
||||
# Disabled for v25.08, unstable
|
||||
# - name: mdns_allow_deny
|
||||
# case: mdns_allow_deny/test.py
|
||||
- name: mdns_allow_deny
|
||||
case: mdns_allow_deny/test.py
|
||||
|
||||
@@ -10,22 +10,55 @@ with three scenarios:
|
||||
3. Allow p1 and p3, deny p2 and p3, traffic only on p1
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
import re
|
||||
import infamy
|
||||
from infamy.util import parallel
|
||||
|
||||
|
||||
def mdns_scan(tgt):
|
||||
"""Trigger Avahi to send traffic on allowed interfaces"""
|
||||
time.sleep(2)
|
||||
tgt.runsh("logger -t scan 'calling avahi-browse ...'")
|
||||
tgt.runsh("avahi-browse -lat")
|
||||
def mdns_scan():
|
||||
"""Start packet captures, trigger mDNS scan, return capture results"""
|
||||
pcap1 = ns1.pcap("host 10.0.1.1 and port 5353")
|
||||
pcap2 = ns2.pcap("host 10.0.2.1 and port 5353")
|
||||
pcap3 = ns3.pcap("host 10.0.3.1 and port 5353")
|
||||
|
||||
with pcap1, pcap2, pcap3:
|
||||
ssh.runsh("logger -t scan 'calling avahi-browse ...'")
|
||||
ssh.runsh("avahi-browse -lat")
|
||||
|
||||
def has_packets(output):
|
||||
if not output:
|
||||
return False
|
||||
lines = output.strip().split('\n')
|
||||
m = re.search(r'^(\d+) packets.*', lines[1])
|
||||
if m and int(m.group(1)) > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
out1 = pcap1.tcpdump("--count")
|
||||
out2 = pcap2.tcpdump("--count")
|
||||
out3 = pcap3.tcpdump("--count")
|
||||
|
||||
return (has_packets(out1), has_packets(out2), has_packets(out3))
|
||||
|
||||
|
||||
def check(ns, expr, must):
|
||||
"""Wrap netns.must_receive() with common defaults"""
|
||||
return ns.must_receive(expr, timeout=3, must=must)
|
||||
def check(expected, allow=None, deny=None):
|
||||
"""Execute complete mDNS test scenario"""
|
||||
try:
|
||||
dut.delete_xpath("/infix-services:mdns/interfaces")
|
||||
except ValueError:
|
||||
# Ignore if xpath doesn't exist (first run)
|
||||
pass
|
||||
|
||||
mdns_config = {"mdns": {"interfaces": {}}}
|
||||
if allow:
|
||||
mdns_config["mdns"]["interfaces"]["allow"] = allow
|
||||
if deny:
|
||||
mdns_config["mdns"]["interfaces"]["deny"] = deny
|
||||
|
||||
dut.put_config_dict("infix-services", mdns_config)
|
||||
|
||||
actual = mdns_scan()
|
||||
if actual != tuple(expected):
|
||||
raise AssertionError(f"Expected {expected}, got {actual}")
|
||||
|
||||
|
||||
with infamy.Test() as test:
|
||||
@@ -45,58 +78,41 @@ with infamy.Test() as test:
|
||||
{
|
||||
"ietf-interfaces": {
|
||||
"interfaces": {
|
||||
"interface": [
|
||||
{
|
||||
"name": p1,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [
|
||||
{
|
||||
"ip": "10.0.1.1",
|
||||
"prefix-length": 24
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
"name": p2,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [
|
||||
{
|
||||
"ip": "10.0.2.1",
|
||||
"prefix-length": 24
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
"name": p3,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [
|
||||
{
|
||||
"ip": "10.0.3.1",
|
||||
"prefix-length": 24
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
},
|
||||
]
|
||||
}
|
||||
"interface": [{
|
||||
"name": p1,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": "10.0.1.1",
|
||||
"prefix-length": 24
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
"name": p2,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": "10.0.2.1",
|
||||
"prefix-length": 24
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
"name": p3,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": "10.0.3.1",
|
||||
"prefix-length": 24
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
"ietf-system": {
|
||||
"system": {
|
||||
"hostname": "dut"
|
||||
}
|
||||
"system": {"hostname": "dut"}
|
||||
},
|
||||
"infix-services": {
|
||||
"mdns": {
|
||||
"enabled": True
|
||||
}
|
||||
"mdns": {"enabled": True}
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -108,53 +124,16 @@ with infamy.Test() as test:
|
||||
ns2.addip("10.0.2.2")
|
||||
ns3.addip("10.0.3.2")
|
||||
|
||||
EXPR1 = "host 10.0.1.1 and port 5353"
|
||||
EXPR2 = "host 10.0.2.1 and port 5353"
|
||||
EXPR3 = "host 10.0.3.1 and port 5353"
|
||||
|
||||
with test.step("Allow mDNS on a single interface: p2"):
|
||||
dut.put_config_dict("infix-services", {
|
||||
"mdns": {
|
||||
"interfaces": {
|
||||
"allow": [p2],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
parallel(lambda: mdns_scan(ssh),
|
||||
lambda: check(ns1, EXPR1, False),
|
||||
lambda: check(ns2, EXPR2, True),
|
||||
lambda: check(ns3, EXPR3, False))
|
||||
# p1:no, p2:yes, p3:no
|
||||
check([False, True, False], allow=[p2])
|
||||
|
||||
with test.step("Deny mDNS on a single interface: p2"):
|
||||
dut.delete_xpath("/infix-services:mdns/interfaces")
|
||||
dut.put_config_dict("infix-services", {
|
||||
"mdns": {
|
||||
"interfaces": {
|
||||
"deny": [p2],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
parallel(lambda: mdns_scan(ssh),
|
||||
lambda: check(ns1, EXPR1, True),
|
||||
lambda: check(ns2, EXPR2, False),
|
||||
lambda: check(ns3, EXPR3, True))
|
||||
# p1:yes, p2:no, p3:yes
|
||||
check([True, False, True], deny=[p2])
|
||||
|
||||
with test.step("Allow mDNS on p1, p3 deny on p2, p3"):
|
||||
dut.delete_xpath("/infix-services:mdns/interfaces")
|
||||
dut.put_config_dict("infix-services", {
|
||||
"mdns": {
|
||||
"interfaces": {
|
||||
"allow": [p1, p3],
|
||||
"deny": [p2, p3],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
parallel(lambda: mdns_scan(ssh),
|
||||
lambda: check(ns1, EXPR1, True),
|
||||
lambda: check(ns2, EXPR2, False),
|
||||
lambda: check(ns3, EXPR3, False))
|
||||
# p1:yes, p2:no, p3:no (deny overrides allow)
|
||||
check([True, False, False], allow=[p1, p3], deny=[p2, p3])
|
||||
|
||||
test.succeed()
|
||||
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
import infamy
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Discover topology and attach to available DUTs"):
|
||||
env = infamy.Env(False)
|
||||
ctrl = env.ptop.get_ctrl()
|
||||
duts = {}
|
||||
duts_state = {}
|
||||
for ix in env.ptop.get_infixen():
|
||||
cport, ixport = env.ptop.get_mgmt_link(ctrl, ix)
|
||||
print(f"Attaching to {ix}:{ixport} via {ctrl}:{cport}")
|
||||
duts[ix] = env.attach(ix, ixport)
|
||||
|
||||
with test.step("Verify bootorder"):
|
||||
for name, tgt in duts.items():
|
||||
expected = env.ptop.get_expected_boot(name)
|
||||
running = tgt.get_data("/ietf-system:system-state")
|
||||
running = running['system-state']['software']['booted']
|
||||
print(f"{name}: booted: {running} expected: {expected}")
|
||||
|
||||
if running != expected:
|
||||
test.fail()
|
||||
test.succeed()
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
import infamy
|
||||
import os
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Discover topology and attach to available DUTs"):
|
||||
env = infamy.Env(False)
|
||||
ctrl = env.ptop.get_ctrl()
|
||||
duts = {}
|
||||
duts_state = {}
|
||||
for ix in env.ptop.get_infixen():
|
||||
cport, ixport = env.ptop.get_mgmt_link(ctrl, ix)
|
||||
print(f"Attaching to {ix}:{ixport} via {ctrl}:{cport}")
|
||||
duts[ix] = env.attach(ix, ixport)
|
||||
|
||||
with test.step("Verify software version"):
|
||||
expected=os.environ.get("VERSION")
|
||||
for name, tgt in duts.items():
|
||||
running = tgt.get_data("/ietf-system:system-state")
|
||||
running = running['system-state']['platform']['os-version']
|
||||
print(f"{name}: booted: {running} expected {expected}")
|
||||
if running != expected:
|
||||
test.fail()
|
||||
test.succeed()
|
||||
@@ -13,27 +13,4 @@ with infamy.Test() as test:
|
||||
else:
|
||||
print(f"Specify PYTHONHASHSEED={seed} to reproduce this test environment")
|
||||
|
||||
with test.step("Discover topology and attach to available DUTs"):
|
||||
env = infamy.Env(False)
|
||||
ctrl = env.ptop.get_ctrl()
|
||||
|
||||
duts = {}
|
||||
for ix in env.ptop.get_infixen():
|
||||
cport, ixport = env.ptop.get_mgmt_link(ctrl, ix)
|
||||
print(f"Attaching to {ix}:{ixport} via {ctrl}:{cport}")
|
||||
duts[ix] = env.attach(ix, ixport)
|
||||
|
||||
with test.step("Log running software versions"):
|
||||
for name, tgt in duts.items():
|
||||
sys = tgt.get_data("/ietf-system:system-state")
|
||||
sw = sys["system-state"]["software"]
|
||||
plt = sys["system-state"]["platform"]
|
||||
|
||||
print(f"{name}:")
|
||||
for k,v in plt.items():
|
||||
print(f" {k:<16s} {v}")
|
||||
|
||||
for k in ("compatible", "booted"):
|
||||
print(f" {k:<16s} {sw[k]}")
|
||||
|
||||
test.succeed()
|
||||
|
||||
@@ -184,6 +184,7 @@ while getopts "b:cCDf:hiKp:q:rt:" opt; do
|
||||
kvm=
|
||||
;;
|
||||
p)
|
||||
version=$(unsquashfs -cat $OPTARG manifest.raucm | awk -F'=' '/^version=/ {print $2}')
|
||||
INFAMY_ARGS="$INFAMY_ARGS -p $OPTARG"
|
||||
;;
|
||||
q)
|
||||
@@ -234,6 +235,7 @@ if [ "$containerize" ]; then
|
||||
--env NINEPM_PROJ_CONFIG="$NINEPM_PROJ_CONFIG" \
|
||||
--env QENETH_PATH="$testdir/templates:$testdir" \
|
||||
--env PS1="$(build_ps1)" \
|
||||
--env VERSION="$version" \
|
||||
$extra_env \
|
||||
--expose 9001-9010 --publish-all \
|
||||
-v "$HOME/.infix/.ash_history":/root/.ash_history \
|
||||
|
||||
+55
-9
@@ -1,26 +1,72 @@
|
||||
"""Fugly URL fetcher"""
|
||||
"""Simple HTTP URL fetcher and content checker
|
||||
|
||||
This module provides the a lightweight wrapper around urllib for testing
|
||||
HTTP endpoints. It's designed specifically for test scenarios where you
|
||||
need to:
|
||||
|
||||
- Fetch HTTP content and verify it contains expected strings
|
||||
- Perform checks from within network namespaces
|
||||
- Validate multiple content patterns in a single request
|
||||
- Handle network errors gracefully in test environments
|
||||
|
||||
Example usage:
|
||||
# Single needle check
|
||||
url = Furl("http://example.com/api")
|
||||
if url.check("success"):
|
||||
print("API returned success")
|
||||
|
||||
# Multiple needle check (all must match)
|
||||
if url.check(["user: alice", "status: active", "role: admin"]):
|
||||
print("All user details found")
|
||||
|
||||
# Check from network namespace
|
||||
with IsolatedMacVlan("eth0") as ns:
|
||||
if url.nscheck(ns, "expected content"):
|
||||
print("Content verified from namespace")
|
||||
"""
|
||||
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
|
||||
class Furl:
|
||||
"""Furl wraps urllib in a way similar to curl"""
|
||||
def __init__(self, url):
|
||||
"""Create new URL checker"""
|
||||
self.url = urllib.parse.quote(url, safe='/:')
|
||||
|
||||
def check(self, needle, timeout=10):
|
||||
"""Connect to web server URL, fetch body and check for needle"""
|
||||
def check(self, needles, timeout=10):
|
||||
"""Connect to web server URL, fetch body and check for needle(s)
|
||||
|
||||
Args:
|
||||
needles: String or list of strings to search for in response
|
||||
timeout: Request timeout in seconds
|
||||
|
||||
Returns:
|
||||
bool: True if all needles found in response, False otherwise
|
||||
"""
|
||||
# Backwards compat, make needles a list for uniform processing
|
||||
if isinstance(needles, str):
|
||||
needles = [needles]
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(self.url, timeout=timeout) as response:
|
||||
text = response.read().decode('utf-8')
|
||||
#print(text)
|
||||
return needle in text
|
||||
except urllib.error.URLError as _:
|
||||
return all(needle in text for needle in needles)
|
||||
except urllib.error.URLError:
|
||||
return False
|
||||
except ConnectionResetError:
|
||||
return False
|
||||
|
||||
def nscheck(self, netns, needle):
|
||||
""""Call check() from netns"""
|
||||
return netns.call(lambda: self.check(needle))
|
||||
def nscheck(self, netns, needles):
|
||||
""""Call check() from netns
|
||||
|
||||
Args:
|
||||
netns: Network namespace to call from
|
||||
needles: String or list of strings to search for in response
|
||||
|
||||
Returns:
|
||||
bool: True if all needles found in response, False otherwise
|
||||
"""
|
||||
return netns.call(lambda: self.check(needles))
|
||||
|
||||
@@ -150,7 +150,6 @@ class Device(Transport):
|
||||
url = f"{self.yang_url}/{schema_name}"
|
||||
data = self._get_raw(url=url, parse=False)
|
||||
|
||||
sys.stdout.write(f"Downloading YANG model {schema_name} ...\r\033[K")
|
||||
data = data.decode('utf-8')
|
||||
with open(f"{yangdir}/{schema_name}", 'w') as json_file:
|
||||
json_file.write(data)
|
||||
|
||||
@@ -114,3 +114,13 @@ def ospf_is_area_nssa(target, area_id):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def ospf_has_neighbors(target):
|
||||
ospf = _get_ospf_status(target)
|
||||
for area in ospf.get("areas", {}).get("area", []):
|
||||
for interface in area.get("interfaces", {}).get("interface", []):
|
||||
if interface.get("neighbors"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -131,6 +131,13 @@ class Topology:
|
||||
|
||||
return qstrip(password) if password is not None else "admin"
|
||||
|
||||
def get_expected_boot(self, node):
|
||||
n = self.dotg.get_node(node)
|
||||
b = n[0] if n else {}
|
||||
boot = b.get("expected_boot")
|
||||
|
||||
return _qstrip(boot)
|
||||
|
||||
def get_link(self, src, dst, flt=lambda _: True):
|
||||
es = self.g.get_edge_data(src, dst)
|
||||
for e in es.values():
|
||||
@@ -150,7 +157,6 @@ class Topology:
|
||||
def get_infixen(self):
|
||||
return self.get_nodes(lambda _, attrs: compatible(attrs, {"requires": {"infix"}}))
|
||||
|
||||
|
||||
def get_attr(self, name, default=None):
|
||||
return _qstrip(self.dotg.get_attributes().get(name, default))
|
||||
|
||||
|
||||
+16
-7
@@ -1,9 +1,10 @@
|
||||
base-dir := $(lastword $(subst :, ,$(BR2_EXTERNAL)))
|
||||
test-dir := $(BR2_EXTERNAL_INFIX_PATH)/test
|
||||
test-dir ?= $(BR2_EXTERNAL_INFIX_PATH)/test
|
||||
ninepm := $(BR2_EXTERNAL_INFIX_PATH)/test/9pm/9pm.py
|
||||
NINEPM_PROJ_CONF ?= $(BR2_EXTERNAL_INFIX_PATH)/test/9pm-proj.yaml
|
||||
spec-dir := $(test-dir)/spec
|
||||
test-specification := $(O)/images/test-specification.pdf
|
||||
|
||||
test-specification := $(BINARIES_DIR)/test-specification.pdf
|
||||
test-report := $(BINARIES_DIR)/test-report.pdf
|
||||
UNIT_TESTS ?= $(test-dir)/case/all-repo.yaml $(test-dir)/case/all-unit.yaml
|
||||
TESTS ?= $(test-dir)/case/all.yaml
|
||||
|
||||
@@ -14,8 +15,7 @@ mode-qeneth := -q $(test-dir)/virt/quad
|
||||
mode-host := -t $(or $(TOPOLOGY),/etc/infamy.dot)
|
||||
mode-run := -t $(BINARIES_DIR)/qemu.dot
|
||||
mode := $(mode-$(TEST_MODE))
|
||||
|
||||
INFIX_IMAGE_ID := $(call qstrip,$(INFIX_IMAGE_ID))
|
||||
INFIX_IMAGE_ID := $(call qstrip,$(INFIX_IMAGE_ID))
|
||||
binaries-$(ARCH) := $(addprefix $(INFIX_IMAGE_ID),.img -disk.qcow2)
|
||||
pkg-$(ARCH) := -p $(O)/images/$(addprefix $(INFIX_IMAGE_ID),.pkg)
|
||||
binaries-x86_64 += OVMF.fd
|
||||
@@ -34,9 +34,18 @@ test-sh:
|
||||
|
||||
test-spec:
|
||||
@esc_infix_name="$(echo $(INFIX_NAME) | sed 's/\//\\\//g')"; \
|
||||
sed 's/{REPLACE}/$(subst ",,$(esc_infix_name)) $(INFIX_VERSION)/' $(spec-dir)/Readme.adoc.in > $(spec-dir)/Readme.adoc
|
||||
sed 's/{REPLACE}/$(subst ",,$(esc_infix_name)) $(INFIX_VERSION)/' \
|
||||
$(spec-dir)/Readme.adoc.in > $(spec-dir)/Readme.adoc
|
||||
@$(spec-dir)/generate_spec.py -s $(test-dir)/case/all.yaml -r $(BR2_EXTERNAL_INFIX_PATH)
|
||||
@asciidoctor-pdf --failure-level INFO --theme $(spec-dir)/theme.yml -a pdf-fontsdir=$(spec-dir)/fonts -o $(test-specification) $(spec-dir)/Readme.adoc
|
||||
@asciidoctor-pdf --failure-level INFO --theme $(spec-dir)/theme.yml \
|
||||
-a pdf-fontsdir=$(spec-dir)/fonts \
|
||||
-o $(test-specification) $(spec-dir)/Readme.adoc
|
||||
|
||||
test-report:
|
||||
asciidoctor-pdf --theme $(spec-dir)/theme.yml \
|
||||
-a logo="image:$(LOGO)" \
|
||||
-a pdf-fontsdir=$(spec-dir)/fonts \
|
||||
-o $(test-report) $(test-dir)/.log/last/report.adoc
|
||||
|
||||
# Unit tests run with random (-r) hostname and container name to
|
||||
# prevent race conditions when running in CI environments.
|
||||
|
||||
@@ -25,6 +25,7 @@ graph "dual" {
|
||||
label="{ <e1> e1 | <e2> e2 | <e3> e3 } | dut1 | { <e4> e4 | <e5> e5 | <e6> e6 }",
|
||||
pos="10,18!",
|
||||
provides="infix",
|
||||
expected_boot="primary",
|
||||
qn_console=9001,
|
||||
qn_mem="384M",
|
||||
qn_usb="dut1.usb"
|
||||
@@ -33,6 +34,7 @@ graph "dual" {
|
||||
label="{ <e1> e1 | <e2> e2 | <e3> e3 } | dut2 | { <e4> e4 | <e5> e5 | <e6> e6 }",
|
||||
pos="10,12!",
|
||||
provides="infix",
|
||||
expected_boot="primary",
|
||||
qn_console=9002,
|
||||
qn_mem="384M",
|
||||
qn_usb="dut2.usb"
|
||||
|
||||
@@ -24,6 +24,7 @@ graph "quad" {
|
||||
label="{ <e1> e1 | <e2> e2 | <e3> e3 | <e4> e4 } | dut1 | { <e5> e5 | <e6> e6 | <e7> e7 | <e8> e8}",
|
||||
pos="10,30!",
|
||||
provides="infix",
|
||||
expected_boot="primary",
|
||||
qn_console=9001,
|
||||
qn_mem="384M",
|
||||
qn_usb="dut1.usb"
|
||||
@@ -32,6 +33,7 @@ graph "quad" {
|
||||
label="{ <e1> e1 | <e2> e2 | <e3> e3 | <e4> e4 } | dut2 | { <e5> e5 | <e6> e6 | <e7> e7 | <e8> e8}",
|
||||
pos="0,20!",
|
||||
provides="infix",
|
||||
expected_boot="primary",
|
||||
qn_console=9002,
|
||||
qn_mem="384M",
|
||||
qn_usb="dut2.usb"
|
||||
@@ -40,6 +42,7 @@ graph "quad" {
|
||||
label="{ <e1> e1 | <e2> e2 | <e3> e3 | <e4> e4 } | dut3 | { <e5> e5 | <e6> e6 | <e7> e7 | <e8> e8}",
|
||||
pos="0,10!",
|
||||
provides="infix",
|
||||
expected_boot="primary",
|
||||
qn_console=9003,
|
||||
qn_mem="384M",
|
||||
qn_usb="dut3.usb"
|
||||
@@ -49,6 +52,7 @@ graph "quad" {
|
||||
label="{ <e1> e1 | <e2> e2 | <e3> e3 | <e4> e4 } | dut4 | { <e5> e5 | <e6> e6 | <e7> e7 | <e8> e8}",
|
||||
pos="10,0!",
|
||||
provides="infix",
|
||||
expected_boot="primary",
|
||||
qn_console=9004,
|
||||
qn_mem="384M",
|
||||
qn_usb="dut4.usb"
|
||||
|
||||
Reference in New Issue
Block a user