Merge lp:~tribaal/charms/trusty/rabbitmq-server/backport-lp1500204-with-next-tests into lp:charms/trusty/rabbitmq-server

Proposed by Chris Glass
Status: Rejected
Rejected by: Chris Glass
Proposed branch: lp:~tribaal/charms/trusty/rabbitmq-server/backport-lp1500204-with-next-tests
Merge into: lp:charms/trusty/rabbitmq-server
Diff against target: 8993 lines (+3836/-4750)
64 files modified
hooks/rabbit_utils.py (+72/-50)
hooks/rabbitmq_server_relations.py (+19/-12)
metadata.yaml (+1/-1)
tests/00-setup (+17/-0)
tests/00_setup.sh (+0/-21)
tests/014-basic-precise-icehouse (+11/-0)
tests/015-basic-trusty-icehouse (+9/-0)
tests/016-basic-trusty-juno (+11/-0)
tests/017-basic-trusty-kilo (+11/-0)
tests/019-basic-vivid-kilo (+9/-0)
tests/020-basic-trusty-liberty (+11/-0)
tests/021-basic-wily-liberty (+9/-0)
tests/10-outofthebox-testing (+0/-30)
tests/10_basic_deploy_test.py (+0/-92)
tests/20-different-repositories (+0/-94)
tests/20_deploy_relations_test.py (+0/-251)
tests/30-switch-ssl (+0/-117)
tests/30_configuration_test.py (+0/-118)
tests/40_test_mirroring_queues.py (+0/-85)
tests/50_test_monitoring.py (+0/-71)
tests/amqp_tester.py (+0/-65)
tests/basic_deployment.py (+492/-0)
tests/charmhelpers/cli/__init__.py (+0/-191)
tests/charmhelpers/cli/benchmark.py (+0/-36)
tests/charmhelpers/cli/commands.py (+0/-32)
tests/charmhelpers/cli/hookenv.py (+0/-23)
tests/charmhelpers/cli/host.py (+0/-31)
tests/charmhelpers/cli/unitdata.py (+0/-39)
tests/charmhelpers/contrib/amulet/__init__.py (+15/-0)
tests/charmhelpers/contrib/amulet/deployment.py (+93/-0)
tests/charmhelpers/contrib/amulet/utils.py (+778/-0)
tests/charmhelpers/contrib/openstack/__init__.py (+15/-0)
tests/charmhelpers/contrib/openstack/amulet/__init__.py (+15/-0)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+198/-0)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+963/-0)
tests/charmhelpers/contrib/ssl/__init__.py (+0/-94)
tests/charmhelpers/contrib/ssl/service.py (+0/-279)
tests/charmhelpers/core/__init__.py (+0/-15)
tests/charmhelpers/core/decorators.py (+0/-57)
tests/charmhelpers/core/files.py (+0/-45)
tests/charmhelpers/core/fstab.py (+0/-134)
tests/charmhelpers/core/hookenv.py (+0/-896)
tests/charmhelpers/core/host.py (+0/-494)
tests/charmhelpers/core/services/__init__.py (+0/-18)
tests/charmhelpers/core/services/base.py (+0/-353)
tests/charmhelpers/core/services/helpers.py (+0/-267)
tests/charmhelpers/core/strutils.py (+0/-42)
tests/charmhelpers/core/sysctl.py (+0/-56)
tests/charmhelpers/core/templating.py (+0/-68)
tests/charmhelpers/core/unitdata.py (+0/-521)
tests/deploy_common.py (+0/-52)
tests/deprecated/00_setup.sh (+21/-0)
tests/deprecated/10-outofthebox-testing (+30/-0)
tests/deprecated/10_basic_deploy_test.py (+92/-0)
tests/deprecated/20-different-repositories (+94/-0)
tests/deprecated/20_deploy_relations_test.py (+251/-0)
tests/deprecated/30-switch-ssl (+117/-0)
tests/deprecated/30_configuration_test.py (+118/-0)
tests/deprecated/40_test_mirroring_queues.py (+85/-0)
tests/deprecated/50_test_monitoring.py (+71/-0)
tests/deprecated/amqp_tester.py (+65/-0)
tests/deprecated/deploy_common.py (+52/-0)
tests/tests.yaml (+20/-0)
unit_tests/test_rabbit_utils.py (+71/-0)
To merge this branch: bzr merge lp:~tribaal/charms/trusty/rabbitmq-server/backport-lp1500204-with-next-tests
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+274291@code.launchpad.net

Description of the change

This branch is a backport to -stable of the patch/diff fixing LP:1500204 here: https://code.launchpad.net/~thedac/charms/trusty/rabbitmq-server/le-ignore-min-cluster/+merge/273474

This basically makes the charm ignore the min-cluster-size option in case juju leader-election is available. If it is, the nodes are told to cluster with the leader.

This brnahc also backports the -next tests since they were fixed to actually work with OSCI.

To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #10962 rabbitmq-server for tribaal mp274291
    UNIT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #11785 rabbitmq-server for tribaal mp274291
    LINT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #7302 rabbitmq-server for tribaal mp274291
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
2015-10-13 17:59:48,192 get_unit_hostnames DEBUG: Unit host names: {'rabbitmq-server/2': 'juju-osci-sv17-machine-4', 'rabbitmq-server/0': 'juju-osci-sv17-machine-2', 'rabbitmq-server/1': 'juju-osci-sv17-machine-3'}
ERROR:root:Make target returned non-zero.

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

113. By Chris Glass

Ripping out test 415 since this feature is not in -stable.

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #11788 rabbitmq-server for tribaal mp274291
    LINT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #10965 rabbitmq-server for tribaal mp274291
    UNIT OK: passed

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

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #7312 rabbitmq-server for tribaal mp274291
    AMULET FAIL: amulet-test failed

AMULET Results (max last 2 lines):
MESSAGE 9@172.17.102.2 [B174F62C-1377-439D-918E-63DA20F1D285-1444797831.92]
ERROR:root:Make target returned non-zero.

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

Unmerged revisions

113. By Chris Glass

Ripping out test 415 since this feature is not in -stable.

112. By Chris Glass

Stable=True

111. By Chris Glass

Ripped out and grafted the -next tests to the stable branch.

