Tara Subedi 5ae719a36e PTP: Change overall sync-state behavior
Currently overall sync-state (/sync/sync-status/sync-state) represents
degraded (HOLDOVER/FREERUN) when any state is degraded: os-clock-sync-
state, ptp-status/lock-state, gnss-status/gnss-sync-status.

This commit changes overall sync-state to represent whether the os-clock
is synced or not to the timing source.

Overall sync-state should only be degraded if the timing sources
involved in providing the end-to-end timing is degraded or lost (i.e. it
should represent the overall timing chain status). ptp4l instances that
perform timing distribution only and are not part of the host timing
chain must not impact the overall sync-state. os-clock-state should only
represent the status of the OS clock sync state to the primary clock
source in an HA configuration.

Unit tests has been added and unit test's python interpreter has been
changed to python3.9 to inline with base-image's python version (3.9.2).

TEST PLAN:
   PASS: T-GM deployment with two ptp4l instances
         trigger different events (stop/start ptp4l/phc2sys/ts2phc
         instances, change clockClass, skew clocks, disable GNSS)
         overall sync-state is not affected by ptp-inst1 and ptp-inst2
         states.
   PASS: T-BC deployment with two ptp4l instances
         trigger different events (stop/start ptp4l/phc2sys instances,
         change clockClass, skew clocks)
         overall sync-state is not affected by secondary ptp instance
         state.

   PASS: Hybrid (T-GM/T-BC) HA deployement: HA with GNSS and PTP source
         trigger different events
         overall sync-state is result of chained relation to chosen
         primary source

Story: 2011370
Task: 51774

Change-Id: Ibfb7fa0f9f8ad09584a5f28b60b0e4649976932c
Signed-off-by: Tara Nath Subedi <tara.subedi@windriver.com>
2025-04-10 16:03:35 -04:00

518 lines
18 KiB
Python

