From 2397e7684c24ec5c5f4511613e803a28a9bd1a90 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 9 Apr 2026 10:23:49 +0200 Subject: [PATCH 1/9] board/aarch64: drag Marvell ESPRESSObin in from the cold - Add a board README to explain how to get Infix running - Update mkimage.sh to allow creating images without boot loader Signed-off-by: Joachim Wiberg --- board/aarch64/README.md | 1 + board/aarch64/marvell-espressobin/README.md | 232 ++++++++++++++++++ .../marvell-espressobin/espressobin.png | Bin 0 -> 215410 bytes .../aarch64/marvell-espressobin/genimage.cfg | 25 -- .../marvell-espressobin/genimage.cfg.in | 61 +++++ utils/mkimage.sh | 36 ++- 6 files changed, 317 insertions(+), 38 deletions(-) create mode 100644 board/aarch64/marvell-espressobin/README.md create mode 100644 board/aarch64/marvell-espressobin/espressobin.png delete mode 100644 board/aarch64/marvell-espressobin/genimage.cfg create mode 100644 board/aarch64/marvell-espressobin/genimage.cfg.in diff --git a/board/aarch64/README.md b/board/aarch64/README.md index 61dca726b..bd6cf79e5 100644 --- a/board/aarch64/README.md +++ b/board/aarch64/README.md @@ -8,6 +8,7 @@ Board Specific Documentation - [Banana Pi BPi-R4](bananapi-bpi-r4/) - [Banana Pi BPi-R64](bananapi-bpi-r64/) - [Marvell CN9130-CRB](marvell-cn9130-crb/) +- [Marvell ESPRESSObin](marvell-espressobin/) - [Microchip SparX-5i PCB135 (eMMC)](microchip-sparx5-pcb135/) - [NanoPi R2S](friendlyarm-nanopi-r2s/) - [Raspberry Pi 64-bit](raspberrypi-rpi64/) diff --git a/board/aarch64/marvell-espressobin/README.md b/board/aarch64/marvell-espressobin/README.md new file mode 100644 index 000000000..409c1aeff --- /dev/null +++ b/board/aarch64/marvell-espressobin/README.md @@ -0,0 +1,232 @@ +# Marvell ESPRESSObin + +The board + +The [ESPRESSObin][0] is a single-board computer based on the [Marvell Armada +3720][1] (dual Cortex-A53, AArch64) SoC and the [Marvell 88E6341][2] (Topaz) +switch, oriented toward networking applications. + +The board design is old but the switch offers full Linux support, including +advanced TSN features for [IEEE 1588-2019][3] (PTP) and [IEEE 802.1AS-2020][4] +(gPTP). + +## Board Variants + +The board has gone through several hardware revisions: + +| Revision | Storage | Notes | +|------------|---------------------|--------------------------------| +| v1, v3, v5 | SPI NOR only | Obsolete; U-Boot always in SPI | +| v7 | SPI NOR + 4 GB eMMC | Current; SD and eMMC usable | +| Ultra | SPI NOR + 4 GB eMMC | High-end variant | + +On **all revisions** the Boot ROM is hardwired to load U-Boot from SPI NOR +flash. There is no strap or jumper to make the Boot ROM load directly from an +SD card. The SD card (or eMMC on v7/Ultra) is used only for the operating +system. + +## Building + +The ESPRESSObin uses ext4 for its rootfs partitions rather than the default +squashfs, because the stock SPI U-Boot lacks squashfs and `blkmap` support. +The `ext4` configuration snippet enables this. Apply it once after selecting +the defconfig, then build and compose the SD card image: + +```sh +make O=x-aarch64 aarch64_defconfig +make O=x-aarch64 apply-ext4 +make O=x-aarch64 + +utils/mkimage.sh -r x-aarch64 marvell-espressobin +``` + +The resulting image (`x-aarch64/images/infix-espressobin-sdcard.img`) contains +a GPT disk with the standard Infix partition layout, using ext4 instead of the +read-only squashfs: + +| Partition | Label | Contents | +|-----------|-----------|------------------------------| +| 1 | aux | RAUC upgrade state (ext4) | +| 2 | primary | Rootfs slot primary (ext4) | +| 3 | secondary | Rootfs slot secondary (ext4) | +| 4 | cfg | Persistent config (ext4) | +| 5 | var | Runtime data (ext4) | + +## Writing to SD Card + +```sh +dd if=infix-espressobin-sdcard.img of=/dev/sdX bs=4M status=progress conv=fsync +``` + +## Upgrading + +The build produces `x-aarch64/images/infix-aarch64-ext4.pkg`, a RAUC bundle +containing the ext4 rootfs. Once the board is running Infix, upgrade over the +network in the usual way: + +``` +upgrade ftp://192.168.1.1/infix-aarch64-ext4.pkg +``` + +RAUC writes the new rootfs to the inactive slot, updates `BOOT_ORDER` in +`/mnt/aux/uboot.env`, and the next boot picks it up automatically. + +> [!NOTE] +> Use `infix-aarch64-ext4.pkg`, not the standard `infix-aarch64.pkg`. The +> standard bundle contains a squashfs rootfs which the stock U-Boot cannot +> boot. + +## Booting with the Stock SPI U-Boot + +The stock Marvell U-Boot has `ext4load` and the standard variables +(`$kernel_addr`, `$fdt_addr`, `$loadaddr`, `$console`, `$image_name`, +`$fdt_name`) already set sensibly. Connect to the board's console port, the +micro USB connector, at 115200 8N1, interrupt autoboot, and paste the commands +below. + +### Environment Variable Reference + +These variables are pre-set in the stock U-Boot environment. Restore them +with these values if they are ever lost or corrupted: + +``` +setenv kernel_addr 0x5000000 +setenv fdt_addr 0x4f00000 +setenv loadaddr 0x5000000 +setenv console 'console=ttyMV0,115200 earlycon=ar3700_uart,0xd0012000' +setenv image_name boot/Image +setenv extra_params quiet +``` + +`$fdt_name` selects the device tree for your specific board revision: + +| Board revision | `fdt_name` value | +|----------------|-----------------------------------------------------| +| v3 / v5 | `boot/marvell/armada-3720-espressobin.dtb` | +| v7 | `boot/marvell/armada-3720-espressobin-v7.dtb` | +| Ultra | `boot/marvell/armada-3720-espressobin-ultra.dtb` | +| v3/v5 eMMC | `boot/marvell/armada-3720-espressobin-emmc.dtb` | +| v7 eMMC | `boot/marvell/armada-3720-espressobin-v7-emmc.dtb` | + +``` +setenv fdt_name boot/marvell/armada-3720-espressobin.dtb # adjust for your board +``` + +### Simple Boot + +Fixed boot from the primary slot, useful for initial bring-up: + +``` +setenv bootcmd 'mmc dev 0; \ + ext4load mmc 0:2 $kernel_addr $image_name; \ + ext4load mmc 0:2 $fdt_addr $fdt_name; \ + setenv bootargs $console root=PARTLABEL=primary rw rootwait $extra_params rauc.slot=primary; \ + booti $kernel_addr - $fdt_addr' +saveenv +``` + +### Automatic Slot Selection (RAUC Integration) + +The CLI `upgrade` command writes to the inactive slot and updates `uboot.env` +on the `aux` partition with the new boot order. On the next boot U-Boot reads +`BOOT_ORDER` from the aux partition and selects the appropriate slot. The +setup below also defines `bootcmd_primary`, `bootcmd_secondary`, and +`bootcmd_net` for manual use (see [Manual Slot Selection](#manual-slot-selection) +and [Netbooting](#netbooting)): + +``` +setenv bootcmd_boot \ + 'mmc dev 0; \ + ext4load mmc 0:$bootpart $kernel_addr $image_name; \ + ext4load mmc 0:$bootpart $fdt_addr $fdt_name; \ + setenv bootargs $console root=PARTLABEL=$bootslot rw rootwait $extra_params rauc.slot=$bootslot; \ + booti $kernel_addr - $fdt_addr' + +setenv bootcmd_primary 'setenv bootpart 2; setenv bootslot primary; run bootcmd_boot' +setenv bootcmd_secondary 'setenv bootpart 3; setenv bootslot secondary; run bootcmd_boot' + +setenv bootcmd_net \ + 'dhcp $kernel_addr $image_name; \ + tftpboot $fdt_addr $fdt_name; \ + setenv bootargs $console root=PARTLABEL=primary rw rootwait $extra_params rauc.slot=primary; \ + booti $kernel_addr - $fdt_addr' + +setenv bootcmd \ + 'setenv bootpart 2; setenv bootslot primary; setenv auxpart 1; \ + if ext4load mmc 0:$auxpart $loadaddr /uboot.env; then \ + env import -b $loadaddr $filesize BOOT_ORDER; \ + fi; \ + if test "$BOOT_ORDER" = "secondary primary" || \ + test "$BOOT_ORDER" = "secondary primary net"; then \ + setenv bootpart 3; setenv bootslot secondary; \ + fi; \ + if test "$BOOT_ORDER" = "net" || \ + test "$BOOT_ORDER" = "net primary" || \ + test "$BOOT_ORDER" = "net secondary primary"; then \ + run bootcmd_net; \ + fi; \ + echo ">> Booting $bootslot from mmc 0:$bootpart ..."; \ + run bootcmd_boot' + +saveenv +``` + +### Manual Slot Selection + +To boot a specific slot without waiting for the autoboot countdown, interrupt +the bootloader (press any key) and run one of the convenience commands defined +above: + +``` +run bootcmd_primary # boot from primary (partition 2) +run bootcmd_secondary # boot from secondary (partition 3) +run bootcmd_net # netboot via DHCP/TFTP +``` + +You can also force a permanent change to which slot boots next by setting +`BOOT_ORDER` directly from Linux (the change persists across reboots): + +```sh +fw_setenv BOOT_ORDER "secondary primary" # next boot: secondary +fw_setenv BOOT_ORDER "primary secondary" # next boot: primary (default) +``` + +### Netbooting + +The stock U-Boot supports TFTP. This is useful for testing a new kernel or +device tree without reflashing the SD card. Set up a TFTP server with the +contents of the Infix `boot/` directory (from the built rootfs at +`x-aarch64/target/boot/`) and configure the variables: + +``` +setenv serverip 192.168.1.1 # IP of your TFTP server +setenv ipaddr 192.168.1.100 # board IP (omit if using dhcp) +saveenv +``` + +Then netboot manually: + +``` +run bootcmd_net +``` + +`bootcmd_net` uses `dhcp` to obtain an IP address and the `$serverip` from +the DHCP server (option 66), then downloads `$image_name` and `$fdt_name` +via TFTP. The kernel mounts the primary SD card slot as root, so the SD +card must still be present. + +To make netbooting the default on next boot (e.g. for iterative kernel +development), set `BOOT_ORDER` from Linux: + +```sh +fw_setenv BOOT_ORDER "net primary secondary" +``` + +This causes `bootcmd` to attempt netboot first; on failure it falls through +to the primary slot on the SD card. + +[0]: https://wiki.espressobin.net/ +[1]: https://www.marvell.com/content/dam/marvell/en/public-collateral/embedded-processors/marvell-embedded-processors-armada-37xx-hardware-specifications.pdf +[2]: https://www.marvell.com/content/dam/marvell/en/public-collateral/switching/marvell-link-street-88E6341-product-brief.pdf +[3]: https://standards.ieee.org/ieee/1588/6825/ +[4]: https://standards.ieee.org/ieee/802.1AS/7121/ diff --git a/board/aarch64/marvell-espressobin/espressobin.png b/board/aarch64/marvell-espressobin/espressobin.png new file mode 100644 index 0000000000000000000000000000000000000000..14147c562d8f34c483fdb6ae579814fe817ab98b GIT binary patch literal 215410 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
F zj;DtAmMB^~;TJ(&*WBOVQ%y>qJ$s&#KYIFZPuCv6Cv?>JOsj&&PcFE;SaNZ>B-1-4 zlLb{#j*sQfnBpWUy_vt<+P_Xg-{8!9mO-l!q+w% ziSUCsYS=UeLuYR!sm=vZWOSXRs!S&(nJuuYXEvJv3+9W0EZ0n?l^hUKsbYv^Mqx|| zi8Cmh3RtCx+9p5TK&BNgrbx}SX0+Dh0W=(1vOqNt(Bok}jlU~R-UB4nrjb@W690!y zbSOXv9w7-4M#qGZ7*r}0wAREl>Op=oS|Q24AO(h;FLEHo@polBth5+zqmP?lmYS{* zAkQs%F8|JRD`UL8!V{^gipP&16U;+T(WYs*ySbt5dx~lz`-Kyi4TE7;Ovz17p>v{+ z*xYh9I};yZlcTdK`}!ap<_KjbNj{m)$+L_yHjKyaN0ef;dhLF+s=E0O9Mnq^LSP6aa!1pr#^uo#XK}0B>gE|RUGixC# zw%Z-Iw|Dfe8~KaGV=#$#A`;d*(6oDcJdJCpY{4&o_4ibh1)u)p51B5`DDsTf4gSrB zA(-LxTqQ)JQX;q=i*K2iiWf0x>{!$f2<-5;Pso|pff(g>!AU5}Fd-raVgRp=^k7O5 zSvUBFk{V*CQ&B0?Eh!=bN?Vlo|6tP6hsZI3Pzq#m0!tGmz`Nu3jS~LskyDxWO_R?1 z5T)6L(G%NhBgL>XOsb06Y)0@h`MbD$OkPxYt;uZ3YJSG*{EXZhCR0nOZ+Y?ZYj)dP z&gU7uJ5ZR&d@-T19VW9B36UHcjzR3bPsOEtPRW*xNKxcRo~-vAQYe9lxD0Hrguk|I%&NdSS5G`{Abx^cfhera3`BTv07D(YC%xsYlOlaNh? zPx^@7g%poGh`o;C>m8M2(*PVaMJ5P4a^cOs6w! zo{b~3HFNAFGB2C*5CmmUe?mSyP zqN=7W7Hc3-pS+l4X$r7Dv}$+YPsZWb&(p68TWvYQZk)Pah<2$H|*CtOn}L}1db7{1Fx6h>0Q8U zDFl6p^j=<@OZ>__&w1~?_gJi!oUhKt&)g^_&H`Oev){8@Z>aZTh~M`;)66n23v_0= z-)>o)tx(o*cD`g$&Zv!I?^>4UD=zv2?e#UwvlTb98;T;wd$|J`ui&1SPiZP8SDK6aeF2ehWhGiI{|tJRz`i!7D}bv@%yuQ4j}?z5*dR*4>z zBg=J4iZYn|VUs5Njx2f~y>z)Tj>RDZQ9XqM42AaaWyd}1gqJ?7IVEIxq4&#I&`SIR z1!_U1P0<>$fRp7n4p@#i1gg(Ml6l9Qn0}T8!xX&dd9dfSKK{P2+S+?<0qU zM0$&@LWA=SISi{5Fm*2g6~_D(O3nM9_Kwi%6n+6#(VK| z#tdMUONw)`lL~`oMJsevXf!_d7#)bli#?N%iHDowxsxU!B!6F>tO5t|OpIyTqr@jQ zx0a{^+E~os-D@m1E3lcSYaQiuhDR}(%vdbXP$rWCwkYvIGnq}PCKHSfwDp$l?OVF8 z<_GV;Lq>xoP}n@}TQY866%}{uEw{I~biU(kdd6fjLEYVrB3Z39P1h2FXEKvQ`tI(I zh-bN6GMUWSZnrYFPv@v;r=;MMqEpVJEE9*1-F{D5=1iw^n!dv-u?^GOinhk)bX`LT zg1j1Im{e0d!sYhfi~hnIio`k1G@S)kTU{4!UkVg2ZpDffDeg{jcMV$H-Ms}`+}+*X z-JReRC=Kq~Kybh3yLa3_z!(Wh&faT1&zz0XjGDW6svl8o{`($!abSi^pCpehD|m>^ zH)p!a7x_DwCg=l-z8PPHHjYJ<@_apGhvoPLZJ}tmnEz|oBfGTAJQfyRJfSv1Nf~0b zfU-s*={Flax4rlwn?D0V5$et`D?G*5d#6O-YU8Mu~pj z$potA9(pquA+FXjNwI%Y*=gy}%PoreWEiy`T~(!%&++KV%7q5)2)xqvx8-$e@9B}U zlK@%D6f@Hn{wa=rJt~T08wKvoDu<*_b#?8%U9@1g)GPdqhO=A!-j@?Qqrj^~MVnmF zfUpsJ7HW>fBm~qvflI7+9zA6uq(I>J<`GyzPoJN2LtbJl836|h|KZ``RAof|I~&Eb z&e)rEVb(ss0Yc;F*rzT*Y_KBZtx}8=ooX^wE95Nv2_Bj&Tu6`lx7Ms@)76bGE0UXE zJ|{fU{gW*Q%fcOVdh=FcG~~U?()>9Ux!_xYtOkn9nc*jJe!8Zj)y`=KNX1C9l9A)o zLa0-hX+d}IsDziM0rX|OC~|AanVA$bSux9He&qU(x@2VBp$!}w1G&5PyD@Gz#Zkfi z;SXp6Sp3{@K6A;jM>8&($=Jinw%0$E!#yIoS{k2mSnvLrGnOoRUmXqE^zl-~h!q6? z>C&y4h?cfuK=r{(m<+%mDbY&x)oEIFNUzbpHySl;GM#B%1x6TdKoetjGa&BjNYx0rp=m-S7U)9duxWZz5M8(q~>;<5is+h!nmI_pce+n&^~N%J@0(bfU656_{N z!Hr@NV9m%gRM$qUg|4jC-JN*}_ac#C;OgU4@mox>i!J{4D?F`uiX{?a!EZp=hB@B( zVGAU1<{X%V%zHX^_rYoYuaMdM9*RKJ_POIYANN0rNZ zE|KptuTy{9^Mp#!ux;kz9mZ#r4Dky&qU0Gcb_%7-*Pjy1)2SD%Az|zv=xf-qDal@# zQX&}+)YT6=%_)ja1sVHVTCaPHppfhIkU3zxYCv%ukle#BZ9vyM*@Naz84SfuJa)c; z)0DnIH6wf5K75O%2z-1J@;ekhX~q5VEp^c+En9Hdb_yw+FFBQSE{r_6v|L`T&JP>^ zhrFhT+a8r|j28bw`TRRiWTRe^hbp(DW(icXdU>kS zaFp4Ekil97A#%JN^7_1a7{F_cG=1#|p= zf;$~ErU{Z4Gq|onGX-u2A_6itV9T!E^+O;#qRn-aN(Hc9Ydd^jf{$s+*mnA=LKNKP zVq!9cN*irp1UNM;Y4w@z8`(KI+h$vhzR5>+6>utn)eFU6SXgJrP)4@rkbhF0KUshh zEkK2Eq+_=;5p2ph8~K%`ie-zJokn$JS<|0$ZZsw<`}c^YGIn$RQ-Kq+%<82c{hE51 zT(UOiNx~k%qn7$kPj1bdH}F6T6nnY3;b-rv`rjy3ptlGkLbTk)&t)QNp)Iws$X)Kk=;dRsWcZpW}CRZclVU zRh7~VHqTXwL|%_%o-cMOVE?+H&VF4c4Z0V%=lXM|K?zo-5jU4(6mEgNpBSw_j_Cjh z-xcGaX&^q{pc~wpypLdaHNV?@Dp0%8O{RvFl$a`xOoY$QNL^PcS;9AZfYIwH&oIf9 zMnA{(kw<>fgE{NT@3xD;PC*-tj$h{fN?yawYTQfnT$b6kvI#p_;ywOvg8%o6Z1el+ zRo4NPAlTFMMBXzBaBSWV>^WG<(s{nUnpK^=*m#0+W+{fVyZ*6%IEkuzIOEM@pFn;5 z>*C|XFyDMEaYsi*6>+rQ0C5D>LA`Hw^o@*rOqx3AzPJWn7pdsxDp2WhXT^pWXk`L{ zzo(k=H{-ri8_eXTpY6y>_CKmO9!p9^vMuA#*_7a}Ikvf<{~)pr$5^Z| z6~KK{F^ffo6y&FNWGGXUrtrWDP>9yvBY2Z7CnJ|rG5(?{B&yj^LDdnnPEKs8NJ?4y zB#%w&H7O($>4Cw3U|!F!7Lk^Tx1eT?#)!JBTEdlG{_C1-yFP;dHX)A}_7$6TWEkJ% z6B)W)=5-Mf)d0mq|4`s3zv$HYLU}Gq1O}`h>7zH;(Ej)$UxgL#tq3$ecYK+wAB4<5pOZrQ+So1-p6q_ zIRf|mqCo^D_k`OyoX4++WQJ*T14PIlY^rMtP6t|m&X2CsM}xGhBNz&0f10uX81T6n zd!h*V-io-{+kOB5JTOH1A4aG>pD=X1yv}$?k{j?w^J_j=F@XTOQ($x=kCv2(VSfu!1!!Zbmu@qfhQ)0RIAN<-XY-cg&Z%BIia}#UXH->m` z{CfBn(f0^_^$WZh2s7^(IpAMb*v7$Ow2K-1LrPI(;3c3&XzKd@Q)0@k_xG@PNoC}5 zT<(Dq7w6+Hfah4R=kQZ<(} zNijcb37`p_v;rg0IMP4=oWI)XW-~i0qbgY7Tcr_-r+`P?UV&)grjrdMw~%qHvuu37 zcjeRrH8cT1FxjkUqafLoBM0aREFy0-PPx=7N>vX&5$xi3%*TV>zZp{?&nFOL7fO{l zD?iWE;bwT^{gj-0XUcv|ZTKZ3Yw|z3>mN+uv0yL0yYCHhl2&6C0u^bB&Ar=9zx6_d zeKUvV2Tzfw#%CXE2Z(6JsE!+|<=~i&{GGylt@u=aagGs4u@}m_Qi@1ANuwcp2$@^h zXGQ+SZ7DFw-@G~|ais889nKw{eZ*q>`=1=TX|bR7-^unNO9Z-MHwvn&kwiL`JQr(V z5DvKUp0hWfE>(OUugN+;u&@vq7&>@A9C*O4ujtJlE{aszf#C>5EO!Xf=JWvRdl7VW z`q$pxZqAW0{}j$nJRtM8RAHpLTpX#y71BD)rfYFzey2v0H_N978u!})WFWu7r_J}4 z?u0*%01lBQprO~%Gb)&_q*Fb^I78uKEo{;QaeX`G?e>1a-yl<8u~N69O4Z>=y~n2T zzory@JyscajX3M>?UHT}PhHcKvpn(33?b4W)Sh~CNB~FoofQL+7XKmfZT%>b|D{xQPgAEJM>kr8fDbIYX9eraPSl+r zQu6#-kb97=O0oqprDS10?I?+!ntZB-Tt~V|qau#ZBo6K_;;f1?e}&y+>HcY%icg^5 zkoC85j?`+D5^9 z2^o5!_3mEoqKQ0C#HH)B+?PiRateUeWGc8Pi&3`veS&qll$E@=Th?$FX#Dw{0S09u zfXbkl+~YmziCfXy?V+yFvS25y^$=G}78-95+@T8%5U*9ow%swuNkoaqd$~9NL0uS` zkHC=VCQnBES-DzK)g|#ECstvyOlR@3WaY=*m&e2@yzJcEU9HcimEUxi(GH}3qU4I> zb9rIB)E)}-Ni#^ST;F3IjRK<7w^&ByUXj=^DqKNwv*@$enjP5nyJqCq$AH2F1-B)?D;YOI^~(dvKd*vEu#@ORewPNIsa&A`|XqO6}!UA z+iV8lw@@cf1*^&G0c;Yrk(zr-!?#~o(Dqn)2WJVKAt?FtC~xn?PtEE?stC*C5LQW!t7^)UW{MT zjzsUB8~Zf|qK;(cqV#ez8K#6hg$>-%P!_2KYCs|=io;<21?*kE13)0n;+V3$2a3?X z49FhXLk*dWLx_QGy<_CW6Bx4%X{0V9zuff}nwo#bY7oj}&RA*`gYc3W2i@?$oxZ_D zujt+;MEABs#)Q1D;~O*8W-OVk+_wGV;6VwTdT$?E#`WVS;t{?E{pk*I|Gu$}UU?>K z9`M`8^+U}E(q}H?{epZ{4|hgn^9U%>=4>Y%#m|BvXRK52y!ciZ-_aJ1MrD}-n&Nj{ zQCUW;TtE4EeVnKr=sc&X^0$&>No)|cqc1qgz8=Q=h>?eEA9#*UOrWe}G4p0IW0J31 zjYdyd_r5-BJUYtQ;B~-`<{HxXty#r4zEhTG<5}6kf(vuy25Qhfgst3TL58Z+1nnj? zb@G4L$qDg_(3+J*=Zg}ptC$o3&c2P8dr!NeIY^qjih2bI!u<4&EG1+Tf3BtXB}UFyQEEnR2T z@T6FhWB&8eiWH%V%|aNaC9jIfDz@p-utnWOq5K9T4gMU}m$o7C;7l}YG`C}pp3m`9 zjd*}BcxB$?=o`VOcf282xs^;}6>@y3vGAXUtefY{e-B3dYPb0zH$l2thOAiUj&ty} zI{@jYlCkfLGIrMAlEEmfz&JE9R0KSFwd%jR2Ub!et>^4g(@R4#*DvFJZ|8^pkEVYt90*k~X`S#m=|Z-HIIm^fCT@OhTKjnC(t|CIKGeb`i_-?OX4 zNnjEbvw-+id)TZ8yVypA3u%FetTR8gnjGEjbrf6kSRFCF0XrRX+(G#jH|3XL$mc&&V=8E(@fu%^vQ+;;sE6n z6)pizGQA1wd1=cC(rAqp2-6xOs@4sx^5l(2{X5Uu`y;evR)mB63;c0V(XAq+yAA<; zH@~j!y$wVls_*}D!Y`#(pFInjI{a-x+t8lS#$d*Ez)qoI7%r5JwG<~?P^Q^WN#7YU z!6qr$(}p3j5@k#iTMPLP)vZB&_faEx5A>62NRaIOck$6Rr4QVRZV`iK_=p{r#+x}V z-59>lrWjUVC{ERV{4cD#tN{g4itW6L-!K`3k0QurxA$&ATbDwNwb-Hf1@FA+h#cbK z6C6>bw>I+WAhpLCABGVbhlgo7)jpRZvJv#@^>zK|uVM(xJ+2K<2@iQ+NAEsk>oZuG zjq5vhaGu%)v2)x2sIC8EOQEH)(mQ;Po?gGNBCEiDjmQuQm^#i^A~RXFUf|_!M$LU$ z4Zxq@r{aqnj3*9hNex(>iwkj{HNDirhrRWl&j~R6GwCpJ^{lMdzB-uZemgOKwR?RT z3z~J+T8vo;e7Ou0-JX4W7Cpoic_fDRyixZC++ySfJrFCcBxb#f8T<7^{xJDx0i!E0 z7AUoQZ_mnJ=j2oiH{DCN>gahtk0!bpcO4LQMXhInPT<)^rnIuq>i1ZXLlwkXBgUW2 z>E2V+20z4>A2aaubww_Iu-Zr#t;1o?g%XugbvdQInts~esDL{e_79$kpR_ zkds;%W5a$kV$ew3StFY74($rXvPY8K?1cuqVmBm0ZO}p5ifA|0xn}46z8+$W_771( zPfof_+S$eiuC9Tjs}Cg?^n`8%Rk#OWd?~|-aUD52$xam5b9B*=b;hFjoZ>d74Oxzm zG`@hj%*cBpYVl9P!n66Bzq70_X~t=&>^hMsC-{-KxB@m8WWLU zb9c>~2(#C!_O=zS^KOhdzS%cFq9U(To4YWT|8dc9Rgpp4tu2B|E0z*<#hwR_AyN4E zzxbsA`-sH8x4X@TuCD!q8#Hp!?mbKq|2)6LFP2)$M;{eUh8e6ii3}68I0$tQSI_I&j@Hcxhe+Q4a9oS9rm-8rY&M3uM3)h11ESf}xTk>*a z#`(`$GY;;NH$9|ZVqeRcidYWStS!c)0K4^?^{y{5Z(KuDb{2Im>sY@k!dEK3WSN2H zh*^#B7PHEbX4L2+bVRZ!-5SnHsLRM%;{4f=>Ox3BD*~V=~ww zo(jB2D`j+iKD^mn+qhi&1zf3~0dMHEUP)}kDqbu$Aw(a!Am?8`NKbVnXHGco= zj9&C)qn<#;6Vfi5otFz+6up;7p7*~w|KE8u@(GjT^5XKTI#t14gzYi4AZ?dmu!$}d z`}FD0udDWN#4miz3skcDoO!doa-xs^9WW6E|*<$Axg($@! zT*~ZKdAeP0Zju*&|C~VZSv_X}`*R%Tt>6oM z;TP;0t2SzK@&ypWqNVg0MW^-4Zf;%s6T1{wQO3`~Kt`TiC@^>uorf>@oO&{~oP&UZ z6f({1w7JF(5C=~qg%-J##@5ecORCvnW@rxGJqBX}<_|60_1`c4_jU{6z{ z1uFiJ->S@t5){Bs_wPOBcb;E`Y(!+H=A~1D8^2H*5-psz%zN@}1eBLKe+@KjZeEN` z>pzz%isbl!Ts>W8OnnCJd3N{qLYHi%v?6FV)bN7m4)IIJM@W{ev0WJo94w@yG7git z?RU8K@IRTGT3KNxqlxV_!ZU-37?yJ`y2Hww+gND7mpq(peP*M5B%79?gryjV68JP(#KyR|A@n4AN)ss`498nuHJHyFezZ7 zFL7`1tbI?vo(RkQ?_a6O4Raf-@vem8!xFxi%>gcNKoO4)M=@H_;t*ZLM2q9!=XCT0 z$7X-0RQ&#oPHdr?aiU3lG!%y_bT96=AuvBztqBp6)HXn_txf8)Qk%QEKGtwi zW~d@MuVL%R=)_pY2x}nbRon#iw4>*PW9 zVr=M6JEuq0+GEu$z9M&Iol8MA(XyQ678+h8AvrUxIc!_A+qu=SJWS*j65$r~j#BaS z;i@F0Zud~j{t%BRQ4JlbvK0A581#1_!{!H$)pvJnn#!qfMVOl|C!?3*e%)KW5;C%b zSRa@%3aCj@^`NwNfB9`_`B=N!24b&6hDbtOf2=-6&kNi-3{~mW-JkZo+5xLcV!ARy zT_>XVUv8BJ=HeD!ljt=sgYBPU8z_h%sYy|swJfYOhk;E4z25lP69)Di?$V%TKHkPV zZJeOafNTUNkbQyIG*QzmE&^+1*8s*MhO*FT#Jx}_5jEqj0=Y^;TC)Qm_*P0W|chXdjyIziyhW#i=8>zOrMR$-%AR!m=ABUMwKT!nsudWhn8IJAf zsLS3$m{$O2&Gcefci@YjdZPUFQOVl=;E9D*y`#LxoZ& z2rufcC^T3V(@WDys>!*es}^pREwJE+RQyKxxe(Y0FHg7E@~(Rh45b3mx85I00by#yE3+H*9UVD|W|3589o@y{Uosv4 zhj%q|qRqX9iByebDyCe^W#bEh45R92$Xo|F9p>oeH4+ICeLy42jb%(^8<8h%v7`>C z>yn!jjn-m#c)iYe7IGj0E<>=jj8R_)u!ao~b@dIL5PhI~L(E!dX5AFpeLQD&;JsK{ ztb14j^oZKv4sSyi!nJeWb~PStMqO#h;y)-oN6h){AII4vY5TsR*qVDy-i6T*3A^bw zaS5}k#&5|9Pc-f53$&CDy$#g5aDM~^RTV0cEUWnymrG(#U{bZfCoUzw_YQ7b?wK17_P#+RgAnc0H z3n7_QtX*Ln_sE<*L^o4tf|gGvGO`|xQ9gbdy8Xx8=44!WEf%7-a?{*@2omG3nin&R z%r-bo@Kn!_maIF}ikN`exTRWvNW1W_R>cuwLF_+cWUD&UdH%BiMYc21-FxZSnj_rCG-`A}Njqi6=l1D+mI1@g`!zgbd7H@vmCZ%# z?jOjf;iDsdCzYQ$5kZn08cK=B==hr1PVx0W+Zj-?{U z#y`-J-AzO6083T9BMm|ngX@Wp)Q)3rZCdCEI_o4{Hi;T&!j+#6j9SYAO*ka>@=BC% zwSzENw$$3WCS6YR8ee2>KK~-z?Abco%f8FynzH14oHuQD9&jghha0QLD>*#?%GlaF zzR-WeDfuM6sq-YTp}c_M7bMK#agZF7Aul>`If$GUeMnace6j0&Ibh(?oxFLck2f`= z%y0mI^tDvaNXT!dE-aU#=Puq~#UH4z+usR_{eB*wYfAtV4#6kq!|CBdgm|XC zIATL0!jtk71zO9+o1GTLWRl#$15*EC%Ssv<=lV_Rc^*|r2}^FcWnPeB*JH$DZR}S} z(9vDHF=A|rG1H8ErYx2DnHMR0A{pb{tw4w@6SS8kiJlOz$-p{dBvtJm>PZ>QT9nii zgZFv_@0He0U-j+V&Nz6i%)Px?F6BxcJBEK)QGT1+rDixE$CN7C4K7T$`MC-Gw77o) zb*PHB?&0+9P^W#ytyQ;4kBMIpqkFL+wCiy&_CVN2pGiI^;l2pN*yjkLB`7cKLl0>>u)^#8W`Pp+0& z&L(HnDwQnQA0d=IR86mw541NXiw`%;cdN!q$qKRcd4ThBrB?&{FN!g^mB`H;Ea7d^juGwVem8eoP)`BpgQut2T zLTesFIMNzVNFEI$s9>1-$3zm5j!2wKX;~};K=ephphxGfw_3EFrcZzTTWZnPC-w73 zGU@^bGf;i3@*{k{{J(q*^fWZc`U z8M(pwA>d(&YiIDZ!5#N!#x1u1$ z9s?I2s7Tb($1*NZLQ#y>+U=PqNv`ly>a6HnG;f`;`i$-5w@Zr=n-%sZVGAIo-E#nG z%Y#AY)zbKvY=3~cHjVoBf!#WX+%3j^?vytNE`9VUWv}07Gz9#ZdWp4g@34glH#I%$ zzv1-Y^k{T!`EZwj{xT&gETiHU_!$sPF*m|in(rxHvUbk*JEy&``FLcYY`C#o3qldC8UP#Si!O6nu@rdbADc%4<eS@f^R(2vd6c?0G8;&I0^4?*>V_?7KoAu1_=4uq z;TUt|9bGmC$DFY*tLJo#vFyLLJ|Mt~OZ#%gf+QY7Wi$zT^x<)BP3f)jnRMYCRj zLugcI(3#K883`b5Jv#~BAIa_nWPaaCn6(TwENMforj`|6k6z7KW?6{G5;k9N+oJO( z3oni~s99L+bZb@Sy%kl_4ac%{>Hq=nfoB^~&;OQOK;jpSiPsQ5+4zovOXy0PSE&Ua$JR?X0UOTp{Ma56gb8)I$8{CH2$M1J(^08k3R^A@=pn@nOV} z4=d-yqhkIBIYUx8;`NT=3L$N3)!fMID-gag1(+Vkk2cSKsn~BH6UQ9npu#Mz6&mDz zZG>4ha{Rezi&Qpa1U zje}61)rq#cjL|LPk;V0w=d3GxkLWdAwY!z^0bez8PR*~DsCT;X|jnnvpKw=ck~4C z4Yg=mRb0<)AaN<0Lm71`uBD|n=QdW`Pq9BhqBC3gV&s1p?8BV3(2(|SGuFn4p|sGU z(@Ia(#G;tGORwglLp2Pi%=>6e4)@dG)sQr>Pil&uA1P$F&ZZz1j9{Eqgq!5NPOavO ze!;Tg5)gSSERg*nAm?qP9wzc5C4y?Fn7SGs&qE6KLphTX@=&79}w zNHP&t`wlhVt^E&znXqzn^eCp8@w1t<)r({hJB*pkz7tt`LW%ES@N=)yM@gF>%D6w& zWtK_BlGFe0wSW6_8&bSyj+l-*x3>Pz+s{u1a;H%xG>gIWSW_`!HJ5}{rOAeD#p
3{hAe6-7*S)x{m4n4^{e<-#zbhj}wc$A9;?$6kP47Zfq!yz&8w z#^RbLEPQu|Je-+J#kt2B&_o;ng`SJ22C74hBn<0S9M52eGn~-wl1lx(FzMm$8rmx zM1?m}yv@x4Lymm~ckryn!iP&)X@CTMxr}Z{5lhHSH2#qIbSZeme#X?u>>8g?=kC}e zpO;a&ePIK2Ie}rCtr;-x?*F?2dW{iAk*=*!5@hNO&%M9u)+hn><_)qqzG0bs=`4k7 z>ebk0Bq1K@gQ}KNYf`q-@2k)2i*)HM1PB}X!|+JVLYt^-yu>1c|Y>@x2H&$mZxx$kgpx{r4eDCBd(q z`d`-W`U+*)i{UkYfURb6p@m=FPaK2>zU#1Ohxms=2mJy#tn_KiJ)@1GJ!0TJRr%Hi z0ETb)3R4QFLiLaE+$F$sF^g--u}Y|m=!Z<+N12`iM*Hw0dbMO%6G^uEeBfi>_t6ud z$5P$bDwNRJPRwbOU`N6manT_(PV!~K4ht_yQ9N$WzA*9-Wwc2Ve|v>sOC)n8?Fb!^65 zPE63`Q3#Lo5ucmdJBHag5$-HFpZ|jtLaY|2TM&(59q5)_G3}KOAa=Tg$9VD z;sPPCDLM58lf?ufA*p-9j2A|i@0h=7G%Rri6r6DdM1h9h3>{poHs#+KevYEgp{s%l z87l_sf>v!1rH)(5XfCCpIZp#4g9Hm=;Q2c|>@iF)V`w`%kDXnTIZM@9X5+*gO#qy) z%INSneBVRV_8V81{<``!4C>jGSZTi_Cn){aV6cE8oWh;Rb4=F3zSqPQ*gEhe9*I#x z{2tVI0*Y9^(L}md`jTyvl#~_yP+iECIX%t&GUwvqDYpV`5AulvgB8M8#t<`xC|=Jn zD^4?=`d(6(cBAC%U`Lm=el zBPsyc(98QviGQ_Fga93mUgX#t+X3H}ot>R`6nN`=b7%V9ciFWoz$xCWd0R$>TrGvO zwM+1C>EXe&{&9A)g*`+eTnbgo_x^$sb!dH^4CuUidC`5$mL((}Q!|}w6RflyeF6J* z_w;0_^F8$*0;AGe{4kt~qT;3`+X_vI<{J0^61k1ueOk<3!`S8OUAR{wizp_hj|_fM z4rV*iTSFfWOg|#Ei3R&%8<*U94J?k#W_(vU?57YiiC8DWW{UKrTJ6M{*E4es2KvA* zL>b@J>?W?vJ3NlWKhHP`v@*cx44K4OC3{vq$V6OOV!aB-&bey1J$HH-VCE)!wDTjN zh&#bwVM*A+ST7918@{Ui1ABZF!$kaPm5n4w0AmaS5d=`TIZUpmC2lf}**Gnn(C!V8 zt^`M)Xy*8DlLnOyp9;gz4t5#wPrYSi!&_Yr`tB@;K0bjz$Pn;5dU{G~EnT*!=qEE< zp|ZrwqV7Twi}WiioW`uWb!pZZ4quWZ)*U6ZqM#Nlm}~0}tB!g$I<+fPVA`2Grb)#2 z(}meC9QtZmIGdo+oZ;rzV~Q7)zWbv?14EF({Y?CLRV+uT}{%_V{Tv210O0E)Xe{1m-+)Mv_k!eu&l*2vv9 zx0*l_ogrg3bxpKmXJY~dT(|}3M`2mBBHO-gCa3S~saw?6^ru0onT#O|<>d?bd%t7h z4w(fMY`8OMq7?6RpY;OVf2h0IpJENy&BdSx5LTct7B=R_!TpNu40f2daEe!P=;>-d~?p z8aamwmhG}cs}Ft&v|7ot#-1n~qoY_I;wKZ##*_L{QE2^*l&&+fnO0`-)3Sf6tOg^x zPpMjovo>a6FA^bI$@ShBMz=KgpPG9!A46?}y&6&~OQuPfGA6`)Y`ER%ix;}fi>w(a zf5^S$2$)7>F_fp>d)PAfhI<$;Ubp=TjPs!0cNJ5!np);02i`T2=hay?^Y%t6u-(6& zJIYFf`fO$&?>BZ->ItB&Q@`ofv$`NTOv7rKywQZ9NJo~vZwiPj_8uKWAod`JhDOj7 zVWu?cHC9$P8?N;T|;n@ewye*l-VPv150)emM5`q0wKk2a>*5(R)Wlo1%HR$ z_zU+}5jaj%;hMCJcPSaV&j))Q2$#vItjOSE}y8|RIQ%9{VS{HCtn=0&y6L0iMIk_rvS!Y^&K^ld#;XsxJVz+2`_%7`%^q$Rx1`J0a+_l&O#H#(Mm& z4!9}*oVQLLqQM5gag02SR777A)pSD~%q0lbp3Yl`5LFULI<;%PHc_0KQNs2XYEM;( zDk~%V!E6q&r}6Ed)=?=L74eFsOI*fE$?r;3js89XQ{C=?DK4qhBuYx|h>Z$!oT#kV z=1@|B==C|}e8UszclUo9phd`k_g1gZ{@Vf9#P5NLPO}A$8Q-H!=q_UK$(<^qPS2a8 znBLOcB#oi|DP^)NDK*9auu)r~r#y&3p~CiHNdDPd$}u|KYAhm9oz{-q;#<;}>=o~v zZD#akoa*7n^DeTstlU-#dMj6xB;G9cM1LAkw@lP4HsiBIU2}QamD=Xh!?1$lAy8MN zn+3(cE4TUhKdYVBW`NA%8PQX&3lZCAs~#hj6(*@}GQxTRm>C}S!@TMW{*CYS{u2Dm zjIw!*GyENkGQE6=j?w30d8US$8J5q*1A0+pq{PHJR(HBJ$`dUGPJ;H3rfRGI-G#%G zZWyXY-sNa5zHs|UJNZSAzgI(*yGK#xv(MPMIbBKi7^!s_?|seOpU4DCrD`dr^w81r zuVgMm;S|hKtBG`R_pn%LgNCjsZ%?)tpJ5!+(uAb-#w)7 zAT8+b%h?~d*4pvG@y3PZzab>ZW^|YljQy0D^bCU85lCM~!iUh@@DHUsds7G+@rPF- zq=dwOG&34Y5thypoJ;X>nw9Ly1%dF4)Empe@gB z=oH%C7z)caGJ-u?4o#n@Qi0R!RK5~NR_D82+@$4QJ!h4*_YU#Vd5It|Z9BH%M~Of| zEXqF638cmcnfX|_RzGR0iptboiU~x(b#vT*Q9U=9yA+yJpHTJH!i}fAA~K`!c*x3S#!$fAKWn(ptJ)PrRrVWzQM}4 z$~%KLF%ul&%aRdZm0Eh;MLW79+N#9~w6xC1CgKyO2oJ+mi}RCo!(`nU(T9sPrw@=-djc+N8%?qm*G_PoRW z6wK$gqI$g#k9PkXc(umJ-{hmrEO23HX}JXY1_PG$t}a=na9J$4jVD+Eiog350Di!1QJye)fXS0X+o{ewD5RPtdne(dQA0 zOC-@JptYCwT@u=QPJSxT>IKxm_W|kXBR6FFy#u-huWDXiy-{65LJ+C6LIvkZ_opaK zu@>EWK2VYlZNVj8hAB6=qJ{d7%+~(W45@Q?FA{FsKh}(}BO0FaPrJgk^|_ct?mmLk zv7f@G$DhD_woEFGJ<2T?B8zdwAMMwuys>qB4n6jR5`Z=4AK}Yj64qzz+z0Ek0(g7Q znlxiqv^0<6*;|FPCp~_IKuG}Rac`;@FQt2U$!-aXM7G&35m&p9!9G9zTSPQOT4Q#0 z^vS61>cKxU=T`Pfu%+_HpPASkVmxn!mFsJXsCj-LTJiB-r{gCzzJ#9x-mBJne#sy4 z)s9S@7x(`4SY4~*(W3P#ifIvbPw;gZxOuuvKD$qwwiKsNGYEi%zD%T)PP(G+sC7Aw zz~Anl^4=F%fl>{!tNDaM4F=Qv=K{V?#MNb))9pbw@vYE5Ji&T~PXD|wPiUW72Z)qR zxH^Kx$M(p~RA+FLFD^@Zs!K1E|20H?8DeK-4OfyhXh!GGdFJ56M$Z^eX<0VaT-$4h z7al`$=~8IbJI*|jz2eZr4i-zx>`j~Us-gM3E~A#F4Tl@0CGMYF^huLIo?#}3j8;?Z z3ySqXl2u3e29_5tz`S)M{q~9`4#GxhzD6Ue~^f@*4O+jhpTHl;ZU| z>E`CBne%r+1brbzu;`wq?(>hpgIzZ#Z{eEiyYI~jBA9~^ua(jJ;Th4xedb|N^Z{{M zZDf;3|?1liW4n<=2h8Koc)7?@nqx;o*1QI$@}=&)3{TAe4}~ z;$*boK?Y34wE_lx15mwzT!z2e)#aG6o2RF=nk9OORgb|=k5zBHTg9pQwuReOW9c z8JYZRKRJVp@8S&$iLtpc;?P3+(s$|%0DNnTP(cm>!{psbVpW)1A{HabmGu=d1AJmL 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�&AWoz7dZt9NX%=T-G>)sycI>>5)g;TY*0Qvf#mYuz*t5#Dy*Y8@)`W zT7x9zu%0XtUg|yH@lsV9AjqXO+k3aL^bzyRx2Z3HHX=)OYnjPFiAa=5msw$$^_V-1 z$go%P`dB`smyXr4)>{=LAxs}+cHxY4!h4}LR6xT`5_B|iroaIBYw}>N(_1R{AUh}b z3Wy9eW#Q`fT7ZoO$#zGgZ!P-ge{dl5h2HYYY)UtUU0tL-I{p_yoo4r^*z?Omx#uVXcUkdEIPGMFvFYddy-IFu|(|H7!4Kj6J6u&sCR`< zJw7!2W~C`TVZUB5OY{u7xxuN=_f_Yu4t$XnkAQn49X8Lte7rpE zU-Gr-Xq$#!wa48av@iSK^;jVLA}v_<)qAaRwpzmVCK`1;;U#}ZnIDTL21Si@C=y|@ zd))JofBC1%*qQ|eUAQ$ePTeyD4W~oOkDj}-`7jcBn!E>GTYAT7J86qt2NLDyBeE7x zc07=-mvy*urv>crwzC!xj=~8y>QG}$n{A=B2~J~LA6h~(Y0({xOpnvtfs@-<7Msy^ z-rK(8QAXID1PTJR4n_x+kN_550fr;rG?GA*_XJ?LB_Egc!X{$UvZksY&!?zUvyd`hxk91CgIS=7VZ}rnj6ubG zm!ino*1lyUP%sxS@|5@H+xdDZgr`wPL@mLOjv~n#EHW&$${=|Jp*87uHX%K+n{Mh&J?6 z4ovSX7$K511Wg+R1{Qs|2%r~Rg6%be?qAIHt^6&=kJBbXb*aH^Rx`LyR_?gD02)oJ zP<3lFI9qESAHL+!{hcZE6XaygZ0y>QvP~>hG)v}av#H8THfgS-no^kMC|ac&UC1;A z`J^B-5b=)ucP;O^>fRp5R71lakB{Pea~jUjJ<`ANND1(cyoh;wNS5ky@D`YsIUbuc~RZ8rIXseN8Nf=!h>xo9~pDUh*V^FCzHq?cPuD*N_nW zTBGgiotJt3RJLN((lBj?N@UtgQ!Wki5*!VC=CX!mJu4a+Mdq$%hoPM94zA?zm8oxo zcfS~F|0t@Sq!LRiPOZs(AvK!kN{1@okqGXV`l4EL*_6`%=PpsBqDh<+&JdF!)D_p5 zAe6N{dRY zO>ESmP%KC7OWvp3~cd@?mc5oMO$D6m1&X2G+Paw&KJFqPfB-INU5l)72i}~~^Sd{5{xEC_IG-sNX=>#g+;4YK{*#ZG^~l45s;(S_ zM0m;zUd)Q!IqVbi$wrhKSvH%k&pK9l>vgR~h@XO?)&h1$WMP#jr>tnGs>vyg`$%&7 zk1Cl+#?P1|%z_i?cFndu@!NYBkrW&xWU&tu%^MKl*Hid?hqtQ60;NqHO*%rA$2qcd zp~FB{GaoWA+GL9mN;VQBT*bs9v)0{e0IN?B?$+j?t+n@~Y?c_LUBjOze`ysvEGj3{ zYY$#v&&E4k(w)`DVo*5ZW}4Dwp^Nr<+<_N)e-D_L9;URkEQ>`)Kg{?fX_#m)w@*9> z8R68aLJEnUwvs16opK%;K4Qd^3Fqe{O1#0AU#xKbzZYP^(iGNZ=SCfcEu&@ADGeU-#UEbU6A)oH_k1W$^wk7w|Y-d#Hq%{fpD$7=8pExBx#+ z6ghS`?YxYqtkTnJl>eHrR98&wLxxe7YKC3)P4FOcL%mS{;YnLl?KBtKYJ5P{q{&=< zo8$;ChCzdG??kQ3%o`507UfYI|K!p93kR>BG*6bttSPan{HaA~K(m4i21`PU z=3Zwvr)m$8!6?5zznCRv5yVlMVDvj*!2ntLkASKsonVW|SJa4JOnStKV(YK<<4gO* z>J(&CaVJeIG{!PF5Ud=1O~OvBK`aiw#jXDQZ&5>J5f4f8(lIqe`;4 zjVh@~Ah9Lhu0?>Il+G_uZi5sAr3ArA_3SI4JMkEbdJuVsbnTo6zF`KwEo0+Va%WMQ zg0fXqLn@Tjm4iP+6J2h2c!f-DRjPgnACaANEy{AGRr&CM755FZsU@T7ONmoS|KZP( zXtq+<&iKOAip5StpZAU0>?OLhZ!mQwRyiflIt!eVLA|O1(z>fispgE`0veJ!$k(%{ zZjps>=lKnIDRi+|RoB|JABqUF&D(UbOOQGgBLpWID@bq|qFYg9!5B(30helqillkE-JO3Ri+`p9>%xGmcid&wXC`d{y#DD3H5_hW ziWr4sfL)J8CJBu$Nhv#`V^g^lHZ$g()|mgQl5}G9|`c1D&+-CSaF#&>N*EF zQ>Lk!cbf?SQp`dY3ibdA329O~?y*OvvXe&}VM zq9I zy?PtHhWooWBr=xL$XxyaTdIg77%77d|MsOrE|XJ5JDCox+FY$ux4Q2Wc!N*F{q}Df z)%F}yegm1F!^(pmhD!;<=L(zjy?K)4@xAM+MtdADHI7-s^M(V%q|WxVf&Rks73bCj z$sTr$Y>m_*sm+kk!lu2E7%UB6X?$*Gl0Ui@Vt;uy=eEi5YSoGR%r`#C^-QRa>c-f5 zRCBK&tQhpXJ-hYtot`$W=5>^OOulDIQS)NOg0+P=&RaTgZvK1uEdP;4V_jKr@~@3(A3Lnn!0X4FYNwwl9x+{D)wv3s7i%(D6B$xWVrsYK;E@7Y<6FPO05|NnJI8>pIE270 zso7!eJJ%sLpMqiP9?u{RM`$l>5)Bns5>SWjG2Fh}9g6r;`TnYOu~A=*%w*Yey#)CFD&jq*i&^LD<9lOjpjIRI9^V1Jn+G+9A#?zb@iYH?7v0+v&k_a!IO07zB z8qyTo@<#FXP5eeE9N}g8mrJJM%>`|ET2o(vSl^zze%uZv0qJ`bUF5Auh3P_f@~>odGq)I`ajZMFU#vw zq}YtU2~4Hv1Z!Uh*AUAz;;}Zlz|_P>Mee33meEsy4H>dSa#PVf7|BQL5l0#n^q;S2 z#IYx+#F*y-qUtdbe+-2kLS3}pU8IE9XBrh7Pp8h@F8waAO4%&r!nNp*>^kMa4?h{m z$?CJt8=jB8UUc+&sZ=`Jdc3P{E8gTN&Ork&hcPG+pa__GixEUb&c^hsV3Zm#*0y0l z`Zr{?i6xJY*$K6tKf_Gm*;#yi+EpW4ZyCMcd`A;`yG&r@VJ^LHN%1fhuU(`ysxfml zyAL(-ctzh?ndgG5)U~W|s^~KwW-70uJ#t%410tPCG$l;YPhXUebt;|&VpwGU)`hf! z!Z-WZc>2Ij=gnBEVik z;xkiPM|MI>R|sd$(E;UFc;8#Ic0Sw}=uU8V@9&=vo3`rPzQjyVX2y$wzdu{2mcat- zgN}wBbj&Vf!afn~D8Q9>I0m1M04;)qgoM4LwW0)_bA}67#DY~q$=bm7ho`r5|HDNP zkc1ewVY}j^{qPkH4Qkg^x+a(zT1mjVn;`r#5j52zrcmkl~S7JH7l5j2}mRI_z^kgix3@ zuKYCDv8T3{@x{!NU*nM~qBMu-e8OjF1mBLjF4K^Z0gwKeHfq2iS++#hN-_q!W&6%O4hh1WhSj!af3kI)Zief!j1b) z{nUR$U3rG9Kcx{>T)D`#T3xSZ88Mgjp+c}}B`U$33RZfYZhkDea@1#h2P32WEn64S z-2WKbLTK+t2?J@#eG1yNEMHs&aHOEh+G(07h)`z_bvmS$ z=pMgm)5cXOIcbDiz^oIUYHH9bAT}1~=T_IkJ}i8aGPaquv1P3>JT^B>GjN|ZW@c%y z#t#mkY1S;F>V0t;0Box01Ev#a>ok$nXB(~K9c`YE4+rE$*(9#Dio*WkMG zn(9QolqI!^&zGh!R1xIZF|UU7B2zX;9A%F&*vxTvgS#Kifnw|TxTvaR~`Y{5usXO56B$q znpt>*vmxW7LZGa=%>~C)?i>R(SZZo8Cvy9`L(@|SUvE#EqASgaVnLiii%}jBtgp5j z@}1CiK_&gje2s}MONfxVt?RN=G1E4{TMb5QhbF>{)0Wj@&V_A=;{hRK;WOnr57Y@) z!-Q}4A|LtO{VUmpGRRm>aodUeh>^noolByAdp;)J?`72J>gqNo?Qu>lh-c={?&6%h zXalT@SAB#U!oPE4`ESk4WI5KEP3vPDU!n_B`a9E}81m~N4*LIs_++AvFO_jLa^`aj zWfiQxCect55gEtCfDmw8424{Ba>mVPZj-1i3>3TA{?d&uGL&!>P>pJ#B$GK~BvnMt zMaa{s<-YuDuA=7*Vc1*72bwkXjn{tKHCpieLWbQnOZ=FeG1Zr{kkseSAld@Y=GuY; z8zRaisL;|m)c0o-PnC6wsW8xK=oG9b6^e#G8eO%d$}ina&41>tf+2x?62CsE#aF4U z*;?mJRz_2!&m5&V=88a9yFYh=IvP}iv2nlwGRo=O{f$XKQwJ`mXMmg+({(Kz}VGb z_D=qwL5+(+&*E%NYH0lgINZkp!|-2anchjjJ1GlKLPqB5?QN)KiUMT(%>)g9(jbX_ z|Kom)=A3JuY@$F$uHXAuTrdAS2;`#f!JMWt5sJnz68-4XZzn(>i?Xhmz`|zpkxe@3 ztD)ImiHV?aF$?ReYPMQyuXrN8SIb+Dn9>k1_vU@`(WA$-1&w>eM44#n{14_p8NW^# zrVu1-^PHqN0%N$G%X#b_9Mfo|g^`UgL5S0}<5H*z!n!Lz2nm!S^oBrbiaetzrT;?4 zg0I(hO;u`1*K$EkKx!ctEm*5@zVZ53__`0NsvH59ymBZd_|H1o#HX=A>wvtF=-zFX zV~lI%IHG+uHESwKbxPF%23}SP*KA5_$)U4Shx96tpIf0jtoLXg;hiNf3RGbUlxx)i zUKy+hhhn+fqLilCioBx8EvBqB4b+UQuk{a&uC?iglNm$UlWRH$wYM7YfhNBPgoGsu zT0nbkk@QkND$5e(gjI6Rffb2DU;<1aTGI-JRZ={~K^R16lvsWtBIG8Fz?i6}-zz-A z!a85?zl@yt#tnzED+BJp^+U{2|zorJUHM^sOw!|5P zMx+zY5d?J!mt6<5usM*siura;<|+n^Gh8L?rliHxe!J{6IYAUsZVT3%C0dzQ_Mn`{Yq20wbyq0P zUDNk^B?HClS~`KJDoR{l@}2kI<4^whkNN215Bc@a{|ldf^dYald`7Q#z&g)yuHwPN zdl(d_2Zy9VfOnSJmDpr0mrG`|1%XL8I6B5V!*sgf$&)7-W0=gQ%w`Mz{*zzP?R6+h zIVaDazh*Lw(kh8Nh!l*;C*`lku zI}?v{1xh&r9m)<@DImCZZzy(*r9iH_dcD^$)l?)@>lfRwx~gs>s@*nRylC zse0W;A`2R({eSy>UE+tJ-t5WeiY&J|{@royN`Ux2R{UTTXLS3~i} zM(k~Ye&Z_ARDZ%c|2z!ELPmhIB=XtD2qJiMgAc8BQLJBuj!v5G(_>{2>hctJp9E z1GWr9FKhvVAWMSfFf*bw9+ETUu*uHd>{wm##*=nh!7rY*_c=w)1um-U*10F_u-5bZ zpMP+key!?@9%-bd2{hWG?baw?Ye{R`B=JU(@)Qpro$)7s@~50We8l4Hgq8CA<&S^J z-~EH%VLlm=8H=N%sT*0px|WOOiq*2gM~PYPZ3uhrsZx=yttfNLXq+<{m1)gSqNo^Y z>qa*2$%OqYM_dL+phy3o=2h8S~PXr)0@D!CGfU`d+Sm`ZuM2E7Up15M}1 zGU)mVfHmNh81u(~ie~7qqDt5OFcs(=yGlYi~BaAbw-lA1{YJ^D3Zv#AGPS*?`NU;=bqlPsA$=jRv9W>Znxhrs3KC0a`g zq_vjGWX58#z$O(th=1s0GU2JGo~Cgf_wU~yQ2pNfl_>GgT1!-JQ+Fx(P-tZHiMS$6 zCNq@E&?+YeLktF@X0=TAQ+16sQgmdQ#^{{j9n-$yjfsc?n*jY(`Ox$;yJZ1&<~=p(l-}$*a{XCbKy@!o}rU&R?~b$&bp=CW)u| zNOI?~He*yuGt(p6qjG|_h7e$yi-nmBaa(^SWZx*AVqts0LZPq-!!IVmqCqK5Q2nGh zam@p`;Bj7DU9^Fi>ZrQ)0yrv*LNh9-bX|jUX`&XiqyZ*cKo4pPGW;uC*i`t!mbU7V z*A6bZz@!FGqS}3;3vGi(`li&mkc4ldXG-*?3~TI=ncF|mD2*~E6@4nj>>=Y*v?Q}A zlaS7B6re^@YD`up1Z#(+KXAo_Dc6q|UGNk^QQC|q8r-JA|2d4L2 zteivXQ5tgzUqssiZDnKYXc7`h;&Q3psTHfmh#15Tp&$EXHpfiTcyYbMM~tG9)n;dW z90NgVK{V1a>K9*|XA%{c_`7O{-!&W?AJDnRg$@@wCbKbU#b@{Ka`5Fed%s7$Sg^qyK39%!@bSl=^3$LEoa1ZP$nr5EI6nOF zBa{*^yUBD;(?$HM!k}mx&!fi=&{}in&NJ*E9`fbAFZlH1&p19lMhDME`qI`M}I~IBLXmA{81ikO_AdE~M zvRo5#z6F#|1$LT8X!_6CkJ@D+Rsg|4$hdNJg%_XyDxd%I9MqnOr9HA)vEp=oKH$1j%KiA9=z$Y@QzeAkifOiR3%rXfod zgRXJ3zQubfn!Jw$`WRAAIUWh>?E4tzWLlH_5Pfuv&&#kjwzDKgECYx}(R>Q{O7wU@bie3jret3=cI^5H|?{qz$q zn+9LX=JDwC1mAYV=ut+YtipGGh+0;Wb(%aV6VL&=4yv}Ib}emaA$p?g zP)__MJ;E!GCJqfuzrGR}IK3fw0WQ*O`lP1z(V%^5;1dl1#DcByj?PH~=zOGZ>Osij zgSf)it_LZV;Itg6%PB=Y1r7TLEkE~W3_V@P*ony5Uj_7zarKwtCP*{_r zm8NPs7OM?iD~VOBbp`7Rn-!Q$v#x4JC|c)fFGXN$avSGl_9GhD}uquQ^D9pieyizPQy|3<7lm+f)?3uZw_B zmVZd0*g5%ZGUX8ckM|MUr1eRYh^WLS%UX>wVk_7-Eg?AayiCCs6Y8eHJJ}0@hY+3Y zdpZvFK^*qAw7>j{#;X5{QI4aE+d{V&i>X9Ih%wT7sY}Z&Cxkd~&217;7Da)z){6Cm z&Iv(aVQU>)Td@yH)x0FMNK%WSvx2mUWtPE*-a1MOQ>{(9@wOizkj7Bj?i8_+=oF(- zhBAear90{1T`*{ylP9xfO(4(pP|aJDr^TYfJBTq8{l0tK?YJ>pZ?)Z%)!TRUe7s#6!IPq^?Xl@PPgYorRpe35 z#lwg9`S|0HsOlwOxpjxT+OS$&5~D|3jj`gNo9DUs_`wq#T{Vp5gd;PZ&XcQ@) zjLI2BIYk-B>i zKK(hUS8t8AJps9=`=N3j?JcG ze}8{)0oZIdT%2Dro=tf0-~k8wN32#WM&*R%a>;VJOk-ymDd%~K?}Wpe=Q*p@0`DW^ z@f6?6`wkY08w2fFj#h}=@dMJ{brbFp16-KF@g4r6>Vl^bXEiW#yHcx9( zOWm&N>5{!&m2m6DzS0;&wb@{F#%8@{c`h7c6$8!2v0f}GlCZF@YsTa8kWM%njgqK} z;rdxstF)H()R=UwrhBU&>GvqLfU|;(_S2=dZRwH?nfFhMf=wnb)-;Xm9SXG57=-)k z+HR;?v7C>bT#b}H%(q+4JBKtzRS9R-@^rMxcNdG75 zSXrFAM5`EtVmk}5AO|Un*jO%Cmoyv8$Uv?)GGswn*Zdq%ILA)Vl zOp#%;Qknysv0QAZHZ8O9oWjZ&@6nfciK?b7BD1|YV+!hSLvy)gS#5ZDcEWVe(!_|i zBeW_}A)-B)m=U7`;|VI_QjkWFIc=h}{x$xrs;3kL8J`Zt-jic?5|WufqLjjtc-lG+ zh^{nO37Lx9*WP{2ZS|1~kX`^516GUkhg>@{%2hprta?(wRj)m%I}=gqg?1DNH56pp%Zg%6`732 ztd?RR9*Dx#w6eIm;HfJSWlbg%I;DpA$`~W%WK2x!UvN^`n3mf&Qh?7!Il#? zt)tZjZ^!t|)4GOLrzq5f@hGP`y}(g0IXEJlj5u9hvRHPEW(Qn7IHnknu;UUl9T7$b z>M_W`#6gF%qIQ2`G5+rDwQ)^BU#2AU0!M zx4~FD@adFtNw=WTF*QZ~q{5^rS${_)+8D?dpf*ETauR zaiV)MiBjA9MVXiY35f%(H*314CCd%t(MX!P5E&Qcu--Xs2IF~~X~yMvFmdlUX=4ms z)AUFZu5&|i>%DO6ZJrI20NFcIaTWu0(^5Ab&h-hFExkvuu?B527R&Wu1Ev!XxvH8} zTv)O!r}csQ>>Nx#jt<1*`T!`nq^tyoFmIJ%B-ZSD#qh6 zZB>KO)OAbOd7P_7@MG1`WcDWglktqrMl>MH#d^@bdQQGG@nbCOH(R3+5Kndbs zjwfVhHUtW6*Ve$e$i+<#Bd-xW2YY*x&K4uSk?B+Pj(Q`7z0?S9a5J*j5@Mif>Y?#! zng)|uT-b8=30wN6Qi@EAz}-hRSO^a9&sqt(Fh-n2V#FqZvF&6H(%J~Rt02ST~bzNiI6j9c5zIF-p{Rt9|>Q=!kZ5qtcs!_PhC^zx5{&4OyY$~hwr_|CbIu9>T+S|*muQneg~av zy<;|$7&@Y)#c0=ZI4QYtIOoQVtBfQ%<95^C z5+bc3q~<0%DFVu(Ak&I__wMpf{^|e15B~g5Il8{bwWn|MkG}abKl{adJbLhm|NbBU zcYO2JZ}4~i@Nd&pHUIiw{vZ7E)6b}zjy44L501o-);TUNF495?KDvQ7*5ANdTS+Ua zyWzt3X%U=`O2(6dU;p)A=lYEs{OE^&#-IHAkGOetkI`6cc{W{O?KQKb&$xE`1+r|! z@!lcjc$5O53^p2clu@<_T7)MUV@xr7Y2>PViU(<-Pc8y#*E_hs@nJXgKt&~rPo*)* z&sYP>BvE1z^r}Z3bu!78&RE`q?fIw)UDfHa_-%8lwM`z&Jsn053lXU(>3O8K7x95$ zzS`iEG4~UkaTSKAAU#@ZhHFtGkKG-$zU>>q{z>P%qVPTdGEb< z_~4_jQ9IB6@gZOP);B3fIpbWjzn?SC6wkcy3?|FCTyIduvZ^XJn+;i!b9CjH4?cUy z+4+iSHP{SCq>GWu)tbzXd35=J&E8Z{ukibS`1?Hj;#Y|# z!=q`ERa%*iX?;x}jG(ljG9jd38JiS)WirRM4T5(W)RJ*h~{+ zKT&+LvFk^ad6p3Y*L8F*1<|BEPb2gINh}LQ=f%n~ds6?=^DU*|^aDq>4~*zH;Qn=o z1f;H$CTc*yz>Y*6FU|)M=TbV77cOOHZ4#?xKsN*d^;C)7!lF0P@9#a8=zz=RnsPKE zrtk$2gl;;WQP@l(ptX$kGh>GYC~Nbfn9H)97+V?lCm+(@R??-<({qQFQdCtnXjXbJ z)Luv|+7j8H>bg!Suu$kmqmdMKskoLxC_`skll{7`2ZXWfB#BGVaPeN>4N3|jFq@2I zTA|c{l+MQ!q7pklV+>VQ(Z-fC&j6uB`s8*b&4oedy`O#H+La?Y-afEDpCz4)!`M9W zsv~(WYJUh6NjwP*MPSrI;~bK@`oJiy$d5xvb?n_@rNq%T*e=(x<=b=GT3%hm5RY+#g!Xba01&5 zZz*U+9UWols&-Gp?kG$aVg1D4opa1)6GE%VVx(PMGEs`^>@oN11uy@>A8_;9Ax1g0 z5r|#sC|30gCm z&4eRcjv0-cbY5-R0E9+jjY4wa(`PZ@gG5LgB!aHD9`eDFPYTLO$?4{T%liuspT5r5 zfBid{*^EE^<9|oxYMy@K8D4(v75?=9{S*GjfBZjj_wGF+8dHd8Cu!kwO*`<*Fq$rL z<96K@gdda*T(u(0K?O3cn9axByLXTO{J;DM#1cMvbeH?5cNrH2cb>Y1i<&RbFR1Q6 zCVucCv%MpJ^LyXtspp<%f~C4xh_#a!iyxyc)@I_p7hAMaWSLD7gKgR_Q;`^TLhubr zYuwJL9vJ90&{GV;FxE49MhuP!Lu$$>p%kF8N)cibQKkt3-VNhCA?8bTrq-Gw X( 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

89CItzl6eMLx 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 Options: - -b boot-dir Path to bootloader build directory (default: O= or output/) + -b boot-dir Path to bootloader build directory (default: same as -r, or O= or output/) -B Boot-only image (no rootfs, for bootloader testing) -d Download bootloader files from latest-boot release -f Force re-download of bootloader even if cached @@ -48,6 +48,9 @@ Examples: # Standalone with separate boot/rootfs builds: $0 -b x-boot -r output raspberrypi-rpi64 + # Rootfs-only (no separate bootloader build, e.g. EspressoBIN): + $0 -r x-aarch64 marvell-espressobin + # With downloaded rootfs and bootloader: $0 -d -r ~/Downloads/rootfs.squashfs friendlyarm-nanopi-r2s @@ -457,13 +460,20 @@ if [ -n "$STANDALONE" ]; then BOOT_DIR=$(find_build_dir) || die "Could not find boot directory. Use -b option" fi else - if [ -z "$BOOT_DIR" ]; then - BOOT_DIR=$(find_build_dir) || die "Could not find boot directory. Use -b option" - fi - if [ -z "$ROOT_DIR" ]; then ROOT_DIR=$(find_build_dir) || die "Could not find rootfs directory. Set O= or use -r option" fi + + if [ -z "$BOOT_DIR" ]; then + # For boards without a separate bootloader build (e.g. EspressoBIN, + # where U-Boot lives in SPI NOR), -r alone is sufficient: default + # the boot directory to the rootfs directory. + if [ -n "$ROOT_DIR" ]; then + BOOT_DIR="$ROOT_DIR" + else + BOOT_DIR=$(find_build_dir) || die "Could not find boot directory. Use -b option" + fi + fi fi # Set up environment variables, some required by genimage.sh @@ -501,8 +511,8 @@ if [ -n "$STANDALONE" ]; then # Build directory with images/ - copy rootfs and partition images log "Copying artifacts from $ROOT_DIR/images/ to $BINARIES_DIR/" cp "$ROOT_DIR/images/rootfs.squashfs" "$BINARIES_DIR/" - # Copy partition images if they exist - for img in aux.ext4 cfg.ext4 var.ext4; do + # Copy partition images and rootfs variants if they exist + for img in aux.ext4 cfg.ext4 var.ext4 rootfs.ext2; do if [ -f "$ROOT_DIR/images/$img" ]; then cp "$ROOT_DIR/images/$img" "$BINARIES_DIR/" fi @@ -514,8 +524,8 @@ if [ -n "$STANDALONE" ]; then # Directory directly containing rootfs.squashfs log "Copying rootfs from $ROOT_DIR/rootfs.squashfs" cp "$ROOT_DIR/rootfs.squashfs" "$BINARIES_DIR/" - # Copy partition images if they exist - for img in aux.ext4 cfg.ext4 var.ext4; do + # Copy partition images and rootfs variants if they exist + for img in aux.ext4 cfg.ext4 var.ext4 rootfs.ext2; do if [ -f "$ROOT_DIR/$img" ]; then cp "$ROOT_DIR/$img" "$BINARIES_DIR/" fi @@ -557,16 +567,16 @@ if [ -n "$DOWNLOAD_BOOT" ]; then ln -sf "$(realpath "$ROOT_DIR")" "$BINARIES_DIR/rootfs.squashfs" elif [ -f "$ROOT_DIR/images/rootfs.squashfs" ]; then ln -sf "$(realpath "$ROOT_DIR/images/rootfs.squashfs")" "$BINARIES_DIR/rootfs.squashfs" - # Link partition images if they exist - for img in aux.ext4 cfg.ext4 var.ext4; do + # Link partition images and rootfs variants if they exist + for img in aux.ext4 cfg.ext4 var.ext4 rootfs.ext2; do if [ -f "$ROOT_DIR/images/$img" ]; then ln -sf "$(realpath "$ROOT_DIR/images/$img")" "$BINARIES_DIR/$img" fi done elif [ -f "$ROOT_DIR/rootfs.squashfs" ]; then ln -sf "$(realpath "$ROOT_DIR/rootfs.squashfs")" "$BINARIES_DIR/rootfs.squashfs" - # Link partition images if they exist - for img in aux.ext4 cfg.ext4 var.ext4; do + # Link partition images and rootfs variants if they exist + for img in aux.ext4 cfg.ext4 var.ext4 rootfs.ext2; do if [ -f "$ROOT_DIR/$img" ]; then ln -sf "$(realpath "$ROOT_DIR/$img")" "$BINARIES_DIR/$img" fi From 5979f4e3c9ad1fe044eb721b601faf3393f8e7af Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 17 Apr 2026 17:57:51 +0200 Subject: [PATCH 2/9] board/aarch64: fix bpi-r4 dip switch values Inverted values for eMMC and SPI NAND. Also use ABCD instead of Position as column name to match board markings. Signed-off-by: Joachim Wiberg --- board/aarch64/bananapi-bpi-r3/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/board/aarch64/bananapi-bpi-r3/README.md b/board/aarch64/bananapi-bpi-r3/README.md index be92ea284..fd39dcf37 100644 --- a/board/aarch64/bananapi-bpi-r3/README.md +++ b/board/aarch64/bananapi-bpi-r3/README.md @@ -70,11 +70,11 @@ The BPI-R3 has a 4-position DIP switch that controls boot media: DIP switches -| Position | Mode | Description | -|----------|-------------|---------------------------------------| -| 0000 | SD card | Boot from microSD card | -| 0110 | eMMC | Boot from internal eMMC (recommended) | -| 1010 | SPI NAND | Boot from SPI NAND (advanced users) | +| ABCD | Mode | Description | +|------|----------|---------------------------------------| +| 0000 | SD card | Boot from microSD card | +| 0101 | SPI NAND | Boot from SPI NAND (advanced users) | +| 1001 | eMMC | Boot from internal eMMC (recommended) | > [!NOTE] > Switch position is read from left to right: "0" = OFF, "1" = ON. @@ -150,6 +150,7 @@ From the U-Boot prompt: usb start fatload usb 0:1 0x50000000 infix-bpi-r3-emmc.img setexpr blocks ${filesize} / 0x200 +mmc dev 0 mmc write 0x50000000 0x0 ${blocks} ``` From b93795c0aa3937095c2e58da8191b7e78d6918c0 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 10 Apr 2026 12:26:30 +0200 Subject: [PATCH 3/9] board/common: add rauc upgrade bundle for ext4 images NOTE: this is intended only for use on devboards like EspressoBin where the onboard bootloader only understands fat and ext2 file systems. To enable, use 'make apply-ext4' Signed-off-by: Joachim Wiberg --- board/common/Config.in | 1 + board/common/image/image-ext4-rauc/Config.in | 27 +++++++++++++++++++ .../common/image/image-ext4-rauc/generate.sh | 27 +++++++++++++++++++ .../image/image-ext4-rauc/image-ext4-rauc.mk | 10 +++++++ configs/snippets/ext4.conf | 1 + 5 files changed, 66 insertions(+) create mode 100644 board/common/image/image-ext4-rauc/Config.in create mode 100755 board/common/image/image-ext4-rauc/generate.sh create mode 100644 board/common/image/image-ext4-rauc/image-ext4-rauc.mk diff --git a/board/common/Config.in b/board/common/Config.in index ed192024e..468a208a3 100644 --- a/board/common/Config.in +++ b/board/common/Config.in @@ -5,6 +5,7 @@ source "$BR2_EXTERNAL_INFIX_PATH/board/common/image/image-itb-aux/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/board/common/image/image-itb-qcow/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/board/common/image/image-itb-gns3a/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/board/common/image/image-itb-rauc/Config.in" +source "$BR2_EXTERNAL_INFIX_PATH/board/common/image/image-ext4-rauc/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/board/common/image/image-itb-dl-release/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/board/common/image/image-readme/Config.in" diff --git a/board/common/image/image-ext4-rauc/Config.in b/board/common/image/image-ext4-rauc/Config.in new file mode 100644 index 000000000..53ad796c4 --- /dev/null +++ b/board/common/image/image-ext4-rauc/Config.in @@ -0,0 +1,27 @@ +config IMAGE_EXT4_RAUC + bool "RAUC upgrade bundle (ext4)" + depends on BR2_TARGET_ROOTFS_EXT2 + select BR2_PACKAGE_HOST_RAUC + help + Create a RAUC upgrade bundle for targets using an ext4 rootfs + image. Intended for development boards whose bootloader does + not support squashfs. + +config IMAGE_EXT4_RAUC_KEY + string "signing key" + depends on IMAGE_EXT4_RAUC + default "${BR2_EXTERNAL_INFIX_PATH}/board/common/signing-keys/development/infix.key" + help + Path to the private key, in PKCS#8 format, used to sign + the RAUC bundle; or a PKCS#11 URI. + +config IMAGE_EXT4_RAUC_CERT + string "signing certificate" + depends on IMAGE_EXT4_RAUC + default "${BR2_EXTERNAL_INFIX_PATH}/board/common/signing-keys/development/infix.crt" + help + Path to the X509 certificate which will be associated with + the bundle signature. + + NOTE: This cert MUST be included in the trust store of the + system on which this bundle is to be installed. diff --git a/board/common/image/image-ext4-rauc/generate.sh b/board/common/image/image-ext4-rauc/generate.sh new file mode 100755 index 000000000..03cd767c7 --- /dev/null +++ b/board/common/image/image-ext4-rauc/generate.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +set -e + +ext2="${BINARIES_DIR}/rootfs.ext2" +pkg="${BINARIES_DIR}/${ARTIFACT}-ext4.pkg" + +# RAUC internally uses the file extension to find a suitable install +# handler, hence the name must be .img +cp -f "${ext2}" "${WORKDIR}/rootfs.img" + +cat >"${WORKDIR}/manifest.raucm" < Date: Fri, 10 Apr 2026 12:30:26 +0200 Subject: [PATCH 4/9] patches/ethtool: backport support for --json -T (show time stamping) This is used by the next commit that adds initial support for PTP/gPTP. Signed-off-by: Joachim Wiberg --- ...son-support-for-T-show-time-stamping.patch | 309 ++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 patches/ethtool/0001-ethtool-Add-json-support-for-T-show-time-stamping.patch diff --git a/patches/ethtool/0001-ethtool-Add-json-support-for-T-show-time-stamping.patch b/patches/ethtool/0001-ethtool-Add-json-support-for-T-show-time-stamping.patch new file mode 100644 index 000000000..8ea2455ba --- /dev/null +++ b/patches/ethtool/0001-ethtool-Add-json-support-for-T-show-time-stamping.patch @@ -0,0 +1,309 @@ +From ccd5fe71878cc3eeea2a3137c7b8d5715e3f4253 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Thu, 9 Apr 2026 11:09:51 +0200 +Subject: [PATCH] ethtool: Add --json support for -T (show-time-stamping) +Organization: Wires + +Wire up JSON output for `ethtool -T`: + + - Mark the -T command with .json = true in the command table + - Wrap tsinfo_reply_cb output in open/close_json_object, use + print_string for the header, and handle the PHC index/none + case with proper JSON primitives + - Convert tsinfo_dump_list to open a JSON array (keyed by the + new json_key parameter) and close it on return; update callers + in tsinfo.c and tsconfig.c with appropriate keys + - Convert tsinfo_dump_cb to use print_string(PRINT_ANY) so + each set bit is emitted as a JSON array element in JSON mode + and as an indented text line in plain mode + - Wrap the netlink request in nl_tsinfo with new_json_obj / + delete_json_obj + +Example output on a Marvell 88e6341 (Topaz) switch (lan0): + + $ ethtool --json -T lan0 | jq + [ + { + "ifname": "lan0", + "capabilities": [ + "hardware-transmit", + "hardware-receive", + "software-receive", + "software-system-clock", + "hardware-raw-clock" + ], + "hwtstamp-provider-index": 0, + "hwtstamp-provider-qualifier": "Precise (IEEE 1588 quality)", + "tx-types": [ + "off", + "on" + ], + "rx-filters": [ + "none", + "ptpv2-l4-event", + "ptpv2-l4-sync", + "ptpv2-l4-delay-req", + "ptpv2-l2-event", + "ptpv2-l2-sync", + "ptpv2-l2-delay-req", + "ptpv2-event", + "ptpv2-sync", + "ptpv2-delay-req" + ] + } + ] + +Example output on a Qemu VM with e1000 NIC (e1): + + $ ethtool --json -T e1 | jq + [ + { + "ifname": "e1", + "capabilities": [ + "software-transmit", + "software-receive", + "software-system-clock" + ], + "phc-index": -1, + "tx-types": [], + "rx-filters": [] + } + ] + +Signed-off-by: Joachim Wiberg +--- + ethtool.c | 1 + + netlink/ts.h | 2 +- + netlink/tsconfig.c | 6 +-- + netlink/tsinfo.c | 106 ++++++++++++++++++++++++++++++--------------- + 4 files changed, 75 insertions(+), 40 deletions(-) + +diff --git a/ethtool.c b/ethtool.c +index 9c8a542..f845dae 100644 +--- a/ethtool.c ++++ b/ethtool.c +@@ -5985,6 +5985,7 @@ static const struct option args[] = { + }, + { + .opts = "-T|--show-time-stamping", ++ .json = true, + .func = do_tsinfo, + .nlfunc = nl_tsinfo, + .help = "Show time stamping capabilities", +diff --git a/netlink/ts.h b/netlink/ts.h +index 9442b44..07f140a 100644 +--- a/netlink/ts.h ++++ b/netlink/ts.h +@@ -17,6 +17,6 @@ int tsinfo_qualifier_parser(struct nl_context *nlctx, + void *dest); + int tsinfo_dump_list(struct nl_context *nlctx, const struct nlattr *attr, + const char *label, const char *if_empty, +- unsigned int stringset_id); ++ unsigned int stringset_id, const char *json_key); + + #endif /* ETHTOOL_NETLINK_TS_H__ */ +diff --git a/netlink/tsconfig.c b/netlink/tsconfig.c +index d427c7b..aab9859 100644 +--- a/netlink/tsconfig.c ++++ b/netlink/tsconfig.c +@@ -52,19 +52,19 @@ int tsconfig_reply_cb(const struct nlmsghdr *nlhdr, void *data) + + ret = tsinfo_dump_list(nlctx, tb[ETHTOOL_A_TSCONFIG_TX_TYPES], + "Hardware Transmit Timestamp Mode", " none", +- ETH_SS_TS_TX_TYPES); ++ ETH_SS_TS_TX_TYPES, "tx-types"); + if (ret < 0) + return err_ret; + + ret = tsinfo_dump_list(nlctx, tb[ETHTOOL_A_TSCONFIG_RX_FILTERS], + "Hardware Receive Filter Mode", " none", +- ETH_SS_TS_RX_FILTERS); ++ ETH_SS_TS_RX_FILTERS, "rx-filters"); + if (ret < 0) + return err_ret; + + ret = tsinfo_dump_list(nlctx, tb[ETHTOOL_A_TSCONFIG_HWTSTAMP_FLAGS], + "Hardware Flags", " none", +- ETH_SS_TS_FLAGS); ++ ETH_SS_TS_FLAGS, "hwtstamp-flags"); + if (ret < 0) + return err_ret; + +diff --git a/netlink/tsinfo.c b/netlink/tsinfo.c +index 187c3ad..da64b50 100644 +--- a/netlink/tsinfo.c ++++ b/netlink/tsinfo.c +@@ -52,6 +52,7 @@ int tsinfo_show_hwprov(const struct nlattr *nest) + return 0; + } + ++ + static int tsinfo_show_stats(const struct nlattr *nest) + { + const struct nlattr *tb[ETHTOOL_A_TS_STAT_MAX + 1] = {}; +@@ -109,39 +110,57 @@ err_close_stats: + static void tsinfo_dump_cb(unsigned int idx, const char *name, bool val, + void *data __maybe_unused) + { ++ char buf[16]; ++ + if (!val) + return; + +- if (name) +- printf("\t%s\n", name); +- else +- printf("\tbit%u\n", idx); ++ if (!name) { ++ snprintf(buf, sizeof(buf), "bit%u", idx); ++ name = buf; ++ } ++ print_string(PRINT_ANY, NULL, "\t%s\n", name); + } + + int tsinfo_dump_list(struct nl_context *nlctx, const struct nlattr *attr, + const char *label, const char *if_empty, +- unsigned int stringset_id) ++ unsigned int stringset_id, const char *json_key) + { + const struct stringset *strings = NULL; +- int ret; ++ bool empty; ++ int ret = 0; + +- printf("%s:", label); +- ret = 0; +- if (!attr || bitset_is_empty(attr, false, &ret)) { +- printf("%s\n", if_empty); +- return ret; +- } +- putchar('\n'); +- if (ret < 0) +- return ret; ++ empty = !attr || bitset_is_empty(attr, false, &ret); + +- if (bitset_is_compact(attr)) { +- ret = netlink_init_ethnl2_socket(nlctx); ++ if (!is_json_context()) { ++ printf("%s:", label); ++ if (empty) { ++ printf("%s\n", if_empty); ++ return ret; ++ } ++ putchar('\n'); ++ if (ret < 0) ++ return ret; ++ } else { + if (ret < 0) + return ret; +- strings = global_stringset(stringset_id, nlctx->ethnl2_socket); ++ open_json_array(json_key, ""); ++ } ++ ++ if (!empty) { ++ if (bitset_is_compact(attr)) { ++ ret = netlink_init_ethnl2_socket(nlctx); ++ if (ret < 0) ++ goto out_close; ++ strings = global_stringset(stringset_id, nlctx->ethnl2_socket); ++ } ++ ret = walk_bitset(attr, strings, tsinfo_dump_cb, NULL); + } +- return walk_bitset(attr, strings, tsinfo_dump_cb, NULL); ++ ++out_close: ++ if (is_json_context()) ++ close_json_array(""); ++ return ret; + } + + int tsinfo_reply_cb(const struct nlmsghdr *nlhdr, void *data) +@@ -163,47 +182,59 @@ int tsinfo_reply_cb(const struct nlmsghdr *nlhdr, void *data) + return err_ret; + + if (silent) +- putchar('\n'); +- printf("Time stamping parameters for %s:\n", nlctx->devname); ++ print_nl(); ++ ++ open_json_object(NULL); ++ print_string(PRINT_ANY, "ifname", ++ "Time stamping parameters for %s:\n", nlctx->devname); + + ret = tsinfo_dump_list(nlctx, tb[ETHTOOL_A_TSINFO_TIMESTAMPING], +- "Capabilities", "", ETH_SS_SOF_TIMESTAMPING); ++ "Capabilities", "", ETH_SS_SOF_TIMESTAMPING, ++ "capabilities"); + if (ret < 0) +- return err_ret; ++ goto err_close_dev; + + if (tb[ETHTOOL_A_TSINFO_HWTSTAMP_PROVIDER]) { + ret = tsinfo_show_hwprov(tb[ETHTOOL_A_TSINFO_HWTSTAMP_PROVIDER]); + if (ret < 0) +- return err_ret; +- ++ goto err_close_dev; + } else if (tb[ETHTOOL_A_TSINFO_PHC_INDEX]) { +- printf("PTP Hardware Clock: "); +- printf("%d\n", +- mnl_attr_get_u32(tb[ETHTOOL_A_TSINFO_PHC_INDEX])); ++ print_uint(PRINT_ANY, "phc-index", ++ "PTP Hardware Clock: %d\n", ++ mnl_attr_get_u32(tb[ETHTOOL_A_TSINFO_PHC_INDEX])); + } else { +- printf("PTP Hardware Clock: "); +- printf("none\n"); ++ if (is_json_context()) ++ print_int(PRINT_JSON, "phc-index", NULL, -1); ++ else ++ printf("PTP Hardware Clock: none\n"); + } + + ret = tsinfo_dump_list(nlctx, tb[ETHTOOL_A_TSINFO_TX_TYPES], + "Hardware Transmit Timestamp Modes", " none", +- ETH_SS_TS_TX_TYPES); ++ ETH_SS_TS_TX_TYPES, "tx-types"); + if (ret < 0) +- return err_ret; ++ goto err_close_dev; + + ret = tsinfo_dump_list(nlctx, tb[ETHTOOL_A_TSINFO_RX_FILTERS], + "Hardware Receive Filter Modes", " none", +- ETH_SS_TS_RX_FILTERS); ++ ETH_SS_TS_RX_FILTERS, "rx-filters"); + if (ret < 0) +- return err_ret; ++ goto err_close_dev; + + if (tb[ETHTOOL_A_TSINFO_STATS]) { + ret = tsinfo_show_stats(tb[ETHTOOL_A_TSINFO_STATS]); + if (ret < 0) +- return err_ret; ++ goto err_close_dev; + } + ++ if (!silent) ++ print_nl(); ++ close_json_object(); + return MNL_CB_OK; ++ ++err_close_dev: ++ close_json_object(); ++ return err_ret; + } + + int tsinfo_qualifier_parser(struct nl_context *nlctx, +@@ -271,5 +302,8 @@ int nl_tsinfo(struct cmd_context *ctx) + if (ret < 0) + return ret; + +- return nlsock_send_get_request(nlsk, tsinfo_reply_cb); ++ new_json_obj(ctx->json); ++ ret = nlsock_send_get_request(nlsk, tsinfo_reply_cb); ++ delete_json_obj(); ++ return ret; + } +-- +2.43.0 + From 45b06dd6706170759dccc1e98ae173b966146383 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 10 Apr 2026 12:25:02 +0200 Subject: [PATCH 5/9] Initial support for IEEE 1588/802.1AS PTP/gPTP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remaining work: - phc2sys YANG model (infix-phc2sys.yang, instance-index + servo params) - ts2phc YANG model (GPS/PPS → PHC → ptp4l GM path) - timemaster coordination (Phase 3, after phc2sys YANG is stable) - show ptp network (YANG action or background-polled topology container) - CMLDS (requires upstream linuxptp + 802.1ASdm foundation) - Full 12-bit sdoId, fault log, performance monitoring Backported patches from linuxptp master: - port: fix unicast negotiation recovery after FAULT_DETECTED - udp: fix port-specific ptp/p2p_dst_ipv4 configuration - pmc: avoid race conditions in agent update - phc2sys: wait until pmc agent is subscribed (startup race) - fix MAC driver incorrect SIOCGHWTSTAMP adjustment flags - pmc_agent: longer update interval when not subscribed - phc2sys: don't disable pmc agent with -s/-d/-w options - port_signaling: respect ptp_minor_version in message header - port: refresh link status on faults - uds: copy server socket ownership in pmc clients (non-root pmc) - uds: don't call chmod() on client socket - port: allow mixing wildcard and exact clock identities - Add pidfile support to ptp4l, phc2sys, and timemaster Signed-off-by: Joachim Wiberg --- .../rootfs/etc/finit.d/available/ptp4l@.conf | 3 + .../rootfs/usr/libexec/infix/init.d/00-probe | 54 + configs/aarch64_defconfig | 1 + configs/aarch64_minimal_defconfig | 1 + configs/arm_defconfig | 1 + configs/arm_minimal_defconfig | 1 + configs/riscv64_defconfig | 1 + configs/x86_64_defconfig | 1 + configs/x86_64_minimal_defconfig | 1 + doc/ptp.md | 461 ++ mkdocs.yml | 1 + ...-negotiation-doesn-t-recover-after-F.patch | 114 + ...cific-ptp-p2p_dst_ipv4-configuration.patch | 92 + ...void-race-conditions-in-agent-update.patch | 82 + ...s-Wait-until-pmc-agent-is-subscribed.patch | 45 + ...MAC-driver-is-configured-with-incorr.patch | 51 + ...nger-update-interval-when-not-subscr.patch | 50 + ...disable-pmc-agent-with-s-d-w-options.patch | 40 + ...-ensure-that-signaling-messages-resp.patch | 29 + ...9-port-Refresh-link-status-on-faults.patch | 82 + ...ship-of-server-socket-in-pmc-clients.patch | 58 + ...ds-Don-t-call-chmod-on-client-socket.patch | 56 + ...g-wildcard-identities-and-exact-iden.patch | 82 + ...port-to-ptp4l-phc2sys-and-timemaster.patch | 386 ++ src/bin/show/__init__.py | 18 + src/confd/src/Makefile.am | 1 + src/confd/src/core.c | 9 + src/confd/src/core.h | 3 + src/confd/src/ptp.c | 465 ++ src/confd/yang/confd.inc | 5 +- .../confd/ieee1588-ptp-tt@2023-08-14.yang | 4326 +++++++++++++++++ .../confd/ieee802-dot1as-gptp@2025-12-10.yang | 1301 +++++ src/confd/yang/confd/infix-if-ptp.yang | 102 + .../yang/confd/infix-if-ptp@2026-04-09.yang | 1 + src/confd/yang/confd/infix-interfaces.yang | 6 + ....yang => infix-interfaces@2026-04-09.yang} | 0 src/confd/yang/confd/infix-ptp.yang | 291 ++ .../yang/confd/infix-ptp@2026-04-07.yang | 1 + src/klish-plugin-infix/xml/infix.xml | 7 + src/statd/python/cli_pretty/cli_pretty.py | 202 + src/statd/python/yanger/__main__.py | 3 + src/statd/python/yanger/ieee1588_ptp.py | 624 +++ .../python/yanger/ietf_interfaces/link.py | 36 +- src/statd/statd.c | 5 +- 44 files changed, 9095 insertions(+), 4 deletions(-) create mode 100644 board/common/rootfs/etc/finit.d/available/ptp4l@.conf create mode 100644 doc/ptp.md create mode 100644 patches/linuxptp/0001-port-Fix-unicast-negotiation-doesn-t-recover-after-F.patch create mode 100644 patches/linuxptp/0002-udp-Fix-port-specific-ptp-p2p_dst_ipv4-configuration.patch create mode 100644 patches/linuxptp/0003-pmc-Avoid-race-conditions-in-agent-update.patch create mode 100644 patches/linuxptp/0004-phc2sys-Wait-until-pmc-agent-is-subscribed.patch create mode 100644 patches/linuxptp/0005-Fix-issue-where-MAC-driver-is-configured-with-incorr.patch create mode 100644 patches/linuxptp/0006-pmc_agent-Use-longer-update-interval-when-not-subscr.patch create mode 100644 patches/linuxptp/0007-phc2sys-Don-t-disable-pmc-agent-with-s-d-w-options.patch create mode 100644 patches/linuxptp/0008-port_signaling.c-ensure-that-signaling-messages-resp.patch create mode 100644 patches/linuxptp/0009-port-Refresh-link-status-on-faults.patch create mode 100644 patches/linuxptp/0010-uds-Copy-ownership-of-server-socket-in-pmc-clients.patch create mode 100644 patches/linuxptp/0011-uds-Don-t-call-chmod-on-client-socket.patch create mode 100644 patches/linuxptp/0012-port-Allow-mixing-wildcard-identities-and-exact-iden.patch create mode 100644 patches/linuxptp/0013-Add-pidfile-support-to-ptp4l-phc2sys-and-timemaster.patch create mode 100644 src/confd/src/ptp.c create mode 100644 src/confd/yang/confd/ieee1588-ptp-tt@2023-08-14.yang create mode 100644 src/confd/yang/confd/ieee802-dot1as-gptp@2025-12-10.yang create mode 100644 src/confd/yang/confd/infix-if-ptp.yang create mode 120000 src/confd/yang/confd/infix-if-ptp@2026-04-09.yang rename src/confd/yang/confd/{infix-interfaces@2025-11-06.yang => infix-interfaces@2026-04-09.yang} (100%) create mode 100644 src/confd/yang/confd/infix-ptp.yang create mode 120000 src/confd/yang/confd/infix-ptp@2026-04-07.yang create mode 100644 src/statd/python/yanger/ieee1588_ptp.py diff --git a/board/common/rootfs/etc/finit.d/available/ptp4l@.conf b/board/common/rootfs/etc/finit.d/available/ptp4l@.conf new file mode 100644 index 000000000..caf4d7c2c --- /dev/null +++ b/board/common/rootfs/etc/finit.d/available/ptp4l@.conf @@ -0,0 +1,3 @@ +service name:ptp4l :%i log:prio:daemon,tag:ptp4l-%i \ + [2345] ptp4l -f /etc/linuxptp/ptp4l-%i.conf \ + -- PTP instance %i diff --git a/board/common/rootfs/usr/libexec/infix/init.d/00-probe b/board/common/rootfs/usr/libexec/infix/init.d/00-probe index aca2a85d9..e2054cefb 100755 --- a/board/common/rootfs/usr/libexec/infix/init.d/00-probe +++ b/board/common/rootfs/usr/libexec/infix/init.d/00-probe @@ -567,6 +567,59 @@ def probe_wifi_radios(out): out["wifi-radios"].append(info) +def probe_ptp_capabilities(out): + """Probe PTP timestamping capabilities per physical interface via ethtool --json -T. + + Only physical interfaces (those with a 'device' sysfs symlink) are probed; + virtual interfaces such as bridges, VLANs, and tun/tap devices are skipped. + Results are stored under out["interfaces"][]["ptp-capabilities"]. + """ + net_base = "/sys/class/net" + if not os.path.exists(net_base): + return + + ifaces = {} + for ifname in sorted(os.listdir(net_base)): + if ifname == "lo": + continue + # Physical interfaces have a 'device' symlink; virtual ones do not. + if not os.path.exists(os.path.join(net_base, ifname, "device")): + continue + + try: + result = subprocess.run( + ["ethtool", "--json", "-T", ifname], + capture_output=True, text=True, timeout=5 + ) + if result.returncode != 0: + continue + data = json.loads(result.stdout)[0] + except Exception: + continue + + caps = { + "capabilities": data.get("capabilities", []), + "tx-types": data.get("tx-types", []), + "rx-filters": data.get("rx-filters", []), + } + + # phc-index is -1 when no PHC is present; omit in that case. + phc = data.get("phc-index", -1) + if phc >= 0: + caps["phc-index"] = phc + + # hwtstamp provider fields are present only on newer kernels/hardware. + if (idx := data.get("hwtstamp-provider-index")) is not None: + caps["hwtstamp-provider-index"] = idx + if (qual := data.get("hwtstamp-provider-qualifier")) is not None: + caps["hwtstamp-provider-qualifier"] = qual + + ifaces[ifname] = {"ptp-capabilities": caps} + + if ifaces: + out.setdefault("interfaces", {}).update(ifaces) + + def main(): out = { "vendor": None, @@ -593,6 +646,7 @@ def main(): return err probe_wifi_radios(out) + probe_ptp_capabilities(out) if not out["factory-password-hash"]: sys.stdout.write("\n\n\033[31mCRITICAL BOOTSTRAP ERROR\n" + diff --git a/configs/aarch64_defconfig b/configs/aarch64_defconfig index 4a5e4ba12..517c97a5d 100644 --- a/configs/aarch64_defconfig +++ b/configs/aarch64_defconfig @@ -75,6 +75,7 @@ BR2_PACKAGE_IPERF3=y BR2_PACKAGE_IPROUTE2=y BR2_PACKAGE_IPTABLES_NFTABLES=y BR2_PACKAGE_IPUTILS=y +BR2_PACKAGE_LINUXPTP=y BR2_PACKAGE_LLDPD=y BR2_PACKAGE_MSTPD=y BR2_PACKAGE_MTR=y diff --git a/configs/aarch64_minimal_defconfig b/configs/aarch64_minimal_defconfig index d518f506c..42433884c 100644 --- a/configs/aarch64_minimal_defconfig +++ b/configs/aarch64_minimal_defconfig @@ -68,6 +68,7 @@ BR2_PACKAGE_FRR=y # BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set BR2_PACKAGE_IPROUTE2=y BR2_PACKAGE_IPUTILS=y +BR2_PACKAGE_LINUXPTP=y BR2_PACKAGE_LLDPD=y BR2_PACKAGE_MSTPD=y BR2_PACKAGE_NETCALC=y diff --git a/configs/arm_defconfig b/configs/arm_defconfig index 6bf8bc2ed..c986bfa14 100644 --- a/configs/arm_defconfig +++ b/configs/arm_defconfig @@ -74,6 +74,7 @@ BR2_PACKAGE_FRR=y BR2_PACKAGE_IPERF3=y BR2_PACKAGE_IPROUTE2=y BR2_PACKAGE_IPUTILS=y +BR2_PACKAGE_LINUXPTP=y BR2_PACKAGE_LLDPD=y BR2_PACKAGE_MSTPD=y BR2_PACKAGE_MTR=y diff --git a/configs/arm_minimal_defconfig b/configs/arm_minimal_defconfig index ce46662fb..04a7559aa 100644 --- a/configs/arm_minimal_defconfig +++ b/configs/arm_minimal_defconfig @@ -70,6 +70,7 @@ BR2_PACKAGE_FRR=y # BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set BR2_PACKAGE_IPROUTE2=y BR2_PACKAGE_IPUTILS=y +BR2_PACKAGE_LINUXPTP=y BR2_PACKAGE_LLDPD=y BR2_PACKAGE_MSTPD=y BR2_PACKAGE_NETCALC=y diff --git a/configs/riscv64_defconfig b/configs/riscv64_defconfig index 88ac2a742..30d59e690 100644 --- a/configs/riscv64_defconfig +++ b/configs/riscv64_defconfig @@ -85,6 +85,7 @@ BR2_PACKAGE_IPERF3=y BR2_PACKAGE_IPROUTE2=y BR2_PACKAGE_IPTABLES_NFTABLES=y BR2_PACKAGE_IPUTILS=y +BR2_PACKAGE_LINUXPTP=y BR2_PACKAGE_LLDPD=y BR2_PACKAGE_MSTPD=y BR2_PACKAGE_MTR=y diff --git a/configs/x86_64_defconfig b/configs/x86_64_defconfig index 6404565d5..f961fb86d 100644 --- a/configs/x86_64_defconfig +++ b/configs/x86_64_defconfig @@ -74,6 +74,7 @@ BR2_PACKAGE_IPERF3=y BR2_PACKAGE_IPROUTE2=y BR2_PACKAGE_IPTABLES_NFTABLES=y BR2_PACKAGE_IPUTILS=y +BR2_PACKAGE_LINUXPTP=y BR2_PACKAGE_LLDPD=y BR2_PACKAGE_MSTPD=y BR2_PACKAGE_MTR=y diff --git a/configs/x86_64_minimal_defconfig b/configs/x86_64_minimal_defconfig index 8ce0c5afd..15c66ce42 100644 --- a/configs/x86_64_minimal_defconfig +++ b/configs/x86_64_minimal_defconfig @@ -67,6 +67,7 @@ BR2_PACKAGE_FRR=y # BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set BR2_PACKAGE_IPROUTE2=y BR2_PACKAGE_IPUTILS=y +BR2_PACKAGE_LINUXPTP=y BR2_PACKAGE_LLDPD=y BR2_PACKAGE_MSTPD=y BR2_PACKAGE_NETCALC=y diff --git a/doc/ptp.md b/doc/ptp.md new file mode 100644 index 000000000..5814ca8e0 --- /dev/null +++ b/doc/ptp.md @@ -0,0 +1,461 @@ +# PTP — Precision Time Protocol + +The Precision Time Protocol (PTP), defined in IEEE 1588-2019, synchronises +clocks across a network to sub-microsecond accuracy. Where NTP (Network Time +Protocol) aims at millisecond accuracy over wide-area networks, PTP is +designed for local-area networks and relies on hardware timestamping in the +network interface to eliminate software-induced jitter. + +PTP works by exchanging timestamped messages between devices. A *grandmaster +clock* — elected by the **Best TimeTransmitter Clock Algorithm (BTCA)** based +on priority, clock class, and accuracy — distributes time to the rest of the +network. Each synchronising device measures the one-way message delay to its +time-transmitter and continuously adjusts its local clock to compensate. + +> [!NOTE] +> The IEEE 1588g-2022 amendment to IEEE 1588-2019 introduced the terms +> *timeTransmitter* and *timeReceiver* as replacements for the former +> *master* and *slave* terminology, and *Best TimeTransmitter Clock +> Algorithm (BTCA)* in place of *BMCA*. This document uses the updated +> terms throughout. You may even see the short forms transmitter and +> receiver here and in online documentation. + +## Clock roles + +Every device in a PTP network takes one of the following roles: + +| Role | Description | +|----------------------------|---------------------------------------------------------------------------------------------| +| **Grandmaster (GM)** | Network-wide time source; elected by BTCA | +| **Time-transmitter** | Sends Sync messages downstream on a port | +| **Time-receiver** | Synchronises to a time-transmitter on a port | +| **Boundary Clock (BC)** | Terminates PTP on each port; acts as time-receiver upstream and time-transmitter downstream | +| **Transparent Clock (TC)** | Passes PTP messages while correcting the residence-time delay accumulated in the device | + +An **Ordinary Clock (OC)** has a single PTP port and is either a +time-transmitter (acting as a grandmaster candidate) or a time-receiver +(a leaf node synchronising to the network). + +## PTP profiles + +A **PTP profile** (as defined in IEEE 1588-2019 §3.1) is a document that +specifies a consistent set of required, permitted, and prohibited PTP +options for a particular application domain — much like a dialect of the +protocol. Examples from the standards world include profiles for power +utilities (IEC/IEEE C37.238), telecom (ITU-T G.8265.1), and +Time-Sensitive Networks. + +Each profile sets a unique value in the `majorSdoId` field of PTP message +headers — a 4-bit identifier that lets devices distinguish traffic belonging +to different profiles on the same link. Profile also determines the network +transport (UDP or Ethernet) and the delay measurement mechanism. + +Currently, two profiles are supported via the `profile` leaf in `default-ds`: + +| `profile` | Standard | majorSdoId | Transport | Delay | +|----------------------|-------------------|:----------:|-----------|----------------| +| `ieee1588` (default) | IEEE 1588-2019 | `0x0` | UDP/IPv4 | `e2e` or `p2p` | +| `ieee802-dot1as` | IEEE 802.1AS-2020 | `0x1` | L2 | `p2p` | + +The **gPTP** (generalized Precision Time Protocol) profile from IEEE 802.1AS-2020 +is used in **TSN** (Time-Sensitive Networking) and **AVB** (Audio/Video Bridging) +applications. Setting `profile ieee802-dot1as` applies all protocol-mandatory +settings automatically — Layer 2 transport, P2P delay measurement, 802.1AS +multicast addressing, path trace, follow-up information, and neighbour propagation +delay thresholds. The user still configures `priority1`, `priority2`, +`domain-number`, `time-receiver-only`, and timer interval leaves. + +The `ieee1588` profile leaves transport and delay mechanism user-configurable +per port. + +## Delay mechanisms + +PTP measures the link delay between neighbours using one of two mechanisms: + +- **End-to-End (E2E)**: Each time-receiver measures the delay to the + grandmaster by sending a `DELAY_REQ` message upstream. Simple to + configure; works with any network topology. +- **Peer-to-Peer (P2P)**: Each port measures its delay to its *immediate + neighbour* independently using `PDELAY_REQ` messages. Enables faster + path-delay updates and is required by the gPTP profile. + +## Data Sets + +IEEE 1588 organises protocol state into named **Data Sets (DS)** — each a +collection of related attributes for one aspect of a PTP instance. You +will encounter these directly in the CLI and in the `show ptp` output: + +| Data Set | CLI node | Contents | +|------------------|----------------|----------------------------------------------------------| +| Default DS | `default-ds` | Instance identity, clock class, priority, domain number | +| Current DS | `current-ds` | Live offset-from-GM, mean path delay, steps-removed | +| Parent DS | `parent-ds` | Grandmaster identity and quality attributes | +| Time Properties DS | `time-properties-ds` | UTC offset, leap-second flags, time source | +| Port DS | `port-ds` | Per-port state, delay mechanism, message intervals | + +## Domains + +A **PTP domain** (0–255) is a logical partition of the network. Devices +only synchronise with others in the same domain. Running multiple +instances on the same device — one per domain, or one per profile — is +fully supported; each instance is independent. + +Each PTP instance is identified on the network by its +`(domain-number, profile)` pair, which must be unique across all instances +on a device. + +> [!NOTE] +> The `show ptp` offset values reflect **PHC** (PTP Hardware Clock) +> synchronisation only. A PHC is the hardware clock exposed by the network +> interface; it tracks the PTP grandmaster but is independent of the Linux +> system clock, which currently is **not** automatically adjusted. + +## Ordinary Clock (time-receiver) + +A typical time-receiver Ordinary Clock, synchronising on interface +`eth0` using the default IEEE 1588 profile: + +

admin@example:/> configure
+admin@example:/config/> edit ptp instance 0
+admin@example:/config/ptp/instance/0/> set default-ds domain-number 0
+admin@example:/config/ptp/instance/0/> set default-ds time-receiver-only true
+admin@example:/config/ptp/instance/0/> edit port 1
+admin@example:/config/ptp/…/0/port/1/> set underlying-interface eth0
+admin@example:/config/ptp/…/0/port/1/> leave
+
+ +## Ordinary Clock (time-transmitter / grandmaster) + +A grandmaster clock with high priority, domain 0: + +
admin@example:/config/> edit ptp instance 0
+admin@example:/config/ptp/instance/0/> set default-ds domain-number 0
+admin@example:/config/ptp/instance/0/> set default-ds priority1 1
+admin@example:/config/ptp/instance/0/> set default-ds priority2 1
+admin@example:/config/ptp/instance/0/> edit port 1
+admin@example:/config/ptp/…/0/port/1/> set underlying-interface eth0
+admin@example:/config/ptp/…/0/port/1/> leave
+
+ +Lower `priority1` values win in the BTCA. A clock with `priority1 1` will +be preferred over the default `128` in any compliant network. + +## Boundary Clock + +A Boundary Clock terminates PTP on each port and re-originates it. Add one +port per interface: + +
admin@example:/config/> edit ptp instance 0
+admin@example:/config/ptp/instance/0/> set default-ds instance-type bc
+admin@example:/config/ptp/instance/0/> set default-ds domain-number 0
+admin@example:/config/ptp/instance/0/> edit port 1
+admin@example:/config/ptp/…/0/port/1/> set underlying-interface eth0
+admin@example:/config/ptp/…/0/port/1/> end
+admin@example:/config/ptp/instance/0/> edit port 2
+admin@example:/config/ptp/…/0/port/2/> set underlying-interface eth1
+admin@example:/config/ptp/…/0/port/2/> leave
+
+ +> [!TIP] +> PTP port numbers are assigned sorted by `port-index`, so `port-index 1` +> becomes PTP port 1, `port-index 2` becomes PTP port 2, and so on. + +## Transparent Clock + +Transparent Clocks correct timestamps end-to-end without terminating PTP. +Use `instance-type p2p-tc` for a P2P TC (preferred in TSN networks) or +`instance-type e2e-tc` for an E2E TC: + +
admin@example:/config/> edit ptp instance 0
+admin@example:/config/ptp/instance/0/> set default-ds instance-type p2p-tc
+admin@example:/config/ptp/instance/0/> set default-ds domain-number 0
+admin@example:/config/ptp/instance/0/> edit port 1
+admin@example:/config/ptp/…/0/port/1/> set underlying-interface eth0
+admin@example:/config/ptp/…/0/port/1/> end
+admin@example:/config/ptp/instance/0/> edit port 2
+admin@example:/config/ptp/…/0/port/2/> set underlying-interface eth1
+admin@example:/config/ptp/…/0/port/2/> leave
+
+ +> [!NOTE] +> For Transparent Clocks the delay mechanism is determined globally by the +> `instance-type` (`p2p-tc` → P2P, `e2e-tc` → E2E). Per-port +> `delay-mechanism` settings have no effect for TC instances. + +## gPTP / IEEE 802.1AS + +The gPTP profile is used in TSN and AVB applications. Setting +`profile ieee802-dot1as` applies all protocol-mandatory options from +IEEE 802.1AS-2020 automatically — Layer 2 transport, P2P delay +measurement, 802.1AS multicast addressing, and related protocol features. + +
admin@example:/config/> edit ptp instance 0
+admin@example:/config/ptp/instance/0/> set default-ds profile ieee802-dot1as
+admin@example:/config/ptp/instance/0/> set default-ds domain-number 0
+admin@example:/config/ptp/instance/0/> set default-ds time-receiver-only true
+admin@example:/config/ptp/instance/0/> edit port 1
+admin@example:/config/ptp/…/0/port/1/> set underlying-interface eth0
+admin@example:/config/ptp/…/0/port/1/> leave
+
+ +> [!NOTE] +> The `ieee802-dot1as` profile enforces Layer 2 transport and P2P delay +> measurement globally, as required by IEEE 802.1AS-2020. Per-port +> `delay-mechanism` settings have no effect for 802.1AS instances. + +## Multiple Instances + +Multiple PTP instances can run simultaneously, one per domain or profile +combination. Each instance must have a unique `(domain-number, profile)` +pair and an independent set of ports: + +
admin@example:/config/> edit ptp instance 0
+admin@example:/config/ptp/instance/0/> set default-ds domain-number 0
+admin@example:/config/ptp/instance/0/> set default-ds profile ieee1588
+admin@example:/config/ptp/instance/0/> edit port 1
+admin@example:/config/ptp/…/0/port/1/> set underlying-interface eth0
+admin@example:/config/ptp/…/0/port/1/> end
+admin@example:/config/ptp/instance/0/> end
+admin@example:/config/ptp/> edit instance 1
+admin@example:/config/ptp/instance/1/> set default-ds domain-number 0
+admin@example:/config/ptp/instance/1/> set default-ds profile ieee802-dot1as
+admin@example:/config/ptp/instance/1/> edit port 1
+admin@example:/config/ptp/…/1/port/1/> set underlying-interface eth1
+admin@example:/config/ptp/…/1/port/1/> leave
+
+ +## Port states + +Each PTP port progresses through a state machine. The current state is +shown in the `show ptp` port table: + +| State | Meaning | +|------------------------|----------------------------------------------------------------| +| `initializing` | Port is starting up, not yet ready to exchange messages | +| `faulty` | A fault condition has been detected on this port | +| `disabled` | Port is administratively disabled | +| `listening` | Awaiting `ANNOUNCE` messages; BTCA has not yet resolved | +| `pre-time-transmitter` | Transitioning towards time-transmitter state | +| `time-transmitter` | Port is acting as time-transmitter on this link | +| `passive` | Another port on this device is already time-transmitter | +| `uncalibrated` | Receiving sync; local clock not yet locked to time-transmitter | +| `time-receiver` | Port is locked and tracking its time-transmitter | + +A port in `uncalibrated` will typically transition to `time-receiver` +within a few seconds once the clock servo has converged. + +## Monitoring + +> [!TIP] Use the ++question++ key in the CLI +> The `show ptp` command has sub-commands — tap ++question++ after +> `show ptp` to see them, or use ++tab++ to complete. + +### Show all PTP instances + +
admin@example:/> show ptp
+PTP Instance 0                          Ordinary Clock · domain 0
+────────────────────────────────────────────────────────────────────
+  Clock identity          : AA-BB-CC-FF-FE-00-11-22
+  Grandmaster             : DD-EE-FF-FF-FE-33-44-55
+  Priority1/Priority2     : 128 / 128
+  GM Priority1/Priority2  : 1 / 1
+  Clock class             : cc-time-receiver-only
+  GM clock class          : cc-primary-sync
+  Time source             : gnss
+  PTP timescale           : yes
+  UTC offset              : 37 s
+  Time traceable          : yes
+  Freq. traceable         : yes
+  Offset from GM          : -42 ns
+  Mean path delay         : 1250 ns
+  Steps removed           : 1
+
+────────────────────────────────────────────────────────────────────
+Ports
+PORT  INTERFACE          STATE                DELAY  LINK DELAY (ns)
+   1  eth0               time-receiver        E2E                  0
+
+────────────────────────────────────────────────────────────────────
+Message Statistics  (▼ rx  ▲ tx)
+PORT  INTERFACE             SYNC ▼  SYNC ▲  ANN ▼  ANN ▲  PD ▼  PD ▲
+   1  eth0                      42       0     15      0     0     0
+
+
+ +Port state is colour-coded: green for `time-transmitter` and `time-receiver` +(actively synchronising), yellow for transient states (`listening`, +`uncalibrated`, `pre-time-transmitter`), and red for fault states (`faulty`, +`disabled`). The *Message Statistics* section is omitted when no counts are +available. + +### Show a specific instance + +
admin@example:/> show ptp 0
+
+ +## Tuning port intervals + +Adjust announcement, sync, and delay-request intervals per port. Values +are expressed as log₂ of the interval in seconds (e.g. `-3` = 125 ms, +`0` = 1 s, `1` = 2 s): + +
admin@example:/config/ptp/…/0/port/1/> set port-ds log-announce-interval 0
+admin@example:/config/ptp/…/0/port/1/> set port-ds log-sync-interval -3
+admin@example:/config/ptp/…/0/port/1/> set port-ds log-min-delay-req-interval 0
+admin@example:/config/ptp/…/0/port/1/> set announce-receipt-timeout 3
+
+ +`announce-receipt-timeout` is a count of announce intervals, not a duration +in seconds. With `log-announce-interval 0` (1 s) and +`announce-receipt-timeout 3`, a port waits 3 s without receiving an +`ANNOUNCE` before declaring the time-transmitter lost and returning to +`listening`. + +## Message exchange + +PTP distributes time using a small set of messages, all of which carry +hardware timestamps at the network interface: + +| Message | Timestamped | Purpose | +|-------------------------|:-----------:|-----------------------------------------------------| +| `ANNOUNCE` | No | Advertises clock quality for BTCA election | +| `SYNC` | Yes | Carries transmitter timestamp to receivers | +| `FOLLOW_UP` | No | Carries precise `t1` in two-step mode | +| `DELAY_REQ` | Yes | Receiver-initiated E2E delay measurement | +| `DELAY_RESP` | No | Time-transmitter reply to `DELAY_REQ` | +| `PDELAY_REQ` | Yes | Initiates P2P neighbour-delay measurement | +| `PDELAY_RESP` | Yes | Neighbour reply to `PDELAY_REQ` | +| `PDELAY_RESP_FOLLOW_UP` | No | Carries precise `PDELAY_RESP` `t3` in two-step mode | + +In **one-step** mode the timestamp is embedded directly into each `SYNC` +message as it leaves the wire, eliminating the need for `FOLLOW_UP`. +In **two-step** mode the `SYNC` carries a placeholder and the precise +transmit timestamp arrives in a subsequent `FOLLOW_UP`. Hardware +timestamping gives high accuracy in both modes; one-step reduces message +overhead at the cost of more demanding hardware support. + +## Message format + +Every PTP message begins with a common 34-octet header, regardless of type. +The structure below follows the traditional IETF bit-field layout: each row +is four octets wide, bit 7 (MSB) is on the left and bit 0 (LSB) on the +right within each octet. + +``` + 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 0-3 |trSpec |msgType| rsv | ver | messageLength | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 4-7 | domainNumber | minorSdoId | flags | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 8-15 | | + + correctionField + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +16-19 | messageTypeSpecific | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +20-27 | | + + clockIdentity + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +28-31 | portNumber | sequenceId | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +32-33 | controlField | logMsgIntvl | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + +- **`trSpec`** (`transportSpecific`, bits 7–4 of octet 0): 4-bit profile + identifier. `0x0` = IEEE 1588, `0x1` = gPTP (802.1AS). Set implicitly + by the `profile` configuration leaf. +- **`msgType`** (`messageType`, bits 3–0 of octet 0): `0x0` SYNC · + `0x1` DELAY_REQ · `0x2` PDELAY_REQ · `0x3` PDELAY_RESP · + `0x8` FOLLOW_UP · `0x9` DELAY_RESP · `0xA` PDELAY_RESP_FOLLOW_UP · + `0xB` ANNOUNCE. +- **`rsv`** (reserved, bits 7–4 of octet 1): Set to zero; ignored on + receipt. +- **`ver`** (`versionPTP`, bits 3–0 of octet 1): PTP version; `2` for + IEEE 1588-2008 and IEEE 1588-2019. +- **`messageLength`** (octets 2–3): Total message length in octets, + including the header. +- **`domainNumber`** (octet 4): PTP domain; receivers silently discard + messages that do not match their configured domain. +- **`minorSdoId`** (octet 5): Reserved in IEEE 1588-2008; carries a + profile sub-identifier in IEEE 1588-2019. +- **`flags`** (octets 6–7): Per-message flags — includes the two-step + flag (set when a FOLLOW_UP will follow a SYNC), UTC offset valid, and + leap-second indicators. +- **`correctionField`** (octets 8–15): Accumulated path correction in + nanoseconds × 2¹⁶. Transparent Clocks add their measured residence + time and link delay here as they forward each message, so the final + time-receiver can subtract the total accumulated delay. +- **`messageTypeSpecific`** (octets 16–19): Reserved in IEEE 1588-2008; + carries message-type-specific data in IEEE 1588-2019. +- **`clockIdentity`** (octets 20–27): EUI-64 identity of the sending + clock — the value shown as "Clock identity" in `show ptp`. +- **`portNumber`** (octets 28–29): Port number of the sender within its + clock; together with `clockIdentity` it forms the unique + `sourcePortIdentity`. +- **`sequenceId`** (octets 30–31): Increments with each message; used to + match a DELAY_REQ to its DELAY_RESP. +- **`controlField`** (octet 32): Deprecated in PTPv2; set to fixed + values per message type for backward compatibility with PTPv1. +- **`logMsgIntvl`** (`logMessageInterval`, octet 33): Log₂ of the + expected interval between messages of this type; `0x7F` means not + applicable. + +The `transportSpecific` and `domainNumber` fields are the quickest way to +verify on the wire that a device is using the profile and domain you +configured. + +### Decoding with Wireshark + +Wireshark decodes PTP messages automatically, expanding every header field +and message-type-specific payload in the packet tree. PTP travels over +two UDP ports — 319 for event messages (SYNC, DELAY_REQ, PDELAY_REQ and +their responses) and 320 for general messages (ANNOUNCE, FOLLOW_UP) — as +well as directly over Ethernet (EtherType `0x88F7`) when layer-2 transport +is in use. + +Use the display filter `ptp` to isolate PTP traffic: + +``` +ptp +``` + +To narrow down to a specific domain or profile (exact field names can be +verified in Wireshark via **View → Internals → Supported Protocols**, +filtering for `ptp`): + +``` +ptp.v2.domainnumber == 0 +ptp.v2.transportspecific == 1 +``` + +This makes it straightforward to confirm which grandmaster a port is +tracking, verify that `correctionField` is being updated by a Transparent +Clock, or diagnose why the BTCA is not electing the expected grandmaster. + +## Glossary + +| Abbreviation | Expansion | Notes | +|--------------|--------------------------------------|-------------------------------------------------------------------| +| AVB | Audio/Video Bridging | IEEE 802.1 precursor to TSN; real-time AV over Ethernet | +| IETF | Internet Engineering Task Force | Standards body; defines RFC for layer-3 and up | +| UDP | User Datagram Protocol | IP transport used by PTP; port 319 (event) and 320 (general) | +| EUI-64 | Extended Unique Identifier (64-bit) | IEEE identifier format used as `clockIdentity` in PTP | +| EtherType | Ethernet frame type field | `0x88F7` identifies PTP over layer-2 Ethernet | +| BC | Boundary Clock | Terminates and re-originates PTP on each port | +| BTCA | Best TimeTransmitter Clock Algorithm | Elects the GM; replaces BMCA from IEEE 1588-2008 | +| CMLDS | Common Mean Link Delay Service | IEEE 1588-2019 §16.6; shared delay service for multiple instances | +| DS | Data Set | Named attribute collection in IEEE 1588 (default-ds, port-ds, …) | +| E2E | End-to-End | Delay mechanism: measures path from GM to time-receiver | +| GM | Grandmaster | PTP network-wide time source, elected by BTCA | +| gPTP | generalized Precision Time Protocol | IEEE 802.1AS profile; used in TSN and AVB | +| NTP | Network Time Protocol | Millisecond-accuracy time protocol for wide-area use | +| OC | Ordinary Clock | Single-port PTP clock; time-transmitter or time-receiver | +| P2P | Peer-to-Peer | Delay mechanism: measures delay to immediate neighbour | +| PHC | PTP Hardware Clock | Hardware clock in the NIC used for PTP timestamping | +| PTP | Precision Time Protocol | IEEE 1588 sub-microsecond clock synchronisation protocol | +| SDO | Standards Development Organization | Body that defines a PTP profile; encoded in `sdo-id` | +| TC | Transparent Clock | Forwards PTP messages, correcting for residence-time delay | +| TSN | Time-Sensitive Networking | IEEE 802.1 standard set for deterministic Ethernet | diff --git a/mkdocs.yml b/mkdocs.yml index bc640dc3d..9e92c10d6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -47,6 +47,7 @@ nav: - Device Discovery: discovery.md - DHCP Server: dhcp.md - NTP Server: ntp.md + - PTP (IEEE 1588/802.1AS): ptp.md - System: - Boot Procedure: boot.md - Configuration: system.md diff --git a/patches/linuxptp/0001-port-Fix-unicast-negotiation-doesn-t-recover-after-F.patch b/patches/linuxptp/0001-port-Fix-unicast-negotiation-doesn-t-recover-after-F.patch new file mode 100644 index 000000000..08c5dae7e --- /dev/null +++ b/patches/linuxptp/0001-port-Fix-unicast-negotiation-doesn-t-recover-after-F.patch @@ -0,0 +1,114 @@ +From d68e6a149340d987ebd19f86a7d792f4aaefdf98 Mon Sep 17 00:00:00 2001 +From: Vincent Cheng +Date: Tue, 17 Sep 2024 15:23:54 -0400 +Subject: [PATCH 01/13] port: Fix unicast negotiation doesn't recover after + FAULT_DETECTED +Organization: Wires + +_Problem_ +After a port link down/up or a tx_timestamp timeout issue, a port acting +as unicast master does not issue ANNC messages after granting unicast +request for ANNC. + +_Analysis_ +When a port FAULT occurs, the port transitions to FAULTY on FAULT_DETECTED +and subsequently port_disable(p) and port_initialize(p) are called on port recovery. + +A port acting as a unicast master, stores clients in p->unicast_service->queue. + +When a port receives a unicast request, unicast_service_add() is called. + +In unicast_service_add(), if the request does not match an entry in +p->unicast_service->queue, FD_UNICAST_SRV_TIMER is started via +unicast_service_rearm_timer(). + +If the unicast request matches an existing p->unicast_service->queue entry +the request is considered an extension and FD_UNICAST_SRV_TIMER must +already be running. + +port_disable() clears FD_UNICAST_SRV_TIMER, ie. stops FD_UNICAST_SRV_TIMER. +However, port_disable() does not clear p->unicast_service->queue. +When the port is restarted, the port retains the previous client data. + +After port recovery, when the client attempts to restart the unicast +service, the request matches an existing entry in p->unicast_service->queue, +and so FD_UNICAST_SRV_TIMER is not started because the port expected +that the FD_UNICAST_SRV_TIMER is already running. + +_Fix_ +This patch clears the unicast client data in port_disable() so +that upon recovery, the initial unicast request will be considered +a new request and trigger the start of the FD_UNICAST_SRV_TIMER. + +v2: +- Add missing sign-off +- Send to develop-request instead of users list + +Signed-off-by: Vincent Cheng +Signed-off-by: Joachim Wiberg +--- + port.c | 1 + + unicast_service.c | 21 +++++++++++++++++++++ + unicast_service.h | 6 ++++++ + 3 files changed, 28 insertions(+) + +diff --git a/port.c b/port.c +index db35a44..282be66 100644 +--- a/port.c ++++ b/port.c +@@ -1985,6 +1985,7 @@ void port_disable(struct port *p) + flush_peer_delay(p); + + p->best = NULL; ++ unicast_service_clear_clients(p); + free_foreign_masters(p); + transport_close(p->trp, &p->fda); + +diff --git a/unicast_service.c b/unicast_service.c +index 687468c..d7a4ecd 100644 +--- a/unicast_service.c ++++ b/unicast_service.c +@@ -571,3 +571,24 @@ int unicast_service_timer(struct port *p) + } + return err; + } ++ ++void unicast_service_clear_clients(struct port *p) ++{ ++ struct unicast_client_address *client, *temp; ++ struct unicast_service_interval *interval; ++ ++ if (!p->unicast_service) { ++ return; ++ } ++ ++ while ((interval = pqueue_extract(p->unicast_service->queue)) != NULL) { ++ ++ LIST_REMOVE(interval, list); ++ ++ LIST_FOREACH_SAFE(client, &interval->clients, list, temp) { ++ LIST_REMOVE(client, list); ++ free(client); ++ } ++ free(interval); ++ } ++} +\ No newline at end of file +diff --git a/unicast_service.h b/unicast_service.h +index f0d6487..8ea1a59 100644 +--- a/unicast_service.h ++++ b/unicast_service.h +@@ -87,4 +87,10 @@ void unicast_service_remove(struct port *p, struct ptp_message *m, + */ + int unicast_service_timer(struct port *p); + ++/** ++ * Clears unicast clients on a given port. ++ * @param p The port in question. ++ */ ++void unicast_service_clear_clients(struct port *p); ++ + #endif +-- +2.43.0 + diff --git a/patches/linuxptp/0002-udp-Fix-port-specific-ptp-p2p_dst_ipv4-configuration.patch b/patches/linuxptp/0002-udp-Fix-port-specific-ptp-p2p_dst_ipv4-configuration.patch new file mode 100644 index 000000000..bad0848c6 --- /dev/null +++ b/patches/linuxptp/0002-udp-Fix-port-specific-ptp-p2p_dst_ipv4-configuration.patch @@ -0,0 +1,92 @@ +From 4ea423b94264e4eeb0c2706fc3485b1fe283cc11 Mon Sep 17 00:00:00 2001 +From: Miroslav Lichvar +Date: Wed, 25 Sep 2024 14:37:20 +0200 +Subject: [PATCH 02/13] udp: Fix port-specific ptp/p2p_dst_ipv4 configuration. +Organization: Wires + +If different ports are configured with a different ptp_dst_ipv4 or +p2p_dst_ipv4 address, only the last port in the configuration works +correctly. This is caused by a global variable holding the +destination address for all ports using the udp transport. + +Move the address to the udp structure to avoid the conflict between +different ports, same as when port-specific scope in udp6 was fixed +in commit a48666bee3dd ("udp6: Make mc6_addr transport-local"). + +Fixes: 8a26c94cc88e ("udp+udp6: Make IP addresses configurable.") +Signed-off-by: Miroslav Lichvar +Signed-off-by: Joachim Wiberg +--- + udp.c | 16 ++++++++-------- + 1 file changed, 8 insertions(+), 8 deletions(-) + +diff --git a/udp.c b/udp.c +index 38d0ec4..c9b5f39 100644 +--- a/udp.c ++++ b/udp.c +@@ -44,6 +44,7 @@ struct udp { + struct transport t; + struct address ip; + struct address mac; ++ struct in_addr mcast_addr[2]; + }; + + static int mcast_bind(int fd, int index) +@@ -146,8 +147,6 @@ no_socket: + + enum { MC_PRIMARY, MC_PDELAY }; + +-static struct in_addr mcast_addr[2]; +- + static int udp_open(struct transport *t, struct interface *iface, + struct fdarray *fda, enum timestamp_type ts_type) + { +@@ -165,22 +164,22 @@ static int udp_open(struct transport *t, struct interface *iface, + sk_interface_addr(name, AF_INET, &udp->ip); + + str = config_get_string(t->cfg, name, "ptp_dst_ipv4"); +- if (!inet_aton(str, &mcast_addr[MC_PRIMARY])) { ++ if (!inet_aton(str, &udp->mcast_addr[MC_PRIMARY])) { + pr_err("invalid ptp_dst_ipv4 %s", str); + return -1; + } + + str = config_get_string(t->cfg, name, "p2p_dst_ipv4"); +- if (!inet_aton(str, &mcast_addr[MC_PDELAY])) { ++ if (!inet_aton(str, &udp->mcast_addr[MC_PDELAY])) { + pr_err("invalid p2p_dst_ipv4 %s", str); + return -1; + } + +- efd = open_socket(name, mcast_addr, EVENT_PORT, ttl); ++ efd = open_socket(name, udp->mcast_addr, EVENT_PORT, ttl); + if (efd < 0) + goto no_event; + +- gfd = open_socket(name, mcast_addr, GENERAL_PORT, ttl); ++ gfd = open_socket(name, udp->mcast_addr, GENERAL_PORT, ttl); + if (gfd < 0) + goto no_general; + +@@ -223,6 +222,7 @@ static int udp_send(struct transport *t, struct fdarray *fda, + enum transport_event event, int peer, void *buf, int len, + struct address *addr, struct hw_timestamp *hwts) + { ++ struct udp *udp = container_of(t, struct udp, t); + struct address addr_buf; + unsigned char junk[1600]; + ssize_t cnt; +@@ -243,8 +243,8 @@ static int udp_send(struct transport *t, struct fdarray *fda, + if (!addr) { + memset(&addr_buf, 0, sizeof(addr_buf)); + addr_buf.sin.sin_family = AF_INET; +- addr_buf.sin.sin_addr = peer ? mcast_addr[MC_PDELAY] : +- mcast_addr[MC_PRIMARY]; ++ addr_buf.sin.sin_addr = peer ? udp->mcast_addr[MC_PDELAY] : ++ udp->mcast_addr[MC_PRIMARY]; + addr_buf.len = sizeof(addr_buf.sin); + addr = &addr_buf; + } +-- +2.43.0 + diff --git a/patches/linuxptp/0003-pmc-Avoid-race-conditions-in-agent-update.patch b/patches/linuxptp/0003-pmc-Avoid-race-conditions-in-agent-update.patch new file mode 100644 index 000000000..2245812ec --- /dev/null +++ b/patches/linuxptp/0003-pmc-Avoid-race-conditions-in-agent-update.patch @@ -0,0 +1,82 @@ +From e76bb37019605dea4acd9ccec620a3faec1ba402 Mon Sep 17 00:00:00 2001 +From: Miroslav Lichvar +Date: Thu, 17 Oct 2024 15:05:21 +0200 +Subject: [PATCH 03/13] pmc: Avoid race conditions in agent update. +Organization: Wires + +The pmc_agent_update() function updates the subscription to +notifications and also the current UTC offset. It uses a timeout of 0 +to avoid blocking. When the pmc client sends the first request, the +response from ptp4l may not come quickly enough to be received in the +same run_pmc() call. It then sends the other request and checks for the +response. If it is the response to the first request, it will be ignored. +The update works correctly only if both responses are quick enough to be +received in the same call, or are both slow enough that they are +received in the next call of the pmc_agent_update() function. + +The function needs to be called a random number of times in order to +finish one update. If the mismatch between requests and responses +happened consistently, the agent would never reach the up-to-date state +and phc2sys would not enter the main synchronization loop. + +Split the update into two phases, where only one thing is updated at a +time. The function now needs to be called at most 3 times to update both +the subscription and UTC offset, assuming it is not interrupted by +another request outside of the agent's update. + +Signed-off-by: Miroslav Lichvar +Reviewed-by: Jacob Keller +Signed-off-by: Joachim Wiberg +--- + pmc_agent.c | 24 ++++++++++++++++++------ + 1 file changed, 18 insertions(+), 6 deletions(-) + +diff --git a/pmc_agent.c b/pmc_agent.c +index 86b6ee6..d1a3367 100644 +--- a/pmc_agent.c ++++ b/pmc_agent.c +@@ -37,6 +37,7 @@ struct pmc_agent { + struct pmc *pmc; + uint64_t pmc_last_update; + uint64_t update_interval; ++ int update_phase; + + struct defaultDS dds; + bool dds_valid; +@@ -427,16 +428,27 @@ int pmc_agent_update(struct pmc_agent *node) + ts = tp.tv_sec * NS_PER_SEC + tp.tv_nsec; + + if (ts - node->pmc_last_update >= node->update_interval) { +- if (node->stay_subscribed) { +- renew_subscription(node, 0); +- } +- if (!pmc_agent_query_utc_offset(node, 0)) { ++ switch (node->update_phase) { ++ case 0: ++ if (node->stay_subscribed && ++ renew_subscription(node, 0)) ++ break; ++ node->update_phase++; ++ /* Fall through */ ++ case 1: ++ if (pmc_agent_query_utc_offset(node, 0)) ++ break; ++ node->update_phase++; ++ /* Fall through */ ++ default: + node->pmc_last_update = ts; ++ node->update_phase = 0; ++ break; + } ++ } else { ++ run_pmc(node, 0, -1, &msg); + } + +- run_pmc(node, 0, -1, &msg); +- + return 0; + } + +-- +2.43.0 + diff --git a/patches/linuxptp/0004-phc2sys-Wait-until-pmc-agent-is-subscribed.patch b/patches/linuxptp/0004-phc2sys-Wait-until-pmc-agent-is-subscribed.patch new file mode 100644 index 000000000..0a25b03d1 --- /dev/null +++ b/patches/linuxptp/0004-phc2sys-Wait-until-pmc-agent-is-subscribed.patch @@ -0,0 +1,45 @@ +From 1942fde263f40d318d56acf824626641263facd0 Mon Sep 17 00:00:00 2001 +From: Miroslav Lichvar +Date: Thu, 17 Oct 2024 15:05:22 +0200 +Subject: [PATCH 04/13] phc2sys: Wait until pmc agent is subscribed. +Organization: Wires + +When phc2sys is configured with multiple domains, different domains may +have their pmc agent subscribed after different number of calls of the +pmc_agent_update() function depending on how quickly responses from +ptp4l are received. If one domain triggers reconfiguration and the other +domain does not have its agent subscribed yet, it will not have any of +its clocks synchronized until a port changes state and triggers another +reconfiguration of the domain. + +To avoid this problem, wait for each domain to have its agent subscribed +before entering the main synchronization loop. Use a 10ms update +interval to speed up the start of phc2sys. + +Signed-off-by: Miroslav Lichvar +Reviewed-by: Jacob Keller +Signed-off-by: Joachim Wiberg +--- + phc2sys.c | 6 ++++++ + 1 file changed, 6 insertions(+) + +diff --git a/phc2sys.c b/phc2sys.c +index 6113539..47e896e 100644 +--- a/phc2sys.c ++++ b/phc2sys.c +@@ -962,6 +962,12 @@ static int auto_init_ports(struct domain *domain) + return -1; + } + ++ while (!pmc_agent_is_subscribed(domain->agent)) { ++ usleep(10000); ++ if (pmc_agent_update(domain->agent) < 0) ++ return -1; ++ } ++ + for (i = 1; i <= number_ports; i++) { + err = pmc_agent_query_port_properties(domain->agent, 1000, i, + &state, ×tamping, +-- +2.43.0 + diff --git a/patches/linuxptp/0005-Fix-issue-where-MAC-driver-is-configured-with-incorr.patch b/patches/linuxptp/0005-Fix-issue-where-MAC-driver-is-configured-with-incorr.patch new file mode 100644 index 000000000..f4c71b442 --- /dev/null +++ b/patches/linuxptp/0005-Fix-issue-where-MAC-driver-is-configured-with-incorr.patch @@ -0,0 +1,51 @@ +From 9e1b0df61c4dc9bb1a4aac11acb06e4f1f2c68f7 Mon Sep 17 00:00:00 2001 +From: William Comly +Date: Thu, 31 Oct 2024 15:10:20 -0400 +Subject: [PATCH 05/13] Fix issue where MAC driver is configured with incorrect + adjustment flags sometimes returned by SIOCGHWTSTAMP test. +Organization: Wires + +Once the check for the VLAN bonding flag is complete, clear the ifreq +message to ensure only the intended configuration and flags are set +in the driver. + +Signed-off-by: William Comly +Signed-off-by: Joachim Wiberg +--- + sk.c | 20 +++++++++----------- + 1 file changed, 9 insertions(+), 11 deletions(-) + +diff --git a/sk.c b/sk.c +index aadb237..4860af2 100644 +--- a/sk.c ++++ b/sk.c +@@ -69,17 +69,15 @@ static int hwts_init(int fd, const char *device, int rx_filter, + /* Test if VLAN over bond is supported. */ + cfg.flags = HWTSTAMP_FLAG_BONDED_PHC_INDEX; + err = ioctl(fd, SIOCGHWTSTAMP, &ifreq); +- if (err < 0) { +- /* +- * Fall back without flag if user runs new build on old kernel +- * or if driver does not support SIOCGHWTSTAMP ioctl. +- */ +- if (errno == EINVAL || errno == EOPNOTSUPP) { +- init_ifreq(&ifreq, &cfg, device); +- } else { +- pr_err("ioctl SIOCGHWTSTAMP failed: %m"); +- return err; +- } ++ if (err < 0 && errno != EINVAL && errno != EOPNOTSUPP) { ++ pr_err("ioctl SIOCGHWTSTAMP failed: %m"); ++ return err; ++ } ++ ++ init_ifreq(&ifreq, &cfg, device); ++ /* If VLAN over bond supported in kernel, configure flag in driver. */ ++ if (err == 0) { ++ cfg.flags = HWTSTAMP_FLAG_BONDED_PHC_INDEX; + } + + switch (sk_hwts_filter_mode) { +-- +2.43.0 + diff --git a/patches/linuxptp/0006-pmc_agent-Use-longer-update-interval-when-not-subscr.patch b/patches/linuxptp/0006-pmc_agent-Use-longer-update-interval-when-not-subscr.patch new file mode 100644 index 000000000..20d4cac36 --- /dev/null +++ b/patches/linuxptp/0006-pmc_agent-Use-longer-update-interval-when-not-subscr.patch @@ -0,0 +1,50 @@ +From 0bb9080c1bf631ae0265475d75860b6acd92ed2c Mon Sep 17 00:00:00 2001 +From: Miroslav Lichvar +Date: Tue, 26 Nov 2024 15:10:32 +0100 +Subject: [PATCH 06/13] pmc_agent: Use longer update interval when not + subscribed. +Organization: Wires + +When phc2sys is started with the -w option, the pmc agent is not +subscribed to events by the pmc_agent_subscribe() function, which also +sets the update interval. The update interval in this case is zero, +which means the pmc agent is trying to update the currentUtcOffset value +on every call of pmc_agent_update(), i.e. on every clock update in +phc2sys. + +Set a default update interval of 60 seconds to reduce the rate of +pmc requests. + +Fixes: e3ca7ea90a9e ("pmc_agent: Make update interval configurable.") +Signed-off-by: Miroslav Lichvar +Reviewed-by: Jacob Keller +Signed-off-by: Joachim Wiberg +--- + pmc_agent.c | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/pmc_agent.c b/pmc_agent.c +index d1a3367..663adc0 100644 +--- a/pmc_agent.c ++++ b/pmc_agent.c +@@ -33,6 +33,9 @@ + #define UPDATES_PER_SUBSCRIPTION 3 + #define MIN_UPDATE_INTERVAL 10 + ++/* Update interval if the agent not subscribed, just polling the UTC offset */ ++#define DEFAULT_UPDATE_INTERVAL 60 ++ + struct pmc_agent { + struct pmc *pmc; + uint64_t pmc_last_update; +@@ -253,6 +256,7 @@ int init_pmc_node(struct config *cfg, struct pmc_agent *node, const char *uds, + } + node->recv_subscribed = recv_subscribed; + node->recv_context = context; ++ node->update_interval = DEFAULT_UPDATE_INTERVAL * NS_PER_SEC; + + return 0; + } +-- +2.43.0 + diff --git a/patches/linuxptp/0007-phc2sys-Don-t-disable-pmc-agent-with-s-d-w-options.patch b/patches/linuxptp/0007-phc2sys-Don-t-disable-pmc-agent-with-s-d-w-options.patch new file mode 100644 index 000000000..a759b8cde --- /dev/null +++ b/patches/linuxptp/0007-phc2sys-Don-t-disable-pmc-agent-with-s-d-w-options.patch @@ -0,0 +1,40 @@ +From 5a97466a0ed88bae244566c2a5dba85ce72e4f01 Mon Sep 17 00:00:00 2001 +From: Miroslav Lichvar +Date: Tue, 26 Nov 2024 15:10:33 +0100 +Subject: [PATCH 07/13] phc2sys: Don't disable pmc agent with -s -d -w options. +Organization: Wires + +When phc2sys is started with -s and -d options to combine a PPS device +and PHC device as a time source, but without an offset specified by +the -O option, the pmc agent is disabled after waiting for ptp4l to have +a port in a synchronized state. This prevents phc2sys from following +changes in the currentUtcOffset value. + +Disable the pmc agent only if no PHC device is specified by the -s +option, i.e. there are no PHC readings to which the UTC offset could be +applied. + +Fixes: 5f1b419c4102 ("phc2sys: Replace magical test with a proper test.") +Signed-off-by: Miroslav Lichvar +Reviewed-by: Jacob Keller +Signed-off-by: Joachim Wiberg +--- + phc2sys.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/phc2sys.c b/phc2sys.c +index 47e896e..5962f6c 100644 +--- a/phc2sys.c ++++ b/phc2sys.c +@@ -1547,7 +1547,7 @@ int main(int argc, char *argv[]) + + if (domains[0].forced_sync_offset || + !phc2sys_using_systemclock(&domains[0]) || +- hardpps_configured(pps_fd)) { ++ (hardpps_configured(pps_fd) && !src_name)) { + pmc_agent_disable(domains[0].agent); + } + } +-- +2.43.0 + diff --git a/patches/linuxptp/0008-port_signaling.c-ensure-that-signaling-messages-resp.patch b/patches/linuxptp/0008-port_signaling.c-ensure-that-signaling-messages-resp.patch new file mode 100644 index 000000000..1f526fc8c --- /dev/null +++ b/patches/linuxptp/0008-port_signaling.c-ensure-that-signaling-messages-resp.patch @@ -0,0 +1,29 @@ +From 9822b22a07e05eb087cedfbb4a21ceecbb8ec74e Mon Sep 17 00:00:00 2001 +From: William Comly +Date: Wed, 19 Feb 2025 10:12:28 -0500 +Subject: [PATCH 08/13] port_signaling.c: ensure that signaling messages + respect ptp_minor_version in message header +Organization: Wires + +Signed-off-by: William Comly +Signed-off-by: Joachim Wiberg +--- + port_signaling.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/port_signaling.c b/port_signaling.c +index ca4202f..cf28756 100644 +--- a/port_signaling.c ++++ b/port_signaling.c +@@ -41,7 +41,7 @@ struct ptp_message *port_signaling_construct(struct port *p, + } + msg->hwts.type = p->timestamping; + msg->header.tsmt = SIGNALING | p->transportSpecific; +- msg->header.ver = PTP_VERSION; ++ msg->header.ver = ptp_hdr_ver; + msg->header.messageLength = sizeof(struct signaling_msg); + msg->header.domainNumber = clock_domain_number(p->clock); + msg->header.sourcePortIdentity = p->portIdentity; +-- +2.43.0 + diff --git a/patches/linuxptp/0009-port-Refresh-link-status-on-faults.patch b/patches/linuxptp/0009-port-Refresh-link-status-on-faults.patch new file mode 100644 index 000000000..5e0ac7e3e --- /dev/null +++ b/patches/linuxptp/0009-port-Refresh-link-status-on-faults.patch @@ -0,0 +1,82 @@ +From b8a3020e806dd6252e7edd434ae3940706a0b516 Mon Sep 17 00:00:00 2001 +From: Miroslav Lichvar +Date: Tue, 4 Mar 2025 15:53:37 +0100 +Subject: [PATCH 09/13] port: Refresh link status on faults. +Organization: Wires + +ptp4l gets the ENOBUFS error on the netlink socket when the kernel has +to drop messages due to full socket buffer. If ptp4l has a port in the +faulty state waiting for the link to go up and that event corresponds +to one of the dropped netlink messages, the port will be stuck in the +faulty state until the link goes down and up again. + +To prevent the port from getting stuck, request the current link status +when dispatching the EV_FAULT_DETECTED event. Also, reopen the socket to +get rid of the buffered messages when handling the fault and again when +reinitializing the port. + +Signed-off-by: Miroslav Lichvar +Reviewed-by: Jacob Keller +Signed-off-by: Joachim Wiberg +--- + port.c | 30 +++++++++++++++++++++++------- + 1 file changed, 23 insertions(+), 7 deletions(-) + +diff --git a/port.c b/port.c +index 282be66..5eea2f2 100644 +--- a/port.c ++++ b/port.c +@@ -1975,6 +1975,20 @@ static int port_cmlds_initialize(struct port *p) + return port_cmlds_renew(p, now.tv_sec); + } + ++static void port_rtnl_initialize(struct port *p) ++{ ++ /* Reopen the socket to get rid of buffered messages */ ++ if (p->fda.fd[FD_RTNL] >= 0) { ++ rtnl_close(p->fda.fd[FD_RTNL]); ++ } ++ p->fda.fd[FD_RTNL] = rtnl_open(); ++ if (p->fda.fd[FD_RTNL] >= 0) { ++ rtnl_link_query(p->fda.fd[FD_RTNL], interface_name(p->iface)); ++ } ++ ++ clock_fda_changed(p->clock); ++} ++ + void port_disable(struct port *p) + { + int i; +@@ -2088,13 +2102,8 @@ int port_initialize(struct port *p) + if (p->bmca == BMCA_NOOP) { + port_set_delay_tmo(p); + } +- if (p->fda.fd[FD_RTNL] == -1) { +- p->fda.fd[FD_RTNL] = rtnl_open(); +- } +- if (p->fda.fd[FD_RTNL] >= 0) { +- const char *ifname = interface_name(p->iface); +- rtnl_link_query(p->fda.fd[FD_RTNL], ifname); +- } ++ ++ port_rtnl_initialize(p); + } + + port_nrate_initialize(p); +@@ -3769,6 +3778,13 @@ int port_state_update(struct port *p, enum fsm_event event, int mdiff) + if (port_link_status_get(p) && clear_fault_asap(&i)) { + pr_notice("%s: clearing fault immediately", p->log_name); + next = p->state_machine(next, EV_FAULT_CLEARED, 0); ++ } else if (event == EV_FAULT_DETECTED) { ++ /* ++ * Reopen the netlink socket and refresh the link ++ * status in case the fault was triggered by a missed ++ * netlink message (ENOBUFS). ++ */ ++ port_rtnl_initialize(p); + } + } + +-- +2.43.0 + diff --git a/patches/linuxptp/0010-uds-Copy-ownership-of-server-socket-in-pmc-clients.patch b/patches/linuxptp/0010-uds-Copy-ownership-of-server-socket-in-pmc-clients.patch new file mode 100644 index 000000000..a0dcb5a58 --- /dev/null +++ b/patches/linuxptp/0010-uds-Copy-ownership-of-server-socket-in-pmc-clients.patch @@ -0,0 +1,58 @@ +From 3dece8ac8b324e702bf54875b53aee1eb49340e0 Mon Sep 17 00:00:00 2001 +From: "Miroslav Lichvar (via linuxptp-devel Mailing List)" + +Date: Thu, 31 Jul 2025 11:35:46 +0200 +Subject: [PATCH 10/13] uds: Copy ownership of server socket in pmc clients. +Organization: Wires + +ptp4l sending a response to a pmc client needs to have permissions to +write to the client's UNIX domain socket. If ptp4l runs under a non-root +user, it cannot write to sockets bound by the pmc client if it did that +as root. + +After binding the client socket, change its owner to the owner of the +server socket, so it can send the client a response. + +Signed-off-by: Miroslav Lichvar +Reviewed-by: Jacob Keller +Signed-off-by: Joachim Wiberg +--- + uds.c | 15 +++++++++++++++ + 1 file changed, 15 insertions(+) + +diff --git a/uds.c b/uds.c +index 4ddee7b..ce7e92d 100644 +--- a/uds.c ++++ b/uds.c +@@ -60,6 +60,7 @@ static int uds_open(struct transport *t, struct interface *iface, struct fdarray + const char* file_mode_cfg; + struct sockaddr_un sa; + mode_t file_mode; ++ struct stat st; + int fd, err; + + fd = socket(AF_LOCAL, SOCK_DGRAM, 0); +@@ -97,6 +98,20 @@ static int uds_open(struct transport *t, struct interface *iface, struct fdarray + uds->address.len = sizeof(sa); + + chmod(name, file_mode); ++ ++ /* ++ * In the client, copy the ownership of the server's socket if it runs ++ * under a non-root user to allow it to send a response to the client ++ * running under root. Avoid following a symlink if the socket is ++ * replaced (e.g. by compromised ptp4l process). ++ */ ++ if (uds_path[0] != '\0') { ++ if (!lstat(uds_path, &st) && (st.st_uid || st.st_gid) && ++ lchown(name, st.st_uid, st.st_gid)) { ++ pr_err("uds: failed to change socket ownership: %m"); ++ } ++ } ++ + fda->fd[FD_EVENT] = -1; + fda->fd[FD_GENERAL] = fd; + return 0; +-- +2.43.0 + diff --git a/patches/linuxptp/0011-uds-Don-t-call-chmod-on-client-socket.patch b/patches/linuxptp/0011-uds-Don-t-call-chmod-on-client-socket.patch new file mode 100644 index 000000000..8ec2ab92f --- /dev/null +++ b/patches/linuxptp/0011-uds-Don-t-call-chmod-on-client-socket.patch @@ -0,0 +1,56 @@ +From 6bfc1de21b64ac17145b119cb636d1c1b3d90a75 Mon Sep 17 00:00:00 2001 +From: "Miroslav Lichvar (via linuxptp-devel Mailing List)" + +Date: Thu, 31 Jul 2025 11:35:47 +0200 +Subject: [PATCH 11/13] uds: Don't call chmod() on client socket. +Organization: Wires + +The pmc clients should not need to modify the permissions of their +socket (following the uds_file_mode setting), they can rely on their +umask. + +Make the chmod() call on the bound socket only in the server. + +This removes a race condition between the bind() and chmod() calls that +could potentially be exploited by ptp4l running under a non-root user. +It could replace the socket with a symlink in order to make the client +running under root to change the mode of a different file. + +Signed-off-by: Miroslav Lichvar +Reviewed-by: Jacob Keller +Signed-off-by: Joachim Wiberg +--- + uds.c | 7 ++++--- + 1 file changed, 4 insertions(+), 3 deletions(-) + +diff --git a/uds.c b/uds.c +index ce7e92d..d74b7a8 100644 +--- a/uds.c ++++ b/uds.c +@@ -97,19 +97,20 @@ static int uds_open(struct transport *t, struct interface *iface, struct fdarray + uds->address.sun = sa; + uds->address.len = sizeof(sa); + +- chmod(name, file_mode); +- + /* + * In the client, copy the ownership of the server's socket if it runs + * under a non-root user to allow it to send a response to the client + * running under root. Avoid following a symlink if the socket is +- * replaced (e.g. by compromised ptp4l process). ++ * replaced (e.g. by compromised ptp4l process). The server just sets ++ * the permissions on its socket per configuration. + */ + if (uds_path[0] != '\0') { + if (!lstat(uds_path, &st) && (st.st_uid || st.st_gid) && + lchown(name, st.st_uid, st.st_gid)) { + pr_err("uds: failed to change socket ownership: %m"); + } ++ } else { ++ chmod(name, file_mode); + } + + fda->fd[FD_EVENT] = -1; +-- +2.43.0 + diff --git a/patches/linuxptp/0012-port-Allow-mixing-wildcard-identities-and-exact-iden.patch b/patches/linuxptp/0012-port-Allow-mixing-wildcard-identities-and-exact-iden.patch new file mode 100644 index 000000000..beb608ae9 --- /dev/null +++ b/patches/linuxptp/0012-port-Allow-mixing-wildcard-identities-and-exact-iden.patch @@ -0,0 +1,82 @@ +From 5a192e152f4d19ff5899960154f1e14bbd0c6bd7 Mon Sep 17 00:00:00 2001 +From: "Maxime Chevallier (via linuxptp-devel Mailing List)" + +Date: Wed, 4 Feb 2026 10:16:11 +0100 +Subject: [PATCH 12/13] port: Allow mixing wildcard identities and exact + identities in messages +Organization: Wires + +A Port Identity is made of a Clock Identity and a Port Number. Each of +these fields allow wildcards values. + +The current implementation checks if either both these fields contain an +exact value, or if both contains a wildcard. + +Make so that we check for wildcards on each field independently. To +avoid hard to read comparisons, introduce helper functions to compare +each field in PortIdentity. + +Signed-off-by: Maxime Chevallier +Signed-off-by: Joachim Wiberg +--- + port_signaling.c | 6 ++++-- + util.h | 26 ++++++++++++++++++++++++++ + 2 files changed, 30 insertions(+), 2 deletions(-) + +diff --git a/port_signaling.c b/port_signaling.c +index cf28756..b34ebe9 100644 +--- a/port_signaling.c ++++ b/port_signaling.c +@@ -151,8 +151,10 @@ int process_signaling(struct port *p, struct ptp_message *m) + } + + /* Ignore signaling messages not addressed to this port. */ +- if (!pid_eq(&m->signaling.targetPortIdentity, &p->portIdentity) && +- !pid_eq(&m->signaling.targetPortIdentity, &wildcard_pid)) { ++ if ((!pid_cid_eq(&m->signaling.targetPortIdentity, &p->portIdentity) && ++ !pid_cid_eq(&m->signaling.targetPortIdentity, &wildcard_pid)) || ++ (!pid_pn_eq(&m->signaling.targetPortIdentity, &p->portIdentity) && ++ !pid_pn_eq(&m->signaling.targetPortIdentity, &wildcard_pid))) { + return 0; + } + +diff --git a/util.h b/util.h +index b228745..7552353 100644 +--- a/util.h ++++ b/util.h +@@ -158,6 +158,32 @@ static inline int pid_eq(const struct PortIdentity *a, + return memcmp(a, b, sizeof(*a)) == 0; + } + ++/** ++ * Compare two port identities for PortIdentity.clockIdentity equality. ++ * ++ * @param a First port identity. ++ * @param b Second port identity. ++ * @return 1 if identities are equal, 0 otherwise. ++ */ ++static inline int pid_cid_eq(const struct PortIdentity *a, ++ const struct PortIdentity *b) ++{ ++ return cid_eq(&a->clockIdentity, &b->clockIdentity); ++} ++ ++/** ++ * Compare two port identities for PortIdentity.portNumber equality. ++ * ++ * @param a First port identity. ++ * @param b Second port identity. ++ * @return 1 if identities are equal, 0 otherwise. ++ */ ++static inline int pid_pn_eq(const struct PortIdentity *a, ++ const struct PortIdentity *b) ++{ ++ return a->portNumber == b->portNumber; ++} ++ + /** + * Convert a string containing a network address into binary form. + * @param type The network transport type of the address. +-- +2.43.0 + diff --git a/patches/linuxptp/0013-Add-pidfile-support-to-ptp4l-phc2sys-and-timemaster.patch b/patches/linuxptp/0013-Add-pidfile-support-to-ptp4l-phc2sys-and-timemaster.patch new file mode 100644 index 000000000..e78e284a0 --- /dev/null +++ b/patches/linuxptp/0013-Add-pidfile-support-to-ptp4l-phc2sys-and-timemaster.patch @@ -0,0 +1,386 @@ +From 4fdb58ce4e052862f0dc7432d84b19febd5256a5 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Fri, 10 Apr 2026 14:33:56 +0200 +Subject: [PATCH 13/13] Add pidfile support to ptp4l, phc2sys, and timemaster +Organization: Wires + +Add pidfile.c derived from OpenBSD via libite. The pidfile() function +creates a PID file and registers an atexit() handler to clean it up on +normal exit. + +For ptp4l and phc2sys the pidfile path is set via the 'pidfile' global +config option (or -u on the ptp4l command line). For timemaster, +which uses its own config format, the path is given with -u . + +Signed-off-by: Joachim Wiberg +--- + config.c | 1 + + makefile | 12 ++--- + phc2sys.c | 7 +++ + pidfile.c | 142 +++++++++++++++++++++++++++++++++++++++++++++++++++ + pidfile.h | 10 ++++ + ptp4l.c | 14 ++++- + timemaster.c | 19 +++++-- + 7 files changed, 194 insertions(+), 11 deletions(-) + create mode 100644 pidfile.c + create mode 100644 pidfile.h + +diff --git a/config.c b/config.c +index d0bc32c..4b46542 100644 +--- a/config.c ++++ b/config.c +@@ -341,6 +341,7 @@ struct config_item config_tab[] = { + PORT_ITEM_INT("power_profile.2011.networkTimeInaccuracy", 0xFFFFFFFF, -1, INT_MAX), + PORT_ITEM_INT("power_profile.2017.totalTimeInaccuracy", 0xFFFFFFFF, -1, INT_MAX), + PORT_ITEM_INT("power_profile.grandmasterID", 0, 0, 0xFFFF), ++ GLOB_ITEM_STR("pidfile", NULL), + GLOB_ITEM_INT("priority1", 128, 0, UINT8_MAX), + GLOB_ITEM_INT("priority2", 128, 0, UINT8_MAX), + GLOB_ITEM_STR("productDescription", ";;"), +diff --git a/makefile b/makefile +index 3c2406b..67622e1 100644 +--- a/makefile ++++ b/makefile +@@ -31,9 +31,9 @@ TS2PHC = ts2phc.o lstab.o nmea.o serial.o sock.o ts2phc_generic_pps_source.o \ + ts2phc_nmea_pps_source.o ts2phc_phc_pps_source.o ts2phc_pps_sink.o ts2phc_pps_source.o + OBJ = bmc.o clock.o clockadj.o clockcheck.o config.o designated_fsm.o \ + e2e_tc.o fault.o $(FILTERS) fsm.o hash.o interface.o monitor.o msg.o phc.o \ +- pmc_common.o port.o port_signaling.o pqueue.o print.o ptp4l.o p2p_tc.o rtnl.o \ +- $(SECURITY) $(SERVOS) sk.o stats.o tc.o $(TRANSP) telecom.o tlv.o tsproc.o \ +- unicast_client.o unicast_fsm.o unicast_service.o util.o version.o ++ pidfile.o pmc_common.o port.o port_signaling.o pqueue.o print.o ptp4l.o \ ++ p2p_tc.o rtnl.o $(SECURITY) $(SERVOS) sk.o stats.o tc.o $(TRANSP) telecom.o \ ++ tlv.o tsproc.o unicast_client.o unicast_fsm.o unicast_service.o util.o version.o + + OBJECTS = $(OBJ) hwstamp_ctl.o nsm.o phc2sys.o phc_ctl.o pmc.o pmc_agent.o \ + pmc_common.o sysoff.o timemaster.o $(TS2PHC) tz2alt.o +@@ -78,14 +78,14 @@ pmc: config.o hash.o interface.o msg.o phc.o pmc.o pmc_common.o print.o \ + $(SECURITY) sk.o tlv.o $(TRANSP) util.o version.o + + phc2sys: clockadj.o clockcheck.o config.o hash.o interface.o msg.o \ +- phc.o phc2sys.o pmc_agent.o pmc_common.o print.o $(SECURITY) $(SERVOS) \ +- sk.o stats.o sysoff.o tlv.o $(TRANSP) util.o version.o ++ phc.o phc2sys.o pidfile.o pmc_agent.o pmc_common.o print.o $(SECURITY) \ ++ $(SERVOS) sk.o stats.o sysoff.o tlv.o $(TRANSP) util.o version.o + + hwstamp_ctl: hwstamp_ctl.o version.o + + phc_ctl: phc_ctl.o phc.o sk.o util.o clockadj.o sysoff.o print.o version.o + +-timemaster: phc.o print.o rtnl.o sk.o timemaster.o util.o version.o ++timemaster: phc.o pidfile.o print.o rtnl.o sk.o timemaster.o util.o version.o + + ts2phc: config.o clockadj.o hash.o interface.o msg.o phc.o pmc_agent.o \ + pmc_common.o print.o $(SECURITY) $(SERVOS) sk.o $(TS2PHC) tlv.o transport.o \ +diff --git a/phc2sys.c b/phc2sys.c +index 5962f6c..b272adf 100644 +--- a/phc2sys.c ++++ b/phc2sys.c +@@ -56,6 +56,7 @@ + #include "sysoff.h" + #include "tlv.h" + #include "uds.h" ++#include "pidfile.h" + #include "util.h" + #include "version.h" + +@@ -1432,6 +1433,12 @@ int main(int argc, char *argv[]) + print_set_syslog(config_get_int(cfg, NULL, "use_syslog")); + print_set_level(config_get_int(cfg, NULL, "logging_level")); + ++ if (config_get_string(cfg, NULL, "pidfile") && ++ pidfile(config_get_string(cfg, NULL, "pidfile"))) { ++ fprintf(stderr, "failed to create pidfile\n"); ++ goto end; ++ } ++ + settings.free_running = config_get_int(cfg, NULL, "free_running"); + settings.servo_type = config_get_int(cfg, NULL, "clock_servo"); + if (settings.free_running || settings.servo_type == CLOCK_SERVO_NTPSHM) { +diff --git a/pidfile.c b/pidfile.c +new file mode 100644 +index 0000000..42121b0 +--- /dev/null ++++ b/pidfile.c +@@ -0,0 +1,142 @@ ++/* Updated by troglobit for libite/finit/uftpd projects 2016/07/04 */ ++/* $OpenBSD: pidfile.c,v 1.11 2015/06/03 02:24:36 millert Exp $ */ ++/* $NetBSD: pidfile.c,v 1.4 2001/02/19 22:43:42 cgd Exp $ */ ++ ++/*- ++ * Copyright (c) 1999 The NetBSD Foundation, Inc. ++ * All rights reserved. ++ * ++ * This code is derived from software contributed to The NetBSD Foundation ++ * by Jason R. Thorpe. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * 1. Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * 2. Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in the ++ * documentation and/or other materials provided with the distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS ++ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED ++ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR ++ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS ++ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR ++ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF ++ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS ++ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN ++ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ++ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE ++ * POSSIBILITY OF SUCH DAMAGE. ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "pidfile.h" ++ ++static char *pidfile_path = NULL; ++static pid_t pidfile_pid = 0; ++ ++static void pidfile_cleanup(void); ++ ++static const char *pidfile_rundir = _PATH_VARRUN; ++extern char *__progname; ++ ++/** ++ * Create or update mtime of process PID file. ++ * @param basename Program name, or NULL, may start with '/' ++ * ++ * If @p basename is NULL the implicit @a __progname variable from the ++ * C-library is used. If @p basename starts with '/' it is used as the ++ * absolute path to the PID file. ++ * ++ * @returns POSIX OK(0) on success, non-zero with errno set on error. ++ */ ++int pidfile(const char *basename) ++{ ++ int save_errno; ++ int atexit_already; ++ pid_t pid; ++ FILE *f; ++ ++ if (basename == NULL) ++ basename = __progname; ++ ++ pid = getpid(); ++ atexit_already = 0; ++ ++ if (pidfile_path != NULL) { ++ if (!access(pidfile_path, R_OK) && pid == pidfile_pid) { ++ utimensat(0, pidfile_path, NULL, 0); ++ return 0; ++ } ++ free(pidfile_path); ++ pidfile_path = NULL; ++ atexit_already = 1; ++ } ++ ++ if (basename[0] != '/') { ++ size_t len = strlen(pidfile_rundir); ++ int slash = pidfile_rundir[len > 0 ? len - 1 : 0] != '/'; ++ ++ if (asprintf(&pidfile_path, "%s%s%s.pid", ++ pidfile_rundir, slash ? "/" : "", basename) == -1) ++ return -1; ++ } else { ++ if (asprintf(&pidfile_path, "%s", basename) == -1) ++ return -1; ++ } ++ ++ if ((f = fopen(pidfile_path, "w")) == NULL) { ++ save_errno = errno; ++ free(pidfile_path); ++ pidfile_path = NULL; ++ errno = save_errno; ++ return -1; ++ } ++ ++ if (fprintf(f, "%ld\n", (long)pid) <= 0 || fflush(f) != 0) { ++ save_errno = errno; ++ (void)fclose(f); ++ (void)unlink(pidfile_path); ++ free(pidfile_path); ++ pidfile_path = NULL; ++ errno = save_errno; ++ return -1; ++ } ++ (void)fclose(f); ++ ++ if (atexit_already) ++ return 0; ++ ++ pidfile_pid = pid; ++ if (atexit(pidfile_cleanup) < 0) { ++ save_errno = errno; ++ (void)unlink(pidfile_path); ++ free(pidfile_path); ++ pidfile_path = NULL; ++ pidfile_pid = 0; ++ errno = save_errno; ++ return -1; ++ } ++ ++ return 0; ++} ++ ++static void pidfile_cleanup(void) ++{ ++ if (pidfile_path != NULL && pidfile_pid == getpid()) { ++ (void)unlink(pidfile_path); ++ free(pidfile_path); ++ pidfile_path = NULL; ++ } ++} +diff --git a/pidfile.h b/pidfile.h +new file mode 100644 +index 0000000..7b68c78 +--- /dev/null ++++ b/pidfile.h +@@ -0,0 +1,10 @@ ++/** ++ * @file pidfile.h ++ * @brief PID file support, derived from OpenBSD via libite. ++ */ ++#ifndef HAVE_PIDFILE_H ++#define HAVE_PIDFILE_H ++ ++int pidfile(const char *basename); ++ ++#endif +diff --git a/ptp4l.c b/ptp4l.c +index ac2ef96..12f5e20 100644 +--- a/ptp4l.c ++++ b/ptp4l.c +@@ -34,6 +34,7 @@ + #include "transport.h" + #include "udp6.h" + #include "uds.h" ++#include "pidfile.h" + #include "util.h" + #include "version.h" + +@@ -63,6 +64,7 @@ static void usage(char *progname) + " -l [num] set the logging level to 'num'\n" + " -m print messages to stdout\n" + " -q do not print messages to the syslog\n" ++ " -u [file] write process ID to 'file'\n" + " -v prints the software version and exits\n" + " -h prints this message and exits\n" + "\n", +@@ -90,7 +92,7 @@ int main(int argc, char *argv[]) + /* Process the command line arguments. */ + progname = strrchr(argv[0], '/'); + progname = progname ? 1+progname : argv[0]; +- while (EOF != (c = getopt_long(argc, argv, "AEP246HSLf:i:p:sl:mqvh", ++ while (EOF != (c = getopt_long(argc, argv, "AEP246HSLf:i:p:sl:mqu:vh", + opts, &index))) { + switch (c) { + case 0: +@@ -163,6 +165,10 @@ int main(int argc, char *argv[]) + case 'q': + config_set_int(cfg, "use_syslog", 0); + break; ++ case 'u': ++ if (config_set_string(cfg, "pidfile", optarg)) ++ goto out; ++ break; + case 'v': + version_show(stdout); + return 0; +@@ -188,6 +194,12 @@ int main(int argc, char *argv[]) + print_set_syslog(config_get_int(cfg, NULL, "use_syslog")); + print_set_level(config_get_int(cfg, NULL, "logging_level")); + ++ if (config_get_string(cfg, NULL, "pidfile") && ++ pidfile(config_get_string(cfg, NULL, "pidfile"))) { ++ fprintf(stderr, "failed to create pidfile\n"); ++ goto out; ++ } ++ + assume_two_step = config_get_int(cfg, NULL, "assume_two_step"); + sk_check_fupsync = config_get_int(cfg, NULL, "check_fup_sync"); + sk_tx_timeout = config_get_int(cfg, NULL, "tx_timestamp_timeout"); +diff --git a/timemaster.c b/timemaster.c +index b367b2f..873083f 100644 +--- a/timemaster.c ++++ b/timemaster.c +@@ -38,6 +38,7 @@ + #include + #include + ++#include "pidfile.h" + #include "print.h" + #include "rtnl.h" + #include "sk.h" +@@ -1531,6 +1532,7 @@ static void usage(char *progname) + "\nusage: %s [options] -f file\n\n" + " -f file specify path to configuration file\n" + " -n only print generated files and commands\n" ++ " -u file write process ID to 'file'\n" + " -l level set logging level (6)\n" + " -m print messages to stdout\n" + " -q do not print messages to syslog\n" +@@ -1543,7 +1545,7 @@ int main(int argc, char **argv) + { + struct timemaster_config *config; + struct script *script; +- char *progname, *config_path = NULL; ++ char *progname, *config_path = NULL, *pid_file = NULL; + int c, ret = 0, log_stdout = 0, log_syslog = 1, dry_run = 0; + + progname = strrchr(argv[0], '/'); +@@ -1553,7 +1555,7 @@ int main(int argc, char **argv) + print_set_verbose(1); + print_set_syslog(0); + +- while (EOF != (c = getopt(argc, argv, "f:nl:mqvh"))) { ++ while (EOF != (c = getopt(argc, argv, "f:nu:l:mqvh"))) { + switch (c) { + case 'f': + config_path = optarg; +@@ -1561,6 +1563,9 @@ int main(int argc, char **argv) + case 'n': + dry_run = 1; + break; ++ case 'u': ++ pid_file = optarg; ++ break; + case 'l': + print_set_level(atoi(optarg)); + break; +@@ -1599,10 +1604,16 @@ int main(int argc, char **argv) + print_set_verbose(log_stdout); + print_set_syslog(log_syslog); + +- if (dry_run) ++ if (dry_run) { + script_print(script); +- else ++ } else { ++ if (pid_file && pidfile(pid_file)) { ++ pr_err("failed to create pidfile %s: %m", pid_file); ++ script_destroy(script); ++ return 1; ++ } + ret = script_run(script); ++ } + + script_destroy(script); + +-- +2.43.0 + diff --git a/src/bin/show/__init__.py b/src/bin/show/__init__.py index c48c365b7..266e642b8 100755 --- a/src/bin/show/__init__.py +++ b/src/bin/show/__init__.py @@ -711,6 +711,23 @@ def keystore(args: List[str]) -> None: print("Usage: show keystore [symmetric | asymmetric ]") +def ptp(args: List[str]) -> None: + data = get_json("/ieee1588-ptp-tt:ptp") + if not data: + print("PTP: no instances running.") + return + + if RAW_OUTPUT: + print(json.dumps(data, indent=2)) + return + + # Optional: filter to a specific instance-index + if args and args[0].isdigit(): + cli_pretty(data, "show-ptp", args[0]) + else: + cli_pretty(data, "show-ptp") + + def execute_command(command: str, args: List[str]): command_mapping = { 'bfd': bfd, @@ -725,6 +742,7 @@ def execute_command(command: str, args: List[str]): 'nacm': nacm, 'ntp': ntp, 'ospf': ospf, + 'ptp': ptp, 'rip': rip, 'routes': routes, 'services': services, diff --git a/src/confd/src/Makefile.am b/src/confd/src/Makefile.am index f457f1ad5..447117994 100644 --- a/src/confd/src/Makefile.am +++ b/src/confd/src/Makefile.am @@ -49,6 +49,7 @@ confd_plugin_la_SOURCES = \ keystore.c \ system.c \ ntp.c \ + ptp.c \ syslog.c \ factory-default.c \ routing.c \ diff --git a/src/confd/src/core.c b/src/confd/src/core.c index 5e4fddaa2..dfd0261d4 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -511,6 +511,10 @@ static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *mod if ((rc = ntp_change(session, config, diff, event, confd))) goto free_diff; + /* ieee1588-ptp-tt */ + if ((rc = ptp_change(session, config, diff, event, confd))) + goto free_diff; + /* infix-services */ if ((rc = services_change(session, config, diff, event, confd))) goto free_diff; @@ -706,6 +710,11 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv) ERROR("Failed to subscribe to infix-meta"); goto err; } + rc = subscribe_model("ieee1588-ptp-tt", &confd, 0); + if (rc) { + ERROR("Failed to subscribe to ieee1588-ptp-tt"); + goto err; + } rc = system_rpc_init(&confd); if (rc) diff --git a/src/confd/src/core.h b/src/confd/src/core.h index f25d91bd4..b56c8bf32 100644 --- a/src/confd/src/core.h +++ b/src/confd/src/core.h @@ -271,4 +271,7 @@ int ntp_cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module, const char *path, sr_event_t event, unsigned request_id, void *priv); int ntp_candidate_init(struct confd *confd); +/* ptp.c */ +int ptp_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd); + #endif /* CONFD_CORE_H_ */ diff --git a/src/confd/src/ptp.c b/src/confd/src/ptp.c new file mode 100644 index 000000000..a8f8d84c7 --- /dev/null +++ b/src/confd/src/ptp.c @@ -0,0 +1,465 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#include +#include +#include + +#include +#include +#include + +#include "core.h" + +#define XPATH_PTP_ "/ieee1588-ptp-tt:ptp" +#define PTP_CONF_DIR "/etc/linuxptp" + +/* + * Map instance-type string to ptp4l clockType keyword. + * Returns NULL for oc/bc (no explicit clockType needed in [global]). + */ +static const char *instance_type_to_clock_type(const char *type) +{ + if (!type) + return NULL; + if (!strcmp(type, "p2p-tc")) + return "P2P_TC"; + if (!strcmp(type, "e2e-tc")) + return "E2E_TC"; + return NULL; +} + +/* + * Emit all protocol-mandatory [global] settings for the chosen profile. + * Returns true when the profile is ieee802-dot1as (802.1AS/gPTP), which + * the caller uses to suppress per-port delay_mechanism output — identical + * to the guard already used for Transparent Clock instances. + * + * ieee802-dot1as sets the full gPTP option set as required by the standard: + * transportSpecific, network_transport, delay_mechanism, multicast MACs, + * gmCapable, follow_up_info, assume_two_step, path_trace_enabled, + * and the tighter neighborPropDelayThresh. + */ +static bool emit_profile_globals(FILE *fp, const char *profile) +{ + bool dot1as = profile && !strcmp(profile, "ieee802-dot1as"); + + if (dot1as) { + fprintf(fp, "transportSpecific 1\n"); + fprintf(fp, "network_transport L2\n"); + fprintf(fp, "delay_mechanism P2P\n"); + fprintf(fp, "ptp_dst_mac 01:80:C2:00:00:0E\n"); + fprintf(fp, "p2p_dst_mac 01:80:C2:00:00:0E\n"); + fprintf(fp, "gmCapable 1\n"); + 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"); + } else { + fprintf(fp, "transportSpecific 0\n"); + } + return dot1as; +} + +/* + * Return true if ifname has hardware TX timestamping capability according + * to the probed data in system.json (confd->root). If the interface is + * absent from system.json (virtual interface, QEMU tap, etc.) or the + * "hardware-transmit" capability string is missing, returns false. + */ +static bool iface_has_hw_timestamp(json_t *root, const char *ifname) +{ + json_t *caps, *list, *entry; + size_t i; + + if (!root || !ifname) + return false; + + caps = json_object_get(json_object_get( + json_object_get(root, "interfaces"), + ifname), "ptp-capabilities"); + if (!caps) + return false; + + list = json_object_get(caps, "capabilities"); + if (!json_is_array(list)) + return false; + + json_array_foreach(list, i, entry) { + const char *s = json_string_value(entry); + if (s && !strcmp(s, "hardware-transmit")) + return true; + } + return false; +} + +/* + * Scan all ports of inst and determine whether to use hardware or software + * timestamping. Emits a syslog WARNING when falling back to software due + * to a mixed or software-only set of port interfaces. + */ +static const char *instance_time_stamping(struct lyd_node *inst, json_t *root) +{ + struct lyd_node *port; + bool any_sw = false; + + LYX_LIST_FOR_EACH(lyd_child(lydx_get_child(inst, "ports")), port, "port") { + const char *iface = lydx_get_cattr(port, "underlying-interface"); + + if (!iface) + continue; + if (iface_has_hw_timestamp(root, iface)) + continue; + + any_sw = true; + break; + } + + if (any_sw) { + WARN("PTP instance has software-only timestamping port(s), " + "falling back to time_stamping software"); + + return "software"; + } + + return "hardware"; +} + +/* + * Write ptp4l config for one PTP instance. + * Config file: /etc/linuxptp/ptp4l-.conf+ (staging) + * + * ptp4l key config layout: + * [global] — instance-wide settings + * [eth0] — per-port interface sections, sorted by port-index + */ +static int write_instance_conf(struct lyd_node *inst, json_t *root) +{ + const char *instance_type, *clock_type, *profile; + struct lyd_node *default_ds, *port, *port_ds, *servo; + bool tc, bc, dot1as; + char path[256]; + const char *v; + uint16_t idx; + FILE *fp; + + v = lydx_get_cattr(inst, "instance-index"); + if (!v) + return SR_ERR_INVAL_ARG; + idx = (uint16_t)atoi(v); + + snprintf(path, sizeof(path), PTP_CONF_DIR "/ptp4l-%u.conf+", idx); + fp = fopen(path, "w"); + if (!fp) { + ERRNO("Failed creating %s", path); + return SR_ERR_SYS; + } + + fprintf(fp, "# Generated by confd — do not edit\n\n"); + fprintf(fp, "[global]\n"); + + 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"); + + clock_type = instance_type_to_clock_type(instance_type); + tc = (clock_type != NULL); + bc = instance_type && !strcmp(instance_type, "bc"); + + /* Unique UDS socket per instance — required for pmc with multiple instances */ + 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)); + + /* Profile — sets transportSpecific and all protocol-mandatory options */ + dot1as = emit_profile_globals(fp, profile); + + /* domainNumber */ + v = lydx_get_cattr(default_ds, "domain-number"); + if (v) + fprintf(fp, "domainNumber %s\n", v); + + /* Transparent Clock clock_type */ + if (tc) + fprintf(fp, "clock_type %s\n", clock_type); + + /* + * Multi-port instances (BC and TC) may span ports on different PHC + * devices when the ports belong to different switch chips (e.g. a + * three-chip board where each mv88e6xxx chip owns its own /dev/ptpN). + * boundary_clock_jbod silences ptp4l's startup PHC-mismatch check and + * lets each port use its own PHC. It is a no-op when all ports share + * the same PHC (single-chip board or software timestamping). + * + * NOTE: for BC on multi-chip hardware, ptp4l only disciplines the PHC + * of the active slave port; the other chips' PHCs will drift unless + * phc2sys(8) -a is also run. That is a separate service (TODO). + */ + if (tc || bc) + fprintf(fp, "boundary_clock_jbod 1\n"); + + /* priority1 / priority2 (not applicable for TC, but harmless) */ + v = lydx_get_cattr(default_ds, "priority1"); + if (v) + fprintf(fp, "priority1 %s\n", v); + v = lydx_get_cattr(default_ds, "priority2"); + if (v) + fprintf(fp, "priority2 %s\n", v); + + /* clientOnly (OC only) — inclusive replacement for slaveOnly in ptp4l 4.x */ + v = lydx_get_cattr(default_ds, "time-receiver-only"); + if (v && !strcmp(v, "true")) + fprintf(fp, "clientOnly 1\n"); + + /* maxStepsRemoved */ + v = lydx_get_cattr(default_ds, "max-steps-removed"); + if (v) + fprintf(fp, "maxStepsRemoved %s\n", v); + + /* servo: step_threshold (0.0 = slew-only, never step) */ + servo = lydx_get_child(inst, "servo"); + if (servo) { + v = lydx_get_cattr(servo, "step-threshold"); + if (v) + fprintf(fp, "step_threshold %s\n", v); + } + + /* + * Transparent Clocks set delay_mechanism globally; ptp4l ignores + * per-port delay_mechanism for TCs. 802.1AS mandates P2P globally + * (already emitted by emit_profile_globals). + */ + if (tc) { + if (!strcmp(clock_type, "P2P_TC")) + fprintf(fp, "delay_mechanism P2P\n"); + else + fprintf(fp, "delay_mechanism E2E\n"); + } + + fprintf(fp, "\n"); + + /* Per-port [interface] sections, sorted by port-index */ + LYX_LIST_FOR_EACH(lyd_child(lydx_get_child(inst, "ports")), port, "port") { + const char *iface; + + iface = lydx_get_cattr(port, "underlying-interface"); + if (!iface) + continue; + + port_ds = lydx_get_child(port, "port-ds"); + if (!port_ds) + continue; + + if (!lydx_is_enabled(port_ds, "port-enable")) + continue; + + fprintf(fp, "[%s]\n", iface); + + v = lydx_get_cattr(port_ds, "log-announce-interval"); + if (v) + fprintf(fp, "logAnnounceInterval %s\n", v); + + v = lydx_get_cattr(port_ds, "announce-receipt-timeout"); + if (v) + fprintf(fp, "announceReceiptTimeout %s\n", v); + + v = lydx_get_cattr(port_ds, "log-sync-interval"); + if (v) + fprintf(fp, "logSyncInterval %s\n", v); + + v = lydx_get_cattr(port_ds, "log-min-delay-req-interval"); + if (v) + fprintf(fp, "logMinDelayReqInterval %s\n", v); + + v = lydx_get_cattr(port_ds, "log-min-pdelay-req-interval"); + if (v) + fprintf(fp, "logMinPdelayReqInterval %s\n", v); + + /* + * delay_mechanism per port — only for OC/BC on ieee1588 profile. + * TC and 802.1AS both set it globally; ptp4l ignores per-port + * overrides in those cases. + */ + if (!tc && !dot1as) { + const char *dm = lydx_get_cattr(port_ds, "delay-mechanism"); + + if (dm) { + if (!strcmp(dm, "p2p")) + fprintf(fp, "delay_mechanism P2P\n"); + else if (!strcmp(dm, "e2e")) + fprintf(fp, "delay_mechanism E2E\n"); + } + } + + v = lydx_get_cattr(port_ds, "delay-asymmetry"); + if (v && strcmp(v, "0")) + fprintf(fp, "delayAsymmetry %s\n", v); + + if (lydx_is_enabled(port_ds, "time-transmitter-only")) + fprintf(fp, "masterOnly 1\n"); + + fprintf(fp, "\n"); + } + + fclose(fp); + return SR_ERR_OK; +} + +/* + * Remove staging config for one instance. + */ +static void remove_staging(uint16_t idx) +{ + char path[256]; + + snprintf(path, sizeof(path), PTP_CONF_DIR "/ptp4l-%u.conf+", idx); + (void)remove(path); +} + +/* + * Activate one instance: rename staging → live, enable finit service. + */ +static int activate_instance(uint16_t idx) +{ + char staging[256], live[256]; + + snprintf(staging, sizeof(staging), PTP_CONF_DIR "/ptp4l-%u.conf+", idx); + snprintf(live, sizeof(live), PTP_CONF_DIR "/ptp4l-%u.conf", idx); + + if (!fexist(staging)) { + (void)remove(live); + return SR_ERR_OK; + } + + if (rename(staging, live)) { + ERRNO("Failed renaming %s → %s", staging, live); + return SR_ERR_SYS; + } + + finit_enablef("ptp4l@%u", idx); + return finit_reloadf("ptp4l@%u", idx); +} + +/* + * Deactivate (disable) one instance and remove its live config. + */ +static void deactivate_instance(uint16_t idx) +{ + char live[256]; + + finit_disablef("ptp4l@%u", idx); + + snprintf(live, sizeof(live), PTP_CONF_DIR "/ptp4l-%u.conf", idx); + (void)remove(live); +} + +/* + * Disable any ptp4l@ services in finit enabled/ whose index is not in the + * currently configured set. Called from SR_EV_DONE after enabling active + * instances, to clean up stale services from a previous config. + */ +static void cleanup_stale_instances(struct lyd_node *config) +{ + const struct dirent *ent; + struct lyd_node *inst; + int idx; + DIR *d; + + d = opendir(FINIT_RCSD "/enabled"); + if (!d) + return; + + while ((ent = readdir(d))) { + bool found = false; + + if (sscanf(ent->d_name, "ptp4l@%d.conf", &idx) != 1) + continue; + + /* Is this index still configured? */ + LYX_LIST_FOR_EACH(lydx_get_descendant(config, "ptp", "instances", "instance", NULL), + inst, "instance") { + const char *v = lydx_get_cattr(inst, "instance-index"); + if (v && atoi(v) == idx) { + found = true; + break; + } + } + + if (!found) + deactivate_instance((uint16_t)idx); + } + + closedir(d); +} + +static int change(sr_session_ctx_t *session, struct lyd_node *config, + struct lyd_node *diff, sr_event_t event, struct confd *confd) +{ + struct lyd_node *instances, *inst; + int rc = SR_ERR_OK; + + if (diff && !lydx_get_xpathf(diff, XPATH_PTP_)) + return SR_ERR_OK; + + switch (event) { + case SR_EV_ENABLED: + case SR_EV_CHANGE: + break; + + case SR_EV_ABORT: + /* Remove any staging files */ + instances = lydx_get_descendant(config, "ptp", "instances", "instance", NULL); + LYX_LIST_FOR_EACH(instances, inst, "instance") { + const char *v = lydx_get_cattr(inst, "instance-index"); + if (v) + remove_staging((uint16_t)atoi(v)); + } + return SR_ERR_OK; + + case SR_EV_DONE: + /* Activate all configured instances */ + instances = lydx_get_descendant(config, "ptp", "instances", "instance", NULL); + LYX_LIST_FOR_EACH(instances, inst, "instance") { + const char *v = lydx_get_cattr(inst, "instance-index"); + if (!v) + continue; + if ((rc = activate_instance((uint16_t)atoi(v)))) + return rc; + } + + /* Disable stale services not in current config */ + cleanup_stale_instances(config); + + if (!instances) + return SR_ERR_OK; + + return SR_ERR_OK; + + default: + return SR_ERR_OK; + } + + /* SR_EV_ENABLED / SR_EV_CHANGE — generate staging configs */ + instances = lydx_get_descendant(config, "ptp", "instances", "instance", NULL); + if (!instances) + return SR_ERR_OK; + + if (mkdir(PTP_CONF_DIR, 0755) && errno != EEXIST) { + ERRNO("Failed creating " PTP_CONF_DIR); + return SR_ERR_SYS; + } + + LYX_LIST_FOR_EACH(instances, inst, "instance") { + rc = write_instance_conf(inst, confd->root); + if (rc) + return rc; + } + + return SR_ERR_OK; +} + +int ptp_change(sr_session_ctx_t *session, struct lyd_node *config, + struct lyd_node *diff, sr_event_t event, struct confd *confd) +{ + return change(session, config, diff, event, confd); +} diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index 27bc3fe9b..83bea27e4 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -47,10 +47,13 @@ MODULES=( "ieee802-ethernet-interface@2019-06-21.yang" "infix-ethernet-interface@2024-02-27.yang" "infix-factory-default@2023-06-28.yang" - "infix-interfaces@2025-11-06.yang -e vlan-filtering" + "infix-interfaces@2026-04-09.yang -e vlan-filtering" "ietf-crypto-types -e cleartext-symmetric-keys" "infix-crypto-types@2026-02-14.yang" "ietf-keystore -e symmetric-keys" "infix-ntp@2026-03-09.yang" "infix-keystore@2025-12-17.yang" + "ieee1588-ptp-tt@2023-08-14.yang -e timestamp-correction" + "ieee802-dot1as-gptp@2025-12-10.yang" + "infix-ptp@2026-04-07.yang" ) diff --git a/src/confd/yang/confd/ieee1588-ptp-tt@2023-08-14.yang b/src/confd/yang/confd/ieee1588-ptp-tt@2023-08-14.yang new file mode 100644 index 000000000..5ccc5ab63 --- /dev/null +++ b/src/confd/yang/confd/ieee1588-ptp-tt@2023-08-14.yang @@ -0,0 +1,4326 @@ +module ieee1588-ptp-tt { + yang-version 1.1; + namespace urn:ieee:std:1588:yang:ieee1588-ptp-tt; + prefix "ptp-tt"; + + import ietf-yang-types { + prefix yang; + } + import ietf-interfaces { + prefix if; + } + + organization "IEEE 1588 Working Group"; + contact + "Web: https://sagroups.ieee.org/1588/ + E-mail: 1588officers@listserv.ieee.org + + Postal: C/O IEEE 1588 Working Group Chair + IEEE Standards Association + 445 Hoes Lane + Piscataway, NJ 08854 + USA"; + description + "This YANG module defines a data model for the configuration + and state of IEEE Std 1588 clocks. IEEE Std 1588 specifies the + Precision Time Protocol (PTP). + + The nodes in this YANG module are designed for compatibility + with ietf-ptp.yang, the YANG data model for IEEE Std 1588-2008, + as specified in IETF RFC 8575. + + NOTE regarding default value: + PTP's concept of 'initialization value' is analogous to YANG's + concept of a 'default value'. According to 8.1.3.4 of + IEEE Std 1588-2019, the initialization value for configuration + is specified in IEEE Std 1588, but that value can be overridden + by a PTP Profile specification, or by the product that + implements PTP. This makes it challenging to repeat the + specification of initialization value using a YANG 'default' + statement, because there is no straightforward mechanism for + a PTP Profile's (or product's) YANG module to import this + module and override its YANG default. Since a YANG management + client can read the default value from the operational + datastore, there is no need to re-specify the default in YANG. + The implementer of PTP refers to the relevant PTP + specifications for the default (not YANG modules). + Therefore, this YANG module avoids use of the YANG 'default' + statement. + + NOTE regarding IEEE Std 1588 classification: + 8.1.2 of IEEE Std 1588-2019 specifies a classification of + each data set member, which corresponds to a leaf in YANG. + The relationship between 1588 classification and + YANG 'config' (i.e., whether the leaf is read-write) is: + - 1588 static: The leaf is 'config false' (read-only). + - 1588 configurable: The leaf is 'config true', which is + the default value for a YANG leaf. + - 1588 dynamic: A judgement is made on a member-by-member + basis. If the member corresponds to the first item of + 8.1.2.1.2 of IEEE Std 1588-2019 (i.e., value from protocol + only, such as log of protocol behavior), the YANG leaf + is 'config false'. Otherwise, the member's value can be + provided by an entity outside PTP (e.g., NETCONF or + RESTCONF client), and therefore the YANG leaf is + 'config true'. + + NOTE regarding terminology (two YANG modules): + To accommodate the need by some organizations to use the + original terminology specified by IEEE Std 1588, and the + need by some other organizations to use the alternative + terminology specified in 4.4 of IEEE Std 1588g-2022, + two YANG modules are provided by IEEE Std 1588e (MIB and + YANG Data Models). For a detailed explanation, see 15.4.2.11 + of IEEE Std 1588e. + This module uses the alternative terminology specified in + 4.4 of IEEE Std 1588g-2022 (timeTransmitter/timeReceiver)."; + + revision 2023-08-14 { + description + "Initial revision."; + reference + "IEEE Std 1588e-2024, IEEE Standard for a Precision Clock + Synchronization Protocol for Networked Measurement and + Control Systems - MIB and YANG Data Models."; + } + + feature fault-log { + description + "Logging of faults detected in the PTP Instance."; + reference + "8.2.6 of IEEE Std 1588-2019"; + } + + feature unicast-negotiation { + description + "Unicast negotiation conducted through use of TLVs."; + reference + "16.1 of IEEE Std 1588-2019"; + } + + feature path-trace { + description + "Use of the PATH_TRACE TLV for tracing the route of + a PTP Announce message through the PTP Network."; + reference + "16.2 of IEEE Std 1588-2019"; + } + + feature alternate-timescale { + description + "The transmission of an ALTERNATE_TIME_OFFSET_INDICATOR TLV + entity from the Grandmaster PTP Instance may indicate the + offset of an alternate timescale from the timescale in + use in the domain."; + reference + "16.3 of IEEE Std 1588-2019"; + } + + feature holdover-upgrade { + description + "A holdover-upgradable PTP Instance can potentially + become the Grandmaster PTP Instance in the event the + previous Grandmaster PTP Instance is disconnected + or its characteristics degrade."; + reference + "16.4 of IEEE Std 1588-2019"; + } + + feature cmlds { + description + "The Common Mean Link Delay Service (CMLDS) is an optional + service that enables any PTP Port that would normally obtain + the value of a link's and + using the peer-to-peer method to instead obtain these + values from this optional service. The CMLDS service is + available to all PTP Instances communicating with a specific + transport mechanism, over the physical link between two PTP + Nodes."; + reference + "16.6 of IEEE Std 1588-2019"; + } + + feature timestamp-correction { + description + "Correction of timestamps using configurable management data."; + reference + "16.7 of IEEE Std 1588-2019"; + } + + feature asymmetry-correction { + description + "Calculation of the on a Direct PTP Link + between two PTP Instances connected using an applicable + bidirectional medium."; + reference + "16.8 of IEEE Std 1588-2019"; + } + + feature time-receiver-monitoring { + description + "Mechanism for monitoring timing information in a PTP Port + in the timereceiver state. The time-receiver-monitoring feature + specifies TLVs that the TimeReceiver PTP Instance transmits + with this information, typically in a Signaling message."; + reference + "16.11 of IEEE Std 1588-2019"; + } + + feature enhanced-metrics { + description + "Mechanism for propagating estimates of various + inaccuracy components affecting the overall expected + PTP Instance Time accuracy. The metrics will be updated + and available for utilization at the various points along + the PTP timing chain: from the Grandmaster Instance, up to + a leaf PTP Instance in the synchronization tree. Each + PTP Instance along the timing path updates the + relevant metrics based on its contribution to the expected + degradation in PTP Instance Time accuracy due to various + induced timing error components."; + reference + "16.12 of IEEE Std 1588-2019"; + } + + feature grandmaster-cluster { + description + "Mechanism for faster selection of the Grandmaster PTP Instance + from the set of PTP Instances for which this option is both + implemented and enabled."; + reference + "17.2 of IEEE Std 1588-2019"; + } + + feature alternate-time-transmitter { + description + "Mechanism for PTP Ports on a PTP Communication Path that + are not currently the time-transmitter port of that + PTP Communicatio Path to exchange PTP timing information with + other PTP Ports on the same PTP Communication Path, and for + each of the other PTP Ports to acquire knowledge of the + characteristics of the transmission path between itself and + each alternate timeTransmitter PTP Port."; + reference + "17.3 of IEEE Std 1588-2019"; + } + + feature unicast-discovery { + description + "Mechanism for PTP to be used over a network that does not + provide multicast. A PTP Instance is configured with the + addresses of PTP Ports of other PTP Instances with which + it should attempt to establish unicast communication. + The PTP Instance may request that these PTP Ports transmit + unicast Announce, Sync, and Delay_Resp messages to it."; + reference + "17.4 of IEEE Std 1588-2019"; + } + + feature acceptable-time-transmitter { + description + "Mechanism that allows PTP Ports in the time-receiver state + to be configured to refuse to synchronize to PTP Instances not + on the acceptable timeTransmitter list."; + reference + "17.5 of IEEE Std 1588-2019"; + } + + feature external-port-config { + description + "External port configuration allows an external entity + (such as YANG-based remote management) to disable the + IEEE Std 1588 state machines that control each port's + state, including the BTCA. Each port's state is + then configured by the external entity."; + reference + "17.6 of IEEE Std 1588-2019"; + } + + feature performance-monitoring { + description + "Collection of performance monitoring logs that can be + read using management."; + reference + "Annex J of IEEE Std 1588-2019"; + } + + feature l1-sync { + description + "Layer 1-based synchronization performance + enhancement."; + reference + "Annex L of IEEE Std 1588-2019"; + } + + identity network-protocol { + description + "Enumeration for the protocol used by a PTP Instance to + transport PTP messages. + YANG identity is used so that a PTP Profile's YANG augment + can assign values, using numeric range F000 to FFFD hex."; + reference + "7.4.1 of IEEE Std 1588-2019"; + } + identity udp-ipv4 { + base network-protocol; + description + "UDP on IPv4. Numeric value is 0001 hex."; + } + identity udp-ipv6 { + base network-protocol; + description + "UDP on IPv6. Numeric value is 0002 hex."; + } + identity ieee802-3 { + base network-protocol; + description + "IEEE Std 802.3 (Ethernet). Numeric value is 0003 hex."; + } + identity devicenet { + base network-protocol; + description + "DeviceNet. Numeric value is 0004 hex."; + } + identity controlnet { + base network-protocol; + description + "ControlNet. Numeric value is 0005 hex."; + } + identity profinet { + base network-protocol; + description + "PROFINET. Numeric value is 0006 hex."; + } + identity otn { + base network-protocol; + description + "Optical Transport Network (OTN). Numeric value + is 0007 hex."; + } + identity unknown { + base network-protocol; + description + "Unknown. Numeric value is FFFE hex."; + } + + identity clock-class { + description + "Enumeration that denotes the traceability, synchronization + state and expected performance of the time or frequency + distributed by the Grandmaster PTP Instance. + IEEE Std 1588 does not specify a name for each clock-class, + but the names below are intended to be as intuitive as possible. + YANG identity is used so that a PTP Profile's YANG augment + can assign values using a numeric range designated for use by + alternate PTP Profiles."; + reference + "7.6.2.5 of IEEE Std 1588-2019"; + } + identity cc-primary-sync { + base clock-class; + description + "A PTP Instance that is synchronized to a primary + reference time source. The timescale distributed shall be PTP. + Numeric value is 6 decimal."; + } + identity cc-primary-sync-lost { + base clock-class; + description + "A PTP Instance that has previously been designated + as clockClass 6, but that has lost the ability to + synchronize to a primary reference time source and is in + holdover mode and within holdover specifications. Or a PTP + Instance designated with clockClass 7 based on the Holdover + Upgrade option. The timescale distributed shall be PTP. + Numeric value is 7 decimal."; + } + identity cc-application-specific-sync { + base clock-class; + description + "A PTP Instance that is synchronized to an + application-specific source of time. The timescale + distributed shall be ARB. + Numeric value is 13 decimal."; + } + identity cc-application-specific-sync-lost { + base clock-class; + description + "A PTP Instance that has previously been designated as + clockClass 13, but that has lost the ability to synchronize + to an application-specific source of time and is in + holdover mode and within holdover specifications. Or a PTP + Instance designated with clockClass 14 based on the Holdover + Upgrade option. The timescale distributed shall be ARB. + Numeric value is 14 decimal."; + } + identity cc-primary-sync-alternative-a { + base clock-class; + description + "Degradation alternative A for a PTP Instance of + clockClass 7 that is not within holdover specification + or that is based on the specifications of the Holdover + Upgrade option. + Numeric value is 52 decimal."; + } + identity cc-application-specific-alternative-a { + base clock-class; + description + "Degradation alternative A for a PTP Instance of + clockClass 14 that is not within holdover specification or + that is based on the specifications of the Holdover Upgrade + option. + Numeric value is 58 decimal."; + } + identity cc-primary-sync-alternative-b { + base clock-class; + description + "Degradation alternative B for a PTP Instance of + clockClass 7 that is not within holdover specification + or that is based on the specifications of the Holdover + Upgrade option. + Numeric value is 187 decimal."; + } + identity cc-application-specific-alternative-b { + base clock-class; + description + "Degradation alternative B for a PTP Instance of + clockClass 14 that is not within holdover specification or + that is based on the specifications of the Holdover Upgrade + option. + Numeric value is 193 decimal."; + } + identity cc-default { + base clock-class; + description + "Default clockClass, used if none of the other + clockClass definitions apply. + Numeric value is 248 decimal."; + } + identity cc-time-receiver-only { + base clock-class; + description + "A PTP Instance that is timeReceiver only. + Numeric value is 255 decimal."; + } + + identity clock-accuracy { + description + "Enumeration that indicates the expected accuracy of a + PTP Instance when it is the Grandmaster PTP Instance, + or in the event it becomes the Grandmaster PTP Instance. + The value shall be conservatively estimated by the PTP + Instance to a precision consistent with the value of the + selected clock-accuracy and of the next lower enumerated + value, for example, for clockAccuracy = 23 hex, between + 250 ns and 1000 ns. + IEEE Std 1588 does not specify a name for each clock-accuracy, + but the names below are intended to be as intuitive as possible. + YANG identity is used so that a PTP Profile's YANG augment + can assign values, using numeric range 80 to FD hex."; + reference + "7.6.2.6 of IEEE Std 1588-2019"; + } + identity ca-time-accurate-to-1000-fs { + base clock-accuracy; + description + "The time is accurate to within 1 ps (1000 fs). + Numeric value is 17 hex."; + } + identity ca-time-accurate-to-2500-fs { + base clock-accuracy; + description + "The time is accurate to within 2.5 ps (2500 fs). + Numeric value is 18 hex."; + } + identity ca-time-accurate-to-10-ps { + base clock-accuracy; + description + "The time is accurate to within 10 ps. + Numeric value is 19 hex."; + } + identity ca-time-accurate-to-25ps { + base clock-accuracy; + description + "The time is accurate to within 25 ps. + Numeric value is 1A hex."; + } + identity ca-time-accurate-to-100-ps { + base clock-accuracy; + description + "The time is accurate to within 100 ps. + Numeric value is 1B hex."; + } + identity ca-time-accurate-to-250-ps { + base clock-accuracy; + description + "The time is accurate to within 250 ps. + Numeric value is 1C hex."; + } + identity ca-time-accurate-to-1000-ps { + base clock-accuracy; + description + "The time is accurate to within 1ns (1000 ps). + Numeric value is 1D hex."; + } + identity ca-time-accurate-to-2500-ps { + base clock-accuracy; + description + "The time is accurate to within 2.5 ns (2500 ps). + Numeric value is 1E hex."; + } + identity ca-time-accurate-to-10-ns { + base clock-accuracy; + description + "The time is accurate to within 10 ns. + Numeric value is 1F hex."; + } + identity ca-time-accurate-to-25-ns { + base clock-accuracy; + description + "The time is accurate to within 25 ns. + Numeric value is 20 hex."; + } + identity ca-time-accurate-to-100-ns { + base clock-accuracy; + description + "The time is accurate to within 100 ns. + Numeric value is 21 hex."; + } + identity ca-time-accurate-to-250-ns { + base clock-accuracy; + description + "The time is accurate to within 250 ns. + Numeric value is 22 hex."; + } + identity ca-time-accurate-to-1000-ns { + base clock-accuracy; + description + "The time is accurate to within 1 us (1000 ns). + Numeric value is 23 hex."; + } + identity ca-time-accurate-to-2500-ns { + base clock-accuracy; + description + "The time is accurate to within 2.5 us (2500 ns). + Numeric value is 24 hex."; + } + identity ca-time-accurate-to-10-us { + base clock-accuracy; + description + "The time is accurate to within 10 us. + Numeric value is 25 hex."; + } + identity ca-time-accurate-to-25-us { + base clock-accuracy; + description + "The time is accurate to within 25 us. + Numeric value is 26 hex."; + } + identity ca-time-accurate-to-100-us { + base clock-accuracy; + description + "The time is accurate to within 100 us. + Numeric value is 27 hex."; + } + identity ca-time-accurate-to-250-us { + base clock-accuracy; + description + "The time is accurate to within 250 us. + Numeric value is 28 hex."; + } + identity ca-time-accurate-to-1000-us { + base clock-accuracy; + description + "The time is accurate to within 1 ms (1000 us). + Numeric value is 29 hex."; + } + identity ca-time-accurate-to-2500-us { + base clock-accuracy; + description + "The time is accurate to within 2.5 ms (2500 us). + Numeric value is 2A hex."; + } + identity ca-time-accurate-to-10-ms { + base clock-accuracy; + description + "The time is accurate to within 10 ms. + Numeric value is 2B hex."; + } + identity ca-time-accurate-to-25-ms { + base clock-accuracy; + description + "The time is accurate to within 25 ms. + Numeric value is 2Chex."; + } + identity ca-time-accurate-to-100-ms { + base clock-accuracy; + description + "The time is accurate to within 100 ms. + Numeric value is 2D hex."; + } + identity ca-time-accurate-to-250-ms { + base clock-accuracy; + description + "The time is accurate to within 250 ms. + Numeric value is 2E hex."; + } + identity ca-time-accurate-to-1-s { + base clock-accuracy; + description + "The time is accurate to within 1 s. + Numeric value is 2F hex."; + } + identity ca-time-accurate-to-10-s { + base clock-accuracy; + description + "The time is accurate to within 10 s. + Numeric value is 30 hex."; + } + identity ca-time-accurate-to-gt-10-s { + base clock-accuracy; + description + "The time accuracy exceeds 10 s. + Numeric value is 31 hex."; + } + + identity time-source { + description + "Enumeration for the source of time used by the Grandmaster + PTP Instance. + YANG identity is used so that a PTP Profile's YANG augment + can assign values, using numeric range F0 to FE hex."; + reference + "7.6.2.8 of IEEE Std 1588-2019"; + } + identity atomic-clock { + base time-source; + description + "Any PTP Instance that is based on an atomic resonance + for frequency, or a PTP Instance directly connected + to a device that is based on an atomic resonance for + frequency. Numeric value is 10 hex."; + } + identity gnss { + base time-source; + description + "Any PTP Instance synchronized to a satellite system that + distributes time and frequency. Numeric value is 20 hex."; + } + identity terrestrial-radio { + base time-source; + description + "Any PTP Instance synchronized via any of the radio + distribution systems that distribute time and frequency. + Numeric value is 30 hex."; + } + identity serial-time-code { + base time-source; + description + "Any PTP Instance synchronized via any of the serial + time code distribution systems that distribute time + and frequency, for example, IRIG-B. + Numeric value is 39 hex."; + } + identity ptp { + base time-source; + description + "Any PTP Instance synchronized to a PTP-based source + of time external to the domain. Numeric value is 40 hex."; + } + identity ntp { + base time-source; + description + "Any PTP Instance synchronized via NTP or Simple Network + Time Protocol (SNTP) servers that distribute time and + frequency. Numeric value is 50 hex."; + } + identity hand-set { + base time-source; + description + "Used for any PTP Instance whose time has been set by + means of a human interface based on observation of a + source of time to within the claimed clock accuracy. + Numeric value is 60 hex."; + } + identity other { + base time-source; + description + "Other source of time and/or frequency not covered by + other values. Numeric value is 90 hex."; + } + identity internal-oscillator { + base time-source; + description + "Any PTP Instance whose frequency is not based on atomic + resonance, and whose time is based on a free-running + oscillator with epoch determined in an arbitrary or + unknown manner. Numeric value is A0 hex."; + } + + typedef time-interval { + type int64; + description + "Time interval, expressed in nanoseconds, multiplied by 2^16. + Positive or negative time intervals outside the maximum range + of this data type shall be encoded as the largest positive and + negative values of the data type, respectively."; + reference + "5.3.2 of IEEE Std 1588-2019"; + } + + typedef clock-identity { + type string { + pattern "[0-9A-F]{2}(-[0-9A-F]{2}){7}"; + } + description + "Identifies unique entities within a PTP Network, + e.g. a PTP Instance or an entity of a common service. + The identity is an 8-octet array, constructed according + to specifications in IEEE Std 1588, using an + organization identifier from the IEEE Registration + Authority. + Each octet is represented in YANG as a pair of + hexadecimal characters, using uppercase for a letter. + Each octet in the array is separated by the dash + character."; + reference + "5.3.4 of IEEE Std 1588-2019 + 7.5.2.2 of IEEE Std 1588-2019"; + } + + typedef relative-difference { + type int64; + description + "Relative difference expressed as a dimensionless + fraction and multiplied by 2^62, with any + remaining fractional part truncated."; + reference + "5.3.11 of IEEE Std 1588-2019"; + } + + typedef instance-type { + type enumeration { + enum oc { + value 0; + description + "Ordinary Clock"; + } + enum bc { + value 1; + description + "Boundary Clock"; + } + enum p2p-tc { + value 2; + description + "Peer-to-peer Transparent Clock"; + } + enum e2e-tc { + value 3; + description + "End-to-end Transparent Clock"; + } + } + description + "Enumeration for the type of PTP Instance. + Values for this enumeration are specified by the IEEE 1588 + standard exclusively."; + reference + "8.2.1.5.5 of IEEE Std 1588-2019"; + } + + typedef fault-severity { + type enumeration { + enum emergency { + value 0; + description + "Emergency: system is unusable"; + } + enum alert { + value 1; + description + "Alert: immediate action needed"; + } + enum critical { + value 2; + description + "Critical: critical conditions"; + } + enum error { + value 3; + description + "Error: error conditions"; + } + enum warning { + value 4; + description + "Warning: warning conditions"; + } + enum notice { + value 5; + description + "Notice: normal but significant condition"; + } + enum informational { + value 6; + description + "Informational: informational messages"; + } + enum debug { + value 7; + description + "Debug: debug-level messages"; + } + } + description + "Enumeration for the severity of a fault record. + Values for this enumeration are specified by the IEEE 1588 + standard exclusively."; + reference + "8.2.6.3 of IEEE Std 1588-2019"; + } + + typedef port-state { + type enumeration { + enum initializing { + value 1; + description + "The PTP Port is initializing its data sets, hardware, and + communication facilities. The PTP Port shall not place any + PTP messages on its communication path."; + } + enum faulty { + value 2; + description + "The fault state of the protocol. Except for PTP management + messages that are a required response to a PTP message + received from the applicable management mechanism, + a PTP Port in this state shall not transmit any PTP related + messages. In a Boundary Clock, no activity on a faulty + PTP Port shall affect the other PTP Ports of the + PTP Instance. If fault activity on a PTP Port in this state + cannot be confined to the faulty PTP Port, then all + PTP Ports shall be in the faulty state."; + } + enum disabled { + value 3; + description + "The PTP Port is disabled. Except for PTP management + messages that are a required response to a PTP message + received from the applicable management mechanism, + a PTP Port in this state shall not transmit any PTP related + messages. In a Boundary Clock, no activity at the PTP Port + shall be allowed to affect the activity at any other + PTP Port of the Boundary Clock. A PTP Port in this state + shall discard all received PTP messages except for PTP + management messages."; + } + enum listening { + value 4; + description + "The PTP Port is waiting for the announce-receipt-timeout + to expire or to receive an Announce message from a + TimeTransmitter PTP Instance. The purpose of this state + is to allow orderly addition of PTP Instances to a domain + (i.e. to know if this PTP Port is truly a port of the + Grandmaster PTP Instance prior to taking that role)."; + } + enum pre-time-transmitter { + value 5; + description + "This port state provides an additional mechanism to + support more orderly reconfiguration of PTP Networks when + PTP Instances are added or deleted, PTP Instance + characteristics change, or connection topology changes. + In this state, a PTP Port behaves as it would if it were in + the time-transmitter state except that it does not place + certain classes of PTP messages on the PTP Communication + Path associated with the PTP Port."; + } + enum time-transmitter { + value 6; + description + "The PTP Port is the source of time on the + PTP Communication Path."; + } + enum passive { + value 7; + description + "The PTP Port is not the source of time on the + PTP Communication Path nor does it synchronize to a + TimeTransmitter Clock (receive time). The PTP Port can + potentially change to time-receiver when PTP Instances are + added or deleted, PTP Instance characteristics change, or + connection topology changes."; + } + enum uncalibrated { + value 8; + description + "The PTP Port is anticipating a change to the time-receiver + state, but it has not yet satisfied all requirements + (implementation or PTP Profile) necessary to ensure + complete synchronization. For example, an implementation + might require a minimum number of PTP Sync messages + in order to completely synchronize its servo algorithm."; + } + enum time-receiver { + value 9; + description + "The PTP Port synchronizes to the PTP Port on the + PTP Communication Path that is in the time-transmitter + state (i.e. receives time)."; + } + } + description + "Enumeration for the state of the protocol engine associated + with the PTP Port. Values for this enumeration are specified + by the IEEE 1588 standard exclusively."; + reference + "8.2.15.3.1 of IEEE Std 1588-2019 + 9.2.5 of IEEE Std 1588-2019"; + } + + typedef delay-mechanism { + type enumeration { + enum e2e { + value 1; + description + "The PTP Port is configured to use the delay + request-response mechanism."; + } + enum p2p { + value 2; + description + "The PTP Port is configured to use the peer-to-peer + delay mechanism."; + } + enum no-mechanism { + value 254; + description + "The PTP Port does not implement the delay mechanism. + This value shall not be used except when the applicable + PTP Profile specifies either: + 1) that the PTP Instance only supports frequency + transfer (syntonization) and that neither path delay + mechanism is to be used or + 2) that the PTP Instance participates in time transfer, + but the system accuracy requirements are such that, + for a segment of the system path, delays can be neglected + allowing PTP Instances in that portion of the PTP Network + to use the no-mechanism value."; + } + enum common-p2p { + value 3; + description + "The PTP Port is configured to use the Common Mean Link + Delay Service option."; + } + enum special { + value 4; + description + "Special Ports do not use either delay mechanism."; + } + } + description + "Enumeration for the path delay measuring mechanism. + Values for this enumeration are specified by the IEEE 1588 + standard exclusively."; + reference + "8.2.15.4.4 of IEEE Std 1588-2019"; + } + + typedef l1sync-state { + type enumeration { + enum disabled { + value 1; + description + "L1Sync is not enabled on this PTP Port, + or the event L1SYNC_RESET has occurred."; + } + enum idle { + value 2; + description + "L1Sync is enabled on this PTP Port. The PTP Port + sends messages with the L1_SYNC TLV. Initialization + occurs in this state."; + } + enum link-alive { + value 3; + description + "The PTP Port sends messages with the L1_SYNC TLV. + The PTP Port is receiving valid L1_SYNC TLV + from a peer PTP Port."; + } + enum config-match { + value 4; + description + "The PTP Port sends messages with the L1_SYNC TLV. + The PTP Port has a compatible configuration profile + when compared with its peer PTP Port configuration + profile received in the L1_SYNC TLV."; + } + enum l1-sync-up { + value 5; + description + "The PTP Port sends messages with the L1_SYNC TLV. + The relationship required by configuration is currently + in place. Synchronization enhancements are performed."; + } + } + description + "Enumeration for states of an L1Sync state machine associated + with an L1Sync port. + Values for this enumeration are specified by the IEEE 1588 + standard exclusively."; + reference + "L.5.3.5 of IEEE Std 1588-2019 + L.7.2 of IEEE Std 1588-2019"; + } + + grouping timestamp { + description + "The IEEE Std 1588 Timestamp type represents a + positive time with respect to the epoch + of PTP Instance Time. + This type is represented in YANG as a grouping, + with leafs seconds-field and nanoseconds-field."; + reference + "5.3.3 of IEEE Std 1588-2019 + 8.2.6.3 of IEEE Std 1588-2019"; + + leaf seconds-field { + type uint64 { + range "0..281474976710655"; + } + description + "The seconds-field member is the integer portion + of the timestamp in units of seconds. Since the + IEEE 1588 type is UInteger48, only 48 bits + are represented in YANG."; + } + + leaf nanoseconds-field { + type uint32; + description + "The nanoseconds-field member is the fractional + portion of the timestamp in units of nanoseconds."; + } + } + grouping port-identity { + description + "The IEEE Std 1588 PortIdentity type identifies a + PTP Port or Link Port."; + reference + "5.3.5 of IEEE Std 1588-2019"; + + leaf clock-identity { + type clock-identity; + description + "IEEE Std 1588 clockIdentity."; + } + + leaf port-number { + type uint16; + description + "IEEE Std 1588 portNumber. + If portNumber is unavailable, the value 0 can + be used, or this leaf can be omitted from the + operational datastore."; + reference + "7.5.2.3 of IEEE Std 1588-2019"; + } + } + + grouping port-address { + description + "The IEEE Std 1588 PortAddress type represents the + protocol address of a PTP Port."; + reference + "5.3.6 of IEEE Std 1588-2019"; + + leaf network-protocol { + type identityref { + base network-protocol; + } + description + "Protocol used by a PTP Instance to transport + PTP messages."; + } + + leaf address-length { + type uint16; + description + "Number of octets in address-field."; + } + + leaf address-field { + type string { + pattern "[0-9A-F]{2}(-[0-9A-F]{2})*"; + } + description + "The protocol address of a PTP Port in the format + defined by the mapping annex of the protocol as + identified by the network-protocol leaf. + The most significant octet of the address-field + is mapped into the octet of the address-field + member with index 0. + Each octet is represented in YANG as a pair of + hexadecimal characters, using uppercase for a letter. + Each octet in the array is separated by the dash + character."; + } + } + + grouping clock-quality { + description + "Quality of a PTP Instance, which contains IEEE Std 1588 + clockClass, clockAccuracy and offsetScaledLogVariance. + PTP Instances with better quality are more likely to + become the Grandmaster PTP Instance."; + reference + "5.3.7 of IEEE Std 1588-2019 + 8.2.1.3.1 of IEEE Std 1588-2019"; + + leaf clock-class { + type identityref { + base clock-class; + } + description + "The clockClass denotes the traceability of the time + or frequency distributed by the clock."; + reference + "7.6.2.5 of IEEE Std 1588-2019 + 8.2.1.3.1.2 of IEEE Std 1588-2019"; + } + + leaf clock-accuracy { + type identityref { + base clock-accuracy; + } + description + "The clockAccuracy indicates the accuracy of the clock + (Local Clock of the PTP Instance)."; + reference + "7.6.2.6 of IEEE Std 1588-2019 + 8.2.1.3.1.3 of IEEE Std 1588-2019"; + } + + leaf offset-scaled-log-variance { + type uint16; + description + "The offsetScaledLogVariance indicates the stability of the + clock (Local Clock of the PTP Instance). It provides an + estimate of the variations of the clock from a linear timescale + when it is not synchronized to another clock using the + protocol."; + reference + "7.6.2.7 of IEEE Std 1588-2019"; + } + } + + grouping fault-record { + description + "Record of a fault in the PTP Instance. + + NOTE - IEEE Std 1588 specifies a member + faultRecordLength for this type, which is needed + for PTP Management Messages, but is not needed for + YANG management."; + reference + "5.3.10 of IEEE Std 1588-2019"; + + container time { + description + "Time the fault occurred as indicated by the Timestamping + Clock of the PTP Instance. A value of all 1's for the + fields in the timestamp shall indicate that the occurrence + time is not available."; + uses timestamp; + } + + leaf severity { + type fault-severity; + description + "Severity of the fault."; + } + + leaf name { + type string; + description + "Name for the fault, unique within the implementation."; + } + + leaf value { + type string; + description + "Any value that may be associated with the fault that is + necessary for fault diagnosis."; + } + + leaf description { + type string; + description + "Any supplementary description of the fault."; + } + } + + grouping communication-capabilities { + description + "Multicast/unicast capabilities for a port + and message type. + These attributes report the values that are transmitted + by this PTP Instance to other PTP Instance(s) in the + network to indicate the multicast/unicast capabilities + for a port and message type. Therefore, the context is + protocol communication, and not YANG configuration."; + reference + "5.3.12 of IEEE Std 1588-2019 + 8.2.25 of IEEE Std 1588-2019 + 16.9.2 of IEEE Std 1588-2019"; + + leaf multicast-capable { + type boolean; + description + "True if the PTP Port is capable of transmitting + PTP messages using multicast communication, + otherwise it shall be false."; + } + + leaf unicast-capable { + type boolean; + description + "True if the PTP Port is capable of transmitting + PTP messages using unicast communication, + otherwise it shall be false."; + } + + leaf unicast-negotiation-capable { + type boolean; + description + "True if the PTP Port is capable negotiating unicast + communication using the unicast negotiation feature, + and unicast-negotiation-port-ds/enable is true, + otherwise the value of shall be false."; + } + + leaf unicast-negotiation-required { + type boolean; + description + "True if the value of unicast-negotiation-capable is true + and the use of the unicast negotiation feature is + required by the implementation, otherwise the value + shall be false."; + } + } + + grouping ptp-instance-performance-parameters { + description + "PTP Instance Performance Monitoring Parameters, + related to the PTP Port or Link Port in the + time-receiver state."; + reference + "Table J.1 of IEEE Std 1588-2019"; + + leaf average-time-transmitter-time-receiver-delay { + type time-interval; + description + "Average of the TimeTransmitterTimeReceiverDelay for this + interval."; + } + leaf minimum-time-transmitter-time-receiver-delay { + type time-interval; + description + "Minimum of the TimeTransmitterTimeReceiverDelay for this + interval."; + } + leaf maximum-time-transmitter-time-receiver-delay { + type time-interval; + description + "Maximum of the TimeTransmitterTimeReceiverDelay for this + interval."; + } + leaf stddev-time-transmitter-time-receiver-delay { + type time-interval; + description + "StdDev of the TimeTransmitterTimeReceiverDelay for this + interval."; + } + leaf average-time-receiver-time-transmitter-delay { + type time-interval; + description + "Average of the TimeReceiverTimeTransmitterDelay for this + interval."; + } + leaf minimum-time-receiver-time-transmitter-delay { + type time-interval; + description + "Minimum of the TimeReceiverTimeTransmitterDelay for this + interval."; + } + leaf maximum-time-receiver-time-transmitter-delay { + type time-interval; + description + "Maximum of the TimeReceiverTimeTransmitterDelay for this + interval."; + } + leaf stddev-time-receiver-time-transmitter-delay { + type time-interval; + description + "StdDev of the TimeReceiverTimeTransmitterDelay for this + interval."; + } + leaf average-mean-path-delay { + type time-interval; + description + "Average of the this interval."; + } + leaf minimum-mean-path-delay { + type time-interval; + description + "Minimum of the for this interval."; + } + leaf maximum-mean-path-delay { + type time-interval; + description + "Maximum of the for this interval."; + } + leaf stddev-mean-path-delay { + type time-interval; + description + "StdDev of the for this interval."; + } + leaf average-offset-from-time-transmitter { + type time-interval; + description + "Average of the for this + interval."; + } + leaf minimum-offset-from-time-transmitter { + type time-interval; + description + "Minimum of the for this + interval."; + } + leaf maximum-offset-from-time-transmitter { + type time-interval; + description + "Maximum of the for this + interval."; + } + leaf stddev-offset-from-time-transmitter { + type time-interval; + description + "StdDev of the for this + interval."; + } + } + + grouping ptp-port-performance-parameters-peer-delay { + description + "PTP Port Performance Monitoring Parameters, + related to the PTP Port or Link Port using the + peer-to-peer delay mechanism."; + reference + "Table J.2 of IEEE Std 1588-2019"; + + leaf average-mean-link-delay { + type time-interval; + description + "Average of the for this interval."; + } + leaf min-mean-link-delay { + type time-interval; + description + "Minimum of the for this interval."; + } + leaf max-mean-link-delay { + type time-interval; + description + "Maximum of the for this interval."; + } + leaf stddev-mean-link-delay { + type time-interval; + description + "StdDev of the for this interval."; + } + } + + grouping additional-performance-parameters { + description + "Additional Performance Monitoring Parameters, + intended to complement ptp-instance-performance-parameters."; + reference + "Table J.3 of IEEE Std 1588-2019"; + + leaf announce-tx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Announce + messages that have been transmitted for this + interval."; + } + leaf announce-rx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Announce + messages from the current GM that have been + received for this interval."; + } + leaf announce-foreign-rx { + type yang:zero-based-counter32; + description + "Counter indicating the total number of Announce + messages from the foreign TimeTransmitters that have been + received for this interval."; + } + leaf sync-tx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Sync + messages that have been transmitted for this + interval."; + } + leaf sync-rx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Sync + messages that have been received for this + interval."; + } + leaf follow-up-tx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Follow_Up + messages that have been transmitted for this + interval."; + } + leaf follow-up-rx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Follow_Up + messages that have been received for this + interval."; + } + leaf delay-req-tx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Delay_Req + messages that have been transmitted for this + interval."; + } + leaf delay-req-rx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Delay_Req + messages that have been received for this + interval."; + } + leaf delay-resp-tx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Delay_Resp + messages that have been transmitted for this + interval."; + } + leaf delay-resp-rx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Delay_Resp + messages that have been received for this + interval."; + } + leaf pdelay-req-tx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Pdelay_Req + messages that have been transmitted for this + interval."; + } + leaf pdelay-req-rx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Pdelay_Req + messages that have been received for this + interval."; + } + leaf pdelay-resp-tx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Pdelay_Resp + messages that have been transmitted for this + interval."; + } + leaf pdelay-resp-rx { + type yang:zero-based-counter32; + description + "Counter indicating the number of Pdelay_Resp + messages that have been received for this + interval."; + } + leaf pdelay-resp-follow-up-tx { + type yang:zero-based-counter32; + description + "Counter indicating the number of + Pdelay_Resp_Follow_Up messages that have + been transmitted for this interval."; + } + leaf pdelay-resp-follow-up-rx { + type yang:zero-based-counter32; + description + "Counter indicating the number of + Pdelay_Resp_Follow_Up messages that have + been transmitted for this interval."; + } + } + + grouping clock-performance-monitoring-data-record { + description + "The IEEE Std 1588 ClockPerformanceMonitoringDataRecord + type is used for PTP Instance performance monitoring + statistics."; + reference + "Table J.4.1 of IEEE Std 1588-2019"; + + leaf index { + type uint16; + description + "Index to each record in the list (0-99)."; + } + + leaf measurement-valid { + type boolean; + description + "The measurement-valid flag shall indicate the data + can be correctly interpreted. Validity is + implementation specific and may be defined in + a PTP Profile. If for some periods the data is not + valid for part of the data collection interval + (e.g. the clock is not locked), a specific + implementation can report the statistics only for + valid data and with measurement-valid true. + + This flag applies to all parameters for a + given measurement period, including PTP Port + and Link Port related."; + } + + leaf period-complete { + type boolean; + description + "The period-complete flag shall indicate that + measurements were performed during the entire + period (15-minute or 24-hour). For example, + if the PTP Instance is disabled for five minutes + of a 15-minute period, period-complete is false. + The period-complete flag is not related to the + validity of measurements that were performed. + + This flag applies to all parameters for a + given measurement period, including PTP Port + and Link Port related."; + } + + leaf pm-time { + type yang:timestamp; + description + "Time of the beginning of the measurement record. + This leaf's type is YANG timestamp, which is based + on system time (also known as local time). System + time is an unsigned integer in units of + 10 milliseconds, using an epoch defined by the + implementation (typically time of boot-up)."; + reference + "IETF RFC 6991"; + } + + uses ptp-instance-performance-parameters; + } + + grouping port-performance-monitoring-peer-delay-data-record { + description + "The IEEE Std 1588 PortPerformanceMonitoringPeerDelayDataRecord + type is used for the PTP Port related performance monitoring + statistics for the peer-to-peer delay measurement mechanism."; + reference + "Table J.4.1 of IEEE Std 1588-2019"; + + leaf index { + type uint16; + description + "Index to each record in the list (0-99)."; + } + + leaf pm-time { + type yang:timestamp; + description + "Time of the beginning of the measurement record. + This leaf's type is YANG timestamp, which is based + on system time (also known as local time). System + time is an unsigned integer in units of + 10 milliseconds, using an epoch defined by the + implementation (typically time of boot-up)."; + reference + "RFC 6991"; + } + + uses ptp-port-performance-parameters-peer-delay; + } + + grouping port-performance-monitoring-data-record { + description + "The IEEE Std 1588 PortPerformanceMonitoringDataRecord + type is used for additional PTP Port related performance + monitoring statistics."; + reference + "Table J.4.1 of IEEE Std 1588-2019"; + + leaf index { + type uint16; + description + "Index to each record in the list (0-99)."; + } + + leaf pm-time { + type yang:timestamp; + description + "Time of the beginning of the measurement record. + This leaf's type is YANG timestamp, which is based + on system time (also known as local time). System + time is an unsigned integer in units of + 10 milliseconds, using an epoch defined by the + implementation (typically time of boot-up)."; + reference + "RFC 6991"; + } + + uses additional-performance-parameters; + } + + container ptp { + description + "Contains all YANG nodes for the PTP data sets. + This hierarchy can be augmented with YANG nodes + for a specific vendor or PTP Profile."; + + container instances { + description + "YANG container that is used to get all PTP Instances. + YANG does not allow get of all elements in a YANG list, + so a YANG container wrapping the YANG list is provided for + that purpose. The naming convention uses plural for the + wrapping YANG container, and singular for the YANG list."; + + list instance { + + key "instance-index"; + + description + "List of one or more PTP Instances in the product (PTP Node). + Each PTP Instance represents a distinct instance of PTP + implementation (i.e. distinct Ordinary Clock, Boundary Clock, + or Transparent Clock), maintaining a distinct time. + PTP Instances may be created or deleted dynamically in + implementations that support dynamic create/delete."; + reference + "8.1.4.2 of IEEE Std 1588-2019"; + + leaf instance-index { + type uint32; + description + "The instance list is indexed using a number that is + unique per PTP Instance within the PTP Node, applicable + to the management context only (i.e. not used in PTP + messages). The domain-number of the PTP Instance is not + used as the key to instance-list, since it is possible + for a PTP Node to contain multiple PTP Instances using + the same domain-number."; + reference + "8.1.4.2 of IEEE Std 1588-2019"; + } + + container default-ds { + description + "The default data set of the PTP Instance."; + reference + "8.2.1 of IEEE Std 1588-2019"; + + leaf two-step-flag { + type boolean; + config false; + status deprecated; + description + "When set to true, the PTP Instance is two-step, + otherwise the PTP Instance is one-step. + This data set member is no longer used. However, + the twoStepFlag of the PTP common header is used. + One step or two step egress behavior is allowed to + be specified per PTP Port, or per PTP Instance. + Management of the one/two step egress behavior of + a PTP Port is not provided by this standard, but + can be specified as extensions to the data sets by a + PTP Profile or a product specification."; + reference + "8.2.1.2.1 of IEEE Std 1588-2019"; + } + + leaf clock-identity { + type clock-identity; + config false; + description + "The IEEE Std 1588 clockIdentity of the PTP Instance."; + reference + "8.2.1.2.2 of IEEE Std 1588-2019"; + } + + leaf number-ports { + type uint16; + config false; + description + "The number of PTP Ports on the PTP Instance. + For an Ordinary Clock, the value shall be one."; + reference + "8.2.1.2.3 of IEEE Std 1588-2019"; + } + + container clock-quality { + description + "The IEEE Std 1588 clockQuality of the PTP Instance. + PTP Instances with better quality are more likely to + become the Grandmaster PTP Instance."; + reference + "8.2.1.3.1 of IEEE Std 1588-2019"; + uses clock-quality; + } + + leaf priority1 { + type uint8; + description + "The IEEE Std 1588 priority1 of the PTP Instance. + Since priority1 is one of the first comparisons + performed by the Best TimeTransmitter Clock Algorithm + (BTCA), this leaf's configuration can be used to + explicitly select a Grandmaster PTP Instance. + Lower values take precedence. + The value of priority1 shall be configurable to any + value in the range 0 to 255, unless restricted by + limits established by the applicable PTP Profile."; + reference + "7.6.2.3 of IEEE Std 1588-2019 + 8.2.1.4.1 of IEEE Std 1588-2019"; + } + + leaf priority2 { + type uint8; + description + "The IEEE Std 1588 priority2 of the PTP Instance. + The priority2 member is compared by the + Best TimeTransmitter Clock Algorithm (BTCA) after + priority1 and clockQuality. + Lower values take precedence. + The value of priority2 shall be configurable to any + value in the range 0 to 255, unless restricted by + limits established by the applicable PTP Profile."; + reference + "7.6.2.4 of IEEE Std 1588-2019 + 8.2.1.4.2 of IEEE Std 1588-2019"; + } + + leaf domain-number { + type uint8; + description + "The IEEE Std 1588 domainNumber of the PTP Instance. + A domain consists of one or more PTP Instances + communicating with each other as defined by the + protocol. A domain shall define the scope of PTP message + communication, state, operations, data sets, and + timescale. Therefore, each domain represents a distinct + time. + Within a PTP Network, a domain is identified by two + data set members: domainNumber and sdoId. + The domainNumber is the primary mechanism for end users + and system integrators to isolate the operation of a + PTP Instance from PTP messages used in other domains. + The value of the domainNumber shall be configurable + to values permitted in IEEE Std 1588, unless the + allowed values are further restricted by the applicable + PTP Profile."; + reference + "7.1 of IEEE Std 1588-2019 + 8.2.1.4.3 of IEEE Std 1588-2019"; + } + + leaf time-receiver-only { + type boolean; + description + "The value of time-receiver-only shall be true if the + PTP Instance is a time-receiver-only PTP Instance + (false for not time-receiver-only). + The time-receiver-only member can be true for + Ordinary Clocks only. + When time-receiver-only is true, the PTP Instance + implements special behavior in the context of the state + machines that determine port-state."; + reference + "8.2.1.4.4 of IEEE Std 1588-2019 + 9.2.2.1 of IEEE Std 1588-2019"; + } + + leaf sdo-id { + type uint16 { + range "0..4095"; + } + description + "The IEEE Std 1588 sdoId of the PTP Instance. + A domain consists of one or more PTP Instances + communicating with each other as defined by the + protocol. A domain shall define the scope of PTP message + communication, state, operations, data sets, and + timescale. Therefore, each domain represents a distinct + time. + Within a PTP Network, a domain is identified by two + data set members: domainNumber and sdoId. + The sdoId of a domain is a 12-bit integer in the + closed range 0 to 4095. + The sdoId member is the primary mechanism for providing + isolation of PTP Instances operating a PTP Profile + specified by a Standards Development Organization (SDO), + from other PTP Instances operating a PTP Profile + specified by a different SDO."; + reference + "7.1 of IEEE Std 1588-2019 + 8.2.1.4.5 of IEEE Std 1588-2019 + 16.5 of IEEE Std 1588-2019"; + } + + container current-time { + description + "For management read, this member shall return the + current value of the PTP Instance Time. + When management write is supported, this member + shall set the PTP Instance Time. + Time originates in the Grandmaster PTP Instance and + is distributed by PTP to other PTP Instances in + the domain. + NOTE 1 - The time in the Grandmaster PTP Instance + is normally determined by interacting with a primary + reference, e.g., GPS, by means outside the scope of + this standard. + NOTE 2 - When this member is used to set time in a + PTP Instance other than the Grandmaster PTP Instance, + the PTP Node can return a management error. + NOTE 3 - If the time is set in a PTP Instance other + than the Grandmaster PTP Instance, it will be + overwritten by the operation of the protocol and will + therefore exist only as a transient."; + reference + "8.2.1.5.1 of IEEE Std 1588-2019"; + uses timestamp; + } + + leaf instance-enable { + type boolean; + description + "Indicates if the PTP Instance is enabled for + PTP operation. + When management write is supported: + - Write of the value true shall cause the PTP Instance + to initialize, only if the value was previously false. + - Write of the value false shall immediately disable + operation of the PTP Instance (i.e. analogous to power + off). + If this leaf is not supported, the PTP Instance shall be + specified-by-design to be enabled (true)."; + reference + "8.2.1.5.2 of IEEE Std 1588-2019"; + } + + leaf external-port-config-enable { + if-feature external-port-config; + type boolean; + description + "This value determines whether the external port + configuration option is in the disabled state (false) + or enabled state (true). + When this value is false, each PTP Port's state + is determined by PTP state machines, including + the Best TimeTransmitter Clock Algorithm (BTCA). + When this value is true, each PTP Port's state + is configured externally, and PTP state machines + are effectively disabled. External configuration + of PTP Port state can be accomplished using the + desiredState member of the port (i.e., + ../ports/port[]/external-port-config-port-ds/ + desired-state)."; + reference + "8.2.1.5.3 of IEEE Std 1588-2019 + 17.6 of IEEE Std 1588-2019"; + } + + leaf max-steps-removed { + type uint8 { + range "2..255"; + } + description + "If the value of stepsRemoved of an Announce message + is greater than or equal to the value of this + max-steps-removed leaf, the Announce message is not + considered in the operation of the + Best TimeTransmitter Clock Algorithm (BTCA). + The value shall be in the closed range 2 to 255. + If the leaf is not supported, the value used shall + be 255."; + reference + "8.2.1.5.4 of IEEE Std 1588-2019 + 9.3.2.5 of IEEE Std 1588-2019"; + } + + leaf instance-type { + type instance-type; + description + "The type of PTP Instance. + This leaf is read-only unless support for write is + explicitly specified by the applicable PTP Profile or + product specification."; + reference + "8.2.1.5.5 of IEEE Std 1588-2019"; + } + } + + container current-ds { + description + "Provides current data from operation + of the protocol."; + reference + "8.2.2 of IEEE Std 1588-2019"; + + leaf steps-removed { + type uint16; + config false; + description + "The number of PTP Communication Paths traversed + between this PTP Instance and the Grandmaster + PTP Instance."; + reference + "8.2.2.2 of IEEE Std 1588-2019"; + } + + leaf offset-from-time-transmitter { + type time-interval; + config false; + description + "The current value of the time difference between + a TimeTransmitter PTP Instance and a + TimeReceiver PTP Instance as computed by the + TimeReceiver PTP Instance. + NOTE - When a PTP Profile requires a Boundary + Clock to transfer offset information internally + from TimeReceiver PTP Port to TimeTransmitter + PTP Port(s), this value effectively returns the offset + from the Grandmaster PTP Instance."; + reference + "8.2.2.3 of IEEE Std 1588-2019"; + } + + leaf mean-delay { + type time-interval; + config false; + description + "The current value of the mean propagation time between + a TimeTransmitter PTP Instance and a + TimeReceiver PTP Instance as computed by the + TimeReceiver PTP Instance. + If the PTP Instance has no PTP Port in time-receiver or + uncalibrated state, this returns zero. + Otherwise, the TimeReceiver PTP Port returns this value + depending on its delay-mechanism: + e2e: mean propagation time over the + PTP Communication Path, i.e. + p2p or common-p2p: mean propagation time over the + PTP Link, i.e. + disabled or special: zero"; + reference + "7.4.2 of IEEE Std 1588-2019 + 8.2.2.4 of IEEE Std 1588-2019"; + } + + leaf mean-path-delay { + type time-interval; + config false; + status deprecated; + description + "In IEEE Std 1588-2008, currentDS.meanDelay was called + currentDS.meanPathDelay. While the specification of + this member is retained in the current standard, the + member is renamed to currentDS.meanDelay. This change + is consistent with other changes that ensure clarity + and consistency of naming, where + - 'path' is associated with the + request-response mechanism + - 'link' is associated with the + peer-to-peer delay mechanism"; + reference + "8.2.2.4 of IEEE Std 1588-2008"; + } + + leaf synchronization-uncertain { + type boolean; + config false; + description + "This boolean is true when synchronization is + uncertain (e.g., not within specification) + in either the Parent PTP Port or this + PTP Instance. The value is copied from a + received Announce message to transmitted Announce + message, such that it reflects uncertain + synchronization from this PTP Instance to the + Grandmaster. Performance metrics for determining + uncertainty are specified by the applicable + PTP Profile."; + reference + "8.2.2.5 of IEEE Std 1588-2019"; + } + } + + container parent-ds { + description + "Provides data learned from the parent of this + PTP Instance (i.e. time-transmitter port on the other + side of the path/link)."; + reference + "8.2.3 of IEEE Std 1588-2019"; + + container parent-port-identity { + config false; + description + "The IEEE Std 1588 portIdentity of the PTP Port on the + TimeTransmitter PTP Instance that issues the Sync + messages used in synchronizing this PTP Instance."; + reference + "8.2.3.2 of IEEE Std 1588-2019"; + uses port-identity; + } + + leaf parent-stats { + type boolean; + config false; + description + "When set to true, the values of + parent-ds/observed-parent-offset-scaled-log-variance + and + parent-ds/observed-parent-clock-phase-change-rate + have been measured and are valid."; + reference + "8.2.3.3 of IEEE Std 1588-2019"; + } + + leaf observed-parent-offset-scaled-log-variance { + type uint16; + config false; + description + "Estimate of the variance of the phase offset of the + Local PTP Clock of the Parent PTP Instance as measured + with respect to the Local PTP Clock in the + TimeReceiver PTP Instance. This measurement is + optional, but if not made, the value of + parent-ds/parent-stats shall be false."; + reference + "7.6.3.3 of IEEE Std 1588-2019 + 7.6.3.5 of IEEE Std 1588-2019 + 8.2.3.4 of IEEE Std 1588-2019"; + } + + leaf observed-parent-clock-phase-change-rate { + type int32; + config false; + description + "Estimate of the phase change rate of the + Local PTP Clock of the Parent PTP Instance as measured + by the TimeReceiver PTP Instance using its + Local PTP Clock. + If the estimate exceeds the capacity of its data type, + this value shall be set to 7FFF FFFF (base 16) or + 8000 0000 (base 16), as appropriate. A positive sign + indicates that the phase change rate in the + Parent PTP Instance is greater than that in the + TimeReceiver PTP Instance. The measurement of this + value is optional, but if not measured, the value of + parent-ds/parent-stats shall be false."; + reference + "7.6.4.4 of IEEE Std 1588-2019 + 8.2.3.5 of IEEE Std 1588-2019"; + } + + leaf grandmaster-identity { + type clock-identity; + config false; + description + "The IEEE Std 1588 clockIdentity of the Grandmaster PTP + Instance."; + reference + "8.2.3.6 of IEEE Std 1588-2019"; + } + + container grandmaster-clock-quality { + config false; + description + "The IEEE Std 1588 clockQuality of the Grandmaster PTP + Instance."; + reference + "8.2.3.7 of IEEE Std 1588-2019"; + uses clock-quality; + } + + leaf grandmaster-priority1 { + type uint8; + config false; + description + "The IEEE Std 1588 priority1 of the Grandmaster PTP + Instance."; + reference + "8.2.3.8 of IEEE Std 1588-2019"; + } + + leaf grandmaster-priority2 { + type uint8; + config false; + description + "The IEEE Std 1588 priority2 of the Grandmaster PTP + Instance."; + reference + "8.2.3.9 of IEEE Std 1588-2019"; + } + + container protocol-address { + description + "The protocol address of the PTP Port + that issues the Sync messages used in synchronizing + this PTP Instance."; + reference + "8.2.3.10 of IEEE Std 1588-2019"; + uses port-address; + } + + leaf synchronization-uncertain { + type boolean; + config false; + description + "This boolean is true when synchronization is + uncertain in the Parent PTP Port."; + reference + "8.2.3.11 of IEEE Std 1588-2019"; + } + } + + container time-properties-ds { + description + "Provides data learned from the current + Grandmaster PTP Instance."; + reference + "8.2.4 of IEEE Std 1588-2019"; + + leaf current-utc-offset { + when "../current-utc-offset-valid='true'"; + type int16; + description + "Specified as in IERS Bulletin C, this provides + the offset from UTC (TAI - UTC). The offset is in + units of seconds."; + reference + "7.2.4 of IEEE Std 1588-2019 + 8.2.4.2 of IEEE Std 1588-2019"; + } + + leaf current-utc-offset-valid { + type boolean; + description + "The value of current-utc-offset-valid shall be true + if the values of current-utc-offset, leap59, and leap61 + are known to be correct, otherwise it shall be false. + NOTE - The constraint for leap59 and leap61 did not + exist in IEEE Std 1588-2008, and for compatibility, + corresponding when statements were not included below."; + reference + "8.2.4.3 of IEEE Std 1588-2019"; + } + + leaf leap59 { + type boolean; + description + "If the timescale is PTP, a true value for leap59 + shall indicate that the last minute of the + current UTC day contains 59 seconds. + If the timescale is not PTP, the value shall be + false."; + reference + "8.2.4.4 of IEEE Std 1588-2019"; + } + + leaf leap61 { + type boolean; + description + "If the timescale is PTP, a true value for leap61 + shall indicate that the last minute of the + current UTC day contains 61 seconds. + If the timescale is not PTP, the value shall be + false."; + reference + "8.2.4.5 of IEEE Std 1588-2019"; + } + + leaf time-traceable { + type boolean; + description + "The value of time-traceable shall be true if the + timescale is traceable to a primary reference; + otherwise, the value shall be false. + The uncertainty specifications appropriate to the + evaluation of whether traceability to a primary + reference is achieved should be defined in the + applicable PTP Profile. In the absence of such a + definition the value of time-traceable is + implementation specific."; + reference + "8.2.4.6 of IEEE Std 1588-2019"; + } + + leaf frequency-traceable { + type boolean; + description + "The value of time-traceable shall be true if the + frequency determining the timescale is traceable + to a primary reference; otherwise, the value shall + be false. + The uncertainty specifications appropriate to the + evaluation of whether traceability to a primary + reference is achieved should be defined in the + applicable PTP Profile. In the absence of such a + definition the value of frequency-traceable is + implementation specific."; + reference + "8.2.4.7 of IEEE Std 1588-2019"; + } + + leaf ptp-timescale { + type boolean; + description + "If ptp-timescale is true, the timescale of + the Grandmaster PTP Instance is PTP, which is + the elapsed time since the PTP epoch measured + using the second defined by International Atomic + Time (TAI). + If ptp-timescale is false, the timescale of + the Grandmaster PTP Instance is ARB, which is + the elapsed time since an arbitrary epoch."; + reference + "7.2.1 of IEEE Std 1588-2019 + 8.2.4.8 of IEEE Std 1588-2019"; + } + + leaf time-source { + type identityref { + base time-source; + } + description + "The source of time used by the Grandmaster + PTP Instance."; + reference + "7.6.2.8 of IEEE Std 1588-2019 + 8.2.4.9 of IEEE Std 1588-2019"; + } + } + + container description-ds { + description + "Provides descriptive information for the PTP Instance."; + reference + "8.2.5 of IEEE Std 1588-2019"; + + leaf manufacturer-identity { + type string { + pattern "[0-9A-F]{2}(-[0-9A-F]{2}){2}"; + } + config false; + description + "3-octet OUI or CID owned by the manufacturer of the + PTP Instance, assigned by the IEEE Registration + Authority. + Each octet is represented in YANG as a pair of + hexadecimal characters, using uppercase for a letter. + Each octet in the array is separated by the dash + character."; + reference + "8.2.5.2 of IEEE Std 1588-2019"; + } + + leaf product-description { + type string { + length "2..64"; + } + config false; + description + "The product-description string shall indicate, in order: + - The name of the manufacturer of the PTP Instance, + manufacturerName, followed by a semicolon (;) + - The model number of the PTP Instance, modelNumber, + followed by a semicolon (;) + - A unique identifier of this PTP Instance, + instanceIdentifier, such as the MAC address or + the serial number. + The content and meaning of the manufacturerName, + modelNumber, and the instanceIdentifier strings are + determined by the manufacturer of the PTP Instance."; + reference + "8.2.5.3 of IEEE Std 1588-2019"; + } + + leaf product-revision { + type string { + length "2..32"; + } + config false; + description + "Indicate the revisions for PTP Instance's + hardware (HW), firmware (FW), and software (SW). + This information shall be semicolon (;) separated + text fields in the order HW;FW;SW. Non-applicable + revisions shall be indicated by a text fields of + zero length."; + reference + "8.2.5.4 of IEEE Std 1588-2019"; + } + + leaf user-description { + type string { + length "0..128"; + } + description + "Configurable description of the product's PTP Instance. + The user-description string should indicate, in order: + - A user-defined name of the PTP Instance, + e.g., Sensor-1, followed by a semicolon (;) + - A user-defined physical location of the PTP Instance, + e.g., Rack-2 Shelf-3."; + reference + "8.2.5.5 of IEEE Std 1588-2019"; + } + } + + container fault-log-ds { + if-feature fault-log; + config false; + description + "Represents an optional mechanism for logging of faults + that occur in the PTP Instance. If one member of + fault-log-ds is supported, all members shall be + supported."; + reference + "8.2.6 of IEEE Std 1588-2019"; + + leaf number-of-fault-records { + type uint16; + config false; + description + "The number of fault records available in + fault-record-list."; + reference + "8.2.6.2 of IEEE Std 1588-2019"; + } + + list fault-record-list { + config false; + description + "List of fault records, number-of-fault-records + in length. + The maximum length of fault-record-list is + implementation-specific. The fault-record-list + is maintained by the PTP Instance until + fault-log-ds.reset is used."; + reference + "8.2.6.3 of IEEE Std 1588-2019"; + + uses fault-record; + } + + action reset { + description + "This action causes the contents of fault-record-list + to be cleared, and number-of-fault-records to be set + to zero."; + reference + "8.2.6.4 of IEEE Std 1588-2019"; + } + } + + // The nonvolatileStorageDS in 8.2.7 of IEEE Std 1588-2019 + // is not applicable for YANG, since protocols like NETCONF + // and RESTCONF specify analogous features for configuration + // storage. + + container path-trace-ds { + if-feature path-trace; + description + "Provides data for the optional path + trace mechanism."; + reference + "16.2 of IEEE Std 1588-2019"; + + leaf-list list { + type clock-identity; + config false; + description + "List of IEEE Std 1588 clock identity values + (type ClockIdentity), in the order provided in the + PATH_TRACE TLV."; + reference + "16.2.2.2.1 of IEEE Std 1588-2019"; + } + + leaf enable { + type boolean; + description + "Allows for enable/disable of the path trace mechanism + using management. If path-trace-ds.enable is true, + the path trace mechanism shall be operational. + If path-trace-ds.enable is false, the path trace + mechanism shall be inactive."; + reference + "16.2.2.3.1 of IEEE Std 1588-2019"; + } + } + + container alternate-timescale-ds { + if-feature alternate-timescale; + description + "Provides data for the optional alternate + timescale offsets mechanism."; + reference + "16.3 of IEEE Std 1588-2019"; + + leaf max-key { + type uint8; + config false; + description + "The value of max-key shall indicate the value of + the largest key-field in the list."; + reference + "16.3.4.3.1 of IEEE Std 1588-2019"; + } + + list list { + key "key-field"; + description + "List of alternate timescales in the PTP Instance. + Elements in the list can be created or deleted, if + those operations are supported by management. + + If management write is supported for items + current-offset, jump-seconds, and time-of-next-jump, + the value for all three items shall be provided + within a single write operation, and the update of + all three items shall be atomic. If any of the three + values fails to update, a management error shall be + returned."; + reference + "16.3.4.4.1 of IEEE Std 1588-2019"; + + leaf key-field { + type uint8; + description + "Unique identifier of each element in the list."; + } + + leaf enable { + type boolean; + description + "If enable is true, the + ALTERNATE_TIME_OFFSET_INDICATOR TLV + for this alternate timescale shall be attached + to Announce messages. If enable is false, the TLV + shall not be attached."; + } + + leaf current-offset { + type int32; + description + "Offset of the alternate time, in seconds, from + PTP Instance Time in the Grandmaster PTP Instance."; + } + + leaf jump-seconds { + type int32; + description + "Size of the next discontinuity, in seconds, in the + alternate timescale. A value of zero indicates that + no discontinuity is expected. A positive value + indicates that the discontinuity will cause the + current-offset of the alternate timescale to + increase."; + } + + leaf time-of-next-jump { + type uint64; + description + "Value of the seconds-field of the transmitting PTP + Instance Time at the time that the next discontinuity + will occur. The discontinuity occurs at the start of + the second indicated by the value of time-of-next-jump. + Only 48-bits are valid (the upper 16-bits are always + zero)."; + } + + leaf display-name { + type string { + length "0..10"; + } + description + "Textual description of the alternate timescale."; + } + } + } + + container holdover-upgrade-ds { + if-feature holdover-upgrade; + description + "Provides data for the optional holdover + upgrade mechanism."; + reference + "16.4 of IEEE Std 1588-2019"; + + leaf enable { + type boolean; + description + "Used to enable (true) or disable (false) the + holdover upgrade mechanism."; + } + } + + container grandmaster-cluster-ds { + if-feature grandmaster-cluster; + description + "Provides data for the optional grandmaster + cluster mechanism."; + reference + "17.2.3 of IEEE Std 1588-2019"; + + leaf max-table-size { + type uint8; + config false; + description + "Maximum number of elements permitted + in the port-address list. + + NOTE - The actualTableSize of IEEE Std 1588 is not + applicable for YANG, since YANG mechanisms can be used + to control the number of elements in port-address."; + } + + leaf log-query-interval { + type int8; + description + "Logarithm to the base 2 of the mean interval in + seconds between unicast Announce messages from + cluster members."; + } + + list port-address { + key "index"; + description + "List of port addresses, one for each member of the + grandmaster cluster."; + + leaf index { + type uint16; + description + "Index to a port address in the list, typically + sequential from 0 to N-1, where N is the number of + port addresses."; + } + + uses port-address; + } + } + + container acceptable-time-transmitter-ds { + if-feature acceptable-time-transmitter; + description + "Provides data for the optional acceptable + timeTransmitter table mechanism."; + reference + "17.5.3 of IEEE Std 1588-2019"; + + leaf max-table-size { + type uint16; + config false; + description + "Maximum number of elements permitted + in the list. + + NOTE - The actualTableSize of IEEE Std 1588 is not + applicable for YANG, since YANG mechanisms can be used + to control the number of elements in list."; + reference + "17.5.3.3.1 of IEEE Std 1588-2019"; + } + + list list { + key "index"; + description + "List of acceptable timeTransmitters in the + PTP Instance. Elements in the list can be created or + deleted, if those operations are supported by + management. + + If management write is supported for items + acceptable-clock-identity, acceptable-port-number, + and alternate-priority1, the value for all three items + shall be provided within a single write operation, + and the update of all three items shall be atomic. + If any of the three values fails to update, a management + error shall be returned."; + reference + "17.5.3.4.2 of IEEE Std 1588-2019"; + + leaf index { + type uint8; + description + "Unique index to each element in the list, typically + sequential from 0 to N-1, where N is the number of + elements."; + } + + container acceptable-port-identity { + description + "The IEEE Std 1588 portIdentity of the + acceptable timeTransmitter."; + uses port-identity; + } + + leaf alternate-priority1 { + type uint8; + description + "The IEEE Std 1588 priority1 used as an alternate + for the acceptable timeTransmitter."; + } + } + } + + container performance-monitoring-ds { + if-feature performance-monitoring; + description + "Provides data for the optional performance + monitoring mechanism, scoped to the PTP Instance."; + reference + "8.2.13 of IEEE Std 1588-2019 + J.5.1 of IEEE Std 1588-2019"; + + leaf enable { + type boolean; + description + "Permits management control over the collection of + performance monitoring data, including + performance-monitoring-ds (PTP Instance), + ports/port[]/performance-monitoring-port-ds + (PTP Port of PTP Instance), and + common-services/cmlds/ports/port[]/ + performance-monitoring-port-ds (CMLDS Link Port + associated with enabled PTP Port)."; + reference + "J.5.1.1 of IEEE Std 1588-2019"; + } + + list record-list { + key "index"; + config false; + max-elements 99; + description + "List of performance monitoring records for the + PTP Instance. The list is organized as follows: + - 97 15-minute measurement records, the current record + at index 0, followed by the most recent 96 records. + - 2 24-hour measurement records, the current record + at index 97, and the previous record at index 98. + + If a record is not implemented for a specific index, + management does not return the record. For example, + if only four 15-minute periods are implemented, + a management request for performance-monitoring-ds/ + record-list[6] returns an error. + + If only some of the data is reported, the same index + values are used. As an example, if only the 24-hour + statistics are accessed, the indexes are still 97 and 98. + + If a specific parameter + (e.g. max-time-transmitter-time-receiver-delay) + is not implemented, management does not return the + parameter (i.e., error). Parameters that are invalid + (not measured correctly) shall be indicated with + one in all bits, except the most significant. This + represents the largest positive value of + time-interval, indicating a value outside the + maximum range."; + reference + "J.5.1.2 of IEEE Std 1588-2019"; + + uses clock-performance-monitoring-data-record; + } + } + + container enhanced-metrics-ds { + if-feature enhanced-metrics; + description + "Provides data for the optional enhanced + synchronization accuracy metrics mechanism."; + reference + "16.12 of IEEE Std 1588-2019"; + + leaf enable { + type boolean; + description + "If the Enhanced Synchronization Accuracy Metrics feature + is implemented, the value true shall indicate that + the feature is enabled on the PTP Instance, and the + value false shall indicate that the option is disabled + on the PTP Instance."; + reference + "8.2.14.2 of IEEE Std 1588-2019"; + } + } + + container ports { + description + "YANG container that is used to get all PTP Ports + in the PTP Instance. + YANG does not allow get of all elements in a YANG list, + so a YANG container wrapping the YANG list is provided for + that purpose. The naming convention uses plural for the + wrapping YANG container, and singular for the YANG list."; + + list port { + key "port-index"; + description + "List of data for each PTP Port in the PTP Instance. + While the PTP Instance is disabled, it is possible to + have zero PTP Ports (i.e., ports not yet created). + While the PTP Instance is enabled, an Ordinary Clock + will have one PTP Port, and a Boundary Clock or + Transparent Clock will have more than one PTP Port."; + reference + "8.1.4.2 of IEEE Std 1588-2019"; + + leaf port-index { + type uint16; + description + "The port list is indexed using a number that is + unique per PTP Port within the PTP Instance, + applicable to the management context only + (i.e., not used in PTP messages)."; + } + + leaf underlying-interface { + type if:interface-ref; + description + "Reference to the configured underlying IETF YANG + interface that is used by this PTP Port for + transport of PTP messages. Among other data, + physical identifiers for the interface + (e.g., MAC address) can be obtained using this + reference."; + reference + "RFC 8343"; + } + + container port-ds { + description + "Primary data set for the PTP Port."; + reference + "8.2.15 of IEEE Std 1588-2019"; + + container port-identity { + config false; + description + "The IEEE Std 1588 portIdentity of this PTP Port."; + reference + "8.2.15.2.1 of IEEE Std 1588-2019"; + uses port-identity; + } + + leaf port-state { + type port-state; + config false; + description + "Current state of the protocol engine associated + with this PTP Port."; + reference + "8.2.15.3.1 of IEEE Std 1588-2019"; + } + + leaf log-min-delay-req-interval { + type int8; + description + "Logarithm to the base 2 of the IEEE Std 1588 + minDelayReqInterval, the minimum permitted + mean time interval between successive Delay_Req + messages sent by a TimeReceiver PTP Instance."; + reference + "7.7.2.4 of IEEE Std 1588-2019 + 8.2.15.3.2 of IEEE Std 1588-2019"; + } + + leaf mean-link-delay { + type time-interval; + config false; + description + "If the value of the delay-mechanism leaf is p2p + this value shall be an estimate of the current + one-way propagation delay on the PTP Link attached + to this PTP Port, computed using the peer-to-peer + delay mechanism. + If the value of the delay-mechanism leaf is + common-p2p, this value shall be equal to the value of + ptp/common-services/cmlds/ports/port[]/port-ds/ + mean-link-delay. + If the value of the delay-mechanism leaf is e2e, + disabled, or special, this value shall be zero."; + reference + "8.2.15.3.3 of IEEE Std 1588-2019"; + } + + leaf peer-mean-path-delay { + type time-interval; + config false; + status deprecated; + description + "In IEEE Std 1588-2008, this data set member was + called portDS.peerMeanPathDelay. While the + specification of this member is retained in the + current standard, the member is renamed to + portDS.meanLinkDelay (i.e., ../mean-link-delay). + This change is consistent with other changes that + ensure clarity and consistency of naming, where + - 'path' is associated with the + request-response mechanism + - 'link' is associated with the + peer-to-peer delay mechanism"; + reference + "8.2.5.3.3 of IEEE Std 1588-2008"; + } + + leaf log-announce-interval { + type int8; + description + "Logarithm to the base 2 of the mean IEEE Std 1588 + announceInterval, the time interval between + successive Announce messages sent by a PTP Port."; + reference + "7.7.2.2 of IEEE Std 1588-2019 + 8.2.15.4.1 of IEEE Std 1588-2019"; + } + + leaf announce-receipt-timeout { + type uint8; + description + "The integral multiple of IEEE Std 1588 + announceInterval that must pass without receipt of + an Announce message before the occurrence of the + event ANNOUNCE_RECEIPT_TIMEOUT_EXPIRES. The range + shall be 2 to 255 subject to further restrictions of + the applicable PTP Profile. While 2 is permissible, + normally the value should be at least 3."; + reference + "7.7.3.1 of IEEE Std 1588-2019 + 8.2.15.4.2 of IEEE Std 1588-2019"; + } + + leaf log-sync-interval { + type int8; + description + "Logarithm to the base 2 of the mean IEEE Std 1588 + syncInterval, the time interval between successive + Sync messages, when transmitted as multicast + messages. The rates for unicast transmissions are + negotiated separately on a per PTP Port basis and + are not constrained by this leaf."; + reference + "7.7.2.3 of IEEE Std 1588-2019 + 8.2.15.4.3 of IEEE Std 1588-2019"; + } + + leaf delay-mechanism { + type delay-mechanism; + description + "The path delay measuring mechanism used by the PTP + Port in computing (propagation delay)."; + reference + "8.2.15.4.4 of IEEE Std 1588-2019"; + } + + leaf log-min-pdelay-req-interval { + type int8; + description + "Logarithm to the base 2 of the IEEE Std 1588 + minPdelayReqInterval, the minimum permitted + mean time interval between successive Pdelay_Req + messages sent over a PTP Link."; + reference + "7.7.2.5 of IEEE Std 1588-2019 + 8.2.15.4.5 of IEEE Std 1588-2019"; + } + + leaf version-number { + type uint8; + description + "The PTP major version in use on the PTP Port. + NOTE - This indicates the version of the + IEEE 1588 standard, and not the version of an + applicable PTP Profile."; + reference + "8.2.15.4.6 of IEEE Std 1588-2019"; + } + + leaf minor-version-number { + type uint8; + description + "The PTP minor version in use on the PTP Port. + NOTE - This indicates the version of the + IEEE 1588 standard, and not the version of an + applicable PTP Profile."; + reference + "8.2.15.4.7 of IEEE Std 1588-2019"; + } + + leaf delay-asymmetry { + type time-interval; + description + "The value of IEEE Std 1588 + applicable to the PTP Port, which is the + difference in transmission time in one direction + as compared to the opposite direction."; + reference + "7.4.2 of IEEE Std 1588-2019 + 8.2.15.4.8 of IEEE Std 1588-2019"; + } + + leaf port-enable { + type boolean; + description + "Indicates if the PTP Port is enabled for + PTP operation. + When management write is supported: + - Write of the value true causes the + DESIGNATED_ENABLED event to occur, even if the + value was previously true. + - Write of the value false causes the + DESIGNATED_DISABLED event to occur, even if the + value was previously false. + If this leaf is not supported, the PTP Port shall be + specified-by-design to be enabled (true)."; + reference + "8.2.15.5.1 of IEEE Std 1588-2019"; + } + + leaf time-transmitter-only { + type boolean; + description + "If the value of time-transmitter-only is true, + the PTP Port shall be in the IEEE Std 1588 + timeTransmitterOnly mode. + If the value is false, the PTP Port shall not be + in the timeTransmitterOnly mode. + When time-transmitter-only is true, the PTP Port + can never enter the time-receiver port-state."; + reference + "8.2.15.5.2 of IEEE Std 1588-2019 + 9.2.2.2 of IEEE Std 1588-2019"; + } + } + + container timestamp-correction-port-ds { + if-feature timestamp-correction; + description + "Provides access to the configurable correction of + timestamps provided to the PTP protocol."; + reference + "8.2.16 of IEEE Std 1588-2019 + 16.7 of IEEE Std 1588-2019"; + + leaf egress-latency { + type time-interval; + description + "Interval between the + provided for a PTP message and the time at which + the message timestamp point of the PTP message + crosses the reference plane."; + reference + "7.3.4.2 of IEEE Std 1588-2019 + 8.2.16.2 of IEEE Std 1588-2019"; + } + + leaf ingress-latency { + type time-interval; + description + "Interval between the time the message timestamp + point of an ingress PTP message crosses the + reference plane and the + provided for the PTP message."; + reference + "7.3.4.2 of IEEE Std 1588-2019 + 8.2.16.3 of IEEE Std 1588-2019"; + } + } + + container asymmetry-correction-port-ds { + if-feature asymmetry-correction; + description + "Provides access to asymmetry correction parameters + that are used to compute the value of + delayAsymmetry>."; + reference + "8.2.17 of IEEE Std 1588-2019 + 16.8 of IEEE Std 1588-2019"; + + leaf constant-asymmetry { + type time-interval; + description + "Constant asymmetry used to fine adjust the + dynamically calculated value of , + when the mechanism to calculate + or certain media is enabled."; + reference + "8.2.17.2 of IEEE Std 1588-2019"; + } + + leaf scaled-delay-coefficient { + type relative-difference; + description + "This is the ."; + reference + "8.2.17.3 of IEEE Std 1588-2019"; + } + + leaf enable { + type boolean; + description + "When this value is true, the mechanism to calculate + for certain media is enabled on + this PTP Port. When this value is false, this + mechanism is disabled on this PTP Port."; + reference + "8.2.17.4 of IEEE Std 1588-2019"; + } + } + + container description-port-ds { + description + "Provides descriptive information for the PTP Port."; + reference + "8.2.18 of IEEE Std 1588-2019"; + + leaf profile-identifier { + type string { + pattern "[0-9A-F]{2}(-[0-9A-F]{2}){5}"; + } + config false; + description + "When profile-identifier is supported, its value + shall identify the PTP Profile implemented by the + PTP Port, using the value assigned by the + organization that created the PTP Profile. + The profile identifier is six octets that identify + the PTP Profile's organization, profile within the + organization, and version. + Each octet is represented in YANG as a pair of + hexadecimal characters, using uppercase for a letter. + Each octet in the array is separated by the dash + character."; + reference + "8.2.18.2 of IEEE Std 1588-2019 + 20.3.3 of IEEE Std 1588-2019"; + } + + container protocol-address { + config false; + description + "Protocol address which is used as the source address + by the network transport protocol for this + PTP Port."; + reference + "8.2.18.3 of IEEE Std 1588-2019"; + uses port-address; + } + } + + container unicast-negotiation-port-ds { + if-feature unicast-negotiation; + description + "Provides management access to the optional unicast + negotiation mechanism."; + reference + "16.1 of IEEE Std 1588-2019"; + + leaf enable { + type boolean; + description + "When enable is false, the unicast negotiation + mechanism is disabled on this PTP Port. + When enable is true, the unicast negotiation + mechanism is enabled on this PTP Port."; + reference + "8.2.19.2 of IEEE Std 1588-2019"; + } + } + + container alternate-time-transmitter-port-ds { + if-feature alternate-time-transmitter; + description + "Provides management access to the optional alternate + timeTransmitter mechanism."; + reference + "17.3.3 of IEEE Std 1588-2019"; + + leaf number-of-alt-time-transmitters { + type uint8; + description + "Limits the number of PTP Ports that can + simultaneously transmit messages with the + alternate timeTransmitter flag set to TRUE."; + reference + "17.3.3.2.1 of IEEE Std 1588-2019"; + } + + leaf tx-alt-multicast-sync { + type boolean; + description + "Controls Sync transmission. If true and the + PTP Port is currently transmitting multicast + Announce messages with alternateTimeTransmitterFlag + TRUE, the PTP Port shall also transmit multicast + Sync and, if a two-step PTP Instance, + Follow_Up messages. Otherwise do not transmit + these messages."; + reference + "17.3.3.2.2 of IEEE Std 1588-2019"; + } + + leaf log-alt-multicast-sync-interval { + type int8; + description + "Logarithm to the base 2 of the mean interval + in seconds between Sync messages transmitted + under the terms of this alternate timeTransmitter + mechanism."; + reference + "17.3.3.2.3 of IEEE Std 1588-2019"; + } + } + + container unicast-discovery-port-ds { + if-feature unicast-discovery; + description + "Provides management access to the optional unicast + discovery mechanism."; + reference + "17.4.3 of IEEE Std 1588-2019"; + + leaf max-table-size { + type uint16; + config false; + description + "Maximum number of elements permitted + in the port-address list. + + NOTE - The actualTableSize of IEEE Std 1588 is not + applicable for YANG, since YANG mechanisms can be + used to control the number of elements in + port-address."; + } + + leaf log-query-interval { + type int8; + description + "Logarithm to the base 2 of the mean interval in + seconds between requests from a PTP Instance for + a unicast Announce message."; + } + + list port-address { + key "index"; + description + "List of port addresses for unicast discovery."; + + leaf index { + type uint16; + description + "Index to a port address in the list, typically + sequential from 0 to N-1, where N is the number of + port addresses."; + } + + uses port-address; + } + } + + container acceptable-time-transmitter-port-ds { + if-feature acceptable-time-transmitter; + description + "Provides management access to the optional + acceptable timeTransmitter mechanism."; + reference + "17.5.4 of IEEE Std 1588-2019"; + + leaf enable { + type boolean; + description + "When enable is false, the acceptable + timeTransmitter table option is not used on this + PTP Port, and the normal operation of the protocol + is in effect. + When enable is true, the acceptable timeTransmitter + table option is used on this PTP Port as specified + in the standard."; + reference + "17.5.4.2.1 of IEEE Std 1588-2019"; + } + } + + container l1-sync-basic-port-ds { + if-feature l1-sync; + description + "Provides data for operation of the optional layer-1 + based synchronization performance enhancement feature. + This data is required when the feature is supported."; + reference + "8.2.23 of IEEE Std 1588-2019 + L.5 of IEEE Std 1588-2019"; + + leaf enabled { + type boolean; + description + "Specifies whether the L1Sync option is enabled + on the PTP Port. If enabled is true, then the + L1Sync message exchange is supported and enabled."; + reference + "L.4.1 of IEEE Std 1588-2019"; + } + + leaf tx-coherent-is-required { + type boolean; + description + "Specifies whether the L1Sync port is required + to be a transmit coherent port."; + reference + "L.4.2 of IEEE Std 1588-2019"; + } + + leaf rx-coherent-is-required { + type boolean; + description + "Specifies whether the L1Sync port is required + to be a receive coherent port."; + reference + "L.4.3 of IEEE Std 1588-2019"; + } + + leaf congruent-is-required { + type boolean; + description + "Specifies whether the L1Sync port is required + to be a congruent port."; + reference + "L.4.4 of IEEE Std 1588-2019"; + } + + leaf opt-params-enabled { + type boolean; + description + "Specifies whether the L1Sync port transmitting + the L1_SYNC TLV extends this TLV with optional + parameters."; + reference + "L.4.5 of IEEE Std 1588-2019"; + } + + leaf log-l1sync-interval { + type int8; + description + "Logarithm to the base 2 of the mean IEEE Std 1588 + L1SyncInterval, the time interval between successive + periodic messages sent by the L1Sync port and + carrying the L1_SYNC TLV."; + reference + "L.4.6 of IEEE Std 1588-2019"; + } + + leaf l1sync-receipt-timeout { + type uint8; + description + "The intergral number of elapsed IEEE Std 1588 + L1SyncIntervals that must pass without receipt + of the L1_SYNC TLV before the L1_SYNC TLV + reception timeout occurs."; + reference + "L.4.7 of IEEE Std 1588-2019"; + } + + leaf link-alive { + type boolean; + config false; + description + "True when a L1_SYNC TLV is received at the PTP Port + and L1Sync is enaled on the PTP Port. False when the + L1_SYNC TLV reception timeout occurs."; + reference + "L.5.3.1 of IEEE Std 1588-2019"; + } + + leaf is-tx-coherent { + type boolean; + config false; + description + "True when the L1Sync port is a transmit coherent + port."; + reference + "L.5.3.2 of IEEE Std 1588-2019"; + } + + leaf is-rx-coherent { + type boolean; + config false; + description + "True when the L1Sync port is a receive coherent + port."; + reference + "L.5.3.3 of IEEE Std 1588-2019"; + } + + leaf is-congruent { + type boolean; + config false; + description + "True when the L1Sync port is a congruent port."; + reference + "L.5.3.4 of IEEE Std 1588-2019"; + } + + leaf l1sync-state { + type l1sync-state; + config false; + description + "Current state of the L1Sync state machine associated + with this L1Sync port."; + reference + "L.5.3.5 of IEEE Std 1588-2019"; + } + + leaf peer-tx-coherent-is-required { + type boolean; + config false; + description + "Specifies whether this L1Sync port is required + to be a transmit coherent port by a peer, + as indicated in the value of the TCR field of the + most recently received L1_SYNC TLV."; + reference + "L.5.3.6 of IEEE Std 1588-2019"; + } + + leaf peer-rx-coherent-is-required { + type boolean; + config false; + description + "Specifies whether this L1Sync port is required + to be a receive coherent port by a peer, + as indicated in the value of the RCR field of the + most recently received L1_SYNC TLV."; + reference + "L.5.3.7 of IEEE Std 1588-2019"; + } + + leaf peer-congruent-is-required { + type boolean; + config false; + description + "Specifies whether this L1Sync port is required + is required to be a congruent port by a peer, + as indicated in the value of the CR field of the + most recently received L1_SYNC TLV."; + reference + "L.5.3.8 of IEEE Std 1588-2019"; + } + + leaf peer-is-tx-coherent { + type boolean; + config false; + description + "True when the peer L1Sync port is a + transmit coherent port + (as received in the L1_SYNC TLV)."; + reference + "L.5.3.9 of IEEE Std 1588-2019"; + } + + leaf peer-is-rx-coherent { + type boolean; + config false; + description + "True when the peer L1Sync port is a + receive coherent port + (as received in the L1_SYNC TLV)."; + reference + "L.5.3.10 of IEEE Std 1588-2019"; + } + + leaf peer-is-congruent { + type boolean; + config false; + description + "True when the peer L1Sync port is a + congruent port + (as received in the L1_SYNC TLV)."; + reference + "L.5.3.11 of IEEE Std 1588-2019"; + } + } + + container l1-sync-opt-params-port-ds { + if-feature l1-sync; + description + "Provides data for operation of the optional layer-1 + based synchronization performance enhancement feature. + This data is optional when the feature is supported."; + reference + "8.2.24 of IEEE Std 1588-2019 + L.8.4 of IEEE Std 1588-2019"; + + leaf timestamps-corrected-tx { + type boolean; + description + "When true, the L1Sync port shall correct the + transmitted egress timestamps with the known value + of the phase offset, as indicated in the Link + Reference Model."; + reference + "L.8.4.2.1 of IEEE Std 1588-2019"; + } + + leaf phase-offset-tx-valid { + type boolean; + config false; + description + "True if and only if the values of the transmission + phase offset parameters (phase-offset-tx + and phase-offset-tx-timestamp) are valid."; + reference + "L.8.4.3.1 of IEEE Std 1588-2019"; + } + + leaf phase-offset-tx { + type time-interval; + config false; + description + "Transmission phase offset, which is the + time difference between the significant instant + with which the passage of the message timestamp + point through the reference plane is aligned, + and the time represented by the captured + timestamp of this passage of the message."; + reference + "L.8.4.3.3 of IEEE Std 1588-2019"; + } + + container phase-offset-tx-timestamp { + config false; + description + "Transmission phase offset timestamp + for the associated transmission phase offset."; + reference + "L.8.4.3.4 of IEEE Std 1588-2019"; + + uses timestamp; + } + + leaf frequency-offset-tx-valid { + type boolean; + config false; + description + "True if and only if the values of the transmission + frequency offset parameters (frequency-offset-tx + and frequency-offset-tx-timestamp) are valid."; + reference + "L.8.4.3.2 of IEEE Std 1588-2019"; + } + + leaf frequency-offset-tx { + type time-interval; + config false; + description + "Transmission frequency offset, multiplied + by one second. Transmission frequency offset + is the known rate of change of the transmission + phase offset."; + reference + "L.8.4.3.5 of IEEE Std 1588-2019"; + } + + container frequency-offset-tx-timestamp { + config false; + description + "Transmission frequency offset timestamp + for the associated transmission frequency + offset."; + reference + "L.8.4.3.6 of IEEE Std 1588-2019"; + + uses timestamp; + } + } + + container communication-cap-port-ds { + config false; + description + "Provides data for multicast/unicast communication + capabilities."; + reference + "8.2.25 of IEEE Std 1588-2019"; + + container sync { + description + "Communication capabilities of the PTP Port with + respect to sending Sync messages."; + + uses communication-capabilities; + } + + container delay-resp { + description + "Communication capabilities of the PTP Port with + respect to sending Delay_Resp messages."; + + uses communication-capabilities; + } + } + + container performance-monitoring-port-ds { + if-feature performance-monitoring; + description + "Provides data for the optional performance + monitoring mechanism, scoped to each PTP Port."; + reference + "8.2.26 of IEEE Std 1588-2019 + J.5.2 of IEEE Std 1588-2019"; + + list record-list-peer-delay { + key "index"; + config false; + max-elements 99; + description + "List of performance monitoring records for the + PTP Port that is using the peer-to-peer delay + measurement mehanism. The list is organized + as follows: + - 97 15-minute measurement records, the current + record at index 0, followed by the most recent + 96 records. + - 2 24-hour measurement records, the current record + at index 97, and the previous record at index 98. + + If a record is not implemented for a specific index, + management does not return the record. For example, + if only four 15-minute periods are implemented, + a management request for + performance-monitoring-port-ds/ + record-list-peer-delay[6] returns an error. + + If only some of the data is reported, the same index + values are used. As an example, if only the 24-hour + statistics are accessed, the indexes are still + 97 and 98. + + If a specific parameter (e.g. min-mean-link-delay) + is not implemented, management does not return the + parameter (i.e., error). Parameters that are invalid + (not measured correctly) shall be indicated with + one in all bits, except the most significant. This + represents the largest positive value of + time-interval, indicating a value outside the + maximum range."; + reference + "J.5.2.1 of IEEE Std 1588-2019"; + + uses port-performance-monitoring-peer-delay-data-record; + } + + list record-list { + key "index"; + config false; + max-elements 99; + description + "List of performance monitoring records for the + PTP Port, not specific to the peer-to-peer delay + measurement mehanism. The list is organized + as follows: + - 97 15-minute measurement records, the current + record at index 0, followed by the most recent + 96 records. + - 2 24-hour measurement records, the current record + at index 97, and the previous record at index 98. + + If a record is not implemented for a specific index, + management does not return the record. For example, + if only four 15-minute periods are implemented, + a management request for + performance-monitoring-port-ds/record-list[6] + returns an error. + + If only some of the data is reported, the same index + values are used. As an example, if only the 24-hour + statistics are accessed, the indexes are still + 97 and 98. + + If a specific parameter (e.g. sync-tx) + is not implemented, management does not return the + parameter (i.e., error). Parameters that are invalid + (not measured correctly) shall be indicated with + with the value zero, indicating that nothing was + counted. + + Each counter in the record shall be initialized to + zero at the start of a new 15-minute and + 24-hour interval."; + reference + "J.5.2.2 of IEEE Std 1588-2019"; + + uses port-performance-monitoring-data-record; + } + } + + container common-services-port-ds { + description + "Provides management access to the common services, + scoped to each PTP Port."; + reference + "16.6.5 of IEEE Std 1588-2019"; + + leaf cmlds-link-port-port-number { + if-feature cmlds; + type uint16; + config false; + description + "Common services operate on all PTP Instances + of the PTP Node. When a common service has + port-specific behavior, it specifies a Link Port, + which represents the physical port that the service + uses to transport PTP messages. In the context of + such a common service, the PTP Port represents a + logical port. + The Common Mean Link Delay Service (CMLDS) is + port-specific, and this leaf provides the + mapping of the PTP Port of this PTP Instance + to the corresponding Link Port in CMLDS. The + Link Port is identified using an IEEE Std 1588 + portNumber. The corresponding Link Port's + portNumber is located in the hierarchy at + /ptp/common-services/cmlds/ports/port[]/port-ds/ + port-identity/port-number."; + reference + "16.6.5.1.1.1 of IEEE Std 1588-2019"; + } + } + + container external-port-config-port-ds { + if-feature external-port-config; + description + "Provides management access to the external + configuration option, scoped to each PTP Port."; + reference + "17.6.3 of IEEE Std 1588-2019"; + + leaf desired-state { + type port-state; + description + "When the value of + default-ds/external-port-config-enable is true, + this desired-state is used to externally configure + the PTP Port's state (i.e., ../../port-ds/port-state) + to a desired value."; + reference + "17.6.3.2 of IEEE Std 1588-2019"; + } + } + + container time-receiver-monitoring-port-ds { + if-feature time-receiver-monitoring; + description + "Provides management access to the optional + TimeReceiver Event Monitor service, scoped to each + PTP Port."; + reference + "16.11.6 of IEEE Std 1588-2019"; + + leaf enable { + type bits { + bit time-receiver-rx-sync-timing-data { + position 0; + description + "True activates generation of the + TIME_RECEIVER_RX_SYNC_TIMING_DATA TLV."; + } + bit time-receiver-rx-sync-computed-data { + position 1; + description + "True activates generation of the + TIME_RECEIVER_RX_SYNC_COMPUTED_DATA TLV."; + } + bit time-receiver-tx-event-timestamps { + position 2; + description + "True activates generation of the + TIME_RECEIVER_TX_EVENT_TIMESTAMPS_DATA TLV."; + } + } + description + "Each bit (boolean flag) indicates whether + the data for a corresponding timeReceiver event + monitoring TLV is computed, and whether the data + is transmitted by the timeReceiver."; + reference + "16.11.6.2 of IEEE Std 1588-2019"; + } + + leaf events-per-rx-sync-timing-tlv { + type uint8; + description + "Indicates the number of events to report per + TIME_RECEIVER_RX_SYNC_TIMING_DATA TLV."; + reference + "16.11.6.3 of IEEE Std 1588-2019"; + } + + leaf events-per-rx-sync-computed-tlv { + type uint8; + description + "Indicates the number of events to report per + TIME_RECEIVER_RX_SYNC_COMPUTED_DATA TLV."; + reference + "16.11.6.4 of IEEE Std 1588-2019"; + } + + leaf events-per-tx-timestamps-tlv { + type uint8; + description + "Indicates the number of events to report per + TIME_RECEIVER_TX_EVENT_TIMESTAMPS_DATA TLV."; + reference + "16.11.6.5 of IEEE Std 1588-2019"; + } + + leaf tx-event-type { + type uint8; + description + "Indicates the event message type selected for + the egress event monitoring. The four low-order + bits are defined to correspond to the + IEEE Std 1588 messageType field."; + reference + "16.11.6.6 of IEEE Std 1588-2019"; + } + + leaf rx-sync-timing-tlv-message-m { + type uint8; + description + "The value M, where M indicates that every Mth + event message is selected for monitoring in the + TIME_RECEIVER_RX_SYNC_TIMING_DATA TLV. + For example, if the value of M is 4, every fourth + event message is selected for monitoring in + the TLV."; + reference + "16.11.6.7 of IEEE Std 1588-2019"; + } + + leaf rx-sync-computed-tlv-message-m { + type uint8; + description + "The value M, where M indicates that every Mth + event message is selected for monitoring in the + TIME_RECEIVER_RX_SYNC_COMPUTED_DATA TLV. + For example, if the value of M is 4, every fourth + event message is selected for monitoring in + the TLV."; + reference + "16.11.6.8 of IEEE Std 1588-2019"; + } + + leaf tx-timestamps-tlv-message-m { + type uint8; + description + "The value M, where M indicates that every Mth + event message is selected for monitoring in the + TIME_RECEIVER_TX_EVENT_TIMESTAMPS_DATA TLV. + For example, if the value of M is 4, every fourth + event message is selected for monitoring in + the TLV."; + reference + "16.11.6.9 of IEEE Std 1588-2019"; + } + } + } + } + } + } + + container transparent-clock-default-ds { + status deprecated; + description + "This default data set was specified in + IEEE Std 1588-2008, and under some interpretations, + it applied to all domains, which in turn means that it + represents multiple Transparent Clocks. + In IEEE Std 1588-2019, this data set is specified as + applying to the PTP Node (all domains), but the data set is + deprecated. For new designs, the standard recommends that + Transparent Clocks use the PTP Instance data sets + (i.e., /ptp/instances/instance[]), such that each + Transparent Clock supports a single PTP Instance and + domain."; + reference + "8.3.1 of IEEE Std 1588-2019"; + + leaf clock-identity { + type clock-identity; + config false; + status deprecated; + description + "The clockIdentity of the local clock."; + reference + "8.3.2.2.1 of IEEE Std 1588-2019"; + } + + leaf number-ports { + type uint16; + config false; + status deprecated; + description + "The number of PTP Ports of the device."; + reference + "8.3.2.2.2 of IEEE Std 1588-2019"; + } + + leaf delay-mechanism { + type delay-mechanism; + status deprecated; + description + "The propagation delay measuring mechanism (e2e or p2p)."; + reference + "8.3.2.3.1 of IEEE Std 1588-2019"; + } + + leaf primary-domain { + type uint8; + status deprecated; + description + "The domainNumber of the primary syntonization domain."; + reference + "8.3.2.3.2 of IEEE Std 1588-2019"; + } + } + + container transparent-clock-ports { + status deprecated; + description + "YANG container that is used to get all ports of the + IEEE Std 1588 transparentClockPortDS. + YANG does not allow get of all elements in a YANG list, + so a YANG container wrapping the YANG list is provided for + that purpose. The naming convention uses plural for the + wrapping YANG container, and singular for the YANG list."; + + list port { + key "port-index"; + status deprecated; + description + "This list of Transparent Clock port data sets was specified + in IEEE Std 1588-2008, and under some interpretations, + it applied to all domains, which in turn means that it + represents multiple Transparent Clocks. + In IEEE Std 1588-2019, this list is specified as + applying to the PTP Node (all domains), but the list is + deprecated. For new designs, the standard recommends that + Transparent Clocks use the PTP Instance data sets + (i.e., /ptp/instances/instance[]), such that each + Transparent Clock supports a single PTP Instance + and domain."; + reference + "8.3.1 of IEEE Std 1588-2019"; + + leaf port-index { + type uint16; + description + "The port list is indexed using a number that is + unique per port within the Transparent Clock, + applicable to the management context only + (i.e., not used in PTP messages)."; + } + + leaf underlying-interface { + type if:interface-ref; + description + "Reference to the configured underlying IETF YANG + interface that is used by this port for + transport of PTP messages. Among other data, + physical identifiers for the interface + (e.g. MAC address) can be obtained using this + reference."; + reference + "RFC 8343"; + } + + container port-ds { + description + "IEEE Std 1588 transparentClockPortDS."; + reference + "8.3.3 of IEEE Std 1588-2019"; + + container port-identity { + config false; + status deprecated; + description + "The IEEE Std 1588 portIdentity of this port."; + reference + "8.3.3.2.1 of IEEE Std 1588-2019"; + uses port-identity; + } + + leaf log-min-pdelay-req-interval { + type int8; + status deprecated; + description + "The logarithm to the base 2 of the + minPdelayReqInterval (minimum permitted mean time + interval between successive Pdelay_Req messages)."; + reference + "8.3.3.3.1 of IEEE Std 1588-2019"; + } + + leaf faulty-flag { + type boolean; + status deprecated; + description + "Shall be true if the port is faulty and false + if the port is operating normally."; + reference + "8.3.3.3.2 of IEEE Std 1588-2019"; + } + + leaf peer-mean-path-delay { + type time-interval; + config false; + status deprecated; + description + "An estimate of the current one-way propagation delay + on the link when the delayMechanism is P2P; otherwise, + it is zero."; + reference + "8.3.3.3.3 of IEEE Std 1588-2019"; + } + } + } + } + + container common-services { + description + "Provides management access to the common services. + Common services operate on all PTP Instances + of the PTP Node."; + + container cmlds { + if-feature cmlds; + description + "The Common Mean Link Delay Service (CMLDS) is an + optional service that enables any PTP Port that would + normally obtain the value of a link's + and using the peer-to-peer method + to instead obtain these values from this optional service. + The CMLDS service is available to all PTP Instances + communicating with a specific transport mechanism, + e.g. using Annex F, over the physical link between two PTP + Nodes. + + In this option, the term Link Port refers to the mechanism + enabling communication with a specific transport mechanism, + e.g. using Annex F, over the physical link between two PTP + Nodes. + + The Common Mean Link Delay Service is designed to run + independently from any PTP Instances communicating + over a Link Port. The service provides information on the + as well as the as the + measured in the timescale used by the service. The service + runs on every Link Port where the CMLDS is present. + Information required by a PTP Port is requested from and + delivered by the service running on the associated + Link Port."; + reference + "16.6.4 of IEEE Std 1588-2019"; + + container default-ds { + description + "The default data set of CMLDS."; + reference + "16.6.4.1 of IEEE Std 1588-2019"; + + leaf clock-identity { + type clock-identity; + config false; + description + "The IEEE Std 1588 clockIdentity used by CMLDS."; + reference + "16.6.4.1.2.1 of IEEE Std 1588-2019"; + } + + leaf number-link-ports { + type uint16; + config false; + description + "The number of Link Ports of CMLDS."; + reference + "16.6.4.1.2.2 of IEEE Std 1588-2019"; + } + } + + container ports { + description + "YANG container that is used to get all Link Ports + of CMLDS. + YANG does not allow get of all elements in a YANG list, + so a YANG container wrapping the YANG list is provided for + that purpose. The naming convention uses plural for the + wrapping YANG container, and singular for the YANG list."; + + list port { + key "port-index"; + description + "List of data for each Link Port of CMLDS. + The list is structured as leafs for each member + of the IEEE Std 1588 cmldsLinkPortDS (primary + Link Port data set), followed by containers for + each optional Link Port data set. Members of data set + cmldsLinkPortDS.commonMeanLinkDelayInformation + are listed directly under the list, in order + to keep the YANG naming hierarchy as short as + possible."; + reference + "16.6.4.2 of IEEE Std 1588-2019"; + + leaf port-index { + type uint16; + description + "The port list is indexed using a number that is + unique per Link Port within the CMLDS, applicable + to the management context only (i.e. not used in PTP + messages)."; + } + + leaf underlying-interface { + type if:interface-ref; + description + "Reference to the configured underlying IETF YANG + interface that is used by this Link Port for + transport of PTP messages. Among other data, + physical identifiers for the interface + (e.g. MAC address) can be obtained using this + reference."; + reference + "RFC 8343"; + } + + container link-port-ds { + description + "The IEEE Std 1588 cmldsLinkPortDS of this Link Port."; + reference + "16.6.4.2 of IEEE Std 1588-2019"; + + container port-identity { + config false; + description + "The IEEE Std 1588 portIdentity of this Link Port."; + reference + "16.6.4.2.2.1 of IEEE Std 1588-2019"; + uses port-identity; + } + + leaf domain-number { + type uint8; + config false; + description + "The IEEE Std 1588 domainNumber used by this + Link Port. This domain number is not configurable, + since its value is determined by the transport + mechanism of the Link Port."; + reference + "16.6.4.2.2.2 of IEEE Std 1588-2019"; + } + + leaf service-measurement-valid { + type boolean; + config false; + description + "This boolean is initialized to false, and will + be false whenever the required PTP messages for + CMLDS are not received on the Link Port. When + the required PTP messages for CMLDS are received, + this boolean is true. + This value is obtained from the + CommonMeanLinkDelayInformation structure returned + by CMLDS."; + reference + "16.6.3.2 of IEEE Std 1588-2019"; + } + + leaf mean-link-delay { + type time-interval; + config false; + description + "Estimate of the current one-way propagation delay + on the PTP Link, i.e., , attached + to this Link Port, computed using the peer-to-peer + delay mechanism. + This value is obtained from the + CommonMeanLinkDelayInformation structure returned + by CMLDS."; + reference + "16.6.3.2 of IEEE Std 1588-2019"; + } + + leaf scaled-neighbor-rate-ratio { + type int32; + config false; + description + "Ratio of the rate of this PTP Node's clock to + the clock of its neighbor attached + to this Link Port, i.e., , + scaled as specified in the standard. + This value is obtained from the + CommonMeanLinkDelayInformation structure returned + by CMLDS."; + reference + "16.6.3.2 of IEEE Std 1588-2019"; + } + + leaf log-min-pdelay-req-interval { + type int8; + description + "Logarithm to the base 2 of the IEEE Std 1588 + minPdelayReqInterval, the minimum permitted + mean time interval between successive Pdelay_Req + messages sent by CMLDS."; + reference + "16.6.4.2.4.1 of IEEE Std 1588-2019"; + } + + leaf version-number { + type uint8; + description + "The PTP major version in use on the Link Port. + NOTE - This indicates the version of the + IEEE 1588 standard, and not the version of an + applicable PTP Profile."; + reference + "16.6.4.2.4.2 of IEEE Std 1588-2019"; + } + + leaf minor-version-number { + type uint8; + description + "The PTP minor version in use on the Link Port. + NOTE - This indicates the version of the + IEEE 1588 standard, and not the version of an + applicable PTP Profile."; + reference + "16.6.4.2.4.3 of IEEE Std 1588-2019"; + } + + leaf delay-asymmetry { + type time-interval; + description + "The value of IEEE Std 1588 + applicable to the Link Port, which is the + difference in transmission time in one direction + as compared to the opposite direction."; + reference + "7.4.2 of IEEE Std 1588-2019 + 16.6.4.2.4.4 of IEEE Std 1588-2019"; + } + } + + container timestamp-correction-port-ds { + if-feature timestamp-correction; + description + "Provides access to the configurable correction of + timestamps provided to the PTP protocol."; + reference + "16.6.4.3 of IEEE Std 1588-2019"; + + leaf egress-latency { + type time-interval; + description + "Interval between the + provided for a PTP message and the time at which + the message timestamp point of the PTP message + crosses the reference plane."; + reference + "7.3.4.2 of IEEE Std 1588-2019 + 8.2.16.2 of IEEE Std 1588-2019"; + } + + leaf ingress-latency { + type time-interval; + description + "Interval between the time the message timestamp + point of an ingress PTP message crosses the + reference plane and the + provided for the PTP message."; + reference + "7.3.4.2 of IEEE Std 1588-2019 + 8.2.16.3 of IEEE Std 1588-2019"; + } + } + + container asymmetry-correction-port-ds { + if-feature asymmetry-correction; + description + "Provides access to asymmetry correction parameters + that are used to compute the value of + ."; + reference + "16.6.4.4 of IEEE Std 1588-2019"; + + leaf enable { + type boolean; + description + "When this value is true, the mechanism to calculate + for certain media is enabled on + this PTP Port. When this value is false, this + mechanism is disabled on this PTP Port."; + reference + "8.2.17.4 of IEEE Std 1588-2019"; + } + + leaf constant-asymmetry { + type time-interval; + description + "Constant asymmetry used to fine adjust the + dynamically calculated value of , + when the mechanism to calculate + or certain media is enabled."; + reference + "8.2.17.2 of IEEE Std 1588-2019"; + } + + leaf scaled-delay-coefficient { + type relative-difference; + description + "This is the ."; + reference + "8.2.17.3 of IEEE Std 1588-2019"; + } + } + + container performance-monitoring-port-ds { + if-feature performance-monitoring; + description + "Provides data for the optional performance + monitoring mechanism, scoped to each Link Port."; + reference + "16.6.4.5 of IEEE Std 1588-2019"; + + list record-list-peer-delay { + key "index"; + config false; + max-elements 99; + description + "List of performance monitoring records for the + Link Port that is using the peer-to-peer delay + measurement mehanism. The list is organized + as follows: + - 97 15-minute measurement records, the current + record at index 0, followed by the most recent + 96 records. + - 2 24-hour measurement records, the current record + at index 97, and the previous record at index 98. + + If a record is not implemented for a specific index, + management does not return the record. For example, + if only four 15-minute periods are implemented, + a management request for + performance-monitoring-port-ds/ + record-list-peer-delay[6] returns an error. + + If only some of the data is reported, the same index + values are used. As an example, if only the 24-hour + statistics are accessed, the indexes are still + 97 and 98. + + If a specific parameter (e.g. min-mean-link-delay) + is not implemented, management does not return the + parameter (i.e., error). Parameters that are invalid + (not measured correctly) shall be indicated with + one in all bits, except the most significant. This + represents the largest positive value of + time-interval, indicating a value outside the + maximum range."; + reference + "J.5.2.1 of IEEE Std 1588-2019"; + + uses port-performance-monitoring-peer-delay-data-record; + } + + list record-list { + key "index"; + config false; + max-elements 99; + description + "List of performance monitoring records for the + Link Port, not specific to the peer-to-peer delay + measurement mehanism. The list is organized + as follows: + - 97 15-minute measurement records, the current + record at index 0, followed by the most recent + 96 records. + - 2 24-hour measurement records, the current record + at index 97, and the previous record at index 98. + + If a record is not implemented for a specific index, + management does not return the record. For example, + if only four 15-minute periods are implemented, + a management request for + performance-monitoring-port-ds/record-list[6] + returns an error. + + If only some of the data is reported, the same index + values are used. As an example, if only the 24-hour + statistics are accessed, the indexes are still + 97 and 98. + + If a specific parameter (e.g. sync-tx) + is not implemented, management does not return the + parameter (i.e., error). Parameters that are invalid + (not measured correctly) shall be indicated with + with the value zero, indicating that nothing was + counted. + + Each counter in the record shall be initialized to + zero at the start of a new 15-minute and + 24-hour interval."; + reference + "J.5.2.2 of IEEE Std 1588-2019"; + + uses port-performance-monitoring-data-record; + } + } + } + } + } + } + } +} diff --git a/src/confd/yang/confd/ieee802-dot1as-gptp@2025-12-10.yang b/src/confd/yang/confd/ieee802-dot1as-gptp@2025-12-10.yang new file mode 100644 index 000000000..af2e94cc9 --- /dev/null +++ b/src/confd/yang/confd/ieee802-dot1as-gptp@2025-12-10.yang @@ -0,0 +1,1301 @@ +module ieee802-dot1as-gptp { + yang-version "1.1"; + namespace urn:ieee:std:802.1AS:yang:ieee802-dot1as-gptp; + prefix dot1as-gptp; + + import ietf-yang-types { + prefix yang; + } + import ieee1588-ptp-tt { + prefix ptp-tt; + } + + organization + "IEEE 802.1 Working Group"; + contact + "WG-URL: http://ieee802.org/1/ + WG-EMail: stds-802-1-l@ieee.org + + Contact: IEEE 802.1 Working Group Chair + Postal: C/O IEEE 802.1 Working Group + IEEE Standards Association + 445 Hoes Lane + Piscataway, NJ 08854 + USA + + E-mail: stds-802-1-chairs@ieee.org"; + description + "Management objects that control timing and synchronization for + time sensitive applications, as specified in Clause 14 of + IEEE Std 802.1AS-2025. + + Copyright (C) IEEE (2025). This version of this YANG module is + part of IEEE Std 802.1AS-2025; see the standard itself for full + legal notices."; + + revision 2025-12-10 { + description + "Published as part of IEEE Std 802.1AS-2025."; + reference + "IEEE Std 802.1AS - Timing and Synchronization for + Time-Sensitive Applications: IEEE Std 802.1AS-2025. + IEEE Std 1588 - IEEE Standard for a Precision Clock + Synchronization Protocol for Networked Measurement and + Control Systems: IEEE Std 1588-2019, IEEE Std 1588g-2022, + IEEE Std 1588e-2024."; + } + typedef scaled-ns { + type string { + pattern "[0-9A-F]{2}(-[0-9A-F]{2}){11}"; + } + description + "The IEEE Std 802.1AS ScaledNs type represents signed values + of time and time interval in units of 2^16 ns, as a signed + 96-bit integer. Each of the 12 octets is represented as a + pair of hexadecimal characters, using uppercase for a letter. + Octets are separated by a dash character. The most + significant octet is first."; + reference + "6.4.3.1 of IEEE Std 802.1AS"; + } + typedef uscaled-ns { + type string { + pattern "[0-9A-F]{2}(-[0-9A-F]{2}){11}"; + } + description + "The IEEE Std 802.1AS UScaledNs type represents unsigned + values of time and time interval in units of 2^16 ns, as an + unsigned 96-bit integer. Each of the 12 octets is represented + as a pair of hexadecimal characters, using uppercase for a + letter. Octets are separated by a dash character. The most + significant octet is first."; + reference + "6.4.3.2 of IEEE Std 802.1AS"; + } + typedef float64 { + type string { + pattern "[0-9A-F]{2}(-[0-9A-F]{2}){7}"; + } + description + "The IEEE Std 802.1AS Float64 type represents IEEE Std 754 + binary64. Each of the 8 octets is represented as a pair of + hexadecimal characters, using uppercase for a letter. Octets + are separated by a dash character. The most significant octet + is first."; + reference + "6.4.2 of IEEE Std 802.1AS"; + } + typedef uinteger48 { + type uint64 { + range "0..281474976710655"; + } + description + "48-bit unsigned integer data type."; + reference + "6.4.2 of IEEE Std 802.1AS"; + } + + augment "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:default-ds" { + description + "Augment IEEE Std 1588 defaultDS."; + leaf gm-capable { + type boolean; + config false; + description + "The value is true if the time-aware system is capable of + being a Grandmaster, and false if the time-aware system is + not capable of being a Grandmaster."; + reference + "14.2.7 of IEEE Std 802.1AS"; + } + leaf current-utc-offset { + when + "../current-utc-offset-valid='true'"; + type int16; + config false; + description + "Offset from UTC (TAI - UTC). The offset is in units of + seconds. This leaf applies to the ClockTimeTransmitter + entity (i.e., local only, unrelated to a remote GM)."; + reference + "14.2.8 of IEEE Std 802.1AS"; + } + leaf current-utc-offset-valid { + type boolean; + config false; + description + "The value of current-utc-offset-valid shall be true if the + value of current-utc-offset is known to be correct, + otherwise it shall be false. This leaf applies to the + ClockTimeTransmitter entity (i.e., local only, unrelated to + a remote GM)."; + reference + "14.2.9 of IEEE Std 802.1AS"; + } + leaf leap59 { + type boolean; + config false; + description + "If the timescale is PTP, a true value for leap59 shall + indicate that the last minute of the current UTC day + contains 59 seconds. If the timescale is not PTP, the value + shall be false. This leaf applies to the + ClockTimeTransmitter entity (i.e., local only, unrelated to + a remote GM)."; + reference + "14.2.10 of IEEE Std 802.1AS"; + } + leaf leap61 { + type boolean; + config false; + description + "If the timescale is PTP, a true value for leap61 shall + indicate that the last minute of the current UTC day + contains 61 seconds. If the timescale is not PTP, the value + shall be false. This leaf applies to the + ClockTimeTransmitter entity (i.e., local only, unrelated to + a remote GM)."; + reference + "14.2.11 of IEEE Std 802.1AS"; + } + leaf time-traceable { + type boolean; + config false; + description + "The value of time-traceable shall be true if the timescale + is traceable to a primary reference; otherwise, the value + shall be false. This leaf applies to the + ClockTimeTransmitter entity (i.e., local only, unrelated to + a remote GM)."; + reference + "14.2.12 of IEEE Std 802.1AS"; + } + leaf frequency-traceable { + type boolean; + config false; + description + "The value of frequency-traceable shall be true if the + frequency determining the timescale is traceable to a + primary reference; otherwise, the value shall be false. + This leaf applies to the ClockTimeTransmitter entity + (i.e., local only, unrelated to a remote GM)."; + reference + "14.2.13 of IEEE Std 802.1AS"; + } + leaf ptp-timescale { + type boolean; + config false; + description + "If ptp-timescale is true, the timescale of the + ClockTimeTransmitter entity is PTP, which is the elapsed + time since the PTP epoch measured using the second defined + by International Atomic Time (TAI). If ptp-timescale is + false, the timescale of the ClockTimeTransmitter entity is + ARB, which is the elapsed time since an arbitrary epoch. + This leaf applies to the ClockTimeTransmitter entity + (i.e., local only, unrelated to a remote GM)."; + reference + "14.2.14 of IEEE Std 802.1AS"; + } + leaf time-source { + type identityref { + base ptp-tt:time-source; + } + config false; + description + "The source of time used by the Grandmaster Clock. This leaf + applies to the ClockTimeTransmitter entity (i.e., local + only, unrelated to a remote GM)."; + reference + "14.2.15 of IEEE Std 802.1AS"; + } + } + + augment "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:current-ds" { + description + "Augment IEEE Std 1588 currentDS."; + leaf last-gm-phase-change { + type scaled-ns; + config false; + description + "Phase change that occurred on the most recent change in + either the Grandmaster PTP Instance or + gm-timebase-indicator leaf."; + reference + "14.3.4 of IEEE Std 802.1AS"; + } + leaf last-gm-freq-change { + type float64; + config false; + description + "Frequency change that occurred on the most recent change in + either the Grandmaster PTP Instance or + gm-timebase-indicator leaf."; + reference + "14.3.5 of IEEE Std 802.1AS"; + } + leaf gm-timebase-indicator { + type uint16; + config false; + description + "The timeBaseIndicator of the current Grandmaster PTP + Instance."; + reference + "14.3.6 of IEEE Std 802.1AS"; + } + leaf gm-change-count { + type yang:counter32; + config false; + description + "This statistics counter tracks the number of times the + Grandmaster PTP Instance has changed in a gPTP domain."; + reference + "14.3.7 of IEEE Std 802.1AS"; + } + leaf time-of-last-gm-change { + type yang:timestamp; + config false; + description + "System time when the most recent Grandmaster Clock change + occurred in a gPTP domain. This leaf's type is YANG + timestamp, which is based on system time. System time is an + unsigned integer in units of 10 milliseconds, using an + epoch defined by the implementation (typically time of + boot-up)."; + reference + "14.3.8 of IEEE Std 802.1AS"; + } + leaf time-of-last-phase-change { + type yang:timestamp; + config false; + description + "System time when the most recent change in Grandmaster + Clock phase occurred. This leaf's type is YANG timestamp, + which is based on system time. System time is an unsigned + integer in units of 10 milliseconds, using an epoch defined + by the implementation (typically time of boot-up)."; + reference + "14.3.9 of IEEE Std 802.1AS"; + } + leaf time-of-last-freq-change { + type yang:timestamp; + config false; + description + "System time when the most recent change in Grandmaster + Clock frequency occurred. This leaf's type is YANG + timestamp, which is based on system time. System time is an + unsigned integer in units of 10 milliseconds, using an + epoch defined by the implementation (typically time of + boot-up)."; + reference + "14.3.10 of IEEE Std 802.1AS"; + } + } + + augment "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:parent-ds" { + description + "Augment IEEE Std 1588 parentDS."; + leaf cumulative-rate-ratio { + type int32; + config false; + description + "Estimate of the ratio of the frequency of the Grandmaster + Clock to the frequency of the LocalClock entity of this PTP + Instance. cumulative-rate-ratio is expressed as the + fractional frequency offset multiplied by 2^41, i.e., the + quantity (rateRatio - 1.0)(2^41)."; + reference + "14.4.3 of IEEE Std 802.1AS"; + } + } + + augment + "/ptp-tt:ptp"+ + "/ptp-tt:instances"+ + "/ptp-tt:instance"+ + "/ptp-tt:ports"+ + "/ptp-tt:port"+ + "/ptp-tt:port-ds" { + description + "Augment IEEE Std 1588 portDS. + + 14.10.4 of IEEE Std 802.1AS specifies ptpPortEnabled + (ptp-port-enabled), which is provided in YANG as the + semantically equivalent node in ieee1588-ptp-tt named + port-enable (in port-ds). + + 14.10.16 of IEEE Std 802.1AS specifies + mgtSettableLogAnnounceInterval (mgt-log-announce-interval), + which is provided in YANG as the semantically equivalent node + in ieee1588-ptp-tt named log-announce-interval (in port-ds). + In the context of IEEE Std 802.1AS, log-announce-interval + cannot be used unless use-mgt-log-announce-interval is true. + + 14.10.21 of IEEE Std 802.1AS specifies + mgtSettableLogSyncInterval (mgt-log-sync-interval), which is + provided in YANG as the semantically equivalent node in + ieee1588-ptp-tt named log-sync-interval (in port-ds). In the + context of IEEE Std 802.1AS, log-sync-interval cannot be used + unless use-mgt-log-sync-interval is true."; + leaf is-measuring-delay { + type boolean; + config false; + description + "Boolean that is true if the port is measuring PTP Link + propagation delay."; + reference + "14.10.6 of IEEE Std 802.1AS"; + } + leaf as-capable { + type boolean; + config false; + description + "Boolean that is true if and only if it is determined that + this PTP Instance and the PTP Instance at the other end of + the link attached to this port can interoperate with each + other via the IEEE Std 802.1AS protocol."; + reference + "10.2.5.1 of IEEE Std 802.1AS + 14.10.7 of IEEE Std 802.1AS"; + } + leaf mean-link-delay-thresh { + type ptp-tt:time-interval; + description + "Propagation time threshold for mean-link-delay, above which + a port is not considered capable of participating in the + IEEE Std 802.1AS protocol."; + reference + "14.10.9 of IEEE Std 802.1AS"; + } + leaf neg-mean-link-delay-thresh { + type ptp-tt:time-interval; + description + "The negative propagation time threshold for + mean-link-delay, below which a port is not considered + capable of participating in the IEEE Std 802.1AS + protocol."; + reference + "14.10.10 of IEEE Std 802.1AS"; + } + leaf neighbor-rate-ratio { + type int32; + config false; + description + "Estimate of the ratio of the frequency of the LocalClock + entity of the PTP Instance at the other end of the link + attached to this PTP Port, to the frequency of the + LocalClock entity of this PTP Instance. neighbor-rate-ratio + is expressed as the fractional frequency offset multiplied + by 2^41, i.e., the quantity (rateRatio - 1.0)(2^41)."; + reference + "14.10.12 of IEEE Std 802.1AS"; + } + leaf initial-log-announce-interval { + type int8; + description + "When use-mgt-log-announce-interval is false (i.e., change + with Signaling message), this is the logarithm to base 2 of + the announce interval used when the port is initialized."; + reference + "14.10.13 of IEEE Std 802.1AS"; + } + leaf current-log-announce-interval { + type int8; + config false; + description + "Logarithm to base 2 of the current announce interval."; + reference + "14.10.14 of IEEE Std 802.1AS"; + } + leaf use-mgt-log-announce-interval { + type boolean; + description + "Boolean that determines the source of the announce + interval. If the value is true, the announce interval + (current-log-announce-interval) is set equal to the value + of mgt-log-announce-interval. If the value is false, the + announce interval is determined by the + AnnounceIntervalSetting state machine (i.e., changed with + Signaling message)."; + reference + "14.10.15 of IEEE Std 802.1AS"; + } + leaf initial-log-sync-interval { + type int8; + description + "When use-mgt-log-sync-interval is false (i.e., change with + Signaling message), this is the logarithm to base 2 of the + sync interval used when the port is initialized."; + reference + "14.10.18 of IEEE Std 802.1AS"; + } + leaf current-log-sync-interval { + type int8; + config false; + description + "Logarithm to base 2 of the current sync interval."; + reference + "14.10.19 of IEEE Std 802.1AS"; + } + leaf use-mgt-log-sync-interval { + type boolean; + description + "Boolean that determines the source of the sync interval. + If the value is true, the sync interval + (current-log-sync-interval) is set equal to the value of + mgt-log-sync-interval. If the value is false, the sync + interval is determined by the SyncIntervalSetting + state machine (i.e., changed with Signaling message)."; + reference + "14.10.20 of IEEE Std 802.1AS"; + } + leaf sync-receipt-timeout { + type uint8; + description + "Number of sync intervals that a timeReceiver port waits + without receiving synchronization information, before + assuming that the timeTransmitter is no longer transmitting + synchronization information and that the BTCA needs to be + run, if appropriate."; + reference + "14.10.22 of IEEE Std 802.1AS"; + } + leaf sync-receipt-timeout-interval { + type uscaled-ns; + config false; + description + "Time interval after which sync receipt timeout occurs if + time-synchronization information has not been received + during the interval."; + reference + "14.10.23 of IEEE Std 802.1AS"; + } + leaf initial-log-pdelay-req-interval { + type int8; + description + "When use-mgt-log-pdelay-req-interval is false (i.e., change + with Signaling message), this is the logarithm to base 2 of + the Pdelay_Req transmit interval used when the port is + initialized."; + reference + "14.10.24 of IEEE Std 802.1AS"; + } + leaf current-log-pdelay-req-interval { + type int8; + config false; + description + "Logarithm to base 2 of the current Pdelay_Req transmit + interval."; + reference + "14.10.25 of IEEE Std 802.1AS"; + } + leaf use-mgt-log-pdelay-req-interval { + type boolean; + description + "Boolean that determines the source of the Pdelay_Req + transmit interval. If the value is true, the Pdelay_Req + transmit interval (current-log-pdelay-req-interval) is set + equal to the value of mgt-log-pdelay-req-interval. If the + value is false, the Pdelay_Req transmit interval is + determined by the LinkDelayIntervalSetting state machine + (i.e., changed with Signaling message)."; + reference + "14.10.26 of IEEE Std 802.1AS"; + } + leaf mgt-log-pdelay-req-interval { + type int8; + description + "Logarithm to base 2 of the Pdelay_Req transmit interval, + used if use-mgt-log-pdelay-req-interval is true. This value + is not used if use-mgt-log-pdelay-req-interval is false."; + reference + "14.10.27 of IEEE Std 802.1AS"; + } + leaf initial-log-gptp-cap-interval { + type int8; + description + "When use-mgt-log-gptp-cap-interval is false (i.e., change + with Signaling message), this is the logarithm to base 2 of + the gPTP capable message interval used when the port is + initialized."; + reference + "14.10.28 of IEEE Std 802.1AS"; + } + leaf current-log-gptp-cap-interval { + type int8; + config false; + description + "Logarithm to base 2 of the current gPTP capable message + interval."; + reference + "14.10.29 of IEEE Std 802.1AS"; + } + leaf use-mgt-log-gptp-cap-interval { + type boolean; + description + "Boolean that determines the source of the gPTP capable + message interval. If the value is true, the gPTP capable + message interval (current-log-gptp-cap-interval) is set + equal to the value of mgt-gptp-cap-req-interval. If the + value is false, the gPTP capable message interval is + determined by the GptpCapableMessageIntervalSetting state + machine (i.e., changed with Signaling message)."; + reference + "14.10.30 of IEEE Std 802.1AS"; + } + leaf mgt-log-gptp-cap-interval { + type int8; + description + "Logarithm to base 2 of the gPTP capable message interval, + used if use-mgt-log-gptp-cap-interval is true. This value + is not used if use-mgt-log-pdelay-req-interval is false."; + reference + "14.10.31 of IEEE Std 802.1AS"; + } + leaf initial-compute-neighbor-rate-ratio { + type boolean; + description + "When use-mgt-compute-neighbor-rate-ratio is false + (i.e., change with Signaling message), this is the initial + value of computeNeighborRateRatio."; + reference + "14.10.32 of IEEE Std 802.1AS"; + } + leaf current-compute-neighbor-rate-ratio { + type boolean; + config false; + description + "Current value of computeNeighborRateRatio."; + reference + "14.10.33 of IEEE Std 802.1AS"; + } + leaf use-mgt-compute-neighbor-rate-ratio { + type boolean; + description + "Boolean that determines the source of + computeNeighborRateRatio. If the value is true, + computeNeighborRateRatio is set equal to the value of + mgt-compute-neighbor-rate-ratio. If the value is false, + computeNeighborRateRatio is determined by the + LinkDelayIntervalSetting state machine (i.e., changed with + Signaling message)."; + reference + "14.10.34 of IEEE Std 802.1AS"; + } + leaf mgt-compute-neighbor-rate-ratio { + type boolean; + description + "Value of computeNeighborRateRatio, used if + use-mgt-compute-neighbor-rate-ratio is true. This value is + not used if use-mgt-compute-neighbor-rate-ratio is false."; + reference + "14.10.35 of IEEE Std 802.1AS"; + } + leaf initial-compute-mean-link-delay { + type boolean; + description + "When use-mgt-compute-mean-link-delay is false (i.e., change + with Signaling message), this is the initial value of + computeMeanLinkDelay."; + reference + "14.10.36 of IEEE Std 802.1AS"; + } + leaf current-compute-mean-link-delay { + type boolean; + config false; + description + "Current value of computeMeanLinkDelay."; + reference + "14.10.37 of IEEE Std 802.1AS"; + } + leaf use-mgt-compute-mean-link-delay { + type boolean; + description + "Boolean that determines the source of computeMeanLinkDelay. + If the value is true, computeMeanLinkDelay is set equal to + the value of mgt-compute-mean-link-delay. If the value is + false, computeMeanLinkDelay is determined by the + LinkDelayIntervalSetting state machine (i.e., changed with + Signaling message)."; + reference + "14.10.38 of IEEE Std 802.1AS"; + } + leaf mgt-compute-mean-link-delay { + type boolean; + description + "Value of computeMeanLinkDelay, used if + use-mgt-compute-mean-link-delay is true. This value is not + used if use-mgt-compute-mean-link-delay is false."; + reference + "14.10.39 of IEEE Std 802.1AS"; + } + leaf allowed-lost-responses { + type uint8; + description + "Number of Pdelay_Req messages for which a valid response is + not received, above which a port is considered to not be + exchanging peer delay messages with its neighbor."; + reference + "14.10.40 of IEEE Std 802.1AS"; + } + leaf allowed-faults { + type uint8; + description + "Number of faults above which asCapable is set to false."; + reference + "14.10.41 of IEEE Std 802.1AS"; + } + leaf gptp-cap-receipt-timeout { + type uint8; + description + "Number of transmission intervals that a port waits without + receiving the gPTP-capable TLV, before assuming that the + neighbor port is no longer invoking the gPTP protocol."; + reference + "14.10.42 of IEEE Std 802.1AS"; + } + leaf nup { + type float64; + description + "For an OLT port of an IEEE Std 802.3 EPON link, this value + is the effective index of refraction for the EPON upstream + wavelength light of the optical path."; + reference + "14.10.44 of IEEE Std 802.1AS"; + } + leaf ndown { + type float64; + description + "For an OLT port of an IEEE 802.3 EPON link, this value is + the effective index of refraction for the EPON downstream + wavelength light of the optical path."; + reference + "14.10.45 of IEEE Std 802.1AS"; + } + leaf one-step-tx-oper { + type boolean; + config false; + description + "This value is true if the port is sending one-step Sync + messages, and false if the port is sending two-step Sync + and Follow_Up messages."; + reference + "14.10.46 of IEEE Std 802.1AS"; + } + leaf one-step-receive { + type boolean; + config false; + description + "This value is true if the port is capable of receiving and + processing one-step Sync messages."; + reference + "14.10.47 of IEEE Std 802.1AS"; + } + leaf one-step-transmit { + type boolean; + config false; + description + "This value is true if the port is capable of transmitting + one-step Sync messages."; + reference + "14.10.48 of IEEE Std 802.1AS"; + } + leaf initial-one-step-tx-oper { + type boolean; + description + "When use-mgt-one-step-tx-oper is false (i.e., change with + Signaling message), this is the initial value of + current-one-step-tx-oper."; + reference + "14.10.49 of IEEE Std 802.1AS"; + } + leaf current-one-step-tx-oper { + type boolean; + config false; + description + "This value is true if the port is configured to transmit + one-step Sync messages, either via management + (mgt-one-step-tx-oper) or Signaling. If both + current-one-step-tx-oper and one-step-transmit are true, + the port transmits one-step Sync messages + (i.e., one-step-tx-oper true)."; + reference + "14.10.50 of IEEE Std 802.1AS"; + } + leaf use-mgt-one-step-tx-oper { + type boolean; + description + "Boolean that determines the source of + current-one-step-tx-oper. If the value is true, + current-one-step-tx-oper is set equal to the value of + mgt-one-step-tx-oper. If the value is false, + current-one-step-tx-oper is determined by the + OneStepTxOperSetting state machine (i.e., changed with + Signaling message)."; + reference + "14.10.51 of IEEE Std 802.1AS"; + } + leaf mgt-one-step-tx-oper { + type boolean; + description + "If use-mgt-one-step-tx-oper is true, + current-one-step-tx-oper is set equal to this value. This + value is not used if use-mgt-one-step-tx-oper is false."; + reference + "14.10.52 of IEEE Std 802.1AS"; + } + leaf sync-locked { + type boolean; + config false; + description + "This value is true if the port will transmit a Sync as soon + as possible after the timeReceiver port receives a Sync + message."; + reference + "14.10.53 of IEEE Std 802.1AS"; + } + leaf-list pdelay-truncated-timestamps { + type uinteger48; + config false; + description + "For full-duplex IEEE Std 802.3 media, and CSN media that + use the peer-to-peer delay mechanism to measure path delay, + the values of the four elements of this leaf-list + correspond to the timestamps t1, t2, t3, and t4, listed in + that order. Each timestamp is expressed in units of + 2^-16 ns (i.e., the value of each array element is equal to + the remainder obtained upon dividing the respective + timestamp, expressed in units of 2^-16 ns, by 2^48). + At any given time, the timestamp values stored in the array + are for the same, and most recently completed, peer delay + message exchange. For each timestamp, only 48-bits are + valid (the upper 16-bits are always zero)."; + reference + "14.10.54 of IEEE Std 802.1AS"; + } + } + + augment + "/ptp-tt:ptp"+ + "/ptp-tt:instances"+ + "/ptp-tt:instance"+ + "/ptp-tt:ports"+ + "/ptp-tt:port" { + description + "Augment to add port-statistics-ds to IEEE Std 1588 PTP + Port."; + container port-statistics-ds { + description + "Provides counters associated with the port of the PTP + Instance."; + reference + "14.12 of IEEE Std 802.1AS"; + leaf rx-sync-count { + type yang:counter32; + config false; + description + "Counter that increments every time synchronization + information is received."; + reference + "14.12.2 of IEEE Std 802.1AS"; + } + leaf rx-one-step-sync-count { + type yang:counter32; + config false; + description + "Counter that increments every time a one-step Sync + message is received."; + reference + "14.12.3 of IEEE Std 802.1AS"; + } + leaf rx-follow-up-count { + type yang:counter32; + config false; + description + "Counter that increments every time a Follow_Up message is + received."; + reference + "14.12.4 of IEEE Std 802.1AS"; + } + leaf rx-pdelay-req-count { + type yang:counter32; + config false; + description + "Counter that increments every time a Pdelay_Req message + is received."; + reference + "14.12.5 of IEEE Std 802.1AS"; + } + leaf rx-pdelay-resp-count { + type yang:counter32; + config false; + description + "Counter that increments every time a Pdelay_Resp message + is received."; + reference + "14.12.6 of IEEE Std 802.1AS"; + } + leaf rx-pdelay-resp-follow-up-count { + type yang:counter32; + config false; + description + "Counter that increments every time a + Pdelay_Resp_Follow_Up message is received."; + reference + "14.12.7 of IEEE Std 802.1AS"; + } + leaf rx-announce-count { + type yang:counter32; + config false; + description + "Counter that increments every time an Announce message is + received."; + reference + "14.12.8 of IEEE Std 802.1AS"; + } + leaf rx-packet-discard-count { + type yang:counter32; + config false; + description + "Counter that increments every time a PTP message of the + respective PTP Instance is discarded."; + reference + "14.12.9 of IEEE Std 802.1AS"; + } + leaf sync-receipt-timeout-count { + type yang:counter32; + config false; + description + "Counter that increments every time a sync receipt timeout + occurs."; + reference + "14.12.10 of IEEE Std 802.1AS"; + } + leaf announce-receipt-timeout-count { + type yang:counter32; + config false; + description + "Counter that increments every time an announce receipt + timeout occurs."; + reference + "14.12.11 of IEEE Std 802.1AS"; + } + leaf pdelay-allowed-lost-exceeded-count { + type yang:counter32; + config false; + description + "Counter that increments every time the value of the + variable lostResponses exceeds the value of the variable + allowedLostResponses, in the RESET state of the + MDPdelayReq state machine."; + reference + "14.12.12 of IEEE Std 802.1AS"; + } + leaf tx-sync-count { + type yang:counter32; + config false; + description + "Counter that increments every time synchronization + information is transmitted."; + reference + "14.12.13 of IEEE Std 802.1AS"; + } + leaf tx-one-step-sync-count { + type yang:counter32; + config false; + description + "Counter that increments every time a one-step Sync + message is transmitted."; + reference + "14.12.14 of IEEE Std 802.1AS"; + } + leaf tx-follow-up-count { + type yang:counter32; + config false; + description + "Counter that increments every time a Follow_Up message is + transmitted."; + reference + "14.12.15 of IEEE Std 802.1AS"; + } + leaf tx-pdelay-req-count { + type yang:counter32; + config false; + description + "Counter that increments every time a Pdelay_Req message + is transmitted."; + reference + "14.12.16 of IEEE Std 802.1AS"; + } + leaf tx-pdelay-resp-count { + type yang:counter32; + config false; + description + "Counter that increments every time a Pdelay_Resp message + is transmitted."; + reference + "14.12.17 of IEEE Std 802.1AS"; + } + leaf tx-pdelay-resp-follow-up-count { + type yang:counter32; + config false; + description + "Counter that increments every time a + Pdelay_Resp_Follow_Up message is transmitted."; + reference + "14.12.18 of IEEE Std 802.1AS"; + } + leaf tx-announce-count { + type yang:counter32; + config false; + description + "Counter that increments every time an Announce message is + transmitted."; + reference + "14.12.19 of IEEE Std 802.1AS"; + } + } + } + + augment + "/ptp-tt:ptp"+ + "/ptp-tt:instances"+ + "/ptp-tt:instance"+ + "/ptp-tt:ports"+ + "/ptp-tt:port" { + description + "Augment to add asymmetry-measurement-mode-ds to IEEE Std 1588 + PTP Port."; + container asymmetry-measurement-mode-ds { + description + "Represents the capability to enable/disable the Asymmetry + Compensation Measurement Procedure on a PTP Port. This data + set is used instead of the CMLDS + asymmetry-measurement-mode-ds when only a single PTP + Instance is present (i.e., CMLDS is not used)."; + reference + "14.15 of IEEE Std 802.1AS + Annex G of IEEE Std 802.1AS"; + leaf enabled { + type boolean; + description + "For full-duplex IEEE Std 802.3 media, the value is true + if an asymmetry measurement is being performed for the + link attached to this PTP Port, and false otherwise. For + all other media, the value shall be false."; + reference + "14.15.2 of IEEE Std 802.1AS"; + } + } + } + + augment + "/ptp-tt:ptp"+ + "/ptp-tt:common-services"+ + "/ptp-tt:cmlds"+ + "/ptp-tt:ports"+ + "/ptp-tt:port"+ + "/ptp-tt:link-port-ds" { + description + "Augment IEEE Std 1588 cmldsLinkPortDS."; + leaf cmlds-link-port-enabled { + type boolean; + config false; + description + "Boolean that is true if both delay-mechanism is common-p2p + and the value of ptp-port-enabled is true, for at least one + PTP Port that uses the CMLDS; otherwise, the value is + false."; + reference + "11.2.18.1 of IEEE Std 802.1AS + 14.18.3 of IEEE Std 802.1AS"; + } + leaf is-measuring-delay { + type boolean; + config false; + description + "This leaf is analogous to is-measuring-delay for a PTP + Port, but applicable to this Link Port."; + reference + "14.18.4 of IEEE Std 802.1AS"; + } + leaf as-capable-across-domains { + type boolean; + config false; + description + "This leaf is true when all PTP Instances (domains) for this + Link Port detect proper exchange of Pdelay messages."; + reference + "11.2.2 of IEEE Std 802.1AS + 14.18.5 of IEEE Std 802.1AS"; + } + leaf mean-link-delay-thresh { + type ptp-tt:time-interval; + description + "Propagation time threshold for mean-link-delay, above which + a Link Port is not considered capable of participating in + the IEEE Std 802.1AS protocol."; + reference + "14.18.7 of IEEE Std 802.1AS"; + } + leaf neg-mean-link-delay-thresh { + type ptp-tt:time-interval; + description + "The negative propagation time threshold for + mean-link-delay, below which a Link Port is not considered + capable of participating in the IEEE Std 802.1AS + protocol."; + reference + "14.18.8 of IEEE Std 802.1AS"; + } + leaf initial-log-pdelay-req-interval { + type int8; + description + "This leaf is analogous to initial-log-pdelay-req-interval + for a PTP Port, but applicable to this Link Port."; + reference + "14.18.11 of IEEE Std 802.1AS"; + } + leaf current-log-pdelay-req-interval { + type int8; + config false; + description + "This leaf is analogous to current-log-pdelay-req-interval + for a PTP Port, but applicable to this Link Port."; + reference + "14.18.12 of IEEE Std 802.1AS"; + } + leaf use-mgt-log-pdelay-req-interval { + type boolean; + description + "This leaf is analogous to use-mgt-log-pdelay-req-interval + for a PTP Port, but applicable to this Link Port."; + reference + "14.18.13 of IEEE Std 802.1AS"; + } + leaf mgt-log-pdelay-req-interval { + type int8; + description + "This leaf is analogous to mgt-log-pdelay-req-interval for + a PTP Port, but applicable to this Link Port."; + reference + "14.18.14 of IEEE Std 802.1AS"; + } + leaf initial-compute-neighbor-rate-ratio { + type boolean; + description + "This leaf is analogous to + initial-compute-neighbor-rate-ratio for a PTP Port, but + applicable to this Link Port."; + reference + "14.18.15 of IEEE Std 802.1AS"; + } + leaf current-compute-neighbor-rate-ratio { + type boolean; + config false; + description + "This leaf is analogous to + current-compute-neighbor-rate-ratio for a PTP Port, but + applicable to this Link Port."; + reference + "14.18.16 of IEEE Std 802.1AS"; + } + leaf use-mgt-compute-neighbor-rate-ratio { + type boolean; + description + "This leaf is analogous to + use-mgt-compute-neighbor-rate-ratio for a PTP Port, but + applicable to this Link Port."; + reference + "14.18.17 of IEEE Std 802.1AS"; + } + leaf mgt-compute-neighbor-rate-ratio { + type boolean; + description + "This leaf is analogous to mgt-compute-neighbor-rate-ratio + for a PTP Port, but applicable to this Link Port."; + reference + "14.18.18 of IEEE Std 802.1AS"; + } + leaf initial-compute-mean-link-delay { + type boolean; + description + "This leaf is analogous to initial-compute-mean-link-delay + for a PTP Port, but applicable to this Link Port."; + reference + "14.18.19 of IEEE Std 802.1AS"; + } + leaf current-compute-mean-link-delay { + type boolean; + config false; + description + "This leaf is analogous to current-compute-mean-link-delay + for a PTP Port, but applicable to this Link Port."; + reference + "14.18.20 of IEEE Std 802.1AS"; + } + leaf use-mgt-compute-mean-link-delay { + type boolean; + description + "This leaf is analogous to use-mgt-compute-mean-link-delay + for a PTP Port, but applicable to this Link Port."; + reference + "14.18.21 of IEEE Std 802.1AS"; + } + leaf mgt-compute-mean-link-delay { + type boolean; + description + "This leaf is analogous to mgt-compute-mean-link-delay for a + PTP Port, but applicable to this Link Port."; + reference + "14.18.22 of IEEE Std 802.1AS"; + } + leaf allowed-lost-responses { + type uint8; + description + "This leaf is analogous to allowed-lost-responses for a PTP + Port, but applicable to this Link Port."; + reference + "14.18.23 of IEEE Std 802.1AS"; + } + leaf allowed-faults { + type uint8; + description + "This leaf is analogous to allowed-faults for a PTP Port, + but applicable to this Link Port."; + reference + "14.18.24 of IEEE Std 802.1AS"; + } + leaf-list pdelay-truncated-timestamps { + type uinteger48; + config false; + description + "This leaf is analogous to pdelay-truncated-timestamps for a + PTP Port, but applicable to this Link Port."; + reference + "14.18.26 of IEEE Std 802.1AS"; + } + } + + augment + "/ptp-tt:ptp"+ + "/ptp-tt:common-services"+ + "/ptp-tt:cmlds"+ + "/ptp-tt:ports"+ + "/ptp-tt:port" { + description + "Augment to add port-statistics-ds to IEEE Std 1588 Link + Port."; + container port-statistics-ds { + description + "This container is analogous to port-statistics-ds for a PTP + Port, but applicable to this Link Port."; + reference + "14.19 of IEEE Std 802.1AS"; + leaf rx-pdelay-req-count { + type yang:counter32; + config false; + description + "This leaf is analogous to rx-pdelay-req-count for a PTP + Port, but applicable to this Link Port."; + reference + "14.19.2 of IEEE Std 802.1AS"; + } + leaf rx-pdelay-resp-count { + type yang:counter32; + config false; + description + "This leaf is analogous to rx-pdelay-resp-count for a PTP + Port, but applicable to this Link Port."; + reference + "14.19.3 of IEEE Std 802.1AS"; + } + leaf rx-pdelay-resp-follow-up-count { + type yang:counter32; + config false; + description + "This leaf is analogous to rx-pdelay-resp-follow-up-count + for a PTP Port, but applicable to this Link Port."; + reference + "14.19.4 of IEEE Std 802.1AS"; + } + leaf rx-packet-discard-count { + type yang:counter32; + config false; + description + "This leaf is analogous to rx-packet-discard-count for a + PTP Port, but applicable to this Link Port."; + reference + "14.19.5 of IEEE Std 802.1AS"; + } + leaf pdelay-allowed-lost-exceeded-count { + type yang:counter32; + config false; + description + "This leaf is analogous to + pdelay-allowed-lost-exceeded-count for a PTP Port, but + applicable to this Link Port."; + reference + "14.19.6 of IEEE Std 802.1AS"; + } + leaf tx-pdelay-req-count { + type yang:counter32; + config false; + description + "This leaf is analogous to tx-pdelay-req-count for a + PTP Port, but applicable to this Link Port."; + reference + "14.19.7 of IEEE Std 802.1AS"; + } + leaf tx-pdelay-resp-count { + type yang:counter32; + config false; + description + "This leaf is analogous to tx-pdelay-resp-count for a + PTP Port, but applicable to this Link Port."; + reference + "14.19.8 of IEEE Std 802.1AS"; + } + leaf tx-pdelay-resp-follow-up-count { + type yang:counter32; + config false; + description + "This leaf is analogous to tx-pdelay-resp-follow-up-count + for a PTP Port, but applicable to this Link Port."; + reference + "14.19.9 of IEEE Std 802.1AS"; + } + } + } + + augment + "/ptp-tt:ptp"+ + "/ptp-tt:common-services"+ + "/ptp-tt:cmlds"+ + "/ptp-tt:ports"+ + "/ptp-tt:port" { + description + "Augment to add asymmetry-measurement-mode-ds to IEEE + Std 1588 Link Port."; + container asymmetry-measurement-mode-ds { + description + "This container is analogous to + asymmetry-measurement-mode-ds for a PTP Port, but + applicable to this Link Port."; + reference + "14.20 of IEEE Std 802.1AS"; + leaf enabled { + type boolean; + description + "This leaf is analogous to + asymmetry-measurement-mode-ds.enabled for a PTP Port, but + applicable to this Link Port."; + reference + "14.20.2 of IEEE Std 802.1AS"; + } + } + } +} diff --git a/src/confd/yang/confd/infix-if-ptp.yang b/src/confd/yang/confd/infix-if-ptp.yang new file mode 100644 index 000000000..256f73e83 --- /dev/null +++ b/src/confd/yang/confd/infix-if-ptp.yang @@ -0,0 +1,102 @@ +submodule infix-if-ptp { + yang-version 1.1; + belongs-to infix-interfaces { + prefix infix-if; + } + + import ietf-interfaces { + prefix if; + } + + organization "KernelKit"; + contact "kernelkit@googlegroups.com"; + description "PTP timestamping capabilities for ietf-interfaces."; + + revision 2026-04-09 { + description "Initial revision."; + reference "internal"; + } + + /* + * Data Nodes + */ + + augment "/if:interfaces/if:interface" { + description + "PTP timestamping capabilities reported by the network driver. + Data is probed at boot via ethtool --json -T and is read-only."; + + container ptp-capabilities { + config false; + description + "PTP hardware and software timestamping capabilities of this + interface, as reported by the driver via ethtool -T. Absent + on virtual interfaces (bridges, VLANs, etc.) that have no + underlying physical device."; + + leaf-list capabilities { + type enumeration { + enum software-transmit { + description "Software TX timestamping supported."; + } + enum software-receive { + description "Software RX timestamping supported."; + } + enum software-system-clock { + description "System clock can be used for SW timestamping."; + } + enum hardware-transmit { + description "Hardware TX timestamping supported."; + } + enum hardware-receive { + description "Hardware RX timestamping supported."; + } + enum hardware-raw-clock { + description "Raw hardware clock (PHC) exposed to userspace."; + } + } + description + "Set of timestamping capability flags reported by the driver. + The presence of hardware-transmit indicates that ptp4l can + use hardware timestamping on this interface."; + } + + leaf phc-index { + type uint32; + description + "PTP Hardware Clock device index (e.g. 0 for /dev/ptp0). + Absent when the interface has no associated PHC."; + } + + leaf-list tx-types { + type string; + description + "Hardware TX timestamp types supported (e.g. 'off', 'on', + 'onestep-sync'). Empty when hardware-transmit is not in + the capabilities set."; + } + + leaf-list rx-filters { + type string; + description + "Hardware RX timestamp filter modes supported (e.g. + 'ptpv2-l2-event', 'ptpv2-l4-sync'). Empty when + hardware-receive is not in the capabilities set."; + } + + leaf hwtstamp-provider-index { + type uint32; + description + "Hardware timestamp provider index. Present only on kernels + and drivers that expose per-provider timestamping (Linux 6.x+)."; + } + + leaf hwtstamp-provider-qualifier { + type string; + description + "Human-readable quality descriptor for the hardware timestamp + provider, e.g. 'Precise (IEEE 1588 quality)'."; + } + } + } +} diff --git a/src/confd/yang/confd/infix-if-ptp@2026-04-09.yang b/src/confd/yang/confd/infix-if-ptp@2026-04-09.yang new file mode 120000 index 000000000..ee7aa240b --- /dev/null +++ b/src/confd/yang/confd/infix-if-ptp@2026-04-09.yang @@ -0,0 +1 @@ +infix-if-ptp.yang \ No newline at end of file diff --git a/src/confd/yang/confd/infix-interfaces.yang b/src/confd/yang/confd/infix-interfaces.yang index 3e6d88ba0..0ace0fe2c 100644 --- a/src/confd/yang/confd/infix-interfaces.yang +++ b/src/confd/yang/confd/infix-interfaces.yang @@ -35,11 +35,17 @@ module infix-interfaces { include infix-if-vxlan; include infix-if-wifi; include infix-if-wireguard; + include infix-if-ptp; organization "KernelKit"; contact "kernelkit@googlegroups.com"; description "Linux bridge and lag extensions for ietf-interfaces."; + revision 2026-04-09 { + description "Add ptp-capabilities submodule for per-interface PTP timestamping info."; + reference "internal"; + } + revision 2025-11-06 { description "Use new tunnel-common grouping for local, remote, ttl, and tos."; reference "internal"; diff --git a/src/confd/yang/confd/infix-interfaces@2025-11-06.yang b/src/confd/yang/confd/infix-interfaces@2026-04-09.yang similarity index 100% rename from src/confd/yang/confd/infix-interfaces@2025-11-06.yang rename to src/confd/yang/confd/infix-interfaces@2026-04-09.yang diff --git a/src/confd/yang/confd/infix-ptp.yang b/src/confd/yang/confd/infix-ptp.yang new file mode 100644 index 000000000..4ea4da51f --- /dev/null +++ b/src/confd/yang/confd/infix-ptp.yang @@ -0,0 +1,291 @@ +module infix-ptp { + yang-version 1.1; + namespace "urn:infix:ptp:ns:yang:1.0"; + prefix infix-ptp; + + import ieee1588-ptp-tt { + prefix ptp-tt; + } + import ieee802-dot1as-gptp { + prefix dot1as-gptp; + } + + organization "KernelKit"; + contact "kernelkit@googlegroups.com"; + description "Augments and deviations for IEEE 1588-2019 and + IEEE 802.1AS-2020 PTP support. + + Profile selection via the profile leaf covers all + protocol-mandatory settings for each profile. The + standard sdo-id leaf is deviated not-supported; the + profile leaf is the authoritative selector. + + Transparent Clock support uses the modern instance-type + approach (p2p-tc / e2e-tc) from IEEE 1588-2019. The + deprecated IEEE 1588-2008 transparent-clock-default-ds + and transparent-clock-ports containers are deviated + not-supported."; + + revision 2026-04-07 { + description "Initial revision."; + reference "internal"; + } + + /* + * Augments + */ + + augment "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:default-ds" { + description + "Profile selection for this PTP instance. + + The profile leaf selects the PTP profile and applies all + protocol-mandatory settings for that profile. The standard + sdo-id leaf is deviated not-supported; use profile instead."; + + leaf profile { + type enumeration { + enum ieee1588 { + value 0; + description + "IEEE 1588-2019 default profile. Uses UDP/IPv4 transport + and E2E delay measurement by default. Network transport + and delay mechanism are user-configurable per port."; + } + enum ieee802-dot1as { + value 1; + description + "IEEE 802.1AS-2020 gPTP profile. Applies all + protocol-mandatory settings: IEEE 802.3 (Layer 2) + transport, P2P delay measurement, and the 802.1AS + multicast group address. Also enables path trace, + follow-up information, and neighbor propagation delay + thresholds as required by the standard. + + User-configurable: priority1, priority2, domain-number, + time-receiver-only, and timer interval leaves."; + } + } + default ieee1588; + description + "PTP profile for this instance. Selects the complete set of + protocol-mandatory settings for the chosen profile. + + The combination of domain-number and profile must be unique + across all PTP instances on this node."; + } + } + + augment "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" { + description + "Clock servo parameters for this PTP instance."; + + container servo { + description + "Clock servo tuning parameters."; + + leaf step-threshold { + type decimal64 { + fraction-digits 9; + } + units "seconds"; + default "0.0"; + description + "Maximum offset from the time transmitter that the servo + corrects by slewing rather than stepping. When the measured + offset exceeds this threshold the clock is stepped abruptly + to the correct time; below the threshold the servo disciplines + the clock by frequency adjustment only. + + The value 0.0 (default) disables stepping: the servo always + slews, which guarantees a monotonic clock at the cost of + potentially very slow convergence when starting from a large + initial offset. + + Setting a non-zero value (for example 0.1 for 100 ms) allows + the servo to step the clock on first lock, achieving fast + initial convergence while keeping the clock monotonic once + it has locked."; + } + } + } + + /* + * Deviations from ieee1588-ptp-tt + */ + + /* + * /ptp/instances/instance/default-ds + */ + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:default-ds/ptp-tt:sdo-id" { + deviate not-supported; + description + "Only the upper 4-bit majorSdoId field is configurable. + Use the profile leaf to select the correct majorSdoId value."; + } + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:default-ds/ptp-tt:current-time" { + deviate not-supported; + description + "Setting PTP instance time via YANG is not supported. + The current PTP time is only observable via pmc and is + presented in operational data."; + } + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:default-ds/ptp-tt:external-port-config-enable" { + deviate not-supported; + description + "The external-port-config feature is not supported."; + } + + /* + * /ptp/instances/instance/description-ds + */ + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:description-ds" { + deviate not-supported; + description + "The description data set is not exposed by pmc and is + not supported in this implementation."; + } + + /* + * /ptp/instances/instance/fault-log-ds + * (feature-gated; not enabled) + */ + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:fault-log-ds" { + deviate not-supported; + description + "Structured fault log is not supported. ptp4l does not + expose a fault log via pmc. Faults are observable in + syslog (tagged ptp4l)."; + } + + /* + * /ptp/instances/instance/path-trace-ds + * (feature-gated; not enabled) + */ + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:path-trace-ds" { + deviate not-supported; + description + "Path trace mechanism is not supported in this implementation."; + } + + /* + * /ptp/instances/instance/alternate-timescale-ds + * (feature-gated; not enabled) + */ + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:alternate-timescale-ds" { + deviate not-supported; + description + "Alternate timescale mechanism is not supported."; + } + + /* + * /ptp/instances/instance/ports/port/port-ds + */ + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:ports/ptp-tt:port/ptp-tt:port-ds" + + "/ptp-tt:peer-mean-path-delay" { + deviate not-supported; + description + "Deprecated in IEEE 1588-2019; superseded by mean-link-delay. + Not supported."; + } + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:ports/ptp-tt:port/ptp-tt:port-ds" + + "/ptp-tt:version-number" { + deviate not-supported; + description + "PTP version is determined by ptp4l at runtime and is not + user-configurable."; + } + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:ports/ptp-tt:port/ptp-tt:port-ds" + + "/ptp-tt:minor-version-number" { + deviate not-supported; + description + "PTP minor version is determined by ptp4l at runtime and is + not user-configurable."; + } + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:ports/ptp-tt:port/ptp-tt:port-ds" + + "/ptp-tt:port-enable" { + deviate add { + default "true"; + } + description + "Ports are enabled by default."; + } + + /* + * /ptp/transparent-clock-default-ds (deprecated IEEE 1588-2008) + */ + + deviation "/ptp-tt:ptp/ptp-tt:transparent-clock-default-ds" { + deviate not-supported; + description + "Deprecated IEEE 1588-2008 container. Use the modern + instance-type = p2p-tc or e2e-tc approach in + /ptp/instances/instance/default-ds/instance-type instead."; + } + + /* + * /ptp/transparent-clock-ports (deprecated IEEE 1588-2008) + */ + + deviation "/ptp-tt:ptp/ptp-tt:transparent-clock-ports" { + deviate not-supported; + description + "Deprecated IEEE 1588-2008 container. Transparent Clock + ports are managed via /ptp/instances/instance/ports instead."; + } + + /* + * /ptp/common-services (CMLDS — Phase 2) + */ + + deviation "/ptp-tt:ptp/ptp-tt:common-services" { + deviate not-supported; + description + "CMLDS (Common Mean Link Delay Service) is not supported yet."; + } + + /* + * Deviations from ieee802-dot1as-gptp + */ + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:ports/ptp-tt:port/ptp-tt:port-ds" + + "/dot1as-gptp:nup" { + deviate not-supported; + description + "EPON upstream refraction index — not applicable to + Ethernet/TSN deployments."; + } + + deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" + + "/ptp-tt:ports/ptp-tt:port/ptp-tt:port-ds" + + "/dot1as-gptp:ndown" { + deviate not-supported; + description + "EPON downstream refraction index — not applicable to + Ethernet/TSN deployments."; + } +} diff --git a/src/confd/yang/confd/infix-ptp@2026-04-07.yang b/src/confd/yang/confd/infix-ptp@2026-04-07.yang new file mode 120000 index 000000000..a0f235955 --- /dev/null +++ b/src/confd/yang/confd/infix-ptp@2026-04-07.yang @@ -0,0 +1 @@ +infix-ptp.yang \ No newline at end of file diff --git a/src/klish-plugin-infix/xml/infix.xml b/src/klish-plugin-infix/xml/infix.xml index b47727f9e..250cd8378 100644 --- a/src/klish-plugin-infix/xml/infix.xml +++ b/src/klish-plugin-infix/xml/infix.xml @@ -422,6 +422,13 @@ echo "Public: $pub" show nacm + + + + + show ptp $KLISH_PARAM_instance + + diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index 1fee3f0f7..bf77df3db 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -5636,6 +5636,203 @@ def show_bfd(json_data): show_bfd_peers_brief(json_data) +def _ptp_ns(yang_val): + """Convert YANG time-interval (ns × 2^16, stored as str) to integer nanoseconds.""" + try: + return int(yang_val) // 65536 + except (TypeError, ValueError): + return None + + +def _ptp_strip(identity): + """Strip YANG module prefix from identityref value. + + 'ieee1588-ptp-tt:cc-default' → 'cc-default' + """ + if identity and ':' in identity: + return identity.split(':', 1)[1] + return identity or '' + + +_PTP_INSTANCE_TYPE_NAMES = { + "oc": "Ordinary Clock", + "bc": "Boundary Clock", + "p2p-tc": "P2P Transparent Clock", + "e2e-tc": "E2E Transparent Clock", +} + +_PTP_PORT_STATE_COLOR = { + "time-transmitter": Decore.green, + "time-receiver": Decore.green, + "pre-time-transmitter": Decore.yellow, + "uncalibrated": Decore.yellow, + "listening": Decore.yellow, + "passive": lambda x: x, + "faulty": Decore.red, + "disabled": Decore.red, + "initializing": lambda x: x, +} + + +def show_ptp(json_data, instance_index=None): + """Show PTP instance status.""" + ptp = json_data.get("ieee1588-ptp-tt:ptp", {}) + instances = ptp.get("instances", {}).get("instance", []) + + if not instances: + print("PTP: no instances configured.") + return + + for inst in instances: + idx = inst.get("instance-index", "?") + + if instance_index is not None and str(idx) != str(instance_index): + continue + + dds = inst.get("default-ds", {}) + cds = inst.get("current-ds", {}) + pds = inst.get("parent-ds", {}) + tpds = inst.get("time-properties-ds", {}) + ports = inst.get("ports", {}).get("port", []) + + itype = _PTP_INSTANCE_TYPE_NAMES.get(dds.get("instance-type", "oc"), "Unknown") + domain = dds.get("domain-number", 0) + clock_id = dds.get("clock-identity", "?") + + # ── Header ──────────────────────────────────────────────────────────── + header = f"PTP Instance {idx}" + subtitle = f"{itype} · domain {domain}" + pad_len = max(1, 40 - len(header)) + pad = " " * pad_len + rule_w = max(68, len(header) + pad_len + len(subtitle)) + print(f"{Decore.bold(header)}{pad}{subtitle}") + print("─" * rule_w) + + # ── Clock / GM identity ─────────────────────────────────────────────── + W = 24 + gm_id = pds.get("grandmaster-identity", "") + print(f" {'Clock identity':<{W}}: {clock_id}") + if gm_id and gm_id != clock_id: + print(f" {'Grandmaster':<{W}}: {gm_id}") + else: + print(f" {'Grandmaster':<{W}}: (self)") + + # ── Priorities ──────────────────────────────────────────────────────── + p1, p2 = dds.get("priority1", "?"), dds.get("priority2", "?") + print(f" {'Priority1/Priority2':<{W}}: {p1} / {p2}") + + if gm_id and gm_id != clock_id: + gp1 = pds.get("grandmaster-priority1", "?") + gp2 = pds.get("grandmaster-priority2", "?") + print(f" {'GM Priority1/Priority2':<{W}}: {gp1} / {gp2}") + + # ── Clock quality ───────────────────────────────────────────────────── + cq = dds.get("clock-quality", {}) + cc = _ptp_strip(cq.get("clock-class", "")) + if cc: + print(f" {'Clock class':<{W}}: {cc}") + + if gm_id and gm_id != clock_id: + gcq = pds.get("grandmaster-clock-quality", {}) + gcc = _ptp_strip(gcq.get("clock-class", "")) + if gcc and gcc != cc: + print(f" {'GM clock class':<{W}}: {gcc}") + + # ── Time source ─────────────────────────────────────────────────────── + ts = _ptp_strip(tpds.get("time-source", "")) + if ts: + print(f" {'Time source':<{W}}: {ts}") + + if dds.get("time-receiver-only"): + print(f" {'Mode':<{W}}: time-receiver only") + + # ── Time properties ─────────────────────────────────────────────────── + ptp_ts = "yes" if tpds.get("ptp-timescale") else "no" + t_trace = "yes" if tpds.get("time-traceable") else "no" + f_trace = "yes" if tpds.get("frequency-traceable") else "no" + utc_off = tpds.get("current-utc-offset") + utc_str = f"{utc_off} s" if utc_off is not None else "N/A" + print(f" {'PTP timescale':<{W}}: {ptp_ts}") + print(f" {'UTC offset':<{W}}: {utc_str}") + print(f" {'Time traceable':<{W}}: {t_trace}") + print(f" {'Freq. traceable':<{W}}: {f_trace}") + + # ── Sync status ─────────────────────────────────────────────────────── + offset = _ptp_ns(cds.get("offset-from-time-transmitter")) + delay = _ptp_ns(cds.get("mean-delay")) + steps = cds.get("steps-removed") + if offset is not None: + print(f" {'Offset from GM':<{W}}: {offset} ns") + if delay is not None: + print(f" {'Mean path delay':<{W}}: {delay} ns") + if steps is not None: + print(f" {'Steps removed':<{W}}: {steps}") + + # ── Ports ───────────────────────────────────────────────────────────── + if ports: + print() + Decore.title("Ports", width=rule_w) + + port_table = SimpleTable([ + Column("PORT", align='right'), + Column("INTERFACE", flexible=True), + Column("STATE", flexible=True), + Column("DELAY"), + Column("LINK DELAY (ns)", align='right'), + ]) + stats_table = SimpleTable([ + Column("PORT", align='right'), + Column("INTERFACE", flexible=True), + Column("SYNC \u25bc", align='right'), + Column("SYNC \u25b2", align='right'), + Column("ANN \u25bc", align='right'), + Column("ANN \u25b2", align='right'), + Column("PD \u25bc", align='right'), + Column("PD \u25b2", align='right'), + ]) + has_stats = False + + for port in ports: + pidx = port.get("port-index", "?") + pds_ = port.get("port-ds", {}) + iface = port.get("underlying-interface", + pds_.get("port-identity", {}).get("clock-identity", "?")) + + state_raw = pds_.get("port-state", "?") + color_fn = _PTP_PORT_STATE_COLOR.get(state_raw, lambda x: x) + state_str = color_fn(state_raw) + + dm = (pds_.get("delay-mechanism") or "?").upper() + mld = _ptp_ns(pds_.get("mean-link-delay")) + mld_str = str(mld) if mld is not None else "" + + port_table.row(str(pidx), iface, state_str, dm, mld_str) + + st = port.get("ieee802-dot1as-gptp:port-statistics-ds", {}) + if st: + has_stats = True + stats_table.row( + str(pidx), iface, + str(st.get("rx-sync-count", 0)), + str(st.get("tx-sync-count", 0)), + str(st.get("rx-announce-count", 0)), + str(st.get("tx-announce-count", 0)), + str(st.get("rx-pdelay-req-count", 0)), + str(st.get("tx-pdelay-req-count", 0)), + ) + + port_table.adjust_padding(rule_w) + port_table.print() + + if has_stats: + print() + Decore.title("Message Statistics (\u25bc\u202frx \u25b2\u202ftx)", width=rule_w) + stats_table.adjust_padding(rule_w) + stats_table.print() + + print() + + def main(): global UNIT_TEST @@ -5695,6 +5892,9 @@ def main(): ks_parser.add_argument('-t', '--type', help='Key type (symmetric or asymmetric)') ks_parser.add_argument('-n', '--name', help='Key name') + subparsers.add_parser('show-ptp', help='Show PTP instance status') \ + .add_argument('instance', nargs='?', help='Instance index (optional)') + subparsers.add_parser('show-ntp', help='Show NTP status') \ .add_argument('-a', '--address', help='Show details for specific address') subparsers.add_parser('show-ntp-tracking', help='Show NTP tracking status') @@ -5768,6 +5968,8 @@ def main(): show_nacm_user(json_data) elif args.command == "show-keystore": show_keystore(json_data, getattr(args, 'type', None), args.name) + elif args.command == "show-ptp": + show_ptp(json_data, getattr(args, 'instance', None)) elif args.command == "show-ntp": show_ntp(json_data, args.address) elif args.command == "show-ntp-tracking": diff --git a/src/statd/python/yanger/__main__.py b/src/statd/python/yanger/__main__.py index f7097c078..c88d4648f 100644 --- a/src/statd/python/yanger/__main__.py +++ b/src/statd/python/yanger/__main__.py @@ -123,6 +123,9 @@ def main(): elif model == 'ietf-bfd-ip-sh': from . import ietf_bfd_ip_sh yang_data = ietf_bfd_ip_sh.operational() + elif model == 'ieee1588-ptp-tt': + from . import ieee1588_ptp + yang_data = ieee1588_ptp.operational() else: common.LOG.warning("Unsupported model %s", model) sys.exit(1) diff --git a/src/statd/python/yanger/ieee1588_ptp.py b/src/statd/python/yanger/ieee1588_ptp.py new file mode 100644 index 000000000..eb2a0a092 --- /dev/null +++ b/src/statd/python/yanger/ieee1588_ptp.py @@ -0,0 +1,624 @@ +"""Operational data for ieee1588-ptp-tt (and ieee802-dot1as-gptp). + +Queries each running ptp4l instance via pmc and maps the output to the +YANG model structure. One ptp4l process runs per instance-index, with +its config at /etc/linuxptp/ptp4l-.conf and its UDS socket at +/var/run/ptp4l-. +""" + +import glob +import os +import re + +from .common import insert, LOG +from .host import HOST + + +# --------------------------------------------------------------------------- +# pmc helpers +# --------------------------------------------------------------------------- + +def _pmc_get(conf, command): + """Run 'pmc -b 0 -f GET ' and return parsed key→value dict. + + pmc output looks like: + \t\t + Blank lines and lines not starting with whitespace are ignored. + Multiple response blocks (one per port for PORT_DATA_SET) each get + their own dict; returns a list of dicts in that case. + """ + lines = HOST.run_multiline( + ["pmc", "-u", "-b", "0", "-f", conf, f"GET {command}"], default=[]) + + blocks = [] + current = {} + for line in lines: + stripped = line.strip() + if not stripped or stripped.startswith("sending:") or \ + "RESPONSE MANAGEMENT" in stripped or \ + "SIGNALING" in stripped: + if current: + blocks.append(current) + current = {} + continue + m = re.match(r'^\s+(\S+)\s+(.+)$', line) + if m: + current[m.group(1)] = m.group(2).strip() + + if current: + blocks.append(current) + + return blocks + + +def _pmc_get_one(conf, command): + """Like _pmc_get but return only the first (or only) block.""" + blocks = _pmc_get(conf, command) + return blocks[0] if blocks else {} + + +# --------------------------------------------------------------------------- +# clockIdentity formatting +# --------------------------------------------------------------------------- + +def _fmt_clock_identity(raw): + """Convert pmc clockIdentity 'aabbcc.fffe.ddeeff' to YANG format 'AA-BB-CC-FF-FE-DD-EE-FF'. + + The YANG typedef clock-identity requires the pattern [0-9A-F]{2}(-[0-9A-F]{2}){7}. + pmc outputs in its own dotted notation e.g. '005182.fffe.112202'. + """ + raw = raw.replace(".", "").replace("-", "").replace(":", "").upper() + if len(raw) == 16: + return "-".join(raw[i:i+2] for i in range(0, 16, 2)) + return raw + + +def _fmt_port_identity(raw): + """Convert 'aabbccfffe001122-1' to dict {clock-identity, port-number}.""" + parts = raw.rsplit("-", 1) + cid = _fmt_clock_identity(parts[0]) if parts else raw + pnum = int(parts[1]) if len(parts) == 2 else 0 + return {"clock-identity": cid, "port-number": pnum} + + +# --------------------------------------------------------------------------- +# clockAccuracy identity mapping +# --------------------------------------------------------------------------- + +# Map clockClass decimal values to ieee1588-ptp-tt identity names (identityref, not uint8). +_CLOCK_CLASS_MAP = { + 6: "ieee1588-ptp-tt:cc-primary-sync", + 7: "ieee1588-ptp-tt:cc-primary-sync-lost", + 13: "ieee1588-ptp-tt:cc-application-specific-sync", + 14: "ieee1588-ptp-tt:cc-application-specific-sync-lost", + 52: "ieee1588-ptp-tt:cc-primary-sync-alternative-a", + 58: "ieee1588-ptp-tt:cc-application-specific-alternative-a", + 187: "ieee1588-ptp-tt:cc-primary-sync-alternative-b", + 193: "ieee1588-ptp-tt:cc-application-specific-alternative-b", + 248: "ieee1588-ptp-tt:cc-default", + 255: "ieee1588-ptp-tt:cc-time-receiver-only", +} + + +def _clock_class_identity(raw): + """Return the YANG identity string for a pmc clockClass decimal value, or None.""" + try: + return _CLOCK_CLASS_MAP.get(int(raw)) + except (ValueError, TypeError): + return None + + +# Map clockAccuracy hex values to ieee1588-ptp-tt identity names (identityref, not uint8). +# Identity names use the 'ca-' prefix as defined in ieee1588-ptp-tt@2023-08-14.yang. +# 0xfe (unknown) has no corresponding identity and is omitted by returning None. +_CLOCK_ACCURACY_MAP = { + 0x17: "ieee1588-ptp-tt:ca-time-accurate-to-1000-fs", + 0x18: "ieee1588-ptp-tt:ca-time-accurate-to-2500-fs", + 0x19: "ieee1588-ptp-tt:ca-time-accurate-to-10-ps", + 0x1a: "ieee1588-ptp-tt:ca-time-accurate-to-25ps", + 0x1b: "ieee1588-ptp-tt:ca-time-accurate-to-100-ps", + 0x1c: "ieee1588-ptp-tt:ca-time-accurate-to-250-ps", + 0x1d: "ieee1588-ptp-tt:ca-time-accurate-to-1000-ps", + 0x1e: "ieee1588-ptp-tt:ca-time-accurate-to-2500-ps", + 0x1f: "ieee1588-ptp-tt:ca-time-accurate-to-10-ns", + 0x20: "ieee1588-ptp-tt:ca-time-accurate-to-25-ns", + 0x21: "ieee1588-ptp-tt:ca-time-accurate-to-100-ns", + 0x22: "ieee1588-ptp-tt:ca-time-accurate-to-250-ns", + 0x23: "ieee1588-ptp-tt:ca-time-accurate-to-1000-ns", + 0x24: "ieee1588-ptp-tt:ca-time-accurate-to-2500-ns", + 0x25: "ieee1588-ptp-tt:ca-time-accurate-to-10-us", + 0x26: "ieee1588-ptp-tt:ca-time-accurate-to-25-us", + 0x27: "ieee1588-ptp-tt:ca-time-accurate-to-100-us", + 0x28: "ieee1588-ptp-tt:ca-time-accurate-to-250-us", + 0x29: "ieee1588-ptp-tt:ca-time-accurate-to-1000-us", + 0x2a: "ieee1588-ptp-tt:ca-time-accurate-to-2500-us", + 0x2b: "ieee1588-ptp-tt:ca-time-accurate-to-10-ms", + 0x2c: "ieee1588-ptp-tt:ca-time-accurate-to-25-ms", + 0x2d: "ieee1588-ptp-tt:ca-time-accurate-to-100-ms", + 0x2e: "ieee1588-ptp-tt:ca-time-accurate-to-250-ms", + 0x2f: "ieee1588-ptp-tt:ca-time-accurate-to-1-s", + 0x30: "ieee1588-ptp-tt:ca-time-accurate-to-10-s", + 0x31: "ieee1588-ptp-tt:ca-time-accurate-to-gt-10-s", +} + + +def _clock_accuracy_identity(raw): + """Return the YANG identity string for a pmc clockAccuracy hex value, or None.""" + try: + return _CLOCK_ACCURACY_MAP.get(int(raw, 16)) + except (ValueError, TypeError): + return None + + +# --------------------------------------------------------------------------- +# time-source identity mapping +# --------------------------------------------------------------------------- + +_TIME_SOURCE_MAP = { + "0x10": "ieee1588-ptp-tt:atomic-clock", + "0x20": "ieee1588-ptp-tt:gnss", + "0x30": "ieee1588-ptp-tt:terrestrial-radio", + "0x39": "ieee1588-ptp-tt:serial-time-code", + "0x40": "ieee1588-ptp-tt:ptp", + "0x50": "ieee1588-ptp-tt:ntp", + "0x60": "ieee1588-ptp-tt:hand-set", + "0x90": "ieee1588-ptp-tt:other", + "0xa0": "ieee1588-ptp-tt:internal-oscillator", +} + + +def _time_source_identity(raw): + return _TIME_SOURCE_MAP.get(raw.lower(), + "ieee1588-ptp-tt:internal-oscillator") + + +# --------------------------------------------------------------------------- +# delay-mechanism and port-state mapping +# --------------------------------------------------------------------------- + +_DELAY_MECH_MAP = { + "E2E": "e2e", + "P2P": "p2p", + "AUTO": "no-mechanism", +} + +_PORT_STATE_MAP = { + "INITIALIZING": "initializing", + "FAULTY": "faulty", + "DISABLED": "disabled", + "LISTENING": "listening", + "PRE_MASTER": "pre-time-transmitter", + "MASTER": "time-transmitter", + "PASSIVE": "passive", + "UNCALIBRATED": "uncalibrated", + "SLAVE": "time-receiver", + "GRAND_MASTER": "time-transmitter", +} + + +# --------------------------------------------------------------------------- +# Per-dataset builders +# --------------------------------------------------------------------------- + +def _build_default_ds(d): + """Map pmc DEFAULT_DATA_SET response to YANG default-ds.""" + ds = {} + + cid = d.get("clockIdentity") + if cid: + ds["clock-identity"] = _fmt_clock_identity(cid) + + v = d.get("numberPorts") + if v: + ds["number-ports"] = int(v) + + cq = {} + v = d.get("clockClass") + if v: + cc = _clock_class_identity(v) + if cc: + cq["clock-class"] = cc + v = d.get("clockAccuracy") + if v: + ca = _clock_accuracy_identity(v) + if ca: + cq["clock-accuracy"] = ca + v = d.get("offsetScaledLogVariance") + if v: + cq["offset-scaled-log-variance"] = int(v, 16) + if cq: + ds["clock-quality"] = cq + + v = d.get("priority1") + if v: + ds["priority1"] = int(v) + v = d.get("priority2") + if v: + ds["priority2"] = int(v) + + v = d.get("domainNumber") + if v: + ds["domain-number"] = int(v) + + v = d.get("clientOnly") or d.get("slaveOnly") # renamed in ptp4l 4.x + if v is not None: + ds["time-receiver-only"] = (v == "1") + + # instance-type: derive from ptp4l GM/time-receiver state (read-only, operational) + # pmc doesn't directly expose clockType in DEFAULT_DATA_SET + # We'll fill instance-type later from the instance's config if possible + + return ds + + +def _build_current_ds(d): + """Map pmc CURRENT_DATA_SET response to YANG current-ds.""" + ds = {} + + v = d.get("stepsRemoved") + if v: + ds["steps-removed"] = int(v) + + v = d.get("offsetFromMaster") + if v: + # ptp4l reports nanoseconds as float; YANG time-interval is ns * 2^16. + # RFC 7951: int64 must be JSON-encoded as a string. + try: + ds["offset-from-time-transmitter"] = str(int(float(v) * 65536)) + except ValueError: + pass + + v = d.get("meanPathDelay") + if v: + try: + ds["mean-delay"] = str(int(float(v) * 65536)) + except ValueError: + pass + + return ds + + +def _build_parent_ds(d): + """Map pmc PARENT_DATA_SET response to YANG parent-ds.""" + ds = {} + + v = d.get("parentPortIdentity") + if v: + ds["parent-port-identity"] = _fmt_port_identity(v) + + v = d.get("parentStats") + if v: + ds["parent-stats"] = (v == "1") + + v = d.get("observedParentOffsetScaledLogVariance") + if v: + try: + ds["observed-parent-offset-scaled-log-variance"] = int(v, 16) + except ValueError: + pass + + v = d.get("observedParentClockPhaseChangeRate") + if v: + try: + ds["observed-parent-clock-phase-change-rate"] = int(v) + except ValueError: + pass + + v = d.get("grandmasterIdentity") + if v: + ds["grandmaster-identity"] = _fmt_clock_identity(v) + + gcq = {} + v = d.get("gm.ClockClass") + if v: + cc = _clock_class_identity(v) + if cc: + gcq["clock-class"] = cc + v = d.get("gm.ClockAccuracy") + if v: + ca = _clock_accuracy_identity(v) + if ca: + gcq["clock-accuracy"] = ca + v = d.get("gm.OffsetScaledLogVariance") + if v: + try: + gcq["offset-scaled-log-variance"] = int(v, 16) + except ValueError: + pass + if gcq: + ds["grandmaster-clock-quality"] = gcq + + v = d.get("grandmasterPriority1") + if v: + ds["grandmaster-priority1"] = int(v) + v = d.get("grandmasterPriority2") + if v: + ds["grandmaster-priority2"] = int(v) + + return ds + + +def _build_time_properties_ds(d): + """Map pmc TIME_PROPERTIES_DATA_SET response to YANG time-properties-ds.""" + ds = {} + + # current-utc-offset has a when condition requiring current-utc-offset-valid='true' + if d.get("currentUtcOffsetValid") in ("1", "true"): + v = d.get("currentUtcOffset") + if v: + ds["current-utc-offset"] = int(v) + + v = d.get("leap61") + if v is not None: + ds["leap61"] = (v == "1") + v = d.get("leap59") + if v is not None: + ds["leap59"] = (v == "1") + v = d.get("currentUtcOffsetValid") + if v is not None: + ds["current-utc-offset-valid"] = (v == "1") + v = d.get("ptpTimescale") + if v is not None: + ds["ptp-timescale"] = (v == "1") + v = d.get("timeTraceable") + if v is not None: + ds["time-traceable"] = (v == "1") + v = d.get("frequencyTraceable") + if v is not None: + ds["frequency-traceable"] = (v == "1") + + v = d.get("timeSource") + if v: + ds["time-source"] = _time_source_identity(v) + + return ds + + +def _build_port_ds(d): + """Map pmc PORT_DATA_SET response to YANG port-ds.""" + ds = {} + + v = d.get("portIdentity") + if v: + ds["port-identity"] = _fmt_port_identity(v) + + v = d.get("portState") + if v: + ds["port-state"] = _PORT_STATE_MAP.get(v, "disabled") + + v = d.get("logMinDelayReqInterval") + if v: + try: + ds["log-min-delay-req-interval"] = int(v) + except ValueError: + pass + + v = d.get("peerMeanPathDelay") + if v: + try: + # RFC 7951: int64 must be JSON-encoded as a string. + ds["mean-link-delay"] = str(int(float(v) * 65536)) + except ValueError: + pass + + v = d.get("logAnnounceInterval") + if v: + try: + ds["log-announce-interval"] = int(v) + except ValueError: + pass + + v = d.get("announceReceiptTimeout") + if v: + try: + ds["announce-receipt-timeout"] = int(v) + except ValueError: + pass + + v = d.get("logSyncInterval") + if v: + try: + ds["log-sync-interval"] = int(v) + except ValueError: + pass + + v = d.get("delayMechanism") + if v: + ds["delay-mechanism"] = _DELAY_MECH_MAP.get(v, "e2e") + + v = d.get("logMinPdelayReqInterval") + if v: + try: + ds["log-min-pdelay-req-interval"] = int(v) + except ValueError: + pass + + v = d.get("versionNumber") + if v: + try: + ds["version-number"] = int(v) + except ValueError: + pass + + v = d.get("portEnable") + if v is not None: + ds["port-enable"] = (v == "1") + + return ds + + +def _build_port_stats(d): + """Map pmc PORT_STATS_NP response to ieee802-dot1as-gptp port-statistics-ds.""" + stats = {} + mapping = { + "rx_Sync": "rx-sync-count", + "rx_Follow_Up": "rx-follow-up-count", + "rx_Pdelay_Req": "rx-pdelay-req-count", + "rx_Pdelay_Resp": "rx-pdelay-resp-count", + "rx_Pdelay_Resp_Follow_Up": "rx-pdelay-resp-follow-up-count", + "rx_Announce": "rx-announce-count", + "tx_Sync": "tx-sync-count", + "tx_Follow_Up": "tx-follow-up-count", + "tx_Pdelay_Req": "tx-pdelay-req-count", + "tx_Pdelay_Resp": "tx-pdelay-resp-count", + "tx_Pdelay_Resp_Follow_Up": "tx-pdelay-resp-follow-up-count", + "tx_Announce": "tx-announce-count", + } + for pmc_key, yang_key in mapping.items(): + v = d.get(pmc_key) + if v is not None: + try: + stats[yang_key] = int(v) + except ValueError: + pass + return stats + + +# --------------------------------------------------------------------------- +# Per-instance builder +# --------------------------------------------------------------------------- + +def _port_interfaces(conf_path): + """Return ordered list of interface names from ptp4l conf (non-global section headers).""" + ifaces = [] + try: + with open(conf_path) as f: + for line in f: + s = line.strip() + if s.startswith('[') and s.endswith(']') and s[1:-1] != 'global': + ifaces.append(s[1:-1]) + except OSError: + pass + return ifaces + + +def _instance_type_from_config(conf_path): + """Read instance-type from a saved config file (best effort).""" + try: + with open(conf_path, "r") as f: + for line in f: + m = re.match(r'\s*clockType\s+(\S+)', line) + if m: + ct = m.group(1).upper() + if ct == "P2P_TC": + return "p2p-tc" + if ct == "E2E_TC": + return "e2e-tc" + if ct == "BOUNDARY_CLOCK": + return "bc" + # Default: if more than one port, bc; otherwise oc + # (approximation — proper detection requires DEFAULT_DATA_SET numberPorts) + except OSError: + pass + return None + + +def _build_instance(idx, conf_path): + """Build one instance dict from pmc queries for instance index idx.""" + inst = {"instance-index": idx} + + # default-ds + dd = _pmc_get_one(conf_path, "DEFAULT_DATA_SET") + if dd: + dds = _build_default_ds(dd) + # Derive instance-type from numberPorts + config file + num_ports = int(dd.get("numberPorts", "0") or "0") + it = _instance_type_from_config(conf_path) + if it is None: + it = "bc" if num_ports > 1 else "oc" + dds["instance-type"] = it + inst["default-ds"] = dds + + # current-ds + cd = _pmc_get_one(conf_path, "CURRENT_DATA_SET") + if cd: + cds = _build_current_ds(cd) + if cds: + inst["current-ds"] = cds + + # parent-ds + pd = _pmc_get_one(conf_path, "PARENT_DATA_SET") + if pd: + pds = _build_parent_ds(pd) + if pds: + inst["parent-ds"] = pds + + # time-properties-ds + tp = _pmc_get_one(conf_path, "TIME_PROPERTIES_DATA_SET") + if tp: + tpds = _build_time_properties_ds(tp) + if tpds: + inst["time-properties-ds"] = tpds + + # ports: PORT_DATA_SET returns one block per port + port_blocks = _pmc_get(conf_path, "PORT_DATA_SET") + stats_blocks = _pmc_get(conf_path, "PORT_STATS_NP") + ifaces = _port_interfaces(conf_path) + + # Build a stats map keyed by portIdentity for quick lookup + stats_by_id = {} + for sb in stats_blocks: + pid = sb.get("portIdentity") + if pid: + stats_by_id[pid] = _build_port_stats(sb) + + ports = [] + for i, pb in enumerate(port_blocks, start=1): + pid_raw = pb.get("portIdentity", "") + port_entry = {} + + # port-index = port number from portIdentity + pid_dict = _fmt_port_identity(pid_raw) + port_entry["port-index"] = pid_dict.get("port-number", i) + + if i <= len(ifaces): + port_entry["underlying-interface"] = ifaces[i - 1] + + pds = _build_port_ds(pb) + if pds: + port_entry["port-ds"] = pds + + # 802.1AS port-statistics-ds + stats = stats_by_id.get(pid_raw) + if stats: + port_entry["ieee802-dot1as-gptp:port-statistics-ds"] = stats + + ports.append(port_entry) + + if ports: + inst["ports"] = {"port": ports} + + return inst + + +# --------------------------------------------------------------------------- +# Top-level entry point +# --------------------------------------------------------------------------- + +def operational(): + """Return operational data for ieee1588-ptp-tt.""" + out = {} + instances = [] + + conf_files = sorted(glob.glob("/etc/linuxptp/ptp4l-*.conf")) + for conf_path in conf_files: + m = re.search(r'ptp4l-(\d+)\.conf$', conf_path) + if not m: + continue + idx = int(m.group(1)) + + # Only include instances with a live UDS socket (i.e. ptp4l running) + uds_path = f"/var/run/ptp4l-{idx}" + if not HOST.exists(uds_path): + continue + + try: + inst = _build_instance(idx, conf_path) + instances.append(inst) + except Exception as e: + LOG.debug("ptp4l-%d: skipping instance: %s", idx, e) + + if instances: + insert(out, "ieee1588-ptp-tt:ptp", "instances", "instance", instances) + + return out diff --git a/src/statd/python/yanger/ietf_interfaces/link.py b/src/statd/python/yanger/ietf_interfaces/link.py index 60665e0ce..4f99c8769 100644 --- a/src/statd/python/yanger/ietf_interfaces/link.py +++ b/src/statd/python/yanger/ietf_interfaces/link.py @@ -117,9 +117,38 @@ def interface_common(iplink, ipaddr): return interface -def interface(iplink, ipaddr): +def ptp_capabilities(ifname, systemjson): + """Return infix-interfaces:ptp-capabilities dict for ifname, or None.""" + caps = systemjson.get("interfaces", {}).get(ifname, {}).get("ptp-capabilities") + if not caps: + return None + + result = {} + if cl := caps.get("capabilities"): + result["capabilities"] = cl + if (phc := caps.get("phc-index")) is not None: + result["phc-index"] = phc + if tx := caps.get("tx-types"): + result["tx-types"] = tx + if rx := caps.get("rx-filters"): + result["rx-filters"] = rx + if (idx := caps.get("hwtstamp-provider-index")) is not None: + result["hwtstamp-provider-index"] = idx + if qual := caps.get("hwtstamp-provider-qualifier"): + result["hwtstamp-provider-qualifier"] = qual + + return result or None + + +def interface(iplink, ipaddr, systemjson=None): interface = interface_common(iplink, ipaddr) + if systemjson is None: + systemjson = {} + + if ptpcap := ptp_capabilities(iplink["ifname"], systemjson): + interface["infix-interfaces:ptp-capabilities"] = ptpcap + match interface["type"]: case "infix-if-type:bridge": if br := bridge.bridge(iplink): @@ -163,8 +192,11 @@ def interface(iplink, ipaddr): def interfaces(ifname=None): + from ..host import HOST + links = common.iplinks(ifname) addrs = common.ipaddrs(ifname) + systemjson = HOST.read_json("/run/system.json", {}) interfaces = [] for ifname, iplink in links.items(): @@ -177,6 +209,6 @@ def interfaces(ifname=None): ipaddr = addrs.get(ifname, {}) - interfaces.append(interface(iplink, ipaddr)) + interfaces.append(interface(iplink, ipaddr, systemjson)) return interfaces diff --git a/src/statd/statd.c b/src/statd/statd.c index affc90740..dac055836 100644 --- a/src/statd/statd.c +++ b/src/statd/statd.c @@ -52,6 +52,7 @@ #define XPATH_LLDP_BASE "/ieee802-dot1ab-lldp:lldp" #define XPATH_FIREWALL_BASE "/infix-firewall:firewall" #define XPATH_NTP_BASE "/ietf-ntp:ntp" +#define XPATH_PTP_BASE "/ieee1588-ptp-tt:ptp" TAILQ_HEAD(sub_head, sub); @@ -111,7 +112,7 @@ static int ly_add_yanger_data(const struct ly_ctx *ctx, struct lyd_node **parent err = lyd_parse_data_fd(ctx, fd, LYD_JSON, LYD_PARSE_ONLY, 0, parent); if (err) - ERROR("Error, parsing yanger data (%d)", err); + ERROR("Error, parsing yanger data (%d): %s", err, ly_errmsg(ctx)); fclose(stream); /* Note: fclose() already closes the underlying fd from fdopen() */ @@ -455,6 +456,8 @@ static int subscribe_to_all(struct statd *statd) return SR_ERR_INTERNAL; if (subscribe(statd, "ietf-ntp", XPATH_NTP_BASE, sr_generic_cb)) return SR_ERR_INTERNAL; + if (subscribe(statd, "ieee1588-ptp-tt", XPATH_PTP_BASE, sr_generic_cb)) + return SR_ERR_INTERNAL; INFO("Successfully subscribed to all models"); return SR_ERR_OK; From 3426c2527b8be4de05bae1bba32a99e344d1e7d4 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 18 Apr 2026 07:14:28 +0200 Subject: [PATCH 6/9] Add phc2sys companion service for BC/TC instances MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On multi-chip DSA hardware (e.g., boards with multiple mv88e6xxx chips), each switch chip has its own independent PHC device. With boundary_clock_jbod enabled, ptp4l starts but only disciplines the active slave port's PHC — the others drift. Automatically start phc2sys -a alongside any BC or TC instance using hardware timestamping. It subscribes to ptp4l's UDS socket, tracks BMCA, and disciplines all non-active PHCs to match the active one. On single-chip hardware it is a harmless no-op. CLOCK_REALTIME is intentionally left untouched. Syncing the system clock to PTP (phc2sys -rr), feeding the PHC from GPS/NTP (ts2phc, phc2sys reverse), and full multi-source coordination (timemaster) are planned as follow-on phases; see the issue tracker for the roadmap. Signed-off-by: Joachim Wiberg --- .../etc/finit.d/available/phc2sys@.conf | 3 + src/confd/src/ptp.c | 82 ++++++++++++++++++- 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 board/common/rootfs/etc/finit.d/available/phc2sys@.conf diff --git a/board/common/rootfs/etc/finit.d/available/phc2sys@.conf b/board/common/rootfs/etc/finit.d/available/phc2sys@.conf new file mode 100644 index 000000000..3dfe8e508 --- /dev/null +++ b/board/common/rootfs/etc/finit.d/available/phc2sys@.conf @@ -0,0 +1,3 @@ +service name:phc2sys :%i log:prio:daemon,tag:phc2sys-%i \ + [2345] phc2sys -a -z /var/run/ptp4l-%i \ + -- PHC synchronization for PTP instance %i diff --git a/src/confd/src/ptp.c b/src/confd/src/ptp.c index a8f8d84c7..38b2204a2 100644 --- a/src/confd/src/ptp.c +++ b/src/confd/src/ptp.c @@ -305,6 +305,80 @@ static int write_instance_conf(struct lyd_node *inst, json_t *root) return SR_ERR_OK; } +/* + * True when a PTP instance needs a phc2sys companion to keep all its + * PHC devices in sync. Required for BC and TC instances on hardware + * with multiple switch chips, where each chip has its own /dev/ptpN. + * On single-chip hardware the function is a no-op: phc2sys -a finds + * no second PHC and exits immediately. OC has one port → one PHC, + * so no sync is ever needed. + */ +static bool needs_phc2sys(struct lyd_node *inst, json_t *root) +{ + struct lyd_node *default_ds = lydx_get_child(inst, "default-ds"); + const char *type = lydx_get_cattr(default_ds, "instance-type"); + + if (!type) + return false; + if (strcmp(type, "bc") && strcmp(type, "p2p-tc") && strcmp(type, "e2e-tc")) + return false; + + return !strcmp(instance_time_stamping(inst, root), "hardware"); +} + +/* + * Enable the phc2sys@ companion service for a multi-port HW instance. + * phc2sys -a subscribes to ptp4l's UDS, discovers the active slave + * port via BMCA, and disciplines all other PHCs to match it. + * No config file is needed — the UDS path is passed on the command line. + */ +static void activate_phc2sys(uint16_t idx) +{ + finit_enablef("phc2sys@%u", idx); + finit_reloadf("phc2sys@%u", idx); +} + +static void deactivate_phc2sys(uint16_t idx) +{ + finit_disablef("phc2sys@%u", idx); +} + +/* + * Disable any phc2sys@ services whose index is no longer configured. + */ +static void cleanup_stale_phc2sys(struct lyd_node *config) +{ + const struct dirent *ent; + struct lyd_node *inst; + DIR *d; + int idx; + + d = opendir(FINIT_RCSD "/enabled"); + if (!d) + return; + + while ((ent = readdir(d))) { + bool found = false; + + if (sscanf(ent->d_name, "phc2sys@%d.conf", &idx) != 1) + continue; + + LYX_LIST_FOR_EACH(lydx_get_descendant(config, "ptp", "instances", "instance", NULL), + inst, "instance") { + const char *v = lydx_get_cattr(inst, "instance-index"); + if (v && atoi(v) == idx) { + found = true; + break; + } + } + + if (!found) + deactivate_phc2sys((uint16_t)idx); + } + + closedir(d); +} + /* * Remove staging config for one instance. */ @@ -423,12 +497,18 @@ static int change(sr_session_ctx_t *session, struct lyd_node *config, const char *v = lydx_get_cattr(inst, "instance-index"); if (!v) continue; - if ((rc = activate_instance((uint16_t)atoi(v)))) + uint16_t idx = (uint16_t)atoi(v); + if ((rc = activate_instance(idx))) return rc; + if (needs_phc2sys(inst, confd->root)) + activate_phc2sys(idx); + else + deactivate_phc2sys(idx); } /* Disable stale services not in current config */ cleanup_stale_instances(config); + cleanup_stale_phc2sys(config); if (!instances) return SR_ERR_OK; From 95bd795347a35d8218f2e5bb57d5b60594efdee1 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 13 Apr 2026 18:19:00 +0200 Subject: [PATCH 7/9] test: add some basic ptp tests Signed-off-by: Joachim Wiberg --- doc/testing.md | 47 +++++ src/confd/src/ptp.c | 22 ++- test/case/all.yaml | 3 + test/case/containers/basic/test.adoc | 4 +- test/case/containers/bridge/test.adoc | 4 +- test/case/containers/enabled/test.adoc | 4 +- test/case/containers/environment/test.adoc | 4 +- test/case/containers/phys/test.adoc | 4 +- test/case/containers/veth/test.adoc | 4 +- test/case/dhcp/client_routes/test.adoc | 4 +- test/case/hardware/gps_simple/test.adoc | 4 +- test/case/hardware/usb/test.adoc | 4 +- test/case/hardware/usb_two_ports/test.adoc | 4 +- test/case/hardware/watchdog/test.adoc | 4 +- test/case/interfaces/bridge_basic/test.adoc | 4 +- .../interfaces/bridge_fwd_dual_dut/test.adoc | 4 +- .../interfaces/bridge_fwd_sgl_dut/test.adoc | 4 +- test/case/interfaces/bridge_veth/test.adoc | 4 +- .../bridge_vlan_separation/test.adoc | 4 +- test/case/interfaces/dual_bridge/test.adoc | 4 +- .../interfaces/iface_enable_disable/test.adoc | 4 +- .../interfaces/iface_phys_address/test.adoc | 4 +- test/case/interfaces/igmp_basic/test.adoc | 4 +- test/case/interfaces/ipv6_address/test.adoc | 4 +- test/case/interfaces/lag_basic/test.adoc | 4 +- test/case/interfaces/routing_basic/test.adoc | 4 +- .../static_multicast_filters/test.adoc | 4 +- test/case/interfaces/tunnel_basic/gre.adoc | 4 +- test/case/interfaces/tunnel_basic/gretap.adoc | 4 +- test/case/interfaces/tunnel_basic/vxlan.adoc | 4 +- .../interfaces/tunnel_bridged/gretap.adoc | 4 +- .../case/interfaces/tunnel_bridged/vxlan.adoc | 4 +- test/case/interfaces/tunnel_ttl/gre.adoc | 4 +- test/case/interfaces/tunnel_ttl/vxlan.adoc | 4 +- .../verify_all_interface_types/test.adoc | 4 +- test/case/interfaces/veth_delete/test.adoc | 4 +- test/case/interfaces/vlan_ping/test.adoc | 4 +- .../interfaces/wireguard_multipoint/test.adoc | 4 +- test/case/misc/operational_all/test.adoc | 4 +- test/case/misc/support_collect/test.adoc | 4 +- .../ntp/client_stratum_selection/test.adoc | 4 +- test/case/ntp/server_client/test.adoc | 4 +- test/case/ntp/server_mode_peer/test.adoc | 4 +- test/case/ntp/server_mode_server/test.adoc | 4 +- .../case/ntp/server_mode_standalone/test.adoc | 4 +- test/case/ptp/Readme.adoc | 46 +++++ test/case/ptp/all.yaml | 18 ++ test/case/ptp/basic/Readme.adoc | 6 + test/case/ptp/basic/ieee1588.adoc | 28 +++ test/case/ptp/basic/ieee1588.py | 1 + test/case/ptp/basic/ieee802dot1as.adoc | 28 +++ test/case/ptp/basic/ieee802dot1as.py | 1 + test/case/ptp/basic/test.adoc | 29 +++ test/case/ptp/basic/test.py | 107 +++++++++++ test/case/ptp/basic/test.yaml | 11 ++ test/case/ptp/basic/topology.dot | 33 ++++ test/case/ptp/basic/topology.svg | 62 ++++++ test/case/ptp/bmca/Readme.adoc | 6 + test/case/ptp/bmca/ieee1588.adoc | 39 ++++ test/case/ptp/bmca/ieee1588.py | 1 + test/case/ptp/bmca/ieee802dot1as.adoc | 39 ++++ test/case/ptp/bmca/ieee802dot1as.py | 1 + test/case/ptp/bmca/test.adoc | 5 + test/case/ptp/bmca/test.py | 113 +++++++++++ test/case/ptp/bmca/test.yaml | 11 ++ test/case/ptp/bmca/topology.dot | 33 ++++ test/case/ptp/bmca/topology.svg | 60 ++++++ test/case/ptp/boundary_clock/Readme.adoc | 6 + test/case/ptp/boundary_clock/ieee1588.adoc | 41 ++++ test/case/ptp/boundary_clock/ieee1588.py | 1 + .../ptp/boundary_clock/ieee802dot1as.adoc | 41 ++++ test/case/ptp/boundary_clock/ieee802dot1as.py | 1 + test/case/ptp/boundary_clock/test.adoc | 5 + test/case/ptp/boundary_clock/test.py | 179 ++++++++++++++++++ test/case/ptp/boundary_clock/test.yaml | 11 ++ test/case/ptp/boundary_clock/topology.dot | 42 ++++ test/case/ptp/boundary_clock/topology.svg | 90 +++++++++ test/case/ptp/port_recovery/Readme.adoc | 1 + test/case/ptp/port_recovery/test.adoc | 32 ++++ test/case/ptp/port_recovery/test.py | 118 ++++++++++++ test/case/ptp/port_recovery/topology.dot | 33 ++++ test/case/ptp/port_recovery/topology.svg | 62 ++++++ test/case/ptp/servo/Readme.adoc | 1 + test/case/ptp/servo/test.adoc | 44 +++++ test/case/ptp/servo/test.py | 154 +++++++++++++++ test/case/ptp/servo/topology.dot | 33 ++++ test/case/ptp/servo/topology.svg | 62 ++++++ test/case/ptp/transparent_clock/Readme.adoc | 10 + test/case/ptp/transparent_clock/e2e.adoc | 43 +++++ test/case/ptp/transparent_clock/e2e.py | 1 + .../ptp/transparent_clock/ieee802dot1as.adoc | 43 +++++ .../ptp/transparent_clock/ieee802dot1as.py | 1 + test/case/ptp/transparent_clock/p2p.adoc | 43 +++++ test/case/ptp/transparent_clock/p2p.py | 1 + test/case/ptp/transparent_clock/test.adoc | 46 +++++ test/case/ptp/transparent_clock/test.py | 175 +++++++++++++++++ test/case/ptp/transparent_clock/test.yaml | 15 ++ test/case/ptp/transparent_clock/topology.dot | 42 ++++ test/case/ptp/transparent_clock/topology.svg | 90 +++++++++ .../ospf_default_route_advertise/test.adoc | 4 +- test/case/routing/ospf_multiarea/test.adoc | 4 +- .../ospf_unnumbered_interface/test.adoc | 4 +- test/case/routing/static_routing/test.adoc | 4 +- .../services/lldp/lldp_admin_status/test.adoc | 4 +- .../lldp/lldp_enable_disable/test.adoc | 4 +- .../services/mdns/mdns_allow_deny/test.adoc | 4 +- .../mdns/mdns_enable_disable/test.adoc | 4 +- .../services/mdns/mdns_reflector/test.adoc | 4 +- .../ssh/ssh_key_authentication/test.adoc | 4 +- .../services/ssh/ssh_server_config/test.adoc | 4 +- test/case/syslog/remote/test.adoc | 4 +- test/case/system/add_delete_user/test.adoc | 4 +- test/case/system/hostname/test.adoc | 4 +- test/case/system/nacm-basic/test.adoc | 4 +- test/case/system/ntp_client/test.adoc | 4 +- test/case/system/timezone/test.adoc | 4 +- .../case/system/timezone_utc_offset/test.adoc | 4 +- test/case/system/upgrade/test.adoc | 4 +- test/case/system/user_admin/test.adoc | 4 +- .../dhcp_ntp_dns_combination/test.adoc | 4 +- test/infamy/ptp.py | 142 ++++++++++++++ test/spec/Readme.adoc.in | 4 + 122 files changed, 2483 insertions(+), 132 deletions(-) create mode 100644 test/case/ptp/Readme.adoc create mode 100644 test/case/ptp/all.yaml create mode 100644 test/case/ptp/basic/Readme.adoc create mode 100644 test/case/ptp/basic/ieee1588.adoc create mode 120000 test/case/ptp/basic/ieee1588.py create mode 100644 test/case/ptp/basic/ieee802dot1as.adoc create mode 120000 test/case/ptp/basic/ieee802dot1as.py create mode 100644 test/case/ptp/basic/test.adoc create mode 100755 test/case/ptp/basic/test.py create mode 100644 test/case/ptp/basic/test.yaml create mode 100644 test/case/ptp/basic/topology.dot create mode 100644 test/case/ptp/basic/topology.svg create mode 100644 test/case/ptp/bmca/Readme.adoc create mode 100644 test/case/ptp/bmca/ieee1588.adoc create mode 120000 test/case/ptp/bmca/ieee1588.py create mode 100644 test/case/ptp/bmca/ieee802dot1as.adoc create mode 120000 test/case/ptp/bmca/ieee802dot1as.py create mode 100644 test/case/ptp/bmca/test.adoc create mode 100755 test/case/ptp/bmca/test.py create mode 100644 test/case/ptp/bmca/test.yaml create mode 100644 test/case/ptp/bmca/topology.dot create mode 100644 test/case/ptp/bmca/topology.svg create mode 100644 test/case/ptp/boundary_clock/Readme.adoc create mode 100644 test/case/ptp/boundary_clock/ieee1588.adoc create mode 120000 test/case/ptp/boundary_clock/ieee1588.py create mode 100644 test/case/ptp/boundary_clock/ieee802dot1as.adoc create mode 120000 test/case/ptp/boundary_clock/ieee802dot1as.py create mode 100644 test/case/ptp/boundary_clock/test.adoc create mode 100755 test/case/ptp/boundary_clock/test.py create mode 100644 test/case/ptp/boundary_clock/test.yaml create mode 100644 test/case/ptp/boundary_clock/topology.dot create mode 100644 test/case/ptp/boundary_clock/topology.svg create mode 120000 test/case/ptp/port_recovery/Readme.adoc create mode 100644 test/case/ptp/port_recovery/test.adoc create mode 100755 test/case/ptp/port_recovery/test.py create mode 100644 test/case/ptp/port_recovery/topology.dot create mode 100644 test/case/ptp/port_recovery/topology.svg create mode 120000 test/case/ptp/servo/Readme.adoc create mode 100644 test/case/ptp/servo/test.adoc create mode 100755 test/case/ptp/servo/test.py create mode 100644 test/case/ptp/servo/topology.dot create mode 100644 test/case/ptp/servo/topology.svg create mode 100644 test/case/ptp/transparent_clock/Readme.adoc create mode 100644 test/case/ptp/transparent_clock/e2e.adoc create mode 120000 test/case/ptp/transparent_clock/e2e.py create mode 100644 test/case/ptp/transparent_clock/ieee802dot1as.adoc create mode 120000 test/case/ptp/transparent_clock/ieee802dot1as.py create mode 100644 test/case/ptp/transparent_clock/p2p.adoc create mode 120000 test/case/ptp/transparent_clock/p2p.py create mode 100644 test/case/ptp/transparent_clock/test.adoc create mode 100755 test/case/ptp/transparent_clock/test.py create mode 100644 test/case/ptp/transparent_clock/test.yaml create mode 100644 test/case/ptp/transparent_clock/topology.dot create mode 100644 test/case/ptp/transparent_clock/topology.svg create mode 100644 test/infamy/ptp.py diff --git a/doc/testing.md b/doc/testing.md index eb3d14760..99abffdcd 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 38b2204a2..c52be690c 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 3e75ace0c..1fdbec3d5 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 e4e543625..7a55ccf31 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 a99dc0919..28f8e458c 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 574e2d931..d2eb4e80f 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 54b8378b9..63eba8be9 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 4a2214542..9f5221500 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 dd557d043..7adb242e5 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 5a35e4f46..2bd6e2d06 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 2715ce0f7..ab3a8d40a 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 69d0c01f9..5ce510abf 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 98d10f66b..cb9e1f6bc 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 ca904cd89..7f2fd15f6 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 58ee1c23a..f943917ab 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 aa6f1e771..c5fa45d18 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 3469e71e3..496d40f1b 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 b3826e90d..acf0a2be8 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 ae06a6e15..cf3282574 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 440e6b119..913a2aff4 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 ba0abf80a..05ed3f838 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 a78817a95..20751240c 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 6a7c51d5b..b1015c5e4 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 048e1c394..810bb0d64 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 443928d8e..ff34995ce 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 1f896230f..f802b81dc 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 e36f9156c..9b297c033 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 745f8ec9d..83b4cc356 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 b358f766f..3701a6547 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 3b4cba0f7..dab247730 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 e664f3f82..51ab43914 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 d6a463d6f..c4531383f 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 fcbfa2961..10f47106d 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 022df7c76..ca2f5a6f8 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 fc15c7d08..e82bbbaf2 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 95b9992ef..372faa71a 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 2b4a211b7..87d91df0b 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 265ac3fe3..14e60dbc0 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 c4e263966..a50ce7dfb 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 c3d4f6a64..70dd100f1 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 56f5ebc52..07a9ecc3a 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 abeb0a336..0f271de6b 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 2469b6717..87c02db13 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 e4cf34eb8..0169a2e82 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 4f7cef5c7..8255e113c 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 000000000..f47f2f3ae --- /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 000000000..a86f28853 --- /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 000000000..cb3079ad3 --- /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 000000000..bfd582fee --- /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 000000000..946566431 --- /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 000000000..3fbc7a1bf --- /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 000000000..946566431 --- /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 000000000..3b02110b7 --- /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 000000000..6d8b50b41 --- /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 000000000..4a4424f8b --- /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 000000000..efa656b63 --- /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 000000000..92ee2de21 --- /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 000000000..cb3079ad3 --- /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 000000000..8ec36caf7 --- /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 000000000..946566431 --- /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 000000000..625efadb4 --- /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 000000000..946566431 --- /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 000000000..1080fefc5 --- /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 000000000..615ee60f4 --- /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 000000000..3246043c7 --- /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 000000000..1d9dcc79b --- /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 000000000..04f715050 --- /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 000000000..cb3079ad3 --- /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 000000000..6aa262fd0 --- /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 000000000..946566431 --- /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 000000000..9293a8463 --- /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 000000000..946566431 --- /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 000000000..1080fefc5 --- /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 000000000..552051a3e --- /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 000000000..5277fe19d --- /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 000000000..66a895d14 --- /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 000000000..411c29ae0 --- /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 000000000..ae32c8412 --- /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 000000000..e58d2f0c2 --- /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 000000000..740d907ff --- /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 000000000..4544ca5ad --- /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 000000000..8c5b1311d --- /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 000000000..ae32c8412 --- /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 000000000..2e297f60e --- /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 000000000..27576fb2d --- /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 000000000..cb32dd36a --- /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 000000000..3a9164474 --- /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 000000000..164bfb7f6 --- /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 000000000..5346f9070 --- /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 000000000..946566431 --- /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 000000000..68d64b802 --- /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 000000000..946566431 --- /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 000000000..cacdb4499 --- /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 000000000..946566431 --- /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 000000000..a07dcf860 --- /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 000000000..f309e89db --- /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 000000000..4bbc87daf --- /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 000000000..3009cf821 --- /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 000000000..cfa0ceb65 --- /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 c3d24e59e..d0aa18992 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 219191523..c6e80935e 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 d066026a3..c6014db35 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 70782fc72..b16073d60 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 58ad98a05..f87f53c6e 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 5f8a02357..90c726ebe 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 2045daca4..c9f736e88 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 aea23bce2..1430ee993 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 4937cd742..75cce147d 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 6d48b5242..93a1fa867 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 801a16b97..37e37a27f 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 5535ac10f..a37b9844b 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 ebbed34ab..23335e5ad 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 7a71726bd..a8a2d1c51 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 d18b8d16b..607aef296 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 6bd64e3e7..fa3315e25 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 d266feabe..056342c02 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 a043e0ae8..c3493680b 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 de3058e71..7bbb6ca21 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 d8ffc3708..a271ce281 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 4d32bc6b7..007ffa766 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 000000000..a2b8826c0 --- /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 0e6a1dbe4..524941533 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[] <<< From e1e2ada4b4c20baf7bd3efefb9c3f10a03881707 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 24 Apr 2026 14:29:28 +0200 Subject: [PATCH 8/9] confd: fix review comments and code cleanup Signed-off-by: Joachim Wiberg --- src/confd/src/ptp.c | 76 +++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 44 deletions(-) diff --git a/src/confd/src/ptp.c b/src/confd/src/ptp.c index c52be690c..e35e2cc85 100644 --- a/src/confd/src/ptp.c +++ b/src/confd/src/ptp.c @@ -25,6 +25,7 @@ static const char *instance_type_to_clock_type(const char *type) return "P2P_TC"; if (!strcmp(type, "e2e-tc")) return "E2E_TC"; + return NULL; } @@ -101,32 +102,34 @@ static bool iface_has_hw_timestamp(json_t *root, const char *ifname) return false; } +static uint16_t instance(struct lyd_node *inst) +{ + return (uint16_t)atoi(lydx_get_cattr(inst, "instance-index")); +} + /* * Scan all ports of inst and determine whether to use hardware or software * timestamping. Emits a syslog WARNING when falling back to software due * to a mixed or software-only set of port interfaces. */ -static const char *instance_time_stamping(struct lyd_node *inst, json_t *root) +static const char *instance_time_stamping(uint16_t idx, struct lyd_node *inst, json_t *root) { + const char *ifname = NULL; struct lyd_node *port; - bool any_sw = false; LYX_LIST_FOR_EACH(lyd_child(lydx_get_child(inst, "ports")), port, "port") { const char *iface = lydx_get_cattr(port, "underlying-interface"); - if (!iface) - continue; if (iface_has_hw_timestamp(root, iface)) continue; - any_sw = true; + ifname = iface; break; } - if (any_sw) { - WARN("PTP instance has software-only timestamping port(s), " - "falling back to time_stamping software"); - + if (ifname) { + WARN("PTP instance %u will use software based timestamping due to " + "missing hardware support on %s", idx, ifname); return "software"; } @@ -151,10 +154,7 @@ static int write_instance_conf(struct lyd_node *inst, json_t *root) uint16_t idx; FILE *fp; - v = lydx_get_cattr(inst, "instance-index"); - if (!v) - return SR_ERR_INVAL_ARG; - idx = (uint16_t)atoi(v); + idx = instance(inst); snprintf(path, sizeof(path), PTP_CONF_DIR "/ptp4l-%u.conf+", idx); fp = fopen(path, "w"); @@ -178,7 +178,7 @@ 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 */ - ts = instance_time_stamping(inst, root); + ts = instance_time_stamping(idx, inst, root); fprintf(fp, "time_stamping %s\n", ts); /* Profile — sets transportSpecific and all protocol-mandatory options */ @@ -200,10 +200,8 @@ static int write_instance_conf(struct lyd_node *inst, json_t *root) * boundary_clock_jbod silences ptp4l's startup PHC-mismatch check and * lets each port use its own PHC. It is a no-op when all ports share * the same PHC (single-chip board or software timestamping). - * - * NOTE: for BC on multi-chip hardware, ptp4l only disciplines the PHC - * of the active slave port; the other chips' PHCs will drift unless - * phc2sys(8) -a is also run. That is a separate service (TODO). + * On multi-chip hardware, phc2sys -a disciplines the secondary PHCs; + * see needs_phc2sys() / activate_phc2sys(). */ if (tc || bc) fprintf(fp, "boundary_clock_jbod 1\n"); @@ -333,7 +331,7 @@ static bool needs_phc2sys(struct lyd_node *inst, json_t *root) if (strcmp(type, "bc") && strcmp(type, "p2p-tc") && strcmp(type, "e2e-tc")) return false; - return !strcmp(instance_time_stamping(inst, root), "hardware"); + return !strcmp(instance_time_stamping(instance(inst), inst, root), "hardware"); } /* @@ -359,24 +357,23 @@ static void deactivate_phc2sys(uint16_t idx) static void cleanup_stale_phc2sys(struct lyd_node *config) { const struct dirent *ent; - struct lyd_node *inst; DIR *d; - int idx; d = opendir(FINIT_RCSD "/enabled"); if (!d) return; while ((ent = readdir(d))) { + struct lyd_node *inst, *instances; bool found = false; + int idx; if (sscanf(ent->d_name, "phc2sys@%d.conf", &idx) != 1) continue; - LYX_LIST_FOR_EACH(lydx_get_descendant(config, "ptp", "instances", "instance", NULL), - inst, "instance") { - const char *v = lydx_get_cattr(inst, "instance-index"); - if (v && atoi(v) == idx) { + instances = lydx_get_descendant(config, "ptp", "instances", "instance", NULL); + LYX_LIST_FOR_EACH(instances, inst, "instance") { + if (instance(inst) == idx) { found = true; break; } @@ -445,8 +442,6 @@ static void deactivate_instance(uint16_t idx) static void cleanup_stale_instances(struct lyd_node *config) { const struct dirent *ent; - struct lyd_node *inst; - int idx; DIR *d; d = opendir(FINIT_RCSD "/enabled"); @@ -454,16 +449,17 @@ static void cleanup_stale_instances(struct lyd_node *config) return; while ((ent = readdir(d))) { + struct lyd_node *inst, *instances; bool found = false; + int idx; if (sscanf(ent->d_name, "ptp4l@%d.conf", &idx) != 1) continue; /* Is this index still configured? */ - LYX_LIST_FOR_EACH(lydx_get_descendant(config, "ptp", "instances", "instance", NULL), - inst, "instance") { - const char *v = lydx_get_cattr(inst, "instance-index"); - if (v && atoi(v) == idx) { + instances = lydx_get_descendant(config, "ptp", "instances", "instance", NULL); + LYX_LIST_FOR_EACH(instances, inst, "instance") { + if (instance(inst) == idx) { found = true; break; } @@ -493,23 +489,19 @@ static int change(sr_session_ctx_t *session, struct lyd_node *config, case SR_EV_ABORT: /* Remove any staging files */ instances = lydx_get_descendant(config, "ptp", "instances", "instance", NULL); - LYX_LIST_FOR_EACH(instances, inst, "instance") { - const char *v = lydx_get_cattr(inst, "instance-index"); - if (v) - remove_staging((uint16_t)atoi(v)); - } + LYX_LIST_FOR_EACH(instances, inst, "instance") + remove_staging(instance(inst)); return SR_ERR_OK; case SR_EV_DONE: /* Activate all configured instances */ instances = lydx_get_descendant(config, "ptp", "instances", "instance", NULL); LYX_LIST_FOR_EACH(instances, inst, "instance") { - const char *v = lydx_get_cattr(inst, "instance-index"); - if (!v) - continue; - uint16_t idx = (uint16_t)atoi(v); + uint16_t idx = instance(inst); + if ((rc = activate_instance(idx))) return rc; + if (needs_phc2sys(inst, confd->root)) activate_phc2sys(idx); else @@ -519,10 +511,6 @@ static int change(sr_session_ctx_t *session, struct lyd_node *config, /* Disable stale services not in current config */ cleanup_stale_instances(config); cleanup_stale_phc2sys(config); - - if (!instances) - return SR_ERR_OK; - return SR_ERR_OK; default: From 4ece445c4478f35ea1212ac1053a35642904a155 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 18 Apr 2026 08:38:47 +0200 Subject: [PATCH 9/9] doc: update ChangeLog Signed-off-by: Joachim Wiberg --- doc/ChangeLog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 7c7840c9f..f5db06add 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -10,6 +10,9 @@ All notable changes to the project are documented in this file. - Upgrade Linux kernel to 6.18.24 (LTS) - Upgrade Buildroot to 2025.02.13 (LTS) +- Add support for PTP/gPTP (IEEE 1588-2019 / 802.1AS) clock synchronization. + Supported clock types: Ordinary Clock, Boundary Clock, and Transparent Clock. + See the User Guide for configuration details - Add support for [Banana Pi BPI-R4][BPI-R4], quad-core Cortex-A73 router with 4x 2.5 GbE switching, dual 10 GbE SFP+. Variants BPI-R4-2g5 and BPI-R4P have one SFP+ replaced by a 2.5 GbE RJ45, with optional PoE on the R4P