Merge lp:~james-page/charms/trusty/nova-compute-vmware/resync into lp:~openstack-charmers/charms/trusty/nova-compute-vmware/next

Proposed by James Page
Status: Merged
Merged at revision: 111
Proposed branch: lp:~james-page/charms/trusty/nova-compute-vmware/resync
Merge into: lp:~openstack-charmers/charms/trusty/nova-compute-vmware/next
Diff against target: 7005 lines (+4730/-512)
58 files modified
hooks/charmhelpers/__init__.py (+16/-0)
hooks/charmhelpers/contrib/__init__.py (+15/-0)
hooks/charmhelpers/contrib/hahelpers/__init__.py (+15/-0)
hooks/charmhelpers/contrib/hahelpers/apache.py (+16/-0)
hooks/charmhelpers/contrib/hahelpers/cluster.py (+73/-5)
hooks/charmhelpers/contrib/network/__init__.py (+15/-0)
hooks/charmhelpers/contrib/network/ip.py (+104/-1)
hooks/charmhelpers/contrib/network/ovs/__init__.py (+16/-0)
hooks/charmhelpers/contrib/network/ufw.py (+140/-22)
hooks/charmhelpers/contrib/openstack/__init__.py (+15/-0)
hooks/charmhelpers/contrib/openstack/alternatives.py (+16/-0)
hooks/charmhelpers/contrib/openstack/amulet/__init__.py (+15/-0)
hooks/charmhelpers/contrib/openstack/amulet/deployment.py (+118/-13)
hooks/charmhelpers/contrib/openstack/amulet/utils.py (+736/-51)
hooks/charmhelpers/contrib/openstack/context.py (+437/-59)
hooks/charmhelpers/contrib/openstack/files/__init__.py (+18/-0)
hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh (+32/-0)
hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh (+30/-0)
hooks/charmhelpers/contrib/openstack/ip.py (+65/-7)
hooks/charmhelpers/contrib/openstack/neutron.py (+136/-3)
hooks/charmhelpers/contrib/openstack/templates/__init__.py (+16/-0)
hooks/charmhelpers/contrib/openstack/templates/ceph.conf (+6/-6)
hooks/charmhelpers/contrib/openstack/templates/git.upstart (+17/-0)
hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken (+9/-0)
hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo (+22/-0)
hooks/charmhelpers/contrib/openstack/templates/section-zeromq (+14/-0)
hooks/charmhelpers/contrib/openstack/templating.py (+46/-3)
hooks/charmhelpers/contrib/openstack/utils.py (+467/-169)
hooks/charmhelpers/contrib/python/__init__.py (+15/-0)
hooks/charmhelpers/contrib/python/packages.py (+50/-6)
hooks/charmhelpers/contrib/storage/__init__.py (+15/-0)
hooks/charmhelpers/contrib/storage/linux/__init__.py (+15/-0)
hooks/charmhelpers/contrib/storage/linux/ceph.py (+248/-19)
hooks/charmhelpers/contrib/storage/linux/loopback.py (+16/-0)
hooks/charmhelpers/contrib/storage/linux/lvm.py (+16/-0)
hooks/charmhelpers/contrib/storage/linux/utils.py (+20/-3)
hooks/charmhelpers/core/__init__.py (+15/-0)
hooks/charmhelpers/core/decorators.py (+16/-0)
hooks/charmhelpers/core/files.py (+45/-0)
hooks/charmhelpers/core/fstab.py (+19/-3)
hooks/charmhelpers/core/hookenv.py (+389/-43)
hooks/charmhelpers/core/host.py (+199/-32)
hooks/charmhelpers/core/hugepage.py (+62/-0)
hooks/charmhelpers/core/kernel.py (+68/-0)
hooks/charmhelpers/core/services/__init__.py (+16/-0)
hooks/charmhelpers/core/services/base.py (+59/-19)
hooks/charmhelpers/core/services/helpers.py (+46/-6)
hooks/charmhelpers/core/strutils.py (+42/-0)
hooks/charmhelpers/core/sysctl.py (+28/-6)
hooks/charmhelpers/core/templating.py (+19/-3)
hooks/charmhelpers/core/unitdata.py (+521/-0)
hooks/charmhelpers/fetch/__init__.py (+48/-15)
hooks/charmhelpers/fetch/archiveurl.py (+33/-11)
hooks/charmhelpers/fetch/bzrurl.py (+25/-1)
hooks/charmhelpers/fetch/giturl.py (+27/-5)
hooks/charmhelpers/payload/__init__.py (+16/-0)
hooks/charmhelpers/payload/execd.py (+16/-0)
unit_tests/test_nova_vmware_contexts.py (+1/-1)
To merge this branch: bzr merge lp:~james-page/charms/trusty/nova-compute-vmware/resync
Reviewer Review Type Date Requested Status
Billy Olsen Approve
OpenStack Charmers Pending
Review via email: mp+271791@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Billy Olsen (billy-olsen) wrote :

Looks to be a simple c-h sync. Unit tests pass, looks fairly straightforward. I'll let OSCI vote before merging, but +1 from me.

review: Approve
Revision history for this message
Billy Olsen (billy-olsen) wrote :

