Browse Source

More complete testing of the os plugins/utils (#24)

The change implements several fixes so that monitor stack can be run in
OpenStack environments supporting both V2 and V3 authentication.

All of the plugins now have a proper name lookup and will pull from a local
cache instead of hammering the API on every request.

The Local caching functionlaity used to be tied to the python shelve
module. Due to issues with Python 3.5 and shelves the library was
replaced with the diskcache lib which implements a caching interface
supporting both py2.7+.

Tests have been added in support of all additions.

Tests have been added for the os_utils module which should bring it
within ~98% of completeness.

Signed-off-by: Kevin Carter <kevin.carter@rackspace.com>
Kevin Carter 2 years ago
parent
commit
775f034a78

+ 2
- 0
.gitignore View File

@@ -44,6 +44,8 @@ nosetests.xml
44 44
 coverage.xml
45 45
 *,cover
46 46
 .hypothesis/
47
+AUTHORS
48
+ChangeLog
47 49
 
48 50
 # Translations
49 51
 *.mo

+ 14
- 14
etc/openstack.ini View File

@@ -4,32 +4,32 @@
4 4
 #  override whatever is needed within the local sections.
5 5
 
6 6
 [DEFAULT]
7
-insecure = false
8
-auth_url = https://example.com:5000/v3
7
+# The verify option is for SSL. If your SSL certificate is not
8
+#  valid set this option to false else omit it or set it true.
9
+insecure = true
10
+
11
+auth_url = https://127.0.0.1:5000/v3
9 12
 
13
+username = admin
14
+password = Secrete
15
+
16
+[keystone]
10 17
 # NOTE(cloudnull):
11 18
 #  When using keystone V3 you will need the .*domain_name configuration options.
12 19
 user_domain_name = default  # This is required when Keystone V3 is being used
13 20
 project_domain_name = default  # This is required when Keystone V3 is being used
14 21
 
22
+[glance]
23
+# NOTE(cloudnull):
15 24
 #  If you're using keystone V2 you will need the tenant_name option.
16 25
 tenant_name = admin  # This is required when Keystone V2 is being used
26
+project_name = admin  # This is required when Keystone V2 is being used
17 27
 
18
-# NEVER Mix and match the options tenant name and domain_name options.
28
+# NEVER Mix and match the options tenant name and domain_name options withiin the same section.
19 29
 #  You are be required to run either V2 or V3 as it pertains to this config.
20 30
 #  If you provide both tenant_name and .*domain_name options at the same time
21 31
 #  the plugins will fail API version negotiation.
22 32
 
23
-username = admin
24
-password = Secrete
25
-# The verify option is for SSL. If your SSL certificate is not
26
-#  valid set this option to false else omit it or set it true.
27
-verify = false
28
-
29
-[keystone]
30
-
31
-[glance]
32
-
33 33
 [nova]
34 34
 project_name = nova
35 35
 
@@ -40,7 +40,7 @@ project_name = nova
40 40
 [cinder]
41 41
 
42 42
 [ironic]
43
-auth_url = https://example2.com:5000/v3
43
+auth_url = https://127.0.1.1:5000/v3
44 44
 project_name = ironic
45 45
 user_domain_name = users
46 46
 project_domain_name = projects

+ 7
- 3
monitorstack/plugins/kvm.py View File

@@ -18,14 +18,15 @@ import socket
18 18
 
19 19
 import click
20 20
 
21
+from monitorstack import utils
21 22
 from monitorstack.cli import pass_context
22 23
 
23 24
 
24 25
 DOC = """Get metrics from a KVM hypervisor."""
25
-COMMAND = 'kvm'
26
+COMMAND_NAME = 'kvm'
26 27
 
27 28
 
28
-@click.command(COMMAND, short_help=DOC.split('\n')[0])
29
+@click.command(COMMAND_NAME, short_help=DOC.split('\n')[0])
29 30
 @pass_context
30 31
 def cli(ctx):
31 32
     """Get metrics from a KVM hypervisor."""
@@ -60,7 +61,10 @@ def cli(ctx):
60 61
 
61 62
     except Exception as exp:
62 63
         output['exit_code'] = 1
63
-        output['message'] = 'kvm failed -- Error: {}'.format(exp)
64
+        output['message'] = '{} failed -- {}'.format(
65
+            COMMAND_NAME,
66
+            utils.log_exception(exp=exp)
67
+        )
64 68
     else:
65 69
         output['exit_code'] = 0
66 70
         output['message'] = 'kvm is ok'

+ 4
- 1
monitorstack/plugins/os_vm_quota_cores.py View File

@@ -53,7 +53,10 @@ def cli(ctx, config_file):
53 53
             variables[project.name] = int(limits['quota_set']['cores'])
54 54
     except Exception as exp:
55 55
         output['exit_code'] = 1
56
-        output['message'] = '{} failed -- Error: {}'.format(COMMAND_NAME, exp)
56
+        output['message'] = '{} failed -- {}'.format(
57
+            COMMAND_NAME,
58
+            utils.log_exception(exp=exp)
59
+        )
57 60
     else:
58 61
         output['exit_code'] = 0
59 62
         output['message'] = '{} is ok'.format(COMMAND_NAME)

+ 4
- 1
monitorstack/plugins/os_vm_quota_instance.py View File

@@ -53,7 +53,10 @@ def cli(ctx, config_file):
53 53
             variables[project.name] = int(limits['quota_set']['instances'])
54 54
     except Exception as exp:
55 55
         output['exit_code'] = 1
56
-        output['message'] = '{} failed -- Error: {}'.format(COMMAND_NAME, exp)
56
+        output['message'] = '{} failed -- {}'.format(
57
+            COMMAND_NAME,
58
+            utils.log_exception(exp=exp)
59
+        )
57 60
     else:
58 61
         output['exit_code'] = 0
59 62
         output['message'] = '{} is ok'.format(COMMAND_NAME)

+ 4
- 1
monitorstack/plugins/os_vm_quota_ram.py View File

@@ -53,7 +53,10 @@ def cli(ctx, config_file):
53 53
             variables[project.name] = int(limits['quota_set']['ram'])
54 54
     except Exception as exp:
55 55
         output['exit_code'] = 1
56
-        output['message'] = '{} failed -- Error: {}'.format(COMMAND_NAME, exp)
56
+        output['message'] = '{} failed -- {}'.format(
57
+            COMMAND_NAME,
58
+            utils.log_exception(exp=exp)
59
+        )
57 60
     else:
58 61
         output['exit_code'] = 0
59 62
         output['message'] = '{} is ok'.format(COMMAND_NAME)

+ 10
- 4
monitorstack/plugins/os_vm_used_cores.py View File

@@ -51,13 +51,19 @@ def cli(ctx, config_file):
51 51
         variables = output['variables']
52 52
         for used in _ost.get_consumer_usage():
53 53
             flavor = flavors[used['flavor']['id']]
54
-            used_collection[used['name']] += int(flavor['vcpus'])
55
-            output['meta'][used['flavor']['id']] = True
56
-            output['meta'][used['flavor']['name']] = True
54
+            project_name = _ost.get_project_name(project_id=used['project_id'])
55
+            used_collection[project_name] += int(flavor['vcpus'])
56
+            flavor_id = used['flavor']['id']
57
+            output['meta'][flavor_id] = True
58
+            flavor_name = _ost.get_flavor_name(flavor_id=flavor_id)
59
+            output['meta'][flavor_name] = True
57 60
         variables.update(used_collection)
58 61
     except Exception as exp:
59 62
         output['exit_code'] = 1
60
-        output['message'] = '{} failed -- Error: {}'.format(COMMAND_NAME, exp)
63
+        output['message'] = '{} failed -- {}'.format(
64
+            COMMAND_NAME,
65
+            utils.log_exception(exp=exp)
66
+        )
61 67
     else:
62 68
         output['exit_code'] = 0
63 69
         output['message'] = '{} is ok'.format(COMMAND_NAME)

+ 10
- 4
monitorstack/plugins/os_vm_used_disk.py View File

@@ -51,13 +51,19 @@ def cli(ctx, config_file):
51 51
         variables = output['variables']
52 52
         for used in _ost.get_consumer_usage():
53 53
             flavor = flavors[used['flavor']['id']]
54
-            used_collection[used['name']] += int(flavor['disk'])
55
-            output['meta'][used['flavor']['id']] = True
56
-            output['meta'][used['flavor']['name']] = True
54
+            project_name = _ost.get_project_name(project_id=used['project_id'])
55
+            used_collection[project_name] += int(flavor['disk'])
56
+            flavor_id = used['flavor']['id']
57
+            output['meta'][flavor_id] = True
58
+            flavor_name = _ost.get_flavor_name(flavor_id=flavor_id)
59
+            output['meta'][flavor_name] = True
57 60
         variables.update(used_collection)
58 61
     except Exception as exp:
59 62
         output['exit_code'] = 1
60
-        output['message'] = '{} failed -- Error: {}'.format(COMMAND_NAME, exp)
63
+        output['message'] = '{} failed -- {}'.format(
64
+            COMMAND_NAME,
65
+            utils.log_exception(exp=exp)
66
+        )
61 67
     else:
62 68
         output['exit_code'] = 0
63 69
         output['message'] = '{} is ok'.format(COMMAND_NAME)

+ 10
- 2
monitorstack/plugins/os_vm_used_instance.py View File

@@ -49,11 +49,19 @@ def cli(ctx, config_file):
49 49
     try:
50 50
         variables = output['variables']
51 51
         for used in _ost.get_consumer_usage():
52
-            used_collection[used['name']] += 1
52
+            project_name = _ost.get_project_name(project_id=used['project_id'])
53
+            used_collection[project_name] += 1
54
+            flavor_id = used['flavor']['id']
55
+            output['meta'][flavor_id] = True
56
+            flavor_name = _ost.get_flavor_name(flavor_id=flavor_id)
57
+            output['meta'][flavor_name] = True
53 58
         variables.update(used_collection)
54 59
     except Exception as exp:
55 60
         output['exit_code'] = 1
56
-        output['message'] = '{} failed -- Error: {}'.format(COMMAND_NAME, exp)
61
+        output['message'] = '{} failed -- {}'.format(
62
+            COMMAND_NAME,
63
+            utils.log_exception(exp=exp)
64
+        )
57 65
     else:
58 66
         output['exit_code'] = 0
59 67
         output['message'] = '{} is ok'.format(COMMAND_NAME)

+ 10
- 4
monitorstack/plugins/os_vm_used_ram.py View File

@@ -51,13 +51,19 @@ def cli(ctx, config_file):
51 51
         variables = output['variables']
52 52
         for used in _ost.get_consumer_usage():
53 53
             flavor = flavors[used['flavor']['id']]
54
-            used_collection[used['name']] += int(flavor['ram'])
55
-            output['meta'][used['flavor']['id']] = True
56
-            output['meta'][used['flavor']['name']] = True
54
+            project_name = _ost.get_project_name(project_id=used['project_id'])
55
+            used_collection[project_name] += int(flavor['ram'])
56
+            flavor_id = used['flavor']['id']
57
+            output['meta'][flavor_id] = True
58
+            flavor_name = _ost.get_flavor_name(flavor_id=flavor_id)
59
+            output['meta'][flavor_name] = True
57 60
         variables.update(used_collection)
58 61
     except Exception as exp:
59 62
         output['exit_code'] = 1
60
-        output['message'] = '{} failed -- Error: {}'.format(COMMAND_NAME, exp)
63
+        output['message'] = '{} failed -- {}'.format(
64
+            COMMAND_NAME,
65
+            utils.log_exception(exp=exp)
66
+        )
61 67
     else:
62 68
         output['exit_code'] = 0
63 69
         output['message'] = '{} is ok'.format(COMMAND_NAME)

+ 107
- 30
monitorstack/utils/__init__.py View File

@@ -13,12 +13,12 @@
13 13
 # limitations under the License.
14 14
 """Common code for utils."""
15 15
 
16
+import functools
16 17
 import os
17
-import shelve
18 18
 import sys
19
-import tempfile
19
+import time
20
+import traceback
20 21
 
21
-# Lower import to support conditional configuration parser
22 22
 try:
23 23
     if sys.version_info > (3, 2, 0):
24 24
         import configparser as ConfigParser
@@ -27,9 +27,50 @@ try:
27 27
 except ImportError:
28 28
         raise SystemExit('No configparser module was found.')
29 29
 
30
+import diskcache
31
+
32
+
33
+def retry(ExceptionToCheck, tries=3, delay=1, backoff=1):  # noqa
34
+    """Retry calling the decorated function using an exponential backoff.
35
+
36
+    Attributes to sources of inspiration:
37
+      http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/
38
+      http://wiki.python.org/moin/PythonDecoratorLibrary#Retry
39
+
40
+    :param ExceptionToCheck: the exception to check. may be a tuple of
41
+                             exceptions to check
42
+    :type ExceptionToCheck: Exception or tuple
43
+    :param tries: number of times to try (not retry) before giving up
44
+    :type tries: int
45
+    :param delay: initial delay between retries in seconds
46
+    :type delay: int
47
+    :param backoff: backoff multiplier e.g. value of 2 will double the delay
48
+                    each retry
49
+    :type backoff: int
50
+    """
51
+    def deco_retry(f):
52
+        @functools.wraps(f)
53
+        def f_retry(*args, **kwargs):
54
+            mtries, mdelay = tries, delay
55
+            while mtries > 1:
56
+                try:
57
+                    return f(*args, **kwargs)
58
+                except ExceptionToCheck:
59
+                    time.sleep(mdelay)
60
+                    mtries -= 1
61
+                    mdelay *= backoff
62
+            return f(*args, **kwargs)
63
+        return f_retry  # true decorator
64
+    return deco_retry
65
+
30 66
 
31 67
 def is_int(value):
32
-    """Check if a variable is an integer."""
68
+    """Check if a variable is an integer.
69
+
70
+    :param value: parameter to evaluate and return
71
+    :type value: str || int || float
72
+    :returns: str || int || float
73
+    """
33 74
     for v_type in [int, float]:
34 75
         try:
35 76
             value = v_type(value)
@@ -42,43 +83,68 @@ def is_int(value):
42 83
 
43 84
 
44 85
 class LocalCache(object):
45
-    """Context Manager for opening and closing access to the DBM."""
86
+    """Context Manager for opening and closing access to the cache objects."""
46 87
 
47
-    def __init__(self):
48
-        """Initialization method for class."""
49
-        """Set the Path to the DBM to create/Open."""
88
+    def __init__(self, cache_path=None):
89
+        """Set the Path cache object.
50 90
 
51
-        self.db_cache = os.path.join(
52
-            tempfile.gettempdir(),
53
-            'monitorstack.openstack.dbm'
54
-        )
91
+        :param cache_file: File path to store cache
92
+        :type cache_file: str
93
+        """
94
+        # If a cache file is provided use it otherwise store one in
95
+        #  the user home folder as a hidden folder.
96
+        self.cache_path = cache_path
97
+        if not self.cache_path:
98
+            self.cache_path = os.path.join(
99
+                os.path.expanduser('~'),
100
+                '.monitorstack.cache'
101
+            )
102
+        elif not self.cache_path.endswith('cache'):
103
+            self.cache_path = '{}.cache'.format(self.cache_path)
104
+
105
+        if not os.path.isdir(self.cache_path):
106
+            os.makedirs(self.cache_path)
55 107
 
56 108
     def __enter__(self):
57
-        """Open the DBM in r/w mode.
109
+        """Open the cache object.
58 110
 
59
-        :return: Open DBM
111
+        :returns: object
60 112
         """
61
-        return self.open_shelve
62
-
63
-    def __exit__(self, type, value, traceback):
64
-        """Close DBM Connection."""
65
-        self.close_shelve()
113
+        return self.open_cache
66 114
 
67
-    def _open_shelve(self):
68
-        return shelve.open(self.db_cache)
115
+    def __exit__(self, *args, **kwargs):
116
+        """Close cache object."""
117
+        self.lc_close()
69 118
 
70 119
     @property
71
-    def open_shelve(self):
72
-        """Open shelved data."""
73
-        return self._open_shelve()
120
+    @retry(ExceptionToCheck=Exception)
121
+    def open_cache(self):
122
+        """Return open caching opbject.
123
+
124
+        :returns: object
125
+        """
126
+        return diskcache.Cache(directory=self.cache_path)
127
+
128
+    def lc_open(self):
129
+        """Open shelved data.
74 130
 
75
-    def close_shelve(self):
131
+        :param cache_file: File path to store cache
132
+        :type cache_file: str
133
+        :returns: object
134
+        """
135
+        return self.open_cache
136
+
137
+    def lc_close(self):
76 138
         """Close shelved data."""
77
-        self.open_shelve.close()
139
+        self.open_cache.close()
78 140
 
79 141
 
80 142
 def read_config(config_file):
81
-    """Read an OpenStack configuration."""
143
+    """Read an OpenStack configuration.
144
+
145
+    :param config_file: path to configuration file.
146
+    :type config_file: str
147
+    """
82 148
     cfg = os.path.abspath(os.path.expanduser(config_file))
83 149
     if not os.path.isfile(cfg):
84 150
         raise IOError('Config file "{}" was not found'.format(cfg))
@@ -89,11 +155,22 @@ def read_config(config_file):
89 155
     args = dict()
90 156
     defaults = dict([(k, v) for k, v in parser.items(section='DEFAULT')])
91 157
     for section in parser.sections():
92
-        if section == 'DEFAULT':
93
-            continue
94
-
95 158
         sec = args[section] = defaults
96 159
         for key, value in parser.items(section):
97 160
             sec[key] = is_int(value=value)
98 161
 
99 162
     return args
163
+
164
+
165
+def log_exception(exp):
166
+    """Return log entries.
167
+
168
+    :param exp: Exception object or name.
169
+    :type exp: str || object
170
+    :return: str
171
+    """
172
+    _trace = [i.strip() for i in str(traceback.format_exc()).splitlines()]
173
+    trace = ' -> '.join(_trace)
174
+    _exception = [i.strip() for i in str(exp).splitlines()]
175
+    exception = ' -> '.join(_exception)
176
+    return 'Exception [ %s ]: Trace: [ %s ]' % (exception, trace)

+ 119
- 35
monitorstack/utils/os_utils.py View File

@@ -15,7 +15,6 @@
15 15
 
16 16
 try:
17 17
     from openstack import connection as os_conn
18
-    from openstack import exceptions as os_exp
19 18
 except ImportError as e:
20 19
     raise SystemExit('OpenStack plugins require access to the OpenStackSDK.'
21 20
                      ' Please install "python-openstacksdk".'
@@ -28,16 +27,32 @@ class OpenStack(object):
28 27
     """Class for reusable OpenStack utility methods."""
29 28
 
30 29
     def __init__(self, os_auth_args):
31
-        """Initialization method for class."""
30
+        """Initialization method for class.
31
+
32
+        :param os_auth_args: dict containing auth creds.
33
+        :type os_auth_args: dict
34
+        """
32 35
         self.os_auth_args = os_auth_args
33 36
 
34 37
     @property
35 38
     def conn(self):
36
-        """Return an OpenStackSDK connection."""
39
+        """Return an OpenStackSDK connection.
40
+
41
+        :returns: object
42
+        """
37 43
         return os_conn.Connection(**self.os_auth_args)
38 44
 
39 45
     def get_consumer_usage(self, servers=None, marker=None, limit=512):
40
-        """Retrieve current usage by an OpenStack cloud consumer."""
46
+        """Retrieve current usage by an OpenStack cloud consumer.
47
+
48
+        :param servers: ID of a given project to lookup.
49
+        :type servers: str || uuid
50
+        :param marker: ID of last server seen.
51
+        :type marker: str || uuid
52
+        :param limit: Number of items a single API call can return.
53
+        :type limit: int
54
+        :returns: list
55
+        """
41 56
         tenant_kwargs = {'details': True, 'all_tenants': True, 'limit': limit}
42 57
         if not servers:
43 58
             servers = list()
@@ -47,54 +62,123 @@ class OpenStack(object):
47 62
 
48 63
         count = 0
49 64
         for server in self.conn.compute.servers(**tenant_kwargs):
50
-            servers.append(server)
65
+            servers.append(server.to_dict())
51 66
             count += 1
52
-
53
-        if count == limit:
54
-            return self.get_consumer_usage(
55
-                servers=servers,
56
-                marker=servers[-1].id
57
-            )
67
+            if count == limit:
68
+                return self.get_consumer_usage(
69
+                    servers=servers,
70
+                    marker=servers[-1]['id']
71
+                )
58 72
 
59 73
         return servers
60 74
 
61 75
     def get_compute_limits(self, project_id, interface='internal'):
62
-        """Determine limits of compute resources."""
63
-        url = self.conn.compute.session.get_endpoint(
76
+        """Return compute resource limits for a project.
77
+
78
+        :param project_id: ID of a given project to lookup.
79
+        :type project_id: str || uuid
80
+        :param interface: Interface name, normally [internal, public, admin].
81
+        :type interface: str
82
+        :returns: dict
83
+        """
84
+        url = self.conn.session.get_endpoint(
64 85
             interface=interface,
65 86
             service_type='compute'
66 87
         )
67
-        quota_data = self.conn.compute.session.get(
88
+        quota_data = self.conn.session.get(
68 89
             url + '/os-quota-sets/' + project_id
69 90
         )
70 91
         return quota_data.json()
71 92
 
72
-    def get_project_name(self, project_id):
73
-        """Retrieve the name of a project."""
74
-        with utils.LocalCache() as c:
75
-            try:
76
-                project_name = c.get(project_id)
77
-                if not project_name:
78
-                    project_info = self.conn.identity.get_project(project_id)
79
-                    project_name = c[project_info.id] = project_info.name
80
-            except os_exp.ResourceNotFound:
81
-                return None
82
-            else:
83
-                return project_name
84
-
85 93
     def get_projects(self):
86
-        """Retrieve a list of projects."""
94
+        """Retrieve a list of projects.
95
+
96
+        :returns: list
97
+        """
87 98
         _consumers = list()
88 99
         with utils.LocalCache() as c:
89 100
             for project in self.conn.identity.projects():
90 101
                 _consumers.append(project)
91
-                c[project.id] = project.name
102
+                cache_key = 'projects_' + str(project.id)
103
+                c.set(
104
+                    cache_key,
105
+                    project.to_dict(),
106
+                    expire=43200,
107
+                    tag='projects'
108
+                )
92 109
         return _consumers
93 110
 
111
+    def get_project(self, project_id):
112
+        """Retrieve project data.
113
+
114
+        :param project_id: ID of a given project to lookup.
115
+        :type project_id: str || uuid
116
+        :returns: dict
117
+        """
118
+        project = None
119
+        cache_key = 'projects_{}'.format(project_id)
120
+        with utils.LocalCache() as c:
121
+            try:
122
+                project = c.get(cache_key)
123
+                if not project:
124
+                    raise LookupError
125
+            except LookupError:
126
+                project_info = self.conn.identity.get_project(project_id)
127
+                project = project_info.to_dict()
128
+                c.set(cache_key, project, expire=43200, tag='projects')
129
+            finally:
130
+                return project
131
+
132
+    def get_project_name(self, project_id):
133
+        """Retrieve the name of a project."""
134
+        return self.get_project(project_id=project_id)['name']
135
+
94 136
     def get_flavors(self):
95
-        """Retrieve a list of flavors."""
96
-        flavor_cache = dict()
97
-        for flavor in self.conn.compute.flavors():
98
-            entry = flavor_cache[flavor['id']] = dict()
99
-            entry.update(flavor)
100
-        return flavor_cache
137
+        """Retrieve all of flavors.
138
+
139
+        :returns: dict
140
+        """
141
+        flavors = dict()
142
+        with utils.LocalCache() as c:
143
+            for flavor in self.conn.compute.flavors():
144
+                _flavor = flavor.to_dict()
145
+                cache_key = 'flavor_' + str(flavor.id)
146
+                c.set(
147
+                    cache_key,
148
+                    _flavor,
149
+                    expire=43200,
150
+                    tag='flavors'
151
+                )
152
+                entry = flavors[flavor.id] = dict()
153
+                entry.update(_flavor)
154
+        return flavors
155
+
156
+    def get_flavor(self, flavor_id):
157
+        """Retrieve a flavor.
158
+
159
+        :param flavor_id: ID of a given flavor to lookup.
160
+        :type flavor_id: int || str
161
+        :returns: dict
162
+        """
163
+        flavor = None
164
+        cache_key = 'flavor_{}'.format(flavor_id)
165
+        with utils.LocalCache() as c:
166
+            try:
167
+                flavor = c.get(cache_key)
168
+                if not flavor:
169
+                    raise LookupError
170
+            except LookupError:
171
+                flavor_info = self.conn.compute.get_flavor(flavor_id)
172
+                flavor = flavor_info.to_dict()
173
+                c.set(cache_key, flavor, expire=43200, tag='flavors')
174
+            finally:
175
+                return flavor
176
+
177
+    def get_flavor_name(self, flavor_id):
178
+        """Retrieve the name of a flavor.
179
+
180
+        :param flavor_id: ID of a given flavor to lookup.
181
+        :type flavor_id: int || str
182
+        :returns: str
183
+        """
184
+        return self.get_flavor(flavor_id=flavor_id)['name']

+ 1
- 0
requirements.txt View File

@@ -1,4 +1,5 @@
1 1
 click
2
+diskcache
2 3
 openstacksdk>=0.9.14
3 4
 psutil>=5.2.0
4 5
 six

+ 14
- 0
tests/__init__.py View File

@@ -13,3 +13,17 @@
13 13
 # See the License for the specific language governing permissions and
14 14
 # limitations under the License.
15 15
 """This an __init__.py."""
16
+
17
+import os
18
+
19
+from monitorstack import utils
20
+
21
+
22
+def read_config():
23
+    """Load the test config file."""
24
+    os_config_file = os.path.expanduser(
25
+        os.path.abspath(
26
+            os.path.dirname(__file__) + '/files/test-openstack.ini'
27
+        )
28
+    )
29
+    return utils.read_config(os_config_file)

+ 0
- 47
tests/files/test-openstack.ini View File

@@ -1,47 +0,0 @@
1
-# Store the authentication credentials needed to query a given OpenStack Service.
2
-#  All sections are overrides for the defaults. If you only need to connect to a
3
-#  single cloud simply store the credentials needd in the DEFAULT section and
4
-#  override whatever is needed within the local sections.
5
-
6
-[DEFAULT]
7
-insecure = false
8
-auth_url = https://localhost:5000/v3
9
-
10
-# NOTE(cloudnull):
11
-#  When using keystone V3 you will need the .*domain_name configuration options.
12
-user_domain_name = default  # This is required when Keystone V3 is being used
13
-project_domain_name = default  # This is required when Keystone V3 is being used
14
-
15
-#  If you're using keystone V2 you will need the tenant_name option.
16
-tenant_name = admin  # This is required when Keystone V2 is being used
17
-
18
-# NEVER Mix and match the options tenant name and domain_name options.
19
-#  You are be required to run either V2 or V3 as it pertains to this config.
20
-#  If you provide both tenant_name and .*domain_name options at the same time
21
-#  the plugins will fail API version negotiation.
22
-
23
-username = admin
24
-password = Secrete
25
-# The verify option is for SSL. If your SSL certificate is not
26
-#  valid set this option to false else omit it or set it true.
27
-verify = false
28
-
29
-[keystone]
30
-
31
-[glance]
32
-
33
-[nova]
34
-project_name = nova
35
-
36
-[neutron]
37
-
38
-[heat]
39
-
40
-[cinder]
41
-
42
-[ironic]
43
-auth_url = https://localhost:5000/v3
44
-project_name = ironic
45
-user_domain_name = users
46
-project_domain_name = projects
47
-password = SuperSecrete

+ 1
- 0
tests/files/test-openstack.ini View File

@@ -0,0 +1 @@
1
+../../etc/openstack.ini

+ 246
- 0
tests/test_os_utils.py View File

@@ -0,0 +1,246 @@
1
+# Copyright 2017, Major Hayden <major@mhtx.net>
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+#     http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+# See the License for the specific language governing permissions and
13
+# limitations under the License.
14
+"""Tests for the os_utils plugin."""
15
+
16
+import unittest
17
+
18
+import mock
19
+
20
+from monitorstack.utils import os_utils
21
+
22
+import tests  # Import the test base module
23
+
24
+
25
+class OpenStackObject(object):
26
+    """Mocked server object."""
27
+
28
+    def __init__(self, id=None, name=None):
29
+        """Mocked server class."""
30
+        self.id = id
31
+        self.name = name
32
+
33
+    def to_dict(self):
34
+        """Mocked dict return."""
35
+        return {
36
+            'id': self.id,
37
+            'name': self.name
38
+        }
39
+
40
+
41
+class MockedOpenStackConn(object):
42
+    """Mocked OpenStack Connection object."""
43
+
44
+    class compute(object):  # noqa
45
+        """Mocked compute class."""
46
+
47
+        @staticmethod
48
+        def servers(*args, **kwargs):
49
+            """Mocked servers method."""
50
+            servers = [
51
+                OpenStackObject(1, 'test1'),
52
+                OpenStackObject(2, 'test2'),
53
+                OpenStackObject(3, 'test3'),
54
+                OpenStackObject(4, 'test4'),
55
+                OpenStackObject(5, 'test5')
56
+            ]
57
+            if 'marker' in kwargs:
58
+                for server in servers:
59
+                    if server.id == kwargs['marker']:
60
+                        index = servers.index(server)
61
+                        servers.pop(index)
62
+                        return servers[index:]
63
+            return servers
64
+
65
+        @staticmethod
66
+        def flavors():
67
+            """Mocked flavors return."""
68
+            return [
69
+                OpenStackObject(1, 'test1'),
70
+                OpenStackObject(2, 'test2'),
71
+                OpenStackObject(3, 'test3'),
72
+                OpenStackObject(4, 'test4'),
73
+                OpenStackObject(5, 'test5')
74
+            ]
75
+
76
+        @staticmethod
77
+        def get_flavor(flavor_id):
78
+            """Return mocked flavor object."""
79
+            return OpenStackObject(
80
+                flavor_id,
81
+                'test_{}'.format(flavor_id)
82
+            )
83
+
84
+    class identity(object):  # noqa
85
+        """Mocked identity object."""
86
+
87
+        @staticmethod
88
+        def projects():
89
+            """Mocked projects return."""
90
+            return [
91
+                OpenStackObject(1, 'test1'),
92
+                OpenStackObject(2, 'test2'),
93
+                OpenStackObject(3, 'test3'),
94
+                OpenStackObject(4, 'test4'),
95
+                OpenStackObject(5, 'test5')
96
+            ]
97
+
98
+        @staticmethod
99
+        def get_project(project_id):
100
+            """Return mocked project object."""
101
+            return OpenStackObject(
102
+                project_id,
103
+                'test_{}'.format(project_id)
104
+            )
105
+
106
+    class session(object):  # noqa
107
+        """Mocked session object."""
108
+
109
+        @staticmethod
110
+        def get_endpoint(interface, service_type):
111
+            """Mocked endpoint return."""
112
+            return "https://127.0.1.1/{}/{}".format(interface, service_type)
113
+
114
+        @staticmethod
115
+        def get(url):
116
+            """Mocked get return."""
117
+            class SessionGet(object):
118
+                """Mocked session object."""
119
+
120
+                def __init__(self, url):
121
+                    """Mocked session get."""
122
+                    self.url = url
123
+
124
+                def json(self):
125
+                    """Mocked json return."""
126
+                    return {'url': self.url}
127
+
128
+            return SessionGet(url=url)
129
+
130
+
131
+class TestOSUtilsConnection(unittest.TestCase):
132
+    """Tests for the utilities."""
133
+
134
+    def test_conn(self):
135
+        """Test the OpenStack connection interface."""
136
+        # load the base class for these tests.
137
+        self.osu = os_utils.OpenStack(
138
+            os_auth_args=tests.read_config()['keystone']
139
+        )
140
+        self.assertTrue(
141
+            isinstance(
142
+                self.osu.conn,
143
+                os_utils.os_conn.Connection
144
+            )
145
+        )
146
+
147
+
148
+class TestOsUtils(unittest.TestCase):
149
+    """Tests for the utilities."""
150
+
151
+    def setUp(self):
152
+        """Setup the test."""
153
+        # load the base class for these tests.
154
+        self.osu = os_utils.OpenStack(
155
+            os_auth_args=tests.read_config()['keystone']
156
+        )
157
+
158
+    def tearDown(self):
159
+        """Tear down the test."""
160
+        pass
161
+
162
+    def test_get_consumer_usage(self):
163
+        """Test retrieving consumer usage."""
164
+        with mock.patch('openstack.connection.Connection') as MockClass:
165
+            MockClass.return_value = MockedOpenStackConn()
166
+            self.assertTrue(isinstance(self.osu.get_consumer_usage(), list))
167
+
168
+    def test_get_consumer_usage_with_servers(self):
169
+        """Test retrieving consumer usage with servers list."""
170
+        with mock.patch('openstack.connection.Connection') as MockClass:
171
+            MockClass.return_value = MockedOpenStackConn()
172
+            servers = self.osu.get_consumer_usage(
173
+                servers=[OpenStackObject(0, 'test0').to_dict()]
174
+            )
175
+            self.assertEquals(len(servers), 6)
176
+
177
+    def test_get_consumer_usage_with_marker(self):
178
+        """Test retrieving consumer usage."""
179
+        with mock.patch('openstack.connection.Connection') as MockClass:
180
+            MockClass.return_value = MockedOpenStackConn()
181
+            servers = self.osu.get_consumer_usage(marker=5)
182
+            self.assertEquals(len(servers), 0)
183
+
184
+        with mock.patch('openstack.connection.Connection') as MockClass:
185
+            MockClass.return_value = MockedOpenStackConn()
186
+            servers = self.osu.get_consumer_usage(marker=2)
187
+            self.assertEquals(len(servers), 3)
188
+
189
+    def test_get_consumer_usage_with_limit(self):
190
+        """Test retrieving consumer usage."""
191
+        with mock.patch('openstack.connection.Connection') as MockClass:
192
+            MockClass.return_value = MockedOpenStackConn()
193
+            servers = self.osu.get_consumer_usage(limit=1)
194
+            self.assertEquals(len(servers), 5)
195
+
196
+    def test_get_compute_limits(self):
197
+        """Test retrieving consumer limits."""
198
+        with mock.patch('openstack.connection.Connection') as MockClass:
199
+            MockClass.return_value = MockedOpenStackConn()
200
+            limits = self.osu.get_compute_limits(project_id='not-a-uuid')
201
+            u = 'https://127.0.1.1/internal/compute/os-quota-sets/not-a-uuid'
202
+            self.assertEquals(limits, {'url': u})
203
+
204
+    def test_get_projects(self):
205
+        """Test retrieving project list."""
206
+        with mock.patch('openstack.connection.Connection') as MockClass:
207
+            MockClass.return_value = MockedOpenStackConn()
208
+            projects = self.osu.get_projects()
209
+            self.assertEquals(len(projects), 5)
210
+
211
+    def test_get_project(self):
212
+        """Test retrieving project dict."""
213
+        with mock.patch('openstack.connection.Connection') as MockClass:
214
+            MockClass.return_value = MockedOpenStackConn()
215
+            project = self.osu.get_project(project_id='12345')
216
+            self.assertEquals(project['id'], '12345')
217
+            self.assertEquals(project['name'], 'test_12345')
218
+
219
+    def test_get_project_name(self):
220
+        """Test retrieving project name."""
221
+        with mock.patch('openstack.connection.Connection') as MockClass:
222
+            MockClass.return_value = MockedOpenStackConn()
223
+            project_name = self.osu.get_project_name(project_id='12345')
224
+            self.assertEquals(project_name, 'test_12345')
225
+
226
+    def test_get_flavors(self):
227
+        """Test retrieving flavors dict."""
228
+        with mock.patch('openstack.connection.Connection') as MockClass:
229
+            MockClass.return_value = MockedOpenStackConn()
230
+            servers = self.osu.get_flavors()
231
+            self.assertEquals(len(servers), 5)
232
+
233
+    def test_get_flavor(self):
234
+        """Test retrieving flavor dict."""
235
+        with mock.patch('openstack.connection.Connection') as MockClass:
236
+            MockClass.return_value = MockedOpenStackConn()
237
+            flavor = self.osu.get_flavor(flavor_id=12345)
238
+            self.assertEquals(flavor['id'], 12345)
239
+            self.assertEquals(flavor['name'], 'test_12345')
240
+
241
+    def test_get_flavor_name(self):
242
+        """Test retrieving flavor name."""
243
+        with mock.patch('openstack.connection.Connection') as MockClass:
244
+            MockClass.return_value = MockedOpenStackConn()
245
+            flavor_name = self.osu.get_flavor_name(flavor_id=12345)
246
+            self.assertEquals(flavor_name, 'test_12345')

+ 52
- 11
tests/test_plugin_kvm.py View File

@@ -15,12 +15,22 @@
15 15
 
16 16
 import json
17 17
 import sys
18
+import unittest
18 19
 
19 20
 from click.testing import CliRunner
20 21
 
21 22
 from monitorstack.cli import cli
22 23
 
23 24
 
25
+def _runner(module):
26
+    runner = CliRunner()
27
+    result = runner.invoke(cli, ['-f', 'json', module])
28
+    try:
29
+        return json.loads(result.output)
30
+    except Exception:
31
+        return result.exception
32
+
33
+
24 34
 class LibvirtStub(object):
25 35
     """Stubbed libvirt class."""
26 36
 
@@ -38,26 +48,44 @@ class LibvirtStub(object):
38 48
 
39 49
         class lookupByID(object):  # noqa
40 50
             """Stubbed lookupByID class."""
41
-            def __init__(self, *args, **kwargs): # noqa
51
+            def __init__(self, *args, **kwargs):  # noqa
42 52
                 pass
43 53
 
44 54
             def maxVcpus(self):  # noqa
45 55
                 return 2
46 56
 
47 57
 
48
-class TestKvm(object):
58
+class LibvirtStubFailed(object):
59
+    """Stubbed libvirt class."""
60
+
61
+    class openReadOnly(object):  # noqa
62
+        """Stubbed openReadOnly class."""
63
+
64
+        def close(self, *args, **kwargs):  # noqa
65
+            pass
66
+
67
+        def listDomainsID(self, *args, **kwargs):  # noqa
68
+            raise RuntimeError('Failed')
69
+
70
+
71
+class TestKvm(unittest.TestCase):
49 72
     """Tests for the kvm monitor."""
50 73
 
51
-    def test_run(self):
52
-        """Ensure the run() method works."""
53
-        sys.modules['libvirt'] = LibvirtStub
74
+    def setUp(self):
75
+        """Setup teardown."""
76
+        self.orig_libvirt = sys.modules.pop('libvirt', None)
77
+        sys.modules['libvirt'] = LibvirtStub()
54 78
 
55
-        runner = CliRunner()
56
-        result = runner.invoke(cli, ['-f', 'json', 'kvm'])
57
-        result_json = json.loads(result.output)
79
+    def tearDown(self):
80
+        """Teardown method."""
81
+        if self.orig_libvirt:
82
+            sys.modules['libvirt'] = self.orig_libvirt
58 83
 
59
-        variables = result_json['variables']
60
-        meta = result_json['meta']
84
+    def test_run_success(self):
85
+        """Ensure the run() method works."""
86
+        result = _runner('kvm')
87
+        variables = result['variables']
88
+        meta = result['meta']
61 89
         assert 'kvm_vms' in variables
62 90
         assert variables['kvm_vms'] == 3
63 91
         assert 'kvm_total_vcpus' in variables
@@ -66,4 +94,17 @@ class TestKvm(object):
66 94
         assert variables['kvm_scheduled_vcpus'] == 6
67 95
         assert 'platform' in meta
68 96
         assert 'kvm_host_id' in meta
69
-        assert result.exit_code == 0
97
+        assert result['exit_code'] == 0
98
+
99
+    def test_run_failure_no_libvirt(self):
100
+        """Ensure the run() method works."""
101
+        sys.modules.pop('libvirt', None)
102
+        result = _runner('kvm')
103
+        self.assertTrue(isinstance(result, SystemExit))
104
+
105
+    def test_run_failure(self):
106
+        """Ensure the run() method works."""
107
+        sys.modules['libvirt'] = LibvirtStubFailed()
108
+        result = _runner('kvm')
109
+        assert result['measurement_name'] == 'kvm'
110
+        assert result['exit_code'] == 1

+ 32
- 7
tests/test_plugin_os_vm.py View File

@@ -38,20 +38,22 @@ class MockProject(object):
38 38
         """Mock init."""
39 39
         self.id = 'testing'
40 40
         self.name = 'testing'
41
+        self.project_id = 12345
41 42
 
42 43
 
43
-def mock_get_consumer_usage(self):
44
+def mock_get_consumer_usage(*args, **kwargs):
44 45
     """Mocked get_consumer_usage()."""
45 46
     return [{
46 47
         'name': 'test_name',
48
+        'project_id': 12345,
47 49
         'flavor': {
48 50
             'id': 1,
49
-            'name': 'flavor_one',
51
+            'name': 'flavor_one'
50 52
         }
51 53
     }]
52 54
 
53 55
 
54
-def mock_get_flavors(self):
56
+def mock_get_flavors(*args, **kwargs):
55 57
     """Mocked get_flavors()."""
56 58
     return {
57 59
         1: {
@@ -63,13 +65,27 @@ def mock_get_flavors(self):
63 65
     }
64 66
 
65 67
 
66
-def mock_get_projects(arg1):
68
+def mock_get_flavor(*args, **kwargs):
69
+    """Mocked get_flavor(id)."""
70
+    return {
71
+        'name': 'flavor_one',
72
+        'vcpus': 2,
73
+        'disk': 10,
74
+        'ram': 1024,
75
+    }
76
+
77
+
78
+def mock_get_project_name(*args, **kwargs):
67 79
     """Mocked get_projects()."""
68
-    projects = MockProject()
69
-    return [projects]
80
+    return 'test_name'
70 81
 
71 82
 
72
-def mock_get_compute_limits(self, project_id, interface):
83
+def mock_get_projects(*args, **kwargs):
84
+    """Mocked get_projects()."""
85
+    return [MockProject()]
86
+
87
+
88
+def mock_get_compute_limits(*args, **kwargs):
73 89
     """Mocked get_compute_limits()."""
74 90
     return {
75 91
         'quota_set': {
@@ -131,6 +147,8 @@ class TestOs(object):
131 147
     def test_os_vm_used_cores_success(self, monkeypatch):
132 148
         """Ensure os_vm_used_cores method works with success."""
133 149
         monkeypatch.setattr(Ost, 'get_flavors', mock_get_flavors)
150
+        monkeypatch.setattr(Ost, 'get_flavor', mock_get_flavor)
151
+        monkeypatch.setattr(Ost, 'get_project_name', mock_get_project_name)
134 152
         monkeypatch.setattr(Ost, 'get_consumer_usage', mock_get_consumer_usage)
135 153
 
136 154
         result = _runner('os_vm_used_cores')
@@ -147,6 +165,8 @@ class TestOs(object):
147 165
     def test_os_vm_used_disk_success(self, monkeypatch):
148 166
         """Ensure os_vm_used_disk method works with success."""
149 167
         monkeypatch.setattr(Ost, 'get_flavors', mock_get_flavors)
168
+        monkeypatch.setattr(Ost, 'get_flavor', mock_get_flavor)
169
+        monkeypatch.setattr(Ost, 'get_project_name', mock_get_project_name)
150 170
         monkeypatch.setattr(Ost, 'get_consumer_usage', mock_get_consumer_usage)
151 171
 
152 172
         result = _runner('os_vm_used_disk')
@@ -162,6 +182,9 @@ class TestOs(object):
162 182
 
163 183
     def test_os_vm_used_instance_success(self, monkeypatch):
164 184
         """Ensure os_vm_used_instance method works with success."""
185
+        monkeypatch.setattr(Ost, 'get_flavors', mock_get_flavors)
186
+        monkeypatch.setattr(Ost, 'get_flavor', mock_get_flavor)
187
+        monkeypatch.setattr(Ost, 'get_project_name', mock_get_project_name)
165 188
         monkeypatch.setattr(Ost, 'get_consumer_usage', mock_get_consumer_usage)
166 189
 
167 190
         result = _runner('os_vm_used_instance')
@@ -178,6 +201,8 @@ class TestOs(object):
178 201
     def test_os_vm_used_ram_success(self, monkeypatch):
179 202
         """Ensure os_vm_used_ram method works with success."""
180 203
         monkeypatch.setattr(Ost, 'get_flavors', mock_get_flavors)
204
+        monkeypatch.setattr(Ost, 'get_flavor', mock_get_flavor)
205
+        monkeypatch.setattr(Ost, 'get_project_name', mock_get_project_name)
181 206
         monkeypatch.setattr(Ost, 'get_consumer_usage', mock_get_consumer_usage)
182 207
 
183 208
         result = _runner('os_vm_used_ram')

+ 93
- 11
tests/test_utils.py View File

@@ -11,7 +11,7 @@
11 11
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 12
 # See the License for the specific language governing permissions and
13 13
 # limitations under the License.
14
-"""Tests for the uptime plugin."""
14
+"""Tests for the utils."""
15 15
 
16 16
 import os
17 17
 import tempfile
@@ -26,21 +26,27 @@ class TestUtils(unittest.TestCase):
26 26
     def setUp(self):
27 27
         """Initial setup for class."""
28 28
         os_config_file = os.path.expanduser(
29
-            os.path.abspath(__file__ + '/../../etc/openstack.ini')
29
+            os.path.abspath(
30
+                os.path.dirname(__file__) + '/files/test-openstack.ini'
31
+            )
30 32
         )
31 33
         self.config = utils.read_config(os_config_file)
32 34
         conf = utils.ConfigParser.RawConfigParser()
33 35
         conf.read([os_config_file])
34 36
         self.config_defaults = conf.defaults()
35 37
 
38
+        self.g_testfile = os.path.join(
39
+            os.path.expanduser('~'),
40
+            '.monitorstack.cache'
41
+        )
42
+        self.t_testfile = tempfile.mkdtemp()
43
+
36 44
     def tearDown(self):
37 45
         """Destroy the local cache."""
38
-        local_cache = os.path.join(
39
-            tempfile.gettempdir(),
40
-            'monitorstack.openstack.dbm'
41
-        )
42
-        if os.path.exists(local_cache):
43
-            os.remove(local_cache)
46
+        for f in [self.g_testfile, self.t_testfile]:
47
+            cache_db = os.path.join(f, 'cache.db')
48
+            if os.path.exists(cache_db):
49
+                os.remove(cache_db)
44 50
 
45 51
     def test_is_int_is_int(self):  # noqa
46 52
         self.assertTrue(isinstance(utils.is_int(value=1), int))
@@ -67,8 +73,84 @@ class TestUtils(unittest.TestCase):
67 73
             for key in self.config_defaults.keys():
68 74
                 self.assertTrue(key in v.keys())
69 75
 
70
-    def test_local_cache(self):
76
+    def test_local_cache_no_file(self):
71 77
         """Test local cache."""
72 78
         with utils.LocalCache() as c:
73
-            c['test_key'] = True
74
-            self.assertTrue('test_key' in c)
79
+            c['test_key1'] = True
80
+            self.assertTrue('test_key1' in c)
81
+
82
+    def test_local_cache_file(self):
83
+        """Test local cache."""
84
+        with utils.LocalCache(cache_path=self.t_testfile) as c:
85
+            c['test_key2'] = True
86
+            self.assertTrue('test_key2' in c)
87
+
88
+    def test_local_cache_no_file_no_context(self):
89
+        """Test local cache without a context manager."""
90
+        c = utils.LocalCache()
91
+        cache = c.lc_open()
92
+        cache['test_key3'] = True
93
+        try:
94
+            self.assertTrue('test_key3' in cache)
95
+        finally:
96
+            c.lc_close()
97
+
98
+        with utils.LocalCache() as c:
99
+            self.assertTrue('test_key3' in c)
100
+
101
+    def test_local_cache_file_no_context(self):
102
+        """Test local cache without a context manager."""
103
+        c = utils.LocalCache(cache_path=self.t_testfile)
104
+        cache = c.lc_open()
105
+        cache['test_key4'] = True
106
+        try:
107
+            self.assertTrue('test_key4' in cache)
108
+        finally:
109
+            c.lc_close()
110
+
111
+        with utils.LocalCache(cache_path=self.t_testfile) as c:
112
+            self.assertTrue('test_key4' in c)
113
+
114
+    def test_local_cache_no_load(self):
115
+        """Test local cache without loading anything."""
116
+        c = utils.LocalCache(cache_path=self.t_testfile)
117
+        c.lc_close()
118
+
119
+    def test_local_cache_named_ext(self):
120
+        """Test local cache without loading anything with a named extension."""
121
+        utils.LocalCache(cache_path='{}.cache'.format(self.t_testfile))
122
+
123
+    def test_retry_failure(self):
124
+        """Test retry decorator for failure."""
125
+        @utils.retry(ExceptionToCheck=BaseException, tries=3, backoff=0,
126
+                     delay=1)
127
+        def _failed():
128
+            """Raise failure exception after retry."""
129
+            raise BaseException
130
+
131
+        self.assertRaises(BaseException, _failed)
132
+
133
+    def test_retry_success(self):
134
+        """Test retry decorator for success."""
135
+        @utils.retry(ExceptionToCheck=BaseException, tries=3, backoff=0,
136
+                     delay=1)
137
+        def _success():
138
+            """Return True after retry."""
139
+            self.count += 1
140
+            if self.count == 3:
141
+                return True
142
+            else:
143
+                raise BaseException
144
+
145
+        self.count = 0
146
+        self.assertEquals(_success(), True)
147
+
148
+    def test_log_exception(self):
149
+        """Test traceback formatter for exception messages."""
150
+        try:
151
+            raise Exception('test-exception')
152
+        except Exception as exp:
153
+            message = utils.log_exception(exp=exp)
154
+
155
+        self.assertTrue('Exception' in message)
156
+        self.assertTrue('Trace' in message)

Loading…
Cancel
Save