diff --git a/test/case/ietf_system/Readme.adoc b/test/case/ietf_system/Readme.adoc
index 0b138e8b..c8d5173d 100644
--- a/test/case/ietf_system/Readme.adoc
+++ b/test/case/ietf_system/Readme.adoc
@@ -16,3 +16,5 @@ include::timezone_utc_offset/Readme.adoc[]
include::ssh_key_authentication/Readme.adoc[]
include::upgrade/Readme.adoc[]
+
+include::ntp_client/Readme.adoc[]
diff --git a/test/case/ietf_system/ietf_system.yaml b/test/case/ietf_system/ietf_system.yaml
index d5c31b36..fa15b9b0 100644
--- a/test/case/ietf_system/ietf_system.yaml
+++ b/test/case/ietf_system/ietf_system.yaml
@@ -16,3 +16,6 @@
- name: ssh_key_authentication
case: ssh_key_authentication/test.py
+
+- name: ntp_client
+ case: ntp_client/test.py
diff --git a/test/case/ietf_system/ntp_client/Readme.adoc b/test/case/ietf_system/ntp_client/Readme.adoc
new file mode 100644
index 00000000..a955b729
--- /dev/null
+++ b/test/case/ietf_system/ntp_client/Readme.adoc
@@ -0,0 +1,24 @@
+=== Simple NTP client test
+==== Description
+Verify NTP client to multiple NTP servers and verify that one get selected
+
+==== Topology
+ifdef::topdoc[]
+image::../../test/case/ietf_system/ntp_client/topology.svg[Simple NTP client test topology]
+endif::topdoc[]
+ifndef::topdoc[]
+ifdef::testgroup[]
+image::ntp_client/topology.svg[Simple NTP client test topology]
+endif::testgroup[]
+ifndef::testgroup[]
+image::topology.svg[Simple NTP client test topology]
+endif::testgroup[]
+endif::topdoc[]
+==== Test sequence
+. Set up topology and attach to target DUT
+. Configure NTP client on 'target'
+. Verify one source is in 'selected' state on 'target'
+
+
+<<<
+
diff --git a/test/case/ietf_system/ntp_client/test.py b/test/case/ietf_system/ntp_client/test.py
new file mode 100755
index 00000000..c7816515
--- /dev/null
+++ b/test/case/ietf_system/ntp_client/test.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python3
+"""
+Basic NTP client test
+
+Verify NTP client with multiple servers, ensure one get selected.
+"""
+
+import infamy
+import infamy.ntp_server as ntp_server
+import infamy.ntp as ntp
+import infamy.util as util
+def config_target(dut, data1, data2, data3):
+ dut.put_config_dicts({
+ "ietf-interfaces": {
+ "interfaces": {
+ "interface": [
+ {
+ "name": data1,
+ "enabled": True,
+ "ipv4": {
+ "address": [{
+ "ip": "192.168.1.2",
+ "prefix-length": 24
+ }]
+ }
+ },
+ {
+ "name": data2,
+ "enabled": True,
+ "ipv4": {
+ "address": [{
+ "ip": "192.168.2.2",
+ "prefix-length": 24
+ }]
+ }
+ },
+ {
+ "name": data3,
+ "enabled": True,
+ "ipv4": {
+ "address": [{
+
+ "ip": "192.168.3.2",
+ "prefix-length": 24
+ }]
+ }
+ }]
+ }
+ },
+ "ietf-system": {
+ "system": {
+ "ntp": {
+ "enabled": True,
+ "server": [{
+ "name": "Server1",
+ "udp": {
+ "address": "192.168.1.1"
+ },
+ "iburst": True
+ },{
+ "name": "Server2",
+ "udp": {
+ "address": "192.168.2.1"
+ },
+ "iburst": True
+ },{
+ "name": "Server3",
+ "udp": {
+ "address": "192.168.3.1"
+ },
+ "iburst": True
+ }]
+ }
+ }
+ }
+ })
+
+with infamy.Test() as test:
+ with test.step("Set up topology and attach to target DUT"):
+ env = infamy.Env()
+ target = env.attach("target", "mgmt")
+
+ with test.step("Configure NTP client on 'target'"):
+ _, data1 = env.ltop.xlate("target", "data1")
+ _, data2 = env.ltop.xlate("target", "data2")
+ _, data3 = env.ltop.xlate("target", "data3")
+
+ config_target(target, data1, data2, data3)
+
+ _, hport1 = env.ltop.xlate("host", "data1")
+ _, hport2 = env.ltop.xlate("host", "data2")
+ _, hport3 = env.ltop.xlate("host", "data3")
+
+ with infamy.IsolatedMacVlan(hport1) as ns1, \
+ infamy.IsolatedMacVlan(hport2) as ns2, \
+ infamy.IsolatedMacVlan(hport3) as ns3:
+ ns1.addip("192.168.1.1")
+ ns2.addip("192.168.2.1")
+ ns3.addip("192.168.3.1")
+
+ with ntp_server.Server(ns1) as ntp1, \
+ ntp_server.Server(ns2) as ntp2, \
+ ntp_server.Server(ns3) as ntp3:
+ with test.step("Verify one source is in 'selected' state on 'target'"):
+ util.until(lambda: ntp.any_source_selected(target), attempts=200)
+ with test.step("Verify three sources exist in NTP client on 'target'"):
+ assert(ntp.number_of_sources(target) == 3)
+
+ test.succeed()
diff --git a/test/case/ietf_system/ntp_client/topology.dot b/test/case/ietf_system/ntp_client/topology.dot
new file mode 120000
index 00000000..fb634244
--- /dev/null
+++ b/test/case/ietf_system/ntp_client/topology.dot
@@ -0,0 +1 @@
+../../../infamy/topologies/1x4.dot
\ No newline at end of file
diff --git a/test/case/ietf_system/ntp_client/topology.svg b/test/case/ietf_system/ntp_client/topology.svg
new file mode 100644
index 00000000..fa3360bd
--- /dev/null
+++ b/test/case/ietf_system/ntp_client/topology.svg
@@ -0,0 +1,60 @@
+
+
+
+
+
diff --git a/test/infamy/ntp.py b/test/infamy/ntp.py
new file mode 100644
index 00000000..4704189e
--- /dev/null
+++ b/test/infamy/ntp.py
@@ -0,0 +1,38 @@
+"""
+NTP client helper
+"""
+
+
+def _get_ntp(target):
+ xpath = "/ietf-system:system-state/infix-system:ntp"
+ data = target.get_data(xpath)
+
+ if data is None:
+ return None
+
+ return data["system-state"].get("infix-system:ntp", None) or data["system-state"].get("ntp", None)
+
+
+def _get_ntp_sources(target):
+ ntp = _get_ntp(target)
+
+ if ntp is None:
+ return []
+
+ return ntp["sources"]["source"]
+
+
+def any_source_selected(target):
+ sources = _get_ntp_sources(target)
+
+ for source in sources:
+ if source["state"] == "selected":
+ return True
+
+ return False
+
+
+def number_of_sources(target):
+ sources = _get_ntp_sources(target)
+
+ return len(sources)
diff --git a/test/infamy/ntp_server.py b/test/infamy/ntp_server.py
new file mode 100644
index 00000000..baa01d85
--- /dev/null
+++ b/test/infamy/ntp_server.py
@@ -0,0 +1,24 @@
+"""Start NTP server in the background"""
+import subprocess
+
+class Server:
+ def __init__(self, netns, iface="iface"):
+ self.iface = iface
+ self.process = None
+ self.netns = netns
+
+ def __enter__(self):
+ self.start()
+
+ def __exit__(self, _, __, ___):
+ self.stop()
+
+ def start(self):
+ cmd=f"ntpd -w -n -l -I {self.iface}"
+ self.process = self.netns.popen(cmd.split(" "),stderr=subprocess.DEVNULL)
+
+ def stop(self):
+ if self.process:
+ self.process.terminate()
+ self.process.wait()
+ self.process = None
diff --git a/test/infamy/topologies/1x4.dot b/test/infamy/topologies/1x4.dot
index a76c5e5c..7b5240c6 100644
--- a/test/infamy/topologies/1x4.dot
+++ b/test/infamy/topologies/1x4.dot
@@ -7,20 +7,20 @@ graph "1x4" {
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
- label="host | { tgt | data0 | data1 | data2 }",
+ label="host | { mgmt | data1 | data2 | data3 }",
pos="0,12!",
kind="controller",
];
target [
- label="{ mgmt | data0 | data1 | data2 } | target",
+ label="{ mgmt | data1 | data2 | data3 } | target",
pos="10,12!",
kind="infix",
];
- host:tgt -- target:mgmt [kind=mgmt]
- host:data0 -- target:data0 [color=black]
+ host:mgmt -- target:mgmt [kind=mgmt]
host:data1 -- target:data1 [color=black]
host:data2 -- target:data2 [color=black]
+ host:data3 -- target:data3 [color=black]
}