diff --git a/.zuul.yaml b/.zuul.yaml index 42d924b..09b6aa4 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -8,10 +8,12 @@ jobs: - openstack-tox-linters - k8sapp-ptp-notification-tox-pylint + - ptp-notification-tox-py36 gate: jobs: - openstack-tox-linters - k8sapp-ptp-notification-tox-pylint + - ptp-notification-tox-py36 post: jobs: - stx-ptp-notification-armada-app-upload-git-mirror @@ -29,6 +31,15 @@ secret: stx-ptp-notification-armada-app-github-secret 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: name: k8sapp-ptp-notification-tox-pylint parent: tox diff --git a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/common/__init__.py b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/common/helpers/__init__.py b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/common/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/common/helpers/cgu_handler.py b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/common/helpers/cgu_handler.py new file mode 100644 index 0000000..2ff9ee3 --- /dev/null +++ b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/common/helpers/cgu_handler.py @@ -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 diff --git a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/common/helpers/constants.py b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/common/helpers/constants.py index dc7be62..dd3bac4 100644 --- a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/common/helpers/constants.py +++ b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/common/helpers/constants.py @@ -26,3 +26,13 @@ GM_IS_PRESENT = "true" CLOCK_CLASS_VALUE1 = "6" CLOCK_CLASS_VALUE2 = "7" 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" diff --git a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/common/helpers/dmesg_watcher.py b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/common/helpers/dmesg_watcher.py new file mode 100644 index 0000000..b40c1f6 --- /dev/null +++ b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/common/helpers/dmesg_watcher.py @@ -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.") + diff --git a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/common/helpers/gnss_monitor.py b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/common/helpers/gnss_monitor.py new file mode 100644 index 0000000..1fb937c --- /dev/null +++ b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/common/helpers/gnss_monitor.py @@ -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 diff --git a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/model/dto/gnssstate.py b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/model/dto/gnssstate.py new file mode 100644 index 0000000..654b812 --- /dev/null +++ b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/model/dto/gnssstate.py @@ -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" + diff --git a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/model/dto/resourcetype.py b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/model/dto/resourcetype.py index 788f477..70b5763 100644 --- a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/model/dto/resourcetype.py +++ b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/model/dto/resourcetype.py @@ -7,3 +7,4 @@ EnumResourceType = wtypes.Enum(str, 'PTP', 'FPGA') class ResourceType(object): TypePTP = "PTP" TypeFPGA = "FPGA" + TypeGNSS = "GNSS" diff --git a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/services/daemon.py b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/services/daemon.py index f434856..2c73421 100644 --- a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/services/daemon.py +++ b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/services/daemon.py @@ -1,218 +1,235 @@ -# -# Copyright (c) 2021 Wind River Systems, Inc. -# -# SPDX-License-Identifier: Apache-2.0 -# - -import os -import json -import time -import oslo_messaging -from oslo_config import cfg -import logging - -import multiprocessing as mp -import threading - -from trackingfunctionsdk.common.helpers import rpc_helper -from trackingfunctionsdk.model.dto.rpc_endpoint import RpcEndpointInfo -from trackingfunctionsdk.model.dto.resourcetype import ResourceType -from trackingfunctionsdk.model.dto.ptpstate import PtpState - -from trackingfunctionsdk.client.ptpeventproducer import PtpEventProducer - -from trackingfunctionsdk.common.helpers import ptpsync as ptpsync - -LOG = logging.getLogger(__name__) - -from trackingfunctionsdk.common.helpers import log_helper -log_helper.config_logger(LOG) - -THIS_NODE_NAME = os.environ.get("THIS_NODE_NAME",'controller-0') - - -'''Entry point of Default Process Worker''' -def ProcessWorkerDefault(event, sqlalchemy_conf_json, broker_transport_endpoint): - worker = PtpWatcherDefault(event, sqlalchemy_conf_json, broker_transport_endpoint) - worker.run() - return - - -class PtpWatcherDefault: - DEFAULT_PTPTRACKER_CONTEXT = { - 'holdover_seconds': 30, - 'poll_freq_seconds': 2 - } - - class PtpRequestHandlerDefault(object): - def __init__(self, watcher): - self.watcher = watcher - self.init_time = time.time() - - def query_status(self, **rpc_kwargs): - self.watcher.ptptracker_context_lock.acquire() - sync_state = self.watcher.ptptracker_context.get('sync_state', PtpState.Freerun) - last_event_time = self.watcher.ptptracker_context.get('last_event_time', time.time()) - self.watcher.ptptracker_context_lock.release() - - lastStatus = { - 'ResourceType': ResourceType.TypePTP, - 'EventData': { - 'State': sync_state - }, - 'ResourceQualifier': { - 'NodeName': self.watcher.node_name - }, - 'EventTimestamp': last_event_time - } - return lastStatus - - def trigger_delivery(self, **rpc_kwargs): - self.watcher.forced_publishing = True - self.watcher.signal_ptp_event() - pass - - def __init__(self, event, sqlalchemy_conf_json, daemon_context_json): - self.sqlalchemy_conf = json.loads(sqlalchemy_conf_json) - self.event = event - self.init_time = time.time() - - self.daemon_context = json.loads(daemon_context_json) - self.ptptracker_context = self.daemon_context.get( - 'ptptracker_context', PtpWatcherDefault.DEFAULT_PTPTRACKER_CONTEXT) - self.ptptracker_context['sync_state'] = PtpState.Freerun - self.ptptracker_context['last_event_time'] = self.init_time - self.ptptracker_context_lock = threading.Lock() - - self.ptp_device_simulated = "true" == self.ptptracker_context.get('device_simulated', "False").lower() - - self.event_timeout = float(self.ptptracker_context['poll_freq_seconds']) - - self.node_name = self.daemon_context['THIS_NODE_NAME'] - self.namespace = self.daemon_context.get('THIS_NAMESPACE', 'notification') - - broker_transport_endpoint = self.daemon_context['NOTIFICATION_TRANSPORT_ENDPOINT'] - - registration_transport_endpoint = self.daemon_context['REGISTRATION_TRANSPORT_ENDPOINT'] - - self.broker_endpoint = RpcEndpointInfo(broker_transport_endpoint) - self.registration_broker_endpoint = RpcEndpointInfo(registration_transport_endpoint) - self.ptpeventproducer = PtpEventProducer( - self.node_name, - self.broker_endpoint.TransportEndpoint, - self.registration_broker_endpoint.TransportEndpoint) - - self.__ptprequest_handler = PtpWatcherDefault.PtpRequestHandlerDefault(self) - self.forced_publishing = False - - def signal_ptp_event(self): - if self.event: - self.event.set() - else: - LOG.warning("Unable to assert ptp event") - pass - - def run(self): - # start location listener - self.__start_listener() - while True: - # annouce the location - forced = self.forced_publishing - self.forced_publishing = False - self.__publish_ptpstatus(forced) - if self.event.wait(self.event_timeout): - LOG.debug("daemon control event is asserted") - self.event.clear() - else: - LOG.debug("daemon control event is timeout") - pass - continue - self.__stop_listener() - - '''Start listener to answer querying from clients''' - def __start_listener(self): - LOG.debug("start listener to answer location querying") - - self.ptpeventproducer.start_status_listener( - self.__ptprequest_handler - ) - return - - def __stop_listener(self): - LOG.debug("stop listener to answer location querying") - - self.ptpeventproducer.stop_status_listener(self.location_info) - return - - def __get_ptp_status(self, holdover_time, freq, sync_state, last_event_time): - new_event = False - new_event_time = last_event_time - if self.ptp_device_simulated: - now = time.time() - timediff = now - last_event_time - if timediff > holdover_time: - new_event = True - new_event_time = now - if sync_state == PtpState.Freerun: - sync_state = PtpState.Locked - elif sync_state == PtpState.Locked: - sync_state = PtpState.Holdover - elif sync_state == PtpState.Holdover: - sync_state = PtpState.Freerun - else: - sync_state = PtpState.Freerun - else: - new_event, sync_state, new_event_time = ptpsync.ptp_status( - holdover_time, freq, sync_state, last_event_time) - return new_event, sync_state, new_event_time - - '''announce location''' - def __publish_ptpstatus(self, forced=False): - holdover_time = float(self.ptptracker_context['holdover_seconds']) - freq = float(self.ptptracker_context['poll_freq_seconds']) - sync_state = self.ptptracker_context.get('sync_state', 'Unknown') - last_event_time = self.ptptracker_context.get('last_event_time', time.time()) - - new_event, sync_state, new_event_time = self.__get_ptp_status( - holdover_time, freq, sync_state, last_event_time) - - if new_event or forced: - # update context - self.ptptracker_context_lock.acquire() - self.ptptracker_context['sync_state'] = sync_state - self.ptptracker_context['last_event_time'] = new_event_time - self.ptptracker_context_lock.release() - - # publish new event - LOG.debug("publish ptp status to clients") - lastStatus = { - 'ResourceType': 'PTP', - 'EventData': { - 'State': sync_state - }, - 'ResourceQualifier': { - 'NodeName': self.node_name - }, - 'EventTimestamp': new_event_time - } - self.ptpeventproducer.publish_status(lastStatus) - return - - -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() +# +# Copyright (c) 2022 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +import os +import json +import time +import oslo_messaging +from oslo_config import cfg +import logging + +import multiprocessing as mp +import threading + +from trackingfunctionsdk.client.ptpeventproducer import PtpEventProducer +from trackingfunctionsdk.common.helpers import ptpsync +from trackingfunctionsdk.common.helpers import log_helper +from trackingfunctionsdk.common.helpers.dmesg_watcher import DmesgWatcher +from trackingfunctionsdk.common.helpers.gnss_monitor import GnssMonitor +from trackingfunctionsdk.model.dto.ptpstate import PtpState +from trackingfunctionsdk.model.dto.resourcetype import ResourceType +from trackingfunctionsdk.model.dto.rpc_endpoint import RpcEndpointInfo +from trackingfunctionsdk.model.dto.resourcetype import ResourceType +from trackingfunctionsdk.model.dto.ptpstate import PtpState + +from trackingfunctionsdk.client.ptpeventproducer import PtpEventProducer + +from trackingfunctionsdk.common.helpers import ptpsync as ptpsync + +LOG = logging.getLogger(__name__) + +from trackingfunctionsdk.common.helpers import log_helper +log_helper.config_logger(LOG) + +THIS_NODE_NAME = os.environ.get("THIS_NODE_NAME",'controller-0') + + +'''Entry point of Default Process Worker''' +def ProcessWorkerDefault(event, sqlalchemy_conf_json, broker_transport_endpoint): + worker = PtpWatcherDefault(event, sqlalchemy_conf_json, broker_transport_endpoint) + worker.run() + return + + +class PtpWatcherDefault: + DEFAULT_PTPTRACKER_CONTEXT = { + 'holdover_seconds': 30, + 'poll_freq_seconds': 2 + } + + class PtpRequestHandlerDefault(object): + def __init__(self, watcher): + self.watcher = watcher + self.init_time = time.time() + + def query_status(self, **rpc_kwargs): + self.watcher.ptptracker_context_lock.acquire() + sync_state = self.watcher.ptptracker_context.get('sync_state', PtpState.Freerun) + last_event_time = self.watcher.ptptracker_context.get('last_event_time', time.time()) + self.watcher.ptptracker_context_lock.release() + + lastStatus = { + 'ResourceType': ResourceType.TypePTP, + 'EventData': { + 'State': sync_state + }, + 'ResourceQualifier': { + 'NodeName': self.watcher.node_name + }, + 'EventTimestamp': last_event_time + } + return lastStatus + + def trigger_delivery(self, **rpc_kwargs): + self.watcher.forced_publishing = True + self.watcher.signal_ptp_event() + pass + + def __init__(self, event, sqlalchemy_conf_json, daemon_context_json): + self.sqlalchemy_conf = json.loads(sqlalchemy_conf_json) + self.event = event + self.init_time = time.time() + + self.daemon_context = json.loads(daemon_context_json) + self.ptptracker_context = self.daemon_context.get( + 'ptptracker_context', PtpWatcherDefault.DEFAULT_PTPTRACKER_CONTEXT) + self.ptptracker_context['sync_state'] = PtpState.Freerun + self.ptptracker_context['last_event_time'] = self.init_time + self.ptptracker_context_lock = threading.Lock() + + self.ptp_device_simulated = "true" == self.ptptracker_context.get('device_simulated', + "False").lower() + + self.event_timeout = float(self.ptptracker_context['poll_freq_seconds']) + + self.node_name = self.daemon_context['THIS_NODE_NAME'] + + self.namespace = self.daemon_context.get('THIS_NAMESPACE', 'notification') + + broker_transport_endpoint = self.daemon_context['NOTIFICATION_TRANSPORT_ENDPOINT'] + + registration_transport_endpoint = self.daemon_context['REGISTRATION_TRANSPORT_ENDPOINT'] + + self.broker_endpoint = RpcEndpointInfo(broker_transport_endpoint) + self.registration_broker_endpoint = RpcEndpointInfo(registration_transport_endpoint) + self.ptpeventproducer = PtpEventProducer( + self.node_name, + self.broker_endpoint.TransportEndpoint, + self.registration_broker_endpoint.TransportEndpoint) + + self.__ptprequest_handler = PtpWatcherDefault.PtpRequestHandlerDefault(self) + self.forced_publishing = False + + self.watcher = DmesgWatcher() + observer_list = [GnssMonitor(i) for i in self.daemon_context['GNSS_CONFIGS']] + for observer in observer_list: + self.watcher.attach(observer) + + self.watcher_thread = threading.Thread(target=self.watcher.run_watcher) + + def signal_ptp_event(self): + if self.event: + self.event.set() + else: + LOG.warning("Unable to assert ptp event") + pass + + def run(self): + # start location listener + self.__start_listener() + # Start dmesg watcher + self.watcher_thread.start() + while True: + # announce the location + forced = self.forced_publishing + self.forced_publishing = False + self.__publish_ptpstatus(forced) + if self.event.wait(self.event_timeout): + LOG.debug("daemon control event is asserted") + self.event.clear() + else: + LOG.debug("daemon control event is timeout") + pass + continue + self.__stop_listener() + + '''Start listener to answer querying from clients''' + def __start_listener(self): + LOG.debug("start listener to answer location querying") + + self.ptpeventproducer.start_status_listener( + self.__ptprequest_handler + ) + return + + def __stop_listener(self): + LOG.debug("stop listener to answer location querying") + + self.ptpeventproducer.stop_status_listener(self.location_info) + return + + def __get_ptp_status(self, holdover_time, freq, sync_state, last_event_time): + new_event = False + new_event_time = last_event_time + if self.ptp_device_simulated: + now = time.time() + timediff = now - last_event_time + if timediff > holdover_time: + new_event = True + new_event_time = now + if sync_state == PtpState.Freerun: + sync_state = PtpState.Locked + elif sync_state == PtpState.Locked: + sync_state = PtpState.Holdover + elif sync_state == PtpState.Holdover: + sync_state = PtpState.Freerun + else: + sync_state = PtpState.Freerun + else: + new_event, sync_state, new_event_time = ptpsync.ptp_status( + holdover_time, freq, sync_state, last_event_time) + return new_event, sync_state, new_event_time + + '''announce location''' + def __publish_ptpstatus(self, forced=False): + holdover_time = float(self.ptptracker_context['holdover_seconds']) + freq = float(self.ptptracker_context['poll_freq_seconds']) + sync_state = self.ptptracker_context.get('sync_state', 'Unknown') + last_event_time = self.ptptracker_context.get('last_event_time', time.time()) + + new_event, sync_state, new_event_time = self.__get_ptp_status( + holdover_time, freq, sync_state, last_event_time) + + if new_event or forced: + # update context + self.ptptracker_context_lock.acquire() + self.ptptracker_context['sync_state'] = sync_state + self.ptptracker_context['last_event_time'] = new_event_time + self.ptptracker_context_lock.release() + + # publish new event + LOG.debug("publish ptp status to clients") + lastStatus = { + 'ResourceType': 'PTP', + 'EventData': { + 'State': sync_state + }, + 'ResourceQualifier': { + 'NodeName': self.node_name + }, + 'EventTimestamp': new_event_time + } + self.ptpeventproducer.publish_status(lastStatus) + return + + +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() diff --git a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/__init__.py b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_cgu_handler.py b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_cgu_handler.py new file mode 100644 index 0000000..e41c505 --- /dev/null +++ b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_cgu_handler.py @@ -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) diff --git a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_dmesg_watcher.py b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_dmesg_watcher.py new file mode 100644 index 0000000..a039767 --- /dev/null +++ b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_dmesg_watcher.py @@ -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: state changed to: " + "locked_ho_ack, pin GNSS-1PPS") + assert self.observer_a.update.called diff --git a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_input_files/mock_cgu_output b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_input_files/mock_cgu_output new file mode 100644 index 0000000..185eca3 --- /dev/null +++ b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_input_files/mock_cgu_output @@ -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 \ No newline at end of file diff --git a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_input_files/mock_dmesg b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_input_files/mock_dmesg new file mode 100644 index 0000000..ce7d51b --- /dev/null +++ b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_input_files/mock_dmesg @@ -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: state changed to: locked_ho_ack, pin GNSS-1PPS +[ 4.859247] ice 0000:18:00.0: 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 \ No newline at end of file diff --git a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_input_files/mock_kern.log b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_input_files/mock_kern.log new file mode 100644 index 0000000..02f4e9d --- /dev/null +++ b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_input_files/mock_kern.log @@ -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: 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: 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: 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: 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 \ No newline at end of file diff --git a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_input_files/ts2phc_invalid.conf b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_input_files/ts2phc_invalid.conf new file mode 100644 index 0000000..b4c1cf1 --- /dev/null +++ b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_input_files/ts2phc_invalid.conf @@ -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 + diff --git a/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_input_files/ts2phc_valid.conf b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_input_files/ts2phc_valid.conf new file mode 100644 index 0000000..eda581f --- /dev/null +++ b/notificationservice-base/centos/docker/ptptrackingfunction/trackingfunctionsdk/tests/test_input_files/ts2phc_valid.conf @@ -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 + diff --git a/requirements.txt b/requirements.txt index c01ade2..83252dc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/test-requirements.txt b/test-requirements.txt index 8ae3e22..ba35c6f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,4 @@ # hacking pulls in flake8 hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 bashate >= 0.2 +unittest2 diff --git a/tox.ini b/tox.ini index d75152d..69d2b7f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = linters +envlist = linters,py36 minversion = 2.3 skipsdist = True sitepackages=False @@ -18,6 +18,16 @@ deps = whitelist_externals = 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] # Treat all E* codes as Errors rather than warnings using: -e 'E*' commands =