mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
test: add new tests for NTP
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
@@ -32,6 +32,9 @@
|
||||
- name: "Interfaces"
|
||||
suite: interfaces/all.yaml
|
||||
|
||||
- name: "NTP Server"
|
||||
suite: ntp/all.yaml
|
||||
|
||||
- name: "Routing"
|
||||
suite: routing/all.yaml
|
||||
|
||||
|
||||
@@ -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[]
|
||||
@@ -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
|
||||
@@ -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
@@ -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--srv2 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt1--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--srv2 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>host:srv1--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--client -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:mgmt2--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--client -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>srv2:swp2--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
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
@@ -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
|
||||
|
||||
|
||||
Executable
+97
@@ -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()
|
||||
@@ -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 "]
|
||||
}
|
||||
@@ -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-server-client-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-server-client-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--server -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt1--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--client -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:mgmt2--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--client -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>server:data--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  </text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
@@ -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
|
||||
|
||||
|
||||
Executable
+111
@@ -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-peer-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-peer-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--peer1 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt1--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--peer2 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:mgmt2--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--peer2 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>peer1:data--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  </text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -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
|
||||
|
||||
|
||||
Executable
+136
@@ -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-upstream-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-upstream-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--upstream -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt1--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--upstream -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:data1--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--downstream -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>host:mgmt2--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--downstream -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>host:data2--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--downstream -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>upstream:conn--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 |
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
@@ -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
|
||||
|
||||
|
||||
Executable
+61
@@ -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--target -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt--target:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-39C108,-39 308.03,-39 308.03,-39"/>
|
||||
</g>
|
||||
<!-- host--target -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:data1--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
@@ -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)
|
||||
|
||||
@@ -52,6 +52,10 @@ include::../case/dhcp/Readme.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
include::../case/ntp/Readme.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
include::../case/hardware/Readme.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
Reference in New Issue
Block a user