#
# Copyright (c) 2025 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import mock
import unittest
import json
import time
from dataclasses import dataclass
from trackingfunctionsdk.common.helpers import constants
from trackingfunctionsdk.model.dto.osclockstate import OsClockState
from trackingfunctionsdk.model.dto.overallclockstate import OverallClockState
from trackingfunctionsdk.model.dto.ptpstate import PtpState
from trackingfunctionsdk.model.dto.gnssstate import GnssState
from trackingfunctionsdk.services.daemon import PtpWatcherDefault
context = {
"THIS_NAMESPACE": "notification",
"THIS_NODE_NAME": "controller-0",
"THIS_POD_IP": "172.16.192.71",
"REGISTRATION_TRANSPORT_ENDPOINT": "rabbit://admin:admin@registration.notification.svc.cluster.local:5672",
"NOTIFICATION_TRANSPORT_ENDPOINT": "rabbit://admin:admin@172.16.192.71:5672",
"GNSS_CONFIGS": [
"/ptp/linuxptp/ptpinstance/ts2phc-ts1.conf",
"/ptp/linuxptp/ptpinstance/ts2phc-ts2.conf",
],
"PHC2SYS_CONFIG": "/ptp/linuxptp/ptpinstance/phc2sys-phc-inst1.conf",
"PHC2SYS_SERVICE_NAME": "phc-inst1",
"PTP4L_CONFIGS": [
"/ptp/linuxptp/ptpinstance/ptp4l-ptp-inst2.conf",
"/ptp/linuxptp/ptpinstance/ptp4l-ptp-inst1.conf",
],
"GNSS_INSTANCES": ["ts1", "ts2"],
"PTP4L_INSTANCES": ["ptp-inst2", "ptp-inst1"],
"ptptracker_context": {"device_simulated": "false", "holdover_seconds": "15"},
"gnsstracker_context": {"holdover_seconds": 30},
"osclocktracker_context": {"holdover_seconds": "15"},
"overalltracker_context": {"holdover_seconds": "15"},
}
@dataclass
class OsClockData:
sync_state: str = OsClockState.Freerun
sync_source: str = "ptp0"
@dataclass
class PTP4lData:
ptp_devices: list[str]
sync_state: str = PtpState.Freerun
sync_source: str = constants.ClockSourceType.TypePTP
@dataclass
class ts2phcData:
ptp_devices: list[str]
sync_state: str = GnssState.Failure_Nofix
@dataclass
class TestData:
osclock: OsClockData
ptp4l: list[PTP4lData]
ts2phc: list[ts2phcData]
class DaemonTests(unittest.TestCase):
@mock.patch("trackingfunctionsdk.services.daemon.PtpMonitor")
@mock.patch("trackingfunctionsdk.services.daemon.OsClockMonitor")
@mock.patch("trackingfunctionsdk.services.daemon.GnssMonitor")
def _setup(self, gnssmonitor_mock, osclockmonitor_mock, ptpmonitor_mock):
event = None
sqlalchemy_conf = {
"url": "sqlite:///apiserver.db",
"echo": False,
"echo_pool": False,
"pool_recycle": 3600,
"encoding": "utf-8",
}
sqlalchemy_conf_json = json.dumps(sqlalchemy_conf)
daemon_context_json = json.dumps(context)
# distint mock class instances, to have distinct mock method on instance basis
gnssmonitor_mock.side_effect = [
mock.Mock(name=item) for item in context["GNSS_CONFIGS"]
]
ptpmonitor_mock.side_effect = [
mock.Mock(name=item) for item in context["PTP4L_CONFIGS"]
]
self.worker = PtpWatcherDefault(
event, sqlalchemy_conf_json, daemon_context_json
)
self.osclockmonitor_mock_instance = self.worker.os_clock_monitor
self.ptpmonitor_mock_instances = self.worker.ptp_monitor_list
self.gnssmonitor_mock_instances = self.worker.observer_list
self.assertEqual(
len(self.gnssmonitor_mock_instances), len(context["GNSS_CONFIGS"])
)
self.assertEqual(
len(self.ptpmonitor_mock_instances), len(context["PTP4L_CONFIGS"])
)
def _test__get_overall_sync_state(self, testdata, expected):
holdover_time = float(context["overalltracker_context"]["holdover_seconds"])
freq = 2
sync_state = OverallClockState.Freerun
last_event_time = time.time()
self.osclockmonitor_mock_instance.get_source_ptp_device.return_value = (
testdata.osclock.sync_source
)
self.osclockmonitor_mock_instance.get_os_clock_state.return_value = (
testdata.osclock.sync_state
)
# test mocking as expected or not.
self.assertEqual(
self.worker.os_clock_monitor.get_source_ptp_device(),
testdata.osclock.sync_source,
)
self.assertEqual(
self.worker.os_clock_monitor.get_os_clock_state(),
testdata.osclock.sync_state,
)
for i, gnssmonitor_mock_instance in enumerate(self.gnssmonitor_mock_instances):
gnssmonitor_mock_instance.get_ptp_devices.return_value = testdata.ts2phc[
i
].ptp_devices
gnssmonitor_mock_instance._state = testdata.ts2phc[i].sync_state
# test mocking as expected or not.
self.assertEqual(
self.gnssmonitor_mock_instances[0].get_ptp_devices(),
testdata.ts2phc[0].ptp_devices,
)
self.assertEqual(
self.gnssmonitor_mock_instances[0]._state, testdata.ts2phc[0].sync_state
)
self.assertEqual(
self.gnssmonitor_mock_instances[1].get_ptp_devices(),
testdata.ts2phc[1].ptp_devices,
)
self.assertEqual(
self.gnssmonitor_mock_instances[1]._state, testdata.ts2phc[1].sync_state
)
for i, ptpmonitor_mock_instance in enumerate(self.ptpmonitor_mock_instances):
ptpmonitor_mock_instance.get_ptp_devices.return_value = testdata.ptp4l[
i
].ptp_devices
ptpmonitor_mock_instance.get_ptp_sync_state.return_value = (
None,
testdata.ptp4l[i].sync_state,
None,
)
ptpmonitor_mock_instance.get_ptp_sync_source.return_value = testdata.ptp4l[
i
].sync_source
# test mocking as expected or not.
self.assertEqual(
self.ptpmonitor_mock_instances[0].get_ptp_devices(),
testdata.ptp4l[0].ptp_devices,
)
self.assertEqual(
self.ptpmonitor_mock_instances[0].get_ptp_sync_state(),
(None, testdata.ptp4l[0].sync_state, None),
)
self.assertEqual(
self.ptpmonitor_mock_instances[0].get_ptp_sync_source(),
testdata.ptp4l[0].sync_source,
)
self.assertEqual(
self.ptpmonitor_mock_instances[1].get_ptp_devices(),
testdata.ptp4l[1].ptp_devices,
)
self.assertEqual(
self.ptpmonitor_mock_instances[1].get_ptp_sync_state(),
(None, testdata.ptp4l[1].sync_state, None),
)
self.assertEqual(
self.ptpmonitor_mock_instances[1].get_ptp_sync_source(),
testdata.ptp4l[1].sync_source,
)
new_event, sync_state, new_event_time = (
self.worker._PtpWatcherDefault__get_overall_sync_state(
holdover_time, freq, sync_state, last_event_time
)
)
# overall sync state assertion
self.assertEqual(sync_state, expected)
def test__get_overall_sync_state__all_are_locked__overall_locked(self):
# when all are locked state -- overall state would be locked
self._setup()
osclockdata = OsClockData(sync_state=OsClockState.Locked, sync_source="ptp0")
ptp4ldata0 = PTP4lData(
ptp_devices=["ptp0"],
sync_state=PtpState.Locked,
sync_source=constants.ClockSourceType.TypePTP,
)
ptp4ldata1 = PTP4lData(
ptp_devices=["ptp1"],
sync_state=PtpState.Locked,
sync_source=constants.ClockSourceType.TypePTP,
)
ts2phcdata0 = ts2phcData(
ptp_devices=["ptp0"], sync_state=GnssState.Synchronized
)
ts2phcdata1 = ts2phcData(
ptp_devices=["ptp1"], sync_state=GnssState.Synchronized
)
testdata = TestData(
osclock=osclockdata,
ptp4l=[ptp4ldata0, ptp4ldata1],
ts2phc=[ts2phcdata0, ts2phcdata1],
)
expected = OverallClockState.Locked
self._test__get_overall_sync_state(testdata, expected)
def test__get_overall_sync_state__osclock_freerun__overall_freerun(self):
# when osclock is on freerun, and others are on locked state -- overall
# state would be freerun
self._setup()
osclockdata = OsClockData(sync_state=OsClockState.Freerun, sync_source="ptp0")
ptp4ldata0 = PTP4lData(
ptp_devices=["ptp0"],
sync_state=PtpState.Locked,
sync_source=constants.ClockSourceType.TypePTP,
)
ptp4ldata1 = PTP4lData(
ptp_devices=["ptp1"],
sync_state=PtpState.Locked,
sync_source=constants.ClockSourceType.TypePTP,
)
ts2phcdata0 = ts2phcData(
ptp_devices=["ptp0"], sync_state=GnssState.Synchronized
)
ts2phcdata1 = ts2phcData(
ptp_devices=["ptp1"], sync_state=GnssState.Synchronized
)
testdata = TestData(
osclock=osclockdata,
ptp4l=[ptp4ldata0, ptp4ldata1],
ts2phc=[ts2phcdata0, ts2phcdata1],
)
expected = OverallClockState.Freerun
self._test__get_overall_sync_state(testdata, expected)
def test__get_overall_sync_state__ptp4l_ptp0_freerun__overall_freerun(self):
# when chained ptp4l ptp0 sync_state is freerun -- overall state would be freerun
self._setup()
osclockdata = OsClockData(sync_state=OsClockState.Locked, sync_source="ptp0")
ptp4ldata0 = PTP4lData(
ptp_devices=["ptp0"],
sync_state=PtpState.Freerun,
sync_source=constants.ClockSourceType.TypePTP,
)
ptp4ldata1 = PTP4lData(
ptp_devices=["ptp1"],
sync_state=PtpState.Locked,
sync_source=constants.ClockSourceType.TypePTP,
)
ts2phcdata0 = ts2phcData(
ptp_devices=["ptp0"], sync_state=GnssState.Synchronized
)
ts2phcdata1 = ts2phcData(
ptp_devices=["ptp1"], sync_state=GnssState.Synchronized
)
testdata = TestData(
osclock=osclockdata,
ptp4l=[ptp4ldata0, ptp4ldata1],
ts2phc=[ts2phcdata0, ts2phcdata1],
)
expected = OverallClockState.Freerun
self._test__get_overall_sync_state(testdata, expected)
def test__get_overall_sync_state__ptp4l_ptp0_locked__overall_locked(self):
# when chained ptp4l ptp0 sync_state is locked -- overall state would be locked
self._setup()
osclockdata = OsClockData(sync_state=OsClockState.Locked, sync_source="ptp0")
ptp4ldata0 = PTP4lData(
ptp_devices=["ptp0"],
sync_state=PtpState.Locked,
sync_source=constants.ClockSourceType.TypePTP,
)
ptp4ldata1 = PTP4lData(
ptp_devices=["ptp1"],
sync_state=PtpState.Freerun,
sync_source=constants.ClockSourceType.TypePTP,
)
ts2phcdata0 = ts2phcData(
ptp_devices=["ptp0"], sync_state=GnssState.Failure_Nofix
)
ts2phcdata1 = ts2phcData(
ptp_devices=["ptp1"], sync_state=GnssState.Failure_Nofix
)
testdata = TestData(
osclock=osclockdata,
ptp4l=[ptp4ldata0, ptp4ldata1],
ts2phc=[ts2phcdata0, ts2phcdata1],
)
expected = OverallClockState.Locked
self._test__get_overall_sync_state(testdata, expected)
def test__get_overall_sync_state__ts2phc_ptp0_freerun__overall_freerun(self):
# when chained ts2phc ptp0 sync_state is freerun -- overall state would be freerun
self._setup()
osclockdata = OsClockData(sync_state=OsClockState.Locked, sync_source="ptp0")
ptp4ldata0 = PTP4lData(
ptp_devices=["ptp0"],
sync_state=PtpState.Locked,
sync_source=constants.ClockSourceType.TypeGNSS,
)
ptp4ldata1 = PTP4lData(
ptp_devices=["ptp1"],
sync_state=PtpState.Locked,
sync_source=constants.ClockSourceType.TypePTP,
)
ts2phcdata0 = ts2phcData(
ptp_devices=["ptp0"], sync_state=GnssState.Failure_Nofix
)
ts2phcdata1 = ts2phcData(
ptp_devices=["ptp1"], sync_state=GnssState.Synchronized
)
testdata = TestData(
osclock=osclockdata,
ptp4l=[ptp4ldata0, ptp4ldata1],
ts2phc=[ts2phcdata0, ts2phcdata1],
)
expected = OverallClockState.Freerun
self._test__get_overall_sync_state(testdata, expected)
def test__get_overall_sync_state__ts2phc_ptp0_locked__overall_locked(self):
# when chained ts2phc ptp0 sync_state is locked -- overall state would be locked
self._setup()
osclockdata = OsClockData(sync_state=OsClockState.Locked, sync_source="ptp0")
ptp4ldata0 = PTP4lData(
ptp_devices=["ptp0"],
sync_state=PtpState.Locked,
sync_source=constants.ClockSourceType.TypeGNSS,
)
ptp4ldata1 = PTP4lData(
ptp_devices=["ptp1"],
sync_state=PtpState.Freerun,
sync_source=constants.ClockSourceType.TypePTP,
)
ts2phcdata0 = ts2phcData(
ptp_devices=["ptp0"], sync_state=GnssState.Synchronized
)
ts2phcdata1 = ts2phcData(
ptp_devices=["ptp1"], sync_state=GnssState.Failure_Nofix
)
testdata = TestData(
osclock=osclockdata,
ptp4l=[ptp4ldata0, ptp4ldata1],
ts2phc=[ts2phcdata0, ts2phcdata1],
)
expected = OverallClockState.Locked
self._test__get_overall_sync_state(testdata, expected)
def test__get_overall_sync_state__ts2phc_ptp0_locked_no_ptp4l_for_ptp0__overall_locked(
self,
):
# when chained ts2phc ptp0 sync_state is locked -- overall state would be locked
# In this case there are no ptp4l instances with ptp0
self._setup()
osclockdata = OsClockData(sync_state=OsClockState.Locked, sync_source="ptp0")
ptp4ldata0 = PTP4lData(
ptp_devices=["ptpx"],
sync_state=PtpState.Freerun,
sync_source=constants.ClockSourceType.TypePTP,
)
ptp4ldata1 = PTP4lData(
ptp_devices=["ptp1"],
sync_state=PtpState.Freerun,
sync_source=constants.ClockSourceType.TypePTP,
)
ts2phcdata0 = ts2phcData(
ptp_devices=["ptp0"], sync_state=GnssState.Synchronized
)
ts2phcdata1 = ts2phcData(
ptp_devices=["ptp1"], sync_state=GnssState.Failure_Nofix
)
testdata = TestData(
osclock=osclockdata,
ptp4l=[ptp4ldata0, ptp4ldata1],
ts2phc=[ts2phcdata0, ts2phcdata1],
)
expected = OverallClockState.Locked
self._test__get_overall_sync_state(testdata, expected)
def test__get_overall_sync_state__no_source_for_ptp0__overall_freerun(self):
# when chained ptp4l ptp0 sync_source NA (neither gnss nor ptp) -- overall
# state would be freerun
# In this case there are no ts2phc instances with ptp0
self._setup()
osclockdata = OsClockData(sync_state=OsClockState.Locked, sync_source="ptp0")
# In this case, practically ptp0's ptp4l instance sync_state would be
# PtpState.Freerun, as there is no sync source. But still using locked state.
ptp4ldata0 = PTP4lData(
ptp_devices=["ptp0"],
sync_state=PtpState.Locked,
sync_source=constants.ClockSourceType.TypeNA,
)
ptp4ldata1 = PTP4lData(
ptp_devices=["ptp1"],
sync_state=PtpState.Locked,
sync_source=constants.ClockSourceType.TypePTP,
)
ts2phcdata0 = ts2phcData(
ptp_devices=["ptpx"], sync_state=GnssState.Synchronized
)
ts2phcdata1 = ts2phcData(
ptp_devices=["ptp1"], sync_state=GnssState.Synchronized
)
testdata = TestData(
osclock=osclockdata,
ptp4l=[ptp4ldata0, ptp4ldata1],
ts2phc=[ts2phcdata0, ts2phcdata1],
)
expected = OverallClockState.Freerun
self._test__get_overall_sync_state(testdata, expected)
def test__get_overall_sync_state__no_backtrack_for_ptp0__overall_freerun(self):
# when chained ptp0 is not included neither on ptp4l nor ts2phc -- overall
# state would be freerun
self._setup()
osclockdata = OsClockData(sync_state=OsClockState.Locked, sync_source="ptp0")
ptp4ldata0 = PTP4lData(
ptp_devices=["ptpx"],
sync_state=PtpState.Locked,
sync_source=constants.ClockSourceType.TypePTP,
)
ptp4ldata1 = PTP4lData(
ptp_devices=["ptp1"],
sync_state=PtpState.Locked,
sync_source=constants.ClockSourceType.TypePTP,
)
ts2phcdata0 = ts2phcData(
ptp_devices=["ptpx"], sync_state=GnssState.Synchronized
)
ts2phcdata1 = ts2phcData(
ptp_devices=["ptp1"], sync_state=GnssState.Synchronized
)
testdata = TestData(
osclock=osclockdata,
ptp4l=[ptp4ldata0, ptp4ldata1],
ts2phc=[ts2phcdata0, ts2phcdata1],
)
expected = OverallClockState.Freerun
self._test__get_overall_sync_state(testdata, expected)
def test__get_overall_sync_state__os_clock_no_ptp_device__overall_freerun(self):
# when there is no sync_source e.g. ptp0 for os_clock -- overall state would be freerun
self._setup()
osclockdata = OsClockData(sync_state=OsClockState.Locked, sync_source=None)
ptp4ldata0 = PTP4lData(
ptp_devices=["ptp0"],
sync_state=PtpState.Locked,
sync_source=constants.ClockSourceType.TypePTP,
)
ptp4ldata1 = PTP4lData(
ptp_devices=["ptp1"],
sync_state=PtpState.Locked,
sync_source=constants.ClockSourceType.TypePTP,
)
ts2phcdata0 = ts2phcData(
ptp_devices=["ptp0"], sync_state=GnssState.Synchronized
)
ts2phcdata1 = ts2phcData(
ptp_devices=["ptp1"], sync_state=GnssState.Synchronized
)
testdata = TestData(
osclock=osclockdata,
ptp4l=[ptp4ldata0, ptp4ldata1],
ts2phc=[ts2phcdata0, ts2phcdata1],
)
expected = OverallClockState.Freerun
self._test__get_overall_sync_state(testdata, expected)