From a0986356b972c8e4a8c466bc766d33bd4aae0671 Mon Sep 17 00:00:00 2001 From: dan-du-car <62157949+dan-du-car@users.noreply.github.com> Date: Tue, 14 Nov 2023 15:33:44 -0500 Subject: [PATCH] Rsu health monitor (#564) # PR Details ## Description RSU health monitor plugin is a V2xHub plugin that interface with its connected RSU directly via SNMP protocol. The plugin is responsible for monitoring the connected RSU status by constantly ping RSU device. For detailed design of this plugin, refer to https://usdot-carma.atlassian.net/wiki/spaces/WFD2/pages/2640740360/RSU+Health+Monitor+Plugin+Design . ## Related Issue NA ## Motivation and Context Data visualization ## How Has This Been Tested? Integration test ## Types of changes - [ ] Defect fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that cause existing functionality to change) ## Checklist: - [ ] I have added any new packages to the sonar-scanner.properties file - [ ] My change requires a change to the documentation. - [ ] I have updated the documentation accordingly. - [x] I have read the **CONTRIBUTING** document. [V2XHUB Contributing Guide](https://github.com/usdot-fhwa-OPS/V2X-Hub/blob/develop/Contributing.md) - [ ] I have added tests to cover my changes. - [ ] All new and existing tests passed. --- .sonarqube/sonar-scanner.properties | 8 +- src/tmx/Messages/include/MessageTypes.h | 1 + src/tmx/Messages/include/RSUStatusMessage.h | 25 ++ src/tmx/TmxUtils/CMakeLists.txt | 12 +- src/tmx/TmxUtils/src/RSU_MIB_4_1.h | 56 +++++ src/tmx/TmxUtils/src/SNMPClient.cpp | 227 ++++++++++++++++++ src/tmx/TmxUtils/src/SNMPClient.h | 113 +++++++++ src/tmx/TmxUtils/src/SNMPClientException.cpp | 8 + src/tmx/TmxUtils/src/SNMPClientException.h | 23 ++ src/tmx/TmxUtils/test/MockSNMPClient.h | 18 ++ src/tmx/TmxUtils/test/test_SNMPClient.cpp | 93 +++++++ .../RSUHealthMonitorPlugin/CMakeLists.txt | 26 ++ .../RSUHealthMonitorPlugin/manifest.json | 51 ++++ .../src/RSUHealthMonitorPlugin.cpp | 90 +++++++ .../src/RSUHealthMonitorPlugin.h | 46 ++++ .../src/RSUHealthMonitorWorker.cpp | 217 +++++++++++++++++ .../src/RSUHealthMonitorWorker.h | 120 +++++++++ .../RSUHealthMonitorPlugin/test/main.cpp | 8 + .../test/test_RSUHealthMonitorWorker.cpp | 108 +++++++++ 19 files changed, 1246 insertions(+), 4 deletions(-) create mode 100644 src/tmx/Messages/include/RSUStatusMessage.h create mode 100644 src/tmx/TmxUtils/src/RSU_MIB_4_1.h create mode 100644 src/tmx/TmxUtils/src/SNMPClient.cpp create mode 100644 src/tmx/TmxUtils/src/SNMPClient.h create mode 100644 src/tmx/TmxUtils/src/SNMPClientException.cpp create mode 100644 src/tmx/TmxUtils/src/SNMPClientException.h create mode 100644 src/tmx/TmxUtils/test/MockSNMPClient.h create mode 100644 src/tmx/TmxUtils/test/test_SNMPClient.cpp create mode 100755 src/v2i-hub/RSUHealthMonitorPlugin/CMakeLists.txt create mode 100755 src/v2i-hub/RSUHealthMonitorPlugin/manifest.json create mode 100755 src/v2i-hub/RSUHealthMonitorPlugin/src/RSUHealthMonitorPlugin.cpp create mode 100755 src/v2i-hub/RSUHealthMonitorPlugin/src/RSUHealthMonitorPlugin.h create mode 100644 src/v2i-hub/RSUHealthMonitorPlugin/src/RSUHealthMonitorWorker.cpp create mode 100644 src/v2i-hub/RSUHealthMonitorPlugin/src/RSUHealthMonitorWorker.h create mode 100644 src/v2i-hub/RSUHealthMonitorPlugin/test/main.cpp create mode 100644 src/v2i-hub/RSUHealthMonitorPlugin/test/test_RSUHealthMonitorWorker.cpp diff --git a/.sonarqube/sonar-scanner.properties b/.sonarqube/sonar-scanner.properties index f6b40e299..d5f118b33 100644 --- a/.sonarqube/sonar-scanner.properties +++ b/.sonarqube/sonar-scanner.properties @@ -55,7 +55,8 @@ sonar.modules= PedestrianPlugin, \ MessageReceiverPlugin, \ CARMAStreetsPlugin, \ ERVCloudForwardingPlugin, \ - CDASimAdapter + CDASimAdapter, \ + RSUHealthMonitorPlugin @@ -82,6 +83,7 @@ TimPlugin.sonar.projectBaseDir =src/v2i-hub/TimPlugin CARMAStreetsPlugin.sonar.projectBaseDir =src/v2i-hub/CARMAStreetsPlugin ERVCloudForwardingPlugin.sonar.projectBaseDir =src/v2i-hub/ERVCloudForwardingPlugin CDASimAdapter.sonar.projectBaseDir =src/v2i-hub/CDASimAdapter +RSUHealthMonitorPlugin.sonar.projectBaseDir =src/v2i-hub/RSUHealthMonitorPlugin @@ -117,7 +119,8 @@ CARMAStreetsPlugin.sonar.exclusions =test/** ERVCloudForwardingPlugin.sonar.sources =src CDASimAdapter.sonar.sources =src CDASimAdapter.sonar.exclusions =test/** - +RSUHealthMonitorPlugin.sonar.sources =src +RSUHealthMonitorPlugin.sonar.exclusions =test/** # Tests # Note: For C++ setting this field does not cause test analysis to occur. It only allows the test source code to be evaluated. @@ -145,3 +148,4 @@ SpatPlugin.sonar.tests=test CARMAStreetsPlugin.sonar.tests=test ERVCloudForwardingPlugin.sonar.tests=test CDASimAdapter.sonar.tests=test +RSUHealthMonitorPlugin.sonar.tests=test diff --git a/src/tmx/Messages/include/MessageTypes.h b/src/tmx/Messages/include/MessageTypes.h index e0fd09c70..3360788fe 100644 --- a/src/tmx/Messages/include/MessageTypes.h +++ b/src/tmx/Messages/include/MessageTypes.h @@ -92,6 +92,7 @@ static CONSTEXPR const char *MSGSUBTYPE_OUTGOING_STRING = "Outgoing"; static CONSTEXPR const char *MSGSUBTYPE_SHUTDOWN_STRING = "Shutdown"; static CONSTEXPR const char *MSGSUBTYPE_TIMESYNC_STRING = "TimeSync"; static CONSTEXPR const char *MSGSUBTYPE_SENSOR_DETECTED_OBJECT_STRING = "SensorDetectedObject"; +static CONSTEXPR const char *MSGSUBTYPE_RSU_STATUS_STRING = "RSUStatus"; } /* End namespace messages */ diff --git a/src/tmx/Messages/include/RSUStatusMessage.h b/src/tmx/Messages/include/RSUStatusMessage.h new file mode 100644 index 000000000..a5e276268 --- /dev/null +++ b/src/tmx/Messages/include/RSUStatusMessage.h @@ -0,0 +1,25 @@ +#pragma once + + +#include +#include "MessageTypes.h" + + +namespace tmx::messages { + + +class RSUStatusMessage : public tmx::message +{ + public: + RSUStatusMessage() {} + + /// Message type for routing this message through TMX core. + static constexpr const char* MessageType = MSGTYPE_APPLICATION_STRING; + + /// Message sub type for routing this message through TMX core. + static constexpr const char* MessageSubType = MSGSUBTYPE_RSU_STATUS_STRING; + }; + +} /* namespace tmx::messages */ + + diff --git a/src/tmx/TmxUtils/CMakeLists.txt b/src/tmx/TmxUtils/CMakeLists.txt index 30c30fb41..ee3c12c5f 100644 --- a/src/tmx/TmxUtils/CMakeLists.txt +++ b/src/tmx/TmxUtils/CMakeLists.txt @@ -1,7 +1,9 @@ PROJECT ( tmxutils CXX ) FILE (GLOB_RECURSE HEADERS "src/" "*.h*") FILE (GLOB_RECURSE SOURCES "src/" "*.c*") - +find_library(NETSNMPAGENT "netsnmpagent") +find_library(NETSNMPMIBS "netsnmpmibs") +find_library(NETSNMP "netsnmp") FIND_PACKAGE (carma-clock) FIND_LIBRARY (UUID_LIBRARY uuid) @@ -16,12 +18,18 @@ TARGET_INCLUDE_DIRECTORIES (${PROJECT_NAME} SYSTEM PUBLIC $ $ ${MYSQL_INCLUDE_DIR} + ${NETSNMP_INCLUDE_DIRS} ${MYSQLCPPCONN_INCLUDE_DIR}) TARGET_LINK_LIBRARIES (${PROJECT_NAME} PUBLIC ${TMXAPI_LIBRARIES} ${MYSQL_LIBRARIES} ${MYSQLCPPCONN_LIBRARIES} ${UUID_LIBRARY} + ${UUID_LIBRARY} + ${NETSNMPAGENT} + ${NETSNMPMIBS} + ${NETSNMP} + ${NETSNMP_LIBRARIES} rdkafka++ ::carma-clock gmock @@ -54,4 +62,4 @@ add_executable(${BINARY} ${TEST_SOURCES}) add_test(NAME ${BINARY} COMMAND ${BINARY}) -target_link_libraries(${BINARY} PUBLIC ${PROJECT_NAME} rdkafka++ gmock ${TMXAPI_LIBRARIES} ${ASN_J2735_LIBRARIES} ${UUID_LIBRARY} gtest) \ No newline at end of file +target_link_libraries(${BINARY} PUBLIC ${PROJECT_NAME} rdkafka++ gmock ${TMXAPI_LIBRARIES} ${ASN_J2735_LIBRARIES} ${UUID_LIBRARY} ${NETSNMPAGENT} ${NETSNMPMIBS} ${NETSNMP} ${NETSNMP_LIBRARIES} gtest) \ No newline at end of file diff --git a/src/tmx/TmxUtils/src/RSU_MIB_4_1.h b/src/tmx/TmxUtils/src/RSU_MIB_4_1.h new file mode 100644 index 000000000..793bf4bea --- /dev/null +++ b/src/tmx/TmxUtils/src/RSU_MIB_4_1.h @@ -0,0 +1,56 @@ +#pragma once +namespace tmx::utils::rsu41::mib::oid +{ + /** + * @brief This header file contains a subset of RSU MIB definition from https://github.com/certificationoperatingcouncil/COC_TestSpecs/blob/master/AppNotes/RSU/RSU-MIB.txt + */ + // Contains the ID given to this RSU. + static constexpr const char *RSU_ID_OID = "1.0.15628.4.1.17.4.0"; + + // Contains the version of this MIB supported by this RSU, e.g. rsuMIB 4.1 rev201812060000Z + static constexpr const char *RSU_MIB_VERSION = "1.0.15628.4.1.17.1.0"; + + // Contains the version of firmware running on this RSU. + static constexpr const char *RSU_FIRMWARE_VERSION = "1.0.15628.4.1.17.2.0"; + + // Contains the name of the manufacturer of this RSU. + static constexpr const char *RSU_MANUFACTURER = "1.0.15628.4.1.17.5.0"; + + // Contains GPS NMEA GPGGA output string. + static constexpr const char *RSU_GPS_OUTPUT_STRING = "1.0.15628.4.1.8.5.0"; + + // Immediate Forward Message Index + static constexpr const char *RSU_IFM_INDEX = "1.0.15628.4.1.5.1.1.0"; + + // Immediate Forward Message PSID. + static constexpr const char *RSU_IFM_PSID = "1.0.15628.4.1.5.1.2.0"; + + // Immediate Forward Message DSRC Message ID + static constexpr const char *RSU_IFM_DSRC_MSG_ID = "1.0.15628.4.1.5.1.3.0"; + + // Immediate Forward Message Transmit Mode + static constexpr const char *RSU_IFM_TX_MODE = "1.0.15628.4.1.5.1.4.0"; + + // DSRC channel set for Immediate Forward Message transmit + static constexpr const char *RSU_IFM_TX_CHANNEL = "1.0.15628.4.1.5.1.5.0"; + + // Set this bit to enable transmission of the message 0=off, 1=on + static constexpr const char *RSU_IFM_ENABLE = "1.0.15628.4.1.5.1.6.0"; + + // Create (4) or Destroy (6) row entry + static constexpr const char *RSU_IFM_STATUS = "1.0.15628.4.1.5.1.7.0"; + + // Specifies the current mode of operation of the RSU and provides capability to transition the device into a new mode, e.g. from the current mode to off, etc + static constexpr const char *RSU_MODE = "1.0.15628.4.1.99.0"; + + /* + SYNTAX INTEGER { + bothOp (0), --both Continuous and Alternating modes are operational + altOp (1), --Alternating mode is operational, + --Continuous mode is not operational + contOp (2), --Continuous mode is operational, + --Alternating mode is not operational + noneOp (3) --neither Continuous nor Alternating mode is operational + */ + static constexpr const char *RSU_CHAN_STATUS = "1.0.15628.4.1.19.1.0"; +} \ No newline at end of file diff --git a/src/tmx/TmxUtils/src/SNMPClient.cpp b/src/tmx/TmxUtils/src/SNMPClient.cpp new file mode 100644 index 000000000..e3398ebbd --- /dev/null +++ b/src/tmx/TmxUtils/src/SNMPClient.cpp @@ -0,0 +1,227 @@ +#include "SNMPClient.h" + +namespace tmx::utils +{ + + // Client defaults to SNMPv3 + snmp_client::snmp_client(const std::string &ip, const int &port, const std::string &community, + const std::string &snmp_user, const std::string &securityLevel, const std::string &authPassPhrase, int snmp_version, int timeout) + + : ip_(ip), port_(port), community_(community), snmp_version_(snmp_version), timeout_(timeout) + { + + PLOG(logDEBUG1) << "Starting SNMP Client. Target device IP address: " << ip_ << ", Target device SNMP port: " << port_; + + // Bring the IP address and port of the target SNMP device in the required form, which is "IPADDRESS:PORT": + std::string ip_port_string = ip_ + ":" + std::to_string(port_); + char *ip_port = &ip_port_string[0]; + + // Initialize SNMP session parameters + init_snmp("carma_snmp"); + snmp_sess_init(&session); + session.peername = ip_port; + session.version = snmp_version_; // SNMP_VERSION_3 + session.securityName = (char *)snmp_user.c_str(); + session.securityNameLen = snmp_user.length(); + + // Fallback behavior to setup a community for SNMP V1/V2 + if (snmp_version_ != SNMP_VERSION_3) + { + session.community = (unsigned char *)community.c_str(); + session.community_len = community_.length(); + } + + // SNMP authorization/privach config + if (securityLevel == "authPriv") + { + session.securityLevel = SNMP_SEC_LEVEL_AUTHPRIV; + } + + else if (securityLevel == "authNoPriv") + { + session.securityLevel = SNMP_SEC_LEVEL_AUTHNOPRIV; + } + + else + session.securityLevel = SNMP_SEC_LEVEL_NOAUTH; + + // Passphrase used for both authentication and privacy + auto phrase_len = authPassPhrase.length(); + auto phrase = (u_char *)authPassPhrase.c_str(); + + // Defining and generating auth config with SHA1 + session.securityAuthProto = snmp_duplicate_objid(usmHMACSHA1AuthProtocol, USM_AUTH_PROTO_SHA_LEN); + session.securityAuthProtoLen = USM_AUTH_PROTO_SHA_LEN; + session.securityAuthKeyLen = USM_AUTH_KU_LEN; + if (session.securityLevel != SNMP_SEC_LEVEL_NOAUTH && generate_Ku(session.securityAuthProto, + session.securityAuthProtoLen, + phrase, phrase_len, + session.securityAuthKey, + &session.securityAuthKeyLen) != SNMPERR_SUCCESS) + { + std::string errMsg = "Error generating Ku from authentication pass phrase. \n"; + throw snmp_client_exception(errMsg); + } + + // Defining and generating priv config with AES (since using SHA1) + session.securityPrivKeyLen = USM_PRIV_KU_LEN; + session.securityPrivProto = + snmp_duplicate_objid(usmAESPrivProtocol, + OID_LENGTH(usmAESPrivProtocol)); + session.securityPrivProtoLen = OID_LENGTH(usmAESPrivProtocol); + + if (session.securityLevel == SNMP_SEC_LEVEL_AUTHPRIV && generate_Ku(session.securityAuthProto, + session.securityAuthProtoLen, + phrase, phrase_len, + session.securityPrivKey, + &session.securityPrivKeyLen) != SNMPERR_SUCCESS) + { + std::string errMsg = "Error generating Ku from privacy pass phrase. \n"; + throw snmp_client_exception(errMsg); + } + + session.timeout = timeout_; + + // Opens the snmp session if it exists + ss = snmp_open(&session); + + if (ss == nullptr) + { + PLOG(logERROR) << "Failed to establish session with target device"; + snmp_sess_perror("snmpget", &session); + throw snmp_client_exception("Failed to establish session with target device"); + } + else + { + PLOG(logINFO) << "Established session with device at " << ip_; + } + } + + snmp_client::~snmp_client() + { + PLOG(logINFO) << "Closing SNMP session"; + snmp_close(ss); + } + + // Original implementation used in Carma Streets https://github.com/usdot-fhwa-stol/snmp-client + bool snmp_client::process_snmp_request(const std::string &input_oid, const request_type &request_type, snmp_response_obj &val) + { + + /*Structure to hold response from the remote host*/ + snmp_pdu *response; + + // Create pdu for the data + if (request_type == request_type::GET) + { + PLOG(logDEBUG1) << "Attempting to GET value for: " << input_oid; + pdu = snmp_pdu_create(SNMP_MSG_GET); + } + else if (request_type == request_type::SET) + { + PLOG(logDEBUG1) << "Attempting to SET value for " << input_oid << " to " << val.val_int; + pdu = snmp_pdu_create(SNMP_MSG_SET); + } + else + { + PLOG(logERROR) << "Invalid request type, method accpets only GET and SET"; + return false; + } + + // Read input OID into an OID variable: + // net-snmp has several methods for creating an OID object + // their documentation suggests using get_node. read_objid seems like a simpler approach + // TO DO: investigate update to get_node + if (!read_objid(input_oid.c_str(), OID, &OID_len)) + { + // If oid cannot be created + PLOG(logERROR) << "OID could not be created from input: " << input_oid; + return false; + } + else + { + + if (request_type == request_type::GET) + { + // Add OID to pdu for get request + snmp_add_null_var(pdu, OID, OID_len); + } + else if (request_type == request_type::SET) + { + if (val.type == snmp_response_obj::response_type::INTEGER) + { + snmp_add_var(pdu, OID, OID_len, 'i', (std::to_string(val.val_int)).c_str()); + } + // Needs to be finalized to support octet string use + else if (val.type == snmp_response_obj::response_type::STRING) + { + PLOG(logERROR) << "Setting string value is currently not supported"; + } + } + + PLOG(logINFO) << "Created OID for input: " << input_oid; + } + // Send the request + int status = snmp_synch_response(ss, pdu, &response); + PLOG(logINFO) << "Response request status: " << status << " (=" << (status == STAT_SUCCESS ? "SUCCESS" : "FAILED") << ")"; + + // Check GET response + if (status == STAT_SUCCESS && response && response->errstat == SNMP_ERR_NOERROR && request_type == request_type::GET) + { + for (auto vars = response->variables; vars; vars = vars->next_variable) + { + // Get value of variable depending on ASN.1 type + // Variable could be a integer, string, bitstring, ojbid, counter : defined here https://github.com/net-snmp/net-snmp/blob/master/include/net-snmp/types.h + // get Integer value + if (vars->type == ASN_INTEGER && vars->val.integer) + { + val.type = snmp_response_obj::response_type::INTEGER; + val.val_int = *vars->val.integer; + } + else if (vars->type == ASN_OCTET_STR && vars->val.string) + { + size_t str_len = vars->val_len; + for (size_t i = 0; i < str_len; ++i) + { + val.val_string.push_back(vars->val.string[i]); + } + val.type = snmp_response_obj::response_type::STRING; + } + } + } + else + { + log_error(status, request_type, response); + return false; + } + + if (response) + { + snmp_free_pdu(response); + OID_len = MAX_OID_LEN; + } + + return true; + } + + int snmp_client::get_port() const + { + return port_; + } + + void snmp_client::log_error(const int &status, const request_type &request_type, const snmp_pdu *response) const + { + + if (status == STAT_SUCCESS) + { + PLOG(logERROR) << "Variable type: " << response->variables->type << ". Error in packet " << static_cast(snmp_errstring(static_cast(response->errstat))); + } + else if (status == STAT_TIMEOUT) + { + PLOG(logERROR) << "Timeout, no response from server"; + } + else + { + PLOG(logERROR) << "Unknown SNMP Error for " << (request_type == request_type::GET ? "GET" : "SET"); + } + } +} // namespace \ No newline at end of file diff --git a/src/tmx/TmxUtils/src/SNMPClient.h b/src/tmx/TmxUtils/src/SNMPClient.h new file mode 100644 index 000000000..75eaa9360 --- /dev/null +++ b/src/tmx/TmxUtils/src/SNMPClient.h @@ -0,0 +1,113 @@ +#pragma once + +#include +#include +#include +#include "PluginLog.h" + +#include "SNMPClientException.h" + +namespace tmx::utils +{ + + enum class request_type + { + GET, + SET, + OTHER // Processing this request type is not a defined behavior, included for testing only + }; + + /** @brief A struct to hold the value being sent to the TSC, can be integer or string. Type needs to be defined*/ + struct snmp_response_obj + { + /** @brief The type of value being requested or set, on the TSC */ + enum class response_type + { + INTEGER, + STRING + }; + + // snmp response values can be any asn.1 supported types. + // Integer and string values can be processed here + int64_t val_int = 0; + std::vector val_string; + response_type type; + + inline bool operator==(const snmp_response_obj &obj2) const + { + return val_int == obj2.val_int && val_string == obj2.val_string && type == obj2.type; + } + }; + + class snmp_client + { + private: + /*variables to store an snmp session*/ + // struct that holds information about who we're going to be talking to + // We need to declare 2 of these, one to fill info with and second which is + // a pointer returned by the library + snmp_session session; + snmp_session *ss; + + /*Structure to hold all of the information that we're going to send to the remote host*/ + snmp_pdu *pdu; + + /*OID is going to hold the location of the information which we want to receive. It will need a size as well*/ + oid OID[MAX_OID_LEN]; + size_t OID_len = MAX_OID_LEN; + + // Values from config + /*Target device IP address*/ + std::string ip_; + /*Target device NTCIP port*/ + int port_ = 0; + /*Target community for establishing snmp communication*/ + std::string community_ = "public"; + /* net-snmp version definition: SNMP_VERSION_1:0 SNMP_VERSION_2c:1 SNMP_VERSION_2u:2 SNMP_VERSION_3:3 + https://github.com/net-snmp/net-snmp/blob/master/include/net-snmp/library/snmp.h */ + int snmp_version_ = 3; // default to 3 since previous versions not compatable currently + /*Time after which the the snmp request times out*/ + int timeout_ = 10000; + + public: + /** @brief Constructor for Traffic Signal Controller Service client. + * Uses the arguments provided to establish an snmp connection + * @param ip The ip ,as a string, for the tsc_client_service to establish an snmp communication with. + * @param port Target port as integer on the host for snmp communication. + * @param community The community id as a string. Defaults to "public" if unassigned. + * @param snmp_version The snmp_version as defined in net-snmp.Default to 0 if unassigned. + * net-snmp version definition: SNMP_VERSION_1:0 SNMP_VERSION_2c:1 SNMP_VERSION_2u:2 SNMP_VERSION_3:3" + * @param timeout The time in microseconds after which an snmp session request expires. Defaults to 100 if unassigned + * **/ + snmp_client(const std::string &ip, const int &port, const std::string &community, const std::string &snmp_user, const std::string &securityLevel, const std::string &authPassPhrase, int snmp_version = 0, int timeout = 100); + + /* Disable default copy constructor*/ + snmp_client(snmp_client &sc) = delete; + + /* Disable default move constructor*/ + snmp_client(snmp_client &&sc) = delete; + + /** @brief Returns true or false depending on whether the request could be processed for given input OID at the Traffic Signal Controller. + * @param input_oid The OID to request information for. + * @param request_type The request type for which the error is being logged. Accepted values are "GET" and "SET" only. + * @param value_int The integer value for the object returned by reference. For "SET" it is the value to be set. + * For "GET", it is the value returned for the returned object by reference. + * This is an optional argument, if not provided, defaults to 0. + * @param value_str String value for the object, returned by reference. Optional argument, if not provided the value is set as an empty string + * @return Integer value at the oid, returns false if value cannot be set/requested or oid doesn't have an integer value to return.*/ + + virtual bool process_snmp_request(const std::string &input_oid, const request_type &request_type, snmp_response_obj &val); + /** @brief Finds error type from status and logs an error. + * @param status The integer value corresponding to net-snmp defined errors. macros considered are STAT_SUCCESS(0) and STAT_TIMEOUT(2) + * @param request_type The request type for which the error is being logged (GET/SET). + * @param response The snmp_pdu struct */ + + virtual int get_port() const; // Returns the current port (should always be 161 or 162) + + void log_error(const int &status, const request_type &request_type, const snmp_pdu *response) const; + + /** @brief Destructor for client. Closes the snmp session**/ + virtual ~snmp_client(); + }; + +} // namespace \ No newline at end of file diff --git a/src/tmx/TmxUtils/src/SNMPClientException.cpp b/src/tmx/TmxUtils/src/SNMPClientException.cpp new file mode 100644 index 000000000..67f82fabe --- /dev/null +++ b/src/tmx/TmxUtils/src/SNMPClientException.cpp @@ -0,0 +1,8 @@ +#include "SNMPClientException.h" + +namespace tmx::utils { + + snmp_client_exception::snmp_client_exception(const std::string &msg): std::runtime_error(msg){}; + + snmp_client_exception::~snmp_client_exception() = default; +} \ No newline at end of file diff --git a/src/tmx/TmxUtils/src/SNMPClientException.h b/src/tmx/TmxUtils/src/SNMPClientException.h new file mode 100644 index 000000000..76020a5b1 --- /dev/null +++ b/src/tmx/TmxUtils/src/SNMPClientException.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +namespace tmx::utils { + /** + * @brief Runtime error related to SNMP client used to communicate with Traffic Signal Controller (NTCIP). + * + * @author Paul Bourelly + */ + class snmp_client_exception : public std::runtime_error{ + public: + /** + * @brief Destructor. + */ + ~snmp_client_exception() override; + /** + * @brief Constructor. + * @param msg String exception message. + */ + explicit snmp_client_exception(const std::string &msg ); + }; +} \ No newline at end of file diff --git a/src/tmx/TmxUtils/test/MockSNMPClient.h b/src/tmx/TmxUtils/test/MockSNMPClient.h new file mode 100644 index 000000000..7e07fe47f --- /dev/null +++ b/src/tmx/TmxUtils/test/MockSNMPClient.h @@ -0,0 +1,18 @@ +#pragma once +#include "SNMPClient.h" +#include +#include + +using namespace tmx::utils; +using namespace std; + +namespace unit_test +{ + class mock_snmp_client : public snmp_client + { + public: + mock_snmp_client(const std::string &ip, const int &port, const std::string &community, const std::string &snmp_user, const std::string &securityLevel, const std::string &authPassPhrase, int snmp_version = 0, int timeout = 100) : snmp_client(ip, port, community, snmp_user, securityLevel, authPassPhrase, snmp_version, timeout){}; + ~mock_snmp_client() = default; + MOCK_METHOD(bool, process_snmp_request, (const std::string &input_oid, const request_type &request_type, snmp_response_obj &val), (override)); + }; +} \ No newline at end of file diff --git a/src/tmx/TmxUtils/test/test_SNMPClient.cpp b/src/tmx/TmxUtils/test/test_SNMPClient.cpp new file mode 100644 index 000000000..3e775945c --- /dev/null +++ b/src/tmx/TmxUtils/test/test_SNMPClient.cpp @@ -0,0 +1,93 @@ + +#include "MockSNMPClient.h" +#include "gtest/gtest.h" +#include "RSU_MIB_4_1.h" + +using namespace tmx::utils; +using namespace std; +using namespace tmx::utils::rsu41::mib::oid; +using testing::_; +using testing::Action; +using testing::DoDefault; +using testing::Return; +using testing::SetArgReferee; +using testing::Throw; + +namespace unit_test +{ + class test_SNMPClient : public ::testing::Test + { + public: + shared_ptr scPtr; + uint16_t port = 161; + test_SNMPClient() + { + scPtr = make_shared("127.0.0.1", port, "public", "test", "authPriv", "testtesttest", SNMP_VERSION_3, 1000); + } + }; + + TEST_F(test_SNMPClient, constructor_error) + { + ASSERT_THROW(snmp_client("127.0.0.1", port, "public", "test", "authPriv", "test", SNMP_VERSION_3, 1000), snmp_client_exception); + ASSERT_NO_THROW(snmp_client("127.0.0.1", port, "public", "test", "authPriv", "testtesttest", SNMP_VERSION_3, 1000)); + ASSERT_NO_THROW(snmp_client("127.0.0.1", port, "public", "test", "authNoPriv", "testtesttest", SNMP_VERSION_3, 1000)); + ASSERT_NO_THROW(snmp_client("127.0.0.1", port, "public", "test", "authNoPriv", "testtesttest", SNMP_VERSION_1, 1000)); + ASSERT_NO_THROW(snmp_client("127.0.0.1", port, "public", "test", "", "testtesttest", SNMP_VERSION_3, 1000)); + ASSERT_THROW(snmp_client("127.0.XX.XX", port, "public", "test", "", "testtesttest", SNMP_VERSION_3, 1000), snmp_client_exception); + } + + TEST_F(test_SNMPClient, get_port) + { + ASSERT_EQ(161, scPtr->get_port()); + } + + TEST_F(test_SNMPClient, log_error) + { + snmp_pdu response; + ASSERT_NO_THROW(scPtr->log_error(STAT_ERROR, request_type::GET, &response)); + ASSERT_NO_THROW(scPtr->log_error(STAT_ERROR, request_type::SET, &response)); + ASSERT_NO_THROW(scPtr->log_error(STAT_SUCCESS, request_type::OTHER, &response)); + ASSERT_NO_THROW(scPtr->log_error(STAT_TIMEOUT, request_type::OTHER, &response)); + } + + TEST_F(test_SNMPClient, process_snmp_request) + { + snmp_response_obj reqponseRSUID; + string rsuId = "RSU4.1"; + vector rsuId_c; + copy(rsuId.begin(), rsuId.end(), back_inserter(rsuId_c)); + reqponseRSUID.val_string = rsuId_c; + reqponseRSUID.type = snmp_response_obj::response_type::STRING; + EXPECT_CALL(*scPtr, process_snmp_request(RSU_ID_OID, request_type::GET, _)).WillRepeatedly(testing::DoAll(SetArgReferee<2>(reqponseRSUID), Return(true))); + EXPECT_CALL(*scPtr, process_snmp_request(RSU_ID_OID, request_type::SET, _)).WillRepeatedly(testing::DoAll(SetArgReferee<2>(reqponseRSUID), Return(true))); + + snmp_response_obj reqponseMode; + reqponseMode.val_int = 2; + reqponseMode.type = snmp_response_obj::response_type::INTEGER; + EXPECT_CALL(*scPtr, process_snmp_request(RSU_MODE, request_type::GET, _)).WillRepeatedly(testing::DoAll(SetArgReferee<2>(reqponseMode), Return(true))); + EXPECT_CALL(*scPtr, process_snmp_request(RSU_MODE, request_type::SET, _)).WillRepeatedly(testing::DoAll(SetArgReferee<2>(reqponseRSUID), Return(true))); + + snmp_response_obj reqponseInvalidOID; + EXPECT_CALL(*scPtr, process_snmp_request("Invalid OID", request_type::GET, _)).WillRepeatedly(testing::DoAll(SetArgReferee<2>(reqponseInvalidOID), Return(false))); + EXPECT_CALL(*scPtr, process_snmp_request("Invalid OID", request_type::SET, _)).WillRepeatedly(testing::DoAll(SetArgReferee<2>(reqponseInvalidOID), Return(false))); + + snmp_response_obj response; + scPtr->process_snmp_request(RSU_ID_OID, request_type::GET, response); + scPtr->process_snmp_request(RSU_ID_OID, request_type::SET, response); + scPtr->process_snmp_request(RSU_MODE, request_type::GET, response); + scPtr->process_snmp_request(RSU_MODE, request_type::SET, response); + scPtr->process_snmp_request("Invalid OID", request_type::GET, response); + scPtr->process_snmp_request("Invalid OID", request_type::SET, response); + + snmp_client scClient("127.0.0.1", port, "public", "test", "authPriv", "testtesttest", SNMP_VERSION_3, 1000); + scClient.process_snmp_request(RSU_ID_OID, request_type::GET, reqponseRSUID); + scClient.process_snmp_request(RSU_ID_OID, request_type::SET, reqponseRSUID); + scClient.process_snmp_request(RSU_ID_OID, request_type::OTHER, reqponseRSUID); + scClient.process_snmp_request("INVALID OID", request_type::GET, reqponseRSUID); + + scClient.process_snmp_request(RSU_MODE, request_type::GET, reqponseMode); + scClient.process_snmp_request(RSU_MODE, request_type::SET, reqponseMode); + scClient.process_snmp_request(RSU_MODE, request_type::OTHER, reqponseMode); + } + +} \ No newline at end of file diff --git a/src/v2i-hub/RSUHealthMonitorPlugin/CMakeLists.txt b/src/v2i-hub/RSUHealthMonitorPlugin/CMakeLists.txt new file mode 100755 index 000000000..d863a4bf1 --- /dev/null +++ b/src/v2i-hub/RSUHealthMonitorPlugin/CMakeLists.txt @@ -0,0 +1,26 @@ +PROJECT(RSUHealthMonitorPlugin VERSION 7.5.1 LANGUAGES CXX) + +set(TMX_PLUGIN_NAME "RSU Health Monitor") + +find_library(libasn1c .) + +BuildTmxPlugin() + +TARGET_LINK_LIBRARIES(${PROJECT_NAME} PUBLIC tmxutils jsoncpp NemaTode) + +############# +## Testing ## +############# +enable_testing() +include_directories(${PROJECT_SOURCE_DIR}/src) +add_library(${PROJECT_NAME}_lib src/RSUHealthMonitorWorker.cpp) +target_link_libraries(${PROJECT_NAME}_lib PUBLIC + tmxutils + NemaTode + jsoncpp) +set(BINARY ${PROJECT_NAME}_test) +file(GLOB_RECURSE TEST_SOURCES LIST_DIRECTORIES false test/*.h test/*.cpp) +set(SOURCES ${TEST_SOURCES} WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/test) +add_executable(${BINARY} ${TEST_SOURCES}) +add_test(NAME ${BINARY} COMMAND ${BINARY}) +target_link_libraries(${BINARY} PUBLIC ${PROJECT_NAME}_lib gtest) \ No newline at end of file diff --git a/src/v2i-hub/RSUHealthMonitorPlugin/manifest.json b/src/v2i-hub/RSUHealthMonitorPlugin/manifest.json new file mode 100755 index 000000000..f2ee94443 --- /dev/null +++ b/src/v2i-hub/RSUHealthMonitorPlugin/manifest.json @@ -0,0 +1,51 @@ +{ + "name": "RSUHealthMonitor", + "description": "Monitor RSU health status", + "version": "@PROJECT_VERSION@", + "exeLocation": "/bin/RSUHealthMonitorPlugin", + "coreIpAddr":"127.0.0.1", + "corePort":24601, + "messageTypes": [], + "configuration": [ + { + "key": "LogLevel", + "default": "INFO", + "description": "The log level for this plugin" + }, + { + "key":"Interval", + "default":"1", + "description": "Sending RSU SNMP GET request at every configured interval. Default every 1 second. Unit of measure: second." + }, + { + "key":"RSUIp", + "default":"192.168.XX.XX", + "description":"An IP address of the RSU the V2X hub is connected to." + }, + { + "key":"SNMPPort", + "default":"161", + "description":"The SNMP port for sending message or command." + }, + { + "key":"AuthPassPhrase", + "default":"dummy", + "description":"SNMP v3 authentication passphrase" + }, + { + "key":"SecurityUser", + "default":"authOnlyUser", + "description":"SNMP Security Name" + }, + { + "key":"SecurityLevel", + "default":"authPriv", + "description":"SNMP Security level" + }, + { + "key":"RSUMIBVersion", + "default":"RSU4.1", + "description":"The version of RSU MIB (Management Information Base). E.G. RSU4.1 or RSU1218. Currently only support RSU4.1" + } + ] +} \ No newline at end of file diff --git a/src/v2i-hub/RSUHealthMonitorPlugin/src/RSUHealthMonitorPlugin.cpp b/src/v2i-hub/RSUHealthMonitorPlugin/src/RSUHealthMonitorPlugin.cpp new file mode 100755 index 000000000..8d81a3b6c --- /dev/null +++ b/src/v2i-hub/RSUHealthMonitorPlugin/src/RSUHealthMonitorPlugin.cpp @@ -0,0 +1,90 @@ +#include "RSUHealthMonitorPlugin.h" + +using namespace RSUHealthMonitor; +using namespace tmx::utils; + +namespace RSUHealthMonitor +{ + + RSUHealthMonitorPlugin::RSUHealthMonitorPlugin(const std::string &name) : PluginClient(name) + { + _rsuWorker = std::make_shared(); + _rsuStatusTimer = make_unique(); + UpdateConfigSettings(); + + // Send SNMP call to RSU periodically at configurable interval. + _timerThId = _rsuStatusTimer->AddPeriodicTick([this]() + { + // Periodic SNMP call to get RSU status based on RSU MIB version 4.1 + auto rsuStatusJson = _rsuWorker->getRSUStatus(_rsuMibVersion, _rsuIp, _snmpPort, _securityUser, _authPassPhrase, _securityLevel, SEC_TO_MICRO); + PLOG(logINFO) << "Updating _interval: " << _interval; + //Broadcast RSU status periodically at _interval + BroadcastRSUStatus(rsuStatusJson); }, + std::chrono::milliseconds(_interval * SEC_TO_MILLI)); + _rsuStatusTimer->Start(); + } + + void RSUHealthMonitorPlugin::UpdateConfigSettings() + { + PLOG(logINFO) << "Updating configuration settings."; + + lock_guard lock(_configMutex); + GetConfigValue("Interval", _interval); + GetConfigValue("RSUIp", _rsuIp); + GetConfigValue("SNMPPort", _snmpPort); + GetConfigValue("AuthPassPhrase", _authPassPhrase); + GetConfigValue("SecurityUser", _securityUser); + GetConfigValue("SecurityLevel", _securityLevel); + GetConfigValue("RSUMIBVersion", _rsuMIBVersionStr); + boost::trim_left(_rsuMIBVersionStr); + boost::trim_right(_rsuMIBVersionStr); + // Support RSU MIB version 4.1 + if (boost::iequals(_rsuMIBVersionStr, RSU4_1_str)) + { + _rsuMibVersion = RSUMibVersion::RSUMIB_V_4_1; + } + else + { + _rsuMibVersion = RSUMibVersion::UNKOWN_MIB_V; + PLOG(logERROR) << "Uknown RSU MIB version: " << _rsuMIBVersionStr; + } + + try + { + _rsuStatusTimer->ChangeFrequency(_timerThId, std::chrono::milliseconds(_interval * SEC_TO_MILLI)); + } + catch (const tmx::TmxException &ex) + { + PLOG(logERROR) << ex.what(); + } + } + + void RSUHealthMonitorPlugin::OnConfigChanged(const char *key, const char *value) + { + PluginClient::OnConfigChanged(key, value); + UpdateConfigSettings(); + } + + void RSUHealthMonitorPlugin::BroadcastRSUStatus(const Json::Value &rsuStatusJson) + { + // Broadcast the RSU status info when there are RSU responses. + if (!rsuStatusJson.empty() && _rsuWorker) + { + auto rsuStatusFields = _rsuWorker->getJsonKeys(rsuStatusJson); + auto configTbl = _rsuWorker->GetRSUStatusConfig(_rsuMibVersion); + + // Only broadcast RSU status when all required fields are present. + if (_rsuWorker->validateAllRequiredFieldsPresent(configTbl, rsuStatusFields)) + { + auto sendRsuStatusMsg = _rsuWorker->convertJsonToTMXMsg(rsuStatusJson); + BroadcastMessage(sendRsuStatusMsg, RSUHealthMonitorPlugin::GetName()); + } + } + } + +} // namespace RSUHealthMonitor + +int main(int argc, char *argv[]) +{ + return run_plugin("RSU Health Monitor", argc, argv); +} diff --git a/src/v2i-hub/RSUHealthMonitorPlugin/src/RSUHealthMonitorPlugin.h b/src/v2i-hub/RSUHealthMonitorPlugin/src/RSUHealthMonitorPlugin.h new file mode 100755 index 000000000..47d8ee1bd --- /dev/null +++ b/src/v2i-hub/RSUHealthMonitorPlugin/src/RSUHealthMonitorPlugin.h @@ -0,0 +1,46 @@ + +#pragma once + +#include "PluginClient.h" +#include +#include "RSUStatusMessage.h" +#include "RSUHealthMonitorWorker.h" + +using namespace tmx::utils; +using namespace std; + +namespace RSUHealthMonitor +{ + + class RSUHealthMonitorPlugin : public PluginClient + { + private: + mutex _configMutex; + uint16_t _interval; + string _rsuIp; + uint16_t _snmpPort; + string _authPassPhrase; + string _securityUser; + string _securityLevel; + string _rsuMIBVersionStr; + RSUMibVersion _rsuMibVersion; + const char *RSU4_1_str = "RSU4.1"; + const char *RSU1218_str = "RSU1218"; + shared_ptr _rsuWorker; + unique_ptr _rsuStatusTimer; + uint _timerThId; + const long SEC_TO_MICRO = 1000000; + const long SEC_TO_MILLI= 1000; + /** + * @brief Broadcast RSU status + * @param Json::Value RSU status in JSON format + */ + void BroadcastRSUStatus(const Json::Value& rsuStatusJson); + + public: + explicit RSUHealthMonitorPlugin(const std::string &name); + void UpdateConfigSettings(); + void OnConfigChanged(const char *key, const char *value) override; + }; + +} // namespace RSUHealthMonitorPlugin \ No newline at end of file diff --git a/src/v2i-hub/RSUHealthMonitorPlugin/src/RSUHealthMonitorWorker.cpp b/src/v2i-hub/RSUHealthMonitorPlugin/src/RSUHealthMonitorWorker.cpp new file mode 100644 index 000000000..6de25d57e --- /dev/null +++ b/src/v2i-hub/RSUHealthMonitorPlugin/src/RSUHealthMonitorWorker.cpp @@ -0,0 +1,217 @@ +#include "RSUHealthMonitorWorker.h" + +namespace RSUHealthMonitor +{ + + RSUHealthMonitorWorker::RSUHealthMonitorWorker() + { + _RSUSTATUSConfigMapPtr = make_shared>(); + // Currently only support RSU MIB version 4.1. Other future supported versions will be inserted here. + RSUStatusConfigTable rsuRstatusTable = constructRsuStatusConfigTable(RSUMibVersion::RSUMIB_V_4_1); + _RSUSTATUSConfigMapPtr->insert({RSUMibVersion::RSUMIB_V_4_1, rsuRstatusTable}); + } + + RSUStatusConfigTable RSUHealthMonitorWorker::constructRsuStatusConfigTable(const RSUMibVersion &mibVersion) const + { + RSUStatusConfigTable rsuStatusTbl; + // Populate custom defined RSU Status table with RSU MIB version 4.1. + if (mibVersion == RSUMibVersion::RSUMIB_V_4_1) + { + RSUFieldOIDStruct rsuID = {"rsuID", RSU_ID_OID, true}; + rsuStatusTbl.push_back(rsuID); + + RSUFieldOIDStruct rsuMibVersion = {"rsuMibVersion", RSU_MIB_VERSION, true}; + rsuStatusTbl.push_back(rsuMibVersion); + + RSUFieldOIDStruct rsuFirmwareVersion = {"rsuFirmwareVersion", RSU_FIRMWARE_VERSION, true}; + rsuStatusTbl.push_back(rsuFirmwareVersion); + + RSUFieldOIDStruct rsuManufacturer = {"rsuManufacturer", RSU_MANUFACTURER, true}; + rsuStatusTbl.push_back(rsuManufacturer); + + RSUFieldOIDStruct rsuGpsOutputString = {"rsuGpsOutputString", RSU_GPS_OUTPUT_STRING, true}; + rsuStatusTbl.push_back(rsuGpsOutputString); + + RSUFieldOIDStruct rsuIFMIndex = {"rsuIFMIndex", RSU_IFM_INDEX, false}; + rsuStatusTbl.push_back(rsuIFMIndex); + + RSUFieldOIDStruct rsuIFMPsid = {"rsuIFMPsid", RSU_IFM_PSID, false}; + rsuStatusTbl.push_back(rsuIFMPsid); + + RSUFieldOIDStruct rsuIFMDsrcMsgId = {"rsuIFMDsrcMsgId", RSU_IFM_DSRC_MSG_ID, false}; + rsuStatusTbl.push_back(rsuIFMDsrcMsgId); + + RSUFieldOIDStruct rsuIFMTxMode = {"rsuIFMTxMode", RSU_IFM_INDEX, false}; + rsuStatusTbl.push_back(rsuIFMTxMode); + + RSUFieldOIDStruct rsuIFMTxChannel = {"rsuIFMTxChannel", RSU_IFM_TX_CHANNEL, false}; + rsuStatusTbl.push_back(rsuIFMTxChannel); + + RSUFieldOIDStruct rsuIFMEnable = {"rsuIFMEnable", RSU_IFM_ENABLE, false}; + rsuStatusTbl.push_back(rsuIFMEnable); + + RSUFieldOIDStruct rsuIFMStatus = {"rsuIFMStatus", RSU_IFM_STATUS, false}; + rsuStatusTbl.push_back(rsuIFMStatus); + + RSUFieldOIDStruct rsuMode = {"rsuMode", RSU_MODE, true}; + rsuStatusTbl.push_back(rsuMode); + + RSUFieldOIDStruct rsuChanStatus = {"rsuChanStatus", RSU_CHAN_STATUS, true}; + rsuStatusTbl.push_back(rsuChanStatus); + } + return rsuStatusTbl; + } + + bool RSUHealthMonitorWorker::validateAllRequiredFieldsPresent(const RSUHealthMonitor::RSUStatusConfigTable &configTbl, const vector &fields) const + { + bool isAllPresent = true; + for (const auto &config : configTbl) + { + if (config.required && find(fields.begin(), fields.end(), config.field) == fields.end()) + { + isAllPresent = false; + PLOG(logWARNING) << "No broadcast as required field " << config.field << " is not present!"; + } + } + return isAllPresent; + } + + RSUStatusConfigTable RSUHealthMonitorWorker::GetRSUStatusConfig(const RSUMibVersion &mibVersion) const + { + RSUStatusConfigTable result; + try + { + result = _RSUSTATUSConfigMapPtr->at(mibVersion); + } + catch (const out_of_range &ex) + { + PLOG(logERROR) << "Unknown MIB version! " << ex.what(); + } + return result; + } + + std::map RSUHealthMonitorWorker::ParseRSUGPS(const std::string &gps_nmea_data) const + { + std::map result; + nmea::NMEAParser parser; + nmea::GPSService gps(parser); + try + { + parser.readLine(gps_nmea_data); + std::stringstream ss; + ss << std::setprecision(8) << std::fixed << gps.fix.latitude << std::endl; + auto latitude_str = ss.str(); + std::stringstream sss; + sss << std::setprecision(8) << std::fixed << gps.fix.longitude << std::endl; + auto longitude_str = sss.str(); + result.insert({std::stod(latitude_str), std::stod(longitude_str)}); + PLOG(logDEBUG) << "Parse GPS NMEA string: " << gps_nmea_data << ". Result (Latitude, Longitude): (" << latitude_str << "," << longitude_str << ")"; + } + catch (const nmea::NMEAParseError &e) + { + PLOG(logERROR) << e.message.c_str(); + } + return result; + } + + Json::Value RSUHealthMonitorWorker::getRSUStatus(const RSUMibVersion &mibVersion, const string &_rsuIp, uint16_t &_snmpPort, const string &_securityUser, const string &_authPassPhrase, const string &_securityLevel, long timeout) + { + auto rsuStatusConfigTbl = GetRSUStatusConfig(mibVersion); + if (rsuStatusConfigTbl.size() == 0) + { + PLOG(logERROR) << "RSU status update call failed due to the RSU status config table is empty!"; + return Json::nullValue; + } + try + { + // Create SNMP client and use SNMP V3 protocol + PLOG(logINFO) << "Update SNMP client: RSU IP: " << _rsuIp << ", RSU port: " << _snmpPort << ", User: " << _securityUser << ", auth pass phrase: " << _authPassPhrase << ", security level: " + << _securityLevel; + auto _snmpClientPtr = std::make_unique(_rsuIp, _snmpPort, "", _securityUser, _securityLevel, _authPassPhrase, SNMP_VERSION_3, timeout); + + Json::Value rsuStatuJson; + // Sending RSU SNMP call for each field as each field has its own OID. + for (const auto &config : rsuStatusConfigTbl) + { + PLOG(logINFO) << "SNMP RSU status call for field:" << config.field << ", OID: " << config.oid; + snmp_response_obj responseVal; + if (_snmpClientPtr) + { + auto success = _snmpClientPtr->process_snmp_request(config.oid, request_type::GET, responseVal); + if (!success && config.required) + { + PLOG(logERROR) << "SNMP session stopped as the required field: " << config.field << " failed! Return empty RSU status!"; + return Json::nullValue; + } + else if (success) + { + rsuStatuJson.append(populateJson(config.field, responseVal)); + } + } + } + return rsuStatuJson; + } + catch (tmx::utils::snmp_client_exception &ex) + { + PLOG(logERROR) << ex.what(); + return Json::nullValue; + } + } + + Json::Value RSUHealthMonitorWorker::populateJson(const string &field, const snmp_response_obj &response) const + { + Json::Value rsuStatuJson; + if (response.type == snmp_response_obj::response_type::INTEGER) + { + rsuStatuJson[field] = response.val_int; + } + else if (response.type == snmp_response_obj::response_type::STRING) + { + string response_str(response.val_string.begin(), response.val_string.end()); + // Proess GPS nmea string + if (boost::iequals(field, "rsuGpsOutputString")) + { + auto gps = ParseRSUGPS(response_str); + rsuStatuJson["rsuGpsOutputStringLatitude"] = gps.begin()->first; + rsuStatuJson["rsuGpsOutputStringLongitude"] = gps.begin()->second; + } + rsuStatuJson[field] = response_str; + } + return rsuStatuJson; + } + + RSUStatusMessage RSUHealthMonitorWorker::convertJsonToTMXMsg(const Json::Value &json) const + { + Json::FastWriter fasterWirter; + string json_str = fasterWirter.write(json); + tmx::messages::RSUStatusMessage rsuStatusMsg; + rsuStatusMsg.set_contents(json_str); + return rsuStatusMsg; + } + + vector RSUHealthMonitorWorker::getJsonKeys(const Json::Value &json) const + { + vector keys; + if (json.isArray()) + { + for (auto itr = json.begin(); itr != json.end(); itr++) + { + if (itr->isObject()) + { + for (auto const &field : itr->getMemberNames()) + { + keys.push_back(field); + } + } + } + } + else if (json.isObject()) + { + for (auto const &field : json.getMemberNames()) + { + keys.push_back(field); + } + } + return keys; + } +} \ No newline at end of file diff --git a/src/v2i-hub/RSUHealthMonitorPlugin/src/RSUHealthMonitorWorker.h b/src/v2i-hub/RSUHealthMonitorPlugin/src/RSUHealthMonitorWorker.h new file mode 100644 index 000000000..ef7a429c5 --- /dev/null +++ b/src/v2i-hub/RSUHealthMonitorPlugin/src/RSUHealthMonitorWorker.h @@ -0,0 +1,120 @@ +#pragma once +#include +#include "RSU_MIB_4_1.h" +#include +#include +#include +#include +#include +#include "PluginLog.h" +#include +#include "SNMPClient.h" +#include +#include "RSUStatusMessage.h" + +using namespace std; +using namespace tmx::utils; +using namespace tmx::utils::rsu41::mib::oid; +using namespace tmx::messages; + +namespace RSUHealthMonitor +{ + enum class RSUMibVersion + { + UNKOWN_MIB_V = 0, + RSUMIB_V_4_1 = 1, + RSUMIB_V_1218 = 2 + }; + + struct RSUFieldOIDStruct + { + string field; + string oid; + bool required; // Indicate whether this field is required to before broadcasting the RSU status. + }; + + /** + * RSUStatusTable is custom defined RSU status information. + * The fields are a subset of the fields from the RSU MIB definition used to quantify the health of the RSU. https://github.com/certificationoperatingcouncil/COC_TestSpecs/blob/master/AppNotes/RSU/RSU-MIB.txt + */ + using RSUStatusConfigTable = vector; + + class RSUHealthMonitorWorker + { + private: + // A map of RSU MIB version used and RSUStatusTable + shared_ptr> _RSUSTATUSConfigMapPtr; + + /** + * @brief Poupate the RSU status table with the specified version of OIDs and fields. + * Private: Only allow to initialze the RSU STATUS MAP once + * @param mibVersion specified + * @return RSUStatusTable the self defined RSU status https://usdot-carma.atlassian.net/wiki/spaces/WFD2/pages/2640740360/RSU+Health+Monitor+Plugin+Design + */ + RSUStatusConfigTable constructRsuStatusConfigTable(const RSUMibVersion &mibVersion) const; + + public: + // Populate the RSU Status Table with predefined fields and their mapping OIDs in constructor + RSUHealthMonitorWorker(); + + // Access to the RSU status table based in the RSU MIB version provided + RSUStatusConfigTable GetRSUStatusConfig(const RSUMibVersion &mibVersion) const; + + /** + * @brief determine if all required fields in the RSU config map _RSUSTATUSConfigMapPtr present in the input fields + * Use _RSUSTATUSConfigMapPtr RSU status config map that defines all fields and whether the fields are required. + * @param RSUStatusConfigTable RSU Status configration table to compare with. + * @param vector Input RSU fields to verify + * @return True if all required fields found. Otherwise, false. + */ + bool validateAllRequiredFieldsPresent(const RSUHealthMonitor::RSUStatusConfigTable &configTbl, const vector &fields) const; + + /** + * @brief Parse NMEA GPS sentense and return GPS related data + * @param gps_nmea_data NMEA GPS sentense + * @return map A map of latitude and longitude + */ + std::map ParseRSUGPS(const std::string &gps_nmea_data) const; + + /** + * @brief Sending SNMP V3 requests to get info for each field in the RSUStatusConfigTable, and return the RSU status in JSON + * Use RSU Status configuration table include RSU field, OIDs, and whether fields are required or optional + * @param RSUMibVersion The RSU MIB version used + * @param string RSU IP address + * @param uint16_t SNMP port + * @param string security user used for SNMP authentication + * @param string authentication password + * @param string security level: authPriv or authNoPriv. + * @param long session time out + */ + Json::Value getRSUStatus(const RSUMibVersion &mibVersion, const string &_rsuIp, uint16_t &_snmpPort, const string &_securityUser, const string &_authPassPhrase, const string &_securityLevel, long timeout); + + /*** + *@brief Convert the JSON message into TMX message + @param Json Input Json value + @return RSUStatusMessage TMX message + */ + RSUStatusMessage convertJsonToTMXMsg(const Json::Value &json) const; + + /** + * @brief Populate Json with snmp response object. + * @param string The field that maps to an OID. + * @param snmp_response_obj The response returned by SNMP call for the OID. + * @return Json value populated with response object. + */ + Json::Value populateJson(const string &field, const snmp_response_obj &response) const; + + /** + * @brief List the keys from the input Json values + * @param Json Input JSON values + * @return vector of key strings + */ + vector getJsonKeys(const Json::Value &json) const; + + // Delete move constructor + RSUHealthMonitorWorker(RSUHealthMonitorWorker &&worker) = delete; + + // Delete copy constructor + RSUHealthMonitorWorker(RSUHealthMonitorWorker &worker) = delete; + }; +} // namespace RSUHealthMonitor diff --git a/src/v2i-hub/RSUHealthMonitorPlugin/test/main.cpp b/src/v2i-hub/RSUHealthMonitorPlugin/test/main.cpp new file mode 100644 index 000000000..ba7cd2667 --- /dev/null +++ b/src/v2i-hub/RSUHealthMonitorPlugin/test/main.cpp @@ -0,0 +1,8 @@ + +#include + +int main(int argc, char **argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/v2i-hub/RSUHealthMonitorPlugin/test/test_RSUHealthMonitorWorker.cpp b/src/v2i-hub/RSUHealthMonitorPlugin/test/test_RSUHealthMonitorWorker.cpp new file mode 100644 index 000000000..9d749f0df --- /dev/null +++ b/src/v2i-hub/RSUHealthMonitorPlugin/test/test_RSUHealthMonitorWorker.cpp @@ -0,0 +1,108 @@ +#include "RSUHealthMonitorWorker.h" +#include + +namespace RSUHealthMonitor +{ + class test_RSUHealthMonitorWorker : public ::testing::Test + { + public: + std::shared_ptr _rsuWorker = std::make_shared(); + }; + + TEST_F(test_RSUHealthMonitorWorker, GetRSUStatusConfig) + { + RSUStatusConfigTable rsuStatusConfigTbl = _rsuWorker->GetRSUStatusConfig(RSUMibVersion::RSUMIB_V_4_1); + ASSERT_EQ(14, rsuStatusConfigTbl.size()); + + rsuStatusConfigTbl = _rsuWorker->GetRSUStatusConfig(RSUMibVersion::UNKOWN_MIB_V); + ASSERT_EQ(0, rsuStatusConfigTbl.size()); + + RSUMibVersion mibVersionDefault; + rsuStatusConfigTbl = _rsuWorker->GetRSUStatusConfig(mibVersionDefault); + ASSERT_EQ(0, rsuStatusConfigTbl.size()); + } + + TEST_F(test_RSUHealthMonitorWorker, validateAllRequiredFieldsPresent) + { + auto config = _rsuWorker->GetRSUStatusConfig(RSUMibVersion::RSUMIB_V_4_1); + vector requiredFields = {"rsuID", "rsuMibVersion", "rsuFirmwareVersion", "rsuManufacturer", "rsuGpsOutputString", "rsuMode", "rsuChanStatus"}; + ASSERT_TRUE(_rsuWorker->validateAllRequiredFieldsPresent(config, requiredFields)); + + requiredFields = {"rsuID", "rsuMibVersion", "rsuFirmwareVersion"}; + ASSERT_FALSE(_rsuWorker->validateAllRequiredFieldsPresent(config, requiredFields)); + } + + TEST_F(test_RSUHealthMonitorWorker, ParseRSUGPS) + { + std::string gps_nmea_data = "$GPGGA,142440.00,3857.3065,N,07708.9734,W,2,18,0.65,86.18,M,-34.722,M,,*62"; + auto gps_map = _rsuWorker->ParseRSUGPS(gps_nmea_data); + ASSERT_EQ(1, gps_map.size()); + double expected_latitude = 38.9551; + double expected_longitude = -77.1496; + for (auto itr = gps_map.begin(); itr != gps_map.end(); itr++) + { + ASSERT_NEAR(expected_latitude, itr->first, 0.001); + ASSERT_NEAR(expected_longitude, itr->second, 0.001); + } + std::string invalid_gps_nmea_data = "$*GPGGA,invalid"; + auto gps_map_invalid = _rsuWorker->ParseRSUGPS(invalid_gps_nmea_data); + ASSERT_EQ(0, gps_map_invalid.size()); + } + + TEST_F(test_RSUHealthMonitorWorker, getRSUStatus) + { + uint16_t port = 161; + auto json = _rsuWorker->getRSUStatus(RSUMibVersion::RSUMIB_V_4_1, "127.0.0.1", port, "test", "testtesttest", "authPriv", 1000); + ASSERT_TRUE(json.empty()); + + json = _rsuWorker->getRSUStatus(RSUMibVersion::RSUMIB_V_4_1, "127.0.0.1", port, "test", "test", "authPriv", 1000); + ASSERT_TRUE(json.empty()); + + json = _rsuWorker->getRSUStatus(RSUMibVersion::RSUMIB_V_1218, "127.0.0.1", port, "test", "test", "authPriv", 1000); + ASSERT_TRUE(json.empty()); + } + + TEST_F(test_RSUHealthMonitorWorker, convertJsonToTMXMsg) + { + Json::Value json; + json["rsuID"] = "RSU4.1"; + json["rsuMode"] = 4; + auto rsuStatusTmxMsg = _rsuWorker->convertJsonToTMXMsg(json); + string expectedStr = "{\"rsuID\":\"RSU4.1\",\"rsuMode\":4}\n"; + ASSERT_EQ(expectedStr, rsuStatusTmxMsg.to_string()); + } + + TEST_F(test_RSUHealthMonitorWorker, populateJson) + { + Json::Value rsuStatusJson; + snmp_response_obj stringObj; + stringObj.type = snmp_response_obj::response_type::STRING; + std::string gps_nmea_data = "$GPGGA,142440.00,3857.3065,N,07708.9734,W,2,18,0.65,86.18,M,-34.722,M,,*62"; + vector rgps_nmea_data_c; + copy(gps_nmea_data.begin(), gps_nmea_data.end(), back_inserter(rgps_nmea_data_c)); + stringObj.val_string = rgps_nmea_data_c; + + auto json = _rsuWorker->populateJson("rsuGpsOutputString", stringObj); + double expected_latitude = 38.9551; + double expected_longitude = -77.1496; + ASSERT_NEAR(expected_latitude, json["rsuGpsOutputStringLatitude"].asDouble(), 0.001); + ASSERT_NEAR(expected_longitude, json["rsuGpsOutputStringLongitude"].asDouble(), 0.001); + rsuStatusJson.append(json); + + snmp_response_obj intObj; + intObj.type = snmp_response_obj::response_type::INTEGER; + intObj.val_int = 4; + + json = _rsuWorker->populateJson("rsuMode", intObj); + ASSERT_EQ(4, json["rsuMode"].asInt64()); + rsuStatusJson.append(json); + + Json::FastWriter fasterWirter; + string json_str = fasterWirter.write(rsuStatusJson); + string expectedStr = "[{\"rsuGpsOutputString\":\"$GPGGA,142440.00,3857.3065,N,07708.9734,W,2,18,0.65,86.18,M,-34.722,M,,*62\",\"rsuGpsOutputStringLatitude\":38.955108330000002,\"rsuGpsOutputStringLongitude\":-77.149556669999996},{\"rsuMode\":4}]\n"; + ASSERT_EQ(expectedStr, json_str); + ASSERT_EQ(4, _rsuWorker->getJsonKeys(rsuStatusJson).size()); + ASSERT_EQ(1, _rsuWorker->getJsonKeys(json).size()); + } + +} \ No newline at end of file