Merge lp:~1chb1n/charms/trusty/mysql/ch-sync-mitaka into lp:charms/trusty/mysql

Proposed by Ryan Beisner on 2016-01-11
Status: Merged
Merged at revision: 153
Proposed branch: lp:~1chb1n/charms/trusty/mysql/ch-sync-mitaka
Merge into: lp:charms/trusty/mysql
Diff against target: 2365 lines (+1387/-234)
22 files modified
Makefile (+1/-0)
hooks/charmhelpers/contrib/network/ip.py (+31/-23)
hooks/charmhelpers/core/hookenv.py (+95/-13)
hooks/charmhelpers/core/host.py (+120/-32)
hooks/charmhelpers/core/hugepage.py (+10/-1)
hooks/charmhelpers/core/kernel.py (+68/-0)
hooks/charmhelpers/core/services/helpers.py (+14/-5)
hooks/charmhelpers/core/strutils.py (+30/-0)
hooks/charmhelpers/core/templating.py (+21/-8)
hooks/charmhelpers/fetch/__init__.py (+10/-2)
hooks/charmhelpers/fetch/archiveurl.py (+1/-1)
hooks/charmhelpers/fetch/bzrurl.py (+22/-32)
hooks/charmhelpers/fetch/giturl.py (+19/-24)
tests/019-basic-trusty-mitaka (+11/-0)
tests/021-basic-xenial-mitaka (+9/-0)
tests/README (+113/-0)
tests/basic_deployment.py (+10/-7)
tests/broken-on-local-019-basic-vivid-kilo (+0/-9)
tests/charmhelpers/contrib/amulet/deployment.py (+4/-2)
tests/charmhelpers/contrib/amulet/utils.py (+284/-62)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+130/-12)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+384/-1)
To merge this branch: bzr merge lp:~1chb1n/charms/trusty/mysql/ch-sync-mitaka
Reviewer Review Type Date Requested Status
Review Queue (community) automated testing Needs Fixing on 2016-01-16
Charles Butler (community) 2016-01-11 Approve on 2016-01-14
Review via email: mp+282209@code.launchpad.net

Commit Message

Sync charm-helpers for Mitaka cloud archive capability; Rm deprecated Vivid release from amulet tests; Update amulet test re: problematic svc check test helper; Move 00-setup to prevent unnecessary extra bootstrap; Update Makefile.

Description of the Change

Sync charm-helpers for Mitaka cloud archive capability; Rm deprecated Vivid release from amulet tests; Update amulet test re: problematic svc check test helper; Move 00-setup to prevent unnecessary extra bootstrap; Update Makefile.

To post a comment you must log in.

charm_unit_test #15961 mysql for 1chb1n mp282209
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/15961/

charm_lint_check #17090 mysql for 1chb1n mp282209
    LINT OK: passed

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

charm_amulet_test #8699 mysql for 1chb1n mp282209
    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/14473592/
Build: http://10.245.162.77:8080/job/charm_amulet_test/8699/

charm_amulet_test #8729 mysql for 1chb1n mp282209
    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/14479160/
Build: http://10.245.162.77:8080/job/charm_amulet_test/8729/

charm_lint_check #17251 mysql for 1chb1n mp282209
    LINT OK: passed

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

charm_unit_test #16117 mysql for 1chb1n mp282209
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/16117/

charm_amulet_test #8758 mysql for 1chb1n mp282209
    AMULET OK: passed

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

Ryan Beisner (1chb1n) wrote :

FYI - Failed tests were prior to removing/replacing the racey/deprecated svc restart check helper.

Charles Butler (lazypower) wrote :

+1 LGTM

Approved and merged

review: Approve
Review Queue (review-queue) wrote :

This item has failed automated testing! Results available here http://juju-ci.vapour.ws:8080/job/charm-bundle-test-lxc/2147/

review: Needs Fixing (automated testing)
Review Queue (review-queue) wrote :

This item has failed automated testing! Results available here http://juju-ci.vapour.ws:8080/job/charm-bundle-test-aws/2127/

review: Needs Fixing (automated testing)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2015-08-18 23:59:45 +0000
3+++ Makefile 2016-01-13 18:53:03 +0000
4@@ -18,6 +18,7 @@
5
6 functional_test:
7 @echo Starting Amulet tests...
8+ @tests/setup/00-setup
9 @juju test -v -p AMULET_HTTP_PROXY,AMULET_OS_VIP --timeout 2700
10
11 bin/charm_helpers_sync.py:
12
13=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
14--- hooks/charmhelpers/contrib/network/ip.py 2015-02-27 10:17:34 +0000
15+++ hooks/charmhelpers/contrib/network/ip.py 2016-01-13 18:53:03 +0000
16@@ -23,7 +23,7 @@
17 from functools import partial
18
19 from charmhelpers.core.hookenv import unit_get
20-from charmhelpers.fetch import apt_install
21+from charmhelpers.fetch import apt_install, apt_update
22 from charmhelpers.core.hookenv import (
23 log,
24 WARNING,
25@@ -32,13 +32,15 @@
26 try:
27 import netifaces
28 except ImportError:
29- apt_install('python-netifaces')
30+ apt_update(fatal=True)
31+ apt_install('python-netifaces', fatal=True)
32 import netifaces
33
34 try:
35 import netaddr
36 except ImportError:
37- apt_install('python-netaddr')
38+ apt_update(fatal=True)
39+ apt_install('python-netaddr', fatal=True)
40 import netaddr
41
42
43@@ -51,7 +53,7 @@
44
45
46 def no_ip_found_error_out(network):
47- errmsg = ("No IP address found in network: %s" % network)
48+ errmsg = ("No IP address found in network(s): %s" % network)
49 raise ValueError(errmsg)
50
51
52@@ -59,7 +61,7 @@
53 """Get an IPv4 or IPv6 address within the network from the host.
54
55 :param network (str): CIDR presentation format. For example,
56- '192.168.1.0/24'.
57+ '192.168.1.0/24'. Supports multiple networks as a space-delimited list.
58 :param fallback (str): If no address is found, return fallback.
59 :param fatal (boolean): If no address is found, fallback is not
60 set and fatal is True then exit(1).
61@@ -73,24 +75,26 @@
62 else:
63 return None
64
65- _validate_cidr(network)
66- network = netaddr.IPNetwork(network)
67- for iface in netifaces.interfaces():
68- addresses = netifaces.ifaddresses(iface)
69- if network.version == 4 and netifaces.AF_INET in addresses:
70- addr = addresses[netifaces.AF_INET][0]['addr']
71- netmask = addresses[netifaces.AF_INET][0]['netmask']
72- cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))
73- if cidr in network:
74- return str(cidr.ip)
75+ networks = network.split() or [network]
76+ for network in networks:
77+ _validate_cidr(network)
78+ network = netaddr.IPNetwork(network)
79+ for iface in netifaces.interfaces():
80+ addresses = netifaces.ifaddresses(iface)
81+ if network.version == 4 and netifaces.AF_INET in addresses:
82+ addr = addresses[netifaces.AF_INET][0]['addr']
83+ netmask = addresses[netifaces.AF_INET][0]['netmask']
84+ cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))
85+ if cidr in network:
86+ return str(cidr.ip)
87
88- if network.version == 6 and netifaces.AF_INET6 in addresses:
89- for addr in addresses[netifaces.AF_INET6]:
90- if not addr['addr'].startswith('fe80'):
91- cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],
92- addr['netmask']))
93- if cidr in network:
94- return str(cidr.ip)
95+ if network.version == 6 and netifaces.AF_INET6 in addresses:
96+ for addr in addresses[netifaces.AF_INET6]:
97+ if not addr['addr'].startswith('fe80'):
98+ cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],
99+ addr['netmask']))
100+ if cidr in network:
101+ return str(cidr.ip)
102
103 if fallback is not None:
104 return fallback
105@@ -435,8 +439,12 @@
106
107 rev = dns.reversename.from_address(address)
108 result = ns_query(rev)
109+
110 if not result:
111- return None
112+ try:
113+ result = socket.gethostbyaddr(address)[0]
114+ except:
115+ return None
116 else:
117 result = address
118
119
120=== modified file 'hooks/charmhelpers/core/hookenv.py'
121--- hooks/charmhelpers/core/hookenv.py 2015-08-19 14:07:56 +0000
122+++ hooks/charmhelpers/core/hookenv.py 2016-01-13 18:53:03 +0000
123@@ -491,6 +491,19 @@
124
125
126 @cached
127+def peer_relation_id():
128+ '''Get the peers relation id if a peers relation has been joined, else None.'''
129+ md = metadata()
130+ section = md.get('peers')
131+ if section:
132+ for key in section:
133+ relids = relation_ids(key)
134+ if relids:
135+ return relids[0]
136+ return None
137+
138+
139+@cached
140 def relation_to_interface(relation_name):
141 """
142 Given the name of a relation, return the interface that relation uses.
143@@ -504,12 +517,12 @@
144 def relation_to_role_and_interface(relation_name):
145 """
146 Given the name of a relation, return the role and the name of the interface
147- that relation uses (where role is one of ``provides``, ``requires``, or ``peer``).
148+ that relation uses (where role is one of ``provides``, ``requires``, or ``peers``).
149
150 :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
151 """
152 _metadata = metadata()
153- for role in ('provides', 'requires', 'peer'):
154+ for role in ('provides', 'requires', 'peers'):
155 interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
156 if interface:
157 return role, interface
158@@ -521,7 +534,7 @@
159 """
160 Given a role and interface name, return a list of relation names for the
161 current charm that use that interface under that role (where role is one
162- of ``provides``, ``requires``, or ``peer``).
163+ of ``provides``, ``requires``, or ``peers``).
164
165 :returns: A list of relation names.
166 """
167@@ -542,7 +555,7 @@
168 :returns: A list of relation names.
169 """
170 results = []
171- for role in ('provides', 'requires', 'peer'):
172+ for role in ('provides', 'requires', 'peers'):
173 results.extend(role_and_interface_to_relations(role, interface_name))
174 return results
175
176@@ -623,6 +636,38 @@
177 return unit_get('private-address')
178
179
180+@cached
181+def storage_get(attribute=None, storage_id=None):
182+ """Get storage attributes"""
183+ _args = ['storage-get', '--format=json']
184+ if storage_id:
185+ _args.extend(('-s', storage_id))
186+ if attribute:
187+ _args.append(attribute)
188+ try:
189+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
190+ except ValueError:
191+ return None
192+
193+
194+@cached
195+def storage_list(storage_name=None):
196+ """List the storage IDs for the unit"""
197+ _args = ['storage-list', '--format=json']
198+ if storage_name:
199+ _args.append(storage_name)
200+ try:
201+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
202+ except ValueError:
203+ return None
204+ except OSError as e:
205+ import errno
206+ if e.errno == errno.ENOENT:
207+ # storage-list does not exist
208+ return []
209+ raise
210+
211+
212 class UnregisteredHookError(Exception):
213 """Raised when an undefined hook is called"""
214 pass
215@@ -767,25 +812,28 @@
216
217
218 def status_get():
219- """Retrieve the previously set juju workload state
220-
221- If the status-set command is not found then assume this is juju < 1.23 and
222- return 'unknown'
223+ """Retrieve the previously set juju workload state and message
224+
225+ If the status-get command is not found then assume this is juju < 1.23 and
226+ return 'unknown', ""
227+
228 """
229- cmd = ['status-get']
230+ cmd = ['status-get', "--format=json", "--include-data"]
231 try:
232- raw_status = subprocess.check_output(cmd, universal_newlines=True)
233- status = raw_status.rstrip()
234- return status
235+ raw_status = subprocess.check_output(cmd)
236 except OSError as e:
237 if e.errno == errno.ENOENT:
238- return 'unknown'
239+ return ('unknown', "")
240 else:
241 raise
242+ else:
243+ status = json.loads(raw_status.decode("UTF-8"))
244+ return (status["status"], status["message"])
245
246
247 def translate_exc(from_exc, to_exc):
248 def inner_translate_exc1(f):
249+ @wraps(f)
250 def inner_translate_exc2(*args, **kwargs):
251 try:
252 return f(*args, **kwargs)
253@@ -830,6 +878,40 @@
254 subprocess.check_call(cmd)
255
256
257+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
258+def payload_register(ptype, klass, pid):
259+ """ is used while a hook is running to let Juju know that a
260+ payload has been started."""
261+ cmd = ['payload-register']
262+ for x in [ptype, klass, pid]:
263+ cmd.append(x)
264+ subprocess.check_call(cmd)
265+
266+
267+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
268+def payload_unregister(klass, pid):
269+ """ is used while a hook is running to let Juju know
270+ that a payload has been manually stopped. The <class> and <id> provided
271+ must match a payload that has been previously registered with juju using
272+ payload-register."""
273+ cmd = ['payload-unregister']
274+ for x in [klass, pid]:
275+ cmd.append(x)
276+ subprocess.check_call(cmd)
277+
278+
279+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
280+def payload_status_set(klass, pid, status):
281+ """is used to update the current status of a registered payload.
282+ The <class> and <id> provided must match a payload that has been previously
283+ registered with juju using payload-register. The <status> must be one of the
284+ follow: starting, started, stopping, stopped"""
285+ cmd = ['payload-status-set']
286+ for x in [klass, pid, status]:
287+ cmd.append(x)
288+ subprocess.check_call(cmd)
289+
290+
291 @cached
292 def juju_version():
293 """Full version string (eg. '1.23.3.1-trusty-amd64')"""
294
295=== modified file 'hooks/charmhelpers/core/host.py'
296--- hooks/charmhelpers/core/host.py 2015-08-19 14:07:56 +0000
297+++ hooks/charmhelpers/core/host.py 2016-01-13 18:53:03 +0000
298@@ -63,55 +63,85 @@
299 return service_result
300
301
302-def service_pause(service_name, init_dir=None):
303+def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
304 """Pause a system service.
305
306 Stop it, and prevent it from starting again at boot."""
307- if init_dir is None:
308- init_dir = "/etc/init"
309- stopped = service_stop(service_name)
310- # XXX: Support systemd too
311- override_path = os.path.join(
312- init_dir, '{}.override'.format(service_name))
313- with open(override_path, 'w') as fh:
314- fh.write("manual\n")
315+ stopped = True
316+ if service_running(service_name):
317+ stopped = service_stop(service_name)
318+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
319+ sysv_file = os.path.join(initd_dir, service_name)
320+ if init_is_systemd():
321+ service('disable', service_name)
322+ elif os.path.exists(upstart_file):
323+ override_path = os.path.join(
324+ init_dir, '{}.override'.format(service_name))
325+ with open(override_path, 'w') as fh:
326+ fh.write("manual\n")
327+ elif os.path.exists(sysv_file):
328+ subprocess.check_call(["update-rc.d", service_name, "disable"])
329+ else:
330+ raise ValueError(
331+ "Unable to detect {0} as SystemD, Upstart {1} or"
332+ " SysV {2}".format(
333+ service_name, upstart_file, sysv_file))
334 return stopped
335
336
337-def service_resume(service_name, init_dir=None):
338+def service_resume(service_name, init_dir="/etc/init",
339+ initd_dir="/etc/init.d"):
340 """Resume a system service.
341
342 Reenable starting again at boot. Start the service"""
343- # XXX: Support systemd too
344- if init_dir is None:
345- init_dir = "/etc/init"
346- override_path = os.path.join(
347- init_dir, '{}.override'.format(service_name))
348- if os.path.exists(override_path):
349- os.unlink(override_path)
350- started = service_start(service_name)
351+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
352+ sysv_file = os.path.join(initd_dir, service_name)
353+ if init_is_systemd():
354+ service('enable', service_name)
355+ elif os.path.exists(upstart_file):
356+ override_path = os.path.join(
357+ init_dir, '{}.override'.format(service_name))
358+ if os.path.exists(override_path):
359+ os.unlink(override_path)
360+ elif os.path.exists(sysv_file):
361+ subprocess.check_call(["update-rc.d", service_name, "enable"])
362+ else:
363+ raise ValueError(
364+ "Unable to detect {0} as SystemD, Upstart {1} or"
365+ " SysV {2}".format(
366+ service_name, upstart_file, sysv_file))
367+
368+ started = service_running(service_name)
369+ if not started:
370+ started = service_start(service_name)
371 return started
372
373
374 def service(action, service_name):
375 """Control a system service"""
376- cmd = ['service', service_name, action]
377+ if init_is_systemd():
378+ cmd = ['systemctl', action, service_name]
379+ else:
380+ cmd = ['service', service_name, action]
381 return subprocess.call(cmd) == 0
382
383
384-def service_running(service):
385+def service_running(service_name):
386 """Determine whether a system service is running"""
387- try:
388- output = subprocess.check_output(
389- ['service', service, 'status'],
390- stderr=subprocess.STDOUT).decode('UTF-8')
391- except subprocess.CalledProcessError:
392- return False
393+ if init_is_systemd():
394+ return service('is-active', service_name)
395 else:
396- if ("start/running" in output or "is running" in output):
397- return True
398- else:
399+ try:
400+ output = subprocess.check_output(
401+ ['service', service_name, 'status'],
402+ stderr=subprocess.STDOUT).decode('UTF-8')
403+ except subprocess.CalledProcessError:
404 return False
405+ else:
406+ if ("start/running" in output or "is running" in output):
407+ return True
408+ else:
409+ return False
410
411
412 def service_available(service_name):
413@@ -126,8 +156,29 @@
414 return True
415
416
417-def adduser(username, password=None, shell='/bin/bash', system_user=False):
418- """Add a user to the system"""
419+SYSTEMD_SYSTEM = '/run/systemd/system'
420+
421+
422+def init_is_systemd():
423+ return os.path.isdir(SYSTEMD_SYSTEM)
424+
425+
426+def adduser(username, password=None, shell='/bin/bash', system_user=False,
427+ primary_group=None, secondary_groups=None):
428+ """
429+ Add a user to the system.
430+
431+ Will log but otherwise succeed if the user already exists.
432+
433+ :param str username: Username to create
434+ :param str password: Password for user; if ``None``, create a system user
435+ :param str shell: The default shell for the user
436+ :param bool system_user: Whether to create a login or system user
437+ :param str primary_group: Primary group for user; defaults to their username
438+ :param list secondary_groups: Optional list of additional groups
439+
440+ :returns: The password database entry struct, as returned by `pwd.getpwnam`
441+ """
442 try:
443 user_info = pwd.getpwnam(username)
444 log('user {0} already exists!'.format(username))
445@@ -142,6 +193,16 @@
446 '--shell', shell,
447 '--password', password,
448 ])
449+ if not primary_group:
450+ try:
451+ grp.getgrnam(username)
452+ primary_group = username # avoid "group exists" error
453+ except KeyError:
454+ pass
455+ if primary_group:
456+ cmd.extend(['-g', primary_group])
457+ if secondary_groups:
458+ cmd.extend(['-G', ','.join(secondary_groups)])
459 cmd.append(username)
460 subprocess.check_call(cmd)
461 user_info = pwd.getpwnam(username)
462@@ -550,7 +611,14 @@
463 os.chdir(cur)
464
465
466-def chownr(path, owner, group, follow_links=True):
467+def chownr(path, owner, group, follow_links=True, chowntopdir=False):
468+ """
469+ Recursively change user and group ownership of files and directories
470+ in given path. Doesn't chown path itself by default, only its children.
471+
472+ :param bool follow_links: Also Chown links if True
473+ :param bool chowntopdir: Also chown path itself if True
474+ """
475 uid = pwd.getpwnam(owner).pw_uid
476 gid = grp.getgrnam(group).gr_gid
477 if follow_links:
478@@ -558,6 +626,10 @@
479 else:
480 chown = os.lchown
481
482+ if chowntopdir:
483+ broken_symlink = os.path.lexists(path) and not os.path.exists(path)
484+ if not broken_symlink:
485+ chown(path, uid, gid)
486 for root, dirs, files in os.walk(path):
487 for name in dirs + files:
488 full = os.path.join(root, name)
489@@ -568,3 +640,19 @@
490
491 def lchownr(path, owner, group):
492 chownr(path, owner, group, follow_links=False)
493+
494+
495+def get_total_ram():
496+ '''The total amount of system RAM in bytes.
497+
498+ This is what is reported by the OS, and may be overcommitted when
499+ there are multiple containers hosted on the same machine.
500+ '''
501+ with open('/proc/meminfo', 'r') as f:
502+ for line in f.readlines():
503+ if line:
504+ key, value, unit = line.split()
505+ if key == 'MemTotal:':
506+ assert unit == 'kB', 'Unknown unit'
507+ return int(value) * 1024 # Classic, not KiB.
508+ raise NotImplementedError()
509
510=== modified file 'hooks/charmhelpers/core/hugepage.py'
511--- hooks/charmhelpers/core/hugepage.py 2015-08-19 14:07:56 +0000
512+++ hooks/charmhelpers/core/hugepage.py 2016-01-13 18:53:03 +0000
513@@ -25,11 +25,13 @@
514 fstab_mount,
515 mkdir,
516 )
517+from charmhelpers.core.strutils import bytes_from_string
518+from subprocess import check_output
519
520
521 def hugepage_support(user, group='hugetlb', nr_hugepages=256,
522 max_map_count=65536, mnt_point='/run/hugepages/kvm',
523- pagesize='2MB', mount=True):
524+ pagesize='2MB', mount=True, set_shmmax=False):
525 """Enable hugepages on system.
526
527 Args:
528@@ -44,11 +46,18 @@
529 group_info = add_group(group)
530 gid = group_info.gr_gid
531 add_user_to_group(user, group)
532+ if max_map_count < 2 * nr_hugepages:
533+ max_map_count = 2 * nr_hugepages
534 sysctl_settings = {
535 'vm.nr_hugepages': nr_hugepages,
536 'vm.max_map_count': max_map_count,
537 'vm.hugetlb_shm_group': gid,
538 }
539+ if set_shmmax:
540+ shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax']))
541+ shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages
542+ if shmmax_minsize > shmmax_current:
543+ sysctl_settings['kernel.shmmax'] = shmmax_minsize
544 sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
545 mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
546 lfstab = fstab.Fstab()
547
548=== added file 'hooks/charmhelpers/core/kernel.py'
549--- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
550+++ hooks/charmhelpers/core/kernel.py 2016-01-13 18:53:03 +0000
551@@ -0,0 +1,68 @@
552+#!/usr/bin/env python
553+# -*- coding: utf-8 -*-
554+
555+# Copyright 2014-2015 Canonical Limited.
556+#
557+# This file is part of charm-helpers.
558+#
559+# charm-helpers is free software: you can redistribute it and/or modify
560+# it under the terms of the GNU Lesser General Public License version 3 as
561+# published by the Free Software Foundation.
562+#
563+# charm-helpers is distributed in the hope that it will be useful,
564+# but WITHOUT ANY WARRANTY; without even the implied warranty of
565+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
566+# GNU Lesser General Public License for more details.
567+#
568+# You should have received a copy of the GNU Lesser General Public License
569+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
570+
571+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
572+
573+from charmhelpers.core.hookenv import (
574+ log,
575+ INFO
576+)
577+
578+from subprocess import check_call, check_output
579+import re
580+
581+
582+def modprobe(module, persist=True):
583+ """Load a kernel module and configure for auto-load on reboot."""
584+ cmd = ['modprobe', module]
585+
586+ log('Loading kernel module %s' % module, level=INFO)
587+
588+ check_call(cmd)
589+ if persist:
590+ with open('/etc/modules', 'r+') as modules:
591+ if module not in modules.read():
592+ modules.write(module)
593+
594+
595+def rmmod(module, force=False):
596+ """Remove a module from the linux kernel"""
597+ cmd = ['rmmod']
598+ if force:
599+ cmd.append('-f')
600+ cmd.append(module)
601+ log('Removing kernel module %s' % module, level=INFO)
602+ return check_call(cmd)
603+
604+
605+def lsmod():
606+ """Shows what kernel modules are currently loaded"""
607+ return check_output(['lsmod'],
608+ universal_newlines=True)
609+
610+
611+def is_module_loaded(module):
612+ """Checks if a kernel module is already loaded"""
613+ matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
614+ return len(matches) > 0
615+
616+
617+def update_initramfs(version='all'):
618+ """Updates an initramfs image"""
619+ return check_call(["update-initramfs", "-k", version, "-u"])
620
621=== modified file 'hooks/charmhelpers/core/services/helpers.py'
622--- hooks/charmhelpers/core/services/helpers.py 2015-08-19 14:07:56 +0000
623+++ hooks/charmhelpers/core/services/helpers.py 2016-01-13 18:53:03 +0000
624@@ -243,33 +243,40 @@
625 :param str source: The template source file, relative to
626 `$CHARM_DIR/templates`
627
628- :param str target: The target to write the rendered template to
629+ :param str target: The target to write the rendered template to (or None)
630 :param str owner: The owner of the rendered file
631 :param str group: The group of the rendered file
632 :param int perms: The permissions of the rendered file
633 :param partial on_change_action: functools partial to be executed when
634 rendered file changes
635+ :param jinja2 loader template_loader: A jinja2 template loader
636+
637+ :return str: The rendered template
638 """
639 def __init__(self, source, target,
640 owner='root', group='root', perms=0o444,
641- on_change_action=None):
642+ on_change_action=None, template_loader=None):
643 self.source = source
644 self.target = target
645 self.owner = owner
646 self.group = group
647 self.perms = perms
648 self.on_change_action = on_change_action
649+ self.template_loader = template_loader
650
651 def __call__(self, manager, service_name, event_name):
652 pre_checksum = ''
653 if self.on_change_action and os.path.isfile(self.target):
654 pre_checksum = host.file_hash(self.target)
655 service = manager.get_service(service_name)
656- context = {}
657+ context = {'ctx': {}}
658 for ctx in service.get('required_data', []):
659 context.update(ctx)
660- templating.render(self.source, self.target, context,
661- self.owner, self.group, self.perms)
662+ context['ctx'].update(ctx)
663+
664+ result = templating.render(self.source, self.target, context,
665+ self.owner, self.group, self.perms,
666+ template_loader=self.template_loader)
667 if self.on_change_action:
668 if pre_checksum == host.file_hash(self.target):
669 hookenv.log(
670@@ -278,6 +285,8 @@
671 else:
672 self.on_change_action()
673
674+ return result
675+
676
677 # Convenience aliases for templates
678 render_template = template = TemplateCallback
679
680=== modified file 'hooks/charmhelpers/core/strutils.py'
681--- hooks/charmhelpers/core/strutils.py 2015-08-19 14:07:56 +0000
682+++ hooks/charmhelpers/core/strutils.py 2016-01-13 18:53:03 +0000
683@@ -18,6 +18,7 @@
684 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
685
686 import six
687+import re
688
689
690 def bool_from_string(value):
691@@ -40,3 +41,32 @@
692
693 msg = "Unable to interpret string value '%s' as boolean" % (value)
694 raise ValueError(msg)
695+
696+
697+def bytes_from_string(value):
698+ """Interpret human readable string value as bytes.
699+
700+ Returns int
701+ """
702+ BYTE_POWER = {
703+ 'K': 1,
704+ 'KB': 1,
705+ 'M': 2,
706+ 'MB': 2,
707+ 'G': 3,
708+ 'GB': 3,
709+ 'T': 4,
710+ 'TB': 4,
711+ 'P': 5,
712+ 'PB': 5,
713+ }
714+ if isinstance(value, six.string_types):
715+ value = six.text_type(value)
716+ else:
717+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
718+ raise ValueError(msg)
719+ matches = re.match("([0-9]+)([a-zA-Z]+)", value)
720+ if not matches:
721+ msg = "Unable to interpret string value '%s' as bytes" % (value)
722+ raise ValueError(msg)
723+ return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
724
725=== modified file 'hooks/charmhelpers/core/templating.py'
726--- hooks/charmhelpers/core/templating.py 2015-08-19 14:07:56 +0000
727+++ hooks/charmhelpers/core/templating.py 2016-01-13 18:53:03 +0000
728@@ -21,13 +21,14 @@
729
730
731 def render(source, target, context, owner='root', group='root',
732- perms=0o444, templates_dir=None, encoding='UTF-8'):
733+ perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None):
734 """
735 Render a template.
736
737 The `source` path, if not absolute, is relative to the `templates_dir`.
738
739- The `target` path should be absolute.
740+ The `target` path should be absolute. It can also be `None`, in which
741+ case no file will be written.
742
743 The context should be a dict containing the values to be replaced in the
744 template.
745@@ -36,6 +37,9 @@
746
747 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
748
749+ The rendered template will be written to the file as well as being returned
750+ as a string.
751+
752 Note: Using this requires python-jinja2; if it is not installed, calling
753 this will attempt to use charmhelpers.fetch.apt_install to install it.
754 """
755@@ -52,17 +56,26 @@
756 apt_install('python-jinja2', fatal=True)
757 from jinja2 import FileSystemLoader, Environment, exceptions
758
759- if templates_dir is None:
760- templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
761- loader = Environment(loader=FileSystemLoader(templates_dir))
762+ if template_loader:
763+ template_env = Environment(loader=template_loader)
764+ else:
765+ if templates_dir is None:
766+ templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
767+ template_env = Environment(loader=FileSystemLoader(templates_dir))
768 try:
769 source = source
770- template = loader.get_template(source)
771+ template = template_env.get_template(source)
772 except exceptions.TemplateNotFound as e:
773 hookenv.log('Could not load template %s from %s.' %
774 (source, templates_dir),
775 level=hookenv.ERROR)
776 raise e
777 content = template.render(context)
778- host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
779- host.write_file(target, content.encode(encoding), owner, group, perms)
780+ if target is not None:
781+ target_dir = os.path.dirname(target)
782+ if not os.path.exists(target_dir):
783+ # This is a terrible default directory permission, as the file
784+ # or its siblings will often contain secrets.
785+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
786+ host.write_file(target, content.encode(encoding), owner, group, perms)
787+ return content
788
789=== modified file 'hooks/charmhelpers/fetch/__init__.py'
790--- hooks/charmhelpers/fetch/__init__.py 2015-08-18 23:35:26 +0000
791+++ hooks/charmhelpers/fetch/__init__.py 2016-01-13 18:53:03 +0000
792@@ -98,6 +98,14 @@
793 'liberty/proposed': 'trusty-proposed/liberty',
794 'trusty-liberty/proposed': 'trusty-proposed/liberty',
795 'trusty-proposed/liberty': 'trusty-proposed/liberty',
796+ # Mitaka
797+ 'mitaka': 'trusty-updates/mitaka',
798+ 'trusty-mitaka': 'trusty-updates/mitaka',
799+ 'trusty-mitaka/updates': 'trusty-updates/mitaka',
800+ 'trusty-updates/mitaka': 'trusty-updates/mitaka',
801+ 'mitaka/proposed': 'trusty-proposed/mitaka',
802+ 'trusty-mitaka/proposed': 'trusty-proposed/mitaka',
803+ 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
804 }
805
806 # The order of this list is very important. Handlers should be listed in from
807@@ -225,12 +233,12 @@
808
809 def apt_mark(packages, mark, fatal=False):
810 """Flag one or more packages using apt-mark"""
811+ log("Marking {} as {}".format(packages, mark))
812 cmd = ['apt-mark', mark]
813 if isinstance(packages, six.string_types):
814 cmd.append(packages)
815 else:
816 cmd.extend(packages)
817- log("Holding {}".format(packages))
818
819 if fatal:
820 subprocess.check_call(cmd, universal_newlines=True)
821@@ -411,7 +419,7 @@
822 importlib.import_module(package),
823 classname)
824 plugin_list.append(handler_class())
825- except (ImportError, AttributeError):
826+ except NotImplementedError:
827 # Skip missing plugins so that they can be ommitted from
828 # installation if desired
829 log("FetchHandler {} not found, skipping plugin".format(
830
831=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
832--- hooks/charmhelpers/fetch/archiveurl.py 2015-08-18 23:35:26 +0000
833+++ hooks/charmhelpers/fetch/archiveurl.py 2016-01-13 18:53:03 +0000
834@@ -108,7 +108,7 @@
835 install_opener(opener)
836 response = urlopen(source)
837 try:
838- with open(dest, 'w') as dest_file:
839+ with open(dest, 'wb') as dest_file:
840 dest_file.write(response.read())
841 except Exception as e:
842 if os.path.isfile(dest):
843
844=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
845--- hooks/charmhelpers/fetch/bzrurl.py 2015-02-04 19:09:09 +0000
846+++ hooks/charmhelpers/fetch/bzrurl.py 2016-01-13 18:53:03 +0000
847@@ -15,60 +15,50 @@
848 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
849
850 import os
851+from subprocess import check_call
852 from charmhelpers.fetch import (
853 BaseFetchHandler,
854- UnhandledSource
855+ UnhandledSource,
856+ filter_installed_packages,
857+ apt_install,
858 )
859 from charmhelpers.core.host import mkdir
860
861-import six
862-if six.PY3:
863- raise ImportError('bzrlib does not support Python3')
864
865-try:
866- from bzrlib.branch import Branch
867- from bzrlib import bzrdir, workingtree, errors
868-except ImportError:
869- from charmhelpers.fetch import apt_install
870- apt_install("python-bzrlib")
871- from bzrlib.branch import Branch
872- from bzrlib import bzrdir, workingtree, errors
873+if filter_installed_packages(['bzr']) != []:
874+ apt_install(['bzr'])
875+ if filter_installed_packages(['bzr']) != []:
876+ raise NotImplementedError('Unable to install bzr')
877
878
879 class BzrUrlFetchHandler(BaseFetchHandler):
880 """Handler for bazaar branches via generic and lp URLs"""
881 def can_handle(self, source):
882 url_parts = self.parse_url(source)
883- if url_parts.scheme not in ('bzr+ssh', 'lp'):
884+ if url_parts.scheme not in ('bzr+ssh', 'lp', ''):
885 return False
886+ elif not url_parts.scheme:
887+ return os.path.exists(os.path.join(source, '.bzr'))
888 else:
889 return True
890
891 def branch(self, source, dest):
892- url_parts = self.parse_url(source)
893- # If we use lp:branchname scheme we need to load plugins
894 if not self.can_handle(source):
895 raise UnhandledSource("Cannot handle {}".format(source))
896- if url_parts.scheme == "lp":
897- from bzrlib.plugin import load_plugins
898- load_plugins()
899- try:
900- local_branch = bzrdir.BzrDir.create_branch_convenience(dest)
901- except errors.AlreadyControlDirError:
902- local_branch = Branch.open(dest)
903- try:
904- remote_branch = Branch.open(source)
905- remote_branch.push(local_branch)
906- tree = workingtree.WorkingTree.open(dest)
907- tree.update()
908- except Exception as e:
909- raise e
910+ if os.path.exists(dest):
911+ check_call(['bzr', 'pull', '--overwrite', '-d', dest, source])
912+ else:
913+ check_call(['bzr', 'branch', source, dest])
914
915- def install(self, source):
916+ def install(self, source, dest=None):
917 url_parts = self.parse_url(source)
918 branch_name = url_parts.path.strip("/").split("/")[-1]
919- dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
920- branch_name)
921+ if dest:
922+ dest_dir = os.path.join(dest, branch_name)
923+ else:
924+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
925+ branch_name)
926+
927 if not os.path.exists(dest_dir):
928 mkdir(dest_dir, perms=0o755)
929 try:
930
931=== modified file 'hooks/charmhelpers/fetch/giturl.py'
932--- hooks/charmhelpers/fetch/giturl.py 2015-08-18 23:35:26 +0000
933+++ hooks/charmhelpers/fetch/giturl.py 2016-01-13 18:53:03 +0000
934@@ -15,24 +15,18 @@
935 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
936
937 import os
938+from subprocess import check_call
939 from charmhelpers.fetch import (
940 BaseFetchHandler,
941- UnhandledSource
942+ UnhandledSource,
943+ filter_installed_packages,
944+ apt_install,
945 )
946-from charmhelpers.core.host import mkdir
947-
948-import six
949-if six.PY3:
950- raise ImportError('GitPython does not support Python 3')
951-
952-try:
953- from git import Repo
954-except ImportError:
955- from charmhelpers.fetch import apt_install
956- apt_install("python-git")
957- from git import Repo
958-
959-from git.exc import GitCommandError # noqa E402
960+
961+if filter_installed_packages(['git']) != []:
962+ apt_install(['git'])
963+ if filter_installed_packages(['git']) != []:
964+ raise NotImplementedError('Unable to install git')
965
966
967 class GitUrlFetchHandler(BaseFetchHandler):
968@@ -40,19 +34,24 @@
969 def can_handle(self, source):
970 url_parts = self.parse_url(source)
971 # TODO (mattyw) no support for ssh git@ yet
972- if url_parts.scheme not in ('http', 'https', 'git'):
973+ if url_parts.scheme not in ('http', 'https', 'git', ''):
974 return False
975+ elif not url_parts.scheme:
976+ return os.path.exists(os.path.join(source, '.git'))
977 else:
978 return True
979
980- def clone(self, source, dest, branch, depth=None):
981+ def clone(self, source, dest, branch="master", depth=None):
982 if not self.can_handle(source):
983 raise UnhandledSource("Cannot handle {}".format(source))
984
985- if depth:
986- Repo.clone_from(source, dest, branch=branch, depth=depth)
987+ if os.path.exists(dest):
988+ cmd = ['git', '-C', dest, 'pull', source, branch]
989 else:
990- Repo.clone_from(source, dest, branch=branch)
991+ cmd = ['git', 'clone', source, dest, '--branch', branch]
992+ if depth:
993+ cmd.extend(['--depth', depth])
994+ check_call(cmd)
995
996 def install(self, source, branch="master", dest=None, depth=None):
997 url_parts = self.parse_url(source)
998@@ -62,12 +61,8 @@
999 else:
1000 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
1001 branch_name)
1002- if not os.path.exists(dest_dir):
1003- mkdir(dest_dir, perms=0o755)
1004 try:
1005 self.clone(source, dest_dir, branch, depth)
1006- except GitCommandError as e:
1007- raise UnhandledSource(e)
1008 except OSError as e:
1009 raise UnhandledSource(e.strerror)
1010 return dest_dir
1011
1012=== renamed file 'tests/020-basic-trusty-liberty' => 'tests/018-basic-trusty-liberty' (properties changed: -x to +x)
1013=== added file 'tests/019-basic-trusty-mitaka'
1014--- tests/019-basic-trusty-mitaka 1970-01-01 00:00:00 +0000
1015+++ tests/019-basic-trusty-mitaka 2016-01-13 18:53:03 +0000
1016@@ -0,0 +1,11 @@
1017+#!/usr/bin/python
1018+
1019+"""Amulet tests on a basic mysql deployment on trusty-mitaka."""
1020+
1021+from basic_deployment import MySQLBasicDeployment
1022+
1023+if __name__ == '__main__':
1024+ deployment = MySQLBasicDeployment(series='trusty',
1025+ openstack='cloud:trusty-mitaka',
1026+ source='cloud:trusty-updates/mitaka')
1027+ deployment.run_tests()
1028
1029=== renamed file 'tests/021-basic-wily-liberty' => 'tests/020-basic-wily-liberty' (properties changed: -x to +x)
1030=== added file 'tests/021-basic-xenial-mitaka'
1031--- tests/021-basic-xenial-mitaka 1970-01-01 00:00:00 +0000
1032+++ tests/021-basic-xenial-mitaka 2016-01-13 18:53:03 +0000
1033@@ -0,0 +1,9 @@
1034+#!/usr/bin/python
1035+
1036+"""Amulet tests on a basic mysql deployment on xenial-mitaka."""
1037+
1038+from basic_deployment import MySQLBasicDeployment
1039+
1040+if __name__ == '__main__':
1041+ deployment = MySQLBasicDeployment(series='xenial')
1042+ deployment.run_tests()
1043
1044=== added file 'tests/README'
1045--- tests/README 1970-01-01 00:00:00 +0000
1046+++ tests/README 2016-01-13 18:53:03 +0000
1047@@ -0,0 +1,113 @@
1048+This directory provides Amulet tests to verify basic deployment functionality
1049+from the perspective of this charm, its requirements and its features, as
1050+exercised in a subset of the full OpenStack deployment test bundle topology.
1051+
1052+Reference: lp:openstack-charm-testing for full test bundles.
1053+
1054+A single topology and configuration is defined and deployed, once for each of
1055+the defined Ubuntu:OpenStack release combos. The ongoing goal is for this
1056+charm to always possess tests and combo definitions for all currently-supported
1057+release combinations of U:OS.
1058+
1059+test_* methods are called in lexical sort order, as with most runners. However,
1060+each individual test method should be idempotent and expected to pass regardless
1061+of run order or Ubuntu:OpenStack combo. When writing or modifying tests,
1062+ensure that every individual test is not dependent on another test_ method.
1063+
1064+Test naming convention, purely for code organization purposes:
1065+ 1xx service and endpoint checks
1066+ 2xx relation checks
1067+ 3xx config checks
1068+ 4xx functional checks
1069+ 9xx restarts, config changes, actions and other final checks
1070+
1071+In order to run tests, charm-tools and juju must be installed:
1072+ sudo add-apt-repository ppa:juju/stable
1073+ sudo apt-get update
1074+ sudo apt-get install charm-tools juju juju-deployer
1075+
1076+Alternatively, tests may be exercised with proposed or development versions
1077+of juju and related tools:
1078+
1079+ # juju proposed version
1080+ sudo add-apt-repository ppa:juju/proposed
1081+ sudo apt-get update
1082+ sudo apt-get install charm-tools juju juju-deployer
1083+
1084+ # juju development version
1085+ sudo add-apt-repository ppa:juju/devel
1086+ sudo apt-get update
1087+ sudo apt-get install charm-tools juju juju-deployer
1088+
1089+Some tests may need to download files. If a web proxy server is required in
1090+the environment, the AMULET_HTTP_PROXY environment variable must be set and
1091+passed into the juju test command. This is unrelated to juju's http proxy
1092+settings or behavior.
1093+
1094+The following examples demonstrate different ways that tests can be executed.
1095+All examples are run from the charm's root directory.
1096+
1097+ * To run all +x tests in the tests directory:
1098+
1099+ bzr branch lp:charms/trusty/foo
1100+ cd foo
1101+ make functional_test
1102+
1103+ * To run the tests against a specific release combo as defined in tests/:
1104+
1105+ bzr branch lp:charms/trusty/foo
1106+ cd foo
1107+ juju test -v -p AMULET_HTTP_PROXY 015-basic-trusty-icehouse
1108+
1109+ * To run tests and keep the juju environment deployed after a failure:
1110+
1111+ bzr branch lp:charms/trusty/foo
1112+ cd foo
1113+ juju test --set-e -v -p AMULET_HTTP_PROXY 015-basic-trusty-icehouse
1114+
1115+ * To re-run a test module against an already deployed environment (one
1116+ that was deployed by a previous call to 'juju test --set-e'):
1117+
1118+ ./tests/015-basic-trusty-icehouse
1119+
1120+ * Even with --set-e, `juju test` will tear down the deployment when all
1121+ tests pass. The following work flow may be more effective when
1122+ iterating on test writing.
1123+
1124+ bzr branch lp:charms/trusty/foo
1125+ cd foo
1126+ ./tests/setup/00-setup
1127+ juju bootstrap
1128+ ./tests/015-basic-trusty-icehouse
1129+ # make some changes, run tests again
1130+ ./tests/015-basic-trusty-icehouse
1131+ # make some changes, run tests again
1132+ ./tests/015-basic-trusty-icehouse
1133+
1134+ * There may be test definitions in the tests/ dir which are not set +x
1135+ executable. This is generally true for deprecated releases, or for
1136+ upcoming releases which are not yet validated and enabled. To enable
1137+ and run these tests:
1138+ bzr branch lp:charms/trusty/foo
1139+ cd foo
1140+ ls tests
1141+ chmod +x tests/017-basic-trusty-kilo
1142+ ./tests/setup/00-setup
1143+ juju bootstrap
1144+ ./tests/017-basic-trusty-kilo
1145+
1146+
1147+Additional notes:
1148+
1149+ * Use DEBUG to turn on debug logging, use ERROR otherwise.
1150+ u = OpenStackAmuletUtils(ERROR)
1151+ u = OpenStackAmuletUtils(DEBUG)
1152+
1153+ * To interact with the deployed environment:
1154+ export OS_USERNAME=admin
1155+ export OS_PASSWORD=openstack
1156+ export OS_TENANT_NAME=admin
1157+ export OS_REGION_NAME=RegionOne
1158+ export OS_AUTH_URL=${OS_AUTH_PROTOCOL:-http}://`juju-deployer -e trusty -f keystone`:5000/v2.0
1159+ keystone user-list
1160+ glance image-list
1161
1162=== modified file 'tests/basic_deployment.py'
1163--- tests/basic_deployment.py 2015-09-23 19:17:20 +0000
1164+++ tests/basic_deployment.py 2016-01-13 18:53:03 +0000
1165@@ -155,7 +155,8 @@
1166 def test_900_restart_on_config_change(self):
1167 """Verify that mysql is restarted when the config is changed."""
1168 sentry = self.mysql_sentry
1169- service = 'mysql'
1170+ service = 'mysqld'
1171+ juju_service = 'mysql'
1172
1173 # Expected default and alternate value
1174 set_default = {'dataset-size': '80%'}
1175@@ -165,13 +166,15 @@
1176 conf_file = '/etc/mysql/my.cnf'
1177
1178 # Make config change, check for service restarts
1179- u.log.debug('Making config change on {}...'.format(service))
1180- self.d.configure(service, set_alternate)
1181+ u.log.debug('Making config change on {}...'.format(juju_service))
1182+ mtime = u.get_sentry_time(sentry)
1183+ self.d.configure(juju_service, set_alternate)
1184
1185 u.log.debug('Checking that the service restarted: {}'.format(service))
1186- if not u.service_restarted(sentry, service, conf_file, sleep_time=30):
1187- self.d.configure(service, set_default)
1188- msg = 'svc {} did not restart after config change'.format(service)
1189+ if not u.validate_service_config_changed(sentry, mtime,
1190+ service, conf_file):
1191+ self.d.configure(juju_service, set_default)
1192+ msg = "svc {} did not restart after config change".format(service)
1193 amulet.raise_status(amulet.FAIL, msg=msg)
1194
1195- self.d.configure(service, set_default)
1196+ self.d.configure(juju_service, set_default)
1197
1198=== removed file 'tests/broken-on-local-019-basic-vivid-kilo'
1199--- tests/broken-on-local-019-basic-vivid-kilo 2015-09-24 21:28:54 +0000
1200+++ tests/broken-on-local-019-basic-vivid-kilo 1970-01-01 00:00:00 +0000
1201@@ -1,9 +0,0 @@
1202-#!/usr/bin/python
1203-
1204-"""Amulet tests on a basic mysql deployment on vivid-kilo."""
1205-
1206-from basic_deployment import MySQLBasicDeployment
1207-
1208-if __name__ == '__main__':
1209- deployment = MySQLBasicDeployment(series='vivid')
1210- deployment.run_tests()
1211
1212=== modified file 'tests/charmhelpers/contrib/amulet/deployment.py'
1213--- tests/charmhelpers/contrib/amulet/deployment.py 2015-04-21 03:39:54 +0000
1214+++ tests/charmhelpers/contrib/amulet/deployment.py 2016-01-13 18:53:03 +0000
1215@@ -51,7 +51,8 @@
1216 if 'units' not in this_service:
1217 this_service['units'] = 1
1218
1219- self.d.add(this_service['name'], units=this_service['units'])
1220+ self.d.add(this_service['name'], units=this_service['units'],
1221+ constraints=this_service.get('constraints'))
1222
1223 for svc in other_services:
1224 if 'location' in svc:
1225@@ -64,7 +65,8 @@
1226 if 'units' not in svc:
1227 svc['units'] = 1
1228
1229- self.d.add(svc['name'], charm=branch_location, units=svc['units'])
1230+ self.d.add(svc['name'], charm=branch_location, units=svc['units'],
1231+ constraints=svc.get('constraints'))
1232
1233 def _add_relations(self, relations):
1234 """Add all of the relations for the services."""
1235
1236=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
1237--- tests/charmhelpers/contrib/amulet/utils.py 2015-08-18 23:10:59 +0000
1238+++ tests/charmhelpers/contrib/amulet/utils.py 2016-01-13 18:53:03 +0000
1239@@ -19,9 +19,11 @@
1240 import logging
1241 import os
1242 import re
1243+import socket
1244 import subprocess
1245 import sys
1246 import time
1247+import uuid
1248
1249 import amulet
1250 import distro_info
1251@@ -114,7 +116,7 @@
1252 # /!\ DEPRECATION WARNING (beisner):
1253 # New and existing tests should be rewritten to use
1254 # validate_services_by_name() as it is aware of init systems.
1255- self.log.warn('/!\\ DEPRECATION WARNING: use '
1256+ self.log.warn('DEPRECATION WARNING: use '
1257 'validate_services_by_name instead of validate_services '
1258 'due to init system differences.')
1259
1260@@ -269,33 +271,52 @@
1261 """Get last modification time of directory."""
1262 return sentry_unit.directory_stat(directory)['mtime']
1263
1264- def _get_proc_start_time(self, sentry_unit, service, pgrep_full=False):
1265- """Get process' start time.
1266-
1267- Determine start time of the process based on the last modification
1268- time of the /proc/pid directory. If pgrep_full is True, the process
1269- name is matched against the full command line.
1270- """
1271- if pgrep_full:
1272- cmd = 'pgrep -o -f {}'.format(service)
1273- else:
1274- cmd = 'pgrep -o {}'.format(service)
1275- cmd = cmd + ' | grep -v pgrep || exit 0'
1276- cmd_out = sentry_unit.run(cmd)
1277- self.log.debug('CMDout: ' + str(cmd_out))
1278- if cmd_out[0]:
1279- self.log.debug('Pid for %s %s' % (service, str(cmd_out[0])))
1280- proc_dir = '/proc/{}'.format(cmd_out[0].strip())
1281- return self._get_dir_mtime(sentry_unit, proc_dir)
1282+ def _get_proc_start_time(self, sentry_unit, service, pgrep_full=None):
1283+ """Get start time of a process based on the last modification time
1284+ of the /proc/pid directory.
1285+
1286+ :sentry_unit: The sentry unit to check for the service on
1287+ :service: service name to look for in process table
1288+ :pgrep_full: [Deprecated] Use full command line search mode with pgrep
1289+ :returns: epoch time of service process start
1290+ :param commands: list of bash commands
1291+ :param sentry_units: list of sentry unit pointers
1292+ :returns: None if successful; Failure message otherwise
1293+ """
1294+ if pgrep_full is not None:
1295+ # /!\ DEPRECATION WARNING (beisner):
1296+ # No longer implemented, as pidof is now used instead of pgrep.
1297+ # https://bugs.launchpad.net/charm-helpers/+bug/1474030
1298+ self.log.warn('DEPRECATION WARNING: pgrep_full bool is no '
1299+ 'longer implemented re: lp 1474030.')
1300+
1301+ pid_list = self.get_process_id_list(sentry_unit, service)
1302+ pid = pid_list[0]
1303+ proc_dir = '/proc/{}'.format(pid)
1304+ self.log.debug('Pid for {} on {}: {}'.format(
1305+ service, sentry_unit.info['unit_name'], pid))
1306+
1307+ return self._get_dir_mtime(sentry_unit, proc_dir)
1308
1309 def service_restarted(self, sentry_unit, service, filename,
1310- pgrep_full=False, sleep_time=20):
1311+ pgrep_full=None, sleep_time=20):
1312 """Check if service was restarted.
1313
1314 Compare a service's start time vs a file's last modification time
1315 (such as a config file for that service) to determine if the service
1316 has been restarted.
1317 """
1318+ # /!\ DEPRECATION WARNING (beisner):
1319+ # This method is prone to races in that no before-time is known.
1320+ # Use validate_service_config_changed instead.
1321+
1322+ # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
1323+ # used instead of pgrep. pgrep_full is still passed through to ensure
1324+ # deprecation WARNS. lp1474030
1325+ self.log.warn('DEPRECATION WARNING: use '
1326+ 'validate_service_config_changed instead of '
1327+ 'service_restarted due to known races.')
1328+
1329 time.sleep(sleep_time)
1330 if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
1331 self._get_file_mtime(sentry_unit, filename)):
1332@@ -304,78 +325,122 @@
1333 return False
1334
1335 def service_restarted_since(self, sentry_unit, mtime, service,
1336- pgrep_full=False, sleep_time=20,
1337- retry_count=2):
1338+ pgrep_full=None, sleep_time=20,
1339+ retry_count=30, retry_sleep_time=10):
1340 """Check if service was been started after a given time.
1341
1342 Args:
1343 sentry_unit (sentry): The sentry unit to check for the service on
1344 mtime (float): The epoch time to check against
1345 service (string): service name to look for in process table
1346- pgrep_full (boolean): Use full command line search mode with pgrep
1347- sleep_time (int): Seconds to sleep before looking for process
1348- retry_count (int): If service is not found, how many times to retry
1349+ pgrep_full: [Deprecated] Use full command line search mode with pgrep
1350+ sleep_time (int): Initial sleep time (s) before looking for file
1351+ retry_sleep_time (int): Time (s) to sleep between retries
1352+ retry_count (int): If file is not found, how many times to retry
1353
1354 Returns:
1355 bool: True if service found and its start time it newer than mtime,
1356 False if service is older than mtime or if service was
1357 not found.
1358 """
1359- self.log.debug('Checking %s restarted since %s' % (service, mtime))
1360+ # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
1361+ # used instead of pgrep. pgrep_full is still passed through to ensure
1362+ # deprecation WARNS. lp1474030
1363+
1364+ unit_name = sentry_unit.info['unit_name']
1365+ self.log.debug('Checking that %s service restarted since %s on '
1366+ '%s' % (service, mtime, unit_name))
1367 time.sleep(sleep_time)
1368- proc_start_time = self._get_proc_start_time(sentry_unit, service,
1369- pgrep_full)
1370- while retry_count > 0 and not proc_start_time:
1371- self.log.debug('No pid file found for service %s, will retry %i '
1372- 'more times' % (service, retry_count))
1373- time.sleep(30)
1374- proc_start_time = self._get_proc_start_time(sentry_unit, service,
1375- pgrep_full)
1376- retry_count = retry_count - 1
1377+ proc_start_time = None
1378+ tries = 0
1379+ while tries <= retry_count and not proc_start_time:
1380+ try:
1381+ proc_start_time = self._get_proc_start_time(sentry_unit,
1382+ service,
1383+ pgrep_full)
1384+ self.log.debug('Attempt {} to get {} proc start time on {} '
1385+ 'OK'.format(tries, service, unit_name))
1386+ except IOError as e:
1387+ # NOTE(beisner) - race avoidance, proc may not exist yet.
1388+ # https://bugs.launchpad.net/charm-helpers/+bug/1474030
1389+ self.log.debug('Attempt {} to get {} proc start time on {} '
1390+ 'failed\n{}'.format(tries, service,
1391+ unit_name, e))
1392+ time.sleep(retry_sleep_time)
1393+ tries += 1
1394
1395 if not proc_start_time:
1396 self.log.warn('No proc start time found, assuming service did '
1397 'not start')
1398 return False
1399 if proc_start_time >= mtime:
1400- self.log.debug('proc start time is newer than provided mtime'
1401- '(%s >= %s)' % (proc_start_time, mtime))
1402+ self.log.debug('Proc start time is newer than provided mtime'
1403+ '(%s >= %s) on %s (OK)' % (proc_start_time,
1404+ mtime, unit_name))
1405 return True
1406 else:
1407- self.log.warn('proc start time (%s) is older than provided mtime '
1408- '(%s), service did not restart' % (proc_start_time,
1409- mtime))
1410+ self.log.warn('Proc start time (%s) is older than provided mtime '
1411+ '(%s) on %s, service did not '
1412+ 'restart' % (proc_start_time, mtime, unit_name))
1413 return False
1414
1415 def config_updated_since(self, sentry_unit, filename, mtime,
1416- sleep_time=20):
1417+ sleep_time=20, retry_count=30,
1418+ retry_sleep_time=10):
1419 """Check if file was modified after a given time.
1420
1421 Args:
1422 sentry_unit (sentry): The sentry unit to check the file mtime on
1423 filename (string): The file to check mtime of
1424 mtime (float): The epoch time to check against
1425- sleep_time (int): Seconds to sleep before looking for process
1426+ sleep_time (int): Initial sleep time (s) before looking for file
1427+ retry_sleep_time (int): Time (s) to sleep between retries
1428+ retry_count (int): If file is not found, how many times to retry
1429
1430 Returns:
1431 bool: True if file was modified more recently than mtime, False if
1432- file was modified before mtime,
1433+ file was modified before mtime, or if file not found.
1434 """
1435- self.log.debug('Checking %s updated since %s' % (filename, mtime))
1436+ unit_name = sentry_unit.info['unit_name']
1437+ self.log.debug('Checking that %s updated since %s on '
1438+ '%s' % (filename, mtime, unit_name))
1439 time.sleep(sleep_time)
1440- file_mtime = self._get_file_mtime(sentry_unit, filename)
1441+ file_mtime = None
1442+ tries = 0
1443+ while tries <= retry_count and not file_mtime:
1444+ try:
1445+ file_mtime = self._get_file_mtime(sentry_unit, filename)
1446+ self.log.debug('Attempt {} to get {} file mtime on {} '
1447+ 'OK'.format(tries, filename, unit_name))
1448+ except IOError as e:
1449+ # NOTE(beisner) - race avoidance, file may not exist yet.
1450+ # https://bugs.launchpad.net/charm-helpers/+bug/1474030
1451+ self.log.debug('Attempt {} to get {} file mtime on {} '
1452+ 'failed\n{}'.format(tries, filename,
1453+ unit_name, e))
1454+ time.sleep(retry_sleep_time)
1455+ tries += 1
1456+
1457+ if not file_mtime:
1458+ self.log.warn('Could not determine file mtime, assuming '
1459+ 'file does not exist')
1460+ return False
1461+
1462 if file_mtime >= mtime:
1463 self.log.debug('File mtime is newer than provided mtime '
1464- '(%s >= %s)' % (file_mtime, mtime))
1465+ '(%s >= %s) on %s (OK)' % (file_mtime,
1466+ mtime, unit_name))
1467 return True
1468 else:
1469- self.log.warn('File mtime %s is older than provided mtime %s'
1470- % (file_mtime, mtime))
1471+ self.log.warn('File mtime is older than provided mtime'
1472+ '(%s < on %s) on %s' % (file_mtime,
1473+ mtime, unit_name))
1474 return False
1475
1476 def validate_service_config_changed(self, sentry_unit, mtime, service,
1477- filename, pgrep_full=False,
1478- sleep_time=20, retry_count=2):
1479+ filename, pgrep_full=None,
1480+ sleep_time=20, retry_count=30,
1481+ retry_sleep_time=10):
1482 """Check service and file were updated after mtime
1483
1484 Args:
1485@@ -383,9 +448,10 @@
1486 mtime (float): The epoch time to check against
1487 service (string): service name to look for in process table
1488 filename (string): The file to check mtime of
1489- pgrep_full (boolean): Use full command line search mode with pgrep
1490- sleep_time (int): Seconds to sleep before looking for process
1491+ pgrep_full: [Deprecated] Use full command line search mode with pgrep
1492+ sleep_time (int): Initial sleep in seconds to pass to test helpers
1493 retry_count (int): If service is not found, how many times to retry
1494+ retry_sleep_time (int): Time in seconds to wait between retries
1495
1496 Typical Usage:
1497 u = OpenStackAmuletUtils(ERROR)
1498@@ -402,15 +468,27 @@
1499 mtime, False if service is older than mtime or if service was
1500 not found or if filename was modified before mtime.
1501 """
1502- self.log.debug('Checking %s restarted since %s' % (service, mtime))
1503- time.sleep(sleep_time)
1504- service_restart = self.service_restarted_since(sentry_unit, mtime,
1505- service,
1506- pgrep_full=pgrep_full,
1507- sleep_time=0,
1508- retry_count=retry_count)
1509- config_update = self.config_updated_since(sentry_unit, filename, mtime,
1510- sleep_time=0)
1511+
1512+ # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
1513+ # used instead of pgrep. pgrep_full is still passed through to ensure
1514+ # deprecation WARNS. lp1474030
1515+
1516+ service_restart = self.service_restarted_since(
1517+ sentry_unit, mtime,
1518+ service,
1519+ pgrep_full=pgrep_full,
1520+ sleep_time=sleep_time,
1521+ retry_count=retry_count,
1522+ retry_sleep_time=retry_sleep_time)
1523+
1524+ config_update = self.config_updated_since(
1525+ sentry_unit,
1526+ filename,
1527+ mtime,
1528+ sleep_time=sleep_time,
1529+ retry_count=retry_count,
1530+ retry_sleep_time=retry_sleep_time)
1531+
1532 return service_restart and config_update
1533
1534 def get_sentry_time(self, sentry_unit):
1535@@ -428,7 +506,6 @@
1536 """Return a list of all Ubuntu releases in order of release."""
1537 _d = distro_info.UbuntuDistroInfo()
1538 _release_list = _d.all
1539- self.log.debug('Ubuntu release list: {}'.format(_release_list))
1540 return _release_list
1541
1542 def file_to_url(self, file_rel_path):
1543@@ -568,6 +645,142 @@
1544
1545 return None
1546
1547+ def validate_sectionless_conf(self, file_contents, expected):
1548+ """A crude conf parser. Useful to inspect configuration files which
1549+ do not have section headers (as would be necessary in order to use
1550+ the configparser). Such as openstack-dashboard or rabbitmq confs."""
1551+ for line in file_contents.split('\n'):
1552+ if '=' in line:
1553+ args = line.split('=')
1554+ if len(args) <= 1:
1555+ continue
1556+ key = args[0].strip()
1557+ value = args[1].strip()
1558+ if key in expected.keys():
1559+ if expected[key] != value:
1560+ msg = ('Config mismatch. Expected, actual: {}, '
1561+ '{}'.format(expected[key], value))
1562+ amulet.raise_status(amulet.FAIL, msg=msg)
1563+
1564+ def get_unit_hostnames(self, units):
1565+ """Return a dict of juju unit names to hostnames."""
1566+ host_names = {}
1567+ for unit in units:
1568+ host_names[unit.info['unit_name']] = \
1569+ str(unit.file_contents('/etc/hostname').strip())
1570+ self.log.debug('Unit host names: {}'.format(host_names))
1571+ return host_names
1572+
1573+ def run_cmd_unit(self, sentry_unit, cmd):
1574+ """Run a command on a unit, return the output and exit code."""
1575+ output, code = sentry_unit.run(cmd)
1576+ if code == 0:
1577+ self.log.debug('{} `{}` command returned {} '
1578+ '(OK)'.format(sentry_unit.info['unit_name'],
1579+ cmd, code))
1580+ else:
1581+ msg = ('{} `{}` command returned {} '
1582+ '{}'.format(sentry_unit.info['unit_name'],
1583+ cmd, code, output))
1584+ amulet.raise_status(amulet.FAIL, msg=msg)
1585+ return str(output), code
1586+
1587+ def file_exists_on_unit(self, sentry_unit, file_name):
1588+ """Check if a file exists on a unit."""
1589+ try:
1590+ sentry_unit.file_stat(file_name)
1591+ return True
1592+ except IOError:
1593+ return False
1594+ except Exception as e:
1595+ msg = 'Error checking file {}: {}'.format(file_name, e)
1596+ amulet.raise_status(amulet.FAIL, msg=msg)
1597+
1598+ def file_contents_safe(self, sentry_unit, file_name,
1599+ max_wait=60, fatal=False):
1600+ """Get file contents from a sentry unit. Wrap amulet file_contents
1601+ with retry logic to address races where a file checks as existing,
1602+ but no longer exists by the time file_contents is called.
1603+ Return None if file not found. Optionally raise if fatal is True."""
1604+ unit_name = sentry_unit.info['unit_name']
1605+ file_contents = False
1606+ tries = 0
1607+ while not file_contents and tries < (max_wait / 4):
1608+ try:
1609+ file_contents = sentry_unit.file_contents(file_name)
1610+ except IOError:
1611+ self.log.debug('Attempt {} to open file {} from {} '
1612+ 'failed'.format(tries, file_name,
1613+ unit_name))
1614+ time.sleep(4)
1615+ tries += 1
1616+
1617+ if file_contents:
1618+ return file_contents
1619+ elif not fatal:
1620+ return None
1621+ elif fatal:
1622+ msg = 'Failed to get file contents from unit.'
1623+ amulet.raise_status(amulet.FAIL, msg)
1624+
1625+ def port_knock_tcp(self, host="localhost", port=22, timeout=15):
1626+ """Open a TCP socket to check for a listening sevice on a host.
1627+
1628+ :param host: host name or IP address, default to localhost
1629+ :param port: TCP port number, default to 22
1630+ :param timeout: Connect timeout, default to 15 seconds
1631+ :returns: True if successful, False if connect failed
1632+ """
1633+
1634+ # Resolve host name if possible
1635+ try:
1636+ connect_host = socket.gethostbyname(host)
1637+ host_human = "{} ({})".format(connect_host, host)
1638+ except socket.error as e:
1639+ self.log.warn('Unable to resolve address: '
1640+ '{} ({}) Trying anyway!'.format(host, e))
1641+ connect_host = host
1642+ host_human = connect_host
1643+
1644+ # Attempt socket connection
1645+ try:
1646+ knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1647+ knock.settimeout(timeout)
1648+ knock.connect((connect_host, port))
1649+ knock.close()
1650+ self.log.debug('Socket connect OK for host '
1651+ '{} on port {}.'.format(host_human, port))
1652+ return True
1653+ except socket.error as e:
1654+ self.log.debug('Socket connect FAIL for'
1655+ ' {} port {} ({})'.format(host_human, port, e))
1656+ return False
1657+
1658+ def port_knock_units(self, sentry_units, port=22,
1659+ timeout=15, expect_success=True):
1660+ """Open a TCP socket to check for a listening sevice on each
1661+ listed juju unit.
1662+
1663+ :param sentry_units: list of sentry unit pointers
1664+ :param port: TCP port number, default to 22
1665+ :param timeout: Connect timeout, default to 15 seconds
1666+ :expect_success: True by default, set False to invert logic
1667+ :returns: None if successful, Failure message otherwise
1668+ """
1669+ for unit in sentry_units:
1670+ host = unit.info['public-address']
1671+ connected = self.port_knock_tcp(host, port, timeout)
1672+ if not connected and expect_success:
1673+ return 'Socket connect failed.'
1674+ elif connected and not expect_success:
1675+ return 'Socket connected unexpectedly.'
1676+
1677+ def get_uuid_epoch_stamp(self):
1678+ """Returns a stamp string based on uuid4 and epoch time. Useful in
1679+ generating test messages which need to be unique-ish."""
1680+ return '[{}-{}]'.format(uuid.uuid4(), time.time())
1681+
1682+# amulet juju action helpers:
1683 def run_action(self, unit_sentry, action,
1684 _check_output=subprocess.check_output):
1685 """Run the named action on a given unit sentry.
1686@@ -594,3 +807,12 @@
1687 output = _check_output(command, universal_newlines=True)
1688 data = json.loads(output)
1689 return data.get(u"status") == "completed"
1690+
1691+ def status_get(self, unit):
1692+ """Return the current service status of this unit."""
1693+ raw_status, return_code = unit.run(
1694+ "status-get --format=json --include-data")
1695+ if return_code != 0:
1696+ return ("unknown", "")
1697+ status = json.loads(raw_status)
1698+ return (status["status"], status["message"])
1699
1700=== modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
1701--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-08-18 23:10:59 +0000
1702+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2016-01-13 18:53:03 +0000
1703@@ -14,12 +14,18 @@
1704 # You should have received a copy of the GNU Lesser General Public License
1705 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1706
1707+import logging
1708+import re
1709+import sys
1710 import six
1711 from collections import OrderedDict
1712 from charmhelpers.contrib.amulet.deployment import (
1713 AmuletDeployment
1714 )
1715
1716+DEBUG = logging.DEBUG
1717+ERROR = logging.ERROR
1718+
1719
1720 class OpenStackAmuletDeployment(AmuletDeployment):
1721 """OpenStack amulet deployment.
1722@@ -28,9 +34,12 @@
1723 that is specifically for use by OpenStack charms.
1724 """
1725
1726- def __init__(self, series=None, openstack=None, source=None, stable=True):
1727+ def __init__(self, series=None, openstack=None, source=None,
1728+ stable=True, log_level=DEBUG):
1729 """Initialize the deployment environment."""
1730 super(OpenStackAmuletDeployment, self).__init__(series)
1731+ self.log = self.get_logger(level=log_level)
1732+ self.log.info('OpenStackAmuletDeployment: init')
1733 self.openstack = openstack
1734 self.source = source
1735 self.stable = stable
1736@@ -38,26 +47,55 @@
1737 # out.
1738 self.current_next = "trusty"
1739
1740+ def get_logger(self, name="deployment-logger", level=logging.DEBUG):
1741+ """Get a logger object that will log to stdout."""
1742+ log = logging
1743+ logger = log.getLogger(name)
1744+ fmt = log.Formatter("%(asctime)s %(funcName)s "
1745+ "%(levelname)s: %(message)s")
1746+
1747+ handler = log.StreamHandler(stream=sys.stdout)
1748+ handler.setLevel(level)
1749+ handler.setFormatter(fmt)
1750+
1751+ logger.addHandler(handler)
1752+ logger.setLevel(level)
1753+
1754+ return logger
1755+
1756 def _determine_branch_locations(self, other_services):
1757 """Determine the branch locations for the other services.
1758
1759 Determine if the local branch being tested is derived from its
1760 stable or next (dev) branch, and based on this, use the corresonding
1761 stable or next branches for the other_services."""
1762+
1763+ self.log.info('OpenStackAmuletDeployment: determine branch locations')
1764+
1765+ # Charms outside the lp:~openstack-charmers namespace
1766 base_charms = ['mysql', 'mongodb', 'nrpe']
1767
1768+ # Force these charms to current series even when using an older series.
1769+ # ie. Use trusty/nrpe even when series is precise, as the P charm
1770+ # does not possess the necessary external master config and hooks.
1771+ force_series_current = ['nrpe']
1772+
1773 if self.series in ['precise', 'trusty']:
1774 base_series = self.series
1775 else:
1776 base_series = self.current_next
1777
1778- if self.stable:
1779- for svc in other_services:
1780+ for svc in other_services:
1781+ if svc['name'] in force_series_current:
1782+ base_series = self.current_next
1783+ # If a location has been explicitly set, use it
1784+ if svc.get('location'):
1785+ continue
1786+ if self.stable:
1787 temp = 'lp:charms/{}/{}'
1788 svc['location'] = temp.format(base_series,
1789 svc['name'])
1790- else:
1791- for svc in other_services:
1792+ else:
1793 if svc['name'] in base_charms:
1794 temp = 'lp:charms/{}/{}'
1795 svc['location'] = temp.format(base_series,
1796@@ -66,10 +104,13 @@
1797 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
1798 svc['location'] = temp.format(self.current_next,
1799 svc['name'])
1800+
1801 return other_services
1802
1803 def _add_services(self, this_service, other_services):
1804 """Add services to the deployment and set openstack-origin/source."""
1805+ self.log.info('OpenStackAmuletDeployment: adding services')
1806+
1807 other_services = self._determine_branch_locations(other_services)
1808
1809 super(OpenStackAmuletDeployment, self)._add_services(this_service,
1810@@ -77,29 +118,102 @@
1811
1812 services = other_services
1813 services.append(this_service)
1814+
1815+ # Charms which should use the source config option
1816 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
1817 'ceph-osd', 'ceph-radosgw']
1818- # Most OpenStack subordinate charms do not expose an origin option
1819- # as that is controlled by the principle.
1820- ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
1821+
1822+ # Charms which can not use openstack-origin, ie. many subordinates
1823+ no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe',
1824+ 'openvswitch-odl', 'neutron-api-odl', 'odl-controller']
1825
1826 if self.openstack:
1827 for svc in services:
1828- if svc['name'] not in use_source + ignore:
1829+ if svc['name'] not in use_source + no_origin:
1830 config = {'openstack-origin': self.openstack}
1831 self.d.configure(svc['name'], config)
1832
1833 if self.source:
1834 for svc in services:
1835- if svc['name'] in use_source and svc['name'] not in ignore:
1836+ if svc['name'] in use_source and svc['name'] not in no_origin:
1837 config = {'source': self.source}
1838 self.d.configure(svc['name'], config)
1839
1840 def _configure_services(self, configs):
1841 """Configure all of the services."""
1842+ self.log.info('OpenStackAmuletDeployment: configure services')
1843 for service, config in six.iteritems(configs):
1844 self.d.configure(service, config)
1845
1846+ def _auto_wait_for_status(self, message=None, exclude_services=None,
1847+ include_only=None, timeout=1800):
1848+ """Wait for all units to have a specific extended status, except
1849+ for any defined as excluded. Unless specified via message, any
1850+ status containing any case of 'ready' will be considered a match.
1851+
1852+ Examples of message usage:
1853+
1854+ Wait for all unit status to CONTAIN any case of 'ready' or 'ok':
1855+ message = re.compile('.*ready.*|.*ok.*', re.IGNORECASE)
1856+
1857+ Wait for all units to reach this status (exact match):
1858+ message = re.compile('^Unit is ready and clustered$')
1859+
1860+ Wait for all units to reach any one of these (exact match):
1861+ message = re.compile('Unit is ready|OK|Ready')
1862+
1863+ Wait for at least one unit to reach this status (exact match):
1864+ message = {'ready'}
1865+
1866+ See Amulet's sentry.wait_for_messages() for message usage detail.
1867+ https://github.com/juju/amulet/blob/master/amulet/sentry.py
1868+
1869+ :param message: Expected status match
1870+ :param exclude_services: List of juju service names to ignore,
1871+ not to be used in conjuction with include_only.
1872+ :param include_only: List of juju service names to exclusively check,
1873+ not to be used in conjuction with exclude_services.
1874+ :param timeout: Maximum time in seconds to wait for status match
1875+ :returns: None. Raises if timeout is hit.
1876+ """
1877+ self.log.info('Waiting for extended status on units...')
1878+
1879+ all_services = self.d.services.keys()
1880+
1881+ if exclude_services and include_only:
1882+ raise ValueError('exclude_services can not be used '
1883+ 'with include_only')
1884+
1885+ if message:
1886+ if isinstance(message, re._pattern_type):
1887+ match = message.pattern
1888+ else:
1889+ match = message
1890+
1891+ self.log.debug('Custom extended status wait match: '
1892+ '{}'.format(match))
1893+ else:
1894+ self.log.debug('Default extended status wait match: contains '
1895+ 'READY (case-insensitive)')
1896+ message = re.compile('.*ready.*', re.IGNORECASE)
1897+
1898+ if exclude_services:
1899+ self.log.debug('Excluding services from extended status match: '
1900+ '{}'.format(exclude_services))
1901+ else:
1902+ exclude_services = []
1903+
1904+ if include_only:
1905+ services = include_only
1906+ else:
1907+ services = list(set(all_services) - set(exclude_services))
1908+
1909+ self.log.debug('Waiting up to {}s for extended status on services: '
1910+ '{}'.format(timeout, services))
1911+ service_messages = {service: message for service in services}
1912+ self.d.sentry.wait_for_messages(service_messages, timeout=timeout)
1913+ self.log.info('OK')
1914+
1915 def _get_openstack_release(self):
1916 """Get openstack release.
1917
1918@@ -111,7 +225,8 @@
1919 self.precise_havana, self.precise_icehouse,
1920 self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
1921 self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
1922- self.wily_liberty) = range(12)
1923+ self.wily_liberty, self.trusty_mitaka,
1924+ self.xenial_mitaka) = range(14)
1925
1926 releases = {
1927 ('precise', None): self.precise_essex,
1928@@ -123,9 +238,11 @@
1929 ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
1930 ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
1931 ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
1932+ ('trusty', 'cloud:trusty-mitaka'): self.trusty_mitaka,
1933 ('utopic', None): self.utopic_juno,
1934 ('vivid', None): self.vivid_kilo,
1935- ('wily', None): self.wily_liberty}
1936+ ('wily', None): self.wily_liberty,
1937+ ('xenial', None): self.xenial_mitaka}
1938 return releases[(self.series, self.openstack)]
1939
1940 def _get_openstack_release_string(self):
1941@@ -142,6 +259,7 @@
1942 ('utopic', 'juno'),
1943 ('vivid', 'kilo'),
1944 ('wily', 'liberty'),
1945+ ('xenial', 'mitaka'),
1946 ])
1947 if self.openstack:
1948 os_origin = self.openstack.split(':')[1]
1949
1950=== modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
1951--- tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-09-23 19:17:20 +0000
1952+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2016-01-13 18:53:03 +0000
1953@@ -18,6 +18,7 @@
1954 import json
1955 import logging
1956 import os
1957+import re
1958 import six
1959 import time
1960 import urllib
1961@@ -27,6 +28,7 @@
1962 import heatclient.v1.client as heat_client
1963 import keystoneclient.v2_0 as keystone_client
1964 import novaclient.v1_1.client as nova_client
1965+import pika
1966 import swiftclient
1967
1968 from charmhelpers.contrib.amulet.utils import (
1969@@ -189,7 +191,9 @@
1970 tenant):
1971 """Authenticates admin user with the keystone admin endpoint."""
1972 self.log.debug('Authenticating keystone admin...')
1973- service_ip = keystone_sentry.info['public-address']
1974+ unit = keystone_sentry
1975+ service_ip = unit.relation('shared-db',
1976+ 'mysql:shared-db')['private-address']
1977 ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
1978 return keystone_client.Client(username=user, password=password,
1979 tenant_name=tenant, auth_url=ep)
1980@@ -600,3 +604,382 @@
1981 self.log.debug('Ceph {} samples (OK): '
1982 '{}'.format(sample_type, samples))
1983 return None
1984+
1985+ # rabbitmq/amqp specific helpers:
1986+
1987+ def rmq_wait_for_cluster(self, deployment, init_sleep=15, timeout=1200):
1988+ """Wait for rmq units extended status to show cluster readiness,
1989+ after an optional initial sleep period. Initial sleep is likely
1990+ necessary to be effective following a config change, as status
1991+ message may not instantly update to non-ready."""
1992+
1993+ if init_sleep:
1994+ time.sleep(init_sleep)
1995+
1996+ message = re.compile('^Unit is ready and clustered$')
1997+ deployment._auto_wait_for_status(message=message,
1998+ timeout=timeout,
1999+ include_only=['rabbitmq-server'])
2000+
2001+ def add_rmq_test_user(self, sentry_units,
2002+ username="testuser1", password="changeme"):
2003+ """Add a test user via the first rmq juju unit, check connection as
2004+ the new user against all sentry units.
2005+
2006+ :param sentry_units: list of sentry unit pointers
2007+ :param username: amqp user name, default to testuser1
2008+ :param password: amqp user password
2009+ :returns: None if successful. Raise on error.
2010+ """
2011+ self.log.debug('Adding rmq user ({})...'.format(username))
2012+
2013+ # Check that user does not already exist
2014+ cmd_user_list = 'rabbitmqctl list_users'
2015+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
2016+ if username in output:
2017+ self.log.warning('User ({}) already exists, returning '
2018+ 'gracefully.'.format(username))
2019+ return
2020+
2021+ perms = '".*" ".*" ".*"'
2022+ cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
2023+ 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
2024+
2025+ # Add user via first unit
2026+ for cmd in cmds:
2027+ output, _ = self.run_cmd_unit(sentry_units[0], cmd)
2028+
2029+ # Check connection against the other sentry_units
2030+ self.log.debug('Checking user connect against units...')
2031+ for sentry_unit in sentry_units:
2032+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
2033+ username=username,
2034+ password=password)
2035+ connection.close()
2036+
2037+ def delete_rmq_test_user(self, sentry_units, username="testuser1"):
2038+ """Delete a rabbitmq user via the first rmq juju unit.
2039+
2040+ :param sentry_units: list of sentry unit pointers
2041+ :param username: amqp user name, default to testuser1
2042+ :param password: amqp user password
2043+ :returns: None if successful or no such user.
2044+ """
2045+ self.log.debug('Deleting rmq user ({})...'.format(username))
2046+
2047+ # Check that the user exists
2048+ cmd_user_list = 'rabbitmqctl list_users'
2049+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
2050+
2051+ if username not in output:
2052+ self.log.warning('User ({}) does not exist, returning '
2053+ 'gracefully.'.format(username))
2054+ return
2055+
2056+ # Delete the user
2057+ cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
2058+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
2059+
2060+ def get_rmq_cluster_status(self, sentry_unit):
2061+ """Execute rabbitmq cluster status command on a unit and return
2062+ the full output.
2063+
2064+ :param unit: sentry unit
2065+ :returns: String containing console output of cluster status command
2066+ """
2067+ cmd = 'rabbitmqctl cluster_status'
2068+ output, _ = self.run_cmd_unit(sentry_unit, cmd)
2069+ self.log.debug('{} cluster_status:\n{}'.format(
2070+ sentry_unit.info['unit_name'], output))
2071+ return str(output)
2072+
2073+ def get_rmq_cluster_running_nodes(self, sentry_unit):
2074+ """Parse rabbitmqctl cluster_status output string, return list of
2075+ running rabbitmq cluster nodes.
2076+
2077+ :param unit: sentry unit
2078+ :returns: List containing node names of running nodes
2079+ """
2080+ # NOTE(beisner): rabbitmqctl cluster_status output is not
2081+ # json-parsable, do string chop foo, then json.loads that.
2082+ str_stat = self.get_rmq_cluster_status(sentry_unit)
2083+ if 'running_nodes' in str_stat:
2084+ pos_start = str_stat.find("{running_nodes,") + 15
2085+ pos_end = str_stat.find("]},", pos_start) + 1
2086+ str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"')
2087+ run_nodes = json.loads(str_run_nodes)
2088+ return run_nodes
2089+ else:
2090+ return []
2091+
2092+ def validate_rmq_cluster_running_nodes(self, sentry_units):
2093+ """Check that all rmq unit hostnames are represented in the
2094+ cluster_status output of all units.
2095+
2096+ :param host_names: dict of juju unit names to host names
2097+ :param units: list of sentry unit pointers (all rmq units)
2098+ :returns: None if successful, otherwise return error message
2099+ """
2100+ host_names = self.get_unit_hostnames(sentry_units)
2101+ errors = []
2102+
2103+ # Query every unit for cluster_status running nodes
2104+ for query_unit in sentry_units:
2105+ query_unit_name = query_unit.info['unit_name']
2106+ running_nodes = self.get_rmq_cluster_running_nodes(query_unit)
2107+
2108+ # Confirm that every unit is represented in the queried unit's
2109+ # cluster_status running nodes output.
2110+ for validate_unit in sentry_units:
2111+ val_host_name = host_names[validate_unit.info['unit_name']]
2112+ val_node_name = 'rabbit@{}'.format(val_host_name)
2113+
2114+ if val_node_name not in running_nodes:
2115+ errors.append('Cluster member check failed on {}: {} not '
2116+ 'in {}\n'.format(query_unit_name,
2117+ val_node_name,
2118+ running_nodes))
2119+ if errors:
2120+ return ''.join(errors)
2121+
2122+ def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None):
2123+ """Check a single juju rmq unit for ssl and port in the config file."""
2124+ host = sentry_unit.info['public-address']
2125+ unit_name = sentry_unit.info['unit_name']
2126+
2127+ conf_file = '/etc/rabbitmq/rabbitmq.config'
2128+ conf_contents = str(self.file_contents_safe(sentry_unit,
2129+ conf_file, max_wait=16))
2130+ # Checks
2131+ conf_ssl = 'ssl' in conf_contents
2132+ conf_port = str(port) in conf_contents
2133+
2134+ # Port explicitly checked in config
2135+ if port and conf_port and conf_ssl:
2136+ self.log.debug('SSL is enabled @{}:{} '
2137+ '({})'.format(host, port, unit_name))
2138+ return True
2139+ elif port and not conf_port and conf_ssl:
2140+ self.log.debug('SSL is enabled @{} but not on port {} '
2141+ '({})'.format(host, port, unit_name))
2142+ return False
2143+ # Port not checked (useful when checking that ssl is disabled)
2144+ elif not port and conf_ssl:
2145+ self.log.debug('SSL is enabled @{}:{} '
2146+ '({})'.format(host, port, unit_name))
2147+ return True
2148+ elif not conf_ssl:
2149+ self.log.debug('SSL not enabled @{}:{} '
2150+ '({})'.format(host, port, unit_name))
2151+ return False
2152+ else:
2153+ msg = ('Unknown condition when checking SSL status @{}:{} '
2154+ '({})'.format(host, port, unit_name))
2155+ amulet.raise_status(amulet.FAIL, msg)
2156+
2157+ def validate_rmq_ssl_enabled_units(self, sentry_units, port=None):
2158+ """Check that ssl is enabled on rmq juju sentry units.
2159+
2160+ :param sentry_units: list of all rmq sentry units
2161+ :param port: optional ssl port override to validate
2162+ :returns: None if successful, otherwise return error message
2163+ """
2164+ for sentry_unit in sentry_units:
2165+ if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port):
2166+ return ('Unexpected condition: ssl is disabled on unit '
2167+ '({})'.format(sentry_unit.info['unit_name']))
2168+ return None
2169+
2170+ def validate_rmq_ssl_disabled_units(self, sentry_units):
2171+ """Check that ssl is enabled on listed rmq juju sentry units.
2172+
2173+ :param sentry_units: list of all rmq sentry units
2174+ :returns: True if successful. Raise on error.
2175+ """
2176+ for sentry_unit in sentry_units:
2177+ if self.rmq_ssl_is_enabled_on_unit(sentry_unit):
2178+ return ('Unexpected condition: ssl is enabled on unit '
2179+ '({})'.format(sentry_unit.info['unit_name']))
2180+ return None
2181+
2182+ def configure_rmq_ssl_on(self, sentry_units, deployment,
2183+ port=None, max_wait=60):
2184+ """Turn ssl charm config option on, with optional non-default
2185+ ssl port specification. Confirm that it is enabled on every
2186+ unit.
2187+
2188+ :param sentry_units: list of sentry units
2189+ :param deployment: amulet deployment object pointer
2190+ :param port: amqp port, use defaults if None
2191+ :param max_wait: maximum time to wait in seconds to confirm
2192+ :returns: None if successful. Raise on error.
2193+ """
2194+ self.log.debug('Setting ssl charm config option: on')
2195+
2196+ # Enable RMQ SSL
2197+ config = {'ssl': 'on'}
2198+ if port:
2199+ config['ssl_port'] = port
2200+
2201+ deployment.d.configure('rabbitmq-server', config)
2202+
2203+ # Wait for unit status
2204+ self.rmq_wait_for_cluster(deployment)
2205+
2206+ # Confirm
2207+ tries = 0
2208+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
2209+ while ret and tries < (max_wait / 4):
2210+ time.sleep(4)
2211+ self.log.debug('Attempt {}: {}'.format(tries, ret))
2212+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
2213+ tries += 1
2214+
2215+ if ret:
2216+ amulet.raise_status(amulet.FAIL, ret)
2217+
2218+ def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60):
2219+ """Turn ssl charm config option off, confirm that it is disabled
2220+ on every unit.
2221+
2222+ :param sentry_units: list of sentry units
2223+ :param deployment: amulet deployment object pointer
2224+ :param max_wait: maximum time to wait in seconds to confirm
2225+ :returns: None if successful. Raise on error.
2226+ """
2227+ self.log.debug('Setting ssl charm config option: off')
2228+
2229+ # Disable RMQ SSL
2230+ config = {'ssl': 'off'}
2231+ deployment.d.configure('rabbitmq-server', config)
2232+
2233+ # Wait for unit status
2234+ self.rmq_wait_for_cluster(deployment)
2235+
2236+ # Confirm
2237+ tries = 0
2238+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
2239+ while ret and tries < (max_wait / 4):
2240+ time.sleep(4)
2241+ self.log.debug('Attempt {}: {}'.format(tries, ret))
2242+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
2243+ tries += 1
2244+
2245+ if ret:
2246+ amulet.raise_status(amulet.FAIL, ret)
2247+
2248+ def connect_amqp_by_unit(self, sentry_unit, ssl=False,
2249+ port=None, fatal=True,
2250+ username="testuser1", password="changeme"):
2251+ """Establish and return a pika amqp connection to the rabbitmq service
2252+ running on a rmq juju unit.
2253+
2254+ :param sentry_unit: sentry unit pointer
2255+ :param ssl: boolean, default to False
2256+ :param port: amqp port, use defaults if None
2257+ :param fatal: boolean, default to True (raises on connect error)
2258+ :param username: amqp user name, default to testuser1
2259+ :param password: amqp user password
2260+ :returns: pika amqp connection pointer or None if failed and non-fatal
2261+ """
2262+ host = sentry_unit.info['public-address']
2263+ unit_name = sentry_unit.info['unit_name']
2264+
2265+ # Default port logic if port is not specified
2266+ if ssl and not port:
2267+ port = 5671
2268+ elif not ssl and not port:
2269+ port = 5672
2270+
2271+ self.log.debug('Connecting to amqp on {}:{} ({}) as '
2272+ '{}...'.format(host, port, unit_name, username))
2273+
2274+ try:
2275+ credentials = pika.PlainCredentials(username, password)
2276+ parameters = pika.ConnectionParameters(host=host, port=port,
2277+ credentials=credentials,
2278+ ssl=ssl,
2279+ connection_attempts=3,
2280+ retry_delay=5,
2281+ socket_timeout=1)
2282+ connection = pika.BlockingConnection(parameters)
2283+ assert connection.server_properties['product'] == 'RabbitMQ'
2284+ self.log.debug('Connect OK')
2285+ return connection
2286+ except Exception as e:
2287+ msg = ('amqp connection failed to {}:{} as '
2288+ '{} ({})'.format(host, port, username, str(e)))
2289+ if fatal:
2290+ amulet.raise_status(amulet.FAIL, msg)
2291+ else:
2292+ self.log.warn(msg)
2293+ return None
2294+
2295+ def publish_amqp_message_by_unit(self, sentry_unit, message,
2296+ queue="test", ssl=False,
2297+ username="testuser1",
2298+ password="changeme",
2299+ port=None):
2300+ """Publish an amqp message to a rmq juju unit.
2301+
2302+ :param sentry_unit: sentry unit pointer
2303+ :param message: amqp message string
2304+ :param queue: message queue, default to test
2305+ :param username: amqp user name, default to testuser1
2306+ :param password: amqp user password
2307+ :param ssl: boolean, default to False
2308+ :param port: amqp port, use defaults if None
2309+ :returns: None. Raises exception if publish failed.
2310+ """
2311+ self.log.debug('Publishing message to {} queue:\n{}'.format(queue,
2312+ message))
2313+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
2314+ port=port,
2315+ username=username,
2316+ password=password)
2317+
2318+ # NOTE(beisner): extra debug here re: pika hang potential:
2319+ # https://github.com/pika/pika/issues/297
2320+ # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw
2321+ self.log.debug('Defining channel...')
2322+ channel = connection.channel()
2323+ self.log.debug('Declaring queue...')
2324+ channel.queue_declare(queue=queue, auto_delete=False, durable=True)
2325+ self.log.debug('Publishing message...')
2326+ channel.basic_publish(exchange='', routing_key=queue, body=message)
2327+ self.log.debug('Closing channel...')
2328+ channel.close()
2329+ self.log.debug('Closing connection...')
2330+ connection.close()
2331+
2332+ def get_amqp_message_by_unit(self, sentry_unit, queue="test",
2333+ username="testuser1",
2334+ password="changeme",
2335+ ssl=False, port=None):
2336+ """Get an amqp message from a rmq juju unit.
2337+
2338+ :param sentry_unit: sentry unit pointer
2339+ :param queue: message queue, default to test
2340+ :param username: amqp user name, default to testuser1
2341+ :param password: amqp user password
2342+ :param ssl: boolean, default to False
2343+ :param port: amqp port, use defaults if None
2344+ :returns: amqp message body as string. Raise if get fails.
2345+ """
2346+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
2347+ port=port,
2348+ username=username,
2349+ password=password)
2350+ channel = connection.channel()
2351+ method_frame, _, body = channel.basic_get(queue)
2352+
2353+ if method_frame:
2354+ self.log.debug('Retreived message from {} queue:\n{}'.format(queue,
2355+ body))
2356+ channel.basic_ack(method_frame.delivery_tag)
2357+ channel.close()
2358+ connection.close()
2359+ return body
2360+ else:
2361+ msg = 'No message retrieved.'
2362+ amulet.raise_status(amulet.FAIL, msg)
2363
2364=== added directory 'tests/setup'
2365=== renamed file 'tests/00-setup' => 'tests/setup/00-setup'

Subscribers

People subscribed via source and target branches

to all changes: