
Rework the pull status and subscription notification to remove the top level instance tags that were returned with the notification. These tags were originally introduced to identify which PTP instance a given notification was associated with. Updates to the ORAN standard have clarified that the instance identification should instead be included in the Resource Address field. This commit includes the following changes: - Update notification format to place instance identifier in Resource Address instead of a top level tag (default behaviour) - Provide a helm override to enable legacy notification format with top level instance tags for compatibility. This can be enabled by setting the override "notification_format=legacy" - Ensure GM clockClass is correctly reported for both BC and GM configs - Changes result in new images for notificationclient-base and notificationservice-base-v2 - Pull status notifications are now delivered as a list of dicts when there are multiple instances in a request. The "overall sync status" message is delivered as a dict because it only ever has a single notification - Push notifications are delivered as individual dicts for each notification, this behaviour remains the same Test plan: Pass: Verify ptp-notification builds and deploys Pass: Verify container image builds Pass: Verify ptp-notification operations (pull status, subscribe, list, delete) Pass: Verify standard and legacy format output for each notification type Partial-bug: 2089035 Signed-off-by: Cole Walker <cole.walker@windriver.com> Change-Id: Ied6674a02b41ed31079a291fc9bace74d95d467a
197 lines
9.8 KiB
Python
197 lines
9.8 KiB
Python
#
|
|
# Copyright (c) 2021-2024 Wind River Systems, Inc.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
|
|
import json
|
|
import logging
|
|
|
|
import multiprocessing as mp
|
|
import threading
|
|
import time
|
|
from datetime import datetime, timezone
|
|
|
|
from notificationclientsdk.model.dto.subscription import SubscriptionInfoV1
|
|
from notificationclientsdk.model.dto.subscription import SubscriptionInfoV2
|
|
from notificationclientsdk.model.dto.resourcetype import ResourceType
|
|
|
|
from notificationclientsdk.repository.subscription_repo import SubscriptionRepo
|
|
|
|
from notificationclientsdk.common.helpers import subscription_helper
|
|
from notificationclientsdk.common.helpers.nodeinfo_helper import NodeInfoHelper
|
|
|
|
from notificationclientsdk.client.notificationservice import NotificationHandlerBase
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
from notificationclientsdk.common.helpers import log_helper
|
|
|
|
log_helper.config_logger(LOG)
|
|
|
|
|
|
class NotificationHandler(NotificationHandlerBase):
|
|
|
|
def __init__(self):
|
|
self.__supported_resource_types = (ResourceType.TypePTP,)
|
|
self.__init_notification_channel()
|
|
pass
|
|
|
|
def __init_notification_channel(self):
|
|
self.notification_lock = threading.Lock()
|
|
self.notification_stat = {}
|
|
|
|
# def handle_notification_delivery(self, notification_info):
|
|
def handle(self, notification_info):
|
|
LOG.debug("start notification delivery")
|
|
subscription_repo = None
|
|
resource_address = None
|
|
try:
|
|
self.notification_lock.acquire()
|
|
LOG.info("Notification handler notification_info %s", notification_info)
|
|
subscription_repo = SubscriptionRepo(autocommit=True)
|
|
if isinstance(notification_info, dict):
|
|
resource_type = notification_info.get('ResourceType', None)
|
|
# Get nodename from resource address
|
|
if resource_type:
|
|
node_name = notification_info.get('ResourceQualifier', {}).get('NodeName', None)
|
|
if not resource_type:
|
|
raise Exception("abnormal notification@{0}".format(node_name))
|
|
if not resource_type in self.__supported_resource_types:
|
|
raise Exception(
|
|
"notification with unsupported resource type:{0}".format(resource_type))
|
|
this_delivery_time = notification_info['EventTimestamp']
|
|
# Get subscriptions from DB to deliver notification to
|
|
entries = subscription_repo.get(Status=1, ResourceType=resource_type)
|
|
else:
|
|
parent_key = list(notification_info.keys())[0]
|
|
source = notification_info[parent_key].get('source', None)
|
|
values = notification_info[parent_key].get('data', {}).get('values', [])
|
|
resource_address = values[0].get('ResourceAddress', None)
|
|
this_delivery_time = notification_info[parent_key].get('time')
|
|
if not resource_address:
|
|
raise Exception("No resource address in notification source".format(source))
|
|
_, node_name, _, _, _ = subscription_helper.parse_resource_address(resource_address)
|
|
# Get subscriptions from DB to deliver notification to.
|
|
# Unable to filter on resource_address here because resource_address may contain
|
|
# either an unknown node name (ie. controller-0) or a '/./' resulting in entries
|
|
# being missed. Instead, filter these v2 subscriptions in the for loop below once
|
|
# the resource path has been obtained.
|
|
entries = subscription_repo.get(Status=1)
|
|
elif isinstance(notification_info, list):
|
|
LOG.debug("Handle list")
|
|
for item in notification_info:
|
|
source = item.get('source', None)
|
|
values = item.get('data', {}).get('values', [])
|
|
resource_address = values[0].get('ResourceAddress', None)
|
|
this_delivery_time = item.get('time')
|
|
if not resource_address:
|
|
raise Exception("No resource address in notification source".format(source))
|
|
_, node_name, _, _, _ = subscription_helper.parse_resource_address(resource_address)
|
|
entries = subscription_repo.get(Status=1)
|
|
|
|
for entry in entries:
|
|
subscriptionid = entry.SubscriptionId
|
|
if entry.ResourceAddress:
|
|
_, entry_node_name, entry_resource_path, _, _ = \
|
|
subscription_helper.parse_resource_address(entry.ResourceAddress)
|
|
_, _, event_resource_path, _, _ = \
|
|
subscription_helper.parse_resource_address(resource_address)
|
|
if not event_resource_path.startswith(entry_resource_path):
|
|
continue
|
|
subscription_dto2 = SubscriptionInfoV2(entry)
|
|
else:
|
|
ResourceQualifierJson = entry.ResourceQualifierJson or '{}'
|
|
ResourceQualifier = json.loads(ResourceQualifierJson)
|
|
# qualify by NodeName
|
|
entry_node_name = ResourceQualifier.get('NodeName', None)
|
|
subscription_dto2 = SubscriptionInfoV1(entry)
|
|
node_name_matched = NodeInfoHelper.match_node_name(entry_node_name, node_name)
|
|
if not node_name_matched:
|
|
continue
|
|
|
|
try:
|
|
last_delivery_time = self.__get_latest_delivery_timestamp(node_name,
|
|
subscriptionid)
|
|
if last_delivery_time and last_delivery_time >= this_delivery_time:
|
|
# skip this entry since already delivered
|
|
LOG.debug("Ignore the outdated notification for: {0}".format(
|
|
entry.SubscriptionId))
|
|
continue
|
|
|
|
notification_to_send = self.__format_timestamps(notification_info)
|
|
LOG.info("Sending notification to subscribers: %s", notification_to_send)
|
|
subscription_helper.notify(subscription_dto2, notification_to_send)
|
|
LOG.info("notification is delivered successfully to {0}".format(
|
|
entry.SubscriptionId))
|
|
self.update_delivery_timestamp(node_name, subscriptionid, this_delivery_time)
|
|
|
|
except Exception as ex:
|
|
LOG.warning("notification is not delivered to {0}:{1}".format(
|
|
entry.SubscriptionId, str(ex)))
|
|
# proceed to next entry
|
|
continue
|
|
finally:
|
|
pass
|
|
LOG.debug("Finished notification delivery")
|
|
return True
|
|
except Exception as ex:
|
|
LOG.warning("Failed to delivery notification:{0}".format(str(ex)))
|
|
return False
|
|
finally:
|
|
self.notification_lock.release()
|
|
if not subscription_repo:
|
|
del subscription_repo
|
|
|
|
def __format_timestamps(self, ptpstatus):
|
|
if isinstance(ptpstatus, list):
|
|
LOG.debug("Format timestamps for standard subscription response")
|
|
for item in ptpstatus:
|
|
item['time'] = datetime.fromtimestamp(
|
|
item['time']).strftime(
|
|
'%Y-%m-%dT%H:%M:%S%fZ')
|
|
elif isinstance(ptpstatus, dict):
|
|
LOG.debug("Format timestamps for response with instance tags")
|
|
try:
|
|
for item in ptpstatus:
|
|
# Change time from float to ascii format
|
|
ptpstatus[item]['time'] = datetime.fromtimestamp(
|
|
ptpstatus[item]['time']).strftime(
|
|
'%Y-%m-%dT%H:%M:%S%fZ')
|
|
except (TypeError, AttributeError):
|
|
LOG.debug("Format timestamp for single notification")
|
|
ptpstatus['time'] = datetime.fromtimestamp(
|
|
ptpstatus['time']).strftime(
|
|
'%Y-%m-%dT%H:%M:%S%fZ')
|
|
return ptpstatus
|
|
|
|
def __get_latest_delivery_timestamp(self, node_name, subscriptionid):
|
|
last_delivery_stat = self.notification_stat.get(node_name, {}).get(subscriptionid, {})
|
|
last_delivery_time = last_delivery_stat.get('EventTimestamp', None)
|
|
return last_delivery_time
|
|
|
|
def update_delivery_timestamp(self, node_name, subscriptionid, this_delivery_time):
|
|
if not self.notification_stat.get(node_name, None):
|
|
self.notification_stat[node_name] = {
|
|
subscriptionid: {
|
|
'EventTimestamp': this_delivery_time
|
|
}
|
|
}
|
|
LOG.debug("delivery time @node: {0},subscription:{1} is added".format(
|
|
node_name, subscriptionid))
|
|
elif not self.notification_stat[node_name].get(subscriptionid, None):
|
|
self.notification_stat[node_name][subscriptionid] = {
|
|
'EventTimestamp': this_delivery_time
|
|
}
|
|
LOG.debug("delivery time @node: {0},subscription:{1} is added".format(
|
|
node_name, subscriptionid))
|
|
else:
|
|
last_delivery_stat = self.notification_stat.get(node_name, {}).get(subscriptionid, {})
|
|
last_delivery_time = last_delivery_stat.get('EventTimestamp', None)
|
|
LOG.debug("last_delivery_time %s this_delivery_time %s" % (last_delivery_time, this_delivery_time))
|
|
if (last_delivery_time and last_delivery_time >= this_delivery_time):
|
|
return
|
|
last_delivery_stat['EventTimestamp'] = this_delivery_time
|
|
LOG.debug("delivery time @node: {0},subscription:{1} is updated".format(
|
|
node_name, subscriptionid))
|