2.3.0 policy-handler - periodically catch_up

- periodically catchup - interval is configurable
     = max_skips defines the number of times the catch_up
        message that is identical to previous one can be skipped
- do not catchup more often than the interval
    even between the manual catchup and auto catchup
- do not send the same catchup message twice in a row
   to the deployment-handler but not exceed a hard limit
   on catchup max_skips
- catchup if the deployment-handler instance is changed

Change-Id: I9a3fcc941e8a9e553abb3952dd882c37e0f5fdfe
Signed-off-by: Alex Shatov <alexs@att.com>
Issue-ID: DCAEGEN2-389
diff --git a/policyhandler/policy_utils.py b/policyhandler/policy_utils.py
index 652f98b..69978b6 100644
--- a/policyhandler/policy_utils.py
+++ b/policyhandler/policy_utils.py
@@ -18,11 +18,12 @@
 
 """policy-client communicates with policy-engine thru REST API"""
 
-import logging
 import json
+import logging
 import re
 
-from .policy_consts import POLICY_ID, POLICY_VERSION, POLICY_NAME, POLICY_BODY, POLICY_CONFIG
+from .policy_consts import (POLICY_BODY, POLICY_CONFIG, POLICY_ID, POLICY_NAME,
+                            POLICY_VERSION)
 
 class PolicyUtils(object):
     """policy-client utils"""
@@ -30,17 +31,6 @@
     _policy_name_ext = re.compile('[.][0-9]+[.][a-zA-Z]+$')
 
     @staticmethod
-    def safe_json_parse(json_str):
-        """try parsing json without exception - returns the json_str back if fails"""
-        if not json_str:
-            return json_str
-        try:
-            return json.loads(json_str)
-        except (ValueError, TypeError) as err:
-            PolicyUtils._logger.warn("unexpected json %s: %s", str(json_str), str(err))
-        return json_str
-
-    @staticmethod
     def extract_policy_id(policy_name):
         """ policy_name  = policy_id + "." + <version> + "." + <extension>
         For instance,
@@ -65,7 +55,7 @@
             return policy
         config = policy.get(POLICY_BODY, {}).get(POLICY_CONFIG)
         if config:
-            policy[POLICY_BODY][POLICY_CONFIG] = PolicyUtils.safe_json_parse(config)
+            policy[POLICY_BODY][POLICY_CONFIG] = Utils.safe_json_parse(config)
         return policy
 
     @staticmethod
@@ -131,3 +121,54 @@
             policies[policy_id] = PolicyUtils.parse_policy_config(policies[policy_id])
 
         return policies
+
+class Utils(object):
+    """general purpose utils"""
+    _logger = logging.getLogger("policy_handler.utils")
+
+    @staticmethod
+    def safe_json_parse(json_str):
+        """try parsing json without exception - returns the json_str back if fails"""
+        if not json_str:
+            return json_str
+        try:
+            return json.loads(json_str)
+        except (ValueError, TypeError) as err:
+            Utils._logger.warn("unexpected json %s: %s", str(json_str), str(err))
+        return json_str
+
+    @staticmethod
+    def are_the_same(body_1, body_2):
+        """check whether both objects are the same"""
+        if (body_1 and not body_2) or (not body_1 and body_2):
+            Utils._logger.debug("only one is empty %s != %s", body_1, body_2)
+            return False
+
+        if body_1 is None and body_2 is None:
+            return True
+
+        if isinstance(body_1, list) and isinstance(body_2, list):
+            if len(body_1) != len(body_2):
+                Utils._logger.debug("len %s != %s", json.dumps(body_1), json.dumps(body_2))
+                return False
+
+            for val_1, val_2 in zip(body_1, body_2):
+                if not Utils.are_the_same(val_1, val_2):
+                    return False
+            return True
+
+        if isinstance(body_1, dict) and isinstance(body_2, dict):
+            if body_1.viewkeys() ^ body_2.viewkeys():
+                Utils._logger.debug("keys %s != %s", json.dumps(body_1), json.dumps(body_2))
+                return False
+
+            for key, val_1 in body_1.iteritems():
+                if not Utils.are_the_same(val_1, body_2[key]):
+                    return False
+            return True
+
+        # ... here when primitive values or mismatched types ...
+        the_same_values = (body_1 == body_2)
+        if not the_same_values:
+            Utils._logger.debug("values %s != %s", body_1, body_2)
+        return the_same_values