Merge lp:~1chb1n/charms/trusty/ubuntu/amulet-wily-enable into lp:charms/trusty/ubuntu

Proposed by Ryan Beisner on 2015-07-27
Status: Merged
Merged at revision: 12
Proposed branch: lp:~1chb1n/charms/trusty/ubuntu/amulet-wily-enable
Merge into: lp:charms/trusty/ubuntu
Diff against target: 387 lines (+241/-33)
5 files modified
tests/020_basic_utopic (+0/-7)
tests/030_basic_wily (+7/-0)
tests/basic_deployment.py (+12/-17)
tests/charmhelpers/contrib/amulet/utils.py (+219/-9)
tests/tests.yaml (+3/-0)
To merge this branch: bzr merge lp:~1chb1n/charms/trusty/ubuntu/amulet-wily-enable
Reviewer Review Type Date Requested Status
Tim Van Steenburgh 2015-07-27 Approve on 2015-07-28
Review via email: mp+265974@code.launchpad.net

Commit message

Add Wily functional test target; Remove EOL Utopic functional test; Refactor functional test to use helper function; Sync tests/charmhelpers.

Description of the change

Add Wily functional test target; Remove EOL Utopic functional test; Refactor functional test to use helper function; Sync tests/charmhelpers.

To post a comment you must log in.

charm_lint_check #7057 ubuntu for 1chb1n mp265974
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/7057/

charm_amulet_test #5392 ubuntu for 1chb1n mp265974
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
make: *** [functional_test] Error 1
ERROR:root:Make target returned non-zero.

Full amulet test output: http://paste.ubuntu.com/11948816/
Build: http://10.245.162.77:8080/job/charm_amulet_test/5392/

Ryan Beisner (1chb1n) wrote :

^ environment issue, re-kicking job...

charm_amulet_test #5393 ubuntu for 1chb1n mp265974
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/5393/

charm_amulet_test #5399 ubuntu for 1chb1n mp265974
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/5399/

14. By Ryan Beisner on 2015-07-27

remove utopic functional test, eol

charm_lint_check #7063 ubuntu for 1chb1n mp265974
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/7063/

charm_amulet_test #5400 ubuntu for 1chb1n mp265974
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/5400/

15. By Ryan Beisner on 2015-07-28

trivial comment update

charm_lint_check #7092 ubuntu for 1chb1n mp265974
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/7092/

charm_amulet_test #5428 ubuntu for 1chb1n mp265974
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/5428/

Tim Van Steenburgh (tvansteenburgh) wrote :

