Merge lp:~hopem/charms/trusty/keystone/lp1499643 into lp:~openstack-charmers-archive/charms/trusty/keystone/next

Proposed by Edward Hope-Morley
Status: Merged
Merged at revision: 179
Proposed branch: lp:~hopem/charms/trusty/keystone/lp1499643
Merge into: lp:~openstack-charmers-archive/charms/trusty/keystone/next
Diff against target: 2301 lines (+1618/-93)
16 files modified
charmhelpers/contrib/network/ip.py (+5/-3)
charmhelpers/contrib/openstack/amulet/deployment.py (+23/-9)
charmhelpers/contrib/openstack/amulet/utils.py (+359/-0)
charmhelpers/contrib/openstack/context.py (+60/-16)
charmhelpers/contrib/openstack/templating.py (+30/-2)
charmhelpers/contrib/openstack/utils.py (+232/-2)
charmhelpers/contrib/storage/linux/ceph.py (+226/-13)
charmhelpers/core/hookenv.py (+32/-0)
charmhelpers/core/host.py (+32/-16)
charmhelpers/core/hugepage.py (+8/-1)
charmhelpers/core/strutils.py (+30/-0)
tests/charmhelpers/contrib/amulet/deployment.py (+4/-2)
tests/charmhelpers/contrib/amulet/utils.py (+193/-19)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+23/-9)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+359/-0)
unit_tests/test_keystone_hooks.py (+2/-1)
To merge this branch: bzr merge lp:~hopem/charms/trusty/keystone/lp1499643
Reviewer Review Type Date Requested Status
Liam Young (community) Approve
Review via email: mp+272411@code.launchpad.net
To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #10767 keystone-next for hopem mp272411
    LINT OK: passed

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

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

charm_unit_test #9944 keystone-next for hopem mp272411
    UNIT OK: passed

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

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

charm_amulet_test #6772 keystone-next for hopem mp272411
    AMULET FAIL: amulet-test failed

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

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

Revision history for this message
Liam Young (gnuoy) wrote :

Amulet fail is unrelated. Approved

review: Approve
Revision history for this message
Ryan Beisner (1chb1n) wrote :

(!) All keystone/next amulet tests with, and after, this commit, are failing in the same place as the test here.

Last known good was keystone/test rev178

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmhelpers/contrib/network/ip.py'
2--- charmhelpers/contrib/network/ip.py 2015-09-03 09:40:28 +0000
3+++ charmhelpers/contrib/network/ip.py 2015-09-25 14:42:33 +0000
4@@ -23,7 +23,7 @@
5 from functools import partial
6
7 from charmhelpers.core.hookenv import unit_get
8-from charmhelpers.fetch import apt_install
9+from charmhelpers.fetch import apt_install, apt_update
10 from charmhelpers.core.hookenv import (
11 log,
12 WARNING,
13@@ -32,13 +32,15 @@
14 try:
15 import netifaces
16 except ImportError:
17- apt_install('python-netifaces')
18+ apt_update(fatal=True)
19+ apt_install('python-netifaces', fatal=True)
20 import netifaces
21
22 try:
23 import netaddr
24 except ImportError:
25- apt_install('python-netaddr')
26+ apt_update(fatal=True)
27+ apt_install('python-netaddr', fatal=True)
28 import netaddr
29
30
31
32=== modified file 'charmhelpers/contrib/openstack/amulet/deployment.py'
33--- charmhelpers/contrib/openstack/amulet/deployment.py 2015-08-19 13:53:50 +0000
34+++ charmhelpers/contrib/openstack/amulet/deployment.py 2015-09-25 14:42:33 +0000
35@@ -44,20 +44,31 @@
36 Determine if the local branch being tested is derived from its
37 stable or next (dev) branch, and based on this, use the corresonding
38 stable or next branches for the other_services."""
39+
40+ # Charms outside the lp:~openstack-charmers namespace
41 base_charms = ['mysql', 'mongodb', 'nrpe']
42
43+ # Force these charms to current series even when using an older series.
44+ # ie. Use trusty/nrpe even when series is precise, as the P charm
45+ # does not possess the necessary external master config and hooks.
46+ force_series_current = ['nrpe']
47+
48 if self.series in ['precise', 'trusty']:
49 base_series = self.series
50 else:
51 base_series = self.current_next
52
53- if self.stable:
54- for svc in other_services:
55+ for svc in other_services:
56+ if svc['name'] in force_series_current:
57+ base_series = self.current_next
58+ # If a location has been explicitly set, use it
59+ if svc.get('location'):
60+ continue
61+ if self.stable:
62 temp = 'lp:charms/{}/{}'
63 svc['location'] = temp.format(base_series,
64 svc['name'])
65- else:
66- for svc in other_services:
67+ else:
68 if svc['name'] in base_charms:
69 temp = 'lp:charms/{}/{}'
70 svc['location'] = temp.format(base_series,
71@@ -66,6 +77,7 @@
72 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
73 svc['location'] = temp.format(self.current_next,
74 svc['name'])
75+
76 return other_services
77
78 def _add_services(self, this_service, other_services):
79@@ -77,21 +89,23 @@
80
81 services = other_services
82 services.append(this_service)
83+
84+ # Charms which should use the source config option
85 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
86 'ceph-osd', 'ceph-radosgw']
87- # Most OpenStack subordinate charms do not expose an origin option
88- # as that is controlled by the principle.
89- ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
90+
91+ # Charms which can not use openstack-origin, ie. many subordinates
92+ no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
93
94 if self.openstack:
95 for svc in services:
96- if svc['name'] not in use_source + ignore:
97+ if svc['name'] not in use_source + no_origin:
98 config = {'openstack-origin': self.openstack}
99 self.d.configure(svc['name'], config)
100
101 if self.source:
102 for svc in services:
103- if svc['name'] in use_source and svc['name'] not in ignore:
104+ if svc['name'] in use_source and svc['name'] not in no_origin:
105 config = {'source': self.source}
106 self.d.configure(svc['name'], config)
107
108
109=== modified file 'charmhelpers/contrib/openstack/amulet/utils.py'
110--- charmhelpers/contrib/openstack/amulet/utils.py 2015-07-16 20:17:38 +0000
111+++ charmhelpers/contrib/openstack/amulet/utils.py 2015-09-25 14:42:33 +0000
112@@ -27,6 +27,7 @@
113 import heatclient.v1.client as heat_client
114 import keystoneclient.v2_0 as keystone_client
115 import novaclient.v1_1.client as nova_client
116+import pika
117 import swiftclient
118
119 from charmhelpers.contrib.amulet.utils import (
120@@ -602,3 +603,361 @@
121 self.log.debug('Ceph {} samples (OK): '
122 '{}'.format(sample_type, samples))
123 return None
124+
125+# rabbitmq/amqp specific helpers:
126+ def add_rmq_test_user(self, sentry_units,
127+ username="testuser1", password="changeme"):
128+ """Add a test user via the first rmq juju unit, check connection as
129+ the new user against all sentry units.
130+
131+ :param sentry_units: list of sentry unit pointers
132+ :param username: amqp user name, default to testuser1
133+ :param password: amqp user password
134+ :returns: None if successful. Raise on error.
135+ """
136+ self.log.debug('Adding rmq user ({})...'.format(username))
137+
138+ # Check that user does not already exist
139+ cmd_user_list = 'rabbitmqctl list_users'
140+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
141+ if username in output:
142+ self.log.warning('User ({}) already exists, returning '
143+ 'gracefully.'.format(username))
144+ return
145+
146+ perms = '".*" ".*" ".*"'
147+ cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
148+ 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
149+
150+ # Add user via first unit
151+ for cmd in cmds:
152+ output, _ = self.run_cmd_unit(sentry_units[0], cmd)
153+
154+ # Check connection against the other sentry_units
155+ self.log.debug('Checking user connect against units...')
156+ for sentry_unit in sentry_units:
157+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
158+ username=username,
159+ password=password)
160+ connection.close()
161+
162+ def delete_rmq_test_user(self, sentry_units, username="testuser1"):
163+ """Delete a rabbitmq user via the first rmq juju unit.
164+
165+ :param sentry_units: list of sentry unit pointers
166+ :param username: amqp user name, default to testuser1
167+ :param password: amqp user password
168+ :returns: None if successful or no such user.
169+ """
170+ self.log.debug('Deleting rmq user ({})...'.format(username))
171+
172+ # Check that the user exists
173+ cmd_user_list = 'rabbitmqctl list_users'
174+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
175+
176+ if username not in output:
177+ self.log.warning('User ({}) does not exist, returning '
178+ 'gracefully.'.format(username))
179+ return
180+
181+ # Delete the user
182+ cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
183+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
184+
185+ def get_rmq_cluster_status(self, sentry_unit):
186+ """Execute rabbitmq cluster status command on a unit and return
187+ the full output.
188+
189+ :param unit: sentry unit
190+ :returns: String containing console output of cluster status command
191+ """
192+ cmd = 'rabbitmqctl cluster_status'
193+ output, _ = self.run_cmd_unit(sentry_unit, cmd)
194+ self.log.debug('{} cluster_status:\n{}'.format(
195+ sentry_unit.info['unit_name'], output))
196+ return str(output)
197+
198+ def get_rmq_cluster_running_nodes(self, sentry_unit):
199+ """Parse rabbitmqctl cluster_status output string, return list of
200+ running rabbitmq cluster nodes.
201+
202+ :param unit: sentry unit
203+ :returns: List containing node names of running nodes
204+ """
205+ # NOTE(beisner): rabbitmqctl cluster_status output is not
206+ # json-parsable, do string chop foo, then json.loads that.
207+ str_stat = self.get_rmq_cluster_status(sentry_unit)
208+ if 'running_nodes' in str_stat:
209+ pos_start = str_stat.find("{running_nodes,") + 15
210+ pos_end = str_stat.find("]},", pos_start) + 1
211+ str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"')
212+ run_nodes = json.loads(str_run_nodes)
213+ return run_nodes
214+ else:
215+ return []
216+
217+ def validate_rmq_cluster_running_nodes(self, sentry_units):
218+ """Check that all rmq unit hostnames are represented in the
219+ cluster_status output of all units.
220+
221+ :param host_names: dict of juju unit names to host names
222+ :param units: list of sentry unit pointers (all rmq units)
223+ :returns: None if successful, otherwise return error message
224+ """
225+ host_names = self.get_unit_hostnames(sentry_units)
226+ errors = []
227+
228+ # Query every unit for cluster_status running nodes
229+ for query_unit in sentry_units:
230+ query_unit_name = query_unit.info['unit_name']
231+ running_nodes = self.get_rmq_cluster_running_nodes(query_unit)
232+
233+ # Confirm that every unit is represented in the queried unit's
234+ # cluster_status running nodes output.
235+ for validate_unit in sentry_units:
236+ val_host_name = host_names[validate_unit.info['unit_name']]
237+ val_node_name = 'rabbit@{}'.format(val_host_name)
238+
239+ if val_node_name not in running_nodes:
240+ errors.append('Cluster member check failed on {}: {} not '
241+ 'in {}\n'.format(query_unit_name,
242+ val_node_name,
243+ running_nodes))
244+ if errors:
245+ return ''.join(errors)
246+
247+ def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None):
248+ """Check a single juju rmq unit for ssl and port in the config file."""
249+ host = sentry_unit.info['public-address']
250+ unit_name = sentry_unit.info['unit_name']
251+
252+ conf_file = '/etc/rabbitmq/rabbitmq.config'
253+ conf_contents = str(self.file_contents_safe(sentry_unit,
254+ conf_file, max_wait=16))
255+ # Checks
256+ conf_ssl = 'ssl' in conf_contents
257+ conf_port = str(port) in conf_contents
258+
259+ # Port explicitly checked in config
260+ if port and conf_port and conf_ssl:
261+ self.log.debug('SSL is enabled @{}:{} '
262+ '({})'.format(host, port, unit_name))
263+ return True
264+ elif port and not conf_port and conf_ssl:
265+ self.log.debug('SSL is enabled @{} but not on port {} '
266+ '({})'.format(host, port, unit_name))
267+ return False
268+ # Port not checked (useful when checking that ssl is disabled)
269+ elif not port and conf_ssl:
270+ self.log.debug('SSL is enabled @{}:{} '
271+ '({})'.format(host, port, unit_name))
272+ return True
273+ elif not port and not conf_ssl:
274+ self.log.debug('SSL not enabled @{}:{} '
275+ '({})'.format(host, port, unit_name))
276+ return False
277+ else:
278+ msg = ('Unknown condition when checking SSL status @{}:{} '
279+ '({})'.format(host, port, unit_name))
280+ amulet.raise_status(amulet.FAIL, msg)
281+
282+ def validate_rmq_ssl_enabled_units(self, sentry_units, port=None):
283+ """Check that ssl is enabled on rmq juju sentry units.
284+
285+ :param sentry_units: list of all rmq sentry units
286+ :param port: optional ssl port override to validate
287+ :returns: None if successful, otherwise return error message
288+ """
289+ for sentry_unit in sentry_units:
290+ if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port):
291+ return ('Unexpected condition: ssl is disabled on unit '
292+ '({})'.format(sentry_unit.info['unit_name']))
293+ return None
294+
295+ def validate_rmq_ssl_disabled_units(self, sentry_units):
296+ """Check that ssl is enabled on listed rmq juju sentry units.
297+
298+ :param sentry_units: list of all rmq sentry units
299+ :returns: True if successful. Raise on error.
300+ """
301+ for sentry_unit in sentry_units:
302+ if self.rmq_ssl_is_enabled_on_unit(sentry_unit):
303+ return ('Unexpected condition: ssl is enabled on unit '
304+ '({})'.format(sentry_unit.info['unit_name']))
305+ return None
306+
307+ def configure_rmq_ssl_on(self, sentry_units, deployment,
308+ port=None, max_wait=60):
309+ """Turn ssl charm config option on, with optional non-default
310+ ssl port specification. Confirm that it is enabled on every
311+ unit.
312+
313+ :param sentry_units: list of sentry units
314+ :param deployment: amulet deployment object pointer
315+ :param port: amqp port, use defaults if None
316+ :param max_wait: maximum time to wait in seconds to confirm
317+ :returns: None if successful. Raise on error.
318+ """
319+ self.log.debug('Setting ssl charm config option: on')
320+
321+ # Enable RMQ SSL
322+ config = {'ssl': 'on'}
323+ if port:
324+ config['ssl_port'] = port
325+
326+ deployment.configure('rabbitmq-server', config)
327+
328+ # Confirm
329+ tries = 0
330+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
331+ while ret and tries < (max_wait / 4):
332+ time.sleep(4)
333+ self.log.debug('Attempt {}: {}'.format(tries, ret))
334+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
335+ tries += 1
336+
337+ if ret:
338+ amulet.raise_status(amulet.FAIL, ret)
339+
340+ def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60):
341+ """Turn ssl charm config option off, confirm that it is disabled
342+ on every unit.
343+
344+ :param sentry_units: list of sentry units
345+ :param deployment: amulet deployment object pointer
346+ :param max_wait: maximum time to wait in seconds to confirm
347+ :returns: None if successful. Raise on error.
348+ """
349+ self.log.debug('Setting ssl charm config option: off')
350+
351+ # Disable RMQ SSL
352+ config = {'ssl': 'off'}
353+ deployment.configure('rabbitmq-server', config)
354+
355+ # Confirm
356+ tries = 0
357+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
358+ while ret and tries < (max_wait / 4):
359+ time.sleep(4)
360+ self.log.debug('Attempt {}: {}'.format(tries, ret))
361+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
362+ tries += 1
363+
364+ if ret:
365+ amulet.raise_status(amulet.FAIL, ret)
366+
367+ def connect_amqp_by_unit(self, sentry_unit, ssl=False,
368+ port=None, fatal=True,
369+ username="testuser1", password="changeme"):
370+ """Establish and return a pika amqp connection to the rabbitmq service
371+ running on a rmq juju unit.
372+
373+ :param sentry_unit: sentry unit pointer
374+ :param ssl: boolean, default to False
375+ :param port: amqp port, use defaults if None
376+ :param fatal: boolean, default to True (raises on connect error)
377+ :param username: amqp user name, default to testuser1
378+ :param password: amqp user password
379+ :returns: pika amqp connection pointer or None if failed and non-fatal
380+ """
381+ host = sentry_unit.info['public-address']
382+ unit_name = sentry_unit.info['unit_name']
383+
384+ # Default port logic if port is not specified
385+ if ssl and not port:
386+ port = 5671
387+ elif not ssl and not port:
388+ port = 5672
389+
390+ self.log.debug('Connecting to amqp on {}:{} ({}) as '
391+ '{}...'.format(host, port, unit_name, username))
392+
393+ try:
394+ credentials = pika.PlainCredentials(username, password)
395+ parameters = pika.ConnectionParameters(host=host, port=port,
396+ credentials=credentials,
397+ ssl=ssl,
398+ connection_attempts=3,
399+ retry_delay=5,
400+ socket_timeout=1)
401+ connection = pika.BlockingConnection(parameters)
402+ assert connection.server_properties['product'] == 'RabbitMQ'
403+ self.log.debug('Connect OK')
404+ return connection
405+ except Exception as e:
406+ msg = ('amqp connection failed to {}:{} as '
407+ '{} ({})'.format(host, port, username, str(e)))
408+ if fatal:
409+ amulet.raise_status(amulet.FAIL, msg)
410+ else:
411+ self.log.warn(msg)
412+ return None
413+
414+ def publish_amqp_message_by_unit(self, sentry_unit, message,
415+ queue="test", ssl=False,
416+ username="testuser1",
417+ password="changeme",
418+ port=None):
419+ """Publish an amqp message to a rmq juju unit.
420+
421+ :param sentry_unit: sentry unit pointer
422+ :param message: amqp message string
423+ :param queue: message queue, default to test
424+ :param username: amqp user name, default to testuser1
425+ :param password: amqp user password
426+ :param ssl: boolean, default to False
427+ :param port: amqp port, use defaults if None
428+ :returns: None. Raises exception if publish failed.
429+ """
430+ self.log.debug('Publishing message to {} queue:\n{}'.format(queue,
431+ message))
432+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
433+ port=port,
434+ username=username,
435+ password=password)
436+
437+ # NOTE(beisner): extra debug here re: pika hang potential:
438+ # https://github.com/pika/pika/issues/297
439+ # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw
440+ self.log.debug('Defining channel...')
441+ channel = connection.channel()
442+ self.log.debug('Declaring queue...')
443+ channel.queue_declare(queue=queue, auto_delete=False, durable=True)
444+ self.log.debug('Publishing message...')
445+ channel.basic_publish(exchange='', routing_key=queue, body=message)
446+ self.log.debug('Closing channel...')
447+ channel.close()
448+ self.log.debug('Closing connection...')
449+ connection.close()
450+
451+ def get_amqp_message_by_unit(self, sentry_unit, queue="test",
452+ username="testuser1",
453+ password="changeme",
454+ ssl=False, port=None):
455+ """Get an amqp message from a rmq juju unit.
456+
457+ :param sentry_unit: sentry unit pointer
458+ :param queue: message queue, default to test
459+ :param username: amqp user name, default to testuser1
460+ :param password: amqp user password
461+ :param ssl: boolean, default to False
462+ :param port: amqp port, use defaults if None
463+ :returns: amqp message body as string. Raise if get fails.
464+ """
465+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
466+ port=port,
467+ username=username,
468+ password=password)
469+ channel = connection.channel()
470+ method_frame, _, body = channel.basic_get(queue)
471+
472+ if method_frame:
473+ self.log.debug('Retreived message from {} queue:\n{}'.format(queue,
474+ body))
475+ channel.basic_ack(method_frame.delivery_tag)
476+ channel.close()
477+ connection.close()
478+ return body
479+ else:
480+ msg = 'No message retrieved.'
481+ amulet.raise_status(amulet.FAIL, msg)
482
483=== modified file 'charmhelpers/contrib/openstack/context.py'
484--- charmhelpers/contrib/openstack/context.py 2015-09-03 09:40:28 +0000
485+++ charmhelpers/contrib/openstack/context.py 2015-09-25 14:42:33 +0000
486@@ -194,10 +194,50 @@
487 class OSContextGenerator(object):
488 """Base class for all context generators."""
489 interfaces = []
490+ related = False
491+ complete = False
492+ missing_data = []
493
494 def __call__(self):
495 raise NotImplementedError
496
497+ def context_complete(self, ctxt):
498+ """Check for missing data for the required context data.
499+ Set self.missing_data if it exists and return False.
500+ Set self.complete if no missing data and return True.
501+ """
502+ # Fresh start
503+ self.complete = False
504+ self.missing_data = []
505+ for k, v in six.iteritems(ctxt):
506+ if v is None or v == '':
507+ if k not in self.missing_data:
508+ self.missing_data.append(k)
509+
510+ if self.missing_data:
511+ self.complete = False
512+ log('Missing required data: %s' % ' '.join(self.missing_data), level=INFO)
513+ else:
514+ self.complete = True
515+ return self.complete
516+
517+ def get_related(self):
518+ """Check if any of the context interfaces have relation ids.
519+ Set self.related and return True if one of the interfaces
520+ has relation ids.
521+ """
522+ # Fresh start
523+ self.related = False
524+ try:
525+ for interface in self.interfaces:
526+ if relation_ids(interface):
527+ self.related = True
528+ return self.related
529+ except AttributeError as e:
530+ log("{} {}"
531+ "".format(self, e), 'INFO')
532+ return self.related
533+
534
535 class SharedDBContext(OSContextGenerator):
536 interfaces = ['shared-db']
537@@ -213,6 +253,7 @@
538 self.database = database
539 self.user = user
540 self.ssl_dir = ssl_dir
541+ self.rel_name = self.interfaces[0]
542
543 def __call__(self):
544 self.database = self.database or config('database')
545@@ -246,6 +287,7 @@
546 password_setting = self.relation_prefix + '_password'
547
548 for rid in relation_ids(self.interfaces[0]):
549+ self.related = True
550 for unit in related_units(rid):
551 rdata = relation_get(rid=rid, unit=unit)
552 host = rdata.get('db_host')
553@@ -257,7 +299,7 @@
554 'database_password': rdata.get(password_setting),
555 'database_type': 'mysql'
556 }
557- if context_complete(ctxt):
558+ if self.context_complete(ctxt):
559 db_ssl(rdata, ctxt, self.ssl_dir)
560 return ctxt
561 return {}
562@@ -278,6 +320,7 @@
563
564 ctxt = {}
565 for rid in relation_ids(self.interfaces[0]):
566+ self.related = True
567 for unit in related_units(rid):
568 rel_host = relation_get('host', rid=rid, unit=unit)
569 rel_user = relation_get('user', rid=rid, unit=unit)
570@@ -287,7 +330,7 @@
571 'database_user': rel_user,
572 'database_password': rel_passwd,
573 'database_type': 'postgresql'}
574- if context_complete(ctxt):
575+ if self.context_complete(ctxt):
576 return ctxt
577
578 return {}
579@@ -348,6 +391,7 @@
580 ctxt['signing_dir'] = cachedir
581
582 for rid in relation_ids(self.rel_name):
583+ self.related = True
584 for unit in related_units(rid):
585 rdata = relation_get(rid=rid, unit=unit)
586 serv_host = rdata.get('service_host')
587@@ -366,7 +410,7 @@
588 'service_protocol': svc_protocol,
589 'auth_protocol': auth_protocol})
590
591- if context_complete(ctxt):
592+ if self.context_complete(ctxt):
593 # NOTE(jamespage) this is required for >= icehouse
594 # so a missing value just indicates keystone needs
595 # upgrading
596@@ -405,6 +449,7 @@
597 ctxt = {}
598 for rid in relation_ids(self.rel_name):
599 ha_vip_only = False
600+ self.related = True
601 for unit in related_units(rid):
602 if relation_get('clustered', rid=rid, unit=unit):
603 ctxt['clustered'] = True
604@@ -437,7 +482,7 @@
605 ha_vip_only = relation_get('ha-vip-only',
606 rid=rid, unit=unit) is not None
607
608- if context_complete(ctxt):
609+ if self.context_complete(ctxt):
610 if 'rabbit_ssl_ca' in ctxt:
611 if not self.ssl_dir:
612 log("Charm not setup for ssl support but ssl ca "
613@@ -469,7 +514,7 @@
614 ctxt['oslo_messaging_flags'] = config_flags_parser(
615 oslo_messaging_flags)
616
617- if not context_complete(ctxt):
618+ if not self.complete:
619 return {}
620
621 return ctxt
622@@ -485,13 +530,15 @@
623
624 log('Generating template context for ceph', level=DEBUG)
625 mon_hosts = []
626- auth = None
627- key = None
628- use_syslog = str(config('use-syslog')).lower()
629+ ctxt = {
630+ 'use_syslog': str(config('use-syslog')).lower()
631+ }
632 for rid in relation_ids('ceph'):
633 for unit in related_units(rid):
634- auth = relation_get('auth', rid=rid, unit=unit)
635- key = relation_get('key', rid=rid, unit=unit)
636+ if not ctxt.get('auth'):
637+ ctxt['auth'] = relation_get('auth', rid=rid, unit=unit)
638+ if not ctxt.get('key'):
639+ ctxt['key'] = relation_get('key', rid=rid, unit=unit)
640 ceph_pub_addr = relation_get('ceph-public-address', rid=rid,
641 unit=unit)
642 unit_priv_addr = relation_get('private-address', rid=rid,
643@@ -500,15 +547,12 @@
644 ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr
645 mon_hosts.append(ceph_addr)
646
647- ctxt = {'mon_hosts': ' '.join(sorted(mon_hosts)),
648- 'auth': auth,
649- 'key': key,
650- 'use_syslog': use_syslog}
651+ ctxt['mon_hosts'] = ' '.join(sorted(mon_hosts))
652
653 if not os.path.isdir('/etc/ceph'):
654 os.mkdir('/etc/ceph')
655
656- if not context_complete(ctxt):
657+ if not self.context_complete(ctxt):
658 return {}
659
660 ensure_packages(['ceph-common'])
661@@ -1367,6 +1411,6 @@
662 'auth_protocol':
663 rdata.get('auth_protocol') or 'http',
664 }
665- if context_complete(ctxt):
666+ if self.context_complete(ctxt):
667 return ctxt
668 return {}
669
670=== modified file 'charmhelpers/contrib/openstack/templating.py'
671--- charmhelpers/contrib/openstack/templating.py 2015-07-29 10:47:17 +0000
672+++ charmhelpers/contrib/openstack/templating.py 2015-09-25 14:42:33 +0000
673@@ -18,7 +18,7 @@
674
675 import six
676
677-from charmhelpers.fetch import apt_install
678+from charmhelpers.fetch import apt_install, apt_update
679 from charmhelpers.core.hookenv import (
680 log,
681 ERROR,
682@@ -29,6 +29,7 @@
683 try:
684 from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
685 except ImportError:
686+ apt_update(fatal=True)
687 apt_install('python-jinja2', fatal=True)
688 from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
689
690@@ -112,7 +113,7 @@
691
692 def complete_contexts(self):
693 '''
694- Return a list of interfaces that have atisfied contexts.
695+ Return a list of interfaces that have satisfied contexts.
696 '''
697 if self._complete_contexts:
698 return self._complete_contexts
699@@ -293,3 +294,30 @@
700 [interfaces.extend(i.complete_contexts())
701 for i in six.itervalues(self.templates)]
702 return interfaces
703+
704+ def get_incomplete_context_data(self, interfaces):
705+ '''
706+ Return dictionary of relation status of interfaces and any missing
707+ required context data. Example:
708+ {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
709+ 'zeromq-configuration': {'related': False}}
710+ '''
711+ incomplete_context_data = {}
712+
713+ for i in six.itervalues(self.templates):
714+ for context in i.contexts:
715+ for interface in interfaces:
716+ related = False
717+ if interface in context.interfaces:
718+ related = context.get_related()
719+ missing_data = context.missing_data
720+ if missing_data:
721+ incomplete_context_data[interface] = {'missing_data': missing_data}
722+ if related:
723+ if incomplete_context_data.get(interface):
724+ incomplete_context_data[interface].update({'related': True})
725+ else:
726+ incomplete_context_data[interface] = {'related': True}
727+ else:
728+ incomplete_context_data[interface] = {'related': False}
729+ return incomplete_context_data
730
731=== modified file 'charmhelpers/contrib/openstack/utils.py'
732--- charmhelpers/contrib/openstack/utils.py 2015-09-03 09:40:28 +0000
733+++ charmhelpers/contrib/openstack/utils.py 2015-09-25 14:42:33 +0000
734@@ -25,6 +25,7 @@
735 import re
736
737 import six
738+import traceback
739 import yaml
740
741 from charmhelpers.contrib.network import ip
742@@ -34,12 +35,16 @@
743 )
744
745 from charmhelpers.core.hookenv import (
746+ action_fail,
747+ action_set,
748 config,
749 log as juju_log,
750 charm_dir,
751 INFO,
752 relation_ids,
753- relation_set
754+ relation_set,
755+ status_set,
756+ hook_name
757 )
758
759 from charmhelpers.contrib.storage.linux.lvm import (
760@@ -49,7 +54,8 @@
761 )
762
763 from charmhelpers.contrib.network.ip import (
764- get_ipv6_addr
765+ get_ipv6_addr,
766+ is_ipv6,
767 )
768
769 from charmhelpers.contrib.python.packages import (
770@@ -114,6 +120,7 @@
771 ('2.2.1', 'kilo'),
772 ('2.2.2', 'kilo'),
773 ('2.3.0', 'liberty'),
774+ ('2.4.0', 'liberty'),
775 ])
776
777 # >= Liberty version->codename mapping
778@@ -142,6 +149,9 @@
779 'glance-common': OrderedDict([
780 ('11.0.0', 'liberty'),
781 ]),
782+ 'openstack-dashboard': OrderedDict([
783+ ('8.0.0', 'liberty'),
784+ ]),
785 }
786
787 DEFAULT_LOOPBACK_SIZE = '5G'
788@@ -510,6 +520,12 @@
789 relation_prefix=None):
790 hosts = get_ipv6_addr(dynamic_only=False)
791
792+ if config('vip'):
793+ vips = config('vip').split()
794+ for vip in vips:
795+ if vip and is_ipv6(vip):
796+ hosts.append(vip)
797+
798 kwargs = {'database': database,
799 'username': database_user,
800 'hostname': json.dumps(hosts)}
801@@ -745,3 +761,217 @@
802 return projects[key]
803
804 return None
805+
806+
807+def os_workload_status(configs, required_interfaces, charm_func=None):
808+ """
809+ Decorator to set workload status based on complete contexts
810+ """
811+ def wrap(f):
812+ @wraps(f)
813+ def wrapped_f(*args, **kwargs):
814+ # Run the original function first
815+ f(*args, **kwargs)
816+ # Set workload status now that contexts have been
817+ # acted on
818+ set_os_workload_status(configs, required_interfaces, charm_func)
819+ return wrapped_f
820+ return wrap
821+
822+
823+def set_os_workload_status(configs, required_interfaces, charm_func=None):
824+ """
825+ Set workload status based on complete contexts.
826+ status-set missing or incomplete contexts
827+ and juju-log details of missing required data.
828+ charm_func is a charm specific function to run checking
829+ for charm specific requirements such as a VIP setting.
830+ """
831+ incomplete_rel_data = incomplete_relation_data(configs, required_interfaces)
832+ state = 'active'
833+ missing_relations = []
834+ incomplete_relations = []
835+ message = None
836+ charm_state = None
837+ charm_message = None
838+
839+ for generic_interface in incomplete_rel_data.keys():
840+ related_interface = None
841+ missing_data = {}
842+ # Related or not?
843+ for interface in incomplete_rel_data[generic_interface]:
844+ if incomplete_rel_data[generic_interface][interface].get('related'):
845+ related_interface = interface
846+ missing_data = incomplete_rel_data[generic_interface][interface].get('missing_data')
847+ # No relation ID for the generic_interface
848+ if not related_interface:
849+ juju_log("{} relation is missing and must be related for "
850+ "functionality. ".format(generic_interface), 'WARN')
851+ state = 'blocked'
852+ if generic_interface not in missing_relations:
853+ missing_relations.append(generic_interface)
854+ else:
855+ # Relation ID exists but no related unit
856+ if not missing_data:
857+ # Edge case relation ID exists but departing
858+ if ('departed' in hook_name() or 'broken' in hook_name()) \
859+ and related_interface in hook_name():
860+ state = 'blocked'
861+ if generic_interface not in missing_relations:
862+ missing_relations.append(generic_interface)
863+ juju_log("{} relation's interface, {}, "
864+ "relationship is departed or broken "
865+ "and is required for functionality."
866+ "".format(generic_interface, related_interface), "WARN")
867+ # Normal case relation ID exists but no related unit
868+ # (joining)
869+ else:
870+ juju_log("{} relations's interface, {}, is related but has "
871+ "no units in the relation."
872+ "".format(generic_interface, related_interface), "INFO")
873+ # Related unit exists and data missing on the relation
874+ else:
875+ juju_log("{} relation's interface, {}, is related awaiting "
876+ "the following data from the relationship: {}. "
877+ "".format(generic_interface, related_interface,
878+ ", ".join(missing_data)), "INFO")
879+ if state != 'blocked':
880+ state = 'waiting'
881+ if generic_interface not in incomplete_relations \
882+ and generic_interface not in missing_relations:
883+ incomplete_relations.append(generic_interface)
884+
885+ if missing_relations:
886+ message = "Missing relations: {}".format(", ".join(missing_relations))
887+ if incomplete_relations:
888+ message += "; incomplete relations: {}" \
889+ "".format(", ".join(incomplete_relations))
890+ state = 'blocked'
891+ elif incomplete_relations:
892+ message = "Incomplete relations: {}" \
893+ "".format(", ".join(incomplete_relations))
894+ state = 'waiting'
895+
896+ # Run charm specific checks
897+ if charm_func:
898+ charm_state, charm_message = charm_func(configs)
899+ if charm_state != 'active' and charm_state != 'unknown':
900+ state = workload_state_compare(state, charm_state)
901+ if message:
902+ message = "{} {}".format(message, charm_message)
903+ else:
904+ message = charm_message
905+
906+ # Set to active if all requirements have been met
907+ if state == 'active':
908+ message = "Unit is ready"
909+ juju_log(message, "INFO")
910+
911+ status_set(state, message)
912+
913+
914+def workload_state_compare(current_workload_state, workload_state):
915+ """ Return highest priority of two states"""
916+ hierarchy = {'unknown': -1,
917+ 'active': 0,
918+ 'maintenance': 1,
919+ 'waiting': 2,
920+ 'blocked': 3,
921+ }
922+
923+ if hierarchy.get(workload_state) is None:
924+ workload_state = 'unknown'
925+ if hierarchy.get(current_workload_state) is None:
926+ current_workload_state = 'unknown'
927+
928+ # Set workload_state based on hierarchy of statuses
929+ if hierarchy.get(current_workload_state) > hierarchy.get(workload_state):
930+ return current_workload_state
931+ else:
932+ return workload_state
933+
934+
935+def incomplete_relation_data(configs, required_interfaces):
936+ """
937+ Check complete contexts against required_interfaces
938+ Return dictionary of incomplete relation data.
939+
940+ configs is an OSConfigRenderer object with configs registered
941+
942+ required_interfaces is a dictionary of required general interfaces
943+ with dictionary values of possible specific interfaces.
944+ Example:
945+ required_interfaces = {'database': ['shared-db', 'pgsql-db']}
946+
947+ The interface is said to be satisfied if anyone of the interfaces in the
948+ list has a complete context.
949+
950+ Return dictionary of incomplete or missing required contexts with relation
951+ status of interfaces and any missing data points. Example:
952+ {'message':
953+ {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
954+ 'zeromq-configuration': {'related': False}},
955+ 'identity':
956+ {'identity-service': {'related': False}},
957+ 'database':
958+ {'pgsql-db': {'related': False},
959+ 'shared-db': {'related': True}}}
960+ """
961+ complete_ctxts = configs.complete_contexts()
962+ incomplete_relations = []
963+ for svc_type in required_interfaces.keys():
964+ # Avoid duplicates
965+ found_ctxt = False
966+ for interface in required_interfaces[svc_type]:
967+ if interface in complete_ctxts:
968+ found_ctxt = True
969+ if not found_ctxt:
970+ incomplete_relations.append(svc_type)
971+ incomplete_context_data = {}
972+ for i in incomplete_relations:
973+ incomplete_context_data[i] = configs.get_incomplete_context_data(required_interfaces[i])
974+ return incomplete_context_data
975+
976+
977+def do_action_openstack_upgrade(package, upgrade_callback, configs):
978+ """Perform action-managed OpenStack upgrade.
979+
980+ Upgrades packages to the configured openstack-origin version and sets
981+ the corresponding action status as a result.
982+
983+ If the charm was installed from source we cannot upgrade it.
984+ For backwards compatibility a config flag (action-managed-upgrade) must
985+ be set for this code to run, otherwise a full service level upgrade will
986+ fire on config-changed.
987+
988+ @param package: package name for determining if upgrade available
989+ @param upgrade_callback: function callback to charm's upgrade function
990+ @param configs: templating object derived from OSConfigRenderer class
991+
992+ @return: True if upgrade successful; False if upgrade failed or skipped
993+ """
994+ ret = False
995+
996+ if git_install_requested():
997+ action_set({'outcome': 'installed from source, skipped upgrade.'})
998+ else:
999+ if openstack_upgrade_available(package):
1000+ if config('action-managed-upgrade'):
1001+ juju_log('Upgrading OpenStack release')
1002+
1003+ try:
1004+ upgrade_callback(configs=configs)
1005+ action_set({'outcome': 'success, upgrade completed.'})
1006+ ret = True
1007+ except:
1008+ action_set({'outcome': 'upgrade failed, see traceback.'})
1009+ action_set({'traceback': traceback.format_exc()})
1010+ action_fail('do_openstack_upgrade resulted in an '
1011+ 'unexpected error')
1012+ else:
1013+ action_set({'outcome': 'action-managed-upgrade config is '
1014+ 'False, skipped upgrade.'})
1015+ else:
1016+ action_set({'outcome': 'no upgrade available.'})
1017+
1018+ return ret
1019
1020=== modified file 'charmhelpers/contrib/storage/linux/ceph.py'
1021--- charmhelpers/contrib/storage/linux/ceph.py 2015-07-16 20:17:38 +0000
1022+++ charmhelpers/contrib/storage/linux/ceph.py 2015-09-25 14:42:33 +0000
1023@@ -28,6 +28,7 @@
1024 import shutil
1025 import json
1026 import time
1027+import uuid
1028
1029 from subprocess import (
1030 check_call,
1031@@ -35,8 +36,10 @@
1032 CalledProcessError,
1033 )
1034 from charmhelpers.core.hookenv import (
1035+ local_unit,
1036 relation_get,
1037 relation_ids,
1038+ relation_set,
1039 related_units,
1040 log,
1041 DEBUG,
1042@@ -56,6 +59,8 @@
1043 apt_install,
1044 )
1045
1046+from charmhelpers.core.kernel import modprobe
1047+
1048 KEYRING = '/etc/ceph/ceph.client.{}.keyring'
1049 KEYFILE = '/etc/ceph/ceph.client.{}.key'
1050
1051@@ -288,17 +293,6 @@
1052 os.chown(data_src_dst, uid, gid)
1053
1054
1055-# TODO: re-use
1056-def modprobe(module):
1057- """Load a kernel module and configure for auto-load on reboot."""
1058- log('Loading kernel module', level=INFO)
1059- cmd = ['modprobe', module]
1060- check_call(cmd)
1061- with open('/etc/modules', 'r+') as modules:
1062- if module not in modules.read():
1063- modules.write(module)
1064-
1065-
1066 def copy_files(src, dst, symlinks=False, ignore=None):
1067 """Copy files from src to dst."""
1068 for item in os.listdir(src):
1069@@ -411,17 +405,52 @@
1070
1071 The API is versioned and defaults to version 1.
1072 """
1073- def __init__(self, api_version=1):
1074+ def __init__(self, api_version=1, request_id=None):
1075 self.api_version = api_version
1076+ if request_id:
1077+ self.request_id = request_id
1078+ else:
1079+ self.request_id = str(uuid.uuid1())
1080 self.ops = []
1081
1082 def add_op_create_pool(self, name, replica_count=3):
1083 self.ops.append({'op': 'create-pool', 'name': name,
1084 'replicas': replica_count})
1085
1086+ def set_ops(self, ops):
1087+ """Set request ops to provided value.
1088+
1089+ Useful for injecting ops that come from a previous request
1090+ to allow comparisons to ensure validity.
1091+ """
1092+ self.ops = ops
1093+
1094 @property
1095 def request(self):
1096- return json.dumps({'api-version': self.api_version, 'ops': self.ops})
1097+ return json.dumps({'api-version': self.api_version, 'ops': self.ops,
1098+ 'request-id': self.request_id})
1099+
1100+ def _ops_equal(self, other):
1101+ if len(self.ops) == len(other.ops):
1102+ for req_no in range(0, len(self.ops)):
1103+ for key in ['replicas', 'name', 'op']:
1104+ if self.ops[req_no][key] != other.ops[req_no][key]:
1105+ return False
1106+ else:
1107+ return False
1108+ return True
1109+
1110+ def __eq__(self, other):
1111+ if not isinstance(other, self.__class__):
1112+ return False
1113+ if self.api_version == other.api_version and \
1114+ self._ops_equal(other):
1115+ return True
1116+ else:
1117+ return False
1118+
1119+ def __ne__(self, other):
1120+ return not self.__eq__(other)
1121
1122
1123 class CephBrokerRsp(object):
1124@@ -431,14 +460,198 @@
1125
1126 The API is versioned and defaults to version 1.
1127 """
1128+
1129 def __init__(self, encoded_rsp):
1130 self.api_version = None
1131 self.rsp = json.loads(encoded_rsp)
1132
1133 @property
1134+ def request_id(self):
1135+ return self.rsp.get('request-id')
1136+
1137+ @property
1138 def exit_code(self):
1139 return self.rsp.get('exit-code')
1140
1141 @property
1142 def exit_msg(self):
1143 return self.rsp.get('stderr')
1144+
1145+
1146+# Ceph Broker Conversation:
1147+# If a charm needs an action to be taken by ceph it can create a CephBrokerRq
1148+# and send that request to ceph via the ceph relation. The CephBrokerRq has a
1149+# unique id so that the client can identity which CephBrokerRsp is associated
1150+# with the request. Ceph will also respond to each client unit individually
1151+# creating a response key per client unit eg glance/0 will get a CephBrokerRsp
1152+# via key broker-rsp-glance-0
1153+#
1154+# To use this the charm can just do something like:
1155+#
1156+# from charmhelpers.contrib.storage.linux.ceph import (
1157+# send_request_if_needed,
1158+# is_request_complete,
1159+# CephBrokerRq,
1160+# )
1161+#
1162+# @hooks.hook('ceph-relation-changed')
1163+# def ceph_changed():
1164+# rq = CephBrokerRq()
1165+# rq.add_op_create_pool(name='poolname', replica_count=3)
1166+#
1167+# if is_request_complete(rq):
1168+# <Request complete actions>
1169+# else:
1170+# send_request_if_needed(get_ceph_request())
1171+#
1172+# CephBrokerRq and CephBrokerRsp are serialized into JSON. Below is an example
1173+# of glance having sent a request to ceph which ceph has successfully processed
1174+# 'ceph:8': {
1175+# 'ceph/0': {
1176+# 'auth': 'cephx',
1177+# 'broker-rsp-glance-0': '{"request-id": "0bc7dc54", "exit-code": 0}',
1178+# 'broker_rsp': '{"request-id": "0da543b8", "exit-code": 0}',
1179+# 'ceph-public-address': '10.5.44.103',
1180+# 'key': 'AQCLDttVuHXINhAAvI144CB09dYchhHyTUY9BQ==',
1181+# 'private-address': '10.5.44.103',
1182+# },
1183+# 'glance/0': {
1184+# 'broker_req': ('{"api-version": 1, "request-id": "0bc7dc54", '
1185+# '"ops": [{"replicas": 3, "name": "glance", '
1186+# '"op": "create-pool"}]}'),
1187+# 'private-address': '10.5.44.109',
1188+# },
1189+# }
1190+
1191+def get_previous_request(rid):
1192+ """Return the last ceph broker request sent on a given relation
1193+
1194+ @param rid: Relation id to query for request
1195+ """
1196+ request = None
1197+ broker_req = relation_get(attribute='broker_req', rid=rid,
1198+ unit=local_unit())
1199+ if broker_req:
1200+ request_data = json.loads(broker_req)
1201+ request = CephBrokerRq(api_version=request_data['api-version'],
1202+ request_id=request_data['request-id'])
1203+ request.set_ops(request_data['ops'])
1204+
1205+ return request
1206+
1207+
1208+def get_request_states(request):
1209+ """Return a dict of requests per relation id with their corresponding
1210+ completion state.
1211+
1212+ This allows a charm, which has a request for ceph, to see whether there is
1213+ an equivalent request already being processed and if so what state that
1214+ request is in.
1215+
1216+ @param request: A CephBrokerRq object
1217+ """
1218+ complete = []
1219+ requests = {}
1220+ for rid in relation_ids('ceph'):
1221+ complete = False
1222+ previous_request = get_previous_request(rid)
1223+ if request == previous_request:
1224+ sent = True
1225+ complete = is_request_complete_for_rid(previous_request, rid)
1226+ else:
1227+ sent = False
1228+ complete = False
1229+
1230+ requests[rid] = {
1231+ 'sent': sent,
1232+ 'complete': complete,
1233+ }
1234+
1235+ return requests
1236+
1237+
1238+def is_request_sent(request):
1239+ """Check to see if a functionally equivalent request has already been sent
1240+
1241+ Returns True if a similair request has been sent
1242+
1243+ @param request: A CephBrokerRq object
1244+ """
1245+ states = get_request_states(request)
1246+ for rid in states.keys():
1247+ if not states[rid]['sent']:
1248+ return False
1249+
1250+ return True
1251+
1252+
1253+def is_request_complete(request):
1254+ """Check to see if a functionally equivalent request has already been
1255+ completed
1256+
1257+ Returns True if a similair request has been completed
1258+
1259+ @param request: A CephBrokerRq object
1260+ """
1261+ states = get_request_states(request)
1262+ for rid in states.keys():
1263+ if not states[rid]['complete']:
1264+ return False
1265+
1266+ return True
1267+
1268+
1269+def is_request_complete_for_rid(request, rid):
1270+ """Check if a given request has been completed on the given relation
1271+
1272+ @param request: A CephBrokerRq object
1273+ @param rid: Relation ID
1274+ """
1275+ broker_key = get_broker_rsp_key()
1276+ for unit in related_units(rid):
1277+ rdata = relation_get(rid=rid, unit=unit)
1278+ if rdata.get(broker_key):
1279+ rsp = CephBrokerRsp(rdata.get(broker_key))
1280+ if rsp.request_id == request.request_id:
1281+ if not rsp.exit_code:
1282+ return True
1283+ else:
1284+ # The remote unit sent no reply targeted at this unit so either the
1285+ # remote ceph cluster does not support unit targeted replies or it
1286+ # has not processed our request yet.
1287+ if rdata.get('broker_rsp'):
1288+ request_data = json.loads(rdata['broker_rsp'])
1289+ if request_data.get('request-id'):
1290+ log('Ignoring legacy broker_rsp without unit key as remote '
1291+ 'service supports unit specific replies', level=DEBUG)
1292+ else:
1293+ log('Using legacy broker_rsp as remote service does not '
1294+ 'supports unit specific replies', level=DEBUG)
1295+ rsp = CephBrokerRsp(rdata['broker_rsp'])
1296+ if not rsp.exit_code:
1297+ return True
1298+
1299+ return False
1300+
1301+
1302+def get_broker_rsp_key():
1303+ """Return broker response key for this unit
1304+
1305+ This is the key that ceph is going to use to pass request status
1306+ information back to this unit
1307+ """
1308+ return 'broker-rsp-' + local_unit().replace('/', '-')
1309+
1310+
1311+def send_request_if_needed(request):
1312+ """Send broker request if an equivalent request has not already been sent
1313+
1314+ @param request: A CephBrokerRq object
1315+ """
1316+ if is_request_sent(request):
1317+ log('Request already sent but not complete, not sending new request',
1318+ level=DEBUG)
1319+ else:
1320+ for rid in relation_ids('ceph'):
1321+ log('Sending request {}'.format(request.request_id), level=DEBUG)
1322+ relation_set(relation_id=rid, broker_req=request.request)
1323
1324=== modified file 'charmhelpers/core/hookenv.py'
1325--- charmhelpers/core/hookenv.py 2015-09-03 09:40:28 +0000
1326+++ charmhelpers/core/hookenv.py 2015-09-25 14:42:33 +0000
1327@@ -623,6 +623,38 @@
1328 return unit_get('private-address')
1329
1330
1331+@cached
1332+def storage_get(attribute="", storage_id=""):
1333+ """Get storage attributes"""
1334+ _args = ['storage-get', '--format=json']
1335+ if storage_id:
1336+ _args.extend(('-s', storage_id))
1337+ if attribute:
1338+ _args.append(attribute)
1339+ try:
1340+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
1341+ except ValueError:
1342+ return None
1343+
1344+
1345+@cached
1346+def storage_list(storage_name=""):
1347+ """List the storage IDs for the unit"""
1348+ _args = ['storage-list', '--format=json']
1349+ if storage_name:
1350+ _args.append(storage_name)
1351+ try:
1352+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
1353+ except ValueError:
1354+ return None
1355+ except OSError as e:
1356+ import errno
1357+ if e.errno == errno.ENOENT:
1358+ # storage-list does not exist
1359+ return []
1360+ raise
1361+
1362+
1363 class UnregisteredHookError(Exception):
1364 """Raised when an undefined hook is called"""
1365 pass
1366
1367=== modified file 'charmhelpers/core/host.py'
1368--- charmhelpers/core/host.py 2015-08-24 08:56:12 +0000
1369+++ charmhelpers/core/host.py 2015-09-25 14:42:33 +0000
1370@@ -63,32 +63,48 @@
1371 return service_result
1372
1373
1374-def service_pause(service_name, init_dir=None):
1375+def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
1376 """Pause a system service.
1377
1378 Stop it, and prevent it from starting again at boot."""
1379- if init_dir is None:
1380- init_dir = "/etc/init"
1381 stopped = service_stop(service_name)
1382- # XXX: Support systemd too
1383- override_path = os.path.join(
1384- init_dir, '{}.override'.format(service_name))
1385- with open(override_path, 'w') as fh:
1386- fh.write("manual\n")
1387+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
1388+ sysv_file = os.path.join(initd_dir, service_name)
1389+ if os.path.exists(upstart_file):
1390+ override_path = os.path.join(
1391+ init_dir, '{}.override'.format(service_name))
1392+ with open(override_path, 'w') as fh:
1393+ fh.write("manual\n")
1394+ elif os.path.exists(sysv_file):
1395+ subprocess.check_call(["update-rc.d", service_name, "disable"])
1396+ else:
1397+ # XXX: Support SystemD too
1398+ raise ValueError(
1399+ "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
1400+ service_name, upstart_file, sysv_file))
1401 return stopped
1402
1403
1404-def service_resume(service_name, init_dir=None):
1405+def service_resume(service_name, init_dir="/etc/init",
1406+ initd_dir="/etc/init.d"):
1407 """Resume a system service.
1408
1409 Reenable starting again at boot. Start the service"""
1410- # XXX: Support systemd too
1411- if init_dir is None:
1412- init_dir = "/etc/init"
1413- override_path = os.path.join(
1414- init_dir, '{}.override'.format(service_name))
1415- if os.path.exists(override_path):
1416- os.unlink(override_path)
1417+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
1418+ sysv_file = os.path.join(initd_dir, service_name)
1419+ if os.path.exists(upstart_file):
1420+ override_path = os.path.join(
1421+ init_dir, '{}.override'.format(service_name))
1422+ if os.path.exists(override_path):
1423+ os.unlink(override_path)
1424+ elif os.path.exists(sysv_file):
1425+ subprocess.check_call(["update-rc.d", service_name, "enable"])
1426+ else:
1427+ # XXX: Support SystemD too
1428+ raise ValueError(
1429+ "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
1430+ service_name, upstart_file, sysv_file))
1431+
1432 started = service_start(service_name)
1433 return started
1434
1435
1436=== modified file 'charmhelpers/core/hugepage.py'
1437--- charmhelpers/core/hugepage.py 2015-08-19 13:49:05 +0000
1438+++ charmhelpers/core/hugepage.py 2015-09-25 14:42:33 +0000
1439@@ -25,11 +25,13 @@
1440 fstab_mount,
1441 mkdir,
1442 )
1443+from charmhelpers.core.strutils import bytes_from_string
1444+from subprocess import check_output
1445
1446
1447 def hugepage_support(user, group='hugetlb', nr_hugepages=256,
1448 max_map_count=65536, mnt_point='/run/hugepages/kvm',
1449- pagesize='2MB', mount=True):
1450+ pagesize='2MB', mount=True, set_shmmax=False):
1451 """Enable hugepages on system.
1452
1453 Args:
1454@@ -49,6 +51,11 @@
1455 'vm.max_map_count': max_map_count,
1456 'vm.hugetlb_shm_group': gid,
1457 }
1458+ if set_shmmax:
1459+ shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax']))
1460+ shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages
1461+ if shmmax_minsize > shmmax_current:
1462+ sysctl_settings['kernel.shmmax'] = shmmax_minsize
1463 sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
1464 mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
1465 lfstab = fstab.Fstab()
1466
1467=== modified file 'charmhelpers/core/strutils.py'
1468--- charmhelpers/core/strutils.py 2015-04-16 19:55:16 +0000
1469+++ charmhelpers/core/strutils.py 2015-09-25 14:42:33 +0000
1470@@ -18,6 +18,7 @@
1471 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1472
1473 import six
1474+import re
1475
1476
1477 def bool_from_string(value):
1478@@ -40,3 +41,32 @@
1479
1480 msg = "Unable to interpret string value '%s' as boolean" % (value)
1481 raise ValueError(msg)
1482+
1483+
1484+def bytes_from_string(value):
1485+ """Interpret human readable string value as bytes.
1486+
1487+ Returns int
1488+ """
1489+ BYTE_POWER = {
1490+ 'K': 1,
1491+ 'KB': 1,
1492+ 'M': 2,
1493+ 'MB': 2,
1494+ 'G': 3,
1495+ 'GB': 3,
1496+ 'T': 4,
1497+ 'TB': 4,
1498+ 'P': 5,
1499+ 'PB': 5,
1500+ }
1501+ if isinstance(value, six.string_types):
1502+ value = six.text_type(value)
1503+ else:
1504+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
1505+ raise ValueError(msg)
1506+ matches = re.match("([0-9]+)([a-zA-Z]+)", value)
1507+ if not matches:
1508+ msg = "Unable to interpret string value '%s' as bytes" % (value)
1509+ raise ValueError(msg)
1510+ return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
1511
1512=== modified file 'tests/charmhelpers/contrib/amulet/deployment.py'
1513--- tests/charmhelpers/contrib/amulet/deployment.py 2015-03-11 11:45:09 +0000
1514+++ tests/charmhelpers/contrib/amulet/deployment.py 2015-09-25 14:42:33 +0000
1515@@ -51,7 +51,8 @@
1516 if 'units' not in this_service:
1517 this_service['units'] = 1
1518
1519- self.d.add(this_service['name'], units=this_service['units'])
1520+ self.d.add(this_service['name'], units=this_service['units'],
1521+ constraints=this_service.get('constraints'))
1522
1523 for svc in other_services:
1524 if 'location' in svc:
1525@@ -64,7 +65,8 @@
1526 if 'units' not in svc:
1527 svc['units'] = 1
1528
1529- self.d.add(svc['name'], charm=branch_location, units=svc['units'])
1530+ self.d.add(svc['name'], charm=branch_location, units=svc['units'],
1531+ constraints=svc.get('constraints'))
1532
1533 def _add_relations(self, relations):
1534 """Add all of the relations for the services."""
1535
1536=== modified file 'tests/charmhelpers/contrib/amulet/utils.py'
1537--- tests/charmhelpers/contrib/amulet/utils.py 2015-09-02 02:20:28 +0000
1538+++ tests/charmhelpers/contrib/amulet/utils.py 2015-09-25 14:42:33 +0000
1539@@ -19,9 +19,11 @@
1540 import logging
1541 import os
1542 import re
1543+import socket
1544 import subprocess
1545 import sys
1546 import time
1547+import uuid
1548
1549 import amulet
1550 import distro_info
1551@@ -324,7 +326,7 @@
1552
1553 def service_restarted_since(self, sentry_unit, mtime, service,
1554 pgrep_full=None, sleep_time=20,
1555- retry_count=2, retry_sleep_time=30):
1556+ retry_count=30, retry_sleep_time=10):
1557 """Check if service was been started after a given time.
1558
1559 Args:
1560@@ -332,8 +334,9 @@
1561 mtime (float): The epoch time to check against
1562 service (string): service name to look for in process table
1563 pgrep_full: [Deprecated] Use full command line search mode with pgrep
1564- sleep_time (int): Seconds to sleep before looking for process
1565- retry_count (int): If service is not found, how many times to retry
1566+ sleep_time (int): Initial sleep time (s) before looking for file
1567+ retry_sleep_time (int): Time (s) to sleep between retries
1568+ retry_count (int): If file is not found, how many times to retry
1569
1570 Returns:
1571 bool: True if service found and its start time it newer than mtime,
1572@@ -357,11 +360,12 @@
1573 pgrep_full)
1574 self.log.debug('Attempt {} to get {} proc start time on {} '
1575 'OK'.format(tries, service, unit_name))
1576- except IOError:
1577+ except IOError as e:
1578 # NOTE(beisner) - race avoidance, proc may not exist yet.
1579 # https://bugs.launchpad.net/charm-helpers/+bug/1474030
1580 self.log.debug('Attempt {} to get {} proc start time on {} '
1581- 'failed'.format(tries, service, unit_name))
1582+ 'failed\n{}'.format(tries, service,
1583+ unit_name, e))
1584 time.sleep(retry_sleep_time)
1585 tries += 1
1586
1587@@ -381,38 +385,62 @@
1588 return False
1589
1590 def config_updated_since(self, sentry_unit, filename, mtime,
1591- sleep_time=20):
1592+ sleep_time=20, retry_count=30,
1593+ retry_sleep_time=10):
1594 """Check if file was modified after a given time.
1595
1596 Args:
1597 sentry_unit (sentry): The sentry unit to check the file mtime on
1598 filename (string): The file to check mtime of
1599 mtime (float): The epoch time to check against
1600- sleep_time (int): Seconds to sleep before looking for process
1601+ sleep_time (int): Initial sleep time (s) before looking for file
1602+ retry_sleep_time (int): Time (s) to sleep between retries
1603+ retry_count (int): If file is not found, how many times to retry
1604
1605 Returns:
1606 bool: True if file was modified more recently than mtime, False if
1607- file was modified before mtime,
1608+ file was modified before mtime, or if file not found.
1609 """
1610- self.log.debug('Checking that %s file updated since '
1611- '%s' % (filename, mtime))
1612 unit_name = sentry_unit.info['unit_name']
1613+ self.log.debug('Checking that %s updated since %s on '
1614+ '%s' % (filename, mtime, unit_name))
1615 time.sleep(sleep_time)
1616- file_mtime = self._get_file_mtime(sentry_unit, filename)
1617+ file_mtime = None
1618+ tries = 0
1619+ while tries <= retry_count and not file_mtime:
1620+ try:
1621+ file_mtime = self._get_file_mtime(sentry_unit, filename)
1622+ self.log.debug('Attempt {} to get {} file mtime on {} '
1623+ 'OK'.format(tries, filename, unit_name))
1624+ except IOError as e:
1625+ # NOTE(beisner) - race avoidance, file may not exist yet.
1626+ # https://bugs.launchpad.net/charm-helpers/+bug/1474030
1627+ self.log.debug('Attempt {} to get {} file mtime on {} '
1628+ 'failed\n{}'.format(tries, filename,
1629+ unit_name, e))
1630+ time.sleep(retry_sleep_time)
1631+ tries += 1
1632+
1633+ if not file_mtime:
1634+ self.log.warn('Could not determine file mtime, assuming '
1635+ 'file does not exist')
1636+ return False
1637+
1638 if file_mtime >= mtime:
1639 self.log.debug('File mtime is newer than provided mtime '
1640- '(%s >= %s) on %s (OK)' % (file_mtime, mtime,
1641- unit_name))
1642+ '(%s >= %s) on %s (OK)' % (file_mtime,
1643+ mtime, unit_name))
1644 return True
1645 else:
1646- self.log.warn('File mtime %s is older than provided mtime %s'
1647- % (file_mtime, mtime))
1648+ self.log.warn('File mtime is older than provided mtime'
1649+ '(%s < on %s) on %s' % (file_mtime,
1650+ mtime, unit_name))
1651 return False
1652
1653 def validate_service_config_changed(self, sentry_unit, mtime, service,
1654 filename, pgrep_full=None,
1655- sleep_time=20, retry_count=2,
1656- retry_sleep_time=30):
1657+ sleep_time=20, retry_count=30,
1658+ retry_sleep_time=10):
1659 """Check service and file were updated after mtime
1660
1661 Args:
1662@@ -457,7 +485,9 @@
1663 sentry_unit,
1664 filename,
1665 mtime,
1666- sleep_time=0)
1667+ sleep_time=sleep_time,
1668+ retry_count=retry_count,
1669+ retry_sleep_time=retry_sleep_time)
1670
1671 return service_restart and config_update
1672
1673@@ -476,7 +506,6 @@
1674 """Return a list of all Ubuntu releases in order of release."""
1675 _d = distro_info.UbuntuDistroInfo()
1676 _release_list = _d.all
1677- self.log.debug('Ubuntu release list: {}'.format(_release_list))
1678 return _release_list
1679
1680 def file_to_url(self, file_rel_path):
1681@@ -616,6 +645,142 @@
1682
1683 return None
1684
1685+ def validate_sectionless_conf(self, file_contents, expected):
1686+ """A crude conf parser. Useful to inspect configuration files which
1687+ do not have section headers (as would be necessary in order to use
1688+ the configparser). Such as openstack-dashboard or rabbitmq confs."""
1689+ for line in file_contents.split('\n'):
1690+ if '=' in line:
1691+ args = line.split('=')
1692+ if len(args) <= 1:
1693+ continue
1694+ key = args[0].strip()
1695+ value = args[1].strip()
1696+ if key in expected.keys():
1697+ if expected[key] != value:
1698+ msg = ('Config mismatch. Expected, actual: {}, '
1699+ '{}'.format(expected[key], value))
1700+ amulet.raise_status(amulet.FAIL, msg=msg)
1701+
1702+ def get_unit_hostnames(self, units):
1703+ """Return a dict of juju unit names to hostnames."""
1704+ host_names = {}
1705+ for unit in units:
1706+ host_names[unit.info['unit_name']] = \
1707+ str(unit.file_contents('/etc/hostname').strip())
1708+ self.log.debug('Unit host names: {}'.format(host_names))
1709+ return host_names
1710+
1711+ def run_cmd_unit(self, sentry_unit, cmd):
1712+ """Run a command on a unit, return the output and exit code."""
1713+ output, code = sentry_unit.run(cmd)
1714+ if code == 0:
1715+ self.log.debug('{} `{}` command returned {} '
1716+ '(OK)'.format(sentry_unit.info['unit_name'],
1717+ cmd, code))
1718+ else:
1719+ msg = ('{} `{}` command returned {} '
1720+ '{}'.format(sentry_unit.info['unit_name'],
1721+ cmd, code, output))
1722+ amulet.raise_status(amulet.FAIL, msg=msg)
1723+ return str(output), code
1724+
1725+ def file_exists_on_unit(self, sentry_unit, file_name):
1726+ """Check if a file exists on a unit."""
1727+ try:
1728+ sentry_unit.file_stat(file_name)
1729+ return True
1730+ except IOError:
1731+ return False
1732+ except Exception as e:
1733+ msg = 'Error checking file {}: {}'.format(file_name, e)
1734+ amulet.raise_status(amulet.FAIL, msg=msg)
1735+
1736+ def file_contents_safe(self, sentry_unit, file_name,
1737+ max_wait=60, fatal=False):
1738+ """Get file contents from a sentry unit. Wrap amulet file_contents
1739+ with retry logic to address races where a file checks as existing,
1740+ but no longer exists by the time file_contents is called.
1741+ Return None if file not found. Optionally raise if fatal is True."""
1742+ unit_name = sentry_unit.info['unit_name']
1743+ file_contents = False
1744+ tries = 0
1745+ while not file_contents and tries < (max_wait / 4):
1746+ try:
1747+ file_contents = sentry_unit.file_contents(file_name)
1748+ except IOError:
1749+ self.log.debug('Attempt {} to open file {} from {} '
1750+ 'failed'.format(tries, file_name,
1751+ unit_name))
1752+ time.sleep(4)
1753+ tries += 1
1754+
1755+ if file_contents:
1756+ return file_contents
1757+ elif not fatal:
1758+ return None
1759+ elif fatal:
1760+ msg = 'Failed to get file contents from unit.'
1761+ amulet.raise_status(amulet.FAIL, msg)
1762+
1763+ def port_knock_tcp(self, host="localhost", port=22, timeout=15):
1764+ """Open a TCP socket to check for a listening sevice on a host.
1765+
1766+ :param host: host name or IP address, default to localhost
1767+ :param port: TCP port number, default to 22
1768+ :param timeout: Connect timeout, default to 15 seconds
1769+ :returns: True if successful, False if connect failed
1770+ """
1771+
1772+ # Resolve host name if possible
1773+ try:
1774+ connect_host = socket.gethostbyname(host)
1775+ host_human = "{} ({})".format(connect_host, host)
1776+ except socket.error as e:
1777+ self.log.warn('Unable to resolve address: '
1778+ '{} ({}) Trying anyway!'.format(host, e))
1779+ connect_host = host
1780+ host_human = connect_host
1781+
1782+ # Attempt socket connection
1783+ try:
1784+ knock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1785+ knock.settimeout(timeout)
1786+ knock.connect((connect_host, port))
1787+ knock.close()
1788+ self.log.debug('Socket connect OK for host '
1789+ '{} on port {}.'.format(host_human, port))
1790+ return True
1791+ except socket.error as e:
1792+ self.log.debug('Socket connect FAIL for'
1793+ ' {} port {} ({})'.format(host_human, port, e))
1794+ return False
1795+
1796+ def port_knock_units(self, sentry_units, port=22,
1797+ timeout=15, expect_success=True):
1798+ """Open a TCP socket to check for a listening sevice on each
1799+ listed juju unit.
1800+
1801+ :param sentry_units: list of sentry unit pointers
1802+ :param port: TCP port number, default to 22
1803+ :param timeout: Connect timeout, default to 15 seconds
1804+ :expect_success: True by default, set False to invert logic
1805+ :returns: None if successful, Failure message otherwise
1806+ """
1807+ for unit in sentry_units:
1808+ host = unit.info['public-address']
1809+ connected = self.port_knock_tcp(host, port, timeout)
1810+ if not connected and expect_success:
1811+ return 'Socket connect failed.'
1812+ elif connected and not expect_success:
1813+ return 'Socket connected unexpectedly.'
1814+
1815+ def get_uuid_epoch_stamp(self):
1816+ """Returns a stamp string based on uuid4 and epoch time. Useful in
1817+ generating test messages which need to be unique-ish."""
1818+ return '[{}-{}]'.format(uuid.uuid4(), time.time())
1819+
1820+# amulet juju action helpers:
1821 def run_action(self, unit_sentry, action,
1822 _check_output=subprocess.check_output):
1823 """Run the named action on a given unit sentry.
1824@@ -642,3 +807,12 @@
1825 output = _check_output(command, universal_newlines=True)
1826 data = json.loads(output)
1827 return data.get(u"status") == "completed"
1828+
1829+ def status_get(self, unit):
1830+ """Return the current service status of this unit."""
1831+ raw_status, return_code = unit.run(
1832+ "status-get --format=json --include-data")
1833+ if return_code != 0:
1834+ return ("unknown", "")
1835+ status = json.loads(raw_status)
1836+ return (status["status"], status["message"])
1837
1838=== modified file 'tests/charmhelpers/contrib/openstack/amulet/deployment.py'
1839--- tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-08-19 13:53:50 +0000
1840+++ tests/charmhelpers/contrib/openstack/amulet/deployment.py 2015-09-25 14:42:33 +0000
1841@@ -44,20 +44,31 @@
1842 Determine if the local branch being tested is derived from its
1843 stable or next (dev) branch, and based on this, use the corresonding
1844 stable or next branches for the other_services."""
1845+
1846+ # Charms outside the lp:~openstack-charmers namespace
1847 base_charms = ['mysql', 'mongodb', 'nrpe']
1848
1849+ # Force these charms to current series even when using an older series.
1850+ # ie. Use trusty/nrpe even when series is precise, as the P charm
1851+ # does not possess the necessary external master config and hooks.
1852+ force_series_current = ['nrpe']
1853+
1854 if self.series in ['precise', 'trusty']:
1855 base_series = self.series
1856 else:
1857 base_series = self.current_next
1858
1859- if self.stable:
1860- for svc in other_services:
1861+ for svc in other_services:
1862+ if svc['name'] in force_series_current:
1863+ base_series = self.current_next
1864+ # If a location has been explicitly set, use it
1865+ if svc.get('location'):
1866+ continue
1867+ if self.stable:
1868 temp = 'lp:charms/{}/{}'
1869 svc['location'] = temp.format(base_series,
1870 svc['name'])
1871- else:
1872- for svc in other_services:
1873+ else:
1874 if svc['name'] in base_charms:
1875 temp = 'lp:charms/{}/{}'
1876 svc['location'] = temp.format(base_series,
1877@@ -66,6 +77,7 @@
1878 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
1879 svc['location'] = temp.format(self.current_next,
1880 svc['name'])
1881+
1882 return other_services
1883
1884 def _add_services(self, this_service, other_services):
1885@@ -77,21 +89,23 @@
1886
1887 services = other_services
1888 services.append(this_service)
1889+
1890+ # Charms which should use the source config option
1891 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
1892 'ceph-osd', 'ceph-radosgw']
1893- # Most OpenStack subordinate charms do not expose an origin option
1894- # as that is controlled by the principle.
1895- ignore = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
1896+
1897+ # Charms which can not use openstack-origin, ie. many subordinates
1898+ no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
1899
1900 if self.openstack:
1901 for svc in services:
1902- if svc['name'] not in use_source + ignore:
1903+ if svc['name'] not in use_source + no_origin:
1904 config = {'openstack-origin': self.openstack}
1905 self.d.configure(svc['name'], config)
1906
1907 if self.source:
1908 for svc in services:
1909- if svc['name'] in use_source and svc['name'] not in ignore:
1910+ if svc['name'] in use_source and svc['name'] not in no_origin:
1911 config = {'source': self.source}
1912 self.d.configure(svc['name'], config)
1913
1914
1915=== modified file 'tests/charmhelpers/contrib/openstack/amulet/utils.py'
1916--- tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-07-01 04:03:20 +0000
1917+++ tests/charmhelpers/contrib/openstack/amulet/utils.py 2015-09-25 14:42:33 +0000
1918@@ -27,6 +27,7 @@
1919 import heatclient.v1.client as heat_client
1920 import keystoneclient.v2_0 as keystone_client
1921 import novaclient.v1_1.client as nova_client
1922+import pika
1923 import swiftclient
1924
1925 from charmhelpers.contrib.amulet.utils import (
1926@@ -602,3 +603,361 @@
1927 self.log.debug('Ceph {} samples (OK): '
1928 '{}'.format(sample_type, samples))
1929 return None
1930+
1931+# rabbitmq/amqp specific helpers:
1932+ def add_rmq_test_user(self, sentry_units,
1933+ username="testuser1", password="changeme"):
1934+ """Add a test user via the first rmq juju unit, check connection as
1935+ the new user against all sentry units.
1936+
1937+ :param sentry_units: list of sentry unit pointers
1938+ :param username: amqp user name, default to testuser1
1939+ :param password: amqp user password
1940+ :returns: None if successful. Raise on error.
1941+ """
1942+ self.log.debug('Adding rmq user ({})...'.format(username))
1943+
1944+ # Check that user does not already exist
1945+ cmd_user_list = 'rabbitmqctl list_users'
1946+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
1947+ if username in output:
1948+ self.log.warning('User ({}) already exists, returning '
1949+ 'gracefully.'.format(username))
1950+ return
1951+
1952+ perms = '".*" ".*" ".*"'
1953+ cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
1954+ 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
1955+
1956+ # Add user via first unit
1957+ for cmd in cmds:
1958+ output, _ = self.run_cmd_unit(sentry_units[0], cmd)
1959+
1960+ # Check connection against the other sentry_units
1961+ self.log.debug('Checking user connect against units...')
1962+ for sentry_unit in sentry_units:
1963+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
1964+ username=username,
1965+ password=password)
1966+ connection.close()
1967+
1968+ def delete_rmq_test_user(self, sentry_units, username="testuser1"):
1969+ """Delete a rabbitmq user via the first rmq juju unit.
1970+
1971+ :param sentry_units: list of sentry unit pointers
1972+ :param username: amqp user name, default to testuser1
1973+ :param password: amqp user password
1974+ :returns: None if successful or no such user.
1975+ """
1976+ self.log.debug('Deleting rmq user ({})...'.format(username))
1977+
1978+ # Check that the user exists
1979+ cmd_user_list = 'rabbitmqctl list_users'
1980+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
1981+
1982+ if username not in output:
1983+ self.log.warning('User ({}) does not exist, returning '
1984+ 'gracefully.'.format(username))
1985+ return
1986+
1987+ # Delete the user
1988+ cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
1989+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
1990+
1991+ def get_rmq_cluster_status(self, sentry_unit):
1992+ """Execute rabbitmq cluster status command on a unit and return
1993+ the full output.
1994+
1995+ :param unit: sentry unit
1996+ :returns: String containing console output of cluster status command
1997+ """
1998+ cmd = 'rabbitmqctl cluster_status'
1999+ output, _ = self.run_cmd_unit(sentry_unit, cmd)
2000+ self.log.debug('{} cluster_status:\n{}'.format(
2001+ sentry_unit.info['unit_name'], output))
2002+ return str(output)
2003+
2004+ def get_rmq_cluster_running_nodes(self, sentry_unit):
2005+ """Parse rabbitmqctl cluster_status output string, return list of
2006+ running rabbitmq cluster nodes.
2007+
2008+ :param unit: sentry unit
2009+ :returns: List containing node names of running nodes
2010+ """
2011+ # NOTE(beisner): rabbitmqctl cluster_status output is not
2012+ # json-parsable, do string chop foo, then json.loads that.
2013+ str_stat = self.get_rmq_cluster_status(sentry_unit)
2014+ if 'running_nodes' in str_stat:
2015+ pos_start = str_stat.find("{running_nodes,") + 15
2016+ pos_end = str_stat.find("]},", pos_start) + 1
2017+ str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"')
2018+ run_nodes = json.loads(str_run_nodes)
2019+ return run_nodes
2020+ else:
2021+ return []
2022+
2023+ def validate_rmq_cluster_running_nodes(self, sentry_units):
2024+ """Check that all rmq unit hostnames are represented in the
2025+ cluster_status output of all units.
2026+
2027+ :param host_names: dict of juju unit names to host names
2028+ :param units: list of sentry unit pointers (all rmq units)
2029+ :returns: None if successful, otherwise return error message
2030+ """
2031+ host_names = self.get_unit_hostnames(sentry_units)
2032+ errors = []
2033+
2034+ # Query every unit for cluster_status running nodes
2035+ for query_unit in sentry_units:
2036+ query_unit_name = query_unit.info['unit_name']
2037+ running_nodes = self.get_rmq_cluster_running_nodes(query_unit)
2038+
2039+ # Confirm that every unit is represented in the queried unit's
2040+ # cluster_status running nodes output.
2041+ for validate_unit in sentry_units:
2042+ val_host_name = host_names[validate_unit.info['unit_name']]
2043+ val_node_name = 'rabbit@{}'.format(val_host_name)
2044+
2045+ if val_node_name not in running_nodes:
2046+ errors.append('Cluster member check failed on {}: {} not '
2047+ 'in {}\n'.format(query_unit_name,
2048+ val_node_name,
2049+ running_nodes))
2050+ if errors:
2051+ return ''.join(errors)
2052+
2053+ def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None):
2054+ """Check a single juju rmq unit for ssl and port in the config file."""
2055+ host = sentry_unit.info['public-address']
2056+ unit_name = sentry_unit.info['unit_name']
2057+
2058+ conf_file = '/etc/rabbitmq/rabbitmq.config'
2059+ conf_contents = str(self.file_contents_safe(sentry_unit,
2060+ conf_file, max_wait=16))
2061+ # Checks
2062+ conf_ssl = 'ssl' in conf_contents
2063+ conf_port = str(port) in conf_contents
2064+
2065+ # Port explicitly checked in config
2066+ if port and conf_port and conf_ssl:
2067+ self.log.debug('SSL is enabled @{}:{} '
2068+ '({})'.format(host, port, unit_name))
2069+ return True
2070+ elif port and not conf_port and conf_ssl:
2071+ self.log.debug('SSL is enabled @{} but not on port {} '
2072+ '({})'.format(host, port, unit_name))
2073+ return False
2074+ # Port not checked (useful when checking that ssl is disabled)
2075+ elif not port and conf_ssl:
2076+ self.log.debug('SSL is enabled @{}:{} '
2077+ '({})'.format(host, port, unit_name))
2078+ return True
2079+ elif not port and not conf_ssl:
2080+ self.log.debug('SSL not enabled @{}:{} '
2081+ '({})'.format(host, port, unit_name))
2082+ return False
2083+ else:
2084+ msg = ('Unknown condition when checking SSL status @{}:{} '
2085+ '({})'.format(host, port, unit_name))
2086+ amulet.raise_status(amulet.FAIL, msg)
2087+
2088+ def validate_rmq_ssl_enabled_units(self, sentry_units, port=None):
2089+ """Check that ssl is enabled on rmq juju sentry units.
2090+
2091+ :param sentry_units: list of all rmq sentry units
2092+ :param port: optional ssl port override to validate
2093+ :returns: None if successful, otherwise return error message
2094+ """
2095+ for sentry_unit in sentry_units:
2096+ if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port):
2097+ return ('Unexpected condition: ssl is disabled on unit '
2098+ '({})'.format(sentry_unit.info['unit_name']))
2099+ return None
2100+
2101+ def validate_rmq_ssl_disabled_units(self, sentry_units):
2102+ """Check that ssl is enabled on listed rmq juju sentry units.
2103+
2104+ :param sentry_units: list of all rmq sentry units
2105+ :returns: True if successful. Raise on error.
2106+ """
2107+ for sentry_unit in sentry_units:
2108+ if self.rmq_ssl_is_enabled_on_unit(sentry_unit):
2109+ return ('Unexpected condition: ssl is enabled on unit '
2110+ '({})'.format(sentry_unit.info['unit_name']))
2111+ return None
2112+
2113+ def configure_rmq_ssl_on(self, sentry_units, deployment,
2114+ port=None, max_wait=60):
2115+ """Turn ssl charm config option on, with optional non-default
2116+ ssl port specification. Confirm that it is enabled on every
2117+ unit.
2118+
2119+ :param sentry_units: list of sentry units
2120+ :param deployment: amulet deployment object pointer
2121+ :param port: amqp port, use defaults if None
2122+ :param max_wait: maximum time to wait in seconds to confirm
2123+ :returns: None if successful. Raise on error.
2124+ """
2125+ self.log.debug('Setting ssl charm config option: on')
2126+
2127+ # Enable RMQ SSL
2128+ config = {'ssl': 'on'}
2129+ if port:
2130+ config['ssl_port'] = port
2131+
2132+ deployment.configure('rabbitmq-server', config)
2133+
2134+ # Confirm
2135+ tries = 0
2136+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
2137+ while ret and tries < (max_wait / 4):
2138+ time.sleep(4)
2139+ self.log.debug('Attempt {}: {}'.format(tries, ret))
2140+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
2141+ tries += 1
2142+
2143+ if ret:
2144+ amulet.raise_status(amulet.FAIL, ret)
2145+
2146+ def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60):
2147+ """Turn ssl charm config option off, confirm that it is disabled
2148+ on every unit.
2149+
2150+ :param sentry_units: list of sentry units
2151+ :param deployment: amulet deployment object pointer
2152+ :param max_wait: maximum time to wait in seconds to confirm
2153+ :returns: None if successful. Raise on error.
2154+ """
2155+ self.log.debug('Setting ssl charm config option: off')
2156+
2157+ # Disable RMQ SSL
2158+ config = {'ssl': 'off'}
2159+ deployment.configure('rabbitmq-server', config)
2160+
2161+ # Confirm
2162+ tries = 0
2163+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
2164+ while ret and tries < (max_wait / 4):
2165+ time.sleep(4)
2166+ self.log.debug('Attempt {}: {}'.format(tries, ret))
2167+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
2168+ tries += 1
2169+
2170+ if ret:
2171+ amulet.raise_status(amulet.FAIL, ret)
2172+
2173+ def connect_amqp_by_unit(self, sentry_unit, ssl=False,
2174+ port=None, fatal=True,
2175+ username="testuser1", password="changeme"):
2176+ """Establish and return a pika amqp connection to the rabbitmq service
2177+ running on a rmq juju unit.
2178+
2179+ :param sentry_unit: sentry unit pointer
2180+ :param ssl: boolean, default to False
2181+ :param port: amqp port, use defaults if None
2182+ :param fatal: boolean, default to True (raises on connect error)
2183+ :param username: amqp user name, default to testuser1
2184+ :param password: amqp user password
2185+ :returns: pika amqp connection pointer or None if failed and non-fatal
2186+ """
2187+ host = sentry_unit.info['public-address']
2188+ unit_name = sentry_unit.info['unit_name']
2189+
2190+ # Default port logic if port is not specified
2191+ if ssl and not port:
2192+ port = 5671
2193+ elif not ssl and not port:
2194+ port = 5672
2195+
2196+ self.log.debug('Connecting to amqp on {}:{} ({}) as '
2197+ '{}...'.format(host, port, unit_name, username))
2198+
2199+ try:
2200+ credentials = pika.PlainCredentials(username, password)
2201+ parameters = pika.ConnectionParameters(host=host, port=port,
2202+ credentials=credentials,
2203+ ssl=ssl,
2204+ connection_attempts=3,
2205+ retry_delay=5,
2206+ socket_timeout=1)
2207+ connection = pika.BlockingConnection(parameters)
2208+ assert connection.server_properties['product'] == 'RabbitMQ'
2209+ self.log.debug('Connect OK')
2210+ return connection
2211+ except Exception as e:
2212+ msg = ('amqp connection failed to {}:{} as '
2213+ '{} ({})'.format(host, port, username, str(e)))
2214+ if fatal:
2215+ amulet.raise_status(amulet.FAIL, msg)
2216+ else:
2217+ self.log.warn(msg)
2218+ return None
2219+
2220+ def publish_amqp_message_by_unit(self, sentry_unit, message,
2221+ queue="test", ssl=False,
2222+ username="testuser1",
2223+ password="changeme",
2224+ port=None):
2225+ """Publish an amqp message to a rmq juju unit.
2226+
2227+ :param sentry_unit: sentry unit pointer
2228+ :param message: amqp message string
2229+ :param queue: message queue, default to test
2230+ :param username: amqp user name, default to testuser1
2231+ :param password: amqp user password
2232+ :param ssl: boolean, default to False
2233+ :param port: amqp port, use defaults if None
2234+ :returns: None. Raises exception if publish failed.
2235+ """
2236+ self.log.debug('Publishing message to {} queue:\n{}'.format(queue,
2237+ message))
2238+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
2239+ port=port,
2240+ username=username,
2241+ password=password)
2242+
2243+ # NOTE(beisner): extra debug here re: pika hang potential:
2244+ # https://github.com/pika/pika/issues/297
2245+ # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw
2246+ self.log.debug('Defining channel...')
2247+ channel = connection.channel()
2248+ self.log.debug('Declaring queue...')
2249+ channel.queue_declare(queue=queue, auto_delete=False, durable=True)
2250+ self.log.debug('Publishing message...')
2251+ channel.basic_publish(exchange='', routing_key=queue, body=message)
2252+ self.log.debug('Closing channel...')
2253+ channel.close()
2254+ self.log.debug('Closing connection...')
2255+ connection.close()
2256+
2257+ def get_amqp_message_by_unit(self, sentry_unit, queue="test",
2258+ username="testuser1",
2259+ password="changeme",
2260+ ssl=False, port=None):
2261+ """Get an amqp message from a rmq juju unit.
2262+
2263+ :param sentry_unit: sentry unit pointer
2264+ :param queue: message queue, default to test
2265+ :param username: amqp user name, default to testuser1
2266+ :param password: amqp user password
2267+ :param ssl: boolean, default to False
2268+ :param port: amqp port, use defaults if None
2269+ :returns: amqp message body as string. Raise if get fails.
2270+ """
2271+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
2272+ port=port,
2273+ username=username,
2274+ password=password)
2275+ channel = connection.channel()
2276+ method_frame, _, body = channel.basic_get(queue)
2277+
2278+ if method_frame:
2279+ self.log.debug('Retreived message from {} queue:\n{}'.format(queue,
2280+ body))
2281+ channel.basic_ack(method_frame.delivery_tag)
2282+ channel.close()
2283+ connection.close()
2284+ return body
2285+ else:
2286+ msg = 'No message retrieved.'
2287+ amulet.raise_status(amulet.FAIL, msg)
2288
2289=== modified file 'unit_tests/test_keystone_hooks.py'
2290--- unit_tests/test_keystone_hooks.py 2015-08-14 16:10:45 +0000
2291+++ unit_tests/test_keystone_hooks.py 2015-09-25 14:42:33 +0000
2292@@ -142,7 +142,8 @@
2293
2294 cfg_dict = {'prefer-ipv6': False,
2295 'database': 'keystone',
2296- 'database-user': 'keystone'}
2297+ 'database-user': 'keystone',
2298+ 'vip': None}
2299
2300 class mock_cls_config():
2301 def __call__(self, key):

Subscribers

People subscribed via source and target branches