From 61a81c2da47044c5e10b87c07a39dcceb479cd51 Mon Sep 17 00:00:00 2001
From: Shay Nehmad <shay.nehmad@guardicore.com>
Date: Wed, 2 Oct 2019 16:31:31 +0300
Subject: [PATCH] Created the report generation sync module and now using it
 exclusivly to create reports.

Almost all debug logs should probably be deleted once testing is done
---
 monkey/monkey_island/cc/resources/root.py     | 25 ++------
 .../cc/services/attack/attack_report.py       |  4 +-
 .../cc/services/reporting/report.py           | 17 ++---
 .../report_generation_synchronisation.py      | 63 +++++++++++++++++++
 4 files changed, 80 insertions(+), 29 deletions(-)
 create mode 100644 monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py

diff --git a/monkey/monkey_island/cc/resources/root.py b/monkey/monkey_island/cc/resources/root.py
index f2978f1ee..d7cae8bd7 100644
--- a/monkey/monkey_island/cc/resources/root.py
+++ b/monkey/monkey_island/cc/resources/root.py
@@ -10,6 +10,7 @@ from monkey_island.cc.database import mongo
 from monkey_island.cc.services.node import NodeService
 from monkey_island.cc.services.reporting.report import ReportService
 from monkey_island.cc.services.attack.attack_report import AttackReportService
+from monkey_island.cc.services.reporting.report_generation_synchronisation import is_report_being_generated, safe_generate_reports
 from monkey_island.cc.utils import local_ip_addresses
 from monkey_island.cc.services.database import Database
 
@@ -20,9 +21,6 @@ logger = logging.getLogger(__name__)
 
 class Root(flask_restful.Resource):
     def __init__(self):
-        # This lock will allow only one thread to generate a report at a time. Report generation can be quite
-        # slow if there is a lot of data, and the UI queries the Root service often; without the lock, these requests
-        # would accumulate, overload the server, eventually causing it to crash.
         self.report_generating_lock = threading.Event()
 
     def get(self, action=None):
@@ -62,8 +60,10 @@ class Root(flask_restful.Resource):
         infection_done = NodeService.is_monkey_finished_running()
 
         if infection_done:
-            if self.should_generate_report():
-                self.generate_report()
+            # Checking is_report_being_generated here, because we don't want to wait to generate a report; rather,
+            # we want to skip and reply.
+            if not is_report_being_generated() and not ReportService.is_latest_report_exists():
+                safe_generate_reports()
             report_done = ReportService.is_report_generated()
         else:  # Infection is not done
             report_done = False
@@ -73,18 +73,3 @@ class Root(flask_restful.Resource):
             run_monkey=is_any_exists,
             infection_done=infection_done,
             report_done=report_done)
-
-    def generate_report(self):
-        # Set the event when entering the critical section
-        self.report_generating_lock.set()
-        # Not using the return value, as the get_report function also saves the report in the DB for later.
-        _ = ReportService.get_report()
-        _ = AttackReportService.get_latest_report()
-        # Clear the event when leaving the critical section
-        self.report_generating_lock.clear()
-
-    def should_generate_report(self):
-        # If the lock is not set, that means no one is generating a report right now.
-        is_any_thread_generating_a_report_right_now = not self.report_generating_lock.is_set()
-        is_there_a_need_for_a_new_report = not ReportService.is_latest_report_exists()
-        return is_any_thread_generating_a_report_right_now and is_there_a_need_for_a_new_report
diff --git a/monkey/monkey_island/cc/services/attack/attack_report.py b/monkey/monkey_island/cc/services/attack/attack_report.py
index c04e6870f..c7457c2f6 100644
--- a/monkey/monkey_island/cc/services/attack/attack_report.py
+++ b/monkey/monkey_island/cc/services/attack/attack_report.py
@@ -6,6 +6,7 @@ from monkey_island.cc.services.attack.technique_reports import T1145, T1105, T10
 from monkey_island.cc.services.attack.technique_reports import T1090, T1041, T1222, T1005, T1018, T1016, T1021, T1064
 from monkey_island.cc.services.attack.attack_config import AttackConfig
 from monkey_island.cc.database import mongo
+from monkey_island.cc.services.reporting.report_generation_synchronisation import safe_generate_attack_report
 
 __author__ = "VakarisZ"
 
@@ -88,7 +89,8 @@ class AttackReportService:
             report_modifytime = latest_report['meta']['latest_monkey_modifytime']
             if monkey_modifytime and report_modifytime and monkey_modifytime == report_modifytime:
                 return latest_report
-        return AttackReportService.generate_new_report()
+
+        return safe_generate_attack_report()
 
     @staticmethod
     def is_report_generated():
