diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md
index 5f443b56..9716df51 100644
--- a/doc/ChangeLog.md
+++ b/doc/ChangeLog.md
@@ -3,6 +3,15 @@ Change Log
All notable changes to the project are documented in this file.
+[v24.12.0][UNRELEASED]
+-------------------------
+
+### Changes
+
+- NTP client status is now availible in YANG.
+
+### Fixes
+
[v24.11.1][] - 2024-11-29
-------------------------
@@ -52,7 +61,6 @@ All notable changes to the project are documented in this file.
forwarded, when the underlying ports are simultaneously attached to
a VLAN filtering bridge.
-
[v24.11.0][] - 2024-11-20
-------------------------
@@ -221,7 +229,7 @@ renamed to ease maintenance, more info below.
with `custom-phys-address` to allow for constructing more free-form
MAC addresses based on the chassis MAC (a.k.a., base MAC) address.
For more information, see the YANG model, a few examples are listed in
- the updated documentation.
+ the updated documentation.
The syntax will be automatically updated in the `startup-config` and
`factory-config` -- make sure to verify the changes and update any
static `factory-config` used for your products
diff --git a/doc/system.md b/doc/system.md
index a1a64210..81dd8d77 100644
--- a/doc/system.md
+++ b/doc/system.md
@@ -24,14 +24,14 @@ available in the system authentication configuration context.
```
admin@host:/config/> edit system authentication user admin
admin@host:/config/system/authentication/user/admin/> change password
-New password:
-Retype password:
+New password:
+Retype password:
admin@host:/config/system/authentication/user/admin/> leave
```
The `change password` command starts an interactive dialogue that asks
for the new password, with a confirmation, and then salts and encrypts
-the password with sha512crypt.
+the password with sha512crypt.
It is also possible to use the `set password ...` command. This allows
setting an already hashed password. To manually hash a password, use
@@ -55,7 +55,7 @@ With SSH keys in place it is possible to disable password login, just
remember to verify SSH login and network connectivity before doing so.
```
-admin@host:/config/> edit system authentication user admin
+admin@host:/config/> edit system authentication user admin
admin@host:/config/system/authentication/user/admin/> edit authorized-key example@host
admin@host:/config/system/authentication/user/admin/authorized-key/example@host/> set algorithm ssh-rsa
admin@host:/config/system/authentication/user/admin/authorized-key/example@host/> set key-data AAAAB3NzaC1yc2EAAAADAQABAAABgQC8iBL42yeMBioFay7lty1C4ZDTHcHyo739gc91rTTH8SKvAE4g8Rr97KOz/8PFtOObBrE9G21K7d6UBuPqmd0RUF2CkXXN/eN2PBSHJ50YprRFt/z/304bsBYkDdflKlPDjuSmZ/+OMp4pTsq0R0eNFlX9wcwxEzooIb7VPEdvWE7AYoBRUdf41u3KBHuvjGd1M6QYJtbFLQMMTiVe5IUfyVSZ1RCxEyAB9fR9CBhtVheTVsY3iG0fZc9eCEo89ErDgtGUTJK4Hxt5yCNwI88YaVmkE85cNtw8YwubWQL3/tGZHfbbQ0fynfB4kWNloyRHFr7E1kDxuX5+pbv26EqRdcOVGucNn7hnGU6C1+ejLWdBD7vgsoilFrEaBWF41elJEPKDzpszEijQ9gTrrWeYOQ+x++lvmOdssDu4KvGmj2K/MQTL2jJYrMJ7GDzsUu3XikChRL7zNfS2jYYQLzovboUCgqfPUsVba9hqeX3U67GsJo+hy5MG9RSry4+ucHs=
@@ -145,7 +145,7 @@ is committed by issuing the `leave` command.
admin@host:/config/> edit system
admin@host:/config/system/> set hostname example
admin@host:/config/system/> leave
-admin@host:/>
+admin@host:/>
```
The hostname is advertised over mDNS-SD in the `.local` domain. If
@@ -203,8 +203,8 @@ admin@host:/>
## NTP Client Configuration
-Below is an example configuration for enabling NTP
-with a specific server and the `iburst` option for faster initial
+Below is an example configuration for enabling NTP
+with a specific server and the `iburst` option for faster initial
synchronization.
```
@@ -216,23 +216,37 @@ admin@host:/config/> set system ntp server ntp-pool iburst
admin@host:/config/> set system ntp server ntp-pool prefer
```
-This configuration enables the NTP client and sets the NTP server to
-`pool.ntp.org` with the `iburst` and `prefer` options. The `iburst`
-option ensures faster initial synchronization, and the `prefer` option
+This configuration enables the NTP client and sets the NTP server to
+`pool.ntp.org` with the `iburst` and `prefer` options. The `iburst`
+option ensures faster initial synchronization, and the `prefer` option
designates this server as preferred.
* `prefer false`: The NTP client will choose the best available source
-based on several factors, such as network delay, stratum, and other
+based on several factors, such as network delay, stratum, and other
metrics (default config).
-* `prefer true`: The NTP client will try to use the preferred server
+* `prefer true`: The NTP client will try to use the preferred server
as the primary source unless it becomes unreachable or unusable.
+### Show NTP Sources
+
+The status for NTP sources is availble in YANG and accessable with
+CLI/NETCONF/RESTCONF.
+
+To view the sources being used by the NTP client, run:
+```
+admin@target:/> show ntp
+ADDRESS MODE STATE STRATUM POLL-INTERVAL
+192.168.1.1 server candidate 1 6
+192.168.2.1 server candidate 1 6
+192.168.3.1 server selected 1 6
+```
+
### Show NTP Status
+To check the status of NTP synchronization (only availble in CLI), use the following command:
-To check the status of NTP synchronization, use the following command:
```
-admin@host:/> show ntp
+admin@host:/> show ntp tracking
Reference ID : C0248F86 (192.36.143.134)
Stratum : 2
Ref time (UTC) : Mon Oct 21 10:06:45 2024
@@ -249,29 +263,9 @@ Leap status : Normal
admin@host:/>
```
-This output provides detailed information about the NTP status, including
+This output provides detailed information about the NTP status, including
reference ID, stratum, time offsets, frequency, and root delay.
-### Show NTP Sources
-
-To view the sources being used by the NTP client, run:
-
-```
-admin@host:/> show ntp sources
-
- .-- Source mode '^' = server, '=' = peer, '#' = local clock.
- / .- Source state '*' = current best, '+' = combined, '-' = not combined,
-| / 'x' = may be in error, '~' = too variable, '?' = unusable.
-|| .- xxxx [ yyyy ] +/- zzzz
-|| Reachability register (octal) -. | xxxx = adjusted offset,
-|| Log2(Polling interval) --. | | yyyy = measured offset,
-|| \ | | zzzz = estimated error.
-|| | | \
-MS Name/IP address Stratum Poll Reach LastRx Last sample
-===============================================================================
-^* 192.36.143.134 1 6 177 9 +278ms[ -3845s] +/- 514ms
-admin@host:/>
-```
> The system uses `chronyd` for Network Time Protocol (NTP)
> synchronization. The output shown here is best explained in the
diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc
index 96573e99..f626676a 100644
--- a/src/confd/yang/confd.inc
+++ b/src/confd/yang/confd.inc
@@ -32,7 +32,7 @@ MODULES=(
"infix-lldp@2023-08-23.yang"
"infix-dhcp-client@2024-09-20.yang"
"infix-meta@2024-10-18.yang"
- "infix-system@2024-09-13.yang"
+ "infix-system@2024-11-27.yang"
"infix-services@2024-05-30.yang"
"ieee802-ethernet-interface@2019-06-21.yang"
"infix-ethernet-interface@2024-02-27.yang"
diff --git a/src/confd/yang/infix-system.yang b/src/confd/yang/infix-system.yang
index 2627ea45..393f4052 100644
--- a/src/confd/yang/infix-system.yang
+++ b/src/confd/yang/infix-system.yang
@@ -10,12 +10,22 @@ module infix-system {
prefix iana-tz;
}
+ import ietf-inet-types {
+ prefix inet;
+ reference
+ "RFC 6991: Common YANG Data Types";
+ }
+
include infix-system-software;
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "Infix augments and deviations to ietf-system.";
+ revision 2024-11-27 {
+ description "Add NTP status";
+ reference "internal";
+ }
revision 2024-09-13 {
description "Add some informative help about different shells and security.";
reference "internal";
@@ -115,6 +125,42 @@ module infix-system {
base shell-type;
}
+ typedef source-state {
+ reference "rfc5905: Network Time Protocol Version 4: Protocol and Algorithms Specification";
+ type enumeration {
+ enum selected {
+ description "Selected NTP server for synchronization";
+ }
+ enum candidate {
+ description "Valid for synchronization, is used together with thes elected best server and also acting as backup";
+ }
+ enum outlier {
+ description "Valid timesource but discarded by the clustering algorithm";
+ }
+ enum unusable {
+ description "Unreachable or unresponsable";
+ }
+ enum falseticker {
+ description "suspected of providing incorrect time";
+ }
+ enum unstable {
+ description "Too variable or unstable source";
+ }
+ }
+ }
+ typedef source-mode {
+ type enumeration {
+ enum server {
+ description "Normal NTP server";
+ }
+ enum peer {
+ description "Symmetric peer";
+ }
+ enum local-clock {
+ description "Reference clock";
+ }
+ }
+}
/*
* Typedefs
*/
@@ -256,6 +302,37 @@ module infix-system {
}
}
+ augment "/sys:system-state" {
+ container ntp {
+ description "NTP status";
+ container sources {
+ list source {
+ key address;
+ leaf address {
+ type inet:ip-address;
+ description "Address to NTP server";
+ }
+ leaf mode {
+ type source-mode;
+ description "Source mode";
+ }
+ leaf state {
+ type source-state;
+ description "Source state";
+ }
+ leaf stratum {
+ type uint8;
+ description "Stratum of NTP server";
+ }
+ leaf poll {
+ type uint8;
+ description "Interval between transmitted NTP messages, expressed as a power of two in seconds";
+ }
+
+ }
+ }
+ }
+ }
deviation "/sys:system/sys:hostname" {
deviate replace {
type infix-sys:hostname;
diff --git a/src/confd/yang/infix-system@2024-09-13.yang b/src/confd/yang/infix-system@2024-11-27.yang
similarity index 100%
rename from src/confd/yang/infix-system@2024-09-13.yang
rename to src/confd/yang/infix-system@2024-11-27.yang
diff --git a/src/klish-plugin-infix/xml/infix.xml b/src/klish-plugin-infix/xml/infix.xml
index 07f0fcef..23f7cf44 100644
--- a/src/klish-plugin-infix/xml/infix.xml
+++ b/src/klish-plugin-infix/xml/infix.xml
@@ -279,10 +279,14 @@
- doas ntp tracking
+
+ sysrepocfg -d operational -X -f json -x \
+ /ietf-system:system-state/infix-system:ntp | \
+ /usr/libexec/statd/cli-pretty "show-ntp"
+
-
- doas ntp sources
+
+ doas ntp tracking
diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py
index e8a370cc..29e2486b 100755
--- a/src/statd/python/cli_pretty/cli_pretty.py
+++ b/src/statd/python/cli_pretty/cli_pretty.py
@@ -54,6 +54,12 @@ class PadUsbPort:
name = 20
state = 10
+class PadNtpSource:
+ address = 16
+ mode = 13
+ state = 13
+ stratum = 11
+ poll = 14
class Decore():
@staticmethod
@@ -719,6 +725,26 @@ def show_hardware(json):
port = USBport(component)
port.print()
+def show_ntp(json):
+ if not json.get("ietf-system:system-state"):
+ print(f"Error, top level \"ietf-system:system-state\" missing")
+ sys.exit(1)
+ hdr = (f"{'ADDRESS':<{PadNtpSource.address}}"
+ f"{'MODE':<{PadNtpSource.mode}}"
+ f"{'STATE':<{PadNtpSource.state}}"
+ f"{'STRATUM':>{PadNtpSource.stratum}}"
+ f"{'POLL-INTERVAL':>{PadNtpSource.poll}}"
+ )
+ print(Decore.invert(hdr))
+ sources = get_json_data({}, json, 'ietf-system:system-state', 'infix-system:ntp', 'sources', 'source')
+ for source in sources:
+ row = f"{source['address']:<{PadNtpSource.address}}"
+ row += f"{source['mode']:<{PadNtpSource.mode}}"
+ row += f"{source['state'] if source['state'] != 'not-combined' else 'not combined':<{PadNtpSource.state}}"
+ row += f"{source['stratum']:>{PadNtpSource.stratum}}"
+ row += f"{source['poll']:>{PadNtpSource.poll}}"
+ print(row)
+
def main():
global UNIT_TEST
@@ -749,6 +775,8 @@ def main():
parser_show_routing_table = subparsers.add_parser('show-hardware', help='Show USB ports')
+ parser_show_ntp_sources = subparsers.add_parser('show-ntp', help='Show NTP sources')
+
args = parser.parse_args()
UNIT_TEST = args.test
@@ -762,6 +790,8 @@ def main():
show_bridge_mdb(json_data)
elif args.command == "show-hardware":
show_hardware(json_data)
+ elif args.command == "show-ntp":
+ show_ntp(json_data)
else:
print(f"Error, unknown command {args.command}")
sys.exit(1)
diff --git a/src/statd/python/yanger/yanger.py b/src/statd/python/yanger/yanger.py
index 53bffca5..cca4b903 100755
--- a/src/statd/python/yanger/yanger.py
+++ b/src/statd/python/yanger/yanger.py
@@ -1053,6 +1053,36 @@ def add_interface(ifname, yang_ifaces):
add_container_ifaces(yang_ifaces)
+def add_system_ntp(out):
+ data = run_cmd(["chronyc", "-c", "sources"], "chronyc-sources.txt", "")
+ source = []
+ state_mode_map = {
+ "^": "server",
+ "=": "peer",
+ "#": "local-clock"
+ }
+ source_state_map = {
+ "*": "selected",
+ "+": "candidate",
+ "-": "outlier",
+ "?": "unusable",
+ "x": "falseticker",
+ "~": "unstable"
+ }
+ for line in data:
+ src = {}
+ line = line.split(',')
+ src["address"] = line[2]
+ src["mode"] = state_mode_map[line[0]]
+ src["state"] = source_state_map[line[1]]
+ src["stratum"] = int(line[3])
+ src["poll"] = int(line[4])
+ source.append(src)
+
+ insert(out, "infix-system:ntp", "sources", "source", source)
+def add_system(yang_data):
+ add_system_ntp(yang_data)
+
def main():
global TESTPATH
global logger
@@ -1133,6 +1163,12 @@ def main():
}
add_container(yang_data['infix-containers:containers']['container'])
+ elif args.model == 'ietf-system':
+ yang_data = {
+ "ietf-system:system-state": {
+ }
+ }
+ add_system(yang_data['ietf-system:system-state'])
else:
logger.warning(f"Unsupported model {args.model}", file=sys.stderr)
sys.exit(1)
diff --git a/src/statd/statd.c b/src/statd/statd.c
index 099380ce..3c194e6c 100644
--- a/src/statd/statd.c
+++ b/src/statd/statd.c
@@ -37,6 +37,7 @@
#define XPATH_ROUTING_BASE "/ietf-routing:routing/control-plane-protocols/control-plane-protocol"
#define XPATH_ROUTING_TABLE "/ietf-routing:routing/ribs"
#define XPATH_HARDWARE_BASE "/ietf-hardware:hardware"
+#define XPATH_SYSTEM_BASE "/ietf-system:system-state"
#define XPATH_ROUTING_OSPF XPATH_ROUTING_BASE "/ospf"
#define XPATH_CONTAIN_BASE "/infix-containers:containers"
@@ -340,6 +341,8 @@ static int subscribe_to_all(struct statd *statd)
return SR_ERR_INTERNAL;
if (subscribe(statd, "ietf-hardware", XPATH_HARDWARE_BASE, sr_generic_cb))
return SR_ERR_INTERNAL;
+ if (subscribe(statd, "ietf-system", XPATH_SYSTEM_BASE, sr_generic_cb))
+ return SR_ERR_INTERNAL;
#ifdef CONTAINERS
if (subscribe(statd, "infix-containers", XPATH_CONTAIN_BASE, sr_generic_cb))
return SR_ERR_INTERNAL;
diff --git a/test/case/cli/cli-output/show-ntp.txt b/test/case/cli/cli-output/show-ntp.txt
new file mode 100644
index 00000000..e825572a
--- /dev/null
+++ b/test/case/cli/cli-output/show-ntp.txt
@@ -0,0 +1,9 @@
+[7mADDRESS MODE STATE STRATUM POLL-INTERVAL[0m
+185.125.190.56 peer candidate 2 8
+185.125.190.58 server unstable 2 7
+185.125.190.57 server falseticker 2 8
+91.189.91.157 local-clock outlier 2 6
+192.121.108.100 server candidate 3 8
+85.24.237.72 server selected 2 8
+162.159.200.1 server unusable 3 7
+85.24.237.71 server candidate 2 8
diff --git a/test/case/cli/run.sh b/test/case/cli/run.sh
index d0ccb981..5d206c4f 100755
--- a/test/case/cli/run.sh
+++ b/test/case/cli/run.sh
@@ -59,6 +59,8 @@ if [ $# -eq 2 ] && [ $1 = "update" ]; then
"$SR_EMULATOR_TOOL" | "$CLI_PRETTY_TOOL" "-t" "show-routing-table" -i "ipv6" > "$CLI_OUTPUT_PATH/show-routes-ipv6.txt"
elif [ $2 = "show-bridge-mdb" ]; then
"$SR_EMULATOR_TOOL" | "$CLI_PRETTY_TOOL" "show-bridge-mdb" > "$CLI_OUTPUT_PATH/show-bridge-mdb.txt"
+ elif [ $2 = "show-ntp" ]; then
+ "$SR_EMULATOR_TOOL" | "$CLI_PRETTY_TOOL" "show-ntp" > "$CLI_OUTPUT_PATH/show-ntp.txt"
else
echo "Unsupported cli-pretty command $2"
exit 1
@@ -67,7 +69,7 @@ if [ $# -eq 2 ] && [ $1 = "update" ]; then
exit 0
fi
-echo "1..10"
+echo "1..11"
echo "# Running:"
# Show interfaces
@@ -89,6 +91,16 @@ if ! diff -u "$CLI_OUTPUT_PATH/show-bridge-mdb.txt" "$CLI_OUTPUT_FILE"; then
fi
ok "\"show bridge mdb\" output looks intact"
+# Show NTP
+echo "# $SR_EMULATOR_TOOL | $CLI_PRETTY_TOOL show-ntp"
+"$SR_EMULATOR_TOOL" | "$CLI_PRETTY_TOOL" "show-ntp" > "$CLI_OUTPUT_FILE"
+
+if ! diff -u "$CLI_OUTPUT_PATH/show-ntp.txt" "$CLI_OUTPUT_FILE"; then
+ print_update_txt
+ fail "\"show ntp\" output has changed"
+fi
+ok "\"show ntp\" output looks intact"
+
# Show ipv4 routes
echo "# $SR_EMULATOR_TOOL | $CLI_PRETTY_TOOL -t show-routing-table -i ipv4"
"$SR_EMULATOR_TOOL" | "$CLI_PRETTY_TOOL" "-t" "show-routing-table" -i "ipv4" > "$CLI_OUTPUT_FILE"
diff --git a/test/case/cli/sysrepo-emulator.sh b/test/case/cli/sysrepo-emulator.sh
index df9cd2eb..49933c4b 100755
--- a/test/case/cli/sysrepo-emulator.sh
+++ b/test/case/cli/sysrepo-emulator.sh
@@ -7,9 +7,11 @@ YANGER_TOOL="$ROOT_PATH/src/statd/python/yanger/yanger.py"
INTERFACES_OUTPUT_FILE="$(mktemp)"
ROUTES_OUTPUT_FILE="$(mktemp)"
+SYSTEM_OUTPUT_FILE="$(mktemp)"
cleanup() {
rm -f "$INTERFACES_OUTPUT_FILE"
rm -f "$ROUTES_OUTPUT_FILE"
+ rm -f "$SYSTEM_OUTPUT_FILE"
}
trap cleanup EXIT
@@ -26,5 +28,7 @@ fi
$YANGER_TOOL "ietf-routing" -t "$SCRIPT_PATH/system-output/" > "$ROUTES_OUTPUT_FILE"
+$YANGER_TOOL "ietf-system" -t "$SCRIPT_PATH/system-output/" > "$SYSTEM_OUTPUT_FILE"
+
# Merge all module files
-jq -s '.[0] * .[1]' $ROUTES_OUTPUT_FILE $INTERFACES_OUTPUT_FILE
+jq -s '.[0] * .[1] * .[2]' $ROUTES_OUTPUT_FILE $INTERFACES_OUTPUT_FILE $SYSTEM_OUTPUT_FILE
diff --git a/test/case/cli/system-output/chronyc-sources.txt b/test/case/cli/system-output/chronyc-sources.txt
new file mode 100644
index 00000000..ba48eaa5
--- /dev/null
+++ b/test/case/cli/system-output/chronyc-sources.txt
@@ -0,0 +1,8 @@
+=,+,185.125.190.56,2,8,377,166,0.005469983,0.005822986,0.033335429
+^,~,185.125.190.58,2,7,377,37,0.003233160,0.003233160,0.028441129
+^,x,185.125.190.57,2,8,377,167,-0.000418652,-0.000065722,0.027703809
+#,-,91.189.91.157,2,6,377,488,0.000970410,0.001490889,0.077348366
+^,+,192.121.108.100,3,8,377,101,0.005757879,0.006114979,0.024731735
+^,*,85.24.237.72,2,8,377,37,0.006444688,0.006805896,0.017862743
+^,?,162.159.200.1,3,7,377,34,-0.000447366,-0.000447366,0.009887429
+^,+,85.24.237.71,2,8,377,169,0.003875532,0.004228305,0.015323908
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..f9c0f4b6
--- /dev/null
+++ b/test/case/ietf_system/ntp_client/Readme.adoc
@@ -0,0 +1,25 @@
+=== Basic NTP client test
+==== Description
+Verify NTP client with multiple servers, ensure one get selected.
+
+==== Topology
+ifdef::topdoc[]
+image::../../test/case/ietf_system/ntp_client/topology.svg[Basic NTP client test topology]
+endif::topdoc[]
+ifndef::topdoc[]
+ifdef::testgroup[]
+image::ntp_client/topology.svg[Basic NTP client test topology]
+endif::testgroup[]
+ifndef::testgroup[]
+image::topology.svg[Basic 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'
+. Verify three sources exist in NTP client 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]
}