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