diff --git a/monkey/monkey_island/cc/services/reporting/report.py b/monkey/monkey_island/cc/services/reporting/report.py
index f00fbc22c..1221200c4 100644
--- a/monkey/monkey_island/cc/services/reporting/report.py
+++ b/monkey/monkey_island/cc/services/reporting/report.py
@@ -1,25 +1,25 @@
-import itertools
 import functools
+import itertools
+import logging
+import time
 
 import ipaddress
-import logging
-
 from bson import json_util
 from enum import Enum
-
 from six import text_type
 
+from common.network.network_range import NetworkRange
 from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst
 from monkey_island.cc.database import mongo
 from monkey_island.cc.models import Monkey
-from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager
 from monkey_island.cc.services.config import ConfigService
 from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups
 from monkey_island.cc.services.edge import EdgeService
 from monkey_island.cc.services.node import NodeService
-from monkey_island.cc.utils import local_ip_addresses, get_subnets
 from monkey_island.cc.services.reporting.pth_report import PTHReportService
-from common.network.network_range import NetworkRange
+from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager
+from monkey_island.cc.services.reporting.report_generation_synchronisation import safe_generate_regular_report
+from monkey_island.cc.utils import local_ip_addresses, get_subnets
 
 __author__ = "itay.mizeretz"
 
@@ -692,6 +692,7 @@ class ReportService:
 
     @staticmethod
     def generate_report():
+        time.sleep(40)
         domain_issues = ReportService.get_domain_issues()
         issues = ReportService.get_issues()
         config_users = ReportService.get_config_users()
@@ -780,7 +781,7 @@ class ReportService:
     def get_report():
         if ReportService.is_latest_report_exists():
             return ReportService.decode_dot_char_before_mongo_insert(mongo.db.report.find_one())
-        return ReportService.generate_report()
+        return safe_generate_regular_report()
 
     @staticmethod
     def did_exploit_type_succeed(exploit_type):
diff --git a/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py b/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py
new file mode 100644
index 000000000..1fe4d8bb8
--- /dev/null
+++ b/monkey/monkey_island/cc/services/reporting/report_generation_synchronisation.py
@@ -0,0 +1,63 @@
+import logging
+import threading
+
+logger = logging.getLogger(__name__)
+
+# These are pseudo-singletons - global Locks. These locks will allow only one thread to generate a report at a time.
+# Report generation can be quite slow if there is a lot of data, and the UI queries the Root service often; without
+# the locks, these requests would accumulate, overload the server, eventually causing it to crash.
+logger.debug("Initializing report generation lock.")
+report_generating_lock = threading.Semaphore()
+__attack_report_generating_lock = threading.Semaphore()
+__regular_report_generating_lock = threading.Semaphore()
+
+
+def safe_generate_reports():
+    # Wait until report generation is available.
+    logger.debug("Waiting for report generation...")
+    # Entering the critical section.
+    report_generating_lock.acquire()
+    logger.debug("Report generation locked.")
+    report = safe_generate_regular_report()
+    attack_report = safe_generate_attack_report()
+    # Leaving the critical section.
+    report_generating_lock.release()
+    logger.debug("Report generation released.")
+    return report, attack_report
+
+
+def safe_generate_regular_report():
+    # Local import to avoid circular imports
+    from monkey_island.cc.services.reporting.report import ReportService
+    logger.debug("Waiting for regular report generation...")
+    __regular_report_generating_lock.acquire()
+    logger.debug("Regular report generation locked.")
+    report = ReportService.generate_report()
+    __regular_report_generating_lock.release()
+    logger.debug("Regular report generation released.")
+    return report
+
+
+def safe_generate_attack_report():
+    # Local import to avoid circular imports
+    from monkey_island.cc.services.attack.attack_report import AttackReportService
+    logger.debug("Waiting for attack report generation...")
+    __attack_report_generating_lock.acquire()
+    logger.debug("Attack report generation locked.")
+    attack_report = AttackReportService.generate_new_report()
+    __attack_report_generating_lock.release()
+    logger.debug("Attack report generation released.")
+    return attack_report
+
+
+def is_report_being_generated():
+    # From https://docs.python.org/2/library/threading.html#threading.Semaphore.acquire:
+    # When invoked with blocking set to false, do not block.
+    #   If a call without an argument would block, return false immediately;
+    #   otherwise, do the same thing as when called without arguments, and return true.
+    is_report_being_generated_right_now = not report_generating_lock.acquire(blocking=False)
+    logger.debug("is_report_being_generated_right_now == " + str(is_report_being_generated_right_now))
+    if not is_report_being_generated_right_now:
+        # We're not using the critical resource; we just checked its state.
+        report_generating_lock.release()
+    return is_report_being_generated_right_now