Add CguHandler, DmesgWatcher, GnssMonitor classes

The CguHandler class reads a given ts2phc config file and uses this to
derive the nmea_serialport, pci address and cgu path. These values can
be short-circuited if they are known in advance.
From there, the cgu is read and parsed into a dict in order to
easily derive the status of the various pins on the NIC.

DmesgWatcher and GnssMonitor use an observer pattern. DmesgWatcher is
the subject and follows entries in a dmesg log for patterns that
GnssMonitor observers care about. It then updates the GnssMonitor
observers with the matched entry allowing GnssMonitor to parse and
handle the update as required. The DmesgWatcher can be extended to
support other observer types in the future.

GnssMonitor attaches to DmesgWatcher and performs the handling for
changes in GNSS status. This includes sending the status change to a
publisher which will be implemented in a future review.

Unit tests are included for CguHandler and DmesgWatcher. GnssMonitor is
covered by the tests as it is built off the functionality of
CguHandler.

Tox has been updated to automatically run the unit tests.

Testing:

Pass: Unit tests pass

Story: 2010056
Task: 45500

Signed-off-by: Cole Walker <cole.walker@windriver.com>
Change-Id: I4be477aa0fce8baa418a3ff450c6b998683ec10b
This commit is contained in:
Cole Walker 2022-05-27 12:46:27 -04:00
parent d0b5a1b644
commit 04bbd6f0ff
21 changed files with 769 additions and 220 deletions

View File

@ -8,10 +8,12 @@
jobs: jobs:
- openstack-tox-linters - openstack-tox-linters
- k8sapp-ptp-notification-tox-pylint - k8sapp-ptp-notification-tox-pylint
- ptp-notification-tox-py36
gate: gate:
jobs: jobs:
- openstack-tox-linters - openstack-tox-linters
- k8sapp-ptp-notification-tox-pylint - k8sapp-ptp-notification-tox-pylint
- ptp-notification-tox-py36
post: post:
jobs: jobs:
- stx-ptp-notification-armada-app-upload-git-mirror - stx-ptp-notification-armada-app-upload-git-mirror
@ -29,6 +31,15 @@
secret: stx-ptp-notification-armada-app-github-secret secret: stx-ptp-notification-armada-app-github-secret
pass-to-parent: true pass-to-parent: true
- job:
name: ptp-notification-tox-py36
parent: tox-py36
description: |
Run py36 test for ptp-notification
nodeset: ubuntu-bionic
vars:
tox_envlist: py36
- job: - job:
name: k8sapp-ptp-notification-tox-pylint name: k8sapp-ptp-notification-tox-pylint
parent: tox parent: tox

View File

@ -0,0 +1,113 @@
#
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import logging
import os
import re
import sys
from trackingfunctionsdk.common.helpers import constants
from trackingfunctionsdk.common.helpers import log_helper
LOG = logging.getLogger(__name__)
log_helper.config_logger(LOG)
class CguHandler:
def __init__(self, config_file, nmea_serialport=None, pci_addr=None, cgu_path=None):
self.config_file = config_file
self.nmea_serialport = nmea_serialport
self.pci_addr = pci_addr
self.cgu_path = cgu_path
self.cgu_output_raw = ""
self.cgu_output_parsed = {}
def get_gnss_nmea_serialport_from_ts2phc_config(self):
# Read a tstphc config file and return the ts2phc.nmea_serialport
nmea_serialport = None
try:
with open(self.config_file, 'r') as infile:
for line in infile:
if constants.NMEA_SERIALPORT in line:
nmea_serialport = line.split(' ')[1].strip('\n')
break
self.nmea_serialport = nmea_serialport
return
except (FileNotFoundError, PermissionError) as err:
LOG.error(err)
raise
def convert_nmea_serialport_to_pci_addr(self, dmesg_path="/logs/dmesg"):
# Parse the nmea_serialport value into a PCI address so that we can later find the cgu
# Returns the address or None
pci_addr = None
# Get only the ttyGNSS_1800_0 portion of the path
nmea_serialport = self.nmea_serialport.split('/')[2]
LOG.debug("Looking for nmea_serialport value: %s" % nmea_serialport)
with open(dmesg_path, 'r') as dmesg:
for line in dmesg:
if nmea_serialport in line:
# Regex split to make any number of spaces the delimiter
# Eg. [ 4.834255] ice 0000:18:00.0: ttyGNSS_1800_0 registered
# Becomes: 0000:18:00.0
pci_addr = re.split(' +', line)[3].strip(':')
self.pci_addr = pci_addr
return
def get_cgu_path_from_pci_addr(self):
# Search for a cgu file using the given pci address
cgu_path = "/ice/" + self.pci_addr + "/cgu"
if os.path.exists(cgu_path):
LOG.debug("PCI address %s has cgu path %s" % (self.pci_addr, cgu_path))
self.cgu_path = cgu_path
return
else:
LOG.error("Could not find cgu path for PCI address %s" % self.pci_addr)
raise FileNotFoundError
def read_cgu(self):
# Read a given cgu path and return the output in a parseable structure
cgu_output = None
if os.path.exists(self.cgu_path):
with open(self.cgu_path, 'r') as infile:
cgu_output = infile.read()
self.cgu_output_raw = cgu_output
return
def cgu_output_to_dict(self):
# Take raw cgu output and parse it into a dict
cgu_output = self.cgu_output_raw.splitlines()
LOG.debug("CGU output: %s" % cgu_output)
cgu_dict = {'input': {},
'EEC DPLL': {
'Current reference': '',
'Status': ''
},
'PPS DPLL': {
'Current reference': '',
'Status': '',
'Phase offset': ''
}
}
for line in cgu_output[7:14]:
# Build a dict out of the 7 line table
dict_to_insert = {re.split(' +', line)[1]: {'state': re.split(' +', line)[4],
'priority': {'EEC': re.split(' +', line)[6],
'PPS': re.split(' +', line)[8]}
}
}
cgu_dict['input'].update(dict_to_insert)
# Add the DPLL data below the table
cgu_dict['EEC DPLL']['Current reference'] = re.split('[ \t]+', cgu_output[16])[3]
cgu_dict['EEC DPLL']['Status'] = re.split('[ \t]+', cgu_output[17])[2]
cgu_dict['PPS DPLL']['Current reference'] = re.split('[ \t]+', cgu_output[20])[3]
cgu_dict['PPS DPLL']['Status'] = re.split('[ \t]+', cgu_output[21])[2]
cgu_dict['PPS DPLL']['Phase offset'] = re.split('[ \t]+', cgu_output[22])[3]
self.cgu_output_parsed = cgu_dict
return

View File

@ -26,3 +26,13 @@ GM_IS_PRESENT = "true"
CLOCK_CLASS_VALUE1 = "6" CLOCK_CLASS_VALUE1 = "6"
CLOCK_CLASS_VALUE2 = "7" CLOCK_CLASS_VALUE2 = "7"
CLOCK_CLASS_VALUE3 = "135" CLOCK_CLASS_VALUE3 = "135"
# ts2phc constants
NMEA_SERIALPORT = "ts2phc.nmea_serialport"
GNSS_PIN = "GNSS-1PPS"
GNSS_LOCKED_HO_ACK = 'locked_ho_ack'
GNSS_DPLL_0 = "DPLL0"
GNSS_DPLL_1 = "DPLL1"
# testing values
CGU_PATH_VALID = "/sys/kernel/debug/ice/0000:18:00.0/cgu"

View File

@ -0,0 +1,69 @@
#
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import logging
from pygtail import Pygtail
from typing import List
from abc import ABC, abstractmethod
from trackingfunctionsdk.common.helpers import log_helper
from trackingfunctionsdk.common.helpers.gnss_monitor import Observer
LOG = logging.getLogger(__name__)
log_helper.config_logger(LOG)
class DmesgSubject(ABC):
@abstractmethod
def attach(self, observer: Observer) -> None:
pass
@abstractmethod
def detach(self, observer: Observer) -> None:
pass
@abstractmethod
def notify(self) -> None:
pass
class DmesgWatcher(DmesgSubject, ABC):
_observers: List[Observer] = []
_checklist = []
_matched_line = ""
def __init__(self, dmesg_log_file="/logs/kern.log"):
self.dmesg_log_file = dmesg_log_file
def parse_dmesg_event(self, dmesg_entry) -> None:
for observer in self._observers:
if observer.dmesg_values_to_check['pin'] in dmesg_entry \
and observer.dmesg_values_to_check['pci_addr'] in dmesg_entry:
matched_line = dmesg_entry
self.notify(observer, matched_line)
def run_watcher(self) -> None:
"""
This is intended to be run as a separate thread to follow the log file for events.
There is currently no support in the NIC device drivers for udev events that
would avoid polling/monitoring.
"""
while True:
for line in Pygtail(self.dmesg_log_file, offset_file="./kern.offset"):
self.parse_dmesg_event(line)
def attach(self, observer: Observer) -> None:
LOG.info("DmesgWatcher: Attached an observer.")
self._observers.append(observer)
def notify(self, observer, matched_line) -> None:
LOG.info("DmesgWatcher: Notifying observers.")
observer.update(self, matched_line)
def detach(self, observer: Observer) -> None:
self._observers.remove(observer)
LOG.debug("Removed an observer.")

View File

@ -0,0 +1,76 @@
#
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import logging
from abc import ABC, abstractmethod
from datetime import datetime
from trackingfunctionsdk.common.helpers import log_helper
from trackingfunctionsdk.common.helpers.cgu_handler import CguHandler
LOG = logging.getLogger(__name__)
log_helper.config_logger(LOG)
class Observer(ABC):
@abstractmethod
def update(self, subject, matched_line) -> None:
"""
Receive update from subject.
"""
pass
class GnssMonitor(Observer):
gnss_eec_state = ""
gnss_pps_state = ""
event_time = None
gnss_cgu_handler = None
def __init__(self, config_file, nmea_serialport=None, pci_addr=None, cgu_path=None):
self.config_file = config_file
# Setup GNSS data
self.gnss_cgu_handler = CguHandler(config_file, nmea_serialport, pci_addr, cgu_path)
if self.gnss_cgu_handler.nmea_serialport is None:
self.gnss_cgu_handler.get_gnss_nmea_serialport_from_ts2phc_config()
if self.gnss_cgu_handler.pci_addr is None:
self.gnss_cgu_handler.convert_nmea_serialport_to_pci_addr()
if self.gnss_cgu_handler.cgu_path is None:
self.gnss_cgu_handler.get_cgu_path_from_pci_addr()
self.gnss_cgu_handler.read_cgu()
self.gnss_cgu_handler.cgu_output_to_dict()
self.dmesg_values_to_check = {'pin': 'GNSS-1PPS', 'pci_addr': self.gnss_cgu_handler.pci_addr}
# Initialize status
if self.gnss_cgu_handler.cgu_output_parsed['EEC DPLL']['Current reference'] == 'GNSS-1PPS':
self.gnss_eec_state = self.gnss_cgu_handler.cgu_output_parsed['EEC DPLL']['Status']
if self.gnss_cgu_handler.cgu_output_parsed['PPS DPLL']['Current reference'] == 'GNSS-1PPS':
self.gnss_pps_state = self.gnss_cgu_handler.cgu_output_parsed['PPS DPLL']['Status']
self.event_time = datetime.now().timestamp()
def update(self, subject, matched_line) -> None:
LOG.info("Kernel event detected. %s" % matched_line)
LOG.debug("GnssMonitor handler logic would run now")
self.set_gnss_status()
def set_gnss_status(self):
self.event_time = datetime.now().timestamp()
self.gnss_cgu_handler.read_cgu()
self.gnss_cgu_handler.cgu_output_to_dict()
self.gnss_eec_state = self.gnss_cgu_handler.cgu_output_parsed['EEC DPLL']['Status']
self.gnss_pps_state = self.gnss_cgu_handler.cgu_output_parsed['PPS DPLL']['Status']
LOG.debug("GNSS EEC Status is: %s" % self.gnss_eec_state)
LOG.debug("GNSS PPS Status is: %s" % self.gnss_pps_state)
def __publish_gnss_status(self, force=False):
LOG.debug("Publish GNSS status.")
# TODO implement a publisher class to handle this
pass

View File

@ -0,0 +1,9 @@
from wsme import types as wtypes
EnumGnssState = wtypes.Enum(str, 'Locked', 'Freerun', 'Holdover')
class GnssState(object):
Locked = "Locked"
Freerun = "Freerun"
Holdover = "Holdover"

View File

@ -7,3 +7,4 @@ EnumResourceType = wtypes.Enum(str, 'PTP', 'FPGA')
class ResourceType(object): class ResourceType(object):
TypePTP = "PTP" TypePTP = "PTP"
TypeFPGA = "FPGA" TypeFPGA = "FPGA"
TypeGNSS = "GNSS"

View File

@ -1,218 +1,235 @@
# #
# Copyright (c) 2021 Wind River Systems, Inc. # Copyright (c) 2022 Wind River Systems, Inc.
# #
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# #
import os import os
import json import json
import time import time
import oslo_messaging import oslo_messaging
from oslo_config import cfg from oslo_config import cfg
import logging import logging
import multiprocessing as mp import multiprocessing as mp
import threading import threading
from trackingfunctionsdk.common.helpers import rpc_helper from trackingfunctionsdk.client.ptpeventproducer import PtpEventProducer
from trackingfunctionsdk.model.dto.rpc_endpoint import RpcEndpointInfo from trackingfunctionsdk.common.helpers import ptpsync
from trackingfunctionsdk.model.dto.resourcetype import ResourceType from trackingfunctionsdk.common.helpers import log_helper
from trackingfunctionsdk.model.dto.ptpstate import PtpState from trackingfunctionsdk.common.helpers.dmesg_watcher import DmesgWatcher
from trackingfunctionsdk.common.helpers.gnss_monitor import GnssMonitor
from trackingfunctionsdk.client.ptpeventproducer import PtpEventProducer from trackingfunctionsdk.model.dto.ptpstate import PtpState
from trackingfunctionsdk.model.dto.resourcetype import ResourceType
from trackingfunctionsdk.common.helpers import ptpsync as ptpsync from trackingfunctionsdk.model.dto.rpc_endpoint import RpcEndpointInfo
from trackingfunctionsdk.model.dto.resourcetype import ResourceType
LOG = logging.getLogger(__name__) from trackingfunctionsdk.model.dto.ptpstate import PtpState
from trackingfunctionsdk.common.helpers import log_helper from trackingfunctionsdk.client.ptpeventproducer import PtpEventProducer
log_helper.config_logger(LOG)
from trackingfunctionsdk.common.helpers import ptpsync as ptpsync
THIS_NODE_NAME = os.environ.get("THIS_NODE_NAME",'controller-0')
LOG = logging.getLogger(__name__)
'''Entry point of Default Process Worker''' from trackingfunctionsdk.common.helpers import log_helper
def ProcessWorkerDefault(event, sqlalchemy_conf_json, broker_transport_endpoint): log_helper.config_logger(LOG)
worker = PtpWatcherDefault(event, sqlalchemy_conf_json, broker_transport_endpoint)
worker.run() THIS_NODE_NAME = os.environ.get("THIS_NODE_NAME",'controller-0')
return
'''Entry point of Default Process Worker'''
class PtpWatcherDefault: def ProcessWorkerDefault(event, sqlalchemy_conf_json, broker_transport_endpoint):
DEFAULT_PTPTRACKER_CONTEXT = { worker = PtpWatcherDefault(event, sqlalchemy_conf_json, broker_transport_endpoint)
'holdover_seconds': 30, worker.run()
'poll_freq_seconds': 2 return
}
class PtpRequestHandlerDefault(object): class PtpWatcherDefault:
def __init__(self, watcher): DEFAULT_PTPTRACKER_CONTEXT = {
self.watcher = watcher 'holdover_seconds': 30,
self.init_time = time.time() 'poll_freq_seconds': 2
}
def query_status(self, **rpc_kwargs):
self.watcher.ptptracker_context_lock.acquire() class PtpRequestHandlerDefault(object):
sync_state = self.watcher.ptptracker_context.get('sync_state', PtpState.Freerun) def __init__(self, watcher):
last_event_time = self.watcher.ptptracker_context.get('last_event_time', time.time()) self.watcher = watcher
self.watcher.ptptracker_context_lock.release() self.init_time = time.time()
lastStatus = { def query_status(self, **rpc_kwargs):
'ResourceType': ResourceType.TypePTP, self.watcher.ptptracker_context_lock.acquire()
'EventData': { sync_state = self.watcher.ptptracker_context.get('sync_state', PtpState.Freerun)
'State': sync_state last_event_time = self.watcher.ptptracker_context.get('last_event_time', time.time())
}, self.watcher.ptptracker_context_lock.release()
'ResourceQualifier': {
'NodeName': self.watcher.node_name lastStatus = {
}, 'ResourceType': ResourceType.TypePTP,
'EventTimestamp': last_event_time 'EventData': {
} 'State': sync_state
return lastStatus },
'ResourceQualifier': {
def trigger_delivery(self, **rpc_kwargs): 'NodeName': self.watcher.node_name
self.watcher.forced_publishing = True },
self.watcher.signal_ptp_event() 'EventTimestamp': last_event_time
pass }
return lastStatus
def __init__(self, event, sqlalchemy_conf_json, daemon_context_json):
self.sqlalchemy_conf = json.loads(sqlalchemy_conf_json) def trigger_delivery(self, **rpc_kwargs):
self.event = event self.watcher.forced_publishing = True
self.init_time = time.time() self.watcher.signal_ptp_event()
pass
self.daemon_context = json.loads(daemon_context_json)
self.ptptracker_context = self.daemon_context.get( def __init__(self, event, sqlalchemy_conf_json, daemon_context_json):
'ptptracker_context', PtpWatcherDefault.DEFAULT_PTPTRACKER_CONTEXT) self.sqlalchemy_conf = json.loads(sqlalchemy_conf_json)
self.ptptracker_context['sync_state'] = PtpState.Freerun self.event = event
self.ptptracker_context['last_event_time'] = self.init_time self.init_time = time.time()
self.ptptracker_context_lock = threading.Lock()
self.daemon_context = json.loads(daemon_context_json)
self.ptp_device_simulated = "true" == self.ptptracker_context.get('device_simulated', "False").lower() self.ptptracker_context = self.daemon_context.get(
'ptptracker_context', PtpWatcherDefault.DEFAULT_PTPTRACKER_CONTEXT)
self.event_timeout = float(self.ptptracker_context['poll_freq_seconds']) self.ptptracker_context['sync_state'] = PtpState.Freerun
self.ptptracker_context['last_event_time'] = self.init_time
self.node_name = self.daemon_context['THIS_NODE_NAME'] self.ptptracker_context_lock = threading.Lock()
self.namespace = self.daemon_context.get('THIS_NAMESPACE', 'notification')
self.ptp_device_simulated = "true" == self.ptptracker_context.get('device_simulated',
broker_transport_endpoint = self.daemon_context['NOTIFICATION_TRANSPORT_ENDPOINT'] "False").lower()
registration_transport_endpoint = self.daemon_context['REGISTRATION_TRANSPORT_ENDPOINT'] self.event_timeout = float(self.ptptracker_context['poll_freq_seconds'])
self.broker_endpoint = RpcEndpointInfo(broker_transport_endpoint) self.node_name = self.daemon_context['THIS_NODE_NAME']
self.registration_broker_endpoint = RpcEndpointInfo(registration_transport_endpoint)
self.ptpeventproducer = PtpEventProducer( self.namespace = self.daemon_context.get('THIS_NAMESPACE', 'notification')
self.node_name,
self.broker_endpoint.TransportEndpoint, broker_transport_endpoint = self.daemon_context['NOTIFICATION_TRANSPORT_ENDPOINT']
self.registration_broker_endpoint.TransportEndpoint)
registration_transport_endpoint = self.daemon_context['REGISTRATION_TRANSPORT_ENDPOINT']
self.__ptprequest_handler = PtpWatcherDefault.PtpRequestHandlerDefault(self)
self.forced_publishing = False self.broker_endpoint = RpcEndpointInfo(broker_transport_endpoint)
self.registration_broker_endpoint = RpcEndpointInfo(registration_transport_endpoint)
def signal_ptp_event(self): self.ptpeventproducer = PtpEventProducer(
if self.event: self.node_name,
self.event.set() self.broker_endpoint.TransportEndpoint,
else: self.registration_broker_endpoint.TransportEndpoint)
LOG.warning("Unable to assert ptp event")
pass self.__ptprequest_handler = PtpWatcherDefault.PtpRequestHandlerDefault(self)
self.forced_publishing = False
def run(self):
# start location listener self.watcher = DmesgWatcher()
self.__start_listener() observer_list = [GnssMonitor(i) for i in self.daemon_context['GNSS_CONFIGS']]
while True: for observer in observer_list:
# annouce the location self.watcher.attach(observer)
forced = self.forced_publishing
self.forced_publishing = False self.watcher_thread = threading.Thread(target=self.watcher.run_watcher)
self.__publish_ptpstatus(forced)
if self.event.wait(self.event_timeout): def signal_ptp_event(self):
LOG.debug("daemon control event is asserted") if self.event:
self.event.clear() self.event.set()
else: else:
LOG.debug("daemon control event is timeout") LOG.warning("Unable to assert ptp event")
pass pass
continue
self.__stop_listener() def run(self):
# start location listener
'''Start listener to answer querying from clients''' self.__start_listener()
def __start_listener(self): # Start dmesg watcher
LOG.debug("start listener to answer location querying") self.watcher_thread.start()
while True:
self.ptpeventproducer.start_status_listener( # announce the location
self.__ptprequest_handler forced = self.forced_publishing
) self.forced_publishing = False
return self.__publish_ptpstatus(forced)
if self.event.wait(self.event_timeout):
def __stop_listener(self): LOG.debug("daemon control event is asserted")
LOG.debug("stop listener to answer location querying") self.event.clear()
else:
self.ptpeventproducer.stop_status_listener(self.location_info) LOG.debug("daemon control event is timeout")
return pass
continue
def __get_ptp_status(self, holdover_time, freq, sync_state, last_event_time): self.__stop_listener()
new_event = False
new_event_time = last_event_time '''Start listener to answer querying from clients'''
if self.ptp_device_simulated: def __start_listener(self):
now = time.time() LOG.debug("start listener to answer location querying")
timediff = now - last_event_time
if timediff > holdover_time: self.ptpeventproducer.start_status_listener(
new_event = True self.__ptprequest_handler
new_event_time = now )
if sync_state == PtpState.Freerun: return
sync_state = PtpState.Locked
elif sync_state == PtpState.Locked: def __stop_listener(self):
sync_state = PtpState.Holdover LOG.debug("stop listener to answer location querying")
elif sync_state == PtpState.Holdover:
sync_state = PtpState.Freerun self.ptpeventproducer.stop_status_listener(self.location_info)
else: return
sync_state = PtpState.Freerun
else: def __get_ptp_status(self, holdover_time, freq, sync_state, last_event_time):
new_event, sync_state, new_event_time = ptpsync.ptp_status( new_event = False
holdover_time, freq, sync_state, last_event_time) new_event_time = last_event_time
return new_event, sync_state, new_event_time if self.ptp_device_simulated:
now = time.time()
'''announce location''' timediff = now - last_event_time
def __publish_ptpstatus(self, forced=False): if timediff > holdover_time:
holdover_time = float(self.ptptracker_context['holdover_seconds']) new_event = True
freq = float(self.ptptracker_context['poll_freq_seconds']) new_event_time = now
sync_state = self.ptptracker_context.get('sync_state', 'Unknown') if sync_state == PtpState.Freerun:
last_event_time = self.ptptracker_context.get('last_event_time', time.time()) sync_state = PtpState.Locked
elif sync_state == PtpState.Locked:
new_event, sync_state, new_event_time = self.__get_ptp_status( sync_state = PtpState.Holdover
holdover_time, freq, sync_state, last_event_time) elif sync_state == PtpState.Holdover:
sync_state = PtpState.Freerun
if new_event or forced: else:
# update context sync_state = PtpState.Freerun
self.ptptracker_context_lock.acquire() else:
self.ptptracker_context['sync_state'] = sync_state new_event, sync_state, new_event_time = ptpsync.ptp_status(
self.ptptracker_context['last_event_time'] = new_event_time holdover_time, freq, sync_state, last_event_time)
self.ptptracker_context_lock.release() return new_event, sync_state, new_event_time
# publish new event '''announce location'''
LOG.debug("publish ptp status to clients") def __publish_ptpstatus(self, forced=False):
lastStatus = { holdover_time = float(self.ptptracker_context['holdover_seconds'])
'ResourceType': 'PTP', freq = float(self.ptptracker_context['poll_freq_seconds'])
'EventData': { sync_state = self.ptptracker_context.get('sync_state', 'Unknown')
'State': sync_state last_event_time = self.ptptracker_context.get('last_event_time', time.time())
},
'ResourceQualifier': { new_event, sync_state, new_event_time = self.__get_ptp_status(
'NodeName': self.node_name holdover_time, freq, sync_state, last_event_time)
},
'EventTimestamp': new_event_time if new_event or forced:
} # update context
self.ptpeventproducer.publish_status(lastStatus) self.ptptracker_context_lock.acquire()
return self.ptptracker_context['sync_state'] = sync_state
self.ptptracker_context['last_event_time'] = new_event_time
self.ptptracker_context_lock.release()
class DaemonControl(object):
# publish new event
def __init__(self, sqlalchemy_conf_json, daemon_context_json, process_worker = None): LOG.debug("publish ptp status to clients")
self.event = mp.Event() lastStatus = {
self.daemon_context = json.loads(daemon_context_json) 'ResourceType': 'PTP',
self.node_name = self.daemon_context['THIS_NODE_NAME'] 'EventData': {
if not process_worker: 'State': sync_state
process_worker = ProcessWorkerDefault },
'ResourceQualifier': {
self. sqlalchemy_conf_json = sqlalchemy_conf_json 'NodeName': self.node_name
self.daemon_context_json = daemon_context_json },
self.process_worker = process_worker 'EventTimestamp': new_event_time
return }
self.ptpeventproducer.publish_status(lastStatus)
def refresh(self): return
self.process_worker(self.event, self.sqlalchemy_conf_json, self.daemon_context_json)
self.event.set()
class DaemonControl(object):
def __init__(self, sqlalchemy_conf_json, daemon_context_json, process_worker = None):
self.event = mp.Event()
self.daemon_context = json.loads(daemon_context_json)
self.node_name = self.daemon_context['THIS_NODE_NAME']
if not process_worker:
process_worker = ProcessWorkerDefault
self.sqlalchemy_conf_json = sqlalchemy_conf_json
self.daemon_context_json = daemon_context_json
self.process_worker = process_worker
return
def refresh(self):
self.process_worker(self.event, self.sqlalchemy_conf_json, self.daemon_context_json)
self.event.set()

View File

@ -0,0 +1,77 @@
#
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import unittest
import mock
from trackingfunctionsdk.common.helpers.cgu_handler import CguHandler
import os
testpath = os.environ.get("TESTPATH", "")
class CguHandlerTests(unittest.TestCase):
testCguHandler = CguHandler(testpath + "test_input_files/ts2phc_valid.conf")
missingCguHandler = CguHandler("./no_such_file.conf")
invalidCguHandler = CguHandler(testpath + "test_input_files/ts2phc_invalid.conf")
def test_get_gnss_nmea_serialport(self):
# Test success path
self.testCguHandler.get_gnss_nmea_serialport_from_ts2phc_config()
self.assertEqual(self.testCguHandler.nmea_serialport, "/dev/ttyGNSS_1800_0")
# Test missing / incorrect config file path
with self.assertRaises(FileNotFoundError):
self.missingCguHandler.get_gnss_nmea_serialport_from_ts2phc_config()
# Test missing nmea_serialport in config
self.invalidCguHandler.get_gnss_nmea_serialport_from_ts2phc_config()
self.assertEqual(self.invalidCguHandler.nmea_serialport,
None)
def test_convert_nmea_serialport_to_pci_addr(self):
# Test success path
self.testCguHandler.get_gnss_nmea_serialport_from_ts2phc_config()
self.testCguHandler.convert_nmea_serialport_to_pci_addr(testpath +
"test_input_files/mock_dmesg")
self.assertEqual(self.testCguHandler.pci_addr, "0000:18:00.0")
# Test pci address not found
self.testCguHandler.nmea_serialport = "/dev/ttyGNSS_not_present"
self.testCguHandler.convert_nmea_serialport_to_pci_addr(testpath +
"test_input_files/mock_dmesg")
self.assertEqual(self.testCguHandler.pci_addr, None)
@mock.patch('trackingfunctionsdk.common.helpers.cgu_handler.os.path')
def test_get_cgu_path_from_pci_addr(self, mock_path):
# Setup mock
mock_path.exists.return_value = True
self.testCguHandler.get_gnss_nmea_serialport_from_ts2phc_config()
self.testCguHandler.convert_nmea_serialport_to_pci_addr(testpath +
"test_input_files/mock_dmesg")
self.testCguHandler.get_cgu_path_from_pci_addr()
self.assertEqual(self.testCguHandler.cgu_path, "/ice/0000:18:00.0/cgu")
mock_path.exists.return_value = False
with self.assertRaises(FileNotFoundError):
self.testCguHandler.get_cgu_path_from_pci_addr()
def test_cgu_output_to_dict(self):
reference_dict = {
'input':
{'CVL-SDP22': {'state': 'invalid', 'priority': {'EEC': '8', 'PPS': '8'}},
'CVL-SDP20': {'state': 'invalid', 'priority': {'EEC': '15', 'PPS': '3'}},
'C827_0-RCLKA': {'state': 'invalid', 'priority': {'EEC': '4', 'PPS': '4'}},
'C827_0-RCLKB': {'state': 'invalid', 'priority': {'EEC': '5', 'PPS': '5'}},
'SMA1': {'state': 'invalid', 'priority': {'EEC': '1', 'PPS': '1'}},
'SMA2/U.FL2': {'state': 'invalid', 'priority': {'EEC': '2', 'PPS': '2'}},
'GNSS-1PPS': {'state': 'valid', 'priority': {'EEC': '0', 'PPS': '0'}}},
'EEC DPLL': {'Current reference': 'GNSS-1PPS', 'Status': 'locked_ho_ack'},
'PPS DPLL': {'Current reference': 'GNSS-1PPS', 'Status': 'locked_ho_ack',
'Phase offset': '295'}}
self.testCguHandler.cgu_path = testpath + "test_input_files/mock_cgu_output"
self.testCguHandler.read_cgu()
self.testCguHandler.cgu_output_to_dict()
self.assertDictEqual(self.testCguHandler.cgu_output_parsed, reference_dict)

View File

@ -0,0 +1,56 @@
#
# Copyright (c) 2022 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import unittest
import os
from unittest.mock import MagicMock
from trackingfunctionsdk.common.helpers.dmesg_watcher import DmesgWatcher
from trackingfunctionsdk.common.helpers.gnss_monitor import GnssMonitor
testpath = os.environ.get("TESTPATH", "")
class DmesgWatcherTests(unittest.TestCase):
testDmesgWatcher = DmesgWatcher()
observer_a = GnssMonitor(testpath + "./test_input_files/ts2phc_valid.conf",
"tty_GNSS_1800_0", "0000:18:00.0",
testpath + "./test_input_files/mock_cgu_output")
observer_b = GnssMonitor(testpath + "./test_input_files/ts2phc_valid.conf",
"tty_GNSS_1a00_0", "0000:1a:00.0",
testpath + "./test_input_files/mock_cgu_output")
def test_parse_dmesg_event(self):
self.testDmesgWatcher.attach(self.observer_a)
self.testDmesgWatcher.notify = MagicMock()
with open(testpath + "./test_input_files/mock_kern.log", 'r') as dmesg:
for line in dmesg:
self.testDmesgWatcher.parse_dmesg_event(line)
assert self.testDmesgWatcher.notify.called
# Test that notify is not called when there is no match
self.testDmesgWatcher.notify.reset_mock()
self.testDmesgWatcher.attach(self.observer_b)
with open(testpath + "./test_input_files/mock_kern.log", 'r') as dmesg:
for line in dmesg:
self.testDmesgWatcher.parse_dmesg_event(line)
assert self.testDmesgWatcher.notify.assert_not_called
def test_attach_detach(self):
self.testDmesgWatcher.attach(self.observer_a)
self.testDmesgWatcher.attach(self.observer_b)
self.assertEqual(len(self.testDmesgWatcher._observers), 2)
self.testDmesgWatcher.detach(self.observer_a)
self.testDmesgWatcher.detach(self.observer_b)
self.assertEqual(len(self.testDmesgWatcher._observers), 0)
def test_notify(self):
self.observer_a.update = MagicMock
self.testDmesgWatcher.notify(observer=self.observer_a,
matched_line="2022-06-03T19:50:05.959 controller-0 kernel: warning [ "
"4.635511] ice 0000:18:00.0: <DPLL1> state changed to: "
"locked_ho_ack, pin GNSS-1PPS")
assert self.observer_a.update.called

View File

@ -0,0 +1,23 @@
Found ZL80032 CGU
DPLL Config ver: 1.3.0.1
CGU Input status:
| | priority |
input (idx) | state | EEC (0) | PPS (1) |
---------------------------------------------------
CVL-SDP22 (0) | invalid | 8 | 8 |
CVL-SDP20 (1) | invalid | 15 | 3 |
C827_0-RCLKA (2) | invalid | 4 | 4 |
C827_0-RCLKB (3) | invalid | 5 | 5 |
SMA1 (4) | invalid | 1 | 1 |
SMA2/U.FL2 (5) | invalid | 2 | 2 |
GNSS-1PPS (6) | valid | 0 | 0 |
EEC DPLL:
Current reference: GNSS-1PPS
Status: locked_ho_ack
PPS DPLL:
Current reference: GNSS-1PPS
Status: locked_ho_ack
Phase offset: 295

View File

@ -0,0 +1,6 @@
[ 4.834255] ice 0000:18:00.0: ttyGNSS_1800_0 registered
[ 4.834258] ice 0000:18:00.0: GNSS TTY init successful
[ 4.846647] ice 0000:18:00.0: <DPLL0> state changed to: locked_ho_ack, pin GNSS-1PPS
[ 4.859247] ice 0000:18:00.0: <DPLL1> state changed to: locked_ho_ack, pin GNSS-1PPS
[ 8.535739] ice 0000:1a:00.0: ttyGNSS_1a00_0 registered
[ 8.535741] ice 0000:1a:00.0: GNSS TTY init successful

View File

@ -0,0 +1,12 @@
2022-06-03T19:36:08.021 controller-0 kernel: info [ 11.122139] ice 0000:18:00.0: ttyGNSS_1800_0 registered
2022-06-03T19:36:08.021 controller-0 kernel: info [ 11.122142] ice 0000:18:00.0: GNSS TTY init successful
2022-06-03T19:36:08.021 controller-0 kernel: warning [ 11.134103] ice 0000:18:00.0: <DPLL0> state changed to: locked_ho_ack, pin GNSS-1PPS
2022-06-03T19:36:08.021 controller-0 kernel: warning [ 11.148040] ice 0000:18:00.0: <DPLL1> state changed to: locked_ho_ack, pin GNSS-1PPS
2022-06-03T19:36:08.022 controller-0 kernel: info [ 14.405736] ice 0000:1a:00.0: ttyGNSS_1a00_0 registered
2022-06-03T19:36:08.022 controller-0 kernel: info [ 14.405737] ice 0000:1a:00.0: GNSS TTY init successful
2022-06-03T19:50:05.958 controller-0 kernel: info [ 4.609339] ice 0000:18:00.0: ttyGNSS_1800_0 registered
2022-06-03T19:50:05.958 controller-0 kernel: info [ 4.609362] ice 0000:18:00.0: GNSS TTY init successful
2022-06-03T19:50:05.958 controller-0 kernel: warning [ 4.621518] ice 0000:18:00.0: <DPLL0> state changed to: locked_ho_ack, pin GNSS-1PPS
2022-06-03T19:50:05.959 controller-0 kernel: warning [ 4.635511] ice 0000:18:00.0: <DPLL1> state changed to: locked_ho_ack, pin GNSS-1PPS
2022-06-03T19:50:05.959 controller-0 kernel: info [ 7.995939] ice 0000:1a:00.0: ttyGNSS_1a00_0 registered
2022-06-03T19:50:05.959 controller-0 kernel: info [ 7.995941] ice 0000:1a:00.0: GNSS TTY init successful

View File

@ -0,0 +1,24 @@
[global]
##
## Default Data Set
##
leapfile /usr/share/zoneinfo/leap-seconds.list
logging_level 7
ts2phc.pulsewidth 100000000
[ens1f0]
##
## Associated interface: oam0
##
ts2phc.extts_polarity rising
[ens2f0]
##
## Associated interface: data0
##
ts2phc.extts_polarity rising

View File

@ -0,0 +1,25 @@
[global]
##
## Default Data Set
##
leapfile /usr/share/zoneinfo/leap-seconds.list
logging_level 7
ts2phc.nmea_serialport /dev/ttyGNSS_1800_0
ts2phc.pulsewidth 100000000
[ens1f0]
##
## Associated interface: oam0
##
ts2phc.extts_polarity rising
[ens2f0]
##
## Associated interface: data0
##
ts2phc.extts_polarity rising

View File

@ -1 +1,10 @@
# Nothing sqlalchemy~=1.4.36
wsme
oslo.messaging
pecan~=1.4.1
WebTest
WebOb~=1.8.7
requests~=2.27.1
mock
PyYAML
pygtail~=0.12.0

View File

@ -1,3 +1,4 @@
# hacking pulls in flake8 # hacking pulls in flake8
hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
bashate >= 0.2 bashate >= 0.2
unittest2

12
tox.ini
View File

@ -1,5 +1,5 @@
[tox] [tox]
envlist = linters envlist = linters,py36
minversion = 2.3 minversion = 2.3
skipsdist = True skipsdist = True
sitepackages=False sitepackages=False
@ -18,6 +18,16 @@ deps =
whitelist_externals = whitelist_externals =
bash bash
[testenv:py36]
basepython = python3.6
setenv =
TESTPATH=./notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/
commands =
unit2 discover -s ./notificationservice-base/centos/docker/ptptrackingfunction
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
[testenv:bashate] [testenv:bashate]
# Treat all E* codes as Errors rather than warnings using: -e 'E*' # Treat all E* codes as Errors rather than warnings using: -e 'E*'
commands = commands =