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)):
321
342
False if service is older than mtime or if service was
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
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,
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
353
proc_start_time = None
355
while tries <= retry_count and not proc_start_time:
357
proc_start_time = self._get_proc_start_time(sentry_unit,
360
self.log.debug('Attempt {} to get {} proc start time on {} '
361
'OK'.format(tries, service, unit_name))
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)
336
370
if not proc_start_time:
337
371
self.log.warn('No proc start time found, assuming service did '
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,
345
self.log.warn('proc start time (%s) is older than provided mtime '
346
'(%s), service did not restart' % (proc_start_time,
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))
350
385
def config_updated_since(self, sentry_unit, filename, mtime,
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'):
623
args = line.split('=')
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)
634
def get_unit_hostnames(self, units):
635
"""Return a dict of juju unit names to hostnames."""
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))
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)
647
self.log.debug('{} `{}` command returned {} '
648
'(OK)'.format(sentry_unit.info['unit_name'],
651
msg = ('{} `{}` command returned {} '
652
'{}'.format(sentry_unit.info['unit_name'],
654
amulet.raise_status(amulet.FAIL, msg=msg)
655
return str(output), code
657
def file_exists_on_unit(self, sentry_unit, file_name):
658
"""Check if a file exists on a unit."""
660
sentry_unit.file_stat(file_name)
664
except Exception as e:
665
msg = 'Error checking file {}: {}'.format(file_name, e)
666
amulet.raise_status(amulet.FAIL, msg=msg)
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
677
while not file_contents and tries < (max_wait / 4):
679
file_contents = sentry_unit.file_contents(file_name)
681
self.log.debug('Attempt {} to open file {} from {} '
682
'failed'.format(tries, file_name,
692
msg = 'Failed to get file contents from unit.'
693
amulet.raise_status(amulet.FAIL, msg)
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.
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
704
# Resolve host name if possible
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))
712
host_human = connect_host
714
# Attempt socket connection
716
knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
717
knock.settimeout(timeout)
718
knock.connect((connect_host, port))
720
self.log.debug('Socket connect OK for host '
721
'{} on port {}.'.format(host_human, port))
723
except socket.error as e:
724
self.log.debug('Socket connect FAIL for'
725
' {} port {} ({})'.format(host_human, port, e))
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
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
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.'
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())
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.