Fix dummy values in gRPC message sent to RC xApp
TS xApp did not populate ran_name, plmn_id, and e2_node_id
in gRPC messages sent to RC xApp. These fields were always
set up with dummy values.
This change maps the nodeb's information from the handoff target cell,
and populates the required information in corresponding fields.
This change also adds a generic REST client API to send HTTP requests,
and allows the E2 Manager endpoint used by TS to changed using the
environment variable SERVICE_E2MGR_HTTP_BASE_URL.
Issue-ID: RICAPP-187
Signed-off-by: Alexandre Huff <alexandrehuff@utfpr.edu.br>
Change-Id: I45acb87ad2391199658d41457919279e46c97fdf
diff --git a/.gitignore b/.gitignore
index bf365a8..d7d021c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.build/
*-
*~
+*.stash.inc
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3502dc4..f55aa94 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -34,7 +34,7 @@
set( major_version "1" ) # until CI supports auto tagging; must hard set
set( minor_version "2" )
-set( patch_level "1" )
+set( patch_level "2" )
set( install_root "${CMAKE_INSTALL_PREFIX}" )
set( install_inc "/usr/local/include" )
@@ -79,16 +79,21 @@
-DDEBUG=${debugging}
)
+
+# bleeding cmake names are short novels; and when lines cannot be split they're a pain
+set ( srcd "${CMAKE_CURRENT_SOURCE_DIR}" )
+
+
# Compiler flags
#
set( CMAKE_POSITION_INDEPENDENT_CODE ON )
if( GPROF ) # if set, we'll set profiling flag on compiles
message( "+++ profiling is on" )
set( CMAKE_C_FLAGS "-pg " )
- set( CMAKE_CPP_FLAGS "-pg " )
+ set( CMAKE_CXX_FLAGS "-pg " )
else()
set( CMAKE_C_FLAGS "-g " )
- set( CMAKE_CPP_FLAGS "-g " )
+ set( CMAKE_CXX_FLAGS "-g " )
message( "+++ profiling is off" )
endif()
unset( GPROF CACHE ) # ensure this does not persist
@@ -96,6 +101,9 @@
# protobuf and grpc stufs
add_subdirectory( ext/protobuf )
+# general stufs
+add_subdirectory( src/utils )
+
# each binary is built from a subset
add_subdirectory( src/ts_xapp )
diff --git a/Dockerfile b/Dockerfile
index 2e09d79..7a2d2e9 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -41,7 +41,7 @@
# versions we snarf from package cloud
ARG RMR_VER=4.8.1
-# ARG SDL_VER=1.0.4
+# ARG SDL_VER=1.4.0
ARG XFCPP_VER=2.3.6
# package cloud urls for wget
@@ -59,10 +59,14 @@
dpkg -i ricxfcpp-dev_${XFCPP_VER}_amd64.deb ricxfcpp_${XFCPP_VER}_amd64.deb
# # snarf up SDL dependencies, then pull SDL package and install
-# RUN apt-get update
-# RUN apt-get install -y libboost-filesystem1.65.1 libboost-system1.65.1 libhiredis0.13
-# RUN wget -nv --content-disposition ${PC_STG_URL}/sdl_${SDL_VER}-1_amd64.deb/download.deb && \
-# wget -nv --content-disposition ${PC_STG_URL}/sdl-dev_${SDL_VER}-1_amd64.deb/download.deb &&\
+# RUN apt-get update && apt-get install -y \
+# libboost-filesystem1.67.0 \
+# libboost-system1.67.0 \
+# libhiredis-dev \
+# libhiredis0.14 \
+# && apt-get clean
+# RUN wget -nv --content-disposition ${PC_REL_URL}/sdl_${SDL_VER}-1_amd64.deb/download.deb && \
+# wget -nv --content-disposition ${PC_REL_URL}/sdl-dev_${SDL_VER}-1_amd64.deb/download.deb &&\
# dpkg -i sdl-dev_${SDL_VER}-1_amd64.deb sdl_${SDL_VER}-1_amd64.deb
RUN git clone https://github.com/Tencent/rapidjson && \
@@ -71,7 +75,7 @@
cd build && \
cmake -DCMAKE_INSTALL_PREFIX=/usr/local .. && \
make install && \
- cd ${STAGE_DIR} && \
+ cd ../../ && \
rm -rf rapidjson
# install curl and gRPC dependencies
@@ -110,10 +114,14 @@
# # sdl doesn't install into /usr/local like everybody else, and we don't want to
# # hunt for it or copy all of /usr, so we must pull and reinstall it.
-# RUN apt-get update
-# RUN apt-get install -y libboost-filesystem1.65.1 libboost-system1.65.1 libhiredis0.13 wget
-# RUN wget -nv --content-disposition ${PC_STG_URL}/sdl_${SDL_VER}-1_amd64.deb/download.deb && \
-# wget -nv --content-disposition ${PC_STG_URL}/sdl-dev_${SDL_VER}-1_amd64.deb/download.deb &&\
+# RUN apt-get update && apt-get install -y \
+# libboost-filesystem1.67.0 \
+# libboost-system1.67.0 \
+# libhiredis-dev \
+# libhiredis0.14 \
+# && apt-get clean
+# RUN wget -nv --content-disposition ${PC_REL_URL}/sdl_${SDL_VER}-1_amd64.deb/download.deb && \
+# wget -nv --content-disposition ${PC_REL_URL}/sdl-dev_${SDL_VER}-1_amd64.deb/download.deb &&\
# dpkg -i sdl-dev_${SDL_VER}-1_amd64.deb sdl_${SDL_VER}-1_amd64.deb
# RUN rm -fr /var/lib/apt/lists
diff --git a/container-tag.yaml b/container-tag.yaml
index 4489ca2..f020aa4 100644
--- a/container-tag.yaml
+++ b/container-tag.yaml
@@ -1,3 +1,3 @@
# this is used by CI jobs to apply a tag when it builds the image
---
-tag: '1.2.1'
+tag: '1.2.2'
diff --git a/docs/user-guide.rst b/docs/user-guide.rst
index 31c219c..d25c714 100644
--- a/docs/user-guide.rst
+++ b/docs/user-guide.rst
@@ -24,9 +24,10 @@
* KPI Monitoring xApp: Gathers the radio and system Key Performance Indicators (KPI) metrics from E2 Nodes and stores them in the Shared Data Layer (SDL).
* Anomaly Detection (AD) xApp: Fetches UE data regularly from SDL, monitors UE metrics and sends the anomalous UEs to Traffic Steering xApp.
-* Traffic Steering xApp (*this one*): Consumes A1 Policy Intent, listens for badly performing UEs, sends prediction requests to QP Driver, and listens for messages that show UE throughput predictions in different cells to make a decision about UE Handover.
-* QoE Prediction Driver (QP Driver) xApp: Generates a feature set of metrics to input to QoE Prediction, based on SDL lookups in UE-Metric and Cell-Metric namespaces.
-* QoE Prediction (QP) xApp: Receives a feature set of metrics for a given UE, and output Throughput predictions on the Serving and any Neighbor cells to Traffic Steering xApp.
+* Traffic Steering xApp (*this one*): Consumes A1 Policy Intent, listens for badly performing UEs, sends prediction requests to QP xApp, and listens for messages from QP that show UE throughput predictions in different cells to make decisions about UE Handover.
+* QoE Prediction (QP) xApp: Generates a feature set of metrics based on SDL lookups in UE-Metric and Cell-Metric namespaces for a given UE, and outputs Throughput predictions on the Serving and any Neighbor cells to the Traffic Steering xApp.
+* RAN Control (RC) xApp: Provides basic implementation of spec compliant E2-SM RC to send RIC Control Request messages to RAN/E2 Nodes.
+
A1 Policy
=========
@@ -43,10 +44,7 @@
{ "threshold": 5 }
-.. FIXME Is the "Serving Cell RSRP" related to "Degradation" in AD message
-
-This Policy instructs Traffic Steering xApp to request a QoE Prediction for any UE whose Serving Cell RSRP is less than 5.
-Traffic Steering logs each A1 Policy update.
+This Policy instructs Traffic Steering xApp to hand-off any UE whose downlink throughput of its current serving cell is 5% below the throughput of any neighboring cell.
Receiving Anomaly Detection
===========================
@@ -70,8 +68,9 @@
Sending QoE Prediction Request
==============================
-Traffic Steering listens for badly performing UEs. When it identifies a UE whose RSRP is below the threshold, it generates
-a QoE Prediction Request message and sends it to the QP Driver xApp. The RMR Message Type is 30000.
+Traffic Steering listens for badly performing UEs.
+Each Anomaly Detection message received from AD xApp, results in a QoE Prediction Request to QP xApp.
+The RMR Message Type is 30000.
The following is an example message body:
.. {"UEPredictionSet" : ["12345"]}
@@ -80,9 +79,6 @@
{ "UEPredictionSet": ["Train passenger 2"] }
-The current version of Traffic Steering xApp does not (yet) consider the A1 policy to generate QoE prediction requests.
-Each Anomaly Detection message received from AD xApp, results in a QoE Prediction Request to QP Driver xApp.
-
Receiving QoE Prediction
========================
@@ -105,7 +101,10 @@
Traffic Steering xApp checks for the Service Cell ID for UE ID, and determines whether the predicted throughput is higher in a neighbor cell.
The first cell in this prediction message is assumed to be the serving cell.
-If predicted throughput is higher in a neighbor cell, Traffic Steering sends a CONTROL message through a REST call to E2 SIM. This message requests to hand-off the corresponding UE, and an example of its payload is as follows:
+Since RC xApp is not mandatory for the Traffic Steering use case, TS xApp sends CONTROL messages using either REST or gRPC calls.
+The CONTROL endpoint is set up in the xApp descriptor file called "config-file.json". Please, check out the "schema.json" file for configuration examples.
+
+The following is an example of a REST message that requests the handover of a given UE:
.. code-block::
@@ -120,4 +119,30 @@
"ttl": 10
}
-Traffic Steering also logs the REST response, which shows whether or not the control operation has succeeded.
+Control messages might also be exchanged with E2 Simulators that implement REST-based interfaces.
+Traffic Steering then logs the REST response showing whether or not the control operation has succeeded.
+
+The gRPC interface is only required to exchange messages with the RC xApp.
+The following is an example of the gRPC message (*string representation*) which requests the RC xApp to handover a given UE:
+
+.. code-block::
+
+ e2NodeID: "000000000001001000110100"
+ plmnID: "02F829"
+ ranName: "enb_208_092_001235"
+ RICE2APHeaderData {
+ RanFuncId: 300
+ RICRequestorID: 1001
+ }
+ RICControlHeaderData {
+ ControlStyle: 3
+ ControlActionId: 1
+ UEID: "Train passenger 2"
+ }
+ RICControlMessageData {
+ TargetCellID: "mnop"
+ }
+
+TS xApp also requires to fetch additional RAN information from the E2 Manager to communicate with RC xApp.
+By default, TS xApp requests information to the default endpoint of E2 Manager in the Kubernetes cluster.
+Still, the default E2 Manager endpoint from TS can be changed using the env variable "SERVICE_E2MGR_HTTP_BASE_URL".
diff --git a/ext/protobuf/CMakeLists.txt b/ext/protobuf/CMakeLists.txt
index 79f9c59..d3b7d57 100644
--- a/ext/protobuf/CMakeLists.txt
+++ b/ext/protobuf/CMakeLists.txt
@@ -18,5 +18,21 @@
# Date: 07 Dec 2021
# Author: Alexandre Huff
-add_library( rc-api STATIC api.pb.h api.pb.cc api.grpc.pb.h api.grpc.pb.cc )
-target_include_directories( rc-api PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
+# For clarity: this generates object, not a lib as the CM command implies.
+#
+add_library( rc_objects OBJECT
+ api.pb.cc api.grpc.pb.cc
+)
+
+target_include_directories (rc_objects PUBLIC
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+ $<INSTALL_INTERFACE:include>
+)
+
+# header files should go into .../include/ts_xapp/
+if( DEV_PKG )
+ install( FILES
+ api.pb.h api.grpc.pb.h
+ DESTINATION ${install_inc}
+ )
+endif()
diff --git a/src/ts_xapp/CMakeLists.txt b/src/ts_xapp/CMakeLists.txt
index fe4fb2b..47fd406 100644
--- a/src/ts_xapp/CMakeLists.txt
+++ b/src/ts_xapp/CMakeLists.txt
@@ -18,7 +18,17 @@
find_package(Protobuf REQUIRED)
add_executable( ts_xapp ts_xapp.cpp )
-target_link_libraries( ts_xapp ricxfcpp;rmr_si;pthread;curl;rc-api;grpc++;${Protobuf_LIBRARY} )
+target_include_directories( ts_xapp PUBLIC ${srcd}/src ${srcd}/ext )
+target_link_libraries( ts_xapp
+ ricxfcpp
+ rmr_si
+ pthread
+ rc_objects
+ grpc++
+ ${Protobuf_LIBRARY}
+ utils_objects
+ curl
+)
install(
TARGETS ts_xapp
diff --git a/src/ts_xapp/Makefile b/src/ts_xapp/Makefile
deleted file mode 100644
index 750559e..0000000
--- a/src/ts_xapp/Makefile
+++ /dev/null
@@ -1,32 +0,0 @@
-# vim: ts=4 sw=4 noet:
-
-#==================================================================================
-# Copyright (c) 2020 AT&T Intellectual Property.
-#
-# 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.
-#==================================================================================
-
-# simple makefile to build the examples. This assumes that the xapp framework
-# library has been installed or the LD_LIBRARY_PATH and C_INCLUDE_PATH environent
-# variables are set to reference the needed files.
-
-%.o:: %.cpp %.hpp
- g++ -g ${prereq%% *} -c
-
-% :: %.cpp
- g++ $< -g -o $@ -lricxfcpp -lrmr_si -lpthread -lm -lsdl
-
-all:: ts_xapp
-
-install::
- cp ts_xapp /usr/local/bin/
diff --git a/src/ts_xapp/ts_xapp.cpp b/src/ts_xapp/ts_xapp.cpp
index 7b32878..6bb05b7 100644
--- a/src/ts_xapp/ts_xapp.cpp
+++ b/src/ts_xapp/ts_xapp.cpp
@@ -56,10 +56,9 @@
#include <rapidjson/reader.h>
#include <rapidjson/prettywriter.h>
-#include <curl/curl.h>
#include <rmr/RIC_message_types.h>
-#include "ricxfcpp/xapp.hpp"
-#include "ricxfcpp/config.hpp"
+#include <ricxfcpp/xapp.hpp>
+#include <ricxfcpp/config.hpp>
/*
FIXME unfortunately this RMR flag has to be disabled
@@ -73,7 +72,9 @@
#include <grpcpp/client_context.h>
#include <grpcpp/create_channel.h>
#include <grpcpp/security/credentials.h>
-#include "../../ext/protobuf/api.grpc.pb.h"
+#include "protobuf/api.grpc.pb.h"
+
+#include "utils/restclient.hpp"
using namespace rapidjson;
@@ -98,6 +99,16 @@
TsControlApi ts_control_api; // api to send control messages
string ts_control_ep; // api target endpoint
+typedef struct nodeb {
+ string ran_name;
+ struct {
+ string plmn_id;
+ string nb_id;
+ } global_nb_id;
+} nodeb_t;
+
+unordered_map<string, shared_ptr<nodeb_t>> cell_map; // maps each cell to its nodeb
+
/* struct UEData {
string serving_cell;
int serving_cell_rsrp;
@@ -244,6 +255,47 @@
}
};
+struct NodebListHandler : public BaseReaderHandler<UTF8<>, NodebListHandler> {
+ vector<string> nodeb_list;
+ string curr_key = "";
+
+ bool Key(const Ch* str, SizeType length, bool copy) {
+ curr_key = str;
+ return true;
+ }
+
+ bool String(const Ch* str, SizeType length, bool copy) {
+ if( curr_key.compare( "inventoryName" ) == 0 ) {
+ nodeb_list.push_back( str );
+ }
+ return true;
+ }
+};
+
+struct NodebHandler : public BaseReaderHandler<UTF8<>, NodebHandler> {
+ string curr_key = "";
+ shared_ptr<nodeb_t> nodeb = make_shared<nodeb_t>();
+
+ bool Key(const Ch* str, SizeType length, bool copy) {
+ curr_key = str;
+ return true;
+ }
+
+ bool String(const Ch* str, SizeType length, bool copy) {
+ if( curr_key.compare( "ranName" ) == 0 ) {
+ nodeb->ran_name = str;
+ } else if( curr_key.compare( "plmnId" ) == 0 ) {
+ nodeb->global_nb_id.plmn_id = str;
+ } else if( curr_key.compare( "nbId" ) == 0 ) {
+ nodeb->global_nb_id.nb_id = str;
+ } else if( curr_key.compare( "cellId" ) == 0 ) {
+ cell_map[str] = nodeb;
+ }
+ return true;
+ }
+
+};
+
/* struct UEDataHandler : public BaseReaderHandler<UTF8<>, UEDataHandler> {
unordered_map<string, string> cell_pred;
@@ -394,100 +446,116 @@
rsrp_threshold = handler.threshold;
}
- mbuf.Send_response( 101, -1, 5, (unsigned char *) "OK1\n" ); // validate that we can use the same buffer for 2 rts calls
- mbuf.Send_response( 101, -1, 5, (unsigned char *) "OK2\n" );
-}
-
-// callback to handle handover reply (json http response)
-size_t handoff_reply_callback( const char *in, size_t size, size_t num, string *out ) {
- const size_t totalBytes( size * num );
- out->append( in, totalBytes );
- return totalBytes;
}
// sends a handover message through REST
-void send_rest_control_request( string msg ) {
- CURL *curl = curl_easy_init();
- curl_easy_setopt( curl, CURLOPT_URL, ts_control_ep.c_str() );
- curl_easy_setopt( curl, CURLOPT_TIMEOUT, 10 );
- curl_easy_setopt( curl, CURLOPT_POST, 1L );
- // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
+void send_rest_control_request( string ue_id, string serving_cell_id, string target_cell_id ) {
+ time_t now;
+ string str_now;
+ static unsigned int seq_number = 0; // static counter, not thread-safe
- // response information
- long httpCode( 0 );
- unique_ptr<string> httpData( new string() );
+ // building a handoff control message
+ now = time( nullptr );
+ str_now = ctime( &now );
+ str_now.pop_back(); // removing the \n character
- curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, handoff_reply_callback );
- curl_easy_setopt( curl, CURLOPT_WRITEDATA, httpData.get());
- curl_easy_setopt( curl, CURLOPT_POSTFIELDS, msg.c_str() );
+ seq_number++; // static counter, not thread-safe
- struct curl_slist *headers = NULL; // needs to free this after easy perform
- headers = curl_slist_append( headers, "Accept: application/json" );
- headers = curl_slist_append( headers, "Content-Type: application/json" );
- curl_easy_setopt( curl, CURLOPT_HTTPHEADER, headers );
+ rapidjson::StringBuffer s;
+ rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(s);
+ writer.StartObject();
+ writer.Key( "command" );
+ writer.String( "HandOff" );
+ writer.Key( "seqNo" );
+ writer.Int( seq_number );
+ writer.Key( "ue" );
+ writer.String( ue_id.c_str() );
+ writer.Key( "fromCell" );
+ writer.String( serving_cell_id.c_str() );
+ writer.Key( "toCell" );
+ writer.String( target_cell_id.c_str() );
+ writer.Key( "timestamp" );
+ writer.String( str_now.c_str() );
+ writer.Key( "reason" );
+ writer.String( "HandOff Control Request from TS xApp" );
+ writer.Key( "ttl" );
+ writer.Int( 10 );
+ writer.EndObject();
+ // creates a message like
+ /* {
+ "command": "HandOff",
+ "seqNo": 1,
+ "ue": "ueid-here",
+ "fromCell": "CID1",
+ "toCell": "CID3",
+ "timestamp": "Sat May 22 10:35:33 2021",
+ "reason": "HandOff Control Request from TS xApp",
+ "ttl": 10
+ } */
+
+ string msg = s.GetString();
cout << "[INFO] Sending a HandOff CONTROL message to \"" << ts_control_ep << "\"\n";
cout << "[INFO] HandOff request is " << msg << endl;
// sending request
- CURLcode res = curl_easy_perform( curl );
- if( res != CURLE_OK ) {
- cout << "[ERROR] curl_easy_perform() failed: " << curl_easy_strerror( res ) << endl;
+ restclient::RestClient client( ts_control_ep );
+ restclient::response_t resp = client.do_post( "", msg ); // we already have the full path in ts_control_ep
- } else {
-
- curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
- if( httpCode == 200 ) {
+ if( resp.status_code == 200 ) {
// ============== DO SOMETHING USEFUL HERE ===============
// Currently, we only print out the HandOff reply
rapidjson::Document document;
- document.Parse( httpData.get()->c_str() );
+ document.Parse( resp.body.c_str() );
rapidjson::StringBuffer s;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(s);
document.Accept( writer );
cout << "[INFO] HandOff reply is " << s.GetString() << endl;
-
- } else if ( httpCode == 404 ) {
- cout << "[ERROR] HTTP 404 Not Found: " << ts_control_ep << endl;
- } else {
- cout << "[ERROR] Unexpected HTTP code " << httpCode << " from " << ts_control_ep << \
- "\n[ERROR] HTTP payload is " << httpData.get()->c_str() << endl;
- }
-
+ } else {
+ cout << "[ERROR] Unexpected HTTP code " << resp.status_code << " from " << \
+ client.getBaseUrl() << \
+ "\n[ERROR] HTTP payload is " << resp.body.c_str() << endl;
}
- curl_slist_free_all( headers );
- curl_easy_cleanup( curl );
}
// sends a handover message to RC xApp through gRPC
-void send_grpc_control_request() {
+void send_grpc_control_request( string ue_id, string target_cell_id ) {
grpc::ClientContext context;
- api::RicControlGrpcReq *request = api::RicControlGrpcReq().New();
+
api::RicControlGrpcRsp response;
+ shared_ptr<api::RicControlGrpcReq> request = make_shared<api::RicControlGrpcReq>();
- api::RICE2APHeader *apHeader = api::RICE2APHeader().New();
- api::RICControlHeader *ctrlHeader = api::RICControlHeader().New();
- api::RICControlMessage *ctrlMsg = api::RICControlMessage().New();
+ api::RICE2APHeader *apHeader = request->mutable_rice2apheaderdata();
+ apHeader->set_ranfuncid( 300 );
+ apHeader->set_ricrequestorid( 1001 );
- request->set_e2nodeid("e2nodeid");
- request->set_plmnid("plmnid");
- request->set_ranname("ranname");
- request->set_allocated_rice2apheaderdata(apHeader);
- request->set_allocated_riccontrolheaderdata(ctrlHeader);
- request->set_allocated_riccontrolmessagedata(ctrlMsg);
- request->set_riccontrolackreqval(api::RIC_CONTROL_ACK_UNKWON); // not yet used in api.proto
+ api::RICControlHeader *ctrlHeader = request->mutable_riccontrolheaderdata();
+ ctrlHeader->set_controlstyle( 3 );
+ ctrlHeader->set_controlactionid( 1 );
+ ctrlHeader->set_ueid( ue_id );
- grpc::Status status = rc_stub->SendRICControlReqServiceGrpc(&context, *request, &response);
+ api::RICControlMessage *ctrlMsg = request->mutable_riccontrolmessagedata();
+ ctrlMsg->set_riccontrolcelltypeval( api::RIC_CONTROL_CELL_UNKWON );
+ ctrlMsg->set_targetcellid( target_cell_id );
- if(status.ok()) {
- /*
- TODO check if this is related to RICControlAckEnum
- if yes, then ACK value should be 2 (RIC_CONTROL_ACK)
- api.proto assumes that 0 is an ACK
- */
- if(response.rspcode() == 0) {
+ auto data = cell_map.find( target_cell_id );
+ if( data != cell_map.end() ) {
+ request->set_e2nodeid( data->second->global_nb_id.nb_id );
+ request->set_plmnid( data->second->global_nb_id.plmn_id );
+ request->set_ranname( data->second->ran_name );
+ } else {
+ request->set_e2nodeid( "unknown_e2nodeid" );
+ request->set_plmnid( "unknown_plmnid" );
+ request->set_ranname( "unknown_ranname" );
+ }
+ request->set_riccontrolackreqval( api::RIC_CONTROL_ACK_UNKWON ); // not yet used in api.proto
+
+ grpc::Status status = rc_stub->SendRICControlReqServiceGrpc( &context, *request, &response );
+
+ if( status.ok() ) {
+ if( response.rspcode() == 0 ) {
cout << "[INFO] Control Request succeeded with code=0, description=" << response.description() << endl;
} else {
cout << "[ERROR] Control Request failed with code=" << response.rspcode()
@@ -499,21 +567,9 @@
<< status.error_code() << ", error_msg=" << status.error_message() << endl;
}
- // FIXME needs to check about memory likeage
}
void prediction_callback( Message& mbuf, int mtype, int subid, int len, Msg_component payload, void* data ) {
-
- time_t now;
- string str_now;
- static unsigned int seq_number = 0; // static counter, not thread-safe
-
- int response_to = 0; // max timeout wating for a response
-
- int send_mtype = 0;
- int rmtype; // received message type
- int delay = 1000000; // mu-sec delay; default 1s
-
string json ((char *)payload.get(), len); // RMR payload might not have a nil terminanted char
cout << "[INFO] Prediction Callback got a message, type=" << mtype << ", length=" << len << "\n";
@@ -559,64 +615,23 @@
}
if ( highest_throughput > serving_cell_throughput ) {
- // building a handoff control message
- now = time( nullptr );
- str_now = ctime( &now );
- str_now.pop_back(); // removing the \n character
-
- seq_number++; // static counter, not thread-safe
-
- rapidjson::StringBuffer s;
- rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(s);
- writer.StartObject();
- writer.Key( "command" );
- writer.String( "HandOff" );
- writer.Key( "seqNo" );
- writer.Int( seq_number );
- writer.Key( "ue" );
- writer.String( handler.ue_id.c_str() );
- writer.Key( "fromCell" );
- writer.String( handler.serving_cell_id.c_str() );
- writer.Key( "toCell" );
- writer.String( highest_throughput_cell_id.c_str() );
- writer.Key( "timestamp" );
- writer.String( str_now.c_str() );
- writer.Key( "reason" );
- writer.String( "HandOff Control Request from TS xApp" );
- writer.Key( "ttl" );
- writer.Int( 10 );
- writer.EndObject();
- // creates a message like
- /* {
- "command": "HandOff",
- "seqNo": 1,
- "ue": "ueid-here",
- "fromCell": "CID1",
- "toCell": "CID3",
- "timestamp": "Sat May 22 10:35:33 2021",
- "reason": "HandOff Control Request from TS xApp",
- "ttl": 10
- } */
// sending a control request message
if ( ts_control_api == TsControlApi::REST ) {
- send_rest_control_request( s.GetString() );
+ send_rest_control_request( handler.ue_id, handler.serving_cell_id, highest_throughput_cell_id );
} else {
- send_grpc_control_request();
+ send_grpc_control_request( handler.ue_id, highest_throughput_cell_id );
}
} else {
cout << "[INFO] The current serving cell \"" << handler.serving_cell_id << "\" is the best one" << endl;
}
- // mbuf.Send_response( 101, -1, 5, (unsigned char *) "OK1\n" ); // validate that we can use the same buffer for 2 rts calls
- // mbuf.Send_response( 101, -1, 5, (unsigned char *) "OK2\n" );
}
void send_prediction_request( vector<string> ues_to_predict ) {
-
std::unique_ptr<Message> msg;
- Msg_component payload; // special type of unique pointer to the payload
+ Msg_component payload; // special type of unique pointer to the payload
int sz;
int i;
@@ -661,7 +676,7 @@
* It parses the payload received from AD xApp, sends an ACK with same UEID as payload to AD xApp, and
* sends a prediction request to the QP Driver xApp.
*/
-void ad_callback( Message& mbuf, int mtype, int subid, int len, Msg_component payload, void* data ) {
+void ad_callback( Message& mbuf, int mtype, int subid, int len, Msg_component payload, void* data ) {
string json ((char *)payload.get(), len); // RMR payload might not have a nil terminanted char
cout << "[INFO] AD Callback got a message, type=" << mtype << ", length=" << len << "\n";
@@ -675,13 +690,75 @@
// just sending ACK to the AD xApp
mbuf.Send_response( TS_ANOMALY_ACK, Message::NO_SUBID, len, nullptr ); // msg type 30004
- // TODO should we use the threshold received in the A1_POLICY_REQ message and compare with Degradation in TS_ANOMALY_UPDATE?
- // if( handler.degradation < rsrp_threshold )
send_prediction_request(handler.prediction_ues);
}
-extern int main( int argc, char** argv ) {
+vector<string> get_nodeb_list( restclient::RestClient& client ) {
+ restclient::response_t response = client.do_get( "/v1/nodeb/states" );
+
+ NodebListHandler handler;
+ if( response.status_code == 200 ) {
+ Reader reader;
+ StringStream ss( response.body.c_str() );
+ reader.Parse( ss, handler );
+
+ cout << "[INFO] nodeb list is " << response.body.c_str() << endl;
+
+ } else {
+ if( response.body.empty() ) {
+ cout << "[ERROR] Unexpected HTTP code " << response.status_code << " from " << client.getBaseUrl() << endl;
+ } else {
+ cout << "[ERROR] Unexpected HTTP code " << response.status_code << " from " << client.getBaseUrl() <<
+ ". HTTP payload is " << response.body.c_str() << endl;
+ }
+ }
+
+ return handler.nodeb_list;
+}
+
+bool build_cell_mapping() {
+ string base_url;
+ char *data = getenv( "SERVICE_E2MGR_HTTP_BASE_URL" );
+ if ( data == NULL ) {
+ base_url = "http://service-ricplt-e2mgr-http.ricplt:3800";
+ } else {
+ base_url = string( data );
+ }
+
+ restclient::RestClient client( base_url );
+
+ vector<string> nb_list = get_nodeb_list( client );
+
+ for( string nb : nb_list ) {
+ string full_path = string("/v1/nodeb/") + nb;
+ restclient::response_t response = client.do_get( full_path );
+ if( response.status_code != 200 ) {
+ if( response.body.empty() ) {
+ cout << "[ERROR] Unexpected HTTP code " << response.status_code << " from " << \
+ client.getBaseUrl() + full_path << endl;
+ } else {
+ cout << "[ERROR] Unexpected HTTP code " << response.status_code << " from " << \
+ client.getBaseUrl() + full_path << ". HTTP payload is " << response.body.c_str() << endl;
+ }
+ return false;
+ }
+
+ try {
+ NodebHandler handler;
+ Reader reader;
+ StringStream ss( response.body.c_str() );
+ reader.Parse( ss, handler );
+ } catch (...) {
+ cout << "[ERROR] Got an exception on parsing nodeb (stringstream read parse)\n";
+ return false;
+ }
+ }
+
+ return true;
+}
+
+extern int main( int argc, char** argv ) {
int nthreads = 1;
char* port = (char *) "4560";
shared_ptr<grpc::Channel> channel;
@@ -697,10 +774,14 @@
ts_control_api = TsControlApi::REST;
} else {
ts_control_api = TsControlApi::gRPC;
- }
- channel = grpc::CreateChannel(ts_control_ep, grpc::InsecureChannelCredentials());
- rc_stub = api::MsgComm::NewStub(channel, grpc::StubOptions());
+ if( !build_cell_mapping() ) {
+ cout << "[ERROR] unable to map cells to nodeb\n";
+ }
+
+ channel = grpc::CreateChannel(ts_control_ep, grpc::InsecureChannelCredentials());
+ rc_stub = api::MsgComm::NewStub(channel, grpc::StubOptions());
+ }
fprintf( stderr, "[TS xApp] listening on port %s\n", port );
xfw = std::unique_ptr<Xapp>( new Xapp( port, true ) );
diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt
new file mode 100644
index 0000000..6b2c778
--- /dev/null
+++ b/src/utils/CMakeLists.txt
@@ -0,0 +1,39 @@
+# vim: sw=4 ts=4 noet:
+#
+#==================================================================================
+# Copyright (c) 2022 Nokia
+# Copyright (c) 2022 AT&T Intellectual Property.
+#
+# 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 clarity: this generates object, not a lib as the CM command implies.
+#
+add_library( utils_objects OBJECT
+ restclient.cpp
+)
+
+target_include_directories (utils_objects PUBLIC
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+ $<INSTALL_INTERFACE:include>
+)
+
+# header files should go into .../include/ts_xapp/
+if( DEV_PKG )
+ install( FILES
+ restclient.hpp
+ DESTINATION ${install_inc}
+ )
+endif()
diff --git a/src/utils/restclient.cpp b/src/utils/restclient.cpp
new file mode 100644
index 0000000..afdb356
--- /dev/null
+++ b/src/utils/restclient.cpp
@@ -0,0 +1,187 @@
+// vi: ts=4 sw=4 noet:
+/*
+==================================================================================
+ Copyright (c) 2022 Alexandre Huff
+
+ 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.
+==================================================================================
+*/
+
+/*
+ Mnemonic: restclient.cpp
+ Abstract: Implements a tiny wrapper on top of libcurl to handle with rest requests.
+
+ Date: 8 Apr 2022
+ Author: Alexandre Huff
+*/
+
+
+#include "restclient.hpp"
+#include <mutex>
+#include <string.h>
+#include <memory>
+
+namespace restclient {
+
+// callback to handle http responses
+static size_t http_response_callback( const char *in, size_t size, size_t num, std::string *out ) {
+ if( out == NULL ) {
+ return 0;
+ }
+ const size_t totalBytes( size * num );
+ out->append( in, totalBytes );
+ return totalBytes;
+}
+
+/*
+ Create a RestClient instance to exchange messages with
+ a given rest api available on baseUrl, which consists of
+ scheme://domain[:port]
+*/
+RestClient::RestClient( std::string baseUrl ) {
+ this->baseUrl = baseUrl;
+
+ if( ! init() ) {
+ fprintf( stderr, "unable to initialize RestClient\n" );
+ }
+}
+
+RestClient::~RestClient( ) {
+ curl_slist_free_all( headers );
+ curl_easy_cleanup( curl );
+}
+
+std::string RestClient::getBaseUrl( ) {
+ return baseUrl;
+}
+
+bool RestClient::init( ) {
+ static std::mutex curl_mutex;
+
+ { // scoped mutex to make curl_global_init thread-safe
+ const std::lock_guard<std::mutex> lock( curl_mutex );
+ curl_global_init( CURL_GLOBAL_DEFAULT );
+ }
+
+ curl = curl_easy_init();
+ if( curl == NULL ) {
+ return false;
+ }
+
+ // curl_easy_setopt( curl, CURLOPT_VERBOSE, 1L );
+ curl_easy_setopt( curl, CURLOPT_TIMEOUT, 5 );
+
+ /* provide a buffer to store errors in */
+ if( curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf) != CURLE_OK ) {
+ fprintf( stderr, "unable to set CURLOPT_ERRORBUFFER\n" );
+ return false;
+ }
+ errbuf[0] = 0;
+
+ if( curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, http_response_callback ) != CURLE_OK ) {
+ fprintf( stderr, "unable to set CURLOPT_WRITEFUNCTION\n" );
+ return false;
+ }
+
+ if( curl_easy_setopt( curl, CURLOPT_WRITEDATA, &response.body ) != CURLE_OK ) {
+ fprintf( stderr, "unable to set CURLOPT_WRITEDATA\n" );
+ return false;
+ }
+
+ headers = curl_slist_append( headers, "Accept: application/json" );
+ headers = curl_slist_append( headers, "Content-Type: application/json" );
+ if( curl_easy_setopt( curl, CURLOPT_HTTPHEADER, headers ) != CURLE_OK ) {
+ fprintf( stderr, "unable to set CURLOPT_HTTPHEADER\n" );
+ return false;
+ }
+
+ return true;
+}
+
+/*
+ Executes a GET request at the path of this RestClient instance.
+ Returns the HTTP status code and the correspoding message body.
+*/
+response_t RestClient::do_get( std::string path ) {
+ response = { 0, "" };
+
+ const std::string endpoint = baseUrl + path;
+
+ if( curl_easy_setopt( curl, CURLOPT_URL, endpoint.c_str() ) != CURLE_OK ) {
+ fprintf( stderr, "unable to set CURLOPT_URL\n" );
+ return response;
+ }
+ if( curl_easy_setopt( curl, CURLOPT_HTTPGET, 1L ) != CURLE_OK ) {
+ fprintf( stderr, "unable to set CURLOPT_HTTPGET\n" );
+ return response;
+ }
+
+ CURLcode res = curl_easy_perform( curl );
+ if( res == CURLE_OK ) {
+ if( curl_easy_getinfo( curl, CURLINFO_RESPONSE_CODE, &response.status_code ) != CURLE_OK ) {
+ fprintf( stderr, "unable to get CURLINFO_RESPONSE_CODE\n" );
+ }
+ } else {
+ size_t len = strlen( errbuf );
+ fprintf( stderr, "unable to complete the request url=%s ", endpoint.c_str() );
+ if(len)
+ fprintf( stderr, "error=%s%s", errbuf,
+ ( ( errbuf[len - 1] != '\n' ) ? "\n" : "") );
+ else
+ fprintf( stderr, "error=%s\n", curl_easy_strerror( res ) );
+ }
+
+ return response;
+}
+
+/*
+ Executes a POST request of a json message at the path of this RestClient instance.
+ Returns the HTTP status code and the correspoding message body.
+*/
+response_t RestClient::do_post( std::string path, std::string json ) {
+ response = { 0, "" };
+
+ const std::string endpoint = baseUrl + path;
+
+ if( curl_easy_setopt( curl, CURLOPT_URL, endpoint.c_str() ) != CURLE_OK ) {
+ fprintf( stderr, "unable to set CURLOPT_URL\n" );
+ return response;
+ }
+ if( curl_easy_setopt( curl, CURLOPT_POST, 1L ) != CURLE_OK ) {
+ fprintf( stderr, "unable to set CURLOPT_POST\n" );
+ return response;
+ }
+ if( curl_easy_setopt( curl, CURLOPT_POSTFIELDS, json.c_str() ) != CURLE_OK ) {
+ fprintf( stderr, "unable to set CURLOPT_POSTFIELDS\n" );
+ return response;
+ }
+
+ CURLcode res = curl_easy_perform( curl );
+ if( res == CURLE_OK ) {
+ if( curl_easy_getinfo( curl, CURLINFO_RESPONSE_CODE, &response.status_code ) != CURLE_OK ) {
+ fprintf( stderr, "unable to get CURLINFO_RESPONSE_CODE\n" );
+ }
+ } else {
+ size_t len = strlen( errbuf );
+ fprintf( stderr, "unable to complete the request url=%s ", endpoint.c_str() );
+ if(len)
+ fprintf( stderr, "error=%s%s", errbuf,
+ ( (errbuf[len - 1] != '\n' ) ? "\n" : "") );
+ else
+ fprintf( stderr, "error=%s\n", curl_easy_strerror( res ) );
+ }
+
+ return response;
+}
+
+} // namespace
diff --git a/src/utils/restclient.hpp b/src/utils/restclient.hpp
new file mode 100644
index 0000000..b2d64ed
--- /dev/null
+++ b/src/utils/restclient.hpp
@@ -0,0 +1,64 @@
+// vi: ts=4 sw=4 noet:
+/*
+==================================================================================
+ Copyright (c) 2022 Alexandre Huff
+
+ 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.
+==================================================================================
+*/
+
+/*
+ Mnemonic: restclient.hpp
+ Abstract: Header for the libcurl wrapper.
+
+ Date: 8 Apr 2022
+ Author: Alexandre Huff
+*/
+
+
+
+#ifndef _CURL_WRAPPER_HPP
+#define _CURL_WRAPPER_HPP
+
+#include <curl/curl.h>
+#include <string>
+
+namespace restclient {
+
+typedef struct resp {
+ long status_code;
+ std::string body;
+} response_t;
+
+class RestClient {
+ private:
+ std::string baseUrl;
+ response_t response;
+ CURL *curl = NULL;
+ struct curl_slist *headers = NULL;
+ char errbuf[CURL_ERROR_SIZE];
+
+ bool init( );
+
+ public:
+ RestClient( std::string baseUrl );
+ ~RestClient();
+ std::string getBaseUrl();
+ response_t do_get( std::string path );
+ response_t do_post( std::string path, std::string json );
+};
+
+} // namespace
+
+
+#endif
diff --git a/test/app/CMakeLists.txt b/test/app/CMakeLists.txt
index 5d3eac2..24b797f 100644
--- a/test/app/CMakeLists.txt
+++ b/test/app/CMakeLists.txt
@@ -55,7 +55,7 @@
)
target_link_libraries(
rc_xapp
- rc-api
+ rc_objects
grpc++
${Protobuf_LIBRARY}
)
diff --git a/test/app/README b/test/app/README
index cbf8477..f77d794 100644
--- a/test/app/README
+++ b/test/app/README
@@ -10,15 +10,19 @@
RMR port 4570.
qp_xapp.cpp
- Simulates both, the QoE Prediction (QP), and the QP Driver xApps.
+ Simulates the QoE Prediction (QP) xApp.
Basically, this program receives Prediction Requests from TS xApp,
computes random throughput values (predictions) for neighbor cells,
and sends that Throughput Prediction to the TS xApp. All steps are
logged in the console. Uses RMR port 4580.
echo-server.py
- Implements a dummy echo server just for testing REST calls from
- TS xApp.
+ Implements a echo server for testing REST calls from TS xApp.
+
+rc_xapp.cpp
+ Simulates the RC xApp. It receives CONTROL messages from TS xApp,
+ and outputs the string representation of the message in the console.
+ Replies TS with an ACK message. Uses gRPC port 50051.
routes.rt
Contains a few RMR routing policies to allow AD, QP, and TS xApps
diff --git a/test/app/rc_xapp.cpp b/test/app/rc_xapp.cpp
index cdf46a0..2ebfba0 100644
--- a/test/app/rc_xapp.cpp
+++ b/test/app/rc_xapp.cpp
@@ -41,7 +41,9 @@
class ControlServiceImpl : public api::MsgComm::Service {
::grpc::Status SendRICControlReqServiceGrpc(::grpc::ServerContext* context, const ::api::RicControlGrpcReq* request,
::api::RicControlGrpcRsp* response) override {
- cout << "[RC] Received a new gRPC message\n";
+
+ cout << "[RC] gRPC message received\n==============================\n"
+ << request->DebugString() << "==============================\n";
/*
TODO check if this is related to RICControlAckEnum
diff --git a/xapp-descriptor/config.json b/xapp-descriptor/config-file.json
similarity index 96%
rename from xapp-descriptor/config.json
rename to xapp-descriptor/config-file.json
index e2eba42..e38cecf 100644
--- a/xapp-descriptor/config.json
+++ b/xapp-descriptor/config-file.json
@@ -1,13 +1,13 @@
{
"xapp_name": "trafficxapp",
- "version": "1.2.1",
+ "version": "1.2.2",
"containers": [
{
"name": "trafficxapp",
"image": {
"registry": "nexus3.o-ran-sc.org:10002",
"name": "o-ran-sc/ric-app-ts",
- "tag": "1.2.1"
+ "tag": "1.2.2"
}
}
],