~openstack-charmers-archive/charms/trusty/neutron-gateway/trunk

« back to all changes in this revision

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

  • Committer: James Page
  • Date: 2015-10-22 13:23:58 UTC
  • Revision ID: james.page@ubuntu.com-20151022132358-qin1nvlnqn4aezaz
Tags: 15.10
15.10 Charm release

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
18
 
import ConfigParser
19
 
import distro_info
20
17
import io
 
18
import json
21
19
import logging
22
20
import os
23
21
import re
24
 
import six
 
22
import socket
 
23
import subprocess
25
24
import sys
26
25
import time
27
 
import urlparse
 
26
import uuid
 
27
 
 
28
import amulet
 
29
import distro_info
 
30
import six
 
31
from six.moves import configparser
 
32
if six.PY3:
 
33
    from urllib import parse as urlparse
 
34
else:
 
35
    import urlparse
28
36
 
29
37
 
30
38
class AmuletUtils(object):
108
116
        # /!\ DEPRECATION WARNING (beisner):
109
117
        # New and existing tests should be rewritten to use
110
118
        # validate_services_by_name() as it is aware of init systems.
111
 
        self.log.warn('/!\\ DEPRECATION WARNING:  use '
 
119
        self.log.warn('DEPRECATION WARNING:  use '
112
120
                      'validate_services_by_name instead of validate_services '
113
121
                      'due to init system differences.')
114
122
 
142
150
 
143
151
            for service_name in services_list:
144
152
                if (self.ubuntu_releases.index(release) >= systemd_switch or
145
 
                        service_name == "rabbitmq-server"):
146
 
                    # init is systemd
 
153
                        service_name in ['rabbitmq-server', 'apache2']):
 
154
                    # init is systemd (or regular sysv)
147
155
                    cmd = 'sudo service {} status'.format(service_name)
 
156
                    output, code = sentry_unit.run(cmd)
 
157
                    service_running = code == 0
148
158
                elif self.ubuntu_releases.index(release) < systemd_switch:
149
159
                    # init is upstart
150
160
                    cmd = 'sudo status {}'.format(service_name)
 
161
                    output, code = sentry_unit.run(cmd)
 
162
                    service_running = code == 0 and "start/running" in output
151
163
 
152
 
                output, code = sentry_unit.run(cmd)
153
164
                self.log.debug('{} `{}` returned '
154
165
                               '{}'.format(sentry_unit.info['unit_name'],
155
166
                                           cmd, code))
156
 
                if code != 0:
157
 
                    return "command `{}` returned {}".format(cmd, str(code))
 
167
                if not service_running:
 
168
                    return u"command `{}` returned {} {}".format(
 
169
                        cmd, output, str(code))
158
170
        return None
159
171
 
160
172
    def _get_config(self, unit, filename):
164
176
        # NOTE(beisner):  by default, ConfigParser does not handle options
165
177
        # with no value, such as the flags used in the mysql my.cnf file.
166
178
        # https://bugs.python.org/issue7005
167
 
        config = ConfigParser.ConfigParser(allow_no_value=True)
 
179
        config = configparser.ConfigParser(allow_no_value=True)
168
180
        config.readfp(io.StringIO(file_contents))
169
181
        return config
170
182
 
259
271
        """Get last modification time of directory."""
260
272
        return sentry_unit.directory_stat(directory)['mtime']
261
273
 
262
 
    def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):
263
 
        """Get process' start time.
264
 
 
265
 
           Determine start time of the process based on the last modification
266
 
           time of the /proc/pid directory. If pgrep_full is True, the process
267
 
           name is matched against the full command line.
268
 
           """
269
 
        if pgrep_full:
270
 
            cmd = 'pgrep -o -f {}'.format(service)
271
 
        else:
272
 
            cmd = 'pgrep -o {}'.format(service)
273
 
        cmd = cmd + '  | grep  -v pgrep || exit 0'
274
 
        cmd_out = sentry_unit.run(cmd)
275
 
        self.log.debug('CMDout: ' + str(cmd_out))
276
 
        if cmd_out[0]:
277
 
            self.log.debug('Pid for %s %s' % (service, str(cmd_out[0])))
278
 
            proc_dir = '/proc/{}'.format(cmd_out[0].strip())
279
 
            return self._get_dir_mtime(sentry_unit, proc_dir)
 
274
    def _get_proc_start_time(self, sentry_unit, service, pgrep_full=None):
 
275
        """Get start time of a process based on the last modification time
 
276
           of the /proc/pid directory.
 
277
 
 
278
        :sentry_unit:  The sentry unit to check for the service on
 
279
        :service:  service name to look for in process table
 
280
        :pgrep_full:  [Deprecated] Use full command line search mode with pgrep
 
281
        :returns:  epoch time of service process start
 
282
        :param commands:  list of bash commands
 
283
        :param sentry_units:  list of sentry unit pointers
 
284
        :returns:  None if successful; Failure message otherwise
 
285
        """
 
286
        if pgrep_full is not None:
 
287
            # /!\ DEPRECATION WARNING (beisner):
 
288
            # No longer implemented, as pidof is now used instead of pgrep.
 
289
            # https://bugs.launchpad.net/charm-helpers/+bug/1474030
 
290
            self.log.warn('DEPRECATION WARNING:  pgrep_full bool is no '
 
291
                          'longer implemented re: lp 1474030.')
 
292
 
 
293
        pid_list = self.get_process_id_list(sentry_unit, service)
 
294
        pid = pid_list[0]
 
295
        proc_dir = '/proc/{}'.format(pid)
 
296
        self.log.debug('Pid for {} on {}: {}'.format(
 
297
            service, sentry_unit.info['unit_name'], pid))
 
298
 
 
299
        return self._get_dir_mtime(sentry_unit, proc_dir)
280
300
 
281
301
    def service_restarted(self, sentry_unit, service, filename,
282
 
                          pgrep_full=False, sleep_time=20):
 
302
                          pgrep_full=None, sleep_time=20):
283
303
        """Check if service was restarted.
284
304
 
285
305
           Compare a service's start time vs a file's last modification time
286
306
           (such as a config file for that service) to determine if the service
287
307
           has been restarted.
288
308
           """
 
309
        # /!\ DEPRECATION WARNING (beisner):
 
310
        # This method is prone to races in that no before-time is known.
 
311
        # Use validate_service_config_changed instead.
 
312
 
 
313
        # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
 
314
        # used instead of pgrep.  pgrep_full is still passed through to ensure
 
315
        # deprecation WARNS.  lp1474030
 
316
        self.log.warn('DEPRECATION WARNING:  use '
 
317
                      'validate_service_config_changed instead of '
 
318
                      'service_restarted due to known races.')
 
319
 
289
320
        time.sleep(sleep_time)
290
321
        if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
291
322
                self._get_file_mtime(sentry_unit, filename)):
294
325
            return False
295
326
 
296
327
    def service_restarted_since(self, sentry_unit, mtime, service,
297
 
                                pgrep_full=False, sleep_time=20,
298
 
                                retry_count=2):
 
328
                                pgrep_full=None, sleep_time=20,
 
329
                                retry_count=30, retry_sleep_time=10):
299
330
        """Check if service was been started after a given time.
300
331
 
301
332
        Args:
302
333
          sentry_unit (sentry): The sentry unit to check for the service on
303
334
          mtime (float): The epoch time to check against
304
335
          service (string): service name to look for in process table
305
 
          pgrep_full (boolean): Use full command line search mode with pgrep
306
 
          sleep_time (int): Seconds to sleep before looking for process
307
 
          retry_count (int): If service is not found, how many times to retry
 
336
          pgrep_full: [Deprecated] Use full command line search mode with pgrep
 
337
          sleep_time (int): Initial sleep time (s) before looking for file
 
338
          retry_sleep_time (int): Time (s) to sleep between retries
 
339
          retry_count (int): If file is not found, how many times to retry
308
340
 
309
341
        Returns:
310
342
          bool: True if service found and its start time it newer than mtime,
311
343
                False if service is older than mtime or if service was
312
344
                not found.
313
345
        """
314
 
        self.log.debug('Checking %s restarted since %s' % (service, mtime))
 
346
        # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
 
347
        # used instead of pgrep.  pgrep_full is still passed through to ensure
 
348
        # deprecation WARNS.  lp1474030
 
