383
387
def config_updated_since(self, sentry_unit, filename, mtime,
388
sleep_time=20, retry_count=30,
389
retry_sleep_time=10):
385
390
"""Check if file was modified after a given time.
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
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.
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)
410
while tries <= retry_count and not file_mtime:
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))
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,
421
time.sleep(retry_sleep_time)
425
self.log.warn('Could not determine file mtime, assuming '
426
'file does not exist')
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,
431
'(%s >= %s) on %s (OK)' % (file_mtime,
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,
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
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'):
654
args = line.split('=')
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)
665
def get_unit_hostnames(self, units):
666
"""Return a dict of juju unit names to hostnames."""
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))
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)
678
self.log.debug('{} `{}` command returned {} '
679
'(OK)'.format(sentry_unit.info['unit_name'],
682
msg = ('{} `{}` command returned {} '
683
'{}'.format(sentry_unit.info['unit_name'],
685
amulet.raise_status(amulet.FAIL, msg=msg)
686
return str(output), code
688
def file_exists_on_unit(self, sentry_unit, file_name):
689
"""Check if a file exists on a unit."""
691
sentry_unit.file_stat(file_name)
695
except Exception as e:
696
msg = 'Error checking file {}: {}'.format(file_name, e)
697
amulet.raise_status(amulet.FAIL, msg=msg)
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
708
while not file_contents and tries < (max_wait / 4):
710
file_contents = sentry_unit.file_contents(file_name)
712
self.log.debug('Attempt {} to open file {} from {} '
713
'failed'.format(tries, file_name,
723
msg = 'Failed to get file contents from unit.'
724
amulet.raise_status(amulet.FAIL, msg)
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.
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
735
# Resolve host name if possible
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))
743
host_human = connect_host
745
# Attempt socket connection
747
knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
748
knock.settimeout(timeout)
749
knock.connect((connect_host, port))
751
self.log.debug('Socket connect OK for host '
752
'{} on port {}.'.format(host_human, port))
754
except socket.error as e:
755
self.log.debug('Socket connect FAIL for'
756
' {} port {} ({})'.format(host_human, port, e))
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
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
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.'
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())
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.