mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-31 04:53:01 +02:00
Initial support for IEEE 1588/802.1AS PTP/gPTP
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 <troglobit@gmail.com>
This commit is contained in:
@@ -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":
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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-<idx>.conf and its UDS socket at
|
||||
/var/run/ptp4l-<idx>.
|
||||
"""
|
||||
|
||||
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 <conf> GET <command>' and return parsed key→value dict.
|
||||
|
||||
pmc output looks like:
|
||||
\t\t<key> <value>
|
||||
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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user