1
0
Fork 0
mirror of https://git.rwth-aachen.de/acs/public/villas/node/ synced 2025-03-09 00:00:00 +01:00
VILLASnode/lib/nodes/ngsi.cpp
Steffen Vogel e5ab276566 fix: Formatting
Signed-off-by: Steffen Vogel <post@steffenvogel.de>
2024-11-04 10:30:14 +01:00

838 lines
22 KiB
C++

/* Node type: OMA Next Generation Services Interface 9 (NGSI) (FIWARE context broker).
*
* Author: Steffen Vogel <post@steffenvogel.de>
* SPDX-FileCopyrightText: 2014-2023 Institute for Automation of Complex Power Systems, RWTH Aachen University
* SPDX-License-Identifier: Apache-2.0
*/
#include <cstdio>
#include <cstring>
#include <curl/curl.h>
#include <jansson.h>
#include <openssl/err.h>
#include <openssl/opensslv.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <villas/exceptions.hpp>
#include <villas/node/config.hpp>
#include <villas/node_compat.hpp>
#include <villas/nodes/ngsi.hpp>
#include <villas/super_node.hpp>
#include <villas/timing.hpp>
#include <villas/utils.hpp>
using namespace villas;
using namespace villas::node;
using namespace villas::utils;
// Some global settings
#if OPENSSL_VERSION_NUMBER < 0x010003000L
// See: https://curl.haxx.se/libcurl/c/opensslthreadlock.html
#define CURL_SSL_REQUIRES_LOCKING
#endif
#ifdef CURL_SSL_REQUIRES_LOCKING
// This array will store all of the mutexes available to OpenSSL.
static pthread_mutex_t *mutex_buf = NULL;
static void handle_error(const char *file, int lineno, const char *msg) {
auto logger = Log::get("curl");
logger->error("** {}:{} {}", file, lineno, msg);
ERR_print_errors_fp(stderr);
// exit(-1);
}
static void curl_ssl_locking_function(int mode, int n, const char *file,
int line) {
if (mode & CRYPTO_LOCK)
pthread_mutex_lock(&mutex_buf[n]);
else
pthread_mutex_unlock(&mutex_buf[n]);
}
static unsigned long curl_ssl_thread_id_function(void) {
return ((unsigned long)pthread_self());
}
#endif // CURL_SSL_REQUIRES_LOCKING
enum NgsiFlags {
NGSI_ENTITY_ATTRIBUTES_IN = (1 << 0),
NGSI_ENTITY_ATTRIBUTES_OUT = (1 << 1),
NGSI_ENTITY_ATTRIBUTES =
NGSI_ENTITY_ATTRIBUTES_IN | NGSI_ENTITY_ATTRIBUTES_OUT,
NGSI_ENTITY_VALUES = (1 << 2),
NGSI_ENTITY_METADATA = (1 << 3),
};
class NgsiMetadata {
public:
NgsiMetadata(json_t *json) { parse(json); }
NgsiMetadata(const std::string &nam, const std::string &typ,
const std::string &val)
: name(nam), type(typ), value(val) {}
void parse(json_t *json) {
int ret;
json_error_t err;
const char *nam, *typ, *val;
ret = json_unpack_ex(json, &err, 0, "{ s: s, s: s, s: s }", "name", &nam,
"type", &typ, "value", &val);
if (ret)
throw ConfigError(json, "node-config-node-ngsi-metadata",
"Failed to parse NGSI metadata");
name = nam;
type = typ;
value = val;
}
std::string name;
std::string type;
std::string value;
};
class NgsiAttribute {
public:
std::string name;
std::string type;
size_t index;
std::list<NgsiMetadata> metadata;
NgsiAttribute(json_t *json, size_t j, Signal::Ptr s) { parse(json, j, s); }
void parse(json_t *json, size_t j, Signal::Ptr s) {
int ret;
json_error_t err;
json_t *json_metadatas = nullptr;
const char *nam = nullptr;
const char *typ = nullptr;
ret = json_unpack_ex(json, &err, 0, "{ s?: s, s?: s, s?: o }",
"ngsi_attribute_name", &nam, "ngsi_attribute_type",
&typ, "ngsi_metadatas", &json_metadatas);
if (ret)
throw ConfigError(json, err, "node-config-node-ngsi",
"Failed to parse NGSI attribute");
// Copy values from node signal, if 'ngsi_attribute' settings not provided
if (s && !nam)
nam = !s->name.empty() ? s->name.c_str() : "";
if (s && !typ)
typ = !s->unit.empty() ? s->unit.c_str() : "";
name = nam;
type = typ;
index = j;
if (json_metadatas) {
if (!json_is_array(json_metadatas))
throw ConfigError(json_metadatas, "node-config-node-ngsi-metadata",
"ngsi_metadata must be a list of objects");
json_t *json_metadata;
// cppcheck-suppress unknownMacro
json_array_foreach(json_metadatas, j, json_metadata)
metadata.emplace_back(json_metadata);
}
// Metadata: index(integer)=j
metadata.emplace_back("index", "integer", fmt::format("{}", j));
}
json_t *build(const struct Sample *const smps[], unsigned cnt, int flags) {
json_t *json_attribute =
json_pack("{ s: s, s: s }", "name", name.c_str(), "type", type.c_str());
if (flags & NGSI_ENTITY_VALUES) {
#if NGSI_VECTORS
// Build value vector
json_t *json_value = json_array();
for (unsigned k = 0; k < cnt; k++) {
const auto *smp = &smps[k];
const auto *data = &smp->data[index];
auto sig = smp->signals->getByIndex(index);
json_array_append_new(
json_value, json_pack("[ f, o, i ]", time_to_double(smp->ts.origin),
data->toJson(sig->type), smp->sequence));
}
#else
const auto *smp = smps[0];
const auto sig = smp->signals->getByIndex(index);
const auto *data = &smp->data[index];
json_t *json_value = data->toJson(sig->type);
#endif
json_object_set(json_attribute, "value", json_value);
}
if (flags & NGSI_ENTITY_METADATA) { // Create Metadata for attribute
json_t *json_metadatas = json_array();
for (auto &meta : metadata) {
json_array_append_new(
json_metadatas,
json_pack("{ s: s, s: s, s: s }", "name", meta.name.c_str(), "type",
meta.type.c_str(), "value", meta.value.c_str()));
}
json_object_set(json_attribute, "metadatas", json_metadatas);
}
return json_attribute;
}
};
struct ngsi_response {
char *data;
size_t len;
};
static json_t *ngsi_build_entity(NodeCompat *n,
const struct Sample *const smps[],
unsigned cnt, int flags) {
auto *i = n->getData<struct ngsi>();
json_t *json_entity = json_pack("{ s: s, s: s, s: b }", "id", i->entity_id,
"type", i->entity_type, "isPattern", 0);
if (flags & NGSI_ENTITY_ATTRIBUTES) {
json_t *json_attrs = json_array();
if (flags & NGSI_ENTITY_ATTRIBUTES_IN) {
for (size_t j = 0; j < list_length(&i->in.signals); j++) {
auto *attr = (NgsiAttribute *)list_at(&i->in.signals, j);
auto *json_attr = attr->build(smps, cnt, flags);
json_array_append_new(json_attrs, json_attr);
}
}
if (flags & NGSI_ENTITY_ATTRIBUTES_OUT) {
for (size_t j = 0; j < list_length(&i->out.signals); j++) {
auto *attr = (NgsiAttribute *)list_at(&i->out.signals, j);
auto *json_attr = attr->build(smps, cnt, flags);
json_array_append_new(json_attrs, json_attr);
}
}
json_object_set(json_entity, "attributes", json_attrs);
}
return json_entity;
}
static int ngsi_parse_entity(NodeCompat *n, json_t *json_entity,
struct Sample *const smps[], unsigned cnt) {
int ret, length = 0;
const char *id, *name, *type;
auto *i = n->getData<struct ngsi>();
size_t l;
json_error_t err;
json_t *json_attr, *json_attrs;
ret = json_unpack_ex(json_entity, &err, 0, "{ s: s, s: s, s: o }", "id", &id,
"type", &type, "attributes", &json_attrs);
if (ret || !json_is_array(json_attrs))
return -1;
if (strcmp(id, i->entity_id) || strcmp(type, i->entity_type))
return -2;
json_array_foreach(json_attrs, l, json_attr) {
NgsiAttribute *attr;
json_error_t err;
json_t *json_metadata, *json_value;
char *end;
const char *value;
// Parse JSON
ret = json_unpack_ex(json_attr, &err, 0, "{ s: s, s: s, s: o, s?: o }",
"name", &name, "type", &type, "value", &json_value,
"metadatas", &json_metadata);
if (ret)
return -3;
// Check attribute name and type
attr = list_lookup_name<NgsiAttribute>(&i->in.signals, name);
if (!attr || attr->type != type)
continue; // skip unknown attributes
length++;
// Check metadata
if (!json_is_array(json_metadata))
return -5;
#ifdef NGSI_VECTORS
json_t *json_tuple;
const char *ts, *seq;
// Check number of values
if (!json_is_array(json_value) || json_array_size(json_value) != cnt)
return -6;
size_t k;
json_array_foreach(json_value, k, json_tuple) {
struct Sample *smp = smps[k];
// Check sample format
if (!json_is_array(json_tuple) || json_array_size(json_tuple) != 3)
return -7;
ret =
json_unpack_ex(json_tuple, &err, 0, "[ s, s, s ]", &ts, &value, &seq);
if (ret)
return -8;
smp->sequence = atoi(seq);
struct timespec tss = time_from_double(strtod(ts, &end));
if (ts == end)
return -9;
smp->ts.origin = tss;
auto *sd = &smp->data[attr->index];
auto sig = n->getInputSignals(false)->getByIndex(attr->index);
if (!sig)
return -11;
if (value[0] == '\0') // No data on Orion CB? -> Use init value
*sd = sig->init;
else {
signal_data_parse_str(sd, sig->type, value, &end);
if (value == end)
return -10;
}
}
#else
struct Sample *smp = smps[0];
// Check number of values
if (!json_is_string(json_value))
return -6;
value = json_string_value(json_value);
auto *data = &smp->data[attr->index];
auto sig = n->getInputSignals(false)->getByIndex(attr->index);
if (!sig)
return -11;
if (value[0] == '\0') // No data on Orion CB? -> Use init value
*data = sig->init;
else {
data->parseString(sig->type, value, &end);
if (value == end)
return -10;
}
#endif
}
for (unsigned k = 0; k < cnt; k++) {
struct Sample *smp = smps[k];
smp->length = length;
smp->signals = n->getInputSignals(false);
smp->flags = (int)SampleFlags::HAS_DATA;
#ifdef NGSI_VECTORS
smp->flags |= (int)(SampleFlags::HAS_SEQUENCE | SampleFlags::HAS_TS_ORIGIN);
#endif
}
return cnt;
}
static int ngsi_parse_signals(json_t *json_signals, struct List *ngsi_signals,
SignalList::Ptr node_signals) {
size_t j;
json_t *json_signal;
if (!json_is_array(json_signals))
return -1;
json_array_foreach(json_signals, j, json_signal) {
auto s = node_signals->getByIndex(j);
auto *a = new NgsiAttribute(json_signal, j, s);
if (!a)
throw MemoryAllocationError();
list_push(ngsi_signals, a);
}
return 0;
}
static int ngsi_parse_context_response(json_t *json_response, int *code,
char **reason, json_t **json_rentity,
Logger logger) {
int ret;
char *codestr;
json_error_t err;
ret = json_unpack_ex(json_response, &err, 0,
"{ s: [ { s: O, s: { s: s, s: s } } ] }",
"contextResponses", "contextElement", json_rentity,
"statusCode", "code", &codestr, "reasonPhrase", reason);
if (ret) {
logger->warn("Failed to find NGSI response code");
return ret;
}
*code = atoi(codestr);
if (*code != 200)
logger->warn("NGSI response: {} {}", codestr, *reason);
return ret;
}
static size_t ngsi_request_writer(void *contents, size_t size, size_t nmemb,
void *userp) {
size_t realsize = size * nmemb;
struct ngsi_response *mem = (struct ngsi_response *)userp;
mem->data = (char *)realloc(mem->data, mem->len + realsize + 1);
if (mem->data == nullptr) // out of memory!
throw MemoryAllocationError();
memcpy(&(mem->data[mem->len]), contents, realsize);
mem->len += realsize;
mem->data[mem->len] = 0;
return realsize;
}
static int ngsi_request(CURL *handle, const char *endpoint,
const char *operation, json_t *json_request,
json_t **json_response, Logger logger) {
struct ngsi_response chunk = {0};
char *post = json_dumps(json_request, JSON_INDENT(4));
int old;
double time;
char url[128];
json_error_t err;
snprintf(url, sizeof(url), "%s/v1/%s", endpoint, operation);
curl_easy_setopt(handle, CURLOPT_URL, url);
curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, ngsi_request_writer);
curl_easy_setopt(handle, CURLOPT_WRITEDATA, (void *)&chunk);
curl_easy_setopt(handle, CURLOPT_POSTFIELDSIZE, strlen(post));
curl_easy_setopt(handle, CURLOPT_POSTFIELDS, post);
logger->debug("Request to context broker: {}\n{}", url, post);
// We don't want to leave the handle in an invalid state
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old);
CURLcode ret = curl_easy_perform(handle);
pthread_setcancelstate(old, nullptr);
if (ret) {
logger->warn("HTTP request failed: {}", curl_easy_strerror(ret));
goto out;
}
curl_easy_getinfo(handle, CURLINFO_TOTAL_TIME, &time);
logger->debug("Request to context broker completed in {} seconds", time);
logger->debug("Response from context broker:\n{}", chunk.data);
*json_response = json_loads(chunk.data, 0, &err);
if (!*json_response)
logger->warn("Received invalid JSON: {} in {}:{}:{}\n{}", err.text,
err.source, err.line, err.column, chunk.data);
out:
free(post);
free(chunk.data);
return ret;
}
static int ngsi_request_context_query(CURL *handle, const char *endpoint,
json_t *json_entity,
json_t **json_rentity, Logger logger) {
int ret, code;
char *reason;
json_t *json_response;
json_t *json_request = json_pack("{ s: [ O ] }", "entities", json_entity);
ret = ngsi_request(handle, endpoint, "queryContext", json_request,
&json_response, logger);
if (ret) {
ret = -1;
goto out;
}
ret = ngsi_parse_context_response(json_response, &code, &reason, json_rentity,
logger);
if (ret)
goto out2;
out2:
json_decref(json_response);
out:
json_decref(json_request);
return ret;
}
static int ngsi_request_context_update(CURL *handle, const char *endpoint,
const char *action, json_t *json_entity,
Logger logger) {
int ret, code;
char *reason;
json_t *json_response;
json_t *json_request = json_pack("{ s: s, s: [ O ] }", "updateAction", action,
"contextElements", json_entity);
ret = ngsi_request(handle, endpoint, "updateContext", json_request,
&json_response, logger);
if (ret)
goto out;
json_t *json_rentity;
ret = ngsi_parse_context_response(json_response, &code, &reason,
&json_rentity, logger);
if (ret)
goto out2;
json_decref(json_rentity);
out2:
json_decref(json_response);
out:
json_decref(json_request);
return ret;
}
int villas::node::ngsi_type_start(villas::node::SuperNode *sn) {
#ifdef CURL_SSL_REQUIRES_LOCKING
mutex_buf = new pthread_mutex_t[CRYPTO_num_locks()];
if (!mutex_buf)
return -1;
for (int i = 0; i < CRYPTO_num_locks(); i++)
pthread_mutex_init(&mutex_buf[i], nullptr);
CRYPTO_set_id_callback(curl_ssl_thread_id_function);
CRYPTO_set_locking_callback(curl_ssl_locking_function);
auto logger = Log::get("curl");
logger->info("Setup libcurl/openssl locking primitives");
#endif // CURL_SSL_REQUIRES_LOCKING
return curl_global_init(CURL_GLOBAL_ALL);
}
int villas::node::ngsi_type_stop() {
#ifdef CURL_SSL_REQUIRES_LOCKING
if (!mutex_buf)
return -1;
CRYPTO_set_id_callback(NULL);
CRYPTO_set_locking_callback(NULL);
for (int i = 0; i < CRYPTO_num_locks(); i++)
pthread_mutex_destroy(&mutex_buf[i]);
delete mutex_buf;
#endif // CURL_SSL_REQUIRES_LOCKING
curl_global_cleanup();
return 0;
}
int villas::node::ngsi_parse(NodeCompat *n, json_t *json) {
auto *i = n->getData<struct ngsi>();
int ret;
json_error_t err;
json_t *json_signals_in = nullptr;
json_t *json_signals_out = nullptr;
int create = 1;
int remove = 1;
ret = json_unpack_ex(json, &err, 0,
"{ s?: s, s: s, s: s, s: s, s?: b, s?: F, s?: F, s?: b, "
"s?: b, s?: { s?: o }, s?: { s?: o } }",
"access_token", &i->access_token, "endpoint",
&i->endpoint, "entity_id", &i->entity_id, "entity_type",
&i->entity_type, "ssl_verify", &i->ssl_verify, "timeout",
&i->timeout, "rate", &i->rate, "create", &create,
"delete", &remove, "in", "signals", &json_signals_in,
"out", "signals", &json_signals_out);
if (ret)
throw ConfigError(json, err, "node-config-node-ngsi");
i->create = create;
i->remove = remove;
if (json_signals_in) {
ret = ngsi_parse_signals(json_signals_in, &i->in.signals, n->in.signals);
if (ret)
throw ConfigError(json_signals_in, "node-config-node-ngsi-in-signals",
"Invalid setting 'in.signals' of node {}",
n->getName());
}
if (json_signals_out) {
ret = ngsi_parse_signals(json_signals_out, &i->out.signals, n->out.signals);
if (ret)
throw ConfigError(json_signals_out, "node-config-node-ngsi-out-signals",
"Invalid setting 'out.signals' of node {}",
n->getName());
}
return 0;
}
char *villas::node::ngsi_print(NodeCompat *n) {
auto *i = n->getData<struct ngsi>();
return strf("endpoint=%s, timeout=%.3f secs", i->endpoint, i->timeout);
}
int villas::node::ngsi_start(NodeCompat *n) {
auto *i = n->getData<struct ngsi>();
i->in.curl = curl_easy_init();
i->out.curl = curl_easy_init();
i->headers = nullptr;
if (i->access_token) {
char buf[128];
snprintf(buf, sizeof(buf), "Auth-Token: %s", i->access_token);
i->headers = curl_slist_append(i->headers, buf);
}
// Create task
if (i->timeout > 1 / i->rate)
n->logger->warn("Timeout is to large for given rate: {}", i->rate);
i->task.setRate(i->rate);
i->headers = curl_slist_append(i->headers, "Accept: application/json");
i->headers = curl_slist_append(i->headers, "Content-Type: application/json");
CURL *handles[] = {i->in.curl, i->out.curl};
for (unsigned p = 0; p < ARRAY_LEN(handles); p++) {
curl_easy_setopt(handles[p], CURLOPT_SSL_VERIFYPEER, i->ssl_verify);
curl_easy_setopt(handles[p], CURLOPT_TIMEOUT_MS, i->timeout * 1e3);
curl_easy_setopt(handles[p], CURLOPT_HTTPHEADER, i->headers);
curl_easy_setopt(handles[p], CURLOPT_USERAGENT, HTTP_USER_AGENT);
}
// Create entity and atributes
if (i->create) {
json_t *json_entity = ngsi_build_entity(
n, nullptr, 0, NGSI_ENTITY_ATTRIBUTES | NGSI_ENTITY_METADATA);
int ret = ngsi_request_context_update(i->out.curl, i->endpoint, "APPEND",
json_entity, n->logger);
if (ret)
throw RuntimeError("Failed to create NGSI context for node {}",
n->getName());
json_decref(json_entity);
}
return 0;
}
int villas::node::ngsi_stop(NodeCompat *n) {
auto *i = n->getData<struct ngsi>();
int ret;
i->task.stop();
// Delete complete entity (not just attributes)
json_t *json_entity = ngsi_build_entity(n, nullptr, 0, 0);
ret = ngsi_request_context_update(i->out.curl, i->endpoint, "DELETE",
json_entity, n->logger);
json_decref(json_entity);
curl_easy_cleanup(i->in.curl);
curl_easy_cleanup(i->out.curl);
curl_slist_free_all(i->headers);
return ret;
}
int villas::node::ngsi_read(NodeCompat *n, struct Sample *const smps[],
unsigned cnt) {
auto *i = n->getData<struct ngsi>();
int ret;
if (i->task.wait() == 0)
throw SystemError("Failed to wait for task");
json_t *json_rentity;
json_t *json_entity = ngsi_build_entity(n, nullptr, 0, 0);
ret = ngsi_request_context_query(i->in.curl, i->endpoint, json_entity,
&json_rentity, n->logger);
if (ret)
goto out;
ret = ngsi_parse_entity(n, json_rentity, smps, cnt);
if (ret)
goto out2;
out2:
json_decref(json_rentity);
out:
json_decref(json_entity);
return ret;
}
int villas::node::ngsi_write(NodeCompat *n, struct Sample *const smps[],
unsigned cnt) {
auto *i = n->getData<struct ngsi>();
int ret;
json_t *json_entity = ngsi_build_entity(
n, smps, cnt, NGSI_ENTITY_ATTRIBUTES_OUT | NGSI_ENTITY_VALUES);
ret = ngsi_request_context_update(i->out.curl, i->endpoint, "UPDATE",
json_entity, n->logger);
json_decref(json_entity);
return ret ? 0 : cnt;
}
int villas::node::ngsi_poll_fds(NodeCompat *n, int fds[]) {
auto *i = n->getData<struct ngsi>();
fds[0] = i->task.getFD();
return 1;
}
int villas::node::ngsi_init(NodeCompat *n) {
int ret;
auto *i = n->getData<struct ngsi>();
new (&i->task) Task();
ret = list_init(&i->in.signals);
if (ret)
return ret;
ret = list_init(&i->out.signals);
if (ret)
return ret;
// Default values
i->access_token = nullptr; // disabled by default
i->ssl_verify = 1; // verify by default
i->timeout = 1; // default value
i->rate = 1; // default value
return 0;
}
int villas::node::ngsi_destroy(NodeCompat *n) {
int ret;
auto *i = n->getData<struct ngsi>();
for (size_t j = 0; j < list_length(&i->in.signals); j++) {
auto *attr = (NgsiAttribute *)list_at(&i->in.signals, j);
delete attr;
}
for (size_t j = 0; j < list_length(&i->out.signals); j++) {
auto *attr = (NgsiAttribute *)list_at(&i->out.signals, j);
delete attr;
}
ret = list_destroy(&i->in.signals);
if (ret)
return ret;
ret = list_destroy(&i->out.signals);
if (ret)
return ret;
i->task.~Task();
return 0;
}
int villas::node::ngsi_reverse(NodeCompat *n) {
auto *i = n->getData<struct ngsi>();
n->swapSignals();
SWAP(i->in.signals, i->out.signals);
return 0;
}
static NodeCompatType p;
__attribute__((constructor(110))) static void register_plugin() {
p.name = "ngsi";
p.description =
"OMA Next Generation Services Interface 10 (libcurl, libjansson)";
#ifdef NGSI_VECTORS
p.vectorize = 0, // unlimited
#else
p.vectorize = 1,
#endif
p.size = sizeof(struct ngsi);
p.type.start = ngsi_type_start;
p.type.stop = ngsi_type_stop;
p.init = ngsi_init;
p.destroy = ngsi_destroy;
p.parse = ngsi_parse;
p.print = ngsi_print;
p.start = ngsi_start;
p.stop = ngsi_stop;
p.read = ngsi_read;
p.write = ngsi_write;
p.poll_fds = ngsi_poll_fds;
p.reverse = ngsi_reverse;
static NodeCompatFactory ncp(&p);
}