269
271
"""Get last modification time of directory."""
270
272
return sentry_unit.directory_stat(directory)['mtime']
272
def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):
273
"""Get process' start time.
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.
280
cmd = 'pgrep -o -f {}'.format(service)
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))
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.
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
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.')
293
pid_list = self.get_process_id_list(sentry_unit, service)
295
proc_dir = '/proc/{}'.format(pid)
296
self.log.debug('Pid for {} on {}: {}'.format(
297
service, sentry_unit.info['unit_name'], pid))
299
return self._get_dir_mtime(sentry_unit, proc_dir)
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.
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.
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.
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.')
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)):
306
327
def service_restarted_since(self, sentry_unit, mtime, service,
307
pgrep_full=False, sleep_time=20,
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.
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
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
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
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,
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))
332
proc_start_time = self._get_proc_start_time(sentry_unit, service,
334
retry_count = retry_count - 1
354
proc_start_time = None
356
while tries <= retry_count and not proc_start_time:
358
proc_start_time = self._get_proc_start_time(sentry_unit,
361
self.log.debug('Attempt {} to get {} proc start time on {} '
362
'OK'.format(tries, service, unit_name))
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,
369
time.sleep(retry_sleep_time)
336
372
if not proc_start_time:
337
373
self.log.warn('No proc start time found, assuming service did '
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,
345
self.log.warn('proc start time (%s) is older than provided mtime '
346
'(%s), service did not restart' % (proc_start_time,
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))
350
387
def config_updated_since(self, sentry_unit, filename, mtime,
388
sleep_time=20, retry_count=30,
389
retry_sleep_time=10):
352
390
"""Check if file was modified after a given time.
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
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.
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)
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')
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,
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,
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
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:
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.