Repository: privacyidea/privacyidea

Issue:
Obtain list of available privacyIDEA nodes in a redundant setup
We have designed the task runner (#992) such that each periodic task holds a list of nodes on which the task should be run. This is important in a redundant setup because e.g. a token janitor job must be run on *one* of the nodes only.

Each privacyIDEA node knows its name from its pi.cfg, but there is currently no way to know all available nodes. However, this would be nice to have: Then, we could provide a list of nodes in the UI from the administrator could choose.

from #1116:
> I am not sure, if we should save the available nodes in the database - so that all PI nodes have the same node list.
> Or if a PI node should somehow "register" in such database table, so that the PI node can take the information
> * what node am I
> * what nodes are available
> from the database or from pi.cfg. Should a node be renamable?





Discussion:
# Registering

A node should at some point write his name to a database table.
This way all nodes automatically know the names of the other nodes.
What happens if a node is decomissionen? We would need a way to remove a node from the database.
Or leave the admin to do so manually. 

Other problem: A node does not know, if it has already registered, unless it looks into the database.
Thus we continously have this registering process. Or would we only start the registering process after an admin login? ``/auth``.

# Nodes in pi.cfg

If we put somehting like this in pi.cfg:

    PI_NODES = ["node1", "node2", "node3"]

this would have to be synced to the redundant machine.
Maybe this is the lower hanging fruit although it is not that elegant.

# Implementation

We should have two lib/utils.py functions like ``get_privacyidea_nodes`` which returns the list of all nodes and ``get_privacyidea_node``, which returns the current node.
Currently we have the ``PI_AUDIT_SERVERNAME`` which could overwrite the hostname in the audit log.

We could use this pi.cfg config variable or be more flexible and add a new one?

    PI_NODE = "node1"
    PI_NODES = ["node1", "node2", "node3"]
I think having a config variable would be OK for now (though it's not so elegant).

Hmm, since the periodic tasks are unrelated to the audit log, I guess having a new ``PI_NODE`` variable would make sense? But wouldn't one usually want ``PI_AUDIT_SERVERNAME`` and ``PI_NODE`` to be the same value?

Resolving patch:
diff --git a/privacyidea/config.py b/privacyidea/config.py
--- a/privacyidea/config.py
+++ b/privacyidea/config.py
@@ -48,6 +48,8 @@ class TestingConfig(Config):
     PI_GNUPG_HOME = "tests/testdata/gpg"
     CACHE_TYPE = "None"
     PI_SCRIPT_HANDLER_DIRECTORY = "tests/testdata/scripts/"
+    PI_NODE = "Node1"
+    PI_NODES = ["Node2"]
 
 
 class ProductionConfig(Config):
diff --git a/privacyidea/lib/config.py b/privacyidea/lib/config.py
--- a/privacyidea/lib/config.py
+++ b/privacyidea/lib/config.py
@@ -861,3 +861,28 @@ def return_saml_attributes_on_fail():
     r = get_from_config(key="ReturnSamlAttributesOnFail", default=False,
                         return_bool=True)
     return r
+
+
+def get_privacyidea_node():
+    """
+    This returns the node name of the privacyIDEA node as found in the pi.cfg
+    file in PI_NODE.
+    If it does not exist, the PI_AUDIT_SERVERNAME is used.
+    :return: the destinct node name
+    """
+    node_name = current_app.config.get("PI_NODE",
+                                       current_app.config.get("PI_AUDIT_SERVERNAME",
+                                                              "localnode"))
+    return node_name
+
+
+def get_privacyidea_nodes():
+    """
+    This returns the list of the nodes, including the own local node name
+    :return: list of nodes
+    """
+    own_node_name = get_privacyidea_node()
+    nodes = current_app.config.get("PI_NODES", [])[:]
+    if own_node_name not in nodes:
+        nodes.append(own_node_name)
+    return nodes
\ No newline at end of file


Test patch:
diff --git a/tests/test_lib_config.py b/tests/test_lib_config.py
--- a/tests/test_lib_config.py
+++ b/tests/test_lib_config.py
@@ -18,7 +18,8 @@
                                     get_token_class_dict,
                                     get_token_types,
                                     get_token_classes, get_token_prefix,
-                                    get_machine_resolver_class_dict
+                                    get_machine_resolver_class_dict,
+                                    get_privacyidea_node, get_privacyidea_nodes
                                     )
 from privacyidea.lib.resolvers.PasswdIdResolver import IdResolver as PWResolver
 from privacyidea.lib.tokens.hotptoken import HotpTokenClass
@@ -228,3 +229,10 @@ def test_06_public_and_admin(self):
         self.assertTrue("secretInfo1" not in a)
         a = get_from_config("secretInfo1", role="public")
         self.assertEqual(a, None)
+
+    def test_07_node_names(self):
+        node = get_privacyidea_node()
+        self.assertEqual(node, "Node1")
+        nodes = get_privacyidea_nodes()
+        self.assertTrue("Node1" in nodes)
+        self.assertTrue("Node2" in nodes)
\ No newline at end of file