~ionutbalutoiu/charms/trusty/neutron-api/next

« back to all changes in this revision

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

  • Committer: David Ames
  • Date: 2015-09-28 17:45:40 UTC
  • mfrom: (145 trunk)
  • mto: This revision was merged to the branch mainline in revision 146.
  • Revision ID: david.ames@canonical.com-20150928174540-wx0t0d3uwgmlsotb
PullĀ inĀ upstream

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
324
326
 
325
327
    def service_restarted_since(self, sentry_unit, mtime, service,
326
328
                                pgrep_full=None, sleep_time=20,
327
 
                                retry_count=2, retry_sleep_time=30):
 
329
                                retry_count=30, retry_sleep_time=10):
328
330
        """Check if service was been started after a given time.
329
331
 
330
332
        Args:
332
334
          mtime (float): The epoch time to check against
333
335
          service (string): service name to look for in process table
334
336
          pgrep_full: [Deprecated] Use full command line search mode with pgrep
335
 
          sleep_time (int): Seconds to sleep before looking for process
336
 
          retry_count (int): If service is not found, how many times to retry
 
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
337
340
 
338
341
        Returns:
339
342
          bool: True if service found and its start time it newer than mtime,
357
360
                                                            pgrep_full)
358
361
                self.log.debug('Attempt {} to get {} proc start time on {} '
359
362
                               'OK'.format(tries, service, unit_name))
360
 
            except IOError:
 
363
            except IOError as e:
361
364
                # NOTE(beisner) - race avoidance, proc may not exist yet.
362
365
                # https://bugs.launchpad.net/charm-helpers/+bug/1474030
363
366
                self.log.debug('Attempt {} to get {} proc start time on {} '
364
 
                               'failed'.format(tries, service, unit_name))
 
367
                               'failed\n{}'.format(tries, service,
 
368
                                                   unit_name, e))
365
369
                time.sleep(retry_sleep_time)
366
370
                tries += 1
367
371
 
381
385
            return False
382
386
 
383
387
    def config_updated_since(self, sentry_unit, filename, mtime,
384
 
                             sleep_time=20):
 
388
                             sleep_time=20, retry_count=30,
 
389
                             retry_sleep_time=10):
385
390
        """Check if file was modified after a given time.
386
391
 
387
392
        Args:
388
393
          sentry_unit (sentry): The sentry unit to check the file mtime on
389
394
          filename (string): The file to check mtime of
390
395
          mtime (float): The epoch time to check against
391
 
          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
392
399
 
393
400
        Returns:
394
401
          bool: True if file was modified more recently than mtime, False if
395
 
                file was modified before mtime,
 
402
                file was modified before mtime, or if file not found.
396
403
        """
397
 
        self.log.debug('Checking that %s file updated since '
398
 
                       '%s' % (filename, mtime))
399
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))
400
407
        time.sleep(sleep_time)
401
 
        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
 
402
429
        if file_mtime >= mtime:
403
430
            self.log.debug('File mtime is newer than provided mtime '
404
 
                           '(%s >= %s) on %s (OK)' % (file_mtime, mtime,
405
 
                                                      unit_name))
 
431
                           '(%s >= %s) on %s (OK)' % (file_mtime,
 
432
                                                      mtime, unit_name))
406
433
            return True
407
434
        else:
408
 
            self.log.warn('File mtime %s is older than provided mtime %s'
409
 
                          % (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))
410
438
            return False
411
439
 
412
440
    def validate_service_config_changed(self, sentry_unit, mtime, service,
413
441
                                        filename, pgrep_full=None,
414
 
                                        sleep_time=20, retry_count=2,
415
 
                                        retry_sleep_time=30):
 
442
                                        sleep_time=20, retry_count=30,
 
443
                                        retry_sleep_time=10):
416
444
        """Check service and file were updated after mtime
417
445
 
418
446
        Args:
457
485
            sentry_unit,
458
486
            filename,
459
487
            mtime,
460
 
            sleep_time=0)
 
488
            sleep_time=sleep_time,
 
489
            retry_count=retry_count,
 
490
            retry_sleep_time=retry_sleep_time)
461
491
 
462
492
        return service_restart and config_update
463
493
 
476
506
        """Return a list of all Ubuntu releases in order of release."""
477
507
        _d = distro_info.UbuntuDistroInfo()
478
508
        _release_list = _d.all
479
 
        self.log.debug('Ubuntu release list: {}'.format(_release_list))
480
509
        return _release_list
481
510
 
482
511
    def file_to_url(self, file_rel_path):
616
645
 
617
646
        return None
618
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:
619
784
    def run_action(self, unit_sentry, action,
620
785
                   _check_output=subprocess.check_output):
621
786
        """Run the named action on a given unit sentry.
642
807
        output = _check_output(command, universal_newlines=True)
643
808
        data = json.loads(output)
644
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"])