Realized that OSCI vote won't touch this other than unit-tests + lint, which I've already run. Lint fails with warnings, but not due to this change. Will merge.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/charmhelpers/__init__.py'
2--- hooks/charmhelpers/__init__.py 2014-12-11 17:48:55 +0000
3+++ hooks/charmhelpers/__init__.py 2015-09-21 10:47:21 +0000
4@@ -1,3 +1,19 @@
5+# Copyright 2014-2015 Canonical Limited.
6+#
7+# This file is part of charm-helpers.
8+#
9+# charm-helpers is free software: you can redistribute it and/or modify
10+# it under the terms of the GNU Lesser General Public License version 3 as
11+# published by the Free Software Foundation.
12+#
13+# charm-helpers is distributed in the hope that it will be useful,
14+# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+# GNU Lesser General Public License for more details.
17+#
18+# You should have received a copy of the GNU Lesser General Public License
19+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
20+
21 # Bootstrap charm-helpers, installing its dependencies if necessary using
22 # only standard libraries.
23 import subprocess
24
25=== modified file 'hooks/charmhelpers/contrib/__init__.py'
26--- hooks/charmhelpers/contrib/__init__.py 2013-07-19 02:37:30 +0000
27+++ hooks/charmhelpers/contrib/__init__.py 2015-09-21 10:47:21 +0000
28@@ -0,0 +1,15 @@
29+# Copyright 2014-2015 Canonical Limited.
30+#
31+# This file is part of charm-helpers.
32+#
33+# charm-helpers is free software: you can redistribute it and/or modify
34+# it under the terms of the GNU Lesser General Public License version 3 as
35+# published by the Free Software Foundation.
36+#
37+# charm-helpers is distributed in the hope that it will be useful,
38+# but WITHOUT ANY WARRANTY; without even the implied warranty of
39+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
40+# GNU Lesser General Public License for more details.
41+#
42+# You should have received a copy of the GNU Lesser General Public License
43+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
44
45=== modified file 'hooks/charmhelpers/contrib/hahelpers/__init__.py'
46--- hooks/charmhelpers/contrib/hahelpers/__init__.py 2013-07-19 02:37:30 +0000
47+++ hooks/charmhelpers/contrib/hahelpers/__init__.py 2015-09-21 10:47:21 +0000
48@@ -0,0 +1,15 @@
49+# Copyright 2014-2015 Canonical Limited.
50+#
51+# This file is part of charm-helpers.
52+#
53+# charm-helpers is free software: you can redistribute it and/or modify
54+# it under the terms of the GNU Lesser General Public License version 3 as
55+# published by the Free Software Foundation.
56+#
57+# charm-helpers is distributed in the hope that it will be useful,
58+# but WITHOUT ANY WARRANTY; without even the implied warranty of
59+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
60+# GNU Lesser General Public License for more details.
61+#
62+# You should have received a copy of the GNU Lesser General Public License
63+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
64
65=== modified file 'hooks/charmhelpers/contrib/hahelpers/apache.py'
66--- hooks/charmhelpers/contrib/hahelpers/apache.py 2014-10-23 17:30:13 +0000
67+++ hooks/charmhelpers/contrib/hahelpers/apache.py 2015-09-21 10:47:21 +0000
68@@ -1,3 +1,19 @@
69+# Copyright 2014-2015 Canonical Limited.
70+#
71+# This file is part of charm-helpers.
72+#
73+# charm-helpers is free software: you can redistribute it and/or modify
74+# it under the terms of the GNU Lesser General Public License version 3 as
75+# published by the Free Software Foundation.
76+#
77+# charm-helpers is distributed in the hope that it will be useful,
78+# but WITHOUT ANY WARRANTY; without even the implied warranty of
79+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
80+# GNU Lesser General Public License for more details.
81+#
82+# You should have received a copy of the GNU Lesser General Public License
83+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
84+
85 #
86 # Copyright 2012 Canonical Ltd.
87 #
88
89=== modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
90--- hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-01-14 15:30:19 +0000
91+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-09-21 10:47:21 +0000
92@@ -1,3 +1,19 @@
93+# Copyright 2014-2015 Canonical Limited.
94+#
95+# This file is part of charm-helpers.
96+#
97+# charm-helpers is free software: you can redistribute it and/or modify
98+# it under the terms of the GNU Lesser General Public License version 3 as
99+# published by the Free Software Foundation.
100+#
101+# charm-helpers is distributed in the hope that it will be useful,
102+# but WITHOUT ANY WARRANTY; without even the implied warranty of
103+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
104+# GNU Lesser General Public License for more details.
105+#
106+# You should have received a copy of the GNU Lesser General Public License
107+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
108+
109 #
110 # Copyright 2012 Canonical Ltd.
111 #
112@@ -28,10 +44,16 @@
113 ERROR,
114 WARNING,
115 unit_get,
116+ is_leader as juju_is_leader
117 )
118 from charmhelpers.core.decorators import (
119 retry_on_exception,
120 )
121+from charmhelpers.core.strutils import (
122+ bool_from_string,
123+)
124+
125+DC_RESOURCE_NAME = 'DC'
126
127
128 class HAIncompleteConfig(Exception):
129@@ -42,17 +64,30 @@
130 pass
131
132
133+class CRMDCNotFound(Exception):
134+ pass
135+
136+
137 def is_elected_leader(resource):
138 """
139 Returns True if the charm executing this is the elected cluster leader.
140
141 It relies on two mechanisms to determine leadership:
142- 1. If the charm is part of a corosync cluster, call corosync to
143+ 1. If juju is sufficiently new and leadership election is supported,
144+ the is_leader command will be used.
145+ 2. If the charm is part of a corosync cluster, call corosync to
146 determine leadership.
147- 2. If the charm is not part of a corosync cluster, the leader is
148+ 3. If the charm is not part of a corosync cluster, the leader is
149 determined as being "the alive unit with the lowest unit numer". In
150 other words, the oldest surviving unit.
151 """
152+ try:
153+ return juju_is_leader()
154+ except NotImplementedError:
155+ log('Juju leadership election feature not enabled'
156+ ', using fallback support',
157+ level=WARNING)
158+
159 if is_clustered():
160 if not is_crm_leader(resource):
161 log('Deferring action to CRM leader.', level=INFO)
162@@ -76,7 +111,33 @@
163 return False
164
165
166-@retry_on_exception(5, base_delay=2, exc_type=CRMResourceNotFound)
167+def is_crm_dc():
168+ """
169+ Determine leadership by querying the pacemaker Designated Controller
170+ """
171+ cmd = ['crm', 'status']
172+ try:
173+ status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
174+ if not isinstance(status, six.text_type):
175+ status = six.text_type(status, "utf-8")
176+ except subprocess.CalledProcessError as ex:
177+ raise CRMDCNotFound(str(ex))
178+
179+ current_dc = ''
180+ for line in status.split('\n'):
181+ if line.startswith('Current DC'):
182+ # Current DC: juju-lytrusty-machine-2 (168108163) - partition with quorum
183+ current_dc = line.split(':')[1].split()[0]
184+ if current_dc == get_unit_hostname():
185+ return True
186+ elif current_dc == 'NONE':
187+ raise CRMDCNotFound('Current DC: NONE')
188+
189+ return False
190+
191+
192+@retry_on_exception(5, base_delay=2,
193+ exc_type=(CRMResourceNotFound, CRMDCNotFound))
194 def is_crm_leader(resource, retry=False):
195 """
196 Returns True if the charm calling this is the elected corosync leader,
197@@ -85,6 +146,8 @@
198 We allow this operation to be retried to avoid the possibility of getting a
199 false negative. See LP #1396246 for more info.
200 """
201+ if resource == DC_RESOURCE_NAME:
202+ return is_crm_dc()
203 cmd = ['crm', 'resource', 'show', resource]
204 try:
205 status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
206@@ -148,7 +211,8 @@
207 .
208 returns: boolean
209 '''
210- if config_get('use-https') == "yes":
211+ use_https = config_get('use-https')
212+ if use_https and bool_from_string(use_https):
213 return True
214 if config_get('ssl_cert') and config_get('ssl_key'):
215 return True
216@@ -205,19 +269,23 @@
217 return public_port - (i * 10)
218
219
220-def get_hacluster_config():
221+def get_hacluster_config(exclude_keys=None):
222 '''
223 Obtains all relevant configuration from charm configuration required
224 for initiating a relation to hacluster:
225
226 ha-bindiface, ha-mcastport, vip
227
228+ param: exclude_keys: list of setting key(s) to be excluded.
229 returns: dict: A dict containing settings keyed by setting name.
230 raises: HAIncompleteConfig if settings are missing.
231 '''
232 settings = ['ha-bindiface', 'ha-mcastport', 'vip']
233 conf = {}
234 for setting in settings:
235+ if exclude_keys and setting in exclude_keys:
236+ continue
237+
238 conf[setting] = config_get(setting)
239 missing = []
240 [missing.append(s) for s, v in six.iteritems(conf) if v is None]
241
242=== modified file 'hooks/charmhelpers/contrib/network/__init__.py'
243--- hooks/charmhelpers/contrib/network/__init__.py 2014-10-23 17:30:13 +0000
244+++ hooks/charmhelpers/contrib/network/__init__.py 2015-09-21 10:47:21 +0000
245@@ -0,0 +1,15 @@
246+# Copyright 2014-2015 Canonical Limited.
247+#
248+# This file is part of charm-helpers.
249+#
250+# charm-helpers is free software: you can redistribute it and/or modify
251+# it under the terms of the GNU Lesser General Public License version 3 as
252+# published by the Free Software Foundation.
253+#
254+# charm-helpers is distributed in the hope that it will be useful,
255+# but WITHOUT ANY WARRANTY; without even the implied warranty of
256+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
257+# GNU Lesser General Public License for more details.
258+#
259+# You should have received a copy of the GNU Lesser General Public License
260+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
261
262=== modified file 'hooks/charmhelpers/contrib/network/ip.py'
263--- hooks/charmhelpers/contrib/network/ip.py 2014-12-10 20:28:57 +0000
264+++ hooks/charmhelpers/contrib/network/ip.py 2015-09-21 10:47:21 +0000
265@@ -1,13 +1,32 @@
266+# Copyright 2014-2015 Canonical Limited.
267+#
268+# This file is part of charm-helpers.
269+#
270+# charm-helpers is free software: you can redistribute it and/or modify
271+# it under the terms of the GNU Lesser General Public License version 3 as
272+# published by the Free Software Foundation.
273+#
274+# charm-helpers is distributed in the hope that it will be useful,
275+# but WITHOUT ANY WARRANTY; without even the implied warranty of
276+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
277+# GNU Lesser General Public License for more details.
278+#
279+# You should have received a copy of the GNU Lesser General Public License
280+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
281+
282 import glob
283 import re
284 import subprocess
285+import six
286+import socket
287
288 from functools import partial
289
290 from charmhelpers.core.hookenv import unit_get
291 from charmhelpers.fetch import apt_install
292 from charmhelpers.core.hookenv import (
293- log
294+ log,
295+ WARNING,
296 )
297
298 try:
299@@ -349,3 +368,87 @@
300 return True
301
302 return False
303+
304+
305+def is_ip(address):
306+ """
307+ Returns True if address is a valid IP address.
308+ """
309+ try:
310+ # Test to see if already an IPv4 address
311+ socket.inet_aton(address)
312+ return True
313+ except socket.error:
314+ return False
315+
316+
317+def ns_query(address):
318+ try:
319+ import dns.resolver
320+ except ImportError:
321+ apt_install('python-dnspython')
322+ import dns.resolver
323+
324+ if isinstance(address, dns.name.Name):
325+ rtype = 'PTR'
326+ elif isinstance(address, six.string_types):
327+ rtype = 'A'
328+ else:
329+ return None
330+
331+ answers = dns.resolver.query(address, rtype)
332+ if answers:
333+ return str(answers[0])
334+ return None
335+
336+
337+def get_host_ip(hostname, fallback=None):
338+ """
339+ Resolves the IP for a given hostname, or returns
340+ the input if it is already an IP.
341+ """
342+ if is_ip(hostname):
343+ return hostname
344+
345+ ip_addr = ns_query(hostname)
346+ if not ip_addr:
347+ try:
348+ ip_addr = socket.gethostbyname(hostname)
349+ except:
350+ log("Failed to resolve hostname '%s'" % (hostname),
351+ level=WARNING)
352+ return fallback
353+ return ip_addr
354+
355+
356+def get_hostname(address, fqdn=True):
357+ """
358+ Resolves hostname for given IP, or returns the input
359+ if it is already a hostname.
360+ """
361+ if is_ip(address):
362+ try:
363+ import dns.reversename
364+ except ImportError:
365+ apt_install("python-dnspython")
366+ import dns.reversename
367+
368+ rev = dns.reversename.from_address(address)
369+ result = ns_query(rev)
370+
371+ if not result:
372+ try:
373+ result = socket.gethostbyaddr(address)[0]
374+ except:
375+ return None
376+ else:
377+ result = address
378+
379+ if fqdn:
380+ # strip trailing .
381+ if result.endswith('.'):
382+ return result[:-1]
383+ else:
384+ return result
385+ else:
386+ return result.split('.')[0]
387
388=== modified file 'hooks/charmhelpers/contrib/network/ovs/__init__.py'
389--- hooks/charmhelpers/contrib/network/ovs/__init__.py 2014-10-23 17:30:13 +0000
390+++ hooks/charmhelpers/contrib/network/ovs/__init__.py 2015-09-21 10:47:21 +0000
391@@ -1,3 +1,19 @@
392+# Copyright 2014-2015 Canonical Limited.
393+#
394+# This file is part of charm-helpers.
395+#
396+# charm-helpers is free software: you can redistribute it and/or modify
397+# it under the terms of the GNU Lesser General Public License version 3 as
398+# published by the Free Software Foundation.
399+#
400+# charm-helpers is distributed in the hope that it will be useful,
401+# but WITHOUT ANY WARRANTY; without even the implied warranty of
402+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
403+# GNU Lesser General Public License for more details.
404+#
405+# You should have received a copy of the GNU Lesser General Public License
406+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
407+
408 ''' Helpers for interacting with OpenvSwitch '''
409 import subprocess
410 import os
411
412=== modified file 'hooks/charmhelpers/contrib/network/ufw.py'
413--- hooks/charmhelpers/contrib/network/ufw.py 2015-01-14 15:30:19 +0000
414+++ hooks/charmhelpers/contrib/network/ufw.py 2015-09-21 10:47:21 +0000
415@@ -1,3 +1,19 @@
416+# Copyright 2014-2015 Canonical Limited.
417+#
418+# This file is part of charm-helpers.
419+#
420+# charm-helpers is free software: you can redistribute it and/or modify
421+# it under the terms of the GNU Lesser General Public License version 3 as
422+# published by the Free Software Foundation.
423+#
424+# charm-helpers is distributed in the hope that it will be useful,
425+# but WITHOUT ANY WARRANTY; without even the implied warranty of
426+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
427+# GNU Lesser General Public License for more details.
428+#
429+# You should have received a copy of the GNU Lesser General Public License
430+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
431+
432 """
433 This module contains helpers to add and remove ufw rules.
434
435@@ -21,13 +37,22 @@
436 >>> ufw.enable()
437 >>> ufw.service('4949', 'close') # munin
438 """
439-
440-__author__ = "Felipe Reyes <felipe.reyes@canonical.com>"
441-
442 import re
443 import os
444 import subprocess
445+
446 from charmhelpers.core import hookenv
447+from charmhelpers.core.kernel import modprobe, is_module_loaded
448+
449+__author__ = "Felipe Reyes <felipe.reyes@canonical.com>"
450+
451+
452+class UFWError(Exception):
453+ pass
454+
455+
456+class UFWIPv6Error(UFWError):
457+ pass
458
459
460 def is_enabled():
461@@ -37,6 +62,7 @@
462 :returns: True if ufw is enabled
463 """
464 output = subprocess.check_output(['ufw', 'status'],
465+ universal_newlines=True,
466 env={'LANG': 'en_US',
467 'PATH': os.environ['PATH']})
468
469@@ -45,27 +71,73 @@
470 return len(m) >= 1
471
472
473-def enable():
474+def is_ipv6_ok(soft_fail=False):
475+ """
476+ Check if IPv6 support is present and ip6tables functional
477+
478+ :param soft_fail: If set to True and IPv6 support is broken, then reports
479+ that the host doesn't have IPv6 support, otherwise a
480+ UFWIPv6Error exception is raised.
481+ :returns: True if IPv6 is working, False otherwise
482+ """
483+
484+ # do we have IPv6 in the machine?
485+ if os.path.isdir('/proc/sys/net/ipv6'):
486+ # is ip6tables kernel module loaded?
487+ if not is_module_loaded('ip6_tables'):
488+ # ip6tables support isn't complete, let's try to load it
489+ try:
490+ modprobe('ip6_tables')
491+ # great, we can load the module
492+ return True
493+ except subprocess.CalledProcessError as ex:
494+ hookenv.log("Couldn't load ip6_tables module: %s" % ex.output,
495+ level="WARN")
496+ # we are in a world where ip6tables isn't working
497+ if soft_fail:
498+ # so we inform that the machine doesn't have IPv6
499+ return False
500+ else:
501+ raise UFWIPv6Error("IPv6 firewall support broken")
502+ else:
503+ # the module is present :)
504+ return True
505+
506+ else:
507+ # the system doesn't have IPv6
508+ return False
509+
510+
511+def disable_ipv6():
512+ """
513+ Disable ufw IPv6 support in /etc/default/ufw
514+ """
515+ exit_code = subprocess.call(['sed', '-i', 's/IPV6=.*/IPV6=no/g',
516+ '/etc/default/ufw'])
517+ if exit_code == 0:
518+ hookenv.log('IPv6 support in ufw disabled', level='INFO')
519+ else:
520+ hookenv.log("Couldn't disable IPv6 support in ufw", level="ERROR")
521+ raise UFWError("Couldn't disable IPv6 support in ufw")
522+
523+
524+def enable(soft_fail=False):
525 """
526 Enable ufw
527
528+ :param soft_fail: If set to True silently disables IPv6 support in ufw,
529+ otherwise a UFWIPv6Error exception is raised when IP6
530+ support is broken.
531 :returns: True if ufw is successfully enabled
532 """
533 if is_enabled():
534 return True
535
536- if not os.path.isdir('/proc/sys/net/ipv6'):
537- # disable IPv6 support in ufw
538- hookenv.log("This machine doesn't have IPv6 enabled", level="INFO")
539- exit_code = subprocess.call(['sed', '-i', 's/IPV6=yes/IPV6=no/g',
540- '/etc/default/ufw'])
541- if exit_code == 0:
542- hookenv.log('IPv6 support in ufw disabled', level='INFO')
543- else:
544- hookenv.log("Couldn't disable IPv6 support in ufw", level="ERROR")
545- raise Exception("Couldn't disable IPv6 support in ufw")
546+ if not is_ipv6_ok(soft_fail):
547+ disable_ipv6()
548
549 output = subprocess.check_output(['ufw', 'enable'],
550+ universal_newlines=True,
551 env={'LANG': 'en_US',
552 'PATH': os.environ['PATH']})
553
554@@ -91,6 +163,7 @@
555 return True
556
557 output = subprocess.check_output(['ufw', 'disable'],
558+ universal_newlines=True,
559 env={'LANG': 'en_US',
560 'PATH': os.environ['PATH']})
561
562@@ -106,7 +179,43 @@
563 return True
564
565
566-def modify_access(src, dst='any', port=None, proto=None, action='allow'):
567+def default_policy(policy='deny', direction='incoming'):
568+ """
569+ Changes the default policy for traffic `direction`
570+
571+ :param policy: allow, deny or reject
572+ :param direction: traffic direction, possible values: incoming, outgoing,
573+ routed
574+ """
575+ if policy not in ['allow', 'deny', 'reject']:
576+ raise UFWError(('Unknown policy %s, valid values: '
577+ 'allow, deny, reject') % policy)
578+
579+ if direction not in ['incoming', 'outgoing', 'routed']:
580+ raise UFWError(('Unknown direction %s, valid values: '
581+ 'incoming, outgoing, routed') % direction)
582+
583+ output = subprocess.check_output(['ufw', 'default', policy, direction],
584+ universal_newlines=True,
585+ env={'LANG': 'en_US',
586+ 'PATH': os.environ['PATH']})
587+ hookenv.log(output, level='DEBUG')
588+
589+ m = re.findall("^Default %s policy changed to '%s'\n" % (direction,
590+ policy),
591+ output, re.M)
592+ if len(m) == 0:
593+ hookenv.log("ufw couldn't change the default policy to %s for %s"
594+ % (policy, direction), level='WARN')
595+ return False
596+ else:
597+ hookenv.log("ufw default policy for %s changed to %s"
598+ % (direction, policy), level='INFO')
599+ return True
600+
601+
602+def modify_access(src, dst='any', port=None, proto=None, action='allow',
603+ index=None):
604 """
605 Grant access to an address or subnet
606
607@@ -118,6 +227,8 @@
608 :param port: destiny port
609 :param proto: protocol (tcp or udp)
610 :param action: `allow` or `delete`
611+ :param index: if different from None the rule is inserted at the given
612+ `index`.
613 """
614 if not is_enabled():
615 hookenv.log('ufw is disabled, skipping modify_access()', level='WARN')
616@@ -125,6 +236,8 @@
617
618 if action == 'delete':
619 cmd = ['ufw', 'delete', 'allow']
620+ elif index is not None:
621+ cmd = ['ufw', 'insert', str(index), action]
622 else:
623 cmd = ['ufw', action]
624
625@@ -135,7 +248,7 @@
626 cmd += ['to', dst]
627
628 if port is not None:
629- cmd += ['port', port]
630+ cmd += ['port', str(port)]
631
632 if proto is not None:
633 cmd += ['proto', proto]
634@@ -153,7 +266,7 @@
635 level='ERROR')
636
637
638-def grant_access(src, dst='any', port=None, proto=None):
639+def grant_access(src, dst='any', port=None, proto=None, index=None):
640 """
641 Grant access to an address or subnet
642
643@@ -164,8 +277,11 @@
644 field has to be set.
645 :param port: destiny port
646 :param proto: protocol (tcp or udp)
647+ :param index: if different from None the rule is inserted at the given
648+ `index`.
649 """
650- return modify_access(src, dst=dst, port=port, proto=proto, action='allow')
651+ return modify_access(src, dst=dst, port=port, proto=proto, action='allow',
652+ index=index)
653
654
655 def revoke_access(src, dst='any', port=None, proto=None):
656@@ -192,9 +308,11 @@
657 :param action: `open` or `close`
658 """
659 if action == 'open':
660- subprocess.check_output(['ufw', 'allow', name])
661+ subprocess.check_output(['ufw', 'allow', str(name)],
662+ universal_newlines=True)
663 elif action == 'close':
664- subprocess.check_output(['ufw', 'delete', 'allow', name])
665+ subprocess.check_output(['ufw', 'delete', 'allow', str(name)],
666+ universal_newlines=True)
667 else:
668- raise Exception(("'{}' not supported, use 'allow' "
669- "or 'delete'").format(action))
670+ raise UFWError(("'{}' not supported, use 'allow' "
671+ "or 'delete'").format(action))
672
673=== modified file 'hooks/charmhelpers/contrib/openstack/__init__.py'
674--- hooks/charmhelpers/contrib/openstack/__init__.py 2013-07-19 02:37:30 +0000
675+++ hooks/charmhelpers/contrib/openstack/__init__.py 2015-09-21 10:47:21 +0000
676@@ -0,0 +1,15 @@
677+# Copyright 2014-2015 Canonical Limited.
678+#
679+# This file is part of charm-helpers.
680+#
681+# charm-helpers is free software: you can redistribute it and/or modify
682+# it under the terms of the GNU Lesser General Public License version 3 as
683+# published by the Free Software Foundation.
684+#
685+# charm-helpers is distributed in the hope that it will be useful,
686+# but WITHOUT ANY WARRANTY; without even the implied warranty of
687+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
688+# GNU Lesser General Public License for more details.
689+#
690+# You should have received a copy of the GNU Lesser General Public License
691+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
692
693=== modified file 'hooks/charmhelpers/contrib/openstack/alternatives.py'
694--- hooks/charmhelpers/contrib/openstack/alternatives.py 2013-10-10 11:09:48 +0000
695+++ hooks/charmhelpers/contrib/openstack/alternatives.py 2015-09-21 10:47:21 +0000
696@@ -1,3 +1,19 @@
697+# Copyright 2014-2015 Canonical Limited.
698+#
699+# This file is part of charm-helpers.
700+#
701+# charm-helpers is free software: you can redistribute it and/or modify
702+# it under the terms of the GNU Lesser General Public License version 3 as
703+# published by the Free Software Foundation.
704+#
705+# charm-helpers is distributed in the hope that it will be useful,
706+# but WITHOUT ANY WARRANTY; without even the implied warranty of
707+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
708+# GNU Lesser General Public License for more details.
709+#
710+# You should have received a copy of the GNU Lesser General Public License
711+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
712+
713 ''' Helper for managing alternatives for file conflict resolution '''
714
715 import subprocess
716
717=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/__init__.py'
718--- hooks/charmhelpers/contrib/openstack/amulet/__init__.py 2014-10-23 17:30:13 +0000
719+++ hooks/charmhelpers/contrib/openstack/amulet/__init__.py 2015-09-21 10:47:21 +0000
720@@ -0,0 +1,15 @@
721+# Copyright 2014-2015 Canonical Limited.
722+#
723+# This file is part of charm-helpers.
724+#
725+# charm-helpers is free software: you can redistribute it and/or modify
726+# it under the terms of the GNU Lesser General Public License version 3 as
727+# published by the Free Software Foundation.
728+#
729+# charm-helpers is distributed in the hope that it will be useful,
730+# but WITHOUT ANY WARRANTY; without even the implied warranty of
731+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
732+# GNU Lesser General Public License for more details.
733+#
734+# You should have received a copy of the GNU Lesser General Public License
735+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
736
737=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/deployment.py'
738--- hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2014-12-10 20:28:57 +0000
739+++ hooks/charmhelpers/contrib/openstack/amulet/deployment.py 2015-09-21 10:47:21 +0000
740@@ -1,4 +1,21 @@
741+# Copyright 2014-2015 Canonical Limited.
742+#
743+# This file is part of charm-helpers.
744+#
745+# charm-helpers is free software: you can redistribute it and/or modify
746+# it under the terms of the GNU Lesser General Public License version 3 as
747+# published by the Free Software Foundation.
748+#
749+# charm-helpers is distributed in the hope that it will be useful,
750+# but WITHOUT ANY WARRANTY; without even the implied warranty of
751+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
752+# GNU Lesser General Public License for more details.
753+#
754+# You should have received a copy of the GNU Lesser General Public License
755+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
756+
757 import six
758+from collections import OrderedDict
759 from charmhelpers.contrib.amulet.deployment import (
760 AmuletDeployment
761 )
762@@ -27,21 +44,40 @@
763 Determine if the local branch being tested is derived from its
764 stable or next (dev) branch, and based on this, use the corresonding
765 stable or next branches for the other_services."""
766- base_charms = ['mysql', 'mongodb', 'rabbitmq-server']
767-
768- if self.stable:
769- for svc in other_services:
770- temp = 'lp:charms/{}'
771- svc['location'] = temp.format(svc['name'])
772+
773+ # Charms outside the lp:~openstack-charmers namespace
774+ base_charms = ['mysql', 'mongodb', 'nrpe']
775+
776+ # Force these charms to current series even when using an older series.
777+ # ie. Use trusty/nrpe even when series is precise, as the P charm
778+ # does not possess the necessary external master config and hooks.
779+ force_series_current = ['nrpe']
780+
781+ if self.series in ['precise', 'trusty']:
782+ base_series = self.series
783 else:
784- for svc in other_services:
785+ base_series = self.current_next
786+
787+ for svc in other_services:
788+ if svc['name'] in force_series_current:
789+ base_series = self.current_next
790+ # If a location has been explicitly set, use it
791+ if svc.get('location'):
792+ continue
793+ if self.stable:
794+ temp = 'lp:charms/{}/{}'
795+ svc['location'] = temp.format(base_series,
796+ svc['name'])
797+ else:
798 if svc['name'] in base_charms:
799- temp = 'lp:charms/{}'
800- svc['location'] = temp.format(svc['name'])
801+ temp = 'lp:charms/{}/{}'
802+ svc['location'] = temp.format(base_series,
803+ svc['name'])
804 else:
805 temp = 'lp:~openstack-charmers/charms/{}/{}/next'
806 svc['location'] = temp.format(self.current_next,
807 svc['name'])
808+
809 return other_services
810
811 def _add_services(self, this_service, other_services):
812@@ -53,18 +89,23 @@
813
814 services = other_services
815 services.append(this_service)
816+
817+ # Charms which should use the source config option
818 use_source = ['mysql', 'mongodb', 'rabbitmq-server', 'ceph',
819 'ceph-osd', 'ceph-radosgw']
820
821+ # Charms which can not use openstack-origin, ie. many subordinates
822+ no_origin = ['cinder-ceph', 'hacluster', 'neutron-openvswitch', 'nrpe']
823+
824 if self.openstack:
825 for svc in services:
826- if svc['name'] not in use_source:
827+ if svc['name'] not in use_source + no_origin:
828 config = {'openstack-origin': self.openstack}
829 self.d.configure(svc['name'], config)
830
831 if self.source:
832 for svc in services:
833- if svc['name'] in use_source:
834+ if svc['name'] in use_source and svc['name'] not in no_origin:
835 config = {'source': self.source}
836 self.d.configure(svc['name'], config)
837
838@@ -79,14 +120,78 @@
839 Return an integer representing the enum value of the openstack
840 release.
841 """
842+ # Must be ordered by OpenStack release (not by Ubuntu release):
843 (self.precise_essex, self.precise_folsom, self.precise_grizzly,
844 self.precise_havana, self.precise_icehouse,
845- self.trusty_icehouse) = range(6)
846+ self.trusty_icehouse, self.trusty_juno, self.utopic_juno,
847+ self.trusty_kilo, self.vivid_kilo, self.trusty_liberty,
848+ self.wily_liberty) = range(12)
849+
850 releases = {
851 ('precise', None): self.precise_essex,
852 ('precise', 'cloud:precise-folsom'): self.precise_folsom,
853 ('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
854 ('precise', 'cloud:precise-havana'): self.precise_havana,
855 ('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
856- ('trusty', None): self.trusty_icehouse}
857+ ('trusty', None): self.trusty_icehouse,
858+ ('trusty', 'cloud:trusty-juno'): self.trusty_juno,
859+ ('trusty', 'cloud:trusty-kilo'): self.trusty_kilo,
860+ ('trusty', 'cloud:trusty-liberty'): self.trusty_liberty,
861+ ('utopic', None): self.utopic_juno,
862+ ('vivid', None): self.vivid_kilo,
863+ ('wily', None): self.wily_liberty}
864 return releases[(self.series, self.openstack)]
865+
866+ def _get_openstack_release_string(self):
867+ """Get openstack release string.
868+
869+ Return a string representing the openstack release.
870+ """
871+ releases = OrderedDict([
872+ ('precise', 'essex'),
873+ ('quantal', 'folsom'),
874+ ('raring', 'grizzly'),
875+ ('saucy', 'havana'),
876+ ('trusty', 'icehouse'),
877+ ('utopic', 'juno'),
878+ ('vivid', 'kilo'),
879+ ('wily', 'liberty'),
880+ ])
881+ if self.openstack:
882+ os_origin = self.openstack.split(':')[1]
883+ return os_origin.split('%s-' % self.series)[1].split('/')[0]
884+ else:
885+ return releases[self.series]
886+
887+ def get_ceph_expected_pools(self, radosgw=False):
888+ """Return a list of expected ceph pools in a ceph + cinder + glance
889+ test scenario, based on OpenStack release and whether ceph radosgw
890+ is flagged as present or not."""
891+
892+ if self._get_openstack_release() >= self.trusty_kilo:
893+ # Kilo or later
894+ pools = [
895+ 'rbd',
896+ 'cinder',
897+ 'glance'
898+ ]
899+ else:
900+ # Juno or earlier
901+ pools = [
902+ 'data',
903+ 'metadata',
904+ 'rbd',
905+ 'cinder',
906+ 'glance'
907+ ]
908+
909+ if radosgw:
910+ pools.extend([
911+ '.rgw.root',
912+ '.rgw.control',
913+ '.rgw',
914+ '.rgw.gc',
915+ '.users.uid'
916+ ])
917+
918+ return pools
919
920=== modified file 'hooks/charmhelpers/contrib/openstack/amulet/utils.py'
921--- hooks/charmhelpers/contrib/openstack/amulet/utils.py 2014-12-10 20:28:57 +0000
922+++ hooks/charmhelpers/contrib/openstack/amulet/utils.py 2015-09-21 10:47:21 +0000
923@@ -1,13 +1,34 @@
924+# Copyright 2014-2015 Canonical Limited.
925+#
926+# This file is part of charm-helpers.
927+#
928+# charm-helpers is free software: you can redistribute it and/or modify
929+# it under the terms of the GNU Lesser General Public License version 3 as
930+# published by the Free Software Foundation.
931+#
932+# charm-helpers is distributed in the hope that it will be useful,
933+# but WITHOUT ANY WARRANTY; without even the implied warranty of
934+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
935+# GNU Lesser General Public License for more details.
936+#
937+# You should have received a copy of the GNU Lesser General Public License
938+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
939+
940+import amulet
941+import json
942 import logging
943 import os
944+import six
945 import time
946 import urllib
947
948+import cinderclient.v1.client as cinder_client
949 import glanceclient.v1.client as glance_client
950+import heatclient.v1.client as heat_client
951 import keystoneclient.v2_0 as keystone_client
952 import novaclient.v1_1.client as nova_client
953-
954-import six
955+import pika
956+import swiftclient
957
958 from charmhelpers.contrib.amulet.utils import (
959 AmuletUtils
960@@ -21,7 +42,7 @@
961 """OpenStack amulet utilities.
962
963 This class inherits from AmuletUtils and has additional support
964- that is specifically for use by OpenStack charms.
965+ that is specifically for use by OpenStack charm tests.
966 """
967
968 def __init__(self, log_level=ERROR):
969@@ -35,6 +56,8 @@
970 Validate actual endpoint data vs expected endpoint data. The ports
971 are used to find the matching endpoint.
972 """
973+ self.log.debug('Validating endpoint data...')
974+ self.log.debug('actual: {}'.format(repr(endpoints)))
975 found = False
976 for ep in endpoints:
977 self.log.debug('endpoint: {}'.format(repr(ep)))
978@@ -61,6 +84,7 @@
979 Validate a list of actual service catalog endpoints vs a list of
980 expected service catalog endpoints.
981 """
982+ self.log.debug('Validating service catalog endpoint data...')
983 self.log.debug('actual: {}'.format(repr(actual)))
984 for k, v in six.iteritems(expected):
985 if k in actual:
986@@ -77,6 +101,7 @@
987 Validate a list of actual tenant data vs list of expected tenant
988 data.
989 """
990+ self.log.debug('Validating tenant data...')
991 self.log.debug('actual: {}'.format(repr(actual)))
992 for e in expected:
993 found = False
994@@ -98,6 +123,7 @@
995 Validate a list of actual role data vs a list of expected role
996 data.
997 """
998+ self.log.debug('Validating role data...')
999 self.log.debug('actual: {}'.format(repr(actual)))
1000 for e in expected:
1001 found = False
1002@@ -118,6 +144,7 @@
1003 Validate a list of actual user data vs a list of expected user
1004 data.
1005 """
1006+ self.log.debug('Validating user data...')
1007 self.log.debug('actual: {}'.format(repr(actual)))
1008 for e in expected:
1009 found = False
1010@@ -139,17 +166,30 @@
1011
1012 Validate a list of actual flavors vs a list of expected flavors.
1013 """
1014+ self.log.debug('Validating flavor data...')
1015 self.log.debug('actual: {}'.format(repr(actual)))
1016 act = [a.name for a in actual]
1017 return self._validate_list_data(expected, act)
1018
1019 def tenant_exists(self, keystone, tenant):
1020 """Return True if tenant exists."""
1021+ self.log.debug('Checking if tenant exists ({})...'.format(tenant))
1022 return tenant in [t.name for t in keystone.tenants.list()]
1023
1024+ def authenticate_cinder_admin(self, keystone_sentry, username,
1025+ password, tenant):
1026+ """Authenticates admin user with cinder."""
1027+ # NOTE(beisner): cinder python client doesn't accept tokens.
1028+ service_ip = \
1029+ keystone_sentry.relation('shared-db',
1030+ 'mysql:shared-db')['private-address']
1031+ ept = "http://{}:5000/v2.0".format(service_ip.strip().decode('utf-8'))
1032+ return cinder_client.Client(username, password, tenant, ept)
1033+
1034 def authenticate_keystone_admin(self, keystone_sentry, user, password,
1035 tenant):
1036 """Authenticates admin user with the keystone admin endpoint."""
1037+ self.log.debug('Authenticating keystone admin...')
1038 unit = keystone_sentry
1039 service_ip = unit.relation('shared-db',
1040 'mysql:shared-db')['private-address']
1041@@ -159,6 +199,7 @@
1042
1043 def authenticate_keystone_user(self, keystone, user, password, tenant):
1044 """Authenticates a regular user with the keystone public endpoint."""
1045+ self.log.debug('Authenticating keystone user ({})...'.format(user))
1046 ep = keystone.service_catalog.url_for(service_type='identity',
1047 endpoint_type='publicURL')
1048 return keystone_client.Client(username=user, password=password,
1049@@ -166,19 +207,49 @@
1050
1051 def authenticate_glance_admin(self, keystone):
1052 """Authenticates admin user with glance."""
1053+ self.log.debug('Authenticating glance admin...')
1054 ep = keystone.service_catalog.url_for(service_type='image',
1055 endpoint_type='adminURL')
1056 return glance_client.Client(ep, token=keystone.auth_token)
1057
1058+ def authenticate_heat_admin(self, keystone):
1059+ """Authenticates the admin user with heat."""
1060+ self.log.debug('Authenticating heat admin...')
1061+ ep = keystone.service_catalog.url_for(service_type='orchestration',
1062+ endpoint_type='publicURL')
1063+ return heat_client.Client(endpoint=ep, token=keystone.auth_token)
1064+
1065 def authenticate_nova_user(self, keystone, user, password, tenant):
1066 """Authenticates a regular user with nova-api."""
1067+ self.log.debug('Authenticating nova user ({})...'.format(user))
1068 ep = keystone.service_catalog.url_for(service_type='identity',
1069 endpoint_type='publicURL')
1070 return nova_client.Client(username=user, api_key=password,
1071 project_id=tenant, auth_url=ep)
1072
1073+ def authenticate_swift_user(self, keystone, user, password, tenant):
1074+ """Authenticates a regular user with swift api."""
1075+ self.log.debug('Authenticating swift user ({})...'.format(user))
1076+ ep = keystone.service_catalog.url_for(service_type='identity',
1077+ endpoint_type='publicURL')
1078+ return swiftclient.Connection(authurl=ep,
1079+ user=user,
1080+ key=password,
1081+ tenant_name=tenant,
1082+ auth_version='2.0')
1083+
1084 def create_cirros_image(self, glance, image_name):
1085- """Download the latest cirros image and upload it to glance."""
1086+ """Download the latest cirros image and upload it to glance,
1087+ validate and return a resource pointer.
1088+
1089+ :param glance: pointer to authenticated glance connection
1090+ :param image_name: display name for new image
1091+ :returns: glance image pointer
1092+ """
1093+ self.log.debug('Creating glance cirros image '
1094+ '({})...'.format(image_name))
1095+
1096+ # Download cirros image
1097 http_proxy = os.getenv('AMULET_HTTP_PROXY')
1098 self.log.debug('AMULET_HTTP_PROXY: {}'.format(http_proxy))
1099 if http_proxy:
1100@@ -187,57 +258,67 @@
1101 else:
1102 opener = urllib.FancyURLopener()
1103
1104- f = opener.open("http://download.cirros-cloud.net/version/released")
1105+ f = opener.open('http://download.cirros-cloud.net/version/released')
1106 version = f.read().strip()
1107- cirros_img = "cirros-{}-x86_64-disk.img".format(version)
1108+ cirros_img = 'cirros-{}-x86_64-disk.img'.format(version)
1109 local_path = os.path.join('tests', cirros_img)
1110
1111 if not os.path.exists(local_path):
1112- cirros_url = "http://{}/{}/{}".format("download.cirros-cloud.net",
1113+ cirros_url = 'http://{}/{}/{}'.format('download.cirros-cloud.net',
1114 version, cirros_img)
1115 opener.retrieve(cirros_url, local_path)
1116 f.close()
1117
1118+ # Create glance image
1119 with open(local_path) as f:
1120 image = glance.images.create(name=image_name, is_public=True,
1121 disk_format='qcow2',
1122 container_format='bare', data=f)
1123- count = 1
1124- status = image.status
1125- while status != 'active' and count < 10:
1126- time.sleep(3)
1127- image = glance.images.get(image.id)
1128- status = image.status
1129- self.log.debug('image status: {}'.format(status))
1130- count += 1
1131-
1132- if status != 'active':
1133- self.log.error('image creation timed out')
1134- return None
1135+
1136+ # Wait for image to reach active status
1137+ img_id = image.id
1138+ ret = self.resource_reaches_status(glance.images, img_id,
1139+ expected_stat='active',
1140+ msg='Image status wait')
1141+ if not ret:
1142+ msg = 'Glance image failed to reach expected state.'
1143+ amulet.raise_status(amulet.FAIL, msg=msg)
1144+
1145+ # Re-validate new image
1146+ self.log.debug('Validating image attributes...')
1147+ val_img_name = glance.images.get(img_id).name
1148+ val_img_stat = glance.images.get(img_id).status
1149+ val_img_pub = glance.images.get(img_id).is_public
1150+ val_img_cfmt = glance.images.get(img_id).container_format
1151+ val_img_dfmt = glance.images.get(img_id).disk_format
1152+ msg_attr = ('Image attributes - name:{} public:{} id:{} stat:{} '
1153+ 'container fmt:{} disk fmt:{}'.format(
1154+ val_img_name, val_img_pub, img_id,
1155+ val_img_stat, val_img_cfmt, val_img_dfmt))
1156+
1157+ if val_img_name == image_name and val_img_stat == 'active' \
1158+ and val_img_pub is True and val_img_cfmt == 'bare' \
1159+ and val_img_dfmt == 'qcow2':
1160+ self.log.debug(msg_attr)
1161+ else:
1162+ msg = ('Volume validation failed, {}'.format(msg_attr))
1163+ amulet.raise_status(amulet.FAIL, msg=msg)
1164
1165 return image
1166
1167 def delete_image(self, glance, image):
1168 """Delete the specified image."""
1169- num_before = len(list(glance.images.list()))
1170- glance.images.delete(image)
1171-
1172- count = 1
1173- num_after = len(list(glance.images.list()))
1174- while num_after != (num_before - 1) and count < 10:
1175- time.sleep(3)
1176- num_after = len(list(glance.images.list()))
1177- self.log.debug('number of images: {}'.format(num_after))
1178- count += 1
1179-
1180- if num_after != (num_before - 1):
1181- self.log.error('image deletion timed out')
1182- return False
1183-
1184- return True
1185+
1186+ # /!\ DEPRECATION WARNING
1187+ self.log.warn('/!\\ DEPRECATION WARNING: use '
1188+ 'delete_resource instead of delete_image.')
1189+ self.log.debug('Deleting glance image ({})...'.format(image))
1190+ return self.delete_resource(glance.images, image, msg='glance image')
1191
1192 def create_instance(self, nova, image_name, instance_name, flavor):
1193 """Create the specified instance."""
1194+ self.log.debug('Creating instance '
1195+ '({}|{}|{})'.format(instance_name, image_name, flavor))
1196 image = nova.images.find(name=image_name)
1197 flavor = nova.flavors.find(name=flavor)
1198 instance = nova.servers.create(name=instance_name, image=image,
1199@@ -260,19 +341,623 @@
1200
1201 def delete_instance(self, nova, instance):
1202 """Delete the specified instance."""
1203- num_before = len(list(nova.servers.list()))
1204- nova.servers.delete(instance)
1205-
1206- count = 1
1207- num_after = len(list(nova.servers.list()))
1208- while num_after != (num_before - 1) and count < 10:
1209- time.sleep(3)
1210- num_after = len(list(nova.servers.list()))
1211- self.log.debug('number of instances: {}'.format(num_after))
1212- count += 1
1213-
1214- if num_after != (num_before - 1):
1215- self.log.error('instance deletion timed out')
1216- return False
1217-
1218- return True
1219+
1220+ # /!\ DEPRECATION WARNING
1221+ self.log.warn('/!\\ DEPRECATION WARNING: use '
1222+ 'delete_resource instead of delete_instance.')
1223+ self.log.debug('Deleting instance ({})...'.format(instance))
1224+ return self.delete_resource(nova.servers, instance,
1225+ msg='nova instance')
1226+
1227+ def create_or_get_keypair(self, nova, keypair_name="testkey"):
1228+ """Create a new keypair, or return pointer if it already exists."""
1229+ try:
1230+ _keypair = nova.keypairs.get(keypair_name)
1231+ self.log.debug('Keypair ({}) already exists, '
1232+ 'using it.'.format(keypair_name))
1233+ return _keypair
1234+ except:
1235+ self.log.debug('Keypair ({}) does not exist, '
1236+ 'creating it.'.format(keypair_name))
1237+
1238+ _keypair = nova.keypairs.create(name=keypair_name)
1239+ return _keypair
1240+
1241+ def create_cinder_volume(self, cinder, vol_name="demo-vol", vol_size=1,
1242+ img_id=None, src_vol_id=None, snap_id=None):
1243+ """Create cinder volume, optionally from a glance image, OR
1244+ optionally as a clone of an existing volume, OR optionally
1245+ from a snapshot. Wait for the new volume status to reach
1246+ the expected status, validate and return a resource pointer.
1247+
1248+ :param vol_name: cinder volume display name
1249+ :param vol_size: size in gigabytes
1250+ :param img_id: optional glance image id
1251+ :param src_vol_id: optional source volume id to clone
1252+ :param snap_id: optional snapshot id to use
1253+ :returns: cinder volume pointer
1254+ """
1255+ # Handle parameter input and avoid impossible combinations
1256+ if img_id and not src_vol_id and not snap_id:
1257+ # Create volume from image
1258+ self.log.debug('Creating cinder volume from glance image...')
1259+ bootable = 'true'
1260+ elif src_vol_id and not img_id and not snap_id:
1261+ # Clone an existing volume
1262+ self.log.debug('Cloning cinder volume...')
1263+ bootable = cinder.volumes.get(src_vol_id).bootable
1264+ elif snap_id and not src_vol_id and not img_id:
1265+ # Create volume from snapshot
1266+ self.log.debug('Creating cinder volume from snapshot...')
1267+ snap = cinder.volume_snapshots.find(id=snap_id)
1268+ vol_size = snap.size
1269+ snap_vol_id = cinder.volume_snapshots.get(snap_id).volume_id
1270+ bootable = cinder.volumes.get(snap_vol_id).bootable
1271+ elif not img_id and not src_vol_id and not snap_id:
1272+ # Create volume
1273+ self.log.debug('Creating cinder volume...')
1274+ bootable = 'false'
1275+ else:
1276+ # Impossible combination of parameters
1277+ msg = ('Invalid method use - name:{} size:{} img_id:{} '
1278+ 'src_vol_id:{} snap_id:{}'.format(vol_name, vol_size,
1279+ img_id, src_vol_id,
1280+ snap_id))
1281+ amulet.raise_status(amulet.FAIL, msg=msg)
1282+
1283+ # Create new volume
1284+ try:
1285+ vol_new = cinder.volumes.create(display_name=vol_name,
1286+ imageRef=img_id,
1287+ size=vol_size,
1288+ source_volid=src_vol_id,
1289+ snapshot_id=snap_id)
1290+ vol_id = vol_new.id
1291+ except Exception as e:
1292+ msg = 'Failed to create volume: {}'.format(e)
1293+ amulet.raise_status(amulet.FAIL, msg=msg)
1294+
1295+ # Wait for volume to reach available status
1296+ ret = self.resource_reaches_status(cinder.volumes, vol_id,
1297+ expected_stat="available",
1298+ msg="Volume status wait")
1299+ if not ret:
1300+ msg = 'Cinder volume failed to reach expected state.'
1301+ amulet.raise_status(amulet.FAIL, msg=msg)
1302+
1303+ # Re-validate new volume
1304+ self.log.debug('Validating volume attributes...')
1305+ val_vol_name = cinder.volumes.get(vol_id).display_name
1306+ val_vol_boot = cinder.volumes.get(vol_id).bootable
1307+ val_vol_stat = cinder.volumes.get(vol_id).status
1308+ val_vol_size = cinder.volumes.get(vol_id).size
1309+ msg_attr = ('Volume attributes - name:{} id:{} stat:{} boot:'
1310+ '{} size:{}'.format(val_vol_name, vol_id,
1311+ val_vol_stat, val_vol_boot,
1312+ val_vol_size))
1313+
1314+ if val_vol_boot == bootable and val_vol_stat == 'available' \
1315+ and val_vol_name == vol_name and val_vol_size == vol_size:
1316+ self.log.debug(msg_attr)
1317+ else:
1318+ msg = ('Volume validation failed, {}'.format(msg_attr))
1319+ amulet.raise_status(amulet.FAIL, msg=msg)
1320+
1321+ return vol_new
1322+
1323+ def delete_resource(self, resource, resource_id,
1324+ msg="resource", max_wait=120):
1325+ """Delete one openstack resource, such as one instance, keypair,
1326+ image, volume, stack, etc., and confirm deletion within max wait time.
1327+
1328+ :param resource: pointer to os resource type, ex:glance_client.images
1329+ :param resource_id: unique name or id for the openstack resource
1330+ :param msg: text to identify purpose in logging
1331+ :param max_wait: maximum wait time in seconds
1332+ :returns: True if successful, otherwise False
1333+ """
1334+ self.log.debug('Deleting OpenStack resource '
1335+ '{} ({})'.format(resource_id, msg))
1336+ num_before = len(list(resource.list()))
1337+ resource.delete(resource_id)
1338+
1339+ tries = 0
1340+ num_after = len(list(resource.list()))
1341+ while num_after != (num_before - 1) and tries < (max_wait / 4):
1342+ self.log.debug('{} delete check: '
1343+ '{} [{}:{}] {}'.format(msg, tries,
1344+ num_before,
1345+ num_after,
1346+ resource_id))
1347+ time.sleep(4)
1348+ num_after = len(list(resource.list()))
1349+ tries += 1
1350+
1351+ self.log.debug('{}: expected, actual count = {}, '
1352+ '{}'.format(msg, num_before - 1, num_after))
1353+
1354+ if num_after == (num_before - 1):
1355+ return True
1356+ else:
1357+ self.log.error('{} delete timed out'.format(msg))
1358+ return False
1359+
1360+ def resource_reaches_status(self, resource, resource_id,
1361+ expected_stat='available',
1362+ msg='resource', max_wait=120):
1363+ """Wait for an openstack resources status to reach an
1364+ expected status within a specified time. Useful to confirm that
1365+ nova instances, cinder vols, snapshots, glance images, heat stacks
1366+ and other resources eventually reach the expected status.
1367+
1368+ :param resource: pointer to os resource type, ex: heat_client.stacks
1369+ :param resource_id: unique id for the openstack resource
1370+ :param expected_stat: status to expect resource to reach
1371+ :param msg: text to identify purpose in logging
1372+ :param max_wait: maximum wait time in seconds
1373+ :returns: True if successful, False if status is not reached
1374+ """
1375+
1376+ tries = 0
1377+ resource_stat = resource.get(resource_id).status
1378+ while resource_stat != expected_stat and tries < (max_wait / 4):
1379+ self.log.debug('{} status check: '
1380+ '{} [{}:{}] {}'.format(msg, tries,
1381+ resource_stat,
1382+ expected_stat,
1383+ resource_id))
1384+ time.sleep(4)
1385+ resource_stat = resource.get(resource_id).status
1386+ tries += 1
1387+
1388+ self.log.debug('{}: expected, actual status = {}, '
1389+ '{}'.format(msg, resource_stat, expected_stat))
1390+
1391+ if resource_stat == expected_stat:
1392+ return True
1393+ else:
1394+ self.log.debug('{} never reached expected status: '
1395+ '{}'.format(resource_id, expected_stat))
1396+ return False
1397+
1398+ def get_ceph_osd_id_cmd(self, index):
1399+ """Produce a shell command that will return a ceph-osd id."""
1400+ return ("`initctl list | grep 'ceph-osd ' | "
1401+ "awk 'NR=={} {{ print $2 }}' | "
1402+ "grep -o '[0-9]*'`".format(index + 1))
1403+
1404+ def get_ceph_pools(self, sentry_unit):
1405+ """Return a dict of ceph pools from a single ceph unit, with
1406+ pool name as keys, pool id as vals."""
1407+ pools = {}
1408+ cmd = 'sudo ceph osd lspools'
1409+ output, code = sentry_unit.run(cmd)
1410+ if code != 0:
1411+ msg = ('{} `{}` returned {} '
1412+ '{}'.format(sentry_unit.info['unit_name'],
1413+ cmd, code, output))
1414+ amulet.raise_status(amulet.FAIL, msg=msg)
1415+
1416+ # Example output: 0 data,1 metadata,2 rbd,3 cinder,4 glance,
1417+ for pool in str(output).split(','):
1418+ pool_id_name = pool.split(' ')
1419+ if len(pool_id_name) == 2:
1420+ pool_id = pool_id_name[0]
1421+ pool_name = pool_id_name[1]
1422+ pools[pool_name] = int(pool_id)
1423+
1424+ self.log.debug('Pools on {}: {}'.format(sentry_unit.info['unit_name'],
1425+ pools))
1426+ return pools
1427+
1428+ def get_ceph_df(self, sentry_unit):
1429+ """Return dict of ceph df json output, including ceph pool state.
1430+
1431+ :param sentry_unit: Pointer to amulet sentry instance (juju unit)
1432+ :returns: Dict of ceph df output
1433+ """
1434+ cmd = 'sudo ceph df --format=json'
1435+ output, code = sentry_unit.run(cmd)
1436+ if code != 0:
1437+ msg = ('{} `{}` returned {} '
1438+ '{}'.format(sentry_unit.info['unit_name'],
1439+ cmd, code, output))
1440+ amulet.raise_status(amulet.FAIL, msg=msg)
1441+ return json.loads(output)
1442+
1443+ def get_ceph_pool_sample(self, sentry_unit, pool_id=0):
1444+ """Take a sample of attributes of a ceph pool, returning ceph
1445+ pool name, object count and disk space used for the specified
1446+ pool ID number.
1447+
1448+ :param sentry_unit: Pointer to amulet sentry instance (juju unit)
1449+ :param pool_id: Ceph pool ID
1450+ :returns: List of pool name, object count, kb disk space used
1451+ """
1452+ df = self.get_ceph_df(sentry_unit)
1453+ pool_name = df['pools'][pool_id]['name']
1454+ obj_count = df['pools'][pool_id]['stats']['objects']
1455+ kb_used = df['pools'][pool_id]['stats']['kb_used']
1456+ self.log.debug('Ceph {} pool (ID {}): {} objects, '
1457+ '{} kb used'.format(pool_name, pool_id,
1458+ obj_count, kb_used))
1459+ return pool_name, obj_count, kb_used
1460+
1461+ def validate_ceph_pool_samples(self, samples, sample_type="resource pool"):
1462+ """Validate ceph pool samples taken over time, such as pool
1463+ object counts or pool kb used, before adding, after adding, and
1464+ after deleting items which affect those pool attributes. The
1465+ 2nd element is expected to be greater than the 1st; 3rd is expected
1466+ to be less than the 2nd.
1467+
1468+ :param samples: List containing 3 data samples
1469+ :param sample_type: String for logging and usage context
1470+ :returns: None if successful, Failure message otherwise
1471+ """
1472+ original, created, deleted = range(3)
1473+ if samples[created] <= samples[original] or \
1474+ samples[deleted] >= samples[created]:
1475+ return ('Ceph {} samples ({}) '
1476+ 'unexpected.'.format(sample_type, samples))
1477+ else:
1478+ self.log.debug('Ceph {} samples (OK): '
1479+ '{}'.format(sample_type, samples))
1480+ return None
1481+
1482+# rabbitmq/amqp specific helpers:
1483+ def add_rmq_test_user(self, sentry_units,
1484+ username="testuser1", password="changeme"):
1485+ """Add a test user via the first rmq juju unit, check connection as
1486+ the new user against all sentry units.
1487+
1488+ :param sentry_units: list of sentry unit pointers
1489+ :param username: amqp user name, default to testuser1
1490+ :param password: amqp user password
1491+ :returns: None if successful. Raise on error.
1492+ """
1493+ self.log.debug('Adding rmq user ({})...'.format(username))
1494+
1495+ # Check that user does not already exist
1496+ cmd_user_list = 'rabbitmqctl list_users'
1497+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
1498+ if username in output:
1499+ self.log.warning('User ({}) already exists, returning '
1500+ 'gracefully.'.format(username))
1501+ return
1502+
1503+ perms = '".*" ".*" ".*"'
1504+ cmds = ['rabbitmqctl add_user {} {}'.format(username, password),
1505+ 'rabbitmqctl set_permissions {} {}'.format(username, perms)]
1506+
1507+ # Add user via first unit
1508+ for cmd in cmds:
1509+ output, _ = self.run_cmd_unit(sentry_units[0], cmd)
1510+
1511+ # Check connection against the other sentry_units
1512+ self.log.debug('Checking user connect against units...')
1513+ for sentry_unit in sentry_units:
1514+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=False,
1515+ username=username,
1516+ password=password)
1517+ connection.close()
1518+
1519+ def delete_rmq_test_user(self, sentry_units, username="testuser1"):
1520+ """Delete a rabbitmq user via the first rmq juju unit.
1521+
1522+ :param sentry_units: list of sentry unit pointers
1523+ :param username: amqp user name, default to testuser1
1524+ :param password: amqp user password
1525+ :returns: None if successful or no such user.
1526+ """
1527+ self.log.debug('Deleting rmq user ({})...'.format(username))
1528+
1529+ # Check that the user exists
1530+ cmd_user_list = 'rabbitmqctl list_users'
1531+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_list)
1532+
1533+ if username not in output:
1534+ self.log.warning('User ({}) does not exist, returning '
1535+ 'gracefully.'.format(username))
1536+ return
1537+
1538+ # Delete the user
1539+ cmd_user_del = 'rabbitmqctl delete_user {}'.format(username)
1540+ output, _ = self.run_cmd_unit(sentry_units[0], cmd_user_del)
1541+
1542+ def get_rmq_cluster_status(self, sentry_unit):
1543+ """Execute rabbitmq cluster status command on a unit and return
1544+ the full output.
1545+
1546+ :param unit: sentry unit
1547+ :returns: String containing console output of cluster status command
1548+ """
1549+ cmd = 'rabbitmqctl cluster_status'
1550+ output, _ = self.run_cmd_unit(sentry_unit, cmd)
1551+ self.log.debug('{} cluster_status:\n{}'.format(
1552+ sentry_unit.info['unit_name'], output))
1553+ return str(output)
1554+
1555+ def get_rmq_cluster_running_nodes(self, sentry_unit):
1556+ """Parse rabbitmqctl cluster_status output string, return list of
1557+ running rabbitmq cluster nodes.
1558+
1559+ :param unit: sentry unit
1560+ :returns: List containing node names of running nodes
1561+ """
1562+ # NOTE(beisner): rabbitmqctl cluster_status output is not
1563+ # json-parsable, do string chop foo, then json.loads that.
1564+ str_stat = self.get_rmq_cluster_status(sentry_unit)
1565+ if 'running_nodes' in str_stat:
1566+ pos_start = str_stat.find("{running_nodes,") + 15
1567+ pos_end = str_stat.find("]},", pos_start) + 1
1568+ str_run_nodes = str_stat[pos_start:pos_end].replace("'", '"')
1569+ run_nodes = json.loads(str_run_nodes)
1570+ return run_nodes
1571+ else:
1572+ return []
1573+
1574+ def validate_rmq_cluster_running_nodes(self, sentry_units):
1575+ """Check that all rmq unit hostnames are represented in the
1576+ cluster_status output of all units.
1577+
1578+ :param host_names: dict of juju unit names to host names
1579+ :param units: list of sentry unit pointers (all rmq units)
1580+ :returns: None if successful, otherwise return error message
1581+ """
1582+ host_names = self.get_unit_hostnames(sentry_units)
1583+ errors = []
1584+
1585+ # Query every unit for cluster_status running nodes
1586+ for query_unit in sentry_units:
1587+ query_unit_name = query_unit.info['unit_name']
1588+ running_nodes = self.get_rmq_cluster_running_nodes(query_unit)
1589+
1590+ # Confirm that every unit is represented in the queried unit's
1591+ # cluster_status running nodes output.
1592+ for validate_unit in sentry_units:
1593+ val_host_name = host_names[validate_unit.info['unit_name']]
1594+ val_node_name = 'rabbit@{}'.format(val_host_name)
1595+
1596+ if val_node_name not in running_nodes:
1597+ errors.append('Cluster member check failed on {}: {} not '
1598+ 'in {}\n'.format(query_unit_name,
1599+ val_node_name,
1600+ running_nodes))
1601+ if errors:
1602+ return ''.join(errors)
1603+
1604+ def rmq_ssl_is_enabled_on_unit(self, sentry_unit, port=None):
1605+ """Check a single juju rmq unit for ssl and port in the config file."""
1606+ host = sentry_unit.info['public-address']
1607+ unit_name = sentry_unit.info['unit_name']
1608+
1609+ conf_file = '/etc/rabbitmq/rabbitmq.config'
1610+ conf_contents = str(self.file_contents_safe(sentry_unit,
1611+ conf_file, max_wait=16))
1612+ # Checks
1613+ conf_ssl = 'ssl' in conf_contents
1614+ conf_port = str(port) in conf_contents
1615+
1616+ # Port explicitly checked in config
1617+ if port and conf_port and conf_ssl:
1618+ self.log.debug('SSL is enabled @{}:{} '
1619+ '({})'.format(host, port, unit_name))
1620+ return True
1621+ elif port and not conf_port and conf_ssl:
1622+ self.log.debug('SSL is enabled @{} but not on port {} '
1623+ '({})'.format(host, port, unit_name))
1624+ return False
1625+ # Port not checked (useful when checking that ssl is disabled)
1626+ elif not port and conf_ssl:
1627+ self.log.debug('SSL is enabled @{}:{} '
1628+ '({})'.format(host, port, unit_name))
1629+ return True
1630+ elif not port and not conf_ssl:
1631+ self.log.debug('SSL not enabled @{}:{} '
1632+ '({})'.format(host, port, unit_name))
1633+ return False
1634+ else:
1635+ msg = ('Unknown condition when checking SSL status @{}:{} '
1636+ '({})'.format(host, port, unit_name))
1637+ amulet.raise_status(amulet.FAIL, msg)
1638+
1639+ def validate_rmq_ssl_enabled_units(self, sentry_units, port=None):
1640+ """Check that ssl is enabled on rmq juju sentry units.
1641+
1642+ :param sentry_units: list of all rmq sentry units
1643+ :param port: optional ssl port override to validate
1644+ :returns: None if successful, otherwise return error message
1645+ """
1646+ for sentry_unit in sentry_units:
1647+ if not self.rmq_ssl_is_enabled_on_unit(sentry_unit, port=port):
1648+ return ('Unexpected condition: ssl is disabled on unit '
1649+ '({})'.format(sentry_unit.info['unit_name']))
1650+ return None
1651+
1652+ def validate_rmq_ssl_disabled_units(self, sentry_units):
1653+ """Check that ssl is enabled on listed rmq juju sentry units.
1654+
1655+ :param sentry_units: list of all rmq sentry units
1656+ :returns: True if successful. Raise on error.
1657+ """
1658+ for sentry_unit in sentry_units:
1659+ if self.rmq_ssl_is_enabled_on_unit(sentry_unit):
1660+ return ('Unexpected condition: ssl is enabled on unit '
1661+ '({})'.format(sentry_unit.info['unit_name']))
1662+ return None
1663+
1664+ def configure_rmq_ssl_on(self, sentry_units, deployment,
1665+ port=None, max_wait=60):
1666+ """Turn ssl charm config option on, with optional non-default
1667+ ssl port specification. Confirm that it is enabled on every
1668+ unit.
1669+
1670+ :param sentry_units: list of sentry units
1671+ :param deployment: amulet deployment object pointer
1672+ :param port: amqp port, use defaults if None
1673+ :param max_wait: maximum time to wait in seconds to confirm
1674+ :returns: None if successful. Raise on error.
1675+ """
1676+ self.log.debug('Setting ssl charm config option: on')
1677+
1678+ # Enable RMQ SSL
1679+ config = {'ssl': 'on'}
1680+ if port:
1681+ config['ssl_port'] = port
1682+
1683+ deployment.configure('rabbitmq-server', config)
1684+
1685+ # Confirm
1686+ tries = 0
1687+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
1688+ while ret and tries < (max_wait / 4):
1689+ time.sleep(4)
1690+ self.log.debug('Attempt {}: {}'.format(tries, ret))
1691+ ret = self.validate_rmq_ssl_enabled_units(sentry_units, port=port)
1692+ tries += 1
1693+
1694+ if ret:
1695+ amulet.raise_status(amulet.FAIL, ret)
1696+
1697+ def configure_rmq_ssl_off(self, sentry_units, deployment, max_wait=60):
1698+ """Turn ssl charm config option off, confirm that it is disabled
1699+ on every unit.
1700+
1701+ :param sentry_units: list of sentry units
1702+ :param deployment: amulet deployment object pointer
1703+ :param max_wait: maximum time to wait in seconds to confirm
1704+ :returns: None if successful. Raise on error.
1705+ """
1706+ self.log.debug('Setting ssl charm config option: off')
1707+
1708+ # Disable RMQ SSL
1709+ config = {'ssl': 'off'}
1710+ deployment.configure('rabbitmq-server', config)
1711+
1712+ # Confirm
1713+ tries = 0
1714+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
1715+ while ret and tries < (max_wait / 4):
1716+ time.sleep(4)
1717+ self.log.debug('Attempt {}: {}'.format(tries, ret))
1718+ ret = self.validate_rmq_ssl_disabled_units(sentry_units)
1719+ tries += 1
1720+
1721+ if ret:
1722+ amulet.raise_status(amulet.FAIL, ret)
1723+
1724+ def connect_amqp_by_unit(self, sentry_unit, ssl=False,
1725+ port=None, fatal=True,
1726+ username="testuser1", password="changeme"):
1727+ """Establish and return a pika amqp connection to the rabbitmq service
1728+ running on a rmq juju unit.
1729+
1730+ :param sentry_unit: sentry unit pointer
1731+ :param ssl: boolean, default to False
1732+ :param port: amqp port, use defaults if None
1733+ :param fatal: boolean, default to True (raises on connect error)
1734+ :param username: amqp user name, default to testuser1
1735+ :param password: amqp user password
1736+ :returns: pika amqp connection pointer or None if failed and non-fatal
1737+ """
1738+ host = sentry_unit.info['public-address']
1739+ unit_name = sentry_unit.info['unit_name']
1740+
1741+ # Default port logic if port is not specified
1742+ if ssl and not port:
1743+ port = 5671
1744+ elif not ssl and not port:
1745+ port = 5672
1746+
1747+ self.log.debug('Connecting to amqp on {}:{} ({}) as '
1748+ '{}...'.format(host, port, unit_name, username))
1749+
1750+ try:
1751+ credentials = pika.PlainCredentials(username, password)
1752+ parameters = pika.ConnectionParameters(host=host, port=port,
1753+ credentials=credentials,
1754+ ssl=ssl,
1755+ connection_attempts=3,
1756+ retry_delay=5,
1757+ socket_timeout=1)
1758+ connection = pika.BlockingConnection(parameters)
1759+ assert connection.server_properties['product'] == 'RabbitMQ'
1760+ self.log.debug('Connect OK')
1761+ return connection
1762+ except Exception as e:
1763+ msg = ('amqp connection failed to {}:{} as '
1764+ '{} ({})'.format(host, port, username, str(e)))
1765+ if fatal:
1766+ amulet.raise_status(amulet.FAIL, msg)
1767+ else:
1768+ self.log.warn(msg)
1769+ return None
1770+
1771+ def publish_amqp_message_by_unit(self, sentry_unit, message,
1772+ queue="test", ssl=False,
1773+ username="testuser1",
1774+ password="changeme",
1775+ port=None):
1776+ """Publish an amqp message to a rmq juju unit.
1777+
1778+ :param sentry_unit: sentry unit pointer
1779+ :param message: amqp message string
1780+ :param queue: message queue, default to test
1781+ :param username: amqp user name, default to testuser1
1782+ :param password: amqp user password
1783+ :param ssl: boolean, default to False
1784+ :param port: amqp port, use defaults if None
1785+ :returns: None. Raises exception if publish failed.
1786+ """
1787+ self.log.debug('Publishing message to {} queue:\n{}'.format(queue,
1788+ message))
1789+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
1790+ port=port,
1791+ username=username,
1792+ password=password)
1793+
1794+ # NOTE(beisner): extra debug here re: pika hang potential:
1795+ # https://github.com/pika/pika/issues/297
1796+ # https://groups.google.com/forum/#!topic/rabbitmq-users/Ja0iyfF0Szw
1797+ self.log.debug('Defining channel...')
1798+ channel = connection.channel()
1799+ self.log.debug('Declaring queue...')
1800+ channel.queue_declare(queue=queue, auto_delete=False, durable=True)
1801+ self.log.debug('Publishing message...')
1802+ channel.basic_publish(exchange='', routing_key=queue, body=message)
1803+ self.log.debug('Closing channel...')
1804+ channel.close()
1805+ self.log.debug('Closing connection...')
1806+ connection.close()
1807+
1808+ def get_amqp_message_by_unit(self, sentry_unit, queue="test",
1809+ username="testuser1",
1810+ password="changeme",
1811+ ssl=False, port=None):
1812+ """Get an amqp message from a rmq juju unit.
1813+
1814+ :param sentry_unit: sentry unit pointer
1815+ :param queue: message queue, default to test
1816+ :param username: amqp user name, default to testuser1
1817+ :param password: amqp user password
1818+ :param ssl: boolean, default to False
1819+ :param port: amqp port, use defaults if None
1820+ :returns: amqp message body as string. Raise if get fails.
1821+ """
1822+ connection = self.connect_amqp_by_unit(sentry_unit, ssl=ssl,
1823+ port=port,
1824+ username=username,
1825+ password=password)
1826+ channel = connection.channel()
1827+ method_frame, _, body = channel.basic_get(queue)
1828+
1829+ if method_frame:
1830+ self.log.debug('Retreived message from {} queue:\n{}'.format(queue,
1831+ body))
1832+ channel.basic_ack(method_frame.delivery_tag)
1833+ channel.close()
1834+ connection.close()
1835+ return body
1836+ else:
1837+ msg = 'No message retrieved.'
1838+ amulet.raise_status(amulet.FAIL, msg)
1839
1840=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
1841--- hooks/charmhelpers/contrib/openstack/context.py 2015-01-14 15:30:19 +0000
1842+++ hooks/charmhelpers/contrib/openstack/context.py 2015-09-21 10:47:21 +0000
1843@@ -1,10 +1,28 @@
1844+# Copyright 2014-2015 Canonical Limited.
1845+#
1846+# This file is part of charm-helpers.
1847+#
1848+# charm-helpers is free software: you can redistribute it and/or modify
1849+# it under the terms of the GNU Lesser General Public License version 3 as
1850+# published by the Free Software Foundation.
1851+#
1852+# charm-helpers is distributed in the hope that it will be useful,
1853+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1854+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1855+# GNU Lesser General Public License for more details.
1856+#
1857+# You should have received a copy of the GNU Lesser General Public License
1858+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1859+
1860 import json
1861 import os
1862+import re
1863 import time
1864 from base64 import b64decode
1865 from subprocess import check_call
1866
1867 import six
1868+import yaml
1869
1870 from charmhelpers.fetch import (
1871 apt_install,
1872@@ -29,8 +47,13 @@
1873 )
1874
1875 from charmhelpers.core.sysctl import create as sysctl_create
1876+from charmhelpers.core.strutils import bool_from_string
1877
1878 from charmhelpers.core.host import (
1879+ get_bond_master,
1880+ is_phy_iface,
1881+ list_nics,
1882+ get_nic_hwaddr,
1883 mkdir,
1884 write_file,
1885 )
1886@@ -47,16 +70,22 @@
1887 )
1888 from charmhelpers.contrib.openstack.neutron import (
1889 neutron_plugin_attribute,
1890+ parse_data_port_mappings,
1891+)
1892+from charmhelpers.contrib.openstack.ip import (
1893+ resolve_address,
1894+ INTERNAL,
1895 )
1896 from charmhelpers.contrib.network.ip import (
1897 get_address_in_network,
1898+ get_ipv4_addr,
1899 get_ipv6_addr,
1900 get_netmask_for_address,
1901 format_ipv6_addr,
1902 is_address_in_network,
1903+ is_bridge_member,
1904 )
1905 from charmhelpers.contrib.openstack.utils import get_host_ip
1906-
1907 CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
1908 ADDRESS_TYPES = ['admin', 'internal', 'public']
1909
1910@@ -88,9 +117,44 @@
1911 def config_flags_parser(config_flags):
1912 """Parses config flags string into dict.
1913
1914+ This parsing method supports a few different formats for the config
1915+ flag values to be parsed:
1916+
1917+ 1. A string in the simple format of key=value pairs, with the possibility
1918+ of specifying multiple key value pairs within the same string. For
1919+ example, a string in the format of 'key1=value1, key2=value2' will
1920+ return a dict of:
1921+
1922+ {'key1': 'value1',
1923+ 'key2': 'value2'}.
1924+
1925+ 2. A string in the above format, but supporting a comma-delimited list
1926+ of values for the same key. For example, a string in the format of
1927+ 'key1=value1, key2=value3,value4,value5' will return a dict of:
1928+
1929+ {'key1', 'value1',
1930+ 'key2', 'value2,value3,value4'}
1931+
1932+ 3. A string containing a colon character (:) prior to an equal
1933+ character (=) will be treated as yaml and parsed as such. This can be
1934+ used to specify more complex key value pairs. For example,
1935+ a string in the format of 'key1: subkey1=value1, subkey2=value2' will
1936+ return a dict of:
1937+
1938+ {'key1', 'subkey1=value1, subkey2=value2'}
1939+
1940 The provided config_flags string may be a list of comma-separated values
1941 which themselves may be comma-separated list of values.
1942 """
1943+ # If we find a colon before an equals sign then treat it as yaml.
1944+ # Note: limit it to finding the colon first since this indicates assignment
1945+ # for inline yaml.
1946+ colon = config_flags.find(':')
1947+ equals = config_flags.find('=')
1948+ if colon > 0:
1949+ if colon < equals or equals < 0:
1950+ return yaml.safe_load(config_flags)
1951+
1952 if config_flags.find('==') >= 0:
1953 log("config_flags is not in expected format (key=value)", level=ERROR)
1954 raise OSContextError
1955@@ -130,10 +194,50 @@
1956 class OSContextGenerator(object):
1957 """Base class for all context generators."""
1958 interfaces = []
1959+ related = False
1960+ complete = False
1961+ missing_data = []
1962
1963 def __call__(self):
1964 raise NotImplementedError
1965
1966+ def context_complete(self, ctxt):
1967+ """Check for missing data for the required context data.
1968+ Set self.missing_data if it exists and return False.
1969+ Set self.complete if no missing data and return True.
1970+ """
1971+ # Fresh start
1972+ self.complete = False
1973+ self.missing_data = []
1974+ for k, v in six.iteritems(ctxt):
1975+ if v is None or v == '':
1976+ if k not in self.missing_data:
1977+ self.missing_data.append(k)
1978+
1979+ if self.missing_data:
1980+ self.complete = False
1981+ log('Missing required data: %s' % ' '.join(self.missing_data), level=INFO)
1982+ else:
1983+ self.complete = True
1984+ return self.complete
1985+
1986+ def get_related(self):
1987+ """Check if any of the context interfaces have relation ids.
1988+ Set self.related and return True if one of the interfaces
1989+ has relation ids.
1990+ """
1991+ # Fresh start
1992+ self.related = False
1993+ try:
1994+ for interface in self.interfaces:
1995+ if relation_ids(interface):
1996+ self.related = True
1997+ return self.related
1998+ except AttributeError as e:
1999+ log("{} {}"
2000+ "".format(self, e), 'INFO')
2001+ return self.related
2002+
2003
2004 class SharedDBContext(OSContextGenerator):
2005 interfaces = ['shared-db']
2006@@ -149,6 +253,7 @@
2007 self.database = database
2008 self.user = user
2009 self.ssl_dir = ssl_dir
2010+ self.rel_name = self.interfaces[0]
2011
2012 def __call__(self):
2013 self.database = self.database or config('database')
2014@@ -175,13 +280,14 @@
2015 unit=local_unit())
2016 if set_hostname != access_hostname:
2017 relation_set(relation_settings={hostname_key: access_hostname})
2018- return ctxt # Defer any further hook execution for now....
2019+ return None # Defer any further hook execution for now....
2020
2021 password_setting = 'password'
2022 if self.relation_prefix:
2023 password_setting = self.relation_prefix + '_password'
2024
2025- for rid in relation_ids('shared-db'):
2026+ for rid in relation_ids(self.interfaces[0]):
2027+ self.related = True
2028 for unit in related_units(rid):
2029 rdata = relation_get(rid=rid, unit=unit)
2030 host = rdata.get('db_host')
2031@@ -193,7 +299,7 @@
2032 'database_password': rdata.get(password_setting),
2033 'database_type': 'mysql'
2034 }
2035- if context_complete(ctxt):
2036+ if self.context_complete(ctxt):
2037 db_ssl(rdata, ctxt, self.ssl_dir)
2038 return ctxt
2039 return {}
2040@@ -214,6 +320,7 @@
2041
2042 ctxt = {}
2043 for rid in relation_ids(self.interfaces[0]):
2044+ self.related = True
2045 for unit in related_units(rid):
2046 rel_host = relation_get('host', rid=rid, unit=unit)
2047 rel_user = relation_get('user', rid=rid, unit=unit)
2048@@ -223,7 +330,7 @@
2049 'database_user': rel_user,
2050 'database_password': rel_passwd,
2051 'database_type': 'postgresql'}
2052- if context_complete(ctxt):
2053+ if self.context_complete(ctxt):
2054 return ctxt
2055
2056 return {}
2057@@ -261,12 +368,30 @@
2058
2059
2060 class IdentityServiceContext(OSContextGenerator):
2061- interfaces = ['identity-service']
2062+
2063+ def __init__(self, service=None, service_user=None, rel_name='identity-service'):
2064+ self.service = service
2065+ self.service_user = service_user
2066+ self.rel_name = rel_name
2067+ self.interfaces = [self.rel_name]
2068
2069 def __call__(self):
2070- log('Generating template context for identity-service', level=DEBUG)
2071+ log('Generating template context for ' + self.rel_name, level=DEBUG)
2072 ctxt = {}
2073- for rid in relation_ids('identity-service'):
2074+
2075+ if self.service and self.service_user:
2076+ # This is required for pki token signing if we don't want /tmp to
2077+ # be used.
2078+ cachedir = '/var/cache/%s' % (self.service)
2079+ if not os.path.isdir(cachedir):
2080+ log("Creating service cache dir %s" % (cachedir), level=DEBUG)
2081+ mkdir(path=cachedir, owner=self.service_user,
2082+ group=self.service_user, perms=0o700)
2083+
2084+ ctxt['signing_dir'] = cachedir
2085+
2086+ for rid in relation_ids(self.rel_name):
2087+ self.related = True
2088 for unit in related_units(rid):
2089 rdata = relation_get(rid=rid, unit=unit)
2090 serv_host = rdata.get('service_host')
2091@@ -275,16 +400,17 @@
2092 auth_host = format_ipv6_addr(auth_host) or auth_host
2093 svc_protocol = rdata.get('service_protocol') or 'http'
2094 auth_protocol = rdata.get('auth_protocol') or 'http'
2095- ctxt = {'service_port': rdata.get('service_port'),
2096- 'service_host': serv_host,
2097- 'auth_host': auth_host,
2098- 'auth_port': rdata.get('auth_port'),
2099- 'admin_tenant_name': rdata.get('service_tenant'),
2100- 'admin_user': rdata.get('service_username'),
2101- 'admin_password': rdata.get('service_password'),
2102- 'service_protocol': svc_protocol,
2103- 'auth_protocol': auth_protocol}
2104- if context_complete(ctxt):
2105+ ctxt.update({'service_port': rdata.get('service_port'),
2106+ 'service_host': serv_host,
2107+ 'auth_host': auth_host,
2108+ 'auth_port': rdata.get('auth_port'),
2109+ 'admin_tenant_name': rdata.get('service_tenant'),
2110+ 'admin_user': rdata.get('service_username'),
2111+ 'admin_password': rdata.get('service_password'),
2112+ 'service_protocol': svc_protocol,
2113+ 'auth_protocol': auth_protocol})
2114+
2115+ if self.context_complete(ctxt):
2116 # NOTE(jamespage) this is required for >= icehouse
2117 # so a missing value just indicates keystone needs
2118 # upgrading
2119@@ -323,6 +449,7 @@
2120 ctxt = {}
2121 for rid in relation_ids(self.rel_name):
2122 ha_vip_only = False
2123+ self.related = True
2124 for unit in related_units(rid):
2125 if relation_get('clustered', rid=rid, unit=unit):
2126 ctxt['clustered'] = True
2127@@ -355,7 +482,7 @@
2128 ha_vip_only = relation_get('ha-vip-only',
2129 rid=rid, unit=unit) is not None
2130
2131- if context_complete(ctxt):
2132+ if self.context_complete(ctxt):
2133 if 'rabbit_ssl_ca' in ctxt:
2134 if not self.ssl_dir:
2135 log("Charm not setup for ssl support but ssl ca "
2136@@ -382,7 +509,12 @@
2137
2138 ctxt['rabbitmq_hosts'] = ','.join(sorted(rabbitmq_hosts))
2139
2140- if not context_complete(ctxt):
2141+ oslo_messaging_flags = conf.get('oslo-messaging-flags', None)
2142+ if oslo_messaging_flags:
2143+ ctxt['oslo_messaging_flags'] = config_flags_parser(
2144+ oslo_messaging_flags)
2145+
2146+ if not self.complete:
2147 return {}
2148
2149 return ctxt
2150@@ -398,13 +530,15 @@
2151
2152 log('Generating template context for ceph', level=DEBUG)
2153 mon_hosts = []
2154- auth = None
2155- key = None
2156- use_syslog = str(config('use-syslog')).lower()
2157+ ctxt = {
2158+ 'use_syslog': str(config('use-syslog')).lower()
2159+ }
2160 for rid in relation_ids('ceph'):
2161 for unit in related_units(rid):
2162- auth = relation_get('auth', rid=rid, unit=unit)
2163- key = relation_get('key', rid=rid, unit=unit)
2164+ if not ctxt.get('auth'):
2165+ ctxt['auth'] = relation_get('auth', rid=rid, unit=unit)
2166+ if not ctxt.get('key'):
2167+ ctxt['key'] = relation_get('key', rid=rid, unit=unit)
2168 ceph_pub_addr = relation_get('ceph-public-address', rid=rid,
2169 unit=unit)
2170 unit_priv_addr = relation_get('private-address', rid=rid,
2171@@ -413,15 +547,12 @@
2172 ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr
2173 mon_hosts.append(ceph_addr)
2174
2175- ctxt = {'mon_hosts': ' '.join(sorted(mon_hosts)),
2176- 'auth': auth,
2177- 'key': key,
2178- 'use_syslog': use_syslog}
2179+ ctxt['mon_hosts'] = ' '.join(sorted(mon_hosts))
2180
2181 if not os.path.isdir('/etc/ceph'):
2182 os.mkdir('/etc/ceph')
2183
2184- if not context_complete(ctxt):
2185+ if not self.context_complete(ctxt):
2186 return {}
2187
2188 ensure_packages(['ceph-common'])
2189@@ -661,7 +792,14 @@
2190 'endpoints': [],
2191 'ext_ports': []}
2192
2193- for cn in self.canonical_names():
2194+ cns = self.canonical_names()
2195+ if cns:
2196+ for cn in cns:
2197+ self.configure_cert(cn)
2198+ else:
2199+ # Expect cert/key provided in config (currently assumed that ca
2200+ # uses ip for cn)
2201+ cn = resolve_address(endpoint_type=INTERNAL)
2202 self.configure_cert(cn)
2203
2204 addresses = self.get_network_addresses()
2205@@ -724,6 +862,19 @@
2206
2207 return ovs_ctxt
2208
2209+ def nuage_ctxt(self):
2210+ driver = neutron_plugin_attribute(self.plugin, 'driver',
2211+ self.network_manager)
2212+ config = neutron_plugin_attribute(self.plugin, 'config',
2213+ self.network_manager)
2214+ nuage_ctxt = {'core_plugin': driver,
2215+ 'neutron_plugin': 'vsp',
2216+ 'neutron_security_groups': self.neutron_security_groups,
2217+ 'local_ip': unit_private_ip(),
2218+ 'config': config}
2219+
2220+ return nuage_ctxt
2221+
2222 def nvp_ctxt(self):
2223 driver = neutron_plugin_attribute(self.plugin, 'driver',
2224 self.network_manager)
2225@@ -788,9 +939,19 @@
2226 'neutron_url': '%s://%s:%s' % (proto, host, '9696')}
2227 return ctxt
2228
2229+ def pg_ctxt(self):
2230+ driver = neutron_plugin_attribute(self.plugin, 'driver',
2231+ self.network_manager)
2232+ config = neutron_plugin_attribute(self.plugin, 'config',
2233+ self.network_manager)
2234+ ovs_ctxt = {'core_plugin': driver,
2235+ 'neutron_plugin': 'plumgrid',
2236+ 'neutron_security_groups': self.neutron_security_groups,
2237+ 'local_ip': unit_private_ip(),
2238+ 'config': config}
2239+ return ovs_ctxt
2240+
2241 def __call__(self):
2242- self._ensure_packages()
2243-
2244 if self.network_manager not in ['quantum', 'neutron']:
2245 return {}
2246
2247@@ -807,6 +968,10 @@
2248 ctxt.update(self.n1kv_ctxt())
2249 elif self.plugin == 'Calico':
2250 ctxt.update(self.calico_ctxt())
2251+ elif self.plugin == 'vsp':
2252+ ctxt.update(self.nuage_ctxt())
2253+ elif self.plugin == 'plumgrid':
2254+ ctxt.update(self.pg_ctxt())
2255
2256 alchemy_flags = config('neutron-alchemy-flags')
2257 if alchemy_flags:
2258@@ -817,6 +982,59 @@
2259 return ctxt
2260
2261
2262+class NeutronPortContext(OSContextGenerator):
2263+
2264+ def resolve_ports(self, ports):
2265+ """Resolve NICs not yet bound to bridge(s)
2266+
2267+ If hwaddress provided then returns resolved hwaddress otherwise NIC.
2268+ """
2269+ if not ports:
2270+ return None
2271+
2272+ hwaddr_to_nic = {}
2273+ hwaddr_to_ip = {}
2274+ for nic in list_nics():
2275+ # Ignore virtual interfaces (bond masters will be identified from
2276+ # their slaves)
2277+ if not is_phy_iface(nic):
2278+ continue
2279+
2280+ _nic = get_bond_master(nic)
2281+ if _nic:
2282+ log("Replacing iface '%s' with bond master '%s'" % (nic, _nic),
2283+ level=DEBUG)
2284+ nic = _nic
2285+
2286+ hwaddr = get_nic_hwaddr(nic)
2287+ hwaddr_to_nic[hwaddr] = nic
2288+ addresses = get_ipv4_addr(nic, fatal=False)
2289+ addresses += get_ipv6_addr(iface=nic, fatal=False)
2290+ hwaddr_to_ip[hwaddr] = addresses
2291+
2292+ resolved = []
2293+ mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I)
2294+ for entry in ports:
2295+ if re.match(mac_regex, entry):
2296+ # NIC is in known NICs and does NOT hace an IP address
2297+ if entry in hwaddr_to_nic and not hwaddr_to_ip[entry]:
2298+ # If the nic is part of a bridge then don't use it
2299+ if is_bridge_member(hwaddr_to_nic[entry]):
2300+ continue
2301+
2302+ # Entry is a MAC address for a valid interface that doesn't
2303+ # have an IP address assigned yet.
2304+ resolved.append(hwaddr_to_nic[entry])
2305+ else:
2306+ # If the passed entry is not a MAC address, assume it's a valid
2307+ # interface, and that the user put it there on purpose (we can
2308+ # trust it to be the real external network).
2309+ resolved.append(entry)
2310+
2311+ # Ensure no duplicates
2312+ return list(set(resolved))
2313+
2314+
2315 class OSConfigFlagContext(OSContextGenerator):
2316 """Provides support for user-defined config flags.
2317
2318@@ -904,13 +1122,22 @@
2319 :param config_file : Service's config file to query sections
2320 :param interface : Subordinate interface to inspect
2321 """
2322- self.service = service
2323 self.config_file = config_file
2324- self.interface = interface
2325+ if isinstance(service, list):
2326+ self.services = service
2327+ else:
2328+ self.services = [service]
2329+ if isinstance(interface, list):
2330+ self.interfaces = interface
2331+ else:
2332+ self.interfaces = [interface]
2333
2334 def __call__(self):
2335 ctxt = {'sections': {}}
2336- for rid in relation_ids(self.interface):
2337+ rids = []
2338+ for interface in self.interfaces:
2339+ rids.extend(relation_ids(interface))
2340+ for rid in rids:
2341 for unit in related_units(rid):
2342 sub_config = relation_get('subordinate_configuration',
2343 rid=rid, unit=unit)
2344@@ -922,29 +1149,32 @@
2345 'setting from %s' % rid, level=ERROR)
2346 continue
2347
2348- if self.service not in sub_config:
2349- log('Found subordinate_config on %s but it contained'
2350- 'nothing for %s service' % (rid, self.service),
2351- level=INFO)
2352- continue
2353-
2354- sub_config = sub_config[self.service]
2355- if self.config_file not in sub_config:
2356- log('Found subordinate_config on %s but it contained'
2357- 'nothing for %s' % (rid, self.config_file),
2358- level=INFO)
2359- continue
2360-
2361- sub_config = sub_config[self.config_file]
2362- for k, v in six.iteritems(sub_config):
2363- if k == 'sections':
2364- for section, config_dict in six.iteritems(v):
2365- log("adding section '%s'" % (section),
2366- level=DEBUG)
2367- ctxt[k][section] = config_dict
2368- else:
2369- ctxt[k] = v
2370-
2371+ for service in self.services:
2372+ if service not in sub_config:
2373+ log('Found subordinate_config on %s but it contained'
2374+ 'nothing for %s service' % (rid, service),
2375+ level=INFO)
2376+ continue
2377+
2378+ sub_config = sub_config[service]
2379+ if self.config_file not in sub_config:
2380+ log('Found subordinate_config on %s but it contained'
2381+ 'nothing for %s' % (rid, self.config_file),
2382+ level=INFO)
2383+ continue
2384+
2385+ sub_config = sub_config[self.config_file]
2386+ for k, v in six.iteritems(sub_config):
2387+ if k == 'sections':
2388+ for section, config_list in six.iteritems(v):
2389+ log("adding section '%s'" % (section),
2390+ level=DEBUG)
2391+ if ctxt[k].get(section):
2392+ ctxt[k][section].extend(config_list)
2393+ else:
2394+ ctxt[k][section] = config_list
2395+ else:
2396+ ctxt[k] = v
2397 log("%d section(s) found" % (len(ctxt['sections'])), level=DEBUG)
2398 return ctxt
2399
2400@@ -1005,6 +1235,8 @@
2401 for unit in related_units(rid):
2402 ctxt['zmq_nonce'] = relation_get('nonce', unit, rid)
2403 ctxt['zmq_host'] = relation_get('host', unit, rid)
2404+ ctxt['zmq_redis_address'] = relation_get(
2405+ 'zmq_redis_address', unit, rid)
2406
2407 return ctxt
2408
2409@@ -1036,3 +1268,149 @@
2410 sysctl_create(sysctl_dict,
2411 '/etc/sysctl.d/50-{0}.conf'.format(charm_name()))
2412 return {'sysctl': sysctl_dict}
2413+
2414+
2415+class NeutronAPIContext(OSContextGenerator):
2416+ '''
2417+ Inspects current neutron-plugin-api relation for neutron settings. Return
2418+ defaults if it is not present.
2419+ '''
2420+ interfaces = ['neutron-plugin-api']
2421+
2422+ def __call__(self):
2423+ self.neutron_defaults = {
2424+ 'l2_population': {
2425+ 'rel_key': 'l2-population',
2426+ 'default': False,
2427+ },
2428+ 'overlay_network_type': {
2429+ 'rel_key': 'overlay-network-type',
2430+ 'default': 'gre',
2431+ },
2432+ 'neutron_security_groups': {
2433+ 'rel_key': 'neutron-security-groups',
2434+ 'default': False,
2435+ },
2436+ 'network_device_mtu': {
2437+ 'rel_key': 'network-device-mtu',
2438+ 'default': None,
2439+ },
2440+ 'enable_dvr': {
2441+ 'rel_key': 'enable-dvr',
2442+ 'default': False,
2443+ },
2444+ 'enable_l3ha': {
2445+ 'rel_key': 'enable-l3ha',
2446+ 'default': False,
2447+ },
2448+ }
2449+ ctxt = self.get_neutron_options({})
2450+ for rid in relation_ids('neutron-plugin-api'):
2451+ for unit in related_units(rid):
2452+ rdata = relation_get(rid=rid, unit=unit)
2453+ if 'l2-population' in rdata:
2454+ ctxt.update(self.get_neutron_options(rdata))
2455+
2456+ return ctxt
2457+
2458+ def get_neutron_options(self, rdata):
2459+ settings = {}
2460+ for nkey in self.neutron_defaults.keys():
2461+ defv = self.neutron_defaults[nkey]['default']
2462+ rkey = self.neutron_defaults[nkey]['rel_key']
2463+ if rkey in rdata.keys():
2464+ if type(defv) is bool:
2465+ settings[nkey] = bool_from_string(rdata[rkey])
2466+ else:
2467+ settings[nkey] = rdata[rkey]
2468+ else:
2469+ settings[nkey] = defv
2470+ return settings
2471+
2472+
2473+class ExternalPortContext(NeutronPortContext):
2474+
2475+ def __call__(self):
2476+ ctxt = {}
2477+ ports = config('ext-port')
2478+ if ports:
2479+ ports = [p.strip() for p in ports.split()]
2480+ ports = self.resolve_ports(ports)
2481+ if ports:
2482+ ctxt = {"ext_port": ports[0]}
2483+ napi_settings = NeutronAPIContext()()
2484+ mtu = napi_settings.get('network_device_mtu')
2485+ if mtu:
2486+ ctxt['ext_port_mtu'] = mtu
2487+
2488+ return ctxt
2489+
2490+
2491+class DataPortContext(NeutronPortContext):
2492+
2493+ def __call__(self):
2494+ ports = config('data-port')
2495+ if ports:
2496+ # Map of {port/mac:bridge}
2497+ portmap = parse_data_port_mappings(ports)
2498+ ports = portmap.keys()
2499+ # Resolve provided ports or mac addresses and filter out those
2500+ # already attached to a bridge.
2501+ resolved = self.resolve_ports(ports)
2502+ # FIXME: is this necessary?
2503+ normalized = {get_nic_hwaddr(port): port for port in resolved
2504+ if port not in ports}
2505+ normalized.update({port: port for port in resolved
2506+ if port in ports})
2507+ if resolved:
2508+ return {bridge: normalized[port] for port, bridge in
2509+ six.iteritems(portmap) if port in normalized.keys()}
2510+
2511+ return None
2512+
2513+
2514+class PhyNICMTUContext(DataPortContext):
2515+
2516+ def __call__(self):
2517+ ctxt = {}
2518+ mappings = super(PhyNICMTUContext, self).__call__()
2519+ if mappings and mappings.values():
2520+ ports = mappings.values()
2521+ napi_settings = NeutronAPIContext()()
2522+ mtu = napi_settings.get('network_device_mtu')
2523+ if mtu:
2524+ ctxt["devs"] = '\\n'.join(ports)
2525+ ctxt['mtu'] = mtu
2526+
2527+ return ctxt
2528+
2529+
2530+class NetworkServiceContext(OSContextGenerator):
2531+
2532+ def __init__(self, rel_name='quantum-network-service'):
2533+ self.rel_name = rel_name
2534+ self.interfaces = [rel_name]
2535+
2536+ def __call__(self):
2537+ for rid in relation_ids(self.rel_name):
2538+ for unit in related_units(rid):
2539+ rdata = relation_get(rid=rid, unit=unit)
2540+ ctxt = {
2541+ 'keystone_host': rdata.get('keystone_host'),
2542+ 'service_port': rdata.get('service_port'),
2543+ 'auth_port': rdata.get('auth_port'),
2544+ 'service_tenant': rdata.get('service_tenant'),
2545+ 'service_username': rdata.get('service_username'),
2546+ 'service_password': rdata.get('service_password'),
2547+ 'quantum_host': rdata.get('quantum_host'),
2548+ 'quantum_port': rdata.get('quantum_port'),
2549+ 'quantum_url': rdata.get('quantum_url'),
2550+ 'region': rdata.get('region'),
2551+ 'service_protocol':
2552+ rdata.get('service_protocol') or 'http',
2553+ 'auth_protocol':
2554+ rdata.get('auth_protocol') or 'http',
2555+ }
2556+ if self.context_complete(ctxt):
2557+ return ctxt
2558+ return {}
2559
2560=== added directory 'hooks/charmhelpers/contrib/openstack/files'
2561=== added file 'hooks/charmhelpers/contrib/openstack/files/__init__.py'
2562--- hooks/charmhelpers/contrib/openstack/files/__init__.py 1970-01-01 00:00:00 +0000
2563+++ hooks/charmhelpers/contrib/openstack/files/__init__.py 2015-09-21 10:47:21 +0000
2564@@ -0,0 +1,18 @@
2565+# Copyright 2014-2015 Canonical Limited.
2566+#
2567+# This file is part of charm-helpers.
2568+#
2569+# charm-helpers is free software: you can redistribute it and/or modify
2570+# it under the terms of the GNU Lesser General Public License version 3 as
2571+# published by the Free Software Foundation.
2572+#
2573+# charm-helpers is distributed in the hope that it will be useful,
2574+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2575+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2576+# GNU Lesser General Public License for more details.
2577+#
2578+# You should have received a copy of the GNU Lesser General Public License
2579+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2580+
2581+# dummy __init__.py to fool syncer into thinking this is a syncable python
2582+# module
2583
2584=== added file 'hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh'
2585--- hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh 1970-01-01 00:00:00 +0000
2586+++ hooks/charmhelpers/contrib/openstack/files/check_haproxy.sh 2015-09-21 10:47:21 +0000
2587@@ -0,0 +1,32 @@
2588+#!/bin/bash
2589+#--------------------------------------------
2590+# This file is managed by Juju
2591+#--------------------------------------------
2592+#
2593+# Copyright 2009,2012 Canonical Ltd.
2594+# Author: Tom Haddon
2595+
2596+CRITICAL=0
2597+NOTACTIVE=''
2598+LOGFILE=/var/log/nagios/check_haproxy.log
2599+AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
2600+
2601+for appserver in $(grep ' server' /etc/haproxy/haproxy.cfg | awk '{print $2'});
2602+do
2603+ output=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 --regex="class=\"(active|backup)(2|3).*${appserver}" -e ' 200 OK')
2604+ if [ $? != 0 ]; then
2605+ date >> $LOGFILE
2606+ echo $output >> $LOGFILE
2607+ /usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -v | grep $appserver >> $LOGFILE 2>&1
2608+ CRITICAL=1
2609+ NOTACTIVE="${NOTACTIVE} $appserver"
2610+ fi
2611+done
2612+
2613+if [ $CRITICAL = 1 ]; then
2614+ echo "CRITICAL:${NOTACTIVE}"
2615+ exit 2
2616+fi
2617+
2618+echo "OK: All haproxy instances looking good"
2619+exit 0
2620
2621=== added file 'hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh'
2622--- hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh 1970-01-01 00:00:00 +0000
2623+++ hooks/charmhelpers/contrib/openstack/files/check_haproxy_queue_depth.sh 2015-09-21 10:47:21 +0000
2624@@ -0,0 +1,30 @@
2625+#!/bin/bash
2626+#--------------------------------------------
2627+# This file is managed by Juju
2628+#--------------------------------------------
2629+#
2630+# Copyright 2009,2012 Canonical Ltd.
2631+# Author: Tom Haddon
2632+
2633+# These should be config options at some stage
2634+CURRQthrsh=0
2635+MAXQthrsh=100
2636+
2637+AUTH=$(grep -r "stats auth" /etc/haproxy | head -1 | awk '{print $4}')
2638+
2639+HAPROXYSTATS=$(/usr/lib/nagios/plugins/check_http -a ${AUTH} -I 127.0.0.1 -p 8888 -u '/;csv' -v)
2640+
2641+for BACKEND in $(echo $HAPROXYSTATS| xargs -n1 | grep BACKEND | awk -F , '{print $1}')
2642+do
2643+ CURRQ=$(echo "$HAPROXYSTATS" | grep $BACKEND | grep BACKEND | cut -d , -f 3)
2644+ MAXQ=$(echo "$HAPROXYSTATS" | grep $BACKEND | grep BACKEND | cut -d , -f 4)
2645+
2646+ if [[ $CURRQ -gt $CURRQthrsh || $MAXQ -gt $MAXQthrsh ]] ; then
2647+ echo "CRITICAL: queue depth for $BACKEND - CURRENT:$CURRQ MAX:$MAXQ"
2648+ exit 2
2649+ fi
2650+done
2651+
2652+echo "OK: All haproxy queue depths looking good"
2653+exit 0
2654+
2655
2656=== modified file 'hooks/charmhelpers/contrib/openstack/ip.py'
2657--- hooks/charmhelpers/contrib/openstack/ip.py 2014-12-10 20:28:57 +0000
2658+++ hooks/charmhelpers/contrib/openstack/ip.py 2015-09-21 10:47:21 +0000
2659@@ -1,6 +1,23 @@
2660+# Copyright 2014-2015 Canonical Limited.
2661+#
2662+# This file is part of charm-helpers.
2663+#
2664+# charm-helpers is free software: you can redistribute it and/or modify
2665+# it under the terms of the GNU Lesser General Public License version 3 as
2666+# published by the Free Software Foundation.
2667+#
2668+# charm-helpers is distributed in the hope that it will be useful,
2669+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2670+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2671+# GNU Lesser General Public License for more details.
2672+#
2673+# You should have received a copy of the GNU Lesser General Public License
2674+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2675+
2676 from charmhelpers.core.hookenv import (
2677 config,
2678 unit_get,
2679+ service_name,
2680 )
2681 from charmhelpers.contrib.network.ip import (
2682 get_address_in_network,
2683@@ -17,15 +34,18 @@
2684 ADDRESS_MAP = {
2685 PUBLIC: {
2686 'config': 'os-public-network',
2687- 'fallback': 'public-address'
2688+ 'fallback': 'public-address',
2689+ 'override': 'os-public-hostname',
2690 },
2691 INTERNAL: {
2692 'config': 'os-internal-network',
2693- 'fallback': 'private-address'
2694+ 'fallback': 'private-address',
2695+ 'override': 'os-internal-hostname',
2696 },
2697 ADMIN: {
2698 'config': 'os-admin-network',
2699- 'fallback': 'private-address'
2700+ 'fallback': 'private-address',
2701+ 'override': 'os-admin-hostname',
2702 }
2703 }
2704
2705@@ -39,15 +59,50 @@
2706 :param endpoint_type: str endpoint type to resolve.
2707 :param returns: str base URL for services on the current service unit.
2708 """
2709- scheme = 'http'
2710- if 'https' in configs.complete_contexts():
2711- scheme = 'https'
2712+ scheme = _get_scheme(configs)
2713+
2714 address = resolve_address(endpoint_type)
2715 if is_ipv6(address):
2716 address = "[{}]".format(address)
2717+
2718 return '%s://%s' % (scheme, address)
2719
2720
2721+def _get_scheme(configs):
2722+ """Returns the scheme to use for the url (either http or https)
2723+ depending upon whether https is in the configs value.
2724+
2725+ :param configs: OSTemplateRenderer config templating object to inspect
2726+ for a complete https context.
2727+ :returns: either 'http' or 'https' depending on whether https is
2728+ configured within the configs context.
2729+ """
2730+ scheme = 'http'
2731+ if configs and 'https' in configs.complete_contexts():
2732+ scheme = 'https'
2733+ return scheme
2734+
2735+
2736+def _get_address_override(endpoint_type=PUBLIC):
2737+ """Returns any address overrides that the user has defined based on the
2738+ endpoint type.
2739+
2740+ Note: this function allows for the service name to be inserted into the
2741+ address if the user specifies {service_name}.somehost.org.
2742+
2743+ :param endpoint_type: the type of endpoint to retrieve the override
2744+ value for.
2745+ :returns: any endpoint address or hostname that the user has overridden
2746+ or None if an override is not present.
2747+ """
2748+ override_key = ADDRESS_MAP[endpoint_type]['override']
2749+ addr_override = config(override_key)
2750+ if not addr_override:
2751+ return None
2752+ else:
2753+ return addr_override.format(service_name=service_name())
2754+
2755+
2756 def resolve_address(endpoint_type=PUBLIC):
2757 """Return unit address depending on net config.
2758
2759@@ -59,7 +114,10 @@
2760
2761 :param endpoint_type: Network endpoing type
2762 """
2763- resolved_address = None
2764+ resolved_address = _get_address_override(endpoint_type)
2765+ if resolved_address:
2766+ return resolved_address
2767+
2768 vips = config('vip')
2769 if vips:
2770 vips = vips.split()
2771
2772=== modified file 'hooks/charmhelpers/contrib/openstack/neutron.py'
2773--- hooks/charmhelpers/contrib/openstack/neutron.py 2015-01-14 15:30:19 +0000
2774+++ hooks/charmhelpers/contrib/openstack/neutron.py 2015-09-21 10:47:21 +0000
2775@@ -1,5 +1,22 @@
2776+# Copyright 2014-2015 Canonical Limited.
2777+#
2778+# This file is part of charm-helpers.
2779+#
2780+# charm-helpers is free software: you can redistribute it and/or modify
2781+# it under the terms of the GNU Lesser General Public License version 3 as
2782+# published by the Free Software Foundation.
2783+#
2784+# charm-helpers is distributed in the hope that it will be useful,
2785+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2786+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2787+# GNU Lesser General Public License for more details.
2788+#
2789+# You should have received a copy of the GNU Lesser General Public License
2790+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2791+
2792 # Various utilies for dealing with Neutron and the renaming from Quantum.
2793
2794+import six
2795 from subprocess import check_output
2796
2797 from charmhelpers.core.hookenv import (
2798@@ -155,13 +172,42 @@
2799 'services': ['calico-felix',
2800 'bird',
2801 'neutron-dhcp-agent',
2802- 'nova-api-metadata'],
2803+ 'nova-api-metadata',
2804+ 'etcd'],
2805 'packages': [[headers_package()] + determine_dkms_package(),
2806 ['calico-compute',
2807 'bird',
2808 'neutron-dhcp-agent',
2809- 'nova-api-metadata']],
2810- 'server_packages': ['neutron-server', 'calico-control'],
2811+ 'nova-api-metadata',
2812+ 'etcd']],
2813+ 'server_packages': ['neutron-server', 'calico-control', 'etcd'],
2814+ 'server_services': ['neutron-server', 'etcd']
2815+ },
2816+ 'vsp': {
2817+ 'config': '/etc/neutron/plugins/nuage/nuage_plugin.ini',
2818+ 'driver': 'neutron.plugins.nuage.plugin.NuagePlugin',
2819+ 'contexts': [
2820+ context.SharedDBContext(user=config('neutron-database-user'),
2821+ database=config('neutron-database'),
2822+ relation_prefix='neutron',
2823+ ssl_dir=NEUTRON_CONF_DIR)],
2824+ 'services': [],
2825+ 'packages': [],
2826+ 'server_packages': ['neutron-server', 'neutron-plugin-nuage'],
2827+ 'server_services': ['neutron-server']
2828+ },
2829+ 'plumgrid': {
2830+ 'config': '/etc/neutron/plugins/plumgrid/plumgrid.ini',
2831+ 'driver': 'neutron.plugins.plumgrid.plumgrid_plugin.plumgrid_plugin.NeutronPluginPLUMgridV2',
2832+ 'contexts': [
2833+ context.SharedDBContext(user=config('database-user'),
2834+ database=config('database'),
2835+ ssl_dir=NEUTRON_CONF_DIR)],
2836+ 'services': [],
2837+ 'packages': [['plumgrid-lxc'],
2838+ ['iovisor-dkms']],
2839+ 'server_packages': ['neutron-server',
2840+ 'neutron-plugin-plumgrid'],
2841 'server_services': ['neutron-server']
2842 }
2843 }
2844@@ -221,3 +267,90 @@
2845 else:
2846 # ensure accurate naming for all releases post-H
2847 return 'neutron'
2848+
2849+
2850+def parse_mappings(mappings, key_rvalue=False):
2851+ """By default mappings are lvalue keyed.
2852+
2853+ If key_rvalue is True, the mapping will be reversed to allow multiple
2854+ configs for the same lvalue.
2855+ """
2856+ parsed = {}
2857+ if mappings:
2858+ mappings = mappings.split()
2859+ for m in mappings:
2860+ p = m.partition(':')
2861+
2862+ if key_rvalue:
2863+ key_index = 2
2864+ val_index = 0
2865+ # if there is no rvalue skip to next
2866+ if not p[1]:
2867+ continue
2868+ else:
2869+ key_index = 0
2870+ val_index = 2
2871+
2872+ key = p[key_index].strip()
2873+ parsed[key] = p[val_index].strip()
2874+
2875+ return parsed
2876+
2877+
2878+def parse_bridge_mappings(mappings):
2879+ """Parse bridge mappings.
2880+
2881+ Mappings must be a space-delimited list of provider:bridge mappings.
2882+
2883+ Returns dict of the form {provider:bridge}.
2884+ """
2885+ return parse_mappings(mappings)
2886+
2887+
2888+def parse_data_port_mappings(mappings, default_bridge='br-data'):
2889+ """Parse data port mappings.
2890+
2891+ Mappings must be a space-delimited list of port:bridge mappings.
2892+
2893+ Returns dict of the form {port:bridge} where port may be an mac address or
2894+ interface name.
2895+ """
2896+
2897+ # NOTE(dosaboy): we use rvalue for key to allow multiple values to be
2898+ # proposed for <port> since it may be a mac address which will differ
2899+ # across units this allowing first-known-good to be chosen.
2900+ _mappings = parse_mappings(mappings, key_rvalue=True)
2901+ if not _mappings or list(_mappings.values()) == ['']:
2902+ if not mappings:
2903+ return {}
2904+
2905+ # For backwards-compatibility we need to support port-only provided in
2906+ # config.
2907+ _mappings = {mappings.split()[0]: default_bridge}
2908+
2909+ ports = _mappings.keys()
2910+ if len(set(ports)) != len(ports):
2911+ raise Exception("It is not allowed to have the same port configured "
2912+ "on more than one bridge")
2913+
2914+ return _mappings
2915+
2916+
2917+def parse_vlan_range_mappings(mappings):
2918+ """Parse vlan range mappings.
2919+
2920+ Mappings must be a space-delimited list of provider:start:end mappings.
2921+
2922+ The start:end range is optional and may be omitted.
2923+
2924+ Returns dict of the form {provider: (start, end)}.
2925+ """
2926+ _mappings = parse_mappings(mappings)
2927+ if not _mappings:
2928+ return {}
2929+
2930+ mappings = {}
2931+ for p, r in six.iteritems(_mappings):
2932+ mappings[p] = tuple(r.split(':'))
2933+
2934+ return mappings
2935
2936=== modified file 'hooks/charmhelpers/contrib/openstack/templates/__init__.py'
2937--- hooks/charmhelpers/contrib/openstack/templates/__init__.py 2013-07-19 02:37:30 +0000
2938+++ hooks/charmhelpers/contrib/openstack/templates/__init__.py 2015-09-21 10:47:21 +0000
2939@@ -1,2 +1,18 @@
2940+# Copyright 2014-2015 Canonical Limited.
2941+#
2942+# This file is part of charm-helpers.
2943+#
2944+# charm-helpers is free software: you can redistribute it and/or modify
2945+# it under the terms of the GNU Lesser General Public License version 3 as
2946+# published by the Free Software Foundation.
2947+#
2948+# charm-helpers is distributed in the hope that it will be useful,
2949+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2950+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2951+# GNU Lesser General Public License for more details.
2952+#
2953+# You should have received a copy of the GNU Lesser General Public License
2954+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2955+
2956 # dummy __init__.py to fool syncer into thinking this is a syncable python
2957 # module
2958
2959=== modified file 'hooks/charmhelpers/contrib/openstack/templates/ceph.conf'
2960--- hooks/charmhelpers/contrib/openstack/templates/ceph.conf 2014-04-04 16:45:38 +0000
2961+++ hooks/charmhelpers/contrib/openstack/templates/ceph.conf 2015-09-21 10:47:21 +0000
2962@@ -5,11 +5,11 @@
2963 ###############################################################################
2964 [global]
2965 {% if auth -%}
2966- auth_supported = {{ auth }}
2967- keyring = /etc/ceph/$cluster.$name.keyring
2968- mon host = {{ mon_hosts }}
2969+auth_supported = {{ auth }}
2970+keyring = /etc/ceph/$cluster.$name.keyring
2971+mon host = {{ mon_hosts }}
2972 {% endif -%}
2973- log to syslog = {{ use_syslog }}
2974- err to syslog = {{ use_syslog }}
2975- clog to syslog = {{ use_syslog }}
2976+log to syslog = {{ use_syslog }}
2977+err to syslog = {{ use_syslog }}
2978+clog to syslog = {{ use_syslog }}
2979
2980
2981=== added file 'hooks/charmhelpers/contrib/openstack/templates/git.upstart'
2982--- hooks/charmhelpers/contrib/openstack/templates/git.upstart 1970-01-01 00:00:00 +0000
2983+++ hooks/charmhelpers/contrib/openstack/templates/git.upstart 2015-09-21 10:47:21 +0000
2984@@ -0,0 +1,17 @@
2985+description "{{ service_description }}"
2986+author "Juju {{ service_name }} Charm <juju@localhost>"
2987+
2988+start on runlevel [2345]
2989+stop on runlevel [!2345]
2990+
2991+respawn
2992+
2993+exec start-stop-daemon --start --chuid {{ user_name }} \
2994+ --chdir {{ start_dir }} --name {{ process_name }} \
2995+ --exec {{ executable_name }} -- \
2996+ {% for config_file in config_files -%}
2997+ --config-file={{ config_file }} \
2998+ {% endfor -%}
2999+ {% if log_file -%}
3000+ --log-file={{ log_file }}
3001+ {% endif -%}
3002
3003=== added file 'hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken'
3004--- hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken 1970-01-01 00:00:00 +0000
3005+++ hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken 2015-09-21 10:47:21 +0000
3006@@ -0,0 +1,9 @@
3007+{% if auth_host -%}
3008+[keystone_authtoken]
3009+identity_uri = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/{{ auth_admin_prefix }}
3010+auth_uri = {{ service_protocol }}://{{ service_host }}:{{ service_port }}/{{ service_admin_prefix }}
3011+admin_tenant_name = {{ admin_tenant_name }}
3012+admin_user = {{ admin_user }}
3013+admin_password = {{ admin_password }}
3014+signing_dir = {{ signing_dir }}
3015+{% endif -%}
3016
3017=== added file 'hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo'
3018--- hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo 1970-01-01 00:00:00 +0000
3019+++ hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo 2015-09-21 10:47:21 +0000
3020@@ -0,0 +1,22 @@
3021+{% if rabbitmq_host or rabbitmq_hosts -%}
3022+[oslo_messaging_rabbit]
3023+rabbit_userid = {{ rabbitmq_user }}
3024+rabbit_virtual_host = {{ rabbitmq_virtual_host }}
3025+rabbit_password = {{ rabbitmq_password }}
3026+{% if rabbitmq_hosts -%}
3027+rabbit_hosts = {{ rabbitmq_hosts }}
3028+{% if rabbitmq_ha_queues -%}
3029+rabbit_ha_queues = True
3030+rabbit_durable_queues = False
3031+{% endif -%}
3032+{% else -%}
3033+rabbit_host = {{ rabbitmq_host }}
3034+{% endif -%}
3035+{% if rabbit_ssl_port -%}
3036+rabbit_use_ssl = True
3037+rabbit_port = {{ rabbit_ssl_port }}
3038+{% if rabbit_ssl_ca -%}
3039+kombu_ssl_ca_certs = {{ rabbit_ssl_ca }}
3040+{% endif -%}
3041+{% endif -%}
3042+{% endif -%}
3043
3044=== added file 'hooks/charmhelpers/contrib/openstack/templates/section-zeromq'
3045--- hooks/charmhelpers/contrib/openstack/templates/section-zeromq 1970-01-01 00:00:00 +0000
3046+++ hooks/charmhelpers/contrib/openstack/templates/section-zeromq 2015-09-21 10:47:21 +0000
3047@@ -0,0 +1,14 @@
3048+{% if zmq_host -%}
3049+# ZeroMQ configuration (restart-nonce: {{ zmq_nonce }})
3050+rpc_backend = zmq
3051+rpc_zmq_host = {{ zmq_host }}
3052+{% if zmq_redis_address -%}
3053+rpc_zmq_matchmaker = redis
3054+matchmaker_heartbeat_freq = 15
3055+matchmaker_heartbeat_ttl = 30
3056+[matchmaker_redis]
3057+host = {{ zmq_redis_address }}
3058+{% else -%}
3059+rpc_zmq_matchmaker = ring
3060+{% endif -%}
3061+{% endif -%}
3062
3063=== modified file 'hooks/charmhelpers/contrib/openstack/templating.py'
3064--- hooks/charmhelpers/contrib/openstack/templating.py 2014-12-10 20:28:57 +0000
3065+++ hooks/charmhelpers/contrib/openstack/templating.py 2015-09-21 10:47:21 +0000
3066@@ -1,3 +1,19 @@
3067+# Copyright 2014-2015 Canonical Limited.
3068+#
3069+# This file is part of charm-helpers.
3070+#
3071+# charm-helpers is free software: you can redistribute it and/or modify
3072+# it under the terms of the GNU Lesser General Public License version 3 as
3073+# published by the Free Software Foundation.
3074+#
3075+# charm-helpers is distributed in the hope that it will be useful,
3076+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3077+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3078+# GNU Lesser General Public License for more details.
3079+#
3080+# You should have received a copy of the GNU Lesser General Public License
3081+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3082+
3083 import os
3084
3085 import six
3086@@ -13,8 +29,8 @@
3087 try:
3088 from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
3089 except ImportError:
3090- # python-jinja2 may not be installed yet, or we're running unittests.
3091- FileSystemLoader = ChoiceLoader = Environment = exceptions = None
3092+ apt_install('python-jinja2', fatal=True)
3093+ from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
3094
3095
3096 class OSConfigException(Exception):
3097@@ -96,7 +112,7 @@
3098
3099 def complete_contexts(self):
3100 '''
3101- Return a list of interfaces that have atisfied contexts.
3102+ Return a list of interfaces that have satisfied contexts.
3103 '''
3104 if self._complete_contexts:
3105 return self._complete_contexts
3106@@ -277,3 +293,30 @@
3107 [interfaces.extend(i.complete_contexts())
3108 for i in six.itervalues(self.templates)]
3109 return interfaces
3110+
3111+ def get_incomplete_context_data(self, interfaces):
3112+ '''
3113+ Return dictionary of relation status of interfaces and any missing
3114+ required context data. Example:
3115+ {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
3116+ 'zeromq-configuration': {'related': False}}
3117+ '''
3118+ incomplete_context_data = {}
3119+
3120+ for i in six.itervalues(self.templates):
3121+ for context in i.contexts:
3122+ for interface in interfaces:
3123+ related = False
3124+ if interface in context.interfaces:
3125+ related = context.get_related()
3126+ missing_data = context.missing_data
3127+ if missing_data:
3128+ incomplete_context_data[interface] = {'missing_data': missing_data}
3129+ if related:
3130+ if incomplete_context_data.get(interface):
3131+ incomplete_context_data[interface].update({'related': True})
3132+ else:
3133+ incomplete_context_data[interface] = {'related': True}
3134+ else:
3135+ incomplete_context_data[interface] = {'related': False}
3136+ return incomplete_context_data
3137
3138=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
3139--- hooks/charmhelpers/contrib/openstack/utils.py 2015-01-14 15:30:19 +0000
3140+++ hooks/charmhelpers/contrib/openstack/utils.py 2015-09-21 10:47:21 +0000
3141@@ -1,4 +1,18 @@
3142-#!/usr/bin/python
3143+# Copyright 2014-2015 Canonical Limited.
3144+#
3145+# This file is part of charm-helpers.
3146+#
3147+# charm-helpers is free software: you can redistribute it and/or modify
3148+# it under the terms of the GNU Lesser General Public License version 3 as
3149+# published by the Free Software Foundation.
3150+#
3151+# charm-helpers is distributed in the hope that it will be useful,
3152+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3153+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3154+# GNU Lesser General Public License for more details.
3155+#
3156+# You should have received a copy of the GNU Lesser General Public License
3157+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3158
3159 # Common python helper functions used for OpenStack charms.
3160 from collections import OrderedDict
3161@@ -7,19 +21,27 @@
3162 import subprocess
3163 import json
3164 import os
3165-import socket
3166 import sys
3167+import re
3168
3169 import six
3170 import yaml
3171
3172+from charmhelpers.contrib.network import ip
3173+
3174+from charmhelpers.core import (
3175+ unitdata,
3176+)
3177+
3178 from charmhelpers.core.hookenv import (
3179 config,
3180 log as juju_log,
3181 charm_dir,
3182 INFO,
3183 relation_ids,
3184- relation_set
3185+ relation_set,
3186+ status_set,
3187+ hook_name
3188 )
3189
3190 from charmhelpers.contrib.storage.linux.lvm import (
3191@@ -32,9 +54,13 @@
3192 get_ipv6_addr
3193 )
3194
3195+from charmhelpers.contrib.python.packages import (
3196+ pip_create_virtualenv,
3197+ pip_install,
3198+)
3199+
3200 from charmhelpers.core.host import lsb_release, mounts, umount
3201 from charmhelpers.fetch import apt_install, apt_cache, install_remote
3202-from charmhelpers.contrib.python.packages import pip_install
3203 from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
3204 from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
3205
3206@@ -44,7 +70,6 @@
3207 DISTRO_PROPOSED = ('deb http://archive.ubuntu.com/ubuntu/ %s-proposed '
3208 'restricted main multiverse universe')
3209
3210-
3211 UBUNTU_OPENSTACK_RELEASE = OrderedDict([
3212 ('oneiric', 'diablo'),
3213 ('precise', 'essex'),
3214@@ -54,6 +79,7 @@
3215 ('trusty', 'icehouse'),
3216 ('utopic', 'juno'),
3217 ('vivid', 'kilo'),
3218+ ('wily', 'liberty'),
3219 ])
3220
3221
3222@@ -66,6 +92,7 @@
3223 ('2014.1', 'icehouse'),
3224 ('2014.2', 'juno'),
3225 ('2015.1', 'kilo'),
3226+ ('2015.2', 'liberty'),
3227 ])
3228
3229 # The ugly duckling
3230@@ -87,8 +114,42 @@
3231 ('2.1.0', 'juno'),
3232 ('2.2.0', 'juno'),
3233 ('2.2.1', 'kilo'),
3234+ ('2.2.2', 'kilo'),
3235+ ('2.3.0', 'liberty'),
3236+ ('2.4.0', 'liberty'),
3237 ])
3238
3239+# >= Liberty version->codename mapping
3240+PACKAGE_CODENAMES = {
3241+ 'nova-common': OrderedDict([
3242+ ('12.0.0', 'liberty'),
3243+ ]),
3244+ 'neutron-common': OrderedDict([
3245+ ('7.0.0', 'liberty'),
3246+ ]),
3247+ 'cinder-common': OrderedDict([
3248+ ('7.0.0', 'liberty'),
3249+ ]),
3250+ 'keystone': OrderedDict([
3251+ ('8.0.0', 'liberty'),
3252+ ]),
3253+ 'horizon-common': OrderedDict([
3254+ ('8.0.0', 'liberty'),
3255+ ]),
3256+ 'ceilometer-common': OrderedDict([
3257+ ('5.0.0', 'liberty'),
3258+ ]),
3259+ 'heat-common': OrderedDict([
3260+ ('5.0.0', 'liberty'),
3261+ ]),
3262+ 'glance-common': OrderedDict([
3263+ ('11.0.0', 'liberty'),
3264+ ]),
3265+ 'openstack-dashboard': OrderedDict([
3266+ ('8.0.0', 'liberty'),
3267+ ]),
3268+}
3269+
3270 DEFAULT_LOOPBACK_SIZE = '5G'
3271
3272
3273@@ -138,9 +199,9 @@
3274 error_out(e)
3275
3276
3277-def get_os_version_codename(codename):
3278+def get_os_version_codename(codename, version_map=OPENSTACK_CODENAMES):
3279 '''Determine OpenStack version number from codename.'''
3280- for k, v in six.iteritems(OPENSTACK_CODENAMES):
3281+ for k, v in six.iteritems(version_map):
3282 if v == codename:
3283 return k
3284 e = 'Could not derive OpenStack version for '\
3285@@ -172,20 +233,31 @@
3286 error_out(e)
3287
3288 vers = apt.upstream_version(pkg.current_ver.ver_str)
3289+ match = re.match('^(\d+)\.(\d+)\.(\d+)', vers)
3290+ if match:
3291+ vers = match.group(0)
3292
3293- try:
3294- if 'swift' in pkg.name:
3295- swift_vers = vers[:5]
3296- if swift_vers not in SWIFT_CODENAMES:
3297- # Deal with 1.10.0 upward
3298- swift_vers = vers[:6]
3299- return SWIFT_CODENAMES[swift_vers]
3300- else:
3301- vers = vers[:6]
3302- return OPENSTACK_CODENAMES[vers]
3303- except KeyError:
3304- e = 'Could not determine OpenStack codename for version %s' % vers
3305- error_out(e)
3306+ # >= Liberty independent project versions
3307+ if (package in PACKAGE_CODENAMES and
3308+ vers in PACKAGE_CODENAMES[package]):
3309+ return PACKAGE_CODENAMES[package][vers]
3310+ else:
3311+ # < Liberty co-ordinated project versions
3312+ try:
3313+ if 'swift' in pkg.name:
3314+ swift_vers = vers[:5]
3315+ if swift_vers not in SWIFT_CODENAMES:
3316+ # Deal with 1.10.0 upward
3317+ swift_vers = vers[:6]
3318+ return SWIFT_CODENAMES[swift_vers]
3319+ else:
3320+ vers = vers[:6]
3321+ return OPENSTACK_CODENAMES[vers]
3322+ except KeyError:
3323+ if not fatal:
3324+ return None
3325+ e = 'Could not determine OpenStack codename for version %s' % vers
3326+ error_out(e)
3327
3328
3329 def get_os_version_package(pkg, fatal=True):
3330@@ -295,6 +367,9 @@
3331 'kilo': 'trusty-updates/kilo',
3332 'kilo/updates': 'trusty-updates/kilo',
3333 'kilo/proposed': 'trusty-proposed/kilo',
3334+ 'liberty': 'trusty-updates/liberty',
3335+ 'liberty/updates': 'trusty-updates/liberty',
3336+ 'liberty/proposed': 'trusty-proposed/liberty',
3337 }
3338
3339 try:
3340@@ -312,6 +387,21 @@
3341 error_out("Invalid openstack-release specified: %s" % rel)
3342
3343
3344+def config_value_changed(option):
3345+ """
3346+ Determine if config value changed since last call to this function.
3347+ """
3348+ hook_data = unitdata.HookData()
3349+ with hook_data():
3350+ db = unitdata.kv()
3351+ current = config(option)
3352+ saved = db.get(option)
3353+ db.set(option, current)
3354+ if saved is None:
3355+ return False
3356+ return current != saved
3357+
3358+
3359 def save_script_rc(script_path="scripts/scriptrc", **env_vars):
3360 """
3361 Write an rc file in the charm-delivered directory containing
3362@@ -345,7 +435,11 @@
3363 import apt_pkg as apt
3364 src = config('openstack-origin')
3365 cur_vers = get_os_version_package(package)
3366- available_vers = get_os_version_install_source(src)
3367+ if "swift" in package:
3368+ codename = get_os_codename_install_source(src)
3369+ available_vers = get_os_version_codename(codename, SWIFT_CODENAMES)
3370+ else:
3371+ available_vers = get_os_version_install_source(src)
3372 apt.init()
3373 return apt.version_compare(available_vers, cur_vers) == 1
3374
3375@@ -404,77 +498,10 @@
3376 else:
3377 zap_disk(block_device)
3378
3379-
3380-def is_ip(address):
3381- """
3382- Returns True if address is a valid IP address.
3383- """
3384- try:
3385- # Test to see if already an IPv4 address
3386- socket.inet_aton(address)
3387- return True
3388- except socket.error:
3389- return False
3390-
3391-
3392-def ns_query(address):
3393- try:
3394- import dns.resolver
3395- except ImportError:
3396- apt_install('python-dnspython')
3397- import dns.resolver
3398-
3399- if isinstance(address, dns.name.Name):
3400- rtype = 'PTR'
3401- elif isinstance(address, six.string_types):
3402- rtype = 'A'
3403- else:
3404- return None
3405-
3406- answers = dns.resolver.query(address, rtype)
3407- if answers:
3408- return str(answers[0])
3409- return None
3410-
3411-
3412-def get_host_ip(hostname):
3413- """
3414- Resolves the IP for a given hostname, or returns
3415- the input if it is already an IP.
3416- """
3417- if is_ip(hostname):
3418- return hostname
3419-
3420- return ns_query(hostname)
3421-
3422-
3423-def get_hostname(address, fqdn=True):
3424- """
3425- Resolves hostname for given IP, or returns the input
3426- if it is already a hostname.
3427- """
3428- if is_ip(address):
3429- try:
3430- import dns.reversename
3431- except ImportError:
3432- apt_install('python-dnspython')
3433- import dns.reversename
3434-
3435- rev = dns.reversename.from_address(address)
3436- result = ns_query(rev)
3437- if not result:
3438- return None
3439- else:
3440- result = address
3441-
3442- if fqdn:
3443- # strip trailing .
3444- if result.endswith('.'):
3445- return result[:-1]
3446- else:
3447- return result
3448- else:
3449- return result.split('.')[0]
3450+is_ip = ip.is_ip
3451+ns_query = ip.ns_query
3452+get_host_ip = ip.get_host_ip
3453+get_hostname = ip.get_hostname
3454
3455
3456 def get_matchmaker_map(mm_file='/etc/oslo/matchmaker_ring.json'):
3457@@ -518,108 +545,379 @@
3458
3459
3460 def git_install_requested():
3461- """Returns true if openstack-origin-git is specified."""
3462- return config('openstack-origin-git') != "None"
3463+ """
3464+ Returns true if openstack-origin-git is specified.
3465+ """
3466+ return config('openstack-origin-git') is not None
3467
3468
3469 requirements_dir = None
3470
3471
3472-def git_clone_and_install(file_name, core_project):
3473- """Clone/install all OpenStack repos specified in yaml config file."""
3474- global requirements_dir
3475-
3476- if file_name == "None":
3477- return
3478-
3479- yaml_file = os.path.join(charm_dir(), file_name)
3480-
3481- # clone/install the requirements project first
3482- installed = _git_clone_and_install_subset(yaml_file,
3483- whitelist=['requirements'])
3484- if 'requirements' not in installed:
3485- error_out('requirements git repository must be specified')
3486-
3487- # clone/install all other projects except requirements and the core project
3488- blacklist = ['requirements', core_project]
3489- _git_clone_and_install_subset(yaml_file, blacklist=blacklist,
3490- update_requirements=True)
3491-
3492- # clone/install the core project
3493- whitelist = [core_project]
3494- installed = _git_clone_and_install_subset(yaml_file, whitelist=whitelist,
3495- update_requirements=True)
3496- if core_project not in installed:
3497- error_out('{} git repository must be specified'.format(core_project))
3498-
3499-
3500-def _git_clone_and_install_subset(yaml_file, whitelist=[], blacklist=[],
3501- update_requirements=False):
3502- """Clone/install subset of OpenStack repos specified in yaml config file."""
3503- global requirements_dir
3504- installed = []
3505-
3506- with open(yaml_file, 'r') as fd:
3507- projects = yaml.load(fd)
3508- for proj, val in projects.items():
3509- # The project subset is chosen based on the following 3 rules:
3510- # 1) If project is in blacklist, we don't clone/install it, period.
3511- # 2) If whitelist is empty, we clone/install everything else.
3512- # 3) If whitelist is not empty, we clone/install everything in the
3513- # whitelist.
3514- if proj in blacklist:
3515- continue
3516- if whitelist and proj not in whitelist:
3517- continue
3518- repo = val['repository']
3519- branch = val['branch']
3520- repo_dir = _git_clone_and_install_single(repo, branch,
3521- update_requirements)
3522- if proj == 'requirements':
3523- requirements_dir = repo_dir
3524- installed.append(proj)
3525- return installed
3526-
3527-
3528-def _git_clone_and_install_single(repo, branch, update_requirements=False):
3529- """Clone and install a single git repository."""
3530- dest_parent_dir = "/mnt/openstack-git/"
3531- dest_dir = os.path.join(dest_parent_dir, os.path.basename(repo))
3532-
3533- if not os.path.exists(dest_parent_dir):
3534- juju_log('Host dir not mounted at {}. '
3535- 'Creating directory there instead.'.format(dest_parent_dir))
3536- os.mkdir(dest_parent_dir)
3537+def _git_yaml_load(projects_yaml):
3538+ """
3539+ Load the specified yaml into a dictionary.
3540+ """
3541+ if not projects_yaml:
3542+ return None
3543+
3544+ return yaml.load(projects_yaml)
3545+
3546+
3547+def git_clone_and_install(projects_yaml, core_project, depth=1):
3548+ """
3549+ Clone/install all specified OpenStack repositories.
3550+
3551+ The expected format of projects_yaml is:
3552+
3553+ repositories:
3554+ - {name: keystone,
3555+ repository: 'git://git.openstack.org/openstack/keystone.git',
3556+ branch: 'stable/icehouse'}
3557+ - {name: requirements,
3558+ repository: 'git://git.openstack.org/openstack/requirements.git',
3559+ branch: 'stable/icehouse'}
3560+
3561+ directory: /mnt/openstack-git
3562+ http_proxy: squid-proxy-url
3563+ https_proxy: squid-proxy-url
3564+
3565+ The directory, http_proxy, and https_proxy keys are optional.
3566+
3567+ """
3568+ global requirements_dir
3569+ parent_dir = '/mnt/openstack-git'
3570+ http_proxy = None
3571+
3572+ projects = _git_yaml_load(projects_yaml)
3573+ _git_validate_projects_yaml(projects, core_project)
3574+
3575+ old_environ = dict(os.environ)
3576+
3577+ if 'http_proxy' in projects.keys():
3578+ http_proxy = projects['http_proxy']
3579+ os.environ['http_proxy'] = projects['http_proxy']
3580+ if 'https_proxy' in projects.keys():
3581+ os.environ['https_proxy'] = projects['https_proxy']
3582+
3583+ if 'directory' in projects.keys():
3584+ parent_dir = projects['directory']
3585+
3586+ pip_create_virtualenv(os.path.join(parent_dir, 'venv'))
3587+
3588+ # Upgrade setuptools and pip from default virtualenv versions. The default
3589+ # versions in trusty break master OpenStack branch deployments.
3590+ for p in ['pip', 'setuptools']:
3591+ pip_install(p, upgrade=True, proxy=http_proxy,
3592+ venv=os.path.join(parent_dir, 'venv'))
3593+
3594+ for p in projects['repositories']:
3595+ repo = p['repository']
3596+ branch = p['branch']
3597+ if p['name'] == 'requirements':
3598+ repo_dir = _git_clone_and_install_single(repo, branch, depth,
3599+ parent_dir, http_proxy,
3600+ update_requirements=False)
3601+ requirements_dir = repo_dir
3602+ else:
3603+ repo_dir = _git_clone_and_install_single(repo, branch, depth,
3604+ parent_dir, http_proxy,
3605+ update_requirements=True)
3606+
3607+ os.environ = old_environ
3608+
3609+
3610+def _git_validate_projects_yaml(projects, core_project):
3611+ """
3612+ Validate the projects yaml.
3613+ """
3614+ _git_ensure_key_exists('repositories', projects)
3615+
3616+ for project in projects['repositories']:
3617+ _git_ensure_key_exists('name', project.keys())
3618+ _git_ensure_key_exists('repository', project.keys())
3619+ _git_ensure_key_exists('branch', project.keys())
3620+
3621+ if projects['repositories'][0]['name'] != 'requirements':
3622+ error_out('{} git repo must be specified first'.format('requirements'))
3623+
3624+ if projects['repositories'][-1]['name'] != core_project:
3625+ error_out('{} git repo must be specified last'.format(core_project))
3626+
3627+
3628+def _git_ensure_key_exists(key, keys):
3629+ """
3630+ Ensure that key exists in keys.
3631+ """
3632+ if key not in keys:
3633+ error_out('openstack-origin-git key \'{}\' is missing'.format(key))
3634+
3635+
3636+def _git_clone_and_install_single(repo, branch, depth, parent_dir, http_proxy,
3637+ update_requirements):
3638+ """
3639+ Clone and install a single git repository.
3640+ """
3641+ dest_dir = os.path.join(parent_dir, os.path.basename(repo))
3642+
3643+ if not os.path.exists(parent_dir):
3644+ juju_log('Directory already exists at {}. '
3645+ 'No need to create directory.'.format(parent_dir))
3646+ os.mkdir(parent_dir)
3647
3648 if not os.path.exists(dest_dir):
3649 juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
3650- repo_dir = install_remote(repo, dest=dest_parent_dir, branch=branch)
3651+ repo_dir = install_remote(repo, dest=parent_dir, branch=branch,
3652+ depth=depth)
3653 else:
3654 repo_dir = dest_dir
3655
3656+ venv = os.path.join(parent_dir, 'venv')
3657+
3658 if update_requirements:
3659 if not requirements_dir:
3660 error_out('requirements repo must be cloned before '
3661 'updating from global requirements.')
3662- _git_update_requirements(repo_dir, requirements_dir)
3663+ _git_update_requirements(venv, repo_dir, requirements_dir)
3664
3665 juju_log('Installing git repo from dir: {}'.format(repo_dir))
3666- pip_install(repo_dir)
3667+ if http_proxy:
3668+ pip_install(repo_dir, proxy=http_proxy, venv=venv)
3669+ else:
3670+ pip_install(repo_dir, venv=venv)
3671
3672 return repo_dir
3673
3674
3675-def _git_update_requirements(package_dir, reqs_dir):
3676- """Update from global requirements.
3677+def _git_update_requirements(venv, package_dir, reqs_dir):
3678+ """
3679+ Update from global requirements.
3680
3681- Update an OpenStack git directory's requirements.txt and
3682- test-requirements.txt from global-requirements.txt."""
3683+ Update an OpenStack git directory's requirements.txt and
3684+ test-requirements.txt from global-requirements.txt.
3685+ """
3686 orig_dir = os.getcwd()
3687 os.chdir(reqs_dir)
3688- cmd = "python update.py {}".format(package_dir)
3689+ python = os.path.join(venv, 'bin/python')
3690+ cmd = [python, 'update.py', package_dir]
3691 try:
3692- subprocess.check_call(cmd.split(' '))
3693+ subprocess.check_call(cmd)
3694 except subprocess.CalledProcessError:
3695 package = os.path.basename(package_dir)
3696- error_out("Error updating {} from global-requirements.txt".format(package))
3697+ error_out("Error updating {} from "
3698+ "global-requirements.txt".format(package))
3699 os.chdir(orig_dir)
3700+
3701+
3702+def git_pip_venv_dir(projects_yaml):
3703+ """
3704+ Return the pip virtualenv path.
3705+ """
3706+ parent_dir = '/mnt/openstack-git'
3707+
3708+ projects = _git_yaml_load(projects_yaml)
3709+
3710+ if 'directory' in projects.keys():
3711+ parent_dir = projects['directory']
3712+
3713+ return os.path.join(parent_dir, 'venv')
3714+
3715+
3716+def git_src_dir(projects_yaml, project):
3717+ """
3718+ Return the directory where the specified project's source is located.
3719+ """
3720+ parent_dir = '/mnt/openstack-git'
3721+
3722+ projects = _git_yaml_load(projects_yaml)
3723+
3724+ if 'directory' in projects.keys():
3725+ parent_dir = projects['directory']
3726+
3727+ for p in projects['repositories']:
3728+ if p['name'] == project:
3729+ return os.path.join(parent_dir, os.path.basename(p['repository']))
3730+
3731+ return None
3732+
3733+
3734+def git_yaml_value(projects_yaml, key):
3735+ """
3736+ Return the value in projects_yaml for the specified key.
3737+ """
3738+ projects = _git_yaml_load(projects_yaml)
3739+
3740+ if key in projects.keys():
3741+ return projects[key]
3742+
3743+ return None
3744+
3745+
3746+def os_workload_status(configs, required_interfaces, charm_func=None):
3747+ """
3748+ Decorator to set workload status based on complete contexts
3749+ """
3750+ def wrap(f):
3751+ @wraps(f)
3752+ def wrapped_f(*args, **kwargs):
3753+ # Run the original function first
3754+ f(*args, **kwargs)
3755+ # Set workload status now that contexts have been
3756+ # acted on
3757+ set_os_workload_status(configs, required_interfaces, charm_func)
3758+ return wrapped_f
3759+ return wrap
3760+
3761+
3762+def set_os_workload_status(configs, required_interfaces, charm_func=None):
3763+ """
3764+ Set workload status based on complete contexts.
3765+ status-set missing or incomplete contexts
3766+ and juju-log details of missing required data.
3767+ charm_func is a charm specific function to run checking
3768+ for charm specific requirements such as a VIP setting.
3769+ """
3770+ incomplete_rel_data = incomplete_relation_data(configs, required_interfaces)
3771+ state = 'active'
3772+ missing_relations = []
3773+ incomplete_relations = []
3774+ message = None
3775+ charm_state = None
3776+ charm_message = None
3777+
3778+ for generic_interface in incomplete_rel_data.keys():
3779+ related_interface = None
3780+ missing_data = {}
3781+ # Related or not?
3782+ for interface in incomplete_rel_data[generic_interface]:
3783+ if incomplete_rel_data[generic_interface][interface].get('related'):
3784+ related_interface = interface
3785+ missing_data = incomplete_rel_data[generic_interface][interface].get('missing_data')
3786+ # No relation ID for the generic_interface
3787+ if not related_interface:
3788+ juju_log("{} relation is missing and must be related for "
3789+ "functionality. ".format(generic_interface), 'WARN')
3790+ state = 'blocked'
3791+ if generic_interface not in missing_relations:
3792+ missing_relations.append(generic_interface)
3793+ else:
3794+ # Relation ID exists but no related unit
3795+ if not missing_data:
3796+ # Edge case relation ID exists but departing
3797+ if ('departed' in hook_name() or 'broken' in hook_name()) \
3798+ and related_interface in hook_name():
3799+ state = 'blocked'
3800+ if generic_interface not in missing_relations:
3801+ missing_relations.append(generic_interface)
3802+ juju_log("{} relation's interface, {}, "
3803+ "relationship is departed or broken "
3804+ "and is required for functionality."
3805+ "".format(generic_interface, related_interface), "WARN")
3806+ # Normal case relation ID exists but no related unit
3807+ # (joining)
3808+ else:
3809+ juju_log("{} relations's interface, {}, is related but has "
3810+ "no units in the relation."
3811+ "".format(generic_interface, related_interface), "INFO")
3812+ # Related unit exists and data missing on the relation
3813+ else:
3814+ juju_log("{} relation's interface, {}, is related awaiting "
3815+ "the following data from the relationship: {}. "
3816+ "".format(generic_interface, related_interface,
3817+ ", ".join(missing_data)), "INFO")
3818+ if state != 'blocked':
3819+ state = 'waiting'
3820+ if generic_interface not in incomplete_relations \
3821+ and generic_interface not in missing_relations:
3822+ incomplete_relations.append(generic_interface)
3823+
3824+ if missing_relations:
3825+ message = "Missing relations: {}".format(", ".join(missing_relations))
3826+ if incomplete_relations:
3827+ message += "; incomplete relations: {}" \
3828+ "".format(", ".join(incomplete_relations))
3829+ state = 'blocked'
3830+ elif incomplete_relations:
3831+ message = "Incomplete relations: {}" \
3832+ "".format(", ".join(incomplete_relations))
3833+ state = 'waiting'
3834+
3835+ # Run charm specific checks
3836+ if charm_func:
3837+ charm_state, charm_message = charm_func(configs)
3838+ if charm_state != 'active' and charm_state != 'unknown':
3839+ state = workload_state_compare(state, charm_state)
3840+ if message:
3841+ message = "{} {}".format(message, charm_message)
3842+ else:
3843+ message = charm_message
3844+
3845+ # Set to active if all requirements have been met
3846+ if state == 'active':
3847+ message = "Unit is ready"
3848+ juju_log(message, "INFO")
3849+
3850+ status_set(state, message)
3851+
3852+
3853+def workload_state_compare(current_workload_state, workload_state):
3854+ """ Return highest priority of two states"""
3855+ hierarchy = {'unknown': -1,
3856+ 'active': 0,
3857+ 'maintenance': 1,
3858+ 'waiting': 2,
3859+ 'blocked': 3,
3860+ }
3861+
3862+ if hierarchy.get(workload_state) is None:
3863+ workload_state = 'unknown'
3864+ if hierarchy.get(current_workload_state) is None:
3865+ current_workload_state = 'unknown'
3866+
3867+ # Set workload_state based on hierarchy of statuses
3868+ if hierarchy.get(current_workload_state) > hierarchy.get(workload_state):
3869+ return current_workload_state
3870+ else:
3871+ return workload_state
3872+
3873+
3874+def incomplete_relation_data(configs, required_interfaces):
3875+ """
3876+ Check complete contexts against required_interfaces
3877+ Return dictionary of incomplete relation data.
3878+
3879+ configs is an OSConfigRenderer object with configs registered
3880+
3881+ required_interfaces is a dictionary of required general interfaces
3882+ with dictionary values of possible specific interfaces.
3883+ Example:
3884+ required_interfaces = {'database': ['shared-db', 'pgsql-db']}
3885+
3886+ The interface is said to be satisfied if anyone of the interfaces in the
3887+ list has a complete context.
3888+
3889+ Return dictionary of incomplete or missing required contexts with relation
3890+ status of interfaces and any missing data points. Example:
3891+ {'message':
3892+ {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
3893+ 'zeromq-configuration': {'related': False}},
3894+ 'identity':
3895+ {'identity-service': {'related': False}},
3896+ 'database':
3897+ {'pgsql-db': {'related': False},
3898+ 'shared-db': {'related': True}}}
3899+ """
3900+ complete_ctxts = configs.complete_contexts()
3901+ incomplete_relations = []
3902+ for svc_type in required_interfaces.keys():
3903+ # Avoid duplicates
3904+ found_ctxt = False
3905+ for interface in required_interfaces[svc_type]:
3906+ if interface in complete_ctxts:
3907+ found_ctxt = True
3908+ if not found_ctxt:
3909+ incomplete_relations.append(svc_type)
3910+ incomplete_context_data = {}
3911+ for i in incomplete_relations:
3912+ incomplete_context_data[i] = configs.get_incomplete_context_data(required_interfaces[i])
3913+ return incomplete_context_data
3914
3915=== modified file 'hooks/charmhelpers/contrib/python/__init__.py'
3916--- hooks/charmhelpers/contrib/python/__init__.py 2014-12-10 20:28:57 +0000
3917+++ hooks/charmhelpers/contrib/python/__init__.py 2015-09-21 10:47:21 +0000
3918@@ -0,0 +1,15 @@
3919+# Copyright 2014-2015 Canonical Limited.
3920+#
3921+# This file is part of charm-helpers.
3922+#
3923+# charm-helpers is free software: you can redistribute it and/or modify
3924+# it under the terms of the GNU Lesser General Public License version 3 as
3925+# published by the Free Software Foundation.
3926+#
3927+# charm-helpers is distributed in the hope that it will be useful,
3928+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3929+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3930+# GNU Lesser General Public License for more details.
3931+#
3932+# You should have received a copy of the GNU Lesser General Public License
3933+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3934
3935=== modified file 'hooks/charmhelpers/contrib/python/packages.py'
3936--- hooks/charmhelpers/contrib/python/packages.py 2014-12-10 20:28:57 +0000
3937+++ hooks/charmhelpers/contrib/python/packages.py 2015-09-21 10:47:21 +0000
3938@@ -1,10 +1,27 @@
3939 #!/usr/bin/env python
3940 # coding: utf-8
3941
3942-__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
3943+# Copyright 2014-2015 Canonical Limited.
3944+#
3945+# This file is part of charm-helpers.
3946+#
3947+# charm-helpers is free software: you can redistribute it and/or modify
3948+# it under the terms of the GNU Lesser General Public License version 3 as
3949+# published by the Free Software Foundation.
3950+#
3951+# charm-helpers is distributed in the hope that it will be useful,
3952+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3953+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3954+# GNU Lesser General Public License for more details.
3955+#
3956+# You should have received a copy of the GNU Lesser General Public License
3957+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3958+
3959+import os
3960+import subprocess
3961
3962 from charmhelpers.fetch import apt_install, apt_update
3963-from charmhelpers.core.hookenv import log
3964+from charmhelpers.core.hookenv import charm_dir, log
3965
3966 try:
3967 from pip import main as pip_execute
3968@@ -13,10 +30,14 @@
3969 apt_install('python-pip')
3970 from pip import main as pip_execute
3971
3972+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
3973+
3974
3975 def parse_options(given, available):
3976 """Given a set of options, check if available"""
3977 for key, value in sorted(given.items()):
3978+ if not value:
3979+ continue
3980 if key in available:
3981 yield "--{0}={1}".format(key, value)
3982
3983@@ -35,14 +56,21 @@
3984 pip_execute(command)
3985
3986
3987-def pip_install(package, fatal=False, **options):
3988+def pip_install(package, fatal=False, upgrade=False, venv=None, **options):
3989 """Install a python package"""
3990- command = ["install"]
3991+ if venv:
3992+ venv_python = os.path.join(venv, 'bin/pip')
3993+ command = [venv_python, "install"]
3994+ else:
3995+ command = ["install"]
3996
3997- available_options = ('proxy', 'src', 'log', "index-url", )
3998+ available_options = ('proxy', 'src', 'log', 'index-url', )
3999 for option in parse_options(options, available_options):
4000 command.append(option)
4001
4002+ if upgrade:
4003+ command.append('--upgrade')
4004+
4005 if isinstance(package, list):
4006 command.extend(package)
4007 else:
4008@@ -50,7 +78,10 @@
4009
4010 log("Installing {} package with options: {}".format(package,
4011 command))
4012- pip_execute(command)
4013+ if venv:
4014+ subprocess.check_call(command)
4015+ else:
4016+ pip_execute(command)
4017
4018
4019 def pip_uninstall(package, **options):
4020@@ -75,3 +106,16 @@
4021 """Returns the list of current python installed packages
4022 """
4023 return pip_execute(["list"])
4024+
4025+
4026+def pip_create_virtualenv(path=None):
4027+ """Create an isolated Python environment."""
4028+ apt_install('python-virtualenv')
4029+
4030+ if path:
4031+ venv_path = path
4032+ else:
4033+ venv_path = os.path.join(charm_dir(), 'venv')
4034+
4035+ if not os.path.exists(venv_path):
4036+ subprocess.check_call(['virtualenv', venv_path])
4037
4038=== modified file 'hooks/charmhelpers/contrib/storage/__init__.py'
4039--- hooks/charmhelpers/contrib/storage/__init__.py 2013-07-19 02:37:30 +0000
4040+++ hooks/charmhelpers/contrib/storage/__init__.py 2015-09-21 10:47:21 +0000
4041@@ -0,0 +1,15 @@
4042+# Copyright 2014-2015 Canonical Limited.
4043+#
4044+# This file is part of charm-helpers.
4045+#
4046+# charm-helpers is free software: you can redistribute it and/or modify
4047+# it under the terms of the GNU Lesser General Public License version 3 as
4048+# published by the Free Software Foundation.
4049+#
4050+# charm-helpers is distributed in the hope that it will be useful,
4051+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4052+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4053+# GNU Lesser General Public License for more details.
4054+#
4055+# You should have received a copy of the GNU Lesser General Public License
4056+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4057
4058=== modified file 'hooks/charmhelpers/contrib/storage/linux/__init__.py'
4059--- hooks/charmhelpers/contrib/storage/linux/__init__.py 2013-07-19 02:37:30 +0000
4060+++ hooks/charmhelpers/contrib/storage/linux/__init__.py 2015-09-21 10:47:21 +0000
4061@@ -0,0 +1,15 @@
4062+# Copyright 2014-2015 Canonical Limited.
4063+#
4064+# This file is part of charm-helpers.
4065+#
4066+# charm-helpers is free software: you can redistribute it and/or modify
4067+# it under the terms of the GNU Lesser General Public License version 3 as
4068+# published by the Free Software Foundation.
4069+#
4070+# charm-helpers is distributed in the hope that it will be useful,
4071+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4072+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4073+# GNU Lesser General Public License for more details.
4074+#
4075+# You should have received a copy of the GNU Lesser General Public License
4076+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4077
4078=== modified file 'hooks/charmhelpers/contrib/storage/linux/ceph.py'
4079--- hooks/charmhelpers/contrib/storage/linux/ceph.py 2015-01-14 15:30:19 +0000
4080+++ hooks/charmhelpers/contrib/storage/linux/ceph.py 2015-09-21 10:47:21 +0000
4081@@ -1,3 +1,19 @@
4082+# Copyright 2014-2015 Canonical Limited.
4083+#
4084+# This file is part of charm-helpers.
4085+#
4086+# charm-helpers is free software: you can redistribute it and/or modify
4087+# it under the terms of the GNU Lesser General Public License version 3 as
4088+# published by the Free Software Foundation.
4089+#
4090+# charm-helpers is distributed in the hope that it will be useful,
4091+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4092+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4093+# GNU Lesser General Public License for more details.
4094+#
4095+# You should have received a copy of the GNU Lesser General Public License
4096+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4097+
4098 #
4099 # Copyright 2012 Canonical Ltd.
4100 #
4101@@ -12,6 +28,7 @@
4102 import shutil
4103 import json
4104 import time
4105+import uuid
4106
4107 from subprocess import (
4108 check_call,
4109@@ -19,8 +36,10 @@
4110 CalledProcessError,
4111 )
4112 from charmhelpers.core.hookenv import (
4113+ local_unit,
4114 relation_get,
4115 relation_ids,
4116+ relation_set,
4117 related_units,
4118 log,
4119 DEBUG,
4120@@ -40,16 +59,18 @@
4121 apt_install,
4122 )
4123
4124+from charmhelpers.core.kernel import modprobe
4125+
4126 KEYRING = '/etc/ceph/ceph.client.{}.keyring'
4127 KEYFILE = '/etc/ceph/ceph.client.{}.key'
4128
4129 CEPH_CONF = """[global]
4130- auth supported = {auth}
4131- keyring = {keyring}
4132- mon host = {mon_hosts}
4133- log to syslog = {use_syslog}
4134- err to syslog = {use_syslog}
4135- clog to syslog = {use_syslog}
4136+auth supported = {auth}
4137+keyring = {keyring}
4138+mon host = {mon_hosts}
4139+log to syslog = {use_syslog}
4140+err to syslog = {use_syslog}
4141+clog to syslog = {use_syslog}
4142 """
4143
4144
4145@@ -272,17 +293,6 @@
4146 os.chown(data_src_dst, uid, gid)
4147
4148
4149-# TODO: re-use
4150-def modprobe(module):
4151- """Load a kernel module and configure for auto-load on reboot."""
4152- log('Loading kernel module', level=INFO)
4153- cmd = ['modprobe', module]
4154- check_call(cmd)
4155- with open('/etc/modules', 'r+') as modules:
4156- if module not in modules.read():
4157- modules.write(module)
4158-
4159-
4160 def copy_files(src, dst, symlinks=False, ignore=None):
4161 """Copy files from src to dst."""
4162 for item in os.listdir(src):
4163@@ -395,17 +405,52 @@
4164
4165 The API is versioned and defaults to version 1.
4166 """
4167- def __init__(self, api_version=1):
4168+ def __init__(self, api_version=1, request_id=None):
4169 self.api_version = api_version
4170+ if request_id:
4171+ self.request_id = request_id
4172+ else:
4173+ self.request_id = str(uuid.uuid1())
4174 self.ops = []
4175
4176 def add_op_create_pool(self, name, replica_count=3):
4177 self.ops.append({'op': 'create-pool', 'name': name,
4178 'replicas': replica_count})
4179
4180+ def set_ops(self, ops):
4181+ """Set request ops to provided value.
4182+
4183+ Useful for injecting ops that come from a previous request
4184+ to allow comparisons to ensure validity.
4185+ """
4186+ self.ops = ops
4187+
4188 @property
4189 def request(self):
4190- return json.dumps({'api-version': self.api_version, 'ops': self.ops})
4191+ return json.dumps({'api-version': self.api_version, 'ops': self.ops,
4192+ 'request-id': self.request_id})
4193+
4194+ def _ops_equal(self, other):
4195+ if len(self.ops) == len(other.ops):
4196+ for req_no in range(0, len(self.ops)):
4197+ for key in ['replicas', 'name', 'op']:
4198+ if self.ops[req_no][key] != other.ops[req_no][key]:
4199+ return False
4200+ else:
4201+ return False
4202+ return True
4203+
4204+ def __eq__(self, other):
4205+ if not isinstance(other, self.__class__):
4206+ return False
4207+ if self.api_version == other.api_version and \
4208+ self._ops_equal(other):
4209+ return True
4210+ else:
4211+ return False
4212+
4213+ def __ne__(self, other):
4214+ return not self.__eq__(other)
4215
4216
4217 class CephBrokerRsp(object):
4218@@ -415,14 +460,198 @@
4219
4220 The API is versioned and defaults to version 1.
4221 """
4222+
4223 def __init__(self, encoded_rsp):
4224 self.api_version = None
4225 self.rsp = json.loads(encoded_rsp)
4226
4227 @property
4228+ def request_id(self):
4229+ return self.rsp.get('request-id')
4230+
4231+ @property
4232 def exit_code(self):
4233 return self.rsp.get('exit-code')
4234
4235 @property
4236 def exit_msg(self):
4237 return self.rsp.get('stderr')
4238+
4239+
4240+# Ceph Broker Conversation:
4241+# If a charm needs an action to be taken by ceph it can create a CephBrokerRq
4242+# and send that request to ceph via the ceph relation. The CephBrokerRq has a
4243+# unique id so that the client can identity which CephBrokerRsp is associated
4244+# with the request. Ceph will also respond to each client unit individually
4245+# creating a response key per client unit eg glance/0 will get a CephBrokerRsp
4246+# via key broker-rsp-glance-0
4247+#
4248+# To use this the charm can just do something like:
4249+#
4250+# from charmhelpers.contrib.storage.linux.ceph import (
4251+# send_request_if_needed,
4252+# is_request_complete,
4253+# CephBrokerRq,
4254+# )
4255+#
4256+# @hooks.hook('ceph-relation-changed')
4257+# def ceph_changed():
4258+# rq = CephBrokerRq()
4259+# rq.add_op_create_pool(name='poolname', replica_count=3)
4260+#
4261+# if is_request_complete(rq):
4262+# <Request complete actions>
4263+# else:
4264+# send_request_if_needed(get_ceph_request())
4265+#
4266+# CephBrokerRq and CephBrokerRsp are serialized into JSON. Below is an example
4267+# of glance having sent a request to ceph which ceph has successfully processed
4268+# 'ceph:8': {
4269+# 'ceph/0': {
4270+# 'auth': 'cephx',
4271+# 'broker-rsp-glance-0': '{"request-id": "0bc7dc54", "exit-code": 0}',
4272+# 'broker_rsp': '{"request-id": "0da543b8", "exit-code": 0}',
4273+# 'ceph-public-address': '10.5.44.103',
4274+# 'key': 'AQCLDttVuHXINhAAvI144CB09dYchhHyTUY9BQ==',
4275+# 'private-address': '10.5.44.103',
4276+# },
4277+# 'glance/0': {
4278+# 'broker_req': ('{"api-version": 1, "request-id": "0bc7dc54", '
4279+# '"ops": [{"replicas": 3, "name": "glance", '
4280+# '"op": "create-pool"}]}'),
4281+# 'private-address': '10.5.44.109',
4282+# },
4283+# }
4284+
4285+def get_previous_request(rid):
4286+ """Return the last ceph broker request sent on a given relation
4287+
4288+ @param rid: Relation id to query for request
4289+ """
4290+ request = None
4291+ broker_req = relation_get(attribute='broker_req', rid=rid,
4292+ unit=local_unit())
4293+ if broker_req:
4294+ request_data = json.loads(broker_req)
4295+ request = CephBrokerRq(api_version=request_data['api-version'],
4296+ request_id=request_data['request-id'])
4297+ request.set_ops(request_data['ops'])
4298+
4299+ return request
4300+
4301+
4302+def get_request_states(request):
4303+ """Return a dict of requests per relation id with their corresponding
4304+ completion state.
4305+
4306+ This allows a charm, which has a request for ceph, to see whether there is
4307+ an equivalent request already being processed and if so what state that
4308+ request is in.
4309+
4310+ @param request: A CephBrokerRq object
4311+ """
4312+ complete = []
4313+ requests = {}
4314+ for rid in relation_ids('ceph'):
4315+ complete = False
4316+ previous_request = get_previous_request(rid)
4317+ if request == previous_request:
4318+ sent = True
4319+ complete = is_request_complete_for_rid(previous_request, rid)
4320+ else:
4321+ sent = False
4322+ complete = False
4323+
4324+ requests[rid] = {
4325+ 'sent': sent,
4326+ 'complete': complete,
4327+ }
4328+
4329+ return requests
4330+
4331+
4332+def is_request_sent(request):
4333+ """Check to see if a functionally equivalent request has already been sent
4334+
4335+ Returns True if a similair request has been sent
4336+
4337+ @param request: A CephBrokerRq object
4338+ """
4339+ states = get_request_states(request)
4340+ for rid in states.keys():
4341+ if not states[rid]['sent']:
4342+ return False
4343+
4344+ return True
4345+
4346+
4347+def is_request_complete(request):
4348+ """Check to see if a functionally equivalent request has already been
4349+ completed
4350+
4351+ Returns True if a similair request has been completed
4352+
4353+ @param request: A CephBrokerRq object
4354+ """
4355+ states = get_request_states(request)
4356+ for rid in states.keys():
4357+ if not states[rid]['complete']:
4358+ return False
4359+
4360+ return True
4361+
4362+
4363+def is_request_complete_for_rid(request, rid):
4364+ """Check if a given request has been completed on the given relation
4365+
4366+ @param request: A CephBrokerRq object
4367+ @param rid: Relation ID
4368+ """
4369+ broker_key = get_broker_rsp_key()
4370+ for unit in related_units(rid):
4371+ rdata = relation_get(rid=rid, unit=unit)
4372+ if rdata.get(broker_key):
4373+ rsp = CephBrokerRsp(rdata.get(broker_key))
4374+ if rsp.request_id == request.request_id:
4375+ if not rsp.exit_code:
4376+ return True
4377+ else:
4378+ # The remote unit sent no reply targeted at this unit so either the
4379+ # remote ceph cluster does not support unit targeted replies or it
4380+ # has not processed our request yet.
4381+ if rdata.get('broker_rsp'):
4382+ request_data = json.loads(rdata['broker_rsp'])
4383+ if request_data.get('request-id'):
4384+ log('Ignoring legacy broker_rsp without unit key as remote '
4385+ 'service supports unit specific replies', level=DEBUG)
4386+ else:
4387+ log('Using legacy broker_rsp as remote service does not '
4388+ 'supports unit specific replies', level=DEBUG)
4389+ rsp = CephBrokerRsp(rdata['broker_rsp'])
4390+ if not rsp.exit_code:
4391+ return True
4392+
4393+ return False
4394+
4395+
4396+def get_broker_rsp_key():
4397+ """Return broker response key for this unit
4398+
4399+ This is the key that ceph is going to use to pass request status
4400+ information back to this unit
4401+ """
4402+ return 'broker-rsp-' + local_unit().replace('/', '-')
4403+
4404+
4405+def send_request_if_needed(request):
4406+ """Send broker request if an equivalent request has not already been sent
4407+
4408+ @param request: A CephBrokerRq object
4409+ """
4410+ if is_request_sent(request):
4411+ log('Request already sent but not complete, not sending new request',
4412+ level=DEBUG)
4413+ else:
4414+ for rid in relation_ids('ceph'):
4415+ log('Sending request {}'.format(request.request_id), level=DEBUG)
4416+ relation_set(relation_id=rid, broker_req=request.request)
4417
4418=== modified file 'hooks/charmhelpers/contrib/storage/linux/loopback.py'
4419--- hooks/charmhelpers/contrib/storage/linux/loopback.py 2014-12-10 20:28:57 +0000
4420+++ hooks/charmhelpers/contrib/storage/linux/loopback.py 2015-09-21 10:47:21 +0000
4421@@ -1,3 +1,19 @@
4422+# Copyright 2014-2015 Canonical Limited.
4423+#
4424+# This file is part of charm-helpers.
4425+#
4426+# charm-helpers is free software: you can redistribute it and/or modify
4427+# it under the terms of the GNU Lesser General Public License version 3 as
4428+# published by the Free Software Foundation.
4429+#
4430+# charm-helpers is distributed in the hope that it will be useful,
4431+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4432+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4433+# GNU Lesser General Public License for more details.
4434+#
4435+# You should have received a copy of the GNU Lesser General Public License
4436+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4437+
4438 import os
4439 import re
4440 from subprocess import (
4441
4442=== modified file 'hooks/charmhelpers/contrib/storage/linux/lvm.py'
4443--- hooks/charmhelpers/contrib/storage/linux/lvm.py 2014-12-10 20:28:57 +0000
4444+++ hooks/charmhelpers/contrib/storage/linux/lvm.py 2015-09-21 10:47:21 +0000
4445@@ -1,3 +1,19 @@
4446+# Copyright 2014-2015 Canonical Limited.
4447+#
4448+# This file is part of charm-helpers.
4449+#
4450+# charm-helpers is free software: you can redistribute it and/or modify
4451+# it under the terms of the GNU Lesser General Public License version 3 as
4452+# published by the Free Software Foundation.
4453+#
4454+# charm-helpers is distributed in the hope that it will be useful,
4455+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4456+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4457+# GNU Lesser General Public License for more details.
4458+#
4459+# You should have received a copy of the GNU Lesser General Public License
4460+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4461+
4462 from subprocess import (
4463 CalledProcessError,
4464 check_call,
4465
4466=== modified file 'hooks/charmhelpers/contrib/storage/linux/utils.py'
4467--- hooks/charmhelpers/contrib/storage/linux/utils.py 2014-12-10 20:28:57 +0000
4468+++ hooks/charmhelpers/contrib/storage/linux/utils.py 2015-09-21 10:47:21 +0000
4469@@ -1,3 +1,19 @@
4470+# Copyright 2014-2015 Canonical Limited.
4471+#
4472+# This file is part of charm-helpers.
4473+#
4474+# charm-helpers is free software: you can redistribute it and/or modify
4475+# it under the terms of the GNU Lesser General Public License version 3 as
4476+# published by the Free Software Foundation.
4477+#
4478+# charm-helpers is distributed in the hope that it will be useful,
4479+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4480+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4481+# GNU Lesser General Public License for more details.
4482+#
4483+# You should have received a copy of the GNU Lesser General Public License
4484+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4485+
4486 import os
4487 import re
4488 from stat import S_ISBLK
4489@@ -27,9 +43,10 @@
4490
4491 :param block_device: str: Full path of block device to clean.
4492 '''
4493+ # https://github.com/ceph/ceph/commit/fdd7f8d83afa25c4e09aaedd90ab93f3b64a677b
4494 # sometimes sgdisk exits non-zero; this is OK, dd will clean up
4495- call(['sgdisk', '--zap-all', '--mbrtogpt',
4496- '--clear', block_device])
4497+ call(['sgdisk', '--zap-all', '--', block_device])
4498+ call(['sgdisk', '--clear', '--mbrtogpt', '--', block_device])
4499 dev_end = check_output(['blockdev', '--getsz',
4500 block_device]).decode('UTF-8')
4501 gpt_end = int(dev_end.split()[0]) - 100
4502@@ -51,4 +68,4 @@
4503 out = check_output(['mount']).decode('UTF-8')
4504 if is_partition:
4505 return bool(re.search(device + r"\b", out))
4506- return bool(re.search(device + r"[0-9]+\b", out))
4507+ return bool(re.search(device + r"[0-9]*\b", out))
4508
4509=== modified file 'hooks/charmhelpers/core/__init__.py'
4510--- hooks/charmhelpers/core/__init__.py 2013-07-19 02:37:30 +0000
4511+++ hooks/charmhelpers/core/__init__.py 2015-09-21 10:47:21 +0000
4512@@ -0,0 +1,15 @@
4513+# Copyright 2014-2015 Canonical Limited.
4514+#
4515+# This file is part of charm-helpers.
4516+#
4517+# charm-helpers is free software: you can redistribute it and/or modify
4518+# it under the terms of the GNU Lesser General Public License version 3 as
4519+# published by the Free Software Foundation.
4520+#
4521+# charm-helpers is distributed in the hope that it will be useful,
4522+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4523+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4524+# GNU Lesser General Public License for more details.
4525+#
4526+# You should have received a copy of the GNU Lesser General Public License
4527+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4528
4529=== modified file 'hooks/charmhelpers/core/decorators.py'
4530--- hooks/charmhelpers/core/decorators.py 2015-01-14 15:30:27 +0000
4531+++ hooks/charmhelpers/core/decorators.py 2015-09-21 10:47:21 +0000
4532@@ -1,3 +1,19 @@
4533+# Copyright 2014-2015 Canonical Limited.
4534+#
4535+# This file is part of charm-helpers.
4536+#
4537+# charm-helpers is free software: you can redistribute it and/or modify
4538+# it under the terms of the GNU Lesser General Public License version 3 as
4539+# published by the Free Software Foundation.
4540+#
4541+# charm-helpers is distributed in the hope that it will be useful,
4542+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4543+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4544+# GNU Lesser General Public License for more details.
4545+#
4546+# You should have received a copy of the GNU Lesser General Public License
4547+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4548+
4549 #
4550 # Copyright 2014 Canonical Ltd.
4551 #
4552
4553=== added file 'hooks/charmhelpers/core/files.py'
4554--- hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
4555+++ hooks/charmhelpers/core/files.py 2015-09-21 10:47:21 +0000
4556@@ -0,0 +1,45 @@
4557+#!/usr/bin/env python
4558+# -*- coding: utf-8 -*-
4559+
4560+# Copyright 2014-2015 Canonical Limited.
4561+#
4562+# This file is part of charm-helpers.
4563+#
4564+# charm-helpers is free software: you can redistribute it and/or modify
4565+# it under the terms of the GNU Lesser General Public License version 3 as
4566+# published by the Free Software Foundation.
4567+#
4568+# charm-helpers is distributed in the hope that it will be useful,
4569+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4570+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4571+# GNU Lesser General Public License for more details.
4572+#
4573+# You should have received a copy of the GNU Lesser General Public License
4574+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4575+
4576+__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
4577+
4578+import os
4579+import subprocess
4580+
4581+
4582+def sed(filename, before, after, flags='g'):
4583+ """
4584+ Search and replaces the given pattern on filename.
4585+
4586+ :param filename: relative or absolute file path.
4587+ :param before: expression to be replaced (see 'man sed')
4588+ :param after: expression to replace with (see 'man sed')
4589+ :param flags: sed-compatible regex flags in example, to make
4590+ the search and replace case insensitive, specify ``flags="i"``.
4591+ The ``g`` flag is always specified regardless, so you do not
4592+ need to remember to include it when overriding this parameter.
4593+ :returns: If the sed command exit code was zero then return,
4594+ otherwise raise CalledProcessError.
4595+ """
4596+ expression = r's/{0}/{1}/{2}'.format(before,
4597+ after, flags)
4598+
4599+ return subprocess.check_call(["sed", "-i", "-r", "-e",
4600+ expression,
4601+ os.path.expanduser(filename)])
4602
4603=== modified file 'hooks/charmhelpers/core/fstab.py'
4604--- hooks/charmhelpers/core/fstab.py 2014-12-10 20:28:57 +0000
4605+++ hooks/charmhelpers/core/fstab.py 2015-09-21 10:47:21 +0000
4606@@ -1,11 +1,27 @@
4607 #!/usr/bin/env python
4608 # -*- coding: utf-8 -*-
4609
4610-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
4611+# Copyright 2014-2015 Canonical Limited.
4612+#
4613+# This file is part of charm-helpers.
4614+#
4615+# charm-helpers is free software: you can redistribute it and/or modify
4616+# it under the terms of the GNU Lesser General Public License version 3 as
4617+# published by the Free Software Foundation.
4618+#
4619+# charm-helpers is distributed in the hope that it will be useful,
4620+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4621+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4622+# GNU Lesser General Public License for more details.
4623+#
4624+# You should have received a copy of the GNU Lesser General Public License
4625+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4626
4627 import io
4628 import os
4629
4630+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
4631+
4632
4633 class Fstab(io.FileIO):
4634 """This class extends file in order to implement a file reader/writer
4635@@ -61,7 +77,7 @@
4636 for line in self.readlines():
4637 line = line.decode('us-ascii')
4638 try:
4639- if line.strip() and not line.startswith("#"):
4640+ if line.strip() and not line.strip().startswith("#"):
4641 yield self._hydrate_entry(line)
4642 except ValueError:
4643 pass
4644@@ -88,7 +104,7 @@
4645
4646 found = False
4647 for index, line in enumerate(lines):
4648- if not line.startswith("#"):
4649+ if line.strip() and not line.strip().startswith("#"):
4650 if self._hydrate_entry(line) == entry:
4651 found = True
4652 break
4653
4654=== modified file 'hooks/charmhelpers/core/hookenv.py'
4655--- hooks/charmhelpers/core/hookenv.py 2014-12-11 13:41:43 +0000
4656+++ hooks/charmhelpers/core/hookenv.py 2015-09-21 10:47:21 +0000
4657@@ -1,14 +1,37 @@
4658+# Copyright 2014-2015 Canonical Limited.
4659+#
4660+# This file is part of charm-helpers.
4661+#
4662+# charm-helpers is free software: you can redistribute it and/or modify
4663+# it under the terms of the GNU Lesser General Public License version 3 as
4664+# published by the Free Software Foundation.
4665+#
4666+# charm-helpers is distributed in the hope that it will be useful,
4667+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4668+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4669+# GNU Lesser General Public License for more details.
4670+#
4671+# You should have received a copy of the GNU Lesser General Public License
4672+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4673+
4674 "Interactions with the Juju environment"
4675 # Copyright 2013 Canonical Ltd.
4676 #
4677 # Authors:
4678 # Charm Helpers Developers <juju@lists.ubuntu.com>
4679
4680+from __future__ import print_function
4681+import copy
4682+from distutils.version import LooseVersion
4683+from functools import wraps
4684+import glob
4685 import os
4686 import json
4687 import yaml
4688 import subprocess
4689 import sys
4690+import errno
4691+import tempfile
4692 from subprocess import CalledProcessError
4693
4694 import six
4695@@ -40,15 +63,18 @@
4696
4697 will cache the result of unit_get + 'test' for future calls.
4698 """
4699+ @wraps(func)
4700 def wrapper(*args, **kwargs):
4701 global cache
4702 key = str((func, args, kwargs))
4703 try:
4704 return cache[key]
4705 except KeyError:
4706- res = func(*args, **kwargs)
4707- cache[key] = res
4708- return res
4709+ pass # Drop out of the exception handler scope.
4710+ res = func(*args, **kwargs)
4711+ cache[key] = res
4712+ return res
4713+ wrapper._wrapped = func
4714 return wrapper
4715
4716
4717@@ -71,7 +97,18 @@
4718 if not isinstance(message, six.string_types):
4719 message = repr(message)
4720 command += [message]
4721- subprocess.call(command)
4722+ # Missing juju-log should not cause failures in unit tests
4723+ # Send log output to stderr
4724+ try:
4725+ subprocess.call(command)
4726+ except OSError as e:
4727+ if e.errno == errno.ENOENT:
4728+ if level:
4729+ message = "{}: {}".format(level, message)
4730+ message = "juju-log: {}".format(message)
4731+ print(message, file=sys.stderr)
4732+ else:
4733+ raise
4734
4735
4736 class Serializable(UserDict):
4737@@ -137,9 +174,19 @@
4738 return os.environ.get('JUJU_RELATION', None)
4739
4740
4741-def relation_id():
4742- """The relation ID for the current relation hook"""
4743- return os.environ.get('JUJU_RELATION_ID', None)
4744+@cached
4745+def relation_id(relation_name=None, service_or_unit=None):
4746+ """The relation ID for the current or a specified relation"""
4747+ if not relation_name and not service_or_unit:
4748+ return os.environ.get('JUJU_RELATION_ID', None)
4749+ elif relation_name and service_or_unit:
4750+ service_name = service_or_unit.split('/')[0]
4751+ for relid in relation_ids(relation_name):
4752+ remote_service = remote_service_name(relid)
4753+ if remote_service == service_name:
4754+ return relid
4755+ else:
4756+ raise ValueError('Must specify neither or both of relation_name and service_or_unit')
4757
4758
4759 def local_unit():
4760@@ -149,7 +196,7 @@
4761
4762 def remote_unit():
4763 """The remote unit for the current relation hook"""
4764- return os.environ['JUJU_REMOTE_UNIT']
4765+ return os.environ.get('JUJU_REMOTE_UNIT', None)
4766
4767
4768 def service_name():
4769@@ -157,9 +204,20 @@
4770 return local_unit().split('/')[0]
4771
4772
4773+@cached
4774+def remote_service_name(relid=None):
4775+ """The remote service name for a given relation-id (or the current relation)"""
4776+ if relid is None:
4777+ unit = remote_unit()
4778+ else:
4779+ units = related_units(relid)
4780+ unit = units[0] if units else None
4781+ return unit.split('/')[0] if unit else None
4782+
4783+
4784 def hook_name():
4785 """The name of the currently executing hook"""
4786- return os.path.basename(sys.argv[0])
4787+ return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
4788
4789
4790 class Config(dict):
4791@@ -209,23 +267,7 @@
4792 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
4793 if os.path.exists(self.path):
4794 self.load_previous()
4795-
4796- def __getitem__(self, key):
4797- """For regular dict lookups, check the current juju config first,
4798- then the previous (saved) copy. This ensures that user-saved values
4799- will be returned by a dict lookup.
4800-
4801- """
4802- try:
4803- return dict.__getitem__(self, key)
4804- except KeyError:
4805- return (self._prev_dict or {})[key]
4806-
4807- def keys(self):
4808- prev_keys = []
4809- if self._prev_dict is not None:
4810- prev_keys = self._prev_dict.keys()
4811- return list(set(prev_keys + list(dict.keys(self))))
4812+ atexit(self._implicit_save)
4813
4814 def load_previous(self, path=None):
4815 """Load previous copy of config from disk.
4816@@ -244,6 +286,9 @@
4817 self.path = path or self.path
4818 with open(self.path) as f:
4819 self._prev_dict = json.load(f)
4820+ for k, v in copy.deepcopy(self._prev_dict).items():
4821+ if k not in self:
4822+ self[k] = v
4823
4824 def changed(self, key):
4825 """Return True if the current value for this key is different from
4826@@ -275,13 +320,13 @@
4827 instance.
4828
4829 """
4830- if self._prev_dict:
4831- for k, v in six.iteritems(self._prev_dict):
4832- if k not in self:
4833- self[k] = v
4834 with open(self.path, 'w') as f:
4835 json.dump(self, f)
4836
4837+ def _implicit_save(self):
4838+ if self.implicit_save:
4839+ self.save()
4840+
4841
4842 @cached
4843 def config(scope=None):
4844@@ -324,18 +369,49 @@
4845 """Set relation information for the current unit"""
4846 relation_settings = relation_settings if relation_settings else {}
4847 relation_cmd_line = ['relation-set']
4848+ accepts_file = "--file" in subprocess.check_output(
4849+ relation_cmd_line + ["--help"], universal_newlines=True)
4850 if relation_id is not None:
4851 relation_cmd_line.extend(('-r', relation_id))
4852- for k, v in (list(relation_settings.items()) + list(kwargs.items())):
4853- if v is None:
4854- relation_cmd_line.append('{}='.format(k))
4855- else:
4856- relation_cmd_line.append('{}={}'.format(k, v))
4857- subprocess.check_call(relation_cmd_line)
4858+ settings = relation_settings.copy()
4859+ settings.update(kwargs)
4860+ for key, value in settings.items():
4861+ # Force value to be a string: it always should, but some call
4862+ # sites pass in things like dicts or numbers.
4863+ if value is not None:
4864+ settings[key] = "{}".format(value)
4865+ if accepts_file:
4866+ # --file was introduced in Juju 1.23.2. Use it by default if
4867+ # available, since otherwise we'll break if the relation data is
4868+ # too big. Ideally we should tell relation-set to read the data from
4869+ # stdin, but that feature is broken in 1.23.2: Bug #1454678.
4870+ with tempfile.NamedTemporaryFile(delete=False) as settings_file:
4871+ settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
4872+ subprocess.check_call(
4873+ relation_cmd_line + ["--file", settings_file.name])
4874+ os.remove(settings_file.name)
4875+ else:
4876+ for key, value in settings.items():
4877+ if value is None:
4878+ relation_cmd_line.append('{}='.format(key))
4879+ else:
4880+ relation_cmd_line.append('{}={}'.format(key, value))
4881+ subprocess.check_call(relation_cmd_line)
4882 # Flush cache of any relation-gets for local unit
4883 flush(local_unit())
4884
4885
4886+def relation_clear(r_id=None):
4887+ ''' Clears any relation data already set on relation r_id '''
4888+ settings = relation_get(rid=r_id,
4889+ unit=local_unit())
4890+ for setting in settings:
4891+ if setting not in ['public-address', 'private-address']:
4892+ settings[setting] = None
4893+ relation_set(relation_id=r_id,
4894+ **settings)
4895+
4896+
4897 @cached
4898 def relation_ids(reltype=None):
4899 """A list of relation_ids"""
4900@@ -415,6 +491,63 @@
4901
4902
4903 @cached
4904+def relation_to_interface(relation_name):
4905+ """
4906+ Given the name of a relation, return the interface that relation uses.
4907+
4908+ :returns: The interface name, or ``None``.
4909+ """
4910+ return relation_to_role_and_interface(relation_name)[1]
4911+
4912+
4913+@cached
4914+def relation_to_role_and_interface(relation_name):
4915+ """
4916+ Given the name of a relation, return the role and the name of the interface
4917+ that relation uses (where role is one of ``provides``, ``requires``, or ``peer``).
4918+
4919+ :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
4920+ """
4921+ _metadata = metadata()
4922+ for role in ('provides', 'requires', 'peer'):
4923+ interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
4924+ if interface:
4925+ return role, interface
4926+ return None, None
4927+
4928+
4929+@cached
4930+def role_and_interface_to_relations(role, interface_name):
4931+ """
4932+ Given a role and interface name, return a list of relation names for the
4933+ current charm that use that interface under that role (where role is one
4934+ of ``provides``, ``requires``, or ``peer``).
4935+
4936+ :returns: A list of relation names.
4937+ """
4938+ _metadata = metadata()
4939+ results = []
4940+ for relation_name, relation in _metadata.get(role, {}).items():
4941+ if relation['interface'] == interface_name:
4942+ results.append(relation_name)
4943+ return results
4944+
4945+
4946+@cached
4947+def interface_to_relations(interface_name):
4948+ """
4949+ Given an interface, return a list of relation names for the current
4950+ charm that use that interface.
4951+
4952+ :returns: A list of relation names.
4953+ """
4954+ results = []
4955+ for role in ('provides', 'requires', 'peer'):
4956+ results.extend(role_and_interface_to_relations(role, interface_name))
4957+ return results
4958+
4959+
4960+@cached
4961 def charm_name():
4962 """Get the name of the current charm as is specified on metadata.yaml"""
4963 return metadata().get('name')
4964@@ -480,6 +613,11 @@
4965 return None
4966
4967
4968+def unit_public_ip():
4969+ """Get this unit's public IP address"""
4970+ return unit_get('public-address')
4971+
4972+
4973 def unit_private_ip():
4974 """Get this unit's private IP address"""
4975 return unit_get('private-address')
4976@@ -512,10 +650,14 @@
4977 hooks.execute(sys.argv)
4978 """
4979
4980- def __init__(self, config_save=True):
4981+ def __init__(self, config_save=None):
4982 super(Hooks, self).__init__()
4983 self._hooks = {}
4984- self._config_save = config_save
4985+
4986+ # For unknown reasons, we allow the Hooks constructor to override
4987+ # config().implicit_save.
4988+ if config_save is not None:
4989+ config().implicit_save = config_save
4990
4991 def register(self, name, function):
4992 """Register a hook"""
4993@@ -523,13 +665,16 @@
4994
4995 def execute(self, args):
4996 """Execute a registered hook based on args[0]"""
4997+ _run_atstart()
4998 hook_name = os.path.basename(args[0])
4999 if hook_name in self._hooks:
5000- self._hooks[hook_name]()
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: