test: add new tests for NTP

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-01-04 12:28:28 +01:00
parent 64a04f5a1d
commit 39b9c7a065
32 changed files with 1440 additions and 3 deletions
+3
View File
@@ -32,6 +32,9 @@
- name: "Interfaces"
suite: interfaces/all.yaml
- name: "NTP Server"
suite: ntp/all.yaml
- name: "Routing"
suite: routing/all.yaml
+28
View File
@@ -0,0 +1,28 @@
:testgroup:
== NTP Server Tests
Tests for NTP server functionality across different operational modes:
- Standalone mode: local reference clock only
- Server mode: sync from upstream while serving clients
- Peer mode: bidirectional synchronization between peers
- Server and client interoperability
- Client stratum selection between multiple servers
include::server_mode_standalone/Readme.adoc[]
<<<
include::server_mode_server/Readme.adoc[]
<<<
include::server_mode_peer/Readme.adoc[]
<<<
include::server_client/Readme.adoc[]
<<<
include::client_stratum_selection/Readme.adoc[]
+15
View File
@@ -0,0 +1,15 @@
---
- name: NTP server standalone mode
case: server_mode_standalone/test.py
- name: NTP server mode
case: server_mode_server/test.py
- name: NTP peer mode
case: server_mode_peer/test.py
- name: NTP server and client interoperability
case: server_client/test.py
- name: NTP client stratum selection
case: client_stratum_selection/test.py
+1
View File
@@ -0,0 +1 @@
test.adoc
@@ -0,0 +1,34 @@
=== NTP client stratum selection
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ntp/client_stratum_selection]
==== Description
Verify NTP client properly selects between multiple servers based on
stratum level.
This test validates NTP clock selection algorithm by configuring a client
to sync from two servers with different stratum levels:
- srv1: Test PC running BusyBox ntpd (stratum ~1 via -l flag)
- srv2: NTP server DUT syncing from srv1 (stratum ~2)
- client: NTP client DUT syncing from both servers
Both servers sync to the same time source (srv2 syncs from srv1),
ensuring time agreement and avoiding the "falseticker" problem. The client
should then select srv1 (lower stratum) as its sync source.
==== Topology
image::topology.svg[NTP client stratum selection topology, align=center, scaledwidth=75%]
==== Sequence
. Set up topology and attach to devices
. Configure srv2 to sync from srv1 and serve with higher stratum
. Wait for srv2 to sync from srv1
. Configure client to sync from both servers
. Wait for client to see both servers
. Verify client selects srv1 (lower stratum)
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""NTP client stratum selection test
Verify NTP client properly selects between multiple servers based on
stratum level.
This test validates NTP clock selection algorithm by configuring a client
to sync from two servers with different stratum levels:
- srv1: Test PC running BusyBox ntpd (stratum ~1 via -l flag)
- srv2: NTP server DUT syncing from srv1 (stratum ~2)
- client: NTP client DUT syncing from both servers
Both servers sync to the same time source (srv2 syncs from srv1),
ensuring time agreement and avoiding the "falseticker" problem. The client
should then select srv1 (lower stratum) as its sync source.
"""
import infamy
from infamy import until
import infamy.ntp as ntp
import infamy.ntp_server as ntp_server
# Network configuration
ips = {
"srv1": "192.168.1.1", # BusyBox ntpd on test PC
"srv2": "192.168.1.2", # Infix NTP server
"client": "192.168.1.3" # Infix NTP client
}
with infamy.Test() as test:
with test.step("Set up topology and attach to devices"):
env = infamy.Env()
srv2 = env.attach("srv2", "mgmt")
client = env.attach("client", "mgmt")
_, swp1 = env.ltop.xlate("srv2", "swp1")
_, swp2 = env.ltop.xlate("srv2", "swp2")
_, eth0 = env.ltop.xlate("client", "eth0")
_, srv1 = env.ltop.xlate("host", "srv1")
with infamy.IsolatedMacVlan(srv1) as ns_srv1:
ns_srv1.addip(ips["srv1"])
with ntp_server.Server(ns_srv1):
with test.step("Configure srv2 to sync from srv1 and serve with higher stratum"):
srv2.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [{
"name": "br0",
"type": "infix-if-type:bridge",
"enabled": True,
"ipv4": {
"address": [{
"ip": ips["srv2"],
"prefix-length": 24,
}]
}
}, {
"name": swp1,
"enabled": True,
"infix-interfaces:bridge-port": {
"bridge": "br0"
}
}, {
"name": swp2,
"enabled": True,
"infix-interfaces:bridge-port": {
"bridge": "br0"
}
}]
}
},
"ietf-ntp": {
"ntp": {
"unicast-configuration": [{
"address": ips["srv1"], # Sync from srv1
"type": "uc-server",
"iburst": True
}]
}
}
})
with test.step("Wait for srv2 to sync from srv1"):
until(lambda: ntp.server_has_associations(srv2), attempts=60)
with test.step("Configure client to sync from both servers"):
client.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [{
"name": eth0,
"enabled": True,
"ipv4": {
"address": [{
"ip": ips["client"],
"prefix-length": 24
}]
}
}]
}
},
"ietf-system": {
"system": {
"ntp": {
"enabled": True,
"server": [{
"name": "srv1",
"udp": {
"address": ips["srv1"]
},
"iburst": True
}, {
"name": "srv2",
"udp": {
"address": ips["srv2"]
},
"iburst": True
}]
}
}
}
})
with test.step("Wait for client to see both servers"):
until(lambda: ntp.number_of_sources(client) == 2, attempts=60)
with test.step("Wait for srv2 stratum to stabilize"):
# Ensure srv2 has synced with srv1 and is advertising
# stratum 2. This prevents race where both advertise
# stratum 1, causing wrong selection
def check_stratums():
srv1 = ntp.get_source_by_address(client, ips["srv1"])
srv2 = ntp.get_source_by_address(client, ips["srv2"])
if not srv1 or not srv2:
return False
srv1_stratum = srv1.get("stratum")
srv2_stratum = srv2.get("stratum")
# Both must have valid stratums and srv1 < srv2
if srv1_stratum and srv2_stratum and srv1_stratum < srv2_stratum:
return True
return False
until(check_stratums, attempts=60)
print(f"srv1 and srv2 stratums verified as different")
with test.step("Verify client selects srv1 (lower stratum)"):
def srv1_selected():
source = ntp.any_source_selected(client)
if source and source.get("address") == ips["srv1"]:
return source
return None
try:
selected = until(srv1_selected, attempts=120)
except Exception:
# Timeout - print diagnostic info
sources = ntp.get_sources(client)
print("DEBUG: Failed to select srv1. Source details:")
for src in sources:
print(f" {src.get('address')}: stratum={src.get('stratum')}, "
f"state={src.get('state')}, poll={src.get('poll')}, "
f"offset={src.get('offset')}")
raise
assert selected is not None, "srv1 was not selected"
print(f"Client correctly selected srv1 ({ips['srv1']}) "
f"with stratum {selected.get('stratum')}")
test.succeed()
@@ -0,0 +1,34 @@
graph "topology" {
layout="neato";
overlap="false";
esep="+20";
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="host | { <mgmt1> mgmt1 | <srv1> srv1 | <> \n\n\n | <mgmt2> mgmt2 }",
pos="0,15!",
requires="controller",
];
srv2 [
label="{ <mgmt> mgmt | <swp1> swp1 } | { srv2 | <swp2> swp2 }",
pos="2,15.25!",
fontsize=12,
requires="infix",
];
client [
label="<mgmt> mgmt | { <eth0> eth0 | client }",
pos="2,14.70!",
fontsize=12,
requires="infix",
];
host:mgmt1 -- srv2:mgmt [requires="mgmt", color="lightgray"]
host:mgmt2 -- client:mgmt [requires="mgmt" color="lightgrey"]
host:srv1 -- srv2:swp1 [taillabel="192.168.1.1", headlabel="192.168.1.2"]
srv2:swp2 -- client:eth0 [headlabel="192.168.1.3"]
}
@@ -0,0 +1,70 @@
<?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: topology Pages: 1 -->
<svg width="510pt" height="144pt"
viewBox="0.00 0.00 510.22 144.01" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 140.01)">
<title>topology</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-140.01 506.22,-140.01 506.22,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-9.73 0,-134.73 108,-134.73 108,-9.73 0,-9.73"/>
<text text-anchor="middle" x="25" y="-68.53" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-9.73 50,-134.73 "/>
<text text-anchor="middle" x="79" y="-119.53" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
<polyline fill="none" stroke="black" points="50,-111.73 108,-111.73 "/>
<text text-anchor="middle" x="79" y="-96.53" font-family="DejaVu Sans Mono, Book" font-size="14.00">srv1</text>
<polyline fill="none" stroke="black" points="50,-88.73 108,-88.73 "/>
<polyline fill="none" stroke="black" points="50,-32.73 108,-32.73 "/>
<text text-anchor="middle" x="79" y="-17.53" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
</g>
<!-- srv2 -->
<g id="node2" class="node">
<title>srv2</title>
<polygon fill="none" stroke="black" points="345.22,-93.51 345.22,-135.51 439.22,-135.51 439.22,-93.51 345.22,-93.51"/>
<text text-anchor="middle" x="368.72" y="-121.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">mgmt</text>
<polyline fill="none" stroke="black" points="345.22,-114.51 392.22,-114.51 "/>
<text text-anchor="middle" x="368.72" y="-100.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">swp1</text>
<polyline fill="none" stroke="black" points="392.22,-93.51 392.22,-135.51 "/>
<text text-anchor="middle" x="415.72" y="-121.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">srv2</text>
<polyline fill="none" stroke="black" points="392.22,-114.51 439.22,-114.51 "/>
<text text-anchor="middle" x="415.72" y="-100.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">swp2</text>
</g>
<!-- host&#45;&#45;srv2 -->
<g id="edge1" class="edge">
<title>host:mgmt1&#45;&#45;srv2:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-123.23C108,-123.23 345.22,-125.51 345.22,-125.51"/>
</g>
<!-- host&#45;&#45;srv2 -->
<g id="edge3" class="edge">
<title>host:srv1&#45;&#45;srv2:swp1</title>
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M108,-100.23C108,-100.23 345.22,-103.51 345.22,-103.51"/>
<text text-anchor="middle" x="302.22" y="-107.31" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.2</text>
<text text-anchor="middle" x="151" y="-104.03" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.1</text>
</g>
<!-- client -->
<g id="node3" class="node">
<title>client</title>
<polygon fill="none" stroke="black" points="337.72,-0.5 337.72,-42.5 446.72,-42.5 446.72,-0.5 337.72,-0.5"/>
<text text-anchor="middle" x="361.22" y="-18.4" font-family="DejaVu Sans Mono, Book" font-size="12.00">mgmt</text>
<polyline fill="none" stroke="black" points="384.72,-0.5 384.72,-42.5 "/>
<text text-anchor="middle" x="415.72" y="-28.9" font-family="DejaVu Sans Mono, Book" font-size="12.00">eth0</text>
<polyline fill="none" stroke="black" points="384.72,-21.5 446.72,-21.5 "/>
<text text-anchor="middle" x="415.72" y="-7.9" font-family="DejaVu Sans Mono, Book" font-size="12.00">client</text>
</g>
<!-- host&#45;&#45;client -->
<g id="edge2" class="edge">
<title>host:mgmt2&#45;&#45;client:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M108,-21.23C108,-21.23 337.22,-21.5 337.22,-21.5"/>
</g>
<!-- srv2&#45;&#45;client -->
<g id="edge4" class="edge">
<title>srv2:swp2&#45;&#45;client:eth0</title>
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M416.22,-93.51C416.22,-93.51 416.22,-42.5 416.22,-42.5"/>
<text text-anchor="middle" x="459.22" y="-46.3" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.3</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

+1
View File
@@ -0,0 +1 @@
test.adoc
+27
View File
@@ -0,0 +1,27 @@
=== NTP server and client interoperability
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ntp/server_client]
==== Description
Verify NTP server and client work together:
1. Server uses ietf-ntp YANG model with refclock-master
2. Client uses ietf-system YANG model
3. Client successfully synchronizes from server
4. Server shows packet statistics
5. Mutual exclusion prevents both modes on same device
==== Topology
image::topology.svg[NTP server and client interoperability topology, align=center, scaledwidth=75%]
==== Sequence
. Set up topology and attach to devices
. Configure NTP server using ietf-ntp model
. Configure NTP client using ietf-system:ntp model
. Verify NTP server has received packets
. Verify NTP client has synchronized
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""NTP server and client interoperability test
Verify NTP server and client work together:
1. Server uses ietf-ntp YANG model with refclock-master
2. Client uses ietf-system YANG model
3. Client successfully synchronizes from server
4. Server shows packet statistics
5. Mutual exclusion prevents both modes on same device
"""
import infamy
from infamy import until
import infamy.ntp as ntp
with infamy.Test() as test:
with test.step("Set up topology and attach to devices"):
env = infamy.Env()
server = env.attach("server", "mgmt")
client = env.attach("client", "mgmt")
_, server_data = env.ltop.xlate("server", "data")
_, client_data = env.ltop.xlate("client", "data")
with test.step("Configure NTP server using ietf-ntp model"):
server.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [{
"name": server_data,
"enabled": True,
"ipv4": {
"address": [{
"ip": "192.168.3.1",
"prefix-length": 24
}]
}
}]
}
},
"ietf-ntp": {
"ntp": {
"refclock-master": {
"master-stratum": 8
},
"interfaces": {
"interface": [
{"name": server_data}
]
}
}
}
})
with test.step("Configure NTP client using ietf-system:ntp model"):
client.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [{
"name": client_data,
"enabled": True,
"ipv4": {
"address": [{
"ip": "192.168.3.2",
"prefix-length": 24
}]
}
}]
}
},
"ietf-system": {
"system": {
"ntp": {
"enabled": True,
"server": [{
"name": "ntp-server",
"udp": {
"address": "192.168.3.1"
},
"iburst": True
}]
}
}
}
})
with test.step("Verify NTP server has received packets"):
until(lambda: ntp.server_has_received_packets(server), attempts=30)
print("Server has received NTP packets from client")
with test.step("Verify NTP client has synchronized"):
selected = until(lambda: ntp.any_source_selected(client), attempts=30)
print(f"Client synchronized to {selected.get('address')} (stratum {selected.get('stratum')})")
test.succeed()
+33
View File
@@ -0,0 +1,33 @@
graph "ntp-server-client-interop" {
layout="neato";
overlap="false";
esep="+22";
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="{ <mgmt1> mgmt1 | <host> \n\nhost\n\n\n | <mgmt2> mgmt2 }",
pos="0,15!",
requires="controller",
];
server [
label="{ <mgmt> mgmt | <data> data } | { server }",
pos="2,15.25!",
fontsize=12,
requires="infix",
];
client [
label="{ <data> data | <mgmt> mgmt } | { client }",
pos="2,14.75!",
fontsize=12,
requires="infix",
];
host:mgmt1 -- server:mgmt [requires="mgmt", color="lightgray"]
host:mgmt2 -- client:mgmt [requires="mgmt" color="lightgrey"]
server:data -- client:data [label="\n\n192.168.3.0/24 "]
}
+58
View File
@@ -0,0 +1,58 @@
<?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: ntp&#45;server&#45;client&#45;interop Pages: 1 -->
<svg width="484pt" height="149pt"
viewBox="0.00 0.00 483.54 149.01" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 145.01)">
<title>ntp&#45;server&#45;client&#45;interop</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-145.01 479.54,-145.01 479.54,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-4 0,-137 58,-137 58,-4 0,-4"/>
<text text-anchor="middle" x="29" y="-121.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
<polyline fill="none" stroke="black" points="0,-114 58,-114 "/>
<text text-anchor="middle" x="29" y="-66.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="0,-27 58,-27 "/>
<text text-anchor="middle" x="29" y="-11.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
</g>
<!-- server -->
<g id="node2" class="node">
<title>server</title>
<polygon fill="none" stroke="black" points="366.54,-98.51 366.54,-140.51 475.54,-140.51 475.54,-98.51 366.54,-98.51"/>
<text text-anchor="middle" x="390.04" y="-126.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">mgmt</text>
<polyline fill="none" stroke="black" points="366.54,-119.51 413.54,-119.51 "/>
<text text-anchor="middle" x="390.04" y="-105.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">data</text>
<polyline fill="none" stroke="black" points="413.54,-98.51 413.54,-140.51 "/>
<text text-anchor="middle" x="444.54" y="-116.41" font-family="DejaVu Sans Mono, Book" font-size="12.00">server</text>
</g>
<!-- host&#45;&#45;server -->
<g id="edge1" class="edge">
<title>host:mgmt1&#45;&#45;server:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M58,-125.5C58,-125.5 366.04,-130.51 366.04,-130.51"/>
</g>
<!-- client -->
<g id="node3" class="node">
<title>client</title>
<polygon fill="none" stroke="black" points="366.54,-0.5 366.54,-42.5 475.54,-42.5 475.54,-0.5 366.54,-0.5"/>
<text text-anchor="middle" x="390.04" y="-28.9" font-family="DejaVu Sans Mono, Book" font-size="12.00">data</text>
<polyline fill="none" stroke="black" points="366.54,-21.5 413.54,-21.5 "/>
<text text-anchor="middle" x="390.04" y="-7.9" font-family="DejaVu Sans Mono, Book" font-size="12.00">mgmt</text>
<polyline fill="none" stroke="black" points="413.54,-0.5 413.54,-42.5 "/>
<text text-anchor="middle" x="444.54" y="-18.4" font-family="DejaVu Sans Mono, Book" font-size="12.00">client</text>
</g>
<!-- host&#45;&#45;client -->
<g id="edge2" class="edge">
<title>host:mgmt2&#45;&#45;client:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M58,-15.5C58,-15.5 366.04,-10.5 366.04,-10.5"/>
</g>
<!-- server&#45;&#45;client -->
<g id="edge3" class="edge">
<title>server:data&#45;&#45;client:data</title>
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M390.04,-98.51C390.04,-98.51 390.04,-42.5 390.04,-42.5"/>
<text text-anchor="middle" x="331.04" y="-74.3" font-family="DejaVu Serif, Book" font-size="14.00">192.168.3.0/24 &#160;</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+1
View File
@@ -0,0 +1 @@
test.adoc
+35
View File
@@ -0,0 +1,35 @@
=== NTP peer mode
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ntp/server_mode_peer]
==== Description
Verify NTP server operating in peer mode with bidirectional
synchronization.
This test validates peer mode where two NTP servers synchronize with
each other bidirectionally. Each server acts as both client and server
to the other:
- peer1: Stratum 8 local clock, peered with peer2
- peer2: Stratum 8 local clock, peered with peer1
The test verifies mutual synchronization and clock selection between
peers. When both peers have the same stratum, NTP's clock selection
algorithm uses the Reference ID (derived from the IP address) as its
tie-breaker. The peer with the numerically lower IP address will be
selected as sync source by the other peer.
==== Topology
image::topology.svg[NTP peer mode topology, align=center, scaledwidth=75%]
==== Sequence
. Set up topology and attach to devices
. Configure DUTs with bidirectional peer relationships
. Verify peers see each other in associations
. Verify peers can reach each other
. Wait for one peer to select the other as sync source
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""NTP peer mode test
Verify NTP server operating in peer mode with bidirectional
synchronization.
This test validates peer mode where two NTP servers synchronize with
each other bidirectionally. Each server acts as both client and server
to the other:
- peer1: Stratum 8 local clock, peered with peer2
- peer2: Stratum 8 local clock, peered with peer1
The test verifies mutual synchronization and clock selection between
peers. When both peers have the same stratum, NTP's clock selection
algorithm uses the Reference ID (derived from the IP address) as its
tie-breaker. The peer with the numerically lower IP address will be
selected as sync source by the other peer.
"""
import infamy
from infamy import until
import infamy.ntp as ntp
def configure_peer(dut, iface, addr, peer, stratum=8):
"""Configure NTP peer with interface and peer relationship."""
dut.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [{
"name": iface,
"enabled": True,
"ipv4": {
"address": [{
"ip": addr,
"prefix-length": 24
}]
}
}]
}
},
"ietf-ntp": {
"ntp": {
"unicast-configuration": [{
"address": peer,
"type": "uc-peer",
"minpoll": 2
}],
"refclock-master": {
"master-stratum": stratum
}
}
}
})
def has_selected_peer(peers):
"""Check if any peer has selected another as sync source."""
for target, _, _, _, peer in peers:
try:
data = target.get_data("/ietf-ntp:ntp/associations")
if not data:
continue
assoc = data.get("ntp", {}).get("associations", {}).get("association", [])
if not assoc:
continue
for assoc in assoc:
if assoc.get("prefer", False) and assoc.get("address") == peer:
return True
except Exception:
continue
return False
with infamy.Test() as test:
with test.step("Set up topology and attach to devices"):
env = infamy.Env()
peer1 = env.attach("peer1", "mgmt")
peer2 = env.attach("peer2", "mgmt")
_, if1 = env.ltop.xlate("peer1", "data")
_, if2 = env.ltop.xlate("peer2", "data")
duts = [
(peer1, if1, "peer1", "192.168.3.1", "192.168.3.2"),
(peer2, if2, "peer2", "192.168.3.2", "192.168.3.1")
]
with test.step("Configure DUTs with bidirectional peer relationships"):
for dut, interface, name, local_ip, peer_ip in duts:
configure_peer(dut, interface, local_ip, peer_ip)
print(f"Configured {name}: {local_ip} peered with {peer_ip}")
with test.step("Verify peers see each other in associations"):
for dut, _, name, _, peer_ip in duts:
until(lambda t=dut, p=peer_ip: ntp.server_has_peer(t, p), attempts=20)
print(f"{name} sees {peer_ip} in associations")
with test.step("Verify peers can reach each other"):
for dut, _, name, _, peer_ip in duts:
until(lambda t=dut, p=peer_ip: ntp.server_peer_reachable(t, p), attempts=60)
print(f"{name} can reach {peer_ip}")
with test.step("Wait for one peer to select the other as sync source"):
until(lambda: has_selected_peer(duts), attempts=120)
test.succeed()
@@ -0,0 +1,33 @@
graph "ntp-peer-mode" {
layout="neato";
overlap="false";
esep="+22";
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="{ <mgmt1> mgmt1 | <host> \n\nhost\n\n\n | <mgmt2> mgmt2 }",
pos="0,15!",
requires="controller",
];
peer1 [
label="{ <mgmt> mgmt | <data> data } | { peer1 }",
pos="2,15.25!",
fontsize=12,
requires="infix",
];
peer2 [
label="{ <data> data | <mgmt> mgmt } | { peer2 }",
pos="2,14.75!",
fontsize=12,
requires="infix",
];
host:mgmt1 -- peer1:mgmt [requires="mgmt", color="lightgray"]
host:mgmt2 -- peer2:mgmt [requires="mgmt" color="lightgrey"]
peer1:data -- peer2:data [label="\n\n192.168.3.0/24 ", dir="both"]
}
@@ -0,0 +1,60 @@
<?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: ntp&#45;peer&#45;mode Pages: 1 -->
<svg width="480pt" height="149pt"
viewBox="0.00 0.00 479.54 149.01" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 145.01)">
<title>ntp&#45;peer&#45;mode</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-145.01 475.54,-145.01 475.54,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-4 0,-137 58,-137 58,-4 0,-4"/>
<text text-anchor="middle" x="29" y="-121.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
<polyline fill="none" stroke="black" points="0,-114 58,-114 "/>
<text text-anchor="middle" x="29" y="-66.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="0,-27 58,-27 "/>
<text text-anchor="middle" x="29" y="-11.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
</g>
<!-- peer1 -->
<g id="node2" class="node">
<title>peer1</title>
<polygon fill="none" stroke="black" points="370.54,-98.51 370.54,-140.51 471.54,-140.51 471.54,-98.51 370.54,-98.51"/>
<text text-anchor="middle" x="394.04" y="-126.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">mgmt</text>
<polyline fill="none" stroke="black" points="370.54,-119.51 417.54,-119.51 "/>
<text text-anchor="middle" x="394.04" y="-105.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">data</text>
<polyline fill="none" stroke="black" points="417.54,-98.51 417.54,-140.51 "/>
<text text-anchor="middle" x="444.54" y="-116.41" font-family="DejaVu Sans Mono, Book" font-size="12.00">peer1</text>
</g>
<!-- host&#45;&#45;peer1 -->
<g id="edge1" class="edge">
<title>host:mgmt1&#45;&#45;peer1:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M58,-125.5C58,-125.5 370.04,-130.51 370.04,-130.51"/>
</g>
<!-- peer2 -->
<g id="node3" class="node">
<title>peer2</title>
<polygon fill="none" stroke="black" points="370.54,-0.5 370.54,-42.5 471.54,-42.5 471.54,-0.5 370.54,-0.5"/>
<text text-anchor="middle" x="394.04" y="-28.9" font-family="DejaVu Sans Mono, Book" font-size="12.00">data</text>
<polyline fill="none" stroke="black" points="370.54,-21.5 417.54,-21.5 "/>
<text text-anchor="middle" x="394.04" y="-7.9" font-family="DejaVu Sans Mono, Book" font-size="12.00">mgmt</text>
<polyline fill="none" stroke="black" points="417.54,-0.5 417.54,-42.5 "/>
<text text-anchor="middle" x="444.54" y="-18.4" font-family="DejaVu Sans Mono, Book" font-size="12.00">peer2</text>
</g>
<!-- host&#45;&#45;peer2 -->
<g id="edge2" class="edge">
<title>host:mgmt2&#45;&#45;peer2:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M58,-15.5C58,-15.5 370.04,-10.5 370.04,-10.5"/>
</g>
<!-- peer1&#45;&#45;peer2 -->
<g id="edge3" class="edge">
<title>peer1:data&#45;&#45;peer2:data</title>
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M394.04,-88.5C394.04,-78.43 394.04,-62.98 394.04,-52.81"/>
<polygon fill="cornflowerblue" stroke="cornflowerblue" stroke-width="2" points="390.54,-88.51 394.04,-98.51 397.54,-88.51 390.54,-88.51"/>
<polygon fill="cornflowerblue" stroke="cornflowerblue" stroke-width="2" points="397.54,-52.5 394.04,-42.5 390.54,-52.5 397.54,-52.5"/>
<text text-anchor="middle" x="335.04" y="-74.45" font-family="DejaVu Serif, Book" font-size="14.00">192.168.3.0/24 &#160;</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

+1
View File
@@ -0,0 +1 @@
test.adoc
@@ -0,0 +1,35 @@
=== NTP server mode
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ntp/server_mode_server]
==== Description
Verify NTP server operating in server mode, syncing from upstream while
serving clients.
This test validates server mode where devices synchronize from upstream
NTP servers while simultaneously serving time to downstream clients. It
creates a two-tier hierarchy:
- Upstream: NTP server with local reference clock (stratum 8)
- Downstream: NTP server that syncs from upstream and serves to clients (stratum 9)
The test verifies both servers operate correctly and serve accurate time.
==== Topology
image::topology.svg[NTP server mode topology, align=center, scaledwidth=75%]
==== Sequence
. Set up topology and attach to devices
. Configure upstream NTP server with local reference clock
. Configure downstream NTP server syncing from upstream
. Verify network connectivity with upstream NTP server
. Query time from upstream NTP server
. Verify upstream NTP server statistics
. Verify network connectivity with downstream NTP server
. Wait for downstream to sync from upstream
. Verify downstream NTP server statistics
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""NTP server mode test
Verify NTP server operating in server mode, syncing from upstream while
serving clients.
This test validates server mode where devices synchronize from upstream
NTP servers while simultaneously serving time to downstream clients. It
creates a two-tier hierarchy:
- Upstream: NTP server with local reference clock (stratum 8)
- Downstream: NTP server that syncs from upstream and serves to clients (stratum 9)
The test verifies both servers operate correctly and serve accurate time.
"""
import infamy
from infamy import until
import infamy.ntp as ntp
with infamy.Test() as test:
with test.step("Set up topology and attach to devices"):
env = infamy.Env()
upstream = env.attach("upstream", "mgmt")
downstream = env.attach("downstream", "mgmt")
# Get interface names for each device
_, upstream_data1 = env.ltop.xlate("upstream", "data1")
_, upstream_conn = env.ltop.xlate("upstream", "conn")
_, hport1 = env.ltop.xlate("host", "data1")
_, downstream_data2 = env.ltop.xlate("downstream", "data2")
_, downstream_conn = env.ltop.xlate("downstream", "conn")
_, hport2 = env.ltop.xlate("host", "data2")
with test.step("Configure upstream NTP server with local reference clock"):
upstream.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [{
"name": upstream_data1,
"enabled": True,
"ipv4": {
"address": [{
"ip": "192.168.1.1",
"prefix-length": 24
}]
}
}, {
"name": upstream_conn,
"enabled": True,
"ipv4": {
"address": [{
"ip": "192.168.3.1",
"prefix-length": 24
}]
}
}]
}
},
"ietf-ntp": {
"ntp": {
"refclock-master": {
"master-stratum": 8
}
}
}
})
with test.step("Configure downstream NTP server syncing from upstream"):
downstream.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [{
"name": downstream_data2,
"enabled": True,
"ipv4": {
"address": [{
"ip": "192.168.2.1",
"prefix-length": 24
}]
}
}, {
"name": downstream_conn,
"enabled": True,
"ipv4": {
"address": [{
"ip": "192.168.3.2",
"prefix-length": 24
}]
}
}]
}
},
"ietf-ntp": {
"ntp": {
"unicast-configuration": [{
"address": "192.168.3.1",
"type": "uc-server",
"iburst": True
}],
"refclock-master": {
"master-stratum": 10
}
}
}
})
with infamy.IsolatedMacVlan(hport1) as ns1:
ns1.addip("192.168.1.2")
with test.step("Verify network connectivity with upstream NTP server"):
ns1.must_reach("192.168.1.1")
with test.step("Query time from upstream NTP server"):
until(lambda: ntp.server_query(ns1, "192.168.1.1"), attempts=20)
with test.step("Verify upstream NTP server statistics"):
until(lambda: ntp.server_has_received_packets(upstream), attempts=20)
with infamy.IsolatedMacVlan(hport2) as ns2:
ns2.addip("192.168.2.2")
with test.step("Verify network connectivity with downstream NTP server"):
ns2.must_reach("192.168.2.1")
with test.step("Wait for downstream to sync from upstream"):
# Give downstream time to sync from upstream
until(lambda: ntp.server_query(ns2, "192.168.2.1"), attempts=30)
with test.step("Verify downstream NTP server statistics"):
until(lambda: ntp.server_has_received_packets(downstream), attempts=20)
test.succeed()
@@ -0,0 +1,36 @@
graph "ntp-upstream-downstream" {
layout="neato";
overlap="false";
esep="+22";
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="host | { <mgmt1> mgmt1 | <data1> data1 | <> \n\n\n | <mgmt2> mgmt2 | <data2> data2 }",
pos="0,15!",
requires="controller",
];
upstream [
label="{ <mgmt> mgmt | <data1> data1 } | { \n upstream \n\n | <conn> conn }",
pos="2,15.25!",
fontsize=12,
requires="infix",
];
downstream [
label="{ <mgmt> mgmt | <data2> data2 } | { <conn> conn | \n downstream \n\n }",
pos="2,14.75!",
fontsize=12,
requires="infix",
];
host:mgmt1 -- upstream:mgmt [requires="mgmt", color="lightgray"]
host:data1 -- upstream:data1 [taillabel="192.168.1.2", headlabel="192.168.1.1"]
host:mgmt2 -- downstream:mgmt [requires="mgmt" color="lightgrey"]
host:data2 -- downstream:data2 [taillabel="192.168.2.2", headlabel="192.168.2.1"]
upstream:conn -- downstream:conn [label="Client/Server\n192.168.3.x"]
}
@@ -0,0 +1,82 @@
<?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: ntp&#45;upstream&#45;downstream Pages: 1 -->
<svg width="689pt" height="205pt"
viewBox="0.00 0.00 689.05 205.01" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 201.01)">
<title>ntp&#45;upstream&#45;downstream</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-201.01 685.05,-201.01 685.05,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-24.51 0,-172.51 108,-172.51 108,-24.51 0,-24.51"/>
<text text-anchor="middle" x="25" y="-94.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-24.51 50,-172.51 "/>
<text text-anchor="middle" x="79" y="-157.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
<polyline fill="none" stroke="black" points="50,-149.51 108,-149.51 "/>
<text text-anchor="middle" x="79" y="-134.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
<polyline fill="none" stroke="black" points="50,-126.51 108,-126.51 "/>
<polyline fill="none" stroke="black" points="50,-70.51 108,-70.51 "/>
<text text-anchor="middle" x="79" y="-55.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
<polyline fill="none" stroke="black" points="50,-47.51 108,-47.51 "/>
<text text-anchor="middle" x="79" y="-32.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">data2</text>
</g>
<!-- upstream -->
<g id="node2" class="node">
<title>upstream</title>
<polygon fill="none" stroke="black" points="485.05,-126.51 485.05,-196.51 631.05,-196.51 631.05,-126.51 485.05,-126.51"/>
<text text-anchor="middle" x="512.05" y="-175.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">mgmt</text>
<polyline fill="none" stroke="black" points="485.05,-161.51 539.05,-161.51 "/>
<text text-anchor="middle" x="512.05" y="-140.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">data1</text>
<polyline fill="none" stroke="black" points="539.05,-126.51 539.05,-196.51 "/>
<text text-anchor="middle" x="585.05" y="-168.91" font-family="DejaVu Sans Mono, Book" font-size="12.00"> upstream </text>
<polyline fill="none" stroke="black" points="539.05,-147.51 631.05,-147.51 "/>
<text text-anchor="middle" x="585.05" y="-133.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">conn</text>
</g>
<!-- host&#45;&#45;upstream -->
<g id="edge1" class="edge">
<title>host:mgmt1&#45;&#45;upstream:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-161.51C108,-161.51 485.05,-179.51 485.05,-179.51"/>
</g>
<!-- host&#45;&#45;upstream -->
<g id="edge2" class="edge">
<title>host:data1&#45;&#45;upstream:data1</title>
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M108,-138.51C108,-138.51 485.05,-143.51 485.05,-143.51"/>
<text text-anchor="middle" x="442.05" y="-147.31" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.1</text>
<text text-anchor="middle" x="151" y="-142.31" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.2</text>
</g>
<!-- downstream -->
<g id="node3" class="node">
<title>downstream</title>
<polygon fill="none" stroke="black" points="477.55,-0.5 477.55,-70.5 638.55,-70.5 638.55,-0.5 477.55,-0.5"/>
<text text-anchor="middle" x="504.55" y="-49.9" font-family="DejaVu Sans Mono, Book" font-size="12.00">mgmt</text>
<polyline fill="none" stroke="black" points="477.55,-35.5 531.55,-35.5 "/>
<text text-anchor="middle" x="504.55" y="-14.9" font-family="DejaVu Sans Mono, Book" font-size="12.00">data2</text>
<polyline fill="none" stroke="black" points="531.55,-0.5 531.55,-70.5 "/>
<text text-anchor="middle" x="585.05" y="-56.9" font-family="DejaVu Sans Mono, Book" font-size="12.00">conn</text>
<polyline fill="none" stroke="black" points="531.55,-49.5 638.55,-49.5 "/>
<text text-anchor="middle" x="585.05" y="-21.9" font-family="DejaVu Sans Mono, Book" font-size="12.00"> downstream </text>
</g>
<!-- host&#45;&#45;downstream -->
<g id="edge3" class="edge">
<title>host:mgmt2&#45;&#45;downstream:mgmt</title>
<path fill="none" stroke="lightgrey" stroke-width="2" d="M108,-58.51C108,-58.51 477.05,-53.5 477.05,-53.5"/>
</g>
<!-- host&#45;&#45;downstream -->
<g id="edge4" class="edge">
<title>host:data2&#45;&#45;downstream:data2</title>
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M108,-35.51C108,-35.51 477.05,-17.5 477.05,-17.5"/>
<text text-anchor="middle" x="434.05" y="-21.3" font-family="DejaVu Serif, Book" font-size="14.00">192.168.2.1</text>
<text text-anchor="middle" x="151" y="-39.31" font-family="DejaVu Serif, Book" font-size="14.00">192.168.2.2</text>
</g>
<!-- upstream&#45;&#45;downstream -->
<g id="edge5" class="edge">
<title>upstream:conn&#45;&#45;downstream:conn</title>
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M585.05,-126.51C585.05,-126.51 585.05,-70.5 585.05,-70.5"/>
<text text-anchor="middle" x="633.05" y="-117.31" font-family="DejaVu Serif, Book" font-size="14.00">Client/Server</text>
<text text-anchor="middle" x="633.05" y="-102.31" font-family="DejaVu Serif, Book" font-size="14.00">192.168.3.x</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

