Skip to content

Commit b2c6bd6

Browse files
committed
test: add SNMP smoke test
Enable the agent with a read-only community and query it from the host. Beyond the agent answering at all, the test checks that IF-MIB ifName at a given index names the interface with that if-index in the operational datastore, which is the switchdev property the feature rests on: every port is a netdev, so the index an NMS sees is the one served over NETCONF. Queries use numeric OIDs, the device carries no MIB text files. The container needs net-snmp-tools, so bump the image to 2.14. The suite checks the tools are present before querying, otherwise a stale image reads as a broken agent. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
1 parent 7b0f2a2 commit b2c6bd6

9 files changed

Lines changed: 266 additions & 1 deletion

File tree

‎test/.env‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# shellcheck disable=SC2034,SC2154
33

44
# Current container image
5-
INFIX_TEST=ghcr.io/kernelkit/infix-test:2.12
5+
INFIX_TEST=ghcr.io/kernelkit/infix-test:2.14
66

77
ixdir=$(readlink -f "$testdir/..")
88
logdir=$(readlink -f "$testdir/.log")

‎test/case/all.yaml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
- name: "Services"
4646
suite: services/all.yaml
4747

48+
- name: "SNMP"
49+
suite: snmp/all.yaml
50+
4851
- name: "Use Case Tests"
4952
suite: use_case/all.yaml
5053

‎test/case/snmp/all.yaml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
- name: Basic
3+
case: basic/test.py

‎test/case/snmp/basic/Readme.adoc‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test.adoc

‎test/case/snmp/basic/test.adoc‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
=== Basic
2+
3+
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/snmp/basic]
4+
5+
==== Description
6+
7+
Enable the SNMP agent with a read-only community and query it over
8+
SNMPv2c from the host. Verify sysObjectID, that IF-MIB ifName at a given index
9+
matches the interface of that if-index in the operational datastore,
10+
that ifHCInOctets is consistent with the interface statistics, that an
11+
unknown community is refused, and that disabling the agent stops it
12+
answering.
13+
14+
Queries use numeric OIDs, so the test does not depend on MIB text files
15+
being installed in the container.
16+
17+
==== Topology
18+
19+
image::topology.svg[Basic topology, align=center, scaledwidth=75%]
20+
21+
==== Sequence
22+
23+
. Set up topology and attach to target DUT
24+
. Set IPv4 address 10.0.0.10/24 on target:data
25+
. Enable SNMP agent with a read-only community
26+
. Read if-index and in-octets for target:data from operational
27+
. Verify the host has the net-snmp client tools
28+
. Verify agent reports the Infix sysObjectID
29+
. Verify IF-MIB ifName at if-index matches the interface name
30+
. Verify IF-MIB ifHCInOctets is consistent with operational data
31+
. Verify an unknown community is refused
32+
. Disable SNMP agent
33+
. Verify agent no longer responds
34+
35+

‎test/case/snmp/basic/test.py‎

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env python3
2+
"""SNMP basic
3+
4+
Enable the SNMP agent with a read-only community and query it over
5+
SNMPv2c from the host. Verify sysObjectID, that IF-MIB ifName at a given index
6+
matches the interface of that if-index in the operational datastore,
7+
that ifHCInOctets is consistent with the interface statistics, that an
8+
unknown community is refused, and that disabling the agent stops it
9+
answering.
10+
11+
Queries use numeric OIDs, so the test does not depend on MIB text files
12+
being installed in the container.
13+
14+
"""
15+
16+
import time
17+
18+
import infamy
19+
import infamy.iface as iface
20+
21+
# SNMPv2-MIB::sysObjectID.0, and the arc confd hands out for Infix
22+
SYSOBJECTID = ".1.3.6.1.2.1.1.2.0"
23+
INFIX_OID = ".1.3.6.1.4.1.61046.1.1.1"
24+
25+
# IF-MIB::ifName (ifXTable) and IF-MIB::ifHCInOctets
26+
IFNAME = ".1.3.6.1.2.1.31.1.1.1.1"
27+
IFHCINOCTETS = ".1.3.6.1.2.1.31.1.1.1.6"
28+
29+
COMMUNITY = "infix-test"
30+
TARGET_IP = "10.0.0.10"
31+
32+
33+
def retry(fn, attempts=10, interval=1):
34+
"""Poll fn until it returns something truthy, None on timeout.
35+
36+
infamy.until() raises instead, which turns a quiet agent into a
37+
traceback rather than a failed step saying what was expected.
38+
"""
39+
for _ in range(attempts):
40+
val = fn()
41+
if val:
42+
return val
43+
time.sleep(interval)
44+
45+
return None
46+
47+
48+
def snmpget(netns, oid, community=COMMUNITY):
49+
"""Return the value half of a single-varbind response, or None."""
50+
rc = netns.runsh(f"snmpget -v2c -c {community} -r 1 -t 2 -On -Oqv "
51+
f"{TARGET_IP} {oid}")
52+
if rc.returncode != 0:
53+
return None
54+
55+
out = rc.stdout.strip()
56+
return out if out and "No Such" not in out and "Timeout" not in out else None
57+
58+
59+
with infamy.Test() as test:
60+
with test.step("Set up topology and attach to target DUT"):
61+
env = infamy.Env()
62+
target = env.attach("target", "mgmt")
63+
_, hport = env.ltop.xlate("host", "data")
64+
65+
if not target.has_model("ietf-snmp"):
66+
test.skip()
67+
68+
with test.step("Set IPv4 address 10.0.0.10/24 on target:data"):
69+
target.put_config_dicts({
70+
"ietf-interfaces": {
71+
"interfaces": {
72+
"interface": [{
73+
"name": target["data"],
74+
"enabled": True,
75+
"ipv4": {
76+
"address": [{
77+
"ip": TARGET_IP,
78+
"prefix-length": 24
79+
}]
80+
}
81+
}]
82+
}
83+
}
84+
})
85+
86+
with test.step("Enable SNMP agent with a read-only community"):
87+
# No version and no listen: the agent should then serve every
88+
# version it supports, on every address, port 161.
89+
target.put_config_dicts({
90+
"ietf-snmp": {
91+
"snmp": {
92+
"engine": {"enabled": True},
93+
"community": [{
94+
"index": "monitor",
95+
"text-name": COMMUNITY,
96+
"security-name": "monitor"
97+
}]
98+
}
99+
}
100+
})
101+
102+
with test.step("Read if-index and in-octets for target:data from operational"):
103+
ifindex = iface.get_param(target, target["data"], "if-index")
104+
stats = iface.get_param(target, target["data"], "statistics") or {}
105+
octets = int(stats.get("in-octets", 0))
106+
print(f"target: {target['data']} is if-index {ifindex}, in-octets {octets}")
107+
if ifindex is None:
108+
print(f"target: no if-index for {target['data']}")
109+
test.fail()
110+
111+
with infamy.IsolatedMacVlan(hport) as netns:
112+
netns.addip("10.0.0.1")
113+
114+
with test.step("Verify the host has the net-snmp client tools"):
115+
# Without this the queries below all come back empty and
116+
# every failure looks like the agent not answering.
117+
if netns.runsh("command -v snmpget").returncode != 0:
118+
print("host: snmpget not found, the test container image "
119+
"needs the net-snmp-tools package")
120+
test.fail()
121+
122+
with test.step("Verify agent reports the Infix sysObjectID"):
123+
# The agent has just been started, give it a moment to bind.
124+
val = retry(lambda: snmpget(netns, SYSOBJECTID))
125+
print(f"host: sysObjectID.0 = {val}")
126+
if val != INFIX_OID:
127+
test.fail()
128+
129+
with test.step("Verify IF-MIB ifName at if-index matches the interface name"):
130+
val = snmpget(netns, f"{IFNAME}.{ifindex}")
131+
print(f"host: ifName.{ifindex} = {val}")
132+
if val != target["data"]:
133+
test.fail()
134+
135+
with test.step("Verify IF-MIB ifHCInOctets is consistent with operational data"):
136+
val = snmpget(netns, f"{IFHCINOCTETS}.{ifindex}")
137+
print(f"host: ifHCInOctets.{ifindex} = {val}")
138+
if val is None or not val.isdigit() or int(val) < octets:
139+
test.fail()
140+
141+
with test.step("Verify an unknown community is refused"):
142+
if snmpget(netns, SYSOBJECTID, "wrong-community") is not None:
143+
test.fail()
144+
145+
with test.step("Disable SNMP agent"):
146+
target.put_config_dicts({
147+
"ietf-snmp": {"snmp": {"engine": {"enabled": False}}}
148+
})
149+
150+
with test.step("Verify agent no longer responds"):
151+
if not retry(lambda: snmpget(netns, SYSOBJECTID) is None):
152+
print("host: agent still answering after being disabled")
153+
test.fail()
154+
155+
test.succeed()

‎test/case/snmp/basic/topology.dot‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
graph "1x2" {
2+
layout="neato";
3+
overlap="false";
4+
esep="+80";
5+
6+
node [shape=record, fontname="DejaVu Sans Mono, Book"];
7+
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
8+
9+
host [
10+
label="host | { <mgmt> mgmt | <data> data }",
11+
pos="0,12!",
12+
requires="controller",
13+
];
14+
15+
target [
16+
label="{ <mgmt> mgmt | <data> data } | target",
17+
pos="10,12!",
18+
requires="infix",
19+
];
20+
21+
host:mgmt -- target:mgmt [requires="mgmt", color="lightgrey"]
22+
host:data -- target:data [color=black, fontcolor=black, fontsize=12, taillabel="10.0.0.1/24", headlabel="10.0.0.10/24"]
23+
}

‎test/case/snmp/basic/topology.svg‎

Lines changed: 44 additions & 0 deletions
Loading

‎test/docker/Dockerfile‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ RUN apk add --no-cache \
3333
linux-headers \
3434
make \
3535
ncurses-dev \
36+
net-snmp-tools \
3637
nmap \
3738
openssh-client \
3839
openssl \

0 commit comments

Comments
 (0)