
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
180 lines
8.0 KiB
Python
180 lines
8.0 KiB
Python
#
|
|
# Copyright (c) 2021-2024 Wind River Systems, Inc.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
|
|
import json
|
|
import logging
|
|
import re
|
|
from datetime import datetime
|
|
|
|
import requests
|
|
from notificationclientsdk.common.helpers import constants, log_helper
|
|
from notificationclientsdk.exception import client_exception
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
log_helper.config_logger(LOG)
|
|
|
|
|
|
def notify(subscriptioninfo, notification, timeout=2, retry=3):
|
|
result = False
|
|
while True:
|
|
retry = retry - 1
|
|
try:
|
|
headers = {'Content-Type': 'application/json'}
|
|
url = subscriptioninfo.EndpointUri
|
|
if 'ResourceType' in notification:
|
|
# version 1
|
|
data = format_notification_data(subscriptioninfo, notification)
|
|
data = json.dumps(data)
|
|
response = requests.post(url, data=data, headers=headers,
|
|
timeout=timeout)
|
|
response.raise_for_status()
|
|
else:
|
|
if isinstance(notification, list):
|
|
# List-type notification response format
|
|
LOG.debug("Formatting subscription response: list")
|
|
# Post notification for each list item
|
|
for item in notification:
|
|
data = json.dumps(item)
|
|
LOG.info("Notification to post %s", (data))
|
|
response = requests.post(url, data=data, headers=headers,
|
|
timeout=timeout)
|
|
response.raise_for_status()
|
|
else:
|
|
# Dict type notification response format
|
|
LOG.debug("Formatting subscription response: dict")
|
|
if notification.get('id', None):
|
|
# Not a nested dict, post the data
|
|
data = json.dumps(notification)
|
|
LOG.info("Notification to post %s", (data))
|
|
response = requests.post(url, data=data, headers=headers,
|
|
timeout=timeout)
|
|
response.raise_for_status()
|
|
else:
|
|
for item in notification:
|
|
# Nested dict with instance tags, post each item
|
|
data = format_notification_data(subscriptioninfo, {item: notification[item]})
|
|
data = json.dumps(data)
|
|
LOG.info("Notification to post %s", (data))
|
|
response = requests.post(url, data=data, headers=headers,
|
|
timeout=timeout)
|
|
response.raise_for_status()
|
|
|
|
if notification == {}:
|
|
if hasattr(subscriptioninfo, 'ResourceType'):
|
|
resource = "{'ResourceType':'" + \
|
|
subscriptioninfo.ResourceType + "'}"
|
|
elif hasattr(subscriptioninfo, 'ResourceAddress'):
|
|
_, _, resource, _, _ = parse_resource_address(
|
|
subscriptioninfo.ResourceAddress)
|
|
raise client_exception.InvalidResource(resource)
|
|
result = True
|
|
return response
|
|
except client_exception.InvalidResource as ex:
|
|
raise ex
|
|
except requests.exceptions.ConnectionError as errc:
|
|
if retry > 0:
|
|
LOG.warning("Retry notifying due to: {0}".format(str(errc)))
|
|
continue
|
|
raise errc
|
|
except requests.exceptions.Timeout as errt:
|
|
if retry > 0:
|
|
LOG.warning("Retry notifying due to: {0}".format(str(errt)))
|
|
continue
|
|
raise errt
|
|
except requests.exceptions.RequestException as ex:
|
|
LOG.warning("Failed to notify due to: {0}".format(str(ex)))
|
|
LOG.warning(" %s", (notification))
|
|
raise ex
|
|
except requests.exceptions.HTTPError as ex:
|
|
LOG.warning("Failed to notify due to: {0}".format(str(ex)))
|
|
raise ex
|
|
except Exception as ex:
|
|
LOG.warning("Failed to notify due to: {0}".format(str(ex)))
|
|
raise ex
|
|
|
|
return result
|
|
|
|
def format_notification_data(subscriptioninfo, notification):
|
|
if isinstance(notification, list):
|
|
return notification
|
|
|
|
# Formatting for legacy notification
|
|
if hasattr(subscriptioninfo, 'ResourceType'):
|
|
LOG.debug("format_notification_data: Found v1 subscription, "
|
|
"no formatting required.")
|
|
return notification
|
|
elif hasattr(subscriptioninfo, 'ResourceAddress'):
|
|
_, _, resource_path, _, _ = parse_resource_address(
|
|
subscriptioninfo.ResourceAddress)
|
|
if resource_path not in constants.RESOURCE_ADDRESS_MAPPINGS.keys():
|
|
raise client_exception.InvalidResource(resource_path)
|
|
resource_mapped_value = constants.RESOURCE_ADDRESS_MAPPINGS[
|
|
resource_path]
|
|
formatted_notification = {resource_mapped_value: []}
|
|
for instance in notification:
|
|
# Add the instance identifier to ResourceAddress for PTP lock-state
|
|
# and PTP clockClass
|
|
if notification[instance]['source'] in [
|
|
constants.SOURCE_SYNC_PTP_CLOCK_CLASS,
|
|
constants.SOURCE_SYNC_PTP_LOCK_STATE]:
|
|
temp_values = notification[instance].get('data', {}).get(
|
|
'values', [])
|
|
resource_address = temp_values[0].get('ResourceAddress', None)
|
|
if instance not in resource_address:
|
|
add_instance_name = resource_address.split('/', 3)
|
|
add_instance_name.insert(3, instance)
|
|
resource_address = '/'.join(add_instance_name)
|
|
notification[instance]['data']['values'][0][
|
|
'ResourceAddress'] = resource_address
|
|
formatted_notification[resource_mapped_value].append(
|
|
notification[instance])
|
|
for instance in formatted_notification[resource_mapped_value]:
|
|
this_delivery_time = instance['time']
|
|
if type(this_delivery_time) != str:
|
|
format_time = datetime.fromtimestamp(
|
|
float(this_delivery_time)).strftime('%Y-%m-%dT%H:%M:%S%fZ')
|
|
instance['time'] = format_time
|
|
else:
|
|
LOG.warning("format_notification_data: No valid source "
|
|
"address found in notification")
|
|
LOG.debug("format_notification_data: Added parent key for client "
|
|
"consumption: %s" % formatted_notification)
|
|
return formatted_notification
|
|
|
|
|
|
def parse_resource_address(resource_address):
|
|
# The format of resource address is:
|
|
# /{clusterName}/{siteName}(/optional/hierarchy/..)/{nodeName}/{resource}
|
|
clusterName = resource_address.split('/')[1]
|
|
nodeName = resource_address.split('/')[2]
|
|
resource_path = '/' + re.split('[/]', resource_address, 3)[3]
|
|
resource_list = re.findall(r'[^/]+', resource_path)
|
|
if len(resource_list) == 4:
|
|
remove_optional = '/' + resource_list[0]
|
|
resource_path = resource_path.replace(remove_optional, '')
|
|
resource_address = resource_address.replace(remove_optional, '')
|
|
optional = resource_list[0]
|
|
LOG.debug("Optional hierarchy found when parsing resource address: %s"
|
|
% optional)
|
|
else:
|
|
optional = None
|
|
|
|
# resource_address is the full address without any optional hierarchy
|
|
# resource_path is the specific identifier for the resource
|
|
return clusterName, nodeName, resource_path, optional, resource_address
|
|
|
|
|
|
def set_nodename_in_resource_address(resource_address, nodename):
|
|
# The format of resource address is:
|
|
# /{clusterName}/{siteName}(/optional/hierarchy/..)/{nodeName}/{resource}
|
|
cluster, _, path, optional, _ = parse_resource_address(
|
|
resource_address)
|
|
resource_address = '/' + cluster + '/' + nodename
|
|
if optional:
|
|
resource_address += '/' + optional
|
|
resource_address += path
|
|
return resource_address
|