mirror of
https://github.com/kernelkit/infix.git
synced 2026-08-01 21:33:02 +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 sysrepo to 4.2.10
|
||||||
- Upgrade netopeer2 (NETCONF) to 2.7.0
|
- Upgrade netopeer2 (NETCONF) to 2.7.0
|
||||||
- Add RIPv2 routing support, issue #582
|
- Add RIPv2 routing support, issue #582
|
||||||
|
- Add NTP server support, issue #904
|
||||||
- Add support for configurable OSPF debug logging, issue #1281. Debug options
|
- Add support for configurable OSPF debug logging, issue #1281. Debug options
|
||||||
can now be enabled per category (bfd, packet, ism, nsm, default-information,
|
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
|
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:
|
- Services:
|
||||||
- Device Discovery: discovery.md
|
- Device Discovery: discovery.md
|
||||||
- DHCP Server: dhcp.md
|
- DHCP Server: dhcp.md
|
||||||
|
- NTP Server: ntp.md
|
||||||
- System:
|
- System:
|
||||||
- Boot Procedure: boot.md
|
- Boot Procedure: boot.md
|
||||||
- Configuration: system.md
|
- Configuration: system.md
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ confd_plugin_la_SOURCES = \
|
|||||||
if-wifi.c \
|
if-wifi.c \
|
||||||
keystore.c \
|
keystore.c \
|
||||||
system.c \
|
system.c \
|
||||||
|
ntp.c \
|
||||||
syslog.c \
|
syslog.c \
|
||||||
factory-default.c \
|
factory-default.c \
|
||||||
routing.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)))
|
if ((rc = keystore_change(session, config, diff, event, confd)))
|
||||||
goto free_diff;
|
goto free_diff;
|
||||||
|
|
||||||
|
/* ietf-ntp */
|
||||||
|
if ((rc = ntp_change(session, config, diff, event, confd)))
|
||||||
|
goto free_diff;
|
||||||
|
|
||||||
/* infix-services */
|
/* infix-services */
|
||||||
if ((rc = services_change(session, config, diff, event, confd)))
|
if ((rc = services_change(session, config, diff, event, confd)))
|
||||||
goto free_diff;
|
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)))
|
if ((rc = meta_change_cb(session, config, diff, event, confd)))
|
||||||
goto free_diff;
|
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)
|
if (cfg)
|
||||||
sr_release_data(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");
|
ERROR("Failed to subscribe to ietf-netconf-acm");
|
||||||
goto err;
|
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);
|
rc = subscribe_model("infix-dhcp-client", &confd, 0);
|
||||||
if (rc) {
|
if (rc) {
|
||||||
ERROR("Failed to subscribe to infix-dhcp-client");
|
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;
|
goto err;
|
||||||
|
|
||||||
rc = dhcp_server_candidate_init(&confd);
|
rc = dhcp_server_candidate_init(&confd);
|
||||||
|
if (rc)
|
||||||
|
goto err;
|
||||||
|
|
||||||
|
rc = ntp_candidate_init(&confd);
|
||||||
if (rc)
|
if (rc)
|
||||||
goto err;
|
goto err;
|
||||||
/* YOUR_INIT GOES HERE */
|
/* YOUR_INIT GOES HERE */
|
||||||
|
|||||||
@@ -247,4 +247,10 @@ int firewall_rpc_init(struct confd *confd);
|
|||||||
int firewall_candidate_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);
|
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_ */
|
#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;
|
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;
|
sr_change_iter_t *iter = NULL;
|
||||||
int rc, err = SR_ERR_OK;
|
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:
|
case SR_EV_DONE:
|
||||||
if (!srx_enabled(session, XPATH_NTP_"/enabled")) {
|
if (!srx_enabled(session, XPATH_NTP_"/enabled")) {
|
||||||
systemf("rm -rf /etc/chrony/conf.d/* /etc/chrony/sources.d/*");
|
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;
|
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");
|
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;
|
return SR_ERR_OK;
|
||||||
|
|
||||||
default:
|
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)))
|
if ((rc = change_auth(session, config, diff, event, confd)))
|
||||||
return rc;
|
return rc;
|
||||||
if ((rc = change_ntp(session, config, diff, event, confd)))
|
if ((rc = change_ntp_client(session, config, diff, event, confd)))
|
||||||
return rc;
|
return rc;
|
||||||
if ((rc = change_dns(session, config, diff, event, confd)))
|
if ((rc = change_dns(session, config, diff, event, confd)))
|
||||||
return rc;
|
return rc;
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
MODULES=(
|
MODULES=(
|
||||||
"ietf-system@2014-08-06.yang -e authentication -e local-users -e ntp -e ntp-udp-port -e timezone-name"
|
"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"
|
"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-interfaces@2018-02-20.yang -e if-mib"
|
||||||
"ietf-ip@2018-02-22.yang -e ipv6-privacy-autoconf"
|
"ietf-ip@2018-02-22.yang -e ipv6-privacy-autoconf"
|
||||||
"ietf-network-instance@2019-01-21.yang"
|
"ietf-network-instance@2019-01-21.yang"
|
||||||
@@ -48,4 +52,5 @@ MODULES=(
|
|||||||
"infix-crypto-types@2025-06-17.yang"
|
"infix-crypto-types@2025-06-17.yang"
|
||||||
"ietf-keystore -e symmetric-keys"
|
"ietf-keystore -e symmetric-keys"
|
||||||
"infix-keystore@2025-12-10.yang"
|
"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>
|
</ACTION>
|
||||||
</COMMAND>
|
</COMMAND>
|
||||||
|
|
||||||
<COMMAND name="ntp" help="Show NTP (client) status">
|
<COMMAND name="ntp" help="Show NTP status">
|
||||||
<ACTION sym="script">
|
<SWITCH name="subcommands" min="0">
|
||||||
show ntp
|
<COMMAND name="source" help="Show NTP source(s)">
|
||||||
</ACTION>
|
<SWITCH name="optional" min="0" max="1">
|
||||||
<SWITCH name="optional" min="0">
|
<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">
|
<COMMAND name="tracking" help="Show NTP tracking">
|
||||||
<ACTION sym="script">doas ntp tracking</ACTION>
|
<ACTION sym="script">show ntp tracking</ACTION>
|
||||||
</COMMAND>
|
</COMMAND>
|
||||||
</SWITCH>
|
</SWITCH>
|
||||||
|
<ACTION sym="script">show ntp</ACTION>
|
||||||
</COMMAND>
|
</COMMAND>
|
||||||
|
|
||||||
<COMMAND name="dhcp-server" help="Show DHCP server status">
|
<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")
|
cli_pretty(data, "show-hardware")
|
||||||
|
|
||||||
def ntp(args: List[str]) -> None:
|
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:
|
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
|
return
|
||||||
|
|
||||||
if RAW_OUTPUT:
|
if RAW_OUTPUT:
|
||||||
print(json.dumps(data, indent=2))
|
print(json.dumps(data, indent=2))
|
||||||
return
|
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:
|
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}%")
|
print(f"CPU Usage : {cpu_usage}%")
|
||||||
|
|
||||||
|
|
||||||
def show_ntp(json):
|
def show_ntp_source_detail_single(source, is_association=False):
|
||||||
if not json.get("ietf-system:system-state"):
|
"""Display detailed information for a single NTP source (auto-detected single source)"""
|
||||||
print("NTP client not enabled.")
|
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
|
return
|
||||||
hdr = (f"{'ADDRESS':<{PadNtpSource.address}}"
|
|
||||||
f"{'MODE':<{PadNtpSource.mode}}"
|
if is_server:
|
||||||
f"{'STATE':<{PadNtpSource.state}}"
|
if sources:
|
||||||
f"{'STRATUM':>{PadNtpSource.stratum}}"
|
print(f"{'Mode':<20}: Relay (no local reference clock)")
|
||||||
f"{'POLL-INTERVAL':>{PadNtpSource.poll}}"
|
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))
|
print(Decore.invert(hdr))
|
||||||
sources = get_json_data({}, json, 'ietf-system:system-state', 'infix-system:ntp', 'sources', 'source')
|
|
||||||
for source in sources:
|
for source in sources:
|
||||||
row = f"{source['address']:<{PadNtpSource.address}}"
|
# Extract fields - handle both association and ietf-system format
|
||||||
row += f"{source['mode']:<{PadNtpSource.mode}}"
|
address = source.get('address', 'N/A')
|
||||||
row += f"{source['state'] if source['state'] != 'not-combined' else 'not combined':<{PadNtpSource.state}}"
|
|
||||||
row += f"{source['stratum']:>{PadNtpSource.stratum}}"
|
if is_server:
|
||||||
row += f"{source['poll']:>{PadNtpSource.poll}}"
|
# 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)
|
print(row)
|
||||||
|
|
||||||
|
|
||||||
@@ -4392,7 +4829,11 @@ def main():
|
|||||||
subparsers.add_parser('show-firewall-log', help='Show firewall log') \
|
subparsers.add_parser('show-firewall-log', help='Show firewall log') \
|
||||||
.add_argument('limit', nargs='?', help='Last N lines, default: all')
|
.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', help='Show BFD sessions')
|
||||||
subparsers.add_parser('show-bfd-status', help='Show BFD status')
|
subparsers.add_parser('show-bfd-status', help='Show BFD status')
|
||||||
@@ -4452,7 +4893,11 @@ def main():
|
|||||||
elif args.command == "show-firewall-log":
|
elif args.command == "show-firewall-log":
|
||||||
show_firewall_logs(args.limit)
|
show_firewall_logs(args.limit)
|
||||||
elif args.command == "show-ntp":
|
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":
|
elif args.command == "show-wifi-radio":
|
||||||
show_wifi_radio(json_data)
|
show_wifi_radio(json_data)
|
||||||
elif args.command == "show-bfd":
|
elif args.command == "show-bfd":
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ def main():
|
|||||||
elif args.model == 'ietf-system':
|
elif args.model == 'ietf-system':
|
||||||
from . import ietf_system
|
from . import ietf_system
|
||||||
yang_data = ietf_system.operational()
|
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':
|
elif args.model == 'ieee802-dot1ab-lldp':
|
||||||
from . import infix_lldp
|
from . import infix_lldp
|
||||||
yang_data = infix_lldp.operational()
|
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)
|
insert(out, "infix-system:ntp", "sources", "source", source)
|
||||||
|
|
||||||
|
|
||||||
def add_dns(out):
|
def add_dns(out):
|
||||||
options = {}
|
options = {}
|
||||||
servers = []
|
servers = []
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
#define XPATH_DHCP_SERVER_BASE "/infix-dhcp-server:dhcp-server"
|
#define XPATH_DHCP_SERVER_BASE "/infix-dhcp-server:dhcp-server"
|
||||||
#define XPATH_LLDP_BASE "/ieee802-dot1ab-lldp:lldp"
|
#define XPATH_LLDP_BASE "/ieee802-dot1ab-lldp:lldp"
|
||||||
#define XPATH_FIREWALL_BASE "/infix-firewall:firewall"
|
#define XPATH_FIREWALL_BASE "/infix-firewall:firewall"
|
||||||
|
#define XPATH_NTP_BASE "/ietf-ntp:ntp"
|
||||||
|
|
||||||
TAILQ_HEAD(sub_head, sub);
|
TAILQ_HEAD(sub_head, sub);
|
||||||
|
|
||||||
@@ -444,6 +445,8 @@ static int subscribe_to_all(struct statd *statd)
|
|||||||
return SR_ERR_INTERNAL;
|
return SR_ERR_INTERNAL;
|
||||||
if (subscribe(statd, "infix-firewall", XPATH_FIREWALL_BASE, sr_generic_cb))
|
if (subscribe(statd, "infix-firewall", XPATH_FIREWALL_BASE, sr_generic_cb))
|
||||||
return SR_ERR_INTERNAL;
|
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");
|
INFO("Successfully subscribed to all models");
|
||||||
return SR_ERR_OK;
|
return SR_ERR_OK;
|
||||||
|
|||||||
@@ -32,6 +32,9 @@
|
|||||||
- name: "Interfaces"
|
- name: "Interfaces"
|
||||||
suite: interfaces/all.yaml
|
suite: interfaces/all.yaml
|
||||||
|
|
||||||
|
- name: "NTP Server"
|
||||||
|
suite: ntp/all.yaml
|
||||||
|
|
||||||
- name: "Routing"
|
- name: "Routing"
|
||||||
suite: routing/all.yaml
|
suite: routing/all.yaml
|
||||||
|
|
||||||
|
|||||||
@@ -25,28 +25,23 @@ with infamy.Test() as test:
|
|||||||
|
|
||||||
target.put_config_dict("ietf-interfaces", {
|
target.put_config_dict("ietf-interfaces", {
|
||||||
"interfaces": {
|
"interfaces": {
|
||||||
"interface": [
|
"interface": [{
|
||||||
{
|
"name": "br0",
|
||||||
"name": "br0",
|
"type": "infix-if-type:bridge",
|
||||||
"type": "infix-if-type:bridge",
|
"enabled": True,
|
||||||
"enabled": True,
|
"ipv4": {
|
||||||
"ipv4": {
|
"address": [{
|
||||||
"address": [
|
"ip": "10.0.0.2",
|
||||||
{
|
"prefix-length": 24,
|
||||||
"ip": "10.0.0.2",
|
}]
|
||||||
"prefix-length": 24,
|
}
|
||||||
}
|
}, {
|
||||||
]
|
"name": tport,
|
||||||
}
|
"enabled": True,
|
||||||
},
|
"infix-interfaces:bridge-port": {
|
||||||
{
|
"bridge": "br0"
|
||||||
"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"):
|
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.
|
# Should be only default route out of the area.
|
||||||
parallel(until(lambda: route.ipv4_route_exist(R3, "0.0.0.0/0"), attempts=200),
|
parallel(lambda: 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),
|
lambda: 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),
|
lambda: 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),
|
lambda: 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),
|
lambda: 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),
|
lambda: 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),
|
lambda: 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),
|
lambda: 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),
|
lambda: 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),
|
lambda: 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))
|
lambda: until(lambda: route.ipv4_route_exist(R3, "11.0.15.0/24") is False, attempts=5))
|
||||||
|
|
||||||
_, hport0 = env.ltop.xlate("PC", "data3")
|
_, hport0 = env.ltop.xlate("PC", "data3")
|
||||||
with infamy.IsolatedMacVlan(hport0) as ns0:
|
with infamy.IsolatedMacVlan(hport0) as ns0:
|
||||||
|
|||||||
@@ -228,8 +228,8 @@ with infamy.Test() as test:
|
|||||||
|
|
||||||
with test.step("Remove all static routes from R1"):
|
with test.step("Remove all static routes from R1"):
|
||||||
R1.delete_xpath("/ietf-routing:routing/control-plane-protocols")
|
R1.delete_xpath("/ietf-routing:routing/control-plane-protocols")
|
||||||
parallel(until(lambda: route.ipv4_route_exist(R1, "192.168.200.1/32") is False),
|
parallel(lambda: 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))
|
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"):
|
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"),
|
infamy.parallel(ns0.must_not_reach("192.168.200.1"),
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
[7mADDRESS MODE STATE STRATUM POLL-INTERVAL[0m
|
Mode : Client
|
||||||
192.168.1.1 server candidate 1 6
|
|
||||||
192.168.2.1 server candidate 1 6
|
[7mADDRESS MODE STATE STRATUM POLL[0m
|
||||||
192.168.3.1 server selected 1 6
|
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_server as ntp_server
|
||||||
import infamy.ntp as ntp
|
import infamy.ntp as ntp
|
||||||
import infamy.util as util
|
import infamy.util as util
|
||||||
|
|
||||||
|
|
||||||
def config_target(dut, data1, data2, data3):
|
def config_target(dut, data1, data2, data3):
|
||||||
dut.put_config_dicts({
|
dut.put_config_dicts({
|
||||||
"ietf-interfaces": {
|
"ietf-interfaces": {
|
||||||
"interfaces": {
|
"interfaces": {
|
||||||
"interface": [
|
"interface": [{
|
||||||
{
|
|
||||||
"name": data1,
|
"name": data1,
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"ipv4": {
|
"ipv4": {
|
||||||
@@ -23,8 +24,7 @@ def config_target(dut, data1, data2, data3):
|
|||||||
"prefix-length": 24
|
"prefix-length": 24
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
"name": data2,
|
"name": data2,
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"ipv4": {
|
"ipv4": {
|
||||||
@@ -33,8 +33,7 @@ def config_target(dut, data1, data2, data3):
|
|||||||
"prefix-length": 24
|
"prefix-length": 24
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
},
|
}, {
|
||||||
{
|
|
||||||
"name": data3,
|
"name": data3,
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"ipv4": {
|
"ipv4": {
|
||||||
@@ -57,13 +56,13 @@ def config_target(dut, data1, data2, data3):
|
|||||||
"address": "192.168.1.1"
|
"address": "192.168.1.1"
|
||||||
},
|
},
|
||||||
"iburst": True
|
"iburst": True
|
||||||
},{
|
}, {
|
||||||
"name": "Server2",
|
"name": "Server2",
|
||||||
"udp": {
|
"udp": {
|
||||||
"address": "192.168.2.1"
|
"address": "192.168.2.1"
|
||||||
},
|
},
|
||||||
"iburst": True
|
"iburst": True
|
||||||
},{
|
}, {
|
||||||
"name": "Server3",
|
"name": "Server3",
|
||||||
"udp": {
|
"udp": {
|
||||||
"address": "192.168.3.1"
|
"address": "192.168.3.1"
|
||||||
@@ -75,6 +74,7 @@ def config_target(dut, data1, data2, data3):
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
with infamy.Test() as test:
|
with infamy.Test() as test:
|
||||||
with test.step("Set up topology and attach to target DUT"):
|
with test.step("Set up topology and attach to target DUT"):
|
||||||
env = infamy.Env()
|
env = infamy.Env()
|
||||||
@@ -104,6 +104,6 @@ with infamy.Test() as test:
|
|||||||
with test.step("Verify one source is in 'selected' state on 'target'"):
|
with test.step("Verify one source is in 'selected' state on 'target'"):
|
||||||
util.until(lambda: ntp.any_source_selected(target), attempts=200)
|
util.until(lambda: ntp.any_source_selected(target), attempts=200)
|
||||||
with test.step("Verify three sources exist in NTP client on 'target'"):
|
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()
|
test.succeed()
|
||||||
|
|||||||
+1
-1
@@ -149,7 +149,7 @@ class Env(object):
|
|||||||
cport, _ = self.ptop.get_mgmt_link(ctrl, node)
|
cport, _ = self.ptop.get_mgmt_link(ctrl, node)
|
||||||
|
|
||||||
print("Waiting for DUTs to become reachable...")
|
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 ...")
|
print(f"Probing {node} on port {cport} for IPv6LL mgmt address ...")
|
||||||
mgmtip = neigh.ll6ping(cport)
|
mgmtip = neigh.ll6ping(cport)
|
||||||
|
|||||||
+18
-3
@@ -319,7 +319,12 @@ class Device(Transport):
|
|||||||
infer_put_dict(self.name, models)
|
infer_put_dict(self.name, models)
|
||||||
|
|
||||||
for model in models.keys():
|
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)
|
lyd = mod.parse_data_dict(models[model], no_state=True, validate=False)
|
||||||
config += lyd.print_mem("xml", with_siblings=True, pretty=False) + "\n"
|
config += lyd.print_mem("xml", with_siblings=True, pretty=False) + "\n"
|
||||||
# print(f"Send new XML config: {config}")
|
# print(f"Send new XML config: {config}")
|
||||||
@@ -327,7 +332,12 @@ class Device(Transport):
|
|||||||
|
|
||||||
def put_config_dict(self, modname, edit):
|
def put_config_dict(self, modname, edit):
|
||||||
"""Convert Python dictionary to XMl and send as configuration"""
|
"""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)
|
lyd = mod.parse_data_dict(edit, no_state=True, validate=False)
|
||||||
config = lyd.print_mem("xml", with_siblings=True, pretty=False)
|
config = lyd.print_mem("xml", with_siblings=True, pretty=False)
|
||||||
# print(f"Send new XML config: {config}")
|
# print(f"Send new XML config: {config}")
|
||||||
@@ -339,7 +349,12 @@ class Device(Transport):
|
|||||||
|
|
||||||
def call_dict(self, modname, call):
|
def call_dict(self, modname, call):
|
||||||
"""Call RPC, Python dictionary version"""
|
"""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)
|
lyd = mod.parse_data_dict(call, rpc=True)
|
||||||
return self.call(lyd.print_mem("xml", with_siblings=True, pretty=False))
|
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"]
|
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):
|
def any_source_selected(target):
|
||||||
|
"""Return the first selected NTP source, or None if no source is selected."""
|
||||||
sources = _get_ntp_sources(target)
|
sources = _get_ntp_sources(target)
|
||||||
|
|
||||||
for source in sources:
|
for source in sources:
|
||||||
if source["state"] == "selected":
|
if source["state"] == "selected":
|
||||||
return True
|
return source
|
||||||
|
|
||||||
return False
|
return None
|
||||||
|
|
||||||
|
|
||||||
def number_of_sources(target):
|
def number_of_sources(target):
|
||||||
sources = _get_ntp_sources(target)
|
sources = _get_ntp_sources(target)
|
||||||
|
|
||||||
return len(sources)
|
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()
|
running = self.get_running()
|
||||||
|
|
||||||
for model in models.keys():
|
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)
|
lyd = mod.parse_data_dict(models[model], no_state=True, validate=False)
|
||||||
running.merge(lyd)
|
running.merge(lyd)
|
||||||
|
|
||||||
@@ -279,7 +284,12 @@ class Device(Transport):
|
|||||||
|
|
||||||
# This is hacky, refactor when rousette have PATCH support.
|
# This is hacky, refactor when rousette have PATCH support.
|
||||||
running = self.get_running()
|
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():
|
for k, _ in edit.items():
|
||||||
module = modname + ":" + k
|
module = modname + ":" + k
|
||||||
|
|||||||
+3
-2
@@ -39,8 +39,9 @@ def parallel(*fns):
|
|||||||
|
|
||||||
def until(fn, attempts=10, interval=1):
|
def until(fn, attempts=10, interval=1):
|
||||||
for attempt in range(attempts):
|
for attempt in range(attempts):
|
||||||
if fn():
|
result = fn()
|
||||||
return
|
if result:
|
||||||
|
return result
|
||||||
|
|
||||||
time.sleep(interval)
|
time.sleep(interval)
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ include::../case/dhcp/Readme.adoc[]
|
|||||||
|
|
||||||
<<<
|
<<<
|
||||||
|
|
||||||
|
include::../case/ntp/Readme.adoc[]
|
||||||
|
|
||||||
|
<<<
|
||||||
|
|
||||||
include::../case/hardware/Readme.adoc[]
|
include::../case/hardware/Readme.adoc[]
|
||||||
|
|
||||||
<<<
|
<<<
|
||||||
|
|||||||
Reference in New Issue
Block a user