Repository: privacyidea/privacyidea

Issue:
Cron runner module: EventCounter
This is a cron runner module based on #992

It is used to save state snapshots to the database #1027 from the ``EventCounter`` table.

An option of this cron runner module could be that the value in the ``EventCounter`` is reset to 0 after read.


Discussion:

Resolving patch:
diff --git a/privacyidea/lib/counter.py b/privacyidea/lib/counter.py
--- a/privacyidea/lib/counter.py
+++ b/privacyidea/lib/counter.py
@@ -72,3 +72,18 @@ def reset(counter_name):
         counter = EventCounter(counter_name, 0)
         counter.save()
     counter.reset()
+
+
+def read(counter_name):
+    """
+    Read the counter value from the database.
+    If the counter_name does not exist, 'None' is returned.
+
+    :param counter_name: The name of the counter
+    :return: The value of the counter
+    """
+    counter = EventCounter.query.filter_by(counter_name=counter_name).first()
+    if not counter:
+        return None
+    else:
+        return counter.counter_value
\ No newline at end of file
diff --git a/privacyidea/lib/periodictask.py b/privacyidea/lib/periodictask.py
--- a/privacyidea/lib/periodictask.py
+++ b/privacyidea/lib/periodictask.py
@@ -28,11 +28,12 @@
 
 from privacyidea.lib.error import ServerError, ParameterError
 from privacyidea.lib.task.hello import HelloTask
+from privacyidea.lib.task.eventcounter import EventCounterTask
 from privacyidea.models import PeriodicTask
 
 log = logging.getLogger(__name__)
 
-TASK_CLASSES = [HelloTask]
+TASK_CLASSES = [EventCounterTask, HelloTask]
 #: TASK_MODULES maps task module identifiers to subclasses of BaseTask
 TASK_MODULES = dict((cls.identifier, cls) for cls in TASK_CLASSES)
 
diff --git a/privacyidea/lib/task/eventcounter.py b/privacyidea/lib/task/eventcounter.py
new file mode 100644
--- /dev/null
+++ b/privacyidea/lib/task/eventcounter.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+#  2018-08-01 Cornelius Kölbel <cornelius.koelbel@netknights.it>
+#             EventCounter Task
+#
+# This code is free software; you can redistribute it and/or
+# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+# License as published by the Free Software Foundation; either
+# version 3 of the License, or any later version.
+#
+# This code is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU AFFERO GENERAL PUBLIC LICENSE for more details.
+#
+# You should have received a copy of the GNU Affero General Public
+# License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+#
+import logging
+from privacyidea.lib.task.base import BaseTask
+from privacyidea.lib.monitoringstats import write_stats
+from privacyidea.lib.counter import reset, read
+from privacyidea.lib.utils import is_true
+
+
+__doc__ = """This task module reads event counters and writes them to the 
+MonitoringStats database table."""
+
+log = logging.getLogger(__name__)
+
+
+class EventCounterTask(BaseTask):
+    identifier = "EventCounter"
+    description = "Write snapshots of the event counter to the MonitoringStats table."
+
+    @property
+    def options(self):
+        return {
+            "event_counter": {
+                "type": "str",
+                "description": "The name of the event counter to read.",
+                "required": True
+            },
+            "stats_key": {
+                "type": "str",
+                "description": "The name of the stats key to write to the MonitoringStats table.",
+                "required": True
+            },
+            "reset_event_counter": {
+                "type": "bool",
+                "description": "Whether to reset the event_counter, if it is read and written to the "
+                               "MonitoringStats table."
+            }
+
+        }
+
+    def do(self, params):
+        event_counter = params.get("event_counter")
+        stats_key = params.get("stats_key")
+        reset_event_counter = params.get("reset_event_counter")
+
+        counter_value = read(event_counter)
+        if is_true(reset_event_counter) and counter_value:
+            reset(event_counter)
+
+        # now write the current value of the counter
+        if counter_value is None:
+            log.warning(u"Trying to create statistics of a counter_value '{0}', "
+                        u"that does not exist.".format(event_counter))
+        else:
+            write_stats(stats_key, counter_value)
+
+        return True
+


Test patch:
diff --git a/tests/test_lib_counter.py b/tests/test_lib_counter.py
--- a/tests/test_lib_counter.py
+++ b/tests/test_lib_counter.py
@@ -4,7 +4,7 @@
 """
 
 from .base import MyTestCase
-from privacyidea.lib.counter import increase, decrease, reset
+from privacyidea.lib.counter import increase, decrease, reset, read
 from privacyidea.models import EventCounter
 
 
@@ -51,6 +51,12 @@ def test_01_increase_and_decrease(self):
         counter = EventCounter.query.filter_by(counter_name="hallo_counter1").first()
         self.assertEqual(counter.counter_value, 0)
 
+        # Test reading counter
+        r = read("hallo_counter1")
+        self.assertEqual(r, 0)
+        r = read("unknown counter")
+        self.assertEqual(r, None)
+
     def test_02_decrease_beyond_zero(self):
         r = increase("hallo_counter2")
         self.assertEqual(r, 1)
diff --git a/tests/test_lib_task_eventcounter.py b/tests/test_lib_task_eventcounter.py
new file mode 100644
--- /dev/null
+++ b/tests/test_lib_task_eventcounter.py
@@ -0,0 +1,58 @@
+"""
+This tests the files
+  lib/task/eventcounter.py
+"""
+
+from .base import MyTestCase
+from privacyidea.lib.counter import increase, read
+from privacyidea.lib.monitoringstats import get_values
+
+from privacyidea.lib.task.eventcounter import EventCounterTask
+
+
+class TaskEventCounterTestCase(MyTestCase):
+
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def test_00_read_eventcounter_to_monitoringstats(self):
+        r = increase("counter1")
+        self.assertEqual(r, 1)
+
+        r = increase("counter2")
+        self.assertEqual(r, 1)
+
+        r = increase("counter1")
+        self.assertEqual(r, 2)
+
+        r = read("counter1")
+        self.assertEqual(r, 2)
+
+        et = EventCounterTask()
+        params = {"event_counter": "counter1",
+                  "stats_key": "C1",
+                  "reset_event_counter": "True"}
+
+        # Now we execute the task
+        et.do(params)
+
+        # The counter "counter1" should be resetted
+        self.assertEqual(read("counter1"), 0)
+
+        # The value "2" should be written to the statistics key C1.
+        stats = get_values("C1")
+        self.assertTrue(len(stats), 1)
+        self.assertTrue(stats[0][1], 2)
+
+        # Now we increase the event counter again
+        increase("counter1")
+
+        # ..and run the event counter task
+        et.do(params)
+        stats = get_values("C1")
+        self.assertTrue(len(stats), 2)
+        self.assertTrue(stats[0][1], 2)
+        self.assertTrue(stats[0][1], 1)