349
 
 
350
        unit_name = sentry_unit.info['unit_name']
 
351
        self.log.debug('Checking that %s service restarted since %s on '
 
352
                       '%s' % (service, mtime, unit_name))
315
353
        time.sleep(sleep_time)
316
 
        proc_start_time = self._get_proc_start_time(sentry_unit, service,
317
 
                                                    pgrep_full)
318
 
        while retry_count > 0 and not proc_start_time:
319
 
            self.log.debug('No pid file found for service %s, will retry %i '
320
 
                           'more times' % (service, retry_count))
321
 
            time.sleep(30)
322
 
            proc_start_time = self._get_proc_start_time(sentry_unit, service,
323
 
                                                        pgrep_full)
324
 
            retry_count = retry_count - 1
 
354
        proc_start_time = None
 
355
        tries = 0
 
356
        while tries <= retry_count and not proc_start_time:
 
357
            try:
 
358
                proc_start_time = self._get_proc_start_time(sentry_unit,
 
359
                                                            service,
 
360
                                                            pgrep_full)
 
361
                self.log.debug('Attempt {} to get {} proc start time on {} '
 
362
                               'OK'.format(tries, service, unit_name))
 
363
            except IOError as e:
 
364
                # NOTE(beisner) - race avoidance, proc may not exist yet.
 
365
                # https://bugs.launchpad.net/charm-helpers/+bug/1474030
 
366
                self.log.debug('Attempt {} to get {} proc start time on {} '
 
367
                               'failed\n{}'.format(tries, service,
 
368
                                                   unit_name, e))
 
369
                time.sleep(retry_sleep_time)
 
370
                tries += 1
325
371
 
326
372
        if not proc_start_time:
327
373
            self.log.warn('No proc start time found, assuming service did '
328
374
                          'not start')
329
375
            return False
330
376
        if proc_start_time >= mtime:
331
 
            self.log.debug('proc start time is newer than provided mtime'
332
 
                           '(%s >= %s)' % (proc_start_time, mtime))
 
377
            self.log.debug('Proc start time is newer than provided mtime'
 
378
                           '(%s >= %s) on %s (OK)' % (proc_start_time,
 
379
                                                      mtime, unit_name))
333
380
            return True
334
381
        else:
335
 
            self.log.warn('proc start time (%s) is older than provided mtime '
336
 
                          '(%s), service did not restart' % (proc_start_time,
337
 
                                                             mtime))
 
382
            self.log.warn('Proc start time (%s) is older than provided mtime '
 
383
                          '(%s) on %s, service did not '
 
384
                          'restart' % (proc_start_time, mtime, unit_name))
338
385
            return False
339
386
 
340
387
    def config_updated_since(self, sentry_unit, filename, mtime,
341
 
                             sleep_time=20):
 
388
                             sleep_time=20, retry_count=30,
 
389
                             retry_sleep_time=10):
342
390
        """Check if file was modified after a given time.
343
391
 
344
392
        Args:
345
393
          sentry_unit (sentry): The sentry unit to check the file mtime on
346
394
          filename (string): The file to check mtime of
347
395
          mtime (float): The epoch time to check against
348
 
          sleep_time (int): Seconds to sleep before looking for process
 
396
          sleep_time (int): Initial sleep time (s) before looking for file
 
397
          retry_sleep_time (int): Time (s) to sleep between retries
 
398
          retry_count (int): If file is not found, how many times to retry
349
399
 
350
400
        Returns:
351
401
          bool: True if file was modified more recently than mtime, False if
352
 
                file was modified before mtime,
 
402
                file was modified before mtime, or if file not found.
353
403
        """
354
 
        self.log.debug('Checking %s updated since %s' % (filename, mtime))
 
404
        unit_name = sentry_unit.info['unit_name']
 
405
        self.log.debug('Checking that %s updated since %s on '
 
406
                       '%s' % (filename, mtime, unit_name))
355
407
        time.sleep(sleep_time)
356
 
        file_mtime = self._get_file_mtime(sentry_unit, filename)
 
408
        file_mtime = None
 
409
        tries = 0
 
410
        while tries <= retry_count and not file_mtime:
 
411
            try:
 
412
                file_mtime = self._get_file_mtime(sentry_unit, filename)
 
413
                self.log.debug('Attempt {} to get {} file mtime on {} '
 
414
                               'OK'.format(tries, filename, unit_name))
 
415
            except IOError as e:
 
416
                # NOTE(beisner) - race avoidance, file may not exist yet.
 
417
                # https://bugs.launchpad.net/charm-helpers/+bug/1474030
 
418
                self.log.debug('Attempt {} to get {} file mtime on {} '
 
419
                               'failed\n{}'.format(tries, filename,
 
420
                                                   unit_name, e))
 
421
                time.sleep(retry_sleep_time)
 
422
                tries += 1
 
423
 
 
424
        if not file_mtime:
 
425
            self.log.warn('Could not determine file mtime, assuming '
 
426
                          'file does not exist')
 
427
            return False
 
428
 
357
429
        if file_mtime >= mtime:
358
430
            self.log.debug('File mtime is newer than provided mtime '
359
 
                           '(%s >= %s)' % (file_mtime, mtime))
 
431
                           '(%s >= %s) on %s (OK)' % (file_mtime,
 
432
                                                      mtime, unit_name))
360
433
            return True
361
434
        else:
362
 
            self.log.warn('File mtime %s is older than provided mtime %s'
363
 
                          % (file_mtime, mtime))
 
435
            self.log.warn('File mtime is older than provided mtime'
 
436
                          '(%s < on %s) on %s' % (file_mtime,
 
437
                                                  mtime, unit_name))
364
438
            return False
365
439
 
366
440
    def validate_service_config_changed(self, sentry_unit, mtime, service,
367
 
                                        filename, pgrep_full=False,
368
 
                                        sleep_time=20, retry_count=2):
 
441
                                        filename, pgrep_full=None,
 
442
                                        sleep_time=20, retry_count=30,
 
443
                                        retry_sleep_time=10):
369
444
        """Check service and file were updated after mtime
370
445
 
371
446
        Args:
373
448
          mtime (float): The epoch time to check against
374
449
          service (string): service name to look for in process table
375
450
          filename (string): The file to check mtime of
376
 
          pgrep_full (boolean): Use full command line search mode with pgrep
377
 
          sleep_time (int): Seconds to sleep before looking for process
 
451
          pgrep_full: [Deprecated] Use full command line search mode with pgrep
 
452
          sleep_time (int): Initial sleep in seconds to pass to test helpers
378
453
          retry_count (int): If service is not found, how many times to retry
 
454
          retry_sleep_time (int): Time in seconds to wait between retries
379
455
 
380
456
        Typical Usage:
381
457
            u = OpenStackAmuletUtils(ERROR)
392
468
                mtime, False if service is older than mtime or if service was
393
469
                not found or if filename was modified before mtime.
394
470
        """
395
 
        self.log.debug('Checking %s restarted since %s' % (service, mtime))
396
 
        time.sleep(sleep_time)
397
 
        service_restart = self.service_restarted_since(sentry_unit, mtime,
398
 
                                                       service,
399
 
                                                       pgrep_full=pgrep_full,
400
 
                                                       sleep_time=0,
401
 
                                                       retry_count=retry_count)
402
 
        config_update = self.config_updated_since(sentry_unit, filename, mtime,
403
 
                                                  sleep_time=0)
 
471
 
 
472
        # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
 
473
        # used instead of pgrep.  pgrep_full is still passed through to ensure
 
474
        # deprecation WARNS.  lp1474030
 
475
 
 
476
        service_restart = self.service_restarted_since(
 
477
            sentry_unit, mtime,
 
478
            service,
 
479
            pgrep_full=pgrep_full,
 
480
            sleep_time=sleep_time,
 
481
            retry_count=retry_count,
 
482
            retry_sleep_time=retry_sleep_time)
 
483
 
 
484
        config_update = self.config_updated_since(
 
485
            sentry_unit,
 
486
            filename,
 
487
            mtime,
 
488
            sleep_time=sleep_time,
 
489
            retry_count=retry_count,
 
490
            retry_sleep_time=retry_sleep_time)
 
491
 
404
492
        return service_restart and config_update
405
493
 