LGTM.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed file 'tests/020_basic_utopic'
2--- tests/020_basic_utopic 2015-04-29 15:28:48 +0000
3+++ tests/020_basic_utopic 1970-01-01 00:00:00 +0000
4@@ -1,7 +0,0 @@
5-#!/usr/bin/python
6-"""Amulet tests on a basic ubuntu charm deployment on utopic."""
7-
8-from basic_deployment import ubuntu_basic_deployment
9-
10-if __name__ == '__main__':
11- ubuntu_basic_deployment(series='utopic')
12
13=== added file 'tests/030_basic_wily'
14--- tests/030_basic_wily 1970-01-01 00:00:00 +0000
15+++ tests/030_basic_wily 2015-07-28 01:35:52 +0000
16@@ -0,0 +1,7 @@
17+#!/usr/bin/python
18+"""Amulet tests on a basic ubuntu charm deployment on wily."""
19+
20+from basic_deployment import ubuntu_basic_deployment
21+
22+if __name__ == '__main__':
23+ ubuntu_basic_deployment(series='wily')
24
25=== modified file 'tests/basic_deployment.py'
26--- tests/basic_deployment.py 2015-04-29 15:28:48 +0000
27+++ tests/basic_deployment.py 2015-07-28 01:35:52 +0000
28@@ -18,32 +18,27 @@
29 u = AmuletUtils(logging.DEBUG)
30 d = amulet.Deployment(series=series)
31 d.add('ubuntu')
32- unit = 'ubuntu/0'
33- lsb_command = 'lsb_release -cs'
34
35 # Deploy services, wait for started state. Fail or skip on timeout.
36 try:
37 d.setup(timeout=seconds)
38+ sentry_unit = d.sentry.unit['ubuntu/0']
39 except amulet.helpers.TimeoutError:
40 message = 'Deployment timed out ({}s)'.format(seconds)
41- amulet.raise_status(amulet.SKIP, msg=message)
42+ amulet.raise_status(amulet.FAIL, msg=message)
43 except:
44 raise
45
46 # Confirm Ubuntu release name from the unit.
47- u.log.debug('Command: {}'.format(lsb_command))
48- output, code = d.sentry.unit[unit].run(lsb_command)
49- u.log.debug('Output: {}'.format(output))
50+ release, ret = u.get_ubuntu_release_from_sentry(sentry_unit)
51+ if ret:
52+ # Something went wrong trying to query the unit, or it is an
53+ # unknown/alien release name based on distro-info validation.
54+ amulet.raise_status(amulet.FAIL, msg=ret)
55
56- if (code != 0):
57- message = 'Command FAIL: {}'.format(lsb_command)
58- u.log.error(message)
59- amulet.raise_status(amulet.FAIL, msg=message)
60+ if release == series:
61+ u.log.info('Release/series check: OK')
62 else:
63- if series in output:
64- message = 'Series: OK'
65- u.log.info(message)
66- else:
67- message = 'Series: FAIL ({})'.format(output)
68- u.log.error(message)
69- amulet.raise_status(amulet.FAIL, msg=message)
70+ msg = 'Release/series check: FAIL ({} != {})'.format(release, series)
71+ u.log.error(msg)
72+ amulet.raise_status(amulet.FAIL, msg=msg)
73
74=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
75--- tests/charmhelpers/contrib/amulet/utils.py 2015-04-29 15:29:06 +0000
76+++ tests/charmhelpers/contrib/amulet/utils.py 2015-07-28 01:35:52 +0000
77@@ -14,14 +14,17 @@
78 # You should have received a copy of the GNU Lesser General Public License
79 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
80
81+import amulet
82 import ConfigParser
83+import distro_info
84 import io
85 import logging
86+import os
87 import re
88+import six
89 import sys
90 import time
91-
92-import six
93+import urlparse
94
95
96 class AmuletUtils(object):
97@@ -33,6 +36,7 @@
98
99 def __init__(self, log_level=logging.ERROR):
100 self.log = self.get_logger(level=log_level)
101+ self.ubuntu_releases = self.get_ubuntu_releases()
102
103 def get_logger(self, name="amulet-logger", level=logging.DEBUG):
104 """Get a logger object that will log to stdout."""
105@@ -70,12 +74,44 @@
106 else:
107 return False
108
109+ def get_ubuntu_release_from_sentry(self, sentry_unit):
110+ """Get Ubuntu release codename from sentry unit.
111+
112+ :param sentry_unit: amulet sentry/service unit pointer
113+ :returns: list of strings - release codename, failure message
114+ """
115+ msg = None
116+ cmd = 'lsb_release -cs'
117+ release, code = sentry_unit.run(cmd)
118+ if code == 0:
119+ self.log.debug('{} lsb_release: {}'.format(
120+ sentry_unit.info['unit_name'], release))
121+ else:
122+ msg = ('{} `{}` returned {} '
123+ '{}'.format(sentry_unit.info['unit_name'],
124+ cmd, release, code))
125+ if release not in self.ubuntu_releases:
126+ msg = ("Release ({}) not found in Ubuntu releases "
127+ "({})".format(release, self.ubuntu_releases))
128+ return release, msg
129+
130 def validate_services(self, commands):
131- """Validate services.
132-
133- Verify the specified services are running on the corresponding
134+ """Validate that lists of commands succeed on service units. Can be
135+ used to verify system services are running on the corresponding
136 service units.
137- """
138+
139+ :param commands: dict with sentry keys and arbitrary command list vals
140+ :returns: None if successful, Failure string message otherwise
141+ """
142+ self.log.debug('Checking status of system services...')
143+
144+ # /!\ DEPRECATION WARNING (beisner):
145+ # New and existing tests should be rewritten to use
146+ # validate_services_by_name() as it is aware of init systems.
147+ self.log.warn('/!\\ DEPRECATION WARNING: use '
148+ 'validate_services_by_name instead of validate_services '
149+ 'due to init system differences.')
150+
151 for k, v in six.iteritems(commands):
152 for cmd in v:
153 output, code = k.run(cmd)
154@@ -86,6 +122,41 @@
155 return "command `{}` returned {}".format(cmd, str(code))
156 return None
157
158+ def validate_services_by_name(self, sentry_services):
159+ """Validate system service status by service name, automatically
160+ detecting init system based on Ubuntu release codename.
161+
162+ :param sentry_services: dict with sentry keys and svc list values
163+ :returns: None if successful, Failure string message otherwise
164+ """
165+ self.log.debug('Checking status of system services...')
166+
167+ # Point at which systemd became a thing
168+ systemd_switch = self.ubuntu_releases.index('vivid')
169+
170+ for sentry_unit, services_list in six.iteritems(sentry_services):
171+ # Get lsb_release codename from unit
172+ release, ret = self.get_ubuntu_release_from_sentry(sentry_unit)
173+ if ret:
174+ return ret
175+
176+ for service_name in services_list:
177+ if (self.ubuntu_releases.index(release) >= systemd_switch or
178+ service_name == "rabbitmq-server"):
179+ # init is systemd
180+ cmd = 'sudo service {} status'.format(service_name)
181+ elif self.ubuntu_releases.index(release) < systemd_switch:
182+ # init is upstart
183+ cmd = 'sudo status {}'.format(service_name)
184+
185+ output, code = sentry_unit.run(cmd)
186+ self.log.debug('{} `{}` returned '
187+ '{}'.format(sentry_unit.info['unit_name'],
188+ cmd, code))
189+ if code != 0:
190+ return "command `{}` returned {}".format(cmd, str(code))
191+ return None
192+
193 def _get_config(self, unit, filename):
194 """Get a ConfigParser object for parsing a unit's config file."""
195 file_contents = unit.file_contents(filename)
196@@ -103,7 +174,15 @@
197
198 Verify that the specified section of the config file contains
199 the expected option key:value pairs.
200+
201+ Compare expected dictionary data vs actual dictionary data.
202+ The values in the 'expected' dictionary can be strings, bools, ints,
203+ longs, or can be a function that evaluates a variable and returns a
204+ bool.
205 """
206+ self.log.debug('Validating config file data ({} in {} on {})'
207+ '...'.format(section, config_file,
208+ sentry_unit.info['unit_name']))
209 config = self._get_config(sentry_unit, config_file)
210
211 if section != 'DEFAULT' and not config.has_section(section):
212@@ -112,9 +191,20 @@
213 for k in expected.keys():
214 if not config.has_option(section, k):
215 return "section [{}] is missing option {}".format(section, k)
216- if config.get(section, k) != expected[k]:
217+
218+ actual = config.get(section, k)
219+ v = expected[k]
220+ if (isinstance(v, six.string_types) or
221+ isinstance(v, bool) or
222+ isinstance(v, six.integer_types)):
223+ # handle explicit values
224+ if actual != v:
225+ return "section [{}] {}:{} != expected {}:{}".format(
226+ section, k, actual, k, expected[k])
227+ # handle function pointers, such as not_null or valid_ip
228+ elif not v(actual):
229 return "section [{}] {}:{} != expected {}:{}".format(
230- section, k, config.get(section, k), k, expected[k])
231+ section, k, actual, k, expected[k])
232 return None
233
234 def _validate_dict_data(self, expected, actual):
235@@ -122,7 +212,7 @@
236
237 Compare expected dictionary data vs actual dictionary data.
238 The values in the 'expected' dictionary can be strings, bools, ints,
239- longs, or can be a function that evaluate a variable and returns a
240+ longs, or can be a function that evaluates a variable and returns a
241 bool.
242 """
243 self.log.debug('actual: {}'.format(repr(actual)))
244@@ -133,8 +223,10 @@
245 if (isinstance(v, six.string_types) or
246 isinstance(v, bool) or
247 isinstance(v, six.integer_types)):
248+ # handle explicit values
249 if v != actual[k]:
250 return "{}:{}".format(k, actual[k])
251+ # handle function pointers, such as not_null or valid_ip
252 elif not v(actual[k]):
253 return "{}:{}".format(k, actual[k])
254 else:
255@@ -321,3 +413,121 @@
256
257 def endpoint_error(self, name, data):
258 return 'unexpected endpoint data in {} - {}'.format(name, data)
259+
260+ def get_ubuntu_releases(self):
261+ """Return a list of all Ubuntu releases in order of release."""
262+ _d = distro_info.UbuntuDistroInfo()
263+ _release_list = _d.all
264+ self.log.debug('Ubuntu release list: {}'.format(_release_list))
265+ return _release_list
266+
267+ def file_to_url(self, file_rel_path):
268+ """Convert a relative file path to a file URL."""
269+ _abs_path = os.path.abspath(file_rel_path)
270+ return urlparse.urlparse(_abs_path, scheme='file').geturl()
271+
272+ def check_commands_on_units(self, commands, sentry_units):
273+ """Check that all commands in a list exit zero on all
274+ sentry units in a list.
275+
276+ :param commands: list of bash commands
277+ :param sentry_units: list of sentry unit pointers
278+ :returns: None if successful; Failure message otherwise
279+ """
280+ self.log.debug('Checking exit codes for {} commands on {} '
281+ 'sentry units...'.format(len(commands),
282+ len(sentry_units)))
283+ for sentry_unit in sentry_units:
284+ for cmd in commands:
285+ output, code = sentry_unit.run(cmd)
286+ if code == 0:
287+ self.log.debug('{} `{}` returned {} '
288+ '(OK)'.format(sentry_unit.info['unit_name'],
289+ cmd, code))
290+ else:
291+ return ('{} `{}` returned {} '
292+ '{}'.format(sentry_unit.info['unit_name'],
293+ cmd, code, output))
294+ return None
295+
296+ def get_process_id_list(self, sentry_unit, process_name):
297+ """Get a list of process ID(s) from a single sentry juju unit
298+ for a single process name.
299+
300+ :param sentry_unit: Pointer to amulet sentry instance (juju unit)
301+ :param process_name: Process name
302+ :returns: List of process IDs
303+ """
304+ cmd = 'pidof {}'.format(process_name)
305+ output, code = sentry_unit.run(cmd)
306+ if code != 0:
307+ msg = ('{} `{}` returned {} '
308+ '{}'.format(sentry_unit.info['unit_name'],
309+ cmd, code, output))
310+ amulet.raise_status(amulet.FAIL, msg=msg)
311+ return str(output).split()
312+
313+ def get_unit_process_ids(self, unit_processes):
314+ """Construct a dict containing unit sentries, process names, and
315+ process IDs."""
316+ pid_dict = {}
317+ for sentry_unit, process_list in unit_processes.iteritems():
318+ pid_dict[sentry_unit] = {}
319+ for process in process_list:
320+ pids = self.get_process_id_list(sentry_unit, process)
321+ pid_dict[sentry_unit].update({process: pids})
322+ return pid_dict
323+
324+ def validate_unit_process_ids(self, expected, actual):
325+ """Validate process id quantities for services on units."""
326+ self.log.debug('Checking units for running processes...')
327+ self.log.debug('Expected PIDs: {}'.format(expected))
328+ self.log.debug('Actual PIDs: {}'.format(actual))
329+
330+ if len(actual) != len(expected):
331+ return ('Unit count mismatch. expected, actual: {}, '
332+ '{} '.format(len(expected), len(actual)))
333+
334+ for (e_sentry, e_proc_names) in expected.iteritems():
335+ e_sentry_name = e_sentry.info['unit_name']
336+ if e_sentry in actual.keys():
337+ a_proc_names = actual[e_sentry]
338+ else:
339+ return ('Expected sentry ({}) not found in actual dict data.'
340+ '{}'.format(e_sentry_name, e_sentry))
341+
342+ if len(e_proc_names.keys()) != len(a_proc_names.keys()):
343+ return ('Process name count mismatch. expected, actual: {}, '
344+ '{}'.format(len(expected), len(actual)))
345+
346+ for (e_proc_name, e_pids_length), (a_proc_name, a_pids) in \
347+ zip(e_proc_names.items(), a_proc_names.items()):
348+ if e_proc_name != a_proc_name:
349+ return ('Process name mismatch. expected, actual: {}, '
350+ '{}'.format(e_proc_name, a_proc_name))
351+
352+ a_pids_length = len(a_pids)
353+ if e_pids_length != a_pids_length:
354+ return ('PID count mismatch. {} ({}) expected, actual: '
355+ '{}, {} ({})'.format(e_sentry_name, e_proc_name,
356+ e_pids_length, a_pids_length,
357+ a_pids))
358+ else:
359+ self.log.debug('PID check OK: {} {} {}: '
360+ '{}'.format(e_sentry_name, e_proc_name,
361+ e_pids_length, a_pids))
362+ return None
363+
364+ def validate_list_of_identical_dicts(self, list_of_dicts):
365+ """Check that all dicts within a list are identical."""
366+ hashes = []
367+ for _dict in list_of_dicts:
368+ hashes.append(hash(frozenset(_dict.items())))
369+
370+ self.log.debug('Hashes: {}'.format(hashes))
371+ if len(set(hashes)) == 1:
372+ self.log.debug('Dicts within list are identical')
373+ else:
374+ return 'Dicts within list are not identical'
375+
376+ return None
377
378=== modified file 'tests/tests.yaml'
379--- tests/tests.yaml 2015-04-29 15:28:48 +0000
380+++ tests/tests.yaml 2015-07-28 01:35:52 +0000
381@@ -7,3 +7,6 @@
382 - ppa:juju/stable
383 packages:
384 - amulet
385+ - python-distro-info
386+ - distro-info
387+ - distro-info-data

Subscribers

People subscribed via source and target branches

to all changes: