diff --git a/CMakeLists.txt b/CMakeLists.txt
index d4107e8fa..53e8b98a4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -24,7 +24,7 @@ endif()
find_package( ecbuild 3.4 REQUIRED HINTS ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/../ecbuild /workspace/ecbuild) # Before project()
-project( ecflow LANGUAGES CXX VERSION 5.13.4 )
+project( ecflow LANGUAGES CXX VERSION 5.13.5 )
# Important: the project version is used, as generated CMake variables, to filter .../ecflow/core/ecflow_version.h.in
diff --git a/Viewer/ecflowUI/src/SessionDialog.cpp b/Viewer/ecflowUI/src/SessionDialog.cpp
index ee97e606b..156ad15b1 100644
--- a/Viewer/ecflowUI/src/SessionDialog.cpp
+++ b/Viewer/ecflowUI/src/SessionDialog.cpp
@@ -30,18 +30,20 @@ SessionDialog::SessionDialog(QWidget* parent) : QDialog(parent) {
// what was saved last time?
std::string lastSessionName = SessionHandler::instance()->lastSessionName();
int index = SessionHandler::instance()->indexFromName(lastSessionName);
- if (index != -1)
+ if (index != -1) {
savedSessionsList_->setCurrentItem(savedSessionsList_->topLevelItem(index)); // select this one in the table
+ }
- if (SessionHandler::instance()->loadLastSessionAtStartup())
+ if (SessionHandler::instance()->loadLastSessionAtStartup()) {
restoreLastSessionCb_->setCheckState(Qt::Checked);
- else
+ }
+ else {
restoreLastSessionCb_->setCheckState(Qt::Unchecked);
+ }
newButton_->setVisible(false); // XXX TODO: enable New Session functionality
// ensure the correct state of the Save button
- on_sessionNameEdit__textChanged();
setButtonsEnabledStatus();
}
@@ -101,39 +103,7 @@ void SessionDialog::setButtonsEnabledStatus() {
cloneButton_->setEnabled(true);
}
}
-
-bool SessionDialog::validSaveName(const std::string& /*name*/) {
- /*
- QString boxName(QObject::tr("Save session"));
- // name empty?
- if (name.empty())
- {
- QMessageBox::critical(0,boxName, tr("Please enter a name for the session"));
- return false;
- }
-
-
- // is there already a session with this name?
- bool sessionWithThisName = (SessionHandler::instance()->find(name) != NULL);
-
- if (sessionWithThisName)
- {
- QMessageBox::critical(0,boxName, tr("A session with that name already exists - please choose another
- name")); return false;
- }
- else
- {
- return true;
- }*/
- return true;
-}
-
-void SessionDialog::on_sessionNameEdit__textChanged() {
- // only allow to save a non-empty session name
- // saveButton_->setEnabled(!sessionNameEdit_->text().isEmpty());
-}
-
-void SessionDialog::on_savedSessionsList__currentRowChanged(int /*currentRow*/) {
+void SessionDialog::on_savedSessionsList__currentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous) {
setButtonsEnabledStatus();
}
@@ -142,6 +112,7 @@ void SessionDialog::on_cloneButton__clicked() {
assert(!sessionName.empty()); // it should not be possible for the name to be empty
SessionRenameDialog renameDialog;
+ renameDialog.setWindowTitle("Clone Session");
renameDialog.exec();
int result = renameDialog.result();
@@ -176,6 +147,7 @@ void SessionDialog::on_renameButton__clicked() {
assert(item); // it should not be possible for the name to be empty
SessionRenameDialog renameDialog;
+ renameDialog.setWindowTitle("Rename Session");
renameDialog.exec();
int result = renameDialog.result();
@@ -202,25 +174,3 @@ void SessionDialog::on_switchToButton__clicked() {
accept(); // close the dialogue and continue loading the main user interface
}
-
-void SessionDialog::on_saveButton__clicked() {
- /*
- std::string name = sessionNameEdit_->text().toStdString();
-
- if (validSaveName(name))
- {
- SessionItem* newSession =
- SessionHandler::instance()->copySession(SessionHandler::instance()->current(), name); if (newSession)
- {
- //SessionHandler::instance()->add(name);
- refreshListOfSavedSessions();
- SessionHandler::instance()->current(newSession);
- QMessageBox::information(0,tr("Session"), tr("Session saved"));
- }
- else
- {
- QMessageBox::critical(0,tr("Session"), tr("Failed to save session"));
- }
- close();
- }*/
-}
diff --git a/Viewer/ecflowUI/src/SessionDialog.hpp b/Viewer/ecflowUI/src/SessionDialog.hpp
index 7eb0266e6..d5772bfcd 100644
--- a/Viewer/ecflowUI/src/SessionDialog.hpp
+++ b/Viewer/ecflowUI/src/SessionDialog.hpp
@@ -28,9 +28,7 @@ class SessionDialog : public QDialog, protected Ui::SessionDialog {
~SessionDialog() override;
public Q_SLOTS:
- void on_saveButton__clicked();
- void on_sessionNameEdit__textChanged();
- void on_savedSessionsList__currentRowChanged(int currentRow);
+ void on_savedSessionsList__currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous);
void on_cloneButton__clicked();
void on_deleteButton__clicked();
void on_renameButton__clicked();
@@ -39,7 +37,6 @@ public Q_SLOTS:
private:
// Ui::SaveSessionAsDialog *ui;
void addSessionToTable(SessionItem* s);
- bool validSaveName(const std::string& name);
void refreshListOfSavedSessions();
void setButtonsEnabledStatus();
std::string selectedSessionName();
diff --git a/Viewer/ecflowUI/src/SessionHandler.cpp b/Viewer/ecflowUI/src/SessionHandler.cpp
index cd46cec57..d96498efe 100644
--- a/Viewer/ecflowUI/src/SessionHandler.cpp
+++ b/Viewer/ecflowUI/src/SessionHandler.cpp
@@ -36,6 +36,13 @@ SessionItem::~SessionItem() {
}
}
+void SessionItem::name(const std::string& name) {
+ name_ = name;
+ // Since the name changes, we need to update the related directory paths
+ dirPath_ = SessionHandler::sessionDirName(name_);
+ qtPath_ = SessionHandler::sessionQtDirName(name_);
+}
+
void SessionItem::checkDir() {
dirPath_ = SessionHandler::sessionDirName(name_);
DirectoryHandler::createDir(dirPath_);
diff --git a/Viewer/ecflowUI/src/SessionHandler.hpp b/Viewer/ecflowUI/src/SessionHandler.hpp
index 47fa68c48..e114c21ad 100644
--- a/Viewer/ecflowUI/src/SessionHandler.hpp
+++ b/Viewer/ecflowUI/src/SessionHandler.hpp
@@ -21,7 +21,7 @@ class SessionItem {
explicit SessionItem(const std::string&);
virtual ~SessionItem();
- void name(const std::string& name) { name_ = name; }
+ void name(const std::string& name);
const std::string& name() const { return name_; }
std::string sessionFile() const;
diff --git a/Viewer/ecflowUI/src/SessionRenameDialog.ui b/Viewer/ecflowUI/src/SessionRenameDialog.ui
index a82d3c899..b30228fd4 100644
--- a/Viewer/ecflowUI/src/SessionRenameDialog.ui
+++ b/Viewer/ecflowUI/src/SessionRenameDialog.ui
@@ -11,7 +11,7 @@
- Add variable
+
true
@@ -26,7 +26,7 @@
- New name:
+
diff --git a/docs/conf.py b/docs/conf.py
index 0dbfbf679..9f1dc5d68 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -93,7 +93,7 @@
def get_ecflow_version():
- version = "5.13.4"
+ version = "5.13.5"
ecflow_version = version.split(".")
print("Extracted ecflow version '" + str(ecflow_version))
return ecflow_version
diff --git a/docs/glossary.rst b/docs/glossary.rst
index c2870816a..57f92e111 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -1,4 +1,3 @@
-
.. index::
single: Glossary
@@ -87,6 +86,18 @@
responsible for polling the Aviso server, and periodically processing the
latest notifications.
+ The authentication credentials file is expected to be in JSON format, following the `ECMWF Web API `_:
+
+ .. code-block:: json
+
+ {
+ "url" : "https://api.ecmwf.int/v1",
+ "key" : "",
+ "email" : ""
+ }
+
+ Only the fields :code:`url`, :code:`key`, and :code:`email` are required; any additional fields are ignored.
+
check point
The check point file is like the :term:`suite definition`, but includes all the state information.
@@ -1417,6 +1428,17 @@
responsible for polling the remote ecFlow server, and periodically
synchronise node status.
+ The authentication credentials file is expected to be in JSON, according to the following format:
+
+ .. code-block:: json
+
+ {
+ "username" : "",
+ "password" : "",
+ }
+
+ Only the fields :code:`username`, and :code:`password` are required; any additional fields are ignored.
+
node
:term:`suite`, :term:`family` and :term:`task` form a hierarchy.
Where a :term:`suite` serves as the root of the hierarchy.
diff --git a/docs/python_api/AvisoAttr.rst b/docs/python_api/AvisoAttr.rst
index 0aa7b6473..d106dff92 100644
--- a/docs/python_api/AvisoAttr.rst
+++ b/docs/python_api/AvisoAttr.rst
@@ -33,6 +33,8 @@ Usage:
t2 = Task('t2')
t2.add_aviso('name', '{...}', 'http://aviso.com', '60', '/path/to/auth')
+The parameters `url`, `schema`, `polling`, and `auth` are optional
+
.. py:method:: AvisoAttr.auth( (AvisoAttr)arg1) -> str :
:module: ecflow
diff --git a/docs/python_api/MirrorAttr.rst b/docs/python_api/MirrorAttr.rst
index 0e613d7b8..a69c0d434 100644
--- a/docs/python_api/MirrorAttr.rst
+++ b/docs/python_api/MirrorAttr.rst
@@ -34,6 +34,8 @@ Usage:
t2 = Task('t2')
t2.add_aviso('name', '/remote/task', 'remote-ecflow', '3141', '60', True, '/path/to/auth')
+The parameters `remote_host`, `remote_port`, `polling`, `ssl`, and `auth` are optional
+
.. py:method:: MirrorAttr.auth( (MirrorAttr)arg1) -> str :
:module: ecflow
diff --git a/docs/release_notes/version_5.13.rst b/docs/release_notes/version_5.13.rst
index ee8845ffc..ee6ba59ca 100644
--- a/docs/release_notes/version_5.13.rst
+++ b/docs/release_notes/version_5.13.rst
@@ -6,6 +6,33 @@ Version 5.13 updates
.. role:: jiraissue
:class: hidden
+Version 5.13.5
+==============
+
+* `Released `__\ on 2024-10-31
+
+General
+-------
+
+- **Fix** correct the token used for Aviso authentication :jiraissue:`ECFLOW-1982`
+
+ecFlow UI
+---------
+
+- **Fix** correct the disabled/enabled status of Session dialog buttons :jiraissue:`ECFLOW-1980`
+
+ecFlow HTTP
+-----------
+
+- **Fix** reorganise :code:`inherited_variables` to allow repeated names of Node ancestors :jiraissue:`ECFLOW-1978`
+- **Fix** add missing inherited variables to response of endppoint :code:`.../tree` :jiraissue:`ECFLOW-1977`
+
+Python
+------
+
+- **Fix** correct :code:`check_job_creation()` crash on suite with Mirror/Aviso attribute :jiraissue:`ECFLOW-1976`
+- **Fix** allow default parameters in Aviso/Mirror attribute constructors :jiraissue:`ECFLOW-1981`
+
Version 5.13.4
==============
diff --git a/docs/rest_api.rst b/docs/rest_api.rst
index 385c23199..055788bed 100644
--- a/docs/rest_api.rst
+++ b/docs/rest_api.rst
@@ -492,11 +492,18 @@ Obtain the tree of all Suites
* - Description
- **Read** a tree with all suites
* - Parameters
- - :code:`content`, (optional), possible values: :code:`basic`, :code:`full`; :code:`with_id`, (optional), possible values: :code:`true`, :code:`false`
+ - :code:`content`, (optional), possible values: :code:`basic`, :code:`full`
+
+ :code:`with_id`, (optional), possible values: :code:`true`, :code:`false`
+
+ :code:`gen_vars`, (optional), possible values: :code:`true`
+
* - Payload
- *empty*
* - Response
- - See below for details.
+ - See :ref:`below ` for details.
+
+.. _response with suite tree:
Response with Suite tree
""""""""""""""""""""""""
@@ -513,7 +520,69 @@ When query parameter :code:`content` is not provided, or query parameter is :cod
}
}
-When query parameters :code:`content=full&with_id=true`, the full Suite tree is provided:
+When query parameter :code:`content=full` is used, the full Suite tree is provided:
+
+.. code:: json
+
+ {
+ "some_suite": {
+ "type": "suite",
+ "state": {
+ "node": "active",
+ "default": "complete"
+ },
+ "attributes": [
+ {
+ "name": "YMD",
+ "value": "20100114",
+ "const": false,
+ "type": "variable"
+ }
+ ],
+ "children": {
+ "some_family": {
+ "type": "family",
+ "state": {
+ "node": "active",
+ "default": "unknown"
+ },
+ "attributes": [],
+ "children": {
+ "some_task": {
+ "type": "task",
+ "state": {
+ "node": "active",
+ "default": "unknown"
+ },
+ "attributes": [
+ {
+ "name": "some_label",
+ "value": "value",
+ "type": "label"
+ },
+ {
+ "name": "some_meter",
+ "min": 0,
+ "max": 30,
+ "value": 0,
+ "type": "meter"
+ },
+ {
+ "name": "some_event",
+ "value": false,
+ "initial_value": false,
+ "type": "event"
+ }
+ ],
+ "aliases": {}
+ }
+ }
+ }
+ }
+ }
+ }
+
+When query parameters :code:`content=full&with_id=true` are used, the full Suite tree with ids is provided:
.. code:: json
@@ -578,6 +647,9 @@ When query parameters :code:`content=full&with_id=true`, the full Suite tree is
}
}
+When using the query parameter :code:`gen_vars=true` the generated variables are included in the :code:`attributes` section.
+Generated variables can be identified by a :code:`generate` attribute set to :code:`true`.
+
Endpoint :code:`/v1/suites/{path}`
----------------------------------
@@ -621,71 +693,15 @@ Obtain the tree of a Node
- **Read** a tree view of the node at :code:`/{path}`
* - Parameters
- :code:`content`, (optional), possible values: :code:`basic`, :code:`full`
- * - Payload
- - *empty*
- * - Response
- - :code:`{"b":{"c":""}}`, when parameter :code:`content` is not provided, or parameter :code:`content=basic`
-
- :code:`{"b":{"state":{"node":"...","default":"..."},"children":{...},"attributes":[...]}`
- when parameter :code:`content=full`
-Response with Node tree
-"""""""""""""""""""""""
+ :code:`with_id`, (optional), possible values: :code:`true`, :code:`false`
-When query parameter :code:`content` is not provided, or query parameter is :code:`content=basic`, the basic Node tree is provided:
+ :code:`gen_vars`, (optional), possible values: :code:`true`
-.. code:: json
-
- {
- "family": {
- "task": ""
- }
- }
-
-When query parameter :code:`content=full`, the full Node tree is provided:
-
-.. code:: json
-
- {
- "some_family": {
- "type": "family",
- "state": {
- "node": "active",
- "default": "unknown"
- },
- "attributes": [],
- "children": {
- "some_task": {
- "type": "task",
- "state": {
- "node": "active",
- "default": "unknown"
- },
- "attributes": [
- {
- "name": "some_label",
- "value": "value",
- "type": "label"
- },
- {
- "name": "some_meter",
- "min": 0,
- "max": 30,
- "value": 0,
- "type": "meter"
- },
- {
- "name": "some_event",
- "value": false,
- "initial_value": false,
- "type": "event"
- }
- ],
- "aliases": {}
- }
- }
- }
- }
+ * - Payload
+ - *empty*
+ * - Response
+ - Same as :ref:`Suite tree `.
Endpoint :code:`/v1/suites/{path}/definition`
---------------------------------------------
@@ -929,10 +945,86 @@ Obtain all Node attributes
* - Payload
- *empty*
* - Response
- - :code:`{"meters":[...],"variables":[...]}`
+ - See :ref:`below ` for details
* - Example
- :code:`curl https://localhost:8080/v1/suites/path/to/node/attributes`
+.. _response with node attributes:
+
+Response with Node attributes
+"""""""""""""""""""""""""""""
+
+The response to a request for node attributes has the following JSON format.
+Notice how the node variables are separated from inherited variables.
+The inherited variables grouped by ancestor node, each identified by the node name and absolute path.
+
+.. code-block:: text
+
+ {
+ "path": "..."
+
+ "meters": [ { ... }, ... ],
+ "limits": [ { ... }, ... ],
+ "inlimits": [ { ... }, ... ],
+ "events": [ { ... }, ... ],
+ "labels": [ { ... }, ... ],
+ "dates": [ { ... }, ... ],
+ "days": [ { ... }, ... ],
+ "crons": [ { ... }, ... ],
+ "times": [ { ... }, ... ],
+ "todays": [ { ... }, ... ],
+ "repeat": { ... },
+ "trigger": { ... },
+ "complete": { ... },
+ "flag": { ... },
+ "late": { ... },
+ "zombies": [ { ... }, ... ],
+ "generics": [ { ... }, ... ],
+ "queues": [ { ... }, ... ],
+ "autocancel": { ... },
+ "autoarchive": { ... },
+ "autorestore": { ... },
+ "avisos": [ { ... }, ... ],
+ "mirrors" : [ { ... }, ... ],
+ "variables": [
+ {
+ "name": "..."
+ "value": "...",
+ "const": true|false
+ },
+ {
+ "name": "..."
+ "value": "...",
+ "generated": true # only present if the variable is generated
+ "const": true|false
+ },
+ ...
+ ]
+ "inherited_variables": [
+ {
+ "name": "......"
+ "path": "......",
+ "variables: [
+ {
+ "name": "..."
+ "value": "...",
+ "type": "variable",
+ "const": true|false
+ },
+ {
+ "name": "..."
+ "value": "...",
+ "type": "variable",
+ "generated": true # only present if the variable is generated
+ "const": true|false
+ },
+ ...
+ ]
+ },
+ ...
+ }
+ }
+
Update a Node attribute
^^^^^^^^^^^^^^^^^^^^^^^
@@ -1262,8 +1354,8 @@ Create a new Server attribute
* - Response
- :code:`{"message":"Attribute added successfully"}`
-Obtain a Server attribute
-^^^^^^^^^^^^^^^^^^^^^^^^^
+Obtain all Server attributes
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. list-table::
:stub-columns: 1
diff --git a/docs/ug/user_manual/ecflow_variables/generated_variables.rst b/docs/ug/user_manual/ecflow_variables/generated_variables.rst
index 3ccb8c561..f8e48228c 100644
--- a/docs/ug/user_manual/ecflow_variables/generated_variables.rst
+++ b/docs/ug/user_manual/ecflow_variables/generated_variables.rst
@@ -25,6 +25,10 @@ The table below shows a list of generated variables.
- Variable name
- Explanation
- Example
+ * - server
+ - ECF_MICRO
+ - The default preprocessor character used during variable substitution
+ - %
* - server
- ECF_TRIES
- The default number of tries for each task
@@ -45,18 +49,46 @@ The table below shows a list of generated variables.
- ECF_JOB_CMD
- Command to be executed to send a job
- %ECF_JOB% 1> %ECF_JOBOUT% 2>&1
+ * - server
+ - ECF_KILL_CMD
+ - Command to be executed to kill a job
+ - kill -15 %ECF_RID%
+ * - server
+ - ECF_STATUS_CMD
+ - Command to be executed to retrieve the job status
+ - ps --pid %ECF_RID% -f > %ECF_JOB%.stat 2>&1
+ * - server
+ - ECF_URL_CMD
+ - Command to be executed to open job files in a browser
+ - ${BROWSER:=firefox} -new-tab %ECF_URL_BASE%/%ECF_URL%
+ * - server
+ - ECF_URL_BASE
+ - The base prefix for the job URL
+ - https://confluence.ecmwf.int
+ * - server
+ - ECF_URL
+ - The stem suffix for the job URL
+ - display/ECFLOW/ecflow+home
* - server
- ECF_LISTS
- Name of the ecFlow white-list file
- ecf.lists
* - server
- - ECF_PASS
+ - ECF_PASSWD
- Default password string to replace the real password, when communicating with the server.
- DJP
+ * - server
+ - ECF_CUSTOM_PASSWD
+ - Default custom password string to replace the real password, when communicating with the server.
+ - DJP
* - server
- ECF_LOG
- Name of the ecFlow history, or log file
- ecf.log
+ * - server
+ - ECF_INTERVAL
+ - The interval between check of time dependencies and job submission
+ - 60
* - server
- ECF_CHECK
- Name of the checkpoint file
@@ -65,54 +97,70 @@ The table below shows a list of generated variables.
- ECF_CHECKOLD
- Name of the backup of the checkpoint file
- ecf.check.b
- * - Suite
+ * - server
+ - ECF_CHECKINTERVAL
+ - The interval, in seconds, between saving current state into the checkpoint file
+ - 120
+ * - server
+ - ECF_CHECKMODE
+ - The mode to perform checkpoint file saving
+ - CHECK_NEVER, CHECK_ON_TIME, CHECK_ALWAYS
+ * - suite
- SUITE
- The name of the suite
- Backarc
- * - server
+ * - suite
- DATE
- Date of the suite in format DD.MM.YYYY
- 21.02.2012
- * - server
+ * - suite
- DAY
- Full name of the weekday
- Sunday
- * - server
+ * - suite
- DD
- Day of the month, with two digits
- 07
- * - server
+ * - suite
- DOW
- Day Of the Week
- 0
- * - server
+ * - suite
- DOY
- Day Of the Year
- 52
- * - server
+ * - suite
- MM
- The month of the year, with two digits
- 02
- * - server
+ * - suite
- MONTH
- Full name of the month
- February
- * - server
+ * - suite
- YYYY
- A year with four digits
- 2012
- * - server
+ * - suite
- ECF_DATE
- Single date in format YYYYMMDD
- 20120221
- * - server
+ * - suite
+ - TIME
+ - Time of the suites clock, HHMM
+ - 2032
+ * - suite
- ECF_TIME
- Time of the suites clock, HH:MM
- 20:32
- * - server
+ * - suite
- ECF_CLOCK
- Composite weekday, month, Day Of the Week, Day Of Year
- sunday:february:0:52
+ * - suite
+ - ECF_JULIAN
+ - Julian day
+ - 2455979
* - family
- FAMILY
- The name of the family, (avoid using)
diff --git a/libs/node/src/ecflow/node/AvisoAttr.cpp b/libs/node/src/ecflow/node/AvisoAttr.cpp
index 31b66f7b2..a5c5eb369 100644
--- a/libs/node/src/ecflow/node/AvisoAttr.cpp
+++ b/libs/node/src/ecflow/node/AvisoAttr.cpp
@@ -88,13 +88,12 @@ bool AvisoAttr::why(std::string& theReasonWhy) const {
void AvisoAttr::reset() {
state_change_no_ = Ecf::incr_state_change_no();
- if (parent_->state() == NState::QUEUED) {
+ if (parent_ && (parent_->state() == NState::QUEUED)) {
start();
}
}
bool AvisoAttr::isFree() const {
- SLOG(D, "AvisoAttr: check Aviso attribute (name: " << name_ << ", listener: " << listener_ << ") is free");
if (controller_ == nullptr) {
return false;
@@ -105,6 +104,7 @@ bool AvisoAttr::isFree() const {
if (notifications.empty()) {
// No notifications, nothing to do -- task continues to wait
+ SLOG(D, "AvisoAttr: (path: " << this->path() << ", name: " << name_ << ", listener: " << listener_ << "): no notifications found");
return false;
}
@@ -143,6 +143,8 @@ bool AvisoAttr::isFree() const {
ecf::visit_parents(*parent_, [n = this->state_change_no_](Node& node) { node.set_state_change_no(n); });
+ SLOG(D, "AvisoAttr: (path: " << this->path() << ", name: " << name_ << ", listener: " << listener_ << ") " << std::string{(is_free ? "" : "no ")} + "notifications found");
+
return is_free;
}
diff --git a/libs/node/src/ecflow/node/MirrorAttr.cpp b/libs/node/src/ecflow/node/MirrorAttr.cpp
index 25db0edde..bce3abbcd 100644
--- a/libs/node/src/ecflow/node/MirrorAttr.cpp
+++ b/libs/node/src/ecflow/node/MirrorAttr.cpp
@@ -160,6 +160,10 @@ void MirrorAttr::mirror() {
std::optional MirrorAttr::resolve_cfg(const std::string& value, std::string_view default_value) const {
// Substitude variable in local value
std::string local = value;
+ if(!parent_) {
+ return std::nullopt;
+ }
+
parent_->variableSubstitution(local);
// Ensure substituted value is not default
@@ -199,9 +203,11 @@ void MirrorAttr::start_controller() {
state_change_no_ = Ecf::incr_state_change_no();
reason_ = Message("Unable to start mirror. Failed to resolve mirror remote host: ", remote_host_).str();
- parent_->flag().set(Flag::REMOTE_ERROR);
- parent_->flag().set_state_change_no(state_change_no_);
- parent_->setStateOnly(NState::UNKNOWN, true);
+ if (parent_) {
+ parent_->flag().set(Flag::REMOTE_ERROR);
+ parent_->flag().set_state_change_no(state_change_no_);
+ parent_->setStateOnly(NState::UNKNOWN, true);
+ }
return;
}
// For the remaining configuration, we use fallback values if the resolution fails
diff --git a/libs/node/src/ecflow/node/Node.cpp b/libs/node/src/ecflow/node/Node.cpp
index 712866388..1e24dfd69 100644
--- a/libs/node/src/ecflow/node/Node.cpp
+++ b/libs/node/src/ecflow/node/Node.cpp
@@ -2668,6 +2668,12 @@ void Node::gen_variables(std::vector& vec) const {
repeat_.gen_variables(vec); // if repeat_ is empty vec is unchanged
}
+std::vector Node::gen_variables() const {
+ std::vector generated;
+ gen_variables(generated);
+ return generated;
+}
+
const Variable& Node::findGenVariable(const std::string& name) const {
return repeat_.find_gen_variable(name); // if repeat_ is empty find returns empty variable by ref
}
diff --git a/libs/node/src/ecflow/node/Node.hpp b/libs/node/src/ecflow/node/Node.hpp
index 73a91fe16..4f6b44376 100644
--- a/libs/node/src/ecflow/node/Node.hpp
+++ b/libs/node/src/ecflow/node/Node.hpp
@@ -420,7 +420,8 @@ class Node : public std::enable_shared_from_this {
ecf::Flag& flag() { return flag_; }
const ecf::Flag& get_flag() const { return flag_; }
- virtual void gen_variables(std::vector&) const;
+ [[deprecated]] virtual void gen_variables(std::vector&) const;
+ std::vector gen_variables() const;
bool getLabelValue(const std::string& name, std::string& value) const;
bool getLabelNewValue(const std::string& name, std::string& value) const;
diff --git a/libs/pyext/ecflow/__init__.py b/libs/pyext/ecflow/__init__.py
index afc50e0da..48334cbdf 100644
--- a/libs/pyext/ecflow/__init__.py
+++ b/libs/pyext/ecflow/__init__.py
@@ -15,6 +15,6 @@
The ecFlow python module
"""
-__version__ = '5.13.4'
+__version__ = '5.13.5'
# http://stackoverflow.com/questions/13040646/how-do-i-create-documentation-with-pydoc
diff --git a/libs/pyext/src/ecflow/python/ExportNodeAttr.cpp b/libs/pyext/src/ecflow/python/ExportNodeAttr.cpp
index 384f6ac7d..627dd40a7 100644
--- a/libs/pyext/src/ecflow/python/ExportNodeAttr.cpp
+++ b/libs/pyext/src/ecflow/python/ExportNodeAttr.cpp
@@ -285,25 +285,82 @@ static job_creation_ctrl_ptr makeJobCreationCtrl() {
static std::shared_ptr aviso_init(const std::string& name,
const std::string& listener,
- const std::string& url,
- const std::string& schema,
- const std::string& polling,
- const std::string& auth) {
+ const std::string& url = AvisoAttr::default_url,
+ const std::string& schema = AvisoAttr::default_schema,
+ const std::string& polling = AvisoAttr::default_polling,
+ const std::string& auth = AvisoAttr::default_auth) {
auto attr = std::make_shared(nullptr, name, listener, url, schema, polling, 0, auth, "");
return attr;
}
+static std::shared_ptr aviso_init_defaults_0(const std::string& name, const std::string& listener) {
+ return aviso_init(name, listener);
+}
+
+static std::shared_ptr
+aviso_init_defaults_1(const std::string& name, const std::string& listener, const std::string& url) {
+ return aviso_init(name, listener, url);
+}
+
+static std::shared_ptr aviso_init_defaults_2(const std::string& name,
+ const std::string& listener,
+ const std::string& url,
+ const std::string& schema) {
+ return aviso_init(name, listener, url, schema);
+}
+
+static std::shared_ptr aviso_init_defaults_3(const std::string& name,
+ const std::string& listener,
+ const std::string& url,
+ const std::string& schema,
+ const std::string& polling) {
+ return aviso_init(name, listener, url, schema, polling);
+}
+
static std::shared_ptr mirror_init(const std::string& name,
const std::string& path,
- const std::string& host,
- const std::string& port,
- const std::string& polling,
- bool ssl,
- const std::string& auth) {
+ const std::string& host = ecf::MirrorAttr::default_remote_host,
+ const std::string& port = ecf::MirrorAttr::default_remote_port,
+ const std::string& polling = ecf::MirrorAttr::default_polling,
+ bool ssl = false,
+ const std::string& auth = ecf::MirrorAttr::default_remote_auth) {
auto attr = std::make_shared(nullptr, name, path, host, port, polling, ssl, auth, "");
return attr;
}
+static std::shared_ptr mirror_init_defaults_0(const std::string& name, const std::string& path) {
+ return mirror_init(name, path);
+}
+
+static std::shared_ptr
+mirror_init_defaults_1(const std::string& name, const std::string& path, const std::string& host) {
+ return mirror_init(name, path, host);
+}
+
+static std::shared_ptr mirror_init_defaults_2(const std::string& name,
+ const std::string& path,
+ const std::string& host,
+ const std::string& port) {
+ return mirror_init(name, path, host, port);
+}
+
+static std::shared_ptr mirror_init_defaults_3(const std::string& name,
+ const std::string& path,
+ const std::string& host,
+ const std::string& port,
+ const std::string& polling) {
+ return mirror_init(name, path, host, port, polling);
+}
+
+static std::shared_ptr mirror_init_defaults_4(const std::string& name,
+ const std::string& path,
+ const std::string& host,
+ const std::string& port,
+ const std::string& polling,
+ bool ssl) {
+ return mirror_init(name, path, host, port, polling, ssl);
+}
+
void export_NodeAttr() {
// Trigger & Complete thin wrapper over Expression, allows us to call: Task("a").add(Trigger("a=1"),Complete("b=1"))
class_>("Trigger", DefsDoc::trigger(), init())
@@ -945,8 +1002,8 @@ void export_NodeAttr() {
.def("step", &Repeat::step, "The increment for the repeat, as an integer")
.def("value", &Repeat::last_valid_value, "The current value of the repeat as an integer");
- void (CronAttr::*add_time_series)(const TimeSeries&) = &CronAttr::addTimeSeries;
- void (CronAttr::*add_time_series_2)(const TimeSlot& s, const TimeSlot& f, const TimeSlot& i) =
+ void (CronAttr::* add_time_series)(const TimeSeries&) = &CronAttr::addTimeSeries;
+ void (CronAttr::* add_time_series_2)(const TimeSlot& s, const TimeSlot& f, const TimeSlot& i) =
&CronAttr::addTimeSeries;
class_>("Cron", NodeAttrDoc::cron_doc())
.def("__init__", raw_function(&cron_raw_constructor, 1)) // will call -> cron_init or cron_init1
@@ -1018,6 +1075,10 @@ void export_NodeAttr() {
class_("AvisoAttr", NodeAttrDoc::aviso_doc())
.def("__init__", make_constructor(&aviso_init))
+ .def("__init__", make_constructor(&aviso_init_defaults_0))
+ .def("__init__", make_constructor(&aviso_init_defaults_1))
+ .def("__init__", make_constructor(&aviso_init_defaults_2))
+ .def("__init__", make_constructor(&aviso_init_defaults_3))
.def(self == self) // __eq__
.def("__str__", &ecf::to_python_string) // __str__
.def("__copy__", copyObject) // __copy__ uses copy constructor
@@ -1045,6 +1106,11 @@ void export_NodeAttr() {
class_("MirrorAttr", NodeAttrDoc::mirror_doc())
.def("__init__", make_constructor(&mirror_init))
+ .def("__init__", make_constructor(&mirror_init_defaults_0))
+ .def("__init__", make_constructor(&mirror_init_defaults_1))
+ .def("__init__", make_constructor(&mirror_init_defaults_2))
+ .def("__init__", make_constructor(&mirror_init_defaults_3))
+ .def("__init__", make_constructor(&mirror_init_defaults_4))
.def(self == self) // __eq__
.def("__str__", &ecf::to_python_string) // __str__
.def("__copy__", copyObject) // __copy__ uses copy constructor
diff --git a/libs/pyext/src/ecflow/python/NodeAttrDoc.cpp b/libs/pyext/src/ecflow/python/NodeAttrDoc.cpp
index 8388b7ee7..5954496c1 100644
--- a/libs/pyext/src/ecflow/python/NodeAttrDoc.cpp
+++ b/libs/pyext/src/ecflow/python/NodeAttrDoc.cpp
@@ -728,7 +728,9 @@ const char* NodeAttrDoc::aviso_doc() {
" AvisoAttr('name', '{...}', 'http://aviso.com', '60', '/path/to/auth'))\n"
"\n"
" t2 = Task('t2')\n"
- " t2.add_aviso('name', '{...}', 'http://aviso.com', '60', '/path/to/auth')\n";
+ " t2.add_aviso('name', '{...}', 'http://aviso.com', '60', '/path/to/auth')\n"
+ "\n"
+ "The parameters `url`, `schema`, `polling`, and `auth` are optional\n";
}
const char* NodeAttrDoc::mirror_doc() {
@@ -754,5 +756,7 @@ const char* NodeAttrDoc::mirror_doc() {
" MirrorAttr('name', '/remote/task', 'remote-ecflow', '3141', '60', True, '/path/to/auth'))\n"
"\n"
" t2 = Task('t2')\n"
- " t2.add_aviso('name', '/remote/task', 'remote-ecflow', '3141', '60', True, '/path/to/auth')\n";
+ " t2.add_aviso('name', '/remote/task', 'remote-ecflow', '3141', '60', True, '/path/to/auth')\n"
+ "\n"
+ "The parameters `remote_host`, `remote_port`, `polling`, `ssl`, and `auth` are optional\n";
}
diff --git a/libs/pyext/test/py_u_TestAviso.py b/libs/pyext/test/py_u_TestAviso.py
index eb7041559..99d7a516e 100644
--- a/libs/pyext/test/py_u_TestAviso.py
+++ b/libs/pyext/test/py_u_TestAviso.py
@@ -25,6 +25,78 @@ def can_create_aviso_from_parameters():
assert aviso.auth() == "auth"
+def can_create_aviso_from_default_parameters_0():
+ suite = ecf.Suite("s1")
+
+ family = ecf.Family("f1")
+ suite.add_family(family)
+
+ task = ecf.Task("f1", ecf.AvisoAttr("name", "listener"))
+ assert len(list(task.avisos)) == 1
+
+ actual = list(task.avisos)[0]
+ assert actual.name() == "name"
+ assert actual.listener() == "listener"
+ assert actual.url() == "%ECF_AVISO_URL%"
+ assert actual.schema() == "%ECF_AVISO_SCHEMA%"
+ assert actual.polling() == "%ECF_AVISO_POLLING%"
+ assert actual.auth() == "%ECF_AVISO_AUTH%"
+
+
+def can_create_aviso_from_default_parameters_1():
+ suite = ecf.Suite("s1")
+
+ family = ecf.Family("f1")
+ suite.add_family(family)
+
+ task = ecf.Task("f1", ecf.AvisoAttr("name", "listener", "url"))
+ assert len(list(task.avisos)) == 1
+
+ actual = list(task.avisos)[0]
+ assert actual.name() == "name"
+ assert actual.listener() == "listener"
+ assert actual.url() == "url"
+ assert actual.schema() == "%ECF_AVISO_SCHEMA%"
+ assert actual.polling() == "%ECF_AVISO_POLLING%"
+ assert actual.auth() == "%ECF_AVISO_AUTH%"
+
+
+def can_create_aviso_from_default_parameters_2():
+ suite = ecf.Suite("s1")
+
+ family = ecf.Family("f1")
+ suite.add_family(family)
+
+ task = ecf.Task("f1", ecf.AvisoAttr("name", "listener", "url", "schema"))
+ assert len(list(task.avisos)) == 1
+
+ actual = list(task.avisos)[0]
+ assert actual.name() == "name"
+ assert actual.listener() == "listener"
+ assert actual.url() == "url"
+ assert actual.schema() == "schema"
+ assert actual.polling() == "%ECF_AVISO_POLLING%"
+ assert actual.auth() == "%ECF_AVISO_AUTH%"
+
+
+def can_create_aviso_from_default_parameters_3():
+ suite = ecf.Suite("s1")
+
+ family = ecf.Family("f1")
+ suite.add_family(family)
+
+ task = ecf.Task("f1", ecf.AvisoAttr("name", "listener", "url", "schema", "polling"))
+ assert len(list(task.avisos)) == 1
+
+ actual = list(task.avisos)[0]
+ assert actual.name() == "name"
+ assert actual.listener() == "listener"
+ assert actual.url() == "url"
+ assert actual.schema() == "schema"
+ assert actual.polling() == "polling"
+ assert actual.auth() == "%ECF_AVISO_AUTH%"
+
+
def can_add_aviso_to_task():
suite = ecf.Suite("s1")
@@ -79,12 +151,35 @@ def cannot_have_multiple_avisos_in_single_task():
assert True
+def can_check_job_creation_with_aviso():
+ defs = ecf.Defs()
+
+ suite = ecf.Suite("s")
+ defs.add_suite(suite)
+
+ family = ecf.Family("f")
+ suite.add_family(family)
+
+ task = ecf.Task("t")
+ family.add_task(task)
+
+ aviso = ecf.AvisoAttr("name", "listener", "url", "schema", "polling", "auth")
+ task.add_aviso(aviso)
+
+ defs.check_job_creation()
+
+
if __name__ == "__main__":
Test.print_test_start(os.path.basename(__file__))
can_create_aviso_from_parameters()
+ can_create_aviso_from_default_parameters_0()
+ can_create_aviso_from_default_parameters_1()
+ can_create_aviso_from_default_parameters_2()
+ can_create_aviso_from_default_parameters_3()
can_add_aviso_to_task()
can_embed_aviso_into_task()
cannot_have_multiple_avisos_in_single_task()
+ can_check_job_creation_with_aviso()
print("All tests pass")
diff --git a/libs/pyext/test/py_u_TestMirror.py b/libs/pyext/test/py_u_TestMirror.py
index fd3f9afed..c3822dabd 100644
--- a/libs/pyext/test/py_u_TestMirror.py
+++ b/libs/pyext/test/py_u_TestMirror.py
@@ -26,6 +26,61 @@ def can_create_mirror_from_parameters():
assert mirror.auth() == "auth"
+def can_create_mirror_from_default_parameters_0():
+ mirror = ecf.MirrorAttr("name", "r_path")
+ assert mirror.name() == "name"
+ assert mirror.remote_path() == "r_path"
+ assert mirror.remote_host() == "%ECF_MIRROR_REMOTE_HOST%"
+ assert mirror.remote_port() == "%ECF_MIRROR_REMOTE_PORT%"
+ assert mirror.polling() == "%ECF_MIRROR_REMOTE_POLLING%"
+ assert mirror.ssl() == False
+ assert mirror.auth() == "%ECF_MIRROR_REMOTE_AUTH%"
+
+
+def can_create_mirror_from_default_parameters_1():
+ mirror = ecf.MirrorAttr("name", "r_path", "r_host")
+ assert mirror.name() == "name"
+ assert mirror.remote_path() == "r_path"
+ assert mirror.remote_host() == "r_host"
+ assert mirror.remote_port() == "%ECF_MIRROR_REMOTE_PORT%"
+ assert mirror.polling() == "%ECF_MIRROR_REMOTE_POLLING%"
+ assert mirror.ssl() == False
+ assert mirror.auth() == "%ECF_MIRROR_REMOTE_AUTH%"
+
+
+def can_create_mirror_from_default_parameters_2():
+ mirror = ecf.MirrorAttr("name", "r_path", "r_host", "r_port")
+ assert mirror.name() == "name"
+ assert mirror.remote_path() == "r_path"
+ assert mirror.remote_host() == "r_host"
+ assert mirror.remote_port() == "r_port"
+ assert mirror.polling() == "%ECF_MIRROR_REMOTE_POLLING%"
+ assert mirror.ssl() == False
+ assert mirror.auth() == "%ECF_MIRROR_REMOTE_AUTH%"
+
+
+def can_create_mirror_from_default_parameters_3():
+ mirror = ecf.MirrorAttr("name", "r_path", "r_host", "r_port", "polling")
+ assert mirror.name() == "name"
+ assert mirror.remote_path() == "r_path"
+ assert mirror.remote_host() == "r_host"
+ assert mirror.remote_port() == "r_port"
+ assert mirror.polling() == "polling"
+ assert mirror.ssl() == False
+ assert mirror.auth() == "%ECF_MIRROR_REMOTE_AUTH%"
+
+
+def can_create_mirror_from_default_parameters_4():
+ mirror = ecf.MirrorAttr("name", "r_path", "r_host", "r_port", "polling", True)
+ assert mirror.name() == "name"
+ assert mirror.remote_path() == "r_path"
+ assert mirror.remote_host() == "r_host"
+ assert mirror.remote_port() == "r_port"
+ assert mirror.polling() == "polling"
+ assert mirror.ssl() == True
+ assert mirror.auth() == "%ECF_MIRROR_AUTH%"
+
+
def can_add_mirror_to_task():
suite = ecf.Suite("s1")
@@ -79,13 +134,35 @@ def cannot_have_multiple_mirrors_in_single_task():
except RuntimeError as e:
assert True
+def can_check_job_creation_with_mirror():
+ defs = ecf.Defs()
+
+ suite = ecf.Suite("s")
+ defs.add_suite(suite)
+
+ family = ecf.Family("f")
+ suite.add_family(family)
+
+ task = ecf.Task("t");
+ family.add_task(task)
+
+ mirror = ecf.MirrorAttr("name", "r_path", "r_host", "r_port", "polling", True, "auth")
+ task.add_mirror(mirror)
+
+ defs.check_job_creation()
+
if __name__ == "__main__":
Test.print_test_start(os.path.basename(__file__))
can_create_mirror_from_parameters()
+ can_create_mirror_from_default_parameters_0()
+ can_create_mirror_from_default_parameters_1()
+ can_create_mirror_from_default_parameters_2()
+ can_create_mirror_from_default_parameters_3()
can_add_mirror_to_task()
can_embed_mirror_into_task()
cannot_have_multiple_mirrors_in_single_task()
+ can_check_job_creation_with_mirror()
print("All tests pass")
diff --git a/libs/rest/src/ecflow/http/ApiV1.cpp b/libs/rest/src/ecflow/http/ApiV1.cpp
index 45e689757..b93feb764 100644
--- a/libs/rest/src/ecflow/http/ApiV1.cpp
+++ b/libs/rest/src/ecflow/http/ApiV1.cpp
@@ -209,13 +209,18 @@ ojson filter_json(const ojson& j, const httplib::Request& r) {
}
static std::string get_tree_content_kind(const httplib::Request& request) {
- constexpr const char* content = "content";
- return request.has_param(content) ? request.get_param_value(content) : "basic";
+ constexpr const char* parameter = "content";
+ return request.has_param(parameter) ? request.get_param_value(parameter) : "basic";
}
static bool get_tree_content_id_flag(const httplib::Request& request) {
- constexpr const char* content = "with_id";
- return request.has_param(content) ? (request.get_param_value(content) == "true" ? true : false) : false;
+ constexpr const char* parameter = "with_id";
+ return request.has_param(parameter) ? (request.get_param_value(parameter) == "true" ? true : false) : false;
+}
+
+static bool get_tree_content_gen_vars(const httplib::Request& request) {
+ constexpr const char* parameter = "gen_vars";
+ return request.has_param(parameter) ? (request.get_param_value(parameter) == "true" ? true : false) : false;
}
} // namespace
@@ -269,7 +274,8 @@ void node_tree_read(const httplib::Request& request, httplib::Response& response
const std::string path = request.matches[1];
std::string tree_kind = get_tree_content_kind(request);
bool with_id = get_tree_content_id_flag(request);
- ojson tree_content = (tree_kind == "full") ? get_full_node_tree(path, with_id) : get_basic_node_tree(path);
+ bool gen_vars = get_tree_content_gen_vars(request);
+ ojson tree_content = (tree_kind == "full") ? get_full_node_tree(path, with_id, gen_vars) : get_basic_node_tree(path);
ojson j = filter_json(tree_content, request);
response.status = HttpStatusCode::success_ok;
response.set_content(j.dump(), "application/json");
diff --git a/libs/rest/src/ecflow/http/ApiV1Impl.cpp b/libs/rest/src/ecflow/http/ApiV1Impl.cpp
index 9a4bcfbbb..1ae0cb453 100644
--- a/libs/rest/src/ecflow/http/ApiV1Impl.cpp
+++ b/libs/rest/src/ecflow/http/ApiV1Impl.cpp
@@ -129,8 +129,8 @@ ojson get_basic_node_tree(const std::string& path) {
return tree.content();
}
-ojson get_full_node_tree(const std::string& path, bool with_id) {
- FullTree tree{with_id};
+ojson get_full_node_tree(const std::string& path, bool with_id, bool with_gen_vars) {
+ FullTree tree{with_id, with_gen_vars};
DefsTreeVisitor(get_defs(), tree).visit_at(path);
return tree.content();
}
@@ -228,6 +228,24 @@ ojson get_server_attributes() {
return j;
}
+static ojson get_node_variables_array(const Node& node) {
+ auto container = ojson::array();
+ // Collect 'normal' variables
+ for (const auto& variable : node.variables()) {
+ ojson object;
+ to_json(object, variable);
+ container.push_back(object);
+ }
+ // ... and generated variables
+ for (const auto& variable : node.gen_variables()) {
+ ojson object;
+ to_json(object, variable);
+ object["generated"] = true;
+ container.push_back(object);
+ }
+ return container;
+}
+
ojson get_node_attributes(const std::string& path) {
ojson j;
@@ -237,7 +255,6 @@ ojson get_node_attributes(const std::string& path) {
j["limits"] = node->limits();
j["inlimits"] = node->inlimits();
j["events"] = node->events();
- j["variables"] = node->variables();
j["labels"] = node->labels();
j["dates"] = node->dates();
j["days"] = node->days();
@@ -259,30 +276,26 @@ ojson get_node_attributes(const std::string& path) {
j["avisos"] = node->avisos();
j["mirrors"] = node->mirrors();
- {
- // Collect 'normal' variables
- auto vars = node->variables();
- // ... and generated variables
- node->gen_variables(vars);
+ j["variables"] = get_node_variables_array(*node);
- j["variables"] = vars;
- }
-
- apply_to_parents(node->parent(), [&j](const Node* n) {
- // Collect 'normal' variables
- auto vars = n->variables();
- // ... and generated variables
- n->gen_variables(vars);
+ {
+ auto inherited = ojson::array();
- j["inherited_variables"][n->name()] = vars;
- });
+ // Collect 'inherited' variables from parents
+ apply_to_parents(node->parent(), [&inherited](const Node* n) {
+ inherited.push_back(ojson::object(
+ {{"name", n->name()}, {"path", n->absNodePath()}, {"variables", get_node_variables_array(*n)}}));
+ });
- auto server_variables = get_defs()->server().server_variables();
- auto user_variables = get_defs()->server().user_variables();
+ // ... and from server
+ auto server_variables = get_defs()->server().server_variables();
+ auto user_variables = get_defs()->server().user_variables();
+ server_variables.insert(server_variables.end(), user_variables.begin(), user_variables.end());
- server_variables.insert(server_variables.end(), user_variables.begin(), user_variables.end());
+ inherited.push_back(ojson::object({{"name", "server"}, {"path", "/"}, {"variables", server_variables}}));
- j["inherited_variables"]["server"] = server_variables;
+ j["inherited_variables"] = inherited;
+ }
return j;
}
diff --git a/libs/rest/src/ecflow/http/ApiV1Impl.hpp b/libs/rest/src/ecflow/http/ApiV1Impl.hpp
index 97c5d1885..adedec3ed 100644
--- a/libs/rest/src/ecflow/http/ApiV1Impl.hpp
+++ b/libs/rest/src/ecflow/http/ApiV1Impl.hpp
@@ -18,7 +18,7 @@
namespace ecf::http {
ojson get_basic_node_tree(const std::string& path);
-ojson get_full_node_tree(const std::string& path, bool with_id);
+ojson get_full_node_tree(const std::string& path, bool with_id, bool with_gen_vars);
ojson get_sparser_node_tree(const std::string& path);
void add_suite(const httplib::Request& request, httplib::Response& response);
diff --git a/libs/rest/src/ecflow/http/HttpLibrary.hpp b/libs/rest/src/ecflow/http/HttpLibrary.hpp
index 0afdacee9..8ee15690a 100644
--- a/libs/rest/src/ecflow/http/HttpLibrary.hpp
+++ b/libs/rest/src/ecflow/http/HttpLibrary.hpp
@@ -23,9 +23,15 @@
#define CPPHTTPLIB_RECV_FLAGS MSG_NOSIGNAL
#endif
-#ifdef ECF_OPENSSL
- #define CPPHTTPLIB_OPENSSL_SUPPORT
+#if defined(ECF_OPENSSL)
+ #include
+ #if OPENSSL_VERSION_NUMBER < 0x1010100fL
+ #warning OpenSSL versions prior to 1.1.1 detected. Aviso ETCD HTTP client will be build without OpenSSL support!
+ #else
+ #define CPPHTTPLIB_OPENSSL_SUPPORT
+ #endif
#endif
+
#ifdef ECF_HTTP_COMPRESSION
#define CPPHTTPLIB_ZLIB_SUPPORT
#endif
diff --git a/libs/rest/src/ecflow/http/TreeGeneration.hpp b/libs/rest/src/ecflow/http/TreeGeneration.hpp
index 8d224b54f..ba33eb3c1 100644
--- a/libs/rest/src/ecflow/http/TreeGeneration.hpp
+++ b/libs/rest/src/ecflow/http/TreeGeneration.hpp
@@ -63,7 +63,11 @@ struct BasicTree
struct FullTree
{
- FullTree(bool with_id) : root_(ojson::object({})), stack_{&root_}, with_id_{with_id} {}
+ FullTree(bool with_id, bool with_gen_vars)
+ : root_(ojson::object({})),
+ stack_{&root_},
+ with_id_{with_id},
+ with_gen_vars_{with_gen_vars} {}
void begin_visit(const Suite& suite) {
ojson& parent_ = *stack_.back();
@@ -74,7 +78,7 @@ struct FullTree
current["id"] = suite.absNodePath();
}
publish_state(suite, current);
- publish_attributes(suite, current);
+ publish_attributes(suite, current, with_gen_vars_);
ojson& children = current["children"] = ojson::object({});
stack_.push_back(&children);
@@ -91,7 +95,7 @@ struct FullTree
current["id"] = family.absNodePath();
}
publish_state(family, current);
- publish_attributes(family, current);
+ publish_attributes(family, current, with_gen_vars_);
ojson& children = current["children"] = ojson::object({});
stack_.push_back(&children);
@@ -108,7 +112,7 @@ struct FullTree
current["id"] = task.absNodePath();
}
publish_state(task, current);
- publish_attributes(task, current);
+ publish_attributes(task, current, with_gen_vars_);
ojson& children = current["aliases"] = ojson::object({});
stack_.push_back(&children);
@@ -126,7 +130,7 @@ struct FullTree
current["id"] = alias.absNodePath();
}
publish_state(alias, current);
- publish_attributes(alias, current);
+ publish_attributes(alias, current, with_gen_vars_);
}
void end_visit(const Alias& alias [[maybe_unused]]) { stack_.pop_back(); }
@@ -141,15 +145,18 @@ struct FullTree
state["default"] = DState::toString(node.dstate());
}
- template
- static ojson publish_atribute(const T& attr, std::string_view type) {
+ template
+ static ojson publish_atribute(const T& attr, std::string_view type, P... parameters) {
auto j = ojson::object({});
to_json(j, attr);
j["type"] = type;
+ if constexpr (sizeof...(P) > 0) {
+ ((j[parameters.first] = parameters.second), ...);
+ }
return j;
}
- static void publish_attributes(const Node& node, ojson& parent) {
+ static void publish_attributes(const Node& node, ojson& parent, bool with_gen_vars) {
ojson& array = parent["attributes"] = ojson::array();
for (const auto& attr : node.labels()) {
@@ -164,6 +171,11 @@ struct FullTree
for (const auto& attr : node.variables()) {
array.emplace_back(publish_atribute(attr, "variable"));
}
+ if (with_gen_vars) {
+ for (const auto& attr : node.gen_variables()) {
+ array.emplace_back(publish_atribute(attr, "variable", std::make_pair("generated", true)));
+ }
+ }
for (const auto& attr : node.limits()) {
array.emplace_back(publish_atribute(*attr, "limit"));
}
@@ -252,6 +264,7 @@ struct FullTree
ojson root_;
std::vector stack_;
bool with_id_;
+ bool with_gen_vars_;
};
} // namespace ecf::http
diff --git a/libs/rest/test/TestApiV1.cpp b/libs/rest/test/TestApiV1.cpp
index 8d06e0eb1..e8318b50f 100644
--- a/libs/rest/test/TestApiV1.cpp
+++ b/libs/rest/test/TestApiV1.cpp
@@ -475,6 +475,71 @@ BOOST_AUTO_TEST_CASE(test_node_full_tree) {
wait_until([] { return false == check_for_path("/v1/suites/full_suite/definition"); });
}
+BOOST_AUTO_TEST_CASE(test_node_full_tree_with_generated_variables) {
+ std::cout << "======== " << boost::unit_test::framework::current_test_case().p_name << " =========" << std::endl;
+
+ // (0) Clean up -- in case there is any left-over from passed/failed tests
+ request("delete", "/v1/suites/full_suite/definition", "", API_KEY);
+ wait_until([] { return false == check_for_path("/v1/suites/full_suite/definition"); });
+
+ // (1) Publish 'full_tree' suite
+
+ std::string suite_definition =
+ R"({"definition" : "suite full_suite\n family f\n task t\n label l \"value\"\n meter m 0 100 50\n event e\n endfamily\nendsuite\n# comment"})";
+ handle_response(request("post", "/v1/suites", suite_definition, API_KEY), HttpStatusCode::success_created);
+ wait_until([] { return check_for_path("/v1/suites/full_suite/definition"); });
+
+ // (2) Retrieve 'full_suite' suite tree, explicitly requesting generated variables
+ {
+ auto result = handle_response(request("get", "/v1/suites/full_suite/f/tree?content=full&gen_vars=true"));
+ auto content = ojson::parse(result.body);
+
+ BOOST_REQUIRE(content.contains("f"));
+ // Check family attributes
+ BOOST_REQUIRE(content["f"].contains("attributes"));
+ BOOST_REQUIRE(content["f"]["attributes"].size() == 2);
+ {
+ size_t count = 0;
+ for (const auto& attr : content["f"]["attributes"]) {
+ BOOST_REQUIRE(attr.contains("type"));
+ if (attr["type"] == "variable") {
+ BOOST_REQUIRE(attr.contains("name"));
+ BOOST_REQUIRE(attr.contains("value"));
+ if (attr.contains("generated")) {
+ BOOST_REQUIRE(attr["generated"] == true);
+ ++count;
+ }
+ }
+ }
+ BOOST_REQUIRE(count >= 2);
+ }
+ BOOST_REQUIRE(content["f"].contains("children"));
+ BOOST_REQUIRE(content["f"]["children"].contains("t"));
+ // Check task attributes
+ BOOST_REQUIRE(content["f"]["children"]["t"].contains("attributes"));
+ BOOST_REQUIRE(content["f"]["children"]["t"]["attributes"].size() == 11);
+ {
+ size_t count = 0;
+ for (const auto& attr : content["f"]["children"]["t"]["attributes"]) {
+ BOOST_REQUIRE(attr.contains("type"));
+ if (attr["type"] == "variable") {
+ BOOST_REQUIRE(attr.contains("name"));
+ BOOST_REQUIRE(attr.contains("value"));
+ if (attr.contains("generated")) {
+ BOOST_REQUIRE(attr["generated"] == true);
+ ++count;
+ }
+ }
+ }
+ BOOST_REQUIRE(count >= 2);
+ }
+ }
+
+ // (4) Clean up
+ request("delete", "/v1/suites/full_suite/definition", "", API_KEY);
+ wait_until([] { return false == check_for_path("/v1/suites/full_suite/definition"); });
+}
+
BOOST_AUTO_TEST_CASE(test_token_authentication) {
std::cout << "======== " << boost::unit_test::framework::current_test_case().p_name << " =========" << std::endl;
diff --git a/libs/service/src/ecflow/service/auth/Credentials.cpp b/libs/service/src/ecflow/service/auth/Credentials.cpp
index e1825b20b..20e0b442a 100644
--- a/libs/service/src/ecflow/service/auth/Credentials.cpp
+++ b/libs/service/src/ecflow/service/auth/Credentials.cpp
@@ -42,7 +42,9 @@ std::optional Credentials::user() const {
std::optional Credentials::key() const {
if (auto key = value("key"); key) {
- return KeyCredentials{std::move(*key)};
+ if (auto email = value("email"); email) {
+ return KeyCredentials{std::move(*email), std::move(*key)};
+ }
}
return std::nullopt;
}
diff --git a/libs/service/src/ecflow/service/auth/Credentials.hpp b/libs/service/src/ecflow/service/auth/Credentials.hpp
index bcb2132a2..a9fae62a5 100644
--- a/libs/service/src/ecflow/service/auth/Credentials.hpp
+++ b/libs/service/src/ecflow/service/auth/Credentials.hpp
@@ -27,6 +27,7 @@ class Credentials {
struct KeyCredentials
{
+ std::string email;
std::string key;
};
diff --git a/libs/service/src/ecflow/service/aviso/Aviso.cpp b/libs/service/src/ecflow/service/aviso/Aviso.cpp
index c0f487b1e..30c1b5d19 100644
--- a/libs/service/src/ecflow/service/aviso/Aviso.cpp
+++ b/libs/service/src/ecflow/service/aviso/Aviso.cpp
@@ -10,6 +10,15 @@
#include "ecflow/service/aviso/Aviso.hpp"
+#if defined(ECF_OPENSSL)
+ #include
+ #if OPENSSL_VERSION_NUMBER < 0x1010100fL
+ #warning OpenSSL versions prior to 1.1.1 detected. Aviso ETCD HTTP client will be build without OpenSSL support!
+ #else
+ #define CPPHTTPLIB_OPENSSL_SUPPORT
+ #endif
+#endif
+
#include
#include
#include
diff --git a/libs/service/src/ecflow/service/aviso/AvisoService.cpp b/libs/service/src/ecflow/service/aviso/AvisoService.cpp
index d6849504d..ad7099d3e 100644
--- a/libs/service/src/ecflow/service/aviso/AvisoService.cpp
+++ b/libs/service/src/ecflow/service/aviso/AvisoService.cpp
@@ -129,7 +129,9 @@ void AvisoService::register_listener(const AvisoSubscribe& listen) {
if (auto auth = listen.auth(); !auth.empty()) {
auto credentials = ecf::service::auth::Credentials::load(auth);
if (auto key_credentials = credentials.key(); key_credentials) {
- inserted.auth_token = key_credentials->key;
+ auto email = key_credentials->email;
+ auto key = key_credentials->key;
+ inserted.auth_token = email + ":" + key;
}
else {
SLOG(I, "AvisoService: no key found in auth token for listener {" << listener.path() << "}");
diff --git a/libs/service/src/ecflow/service/aviso/etcd/Client.cpp b/libs/service/src/ecflow/service/aviso/etcd/Client.cpp
index 130795aba..c054a4008 100644
--- a/libs/service/src/ecflow/service/aviso/etcd/Client.cpp
+++ b/libs/service/src/ecflow/service/aviso/etcd/Client.cpp
@@ -42,7 +42,7 @@ std::vector> Client::poll(std::string_view k
if (!auth_token_.empty()) {
SLOG(D, "EtcdClient: using authorization token");
- headers.emplace("Authorization", "Bearer " + auth_token_);
+ headers.emplace("Authorization", "EmailKey " + auth_token_);
}
auto range = Range(key_prefix);