mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-23 17:33:01 +02:00
Merge pull request #1302 from kernelkit/ntp-server
This commit is contained in:
@@ -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
|
||||
|
||||
+174
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -40,6 +40,7 @@ confd_plugin_la_SOURCES = \
|
||||
if-wifi.c \
|
||||
keystore.c \
|
||||
system.c \
|
||||
ntp.c \
|
||||
syslog.c \
|
||||
factory-default.c \
|
||||
routing.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 */
|
||||
|
||||
@@ -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_ */
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
/* SPDX-License-Identifier: BSD-3-Clause */
|
||||
|
||||
#include <assert.h>
|
||||
#include <jansson.h>
|
||||
|
||||
#include <srx/common.h>
|
||||
#include <srx/lyx.h>
|
||||
#include <srx/srx_val.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -283,7 +283,7 @@ err:
|
||||
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;
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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: <https://datatracker.ietf.org/wg/netmod/>
|
||||
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.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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: <https://datatracker.ietf.org/wg/netmod/>
|
||||
WG List: <mailto:netmod@ietf.org>
|
||||
|
||||
Editor: Mahesh Jethanandani
|
||||
<mjethanandani@gmail.com>";
|
||||
|
||||
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.";
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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: <https://datatracker.ietf.org/wg/netmod/>
|
||||
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.";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
infix-ntp.yang
|
||||
@@ -324,15 +324,19 @@
|
||||
</ACTION>
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="ntp" help="Show NTP (client) status">
|
||||
<ACTION sym="script">
|
||||
show ntp
|
||||
</ACTION>
|
||||
<SWITCH name="optional" min="0">
|
||||
<COMMAND name="ntp" help="Show NTP status">
|
||||
<SWITCH name="subcommands" min="0">
|
||||
<COMMAND name="source" help="Show NTP source(s)">
|
||||
<SWITCH name="optional" min="0" max="1">
|
||||
<PARAM name="address" ptype="/STRING" help="Show details for specific source address"/>
|
||||
</SWITCH>
|
||||
<ACTION sym="script">show ntp source $KLISH_PARAM_address</ACTION>
|
||||
</COMMAND>
|
||||
<COMMAND name="tracking" help="Show NTP tracking">
|
||||
<ACTION sym="script">doas ntp tracking</ACTION>
|
||||
<ACTION sym="script">show ntp tracking</ACTION>
|
||||
</COMMAND>
|
||||
</SWITCH>
|
||||
<ACTION sym="script">show ntp</ACTION>
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="dhcp-server" help="Show DHCP server status">
|
||||
|
||||
+72
-3
@@ -69,15 +69,84 @@ def hardware(args: List[str]) -> None:
|
||||
cli_pretty(data, "show-hardware")
|
||||
|
||||
def ntp(args: List[str]) -> None:
|
||||
data = run_sysrepocfg("/system-state/ntp")
|
||||
# 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:
|
||||
print("No ntp data retrieved.")
|
||||
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")
|
||||
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:
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
hdr = (f"{'ADDRESS':<{PadNtpSource.address}}"
|
||||
f"{'MODE':<{PadNtpSource.mode}}"
|
||||
f"{'STATE':<{PadNtpSource.state}}"
|
||||
f"{'STRATUM':>{PadNtpSource.stratum}}"
|
||||
f"{'POLL-INTERVAL':>{PadNtpSource.poll}}"
|
||||
)
|
||||
|
||||
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
|
||||
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":
|
||||
|
||||
@@ -78,6 +78,9 @@ def main():
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
@@ -63,6 +63,7 @@ def add_ntp(out):
|
||||
|
||||
insert(out, "infix-system:ntp", "sources", "source", source)
|
||||
|
||||
|
||||
def add_dns(out):
|
||||
options = {}
|
||||
servers = []
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
- name: "Interfaces"
|
||||
suite: interfaces/all.yaml
|
||||
|
||||
- name: "NTP Server"
|
||||
suite: ntp/all.yaml
|
||||
|
||||
- name: "Routing"
|
||||
suite: routing/all.yaml
|
||||
|
||||
|
||||
@@ -25,28 +25,23 @@ with infamy.Test() as test:
|
||||
|
||||
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"
|
||||
}
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
:testgroup:
|
||||
== NTP Server Tests
|
||||
|
||||
Tests for NTP server functionality across different operational modes:
|
||||
|
||||
- Standalone mode: local reference clock only
|
||||
- Server mode: sync from upstream while serving clients
|
||||
- Peer mode: bidirectional synchronization between peers
|
||||
- Server and client interoperability
|
||||
- Client stratum selection between multiple servers
|
||||
|
||||
include::server_mode_standalone/Readme.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
include::server_mode_server/Readme.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
include::server_mode_peer/Readme.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
include::server_client/Readme.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
include::client_stratum_selection/Readme.adoc[]
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
- name: NTP server standalone mode
|
||||
case: server_mode_standalone/test.py
|
||||
|
||||
- name: NTP server mode
|
||||
case: server_mode_server/test.py
|
||||
|
||||
- name: NTP peer mode
|
||||
case: server_mode_peer/test.py
|
||||
|
||||
- name: NTP server and client interoperability
|
||||
case: server_client/test.py
|
||||
|
||||
- name: NTP client stratum selection
|
||||
case: client_stratum_selection/test.py
|
||||
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
@@ -0,0 +1,34 @@
|
||||
=== NTP client stratum selection
|
||||
|
||||
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ntp/client_stratum_selection]
|
||||
|
||||
==== Description
|
||||
|
||||
Verify NTP client properly selects between multiple servers based on
|
||||
stratum level.
|
||||
|
||||
This test validates NTP clock selection algorithm by configuring a client
|
||||
to sync from two servers with different stratum levels:
|
||||
|
||||
- srv1: Test PC running BusyBox ntpd (stratum ~1 via -l flag)
|
||||
- srv2: NTP server DUT syncing from srv1 (stratum ~2)
|
||||
- client: NTP client DUT syncing from both servers
|
||||
|
||||
Both servers sync to the same time source (srv2 syncs from srv1),
|
||||
ensuring time agreement and avoiding the "falseticker" problem. The client
|
||||
should then select srv1 (lower stratum) as its sync source.
|
||||
|
||||
==== Topology
|
||||
|
||||
image::topology.svg[NTP client stratum selection topology, align=center, scaledwidth=75%]
|
||||
|
||||
==== Sequence
|
||||
|
||||
. Set up topology and attach to devices
|
||||
. Configure srv2 to sync from srv1 and serve with higher stratum
|
||||
. Wait for srv2 to sync from srv1
|
||||
. Configure client to sync from both servers
|
||||
. Wait for client to see both servers
|
||||
. Verify client selects srv1 (lower stratum)
|
||||
|
||||
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""NTP client stratum selection test
|
||||
|
||||
Verify NTP client properly selects between multiple servers based on
|
||||
stratum level.
|
||||
|
||||
This test validates NTP clock selection algorithm by configuring a client
|
||||
to sync from two servers with different stratum levels:
|
||||
|
||||
- srv1: Test PC running BusyBox ntpd (stratum ~1 via -l flag)
|
||||
- srv2: NTP server DUT syncing from srv1 (stratum ~2)
|
||||
- client: NTP client DUT syncing from both servers
|
||||
|
||||
Both servers sync to the same time source (srv2 syncs from srv1),
|
||||
ensuring time agreement and avoiding the "falseticker" problem. The client
|
||||
should then select srv1 (lower stratum) as its sync source.
|
||||
|
||||
"""
|
||||
|
||||
import infamy
|
||||
from infamy import until
|
||||
import infamy.ntp as ntp
|
||||
import infamy.ntp_server as ntp_server
|
||||
|
||||
# Network configuration
|
||||
ips = {
|
||||
"srv1": "192.168.1.1", # BusyBox ntpd on test PC
|
||||
"srv2": "192.168.1.2", # Infix NTP server
|
||||
"client": "192.168.1.3" # Infix NTP client
|
||||
}
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Set up topology and attach to devices"):
|
||||
env = infamy.Env()
|
||||
srv2 = env.attach("srv2", "mgmt")
|
||||
client = env.attach("client", "mgmt")
|
||||
|
||||
_, swp1 = env.ltop.xlate("srv2", "swp1")
|
||||
_, swp2 = env.ltop.xlate("srv2", "swp2")
|
||||
_, eth0 = env.ltop.xlate("client", "eth0")
|
||||
_, srv1 = env.ltop.xlate("host", "srv1")
|
||||
|
||||
with infamy.IsolatedMacVlan(srv1) as ns_srv1:
|
||||
ns_srv1.addip(ips["srv1"])
|
||||
|
||||
with ntp_server.Server(ns_srv1):
|
||||
with test.step("Configure srv2 to sync from srv1 and serve with higher stratum"):
|
||||
srv2.put_config_dicts({
|
||||
"ietf-interfaces": {
|
||||
"interfaces": {
|
||||
"interface": [{
|
||||
"name": "br0",
|
||||
"type": "infix-if-type:bridge",
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": ips["srv2"],
|
||||
"prefix-length": 24,
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
"name": swp1,
|
||||
"enabled": True,
|
||||
"infix-interfaces:bridge-port": {
|
||||
"bridge": "br0"
|
||||
}
|
||||
}, {
|
||||
"name": swp2,
|
||||
"enabled": True,
|
||||
"infix-interfaces:bridge-port": {
|
||||
"bridge": "br0"
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
"ietf-ntp": {
|
||||
"ntp": {
|
||||
"unicast-configuration": [{
|
||||
"address": ips["srv1"], # Sync from srv1
|
||||
"type": "uc-server",
|
||||
"iburst": True
|
||||
}]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
with test.step("Wait for srv2 to sync from srv1"):
|
||||
until(lambda: ntp.server_has_associations(srv2), attempts=60)
|
||||
|
||||
with test.step("Configure client to sync from both servers"):
|
||||
client.put_config_dicts({
|
||||
"ietf-interfaces": {
|
||||
"interfaces": {
|
||||
"interface": [{
|
||||
"name": eth0,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": ips["client"],
|
||||
"prefix-length": 24
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
"ietf-system": {
|
||||
"system": {
|
||||
"ntp": {
|
||||
"enabled": True,
|
||||
"server": [{
|
||||
"name": "srv1",
|
||||
"udp": {
|
||||
"address": ips["srv1"]
|
||||
},
|
||||
"iburst": True
|
||||
}, {
|
||||
"name": "srv2",
|
||||
"udp": {
|
||||
"address": ips["srv2"]
|
||||
},
|
||||
"iburst": True
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
with test.step("Wait for client to see both servers"):
|
||||
until(lambda: ntp.number_of_sources(client) == 2, attempts=60)
|
||||
|
||||
with test.step("Wait for srv2 stratum to stabilize"):
|
||||
# Ensure srv2 has synced with srv1 and is advertising
|
||||
# stratum 2. This prevents race where both advertise
|
||||
# stratum 1, causing wrong selection
|
||||
def check_stratums():
|
||||
srv1 = ntp.get_source_by_address(client, ips["srv1"])
|
||||
srv2 = ntp.get_source_by_address(client, ips["srv2"])
|
||||
|
||||
if not srv1 or not srv2:
|
||||
return False
|
||||
|
||||
srv1_stratum = srv1.get("stratum")
|
||||
srv2_stratum = srv2.get("stratum")
|
||||
|
||||
# Both must have valid stratums and srv1 < srv2
|
||||
if srv1_stratum and srv2_stratum and srv1_stratum < srv2_stratum:
|
||||
return True
|
||||
return False
|
||||
|
||||
until(check_stratums, attempts=60)
|
||||
print(f"srv1 and srv2 stratums verified as different")
|
||||
|
||||
with test.step("Verify client selects srv1 (lower stratum)"):
|
||||
def srv1_selected():
|
||||
source = ntp.any_source_selected(client)
|
||||
if source and source.get("address") == ips["srv1"]:
|
||||
return source
|
||||
return None
|
||||
|
||||
try:
|
||||
selected = until(srv1_selected, attempts=120)
|
||||
except Exception:
|
||||
# Timeout - print diagnostic info
|
||||
sources = ntp.get_sources(client)
|
||||
print("DEBUG: Failed to select srv1. Source details:")
|
||||
for src in sources:
|
||||
print(f" {src.get('address')}: stratum={src.get('stratum')}, "
|
||||
f"state={src.get('state')}, poll={src.get('poll')}, "
|
||||
f"offset={src.get('offset')}")
|
||||
raise
|
||||
|
||||
assert selected is not None, "srv1 was not selected"
|
||||
print(f"Client correctly selected srv1 ({ips['srv1']}) "
|
||||
f"with stratum {selected.get('stratum')}")
|
||||
|
||||
test.succeed()
|
||||
@@ -0,0 +1,34 @@
|
||||
graph "topology" {
|
||||
layout="neato";
|
||||
overlap="false";
|
||||
esep="+20";
|
||||
|
||||
node [shape=record, fontname="DejaVu Sans Mono, Book"];
|
||||
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||
|
||||
host [
|
||||
label="host | { <mgmt1> mgmt1 | <srv1> srv1 | <> \n\n\n | <mgmt2> mgmt2 }",
|
||||
pos="0,15!",
|
||||
requires="controller",
|
||||
];
|
||||
|
||||
srv2 [
|
||||
label="{ <mgmt> mgmt | <swp1> swp1 } | { srv2 | <swp2> swp2 }",
|
||||
pos="2,15.25!",
|
||||
fontsize=12,
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
client [
|
||||
label="<mgmt> mgmt | { <eth0> eth0 | client }",
|
||||
pos="2,14.70!",
|
||||
fontsize=12,
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
host:mgmt1 -- srv2:mgmt [requires="mgmt", color="lightgray"]
|
||||
host:mgmt2 -- client:mgmt [requires="mgmt" color="lightgrey"]
|
||||
|
||||
host:srv1 -- srv2:swp1 [taillabel="192.168.1.1", headlabel="192.168.1.2"]
|
||||
srv2:swp2 -- client:eth0 [headlabel="192.168.1.3"]
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Title: topology Pages: 1 -->
|
||||
<svg width="510pt" height="144pt"
|
||||
viewBox="0.00 0.00 510.22 144.01" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 140.01)">
|
||||
<title>topology</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-140.01 506.22,-140.01 506.22,4 -4,4"/>
|
||||
<!-- host -->
|
||||
<g id="node1" class="node">
|
||||
<title>host</title>
|
||||
<polygon fill="none" stroke="black" points="0,-9.73 0,-134.73 108,-134.73 108,-9.73 0,-9.73"/>
|
||||
<text text-anchor="middle" x="25" y="-68.53" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||
<polyline fill="none" stroke="black" points="50,-9.73 50,-134.73 "/>
|
||||
<text text-anchor="middle" x="79" y="-119.53" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
|
||||
<polyline fill="none" stroke="black" points="50,-111.73 108,-111.73 "/>
|
||||
<text text-anchor="middle" x="79" y="-96.53" font-family="DejaVu Sans Mono, Book" font-size="14.00">srv1</text>
|
||||
<polyline fill="none" stroke="black" points="50,-88.73 108,-88.73 "/>
|
||||
<polyline fill="none" stroke="black" points="50,-32.73 108,-32.73 "/>
|
||||
<text text-anchor="middle" x="79" y="-17.53" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
|
||||
</g>
|
||||
<!-- srv2 -->
|
||||
<g id="node2" class="node">
|
||||
<title>srv2</title>
|
||||
<polygon fill="none" stroke="black" points="345.22,-93.51 345.22,-135.51 439.22,-135.51 439.22,-93.51 345.22,-93.51"/>
|
||||
<text text-anchor="middle" x="368.72" y="-121.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="345.22,-114.51 392.22,-114.51 "/>
|
||||
<text text-anchor="middle" x="368.72" y="-100.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">swp1</text>
|
||||
<polyline fill="none" stroke="black" points="392.22,-93.51 392.22,-135.51 "/>
|
||||
<text text-anchor="middle" x="415.72" y="-121.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">srv2</text>
|
||||
<polyline fill="none" stroke="black" points="392.22,-114.51 439.22,-114.51 "/>
|
||||
<text text-anchor="middle" x="415.72" y="-100.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">swp2</text>
|
||||
</g>
|
||||
<!-- host--srv2 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt1--srv2:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-123.23C108,-123.23 345.22,-125.51 345.22,-125.51"/>
|
||||
</g>
|
||||
<!-- host--srv2 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>host:srv1--srv2:swp1</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M108,-100.23C108,-100.23 345.22,-103.51 345.22,-103.51"/>
|
||||
<text text-anchor="middle" x="302.22" y="-107.31" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.2</text>
|
||||
<text text-anchor="middle" x="151" y="-104.03" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.1</text>
|
||||
</g>
|
||||
<!-- client -->
|
||||
<g id="node3" class="node">
|
||||
<title>client</title>
|
||||
<polygon fill="none" stroke="black" points="337.72,-0.5 337.72,-42.5 446.72,-42.5 446.72,-0.5 337.72,-0.5"/>
|
||||
<text text-anchor="middle" x="361.22" y="-18.4" font-family="DejaVu Sans Mono, Book" font-size="12.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="384.72,-0.5 384.72,-42.5 "/>
|
||||
<text text-anchor="middle" x="415.72" y="-28.9" font-family="DejaVu Sans Mono, Book" font-size="12.00">eth0</text>
|
||||
<polyline fill="none" stroke="black" points="384.72,-21.5 446.72,-21.5 "/>
|
||||
<text text-anchor="middle" x="415.72" y="-7.9" font-family="DejaVu Sans Mono, Book" font-size="12.00">client</text>
|
||||
</g>
|
||||
<!-- host--client -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:mgmt2--client:mgmt</title>
|
||||
<path fill="none" stroke="lightgrey" stroke-width="2" d="M108,-21.23C108,-21.23 337.22,-21.5 337.22,-21.5"/>
|
||||
</g>
|
||||
<!-- srv2--client -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>srv2:swp2--client:eth0</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M416.22,-93.51C416.22,-93.51 416.22,-42.5 416.22,-42.5"/>
|
||||
<text text-anchor="middle" x="459.22" y="-46.3" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.3</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
+1
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
@@ -0,0 +1,27 @@
|
||||
=== NTP server and client interoperability
|
||||
|
||||
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ntp/server_client]
|
||||
|
||||
==== Description
|
||||
|
||||
Verify NTP server and client work together:
|
||||
|
||||
1. Server uses ietf-ntp YANG model with refclock-master
|
||||
2. Client uses ietf-system YANG model
|
||||
3. Client successfully synchronizes from server
|
||||
4. Server shows packet statistics
|
||||
5. Mutual exclusion prevents both modes on same device
|
||||
|
||||
==== Topology
|
||||
|
||||
image::topology.svg[NTP server and client interoperability topology, align=center, scaledwidth=75%]
|
||||
|
||||
==== Sequence
|
||||
|
||||
. Set up topology and attach to devices
|
||||
. Configure NTP server using ietf-ntp model
|
||||
. Configure NTP client using ietf-system:ntp model
|
||||
. Verify NTP server has received packets
|
||||
. Verify NTP client has synchronized
|
||||
|
||||
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""NTP server and client interoperability test
|
||||
|
||||
Verify NTP server and client work together:
|
||||
|
||||
1. Server uses ietf-ntp YANG model with refclock-master
|
||||
2. Client uses ietf-system YANG model
|
||||
3. Client successfully synchronizes from server
|
||||
4. Server shows packet statistics
|
||||
5. Mutual exclusion prevents both modes on same device
|
||||
"""
|
||||
|
||||
import infamy
|
||||
from infamy import until
|
||||
import infamy.ntp as ntp
|
||||
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Set up topology and attach to devices"):
|
||||
env = infamy.Env()
|
||||
server = env.attach("server", "mgmt")
|
||||
client = env.attach("client", "mgmt")
|
||||
|
||||
_, server_data = env.ltop.xlate("server", "data")
|
||||
_, client_data = env.ltop.xlate("client", "data")
|
||||
|
||||
with test.step("Configure NTP server using ietf-ntp model"):
|
||||
server.put_config_dicts({
|
||||
"ietf-interfaces": {
|
||||
"interfaces": {
|
||||
"interface": [{
|
||||
"name": server_data,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": "192.168.3.1",
|
||||
"prefix-length": 24
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
"ietf-ntp": {
|
||||
"ntp": {
|
||||
"refclock-master": {
|
||||
"master-stratum": 8
|
||||
},
|
||||
"interfaces": {
|
||||
"interface": [
|
||||
{"name": server_data}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
with test.step("Configure NTP client using ietf-system:ntp model"):
|
||||
client.put_config_dicts({
|
||||
"ietf-interfaces": {
|
||||
"interfaces": {
|
||||
"interface": [{
|
||||
"name": client_data,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": "192.168.3.2",
|
||||
"prefix-length": 24
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
"ietf-system": {
|
||||
"system": {
|
||||
"ntp": {
|
||||
"enabled": True,
|
||||
"server": [{
|
||||
"name": "ntp-server",
|
||||
"udp": {
|
||||
"address": "192.168.3.1"
|
||||
},
|
||||
"iburst": True
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
with test.step("Verify NTP server has received packets"):
|
||||
until(lambda: ntp.server_has_received_packets(server), attempts=30)
|
||||
print("Server has received NTP packets from client")
|
||||
|
||||
with test.step("Verify NTP client has synchronized"):
|
||||
selected = until(lambda: ntp.any_source_selected(client), attempts=30)
|
||||
print(f"Client synchronized to {selected.get('address')} (stratum {selected.get('stratum')})")
|
||||
|
||||
test.succeed()
|
||||
@@ -0,0 +1,33 @@
|
||||
graph "ntp-server-client-interop" {
|
||||
layout="neato";
|
||||
overlap="false";
|
||||
esep="+22";
|
||||
|
||||
node [shape=record, fontname="DejaVu Sans Mono, Book"];
|
||||
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||
|
||||
host [
|
||||
label="{ <mgmt1> mgmt1 | <host> \n\nhost\n\n\n | <mgmt2> mgmt2 }",
|
||||
pos="0,15!",
|
||||
requires="controller",
|
||||
];
|
||||
|
||||
server [
|
||||
label="{ <mgmt> mgmt | <data> data } | { server }",
|
||||
pos="2,15.25!",
|
||||
fontsize=12,
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
client [
|
||||
label="{ <data> data | <mgmt> mgmt } | { client }",
|
||||
pos="2,14.75!",
|
||||
fontsize=12,
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
host:mgmt1 -- server:mgmt [requires="mgmt", color="lightgray"]
|
||||
host:mgmt2 -- client:mgmt [requires="mgmt" color="lightgrey"]
|
||||
|
||||
server:data -- client:data [label="\n\n192.168.3.0/24 "]
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Title: ntp-server-client-interop Pages: 1 -->
|
||||
<svg width="484pt" height="149pt"
|
||||
viewBox="0.00 0.00 483.54 149.01" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 145.01)">
|
||||
<title>ntp-server-client-interop</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-145.01 479.54,-145.01 479.54,4 -4,4"/>
|
||||
<!-- host -->
|
||||
<g id="node1" class="node">
|
||||
<title>host</title>
|
||||
<polygon fill="none" stroke="black" points="0,-4 0,-137 58,-137 58,-4 0,-4"/>
|
||||
<text text-anchor="middle" x="29" y="-121.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
|
||||
<polyline fill="none" stroke="black" points="0,-114 58,-114 "/>
|
||||
<text text-anchor="middle" x="29" y="-66.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||
<polyline fill="none" stroke="black" points="0,-27 58,-27 "/>
|
||||
<text text-anchor="middle" x="29" y="-11.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
|
||||
</g>
|
||||
<!-- server -->
|
||||
<g id="node2" class="node">
|
||||
<title>server</title>
|
||||
<polygon fill="none" stroke="black" points="366.54,-98.51 366.54,-140.51 475.54,-140.51 475.54,-98.51 366.54,-98.51"/>
|
||||
<text text-anchor="middle" x="390.04" y="-126.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="366.54,-119.51 413.54,-119.51 "/>
|
||||
<text text-anchor="middle" x="390.04" y="-105.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">data</text>
|
||||
<polyline fill="none" stroke="black" points="413.54,-98.51 413.54,-140.51 "/>
|
||||
<text text-anchor="middle" x="444.54" y="-116.41" font-family="DejaVu Sans Mono, Book" font-size="12.00">server</text>
|
||||
</g>
|
||||
<!-- host--server -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt1--server:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M58,-125.5C58,-125.5 366.04,-130.51 366.04,-130.51"/>
|
||||
</g>
|
||||
<!-- client -->
|
||||
<g id="node3" class="node">
|
||||
<title>client</title>
|
||||
<polygon fill="none" stroke="black" points="366.54,-0.5 366.54,-42.5 475.54,-42.5 475.54,-0.5 366.54,-0.5"/>
|
||||
<text text-anchor="middle" x="390.04" y="-28.9" font-family="DejaVu Sans Mono, Book" font-size="12.00">data</text>
|
||||
<polyline fill="none" stroke="black" points="366.54,-21.5 413.54,-21.5 "/>
|
||||
<text text-anchor="middle" x="390.04" y="-7.9" font-family="DejaVu Sans Mono, Book" font-size="12.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="413.54,-0.5 413.54,-42.5 "/>
|
||||
<text text-anchor="middle" x="444.54" y="-18.4" font-family="DejaVu Sans Mono, Book" font-size="12.00">client</text>
|
||||
</g>
|
||||
<!-- host--client -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:mgmt2--client:mgmt</title>
|
||||
<path fill="none" stroke="lightgrey" stroke-width="2" d="M58,-15.5C58,-15.5 366.04,-10.5 366.04,-10.5"/>
|
||||
</g>
|
||||
<!-- server--client -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>server:data--client:data</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M390.04,-98.51C390.04,-98.51 390.04,-42.5 390.04,-42.5"/>
|
||||
<text text-anchor="middle" x="331.04" y="-74.3" font-family="DejaVu Serif, Book" font-size="14.00">192.168.3.0/24  </text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
@@ -0,0 +1,35 @@
|
||||
=== NTP peer mode
|
||||
|
||||
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ntp/server_mode_peer]
|
||||
|
||||
==== Description
|
||||
|
||||
Verify NTP server operating in peer mode with bidirectional
|
||||
synchronization.
|
||||
|
||||
This test validates peer mode where two NTP servers synchronize with
|
||||
each other bidirectionally. Each server acts as both client and server
|
||||
to the other:
|
||||
|
||||
- peer1: Stratum 8 local clock, peered with peer2
|
||||
- peer2: Stratum 8 local clock, peered with peer1
|
||||
|
||||
The test verifies mutual synchronization and clock selection between
|
||||
peers. When both peers have the same stratum, NTP's clock selection
|
||||
algorithm uses the Reference ID (derived from the IP address) as its
|
||||
tie-breaker. The peer with the numerically lower IP address will be
|
||||
selected as sync source by the other peer.
|
||||
|
||||
==== Topology
|
||||
|
||||
image::topology.svg[NTP peer mode topology, align=center, scaledwidth=75%]
|
||||
|
||||
==== Sequence
|
||||
|
||||
. Set up topology and attach to devices
|
||||
. Configure DUTs with bidirectional peer relationships
|
||||
. Verify peers see each other in associations
|
||||
. Verify peers can reach each other
|
||||
. Wait for one peer to select the other as sync source
|
||||
|
||||
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""NTP peer mode test
|
||||
|
||||
Verify NTP server operating in peer mode with bidirectional
|
||||
synchronization.
|
||||
|
||||
This test validates peer mode where two NTP servers synchronize with
|
||||
each other bidirectionally. Each server acts as both client and server
|
||||
to the other:
|
||||
|
||||
- peer1: Stratum 8 local clock, peered with peer2
|
||||
- peer2: Stratum 8 local clock, peered with peer1
|
||||
|
||||
The test verifies mutual synchronization and clock selection between
|
||||
peers. When both peers have the same stratum, NTP's clock selection
|
||||
algorithm uses the Reference ID (derived from the IP address) as its
|
||||
tie-breaker. The peer with the numerically lower IP address will be
|
||||
selected as sync source by the other peer.
|
||||
|
||||
"""
|
||||
|
||||
import infamy
|
||||
from infamy import until
|
||||
import infamy.ntp as ntp
|
||||
|
||||
|
||||
def configure_peer(dut, iface, addr, peer, stratum=8):
|
||||
"""Configure NTP peer with interface and peer relationship."""
|
||||
dut.put_config_dicts({
|
||||
"ietf-interfaces": {
|
||||
"interfaces": {
|
||||
"interface": [{
|
||||
"name": iface,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": addr,
|
||||
"prefix-length": 24
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
"ietf-ntp": {
|
||||
"ntp": {
|
||||
"unicast-configuration": [{
|
||||
"address": peer,
|
||||
"type": "uc-peer",
|
||||
"minpoll": 2
|
||||
}],
|
||||
"refclock-master": {
|
||||
"master-stratum": stratum
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
def has_selected_peer(peers):
|
||||
"""Check if any peer has selected another as sync source."""
|
||||
for target, _, _, _, peer in peers:
|
||||
try:
|
||||
data = target.get_data("/ietf-ntp:ntp/associations")
|
||||
if not data:
|
||||
continue
|
||||
|
||||
assoc = data.get("ntp", {}).get("associations", {}).get("association", [])
|
||||
if not assoc:
|
||||
continue
|
||||
|
||||
for assoc in assoc:
|
||||
if assoc.get("prefer", False) and assoc.get("address") == peer:
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
return False
|
||||
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Set up topology and attach to devices"):
|
||||
env = infamy.Env()
|
||||
peer1 = env.attach("peer1", "mgmt")
|
||||
peer2 = env.attach("peer2", "mgmt")
|
||||
|
||||
_, if1 = env.ltop.xlate("peer1", "data")
|
||||
_, if2 = env.ltop.xlate("peer2", "data")
|
||||
|
||||
duts = [
|
||||
(peer1, if1, "peer1", "192.168.3.1", "192.168.3.2"),
|
||||
(peer2, if2, "peer2", "192.168.3.2", "192.168.3.1")
|
||||
]
|
||||
|
||||
with test.step("Configure DUTs with bidirectional peer relationships"):
|
||||
for dut, interface, name, local_ip, peer_ip in duts:
|
||||
configure_peer(dut, interface, local_ip, peer_ip)
|
||||
print(f"Configured {name}: {local_ip} peered with {peer_ip}")
|
||||
|
||||
with test.step("Verify peers see each other in associations"):
|
||||
for dut, _, name, _, peer_ip in duts:
|
||||
until(lambda t=dut, p=peer_ip: ntp.server_has_peer(t, p), attempts=20)
|
||||
print(f"{name} sees {peer_ip} in associations")
|
||||
|
||||
with test.step("Verify peers can reach each other"):
|
||||
for dut, _, name, _, peer_ip in duts:
|
||||
until(lambda t=dut, p=peer_ip: ntp.server_peer_reachable(t, p), attempts=60)
|
||||
print(f"{name} can reach {peer_ip}")
|
||||
|
||||
with test.step("Wait for one peer to select the other as sync source"):
|
||||
until(lambda: has_selected_peer(duts), attempts=120)
|
||||
|
||||
test.succeed()
|
||||
@@ -0,0 +1,33 @@
|
||||
graph "ntp-peer-mode" {
|
||||
layout="neato";
|
||||
overlap="false";
|
||||
esep="+22";
|
||||
|
||||
node [shape=record, fontname="DejaVu Sans Mono, Book"];
|
||||
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||
|
||||
host [
|
||||
label="{ <mgmt1> mgmt1 | <host> \n\nhost\n\n\n | <mgmt2> mgmt2 }",
|
||||
pos="0,15!",
|
||||
requires="controller",
|
||||
];
|
||||
|
||||
peer1 [
|
||||
label="{ <mgmt> mgmt | <data> data } | { peer1 }",
|
||||
pos="2,15.25!",
|
||||
fontsize=12,
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
peer2 [
|
||||
label="{ <data> data | <mgmt> mgmt } | { peer2 }",
|
||||
pos="2,14.75!",
|
||||
fontsize=12,
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
host:mgmt1 -- peer1:mgmt [requires="mgmt", color="lightgray"]
|
||||
host:mgmt2 -- peer2:mgmt [requires="mgmt" color="lightgrey"]
|
||||
|
||||
peer1:data -- peer2:data [label="\n\n192.168.3.0/24 ", dir="both"]
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Title: ntp-peer-mode Pages: 1 -->
|
||||
<svg width="480pt" height="149pt"
|
||||
viewBox="0.00 0.00 479.54 149.01" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 145.01)">
|
||||
<title>ntp-peer-mode</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-145.01 475.54,-145.01 475.54,4 -4,4"/>
|
||||
<!-- host -->
|
||||
<g id="node1" class="node">
|
||||
<title>host</title>
|
||||
<polygon fill="none" stroke="black" points="0,-4 0,-137 58,-137 58,-4 0,-4"/>
|
||||
<text text-anchor="middle" x="29" y="-121.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
|
||||
<polyline fill="none" stroke="black" points="0,-114 58,-114 "/>
|
||||
<text text-anchor="middle" x="29" y="-66.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||
<polyline fill="none" stroke="black" points="0,-27 58,-27 "/>
|
||||
<text text-anchor="middle" x="29" y="-11.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
|
||||
</g>
|
||||
<!-- peer1 -->
|
||||
<g id="node2" class="node">
|
||||
<title>peer1</title>
|
||||
<polygon fill="none" stroke="black" points="370.54,-98.51 370.54,-140.51 471.54,-140.51 471.54,-98.51 370.54,-98.51"/>
|
||||
<text text-anchor="middle" x="394.04" y="-126.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="370.54,-119.51 417.54,-119.51 "/>
|
||||
<text text-anchor="middle" x="394.04" y="-105.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">data</text>
|
||||
<polyline fill="none" stroke="black" points="417.54,-98.51 417.54,-140.51 "/>
|
||||
<text text-anchor="middle" x="444.54" y="-116.41" font-family="DejaVu Sans Mono, Book" font-size="12.00">peer1</text>
|
||||
</g>
|
||||
<!-- host--peer1 -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt1--peer1:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M58,-125.5C58,-125.5 370.04,-130.51 370.04,-130.51"/>
|
||||
</g>
|
||||
<!-- peer2 -->
|
||||
<g id="node3" class="node">
|
||||
<title>peer2</title>
|
||||
<polygon fill="none" stroke="black" points="370.54,-0.5 370.54,-42.5 471.54,-42.5 471.54,-0.5 370.54,-0.5"/>
|
||||
<text text-anchor="middle" x="394.04" y="-28.9" font-family="DejaVu Sans Mono, Book" font-size="12.00">data</text>
|
||||
<polyline fill="none" stroke="black" points="370.54,-21.5 417.54,-21.5 "/>
|
||||
<text text-anchor="middle" x="394.04" y="-7.9" font-family="DejaVu Sans Mono, Book" font-size="12.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="417.54,-0.5 417.54,-42.5 "/>
|
||||
<text text-anchor="middle" x="444.54" y="-18.4" font-family="DejaVu Sans Mono, Book" font-size="12.00">peer2</text>
|
||||
</g>
|
||||
<!-- host--peer2 -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:mgmt2--peer2:mgmt</title>
|
||||
<path fill="none" stroke="lightgrey" stroke-width="2" d="M58,-15.5C58,-15.5 370.04,-10.5 370.04,-10.5"/>
|
||||
</g>
|
||||
<!-- peer1--peer2 -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>peer1:data--peer2:data</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M394.04,-88.5C394.04,-78.43 394.04,-62.98 394.04,-52.81"/>
|
||||
<polygon fill="cornflowerblue" stroke="cornflowerblue" stroke-width="2" points="390.54,-88.51 394.04,-98.51 397.54,-88.51 390.54,-88.51"/>
|
||||
<polygon fill="cornflowerblue" stroke="cornflowerblue" stroke-width="2" points="397.54,-52.5 394.04,-42.5 390.54,-52.5 397.54,-52.5"/>
|
||||
<text text-anchor="middle" x="335.04" y="-74.45" font-family="DejaVu Serif, Book" font-size="14.00">192.168.3.0/24  </text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
@@ -0,0 +1,35 @@
|
||||
=== NTP server mode
|
||||
|
||||
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ntp/server_mode_server]
|
||||
|
||||
==== Description
|
||||
|
||||
Verify NTP server operating in server mode, syncing from upstream while
|
||||
serving clients.
|
||||
|
||||
This test validates server mode where devices synchronize from upstream
|
||||
NTP servers while simultaneously serving time to downstream clients. It
|
||||
creates a two-tier hierarchy:
|
||||
|
||||
- Upstream: NTP server with local reference clock (stratum 8)
|
||||
- Downstream: NTP server that syncs from upstream and serves to clients (stratum 9)
|
||||
|
||||
The test verifies both servers operate correctly and serve accurate time.
|
||||
|
||||
==== Topology
|
||||
|
||||
image::topology.svg[NTP server mode topology, align=center, scaledwidth=75%]
|
||||
|
||||
==== Sequence
|
||||
|
||||
. Set up topology and attach to devices
|
||||
. Configure upstream NTP server with local reference clock
|
||||
. Configure downstream NTP server syncing from upstream
|
||||
. Verify network connectivity with upstream NTP server
|
||||
. Query time from upstream NTP server
|
||||
. Verify upstream NTP server statistics
|
||||
. Verify network connectivity with downstream NTP server
|
||||
. Wait for downstream to sync from upstream
|
||||
. Verify downstream NTP server statistics
|
||||
|
||||
|
||||
Executable
+136
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""NTP server mode test
|
||||
|
||||
Verify NTP server operating in server mode, syncing from upstream while
|
||||
serving clients.
|
||||
|
||||
This test validates server mode where devices synchronize from upstream
|
||||
NTP servers while simultaneously serving time to downstream clients. It
|
||||
creates a two-tier hierarchy:
|
||||
|
||||
- Upstream: NTP server with local reference clock (stratum 8)
|
||||
- Downstream: NTP server that syncs from upstream and serves to clients (stratum 9)
|
||||
|
||||
The test verifies both servers operate correctly and serve accurate time.
|
||||
|
||||
"""
|
||||
|
||||
import infamy
|
||||
from infamy import until
|
||||
import infamy.ntp as ntp
|
||||
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Set up topology and attach to devices"):
|
||||
env = infamy.Env()
|
||||
upstream = env.attach("upstream", "mgmt")
|
||||
downstream = env.attach("downstream", "mgmt")
|
||||
|
||||
# Get interface names for each device
|
||||
_, upstream_data1 = env.ltop.xlate("upstream", "data1")
|
||||
_, upstream_conn = env.ltop.xlate("upstream", "conn")
|
||||
_, hport1 = env.ltop.xlate("host", "data1")
|
||||
|
||||
_, downstream_data2 = env.ltop.xlate("downstream", "data2")
|
||||
_, downstream_conn = env.ltop.xlate("downstream", "conn")
|
||||
_, hport2 = env.ltop.xlate("host", "data2")
|
||||
|
||||
with test.step("Configure upstream NTP server with local reference clock"):
|
||||
upstream.put_config_dicts({
|
||||
"ietf-interfaces": {
|
||||
"interfaces": {
|
||||
"interface": [{
|
||||
"name": upstream_data1,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": "192.168.1.1",
|
||||
"prefix-length": 24
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
"name": upstream_conn,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": "192.168.3.1",
|
||||
"prefix-length": 24
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
"ietf-ntp": {
|
||||
"ntp": {
|
||||
"refclock-master": {
|
||||
"master-stratum": 8
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
with test.step("Configure downstream NTP server syncing from upstream"):
|
||||
downstream.put_config_dicts({
|
||||
"ietf-interfaces": {
|
||||
"interfaces": {
|
||||
"interface": [{
|
||||
"name": downstream_data2,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": "192.168.2.1",
|
||||
"prefix-length": 24
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
"name": downstream_conn,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": "192.168.3.2",
|
||||
"prefix-length": 24
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
"ietf-ntp": {
|
||||
"ntp": {
|
||||
"unicast-configuration": [{
|
||||
"address": "192.168.3.1",
|
||||
"type": "uc-server",
|
||||
"iburst": True
|
||||
}],
|
||||
"refclock-master": {
|
||||
"master-stratum": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
with infamy.IsolatedMacVlan(hport1) as ns1:
|
||||
ns1.addip("192.168.1.2")
|
||||
|
||||
with test.step("Verify network connectivity with upstream NTP server"):
|
||||
ns1.must_reach("192.168.1.1")
|
||||
|
||||
with test.step("Query time from upstream NTP server"):
|
||||
until(lambda: ntp.server_query(ns1, "192.168.1.1"), attempts=20)
|
||||
|
||||
with test.step("Verify upstream NTP server statistics"):
|
||||
until(lambda: ntp.server_has_received_packets(upstream), attempts=20)
|
||||
|
||||
with infamy.IsolatedMacVlan(hport2) as ns2:
|
||||
ns2.addip("192.168.2.2")
|
||||
|
||||
with test.step("Verify network connectivity with downstream NTP server"):
|
||||
ns2.must_reach("192.168.2.1")
|
||||
|
||||
with test.step("Wait for downstream to sync from upstream"):
|
||||
# Give downstream time to sync from upstream
|
||||
until(lambda: ntp.server_query(ns2, "192.168.2.1"), attempts=30)
|
||||
|
||||
with test.step("Verify downstream NTP server statistics"):
|
||||
until(lambda: ntp.server_has_received_packets(downstream), attempts=20)
|
||||
|
||||
test.succeed()
|
||||
@@ -0,0 +1,36 @@
|
||||
graph "ntp-upstream-downstream" {
|
||||
layout="neato";
|
||||
overlap="false";
|
||||
esep="+22";
|
||||
|
||||
node [shape=record, fontname="DejaVu Sans Mono, Book"];
|
||||
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||
|
||||
host [
|
||||
label="host | { <mgmt1> mgmt1 | <data1> data1 | <> \n\n\n | <mgmt2> mgmt2 | <data2> data2 }",
|
||||
pos="0,15!",
|
||||
requires="controller",
|
||||
];
|
||||
|
||||
upstream [
|
||||
label="{ <mgmt> mgmt | <data1> data1 } | { \n upstream \n\n | <conn> conn }",
|
||||
pos="2,15.25!",
|
||||
fontsize=12,
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
downstream [
|
||||
label="{ <mgmt> mgmt | <data2> data2 } | { <conn> conn | \n downstream \n\n }",
|
||||
pos="2,14.75!",
|
||||
fontsize=12,
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
host:mgmt1 -- upstream:mgmt [requires="mgmt", color="lightgray"]
|
||||
host:data1 -- upstream:data1 [taillabel="192.168.1.2", headlabel="192.168.1.1"]
|
||||
|
||||
host:mgmt2 -- downstream:mgmt [requires="mgmt" color="lightgrey"]
|
||||
host:data2 -- downstream:data2 [taillabel="192.168.2.2", headlabel="192.168.2.1"]
|
||||
|
||||
upstream:conn -- downstream:conn [label="Client/Server\n192.168.3.x"]
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Title: ntp-upstream-downstream Pages: 1 -->
|
||||
<svg width="689pt" height="205pt"
|
||||
viewBox="0.00 0.00 689.05 205.01" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 201.01)">
|
||||
<title>ntp-upstream-downstream</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-201.01 685.05,-201.01 685.05,4 -4,4"/>
|
||||
<!-- host -->
|
||||
<g id="node1" class="node">
|
||||
<title>host</title>
|
||||
<polygon fill="none" stroke="black" points="0,-24.51 0,-172.51 108,-172.51 108,-24.51 0,-24.51"/>
|
||||
<text text-anchor="middle" x="25" y="-94.81" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||
<polyline fill="none" stroke="black" points="50,-24.51 50,-172.51 "/>
|
||||
<text text-anchor="middle" x="79" y="-157.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt1</text>
|
||||
<polyline fill="none" stroke="black" points="50,-149.51 108,-149.51 "/>
|
||||
<text text-anchor="middle" x="79" y="-134.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
|
||||
<polyline fill="none" stroke="black" points="50,-126.51 108,-126.51 "/>
|
||||
<polyline fill="none" stroke="black" points="50,-70.51 108,-70.51 "/>
|
||||
<text text-anchor="middle" x="79" y="-55.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt2</text>
|
||||
<polyline fill="none" stroke="black" points="50,-47.51 108,-47.51 "/>
|
||||
<text text-anchor="middle" x="79" y="-32.31" font-family="DejaVu Sans Mono, Book" font-size="14.00">data2</text>
|
||||
</g>
|
||||
<!-- upstream -->
|
||||
<g id="node2" class="node">
|
||||
<title>upstream</title>
|
||||
<polygon fill="none" stroke="black" points="485.05,-126.51 485.05,-196.51 631.05,-196.51 631.05,-126.51 485.05,-126.51"/>
|
||||
<text text-anchor="middle" x="512.05" y="-175.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="485.05,-161.51 539.05,-161.51 "/>
|
||||
<text text-anchor="middle" x="512.05" y="-140.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">data1</text>
|
||||
<polyline fill="none" stroke="black" points="539.05,-126.51 539.05,-196.51 "/>
|
||||
<text text-anchor="middle" x="585.05" y="-168.91" font-family="DejaVu Sans Mono, Book" font-size="12.00"> upstream </text>
|
||||
<polyline fill="none" stroke="black" points="539.05,-147.51 631.05,-147.51 "/>
|
||||
<text text-anchor="middle" x="585.05" y="-133.91" font-family="DejaVu Sans Mono, Book" font-size="12.00">conn</text>
|
||||
</g>
|
||||
<!-- host--upstream -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt1--upstream:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-161.51C108,-161.51 485.05,-179.51 485.05,-179.51"/>
|
||||
</g>
|
||||
<!-- host--upstream -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:data1--upstream:data1</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M108,-138.51C108,-138.51 485.05,-143.51 485.05,-143.51"/>
|
||||
<text text-anchor="middle" x="442.05" y="-147.31" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.1</text>
|
||||
<text text-anchor="middle" x="151" y="-142.31" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.2</text>
|
||||
</g>
|
||||
<!-- downstream -->
|
||||
<g id="node3" class="node">
|
||||
<title>downstream</title>
|
||||
<polygon fill="none" stroke="black" points="477.55,-0.5 477.55,-70.5 638.55,-70.5 638.55,-0.5 477.55,-0.5"/>
|
||||
<text text-anchor="middle" x="504.55" y="-49.9" font-family="DejaVu Sans Mono, Book" font-size="12.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="477.55,-35.5 531.55,-35.5 "/>
|
||||
<text text-anchor="middle" x="504.55" y="-14.9" font-family="DejaVu Sans Mono, Book" font-size="12.00">data2</text>
|
||||
<polyline fill="none" stroke="black" points="531.55,-0.5 531.55,-70.5 "/>
|
||||
<text text-anchor="middle" x="585.05" y="-56.9" font-family="DejaVu Sans Mono, Book" font-size="12.00">conn</text>
|
||||
<polyline fill="none" stroke="black" points="531.55,-49.5 638.55,-49.5 "/>
|
||||
<text text-anchor="middle" x="585.05" y="-21.9" font-family="DejaVu Sans Mono, Book" font-size="12.00"> downstream </text>
|
||||
</g>
|
||||
<!-- host--downstream -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>host:mgmt2--downstream:mgmt</title>
|
||||
<path fill="none" stroke="lightgrey" stroke-width="2" d="M108,-58.51C108,-58.51 477.05,-53.5 477.05,-53.5"/>
|
||||
</g>
|
||||
<!-- host--downstream -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>host:data2--downstream:data2</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M108,-35.51C108,-35.51 477.05,-17.5 477.05,-17.5"/>
|
||||
<text text-anchor="middle" x="434.05" y="-21.3" font-family="DejaVu Serif, Book" font-size="14.00">192.168.2.1</text>
|
||||
<text text-anchor="middle" x="151" y="-39.31" font-family="DejaVu Serif, Book" font-size="14.00">192.168.2.2</text>
|
||||
</g>
|
||||
<!-- upstream--downstream -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>upstream:conn--downstream:conn</title>
|
||||
<path fill="none" stroke="cornflowerblue" stroke-width="2" d="M585.05,-126.51C585.05,-126.51 585.05,-70.5 585.05,-70.5"/>
|
||||
<text text-anchor="middle" x="633.05" y="-117.31" font-family="DejaVu Serif, Book" font-size="14.00">Client/Server</text>
|
||||
<text text-anchor="middle" x="633.05" y="-102.31" font-family="DejaVu Serif, Book" font-size="14.00">192.168.3.x</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1 @@
|
||||
test.adoc
|
||||
@@ -0,0 +1,35 @@
|
||||
graph "1x2" {
|
||||
graph [bb="0,0,432.03,50.5",
|
||||
esep="+80",
|
||||
layout=neato,
|
||||
overlap=false
|
||||
];
|
||||
node [fontname="DejaVu Sans Mono, Book",
|
||||
label="\N",
|
||||
shape=record
|
||||
];
|
||||
edge [color=cornflowerblue,
|
||||
fontname="DejaVu Serif, Book",
|
||||
penwidth=2
|
||||
];
|
||||
host [height=0.65278,
|
||||
label="host | { <mgmt> mgmt | <data1> data1 }",
|
||||
pos="54,27",
|
||||
rects="0,4,50,50 50,27,108,50 50,4,108,27",
|
||||
requires=controller,
|
||||
width=1.5];
|
||||
target [height=0.65278,
|
||||
label="{ <mgmt> mgmt | <data1> data1 } | target",
|
||||
pos="370.03,27",
|
||||
rects="308.03,27,366.03,50 308.03,4,366.03,27 366.03,4,432.03,50",
|
||||
requires=infix,
|
||||
width=1.7222];
|
||||
host:mgmt -- target:mgmt [color=lightgray,
|
||||
pos="108,39 108,39 308.03,39 308.03,39",
|
||||
requires=mgmt];
|
||||
host:data1 -- target:data1 [color=blue,
|
||||
fontcolor=blue,
|
||||
label="192.168.1.0/24",
|
||||
lp="235.27,7.5",
|
||||
pos="108,15 108,15 308.03,15 308.03,15"];
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
=== NTP server standalone mode
|
||||
|
||||
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ntp/server_mode_standalone]
|
||||
|
||||
==== Description
|
||||
|
||||
Verify NTP server operating in standalone mode with only a local reference clock.
|
||||
|
||||
This test validates the basic standalone mode where the NTP server uses only
|
||||
its local reference clock (stratum 8) to serve time to clients, without
|
||||
syncing from any upstream sources.
|
||||
|
||||
==== Topology
|
||||
|
||||
image::topology.svg[NTP server standalone mode topology, align=center, scaledwidth=75%]
|
||||
|
||||
==== Sequence
|
||||
|
||||
. Set up topology and attach to target DUT
|
||||
. Configure interface and NTP server
|
||||
. Verify network connectivity with NTP server
|
||||
. Query time from NTP server
|
||||
. Verify NTP server statistics
|
||||
|
||||
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""NTP server standalone mode test
|
||||
|
||||
Verify NTP server operating in standalone mode with only a local reference clock.
|
||||
|
||||
This test validates the basic standalone mode where the NTP server uses only
|
||||
its local reference clock (stratum 8) to serve time to clients, without
|
||||
syncing from any upstream sources.
|
||||
"""
|
||||
|
||||
import infamy
|
||||
from infamy import until
|
||||
import infamy.ntp as ntp
|
||||
|
||||
|
||||
with infamy.Test() as test:
|
||||
with test.step("Set up topology and attach to target DUT"):
|
||||
env = infamy.Env()
|
||||
target = env.attach("target", "mgmt")
|
||||
_, data1 = env.ltop.xlate("target", "data1")
|
||||
_, hport1 = env.ltop.xlate("host", "data1")
|
||||
|
||||
with test.step("Configure interface and NTP server"):
|
||||
|
||||
target.put_config_dicts({
|
||||
"ietf-interfaces": {
|
||||
"interfaces": {
|
||||
"interface": [{
|
||||
"name": data1,
|
||||
"enabled": True,
|
||||
"ipv4": {
|
||||
"address": [{
|
||||
"ip": "192.168.1.1",
|
||||
"prefix-length": 24
|
||||
}]
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
"ietf-ntp": {
|
||||
"ntp": {
|
||||
"refclock-master": {
|
||||
"master-stratum": 8
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
with infamy.IsolatedMacVlan(hport1) as ns1:
|
||||
ns1.addip("192.168.1.2")
|
||||
|
||||
with test.step("Verify network connectivity with NTP server"):
|
||||
ns1.must_reach("192.168.1.1")
|
||||
|
||||
with test.step("Query time from NTP server"):
|
||||
until(lambda: ntp.server_query(ns1, "192.168.1.1"), attempts=20)
|
||||
|
||||
with test.step("Verify NTP server statistics"):
|
||||
until(lambda: ntp.server_has_received_packets(target), attempts=20)
|
||||
|
||||
test.succeed()
|
||||
@@ -0,0 +1,23 @@
|
||||
graph "1x2" {
|
||||
layout = "neato";
|
||||
overlap = false;
|
||||
esep = "+80";
|
||||
|
||||
node [shape=record, fontname="DejaVu Sans Mono, Book"];
|
||||
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||
|
||||
host [
|
||||
label="host | { <mgmt> mgmt | <data1> data1 }",
|
||||
pos="1,1!",
|
||||
requires="controller"
|
||||
];
|
||||
|
||||
target [
|
||||
label="{ <mgmt> mgmt | <data1> data1 } | target",
|
||||
pos="3,1!",
|
||||
requires="infix",
|
||||
];
|
||||
|
||||
host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
|
||||
host:data1 -- target:data1 [color=blue, fontcolor=blue, label="192.168.1.0/24"]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
graph "1x2" {
|
||||
graph [bb="0,0,432.03,50.5",
|
||||
esep="+80",
|
||||
layout=neato,
|
||||
overlap=false
|
||||
];
|
||||
node [fontname="DejaVu Sans Mono, Book",
|
||||
label="\N",
|
||||
shape=record
|
||||
];
|
||||
edge [color=cornflowerblue,
|
||||
fontname="DejaVu Serif, Book",
|
||||
penwidth=2
|
||||
];
|
||||
host [height=0.65278,
|
||||
label="host | { <mgmt> mgmt | <data1> data1 }",
|
||||
pos="54,27",
|
||||
rects="0,4,50,50 50,27,108,50 50,4,108,27",
|
||||
requires=controller,
|
||||
width=1.5];
|
||||
target [height=0.65278,
|
||||
label="{ <mgmt> mgmt | <data1> data1 } | target",
|
||||
pos="370.03,27",
|
||||
rects="308.03,27,366.03,50 308.03,4,366.03,27 366.03,4,432.03,50",
|
||||
requires=infix,
|
||||
width=1.7222];
|
||||
host:mgmt -- target:mgmt [color=lightgray,
|
||||
pos="108,39 108,39 308.03,39 308.03,39",
|
||||
requires=mgmt];
|
||||
host:data1 -- target:data1 [color=blue,
|
||||
fontcolor=blue,
|
||||
label="192.168.1.0/24",
|
||||
lp="235.27,7.5",
|
||||
pos="108,15 108,15 308.03,15 308.03,15"];
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<!-- Title: 1x2 Pages: 1 -->
|
||||
<svg width="440pt" height="59pt"
|
||||
viewBox="0.00 0.00 440.03 58.50" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 54.5)">
|
||||
<title>1x2</title>
|
||||
<polygon fill="white" stroke="transparent" points="-4,4 -4,-54.5 436.03,-54.5 436.03,4 -4,4"/>
|
||||
<!-- host -->
|
||||
<g id="node1" class="node">
|
||||
<title>host</title>
|
||||
<polygon fill="none" stroke="black" points="0,-4 0,-50 108,-50 108,-4 0,-4"/>
|
||||
<text text-anchor="middle" x="25" y="-23.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||
<polyline fill="none" stroke="black" points="50,-4 50,-50 "/>
|
||||
<text text-anchor="middle" x="79" y="-34.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="50,-27 108,-27 "/>
|
||||
<text text-anchor="middle" x="79" y="-11.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
|
||||
</g>
|
||||
<!-- target -->
|
||||
<g id="node2" class="node">
|
||||
<title>target</title>
|
||||
<polygon fill="none" stroke="black" points="308.03,-4 308.03,-50 432.03,-50 432.03,-4 308.03,-4"/>
|
||||
<text text-anchor="middle" x="337.03" y="-34.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||
<polyline fill="none" stroke="black" points="308.03,-27 366.03,-27 "/>
|
||||
<text text-anchor="middle" x="337.03" y="-11.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
|
||||
<polyline fill="none" stroke="black" points="366.03,-4 366.03,-50 "/>
|
||||
<text text-anchor="middle" x="399.03" y="-23.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
|
||||
</g>
|
||||
<!-- host--target -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt--target:mgmt</title>
|
||||
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-39C108,-39 308.03,-39 308.03,-39"/>
|
||||
</g>
|
||||
<!-- host--target -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>host:data1--target:data1</title>
|
||||
<path fill="none" stroke="blue" stroke-width="2" d="M108,-15C108,-15 308.03,-15 308.03,-15"/>
|
||||
<text text-anchor="middle" x="235.27" y="-3.8" font-family="DejaVu Serif, Book" font-size="14.00" fill="blue">192.168.1.0/24</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -630,17 +630,17 @@ with infamy.Test() as test:
|
||||
|
||||
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:
|
||||
|
||||
@@ -228,8 +228,8 @@ with infamy.Test() as test:
|
||||
|
||||
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"),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
[7mADDRESS MODE STATE STRATUM POLL-INTERVAL[0m
|
||||
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
|
||||
|
||||
[7mADDRESS MODE STATE STRATUM POLL[0m
|
||||
192.168.1.1 server candidate 1 64s
|
||||
192.168.2.1 server candidate 1 64s
|
||||
192.168.3.1 server selected 1 64s
|
||||
|
||||
@@ -9,12 +9,13 @@ import infamy
|
||||
import infamy.ntp_server as ntp_server
|
||||
import infamy.ntp as ntp
|
||||
import infamy.util as util
|
||||
|
||||
|
||||
def config_target(dut, data1, data2, data3):
|
||||
dut.put_config_dicts({
|
||||
"ietf-interfaces": {
|
||||
"interfaces": {
|
||||
"interface": [
|
||||
{
|
||||
"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 @@ with infamy.Test() as test:
|
||||
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()
|
||||
|
||||
+1
-1
@@ -149,7 +149,7 @@ class Env(object):
|
||||
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)
|
||||
|
||||
+18
-3
@@ -319,7 +319,12 @@ class Device(Transport):
|
||||
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 @@ class Device(Transport):
|
||||
|
||||
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 @@ class Device(Transport):
|
||||
|
||||
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))
|
||||
|
||||
|
||||
+106
-3
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
NTP client helper
|
||||
NTP client and server helpers
|
||||
"""
|
||||
|
||||
|
||||
@@ -22,17 +22,120 @@ def _get_ntp_sources(target):
|
||||
return ntp["sources"]["source"]
|
||||
|
||||
|
||||
def get_sources(target):
|
||||
"""Get list of NTP sources from operational state."""
|
||||
return _get_ntp_sources(target)
|
||||
|
||||
|
||||
def get_source_by_address(target, address):
|
||||
"""Get NTP source by address, or None if not found."""
|
||||
sources = _get_ntp_sources(target)
|
||||
for source in sources:
|
||||
if source.get("address") == address:
|
||||
return source
|
||||
return None
|
||||
|
||||
|
||||
def any_source_selected(target):
|
||||
"""Return the first selected NTP source, or None if no source is selected."""
|
||||
sources = _get_ntp_sources(target)
|
||||
|
||||
for source in sources:
|
||||
if source["state"] == "selected":
|
||||
return True
|
||||
return source
|
||||
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
def number_of_sources(target):
|
||||
sources = _get_ntp_sources(target)
|
||||
|
||||
return len(sources)
|
||||
|
||||
|
||||
def server_has_received_packets(target):
|
||||
"""Verify NTP server (ietf-ntp) has received packets."""
|
||||
try:
|
||||
data = target.get_data("/ietf-ntp:ntp/ntp-statistics")
|
||||
if not data:
|
||||
return False
|
||||
|
||||
stats = data["ntp"].get("ntp-statistics", {})
|
||||
if not stats:
|
||||
return False
|
||||
|
||||
packets_received = int(stats.get("packet-received", 0))
|
||||
return packets_received > 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def server_query(netns, server_ip, expected_stratum=None):
|
||||
"""Query NTP server from a network namespace and return True if successful.
|
||||
|
||||
Optionally verify the stratum level if expected_stratum is provided.
|
||||
"""
|
||||
result = netns.runsh(f"timeout 1 ntpd -qwp {server_ip}")
|
||||
output = result.stdout if result.stdout else ""
|
||||
|
||||
if f"ntpd: reply from {server_ip}" not in output or "offset" not in output:
|
||||
return False
|
||||
|
||||
if expected_stratum is not None:
|
||||
# Extract stratum from output like: "stratum 8"
|
||||
for line in output.split('\n'):
|
||||
if 'stratum' in line.lower():
|
||||
try:
|
||||
stratum = int(line.split()[-1])
|
||||
return stratum == expected_stratum
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def server_has_associations(target):
|
||||
"""Verify NTP server (ietf-ntp) has any associations."""
|
||||
try:
|
||||
data = target.get_data("/ietf-ntp:ntp/associations")
|
||||
if not data:
|
||||
return False
|
||||
|
||||
associations = data.get("ntp", {}).get("associations", {}).get("association", [])
|
||||
return len(associations) > 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def server_has_peer(target, peer_address):
|
||||
"""Verify NTP server (ietf-ntp) has a peer association with given address."""
|
||||
try:
|
||||
data = target.get_data("/ietf-ntp:ntp/associations")
|
||||
if not data:
|
||||
return False
|
||||
|
||||
associations = data.get("ntp", {}).get("associations", {}).get("association", [])
|
||||
if not associations:
|
||||
return False
|
||||
|
||||
# Check if peer association exists with the given address
|
||||
# local-mode will be "ietf-ntp:active" or "active" depending on namespace handling
|
||||
for assoc in associations:
|
||||
local_mode = assoc.get("local-mode", "")
|
||||
if (assoc.get("address") == peer_address and
|
||||
(local_mode == "ietf-ntp:active" or local_mode == "active")):
|
||||
return True
|
||||
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def server_peer_reachable(target, peer_address):
|
||||
"""Verify NTP peer association exists (peer is configured and running)."""
|
||||
# For now, just check if the association exists
|
||||
# The YANG associations container doesn't expose reach/state info
|
||||
# but if the association shows up, it means chronyd is running and
|
||||
# communicating with the peer
|
||||
return server_has_peer(target, peer_address)
|
||||
|
||||
+12
-2
@@ -266,7 +266,12 @@ class Device(Transport):
|
||||
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 @@ class Device(Transport):
|
||||
|
||||
# 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
|
||||
|
||||
+3
-2
@@ -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)
|
||||
|
||||
|
||||
@@ -52,6 +52,10 @@ include::../case/dhcp/Readme.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
include::../case/ntp/Readme.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
include::../case/hardware/Readme.adoc[]
|
||||
|
||||
<<<
|
||||
|
||||
Reference in New Issue
Block a user