From: Henrik Andersson Date: Thu, 6 May 2021 05:26:57 +0000 (+0000) Subject: Merge "First version link failure use case" X-Git-Tag: 2.2.0~51 X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=commitdiff_plain;h=c4d4c18bdac10d5ae35f24d8bcd9db2271124c0b;hp=cc00339fa5e6f3ca9b6eb67b76061594706e7a29;p=nonrtric.git Merge "First version link failure use case" --- diff --git a/test/auto-test/startMR.sh b/test/auto-test/startMR.sh new file mode 100755 index 00000000..bb4e5fa5 --- /dev/null +++ b/test/auto-test/startMR.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# ============LICENSE_START=============================================== +# Copyright (C) 2021 Nordix Foundation. All rights reserved. +# ======================================================================== +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============LICENSE_END================================================= +# + + +TC_ONELINE_DESCR="Starts DMAAP MR" + +#App names to include in the test when running docker, space separated list +DOCKER_INCLUDED_IMAGES="MR DMAAPMR" + +#App names to include in the test when running kubernetes, space separated list +KUBE_INCLUDED_IMAGES="CP CR MR PA RICSIM SDNC KUBEPROXY NGW" +#Prestarted app (not started by script) to include in the test when running kubernetes, space separated list +KUBE_PRESTARTED_IMAGES="" + +#Ignore image in DOCKER_INCLUDED_IMAGES, KUBE_INCLUDED_IMAGES if +#the image is not configured in the supplied env_file +#Used for images not applicable to all supported profile +CONDITIONALLY_IGNORED_IMAGES="NGW" + +#Supported test environment profiles +SUPPORTED_PROFILES="ONAP-GUILIN ONAP-HONOLULU ORAN-CHERRY ORAN-DAWN" +#Supported run modes +SUPPORTED_RUNMODES="DOCKER KUBE" + +. ../common/testcase_common.sh $@ +. ../common/agent_api_functions.sh +. ../common/consul_cbs_functions.sh +. ../common/control_panel_api_functions.sh +. ../common/controller_api_functions.sh +. ../common/cr_api_functions.sh +. ../common/mr_api_functions.sh +. ../common/ricsimulator_api_functions.sh +. ../common/http_proxy_api_functions.sh +. ../common/kube_proxy_api_functions.sh +. ../common/gateway_api_functions.sh + +setup_testenvironment + +#### TEST BEGIN #### + +clean_environment +start_mr +docker kill mr-stub + + +print_result + + diff --git a/test/usecases/linkfailure/README.md b/test/usecases/linkfailure/README.md new file mode 100644 index 00000000..76c22d94 --- /dev/null +++ b/test/usecases/linkfailure/README.md @@ -0,0 +1,42 @@ +# Use case Link Failure +# General + +The Link Failure use case test provides a python script that regularly polls DMaaP Message Router (MR) for "CUS Link Failure" +messages. + +When such a message appears with the "eventSeverity" set to "CRITICAL" a configuration change message with the +"administrative-state" set to "UNLOCKED" will be sent to the O-DU mapped to the O-RU that sent the alarm. + +When such a message appears with the "eventSeverity" set to "NORMAL" a printout will be made to signal that the +alarm has been cleared, provided that the verbose option has been used when the test was started. + +# Prerequisits +To run this script Python3 needs to be installed. To install the script's dependencies, run the following command from +the `src` folder: `pip install -r requirements.txt` + +Also, the MR needs to be up and running with a topic created for the alarms and there must be an endpoint for the +configuration change event that will accept these. + +For convenience, a message generator and a change event endpoint simulator are provided. + +# How to run +Go to the `src/` folder and run `python3 main.py`. The script will start and run until stopped. Use the `-h` option to +see the options available for the script. + + +## License + +Copyright (C) 2021 Nordix Foundation. +Licensed under the Apache License, Version 2.0 (the "License") +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +For more information about license please see the [LICENSE](LICENSE.txt) file for details. diff --git a/test/usecases/linkfailure/requirements.txt b/test/usecases/linkfailure/requirements.txt new file mode 100644 index 00000000..906210eb --- /dev/null +++ b/test/usecases/linkfailure/requirements.txt @@ -0,0 +1,3 @@ +requests==2.25.1 +json==3.9.4 +time==3.9.4 \ No newline at end of file diff --git a/test/usecases/linkfailure/src/main.py b/test/usecases/linkfailure/src/main.py new file mode 100644 index 00000000..3629ea83 --- /dev/null +++ b/test/usecases/linkfailure/src/main.py @@ -0,0 +1,134 @@ + +# ============LICENSE_START=============================================== +# Copyright (C) 2021 Nordix Foundation. All rights reserved. +# ======================================================================== +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============LICENSE_END================================================= +# + +import argparse +import ast +import requests +import json +import time + +SDNR_PATH = "/rests/data/network-topology:network-topology/topology=topology-netconf/node=O-RAN-DU-01/yang-ext:mount/o-ran-sc-du-hello-world:network-function/du-to-ru-connection=" + +UNLOCK_MESSAGE = { + "o-ran-sc-du-hello-world:du-to-ru-connection": [ + { + "name":"", + "administrative-state":"UNLOCKED" + } + ] +} + + +def is_message_new_link_failure(message): + msg_as_json = json.loads(message) + event_headers = msg_as_json["event"]["commonEventHeader"] + + link_failure = False + if (event_headers["domain"] == "fault"): + fault_fields = msg_as_json["event"]["faultFields"] + link_failure = fault_fields["specificProblem"] == "CUS Link Failure" and fault_fields["eventSeverity"] == "CRITICAL" + + return link_failure + + +def is_message_clear_link_failure(message): + msg_as_json = json.loads(message) + event_headers = msg_as_json["event"]["commonEventHeader"] + + link_failure_clear = False + if (event_headers["domain"] == "fault"): + fault_fields = msg_as_json["event"]["faultFields"] + link_failure_clear = fault_fields["specificProblem"] == "CUS Link Failure" and fault_fields["eventSeverity"] == "NORMAL" + + return link_failure_clear + + +def handle_link_failure(message, o_ru_to_o_du_map, sdnr_address): + verboseprint("Got a link failure: ") + alarm_msg_as_json = json.loads(message) + event_headers = alarm_msg_as_json["event"]["commonEventHeader"] + o_ru_id = event_headers["reportingEntityId"] + verboseprint("O-RU ID: " + o_ru_id) + o_du_id = o_ru_to_o_du_map[o_ru_id] + verboseprint("O-DU ID: " + o_du_id) + unlock_msg = json.loads(json.dumps(UNLOCK_MESSAGE)) + unlock_msg["o-ran-sc-du-hello-world:du-to-ru-connection"][0]["name"] = o_du_id + response = requests.post(sdnr_address + SDNR_PATH + o_du_id, json=unlock_msg) + print(response) + + +def handle_clear_link_failure(message): + msg_as_json = json.loads(message) + event_headers = msg_as_json["event"]["commonEventHeader"] + o_ru_id = event_headers["reportingEntityId"] + verboseprint("Cleared Link Failure for O-RU ID: " + o_ru_id) + + +def read_o_ru_to_o_du_map_from_file(map_file): + file = open(map_file, "r") + contents = file.read() + dictionary = ast.literal_eval(contents) + file.close() + return dictionary + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(prog='PROG') + parser.add_argument('--mrHost', help='The URL of the MR host', default="http://message-router.onap") + parser.add_argument('--mrPort', help='The port of the MR host', type=int, default=3904) + parser.add_argument('--mrTopic', help='The topic to poll messages from', default="ALARMS-WRITE") + parser.add_argument('--sdnrHost', help='The URL of the SNDR host', default="http://localhost") + parser.add_argument('--sdnrPort', help='The port of the SDNR host', type=int, default=9990) + parser.add_argument('--oRuTooDuMapFile', help='A file with the mapping between O-RU ID and O-DU ID as a dictionary', default="o-ru-to-o-du-map.txt") + parser.add_argument('--pollTime', help='The time between polls', type=int, default=10) + parser.add_argument('-v', '--verbose', action='store_true', help='Turn on verbose printing') + parser.add_argument('--version', action='version', version='%(prog)s 1.0') + args = vars(parser.parse_args()) + mr_host = args["mrHost"] + mr_port = args["mrPort"] + mr_topic = args["mrTopic"] + sdnr_host = args["sdnrHost"] + sdnr_port = args["sdnrPort"] + o_ru_to_o_du_map = read_o_ru_to_o_du_map_from_file(args["oRuTooDuMapFile"]) + pollTime = args["pollTime"] + + if args["verbose"]: + + def verboseprint(*args, **kwargs): + print(*args, **kwargs) + + else: + verboseprint = lambda *a, **k: None # do-nothing function + + verboseprint("Using MR address: " + mr_host + ":" + str(mr_port) + " and topic: " + mr_topic) + verboseprint("Using SDNR address: " + sdnr_host + ":" + str(sdnr_port)) + verboseprint("Starting with " + str(pollTime) + " seconds between polls") + mr_address = mr_host + ":" + str(mr_port) + "/events/" + mr_topic + "/users/test/" + sdnr_address = sdnr_host + ":" + str(sdnr_port) + + while True: + response = requests.get(mr_address) + verboseprint("Polling") + messages = response.json() + for message in messages: + if (is_message_new_link_failure(message)): + handle_link_failure(message, o_ru_to_o_du_map, sdnr_address) + elif (is_message_clear_link_failure(message)): + handle_clear_link_failure(message) + + time.sleep(pollTime) diff --git a/test/usecases/linkfailure/src/message_generator.py b/test/usecases/linkfailure/src/message_generator.py new file mode 100644 index 00000000..0ab37e5e --- /dev/null +++ b/test/usecases/linkfailure/src/message_generator.py @@ -0,0 +1,101 @@ + +# ============LICENSE_START=============================================== +# Copyright (C) 2021 Nordix Foundation. All rights reserved. +# ======================================================================== +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============LICENSE_END================================================= +# + +import requests +import time +import random +import json + +# Randomly, between 0 and 10 seconds sends a "CUS Link Failure" alarm event to the Message Router. The ID of the O-RU is also +# randomly generated between 0 and 9. +# When the modulo of the ID is 1, a "heartbeat" message will also be sent to MR. + +linkFailureMessage = { + "event": { + "commonEventHeader": { + "domain": "fault", + "eventId": "nt:network-topology/nt:topology/nt:node/nt:node-id", + "eventName": "fault_O-RAN-RU-Fault_Alarms_CUS_Link_Failure", + "eventType": "O-RAN-RU-Fault_Alarms", + "sequence": 0, + "priority": "High", + "reportingEntityId": "uro1", + "reportingEntityName": "@controllerName@", + "sourceId": "", + "sourceName": "nt:network-topology/nt:topology/nt:node/nt:node-id", + "startEpochMicrosec": "@timestamp@", + "lastEpochMicrosec": "@timestamp@", + "nfNamingCode": "", + "nfVendorName": "ietf-hardware (RFC8348) /hardware/component[not(parent)][1]/mfg-name", + "timeZoneOffset": "+00:00", + "version": "4.1", + "vesEventListenerVersion": "7.2.1" + }, + "faultFields": { + "faultFieldsVersion": "4.0", + "alarmCondition": "o-ran-fm:alarm-notif/fault-id", + "alarmInterfaceA": "o-ran-fm:alarm-notif/fault-source", + "eventSourceType": "ietf-hardware (RFC8348) /hardware/component[not(parent)][1]/mfg-model or \"O-RU\"", + "specificProblem": "CUS Link Failure", + "eventSeverity": "CRITICAL", + "vfStatus": "Active", + "alarmAdditionalInformation": { + "eventTime": "@eventTime@", + "equipType": "@type@", + "vendor": "@vendor@", + "model": "@model@" + } + } + } +} + +heartBeatMessage = { + "event": { + "commonEventHeader": { + "version": 3.0, + "domain": "heartbeat", + "eventName": "Heartbeat\_vIsbcMmc", + "eventId": "ab305d54-85b4-a31b-7db2fb6b9e546015", + "sequence": 0, + "priority": "Normal", + "reportingEntityId": "cc305d54-75b4-431badb2eb6b9e541234", + "reportingEntityName": "EricssonOamVf", + "sourceId": "de305d54-75b4-431b-adb2-eb6b9e546014", + "sourceName": "ibcx0001vm002ssc001", + "nfNamingCode": "ibcx", + "nfcNamingCode": "ssc", + "startEpochMicrosec": 1413378172000000, + "lastEpochMicrosec": 1413378172000000 + } + } + } + +while True: + random_time = int(10 * random.random()) + if (random_time % 3 == 1): + print("Sent heart beat") + requests.post("http://localhost:3904/events/ALARMS-WRITE", json=heartBeatMessage); + + o_ru_id = "O-RAN-RU-0" + str(random_time) + print("Sent link failure for O-RAN-RU: " + o_ru_id) + msg_as_json = json.loads(json.dumps(linkFailureMessage)) + msg_as_json["event"]["commonEventHeader"]["reportingEntityId"] = o_ru_id + requests.post("http://localhost:3904/events/ALARMS-WRITE", json=msg_as_json); + + time.sleep(random_time) + diff --git a/test/usecases/linkfailure/src/o-ru-to-o-du-map.txt b/test/usecases/linkfailure/src/o-ru-to-o-du-map.txt new file mode 100644 index 00000000..62128836 --- /dev/null +++ b/test/usecases/linkfailure/src/o-ru-to-o-du-map.txt @@ -0,0 +1,12 @@ +{ + "O-RAN-RU-00": "O-RAN-DU-0", + "O-RAN-RU-01": "O-RAN-DU-1", + "O-RAN-RU-02": "O-RAN-DU-2", + "O-RAN-RU-03": "O-RAN-DU-3", + "O-RAN-RU-04": "O-RAN-DU-4", + "O-RAN-RU-05": "O-RAN-DU-5", + "O-RAN-RU-06": "O-RAN-DU-6", + "O-RAN-RU-07": "O-RAN-DU-7", + "O-RAN-RU-08": "O-RAN-DU-8", + "O-RAN-RU-09": "O-RAN-DU-9" +} \ No newline at end of file diff --git a/test/usecases/linkfailure/src/sdnc_simulator.py b/test/usecases/linkfailure/src/sdnc_simulator.py new file mode 100644 index 00000000..bc9e4b50 --- /dev/null +++ b/test/usecases/linkfailure/src/sdnc_simulator.py @@ -0,0 +1,112 @@ + +# ============LICENSE_START=============================================== +# Copyright (C) 2021 Nordix Foundation. All rights reserved. +# ======================================================================== +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============LICENSE_END================================================= +# + +from flask import Flask, request +from flask import Response +import json +import random +import requests +import threading + +# Provides an endpoint for the "UNLOCK" configuration change for an O-DU. +# Stores the ID of the O-DU and randomly, after between 0 and 10 seconds, sends an Alarm Notification that clears the +# "CUS Link Failure" alarm event to MR. +app = Flask(__name__) + +# Server info +HOST_IP = "::" +HOST_PORT = 9990 +APP_URL = "/rests/data/network-topology:network-topology/topology=topology-netconf/node=O-RAN-DU-01/yang-ext:mount/" + +linkFailureMessage = { + "event": { + "commonEventHeader": { + "domain": "fault", + "eventId": "nt:network-topology/nt:topology/nt:node/nt:node-id", + "eventName": "fault_O-RAN-RU-Fault_Alarms_CUS_Link_Failure", + "eventType": "O-RAN-RU-Fault_Alarms", + "sequence": 0, + "priority": "High", + "reportingEntityId": "uro1", + "reportingEntityName": "@controllerName@", + "sourceId": "", + "sourceName": "nt:network-topology/nt:topology/nt:node/nt:node-id", + "startEpochMicrosec": "@timestamp@", + "lastEpochMicrosec": "@timestamp@", + "nfNamingCode": "", + "nfVendorName": "ietf-hardware (RFC8348) /hardware/component[not(parent)][1]/mfg-name", + "timeZoneOffset": "+00:00", + "version": "4.1", + "vesEventListenerVersion": "7.2.1" + }, + "faultFields": { + "faultFieldsVersion": "4.0", + "alarmCondition": "o-ran-fm:alarm-notif/fault-id", + "alarmInterfaceA": "o-ran-fm:alarm-notif/fault-source", + "eventSourceType": "ietf-hardware (RFC8348) /hardware/component[not(parent)][1]/mfg-model or \"O-RU\"", + "specificProblem": "CUS Link Failure", + "eventSeverity": "NORMAL", + "vfStatus": "Active", + "alarmAdditionalInformation": { + "eventTime": "@eventTime@", + "equipType": "@type@", + "vendor": "@vendor@", + "model": "@model@" + } + } + } +} + + +class AlarmClearThread (threading.Thread): + + def __init__(self, sleep_time, o_ru_id): + threading.Thread.__init__(self) + self.sleep_time = sleep_time + self.o_ru_id = o_ru_id + + def run(self): + print(f'Sleeping: {self.sleep_time} before clearing O-DU: {self.o_ru_id}') + msg_as_json = json.loads(json.dumps(linkFailureMessage)) + msg_as_json["event"]["commonEventHeader"]["reportingEntityId"] = self.o_ru_id + requests.post("http://localhost:3904/events/ALARMS-WRITE", json=msg_as_json); + + +# I'm alive function +@app.route('/', + methods=['GET']) +def index(): + return 'OK', 200 + + +@app.route(APP_URL + "o-ran-sc-du-hello-world:network-function/", + methods=['POST']) +def sendrequest(id): + o_du_id = id.split("=")[1] + print("Config change for O-DU with ID " + o_du_id) + payload = json.loads(json.dumps(request.json)) + o_ru_id = payload["o-ran-sc-du-hello-world:du-to-ru-connection"][0]["name"] + random_time = int(10 * random.random()) + alarm_clear_thread = AlarmClearThread(random_time, o_ru_id) + alarm_clear_thread.start() + + return Response(status=201) + + +if __name__ == "__main__": + app.run(port=HOST_PORT, host=HOST_IP)