From 11a46a4ddf9a5ee409a3ca7686f458ae7de9dadb Mon Sep 17 00:00:00 2001 From: Alexandre Huff Date: Tue, 26 Apr 2022 09:42:11 -0300 Subject: [PATCH] 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 Change-Id: I45acb87ad2391199658d41457919279e46c97fdf --- .gitignore | 1 + CMakeLists.txt | 14 +- Dockerfile | 28 +- container-tag.yaml | 2 +- docs/user-guide.rst | 53 +++- ext/protobuf/CMakeLists.txt | 20 +- src/ts_xapp/CMakeLists.txt | 12 +- src/ts_xapp/ts_xapp.cpp | 353 +++++++++++++--------- src/{ts_xapp/Makefile => utils/CMakeLists.txt} | 35 ++- src/utils/restclient.cpp | 187 ++++++++++++ src/utils/restclient.hpp | 64 ++++ test/app/CMakeLists.txt | 2 +- test/app/README | 10 +- test/app/rc_xapp.cpp | 4 +- xapp-descriptor/{config.json => config-file.json} | 4 +- 15 files changed, 601 insertions(+), 188 deletions(-) rename src/{ts_xapp/Makefile => utils/CMakeLists.txt} (59%) create mode 100644 src/utils/restclient.cpp create mode 100644 src/utils/restclient.hpp rename xapp-descriptor/{config.json => config-file.json} (96%) 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 @@ cmake_minimum_required( VERSION 3.14 ) 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 @@ add_definitions( -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 @@ unset( GPROF CACHE ) # ensure this does not persist # 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 @@ WORKDIR /playpen # 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 @@ RUN wget -nv --content-disposition ${PC_REL_URL}/ricxfcpp-dev_${XFCPP_VER}_amd64 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 @@ RUN git clone https://github.com/Tencent/rapidjson && \ 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 @@ FROM ubuntu:20.04 # # 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 @@ The current Use Case is comprised of five xApps: * 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 @@ An example Policy follows: { "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 @@ The following is an example message body: 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 @@ The following is an example message body: { "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 @@ This message provides predictions for UE ID "Train passenger 2". For its servic 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 @@ If predicted throughput is higher in a neighbor cell, Traffic Steering sends a C "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 + $ + $ +) + +# 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/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 #include -#include #include -#include "ricxfcpp/xapp.hpp" -#include "ricxfcpp/config.hpp" +#include +#include /* FIXME unfortunately this RMR flag has to be disabled @@ -73,7 +72,9 @@ #include #include #include -#include "../../ext/protobuf/api.grpc.pb.h" +#include "protobuf/api.grpc.pb.h" + +#include "utils/restclient.hpp" using namespace rapidjson; @@ -98,6 +99,16 @@ enum class TsControlApi { REST, gRPC }; 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> cell_map; // maps each cell to its nodeb + /* struct UEData { string serving_cell; int serving_cell_rsrp; @@ -244,6 +255,47 @@ struct AnomalyHandler : public BaseReaderHandler, AnomalyHandler> { } }; +struct NodebListHandler : public BaseReaderHandler, NodebListHandler> { + vector 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, NodebHandler> { + string curr_key = ""; + shared_ptr nodeb = make_shared(); + + 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, UEDataHandler> { unordered_map cell_pred; @@ -394,100 +446,116 @@ void policy_callback( Message& mbuf, int mtype, int subid, int len, Msg_componen 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); - - // response information - long httpCode( 0 ); - unique_ptr httpData( new string() ); - - 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() ); - - 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 ); +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 + + // 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 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; - - } else { + restclient::RestClient client( ts_control_ep ); + restclient::response_t resp = client.do_post( "", msg ); // we already have the full path in ts_control_ep - 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 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 request = make_shared(); + + api::RICE2APHeader *apHeader = request->mutable_rice2apheaderdata(); + apHeader->set_ranfuncid( 300 ); + apHeader->set_ricrequestorid( 1001 ); + + api::RICControlHeader *ctrlHeader = request->mutable_riccontrolheaderdata(); + ctrlHeader->set_controlstyle( 3 ); + ctrlHeader->set_controlactionid( 1 ); + ctrlHeader->set_ueid( ue_id ); + + api::RICControlMessage *ctrlMsg = request->mutable_riccontrolmessagedata(); + ctrlMsg->set_riccontrolcelltypeval( api::RIC_CONTROL_CELL_UNKWON ); + ctrlMsg->set_targetcellid( target_cell_id ); + + 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 ); - api::RICE2APHeader *apHeader = api::RICE2APHeader().New(); - api::RICControlHeader *ctrlHeader = api::RICControlHeader().New(); - api::RICControlMessage *ctrlMsg = api::RICControlMessage().New(); - - 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 - - grpc::Status status = rc_stub->SendRICControlReqServiceGrpc(&context, *request, &response); - - 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) { + 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 @@ void send_grpc_control_request() { << 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 @@ void prediction_callback( Message& mbuf, int mtype, int subid, int len, Msg_comp } 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 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 ues_to_predict ) { - std::unique_ptr 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 @@ void send_prediction_request( vector ues_to_predict ) { * 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 @@ void ad_callback( Message& mbuf, int mtype, int subid, int len, Msg_component pa // 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 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 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 channel; @@ -697,10 +774,14 @@ extern int main( int argc, char** argv ) { 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( new Xapp( port, true ) ); diff --git a/src/ts_xapp/Makefile b/src/utils/CMakeLists.txt similarity index 59% rename from src/ts_xapp/Makefile rename to src/utils/CMakeLists.txt index 750559e..6b2c778 100644 --- a/src/ts_xapp/Makefile +++ b/src/utils/CMakeLists.txt @@ -1,7 +1,8 @@ -# vim: ts=4 sw=4 noet: - +# vim: sw=4 ts=4 noet: +# #================================================================================== -# Copyright (c) 2020 AT&T Intellectual Property. +# 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. @@ -15,18 +16,24 @@ # 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 +# For clarity: this generates object, not a lib as the CM command implies. +# +add_library( utils_objects OBJECT + restclient.cpp +) -all:: ts_xapp +target_include_directories (utils_objects PUBLIC + $ + $ +) -install:: - cp ts_xapp /usr/local/bin/ +# 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 +#include +#include + +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 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 +#include + +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 @@ add_executable( ) 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 @@ ad_xapp.cpp 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 @@ using namespace std; 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" } } ], -- 2.16.6