Merge pull request #820 from kernelkit/ospf-container

Ospf container
This commit is contained in:
Mattias Walström
2024-11-25 15:08:01 +01:00
committed by GitHub
28 changed files with 2512 additions and 101 deletions
+1
View File
@@ -8,3 +8,4 @@
/test/.venv
/test/.log
/local.mk
/test/spec/Readme.adoc
+7 -7
View File
@@ -27,13 +27,13 @@ it maintenance-free. Configuration and data, e.g, containers, is stored
on separate partitions to ensure complete separation from system files
and allow for seamless backup, restore, and provisioning.
In itself Infix is perfectly suited for dedicated networking tasks and
native support for Docker containers provides a versatile platform that
can easily be adapted to any customer need. Be it legacy applications,
network protocols, process monitoring, or edge data analysis, it can run
close to end equipment. Either directly connected on dedicated Ethernet
ports or indirectly using virtual network cables to exist on the same
LAN as other connected equipment.
In itself, Infix is perfectly suited for dedicated networking tasks, and
with native support for Docker containers, the operating system provides
a versatile platform that can easily be adapted to any customer need.
Be it legacy applications, network protocols, process monitoring, or
edge data analysis, it can run close to end equipment. Either directly
connected on dedicated Ethernet ports or indirectly using virtual
network cables to exist on the same LAN as other connected equipment.
The simple design of Infix provides complete control over both system
and data, minimal cognitive burden, and makes it incredibly easy to get
+2 -1
View File
@@ -33,6 +33,7 @@ CONFIG_KALLSYMS_ALL=y
CONFIG_PROFILING=y
CONFIG_SMP=y
CONFIG_EFI=y
CONFIG_KPROBES=y
# CONFIG_GCC_PLUGINS is not set
CONFIG_MODULES=y
CONFIG_MODULE_UNLOAD=y
@@ -264,5 +265,5 @@ CONFIG_PANIC_ON_OOPS=y
CONFIG_PANIC_TIMEOUT=20
CONFIG_DETECT_HUNG_TASK=y
CONFIG_BOOTPARAM_HUNG_TASK_PANIC=y
# CONFIG_FTRACE is not set
CONFIG_FUNCTION_TRACER=y
CONFIG_UNWINDER_FRAME_POINTER=y
+6
View File
@@ -66,8 +66,14 @@ int core_post_hook(sr_session_ctx_t *session, uint32_t sub_id, const char *modul
num_changes = 0;
return SR_ERR_OK;
case SR_EV_DONE:
if (num_changes <= 0) {
ERROR("BUG in core_post_hook(): callback tracking out of sync");
abort();
}
if (--num_changes == 0)
break;
return SR_ERR_OK;
default:
ERROR("core_post_hook() should not be called with event %s", ev2str(event));
+4 -2
View File
@@ -492,10 +492,12 @@ def add_ospf(control_protocols):
neighbor = {}
neighbor["neighbor-router-id"] = neigh["neighborIp"]
neighbor["address"] = neigh["ifaceAddress"]
neighbor["dr-router-id"] = neigh["routerDesignatedId"]
neighbor["bdr-router-id"] = neigh["routerDesignatedBackupId"]
neighbor["dead-timer"] = neigh["routerDeadIntervalTimerDueMsec"]
neighbor["state"] = frr_to_ietf_neighbor_state(neigh["nbrState"])
if neigh.get("routerDesignatedId"):
neighbor["dr-router-id"] = neigh["routerDesignatedId"]
if neigh.get("routerDesignatedBackupId"):
neighbor["bdr-router-id"] = neigh["routerDesignatedBackupId"]
neighbors.append(neighbor)
interface["ietf-ospf:neighbors"] = {}
+3
View File
@@ -28,3 +28,6 @@
- name: infix-services
suite: infix_services/infix_services.yaml
- name: use-cases
suite: use_case/use_case.yml
+2 -26
View File
@@ -4,16 +4,11 @@ Upgrade
Verify it is possible to upgrade.
"""
import concurrent.futures
import functools
import http.server
import os
import socket
import time
import netifaces
import infamy
import infamy.file_server as srv
SRVPORT = 8008
@@ -27,25 +22,6 @@ PKGPATH = os.path.join(
"package"
)
class FileServer(http.server.HTTPServer):
class RequestHandler(http.server.SimpleHTTPRequestHandler):
def log_message(*args, **kwargs):
pass
address_family = socket.AF_INET6
def __init__(self, server_address, directory):
rh = functools.partial(FileServer.RequestHandler, directory=directory)
self.__tp = concurrent.futures.ThreadPoolExecutor(max_workers=1)
super().__init__(server_address, rh)
def __enter__(self):
self.__tp.submit(self.serve_forever)
def __exit__(self, _, __, ___):
self.shutdown()
self.__tp.shutdown()
with infamy.Test() as test:
with test.step("Set up topology and attach to target DUT"):
env = infamy.Env()
@@ -66,7 +42,7 @@ with infamy.Test() as test:
hip = netifaces.ifaddresses(hport)[netifaces.AF_INET6][0]["addr"]
hip = hip.replace(f"%{hport}", f"%{tport}")
with FileServer(("::", SRVPORT), BUNDLEDIR):
with srv.FileServer(("::", SRVPORT), BUNDLEDIR):
with test.step("Start installation of selected package"):
print(f"Installing {os.path.basename(env.args.package)}")
+8
View File
@@ -0,0 +1,8 @@
:testgroup:
== Use case tests
Contains test of multiple parts of Infix together.
<<<
include::ospf_container/Readme.adoc[]
@@ -0,0 +1,98 @@
=== OSPF Container
==== Description
This use-case test verifies connectivity in an OSPF network to services
running as hosted containers inside each router.
NOTE: The _Controller_, _ABR_, and `data` connections are simulated by
the test PC. The `ringN` ports are connected to other DUTs via the test
PC, which can act as a link breaker.
.Use-case overview.
[#img-overview]
ifdef::topdoc[]
image::../../test/case/use_case/ospf_container/overview.svg[]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::ospf_container/overview.svg[]
endif::testgroup[]
ifndef::testgroup[]
image::overview.svg[]
endif::testgroup[]
endif::topdoc[]
The DUTs are connected in a routed topology inside their own OSPF area.
A single area border router (ABR) is used to access the controller
network in OSPF area 0. Each router also has "test point" connections
where the controller can attach other than its connection in area 0.
- The ringN ports are intended to be connected to neighboring DUTs, but
may at each end of the bus be used as test points
- The data ports are intended to be test points for verifying
connectivity with container B via br1
- The uplink ports are for connecting to the ABR, at least one of the
DUTs should not have a connection to the ABR, this to verify routing
via another DUT
- Area 1 is 10.1.Rn.0/16, and each router is assigned a /24
Each DUT hosts one application container and one system container, all
have the same setup, with only different subnets assigned. A third
container is used to manipulate the firewall of each DUT, providing port
forwarding and masquerading.
Devices attached to the first bridge, `br0`, are supposed to be easily
accesible using IPv4, so internally they use IPv4 too, and to avoid any
risk of clashing with external IP subnets, IPv4 link-local addresses are
employed: `br0` request 169.254.1.1, so the second container (B) always
can reach it, the first container (A) reqquest 169.254.1.2 and the
second (B) request 169.254.1.3. The network for devices attached to the
second bridge, `br1`, only use IPv6 link-local addresses.
.Internal network setup, here router R1 on subnet 10.1.1.1/24.
[#img-setup]
ifdef::topdoc[]
image::../../test/case/use_case/ospf_container/internal-network.svg[Internal networks]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::ospf_container/internal-network.svg[Internal networks]
endif::testgroup[]
ifndef::testgroup[]
image::internal-network.svg[Internal networks]
endif::testgroup[]
endif::topdoc[]
- *Container A* runs a very basic web server, it runs on port 80 inside
the container, and `br0`, but is accessible outside on port 8080.
The controller connects to each of these servers from OSPF area 0.
For the controller to be able to distinguish between the servers,
they all serve slightly different content
- *Container B* runs a complete system with an SSH server. During the
test, the controller connects to this container using the `data` port
to ensure the container can access all other parts of the network.
To distinguish between the different container B's, each container
will have a unique hostname derived from the chassis MAC address
==== Topology
ifdef::topdoc[]
image::../../test/case/use_case/ospf_container/topology.svg[OSPF Container topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::ospf_container/topology.svg[OSPF Container topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[OSPF Container topology]
endif::testgroup[]
endif::topdoc[]
==== Test sequence
. Set up topology and attach to target DUTs
. Configure DUTs
. Wait for all routers to peer
. Verify ABR:data can access container A on R1 (10.1.1.101)
. Verify ABR:data can access container A on R2 (10.1.2.101)
. Verify ABR:data can access container A on R3 (10.1.3.101)
<<<
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 720 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 406 KiB

+644
View File
@@ -0,0 +1,644 @@
#!/usr/bin/env python3
r"""OSPF Container
This use-case test verifies connectivity in an OSPF network to services
running as hosted containers inside each router.
NOTE: The _Controller_, _ABR_, and `data` connections are simulated by
the test PC. The `ringN` ports are connected to other DUTs via the test
PC, which can act as a link breaker.
.Use-case overview.
[#img-overview]
image::overview.svg[]
The DUTs are connected in a routed topology inside their own OSPF area.
A single area border router (ABR) is used to access the controller
network in OSPF area 0. Each router also has "test point" connections
where the controller can attach other than its connection in area 0.
- The ringN ports are intended to be connected to neighboring DUTs, but
may at each end of the bus be used as test points
- The data ports are intended to be test points for verifying
connectivity with container B via br1
- The uplink ports are for connecting to the ABR, at least one of the
DUTs should not have a connection to the ABR, this to verify routing
via another DUT
- Area 1 is 10.1.Rn.0/16, and each router is assigned a /24
Each DUT hosts one application container and one system container, all
have the same setup, with only different subnets assigned. A third
container is used to manipulate the firewall of each DUT, providing port
forwarding and masquerading.
Devices attached to the first bridge, `br0`, are supposed to be easily
accesible using IPv4, so internally they use IPv4 too, and to avoid any
risk of clashing with external IP subnets, IPv4 link-local addresses are
employed: `br0` request 169.254.1.1, so the second container (B) always
can reach it, the first container (A) reqquest 169.254.1.2 and the
second (B) request 169.254.1.3. The network for devices attached to the
second bridge, `br1`, only use IPv6 link-local addresses.
.Internal network setup, here router R1 on subnet 10.1.1.1/24.
[#img-setup]
image::internal-network.svg[Internal networks]
- *Container A* runs a very basic web server, it runs on port 80 inside
the container, and `br0`, but is accessible outside on port 8080.
The controller connects to each of these servers from OSPF area 0.
For the controller to be able to distinguish between the servers,
they all serve slightly different content
- *Container B* runs a complete system with an SSH server. During the
test, the controller connects to this container using the `data` port
to ensure the container can access all other parts of the network.
To distinguish between the different container B's, each container
will have a unique hostname derived from the chassis MAC address
"""
import infamy
import infamy.util as util
import infamy.route as route
from infamy.furl import Furl
BODY = "<html><body><p>Router responding</p></body></html>"
def create_vlan_bridge(ns):
return ns.runsh("""
ip link add dev br0 type bridge
ip link set dev br0 up
ip link set dev iface1 up
ip link set dev iface2 up
ip link set dev iface1 master br0
ip link set dev iface2 master br0
ip link set dev br0 type bridge vlan_filtering 1
bridge vlan del dev br0 vid 1 self
bridge vlan del dev iface1 vid 1
bridge vlan del dev iface2 vid 1
bridge vlan add dev br0 vid 8 self
bridge vlan add dev iface1 vid 8
bridge vlan add dev iface2 vid 8
""")
def config_generic(target, router, ring1, ring2, link):
router_ip=f"10.1.{router}.1"
link_ip=f"10.1.{router}.{101}"
firewall_config = util.to_binary(f"""#!/usr/sbin/nft -f
flush ruleset
define UPLINK = "{link}"
define WIP = 169.254.1.2
"""
"""
table ip nat {
chain prerouting {
type nat hook prerouting priority filter; policy accept;
iif $UPLINK tcp dport 8080 dnat to 169.254.1.2:91
iif "br0" ip daddr 169.254.1.2 dnat to 169.254.1.1
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oif $UPLINK masquerade
oif "br0" ip daddr $WIP snat to 169.254.1.1
}
}
""")
data = util.to_binary(BODY)
target.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [
{
"name": "lo",
"type": "infix-if-type:loopback",
"ietf-ip:ipv4": {
"address": [
{
"ip": "127.0.0.1",
"prefix-length": 8
}
]
},
"ietf-ip:ipv6": {
"address": [
{
"ip": "::1",
"prefix-length": 128
}
]
}
},
{
"name": ring1,
"ietf-ip:ipv6": {},
"infix-interfaces:bridge-port": {
"bridge": "br0"
}
},
{
"name": f"{ring1}.8",
"type": "infix-if-type:vlan",
"infix-interfaces:vlan": {
"tag-type": "ieee802-dot1q-types:c-vlan",
"id": 8,
"lower-layer-if": ring1
},
"ipv4": {
"address": [{
"ip": router_ip,
"prefix-length": 32
}]
}
},
{
"name": ring2,
"ietf-ip:ipv6": {},
"infix-interfaces:bridge-port": {
"bridge": "br0"
}
},
{
"name": f"{ring2}.8",
"type": "infix-if-type:vlan",
"infix-interfaces:vlan": {
"tag-type": "ieee802-dot1q-types:c-vlan",
"id": 8,
"lower-layer-if": ring2
},
"ipv4": {
"address": [{
"ip": router_ip,
"prefix-length": 32
}]
}
},
{
"name": link,
"ipv4": {
"address": [{
"ip": link_ip,
"prefix-length": 24
}],
"forwarding": True
},
"ietf-ip:ipv6": {}
},
{
"name": "br0",
"type": "infix-if-type:bridge",
"ietf-ip:ipv4": {
"enabled": True,
"forwarding": True,
"infix-ip:autoconf": {
"enabled": True,
"request-address": "169.254.1.1"
}
},
"ietf-ip:ipv6": {
"enabled": True
},
"infix-interfaces:bridge": {
"ieee-group-forward": [
"lldp"
]
}
},
{
"name": "br1",
"type": "infix-if-type:bridge",
"infix-interfaces:bridge": {
"vlans": {
"vlan": [
{
"vid": 6,
"untagged": [
"veth1b"
]
}
]
}
}
},
{
"name": "veth0a",
"type": "infix-if-type:veth",
"infix-interfaces:container-network": {
"type": "host",
},
"ipv4": {
"address": [
{
"ip": "169.254.1.2", # This only for test, use zeroconf in real-life
"prefix-length": 16
}
]
},
"infix-interfaces:veth": {
"peer": "veth0b"
},
"infix-interfaces:custom-phys-address": {
"chassis": {
"offset": "06:00:00:00:00:00"
}
}
},
{
"name": "veth0b",
"type": "infix-if-type:veth",
"infix-interfaces:bridge-port": {
"bridge": "br0"
},
"infix-interfaces:veth": {
"peer": "veth0a"
}
},
{
"name": "veth1a",
"type": "infix-if-type:veth",
"infix-interfaces:container-network": {
"type": "host"
},
"infix-interfaces:veth": {
"peer": "veth1b"
},
"infix-interfaces:custom-phys-address": {
"chassis": {
"offset": "06:00:00:00:00:00"
}
}
},
{
"name": "veth1b",
"type": "infix-if-type:veth",
"infix-interfaces:bridge-port": {
"bridge": "br1",
"pvid": 6
},
"infix-interfaces:veth": {
"peer": "veth1a"
}
},
{
"name": "veth2a",
"type": "infix-if-type:veth",
"infix-interfaces:container-network": {
"type": "host"
},
"infix-interfaces:veth": {
"peer": "veth2b"
},
"infix-interfaces:custom-phys-address": {
"chassis": {
"offset": "06:00:00:00:00:00"
}
}
},
{
"name": "veth2b",
"type": "infix-if-type:veth",
"infix-interfaces:bridge-port": {
"bridge": "br0"
},
"infix-interfaces:veth": {
"peer": "veth2a"
}
},
{
"name": "veth3a",
"type": "infix-if-type:veth",
"infix-interfaces:container-network": {
"type": "host"
},
"infix-interfaces:veth": {
"peer": "veth3b"
},
"infix-interfaces:custom-phys-address": {
"chassis": {
"offset": "06:00:00:00:00:00"
}
}
},
{
"name": "veth3b",
"type": "infix-if-type:veth",
"infix-interfaces:bridge-port": {
"bridge": "br1",
"pvid": 6
},
"infix-interfaces:veth": {
"peer": "veth3a"
}
}
]}
},
"infix-containers": {
"containers": {
"container": [
{
"name": "container-A",
"image": f"oci-archive:{infamy.Container.HTTPD_IMAGE}",
"command": "/usr/sbin/httpd -f -v -p 91",
"hostname": "web-container-%m",
"restart-policy": "retry",
"mount": [{
"name": "index.html",
"content": f"{data}",
"target": "/var/www/index.html"
}],
"network": {
"interface": [{
"name": "veth0a",
"option": [
"interface_name=br0",
]},
{
"name": "veth1a",
"option": [
"interface_name=br1"
]}
],
"publish": [ "91" ]
}
},
{
"name": "container-B",
"image": f"oci-archive:{infamy.Container.HTTPD_IMAGE}",
"hostname": "web-container-%m",
"privileged": True,
"restart-policy": "retry",
"network": {
"interface": [
{
"name": "veth2a",
"option": ["ip=169.254.1.3"] # This only for test, use zeroconf in real-life
},
{
"name": "veth3a"
}
]
}
},
{
"name": "firewall",
"image": f"oci-archive:{infamy.Container.NFTABLES_IMAGE}",
"network": {
"host": True
},
"mount": [{
"name": "nftables.conf",
"content": firewall_config,
"target": "/etc/nftables.conf"
}
],
"privileged": True
}
]
}
},
"ietf-routing": {
"routing": {
"control-plane-protocols": {
"control-plane-protocol": [{
"type": "infix-routing:ospfv2",
"name": "default",
"ietf-ospf:ospf": {
"explicit-router-id": router_ip,
"areas": {
"area": [{
"area-id": "0.0.0.1",
"area-type": "nssa-area",
"interfaces": {
"interface": [{
"name": f"{ring1}.8",
"interface-type": "point-to-point",
"enabled": True
},
{
"name": f"{ring2}.8",
"interface-type": "point-to-point",
"enabled": True
},
{
"name": link,
"enabled": True
}]
}
}]
}
}
}]
}
}
}
})
def config_abr(target, data, link1, link2, link3):
target.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [
{
"name": "lo",
"type": "infix-if-type:loopback",
"ietf-ip:ipv4": {
"address": [
{
"ip": "127.0.0.1",
"prefix-length": 8
}
]
},
"ietf-ip:ipv6": {
"address": [
{
"ip": "::1",
"prefix-length": 128
}
]
}
},
{
"name": data,
"ipv4": {
"forwarding": True,
"address": [{
"ip": "192.168.100.1",
"prefix-length": 24
}]
}
},
{
"name": link1,
"ipv4": {
"forwarding": True,
"address": [{
"ip": "10.1.1.100",
"prefix-length": 24
}]
}
},
{
"name": link2,
"ipv4": {
"forwarding": True,
"address": [{
"ip": "10.1.2.100",
"prefix-length": 24
}]
}
},
{
"name": link3,
"ipv4": {
"forwarding": True,
"address": [{
"ip": "10.1.3.100",
"prefix-length": 24
}]
}
}]
}
},
"ietf-routing": {
"routing": {
"control-plane-protocols": {
"control-plane-protocol": [
{
"type": "infix-routing:ospfv2",
"name": "default",
"ietf-ospf:ospf": {
"areas": {
"area": [
{
"area-id": "0.0.0.0",
"interfaces": {
"interface": [
{
"name": data,
"enabled": True
}
]
}
},
{
"area-id": "0.0.0.1",
"area-type": "nssa-area",
"interfaces": {
"interface":
[
{
"name": link1,
"enabled": True
},
{
"name": link2,
"enabled": True
},
{
"name": link3,
"enabled": True
},
]
}
}
]
}
}
}
]
}
}
}
})
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")
R3 = env.attach("R3", "mgmt")
ABR = env.attach("ABR", "mgmt")
if not R1.has_model("infix-containers"):
test.skip()
if not R2.has_model("infix-containers"):
test.skip()
if not R3.has_model("infix-containers"):
test.skip()
with test.step("Configure DUTs"):
_, R1ring1 = env.ltop.xlate("R1", "ring1")
_, R1ring2 = env.ltop.xlate("R1", "ring2")
_, R2ring1 = env.ltop.xlate("R2", "ring1")
_, R2ring2 = env.ltop.xlate("R2", "ring2")
_, R3ring1 = env.ltop.xlate("R3", "ring1")
_, R3ring2 = env.ltop.xlate("R3", "ring2")
_, hostR1ring1 = env.ltop.xlate("host", "R1ring1")
_, hostR1ring2 = env.ltop.xlate("host", "R1ring2")
_, hostR2ring1 = env.ltop.xlate("host", "R2ring1")
_, hostR2ring2 = env.ltop.xlate("host", "R2ring2")
_, hostR3ring1 = env.ltop.xlate("host", "R3ring1")
_, hostR3ring2 = env.ltop.xlate("host", "R3ring2")
_, R1link = env.ltop.xlate("R1", "link")
_, R2link = env.ltop.xlate("R2", "link")
_, R3link = env.ltop.xlate("R3", "link")
_, R1data = env.ltop.xlate("R1", "data")
_, R3data = env.ltop.xlate("R3", "data")
_, ABRdata = env.ltop.xlate("ABR", "data")
_, ABRlink1 = env.ltop.xlate("ABR", "link1")
_, ABRlink2 = env.ltop.xlate("ABR", "link2")
_, ABRlink3 = env.ltop.xlate("ABR", "link3")
util.parallel(lambda: config_generic(R1, 1, R1ring1, R1ring2, R1link),
lambda: config_generic(R2, 2, R2ring1, R2ring2, R2link),
lambda: config_generic(R3, 3, R3ring1, R3ring2, R3link),
lambda: config_abr(ABR, ABRdata, ABRlink1, ABRlink2, ABRlink3))
with infamy.IsolatedMacVlans({hostR1ring1: "iface1", hostR2ring2: "iface2"}) as sw1,\
infamy.IsolatedMacVlans({hostR2ring1: "iface1", hostR3ring2: "iface2"}) as sw2, \
infamy.IsolatedMacVlans({hostR3ring1: "iface1", hostR1ring2: "iface2"}) as sw3:
create_vlan_bridge(sw1)
create_vlan_bridge(sw2)
create_vlan_bridge(sw3)
#breakpoint()
_, hport0 = env.ltop.xlate("host", "data4")
with test.step("Wait for all routers to peer"):
util.until(lambda: route.ospf_get_neighbor(R1, "0.0.0.1", f"{R1ring1}.8", "10.1.2.1"), attempts=200)
util.until(lambda: route.ospf_get_neighbor(R1, "0.0.0.1", f"{R1ring2}.8", "10.1.3.1"), attempts=200)
util.until(lambda: route.ospf_get_neighbor(R2, "0.0.0.1", f"{R2ring1}.8", "10.1.3.1"), attempts=200)
util.until(lambda: route.ospf_get_neighbor(R2, "0.0.0.1", f"{R2ring2}.8", "10.1.1.1"), attempts=200)
util.until(lambda: route.ospf_get_neighbor(R3, "0.0.0.1", f"{R3ring1}.8", "10.1.1.1"), attempts=200)
util.until(lambda: route.ospf_get_neighbor(R3, "0.0.0.1", f"{R3ring2}.8", "10.1.2.1"), attempts=200)
util.until(lambda: route.ospf_get_neighbor(ABR, "0.0.0.1", ABRlink1, "10.1.1.1"), attempts=200)
util.until(lambda: route.ospf_get_neighbor(ABR, "0.0.0.1", ABRlink2, "10.1.2.1"), attempts=200)
util.until(lambda: route.ospf_get_neighbor(ABR, "0.0.0.1", ABRlink3, "10.1.3.1"), attempts=200)
with infamy.IsolatedMacVlan(hport0) as ns:
ns.addip("192.168.100.2")
ns.addroute("0.0.0.0/0", "192.168.100.1")
#breakpoint()
with test.step("Verify ABR:data can access container A on R1 (10.1.1.101)"):
furl=Furl("http://10.1.1.101:8080")
util.until(lambda: furl.nscheck(ns, BODY))
with test.step("Verify ABR:data can access container A on R2 (10.1.2.101)"):
furl=Furl("http://10.1.2.101:8080")
util.until(lambda: furl.nscheck(ns, BODY))
with test.step("Verify ABR:data can access container A on R3 (10.1.3.101)"):
furl=Furl("http://10.1.3.101:8080")
util.until(lambda: furl.nscheck(ns, BODY))
test.succeed()
@@ -0,0 +1,62 @@
graph "ospf_containers" {
layout="neato";
overlap=false;
esep="+20";
splines=true;
size=10;
node [ shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="black", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label=" { { <R1ring1> R1ring1 | <R1ring2> R1ring2 | <data1> data1 | <mgmt1> mgmt1 | <mgmt2> mgmt2 | <data2> data2 | <R2ring1> R2ring1 | <R2ring2> R2ring2 | <data3> data3 | <mgmt3> mgmt3 | <R3ring1> R3ring1 | <R3ring2> R3ring2 | <mgmt4> mgmt4 | <data4> data4 } | host } ",
pos="0,5!"
kind="controller",
];
R1 [
label="{{ <link> link } | R1 | {<ring1> ring1 | <ring2> ring2 | <data> data | <mgmt> mgmt }}"
pos="-1,6!"
kind="infix",
];
R2 [
label="{{ <link> link } | R2 | { <mgmt> mgmt | <data> data | <ring1> ring1 | <ring2> ring2 }}"
pos="0,6!",
kind="infix",
];
R3 [
label="{ <link> link | R3 | { <data> data | <mgmt> mgmt | <ring1> ring1 | <ring2> ring2 }}"
pos="1,6!"
kind="infix",
];
ABR [
label="{ ABR | {<link1> link1 | <link2> link2 |<link3> link3 | <mgmt> mgmt | <data> data }}"
pos="2,6.5!"
kind="infix",
];
host:mgmt1 -- R1:mgmt [kind=mgmt, color="lightgray"]
host:mgmt2 -- R2:mgmt [kind=mgmt, color="lightgray"]
host:mgmt3 -- R3:mgmt [kind=mgmt, color="lightgray"]
host:mgmt4 -- ABR:mgmt [kind=mgmt, color="lightgray"]
# host-Dut links
host:data1 -- R1:data
host:data2 -- R2:data
host:data3 -- R3:data
host:data4 -- ABR:data
host:R1ring1 -- R1:ring1
host:R2ring1 -- R2:ring1
host:R3ring1 -- R3:ring1
host:R1ring2 -- R1:ring2
host:R2ring2 -- R2:ring2
host:R3ring2 -- R3:ring2
R1:link -- ABR:link1
R2:link -- ABR:link2
R3:link -- ABR:link3
}
@@ -0,0 +1,195 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: ospf_containers Pages: 1 -->
<svg width="720pt" height="291pt"
viewBox="0.00 0.00 720.00 291.37" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(0.64 0.64) rotate(0) translate(4 453.72)">
<title>ospf_containers</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-453.72 1127.05,-453.72 1127.05,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-0.5 0,-46.5 908,-46.5 908,-0.5 0,-0.5"/>
<text text-anchor="middle" x="37" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">R1ring1</text>
<polyline fill="none" stroke="black" points="74,-23.5 74,-46.5 "/>
<text text-anchor="middle" x="111" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">R1ring2</text>
<polyline fill="none" stroke="black" points="148,-23.5 148,-46.5 "/>
<text text-anchor="middle" x="177" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
<polyline fill="none" stroke="black" points="206,-23.5 206,-46.5 "/>
<text text-anchor="middle" x="235" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
<polyline fill="none" stroke="black" points="264,-23.5 264,-46.5 "/>
<text text-anchor="middle" x="293" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
<polyline fill="none" stroke="black" points="322,-23.5 322,-46.5 "/>
<text text-anchor="middle" x="351" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data2</text>
<polyline fill="none" stroke="black" points="380,-23.5 380,-46.5 "/>
<text text-anchor="middle" x="417" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">R2ring1</text>
<polyline fill="none" stroke="black" points="454,-23.5 454,-46.5 "/>
<text text-anchor="middle" x="491" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">R2ring2</text>
<polyline fill="none" stroke="black" points="528,-23.5 528,-46.5 "/>
<text text-anchor="middle" x="557" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data3</text>
<polyline fill="none" stroke="black" points="586,-23.5 586,-46.5 "/>
<text text-anchor="middle" x="615" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt3</text>
<polyline fill="none" stroke="black" points="644,-23.5 644,-46.5 "/>
<text text-anchor="middle" x="681" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">R3ring1</text>
<polyline fill="none" stroke="black" points="718,-23.5 718,-46.5 "/>
<text text-anchor="middle" x="755" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">R3ring2</text>
<polyline fill="none" stroke="black" points="792,-23.5 792,-46.5 "/>
<text text-anchor="middle" x="821" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt4</text>
<polyline fill="none" stroke="black" points="850,-23.5 850,-46.5 "/>
<text text-anchor="middle" x="879" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data4</text>
<polyline fill="none" stroke="black" points="0,-23.5 908,-23.5 "/>
<text text-anchor="middle" x="454" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
</g>
<!-- R1 -->
<g id="node2" class="node">
<title>R1</title>
<polygon fill="none" stroke="black" points="79.97,-255.03 79.97,-324.03 295.97,-324.03 295.97,-255.03 79.97,-255.03"/>
<text text-anchor="middle" x="187.97" y="-308.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="79.97,-301.03 295.97,-301.03 "/>
<text text-anchor="middle" x="187.97" y="-285.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">R1</text>
<polyline fill="none" stroke="black" points="79.97,-278.03 295.97,-278.03 "/>
<text text-anchor="middle" x="108.97" y="-262.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">ring1</text>
<polyline fill="none" stroke="black" points="137.97,-255.03 137.97,-278.03 "/>
<text text-anchor="middle" x="166.97" y="-262.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">ring2</text>
<polyline fill="none" stroke="black" points="195.97,-255.03 195.97,-278.03 "/>
<text text-anchor="middle" x="220.97" y="-262.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="245.97,-255.03 245.97,-278.03 "/>
<text text-anchor="middle" x="270.97" y="-262.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
</g>
<!-- host&#45;&#45;R1 -->
<g id="edge1" class="edge">
<title>host:mgmt1&#45;&#45;R1:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M235,-46.5C235,-46.5 323.15,-144.12 315.97,-235.03 314.67,-251.57 296.04,-266.6 296.04,-266.6"/>
</g>
<!-- host&#45;&#45;R1 -->
<g id="edge5" class="edge">
<title>host:data1&#45;&#45;R1:data</title>
<path fill="none" stroke="black" stroke-width="2" d="M177,-46.5C177,-46.5 220.97,-254.53 220.97,-254.53"/>
</g>
<!-- host&#45;&#45;R1 -->
<g id="edge9" class="edge">
<title>host:R1ring1&#45;&#45;R1:ring1</title>
<path fill="none" stroke="black" stroke-width="2" d="M37,-46.5C37,-46.5 108.97,-254.53 108.97,-254.53"/>
</g>
<!-- host&#45;&#45;R1 -->
<g id="edge12" class="edge">
<title>host:R1ring2&#45;&#45;R1:ring2</title>
<path fill="none" stroke="black" stroke-width="2" d="M111,-46.5C111,-46.5 166.97,-254.53 166.97,-254.53"/>
</g>
<!-- R2 -->
<g id="node3" class="node">
<title>R2</title>
<polygon fill="none" stroke="black" points="346,-255.03 346,-324.03 562,-324.03 562,-255.03 346,-255.03"/>
<text text-anchor="middle" x="454" y="-308.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="346,-301.03 562,-301.03 "/>
<text text-anchor="middle" x="454" y="-285.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">R2</text>
<polyline fill="none" stroke="black" points="346,-278.03 562,-278.03 "/>
<text text-anchor="middle" x="371" y="-262.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="396,-255.03 396,-278.03 "/>
<text text-anchor="middle" x="421" y="-262.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="446,-255.03 446,-278.03 "/>
<text text-anchor="middle" x="475" y="-262.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">ring1</text>
<polyline fill="none" stroke="black" points="504,-255.03 504,-278.03 "/>
<text text-anchor="middle" x="533" y="-262.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">ring2</text>
</g>
<!-- host&#45;&#45;R2 -->
<g id="edge2" class="edge">
<title>host:mgmt2&#45;&#45;R2:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M293,-46.5C293,-46.5 371,-254.53 371,-254.53"/>
</g>
<!-- host&#45;&#45;R2 -->
<g id="edge6" class="edge">
<title>host:data2&#45;&#45;R2:data</title>
<path fill="none" stroke="black" stroke-width="2" d="M351,-46.5C351,-46.5 421,-254.53 421,-254.53"/>
</g>
<!-- host&#45;&#45;R2 -->
<g id="edge10" class="edge">
<title>host:R2ring1&#45;&#45;R2:ring1</title>
<path fill="none" stroke="black" stroke-width="2" d="M417,-46.5C417,-46.5 475,-254.53 475,-254.53"/>
</g>
<!-- host&#45;&#45;R2 -->
<g id="edge13" class="edge">
<title>host:R2ring2&#45;&#45;R2:ring2</title>
<path fill="none" stroke="black" stroke-width="2" d="M491,-46.5C491,-46.5 533,-254.53 533,-254.53"/>
</g>
<!-- R3 -->
<g id="node4" class="node">
<title>R3</title>
<polygon fill="none" stroke="black" points="612.03,-255.03 612.03,-324.03 828.03,-324.03 828.03,-255.03 612.03,-255.03"/>
<text text-anchor="middle" x="720.03" y="-308.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">link</text>
<polyline fill="none" stroke="black" points="612.03,-301.03 828.03,-301.03 "/>
<text text-anchor="middle" x="720.03" y="-285.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">R3</text>
<polyline fill="none" stroke="black" points="612.03,-278.03 828.03,-278.03 "/>
<text text-anchor="middle" x="637.03" y="-262.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
<polyline fill="none" stroke="black" points="662.03,-255.03 662.03,-278.03 "/>
<text text-anchor="middle" x="687.03" y="-262.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="712.03,-255.03 712.03,-278.03 "/>
<text text-anchor="middle" x="741.03" y="-262.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">ring1</text>
<polyline fill="none" stroke="black" points="770.03,-255.03 770.03,-278.03 "/>
<text text-anchor="middle" x="799.03" y="-262.83" font-family="DejaVu Sans Mono, Book" font-size="14.00">ring2</text>
</g>
<!-- host&#45;&#45;R3 -->
<g id="edge3" class="edge">
<title>host:mgmt3&#45;&#45;R3:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M615,-46.5C615,-46.5 687.03,-254.53 687.03,-254.53"/>
</g>
<!-- host&#45;&#45;R3 -->
<g id="edge7" class="edge">
<title>host:data3&#45;&#45;R3:data</title>
<path fill="none" stroke="black" stroke-width="2" d="M557,-46.5C557,-46.5 560.94,-155.68 592.03,-235.03 598.08,-250.48 611.96,-266.6 611.96,-266.6"/>
</g>
<!-- host&#45;&#45;R3 -->
<g id="edge11" class="edge">
<title>host:R3ring1&#45;&#45;R3:ring1</title>
<path fill="none" stroke="black" stroke-width="2" d="M681,-46.5C681,-46.5 741.03,-254.53 741.03,-254.53"/>
</g>
<!-- host&#45;&#45;R3 -->
<g id="edge14" class="edge">
<title>host:R3ring2&#45;&#45;R3:ring2</title>
<path fill="none" stroke="black" stroke-width="2" d="M755,-46.5C755,-46.5 799.03,-254.53 799.03,-254.53"/>
</g>
<!-- ABR -->
<g id="node5" class="node">
<title>ABR</title>
<polygon fill="none" stroke="black" points="849.05,-399.54 849.05,-445.54 1123.05,-445.54 1123.05,-399.54 849.05,-399.54"/>
<text text-anchor="middle" x="986.05" y="-430.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">ABR</text>
<polyline fill="none" stroke="black" points="849.05,-422.54 1123.05,-422.54 "/>
<text text-anchor="middle" x="878.05" y="-407.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">link1</text>
<polyline fill="none" stroke="black" points="907.05,-399.54 907.05,-422.54 "/>
<text text-anchor="middle" x="936.05" y="-407.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">link2</text>
<polyline fill="none" stroke="black" points="965.05,-399.54 965.05,-422.54 "/>
<text text-anchor="middle" x="994.05" y="-407.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">link3</text>
<polyline fill="none" stroke="black" points="1023.05,-399.54 1023.05,-422.54 "/>
<text text-anchor="middle" x="1048.05" y="-407.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="1073.05,-399.54 1073.05,-422.54 "/>
<text text-anchor="middle" x="1098.05" y="-407.34" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
</g>
<!-- host&#45;&#45;ABR -->
<g id="edge4" class="edge">
<title>host:mgmt4&#45;&#45;ABR:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M821,-46.5C821,-46.5 1048.05,-399.54 1048.05,-399.54"/>
</g>
<!-- host&#45;&#45;ABR -->
<g id="edge8" class="edge">
<title>host:data4&#45;&#45;ABR:data</title>
<path fill="none" stroke="black" stroke-width="2" d="M879,-46.5C879,-46.5 1098.05,-399.54 1098.05,-399.54"/>
</g>
<!-- R1&#45;&#45;ABR -->
<g id="edge15" class="edge">
<title>R1:link&#45;&#45;ABR:link1</title>
<path fill="none" stroke="black" stroke-width="2" d="M295.97,-312.53C295.97,-312.53 317.37,-339.66 326,-344.03 535.15,-449.72 849.05,-410.54 849.05,-410.54"/>
</g>
<!-- R2&#45;&#45;ABR -->
<g id="edge16" class="edge">
<title>R2:link&#45;&#45;ABR:link2</title>
<path fill="none" stroke="black" stroke-width="2" d="M562,-312.53C562,-312.53 583.44,-339.57 592.03,-344.03 686.56,-393.12 724.01,-361.89 829.05,-379.54 876.8,-387.56 936.15,-399.5 936.15,-399.5"/>
</g>
<!-- R3&#45;&#45;ABR -->
<g id="edge17" class="edge">
<title>R3:link&#45;&#45;ABR:link3</title>
<path fill="none" stroke="black" stroke-width="2" d="M828.03,-312.53C828.03,-312.53 994.05,-399.54 994.05,-399.54"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

+3
View File
@@ -0,0 +1,3 @@
---
- name: ospf_container
case: ospf_container/test.py
+1 -1
View File
@@ -3,7 +3,7 @@ import os
from .container import Container
from .env import Env
from .furl import Furl
from .netns import IsolatedMacVlan
from .netns import IsolatedMacVlan,IsolatedMacVlans
from .sniffer import Sniffer
from .tap import Test
from .util import parallel, until
+28
View File
@@ -0,0 +1,28 @@
"""
Basic file server over HTTP
"""
import concurrent.futures
import functools
import http.server
import socket
class FileServer(http.server.HTTPServer):
"""Open web server on (address, port) serving files from directory"""
class RequestHandler(http.server.SimpleHTTPRequestHandler):
def log_message(self, *args, **kwargs):
pass
address_family = socket.AF_INET6
def __init__(self, server_address, directory):
rh = functools.partial(FileServer.RequestHandler, directory=directory)
self.__tp = concurrent.futures.ThreadPoolExecutor(max_workers=1)
super().__init__(server_address, rh)
def __enter__(self):
self.__tp.submit(self.serve_forever)
def __exit__(self, _, __, ___):
self.shutdown()
self.__tp.shutdown()
+2 -2
View File
@@ -70,9 +70,9 @@ def _get_ospf_status(target):
rib = protos.get("control-plane-protocol", {})
for p in rib:
if p["type"] == "infix-routing:ospfv2":
return p.get("ospf") or p.get("ietf-ospf:ospf")
return p.get("ospf") or p.get("ietf-ospf:ospf", {})
return []
return {}
def _get_ospf_status_area(target, area_id):
+2 -2
View File
@@ -3,8 +3,8 @@ graph "1x3" {
overlap="false";
esep="+80";
node [shape=record, fontname="monospace"];
edge [color="cornflowerblue", penwidth="2"];
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="host | { <tgt> tgt | <data0> data0 | <data1> data1 }",
+2 -2
View File
@@ -3,8 +3,8 @@ graph "1x4" {
overlap="false";
esep="+80";
node [shape=record, fontname="monospace"];
edge [color="cornflowerblue", penwidth="2"];
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="host | { <tgt> tgt | <data0> data0 | <data1> data1 | <data2> data2 }",
+2 -2
View File
@@ -3,8 +3,8 @@ graph "2x2" {
overlap="false";
esep="+40";
node [shape=record, fontname="monospace"];
edge [color="cornflowerblue", penwidth="2"];
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="host | { <mgmt1> mgmt1 | <data1> data1 | <mgmt2> mgmt2 | <data2> data2 }",
+2 -2
View File
@@ -3,8 +3,8 @@ graph "2x4" {
overlap="false";
esep="+40";
node [shape=record, fontname="monospace"];
edge [color="cornflowerblue", penwidth="2"];
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="host | { <mgmt1> mgmt1 | <data10> data10 | <data11> data11 | <> \n\n\n\n\n\n | <mgmt2> mgmt2 | <data20> data20 | <data21> data21 }",
+5 -5
View File
@@ -4,9 +4,9 @@ graph "ring-4-duts" {
esep="+20";
splines=true;
size=10;
node [shape=record, fontname="monospace"];
edge [color="cornflowerblue", penwidth="2"];
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label=" { host | { <mgmt4> mgmt4 | <data4> data4 | <mgmt1> mgmt1 | <data1> data1 | <data2> data2 | <mgmt2> mgmt2 | <data3> data3 | <mgmt3> mgmt3 } }",
@@ -36,7 +36,7 @@ graph "ring-4-duts" {
label="{ <ring2> ring2 | <cross> cross | <ring1> ring1 } | { { <data> data | <mgmt> mgmt } | \n R3 \n10.0.0.3/32 \n(lo) }",
pos="250,50!"
kind="infix",
];
];
host:mgmt1 -- R1:mgmt [kind=mgmt, color="lightgray"]
host:mgmt2 -- R2:mgmt [kind=mgmt, color="lightgray"]
@@ -51,7 +51,7 @@ graph "ring-4-duts" {
# Ring
R1:ring1 -- R2:ring2 [color="blue",headlabel=".2", label="10.0.12.1/30", taillabel=".1", labeldistance=1, fontcolor="blue"]
R2:ring1 -- R3:ring2 [color="blue",headlabel=".2", label="10.0.23.0/30", taillabel=".1", labeldistance=1, fontcolor="blue"]
R2:ring1 -- R3:ring2 [color="blue",headlabel=".2", label="10.0.23.0/30", taillabel=".1", labeldistance=1, fontcolor="blue"]
R3:ring1 -- R4:ring2 [color="blue",headlabel=".2", label="192.168.4.0/24", taillabel=".1", labeldistance=1 fontcolor="blue"]
R4:ring1 -- R1:ring2 [color="blue",headlabel=".2", label="10.0.41.0/30", taillabel=".1", labeldistance=1, fontcolor="blue"]
-44
View File
@@ -1,44 +0,0 @@
:topdoc:
= Test specification
v24.10.2-2-g032a51b9-dirty
:title-page:
:toc:
:toclevels: 2
:sectnums:
:sectnumlevels: 2
<<<
include::../case/misc/Readme.adoc[]
<<<
include::../case/ietf_system/Readme.adoc[]
<<<
include::../case/ietf_syslog/Readme.adoc[]
<<<
include::../case/ietf_interfaces/Readme.adoc[]
<<<
include::../case/ietf_routing/Readme.adoc[]
<<<
include::../case/infix_containers/Readme.adoc[]
<<<
include::../case/infix_dhcp/Readme.adoc[]
<<<
include::../case/ietf_hardware/Readme.adoc[]
<<<
include::../case/infix_services/Readme.adoc[]
+4
View File
@@ -42,3 +42,7 @@ include::../case/ietf_hardware/Readme.adoc[]
<<<
include::../case/infix_services/Readme.adoc[]
<<<
include::../case/use_case/Readme.adoc[]
+47 -4
View File
@@ -1,7 +1,6 @@
#!/bin/env python3
import os
import ast
import graphviz
import argparse
import io
import sys
@@ -9,15 +8,47 @@ import re
from pathlib import Path
# Define a custom NodeVisitor to extract the arguments
import graphviz
def replace_image_tag(text, test_dir):
"""
Convert images added in the description and replace with the required ifdefs to work
generating the test specifcation as well.
"""
pattern = r"image::(?P<image_name>\S+)\[(?P<label>[^\]]*)\]"
def repl(match):
image_name = match.group("image_name")
label = match.group("label")
return f"""ifdef::topdoc[]
image::../../{test_dir}/{image_name}[{label}]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::{'/'.join(Path(test_dir).parts[3:])}/{image_name}[{label}]
endif::testgroup[]
ifndef::testgroup[]
image::{image_name}[{label}]
endif::testgroup[]
endif::topdoc[]"""
return re.sub(pattern, repl, text)
class TestStepVisitor(ast.NodeVisitor):
"""
A custom test step visitor to grab the test description (docstring)
and the test steps test.step(.....) for the test case.
"""
def __init__(self):
self.test_steps=[]
self.name = ""
self.description = ""
def visit_Module(self, node):
"""Extract docstring from a module."""
"""Extract docstring from a test."""
docstring = ast.get_docstring(node)
if docstring:
lines = docstring.splitlines()
@@ -27,7 +58,7 @@ class TestStepVisitor(ast.NodeVisitor):
for line in lines:
if self.name == "":
self.name=line.strip()
elif newline_parsed == False and line == "":
elif newline_parsed is False and line == "":
newline_parsed = True
continue # Skip mandatory newline
else:
@@ -37,6 +68,7 @@ class TestStepVisitor(ast.NodeVisitor):
# Check for test.step() for the actual test steps
def visit_Call(self, node):
"""Extract test.step from test"""
if isinstance(node.func, ast.Attribute) and node.func.attr == 'step':
if isinstance(node.func.value, ast.Name) and node.func.value.id == 'test':
if node.args and isinstance(node.args[0], ast.Constant):
@@ -44,6 +76,7 @@ class TestStepVisitor(ast.NodeVisitor):
self.generic_visit(node) # Continue visiting other nodes
class TestCase:
"""All test specifcation resources for a test case"""
def __init__(self, directory, rootdir=None):
self.test_dir=Path(directory)
if rootdir:
@@ -63,6 +96,7 @@ class TestCase:
self.test_steps=visitor.test_steps
def generate_topology(self):
"""Generate SVG file from the topology.dot file"""
with open(self.topology_dot, 'r') as dot_file:
dot_graph = dot_file.read()
graph = graphviz.Source(dot_graph)
@@ -75,8 +109,11 @@ class TestCase:
with open(f"{self.topology_image}.svg", 'w') as f:
f.write(mod_content)
def generate_specification(self):
"""Generate a Readme.adoc for the test case"""
self.generate_topology()
self.description = replace_image_tag(self.description, self.test_dir)
with open(self.specification, "w") as spec:
spec.write(f"=== {self.name}\n")
spec.write("==== Description\n")
@@ -98,6 +135,10 @@ class TestCase:
spec.write("\n\n<<<\n\n") # need empty lines to pagebreak
def parse_directory_tree(directory):
"""P
arse a directory for subdirectories with a test.py
and a topology.dot files
"""
directories=[]
for dirpath, dirnames, filenames in os.walk(directory):
testscript = False
@@ -126,6 +167,8 @@ sys.stderr = output_capture
directories = parse_directory_tree(args.directory)
error_string = ""
for directory in directories:
# This is hacky, graphviz output error only to stdout and return successful(always).
# If everything goes well, output shall be empty, fail on any output
output_capture.truncate(0)
output_capture.seek(0)
test_case = TestCase(directory, args.root_dir)