"Cursed, cursed creator! Why did I live?"

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/rabbit_utils.py'
2--- hooks/rabbit_utils.py 2015-09-23 19:36:14 +0000
3+++ hooks/rabbit_utils.py 2015-10-13 18:33:25 +0000
4@@ -6,6 +6,8 @@
5 import subprocess
6 import glob
7 import tempfile
8+import random
9+import time
10
11 from charmhelpers.contrib.openstack.utils import (
12 get_hostname,
13@@ -13,12 +15,10 @@
14
15 from charmhelpers.core.hookenv import (
16 config,
17- relation_ids,
18- relation_get,
19- related_units,
20 log, ERROR,
21 INFO,
22- service_name
23+ service_name,
24+ cached,
25 )
26
27 from charmhelpers.core.host import (
28@@ -242,68 +242,42 @@
29 cluster_cmd = 'join_cluster'
30 else:
31 cluster_cmd = 'cluster'
32- out = subprocess.check_output([RABBITMQ_CTL, 'cluster_status'])
33- log('cluster status is %s' % str(out))
34-
35- # check if node is already clustered
36- total_nodes = 1
37- running_nodes = []
38- m = re.search("\{running_nodes,\[(.*?)\]\}", out.strip(), re.DOTALL)
39- if m is not None:
40- running_nodes = m.group(1).split(',')
41- running_nodes = [x.replace("'", '') for x in running_nodes]
42- total_nodes = len(running_nodes)
43-
44- if total_nodes > 1:
45+
46+ if clustered():
47 log('Node is already clustered, skipping')
48 return False
49
50- # check all peers and try to cluster with them
51- available_nodes = []
52- for r_id in relation_ids('cluster'):
53- for unit in related_units(r_id):
54- if config('prefer-ipv6'):
55- address = relation_get('hostname',
56- rid=r_id, unit=unit)
57- else:
58- address = relation_get('private-address',
59- rid=r_id, unit=unit)
60- if address is not None:
61- node = get_hostname(address, fqdn=False)
62- if node:
63- available_nodes.append(node)
64- else:
65- log('Cannot resolve hostname for {} '
66- 'using DNS servers'.format(address), level='WARNING')
67-
68- if len(available_nodes) == 0:
69+ # Check the leader and try to cluster with it
70+ if len(leader_node()) == 0:
71 log('No nodes available to cluster with')
72 return False
73
74- # iterate over all the nodes, join to the first available
75 num_tries = 0
76- for node in available_nodes:
77- log('Clustering with remote rabbit host (%s).' % node)
78- if node in running_nodes:
79+ for node in leader_node():
80+ if node in running_nodes():
81 log('Host already clustered with %s.' % node)
82 return False
83-
84+ # NOTE: The primary problem rabbitmq has clustering is when
85+ # more than one node attempts to cluster at the same time.
86+ # The asynchronous nature of hook firing nearly guarantees
87+ # this. Using random time wait is a hack until we can
88+ # implement charmhelpers.coordinator.
89+ time.sleep(random.random()*100)
90+ log('Clustering with remote rabbit host (%s).' % node)
91 try:
92 cmd = [RABBITMQ_CTL, 'stop_app']
93 subprocess.check_call(cmd)
94- cmd = [RABBITMQ_CTL, cluster_cmd, 'rabbit@%s' % node]
95- try:
96- subprocess.check_output(cmd, stderr=subprocess.STDOUT)
97- except subprocess.CalledProcessError as e:
98- if not e.returncode == 2 or \
99- "{ok,already_member}" not in e.output:
100- raise e
101+ cmd = [RABBITMQ_CTL, cluster_cmd, node]
102+ subprocess.check_output(cmd, stderr=subprocess.STDOUT)
103 cmd = [RABBITMQ_CTL, 'start_app']
104 subprocess.check_call(cmd)
105 log('Host clustered with %s.' % node)
106 return True
107- except:
108- log('Failed to cluster with %s.' % node)
109+ except subprocess.CalledProcessError as e:
110+ log('Failed to cluster with %s. Exception: %s'
111+ % (node, e))
112+ cmd = [RABBITMQ_CTL, 'start_app']
113+ subprocess.check_call(cmd)
114 # continue to the next node
115 num_tries += 1
116 if num_tries > config('max-cluster-tries'):
117@@ -591,3 +565,51 @@
118 for v in restart_map().values():
119 _services = _services + v
120 return list(set(_services))
121+
122+
123+def running_nodes():
124+ ''' Determine the current set of running rabbitmq-units in the cluster '''
125+ out = subprocess.check_output([RABBITMQ_CTL, 'cluster_status'])
126+
127+ running_nodes = []
128+ m = re.search("\{running_nodes,\[(.*?)\]\}", out.strip(), re.DOTALL)
129+ if m is not None:
130+ running_nodes = m.group(1).split(',')
131+ running_nodes = [x.replace("'", '').strip() for x in running_nodes]
132+
133+ return running_nodes
134+
135+
136+@cached
137+def leader_node():
138+ ''' Provide the leader node for clustering '''
139+ # Each rabbitmq node should join_cluster with the leader
140+ # to avoid split-brain clusters.
141+ leader_node_ip = peer_retrieve('leader_node_ip')
142+ if leader_node_ip:
143+ return ["rabbit@" + get_node_hostname(leader_node_ip)]
144+ else:
145+ return []
146+
147+
148+def get_node_hostname(address):
149+ ''' Resolve IP address to hostname for nodes '''
150+ node = get_hostname(address, fqdn=False)
151+ if node:
152+ return node
153+ else:
154+ log('Cannot resolve hostname for {} using DNS servers'.format(address),
155+ level='WARNING')
156+ return None
157+
158+
159+@cached
160+def clustered():
161+ ''' Determine whether local rabbitmq-server is clustered '''
162+ # NOTE: A rabbitmq node can only join a cluster once.
163+ # Simply checking for more than one running node tells us
164+ # if this unit is in a cluster.
165+ if len(running_nodes()) > 1:
166+ return True
167+ else:
168+ return False
169
170=== modified file 'hooks/rabbitmq_server_relations.py'
171--- hooks/rabbitmq_server_relations.py 2015-09-23 19:36:14 +0000
172+++ hooks/rabbitmq_server_relations.py 2015-10-13 18:33:25 +0000
173@@ -63,6 +63,7 @@
174 UnregisteredHookError,
175 is_leader,
176 charm_dir,
177+ unit_private_ip,
178 )
179 from charmhelpers.core.host import (
180 cmp_pkgrevno,
181@@ -257,16 +258,23 @@
182 """
183 min_size = config('min-cluster-size')
184 if min_size:
185- size = 0
186- for rid in relation_ids('cluster'):
187- size = len(related_units(rid))
188+ # Ignore min-cluster-size if juju has leadership election
189+ try:
190+ is_leader()
191+ log("Ignoring min-cluster-size in favour of Juju leader election")
192+ return True
193+ except NotImplementedError:
194+ log("Leader election is not available, using min-cluster-size")
195+ size = 0
196+ for rid in relation_ids('cluster'):
197+ size = len(related_units(rid))
198
199- # Include this unit
200- size += 1
201- if min_size > size:
202- log("Insufficient number of peer units to form cluster "
203- "(expected=%s, got=%s)" % (min_size, size), level=INFO)
204- return False
205+ # Include this unit
206+ size += 1
207+ if min_size > size:
208+ log("Insufficient number of peer units to form cluster "
209+ "(expected=%s, got=%s)" % (min_size, size), level=INFO)
210+ return False
211
212 return True
213
214@@ -315,6 +323,7 @@
215 log('Leader peer_storing cookie', level=INFO)
216 cookie = open(rabbit.COOKIE_PATH, 'r').read().strip()
217 peer_store('cookie', cookie)
218+ peer_store('leader_node_ip', unit_private_ip())
219
220
221 @hooks.hook('cluster-relation-changed')
222@@ -341,10 +350,8 @@
223 rabbit.update_hosts_file({private_address: hostname})
224
225 if not is_sufficient_peers():
226- # Stop rabbit until leader has finished configuring
227- log('Not enough peers, stopping until leader is configured',
228+ log('Not enough peers, waiting until leader is configured',
229 level=INFO)
230- service_stop('rabbitmq-server')
231 return
232
233 # sync the cookie with peers if necessary
234
235=== modified file 'metadata.yaml'
236--- metadata.yaml 2013-11-15 19:15:16 +0000
237+++ metadata.yaml 2015-10-13 18:33:25 +0000
238@@ -5,7 +5,7 @@
239 RabbitMQ is an implementation of AMQP, the emerging standard for high
240 performance enterprise messaging. The RabbitMQ server is a robust and
241 scalable implementation of an AMQP broker.
242-categories: ["misc"]
243+tags: ["misc"]
244 provides:
245 amqp:
246 interface: rabbitmq
247
248=== added file 'tests/00-setup'
249--- tests/00-setup 1970-01-01 00:00:00 +0000
250+++ tests/00-setup 2015-10-13 18:33:25 +0000
251@@ -0,0 +1,17 @@
252+#!/bin/bash
253+
254+set -ex
255+
256+sudo add-apt-repository --yes ppa:juju/stable
257+sudo apt-get update --yes
258+sudo apt-get install --yes amulet \
259+ distro-info-data \
260+ python-cinderclient \
261+ python-distro-info \
262+ python-glanceclient \
263+ python-heatclient \
264+ python-keystoneclient \
265+ python-neutronclient \
266+ python-novaclient \
267+ python-pika \
268+ python-swiftclient
269
270=== removed file 'tests/00_setup.sh'
271--- tests/00_setup.sh 2015-04-09 14:31:29 +0000
272+++ tests/00_setup.sh 1970-01-01 00:00:00 +0000
273@@ -1,21 +0,0 @@
274-#!/bin/sh
275-
276-# Install Juju Amulet and any other applications that are needed for the tests.
277-
278-set -x
279-
280-# Check if amulet is installed before adding repository and updating apt-get.
281-dpkg -s amulet
282-if [ $? -ne 0 ]; then
283- sudo add-apt-repository -y ppa:juju/stable
284- sudo apt-get update
285- sudo apt-get install -y amulet
286-fi
287-
288-# Install any additional python packages, or software here.
289-sudo apt-get install -y python python-pika python3-requests python3-setuptools
290-
291-# Set http proxy if amulet is using one.
292-[[ -n "$AMULET_HTTP_PROXY" ]] && export http_proxy="$AMULET_HTTP_PROXY" && export https_proxy="$AMULET_HTTP_PROXY"
293-
294-sudo easy_install3 python3-pika
295
296=== added file 'tests/014-basic-precise-icehouse'
297--- tests/014-basic-precise-icehouse 1970-01-01 00:00:00 +0000
298+++ tests/014-basic-precise-icehouse 2015-10-13 18:33:25 +0000
299@@ -0,0 +1,11 @@
300+#!/usr/bin/python
301+
302+"""Amulet tests on a basic rabbitmq-server deployment on precise-icehouse."""
303+
304+from basic_deployment import RmqBasicDeployment
305+
306+if __name__ == '__main__':
307+ deployment = RmqBasicDeployment(series='precise',
308+ openstack='cloud:precise-icehouse',
309+ source='cloud:precise-updates/icehouse')
310+ deployment.run_tests()
311
312=== added file 'tests/015-basic-trusty-icehouse'
313--- tests/015-basic-trusty-icehouse 1970-01-01 00:00:00 +0000
314+++ tests/015-basic-trusty-icehouse 2015-10-13 18:33:25 +0000
315@@ -0,0 +1,9 @@
316+#!/usr/bin/python
317+
318+"""Amulet tests on a basic rabbitmq-server deployment on trusty-icehouse."""
319+
320+from basic_deployment import RmqBasicDeployment
321+
322+if __name__ == '__main__':
323+ deployment = RmqBasicDeployment(series='trusty')
324+ deployment.run_tests()
325
326=== added file 'tests/016-basic-trusty-juno'
327--- tests/016-basic-trusty-juno 1970-01-01 00:00:00 +0000
328+++ tests/016-basic-trusty-juno 2015-10-13 18:33:25 +0000
329@@ -0,0 +1,11 @@
330+#!/usr/bin/python
331+
332+"""Amulet tests on a basic rabbitmq-server deployment on trusty-juno."""
333+
334+from basic_deployment import RmqBasicDeployment
335+
336+if __name__ == '__main__':
337+ deployment = RmqBasicDeployment(series='trusty',
338+ openstack='cloud:trusty-juno',
339+ source='cloud:trusty-updates/juno')
340+ deployment.run_tests()
341
342=== added file 'tests/017-basic-trusty-kilo'
343--- tests/017-basic-trusty-kilo 1970-01-01 00:00:00 +0000
344+++ tests/017-basic-trusty-kilo 2015-10-13 18:33:25 +0000
345@@ -0,0 +1,11 @@
346+#!/usr/bin/python
347+
348+"""Amulet tests on a basic rabbitmq-server deployment on trusty-kilo."""
349+
350+from basic_deployment import RmqBasicDeployment
351+
352+if __name__ == '__main__':
353+ deployment = RmqBasicDeployment(series='trusty',
354+ openstack='cloud:trusty-kilo',
355+ source='cloud:trusty-updates/kilo')
356+ deployment.run_tests()
357
358=== added file 'tests/019-basic-vivid-kilo'
359--- tests/019-basic-vivid-kilo 1970-01-01 00:00:00 +0000
360+++ tests/019-basic-vivid-kilo 2015-10-13 18:33:25 +0000
361@@ -0,0 +1,9 @@
362+#!/usr/bin/python
363+
364+"""Amulet tests on a basic rabbitmq-server deployment on vivid-kilo."""
365+
366+from basic_deployment import RmqBasicDeployment
367+
368+if __name__ == '__main__':
369+ deployment = RmqBasicDeployment(series='vivid')
370+ deployment.run_tests()
371
372=== added file 'tests/020-basic-trusty-liberty'
373--- tests/020-basic-trusty-liberty 1970-01-01 00:00:00 +0000
374+++ tests/020-basic-trusty-liberty 2015-10-13 18:33:25 +0000
375@@ -0,0 +1,11 @@
376+#!/usr/bin/python
377+
378+"""Amulet tests on a basic rabbitmq-server deployment on trusty-liberty."""
379+
380+from basic_deployment import RmqBasicDeployment
381+
382+if __name__ == '__main__':
383+ deployment = RmqBasicDeployment(series='trusty',
384+ openstack='cloud:trusty-liberty',
385+ source='cloud:trusty-updates/liberty')
386+ deployment.run_tests()
387
388=== added file 'tests/021-basic-wily-liberty'
389--- tests/021-basic-wily-liberty 1970-01-01 00:00:00 +0000
390+++ tests/021-basic-wily-liberty 2015-10-13 18:33:25 +0000
391@@ -0,0 +1,9 @@
392+#!/usr/bin/python
393+
394+"""Amulet tests on a basic rabbitmq-server deployment on wily-liberty."""
395+
396+from basic_deployment import RmqBasicDeployment
397+
398+if __name__ == '__main__':
399+ deployment = RmqBasicDeployment(series='wily')
400+ deployment.run_tests()
401
402=== removed file 'tests/10-outofthebox-testing'
403--- tests/10-outofthebox-testing 2015-04-15 04:35:17 +0000
404+++ tests/10-outofthebox-testing 1970-01-01 00:00:00 +0000
405@@ -1,30 +0,0 @@
406-#!/usr/bin/python3
407-
408-import amulet
409-import pika
410-
411-d = amulet.Deployment(series='trusty')
412-
413-d.add('rabbitmq-server')
414-d.expose('rabbitmq-server')
415-
416-# Don't forget to expose using d.expose(service)
417-
418-try:
419- d.setup(timeout=3000)
420- d.sentry.wait()
421-except amulet.helpers.TimeoutError:
422- amulet.raise_status(amulet.SKIP, msg="Environment wasn't stood up in time")
423-except:
424- raise
425-
426-server = d.sentry.unit['rabbitmq-server/0']
427-host = server.info['public-address']
428-
429-try:
430- connection = pika.BlockingConnection(pika.ConnectionParameters(host=host))
431-except Exception as e:
432- amulet.raise_status(
433- amulet.FAIL,
434- str(e)
435- )
436
437=== removed file 'tests/10_basic_deploy_test.py'
438--- tests/10_basic_deploy_test.py 2015-04-15 07:59:43 +0000
439+++ tests/10_basic_deploy_test.py 1970-01-01 00:00:00 +0000
440@@ -1,92 +0,0 @@
441-#!/usr/bin/python3
442-
443-# This Amulet test performs a basic deploy and checks if rabbitmq is running.
444-
445-import amulet
446-import os
447-import socket
448-import ssl
449-from deploy_common import CA
450-
451-# The number of seconds to wait for the environment to setup.
452-seconds = 900
453-# Get the directory in this way to load the files from the tests directory.
454-path = os.path.abspath(os.path.dirname(__file__))
455-
456-ca = CA()
457-
458-# Create a dictionary for the rabbitmq configuration.
459-rabbitmq_configuration = {
460- 'ssl_enabled': True,
461- 'ssl_key': ca.get_key(),
462- 'ssl_cert': ca.get_cert(),
463- 'ssl_port': 5671
464-}
465-
466-d = amulet.Deployment(series='trusty')
467-# Add the rabbitmq-server charm to the deployment.
468-d.add('rabbitmq-server')
469-# Configure options on the rabbitmq-server.
470-d.configure('rabbitmq-server', rabbitmq_configuration)
471-# Expose the server so we can connect.
472-d.expose('rabbitmq-server')
473-
474-try:
475- # Execute the deployer with the current mapping.
476- d.setup(timeout=seconds)
477-except amulet.helpers.TimeoutError:
478- message = 'The environment did not setup in %d seconds.' % seconds
479- # The SKIP status enables skip or fail the test based on configuration.
480- amulet.raise_status(amulet.SKIP, msg=message)
481-except:
482- raise
483-print('The rabbitmq-server has been successfully deployed.')
484-
485-###############################################################################
486-# Verify that the rabbit service is running on the deployed server.
487-###############################################################################
488-rabbitmq_sentry = d.sentry.unit['rabbitmq-server/0']
489-# Get the public address for rabbitmq-server instance.
490-server_address = rabbitmq_sentry.info['public-address']
491-# Create the command that checks if the rabbitmq-server service is running.
492-command = 'rabbitmqctl status'
493-print(command)
494-# Execute the command on the deployed service.
495-output, code = rabbitmq_sentry.run(command)
496-print(output)
497-# Check the return code for the success and failure of this test.
498-if (code != 0):
499- message = 'The ' + command + ' did not return the expected code of 0.'
500- amulet.raise_status(amulet.FAIL, msg=message)
501-else:
502- print('The rabbitmq-server is running on %s' % server_address)
503-
504-###############################################################################
505-# Test the ssl certificate.
506-###############################################################################
507-# Get the port for ssl_port instance.
508-server_port = rabbitmq_configuration['ssl_port']
509-
510-print('Testing ssl connection to rabbitmq-server.')
511-try:
512- # Create a normal socket.
513- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
514- # Require a certificate from the server, since a self-signed certificate
515- # was used, the ca_certs must be the server certificate file itself.
516- ssl_sock = ssl.wrap_socket(s, ca_certs=ca.ca_cert_path(),
517- cert_reqs=ssl.CERT_REQUIRED)
518- # Connect to the rabbitmq server using ssl.
519- ssl_sock.connect((server_address, server_port))
520- # Get the certificate.
521- certificate = ssl_sock.getpeercert()
522- # SSL socket connected and got the certificate, this passes the ssl test!
523- print('Connected to the rabbitmq-server {0}:{1} using ssl!'.format(
524- server_address, server_port))
525-except Exception as e:
526- message = 'Failed to create an ssl connection to {0}:{1}\n{2}'.format(
527- server_address, server_port, str(e))
528- amulet.raise_status(amulet.FAIL, msg=message)
529-finally:
530- ssl_sock.close()
531-
532-print('The rabbitmq-server passed the basic deploy test!')
533
534=== removed file 'tests/20-different-repositories'
535--- tests/20-different-repositories 2015-04-15 20:55:40 +0000
536+++ tests/20-different-repositories 1970-01-01 00:00:00 +0000
537@@ -1,94 +0,0 @@
538-#!/usr/bin/python3
539-
540-import amulet
541-import os
542-import pika
543-import subprocess
544-import time
545-
546-# The juju environment should be bootstrapped at this point, but depending
547-# on the test environment an http proxy may be necessary. Check to see if
548-# one is configured via the environment variable and set it if it is.
549-# The settings of these proxy variables should only survive the current
550-# lifetime of the bootstrapped environment and will be torn down for the
551-# next test.
552-if os.environ.get('AMULET_HTTP_PROXY', None):
553- proxy_setting = 'http-proxy={}'.format(os.environ['AMULET_HTTP_PROXY'])
554- subprocess.call(['juju', 'set-env', proxy_setting])
555-
556-if os.environ.get('AMULET_HTTPS_PROXY', None):
557- proxy_setting = 'https-proxy={}'.format(os.environ['AMULET_HTTPS_PROXY'])
558- subprocess.call(['juju', 'set-env', proxy_setting])
559-
560-
561-d = amulet.Deployment(series='trusty')
562-
563-d.add('rabbitmq-server')
564-d.configure('rabbitmq-server', {
565- "source": "deb http://www.rabbitmq.com/debian/ testing main",
566- "key": """-----BEGIN PGP PUBLIC KEY BLOCK-----
567-Version: GnuPG v1.4.6 (GNU/Linux)
568-
569-mQGiBEaOQ/IRBACs/n609zN+OzlK9qDkFWwYKfPG+BlgqBj5MSy0XE2K8cE3bWSV
570-2WftTe/TGEfW0hknXt1PyBla0cnO9Up1xCn142vo8bvUug8WjrxLQBBiAf11FAOR
571-dt9roGe4IWw/Lakgb88re09ZYKmOL9H7MEpvMqtjdWjFSq4zeeGa8rGEswCgnQLb
572-ZD/MNlUNQwQVCs+vVRdgpzcD+QELSc2EeYl4tef0NiUaZQt+mjFTs3DjQNDTjXao
573-ETVAqECx4kavcshx5tSE5JbbQPIMiUgh0h9J3z3uZsBVnx6P82aW/QTw+jLhsQry
574-/i3Z/+pS66mk6EWhAAYF/SPVqM/06BZh0ZvUmeG9WGGJXD9CUN1Wfi2mt42L2zhT
575-xg3uBACoIs5/GORi0H2i+blLiFSxTroXw+TdxiP+mfjdPho0oXJQTljXBgG70VfX
576-XW9sWsYtekqXBsmwMcbCZTjZGul/8jAUlUoYfthRw9KpP9N8Q7wB8Flx9jEv0M0H
577-tV1KTrLuXNZvEAB1sECMa7RRrV1yO4wyYDsOXiZNTL6rYugOU7QwUmFiYml0TVEg
578-UmVsZWFzZSBTaWduaW5nIEtleSA8aW5mb0ByYWJiaXRtcS5jb20+iGAEExECACAF
579-AkaOQ/ICGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRD3uM6mBW6OVkymAJ0R
580-6MwiZNRuTAttgYf1Xe7dK7HpzACfZioV/LqnDh7XvcTJEl+r4GB19by5Ag0ERo5D
581-+xAIAKu1ZxtAZjwlNLb0L5uwrEP7nTbRTNUYoEtE8+CNDSLLlmRIvBriKYNGicnz
582-Ebq2kDnAoyH38ACIMNayrkqc6I4l3BD2sv7zPZCd4qAbyFCu6gnewTANTWkVuH60
583-R65QQ8pM8sM+VZAMSoMkDSP4u248xOzFyGgVYuuWuR/sIRcaA02FW9TGvZQ7fNoF
584-rf6UbKSYkjpY767IW8q0b68vKzSLw0GQvH+dsvhaj80hjKJ06+IZ9Gdi/b4+AIT2
585-YWyWmrHo2QhnUmsarNdtusesQGQtiYgZw95PJJkzR0AttuPPfPNGLYZtVJenvOCC
586-jsK5uUL3/eEQ3UWGs+BKEyA/qLMAAwUH/2kIFCdgCw2DnL87TO+vruhGjsM7NjXf
587-57F4ojTdblFd6AerjRhMgICdzCF9WkFROdBSyQ/GajoNU81kbHZglxmKyKkVwWEb
588-G7pmSIc/sk5Z7OP/zrg4h8ZGzvMbRy0XLf86lQhbDE3AcHMeJCcShIWAHAbygnYW
589-j0KRhZiyqxqx4mrZQDZEWI7S1G9YNvgu1GS9EEKEpmxDEOME9nJZLi9o7mTeD1QV
590-TyOzWHkpQ42QcgrFuG7RMxDaQK6bdinNTl8aPmMoPamGzotSt4aMoVMiNxjatnlH
591-pqQ5UJlqbB5FGLnwJ0773WzgRdxIwSIxkFhL/Mq4agf4an8151kqcZCISQQYEQIA
592-CQUCRo5D+wIbDAAKCRD3uM6mBW6OVhLmAKCYY152B/10n7aUNKejs92NsNAnPACf
593-ZwbDOKBXGfkCPuRx5j/AGneASNU=
594-=Ry+c
595------END PGP PUBLIC KEY BLOCK-----""",
596- "management_plugin": True,
597-})
598-d.expose('rabbitmq-server')
599-
600-# Don't forget to expose using d.expose(service)
601-
602-try:
603- # TODO(billy-olsen), juju test --timeout fails to pass the timeout values
604- # into the environment and the charm isn't the best of places to select
605- # a viable timeout since so muc is attributed to the environment anyways.
606- # Need to fix this the right way, but for now we'll bump the timeout.
607- d.setup(timeout=2700)
608- d.sentry.wait()
609-except amulet.helpers.TimeoutError:
610- amulet.raise_status(amulet.SKIP, msg="Environment wasn't stood up in time")
611-except:
612- raise
613-
614-server = d.sentry.unit['rabbitmq-server/0']
615-host = server.info['public-address']
616-
617-# TODO(wolsen) It looks like in rabbitmq version 3.3.0 the guest count was
618-# disabled by default and therefore we need to do the following bit of code
619-# in order to allow guest access for the test to succeed.
620-server.run('echo "[{rabbit, [{loopback_users, []}]}]." > '
621- '/etc/rabbitmq/rabbitmq.config')
622-server.run('service rabbitmq-server restart')
623-
624-
625-try:
626- connection = pika.BlockingConnection(pika.ConnectionParameters(host=host))
627-except Exception as e:
628- amulet.raise_status(
629- amulet.FAIL,
630- str(e)
631- )
632
633=== removed file 'tests/20_deploy_relations_test.py'
634--- tests/20_deploy_relations_test.py 2015-08-10 17:50:50 +0000
635+++ tests/20_deploy_relations_test.py 1970-01-01 00:00:00 +0000
636@@ -1,251 +0,0 @@
637-#!/usr/bin/python3
638-
639-# This Amulet test deploys rabbitmq-server, and the related charms.
640-
641-import amulet
642-import os
643-import subprocess
644-import time
645-
646-# The number of seconds to wait for the environment to setup.
647-seconds = 2700
648-# The number of units to scale rabbitmq-server to.
649-scale = 2
650-# The port that amqp traffic is sent on.
651-amqp_port = '5672'
652-# The directory to use as a block devie for the ceph
653-devices = '/srv/osd1'
654-# The default version of ceph does not support directories as devices.
655-havana = 'cloud:precise-updates/havana'
656-# Create a dictionary of configuration values for ceph.
657-ceph_configuration = {
658- 'fsid': 'ecbb8960-0e21-11e2-b495-83a88f44db01',
659- 'monitor-secret': 'AQBomftSyK1LORAAhg71ukxBxN9ml90stexqEw==',
660- 'osd-devices': devices,
661- 'source': havana
662-}
663-# Create a dictionary of configuration values for cinder.
664-cinder_configuration = {
665- 'block-device': 'None'
666-}
667-# Create a dictionary of the rabbit configuration values.
668-rabbit_configuration = {
669- 'vip': '192.168.77.11',
670- 'vip_cidr': 19,
671- 'vip_iface': 'eth0',
672- 'ha-bindiface': 'eth0',
673- 'ha-mcastport': 5406,
674- 'rbd-size': '2G',
675- 'rbd-name': 'testrabbit1'
676-}
677-
678-# The AMQP package is only available for python version 2.x.
679-python2 = '/usr/bin/python'
680-if not os.path.isfile(python2):
681- error_message = 'Error, python version 2 is required for this test.'
682- amulet.raise_status(amulet.FAIL, msg=error_message)
683-
684-series = 'trusty'
685-d = amulet.Deployment(series=series)
686-# Add rabbitmq-server to the deployment.
687-d.add('rabbitmq-server', units=scale)
688-
689-# TODO(billy-olsen) - Rework this following set of code to be more in-line
690-# with how the other openstack services are done. For now, we want to test
691-# the current branch with the appropriate branches of related charms in
692-# order to test /next with /next branches and /trunk with /trunk branches.
693-stable = True
694-
695-
696-def determine_charm_branches(services):
697- if stable:
698- for svc in services:
699- temp = 'lp:charms/{}'
700- svc['location'] = temp.format(svc['name'])
701- else:
702- for svc in services:
703- temp = 'lp:~openstack-charmers/charms/{}/{}/next'
704- svc['location'] = temp.format(series, svc['name'])
705-
706- return services
707-
708-
709-def add_services(services):
710- """
711- Adds services to the deployment. The input is a list of dicts with
712- the key of 'name' name for the service name. The branch location
713- will be determined automatically.
714- """
715- services = determine_charm_branches(services)
716-
717- for svc in services:
718- d.add(svc['name'], charm=svc['location'])
719-
720-
721-services_to_add = [
722- {'name': 'ceph'},
723- {'name': 'cinder'},
724- {'name': 'hacluster'},
725-]
726-
727-# Add the services to the deployment
728-add_services(services_to_add)
729-
730-# The ceph charm requires configuration to deploy successfully.
731-d.configure('ceph', ceph_configuration)
732-# Configure the cinder charm.
733-d.configure('cinder', cinder_configuration)
734-# Configure the rabbit charm.
735-d.configure('rabbitmq-server', rabbit_configuration)
736-# Add relation from rabbitmq-server to ceph testing the ceph relation.
737-d.relate('rabbitmq-server:ceph', 'ceph:client')
738-# Add relation from rabbitmq-server to cinder testing the amqp relation.
739-d.relate('rabbitmq-server:amqp', 'cinder:amqp')
740-# Add relation from rabibtmq-server to hacluster testing the ha relation.
741-d.relate('rabbitmq-server:ha', 'hacluster:ha')
742-# Expose the rabbitmq-server.
743-d.expose('rabbitmq-server')
744-
745-try:
746- # Execute the deployer with the current mapping.
747- d.setup(timeout=seconds)
748- # Wait for the relation to finish the transations.
749- d.sentry.wait(seconds)
750-except amulet.helpers.TimeoutError:
751- message = 'The environment did not setup in %d seconds.' % seconds
752- # The SKIP status enables skip or fail the test based on configuration.
753- amulet.raise_status(amulet.FAIL, msg=message)
754-except:
755- raise
756-print('The environment successfully deployed.')
757-
758-# Create a counter to make the messages unique.
759-counter = 1
760-# Get the directory in this way to load the files from the tests directory.
761-path = os.path.abspath(os.path.dirname(__file__))
762-# Create a path to the python test file to call.
763-amqp_tester = os.path.join(path, 'amqp_tester.py')
764-if not os.path.isfile(amqp_tester):
765- error_message = 'Unable to locate python test file %s' % amqp_tester
766- amulet.raise_status(amulet.FAIL, msg=error_message)
767-
768-# Verify the ceph unit was created.
769-ceph_unit = d.sentry.unit['ceph/0']
770-# Verify the cinder unit was created.
771-cinder_unit = d.sentry.unit['cinder/0']
772-rabbit_units = []
773-for n in range(scale):
774- # Get each rabbitmq unit that was deployed.
775- rabbit_units.append(d.sentry.unit['rabbitmq-server/%d' % n])
776-
777-# Iterate over every rabbitmq-unit to get the different relations.
778-for rabbit_unit in rabbit_units:
779- ###########################################################################
780- # Test Relations
781- ###########################################################################
782- # Verify the ceph relation was created for the rabbit unit.
783- rabbit_relation = rabbit_unit.relation('ceph', 'ceph:client')
784- print('rabbit relation to ceph:')
785- for key, value in rabbit_relation.items():
786- print(key, value)
787- # Verify the amqp relation was created for the rabbit unit.
788- rabbit_relation = rabbit_unit.relation('amqp', 'cinder:amqp')
789- print('rabbit relation to amqp:')
790- for key, value in rabbit_relation.items():
791- print(key, value)
792-
793- # The hacluster charm is a subordinate, since the relation-sentry is also
794- # a subordinate charm no sentry is created for the hacluster relation.
795-
796- # Verify the rabbit relation was created with the ceph unit.
797- ceph_relation = ceph_unit.relation('client', 'rabbitmq-server:ceph')
798- print('ceph relation to rabbitmq-server:')
799- for key, value in ceph_relation.items():
800- print(key, value)
801- # Verify the rabbit relation was created with the cinder unit.
802- cinder_relation = cinder_unit.relation('amqp', 'rabbitmq-server:amqp')
803- print('cinder relation to rabbitmq-server:')
804- for key, value in cinder_relation.items():
805- print(key, value)
806-
807- ###########################################################################
808- # Test AMQP
809- ###########################################################################
810-
811- # The AMQP python library is only available for python2 at this time.
812- # Call out a command to run the python2 code to test the AMQP protocol.
813-
814- # Get the public address for rabbitmq-server instance.
815- server_address = rabbit_unit.info['public-address']
816- # Create a time stamp to help make the AMQP message unique.
817- time_stamp = time.strftime('%F %r')
818- # Create the message to send on the AMPQ protocol.
819- amqp_message = "Message #{0} to send using the AMPQ protocol {1}".format(
820- counter, time_stamp)
821- # Create the command with arguments that sends the message.
822- send_command = [python2, amqp_tester, server_address, amqp_port,
823- amqp_message]
824- print(send_command)
825- # Call the python command to send the AMQP message to the server.
826- output = subprocess.check_output(send_command)
827- # Create the command with arguments to receive messages.
828- receive_command = [python2, amqp_tester, server_address, amqp_port]
829- print(receive_command)
830- # Call the python command to receive the AMQP message from the same server.
831- output = subprocess.check_output(receive_command)
832- # The output is a byte string so convert the message to a byte string.
833- if output.find(amqp_message.encode()) == -1:
834- print('The AMQP test to {0}:{1} failed.'.format(server_address,
835- amqp_port))
836- amulet.raise_status(amulet.FAIL, msg=output)
837- else:
838- print('The AMQP test to {0}:{1} completed successfully.'.format(
839- server_address, amqp_port))
840- counter += 1
841-
842- ###########################################################################
843- # Verify that the rabbitmq cluster status is correct.
844- ###########################################################################
845- # Create the command that checks if the rabbitmq-server service is running.
846- command = 'rabbitmqctl cluster_status'
847- print(command)
848- # Execute the command on the deployed service.
849- output, code = rabbit_unit.run(command)
850- print(output)
851- # Check the return code for the success and failure of this test.
852- if (code != 0):
853- message = 'The ' + command + ' did not return the expected code of 0.'
854- amulet.raise_status(amulet.FAIL, msg=message)
855- else:
856- print('The rabbitmq-server cluster status is OK.')
857-
858-###############################################################################
859-# Test the AMQP messages can be sent from and read from another.
860-###############################################################################
861-# Get the public address for rabbitmq-server instance 0.
862-send_address = rabbit_units[0].info['public-address']
863-# Create a message to send from instance 0 and read it from instance 1.
864-amqp_message = "Message #{0} sent from {1} using the AMQP protocol.".format(
865- counter, send_address)
866-counter += 1
867-# Create the command that sends the message to instance 0.
868-send_command = [python2, amqp_tester, send_address, amqp_port, amqp_message]
869-print(send_command)
870-output = subprocess.check_output(send_command)
871-# Get the public address for rabbitmq-server instance 1.
872-receive_address = rabbit_units[1].info['public-address']
873-# Create the command that receives the message from instance 1.
874-recieve_command = [python2, amqp_tester, receive_address, amqp_port]
875-print(recieve_command)
876-output = subprocess.check_output(receive_command)
877-# The output is a byte string so convert the message to a byte string.
878-if output.find(amqp_message.encode()) == -1:
879- print(output)
880- message = 'Server {0} did not receive the AMQP message "{1}"'.format(
881- receive_address, amqp_message)
882- amulet.raise_status(amulet.FAIL, msg=message)
883-else:
884- print('Server {0} received the AMQP message sent from {1}'.format(
885- receive_address, send_address))
886-
887-print('The rabbitmq-server charm passed this relations test.')
888
889=== removed file 'tests/30-switch-ssl'
890--- tests/30-switch-ssl 2015-04-15 07:59:43 +0000
891+++ tests/30-switch-ssl 1970-01-01 00:00:00 +0000
892@@ -1,117 +0,0 @@
893-#!/usr/bin/python3
894-
895-import amulet
896-import pika
897-import time
898-
899-d = amulet.Deployment(series='trusty')
900-
901-d.add('rabbitmq-server')
902-d.expose('rabbitmq-server')
903-
904-# Don't forget to expose using d.expose(service)
905-
906-try:
907- # TODO(billy-olsen), juju test --timeout fails to pass the timeout values
908- # into the environment and the charm isn't the best of places to select
909- # a viable timeout since so muc is attributed to the environment anyways.
910- # Need to fix this the right way, but for now we'll bump the timeout.
911- d.setup(timeout=2700)
912- d.sentry.wait()
913-except amulet.helpers.TimeoutError:
914- amulet.raise_status(amulet.SKIP, msg="Environment wasn't stood up in time")
915-except:
916- raise
917-
918-server = d.sentry.unit['rabbitmq-server/0']
919-host = server.info['public-address']
920-
921-
922-# Connects without ssl
923-try:
924- connection = pika.BlockingConnection(pika.ConnectionParameters(host=host,
925- ssl=False))
926-except Exception as e:
927- amulet.raise_status(
928- amulet.FAIL,
929- "Insecure connection failed with ssl=off: {}".format(str(e))
930- )
931-
932-# Doesn't connect with ssl
933-try:
934- connection = pika.BlockingConnection(pika.ConnectionParameters(host=host,
935- ssl=True))
936-except Exception as e:
937- pass
938-else:
939- amulet.raise_status(
940- amulet.FAIL,
941- 'SSL enabled when it shouldn\'t.'
942- )
943-
944-d.configure('rabbitmq-server', {
945- 'ssl': 'on'
946-})
947-
948-# There's a race for changing the configuration of a deployment.
949-# The configure from the juju client side happens fairly quickly, and the
950-# sentry.wait() can fire before the config-changed hooks do, which causes
951-# the wait to end...
952-time.sleep(10)
953-d.sentry.wait()
954-
955-# Connects without ssl
956-try:
957- connection = pika.BlockingConnection(pika.ConnectionParameters(host=host,
958- ssl=False))
959-except Exception as e:
960- amulet.raise_status(
961- amulet.FAIL,
962- "Insecure connection fails with ssl=on: {}".format(str(e))
963- )
964-
965-# Connects with ssl
966-try:
967- connection = pika.BlockingConnection(pika.ConnectionParameters(host=host,
968- port=5671,
969- ssl=True))
970-except Exception as e:
971- amulet.raise_status(
972- amulet.FAIL,
973- "Secure connection fails with ssl=on"
974- )
975-
976-d.configure('rabbitmq-server', {
977- 'ssl': 'only'
978-})
979-
980-# There's a race for changing the configuration of a deployment.
981-# The configure from the juju client side happens fairly quickly, and the
982-# sentry.wait() can fire before the config-changed hooks do, which causes
983-# the wait to end...
984-time.sleep(10)
985-d.sentry.wait()
986-
987-
988-# Doesn't connect without ssl
989-try:
990- connection = pika.BlockingConnection(pika.ConnectionParameters(host=host,
991- ssl=False))
992-except Exception as e:
993- pass
994-else:
995- amulet.raise_status(
996- amulet.FAIL,
997- "Connects without SSL when it shouldn't"
998- )
999-
1000-# Connects with ssl
1001-try:
1002- connection = pika.BlockingConnection(pika.ConnectionParameters(host=host,
1003- port=5671,
1004- ssl=True))
1005-except Exception as e:
1006- amulet.raise_status(
1007- amulet.FAIL,
1008- "Secure connection fails with ssl=only: {}".format(str(e))
1009- )
1010
1011=== removed file 'tests/30_configuration_test.py'
1012--- tests/30_configuration_test.py 2015-04-15 07:59:43 +0000
1013+++ tests/30_configuration_test.py 1970-01-01 00:00:00 +0000
1014@@ -1,118 +0,0 @@
1015-#!/usr/bin/python3
1016-
1017-# This Amulet test exercises the configuration options for rabbitmq-server.
1018-
1019-import amulet
1020-import os
1021-import socket
1022-import ssl
1023-from deploy_common import CA
1024-
1025-# The number of seconds to wait for the environment to setup.
1026-seconds = 2700
1027-# Get the directory in this way to load the files from the tests directory.
1028-path = os.path.abspath(os.path.dirname(__file__))
1029-
1030-ca = CA()
1031-
1032-privateKey = ca.get_key()
1033-certificate = ca.get_cert()
1034-
1035-# Create a dictionary of all the configuration values.
1036-rabbit_configuration = {
1037- 'management_plugin': True,
1038- 'ssl_enabled': True,
1039- 'ssl_port': 5999,
1040- 'ssl_key': privateKey,
1041- 'ssl_cert': certificate,
1042-}
1043-
1044-d = amulet.Deployment(series='trusty')
1045-# Add the rabbitmq-server charm to the deployment.
1046-d.add('rabbitmq-server')
1047-# Configure all the options on rabbitmq-server.
1048-d.configure('rabbitmq-server', rabbit_configuration)
1049-# Expose the rabbitmq-server.
1050-d.expose('rabbitmq-server')
1051-
1052-try:
1053- # Execute the deployer with the current mapping.
1054- d.setup(timeout=seconds)
1055- # Wait for the relation to finish the transations.
1056- d.sentry.wait(seconds)
1057-except amulet.helpers.TimeoutError:
1058- message = 'The environment did not setup in %d seconds.' % seconds
1059- # The SKIP status enables skip or fail the test based on configuration.
1060- amulet.raise_status(amulet.SKIP, msg=message)
1061-except:
1062- raise
1063-
1064-rabbit_unit = d.sentry.unit['rabbitmq-server/0']
1065-###############################################################################
1066-# Verify that the rabbit service is running on the deployed server.
1067-###############################################################################
1068-# Create the command that checks if the rabbitmq-server service is running.
1069-command = 'rabbitmqctl status'
1070-print(command)
1071-# Execute the command on the deployed service.
1072-output, code = rabbit_unit.run(command)
1073-print(output)
1074-# Check the return code for the success and failure of this test.
1075-if (code != 0):
1076- message = 'The ' + command + ' did not return the expected code of 0.'
1077- amulet.raise_status(amulet.FAIL, msg=message)
1078-else:
1079- print('The rabbitmq-server is running.')
1080-
1081-###############################################################################
1082-# Verify the configuration values.
1083-###############################################################################
1084-# Get the contents of the private key from the rabbitmq-server
1085-contents = rabbit_unit.file_contents('/etc/rabbitmq/rabbit-server-privkey.pem')
1086-# Verify the private key was saved on the rabbitmq server correctly.
1087-if contents != privateKey:
1088- message = 'The private keys did not match!'
1089- amulet.raise_status(amulet.FAIL, msg=message)
1090-else:
1091- print('The private keys was configured properly on the rabbitmq server.')
1092-
1093-# Get the contents of the certificate from the rabbitmq-server.
1094-contents = rabbit_unit.file_contents('/etc/rabbitmq/rabbit-server-cert.pem')
1095-# Verify the certificate was saved on the rabbitmq server correctly.
1096-if contents != certificate:
1097- message = 'The certificates did not match!'
1098- amulet.raise_status(amulet.FAIL, msg=message)
1099-else:
1100- print('The certificate was configured properly on the rabbitmq server.')
1101-
1102-# Get the public address for rabbitmq-server instance.
1103-rabbit_host = rabbit_unit.info['public-address']
1104-
1105-###############################################################################
1106-# Verify that SSL is set up on the non-default port.
1107-###############################################################################
1108-# Get the port for ssl_port instance.
1109-ssl_port = rabbit_configuration['ssl_port']
1110-
1111-try:
1112- # Create a normal socket.
1113- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1114- # Require a certificate from the server, since a self-signed certificate
1115- # was used, the ca_certs must be the server certificate file itself.
1116- ssl_sock = ssl.wrap_socket(s, ca_certs=ca.ca_cert_path(),
1117- cert_reqs=ssl.CERT_REQUIRED)
1118- # Connect to the rabbitmq server using ssl.
1119- ssl_sock.connect((rabbit_host, ssl_port))
1120- # Get the certificate.
1121- certificate = ssl_sock.getpeercert()
1122- # SSL scoket connected and got the certificate, this passes the ssl test!
1123- print('Connected to the rabbitmq-server {0}:{1} using ssl!'.format(
1124- rabbit_host, ssl_port))
1125-except Exception as e:
1126- message = 'Failed to create an ssl connection to {0}:{1}\n{2}'.format(
1127- rabbit_host, ssl_port, str(e))
1128- amulet.raise_status(amulet.FAIL, msg=message)
1129-finally:
1130- ssl_sock.close()
1131-
1132-print('The rabbitmq-server passed the configuration tests.')
1133
1134=== removed file 'tests/40_test_mirroring_queues.py'
1135--- tests/40_test_mirroring_queues.py 2015-04-15 04:35:17 +0000
1136+++ tests/40_test_mirroring_queues.py 1970-01-01 00:00:00 +0000
1137@@ -1,85 +0,0 @@
1138-#!/usr/bin/python
1139-#
1140-# This Amulet test deploys rabbitmq-server
1141-#
1142-# Note: We use python2, because pika doesn't support python3
1143-
1144-import amulet
1145-import pika
1146-import telnetlib
1147-
1148-
1149-# The number of seconds to wait for the environment to setup.
1150-seconds = 2700
1151-
1152-d = amulet.Deployment(series="trusty")
1153-# Add the rabbitmq-server charm to the deployment.
1154-d.add('rabbitmq-server', units=2)
1155-
1156-# Create a configuration.
1157-configuration = {'mirroring-queues': True,
1158- 'management_plugin': True}
1159-d.configure('rabbitmq-server', configuration)
1160-d.expose('rabbitmq-server')
1161-
1162-try:
1163- d.setup(timeout=seconds)
1164- d.sentry.wait(seconds)
1165-except amulet.helpers.TimeoutError:
1166- message = 'The environment did not setup in %d seconds.' % seconds
1167- amulet.raise_status(amulet.SKIP, msg=message)
1168-except:
1169- raise
1170-
1171-
1172-rabbit_unit = d.sentry.unit['rabbitmq-server/0']
1173-rabbit_unit2 = d.sentry.unit['rabbitmq-server/1']
1174-
1175-commands = ['service rabbitmq-server status',
1176- 'rabbitmqctl cluster_status']
1177-
1178-for cmd in commands:
1179- output, code = rabbit_unit.run(cmd)
1180- message = cmd + ' | exit code: %d.' % code
1181- print(message)
1182- print(output)
1183-
1184- if code != 0:
1185- amulet.raise_status(amulet.FAIL, msg=message)
1186-
1187-rabbit_addr1 = rabbit_unit.info["public-address"]
1188-rabbit_port = "5672"
1189-rabbit_url = 'amqp://guest:guest@%s:%s/%%2F' % (rabbit_addr1, rabbit_port)
1190-
1191-print('Connecting to %s' % rabbit_url)
1192-conn1 = pika.BlockingConnection(pika.connection.URLParameters(rabbit_url))
1193-channel = conn1.channel()
1194-print('Declaring queue')
1195-channel.queue_declare(queue='hello')
1196-orig_msg = 'Hello World!'
1197-print('Publishing message: %s' % orig_msg)
1198-channel.basic_publish(exchange='',
1199- routing_key='hello',
1200- body=orig_msg)
1201-
1202-print('stopping rabbit in unit 0')
1203-rabbit_unit.run('service rabbitmq-server stop')
1204-
1205-print('Consuming message from second unit')
1206-rabbit_addr2 = rabbit_unit2.info["public-address"]
1207-rabbit_url2 = 'amqp://guest:guest@%s:%s/%%2F' % (rabbit_addr2, rabbit_port)
1208-conn2 = pika.BlockingConnection(pika.connection.URLParameters(rabbit_url2))
1209-channel2 = conn2.channel()
1210-method_frame, header_frame, body = channel2.basic_get('hello')
1211-
1212-if method_frame:
1213- print(method_frame, header_frame, body)
1214- assert body == orig_msg, '%s != %s' % (body, orig_msg)
1215- channel2.basic_ack(method_frame.delivery_tag)
1216-else:
1217- raise Exception('No message returned')
1218-
1219-# check the management plugin is running
1220-mgmt_port = "15672"
1221-print('Checking management port')
1222-telnetlib.Telnet(rabbit_addr2, mgmt_port)
1223
1224=== removed file 'tests/50_test_monitoring.py'
1225--- tests/50_test_monitoring.py 2015-04-20 11:15:15 +0000
1226+++ tests/50_test_monitoring.py 1970-01-01 00:00:00 +0000
1227@@ -1,71 +0,0 @@
1228-#!/usr/bin/python3
1229-
1230-# This Amulet test performs a basic deploy and checks if rabbitmq is running.
1231-
1232-import amulet
1233-import time
1234-
1235-# The number of seconds to wait for the environment to setup.
1236-seconds = 900
1237-
1238-# Create a dictionary for the rabbitmq configuration.
1239-rabbitmq_configuration = {
1240- 'stats_cron_schedule': '*/1 * * * *'
1241-}
1242-d = amulet.Deployment(series='trusty')
1243-# Add the rabbitmq-server charm to the deployment.
1244-d.add('rabbitmq-server')
1245-# Configure options on the rabbitmq-server.
1246-d.configure('rabbitmq-server', rabbitmq_configuration)
1247-# Expose the server so we can connect.
1248-d.expose('rabbitmq-server')
1249-# XXX Remove charm= once this branch lands in the charm store
1250-d.add('nrpe-external-master',
1251- charm='lp:~gnuoy/charms/trusty/nrpe/services-rewrite')
1252-d.relate('rabbitmq-server:nrpe-external-master',
1253- 'nrpe-external-master:nrpe-external-master')
1254-
1255-try:
1256- # Execute the deployer with the current mapping.
1257- d.setup(timeout=seconds)
1258-except amulet.helpers.TimeoutError:
1259- message = 'The environment did not setup in %d seconds.' % seconds
1260- # The SKIP status enables skip or fail the test based on configuration.
1261- amulet.raise_status(amulet.SKIP, msg=message)
1262-except:
1263- raise
1264-print('The rabbitmq-server has been successfully deployed and related '
1265- 'to nrpe-external-master.')
1266-
1267-###############################################################################
1268-# # Verify nagios checks
1269-###############################################################################
1270-rabbitmq_sentry = d.sentry.unit['rabbitmq-server/0']
1271-
1272-command = 'bash -c "$(egrep -oh /usr/local.* ' \
1273- '/etc/nagios/nrpe.d/check_rabbitmq.cfg)"'
1274-print(command)
1275-output, code = rabbitmq_sentry.run(command)
1276-print(output)
1277-if (code != 0):
1278- message = 'The ' + command + ' did not return the expected code of 0.'
1279- amulet.raise_status(amulet.FAIL, msg=message)
1280-else:
1281- print('The rabbitmq-server check_rabbitmq is OK')
1282-
1283-print('Sleeping 70 seconds to make sure the monitoring cron has run')
1284-time.sleep(70)
1285-
1286-command = 'bash -c "$(egrep -oh /usr/local.* ' \
1287- '/etc/nagios/nrpe.d/check_rabbitmq_queue.cfg)"'
1288-print(command)
1289-output, code = rabbitmq_sentry.run(command)
1290-print(output)
1291-if (code != 0):
1292- message = 'The ' + command + ' did not return the expected code of 0.'
1293- amulet.raise_status(amulet.FAIL, msg=message)
1294-else:
1295- print('The rabbitmq-server check_rabbitmq_queue is OK')
1296-
1297-# Success!
1298-print('The rabbitmq-server passed the monitoring tests!')
1299
1300=== removed file 'tests/amqp_tester.py'
1301--- tests/amqp_tester.py 2015-01-12 16:53:33 +0000
1302+++ tests/amqp_tester.py 1970-01-01 00:00:00 +0000
1303@@ -1,65 +0,0 @@
1304-#!/usr/bin/python
1305-
1306-# This class uses Python to make AMQP calls to send and receive messages.
1307-# To send an AMQP message call this module with a host, port, and message.
1308-# To receive an AMQP message call this module with a host and port only.
1309-
1310-import logging
1311-import pika
1312-import sys
1313-
1314-
1315-def send(host, port, message, queue='test'):
1316- """ Send an AMQP message to a host and port."""
1317- connection = None
1318- try:
1319- parameters = pika.ConnectionParameters(host, port)
1320- connection = pika.BlockingConnection(parameters)
1321-
1322- channel = connection.channel()
1323- channel.queue_declare(queue)
1324- channel.basic_publish(exchange='', routing_key=queue, body=message)
1325- print('Message published to {0}:{1}'.format(host, port))
1326- except Exception as e:
1327- print('Unable to send message to {0}:{1}'.format(host, port))
1328- print(e)
1329- finally:
1330- if connection:
1331- connection.close()
1332-
1333-
1334-def callback(ch, method, properties, body):
1335- """ Handle the callback when the channel receives a message. """
1336- print(body)
1337-
1338-
1339-def receive(host, port, queue='test'):
1340- """ Connects to host and port, and consumes AMQP messages. """
1341- connection = None
1342- try:
1343- parameters = pika.ConnectionParameters(host, port)
1344- connection = pika.BlockingConnection(parameters)
1345- channel = connection.channel()
1346- channel.queue_declare(queue)
1347- channel.basic_consume(callback, queue, no_ack=True)
1348- except Exception as e:
1349- print('Unable to receive message from {0}:{1}'.format(host, port))
1350- print(e)
1351- finally:
1352- if connection:
1353- connection.close()
1354-
1355-# Needed to disable pika complaining about logging levels not set.
1356-logging.basicConfig(level=logging.ERROR)
1357-
1358-if len(sys.argv) == 3:
1359- host = sys.argv[1]
1360- port = int(sys.argv[2])
1361- receive(host, port)
1362-elif len(sys.argv) > 3:
1363- host = sys.argv[1]
1364- port = int(sys.argv[2])
1365- message = ' '.join(sys.argv[3:])
1366- send(host, port, message)
1367-else:
1368- print('Not enough arguments, host and port are required.')
1369
1370=== added file 'tests/basic_deployment.py'
1371--- tests/basic_deployment.py 1970-01-01 00:00:00 +0000
1372+++ tests/basic_deployment.py 2015-10-13 18:33:25 +0000
1373@@ -0,0 +1,492 @@
1374+#!/usr/bin/python
1375+"""
1376+Basic 3-node rabbitmq-server native cluster + nrpe functional tests
1377+
1378+Cinder is present to exercise and inspect amqp relation functionality.
1379+
1380+Each individual test is idempotent, in that it creates/deletes
1381+a rmq test user, enables or disables ssl as needed.
1382+
1383+Test order is not required, however tests are numbered to keep
1384+relevant tests grouped together in run order.
1385+"""
1386+
1387+import amulet
1388+import time
1389+
1390+from charmhelpers.contrib.openstack.amulet.deployment import (
1391+ OpenStackAmuletDeployment
1392+)
1393+
1394+from charmhelpers.contrib.openstack.amulet.utils import (
1395+ OpenStackAmuletUtils,
1396+ DEBUG,
1397+ # ERROR
1398+)
1399+
1400+# Use DEBUG to turn on debug logging
1401+u = OpenStackAmuletUtils(DEBUG)
1402+
1403+
1404+class RmqBasicDeployment(OpenStackAmuletDeployment):
1405+ """Amulet tests on a basic rabbitmq cluster deployment. Verify
1406+ relations, service status, users and endpoint service catalog."""
1407+
1408+ def __init__(self, series=None, openstack=None, source=None, stable=True):
1409+ """Deploy the entire test environment."""
1410+ super(RmqBasicDeployment, self).__init__(series, openstack, source,
1411+ stable)
1412+ self._add_services()
1413+ self._add_relations()
1414+ self._configure_services()
1415+ self._deploy()
1416+ self._initialize_tests()
1417+
1418+ def _add_services(self):
1419+ """Add services
1420+
1421+ Add the services that we're testing, where rmq is local,
1422+ and the rest of the service are from lp branches that are
1423+ compatible with the local charm (e.g. stable or next).
1424+ """
1425+ this_service = {
1426+ 'name': 'rabbitmq-server',
1427+ 'units': 3
1428+ }
1429+ other_services = [{'name': 'cinder'},
1430+ {'name': 'nrpe'}]
1431+
1432+ super(RmqBasicDeployment, self)._add_services(this_service,
1433+ other_services)
1434+
1435+ def _add_relations(self):
1436+ """Add relations for the services."""
1437+ relations = {'cinder:amqp': 'rabbitmq-server:amqp',
1438+ 'nrpe:nrpe-external-master':
1439+ 'rabbitmq-server:nrpe-external-master'}
1440+
1441+ super(RmqBasicDeployment, self)._add_relations(relations)
1442+
1443+ def _configure_services(self):
1444+ """Configure all of the services."""
1445+ rmq_config = {
1446+ 'min-cluster-size': '3',
1447+ 'max-cluster-tries': '6',
1448+ 'ssl': 'off',
1449+ 'management_plugin': 'False',
1450+ 'stats_cron_schedule': '*/1 * * * *'
1451+ }
1452+ cinder_config = {}
1453+ configs = {'rabbitmq-server': rmq_config,
1454+ 'cinder': cinder_config}
1455+ super(RmqBasicDeployment, self)._configure_services(configs)
1456+
1457+ def _initialize_tests(self):
1458+ """Perform final initialization before tests get run."""
1459+ # Access the sentries for inspecting service units
1460+ self.rmq0_sentry = self.d.sentry.unit['rabbitmq-server/0']
1461+ self.rmq1_sentry = self.d.sentry.unit['rabbitmq-server/1']
1462+ self.rmq2_sentry = self.d.sentry.unit['rabbitmq-server/2']
1463+ self.cinder_sentry = self.d.sentry.unit['cinder/0']
1464+ self.nrpe_sentry = self.d.sentry.unit['nrpe/0']
1465+ u.log.debug('openstack release val: {}'.format(
1466+ self._get_openstack_release()))
1467+ u.log.debug('openstack release str: {}'.format(
1468+ self._get_openstack_release_string()))
1469+
1470+ # Let things settle a bit before moving forward
1471+ time.sleep(30)
1472+
1473+ def _get_rmq_sentry_units(self):
1474+ """Local helper specific to this 3-node rmq series of tests."""
1475+ return [self.rmq0_sentry,
1476+ self.rmq1_sentry,
1477+ self.rmq2_sentry]
1478+
1479+ def _test_rmq_amqp_messages_all_units(self, sentry_units,
1480+ ssl=False, port=None):
1481+ """Reusable test to send amqp messages to every listed rmq unit
1482+ and check every listed rmq unit for messages.
1483+
1484+ :param sentry_units: list of sentry units
1485+ :returns: None if successful. Raise on error.
1486+ """
1487+
1488+ # Add test user if it does not already exist
1489+ u.add_rmq_test_user(sentry_units)
1490+
1491+ # Handle ssl
1492+ if ssl:
1493+ u.configure_rmq_ssl_on(sentry_units, self.d, port=port)
1494+ else:
1495+ u.configure_rmq_ssl_off(sentry_units, self.d)
1496+
1497+ # Publish and get amqp messages in all possible unit combinations.
1498+ # Qty of checks == (qty of units) ^ 2
1499+ amqp_msg_counter = 1
1500+ host_names = u.get_unit_hostnames(sentry_units)
1501+
1502+ for dest_unit in sentry_units:
1503+ dest_unit_name = dest_unit.info['unit_name']
1504+ dest_unit_host = dest_unit.info['public-address']
1505+ dest_unit_host_name = host_names[dest_unit_name]
1506+
1507+ for check_unit in sentry_units:
1508+ check_unit_name = check_unit.info['unit_name']
1509+ check_unit_host = check_unit.info['public-address']
1510+ check_unit_host_name = host_names[check_unit_name]
1511+
1512+ amqp_msg_stamp = u.get_uuid_epoch_stamp()
1513+ amqp_msg = ('Message {}@{} {}'.format(amqp_msg_counter,
1514+ dest_unit_host,
1515+ amqp_msg_stamp)).upper()
1516+ # Publish amqp message
1517+ u.log.debug('Publish message to: {} '
1518+ '({} {})'.format(dest_unit_host,
1519+ dest_unit_name,
1520+ dest_unit_host_name))
1521+
1522+ u.publish_amqp_message_by_unit(dest_unit,
1523+ amqp_msg, ssl=ssl,
1524+ port=port)
1525+
1526+ # Wait a bit before checking for message
1527+ time.sleep(2)
1528+
1529+ # Get amqp message
1530+ u.log.debug('Get message from: {} '
1531+ '({} {})'.format(check_unit_host,
1532+ check_unit_name,
1533+ check_unit_host_name))
1534+
1535+ amqp_msg_rcvd = u.get_amqp_message_by_unit(check_unit,
1536+ ssl=ssl,
1537+ port=port)
1538+
1539+ # Validate amqp message content
1540+ if amqp_msg == amqp_msg_rcvd:
1541+ u.log.debug('Message {} received '
1542+ 'OK.'.format(amqp_msg_counter))
1543+ else:
1544+ u.log.error('Expected: {}'.format(amqp_msg))
1545+ u.log.error('Actual: {}'.format(amqp_msg_rcvd))
1546+ msg = 'Message {} mismatch.'.format(amqp_msg_counter)
1547+ amulet.raise_status(amulet.FAIL, msg)
1548+
1549+ amqp_msg_counter += 1
1550+
1551+ # Delete the test user
1552+ u.delete_rmq_test_user(sentry_units)
1553+
1554+ def test_100_rmq_processes(self):
1555+ """Verify that the expected service processes are running
1556+ on each rabbitmq-server unit."""
1557+
1558+ # Beam and epmd sometimes briefly have more than one PID,
1559+ # True checks for at least 1.
1560+ rmq_processes = {
1561+ 'beam': True,
1562+ 'epmd': True,
1563+ }
1564+
1565+ # Units with process names and PID quantities expected
1566+ expected_processes = {
1567+ self.rmq0_sentry: rmq_processes,
1568+ self.rmq1_sentry: rmq_processes,
1569+ self.rmq2_sentry: rmq_processes
1570+ }
1571+
1572+ actual_pids = u.get_unit_process_ids(expected_processes)
1573+ ret = u.validate_unit_process_ids(expected_processes, actual_pids)
1574+ if ret:
1575+ amulet.raise_status(amulet.FAIL, msg=ret)
1576+
1577+ u.log.info('OK\n')
1578+
1579+ def test_102_services(self):
1580+ """Verify that the expected services are running on the
1581+ corresponding service units."""
1582+ services = {
1583+ self.rmq0_sentry: ['rabbitmq-server'],
1584+ self.rmq1_sentry: ['rabbitmq-server'],
1585+ self.rmq2_sentry: ['rabbitmq-server'],
1586+ self.cinder_sentry: ['cinder-api',
1587+ 'cinder-scheduler',
1588+ 'cinder-volume'],
1589+ }
1590+ ret = u.validate_services_by_name(services)
1591+ if ret:
1592+ amulet.raise_status(amulet.FAIL, msg=ret)
1593+
1594+ u.log.info('OK\n')
1595+
1596+ def test_200_rmq_cinder_amqp_relation(self):
1597+ """Verify the rabbitmq-server:cinder amqp relation data"""
1598+ u.log.debug('Checking rmq:cinder amqp relation data...')
1599+ unit = self.rmq0_sentry
1600+ relation = ['amqp', 'cinder:amqp']
1601+ expected = {
1602+ 'private-address': u.valid_ip,
1603+ 'password': u.not_null,
1604+ 'hostname': u.valid_ip
1605+ }
1606+ ret = u.validate_relation_data(unit, relation, expected)
1607+ if ret:
1608+ msg = u.relation_error('amqp cinder', ret)
1609+ amulet.raise_status(amulet.FAIL, msg=msg)
1610+
1611+ u.log.info('OK\n')
1612+
1613+ def test_201_cinder_rmq_amqp_relation(self):
1614+ """Verify the cinder:rabbitmq-server amqp relation data"""
1615+ u.log.debug('Checking cinder:rmq amqp relation data...')
1616+ unit = self.cinder_sentry
1617+ relation = ['amqp', 'rabbitmq-server:amqp']
1618+ expected = {
1619+ 'private-address': u.valid_ip,
1620+ 'vhost': 'openstack',
1621+ 'username': u.not_null
1622+ }
1623+ ret = u.validate_relation_data(unit, relation, expected)
1624+ if ret:
1625+ msg = u.relation_error('cinder amqp', ret)
1626+ amulet.raise_status(amulet.FAIL, msg=msg)
1627+
1628+ u.log.info('OK\n')
1629+
1630+ def test_202_rmq_nrpe_ext_master_relation(self):
1631+ """Verify rabbitmq-server:nrpe nrpe-external-master relation data"""
1632+ u.log.debug('Checking rmq:nrpe external master relation data...')
1633+ unit = self.rmq0_sentry
1634+ relation = ['nrpe-external-master',
1635+ 'nrpe:nrpe-external-master']
1636+
1637+ mon_sub = ('monitors:\n remote:\n nrpe:\n rabbitmq: '
1638+ '{command: check_rabbitmq}\n rabbitmq_queue: '
1639+ '{command: check_rabbitmq_queue}\n')
1640+
1641+ expected = {
1642+ 'private-address': u.valid_ip,
1643+ 'monitors': mon_sub
1644+ }
1645+
1646+ ret = u.validate_relation_data(unit, relation, expected)
1647+ if ret:
1648+ msg = u.relation_error('amqp nrpe', ret)
1649+ amulet.raise_status(amulet.FAIL, msg=msg)
1650+
1651+ u.log.info('OK\n')
1652+
1653+ def test_203_nrpe_rmq_ext_master_relation(self):
1654+ """Verify nrpe:rabbitmq-server nrpe-external-master relation data"""
1655+ u.log.debug('Checking nrpe:rmq external master relation data...')
1656+ unit = self.nrpe_sentry
1657+ relation = ['nrpe-external-master',
1658+ 'rabbitmq-server:nrpe-external-master']
1659+
1660+ expected = {
1661+ 'private-address': u.valid_ip
1662+ }
1663+
1664+ ret = u.validate_relation_data(unit, relation, expected)
1665+ if ret:
1666+ msg = u.relation_error('nrpe amqp', ret)
1667+ amulet.raise_status(amulet.FAIL, msg=msg)
1668+
1669+ u.log.info('OK\n')
1670+
1671+ def test_300_rmq_config(self):
1672+ """Verify the data in the rabbitmq conf file."""
1673+ conf = '/etc/rabbitmq/rabbitmq-env.conf'
1674+ sentry_units = self._get_rmq_sentry_units()
1675+ for unit in sentry_units:
1676+ host_name = unit.file_contents('/etc/hostname').strip()
1677+ u.log.debug('Checking rabbitmq config file data on '
1678+ '{} ({})...'.format(unit.info['unit_name'],
1679+ host_name))
1680+ expected = {
1681+ 'RABBITMQ_NODENAME': 'rabbit@{}'.format(host_name)
1682+ }
1683+
1684+ file_contents = unit.file_contents(conf)
1685+ u.validate_sectionless_conf(file_contents, expected)
1686+
1687+ u.log.info('OK\n')
1688+
1689+ def test_400_rmq_cluster_running_nodes(self):
1690+ """Verify that cluster status from each rmq juju unit shows
1691+ every cluster node as a running member in that cluster."""
1692+ u.log.debug('Checking that all units are in cluster_status '
1693+ 'running nodes...')
1694+
1695+ sentry_units = self._get_rmq_sentry_units()
1696+
1697+ ret = u.validate_rmq_cluster_running_nodes(sentry_units)
1698+ if ret:
1699+ amulet.raise_status(amulet.FAIL, msg=ret)
1700+
1701+ u.log.info('OK\n')
1702+
1703+ def test_402_rmq_connect_with_ssl_off(self):
1704+ """Verify successful non-ssl amqp connection to all units when
1705+ charm config option for ssl is set False."""
1706+ u.log.debug('Confirming that non-ssl connection succeeds when '
1707+ 'ssl config is off...')
1708+ sentry_units = self._get_rmq_sentry_units()
1709+ u.add_rmq_test_user(sentry_units)
1710+ u.configure_rmq_ssl_off(sentry_units, self.d)
1711+
1712+ # Check amqp connection for all units, expect connections to succeed
1713+ for unit in sentry_units:
1714+ connection = u.connect_amqp_by_unit(unit, ssl=False, fatal=False)
1715+ connection.close()
1716+
1717+ u.delete_rmq_test_user(sentry_units)
1718+ u.log.info('OK\n')
1719+
1720+ def test_404_rmq_ssl_connect_with_ssl_off(self):
1721+ """Verify unsuccessful ssl amqp connection to all units when
1722+ charm config option for ssl is set False."""
1723+ u.log.debug('Confirming that ssl connection fails when ssl '
1724+ 'config is off...')
1725+ sentry_units = self._get_rmq_sentry_units()
1726+ u.add_rmq_test_user(sentry_units)
1727+ u.configure_rmq_ssl_off(sentry_units, self.d)
1728+
1729+ # Check ssl amqp connection for all units, expect connections to fail
1730+ for unit in sentry_units:
1731+ connection = u.connect_amqp_by_unit(unit, ssl=True,
1732+ port=5971, fatal=False)
1733+ if connection:
1734+ connection.close()
1735+ msg = 'SSL connection unexpectedly succeeded with ssl=off'
1736+ amulet.raise_status(amulet.FAIL, msg)
1737+
1738+ u.delete_rmq_test_user(sentry_units)
1739+ u.log.info('OK - Confirmed that ssl connection attempt fails '
1740+ 'when ssl config is off.')
1741+
1742+ def test_406_rmq_amqp_messages_all_units_ssl_off(self):
1743+ """Send amqp messages to every rmq unit and check every rmq unit
1744+ for messages. Standard amqp tcp port, no ssl."""
1745+ u.log.debug('Checking amqp message publish/get on all units '
1746+ '(ssl off)...')
1747+
1748+ sentry_units = self._get_rmq_sentry_units()
1749+ self._test_rmq_amqp_messages_all_units(sentry_units, ssl=False)
1750+ u.log.info('OK\n')
1751+
1752+ def test_408_rmq_amqp_messages_all_units_ssl_on(self):
1753+ """Send amqp messages with ssl enabled, to every rmq unit and
1754+ check every rmq unit for messages. Standard ssl tcp port."""
1755+ u.log.debug('Checking amqp message publish/get on all units '
1756+ '(ssl on)...')
1757+
1758+ sentry_units = self._get_rmq_sentry_units()
1759+ self._test_rmq_amqp_messages_all_units(sentry_units,
1760+ ssl=True, port=5671)
1761+ u.log.info('OK\n')
1762+
1763+ def test_410_rmq_amqp_messages_all_units_ssl_alt_port(self):
1764+ """Send amqp messages with ssl on, to every rmq unit and check
1765+ every rmq unit for messages. Custom ssl tcp port."""
1766+ u.log.debug('Checking amqp message publish/get on all units '
1767+ '(ssl on)...')
1768+
1769+ sentry_units = self._get_rmq_sentry_units()
1770+ self._test_rmq_amqp_messages_all_units(sentry_units,
1771+ ssl=True, port=5999)
1772+ u.log.info('OK\n')
1773+
1774+ def test_412_rmq_management_plugin(self):
1775+ """Enable and check management plugin."""
1776+ u.log.debug('Checking tcp socket connect to management plugin '
1777+ 'port on all rmq units...')
1778+
1779+ sentry_units = self._get_rmq_sentry_units()
1780+ mgmt_port = 15672
1781+
1782+ # Enable management plugin
1783+ u.log.debug('Enabling management_plugin charm config option...')
1784+ config = {'management_plugin': 'True'}
1785+ self.d.configure('rabbitmq-server', config)
1786+
1787+ # Check tcp connect to management plugin port
1788+ max_wait = 120
1789+ tries = 0
1790+ ret = u.port_knock_units(sentry_units, mgmt_port)
1791+ while ret and tries < (max_wait / 12):
1792+ time.sleep(12)
1793+ u.log.debug('Attempt {}: {}'.format(tries, ret))
1794+ ret = u.port_knock_units(sentry_units, mgmt_port)
1795+ tries += 1
1796+
1797+ if ret:
1798+ amulet.raise_status(amulet.FAIL, ret)
1799+ else:
1800+ u.log.debug('Connect to all units (OK)\n')
1801+
1802+ # Disable management plugin
1803+ u.log.debug('Disabling management_plugin charm config option...')
1804+ config = {'management_plugin': 'False'}
1805+ self.d.configure('rabbitmq-server', config)
1806+
1807+ # Negative check - tcp connect to management plugin port
1808+ u.log.info('Expect tcp connect fail since charm config '
1809+ 'option is disabled.')
1810+ tries = 0
1811+ ret = u.port_knock_units(sentry_units, mgmt_port, expect_success=False)
1812+ while ret and tries < (max_wait / 12):
1813+ time.sleep(12)
1814+ u.log.debug('Attempt {}: {}'.format(tries, ret))
1815+ ret = u.port_knock_units(sentry_units, mgmt_port,
1816+ expect_success=False)
1817+ tries += 1
1818+
1819+ if ret:
1820+ amulet.raise_status(amulet.FAIL, ret)
1821+ else:
1822+ u.log.info('Confirm mgmt port closed on all units (OK)\n')
1823+
1824+ def test_414_rmq_nrpe_monitors(self):
1825+ """Check rabbimq-server nrpe monitor basic functionality."""
1826+ sentry_units = self._get_rmq_sentry_units()
1827+ host_names = u.get_unit_hostnames(sentry_units)
1828+
1829+ # check_rabbitmq monitor
1830+ u.log.debug('Checking nrpe check_rabbitmq on units...')
1831+ cmds = ['egrep -oh /usr/local.* /etc/nagios/nrpe.d/'
1832+ 'check_rabbitmq.cfg']
1833+ ret = u.check_commands_on_units(cmds, sentry_units)
1834+ if ret:
1835+ amulet.raise_status(amulet.FAIL, msg=ret)
1836+
1837+ u.log.debug('Sleeping 70s for 1m cron job to run...')
1838+ time.sleep(70)
1839+
1840+ # check_rabbitmq_queue monitor
1841+ u.log.debug('Checking nrpe check_rabbitmq_queue on units...')
1842+ cmds = ['egrep -oh /usr/local.* /etc/nagios/nrpe.d/'
1843+ 'check_rabbitmq_queue.cfg']
1844+ ret = u.check_commands_on_units(cmds, sentry_units)
1845+ if ret:
1846+ amulet.raise_status(amulet.FAIL, msg=ret)
1847+
1848+ # check dat file existence
1849+ u.log.debug('Checking nrpe dat file existence on units...')
1850+ for sentry_unit in sentry_units:
1851+ unit_name = sentry_unit.info['unit_name']
1852+ unit_host_name = host_names[unit_name]
1853+
1854+ cmds = [
1855+ 'stat /var/lib/rabbitmq/data/{}_general_stats.dat'.format(
1856+ unit_host_name),
1857+ 'stat /var/lib/rabbitmq/data/{}_queue_stats.dat'.format(
1858+ unit_host_name)
1859+ ]
1860+
1861+ ret = u.check_commands_on_units(cmds, [sentry_unit])
1862+ if ret:
1863+ amulet.raise_status(amulet.FAIL, msg=ret)
1864+
1865+ u.log.info('OK\n')
1866
1867=== removed directory 'tests/charmhelpers/cli'
1868=== removed file 'tests/charmhelpers/cli/__init__.py'
1869--- tests/charmhelpers/cli/__init__.py 2015-09-23 12:04:43 +0000
1870+++ tests/charmhelpers/cli/__init__.py 1970-01-01 00:00:00 +0000
1871@@ -1,191 +0,0 @@
1872-# Copyright 2014-2015 Canonical Limited.
1873-#
1874-# This file is part of charm-helpers.
1875-#
1876-# charm-helpers is free software: you can redistribute it and/or modify
1877-# it under the terms of the GNU Lesser General Public License version 3 as
1878-# published by the Free Software Foundation.
1879-#
1880-# charm-helpers is distributed in the hope that it will be useful,
1881-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1882-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1883-# GNU Lesser General Public License for more details.
1884-#
1885-# You should have received a copy of the GNU Lesser General Public License
1886-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1887-
1888-import inspect
1889-import argparse
1890-import sys
1891-
1892-from six.moves import zip
1893-
1894-from charmhelpers.core import unitdata
1895-
1896-
1897-class OutputFormatter(object):
1898- def __init__(self, outfile=sys.stdout):
1899- self.formats = (
1900- "raw",
1901- "json",
1902- "py",
1903- "yaml",
1904- "csv",
1905- "tab",
1906- )
1907- self.outfile = outfile
1908-
1909- def add_arguments(self, argument_parser):
1910- formatgroup = argument_parser.add_mutually_exclusive_group()
1911- choices = self.supported_formats
1912- formatgroup.add_argument("--format", metavar='FMT',
1913- help="Select output format for returned data, "
1914- "where FMT is one of: {}".format(choices),
1915- choices=choices, default='raw')
1916- for fmt in self.formats:
1917- fmtfunc = getattr(self, fmt)
1918- formatgroup.add_argument("-{}".format(fmt[0]),
1919- "--{}".format(fmt), action='store_const',
1920- const=fmt, dest='format',
1921- help=fmtfunc.__doc__)
1922-
1923- @property
1924- def supported_formats(self):
1925- return self.formats
1926-
1927- def raw(self, output):
1928- """Output data as raw string (default)"""
1929- if isinstance(output, (list, tuple)):
1930- output = '\n'.join(map(str, output))
1931- self.outfile.write(str(output))
1932-
1933- def py(self, output):
1934- """Output data as a nicely-formatted python data structure"""
1935- import pprint
1936- pprint.pprint(output, stream=self.outfile)
1937-
1938- def json(self, output):
1939- """Output data in JSON format"""
1940- import json
1941- json.dump(output, self.outfile)
1942-
1943- def yaml(self, output):
1944- """Output data in YAML format"""
1945- import yaml
1946- yaml.safe_dump(output, self.outfile)
1947-
1948- def csv(self, output):
1949- """Output data as excel-compatible CSV"""
1950- import csv
1951- csvwriter = csv.writer(self.outfile)
1952- csvwriter.writerows(output)
1953-
1954- def tab(self, output):
1955- """Output data in excel-compatible tab-delimited format"""
1956- import csv
1957- csvwriter = csv.writer(self.outfile, dialect=csv.excel_tab)
1958- csvwriter.writerows(output)
1959-
1960- def format_output(self, output, fmt='raw'):
1961- fmtfunc = getattr(self, fmt)
1962- fmtfunc(output)
1963-
1964-
1965-class CommandLine(object):
1966- argument_parser = None
1967- subparsers = None
1968- formatter = None
1969- exit_code = 0
1970-
1971- def __init__(self):
1972- if not self.argument_parser:
1973- self.argument_parser = argparse.ArgumentParser(description='Perform common charm tasks')
1974- if not self.formatter:
1975- self.formatter = OutputFormatter()
1976- self.formatter.add_arguments(self.argument_parser)
1977- if not self.subparsers:
1978- self.subparsers = self.argument_parser.add_subparsers(help='Commands')
1979-
1980- def subcommand(self, command_name=None):
1981- """
1982- Decorate a function as a subcommand. Use its arguments as the
1983- command-line arguments"""
1984- def wrapper(decorated):
1985- cmd_name = command_name or decorated.__name__
1986- subparser = self.subparsers.add_parser(cmd_name,
1987- description=decorated.__doc__)
1988- for args, kwargs in describe_arguments(decorated):
1989- subparser.add_argument(*args, **kwargs)
1990- subparser.set_defaults(func=decorated)
1991- return decorated
1992- return wrapper
1993-
1994- def test_command(self, decorated):
1995- """
1996- Subcommand is a boolean test function, so bool return values should be
1997- converted to a 0/1 exit code.
1998- """
1999- decorated._cli_test_command = True
2000- return decorated
2001-
2002- def no_output(self, decorated):
2003- """
2004- Subcommand is not expected to return a value, so don't print a spurious None.
2005- """
2006- decorated._cli_no_output = True
2007- return decorated
2008-
2009- def subcommand_builder(self, command_name, description=None):
2010- """
2011- Decorate a function that builds a subcommand. Builders should accept a
2012- single argument (the subparser instance) and return the function to be
2013- run as the command."""
2014- def wrapper(decorated):
2015- subparser = self.subparsers.add_parser(command_name)
2016- func = decorated(subparser)
2017- subparser.set_defaults(func=func)
2018- subparser.description = description or func.__doc__
2019- return wrapper
2020-
2021- def run(self):
2022- "Run cli, processing arguments and executing subcommands."
2023- arguments = self.argument_parser.parse_args()
2024- argspec = inspect.getargspec(arguments.func)
2025- vargs = []
2026- for arg in argspec.args:
2027- vargs.append(getattr(arguments, arg))
2028- if argspec.varargs:
2029- vargs.extend(getattr(arguments, argspec.varargs))
2030- output = arguments.func(*vargs)
2031- if getattr(arguments.func, '_cli_test_command', False):
2032- self.exit_code = 0 if output else 1
2033- output = ''
2034- if getattr(arguments.func, '_cli_no_output', False):
2035- output = ''
2036- self.formatter.format_output(output, arguments.format)
2037- if unitdata._KV:
2038- unitdata._KV.flush()
2039-
2040-
2041-cmdline = CommandLine()
2042-
2043-
2044-def describe_arguments(func):
2045- """
2046- Analyze a function's signature and return a data structure suitable for
2047- passing in as arguments to an argparse parser's add_argument() method."""
2048-
2049- argspec = inspect.getargspec(func)
2050- # we should probably raise an exception somewhere if func includes **kwargs
2051- if argspec.defaults:
2052- positional_args = argspec.args[:-len(argspec.defaults)]
2053- keyword_names = argspec.args[-len(argspec.defaults):]
2054- for arg, default in zip(keyword_names, argspec.defaults):
2055- yield ('--{}'.format(arg),), {'default': default}
2056- else:
2057- positional_args = argspec.args
2058-
2059- for arg in positional_args:
2060- yield (arg,), {}
2061- if argspec.varargs:
2062- yield (argspec.varargs,), {'nargs': '*'}
2063
2064=== removed file 'tests/charmhelpers/cli/benchmark.py'
2065--- tests/charmhelpers/cli/benchmark.py 2015-08-10 16:38:33 +0000
2066+++ tests/charmhelpers/cli/benchmark.py 1970-01-01 00:00:00 +0000
2067@@ -1,36 +0,0 @@
2068-# Copyright 2014-2015 Canonical Limited.
2069-#
2070-# This file is part of charm-helpers.
2071-#
2072-# charm-helpers is free software: you can redistribute it and/or modify
2073-# it under the terms of the GNU Lesser General Public License version 3 as
2074-# published by the Free Software Foundation.
2075-#
2076-# charm-helpers is distributed in the hope that it will be useful,
2077-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2078-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2079-# GNU Lesser General Public License for more details.
2080-#
2081-# You should have received a copy of the GNU Lesser General Public License
2082-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2083-
2084-from . import cmdline
2085-from charmhelpers.contrib.benchmark import Benchmark
2086-
2087-
2088-@cmdline.subcommand(command_name='benchmark-start')
2089-def start():
2090- Benchmark.start()
2091-
2092-
2093-@cmdline.subcommand(command_name='benchmark-finish')
2094-def finish():
2095- Benchmark.finish()
2096-
2097-
2098-@cmdline.subcommand_builder('benchmark-composite', description="Set the benchmark composite score")
2099-def service(subparser):
2100- subparser.add_argument("value", help="The composite score.")
2101- subparser.add_argument("units", help="The units the composite score represents, i.e., 'reads/sec'.")
2102- subparser.add_argument("direction", help="'asc' if a lower score is better, 'desc' if a higher score is better.")
2103- return Benchmark.set_composite_score
2104
2105=== removed file 'tests/charmhelpers/cli/commands.py'
2106--- tests/charmhelpers/cli/commands.py 2015-09-23 12:04:43 +0000
2107+++ tests/charmhelpers/cli/commands.py 1970-01-01 00:00:00 +0000
2108@@ -1,32 +0,0 @@
2109-# Copyright 2014-2015 Canonical Limited.
2110-#
2111-# This file is part of charm-helpers.
2112-#
2113-# charm-helpers is free software: you can redistribute it and/or modify
2114-# it under the terms of the GNU Lesser General Public License version 3 as
2115-# published by the Free Software Foundation.
2116-#
2117-# charm-helpers is distributed in the hope that it will be useful,
2118-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2119-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2120-# GNU Lesser General Public License for more details.
2121-#
2122-# You should have received a copy of the GNU Lesser General Public License
2123-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2124-
2125-"""
2126-This module loads sub-modules into the python runtime so they can be
2127-discovered via the inspect module. In order to prevent flake8 from (rightfully)
2128-telling us these are unused modules, throw a ' # noqa' at the end of each import
2129-so that the warning is suppressed.
2130-"""
2131-
2132-from . import CommandLine # noqa
2133-
2134-"""
2135-Import the sub-modules which have decorated subcommands to register with chlp.
2136-"""
2137-from . import host # noqa
2138-from . import benchmark # noqa
2139-from . import unitdata # noqa
2140-from . import hookenv # noqa
2141
2142=== removed file 'tests/charmhelpers/cli/hookenv.py'
2143--- tests/charmhelpers/cli/hookenv.py 2015-09-23 12:04:43 +0000
2144+++ tests/charmhelpers/cli/hookenv.py 1970-01-01 00:00:00 +0000
2145@@ -1,23 +0,0 @@
2146-# Copyright 2014-2015 Canonical Limited.
2147-#
2148-# This file is part of charm-helpers.
2149-#
2150-# charm-helpers is free software: you can redistribute it and/or modify
2151-# it under the terms of the GNU Lesser General Public License version 3 as
2152-# published by the Free Software Foundation.
2153-#
2154-# charm-helpers is distributed in the hope that it will be useful,
2155-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2156-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2157-# GNU Lesser General Public License for more details.
2158-#
2159-# You should have received a copy of the GNU Lesser General Public License
2160-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2161-
2162-from . import cmdline
2163-from charmhelpers.core import hookenv
2164-
2165-
2166-cmdline.subcommand('relation-id')(hookenv.relation_id._wrapped)
2167-cmdline.subcommand('service-name')(hookenv.service_name)
2168-cmdline.subcommand('remote-service-name')(hookenv.remote_service_name._wrapped)
2169
2170=== removed file 'tests/charmhelpers/cli/host.py'
2171--- tests/charmhelpers/cli/host.py 2015-08-10 16:38:33 +0000
2172+++ tests/charmhelpers/cli/host.py 1970-01-01 00:00:00 +0000
2173@@ -1,31 +0,0 @@
2174-# Copyright 2014-2015 Canonical Limited.
2175-#
2176-# This file is part of charm-helpers.
2177-#
2178-# charm-helpers is free software: you can redistribute it and/or modify
2179-# it under the terms of the GNU Lesser General Public License version 3 as
2180-# published by the Free Software Foundation.
2181-#
2182-# charm-helpers is distributed in the hope that it will be useful,
2183-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2184-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2185-# GNU Lesser General Public License for more details.
2186-#
2187-# You should have received a copy of the GNU Lesser General Public License
2188-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2189-
2190-from . import cmdline
2191-from charmhelpers.core import host
2192-
2193-
2194-@cmdline.subcommand()
2195-def mounts():
2196- "List mounts"
2197- return host.mounts()
2198-
2199-
2200-@cmdline.subcommand_builder('service', description="Control system services")
2201-def service(subparser):
2202- subparser.add_argument("action", help="The action to perform (start, stop, etc...)")
2203- subparser.add_argument("service_name", help="Name of the service to control")
2204- return host.service
2205
2206=== removed file 'tests/charmhelpers/cli/unitdata.py'
2207--- tests/charmhelpers/cli/unitdata.py 2015-08-10 16:38:33 +0000
2208+++ tests/charmhelpers/cli/unitdata.py 1970-01-01 00:00:00 +0000
2209@@ -1,39 +0,0 @@
2210-# Copyright 2014-2015 Canonical Limited.
2211-#
2212-# This file is part of charm-helpers.
2213-#
2214-# charm-helpers is free software: you can redistribute it and/or modify
2215-# it under the terms of the GNU Lesser General Public License version 3 as
2216-# published by the Free Software Foundation.
2217-#
2218-# charm-helpers is distributed in the hope that it will be useful,
2219-# but WITHOUT ANY WARRANTY; without even the implied warranty of
2220-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2221-# GNU Lesser General Public License for more details.
2222-#
2223-# You should have received a copy of the GNU Lesser General Public License
2224-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2225-
2226-from . import cmdline
2227-from charmhelpers.core import unitdata
2228-
2229-
2230-@cmdline.subcommand_builder('unitdata', description="Store and retrieve data")
2231-def unitdata_cmd(subparser):
2232- nested = subparser.add_subparsers()
2233- get_cmd = nested.add_parser('get', help='Retrieve data')
2234- get_cmd.add_argument('key', help='Key to retrieve the value of')
2235- get_cmd.set_defaults(action='get', value=None)
2236- set_cmd = nested.add_parser('set', help='Store data')
2237- set_cmd.add_argument('key', help='Key to set')
2238- set_cmd.add_argument('value', help='Value to store')
2239- set_cmd.set_defaults(action='set')
2240-
2241- def _unitdata_cmd(action, key, value):
2242- if action == 'get':
2243- return unitdata.kv().get(key)
2244- elif action == 'set':
2245- unitdata.kv().set(key, value)
2246- unitdata.kv().flush()
2247- return ''
2248- return _unitdata_cmd
2249
2250=== added directory 'tests/charmhelpers/contrib/amulet'
2251=== added file 'tests/charmhelpers/contrib/amulet/__init__.py'
2252--- tests/charmhelpers/contrib/amulet/__init__.py 1970-01-01 00:00:00 +0000
2253+++ tests/charmhelpers/contrib/amulet/__init__.py 2015-10-13 18:33:25 +0000
2254@@ -0,0 +1,15 @@
2255+# Copyright 2014-2015 Canonical Limited.
2256+#
2257+# This file is part of charm-helpers.
2258+#
2259+# charm-helpers is free software: you can redistribute it and/or modify
2260+# it under the terms of the GNU Lesser General Public License version 3 as
2261+# published by the Free Software Foundation.
2262+#
2263+# charm-helpers is distributed in the hope that it will be useful,
2264+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2265+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2266+# GNU Lesser General Public License for more details.
2267+#
2268+# You should have received a copy of the GNU Lesser General Public License
2269+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2270
2271=== added file 'tests/charmhelpers/contrib/amulet/deployment.py'
2272--- tests/charmhelpers/contrib/amulet/deployment.py 1970-01-01 00:00:00 +0000
2273+++ tests/charmhelpers/contrib/amulet/deployment.py 2015-10-13 18:33:25 +0000
2274@@ -0,0 +1,93 @@
2275+# Copyright 2014-2015 Canonical Limited.
2276+#
2277+# This file is part of charm-helpers.
2278+#
2279+# charm-helpers is free software: you can redistribute it and/or modify
2280+# it under the terms of the GNU Lesser General Public License version 3 as
2281+# published by the Free Software Foundation.
2282+#
2283+# charm-helpers is distributed in the hope that it will be useful,
2284+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2285+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2286+# GNU Lesser General Public License for more details.
2287+#
2288+# You should have received a copy of the GNU Lesser General Public License
2289+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2290+
2291+import amulet
2292+import os
2293+import six
2294+
2295+
2296+class AmuletDeployment(object):
2297+ """Amulet deployment.
2298+
2299+ This class provides generic Amulet deployment and test runner
2300+ methods.
2301+ """
2302+
2303+ def __init__(self, series=None):
2304+ """Initialize the deployment environment."""
2305+ self.series = None
2306+
2307+ if series:
2308+ self.series = series
2309+ self.d = amulet.Deployment(series=self.series)
2310+ else:
2311+ self.d = amulet.Deployment()
2312+
2313+ def _add_services(self, this_service, other_services):
2314+ """Add services.
2315+
2316+ Add services to the deployment where this_service is the local charm
2317+ that we're testing and other_services are the other services that
2318+ are being used in the local amulet tests.
2319+ """
2320+ if this_service['name'] != os.path.basename(os.getcwd()):
2321+ s = this_service['name']
2322+ msg = "The charm's root directory name needs to be {}".format(s)
2323+ amulet.raise_status(amulet.FAIL, msg=msg)
2324+
2325+ if 'units' not in this_service:
2326+ this_service['units'] = 1
2327+
2328+ self.d.add(this_service['name'], units=this_service['units'])
2329+
2330+ for svc in other_services:
2331+ if 'location' in svc:
2332+ branch_location = svc['location']
2333+ elif self.series:
2334+ branch_location = 'cs:{}/{}'.format(self.series, svc['name']),
2335+ else:
2336+ branch_location = None
2337+
2338+ if 'units' not in svc:
2339+ svc['units'] = 1
2340+
2341+ self.d.add(svc['name'], charm=branch_location, units=svc['units'])
2342+
2343+ def _add_relations(self, relations):
2344+ """Add all of the relations for the services."""
2345+ for k, v in six.iteritems(relations):
2346+ self.d.relate(k, v)
2347+
2348+ def _configure_services(self, configs):
2349+ """Configure all of the services."""
2350+ for service, config in six.iteritems(configs):
2351+ self.d.configure(service, config)
2352+
2353+ def _deploy(self):
2354+ """Deploy environment and wait for all hooks to finish executing."""
2355+ try:
2356+ self.d.setup(timeout=900)
2357+ self.d.sentry.wait(timeout=900)
2358+ except amulet.helpers.TimeoutError:
2359+ amulet.raise_status(amulet.FAIL, msg="Deployment timed out")
2360+ except Exception:
2361+ raise
2362+
2363+ def run_tests(self):
2364+ """Run all of the methods that are prefixed with 'test_'."""
2365+ for test in dir(self):
2366+ if test.startswith('test_'):
2367+ getattr(self, test)()
2368
2369=== added file 'tests/charmhelpers/contrib/amulet/utils.py'
2370--- tests/charmhelpers/contrib/amulet/utils.py 1970-01-01 00:00:00 +0000
2371+++ tests/charmhelpers/contrib/amulet/utils.py 2015-10-13 18:33:25 +0000
2372@@ -0,0 +1,778 @@
2373+# Copyright 2014-2015 Canonical Limited.
2374+#
2375+# This file is part of charm-helpers.
2376+#
2377+# charm-helpers is free software: you can redistribute it and/or modify
2378+# it under the terms of the GNU Lesser General Public License version 3 as
2379+# published by the Free Software Foundation.
2380+#
2381+# charm-helpers is distributed in the hope that it will be useful,
2382+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2383+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2384+# GNU Lesser General Public License for more details.
2385+#
2386+# You should have received a copy of the GNU Lesser General Public License
2387+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2388+
2389+import io
2390+import json
2391+import logging
2392+import os
2393+import re
2394+import socket
2395+import subprocess
2396+import sys
2397+import time
2398+import uuid
2399+
2400+import amulet
2401+import distro_info
2402+import six
2403+from six.moves import configparser
2404+if six.PY3:
2405+ from urllib import parse as urlparse
2406+else:
2407+ import urlparse
2408+
2409+
2410+class AmuletUtils(object):
2411+ """Amulet utilities.
2412+
2413+ This class provides common utility functions that are used by Amulet
2414+ tests.
2415+ """
2416+
2417+ def __init__(self, log_level=logging.ERROR):
2418+ self.log = self.get_logger(level=log_level)
2419+ self.ubuntu_releases = self.get_ubuntu_releases()
2420+
2421+ def get_logger(self, name="amulet-logger", level=logging.DEBUG):
2422+ """Get a logger object that will log to stdout."""
2423+ log = logging
2424+ logger = log.getLogger(name)
2425+ fmt = log.Formatter("%(asctime)s %(funcName)s "
2426+ "%(levelname)s: %(message)s")
2427+
2428+ handler = log.StreamHandler(stream=sys.stdout)
2429+ handler.setLevel(level)
2430+ handler.setFormatter(fmt)
2431+
2432+ logger.addHandler(handler)
2433+ logger.setLevel(level)
2434+
2435+ return logger
2436+
2437+ def valid_ip(self, ip):
2438+ if re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", ip):
2439+ return True
2440+ else:
2441+ return False
2442+
2443+ def valid_url(self, url):
2444+ p = re.compile(
2445+ r'^(?:http|ftp)s?://'
2446+ r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # noqa
2447+ r'localhost|'
2448+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
2449+ r'(?::\d+)?'
2450+ r'(?:/?|[/?]\S+)$',
2451+ re.IGNORECASE)
2452+ if p.match(url):
2453+ return True
2454+ else:
2455+ return False
2456+
2457+ def get_ubuntu_release_from_sentry(self, sentry_unit):
2458+ """Get Ubuntu release codename from sentry unit.
2459+
2460+ :param sentry_unit: amulet sentry/service unit pointer
2461+ :returns: list of strings - release codename, failure message
2462+ """
2463+ msg = None
2464+ cmd = 'lsb_release -cs'
2465+ release, code = sentry_unit.run(cmd)
2466+ if code == 0:
2467+ self.log.debug('{} lsb_release: {}'.format(
2468+ sentry_unit.info['unit_name'], release))
2469+ else:
2470+ msg = ('{} `{}` returned {} '
2471+ '{}'.format(sentry_unit.info['unit_name'],
2472+ cmd, release, code))
2473+ if release not in self.ubuntu_releases:
2474+ msg = ("Release ({}) not found in Ubuntu releases "
2475+ "({})".format(release, self.ubuntu_releases))
2476+ return release, msg
2477+
2478+ def validate_services(self, commands):
2479+ """Validate that lists of commands succeed on service units. Can be
2480+ used to verify system services are running on the corresponding
2481+ service units.
2482+
2483+ :param commands: dict with sentry keys and arbitrary command list vals
2484+ :returns: None if successful, Failure string message otherwise
2485+ """
2486+ self.log.debug('Checking status of system services...')
2487+
2488+ # /!\ DEPRECATION WARNING (beisner):
2489+ # New and existing tests should be rewritten to use
2490+ # validate_services_by_name() as it is aware of init systems.
2491+ self.log.warn('DEPRECATION WARNING: use '
2492+ 'validate_services_by_name instead of validate_services '
2493+ 'due to init system differences.')
2494+
2495+ for k, v in six.iteritems(commands):
2496+ for cmd in v:
2497+ output, code = k.run(cmd)
2498+ self.log.debug('{} `{}` returned '
2499+ '{}'.format(k.info['unit_name'],
2500+ cmd, code))
2501+ if code != 0:
2502+ return "command `{}` returned {}".format(cmd, str(code))
2503+ return None
2504+
2505+ def validate_services_by_name(self, sentry_services):
2506+ """Validate system service status by service name, automatically
2507+ detecting init system based on Ubuntu release codename.
2508+
2509+ :param sentry_services: dict with sentry keys and svc list values
2510+ :returns: None if successful, Failure string message otherwise
2511+ """
2512+ self.log.debug('Checking status of system services...')
2513+
2514+ # Point at which systemd became a thing
2515+ systemd_switch = self.ubuntu_releases.index('vivid')
2516+
2517+ for sentry_unit, services_list in six.iteritems(sentry_services):
2518+ # Get lsb_release codename from unit
2519+ release, ret = self.get_ubuntu_release_from_sentry(sentry_unit)
2520+ if ret:
2521+ return ret
2522+
2523+ for service_name in services_list:
2524+ if (self.ubuntu_releases.index(release) >= systemd_switch or
2525+ service_name in ['rabbitmq-server', 'apache2']):
2526+ # init is systemd (or regular sysv)
2527+ cmd = 'sudo service {} status'.format(service_name)
2528+ output, code = sentry_unit.run(cmd)
2529+ service_running = code == 0
2530+ elif self.ubuntu_releases.index(release) < systemd_switch:
2531+ # init is upstart
2532+ cmd = 'sudo status {}'.format(service_name)
2533+ output, code = sentry_unit.run(cmd)
2534+ service_running = code == 0 and "start/running" in output
2535+
2536+ self.log.debug('{} `{}` returned '
2537+ '{}'.format(sentry_unit.info['unit_name'],
2538+ cmd, code))
2539+ if not service_running:
2540+ return u"command `{}` returned {} {}".format(
2541+ cmd, output, str(code))
2542+ return None
2543+
2544+ def _get_config(self, unit, filename):
2545+ """Get a ConfigParser object for parsing a unit's config file."""
2546+ file_contents = unit.file_contents(filename)
2547+
2548+ # NOTE(beisner): by default, ConfigParser does not handle options
2549+ # with no value, such as the flags used in the mysql my.cnf file.
2550+ # https://bugs.python.org/issue7005
2551+ config = configparser.ConfigParser(allow_no_value=True)
2552+ config.readfp(io.StringIO(file_contents))
2553+ return config
2554+
2555+ def validate_config_data(self, sentry_unit, config_file, section,
2556+ expected):
2557+ """Validate config file data.
2558+
2559+ Verify that the specified section of the config file contains
2560+ the expected option key:value pairs.
2561+
2562+ Compare expected dictionary data vs actual dictionary data.
2563+ The values in the 'expected' dictionary can be strings, bools, ints,
2564+ longs, or can be a function that evaluates a variable and returns a
2565+ bool.
2566+ """
2567+ self.log.debug('Validating config file data ({} in {} on {})'
2568+ '...'.format(section, config_file,
2569+ sentry_unit.info['unit_name']))
2570+ config = self._get_config(sentry_unit, config_file)
2571+
2572+ if section != 'DEFAULT' and not config.has_section(section):
2573+ return "section [{}] does not exist".format(section)
2574+
2575+ for k in expected.keys():
2576+ if not config.has_option(section, k):
2577+ return "section [{}] is missing option {}".format(section, k)
2578+
2579+ actual = config.get(section, k)
2580+ v = expected[k]
2581+ if (isinstance(v, six.string_types) or
2582+ isinstance(v, bool) or
2583+ isinstance(v, six.integer_types)):
2584+ # handle explicit values
2585+ if actual != v:
2586+ return "section [{}] {}:{} != expected {}:{}".format(
2587+ section, k, actual, k, expected[k])
2588+ # handle function pointers, such as not_null or valid_ip
2589+ elif not v(actual):
2590+ return "section [{}] {}:{} != expected {}:{}".format(
2591+ section, k, actual, k, expected[k])
2592+ return None
2593+
2594+ def _validate_dict_data(self, expected, actual):
2595+ """Validate dictionary data.
2596+
2597+ Compare expected dictionary data vs actual dictionary data.
2598+ The values in the 'expected' dictionary can be strings, bools, ints,
2599+ longs, or can be a function that evaluates a variable and returns a
2600+ bool.
2601+ """
2602+ self.log.debug('actual: {}'.format(repr(actual)))
2603+ self.log.debug('expected: {}'.format(repr(expected)))
2604+
2605+ for k, v in six.iteritems(expected):
2606+ if k in actual:
2607+ if (isinstance(v, six.string_types) or
2608+ isinstance(v, bool) or
2609+ isinstance(v, six.integer_types)):
2610+ # handle explicit values
2611+ if v != actual[k]:
2612+ return "{}:{}".format(k, actual[k])
2613+ # handle function pointers, such as not_null or valid_ip
2614+ elif not v(actual[k]):
2615+ return "{}:{}".format(k, actual[k])
2616+ else:
2617+ return "key '{}' does not exist".format(k)
2618+ return None
2619+
2620+ def validate_relation_data(self, sentry_unit, relation, expected):
2621+ """Validate actual relation data based on expected relation data."""
2622+ actual = sentry_unit.relation(relation[0], relation[1])
2623+ return self._validate_dict_data(expected, actual)
2624+
2625+ def _validate_list_data(self, expected, actual):
2626+ """Compare expected list vs actual list data."""
2627+ for e in expected:
2628+ if e not in actual:
2629+ return "expected item {} not found in actual list".format(e)
2630+ return None
2631+
2632+ def not_null(self, string):
2633+ if string is not None:
2634+ return True
2635+ else:
2636+ return False
2637+
2638+ def _get_file_mtime(self, sentry_unit, filename):
2639+ """Get last modification time of file."""
2640+ return sentry_unit.file_stat(filename)['mtime']
2641+
2642+ def _get_dir_mtime(self, sentry_unit, directory):
2643+ """Get last modification time of directory."""
2644+ return sentry_unit.directory_stat(directory)['mtime']
2645+
2646+ def _get_proc_start_time(self, sentry_unit, service, pgrep_full=None):
2647+ """Get start time of a process based on the last modification time
2648+ of the /proc/pid directory.
2649+
2650+ :sentry_unit: The sentry unit to check for the service on
2651+ :service: service name to look for in process table
2652+ :pgrep_full: [Deprecated] Use full command line search mode with pgrep
2653+ :returns: epoch time of service process start
2654+ :param commands: list of bash commands
2655+ :param sentry_units: list of sentry unit pointers
2656+ :returns: None if successful; Failure message otherwise
2657+ """
2658+ if pgrep_full is not None:
2659+ # /!\ DEPRECATION WARNING (beisner):
2660+ # No longer implemented, as pidof is now used instead of pgrep.
2661+ # https://bugs.launchpad.net/charm-helpers/+bug/1474030
2662+ self.log.warn('DEPRECATION WARNING: pgrep_full bool is no '
2663+ 'longer implemented re: lp 1474030.')
2664+
2665+ pid_list = self.get_process_id_list(sentry_unit, service)
2666+ pid = pid_list[0]
2667+ proc_dir = '/proc/{}'.format(pid)
2668+ self.log.debug('Pid for {} on {}: {}'.format(
2669+ service, sentry_unit.info['unit_name'], pid))
2670+
2671+ return self._get_dir_mtime(sentry_unit, proc_dir)
2672+
2673+ def service_restarted(self, sentry_unit, service, filename,
2674+ pgrep_full=None, sleep_time=20):
2675+ """Check if service was restarted.
2676+
2677+ Compare a service's start time vs a file's last modification time
2678+ (such as a config file for that service) to determine if the service
2679+ has been restarted.
2680+ """
2681+ # /!\ DEPRECATION WARNING (beisner):
2682+ # This method is prone to races in that no before-time is known.
2683+ # Use validate_service_config_changed instead.
2684+
2685+ # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
2686+ # used instead of pgrep. pgrep_full is still passed through to ensure
2687+ # deprecation WARNS. lp1474030
2688+ self.log.warn('DEPRECATION WARNING: use '
2689+ 'validate_service_config_changed instead of '
2690+ 'service_restarted due to known races.')
2691+
2692+ time.sleep(sleep_time)
2693+ if (self._get_proc_start_time(sentry_unit, service, pgrep_full) >=
2694+ self._get_file_mtime(sentry_unit, filename)):
2695+ return True
2696+ else:
2697+ return False
2698+
2699+ def service_restarted_since(self, sentry_unit, mtime, service,
2700+ pgrep_full=None, sleep_time=20,
2701+ retry_count=2, retry_sleep_time=30):
2702+ """Check if service was been started after a given time.
2703+
2704+ Args:
2705+ sentry_unit (sentry): The sentry unit to check for the service on
2706+ mtime (float): The epoch time to check against
2707+ service (string): service name to look for in process table
2708+ pgrep_full: [Deprecated] Use full command line search mode with pgrep
2709+ sleep_time (int): Seconds to sleep before looking for process
2710+ retry_count (int): If service is not found, how many times to retry
2711+
2712+ Returns:
2713+ bool: True if service found and its start time it newer than mtime,
2714+ False if service is older than mtime or if service was
2715+ not found.
2716+ """
2717+ # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
2718+ # used instead of pgrep. pgrep_full is still passed through to ensure
2719+ # deprecation WARNS. lp1474030
2720+
2721+ unit_name = sentry_unit.info['unit_name']
2722+ self.log.debug('Checking that %s service restarted since %s on '
2723+ '%s' % (service, mtime, unit_name))
2724+ time.sleep(sleep_time)
2725+ proc_start_time = None
2726+ tries = 0
2727+ while tries <= retry_count and not proc_start_time:
2728+ try:
2729+ proc_start_time = self._get_proc_start_time(sentry_unit,
2730+ service,
2731+ pgrep_full)
2732+ self.log.debug('Attempt {} to get {} proc start time on {} '
2733+ 'OK'.format(tries, service, unit_name))
2734+ except IOError:
2735+ # NOTE(beisner) - race avoidance, proc may not exist yet.
2736+ # https://bugs.launchpad.net/charm-helpers/+bug/1474030
2737+ self.log.debug('Attempt {} to get {} proc start time on {} '
2738+ 'failed'.format(tries, service, unit_name))
2739+ time.sleep(retry_sleep_time)
2740+ tries += 1
2741+
2742+ if not proc_start_time:
2743+ self.log.warn('No proc start time found, assuming service did '
2744+ 'not start')
2745+ return False
2746+ if proc_start_time >= mtime:
2747+ self.log.debug('Proc start time is newer than provided mtime'
2748+ '(%s >= %s) on %s (OK)' % (proc_start_time,
2749+ mtime, unit_name))
2750+ return True
2751+ else:
2752+ self.log.warn('Proc start time (%s) is older than provided mtime '
2753+ '(%s) on %s, service did not '
2754+ 'restart' % (proc_start_time, mtime, unit_name))
2755+ return False
2756+
2757+ def config_updated_since(self, sentry_unit, filename, mtime,
2758+ sleep_time=20):
2759+ """Check if file was modified after a given time.
2760+
2761+ Args:
2762+ sentry_unit (sentry): The sentry unit to check the file mtime on
2763+ filename (string): The file to check mtime of
2764+ mtime (float): The epoch time to check against
2765+ sleep_time (int): Seconds to sleep before looking for process
2766+
2767+ Returns:
2768+ bool: True if file was modified more recently than mtime, False if
2769+ file was modified before mtime,
2770+ """
2771+ self.log.debug('Checking %s updated since %s' % (filename, mtime))
2772+ time.sleep(sleep_time)
2773+ file_mtime = self._get_file_mtime(sentry_unit, filename)
2774+ if file_mtime >= mtime:
2775+ self.log.debug('File mtime is newer than provided mtime '
2776+ '(%s >= %s)' % (file_mtime, mtime))
2777+ return True
2778+ else:
2779+ self.log.warn('File mtime %s is older than provided mtime %s'
2780+ % (file_mtime, mtime))
2781+ return False
2782+
2783+ def validate_service_config_changed(self, sentry_unit, mtime, service,
2784+ filename, pgrep_full=None,
2785+ sleep_time=20, retry_count=2,
2786+ retry_sleep_time=30):
2787+ """Check service and file were updated after mtime
2788+
2789+ Args:
2790+ sentry_unit (sentry): The sentry unit to check for the service on
2791+ mtime (float): The epoch time to check against
2792+ service (string): service name to look for in process table
2793+ filename (string): The file to check mtime of
2794+ pgrep_full: [Deprecated] Use full command line search mode with pgrep
2795+ sleep_time (int): Initial sleep in seconds to pass to test helpers
2796+ retry_count (int): If service is not found, how many times to retry
2797+ retry_sleep_time (int): Time in seconds to wait between retries
2798+
2799+ Typical Usage:
2800+ u = OpenStackAmuletUtils(ERROR)
2801+ ...
2802+ mtime = u.get_sentry_time(self.cinder_sentry)
2803+ self.d.configure('cinder', {'verbose': 'True', 'debug': 'True'})
2804+ if not u.validate_service_config_changed(self.cinder_sentry,
2805+ mtime,
2806+ 'cinder-api',
2807+ '/etc/cinder/cinder.conf')
2808+ amulet.raise_status(amulet.FAIL, msg='update failed')
2809+ Returns:
2810+ bool: True if both service and file where updated/restarted after
2811+ mtime, False if service is older than mtime or if service was
2812+ not found or if filename was modified before mtime.
2813+ """
2814+
2815+ # NOTE(beisner) pgrep_full is no longer implemented, as pidof is now
2816+ # used instead of pgrep. pgrep_full is still passed through to ensure
2817+ # deprecation WARNS. lp1474030
2818+
2819+ service_restart = self.service_restarted_since(
2820+ sentry_unit, mtime,
2821+ service,
2822+ pgrep_full=pgrep_full,
2823+ sleep_time=sleep_time,
2824+ retry_count=retry_count,
2825+ retry_sleep_time=retry_sleep_time)
2826+
2827+ config_update = self.config_updated_since(
2828+ sentry_unit,
2829+ filename,
2830+ mtime,
2831+ sleep_time=0)
2832+
2833+ return service_restart and config_update
2834+
2835+ def get_sentry_time(self, sentry_unit):
2836+ """Return current epoch time on a sentry"""
2837+ cmd = "date +'%s'"
2838+ return float(sentry_unit.run(cmd)[0])
2839+
2840+ def relation_error(self, name, data):
2841+ return 'unexpected relation data in {} - {}'.format(name, data)
2842+
2843+ def endpoint_error(self, name, data):
2844+ return 'unexpected endpoint data in {} - {}'.format(name, data)
2845+
2846+ def get_ubuntu_releases(self):
2847+ """Return a list of all Ubuntu releases in order of release."""
2848+ _d = distro_info.UbuntuDistroInfo()
2849+ _release_list = _d.all
2850+ return _release_list
2851+
2852+ def file_to_url(self, file_rel_path):
2853+ """Convert a relative file path to a file URL."""
2854+ _abs_path = os.path.abspath(file_rel_path)
2855+ return urlparse.urlparse(_abs_path, scheme='file').geturl()
2856+
2857+ def check_commands_on_units(self, commands, sentry_units):
2858+ """Check that all commands in a list exit zero on all
2859+ sentry units in a list.
2860+
2861+ :param commands: list of bash commands
2862+ :param sentry_units: list of sentry unit pointers
2863+ :returns: None if successful; Failure message otherwise
2864+ """
2865+ self.log.debug('Checking exit codes for {} commands on {} '
2866+ 'sentry units...'.format(len(commands),
2867+ len(sentry_units)))
2868+ for sentry_unit in sentry_units:
2869+ for cmd in commands:
2870+ output, code = sentry_unit.run(cmd)
2871+ if code == 0:
2872+ self.log.debug('{} `{}` returned {} '
2873+ '(OK)'.format(sentry_unit.info['unit_name'],
2874+ cmd, code))
2875+ else:
2876+ return ('{} `{}` returned {} '
2877+ '{}'.format(sentry_unit.info['unit_name'],
2878+ cmd, code, output))
2879+ return None
2880+
2881+ def get_process_id_list(self, sentry_unit, process_name,
2882+ expect_success=True):
2883+ """Get a list of process ID(s) from a single sentry juju unit
2884+ for a single process name.
2885+
2886+ :param sentry_unit: Amulet sentry instance (juju unit)
2887+ :param process_name: Process name
2888+ :param expect_success: If False, expect the PID to be missing,
2889+ raise if it is present.
2890+ :returns: List of process IDs
2891+ """
2892+ cmd = 'pidof -x {}'.format(process_name)
2893+ if not expect_success:
2894+ cmd += " || exit 0 && exit 1"
2895+ output, code = sentry_unit.run(cmd)
2896+ if code != 0:
2897+ msg = ('{} `{}` returned {} '
2898+ '{}'.format(sentry_unit.info['unit_name'],
2899+ cmd, code, output))
2900+ amulet.raise_status(amulet.FAIL, msg=msg)
2901+ return str(output).split()
2902+
2903+ def get_unit_process_ids(self, unit_processes, expect_success=True):
2904+ """Construct a dict containing unit sentries, process names, and
2905+ process IDs.
2906+
2907+ :param unit_processes: A dictionary of Amulet sentry instance
2908+ to list of process names.
2909+ :param expect_success: if False expect the processes to not be
2910+ running, raise if they are.
2911+ :returns: Dictionary of Amulet sentry instance to dictionary
2912+ of process names to PIDs.
2913+ """
2914+ pid_dict = {}
2915+ for sentry_unit, process_list in six.iteritems(unit_processes):
2916+ pid_dict[sentry_unit] = {}
2917+ for process in process_list:
2918+ pids = self.get_process_id_list(
2919+ sentry_unit, process, expect_success=expect_success)
2920+ pid_dict[sentry_unit].update({process: pids})
2921+ return pid_dict
2922+
2923+ def validate_unit_process_ids(self, expected, actual):
2924+ """Validate process id quantities for services on units."""
2925+ self.log.debug('Checking units for running processes...')
2926+ self.log.debug('Expected PIDs: {}'.format(expected))
2927+ self.log.debug('Actual PIDs: {}'.format(actual))
2928+
2929+ if len(actual) != len(expected):
2930+ return ('Unit count mismatch. expected, actual: {}, '
2931+ '{} '.format(len(expected), len(actual)))
2932+
2933+ for (e_sentry, e_proc_names) in six.iteritems(expected):
2934+ e_sentry_name = e_sentry.info['unit_name']
2935+ if e_sentry in actual.keys():
2936+ a_proc_names = actual[e_sentry]
2937+ else:
2938+ return ('Expected sentry ({}) not found in actual dict data.'
2939+ '{}'.format(e_sentry_name, e_sentry))
2940+
2941+ if len(e_proc_names.keys()) != len(a_proc_names.keys()):
2942+ return ('Process name count mismatch. expected, actual: {}, '
2943+ '{}'.format(len(expected), len(actual)))
2944+
2945+ for (e_proc_name, e_pids_length), (a_proc_name, a_pids) in \
2946+ zip(e_proc_names.items(), a_proc_names.items()):
2947+ if e_proc_name != a_proc_name:
2948+ return ('Process name mismatch. expected, actual: {}, '
2949+ '{}'.format(e_proc_name, a_proc_name))
2950+
2951+ a_pids_length = len(a_pids)
2952+ fail_msg = ('PID count mismatch. {} ({}) expected, actual: '
2953+ '{}, {} ({})'.format(e_sentry_name, e_proc_name,
2954+ e_pids_length, a_pids_length,
2955+ a_pids))
2956+
2957+ # If expected is not bool, ensure PID quantities match
2958+ if not isinstance(e_pids_length, bool) and \
2959+ a_pids_length != e_pids_length:
2960+ return fail_msg
2961+ # If expected is bool True, ensure 1 or more PIDs exist
2962+ elif isinstance(e_pids_length, bool) and \
2963+ e_pids_length is True and a_pids_length < 1:
2964+ return fail_msg
2965+ # If expected is bool False, ensure 0 PIDs exist
2966+ elif isinstance(e_pids_length, bool) and \
2967+ e_pids_length is False and a_pids_length != 0:
2968+ return fail_msg
2969+ else:
2970+ self.log.debug('PID check OK: {} {} {}: '
2971+ '{}'.format(e_sentry_name, e_proc_name,
2972+ e_pids_length, a_pids))
2973+ return None
2974+
2975+ def validate_list_of_identical_dicts(self, list_of_dicts):
2976+ """Check that all dicts within a list are identical."""
2977+ hashes = []
2978+ for _dict in list_of_dicts:
2979+ hashes.append(hash(frozenset(_dict.items())))
2980+
2981+ self.log.debug('Hashes: {}'.format(hashes))
2982+ if len(set(hashes)) == 1:
2983+ self.log.debug('Dicts within list are identical')
2984+ else:
2985+ return 'Dicts within list are not identical'
2986+
2987+ return None
2988+
2989+ def validate_sectionless_conf(self, file_contents, expected):
2990+ """A crude conf parser. Useful to inspect configuration files which
2991+ do not have section headers (as would be necessary in order to use
2992+ the configparser). Such as openstack-dashboard or rabbitmq confs."""
2993+ for line in file_contents.split('\n'):
2994+ if '=' in line:
2995+ args = line.split('=')
2996+ if len(args) <= 1:
2997+ continue
2998+ key = args[0].strip()
2999+ value = args[1].strip()
3000+ if key in expected.keys():
3001+ if expected[key] != value:
3002+ msg = ('Config mismatch. Expected, actual: {}, '
3003+ '{}'.format(expected[key], value))
3004+ amulet.raise_status(amulet.FAIL, msg=msg)
3005+
3006+ def get_unit_hostnames(self, units):
3007+ """Return a dict of juju unit names to hostnames."""
3008+ host_names = {}
3009+ for unit in units:
3010+ host_names[unit.info['unit_name']] = \
3011+ str(unit.file_contents('/etc/hostname').strip())
3012+ self.log.debug('Unit host names: {}'.format(host_names))
3013+ return host_names
3014+
3015+ def run_cmd_unit(self, sentry_unit, cmd):
3016+ """Run a command on a unit, return the output and exit code."""
3017+ output, code = sentry_unit.run(cmd)
3018+ if code == 0:
3019+ self.log.debug('{} `{}` command returned {} '
3020+ '(OK)'.format(sentry_unit.info['unit_name'],
3021+ cmd, code))
3022+ else:
3023+ msg = ('{} `{}` command returned {} '
3024+ '{}'.format(sentry_unit.info['unit_name'],
3025+ cmd, code, output))
3026+ amulet.raise_status(amulet.FAIL, msg=msg)
3027+ return str(output), code
3028+
3029+ def file_exists_on_unit(self, sentry_unit, file_name):
3030+ """Check if a file exists on a unit."""
3031+ try:
3032+ sentry_unit.file_stat(file_name)
3033+ return True
3034+ except IOError:
3035+ return False
3036+ except Exception as e:
3037+ msg = 'Error checking file {}: {}'.format(file_name, e)
3038+ amulet.raise_status(amulet.FAIL, msg=msg)
3039+
3040+ def file_contents_safe(self, sentry_unit, file_name,
3041+ max_wait=60, fatal=False):
3042+ """Get file contents from a sentry unit. Wrap amulet file_contents
3043+ with retry logic to address races where a file checks as existing,
3044+ but no longer exists by the time file_contents is called.
3045+ Return None if file not found. Optionally raise if fatal is True."""
3046+ unit_name = sentry_unit.info['unit_name']
3047+ file_contents = False
3048+ tries = 0
3049+ while not file_contents and tries < (max_wait / 4):
3050+ try:
3051+ file_contents = sentry_unit.file_contents(file_name)
3052+ except IOError:
3053+ self.log.debug('Attempt {} to open file {} from {} '
3054+ 'failed'.format(tries, file_name,
3055+ unit_name))
3056+ time.sleep(4)
3057+ tries += 1
3058+
3059+ if file_contents:
3060+ return file_contents
3061+ elif not fatal:
3062+ return None
3063+ elif fatal:
3064+ msg = 'Failed to get file contents from unit.'
3065+ amulet.raise_status(amulet.FAIL, msg)
3066+
3067+ def port_knock_tcp(self, host="localhost", port=22, timeout=15):
3068+ """Open a TCP socket to check for a listening sevice on a host.
3069+
3070+ :param host: host name or IP address, default to localhost
3071+ :param port: TCP port number, default to 22
3072+ :param timeout: Connect timeout, default to 15 seconds
3073+ :returns: True if successful, False if connect failed
3074+ """
3075+
3076+ # Resolve host name if possible
3077+ try:
3078+ connect_host = socket.gethostbyname(host)
3079+ host_human = "{} ({})".format(connect_host, host)
3080+ except socket.error as e:
3081+ self.log.warn('Unable to resolve address: '
3082+ '{} ({}) Trying anyway!'.format(host, e))
3083+ connect_host = host
3084+ host_human = connect_host
3085+
3086+ # Attempt socket connection
3087+ try:
3088+ knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
3089+ knock.settimeout(timeout)
3090+ knock.connect((connect_host, port))
3091+ knock.close()
3092+ self.log.debug('Socket connect OK for host '
3093+ '{} on port {}.'.format(host_human, port))
3094+ return True
3095+ except socket.error as e:
3096+ self.log.debug('Socket connect FAIL for'
3097+ ' {} port {} ({})'.format(host_human, port, e))
3098+ return False
3099+
3100+ def port_knock_units(self, sentry_units, port=22,
3101+ timeout=15, expect_success=True):
3102+ """Open a TCP socket to check for a listening sevice on each
3103+ listed juju unit.
3104+
3105+ :param sentry_units: list of sentry unit pointers
3106+ :param port: TCP port number, default to 22
3107+ :param timeout: Connect timeout, default to 15 seconds
3108+ :expect_success: True by default, set False to invert logic
3109+ :returns: None if successful, Failure message otherwise
3110+ """
3111+ for unit in sentry_units:
3112+ host = unit.info['public-address']
3113+ connected = self.port_knock_tcp(host, port, timeout)
3114+ if not connected and expect_success:
3115+ return 'Socket connect failed.'
3116+ elif connected and not expect_success:
3117+ return 'Socket connected unexpectedly.'
3118+
3119+ def get_uuid_epoch_stamp(self):
3120+ """Returns a stamp string based on uuid4 and epoch time. Useful in
3121+ generating test messages which need to be unique-ish."""
3122+ return '[{}-{}]'.format(uuid.uuid4(), time.time())
3123+
3124+# amulet juju action helpers:
3125+ def run_action(self, unit_sentry, action,
3126+ _check_output=subprocess.check_output):
3127+ """Run the named action on a given unit sentry.
3128+
3129+ _check_output parameter is used for dependency injection.
3130+
3131+ @return action_id.
3132+ """
3133+ unit_id = unit_sentry.info["unit_name"]
3134+ command = ["juju", "action", "do", "--format=json", unit_id, action]
3135+ self.log.info("Running command: %s\n" % " ".join(command))
3136+ output = _check_output(command, universal_newlines=True)
3137+ data = json.loads(output)
3138+ action_id = data[u'Action queued with id']
3139+ return action_id
3140+
3141+ def wait_on_action(self, action_id, _check_output=subprocess.check_output):
3142+ """Wait for a given action, returning if it completed or not.
3143+
3144+ _check_output parameter is used for dependency injection.
3145+ """
3146+ command = ["juju", "action", "fetch", "--format=json", "--wait=0",
3147+ action_id]
3148+ output = _check_output(command, universal_newlines=True)
3149+ data = json.loads(output)
3150+ return data.get(u"status") == "completed"
3151
3152=== added directory 'tests/charmhelpers/contrib/openstack'
3153=== added file 'tests/charmhelpers/contrib/openstack/__init__.py'
3154--- tests/charmhelpers/contrib/openstack/__init__.py 1970-01-01 00:00:00 +0000
3155+++ tests/charmhelpers/contrib/openstack/__init__.py 2015-10-13 18:33:25 +0000
3156@@ -0,0 +1,15 @@
3157+# Copyright 2014-2015 Canonical Limited.
3158+#
3159+# This file is part of charm-helpers.
3160+#
3161+# charm-helpers is free software: you can redistribute it and/or modify
3162+# it under the terms of the GNU Lesser General Public License version 3 as
3163+# published by the Free Software Foundation.
3164+#
3165+# charm-helpers is distributed in the hope that it will be useful,
3166+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3167+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3168+# GNU Lesser General Public License for more details.
3169+#
3170+# You should have received a copy of the GNU Lesser General Public License
3171+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3172
3173=== added directory 'tests/charmhelpers/contrib/openstack/amulet'
3174=== added file 'tests/charmhelpers/contrib/openstack/amulet/__init__.py'
3175--- tests/charmhelpers/contrib/openstack/amulet/__init__.py 1970-01-01 00:00:00 +0000
3176+++ tests/charmhelpers/contrib/openstack/amulet/__init__.py 2015-10-13 18:33:25 +0000
3177@@ -0,0 +1,15 @@
3178+# Copyright 2014-2015 Canonical Limited.
3179+#
3180+# This file is part of charm-helpers.
3181+#
3182+# charm-helpers is free software: you can redistribute it and/or modify
3183+# it under the terms of the GNU Lesser General Public License version 3 as
3184+# published by the Free Software Foundation.
3185+#
3186+# charm-helpers is distributed in the hope that it will be useful,
3187+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3188+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3189+# GNU Lesser General Public License for more details.
3190+#
3191+# You should have received a copy of the GNU Lesser General Public License
3192+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3193
3194=== added file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
3195--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 1970-01-01 00:00:00 +0000
3196+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-10-13 18:33:25 +0000
3197@@ -0,0 +1,198 @@
3198+# Copyright 2014-2015 Canonical Limited.
3199+#
3200+# This file is part of charm-helpers.
3201+#
3202+# charm-helpers is free software: you can redistribute it and/or modify
3203+# it under the terms of the GNU Lesser General Public License version 3 as
3204+# published by the Free Software Foundation.
3205+#
3206+# charm-helpers is distributed in the hope that it will be useful,
3207+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3208+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3209+# GNU Lesser General Public License for more details.
3210+#
3211+# You should have received a copy of the GNU Lesser General Public License
3212+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3213+
3214+import six
3215+from collections import OrderedDict
3216+from charmhelpers.contrib.amulet.deployment import (
3217+ AmuletDeployment
3218+)
3219+
3220+
3221+class OpenStackAmuletDeployment(AmuletDeployment):
3222+ """OpenStack amulet deployment.
3223+
3224+ This class inherits from AmuletDeployment and has additional support
3225+ that is specifically for use by OpenStack charms.
3226+ """
3227+
3228+ def __init__(self, series=None, openstack=None, source=None, stable=True):
3229+ """Initialize the deployment environment."""
3230+ super(OpenStackAmuletDeployment, self).__init__(series)
3231+ self.openstack = openstack
3232+ self.source = source
3233+ self.stable = stable
3234+ # Note(coreycb): this needs to be changed when new next branches come
3235+ # out.
3236+ self.current_next = "trusty"
3237+
3238+ def _determine_branch_locations(self, other_services):
3239+ """Determine the branch locations for the other services.
3240+
3241+ Determine if the local branch being tested is derived from its
3242+ stable or next (dev) branch, and based on this, use the corresonding
3243+ stable or next branches for the other_services."""
3244+
3245+ # Charms outside the lp:~openstack-charmers namespace
3246+ base_charms = ['mysql', 'mongodb', 'nrpe']
3247+
3248+ # Force these charms to current series even when using an older series.
3249+ # ie. Use trusty/nrpe even when series is precise, as the P charm
3250+ # does not possess the necessary external master config and hooks.
3251+ force_series_current = ['nrpe']
3252+
3253+ if self.series in ['precise', 'trusty']:
3254+ base_series = self.series
3255+ else:
3256+ base_series = self.current_next
3257+
3258+ if self.stable:
3259+ for svc in other_services:
3260+ if svc['name'] in force_series_current:
3261+ base_series = self.current_next
3262+
3263+ temp = 'lp:charms/{}/{}'
3264+ svc['location'] = temp.format(base_series,
3265+ svc['name'])
3266+ else:
3267+ for svc in other_services:
3268+ if svc['name'] in force_series_current:
3269+ base_series = self.current_next
3270+
3271+ if svc['name'] in base_charms:
3272+ temp = 'lp:charms/{}/{}'
3273+ svc['location'] = temp.format(base_series,
3274+ svc['name'])
3275+ else:
3276+ temp = 'lp:~openstack-charmers/charms/{}/{}/next'
3277+ svc['location'] = temp.format(self.current_next,
3278+ svc['name'])
3279+ return other_services
3280+
3281+ def _add_services(self, this_service, other_services):
3282+ """Add services to the deployment and set openstack-origin/source."""
3283+ other_services = self._determine_branch_locations(other_services)
3284+
3285+ super(OpenStackAmuletDeployment, self)._add_services(this_service,
3286+ other_services)
3287+
3288+ services = other_services
3289+ services.append(this_service)
3290+
3291+ # Charms which should use the source config option
3292+ use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
3293+ 'ceph-osd', 'ceph-radosgw']
3294+
3295+ # Charms which can not use openstack-origin, ie. many subordinates
3296+ no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
3297+
3298+ if self.openstack:
3299+ for svc in services:
3300+ if svc['name'] not in use_source + no_origin:
3301+ config = {'openstack-origin': self.openstack}
3302+ self.d.configure(svc['name'], config)
3303+
3304+ if self.source:
3305+ for svc in services:
3306+ if svc['name'] in use_source and svc['name'] not in no_origin:
3307+ config = {'source': self.source}
3308+ self.d.configure(svc['name'], config)
3309+
3310+ def _configure_services(self, configs):
3311+ """Configure all of the services."""
3312+ for service, config in six.iteritems(configs):
3313+ self.d.configure(service, config)
3314+
3315+ def _get_openstack_release(self):
3316+ """Get openstack release.
3317+
3318+ Return an integer representing the enum value of the openstack
3319+ release.
3320+ """
3321+ # Must be ordered by OpenStack release (not by Ubuntu release):
3322+ (self.precise_essex, self.precise_folsom, self.precise_grizzly,
3323+ self.precise_havana, self.precise_icehouse,
3324+ self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
3325+ self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
3326+ self.wily_liberty) = range(12)
3327+
3328+ releases = {
3329+ ('precise', None): self.precise_essex,
3330+ ('precise', 'cloud:precise-folsom'): self.precise_folsom,
3331+ ('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
3332+ ('precise', 'cloud:precise-havana'): self.precise_havana,
3333+ ('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
3334+ ('trusty', None): self.trusty_icehouse,
3335+ ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
3336+ ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
3337+ ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
3338+ ('utopic', None): self.utopic_juno,
3339+ ('vivid', None): self.vivid_kilo,
3340+ ('wily', None): self.wily_liberty}
3341+ return releases[(self.series, self.openstack)]
3342+
3343+ def _get_openstack_release_string(self):
3344+ """Get openstack release string.
3345+
3346+ Return a string representing the openstack release.
3347+ """
3348+ releases = OrderedDict([
3349+ ('precise', 'essex'),
3350+ ('quantal', 'folsom'),
3351+ ('raring', 'grizzly'),
3352+ ('saucy', 'havana'),
3353+ ('trusty', 'icehouse'),
3354+ ('utopic', 'juno'),
3355+ ('vivid', 'kilo'),
3356+ ('wily', 'liberty'),
3357+ ])
3358+ if self.openstack:
3359+ os_origin = self.openstack.split(':')[1]
3360+ return os_origin.split('%s-' % self.series)[1].split('/')[0]
3361+ else:
3362+ return releases[self.series]
3363+
3364+ def get_ceph_expected_pools(self, radosgw=False):
3365+ """Return a list of expected ceph pools in a ceph + cinder + glance
3366+ test scenario, based on OpenStack release and whether ceph radosgw
3367+ is flagged as present or not."""
3368+
3369+ if self._get_openstack_release() >= self.trusty_kilo:
3370+ # Kilo or later
3371+ pools = [
3372+ 'rbd',
3373+ 'cinder',
3374+ 'glance'
3375+ ]
3376+ else:
3377+ # Juno or earlier
3378+ pools = [
3379+ 'data',
3380+ 'metadata',
3381+ 'rbd',
3382+ 'cinder',
3383+ 'glance'
3384+ ]
3385+
3386+ if radosgw:
3387+ pools.extend([
3388+ '.rgw.root',
3389+ '.rgw.control',
3390+ '.rgw',
3391+ '.rgw.gc',
3392+ '.users.uid'
3393+ ])
3394+
3395+ return pools
3396
3397=== added file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
3398--- tests/charmhelpers/contrib/openstack/amulet/utils.py 1970-01-01 00:00:00 +0000
3399+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-10-13 18:33:25 +0000
3400@@ -0,0 +1,963 @@
3401+# Copyright 2014-2015 Canonical Limited.
3402+#
3403+# This file is part of charm-helpers.
3404+#
3405+# charm-helpers is free software: you can redistribute it and/or modify
3406+# it under the terms of the GNU Lesser General Public License version 3 as
3407+# published by the Free Software Foundation.
3408+#
3409+# charm-helpers is distributed in the hope that it will be useful,
3410+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3411+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3412+# GNU Lesser General Public License for more details.
3413+#
3414+# You should have received a copy of the GNU Lesser General Public License
3415+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3416+
3417+import amulet
3418+import json
3419+import logging
3420+import os
3421+import six
3422+import time
3423+import urllib
3424+
3425+import cinderclient.v1.client as cinder_client
3426+import glanceclient.v1.client as glance_client
3427+import heatclient.v1.client as heat_client
3428+import keystoneclient.v2_0 as keystone_client
3429+import novaclient.v1_1.client as nova_client
3430+import pika
3431+import swiftclient
3432+
3433+from charmhelpers.contrib.amulet.utils import (
3434+ AmuletUtils
3435+)
3436+
3437+DEBUG = logging.DEBUG
3438+ERROR = logging.ERROR
3439+
3440+
3441+class OpenStackAmuletUtils(AmuletUtils):
3442+ """OpenStack amulet utilities.
3443+
3444+ This class inherits from AmuletUtils and has additional support
3445+ that is specifically for use by OpenStack charm tests.
3446+ """
3447+
3448+ def __init__(self, log_level=ERROR):
3449+ """Initialize the deployment environment."""
3450+ super(OpenStackAmuletUtils, self).__init__(log_level)
3451+
3452+ def validate_endpoint_data(self, endpoints, admin_port, internal_port,
3453+ public_port, expected):
3454+ """Validate endpoint data.
3455+
3456+ Validate actual endpoint data vs expected endpoint data. The ports
3457+ are used to find the matching endpoint.
3458+ """
3459+ self.log.debug('Validating endpoint data...')
3460+ self.log.debug('actual: {}'.format(repr(endpoints)))
3461+ found = False
3462+ for ep in endpoints:
3463+ self.log.debug('endpoint: {}'.format(repr(ep)))
3464+ if (admin_port in ep.adminurl and
3465+ internal_port in ep.internalurl and
3466+ public_port in ep.publicurl):
3467+ found = True
3468+ actual = {'id': ep.id,
3469+ 'region': ep.region,
3470+ 'adminurl': ep.adminurl,
3471+ 'internalurl': ep.internalurl,
3472+ 'publicurl': ep.publicurl,
3473+ 'service_id': ep.service_id}
3474+ ret = self._validate_dict_data(expected, actual)
3475+ if ret:
3476+ return 'unexpected endpoint data - {}'.format(ret)
3477+
3478+ if not found:
3479+ return 'endpoint not found'
3480+
3481+ def validate_svc_catalog_endpoint_data(self, expected, actual):
3482+ """Validate service catalog endpoint data.
3483+
3484+ Validate a list of actual service catalog endpoints vs a list of
3485+ expected service catalog endpoints.
3486+ """
3487+ self.log.debug('Validating service catalog endpoint data...')
3488+ self.log.debug('actual: {}'.format(repr(actual)))
3489+ for k, v in six.iteritems(expected):
3490+ if k in actual:
3491+ ret = self._validate_dict_data(expected[k][0], actual[k][0])
3492+ if ret:
3493+ return self.endpoint_error(k, ret)
3494+ else:
3495+ return "endpoint {} does not exist".format(k)
3496+ return ret
3497+
3498+ def validate_tenant_data(self, expected, actual):
3499+ """Validate tenant data.
3500+
3501+ Validate a list of actual tenant data vs list of expected tenant
3502+ data.
3503+ """
3504+ self.log.debug('Validating tenant data...')
3505+ self.log.debug('actual: {}'.format(repr(actual)))
3506+ for e in expected:
3507+ found = False
3508+ for act in actual:
3509+ a = {'enabled': act.enabled, 'description': act.description,
3510+ 'name': act.name, 'id': act.id}
3511+ if e['name'] == a['name']:
3512+ found = True
3513+ ret = self._validate_dict_data(e, a)
3514+ if ret:
3515+ return "unexpected tenant data - {}".format(ret)
3516+ if not found:
3517+ return "tenant {} does not exist".format(e['name'])
3518+ return ret
3519+
3520+ def validate_role_data(self, expected, actual):
3521+ """Validate role data.
3522+
3523+ Validate a list of actual role data vs a list of expected role
3524+ data.
3525+ """
3526+ self.log.debug('Validating role data...')
3527+ self.log.debug('actual: {}'.format(repr(actual)))
3528+ for e in expected:
3529+ found = False
3530+ for act in actual:
3531+ a = {'name': act.name, 'id': act.id}
3532+ if e['name'] == a['name']:
3533+ found = True
3534+ ret = self._validate_dict_data(e, a)
3535+ if ret:
3536+ return "unexpected role data - {}".format(ret)
3537+ if not found:
3538+ return "role {} does not exist".format(e['name'])
3539+ return ret
3540+
3541+ def validate_user_data(self, expected, actual):
3542+ """Validate user data.
3543+
3544+ Validate a list of actual user data vs a list of expected user
3545+ data.
3546+ """
3547+ self.log.debug('Validating user data...')
3548+ self.log.debug('actual: {}'.format(repr(actual)))
3549+ for e in expected:
3550+ found = False
3551+ for act in actual:
3552+ a = {'enabled': act.enabled, 'name': act.name,
3553+ 'email': act.email, 'tenantId': act.tenantId,
3554+ 'id': act.id}
3555+ if e['name'] == a['name']:
3556+ found = True
3557+ ret = self._validate_dict_data(e, a)
3558+ if ret:
3559+ return "unexpected user data - {}".format(ret)
3560+ if not found:
3561+ return "user {} does not exist".format(e['name'])
3562+ return ret
3563+
3564+ def validate_flavor_data(self, expected, actual):
3565+ """Validate flavor data.
3566+
3567+ Validate a list of actual flavors vs a list of expected flavors.
3568+ """
3569+ self.log.debug('Validating flavor data...')
3570+ self.log.debug('actual: {}'.format(repr(actual)))
3571+ act = [a.name for a in actual]
3572+ return self._validate_list_data(expected, act)
3573+
3574+ def tenant_exists(self, keystone, tenant):
3575+ """Return True if tenant exists."""
3576+ self.log.debug('Checking if tenant exists ({})...'.format(tenant))
3577+ return tenant in [t.name for t in keystone.tenants.list()]
3578+
3579+ def authenticate_cinder_admin(self, keystone_sentry, username,
3580+ password, tenant):
3581+ """Authenticates admin user with cinder."""
3582+ # NOTE(beisner): cinder python client doesn't accept tokens.
3583+ service_ip = \
3584+ keystone_sentry.relation('shared-db',
3585+ 'mysql:shared-db')['private-address']
3586+ ept = "http://{}:5000/v2.0".format(service_ip.strip().decode('utf-8'))
3587+ return cinder_client.Client(username, password, tenant, ept)
3588+
3589+ def authenticate_keystone_admin(self, keystone_sentry, user, password,
3590+ tenant):
3591+ """Authenticates admin user with the keystone admin endpoint."""
3592+ self.log.debug('Authenticating keystone admin...')
3593+ unit = keystone_sentry
3594+ service_ip = unit.relation('shared-db',
3595+ 'mysql:shared-db')['private-address']
3596+ ep = "http://{}:35357/v2.0".format(service_ip.strip().decode('utf-8'))
3597+ return keystone_client.Client(username=user, password=password,
3598+ tenant_name=tenant, auth_url=ep)
3599+
3600+ def authenticate_keystone_user(self, keystone, user, password, tenant):
3601+ """Authenticates a regular user with the keystone public endpoint."""
3602+ self.log.debug('Authenticating keystone user ({})...'.format(user))
3603+ ep = keystone.service_catalog.url_for(service_type='identity',
3604+ endpoint_type='publicURL')
3605+ return keystone_client.Client(username=user, password=password,
3606+ tenant_name=tenant, auth_url=ep)
3607+
3608+ def authenticate_glance_admin(self, keystone):
3609+ """Authenticates admin user with glance."""
3610+ self.log.debug('Authenticating glance admin...')
3611+ ep = keystone.service_catalog.url_for(service_type='image',
3612+ endpoint_type='adminURL')
3613+ return glance_client.Client(ep, token=keystone.auth_token)
3614+
3615+ def authenticate_heat_admin(self, keystone):
3616+ """Authenticates the admin user with heat."""
3617+ self.log.debug('Authenticating heat admin...')
3618+ ep = keystone.service_catalog.url_for(service_type='orchestration',
3619+ endpoint_type='publicURL')
3620+ return heat_client.Client(endpoint=ep, token=keystone.auth_token)
3621+
3622+ def authenticate_nova_user(self, keystone, user, password, tenant):
3623+ """Authenticates a regular user with nova-api."""
3624+ self.log.debug('Authenticating nova user ({})...'.format(user))
3625+ ep = keystone.service_catalog.url_for(service_type='identity',
3626+ endpoint_type='publicURL')
3627+ return nova_client.Client(username=user, api_key=password,
3628+ project_id=tenant, auth_url=ep)
3629+
3630+ def authenticate_swift_user(self, keystone, user, password, tenant):
3631+ """Authenticates a regular user with swift api."""
3632+ self.log.debug('Authenticating swift user ({})...'.format(user))
3633+ ep = keystone.service_catalog.url_for(service_type='identity',
3634+ endpoint_type='publicURL')
3635+ return swiftclient.Connection(authurl=ep,
3636+ user=user,
3637+ key=password,
3638+ tenant_name=tenant,
3639+ auth_version='2.0')
3640+
3641+ def create_cirros_image(self, glance, image_name):
3642+ """Download the latest cirros image and upload it to glance,
3643+ validate and return a resource pointer.
3644+
3645+ :param glance: pointer to authenticated glance connection
3646+ :param image_name: display name for new image
3647+ :returns: glance image pointer
3648+ """
3649+ self.log.debug('Creating glance cirros image '
3650+ '({})...'.format(image_name))
3651+
3652+ # Download cirros image
3653+ http_proxy = os.getenv('AMULET_HTTP_PROXY')
3654+ self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
3655+ if http_proxy:
3656+ proxies = {'http': http_proxy}
3657+ opener = urllib.FancyURLopener(proxies)
3658+ else:
3659+ opener = urllib.FancyURLopener()
3660+
3661+ f = opener.open('http://download.cirros-cloud.net/version/released')
3662+ version = f.read().strip()
3663+ cirros_img = 'cirros-{}-x86_64-disk.img'.format(version)
3664+ local_path = os.path.join('tests', cirros_img)
3665+
3666+ if not os.path.exists(local_path):
3667+ cirros_url = 'http://{}/{}/{}'.format('download.cirros-cloud.net',
3668+ version, cirros_img)
3669+ opener.retrieve(cirros_url, local_path)
3670+ f.close()
3671+
3672+ # Create glance image
3673+ with open(local_path) as f:
3674+ image = glance.images.create(name=image_name, is_public=True,
3675+ disk_format='qcow2',
3676+ container_format='bare', data=f)
3677+
3678+ # Wait for image to reach active status
3679+ img_id = image.id
3680+ ret = self.resource_reaches_status(glance.images, img_id,
3681+ expected_stat='active',
3682+ msg='Image status wait')
3683+ if not ret:
3684+ msg = 'Glance image failed to reach expected state.'
3685+ amulet.raise_status(amulet.FAIL, msg=msg)
3686+
3687+ # Re-validate new image
3688+ self.log.debug('Validating image attributes...')
3689+ val_img_name = glance.images.get(img_id).name
3690+ val_img_stat = glance.images.get(img_id).status
3691+ val_img_pub = glance.images.get(img_id).is_public
3692+ val_img_cfmt = glance.images.get(img_id).container_format
3693+ val_img_dfmt = glance.images.get(img_id).disk_format
3694+ msg_attr = ('Image attributes - name:{} public:{} id:{} stat:{} '
3695+ 'container fmt:{} disk fmt:{}'.format(
3696+ val_img_name, val_img_pub, img_id,
3697+ val_img_stat, val_img_cfmt, val_img_dfmt))
3698+
3699+ if val_img_name == image_name and val_img_stat == 'active' \
3700+ and val_img_pub is True and val_img_cfmt == 'bare' \
3701+ and val_img_dfmt == 'qcow2':
3702+ self.log.debug(msg_attr)
3703+ else:
3704+ msg = ('Volume validation failed, {}'.format(msg_attr))
3705+ amulet.raise_status(amulet.FAIL, msg=msg)
3706+
3707+ return image
3708+
3709+ def delete_image(self, glance, image):
3710+ """Delete the specified image."""
3711+
3712+ # /!\ DEPRECATION WARNING
3713+ self.log.warn('/!\\ DEPRECATION WARNING: use '
3714+ 'delete_resource instead of delete_image.')
3715+ self.log.debug('Deleting glance image ({})...'.format(image))
3716+ return self.delete_resource(glance.images, image, msg='glance image')
3717+
3718+ def create_instance(self, nova, image_name, instance_name, flavor):
3719+ """Create the specified instance."""
3720+ self.log.debug('Creating instance '
3721+ '({}|{}|{})'.format(instance_name, image_name, flavor))
3722+ image = nova.images.find(name=image_name)
3723+ flavor = nova.flavors.find(name=flavor)
3724+ instance = nova.servers.create(name=instance_name, image=image,
3725+ flavor=flavor)
3726+
3727+ count = 1
3728+ status = instance.status
3729+ while status != 'ACTIVE' and count < 60:
3730+ time.sleep(3)
3731+ instance = nova.servers.get(instance.id)
3732+ status = instance.status
3733+ self.log.debug('instance status: {}'.format(status))
3734+ count += 1
3735+
3736+ if status != 'ACTIVE':
3737+ self.log.error('instance creation timed out')
3738+ return None
3739+
3740+ return instance
3741+
3742+ def delete_instance(self, nova, instance):
3743+ """Delete the specified instance."""
3744+
3745+ # /!\ DEPRECATION WARNING
3746+ self.log.warn('/!\\ DEPRECATION WARNING: use '
3747+ 'delete_resource instead of delete_instance.')
3748+ self.log.debug('Deleting instance ({})...'.format(instance))
3749+ return self.delete_resource(nova.servers, instance,
3750+ msg='nova instance')
3751+
3752+ def create_or_get_keypair(self, nova, keypair_name="testkey"):
3753+ """Create a new keypair, or return pointer if it already exists."""
3754+ try:
3755+ _keypair = nova.keypairs.get(keypair_name)
3756+ self.log.debug('Keypair ({}) already exists, '
3757+ 'using it.'.format(keypair_name))
3758+ return _keypair
3759+ except:
3760+ self.log.debug('Keypair ({}) does not exist, '
3761+ 'creating it.'.format(keypair_name))
3762+
3763+ _keypair = nova.keypairs.create(name=keypair_name)
3764+ return _keypair
3765+
3766+ def create_cinder_volume(self, cinder, vol_name="demo-vol", vol_size=1,
3767+ img_id=None, src_vol_id=None, snap_id=None):
3768+ """Create cinder volume, optionally from a glance image, OR
3769+ optionally as a clone of an existing volume, OR optionally
3770+ from a snapshot. Wait for the new volume status to reach
3771+ the expected status, validate and return a resource pointer.
3772+
3773+ :param vol_name: cinder volume display name
3774+ :param vol_size: size in gigabytes
3775+ :param img_id: optional glance image id
3776+ :param src_vol_id: optional source volume id to clone
3777+ :param snap_id: optional snapshot id to use
3778+ :returns: cinder volume pointer
3779+ """
3780+ # Handle parameter input and avoid impossible combinations
3781+ if img_id and not src_vol_id and not snap_id:
3782+ # Create volume from image
3783+ self.log.debug('Creating cinder volume from glance image...')
3784+ bootable = 'true'
3785+ elif src_vol_id and not img_id and not snap_id:
3786+ # Clone an existing volume
3787+ self.log.debug('Cloning cinder volume...')
3788+ bootable = cinder.volumes.get(src_vol_id).bootable
3789+ elif snap_id and not src_vol_id and not img_id:
3790+ # Create volume from snapshot
3791+ self.log.debug('Creating cinder volume from snapshot...')
3792+ snap = cinder.volume_snapshots.find(id=snap_id)
3793+ vol_size = snap.size
3794+ snap_vol_id = cinder.volume_snapshots.get(snap_id).volume_id
3795+ bootable = cinder.volumes.get(snap_vol_id).bootable
3796+ elif not img_id and not src_vol_id and not snap_id:
3797+ # Create volume
3798+ self.log.debug('Creating cinder volume...')
3799+ bootable = 'false'
3800+ else:
3801+ # Impossible combination of parameters
3802+ msg = ('Invalid method use - name:{} size:{} img_id:{} '
3803+ 'src_vol_id:{} snap_id:{}'.format(vol_name, vol_size,
3804+ img_id, src_vol_id,
3805+ snap_id))
3806+ amulet.raise_status(amulet.FAIL, msg=msg)
3807+
3808+ # Create new volume
3809+ try:
3810+ vol_new = cinder.volumes.create(display_name=vol_name,
3811+ imageRef=img_id,
3812+ size=vol_size,
3813+ source_volid=src_vol_id,
3814+ snapshot_id=snap_id)
3815+ vol_id = vol_new.id
3816+ except Exception as e:
3817+ msg = 'Failed to create volume: {}'.format(e)
3818+ amulet.raise_status(amulet.FAIL, msg=msg)
3819+
3820+ # Wait for volume to reach available status
3821+ ret = self.resource_reaches_status(cinder.volumes, vol_id,
3822+ expected_stat="available",
3823+ msg="Volume status wait")
3824+ if not ret:
3825+ msg = 'Cinder volume failed to reach expected state.'
3826+ amulet.raise_status(amulet.FAIL, msg=msg)
3827+
3828+ # Re-validate new volume
3829+ self.log.debug('Validating volume attributes...')
3830+ val_vol_name = cinder.volumes.get(vol_id).display_name
3831+ val_vol_boot = cinder.volumes.get(vol_id).bootable
3832+ val_vol_stat = cinder.volumes.get(vol_id).status
3833+ val_vol_size = cinder.volumes.get(vol_id).size
3834+ msg_attr = ('Volume attributes - name:{} id:{} stat:{} boot:'
3835+ '{} size:{}'.format(val_vol_name, vol_id,
3836+ val_vol_stat, val_vol_boot,
3837+ val_vol_size))
3838+
3839+ if val_vol_boot == bootable and val_vol_stat == 'available' \
3840+ and val_vol_name == vol_name and val_vol_size == vol_size:
3841+ self.log.debug(msg_attr)
3842+ else:
3843+ msg = ('Volume validation failed, {}'.format(msg_attr))
3844+ amulet.raise_status(amulet.FAIL, msg=msg)
3845+
3846+ return vol_new
3847+
3848+ def delete_resource(self, resource, resource_id,
3849+ msg="resource", max_wait=120):
3850+ """Delete one openstack resource, such as one instance, keypair,
3851+ image, volume, stack, etc., and confirm deletion within max wait time.
3852+
3853+ :param resource: pointer to os resource type, ex:glance_client.images
3854+ :param resource_id: unique name or id for the openstack resource
3855+ :param msg: text to identify purpose in logging
3856+ :param max_wait: maximum wait time in seconds
3857+ :returns: True if successful, otherwise False
3858+ """
3859+ self.log.debug('Deleting OpenStack resource '
3860+ '{} ({})'.format(resource_id, msg))
3861+ num_before = len(list(resource.list()))
3862+ resource.delete(resource_id)
3863+
3864+ tries = 0
3865+ num_after = len(list(resource.list()))
3866+ while num_after != (num_before - 1) and tries < (max_wait / 4):
3867+ self.log.debug('{} delete check: '
3868+ '{} [{}:{}] {}'.format(msg, tries,
3869+ num_before,
3870+ num_after,
3871+ resource_id))
3872+ time.sleep(4)
3873+ num_after = len(list(resource.list()))
3874+ tries += 1
3875+
3876+ self.log.debug('{}: expected, actual count = {}, '
3877+ '{}'.format(msg, num_before - 1, num_after))
3878+
3879+ if num_after == (num_before - 1):
3880+ return True
3881+ else:
3882+ self.log.error('{} delete timed out'.format(msg))
3883+ return False
3884+
3885+ def resource_reaches_status(self, resource, resource_id,
3886+ expected_stat='available',
3887+ msg='resource', max_wait=120):
3888+ """Wait for an openstack resources status to reach an
3889+ expected status within a specified time. Useful to confirm that
3890+ nova instances, cinder vols, snapshots, glance images, heat stacks
3891+ and other resources eventually reach the expected status.
3892+
3893+ :param resource: pointer to os resource type, ex: heat_client.stacks
3894+ :param resource_id: unique id for the openstack resource
3895+ :param expected_stat: status to expect resource to reach
3896+ :param msg: text to identify purpose in logging
3897+ :param max_wait: maximum wait time in seconds
3898+ :returns: True if successful, False if status is not reached
3899+ """
3900+
3901+ tries = 0
3902+ resource_stat = resource.get(resource_id).status
3903+ while resource_stat != expected_stat and tries < (max_wait / 4):
3904+ self.log.debug('{} status check: '
3905+ '{} [{}:{}] {}'.format(msg, tries,
3906+ resource_stat,
3907+ expected_stat,
3908+ resource_id))
3909+ time.sleep(4)
3910+ resource_stat = resource.get(resource_id).status
3911+ tries += 1
3912+
3913+ self.log.debug('{}: expected, actual status = {}, '
3914+ '{}'.format(msg, resource_stat, expected_stat))
3915+
3916+ if resource_stat == expected_stat:
3917+ return True
3918+ else:
3919+ self.log.debug('{} never reached expected status: '
3920+ '{}'.format(resource_id, expected_stat))
3921+ return False
3922+
3923+ def get_ceph_osd_id_cmd(self, index):
3924+ """Produce a shell command that will return a ceph-osd id."""
3925+ return ("`initctl list | grep 'ceph-osd ' | "
3926+ "awk 'NR=={} {{ print $2 }}' | "
3927+ "grep -o '[0-9]*'`".format(index + 1))
3928+
3929+ def get_ceph_pools(self, sentry_unit):
3930+ """Return a dict of ceph pools from a single ceph unit, with
3931+ pool name as keys, pool id as vals."""
3932+ pools = {}
3933+ cmd = 'sudo ceph osd lspools'
3934+ output, code = sentry_unit.run(cmd)
3935+ if code != 0:
3936+ msg = ('{} `{}` returned {} '
3937+ '{}'.format(sentry_unit.info['unit_name'],
3938+ cmd, code, output))
3939+ amulet.raise_status(amulet.FAIL, msg=msg)
3940+
3941+ # Example output: 0 data,1 metadata,2 rbd,3 cinder,4 glance,
3942+ for pool in str(output).split(','):
3943+ pool_id_name = pool.split(' ')
3944+ if len(pool_id_name) == 2:
3945+ pool_id = pool_id_name[0]
3946+ pool_name = pool_id_name[1]
3947+ pools[pool_name] = int(pool_id)
3948+
3949+ self.log.debug('Pools on {}: {}'.format(sentry_unit.info['unit_name'],
3950+ pools))
3951+ return pools
3952+
3953+ def get_ceph_df(self, sentry_unit):
3954+ """Return dict of ceph df json output, including ceph pool state.
3955+
3956+ :param sentry_unit: Pointer to amulet sentry instance (juju unit)
3957+ :returns: Dict of ceph df output
3958+ """
3959+ cmd = 'sudo ceph df --format=json'
3960+ output, code = sentry_unit.run(cmd)
3961+ if code != 0:
3962+ msg = ('{} `{}` returned {} '
3963+ '{}'.format(sentry_unit.info['unit_name'],
3964+ cmd, code, output))
3965+ amulet.raise_status(amulet.FAIL, msg=msg)
3966+ return json.loads(output)
3967+
3968+ def get_ceph_pool_sample(self, sentry_unit, pool_id=0):
3969+ """Take a sample of attributes of a ceph pool, returning ceph
3970+ pool name, object count and disk space used for the specified
3971+ pool ID number.
3972+
3973+ :param sentry_unit: Pointer to amulet sentry instance (juju unit)
3974+ :param pool_id: Ceph pool ID
3975+ :returns: List of pool name, object count, kb disk space used
3976+ """
3977+ df = self.get_ceph_df(sentry_unit)
3978+ pool_name = df['pools'][pool_id]['name']
3979+ obj_count = df['pools'][pool_id]['stats']['objects']
3980+ kb_used = df['pools'][pool_id]['stats']['kb_used']
3981+ self.log.debug('Ceph {} pool (ID {}): {} objects, '
3982+ '{} kb used'.format(pool_name, pool_id,
3983+ obj_count, kb_used))
3984+ return pool_name, obj_count, kb_used
3985+
3986+ def validate_ceph_pool_samples(self, samples, sample_type="resource pool"):
3987+ """Validate ceph pool samples taken over time, such as pool
3988+ object counts or pool kb used, before adding, after adding, and
3989+ after deleting items which affect those pool attributes. The
3990+ 2nd element is expected to be greater than the 1st; 3rd is expected
3991+ to be less than the 2nd.
3992+
3993+ :param samples: List containing 3 data samples
3994+ :param sample_type: String for logging and usage context
3995+ :returns: None if successful, Failure message otherwise
3996+ """
3997+ original, created, deleted = range(3)
3998+ if samples[created] <= samples[original] or \
3999+ samples[deleted] >= samples[created]:
4000+ return ('Ceph {} samples ({}) '
4001+ 'unexpected.'.format(sample_type, samples))
4002+ else:
4003+ self.log.debug('Ceph {} samples (OK): '
4004+ '{}'.format(sample_type, samples))
4005+ return None
4006+
4007+# rabbitmq/amqp specific helpers:
4008+ def add_rmq_test_user(self, sentry_units,
4009+ username="testuser1", password="changeme"):
4010+ """Add a test user via the first rmq juju unit, check connection as
4011+ the new user against all sentry units.
4012+
4013+ :param sentry_units: list of sentry unit pointers
4014+ :param username: amqp user name, default to testuser1
4015+ :param password: amqp user password
4016+ :returns: None if successful. Raise on error.
4017+ """
4018+ self.log.debug('Adding rmq user ({})...'.format(username))
4019+
4020+ # Check that user does not already exist
4021+ cmd_user_list = 'rabbitmqctl list_users'
4022+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
4023+ if username in output:
4024+ self.log.warning('User ({}) already exists, returning '
4025+ 'gracefully.'.format(username))
4026+ return
4027+
4028+ perms = '".*" ".*" ".*"'
4029+ cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
4030+ 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
4031+
4032+ # Add user via first unit
4033+ for cmd in cmds:
4034+ output, _ = self.run_cmd_unit(sentry_units[0], cmd)
4035+
4036+ # Check connection against the other sentry_units
4037+ self.log.debug('Checking user connect against units...')
4038+ for sentry_unit in sentry_units:
4039+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
4040+ username=username,
4041+ password=password)
4042+ connection.close()
4043+
4044+ def delete_rmq_test_user(self, sentry_units, username="testuser1"):
4045+ """Delete a rabbitmq user via the first rmq juju unit.
4046+
4047+ :param sentry_units: list of sentry unit pointers
4048+ :param username: amqp user name, default to testuser1
4049+ :param password: amqp user password
4050+ :returns: None if successful or no such user.
4051+ """
4052+ self.log.debug('Deleting rmq user ({})...'.format(username))
4053+
4054+ # Check that the user exists
4055+ cmd_user_list = 'rabbitmqctl list_users'
4056+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
4057+
4058+ if username not in output:
4059+ self.log.warning('User ({}) does not exist, returning '
4060+ 'gracefully.'.format(username))
4061+ return
4062+
4063+ # Delete the user
4064+ cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
4065+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
4066+
4067+ def get_rmq_cluster_status(self, sentry_unit):
4068+ """Execute rabbitmq cluster status command on a unit and return
4069+ the full output.
4070+
4071+ :param unit: sentry unit
4072+ :returns: String containing console output of cluster status command
4073+ """
4074+ cmd = 'rabbitmqctl cluster_status'
4075+ output, _ = self.run_cmd_unit(sentry_unit, cmd)
4076+ self.log.debug('{} cluster_status:\n{}'.format(
4077+ sentry_unit.info['unit_name'], output))
4078+ return str(output)
4079+
4080+ def get_rmq_cluster_running_nodes(self, sentry_unit):
4081+ """Parse rabbitmqctl cluster_status output string, return list of
4082+ running rabbitmq cluster nodes.
4083+
4084+ :param unit: sentry unit
4085+ :returns: List containing node names of running nodes
4086+ """
4087+ # NOTE(beisner): rabbitmqctl cluster_status output is not
4088+ # json-parsable, do string chop foo, then json.loads that.
4089+ str_stat = self.get_rmq_cluster_status(sentry_unit)
4090+ if 'running_nodes' in str_stat:
4091+ pos_start = str_stat.find("{running_nodes,") + 15
4092+ pos_end = str_stat.find("]},", pos_start) + 1
4093+ str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"')
4094+ run_nodes = json.loads(str_run_nodes)
4095+ return run_nodes
4096+ else:
4097+ return []
4098+
4099+ def validate_rmq_cluster_running_nodes(self, sentry_units):
4100+ """Check that all rmq unit hostnames are represented in the
4101+ cluster_status output of all units.
4102+
4103+ :param host_names: dict of juju unit names to host names
4104+ :param units: list of sentry unit pointers (all rmq units)
4105+ :returns: None if successful, otherwise return error message
4106+ """
4107+ host_names = self.get_unit_hostnames(sentry_units)
4108+ errors = []
4109+
4110+ # Query every unit for cluster_status running nodes
4111+ for query_unit in sentry_units:
4112+ query_unit_name = query_unit.info['unit_name']
4113+ running_nodes = self.get_rmq_cluster_running_nodes(query_unit)
4114+
4115+ # Confirm that every unit is represented in the queried unit's
4116+ # cluster_status running nodes output.
4117+ for validate_unit in sentry_units:
4118+ val_host_name = host_names[validate_unit.info['unit_name']]
4119+ val_node_name = 'rabbit@{}'.format(val_host_name)
4120+
4121+ if val_node_name not in running_nodes:
4122+ errors.append('Cluster member check failed on {}: {} not '
4123+ 'in {}\n'.format(query_unit_name,
4124+ val_node_name,
4125+ running_nodes))
4126+ if errors:
4127+ return ''.join(errors)
4128+
4129+ def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None):
4130+ """Check a single juju rmq unit for ssl and port in the config file."""
4131+ host = sentry_unit.info['public-address']
4132+ unit_name = sentry_unit.info['unit_name']
4133+
4134+ conf_file = '/etc/rabbitmq/rabbitmq.config'
4135+ conf_contents = str(self.file_contents_safe(sentry_unit,
4136+ conf_file, max_wait=16))
4137+ # Checks
4138+ conf_ssl = 'ssl' in conf_contents
4139+ conf_port = str(port) in conf_contents
4140+
4141+ # Port explicitly checked in config
4142+ if port and conf_port and conf_ssl:
4143+ self.log.debug('SSL is enabled @{}:{} '
4144+ '({})'.format(host, port, unit_name))
4145+ return True
4146+ elif port and not conf_port and conf_ssl:
4147+ self.log.debug('SSL is enabled @{} but not on port {} '
4148+ '({})'.format(host, port, unit_name))
4149+ return False
4150+ # Port not checked (useful when checking that ssl is disabled)
4151+ elif not port and conf_ssl:
4152+ self.log.debug('SSL is enabled @{}:{} '
4153+ '({})'.format(host, port, unit_name))
4154+ return True
4155+ elif not conf_ssl:
4156+ self.log.debug('SSL not enabled @{}:{} '
4157+ '({})'.format(host, port, unit_name))
4158+ return False
4159+ else:
4160+ msg = ('Unknown condition when checking SSL status @{}:{} '
4161+ '({})'.format(host, port, unit_name))
4162+ amulet.raise_status(amulet.FAIL, msg)
4163+
4164+ def validate_rmq_ssl_enabled_units(self, sentry_units, port=None):
4165+ """Check that ssl is enabled on rmq juju sentry units.
4166+
4167+ :param sentry_units: list of all rmq sentry units
4168+ :param port: optional ssl port override to validate
4169+ :returns: None if successful, otherwise return error message
4170+ """
4171+ for sentry_unit in sentry_units:
4172+ if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port):
4173+ return ('Unexpected condition: ssl is disabled on unit '
4174+ '({})'.format(sentry_unit.info['unit_name']))
4175+ return None
4176+
4177+ def validate_rmq_ssl_disabled_units(self, sentry_units):
4178+ """Check that ssl is enabled on listed rmq juju sentry units.
4179+
4180+ :param sentry_units: list of all rmq sentry units
4181+ :returns: True if successful. Raise on error.
4182+ """
4183+ for sentry_unit in sentry_units:
4184+ if self.rmq_ssl_is_enabled_on_unit(sentry_unit):
4185+ return ('Unexpected condition: ssl is enabled on unit '
4186+ '({})'.format(sentry_unit.info['unit_name']))
4187+ return None
4188+
4189+ def configure_rmq_ssl_on(self, sentry_units, deployment,
4190+ port=None, max_wait=60):
4191+ """Turn ssl charm config option on, with optional non-default
4192+ ssl port specification. Confirm that it is enabled on every
4193+ unit.
4194+
4195+ :param sentry_units: list of sentry units
4196+ :param deployment: amulet deployment object pointer
4197+ :param port: amqp port, use defaults if None
4198+ :param max_wait: maximum time to wait in seconds to confirm
4199+ :returns: None if successful. Raise on error.
4200+ """
4201+ self.log.debug('Setting ssl charm config option: on')
4202+
4203+ # Enable RMQ SSL
4204+ config = {'ssl': 'on'}
4205+ if port:
4206+ config['ssl_port'] = port
4207+
4208+ deployment.configure('rabbitmq-server', config)
4209+
4210+ # Confirm
4211+ tries = 0
4212+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
4213+ while ret and tries < (max_wait / 4):
4214+ time.sleep(4)
4215+ self.log.debug('Attempt {}: {}'.format(tries, ret))
4216+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
4217+ tries += 1
4218+
4219+ if ret:
4220+ amulet.raise_status(amulet.FAIL, ret)
4221+
4222+ def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60):
4223+ """Turn ssl charm config option off, confirm that it is disabled
4224+ on every unit.
4225+
4226+ :param sentry_units: list of sentry units
4227+ :param deployment: amulet deployment object pointer
4228+ :param max_wait: maximum time to wait in seconds to confirm
4229+ :returns: None if successful. Raise on error.
4230+ """
4231+ self.log.debug('Setting ssl charm config option: off')
4232+
4233+ # Disable RMQ SSL
4234+ config = {'ssl': 'off'}
4235+ deployment.configure('rabbitmq-server', config)
4236+
4237+ # Confirm
4238+ tries = 0
4239+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
4240+ while ret and tries < (max_wait / 4):
4241+ time.sleep(4)
4242+ self.log.debug('Attempt {}: {}'.format(tries, ret))
4243+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
4244+ tries += 1
4245+
4246+ if ret:
4247+ amulet.raise_status(amulet.FAIL, ret)
4248+
4249+ def connect_amqp_by_unit(self, sentry_unit, ssl=False,
4250+ port=None, fatal=True,
4251+ username="testuser1", password="changeme"):
4252+ """Establish and return a pika amqp connection to the rabbitmq service
4253+ running on a rmq juju unit.
4254+
4255+ :param sentry_unit: sentry unit pointer
4256+ :param ssl: boolean, default to False
4257+ :param port: amqp port, use defaults if None
4258+ :param fatal: boolean, default to True (raises on connect error)
4259+ :param username: amqp user name, default to testuser1
4260+ :param password: amqp user password
4261+ :returns: pika amqp connection pointer or None if failed and non-fatal
4262+ """
4263+ host = sentry_unit.info['public-address']
4264+ unit_name = sentry_unit.info['unit_name']
4265+
4266+ # Default port logic if port is not specified
4267+ if ssl and not port:
4268+ port = 5671
4269+ elif not ssl and not port:
4270+ port = 5672
4271+
4272+ self.log.debug('Connecting to amqp on {}:{} ({}) as '
4273+ '{}...'.format(host, port, unit_name, username))
4274+
4275+ try:
4276+ credentials = pika.PlainCredentials(username, password)
4277+ parameters = pika.ConnectionParameters(host=host, port=port,
4278+ credentials=credentials,
4279+ ssl=ssl,
4280+ connection_attempts=3,
4281+ retry_delay=5,
4282+ socket_timeout=1)
4283+ connection = pika.BlockingConnection(parameters)
4284+ assert connection.server_properties['product'] == 'RabbitMQ'
4285+ self.log.debug('Connect OK')
4286+ return connection
4287+ except Exception as e:
4288+ msg = ('amqp connection failed to {}:{} as '
4289+ '{} ({})'.format(host, port, username, str(e)))
4290+ if fatal:
4291+ amulet.raise_status(amulet.FAIL, msg)
4292+ else:
4293+ self.log.warn(msg)
4294+ return None
4295+
4296+ def publish_amqp_message_by_unit(self, sentry_unit, message,
4297+ queue="test", ssl=False,
4298+ username="testuser1",
4299+ password="changeme",
4300+ port=None):
4301+ """Publish an amqp message to a rmq juju unit.
4302+
4303+ :param sentry_unit: sentry unit pointer
4304+ :param message: amqp message string
4305+ :param queue: message queue, default to test
4306+ :param username: amqp user name, default to testuser1
4307+ :param password: amqp user password
4308+ :param ssl: boolean, default to False
4309+ :param port: amqp port, use defaults if None
4310+ :returns: None. Raises exception if publish failed.
4311+ """
4312+ self.log.debug('Publishing message to {} queue:\n{}'.format(queue,
4313+ message))
4314+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
4315+ port=port,
4316+ username=username,
4317+ password=password)
4318+
4319+ # NOTE(beisner): extra debug here re: pika hang potential:
4320+ # https://github.com/pika/pika/issues/297
4321+ # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw
4322+ self.log.debug('Defining channel...')
4323+ channel = connection.channel()
4324+ self.log.debug('Declaring queue...')
4325+ channel.queue_declare(queue=queue, auto_delete=False, durable=True)
4326+ self.log.debug('Publishing message...')
4327+ channel.basic_publish(exchange='', routing_key=queue, body=message)
4328+ self.log.debug('Closing channel...')
4329+ channel.close()
4330+ self.log.debug('Closing connection...')
4331+ connection.close()
4332+
4333+ def get_amqp_message_by_unit(self, sentry_unit, queue="test",
4334+ username="testuser1",
4335+ password="changeme",
4336+ ssl=False, port=None):
4337+ """Get an amqp message from a rmq juju unit.
4338+
4339+ :param sentry_unit: sentry unit pointer
4340+ :param queue: message queue, default to test
4341+ :param username: amqp user name, default to testuser1
4342+ :param password: amqp user password
4343+ :param ssl: boolean, default to False
4344+ :param port: amqp port, use defaults if None
4345+ :returns: amqp message body as string. Raise if get fails.
4346+ """
4347+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
4348+ port=port,
4349+ username=username,
4350+ password=password)
4351+ channel = connection.channel()
4352+ method_frame, _, body = channel.basic_get(queue)
4353+
4354+ if method_frame:
4355+ self.log.debug('Retreived message from {} queue:\n{}'.format(queue,
4356+ body))
4357+ channel.basic_ack(method_frame.delivery_tag)
4358+ channel.close()
4359+ connection.close()
4360+ return body
4361+ else:
4362+ msg = 'No message retrieved.'
4363+ amulet.raise_status(amulet.FAIL, msg)
4364
4365=== removed directory 'tests/charmhelpers/contrib/ssl'
4366=== removed file 'tests/charmhelpers/contrib/ssl/__init__.py'
4367--- tests/charmhelpers/contrib/ssl/__init__.py 2015-04-13 22:11:34 +0000
4368+++ tests/charmhelpers/contrib/ssl/__init__.py 1970-01-01 00:00:00 +0000
4369@@ -1,94 +0,0 @@
4370-# Copyright 2014-2015 Canonical Limited.
4371-#
4372-# This file is part of charm-helpers.
4373-#
4374-# charm-helpers is free software: you can redistribute it and/or modify
4375-# it under the terms of the GNU Lesser General Public License version 3 as
4376-# published by the Free Software Foundation.
4377-#
4378-# charm-helpers is distributed in the hope that it will be useful,
4379-# but WITHOUT ANY WARRANTY; without even the implied warranty of
4380-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4381-# GNU Lesser General Public License for more details.
4382-#
4383-# You should have received a copy of the GNU Lesser General Public License
4384-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4385-
4386-import subprocess
4387-from charmhelpers.core import hookenv
4388-
4389-
4390-def generate_selfsigned(keyfile, certfile, keysize="1024", config=None, subject=None, cn=None):
4391- """Generate selfsigned SSL keypair
4392-
4393- You must provide one of the 3 optional arguments:
4394- config, subject or cn
4395- If more than one is provided the leftmost will be used
4396-
4397- Arguments:
4398- keyfile -- (required) full path to the keyfile to be created
4399- certfile -- (required) full path to the certfile to be created
4400- keysize -- (optional) SSL key length
4401- config -- (optional) openssl configuration file
4402- subject -- (optional) dictionary with SSL subject variables
4403- cn -- (optional) cerfificate common name
4404-
4405- Required keys in subject dict:
4406- cn -- Common name (eq. FQDN)
4407-
4408- Optional keys in subject dict
4409- country -- Country Name (2 letter code)
4410- state -- State or Province Name (full name)
4411- locality -- Locality Name (eg, city)
4412- organization -- Organization Name (eg, company)
4413- organizational_unit -- Organizational Unit Name (eg, section)
4414- email -- Email Address
4415- """
4416-
4417- cmd = []
4418- if config:
4419- cmd = ["/usr/bin/openssl", "req", "-new", "-newkey",
4420- "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509",
4421- "-keyout", keyfile,
4422- "-out", certfile, "-config", config]
4423- elif subject:
4424- ssl_subject = ""
4425- if "country" in subject:
4426- ssl_subject = ssl_subject + "/C={}".format(subject["country"])
4427- if "state" in subject:
4428- ssl_subject = ssl_subject + "/ST={}".format(subject["state"])
4429- if "locality" in subject:
4430- ssl_subject = ssl_subject + "/L={}".format(subject["locality"])
4431- if "organization" in subject:
4432- ssl_subject = ssl_subject + "/O={}".format(subject["organization"])
4433- if "organizational_unit" in subject:
4434- ssl_subject = ssl_subject + "/OU={}".format(subject["organizational_unit"])
4435- if "cn" in subject:
4436- ssl_subject = ssl_subject + "/CN={}".format(subject["cn"])
4437- else:
4438- hookenv.log("When using \"subject\" argument you must "
4439- "provide \"cn\" field at very least")
4440- return False
4441- if "email" in subject:
4442- ssl_subject = ssl_subject + "/emailAddress={}".format(subject["email"])
4443-
4444- cmd = ["/usr/bin/openssl", "req", "-new", "-newkey",
4445- "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509",
4446- "-keyout", keyfile,
4447- "-out", certfile, "-subj", ssl_subject]
4448- elif cn:
4449- cmd = ["/usr/bin/openssl", "req", "-new", "-newkey",
4450- "rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509",
4451- "-keyout", keyfile,
4452- "-out", certfile, "-subj", "/CN={}".format(cn)]
4453-
4454- if not cmd:
4455- hookenv.log("No config, subject or cn provided,"
4456- "unable to generate self signed SSL certificates")
4457- return False
4458- try:
4459- subprocess.check_call(cmd)
4460- return True
4461- except Exception as e:
4462- print("Execution of openssl command failed:\n{}".format(e))
4463- return False
4464
4465=== removed file 'tests/charmhelpers/contrib/ssl/service.py'
4466--- tests/charmhelpers/contrib/ssl/service.py 2015-04-16 21:35:24 +0000
4467+++ tests/charmhelpers/contrib/ssl/service.py 1970-01-01 00:00:00 +0000
4468@@ -1,279 +0,0 @@
4469-# Copyright 2014-2015 Canonical Limited.
4470-#
4471-# This file is part of charm-helpers.
4472-#
4473-# charm-helpers is free software: you can redistribute it and/or modify
4474-# it under the terms of the GNU Lesser General Public License version 3 as
4475-# published by the Free Software Foundation.
4476-#
4477-# charm-helpers is distributed in the hope that it will be useful,
4478-# but WITHOUT ANY WARRANTY; without even the implied warranty of
4479-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4480-# GNU Lesser General Public License for more details.
4481-#
4482-# You should have received a copy of the GNU Lesser General Public License
4483-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4484-
4485-import os
4486-from os.path import join as path_join
4487-from os.path import exists
4488-import subprocess
4489-
4490-from charmhelpers.core.hookenv import log, DEBUG
4491-
4492-STD_CERT = "standard"
4493-
4494-# Mysql server is fairly picky about cert creation
4495-# and types, spec its creation separately for now.
4496-MYSQL_CERT = "mysql"
4497-
4498-
4499-class ServiceCA(object):
4500-
4501- default_expiry = str(365 * 2)
4502- default_ca_expiry = str(365 * 6)
4503-
4504- def __init__(self, name, ca_dir, cert_type=STD_CERT):
4505- self.name = name
4506- self.ca_dir = ca_dir
4507- self.cert_type = cert_type
4508-
4509- ###############
4510- # Hook Helper API
4511- @staticmethod
4512- def get_ca(type=STD_CERT):
4513- service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
4514- ca_path = os.path.join(os.environ['CHARM_DIR'], 'ca')
4515- ca = ServiceCA(service_name, ca_path, type)
4516- ca.init()
4517- return ca
4518-
4519- @classmethod
4520- def get_service_cert(cls, type=STD_CERT):
4521- service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
4522- ca = cls.get_ca()
4523- crt, key = ca.get_or_create_cert(service_name)
4524- return crt, key, ca.get_ca_bundle()
4525-
4526- ###############
4527-
4528- def init(self):
4529- log("initializing service ca", level=DEBUG)
4530- if not exists(self.ca_dir):
4531- self._init_ca_dir(self.ca_dir)
4532- self._init_ca()
4533-
4534- @property
4535- def ca_key(self):
4536- return path_join(self.ca_dir, 'private', 'cacert.key')
4537-
4538- @property
4539- def ca_cert(self):
4540- return path_join(self.ca_dir, 'cacert.pem')
4541-
4542- @property
4543- def ca_conf(self):
4544- return path_join(self.ca_dir, 'ca.cnf')
4545-
4546- @property
4547- def signing_conf(self):
4548- return path_join(self.ca_dir, 'signing.cnf')
4549-
4550- def _init_ca_dir(self, ca_dir):
4551- os.mkdir(ca_dir)
4552- for i in ['certs', 'crl', 'newcerts', 'private']:
4553- sd = path_join(ca_dir, i)
4554- if not exists(sd):
4555- os.mkdir(sd)
4556-
4557- if not exists(path_join(ca_dir, 'serial')):
4558- with open(path_join(ca_dir, 'serial'), 'w') as fh:
4559- fh.write('02\n')
4560-
4561- if not exists(path_join(ca_dir, 'index.txt')):
4562- with open(path_join(ca_dir, 'index.txt'), 'w') as fh:
4563- fh.write('')
4564-
4565- def _init_ca(self):
4566- """Generate the root ca's cert and key.
4567- """
4568- if not exists(path_join(self.ca_dir, 'ca.cnf')):
4569- with open(path_join(self.ca_dir, 'ca.cnf'), 'w') as fh:
4570- fh.write(
4571- CA_CONF_TEMPLATE % (self.get_conf_variables()))
4572-
4573- if not exists(path_join(self.ca_dir, 'signing.cnf')):
4574- with open(path_join(self.ca_dir, 'signing.cnf'), 'w') as fh:
4575- fh.write(
4576- SIGNING_CONF_TEMPLATE % (self.get_conf_variables()))
4577-
4578- if exists(self.ca_cert) or exists(self.ca_key):
4579- raise RuntimeError("Initialized called when CA already exists")
4580- cmd = ['openssl', 'req', '-config', self.ca_conf,
4581- '-x509', '-nodes', '-newkey', 'rsa',
4582- '-days', self.default_ca_expiry,
4583- '-keyout', self.ca_key, '-out', self.ca_cert,
4584- '-outform', 'PEM']
4585- output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
4586- log("CA Init:\n %s" % output, level=DEBUG)
4587-
4588- def get_conf_variables(self):
4589- return dict(
4590- org_name="juju",
4591- org_unit_name="%s service" % self.name,
4592- common_name=self.name,
4593- ca_dir=self.ca_dir)
4594-
4595- def get_or_create_cert(self, common_name):
4596- if common_name in self:
4597- return self.get_certificate(common_name)
4598- return self.create_certificate(common_name)
4599-
4600- def create_certificate(self, common_name):
4601- if common_name in self:
4602- return self.get_certificate(common_name)
4603- key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
4604- crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
4605- csr_p = path_join(self.ca_dir, "certs", "%s.csr" % common_name)
4606- self._create_certificate(common_name, key_p, csr_p, crt_p)
4607- return self.get_certificate(common_name)
4608-
4609- def get_certificate(self, common_name):
4610- if common_name not in self:
4611- raise ValueError("No certificate for %s" % common_name)
4612- key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
4613- crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
4614- with open(crt_p) as fh:
4615- crt = fh.read()
4616- with open(key_p) as fh:
4617- key = fh.read()
4618- return crt, key
4619-
4620- def __contains__(self, common_name):
4621- crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
4622- return exists(crt_p)
4623-
4624- def _create_certificate(self, common_name, key_p, csr_p, crt_p):
4625- template_vars = self.get_conf_variables()
4626- template_vars['common_name'] = common_name
4627- subj = '/O=%(org_name)s/OU=%(org_unit_name)s/CN=%(common_name)s' % (
4628- template_vars)
4629-
4630- log("CA Create Cert %s" % common_name, level=DEBUG)
4631- cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa:2048',
4632- '-nodes', '-days', self.default_expiry,
4633- '-keyout', key_p, '-out', csr_p, '-subj', subj]
4634- subprocess.check_call(cmd, stderr=subprocess.PIPE)
4635- cmd = ['openssl', 'rsa', '-in', key_p, '-out', key_p]
4636- subprocess.check_call(cmd, stderr=subprocess.PIPE)
4637-
4638- log("CA Sign Cert %s" % common_name, level=DEBUG)
4639- if self.cert_type == MYSQL_CERT:
4640- cmd = ['openssl', 'x509', '-req',
4641- '-in', csr_p, '-days', self.default_expiry,
4642- '-CA', self.ca_cert, '-CAkey', self.ca_key,
4643- '-set_serial', '01', '-out', crt_p]
4644- else:
4645- cmd = ['openssl', 'ca', '-config', self.signing_conf,
4646- '-extensions', 'req_extensions',
4647- '-days', self.default_expiry, '-notext',
4648- '-in', csr_p, '-out', crt_p, '-subj', subj, '-batch']
4649- log("running %s" % " ".join(cmd), level=DEBUG)
4650- subprocess.check_call(cmd, stderr=subprocess.PIPE)
4651-
4652- def get_ca_bundle(self):
4653- with open(self.ca_cert) as fh:
4654- return fh.read()
4655-
4656-
4657-CA_CONF_TEMPLATE = """
4658-[ ca ]
4659-default_ca = CA_default
4660-
4661-[ CA_default ]
4662-dir = %(ca_dir)s
4663-policy = policy_match
4664-database = $dir/index.txt
4665-serial = $dir/serial
4666-certs = $dir/certs
4667-crl_dir = $dir/crl
4668-new_certs_dir = $dir/newcerts
4669-certificate = $dir/cacert.pem
4670-private_key = $dir/private/cacert.key
4671-RANDFILE = $dir/private/.rand
4672-default_md = default
4673-
4674-[ req ]
4675-default_bits = 1024
4676-default_md = sha1
4677-
4678-prompt = no
4679-distinguished_name = ca_distinguished_name
4680-
4681-x509_extensions = ca_extensions
4682-
4683-[ ca_distinguished_name ]
4684-organizationName = %(org_name)s
4685-organizationalUnitName = %(org_unit_name)s Certificate Authority
4686-
4687-
4688-[ policy_match ]
4689-countryName = optional
4690-stateOrProvinceName = optional
4691-organizationName = match
4692-organizationalUnitName = optional
4693-commonName = supplied
4694-
4695-[ ca_extensions ]
4696-basicConstraints = critical,CA:true
4697-subjectKeyIdentifier = hash
4698-authorityKeyIdentifier = keyid:always, issuer
4699-keyUsage = cRLSign, keyCertSign
4700-"""
4701-
4702-
4703-SIGNING_CONF_TEMPLATE = """
4704-[ ca ]
4705-default_ca = CA_default
4706-
4707-[ CA_default ]
4708-dir = %(ca_dir)s
4709-policy = policy_match
4710-database = $dir/index.txt
4711-serial = $dir/serial
4712-certs = $dir/certs
4713-crl_dir = $dir/crl
4714-new_certs_dir = $dir/newcerts
4715-certificate = $dir/cacert.pem
4716-private_key = $dir/private/cacert.key
4717-RANDFILE = $dir/private/.rand
4718-default_md = default
4719-
4720-[ req ]
4721-default_bits = 1024
4722-default_md = sha1
4723-
4724-prompt = no
4725-distinguished_name = req_distinguished_name
4726-
4727-x509_extensions = req_extensions
4728-
4729-[ req_distinguished_name ]
4730-organizationName = %(org_name)s
4731-organizationalUnitName = %(org_unit_name)s machine resources
4732-commonName = %(common_name)s
4733-
4734-[ policy_match ]
4735-countryName = optional
4736-stateOrProvinceName = optional
4737-organizationName = match
4738-organizationalUnitName = optional
4739-commonName = supplied
4740-
4741-[ req_extensions ]
4742-basicConstraints = CA:false
4743-subjectKeyIdentifier = hash
4744-authorityKeyIdentifier = keyid:always, issuer
4745-keyUsage = digitalSignature, keyEncipherment, keyAgreement
4746-extendedKeyUsage = serverAuth, clientAuth
4747-"""
4748
4749=== removed directory 'tests/charmhelpers/core'
4750=== removed file 'tests/charmhelpers/core/__init__.py'
4751--- tests/charmhelpers/core/__init__.py 2015-04-13 22:11:34 +0000
4752+++ tests/charmhelpers/core/__init__.py 1970-01-01 00:00:00 +0000
4753@@ -1,15 +0,0 @@
4754-# Copyright 2014-2015 Canonical Limited.
4755-#
4756-# This file is part of charm-helpers.
4757-#
4758-# charm-helpers is free software: you can redistribute it and/or modify
4759-# it under the terms of the GNU Lesser General Public License version 3 as
4760-# published by the Free Software Foundation.
4761-#
4762-# charm-helpers is distributed in the hope that it will be useful,
4763-# but WITHOUT ANY WARRANTY; without even the implied warranty of
4764-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4765-# GNU Lesser General Public License for more details.
4766-#
4767-# You should have received a copy of the GNU Lesser General Public License
4768-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4769
4770=== removed file 'tests/charmhelpers/core/decorators.py'
4771--- tests/charmhelpers/core/decorators.py 2015-04-13 22:11:34 +0000
4772+++ tests/charmhelpers/core/decorators.py 1970-01-01 00:00:00 +0000
4773@@ -1,57 +0,0 @@
4774-# Copyright 2014-2015 Canonical Limited.
4775-#
4776-# This file is part of charm-helpers.
4777-#
4778-# charm-helpers is free software: you can redistribute it and/or modify
4779-# it under the terms of the GNU Lesser General Public License version 3 as
4780-# published by the Free Software Foundation.
4781-#
4782-# charm-helpers is distributed in the hope that it will be useful,
4783-# but WITHOUT ANY WARRANTY; without even the implied warranty of
4784-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4785-# GNU Lesser General Public License for more details.
4786-#
4787-# You should have received a copy of the GNU Lesser General Public License
4788-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4789-
4790-#
4791-# Copyright 2014 Canonical Ltd.
4792-#
4793-# Authors:
4794-# Edward Hope-Morley <opentastic@gmail.com>
4795-#
4796-
4797-import time
4798-
4799-from charmhelpers.core.hookenv import (
4800- log,
4801- INFO,
4802-)
4803-
4804-
4805-def retry_on_exception(num_retries, base_delay=0, exc_type=Exception):
4806- """If the decorated function raises exception exc_type, allow num_retries
4807- retry attempts before raise the exception.
4808- """
4809- def _retry_on_exception_inner_1(f):
4810- def _retry_on_exception_inner_2(*args, **kwargs):
4811- retries = num_retries
4812- multiplier = 1
4813- while True:
4814- try:
4815- return f(*args, **kwargs)
4816- except exc_type:
4817- if not retries:
4818- raise
4819-
4820- delay = base_delay * multiplier
4821- multiplier += 1
4822- log("Retrying '%s' %d more times (delay=%s)" %
4823- (f.__name__, retries, delay), level=INFO)
4824- retries -= 1
4825- if delay:
4826- time.sleep(delay)
4827-
4828- return _retry_on_exception_inner_2
4829-
4830- return _retry_on_exception_inner_1
4831
4832=== removed file 'tests/charmhelpers/core/files.py'
4833--- tests/charmhelpers/core/files.py 2015-08-10 16:38:33 +0000
4834+++ tests/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
4835@@ -1,45 +0,0 @@
4836-#!/usr/bin/env python
4837-# -*- coding: utf-8 -*-
4838-
4839-# Copyright 2014-2015 Canonical Limited.
4840-#
4841-# This file is part of charm-helpers.
4842-#
4843-# charm-helpers is free software: you can redistribute it and/or modify
4844-# it under the terms of the GNU Lesser General Public License version 3 as
4845-# published by the Free Software Foundation.
4846-#
4847-# charm-helpers is distributed in the hope that it will be useful,
4848-# but WITHOUT ANY WARRANTY; without even the implied warranty of
4849-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4850-# GNU Lesser General Public License for more details.
4851-#
4852-# You should have received a copy of the GNU Lesser General Public License
4853-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4854-
4855-__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
4856-
4857-import os
4858-import subprocess
4859-
4860-
4861-def sed(filename, before, after, flags='g'):
4862- """
4863- Search and replaces the given pattern on filename.
4864-
4865- :param filename: relative or absolute file path.
4866- :param before: expression to be replaced (see 'man sed')
4867- :param after: expression to replace with (see 'man sed')
4868- :param flags: sed-compatible regex flags in example, to make
4869- the search and replace case insensitive, specify ``flags="i"``.
4870- The ``g`` flag is always specified regardless, so you do not
4871- need to remember to include it when overriding this parameter.
4872- :returns: If the sed command exit code was zero then return,
4873- otherwise raise CalledProcessError.
4874- """
4875- expression = r's/{0}/{1}/{2}'.format(before,
4876- after, flags)
4877-
4878- return subprocess.check_call(["sed", "-i", "-r", "-e",
4879- expression,
4880- os.path.expanduser(filename)])
4881
4882=== removed file 'tests/charmhelpers/core/fstab.py'
4883--- tests/charmhelpers/core/fstab.py 2015-04-13 22:11:34 +0000
4884+++ tests/charmhelpers/core/fstab.py 1970-01-01 00:00:00 +0000
4885@@ -1,134 +0,0 @@
4886-#!/usr/bin/env python
4887-# -*- coding: utf-8 -*-
4888-
4889-# Copyright 2014-2015 Canonical Limited.
4890-#
4891-# This file is part of charm-helpers.
4892-#
4893-# charm-helpers is free software: you can redistribute it and/or modify
4894-# it under the terms of the GNU Lesser General Public License version 3 as
4895-# published by the Free Software Foundation.
4896-#
4897-# charm-helpers is distributed in the hope that it will be useful,
4898-# but WITHOUT ANY WARRANTY; without even the implied warranty of
4899-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4900-# GNU Lesser General Public License for more details.
4901-#
4902-# You should have received a copy of the GNU Lesser General Public License
4903-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4904-
4905-import io
4906-import os
4907-
4908-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
4909-
4910-
4911-class Fstab(io.FileIO):
4912- """This class extends file in order to implement a file reader/writer
4913- for file `/etc/fstab`
4914- """
4915-
4916- class Entry(object):
4917- """Entry class represents a non-comment line on the `/etc/fstab` file
4918- """
4919- def __init__(self, device, mountpoint, filesystem,
4920- options, d=0, p=0):
4921- self.device = device
4922- self.mountpoint = mountpoint
4923- self.filesystem = filesystem
4924-
4925- if not options:
4926- options = "defaults"
4927-
4928- self.options = options
4929- self.d = int(d)
4930- self.p = int(p)
4931-
4932- def __eq__(self, o):
4933- return str(self) == str(o)
4934-
4935- def __str__(self):
4936- return "{} {} {} {} {} {}".format(self.device,
4937- self.mountpoint,
4938- self.filesystem,
4939- self.options,
4940- self.d,
4941- self.p)
4942-
4943- DEFAULT_PATH = os.path.join(os.path.sep, 'etc', 'fstab')
4944-
4945- def __init__(self, path=None):
4946- if path:
4947- self._path = path
4948- else:
4949- self._path = self.DEFAULT_PATH
4950- super(Fstab, self).__init__(self._path, 'rb+')
4951-
4952- def _hydrate_entry(self, line):
4953- # NOTE: use split with no arguments to split on any
4954- # whitespace including tabs
4955- return Fstab.Entry(*filter(
4956- lambda x: x not in ('', None),
4957- line.strip("\n").split()))
4958-
4959- @property
4960- def entries(self):
4961- self.seek(0)
4962- for line in self.readlines():
4963- line = line.decode('us-ascii')
4964- try:
4965- if line.strip() and not line.strip().startswith("#"):
4966- yield self._hydrate_entry(line)
4967- except ValueError:
4968- pass
4969-
4970- def get_entry_by_attr(self, attr, value):
4971- for entry in self.entries:
4972- e_attr = getattr(entry, attr)
4973- if e_attr == value:
4974- return entry
4975- return None
4976-
4977- def add_entry(self, entry):
4978- if self.get_entry_by_attr('device', entry.device):
4979- return False
4980-
4981- self.write((str(entry) + '\n').encode('us-ascii'))
4982- self.truncate()
4983- return entry
4984-
4985- def remove_entry(self, entry):
4986- self.seek(0)
4987-
4988- lines = [l.decode('us-ascii') for l in self.readlines()]
4989-
4990- found = False
4991- for index, line in enumerate(lines):
4992- if line.strip() and not line.strip().startswith("#"):
4993- if self._hydrate_entry(line) == entry:
4994- found = True
4995- break
4996-
4997- if not found:
4998- return False
4999-
5000- lines.remove(line)
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches