~junaidali/charms/trusty/plumgrid-gateway/oil-sapi-changes

« back to all changes in this revision

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

  • Committer: James Page
  • Date: 2016-05-25 16:16:36 UTC
  • mfrom: (12.1.16 plumgrid-gateway)
  • Revision ID: james.page@ubuntu.com-20160525161636-ziy44dl43fgnlmn6
Add support for Liberty and Mitaka.

Improved pg-restart.

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]
500
601
                return ('Process name count mismatch.  expected, actual: {}, '
501
602
                        '{}'.format(len(expected), len(actual)))
502
603
 
503
 
            for (e_proc_name, e_pids_length), (a_proc_name, a_pids) in \
 
604
            for (e_proc_name, e_pids), (a_proc_name, a_pids) in \
504
605
                    zip(e_proc_names.items(), a_proc_names.items()):
505
606
                if e_proc_name != a_proc_name:
506
607
                    return ('Process name mismatch.  expected, actual: {}, '
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
 
                                                 e_pids_length, a_pids_length,
 
613
                                                 e_pids, a_pids_length,
514
614
                                                 a_pids))
 
615
 
 
616
                # If expected is a list, ensure at least one PID quantity match
 
617
                if isinstance(e_pids, list) and \
 
618
                        a_pids_length not in e_pids:
 
619
                    return fail_msg
 
620
                # If expected is not bool and not list,
 
621
                # ensure PID quantities match
 
622
                elif not isinstance(e_pids, bool) and \
 
623
                        not isinstance(e_pids, list) and \
 
624
                        a_pids_length != e_pids:
 
625
                    return fail_msg
 
626
                # If expected is bool True, ensure 1 or more PIDs exist
 
627
                elif isinstance(e_pids, bool) and \
 
628
                        e_pids is True and a_pids_length < 1:
 
629
                    return fail_msg
 
630
                # If expected is bool False, ensure 0 PIDs exist
 
631
                elif isinstance(e_pids, bool) and \
 
632
                        e_pids is False and a_pids_length != 0:
 
633
                    return fail_msg
515
634
                else:
516
635
                    self.log.debug('PID check OK: {} {} {}: '
517
636
                                   '{}'.format(e_sentry_name, e_proc_name,
518
 
                                               e_pids_length, a_pids))
 
637
                                               e_pids, a_pids))
519
638
        return None
520
639
 
521
640
    def validate_list_of_identical_dicts(self, list_of_dicts):
531
650
            return 'Dicts within list are not identical'
532
651
 
533
652
        return None
 
653
 
 
654
    def validate_sectionless_conf(self, file_contents, expected):
 
655
        """A crude conf parser.  Useful to inspect configuration files which
 
656
        do not have section headers (as would be necessary in order to use
 
657
        the configparser).  Such as openstack-dashboard or rabbitmq confs."""
 
658
        for line in file_contents.split('\n'):
 
659
            if '=' in line:
 
660
                args = line.split('=')
 
661
                if len(args) <= 1:
 
662
                    continue
 
663
                key = args[0].strip()
 
664
                value = args[1].strip()
 
665
                if key in expected.keys():
 
666
                    if expected[key] != value:
 
667
                        msg = ('Config mismatch.  Expected, actual:  {}, '
 
668
                               '{}'.format(expected[key], value))
 
669
                        amulet.raise_status(amulet.FAIL, msg=msg)
 
670
 
 
671
    def get_unit_hostnames(self, units):
 
672
        """Return a dict of juju unit names to hostnames."""
 
673
        host_names = {}
 
674
        for unit in units:
 
675
            host_names[unit.info['unit_name']] = \
 
676
                str(unit.file_contents('/etc/hostname').strip())
 
677
        self.log.debug('Unit host names: {}'.format(host_names))
 
678
        return host_names
 
679
 
 
680
    def run_cmd_unit(self, sentry_unit, cmd):
 
681
        """Run a command on a unit, return the output and exit code."""
 
682
        output, code = sentry_unit.run(cmd)
 
683
        if code == 0:
 
684
            self.log.debug('{} `{}` command returned {} '
 
685
                           '(OK)'.format(sentry_unit.info['unit_name'],
 
686
                                         cmd, code))
 
687
        else:
 
688
            msg = ('{} `{}` command returned {} '
 
689
                   '{}'.format(sentry_unit.info['unit_name'],
 
690
                               cmd, code, output))
 
691
            amulet.raise_status(amulet.FAIL, msg=msg)
 
692
        return str(output), code
 
693
 
 
694
    def file_exists_on_unit(self, sentry_unit, file_name):
 
695
        """Check if a file exists on a unit."""
 
696
        try:
 
697
            sentry_unit.file_stat(file_name)
 
698
            return True
 
699
        except IOError:
 
700
            return False
 
701
        except Exception as e:
 
702
            msg = 'Error checking file {}: {}'.format(file_name, e)
 
703
            amulet.raise_status(amulet.FAIL, msg=msg)
 
704
 
 
705
    def file_contents_safe(self, sentry_unit, file_name,
 
706
                           max_wait=60, fatal=False):
 
707
        """Get file contents from a sentry unit.  Wrap amulet file_contents
 
708
        with retry logic to address races where a file checks as existing,
 
709
        but no longer exists by the time file_contents is called.
 
710
        Return None if file not found. Optionally raise if fatal is True."""
 
711
        unit_name = sentry_unit.info['unit_name']
 
712
        file_contents = False
 
713
        tries = 0
 
714
        while not file_contents and tries < (max_wait / 4):
 
715
            try:
 
716
                file_contents = sentry_unit.file_contents(file_name)
 
717
            except IOError:
 
718
                self.log.debug('Attempt {} to open file {} from {} '
 
719
                               'failed'.format(tries, file_name,
 
720
                                               unit_name))
 
721
                time.sleep(4)
 
722
                tries += 1
 
723
 
 
724
        if file_contents:
 
725
            return file_contents
 
726
        elif not fatal:
 
727
            return None
 
728
        elif fatal:
 
729
            msg = 'Failed to get file contents from unit.'
 
730
            amulet.raise_status(amulet.FAIL, msg)
 
731
 
 
732
    def port_knock_tcp(self, host="localhost", port=22, timeout=15):
 
733
        """Open a TCP socket to check for a listening sevice on a host.
 
734
 
 
735
        :param host: host name or IP address, default to localhost
 
736
        :param port: TCP port number, default to 22
 
737
        :param timeout: Connect timeout, default to 15 seconds
 
738
        :returns: True if successful, False if connect failed
 
739
        """
 
740
 
 
741
        # Resolve host name if possible
 
742
        try:
 
743
            connect_host = socket.gethostbyname(host)
 
744
            host_human = "{} ({})".format(connect_host, host)
 
745
        except socket.error as e:
 
746
            self.log.warn('Unable to resolve address: '
 
747
                          '{} ({}) Trying anyway!'.format(host, e))
 
748
            connect_host = host
 
749
            host_human = connect_host
 
750
 
 
751
        # Attempt socket connection
 
752
        try:
 
753
            knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
754
            knock.settimeout(timeout)
 
755
            knock.connect((connect_host, port))
 
756
            knock.close()
 
757
            self.log.debug('Socket connect OK for host '
 
758
                           '{} on port {}.'.format(host_human, port))
 
759
            return True
 
760
        except socket.error as e:
 
761
            self.log.debug('Socket connect FAIL for'
 
762
                           ' {} port {} ({})'.format(host_human, port, e))
 
763
            return False
 
764
 
 
765
    def port_knock_units(self, sentry_units, port=22,
 
766
                         timeout=15, expect_success=True):
 
767
        """Open a TCP socket to check for a listening sevice on each
 
768
        listed juju unit.
 
769
 
 
770
        :param sentry_units: list of sentry unit pointers
 
771
        :param port: TCP port number, default to 22
 
772
        :param timeout: Connect timeout, default to 15 seconds
 
773
        :expect_success: True by default, set False to invert logic
 
774
        :returns: None if successful, Failure message otherwise
 
775
        """
 
776
        for unit in sentry_units:
 
777
            host = unit.info['public-address']
 
778
            connected = self.port_knock_tcp(host, port, timeout)
 
779
            if not connected and expect_success:
 
780
                return 'Socket connect failed.'
 
781
            elif connected and not expect_success:
 
782
                return 'Socket connected unexpectedly.'
 
783
 
 
784
    def get_uuid_epoch_stamp(self):
 
785
        """Returns a stamp string based on uuid4 and epoch time.  Useful in
 
786
        generating test messages which need to be unique-ish."""
 
787
        return '[{}-{}]'.format(uuid.uuid4(), time.time())
 
788
 
 
789
# amulet juju action helpers:
 
790
    def run_action(self, unit_sentry, action,
 
791
                   _check_output=subprocess.check_output,
 
792
                   params=None):
 
793
        """Run the named action on a given unit sentry.
 
794
 
 
795
        params a dict of parameters to use
 
796
        _check_output parameter is used for dependency injection.
 
797
 
 
798
        @return action_id.
 
799
        """
 
800
        unit_id = unit_sentry.info["unit_name"]
 
801
        command = ["juju", "action", "do", "--format=json", unit_id, action]
 
802
        if params is not None:
 
803
            for key, value in params.iteritems():
 
804
                command.append("{}={}".format(key, value))
 
805
        self.log.info("Running command: %s\n" % " ".join(command))
 
806
        output = _check_output(command, universal_newlines=True)
 
807
        data = json.loads(output)
 
808
        action_id = data[u'Action queued with id']
 
809
        return action_id
 
810
 
 
811
    def wait_on_action(self, action_id, _check_output=subprocess.check_output):
 
812
        """Wait for a given action, returning if it completed or not.
 
813
 
 
814
        _check_output parameter is used for dependency injection.
 
815
        """
 
816
        command = ["juju", "action", "fetch", "--format=json", "--wait=0",
 
817
                   action_id]
 
818
        output = _check_output(command, universal_newlines=True)
 
819
        data = json.loads(output)
 
820
        return data.get(u"status") == "completed"
 
821
 
 
822
    def status_get(self, unit):
 
823
        """Return the current service status of this unit."""
 
824
        raw_status, return_code = unit.run(
 
825
            "status-get --format=json --include-data")
 
826
        if return_code != 0:
 
827
            return ("unknown", "")
 
828
        status = json.loads(raw_status)
 
829
        return (status["status"], status["message"])