406
494
    def get_sentry_time(self, sentry_unit):
418
506
        """Return a list of all Ubuntu releases in order of release."""
419
507
        _d = distro_info.UbuntuDistroInfo()
420
508
        _release_list = _d.all
421
 
        self.log.debug('Ubuntu release list: {}'.format(_release_list))
422
509
        return _release_list
423
510
 
424
511
    def file_to_url(self, file_rel_path):
450
537
                                        cmd, code, output))
451
538
        return None
452
539
 
453
 
    def get_process_id_list(self, sentry_unit, process_name):
 
540
    def get_process_id_list(self, sentry_unit, process_name,
 
541
                            expect_success=True):
454
542
        """Get a list of process ID(s) from a single sentry juju unit
455
543
        for a single process name.
456
544
 
457
 
        :param sentry_unit: Pointer to amulet sentry instance (juju unit)
 
545
        :param sentry_unit: Amulet sentry instance (juju unit)
458
546
        :param process_name: Process name
 
547
        :param expect_success: If False, expect the PID to be missing,
 
548
            raise if it is present.
459
549
        :returns: List of process IDs
460
550
        """
461
 
        cmd = 'pidof {}'.format(process_name)
 
551
        cmd = 'pidof -x {}'.format(process_name)
 
552
        if not expect_success:
 
553
            cmd += " || exit 0 && exit 1"
462
554
        output, code = sentry_unit.run(cmd)
463
555
        if code != 0:
464
556
            msg = ('{} `{}` returned {} '
467
559
            amulet.raise_status(amulet.FAIL, msg=msg)
468
560
        return str(output).split()
469
561
 
470
 
    def get_unit_process_ids(self, unit_processes):
 
562
    def get_unit_process_ids(self, unit_processes, expect_success=True):
471
563
        """Construct a dict containing unit sentries, process names, and
472
 
        process IDs."""
 
564
        process IDs.
 
565
 
 
566
        :param unit_processes: A dictionary of Amulet sentry instance
 
567
            to list of process names.
 
568
        :param expect_success: if False expect the processes to not be
 
569
            running, raise if they are.
 
570
        :returns: Dictionary of Amulet sentry instance to dictionary
 
571
            of process names to PIDs.
 
572
        """
473
573
        pid_dict = {}
474
 
        for sentry_unit, process_list in unit_processes.iteritems():
 
574
        for sentry_unit, process_list in six.iteritems(unit_processes):
475
575
            pid_dict[sentry_unit] = {}
476
576
            for process in process_list:
477
 
                pids = self.get_process_id_list(sentry_unit, process)
 
577
                pids = self.get_process_id_list(
 
578
                    sentry_unit, process, expect_success=expect_success)
478
579
                pid_dict[sentry_unit].update({process: pids})
479
580
        return pid_dict
480
581
 
488
589
            return ('Unit count mismatch.  expected, actual: {}, '
489
590
                    '{} '.format(len(expected), len(actual)))
490
591
 
491
 
        for (e_sentry, e_proc_names) in expected.iteritems():
 
592
        for (e_sentry, e_proc_names) in six.iteritems(expected):
492
593
            e_sentry_name = e_sentry.info['unit_name']
493
594
            if e_sentry in actual.keys():
494
595
                a_proc_names = actual[e_sentry]
507
608
                            '{}'.format(e_proc_name, a_proc_name))
508
609
 
509
610
                a_pids_length = len(a_pids)
510
 
                if e_pids_length != a_pids_length:
511
 
                    return ('PID count mismatch. {} ({}) expected, actual: '
 
611
                fail_msg = ('PID count mismatch. {} ({}) expected, actual: '
512
612
                            '{}, {} ({})'.format(e_sentry_name, e_proc_name,
513
613
                                                 e_pids_length, a_pids_length,
514
614
                                                 a_pids))
 
615
 
 
616
                # If expected is not bool, ensure PID quantities match
 
617
                if not isinstance(e_pids_length, bool) and \
 
618
                        a_pids_length != e_pids_length:
 
619
                    return fail_msg
 
620
                # If expected is bool True, ensure 1 or more PIDs exist
 
621
                elif isinstance(e_pids_length, bool) and \
 
622
                        e_pids_length is True and a_pids_length < 1:
 
623
                    return fail_msg
 
624
                # If expected is bool False, ensure 0 PIDs exist
 
625
                elif isinstance(e_pids_length, bool) and \
 
626
                        e_pids_length is False and a_pids_length != 0:
 
627
                    return fail_msg
515
628
                else:
516
629
                    self.log.debug('PID check OK: {} {} {}: '
517
630
                                   '{}'.format(e_sentry_name, e_proc_name,
531
644
            return 'Dicts within list are not identical'
532
645
 
533
646
        return None
 
647
 
 
648
    def validate_sectionless_conf(self, file_contents, expected):
 
649
        """A crude conf parser.  Useful to inspect configuration files which
 
650
        do not have section headers (as would be necessary in order to use
 
651
        the configparser).  Such as openstack-dashboard or rabbitmq confs."""
 
652
        for line in file_contents.split('\n'):
 
653
            if '=' in line:
 
654
                args = line.split('=')
 
655
                if len(args) <= 1:
 
656
                    continue
 
657
                key = args[0].strip()
 
658
                value = args[1].strip()
 
659
                if key in expected.keys():
 
660
                    if expected[key] != value:
 
661
                        msg = ('Config mismatch.  Expected, actual:  {}, '
 
662
                               '{}'.format(expected[key], value))
 
663
                        amulet.raise_status(amulet.FAIL, msg=msg)
 
664
 
 
665
    def get_unit_hostnames(self, units):
 
666
        """Return a dict of juju unit names to hostnames."""
 
667
        host_names = {}
 
668
        for unit in units:
 
669
            host_names[unit.info['unit_name']] = \
 
670
                str(unit.file_contents('/etc/hostname').strip())
 
671
        self.log.debug('Unit host names: {}'.format(host_names))
 
672
        return host_names
 
673
 
 
674
    def run_cmd_unit(self, sentry_unit, cmd):
 
675
        """Run a command on a unit, return the output and exit code."""
 
676
        output, code = sentry_unit.run(cmd)
 
677
        if code == 0:
 
678
            self.log.debug('{} `{}` command returned {} '
 
679
                           '(OK)'.format(sentry_unit.info['unit_name'],
 
680
                                         cmd, code))
 
681
        else:
 
682
            msg = ('{} `{}` command returned {} '
 
683
                   '{}'.format(sentry_unit.info['unit_name'],
 
684
                               cmd, code, output))
 
685
            amulet.raise_status(amulet.FAIL, msg=msg)
 
686
        return str(output), code
 
687
 
 
688
    def file_exists_on_unit(self, sentry_unit, file_name):
 
689
        """Check if a file exists on a unit."""
 
690
        try:
 
691
            sentry_unit.file_stat(file_name)
 
692
            return True
 
693
        except IOError:
 
694
            return False
 
695
        except Exception as e:
 
696
            msg = 'Error checking file {}: {}'.format(file_name, e)
 
697
            amulet.raise_status(amulet.FAIL, msg=msg)
 
698
 
 
699
    def file_contents_safe(self, sentry_unit, file_name,
 
700
                           max_wait=60, fatal=False):
 
701
        """Get file contents from a sentry unit.  Wrap amulet file_contents
 
702
        with retry logic to address races where a file checks as existing,
 
703
        but no longer exists by the time file_contents is called.
 
704
        Return None if file not found. Optionally raise if fatal is True."""
 
705
        unit_name = sentry_unit.info['unit_name']
 
706
        file_contents = False
 
707
        tries = 0
 
708
        while not file_contents and tries < (max_wait / 4):
 
709
            try:
 
710
                file_contents = sentry_unit.file_contents(file_name)
 
711
            except IOError:
 
712
                self.log.debug('Attempt {} to open file {} from {} '
 
713
                               'failed'.format(tries, file_name,
 
714
                                               unit_name))
 
715
                time.sleep(4)
 
716
                tries += 1
 
717
 
 
718
        if file_contents:
 
719
            return file_contents
 
720
        elif not fatal:
 
721
            return None
 
722
        elif fatal:
 
723
            msg = 'Failed to get file contents from unit.'
 
724
            amulet.raise_status(amulet.FAIL, msg)
 
725
 
 
726
    def port_knock_tcp(self, host="localhost", port=22, timeout=15):
 
727
        """Open a TCP socket to check for a listening sevice on a host.
 
728
 
 
729
        :param host: host name or IP address, default to localhost
 
730
        :param port: TCP port number, default to 22
 
731
        :param timeout: Connect timeout, default to 15 seconds
 
732
        :returns: True if successful, False if connect failed
 
733
        """
 
734
 
 
735
        # Resolve host name if possible
 
736
        try:
 
737
            connect_host = socket.gethostbyname(host)
 
738
            host_human = "{} ({})".format(connect_host, host)
 
739
        except socket.error as e:
 
740
            self.log.warn('Unable to resolve address: '
 
741
                          '{} ({}) Trying anyway!'.format(host, e))
 
742
            connect_host = host
 
743
            host_human = connect_host
 
744
 
 
745
        # Attempt socket connection
 
746
        try:
 
747
            knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
748
            knock.settimeout(timeout)
 
749
            knock.connect((connect_host, port))
 
750
            knock.close()
 
751
            self.log.debug('Socket connect OK for host '
 
752
                           '{} on port {}.'.format(host_human, port))
 
753
            return True
 
754
        except socket.error as e:
 
755
            self.log.debug('Socket connect FAIL for'
 
756
                           ' {} port {} ({})'.format(host_human, port, e))
 
757
            return False
 
758
 
 
759
    def port_knock_units(self, sentry_units, port=22,
 
760
                         timeout=15, expect_success=True):
 
761
        """Open a TCP socket to check for a listening sevice on each
 
762
        listed juju unit.
 
763
 
 
764
        :param sentry_units: list of sentry unit pointers
 
765
        :param port: TCP port number, default to 22
 
766
        :param timeout: Connect timeout, default to 15 seconds
 
767
        :expect_success: True by default, set False to invert logic
 
768
        :returns: None if successful, Failure message otherwise
 
769
        """
 
770
        for unit in sentry_units:
 
771
            host = unit.info['public-address']
 
772
            connected = self.port_knock_tcp(host, port, timeout)
 
773
            if not connected and expect_success:
 
774
                return 'Socket connect failed.'
 
775
            elif connected and not expect_success:
 
776
                return 'Socket connected unexpectedly.'
 
777
 
 
778
    def get_uuid_epoch_stamp(self):
 
779
        """Returns a stamp string based on uuid4 and epoch time.  Useful in
 
780
        generating test messages which need to be unique-ish."""
 
781
        return '[{}-{}]'.format(uuid.uuid4(), time.time())
 
782
 
 
783
# amulet juju action helpers:
 
784
    def run_action(self, unit_sentry, action,
 
785
                   _check_output=subprocess.check_output):
 
786
        """Run the named action on a given unit sentry.
 
787
 
 
788
        _check_output parameter is used for dependency injection.
 
789
 
 
790
        @return action_id.
 
791
        """
 
792
        unit_id = unit_sentry.info["unit_name"]
 
793
        command = ["juju", "action", "do", "--format=json", unit_id, action]
 
794
        self.log.info("Running command: %s\n" % " ".join(command))
 
795
        output = _check_output(command, universal_newlines=True)
 
796
        data = json.loads(output)
 
797
        action_id = data[u'Action queued with id']
 
798
        return action_id
 
799
 
 
800
    def wait_on_action(self, action_id, _check_output=subprocess.check_output):
 
801
        """Wait for a given action, returning if it completed or not.
 
802
 
 
803
        _check_output parameter is used for dependency injection.
 
804
        """
 
805
        command = ["juju", "action", "fetch", "--format=json", "--wait=0",
 
806
                   action_id]
 
807
        output = _check_output(command, universal_newlines=True)
 
808
        data = json.loads(output)
 
809
        return data.get(u"status") == "completed"
 
810
 
 
811
    def status_get(self, unit):
 
812
        """Return the current service status of this unit."""
 
813
        raw_status, return_code = unit.run(
 
814
            "status-get --format=json --include-data")
 
815
        if return_code != 0:
 
816
            return ("unknown", "")
 
817
        status = json.loads(raw_status)
 
818
        return (status["status"], status["message"])