diff --git a/test/case/ietf_interfaces/Readme.adoc b/test/case/ietf_interfaces/Readme.adoc
index ff5f893e..42f6061e 100644
--- a/test/case/ietf_interfaces/Readme.adoc
+++ b/test/case/ietf_interfaces/Readme.adoc
@@ -33,6 +33,8 @@ include::bridge_vlan_separation/Readme.adoc[]
include::dual_bridge/Readme.adoc[]
+include::lag_basic/Readme.adoc[]
+
include::igmp_basic/Readme.adoc[]
include::igmp_vlan/Readme.adoc[]
diff --git a/test/case/ietf_interfaces/ietf_interfaces.yaml b/test/case/ietf_interfaces/ietf_interfaces.yaml
index 2f2bbe8e..4709465b 100644
--- a/test/case/ietf_interfaces/ietf_interfaces.yaml
+++ b/test/case/ietf_interfaces/ietf_interfaces.yaml
@@ -35,6 +35,9 @@
- name: ipv4_autoconf
case: ipv4_autoconf/test.py
+- name: lag_basic
+ case: lag_basic/test.py
+
- name: bridge_fwd_sgl_dut
case: bridge_fwd_sgl_dut/test.py
diff --git a/test/case/ietf_interfaces/lag_basic/Readme.adoc b/test/case/ietf_interfaces/lag_basic/Readme.adoc
new file mode 120000
index 00000000..ae7efc4b
--- /dev/null
+++ b/test/case/ietf_interfaces/lag_basic/Readme.adoc
@@ -0,0 +1 @@
+lag_basic.adoc
\ No newline at end of file
diff --git a/test/case/ietf_interfaces/lag_basic/lag-basic.svg b/test/case/ietf_interfaces/lag_basic/lag-basic.svg
new file mode 100644
index 00000000..b25aa63b
--- /dev/null
+++ b/test/case/ietf_interfaces/lag_basic/lag-basic.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/test/case/ietf_interfaces/lag_basic/lag_basic.adoc b/test/case/ietf_interfaces/lag_basic/lag_basic.adoc
new file mode 100644
index 00000000..32fec881
--- /dev/null
+++ b/test/case/ietf_interfaces/lag_basic/lag_basic.adoc
@@ -0,0 +1,43 @@
+=== Ling Aggregation Basic
+==== Description
+Verify communication over a link aggregate in static and LACP operating
+modes during basic failure scenarios.
+
+.Internal network setup, PC verifies connectivity with dut2 via dut1
+ifdef::topdoc[]
+image::../../test/case/ietf_interfaces/lag_basic/lag-basic.svg[Internal networks]
+endif::topdoc[]
+ifndef::topdoc[]
+ifdef::testgroup[]
+image::lag_basic/lag-basic.svg[Internal networks]
+endif::testgroup[]
+ifndef::testgroup[]
+image::lag-basic.svg[Internal networks]
+endif::testgroup[]
+endif::topdoc[]
+
+The host verifies connectivity with dut2 via dut1 over the aggregate for
+each test step using the `mon` interface.
+
+==== Topology
+ifdef::topdoc[]
+image::{topdoc}../../test/case/ietf_interfaces/lag_basic/topology.svg[Ling Aggregation Basic topology]
+endif::topdoc[]
+ifndef::topdoc[]
+ifdef::testgroup[]
+image::lag_basic/topology.svg[Ling Aggregation Basic topology]
+endif::testgroup[]
+ifndef::testgroup[]
+image::topology.svg[Ling Aggregation Basic topology]
+endif::testgroup[]
+endif::topdoc[]
+==== Test sequence
+. Set up topology and attach to target DUTs
+. Set up LACP link aggregate, lag0, on dut1 and dut2
+. Verify failure modes for lacp mode
+. Set up static link aggregate, lag0, on dut1 and dut2
+. Verify failure modes for static mode
+
+
+<<<
+
diff --git a/test/case/ietf_interfaces/lag_basic/test.py b/test/case/ietf_interfaces/lag_basic/test.py
new file mode 100755
index 00000000..9cbd0d25
--- /dev/null
+++ b/test/case/ietf_interfaces/lag_basic/test.py
@@ -0,0 +1,165 @@
+#!/usr/bin/env python3
+r"""Ling Aggregation Basic
+
+Verify communication over a link aggregate in static and LACP operating
+modes during basic failure scenarios.
+
+.Internal network setup, PC verifies connectivity with dut2 via dut1
+image::lag-basic.svg[Internal networks]
+
+The host verifies connectivity with dut2 via dut1 over the aggregate for
+each test step using the `mon` interface.
+
+"""
+from time import sleep, time
+from datetime import datetime
+import infamy
+from infamy.util import parallel, until
+
+
+class DumbLinkBreaker:
+ """Encapsulates basic, dumb link-breaking ops over SSH."""
+
+ def __init__(self, sys, dut, netns):
+ self.env = sys
+ self.dut = dut
+ self.net = netns
+ self.tgt = {}
+ for i, (name, _) in dut.items():
+ self.tgt[i] = env.attach(name, "mgmt", "ssh")
+
+ def set_link(self, link, updown):
+ """Set link up or down, verify before returning."""
+ def set_and_verify(i):
+ name, dut = self.dut[i]
+
+ cmd = self.tgt[i].runsh(f"sudo ip link set {dut[link]} {updown}")
+ if cmd.returncode:
+ for out in [cmd.stdout, cmd.stderr]:
+ if out:
+ print(f"{name}: {out.rstrip()}")
+ raise RuntimeError(f"{name}: failed setting {link} {updown}")
+
+ for _ in range(10):
+ sleep(0.1)
+ check = self.tgt[i].runsh(f"ip link show {dut[link]}")
+ if f"state {updown.upper()}" in check.stdout:
+ break
+ else:
+ raise RuntimeError(f"{name}: {dut[link]} did not go {updown}")
+
+ parallel(*[lambda i=i: set_and_verify(i) for i in self.dut])
+
+ def fail_check(self, peer):
+ """Verify connectivity with peer during link failure."""
+ sequence = [
+ [("link1", "up"), ("link2", "up")],
+ [("link1", "down"), ("link2", "up")],
+ [("link1", "up"), ("link2", "down")],
+ [("link1", "up"), ("link2", "up")]
+ ]
+
+ total_start = time()
+ for state in sequence:
+ state_start = time()
+ print(f"{datetime.now().strftime('%H:%M:%S.%f')[:-3]} {state}")
+
+ for link, updown in state:
+ self.set_link(link, updown)
+ self.net.must_reach(peer, timeout=10)
+
+ print(f"Completed in {time() - state_start:.2f}s")
+
+ print(f"Total time: {time() - total_start:.2f}s")
+
+
+def lag_init(mode):
+ """Set up mode specific attributes for the LAG"""
+ if mode == "lacp":
+ lag = [{
+ "name": "lag0",
+ "lag": {"lacp": {"rate": "fast"}}
+ }]
+ else:
+ lag = []
+ return lag
+
+
+def net_init(host, addr):
+ """Set up DUT network, dut1 bridges host port with lag0"""
+ if host:
+ net = [{
+ "name": "br0",
+ "type": "infix-if-type:bridge",
+ }, {
+ "name": host,
+ "bridge-port": {"bridge": "br0"}
+ }, {
+ "name": "lag0",
+ "bridge-port": {"bridge": "br0"}
+ }]
+ else:
+ net = [{
+ "name": "lag0",
+ "ipv4": {
+ "address": [{"ip": addr, "prefix-length": 24}]
+ }
+ }]
+ return net
+
+
+def dut_init(dut, mode, addr):
+ """Set up link aggregate on dut"""
+ net = net_init(dut["mon"], addr)
+ lag = lag_init(mode)
+
+ dut.put_config_dict("ietf-interfaces", {
+ "interfaces": {
+ "interface": [{
+ "name": "lag0",
+ "type": "infix-if-type:lag",
+ "lag": {
+ "mode": mode,
+ "link-monitor": {"interval": 100}
+ }
+ }, {
+ "name": dut["link1"],
+ "lag-port": {"lag": "lag0"}
+ }, {
+ "name": dut["link2"],
+ "lag-port": {"lag": "lag0"}
+ }] + net + lag
+ }
+ })
+
+
+with infamy.Test() as test:
+ with test.step("Set up topology and attach to target DUTs"):
+ env = infamy.Env()
+ dut1 = env.attach("dut1", "mgmt")
+ dut2 = env.attach("dut2", "mgmt")
+
+ _, mon = env.ltop.xlate("host", "mon")
+ with infamy.IsolatedMacVlan(mon) as ns:
+ dm = {
+ '1': ("dut1", dut1),
+ '2': ("dut2", dut2)
+ }
+ lb = DumbLinkBreaker(env, dm, ns)
+ ns.addip("192.168.2.1")
+
+ with test.step("Set up LACP link aggregate, lag0, on dut1 and dut2"):
+ parallel(lambda: dut_init(dut1, "lacp", None),
+ lambda: dut_init(dut2, "lacp", "192.168.2.42"))
+
+ with test.step("Verify failure modes for lacp mode"):
+ lb.fail_check("192.168.2.42")
+
+ with test.step("Set up static link aggregate, lag0, on dut1 and dut2"):
+ parallel(lambda: dut_init(dut1, "static", None),
+ lambda: dut_init(dut2, "static", "192.168.2.42"))
+
+ with test.step("Verify failure modes for static mode"):
+ lb.fail_check("192.168.2.42")
+
+ test.succeed()
diff --git a/test/case/ietf_interfaces/lag_basic/topology.dot b/test/case/ietf_interfaces/lag_basic/topology.dot
new file mode 100644
index 00000000..9715914e
--- /dev/null
+++ b/test/case/ietf_interfaces/lag_basic/topology.dot
@@ -0,0 +1,33 @@
+graph "lag" {
+ layout="neato";
+ overlap="false";
+ esep="+23";
+
+ node [shape=record, fontsize=12, fontname="DejaVu Sans Mono, Book"];
+ edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
+
+ host [
+ label="host | { mgmt1 | mon | \n\n\n\n | mgmt2 }",
+ pos="0,15!",
+ requires="controller",
+ ];
+
+ dut1 [
+ label="{ mgmt | mon } | { dut1\r | { link1 | link2 } }",
+ pos="2,15.25!",
+ requires="infix",
+ ];
+
+ dut2 [
+ label=" mgmt | { { link1 | link2 } | dut2\r }",
+ pos="2,14.75!",
+ requires="infix",
+ ];
+
+ host:mgmt1 -- dut1:mgmt [requires="mgmt", color=lightgray]
+ host:mon -- dut1:mon // Monitor connection to dut2 via dut1
+ host:mgmt2 -- dut2:mgmt [requires="mgmt", color=lightgrey]
+
+ dut1:link1 -- dut2:link1 [color=black, fontcolor=black, penwidth=3]
+ dut1:link2 -- dut2:link2 [color=black, fontcolor=black, penwidth=3]
+}
diff --git a/test/case/ietf_interfaces/lag_basic/topology.svg b/test/case/ietf_interfaces/lag_basic/topology.svg
new file mode 100644
index 00000000..d346d430
--- /dev/null
+++ b/test/case/ietf_interfaces/lag_basic/topology.svg
@@ -0,0 +1,76 @@
+
+
+
+
+