1
# Copyright 2014-2015 Canonical Limited.
3
# This file is part of charm-helpers.
5
# charm-helpers is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License version 3 as
7
# published by the Free Software Foundation.
9
# charm-helpers is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU Lesser General Public License for more details.
14
# You should have received a copy of the GNU Lesser General Public License
15
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
31
from six.moves import configparser
33
from urllib import parse as urlparse
38
class AmuletUtils(object):
41
This class provides common utility functions that are used by Amulet
45
def __init__(self, log_level=logging.ERROR):
46
self.log = self.get_logger(level=log_level)
47
self.ubuntu_releases = self.get_ubuntu_releases()
49
def get_logger(self, name="amulet-logger", level=logging.DEBUG):
50
"""Get a logger object that will log to stdout."""
52
logger = log.getLogger(name)
53
fmt = log.Formatter("%(asctime)s %(funcName)s "
54
"%(levelname)s: %(message)s")
56
handler = log.StreamHandler(stream=sys.stdout)
57
handler.setLevel(level)
58
handler.setFormatter(fmt)
60
logger.addHandler(handler)
61
logger.setLevel(level)
65
def valid_ip(self, ip):
66
if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
71
def valid_url(self, url):
74
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # noqa
76
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
85
def get_ubuntu_release_from_sentry(self, sentry_unit):
86
"""Get Ubuntu release codename from sentry unit.
88
:param sentry_unit: amulet sentry/service unit pointer
89
:returns: list of strings - release codename, failure message
92
cmd = 'lsb_release -cs'
93
release, code = sentry_unit.run(cmd)
95
self.log.debug('{} lsb_release: {}'.format(
96
sentry_unit.info['unit_name'], release))
98
msg = ('{} `{}` returned {} '
99
'{}'.format(sentry_unit.info['unit_name'],
101
if release not in self.ubuntu_releases:
102
msg = ("Release ({}) not found in Ubuntu releases "
103
"({})".format(release, self.ubuntu_releases))
106
def validate_services(self, commands):
107
"""Validate that lists of commands succeed on service units. Can be
108
used to verify system services are running on the corresponding
111
:param commands: dict with sentry keys and arbitrary command list vals
112
:returns: None if successful, Failure string message otherwise
114
self.log.debug('Checking status of system services...')
116
# /!\ DEPRECATION WARNING (beisner):
117
# New and existing tests should be rewritten to use
118
# validate_services_by_name() as it is aware of init systems.
119
self.log.warn('DEPRECATION WARNING: use '
120
'validate_services_by_name instead of validate_services '
121
'due to init system differences.')
123
for k, v in six.iteritems(commands):
125
output, code = k.run(cmd)
126
self.log.debug('{} `{}` returned '
127
'{}'.format(k.info['unit_name'],
130
return "command `{}` returned {}".format(cmd, str(code))
133
def validate_services_by_name(self, sentry_services):
134
"""Validate system service status by service name, automatically
135
detecting init system based on Ubuntu release codename.
137
:param sentry_services: dict with sentry keys and svc list values
138
:returns: None if successful, Failure string message otherwise
140
self.log.debug('Checking status of system services...')
142
# Point at which systemd became a thing
143
systemd_switch = self.ubuntu_releases.index('vivid')
145
for sentry_unit, services_list in six.iteritems(sentry_services):
146
# Get lsb_release codename from unit
147
release, ret = self.get_ubuntu_release_from_sentry(sentry_unit)
151
for service_name in services_list:
152
if (self.ubuntu_releases.index(release) >= systemd_switch or
153
service_name in ['rabbitmq-server', 'apache2']):
154
# init is systemd (or regular sysv)
155
cmd = 'sudo service {} status'.format(service_name)
156
output, code = sentry_unit.run(cmd)
157
service_running = code == 0
158
elif self.ubuntu_releases.index(release) < systemd_switch:
160
cmd = 'sudo status {}'.format(service_name)
161
output, code = sentry_unit.run(cmd)
162
service_running = code == 0 and "start/running" in output
164
self.log.debug('{} `{}` returned '
165
'{}'.format(sentry_unit.info['unit_name'],
167
if not service_running:
168
return u"command `{}` returned {} {}".format(
169
cmd, output, str(code))
172
def _get_config(self, unit, filename):
173
"""Get a ConfigParser object for parsing a unit's config file."""
174
file_contents = unit.file_contents(filename)
176
# NOTE(beisner): by default, ConfigParser does not handle options
177
# with no value, such as the flags used in the mysql my.cnf file.
178
# https://bugs.python.org/issue7005
179
config = configparser.ConfigParser(allow_no_value=True)
180
config.readfp(io.StringIO(file_contents))
183
def validate_config_data(self, sentry_unit, config_file, section,
185
"""Validate config file data.
187
Verify that the specified section of the config file contains
188
the expected option key:value pairs.
190
Compare expected dictionary data vs actual dictionary data.
191
The values in the 'expected' dictionary can be strings, bools, ints,
192
longs, or can be a function that evaluates a variable and returns a
195
self.log.debug('Validating config file data ({} in {} on {})'
196
'...'.format(section, config_file,
197
sentry_unit.info['unit_name']))
198
config = self._get_config(sentry_unit, config_file)
200
if section != 'DEFAULT' and not config.has_section(section):
201
return "section [{}] does not exist".format(section)
203
for k in expected.keys():
204
if not config.has_option(section, k):
205
return "section [{}] is missing option {}".format(section, k)
207
actual = config.get(section, k)
209
if (isinstance(v, six.string_types) or
210
isinstance(v, bool) or
211
isinstance(v, six.integer_types)):
212
# handle explicit values
214
return "section [{}] {}:{} != expected {}:{}".format(
215
section, k, actual, k, expected[k])
216
# handle function pointers, such as not_null or valid_ip
218
return "section [{}] {}:{} != expected {}:{}".format(
219
section, k, actual, k, expected[k])
222
def _validate_dict_data(self, expected, actual):
223
"""Validate dictionary data.
225
Compare expected dictionary data vs actual dictionary data.
226
The values in the 'expected' dictionary can be strings, bools, ints,
227
longs, or can be a function that evaluates a variable and returns a
230
self.log.debug('actual: {}'.format(repr(actual)))
231
self.log.debug('expected: {}'.format(repr(expected)))
233
for k, v in six.iteritems(expected):
235
if (isinstance(v, six.string_types) or
236
isinstance(v, bool) or
237
isinstance(v, six.integer_types)):
238
# handle explicit values
240
return "{}:{}".format(k, actual[k])
241
# handle function pointers, such as not_null or valid_ip
242
elif not v(actual[k]):
243
return "{}:{}".format(k, actual[k])
245
return "key '{}' does not exist".format(k)
248
def validate_relation_data(self, sentry_unit, relation, expected):
249
"""Validate actual relation data based on expected relation data."""
250
actual = sentry_unit.relation(relation[0], relation[1])
251
return self._validate_dict_data(expected, actual)
253
def _validate_list_data(self, expected, actual):
254
"""Compare expected list vs actual list data."""
257
return "expected item {} not found in actual list".format(e)
260
def not_null(self, string):
261
if string is not None:
266
def _get_file_mtime(self, sentry_unit, filename):
267
"""Get last modification time of file."""
268
return sentry_unit.file_stat(filename)['mtime']
270
def _get_dir_mtime(self, sentry_unit, directory):
271
"""Get last modification time of directory."""
272
return sentry_unit.directory_stat(directory)['mtime']
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)
301
def service_restarted(self, sentry_unit, service, filename,
302
pgrep_full=None, sleep_time=20):
303
"""Check if service was restarted.
305
Compare a service's start time vs a file's last modification time
306
(such as a config file for that service) to determine if the service
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.')
320
time.sleep(sleep_time)
321
if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
322
self._get_file_mtime(sentry_unit, filename)):
327
def service_restarted_since(self, sentry_unit, mtime, service,
328
pgrep_full=None, sleep_time=20,
329
retry_count=30, retry_sleep_time=10):
330
"""Check if service was been started after a given time.
333
sentry_unit (sentry): The sentry unit to check for the service on
334
mtime (float): The epoch time to check against
335
service (string): service name to look for in process table
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
342
bool: True if service found and its start time it newer than mtime,
343
False if service is older than mtime or if service was
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))
353
time.sleep(sleep_time)
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)
372
if not proc_start_time:
373
self.log.warn('No proc start time found, assuming service did '
376
if 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,
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))
387
def config_updated_since(self, sentry_unit, filename, mtime,
388
sleep_time=20, retry_count=30,
389
retry_sleep_time=10):
390
"""Check if file was modified after a given time.
393
sentry_unit (sentry): The sentry unit to check the file mtime on
394
filename (string): The file to check mtime of
395
mtime (float): The epoch time to check against
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
401
bool: True if file was modified more recently than mtime, False if
402
file was modified before mtime, or if file not found.
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))
407
time.sleep(sleep_time)
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')
429
if file_mtime >= mtime:
430
self.log.debug('File mtime is newer than provided mtime '
431
'(%s >= %s) on %s (OK)' % (file_mtime,
435
self.log.warn('File mtime is older than provided mtime'
436
'(%s < on %s) on %s' % (file_mtime,
440
def validate_service_config_changed(self, sentry_unit, mtime, service,
441
filename, pgrep_full=None,
442
sleep_time=20, retry_count=30,
443
retry_sleep_time=10):
444
"""Check service and file were updated after mtime
447
sentry_unit (sentry): The sentry unit to check for the service on
448
mtime (float): The epoch time to check against
449
service (string): service name to look for in process table
450
filename (string): The file to check mtime of
451
pgrep_full: [Deprecated] Use full command line search mode with pgrep
452
sleep_time (int): Initial sleep in seconds to pass to test helpers
453
retry_count (int): If service is not found, how many times to retry
454
retry_sleep_time (int): Time in seconds to wait between retries
457
u = OpenStackAmuletUtils(ERROR)
459
mtime = u.get_sentry_time(self.cinder_sentry)
460
self.d.configure('cinder', {'verbose': 'True', 'debug': 'True'})
461
if not u.validate_service_config_changed(self.cinder_sentry,
464
'/etc/cinder/cinder.conf')
465
amulet.raise_status(amulet.FAIL, msg='update failed')
467
bool: True if both service and file where updated/restarted after
468
mtime, False if service is older than mtime or if service was
469
not found or if filename was modified before mtime.
472
# NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
473
# used instead of pgrep. pgrep_full is still passed through to ensure
474
# deprecation WARNS. lp1474030
476
service_restart = self.service_restarted_since(
479
pgrep_full=pgrep_full,
480
sleep_time=sleep_time,
481
retry_count=retry_count,
482
retry_sleep_time=retry_sleep_time)
484
config_update = self.config_updated_since(
488
sleep_time=sleep_time,
489
retry_count=retry_count,
490
retry_sleep_time=retry_sleep_time)
492
return service_restart and config_update
494
def get_sentry_time(self, sentry_unit):
495
"""Return current epoch time on a sentry"""
497
return float(sentry_unit.run(cmd)[0])
499
def relation_error(self, name, data):
500
return 'unexpected relation data in {} - {}'.format(name, data)
502
def endpoint_error(self, name, data):
503
return 'unexpected endpoint data in {} - {}'.format(name, data)
505
def get_ubuntu_releases(self):
506
"""Return a list of all Ubuntu releases in order of release."""
507
_d = distro_info.UbuntuDistroInfo()
508
_release_list = _d.all
511
def file_to_url(self, file_rel_path):
512
"""Convert a relative file path to a file URL."""
513
_abs_path = os.path.abspath(file_rel_path)
514
return urlparse.urlparse(_abs_path, scheme='file').geturl()
516
def check_commands_on_units(self, commands, sentry_units):
517
"""Check that all commands in a list exit zero on all
518
sentry units in a list.
520
:param commands: list of bash commands
521
:param sentry_units: list of sentry unit pointers
522
:returns: None if successful; Failure message otherwise
524
self.log.debug('Checking exit codes for {} commands on {} '
525
'sentry units...'.format(len(commands),
527
for sentry_unit in sentry_units:
529
output, code = sentry_unit.run(cmd)
531
self.log.debug('{} `{}` returned {} '
532
'(OK)'.format(sentry_unit.info['unit_name'],
535
return ('{} `{}` returned {} '
536
'{}'.format(sentry_unit.info['unit_name'],
540
def get_process_id_list(self, sentry_unit, process_name,
541
expect_success=True):
542
"""Get a list of process ID(s) from a single sentry juju unit
543
for a single process name.
545
:param sentry_unit: Amulet sentry instance (juju unit)
546
:param process_name: Process name
547
:param expect_success: If False, expect the PID to be missing,
548
raise if it is present.
549
:returns: List of process IDs
551
cmd = 'pidof -x {}'.format(process_name)
552
if not expect_success:
553
cmd += " || exit 0 && exit 1"
554
output, code = sentry_unit.run(cmd)
556
msg = ('{} `{}` returned {} '
557
'{}'.format(sentry_unit.info['unit_name'],
559
amulet.raise_status(amulet.FAIL, msg=msg)
560
return str(output).split()
562
def get_unit_process_ids(self, unit_processes, expect_success=True):
563
"""Construct a dict containing unit sentries, process names, and
566
:param unit_processes: A dictionary of Amulet sentry instance
567
to list of process names.
568
:param expect_success: if False expect the processes to not be
569
running, raise if they are.
570
:returns: Dictionary of Amulet sentry instance to dictionary
571
of process names to PIDs.
574
for sentry_unit, process_list in six.iteritems(unit_processes):
575
pid_dict[sentry_unit] = {}
576
for process in process_list:
577
pids = self.get_process_id_list(
578
sentry_unit, process, expect_success=expect_success)
579
pid_dict[sentry_unit].update({process: pids})
582
def validate_unit_process_ids(self, expected, actual):
583
"""Validate process id quantities for services on units."""
584
self.log.debug('Checking units for running processes...')
585
self.log.debug('Expected PIDs: {}'.format(expected))
586
self.log.debug('Actual PIDs: {}'.format(actual))
588
if len(actual) != len(expected):
589
return ('Unit count mismatch. expected, actual: {}, '
590
'{} '.format(len(expected), len(actual)))
592
for (e_sentry, e_proc_names) in six.iteritems(expected):
593
e_sentry_name = e_sentry.info['unit_name']
594
if e_sentry in actual.keys():
595
a_proc_names = actual[e_sentry]
597
return ('Expected sentry ({}) not found in actual dict data.'
598
'{}'.format(e_sentry_name, e_sentry))
600
if len(e_proc_names.keys()) != len(a_proc_names.keys()):
601
return ('Process name count mismatch. expected, actual: {}, '
602
'{}'.format(len(expected), len(actual)))
604
for (e_proc_name, e_pids_length), (a_proc_name, a_pids) in \
605
zip(e_proc_names.items(), a_proc_names.items()):
606
if e_proc_name != a_proc_name:
607
return ('Process name mismatch. expected, actual: {}, '
608
'{}'.format(e_proc_name, a_proc_name))
610
a_pids_length = len(a_pids)
611
fail_msg = ('PID count mismatch. {} ({}) expected, actual: '
612
'{}, {} ({})'.format(e_sentry_name, e_proc_name,
613
e_pids_length, a_pids_length,
616
# If expected is not bool, ensure PID quantities match
617
if not isinstance(e_pids_length, bool) and \
618
a_pids_length != e_pids_length:
620
# If expected is bool True, ensure 1 or more PIDs exist
621
elif isinstance(e_pids_length, bool) and \
622
e_pids_length is True and a_pids_length < 1:
624
# If expected is bool False, ensure 0 PIDs exist
625
elif isinstance(e_pids_length, bool) and \
626
e_pids_length is False and a_pids_length != 0:
629
self.log.debug('PID check OK: {} {} {}: '
630
'{}'.format(e_sentry_name, e_proc_name,
631
e_pids_length, a_pids))
634
def validate_list_of_identical_dicts(self, list_of_dicts):
635
"""Check that all dicts within a list are identical."""
637
for _dict in list_of_dicts:
638
hashes.append(hash(frozenset(_dict.items())))
640
self.log.debug('Hashes: {}'.format(hashes))
641
if len(set(hashes)) == 1:
642
self.log.debug('Dicts within list are identical')
644
return 'Dicts within list are not identical'
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:
784
def run_action(self, unit_sentry, action,
785
_check_output=subprocess.check_output):
786
"""Run the named action on a given unit sentry.
788
_check_output parameter is used for dependency injection.
792
unit_id = unit_sentry.info["unit_name"]
793
command = ["juju", "action", "do", "--format=json", unit_id, action]
794
self.log.info("Running command: %s\n" % " ".join(command))
795
output = _check_output(command, universal_newlines=True)
796
data = json.loads(output)
797
action_id = data[u'Action queued with id']
800
def wait_on_action(self, action_id, _check_output=subprocess.check_output):
801
"""Wait for a given action, returning if it completed or not.
803
_check_output parameter is used for dependency injection.
805
command = ["juju", "action", "fetch", "--format=json", "--wait=0",
807
output = _check_output(command, universal_newlines=True)
808
data = json.loads(output)
809
return data.get(u"status") == "completed"
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")
816
return ("unknown", "")
817
status = json.loads(raw_status)
818
return (status["status"], status["message"])