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 @@ +ADDRESS MODE STATE STRATUM POLL-INTERVAL +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 @@ + + + + + + +1x4 + + + +host + +host + +mgmt + +data1 + +data2 + +data3 + + + +target + +mgmt + +data1 + +data2 + +data3 + +target + + + +host:mgmt--target:mgmt + + + + +host:data1--target:data1 + + + + +host:data2--target:data2 + + + + +host:data3--target:data3 + + + + 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] }