added utils (Logged and Retry) and test_utils

Change-Id: Ia0e47636409a82cdd6f1e7af7b16b772d3ce16dd
diff --git a/test_utils.py b/test_utils.py
new file mode 100644
index 0000000..fadeab5
--- /dev/null
+++ b/test_utils.py
@@ -0,0 +1,31 @@
+#-*- coding: utf-8 -*-
+
+import unittest
+
+import utils
+
+
+logger = utils.logger.getChild('TestUtils')
+
+
+class TestUtils(unittest.TestCase):
+
+    def setUp(self):
+        self.gen10 = self.get_generator(10)
+
+    def get_generator(self, max):
+        for i in xrange(max):
+            yield i
+
+    def test_retry_3_from_5(self):
+        res = utils.Retry(timeout=1,
+                          attempts=5).wait_result(self.gen10.next, 3)
+        self.assertEqual(res, 3)
+
+    def test_retry_failed(self):
+        retryer = utils.Retry(timeout=1, attempts=3)
+        self.assertRaises(utils.ResultNotProduced,
+                          retryer.wait_result, self.gen10.next, 5)
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000..f92b6e6
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,104 @@
+#-*- coding: utf-8 -*-
+
+
+import logging
+import os
+import time
+
+
+logging.basicConfig()
+logger = logging.getLogger('safe_rsync')
+
+loglevel = os.environ.get('LOGLEVEL', 'INFO')
+if loglevel not in ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'):
+    logger.warn('LOGLEVEL environment variable has wrong value=={}. Using '
+                '"INFO" by default.'.format(loglevel))
+    loglevel = 'INFO'
+logger.setLevel(loglevel)
+
+
+def logged(logger=None):
+    if logger is None:
+        logger = globals().get('logger')
+
+    def wrap(f):
+        def wrapped_f(*args, **kwargs):
+            logger.debug('Starting {}({}, {}) (defaults: {})'
+                         ''.format(f.__name__,
+                                   str(args),
+                                   str(kwargs),
+                                   str(f.__defaults__))
+                         )
+            r = f(*args, **kwargs)
+            logger.debug('{} done with result "{}".'.
+                         format(f.__name__, str(r)))
+            return r
+        return wrapped_f
+    return wrap
+
+
+# retry decorator
+def retry(expected_status, timeout=None, attempts=None):
+
+    def wrap(f):
+        def wrapped_f(*args, **kwargs):
+            r = f(*args, **kwargs)
+            return r
+        return wrapped_f
+    return wrap
+
+
+class ResultNotProduced(Exception):
+    def __init__(self, value):
+        self.value = value
+
+    def __str__(self):
+        return repr(self.value)
+
+
+class Retry(object):
+    """
+    Waits while the function reaches the specified status.
+
+    :param function: function that returns some status
+    :param expected_status: status the machine should turn to
+    :param attempts: how many times to check status
+    :param timeout: timeout in seconds before attempts
+    :return: True if node moves to the specified status, False otherwise
+    :Examples:
+    Retry(timeout=3, attempts=10).wait(function, result, param1, param2)
+    Retry().wait_result(function, result, param1, param2)
+    """
+
+    def __init__(self, timeout=5, attempts=10):
+        self.timeout = timeout
+        self.attempts = attempts
+        self.logger = globals().get('logger').getChild('Retry')
+
+    def wait_result(self, function, expected_result, *args, **kwargs):
+
+        self.logger.debug('Wait for {}() == {}...'
+                          ''.format(function.__name__, str(expected_result)))
+
+        @logged(self.logger)
+        def f():
+            return function(*args, **kwargs)
+
+        attempt = 1
+        while attempt <= self.attempts:
+            try:
+                result = f()
+            except Exception as e:
+                self.logger.error('Exception on function {}: {}'
+                                  ''.format(function.__name__, str(e)))
+                raise
+            else:
+                if result == expected_result:
+                    self.logger.debug('Got on attempt #{}:'.format(attempt))
+                    return result
+                attempt += 1
+                time.sleep(self.timeout)
+
+        raise ResultNotProduced('Result "{}" was not produced during '
+                                '{} attempts.'
+                                ''.format(expected_result, attempt - 1))