~chris.macnaughton/charms/trusty/ceph-osd/storage-hooks

« back to all changes in this revision

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

  • Committer: Corey Bryant
  • Date: 2016-01-25 14:14:48 UTC
  • mfrom: (58.1.2 ceph-osd)
  • Revision ID: corey.bryant@canonical.com-20160125141448-9r760mmrop4bay07
Tags: 16.01
[beisner, r=corey.bryant] Sync charm-helpers and enable amulet tests 018 and 020

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
import logging
20
20
import os
21
21
import re
 
22
import socket
22
23
import subprocess
23
24
import sys
24
25
import time
 
26
import uuid
25
27
 
26
28
import amulet
27
29
import distro_info
114
116
        # /!\ DEPRECATION WARNING (beisner):
115
117
        # New and existing tests should be rewritten to use
116
118
        # validate_services_by_name() as it is aware of init systems.
117
 
        self.log.warn('/!\\ DEPRECATION WARNING:  use '
 
119
        self.log.warn('DEPRECATION WARNING:  use '
118
120
                      'validate_services_by_name instead of validate_services '
119
121
                      'due to init system differences.')
120
122
 
269
271
        """Get last modification time of directory."""
270
272
        return sentry_unit.directory_stat(directory)['mtime']
271
273
 
272
 
    def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):
273
 
        """Get process' start time.
274
 
 
275
 
           Determine start time of the process based on the last modification
276
 
           time of the /proc/pid directory. If pgrep_full is True, the process
277
 
           name is matched against the full command line.
278
 
           """
279
 
        if pgrep_full:
280
 
            cmd = 'pgrep -o -f {}'.format(service)
281
 
        else:
282
 
            cmd = 'pgrep -o {}'.format(service)
283
 
        cmd = cmd + '  | grep  -v pgrep || exit 0'
284
 
        cmd_out = sentry_unit.run(cmd)
285
 
        self.log.debug('CMDout: ' + str(cmd_out))
286
 
        if cmd_out[0]:
287
 
            self.log.debug('Pid for %s %s' % (service, str(cmd_out[0])))
288
 
            proc_dir = '/proc/{}'.format(cmd_out[0].strip())
289
 
            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)
290
300
 
291
301
    def service_restarted(self, sentry_unit, service, filename,
292
 
                          pgrep_full=False, sleep_time=20):
 
302
                          pgrep_full=None, sleep_time=20):
293
303
        """Check if service was restarted.
294
304
 
295
305
           Compare a service's start time vs a file's last modification time
296
306
           (such as a config file for that service) to determine if the service
297
307
           has been restarted.
298
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
 
299
320
        time.sleep(sleep_time)
300
321
        if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
301
322
                self._get_file_mtime(sentry_unit, filename)):
304
325
            return False
305
326
 
306
327
    def service_restarted_since(self, sentry_unit, mtime, service,
307
 
                                pgrep_full=False, sleep_time=20,
308
 
                                retry_count=2):
 
328
                                pgrep_full=None, sleep_time=20,
 
329
                                retry_count=30, retry_sleep_time=10):
309
330
        """Check if service was been started after a given time.
310
331
 
311
332
        Args:
312
333
          sentry_unit (sentry): The sentry unit to check for the service on
313
334
          mtime (float): The epoch time to check against
314
335
          service (string): service name to look for in process table
315
 
          pgrep_full (boolean): Use full command line search mode with pgrep
316
 
          sleep_time (int): Seconds to sleep before looking for process
317
 
          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
318
340
 
319
341
        Returns:
320
342
          bool: True if service found and its start time it newer than mtime,
321
343
                False if service is older than mtime or if service was
322
344
                not found.
323
345
        """
324
 
        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))
325
353
        time.sleep(sleep_time)
326
 
        proc_start_time = self._get_proc_start_time(sentry_unit, service,
327
 
                                                    pgrep_full)
328
 
        while retry_count > 0 and not proc_start_time:
329
 
            self.log.debug('No pid file found for service %s, will retry %i '
330
 
                           'more times' % (service, retry_count))
331
 
            time.sleep(30)
332
 
            proc_start_time = self._get_proc_start_time(sentry_unit, service,
333
 
                                                        pgrep_full)
334
 
            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
335
371
 
336
372
        if not proc_start_time:
337
373
            self.log.warn('No proc start time found, assuming service did '
338
374
                          'not start')
339
375
            return False
340
376
        if proc_start_time >= mtime:
341
 
            self.log.debug('proc start time is newer than provided mtime'
342
 
                           '(%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))
343
380
            return True
344
381
        else:
345
 
            self.log.warn('proc start time (%s) is older than provided mtime '
346
 
                          '(%s), service did not restart' % (proc_start_time,
347
 
                                                             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))
348
385
            return False
349
386
 
350
387
    def config_updated_since(self, sentry_unit, filename, mtime,
351
 
                             sleep_time=20):
 
388
                             sleep_time=20, retry_count=30,
 
389
                             retry_sleep_time=10):
352
390
        """Check if file was modified after a given time.
353
391
 
354
392
        Args:
355
393
          sentry_unit (sentry): The sentry unit to check the file mtime on
356
394
          filename (string): The file to check mtime of
357
395
          mtime (float): The epoch time to check against
358
 
          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
359
399
 
360
400
        Returns:
361
401
          bool: True if file was modified more recently than mtime, False if
362
 
                file was modified before mtime,
 
402
                file was modified before mtime, or if file not found.
363
403
        """
364
 
        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))
365
407
        time.sleep(sleep_time)
366
 
        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
 
367
429
        if file_mtime >= mtime:
368
430
            self.log.debug('File mtime is newer than provided mtime '
369
 
                           '(%s >= %s)' % (file_mtime, mtime))
 
431
                           '(%s >= %s) on %s (OK)' % (file_mtime,
 
432
                                                      mtime, unit_name))
370
433
            return True
371
434
        else:
372
 
            self.log.warn('File mtime %s is older than provided mtime %s'
373
 
                          % (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))
374
438
            return False
375
439
 
376
440
    def validate_service_config_changed(self, sentry_unit, mtime, service,
377
 
                                        filename, pgrep_full=False,
378
 
                                        sleep_time=20, retry_count=2):
 
441
                                        filename, pgrep_full=None,
 
442
                                        sleep_time=20, retry_count=30,
 
443
                                        retry_sleep_time=10):
379
444
        """Check service and file were updated after mtime
380
445
 
381
446
        Args:
383
448
          mtime (float): The epoch time to check against
384
449
          service (string): service name to look for in process table
385
450
          filename (string): The file to check mtime of
386
 
          pgrep_full (boolean): Use full command line search mode with pgrep
387
 
          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
388
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
389
455
 
390
456
        Typical Usage:
391
457
            u = OpenStackAmuletUtils(ERROR)
402
468
                mtime, False if service is older than mtime or if service was
403
469
                not found or if filename was modified before mtime.
404
470
        """
405
 
        self.log.debug('Checking %s restarted since %s' % (service, mtime))
406
 
        time.sleep(sleep_time)
407
 
        service_restart = self.service_restarted_since(sentry_unit, mtime,
408
 
                                                       service,
409
 
                                                       pgrep_full=pgrep_full,
410
 
                                                       sleep_time=0,
411
 
                                                       retry_count=retry_count)
412
 
        config_update = self.config_updated_since(sentry_unit, filename, mtime,
413
 
                                                  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
 
414
492
        return service_restart and config_update
415
493
 
416
494
    def get_sentry_time(self, sentry_unit):
428
506
        """Return a list of all Ubuntu releases in order of release."""
429
507
        _d = distro_info.UbuntuDistroInfo()
430
508
        _release_list = _d.all
431
 
        self.log.debug('Ubuntu release list: {}'.format(_release_list))
432
509
        return _release_list
433
510
 
434
511
    def file_to_url(self, file_rel_path):
568
645
 
569
646
        return None
570
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:
571
784
    def run_action(self, unit_sentry, action,
572
785
                   _check_output=subprocess.check_output):
573
786
        """Run the named action on a given unit sentry.
594
807
        output = _check_output(command, universal_newlines=True)
595
808
        data = json.loads(output)
596
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"])