~openstack-charm-sync/charms/trusty/cinder-ceph/next

« back to all changes in this revision

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

  • Committer: Edward Hope-Morley
  • Date: 2015-09-16 01:07:56 UTC
  • mfrom: (40.1.7 trunk)
  • Revision ID: edward.hope-morley@canonical.com-20150916010756-6hqtjf3ogftq55p8
[gnuoy,r=hopem]

Update cinder-ceph broker code to scale-out bug.

Closes-Bug: #1453940

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=2, retry_sleep_time=30):
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
 
336
          pgrep_full: [Deprecated] Use full command line search mode with pgrep
316
337
          sleep_time (int): Seconds to sleep before looking for process
317
338
          retry_count (int): If service is not found, how many times to retry
318
339
 
321
342
                False if service is older than mtime or if service was
322
343
                not found.
323
344
        """
324
 
        self.log.debug('Checking %s restarted since %s' % (service, mtime))
 
345
        # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
 
346
        # used instead of pgrep.  pgrep_full is still passed through to ensure
 
347
        # deprecation WARNS.  lp1474030
 
348
 
 
349
        unit_name = sentry_unit.info['unit_name']
 
350
        self.log.debug('Checking that %s service restarted since %s on '
 
351
                       '%s' % (service, mtime, unit_name))
325
352
        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
 
353
        proc_start_time = None
 
354
        tries = 0
 
355
        while tries <= retry_count and not proc_start_time:
 
356
            try:
 
357
                proc_start_time = self._get_proc_start_time(sentry_unit,
 
358
                                                            service,
 
359
                                                            pgrep_full)
 
360
                self.log.debug('Attempt {} to get {} proc start time on {} '
 
361
                               'OK'.format(tries, service, unit_name))
 
362
            except IOError:
 
363
                # NOTE(beisner) - race avoidance, proc may not exist yet.
 
364
                # https://bugs.launchpad.net/charm-helpers/+bug/1474030
 
365
                self.log.debug('Attempt {} to get {} proc start time on {} '
 
366
                               'failed'.format(tries, service, unit_name))
 
367
                time.sleep(retry_sleep_time)
 
368
                tries += 1
335
369
 
336
370
        if not proc_start_time:
337
371
            self.log.warn('No proc start time found, assuming service did '
338
372
                          'not start')
339
373
            return False
340
374
        if proc_start_time >= mtime:
341
 
            self.log.debug('proc start time is newer than provided mtime'
342
 
                           '(%s >= %s)' % (proc_start_time, mtime))
 
375
            self.log.debug('Proc start time is newer than provided mtime'
 
376
                           '(%s >= %s) on %s (OK)' % (proc_start_time,
 
377
                                                      mtime, unit_name))
343
378
            return True
344
379
        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))
 
380
            self.log.warn('Proc start time (%s) is older than provided mtime '
 
381
                          '(%s) on %s, service did not '
 
382
                          'restart' % (proc_start_time, mtime, unit_name))
348
383
            return False
349
384
 
350
385
    def config_updated_since(self, sentry_unit, filename, mtime,
374
409
            return False
375
410
 
376
411
    def validate_service_config_changed(self, sentry_unit, mtime, service,
377
 
                                        filename, pgrep_full=False,
378
 
                                        sleep_time=20, retry_count=2):
 
412
                                        filename, pgrep_full=None,
 
413
                                        sleep_time=20, retry_count=2,
 
414
                                        retry_sleep_time=30):
379
415
        """Check service and file were updated after mtime
380
416
 
381
417
        Args:
383
419
          mtime (float): The epoch time to check against
384
420
          service (string): service name to look for in process table
385
421
          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
 
422
          pgrep_full: [Deprecated] Use full command line search mode with pgrep
 
423
          sleep_time (int): Initial sleep in seconds to pass to test helpers
388
424
          retry_count (int): If service is not found, how many times to retry
 
425
          retry_sleep_time (int): Time in seconds to wait between retries
389
426
 
390
427
        Typical Usage:
391
428
            u = OpenStackAmuletUtils(ERROR)
402
439
                mtime, False if service is older than mtime or if service was
403
440
                not found or if filename was modified before mtime.
404
441
        """
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)
 
442
 
 
443
        # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
 
444
        # used instead of pgrep.  pgrep_full is still passed through to ensure
 
445
        # deprecation WARNS.  lp1474030
 
446
 
 
447
        service_restart = self.service_restarted_since(
 
448
            sentry_unit, mtime,
 
449
            service,
 
450
            pgrep_full=pgrep_full,
 
451
            sleep_time=sleep_time,
 
452
            retry_count=retry_count,
 
453
            retry_sleep_time=retry_sleep_time)
 
454
 
 
455
        config_update = self.config_updated_since(
 
456
            sentry_unit,
 
457
            filename,
 
458
            mtime,
 
459
            sleep_time=0)
 
460
 
414
461
        return service_restart and config_update
415
462
 
416
463
    def get_sentry_time(self, sentry_unit):
428
475
        """Return a list of all Ubuntu releases in order of release."""
429
476
        _d = distro_info.UbuntuDistroInfo()
430
477
        _release_list = _d.all
431
 
        self.log.debug('Ubuntu release list: {}'.format(_release_list))
432
478
        return _release_list
433
479
 
434
480
    def file_to_url(self, file_rel_path):
568
614
 
569
615
        return None
570
616
 
 
617
    def validate_sectionless_conf(self, file_contents, expected):
 
618
        """A crude conf parser.  Useful to inspect configuration files which
 
619
        do not have section headers (as would be necessary in order to use
 
620
        the configparser).  Such as openstack-dashboard or rabbitmq confs."""
 
621
        for line in file_contents.split('\n'):
 
622
            if '=' in line:
 
623
                args = line.split('=')
 
624
                if len(args) <= 1:
 
625
                    continue
 
626
                key = args[0].strip()
 
627
                value = args[1].strip()
 
628
                if key in expected.keys():
 
629
                    if expected[key] != value:
 
630
                        msg = ('Config mismatch.  Expected, actual:  {}, '
 
631
                               '{}'.format(expected[key], value))
 
632
                        amulet.raise_status(amulet.FAIL, msg=msg)
 
633
 
 
634
    def get_unit_hostnames(self, units):
 
635
        """Return a dict of juju unit names to hostnames."""
 
636
        host_names = {}
 
637
        for unit in units:
 
638
            host_names[unit.info['unit_name']] = \
 
639
                str(unit.file_contents('/etc/hostname').strip())
 
640
        self.log.debug('Unit host names: {}'.format(host_names))
 
641
        return host_names
 
642
 
 
643
    def run_cmd_unit(self, sentry_unit, cmd):
 
644
        """Run a command on a unit, return the output and exit code."""
 
645
        output, code = sentry_unit.run(cmd)
 
646
        if code == 0:
 
647
            self.log.debug('{} `{}` command returned {} '
 
648
                           '(OK)'.format(sentry_unit.info['unit_name'],
 
649
                                         cmd, code))
 
650
        else:
 
651
            msg = ('{} `{}` command returned {} '
 
652
                   '{}'.format(sentry_unit.info['unit_name'],
 
653
                               cmd, code, output))
 
654
            amulet.raise_status(amulet.FAIL, msg=msg)
 
655
        return str(output), code
 
656
 
 
657
    def file_exists_on_unit(self, sentry_unit, file_name):
 
658
        """Check if a file exists on a unit."""
 
659
        try:
 
660
            sentry_unit.file_stat(file_name)
 
661
            return True
 
662
        except IOError:
 
663
            return False
 
664
        except Exception as e:
 
665
            msg = 'Error checking file {}: {}'.format(file_name, e)
 
666
            amulet.raise_status(amulet.FAIL, msg=msg)
 
667
 
 
668
    def file_contents_safe(self, sentry_unit, file_name,
 
669
                           max_wait=60, fatal=False):
 
670
        """Get file contents from a sentry unit.  Wrap amulet file_contents
 
671
        with retry logic to address races where a file checks as existing,
 
672
        but no longer exists by the time file_contents is called.
 
673
        Return None if file not found. Optionally raise if fatal is True."""
 
674
        unit_name = sentry_unit.info['unit_name']
 
675
        file_contents = False
 
676
        tries = 0
 
677
        while not file_contents and tries < (max_wait / 4):
 
678
            try:
 
679
                file_contents = sentry_unit.file_contents(file_name)
 
680
            except IOError:
 
681
                self.log.debug('Attempt {} to open file {} from {} '
 
682
                               'failed'.format(tries, file_name,
 
683
                                               unit_name))
 
684
                time.sleep(4)
 
685
                tries += 1
 
686
 
 
687
        if file_contents:
 
688
            return file_contents
 
689
        elif not fatal:
 
690
            return None
 
691
        elif fatal:
 
692
            msg = 'Failed to get file contents from unit.'
 
693
            amulet.raise_status(amulet.FAIL, msg)
 
694
 
 
695
    def port_knock_tcp(self, host="localhost", port=22, timeout=15):
 
696
        """Open a TCP socket to check for a listening sevice on a host.
 
697
 
 
698
        :param host: host name or IP address, default to localhost
 
699
        :param port: TCP port number, default to 22
 
700
        :param timeout: Connect timeout, default to 15 seconds
 
701
        :returns: True if successful, False if connect failed
 
702
        """
 
703
 
 
704
        # Resolve host name if possible
 
705
        try:
 
706
            connect_host = socket.gethostbyname(host)
 
707
            host_human = "{} ({})".format(connect_host, host)
 
708
        except socket.error as e:
 
709
            self.log.warn('Unable to resolve address: '
 
710
                          '{} ({}) Trying anyway!'.format(host, e))
 
711
            connect_host = host
 
712
            host_human = connect_host
 
713
 
 
714
        # Attempt socket connection
 
715
        try:
 
716
            knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
717
            knock.settimeout(timeout)
 
718
            knock.connect((connect_host, port))
 
719
            knock.close()
 
720
            self.log.debug('Socket connect OK for host '
 
721
                           '{} on port {}.'.format(host_human, port))
 
722
            return True
 
723
        except socket.error as e:
 
724
            self.log.debug('Socket connect FAIL for'
 
725
                           ' {} port {} ({})'.format(host_human, port, e))
 
726
            return False
 
727
 
 
728
    def port_knock_units(self, sentry_units, port=22,
 
729
                         timeout=15, expect_success=True):
 
730
        """Open a TCP socket to check for a listening sevice on each
 
731
        listed juju unit.
 
732
 
 
733
        :param sentry_units: list of sentry unit pointers
 
734
        :param port: TCP port number, default to 22
 
735
        :param timeout: Connect timeout, default to 15 seconds
 
736
        :expect_success: True by default, set False to invert logic
 
737
        :returns: None if successful, Failure message otherwise
 
738
        """
 
739
        for unit in sentry_units:
 
740
            host = unit.info['public-address']
 
741
            connected = self.port_knock_tcp(host, port, timeout)
 
742
            if not connected and expect_success:
 
743
                return 'Socket connect failed.'
 
744
            elif connected and not expect_success:
 
745
                return 'Socket connected unexpectedly.'
 
746
 
 
747
    def get_uuid_epoch_stamp(self):
 
748
        """Returns a stamp string based on uuid4 and epoch time.  Useful in
 
749
        generating test messages which need to be unique-ish."""
 
750
        return '[{}-{}]'.format(uuid.uuid4(), time.time())
 
751
 
 
752
# amulet juju action helpers:
571
753
    def run_action(self, unit_sentry, action,
572
754
                   _check_output=subprocess.check_output):
573
755
        """Run the named action on a given unit sentry.