+1
View File
@@ -0,0 +1 @@
test.adoc
+35
View File
@@ -0,0 +1,35 @@
graph "1x2" {
graph [bb="0,0,432.03,50.5",
esep="+80",
layout=neato,
overlap=false
];
node [fontname="DejaVu Sans Mono, Book",
label="\N",
shape=record
];
edge [color=cornflowerblue,
fontname="DejaVu Serif, Book",
penwidth=2
];
host [height=0.65278,
label="host | { <mgmt> mgmt | <data1> data1 }",
pos="54,27",
rects="0,4,50,50 50,27,108,50 50,4,108,27",
requires=controller,
width=1.5];
target [height=0.65278,
label="{ <mgmt> mgmt | <data1> data1 } | target",
pos="370.03,27",
rects="308.03,27,366.03,50 308.03,4,366.03,27 366.03,4,432.03,50",
requires=infix,
width=1.7222];
host:mgmt -- target:mgmt [color=lightgray,
pos="108,39 108,39 308.03,39 308.03,39",
requires=mgmt];
host:data1 -- target:data1 [color=blue,
fontcolor=blue,
label="192.168.1.0/24",
lp="235.27,7.5",
pos="108,15 108,15 308.03,15 308.03,15"];
}
@@ -0,0 +1,25 @@
=== NTP server standalone mode
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ntp/server_mode_standalone]
==== Description
Verify NTP server operating in standalone mode with only a local reference clock.
This test validates the basic standalone mode where the NTP server uses only
its local reference clock (stratum 8) to serve time to clients, without
syncing from any upstream sources.
==== Topology
image::topology.svg[NTP server standalone mode topology, align=center, scaledwidth=75%]
==== Sequence
. Set up topology and attach to target DUT
. Configure interface and NTP server
. Verify network connectivity with NTP server
. Query time from NTP server
. Verify NTP server statistics
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""NTP server standalone mode test
Verify NTP server operating in standalone mode with only a local reference clock.
This test validates the basic standalone mode where the NTP server uses only
its local reference clock (stratum 8) to serve time to clients, without
syncing from any upstream sources.
"""
import infamy
from infamy import until
import infamy.ntp as ntp
with infamy.Test() as test:
with test.step("Set up topology and attach to target DUT"):
env = infamy.Env()
target = env.attach("target", "mgmt")
_, data1 = env.ltop.xlate("target", "data1")
_, hport1 = env.ltop.xlate("host", "data1")
with test.step("Configure interface and NTP server"):
target.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [{
"name": data1,
"enabled": True,
"ipv4": {
"address": [{
"ip": "192.168.1.1",
"prefix-length": 24
}]
}
}]
}
},
"ietf-ntp": {
"ntp": {
"refclock-master": {
"master-stratum": 8
}
}
}
})
with infamy.IsolatedMacVlan(hport1) as ns1:
ns1.addip("192.168.1.2")
with test.step("Verify network connectivity with NTP server"):
ns1.must_reach("192.168.1.1")
with test.step("Query time from NTP server"):
until(lambda: ntp.server_query(ns1, "192.168.1.1"), attempts=20)
with test.step("Verify NTP server statistics"):
until(lambda: ntp.server_has_received_packets(target), attempts=20)
test.succeed()
@@ -0,0 +1,23 @@
graph "1x2" {
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 }",
pos="1,1!",
requires="controller"
];
target [
label="{ <mgmt> mgmt | <data1> data1 } | target",
pos="3,1!",
requires="infix",
];
host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
host:data1 -- target:data1 [color=blue, fontcolor=blue, label="192.168.1.0/24"]
}
@@ -0,0 +1,35 @@
graph "1x2" {
graph [bb="0,0,432.03,50.5",
esep="+80",
layout=neato,
overlap=false
];
node [fontname="DejaVu Sans Mono, Book",
label="\N",
shape=record
];
edge [color=cornflowerblue,
fontname="DejaVu Serif, Book",
penwidth=2
];
host [height=0.65278,
label="host | { <mgmt> mgmt | <data1> data1 }",
pos="54,27",
rects="0,4,50,50 50,27,108,50 50,4,108,27",
requires=controller,
width=1.5];
target [height=0.65278,
label="{ <mgmt> mgmt | <data1> data1 } | target",
pos="370.03,27",
rects="308.03,27,366.03,50 308.03,4,366.03,27 366.03,4,432.03,50",
requires=infix,
width=1.7222];
host:mgmt -- target:mgmt [color=lightgray,
pos="108,39 108,39 308.03,39 308.03,39",
requires=mgmt];
host:data1 -- target:data1 [color=blue,
fontcolor=blue,
label="192.168.1.0/24",
lp="235.27,7.5",
pos="108,15 108,15 308.03,15 308.03,15"];
}
@@ -0,0 +1,43 @@
<?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: 1x2 Pages: 1 -->
<svg width="440pt" height="59pt"
viewBox="0.00 0.00 440.03 58.50" 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 54.5)">
<title>1x2</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-54.5 436.03,-54.5 436.03,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-4 0,-50 108,-50 108,-4 0,-4"/>
<text text-anchor="middle" x="25" y="-23.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-4 50,-50 "/>
<text text-anchor="middle" x="79" y="-34.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="50,-27 108,-27 "/>
<text text-anchor="middle" x="79" y="-11.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
</g>
<!-- target -->
<g id="node2" class="node">
<title>target</title>
<polygon fill="none" stroke="black" points="308.03,-4 308.03,-50 432.03,-50 432.03,-4 308.03,-4"/>
<text text-anchor="middle" x="337.03" y="-34.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="308.03,-27 366.03,-27 "/>
<text text-anchor="middle" x="337.03" y="-11.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
<polyline fill="none" stroke="black" points="366.03,-4 366.03,-50 "/>
<text text-anchor="middle" x="399.03" y="-23.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,-39C108,-39 308.03,-39 308.03,-39"/>
</g>
<!-- host&#45;&#45;target -->
<g id="edge2" class="edge">
<title>host:data1&#45;&#45;target:data1</title>
<path fill="none" stroke="blue" stroke-width="2" d="M108,-15C108,-15 308.03,-15 308.03,-15"/>
<text text-anchor="middle" x="235.27" y="-3.8" font-family="DejaVu Serif, Book" font-size="14.00" fill="blue">192.168.1.0/24</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+106 -3
View File
@@ -1,5 +1,5 @@
"""
NTP client helper
NTP client and server helpers
"""
@@ -22,17 +22,120 @@ def _get_ntp_sources(target):
return ntp["sources"]["source"]
def get_sources(target):
"""Get list of NTP sources from operational state."""
return _get_ntp_sources(target)
def get_source_by_address(target, address):
"""Get NTP source by address, or None if not found."""
sources = _get_ntp_sources(target)
for source in sources:
if source.get("address") == address:
return source
return None
def any_source_selected(target):
"""Return the first selected NTP source, or None if no source is selected."""
sources = _get_ntp_sources(target)
for source in sources:
if source["state"] == "selected":
return True
return source
return False
return None
def number_of_sources(target):
sources = _get_ntp_sources(target)
return len(sources)
def server_has_received_packets(target):
"""Verify NTP server (ietf-ntp) has received packets."""
try:
data = target.get_data("/ietf-ntp:ntp/ntp-statistics")
if not data:
return False
stats = data["ntp"].get("ntp-statistics", {})
if not stats:
return False
packets_received = int(stats.get("packet-received", 0))
return packets_received > 0
except Exception:
return False
def server_query(netns, server_ip, expected_stratum=None):
"""Query NTP server from a network namespace and return True if successful.
Optionally verify the stratum level if expected_stratum is provided.
"""
result = netns.runsh(f"timeout 1 ntpd -qwp {server_ip}")
output = result.stdout if result.stdout else ""
if f"ntpd: reply from {server_ip}" not in output or "offset" not in output:
return False
if expected_stratum is not None:
# Extract stratum from output like: "stratum 8"
for line in output.split('\n'):
if 'stratum' in line.lower():
try:
stratum = int(line.split()[-1])
return stratum == expected_stratum
except (ValueError, IndexError):
pass
return False
return True
def server_has_associations(target):
"""Verify NTP server (ietf-ntp) has any associations."""
try:
data = target.get_data("/ietf-ntp:ntp/associations")
if not data:
return False
associations = data.get("ntp", {}).get("associations", {}).get("association", [])
return len(associations) > 0
except Exception:
return False
def server_has_peer(target, peer_address):
"""Verify NTP server (ietf-ntp) has a peer association with given address."""
try:
data = target.get_data("/ietf-ntp:ntp/associations")
if not data:
return False
associations = data.get("ntp", {}).get("associations", {}).get("association", [])
if not associations:
return False
# Check if peer association exists with the given address
# local-mode will be "ietf-ntp:active" or "active" depending on namespace handling
for assoc in associations:
local_mode = assoc.get("local-mode", "")
if (assoc.get("address") == peer_address and
(local_mode == "ietf-ntp:active" or local_mode == "active")):
return True
return False
except Exception:
return False
def server_peer_reachable(target, peer_address):
"""Verify NTP peer association exists (peer is configured and running)."""
# For now, just check if the association exists
# The YANG associations container doesn't expose reach/state info
# but if the association shows up, it means chronyd is running and
# communicating with the peer
return server_has_peer(target, peer_address)
+4
View File
@@ -52,6 +52,10 @@ include::../case/dhcp/Readme.adoc[]
<<<
include::../case/ntp/Readme.adoc[]
<<<
include::../case/hardware/Readme.adoc[]
<<<