From: Eduardo Ramos Testillano (eramedu) Date: Sun, 10 May 2020 02:46:17 +0000 (+0200) Subject: Add first work package for REST API implementation X-Git-Url: https://git.teslayout.com/public/public/public/?a=commitdiff_plain;h=c881c12ed7e116f1d43760a0d9873f860c10a357;p=anna.git Add first work package for REST API implementation INSTALL_ADML_HTTP.md documentation about: * New tools to generate ADML-HTTP deployment, which is an ADML with HTTP endpoint configured. * Docker image generation for ADML HTTP deployment together with an nginx configured as reverse proxy in order to offer an HTTP2 server. The HTTP2 requests will be proxied to native HTTP11 http server provided by ADML. * Instructions to start the docker container and execute the pytest component test EventOperation classes which will centralize event operation from Launcher.cpp and also from HTTP handler. New XML dump operations from string (not only from file) at Launcher.cpp. Modify all deployment to remove legacy operation script for curl. All of them will be based (specially "burst" dedicated deployment) on signal USR2 management. HELP.md for complete REST API documentation Pytest component test structure, pending FSM testing and flow operations New library (json to xml converter) is used to transform REST operations into native xml language. --- diff --git a/INSTALL_ADML_HTTP.md b/INSTALL_ADML_HTTP.md index 785c4dd..973862b 100644 --- a/INSTALL_ADML_HTTP.md +++ b/INSTALL_ADML_HTTP.md @@ -2,7 +2,7 @@ ## Deployment -Execute 'example/diameter/launcher/deploy-adml-http.sh' and follow instructions. +Execute this [script](./example/diameter/launcher/deploy-adml-http.sh) and follow instructions. ## Enabling HTTP2 Service @@ -14,8 +14,74 @@ To build the docker image, execute this script: ## Running docker image -> docker run --rm -d --network host --name adml-http anna-adml-http: +> docker run --rm -d -p 8074:8074 --name adml-http anna-adml-http: -## Monitoring ADML traces +Service port is **8074**. + +## Basic operations + +### Monitoring ADML traces + +> docker exec -it adml-http tail -F /opt/adml/launcher.trace + +### Checking available diameter dictionaries + +> docker exec -it adml-http ls -1 /opt/adml/stacks +> +> DictionaryGx.16777238.xml +> DictionaryRx.16777236.xml +> DictionarySy.16777302.xml +> diameter_base.0.xml + +### Configure diameter nodes + +You may use the *REST API* to load diameter services into *ADML*. This is the way to load stacks and nodes (origin hosts) in order to be up and running. + +For example, consider an Origin-Host acting as diameter client with the name: **afHost.afRealm.com** and also a node named **ownHostId.operatorRealm.com** acting as diameter server, both using the *Rx interface* ( **/stacks/DictionaryRx.16777236.xml)**. You could configure any number of stacks/nodes to setup different diameter interfaces even having proxy/relay/translation capabilities. + +You could load any number of services given by this [file specification](./example/diameter/launcher/resources/services_examples/services.dtd). Also, you could configure a single file and load together all the needed elements, for example: + +`` + `` + `` + `` +`` + +The REST API works with json format, then we shall use the [xml2json.py](./example/diameter/launcher/resources/rest_api/helpers/diameterJsonHelper/xml2json.py) tool to convert the previous one and insert on a node called "**servicesJson**": + +> { +> **"servicesJson": {** +> "services": { +> "node": [ +> { +> "@applicationId": "16777236", +> "@entity": "localhost:3868", +> "@originHost": "afHost.afRealm.com" +> }, +> { +> "@applicationId": "16777236", +> "@diameterServer": "localhost:3868", +> "@diameterServerSessions": "1", +> "@originHost": "ownHostId.operatorRealm.com" +> } +> ], +> "stack": { +> "@dictionary": "stacks/DictionaryRx.16777236.xml", +> "@fixMode": "Always", +> "@id": "16777236", +> "@ignoreFlagsOnValidation": "yes" +> } +> } +> **}** +> } + +Then, you could load the former *services.json* file: + +> nghttp -v -H ":method: POST" -d services.json http://localhost:8074/services + + + +## Component Test + +Read [this](./example/diameter/launcher/resources/rest_api/README.md). -> docker exec -it adml-http tail -F /opt/adml/launcher.trace \ No newline at end of file diff --git a/docker-images/anna-adml-http/Dockerfile b/docker-images/anna-adml-http/Dockerfile index fb75eb6..685279f 100644 --- a/docker-images/anna-adml-http/Dockerfile +++ b/docker-images/anna-adml-http/Dockerfile @@ -2,6 +2,12 @@ FROM nginx COPY etc/ /etc COPY opt/adml/ /opt/adml +RUN apt-get update && apt-get install -y \ + procps + COPY starter.sh /var/starter.sh +EXPOSE 8074 +WORKDIR /opt/adml + CMD ["sh", "/var/starter.sh"] diff --git a/example/diameter/launcher/EventOperation.cpp b/example/diameter/launcher/EventOperation.cpp index 2b80529..be0c168 100644 --- a/example/diameter/launcher/EventOperation.cpp +++ b/example/diameter/launcher/EventOperation.cpp @@ -11,9 +11,699 @@ // Process #include +#include -std::string EventOperation::json2piped(const json &j) { - std::string result = "operacion piped"; +// Project +#include + +//// Standard +//#include // std::istringstream +//#include // std::cout +//#include // ceil +//#include +#include // chdir +//#include +// +//// Project +#include +#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +#include +//#include +//#include +// +//// Process +//#include +//#include +//#include +#include +#include +//#include +//#include + + +///////////////////// +// Node management // +///////////////////// +bool EventOperation::node(std::string &response, const std::string & name) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + anna::diameter::comm::OriginHost *workingNode; + try { workingNode = my_app.getWorkingNode(); } catch(anna::RuntimeException &ex) { ex.trace(); } + + if (name != "") { + if (my_app.setWorkingNode(name)) { + response = anna::functions::asString("Forced node is now '%s'", name.c_str()); + my_app.setOperatedHost(my_app.getWorkingNode()); // now is the new one + } + else { + response = anna::functions::asString("Node '%s' invalid. Nothing done", name.c_str()); + } + } + else { + if (workingNode) { + if (a_http) { + response = anna::functions::encodeBase64(workingNode->asXMLString()); + } + else { + response = workingNode->asXMLString(); + } + } + else { + response = "Working node is automatic"; + } + } + return true; // OK +} + +bool EventOperation::node_auto(std::string &response) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + my_app.setNodeAuto(); + response = "Working node has been set to automatic"; + + return true; // OK +} + +//////////////////////// +// Parsing operations // +//////////////////////// +bool EventOperation::code(std::string &response, const std::string & diameterJson) { + + bool success; + std::string diameterXml = anna::json::functions::json2xml(diameterJson, success); + if (!success) { + response += "json to xml failed, unable to encode !"; + return false; + } + anna::diameter::codec::Message codecMsg; // auxiliary codec message + codecMsg.loadXMLString(diameterXml); + response = anna::functions::asHexString(codecMsg.code()); + + return true; // OK +} + +bool EventOperation::decode(std::string &response, const std::string & diameterHex) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + anna::DataBlock db_aux(true); + anna::functions::fromHexString(diameterHex, db_aux); + anna::diameter::codec::Message codecMsg; // auxiliary codec message + try { + codecMsg.decode(db_aux); + std::string xmlString = codecMsg.asXMLString(); + response = anna::functions::encodeBase64(xmlString); + } + catch(anna::RuntimeException &ex) { ex.trace(); } + + return true; // OK +} + +bool EventOperation::loadmsg(std::string &response, const std::string & diameterJson) { + + bool success; + std::string diameterXml = anna::json::functions::json2xml(diameterJson, success); + if (!success) { + response += "json to xml failed, unable to load message !"; + return false; + } + anna::diameter::codec::Message codecMsg; // auxiliary codec message + codecMsg.loadXMLString(diameterXml); + response = anna::functions::encodeBase64(codecMsg.asXMLString()); + + return true; // OK +} + +///////////////// +// Hot changes // +///////////////// +bool EventOperation::services(std::string &response, const std::string & servicesJson) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + bool success; + std::string servicesXml = anna::json::functions::json2xml(servicesJson, success); + if (!success) { + response += "json to xml failed, unable to load services !"; + return false; + } + + try { + my_app.loadServicesFromXMLString(servicesXml, true /* bind entities */); + } + catch(anna::RuntimeException &ex) { + ex.trace(); + response += "loaded services with errors"; + return false; + } + response = "loaded services correctly"; + + return true; // OK +} + +bool EventOperation::diameterServerSessions(std::string &response, int sessions) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + try { + my_app.getOperatedServer()->setMaxConnections(sessions); + } + catch(anna::RuntimeException &ex) { + ex.trace(); + response += "fail to operate the server"; + return false; + } + response = "new sessions successfully established on operated diameter server"; + + return true; // OK +} + +bool EventOperation::change_dir(std::string &response, const std::string & directory) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + std::string dir = directory; + if (dir == "") dir = my_app.getInitialWorkingDirectory(); + bool result = (chdir(dir.c_str()) == 0); + + if (result) + response = "New execution directory configured: "; + else + response = "Cannot assign provided execution directory: "; + + response += dir; return result; } + +//////////////////////////////// +// Client sessions visibility // +//////////////////////////////// +bool EventOperation::visibility(std::string &response, const std::string & action, const std::string &addressPort, int socket) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + response = ""; + + if(addressPort != "") { + if(socket != -1) { + std::string key = addressPort; + key += "|"; + key += anna::functions::asString(socket); + + if(action == "hide") my_app.getOperatedEngine()->findClientSession(key)->hide(); + if(action == "show") my_app.getOperatedEngine()->findClientSession(key)->show(); + if(action == "hidden") response = my_app.getOperatedEngine()->findClientSession(key)->hidden() ? "true" : "false"; + if(action == "shown") response = my_app.getOperatedEngine()->findClientSession(key)->shown() ? "true" : "false"; + } else { + std::string address; + int port; + anna::functions::getAddressAndPortFromSocketLiteral(addressPort, address, port); + + if(action == "hide") my_app.getOperatedEngine()->findServer(address, port)->hide(); + if(action == "show") my_app.getOperatedEngine()->findServer(address, port)->show(); + if(action == "hidden") response = my_app.getOperatedEngine()->findServer(address, port)->hidden() ? "true" : "false"; + if(action == "shown") response = my_app.getOperatedEngine()->findServer(address, port)->shown() ? "true" : "false"; + } + } else { + if(action == "hide") my_app.getOperatedEntity()->hide(); + if(action == "show") my_app.getOperatedEntity()->show(); + if(action == "hidden") response = my_app.getOperatedEntity()->hidden() ? "true" : "false"; + if(action == "shown") response = my_app.getOperatedEntity()->shown() ? "true" : "false"; + } + + return true; // OK +} + + +/////////////// +// Snapshots // +/////////////// +bool EventOperation::collect(std::string &response) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + my_app.resetCounters(); + my_app.resetStatistics(); + response = "All process counters & statistic information have been reset"; + + return true; // OK +} + +bool EventOperation::context(std::string &response, const std::string & targetFile) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + std::string contextFile = (targetFile != "") ? targetFile : anna::functions::asString("/var/tmp/anna.context.%05d", my_app.getPid()); + my_app.writeContext(contextFile); + response = anna::functions::asString("Context dumped on file '%s'", contextFile.c_str()); + + return true; // OK +} + +bool EventOperation::forceCountersRecord(std::string &response) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + my_app.forceCountersRecord(); + response = "Current counters have been dump to disk"; + + return true; // OK +} + +bool EventOperation::log_statistics_samples(std::string &response, const std::string & list) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + my_app.logStatisticsSamples(list); + response = anna::functions::asString("Log statistics samples for '%s' concepts", list.c_str()); + + return true; // OK +} + +bool EventOperation::show_oam(std::string &response) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + anna::xml::Node root("root"); + response = anna::xml::Compiler().apply(my_app.oamAsXML(&root)); + if (a_http) + response = anna::functions::encodeBase64(response); + + return true; // OK +} + +bool EventOperation::show_stats(std::string &response) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + anna::xml::Node root("root"); + response = anna::xml::Compiler().apply(my_app.statsAsXML(&root)); + if (a_http) + response = anna::functions::encodeBase64(response); + + return true; // OK +} + +///////////////////// +// Flow operations // +///////////////////// +bool EventOperation::sendmsg2e(std::string &response, const std::string & diameterJson) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::sendmsg2c(std::string &response, const std::string & diameterJson) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::answermsg2e(std::string &response, const std::string & diameterJson) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::answermsg2c(std::string &response, const std::string & diameterJson) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::answermsg2e_action(std::string &response, const std::string & action) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::answermsg2c_action(std::string &response, const std::string & action) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::sendhex2e(std::string &response, const std::string & diameterHex) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::sendhex2c(std::string &response, const std::string & diameterHex) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + + +///////////////// +// FSM testing // +///////////////// +bool EventOperation::test_id__description(std::string &response, unsigned int id, const std::string & description) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test_id__ip_limit(std::string &response, unsigned int id, int amount) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test_id__timeout(std::string &response, unsigned int id, int msecs) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test_id__sendmsg2e(std::string &response, unsigned int id, const std::string & diameterJson, int stepNumber) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test_id__sendmsg2c(std::string &response, unsigned int id, const std::string & diameterJson, int stepNumber) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test_id__delay(std::string &response, unsigned int id, int msecs) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test_id__sh_command(std::string &response, unsigned int id, const std::string & script) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test_id__waitfe_hex(std::string &response, unsigned int id, const std::string & hex, bool strict) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test_id__waitfc_hex(std::string &response, unsigned int id, const std::string & hex, bool strict) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test_id__waitfe_msg(std::string &response, unsigned int id, const std::string & diameterJson, bool strict) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test_id__waitfc_msg(std::string &response, unsigned int id, const std::string & diameterJson, bool strict) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test_id__waitfe(std::string &response, unsigned int id, const std::string & condition) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test_id__waitfc(std::string &response, unsigned int id, const std::string & condition) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +///////////////////////// +// Testcases execution // +///////////////////////// +bool EventOperation::test__ttps(std::string &response, int amount) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__next(std::string &response, int syncAmount) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__ip_limit(std::string &response, int amount) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__goto(std::string &response, int id) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__run(std::string &response, int id) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__look(std::string &response, int id) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__state(std::string &response, int id) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__interact(std::string &response, int amount, unsigned int id) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__reset(std::string &response, bool soft_hard, unsigned int id) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__repeats(std::string &response, int amount) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__auto_reset(std::string &response, bool soft_hard) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__initialized(std::string &response) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__finished(std::string &response) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__clear(std::string &response) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__junit(std::string &response, const std::string & targetFile) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__summary_counts(std::string &response) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__summary_states(std::string &response) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__summary(std::string &response) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__report(std::string &response, const std::string & state, bool enable) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__report_hex(std::string &response, bool enable) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + +bool EventOperation::test__dump_stdout(std::string &response, bool enable) { + + Launcher& my_app = static_cast (anna::app::functions::getApp()); + + + + return true; // OK +} + + diff --git a/example/diameter/launcher/EventOperation.hpp b/example/diameter/launcher/EventOperation.hpp index 2d9e056..59ede33 100644 --- a/example/diameter/launcher/EventOperation.hpp +++ b/example/diameter/launcher/EventOperation.hpp @@ -10,17 +10,145 @@ #define example_diameter_launcher_EventOperation_hpp // Project -#include // STL #include -using json = nlohmann::json; +//// Standard +//#include // std::istringstream +//#include // std::cout +//#include // ceil +//#include +//#include // chdir +//#include +// +// Project +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +//#include +// +//// Process +//#include +//#include +//#include +//#include +//#include +//#include + class EventOperation { + bool a_http; + public: - static std::string json2piped(const json &j); + /** + * EventOperation constructor + * + * @param http Indicates that HTTP interface is used. + * + * At signal interface, the response is written directly as output, but HTTP + * one answers json. + * + * TODO: xml2json implementation (i.e., https://github.com/Cheedoong/xml2json). + * As only 3 operations are going to answer xml content: + * - working node report ('node') + * - diameter hex decode ('decode') + * - reinterpret json into xml ('loadmsg') + * the effort does not compensate having them responding full json representation. + * + * So, base64 encoding will be used for those xml representations. + * + * Anna Suite used xml natively for many things, json will be only used for + * REST API on requests and will be transformed internally. + */ + EventOperation(bool http) : a_http(http) {;} + + // Node management + bool node(std::string &response, const std::string & name); + bool node_auto(std::string &response); + + // Parsing operations + bool code(std::string &response, const std::string & diameterJson); + bool decode(std::string &response, const std::string & diameterHex); + bool loadmsg(std::string &response, const std::string & diameterJson); + + // Hot changes + bool services(std::string &response, const std::string & servicesJson); + bool diameterServerSessions(std::string &response, int sessions); + bool change_dir(std::string &response, const std::string & directory = ""); + + // Client sessions visibility + bool visibility(std::string &response, const std::string & action, const std::string &addressPort = "", int socket = -1); + + // Snapshots + bool collect(std::string &response); + bool context(std::string &response, const std::string & targetFile = ""); + bool forceCountersRecord(std::string &response); + bool log_statistics_samples(std::string &response, const std::string & list); + bool show_oam(std::string &response); + bool show_stats(std::string &response); + + // Flow operations + bool sendmsg2e(std::string &response, const std::string & diameterJson); + bool sendmsg2c(std::string &response, const std::string & diameterJson); + bool answermsg2e(std::string &response, const std::string & diameterJson); + bool answermsg2c(std::string &response, const std::string & diameterJson); + bool answermsg2e_action(std::string &response, const std::string & action = "list"); + bool answermsg2c_action(std::string &response, const std::string & action = "list"); + bool sendhex2e(std::string &response, const std::string & diameterHex); + bool sendhex2c(std::string &response, const std::string & diameterHex); + + // FSM testing + // test_id__ + bool test_id__description(std::string &response, unsigned int id, const std::string & description); + bool test_id__ip_limit(std::string &response, unsigned int id, int amount = 1); + bool test_id__timeout(std::string &response, unsigned int id, int msecs); + bool test_id__sendmsg2e(std::string &response, unsigned int id, const std::string & diameterJson, int stepNumber = -1); + bool test_id__sendmsg2c(std::string &response, unsigned int id, const std::string & diameterJson, int stepNumber = -1); + bool test_id__delay(std::string &response, unsigned int id, int msecs); + bool test_id__sh_command(std::string &response, unsigned int id, const std::string & script); + bool test_id__waitfe_hex(std::string &response, unsigned int id, const std::string & hex, bool strict = false); + bool test_id__waitfc_hex(std::string &response, unsigned int id, const std::string & hex, bool strict = false); + bool test_id__waitfe_msg(std::string &response, unsigned int id, const std::string & diameterJson, bool strict = false); + bool test_id__waitfc_msg(std::string &response, unsigned int id, const std::string & diameterJson, bool strict = false); + bool test_id__waitfe(std::string &response, unsigned int id, const std::string & condition); + bool test_id__waitfc(std::string &response, unsigned int id, const std::string & condition); + + // Testcases execution + // test__ + bool test__ttps(std::string &response, int amount); + bool test__next(std::string &response, int syncAmount = 1); + bool test__ip_limit(std::string &response, int amount = -2 /* show current ip-limit and in-progress test cases amount */); + bool test__goto(std::string &response, int id); + bool test__run(std::string &response, int id); + bool test__look(std::string &response, int id = -1 /* current */); + bool test__state(std::string &response, int id = -1 /* current */); + bool test__interact(std::string &response, int amount, unsigned int id = -1 /* current */); + bool test__reset(std::string &response, bool soft_hard = true, unsigned int id = -2 /* apply to all the tests */); + bool test__repeats(std::string &response, int amount); + bool test__auto_reset(std::string &response, bool soft_hard); + bool test__initialized(std::string &response); + bool test__finished(std::string &response); + bool test__clear(std::string &response); + bool test__junit(std::string &response, const std::string & targetFile); + bool test__summary_counts(std::string &response); + bool test__summary_states(std::string &response); + bool test__summary(std::string &response); + bool test__report(std::string &response, + const std::string & state = "all" /* initialized|in-progress|failed|success|[all]|none */, bool enable = true); + bool test__report_hex(std::string &response, bool enable = true); + bool test__dump_stdout(std::string &response, bool enable = true); }; #endif diff --git a/example/diameter/launcher/Launcher.cpp b/example/diameter/launcher/Launcher.cpp index 1940402..d0151cd 100644 --- a/example/diameter/launcher/Launcher.cpp +++ b/example/diameter/launcher/Launcher.cpp @@ -150,15 +150,15 @@ Launcher::Launcher() : anna::comm::Application("launcher", "DiameterLauncher", " std::string Launcher::getSignalUSR2InputFile() const throw() { - return (a_initialWorkingDirectory + "/" + SIGUSR2_TASKS_INPUT_FILENAME); + return (getInitialWorkingDirectory() + "/" + SIGUSR2_TASKS_INPUT_FILENAME); } std::string Launcher::getSignalUSR2OutputFile() const throw() { - return (a_initialWorkingDirectory + "/" + SIGUSR2_TASKS_OUTPUT_FILENAME); + return (getInitialWorkingDirectory() + "/" + SIGUSR2_TASKS_OUTPUT_FILENAME); } -void Launcher::servicesFromXML(const anna::xml::Node* servicesNode, bool eventOperation) throw(anna::RuntimeException) { +void Launcher::servicesFromXML(const anna::xml::Node* servicesNode, bool bindResources) throw(anna::RuntimeException) { CommandLine& cl(anna::CommandLine::instantiate()); bool allLogsDisabled = cl.exists("disableLogs"); @@ -203,6 +203,9 @@ void Launcher::servicesFromXML(const anna::xml::Node* servicesNode, bool eventOp if (stackEngine.getDictionary(id_value)) { // Ignore (but don't fail) dictionary load with same stack id already registered LOGWARNING(anna::Logger::warning(anna::functions::asString("Ignore dictionary load for stack id already registered: %llu", id_value), ANNA_FILE_LOCATION)); + // Delta loads, in case we provide base protocol already registered (comm::Engine will need 'bpd') + if (id_value == 0) + bpd = stackEngine.getDictionary(0); continue; } @@ -389,7 +392,7 @@ void Launcher::servicesFromXML(const anna::xml::Node* servicesNode, bool eventOp a_workingNode->createEntity(entity->getValue(), ceaTimeoutMs, answersTimeoutMs); a_workingNode->getEntity()->setSessionBasedModelsType(sessionBasedModelsTypeEnum); a_workingNode->getEntity()->setBalance(balance ? (balance->getValue() == "yes") : false); // for sendings - if (eventOperation) a_workingNode->getEntity()->bind(); + if (bindResources) a_workingNode->getEntity()->bind(); } } @@ -415,7 +418,7 @@ void Launcher::servicesFromXML(const anna::xml::Node* servicesNode, bool eventOp // Lazy initialization for comm engine: - if (eventOperation) commEngine->lazyInitialize(); + if (bindResources) commEngine->lazyInitialize(); // Node and Codec Engine registration /////////////////////////////////////////////////////// ohm.registerOriginHost(originHost->getValue(), a_workingNode); @@ -432,7 +435,7 @@ void Launcher::servicesFromXML(const anna::xml::Node* servicesNode, bool eventOp } -void Launcher::loadServices(const std::string & xmlPathFile, bool eventOperation) throw(anna::RuntimeException) { +void Launcher::loadServicesFromFile(const std::string & xmlPathFile, bool bindResources) throw(anna::RuntimeException) { if (xmlPathFile == "null" || xmlPathFile == "") { LOGWARNING(anna::Logger::warning("Ignoring services configuration on start: empty or 'null' string provided as xml file. Use management interface (operation 'services') in order to add services", ANNA_FILE_LOCATION)); @@ -469,7 +472,35 @@ void Launcher::loadServices(const std::string & xmlPathFile, bool eventOperation trace += anna::xml::Compiler().apply(rootNode); anna::Logger::debug(trace, ANNA_FILE_LOCATION); ); - servicesFromXML(rootNode, eventOperation); + servicesFromXML(rootNode, bindResources); +} + + +void Launcher::loadServicesFromXMLString(const std::string & xmlString, bool bindResources) throw(anna::RuntimeException) { + + anna::xml::DocumentMemory xmlDocument; // has private copy constructor defined but not implemented to avoid inhenrit/copy (is very heavy) + anna::xml::DTDMemory xmlDTD; + const anna::xml::Node *rootNode; + xmlDocument.initialize(xmlString.c_str()); + xmlDTD.initialize(ServicesDTD); + try { + rootNode = xmlDocument.parse(xmlDTD); // Parsing: fail here if xml violates dtd + } + catch (anna::RuntimeException &ex) { + LOGWARNING( + std::string msg = "Services DTD schema:\n\n"; + msg += ServicesDTD; + anna::Logger::warning(msg, ANNA_FILE_LOCATION); + ); + throw ex; + } + + LOGDEBUG( + std::string trace = "Loaded XML String:\n"; + trace += anna::xml::Compiler().apply(rootNode); + anna::Logger::debug(trace, ANNA_FILE_LOCATION); + ); + servicesFromXML(rootNode, bindResources); } @@ -568,6 +599,10 @@ anna::diameter::comm::OriginHost *Launcher::getOperatedHost() const throw(anna:: return a_operatedHost; } +void Launcher::setOperatedHost(anna::diameter::comm::OriginHost *op) { + a_operatedHost = op; +} + MyDiameterEntity *Launcher::getOperatedEntity() const throw(anna::RuntimeException) { MyDiameterEntity *result = (MyDiameterEntity *)(getOperatedHost()->getEntity()); if (!result) @@ -622,7 +657,7 @@ throw(anna::RuntimeException) { anna::Logger::setLevel(anna::Logger::asLevel(cl.getValue("trace"))); // Load launcher services: - loadServices(cl.getValue("services")); // before run (have components to be created) + loadServicesFromFile(cl.getValue("services"), false /* no bind at the moment */); // before run (have components to be created) } void Launcher::run() @@ -941,7 +976,7 @@ void Launcher::logStatisticsSamples(const std::string &conceptsList) throw() { } -bool Launcher::eventOperation(const std::string &operation, std::string &response_content) throw(anna::RuntimeException) { +bool Launcher::eventOperation(const std::string &operation, std::string &response) throw(anna::RuntimeException) { bool result = true; @@ -949,10 +984,13 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons if (operation == "") return result; // ignore LOGDEBUG(anna::Logger::debug(anna::functions::asString("Operation: %s", operation.c_str()), ANNA_FILE_LOCATION)); + EventOperation eop(false /* not HTTP, it is SIGUSR2 */); + // Default response: - response_content = "Operation processed with exception: "; - response_content += operation; - std::string opt_response_content = ""; // aditional response content + //response = "Operation processed with exception: "; + response = "Internal error (check ADML traces): "; + response += operation; + std::string opt_response = ""; // aditional response content anna::DataBlock db_aux(true); anna::diameter::codec::Message codecMsg; // auxiliary codec message @@ -971,11 +1009,11 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons if (args == "" && op_size != 7) throw anna::RuntimeException("Wrong body content format on HTTP Request. Check 'HELP.md' for more information.", ANNA_FILE_LOCATION); try { - p.execute(args, response_content); + p.execute(args, response); } catch(anna::RuntimeException &ex) { ex.trace(); - response_content = ex.asString(); + response = ex.asString(); return false; } return true; // OK @@ -983,29 +1021,20 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons // Reset performance data: if(operation == "collect") { - resetCounters(); - resetStatistics(); - response_content = "All process counters & statistic information have been reset"; - return true; // OK + return eop.collect(response); } // Counters dump on demand: if(operation == "forceCountersRecord") { - forceCountersRecord(); - response_content = "Current counters have been dump to disk"; - return true; // OK + return eop.forceCountersRecord(response); } // OAM & statistics: if(operation == "show-oam") { - anna::xml::Node root("root"); - response_content = anna::xml::Compiler().apply(oamAsXML(&root)); - return true; // OK + return eop.show_oam(response); } if(operation == "show-stats") { - anna::xml::Node root("root"); - response_content = anna::xml::Compiler().apply(statsAsXML(&root)); - return true; // OK + return eop.show_stats(response); } /////////////////////////////////////////////////////////////////// @@ -1055,7 +1084,7 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons if((opType == "log-statistics-samples") && (numParams != 1)) wrongBody = true; if((opType == "node") && (numParams > 1)) wrongBody = true; - if((opType == "node_auto") && (numParams > 0)) wrongBody = true; + if((opType == "node-auto") && (numParams > 0)) wrongBody = true; if(((opType == "code") || (opType == "decode")) && (numParams != 2)) wrongBody = true; @@ -1077,76 +1106,40 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons // Operations: if(opType == "context") { - std::string contextFile = ((numParams == 1) ? param1 : anna::functions::asString("/var/tmp/anna.context.%05d", getPid())); - writeContext(contextFile); - response_content = anna::functions::asString("Context dumped on file '%s'", contextFile.c_str()); - return true; // OK + return eop.context(response, (numParams == 1) ? param1 : ""); } if(opType == "log-statistics-samples") { - logStatisticsSamples(param1); - response_content = anna::functions::asString("Log statistics samples for '%s' concepts", param1.c_str()); - return true; // OK + return eop.log_statistics_samples(response, param1); } // Change execution directory: if(opType == "change-dir") { - if (param1 == "") param1 = a_initialWorkingDirectory; - result = (chdir(param1.c_str()) == 0); - - if (result) - response_content = "New execution directory configured: "; - else - response_content = "Cannot assign provided execution directory: "; - - response_content += param1; - return result; + return eop.change_dir(response, param1); } if(opType == "services") { std::string servicesFile = ((numParams == 1) ? param1 : "services.xml"); try { - loadServices(servicesFile, true /* bind entities */); + loadServicesFromFile(servicesFile, true /* bind entities */); } catch(anna::RuntimeException &ex) { ex.trace(); - response_content = anna::functions::asString("Loaded services from file '%s' with errors", servicesFile.c_str()); + response = anna::functions::asString("Loaded services from file '%s' with errors", servicesFile.c_str()); return false; } - response_content = anna::functions::asString("Loaded services from file '%s'", servicesFile.c_str()); + response = anna::functions::asString("Loaded services from file '%s'", servicesFile.c_str()); return true; // OK } // Host switch: if(opType == "node") { - if (param1 != "") { - if (setWorkingNode(param1)) response_content = anna::functions::asString("Forced node is now '%s'", param1.c_str()); - } - else { - if (a_workingNode) { - response_content = "Working node is forced to be: \n\n"; - response_content += a_workingNode->asXMLString(); - } - else { - response_content = "Working node is automatic"; - } - } - return true; // OK + return eop.node(response, param1); } - if(opType == "node_auto") { - a_workingNode = NULL; - response_content = "Working node has been set to automatic"; - return true; // OK + else if(opType == "node-auto") { + return eop.node_auto(response); } - // Operated host from possible forced-working node: - a_operatedHost = a_workingNode ? a_workingNode /* priority */: NULL /* auto */; - // Use later: - // If any message is managed: updateOperatedOriginHostWithMessage(codecMessage) - // To operate, use the exception-protected methods which never will return NULL: - // getOperatedHost(), getOperatedEntity(), getOperatedServer(), getOperatedEngine() - - if(opType == "code") { codecMsg.loadXMLFile(param1); std::string hexString = anna::functions::asHexString(codecMsg.code()); @@ -1168,42 +1161,8 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons outfile.write(xmlString.c_str(), xmlString.size()); outfile.close(); } else if((opType == "hide") || (opType == "show") || (opType == "hidden") || (opType == "shown")) { + return eop.visibility(opt_response, opType, param1, (param2 != "") ? atoi(param2.c_str()) : -1); - if(param1 != "") { - if(param2 != "") { - std::string key = param1; - key += "|"; - key += param2; - - if(opType == "hide") getOperatedEngine()->findClientSession(key)->hide(); - - if(opType == "show") getOperatedEngine()->findClientSession(key)->show(); - - if(opType == "hidden") opt_response_content = getOperatedEngine()->findClientSession(key)->hidden() ? "true" : "false"; - - if(opType == "shown") opt_response_content = getOperatedEngine()->findClientSession(key)->shown() ? "true" : "false"; - } else { - std::string address; - int port; - anna::functions::getAddressAndPortFromSocketLiteral(param1, address, port); - - if(opType == "hide") getOperatedEngine()->findServer(address, port)->hide(); - - if(opType == "show") getOperatedEngine()->findServer(address, port)->show(); - - if(opType == "hidden") opt_response_content = getOperatedEngine()->findServer(address, port)->hidden() ? "true" : "false"; - - if(opType == "shown") opt_response_content = getOperatedEngine()->findServer(address, port)->shown() ? "true" : "false"; - } - } else { - if(opType == "hide") getOperatedEntity()->hide(); - - if(opType == "show") getOperatedEntity()->show(); - - if(opType == "hidden") opt_response_content = getOperatedEntity()->hidden() ? "true" : "false"; - - if(opType == "shown") opt_response_content = getOperatedEntity()->shown() ? "true" : "false"; - } } else if((opType == "sendxml2e") || (opType == "sendhex2e")) { anna::diameter::comm::Message *msg; @@ -1252,9 +1211,9 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons // burst|look| Show programmed burst message for order provided, current when missing. if(param1 == "clear") { - opt_response_content = "removed "; - opt_response_content += anna::functions::asString(getOperatedHost()->clearBurst()); - opt_response_content += " elements"; + opt_response = "removed "; + opt_response += anna::functions::asString(getOperatedHost()->clearBurst()); + opt_response += " elements"; } else if(param1 == "load") { if(param2 == "") throw anna::RuntimeException("Missing xml path file for burst load operation", ANNA_FILE_LOCATION); @@ -1263,10 +1222,10 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons try { codecMsg.valid(); } catch(anna::RuntimeException &ex) { ex.trace(); } // at least we need to see validation errors although it will continue loading (see validation mode configured in launcher) int position = getOperatedHost()->loadBurstMessage(codecMsg.code()); - opt_response_content = "loaded '"; - opt_response_content += param2; - opt_response_content += "' file into burst list position "; - opt_response_content += anna::functions::asString(position); + opt_response = "loaded '"; + opt_response += param2; + opt_response += "' file into burst list position "; + opt_response += anna::functions::asString(position); } else if(param1 == "start") { if(param2 == "") throw anna::RuntimeException("Missing initial load for burst start operation", ANNA_FILE_LOCATION); @@ -1274,8 +1233,8 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons int processed = getOperatedHost()->startBurst(initialLoad); if(processed > 0) { - opt_response_content = "initial load completed for "; - opt_response_content += anna::functions::entriesAsString(processed, "message"); + opt_response = "initial load completed for "; + opt_response += anna::functions::entriesAsString(processed, "message"); } } else if(param1 == "push") { if(param2 == "") throw anna::RuntimeException("Missing load amount for burst push operation", ANNA_FILE_LOCATION); @@ -1283,8 +1242,8 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons int pushed = getOperatedHost()->pushBurst(atoi(param2.c_str())); if(pushed > 0) { - opt_response_content = "pushed "; - opt_response_content += anna::functions::entriesAsString(pushed, "message"); + opt_response = "pushed "; + opt_response += anna::functions::entriesAsString(pushed, "message"); } } else if(param1 == "pop") { if(param2 == "") throw anna::RuntimeException("Missing amount for burst pop operation", ANNA_FILE_LOCATION); @@ -1293,39 +1252,39 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons int popped = getOperatedHost()->popBurst(releaseLoad); if(popped > 0) { - opt_response_content = "burst popped for "; - opt_response_content += anna::functions::entriesAsString(popped, "message"); + opt_response = "burst popped for "; + opt_response += anna::functions::entriesAsString(popped, "message"); } } else if(param1 == "stop") { int left = getOperatedHost()->stopBurst(); if(left != -1) { - opt_response_content += anna::functions::entriesAsString(left, "message"); - opt_response_content += " left to the end of the cycle"; + opt_response += anna::functions::entriesAsString(left, "message"); + opt_response += " left to the end of the cycle"; } } else if(param1 == "repeat") { if(param2 == "") param2 = "yes"; bool repeat = (param2 == "yes"); getOperatedHost()->repeatBurst(repeat); - opt_response_content += (repeat ? "repeat enabled" : "repeat disabled"); + opt_response += (repeat ? "repeat enabled" : "repeat disabled"); } else if(param1 == "send") { if(param2 == "") throw anna::RuntimeException("Missing amount for burst send operation", ANNA_FILE_LOCATION); int sent = getOperatedHost()->sendBurst(atoi(param2.c_str())); if(sent > 0) { - opt_response_content = "sent "; - opt_response_content += anna::functions::entriesAsString(sent, "message"); + opt_response = "sent "; + opt_response += anna::functions::entriesAsString(sent, "message"); } } else if(param1 == "goto") { if(param2 == "") throw anna::RuntimeException("Missing order position for burst goto operation", ANNA_FILE_LOCATION); - opt_response_content = getOperatedHost()->gotoBurst(atoi(param2.c_str())); + opt_response = getOperatedHost()->gotoBurst(atoi(param2.c_str())); } else if(param1 == "look") { int order = ((param2 != "") ? atoi(param2.c_str()) : -1); - opt_response_content = "\n\n"; - opt_response_content += getOperatedHost()->lookBurst(order); + opt_response = "\n\n"; + opt_response += getOperatedHost()->lookBurst(order); } else { throw anna::RuntimeException("Wrong body content format on HTTP Request for 'burst' operation (unexpected action parameter). Check 'HELP.md' for more information.", ANNA_FILE_LOCATION); } @@ -1340,6 +1299,7 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons // Enables/disables report generation for a certain test case state: initialized, in-progress ... // test|report-hex[|[yes]|no] Reports could include the diameter messages in hexadecimal format. Disabled by default. // test|goto| Updates current test pointer position. + // test|run| Executes the given test case // test|look[|id] Show programmed test case for id provided, current when missing ... // test|state[|id] Show test case state for id provided, current when missing ... // test|interact|amount|id Makes interactive a specific test case id. The amount is the margin of execution steps ... @@ -1355,12 +1315,12 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons bool success = ((param2 != "") ? testManager.configureTTPS(atoi(param2.c_str())) : false); if (success) { - opt_response_content = "assigned new test launch rate to "; - opt_response_content += anna::functions::asString(atoi(param2.c_str())); - opt_response_content += " events per second"; + opt_response = "assigned new test launch rate to "; + opt_response += anna::functions::asString(atoi(param2.c_str())); + opt_response += " events per second"; } else { - opt_response_content += "unable to configure the test rate provided"; + opt_response += "unable to configure the test rate provided"; } } else if (param1 == "next") { @@ -1374,10 +1334,10 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons bool success = testManager.execTestCases(sync_amount); - opt_response_content = (success ? "" : "not completely " /* completed cycle and no repeats, rare case */); - opt_response_content += "processed "; - opt_response_content += anna::functions::asString(sync_amount); - opt_response_content += ((sync_amount > 1) ? " test cases synchronously" : " test case"); + opt_response = (success ? "" : "not completely " /* completed cycle and no repeats, rare case */); + opt_response += "processed "; + opt_response += anna::functions::asString(sync_amount); + opt_response += ((sync_amount > 1) ? " test cases synchronously" : " test case"); } else if(param1 == "ip-limit") { if (numParams > 2) @@ -1387,16 +1347,16 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons if (param2 != "") { limit = atoi(param2.c_str()); testManager.setInProgressLimit(limit); - opt_response_content = "new in-progress limit: "; - opt_response_content += (limit != UINT_MAX) ? anna::functions::asString(limit) : ""; + opt_response = "new in-progress limit: "; + opt_response += (limit != UINT_MAX) ? anna::functions::asString(limit) : ""; } else { - opt_response_content = "in-progress limit amount: "; + opt_response = "in-progress limit amount: "; limit = testManager.getInProgressLimit(); - opt_response_content += (limit != UINT_MAX) ? anna::functions::asString(limit) : ""; - opt_response_content += "; currently there are "; - opt_response_content += anna::functions::asString(testManager.getInProgressCount()); - opt_response_content += " test cases running"; + opt_response += (limit != UINT_MAX) ? anna::functions::asString(limit) : ""; + opt_response += "; currently there are "; + opt_response += anna::functions::asString(testManager.getInProgressCount()); + opt_response += " test cases running"; } } else if(param1 == "repeats") { @@ -1406,7 +1366,7 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons if (repeats < 0) repeats = -1; testManager.setPoolRepeats(repeats); std::string nolimit = (repeats != -1) ? "":" [no limit]"; - opt_response_content += anna::functions::asString("Pool repeats: %d%s (current cycle: %d)", repeats, nolimit.c_str(), testManager.getPoolCycle()); + opt_response += anna::functions::asString("Pool repeats: %d%s (current cycle: %d)", repeats, nolimit.c_str(), testManager.getPoolCycle()); } else if(param1 == "report") { if (numParams > 3) @@ -1436,10 +1396,10 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons else throw anna::RuntimeException("Wrong body content format on HTTP Request. Check 'HELP.md' for more information.", ANNA_FILE_LOCATION); - opt_response_content += (enable ? "report enabled " : "report disabled "); - opt_response_content += "for tests in '"; - opt_response_content += param2; - opt_response_content += "' state"; + opt_response += (enable ? "report enabled " : "report disabled "); + opt_response += "for tests in '"; + opt_response += param2; + opt_response += "' state"; } else if(param1 == "report-hex") { if (numParams > 2) @@ -1447,7 +1407,7 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons if(param2 == "") param2 = "yes"; testManager.setDumpHex((param2 == "yes")); - opt_response_content += (testManager.getDumpHex() ? "report includes hexadecimal messages" : "report excludes hexadecimal messages"); + opt_response += (testManager.getDumpHex() ? "report includes hexadecimal messages" : "report excludes hexadecimal messages"); } else if(param1 == "dump-stdout") { if (numParams > 2) @@ -1455,7 +1415,7 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons if(param2 == "") param2 = "yes"; testManager.setDumpStdout((param2 == "yes")); - opt_response_content += (testManager.getDumpHex() ? "test manager dumps progress into stdout" : "test manager does not dump progress into stdout"); + opt_response += (testManager.getDumpHex() ? "test manager dumps progress into stdout" : "test manager does not dump progress into stdout"); } else if(param1 == "goto") { if (numParams > 2) @@ -1464,13 +1424,13 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons if(param2 == "") throw anna::RuntimeException("Missing id for test goto operation", ANNA_FILE_LOCATION); int id = atoi(param2.c_str()); if (testManager.gotoTestCase(id)) { - opt_response_content = "position updated for id provided ("; + opt_response = "position updated for id provided ("; } else { - opt_response_content = "cannot found test id ("; + opt_response = "cannot found test id ("; } - opt_response_content += anna::functions::asString(id); - opt_response_content += ")"; + opt_response += anna::functions::asString(id); + opt_response += ")"; } else if(param1 == "run") { if (numParams > 2) @@ -1479,13 +1439,13 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons if(param2 == "") throw anna::RuntimeException("Missing id for test run operation", ANNA_FILE_LOCATION); int id = atoi(param2.c_str()); if (testManager.runTestCase(id)) { - opt_response_content = "test executed for id provided ("; + opt_response = "test executed for id provided ("; } else { - opt_response_content = "cannot found test id ("; + opt_response = "cannot found test id ("; } - opt_response_content += anna::functions::asString(id); - opt_response_content += ")"; + opt_response += anna::functions::asString(id); + opt_response += ")"; } else if(param1 == "look") { if (numParams > 2) @@ -1495,18 +1455,18 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons anna::testing::TestCase *testCase = testManager.findTestCase(id); if (testCase) { - response_content = testCase->asXMLString(); + response = testCase->asXMLString(); return true; // OK } else { result = false; if (id == -1) { - opt_response_content = "no current test case detected (testing started ?)"; + opt_response = "no current test case detected (testing started ?)"; } else { - opt_response_content = "cannot found test id ("; - opt_response_content += anna::functions::asString(id); - opt_response_content += ")"; + opt_response = "cannot found test id ("; + opt_response += anna::functions::asString(id); + opt_response += ")"; } } } @@ -1518,18 +1478,18 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons anna::testing::TestCase *testCase = testManager.findTestCase(id); if (testCase) { - response_content = anna::testing::TestCase::asText(testCase->getState()); + response = anna::testing::TestCase::asText(testCase->getState()); return testCase->isSuccess(); } else { result = false; if (id == -1) { - opt_response_content = "no current test case detected (testing started ?)"; + opt_response = "no current test case detected (testing started ?)"; } else { - opt_response_content = "cannot found test id ("; - opt_response_content += anna::functions::asString(id); - opt_response_content += ")"; + opt_response = "cannot found test id ("; + opt_response += anna::functions::asString(id); + opt_response += ")"; } } } @@ -1546,23 +1506,23 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons if (testCase) { if (amount == -1) { testCase->makeInteractive(false); - opt_response_content = "interactive mode disabled"; + opt_response = "interactive mode disabled"; } else { testCase->addInteractiveAmount(amount); - opt_response_content = "added interactive amount of "; - opt_response_content += anna::functions::asString(amount); - opt_response_content += " units"; - if (amount == 0) opt_response_content += " (0: freezing a non-interactive testcase, no effect on already interactive)"; + opt_response = "added interactive amount of "; + opt_response += anna::functions::asString(amount); + opt_response += " units"; + if (amount == 0) opt_response += " (0: freezing a non-interactive testcase, no effect on already interactive)"; } - opt_response_content += " for test case id "; - opt_response_content += anna::functions::asString(id); + opt_response += " for test case id "; + opt_response += anna::functions::asString(id); } else { result = false; - opt_response_content = "cannot found test id ("; - opt_response_content += anna::functions::asString(id); - opt_response_content += ")"; + opt_response = "cannot found test id ("; + opt_response += anna::functions::asString(id); + opt_response += ")"; } } else if(param1 == "reset") { @@ -1578,22 +1538,22 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons if (testCase) { bool done = testCase->reset(param2 == "hard"); - opt_response_content = "test "; - opt_response_content += param2; - opt_response_content += " reset for id "; - opt_response_content += anna::functions::asString(id); - opt_response_content += done ? ": done": ": not done"; + opt_response = "test "; + opt_response += param2; + opt_response += " reset for id "; + opt_response += anna::functions::asString(id); + opt_response += done ? ": done": ": not done"; } else { if (id == -1) { bool anyReset = testManager.resetPool(param2 == "hard"); - opt_response_content = param2; opt_response_content += " reset have been sent to all programmed tests: "; opt_response_content += anyReset ? "some/all have been reset" : "nothing was reset"; + opt_response = param2; opt_response += " reset have been sent to all programmed tests: "; opt_response += anyReset ? "some/all have been reset" : "nothing was reset"; } else { result = false; - opt_response_content = "cannot found test id ("; - opt_response_content += anna::functions::asString(id); - opt_response_content += ")"; + opt_response = "cannot found test id ("; + opt_response += anna::functions::asString(id); + opt_response += ")"; } } } @@ -1605,45 +1565,45 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons throw anna::RuntimeException("Wrong body content format on HTTP Request. Check 'HELP.md' for more information.", ANNA_FILE_LOCATION); testManager.setAutoResetHard(param2 == "hard"); - opt_response_content += anna::functions::asString("Auto-reset configured to '%s'", param2.c_str()); + opt_response += anna::functions::asString("Auto-reset configured to '%s'", param2.c_str()); } else if(param1 == "initialized") { if (numParams > 1) throw anna::RuntimeException("Wrong body content format on HTTP Request. Check 'HELP.md' for more information.", ANNA_FILE_LOCATION); - opt_response_content = anna::functions::asString("%lu", testManager.getInitializedCount()); + opt_response = anna::functions::asString("%lu", testManager.getInitializedCount()); } else if(param1 == "finished") { if (numParams > 1) throw anna::RuntimeException("Wrong body content format on HTTP Request. Check 'HELP.md' for more information.", ANNA_FILE_LOCATION); - opt_response_content = anna::functions::asString("%lu", testManager.getFinishedCount()); + opt_response = anna::functions::asString("%lu", testManager.getFinishedCount()); } else if(param1 == "clear") { if (numParams > 1) throw anna::RuntimeException("Wrong body content format on HTTP Request. Check 'HELP.md' for more information.", ANNA_FILE_LOCATION); if (testManager.clearPool()) { - opt_response_content = "all the programmed test cases have been dropped"; + opt_response = "all the programmed test cases have been dropped"; } else { - opt_response_content = "there are not programmed test cases to be removed"; + opt_response = "there are not programmed test cases to be removed"; } } else if(param1 == "junit") { - response_content = testManager.junitAsXMLString(); + response = testManager.junitAsXMLString(); return true; // OK } else if(param1 == "summary-counts") { - response_content = testManager.summaryCounts(); + response = testManager.summaryCounts(); return true; // OK } else if(param1 == "summary-states") { - response_content = testManager.summaryStates(); + response = testManager.summaryStates(); return true; // OK } else if(param1 == "summary") { - response_content = testManager.asXMLString(); + response = testManager.asXMLString(); return true; // OK } else { @@ -1854,7 +1814,7 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons } } else if(opType == "loadxml") { codecMsg.loadXMLFile(param1); - response_content = codecMsg.asXMLString(); + response = codecMsg.asXMLString(); return true; // OK } else if(opType == "diameterServerSessions") { int diameterServerSessions = atoi(param1.c_str()); @@ -1862,7 +1822,7 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons } else if(opType == "answerxml2c") { if(param1 == "") { // programmed answers FIFO's to stdout - response_content = getOperatedServer()->getReactingAnswers()->asString("ANSWERS TO CLIENT"); + response = getOperatedServer()->getReactingAnswers()->asString("ANSWERS TO CLIENT"); return true; // OK } else if (param1 == "rotate") { getOperatedServer()->getReactingAnswers()->rotate(true); @@ -1888,7 +1848,7 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons } else if(opType == "answerxml2e") { if(param1 == "") { // programmed answers FIFO's to stdout - response_content = getOperatedEntity()->getReactingAnswers()->asString("ANSWERS TO ENTITY"); + response = getOperatedEntity()->getReactingAnswers()->asString("ANSWERS TO ENTITY"); return true; // OK } else if (param1 == "rotate") { getOperatedEntity()->getReactingAnswers()->rotate(true); @@ -1916,10 +1876,10 @@ bool Launcher::eventOperation(const std::string &operation, std::string &respons } // HTTP response - response_content = "Operation correctly processed: "; response_content += operation; - if (opt_response_content != "") { - response_content += " => "; - response_content += opt_response_content; + response = "Operation correctly processed: "; response += operation; + if (opt_response != "") { + response += " => "; + response += opt_response; } return result; diff --git a/example/diameter/launcher/Launcher.hpp b/example/diameter/launcher/Launcher.hpp index 399a0f8..dab3415 100644 --- a/example/diameter/launcher/Launcher.hpp +++ b/example/diameter/launcher/Launcher.hpp @@ -74,7 +74,7 @@ class Launcher : public anna::comm::Application { std::string getSignalUSR2InputFile() const throw(); std::string getSignalUSR2OutputFile() const throw(); - void servicesFromXML(const anna::xml::Node* servicesNode, bool eventOperation) throw(anna::RuntimeException); + void servicesFromXML(const anna::xml::Node* servicesNode, bool bindResources) throw(anna::RuntimeException); anna::Millisecond checkTimeMeasure(const std::string ¶meter, const std::string &value) throw(anna::RuntimeException); void initialize() throw(anna::RuntimeException); // HTTP void run() throw(anna::RuntimeException); @@ -85,10 +85,12 @@ public: Launcher(); //~Launcher(); TODO - void loadServices(const std::string & xmlPathFile, bool eventOperation = false) throw(anna::RuntimeException); + void loadServicesFromFile(const std::string & xmlPathFile, bool bindResources) throw(anna::RuntimeException); + void loadServicesFromXMLString(const std::string & xmlString, bool bindResources) throw(anna::RuntimeException); void startServices() throw(anna::RuntimeException); bool setWorkingNode(const std::string &name) throw(); + void setNodeAuto() { a_workingNode = NULL; a_operatedHost = NULL; } anna::diameter::comm::OriginHost *getOriginHost(const std::string &name) const throw(anna::RuntimeException); anna::diameter::comm::OriginHost *getOriginHost(const anna::diameter::codec::Message &message) const throw(anna::RuntimeException); bool uniqueOriginHost() const throw(); @@ -97,11 +99,13 @@ public: void updateOperatedOriginHostWithMessage(const anna::diameter::codec::Message &message) throw(anna::RuntimeException); anna::diameter::comm::OriginHost *getWorkingNode() const throw(anna::RuntimeException); anna::diameter::comm::OriginHost *getOperatedHost() const throw(anna::RuntimeException); + void setOperatedHost(anna::diameter::comm::OriginHost *); MyDiameterEntity *getOperatedEntity() const throw(anna::RuntimeException); MyLocalServer *getOperatedServer() const throw(anna::RuntimeException); MyDiameterEngine *getOperatedEngine() const throw(anna::RuntimeException); /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const std::string & getInitialWorkingDirectory() const { return a_initialWorkingDirectory; } MyCommunicator *getCommunicator() throw() { return a_communicator; } bool eventOperation(const std::string &, std::string &) throw(anna::RuntimeException); // returns success/failed diff --git a/example/diameter/launcher/MyHandler.cpp b/example/diameter/launcher/MyHandler.cpp index 6638fc3..ed49f4f 100644 --- a/example/diameter/launcher/MyHandler.cpp +++ b/example/diameter/launcher/MyHandler.cpp @@ -10,74 +10,551 @@ #include #include #include +#include // Project #include +#include // Process #include #include #include +void MyHandler::splitURI(const std::string &uri, std::string & operation, std::string & param1, std::string & param2) const { + + std::string::size_type slash_pos = uri.find("/", 1u); + if (slash_pos == std::string::npos) { + operation = uri.substr(0); + } + else { + operation = uri.substr(0, slash_pos); + param1 = uri.substr(slash_pos + 1); + + std::string::size_type slash2_pos = uri.find("/", slash_pos + 1); + if (slash2_pos != std::string::npos) { + param1 = uri.substr(slash_pos + 1, slash2_pos - slash_pos - 1); + param2 = uri.substr(slash2_pos + 1); + } + } +} + +void MyHandler::sendResponse(anna::comm::ClientSocket& clientSocket, anna::http::Response *response) +{ + try { + clientSocket.send(*response); + } catch(Exception& ex) { + ex.trace(); + } +} void MyHandler::evRequest(anna::comm::ClientSocket& clientSocket, const anna::http::Request& request) throw(anna::RuntimeException) { - const anna::DataBlock& body = request.getBody(); - - if(body.getSize() == 0) - throw anna::RuntimeException("Missing operation parameters on HTTP request", ANNA_FILE_LOCATION); - LOGINFORMATION( - std::string msg("Received body: "); - msg += anna::functions::asString(body); - anna::Logger::information(msg, ANNA_FILE_LOCATION); - ); + const anna::DataBlock& body = request.getBody(); + anna::http::Method::Type::_v method = request.getMethod(); + std::string uri = request.getURI(); + bool isGET = (method == anna::http::Method::Type::Get); + bool isPOST = (method == anna::http::Method::Type::Post); std::string body_content; - body_content.assign(body.getData(), body.getSize()); - auto json_body = nlohmann::json::parse(body_content); + nlohmann::json json_body; + std::stringstream ss; - // Operation: - std::string response_content; + anna::http::Response* response = allocateResponse(); + response->setStatusCode(200); - bool opOk{}; + if (isGET) { + if(body.getSize() != 0) + response->setStatusCode(400); // bad request + } + else if (isPOST) { + if(body.getSize() != 0) { + body_content.assign(body.getData(), body.getSize()); // assign body content: - try { - Launcher& my_app = static_cast (anna::app::functions::getApp()); - opOk = my_app.eventOperation(EventOperation::json2piped(json_body), response_content); - } catch(RuntimeException &ex) { - ex.trace(); - opOk = false; + try { + json_body = nlohmann::json::parse(body_content); + LOGINFORMATION( + std::string msg("Json body received:\n\n"); + msg += json_body.dump(4); // pretty print json body + anna::Logger::information(msg, ANNA_FILE_LOCATION); + ); + } + catch (nlohmann::json::parse_error& e) + { + ss << "Json body parse error: " << e.what() << '\n' + << "exception id: " << e.id << '\n' + << "byte position of error: " << e.byte << std::endl; + anna::Logger::error(ss.str(), ANNA_FILE_LOCATION); + } + } + } + else { + response->setStatusCode(405); // method not allowed } - anna::http::Response* response = allocateResponse(); - response->setStatusCode(200); // http://en.wikipedia.org/wiki/List_of_HTTP_status_codes - -// EXAMPLES TO SET HEADERS: -// response->find(anna::http::Header::Type::Date)->setValue("Mon, 30 Jan 2006 14:36:18 GMT"); -// anna::http::Header* keepAlive = response->find("Keep-Alive"); -// -// if (keepAlive == NULL) -// keepAlive = response->createHeader("Keep-Alive"); -// -// keepAlive->setValue("Verificacion del cambio 1.0.7"); - - // Content-Type header: - anna::http::Header* contentType = response->find("Content-Type"); - if (contentType == NULL) - contentType = response->createHeader("Content-Type"); + ////////////////////// + // Prepare response // + ////////////////////// + + // Headers: + http::Header* contentType =response->find(http::Header::Type::ContentType); + if(contentType == NULL) + contentType = response->createHeader(http::Header::Type::ContentType); contentType->setValue("application/json"); - // Json Body for response: - std::stringstream ss; + // Body: + std::string response_content = "Internal error (check ADML traces): "; + bool success = false; + if (isGET) { + success = doGET(uri, response_content); + } + else if (isPOST) { + try { + success = doPOST(uri, json_body, response_content); + } + catch(anna::RuntimeException &ex) { + ex.trace(); + LOGINFORMATION(anna::Logger::information("XXXXXXXXXXXX EXCEPCION PENDIENTE DE CATCHEAR ABAJO !!! XXXXXXXXXXXXXXXXX", ANNA_FILE_LOCATION)); + } + } + + ss << R"({ "success":")" << (success ? "true":"false") + << R"(", "response": )" << std::quoted(response_content); + ss << R"( })"; - ss << R"({ "result":")" << (opOk ? "true":"false") << R"(", "data": )" << std::quoted(response_content) << R"( })"; anna::DataBlock db_content(true); db_content = ss.str(); response->setBody(db_content); - try { - clientSocket.send(*response); - } catch(Exception& ex) { - ex.trace(); + sendResponse(clientSocket, response); +} + +bool MyHandler::doGET(const std::string &uri, std::string &response) { + + bool result = false; + + EventOperation eop(true /* is HTTP */); + + // Snapshots + if (uri == "/show-oam") { + result = eop.show_oam(response); + } + else if (uri == "/show-stats") { + result = eop.show_stats(response); + } + + return result; +} + +bool MyHandler::doPOST(const std::string &uri, const nlohmann::json &j, std::string &response) { + + bool result = false; + + EventOperation eop(true /* is HTTP */); + + std::string opType{}, param1{}, param2{}; + splitURI(uri, opType, param1, param2); + + // Node management + if (opType == "/node") { + auto it = j.find("name"); + if (it != j.end()) + result = eop.node(response, *it); + else + response += "missing 'name' string field"; + } + else if (opType == "/node-auto") { + result = eop.node_auto(response); + } + + // Parsing operations + else if (opType == "/code") { + auto it = j.find("diameterJson"); + if (it != j.end()) + result = eop.code(response, it->dump(4)); // get the object as string (always indentation = 4) + else + response += "missing 'diameterJson' object field"; + } + else if (opType == "/decode") { + auto it = j.find("diameterHex"); + if (it != j.end()) + result = eop.decode(response, *it); + else + response += "missing 'diameterHex' string field"; + } + else if (opType == "/loadmsg") { + auto it = j.find("diameterJson"); + if (it != j.end()) + result = eop.loadmsg(response, it->dump(4)); // get the object as string (always indentation = 4) + else + response += "missing 'diameterJson' object field"; + } + + // Hot changes + else if (opType == "/services") { + auto it = j.find("servicesJson"); + if (it != j.end()) { + result = eop.services(response, it->dump(4)); // get the object as string (always indentation = 4) + } + else + response += "missing 'servicesJson' object field"; + } + else if (opType == "/diameterServerSessions") { + auto it = j.find("sessions"); + if (it != j.end()) + result = eop.diameterServerSessions(response, it->get()); + else + response += "missing 'session' integer field"; + } + else if (opType == "/change-dir") { + auto it = j.find("directory"); + std::string directory = (it != j.end()) ? *it : ""; // default is: restore initial directory + result = eop.change_dir(response, directory); + } + + // Client sessions visibility + else if (opType == "/visibility") { + auto it = j.find("action"); + if (it != j.end()) { + std::string action = *it; + + it = j.find("addressPort"); + std::string addressPort = (it != j.end()) ? *it : ""; + + it = j.find("socket"); + int socket = (it != j.end()) ? it->get() : -1; + + result = eop.visibility(response, action, addressPort, socket); + } + else + response += "missing 'action' string field"; + } + + // Snapshots + else if (opType == "/collect") { + result = eop.collect(response); } + else if (opType == "/context") { + auto it = j.find("targetFile"); + std::string targetFile = (it != j.end()) ? *it : ""; + result = eop.context(response, targetFile); + } + else if (opType == "/forceCountersRecord") { + result = eop.forceCountersRecord(response); + } + else if (opType == "/log-statistics-samples") { + auto it = j.find("list"); + if (it != j.end()) + result = eop.log_statistics_samples(response, *it); + else + response += "missing 'list' string field"; + } + + // Flow operations + else if ((opType == "/sendmsg2e")||(opType == "/sendmsg2c")) { + auto it = j.find("diameterJson"); + if (it != j.end()) + if (opType == "/sendmsg2e") + result = eop.sendmsg2e(response, it->dump(4)); // get the object as string (always indentation = 4) + else + result = eop.sendmsg2c(response, it->dump(4)); // get the object as string (always indentation = 4) + else + response += "missing 'diameterJson' object field"; + } + else if ((opType == "/answermsg2e")||(opType == "/answermsg2c")) { + auto itJ = j.find("diameterJson"); + auto itA = j.find("action"); + bool hasJ = (itJ != j.end()); + bool hasA = (itA != j.end()); + + if (hasJ != hasA) { // XOR + + if (opType == "/answermsg2e") { + if (hasJ) + result = eop.answermsg2e(response, itJ->dump(4)); // get the object as string (always indentation = 4) + else + result = eop.answermsg2e_action(response, *itA); + } + else { + if (hasJ) + result = eop.answermsg2c(response, itJ->dump(4)); // get the object as string (always indentation = 4) + else + result = eop.answermsg2c_action(response, *itA); + } + } + else + response += "missing 'diameterJson' object or 'action' string field (only one accepted)"; + } + else if ((opType == "/sendhex2e")||(opType == "/sendhex2c")) { + auto it = j.find("diameterHex"); + if (it != j.end()) + if (opType == "/sendhex2e") + result = eop.sendhex2e(response, *it); + else + result = eop.sendhex2c(response, *it); + else + response += "missing 'diameterHex' string field"; + } + + // FSM testing + // test_id__ + else if (opType == "/testid-description") { + auto it = j.find("description"); + if (it != j.end()) + result = eop.test_id__description(response, atoi(param1.c_str()), *it); + else + response += "missing 'description' string field"; + } + else if (opType == "/testid-ip-limit") { + auto it = j.find("amount"); + int amount = (it != j.end()) ? it->get() : 1; + result = eop.test_id__ip_limit(response, atoi(param1.c_str()), amount); + } + else if (opType == "/testid-timeout") { + auto it = j.find("msecs"); + if (it != j.end()) + result = eop.test_id__timeout(response, atoi(param1.c_str()), it->get()); + else + response += "missing 'msecs' integer field"; + } + else if ((opType == "/testid-sendmsg2e")||(opType == "/testid-sendmsg2c")) { + auto it = j.find("diameterJson"); + if (it != j.end()) { + + auto itS = j.find("stepNumber"); + int stepNumber = (itS != j.end()) ? itS->get() : -1; + + if (opType == "/testid-sendmsg2e") + result = eop.test_id__sendmsg2e(response, atoi(param1.c_str()), it->dump(4), stepNumber); // get the object as string (always indentation = 4) + else + result = eop.test_id__sendmsg2c(response, atoi(param1.c_str()), it->dump(4), stepNumber); // get the object as string (always indentation = 4) + } + else + response += "missing 'diameterJson' object field"; + } + else if (opType == "/testid-delay") { + auto it = j.find("msecs"); + if (it != j.end()) + result = eop.test_id__delay(response, atoi(param1.c_str()), it->get()); + else + response += "missing 'msecs' integer field"; + } + else if (opType == "/testid-sh-command") { + auto it = j.find("script"); + if (it != j.end()) + result = eop.test_id__sh_command(response, atoi(param1.c_str()), *it); + else + response += "missing 'script' string field"; + } + else if ((opType == "/testid-waitfe-hex")||(opType == "/testid-waitfc-hex")) { + auto it = j.find("hex"); + if (it != j.end()) { + + auto itS = j.find("strict"); + bool strict = (itS != j.end()) ? (*itS == "true") : false; + + if (opType == "/testid-waitfe-hex") + result = eop.test_id__waitfe_hex(response, atoi(param1.c_str()), *it, strict); + else + result = eop.test_id__waitfc_hex(response, atoi(param1.c_str()), *it, strict); + } + else + response += "missing 'hex' string field"; + } + else if ((opType == "/testid-waitfe-msg")||(opType == "/testid-waitfc-msg")) { + auto it = j.find("diameterJson"); + if (it != j.end()) { + + auto itS = j.find("strict"); + bool strict = (itS != j.end()) ? (*itS == "true") : false; + + if (opType == "/testid-waitfe-msg") + result = eop.test_id__waitfe_msg(response, atoi(param1.c_str()), it->dump(4), strict); // get the object as string (always indentation = 4) + else + result = eop.test_id__waitfc_msg(response, atoi(param1.c_str()), it->dump(4), strict); // get the object as string (always indentation = 4) + } + else + response += "missing 'diameterJson' object field"; + } + else if ((opType == "/testid-waitfe")||(opType == "/testid-waitfc")) { + auto it = j.find("condition"); + if (it != j.end()) { + // [code]|[bitR]|[hopByHop]|[applicationId]|[sessionId]|[resultCode]|[msisdn]|[imsi]|[serviceContextId] + auto it_code = it->find("code"); + auto it_bitR = it->find("bitR"); + auto it_hopByHop = it->find("hopByHop"); + auto it_applicationId = it->find("applicationId"); + auto it_sessionId = it->find("sessionId"); + auto it_resultCode = it->find("resultCode"); + auto it_msisdn = it->find("msisdn"); + auto it_imsi = it->find("imsi"); + auto it_serviceContextId = it->find("serviceContextId"); + + std::string condition; + condition += (it_code != it->end()) ? *it_code : ""; condition += "|"; + condition += (it_bitR != it->end()) ? *it_bitR : ""; condition += "|"; + condition += (it_hopByHop != it->end()) ? *it_hopByHop : ""; condition += "|"; + condition += (it_applicationId != it->end()) ? *it_applicationId : ""; condition += "|"; + condition += (it_sessionId != it->end()) ? *it_sessionId : ""; condition += "|"; + condition += (it_resultCode != it->end()) ? *it_resultCode : ""; condition += "|"; + condition += (it_msisdn != it->end()) ? *it_msisdn : ""; condition += "|"; + condition += (it_imsi != it->end()) ? *it_imsi : ""; condition += "|"; + condition += (it_serviceContextId != it->end()) ? *it_serviceContextId : ""; + + if (opType == "/testid-waitfe") + result = eop.test_id__waitfe(response, atoi(param1.c_str()), condition); + else + result = eop.test_id__waitfc(response, atoi(param1.c_str()), condition); + } + else + response += "missing 'condition' object field"; + } + + // Testcases execution + // test__ + else if (opType == "/test-ttps") { + auto it = j.find("amount"); + if (it != j.end()) + result = eop.test__ttps(response, it->get()); + else + response += "missing 'amount' integer field"; + } + else if (opType == "/test-next") { + auto it = j.find("syncAmount"); + int syncAmount = (it != j.end()) ? it->get() : 1; + result = eop.test__next(response, syncAmount); + } + else if (opType == "/test-ip-limit") { + auto it = j.find("amount"); + int amount = (it != j.end()) ? it->get() : -2; // default is: show current ip-limit and in-progress test cases amount + result = eop.test__ip_limit(response, amount); + } + else if (opType == "/test-goto") { + auto it = j.find("id"); + if (it != j.end()) + result = eop.test__goto(response, it->get()); + else + response += "missing 'id' integer field"; + } + else if (opType == "/test-run") { + auto it = j.find("id"); + if (it != j.end()) + result = eop.test__run(response, it->get()); + else + response += "missing 'id' integer field"; + } + else if (opType == "/test-look") { + auto it = j.find("id"); + int id = (it != j.end()) ? it->get() : -1; // default is: current + result = eop.test__look(response, id); + } + else if (opType == "/test-state") { + auto it = j.find("id"); + int id = (it != j.end()) ? it->get() : -1; // default is: current + result = eop.test__state(response, id); + } + else if (opType == "/test-interact") { + auto it = j.find("amount"); + if (it != j.end()) { + + auto itI = j.find("id"); + int id = (itI != j.end()) ? itI->get() : -1; // default is: current + + result = eop.test__interact(response, it->get(), id); + } + else + response += "missing 'amount' integer field"; + } + else if (opType == "/test-reset") { + auto it = j.find("type"); + if (it != j.end()) { + + auto itI = j.find("id"); + int id = (itI != j.end()) ? itI->get() : -2; // default is: apply to all the tests + + if ((*it == "soft") || (*it == "hard")) { + result = eop.test__reset(response, (*it == "soft"), id); + } + else + response += "invalid 'type' string field (allowed: soft|hard)"; + } + else + response += "missing 'type' string field"; + } + else if (opType == "/test-repeats") { + auto it = j.find("amount"); + if (it != j.end()) + result = eop.test__repeats(response, it->get()); + else + response += "missing 'amount' integer field"; + } + else if (opType == "/test-auto-reset") { + auto it = j.find("type"); + if (it != j.end()) { + + if ((*it == "soft") || (*it == "hard")) { + result = eop.test__auto_reset(response, (*it == "soft")); + } + else + response += "invalid 'type' string field (allowed: soft|hard)"; + } + else + response += "missing 'type' string field"; + } + else if (opType == "/test-initialized") { + result = eop.test__initialized(response); + } + else if (opType == "/test-finished") { + result = eop.test__finished(response); + } + else if (opType == "/test-clear") { + result = eop.test__clear(response); + } + else if (opType == "/test-junit") { + auto it = j.find("targetFile"); + std::string targetFile = (it != j.end()) ? *it : ""; // default is: get junit on response instead dumping on file + result = eop.test__junit(response, targetFile); + } + else if (opType == "/test-summary-counts") { + result = eop.test__summary_counts(response); + } + else if (opType == "/test-summary-states") { + result = eop.test__summary_states(response); + } + else if (opType == "/test-summary") { + result = eop.test__summary(response); + } + else if (opType == "/test-report") { + auto it = j.find("state"); + std::string state = (it != j.end()) ? *it : "all"; // initialized|in-progress|failed|success|[all]|none + + auto itA = j.find("action"); + std::string action = (itA != j.end()) ? *itA : "enable"; // default is: enable + + if ((action == "enable") || (action == "disable")) { + result = eop.test__report(response, state, (action == "enable")); + } + else + response += "invalid 'action' string field (allowed: enable|disable)"; + } + else if ((opType == "/test-report-hex")||(opType == "/test-dump_stdout")) { + + auto itA = j.find("action"); + std::string action = (itA != j.end()) ? *itA : "enable"; // default is: enable + + if ((action == "enable") || (action == "disable")) { + bool enable = (action == "enable"); + + if (opType == "/test-report-hex") + result = eop.test__report_hex(response, enable); + else + result = eop.test__dump_stdout(response, enable); + } + else + response += "invalid 'action' string field (allowed: enable|disable)"; + } + + + return result; } + diff --git a/example/diameter/launcher/MyHandler.hpp b/example/diameter/launcher/MyHandler.hpp index 5f04d91..9e849a3 100644 --- a/example/diameter/launcher/MyHandler.hpp +++ b/example/diameter/launcher/MyHandler.hpp @@ -12,6 +12,7 @@ // Project #include #include +#include class MyHandler : public anna::http::Handler { @@ -21,9 +22,15 @@ public: } private: + void splitURI(const std::string &uri, std::string &operation, std::string & param1, std::string & param2) const; + + void sendResponse(anna::comm::ClientSocket&, anna::http::Response *response); + bool doGET(const std::string &uri, std::string &response); + bool doPOST(const std::string &uri, const nlohmann::json &j, std::string &reponse); void evRequest(anna::comm::ClientSocket&, const anna::http::Request& request) throw(anna::RuntimeException); void evResponse(anna::comm::ClientSocket&, const anna::http::Response&) throw(anna::RuntimeException) {;} }; #endif + diff --git a/example/diameter/launcher/deploy-adml-http.sh b/example/diameter/launcher/deploy-adml-http.sh index 8547b61..0fc848e 100755 --- a/example/diameter/launcher/deploy-adml-http.sh +++ b/example/diameter/launcher/deploy-adml-http.sh @@ -71,7 +71,7 @@ mkdir -p ${ADML}/counters mkdir -p ${ADML}/test-reports # Scripts: -cp ${PROJECT_ROOT}/example/diameter/launcher/resources/scripts/operation_curl.sh ${ADML}/operation.sh +cp ${PROJECT_ROOT}/example/diameter/launcher/resources/scripts/operation.sh ${ADML} # Templates: mkdir $DTDs @@ -115,6 +115,13 @@ echo "localhost:8000" > ${ADML}/.httpServer # Help cp ${PROJECT_ROOT}/example/diameter/launcher/resources/HELP.md ${ADML} +# Simple services which only loads the diameter base dictionary: +cat << EOF > ${ADML}/services.xml + + + +EOF + # Remove AOTS actions: rm ${ADML}/ACTIONS.md diff --git a/example/diameter/launcher/deploy-aots.sh b/example/diameter/launcher/deploy-aots.sh index 3319dc5..d947578 100755 --- a/example/diameter/launcher/deploy-aots.sh +++ b/example/diameter/launcher/deploy-aots.sh @@ -71,7 +71,7 @@ mkdir -p ${ADML}/counters mkdir -p ${ADML}/test-reports # Scripts: -cp ${PROJECT_ROOT}/example/diameter/launcher/resources/scripts/operation_signal.sh ${ADML}/operation.sh +cp ${PROJECT_ROOT}/example/diameter/launcher/resources/scripts/operation.sh ${ADML} # Templates: mkdir $DTDs diff --git a/example/diameter/launcher/deployments/advanced/answerXml.sh b/example/diameter/launcher/deployments/advanced/answerXml.sh index ce4aa40..7a98cad 100755 --- a/example/diameter/launcher/deployments/advanced/answerXml.sh +++ b/example/diameter/launcher/deployments/advanced/answerXml.sh @@ -1,13 +1,9 @@ #!/bin/bash -> curl_log.txt -TRACE="--trace-ascii curl_log.txt" -SERVER=`cat .httpServer` - echo echo -echo "Use: $0 [xml_file] [2e]" +echo "Use: $0 [json file for REST API] [2e]" echo -echo "Programm 'xml_file' answer to the diameter client or to the server (entity) when" +echo "Program answer to the diameter client or to the server (entity) when" echo "'2e' parameter is provided. If missing xml file, current programmed answers will" echo "be shown: '$0' (answers to client), '$0 2e' (answers to server)." echo @@ -16,7 +12,7 @@ then if test "$1" = "2e" then operation="answerxml2e" - else + else [[ ! -f "$1" ]] && { echo "ERROR: file '$1' not found" ; echo; echo; exit ; } operation="answerxml|$1" [[ "$2" = "2e" ]] && operation="answerxml2e|$1" @@ -25,5 +21,4 @@ else operation="answerxml" fi echo -curl -m 1 --data "$operation" $TRACE ${SERVER} - +./operation.sh "$operation" diff --git a/example/diameter/launcher/deployments/advanced/batch.sh b/example/diameter/launcher/deployments/advanced/batch.sh deleted file mode 100755 index 9699243..0000000 --- a/example/diameter/launcher/deployments/advanced/batch.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash -> curl_log.txt -TRACE="--trace-ascii curl_log.txt" -SERVER=`cat .httpServer` - -use () { - - echo "Use: $0 [time between operations: 0 second by default]" - echo - echo " Batch launcher script" - echo " ---------------------" - echo - echo " Test file must contain a operations with this syntax: |||..." - echo " Three operations: code, decode and sendxml:" - echo - echo " code|| i.e.: code|1.xml|2.hex" - echo " decode|| i.e.: decode|2.hex|1.xml-bis" - echo " sendxml| i.e.: sendxml|1.xml" - echo - echo " Test file example:" - echo - echo " $0 1xml-23.txt" - echo - echo " where 1xml-23.txt contains:" - echo " Encode 1.xml to 2.hex|code|1.xml|2.hex" - echo " Decode 2.hex to 3.xml (we will diff 1.xml and 3.xml)|decode|2.hex|3.xml" - echo - echo - echo " Test file could contain any number of operations and could include comments (will be ignored)." - echo - exit -} - -echo -echo -[[ "$1" = "" ]] && use -LAPSE=${2:-0} -echo -echo "Test '$1' is going to be launched:" -echo -cat $1 -echo -echo "Pulse ENTER para lanzar, CTRL+C para abortar..." -read dummy - -while read -r line -do - comment=$(echo $line | grep "^#") - ok= - [[ "$comment" = "" ]] && { [[ "$line" != "" ]] && ok=s ; } - if test "$ok" = "s" - then - echo "Launching $(echo $line | cut -d'|' -f1) ..." - sleep $LAPSE - operation=$(echo $line | cut -d'|' -f2-) - curl -m 1 --data "$operation" $TRACE ${SERVER} - fi - -done < $1 - diff --git a/example/diameter/launcher/deployments/advanced/burst.sh b/example/diameter/launcher/deployments/advanced/burst.sh index b05b6e0..7b912cf 100755 --- a/example/diameter/launcher/deployments/advanced/burst.sh +++ b/example/diameter/launcher/deployments/advanced/burst.sh @@ -1,7 +1,4 @@ #!/bin/bash -> curl_log.txt -TRACE="--trace-ascii curl_log.txt" -SERVER=`cat .httpServer` burstMargin__dflt=1 salir () { @@ -51,7 +48,7 @@ uso () { echo echo " resume:" echo " Resume an stopped burst launch from the same point (start will reset" - echo " the work pointer to the first burst list position). This is equivalent" + echo " the work pointer to the first burst list position). This is equivalent" echo " to one-message-push operation." echo echo " repeat [[yes] | no]:" @@ -77,18 +74,14 @@ uso () { echo " Show programmed burst message for order provided." echo echo - salir -} - -_curl () { - curl -m 5 --data "$1" $TRACE ${SERVER} + salir } echo echo [[ "$1" = "" ]] && uso case $1 in - clear) _curl "burst|clear" + clear) ./operation.sh "burst|clear" ;; load) [[ "$3" = "" ]] && uso [[ ! -f "${3}.sh" ]] && salir "Burst generation file '${3}.sh' not found!" @@ -97,43 +90,43 @@ case $1 in then count=$2 RESTO=$((count%4)) - [[ "$RESTO" != "0" ]] && salir "Data context should load a multiple of 4 (messages generated by data.sh)" + [[ "$RESTO" != "0" ]] && salir "Data context should load a multiple of 4 (messages generated by data.sh)" fi count=1 while test "$count" -le "$2" do ${3}.sh $count > .${3}.xml echo -n "Loading message ${count}; " - _curl "burst|load|.${3}.xml" + ./operation.sh "burst|load|.${3}.xml" count=$((count+1)) done ;; start) load=$burstMargin__dflt [[ "$2" != "" ]] && load=$2 entero $load - _curl "burst|start|$load" + ./operation.sh "burst|start|$load" ;; pop) release=$burstMargin__dflt [[ "$2" != "" ]] && release=$2 entero $release - _curl "burst|pop|$release" + ./operation.sh "burst|pop|$release" ;; push) [[ "$2" = "" ]] && uso entero $2 - _curl "burst|push|$2" + ./operation.sh "burst|push|$2" ;; - stop) _curl "burst|stop" + stop) ./operation.sh "burst|stop" ;; - resume) _curl "burst|push|1" + resume) ./operation.sh "burst|push|1" ;; repeat) repeat=yes [[ "$2" != "" ]] && repeat=$2 - _curl "burst|repeat|$repeat" + ./operation.sh "burst|repeat|$repeat" ;; send) amount=1 [[ "$2" != "" ]] && amount=$2 entero $amount - _curl "burst|send|$amount" + ./operation.sh "burst|send|$amount" ;; sendXS) [[ "$2" = "" ]] && uso limit=$3 @@ -157,13 +150,13 @@ case $1 in TPS=$READ_TPS [[ "$TPS" = "0" ]] && salir "Test stopped due to 0-tps value read" # Background: - _curl "burst|send|$amount" & + ./operation.sh "burst|send|$amount" & count=$((count+amount)) AFTER_ns=`date +%s%N` # Real tps: REAL_TPS=$(calc "1000000000 * $count / ($AFTER_ns - $BEGIN_ns)") echo $REAL_TPS > .real_tps - + COEF=1 [[ $(calc "$TPS > $REAL_TPS") = "1" ]] && COEF=$(calc "$REAL_TPS / $TPS") K=$(calc "$COEF ^ 10") @@ -173,11 +166,11 @@ case $1 in ;; goto) [[ "$2" = "" ]] && uso entero $2 - _curl "burst|goto|$2" + ./operation.sh "burst|goto|$2" ;; look) [[ "$2" = "" ]] && uso entero $2 - _curl "burst|look|$2" + ./operation.sh "burst|look|$2" ;; *) uso esac diff --git a/example/diameter/launcher/deployments/advanced/clientSocketManager.sh b/example/diameter/launcher/deployments/advanced/clientSocketManager.sh index 4c801b2..14c2977 100755 --- a/example/diameter/launcher/deployments/advanced/clientSocketManager.sh +++ b/example/diameter/launcher/deployments/advanced/clientSocketManager.sh @@ -1,7 +1,4 @@ #!/bin/bash -> curl_log.txt -TRACE="--trace-ascii curl_log.txt" -SERVER=`cat .httpServer` ENTITY=`cat .entity 2>/dev/null` # If missing following, 1 is assigned (ENTITY_SS_1 is ENTITY_SS - 1) ENTITY_SS=`cat .entityServerSessions 2>/dev/null` @@ -24,19 +21,19 @@ states () { echo echo "Select option to switch (0 = quit):" echo - option=1 + option=1 for i in `echo $ENTITY | sed 's/,/ /g'` do for j in `seq 0 $ENTITY_SS_1` do TARGET="${i}|${j}" - RES=$(curl -m 1 --data "shown|$TARGET" ${SERVER} 2>&1 | tail -1 | grep "true$") + RES=$(./operation.sh "shown|$TARGET" | tail -1 | grep "true$") res=hidden [[ "$RES" != "" ]] && res=shown echo "${option}. $TARGET ($res)" action=show [[ "$RES" != "" ]] && action=hide - echo "curl -m 1 --data \"$action|$TARGET\" ${SERVER}" > .switch_${option} + echo "./operation \"$action|$TARGET\"" > .switch_${option} chmod a+x .switch_${option} option=$((option+1)) done @@ -52,7 +49,7 @@ then states read option [[ "$option" = "0" ]] && break - .switch_${option} + .switch_${option} done echo echo "Exiting" @@ -66,5 +63,5 @@ echo echo operation="$1|$TARGET" [[ "$TARGET" = "" ]] && operation="$1" -curl -m 1 --data "$operation" $TRACE ${SERVER} +./operation.sh "$operation" diff --git a/example/diameter/launcher/deployments/advanced/code.sh b/example/diameter/launcher/deployments/advanced/code.sh index 9ca200c..385dde1 100755 --- a/example/diameter/launcher/deployments/advanced/code.sh +++ b/example/diameter/launcher/deployments/advanced/code.sh @@ -1,7 +1,4 @@ #!/bin/bash -> curl_log.txt -TRACE="--trace-ascii curl_log.txt" -SERVER=`cat .httpServer` use () { @@ -16,6 +13,5 @@ echo [[ "$1" = "" ]] && use [[ ! -f "$1" ]] && { echo "ERROR: file '$1' not found" ; echo; echo; exit ; } echo -operation="code|$1|${1}.hex" -curl -m 1 --data "$operation" $TRACE ${SERVER} +./operation.sh "code|$1|${1}.hex" diff --git a/example/diameter/launcher/deployments/advanced/collect.sh b/example/diameter/launcher/deployments/advanced/collect.sh index 5fb95ec..f1853df 100755 --- a/example/diameter/launcher/deployments/advanced/collect.sh +++ b/example/diameter/launcher/deployments/advanced/collect.sh @@ -1,6 +1,3 @@ #!/bin/bash -> curl_log.txt -TRACE="--trace-ascii curl_log.txt" -SERVER=`cat .httpServer` -curl -m 1 --data "collect" $TRACE ${SERVER} +./operation.sh "collect" diff --git a/example/diameter/launcher/deployments/advanced/decode.sh b/example/diameter/launcher/deployments/advanced/decode.sh index 3924f73..e771612 100755 --- a/example/diameter/launcher/deployments/advanced/decode.sh +++ b/example/diameter/launcher/deployments/advanced/decode.sh @@ -1,8 +1,4 @@ #!/bin/bash -> curl_log.txt -TRACE="--trace-ascii curl_log.txt" -SERVER=`cat .httpServer` - use () { echo "Use: $0 " @@ -16,6 +12,5 @@ echo [[ "$1" = "" ]] && use [[ ! -f "$1" ]] && { echo "ERROR: file '$1' not found" ; echo; echo; exit ; } echo -operation="decode|$1|${1}.xml" -curl -m 1 --data "$operation" $TRACE ${SERVER} +./operation.sh "decode|$1|${1}.xml" diff --git a/example/diameter/launcher/deployments/advanced/diameterServerSessions.sh b/example/diameter/launcher/deployments/advanced/diameterServerSessions.sh index dc72c87..404652d 100755 --- a/example/diameter/launcher/deployments/advanced/diameterServerSessions.sh +++ b/example/diameter/launcher/deployments/advanced/diameterServerSessions.sh @@ -1,7 +1,4 @@ #!/bin/bash -> curl_log.txt -TRACE="--trace-ascii curl_log.txt" -SERVER=`cat .httpServer` use () { @@ -15,6 +12,5 @@ use () { echo [[ "$1" = "" ]] && use echo -operation="diameterServerSessions|$1" -curl -m 1 --data "$operation" $TRACE ${SERVER} +./operation.sh "diameterServerSessions|$1" diff --git a/example/diameter/launcher/deployments/advanced/loadXml.sh b/example/diameter/launcher/deployments/advanced/loadXml.sh index c2ac03f..8991e3e 100755 --- a/example/diameter/launcher/deployments/advanced/loadXml.sh +++ b/example/diameter/launcher/deployments/advanced/loadXml.sh @@ -1,7 +1,4 @@ #!/bin/bash -> curl_log.txt -TRACE="--trace-ascii curl_log.txt" -SERVER=`cat .httpServer` use () { @@ -18,6 +15,5 @@ echo [[ "$1" = "" ]] && use [[ ! -f "$1" ]] && { echo "ERROR: file '$1' not found" ; echo; echo; exit ; } echo -operation="loadxml|$1" -curl -m 1 --data "$operation" $TRACE ${SERVER} +./operation.sh "loadxml|$1" diff --git a/example/diameter/launcher/deployments/advanced/operation.sh b/example/diameter/launcher/deployments/advanced/operation.sh new file mode 120000 index 0000000..62e0537 --- /dev/null +++ b/example/diameter/launcher/deployments/advanced/operation.sh @@ -0,0 +1 @@ +../../resources/scripts/operation.sh \ No newline at end of file diff --git a/example/diameter/launcher/deployments/advanced/sendXml.sh b/example/diameter/launcher/deployments/advanced/sendXml.sh index d9aaa33..7237686 100755 --- a/example/diameter/launcher/deployments/advanced/sendXml.sh +++ b/example/diameter/launcher/deployments/advanced/sendXml.sh @@ -1,7 +1,4 @@ #!/bin/bash -> curl_log.txt -TRACE="--trace-ascii curl_log.txt" -SERVER=`cat .httpServer` use () { @@ -19,5 +16,5 @@ echo operation="sendxml|$1" [[ "$2" = "2c" ]] && operation="sendxml2c|$1" -curl -m 1 --data "$operation" $TRACE ${SERVER} +./operation.sh "$operation" diff --git a/example/diameter/launcher/deployments/basic/operation.sh b/example/diameter/launcher/deployments/basic/operation.sh index c9b45ae..62e0537 120000 --- a/example/diameter/launcher/deployments/basic/operation.sh +++ b/example/diameter/launcher/deployments/basic/operation.sh @@ -1 +1 @@ -../../resources/scripts/operation_signal.sh \ No newline at end of file +../../resources/scripts/operation.sh \ No newline at end of file diff --git a/example/diameter/launcher/deployments/ft-client/operation.sh b/example/diameter/launcher/deployments/ft-client/operation.sh index c9b45ae..62e0537 120000 --- a/example/diameter/launcher/deployments/ft-client/operation.sh +++ b/example/diameter/launcher/deployments/ft-client/operation.sh @@ -1 +1 @@ -../../resources/scripts/operation_signal.sh \ No newline at end of file +../../resources/scripts/operation.sh \ No newline at end of file diff --git a/example/diameter/launcher/deployments/st-client/operation.sh b/example/diameter/launcher/deployments/st-client/operation.sh index c9b45ae..62e0537 120000 --- a/example/diameter/launcher/deployments/st-client/operation.sh +++ b/example/diameter/launcher/deployments/st-client/operation.sh @@ -1 +1 @@ -../../resources/scripts/operation_signal.sh \ No newline at end of file +../../resources/scripts/operation.sh \ No newline at end of file diff --git a/example/diameter/launcher/libraries.txt b/example/diameter/launcher/libraries.txt index 88ac68e..f1646a5 100644 --- a/example/diameter/launcher/libraries.txt +++ b/example/diameter/launcher/libraries.txt @@ -12,6 +12,7 @@ anna_launcher_procedure_default_shared anna_testing_shared anna_io_static anna_core_static +anna_json_static pthread rt xml2 diff --git a/example/diameter/launcher/main.cpp b/example/diameter/launcher/main.cpp index a827759..ca6d28d 100644 --- a/example/diameter/launcher/main.cpp +++ b/example/diameter/launcher/main.cpp @@ -56,7 +56,7 @@ int main(int argc, const char** argv) { // Communications commandLine.add("reconnectionPeriod", anna::CommandLine::Argument::Optional, "Milliseconds to recover diameter client-session when server connection has been broken. If missing, default value of 10000 will be assigned"); commandLine.add("httpServer", anna::CommandLine::Argument::Optional, "HTTP Management interface address (using i.e. curl tool) in '
:' format. For example: 10.20.30.40:8080"); - commandLine.add("httpServerShared", anna::CommandLine::Argument::Optional, "Enables shared bind for HTTP Management interface address. It would be useful i.e. to allow a great amount of curl operations per second", false); + commandLine.add("httpServerShared", anna::CommandLine::Argument::Optional, "Enables shared bind for HTTP Management interface address. It would be useful i.e. to allow a great amount of HTTP1.1 operations per second", false); // Automatic error answer commandLine.add("ignoreErrors", anna::CommandLine::Argument::Optional, "Local server skips requests errors analysis which would prepare automatic answers for them when a problem is found. If no answer is programmed and entity is configured, a failed request would be forwarded (delegates at the end point) even if this parameter is missing", false); diff --git a/example/diameter/launcher/resources/HELP.md b/example/diameter/launcher/resources/HELP.md index 69f0615..8b453fa 100644 --- a/example/diameter/launcher/resources/HELP.md +++ b/example/diameter/launcher/resources/HELP.md @@ -94,7 +94,7 @@ All the subsequent operations will be forced to work with this node, which makes Empty parameter will show the current configuration. -**node_auto** +**node-auto** Returns to the default behaviour (smart node selection). Depending on the operation, this could imply a global action scope, affecting to all the registered hosts. @@ -125,14 +125,31 @@ If you need to load services as deltas, you must firstly load the diameter base Updates the maximum number of accepted connections to diameter server socket. -**context[|]** +**change-dir[|directory]** -Application context could also be written by mean this operation (not only through SIGUSR1). If optional path file is missing, default '/var/tmp/anna.context.' will be used. +Changes the execution point which could be fine to ease some file system interaction tasks. Be care about some requirements (for example if you have a user defined counters directory as relative path this must exists from the new execution directory). If nothing provided, initial working directory will be restored. + +### Client sessions visibility + +|
:| + +*Actions*: hide, show (update state) and hidden, shown (query state). +Acts over a client session for messages delivery (except CER/A, DWR/A, DPR/A). +If missing server (first parameter) all applications sockets will be affected. +If missing socket (second parameter) for specific server, all its sockets will be affected. + +All application client sessions are shown on startup, but standard delivery only use primary server ones except if fails. Balance configuration use all the allowed sockets. You could also use command line 'sessionBasedModelsClientSocketSelection' to force traffic flow over certain client sessions, but for this, hide/show feature seems easier. + +### Snapshots **collect** Reset statistics and counters to start a new test stage of performance measurement. Context data can be written at '/var/tmp/anna.context.' by mean 'kill -10 ' or sending operation 'context|[target file]'. This operation applies over all the registered host nodes except if one specific working node has been set. +**context[|]** + +Application context could also be written by mean this operation (not only through SIGUSR1). If optional path file is missing, default '/var/tmp/anna.context.' will be used. + **forceCountersRecord** Forces dump to file the current counters of the process. @@ -141,10 +158,6 @@ Forces dump to file the current counters of the process. Log statistics samples for the provided comma-separated concept id list, over './sample..csv' files. For example: "1,2" will log concepts 1 and 2. Reserved words "all"/"none" activates/deactivates all registered statistics concept identifiers. That ids are shown at context dump. -**change-dir[|directory]** - -Changes the execution point which could be fine to ease some file system interaction tasks. Be care about some requirements (for example if you have a user defined counters directory as relative path this must exists from the new execution directory). If nothing provided, initial working directory will be restored. - **show-oam** Dumps current counters of the process. This is also done at process context dump. @@ -153,15 +166,6 @@ Dumps current counters of the process. This is also done at process context dump Dumps statistics of the process. This is also done at process context dump. -**visibility||
:** - -*Actions*: hide, show (update state) and hidden, shown (query state). -Acts over a client session for messages delivery (except CER/A, DWR/A, DPR/A). -If missing server (first parameter) all applications sockets will be affected. -If missing socket (second parameter) for specific server, all its sockets will be affected. - -All application client sessions are shown on startup, but standard delivery only use primary server ones except if fails. Balance configuration use all the allowed sockets. You could also use command line 'sessionBasedModelsClientSocketSelection' to force traffic flow over certain client sessions, but for this, hide/show feature seems easier. - ### Flow operations **sendxml2e|** @@ -325,7 +329,7 @@ Adds a new step to the test case with provided identifier. If provided identifie > If not, the hex content will be understood as whole message and then, borders will be added (^$) and sequence information bypassed even for diameter answers. > > **wait-xml|[|strict]** -> Wait condition from entity (waitfe-xml) or client (waitfc-xml) to match the serialized xml content for received messages against source file (xml representation). Fix mode must be enabled to avoid unexpected matching behaviour. If you need a strict matching you must add parameter 'strict', if not, regexp is built ignoring sequence information (hop-by-hop-id=\"[0-9]+\" end-to-end-id=\"[0-9]+\") and Origin-State-Id value. All LF codes will be internally removed when comparison is executed in order to ease xml content configuration. +> Wait condition from entity (waitfe-xml) or client (waitfc-xml) to match the serialized xml content for received messages against source file (xml representation). Fix mode must be enabled to avoid unexpected matching behaviour. If you need a strict matching you must add parameter 'strict', if not, regexp is built ignoring sequence information (hop-by-hop-id=\"[0-9]+\" end-to-end-id=\"[0-9]+\") and Origin-State-Id value. > > **wait|** > @@ -523,19 +527,51 @@ This operation requires advanced programming and knowlegde of ANNA Diameter stac # HTTP Interface -All the operations described above can be used through the optional HTTP interface. You only have +Most of the operations described above can be used through the optional HTTP1.1 interface. You only have to define the http server at the command line with something like: '--httpServer localhost:8000'. -REST API specification: +REST API specification. + +**We will describe briefly the supported *GET* & *POST* operations, but you could extend the explanation from SIGUSR2 interface description above**. + +Implemented status codes: *200* (ok), *400* (bad request) and *405* (method not allowed). ## GET > curl -v --request GET localhost:8000 -HTTP2 is not served, so, you should use *nginx* reverse proxy or similar, to pass HTTP1 requests to the server, allowing the external use of HTTP2 clients like *nghttp*: +You could also expose the service as HTTP2 server through *nginx* working as reverse proxy (*adml-http* image is built in this way although internally provides HTTP operations script only for 1.1). Then you could use HTTP2 clients like *nghttp*: > nghttp -v -H ":method: GET" "" -### GET /xxxx + + +### Snapshots + +#### GET /show-oam + +Retrieve oam xml report (will be *base64-encoded* at response). + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +#### GET /show-stats + +Retrieve statistics xml report (will be *base64-encoded* at response). + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` @@ -543,6 +579,685 @@ HTTP2 is not served, so, you should use *nginx* reverse proxy or similar, to pas > curl -v --request POST -H "Content-Type: application/json" localhost:8000 -d@test.json -HTTP2 is not served, so, you should use *nginx* reverse proxy or similar, to pass HTTP1 requests to the server, allowing the external use of HTTP2 clients like *nghttp*: +You could also expose the service as HTTP2 server through *nginx* working as reverse proxy (*adml-http* image is built in this way although internally provides HTTP operations script only for 1.1). Then you could use HTTP2 clients like *nghttp*: + +> nghttp -v -H ":method: POST" -d test.json "" + + + +**Important note:** Anna Suite work natively with xml. Although REST API supports diameter messages (*diameterJson* fields) and services configuration (*servicesJson* fields) in *json* format, these messages are not validated directly with a *json schema*. Instead, the *json* object are converted to *xml* at ADML and the resulting xml representations are validates against *dtd* schemas: + +Schema for diameter messages: [here](../../../../include/anna/diameter/codec/message.dtd). + +Schema for configuration services: [here](./services_examples/services.dtd). + +There are lots of examples for *xml diameter messages and xml services* (normally deployed with *ADML* at `./xml_examples` and `./services_examples`), then you could transform them to the *json format* accepted by the REST API easily, for example using *xmltodict* python module: + +``` +#!/usr/bin/python3 +import xmltodict +import json +import sys + +try: + file = sys.argv[1] +except: + print("Usage: " + sys.argv[0] + " ") + sys.exit(1) + +try: + with open(file, 'r') as myfile: + xml = myfile.read() +except: + print("ERROR reading '" + file + "'") + sys.exit(1) + +# although default attribute prefix is '@', anyway we will +# force the value just in case xmltodict implementation +# changes. The anna::core::functions::json2xml helper, +# assumes this prefix in order to work properly. +my_dict=xmltodict.parse(xml, attr_prefix='@') +json_data=json.dumps(my_dict, indent=3, sort_keys=True) +print(json_data) +``` + +**Former script is packaged** together with REST API component tests under `./diameterJsonHelper/xml2json.py`. + +Native example for *AA-Request* diameter message: + +``` + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +Corresponding content in *json* format: + +``` +{ + "message": { + "@application-id": "16777236", + "@end-to-end-id": "0", + "@hop-by-hop-id": "0", + "@name": "AA-Request", + "@p-bit": "yes", + "@version": "1", + "avp": [ + { + "@data": "test1;afNodeHostname.nodeHostRealm.com;1;8033450", + "@name": "Session-Id" + }, + { + "@data": "16777236", + "@name": "Auth-Application-Id" + }, + { + "@data": "afHost.afRealm.com", + "@name": "Origin-Host" + }, + { + "@data": "afRealm.com", + "@name": "Origin-Realm" + }, + { + "@data": "operatorRealm.com", + "@name": "Destination-Realm" + }, + { + "@data": "ownHostId.operatorRealm.com", + "@name": "Destination-Host" + }, + { + "@hex-data": "313232", + "@name": "AF-Application-Identifier" + }, + { + "@name": "Media-Component-Description", + "avp": [ + { + "@data": "0", + "@name": "Media-Component-Number" + }, + { + "@hex-data": "313232", + "@name": "AF-Application-Identifier" + }, + { + "@data": "127", + "@name": "Max-Requested-Bandwidth-UL" + }, + { + "@data": "133", + "@name": "Max-Requested-Bandwidth-DL" + }, + { + "@alias": "ENABLED", + "@data": "2", + "@name": "Flow-Status" + }, + { + "@alias": "DEFAULT", + "@data": "0", + "@name": "Reservation-Priority" + } + ] + }, + { + "@alias": "FINAL_SERVICE_INFORMATION", + "@data": "0", + "@name": "Service-Info-Status" + }, + { + "@name": "Subscription-Id", + "avp": [ + { + "@alias": "END_USER_E164", + "@data": "0", + "@name": "Subscription-Id-Type" + }, + { + "@data": "626037099", + "@name": "Subscription-Id-Data" + } + ] + }, + { + "@hex-data": "3139322e3136382e302e31", + "@name": "Framed-IP-Address" + }, + { + "@hex-data": "5741502e4d4f564953544152", + "@name": "Called-Station-Id" + } + ] + } +} +``` + +Native example for a services configuration example: + +``` + + + + + + + + + +``` + +And the converted *json* equivalent: + +``` +{ + "services": { + "node": [ + { + "@applicationId": "0", + "@entity": "localhost:3868", + "@originHost": "afHost.afRealm.com" + }, + { + "@applicationId": "0", + "@diameterServer": "localhost:3868", + "@diameterServerSessions": "1", + "@originHost": "ownHostId.operatorRealm.com" + } + ], + "stack": { + "@dictionary": "dictionary.xml", + "@fixMode": "Always", + "@id": "0", + "@ignoreFlagsOnValidation": "yes" + } + } +} +``` + + + +**Note that the '@' character is mandatory to indicate attribute keys, and it is necessary to complete the *"json to xml"* conversion successfully at ADML REST Server endpoint.** + + + +### Node management + +#### POST /node + +Selects a context working node by mean a registered name (origin-host). If empty, current node information is retrieved. + +**Request body**: + +``` +{ + "name":"[node name]" +} +``` + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +#### POST /node-auto + +Smart node selection. + +**Request body**: none + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +### Parsing operations + +#### POST /code + +Encodes a diameter json into hexadecimal representation. + +**Request body**: + +``` +{ + "diameterJson": +} +``` + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +#### POST /decode + +Decodes an hexadecimal string (no spaces, no colons, i.e.: `01000150c0...`), into diameter xml (*base64-encoded*). + +**Request body**: + +``` +{ + "diameterHex":"" +} +``` + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +#### POST /loadmsg + +Reinterprets diameter json into xml (*base64-encoded*). + +**Request body**: + +``` +{ + "diameterJson": +} +``` + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +### Hot changes + +#### POST /services + +Referred files (*dictionaries, cer, cea, etc.*) shall be accesible for ADML and are not provided in this operation. + +**Request body**: + +``` +{ + "servicesJson": +} +``` + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +#### POST /diameterServerSessions + +Updates diameter server sessions to be accepted. + +**Request body**: + +``` +{ + "sessions": +} +``` + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +#### POST /change-dir + +Updates ADML working directory. + +**Request body**: + +``` +{ + "directory":"" +} +``` + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +### Client sessions visibility + +#### POST /visibility + +**Request body**: + +``` +{ + "action":"" + [, "addressPort":""] + [, "socket":] +} +``` + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +### Snapshots + +#### POST /collect + +Reset statistics and counters. + +**Request body**: none + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +#### POST /context + +Dump ADML context at file path provided. If empty, default path is selected. Context information is not retrieved in the response, so, file is related to ADML execution context. + +**Request body**: + +``` +{ + "targetFile":"[file path]" +} +``` + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +#### POST /forceCountersRecord + +Forces dump to file the current counters of the process. + +**Request body**: none + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +#### POST /log-statistics-samples + +Set the statistics concepts to be logged. To know the concept indentifiers registered, get the ADML context information. + +**Request body**: + +``` +{ + "list":"" +} +``` + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +### Flow operations + +#### POST /sendmsg2e + +Sends diameter json message **to**(2) the connected **entity**(e). + +**Request body**: + +``` +{ + "diameterJson": +} +``` + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +#### POST /sendmsg2c + +Sends diameter json message **to**(2) the connected **client**(c). + +**Request body**: + +``` +{ + "diameterJson": +} +``` + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +#### POST /answermsg2e + +Answers diameter json message **to**(2) the connected **entity**(e). + +Mocking FIFO queue based in message code. + +**Request body**: + +``` +{ + "diameterJson": +} +``` + +or + +``` +{ + "action":"" +} +``` + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +#### POST /answermsg2c + +Answers diameter json message **to**(2) the connected **client**(c). + +Mocking FIFO queue based in message code. + +**Request body**: + +``` +{ + "diameterJson": +} +``` + +or + +``` +{ + "action":"" +} +``` + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +#### POST /sendhex2e + +Sends diameter expressed in hexadecimal string (no spaces, no colons, i.e.: `01000150c0...`), **to**(2) the connected **entity**(e). + +**Request body**: + +``` +{ + "diameterHex":"" +} +``` + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +#### POST /sendhex2c + +Sends diameter expressed in hexadecimal string (no spaces, no colons, i.e.: `01000150c0...`), **to**(2) the connected **client**(c). + +**Request body**: + +``` +{ + "diameterHex":"" +} +``` + +**Response body**: + +``` +{ + "result":"", + "response":"" +} +``` + +### FSM testing + +ADML implements a bulting *Finite State Machine* to plan testing flows with a great flexibility. + +#### POST /xxxxxxxx + +Referred files (*dictionaries, cer, cea, etc.*) shall be accesible for ADML and are not provided in this operation. + +**Request body**: { "servicesJson":} + +**Response body**: { "result":"", "response":""} + +#### POST /xxxxxxxx + +Referred files (*dictionaries, cer, cea, etc.*) shall be accesible for ADML and are not provided in this operation. + +**Request body**: { "servicesJson":} + +**Response body**: { "result":"", "response":""} + + -> nghttp -v -H ":method: POST" -d test.json "" \ No newline at end of file diff --git a/example/diameter/launcher/resources/rest_api/README.md b/example/diameter/launcher/resources/rest_api/README.md new file mode 100644 index 0000000..5eb6dba --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/README.md @@ -0,0 +1,42 @@ +# ADML REST API + +## References + +Read [deployment instructions](../../../../../INSTALL_ADML_HTTP.md) document to **create** *ADML HTTP* image and **run** it. + +Read [REST API](../../../../../example/diameter/launcher/resources/HELP.md) implementation to learn how to control *ADML HTTP* execution. + +## Helpers + +Located at directory [helpers](./helpers). + +These helpers eases *pcaps* translation into *REST API* input artifacts: + +### tsharkDecoder.sh + +This utility disects a *pcap* file and gets metadata and hexadecimal encoding for diameter messages found in the capture. Those files are named as `.hex` and `.metadata`. Also, a list of detected *Origin-Host AVPs* is dump in the file `origin-hosts`. + +For example: + +> ./tsharkDecoder.sh example.pcap +> + +### decodeHex.sh + +Use this to decode *hex* files. For example: + +> ./decodeHex.sh 8.hex + +This will create the corresponding diameter message representation in two formats: + +- **xml** +- **json** (through diameterJsonHelper resources) + + + +## Component Test + +Go to [ct](./ct) directory and execute: + +1. pip3 install -r requirements.txt +2. pytest *-or-* pytest -n (parallel execution) \ No newline at end of file diff --git a/example/diameter/launcher/resources/rest_api/ct/client-sessions-visibility/visibility_test.py b/example/diameter/launcher/resources/rest_api/ct/client-sessions-visibility/visibility_test.py new file mode 100644 index 0000000..ad6fce6 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/client-sessions-visibility/visibility_test.py @@ -0,0 +1,48 @@ +import pytest + + +def test_001_disable_operated_client_channel_confirm_disabled_state_adml(admlc): + + # FIRST WE SET THE CLIENT ORIGIN HOST: + requestBody = { "name":"afHost.afRealm.com" } + responseBodyRef = { "success":"true", "response":"Forced node is now 'afHost.afRealm.com'" } + + # Send POST + response = admlc.postDict("/node", requestBody) + + # Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) + + requestBodyHide = { "action":"hide" } + requestBodyShown = { "action":"shown" } + requestBodyHidden = { "action":"hidden" } + + responseBodyHideRef = { "success":"true", "response":"" } + responseBodyTrueRef = { "success":"true", "response":"true" } + responseBodyFalseRef = { "success":"true", "response":"false" } + + response = admlc.postDict("/visibility", requestBodyHide) + admlc.assert_response__status_body_headers(response, 200, responseBodyHideRef) + response = admlc.postDict("/visibility", requestBodyShown) + admlc.assert_response__status_body_headers(response, 200, responseBodyFalseRef) + response = admlc.postDict("/visibility", requestBodyHidden) + admlc.assert_response__status_body_headers(response, 200, responseBodyTrueRef) + +def test_002_enable_operated_client_channel_confirm_enabled_state_adml(admlc): + + # ENABLE (show) / DISABLE (hide) / CHECK (hidden/shown) + requestBodyShow = { "action":"show" } + requestBodyShown = { "action":"shown" } + requestBodyHidden = { "action":"hidden" } + + responseBodyShowRef = { "success":"true", "response":"" } + responseBodyTrueRef = { "success":"true", "response":"true" } + responseBodyFalseRef = { "success":"true", "response":"false" } + + response = admlc.postDict("/visibility", requestBodyShow) + admlc.assert_response__status_body_headers(response, 200, responseBodyShowRef) + response = admlc.postDict("/visibility", requestBodyShown) + admlc.assert_response__status_body_headers(response, 200, responseBodyTrueRef) + response = admlc.postDict("/visibility", requestBodyHidden) + admlc.assert_response__status_body_headers(response, 200, responseBodyFalseRef) + diff --git a/example/diameter/launcher/resources/rest_api/ct/conftest.py b/example/diameter/launcher/resources/rest_api/ct/conftest.py new file mode 100644 index 0000000..d6ac9ef --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/conftest.py @@ -0,0 +1,352 @@ +# Keep sorted +import base64 +from collections import defaultdict +import glob +from hyper import HTTP20Connection +#import inspect +import json +import logging +import os +import pytest +import re + +############# +# CONSTANTS # +############# + +# Endpoint +ADML_HOST = 'localhost' +ADML_PORT = '8074' +ADML_ENDPOINT = ADML_HOST + ':' + ADML_PORT +ADML_URI_PREFIX = '' + +# Headers +CONTENT_LENGTH = 'content-length' + +# Flow calculation throw admlf (ADML Flow) fixture: +# flow = admlf.getId() +# +# For sequenced tests, the id is just a monotonically increased number from 1. +# For parallel tests, this id is sequenced from 1 for every worker, then, globally, +# this is a handicap to manage flow ids (tests ids) for ADML FSM (finite state machine). +# We consider a base multiplier of 10000, so collisions would take place when workers +# reserves more than 10000 flows. With a 'worst-case' assumption of `5 flows per test case`, +# you should cover up to 5000 per worker. Anyway, feel free to increase this value, +# specially if you are thinking in use pytest and ADML Agent for system tests. +FLOW_BASE_MULTIPLIER = 10000 + +###################### +# CLASSES & FIXTURES # +###################### + +class Sequencer(object): + def __init__(self, request): + self.sequence = 0 + self.request = request + + def __wid(self): + """ + Returns the worker id, or 'master' if not parallel execution is done + """ + wid = 'master' + if hasattr(self.request.config, 'slaveinput'): + wid = self.request.config.slaveinput['slaveid'] + + return wid + + def getId(self): + """ + Returns the next identifier value (monotonically increased in every call) + """ + self.sequence += 1 + + wid = self.__wid() + if wid == "master": + return self.sequence + + # Workers are named: wd0, wd1, wd2, etc. + wid_number = int(re.findall(r'\d+', wid)[0]) + + return FLOW_BASE_MULTIPLIER * wid_number + self.sequence + + + +@pytest.fixture(scope='session') +def admlf(request): + """ + ADML Flow + """ + return Sequencer(request) + + + +# Logging +class MyLogger: + + # CRITICAL ERROR WARNING INFO DEBUG NOSET + def setLevelInfo(): logging.getLogger().setLevel(logging.INFO) + def setLevelDebug(): logging.getLogger().setLevel(logging.DEBUG) + + def error(message): logging.getLogger().error(message) + def warning(message): logging.getLogger().warning(message) + def info(message): logging.getLogger().info(message) + def debug(message): logging.getLogger().debug(message) + +@pytest.fixture(scope='session') +def mylogger(): + return MyLogger + +MyLogger.logger = logging.getLogger('CT') + +# Base64 encoding: +@pytest.fixture(scope='session') +def b64_encode(): + def encode(message): + message_bytes = message.encode('ascii') + base64_bytes = base64.b64encode(message_bytes) + return base64_bytes.decode('ascii') + return encode + +# Base64 decoding: +@pytest.fixture(scope='session') +def b64_decode(): + def decode(base64_message): + base64_bytes = base64_message.encode('ascii') + message_bytes = base64.b64decode(base64_bytes) + return message_bytes.decode('ascii') + return decode + +# HTTP communication: +class RestClient(object): + """A client helper to perform rest operations: GET, POST. + + Attributes: + endpoint: server endpoint to make the HTTP2.0 connection + """ + + def __init__(self, endpoint): + """Return a RestClient object for ADML endpoint.""" + self._endpoint = endpoint + self._ip = self._endpoint.split(':')[0] + self._connection = HTTP20Connection(host=self._endpoint) + + def _log_http(self, kind, method, url, body, headers): + length = len(body) if body else 0 + MyLogger.info( + '{} {}{} {} headers: {!s} data: {}:{!a}'.format( + method, self._endpoint, url, kind, headers, length, body)) + + def _log_request(self, method, url, body, headers): + self._log_http('REQUEST', method, url, body, headers) + + def _log_response(self, method, url, response): + self._log_http( + 'RESPONSE:{}'.format(response["status"]), method, url, + response["body"], response["headers"]) + + #def log_event(self, level, log_msg): + # # Log caller function name and formated message + # MyLogger.logger.log(level, inspect.getouterframes(inspect.currentframe())[1].function + ': {!a}'.format(log_msg)) + + def parse(self, response): + response_body = response.read(decode_content=True).decode('utf-8') + if len(response_body) != 0: + response_body_dict = json.loads(response_body) + else: + response_body_dict = '' + response_data = { "status":response.status, "body":response_body_dict, "headers":response.headers } + return response_data + + def request(self, requestMethod, requestUrl, requestBody=None, requestHeaders=None): + """ + Returns response data dictionary with 'status', 'body' and 'headers' + """ + requestBody = RestClient._pad_body_and_length(requestBody, requestHeaders) + self._log_request(requestMethod, requestUrl, requestBody, requestHeaders) + self._connection.request(method=requestMethod, url=requestUrl, body=requestBody, headers=requestHeaders) + response = self.parse(self._connection.get_response()) + self._log_response(requestMethod, requestUrl, response) + return response + + def _pad_body_and_length(requestBody, requestHeaders): + """Pad the body and adjust content-length if needed. + When the length of the body is multiple of 1024 this function appends + one space to the body and increases by one the content-length. + + This is a workaround for hyper issue 355 [0]. + The issue has been fixed but it has not been released yet. + + [0]: https://github.com/Lukasa/hyper/issues/355 + + EXAMPLE + >>> body, headers = ' '*1024, { 'content-length':'41' } + >>> body = RestClient._pad_body_and_length(body, headers) + >>> ( len(body), headers['content-length'] ) + (1025, '42') + """ + if requestBody and 0 == (len(requestBody) % 1024): + logging.warning( "RestClient.request:" + + " padding body because" + + " its length ({})".format(len(requestBody)) + + " is multiple of 1024") + requestBody += " " + content_length = CONTENT_LENGTH + if requestHeaders and content_length in requestHeaders: + length = int(requestHeaders[content_length]) + requestHeaders[content_length] = str(length+1) + return requestBody + + def get(self, requestUrl): + return self.request('GET', requestUrl) + + def post(self, requestUrl, requestBody = None, requestHeaders={'content-type': 'application/json'}): + return self.request('POST', requestUrl, requestBody, requestHeaders) + + def postDict(self, requestUrl, requestBody = None, requestHeaders={'content-type': 'application/json'}): + """ + Accepts request body as python dictionary + """ + requestBodyJson = None + if requestBody: requestBodyJson = json.dumps(requestBody, indent=None, separators=(',', ':')) + return self.request('POST', requestUrl, requestBodyJson, requestHeaders) + + + #def delete(self, requestUrl): + # return self.request('DELETE', requestUrl) + + def __assert_received_expected(self, received, expected, what): + match = (received == expected) + log = "Received {what}: {received} | Expected {what}: {expected}".format(received=received, expected=expected, what=what) + if match: MyLogger.info(log) + else: MyLogger.error(log) + + assert match + + def check_response_status(self, received, expected, **kwargs): + """ + received: status code received (got from response data parsed, field 'status') + expected: status code expected + """ + self.__assert_received_expected(received, expected, "status code") + + #def check_expected_cause(self, response, **kwargs): + # """ + # received: response data parsed where field 'body'->'cause' is analyzed + # kwargs: aditional regexp to match expected cause + # """ + # if "expected_cause" in kwargs: + # received_content = response["body"] + # received_cause = received_content.get("cause", "") + # regular_expr_cause = kwargs["expected_cause"] + # regular_expr_flag = kwargs.get("regular_expression_flag", 0) + # matchObj = re.match(regular_expr_cause, received_cause, regular_expr_flag) + # log = 'Test error cause: "{}"~=/{}/.'.format(received_cause, regular_expr_cause) + # if matchObj: MyLogger.info(log) + # else: MyLogger.error(log) + # + # assert matchObj is not None + + def check_response_body(self, received, expected, inputJsonString = False): + """ + received: body content received (got from response data parsed, field 'body') + expected: body content expected + inputJsonString: input parameters as json string (default are python dictionaries) + """ + if inputJsonString: + # Decode json: + received = json.loads(received) + expected = json.loads(expected) + + self.__assert_received_expected(received, expected, "body") + + def check_response_headers(self, received, expected): + """ + received: headers received (got from response data parsed, field 'headers') + expected: headers expected + """ + self.__assert_received_expected(received, expected, "headers") + + def assert_response__status_body_headers(self, response, status, bodyDict, headersDict = None): + """ + response: Response parsed data + status: numeric status code + body: body dictionary to match with + headers: headers dictionary to match with (by default, not checked: internally length and content-type application/json is verified) + """ + self.check_response_status(response["status"], status) + self.check_response_body(response["body"], bodyDict) + if headersDict: self.check_response_headers(response["headers"], headersDict) + + + def close(self): + self._connection.close() + + +# ADML Client simple fixture +@pytest.fixture(scope='session') +def admlc(): + admlc = RestClient(ADML_ENDPOINT) + yield admlc + admlc.close() + print("ADMLC Teardown") + +@pytest.fixture(scope='session') +def resources(): + resourcesDict={} + MyLogger.info("Gathering test suite resources ...") + for resource in glob.glob('resources/*'): + f = open(resource, "r") + name = os.path.basename(resource) + resourcesDict[name] = f.read() + f.close() + + def get_resources(key, **kwargs): + # Be careful with templates containing curly braces: + # https://stackoverflow.com/questions/5466451/how-can-i-print-literal-curly-brace-characters-in-python-string-and-also-use-fo + resource = resourcesDict[key] + + if kwargs: + args = defaultdict (str, kwargs) + resource = resource.format_map(args) + + return resource + + yield get_resources + +################ +# Experimental # +################ + +REQUEST_BODY_DIAMETER_HEX = ''' +{{ + "diameterHex":"{diameterHex}" +}}''' + +REQUEST_BODY_NODE = { + "name":"{name}" +} + + +PARAMS = [ + (ADML_URI_PREFIX, '/decode', REQUEST_BODY_DIAMETER_HEX, ADML_ENDPOINT), +] + +# Share RestClient connection for all the tests: session-scoped fixture +@pytest.fixture(scope="session", params=PARAMS) +def request_data(request): + admlc = RestClient(request.param[3]) + def get_request_data(**kwargs): + args = defaultdict (str, kwargs) + uri_prefix = request.param[0] + request_uri_suffix=request.param[1] + formatted_uri=uri_prefix + request_uri_suffix.format_map(args) + request_body=request.param[2] + formatted_request_body=request_body.format_map(args) + return formatted_uri,formatted_request_body,admlc + + yield get_request_data + admlc.close() + print("RestClient Teardown") + +# Fixture usage example: requestUrl,requestBody,admlc = request_data(diameterHex="") + diff --git a/example/diameter/launcher/resources/rest_api/ct/hot-changes/change-dir_test.py b/example/diameter/launcher/resources/rest_api/ct/hot-changes/change-dir_test.py new file mode 100644 index 0000000..76ccee4 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/hot-changes/change-dir_test.py @@ -0,0 +1,36 @@ +import pytest + + +def test_001_change_working_directory_for_adml(admlc): + + requestBody = { "directory":"/tmp" } + responseBodyRef = { "success":"true", "response":"New execution directory configured: /tmp" } + + # Send POST + response = admlc.postDict("/change-dir", requestBody) + + # Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) + +def test_002_change_to_invalid_working_directory_for_adml(admlc): + + requestBody = { "directory":"/this/is/invalid" } + responseBodyRef = { "success":"false", "response":"Cannot assign provided execution directory: /this/is/invalid" } + + # Send POST + response = admlc.postDict("/change-dir", requestBody) + + # Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) + +def test_003_restore_working_directory_for_adml(admlc): + + requestBody = { "directory":"" } + responseBodyRef = { "success":"true", "response":"New execution directory configured: /opt/adml" } + + # Send POST + response = admlc.postDict("/change-dir", requestBody) + + # Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) + diff --git a/example/diameter/launcher/resources/rest_api/ct/hot-changes/diameterServerSessions_test.py b/example/diameter/launcher/resources/rest_api/ct/hot-changes/diameterServerSessions_test.py new file mode 100644 index 0000000..3ad5130 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/hot-changes/diameterServerSessions_test.py @@ -0,0 +1,25 @@ +import pytest + + +def test_001_configure_diameter_server_as_operated_node_at_adml(admlc): + + requestBody = { "name":"ownHostId.operatorRealm.com" } + responseBodyRef = { "success":"true", "response":"Forced node is now 'ownHostId.operatorRealm.com'" } + + # Send POST + response = admlc.postDict("/node", requestBody) + + # Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) + +def test_002_diameter_server_sessions_configuration_at_adml(admlc): + + requestBody = { "sessions":10 } + responseBodyRef = { "success":"true", "response":"new sessions successfully established on operated diameter server" } + + # Send POST + response = admlc.postDict("/diameterServerSessions", requestBody) + + # Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) + diff --git a/example/diameter/launcher/resources/rest_api/ct/hot-changes/services_test.py b/example/diameter/launcher/resources/rest_api/ct/hot-changes/services_test.py new file mode 100644 index 0000000..18e3bfc --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/hot-changes/services_test.py @@ -0,0 +1,14 @@ +import pytest + +@pytest.mark.run(order=1) +@pytest.mark.xfail(reason="This will fail if already provisioned (if you want success here, restart the ADML HTTP Service)") +def test_001_given_services_json_representation_i_want_to_load_it_through_adml_service(resources, admlc): + + requestBody = resources("servicesJson-request.json") + responseBodyRef = { "success":"true", "response":"loaded services correctly" } + + # Send POST + response = admlc.post("/services", requestBody) + + ### Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) diff --git a/example/diameter/launcher/resources/rest_api/ct/node-management/node-auto_test.py b/example/diameter/launcher/resources/rest_api/ct/node-management/node-auto_test.py new file mode 100644 index 0000000..1eb54a5 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/node-management/node-auto_test.py @@ -0,0 +1,11 @@ +import pytest + +def test_001_i_want_to_set_automatic_node_mode_at_adml(admlc): + + responseBodyRef = { "success":"true", "response":"Working node has been set to automatic" } + + # Send POST + response = admlc.post("/node-auto") + + # Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) diff --git a/example/diameter/launcher/resources/rest_api/ct/node-management/node_test.py b/example/diameter/launcher/resources/rest_api/ct/node-management/node_test.py new file mode 100644 index 0000000..b5321c0 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/node-management/node_test.py @@ -0,0 +1,35 @@ +import pytest + + +def test_001_given_node_name_i_want_to_force_it_to_be_set_as_operated_node_at_adml(admlc): + + requestBody = { "name":"afHost.afRealm.com" } + responseBodyRef = { "success":"true", "response":"Forced node is now 'afHost.afRealm.com'" } + + # Send POST + response = admlc.postDict("/node", requestBody) + + # Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) + +def test_002_i_want_to_know_current_operated_node_at_adml(admlc): + + requestBody = { "name":"" } + + # Send POST + response = admlc.postDict("/node", requestBody) + + # Verify response is ok (omit response content because it is informative and have dynamic data (timing)): + assert response["status"] == 200 + assert response["body"]["success"] == "true" + +def test_001_given_an_invalid_node_name_i_want_to_confirm_the_error_when_trying_to_enable_it_at_adml(admlc): + + requestBody = { "name":"this_does_not_exists" } + responseBodyRef = { "success":"true", "response":"Node 'this_does_not_exists' invalid. Nothing done" } + + # Send POST + response = admlc.postDict("/node", requestBody) + + # Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) diff --git a/example/diameter/launcher/resources/rest_api/ct/parsing-operations/code_test.py b/example/diameter/launcher/resources/rest_api/ct/parsing-operations/code_test.py new file mode 100644 index 0000000..226cdae --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/parsing-operations/code_test.py @@ -0,0 +1,15 @@ +import pytest + +def test_001_given_diameter_json_representation_i_want_to_encode_it_through_adml_service(resources, admlc): + + requestBody = resources("diameterJson-request.json") + + # reponse field in response body, should carry the diameter encoding for json message sent to ADML: + diameterHex = resources("aar.hex") + responseBodyRef = { "success":"true", "response":"{}".format(diameterHex.rstrip()) } + + # Send POST + response = admlc.post("/code", requestBody) + + ### Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) diff --git a/example/diameter/launcher/resources/rest_api/ct/parsing-operations/decode_test.py b/example/diameter/launcher/resources/rest_api/ct/parsing-operations/decode_test.py new file mode 100644 index 0000000..d24e51d --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/parsing-operations/decode_test.py @@ -0,0 +1,18 @@ +import pytest + +def test_001_given_diameter_hex_representation_i_want_to_decode_it_through_adml_service(b64_encode, resources, admlc): + + diameterHex = resources("aaa.hex") + #requestBody = resources("diameterHex.json.in", diameterHex=diameterHex.rstrip()) + requestBodyDict = { "diameterHex":"{}".format(diameterHex.rstrip()) } + + # reponse field in response body, should carry the base64 encoding for xml message decoded by ADML: + xmlExpected = resources("aaa.xml") + responseBodyRef = { "success":"true", "response":"{}".format(b64_encode(xmlExpected)) } + + # Send POST + #response = admlc.post("/decode", requestBody) + response = admlc.postDict("/decode", requestBodyDict) + + # Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) diff --git a/example/diameter/launcher/resources/rest_api/ct/parsing-operations/loadmsg_test.py b/example/diameter/launcher/resources/rest_api/ct/parsing-operations/loadmsg_test.py new file mode 100644 index 0000000..299f299 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/parsing-operations/loadmsg_test.py @@ -0,0 +1,16 @@ +import pytest + + +def test_001_given_diameter_json_representation_i_want_to_load_it_through_adml_service(b64_encode, resources, admlc): + + requestBody = resources("diameterJson-request.json") + + # reponse field in response body, should carry the base64 encoding for xml message loaded by ADML: + xmlExpected = resources("aar.xml") + responseBodyRef = { "success":"true", "response":"{}".format(b64_encode(xmlExpected)) } + + # Send POST + response = admlc.post("/loadmsg", requestBody) + + ### Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) diff --git a/example/diameter/launcher/resources/rest_api/ct/pytest.ini b/example/diameter/launcher/resources/rest_api/ct/pytest.ini new file mode 100644 index 0000000..e2fc311 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/pytest.ini @@ -0,0 +1,14 @@ +[pytest] +addopts = -v --junitxml=/tmp/junit.xml -n 0 + +log_format=%(asctime)s|%(name)s|%(filename)s:%(lineno)d|%(levelname)s|%(message)s +log_date_format = %Y-%m-%d %H:%M:%S +log_level=INFO +# --log-level=DEBUG in addopts (warning by default) + +# JUNIT +# --capture tee-sys: captures to terminal but keeping them in junit +# -s: capture=no (shortcut for debugging, but junit will miss the info) +junit_suite_name = "ADML REST API" +junit_logging = out-err +junit_family = xunit1 diff --git a/example/diameter/launcher/resources/rest_api/ct/requirements.txt b/example/diameter/launcher/resources/rest_api/ct/requirements.txt new file mode 100644 index 0000000..41622eb --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/requirements.txt @@ -0,0 +1,2 @@ +pytest-ordering +pytest-xdist diff --git a/example/diameter/launcher/resources/rest_api/ct/resources/aaa.hex b/example/diameter/launcher/resources/rest_api/ct/resources/aaa.hex new file mode 100644 index 0000000..08931a3 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/resources/aaa.hex @@ -0,0 +1 @@ +010000f80000010901000014000c73c30004cee4000001024000000c01000014000001074000004274635f30315f46756c6c415650733b61664e6f6465486f73746e616d652e61664e6f6465486f73745265616c6d2e636f6d3b313b38353936303800000000010840000027736170634f776e486f737449642e6f70657261746f725265616c6d2e636f6d000000010c4000000c000007d1000001164000000c551192a700000128400000196f70657261746f725265616c6d2e636f6d0000000000027480000038000028af0000010a4000000c000028af0000027580000010000028af000000010000027680000010000028af00000013 diff --git a/example/diameter/launcher/resources/rest_api/ct/resources/aaa.xml b/example/diameter/launcher/resources/rest_api/ct/resources/aaa.xml new file mode 100644 index 0000000..8338a6a --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/resources/aaa.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/example/diameter/launcher/resources/rest_api/ct/resources/aar.hex b/example/diameter/launcher/resources/rest_api/ct/resources/aar.hex new file mode 100644 index 0000000..9a2aeb9 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/resources/aar.hex @@ -0,0 +1 @@ +01000150c000010901000014000000000000000000000107400000246f6373333b313333323737343433303b313b31333332373734343330000001024000000c01000014000001084000000c4f435333000001284000000c4f4353330000011b4000000c4f435333000001254000000c4f435333000001f8c000000f000028af3132320000000205c000006c000028af00000206c0000010000028af00000000000001f8c000000f000028af3132320000000204c0000010000028af0000007f00000203c0000010000028af00000085000001ffc0000010000028af00000002000001ca80000010000032db000000000000020fc0000010000028af00000000000001bb40000028000001c24000000c00000000000001bc4000001136323630333730393900000000000008400000133139322e3136382e302e31000000001e400000145741502e4d4f564953544152 diff --git a/example/diameter/launcher/resources/rest_api/ct/resources/aar.xml b/example/diameter/launcher/resources/rest_api/ct/resources/aar.xml new file mode 100644 index 0000000..9acf9d5 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/resources/aar.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/diameter/launcher/resources/rest_api/ct/resources/diameterHex.json.in b/example/diameter/launcher/resources/rest_api/ct/resources/diameterHex.json.in new file mode 100644 index 0000000..138a771 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/resources/diameterHex.json.in @@ -0,0 +1,3 @@ +{{ + "diameterHex":"{diameterHex}" +}} diff --git a/example/diameter/launcher/resources/rest_api/ct/resources/diameterJson-request.json b/example/diameter/launcher/resources/rest_api/ct/resources/diameterJson-request.json new file mode 100644 index 0000000..2dbc65d --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/resources/diameterJson-request.json @@ -0,0 +1,100 @@ +{ + "diameterJson": { + "message": { + "@application-id": "16777236", + "@end-to-end-id": "0", + "@hop-by-hop-id": "0", + "@name": "AA-Request", + "@p-bit": "yes", + "@version": "1", + "avp": [ + { + "@data": "ocs3;1332774430;1;1332774430", + "@name": "Session-Id" + }, + { + "@data": "16777236", + "@name": "Auth-Application-Id" + }, + { + "@data": "OCS3", + "@name": "Origin-Host" + }, + { + "@data": "OCS3", + "@name": "Origin-Realm" + }, + { + "@data": "OCS3", + "@name": "Destination-Realm" + }, + { + "@data": "OCS3", + "@name": "Destination-Host" + }, + { + "@hex-data": "313232", + "@name": "AF-Application-Identifier" + }, + { + "@name": "Media-Component-Description", + "avp": [ + { + "@data": "0", + "@name": "Media-Component-Number" + }, + { + "@hex-data": "313232", + "@name": "AF-Application-Identifier" + }, + { + "@data": "127", + "@name": "Max-Requested-Bandwidth-UL" + }, + { + "@data": "133", + "@name": "Max-Requested-Bandwidth-DL" + }, + { + "@alias": "ENABLED", + "@data": "2", + "@name": "Flow-Status" + }, + { + "@alias": "DEFAULT", + "@data": "0", + "@name": "Reservation-Priority" + } + ] + }, + { + "@alias": "FINAL_SERVICE_INFORMATION", + "@data": "0", + "@name": "Service-Info-Status" + }, + { + "@name": "Subscription-Id", + "avp": [ + { + "@alias": "END_USER_E164", + "@data": "0", + "@name": "Subscription-Id-Type" + }, + { + "@data": "626037099", + "@name": "Subscription-Id-Data" + } + ] + }, + { + "@hex-data": "3139322e3136382e302e31", + "@name": "Framed-IP-Address" + }, + { + "@hex-data": "5741502e4d4f564953544152", + "@name": "Called-Station-Id" + } + ] + } + } +} diff --git a/example/diameter/launcher/resources/rest_api/ct/resources/servicesJson-request.json b/example/diameter/launcher/resources/rest_api/ct/resources/servicesJson-request.json new file mode 100644 index 0000000..0aa21cd --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/resources/servicesJson-request.json @@ -0,0 +1,25 @@ +{ + "servicesJson": { + "services": { + "node": [ + { + "@applicationId": "16777236", + "@entity": "localhost:3868", + "@originHost": "afHost.afRealm.com" + }, + { + "@applicationId": "16777236", + "@diameterServer": "localhost:3868", + "@diameterServerSessions": "1", + "@originHost": "ownHostId.operatorRealm.com" + } + ], + "stack": { + "@dictionary": "stacks/DictionaryRx.16777236.xml", + "@fixMode": "Always", + "@id": "16777236", + "@ignoreFlagsOnValidation": "yes" + } + } + } +} diff --git a/example/diameter/launcher/resources/rest_api/ct/snapshots/collect_test.py b/example/diameter/launcher/resources/rest_api/ct/snapshots/collect_test.py new file mode 100644 index 0000000..1855b33 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/snapshots/collect_test.py @@ -0,0 +1,12 @@ +import pytest + + +def test_001_collect_oam_and_statistics_at_adml(admlc): + + responseBodyRef = { "success":"true", "response":"All process counters & statistic information have been reset" } + + # Send POST + response = admlc.post("/collect") + + # Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) diff --git a/example/diameter/launcher/resources/rest_api/ct/snapshots/context_test.py b/example/diameter/launcher/resources/rest_api/ct/snapshots/context_test.py new file mode 100644 index 0000000..1aa8b17 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/snapshots/context_test.py @@ -0,0 +1,34 @@ +import pytest +import re + + +def test_001_given_a_target_file_i_want_to_dump_the_adml_context(admlc): + + requestBody = { "targetFile":"/tmp/context.xml" } + responseBodyRef = { "success":"true", "response":"Context dumped on file '/tmp/context.xml'" } + + # Send POST + response = admlc.postDict("/context", requestBody) + + # Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) + +def test_002_missing_a_target_file_i_want_to_dump_the_adml_context_at_default_location(admlc, mylogger): + + requestBody = { "targetFile":"" } + responseBodyRef = { "success":"true", "response":"Context dumped on file '/tmp/context.xml'" } + + # Send POST + response = admlc.postDict("/context", requestBody) + + # Verify response is ok (omit response content because it is informative and have dynamic data (timing)): + assert response["status"] == 200 + assert response["body"]["success"] == "true" + + body_response = response["body"]["response"] + pattern = "Context dumped on file '/var/tmp/anna.context.[0-9]*'" + result = re.match(pattern, body_response) + if not result: + mylogger.error("Body response \"" + body_response + "\" does not match the pattern \"" + pattern + "\"") + + assert result diff --git a/example/diameter/launcher/resources/rest_api/ct/snapshots/forceCountersRecord_test.py b/example/diameter/launcher/resources/rest_api/ct/snapshots/forceCountersRecord_test.py new file mode 100644 index 0000000..e286092 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/snapshots/forceCountersRecord_test.py @@ -0,0 +1,12 @@ +import pytest + + +def test_001_i_want_to_force_counters_record_at_adml(admlc): + + responseBodyRef = { "success":"true", "response":"Current counters have been dump to disk" } + + # Send POST + response = admlc.post("/forceCountersRecord") + + # Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) diff --git a/example/diameter/launcher/resources/rest_api/ct/snapshots/log-statistics-samples_test.py b/example/diameter/launcher/resources/rest_api/ct/snapshots/log-statistics-samples_test.py new file mode 100644 index 0000000..82545f5 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/snapshots/log-statistics-samples_test.py @@ -0,0 +1,35 @@ +import pytest + + +def test_001_i_want_to_log_statistics_samples_for_concept_ids_1_and_2_at_adml(admlc): + + requestBody = { "list":"1,2" } + responseBodyRef = { "success":"true", "response":"Log statistics samples for '1,2' concepts" } + + # Send POST + response = admlc.postDict("/log-statistics-samples", requestBody) + + # Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) + +def test_002_i_want_to_disable_log_statistics_samples_at_adml(admlc): + + requestBody = { "list":"none" } + responseBodyRef = { "success":"true", "response":"Log statistics samples for 'none' concepts" } + + # Send POST + response = admlc.postDict("/log-statistics-samples", requestBody) + + # Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) + +def test_003_i_want_to_enable_log_statistics_samples_for_every_concept_id_registered_at_adml(admlc): + + requestBody = { "list":"all" } + responseBodyRef = { "success":"true", "response":"Log statistics samples for 'all' concepts" } + + # Send POST + response = admlc.postDict("/log-statistics-samples", requestBody) + + # Verify response + admlc.assert_response__status_body_headers(response, 200, responseBodyRef) diff --git a/example/diameter/launcher/resources/rest_api/ct/snapshots/show-oam_test.py b/example/diameter/launcher/resources/rest_api/ct/snapshots/show-oam_test.py new file mode 100644 index 0000000..1256c1c --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/snapshots/show-oam_test.py @@ -0,0 +1,11 @@ +import pytest + + +def test_001_show_oam_from_adml(admlc): + + # Send GET + response = admlc.get("/show-oam") + + # Verify response is ok (omit response content because it is informative and have dynamic data (timing)): + assert response["status"] == 200 + assert response["body"]["success"] == "true" diff --git a/example/diameter/launcher/resources/rest_api/ct/snapshots/show-stats_test.py b/example/diameter/launcher/resources/rest_api/ct/snapshots/show-stats_test.py new file mode 100644 index 0000000..789e826 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/ct/snapshots/show-stats_test.py @@ -0,0 +1,11 @@ +import pytest + + +def test_001_show_statistics_from_adml(admlc): + + # Send GET + response = admlc.get("/show-stats") + + # Verify response is ok (omit response content because it is informative and have dynamic data (timing)): + assert response["status"] == 200 + assert response["body"]["success"] == "true" diff --git a/example/diameter/launcher/resources/rest_api/helpers/README b/example/diameter/launcher/resources/rest_api/helpers/README new file mode 100644 index 0000000..133b2c0 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/helpers/README @@ -0,0 +1,14 @@ +1) Disect a pcap by mean the 'tsharkDecoder.sh' utility. For example: + + ./tsharkDecoder.sh example.pcap + +2) Check the files created (metadata & hex) for every pcap frame. + The 'origin-hosts' file content all the Origin-Host names found in the pcap capture. + +3) Decode an hex file by mean the 'decodeHex.sh' script. For example: + + ./decodeHex.sh 8.hex + + This will create the corresponding diameter message representation in two formats: + 3.1) xml format + 3.2) json format (through diameterJsonHelper resources) diff --git a/example/diameter/launcher/resources/rest_api/helpers/decodeHex.sh b/example/diameter/launcher/resources/rest_api/helpers/decodeHex.sh new file mode 100755 index 0000000..ea8d9fa --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/helpers/decodeHex.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +[ -z "$1" ] && { echo "Usage: " ; exit 1 ; } +[ ! -f "$1" ] && { echo "Cannot find '$1' !" ; exit 1 ; } + +ENDPOINT=$(cat ../.httpServer 2>/dev/null) +[ -z "${ENDPOINT}" ] && ENDPOINT=localhost:8000 + +cd $(dirname $0) +tmpfile=$(mktemp) +cat << EOF > $tmpfile +{ + "diameterHex":"$(cat $1)" +} +EOF +responseField=$(curl -X POST -H 'Accept: application/json' -H 'Content-Type: application/json' -d@${tmpfile} http://${ENDPOINT}/decode 2>/dev/null | jq '.response') +rm ${tmpfile} +[ -z "${responseField}" ] && { echo "Unexpected error. Check that ADML is started and the corresponding diameter dictionary is loaded." ; exit 1 ; } + +# Remove surrounding quotes: +base64=$(echo ${responseField} | tr -d '"') + +# Decode response (base64), which is xml: +echo ${base64} | base64 -d > ${1}.xml +echo "Created '${1}.xml'" + +# Also create the json version: +diameterJsonHelper/xml2json.py ${1}.xml > ${1}.xml.json +echo "Created '${1}.xml.json'" diff --git a/example/diameter/launcher/resources/rest_api/helpers/diameterJsonHelper/examples/aar.json b/example/diameter/launcher/resources/rest_api/helpers/diameterJsonHelper/examples/aar.json new file mode 100644 index 0000000..47472f8 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/helpers/diameterJsonHelper/examples/aar.json @@ -0,0 +1,98 @@ +{ + "message": { + "@application-id": "16777236", + "@end-to-end-id": "0", + "@hop-by-hop-id": "0", + "@name": "AA-Request", + "@p-bit": "yes", + "@version": "1", + "avp": [ + { + "@data": "test1;afNodeHostname.nodeHostRealm.com;1;8033450", + "@name": "Session-Id" + }, + { + "@data": "16777236", + "@name": "Auth-Application-Id" + }, + { + "@data": "afHost.afRealm.com", + "@name": "Origin-Host" + }, + { + "@data": "afRealm.com", + "@name": "Origin-Realm" + }, + { + "@data": "operatorRealm.com", + "@name": "Destination-Realm" + }, + { + "@data": "ownHostId.operatorRealm.com", + "@name": "Destination-Host" + }, + { + "@hex-data": "313232", + "@name": "AF-Application-Identifier" + }, + { + "@name": "Media-Component-Description", + "avp": [ + { + "@data": "0", + "@name": "Media-Component-Number" + }, + { + "@hex-data": "313232", + "@name": "AF-Application-Identifier" + }, + { + "@data": "127", + "@name": "Max-Requested-Bandwidth-UL" + }, + { + "@data": "133", + "@name": "Max-Requested-Bandwidth-DL" + }, + { + "@alias": "ENABLED", + "@data": "2", + "@name": "Flow-Status" + }, + { + "@alias": "DEFAULT", + "@data": "0", + "@name": "Reservation-Priority" + } + ] + }, + { + "@alias": "FINAL_SERVICE_INFORMATION", + "@data": "0", + "@name": "Service-Info-Status" + }, + { + "@name": "Subscription-Id", + "avp": [ + { + "@alias": "END_USER_E164", + "@data": "0", + "@name": "Subscription-Id-Type" + }, + { + "@data": "626037099", + "@name": "Subscription-Id-Data" + } + ] + }, + { + "@hex-data": "3139322e3136382e302e31", + "@name": "Framed-IP-Address" + }, + { + "@hex-data": "5741502e4d4f564953544152", + "@name": "Called-Station-Id" + } + ] + } +} diff --git a/example/diameter/launcher/resources/rest_api/helpers/diameterJsonHelper/examples/aar.xml b/example/diameter/launcher/resources/rest_api/helpers/diameterJsonHelper/examples/aar.xml new file mode 100644 index 0000000..9ac1f67 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/helpers/diameterJsonHelper/examples/aar.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/diameter/launcher/resources/rest_api/helpers/diameterJsonHelper/examples/services.json b/example/diameter/launcher/resources/rest_api/helpers/diameterJsonHelper/examples/services.json new file mode 100644 index 0000000..3d8488d --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/helpers/diameterJsonHelper/examples/services.json @@ -0,0 +1,23 @@ +{ + "services": { + "node": [ + { + "@applicationId": "0", + "@entity": "localhost:3868", + "@originHost": "afHost.afRealm.com" + }, + { + "@applicationId": "0", + "@diameterServer": "localhost:3868", + "@diameterServerSessions": "1", + "@originHost": "ownHostId.operatorRealm.com" + } + ], + "stack": { + "@dictionary": "dictionary.xml", + "@fixMode": "Always", + "@id": "0", + "@ignoreFlagsOnValidation": "yes" + } + } +} diff --git a/example/diameter/launcher/resources/rest_api/helpers/diameterJsonHelper/examples/services.xml b/example/diameter/launcher/resources/rest_api/helpers/diameterJsonHelper/examples/services.xml new file mode 100644 index 0000000..daca002 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/helpers/diameterJsonHelper/examples/services.xml @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/example/diameter/launcher/resources/rest_api/helpers/diameterJsonHelper/xml2json.py b/example/diameter/launcher/resources/rest_api/helpers/diameterJsonHelper/xml2json.py new file mode 100755 index 0000000..00c6326 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/helpers/diameterJsonHelper/xml2json.py @@ -0,0 +1,25 @@ +#!/usr/bin/python3 +import xmltodict +import json +import sys + +try: + file = sys.argv[1] +except: + print("Usage: " + sys.argv[0] + " ") + sys.exit(1) + +try: + with open(file, 'r') as myfile: + xml = myfile.read() +except: + print("ERROR reading '" + file + "'") + sys.exit(1) + +# although default attribute prefix is '@', anyway we will +# force the value just in case xmltodict implementation +# changes. The anna::core::functions::json2xml helper, +# assumes this prefix in order to work properly. +my_dict=xmltodict.parse(xml, attr_prefix='@') +json_data=json.dumps(my_dict, indent=3, sort_keys=True) +print(json_data) diff --git a/example/diameter/launcher/resources/rest_api/helpers/example.pcap b/example/diameter/launcher/resources/rest_api/helpers/example.pcap new file mode 120000 index 0000000..db6ca40 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/helpers/example.pcap @@ -0,0 +1 @@ +../../../../pcapDecoder/example.pcap \ No newline at end of file diff --git a/example/diameter/launcher/resources/rest_api/helpers/tsharkDecoder.sh b/example/diameter/launcher/resources/rest_api/helpers/tsharkDecoder.sh new file mode 120000 index 0000000..d5d76c2 --- /dev/null +++ b/example/diameter/launcher/resources/rest_api/helpers/tsharkDecoder.sh @@ -0,0 +1 @@ +../../../../pcapDecoder/tsharkDecoder.sh \ No newline at end of file diff --git a/example/diameter/launcher/resources/scripts/operation.sh b/example/diameter/launcher/resources/scripts/operation.sh new file mode 100755 index 0000000..87e4ce5 --- /dev/null +++ b/example/diameter/launcher/resources/scripts/operation.sh @@ -0,0 +1,141 @@ +#!/bin/bash + +############# +# VARIABLES # +############# +TIMEOUT__dflt=3 +SCR_BN=`basename $0` + +############# +# FUNCTIONS # +############# +_exit() { + echo -e "\n$1\n" + exit 1 +} + +usage() { + echo + echo "Usage: $0 [-h|--help] [-t|--timeout ] [-f|--file ] [-p|--ping] [operation] " + echo + echo " -h|--help: this usage help." + echo " -t|--timeout : timeout for operation in seconds." + echo " Defaults to $TIMEOUT__dflt seconds if not provided." + echo " -f|--file : file with one operation per line (comments # allowed)." + echo " -p|--ping: Check the target process id." + echo " Returns 1 (dead) or 0 (alive)." + echo " operation: quoted operation string. Will be ignored if file" + echo " option is present. It is a positional argument" + echo " (the last one) when present." + echo + echo " For example:" + echo " $0 help" + echo " $0 \"test|ttps|50\"" + echo " $0 --file myOperationsList.txt" + echo " $0 --timeout 10 --file ./bigList.txt" + _exit +} + +parse_arguments() { + is_file= + timeout=$TIMEOUT__dflt + file= + operation= + ping= + + while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + usage + ;; + + -t|--timeout) + timeout=$2 + [ -z "$timeout" ] && _exit "Missing timeout value" + shift + ;; + + -f|--file) + is_file=yes + file="$2" + [ -z "$file" ] && _exit "Missing file" + [ ! -f "$file" ] && _exit "Can't found provided file '$file'." + shift + ;; + + -p|--ping) + ping=yes + ;; + + *) + first=$(echo $1 | cut -c1) + [ "$first" = "-" ] && _exit "Unsupported script option: $1. Type '$SCR_BN -h' (or --help) to print the available options." + operation="$@" + break + ;; + esac + shift + done + + [ -z "$is_file" -a -z "$operation" -a -z "$ping" ] && _exit "Missing operation or file with operations" +} + +# $1: pid to check +check_pid() { + kill -0 $1 2>/dev/null + return $? +} + +############# +# EXECUTION # +############# +cd `dirname $0` +# Get the PID: +[ ! -f .pid ] && _exit "Can't found '`pwd`/.pid'.\nTry to pgrep your process name and dump pid to that file." +PID=`cat .pid` + +# Arguments: +[ "$1" = "" -o "$1" = "--help" -o "$1" = "-h" ] && usage +parse_arguments $@ + +# Check pid: +check_pid $PID +res=$? +[ -n "$ping" ] && exit $res +[ $res -ne 0 ] && _exit "Operation error: missing process with pid $PID" + +# Send operation: +if [ -n "$is_file" ] +then + cp $file sigusr2.in +else + echo $operation > sigusr2.in +fi +0>sigusr2.out +check_pid $PID +kill -s SIGUSR2 $PID + +# Detect EOF and print all except that last line: +count=$((10*timeout)) +expired=yes +while [ $count -gt 0 ] +do + sleep 0.1 + count=$((count-1)) + if tail -1 sigusr2.out | grep "^EOF" >/dev/null; then + expired= + break; + fi +done + +if [ -z "$expired" ] +then + head --lines=-1 sigusr2.out +else + _exit "Operation error: timeout expired ($timeout seconds)" +fi + +exception=$(grep "^Operation processed with exception: " sigusr2.out) +[ $? -eq 0 ] && _exit "(detected 'exception' within operation output: see 'launcher.trace')" +exit 0 + diff --git a/example/diameter/launcher/resources/scripts/operation_curl.sh b/example/diameter/launcher/resources/scripts/operation_curl.sh deleted file mode 100755 index 18211fe..0000000 --- a/example/diameter/launcher/resources/scripts/operation_curl.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -############# -# FUNCTIONS # -############# -_exit () { - echo -e "\n$1\n" - exit 1 -} - -############# -# EXECUTION # -############# -cd `dirname $0` -echo -# Get the HTTP Server: -[ ! -f .httpServer ] && _exit "Can't found '`pwd`/.httpServer' to use with curl tool." -SERVER=`cat .httpServer` - -# Send operation: -[ "$1" = "" ] && _exit "Usage: $0 ; i.e.: $0 node" -#> curl_log.txt -#TRACE="--trace-ascii curl_log.txt" -curl -m 1 --data "$1" $SERVER - diff --git a/example/diameter/launcher/resources/scripts/operation_signal.sh b/example/diameter/launcher/resources/scripts/operation_signal.sh deleted file mode 100755 index 87e4ce5..0000000 --- a/example/diameter/launcher/resources/scripts/operation_signal.sh +++ /dev/null @@ -1,141 +0,0 @@ -#!/bin/bash - -############# -# VARIABLES # -############# -TIMEOUT__dflt=3 -SCR_BN=`basename $0` - -############# -# FUNCTIONS # -############# -_exit() { - echo -e "\n$1\n" - exit 1 -} - -usage() { - echo - echo "Usage: $0 [-h|--help] [-t|--timeout ] [-f|--file ] [-p|--ping] [operation] " - echo - echo " -h|--help: this usage help." - echo " -t|--timeout : timeout for operation in seconds." - echo " Defaults to $TIMEOUT__dflt seconds if not provided." - echo " -f|--file : file with one operation per line (comments # allowed)." - echo " -p|--ping: Check the target process id." - echo " Returns 1 (dead) or 0 (alive)." - echo " operation: quoted operation string. Will be ignored if file" - echo " option is present. It is a positional argument" - echo " (the last one) when present." - echo - echo " For example:" - echo " $0 help" - echo " $0 \"test|ttps|50\"" - echo " $0 --file myOperationsList.txt" - echo " $0 --timeout 10 --file ./bigList.txt" - _exit -} - -parse_arguments() { - is_file= - timeout=$TIMEOUT__dflt - file= - operation= - ping= - - while [ $# -gt 0 ]; do - case "$1" in - -h|--help) - usage - ;; - - -t|--timeout) - timeout=$2 - [ -z "$timeout" ] && _exit "Missing timeout value" - shift - ;; - - -f|--file) - is_file=yes - file="$2" - [ -z "$file" ] && _exit "Missing file" - [ ! -f "$file" ] && _exit "Can't found provided file '$file'." - shift - ;; - - -p|--ping) - ping=yes - ;; - - *) - first=$(echo $1 | cut -c1) - [ "$first" = "-" ] && _exit "Unsupported script option: $1. Type '$SCR_BN -h' (or --help) to print the available options." - operation="$@" - break - ;; - esac - shift - done - - [ -z "$is_file" -a -z "$operation" -a -z "$ping" ] && _exit "Missing operation or file with operations" -} - -# $1: pid to check -check_pid() { - kill -0 $1 2>/dev/null - return $? -} - -############# -# EXECUTION # -############# -cd `dirname $0` -# Get the PID: -[ ! -f .pid ] && _exit "Can't found '`pwd`/.pid'.\nTry to pgrep your process name and dump pid to that file." -PID=`cat .pid` - -# Arguments: -[ "$1" = "" -o "$1" = "--help" -o "$1" = "-h" ] && usage -parse_arguments $@ - -# Check pid: -check_pid $PID -res=$? -[ -n "$ping" ] && exit $res -[ $res -ne 0 ] && _exit "Operation error: missing process with pid $PID" - -# Send operation: -if [ -n "$is_file" ] -then - cp $file sigusr2.in -else - echo $operation > sigusr2.in -fi -0>sigusr2.out -check_pid $PID -kill -s SIGUSR2 $PID - -# Detect EOF and print all except that last line: -count=$((10*timeout)) -expired=yes -while [ $count -gt 0 ] -do - sleep 0.1 - count=$((count-1)) - if tail -1 sigusr2.out | grep "^EOF" >/dev/null; then - expired= - break; - fi -done - -if [ -z "$expired" ] -then - head --lines=-1 sigusr2.out -else - _exit "Operation error: timeout expired ($timeout seconds)" -fi - -exception=$(grep "^Operation processed with exception: " sigusr2.out) -[ $? -eq 0 ] && _exit "(detected 'exception' within operation output: see 'launcher.trace')" -exit 0 - diff --git a/example/diameter/launcher/resources/scripts/operation_tps.sh b/example/diameter/launcher/resources/scripts/operation_tps.sh deleted file mode 100755 index 3c5d7ff..0000000 --- a/example/diameter/launcher/resources/scripts/operation_tps.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - echo " sendXS [amount: default -1 (no limit)]:" - echo " send messages from burst list with the TPS provided. User could hot change" - echo " speed by mean 'echo > .tps' at another shell. You can stop the load" - echo " removing that hidden file or using CTRL+C from the shell where you launched" - echo " the burst command. Real tps is dumped on '.real_tps' file during testing." - echo " You could limit the amount of messages sent by mean the second parameter." - echo " No limit is established by default (-1 or negative value)." -> curl_log.txt -TRACE="--trace-ascii curl_log.txt" -SERVER=`cat .httpServer` - -use () { - - echo "Use: $0 [2c]" - echo - echo "Sends 'xml_file' to the diameter server or to the client when '2c' parameter is provided." - echo - exit -} - - sendXS) [[ "$2" = "" ]] && uso - limit=$3 - [[ "$limit" = "" ]] && limit=-1 - entero $2 - entero $limit - TPS=0 - count=0 - amount=1 - echo $2 > .tps - while test -f .tps - do - [[ "$count" = "$limit" ]] && break - BEFORE_ns=`date +%s%N` - READ_TPS=`cat .tps` - # Hot change could make .tps still unavailable: - [[ "$READ_TPS" = "" ]] && READ_TPS=$TPS - # Volvemos a calcular medias (REAL_TPS) cada 10 segundos o cuando cambia el TPS en caliente: - [[ "$READ_TPS" != "$TPS" ]] && { BEGIN_ns=$BEFORE_ns ; count=0 ; } - [[ $count = $((10*TPS)) ]] && { BEGIN_ns=$BEFORE_ns ; count=0 ; } - TPS=$READ_TPS - [[ "$TPS" = "0" ]] && salir "Test stopped due to 0-tps value read" - # Background: - _curl "burst|send|$amount" & - count=$((count+amount)) - AFTER_ns=`date +%s%N` - # Real tps: - REAL_TPS=$(calc "1000000000 * $count / ($AFTER_ns - $BEGIN_ns)") - echo $REAL_TPS > .real_tps - - COEF=1 - [[ $(calc "$TPS > $REAL_TPS") = "1" ]] && COEF=$(calc "$REAL_TPS / $TPS") - K=$(calc "$COEF ^ 10") - amount=$(calc "scale=0;1/$K") - usleep $(calc "$K * 1000000/$TPS") - done - ;; -echo -[[ "$1" = "" ]] && use -[[ ! -f "$1" ]] && { echo "ERROR: file '$1' not found" ; echo; echo; exit ; } -echo -operation="sendxml2e|$1" -[[ "$2" = "2c" ]] && operation="sendxml2c|$1" - -curl -m 1 --data "$operation" $TRACE ${SERVER} - diff --git a/include/anna/json/SaxConsumer.hpp b/include/anna/json/SaxConsumer.hpp index a3cd194..1557575 100644 --- a/include/anna/json/SaxConsumer.hpp +++ b/include/anna/json/SaxConsumer.hpp @@ -21,106 +21,121 @@ namespace json { class SaxConsumer : public nlohmann::json::json_sax_t { + char attr_prefix_; int indent_; - bool started_, last_was_start_, last_was_array_; std::stringstream result_; std::stringstream current_object_; std::stack nodes_stack_; std::string key_; + bool has_attributes_; + + const std::string & get_top() { return nodes_stack_.top(); } + public: - SaxConsumer() : started_(false), last_was_start_(false), last_was_array_(false), indent_(-ANNA_XML_INDENTATION_SPACES) {} + SaxConsumer(char attrPrefix = '@') : attr_prefix_(attrPrefix), + has_attributes_(false), + indent_(-ANNA_XML_INDENTATION_SPACES) {}; const std::stringstream & getResult() const { return result_; } bool null() override { current_object_ << ""; - last_was_start_ = false; + has_attributes_ = true; return true; } bool boolean(bool val) override { current_object_ << std::quoted(val ? "true" : "false"); - last_was_start_ = false; + has_attributes_ = true; return true; } bool number_integer(number_integer_t val) override { current_object_ << std::quoted(std::to_string(val)); - last_was_start_ = false; + has_attributes_ = true; return true; } bool number_unsigned(number_unsigned_t val) override { current_object_ << std::quoted(std::to_string(val)); - last_was_start_ = false; + has_attributes_ = true; return true; } bool number_float(number_float_t val, const string_t& s) override { current_object_ << std::quoted(s); - last_was_start_ = false; + has_attributes_ = true; return true; } bool string(string_t& val) override { current_object_ << std::quoted(val); - last_was_start_ = false; + has_attributes_ = true; return true; } bool start_object(std::size_t elements) override { - if (!started_) { started_ = true ; return true; } - indent_ += ANNA_XML_INDENTATION_SPACES; - if (last_was_start_) result_ << ">\n"; - last_was_start_ = true; - if (!last_was_array_) nodes_stack_.push(key_); - result_ << std::string(indent_, ' ') << "<" << nodes_stack_.top(); - if (last_was_array_) nodes_stack_.push(key_); - last_was_array_ = false; + if (key_ == "") return true; // global object condition (first start object) + nodes_stack_.push(key_); // push on starts + indent_ += ANNA_XML_INDENTATION_SPACES; // increase indentation on object start + + if (indent_ != 0) { + // New object when previous hadn't attributes: + if (current_object_.str().empty() && !has_attributes_) result_ <<">"; + result_ << "\n"; + } + result_ << std::string(indent_, ' ') << "<" << get_top(); + + has_attributes_ = false; return true; } bool end_object() override { - std::string close = "/>"; - if (current_object_.str().empty()) close = ""; - result_ << current_object_.str() << close; if (indent_ < 0) return true; - if (close == "") result_ << std::string(indent_, ' ') << ""; - indent_ -= ANNA_XML_INDENTATION_SPACES; - result_ << "\n"; - current_object_.str(""); - nodes_stack_.pop(); + if (current_object_.str().empty()) { + result_ << "\n" << std::string(indent_, ' ') << ""; + } + else { + result_ << current_object_.str() << "/>"; + current_object_.str(""); + } + + nodes_stack_.pop(); // pop on ends + indent_ -= ANNA_XML_INDENTATION_SPACES; // decrease indentation on object end return true; } bool start_array(std::size_t elements) override { - nodes_stack_.push(key_); - result_ << current_object_.str() << ">\n"; + nodes_stack_.push(key_); // push on starts + + result_ << current_object_.str(); + current_object_.str(""); + has_attributes_ = false; + return true; } bool end_array() override { - nodes_stack_.pop(); - last_was_array_ = true; + nodes_stack_.pop(); // pop on ends return true; } bool key(string_t& val) override { - if (val[0] != '@') { + if (val[0] != attr_prefix_) { key_ = val; } else {