diff --git a/doc/testing.md b/doc/testing.md index eb3d1476..99abffdc 100644 --- a/doc/testing.md +++ b/doc/testing.md @@ -359,6 +359,53 @@ $ make test-spec ... ``` +### Node and Link Capabilities + +Logical topology files (`topology.dot`) declare what each node and link +*requires*; physical topology files declare what each node and link +*provides*. When mapping a logical topology to physical hardware, infamy +only assigns a physical node to a logical node when the physical node's +`provides` set is a superset of the logical node's `requires` set. Tests +are skipped if no matching physical topology can be found. + +#### Declaring requirements (logical topology) + +```dot +dut [ + requires="infix", +]; + +host:data -- dut:data [requires="ptp-hwts"] +``` + +#### Declaring capabilities (physical topology) + +```dot +switch1 [ + provides="infix", +]; + +switch1:eth0 -- switch2:eth0 [provides="ptp-hwts"] +``` + +#### Node capabilities + +| Capability | Meaning | +|-------------------|-------------------------------------------------------------------------| +| `controller` | Reserved for the host/controller node; never assigned to a DUT | +| `infix` | Node runs Infix OS — required by virtually all DUT nodes | +| `gps` | Node has a GPS receiver available as a time reference | +| `watchdog` | Node has a hardware watchdog device | + +#### Link capabilities + +| Capability | Meaning | +|-------------------|-------------------------------------------------------------------------| +| `mgmt` | Link is a management path (typically coloured grey in diagrams) | +| `ieee-mc` | Link carries IEEE multicast traffic (required by LAG and some L2 tests) | +| `link-ctrl copper`| Link supports copper speed/duplex control | +| `ptp-hwts` | Both ends of this link support PTP hardware timestamping (PHC); required for sub-microsecond accuracy | + ### Test Development For adding a new test to the automated regression test suite, it's best diff --git a/src/confd/src/ptp.c b/src/confd/src/ptp.c index 38b2204a..c52be690 100644 --- a/src/confd/src/ptp.c +++ b/src/confd/src/ptp.c @@ -39,7 +39,7 @@ static const char *instance_type_to_clock_type(const char *type) * gmCapable, follow_up_info, assume_two_step, path_trace_enabled, * and the tighter neighborPropDelayThresh. */ -static bool emit_profile_globals(FILE *fp, const char *profile) +static bool emit_profile_globals(FILE *fp, const char *profile, bool hw_ts) { bool dot1as = profile && !strcmp(profile, "ieee802-dot1as"); @@ -53,7 +53,16 @@ static bool emit_profile_globals(FILE *fp, const char *profile) fprintf(fp, "follow_up_info 1\n"); fprintf(fp, "assume_two_step 1\n"); fprintf(fp, "path_trace_enabled 1\n"); - fprintf(fp, "neighborPropDelayThresh 800\n"); + /* + * 802.1AS P2P gate: if meanLinkDelay exceeds this threshold the + * port stays asCapable=false and never leaves LISTENING. 800 ns + * is the 802.1AS default and is appropriate only for hardware + * timestamping — software timestamps (QEMU, tap interfaces) can + * easily produce peer delays in the microsecond range, which + * would keep both ports stuck in LISTENING indefinitely. + */ + if (hw_ts) + fprintf(fp, "neighborPropDelayThresh 800\n"); } else { fprintf(fp, "transportSpecific 0\n"); } @@ -134,7 +143,7 @@ static const char *instance_time_stamping(struct lyd_node *inst, json_t *root) */ static int write_instance_conf(struct lyd_node *inst, json_t *root) { - const char *instance_type, *clock_type, *profile; + const char *instance_type, *clock_type, *profile, *ts; struct lyd_node *default_ds, *port, *port_ds, *servo; bool tc, bc, dot1as; char path[256]; @@ -159,7 +168,7 @@ static int write_instance_conf(struct lyd_node *inst, json_t *root) default_ds = lydx_get_child(inst, "default-ds"); instance_type = lydx_get_cattr(default_ds, "instance-type"); - profile = lydx_get_cattr(default_ds, "infix-ptp:profile"); + profile = lydx_get_cattr(default_ds, "profile"); clock_type = instance_type_to_clock_type(instance_type); tc = (clock_type != NULL); @@ -169,10 +178,11 @@ static int write_instance_conf(struct lyd_node *inst, json_t *root) fprintf(fp, "uds_address /var/run/ptp4l-%u\n", idx); /* Timestamping mode: hardware if all ports support it, software otherwise */ - fprintf(fp, "time_stamping %s\n", instance_time_stamping(inst, root)); + ts = instance_time_stamping(inst, root); + fprintf(fp, "time_stamping %s\n", ts); /* Profile — sets transportSpecific and all protocol-mandatory options */ - dot1as = emit_profile_globals(fp, profile); + dot1as = emit_profile_globals(fp, profile, !strcmp(ts, "hardware")); /* domainNumber */ v = lydx_get_cattr(default_ds, "domain-number"); diff --git a/test/case/all.yaml b/test/case/all.yaml index 3e75ace0..1fdbec3d 100644 --- a/test/case/all.yaml +++ b/test/case/all.yaml @@ -39,6 +39,9 @@ - name: "NTP Server" suite: ntp/all.yaml +- name: "PTP" + suite: ptp/all.yaml + - name: "Routing" suite: routing/all.yaml diff --git a/test/case/containers/basic/test.adoc b/test/case/containers/basic/test.adoc index e4e54362..7a55ccf3 100644 --- a/test/case/containers/basic/test.adoc +++ b/test/case/containers/basic/test.adoc @@ -1,4 +1,4 @@ -=== Container basic +=== Container Basic ifdef::topdoc[:imagesdir: {topdoc}../../test/case/containers/basic] @@ -12,7 +12,7 @@ The RPC actions: stop + start, and restart are also verified. ==== Topology -image::topology.svg[Container basic topology, align=center, scaledwidth=75%] +image::topology.svg[Container Basic topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/containers/bridge/test.adoc b/test/case/containers/bridge/test.adoc index a99dc091..28f8e458 100644 --- a/test/case/containers/bridge/test.adoc +++ b/test/case/containers/bridge/test.adoc @@ -1,4 +1,4 @@ -=== Container with bridge network +=== Container with Bridge Network ifdef::topdoc[:imagesdir: {topdoc}../../test/case/containers/bridge] @@ -13,7 +13,7 @@ port accessed from the host. ==== Topology -image::topology.svg[Container with bridge network topology, align=center, scaledwidth=75%] +image::topology.svg[Container with Bridge Network topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/containers/enabled/test.adoc b/test/case/containers/enabled/test.adoc index 574e2d93..d2eb4e80 100644 --- a/test/case/containers/enabled/test.adoc +++ b/test/case/containers/enabled/test.adoc @@ -1,4 +1,4 @@ -=== Container enabled/disabled +=== Container Enabled/Disabled ifdef::topdoc[:imagesdir: {topdoc}../../test/case/containers/enabled] @@ -15,7 +15,7 @@ Uses operational datastore to verify container running status. ==== Topology -image::topology.svg[Container enabled/disabled topology, align=center, scaledwidth=75%] +image::topology.svg[Container Enabled/Disabled topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/containers/environment/test.adoc b/test/case/containers/environment/test.adoc index 54b8378b..63eba8be 100644 --- a/test/case/containers/environment/test.adoc +++ b/test/case/containers/environment/test.adoc @@ -1,4 +1,4 @@ -=== Container environment variables +=== Container Environment Variables ifdef::topdoc[:imagesdir: {topdoc}../../test/case/containers/environment] @@ -15,7 +15,7 @@ changing an environment variable triggers a container restart. ==== Topology -image::topology.svg[Container environment variables topology, align=center, scaledwidth=75%] +image::topology.svg[Container Environment Variables topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/containers/phys/test.adoc b/test/case/containers/phys/test.adoc index 4a221454..9f522150 100644 --- a/test/case/containers/phys/test.adoc +++ b/test/case/containers/phys/test.adoc @@ -1,4 +1,4 @@ -=== Container with physical interface +=== Container with Physical Interface ifdef::topdoc[:imagesdir: {topdoc}../../test/case/containers/phys] @@ -9,7 +9,7 @@ given a physical interface instead of an end of a VETH pair. ==== Topology -image::topology.svg[Container with physical interface topology, align=center, scaledwidth=75%] +image::topology.svg[Container with Physical Interface topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/containers/veth/test.adoc b/test/case/containers/veth/test.adoc index dd557d04..7adb242e 100644 --- a/test/case/containers/veth/test.adoc +++ b/test/case/containers/veth/test.adoc @@ -1,4 +1,4 @@ -=== Container with VETH pair +=== Container with VETH Pair ifdef::topdoc[:imagesdir: {topdoc}../../test/case/containers/veth] @@ -19,7 +19,7 @@ regular bridge, a VETH pair connects the container to the bridge. ==== Topology -image::topology.svg[Container with VETH pair topology, align=center, scaledwidth=75%] +image::topology.svg[Container with VETH Pair topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/dhcp/client_routes/test.adoc b/test/case/dhcp/client_routes/test.adoc index 5a35e4f4..2bd6e2d0 100644 --- a/test/case/dhcp/client_routes/test.adoc +++ b/test/case/dhcp/client_routes/test.adoc @@ -1,4 +1,4 @@ -=== DHCP option 121 vs option 3 +=== DHCP Option 121 vs Option 3 ifdef::topdoc[:imagesdir: {topdoc}../../test/case/dhcp/client_routes] @@ -20,7 +20,7 @@ via 192.168.0.1 and a default route (option 121) via 192.168.0.254. ==== Topology -image::topology.svg[DHCP option 121 vs option 3 topology, align=center, scaledwidth=75%] +image::topology.svg[DHCP Option 121 vs Option 3 topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/hardware/gps_simple/test.adoc b/test/case/hardware/gps_simple/test.adoc index 2715ce0f..ab3a8d40 100644 --- a/test/case/hardware/gps_simple/test.adoc +++ b/test/case/hardware/gps_simple/test.adoc @@ -1,4 +1,4 @@ -=== GPS receiver basic test +=== GPS Receiver Basic Test ifdef::topdoc[:imagesdir: {topdoc}../../test/case/hardware/gps_simple] @@ -12,7 +12,7 @@ which appear as virtio serial ports inside the guest. ==== Topology -image::topology.svg[GPS receiver basic test topology, align=center, scaledwidth=75%] +image::topology.svg[GPS Receiver Basic Test topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/hardware/usb/test.adoc b/test/case/hardware/usb/test.adoc index 69d0c01f..5ce510ab 100644 --- a/test/case/hardware/usb/test.adoc +++ b/test/case/hardware/usb/test.adoc @@ -1,4 +1,4 @@ -=== USB configuration +=== USB Configuration ifdef::topdoc[:imagesdir: {topdoc}../../test/case/hardware/usb] @@ -16,7 +16,7 @@ port is handled correctly. ==== Topology -image::topology.svg[USB configuration topology, align=center, scaledwidth=75%] +image::topology.svg[USB Configuration topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/hardware/usb_two_ports/test.adoc b/test/case/hardware/usb_two_ports/test.adoc index 98d10f66..cb9e1f6b 100644 --- a/test/case/hardware/usb_two_ports/test.adoc +++ b/test/case/hardware/usb_two_ports/test.adoc @@ -1,4 +1,4 @@ -=== USB configuration with two USB ports +=== USB Configuration with Two USB Ports ifdef::topdoc[:imagesdir: {topdoc}../../test/case/hardware/usb_two_ports] @@ -9,7 +9,7 @@ when having two USB ports. ==== Topology -image::topology.svg[USB configuration with two USB ports topology, align=center, scaledwidth=75%] +image::topology.svg[USB Configuration with Two USB Ports topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/hardware/watchdog/test.adoc b/test/case/hardware/watchdog/test.adoc index ca904cd8..7f2fd15f 100644 --- a/test/case/hardware/watchdog/test.adoc +++ b/test/case/hardware/watchdog/test.adoc @@ -1,4 +1,4 @@ -=== Watchdog reset on system lockup +=== Watchdog Reset on System Lockup ifdef::topdoc[:imagesdir: {topdoc}../../test/case/hardware/watchdog] @@ -14,7 +14,7 @@ timeout. ==== Topology -image::topology.svg[Watchdog reset on system lockup topology, align=center, scaledwidth=75%] +image::topology.svg[Watchdog Reset on System Lockup topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/bridge_basic/test.adoc b/test/case/interfaces/bridge_basic/test.adoc index 58ee1c23..f943917a 100644 --- a/test/case/interfaces/bridge_basic/test.adoc +++ b/test/case/interfaces/bridge_basic/test.adoc @@ -1,4 +1,4 @@ -=== Bridge basic +=== Bridge Basic ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/bridge_basic] @@ -16,7 +16,7 @@ Test basic connectivity to a bridge ==== Topology -image::topology.svg[Bridge basic topology, align=center, scaledwidth=75%] +image::topology.svg[Bridge Basic topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/bridge_fwd_dual_dut/test.adoc b/test/case/interfaces/bridge_fwd_dual_dut/test.adoc index aa6f1e77..c5fa45d1 100644 --- a/test/case/interfaces/bridge_fwd_dual_dut/test.adoc +++ b/test/case/interfaces/bridge_fwd_dual_dut/test.adoc @@ -1,4 +1,4 @@ -=== Bridge forwarding dual DUTs +=== Bridge Forwarding Dual DUTs ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/bridge_fwd_dual_dut] @@ -28,7 +28,7 @@ Ping through two bridges on two different DUTs. ==== Topology -image::topology.svg[Bridge forwarding dual DUTs topology, align=center, scaledwidth=75%] +image::topology.svg[Bridge Forwarding Dual DUTs topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/bridge_fwd_sgl_dut/test.adoc b/test/case/interfaces/bridge_fwd_sgl_dut/test.adoc index 3469e71e..496d40f1 100644 --- a/test/case/interfaces/bridge_fwd_sgl_dut/test.adoc +++ b/test/case/interfaces/bridge_fwd_sgl_dut/test.adoc @@ -1,4 +1,4 @@ -=== Bridge forwarding single DUTs +=== Bridge Forwarding Single DUTs ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/bridge_fwd_sgl_dut] @@ -28,7 +28,7 @@ Tests forwarding through a DUT with two bridged interfaces on one DUT. ==== Topology -image::topology.svg[Bridge forwarding single DUTs topology, align=center, scaledwidth=75%] +image::topology.svg[Bridge Forwarding Single DUTs topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/bridge_veth/test.adoc b/test/case/interfaces/bridge_veth/test.adoc index b3826e90..acf0a2be 100644 --- a/test/case/interfaces/bridge_veth/test.adoc +++ b/test/case/interfaces/bridge_veth/test.adoc @@ -1,4 +1,4 @@ -=== Bridge with a physical port and a veth +=== Bridge with a Physical Port and a Veth ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/bridge_veth] @@ -18,7 +18,7 @@ PING --> br0 ==== Topology -image::topology.svg[Bridge with a physical port and a veth topology, align=center, scaledwidth=75%] +image::topology.svg[Bridge with a Physical Port and a Veth topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/bridge_vlan_separation/test.adoc b/test/case/interfaces/bridge_vlan_separation/test.adoc index ae06a6e1..cf328257 100644 --- a/test/case/interfaces/bridge_vlan_separation/test.adoc +++ b/test/case/interfaces/bridge_vlan_separation/test.adoc @@ -1,4 +1,4 @@ -=== Bridge VLAN separation +=== Bridge VLAN Separation ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/bridge_vlan_separation] @@ -27,7 +27,7 @@ Test that two VLANs are correctly separated in the bridge ==== Topology -image::topology.svg[Bridge VLAN separation topology, align=center, scaledwidth=75%] +image::topology.svg[Bridge VLAN Separation topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/dual_bridge/test.adoc b/test/case/interfaces/dual_bridge/test.adoc index 440e6b11..913a2aff 100644 --- a/test/case/interfaces/dual_bridge/test.adoc +++ b/test/case/interfaces/dual_bridge/test.adoc @@ -1,4 +1,4 @@ -=== Dual bridges on one device +=== Dual Bridges on One Device ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/dual_bridge] @@ -15,7 +15,7 @@ PC - target:data veth0a - veth0b ==== Topology -image::topology.svg[Dual bridges on one device topology, align=center, scaledwidth=75%] +image::topology.svg[Dual Bridges on One Device topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/iface_enable_disable/test.adoc b/test/case/interfaces/iface_enable_disable/test.adoc index ba0abf80..05ed3f83 100644 --- a/test/case/interfaces/iface_enable_disable/test.adoc +++ b/test/case/interfaces/iface_enable_disable/test.adoc @@ -1,4 +1,4 @@ -=== Interface status +=== Interface Status ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/iface_enable_disable] @@ -11,7 +11,7 @@ Both admin-status and oper-status are verified. ==== Topology -image::topology.svg[Interface status topology, align=center, scaledwidth=75%] +image::topology.svg[Interface Status topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/iface_phys_address/test.adoc b/test/case/interfaces/iface_phys_address/test.adoc index a78817a9..20751240 100644 --- a/test/case/interfaces/iface_phys_address/test.adoc +++ b/test/case/interfaces/iface_phys_address/test.adoc @@ -1,4 +1,4 @@ -=== Custom MAC address on interface +=== Custom MAC Address on Interface ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/iface_phys_address] @@ -10,7 +10,7 @@ an offset applied. ==== Topology -image::topology.svg[Custom MAC address on interface topology, align=center, scaledwidth=75%] +image::topology.svg[Custom MAC Address on Interface topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/igmp_basic/test.adoc b/test/case/interfaces/igmp_basic/test.adoc index 6a7c51d5..b1015c5e 100644 --- a/test/case/interfaces/igmp_basic/test.adoc +++ b/test/case/interfaces/igmp_basic/test.adoc @@ -1,4 +1,4 @@ -=== IGMP basic +=== IGMP Basic ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/igmp_basic] @@ -33,7 +33,7 @@ the group. ==== Topology -image::topology.svg[IGMP basic topology, align=center, scaledwidth=75%] +image::topology.svg[IGMP Basic topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/ipv6_address/test.adoc b/test/case/interfaces/ipv6_address/test.adoc index 048e1c39..810bb0d6 100644 --- a/test/case/interfaces/ipv6_address/test.adoc +++ b/test/case/interfaces/ipv6_address/test.adoc @@ -1,4 +1,4 @@ -=== Interface IPv6 autoconf for bridges +=== Interface IPv6 Autoconf for Bridges ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/ipv6_address] @@ -9,7 +9,7 @@ See issue #473 for details. ==== Topology -image::topology.svg[Interface IPv6 autoconf for bridges topology, align=center, scaledwidth=75%] +image::topology.svg[Interface IPv6 Autoconf for Bridges topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/lag_basic/test.adoc b/test/case/interfaces/lag_basic/test.adoc index 443928d8..ff34995c 100644 --- a/test/case/interfaces/lag_basic/test.adoc +++ b/test/case/interfaces/lag_basic/test.adoc @@ -1,4 +1,4 @@ -=== Ling Aggregation Basic +=== Link Aggregation Basic ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/lag_basic] @@ -15,7 +15,7 @@ each test step using the `mon` interface. ==== Topology -image::topology.svg[Ling Aggregation Basic topology, align=center, scaledwidth=75%] +image::topology.svg[Link Aggregation Basic topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/routing_basic/test.adoc b/test/case/interfaces/routing_basic/test.adoc index 1f896230..f802b81d 100644 --- a/test/case/interfaces/routing_basic/test.adoc +++ b/test/case/interfaces/routing_basic/test.adoc @@ -1,4 +1,4 @@ -=== Routing basic +=== Routing Basic ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/routing_basic] @@ -12,7 +12,7 @@ expected to be lost. ==== Topology -image::topology.svg[Routing basic topology, align=center, scaledwidth=75%] +image::topology.svg[Routing Basic topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/static_multicast_filters/test.adoc b/test/case/interfaces/static_multicast_filters/test.adoc index e36f9156..9b297c03 100644 --- a/test/case/interfaces/static_multicast_filters/test.adoc +++ b/test/case/interfaces/static_multicast_filters/test.adoc @@ -1,4 +1,4 @@ -=== Static multicast filters +=== Static Multicast Filters ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/static_multicast_filters] @@ -24,7 +24,7 @@ enabled when using static multicast filters) ==== Topology -image::topology.svg[Static multicast filters topology, align=center, scaledwidth=75%] +image::topology.svg[Static Multicast Filters topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/tunnel_basic/gre.adoc b/test/case/interfaces/tunnel_basic/gre.adoc index 745f8ec9..83b4cc35 100644 --- a/test/case/interfaces/tunnel_basic/gre.adoc +++ b/test/case/interfaces/tunnel_basic/gre.adoc @@ -1,4 +1,4 @@ -=== GRE point-to-point +=== GRE Point-to-Point ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/tunnel_basic] @@ -14,7 +14,7 @@ connectivity with the second DUT through the tunnel. ==== Topology -image::topology.svg[GRE point-to-point topology, align=center, scaledwidth=75%] +image::topology.svg[GRE Point-to-Point topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/tunnel_basic/gretap.adoc b/test/case/interfaces/tunnel_basic/gretap.adoc index b358f766..3701a654 100644 --- a/test/case/interfaces/tunnel_basic/gretap.adoc +++ b/test/case/interfaces/tunnel_basic/gretap.adoc @@ -1,4 +1,4 @@ -=== GRETAP point-to-point +=== GRETAP Point-to-Point ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/tunnel_basic] @@ -14,7 +14,7 @@ connectivity with the second DUT through the tunnel. ==== Topology -image::topology.svg[GRETAP point-to-point topology, align=center, scaledwidth=75%] +image::topology.svg[GRETAP Point-to-Point topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/tunnel_basic/vxlan.adoc b/test/case/interfaces/tunnel_basic/vxlan.adoc index 3b4cba0f..dab24773 100644 --- a/test/case/interfaces/tunnel_basic/vxlan.adoc +++ b/test/case/interfaces/tunnel_basic/vxlan.adoc @@ -1,4 +1,4 @@ -=== VXLAN point-to-point +=== VXLAN Point-to-Point ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/tunnel_basic] @@ -14,7 +14,7 @@ connectivity with the second DUT through the tunnel. ==== Topology -image::topology.svg[VXLAN point-to-point topology, align=center, scaledwidth=75%] +image::topology.svg[VXLAN Point-to-Point topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/tunnel_bridged/gretap.adoc b/test/case/interfaces/tunnel_bridged/gretap.adoc index e664f3f8..51ab4391 100644 --- a/test/case/interfaces/tunnel_bridged/gretap.adoc +++ b/test/case/interfaces/tunnel_bridged/gretap.adoc @@ -1,4 +1,4 @@ -=== GRETAP bridged with physical interface +=== GRETAP Bridged with Physical Interface ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/tunnel_bridged] @@ -13,7 +13,7 @@ first DUT. On host, verify connectivity with the second DUT through tunnel. ==== Topology -image::topology.svg[GRETAP bridged with physical interface topology, align=center, scaledwidth=75%] +image::topology.svg[GRETAP Bridged with Physical Interface topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/tunnel_bridged/vxlan.adoc b/test/case/interfaces/tunnel_bridged/vxlan.adoc index d6a463d6..c4531383 100644 --- a/test/case/interfaces/tunnel_bridged/vxlan.adoc +++ b/test/case/interfaces/tunnel_bridged/vxlan.adoc @@ -1,4 +1,4 @@ -=== VXLAN bridged with physical interface +=== VXLAN Bridged with Physical Interface ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/tunnel_bridged] @@ -13,7 +13,7 @@ first DUT. On host, verify connectivity with the second DUT through tunnel. ==== Topology -image::topology.svg[VXLAN bridged with physical interface topology, align=center, scaledwidth=75%] +image::topology.svg[VXLAN Bridged with Physical Interface topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/tunnel_ttl/gre.adoc b/test/case/interfaces/tunnel_ttl/gre.adoc index fcbfa296..10f47106 100644 --- a/test/case/interfaces/tunnel_ttl/gre.adoc +++ b/test/case/interfaces/tunnel_ttl/gre.adoc @@ -1,4 +1,4 @@ -=== GRE Tunnel TTL verification +=== GRE Tunnel TTL Verification ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/tunnel_ttl] @@ -18,7 +18,7 @@ many hops and the TTL would reach zero before the last routing step.) ==== Topology -image::topology.svg[GRE Tunnel TTL verification topology, align=center, scaledwidth=75%] +image::topology.svg[GRE Tunnel TTL Verification topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/tunnel_ttl/vxlan.adoc b/test/case/interfaces/tunnel_ttl/vxlan.adoc index 022df7c7..ca2f5a6f 100644 --- a/test/case/interfaces/tunnel_ttl/vxlan.adoc +++ b/test/case/interfaces/tunnel_ttl/vxlan.adoc @@ -1,4 +1,4 @@ -=== VXLAN Tunnel TTL verification +=== VXLAN Tunnel TTL Verification ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/tunnel_ttl] @@ -18,7 +18,7 @@ many hops and the TTL would reach zero before the last routing step.) ==== Topology -image::topology.svg[VXLAN Tunnel TTL verification topology, align=center, scaledwidth=75%] +image::topology.svg[VXLAN Tunnel TTL Verification topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/verify_all_interface_types/test.adoc b/test/case/interfaces/verify_all_interface_types/test.adoc index fc15c7d0..e82bbbaf 100644 --- a/test/case/interfaces/verify_all_interface_types/test.adoc +++ b/test/case/interfaces/verify_all_interface_types/test.adoc @@ -1,4 +1,4 @@ -=== Verify that all interface types can be created +=== Verify that All Interface Types Can Be Created ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/verify_all_interface_types] @@ -20,7 +20,7 @@ slightly longer than sending the entire configuration at once. ==== Topology -image::topology.svg[Verify that all interface types can be created topology, align=center, scaledwidth=75%] +image::topology.svg[Verify that All Interface Types Can Be Created topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/veth_delete/test.adoc b/test/case/interfaces/veth_delete/test.adoc index 95b9992e..372faa71 100644 --- a/test/case/interfaces/veth_delete/test.adoc +++ b/test/case/interfaces/veth_delete/test.adoc @@ -1,4 +1,4 @@ -=== Verify that VETH pairs can be deleted +=== Verify that VETH Pairs Can Be Deleted ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/veth_delete] @@ -14,7 +14,7 @@ from any other step. This to trigger a new configuration "generation". ==== Topology -image::topology.svg[Verify that VETH pairs can be deleted topology, align=center, scaledwidth=75%] +image::topology.svg[Verify that VETH Pairs Can Be Deleted topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/vlan_ping/test.adoc b/test/case/interfaces/vlan_ping/test.adoc index 2b4a211b..87d91df0 100644 --- a/test/case/interfaces/vlan_ping/test.adoc +++ b/test/case/interfaces/vlan_ping/test.adoc @@ -1,4 +1,4 @@ -=== VLAN ping connectivity +=== VLAN Ping Connectivity ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/vlan_ping] @@ -8,7 +8,7 @@ Very basic test if the VLAN interface configuration works. ==== Topology -image::topology.svg[VLAN ping connectivity topology, align=center, scaledwidth=75%] +image::topology.svg[VLAN Ping Connectivity topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/interfaces/wireguard_multipoint/test.adoc b/test/case/interfaces/wireguard_multipoint/test.adoc index 265ac3fe..14e60dbc 100644 --- a/test/case/interfaces/wireguard_multipoint/test.adoc +++ b/test/case/interfaces/wireguard_multipoint/test.adoc @@ -1,4 +1,4 @@ -=== WireGuard multipoint +=== WireGuard Multipoint ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/wireguard_multipoint] @@ -41,7 +41,7 @@ Security boundaries: ==== Topology -image::topology.svg[WireGuard multipoint topology, align=center, scaledwidth=75%] +image::topology.svg[WireGuard Multipoint topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/misc/operational_all/test.adoc b/test/case/misc/operational_all/test.adoc index c4e26396..a50ce7df 100644 --- a/test/case/misc/operational_all/test.adoc +++ b/test/case/misc/operational_all/test.adoc @@ -1,4 +1,4 @@ -=== Get operational +=== Get Operational ifdef::topdoc[:imagesdir: {topdoc}../../test/case/misc/operational_all] @@ -8,7 +8,7 @@ Basic test just to get operational from test-config without errors. ==== Topology -image::topology.svg[Get operational topology, align=center, scaledwidth=75%] +image::topology.svg[Get Operational topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/misc/support_collect/test.adoc b/test/case/misc/support_collect/test.adoc index c3d4f6a6..70dd100f 100644 --- a/test/case/misc/support_collect/test.adoc +++ b/test/case/misc/support_collect/test.adoc @@ -1,4 +1,4 @@ -=== Support data collection +=== Support Data Collection ifdef::topdoc[:imagesdir: {topdoc}../../test/case/misc/support_collect] @@ -10,7 +10,7 @@ encryption (when available on target). ==== Topology -image::topology.svg[Support data collection topology, align=center, scaledwidth=75%] +image::topology.svg[Support Data Collection topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/ntp/client_stratum_selection/test.adoc b/test/case/ntp/client_stratum_selection/test.adoc index 56f5ebc5..07a9ecc3 100644 --- a/test/case/ntp/client_stratum_selection/test.adoc +++ b/test/case/ntp/client_stratum_selection/test.adoc @@ -1,4 +1,4 @@ -=== NTP client stratum selection +=== NTP Client Stratum Selection ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ntp/client_stratum_selection] @@ -20,7 +20,7 @@ should then select srv1 (lower stratum) as its sync source. ==== Topology -image::topology.svg[NTP client stratum selection topology, align=center, scaledwidth=75%] +image::topology.svg[NTP Client Stratum Selection topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/ntp/server_client/test.adoc b/test/case/ntp/server_client/test.adoc index abeb0a33..0f271de6 100644 --- a/test/case/ntp/server_client/test.adoc +++ b/test/case/ntp/server_client/test.adoc @@ -1,4 +1,4 @@ -=== NTP server and client interoperability +=== NTP Server and Client Interoperability ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ntp/server_client] @@ -14,7 +14,7 @@ Verify NTP server and client work together: ==== Topology -image::topology.svg[NTP server and client interoperability topology, align=center, scaledwidth=75%] +image::topology.svg[NTP Server and Client Interoperability topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/ntp/server_mode_peer/test.adoc b/test/case/ntp/server_mode_peer/test.adoc index 2469b671..87c02db1 100644 --- a/test/case/ntp/server_mode_peer/test.adoc +++ b/test/case/ntp/server_mode_peer/test.adoc @@ -1,4 +1,4 @@ -=== NTP peer mode +=== NTP Peer Mode ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ntp/server_mode_peer] @@ -22,7 +22,7 @@ selected as sync source by the other peer. ==== Topology -image::topology.svg[NTP peer mode topology, align=center, scaledwidth=75%] +image::topology.svg[NTP Peer Mode topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/ntp/server_mode_server/test.adoc b/test/case/ntp/server_mode_server/test.adoc index e4cf34eb..0169a2e8 100644 --- a/test/case/ntp/server_mode_server/test.adoc +++ b/test/case/ntp/server_mode_server/test.adoc @@ -1,4 +1,4 @@ -=== NTP server mode +=== NTP Server Mode ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ntp/server_mode_server] @@ -18,7 +18,7 @@ The test verifies both servers operate correctly and serve accurate time. ==== Topology -image::topology.svg[NTP server mode topology, align=center, scaledwidth=75%] +image::topology.svg[NTP Server Mode topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/ntp/server_mode_standalone/test.adoc b/test/case/ntp/server_mode_standalone/test.adoc index 4f7cef5c..8255e113 100644 --- a/test/case/ntp/server_mode_standalone/test.adoc +++ b/test/case/ntp/server_mode_standalone/test.adoc @@ -1,4 +1,4 @@ -=== NTP server standalone mode +=== NTP Server Standalone Mode ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ntp/server_mode_standalone] @@ -12,7 +12,7 @@ syncing from any upstream sources. ==== Topology -image::topology.svg[NTP server standalone mode topology, align=center, scaledwidth=75%] +image::topology.svg[NTP Server Standalone Mode topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/ptp/Readme.adoc b/test/case/ptp/Readme.adoc new file mode 100644 index 00000000..f47f2f3a --- /dev/null +++ b/test/case/ptp/Readme.adoc @@ -0,0 +1,46 @@ +:testgroup: +== PTP Tests + +Tests for PTP (IEEE 1588 Precision Time Protocol) functionality across +different clock types and operational scenarios: + + - Basic smoke test: full stack end-to-end + - BTCA: grandmaster election and runtime re-election via `priority1` + - Boundary Clock: two-port BC forwarding time with `steps-removed` accounting + - Transparent Clock: E2E and P2P TC on hardware-timestamping nodes + - Port fault recovery: link-down/link-up state machine and re-convergence + +The offset convergence threshold in the tests varies with the timestamping +capability of the nodes under test: + +[cols="1,1,3",options="header"] +|=== +| Timestamping | Default Threshold | Typical scenario +| Software | 100 000 ns | Virtual machines, QEMU tap interfaces +| Hardware | 1 000 ns | Nodes with PHC hardware timestamping +|=== + +Tests that accept a `--threshold-ns` option may use that value instead. +When no option is given, the threshold is selected automatically based +on the `ptp-hwts` capability of the relevant node in the physical test +system's topology file. + +<<< + +include::basic/Readme.adoc[] + +<<< + +include::bmca/Readme.adoc[] + +<<< + +include::boundary_clock/Readme.adoc[] + +<<< + +include::transparent_clock/Readme.adoc[] + +<<< + +include::port_recovery/Readme.adoc[] diff --git a/test/case/ptp/all.yaml b/test/case/ptp/all.yaml new file mode 100644 index 00000000..a86f2885 --- /dev/null +++ b/test/case/ptp/all.yaml @@ -0,0 +1,18 @@ +--- +- name: PTP basic + suite: basic/test.yaml + +- name: PTP BTCA grandmaster election + suite: bmca/test.yaml + +- name: PTP boundary clock + suite: boundary_clock/test.yaml + +- name: PTP transparent clock + suite: transparent_clock/test.yaml + +- name: PTP port fault recovery + case: port_recovery/test.py + +- name: PTP servo step-threshold + case: servo/test.py diff --git a/test/case/ptp/basic/Readme.adoc b/test/case/ptp/basic/Readme.adoc new file mode 100644 index 00000000..cb3079ad --- /dev/null +++ b/test/case/ptp/basic/Readme.adoc @@ -0,0 +1,6 @@ +include::ieee1588.adoc[] + +<<< + +include::ieee802dot1as.adoc[] + diff --git a/test/case/ptp/basic/ieee1588.adoc b/test/case/ptp/basic/ieee1588.adoc new file mode 100644 index 00000000..bfd582fe --- /dev/null +++ b/test/case/ptp/basic/ieee1588.adoc @@ -0,0 +1,28 @@ +=== PTP basic (IEEE 1588) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/basic] + +==== Description + +Verify basic PTP operation end-to-end: clock configuration, port state +transitions, and clock servo convergence. + +Two Ordinary Clocks are connected back-to-back. The grandmaster is +configured with `priority1=1` so it always wins the BTCA election; the +time receiver is configured with `time-receiver-only` so it never +attempts to become grandmaster. The test is run once per supported +profile, covering both IEEE 1588-2019 (UDP/IPv4, E2E) and IEEE 802.1AS +(Layer 2, P2P). + +==== Topology + +image::topology.svg[PTP basic (IEEE 1588) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (OC, ieee1588, priority1=1) and time receiver (ieee1588, priority1=128, client-only) +. Wait for grandmaster and time receiver ports to reach active states +. Wait for time receiver offset to converge + + diff --git a/test/case/ptp/basic/ieee1588.py b/test/case/ptp/basic/ieee1588.py new file mode 120000 index 00000000..94656643 --- /dev/null +++ b/test/case/ptp/basic/ieee1588.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/test/case/ptp/basic/ieee802dot1as.adoc b/test/case/ptp/basic/ieee802dot1as.adoc new file mode 100644 index 00000000..3fbc7a1b --- /dev/null +++ b/test/case/ptp/basic/ieee802dot1as.adoc @@ -0,0 +1,28 @@ +=== PTP basic (IEEE 802.1AS) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/basic] + +==== Description + +Verify basic PTP operation end-to-end: clock configuration, port state +transitions, and clock servo convergence. + +Two Ordinary Clocks are connected back-to-back. The grandmaster is +configured with `priority1=1` so it always wins the BTCA election; the +time receiver is configured with `time-receiver-only` so it never +attempts to become grandmaster. The test is run once per supported +profile, covering both IEEE 1588-2019 (UDP/IPv4, E2E) and IEEE 802.1AS +(Layer 2, P2P). + +==== Topology + +image::topology.svg[PTP basic (IEEE 802.1AS) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (OC, ieee802-dot1as, priority1=1) and time receiver (ieee802-dot1as, priority1=128, client-only) +. Wait for grandmaster and time receiver ports to reach active states +. Wait for time receiver offset to converge + + diff --git a/test/case/ptp/basic/ieee802dot1as.py b/test/case/ptp/basic/ieee802dot1as.py new file mode 120000 index 00000000..94656643 --- /dev/null +++ b/test/case/ptp/basic/ieee802dot1as.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/test/case/ptp/basic/test.adoc b/test/case/ptp/basic/test.adoc new file mode 100644 index 00000000..3b02110b --- /dev/null +++ b/test/case/ptp/basic/test.adoc @@ -0,0 +1,29 @@ +=== PTP basic + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/basic] + +==== Description + +Verify basic PTP operation end-to-end: clock configuration, port state +transitions, and clock servo convergence. + +Two Ordinary Clocks are connected back-to-back. The grandmaster is +configured with `priority1=1` so it always wins the BTCA election; the +time receiver is configured with `time-receiver-only` so it never +attempts to become grandmaster. Software timestamping is used, making +the test suitable for virtual machines as well as real hardware. + +This is the smoke test: if it passes, the full PTP stack is working. + +==== Topology + +image::topology.svg[PTP basic topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (priority1=1) and time receiver (priority1=128, client-only) +. Wait for grandmaster and time receiver ports to reach active states +. Wait for time receiver offset to converge + + diff --git a/test/case/ptp/basic/test.py b/test/case/ptp/basic/test.py new file mode 100755 index 00000000..6d8b50b4 --- /dev/null +++ b/test/case/ptp/basic/test.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""PTP basic + +Verify basic PTP operation end-to-end: clock configuration, port state +transitions, and clock servo convergence. + +Two Ordinary Clocks are connected back-to-back. The grandmaster is +configured with `priority1=1` so it always wins the BTCA election; the +time receiver is configured with `time-receiver-only` so it never +attempts to become grandmaster. The test is run once per supported +profile, covering both IEEE 1588-2019 (UDP/IPv4, E2E) and IEEE 802.1AS +(Layer 2, P2P). +""" + +import infamy +import infamy.ptp as ptp +from infamy import until +from infamy.util import parallel + + +class ArgumentParser(infamy.ArgumentParser): + def __init__(self): + super().__init__() + self.args.add_argument("--profile", default="ieee1588", + choices=["ieee1588", "ieee802-dot1as"]) + self.args.add_argument("--threshold-ns", type=int, default=None) + + +def configure_oc(iface, priority1, client_only, profile, ip=None): + config = { + "ieee1588-ptp-tt": { + "ptp": { + "instances": { + "instance": [{ + "instance-index": 0, + "default-ds": { + "instance-type": "oc", + "domain-number": 0, + "priority1": priority1, + "priority2": 128, + "infix-ptp:profile": profile, + "time-receiver-only": client_only, + }, + "ports": { + "port": [{ + "port-index": 1, + "underlying-interface": iface, + }] + } + }] + } + } + } + } + + # Always enable the underlying interface — ptp4l needs it up regardless + # of profile. IEEE 1588 also needs an IPv4 address for UDP transport. + iface_cfg = {"name": iface, "enabled": True} + if profile == "ieee1588": + iface_cfg["ipv4"] = {"address": [{"ip": ip, "prefix-length": 30}]} + config["ietf-interfaces"] = { + "interfaces": {"interface": [iface_cfg]} + } + + # Fast timers: 250 ms announce/sync intervals speed up port state transitions + # and convergence compared to the 1 s defaults. + port_ds = { + "log-announce-interval": -2, + "announce-receipt-timeout": 2, + "log-sync-interval": -2, + } + if profile == "ieee1588": + port_ds["delay-mechanism"] = "e2e" + config["ieee1588-ptp-tt"]["ptp"]["instances"]["instance"][0] \ + ["ports"]["port"][0]["port-ds"] = port_ds + + return config + + +with infamy.Test() as test: + with test.step("Set up topology and attach to DUTs"): + arg = ArgumentParser() + env = infamy.Env(args=arg) + profile = env.args.profile + gm = env.attach("gm", "mgmt") + receiver = env.attach("receiver", "mgmt") + + _, gm_iface = env.ltop.xlate("gm", "data") + _, receiver_iface = env.ltop.xlate("receiver", "data") + threshold_ns = env.args.threshold_ns or ptp.default_threshold(env, "receiver") + + with test.step(f"Configure grandmaster (OC, {profile}, priority1=1) and time receiver ({profile}, priority1=128, client-only)"): + gm.put_config_dicts(configure_oc(gm_iface, priority1=1, + client_only=False, profile=profile, + ip="192.168.100.1")) + receiver.put_config_dicts(configure_oc(receiver_iface, priority1=128, + client_only=True, profile=profile, + ip="192.168.100.2")) + + with test.step("Wait for grandmaster and time receiver ports to reach active states"): + parallel(lambda: until(lambda: ptp.is_time_transmitter(gm), attempts=60), + lambda: until(lambda: ptp.is_time_receiver(receiver), attempts=60)) + + with test.step("Wait for time receiver offset to converge"): + until(lambda: ptp.has_converged(receiver, threshold_ns), attempts=120) + + test.succeed() diff --git a/test/case/ptp/basic/test.yaml b/test/case/ptp/basic/test.yaml new file mode 100644 index 00000000..4a4424f8 --- /dev/null +++ b/test/case/ptp/basic/test.yaml @@ -0,0 +1,11 @@ +--- +- settings: + test-spec: .adoc + +- name: PTP basic (IEEE 1588) + case: ieee1588.py + opts: ["--profile", "ieee1588"] + +- name: PTP basic (IEEE 802.1AS) + case: ieee802dot1as.py + opts: ["--profile", "ieee802-dot1as"] diff --git a/test/case/ptp/basic/topology.dot b/test/case/ptp/basic/topology.dot new file mode 100644 index 00000000..efa656b6 --- /dev/null +++ b/test/case/ptp/basic/topology.dot @@ -0,0 +1,33 @@ +graph "ptp-basic" { + layout="neato"; + overlap="false"; + esep="+22"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="{ mgmt1 | \n\nhost\n\n\n | mgmt2 }", + pos="0,15!", + requires="controller", + ]; + + gm [ + label="{ mgmt | data } | { gm\npriority1=1 }", + pos="2,15.25!", + fontsize=12, + requires="infix", + ]; + + receiver [ + label="{ data | mgmt } | { receiver\npriority1=128 }", + pos="2,14.75!", + fontsize=12, + requires="infix", + ]; + + host:mgmt1 -- gm:mgmt [requires="mgmt", color="lightgray"] + host:mgmt2 -- receiver:mgmt [requires="mgmt", color="lightgray"] + + gm:data -- receiver:data [label="\n\n192.168.100.0/30 ", dir="both"] +} diff --git a/test/case/ptp/basic/topology.svg b/test/case/ptp/basic/topology.svg new file mode 100644 index 00000000..92ee2de2 --- /dev/null +++ b/test/case/ptp/basic/topology.svg @@ -0,0 +1,62 @@ + + + + + + +ptp-basic + + + +host + +mgmt1 + +host + +mgmt2 + + + +gm + +mgmt + +data + +gm +priority1=1 + + + +host:mgmt1--gm:mgmt + + + + +receiver + +data + +mgmt + +receiver +priority1=128 + + + +host:mgmt2--receiver:mgmt + + + + +gm:data--receiver:data + + + +192.168.100.0/30   + + + diff --git a/test/case/ptp/bmca/Readme.adoc b/test/case/ptp/bmca/Readme.adoc new file mode 100644 index 00000000..cb3079ad --- /dev/null +++ b/test/case/ptp/bmca/Readme.adoc @@ -0,0 +1,6 @@ +include::ieee1588.adoc[] + +<<< + +include::ieee802dot1as.adoc[] + diff --git a/test/case/ptp/bmca/ieee1588.adoc b/test/case/ptp/bmca/ieee1588.adoc new file mode 100644 index 00000000..8ec36caf --- /dev/null +++ b/test/case/ptp/bmca/ieee1588.adoc @@ -0,0 +1,39 @@ +=== PTP BTCA grandmaster election (IEEE 1588) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/bmca] + +==== Description + +Verify that the Best TimeTransmitter Clock Algorithm (BTCA) selects the clock +with the lowest `priority1` as grandmaster, and that a change of `priority1` +at runtime triggers a new election with the correct result. + +Two Ordinary Clocks are connected back-to-back. Both announce themselves as +potential grandmasters. In round one, *alpha* holds `priority1=1` and wins +the election; *beta* (`priority1=128`) becomes the time receiver. In round +two, *alpha* is reconfigured to priority1=200 without restarting; the BTCA +re-runs and beta wins, becoming the new grandmaster. The test verifies that +alpha's `parent-ds` `grandmaster-identity` changes to beta's `clock-identity`, +confirming that the re-election is reflected in the operational datastore. + +Announce intervals are reduced to 250 ms (`log-announce-interval -2`) and the +announce receipt timeout to 2 intervals (500 ms) to make re-election complete +in roughly one second rather than the default three. + +The test is run for both IEEE 1588-2019 (UDP/IPv4, E2E) and IEEE 802.1AS +(Layer 2, P2P) profiles. + +==== Topology + +image::topology.svg[PTP BTCA grandmaster election (IEEE 1588) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure both DUTs (ieee1588); alpha has lower priority1 +. Verify initial election: alpha is grandmaster, beta is time receiver +. Reconfigure alpha with worse priority1=200 +. Verify beta wins re-election (is own grandmaster) +. Verify alpha tracks beta as grandmaster + + diff --git a/test/case/ptp/bmca/ieee1588.py b/test/case/ptp/bmca/ieee1588.py new file mode 120000 index 00000000..94656643 --- /dev/null +++ b/test/case/ptp/bmca/ieee1588.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/test/case/ptp/bmca/ieee802dot1as.adoc b/test/case/ptp/bmca/ieee802dot1as.adoc new file mode 100644 index 00000000..625efadb --- /dev/null +++ b/test/case/ptp/bmca/ieee802dot1as.adoc @@ -0,0 +1,39 @@ +=== PTP BTCA grandmaster election (IEEE 802.1AS) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/bmca] + +==== Description + +Verify that the Best TimeTransmitter Clock Algorithm (BTCA) selects the clock +with the lowest `priority1` as grandmaster, and that a change of `priority1` +at runtime triggers a new election with the correct result. + +Two Ordinary Clocks are connected back-to-back. Both announce themselves as +potential grandmasters. In round one, *alpha* holds `priority1=1` and wins +the election; *beta* (`priority1=128`) becomes the time receiver. In round +two, *alpha* is reconfigured to priority1=200 without restarting; the BTCA +re-runs and beta wins, becoming the new grandmaster. The test verifies that +alpha's `parent-ds` `grandmaster-identity` changes to beta's `clock-identity`, +confirming that the re-election is reflected in the operational datastore. + +Announce intervals are reduced to 250 ms (`log-announce-interval -2`) and the +announce receipt timeout to 2 intervals (500 ms) to make re-election complete +in roughly one second rather than the default three. + +The test is run for both IEEE 1588-2019 (UDP/IPv4, E2E) and IEEE 802.1AS +(Layer 2, P2P) profiles. + +==== Topology + +image::topology.svg[PTP BTCA grandmaster election (IEEE 802.1AS) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure both DUTs (ieee802-dot1as); alpha has lower priority1 +. Verify initial election: alpha is grandmaster, beta is time receiver +. Reconfigure alpha with worse priority1=200 +. Verify beta wins re-election (is own grandmaster) +. Verify alpha tracks beta as grandmaster + + diff --git a/test/case/ptp/bmca/ieee802dot1as.py b/test/case/ptp/bmca/ieee802dot1as.py new file mode 120000 index 00000000..94656643 --- /dev/null +++ b/test/case/ptp/bmca/ieee802dot1as.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/test/case/ptp/bmca/test.adoc b/test/case/ptp/bmca/test.adoc new file mode 100644 index 00000000..1080fefc --- /dev/null +++ b/test/case/ptp/bmca/test.adoc @@ -0,0 +1,5 @@ +include::ieee1588.adoc[] + +<<< + +include::ieee802dot1as.adoc[] diff --git a/test/case/ptp/bmca/test.py b/test/case/ptp/bmca/test.py new file mode 100755 index 00000000..615ee60f --- /dev/null +++ b/test/case/ptp/bmca/test.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""PTP BTCA grandmaster election + +Verify that the Best TimeTransmitter Clock Algorithm (BTCA) selects the clock +with the lowest `priority1` as grandmaster, and that a change of `priority1` +at runtime triggers a new election with the correct result. + +Two Ordinary Clocks are connected back-to-back. Both announce themselves as +potential grandmasters. In round one, *alpha* holds `priority1=1` and wins +the election; *beta* (`priority1=128`) becomes the time receiver. In round +two, *alpha* is reconfigured to priority1=200 without restarting; the BTCA +re-runs and beta wins, becoming the new grandmaster. The test verifies that +alpha's `parent-ds` `grandmaster-identity` changes to beta's `clock-identity`, +confirming that the re-election is reflected in the operational datastore. + +Announce intervals are reduced to 250 ms (`log-announce-interval -2`) and the +announce receipt timeout to 2 intervals (500 ms) to make re-election complete +in roughly one second rather than the default three. + +The test is run for both IEEE 1588-2019 (UDP/IPv4, E2E) and IEEE 802.1AS +(Layer 2, P2P) profiles. +""" + +import infamy +import infamy.ptp as ptp +from infamy import until +from infamy.util import parallel + + +class ArgumentParser(infamy.ArgumentParser): + def __init__(self): + super().__init__() + self.args.add_argument("--profile", default="ieee1588", + choices=["ieee1588", "ieee802-dot1as"]) + + +def configure_oc(iface, priority1, profile, ip=None): + iface_cfg = {"name": iface, "enabled": True} + if profile == "ieee1588": + iface_cfg["ipv4"] = {"address": [{"ip": ip, "prefix-length": 30}]} + + port_ds = { + "log-announce-interval": -2, + "announce-receipt-timeout": 2, + "log-sync-interval": -2, + } + if profile == "ieee1588": + port_ds["delay-mechanism"] = "e2e" + + return { + "ietf-interfaces": { + "interfaces": {"interface": [iface_cfg]} + }, + "ieee1588-ptp-tt": { + "ptp": { + "instances": { + "instance": [{ + "instance-index": 0, + "default-ds": { + "instance-type": "oc", + "domain-number": 0, + "priority1": priority1, + "priority2": 128, + "infix-ptp:profile": profile, + "time-receiver-only": False, + }, + "ports": { + "port": [{ + "port-index": 1, + "underlying-interface": iface, + "port-ds": port_ds, + }] + } + }] + } + } + } + } + + +with infamy.Test() as test: + with test.step("Set up topology and attach to DUTs"): + arg = ArgumentParser() + env = infamy.Env(args=arg) + profile = env.args.profile + alpha = env.attach("alpha", "mgmt") + beta = env.attach("beta", "mgmt") + + _, if_alpha = env.ltop.xlate("alpha", "data") + _, if_beta = env.ltop.xlate("beta", "data") + + with test.step(f"Configure both DUTs ({profile}); alpha has lower priority1"): + alpha.put_config_dicts(configure_oc(if_alpha, priority1=1, + profile=profile, ip="192.168.100.1")) + beta.put_config_dicts(configure_oc(if_beta, priority1=128, + profile=profile, ip="192.168.100.2")) + + with test.step("Verify initial election: alpha is grandmaster, beta is time receiver"): + parallel(lambda: until(lambda: ptp.is_own_gm(alpha), attempts=60), + lambda: until(lambda: ptp.is_time_receiver(beta), attempts=60)) + + with test.step("Reconfigure alpha with worse priority1=200"): + alpha.put_config_dicts(configure_oc(if_alpha, priority1=200, + profile=profile, ip="192.168.100.1")) + + with test.step("Verify beta wins re-election (is own grandmaster)"): + until(lambda: ptp.is_own_gm(beta), attempts=30) + + with test.step("Verify alpha tracks beta as grandmaster"): + until(lambda: ptp.grandmaster_identity(alpha) == ptp.clock_identity(beta), + attempts=30) + + test.succeed() diff --git a/test/case/ptp/bmca/test.yaml b/test/case/ptp/bmca/test.yaml new file mode 100644 index 00000000..3246043c --- /dev/null +++ b/test/case/ptp/bmca/test.yaml @@ -0,0 +1,11 @@ +--- +- settings: + test-spec: .adoc + +- name: PTP BTCA grandmaster election (IEEE 1588) + case: ieee1588.py + opts: ["--profile", "ieee1588"] + +- name: PTP BTCA grandmaster election (IEEE 802.1AS) + case: ieee802dot1as.py + opts: ["--profile", "ieee802-dot1as"] diff --git a/test/case/ptp/bmca/topology.dot b/test/case/ptp/bmca/topology.dot new file mode 100644 index 00000000..1d9dcc79 --- /dev/null +++ b/test/case/ptp/bmca/topology.dot @@ -0,0 +1,33 @@ +graph "ptp-bmca" { + layout="neato"; + overlap="false"; + esep="+22"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="{ mgmt1 | \n\nhost\n\n\n | mgmt2 }", + pos="0,15!", + requires="controller", + ]; + + alpha [ + label="{ mgmt | data } | { alpha }", + pos="2,15.25!", + fontsize=12, + requires="infix", + ]; + + beta [ + label="{ data | mgmt } | { beta }", + pos="2,14.75!", + fontsize=12, + requires="infix", + ]; + + host:mgmt1 -- alpha:mgmt [requires="mgmt", color="lightgray"] + host:mgmt2 -- beta:mgmt [requires="mgmt", color="lightgray"] + + alpha:data -- beta:data [label="192.168.101.0/30 ", dir="both"] +} diff --git a/test/case/ptp/bmca/topology.svg b/test/case/ptp/bmca/topology.svg new file mode 100644 index 00000000..04f71505 --- /dev/null +++ b/test/case/ptp/bmca/topology.svg @@ -0,0 +1,60 @@ + + + + + + +ptp-bmca + + + +host + +mgmt1 + +host + +mgmt2 + + + +alpha + +mgmt + +data + +alpha + + + +host:mgmt1--alpha:mgmt + + + + +beta + +data + +mgmt + +beta + + + +host:mgmt2--beta:mgmt + + + + +alpha:data--beta:data + + + +192.168.101.0/30 + + + diff --git a/test/case/ptp/boundary_clock/Readme.adoc b/test/case/ptp/boundary_clock/Readme.adoc new file mode 100644 index 00000000..cb3079ad --- /dev/null +++ b/test/case/ptp/boundary_clock/Readme.adoc @@ -0,0 +1,6 @@ +include::ieee1588.adoc[] + +<<< + +include::ieee802dot1as.adoc[] + diff --git a/test/case/ptp/boundary_clock/ieee1588.adoc b/test/case/ptp/boundary_clock/ieee1588.adoc new file mode 100644 index 00000000..6aa262fd --- /dev/null +++ b/test/case/ptp/boundary_clock/ieee1588.adoc @@ -0,0 +1,41 @@ +=== PTP boundary clock (IEEE 1588) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/boundary_clock] + +==== Description + +Verify that a Boundary Clock (BC) correctly receives time on one port and +distributes it on another, and that the downstream time receiver sees exactly +one additional hop (`steps-removed=2`). + +Three nodes are connected in a chain: a grandmaster Ordinary Clock (OC, +`priority1=1`), a Boundary Clock (BC, `priority1=64`) with two ports, and a +time-receiver Ordinary Clock (OC, `priority1=128`). + +The BC's upstream port (toward the GM) must reach time-receiver state; the +downstream port (toward the time receiver) must reach time-transmitter state. +The time receiver's `steps-removed` counter must equal 2: the BC increments +`steps-removed` to 1 in the ANNOUNCE messages it forwards, and the time +receiver adds 1 more when it stores the value in its `currentDS`. An OC +directly connected to the GM shows 1, so the BC adds exactly one extra hop. + +The test is run for both IEEE 1588-2019 (UDP/IPv4, E2E) and IEEE 802.1AS +(Layer 2, P2P) profiles. + +==== Topology + +image::topology.svg[PTP boundary clock (IEEE 1588) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (OC, ieee1588, priority1=1) and boundary clock (BC, ieee1588, priority1=64, two ports) +. Wait for BC uplink port to become time-receiver +. Wait for BC dnlink port to become time-transmitter +. Wait for boundary clock offset to converge +. Configure time receiver (OC, ieee1588, priority1=128, client-only) +. Wait for time receiver to reach time-receiver state +. Verify time receiver steps-removed equals 2 (one BC hop) +. Wait for time receiver offset to converge + + diff --git a/test/case/ptp/boundary_clock/ieee1588.py b/test/case/ptp/boundary_clock/ieee1588.py new file mode 120000 index 00000000..94656643 --- /dev/null +++ b/test/case/ptp/boundary_clock/ieee1588.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/test/case/ptp/boundary_clock/ieee802dot1as.adoc b/test/case/ptp/boundary_clock/ieee802dot1as.adoc new file mode 100644 index 00000000..9293a846 --- /dev/null +++ b/test/case/ptp/boundary_clock/ieee802dot1as.adoc @@ -0,0 +1,41 @@ +=== PTP boundary clock (IEEE 802.1AS) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/boundary_clock] + +==== Description + +Verify that a Boundary Clock (BC) correctly receives time on one port and +distributes it on another, and that the downstream time receiver sees exactly +one additional hop (`steps-removed=2`). + +Three nodes are connected in a chain: a grandmaster Ordinary Clock (OC, +`priority1=1`), a Boundary Clock (BC, `priority1=64`) with two ports, and a +time-receiver Ordinary Clock (OC, `priority1=128`). + +The BC's upstream port (toward the GM) must reach time-receiver state; the +downstream port (toward the time receiver) must reach time-transmitter state. +The time receiver's `steps-removed` counter must equal 2: the BC increments +`steps-removed` to 1 in the ANNOUNCE messages it forwards, and the time +receiver adds 1 more when it stores the value in its `currentDS`. An OC +directly connected to the GM shows 1, so the BC adds exactly one extra hop. + +The test is run for both IEEE 1588-2019 (UDP/IPv4, E2E) and IEEE 802.1AS +(Layer 2, P2P) profiles. + +==== Topology + +image::topology.svg[PTP boundary clock (IEEE 802.1AS) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (OC, ieee802-dot1as, priority1=1) and boundary clock (BC, ieee802-dot1as, priority1=64, two ports) +. Wait for BC uplink port to become time-receiver +. Wait for BC dnlink port to become time-transmitter +. Wait for boundary clock offset to converge +. Configure time receiver (OC, ieee802-dot1as, priority1=128, client-only) +. Wait for time receiver to reach time-receiver state +. Verify time receiver steps-removed equals 2 (one BC hop) +. Wait for time receiver offset to converge + + diff --git a/test/case/ptp/boundary_clock/ieee802dot1as.py b/test/case/ptp/boundary_clock/ieee802dot1as.py new file mode 120000 index 00000000..94656643 --- /dev/null +++ b/test/case/ptp/boundary_clock/ieee802dot1as.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/test/case/ptp/boundary_clock/test.adoc b/test/case/ptp/boundary_clock/test.adoc new file mode 100644 index 00000000..1080fefc --- /dev/null +++ b/test/case/ptp/boundary_clock/test.adoc @@ -0,0 +1,5 @@ +include::ieee1588.adoc[] + +<<< + +include::ieee802dot1as.adoc[] diff --git a/test/case/ptp/boundary_clock/test.py b/test/case/ptp/boundary_clock/test.py new file mode 100755 index 00000000..552051a3 --- /dev/null +++ b/test/case/ptp/boundary_clock/test.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +"""PTP boundary clock + +Verify that a Boundary Clock (BC) correctly receives time on one port and +distributes it on another, and that the downstream time receiver sees exactly +one additional hop (`steps-removed=2`). + +Three nodes are connected in a chain: a grandmaster Ordinary Clock (OC, +`priority1=1`), a Boundary Clock (BC, `priority1=64`) with two ports, and a +time-receiver Ordinary Clock (OC, `priority1=128`). + +The BC's upstream port (toward the GM) must reach time-receiver state; the +downstream port (toward the time receiver) must reach time-transmitter state. +The time receiver's `steps-removed` counter must equal 2: the BC increments +`steps-removed` to 1 in the ANNOUNCE messages it forwards, and the time +receiver adds 1 more when it stores the value in its `currentDS`. An OC +directly connected to the GM shows 1, so the BC adds exactly one extra hop. + +The test is run for both IEEE 1588-2019 (UDP/IPv4, E2E) and IEEE 802.1AS +(Layer 2, P2P) profiles. +""" + +import infamy +import infamy.ptp as ptp +from infamy import until + + +class ArgumentParser(infamy.ArgumentParser): + def __init__(self): + super().__init__() + self.args.add_argument("--profile", default="ieee1588", + choices=["ieee1588", "ieee802-dot1as"]) + self.args.add_argument("--threshold-ns", type=int, default=None) + + +def configure_oc(iface, priority1, profile, client_only=False, ip=None): + iface_cfg = {"name": iface, "enabled": True} + if profile == "ieee1588": + iface_cfg["ipv4"] = {"address": [{"ip": ip, "prefix-length": 30}]} + + port_ds = { + "log-announce-interval": -2, + "announce-receipt-timeout": 2, + "log-sync-interval": -2, + } + if profile == "ieee1588": + port_ds["delay-mechanism"] = "e2e" + + return { + "ietf-interfaces": { + "interfaces": {"interface": [iface_cfg]} + }, + "ieee1588-ptp-tt": { + "ptp": { + "instances": { + "instance": [{ + "instance-index": 0, + "default-ds": { + "instance-type": "oc", + "domain-number": 0, + "priority1": priority1, + "priority2": 128, + "infix-ptp:profile": profile, + "time-receiver-only": client_only, + }, + "ports": { + "port": [{ + "port-index": 1, + "underlying-interface": iface, + "port-ds": port_ds, + }] + } + }] + } + } + } + } + + +def configure_bc(uplink_iface, dnlink_iface, profile, + uplink_ip=None, dnlink_ip=None, priority1=64): + ifaces = [{"name": uplink_iface, "enabled": True}, + {"name": dnlink_iface, "enabled": True}] + if profile == "ieee1588": + ifaces[0]["ipv4"] = {"address": [{"ip": uplink_ip, "prefix-length": 30}]} + ifaces[1]["ipv4"] = {"address": [{"ip": dnlink_ip, "prefix-length": 30}]} + + port_ds = { + "log-announce-interval": -2, + "announce-receipt-timeout": 2, + "log-sync-interval": -2, + } + if profile == "ieee1588": + port_ds["delay-mechanism"] = "e2e" + + return { + "ietf-interfaces": { + "interfaces": {"interface": ifaces} + }, + "ieee1588-ptp-tt": { + "ptp": { + "instances": { + "instance": [{ + "instance-index": 0, + "default-ds": { + "instance-type": "bc", + "domain-number": 0, + "priority1": priority1, + "priority2": 128, + "infix-ptp:profile": profile, + }, + "ports": { + "port": [ + { + "port-index": 1, + "underlying-interface": uplink_iface, + "port-ds": port_ds, + }, + { + "port-index": 2, + "underlying-interface": dnlink_iface, + "port-ds": port_ds, + } + ] + } + }] + } + } + } + } + + +with infamy.Test() as test: + with test.step("Set up topology and attach to DUTs"): + arg = ArgumentParser() + env = infamy.Env(args=arg) + profile = env.args.profile + gm = env.attach("gm", "mgmt") + bc = env.attach("bc", "mgmt") + receiver = env.attach("receiver", "mgmt") + + _, gm_iface = env.ltop.xlate("gm", "data") + _, bc_uplink = env.ltop.xlate("bc", "uplink") + _, bc_dnlink = env.ltop.xlate("bc", "dnlink") + _, recv_iface = env.ltop.xlate("receiver", "data") + threshold_ns = env.args.threshold_ns or ptp.default_threshold(env, "receiver", hops=2) + + with test.step(f"Configure grandmaster (OC, {profile}, priority1=1) and boundary clock (BC, {profile}, priority1=64, two ports)"): + gm.put_config_dicts(configure_oc(gm_iface, priority1=1, + profile=profile, ip="192.168.100.1")) + bc.put_config_dicts(configure_bc(bc_uplink, bc_dnlink, profile=profile, + uplink_ip="192.168.100.2", + dnlink_ip="192.168.101.1")) + + with test.step("Wait for BC uplink port to become time-receiver"): + until(lambda: ptp.is_time_receiver(bc, port_idx=1), attempts=60) + + with test.step("Wait for BC dnlink port to become time-transmitter"): + until(lambda: ptp.is_time_transmitter(bc, port_idx=2), attempts=60) + + with test.step("Wait for boundary clock offset to converge"): + bc_threshold_ns = env.args.threshold_ns or ptp.default_threshold(env, "bc", hops=1) + until(lambda: ptp.has_converged(bc, bc_threshold_ns), attempts=120) + + with test.step(f"Configure time receiver (OC, {profile}, priority1=128, client-only)"): + receiver.put_config_dicts(configure_oc(recv_iface, priority1=128, + profile=profile, client_only=True, + ip="192.168.101.2")) + + with test.step("Wait for time receiver to reach time-receiver state"): + until(lambda: ptp.is_time_receiver(receiver), attempts=60) + + with test.step("Verify time receiver steps-removed equals 2 (one BC hop)"): + until(lambda: ptp.steps_removed(receiver) == 2, attempts=30) + + with test.step("Wait for time receiver offset to converge"): + until(lambda: ptp.has_converged(receiver, threshold_ns), attempts=120) + + test.succeed() diff --git a/test/case/ptp/boundary_clock/test.yaml b/test/case/ptp/boundary_clock/test.yaml new file mode 100644 index 00000000..5277fe19 --- /dev/null +++ b/test/case/ptp/boundary_clock/test.yaml @@ -0,0 +1,11 @@ +--- +- settings: + test-spec: .adoc + +- name: PTP boundary clock (IEEE 1588) + case: ieee1588.py + opts: ["--profile", "ieee1588"] + +- name: PTP boundary clock (IEEE 802.1AS) + case: ieee802dot1as.py + opts: ["--profile", "ieee802-dot1as"] diff --git a/test/case/ptp/boundary_clock/topology.dot b/test/case/ptp/boundary_clock/topology.dot new file mode 100644 index 00000000..66a895d1 --- /dev/null +++ b/test/case/ptp/boundary_clock/topology.dot @@ -0,0 +1,42 @@ +graph "ptp-boundary-clock" { + layout="neato"; + overlap="false"; + esep="+22"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="{ mgmt1 | \n\nhost\n\n | mgmt2 | mgmt3 }", + pos="0,15!", + requires="controller", + ]; + + gm [ + label="{ mgmt | data } | { gm\npriority1=1 }", + pos="2,15.5!", + fontsize=12, + requires="infix", + ]; + + bc [ + label="{ uplink | mgmt | dnlink } | { bc\npriority1=64 }", + pos="2,15!", + fontsize=12, + requires="infix", + ]; + + receiver [ + label="{ data | mgmt } | { receiver\npriority1=128 }", + pos="2,14.5!", + fontsize=12, + requires="infix", + ]; + + host:mgmt1 -- gm:mgmt [requires="mgmt", color="lightgray"] + host:mgmt2 -- bc:mgmt [requires="mgmt", color="lightgray"] + host:mgmt3 -- receiver:mgmt [requires="mgmt", color="lightgray"] + + gm:data -- bc:uplink [label="192.168.102.0/30 ", dir="both"] + bc:dnlink -- receiver:data [label="192.168.103.0/30 ", dir="both"] +} diff --git a/test/case/ptp/boundary_clock/topology.svg b/test/case/ptp/boundary_clock/topology.svg new file mode 100644 index 00000000..411c29ae --- /dev/null +++ b/test/case/ptp/boundary_clock/topology.svg @@ -0,0 +1,90 @@ + + + + + + +ptp-boundary-clock + + + +host + +mgmt1 + +host + +mgmt2 + +mgmt3 + + + +gm + +mgmt + +data + +gm +priority1=1 + + + +host:mgmt1--gm:mgmt + + + + +bc + +uplink + +mgmt + +dnlink + +bc +priority1=64 + + + +host:mgmt2--bc:mgmt + + + + +receiver + +data + +mgmt + +receiver +priority1=128 + + + +host:mgmt3--receiver:mgmt + + + + +gm:data--bc:uplink + + + +192.168.102.0/30 + + + +bc:dnlink--receiver:data + + + +192.168.103.0/30 + + + diff --git a/test/case/ptp/port_recovery/Readme.adoc b/test/case/ptp/port_recovery/Readme.adoc new file mode 120000 index 00000000..ae32c841 --- /dev/null +++ b/test/case/ptp/port_recovery/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/ptp/port_recovery/test.adoc b/test/case/ptp/port_recovery/test.adoc new file mode 100644 index 00000000..e58d2f0c --- /dev/null +++ b/test/case/ptp/port_recovery/test.adoc @@ -0,0 +1,32 @@ +=== PTP port fault recovery + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/port_recovery] + +==== Description + +Verify that the PTP port state machine correctly detects a link fault +and recovers to time-receiver state once the link is restored. + +Two Ordinary Clocks are connected back-to-back. Once the time receiver +has converged, the grandmaster's data interface is disabled. The time +receiver must leave time-receiver state within a short timeout. When +the interface is re-enabled, the time receiver must return to +time-receiver state and its offset must converge again to within the +configured threshold. + +==== Topology + +image::topology.svg[PTP port fault recovery topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (priority1=1) and time receiver (client-only) +. Wait for initial convergence +. Disable grandmaster data interface to trigger fault +. Verify time receiver leaves time-receiver state +. Re-enable grandmaster data interface +. Wait for time receiver to return to time-receiver state after recovery +. Wait for offset to re-converge + + diff --git a/test/case/ptp/port_recovery/test.py b/test/case/ptp/port_recovery/test.py new file mode 100755 index 00000000..740d907f --- /dev/null +++ b/test/case/ptp/port_recovery/test.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""PTP port fault recovery + +Verify that the PTP port state machine correctly detects a link fault +and recovers to time-receiver state once the link is restored. + +Two Ordinary Clocks are connected back-to-back. Once the time receiver +has converged, the grandmaster's data interface is disabled. The time +receiver must leave time-receiver state within a short timeout. When +the interface is re-enabled, the time receiver must return to +time-receiver state and its offset must converge again to within the +configured threshold. +""" + +import infamy +import infamy.ptp as ptp +from infamy import until + + +def configure_oc(iface, ip, priority1, client_only, dm="e2e"): + return { + "ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": iface, + "enabled": True, + "ipv4": { + "address": [{"ip": ip, "prefix-length": 30}] + } + }] + } + }, + "ieee1588-ptp-tt": { + "ptp": { + "instances": { + "instance": [{ + "instance-index": 0, + "default-ds": { + "instance-type": "oc", + "domain-number": 0, + "priority1": priority1, + "priority2": 128, + "infix-ptp:profile": "ieee1588", + "time-receiver-only": client_only, + }, + "ports": { + "port": [{ + "port-index": 1, + "underlying-interface": iface, + "port-ds": { + "delay-mechanism": dm, + "log-announce-interval": -2, + "announce-receipt-timeout": 2, + "log-sync-interval": -2, + } + }] + } + }] + } + } + } + } + + +def set_iface_enabled(target, iface, enabled): + target.put_config_dict("ietf-interfaces", { + "interfaces": { + "interface": [{ + "name": iface, + "enabled": enabled, + }] + } + }) + + +class ArgumentParser(infamy.ArgumentParser): + def __init__(self): + super().__init__() + self.args.add_argument("--threshold-ns", type=int, default=None) + + +with infamy.Test() as test: + with test.step("Set up topology and attach to DUTs"): + arg = ArgumentParser() + env = infamy.Env(args=arg) + gm = env.attach("gm", "mgmt") + receiver = env.attach("receiver", "mgmt") + + _, gm_iface = env.ltop.xlate("gm", "data") + _, receiver_iface = env.ltop.xlate("receiver", "data") + threshold_ns = env.args.threshold_ns or ptp.default_threshold(env, "receiver") + + with test.step("Configure grandmaster (priority1=1) and time receiver (client-only)"): + gm.put_config_dicts(configure_oc(gm_iface, "192.168.100.1", + priority1=1, client_only=False)) + receiver.put_config_dicts(configure_oc(receiver_iface, "192.168.100.2", + priority1=128, client_only=True)) + + with test.step("Wait for initial convergence"): + until(lambda: ptp.is_time_receiver(receiver) and ptp.has_converged(receiver, threshold_ns), + attempts=120) + + with test.step("Disable grandmaster data interface to trigger fault"): + set_iface_enabled(gm, gm_iface, False) + + with test.step("Verify time receiver leaves time-receiver state"): + until(lambda: not ptp.is_time_receiver(receiver), attempts=30) + + with test.step("Re-enable grandmaster data interface"): + set_iface_enabled(gm, gm_iface, True) + + with test.step("Wait for time receiver to return to time-receiver state after recovery"): + until(lambda: ptp.is_time_receiver(receiver), attempts=120) + + with test.step("Wait for offset to re-converge"): + until(lambda: ptp.has_converged(receiver, threshold_ns), attempts=120) + + test.succeed() diff --git a/test/case/ptp/port_recovery/topology.dot b/test/case/ptp/port_recovery/topology.dot new file mode 100644 index 00000000..4544ca5a --- /dev/null +++ b/test/case/ptp/port_recovery/topology.dot @@ -0,0 +1,33 @@ +graph "ptp-port-recovery" { + layout="neato"; + overlap="false"; + esep="+22"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="{ mgmt1 | \n\nhost\n\n\n | mgmt2 }", + pos="0,15!", + requires="controller", + ]; + + gm [ + label="{ mgmt | data } | { gm\npriority1=1 }", + pos="2,15.25!", + fontsize=12, + requires="infix", + ]; + + receiver [ + label="{ data | mgmt } | { receiver\npriority1=128 }", + pos="2,14.75!", + fontsize=12, + requires="infix", + ]; + + host:mgmt1 -- gm:mgmt [requires="mgmt", color="lightgray"] + host:mgmt2 -- receiver:mgmt [requires="mgmt", color="lightgray"] + + gm:data -- receiver:data [label="\n\n192.168.106.0/30 ", dir="both"] +} diff --git a/test/case/ptp/port_recovery/topology.svg b/test/case/ptp/port_recovery/topology.svg new file mode 100644 index 00000000..8c5b1311 --- /dev/null +++ b/test/case/ptp/port_recovery/topology.svg @@ -0,0 +1,62 @@ + + + + + + +ptp-port-recovery + + + +host + +mgmt1 + +host + +mgmt2 + + + +gm + +mgmt + +data + +gm +priority1=1 + + + +host:mgmt1--gm:mgmt + + + + +receiver + +data + +mgmt + +receiver +priority1=128 + + + +host:mgmt2--receiver:mgmt + + + + +gm:data--receiver:data + + + +192.168.106.0/30   + + + diff --git a/test/case/ptp/servo/Readme.adoc b/test/case/ptp/servo/Readme.adoc new file mode 120000 index 00000000..ae32c841 --- /dev/null +++ b/test/case/ptp/servo/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/ptp/servo/test.adoc b/test/case/ptp/servo/test.adoc new file mode 100644 index 00000000..2e297f60 --- /dev/null +++ b/test/case/ptp/servo/test.adoc @@ -0,0 +1,44 @@ +=== PTP servo step-threshold + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/servo] + +==== Description + +Verify that configuring a non-zero `step-threshold` allows the clock servo +to correct a large time offset by stepping rather than slewing. + +Two Ordinary Clocks are connected back-to-back using the IEEE 1588 profile. +After initial convergence the receiver is reconfigured with +`step-threshold=1.0 s` and ptp4l restarts. Because the offset at restart +is near zero, `first_step_threshold` (ptp4l's per-startup step gate) does +not trigger, so the restart itself is convergence-neutral. + +Once the receiver has re-locked, the grandmaster clock is stepped forward by +10 seconds using phc_ctl (hardware PHC) or the system clock (software +timestamping). The 10-second offset exceeds the 1-second step-threshold, so +the servo steps the clock immediately and the receiver converges within a +few seconds. + +Note: a negative test (verify that offset=10 s does *not* converge without +step-threshold) is not included here because it is unreliable across +platforms. On physical hardware the kernel caps clock frequency adjustment +at ~500 ppm, making a 10-second slew take ~5.5 hours; on virtual clocks +(QEMU) no such limit applies and the servo can slew the offset away in +seconds. Full negative coverage requires exposing first_step_threshold and +max_frequency in the YANG model — see TODO.org. + +==== Topology + +image::topology.svg[PTP servo step-threshold topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (OC, IEEE 1588, priority1=1) and time receiver +. Wait for grandmaster and time receiver ports to reach active states +. Wait for initial convergence +. Reconfigure receiver with step-threshold=1.0 s +. Inject {STEP_SEC}-second offset on grandmaster clock +. Verify receiver converges by stepping (step-threshold=1.0 s) + + diff --git a/test/case/ptp/servo/test.py b/test/case/ptp/servo/test.py new file mode 100755 index 00000000..27576fb2 --- /dev/null +++ b/test/case/ptp/servo/test.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +"""PTP servo step-threshold + +Verify that configuring a non-zero `step-threshold` allows the clock servo +to correct a large time offset by stepping rather than slewing. + +Two Ordinary Clocks are connected back-to-back using the IEEE 1588 profile. +After initial convergence the receiver is reconfigured with +`step-threshold=1.0 s` and ptp4l restarts. Because the offset at restart +is near zero, `first_step_threshold` (ptp4l's per-startup step gate) does +not trigger, so the restart itself is convergence-neutral. + +Once the receiver has re-locked, the grandmaster clock is stepped forward by +10 seconds using phc_ctl (hardware PHC) or the system clock (software +timestamping). The 10-second offset exceeds the 1-second step-threshold, so +the servo steps the clock immediately and the receiver converges within a +few seconds. + +Note: a negative test (verify that offset=10 s does *not* converge without +step-threshold) is not included here because it is unreliable across +platforms. On physical hardware the kernel caps clock frequency adjustment +at ~500 ppm, making a 10-second slew take ~5.5 hours; on virtual clocks +(QEMU) no such limit applies and the servo can slew the offset away in +seconds. Full negative coverage requires exposing first_step_threshold and +max_frequency in the YANG model — see TODO.org. +""" + +import infamy +import infamy.ptp as ptp +from infamy import until +from infamy.util import parallel + +STEP_SEC = 10 + + +class ArgumentParser(infamy.ArgumentParser): + def __init__(self): + super().__init__() + self.args.add_argument("--threshold-ns", type=int, default=None) + + +def configure_oc(iface, priority1, client, ip, step_threshold=None): + config = { + "ieee1588-ptp-tt": { + "ptp": { + "instances": { + "instance": [{ + "instance-index": 0, + "default-ds": { + "instance-type": "oc", + "domain-number": 0, + "priority1": priority1, + "priority2": 128, + "infix-ptp:profile": "ieee1588", + "time-receiver-only": client, + }, + "ports": { + "port": [{ + "port-index": 1, + "underlying-interface": iface, + }] + } + }] + } + } + } + } + + iface_cfg = {"name": iface, "enabled": True, + "ipv4": {"address": [{"ip": ip, "prefix-length": 30}]}} + config["ietf-interfaces"] = {"interfaces": {"interface": [iface_cfg]}} + + port_ds = { + "log-announce-interval": -2, + "announce-receipt-timeout": 2, + "log-sync-interval": -2, + "delay-mechanism": "e2e", + } + inst = config["ieee1588-ptp-tt"]["ptp"]["instances"]["instance"][0] + inst["ports"]["port"][0]["port-ds"] = port_ds + + if step_threshold is not None: + inst["infix-ptp:servo"] = {"step-threshold": str(step_threshold)} + + return config + + +def step_clock(ssh, iface, seconds): + """Step the PTP clock on the node owning iface forward by seconds. + + Uses phc_ctl on the hardware PHC when available (sysfs discovery); + falls back to date(1) for software-timestamping nodes where ptp4l + disciplines CLOCK_REALTIME directly. + """ + rc = ssh.runsh(f"ls /sys/class/net/{iface}/device/ptp/ 2>/dev/null") + phc = rc.stdout.strip().split()[0] if rc.returncode == 0 and rc.stdout.strip() else None + if phc: + ssh.runsh(f"phc_ctl /dev/{phc} adj {float(seconds)}") + else: + ssh.runsh(f"date -s @$(( $(date +%s) + {seconds} ))") + + +with infamy.Test() as test: + with test.step("Set up topology and attach to DUTs"): + arg = ArgumentParser() + env = infamy.Env(args=arg) + gm = env.attach("gm", "mgmt") + receiver = env.attach("receiver", "mgmt") + + _, gm_iface = env.ltop.xlate("gm", "data") + _, receiver_iface = env.ltop.xlate("receiver", "data") + threshold_ns = env.args.threshold_ns or ptp.default_threshold(env, "receiver") + + with test.step("Configure grandmaster (OC, IEEE 1588, priority1=1) and time receiver"): + gm.put_config_dicts(configure_oc(gm_iface, priority1=1, + client=False, ip="192.168.100.1")) + receiver.put_config_dicts(configure_oc(receiver_iface, priority1=128, + client=True, ip="192.168.100.2")) + + with test.step("Wait for grandmaster and time receiver ports to reach active states"): + def gm_ready(): + if not ptp.is_time_transmitter(gm): + # print(f"{ptp.port_state_dbg(gm)}") + return False + return True + + def receiver_ready(): + if not ptp.is_time_receiver(receiver): + # print(f"{ptp.port_state_dbg(receiver)}") + return False + return True + + parallel(lambda: until(gm_ready, attempts=60), + lambda: until(receiver_ready, attempts=60)) + + with test.step("Wait for initial convergence"): + until(lambda: ptp.has_converged(receiver, threshold_ns), attempts=120) + + with test.step("Reconfigure receiver with step-threshold=1.0 s"): + # ptp4l restarts while the offset is near zero so first_step_threshold + # does not trigger; the restart itself is convergence-neutral. + receiver.put_config_dicts(configure_oc(receiver_iface, priority1=128, + client=True, ip="192.168.100.2", + step_threshold=1.0)) + until(lambda: ptp.has_converged(receiver, threshold_ns), attempts=60) + + with test.step(f"Inject {STEP_SEC}-second offset on grandmaster clock"): + gmssh = env.attach("gm", "mgmt", "ssh") + step_clock(gmssh, gm_iface, STEP_SEC) + + with test.step("Verify receiver converges by stepping (step-threshold=1.0 s)"): + until(lambda: ptp.has_converged(receiver, threshold_ns), attempts=60) + + test.succeed() diff --git a/test/case/ptp/servo/topology.dot b/test/case/ptp/servo/topology.dot new file mode 100644 index 00000000..cb32dd36 --- /dev/null +++ b/test/case/ptp/servo/topology.dot @@ -0,0 +1,33 @@ +graph "ptp-servo" { + layout="neato"; + overlap="false"; + esep="+22"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="{ mgmt1 | \n\nhost\n\n\n | mgmt2 }", + pos="0,15!", + requires="controller", + ]; + + gm [ + label="{ mgmt | data } | { gm\npriority1=1 }", + pos="2,15.25!", + fontsize=12, + requires="infix", + ]; + + receiver [ + label="{ data | mgmt } | { receiver\npriority1=128 }", + pos="2,14.75!", + fontsize=12, + requires="infix", + ]; + + host:mgmt1 -- gm:mgmt [requires="mgmt", color="lightgray"] + host:mgmt2 -- receiver:mgmt [requires="mgmt", color="lightgray"] + + gm:data -- receiver:data [label="\n\n192.168.100.0/30 ", dir="both"] +} diff --git a/test/case/ptp/servo/topology.svg b/test/case/ptp/servo/topology.svg new file mode 100644 index 00000000..3a916447 --- /dev/null +++ b/test/case/ptp/servo/topology.svg @@ -0,0 +1,62 @@ + + + + + + +ptp-servo + + + +host + +mgmt1 + +host + +mgmt2 + + + +gm + +mgmt + +data + +gm +priority1=1 + + + +host:mgmt1--gm:mgmt + + + + +receiver + +data + +mgmt + +receiver +priority1=128 + + + +host:mgmt2--receiver:mgmt + + + + +gm:data--receiver:data + + + +192.168.100.0/30   + + + diff --git a/test/case/ptp/transparent_clock/Readme.adoc b/test/case/ptp/transparent_clock/Readme.adoc new file mode 100644 index 00000000..164bfb7f --- /dev/null +++ b/test/case/ptp/transparent_clock/Readme.adoc @@ -0,0 +1,10 @@ +include::e2e.adoc[] + +<<< + +include::p2p.adoc[] + +<<< + +include::ieee802dot1as.adoc[] + diff --git a/test/case/ptp/transparent_clock/e2e.adoc b/test/case/ptp/transparent_clock/e2e.adoc new file mode 100644 index 00000000..5346f907 --- /dev/null +++ b/test/case/ptp/transparent_clock/e2e.adoc @@ -0,0 +1,43 @@ +=== PTP transparent clock (E2E) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/transparent_clock] + +==== Description + +Verify that an E2E or P2P Transparent Clock (TC) passes timing transparently +through a hardware switch without adding a boundary-clock hop, and that the +downstream time receiver converges to the grandmaster's time. + +Three nodes are connected in a chain: a grandmaster Ordinary Clock +(`priority1=1`), a Transparent Clock, and a time-receiver Ordinary Clock +(`priority1=128`). + +The TC updates the correction field in each Sync and Delay_Req message to +account for its own residence time. Because a TC is transparent, the time +receiver's `steps-removed` counter must equal 1 — unlike a Boundary Clock, +which would give 2. A TC passes ANNOUNCE messages unchanged (`stepsRemoved=0` +from the GM), and the time receiver adds 1 when it stores the value in +`currentDS`, giving a total of 1. A BC increments `stepsRemoved` to 1 before +forwarding, and the receiver adds 1 more, giving 2. The time receiver's offset must converge within the configured threshold +(default is tighter when the topology provides hardware timestamping links). + +The delay mechanism (E2E or P2P) is controlled by the test suite for +IEEE 1588 runs. When the profile is IEEE 802.1AS the delay mechanism is +always P2P (mandated by the standard) and Layer 2 transport is used. + +==== Topology + +image::topology.svg[PTP transparent clock (E2E) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (OC, priority1=1, {dm}) +. Configure transparent clock ({dm}-tc, {profile}) +. Configure time receiver (OC, priority1=128, client-only) +. Wait for grandmaster port to become time-transmitter +. Wait for time receiver to reach time-receiver state +. Verify time receiver steps-removed equals 1 +. Wait for time receiver offset to converge + + diff --git a/test/case/ptp/transparent_clock/e2e.py b/test/case/ptp/transparent_clock/e2e.py new file mode 120000 index 00000000..94656643 --- /dev/null +++ b/test/case/ptp/transparent_clock/e2e.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/test/case/ptp/transparent_clock/ieee802dot1as.adoc b/test/case/ptp/transparent_clock/ieee802dot1as.adoc new file mode 100644 index 00000000..68d64b80 --- /dev/null +++ b/test/case/ptp/transparent_clock/ieee802dot1as.adoc @@ -0,0 +1,43 @@ +=== PTP transparent clock (IEEE 802.1AS) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/transparent_clock] + +==== Description + +Verify that an E2E or P2P Transparent Clock (TC) passes timing transparently +through a hardware switch without adding a boundary-clock hop, and that the +downstream time receiver converges to the grandmaster's time. + +Three nodes are connected in a chain: a grandmaster Ordinary Clock +(`priority1=1`), a Transparent Clock, and a time-receiver Ordinary Clock +(`priority1=128`). + +The TC updates the correction field in each Sync and Delay_Req message to +account for its own residence time. Because a TC is transparent, the time +receiver's `steps-removed` counter must equal 1 — unlike a Boundary Clock, +which would give 2. A TC passes ANNOUNCE messages unchanged (`stepsRemoved=0` +from the GM), and the time receiver adds 1 when it stores the value in +`currentDS`, giving a total of 1. A BC increments `stepsRemoved` to 1 before +forwarding, and the receiver adds 1 more, giving 2. The time receiver's offset must converge within the configured threshold +(default is tighter when the topology provides hardware timestamping links). + +The delay mechanism (E2E or P2P) is controlled by the test suite for +IEEE 1588 runs. When the profile is IEEE 802.1AS the delay mechanism is +always P2P (mandated by the standard) and Layer 2 transport is used. + +==== Topology + +image::topology.svg[PTP transparent clock (IEEE 802.1AS) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (OC, priority1=1, {dm}) +. Configure transparent clock ({dm}-tc, ieee802-dot1as) +. Configure time receiver (OC, priority1=128, client-only) +. Wait for grandmaster port to become time-transmitter +. Wait for time receiver to reach time-receiver state +. Verify time receiver steps-removed equals 1 +. Wait for time receiver offset to converge + + diff --git a/test/case/ptp/transparent_clock/ieee802dot1as.py b/test/case/ptp/transparent_clock/ieee802dot1as.py new file mode 120000 index 00000000..94656643 --- /dev/null +++ b/test/case/ptp/transparent_clock/ieee802dot1as.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/test/case/ptp/transparent_clock/p2p.adoc b/test/case/ptp/transparent_clock/p2p.adoc new file mode 100644 index 00000000..cacdb449 --- /dev/null +++ b/test/case/ptp/transparent_clock/p2p.adoc @@ -0,0 +1,43 @@ +=== PTP transparent clock (P2P) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/transparent_clock] + +==== Description + +Verify that an E2E or P2P Transparent Clock (TC) passes timing transparently +through a hardware switch without adding a boundary-clock hop, and that the +downstream time receiver converges to the grandmaster's time. + +Three nodes are connected in a chain: a grandmaster Ordinary Clock +(`priority1=1`), a Transparent Clock, and a time-receiver Ordinary Clock +(`priority1=128`). + +The TC updates the correction field in each Sync and Delay_Req message to +account for its own residence time. Because a TC is transparent, the time +receiver's `steps-removed` counter must equal 1 — unlike a Boundary Clock, +which would give 2. A TC passes ANNOUNCE messages unchanged (`stepsRemoved=0` +from the GM), and the time receiver adds 1 when it stores the value in +`currentDS`, giving a total of 1. A BC increments `stepsRemoved` to 1 before +forwarding, and the receiver adds 1 more, giving 2. The time receiver's offset must converge within the configured threshold +(default is tighter when the topology provides hardware timestamping links). + +The delay mechanism (E2E or P2P) is controlled by the test suite for +IEEE 1588 runs. When the profile is IEEE 802.1AS the delay mechanism is +always P2P (mandated by the standard) and Layer 2 transport is used. + +==== Topology + +image::topology.svg[PTP transparent clock (P2P) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (OC, priority1=1, {dm}) +. Configure transparent clock ({dm}-tc, {profile}) +. Configure time receiver (OC, priority1=128, client-only) +. Wait for grandmaster port to become time-transmitter +. Wait for time receiver to reach time-receiver state +. Verify time receiver steps-removed equals 1 +. Wait for time receiver offset to converge + + diff --git a/test/case/ptp/transparent_clock/p2p.py b/test/case/ptp/transparent_clock/p2p.py new file mode 120000 index 00000000..94656643 --- /dev/null +++ b/test/case/ptp/transparent_clock/p2p.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/test/case/ptp/transparent_clock/test.adoc b/test/case/ptp/transparent_clock/test.adoc new file mode 100644 index 00000000..a07dcf86 --- /dev/null +++ b/test/case/ptp/transparent_clock/test.adoc @@ -0,0 +1,46 @@ +=== PTP transparent clock (P2P) + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/ptp/transparent_clock] + +==== Description + +Verify that an E2E or P2P Transparent Clock (TC) passes timing +transparently through a hardware switch without adding a boundary-clock +hop, and that the downstream time receiver converges to the +grandmaster's time. + +Three nodes are connected in a chain: a grandmaster Ordinary Clock +(priority1=1), a Transparent Clock with hardware timestamping, and a +time-receiver Ordinary Clock (priority1=128). + +The TC updates the correction field in each Sync and Delay_Req message +to account for its own residence time. Because a TC is transparent, +the time receiver's steps-removed counter must equal 1 — unlike a +Boundary Clock, which would give 2. A TC passes ANNOUNCE messages +unchanged (stepsRemoved=0 from the GM), and the time receiver adds 1 +when it stores the value in currentDS, giving a total of 1. A BC +increments stepsRemoved to 1 before forwarding, and the receiver adds +1 more, giving 2. The time receiver's offset must converge within the +tight threshold appropriate for hardware timestamping. + +The delay mechanism (E2E or P2P) is injected via the --delay-mechanism +argument from the test suite YAML, allowing both variants to run from +the same test script. The TC node requires hardware PTP timestamping +support (capability: ptp-hwts). + +==== Topology + +image::topology.svg[PTP transparent clock (P2P) topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to DUTs +. Configure grandmaster (OC, priority1=1, {dm}) +. Configure transparent clock ({dm.upper()}-TC) +. Configure time receiver (OC, priority1=128, client-only) +. Wait for grandmaster port to become time-transmitter +. Wait for time receiver to reach time-receiver state +. Verify time receiver steps-removed equals 1 (TC adds no boundary-clock hop) +. Wait for time receiver offset to converge + + diff --git a/test/case/ptp/transparent_clock/test.py b/test/case/ptp/transparent_clock/test.py new file mode 100755 index 00000000..f309e89d --- /dev/null +++ b/test/case/ptp/transparent_clock/test.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +"""PTP transparent clock + +Verify that an E2E or P2P Transparent Clock (TC) passes timing transparently +through a hardware switch without adding a boundary-clock hop, and that the +downstream time receiver converges to the grandmaster's time. + +Three nodes are connected in a chain: a grandmaster Ordinary Clock +(`priority1=1`), a Transparent Clock, and a time-receiver Ordinary Clock +(`priority1=128`). + +The TC updates the correction field in each Sync and Delay_Req message to +account for its own residence time. Because a TC is transparent, the time +receiver's `steps-removed` counter must equal 1 — unlike a Boundary Clock, +which would give 2. A TC passes ANNOUNCE messages unchanged (`stepsRemoved=0` +from the GM), and the time receiver adds 1 when it stores the value in +`currentDS`, giving a total of 1. A BC increments `stepsRemoved` to 1 before +forwarding, and the receiver adds 1 more, giving 2. The time receiver's offset must converge within the configured threshold +(default is tighter when the topology provides hardware timestamping links). + +The delay mechanism (E2E or P2P) is controlled by the test suite for +IEEE 1588 runs. When the profile is IEEE 802.1AS the delay mechanism is +always P2P (mandated by the standard) and Layer 2 transport is used. +""" + +import infamy +import infamy.ptp as ptp +from infamy import until + + +class ArgumentParser(infamy.ArgumentParser): + def __init__(self): + super().__init__() + self.args.add_argument("--profile", default="ieee1588", + choices=["ieee1588", "ieee802-dot1as"]) + self.args.add_argument("--delay-mechanism", default="e2e", + choices=["e2e", "p2p"]) + self.args.add_argument("--threshold-ns", type=int, default=None) + + +def configure_oc(iface, priority1, profile, client_only=False, ip=None, dm="e2e"): + iface_cfg = {"name": iface, "enabled": True} + if profile == "ieee1588": + iface_cfg["ipv4"] = {"address": [{"ip": ip, "prefix-length": 30}]} + + port_ds = { + "log-announce-interval": -2, + "announce-receipt-timeout": 2, + "log-sync-interval": -2, + } + if profile == "ieee1588": + port_ds["delay-mechanism"] = dm + + return { + "ietf-interfaces": { + "interfaces": {"interface": [iface_cfg]} + }, + "ieee1588-ptp-tt": { + "ptp": { + "instances": { + "instance": [{ + "instance-index": 0, + "default-ds": { + "instance-type": "oc", + "domain-number": 0, + "priority1": priority1, + "priority2": 128, + "infix-ptp:profile": profile, + "time-receiver-only": client_only, + }, + "ports": { + "port": [{ + "port-index": 1, + "underlying-interface": iface, + "port-ds": port_ds, + }] + } + }] + } + } + } + } + + +def configure_tc(uplink_iface, dnlink_iface, profile, dm="e2e", + uplink_ip=None, dnlink_ip=None): + if profile == "ieee802-dot1as": + instance_type = "p2p-tc" + else: + instance_type = "p2p-tc" if dm == "p2p" else "e2e-tc" + + ifaces = [{"name": uplink_iface, "enabled": True}, + {"name": dnlink_iface, "enabled": True}] + if profile == "ieee1588": + ifaces[0]["ipv4"] = {"address": [{"ip": uplink_ip, "prefix-length": 30}]} + ifaces[1]["ipv4"] = {"address": [{"ip": dnlink_ip, "prefix-length": 30}]} + + return { + "ietf-interfaces": { + "interfaces": {"interface": ifaces} + }, + "ieee1588-ptp-tt": { + "ptp": { + "instances": { + "instance": [{ + "instance-index": 0, + "default-ds": { + "instance-type": instance_type, + "domain-number": 0, + "infix-ptp:profile": profile, + }, + "ports": { + "port": [ + { + "port-index": 1, + "underlying-interface": uplink_iface, + "port-ds": {"log-sync-interval": -2}, + }, + { + "port-index": 2, + "underlying-interface": dnlink_iface, + "port-ds": {"log-sync-interval": -2}, + } + ] + } + }] + } + } + } + } + + +with infamy.Test() as test: + with test.step("Set up topology and attach to DUTs"): + arg = ArgumentParser() + env = infamy.Env(args=arg) + profile = env.args.profile + dm = "p2p" if profile == "ieee802-dot1as" else env.args.delay_mechanism + gm = env.attach("gm", "mgmt") + tc = env.attach("tc", "mgmt") + receiver = env.attach("receiver", "mgmt") + + gm_iface = gm["data"] + tc_uplink = tc["uplink"] + tc_dnlink = tc["dnlink"] + receiver_iface = receiver["data"] + threshold_ns = env.args.threshold_ns or ptp.default_threshold(env, "tc") + + with test.step(f"Configure grandmaster (OC, priority1=1, {dm})"): + gm.put_config_dicts(configure_oc(gm_iface, priority1=1, + profile=profile, ip="192.168.100.1", dm=dm)) + + with test.step(f"Configure transparent clock ({dm}-tc, {profile})"): + tc.put_config_dicts(configure_tc(tc_uplink, tc_dnlink, profile=profile, dm=dm, + uplink_ip="192.168.100.2", + dnlink_ip="192.168.101.1")) + + with test.step("Configure time receiver (OC, priority1=128, client-only)"): + receiver.put_config_dicts(configure_oc(receiver_iface, priority1=128, + profile=profile, client_only=True, + ip="192.168.101.2", dm=dm)) + + with test.step("Wait for grandmaster port to become time-transmitter"): + until(lambda: ptp.is_time_transmitter(gm), attempts=60) + + with test.step("Wait for time receiver to reach time-receiver state"): + until(lambda: ptp.is_time_receiver(receiver), attempts=60) + + with test.step("Verify time receiver steps-removed equals 1"): + until(lambda: ptp.steps_removed(receiver) == 1, attempts=60) + + with test.step("Wait for time receiver offset to converge"): + until(lambda: ptp.has_converged(receiver, threshold_ns), attempts=180) + + test.succeed() diff --git a/test/case/ptp/transparent_clock/test.yaml b/test/case/ptp/transparent_clock/test.yaml new file mode 100644 index 00000000..4bbc87da --- /dev/null +++ b/test/case/ptp/transparent_clock/test.yaml @@ -0,0 +1,15 @@ +--- +- settings: + test-spec: .adoc + +- name: PTP transparent clock (E2E) + case: e2e.py + opts: ["--delay-mechanism", "e2e"] + +- name: PTP transparent clock (P2P) + case: p2p.py + opts: ["--delay-mechanism", "p2p"] + +- name: PTP transparent clock (IEEE 802.1AS) + case: ieee802dot1as.py + opts: ["--profile", "ieee802-dot1as"] diff --git a/test/case/ptp/transparent_clock/topology.dot b/test/case/ptp/transparent_clock/topology.dot new file mode 100644 index 00000000..3009cf82 --- /dev/null +++ b/test/case/ptp/transparent_clock/topology.dot @@ -0,0 +1,42 @@ +graph "ptp-transparent-clock" { + layout="neato"; + overlap="false"; + esep="+22"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="{ mgmt1 | \n\nhost\n\n | mgmt2 | mgmt3 }", + pos="0,15!", + requires="controller", + ]; + + gm [ + label="{ mgmt | data } | { gm\npriority1=1 }", + pos="2,15.5!", + fontsize=12, + requires="infix", + ]; + + tc [ + label="{ uplink | mgmt | dnlink } | { tc\nE2E-TC or P2P-TC }", + pos="2,15!", + fontsize=12, + requires="infix", + ]; + + receiver [ + label="{ data | mgmt } | { receiver\npriority1=128 }", + pos="2,14.5!", + fontsize=12, + requires="infix", + ]; + + host:mgmt1 -- gm:mgmt [requires="mgmt", color="lightgray"] + host:mgmt2 -- tc:mgmt [requires="mgmt", color="lightgray"] + host:mgmt3 -- receiver:mgmt [requires="mgmt", color="lightgray"] + + gm:data -- tc:uplink [label="192.168.104.0/30 ", dir="both"] + tc:dnlink -- receiver:data [label="192.168.105.0/30 ", dir="both"] +} diff --git a/test/case/ptp/transparent_clock/topology.svg b/test/case/ptp/transparent_clock/topology.svg new file mode 100644 index 00000000..cfa0ceb6 --- /dev/null +++ b/test/case/ptp/transparent_clock/topology.svg @@ -0,0 +1,90 @@ + + + + + + +ptp-transparent-clock + + + +host + +mgmt1 + +host + +mgmt2 + +mgmt3 + + + +gm + +mgmt + +data + +gm +priority1=1 + + + +host:mgmt1--gm:mgmt + + + + +tc + +uplink + +mgmt + +dnlink + +tc +E2E-TC or P2P-TC + + + +host:mgmt2--tc:mgmt + + + + +receiver + +data + +mgmt + +receiver +priority1=128 + + + +host:mgmt3--receiver:mgmt + + + + +gm:data--tc:uplink + + + +192.168.104.0/30 + + + +tc:dnlink--receiver:data + + + +192.168.105.0/30 + + + diff --git a/test/case/routing/ospf_default_route_advertise/test.adoc b/test/case/routing/ospf_default_route_advertise/test.adoc index c3d24e59..d0aa1899 100644 --- a/test/case/routing/ospf_default_route_advertise/test.adoc +++ b/test/case/routing/ospf_default_route_advertise/test.adoc @@ -1,4 +1,4 @@ -=== OSPF Default route advertise +=== OSPF Default Route Advertise ifdef::topdoc[:imagesdir: {topdoc}../../test/case/routing/ospf_default_route_advertise] @@ -33,7 +33,7 @@ unless _always_ is set for _default-route-advertising_. ==== Topology -image::topology.svg[OSPF Default route advertise topology, align=center, scaledwidth=75%] +image::topology.svg[OSPF Default Route Advertise topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/routing/ospf_multiarea/test.adoc b/test/case/routing/ospf_multiarea/test.adoc index 21919152..c6e80935 100644 --- a/test/case/routing/ospf_multiarea/test.adoc +++ b/test/case/routing/ospf_multiarea/test.adoc @@ -1,4 +1,4 @@ -=== OSPF with multiple areas +=== OSPF with Multiple Areas ifdef::topdoc[:imagesdir: {topdoc}../../test/case/routing/ospf_multiarea] @@ -28,7 +28,7 @@ explicit router-id. ==== Topology -image::topology.svg[OSPF with multiple areas topology, align=center, scaledwidth=75%] +image::topology.svg[OSPF with Multiple Areas topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/routing/ospf_unnumbered_interface/test.adoc b/test/case/routing/ospf_unnumbered_interface/test.adoc index d066026a..c6014db3 100644 --- a/test/case/routing/ospf_unnumbered_interface/test.adoc +++ b/test/case/routing/ospf_unnumbered_interface/test.adoc @@ -1,4 +1,4 @@ -=== OSPF unnumbered interfaces +=== OSPF Unnumbered Interfaces ifdef::topdoc[:imagesdir: {topdoc}../../test/case/routing/ospf_unnumbered_interface] @@ -13,7 +13,7 @@ configuration and passive to function ==== Topology -image::topology.svg[OSPF unnumbered interfaces topology, align=center, scaledwidth=75%] +image::topology.svg[OSPF Unnumbered Interfaces topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/routing/static_routing/test.adoc b/test/case/routing/static_routing/test.adoc index 70782fc7..b16073d6 100644 --- a/test/case/routing/static_routing/test.adoc +++ b/test/case/routing/static_routing/test.adoc @@ -1,4 +1,4 @@ -=== Static routing +=== Static Routing ifdef::topdoc[:imagesdir: {topdoc}../../test/case/routing/static_routing] @@ -9,7 +9,7 @@ that data forwarding works as expected via an intermediate device. ==== Topology -image::topology.svg[Static routing topology, align=center, scaledwidth=75%] +image::topology.svg[Static Routing topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/services/lldp/lldp_admin_status/test.adoc b/test/case/services/lldp/lldp_admin_status/test.adoc index 58ad98a0..f87f53c6 100644 --- a/test/case/services/lldp/lldp_admin_status/test.adoc +++ b/test/case/services/lldp/lldp_admin_status/test.adoc @@ -1,4 +1,4 @@ -=== LLDP admin status +=== LLDP Admin Status ifdef::topdoc[:imagesdir: {topdoc}../../test/case/services/lldp/lldp_admin_status] @@ -8,7 +8,7 @@ Verify that LLDP admin status is set properly by lldpd ==== Topology -image::topology.svg[LLDP admin status topology, align=center, scaledwidth=75%] +image::topology.svg[LLDP Admin Status topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/services/lldp/lldp_enable_disable/test.adoc b/test/case/services/lldp/lldp_enable_disable/test.adoc index 5f8a0235..90c726eb 100644 --- a/test/case/services/lldp/lldp_enable_disable/test.adoc +++ b/test/case/services/lldp/lldp_enable_disable/test.adoc @@ -1,4 +1,4 @@ -=== LLDP enable/disable +=== LLDP Enable/Disable ifdef::topdoc[:imagesdir: {topdoc}../../test/case/services/lldp/lldp_enable_disable] @@ -9,7 +9,7 @@ Operation and non-operation are confirmed using tcpdump. ==== Topology -image::topology.svg[LLDP enable/disable topology, align=center, scaledwidth=75%] +image::topology.svg[LLDP Enable/Disable topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/services/mdns/mdns_allow_deny/test.adoc b/test/case/services/mdns/mdns_allow_deny/test.adoc index 2045daca..c9f736e8 100644 --- a/test/case/services/mdns/mdns_allow_deny/test.adoc +++ b/test/case/services/mdns/mdns_allow_deny/test.adoc @@ -1,4 +1,4 @@ -=== mDNS allow/deny interfaces +=== mDNS Allow/Deny Interfaces ifdef::topdoc[:imagesdir: {topdoc}../../test/case/services/mdns/mdns_allow_deny] @@ -14,7 +14,7 @@ with three scenarios: ==== Topology -image::topology.svg[mDNS allow/deny interfaces topology, align=center, scaledwidth=75%] +image::topology.svg[mDNS Allow/Deny Interfaces topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/services/mdns/mdns_enable_disable/test.adoc b/test/case/services/mdns/mdns_enable_disable/test.adoc index aea23bce..1430ee99 100644 --- a/test/case/services/mdns/mdns_enable_disable/test.adoc +++ b/test/case/services/mdns/mdns_enable_disable/test.adoc @@ -1,4 +1,4 @@ -=== mDNS enable/disable +=== mDNS Enable/Disable ifdef::topdoc[:imagesdir: {topdoc}../../test/case/services/mdns/mdns_enable_disable] @@ -9,7 +9,7 @@ Operation and non-operation are confirmed using tcpdump. ==== Topology -image::topology.svg[mDNS enable/disable topology, align=center, scaledwidth=75%] +image::topology.svg[mDNS Enable/Disable topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/services/mdns/mdns_reflector/test.adoc b/test/case/services/mdns/mdns_reflector/test.adoc index 4937cd74..75cce147 100644 --- a/test/case/services/mdns/mdns_reflector/test.adoc +++ b/test/case/services/mdns/mdns_reflector/test.adoc @@ -1,4 +1,4 @@ -=== mDNS reflector +=== mDNS Reflector ifdef::topdoc[:imagesdir: {topdoc}../../test/case/services/mdns/mdns_reflector] @@ -17,7 +17,7 @@ We verify operation with two scenarios: ==== Topology -image::topology.svg[mDNS reflector topology, align=center, scaledwidth=75%] +image::topology.svg[mDNS Reflector topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/services/ssh/ssh_key_authentication/test.adoc b/test/case/services/ssh/ssh_key_authentication/test.adoc index 6d48b524..93a1fa86 100644 --- a/test/case/services/ssh/ssh_key_authentication/test.adoc +++ b/test/case/services/ssh/ssh_key_authentication/test.adoc @@ -1,4 +1,4 @@ -=== Generate ssh key pair +=== Generate SSH Key Pair ifdef::topdoc[:imagesdir: {topdoc}../../test/case/services/ssh/ssh_key_authentication] @@ -8,7 +8,7 @@ Verify that 'guest' user can fetch data using only the 'public' key ==== Topology -image::topology.svg[Generate ssh key pair topology, align=center, scaledwidth=75%] +image::topology.svg[Generate SSH Key Pair topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/services/ssh/ssh_server_config/test.adoc b/test/case/services/ssh/ssh_server_config/test.adoc index 801a16b9..37e37a27 100644 --- a/test/case/services/ssh/ssh_server_config/test.adoc +++ b/test/case/services/ssh/ssh_server_config/test.adoc @@ -1,4 +1,4 @@ -=== SSH server configuration +=== SSH Server Configuration ifdef::topdoc[:imagesdir: {topdoc}../../test/case/services/ssh/ssh_server_config] @@ -11,7 +11,7 @@ Test SSH server functionality with pre-defined key pair: ==== Topology -image::topology.svg[SSH server configuration topology, align=center, scaledwidth=75%] +image::topology.svg[SSH Server Configuration topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/syslog/remote/test.adoc b/test/case/syslog/remote/test.adoc index 5535ac10..a37b9844 100644 --- a/test/case/syslog/remote/test.adoc +++ b/test/case/syslog/remote/test.adoc @@ -1,4 +1,4 @@ -=== Remote syslog +=== Remote Syslog ifdef::topdoc[:imagesdir: {topdoc}../../test/case/syslog/remote] @@ -8,7 +8,7 @@ Verify logging to remote, acting as a remote, and RFC5424 log format. ==== Topology -image::topology.svg[Remote syslog topology, align=center, scaledwidth=75%] +image::topology.svg[Remote Syslog topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/system/add_delete_user/test.adoc b/test/case/system/add_delete_user/test.adoc index ebbed34a..23335e5a 100644 --- a/test/case/system/add_delete_user/test.adoc +++ b/test/case/system/add_delete_user/test.adoc @@ -1,4 +1,4 @@ -=== Add/delete user +=== Add/Delete User ifdef::topdoc[:imagesdir: {topdoc}../../test/case/system/add_delete_user] @@ -9,7 +9,7 @@ with yescrypt. ==== Topology -image::topology.svg[Add/delete user topology, align=center, scaledwidth=75%] +image::topology.svg[Add/Delete User topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/system/hostname/test.adoc b/test/case/system/hostname/test.adoc index 7a71726b..a8a2d1c5 100644 --- a/test/case/system/hostname/test.adoc +++ b/test/case/system/hostname/test.adoc @@ -1,4 +1,4 @@ -=== Set hostname +=== Set Hostname ifdef::topdoc[:imagesdir: {topdoc}../../test/case/system/hostname] @@ -11,7 +11,7 @@ base MAC address. E.g., ix-01-01-01. ==== Topology -image::topology.svg[Set hostname topology, align=center, scaledwidth=75%] +image::topology.svg[Set Hostname topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/system/nacm-basic/test.adoc b/test/case/system/nacm-basic/test.adoc index d18b8d16..607aef29 100644 --- a/test/case/system/nacm-basic/test.adoc +++ b/test/case/system/nacm-basic/test.adoc @@ -1,4 +1,4 @@ -=== Basic NACM permissions +=== Basic NACM Permissions ifdef::topdoc[:imagesdir: {topdoc}../../test/case/system/nacm-basic] @@ -22,7 +22,7 @@ Verifies that: ==== Topology -image::topology.svg[Basic NACM permissions topology, align=center, scaledwidth=75%] +image::topology.svg[Basic NACM Permissions topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/system/ntp_client/test.adoc b/test/case/system/ntp_client/test.adoc index 6bd64e3e..fa3315e2 100644 --- a/test/case/system/ntp_client/test.adoc +++ b/test/case/system/ntp_client/test.adoc @@ -1,4 +1,4 @@ -=== Basic NTP client test +=== Basic NTP Client Test ifdef::topdoc[:imagesdir: {topdoc}../../test/case/system/ntp_client] @@ -8,7 +8,7 @@ Verify NTP client with multiple servers, ensure one get selected. ==== Topology -image::topology.svg[Basic NTP client test topology, align=center, scaledwidth=75%] +image::topology.svg[Basic NTP Client Test topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/system/timezone/test.adoc b/test/case/system/timezone/test.adoc index d266feab..056342c0 100644 --- a/test/case/system/timezone/test.adoc +++ b/test/case/system/timezone/test.adoc @@ -1,4 +1,4 @@ -=== Set timezone using timezone name +=== Set Timezone Using Timezone Name ifdef::topdoc[:imagesdir: {topdoc}../../test/case/system/timezone] @@ -8,7 +8,7 @@ Verify that it is possible to set timezone using timezone names. ==== Topology -image::topology.svg[Set timezone using timezone name topology, align=center, scaledwidth=75%] +image::topology.svg[Set Timezone Using Timezone Name topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/system/timezone_utc_offset/test.adoc b/test/case/system/timezone_utc_offset/test.adoc index a043e0ae..c3493680 100644 --- a/test/case/system/timezone_utc_offset/test.adoc +++ b/test/case/system/timezone_utc_offset/test.adoc @@ -1,4 +1,4 @@ -=== Set timezone with UTC offset +=== Set Timezone with UTC Offset ifdef::topdoc[:imagesdir: {topdoc}../../test/case/system/timezone_utc_offset] @@ -8,7 +8,7 @@ Verify that it is possible to set timezone using UTC offset ==== Topology -image::topology.svg[Set timezone with UTC offset topology, align=center, scaledwidth=75%] +image::topology.svg[Set Timezone with UTC Offset topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/system/upgrade/test.adoc b/test/case/system/upgrade/test.adoc index de3058e7..7bbb6ca2 100644 --- a/test/case/system/upgrade/test.adoc +++ b/test/case/system/upgrade/test.adoc @@ -1,4 +1,4 @@ -=== System upgrade +=== System Upgrade ifdef::topdoc[:imagesdir: {topdoc}../../test/case/system/upgrade] @@ -8,7 +8,7 @@ Verify system upgrade functionality. ==== Topology -image::topology.svg[System upgrade topology, align=center, scaledwidth=75%] +image::topology.svg[System Upgrade topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/system/user_admin/test.adoc b/test/case/system/user_admin/test.adoc index d8ffc370..a271ce28 100644 --- a/test/case/system/user_admin/test.adoc +++ b/test/case/system/user_admin/test.adoc @@ -1,4 +1,4 @@ -=== Add admin user +=== Add Admin User ifdef::topdoc[:imagesdir: {topdoc}../../test/case/system/user_admin] @@ -9,7 +9,7 @@ check that it when added as admin it is also the case in Linux. ==== Topology -image::topology.svg[Add admin user topology, align=center, scaledwidth=75%] +image::topology.svg[Add Admin User topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/case/use_case/dhcp_ntp_dns_combination/test.adoc b/test/case/use_case/dhcp_ntp_dns_combination/test.adoc index 4d32bc6b..007ffa76 100644 --- a/test/case/use_case/dhcp_ntp_dns_combination/test.adoc +++ b/test/case/use_case/dhcp_ntp_dns_combination/test.adoc @@ -1,4 +1,4 @@ -=== DHCP NTP DNS combination +=== DHCP NTP DNS Combination ifdef::topdoc[:imagesdir: {topdoc}../../test/case/use_case/dhcp_ntp_dns_combination] @@ -9,7 +9,7 @@ servers from a DHCP server. ==== Topology -image::topology.svg[DHCP NTP DNS combination topology, align=center, scaledwidth=75%] +image::topology.svg[DHCP NTP DNS Combination topology, align=center, scaledwidth=75%] ==== Sequence diff --git a/test/infamy/ptp.py b/test/infamy/ptp.py new file mode 100644 index 00000000..a2b8826c --- /dev/null +++ b/test/infamy/ptp.py @@ -0,0 +1,142 @@ +"""PTP (IEEE 1588) test helpers + +Query PTP operational data from the ieee1588-ptp-tt YANG model. +All functions are None-safe and intended for use with until(): + + until(lambda: ptp.is_time_receiver(target), attempts=60) +""" + + +def _get_instance(target, idx=0): + data = target.get_data("/ieee1588-ptp-tt:ptp") or {} + instances = (data.get("ptp", {}) + .get("instances", {}) + .get("instance", [])) + for inst in instances: + if inst.get("instance-index") == idx: + return inst + return None + + +def port_state(target, port_idx=1, inst_idx=0): + """Return port-state string for given port, or None.""" + inst = _get_instance(target, inst_idx) + if not inst: + return None + for port in inst.get("ports", {}).get("port", []): + if port.get("port-index") == port_idx: + return port.get("port-ds", {}).get("port-state") + return None + + +def is_time_receiver(target, port_idx=1, inst_idx=0): + """True when port is in time-receiver state.""" + return port_state(target, port_idx, inst_idx) == "time-receiver" + + +def is_time_transmitter(target, port_idx=1, inst_idx=0): + """True when port is in time-transmitter state.""" + return port_state(target, port_idx, inst_idx) == "time-transmitter" + + +def offset_ns(target, inst_idx=0): + """Return offset-from-time-transmitter in nanoseconds, or None. + + The YANG value is scaled nanoseconds (int64 × 2^16 stored as string). + """ + inst = _get_instance(target, inst_idx) + if not inst: + return None + raw = inst.get("current-ds", {}).get("offset-from-time-transmitter") + try: + return int(raw) // 65536 + except (TypeError, ValueError): + return None + + +def steps_removed(target, inst_idx=0): + """Return steps-removed count, or None.""" + inst = _get_instance(target, inst_idx) + return inst.get("current-ds", {}).get("steps-removed") if inst else None + + +def grandmaster_identity(target, inst_idx=0): + """Return grandmaster-identity string from parent-ds, or None.""" + inst = _get_instance(target, inst_idx) + return inst.get("parent-ds", {}).get("grandmaster-identity") if inst else None + + +def clock_identity(target, inst_idx=0): + """Return this device's clock-identity string from default-ds, or None.""" + inst = _get_instance(target, inst_idx) + return inst.get("default-ds", {}).get("clock-identity") if inst else None + + +def is_own_gm(target, inst_idx=0): + """True when device is its own grandmaster (acting as GM). + + Compares clock-identity to grandmaster-identity; equal means the + device won the BTCA election and is distributing its own time. + """ + cid = clock_identity(target, inst_idx) + gm = grandmaster_identity(target, inst_idx) + return cid is not None and cid == gm + + +def has_converged(target, threshold_ns=100_000, inst_idx=0): + """True when |offset-from-time-transmitter| < threshold_ns.""" + off = offset_ns(target, inst_idx) + if off is None: + return False + return abs(off) < threshold_ns + + +def port_state_dbg(target, port_idx=1, inst_idx=0): + """Return a diagnostic string with instance/port state, or an error hint. + + Useful in until() lambdas and test step output to show what is actually + being observed when a state check does not converge:: + + until(lambda: is_time_transmitter(gm) or not print(port_state_dbg(gm)), + attempts=60) + """ + data = target.get_data("/ieee1588-ptp-tt:ptp") or {} + if not data: + return f"{target.name}: no PTP operational data (ptp4l not running?)" + + instances = (data.get("ptp", {}) + .get("instances", {}) + .get("instance", [])) + if not instances: + return f"{target.name}: PTP data present but no instances" + + parts = [] + for inst in instances: + idx = inst.get("instance-index", "?") + for port in inst.get("ports", {}).get("port", []): + pidx = port.get("port-index", "?") + state = port.get("port-ds", {}).get("port-state", "?") + parts.append(f"inst={idx} port={pidx} state={state}") + + return f"{target.name}: " + (", ".join(parts) if parts else "no ports") + + +def default_threshold(env, logical_node, hops=1): + """Return a convergence threshold suited to the node's timestamping capability. + + Queries the physical topology for ptp-hwts on any link connected to the + physical node matched to logical_node. Returns 1000 ns (1 µs) per hop for + hardware-timestamping nodes or 100000 ns (100 µs) for software timestamping. + + Use hops=2 for a receiver behind a Boundary Clock — each BC hop adds + phc2sys relay jitter on multi-chip hardware. + + Pass --threshold-ns on the command line to override. + """ + phys = env.ltop.xlate(logical_node) + g = env.ptop.g + has_hwts = any( + "ptp-hwts" in data.get("provides", set()) + for _, _, data in g.edges(phys, data=True) + ) + return 1_000 * hops if has_hwts else 100_000 diff --git a/test/spec/Readme.adoc.in b/test/spec/Readme.adoc.in index 0e6a1dbe..52494153 100644 --- a/test/spec/Readme.adoc.in +++ b/test/spec/Readme.adoc.in @@ -56,6 +56,10 @@ include::../case/ntp/Readme.adoc[] <<< +include::../case/ptp/Readme.adoc[] + +<<< + include::../case/hardware/Readme.adoc[] <<<