Andre Mauricio Zelak 1ac45c8f43 Add support to '/sync' aka sync all subscription
When adding a new subscription check for an existing matching one,
considering the source uri hierachy. Deny a new individual
if there is already a sync all subscription, and deny
a new sync all if there is already an invidual one.

After a new sync all subscription is created a set of event messages
are sent to the client containing the initial state of each source
down in the hierarchy. And, every time one of the source states changes
a new message is sent.

Test Plan:
PASS: Build the container images
PASS: Mannually deploy them and test with v2 client
PASS: Create a '/././sync' subscription and check the event messages
PASS: Check current subscription list
PASS: Change GNSS sync state and check the event messages
PASS: Attempt to create a new individual subscription and
      check it fails
PASS: Delete the '/././sync' subscription
PASS: Check current subscription list again

Closes-bug: 2009188

Signed-off-by: Andre Mauricio Zelak <andre.zelak@windriver.com>
Change-Id: I90b642e73f30fb1798f4a93ab5313411c177949c
2023-03-10 17:29:05 -03:00

137 lines
5.7 KiB
Python

#
# Copyright (c) 2021-2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import json
import re
import requests
import logging
from datetime import datetime
from notificationclientsdk.common.helpers import constants
from notificationclientsdk.common.helpers import 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
for item in notification:
data = format_notification_data(subscriptioninfo, {item: notification[item]})
data = json.dumps(data)
response = requests.post(url, data=data, headers=headers,
timeout=timeout)
response.raise_for_status()
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)))
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 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:
raise Exception("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