~jjo/charms/trusty/ubuntu/add-execd-and-charmhelpers-syncing

« back to all changes in this revision

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

  • Committer: Tim Van Steenburgh
  • Date: 2015-07-28 12:26:21 UTC
  • mfrom: (11.1.4 ubuntu)
  • Revision ID: tim.van.steenburgh@canonical.com-20150728122621-wuhg0fqgj5wi2127
[1chb1n] Remove utopic test, add wily test

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# You should have received a copy of the GNU Lesser General Public License
15
15
# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
16
16
 
 
17
import amulet
17
18
import ConfigParser
 
19
import distro_info
18
20
import io
19
21
import logging
 
22
import os
20
23
import re
 
24
import six
21
25
import sys
22
26
import time
23
 
 
24
 
import six
 
27
import urlparse
25
28
 
26
29
 
27
30
class AmuletUtils(object):
33
36
 
34
37
    def __init__(self, log_level=logging.ERROR):
35
38
        self.log = self.get_logger(level=log_level)
 
39
        self.ubuntu_releases = self.get_ubuntu_releases()
36
40
 
37
41
    def get_logger(self, name="amulet-logger", level=logging.DEBUG):
38
42
        """Get a logger object that will log to stdout."""
70
74
        else:
71
75
            return False
72
76
 
 
77
    def get_ubuntu_release_from_sentry(self, sentry_unit):
 
78
        """Get Ubuntu release codename from sentry unit.
 
79
 
 
80
        :param sentry_unit: amulet sentry/service unit pointer
 
81
        :returns: list of strings - release codename, failure message
 
82
        """
 
83
        msg = None
 
84
        cmd = 'lsb_release -cs'
 
85
        release, code = sentry_unit.run(cmd)
 
86
        if code == 0:
 
87
            self.log.debug('{} lsb_release: {}'.format(
 
88
                sentry_unit.info['unit_name'], release))
 
89
        else:
 
90
            msg = ('{} `{}` returned {} '
 
91
                   '{}'.format(sentry_unit.info['unit_name'],
 
92
                               cmd, release, code))
 
93
        if release not in self.ubuntu_releases:
 
94
            msg = ("Release ({}) not found in Ubuntu releases "
 
95
                   "({})".format(release, self.ubuntu_releases))
 
96
        return release, msg
 
97
 
73
98
    def validate_services(self, commands):
74
 
        """Validate services.
75
 
 
76
 
           Verify the specified services are running on the corresponding
 
99
        """Validate that lists of commands succeed on service units.  Can be
 
100
           used to verify system services are running on the corresponding
77
101
           service units.
78
 
           """
 
102
 
 
103
        :param commands: dict with sentry keys and arbitrary command list vals
 
104
        :returns: None if successful, Failure string message otherwise
 
105
        """
 
106
        self.log.debug('Checking status of system services...')
 
107
 
 
108
        # /!\ DEPRECATION WARNING (beisner):
 
109
        # New and existing tests should be rewritten to use
 
110
        # validate_services_by_name() as it is aware of init systems.
 
111
        self.log.warn('/!\\ DEPRECATION WARNING:  use '
 
112
                      'validate_services_by_name instead of validate_services '
 
113
                      'due to init system differences.')
 
114
 
79
115
        for k, v in six.iteritems(commands):
80
116
            for cmd in v:
81
117
                output, code = k.run(cmd)
86
122
                    return "command `{}` returned {}".format(cmd, str(code))
87
123
        return None
88
124
 
 
125
    def validate_services_by_name(self, sentry_services):
 
126
        """Validate system service status by service name, automatically
 
127
           detecting init system based on Ubuntu release codename.
 
128
 
 
129
        :param sentry_services: dict with sentry keys and svc list values
 
130
        :returns: None if successful, Failure string message otherwise
 
131
        """
 
132
        self.log.debug('Checking status of system services...')
 
133
 
 
134
        # Point at which systemd became a thing
 
135
        systemd_switch = self.ubuntu_releases.index('vivid')
 
136
 
 
137
        for sentry_unit, services_list in six.iteritems(sentry_services):
 
138
            # Get lsb_release codename from unit
 
139
            release, ret = self.get_ubuntu_release_from_sentry(sentry_unit)
 
140
            if ret:
 
141
                return ret
 
142
 
 
143
            for service_name in services_list:
 
144
                if (self.ubuntu_releases.index(release) >= systemd_switch or
 
145
                        service_name == "rabbitmq-server"):
 
146
                    # init is systemd
 
147
                    cmd = 'sudo service {} status'.format(service_name)
 
148
                elif self.ubuntu_releases.index(release) < systemd_switch:
 
149
                    # init is upstart
 
150
                    cmd = 'sudo status {}'.format(service_name)
 
151
 
 
152
                output, code = sentry_unit.run(cmd)
 
153
                self.log.debug('{} `{}` returned '
 
154
                               '{}'.format(sentry_unit.info['unit_name'],
 
155
                                           cmd, code))
 
156
                if code != 0:
 
157
                    return "command `{}` returned {}".format(cmd, str(code))
 
158
        return None
 
159
 
89
160
    def _get_config(self, unit, filename):
90
161
        """Get a ConfigParser object for parsing a unit's config file."""
91
162
        file_contents = unit.file_contents(filename)
103
174
 
104
175
           Verify that the specified section of the config file contains
105
176
           the expected option key:value pairs.
 
177
 
 
178
           Compare expected dictionary data vs actual dictionary data.
 
179
           The values in the 'expected' dictionary can be strings, bools, ints,
 
180
           longs, or can be a function that evaluates a variable and returns a
 
181
           bool.
106
182
           """
 
183
        self.log.debug('Validating config file data ({} in {} on {})'
 
184
                       '...'.format(section, config_file,
 
185
                                    sentry_unit.info['unit_name']))
107
186
        config = self._get_config(sentry_unit, config_file)
108
187
 
109
188
        if section != 'DEFAULT' and not config.has_section(section):
112
191
        for k in expected.keys():
113
192
            if not config.has_option(section, k):
114
193
                return "section [{}] is missing option {}".format(section, k)
115
 
            if config.get(section, k) != expected[k]:
 
194
 
 
195
            actual = config.get(section, k)
 
196
            v = expected[k]
 
197
            if (isinstance(v, six.string_types) or
 
198
                    isinstance(v, bool) or
 
199
                    isinstance(v, six.integer_types)):
 
200
                # handle explicit values
 
201
                if actual != v:
 
202
                    return "section [{}] {}:{} != expected {}:{}".format(
 
203
                           section, k, actual, k, expected[k])
 
204
            # handle function pointers, such as not_null or valid_ip
 
205
            elif not v(actual):
116
206
                return "section [{}] {}:{} != expected {}:{}".format(
117
 
                       section, k, config.get(section, k), k, expected[k])
 
207
                       section, k, actual, k, expected[k])
118
208
        return None
119
209
 
120
210
    def _validate_dict_data(self, expected, actual):
122
212
 
123
213
           Compare expected dictionary data vs actual dictionary data.
124
214
           The values in the 'expected' dictionary can be strings, bools, ints,
125
 
           longs, or can be a function that evaluate a variable and returns a
 
215
           longs, or can be a function that evaluates a variable and returns a
126
216
           bool.
127
217
           """
128
218
        self.log.debug('actual: {}'.format(repr(actual)))
133
223
                if (isinstance(v, six.string_types) or
134
224
                        isinstance(v, bool) or
135
225
                        isinstance(v, six.integer_types)):
 
226
                    # handle explicit values
136
227
                    if v != actual[k]:
137
228
                        return "{}:{}".format(k, actual[k])
 
229
                # handle function pointers, such as not_null or valid_ip
138
230
                elif not v(actual[k]):
139
231
                    return "{}:{}".format(k, actual[k])
140
232
            else:
321
413
 
322
414
    def endpoint_error(self, name, data):
323
415
        return 'unexpected endpoint data in {} - {}'.format(name, data)
 
416
 
 
417
    def get_ubuntu_releases(self):
 
418
        """Return a list of all Ubuntu releases in order of release."""
 
419
        _d = distro_info.UbuntuDistroInfo()
 
420
        _release_list = _d.all
 
421
        self.log.debug('Ubuntu release list: {}'.format(_release_list))
 
422
        return _release_list
 
423
 
 
424
    def file_to_url(self, file_rel_path):
 
425
        """Convert a relative file path to a file URL."""
 
426
        _abs_path = os.path.abspath(file_rel_path)
 
427
        return urlparse.urlparse(_abs_path, scheme='file').geturl()
 
428
 
 
429
    def check_commands_on_units(self, commands, sentry_units):
 
430
        """Check that all commands in a list exit zero on all
 
431
        sentry units in a list.
 
432
 
 
433
        :param commands:  list of bash commands
 
434
        :param sentry_units:  list of sentry unit pointers
 
435
        :returns: None if successful; Failure message otherwise
 
436
        """
 
437
        self.log.debug('Checking exit codes for {} commands on {} '
 
438
                       'sentry units...'.format(len(commands),
 
439
                                                len(sentry_units)))
 
440
        for sentry_unit in sentry_units:
 
