Merge pull request #1039 from kernelkit/verify-ieee-group-fwd

Verify ieee group fwd
This commit is contained in:
Ahmed Karic
2025-05-16 14:26:42 +02:00
committed by GitHub
57 changed files with 598 additions and 204 deletions
+5
View File
@@ -12,6 +12,10 @@ All notable changes to the project are documented in this file.
- Upgrade netopeer2 (NETCONF) to 2.4.1
- Add documentation on Infix upgrading and downgrading, issue #1009
- Add HDMI and USB support for iMX8MP-evk
- Enforced strict format for LLDP destination MAC address:
- Only accepts colon-separated format: `01:80:C2:00:00:0E`
- Add `show lldp` command to show discovered neighbors per interface.
- Add configuration support for per-interface LLDP administrative status
### Fixes
- Fix containers with multiple mounts
@@ -128,6 +132,7 @@ All notable changes to the project are documented in this file.
- Add support for GRE/GRETAP tunnels
- Add support for STP/RSTP on bridges
- Add support for VXLAN tunnels
- Add support for configuring global LLDP `message-tx-interval`
### Fixes
+58 -1
View File
@@ -46,7 +46,7 @@ admin@infix-c0-ff-ee:~$
## LLDP
Infix supports LLDP (IEEE 802.1AB). For a device with factory default
Infix supports LLDP (IEEE 802.1AB). For a device with factory default
settings, the link-local IPv6 address can be read from the Management
Address TLV using *tcpdump* or other sniffing tools[^1]:
@@ -131,6 +131,10 @@ tcpdump: listening on tap0, link-type EN10MB (Ethernet), snapshot length 262144
linux-pc:#
```
The following capabilities are available via NETCONF/RESTCONF or the Infix CLI.
### LLDP Enable/Disable
The LLDP service can be disabled using the following commands.
```
@@ -140,6 +144,59 @@ admin@infix-c0-ff-ee:/config/> leave
admin@infix-c0-ff-ee:/>
```
To reenable it from the CLI config mode:
```
admin@test-00-01-00:/config/> set lldp enabled
admin@test-00-01-00:/config/> leave
```
### LLDP Message Transmission Interval
By default, LLDP uses a `message-tx-interval` of 30 seconds, as defined
by the IEEE standard. Infix allows this value to be customized.
To change it using the CLI:
```
admin@test-00-01-00:/config/> set lldp message-tx-interval 1
admin@test-00-01-00:/config/> leave
```
### LLDP Administrative Status per Interface
Infix supports configuring the LLDP administrative status on a per-port
basis. The default mode is `tx-and-rx`, but the following options are
also supported:
- `rx-only` Receive LLDP packets only
- `tx-only` Transmit LLDP packets only
- `disabled` Disable LLDP on the interface
Example configuration:
```
admin@test-00-01-00:/config/> set lldp port e8 dest-mac-address 01:80:C2:00:00:0E admin-status disabled
admin@test-00-01-00:/config/> set lldp port e5 dest-mac-address 01:80:C2:00:00:0E admin-status rx-only
admin@test-00-01-00:/config/> set lldp port e6 dest-mac-address 01:80:C2:00:00:0E admin-status tx-only
admin@test-00-01-00:/config/> leave
```
> [!NOTE]
> The destination MAC address must be the standard LLDP multicast
> address: `01:80:C2:00:00:0E`.
### Displaying LLDP Neighbor Information
In CLI mode, Infix also provides a convenient `show lldp` command to
list LLDP neighbors detected on each interface:
```
admin@test-00-01-00:/> show lldp
INTERFACE REM-IDX TIME CHASSIS-ID PORT-ID
e5 1 902 00:a0:85:00:04:01 00:a0:85:00:04:07
e6 3 897 00:a0:85:00:03:01 00:a0:85:00:03:07
e8 2 901 00:a0:85:00:02:01 00:a0:85:00:02:05
```
## mDNS-SD
+1 -1
View File
@@ -25,7 +25,7 @@ MODULES=(
"infix-if-type@2025-02-12.yang"
"infix-routing@2024-11-27.yang"
"ieee802-dot1ab-lldp@2022-03-15.yang"
"infix-lldp@2025-01-08.yang"
"infix-lldp@2025-05-05.yang"
"infix-dhcp-common@2025-01-29.yang"
"infix-dhcp-client@2025-01-29.yang"
"infix-dhcp-server@2025-01-29.yang"
+23
View File
@@ -11,6 +11,13 @@ module infix-lldp {
contact "kernelkit@googlegroups.com";
description "Infix augments and deviations to ieee-dot1ab-lldp.";
revision 2025-05-05 {
description
"Restrict 'dest-mac-address' to only allow the LLDP multicast MAC
address (01:80:C2:00:00:0E).";
reference "internal";
}
revision 2025-01-08 {
description "Enable std. /lldp:lldp/message-tx-interval";
}
@@ -32,6 +39,22 @@ module infix-lldp {
}
}
deviation "/lldp:lldp/lldp:port/lldp:dest-mac-address" {
deviate replace {
type string {
pattern "([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}";
}
}
}
deviation "/lldp:lldp/lldp:port/lldp:dest-mac-address" {
deviate add {
must "translate(., 'abcdef', 'ABCDEF') = '01:80:C2:00:00:0E'" {
error-message "Only the LLDP Multicast MAC (01:80:C2:00:00:0E) is allowed.";
}
}
}
deviation "/lldp:lldp/lldp:message-fast-tx" {
deviate not-supported;
}
+22 -15
View File
@@ -373,6 +373,12 @@
</ACTION>
</COMMAND>
<COMMAND name="lldp" help="Show LLDP neighbors">
<ACTION sym="script">
show lldp
</ACTION>
</COMMAND>
<COMMAND name="hardware" help="Show hardware information">
<ACTION sym="script" in="tty" out="tty" interrupt="true">
show hardware |pager
@@ -471,22 +477,22 @@
<ACTION sym="script">sysinfo</ACTION>
</COMMAND>
-->
<COMMAND name="version" help="Show software version">
<ACTION sym="script" in="tty" interrupt="true">cat /etc/version</ACTION>
</COMMAND>
<COMMAND name="factory-config" help="Show factory-config">
<ACTION sym="script" in="tty" out="tty" interrupt="true">jq -C . /etc/factory-config.cfg |pager</ACTION>
</COMMAND>
<COMMAND name="running-config" help="Show running-config">
<ACTION sym="script" in="tty" out="tty" interrupt="true">sysrepocfg -X -f json | jq -C . |pager</ACTION>
</COMMAND>
<COMMAND name="startup-config" help="Show startup-config">
<ACTION sym="script" in="tty" out="tty" interrupt="true">jq -C . /cfg/startup-config.cfg |pager</ACTION>
</COMMAND>
<COMMAND name="version" help="Show software version">
<ACTION sym="script" in="tty" interrupt="true">cat /etc/version</ACTION>
</COMMAND>
<COMMAND name="factory-config" help="Show factory-config">
<ACTION sym="script" in="tty" out="tty" interrupt="true">jq -C . /etc/factory-config.cfg |pager</ACTION>
</COMMAND>
<COMMAND name="running-config" help="Show running-config">
<ACTION sym="script" in="tty" out="tty" interrupt="true">sysrepocfg -X -f json | jq -C . |pager</ACTION>
</COMMAND>
<COMMAND name="startup-config" help="Show startup-config">
<ACTION sym="script" in="tty" out="tty" interrupt="true">jq -C . /cfg/startup-config.cfg |pager</ACTION>
</COMMAND>
<COMMAND name="factory-reset" help="Restore the system to factory default state">
<ACTION sym="script" in="tty" out="tty" interrupt="true">
/bin/yorn "This will restore the device to factory defaults"
@@ -581,6 +587,7 @@
</ACTION>
</COMMAND>
</COMMAND>
</VIEW>
<VIEW name="config">
+10
View File
@@ -154,6 +154,15 @@ def routes(args: List[str]):
return
cli_pretty(data, "show-routing-table", "-i", ip_version)
def lldp(args: List[str]):
data = run_sysrepocfg("/ieee802-dot1ab-lldp:lldp")
if not data:
print("No lldp data retrieved.")
return
if RAW_OUTPUT:
print(json.dumps(data, indent=2))
return
cli_pretty(data, "show-lldp")
def execute_command(command: str, args: List[str]):
command_mapping = {
@@ -162,6 +171,7 @@ def execute_command(command: str, args: List[str]):
'interface': interface,
'ntp': ntp,
'routes': routes,
'lldp': lldp,
'software' : software,
'stp': stp,
}
+110 -29
View File
@@ -20,6 +20,7 @@ class PadMdb:
group = 20
ports = 45
class PadStpPort:
port = 12
id = 7
@@ -30,6 +31,7 @@ class PadStpPort:
total = 12 + 7 + 12 + 12 + 6 + 31
class PadRoute:
dest = 30
pref = 8
@@ -57,6 +59,7 @@ class PadSoftware:
state = 10
version = 23
class PadDhcpServer:
ip = 17
mac = 19
@@ -64,11 +67,13 @@ class PadDhcpServer:
cid = 19
exp = 10
class PadUsbPort:
title = 30
name = 20
state = 10
class PadNtpSource:
address = 16
mode = 13
@@ -76,6 +81,7 @@ class PadNtpSource:
stratum = 11
poll = 14
class Decore():
@staticmethod
def decorate(sgr, txt, restore="0"):
@@ -109,11 +115,21 @@ class Decore():
def gray_bg(txt):
return Decore.decorate("100", txt)
class PadLldp:
interface = 16
rem_idx = 10
time = 12
chassis_id = 20
port_id = 20
def datetime_now():
if UNIT_TEST:
return datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
return datetime.now(timezone.utc)
def get_json_data(default, indata, *args):
data = indata
for arg in args:
@@ -124,12 +140,14 @@ def get_json_data(default, indata, *args):
return data
def remove_yang_prefix(key):
parts = key.split(":", 1)
if len(parts) > 1:
return parts[1]
return key
class Date(datetime):
def _pretty_delta(delta):
assert(delta.total_seconds() > 0)
@@ -179,7 +197,6 @@ class Date(datetime):
return cls.strptime(f"{date}+{tz}", "%Y-%m-%dT%H:%M:%S%z")
class Route:
def __init__(self, data, ip):
self.data = data
@@ -321,6 +338,7 @@ class Software:
print(f"SHA-256 : {self.hash}")
print(f"Installed : {self.date}")
class USBport:
def __init__(self, data):
self.data = data
@@ -333,6 +351,7 @@ class USBport:
row += f"{self.state:<{PadUsbPort.state}}"
print(row)
class STPBridgeID:
def __init__(self, id):
self.id = id
@@ -345,6 +364,7 @@ class STPBridgeID:
)
return f"{prio:1x}.{sysid:03x}.{addr}"
class STPPortID:
def __init__(self, id):
self.id = id
@@ -356,6 +376,7 @@ class STPPortID:
)
return f"{prio:1x}.{pid:03x}"
class DhcpServer:
def __init__(self, data):
self.data = data
@@ -533,6 +554,7 @@ class Iface:
def is_gretap(self):
return self.data['type'] == "infix-if-type:gretap"
def oper(self, detail=False):
"""Remap in brief overview to fit column widths."""
if not detail and self.oper_status == "lower-layer-down":
@@ -719,7 +741,7 @@ class Iface:
lower.pr_name(pipe)
lower.pr_proto_lag()
def pr_veth(self, _ifaces):
def pr_veth(self):
self.pr_name(pipe="")
self.pr_proto_veth()
self.pr_proto_ipv4()
@@ -930,6 +952,25 @@ class Iface:
print(f"{' port':<{20}}: {tc.get('port', 'UNKNOWN')}")
class LldpNeighbor:
def __init__(self, iface, data):
self.interface = iface
self.remote_index = data.get("remote-index", 0)
self.time_mark = data.get("time-mark", 0)
self.chassis_id = data.get("chassis-id", "unknown")
self.port_id = data.get("port-id", "unknown")
def print(self):
row = (
f"{self.interface:<{PadLldp.interface}}"
f"{self.remote_index:<{PadLldp.rem_idx}}"
f"{self.time_mark:<{PadLldp.time}}"
f"{self.chassis_id:<{PadLldp.chassis_id}}"
f"{self.port_id:<{PadLldp.port_id}}"
)
print(row)
def find_iface(_ifaces, name):
for _iface in [Iface(data) for data in _ifaces]:
if _iface.name == name:
@@ -941,9 +982,11 @@ def find_iface(_ifaces, name):
def version_sort(s):
return [int(x) if x.isdigit() else x for x in re.split(r'(\d+)', s)]
def ifname_sort(iface):
return version_sort(iface["name"])
def brport_sort(iface):
brname = iface.get("infix-interfaces:bridge-port", {}).get("bridge", "")
return version_sort(brname) + version_sort(iface["name"])
@@ -987,7 +1030,7 @@ def pr_interface_list(json):
continue
if iface.is_veth():
iface.pr_veth(ifaces)
iface.pr_veth()
continue
if iface.is_gre():
@@ -1057,7 +1100,6 @@ def show_bridge_mdb(json):
iface.pr_vlans_mdb(iface.name)
def show_bridge_stp_port(ifname, brport):
stp = brport["stp"]
@@ -1091,6 +1133,7 @@ def show_bridge_stp_port(ifname, brport):
)
print(row)
def show_bridge_stp(json):
if not json.get("ietf-interfaces:interfaces"):
print("Error, top level \"ietf-interfaces:interface\" missing")
@@ -1198,6 +1241,7 @@ def show_software(json, name):
if slot.is_rootfs():
slot.print()
def show_hardware(json):
if not json.get("ietf-hardware:hardware"):
print(f"Error, top level \"ietf-hardware:component\" missing")
@@ -1216,6 +1260,7 @@ def show_hardware(json):
port = USBport(component)
port.print()
def show_ntp(json):
if not json.get("ietf-system:system-state"):
print("NTP client not enabled.")
@@ -1236,6 +1281,7 @@ def show_ntp(json):
row += f"{source['poll']:>{PadNtpSource.poll}}"
print(row)
def show_dhcp_server(json, stats):
data = json.get("infix-dhcp-server:dhcp-server")
if not data:
@@ -1255,6 +1301,37 @@ def show_dhcp_server(json, stats):
print(Decore.invert(hdr))
server.print()
def show_lldp(json):
if not json.get("ieee802-dot1ab-lldp:lldp"):
print("Error: No LLDP data available.")
sys.exit(1)
lldp_ports = json["ieee802-dot1ab-lldp:lldp"].get("port", [])
if not lldp_ports:
print("No LLDP neighbors found.")
return
header = (
f"{'INTERFACE':<{PadLldp.interface}}"
f"{'REM-IDX':<{PadLldp.rem_idx}}"
f"{'TIME':<{PadLldp.time}}"
f"{'CHASSIS-ID':<{PadLldp.chassis_id}}"
f"{'PORT-ID':<{PadLldp.port_id}}"
)
print(Decore.invert(header))
for port_data in lldp_ports:
port_name = port_data["name"]
neighbors = port_data.get("remote-systems-data", [])
for neighbor in neighbors:
entry = LldpNeighbor(port_name, neighbor)
entry.print()
def main():
global UNIT_TEST
@@ -1272,47 +1349,51 @@ def main():
parser.add_argument('-t', '--test', action='store_true', help='Enable unit test mode')
parser_show_routing_table = subparsers.add_parser('show-routing-table', help='Show the routing table')
parser_show_routing_table.add_argument('-i', '--ip', required=True, help='IPv4 or IPv6 address')
subparsers.add_parser('show-boot-order', help='Show NTP sources')
parser_show_interfaces = subparsers.add_parser('show-interfaces', help='Show interfaces')
parser_show_interfaces.add_argument('-n', '--name', help='Interface name')
subparsers.add_parser('show-bridge-mdb', help='Show bridge MDB')
parser_show_bridge_mdb = subparsers.add_parser('show-bridge-mdb', help='Show bridge MDB')
parser_show_bridge_stp = subparsers.add_parser('show-bridge-stp',
help='Show spanning tree state')
subparsers.add_parser('show-bridge-stp', help='Show spanning tree state')
parser_show_software = subparsers.add_parser('show-software', help='Show software versions')
parser_show_software.add_argument('-n', '--name', help='Slotname')
subparsers.add_parser('show-dhcp-server', help='Show DHCP server') \
.add_argument("-s", "--stats", action="store_true", help="Show server statistics")
parser_show_hardware = subparsers.add_parser('show-hardware', help='Show USB ports')
subparsers.add_parser('show-hardware', help='Show USB ports')
parser_show_ntp_sources = subparsers.add_parser('show-ntp', help='Show NTP sources')
subparsers.add_parser('show-interfaces', help='Show interfaces') \
.add_argument('-n', '--name', help='Interface name')
parser_show_boot_order = subparsers.add_parser('show-boot-order', help='Show NTP sources')
subparsers.add_parser('show-lldp', help='Show LLDP neighbors')
parser_dhcp_srv = subparsers.add_parser('show-dhcp-server', help='Show DHCP server')
parser_dhcp_srv.add_argument("-s", "--stats", action="store_true", help="Show server statistics")
subparsers.add_parser('show-ntp', help='Show NTP sources')
subparsers.add_parser('show-routing-table', help='Show the routing table') \
.add_argument('-i', '--ip', required=True, help='IPv4 or IPv6 address')
subparsers.add_parser('show-software', help='Show software versions') \
.add_argument('-n', '--name', help='Slotname')
args = parser.parse_args()
UNIT_TEST = args.test
if args.command == "show-interfaces":
if args.command == "show-bridge-mdb":
show_bridge_mdb(json_data)
elif args.command == "show-bridge-stp":
show_bridge_stp(json_data)
elif args.command == "show-dhcp-server":
show_dhcp_server(json_data, args.stats)
elif args.command == "show-hardware":
show_hardware(json_data)
elif args.command == "show-interfaces":
show_interfaces(json_data, args.name)
elif args.command == "show-lldp":
show_lldp(json_data)
elif args.command == "show-ntp":
show_ntp(json_data)
elif args.command == "show-routing-table":
show_routing_table(json_data, args.ip)
elif args.command == "show-software":
show_software(json_data, args.name)
elif args.command == "show-bridge-mdb":
show_bridge_mdb(json_data)
elif args.command == "show-bridge-stp":
show_bridge_stp(json_data)
elif args.command == "show-hardware":
show_hardware(json_data)
elif args.command == "show-ntp":
show_ntp(json_data)
elif args.command == "show-dhcp-server":
show_dhcp_server(json_data, args.stats)
else:
print(f"Error, unknown command '{args.command}'")
sys.exit(1)
+23 -17
View File
@@ -4,20 +4,35 @@ from collections import defaultdict
def operational():
"""Retrieve LLDP neighbor information and store in remote-systems-data under the correct port."""
# Reference: https://www.ieee802.org/1/files/public/YANGs/ieee802-types.yang
subtype_mapping = {
"component": "chassis-component",
# https://www.ieee802.org/1/files/public/YANGs/ieee802-types.yang
# <chassis-component> and <port-component> subtypes are not supported in
# https://github.com/lldpd/lldpd until v1.0.19
chassis_id_subtype_mapping = {
#"unhandled": "chassis-component",
"ifalias": "interface-alias",
"port": "port-component",
#"unhandled": "port-component",
"mac": "mac-address",
"ip": "network-address",
"ifname": "interface-name",
"local": "local"
}
DEFAULT_MAC = "00-00-00-00-00-00"
# https://www.ieee802.org/1/files/public/YANGs/ieee802-types.yang
# <port-component> and <agent-circuit-id> subtypes are not supported in
# https://github.com/lldpd/lldpd until v1.0.19
port_id_subtype_mapping = {
"ifalias": "interface-alias",
#"unhandled": "port-component",
"mac": "mac-address",
"ip": "network-address",
"ifname": "interface-name",
#"unhandled": "agent-circuit-id"
"local": "local"
}
port_data = defaultdict(lambda: {"remote-systems-data": [], "dest-mac-address": None})
LLDP_MULTICAST_MAC = "01:80:C2:00:00:0E"
port_data = defaultdict(lambda: {"remote-systems-data": [], "dest-mac-address": LLDP_MULTICAST_MAC})
data = HOST.run_json(["lldpcli", "show", "neighbors", "-f", "json"])
@@ -32,18 +47,12 @@ def operational():
time_mark = parse_time(iface_data.get("age"))
chassis = iface_data.get("chassis", {})
chassis_id_type, chassis_id_value = extract_chassis_id(chassis, subtype_mapping)
chassis_id_type, chassis_id_value = extract_chassis_id(chassis, chassis_id_subtype_mapping)
port_info = iface_data.get("port", {})
port_id_type = subtype_mapping.get(port_info.get("id", {}).get("type"), "unknown")
port_id_type = port_id_subtype_mapping.get(port_info.get("id", {}).get("type"), "unknown")
port_id_value = port_info.get("id", {}).get("value", "")
dest_mac_address = (
chassis_id_value.replace(":", "-") if chassis_id_type == "mac-address" else
port_id_value.replace(":", "-") if port_id_type == "mac-address" else
DEFAULT_MAC
)
remote_entry = {
"time-mark": time_mark,
"remote-index": remote_index,
@@ -55,9 +64,6 @@ def operational():
port_data[iface_name]["remote-systems-data"].append(remote_entry)
if port_data[iface_name]["dest-mac-address"] is None:
port_data[iface_name]["dest-mac-address"] = dest_mac_address
formatted_output = {
"ieee802-dot1ab-lldp:lldp": {
"port": [
@@ -1,25 +0,0 @@
graph "1x3" {
layout="neato";
overlap="false";
esep="+80";
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="host | { <mgmt> mgmt | <data1> data1 | <data2> data2 }",
pos="0,12!",
requires="controller",
];
target [
label="{ <mgmt> mgmt | <data1> data1 | <data2> data2 } | target",
pos="10,12!",
requires="infix",
];
host:mgmt -- target:mgmt [requires="mgmt", color=lightgrey]
host:data1 -- target:data1 [color=black]
host:data2 -- target:data2 [color=black]
}
+1
View File
@@ -0,0 +1 @@
../../../infamy/topologies/1x3.dot
@@ -35,7 +35,7 @@
<!-- host&#45;&#45;target -->
<g id="edge1" class="edge">
<title>host:mgmt&#45;&#45;target:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M108,-58C108,-58 308.03,-58 308.03,-58"/>
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-58C108,-58 308.03,-58 308.03,-58"/>
</g>
<!-- host&#45;&#45;target -->
<g id="edge2" class="edge">

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -8,14 +8,14 @@ attached to a VLAN filtering bridge are always locally terminated.
.---------------------------.
| target |
| |
| data0.10 br0 data1.10 |
| data1.10 br0 data2.10 |
| \ / \ / |
'------data0-----data1------'
'------data1-----data2------'
| |
| |
.------data0-----data1------.
.------data1-----data2------.
| / : \ |
| data0.10 : data1.10 |
| data1.10 : data2.10 |
| : |
| host |
| : |
@@ -23,7 +23,7 @@ attached to a VLAN filtering bridge are always locally terminated.
....
In this setup, even though VLAN 10 is allowed to ingress and egress on
both `data0` and `data1`, _bridging_ of packets from one to the other
both `data1` and `data2`, _bridging_ of packets from one to the other
must _not_ be allowed.
"""
@@ -35,10 +35,10 @@ with infamy.Test() as test:
tgt = env.attach("target", "mgmt")
with test.step("Configure bridge and VLAN interfaces on target"):
_, hdata0 = env.ltop.xlate( "host", "data0")
_, hdata1 = env.ltop.xlate( "host", "data1")
_, ddata0 = env.ltop.xlate("target", "data0")
_, hdata2 = env.ltop.xlate( "host", "data2")
_, ddata1 = env.ltop.xlate("target", "data1")
_, ddata2 = env.ltop.xlate("target", "data2")
tgt.put_config_dicts({
"ietf-interfaces": {
@@ -53,35 +53,12 @@ with infamy.Test() as test:
"vlan": [
{
"vid": 1,
"untagged": [ddata0, ddata1]
"untagged": [ddata1, ddata2]
},
]
}
}
},
{
"name": ddata0,
"infix-interfaces:bridge-port": {
"pvid": 1,
"bridge": "br0"
}
},
{
"name": f"{ddata0}.10",
"type": "infix-if-type:vlan",
"vlan": {
"id": 10,
"lower-layer-if": ddata0,
},
"ipv4": {
"address": [
{
"ip": "10.10.1.2",
"prefix-length": 24,
}
]
}
},
{
"name": ddata1,
"infix-interfaces:bridge-port": {
@@ -96,6 +73,29 @@ with infamy.Test() as test:
"id": 10,
"lower-layer-if": ddata1,
},
"ipv4": {
"address": [
{
"ip": "10.10.1.2",
"prefix-length": 24,
}
]
}
},
{
"name": ddata2,
"infix-interfaces:bridge-port": {
"pvid": 1,
"bridge": "br0"
}
},
{
"name": f"{ddata2}.10",
"type": "infix-if-type:vlan",
"vlan": {
"id": 10,
"lower-layer-if": ddata2,
},
"ipv4": {
"address": [
{
@@ -110,8 +110,8 @@ with infamy.Test() as test:
}
})
with infamy.IsolatedMacVlan(hdata0) as ns0, \
infamy.IsolatedMacVlan(hdata1) as ns1:
with infamy.IsolatedMacVlan(hdata1) as ns0, \
infamy.IsolatedMacVlan(hdata2) as ns1:
with test.step("Configure IP addresses and VLAN interfaces on host"):
ns0.addip("10.0.1.1")
@@ -128,17 +128,17 @@ with infamy.Test() as test:
ip addr add 10.10.2.1/24 dev vlan10
""")
with test.step("Verify that host:data0 reaches host:data1 with untagged packets"):
with test.step("Verify that host:data1 reaches host:data2 with untagged packets"):
ns0.must_reach("10.0.1.2")
with test.step("Verify that traffic on VLAN 10 from host:data0 does not reach host:data1"):
with test.step("Verify that traffic on VLAN 10 from host:data1 does not reach host:data2"):
infamy.parallel(lambda: ns0.runsh("timeout -s INT 5 ping -i 0.2 -b 10.10.1.255 || true"),
lambda: ns1.must_not_receive("ip src 10.10.1.1"))
with test.step("Verify that host:data0 can reach target on VLAN 10"):
with test.step("Verify that host:data1 can reach target on VLAN 10"):
ns0.must_reach("10.10.1.2")
with test.step("Verify that host:data1 can reach target on VLAN 10"):
with test.step("Verify that host:data2 can reach target on VLAN 10"):
ns1.must_reach("10.10.2.2")
test.succeed()
@@ -14,11 +14,11 @@
<polygon fill="none" stroke="black" points="0,-0.5 0,-69.5 108,-69.5 108,-0.5 0,-0.5"/>
<text text-anchor="middle" x="25" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-0.5 50,-69.5 "/>
<text text-anchor="middle" x="79" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">tgt</text>
<text text-anchor="middle" x="79" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="50,-46.5 108,-46.5 "/>
<text text-anchor="middle" x="79" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data0</text>
<text text-anchor="middle" x="79" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
<polyline fill="none" stroke="black" points="50,-23.5 108,-23.5 "/>
<text text-anchor="middle" x="79" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
<text text-anchor="middle" x="79" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data2</text>
</g>
<!-- target -->
<g id="node2" class="node">
@@ -26,25 +26,25 @@
<polygon fill="none" stroke="black" points="308.03,-0.5 308.03,-69.5 432.03,-69.5 432.03,-0.5 308.03,-0.5"/>
<text text-anchor="middle" x="337.03" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="308.03,-46.5 366.03,-46.5 "/>
<text text-anchor="middle" x="337.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data0</text>
<text text-anchor="middle" x="337.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
<polyline fill="none" stroke="black" points="308.03,-23.5 366.03,-23.5 "/>
<text text-anchor="middle" x="337.03" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
<text text-anchor="middle" x="337.03" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data2</text>
<polyline fill="none" stroke="black" points="366.03,-0.5 366.03,-69.5 "/>
<text text-anchor="middle" x="399.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
</g>
<!-- host&#45;&#45;target -->
<g id="edge1" class="edge">
<title>host:tgt&#45;&#45;target:mgmt</title>
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M108,-58C108,-58 308.03,-58 308.03,-58"/>
<title>host:mgmt&#45;&#45;target:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-58C108,-58 308.03,-58 308.03,-58"/>
</g>
<!-- host&#45;&#45;target -->
<g id="edge2" class="edge">
<title>host:data0&#45;&#45;target:data0</title>
<title>host:data1&#45;&#45;target:data1</title>
<path fill="none" stroke="black" stroke-width="2" d="M108,-35C108,-35 308.03,-35 308.03,-35"/>
</g>
<!-- host&#45;&#45;target -->
<g id="edge3" class="edge">
<title>host:data1&#45;&#45;target:data1</title>
<title>host:data2&#45;&#45;target:data2</title>
<path fill="none" stroke="black" stroke-width="2" d="M108,-12C108,-12 308.03,-12 308.03,-12"/>
</g>
</g>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -7,14 +7,14 @@ attached to a VLAN filtering bridge are always locally terminated.
.---------------------------.
| target |
| |
| data0.10 br0 data1.10 |
| data1.10 br0 data2.10 |
| \ / \ / |
'------data0-----data1------'
'------data1-----data2------'
| |
| |
.------data0-----data1------.
.------data1-----data2------.
| / : \ |
| data0.10 : data1.10 |
| data1.10 : data2.10 |
| : |
| host |
| : |
@@ -22,7 +22,7 @@ attached to a VLAN filtering bridge are always locally terminated.
....
In this setup, even though VLAN 10 is allowed to ingress and egress on
both `data0` and `data1`, _bridging_ of packets from one to the other
both `data1` and `data2`, _bridging_ of packets from one to the other
must _not_ be allowed.
==== Topology
@@ -41,10 +41,10 @@ endif::topdoc[]
. Set up topology and attach to target
. Configure bridge and VLAN interfaces on target
. Configure IP addresses and VLAN interfaces on host
. Verify that host:data0 reaches host:data1 with untagged packets
. Verify that traffic on VLAN 10 from host:data0 does not reach host:data1
. Verify that host:data0 can reach target on VLAN 10
. Verify that host:data1 reaches host:data2 with untagged packets
. Verify that traffic on VLAN 10 from host:data1 does not reach host:data2
. Verify that host:data1 can reach target on VLAN 10
. Verify that host:data2 can reach target on VLAN 10
<<<
+8 -6
View File
@@ -3,14 +3,16 @@
<<<
include::mdns_enable_disable/Readme.adoc[]
include::mdns/mdns_enable_disable/Readme.adoc[]
include::mdns_allow_deny/Readme.adoc[]
include::mdns/mdns_allow_deny/Readme.adoc[]
include::lldp_enable_disable/Readme.adoc[]
include::lldp/lldp_enable_disable/Readme.adoc[]
include::lldp_admin_status/Readme.adoc[]
include::lldp/lldp_admin_status/Readme.adoc[]
include::ssh_server_config/Readme.adoc[]
include::lldp/lldp_ieee_group_forward/Readme.adoc[]
include::ssh_key_authentication/Readme.adoc[]
include::ssh/ssh_server_config/Readme.adoc[]
include::ssh/ssh_key_authentication/Readme.adoc[]
+3 -3
View File
@@ -1,9 +1,9 @@
---
- name: lldp
suite: lldp.yaml
suite: lldp/lldp.yaml
- name: mdns
suite: mdns.yaml
suite: mdns/mdns.yaml
- name: ssh
suite: ssh.yaml
suite: ssh/ssh.yaml
@@ -4,3 +4,6 @@
- name: lldp_admin_status
case: lldp_admin_status/test.py
- name: lldp_ieee_group_forward
case: lldp_ieee_group_forward/test.py
@@ -4,11 +4,11 @@ Verify that LLDP admin status is set properly by lldpd
==== Topology
ifdef::topdoc[]
image::{topdoc}../../test/case/infix_services/lldp_admin_status/topology.svg[LLDP admin status topology]
image::{topdoc}../../test/case/infix_services/lldp/lldp_admin_status/topology.svg[LLDP admin status topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::lldp_admin_status/topology.svg[LLDP admin status topology]
image::lldp/lldp_admin_status/topology.svg[LLDP admin status topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[LLDP admin status topology]
@@ -22,7 +22,7 @@ def capture_traffic(iface, sec):
return sniffer.output()
def send_lldp_packet(iface, chassis_id, chassis_id_subtype, ttl=3):
eth = Ether(dst="01:80:c2:00:00:0e", type=0x88cc)
eth = Ether(src="02:01:02:03:04:05", dst="01:80:C2:00:00:0E", type=0x88cc)
lldpdu = eth / LLDPDU()
lldpdu /= LLDPDUChassisID(subtype=chassis_id_subtype, id=chassis_id)
lldpdu /= LLDPDUPortID(subtype=5, id=iface)
@@ -45,7 +45,7 @@ def verify_admin_status(test, target, port, admin_status, local_capture, remote_
"lldp": {
"port": [{
"name": target["data"],
"dest-mac-address": "00-00-00-00-00-00",
"dest-mac-address": "01:80:C2:00:00:0E",
"admin-status": admin_status
}]
}

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -5,11 +5,11 @@ Operation and non-operation are confirmed using tcpdump.
==== Topology
ifdef::topdoc[]
image::{topdoc}../../test/case/infix_services/lldp_enable_disable/topology.svg[LLDP enable disable topology]
image::{topdoc}../../test/case/infix_services/lldp/lldp_enable_disable/topology.svg[LLDP enable disable topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::lldp_enable_disable/topology.svg[LLDP enable disable topology]
image::lldp/lldp_enable_disable/topology.svg[LLDP enable disable topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[LLDP enable disable topology]

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1 @@
lldp_ieee_group_forward.adoc
@@ -0,0 +1,27 @@
=== LLDP IEEE Group Forward
==== Description
Verify that LLDP packets can be flooded to all ports on a bridge.
Operation and non-operation are confirmed using tcpdump.
==== Topology
ifdef::topdoc[]
image::{topdoc}../../test/case/infix_services/lldp/lldp_ieee_group_forward/topology.svg[LLDP IEEE Group Forward topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::lldp/lldp_ieee_group_forward/topology.svg[LLDP IEEE Group Forward topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[LLDP IEEE Group Forward topology]
endif::testgroup[]
endif::topdoc[]
==== Test sequence
. Set up topology and attach to target DUT
. Configure interfaces and disable LLDP daemon
. Verify LLDP absence on host:data2
. Enable LLDP flooding by setting group forward
. Verify LLDP arrival on host:data2
<<<
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""LLDP IEEE Group Forward
Verify that LLDP packets can be flooded to all ports on a bridge.
Operation and non-operation are confirmed using tcpdump.
"""
import time
import threading
import infamy
from scapy.all import Ether, sendp
from scapy.contrib.lldp import (
LLDPDU, LLDPDUChassisID, LLDPDUPortID,
LLDPDUTimeToLive, LLDPDUEndOfLLDPDU
)
SNIFFING_DURATION = 3
LLDP_INTERVAL = 1
LLDP_TTL = 3
LLDP_MULTICAST_MAC = "01:80:C2:00:00:0E"
LLDP_ETHERTYPE = 0x88cc
CHASSIS_ID = "Chassis-Test"
SRC_MAC = "02:01:02:03:04:05"
BRIDGE = "br0"
def send_lldp_packet(iface):
eth = Ether(src=SRC_MAC, dst=LLDP_MULTICAST_MAC, type=LLDP_ETHERTYPE)
lldpdu = eth / LLDPDU()
lldpdu /= LLDPDUChassisID(subtype=7, id=CHASSIS_ID)
lldpdu /= LLDPDUPortID(subtype=5, id=iface)
lldpdu /= LLDPDUTimeToLive(ttl=LLDP_TTL)
lldpdu /= LLDPDUEndOfLLDPDU()
sendp(lldpdu, iface=iface, verbose=False)
def send_lldp_packets_continuously(iface, stop_event):
time.sleep(LLDP_INTERVAL/2)
while not stop_event.is_set():
send_lldp_packet(iface)
time.sleep(LLDP_INTERVAL)
def capture_traffic(iface):
with infamy.IsolatedMacVlan(iface) as netns:
sniffer = infamy.Sniffer(netns, f"ether proto {LLDP_ETHERTYPE}")
with sniffer:
time.sleep(SNIFFING_DURATION)
return sniffer.output()
def start_lldp_sender_thread(iface):
stop = threading.Event()
thread = threading.Thread(
target=send_lldp_packets_continuously,
args=(iface, stop)
)
thread.start()
return stop, thread
with infamy.Test() as test:
with test.step("Set up topology and attach to target DUT"):
env = infamy.Env()
target = env.attach("target", "mgmt")
_, hdata1 = env.ltop.xlate("host", "data1")
_, hdata2 = env.ltop.xlate("host", "data2")
with test.step("Configure interfaces and disable LLDP daemon"):
target.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [
{
"name": BRIDGE,
"type": "infix-if-type:bridge",
"enabled": True,
},
{
"name": target["data1"],
"enabled": True,
"infix-interfaces:bridge-port": {
"bridge": "br0"
}
},
{
"name": target["data2"],
"enabled": True,
"infix-interfaces:bridge-port": {
"bridge": "br0"
}
}
]
}
},
"ieee802-dot1ab-lldp": {
"lldp": {
"enabled": False
}
}
})
stop_event, sender_thread = start_lldp_sender_thread(hdata1)
try:
with test.step("Verify LLDP absence on host:data2"):
captured_traffic = capture_traffic(hdata2)
if "LLDP" in captured_traffic.stdout:
test.fail()
finally:
stop_event.set()
sender_thread.join()
with test.step("Enable LLDP flooding by setting group forward"):
target.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [
{
"name": BRIDGE,
"infix-interfaces:bridge": {
"ieee-group-forward": [
"lldp"
]
}
}
]
}
}
})
stop_event, sender_thread = start_lldp_sender_thread(hdata1)
try:
with test.step("Verify LLDP arrival on host:data2"):
captured_traffic = capture_traffic(hdata2)
if "LLDP" not in captured_traffic.stdout:
test.fail()
finally:
stop_event.set()
sender_thread.join()
test.succeed()
@@ -0,0 +1,24 @@
graph "1x3" {
layout="neato";
overlap="false";
esep="+80";
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="host | { <mgmt> mgmt | <data1> data1 | <data2> data2 }",
pos="0,12!",
requires="controller",
];
target [
label="{ <mgmt> mgmt | <data1> data1 | <data2> data2 } | target",
pos="10,12!",
requires="infix",
];
host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
host:data1 -- target:data1 [color="black", requires="ieee-mc"]
host:data2 -- target:data2 [color="black", requires="ieee-mc"]
}

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -10,11 +10,11 @@ with three scenarios:
==== Topology
ifdef::topdoc[]
image::{topdoc}../../test/case/infix_services/mdns_allow_deny/topology.svg[mDNS allow/deny interfaces topology]
image::{topdoc}../../test/case/infix_services/mdns/mdns_allow_deny/topology.svg[mDNS allow/deny interfaces topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::mdns_allow_deny/topology.svg[mDNS allow/deny interfaces topology]
image::mdns/mdns_allow_deny/topology.svg[mDNS allow/deny interfaces topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[mDNS allow/deny interfaces topology]

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -5,11 +5,11 @@ Operation and non-operation are confirmed using tcpdump.
==== Topology
ifdef::topdoc[]
image::{topdoc}../../test/case/infix_services/mdns_enable_disable/topology.svg[mDNS enable disable topology]
image::{topdoc}../../test/case/infix_services/mdns/mdns_enable_disable/topology.svg[mDNS enable disable topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::mdns_enable_disable/topology.svg[mDNS enable disable topology]
image::mdns/mdns_enable_disable/topology.svg[mDNS enable disable topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[mDNS enable disable topology]

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -4,11 +4,11 @@ Verify that 'guest' user can fetch data using only the 'public' key
==== Topology
ifdef::topdoc[]
image::{topdoc}../../test/case/infix_services/ssh_key_authentication/topology.svg[Generate ssh key pair topology]
image::{topdoc}../../test/case/infix_services/ssh/ssh_key_authentication/topology.svg[Generate ssh key pair topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::ssh_key_authentication/topology.svg[Generate ssh key pair topology]
image::ssh/ssh_key_authentication/topology.svg[Generate ssh key pair topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[Generate ssh key pair topology]
@@ -0,0 +1 @@
../../../../infamy/topologies/1x1.dot

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -7,11 +7,11 @@ Test SSH server functionality with pre-defined key pair:
==== Topology
ifdef::topdoc[]
image::{topdoc}../../test/case/infix_services/ssh_server_config/topology.svg[SSH server configuration topology]
image::{topdoc}../../test/case/infix_services/ssh/ssh_server_config/topology.svg[SSH server configuration topology]
endif::topdoc[]
ifndef::topdoc[]
ifdef::testgroup[]
image::ssh_server_config/topology.svg[SSH server configuration topology]
image::ssh/ssh_server_config/topology.svg[SSH server configuration topology]
endif::testgroup[]
ifndef::testgroup[]
image::topology.svg[SSH server configuration topology]
@@ -0,0 +1 @@
../../../../infamy/topologies/1x3.dot
@@ -0,0 +1,51 @@
<?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: 1x3 Pages: 1 -->
<svg width="440pt" height="78pt"
viewBox="0.00 0.00 440.03 78.00" 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 74)">
<title>1x3</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-74 436.03,-74 436.03,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-0.5 0,-69.5 108,-69.5 108,-0.5 0,-0.5"/>
<text text-anchor="middle" x="25" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-0.5 50,-69.5 "/>
<text text-anchor="middle" x="79" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="50,-46.5 108,-46.5 "/>
<text text-anchor="middle" x="79" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
<polyline fill="none" stroke="black" points="50,-23.5 108,-23.5 "/>
<text text-anchor="middle" x="79" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data2</text>
</g>
<!-- target -->
<g id="node2" class="node">
<title>target</title>
<polygon fill="none" stroke="black" points="308.03,-0.5 308.03,-69.5 432.03,-69.5 432.03,-0.5 308.03,-0.5"/>
<text text-anchor="middle" x="337.03" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="308.03,-46.5 366.03,-46.5 "/>
<text text-anchor="middle" x="337.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
<polyline fill="none" stroke="black" points="308.03,-23.5 366.03,-23.5 "/>
<text text-anchor="middle" x="337.03" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data2</text>
<polyline fill="none" stroke="black" points="366.03,-0.5 366.03,-69.5 "/>
<text text-anchor="middle" x="399.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
</g>
<!-- host&#45;&#45;target -->
<g id="edge1" class="edge">
<title>host:mgmt&#45;&#45;target:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-58C108,-58 308.03,-58 308.03,-58"/>
</g>
<!-- host&#45;&#45;target -->
<g id="edge2" class="edge">
<title>host:data1&#45;&#45;target:data1</title>
<path fill="none" stroke="black" stroke-width="2" d="M108,-35C108,-35 308.03,-35 308.03,-35"/>
</g>
<!-- host&#45;&#45;target -->
<g id="edge3" class="edge">
<title>host:data2&#45;&#45;target:data2</title>
<path fill="none" stroke="black" stroke-width="2" d="M108,-12C108,-12 308.03,-12 308.03,-12"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -1 +0,0 @@
../../../infamy/topologies/1x1.dot
@@ -1,24 +0,0 @@
graph "1x3" {
layout="neato";
overlap="false";
esep="+80";
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="host | { <mgmt> mgmt | <data1> data1 | <data2> data2 }",
pos="0,12!",
requires="controller",
];
target [
label="{ <mgmt> mgmt | <data1> data1 | <data2> data2 } | target",
pos="10,12!",
requires="infix",
];
host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
host:data1 -- target:data1 [color="black"]
host:data2 -- target:data2 [color="black"]
}
+12 -13
View File
@@ -4,22 +4,21 @@ graph "1x3" {
esep="+80";
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="host | { <tgt> tgt | <data0> data0 | <data1> data1 }",
pos="0,12!",
requires="controller",
label="host | { <mgmt> mgmt | <data1> data1 | <data2> data2 }",
pos="0,12!",
requires="controller",
];
target [
label="{ <mgmt> mgmt | <data0> data0 | <data1> data1 } | target",
pos="10,12!",
requires="infix",
target [
label="{ <mgmt> mgmt | <data1> data1 | <data2> data2 } | target",
pos="10,12!",
requires="infix",
];
host:tgt -- target:mgmt [requires="mgmt"]
host:data0 -- target:data0 [color=black]
host:data1 -- target:data1 [color=black]
}
host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
host:data1 -- target:data1 [color="black"]
host:data2 -- target:data2 [color="black"]
}