~1chb1n/charms/trusty/hacluster/next.1601-test-update2

« back to all changes in this revision

Viewing changes to tests/charmhelpers/contrib/amulet/utils.py

  • Committer: Billy Olsen
  • Date: 2015-05-01 12:43:29 UTC
  • mfrom: (43.2.8 hacluster.cleanup)
  • Revision ID: billy.olsen@canonical.com-20150501124329-aj6vf5rw9ce95n47
[hopem,r=wolsen]

Refactor and clean-up the hacluster charm.
This makes the code format and layout more consistent with
the rest of the openstack charms.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2014-2015 Canonical Limited.
 
2
#
 
3
# This file is part of charm-helpers.
 
4
#
 
5
# charm-helpers is free software: you can redistribute it and/or modify
 
6
# it under the terms of the GNU Lesser General Public License version 3 as
 
7
# published by the Free Software Foundation.
 
8
#
 
9
# charm-helpers is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU Lesser General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU Lesser General Public License
 
15
# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
 
16
 
 
17
import ConfigParser
 
18
import io
 
19
import logging
 
20
import re
 
21
import sys
 
22
import time
 
23
 
 
24
import six
 
25
 
 
26
 
 
27
class AmuletUtils(object):
 
28
    """Amulet utilities.
 
29
 
 
30
       This class provides common utility functions that are used by Amulet
 
31
       tests.
 
32
       """
 
33
 
 
34
    def __init__(self, log_level=logging.ERROR):
 
35
        self.log = self.get_logger(level=log_level)
 
36
 
 
37
    def get_logger(self, name="amulet-logger", level=logging.DEBUG):
 
38
        """Get a logger object that will log to stdout."""
 
39
        log = logging
 
40
        logger = log.getLogger(name)
 
41
        fmt = log.Formatter("%(asctime)s %(funcName)s "
 
42
                            "%(levelname)s: %(message)s")
 
43
 
 
44
        handler = log.StreamHandler(stream=sys.stdout)
 
45
        handler.setLevel(level)
 
46
        handler.setFormatter(fmt)
 
47
 
 
48
        logger.addHandler(handler)
 
49
        logger.setLevel(level)
 
50
 
 
51
        return logger
 
52
 
 
53
    def valid_ip(self, ip):
 
54
        if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
 
55
            return True
 
56
        else:
 
57
            return False
 
58
 
 
59
    def valid_url(self, url):
 
60
        p = re.compile(
 
61
            r'^(?:http|ftp)s?://'
 
62
            r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|'  # noqa
 
63
            r'localhost|'
 
64
            r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
 
65
            r'(?::\d+)?'
 
66
            r'(?:/?|[/?]\S+)$',
 
67
            re.IGNORECASE)
 
68
        if p.match(url):
 
69
            return True
 
70
        else:
 
71
            return False
 
72
 
 
73
    def validate_services(self, commands):
 
74
        """Validate services.
 
75
 
 
76
           Verify the specified services are running on the corresponding
 
77
           service units.
 
78
           """
 
79
        for k, v in six.iteritems(commands):
 
80
            for cmd in v:
 
81
                output, code = k.run(cmd)
 
82
                if code != 0:
 
83
                    return "command `{}` returned {}".format(cmd, str(code))
 
84
        return None
 
85
 
 
86
    def _get_config(self, unit, filename):
 
87
        """Get a ConfigParser object for parsing a unit's config file."""
 
88
        file_contents = unit.file_contents(filename)
 
89
        config = ConfigParser.ConfigParser()
 
90
        config.readfp(io.StringIO(file_contents))
 
91
        return config
 
92
 
 
93
    def validate_config_data(self, sentry_unit, config_file, section,
 
94
                             expected):
 
95
        """Validate config file data.
 
96
 
 
97
           Verify that the specified section of the config file contains
 
98
           the expected option key:value pairs.
 
99
           """
 
100
        config = self._get_config(sentry_unit, config_file)
 
101
 
 
102
        if section != 'DEFAULT' and not config.has_section(section):
 
103
            return "section [{}] does not exist".format(section)
 
104
 
 
105
        for k in expected.keys():
 
106
            if not config.has_option(section, k):
 
107
                return "section [{}] is missing option {}".format(section, k)
 
108
            if config.get(section, k) != expected[k]:
 
109
                return "section [{}] {}:{} != expected {}:{}".format(
 
110
                       section, k, config.get(section, k), k, expected[k])
 
111
        return None
 
112
 
 
113
    def _validate_dict_data(self, expected, actual):
 
114
        """Validate dictionary data.
 
115
 
 
116
           Compare expected dictionary data vs actual dictionary data.
 
117
           The values in the 'expected' dictionary can be strings, bools, ints,
 
118
           longs, or can be a function that evaluate a variable and returns a
 
119
           bool.
 
120
           """
 
121
        for k, v in six.iteritems(expected):
 
122
            if k in actual:
 
123
                if (isinstance(v, six.string_types) or
 
124
                        isinstance(v, bool) or
 
125
                        isinstance(v, six.integer_types)):
 
126
                    if v != actual[k]:
 
127
                        return "{}:{}".format(k, actual[k])
 
128
                elif not v(actual[k]):
 
129
                    return "{}:{}".format(k, actual[k])
 
130
            else:
 
131
                return "key '{}' does not exist".format(k)
 
132
        return None
 
133
 
 
134
    def validate_relation_data(self, sentry_unit, relation, expected):
 
135
        """Validate actual relation data based on expected relation data."""
 
136
        actual = sentry_unit.relation(relation[0], relation[1])
 
137
        self.log.debug('actual: {}'.format(repr(actual)))
 
138
        return self._validate_dict_data(expected, actual)
 
139
 
 
140
    def _validate_list_data(self, expected, actual):
 
141
        """Compare expected list vs actual list data."""
 
142
        for e in expected:
 
143
            if e not in actual:
 
144
                return "expected item {} not found in actual list".format(e)
 
145
        return None
 
146
 
 
147
    def not_null(self, string):
 
148
        if string is not None:
 
149
            return True
 
150
        else:
 
151
            return False
 
152
 
 
153
    def _get_file_mtime(self, sentry_unit, filename):
 
154
        """Get last modification time of file."""
 
155
        return sentry_unit.file_stat(filename)['mtime']
 
156
 
 
157
    def _get_dir_mtime(self, sentry_unit, directory):
 
158
        """Get last modification time of directory."""
 
159
        return sentry_unit.directory_stat(directory)['mtime']
 
160
 
 
161
    def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):
 
162
        """Get process' start time.
 
163
 
 
164
           Determine start time of the process based on the last modification
 
165
           time of the /proc/pid directory. If pgrep_full is True, the process
 
166
           name is matched against the full command line.
 
167
           """
 
168
        if pgrep_full:
 
169
            cmd = 'pgrep -o -f {}'.format(service)
 
170
        else:
 
171
            cmd = 'pgrep -o {}'.format(service)
 
172
        cmd = cmd + '  | grep  -v pgrep || exit 0'
 
173
        cmd_out = sentry_unit.run(cmd)
 
174
        self.log.debug('CMDout: ' + str(cmd_out))
 
175
        if cmd_out[0]:
 
176
            self.log.debug('Pid for %s %s' % (service, str(cmd_out[0])))
 
177
            proc_dir = '/proc/{}'.format(cmd_out[0].strip())
 
178
            return self._get_dir_mtime(sentry_unit, proc_dir)
 
179
 
 
180
    def service_restarted(self, sentry_unit, service, filename,
 
181
                          pgrep_full=False, sleep_time=20):
 
182
        """Check if service was restarted.
 
183
 
 
184
           Compare a service's start time vs a file's last modification time
 
185
           (such as a config file for that service) to determine if the service
 
186
           has been restarted.
 
187
           """
 
188
        time.sleep(sleep_time)
 
189
        if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
 
190
                self._get_file_mtime(sentry_unit, filename)):
 
191
            return True
 
192
        else:
 
193
            return False
 
194
 
 
195
    def service_restarted_since(self, sentry_unit, mtime, service,
 
196
                                pgrep_full=False, sleep_time=20,
 
197
                                retry_count=2):
 
198
        """Check if service was been started after a given time.
 
199
 
 
200
        Args:
 
201
          sentry_unit (sentry): The sentry unit to check for the service on
 
202
          mtime (float): The epoch time to check against
 
203
          service (string): service name to look for in process table
 
204
          pgrep_full (boolean): Use full command line search mode with pgrep
 
205
          sleep_time (int): Seconds to sleep before looking for process
 
206
          retry_count (int): If service is not found, how many times to retry
 
207
 
 
208
        Returns:
 
209
          bool: True if service found and its start time it newer than mtime,
 
210
                False if service is older than mtime or if service was
 
211
                not found.
 
212
        """
 
213
        self.log.debug('Checking %s restarted since %s' % (service, mtime))
 
214
        time.sleep(sleep_time)
 
215
        proc_start_time = self._get_proc_start_time(sentry_unit, service,
 
216
                                                    pgrep_full)
 
217
        while retry_count > 0 and not proc_start_time:
 
218
            self.log.debug('No pid file found for service %s, will retry %i '
 
219
                           'more times' % (service, retry_count))
 
220
            time.sleep(30)
 
221
            proc_start_time = self._get_proc_start_time(sentry_unit, service,
 
222
                                                        pgrep_full)
 
223
            retry_count = retry_count - 1
 
224
 
 
225
        if not proc_start_time:
 
226
            self.log.warn('No proc start time found, assuming service did '
 
227
                          'not start')
 
228
            return False
 
229
        if proc_start_time >= mtime:
 
230
            self.log.debug('proc start time is newer than provided mtime'
 
231
                           '(%s >= %s)' % (proc_start_time, mtime))
 
232
            return True
 
233
        else:
 
234
            self.log.warn('proc start time (%s) is older than provided mtime '
 
235
                          '(%s), service did not restart' % (proc_start_time,
 
236
                                                             mtime))
 
237
            return False
 
238
 
 
239
    def config_updated_since(self, sentry_unit, filename, mtime,
 
240
                             sleep_time=20):
 
241
        """Check if file was modified after a given time.
 
242
 
 
243
        Args:
 
244
          sentry_unit (sentry): The sentry unit to check the file mtime on
 
245
          filename (string): The file to check mtime of
 
246
          mtime (float): The epoch time to check against
 
247
          sleep_time (int): Seconds to sleep before looking for process
 
248
 
 
249
        Returns:
 
250
          bool: True if file was modified more recently than mtime, False if
 
251
                file was modified before mtime,
 
252
        """
 
253
        self.log.debug('Checking %s updated since %s' % (filename, mtime))
 
254
        time.sleep(sleep_time)
 
255
        file_mtime = self._get_file_mtime(sentry_unit, filename)
 
256
        if file_mtime >= mtime:
 
257
            self.log.debug('File mtime is newer than provided mtime '
 
258
                           '(%s >= %s)' % (file_mtime, mtime))
 
259
            return True
 
260
        else:
 
261
            self.log.warn('File mtime %s is older than provided mtime %s'
 
262
                          % (file_mtime, mtime))
 
263
            return False
 
264
 
 
265
    def validate_service_config_changed(self, sentry_unit, mtime, service,
 
266
                                        filename, pgrep_full=False,
 
267
                                        sleep_time=20, retry_count=2):
 
268
        """Check service and file were updated after mtime
 
269
 
 
270
        Args:
 
271
          sentry_unit (sentry): The sentry unit to check for the service on
 
272
          mtime (float): The epoch time to check against
 
273
          service (string): service name to look for in process table
 
274
          filename (string): The file to check mtime of
 
275
          pgrep_full (boolean): Use full command line search mode with pgrep
 
276
          sleep_time (int): Seconds to sleep before looking for process
 
277
          retry_count (int): If service is not found, how many times to retry
 
278
 
 
279
        Typical Usage:
 
280
            u = OpenStackAmuletUtils(ERROR)
 
281
            ...
 
282
            mtime = u.get_sentry_time(self.cinder_sentry)
 
283
            self.d.configure('cinder', {'verbose': 'True', 'debug': 'True'})
 
284
            if not u.validate_service_config_changed(self.cinder_sentry,
 
285
                                                     mtime,
 
286
                                                     'cinder-api',
 
287
                                                     '/etc/cinder/cinder.conf')
 
288
                amulet.raise_status(amulet.FAIL, msg='update failed')
 
289
        Returns:
 
290
          bool: True if both service and file where updated/restarted after
 
291
                mtime, False if service is older than mtime or if service was
 
292
                not found or if filename was modified before mtime.
 
293
        """
 
294
        self.log.debug('Checking %s restarted since %s' % (service, mtime))
 
295
        time.sleep(sleep_time)
 
296
        service_restart = self.service_restarted_since(sentry_unit, mtime,
 
297
                                                       service,
 
298
                                                       pgrep_full=pgrep_full,
 
299
                                                       sleep_time=0,
 
300
                                                       retry_count=retry_count)
 
301
        config_update = self.config_updated_since(sentry_unit, filename, mtime,
 
302
                                                  sleep_time=0)
 
303
        return service_restart and config_update
 
304
 
 
305
    def get_sentry_time(self, sentry_unit):
 
306
        """Return current epoch time on a sentry"""
 
307
        cmd = "date +'%s'"
 
308
        return float(sentry_unit.run(cmd)[0])
 
309
 
 
310
    def relation_error(self, name, data):
 
311
        return 'unexpected relation data in {} - {}'.format(name, data)
 
312
 
 
313
    def endpoint_error(self, name, data):
 
314
        return 'unexpected endpoint data in {} - {}'.format(name, data)