441
            for cmd in commands:
 
442
                output, code = sentry_unit.run(cmd)
 
443
                if code == 0:
 
444
                    self.log.debug('{} `{}` returned {} '
 
445
                                   '(OK)'.format(sentry_unit.info['unit_name'],
 
446
                                                 cmd, code))
 
447
                else:
 
448
                    return ('{} `{}` returned {} '
 
449
                            '{}'.format(sentry_unit.info['unit_name'],
 
450
                                        cmd, code, output))
 
451
        return None
 
452
 
 
453
    def get_process_id_list(self, sentry_unit, process_name):
 
454
        """Get a list of process ID(s) from a single sentry juju unit
 
455
        for a single process name.
 
456
 
 
457
        :param sentry_unit: Pointer to amulet sentry instance (juju unit)
 
458
        :param process_name: Process name
 
459
        :returns: List of process IDs
 
460
        """
 
461
        cmd = 'pidof {}'.format(process_name)
 
462
        output, code = sentry_unit.run(cmd)
 
463
        if code != 0:
 
464
            msg = ('{} `{}` returned {} '
 
465
                   '{}'.format(sentry_unit.info['unit_name'],
 
466
                               cmd, code, output))
 
467
            amulet.raise_status(amulet.FAIL, msg=msg)
 
468
        return str(output).split()
 
469
 
 
470
    def get_unit_process_ids(self, unit_processes):
 
471
        """Construct a dict containing unit sentries, process names, and
 
472
        process IDs."""
 
473
        pid_dict = {}
 
474
        for sentry_unit, process_list in unit_processes.iteritems():
 
475
            pid_dict[sentry_unit] = {}
 
476
            for process in process_list:
 
477
                pids = self.get_process_id_list(sentry_unit, process)
 
478
                pid_dict[sentry_unit].update({process: pids})
 
479
        return pid_dict
 
480
 
 
481
    def validate_unit_process_ids(self, expected, actual):
 
482
        """Validate process id quantities for services on units."""
 
483
        self.log.debug('Checking units for running processes...')
 
484
        self.log.debug('Expected PIDs: {}'.format(expected))
 
485
        self.log.debug('Actual PIDs: {}'.format(actual))
 
486
 
 
487
        if len(actual) != len(expected):
 
488
            return ('Unit count mismatch.  expected, actual: {}, '
 
489
                    '{} '.format(len(expected), len(actual)))
 
490
 
 
491
        for (e_sentry, e_proc_names) in expected.iteritems():
 
492
            e_sentry_name = e_sentry.info['unit_name']
 
493
            if e_sentry in actual.keys():
 
494
                a_proc_names = actual[e_sentry]
 
495
            else:
 
496
                return ('Expected sentry ({}) not found in actual dict data.'
 
497
                        '{}'.format(e_sentry_name, e_sentry))
 
498
 
 
499
            if len(e_proc_names.keys()) != len(a_proc_names.keys()):
 
500
                return ('Process name count mismatch.  expected, actual: {}, '
 
501
                        '{}'.format(len(expected), len(actual)))
 
502
 
 
503
            for (e_proc_name, e_pids_length), (a_proc_name, a_pids) in \
 
504
                    zip(e_proc_names.items(), a_proc_names.items()):
 
505
                if e_proc_name != a_proc_name:
 
506
                    return ('Process name mismatch.  expected, actual: {}, '
 
507
                            '{}'.format(e_proc_name, a_proc_name))
 
508
 
 
509
                a_pids_length = len(a_pids)
 
510
                if e_pids_length != a_pids_length:
 
511
                    return ('PID count mismatch. {} ({}) expected, actual: '
 
512
                            '{}, {} ({})'.format(e_sentry_name, e_proc_name,
 
513
                                                 e_pids_length, a_pids_length,
 
514
                                                 a_pids))
 
515
                else:
 
516
                    self.log.debug('PID check OK: {} {} {}: '
 
517
                                   '{}'.format(e_sentry_name, e_proc_name,
 
518
                                               e_pids_length, a_pids))
 
519
        return None
 
520
 
 
521
    def validate_list_of_identical_dicts(self, list_of_dicts):
 
522
        """Check that all dicts within a list are identical."""
 
523
        hashes = []
 
524
        for _dict in list_of_dicts:
 
525
            hashes.append(hash(frozenset(_dict.items())))
 
526
 
 
527
        self.log.debug('Hashes: {}'.format(hashes))
 
528
        if len(set(hashes)) == 1:
 
529
            self.log.debug('Dicts within list are identical')
 
530
        else:
 
531
            return 'Dicts within list are not identical'
 
532
 
 
533
        return None