thomas vor 4 Jahren
Commit
8c0700da25

+ 11 - 0
.gitignore

@@ -0,0 +1,11 @@
+build
+build-arm
+nbproject
+CMakeLists.txt.user
+.vscode
+.vs
+CMakeSettings.json
+out
+CMakeFiles
+build-raspberry
+

+ 9 - 0
.gitmodules

@@ -0,0 +1,9 @@
+[submodule "extern/open62541"]
+	path = extern/open62541
+	url = https://github.com/open62541/open62541.git
+[submodule "extern/libconfig"]
+	path = extern/libconfig
+	url = https://github.com/hyperrealm/libconfig.git
+[submodule "extern/software-watchdog"]
+	path = extern/software-watchdog
+	url = https://intra.acdp.at/gogs/thomas/software-watchdog.git

+ 5 - 0
CMakeLists.txt

@@ -0,0 +1,5 @@
+cmake_minimum_required (VERSION 2.6)
+project(ua-stress-test)
+set(CMAKE_BUILD_TYPE DEBUG)
+
+add_subdirectory(src)

+ 23 - 0
README.md

@@ -0,0 +1,23 @@
+# UA Stress Test
+
+The ua-stress-test program ca be used to test the behavior of an OPC UA server under heavy load. It opens a configurable number of OPC UA connections to the server and sends a configurable number of read requests per second to the server.
+
+## Pulling the repo and all submodules
+
+This repo contains multiple submodules. To get everything you need, clone this repo to your computer and get the required submodules:
+
+```shell
+git clone https://intra.acdp.at/gogs/opc-ua/ua-stress-test.git
+git submodule update --init --recursive
+```
+
+## Building locally
+
+```shell
+sudo apt install libconfig-dev
+mkdir build
+cd build
+cmake ..
+make
+```
+

+ 1 - 0
extern/open62541

@@ -0,0 +1 @@
+Subproject commit 30ac5ddd892e6ce82bef4de7890f06854aca9736

+ 8 - 0
src/CMakeLists.txt

@@ -0,0 +1,8 @@
+cmake_minimum_required (VERSION 2.6)
+
+option(BUILD_UASTRESSTEST "Build the OPC UA Client that performs the stress test" ON)
+
+if(BUILD_UASTRESSTEST)
+    add_subdirectory(ua-stress-test)
+endif()
+

+ 28 - 0
src/ua-stress-test/CMakeLists.txt

@@ -0,0 +1,28 @@
+cmake_minimum_required (VERSION 2.6)
+
+set(EXTERNALS_DIR ../../extern)
+
+# by default, do not build any tests, examples or shared libraries
+set(BUILD_TESTS OFF CACHE BOOL "Enable tests")
+set(BUILD_EXAMPLES OFF CACHE BOOL "Enable examples")
+set(BUILD_SHARED_LIBS OFF CACHE BOOL "Enable shared libs")
+
+# build open62541
+set(OPEN62541_DIR ${EXTERNALS_DIR}/open62541)
+# set(UA_NAMESPACE_ZERO FULL CACHE STRING "open62541 namespace zero MINIMAL, REDUCED, FULL")
+# set(UA_ENABLE_PUBSUB ON CACHE BOOL "Enable open62541 PubSub information model")
+# set(UA_ENABLE_PUBSUB_INFORMATIONMODEL ON CACHE BOOL "Enable open62541 PubSub information model")
+add_subdirectory(${OPEN62541_DIR} open62541)
+
+# build the ua-stress-test itself
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/ua-stress-test")
+set(UASTRESSTEST_SOURCE_FILES 
+    helper_functions.cpp
+    main.cpp
+    )
+
+add_executable(ua-stress-test ${UASTRESSTEST_SOURCE_FILES})
+target_link_libraries(ua-stress-test PUBLIC
+    open62541
+    config)
+file(COPY config.cfg DESTINATION "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")

+ 8 - 0
src/ua-stress-test/config.cfg

@@ -0,0 +1,8 @@
+NumberOfConnections:1;
+OpcUaReadsPerConnectionPerS:1;
+EnableConsoleOutput:true;
+
+OpcUa:
+{
+    ServerAddress:"opc.tcp://192.168.40.15:48030";
+};

+ 116 - 0
src/ua-stress-test/helper_functions.cpp

@@ -0,0 +1,116 @@
+#include <open62541/server.h>
+#include <open62541/plugin/log_stdout.h>
+#include "helper_functions.h"
+#include <libconfig.h>
+// C++ includes
+#include <iostream>
+
+using namespace std;
+UA_StatusCode setVariable(UA_Server* server, void *value, const UA_DataType *dataType, UA_NodeId variableNodeid) {
+    UA_StatusCode res;
+    UA_Variant var;
+    UA_Variant_setScalar(&var, value, dataType);
+    res = UA_Server_writeValue(server, variableNodeid, var);
+    if(res != UA_STATUSCODE_GOOD)
+        UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Failed to init variable node NS=%d,i=%d. Error: %s\n", variableNodeid.namespaceIndex, variableNodeid.identifier.numeric, UA_StatusCode_name(res));
+    return res;
+}
+
+UA_StatusCode setEURangeVariable(UA_Server* server, UA_Double low, UA_Double high, UA_NodeId euRangeVariableNodeid) {
+    UA_StatusCode res;
+    UA_Range range;
+    UA_Range_init(&range);
+    range.low = low;
+    range.high = high;
+    res = setVariable(server, &range, &UA_TYPES[UA_TYPES_RANGE], euRangeVariableNodeid);
+    if(res != UA_STATUSCODE_GOOD)
+        UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Failed to set range for node NS=%d,i=%d. Error: %s\n", euRangeVariableNodeid.namespaceIndex, euRangeVariableNodeid.identifier.numeric, UA_StatusCode_name(res));
+    return res ;
+}
+
+// UA State-machine handling
+bool inState(UA_Server* server, UA_NodeId currentStateIdNodeId, UA_NodeId stateNodeId) {
+    UA_Variant out;
+    UA_Variant_init(&out);
+    UA_Server_readValue(server, currentStateIdNodeId, &out);
+    UA_NodeId *currentStateId = (UA_NodeId*)(out.data);
+    if(UA_NodeId_equal(currentStateId, &stateNodeId))
+        return true;
+    return false;
+}
+
+UA_StatusCode setCurrentState(UA_Server *server, UA_NodeId currentStateVariableNodeId, UA_NodeId currentStateVariableIdNodeId, UA_NodeId newStateNodeId) {
+    // set the Id property of the CurrentState variable to newStateNodeId
+    UA_Variant var;
+    UA_Variant_init(&var);
+    UA_Variant_setScalar(&var, &newStateNodeId, &UA_TYPES[UA_TYPES_NODEID]);
+    UA_Server_writeValue(server, currentStateVariableIdNodeId, var);
+    
+    // set the CurrentState variable to a human-readable string
+    UA_LocalizedText stateName;
+    UA_LocalizedText_init(&stateName);
+    UA_Server_readDisplayName(server, newStateNodeId, &stateName);
+    UA_Variant_setScalar(&var, &stateName, &UA_TYPES[UA_TYPES_LOCALIZEDTEXT]);
+    UA_Server_writeValue(server, currentStateVariableNodeId, var);
+}
+
+
+
+// Configuration file handling
+bool getIntFromConfig(const config_t* cfg, const char* name, int *res) {
+    if (!config_lookup_int(cfg, name, res)) {
+        cerr << "No " << name << " setting in configuration file." << endl;
+        return false;
+    }
+    return true;
+}
+
+bool getBoolFromConfig(const config_t* cfg, const char* name, bool *res) {
+    int resInt;
+    *res = false;
+    if (!config_lookup_bool(cfg, name, &resInt)) {
+        cerr << "No " << name << " setting in configuration file." << endl;
+        return false;
+    }
+    if (resInt != 0)
+        *res = true;
+
+    return true;
+}
+
+bool getStringFromConfig(const config_t* cfg, const char* name, char **res) {
+    if (!config_lookup_string(cfg, name, (const char **) res)) {
+        cerr << "No " << name << " setting in configuration file." << endl;
+        return false;
+    }
+    return true;
+}
+
+bool getStringListFromConfig(const config_t* cfg, const char* name, char **res, int *listLength) {
+    config_setting_t *setting = config_lookup(cfg, name);
+    *listLength = config_setting_length(setting);
+
+    if (*listLength == 0) {
+        cerr << "No " << name << " setting in configuration file or wrong format. Note that a list of strings should look like this: (\"item1\", \"item2\")." << endl;
+        return false;
+    }
+
+    for(int i = 0; i < *listLength; i++){
+        const char *elem = config_setting_get_string_elem (setting, i);
+        if (elem == NULL) {
+        cerr << "No " << name << " setting in configuration file or wrong format. Note that a list of strings should look like this: (\"item1\", \"item2\")." << endl;
+            return false;
+        }
+        res[i] = strdup(elem);
+    }
+    return true;
+}
+
+bool getDoubleFromConfig(const config_t* cfg, const char* name, double *res) {
+    if (!config_lookup_float(cfg, name, res)) {
+        cerr << "No " << name << " setting in configuration file." << endl;
+        return false;
+    }
+    return true;
+}
+

+ 34 - 0
src/ua-stress-test/helper_functions.h

@@ -0,0 +1,34 @@
+#ifndef HELPER_FUNCTIONS_H
+#define HELPER_FUNCTIONS_H
+
+#include <open62541/server.h>
+#include <libconfig.h>
+
+
+/* Sets an OPC UA variable
+*/
+UA_StatusCode setVariable(UA_Server* server, void *value, const UA_DataType *dataType, UA_NodeId variableNodeid);
+
+/* Sets and OPC UA range variable with min and max value
+*/
+UA_StatusCode setEURangeVariable(UA_Server* server, UA_Double low, UA_Double high, UA_NodeId euRangeVariableNodeid);
+
+// UA State-machine handling
+bool inState(UA_Server* server, UA_NodeId currentStateIdNodeId, UA_NodeId stateNodeId);
+
+UA_StatusCode setCurrentState(UA_Server *server, UA_NodeId currentStateVariableNodeId, UA_NodeId currentStateVariableIdNodeId, UA_NodeId newStateNodeId);
+
+
+// Configuration file handling
+bool getIntFromConfig(const config_t* cfg, const char* name, int *res);
+
+bool getBoolFromConfig(const config_t* cfg, const char* name, bool *res);
+
+bool getStringFromConfig(const config_t* cfg, const char* name, char **res);
+
+bool getStringListFromConfig(const config_t* cfg, const char* name, char **res, int *listLength);
+
+bool getDoubleFromConfig(const config_t* cfg, const char* name, double *res);
+
+
+#endif /* HELPER_FUNCTIONS_H */

+ 129 - 0
src/ua-stress-test/main.cpp

@@ -0,0 +1,129 @@
+// This is a minimal example that starts a number of workerThreads and terminates them afer a key press
+
+// C includes
+#include <stdio.h>
+#include <libconfig.h>
+#include <signal.h>
+#include <open62541/client_config_default.h>
+#include <open62541/client_highlevel.h>
+
+// C++ includes
+#include <iostream>
+#include <thread>
+
+// custom includes
+#include "helper_functions.h"
+
+// OPC UA general includes
+#include <open62541/server.h>
+#include "helper_functions.h"
+#include <list>
+
+int cfg_NumberOfConnections = 10;
+int cfg_OpcUaReadsPerConnectionPerS = 1;
+bool cfg_EnableConsoleOutput;
+char *cfg_OpcUa_ServerAddress;
+
+using namespace std;
+
+bool stop = false; // when set to true, the client threads terminate
+
+void workerThread()
+{
+    static int totalThreads = 0;
+    int threadId = totalThreads++;
+    cout << "Thread " << threadId << " starting" << endl;
+
+    // open OPC UA connection to server
+    UA_Client *client = UA_Client_new();
+    UA_ClientConfig_setDefault(UA_Client_getConfig(client));
+    UA_StatusCode retval = UA_Client_connect(client, cfg_OpcUa_ServerAddress); // Should be something like "opc.tcp://localhost:4840"
+    if(retval != UA_STATUSCODE_GOOD) {
+        UA_Client_delete(client);
+        cerr << "Could not open OPC UA connection to " << cfg_OpcUa_ServerAddress << endl;
+        return;
+    }
+    
+    // TODO: maybe wait for a specific point in time to allow all threads to start before sending requests
+
+    const UA_NodeId nodeId = UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME);
+    chrono::system_clock::time_point startTime = chrono::system_clock::now();
+    unsigned long intervalUs = 1.0 / ((double)cfg_OpcUaReadsPerConnectionPerS) * 1000000.0;
+    UA_Variant value;
+    UA_DateTime raw_date; 
+    UA_DateTimeStruct dts; 
+    for(int i = 0; !stop; i++) // loop repeats every intervalUs microseconds
+    {
+        std::this_thread::sleep_until(startTime + chrono::microseconds(i * intervalUs));
+        retval = UA_Client_readValueAttribute(client, nodeId, &value);
+        if (cfg_EnableConsoleOutput) {
+            raw_date = *(UA_DateTime *) value.data;
+            dts = UA_DateTime_toStruct(raw_date);
+            cout << "Thread " << threadId << ": " << dts.day << "-" << dts.month << "-" << dts.year << "-" << dts.hour << "-" << dts.min << "-" << dts.sec << endl;
+        }
+        if (retval != UA_STATUSCODE_GOOD) {
+            UA_Client_delete(client);
+            cerr << "Could not read node NS=" << nodeId.namespaceIndex << ",i=" << nodeId.identifier.numeric << " from OPC UA server" << endl;
+            return;
+        }
+    }
+
+    cout << "Thread " << threadId << " terminating" << endl;
+}
+
+int main(int argc, char **argv) {
+    printf("This is the ua-stress-test\n");
+
+    // Check if a config file is provided via input arguments or use config.cfg
+    string configFilename = "config.cfg";
+    if (argc < 2) {
+        cout << "No configuration file specified, using " << configFilename << endl;
+    }
+    else {
+        configFilename = argv[1]; // use the input argument as confiFilename
+    }
+    // Check if the configuration file exists
+    if(-1 == access(configFilename.c_str(), 0)) {
+        cerr << "Could not find configuration file %s" << endl;
+        return EXIT_FAILURE;
+    }
+    // Load the configuration file
+    config_t cfg;
+    config_setting_t *setting;
+    config_init(&cfg);
+    if(! config_read_file(&cfg, configFilename.c_str())){
+        cerr << "Error in the config file: " << config_error_file(&cfg) << ":" << config_error_line(&cfg) << " - " << config_error_text(&cfg) << endl;
+        config_destroy(&cfg);
+        return EXIT_FAILURE;
+    }
+    // Read relevant values from the configuration file
+    bool configOk = true;
+    configOk &= getIntFromConfig(&cfg, "NumberOfConnections", &cfg_NumberOfConnections);
+    configOk &= getIntFromConfig(&cfg, "OpcUaReadsPerConnectionPerS", &cfg_OpcUaReadsPerConnectionPerS);
+    configOk &= getBoolFromConfig(&cfg, "EnableConsoleOutput", &cfg_EnableConsoleOutput);
+    configOk &= getStringFromConfig(&cfg, "OpcUa.ServerAddress", &cfg_OpcUa_ServerAddress);
+    if(!configOk){
+        config_destroy(&cfg);
+        return EXIT_FAILURE;
+    }
+    
+    // start a thread for each connection
+    list<thread> threadList;
+    for (int i = 0; i < cfg_NumberOfConnections; i++) {  // loop repeats every intervalUs microseconds
+        threadList.push_back(thread(workerThread));
+    }
+
+    cout << "Press ENTER to stop the stress test" << endl;
+    getchar();
+    cout << "Stopping..." << endl;
+
+    // wait for threads to terminate
+    stop = true;
+    list<thread>::iterator it;
+    for (it = threadList.begin(); it != threadList.end(); ++it){
+        it->join();
+    }
+
+    return EXIT_SUCCESS;
+}
+