From efe4903d9ce78a4e81ad2b46b0be1a807f6ccff3 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 3 Dec 2025 20:15:34 +0100 Subject: [PATCH 1/7] confd: initial support for NTP server Fixes #904 Signed-off-by: Joachim Wiberg --- src/confd/src/Makefile.am | 1 + src/confd/src/core.c | 27 + src/confd/src/core.h | 6 + src/confd/src/ntp.c | 243 +++ src/confd/src/system.c | 10 +- src/confd/yang/confd.inc | 5 + .../ietf-access-control-list@2019-03-04.yang | 668 ++++++++ .../confd/ietf-ethertypes@2019-03-04.yang | 381 +++++ src/confd/yang/confd/ietf-ntp@2022-07-05.yang | 1482 +++++++++++++++++ .../confd/ietf-packet-fields@2019-03-04.yang | 576 +++++++ src/confd/yang/confd/infix-ntp.yang | 171 ++ .../yang/confd/infix-ntp@2025-12-03.yang | 1 + src/klish-plugin-infix/xml/infix.xml | 16 +- src/show/show.py | 75 +- src/statd/python/cli_pretty/cli_pretty.py | 479 +++++- src/statd/python/yanger/__main__.py | 3 + src/statd/python/yanger/ietf_ntp.py | 363 ++++ src/statd/python/yanger/ietf_system.py | 1 + src/statd/statd.c | 3 + test/case/statd/system/cli/show-ntp | 10 +- 20 files changed, 4487 insertions(+), 34 deletions(-) create mode 100644 src/confd/src/ntp.c create mode 100644 src/confd/yang/confd/ietf-access-control-list@2019-03-04.yang create mode 100644 src/confd/yang/confd/ietf-ethertypes@2019-03-04.yang create mode 100644 src/confd/yang/confd/ietf-ntp@2022-07-05.yang create mode 100644 src/confd/yang/confd/ietf-packet-fields@2019-03-04.yang create mode 100644 src/confd/yang/confd/infix-ntp.yang create mode 120000 src/confd/yang/confd/infix-ntp@2025-12-03.yang create mode 100644 src/statd/python/yanger/ietf_ntp.py diff --git a/src/confd/src/Makefile.am b/src/confd/src/Makefile.am index 8db05041e..1d379e698 100644 --- a/src/confd/src/Makefile.am +++ b/src/confd/src/Makefile.am @@ -40,6 +40,7 @@ confd_plugin_la_SOURCES = \ if-wifi.c \ keystore.c \ system.c \ + ntp.c \ syslog.c \ factory-default.c \ routing.c \ diff --git a/src/confd/src/core.c b/src/confd/src/core.c index df8754b71..fab04e787 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -328,6 +328,10 @@ static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *mod if ((rc = keystore_change(session, config, diff, event, confd))) goto free_diff; + /* ietf-ntp */ + if ((rc = ntp_change(session, config, diff, event, confd))) + goto free_diff; + /* infix-services */ if ((rc = services_change(session, config, diff, event, confd))) goto free_diff; @@ -366,6 +370,20 @@ static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *mod if ((rc = meta_change_cb(session, config, diff, event, confd))) goto free_diff; + /* + * Manage chronyd service enable/disable state. Must be done + * after both ietf-system:ntp and ietf-ntp have are done. + */ + if (event == SR_EV_DONE && config) { + bool client = false; + bool server = false; + + client = srx_enabled(session, "/ietf-system:system/ntp/enabled"); + server = lydx_get_xpathf(config, "/ietf-ntp:ntp") != NULL; + + systemf("initctl -nbq %s chronyd", client || server ? "enable" : "disable"); + } + if (cfg) sr_release_data(cfg); @@ -454,6 +472,11 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv) ERROR("Failed to subscribe to ietf-netconf-acm"); goto err; } + rc = subscribe_model("ietf-ntp", &confd, 0); + if (rc) { + ERROR("Failed to subscribe to ietf-ntp"); + goto err; + } rc = subscribe_model("infix-dhcp-client", &confd, 0); if (rc) { ERROR("Failed to subscribe to infix-dhcp-client"); @@ -554,6 +577,10 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv) goto err; rc = dhcp_server_candidate_init(&confd); + if (rc) + goto err; + + rc = ntp_candidate_init(&confd); if (rc) goto err; /* YOUR_INIT GOES HERE */ diff --git a/src/confd/src/core.h b/src/confd/src/core.h index d376aa49c..e65f27053 100644 --- a/src/confd/src/core.h +++ b/src/confd/src/core.h @@ -247,4 +247,10 @@ int firewall_rpc_init(struct confd *confd); int firewall_candidate_init(struct confd *confd); int firewall_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd); +/* ntp.c */ +int ntp_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd); +int ntp_cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module, + const char *path, sr_event_t event, unsigned request_id, void *priv); +int ntp_candidate_init(struct confd *confd); + #endif /* CONFD_CORE_H_ */ diff --git a/src/confd/src/ntp.c b/src/confd/src/ntp.c new file mode 100644 index 000000000..9813a6f2e --- /dev/null +++ b/src/confd/src/ntp.c @@ -0,0 +1,243 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#include +#include + +#include +#include +#include + +#include "core.h" + +#define XPATH_NTP_ "/ietf-ntp:ntp" +#define NTP_CONF "/etc/chrony/conf.d/ntp-server.conf" +#define NTP_PREV NTP_CONF "-" +#define NTP_NEXT NTP_CONF "+" + +static int change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, + sr_event_t event, struct confd *confd) +{ + struct lyd_node *ntp, *entry, *makestep, *refclock; + const char *port; + FILE *fp; + + if (diff && !lydx_get_xpathf(diff, XPATH_NTP_)) + return SR_ERR_OK; + + switch (event) { + case SR_EV_ENABLED: /* first time, on register. */ + case SR_EV_CHANGE: /* regular change (copy cand running) */ + /* Generate next config */ + break; + + case SR_EV_ABORT: /* User abort, or other plugin failed */ + (void)remove(NTP_NEXT); + return SR_ERR_OK; + + case SR_EV_DONE: + /* Check if NTP container exists (presence container) */ + if (!lydx_get_xpathf(config, XPATH_NTP_)) { + DEBUG("NTP server disabled, removing config"); + systemf("rm -f %s", NTP_CONF); + + return SR_ERR_OK; + } + + /* Check if passed validation in previous event */ + if (!fexist(NTP_NEXT)) + return SR_ERR_OK; + + (void)remove(NTP_PREV); + (void)rename(NTP_CONF, NTP_PREV); + (void)rename(NTP_NEXT, NTP_CONF); + + /* Reload chronyd to pick up new config */ + if (systemf("chronyc reload sources >/dev/null 2>&1")) + ERRNO("Failed reloading chronyd sources"); + + systemf("initctl -nbq touch chronyd"); + return SR_ERR_OK; + + default: + return SR_ERR_OK; + } + + ntp = lydx_get_xpathf(config, XPATH_NTP_); + if (!ntp) + return SR_ERR_OK; + + fp = fopen(NTP_NEXT, "w"); + if (!fp) { + ERROR("Failed creating %s: %s", NTP_NEXT, strerror(errno)); + return SR_ERR_SYS; + } + + fprintf(fp, "# Generated by confd\n"); + fprintf(fp, "# This file configures chronyd as an NTP server\n\n"); + + /* Port configuration (optional) */ + port = lydx_get_cattr(ntp, "port"); + if (port) { + fprintf(fp, "# Custom NTP port\n"); + fprintf(fp, "port %d\n\n", atoi(port)); + } + + /* makestep configuration - allow clock stepping for fast initial sync */ + makestep = lydx_get_child(ntp, "makestep"); + if (makestep) { + const char *threshold = lydx_get_cattr(makestep, "threshold"); + const char *limit = lydx_get_cattr(makestep, "limit"); + + fprintf(fp, "# Allow clock stepping for fast initial sync\n"); + fprintf(fp, "makestep %.1f %d\n\n", atof(threshold), atoi(limit)); + } + + fprintf(fp, "# Upstream NTP servers and peers\n"); + LYX_LIST_FOR_EACH(lyd_child(ntp), entry, "unicast-configuration") { + const char *address, *type, *minpoll, *maxpoll, *version, *srcport; + const char *directive = NULL; + bool prefer, burst, iburst; + + address = lydx_get_cattr(entry, "address"); + type = lydx_get_cattr(entry, "type"); + minpoll = lydx_get_cattr(entry, "minpoll"); + maxpoll = lydx_get_cattr(entry, "maxpoll"); + version = lydx_get_cattr(entry, "version"); + srcport = lydx_get_cattr(entry, "port"); + prefer = lydx_get_bool(entry, "prefer"); + burst = lydx_get_bool(entry, "burst"); + iburst = lydx_get_bool(entry, "iburst"); + + if (type && strstr(type, "uc-server")) + directive = "server"; + else if (type && strstr(type, "uc-peer")) + directive = "peer"; + + if (directive && address) { + fprintf(fp, "%s %s", directive, address); + if (srcport) + fprintf(fp, " port %s", srcport); + if (iburst) + fprintf(fp, " iburst"); + if (burst) + fprintf(fp, " burst"); + if (prefer) + fprintf(fp, " prefer"); + if (minpoll) + fprintf(fp, " minpoll %s", minpoll); + if (maxpoll) + fprintf(fp, " maxpoll %s", maxpoll); + if (version) + fprintf(fp, " version %s", version); + fprintf(fp, "\n"); + } + } + fprintf(fp, "\n"); + + /* Reference clock (local stratum) - fallback time source */ + refclock = lydx_get_child(ntp, "refclock-master"); + if (refclock) { + int stratum = atoi(lydx_get_cattr(refclock, "master-stratum")); + + /* Only configure local clock if stratum is valid (1-15) */ + if (stratum >= 1 && stratum <= 15) { + fprintf(fp, "# Local reference clock - fallback stratum %d source\n", stratum); + fprintf(fp, "local stratum %d orphan\n\n", stratum); + } + } + + /* + * Enable NTP server mode - allow clients to query us + * + * Using 'allow' without arguments permits all clients. + * In a future version with access-rules support, we could + * restrict to specific subnets. + */ + fprintf(fp, "# Allow NTP clients to query this server\n"); + fprintf(fp, "allow\n\n"); + + /* + * Enable RTC synchronization + * + * On Linux, the kernel will copy system time to the hardware RTC + * every 11 minutes when the clock is synchronized. This keeps the + * RTC accurate for the next boot, which is important for embedded + * systems without continuous network connectivity. + */ + fprintf(fp, "# Synchronize system time to hardware RTC\n"); + fprintf(fp, "rtcsync\n"); + + fclose(fp); + return SR_ERR_OK; +} + +/* + * Inference callback for NTP server configuration + * + * When a user creates the /ietf-ntp:ntp presence container without + * any configuration, we infer sensible defaults: + * + * - refclock-master container (stratum defaults to 16 per YANG model) + * - makestep with threshold 1.0 and limit 3 (fast initial sync for embedded) + * + * This provides a usable NTP server configuration with just 'edit ntp' + 'leave'. + */ +static int cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module, + const char *path, sr_event_t event, unsigned request_id, void *priv) +{ + sr_val_t inferred_container = { .type = SR_CONTAINER_PRESENCE_T }; + size_t cnt = 0; + + if (event != SR_EV_UPDATE && event != SR_EV_CHANGE) + return SR_ERR_OK; + + /* Check if NTP container exists */ + if (srx_nitems(session, &cnt, XPATH_NTP_) || !cnt) + return SR_ERR_OK; + + /* Check if refclock-master already configured */ + if (!srx_nitems(session, &cnt, XPATH_NTP_"/refclock-master") && cnt > 0) + return SR_ERR_OK; + + /* Check if unicast-configuration already configured */ + if (!srx_nitems(session, &cnt, XPATH_NTP_"/unicast-configuration") && cnt > 0) + return SR_ERR_OK; + + /* Infer refclock-master container (let YANG provide default stratum 16) */ + DEBUG("Inferring NTP refclock-master container"); + srx_set_item(session, &inferred_container, 0, XPATH_NTP_"/refclock-master"); + + /* Infer makestep for fast initial sync (critical for embedded systems) */ + if (!srx_nitems(session, &cnt, XPATH_NTP_"/infix-ntp:makestep") || cnt == 0) { + DEBUG("Inferring NTP makestep container"); + /* Create presence container (let YANG provide defaults: threshold 1.0, limit 3) */ + srx_set_item(session, &inferred_container, 0, XPATH_NTP_"/infix-ntp:makestep"); + } + + return SR_ERR_OK; +} + +int ntp_change(sr_session_ctx_t *session, struct lyd_node *config, + struct lyd_node *diff, sr_event_t event, struct confd *confd) +{ + return change(session, config, diff, event, confd); +} + +int ntp_cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module, + const char *path, sr_event_t event, unsigned request_id, void *priv) +{ + return cand(session, sub_id, module, path, event, request_id, priv); +} + +int ntp_candidate_init(struct confd *confd) +{ + int rc; + + REGISTER_CHANGE(confd->cand, "ietf-ntp", XPATH_NTP_ "//.", SR_SUBSCR_UPDATE, + ntp_cand, confd, &confd->sub); + + return SR_ERR_OK; +fail: + ERROR("init failed: %s", sr_strerror(rc)); + return rc; +} diff --git a/src/confd/src/system.c b/src/confd/src/system.c index 05ddc3eaa..33a371bed 100644 --- a/src/confd/src/system.c +++ b/src/confd/src/system.c @@ -283,7 +283,7 @@ static int change_clock(sr_session_ctx_t *session, struct lyd_node *config, stru return rc; } -static int change_ntp(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd) +static int change_ntp_client(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd) { sr_change_iter_t *iter = NULL; int rc, err = SR_ERR_OK; @@ -305,7 +305,8 @@ static int change_ntp(sr_session_ctx_t *session, struct lyd_node *config, struct case SR_EV_DONE: if (!srx_enabled(session, XPATH_NTP_"/enabled")) { systemf("rm -rf /etc/chrony/conf.d/* /etc/chrony/sources.d/*"); - systemf("initctl -nbq disable chronyd"); + /* Note: chronyd enable/disable is managed centrally in core.c */ + systemf("initctl -nbq touch chronyd"); return SR_ERR_OK; } @@ -314,7 +315,8 @@ static int change_ntp(sr_session_ctx_t *session, struct lyd_node *config, struct erase("/run/chrony/.changes"); } - systemf("initctl -nbq enable chronyd"); + /* Note: chronyd enable/disable is managed centrally in core.c */ + systemf("initctl -nbq touch chronyd"); return SR_ERR_OK; default: @@ -1608,7 +1610,7 @@ int system_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd if ((rc = change_auth(session, config, diff, event, confd))) return rc; - if ((rc = change_ntp(session, config, diff, event, confd))) + if ((rc = change_ntp_client(session, config, diff, event, confd))) return rc; if ((rc = change_dns(session, config, diff, event, confd))) return rc; diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index 025665051..bb136003d 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -3,6 +3,10 @@ MODULES=( "ietf-system@2014-08-06.yang -e authentication -e local-users -e ntp -e ntp-udp-port -e timezone-name" "iana-timezones@2013-11-19.yang" + "ietf-ntp@2022-07-05.yang -e ntp-port -e unicast-configuration" + "ietf-access-control-list@2019-03-04.yang" + "ietf-packet-fields@2019-03-04.yang" + "ietf-ethertypes@2019-03-04.yang" "ietf-interfaces@2018-02-20.yang -e if-mib" "ietf-ip@2018-02-22.yang -e ipv6-privacy-autoconf" "ietf-network-instance@2019-01-21.yang" @@ -48,4 +52,5 @@ MODULES=( "infix-crypto-types@2025-06-17.yang" "ietf-keystore -e symmetric-keys" "infix-keystore@2025-12-10.yang" + "infix-ntp@2025-12-03.yang" ) diff --git a/src/confd/yang/confd/ietf-access-control-list@2019-03-04.yang b/src/confd/yang/confd/ietf-access-control-list@2019-03-04.yang new file mode 100644 index 000000000..8bb0b6261 --- /dev/null +++ b/src/confd/yang/confd/ietf-access-control-list@2019-03-04.yang @@ -0,0 +1,668 @@ +module ietf-access-control-list { + yang-version 1.1; + namespace "urn:ietf:params:xml:ns:yang:ietf-access-control-list"; + prefix acl; + + import ietf-yang-types { + prefix yang; + reference + "RFC 6991 - Common YANG Data Types."; + } + + import ietf-packet-fields { + prefix pf; + reference + "RFC 8519 - YANG Data Model for Network Access Control + Lists (ACLs)."; + } + + import ietf-interfaces { + prefix if; + reference + "RFC 8343 - A YANG Data Model for Interface Management."; + } + + organization + "IETF NETMOD (Network Modeling) Working Group."; + + contact + "WG Web: + WG List: netmod@ietf.org + + Editor: Mahesh Jethanandani + mjethanandani@gmail.com + Editor: Lisa Huang + huangyi_99@yahoo.com + Editor: Sonal Agarwal + sagarwal12@gmail.com + Editor: Dana Blair + dana@blairhome.com"; + + description + "This YANG module defines a component that describes the + configuration and monitoring of Access Control Lists (ACLs). + + The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL', + 'SHALL NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED', + 'NOT RECOMMENDED', 'MAY', and 'OPTIONAL' in this document + are to be interpreted as described in BCP 14 (RFC 2119) + (RFC 8174) when, and only when, they appear in all + capitals, as shown here. + + Copyright (c) 2019 IETF Trust and the persons identified as + the document authors. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject + to the license terms contained in, the Simplified BSD + License set forth in Section 4.c of the IETF Trust's Legal + Provisions Relating to IETF Documents + (http://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC 8519; see + the RFC itself for full legal notices."; + + revision 2019-03-04 { + description + "Initial version."; + reference + "RFC 8519: YANG Data Model for Network Access Control + Lists (ACLs)."; + } + + /* + * Identities + */ + /* + * Forwarding actions for a packet + */ + + identity forwarding-action { + description + "Base identity for actions in the forwarding category."; + } + + identity accept { + base forwarding-action; + description + "Accept the packet."; + } + + identity drop { + base forwarding-action; + description + "Drop packet without sending any ICMP error message."; + } + + identity reject { + base forwarding-action; + description + "Drop the packet and send an ICMP error message to the source."; + } + + /* + * Logging actions for a packet + */ + + identity log-action { + description + "Base identity for defining the destination for logging + actions."; + } + + identity log-syslog { + base log-action; + description + "System log (syslog) the information for the packet."; + } + identity log-none { + base log-action; + description + "No logging for the packet."; + } + + /* + * ACL type identities + */ + + identity acl-base { + description + "Base Access Control List type for all Access Control List type + identifiers."; + } + + identity ipv4-acl-type { + base acl:acl-base; + if-feature "ipv4"; + description + "An ACL that matches on fields from the IPv4 header + (e.g., IPv4 destination address) and Layer 4 headers (e.g., TCP + destination port). An ACL of type ipv4 does not contain + matches on fields in the Ethernet header or the IPv6 header."; + } + + identity ipv6-acl-type { + base acl:acl-base; + if-feature "ipv6"; + description + "An ACL that matches on fields from the IPv6 header + (e.g., IPv6 destination address) and Layer 4 headers (e.g., TCP + destination port). An ACL of type ipv6 does not contain + matches on fields in the Ethernet header or the IPv4 header."; + } + + identity eth-acl-type { + base acl:acl-base; + if-feature "eth"; + description + "An ACL that matches on fields in the Ethernet header, + like 10/100/1000baseT or a Wi-Fi Access Control List. An ACL + of type ethernet does not contain matches on fields in the + IPv4 header, the IPv6 header, or Layer 4 headers."; + } + + identity mixed-eth-ipv4-acl-type { + base acl:eth-acl-type; + base acl:ipv4-acl-type; + if-feature "mixed-eth-ipv4"; + description + "An ACL that contains a mix of entries that match + on fields in Ethernet headers and in IPv4 headers. + Matching on Layer 4 header fields may also exist in the + list."; + } + + identity mixed-eth-ipv6-acl-type { + base acl:eth-acl-type; + base acl:ipv6-acl-type; + if-feature "mixed-eth-ipv6"; + description + "An ACL that contains a mix of entries that match on fields + in Ethernet headers and in IPv6 headers. Matching + on Layer 4 header fields may also exist in the list."; + } + + identity mixed-eth-ipv4-ipv6-acl-type { + base acl:eth-acl-type; + base acl:ipv4-acl-type; + base acl:ipv6-acl-type; + if-feature "mixed-eth-ipv4-ipv6"; + description + "An ACL that contains a mix of entries that + match on fields in Ethernet headers, IPv4 headers, and IPv6 + headers. Matching on Layer 4 header fields may also exist + in the list."; + } + + /* + * Features + */ + + /* + * Features supported by device + */ + feature match-on-eth { + description + "The device can support matching on Ethernet headers."; + } + + feature match-on-ipv4 { + description + "The device can support matching on IPv4 headers."; + } + + feature match-on-ipv6 { + description + "The device can support matching on IPv6 headers."; + } + + feature match-on-tcp { + description + "The device can support matching on TCP headers."; + } + + feature match-on-udp { + description + "The device can support matching on UDP headers."; + } + + feature match-on-icmp { + description + "The device can support matching on ICMP (v4 and v6) headers."; + } + + /* + * Header classifications combinations supported by + * device + */ + + feature eth { + if-feature "match-on-eth"; + description + "Plain Ethernet ACL supported."; + } + + feature ipv4 { + if-feature "match-on-ipv4"; + description + "Plain IPv4 ACL supported."; + } + + feature ipv6 { + if-feature "match-on-ipv6"; + description + "Plain IPv6 ACL supported."; + } + + feature mixed-eth-ipv4 { + if-feature "match-on-eth and match-on-ipv4"; + description + "Ethernet and IPv4 ACL combinations supported."; + } + feature mixed-eth-ipv6 { + if-feature "match-on-eth and match-on-ipv6"; + description + "Ethernet and IPv6 ACL combinations supported."; + } + + feature mixed-eth-ipv4-ipv6 { + if-feature + "match-on-eth and match-on-ipv4 + and match-on-ipv6"; + description + "Ethernet, IPv4, and IPv6 ACL combinations supported."; + } + + /* + * Stats Features + */ + feature interface-stats { + description + "ACL counters are available and reported only per interface."; + } + + feature acl-aggregate-stats { + description + "ACL counters are aggregated over all interfaces and reported + only per ACL entry."; + } + + /* + * Attachment point features + */ + feature interface-attachment { + description + "ACLs are set on interfaces."; + } + + /* + * Typedefs + */ + typedef acl-type { + type identityref { + base acl-base; + } + description + "This type is used to refer to an ACL type."; + } + + /* + * Groupings + */ + grouping acl-counters { + description + "Common grouping for ACL counters."; + leaf matched-packets { + type yang:counter64; + config false; + description + "Count of the number of packets matching the current ACL + entry. + + An implementation should provide this counter on a + per-interface, per-ACL-entry basis if possible. + + If an implementation only supports ACL counters on a per- + entry basis (i.e., not broken out per interface), then the + value should be equal to the aggregate count across all + interfaces. + + An implementation that provides counters on a per-entry, per- + interface basis is not required to also provide an aggregate + count, e.g., per entry -- the user is expected to be able to + implement the required aggregation if such a count is + needed."; + } + + leaf matched-octets { + type yang:counter64; + config false; + description + "Count of the number of octets (bytes) matching the current + ACL entry. + + An implementation should provide this counter on a + per-interface, per-ACL-entry basis if possible. + + If an implementation only supports ACL counters per entry + (i.e., not broken out per interface), then the value + should be equal to the aggregate count across all interfaces. + + An implementation that provides counters per entry per + interface is not required to also provide an aggregate count, + e.g., per entry -- the user is expected to be able to + implement the required aggregation if such a count is needed."; + } + } + + /* + * Configuration and monitoring data nodes + */ + + container acls { + description + "This is a top-level container for Access Control Lists. + It can have one or more acl nodes."; + list acl { + key "name"; + description + "An ACL is an ordered list of ACEs. Each ACE has a + list of match criteria and a list of actions. + Since there are several kinds of ACLs implemented + with different attributes for different vendors, + this model accommodates customizing ACLs for + each kind and for each vendor."; + leaf name { + type string { + length "1..64"; + } + description + "The name of the access list. A device MAY further + restrict the length of this name; space and special + characters are not allowed."; + } + leaf type { + type acl-type; + description + "Type of ACL. Indicates the primary intended + type of match criteria (e.g., Ethernet, IPv4, IPv6, mixed, + etc.) used in the list instance."; + } + container aces { + description + "The aces container contains one or more ACE nodes."; + list ace { + key "name"; + ordered-by user; + description + "List of ACEs."; + leaf name { + type string { + length "1..64"; + } + description + "A unique name identifying this ACE."; + } + container matches { + description + "The rules in this set determine what fields will be + matched upon before any action is taken on them. + The rules are selected based on the feature set + defined by the server and the acl-type defined. + If no matches are defined in a particular container, + then any packet will match that container. If no + matches are specified at all in an ACE, then any + packet will match the ACE."; + + choice l2 { + container eth { + when "derived-from-or-self(/acls/acl/type, " + + "'acl:eth-acl-type')"; + if-feature "match-on-eth"; + uses pf:acl-eth-header-fields; + description + "Rule set that matches Ethernet headers."; + } + description + "Match Layer 2 headers, for example, Ethernet + header fields."; + } + + choice l3 { + container ipv4 { + when "derived-from-or-self(/acls/acl/type, " + + "'acl:ipv4-acl-type')"; + if-feature "match-on-ipv4"; + uses pf:acl-ip-header-fields; + uses pf:acl-ipv4-header-fields; + description + "Rule set that matches IPv4 headers."; + } + + container ipv6 { + when "derived-from-or-self(/acls/acl/type, " + + "'acl:ipv6-acl-type')"; + if-feature "match-on-ipv6"; + uses pf:acl-ip-header-fields; + uses pf:acl-ipv6-header-fields; + description + "Rule set that matches IPv6 headers."; + } + description + "Choice of either IPv4 or IPv6 headers"; + } + choice l4 { + container tcp { + if-feature "match-on-tcp"; + uses pf:acl-tcp-header-fields; + container source-port { + choice source-port { + case range-or-operator { + uses pf:port-range-or-operator; + description + "Source port definition from range or + operator."; + } + description + "Choice of source port definition using + range/operator or a choice to support future + 'case' statements, such as one enabling a + group of source ports to be referenced."; + } + description + "Source port definition."; + } + container destination-port { + choice destination-port { + case range-or-operator { + uses pf:port-range-or-operator; + description + "Destination port definition from range or + operator."; + } + description + "Choice of destination port definition using + range/operator or a choice to support future + 'case' statements, such as one enabling a + group of destination ports to be referenced."; + } + description + "Destination port definition."; + } + description + "Rule set that matches TCP headers."; + } + + container udp { + if-feature "match-on-udp"; + uses pf:acl-udp-header-fields; + container source-port { + choice source-port { + case range-or-operator { + uses pf:port-range-or-operator; + description + "Source port definition from range or + operator."; + } + description + "Choice of source port definition using + range/operator or a choice to support future + 'case' statements, such as one enabling a + group of source ports to be referenced."; + } + description + "Source port definition."; + } + container destination-port { + choice destination-port { + case range-or-operator { + uses pf:port-range-or-operator; + description + "Destination port definition from range or + operator."; + } + description + "Choice of destination port definition using + range/operator or a choice to support future + 'case' statements, such as one enabling a + group of destination ports to be referenced."; + } + description + "Destination port definition."; + } + description + "Rule set that matches UDP headers."; + } + + container icmp { + if-feature "match-on-icmp"; + uses pf:acl-icmp-header-fields; + description + "Rule set that matches ICMP headers."; + } + description + "Choice of TCP, UDP, or ICMP headers."; + } + + leaf egress-interface { + type if:interface-ref; + description + "Egress interface. This should not be used if this ACL + is attached as an egress ACL (or the value should + equal the interface to which the ACL is attached)."; + } + + leaf ingress-interface { + type if:interface-ref; + description + "Ingress interface. This should not be used if this ACL + is attached as an ingress ACL (or the value should + equal the interface to which the ACL is attached)."; + } + } + + container actions { + description + "Definition of actions for this ace entry."; + leaf forwarding { + type identityref { + base forwarding-action; + } + mandatory true; + description + "Specifies the forwarding action per ace entry."; + } + + leaf logging { + type identityref { + base log-action; + } + default "log-none"; + description + "Specifies the log action and destination for + matched packets. Default value is not to log the + packet."; + } + } + container statistics { + if-feature "acl-aggregate-stats"; + config false; + description + "Statistics gathered across all attachment points for the + given ACL."; + uses acl-counters; + } + } + } + } + container attachment-points { + description + "Enclosing container for the list of + attachment points on which ACLs are set."; + /* + * Groupings + */ + grouping interface-acl { + description + "Grouping for per-interface ingress ACL data."; + container acl-sets { + description + "Enclosing container for the list of ingress ACLs on the + interface."; + list acl-set { + key "name"; + ordered-by user; + description + "List of ingress ACLs on the interface."; + leaf name { + type leafref { + path "/acls/acl/name"; + } + description + "Reference to the ACL name applied on the ingress."; + } + list ace-statistics { + if-feature "interface-stats"; + key "name"; + config false; + description + "List of ACEs."; + leaf name { + type leafref { + path "/acls/acl/aces/ace/name"; + } + description + "Name of the ace entry."; + } + uses acl-counters; + } + } + } + } + + list interface { + if-feature "interface-attachment"; + key "interface-id"; + description + "List of interfaces on which ACLs are set."; + + leaf interface-id { + type if:interface-ref; + description + "Reference to the interface id list key."; + } + + container ingress { + uses interface-acl; + description + "The ACLs applied to the ingress interface."; + } + container egress { + uses interface-acl; + description + "The ACLs applied to the egress interface."; + } + } + } + } +} diff --git a/src/confd/yang/confd/ietf-ethertypes@2019-03-04.yang b/src/confd/yang/confd/ietf-ethertypes@2019-03-04.yang new file mode 100644 index 000000000..fd055074a --- /dev/null +++ b/src/confd/yang/confd/ietf-ethertypes@2019-03-04.yang @@ -0,0 +1,381 @@ +module ietf-ethertypes { + namespace "urn:ietf:params:xml:ns:yang:ietf-ethertypes"; + prefix ethertypes; + + organization + "IETF NETMOD (Network Modeling) Working Group."; + + contact + "WG Web: + WG List: + + Editor: Mahesh Jethanandani + "; + + description + "This module contains common definitions for the + Ethertype used by different modules. It is a + placeholder module, till such time that IEEE + starts a project to define these Ethertypes + and publishes a standard. + + At that time, this module can be deprecated. + + Copyright (c) 2019 IETF Trust and the persons identified as + the document authors. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject + to the license terms contained in, the Simplified BSD + License set forth in Section 4.c of the IETF Trust's Legal + Provisions Relating to IETF Documents + (http://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC 8519; see + the RFC itself for full legal notices."; + + revision 2019-03-04 { + description + "Initial revision."; + reference + "RFC 8519: YANG Data Model for Network Access Control + Lists (ACLs)."; + } + + typedef ethertype { + type union { + type uint16; + type enumeration { + enum ipv4 { + value 2048; + description + "Internet Protocol version 4 (IPv4) with a + hex value of 0x0800."; + reference + "RFC 791: Internet Protocol."; + } + enum arp { + value 2054; + description + "Address Resolution Protocol (ARP) with a + hex value of 0x0806."; + reference + "RFC 826: An Ethernet Address Resolution Protocol: Or + Converting Network Protocol Addresses to 48.bit + Ethernet Address for Transmission on Ethernet + Hardware."; + } + enum wlan { + value 2114; + description + "Wake-on-LAN. Hex value of 0x0842."; + } + enum trill { + value 8947; + description + "Transparent Interconnection of Lots of Links. + Hex value of 0x22F3."; + reference + "RFC 6325: Routing Bridges (RBridges): Base Protocol + Specification."; + } + enum srp { + value 8938; + description + "Stream Reservation Protocol. Hex value of + 0x22EA."; + reference + "IEEE 801.1Q-2011."; + } + enum decnet { + value 24579; + description + "DECnet Phase IV. Hex value of 0x6003."; + } + enum rarp { + value 32821; + description + "Reverse Address Resolution Protocol. + Hex value 0x8035."; + reference + "RFC 903: A Reverse Address Resolution Protocol."; + } + enum appletalk { + value 32923; + description + "Appletalk (Ethertalk). Hex value of 0x809B."; + } + enum aarp { + value 33011; + description + "Appletalk Address Resolution Protocol. Hex value + of 0x80F3."; + } + enum vlan { + value 33024; + description + "VLAN-tagged frame (IEEE 802.1Q) and Shortest Path + Bridging IEEE 802.1aq with Network-Network + Interface (NNI) compatibility. Hex value of + 0x8100."; + reference + "IEEE 802.1Q."; + } + enum ipx { + value 33079; + description + "Internetwork Packet Exchange (IPX). Hex value + of 0x8137."; + } + enum qnx { + value 33284; + description + "QNX Qnet. Hex value of 0x8204."; + } + enum ipv6 { + value 34525; + description + "Internet Protocol Version 6 (IPv6). Hex value + of 0x86DD."; + reference + "RFC 8200: Internet Protocol, Version 6 (IPv6) + Specification + RFC 8201: Path MTU Discovery for IP version 6."; + } + enum efc { + value 34824; + description + "Ethernet flow control using pause frames. + Hex value of 0x8808."; + reference + "IEEE 802.1Qbb."; + } + enum esp { + value 34825; + description + "Ethernet Slow Protocol. Hex value of 0x8809."; + reference + "IEEE 802.3-2015."; + } + enum cobranet { + value 34841; + description + "CobraNet. Hex value of 0x8819."; + } + enum mpls-unicast { + value 34887; + description + "Multiprotocol Label Switching (MPLS) unicast traffic. + Hex value of 0x8847."; + reference + "RFC 3031: Multiprotocol Label Switching Architecture."; + } + enum mpls-multicast { + value 34888; + description + "MPLS multicast traffic. Hex value of 0x8848."; + reference + "RFC 3031: Multiprotocol Label Switching Architecture."; + } + enum pppoe-discovery { + value 34915; + description + "Point-to-Point Protocol over Ethernet. Used during + the discovery process. Hex value of 0x8863."; + reference + "RFC 2516: A Method for Transmitting PPP Over Ethernet + (PPPoE)."; + } + enum pppoe-session { + value 34916; + description + "Point-to-Point Protocol over Ethernet. Used during + session stage. Hex value of 0x8864."; + reference + "RFC 2516: A Method for Transmitting PPP Over Ethernet + (PPPoE)."; + } + enum intel-ans { + value 34925; + description + "Intel Advanced Networking Services. Hex value of + 0x886D."; + } + enum jumbo-frames { + value 34928; + description + "Jumbo frames or Ethernet frames with more than + 1500 bytes of payload, up to 9000 bytes."; + } + enum homeplug { + value 34939; + description + "Family name for the various power line + communications. Hex value of 0x887B."; + } + enum eap { + value 34958; + description + "Ethernet Access Protocol (EAP) over LAN. Hex value + of 0x888E."; + reference + "IEEE 802.1X."; + } + enum profinet { + value 34962; + description + "PROcess FIeld Net (PROFINET). Hex value of 0x8892."; + } + enum hyperscsi { + value 34970; + description + "Small Computer System Interface (SCSI) over Ethernet. + Hex value of 0x889A."; + } + enum aoe { + value 34978; + description + "Advanced Technology Advancement (ATA) over Ethernet. + Hex value of 0x88A2."; + } + enum ethercat { + value 34980; + description + "Ethernet for Control Automation Technology (EtherCAT). + Hex value of 0x88A4."; + } + enum provider-bridging { + value 34984; + description + "Provider Bridging (802.1ad) and Shortest Path Bridging + (801.1aq). Hex value of 0x88A8."; + reference + "IEEE 802.1ad and IEEE 802.1aq)."; + } + enum ethernet-powerlink { + value 34987; + description + "Ethernet Powerlink. Hex value of 0x88AB."; + } + enum goose { + value 35000; + description + "Generic Object Oriented Substation Event (GOOSE). + Hex value of 0x88B8."; + reference + "IEC/ISO 8802-2 and 8802-3."; + } + enum gse { + value 35001; + description + "Generic Substation Events. Hex value of 88B9."; + reference + "IEC 61850."; + } + enum sv { + value 35002; + description + "Sampled Value Transmission. Hex value of 0x88BA."; + reference + "IEC 61850."; + } + enum lldp { + value 35020; + description + "Link Layer Discovery Protocol (LLDP). Hex value of + 0x88CC."; + reference + "IEEE 802.1AB."; + } + enum sercos { + value 35021; + description + "Sercos Interface. Hex value of 0x88CD."; + } + enum wsmp { + value 35036; + description + "WAVE Short Message Protocol (WSMP). Hex value of + 0x88DC."; + } + enum homeplug-av-mme { + value 35041; + description + "HomePlug AV Mobile Management Entity (MME). Hex value + of 88E1."; + } + enum mrp { + value 35043; + description + "Media Redundancy Protocol (MRP). Hex value of + 0x88E3."; + reference + "IEC 62439-2."; + } + enum macsec { + value 35045; + description + "MAC Security. Hex value of 0x88E5."; + reference + "IEEE 802.1AE."; + } + enum pbb { + value 35047; + description + "Provider Backbone Bridges (PBB). Hex value of + 0x88E7."; + reference + "IEEE 802.1ah."; + } + enum cfm { + value 35074; + description + "Connectivity Fault Management (CFM). Hex value of + 0x8902."; + reference + "IEEE 802.1ag."; + } + enum fcoe { + value 35078; + description + "Fiber Channel over Ethernet (FCoE). Hex value of + 0x8906."; + reference + "T11 FC-BB-5."; + } + enum fcoe-ip { + value 35092; + description + "FCoE Initialization Protocol. Hex value of 0x8914."; + } + enum roce { + value 35093; + description + "RDMA over Converged Ethernet (RoCE). Hex value of + 0x8915."; + } + enum tte { + value 35101; + description + "TTEthernet Protocol Control Frame (TTE). Hex value + of 0x891D."; + reference + "SAE AS6802."; + } + enum hsr { + value 35119; + description + "High-availability Seamless Redundancy (HSR). Hex + value of 0x892F."; + reference + "IEC 62439-3:2016."; + } + } + } + description + "The uint16 type placeholder is defined to enable + users to manage their own ethertypes not + covered by the module. Otherwise, the module contains + enum definitions for the more commonly used ethertypes."; + } +} diff --git a/src/confd/yang/confd/ietf-ntp@2022-07-05.yang b/src/confd/yang/confd/ietf-ntp@2022-07-05.yang new file mode 100644 index 000000000..0ac851e25 --- /dev/null +++ b/src/confd/yang/confd/ietf-ntp@2022-07-05.yang @@ -0,0 +1,1482 @@ +module ietf-ntp { + yang-version 1.1; + namespace "urn:ietf:params:xml:ns:yang:ietf-ntp"; + prefix ntp; + + import ietf-yang-types { + prefix yang; + reference + "RFC 6991: Common YANG Data Types"; + } + import ietf-inet-types { + prefix inet; + reference + "RFC 6991: Common YANG Data Types"; + } + import ietf-interfaces { + prefix if; + reference + "RFC 8343: A YANG Data Model for Interface Management"; + } + import ietf-system { + prefix sys; + reference + "RFC 7317: A YANG Data Model for System Management"; + } + import ietf-access-control-list { + prefix acl; + reference + "RFC 8519: YANG Data Model for Network Access Control + Lists (ACLs)"; + } + import ietf-routing-types { + prefix rt-types; + reference + "RFC 8294: Common YANG Data Types for the Routing Area"; + } + import ietf-netconf-acm { + prefix nacm; + reference + "RFC 8341: Network Configuration Access Control Model"; + } + + organization + "IETF NTP (Network Time Protocol) Working Group"; + contact + "WG Web: + WG List: + Editor: Ankit Kumar Sinha + "; + description + "This document defines a YANG data model that can be used + to configure and manage Network Time Protocol (NTP) version 4. + It can also be used to configure and manage version 3. + The data model includes configuration data and state data. + + The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL', 'SHALL + NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED', 'NOT RECOMMENDED', + 'MAY', and 'OPTIONAL' in this document are to be interpreted as + described in BCP 14 (RFC 2119) (RFC 8174) when, and only when, + they appear in all capitals, as shown here. + + Copyright (c) 2022 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject + to the license terms contained in, the Revised BSD License + set forth in Section 4.c of the IETF Trust's Legal Provisions + Relating to IETF Documents + (https://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC 9249; see the + RFC itself for full legal notices."; + + revision 2022-07-05 { + description + "Initial revision"; + reference + "RFC 9249: A YANG Data Model for NTP"; + } + + /* Typedef Definitions */ + + typedef ntp-stratum { + type uint8 { + range "1..16"; + } + description + "The level of each server in the hierarchy is defined by + a stratum. Primary servers are assigned with stratum + one; secondary servers at each lower level are assigned with + one stratum greater than the preceding level."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 3"; + } + + typedef ntp-version { + type uint8 { + range "3..max"; + } + default "4"; + description + "The current NTP version supported by the corresponding + association"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 1"; + } + + typedef refid { + type union { + type inet:ipv4-address; + type uint32; + type string { + length "4"; + } + } + description + "A code identifying the particular server or reference + clock. The interpretation depends upon stratum. It + could be an IPv4 address, the first 32 bits of the MD5 hash + of the IPv6 address, or a string for the Reference Identifier + and kiss codes. Some examples: + + -- a refclock ID like '127.127.1.0' for local clock sync + + -- uni/multi/broadcast associations for IPv4 will look like + '203.0.113.1' and '0x4321FEDC' for IPv6 + + -- sync with a primary source will look like 'DCN', 'NIST', + 'ATOM' + + -- kiss codes will look like 'AUTH', 'DROP', or 'RATE' + + Note that the use of an MD5 hash for IPv6 addresses is not + for cryptographic purposes."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 7.3"; + } + + typedef ntp-date-and-time { + type union { + type yang:date-and-time; + type uint8; + } + description + "Follows the date-and-time format when valid values exist. + Otherwise, allows for setting a special value such as + zero."; + reference + "RFC 6991: Common YANG Data Types"; + } + + typedef log2seconds { + type int8; + description + "An 8-bit signed integer that represents signed log2 + seconds."; + } + + /* features */ + + feature ntp-port { + description + "Support for NTP port configuration"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 7.2"; + } + + feature authentication { + description + "Support for NTP symmetric key authentication"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 7.3"; + } + + feature deprecated { + description + "Support deprecated MD5-based authentication (RFC 8573), + SHA-1, or any other deprecated authentication mechanism. + It is enabled to support legacy compatibility when secure + cryptographic algorithms are not available to use. + It is also used to configure keystrings in ASCII format."; + reference + "RFC 1321: The MD5 Message-Digest Algorithm, + RFC 3174: US Secure Hash Algorithm 1 (SHA1), + SHS: Secure Hash Standard (SHS) (FIPS PUB 180-4)"; + } + + feature hex-key-string { + description + "Support hexadecimal key string"; + } + + feature access-rules { + description + "Support for NTP access control"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 9.2"; + } + + feature unicast-configuration { + description + "Support for NTP client/server or active/passive + in unicast"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 3"; + } + + feature broadcast-server { + description + "Support for broadcast server"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 3"; + } + + feature broadcast-client { + description + "Support for broadcast client"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 3"; + } + + feature multicast-server { + description + "Support for multicast server"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 3.1"; + } + feature multicast-client { + description + "Support for multicast client"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 3.1"; + } + + feature manycast-server { + description + "Support for manycast server"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 3.1"; + } + + feature manycast-client { + description + "Support for manycast client"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 3.1"; + } + + /* Identity */ + /* unicast-configurations types */ + + identity unicast-configuration-type { + if-feature "unicast-configuration"; + description + "This defines NTP unicast mode of operation as used + for unicast-configurations."; + } + + identity uc-server { + if-feature "unicast-configuration"; + base unicast-configuration-type; + description + "Use client association mode where the unicast server + address is configured."; + } + + identity uc-peer { + if-feature "unicast-configuration"; + base unicast-configuration-type; + description + "Use symmetric active association mode where the peer + address is configured."; + } + + /* association-modes */ + + identity association-mode { + description + "The NTP association modes"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 3"; + } + + identity active { + base association-mode; + description + "Use symmetric active association mode (mode 1). + This device may synchronize with its NTP peer + or provide synchronization to a configured NTP peer."; + } + + identity passive { + base association-mode; + description + "Use symmetric passive association mode (mode 2). + This device has learned this association dynamically. + This device may synchronize with its NTP peer."; + } + + identity client { + base association-mode; + description + "Use client association mode (mode 3). + This device will not provide synchronization + to the configured NTP server."; + } + + identity server { + base association-mode; + description + "Use server association mode (mode 4). + This device will provide synchronization to + NTP clients."; + } + + identity broadcast-server { + base association-mode; + description + "Use broadcast server mode (mode 5). + This mode defines that it's either working + as a broadcast server or a multicast server."; + } + + identity broadcast-client { + base association-mode; + description + "This mode defines that it's either working + as a broadcast client (mode 6) or a multicast client."; + } + + /* access-mode */ + + identity access-mode { + if-feature "access-rules"; + description + "This defines NTP access-modes. These identify + how the ACL is applied with NTP."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 9.2"; + } + + identity peer-access-mode { + if-feature "access-rules"; + base access-mode; + description + "Permit others to synchronize their time with this NTP + or vice versa. + NTP control queries are also accepted. This enables + full access authority."; + } + + identity server-access-mode { + if-feature "access-rules"; + base access-mode; + description + "Permit others to synchronize their time with this NTP + entity, but vice versa is not supported. NTP control + queries are accepted."; + } + + identity server-only-access-mode { + if-feature "access-rules"; + base access-mode; + description + "Permit others to synchronize their time with this NTP + entity, but vice versa is not supported. NTP control + queries are not accepted."; + } + + identity query-only-access-mode { + if-feature "access-rules"; + base access-mode; + description + "Only control queries are accepted."; + } + + /* clock-state */ + + identity clock-state { + description + "This defines NTP clock status at a high level."; + } + + identity synchronized { + base clock-state; + description + "Indicates that the local clock has been synchronized with + an NTP server or the reference clock."; + } + + identity unsynchronized { + base clock-state; + description + "Indicates that the local clock has not been synchronized + with any NTP server."; + } + + /* ntp-sync-state */ + + identity ntp-sync-state { + description + "This defines NTP clock sync state at a more granular + level. Referred to as 'Clock state definitions' in + RFC 5905."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Appendix A.1.1"; + } + + identity clock-never-set { + base ntp-sync-state; + description + "Indicates the clock was never set."; + } + + identity freq-set-by-cfg { + base ntp-sync-state; + description + "Indicates the clock frequency is set by + NTP configuration or file."; + } + + identity spike { + base ntp-sync-state; + description + "Indicates a spike is detected."; + } + + identity freq { + base ntp-sync-state; + description + "Indicates the frequency mode."; + } + + identity clock-synchronized { + base ntp-sync-state; + description + "Indicates that the clock is synchronized."; + } + + /* crypto-algorithm */ + + identity crypto-algorithm { + description + "Base identity of cryptographic algorithm options."; + } + + identity md5 { + if-feature "deprecated"; + base crypto-algorithm; + description + "The MD5 algorithm. Note that RFC 8573 + deprecates the use of MD5-based authentication."; + reference + "RFC 1321: The MD5 Message-Digest Algorithm"; + } + + identity sha-1 { + if-feature "deprecated"; + base crypto-algorithm; + description + "The SHA-1 algorithm"; + reference + "RFC 3174: US Secure Hash Algorithm 1 (SHA1)"; + } + + identity hmac-sha-1 { + if-feature "deprecated"; + base crypto-algorithm; + description + "HMAC-SHA-1 authentication algorithm"; + reference + "SHS: Secure Hash Standard (SHS) (FIPS PUB 180-4)"; + } + + identity hmac-sha1-12 { + if-feature "deprecated"; + base crypto-algorithm; + description + "The HMAC-SHA1-12 algorithm"; + } + + identity hmac-sha-256 { + description + "HMAC-SHA-256 authentication algorithm"; + reference + "SHS: Secure Hash Standard (SHS) (FIPS PUB 180-4)"; + } + + identity hmac-sha-384 { + description + "HMAC-SHA-384 authentication algorithm"; + reference + "SHS: Secure Hash Standard (SHS) (FIPS PUB 180-4)"; + } + + identity hmac-sha-512 { + description + "HMAC-SHA-512 authentication algorithm"; + reference + "SHS: Secure Hash Standard (SHS) (FIPS PUB 180-4)"; + } + + identity aes-cmac { + base crypto-algorithm; + description + "The AES-CMAC algorithm -- required by + RFC 8573 for MAC for the NTP."; + reference + "RFC 4493: The AES-CMAC Algorithm, + RFC 8573: Message Authentication Code for the Network + Time Protocol"; + } + + /* Groupings */ + + grouping key { + description + "The key"; + nacm:default-deny-all; + choice key-string-style { + description + "Key string styles"; + case keystring { + leaf keystring { + if-feature "deprecated"; + type string; + description + "Key string in ASCII format"; + } + } + case hexadecimal { + if-feature "hex-key-string"; + leaf hexadecimal-string { + type yang:hex-string; + description + "Key in hexadecimal string format. When compared + to ASCII, specification in hexadecimal affords + greater key entropy with the same number of + internal key-string octets. Additionally, it + discourages use of well-known words or + numbers."; + } + } + } + } + + grouping authentication-key { + description + "To define an authentication key for an NTP + time source."; + leaf keyid { + type uint32 { + range "1..max"; + } + description + "Authentication key identifier"; + } + leaf algorithm { + type identityref { + base crypto-algorithm; + } + description + "Authentication algorithm. Note that RFC 8573 + deprecates the use of MD5-based authentication + and recommends AES-CMAC."; + } + container key { + uses key; + description + "The key. Note that RFC 8573 deprecates the use + of MD5-based authentication."; + } + leaf istrusted { + type boolean; + description + "Keyid is trusted or not"; + } + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Sections 7.3 and 7.4"; + } + + grouping authentication { + description + "Authentication"; + choice authentication-type { + description + "Type of authentication"; + case symmetric-key { + leaf keyid { + type leafref { + path "/ntp:ntp/ntp:authentication/" + + "ntp:authentication-keys/ntp:keyid"; + } + description + "Authentication key id referenced in this + association."; + } + } + } + } + + grouping statistics { + description + "NTP packet statistic"; + leaf discontinuity-time { + type ntp-date-and-time; + description + "The time on the most recent occasion at which any one or + more of these NTP counters suffered a discontinuity. If + no such discontinuities have occurred, then this node + contains the time the NTP association was + (re-)initialized."; + } + leaf packet-sent { + type yang:counter32; + description + "The total number of NTP packets delivered to the + transport service by this NTP entity for this + association. + Discontinuities in the value of this counter can occur + upon cold start, reinitialization of the NTP entity or the + management system, and at other times."; + } + leaf packet-sent-fail { + type yang:counter32; + description + "The number of times NTP packet sending failed."; + } + leaf packet-received { + type yang:counter32; + description + "The total number of NTP packets delivered to the + NTP entity from this association. + Discontinuities in the value of this counter can occur + upon cold start, reinitialization of the NTP entity or the + management system, and at other times."; + } + leaf packet-dropped { + type yang:counter32; + description + "The total number of NTP packets that were delivered + to this NTP entity from this association and that this + entity was not able to process due to an NTP error. + Discontinuities in the value of this counter can occur + upon cold start, reinitialization of the NTP entity or the + management system, and at other times."; + } + } + + grouping common-attributes { + description + "NTP common attributes for configuration"; + leaf minpoll { + type log2seconds; + default "6"; + description + "The minimum poll interval used in this association"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 7.2"; + } + leaf maxpoll { + type log2seconds; + default "10"; + description + "The maximum poll interval used in this association"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 7.2"; + } + leaf port { + if-feature "ntp-port"; + type inet:port-number { + range "123 | 1024..max"; + } + default "123"; + description + "Specify the port used to send NTP packets."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 7.2"; + } + leaf version { + type ntp-version; + description + "NTP version"; + } + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification"; + } + + grouping association-ref { + description + "Reference to NTP association mode"; + leaf associations-address { + type leafref { + path "/ntp:ntp/ntp:associations/ntp:association" + + "/ntp:address"; + } + description + "Indicates the association's address + that results in clock synchronization."; + } + leaf associations-local-mode { + type leafref { + path "/ntp:ntp/ntp:associations/ntp:association" + + "/ntp:local-mode"; + } + description + "Indicates the association's local-mode + that results in clock synchronization."; + } + leaf associations-isconfigured { + type leafref { + path "/ntp:ntp/ntp:associations/ntp:association/" + + "ntp:isconfigured"; + } + description + "Indicates if the association (that resulted in the + clock synchronization) is explicitly configured."; + } + } + + container ntp { + when 'false() = boolean(/sys:system/sys:ntp)' { + description + "Applicable when the system /sys/ntp/ is not used."; + } + presence "NTP is enabled and system should attempt to + synchronize the system clock with an NTP server + from the 'ntp/associations' list."; + description + "Configuration parameters for NTP"; + leaf port { + if-feature "ntp-port"; + type inet:port-number { + range "123 | 1024..max"; + } + default "123"; + description + "Specify the port used to send and receive NTP packets."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 7.2"; + } + container refclock-master { + presence "NTP master clock is enabled."; + description + "Configures the local clock of this device as NTP server."; + leaf master-stratum { + type ntp-stratum; + default "16"; + description + "Stratum level from which NTP clients get their time + synchronized."; + } + } + container authentication { + if-feature "authentication"; + description + "Configuration of authentication"; + leaf auth-enabled { + type boolean; + default "false"; + description + "Controls whether NTP authentication is enabled + or disabled on this device."; + } + list authentication-keys { + key "keyid"; + uses authentication-key; + description + "List of authentication keys"; + } + } + container access-rules { + if-feature "access-rules"; + description + "Configuration to control access to NTP service + by using the NTP access-group feature. + The access-mode identifies how the ACL is + applied with NTP."; + list access-rule { + key "access-mode"; + description + "List of access rules"; + leaf access-mode { + type identityref { + base access-mode; + } + description + "The NTP access-mode. Some of the possible values + include peer, server, synchronization, query, + etc."; + } + leaf acl { + type leafref { + path "/acl:acls/acl:acl/acl:name"; + } + description + "Control access configuration to be used."; + } + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 9.2"; + } + } + container clock-state { + config false; + description + "Clock operational state of the NTP"; + container system-status { + description + "System status of NTP"; + leaf clock-state { + type identityref { + base clock-state; + } + mandatory true; + description + "The state of the system clock. Some of the possible + values include synchronized and unsynchronized."; + } + leaf clock-stratum { + type ntp-stratum; + mandatory true; + description + "The NTP entity's own stratum value. Should be one + greater than the preceding level. + 16 if unsynchronized."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 3"; + } + leaf clock-refid { + type refid; + mandatory true; + description + "A code identifying the particular server or reference + clock. The interpretation depends upon stratum. It + could be an IPv4 address, the first 32 bits of the MD5 + hash of the IPv6 address, or a string for the Reference + Identifier and kiss codes. Some examples: + + -- a refclock ID like '127.127.1.0' for local clock sync + + -- uni/multi/broadcast associations for IPv4 will look + like '203.0.113.1' and '0x4321FEDC' for IPv6 + + -- sync with primary source will look like 'DCN', + 'NIST', 'ATOM' + + -- kiss codes will look like 'AUTH', 'DROP', 'RATE' + + Note that the use of MD5 hash for IPv6 address is not + for cryptographic purposes."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 7.3"; + } + uses association-ref { + description + "Reference to association"; + } + leaf nominal-freq { + type decimal64 { + fraction-digits 4; + } + units "Hz"; + mandatory true; + description + "The nominal frequency of the local clock. An ideal + frequency with zero uncertainty."; + } + leaf actual-freq { + type decimal64 { + fraction-digits 4; + } + units "Hz"; + mandatory true; + description + "The actual frequency of the local clock"; + } + leaf clock-precision { + type log2seconds; + mandatory true; + description + "Clock precision of this system in signed integer format, + in log 2 seconds - (prec=2^(-n)). A value of 5 would + mean 2^-5 = 0.03125 seconds = 31.25 ms."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 7.3"; + } + leaf clock-offset { + type decimal64 { + fraction-digits 3; + } + units "milliseconds"; + description + "The signed time offset to the current selected reference + time source, e.g., '0.032ms' or '1.232ms'. The negative + value indicates that the local clock is behind the + current selected reference time source."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 9.1"; + } + leaf root-delay { + type decimal64 { + fraction-digits 3; + } + units "milliseconds"; + description + "Total delay along the path to the root clock"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Sections 4 and 7.3"; + } + leaf root-dispersion { + type decimal64 { + fraction-digits 3; + } + units "milliseconds"; + description + "The dispersion to the local clock + and the root clock, e.g., '6.927ms'."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Sections 4, 7.3, and 10"; + } + leaf reference-time { + type ntp-date-and-time; + description + "The reference timestamp. Time when the system clock was + last set or corrected."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 7.3"; + } + leaf sync-state { + type identityref { + base ntp-sync-state; + } + mandatory true; + description + "The synchronization status of the local clock. Referred + to as 'Clock state definitions' in RFC 5905."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Appendix A.1.1"; + } + } + } + list unicast-configuration { + if-feature "unicast-configuration"; + key "address type"; + description + "List of NTP unicast-configurations"; + leaf address { + type inet:ip-address; + description + "Address of this association"; + } + leaf type { + type identityref { + base unicast-configuration-type; + } + description + "The unicast configuration type, for example, + unicast-server"; + } + container authentication { + if-feature "authentication"; + description + "Authentication used for this association"; + uses authentication; + } + leaf prefer { + type boolean; + default "false"; + description + "Whether or not this association is preferred"; + } + leaf burst { + type boolean; + default "false"; + description + "If set, a series of packets are sent instead of a single + packet within each synchronization interval to achieve + faster synchronization."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol + and Algorithms Specification, Section 13.1"; + } + leaf iburst { + type boolean; + default "false"; + description + "If set, a series of packets are sent instead of a single + packet within the initial synchronization interval to + achieve faster initial synchronization."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol + and Algorithms Specification, Section 13.1"; + } + leaf source { + type if:interface-ref; + description + "The interface whose IP address is used by this association + as the source address."; + } + uses common-attributes { + description + "Common attributes like port, version, and min and max + poll."; + } + } + container associations { + description + "Association parameters"; + list association { + key "address local-mode isconfigured"; + config false; + description + "List of NTP associations. Here address, local-mode, + and isconfigured are required to uniquely identify + a particular association. Let's take the following + examples: + + 1) If RT1 is acting as broadcast server + and RT2 is acting as broadcast client, then RT2 + will form a dynamic association with the address as + RT1, local-mode as client, and isconfigured as false. + + 2) When RT2 is configured with unicast server RT1, + then RT2 will form an association with the address as + RT1, local-mode as client, and isconfigured as true. + + Thus, all three leaves are needed as key to uniquely + identify the association."; + leaf address { + type inet:ip-address; + description + "The remote address of this association. Represents the + IP address of a unicast/multicast/broadcast address."; + } + leaf local-mode { + type identityref { + base association-mode; + } + description + "Local-mode of this NTP association"; + } + leaf isconfigured { + type boolean; + description + "Indicates if this association is configured (true) or + dynamically learned (false)."; + } + leaf stratum { + type ntp-stratum; + description + "The association stratum value"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 3"; + } + leaf refid { + type refid; + description + "A code identifying the particular server or reference + clock. The interpretation depends upon stratum. It + could be an IPv4 address or first 32 bits of the MD5 + hash of the IPv6 address or a string for the Reference + Identifier and kiss codes. Some examples: + + -- a refclock ID like '127.127.1.0' for local clock sync + + -- uni/multi/broadcast associations for IPv4 will look + like '203.0.113.1' and '0x4321FEDC' for IPv6 + + -- sync with primary source will look like 'DCN', + 'NIST', or 'ATOM' + + -- kiss codes will look like 'AUTH', 'DROP', or 'RATE' + + Note that the use of an MD5 hash for IPv6 address is + not for cryptographic purposes."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 7.3"; + } + leaf authentication { + if-feature "authentication"; + type leafref { + path "/ntp:ntp/ntp:authentication/" + + "ntp:authentication-keys/ntp:keyid"; + } + description + "Authentication key used for this association"; + } + leaf prefer { + type boolean; + default "false"; + description + "Indicates if this association is preferred"; + } + leaf peer-interface { + type if:interface-ref; + description + "The interface that is used for communication"; + } + uses common-attributes { + description + "Common attributes like port, version, and min and + max poll"; + } + leaf reach { + type uint8; + description + "An 8-bit shift register that tracks packet + generation and receipt. It is used to determine + whether the server is reachable and the data are + fresh."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Sections 9.2 and 13"; + } + leaf unreach { + type uint8; + units "seconds"; + description + "A count of how long in second the server has been + unreachable, i.e., the reach value has been zero."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Sections 9.2 and 13"; + } + leaf poll { + type log2seconds; + description + "The polling interval for current association in signed + log2 seconds."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 7.3"; + } + leaf now { + type uint32; + units "seconds"; + description + "The time since the last NTP packet was + received or last synchronized."; + } + leaf offset { + type decimal64 { + fraction-digits 3; + } + units "milliseconds"; + description + "The signed offset between the local clock + and the peer clock, e.g., '0.032ms' or '1.232ms'. The + negative value indicates that the local clock is behind + the peer."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 8"; + } + leaf delay { + type decimal64 { + fraction-digits 3; + } + units "milliseconds"; + description + "The network delay between the local clock + and the peer clock"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 8"; + } + leaf dispersion { + type decimal64 { + fraction-digits 3; + } + units "milliseconds"; + description + "The root dispersion between the local clock + and the peer clock."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 10"; + } + leaf originate-time { + type ntp-date-and-time; + description + "This is the local time, in timestamp format, + when the latest NTP packet was sent to the peer + (called T1)."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol and + Algorithms Specification, Section 8"; + } + leaf receive-time { + type ntp-date-and-time; + description + "This is the local time, in timestamp format, + when the latest NTP packet arrived at the peer + (called T2). If the peer becomes unreachable, + the value is set to zero."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol + and Algorithms Specification, Section 8"; + } + leaf transmit-time { + type ntp-date-and-time; + description + "This is the local time, in timestamp format, + at which the NTP packet departed the peer + (called T3). If the peer becomes unreachable, + the value is set to zero."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol + and Algorithms Specification, Section 8"; + } + leaf input-time { + type ntp-date-and-time; + description + "This is the local time, in timestamp format, + when the latest NTP message from the peer arrived + (called T4). If the peer becomes unreachable, + value is set to zero."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol + and Algorithms Specification, Section 8"; + } + container ntp-statistics { + description + "Per peer packet send and receive statistics"; + uses statistics { + description + "NTP send and receive packet statistics"; + } + } + } + } + container interfaces { + description + "Configuration parameters for NTP interfaces"; + list interface { + key "name"; + description + "List of interfaces"; + leaf name { + type if:interface-ref; + description + "The interface name"; + } + container broadcast-server { + if-feature "broadcast-server"; + presence "NTP broadcast-server is configured on this + interface."; + description + "Configuration of broadcast server"; + leaf ttl { + type uint8; + description + "Specifies the time to live (TTL) for a + broadcast packet"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol + and Algorithms Specification, Section 3.1"; + } + container authentication { + if-feature "authentication"; + description + "Authentication used on this interface"; + uses authentication; + } + uses common-attributes { + description + "Common attributes such as port, version, and min and + max poll"; + } + reference + "RFC 5905: Network Time Protocol Version 4: Protocol + and Algorithms Specification, Section 3.1"; + } + container broadcast-client { + if-feature "broadcast-client"; + presence "NTP broadcast-client is configured on this + interface."; + description + "Configuration of broadcast client"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol + and Algorithms Specification, Section 3.1"; + } + list multicast-server { + if-feature "multicast-server"; + key "address"; + description + "Configuration of multicast server"; + leaf address { + type rt-types:ip-multicast-group-address; + description + "The IP address to send NTP multicast packets"; + } + leaf ttl { + type uint8; + description + "Specifies the TTL for a multicast packet"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol + and Algorithms Specification, Section 3.1"; + } + container authentication { + if-feature "authentication"; + description + "Authentication used on this interface"; + uses authentication; + } + uses common-attributes { + description + "Common attributes such as port, version, and min and + max poll"; + } + reference + "RFC 5905: Network Time Protocol Version 4: Protocol + and Algorithms Specification, Section 3.1"; + } + list multicast-client { + if-feature "multicast-client"; + key "address"; + description + "Configuration of a multicast client"; + leaf address { + type rt-types:ip-multicast-group-address; + description + "The IP address of the multicast group to + join"; + } + reference + "RFC 5905: Network Time Protocol Version 4: Protocol + and Algorithms Specification, Section 3.1"; + } + list manycast-server { + if-feature "manycast-server"; + key "address"; + description + "Configuration of a manycast server"; + leaf address { + type rt-types:ip-multicast-group-address; + description + "The multicast group IP address to receive + manycast client messages."; + } + reference + "RFC 5905: Network Time Protocol Version 4: Protocol + and Algorithms Specification, Section 3.1"; + } + list manycast-client { + if-feature "manycast-client"; + key "address"; + description + "Configuration of manycast-client"; + leaf address { + type rt-types:ip-multicast-group-address; + description + "The group IP address that the manycast client + broadcasts the request message to"; + } + container authentication { + if-feature "authentication"; + description + "Authentication used on this interface"; + uses authentication; + } + leaf ttl { + type uint8; + description + "Specifies the maximum TTL for the expanding + ring search"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol + and Algorithms Specification, Section 3.1"; + } + leaf minclock { + type uint8; + description + "The minimum manycast survivors in this + association"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol + and Algorithms Specification, Section 13.2"; + } + leaf maxclock { + type uint8; + description + "The maximum manycast candidates in this + association"; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol + and Algorithms Specification, Section 13.2"; + } + leaf beacon { + type log2seconds; + description + "The beacon is the upper limit of the poll interval. + When the TTL reaches its limit without finding the + minimum number of manycast servers, the poll interval + increases until reaching the beacon value, when it + starts over from the beginning."; + reference + "RFC 5905: Network Time Protocol Version 4: Protocol + and Algorithms Specification, Section 13.2"; + } + uses common-attributes { + description + "Common attributes like port, version, and min and + max poll"; + } + reference + "RFC 5905: Network Time Protocol Version 4: Protocol + and Algorithms Specification, Section 3.1"; + } + } + } + container ntp-statistics { + config false; + description + "Total NTP packet statistics"; + uses statistics { + description + "NTP send and receive packet statistics"; + } + } + } + + rpc statistics-reset { + description + "Reset statistics collected."; + input { + choice association-or-all { + description + "Resets statistics for a particular association or + all."; + case association { + uses association-ref; + description + "This resets all the statistics collected for + the association."; + } + case all { + description + "This resets all the statistics collected."; + } + } + } + } +} diff --git a/src/confd/yang/confd/ietf-packet-fields@2019-03-04.yang b/src/confd/yang/confd/ietf-packet-fields@2019-03-04.yang new file mode 100644 index 000000000..2fb797bd8 --- /dev/null +++ b/src/confd/yang/confd/ietf-packet-fields@2019-03-04.yang @@ -0,0 +1,576 @@ +module ietf-packet-fields { + yang-version 1.1; + namespace "urn:ietf:params:xml:ns:yang:ietf-packet-fields"; + prefix packet-fields; + + import ietf-inet-types { + prefix inet; + reference + "RFC 6991 - Common YANG Data Types."; + } + + import ietf-yang-types { + prefix yang; + reference + "RFC 6991 - Common YANG Data Types."; + } + + import ietf-ethertypes { + prefix eth; + reference + "RFC 8519 - YANG Data Model for Network Access Control + Lists (ACLs)."; + } + + organization + "IETF NETMOD (Network Modeling) Working Group."; + + contact + "WG Web: + WG List: netmod@ietf.org + + Editor: Mahesh Jethanandani + mjethanandani@gmail.com + Editor: Lisa Huang + huangyi_99@yahoo.com + Editor: Sonal Agarwal + sagarwal12@gmail.com + Editor: Dana Blair + dana@blairhome.com"; + + description + "This YANG module defines groupings that are used by + the ietf-access-control-list YANG module. Their usage + is not limited to ietf-access-control-list and can be + used anywhere as applicable. + + Copyright (c) 2019 IETF Trust and the persons identified as + the document authors. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject + to the license terms contained in, the Simplified BSD + License set forth in Section 4.c of the IETF Trust's Legal + Provisions Relating to IETF Documents + (http://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC 8519; see + the RFC itself for full legal notices."; + + revision 2019-03-04 { + description + "Initial version."; + reference + "RFC 8519: YANG Data Model for Network Access Control + Lists (ACLs)."; + } + + /* + * Typedefs + */ + typedef operator { + type enumeration { + enum lte { + description + "Less than or equal to."; + } + enum gte { + description + "Greater than or equal to."; + } + enum eq { + description + "Equal to."; + } + enum neq { + description + "Not equal to."; + } + } + description + "The source and destination port range definitions + can be further qualified using an operator. An + operator is needed only if the lower-port is specified + and the upper-port is not specified. The operator + therefore further qualifies the lower-port only."; + } + + /* + * Groupings + */ + grouping port-range-or-operator { + choice port-range-or-operator { + case range { + leaf lower-port { + type inet:port-number; + must '. <= ../upper-port' { + error-message + "The lower-port must be less than or equal to + the upper-port."; + } + mandatory true; + description + "Lower boundary for a port."; + } + leaf upper-port { + type inet:port-number; + mandatory true; + description + "Upper boundary for a port."; + } + } + case operator { + leaf operator { + type operator; + default "eq"; + description + "Operator to be applied on the port below."; + } + leaf port { + type inet:port-number; + mandatory true; + description + "Port number along with the operator on which to + match."; + } + } + description + "Choice of specifying a port range or a single + port along with an operator."; + } + description + "Grouping for port definitions in the form of a + choice statement."; + } + + grouping acl-ip-header-fields { + description + "IP header fields common to IPv4 and IPv6"; + reference + "RFC 791: Internet Protocol."; + + leaf dscp { + type inet:dscp; + description + "Differentiated Services Code Point."; + reference + "RFC 2474: Definition of the Differentiated Services + Field (DS Field) in the IPv4 and IPv6 + Headers."; + } + + leaf ecn { + type uint8 { + range "0..3"; + } + description + "Explicit Congestion Notification."; + reference + "RFC 3168: The Addition of Explicit Congestion + Notification (ECN) to IP."; + } + + leaf length { + type uint16; + description + "In the IPv4 header field, this field is known as the Total + Length. Total Length is the length of the datagram, measured + in octets, including internet header and data. + + In the IPv6 header field, this field is known as the Payload + Length, which is the length of the IPv6 payload, i.e., the rest + of the packet following the IPv6 header, in octets."; + reference + "RFC 791: Internet Protocol + RFC 8200: Internet Protocol, Version 6 (IPv6) Specification."; + } + leaf ttl { + type uint8; + description + "This field indicates the maximum time the datagram is allowed + to remain in the internet system. If this field contains the + value zero, then the datagram must be dropped. + + In IPv6, this field is known as the Hop Limit."; + reference + "RFC 791: Internet Protocol + RFC 8200: Internet Protocol, Version 6 (IPv6) Specification."; + } + leaf protocol { + type uint8; + description + "Internet Protocol number. Refers to the protocol of the + payload. In IPv6, this field is known as 'next-header', + and if extension headers are present, the protocol is + present in the 'upper-layer' header."; + reference + "RFC 791: Internet Protocol + RFC 8200: Internet Protocol, Version 6 (IPv6) Specification."; + } + } + + grouping acl-ipv4-header-fields { + description + "Fields in the IPv4 header."; + leaf ihl { + type uint8 { + range "5..60"; + } + description + "In an IPv4 header field, the Internet Header Length (IHL) is + the length of the internet header in 32-bit words and + thus points to the beginning of the data. Note that the + minimum value for a correct header is 5."; + } + leaf flags { + type bits { + bit reserved { + position 0; + description + "Reserved. Must be zero."; + } + bit fragment { + position 1; + description + "Setting the value to 0 indicates may fragment, while + setting the value to 1 indicates do not fragment."; + } + bit more { + position 2; + description + "Setting the value to 0 indicates this is the last fragment, + and setting the value to 1 indicates more fragments are + coming."; + } + } + description + "Bit definitions for the Flags field in the IPv4 header."; + } + leaf offset { + type uint16 { + range "20..65535"; + } + description + "The fragment offset is measured in units of 8 octets (64 bits). + The first fragment has offset zero. The length is 13 bits"; + } + leaf identification { + type uint16; + description + "An identifying value assigned by the sender to aid in + assembling the fragments of a datagram."; + } + + choice destination-network { + case destination-ipv4-network { + leaf destination-ipv4-network { + type inet:ipv4-prefix; + description + "Destination IPv4 address prefix."; + } + } + description + "Choice of specifying a destination IPv4 address or + referring to a group of IPv4 destination addresses."; + } + + choice source-network { + case source-ipv4-network { + leaf source-ipv4-network { + type inet:ipv4-prefix; + description + "Source IPv4 address prefix."; + } + } + description + "Choice of specifying a source IPv4 address or + referring to a group of IPv4 source addresses."; + } + } + + grouping acl-ipv6-header-fields { + description + "Fields in the IPv6 header."; + + choice destination-network { + case destination-ipv6-network { + leaf destination-ipv6-network { + type inet:ipv6-prefix; + description + "Destination IPv6 address prefix."; + } + } + description + "Choice of specifying a destination IPv6 address + or referring to a group of IPv6 destination + addresses."; + } + + choice source-network { + case source-ipv6-network { + leaf source-ipv6-network { + type inet:ipv6-prefix; + description + "Source IPv6 address prefix."; + } + } + description + "Choice of specifying a source IPv6 address or + referring to a group of IPv6 source addresses."; + } + + leaf flow-label { + type inet:ipv6-flow-label; + description + "IPv6 Flow label."; + } + reference + "RFC 4291: IP Version 6 Addressing Architecture + RFC 4007: IPv6 Scoped Address Architecture + RFC 5952: A Recommendation for IPv6 Address Text + Representation."; + } + + grouping acl-eth-header-fields { + description + "Fields in the Ethernet header."; + leaf destination-mac-address { + type yang:mac-address; + description + "Destination IEEE 802 Media Access Control (MAC) + address."; + } + leaf destination-mac-address-mask { + type yang:mac-address; + description + "Destination IEEE 802 MAC address mask."; + } + leaf source-mac-address { + type yang:mac-address; + description + "Source IEEE 802 MAC address."; + } + leaf source-mac-address-mask { + type yang:mac-address; + description + "Source IEEE 802 MAC address mask."; + } + leaf ethertype { + type eth:ethertype; + description + "The Ethernet Type (or Length) value represented + in the canonical order defined by IEEE 802. + The canonical representation uses lowercase + characters."; + reference + "IEEE 802-2014, Clause 9.2."; + } + reference + "IEEE 802: IEEE Standard for Local and Metropolitan + Area Networks: Overview and Architecture."; + } + + grouping acl-tcp-header-fields { + description + "Collection of TCP header fields that can be used to + set up a match filter."; + leaf sequence-number { + type uint32; + description + "Sequence number that appears in the packet."; + } + leaf acknowledgement-number { + type uint32; + description + "The acknowledgement number that appears in the + packet."; + } + leaf data-offset { + type uint8 { + range "5..15"; + } + description + "Specifies the size of the TCP header in 32-bit + words. The minimum size header is 5 words and + the maximum is 15 words; thus, this gives a + minimum size of 20 bytes and a maximum of 60 + bytes, allowing for up to 40 bytes of options + in the header."; + } + leaf reserved { + type uint8; + description + "Reserved for future use."; + } + leaf flags { + type bits { + bit cwr { + position 1; + description + "The Congestion Window Reduced (CWR) flag is set + by the sending host to indicate that it received + a TCP segment with the ECN-Echo (ECE) flag set + and had responded in the congestion control + mechanism."; + reference + "RFC 3168: The Addition of Explicit Congestion + Notification (ECN) to IP."; + } + bit ece { + position 2; + description + "ECN-Echo has a dual role, depending on the value + of the SYN flag. It indicates the following: if + the SYN flag is set (1), the TCP peer is ECN + capable, and if the SYN flag is clear (0), a packet + with the Congestion Experienced flag set (ECN=11) + in the IP header was received during normal + transmission (added to the header by RFC 3168). + This serves as an indication of network congestion + (or impending congestion) to the TCP sender."; + reference + "RFC 3168: The Addition of Explicit Congestion + Notification (ECN) to IP."; + } + bit urg { + position 3; + description + "Indicates that the Urgent Pointer field is significant."; + } + bit ack { + position 4; + description + "Indicates that the Acknowledgement field is significant. + All packets after the initial SYN packet sent by the + client should have this flag set."; + } + bit psh { + position 5; + description + "Push function. Asks to push the buffered data to the + receiving application."; + } + bit rst { + position 6; + description + "Reset the connection."; + } + bit syn { + position 7; + description + "Synchronize sequence numbers. Only the first packet + sent from each end should have this flag set. Some + other flags and fields change meaning based on this + flag, and some are only valid for when it is set, + and others when it is clear."; + } + bit fin { + position 8; + description + "Last package from the sender."; + } + } + description + "Also known as Control Bits. Contains nine 1-bit flags."; + reference + "RFC 793: Transmission Control Protocol."; + } + leaf window-size { + type uint16; + units "bytes"; + description + "The size of the receive window, which specifies + the number of window size units beyond the segment + identified by the sequence number in the Acknowledgement + field that the sender of this segment is currently + willing to receive."; + } + leaf urgent-pointer { + type uint16; + description + "This field is an offset from the sequence number + indicating the last urgent data byte."; + } + leaf options { + type binary { + length "1..40"; + } + description + "The length of this field is determined by the + Data Offset field. Options have up to three + fields: Option-Kind (1 byte), Option-Length + (1 byte), and Option-Data (variable). The Option-Kind + field indicates the type of option and is the + only field that is not optional. Depending on + what kind of option we are dealing with, + the next two fields may be set: the Option-Length + field indicates the total length of the option, + and the Option-Data field contains the value of + the option, if applicable."; + } + } + + grouping acl-udp-header-fields { + description + "Collection of UDP header fields that can be used + to set up a match filter."; + leaf length { + type uint16; + description + "A field that specifies the length in bytes of + the UDP header and UDP data. The minimum + length is 8 bytes because that is the length of + the header. The field size sets a theoretical + limit of 65,535 bytes (8-byte header plus 65,527 + bytes of data) for a UDP datagram. However, the + actual limit for the data length, which is + imposed by the underlying IPv4 protocol, is + 65,507 bytes (65,535 minus 8-byte UDP header + minus 20-byte IP header). + + In IPv6 jumbograms, it is possible to have + UDP packets of a size greater than 65,535 bytes. + RFC 2675 specifies that the Length field is set + to zero if the length of the UDP header plus + UDP data is greater than 65,535."; + } + } + + grouping acl-icmp-header-fields { + description + "Collection of ICMP header fields that can be + used to set up a match filter."; + leaf type { + type uint8; + description + "Also known as control messages."; + reference + "RFC 792: Internet Control Message Protocol + RFC 4443: Internet Control Message Protocol (ICMPv6) + for Internet Protocol Version 6 (IPv6) + Specification."; + } + leaf code { + type uint8; + description + "ICMP subtype. Also known as control messages."; + reference + "RFC 792: Internet Control Message Protocol + RFC 4443: Internet Control Message Protocol (ICMPv6) + for Internet Protocol Version 6 (IPv6) + Specification."; + } + leaf rest-of-header { + type binary; + description + "Unbounded in length, the contents vary based on the + ICMP type and code. Also referred to as 'Message Body' + in ICMPv6."; + reference + "RFC 792: Internet Control Message Protocol + RFC 4443: Internet Control Message Protocol (ICMPv6) + for Internet Protocol Version 6 (IPv6) + Specification."; + } + } +} diff --git a/src/confd/yang/confd/infix-ntp.yang b/src/confd/yang/confd/infix-ntp.yang new file mode 100644 index 000000000..d3576b85b --- /dev/null +++ b/src/confd/yang/confd/infix-ntp.yang @@ -0,0 +1,171 @@ +module infix-ntp { + yang-version 1.1; + namespace "urn:infix:ntp:ns:yang:1.0"; + prefix infix-ntp; + + import ietf-ntp { + prefix ntp; + reference + "RFC 9249: A YANG Data Model for NTP"; + } + + import ietf-inet-types { + prefix inet; + reference + "RFC 6991: Common YANG Data Types"; + } + + organization "KernelKit"; + contact "kernelkit@googlegroups.com"; + description "Infix deviations and augments to ietf-ntp."; + + revision 2025-12-03 { + description + "Initial release - NTP server support. + + Implements NTP server functionality using chronyd as the + underlying daemon. Supports standalone, server, and peer modes + with configurable poll intervals, makestep for fast initial sync, + and rtcsync for hardware RTC synchronization. Mutual exclusion + with ietf-system:ntp enforced via YANG when statement."; + reference "internal"; + } + + /* + * Augments for Infix-specific features + */ + + augment "/ntp:ntp" { + container makestep { + presence "Enable clock stepping for large offsets"; + description + "Allow system clock to step (jump) immediately when offset is large. + This is useful for fast initial synchronization on systems that boot + with incorrect time (e.g., epoch time when no RTC present)."; + + leaf threshold { + type decimal64 { + fraction-digits 1; + range "0.1..max"; + } + default "1.0"; + units "seconds"; + description + "Threshold in seconds. If clock offset exceeds this value, + step the clock instead of slewing it slowly."; + } + + leaf limit { + type int32 { + range "-1 | 0..max"; + } + default "3"; + description + "Number of clock updates during which stepping is allowed. + After this many updates, only slewing is used. Special values: + -1 = always allow stepping (not recommended) + 0 = never step (defeats purpose of this directive) + 1-N = allow stepping for first N updates (recommended: 3)"; + } + } + } + + /* + * Additional operational state fields from chronyd + * that are not part of the standard ietf-ntp model + */ + augment "/ntp:ntp/ntp:clock-state/ntp:system-status" { + leaf last-offset { + type decimal64 { + fraction-digits 9; + } + config false; + units "seconds"; + description + "Estimated offset of the last clock update."; + } + + leaf rms-offset { + type decimal64 { + fraction-digits 9; + } + config false; + units "seconds"; + description + "Long-term average of the offset value."; + } + + leaf residual-freq { + type decimal64 { + fraction-digits 3; + } + config false; + units "ppm"; + description + "Residual frequency indicating how much the frequency has + changed relative to the previous value."; + } + + leaf skew { + type decimal64 { + fraction-digits 3; + } + config false; + units "ppm"; + description + "Estimated error bound on the frequency."; + } + + leaf update-interval { + type decimal64 { + fraction-digits 1; + } + config false; + units "seconds"; + description + "Interval between the last two clock updates."; + } + } + + /* + * Deviations for unsupported features + */ + + deviation "/ntp:ntp/ntp:access-rules" { + deviate not-supported; + } + + deviation "/ntp:statistics-reset" { + deviate not-supported; + } + + deviation "/ntp:ntp/ntp:interfaces" { + deviate not-supported; + } + + deviation "/ntp:ntp/ntp:unicast-configuration/ntp:address" { + deviate replace { + type inet:host; + } + } + + deviation "/ntp:ntp/ntp:unicast-configuration/ntp:iburst" { + deviate add { + must "not(ntp:iburst and ../type = 'ntp:uc-peer')" { + error-message "iburst option is not supported in peer mode"; + } + } + } + + deviation "/ntp:ntp/ntp:unicast-configuration/ntp:burst" { + deviate add { + must "not(ntp:burst and ../type = 'ntp:uc-peer')" { + error-message "burst option is not supported in peer mode"; + } + } + } + + deviation "/ntp:ntp/ntp:unicast-configuration/ntp:source" { + deviate not-supported; + } +} diff --git a/src/confd/yang/confd/infix-ntp@2025-12-03.yang b/src/confd/yang/confd/infix-ntp@2025-12-03.yang new file mode 120000 index 000000000..8bcf0feb5 --- /dev/null +++ b/src/confd/yang/confd/infix-ntp@2025-12-03.yang @@ -0,0 +1 @@ +infix-ntp.yang \ No newline at end of file diff --git a/src/klish-plugin-infix/xml/infix.xml b/src/klish-plugin-infix/xml/infix.xml index bb3d6a1ea..4a4317c1b 100644 --- a/src/klish-plugin-infix/xml/infix.xml +++ b/src/klish-plugin-infix/xml/infix.xml @@ -324,15 +324,19 @@ - - - show ntp - - + + + + + + + show ntp source $KLISH_PARAM_address + - doas ntp tracking + show ntp tracking + show ntp diff --git a/src/show/show.py b/src/show/show.py index 2ec78ba3d..e0fd57a96 100755 --- a/src/show/show.py +++ b/src/show/show.py @@ -69,16 +69,85 @@ def hardware(args: List[str]) -> None: cli_pretty(data, "show-hardware") def ntp(args: List[str]) -> None: - data = run_sysrepocfg("/system-state/ntp") - if not data: - print("No ntp data retrieved.") + # Create argument parser for ntp subcommands + parser = argparse.ArgumentParser(prog='show ntp', add_help=False) + + # Add subparsers for source and tracking + subparsers = parser.add_subparsers(dest='subcommand', help='NTP subcommands') + + # source subcommand + source_parser = subparsers.add_parser('source', help='Show NTP source(s)') + source_parser.add_argument('address', nargs='?', help='Show details for specific source address') + + # tracking subcommand + tracking_parser = subparsers.add_parser('tracking', help='Show NTP tracking status') + + # Parse the arguments + try: + parsed_args = parser.parse_args(args) + except SystemExit: + # argparse calls sys.exit on error, catch it return + # Dispatch to appropriate handler + if parsed_args.subcommand == 'source': + return ntp_source([parsed_args.address] if parsed_args.address else []) + elif parsed_args.subcommand == 'tracking': + return ntp_tracking([]) + + # Default: show ntp (no subcommand or address) + # Fetch both client and server operational data + client_data = run_sysrepocfg("/system-state/ntp") + server_data = run_sysrepocfg("/ietf-ntp:ntp") + + # Merge into single data structure + data = {} + if client_data: + data.update(client_data) + if server_data: + data.update(server_data) + if RAW_OUTPUT: + if not data: + print("No ntp data retrieved.") + return print(json.dumps(data, indent=2)) return + + # Always call cli_pretty, even with empty data, to show proper message + if not data: + data = {} + + # Default show ntp - summary view (no address support at top level) cli_pretty(data, "show-ntp") +def ntp_tracking(args: List[str]) -> None: + data = run_sysrepocfg("/ietf-ntp:ntp") + if not data: + print("No ntp server data retrieved.") + return + + if RAW_OUTPUT: + print(json.dumps(data, indent=2)) + return + cli_pretty(data, "show-ntp-tracking") + +def ntp_source(args: List[str]) -> None: + data = run_sysrepocfg("/ietf-ntp:ntp") + if not data: + print("No ntp server data retrieved.") + return + + if RAW_OUTPUT: + print(json.dumps(data, indent=2)) + return + + # Pass address argument if provided + if len(args) > 0 and args[0]: + cli_pretty(data, "show-ntp-source", "-a", args[0]) + else: + cli_pretty(data, "show-ntp-source") + def is_valid_interface_name(interface_name: str) -> bool: """ Validates a Linux network interface name. diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index 7405e70c4..80e3379e1 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -2387,24 +2387,461 @@ def show_container_detail(json, name): print(f"CPU Usage : {cpu_usage}%") -def show_ntp(json): - if not json.get("ietf-system:system-state"): - print("NTP client not enabled.") +def show_ntp_source_detail_single(source, is_association=False): + """Display detailed information for a single NTP source (auto-detected single source)""" + print(f"{'Server address':<20}: {source.get('address', 'N/A')}") + + # State + if is_association: + prefer = source.get("prefer", False) + state_desc = "Selected sync source" if prefer else "Candidate" + else: + state = source.get('state', 'unknown') + state_desc = { + 'selected': 'Selected sync source', + 'candidate': 'Candidate', + 'unreach': 'Unreachable', + 'not-combined': 'Not combined' + }.get(state, state) + print(f"{'State':<20}: {state_desc}") + + # Stratum + stratum = source.get('stratum') + if stratum is not None: + print(f"{'Stratum':<20}: {stratum}") + + # Poll interval + poll = source.get('poll') + if poll is not None: + poll_seconds = 2 ** poll + print(f"{'Poll interval':<20}: {poll} (2^{poll} seconds = {poll_seconds}s)") + +def show_ntp(json, address=None): + """Unified NTP status display for both client and server modes""" + ntp_data = json.get("ietf-ntp:ntp", {}) + port = ntp_data.get("port") + is_server = port is not None + + sources = [] + if is_server: + associations = ntp_data.get("associations", {}).get("association", []) + sources = associations + else: + system_state = json.get("ietf-system:system-state", {}) + if system_state: + sources = get_json_data({}, json, 'ietf-system:system-state', 'infix-system:ntp', 'sources', 'source') + + if address: + matching = [s for s in sources if s.get('address') == address] + if not matching: + print(f"No NTP source found with address: {address}") + return + if is_server: + show_ntp_association_detail(matching[0]) + else: + show_ntp_source_detail_single(matching[0], False) + return + + if is_server: + if sources: + print(f"{'Mode':<20}: Relay (no local reference clock)") + else: + print(f"{'Mode':<20}: Server (local reference clock)") + print(f"{'Port':<20}: {port}") + + # Show operational stratum + refclock = ntp_data.get("refclock-master") + if refclock: + stratum = refclock.get("master-stratum") + if stratum is not None: + print(f"{'Stratum':<20}: {stratum}") + + # Show reference time + clock_state = ntp_data.get("clock-state", {}).get("system-status", {}) + ref_time = clock_state.get("reference-time") + if ref_time: + from datetime import datetime + try: + dt = datetime.fromisoformat(ref_time.replace("Z", "+00:00")) + ref_time_str = dt.strftime("%a %b %d %H:%M:%S %Y") + print(f"{'Ref time (UTC)':<20}: {ref_time_str}") + except (ValueError, AttributeError): + pass + + interfaces = ntp_data.get("interfaces", {}).get("interface", []) + if interfaces: + print(f"{'Interfaces':<20}: {', '.join([iface.get('name', '?') for iface in interfaces])}") + else: + print(f"{'Interfaces':<20}: All") + + stats = ntp_data.get("ntp-statistics") + if stats: + print(f"{'Packets Received':<20}: {stats.get('packet-received', 0):,}") + print(f"{'Packets Sent':<20}: {stats.get('packet-sent', 0):,}") + print(f"{'Packets Dropped':<20}: {stats.get('packet-dropped', 0):,}") + print(f"{'Send Failures':<20}: {stats.get('packet-sent-fail', 0):,}") + else: + print(f"{'Mode':<20}: Client") + + # Show local clock state in client mode + clock_state = ntp_data.get("clock-state", {}).get("system-status", {}) + + # Show local operational stratum + stratum = clock_state.get("clock-stratum") + if stratum is not None: + print(f"{'Stratum':<20}: {stratum}") + + # Show reference time + ref_time = clock_state.get("reference-time") + if ref_time: + from datetime import datetime + try: + dt = datetime.fromisoformat(ref_time.replace("Z", "+00:00")) + ref_time_str = dt.strftime("%a %b %d %H:%M:%S %Y") + print(f"{'Ref time (UTC)':<20}: {ref_time_str}") + except (ValueError, AttributeError): + pass + + if len(sources) == 0: + return + if len(sources) == 1: + show_ntp_source_detail_single(sources[0], is_server) return - hdr = (f"{'ADDRESS':<{PadNtpSource.address}}" - f"{'MODE':<{PadNtpSource.mode}}" - f"{'STATE':<{PadNtpSource.state}}" - f"{'STRATUM':>{PadNtpSource.stratum}}" - f"{'POLL-INTERVAL':>{PadNtpSource.poll}}" - ) + print() + hdr = (f"{'ADDRESS':<{PadNtpSource.address}}" + f"{'MODE':<{PadNtpSource.mode}}" + f"{'STATE':<{PadNtpSource.state}}" + f"{'STRATUM':>{PadNtpSource.stratum}}" + f"{'POLL':>{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}}" + # Extract fields - handle both association and ietf-system format + address = source.get('address', 'N/A') + + if is_server: + # Association format + local_mode = source.get("local-mode", "") + if ":" in local_mode: + local_mode = local_mode.split(":")[-1] + mode_str = local_mode + prefer = source.get("prefer", False) + state_str = "selected" if prefer else "candidate" + else: + # ietf-system format + mode_str = source.get('mode', 'unknown') + state_str = source.get('state', 'unknown') + if state_str == 'not-combined': + state_str = 'not combined' + + stratum = source.get('stratum', 0) + poll = source.get('poll', 0) + poll_str = f"{2**poll}s" if poll else "-" + + row = f"{address:<{PadNtpSource.address}}" + row += f"{mode_str:<{PadNtpSource.mode}}" + row += f"{state_str:<{PadNtpSource.state}}" + row += f"{stratum:>{PadNtpSource.stratum}}" + row += f"{poll_str:>{PadNtpSource.poll}}" + print(row) + + +def show_ntp_tracking(json): + """Display NTP clock tracking state using YANG operational data""" + ntp_data = json.get("ietf-ntp:ntp") + if not ntp_data: + print("NTP server not enabled.") + return + + clock_state = ntp_data.get("clock-state", {}).get("system-status", {}) + if not clock_state: + print("No clock state data available.") + return + + # Reference ID + refid = clock_state.get("clock-refid", "N/A") + print(f"{'Reference ID':<20}: {refid}") + + # Stratum + stratum = clock_state.get("clock-stratum", 16) + print(f"{'Stratum':<20}: {stratum}") + + # Reference time (show epoch if not set) + ref_time = clock_state.get("reference-time") + if ref_time: + from datetime import datetime + try: + dt = datetime.fromisoformat(ref_time.replace("Z", "+00:00")) + ref_time_str = dt.strftime("%a %b %d %H:%M:%S %Y") + except (ValueError, AttributeError): + ref_time_str = ref_time + else: + ref_time_str = "Thu Jan 01 00:00:00 1970" + print(f"{'Ref time (UTC)':<20}: {ref_time_str}") + + # System time offset (3 fraction-digits in ms = microsecond precision) + offset = clock_state.get("clock-offset") + if offset is not None: + offset_sec = float(offset) / 1000.0 + sign = "slow" if offset_sec >= 0 else "fast" + print(f"{'System time':<20}: {abs(offset_sec):.6f} seconds {sign} of NTP time") + + # Last offset (infix-ntp augment) + last_offset = clock_state.get("infix-ntp:last-offset") + if last_offset is not None: + last_offset_sec = float(last_offset) + sign = "+" if last_offset_sec >= 0 else "" + print(f"{'Last offset':<20}: {sign}{last_offset_sec:.9f} seconds") + else: + print(f"{'Last offset':<20}: N/A") + + # RMS offset (infix-ntp augment) + rms_offset = clock_state.get("infix-ntp:rms-offset") + if rms_offset is not None: + print(f"{'RMS offset':<20}: {float(rms_offset):.9f} seconds") + else: + print(f"{'RMS offset':<20}: N/A") + + # Frequency (convert from Hz difference to ppm) + nominal_freq = clock_state.get("nominal-freq") + actual_freq = clock_state.get("actual-freq") + if nominal_freq and actual_freq: + freq_diff_hz = float(actual_freq) - float(nominal_freq) + freq_ppm = (freq_diff_hz / float(nominal_freq)) * 1000000.0 + if freq_ppm == 0.0: + direction = "slow" + else: + direction = "slow" if freq_ppm < 0 else "fast" + print(f"{'Frequency':<20}: {abs(freq_ppm):.3f} ppm {direction}") + + # Residual freq (infix-ntp augment) + residual_freq = clock_state.get("infix-ntp:residual-freq") + if residual_freq is not None: + residual_freq_val = float(residual_freq) + sign = "+" if residual_freq_val >= 0 else "" + print(f"{'Residual freq':<20}: {sign}{residual_freq_val:.3f} ppm") + else: + print(f"{'Residual freq':<20}: N/A") + + # Skew (infix-ntp augment) + skew = clock_state.get("infix-ntp:skew") + if skew is not None: + print(f"{'Skew':<20}: {float(skew):.3f} ppm") + else: + print(f"{'Skew':<20}: N/A") + + # Root delay (3 fraction-digits in ms = microsecond precision) + root_delay = clock_state.get("root-delay") + if root_delay is not None: + root_delay_sec = float(root_delay) / 1000.0 + print(f"{'Root delay':<20}: {root_delay_sec:.6f} seconds") + + # Root dispersion (3 fraction-digits in ms = microsecond precision) + root_disp = clock_state.get("root-dispersion") + if root_disp is not None: + root_disp_sec = float(root_disp) / 1000.0 + print(f"{'Root dispersion':<20}: {root_disp_sec:.6f} seconds") + + # Update interval (infix-ntp augment) + update_interval = clock_state.get("infix-ntp:update-interval") + if update_interval is not None: + print(f"{'Update interval':<20}: {float(update_interval):.1f} seconds") + else: + print(f"{'Update interval':<20}: N/A") + + # Leap status + sync_state = clock_state.get("sync-state", "") + if "clock-synchronized" in sync_state: + leap_status = "Normal" + elif "clock-never-set" in sync_state: + leap_status = "Not synchronised" + else: + leap_status = "Unknown" + print(f"{'Leap status':<20}: {leap_status}") + + +def show_ntp_association_detail(assoc): + """Display detailed information for a single NTP association""" + print(f"{'Address':<20}: {assoc.get('address', 'N/A')}") + + # Mode + local_mode = assoc.get("local-mode", "") + if ":" in local_mode: + local_mode = local_mode.split(":")[-1] + + mode_desc = { + 'client': 'Server (client mode) [^]', + 'active': 'Peer (symmetric active) [=]', + 'broadcast-client': 'Broadcast/Local refclock [#]' + }.get(local_mode, local_mode) + print(f"{'Mode':<20}: {mode_desc}") + + # State/Prefer + prefer = assoc.get("prefer", False) + state_desc = "Selected sync source [*]" if prefer else "Candidate [+]" + print(f"{'State':<20}: {state_desc}") + + # Configured + isconfigured = assoc.get("isconfigured", False) + print(f"{'Configured':<20}: {'Yes' if isconfigured else 'No (dynamic)'}") + + # Stratum + stratum = assoc.get("stratum") + if stratum is not None: + print(f"{'Stratum':<20}: {stratum}") + + # Poll interval + poll = assoc.get("poll") + if poll is not None: + print(f"{'Poll interval':<20}: {poll} (2^{poll} seconds = {2**poll}s)") + + # Reachability + reach = assoc.get("reach") + if reach is not None: + print(f"{'Reachability':<20}: {reach:03o} (octal) = {reach:08b}b") + + # Time since last packet + now = assoc.get("now") + if now is not None: + print(f"{'Last RX':<20}: {now}s ago") + + # Offset + offset = assoc.get("offset") + if offset is not None: + offset_ms = float(offset) + if abs(offset_ms) < 1.0: + offset_str = f"{offset_ms * 1000.0:+.1f}us ({offset_ms:+.6f}ms)" + else: + offset_str = f"{offset_ms:+.3f}ms ({offset_ms / 1000.0:+.6f}s)" + print(f"{'Offset':<20}: {offset_str}") + + # Delay + delay = assoc.get("delay") + if delay is not None: + delay_ms = float(delay) + if abs(delay_ms) < 1.0: + delay_str = f"{delay_ms * 1000.0:.1f}us ({delay_ms:.6f}ms)" + else: + delay_str = f"{delay_ms:.3f}ms ({delay_ms / 1000.0:.6f}s)" + print(f"{'Delay':<20}: {delay_str}") + + # Dispersion + dispersion = assoc.get("dispersion") + if dispersion is not None: + disp_ms = float(dispersion) + if abs(disp_ms) < 1.0: + disp_str = f"{disp_ms * 1000.0:.1f}us ({disp_ms:.6f}ms)" + else: + disp_str = f"{disp_ms:.3f}ms ({disp_ms / 1000.0:.6f}s)" + print(f"{'Dispersion':<20}: {disp_str}") + +def show_ntp_source(json, address=None): + """Display NTP associations/sources""" + ntp_data = json.get("ietf-ntp:ntp") + if not ntp_data: + print("NTP server not enabled.") + return + + associations = ntp_data.get("associations", {}).get("association", []) + if not associations: + print("No NTP associations found.") + return + + # If address specified, show detailed view for that association + if address: + matching = [a for a in associations if a.get('address') == address] + if not matching: + print(f"No NTP association found with address: {address}") + return + show_ntp_association_detail(matching[0]) + return + + # If single association, show detailed view automatically + if len(associations) == 1: + show_ntp_association_detail(associations[0]) + return + + # First pass: determine maximum address width needed + max_addr_len = len("Name/IP address") # Minimum width to match chronyc header + for assoc in associations: + addr_len = len(assoc.get("address", "")) + if addr_len > max_addr_len: + max_addr_len = addr_len + + # Cap at reasonable maximum (IPv6 can be up to 39 chars uncompressed) + max_addr_len = min(max_addr_len, 39) + + # Table header - similar to chronyc sources + hdr = f"{'MS':<3}{f'Name/IP address':<{max_addr_len}} {'Stratum':>7} {'Poll':>4} {'Reach':>5} {'LastRx':>6} {'Last sample':>24}" + print(Decore.invert(hdr)) + + # Display each association + for assoc in associations: + # State indicator: * = prefer (sync source), + = candidate + prefer = assoc.get("prefer", False) + state = "*" if prefer else "+" + + # Mode indicator + local_mode = assoc.get("local-mode", "") + if ":" in local_mode: + local_mode = local_mode.split(":")[-1] + # Map to chronyc-style mode indicators + mode_indicator = "^" # Default to server mode + if local_mode == "active": + mode_indicator = "=" + elif local_mode == "broadcast-client": + mode_indicator = "#" + + address = assoc.get("address", "N/A") + stratum = assoc.get("stratum", 0) + + # Poll interval (log2 seconds) + poll = assoc.get("poll") + poll_str = str(poll) if poll is not None else "-" + + # Reachability register (display as octal) + reach = assoc.get("reach") + if reach is not None: + reach_str = f"{reach:03o}" + else: + reach_str = "-" + + # Time since last packet (LastRx) + now = assoc.get("now") + now_str = str(now) if now is not None else "-" + + # Offset (in milliseconds, convert to microseconds for display) + offset = assoc.get("offset") + if offset is not None: + offset_ms = float(offset) + if abs(offset_ms) < 1.0: + # Show in microseconds if less than 1ms + offset_str = f"{offset_ms * 1000.0:+.0f}us" + else: + # Show in milliseconds + offset_str = f"{offset_ms:+.3f}ms" + else: + offset_str = "-" + + # Delay (in milliseconds) - show as +/- similar to chronyc + delay = assoc.get("delay") + if delay is not None: + delay_ms = float(delay) + if abs(delay_ms) < 1.0: + delay_str = f"+/- {delay_ms * 1000.0:.0f}us" + else: + delay_str = f"+/- {delay_ms:.3f}ms" + else: + delay_str = "" + + # Last sample column combines offset and delay like chronyc + if offset_str != "-": + last_sample = f"{offset_str:>12} {delay_str}" + else: + last_sample = "-" + + # Format row + ms_col = f"{mode_indicator}{state}" + row = f"{ms_col:<3}{address:<{max_addr_len}} {stratum:>7} {poll_str:>4} {reach_str:>5} {now_str:>6} {last_sample:>24}" print(row) @@ -4392,7 +4829,11 @@ def main(): subparsers.add_parser('show-firewall-log', help='Show firewall log') \ .add_argument('limit', nargs='?', help='Last N lines, default: all') - subparsers.add_parser('show-ntp', help='Show NTP sources') + subparsers.add_parser('show-ntp', help='Show NTP status') \ + .add_argument('-a', '--address', help='Show details for specific address') + subparsers.add_parser('show-ntp-tracking', help='Show NTP tracking status') + subparsers.add_parser('show-ntp-source', help='Show NTP associations/sources') \ + .add_argument('-a', '--address', help='Show details for specific source') subparsers.add_parser('show-bfd', help='Show BFD sessions') subparsers.add_parser('show-bfd-status', help='Show BFD status') @@ -4452,7 +4893,11 @@ def main(): elif args.command == "show-firewall-log": show_firewall_logs(args.limit) elif args.command == "show-ntp": - show_ntp(json_data) + show_ntp(json_data, args.address) + elif args.command == "show-ntp-tracking": + show_ntp_tracking(json_data) + elif args.command == "show-ntp-source": + show_ntp_source(json_data, args.address) elif args.command == "show-wifi-radio": show_wifi_radio(json_data) elif args.command == "show-bfd": diff --git a/src/statd/python/yanger/__main__.py b/src/statd/python/yanger/__main__.py index ddffaa09f..09b76adbf 100644 --- a/src/statd/python/yanger/__main__.py +++ b/src/statd/python/yanger/__main__.py @@ -78,6 +78,9 @@ def dirpath(path): elif args.model == 'ietf-system': from . import ietf_system yang_data = ietf_system.operational() + elif args.model == 'ietf-ntp': + from . import ietf_ntp + yang_data = ietf_ntp.operational() elif args.model == 'ieee802-dot1ab-lldp': from . import infix_lldp yang_data = infix_lldp.operational() diff --git a/src/statd/python/yanger/ietf_ntp.py b/src/statd/python/yanger/ietf_ntp.py new file mode 100644 index 000000000..cdfade9e2 --- /dev/null +++ b/src/statd/python/yanger/ietf_ntp.py @@ -0,0 +1,363 @@ +import subprocess + +from .common import insert +from .host import HOST + + +def add_ntp_associations(out): + """Add NTP association information from chronyc sources and sourcestats""" + try: + # Get basic source information + sources_data = HOST.run_multiline(["chronyc", "-c", "sources"], []) + if not sources_data: + return + + # Get statistical information (offset, dispersion) + stats_data = HOST.run_multiline(["chronyc", "-c", "sourcestats"], []) + + # Build a map of address -> stats for quick lookup + stats_map = {} + if stats_data: + for line in stats_data: + parts = line.split(',') + if len(parts) >= 8: + address = parts[0] + stats_map[address] = { + "offset": parts[6], # Estimated offset in seconds + "std_dev": parts[7] # Standard deviation in seconds + } + + associations = [] + # Map chronyd mode indicators to ietf-ntp association-mode identities + mode_map = { + "^": "ietf-ntp:client", # We're client to this server + "=": "ietf-ntp:active", # Peer mode (symmetric active) + "#": "ietf-ntp:broadcast-client" # Local refclock (closest match) + } + + # chronyc -c sources format: + # [0]=Mode, [1]=State, [2]=Address, [3]=Stratum, [4]=Poll, [5]=Reach, + # [6]=LastRx, [7]=LastOffset, [8]=OffsetAtLastUpdate, [9]=Error + for line in sources_data: + parts = line.split(',') + if len(parts) < 10: + continue + + mode_indicator = parts[0] + state_indicator = parts[1] + address = parts[2] + stratum = int(parts[3]) + + # Skip sources with invalid stratum (0 means unreachable/not yet synced) + # YANG model requires stratum to be in range 1..16 + if stratum < 1 or stratum > 16: + continue + + assoc = {} + assoc["address"] = address + assoc["local-mode"] = mode_map.get(mode_indicator, "ietf-ntp:client") + assoc["isconfigured"] = True + assoc["stratum"] = stratum + + # Prefer indicator: * means current sync source + if state_indicator == "*": + assoc["prefer"] = True + + # Reachability register (octal string to decimal uint8) + try: + reach_octal = parts[5] + assoc["reach"] = int(reach_octal, 8) + except (ValueError, IndexError): + pass + + # Poll interval (already in log2 seconds) + try: + assoc["poll"] = int(parts[4]) + except (ValueError, IndexError): + pass + + # Time since last packet (now) + try: + assoc["now"] = int(parts[6]) + except (ValueError, IndexError): + pass + + # Offset: prefer sourcestats data if available, otherwise use sources + # Convert from seconds to milliseconds with 3 fraction-digits + try: + if address in stats_map: + offset_sec = float(stats_map[address]["offset"]) + else: + # Use last offset from sources output (parts[7]) + offset_sec = float(parts[7]) + assoc["offset"] = f"{offset_sec * 1000.0:.3f}" + except (ValueError, IndexError): + pass + + # Delay: use error estimate from sources (parts[9]) + # Convert from seconds to milliseconds with 3 fraction-digits + try: + delay_sec = float(parts[9]) + # chronyd reports this as error bound, use absolute value + assoc["delay"] = f"{abs(delay_sec) * 1000.0:.3f}" + except (ValueError, IndexError): + pass + + # Dispersion: use standard deviation from sourcestats + # Convert from seconds to milliseconds with 3 fraction-digits + try: + if address in stats_map: + disp_sec = float(stats_map[address]["std_dev"]) + assoc["dispersion"] = f"{disp_sec * 1000.0:.3f}" + except (ValueError, IndexError): + pass + + associations.append(assoc) + + if associations: + insert(out, "ietf-ntp:ntp", "associations", "association", associations) + except Exception: + # NTP not running or no sources configured, silently skip + pass + + +def add_ntp_clock_state(out): + """Add NTP clock state from chronyc tracking""" + try: + data = HOST.run_multiline(["chronyc", "-c", "tracking"], []) + if not data or len(data) == 0: + return + + # Parse tracking output (CSV format) + # Format: Ref-ID(IP),Ref-ID(name),Stratum,Ref-time,System-time,Last-offset, + # RMS-offset,Frequency,Residual-freq,Skew,Root-delay,Root-dispersion, + # Update-interval,Leap-status + parts = data[0].split(',') + if len(parts) < 14: + return + + clock_state = {} + system_status = {} + + # chronyd uses stratum 0 for "not synchronized", YANG requires 1-16 + stratum_raw = int(parts[2]) + stratum = 16 if stratum_raw == 0 else stratum_raw + + if stratum == 16: + system_status["clock-state"] = "ietf-ntp:unsynchronized" + else: + system_status["clock-state"] = "ietf-ntp:synchronized" + + system_status["clock-stratum"] = stratum + + # Convert hex Ref-ID to IPv4 dotted notation + # "00000000" -> "0.0.0.0", "7F7F0101" -> "127.127.1.1" + refid_ip = parts[0] + refid_name = parts[1] + + if refid_name: + system_status["clock-refid"] = refid_name + elif refid_ip and len(refid_ip) == 8: + try: + a = int(refid_ip[0:2], 16) + b = int(refid_ip[2:4], 16) + c = int(refid_ip[4:6], 16) + d = int(refid_ip[6:8], 16) + system_status["clock-refid"] = f"{a}.{b}.{c}.{d}" + except ValueError: + system_status["clock-refid"] = refid_ip if refid_ip else "0.0.0.0" + else: + system_status["clock-refid"] = refid_ip if refid_ip else "0.0.0.0" + + # Add clock frequencies (in Hz) + # chronyd reports frequency offset in ppm, need to convert to Hz + # Nominal frequency is typically 1000000000 Hz for system clock + # Format with fraction-digits 4 to avoid scientific notation + try: + freq_ppm = float(parts[7]) + nominal = 1000000000.0 + actual = nominal * (1.0 + freq_ppm / 1000000.0) + system_status["nominal-freq"] = f"{nominal:.4f}" + system_status["actual-freq"] = f"{actual:.4f}" + except (ValueError, IndexError): + pass + + # Clock precision (use skew as approximation, converted to log2 seconds) + # chronyd reports skew in ppm, we'll use a fixed precision value + # Most systems have precision around -6 to -20 (2^-6 to 2^-20 seconds) + system_status["clock-precision"] = -20 # ~1 microsecond precision + + # Clock offset (System-time column, already in seconds) + # Convert to milliseconds with fraction-digits 3 to match YANG + try: + offset_sec = float(parts[4]) + system_status["clock-offset"] = f"{offset_sec * 1000.0:.3f}" + except (ValueError, IndexError): + pass + + # Root delay (in seconds, convert to milliseconds) + # Format with fraction-digits 3 to match YANG + try: + root_delay_sec = float(parts[10]) + system_status["root-delay"] = f"{root_delay_sec * 1000.0:.3f}" + except (ValueError, IndexError): + pass + + # Root dispersion (in seconds, convert to milliseconds) + # Format with fraction-digits 3 to match YANG + try: + root_disp_sec = float(parts[11]) + system_status["root-dispersion"] = f"{root_disp_sec * 1000.0:.3f}" + except (ValueError, IndexError): + pass + + # Reference time (Ref-time in seconds since epoch) + # YANG expects ntp-date-and-time format, but we'll provide Unix timestamp + try: + ref_time = float(parts[3]) + if ref_time > 0: + # Convert to ISO 8601 timestamp + from datetime import datetime + dt = datetime.utcfromtimestamp(ref_time) + system_status["reference-time"] = dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")[:-4] + "Z" + except (ValueError, IndexError, OSError): + pass + + # Sync state based on leap status + # chronyd leap status: 0=Normal, 1=Insert second, 2=Delete second, 3=Not synchronized + try: + leap_status_str = parts[13].strip() + if leap_status_str == "Not synchronised" or stratum == 16: + system_status["sync-state"] = "ietf-ntp:clock-never-set" + else: + system_status["sync-state"] = "ietf-ntp:clock-synchronized" + except (ValueError, IndexError): + # Default based on stratum + if stratum == 16: + system_status["sync-state"] = "ietf-ntp:clock-never-set" + else: + system_status["sync-state"] = "ietf-ntp:clock-synchronized" + + # Infix-specific augments: additional chronyd operational data + # Last offset (parts[5], in seconds, 9 fraction-digits) + try: + last_offset = float(parts[5]) + system_status["infix-ntp:last-offset"] = f"{last_offset:.9f}" + except (ValueError, IndexError): + pass + + # RMS offset (parts[6], in seconds, 9 fraction-digits) + try: + rms_offset = float(parts[6]) + system_status["infix-ntp:rms-offset"] = f"{rms_offset:.9f}" + except (ValueError, IndexError): + pass + + # Residual frequency (parts[8], in ppm, 3 fraction-digits) + try: + residual_freq = float(parts[8]) + system_status["infix-ntp:residual-freq"] = f"{residual_freq:.3f}" + except (ValueError, IndexError): + pass + + # Skew (parts[9], in ppm, 3 fraction-digits) + try: + skew = float(parts[9]) + system_status["infix-ntp:skew"] = f"{skew:.3f}" + except (ValueError, IndexError): + pass + + # Update interval (parts[12], in seconds, 1 fraction-digit) + try: + update_interval = float(parts[12]) + system_status["infix-ntp:update-interval"] = f"{update_interval:.1f}" + except (ValueError, IndexError): + pass + + clock_state["system-status"] = system_status + insert(out, "ietf-ntp:ntp", "clock-state", clock_state) + except Exception: + # NTP not running, silently skip + pass + + +def add_ntp_server_status(out): + """Add NTP server operational status (port and stratum) + + Note: This must be called after add_ntp_clock_state() so we can + reuse the stratum already extracted from chronyc tracking. + """ + try: + ntp_data = out.get("ietf-ntp:ntp", {}) + clock_state = ntp_data.get("clock-state", {}) + system_status = clock_state.get("system-status", {}) + stratum = system_status.get("clock-stratum") + + if stratum is not None: + # Populate refclock-master with operational stratum + # This shows what stratum we're actually operating at + refclock = { + "master-stratum": stratum + } + insert(out, "ietf-ntp:ntp", "refclock-master", refclock) + + # Get actual listening port, excluding loopback (command port) + # UNCONN 0 0 0.0.0.0:123 0.0.0.0:* users:(("chronyd",pid=5441)) + # UNCONN 0 0 *:123 *:* users:(("chronyd",pid=5441)) + ss_lines = HOST.run_multiline(["ss", "-ulnp"], []) + for line in ss_lines: + if "chronyd" not in line: + continue + if "127.0.0.1" in line or "[::1]" in line: + continue + + parts = line.split() + if len(parts) >= 5: + local_addr = parts[3] + port_str = local_addr.split(':')[-1] + if port_str.isdigit(): + insert(out, "ietf-ntp:ntp", "port", int(port_str)) + break + + except Exception: + # NTP server not running, silently skip + pass + + +def add_ntp_server_stats(out): + """Add NTP server statistics if ietf-ntp is active""" + try: + # Get server statistics from chronyd + data = HOST.run_multiline(["chronyc", "-c", "serverstats"], []) + if not data or len(data) == 0: + return + + # Parse serverstats output (CSV format) + # Format: NTPpacketsreceived,NTPpacketsdropped,Cmdpacketsreceived, + # Cmdpacketsdropped,Clientlogsizeactive,Clientlogmemory, + # Ratelimitdrops,NTPpktsresp,NTPpktsresp-fail + parts = data[0].split(',') + if len(parts) < 9: + return + + stats = {} + stats["packet-received"] = int(parts[0]) + stats["packet-dropped"] = int(parts[1]) + stats["packet-sent"] = int(parts[7]) + stats["packet-sent-fail"] = int(parts[8]) + + insert(out, "ietf-ntp:ntp", "ntp-statistics", stats) + except Exception: + # NTP server not running or not configured, silently skip + pass + + +def operational(): + """Get operational state for ietf-ntp module""" + out = {} + add_ntp_associations(out) + add_ntp_clock_state(out) + add_ntp_server_status(out) + add_ntp_server_stats(out) + + return out diff --git a/src/statd/python/yanger/ietf_system.py b/src/statd/python/yanger/ietf_system.py index ba7aa1b0a..5cff7ed72 100644 --- a/src/statd/python/yanger/ietf_system.py +++ b/src/statd/python/yanger/ietf_system.py @@ -63,6 +63,7 @@ def add_ntp(out): insert(out, "infix-system:ntp", "sources", "source", source) + def add_dns(out): options = {} servers = [] diff --git a/src/statd/statd.c b/src/statd/statd.c index bdd0024ec..da02b7300 100644 --- a/src/statd/statd.c +++ b/src/statd/statd.c @@ -50,6 +50,7 @@ #define XPATH_DHCP_SERVER_BASE "/infix-dhcp-server:dhcp-server" #define XPATH_LLDP_BASE "/ieee802-dot1ab-lldp:lldp" #define XPATH_FIREWALL_BASE "/infix-firewall:firewall" +#define XPATH_NTP_BASE "/ietf-ntp:ntp" TAILQ_HEAD(sub_head, sub); @@ -444,6 +445,8 @@ static int subscribe_to_all(struct statd *statd) return SR_ERR_INTERNAL; if (subscribe(statd, "infix-firewall", XPATH_FIREWALL_BASE, sr_generic_cb)) return SR_ERR_INTERNAL; + if (subscribe(statd, "ietf-ntp", XPATH_NTP_BASE, sr_generic_cb)) + return SR_ERR_INTERNAL; INFO("Successfully subscribed to all models"); return SR_ERR_OK; diff --git a/test/case/statd/system/cli/show-ntp b/test/case/statd/system/cli/show-ntp index 5ed953976..46b6a4229 100644 --- a/test/case/statd/system/cli/show-ntp +++ b/test/case/statd/system/cli/show-ntp @@ -1,4 +1,6 @@ -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 +Mode : Client + +ADDRESS MODE STATE STRATUM POLL +192.168.1.1 server candidate 1 64s +192.168.2.1 server candidate 1 64s +192.168.3.1 server selected 1 64s From 2b082584921f2134f7d9f538341578ed60b82602 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 3 Dec 2025 20:16:54 +0100 Subject: [PATCH 2/7] test/infamy: improve feedback on netconf/restconf error a bit Signed-off-by: Joachim Wiberg --- test/infamy/netconf.py | 21 ++++++++++++++++++--- test/infamy/restconf.py | 14 ++++++++++++-- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/test/infamy/netconf.py b/test/infamy/netconf.py index f2c7e092e..ebeacb8a2 100644 --- a/test/infamy/netconf.py +++ b/test/infamy/netconf.py @@ -319,7 +319,12 @@ def put_config_dicts(self, models): infer_put_dict(self.name, models) for model in models.keys(): - mod = self.ly.get_module(model) + try: + mod = self.ly.get_module(model) + except libyang.util.LibyangError: + raise Exception(f"YANG model '{model}' not found on device. " + f"Model may not be installed or enabled. " + f"Available models can be checked with get_schema_list()") from None lyd = mod.parse_data_dict(models[model], no_state=True, validate=False) config += lyd.print_mem("xml", with_siblings=True, pretty=False) + "\n" # print(f"Send new XML config: {config}") @@ -327,7 +332,12 @@ def put_config_dicts(self, models): def put_config_dict(self, modname, edit): """Convert Python dictionary to XMl and send as configuration""" - mod = self.ly.get_module(modname) + try: + mod = self.ly.get_module(modname) + except libyang.util.LibyangError: + raise Exception(f"YANG model '{modname}' not found on device. " + f"Model may not be installed or enabled. " + f"Available models can be checked with get_schema_list()") from None lyd = mod.parse_data_dict(edit, no_state=True, validate=False) config = lyd.print_mem("xml", with_siblings=True, pretty=False) # print(f"Send new XML config: {config}") @@ -339,7 +349,12 @@ def call(self, call): def call_dict(self, modname, call): """Call RPC, Python dictionary version""" - mod = self.ly.get_module(modname) + try: + mod = self.ly.get_module(modname) + except libyang.util.LibyangError: + raise Exception(f"YANG model '{modname}' not found on device. " + f"Model may not be installed or enabled. " + f"Available models can be checked with get_schema_list()") from None lyd = mod.parse_data_dict(call, rpc=True) return self.call(lyd.print_mem("xml", with_siblings=True, pretty=False)) diff --git a/test/infamy/restconf.py b/test/infamy/restconf.py index 04c922014..48654138b 100644 --- a/test/infamy/restconf.py +++ b/test/infamy/restconf.py @@ -266,7 +266,12 @@ def put_config_dicts(self, models): running = self.get_running() for model in models.keys(): - mod = self.lyctx.get_module(model) + try: + mod = self.lyctx.get_module(model) + except libyang.util.LibyangError: + raise Exception(f"YANG model '{model}' not found on device. " + f"Model may not be installed or enabled. " + f"Available models can be checked with get_schema_list()") from None lyd = mod.parse_data_dict(models[model], no_state=True, validate=False) running.merge(lyd) @@ -279,7 +284,12 @@ def put_config_dict(self, modname, edit): # This is hacky, refactor when rousette have PATCH support. running = self.get_running() - mod = self.lyctx.get_module(modname) + try: + mod = self.lyctx.get_module(modname) + except libyang.util.LibyangError: + raise Exception(f"YANG model '{modname}' not found on device. " + f"Model may not be installed or enabled. " + f"Available models can be checked with get_schema_list()") from None for k, _ in edit.items(): module = modname + ":" + k From 64a04f5a1d06f048f186c38199cf1fd9b057423b Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 3 Jan 2026 21:26:20 +0100 Subject: [PATCH 3/7] test/infamy: allow until() to return the result of the function Signed-off-by: Joachim Wiberg --- test/case/routing/ospf_multiarea/test.py | 22 +++++++++++----------- test/case/routing/static_routing/test.py | 4 ++-- test/infamy/env.py | 2 +- test/infamy/util.py | 5 +++-- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/test/case/routing/ospf_multiarea/test.py b/test/case/routing/ospf_multiarea/test.py index bcc52a3dc..6da4a8787 100755 --- a/test/case/routing/ospf_multiarea/test.py +++ b/test/case/routing/ospf_multiarea/test.py @@ -630,17 +630,17 @@ def disable_link(target, link): with test.step("Verify on R3, there are no routes beyond 10.0.23.1, just a default route"): # Should be only default route out of the area. - parallel(until(lambda: route.ipv4_route_exist(R3, "0.0.0.0/0"), attempts=200), - until(lambda: route.ipv4_route_exist(R3, "10.0.12.0/30") is False, attempts=5), - until(lambda: route.ipv4_route_exist(R3, "10.0.12.0/30") is False, attempts=5), - until(lambda: route.ipv4_route_exist(R3, "11.0.8.0/24") is False, attempts=5), - until(lambda: route.ipv4_route_exist(R3, "11.0.9.0/24") is False, attempts=5), - until(lambda: route.ipv4_route_exist(R3, "11.0.10.0/24") is False, attempts=5), - until(lambda: route.ipv4_route_exist(R3, "11.0.11.0/24") is False, attempts=5), - until(lambda: route.ipv4_route_exist(R3, "11.0.12.0/24") is False, attempts=5), - until(lambda: route.ipv4_route_exist(R3, "11.0.13.0/24") is False, attempts=5), - until(lambda: route.ipv4_route_exist(R3, "11.0.14.0/24") is False, attempts=5), - until(lambda: route.ipv4_route_exist(R3, "11.0.15.0/24") is False, attempts=5)) + parallel(lambda: until(lambda: route.ipv4_route_exist(R3, "0.0.0.0/0"), attempts=200), + lambda: until(lambda: route.ipv4_route_exist(R3, "10.0.12.0/30") is False, attempts=5), + lambda: until(lambda: route.ipv4_route_exist(R3, "10.0.12.0/30") is False, attempts=5), + lambda: until(lambda: route.ipv4_route_exist(R3, "11.0.8.0/24") is False, attempts=5), + lambda: until(lambda: route.ipv4_route_exist(R3, "11.0.9.0/24") is False, attempts=5), + lambda: until(lambda: route.ipv4_route_exist(R3, "11.0.10.0/24") is False, attempts=5), + lambda: until(lambda: route.ipv4_route_exist(R3, "11.0.11.0/24") is False, attempts=5), + lambda: until(lambda: route.ipv4_route_exist(R3, "11.0.12.0/24") is False, attempts=5), + lambda: until(lambda: route.ipv4_route_exist(R3, "11.0.13.0/24") is False, attempts=5), + lambda: until(lambda: route.ipv4_route_exist(R3, "11.0.14.0/24") is False, attempts=5), + lambda: until(lambda: route.ipv4_route_exist(R3, "11.0.15.0/24") is False, attempts=5)) _, hport0 = env.ltop.xlate("PC", "data3") with infamy.IsolatedMacVlan(hport0) as ns0: diff --git a/test/case/routing/static_routing/test.py b/test/case/routing/static_routing/test.py index 665f8705d..bcf14f3d8 100755 --- a/test/case/routing/static_routing/test.py +++ b/test/case/routing/static_routing/test.py @@ -228,8 +228,8 @@ def config_target2(target, link): with test.step("Remove all static routes from R1"): R1.delete_xpath("/ietf-routing:routing/control-plane-protocols") - parallel(until(lambda: route.ipv4_route_exist(R1, "192.168.200.1/32") is False), - until(lambda: route.ipv6_route_exist(R1, "2001:db8:3c4d:200::1/128") is False)) + parallel(lambda: until(lambda: route.ipv4_route_exist(R1, "192.168.200.1/32") is False), + lambda: until(lambda: route.ipv6_route_exist(R1, "2001:db8:3c4d:200::1/128") is False)) with test.step("Verify R2 is no longer reachable on either IPv4 or IPv6 from PC:data"): infamy.parallel(ns0.must_not_reach("192.168.200.1"), diff --git a/test/infamy/env.py b/test/infamy/env.py index caac53a04..19ca29577 100644 --- a/test/infamy/env.py +++ b/test/infamy/env.py @@ -149,7 +149,7 @@ def attach(self, node, port="mgmt", protocol=None, test_reset=True, username=Non cport, _ = self.ptop.get_mgmt_link(ctrl, node) print("Waiting for DUTs to become reachable...") - util.parallel(util.until(lambda: self.is_reachable(node, cport), 300)) + util.parallel(lambda: util.until(lambda: self.is_reachable(node, cport), 300)) print(f"Probing {node} on port {cport} for IPv6LL mgmt address ...") mgmtip = neigh.ll6ping(cport) diff --git a/test/infamy/util.py b/test/infamy/util.py index 174cae4ab..4a40e600e 100644 --- a/test/infamy/util.py +++ b/test/infamy/util.py @@ -39,8 +39,9 @@ def parallel(*fns): def until(fn, attempts=10, interval=1): for attempt in range(attempts): - if fn(): - return + result = fn() + if result: + return result time.sleep(interval) From 39b9c7a0657fad6b555599ca4862480dfbadec22 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 3 Dec 2025 20:19:40 +0100 Subject: [PATCH 4/7] test: add new tests for NTP Signed-off-by: Joachim Wiberg --- test/case/all.yaml | 3 + test/case/ntp/Readme.adoc | 28 +++ test/case/ntp/all.yaml | 15 ++ .../ntp/client_stratum_selection/Readme.adoc | 1 + .../ntp/client_stratum_selection/test.adoc | 34 ++++ .../case/ntp/client_stratum_selection/test.py | 176 ++++++++++++++++++ .../ntp/client_stratum_selection/topology.dot | 34 ++++ .../ntp/client_stratum_selection/topology.svg | 70 +++++++ test/case/ntp/server_client/Readme.adoc | 1 + test/case/ntp/server_client/test.adoc | 27 +++ test/case/ntp/server_client/test.py | 97 ++++++++++ test/case/ntp/server_client/topology.dot | 33 ++++ test/case/ntp/server_client/topology.svg | 58 ++++++ test/case/ntp/server_mode_peer/Readme.adoc | 1 + test/case/ntp/server_mode_peer/test.adoc | 35 ++++ test/case/ntp/server_mode_peer/test.py | 111 +++++++++++ test/case/ntp/server_mode_peer/topology.dot | 33 ++++ test/case/ntp/server_mode_peer/topology.svg | 60 ++++++ test/case/ntp/server_mode_server/Readme.adoc | 1 + test/case/ntp/server_mode_server/test.adoc | 35 ++++ test/case/ntp/server_mode_server/test.py | 136 ++++++++++++++ test/case/ntp/server_mode_server/topology.dot | 36 ++++ test/case/ntp/server_mode_server/topology.svg | 82 ++++++++ .../ntp/server_mode_standalone/Readme.adoc | 1 + test/case/ntp/server_mode_standalone/svg | 35 ++++ .../case/ntp/server_mode_standalone/test.adoc | 25 +++ test/case/ntp/server_mode_standalone/test.py | 61 ++++++ .../ntp/server_mode_standalone/topology.dot | 23 +++ .../server_mode_standalone/topology.dot.dot | 35 ++++ .../ntp/server_mode_standalone/topology.svg | 43 +++++ test/infamy/ntp.py | 109 ++++++++++- test/spec/Readme.adoc.in | 4 + 32 files changed, 1440 insertions(+), 3 deletions(-) create mode 100644 test/case/ntp/Readme.adoc create mode 100644 test/case/ntp/all.yaml create mode 120000 test/case/ntp/client_stratum_selection/Readme.adoc create mode 100644 test/case/ntp/client_stratum_selection/test.adoc create mode 100755 test/case/ntp/client_stratum_selection/test.py create mode 100644 test/case/ntp/client_stratum_selection/topology.dot create mode 100644 test/case/ntp/client_stratum_selection/topology.svg create mode 120000 test/case/ntp/server_client/Readme.adoc create mode 100644 test/case/ntp/server_client/test.adoc create mode 100755 test/case/ntp/server_client/test.py create mode 100644 test/case/ntp/server_client/topology.dot create mode 100644 test/case/ntp/server_client/topology.svg create mode 120000 test/case/ntp/server_mode_peer/Readme.adoc create mode 100644 test/case/ntp/server_mode_peer/test.adoc create mode 100755 test/case/ntp/server_mode_peer/test.py create mode 100644 test/case/ntp/server_mode_peer/topology.dot create mode 100644 test/case/ntp/server_mode_peer/topology.svg create mode 120000 test/case/ntp/server_mode_server/Readme.adoc create mode 100644 test/case/ntp/server_mode_server/test.adoc create mode 100755 test/case/ntp/server_mode_server/test.py create mode 100644 test/case/ntp/server_mode_server/topology.dot create mode 100644 test/case/ntp/server_mode_server/topology.svg create mode 120000 test/case/ntp/server_mode_standalone/Readme.adoc create mode 100644 test/case/ntp/server_mode_standalone/svg create mode 100644 test/case/ntp/server_mode_standalone/test.adoc create mode 100755 test/case/ntp/server_mode_standalone/test.py create mode 100644 test/case/ntp/server_mode_standalone/topology.dot create mode 100644 test/case/ntp/server_mode_standalone/topology.dot.dot create mode 100644 test/case/ntp/server_mode_standalone/topology.svg diff --git a/test/case/all.yaml b/test/case/all.yaml index 1f6987111..9177fcdf6 100644 --- a/test/case/all.yaml +++ b/test/case/all.yaml @@ -32,6 +32,9 @@ - name: "Interfaces" suite: interfaces/all.yaml +- name: "NTP Server" + suite: ntp/all.yaml + - name: "Routing" suite: routing/all.yaml diff --git a/test/case/ntp/Readme.adoc b/test/case/ntp/Readme.adoc new file mode 100644 index 000000000..13c39dfd6 --- /dev/null +++ b/test/case/ntp/Readme.adoc @@ -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[] diff --git a/test/case/ntp/all.yaml b/test/case/ntp/all.yaml new file mode 100644 index 000000000..d500bb9e7 --- /dev/null +++ b/test/case/ntp/all.yaml @@ -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 diff --git a/test/case/ntp/client_stratum_selection/Readme.adoc b/test/case/ntp/client_stratum_selection/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/ntp/client_stratum_selection/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/ntp/client_stratum_selection/test.adoc b/test/case/ntp/client_stratum_selection/test.adoc new file mode 100644 index 000000000..9c458894f --- /dev/null +++ b/test/case/ntp/client_stratum_selection/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) + + diff --git a/test/case/ntp/client_stratum_selection/test.py b/test/case/ntp/client_stratum_selection/test.py new file mode 100755 index 000000000..14f589e80 --- /dev/null +++ b/test/case/ntp/client_stratum_selection/test.py @@ -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() diff --git a/test/case/ntp/client_stratum_selection/topology.dot b/test/case/ntp/client_stratum_selection/topology.dot new file mode 100644 index 000000000..47c4f5167 --- /dev/null +++ b/test/case/ntp/client_stratum_selection/topology.dot @@ -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 | srv1 | <> \n\n\n | mgmt2 }", + pos="0,15!", + requires="controller", + ]; + + srv2 [ + label="{ mgmt | swp1 } | { srv2 | swp2 }", + pos="2,15.25!", + fontsize=12, + requires="infix", + ]; + + client [ + label=" mgmt | { 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"] +} diff --git a/test/case/ntp/client_stratum_selection/topology.svg b/test/case/ntp/client_stratum_selection/topology.svg new file mode 100644 index 000000000..bf224172e --- /dev/null +++ b/test/case/ntp/client_stratum_selection/topology.svg @@ -0,0 +1,70 @@ + + + + + + +topology + + + +host + +host + +mgmt1 + +srv1 + + +mgmt2 + + + +srv2 + +mgmt + +swp1 + +srv2 + +swp2 + + + +host:mgmt1--srv2:mgmt + + + + +host:srv1--srv2:swp1 + +192.168.1.2 +192.168.1.1 + + + +client + +mgmt + +eth0 + +client + + + +host:mgmt2--client:mgmt + + + + +srv2:swp2--client:eth0 + +192.168.1.3 + + + diff --git a/test/case/ntp/server_client/Readme.adoc b/test/case/ntp/server_client/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/ntp/server_client/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/ntp/server_client/test.adoc b/test/case/ntp/server_client/test.adoc new file mode 100644 index 000000000..abeb0a336 --- /dev/null +++ b/test/case/ntp/server_client/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 + + diff --git a/test/case/ntp/server_client/test.py b/test/case/ntp/server_client/test.py new file mode 100755 index 000000000..fd3de100b --- /dev/null +++ b/test/case/ntp/server_client/test.py @@ -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() diff --git a/test/case/ntp/server_client/topology.dot b/test/case/ntp/server_client/topology.dot new file mode 100644 index 000000000..a94db5336 --- /dev/null +++ b/test/case/ntp/server_client/topology.dot @@ -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 | \n\nhost\n\n\n | mgmt2 }", + pos="0,15!", + requires="controller", + ]; + + server [ + label="{ mgmt | data } | { server }", + pos="2,15.25!", + fontsize=12, + requires="infix", + ]; + + client [ + label="{ data | 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 "] +} diff --git a/test/case/ntp/server_client/topology.svg b/test/case/ntp/server_client/topology.svg new file mode 100644 index 000000000..68ee821bf --- /dev/null +++ b/test/case/ntp/server_client/topology.svg @@ -0,0 +1,58 @@ + + + + + + +ntp-server-client-interop + + + +host + +mgmt1 + +host + +mgmt2 + + + +server + +mgmt + +data + +server + + + +host:mgmt1--server:mgmt + + + + +client + +data + +mgmt + +client + + + +host:mgmt2--client:mgmt + + + + +server:data--client:data + +192.168.3.0/24   + + + diff --git a/test/case/ntp/server_mode_peer/Readme.adoc b/test/case/ntp/server_mode_peer/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/ntp/server_mode_peer/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/ntp/server_mode_peer/test.adoc b/test/case/ntp/server_mode_peer/test.adoc new file mode 100644 index 000000000..2469b6717 --- /dev/null +++ b/test/case/ntp/server_mode_peer/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 + + diff --git a/test/case/ntp/server_mode_peer/test.py b/test/case/ntp/server_mode_peer/test.py new file mode 100755 index 000000000..68f024487 --- /dev/null +++ b/test/case/ntp/server_mode_peer/test.py @@ -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() diff --git a/test/case/ntp/server_mode_peer/topology.dot b/test/case/ntp/server_mode_peer/topology.dot new file mode 100644 index 000000000..b99361465 --- /dev/null +++ b/test/case/ntp/server_mode_peer/topology.dot @@ -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 | \n\nhost\n\n\n | mgmt2 }", + pos="0,15!", + requires="controller", + ]; + + peer1 [ + label="{ mgmt | data } | { peer1 }", + pos="2,15.25!", + fontsize=12, + requires="infix", + ]; + + peer2 [ + label="{ data | 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"] +} diff --git a/test/case/ntp/server_mode_peer/topology.svg b/test/case/ntp/server_mode_peer/topology.svg new file mode 100644 index 000000000..b41dcc776 --- /dev/null +++ b/test/case/ntp/server_mode_peer/topology.svg @@ -0,0 +1,60 @@ + + + + + + +ntp-peer-mode + + + +host + +mgmt1 + +host + +mgmt2 + + + +peer1 + +mgmt + +data + +peer1 + + + +host:mgmt1--peer1:mgmt + + + + +peer2 + +data + +mgmt + +peer2 + + + +host:mgmt2--peer2:mgmt + + + + +peer1:data--peer2:data + + + +192.168.3.0/24   + + + diff --git a/test/case/ntp/server_mode_server/Readme.adoc b/test/case/ntp/server_mode_server/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/ntp/server_mode_server/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/ntp/server_mode_server/test.adoc b/test/case/ntp/server_mode_server/test.adoc new file mode 100644 index 000000000..e4cf34eb8 --- /dev/null +++ b/test/case/ntp/server_mode_server/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 + + diff --git a/test/case/ntp/server_mode_server/test.py b/test/case/ntp/server_mode_server/test.py new file mode 100755 index 000000000..46ccbe2c6 --- /dev/null +++ b/test/case/ntp/server_mode_server/test.py @@ -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() diff --git a/test/case/ntp/server_mode_server/topology.dot b/test/case/ntp/server_mode_server/topology.dot new file mode 100644 index 000000000..317fdd0df --- /dev/null +++ b/test/case/ntp/server_mode_server/topology.dot @@ -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 | data1 | <> \n\n\n | mgmt2 | data2 }", + pos="0,15!", + requires="controller", + ]; + + upstream [ + label="{ mgmt | data1 } | { \n upstream \n\n | conn }", + pos="2,15.25!", + fontsize=12, + requires="infix", + ]; + + downstream [ + label="{ mgmt | data2 } | { 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"] +} diff --git a/test/case/ntp/server_mode_server/topology.svg b/test/case/ntp/server_mode_server/topology.svg new file mode 100644 index 000000000..a1f20af43 --- /dev/null +++ b/test/case/ntp/server_mode_server/topology.svg @@ -0,0 +1,82 @@ + + + + + + +ntp-upstream-downstream + + + +host + +host + +mgmt1 + +data1 + + +mgmt2 + +data2 + + + +upstream + +mgmt + +data1 + + upstream + +conn + + + +host:mgmt1--upstream:mgmt + + + + +host:data1--upstream:data1 + +192.168.1.1 +192.168.1.2 + + + +downstream + +mgmt + +data2 + +conn + + downstream + + + +host:mgmt2--downstream:mgmt + + + + +host:data2--downstream:data2 + +192.168.2.1 +192.168.2.2 + + + +upstream:conn--downstream:conn + +Client/Server +192.168.3.x + + + diff --git a/test/case/ntp/server_mode_standalone/Readme.adoc b/test/case/ntp/server_mode_standalone/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/ntp/server_mode_standalone/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/ntp/server_mode_standalone/svg b/test/case/ntp/server_mode_standalone/svg new file mode 100644 index 000000000..cf0ddb452 --- /dev/null +++ b/test/case/ntp/server_mode_standalone/svg @@ -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 | 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 | 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"]; +} diff --git a/test/case/ntp/server_mode_standalone/test.adoc b/test/case/ntp/server_mode_standalone/test.adoc new file mode 100644 index 000000000..4f7cef5c7 --- /dev/null +++ b/test/case/ntp/server_mode_standalone/test.adoc @@ -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 + + diff --git a/test/case/ntp/server_mode_standalone/test.py b/test/case/ntp/server_mode_standalone/test.py new file mode 100755 index 000000000..57b78fbdf --- /dev/null +++ b/test/case/ntp/server_mode_standalone/test.py @@ -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() diff --git a/test/case/ntp/server_mode_standalone/topology.dot b/test/case/ntp/server_mode_standalone/topology.dot new file mode 100644 index 000000000..59a2c81cb --- /dev/null +++ b/test/case/ntp/server_mode_standalone/topology.dot @@ -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 | data1 }", + pos="1,1!", + requires="controller" + ]; + + target [ + label="{ mgmt | 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"] +} diff --git a/test/case/ntp/server_mode_standalone/topology.dot.dot b/test/case/ntp/server_mode_standalone/topology.dot.dot new file mode 100644 index 000000000..cf0ddb452 --- /dev/null +++ b/test/case/ntp/server_mode_standalone/topology.dot.dot @@ -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 | 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 | 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"]; +} diff --git a/test/case/ntp/server_mode_standalone/topology.svg b/test/case/ntp/server_mode_standalone/topology.svg new file mode 100644 index 000000000..250d9d3ab --- /dev/null +++ b/test/case/ntp/server_mode_standalone/topology.svg @@ -0,0 +1,43 @@ + + + + + + +1x2 + + + +host + +host + +mgmt + +data1 + + + +target + +mgmt + +data1 + +target + + + +host:mgmt--target:mgmt + + + + +host:data1--target:data1 + +192.168.1.0/24 + + + diff --git a/test/infamy/ntp.py b/test/infamy/ntp.py index 03a599af9..c05e6cd3d 100644 --- a/test/infamy/ntp.py +++ b/test/infamy/ntp.py @@ -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) diff --git a/test/spec/Readme.adoc.in b/test/spec/Readme.adoc.in index 4b0748e60..0e6a1dbe4 100644 --- a/test/spec/Readme.adoc.in +++ b/test/spec/Readme.adoc.in @@ -52,6 +52,10 @@ include::../case/dhcp/Readme.adoc[] <<< +include::../case/ntp/Readme.adoc[] + +<<< + include::../case/hardware/Readme.adoc[] <<< From 113b43ecaefea55f7f1a922e6d5eccbe93e5a115 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 3 Jan 2026 21:26:59 +0100 Subject: [PATCH 5/7] test: minor cleanup, whitespace only Signed-off-by: Joachim Wiberg --- test/case/interfaces/bridge_basic/test.py | 39 ++++++++++------------- test/case/system/ntp_client/test.py | 18 +++++------ 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/test/case/interfaces/bridge_basic/test.py b/test/case/interfaces/bridge_basic/test.py index 180d21266..ab4b0f715 100755 --- a/test/case/interfaces/bridge_basic/test.py +++ b/test/case/interfaces/bridge_basic/test.py @@ -25,28 +25,23 @@ target.put_config_dict("ietf-interfaces", { "interfaces": { - "interface": [ - { - "name": "br0", - "type": "infix-if-type:bridge", - "enabled": True, - "ipv4": { - "address": [ - { - "ip": "10.0.0.2", - "prefix-length": 24, - } - ] - } - }, - { - "name": tport, - "enabled": True, - "infix-interfaces:bridge-port": { - "bridge": "br0" - } - }, - ] + "interface": [{ + "name": "br0", + "type": "infix-if-type:bridge", + "enabled": True, + "ipv4": { + "address": [{ + "ip": "10.0.0.2", + "prefix-length": 24, + }] + } + }, { + "name": tport, + "enabled": True, + "infix-interfaces:bridge-port": { + "bridge": "br0" + } + }] } }) diff --git a/test/case/system/ntp_client/test.py b/test/case/system/ntp_client/test.py index c7816515d..8bb820803 100755 --- a/test/case/system/ntp_client/test.py +++ b/test/case/system/ntp_client/test.py @@ -9,12 +9,13 @@ 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": [ - { + "interface": [{ "name": data1, "enabled": True, "ipv4": { @@ -23,8 +24,7 @@ def config_target(dut, data1, data2, data3): "prefix-length": 24 }] } - }, - { + }, { "name": data2, "enabled": True, "ipv4": { @@ -33,8 +33,7 @@ def config_target(dut, data1, data2, data3): "prefix-length": 24 }] } - }, - { + }, { "name": data3, "enabled": True, "ipv4": { @@ -57,13 +56,13 @@ def config_target(dut, data1, data2, data3): "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" @@ -75,6 +74,7 @@ def config_target(dut, data1, data2, data3): } }) + with infamy.Test() as test: with test.step("Set up topology and attach to target DUT"): env = infamy.Env() @@ -104,6 +104,6 @@ def config_target(dut, data1, data2, data3): 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) + assert ntp.number_of_sources(target) == 3 test.succeed() From fca606cc7ccfc031d3817c51f64147df6ff3d936 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 3 Dec 2025 20:20:12 +0100 Subject: [PATCH 6/7] doc: add NTP server documentation Signed-off-by: Joachim Wiberg --- doc/ntp.md | 174 +++++++++++++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 175 insertions(+) create mode 100644 doc/ntp.md diff --git a/doc/ntp.md b/doc/ntp.md new file mode 100644 index 000000000..9fa07ab3e --- /dev/null +++ b/doc/ntp.md @@ -0,0 +1,174 @@ +# NTP Server + +The NTP (Network Time Protocol) server provides accurate time synchronization +for network clients. It supports both standalone operation with a local +reference clock and hybrid mode where it synchronizes with upstream servers +while serving time to downstream clients. + +> [!NOTE] +> The NTP server is mutually exclusive with the NTP client in system +> configuration context. + +## Standalone Mode + +Configure a standalone NTP server using only a local reference clock: + +``` +admin@example:/> configure +admin@example:/config/> edit ntp +admin@example:/config/ntp/> leave +``` + +When setting up NTP via the CLI the system automatically configures a local +reference clock. The default [stratum](#ntp-stratum-levels) is 16 (unsynchronized), +which is suitable for isolated networks. For production use, configure a specific +stratum level: + +``` +admin@example:/config/> edit ntp +admin@example:/config/ntp/> set refclock-master master-stratum 10 +admin@example:/config/ntp/> leave +``` + +## Server Mode + +Synchronize from upstream NTP servers while serving time to clients: + +``` +admin@example:/config/> edit ntp +admin@example:/config/ntp/> edit unicast-configuration 0.pool.ntp.org type uc-server +admin@example:/config/ntp/…/0.pool.ntp.org/type/uc-server/> set iburst true +admin@example:/config/ntp/…/0.pool.ntp.org/type/uc-server/> end +admin@example:/config/ntp/> edit unicast-configuration 1.pool.ntp.org type uc-server +admin@example:/config/ntp/…/1.pool.ntp.org/type/uc-server/> set iburst true +admin@example:/config/ntp/…/1.pool.ntp.org/type/uc-server/> end +admin@example:/config/ntp/> leave +``` + +The `unicast-configuration` uses a composite key with both address and type. +Both hostnames and IP addresses are supported. The `iburst` option enables +fast initial synchronization. + +## Peer Mode + +In peer mode, two NTP servers synchronize with each other bidirectionally. +Each server acts as both client and server to the other: + +**First peer:** + +``` +admin@peer1:/config/> edit ntp +admin@peer1:/config/ntp/> edit unicast-configuration 192.168.1.2 type uc-peer +admin@peer1:/config/ntp/…/192.168.1.2/type/uc-peer/> end +admin@peer1:/config/ntp/> set refclock-master master-stratum 8 +admin@peer1:/config/ntp/> leave +``` + +**Second peer:** + +``` +admin@peer2:/config/> edit ntp +admin@peer2:/config/ntp/> edit unicast-configuration 192.168.1.1 type uc-peer +admin@peer2:/config/ntp/…/192.168.1.1/type/uc-peer/> end +admin@peer2:/config/ntp/> set refclock-master master-stratum 8 +admin@peer2:/config/ntp/> leave +``` + +This configuration provides mutual synchronization between peers. If one peer +fails, the other continues to serve time to clients. + +> [!NOTE] +> The `iburst` and `burst` options are not supported in peer mode. + +### Peer Selection in Symmetric Mode + +When both peers have the same stratum (as in the example above where both are +stratum 8), NTP's clock selection algorithm uses the **Reference ID** as the +tie-breaker. The Reference ID is typically derived from the peer's IP address +when using a local reference clock. + +This means the peer with the **numerically lower IP address** will be selected +as the sync source by the other peer. In the example above: + +- peer1 (192.168.1.1) has a lower Reference ID +- peer2 (192.168.1.2) will select peer1 as sync source + +This behavior is deterministic and ensures stable clock selection. If you need +a specific peer to be selected, configure it with a lower stratum level than +the other peer. + +## Timing Configuration + +### Poll Intervals + +Control how often the NTP server polls upstream sources: + +``` +admin@example:/config/ntp/> edit unicast-configuration 0.pool.ntp.org type uc-server +admin@example:/config/ntp/…/0.pool.ntp.org/type/uc-server/> set minpoll 4 +admin@example:/config/ntp/…/0.pool.ntp.org/type/uc-server/> set maxpoll 10 +admin@example:/config/ntp/…/0.pool.ntp.org/type/uc-server/> end +``` + +Poll intervals are specified as powers of 2: +- `minpoll 4` = poll every 2^4 = 16 seconds (minimum polling rate) +- `maxpoll 10` = poll every 2^10 = 1024 seconds (maximum polling rate) +- Defaults: minpoll 6 (64 seconds), maxpoll 10 (1024 seconds) + +Use shorter intervals (minpoll 2-4) for faster convergence in test environments +or peer configurations. Use defaults for production servers. + +### Fast Initial Synchronization + +The `makestep` directive is automatically configured with safe defaults (1.0 +seconds threshold, 3 updates limit) when creating an NTP server. This is +critical for embedded systems without RTC that boot with epoch time. + +To customize the values: + +``` +admin@example:/config/ntp/> edit makestep +admin@example:/config/ntp/makestep/> set threshold 2.0 +admin@example:/config/ntp/makestep/> set limit 1 +admin@example:/config/ntp/makestep/> end +``` + +- **threshold** - If clock offset exceeds this (in seconds), step immediately + instead of slewing slowly +- **limit** - Number of updates during which stepping is allowed. After this, + only gradual slewing is used for security + +With these defaults, a device booting at epoch time (1970-01-01) will sync to +correct time within seconds instead of hours. + +## Monitoring + +Check NTP server statistics: + +``` +admin@example:/> show ntp server +NTP SERVER CONFIGURATION +Local Stratum : 10 + +SERVER STATISTICS +Packets Received : 142 +Packets Sent : 142 +Packets Dropped : 0 +Send Failures : 0 +``` + +## NTP Stratum Levels + +NTP uses a hierarchical system called **stratum** to indicate distance from +authoritative time sources: + +- **Stratum 0**: Reference clocks (atomic clocks) +- **Stratum 1**: Servers directly connected to stratum 0 (e.g., GPS receivers) +- **Stratum 2-15**: Servers that sync from lower stratum (each hop adds one) +- **Stratum 16**: Unsynchronized (invalid) + +The default stratum (16) is not suitable for distributing time in isolated +networks, so when setting up an NTP server remember to adjust this value. +Use, e.g., `10`, this is a safe, low-priority value that ensures clients will +prefer upstream-synchronized servers (stratum 1-9) while still having a +fallback time source in isolated networks. diff --git a/mkdocs.yml b/mkdocs.yml index 5d179c4f4..6aae8a6f0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,6 +37,7 @@ nav: - Services: - Device Discovery: discovery.md - DHCP Server: dhcp.md + - NTP Server: ntp.md - System: - Boot Procedure: boot.md - Configuration: system.md From a5efb1887f0aaafa179a6d6f4c2cf78a99177fde Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 3 Dec 2025 20:26:02 +0100 Subject: [PATCH 7/7] doc: update ChangeLog, NTP support Signed-off-by: Joachim Wiberg --- doc/ChangeLog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 9431cff63..06e942eaf 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -27,6 +27,7 @@ All notable changes to the project are documented in this file. - Upgrade sysrepo to 4.2.10 - Upgrade netopeer2 (NETCONF) to 2.7.0 - Add RIPv2 routing support, issue #582 +- Add NTP server support, issue #904 - Add support for configurable OSPF debug logging, issue #1281. Debug options can now be enabled per category (bfd, packet, ism, nsm, default-information, nssa). All debug options are disabled by default to prevent log flooding in