Cole Walker bbeef95744 Rework notification messages to remove instance tags
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
2024-11-20 09:15:35 -05:00

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))