Merge lp:~celebdor/charms/trusty/neutron-agents-midonet/split into lp:~celebdor/charms/trusty/neutron-agents-midonet/trunk

Proposed by Antoni Segura Puimedon
Status: Merged
Merged at revision: 44
Proposed branch: lp:~celebdor/charms/trusty/neutron-agents-midonet/split
Merge into: lp:~celebdor/charms/trusty/neutron-agents-midonet/trunk
Diff against target: 11028 lines (+9153/-405)
78 files modified
.bzrignore (+1/-0)
charm-helpers-hooks.yaml (+14/-0)
config.yaml (+59/-4)
hooks/amqp-relation-broken (+21/-0)
hooks/amqp-relation-changed (+21/-0)
hooks/amqp-relation-joined (+25/-0)
hooks/charmhelpers/contrib/__init__.py (+15/-0)
hooks/charmhelpers/contrib/hahelpers/__init__.py (+15/-0)
hooks/charmhelpers/contrib/hahelpers/apache.py (+82/-0)
hooks/charmhelpers/contrib/hahelpers/cluster.py (+316/-0)
hooks/charmhelpers/contrib/network/__init__.py (+15/-0)
hooks/charmhelpers/contrib/network/ip.py (+456/-0)
hooks/charmhelpers/contrib/openstack/__init__.py (+15/-0)
hooks/charmhelpers/contrib/openstack/context.py (+1443/-0)
hooks/charmhelpers/contrib/openstack/ip.py (+151/-0)
hooks/charmhelpers/contrib/openstack/neutron.py (+370/-0)
hooks/charmhelpers/contrib/openstack/templates/__init__.py (+18/-0)
hooks/charmhelpers/contrib/openstack/templates/ceph.conf (+21/-0)
hooks/charmhelpers/contrib/openstack/templates/git.upstart (+17/-0)
hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg (+58/-0)
hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend (+24/-0)
hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend.conf (+24/-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 (+323/-0)
hooks/charmhelpers/contrib/openstack/utils.py (+982/-0)
hooks/charmhelpers/contrib/python/__init__.py (+15/-0)
hooks/charmhelpers/contrib/python/debug.py (+56/-0)
hooks/charmhelpers/contrib/python/packages.py (+121/-0)
hooks/charmhelpers/contrib/python/rpdb.py (+58/-0)
hooks/charmhelpers/contrib/python/version.py (+34/-0)
hooks/charmhelpers/contrib/storage/__init__.py (+15/-0)
hooks/charmhelpers/contrib/storage/linux/__init__.py (+15/-0)
hooks/charmhelpers/contrib/storage/linux/ceph.py (+657/-0)
hooks/charmhelpers/contrib/storage/linux/loopback.py (+78/-0)
hooks/charmhelpers/contrib/storage/linux/lvm.py (+105/-0)
hooks/charmhelpers/contrib/storage/linux/utils.py (+71/-0)
hooks/charmhelpers/core/decorators.py (+57/-0)
hooks/charmhelpers/core/hookenv.py (+365/-42)
hooks/charmhelpers/core/host.py (+171/-24)
hooks/charmhelpers/core/services/base.py (+43/-19)
hooks/charmhelpers/core/services/helpers.py (+18/-2)
hooks/charmhelpers/core/strutils.py (+72/-0)
hooks/charmhelpers/core/sysctl.py (+56/-0)
hooks/charmhelpers/core/unitdata.py (+521/-0)
hooks/charmhelpers/fetch/__init__.py (+32/-15)
hooks/charmhelpers/fetch/archiveurl.py (+7/-1)
hooks/charmhelpers/fetch/giturl.py (+8/-6)
hooks/install (+17/-2)
hooks/midonet-relation-broken (+21/-0)
hooks/midonet-relation-changed (+21/-0)
hooks/midonet-relation-joined (+21/-0)
hooks/midonet_helpers/puppet.py (+71/-17)
hooks/midonet_helpers/relations.py (+1/-9)
hooks/midonet_helpers/repositories.py (+28/-0)
hooks/relations.py (+0/-10)
hooks/services.py (+88/-5)
metadata.yaml (+9/-2)
templates/common.yaml (+1/-0)
templates/dhcp_agent.ini (+10/-2)
templates/juno/neutron.conf (+8/-0)
templates/juno/nova.conf (+25/-0)
templates/kilo/neutron.conf (+7/-0)
templates/kilo/nova.conf (+29/-0)
templates/metadata_agent.ini (+5/-3)
templates/parts/rabbitmq (+21/-0)
tests/00-setup (+3/-0)
tests/010-basic-trusty-juno (+4/-3)
tests/011-basic-trusty-kilo (+4/-3)
tests/basic_deployment.py (+126/-89)
tests/charmhelpers/contrib/amulet/deployment.py (+4/-2)
tests/charmhelpers/contrib/amulet/utils.py (+564/-69)
tests/charmhelpers/contrib/openstack/amulet/deployment.py (+163/-13)
tests/charmhelpers/contrib/openstack/amulet/utils.py (+742/-51)
unit_tests/midonet_helpers/test.py (+8/-8)
unit_tests/test_context.py (+42/-1)
unit_tests/test_templates.py (+4/-3)
To merge this branch: bzr merge lp:~celebdor/charms/trusty/neutron-agents-midonet/split
Reviewer Review Type Date Requested Status
Antoni Segura Puimedon Pending
Review via email: mp+285081@code.launchpad.net
To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2015-08-06 23:20:27 +0000
3+++ .bzrignore 2016-02-04 15:46:27 +0000
4@@ -4,3 +4,4 @@
5 tags
6 .venv2
7 .venv3
8+.unit-state.db
9
10=== modified file 'charm-helpers-hooks.yaml'
11--- charm-helpers-hooks.yaml 2015-08-06 23:20:27 +0000
12+++ charm-helpers-hooks.yaml 2016-02-04 15:46:27 +0000
13@@ -1,9 +1,23 @@
14 branch: lp:~openstack-charmers/charm-helpers/stable
15 destination: hooks/charmhelpers
16 include:
17+ - contrib.hahelpers
18+ - contrib.openstack.context
19+ - contrib.openstack.ip
20+ - contrib.openstack.neutron
21+ - contrib.openstack.templates|inc=*
22+ - contrib.openstack.templating
23+ - contrib.openstack.utils
24+ - contrib.network.ip
25+ - contrib.python
26+ - contrib.storage.linux
27+ - core.decorators
28 - core.fstab
29 - core.hookenv
30 - core.host
31 - core.services
32+ - core.strutils
33+ - core.sysctl
34 - core.templating
35+ - core.unitdata
36 - fetch
37
38=== modified file 'config.yaml'
39--- config.yaml 2015-08-06 23:20:27 +0000
40+++ config.yaml 2016-02-04 15:46:27 +0000
41@@ -14,7 +14,62 @@
42 # limitations under the License.
43 #
44 options:
45- shared_secret:
46- type: string
47- default: ""
48- description: nova-api metadata API and neutron-metada-agent shared secret
49+ openstack-origin:
50+ default: distro
51+ type: string
52+ description: |
53+ Repository from which to install. May be one of the following:
54+ distro (default), ppa:somecustom/ppa, a deb url sources entry,
55+ or a supported Cloud Archive release pocket.
56+
57+ Supported Cloud Archive sources include:
58+
59+ cloud:<series>-<openstack-release>
60+ cloud:<series>-<openstack-release>/updates
61+ cloud:<series>-<openstack-release>/staging
62+ cloud:<series>-<openstack-release>/proposed
63+
64+ For series=Precise we support cloud archives for openstack-release:
65+ * icehouse
66+
67+ For series=Trusty we support cloud archives for openstack-release:
68+ * juno
69+ * kilo
70+ * ...
71+
72+ NOTE: updating this setting to a source that is known to provide
73+ a later version of OpenStack will trigger a software upgrade.
74+
75+ NOTE: when openstack-origin-git is specified, openstack specific
76+ packages will be installed from source rather than from the
77+ openstack-origin repository.
78+ midonet-origin:
79+ default: midonet-2015.06
80+ type: string
81+ description: |
82+ 'mem-1.8', 'mem-1.9',
83+ 'midonet-2015.06'
84+
85+ NOTE: updating this setting to a source that is known to provide a later
86+ version of MidoNet (do not change between MEM and MidoNet) will
87+ trigger a software upgrade.
88+ mem-username:
89+ type: string
90+ default:
91+ description: |
92+ The Midokura Enterprise MidoNet username credentials to access the
93+ repository.
94+ mem-password:
95+ type: string
96+ default:
97+ description: |
98+ The Midokura Enterprise MidoNet password credentials to access the
99+ repository.
100+ rabbit-user:
101+ default: neutronagents
102+ type: string
103+ description: Username used to access rabbitmq queue
104+ rabbit-vhost:
105+ default: openstack
106+ type: string
107+ description: Rabbitmq vhost
108
109=== added file 'hooks/amqp-relation-broken'
110--- hooks/amqp-relation-broken 1970-01-01 00:00:00 +0000
111+++ hooks/amqp-relation-broken 2016-02-04 15:46:27 +0000
112@@ -0,0 +1,21 @@
113+#!/usr/bin/env python
114+# vim: tabstop=4 shiftwidth=4 softtabstop=4 filetype=python
115+#
116+# Copyright (c) 2015 Midokura SARL, All Rights Reserved.
117+#
118+# Licensed under the Apache License, Version 2.0 (the "License");
119+# you may not use this file except in compliance with the License.
120+# You may obtain a copy of the License at
121+#
122+# http://www.apache.org/licenses/LICENSE-2.0
123+#
124+# Unless required by applicable law or agreed to in writing, software
125+# distributed under the License is distributed on an "AS IS" BASIS,
126+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
127+# See the License for the specific language governing permissions and
128+# limitations under the License.
129+#
130+import services
131+
132+
133+services.manage()
134
135=== added file 'hooks/amqp-relation-changed'
136--- hooks/amqp-relation-changed 1970-01-01 00:00:00 +0000
137+++ hooks/amqp-relation-changed 2016-02-04 15:46:27 +0000
138@@ -0,0 +1,21 @@
139+#!/usr/bin/env python
140+# vim: tabstop=4 shiftwidth=4 softtabstop=4 filetype=python
141+#
142+# Copyright (c) 2015 Midokura SARL, All Rights Reserved.
143+#
144+# Licensed under the Apache License, Version 2.0 (the "License");
145+# you may not use this file except in compliance with the License.
146+# You may obtain a copy of the License at
147+#
148+# http://www.apache.org/licenses/LICENSE-2.0
149+#
150+# Unless required by applicable law or agreed to in writing, software
151+# distributed under the License is distributed on an "AS IS" BASIS,
152+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
153+# See the License for the specific language governing permissions and
154+# limitations under the License.
155+#
156+import services
157+
158+
159+services.manage()
160
161=== added file 'hooks/amqp-relation-joined'
162--- hooks/amqp-relation-joined 1970-01-01 00:00:00 +0000
163+++ hooks/amqp-relation-joined 2016-02-04 15:46:27 +0000
164@@ -0,0 +1,25 @@
165+#!/usr/bin/env python
166+# vim: tabstop=4 shiftwidth=4 softtabstop=4 filetype=python
167+#
168+# Copyright (c) 2015 Midokura SARL, All Rights Reserved.
169+#
170+# Licensed under the Apache License, Version 2.0 (the "License");
171+# you may not use this file except in compliance with the License.
172+# You may obtain a copy of the License at
173+#
174+# http://www.apache.org/licenses/LICENSE-2.0
175+#
176+# Unless required by applicable law or agreed to in writing, software
177+# distributed under the License is distributed on an "AS IS" BASIS,
178+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
179+# See the License for the specific language governing permissions and
180+# limitations under the License.
181+#
182+from charmhelpers.core import hookenv
183+import services
184+
185+config = hookenv.config()
186+hookenv.relation_set(username=config.get('rabbit-user'),
187+ vhost=config.get('rabbit-vhost'))
188+
189+services.manage()
190
191=== added directory 'hooks/charmhelpers/contrib'
192=== added file 'hooks/charmhelpers/contrib/__init__.py'
193--- hooks/charmhelpers/contrib/__init__.py 1970-01-01 00:00:00 +0000
194+++ hooks/charmhelpers/contrib/__init__.py 2016-02-04 15:46:27 +0000
195@@ -0,0 +1,15 @@
196+# Copyright 2014-2015 Canonical Limited.
197+#
198+# This file is part of charm-helpers.
199+#
200+# charm-helpers is free software: you can redistribute it and/or modify
201+# it under the terms of the GNU Lesser General Public License version 3 as
202+# published by the Free Software Foundation.
203+#
204+# charm-helpers is distributed in the hope that it will be useful,
205+# but WITHOUT ANY WARRANTY; without even the implied warranty of
206+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
207+# GNU Lesser General Public License for more details.
208+#
209+# You should have received a copy of the GNU Lesser General Public License
210+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
211
212=== added directory 'hooks/charmhelpers/contrib/hahelpers'
213=== added file 'hooks/charmhelpers/contrib/hahelpers/__init__.py'
214--- hooks/charmhelpers/contrib/hahelpers/__init__.py 1970-01-01 00:00:00 +0000
215+++ hooks/charmhelpers/contrib/hahelpers/__init__.py 2016-02-04 15:46:27 +0000
216@@ -0,0 +1,15 @@
217+# Copyright 2014-2015 Canonical Limited.
218+#
219+# This file is part of charm-helpers.
220+#
221+# charm-helpers is free software: you can redistribute it and/or modify
222+# it under the terms of the GNU Lesser General Public License version 3 as
223+# published by the Free Software Foundation.
224+#
225+# charm-helpers is distributed in the hope that it will be useful,
226+# but WITHOUT ANY WARRANTY; without even the implied warranty of
227+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
228+# GNU Lesser General Public License for more details.
229+#
230+# You should have received a copy of the GNU Lesser General Public License
231+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
232
233=== added file 'hooks/charmhelpers/contrib/hahelpers/apache.py'
234--- hooks/charmhelpers/contrib/hahelpers/apache.py 1970-01-01 00:00:00 +0000
235+++ hooks/charmhelpers/contrib/hahelpers/apache.py 2016-02-04 15:46:27 +0000
236@@ -0,0 +1,82 @@
237+# Copyright 2014-2015 Canonical Limited.
238+#
239+# This file is part of charm-helpers.
240+#
241+# charm-helpers is free software: you can redistribute it and/or modify
242+# it under the terms of the GNU Lesser General Public License version 3 as
243+# published by the Free Software Foundation.
244+#
245+# charm-helpers is distributed in the hope that it will be useful,
246+# but WITHOUT ANY WARRANTY; without even the implied warranty of
247+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
248+# GNU Lesser General Public License for more details.
249+#
250+# You should have received a copy of the GNU Lesser General Public License
251+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
252+
253+#
254+# Copyright 2012 Canonical Ltd.
255+#
256+# This file is sourced from lp:openstack-charm-helpers
257+#
258+# Authors:
259+# James Page <james.page@ubuntu.com>
260+# Adam Gandelman <adamg@ubuntu.com>
261+#
262+
263+import subprocess
264+
265+from charmhelpers.core.hookenv import (
266+ config as config_get,
267+ relation_get,
268+ relation_ids,
269+ related_units as relation_list,
270+ log,
271+ INFO,
272+)
273+
274+
275+def get_cert(cn=None):
276+ # TODO: deal with multiple https endpoints via charm config
277+ cert = config_get('ssl_cert')
278+ key = config_get('ssl_key')
279+ if not (cert and key):
280+ log("Inspecting identity-service relations for SSL certificate.",
281+ level=INFO)
282+ cert = key = None
283+ if cn:
284+ ssl_cert_attr = 'ssl_cert_{}'.format(cn)
285+ ssl_key_attr = 'ssl_key_{}'.format(cn)
286+ else:
287+ ssl_cert_attr = 'ssl_cert'
288+ ssl_key_attr = 'ssl_key'
289+ for r_id in relation_ids('identity-service'):
290+ for unit in relation_list(r_id):
291+ if not cert:
292+ cert = relation_get(ssl_cert_attr,
293+ rid=r_id, unit=unit)
294+ if not key:
295+ key = relation_get(ssl_key_attr,
296+ rid=r_id, unit=unit)
297+ return (cert, key)
298+
299+
300+def get_ca_cert():
301+ ca_cert = config_get('ssl_ca')
302+ if ca_cert is None:
303+ log("Inspecting identity-service relations for CA SSL certificate.",
304+ level=INFO)
305+ for r_id in relation_ids('identity-service'):
306+ for unit in relation_list(r_id):
307+ if ca_cert is None:
308+ ca_cert = relation_get('ca_cert',
309+ rid=r_id, unit=unit)
310+ return ca_cert
311+
312+
313+def install_ca_cert(ca_cert):
314+ if ca_cert:
315+ with open('/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt',
316+ 'w') as crt:
317+ crt.write(ca_cert)
318+ subprocess.check_call(['update-ca-certificates', '--fresh'])
319
320=== added file 'hooks/charmhelpers/contrib/hahelpers/cluster.py'
321--- hooks/charmhelpers/contrib/hahelpers/cluster.py 1970-01-01 00:00:00 +0000
322+++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2016-02-04 15:46:27 +0000
323@@ -0,0 +1,316 @@
324+# Copyright 2014-2015 Canonical Limited.
325+#
326+# This file is part of charm-helpers.
327+#
328+# charm-helpers is free software: you can redistribute it and/or modify
329+# it under the terms of the GNU Lesser General Public License version 3 as
330+# published by the Free Software Foundation.
331+#
332+# charm-helpers is distributed in the hope that it will be useful,
333+# but WITHOUT ANY WARRANTY; without even the implied warranty of
334+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
335+# GNU Lesser General Public License for more details.
336+#
337+# You should have received a copy of the GNU Lesser General Public License
338+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
339+
340+#
341+# Copyright 2012 Canonical Ltd.
342+#
343+# Authors:
344+# James Page <james.page@ubuntu.com>
345+# Adam Gandelman <adamg@ubuntu.com>
346+#
347+
348+"""
349+Helpers for clustering and determining "cluster leadership" and other
350+clustering-related helpers.
351+"""
352+
353+import subprocess
354+import os
355+
356+from socket import gethostname as get_unit_hostname
357+
358+import six
359+
360+from charmhelpers.core.hookenv import (
361+ log,
362+ relation_ids,
363+ related_units as relation_list,
364+ relation_get,
365+ config as config_get,
366+ INFO,
367+ ERROR,
368+ WARNING,
369+ unit_get,
370+ is_leader as juju_is_leader
371+)
372+from charmhelpers.core.decorators import (
373+ retry_on_exception,
374+)
375+from charmhelpers.core.strutils import (
376+ bool_from_string,
377+)
378+
379+DC_RESOURCE_NAME = 'DC'
380+
381+
382+class HAIncompleteConfig(Exception):
383+ pass
384+
385+
386+class CRMResourceNotFound(Exception):
387+ pass
388+
389+
390+class CRMDCNotFound(Exception):
391+ pass
392+
393+
394+def is_elected_leader(resource):
395+ """
396+ Returns True if the charm executing this is the elected cluster leader.
397+
398+ It relies on two mechanisms to determine leadership:
399+ 1. If juju is sufficiently new and leadership election is supported,
400+ the is_leader command will be used.
401+ 2. If the charm is part of a corosync cluster, call corosync to
402+ determine leadership.
403+ 3. If the charm is not part of a corosync cluster, the leader is
404+ determined as being "the alive unit with the lowest unit numer". In
405+ other words, the oldest surviving unit.
406+ """
407+ try:
408+ return juju_is_leader()
409+ except NotImplementedError:
410+ log('Juju leadership election feature not enabled'
411+ ', using fallback support',
412+ level=WARNING)
413+
414+ if is_clustered():
415+ if not is_crm_leader(resource):
416+ log('Deferring action to CRM leader.', level=INFO)
417+ return False
418+ else:
419+ peers = peer_units()
420+ if peers and not oldest_peer(peers):
421+ log('Deferring action to oldest service unit.', level=INFO)
422+ return False
423+ return True
424+
425+
426+def is_clustered():
427+ for r_id in (relation_ids('ha') or []):
428+ for unit in (relation_list(r_id) or []):
429+ clustered = relation_get('clustered',
430+ rid=r_id,
431+ unit=unit)
432+ if clustered:
433+ return True
434+ return False
435+
436+
437+def is_crm_dc():
438+ """
439+ Determine leadership by querying the pacemaker Designated Controller
440+ """
441+ cmd = ['crm', 'status']
442+ try:
443+ status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
444+ if not isinstance(status, six.text_type):
445+ status = six.text_type(status, "utf-8")
446+ except subprocess.CalledProcessError as ex:
447+ raise CRMDCNotFound(str(ex))
448+
449+ current_dc = ''
450+ for line in status.split('\n'):
451+ if line.startswith('Current DC'):
452+ # Current DC: juju-lytrusty-machine-2 (168108163) - partition with quorum
453+ current_dc = line.split(':')[1].split()[0]
454+ if current_dc == get_unit_hostname():
455+ return True
456+ elif current_dc == 'NONE':
457+ raise CRMDCNotFound('Current DC: NONE')
458+
459+ return False
460+
461+
462+@retry_on_exception(5, base_delay=2,
463+ exc_type=(CRMResourceNotFound, CRMDCNotFound))
464+def is_crm_leader(resource, retry=False):
465+ """
466+ Returns True if the charm calling this is the elected corosync leader,
467+ as returned by calling the external "crm" command.
468+
469+ We allow this operation to be retried to avoid the possibility of getting a
470+ false negative. See LP #1396246 for more info.
471+ """
472+ if resource == DC_RESOURCE_NAME:
473+ return is_crm_dc()
474+ cmd = ['crm', 'resource', 'show', resource]
475+ try:
476+ status = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
477+ if not isinstance(status, six.text_type):
478+ status = six.text_type(status, "utf-8")
479+ except subprocess.CalledProcessError:
480+ status = None
481+
482+ if status and get_unit_hostname() in status:
483+ return True
484+
485+ if status and "resource %s is NOT running" % (resource) in status:
486+ raise CRMResourceNotFound("CRM resource %s not found" % (resource))
487+
488+ return False
489+
490+
491+def is_leader(resource):
492+ log("is_leader is deprecated. Please consider using is_crm_leader "
493+ "instead.", level=WARNING)
494+ return is_crm_leader(resource)
495+
496+
497+def peer_units(peer_relation="cluster"):
498+ peers = []
499+ for r_id in (relation_ids(peer_relation) or []):
500+ for unit in (relation_list(r_id) or []):
501+ peers.append(unit)
502+ return peers
503+
504+
505+def peer_ips(peer_relation='cluster', addr_key='private-address'):
506+ '''Return a dict of peers and their private-address'''
507+ peers = {}
508+ for r_id in relation_ids(peer_relation):
509+ for unit in relation_list(r_id):
510+ peers[unit] = relation_get(addr_key, rid=r_id, unit=unit)
511+ return peers
512+
513+
514+def oldest_peer(peers):
515+ """Determines who the oldest peer is by comparing unit numbers."""
516+ local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1])
517+ for peer in peers:
518+ remote_unit_no = int(peer.split('/')[1])
519+ if remote_unit_no < local_unit_no:
520+ return False
521+ return True
522+
523+
524+def eligible_leader(resource):
525+ log("eligible_leader is deprecated. Please consider using "
526+ "is_elected_leader instead.", level=WARNING)
527+ return is_elected_leader(resource)
528+
529+
530+def https():
531+ '''
532+ Determines whether enough data has been provided in configuration
533+ or relation data to configure HTTPS
534+ .
535+ returns: boolean
536+ '''
537+ use_https = config_get('use-https')
538+ if use_https and bool_from_string(use_https):
539+ return True
540+ if config_get('ssl_cert') and config_get('ssl_key'):
541+ return True
542+ for r_id in relation_ids('identity-service'):
543+ for unit in relation_list(r_id):
544+ # TODO - needs fixing for new helper as ssl_cert/key suffixes with CN
545+ rel_state = [
546+ relation_get('https_keystone', rid=r_id, unit=unit),
547+ relation_get('ca_cert', rid=r_id, unit=unit),
548+ ]
549+ # NOTE: works around (LP: #1203241)
550+ if (None not in rel_state) and ('' not in rel_state):
551+ return True
552+ return False
553+
554+
555+def determine_api_port(public_port, singlenode_mode=False):
556+ '''
557+ Determine correct API server listening port based on
558+ existence of HTTPS reverse proxy and/or haproxy.
559+
560+ public_port: int: standard public port for given service
561+
562+ singlenode_mode: boolean: Shuffle ports when only a single unit is present
563+
564+ returns: int: the correct listening port for the API service
565+ '''
566+ i = 0
567+ if singlenode_mode:
568+ i += 1
569+ elif len(peer_units()) > 0 or is_clustered():
570+ i += 1
571+ if https():
572+ i += 1
573+ return public_port - (i * 10)
574+
575+
576+def determine_apache_port(public_port, singlenode_mode=False):
577+ '''
578+ Description: Determine correct apache listening port based on public IP +
579+ state of the cluster.
580+
581+ public_port: int: standard public port for given service
582+
583+ singlenode_mode: boolean: Shuffle ports when only a single unit is present
584+
585+ returns: int: the correct listening port for the HAProxy service
586+ '''
587+ i = 0
588+ if singlenode_mode:
589+ i += 1
590+ elif len(peer_units()) > 0 or is_clustered():
591+ i += 1
592+ return public_port - (i * 10)
593+
594+
595+def get_hacluster_config(exclude_keys=None):
596+ '''
597+ Obtains all relevant configuration from charm configuration required
598+ for initiating a relation to hacluster:
599+
600+ ha-bindiface, ha-mcastport, vip
601+
602+ param: exclude_keys: list of setting key(s) to be excluded.
603+ returns: dict: A dict containing settings keyed by setting name.
604+ raises: HAIncompleteConfig if settings are missing.
605+ '''
606+ settings = ['ha-bindiface', 'ha-mcastport', 'vip']
607+ conf = {}
608+ for setting in settings:
609+ if exclude_keys and setting in exclude_keys:
610+ continue
611+
612+ conf[setting] = config_get(setting)
613+ missing = []
614+ [missing.append(s) for s, v in six.iteritems(conf) if v is None]
615+ if missing:
616+ log('Insufficient config data to configure hacluster.', level=ERROR)
617+ raise HAIncompleteConfig
618+ return conf
619+
620+
621+def canonical_url(configs, vip_setting='vip'):
622+ '''
623+ Returns the correct HTTP URL to this host given the state of HTTPS
624+ configuration and hacluster.
625+
626+ :configs : OSTemplateRenderer: A config tempating object to inspect for
627+ a complete https context.
628+
629+ :vip_setting: str: Setting in charm config that specifies
630+ VIP address.
631+ '''
632+ scheme = 'http'
633+ if 'https' in configs.complete_contexts():
634+ scheme = 'https'
635+ if is_clustered():
636+ addr = config_get(vip_setting)
637+ else:
638+ addr = unit_get('private-address')
639+ return '%s://%s' % (scheme, addr)
640
641=== added directory 'hooks/charmhelpers/contrib/network'
642=== added file 'hooks/charmhelpers/contrib/network/__init__.py'
643--- hooks/charmhelpers/contrib/network/__init__.py 1970-01-01 00:00:00 +0000
644+++ hooks/charmhelpers/contrib/network/__init__.py 2016-02-04 15:46:27 +0000
645@@ -0,0 +1,15 @@
646+# Copyright 2014-2015 Canonical Limited.
647+#
648+# This file is part of charm-helpers.
649+#
650+# charm-helpers is free software: you can redistribute it and/or modify
651+# it under the terms of the GNU Lesser General Public License version 3 as
652+# published by the Free Software Foundation.
653+#
654+# charm-helpers is distributed in the hope that it will be useful,
655+# but WITHOUT ANY WARRANTY; without even the implied warranty of
656+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
657+# GNU Lesser General Public License for more details.
658+#
659+# You should have received a copy of the GNU Lesser General Public License
660+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
661
662=== added file 'hooks/charmhelpers/contrib/network/ip.py'
663--- hooks/charmhelpers/contrib/network/ip.py 1970-01-01 00:00:00 +0000
664+++ hooks/charmhelpers/contrib/network/ip.py 2016-02-04 15:46:27 +0000
665@@ -0,0 +1,456 @@
666+# Copyright 2014-2015 Canonical Limited.
667+#
668+# This file is part of charm-helpers.
669+#
670+# charm-helpers is free software: you can redistribute it and/or modify
671+# it under the terms of the GNU Lesser General Public License version 3 as
672+# published by the Free Software Foundation.
673+#
674+# charm-helpers is distributed in the hope that it will be useful,
675+# but WITHOUT ANY WARRANTY; without even the implied warranty of
676+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
677+# GNU Lesser General Public License for more details.
678+#
679+# You should have received a copy of the GNU Lesser General Public License
680+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
681+
682+import glob
683+import re
684+import subprocess
685+import six
686+import socket
687+
688+from functools import partial
689+
690+from charmhelpers.core.hookenv import unit_get
691+from charmhelpers.fetch import apt_install, apt_update
692+from charmhelpers.core.hookenv import (
693+ log,
694+ WARNING,
695+)
696+
697+try:
698+ import netifaces
699+except ImportError:
700+ apt_update(fatal=True)
701+ apt_install('python-netifaces', fatal=True)
702+ import netifaces
703+
704+try:
705+ import netaddr
706+except ImportError:
707+ apt_update(fatal=True)
708+ apt_install('python-netaddr', fatal=True)
709+ import netaddr
710+
711+
712+def _validate_cidr(network):
713+ try:
714+ netaddr.IPNetwork(network)
715+ except (netaddr.core.AddrFormatError, ValueError):
716+ raise ValueError("Network (%s) is not in CIDR presentation format" %
717+ network)
718+
719+
720+def no_ip_found_error_out(network):
721+ errmsg = ("No IP address found in network: %s" % network)
722+ raise ValueError(errmsg)
723+
724+
725+def get_address_in_network(network, fallback=None, fatal=False):
726+ """Get an IPv4 or IPv6 address within the network from the host.
727+
728+ :param network (str): CIDR presentation format. For example,
729+ '192.168.1.0/24'.
730+ :param fallback (str): If no address is found, return fallback.
731+ :param fatal (boolean): If no address is found, fallback is not
732+ set and fatal is True then exit(1).
733+ """
734+ if network is None:
735+ if fallback is not None:
736+ return fallback
737+
738+ if fatal:
739+ no_ip_found_error_out(network)
740+ else:
741+ return None
742+
743+ _validate_cidr(network)
744+ network = netaddr.IPNetwork(network)
745+ for iface in netifaces.interfaces():
746+ addresses = netifaces.ifaddresses(iface)
747+ if network.version == 4 and netifaces.AF_INET in addresses:
748+ addr = addresses[netifaces.AF_INET][0]['addr']
749+ netmask = addresses[netifaces.AF_INET][0]['netmask']
750+ cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask))
751+ if cidr in network:
752+ return str(cidr.ip)
753+
754+ if network.version == 6 and netifaces.AF_INET6 in addresses:
755+ for addr in addresses[netifaces.AF_INET6]:
756+ if not addr['addr'].startswith('fe80'):
757+ cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'],
758+ addr['netmask']))
759+ if cidr in network:
760+ return str(cidr.ip)
761+
762+ if fallback is not None:
763+ return fallback
764+
765+ if fatal:
766+ no_ip_found_error_out(network)
767+
768+ return None
769+
770+
771+def is_ipv6(address):
772+ """Determine whether provided address is IPv6 or not."""
773+ try:
774+ address = netaddr.IPAddress(address)
775+ except netaddr.AddrFormatError:
776+ # probably a hostname - so not an address at all!
777+ return False
778+
779+ return address.version == 6
780+
781+
782+def is_address_in_network(network, address):
783+ """
784+ Determine whether the provided address is within a network range.
785+
786+ :param network (str): CIDR presentation format. For example,
787+ '192.168.1.0/24'.
788+ :param address: An individual IPv4 or IPv6 address without a net
789+ mask or subnet prefix. For example, '192.168.1.1'.
790+ :returns boolean: Flag indicating whether address is in network.
791+ """
792+ try:
793+ network = netaddr.IPNetwork(network)
794+ except (netaddr.core.AddrFormatError, ValueError):
795+ raise ValueError("Network (%s) is not in CIDR presentation format" %
796+ network)
797+
798+ try:
799+ address = netaddr.IPAddress(address)
800+ except (netaddr.core.AddrFormatError, ValueError):
801+ raise ValueError("Address (%s) is not in correct presentation format" %
802+ address)
803+
804+ if address in network:
805+ return True
806+ else:
807+ return False
808+
809+
810+def _get_for_address(address, key):
811+ """Retrieve an attribute of or the physical interface that
812+ the IP address provided could be bound to.
813+
814+ :param address (str): An individual IPv4 or IPv6 address without a net
815+ mask or subnet prefix. For example, '192.168.1.1'.
816+ :param key: 'iface' for the physical interface name or an attribute
817+ of the configured interface, for example 'netmask'.
818+ :returns str: Requested attribute or None if address is not bindable.
819+ """
820+ address = netaddr.IPAddress(address)
821+ for iface in netifaces.interfaces():
822+ addresses = netifaces.ifaddresses(iface)
823+ if address.version == 4 and netifaces.AF_INET in addresses:
824+ addr = addresses[netifaces.AF_INET][0]['addr']
825+ netmask = addresses[netifaces.AF_INET][0]['netmask']
826+ network = netaddr.IPNetwork("%s/%s" % (addr, netmask))
827+ cidr = network.cidr
828+ if address in cidr:
829+ if key == 'iface':
830+ return iface
831+ else:
832+ return addresses[netifaces.AF_INET][0][key]
833+
834+ if address.version == 6 and netifaces.AF_INET6 in addresses:
835+ for addr in addresses[netifaces.AF_INET6]:
836+ if not addr['addr'].startswith('fe80'):
837+ network = netaddr.IPNetwork("%s/%s" % (addr['addr'],
838+ addr['netmask']))
839+ cidr = network.cidr
840+ if address in cidr:
841+ if key == 'iface':
842+ return iface
843+ elif key == 'netmask' and cidr:
844+ return str(cidr).split('/')[1]
845+ else:
846+ return addr[key]
847+
848+ return None
849+
850+
851+get_iface_for_address = partial(_get_for_address, key='iface')
852+
853+
854+get_netmask_for_address = partial(_get_for_address, key='netmask')
855+
856+
857+def format_ipv6_addr(address):
858+ """If address is IPv6, wrap it in '[]' otherwise return None.
859+
860+ This is required by most configuration files when specifying IPv6
861+ addresses.
862+ """
863+ if is_ipv6(address):
864+ return "[%s]" % address
865+
866+ return None
867+
868+
869+def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False,
870+ fatal=True, exc_list=None):
871+ """Return the assigned IP address for a given interface, if any."""
872+ # Extract nic if passed /dev/ethX
873+ if '/' in iface:
874+ iface = iface.split('/')[-1]
875+
876+ if not exc_list:
877+ exc_list = []
878+
879+ try:
880+ inet_num = getattr(netifaces, inet_type)
881+ except AttributeError:
882+ raise Exception("Unknown inet type '%s'" % str(inet_type))
883+
884+ interfaces = netifaces.interfaces()
885+ if inc_aliases:
886+ ifaces = []
887+ for _iface in interfaces:
888+ if iface == _iface or _iface.split(':')[0] == iface:
889+ ifaces.append(_iface)
890+
891+ if fatal and not ifaces:
892+ raise Exception("Invalid interface '%s'" % iface)
893+
894+ ifaces.sort()
895+ else:
896+ if iface not in interfaces:
897+ if fatal:
898+ raise Exception("Interface '%s' not found " % (iface))
899+ else:
900+ return []
901+
902+ else:
903+ ifaces = [iface]
904+
905+ addresses = []
906+ for netiface in ifaces:
907+ net_info = netifaces.ifaddresses(netiface)
908+ if inet_num in net_info:
909+ for entry in net_info[inet_num]:
910+ if 'addr' in entry and entry['addr'] not in exc_list:
911+ addresses.append(entry['addr'])
912+
913+ if fatal and not addresses:
914+ raise Exception("Interface '%s' doesn't have any %s addresses." %
915+ (iface, inet_type))
916+
917+ return sorted(addresses)
918+
919+
920+get_ipv4_addr = partial(get_iface_addr, inet_type='AF_INET')
921+
922+
923+def get_iface_from_addr(addr):
924+ """Work out on which interface the provided address is configured."""
925+ for iface in netifaces.interfaces():
926+ addresses = netifaces.ifaddresses(iface)
927+ for inet_type in addresses:
928+ for _addr in addresses[inet_type]:
929+ _addr = _addr['addr']
930+ # link local
931+ ll_key = re.compile("(.+)%.*")
932+ raw = re.match(ll_key, _addr)
933+ if raw:
934+ _addr = raw.group(1)
935+
936+ if _addr == addr:
937+ log("Address '%s' is configured on iface '%s'" %
938+ (addr, iface))
939+ return iface
940+
941+ msg = "Unable to infer net iface on which '%s' is configured" % (addr)
942+ raise Exception(msg)
943+
944+
945+def sniff_iface(f):
946+ """Ensure decorated function is called with a value for iface.
947+
948+ If no iface provided, inject net iface inferred from unit private address.
949+ """
950+ def iface_sniffer(*args, **kwargs):
951+ if not kwargs.get('iface', None):
952+ kwargs['iface'] = get_iface_from_addr(unit_get('private-address'))
953+
954+ return f(*args, **kwargs)
955+
956+ return iface_sniffer
957+
958+
959+@sniff_iface
960+def get_ipv6_addr(iface=None, inc_aliases=False, fatal=True, exc_list=None,
961+ dynamic_only=True):
962+ """Get assigned IPv6 address for a given interface.
963+
964+ Returns list of addresses found. If no address found, returns empty list.
965+
966+ If iface is None, we infer the current primary interface by doing a reverse
967+ lookup on the unit private-address.
968+
969+ We currently only support scope global IPv6 addresses i.e. non-temporary
970+ addresses. If no global IPv6 address is found, return the first one found
971+ in the ipv6 address list.
972+ """
973+ addresses = get_iface_addr(iface=iface, inet_type='AF_INET6',
974+ inc_aliases=inc_aliases, fatal=fatal,
975+ exc_list=exc_list)
976+
977+ if addresses:
978+ global_addrs = []
979+ for addr in addresses:
980+ key_scope_link_local = re.compile("^fe80::..(.+)%(.+)")
981+ m = re.match(key_scope_link_local, addr)
982+ if m:
983+ eui_64_mac = m.group(1)
984+ iface = m.group(2)
985+ else:
986+ global_addrs.append(addr)
987+
988+ if global_addrs:
989+ # Make sure any found global addresses are not temporary
990+ cmd = ['ip', 'addr', 'show', iface]
991+ out = subprocess.check_output(cmd).decode('UTF-8')
992+ if dynamic_only:
993+ key = re.compile("inet6 (.+)/[0-9]+ scope global dynamic.*")
994+ else:
995+ key = re.compile("inet6 (.+)/[0-9]+ scope global.*")
996+
997+ addrs = []
998+ for line in out.split('\n'):
999+ line = line.strip()
1000+ m = re.match(key, line)
1001+ if m and 'temporary' not in line:
1002+ # Return the first valid address we find
1003+ for addr in global_addrs:
1004+ if m.group(1) == addr:
1005+ if not dynamic_only or \
1006+ m.group(1).endswith(eui_64_mac):
1007+ addrs.append(addr)
1008+
1009+ if addrs:
1010+ return addrs
1011+
1012+ if fatal:
1013+ raise Exception("Interface '%s' does not have a scope global "
1014+ "non-temporary ipv6 address." % iface)
1015+
1016+ return []
1017+
1018+
1019+def get_bridges(vnic_dir='/sys/devices/virtual/net'):
1020+ """Return a list of bridges on the system."""
1021+ b_regex = "%s/*/bridge" % vnic_dir
1022+ return [x.replace(vnic_dir, '').split('/')[1] for x in glob.glob(b_regex)]
1023+
1024+
1025+def get_bridge_nics(bridge, vnic_dir='/sys/devices/virtual/net'):
1026+ """Return a list of nics comprising a given bridge on the system."""
1027+ brif_regex = "%s/%s/brif/*" % (vnic_dir, bridge)
1028+ return [x.split('/')[-1] for x in glob.glob(brif_regex)]
1029+
1030+
1031+def is_bridge_member(nic):
1032+ """Check if a given nic is a member of a bridge."""
1033+ for bridge in get_bridges():
1034+ if nic in get_bridge_nics(bridge):
1035+ return True
1036+
1037+ return False
1038+
1039+
1040+def is_ip(address):
1041+ """
1042+ Returns True if address is a valid IP address.
1043+ """
1044+ try:
1045+ # Test to see if already an IPv4 address
1046+ socket.inet_aton(address)
1047+ return True
1048+ except socket.error:
1049+ return False
1050+
1051+
1052+def ns_query(address):
1053+ try:
1054+ import dns.resolver
1055+ except ImportError:
1056+ apt_install('python-dnspython')
1057+ import dns.resolver
1058+
1059+ if isinstance(address, dns.name.Name):
1060+ rtype = 'PTR'
1061+ elif isinstance(address, six.string_types):
1062+ rtype = 'A'
1063+ else:
1064+ return None
1065+
1066+ answers = dns.resolver.query(address, rtype)
1067+ if answers:
1068+ return str(answers[0])
1069+ return None
1070+
1071+
1072+def get_host_ip(hostname, fallback=None):
1073+ """
1074+ Resolves the IP for a given hostname, or returns
1075+ the input if it is already an IP.
1076+ """
1077+ if is_ip(hostname):
1078+ return hostname
1079+
1080+ ip_addr = ns_query(hostname)
1081+ if not ip_addr:
1082+ try:
1083+ ip_addr = socket.gethostbyname(hostname)
1084+ except:
1085+ log("Failed to resolve hostname '%s'" % (hostname),
1086+ level=WARNING)
1087+ return fallback
1088+ return ip_addr
1089+
1090+
1091+def get_hostname(address, fqdn=True):
1092+ """
1093+ Resolves hostname for given IP, or returns the input
1094+ if it is already a hostname.
1095+ """
1096+ if is_ip(address):
1097+ try:
1098+ import dns.reversename
1099+ except ImportError:
1100+ apt_install("python-dnspython")
1101+ import dns.reversename
1102+
1103+ rev = dns.reversename.from_address(address)
1104+ result = ns_query(rev)
1105+
1106+ if not result:
1107+ try:
1108+ result = socket.gethostbyaddr(address)[0]
1109+ except:
1110+ return None
1111+ else:
1112+ result = address
1113+
1114+ if fqdn:
1115+ # strip trailing .
1116+ if result.endswith('.'):
1117+ return result[:-1]
1118+ else:
1119+ return result
1120+ else:
1121+ return result.split('.')[0]
1122
1123=== added directory 'hooks/charmhelpers/contrib/openstack'
1124=== added file 'hooks/charmhelpers/contrib/openstack/__init__.py'
1125--- hooks/charmhelpers/contrib/openstack/__init__.py 1970-01-01 00:00:00 +0000
1126+++ hooks/charmhelpers/contrib/openstack/__init__.py 2016-02-04 15:46:27 +0000
1127@@ -0,0 +1,15 @@
1128+# Copyright 2014-2015 Canonical Limited.
1129+#
1130+# This file is part of charm-helpers.
1131+#
1132+# charm-helpers is free software: you can redistribute it and/or modify
1133+# it under the terms of the GNU Lesser General Public License version 3 as
1134+# published by the Free Software Foundation.
1135+#
1136+# charm-helpers is distributed in the hope that it will be useful,
1137+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1138+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1139+# GNU Lesser General Public License for more details.
1140+#
1141+# You should have received a copy of the GNU Lesser General Public License
1142+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1143
1144=== added file 'hooks/charmhelpers/contrib/openstack/context.py'
1145--- hooks/charmhelpers/contrib/openstack/context.py 1970-01-01 00:00:00 +0000
1146+++ hooks/charmhelpers/contrib/openstack/context.py 2016-02-04 15:46:27 +0000
1147@@ -0,0 +1,1443 @@
1148+# Copyright 2014-2015 Canonical Limited.
1149+#
1150+# This file is part of charm-helpers.
1151+#
1152+# charm-helpers is free software: you can redistribute it and/or modify
1153+# it under the terms of the GNU Lesser General Public License version 3 as
1154+# published by the Free Software Foundation.
1155+#
1156+# charm-helpers is distributed in the hope that it will be useful,
1157+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1158+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1159+# GNU Lesser General Public License for more details.
1160+#
1161+# You should have received a copy of the GNU Lesser General Public License
1162+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1163+
1164+import glob
1165+import json
1166+import os
1167+import re
1168+import time
1169+from base64 import b64decode
1170+from subprocess import check_call
1171+
1172+import six
1173+import yaml
1174+
1175+from charmhelpers.fetch import (
1176+ apt_install,
1177+ filter_installed_packages,
1178+)
1179+from charmhelpers.core.hookenv import (
1180+ config,
1181+ is_relation_made,
1182+ local_unit,
1183+ log,
1184+ relation_get,
1185+ relation_ids,
1186+ related_units,
1187+ relation_set,
1188+ unit_get,
1189+ unit_private_ip,
1190+ charm_name,
1191+ DEBUG,
1192+ INFO,
1193+ WARNING,
1194+ ERROR,
1195+)
1196+
1197+from charmhelpers.core.sysctl import create as sysctl_create
1198+from charmhelpers.core.strutils import bool_from_string
1199+
1200+from charmhelpers.core.host import (
1201+ get_bond_master,
1202+ is_phy_iface,
1203+ list_nics,
1204+ get_nic_hwaddr,
1205+ mkdir,
1206+ write_file,
1207+)
1208+from charmhelpers.contrib.hahelpers.cluster import (
1209+ determine_apache_port,
1210+ determine_api_port,
1211+ https,
1212+ is_clustered,
1213+)
1214+from charmhelpers.contrib.hahelpers.apache import (
1215+ get_cert,
1216+ get_ca_cert,
1217+ install_ca_cert,
1218+)
1219+from charmhelpers.contrib.openstack.neutron import (
1220+ neutron_plugin_attribute,
1221+ parse_data_port_mappings,
1222+)
1223+from charmhelpers.contrib.openstack.ip import (
1224+ resolve_address,
1225+ INTERNAL,
1226+)
1227+from charmhelpers.contrib.network.ip import (
1228+ get_address_in_network,
1229+ get_ipv4_addr,
1230+ get_ipv6_addr,
1231+ get_netmask_for_address,
1232+ format_ipv6_addr,
1233+ is_address_in_network,
1234+ is_bridge_member,
1235+)
1236+from charmhelpers.contrib.openstack.utils import get_host_ip
1237+CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
1238+ADDRESS_TYPES = ['admin', 'internal', 'public']
1239+
1240+
1241+class OSContextError(Exception):
1242+ pass
1243+
1244+
1245+def ensure_packages(packages):
1246+ """Install but do not upgrade required plugin packages."""
1247+ required = filter_installed_packages(packages)
1248+ if required:
1249+ apt_install(required, fatal=True)
1250+
1251+
1252+def context_complete(ctxt):
1253+ _missing = []
1254+ for k, v in six.iteritems(ctxt):
1255+ if v is None or v == '':
1256+ _missing.append(k)
1257+
1258+ if _missing:
1259+ log('Missing required data: %s' % ' '.join(_missing), level=INFO)
1260+ return False
1261+
1262+ return True
1263+
1264+
1265+def config_flags_parser(config_flags):
1266+ """Parses config flags string into dict.
1267+
1268+ This parsing method supports a few different formats for the config
1269+ flag values to be parsed:
1270+
1271+ 1. A string in the simple format of key=value pairs, with the possibility
1272+ of specifying multiple key value pairs within the same string. For
1273+ example, a string in the format of 'key1=value1, key2=value2' will
1274+ return a dict of:
1275+
1276+ {'key1': 'value1',
1277+ 'key2': 'value2'}.
1278+
1279+ 2. A string in the above format, but supporting a comma-delimited list
1280+ of values for the same key. For example, a string in the format of
1281+ 'key1=value1, key2=value3,value4,value5' will return a dict of:
1282+
1283+ {'key1', 'value1',
1284+ 'key2', 'value2,value3,value4'}
1285+
1286+ 3. A string containing a colon character (:) prior to an equal
1287+ character (=) will be treated as yaml and parsed as such. This can be
1288+ used to specify more complex key value pairs. For example,
1289+ a string in the format of 'key1: subkey1=value1, subkey2=value2' will
1290+ return a dict of:
1291+
1292+ {'key1', 'subkey1=value1, subkey2=value2'}
1293+
1294+ The provided config_flags string may be a list of comma-separated values
1295+ which themselves may be comma-separated list of values.
1296+ """
1297+ # If we find a colon before an equals sign then treat it as yaml.
1298+ # Note: limit it to finding the colon first since this indicates assignment
1299+ # for inline yaml.
1300+ colon = config_flags.find(':')
1301+ equals = config_flags.find('=')
1302+ if colon > 0:
1303+ if colon < equals or equals < 0:
1304+ return yaml.safe_load(config_flags)
1305+
1306+ if config_flags.find('==') >= 0:
1307+ log("config_flags is not in expected format (key=value)", level=ERROR)
1308+ raise OSContextError
1309+
1310+ # strip the following from each value.
1311+ post_strippers = ' ,'
1312+ # we strip any leading/trailing '=' or ' ' from the string then
1313+ # split on '='.
1314+ split = config_flags.strip(' =').split('=')
1315+ limit = len(split)
1316+ flags = {}
1317+ for i in range(0, limit - 1):
1318+ current = split[i]
1319+ next = split[i + 1]
1320+ vindex = next.rfind(',')
1321+ if (i == limit - 2) or (vindex < 0):
1322+ value = next
1323+ else:
1324+ value = next[:vindex]
1325+
1326+ if i == 0:
1327+ key = current
1328+ else:
1329+ # if this not the first entry, expect an embedded key.
1330+ index = current.rfind(',')
1331+ if index < 0:
1332+ log("Invalid config value(s) at index %s" % (i), level=ERROR)
1333+ raise OSContextError
1334+ key = current[index + 1:]
1335+
1336+ # Add to collection.
1337+ flags[key.strip(post_strippers)] = value.rstrip(post_strippers)
1338+
1339+ return flags
1340+
1341+
1342+class OSContextGenerator(object):
1343+ """Base class for all context generators."""
1344+ interfaces = []
1345+ related = False
1346+ complete = False
1347+ missing_data = []
1348+
1349+ def __call__(self):
1350+ raise NotImplementedError
1351+
1352+ def context_complete(self, ctxt):
1353+ """Check for missing data for the required context data.
1354+ Set self.missing_data if it exists and return False.
1355+ Set self.complete if no missing data and return True.
1356+ """
1357+ # Fresh start
1358+ self.complete = False
1359+ self.missing_data = []
1360+ for k, v in six.iteritems(ctxt):
1361+ if v is None or v == '':
1362+ if k not in self.missing_data:
1363+ self.missing_data.append(k)
1364+
1365+ if self.missing_data:
1366+ self.complete = False
1367+ log('Missing required data: %s' % ' '.join(self.missing_data), level=INFO)
1368+ else:
1369+ self.complete = True
1370+ return self.complete
1371+
1372+ def get_related(self):
1373+ """Check if any of the context interfaces have relation ids.
1374+ Set self.related and return True if one of the interfaces
1375+ has relation ids.
1376+ """
1377+ # Fresh start
1378+ self.related = False
1379+ try:
1380+ for interface in self.interfaces:
1381+ if relation_ids(interface):
1382+ self.related = True
1383+ return self.related
1384+ except AttributeError as e:
1385+ log("{} {}"
1386+ "".format(self, e), 'INFO')
1387+ return self.related
1388+
1389+
1390+class SharedDBContext(OSContextGenerator):
1391+ interfaces = ['shared-db']
1392+
1393+ def __init__(self,
1394+ database=None, user=None, relation_prefix=None, ssl_dir=None):
1395+ """Allows inspecting relation for settings prefixed with
1396+ relation_prefix. This is useful for parsing access for multiple
1397+ databases returned via the shared-db interface (eg, nova_password,
1398+ quantum_password)
1399+ """
1400+ self.relation_prefix = relation_prefix
1401+ self.database = database
1402+ self.user = user
1403+ self.ssl_dir = ssl_dir
1404+ self.rel_name = self.interfaces[0]
1405+
1406+ def __call__(self):
1407+ self.database = self.database or config('database')
1408+ self.user = self.user or config('database-user')
1409+ if None in [self.database, self.user]:
1410+ log("Could not generate shared_db context. Missing required charm "
1411+ "config options. (database name and user)", level=ERROR)
1412+ raise OSContextError
1413+
1414+ ctxt = {}
1415+
1416+ # NOTE(jamespage) if mysql charm provides a network upon which
1417+ # access to the database should be made, reconfigure relation
1418+ # with the service units local address and defer execution
1419+ access_network = relation_get('access-network')
1420+ if access_network is not None:
1421+ if self.relation_prefix is not None:
1422+ hostname_key = "{}_hostname".format(self.relation_prefix)
1423+ else:
1424+ hostname_key = "hostname"
1425+ access_hostname = get_address_in_network(access_network,
1426+ unit_get('private-address'))
1427+ set_hostname = relation_get(attribute=hostname_key,
1428+ unit=local_unit())
1429+ if set_hostname != access_hostname:
1430+ relation_set(relation_settings={hostname_key: access_hostname})
1431+ return None # Defer any further hook execution for now....
1432+
1433+ password_setting = 'password'
1434+ if self.relation_prefix:
1435+ password_setting = self.relation_prefix + '_password'
1436+
1437+ for rid in relation_ids(self.interfaces[0]):
1438+ self.related = True
1439+ for unit in related_units(rid):
1440+ rdata = relation_get(rid=rid, unit=unit)
1441+ host = rdata.get('db_host')
1442+ host = format_ipv6_addr(host) or host
1443+ ctxt = {
1444+ 'database_host': host,
1445+ 'database': self.database,
1446+ 'database_user': self.user,
1447+ 'database_password': rdata.get(password_setting),
1448+ 'database_type': 'mysql'
1449+ }
1450+ if self.context_complete(ctxt):
1451+ db_ssl(rdata, ctxt, self.ssl_dir)
1452+ return ctxt
1453+ return {}
1454+
1455+
1456+class PostgresqlDBContext(OSContextGenerator):
1457+ interfaces = ['pgsql-db']
1458+
1459+ def __init__(self, database=None):
1460+ self.database = database
1461+
1462+ def __call__(self):
1463+ self.database = self.database or config('database')
1464+ if self.database is None:
1465+ log('Could not generate postgresql_db context. Missing required '
1466+ 'charm config options. (database name)', level=ERROR)
1467+ raise OSContextError
1468+
1469+ ctxt = {}
1470+ for rid in relation_ids(self.interfaces[0]):
1471+ self.related = True
1472+ for unit in related_units(rid):
1473+ rel_host = relation_get('host', rid=rid, unit=unit)
1474+ rel_user = relation_get('user', rid=rid, unit=unit)
1475+ rel_passwd = relation_get('password', rid=rid, unit=unit)
1476+ ctxt = {'database_host': rel_host,
1477+ 'database': self.database,
1478+ 'database_user': rel_user,
1479+ 'database_password': rel_passwd,
1480+ 'database_type': 'postgresql'}
1481+ if self.context_complete(ctxt):
1482+ return ctxt
1483+
1484+ return {}
1485+
1486+
1487+def db_ssl(rdata, ctxt, ssl_dir):
1488+ if 'ssl_ca' in rdata and ssl_dir:
1489+ ca_path = os.path.join(ssl_dir, 'db-client.ca')
1490+ with open(ca_path, 'w') as fh:
1491+ fh.write(b64decode(rdata['ssl_ca']))
1492+
1493+ ctxt['database_ssl_ca'] = ca_path
1494+ elif 'ssl_ca' in rdata:
1495+ log("Charm not setup for ssl support but ssl ca found", level=INFO)
1496+ return ctxt
1497+
1498+ if 'ssl_cert' in rdata:
1499+ cert_path = os.path.join(
1500+ ssl_dir, 'db-client.cert')
1501+ if not os.path.exists(cert_path):
1502+ log("Waiting 1m for ssl client cert validity", level=INFO)
1503+ time.sleep(60)
1504+
1505+ with open(cert_path, 'w') as fh:
1506+ fh.write(b64decode(rdata['ssl_cert']))
1507+
1508+ ctxt['database_ssl_cert'] = cert_path
1509+ key_path = os.path.join(ssl_dir, 'db-client.key')
1510+ with open(key_path, 'w') as fh:
1511+ fh.write(b64decode(rdata['ssl_key']))
1512+
1513+ ctxt['database_ssl_key'] = key_path
1514+
1515+ return ctxt
1516+
1517+
1518+class IdentityServiceContext(OSContextGenerator):
1519+
1520+ def __init__(self, service=None, service_user=None, rel_name='identity-service'):
1521+ self.service = service
1522+ self.service_user = service_user
1523+ self.rel_name = rel_name
1524+ self.interfaces = [self.rel_name]
1525+
1526+ def __call__(self):
1527+ log('Generating template context for ' + self.rel_name, level=DEBUG)
1528+ ctxt = {}
1529+
1530+ if self.service and self.service_user:
1531+ # This is required for pki token signing if we don't want /tmp to
1532+ # be used.
1533+ cachedir = '/var/cache/%s' % (self.service)
1534+ if not os.path.isdir(cachedir):
1535+ log("Creating service cache dir %s" % (cachedir), level=DEBUG)
1536+ mkdir(path=cachedir, owner=self.service_user,
1537+ group=self.service_user, perms=0o700)
1538+
1539+ ctxt['signing_dir'] = cachedir
1540+
1541+ for rid in relation_ids(self.rel_name):
1542+ self.related = True
1543+ for unit in related_units(rid):
1544+ rdata = relation_get(rid=rid, unit=unit)
1545+ serv_host = rdata.get('service_host')
1546+ serv_host = format_ipv6_addr(serv_host) or serv_host
1547+ auth_host = rdata.get('auth_host')
1548+ auth_host = format_ipv6_addr(auth_host) or auth_host
1549+ svc_protocol = rdata.get('service_protocol') or 'http'
1550+ auth_protocol = rdata.get('auth_protocol') or 'http'
1551+ ctxt.update({'service_port': rdata.get('service_port'),
1552+ 'service_host': serv_host,
1553+ 'auth_host': auth_host,
1554+ 'auth_port': rdata.get('auth_port'),
1555+ 'admin_tenant_name': rdata.get('service_tenant'),
1556+ 'admin_user': rdata.get('service_username'),
1557+ 'admin_password': rdata.get('service_password'),
1558+ 'service_protocol': svc_protocol,
1559+ 'auth_protocol': auth_protocol})
1560+
1561+ if self.context_complete(ctxt):
1562+ # NOTE(jamespage) this is required for >= icehouse
1563+ # so a missing value just indicates keystone needs
1564+ # upgrading
1565+ ctxt['admin_tenant_id'] = rdata.get('service_tenant_id')
1566+ return ctxt
1567+
1568+ return {}
1569+
1570+
1571+class AMQPContext(OSContextGenerator):
1572+
1573+ def __init__(self, ssl_dir=None, rel_name='amqp', relation_prefix=None):
1574+ self.ssl_dir = ssl_dir
1575+ self.rel_name = rel_name
1576+ self.relation_prefix = relation_prefix
1577+ self.interfaces = [rel_name]
1578+
1579+ def __call__(self):
1580+ log('Generating template context for amqp', level=DEBUG)
1581+ conf = config()
1582+ if self.relation_prefix:
1583+ user_setting = '%s-rabbit-user' % (self.relation_prefix)
1584+ vhost_setting = '%s-rabbit-vhost' % (self.relation_prefix)
1585+ else:
1586+ user_setting = 'rabbit-user'
1587+ vhost_setting = 'rabbit-vhost'
1588+
1589+ try:
1590+ username = conf[user_setting]
1591+ vhost = conf[vhost_setting]
1592+ except KeyError as e:
1593+ log('Could not generate shared_db context. Missing required charm '
1594+ 'config options: %s.' % e, level=ERROR)
1595+ raise OSContextError
1596+
1597+ ctxt = {}
1598+ for rid in relation_ids(self.rel_name):
1599+ ha_vip_only = False
1600+ self.related = True
1601+ for unit in related_units(rid):
1602+ if relation_get('clustered', rid=rid, unit=unit):
1603+ ctxt['clustered'] = True
1604+ vip = relation_get('vip', rid=rid, unit=unit)
1605+ vip = format_ipv6_addr(vip) or vip
1606+ ctxt['rabbitmq_host'] = vip
1607+ else:
1608+ host = relation_get('private-address', rid=rid, unit=unit)
1609+ host = format_ipv6_addr(host) or host
1610+ ctxt['rabbitmq_host'] = host
1611+
1612+ ctxt.update({
1613+ 'rabbitmq_user': username,
1614+ 'rabbitmq_password': relation_get('password', rid=rid,
1615+ unit=unit),
1616+ 'rabbitmq_virtual_host': vhost,
1617+ })
1618+
1619+ ssl_port = relation_get('ssl_port', rid=rid, unit=unit)
1620+ if ssl_port:
1621+ ctxt['rabbit_ssl_port'] = ssl_port
1622+
1623+ ssl_ca = relation_get('ssl_ca', rid=rid, unit=unit)
1624+ if ssl_ca:
1625+ ctxt['rabbit_ssl_ca'] = ssl_ca
1626+
1627+ if relation_get('ha_queues', rid=rid, unit=unit) is not None:
1628+ ctxt['rabbitmq_ha_queues'] = True
1629+
1630+ ha_vip_only = relation_get('ha-vip-only',
1631+ rid=rid, unit=unit) is not None
1632+
1633+ if self.context_complete(ctxt):
1634+ if 'rabbit_ssl_ca' in ctxt:
1635+ if not self.ssl_dir:
1636+ log("Charm not setup for ssl support but ssl ca "
1637+ "found", level=INFO)
1638+ break
1639+
1640+ ca_path = os.path.join(
1641+ self.ssl_dir, 'rabbit-client-ca.pem')
1642+ with open(ca_path, 'w') as fh:
1643+ fh.write(b64decode(ctxt['rabbit_ssl_ca']))
1644+ ctxt['rabbit_ssl_ca'] = ca_path
1645+
1646+ # Sufficient information found = break out!
1647+ break
1648+
1649+ # Used for active/active rabbitmq >= grizzly
1650+ if (('clustered' not in ctxt or ha_vip_only) and
1651+ len(related_units(rid)) > 1):
1652+ rabbitmq_hosts = []
1653+ for unit in related_units(rid):
1654+ host = relation_get('private-address', rid=rid, unit=unit)
1655+ host = format_ipv6_addr(host) or host
1656+ rabbitmq_hosts.append(host)
1657+
1658+ ctxt['rabbitmq_hosts'] = ','.join(sorted(rabbitmq_hosts))
1659+
1660+ oslo_messaging_flags = conf.get('oslo-messaging-flags', None)
1661+ if oslo_messaging_flags:
1662+ ctxt['oslo_messaging_flags'] = config_flags_parser(
1663+ oslo_messaging_flags)
1664+
1665+ if not self.complete:
1666+ return {}
1667+
1668+ return ctxt
1669+
1670+
1671+class CephContext(OSContextGenerator):
1672+ """Generates context for /etc/ceph/ceph.conf templates."""
1673+ interfaces = ['ceph']
1674+
1675+ def __call__(self):
1676+ if not relation_ids('ceph'):
1677+ return {}
1678+
1679+ log('Generating template context for ceph', level=DEBUG)
1680+ mon_hosts = []
1681+ ctxt = {
1682+ 'use_syslog': str(config('use-syslog')).lower()
1683+ }
1684+ for rid in relation_ids('ceph'):
1685+ for unit in related_units(rid):
1686+ if not ctxt.get('auth'):
1687+ ctxt['auth'] = relation_get('auth', rid=rid, unit=unit)
1688+ if not ctxt.get('key'):
1689+ ctxt['key'] = relation_get('key', rid=rid, unit=unit)
1690+ ceph_pub_addr = relation_get('ceph-public-address', rid=rid,
1691+ unit=unit)
1692+ unit_priv_addr = relation_get('private-address', rid=rid,
1693+ unit=unit)
1694+ ceph_addr = ceph_pub_addr or unit_priv_addr
1695+ ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr
1696+ mon_hosts.append(ceph_addr)
1697+
1698+ ctxt['mon_hosts'] = ' '.join(sorted(mon_hosts))
1699+
1700+ if not os.path.isdir('/etc/ceph'):
1701+ os.mkdir('/etc/ceph')
1702+
1703+ if not self.context_complete(ctxt):
1704+ return {}
1705+
1706+ ensure_packages(['ceph-common'])
1707+ return ctxt
1708+
1709+
1710+class HAProxyContext(OSContextGenerator):
1711+ """Provides half a context for the haproxy template, which describes
1712+ all peers to be included in the cluster. Each charm needs to include
1713+ its own context generator that describes the port mapping.
1714+ """
1715+ interfaces = ['cluster']
1716+
1717+ def __init__(self, singlenode_mode=False):
1718+ self.singlenode_mode = singlenode_mode
1719+
1720+ def __call__(self):
1721+ if not relation_ids('cluster') and not self.singlenode_mode:
1722+ return {}
1723+
1724+ if config('prefer-ipv6'):
1725+ addr = get_ipv6_addr(exc_list=[config('vip')])[0]
1726+ else:
1727+ addr = get_host_ip(unit_get('private-address'))
1728+
1729+ l_unit = local_unit().replace('/', '-')
1730+ cluster_hosts = {}
1731+
1732+ # NOTE(jamespage): build out map of configured network endpoints
1733+ # and associated backends
1734+ for addr_type in ADDRESS_TYPES:
1735+ cfg_opt = 'os-{}-network'.format(addr_type)
1736+ laddr = get_address_in_network(config(cfg_opt))
1737+ if laddr:
1738+ netmask = get_netmask_for_address(laddr)
1739+ cluster_hosts[laddr] = {'network': "{}/{}".format(laddr,
1740+ netmask),
1741+ 'backends': {l_unit: laddr}}
1742+ for rid in relation_ids('cluster'):
1743+ for unit in related_units(rid):
1744+ _laddr = relation_get('{}-address'.format(addr_type),
1745+ rid=rid, unit=unit)
1746+ if _laddr:
1747+ _unit = unit.replace('/', '-')
1748+ cluster_hosts[laddr]['backends'][_unit] = _laddr
1749+
1750+ # NOTE(jamespage) add backend based on private address - this
1751+ # with either be the only backend or the fallback if no acls
1752+ # match in the frontend
1753+ cluster_hosts[addr] = {}
1754+ netmask = get_netmask_for_address(addr)
1755+ cluster_hosts[addr] = {'network': "{}/{}".format(addr, netmask),
1756+ 'backends': {l_unit: addr}}
1757+ for rid in relation_ids('cluster'):
1758+ for unit in related_units(rid):
1759+ _laddr = relation_get('private-address',
1760+ rid=rid, unit=unit)
1761+ if _laddr:
1762+ _unit = unit.replace('/', '-')
1763+ cluster_hosts[addr]['backends'][_unit] = _laddr
1764+
1765+ ctxt = {
1766+ 'frontends': cluster_hosts,
1767+ 'default_backend': addr
1768+ }
1769+
1770+ if config('haproxy-server-timeout'):
1771+ ctxt['haproxy_server_timeout'] = config('haproxy-server-timeout')
1772+
1773+ if config('haproxy-client-timeout'):
1774+ ctxt['haproxy_client_timeout'] = config('haproxy-client-timeout')
1775+
1776+ if config('prefer-ipv6'):
1777+ ctxt['ipv6'] = True
1778+ ctxt['local_host'] = 'ip6-localhost'
1779+ ctxt['haproxy_host'] = '::'
1780+ ctxt['stat_port'] = ':::8888'
1781+ else:
1782+ ctxt['local_host'] = '127.0.0.1'
1783+ ctxt['haproxy_host'] = '0.0.0.0'
1784+ ctxt['stat_port'] = ':8888'
1785+
1786+ for frontend in cluster_hosts:
1787+ if (len(cluster_hosts[frontend]['backends']) > 1 or
1788+ self.singlenode_mode):
1789+ # Enable haproxy when we have enough peers.
1790+ log('Ensuring haproxy enabled in /etc/default/haproxy.',
1791+ level=DEBUG)
1792+ with open('/etc/default/haproxy', 'w') as out:
1793+ out.write('ENABLED=1\n')
1794+
1795+ return ctxt
1796+
1797+ log('HAProxy context is incomplete, this unit has no peers.',
1798+ level=INFO)
1799+ return {}
1800+
1801+
1802+class ImageServiceContext(OSContextGenerator):
1803+ interfaces = ['image-service']
1804+
1805+ def __call__(self):
1806+ """Obtains the glance API server from the image-service relation.
1807+ Useful in nova and cinder (currently).
1808+ """
1809+ log('Generating template context for image-service.', level=DEBUG)
1810+ rids = relation_ids('image-service')
1811+ if not rids:
1812+ return {}
1813+
1814+ for rid in rids:
1815+ for unit in related_units(rid):
1816+ api_server = relation_get('glance-api-server',
1817+ rid=rid, unit=unit)
1818+ if api_server:
1819+ return {'glance_api_servers': api_server}
1820+
1821+ log("ImageService context is incomplete. Missing required relation "
1822+ "data.", level=INFO)
1823+ return {}
1824+
1825+
1826+class ApacheSSLContext(OSContextGenerator):
1827+ """Generates a context for an apache vhost configuration that configures
1828+ HTTPS reverse proxying for one or many endpoints. Generated context
1829+ looks something like::
1830+
1831+ {
1832+ 'namespace': 'cinder',
1833+ 'private_address': 'iscsi.mycinderhost.com',
1834+ 'endpoints': [(8776, 8766), (8777, 8767)]
1835+ }
1836+
1837+ The endpoints list consists of a tuples mapping external ports
1838+ to internal ports.
1839+ """
1840+ interfaces = ['https']
1841+
1842+ # charms should inherit this context and set external ports
1843+ # and service namespace accordingly.
1844+ external_ports = []
1845+ service_namespace = None
1846+
1847+ def enable_modules(self):
1848+ cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http']
1849+ check_call(cmd)
1850+
1851+ def configure_cert(self, cn=None):
1852+ ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
1853+ mkdir(path=ssl_dir)
1854+ cert, key = get_cert(cn)
1855+ if cn:
1856+ cert_filename = 'cert_{}'.format(cn)
1857+ key_filename = 'key_{}'.format(cn)
1858+ else:
1859+ cert_filename = 'cert'
1860+ key_filename = 'key'
1861+
1862+ write_file(path=os.path.join(ssl_dir, cert_filename),
1863+ content=b64decode(cert))
1864+ write_file(path=os.path.join(ssl_dir, key_filename),
1865+ content=b64decode(key))
1866+
1867+ def configure_ca(self):
1868+ ca_cert = get_ca_cert()
1869+ if ca_cert:
1870+ install_ca_cert(b64decode(ca_cert))
1871+
1872+ def canonical_names(self):
1873+ """Figure out which canonical names clients will access this service.
1874+ """
1875+ cns = []
1876+ for r_id in relation_ids('identity-service'):
1877+ for unit in related_units(r_id):
1878+ rdata = relation_get(rid=r_id, unit=unit)
1879+ for k in rdata:
1880+ if k.startswith('ssl_key_'):
1881+ cns.append(k.lstrip('ssl_key_'))
1882+
1883+ return sorted(list(set(cns)))
1884+
1885+ def get_network_addresses(self):
1886+ """For each network configured, return corresponding address and vip
1887+ (if available).
1888+
1889+ Returns a list of tuples of the form:
1890+
1891+ [(address_in_net_a, vip_in_net_a),
1892+ (address_in_net_b, vip_in_net_b),
1893+ ...]
1894+
1895+ or, if no vip(s) available:
1896+
1897+ [(address_in_net_a, address_in_net_a),
1898+ (address_in_net_b, address_in_net_b),
1899+ ...]
1900+ """
1901+ addresses = []
1902+ if config('vip'):
1903+ vips = config('vip').split()
1904+ else:
1905+ vips = []
1906+
1907+ for net_type in ['os-internal-network', 'os-admin-network',
1908+ 'os-public-network']:
1909+ addr = get_address_in_network(config(net_type),
1910+ unit_get('private-address'))
1911+ if len(vips) > 1 and is_clustered():
1912+ if not config(net_type):
1913+ log("Multiple networks configured but net_type "
1914+ "is None (%s)." % net_type, level=WARNING)
1915+ continue
1916+
1917+ for vip in vips:
1918+ if is_address_in_network(config(net_type), vip):
1919+ addresses.append((addr, vip))
1920+ break
1921+
1922+ elif is_clustered() and config('vip'):
1923+ addresses.append((addr, config('vip')))
1924+ else:
1925+ addresses.append((addr, addr))
1926+
1927+ return sorted(addresses)
1928+
1929+ def __call__(self):
1930+ if isinstance(self.external_ports, six.string_types):
1931+ self.external_ports = [self.external_ports]
1932+
1933+ if not self.external_ports or not https():
1934+ return {}
1935+
1936+ self.configure_ca()
1937+ self.enable_modules()
1938+
1939+ ctxt = {'namespace': self.service_namespace,
1940+ 'endpoints': [],
1941+ 'ext_ports': []}
1942+
1943+ cns = self.canonical_names()
1944+ if cns:
1945+ for cn in cns:
1946+ self.configure_cert(cn)
1947+ else:
1948+ # Expect cert/key provided in config (currently assumed that ca
1949+ # uses ip for cn)
1950+ cn = resolve_address(endpoint_type=INTERNAL)
1951+ self.configure_cert(cn)
1952+
1953+ addresses = self.get_network_addresses()
1954+ for address, endpoint in sorted(set(addresses)):
1955+ for api_port in self.external_ports:
1956+ ext_port = determine_apache_port(api_port,
1957+ singlenode_mode=True)
1958+ int_port = determine_api_port(api_port, singlenode_mode=True)
1959+ portmap = (address, endpoint, int(ext_port), int(int_port))
1960+ ctxt['endpoints'].append(portmap)
1961+ ctxt['ext_ports'].append(int(ext_port))
1962+
1963+ ctxt['ext_ports'] = sorted(list(set(ctxt['ext_ports'])))
1964+ return ctxt
1965+
1966+
1967+class NeutronContext(OSContextGenerator):
1968+ interfaces = []
1969+
1970+ @property
1971+ def plugin(self):
1972+ return None
1973+
1974+ @property
1975+ def network_manager(self):
1976+ return None
1977+
1978+ @property
1979+ def packages(self):
1980+ return neutron_plugin_attribute(self.plugin, 'packages',
1981+ self.network_manager)
1982+
1983+ @property
1984+ def neutron_security_groups(self):
1985+ return None
1986+
1987+ def _ensure_packages(self):
1988+ for pkgs in self.packages:
1989+ ensure_packages(pkgs)
1990+
1991+ def _save_flag_file(self):
1992+ if self.network_manager == 'quantum':
1993+ _file = '/etc/nova/quantum_plugin.conf'
1994+ else:
1995+ _file = '/etc/nova/neutron_plugin.conf'
1996+
1997+ with open(_file, 'wb') as out:
1998+ out.write(self.plugin + '\n')
1999+
2000+ def ovs_ctxt(self):
2001+ driver = neutron_plugin_attribute(self.plugin, 'driver',
2002+ self.network_manager)
2003+ config = neutron_plugin_attribute(self.plugin, 'config',
2004+ self.network_manager)
2005+ ovs_ctxt = {'core_plugin': driver,
2006+ 'neutron_plugin': 'ovs',
2007+ 'neutron_security_groups': self.neutron_security_groups,
2008+ 'local_ip': unit_private_ip(),
2009+ 'config': config}
2010+
2011+ return ovs_ctxt
2012+
2013+ def nuage_ctxt(self):
2014+ driver = neutron_plugin_attribute(self.plugin, 'driver',
2015+ self.network_manager)
2016+ config = neutron_plugin_attribute(self.plugin, 'config',
2017+ self.network_manager)
2018+ nuage_ctxt = {'core_plugin': driver,
2019+ 'neutron_plugin': 'vsp',
2020+ 'neutron_security_groups': self.neutron_security_groups,
2021+ 'local_ip': unit_private_ip(),
2022+ 'config': config}
2023+
2024+ return nuage_ctxt
2025+
2026+ def nvp_ctxt(self):
2027+ driver = neutron_plugin_attribute(self.plugin, 'driver',
2028+ self.network_manager)
2029+ config = neutron_plugin_attribute(self.plugin, 'config',
2030+ self.network_manager)
2031+ nvp_ctxt = {'core_plugin': driver,
2032+ 'neutron_plugin': 'nvp',
2033+ 'neutron_security_groups': self.neutron_security_groups,
2034+ 'local_ip': unit_private_ip(),
2035+ 'config': config}
2036+
2037+ return nvp_ctxt
2038+
2039+ def n1kv_ctxt(self):
2040+ driver = neutron_plugin_attribute(self.plugin, 'driver',
2041+ self.network_manager)
2042+ n1kv_config = neutron_plugin_attribute(self.plugin, 'config',
2043+ self.network_manager)
2044+ n1kv_user_config_flags = config('n1kv-config-flags')
2045+ restrict_policy_profiles = config('n1kv-restrict-policy-profiles')
2046+ n1kv_ctxt = {'core_plugin': driver,
2047+ 'neutron_plugin': 'n1kv',
2048+ 'neutron_security_groups': self.neutron_security_groups,
2049+ 'local_ip': unit_private_ip(),
2050+ 'config': n1kv_config,
2051+ 'vsm_ip': config('n1kv-vsm-ip'),
2052+ 'vsm_username': config('n1kv-vsm-username'),
2053+ 'vsm_password': config('n1kv-vsm-password'),
2054+ 'restrict_policy_profiles': restrict_policy_profiles}
2055+
2056+ if n1kv_user_config_flags:
2057+ flags = config_flags_parser(n1kv_user_config_flags)
2058+ n1kv_ctxt['user_config_flags'] = flags
2059+
2060+ return n1kv_ctxt
2061+
2062+ def calico_ctxt(self):
2063+ driver = neutron_plugin_attribute(self.plugin, 'driver',
2064+ self.network_manager)
2065+ config = neutron_plugin_attribute(self.plugin, 'config',
2066+ self.network_manager)
2067+ calico_ctxt = {'core_plugin': driver,
2068+ 'neutron_plugin': 'Calico',
2069+ 'neutron_security_groups': self.neutron_security_groups,
2070+ 'local_ip': unit_private_ip(),
2071+ 'config': config}
2072+
2073+ return calico_ctxt
2074+
2075+ def neutron_ctxt(self):
2076+ if https():
2077+ proto = 'https'
2078+ else:
2079+ proto = 'http'
2080+
2081+ if is_clustered():
2082+ host = config('vip')
2083+ else:
2084+ host = unit_get('private-address')
2085+
2086+ ctxt = {'network_manager': self.network_manager,
2087+ 'neutron_url': '%s://%s:%s' % (proto, host, '9696')}
2088+ return ctxt
2089+
2090+ def pg_ctxt(self):
2091+ driver = neutron_plugin_attribute(self.plugin, 'driver',
2092+ self.network_manager)
2093+ config = neutron_plugin_attribute(self.plugin, 'config',
2094+ self.network_manager)
2095+ ovs_ctxt = {'core_plugin': driver,
2096+ 'neutron_plugin': 'plumgrid',
2097+ 'neutron_security_groups': self.neutron_security_groups,
2098+ 'local_ip': unit_private_ip(),
2099+ 'config': config}
2100+ return ovs_ctxt
2101+
2102+ def midonet_ctxt(self):
2103+ driver = neutron_plugin_attribute(self.plugin, 'driver',
2104+ self.network_manager)
2105+ midonet_config = neutron_plugin_attribute(self.plugin, 'config',
2106+ self.network_manager)
2107+ mido_ctxt = {'core_plugin': driver,
2108+ 'neutron_plugin': 'midonet',
2109+ 'neutron_security_groups': self.neutron_security_groups,
2110+ 'local_ip': unit_private_ip(),
2111+ 'config': midonet_config}
2112+
2113+ return mido_ctxt
2114+
2115+ def __call__(self):
2116+ if self.network_manager not in ['quantum', 'neutron']:
2117+ return {}
2118+
2119+ if not self.plugin:
2120+ return {}
2121+
2122+ ctxt = self.neutron_ctxt()
2123+
2124+ if self.plugin == 'ovs':
2125+ ctxt.update(self.ovs_ctxt())
2126+ elif self.plugin in ['nvp', 'nsx']:
2127+ ctxt.update(self.nvp_ctxt())
2128+ elif self.plugin == 'n1kv':
2129+ ctxt.update(self.n1kv_ctxt())
2130+ elif self.plugin == 'Calico':
2131+ ctxt.update(self.calico_ctxt())
2132+ elif self.plugin == 'vsp':
2133+ ctxt.update(self.nuage_ctxt())
2134+ elif self.plugin == 'plumgrid':
2135+ ctxt.update(self.pg_ctxt())
2136+ elif self.plugin == 'midonet':
2137+ ctxt.update(self.midonet_ctxt())
2138+
2139+ alchemy_flags = config('neutron-alchemy-flags')
2140+ if alchemy_flags:
2141+ flags = config_flags_parser(alchemy_flags)
2142+ ctxt['neutron_alchemy_flags'] = flags
2143+
2144+ self._save_flag_file()
2145+ return ctxt
2146+
2147+
2148+class NeutronPortContext(OSContextGenerator):
2149+
2150+ def resolve_ports(self, ports):
2151+ """Resolve NICs not yet bound to bridge(s)
2152+
2153+ If hwaddress provided then returns resolved hwaddress otherwise NIC.
2154+ """
2155+ if not ports:
2156+ return None
2157+
2158+ hwaddr_to_nic = {}
2159+ hwaddr_to_ip = {}
2160+ for nic in list_nics():
2161+ # Ignore virtual interfaces (bond masters will be identified from
2162+ # their slaves)
2163+ if not is_phy_iface(nic):
2164+ continue
2165+
2166+ _nic = get_bond_master(nic)
2167+ if _nic:
2168+ log("Replacing iface '%s' with bond master '%s'" % (nic, _nic),
2169+ level=DEBUG)
2170+ nic = _nic
2171+
2172+ hwaddr = get_nic_hwaddr(nic)
2173+ hwaddr_to_nic[hwaddr] = nic
2174+ addresses = get_ipv4_addr(nic, fatal=False)
2175+ addresses += get_ipv6_addr(iface=nic, fatal=False)
2176+ hwaddr_to_ip[hwaddr] = addresses
2177+
2178+ resolved = []
2179+ mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I)
2180+ for entry in ports:
2181+ if re.match(mac_regex, entry):
2182+ # NIC is in known NICs and does NOT hace an IP address
2183+ if entry in hwaddr_to_nic and not hwaddr_to_ip[entry]:
2184+ # If the nic is part of a bridge then don't use it
2185+ if is_bridge_member(hwaddr_to_nic[entry]):
2186+ continue
2187+
2188+ # Entry is a MAC address for a valid interface that doesn't
2189+ # have an IP address assigned yet.
2190+ resolved.append(hwaddr_to_nic[entry])
2191+ else:
2192+ # If the passed entry is not a MAC address, assume it's a valid
2193+ # interface, and that the user put it there on purpose (we can
2194+ # trust it to be the real external network).
2195+ resolved.append(entry)
2196+
2197+ # Ensure no duplicates
2198+ return list(set(resolved))
2199+
2200+
2201+class OSConfigFlagContext(OSContextGenerator):
2202+ """Provides support for user-defined config flags.
2203+
2204+ Users can define a comma-seperated list of key=value pairs
2205+ in the charm configuration and apply them at any point in
2206+ any file by using a template flag.
2207+
2208+ Sometimes users might want config flags inserted within a
2209+ specific section so this class allows users to specify the
2210+ template flag name, allowing for multiple template flags
2211+ (sections) within the same context.
2212+
2213+ NOTE: the value of config-flags may be a comma-separated list of
2214+ key=value pairs and some Openstack config files support
2215+ comma-separated lists as values.
2216+ """
2217+
2218+ def __init__(self, charm_flag='config-flags',
2219+ template_flag='user_config_flags'):
2220+ """
2221+ :param charm_flag: config flags in charm configuration.
2222+ :param template_flag: insert point for user-defined flags in template
2223+ file.
2224+ """
2225+ super(OSConfigFlagContext, self).__init__()
2226+ self._charm_flag = charm_flag
2227+ self._template_flag = template_flag
2228+
2229+ def __call__(self):
2230+ config_flags = config(self._charm_flag)
2231+ if not config_flags:
2232+ return {}
2233+
2234+ return {self._template_flag:
2235+ config_flags_parser(config_flags)}
2236+
2237+
2238+class SubordinateConfigContext(OSContextGenerator):
2239+
2240+ """
2241+ Responsible for inspecting relations to subordinates that
2242+ may be exporting required config via a json blob.
2243+
2244+ The subordinate interface allows subordinates to export their
2245+ configuration requirements to the principle for multiple config
2246+ files and multiple serivces. Ie, a subordinate that has interfaces
2247+ to both glance and nova may export to following yaml blob as json::
2248+
2249+ glance:
2250+ /etc/glance/glance-api.conf:
2251+ sections:
2252+ DEFAULT:
2253+ - [key1, value1]
2254+ /etc/glance/glance-registry.conf:
2255+ MYSECTION:
2256+ - [key2, value2]
2257+ nova:
2258+ /etc/nova/nova.conf:
2259+ sections:
2260+ DEFAULT:
2261+ - [key3, value3]
2262+
2263+
2264+ It is then up to the principle charms to subscribe this context to
2265+ the service+config file it is interestd in. Configuration data will
2266+ be available in the template context, in glance's case, as::
2267+
2268+ ctxt = {
2269+ ... other context ...
2270+ 'subordinate_configuration': {
2271+ 'DEFAULT': {
2272+ 'key1': 'value1',
2273+ },
2274+ 'MYSECTION': {
2275+ 'key2': 'value2',
2276+ },
2277+ }
2278+ }
2279+ """
2280+
2281+ def __init__(self, service, config_file, interface):
2282+ """
2283+ :param service : Service name key to query in any subordinate
2284+ data found
2285+ :param config_file : Service's config file to query sections
2286+ :param interface : Subordinate interface to inspect
2287+ """
2288+ self.config_file = config_file
2289+ if isinstance(service, list):
2290+ self.services = service
2291+ else:
2292+ self.services = [service]
2293+ if isinstance(interface, list):
2294+ self.interfaces = interface
2295+ else:
2296+ self.interfaces = [interface]
2297+
2298+ def __call__(self):
2299+ ctxt = {'sections': {}}
2300+ rids = []
2301+ for interface in self.interfaces:
2302+ rids.extend(relation_ids(interface))
2303+ for rid in rids:
2304+ for unit in related_units(rid):
2305+ sub_config = relation_get('subordinate_configuration',
2306+ rid=rid, unit=unit)
2307+ if sub_config and sub_config != '':
2308+ try:
2309+ sub_config = json.loads(sub_config)
2310+ except:
2311+ log('Could not parse JSON from '
2312+ 'subordinate_configuration setting from %s'
2313+ % rid, level=ERROR)
2314+ continue
2315+
2316+ for service in self.services:
2317+ if service not in sub_config:
2318+ log('Found subordinate_configuration on %s but it '
2319+ 'contained nothing for %s service'
2320+ % (rid, service), level=INFO)
2321+ continue
2322+
2323+ sub_config = sub_config[service]
2324+ if self.config_file not in sub_config:
2325+ log('Found subordinate_configuration on %s but it '
2326+ 'contained nothing for %s'
2327+ % (rid, self.config_file), level=INFO)
2328+ continue
2329+
2330+ sub_config = sub_config[self.config_file]
2331+ for k, v in six.iteritems(sub_config):
2332+ if k == 'sections':
2333+ for section, config_list in six.iteritems(v):
2334+ log("adding section '%s'" % (section),
2335+ level=DEBUG)
2336+ if ctxt[k].get(section):
2337+ ctxt[k][section].extend(config_list)
2338+ else:
2339+ ctxt[k][section] = config_list
2340+ else:
2341+ ctxt[k] = v
2342+ log("%d section(s) found" % (len(ctxt['sections'])), level=DEBUG)
2343+ return ctxt
2344+
2345+
2346+class LogLevelContext(OSContextGenerator):
2347+
2348+ def __call__(self):
2349+ ctxt = {}
2350+ ctxt['debug'] = \
2351+ False if config('debug') is None else config('debug')
2352+ ctxt['verbose'] = \
2353+ False if config('verbose') is None else config('verbose')
2354+
2355+ return ctxt
2356+
2357+
2358+class SyslogContext(OSContextGenerator):
2359+
2360+ def __call__(self):
2361+ ctxt = {'use_syslog': config('use-syslog')}
2362+ return ctxt
2363+
2364+
2365+class BindHostContext(OSContextGenerator):
2366+
2367+ def __call__(self):
2368+ if config('prefer-ipv6'):
2369+ return {'bind_host': '::'}
2370+ else:
2371+ return {'bind_host': '0.0.0.0'}
2372+
2373+
2374+class WorkerConfigContext(OSContextGenerator):
2375+
2376+ @property
2377+ def num_cpus(self):
2378+ try:
2379+ from psutil import NUM_CPUS
2380+ except ImportError:
2381+ apt_install('python-psutil', fatal=True)
2382+ from psutil import NUM_CPUS
2383+
2384+ return NUM_CPUS
2385+
2386+ def __call__(self):
2387+ multiplier = config('worker-multiplier') or 0
2388+ ctxt = {"workers": self.num_cpus * multiplier}
2389+ return ctxt
2390+
2391+
2392+class ZeroMQContext(OSContextGenerator):
2393+ interfaces = ['zeromq-configuration']
2394+
2395+ def __call__(self):
2396+ ctxt = {}
2397+ if is_relation_made('zeromq-configuration', 'host'):
2398+ for rid in relation_ids('zeromq-configuration'):
2399+ for unit in related_units(rid):
2400+ ctxt['zmq_nonce'] = relation_get('nonce', unit, rid)
2401+ ctxt['zmq_host'] = relation_get('host', unit, rid)
2402+ ctxt['zmq_redis_address'] = relation_get(
2403+ 'zmq_redis_address', unit, rid)
2404+
2405+ return ctxt
2406+
2407+
2408+class NotificationDriverContext(OSContextGenerator):
2409+
2410+ def __init__(self, zmq_relation='zeromq-configuration',
2411+ amqp_relation='amqp'):
2412+ """
2413+ :param zmq_relation: Name of Zeromq relation to check
2414+ """
2415+ self.zmq_relation = zmq_relation
2416+ self.amqp_relation = amqp_relation
2417+
2418+ def __call__(self):
2419+ ctxt = {'notifications': 'False'}
2420+ if is_relation_made(self.amqp_relation):
2421+ ctxt['notifications'] = "True"
2422+
2423+ return ctxt
2424+
2425+
2426+class SysctlContext(OSContextGenerator):
2427+ """This context check if the 'sysctl' option exists on configuration
2428+ then creates a file with the loaded contents"""
2429+ def __call__(self):
2430+ sysctl_dict = config('sysctl')
2431+ if sysctl_dict:
2432+ sysctl_create(sysctl_dict,
2433+ '/etc/sysctl.d/50-{0}.conf'.format(charm_name()))
2434+ return {'sysctl': sysctl_dict}
2435+
2436+
2437+class NeutronAPIContext(OSContextGenerator):
2438+ '''
2439+ Inspects current neutron-plugin-api relation for neutron settings. Return
2440+ defaults if it is not present.
2441+ '''
2442+ interfaces = ['neutron-plugin-api']
2443+
2444+ def __call__(self):
2445+ self.neutron_defaults = {
2446+ 'l2_population': {
2447+ 'rel_key': 'l2-population',
2448+ 'default': False,
2449+ },
2450+ 'overlay_network_type': {
2451+ 'rel_key': 'overlay-network-type',
2452+ 'default': 'gre',
2453+ },
2454+ 'neutron_security_groups': {
2455+ 'rel_key': 'neutron-security-groups',
2456+ 'default': False,
2457+ },
2458+ 'network_device_mtu': {
2459+ 'rel_key': 'network-device-mtu',
2460+ 'default': None,
2461+ },
2462+ 'enable_dvr': {
2463+ 'rel_key': 'enable-dvr',
2464+ 'default': False,
2465+ },
2466+ 'enable_l3ha': {
2467+ 'rel_key': 'enable-l3ha',
2468+ 'default': False,
2469+ },
2470+ }
2471+ ctxt = self.get_neutron_options({})
2472+ for rid in relation_ids('neutron-plugin-api'):
2473+ for unit in related_units(rid):
2474+ rdata = relation_get(rid=rid, unit=unit)
2475+ if 'l2-population' in rdata:
2476+ ctxt.update(self.get_neutron_options(rdata))
2477+
2478+ return ctxt
2479+
2480+ def get_neutron_options(self, rdata):
2481+ settings = {}
2482+ for nkey in self.neutron_defaults.keys():
2483+ defv = self.neutron_defaults[nkey]['default']
2484+ rkey = self.neutron_defaults[nkey]['rel_key']
2485+ if rkey in rdata.keys():
2486+ if type(defv) is bool:
2487+ settings[nkey] = bool_from_string(rdata[rkey])
2488+ else:
2489+ settings[nkey] = rdata[rkey]
2490+ else:
2491+ settings[nkey] = defv
2492+ return settings
2493+
2494+
2495+class ExternalPortContext(NeutronPortContext):
2496+
2497+ def __call__(self):
2498+ ctxt = {}
2499+ ports = config('ext-port')
2500+ if ports:
2501+ ports = [p.strip() for p in ports.split()]
2502+ ports = self.resolve_ports(ports)
2503+ if ports:
2504+ ctxt = {"ext_port": ports[0]}
2505+ napi_settings = NeutronAPIContext()()
2506+ mtu = napi_settings.get('network_device_mtu')
2507+ if mtu:
2508+ ctxt['ext_port_mtu'] = mtu
2509+
2510+ return ctxt
2511+
2512+
2513+class DataPortContext(NeutronPortContext):
2514+
2515+ def __call__(self):
2516+ ports = config('data-port')
2517+ if ports:
2518+ # Map of {port/mac:bridge}
2519+ portmap = parse_data_port_mappings(ports)
2520+ ports = portmap.keys()
2521+ # Resolve provided ports or mac addresses and filter out those
2522+ # already attached to a bridge.
2523+ resolved = self.resolve_ports(ports)
2524+ # FIXME: is this necessary?
2525+ normalized = {get_nic_hwaddr(port): port for port in resolved
2526+ if port not in ports}
2527+ normalized.update({port: port for port in resolved
2528+ if port in ports})
2529+ if resolved:
2530+ return {normalized[port]: bridge for port, bridge in
2531+ six.iteritems(portmap) if port in normalized.keys()}
2532+
2533+ return None
2534+
2535+
2536+class PhyNICMTUContext(DataPortContext):
2537+
2538+ def __call__(self):
2539+ ctxt = {}
2540+ mappings = super(PhyNICMTUContext, self).__call__()
2541+ if mappings and mappings.keys():
2542+ ports = sorted(mappings.keys())
2543+ napi_settings = NeutronAPIContext()()
2544+ mtu = napi_settings.get('network_device_mtu')
2545+ all_ports = set()
2546+ # If any of ports is a vlan device, its underlying device must have
2547+ # mtu applied first.
2548+ for port in ports:
2549+ for lport in glob.glob("/sys/class/net/%s/lower_*" % port):
2550+ lport = os.path.basename(lport)
2551+ all_ports.add(lport.split('_')[1])
2552+
2553+ all_ports = list(all_ports)
2554+ all_ports.extend(ports)
2555+ if mtu:
2556+ ctxt["devs"] = '\\n'.join(all_ports)
2557+ ctxt['mtu'] = mtu
2558+
2559+ return ctxt
2560+
2561+
2562+class NetworkServiceContext(OSContextGenerator):
2563+
2564+ def __init__(self, rel_name='quantum-network-service'):
2565+ self.rel_name = rel_name
2566+ self.interfaces = [rel_name]
2567+
2568+ def __call__(self):
2569+ for rid in relation_ids(self.rel_name):
2570+ for unit in related_units(rid):
2571+ rdata = relation_get(rid=rid, unit=unit)
2572+ ctxt = {
2573+ 'keystone_host': rdata.get('keystone_host'),
2574+ 'service_port': rdata.get('service_port'),
2575+ 'auth_port': rdata.get('auth_port'),
2576+ 'service_tenant': rdata.get('service_tenant'),
2577+ 'service_username': rdata.get('service_username'),
2578+ 'service_password': rdata.get('service_password'),
2579+ 'quantum_host': rdata.get('quantum_host'),
2580+ 'quantum_port': rdata.get('quantum_port'),
2581+ 'quantum_url': rdata.get('quantum_url'),
2582+ 'region': rdata.get('region'),
2583+ 'service_protocol':
2584+ rdata.get('service_protocol') or 'http',
2585+ 'auth_protocol':
2586+ rdata.get('auth_protocol') or 'http',
2587+ }
2588+ if self.context_complete(ctxt):
2589+ return ctxt
2590+ return {}
2591
2592=== added file 'hooks/charmhelpers/contrib/openstack/ip.py'
2593--- hooks/charmhelpers/contrib/openstack/ip.py 1970-01-01 00:00:00 +0000
2594+++ hooks/charmhelpers/contrib/openstack/ip.py 2016-02-04 15:46:27 +0000
2595@@ -0,0 +1,151 @@
2596+# Copyright 2014-2015 Canonical Limited.
2597+#
2598+# This file is part of charm-helpers.
2599+#
2600+# charm-helpers is free software: you can redistribute it and/or modify
2601+# it under the terms of the GNU Lesser General Public License version 3 as
2602+# published by the Free Software Foundation.
2603+#
2604+# charm-helpers is distributed in the hope that it will be useful,
2605+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2606+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2607+# GNU Lesser General Public License for more details.
2608+#
2609+# You should have received a copy of the GNU Lesser General Public License
2610+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2611+
2612+from charmhelpers.core.hookenv import (
2613+ config,
2614+ unit_get,
2615+ service_name,
2616+)
2617+from charmhelpers.contrib.network.ip import (
2618+ get_address_in_network,
2619+ is_address_in_network,
2620+ is_ipv6,
2621+ get_ipv6_addr,
2622+)
2623+from charmhelpers.contrib.hahelpers.cluster import is_clustered
2624+
2625+PUBLIC = 'public'
2626+INTERNAL = 'int'
2627+ADMIN = 'admin'
2628+
2629+ADDRESS_MAP = {
2630+ PUBLIC: {
2631+ 'config': 'os-public-network',
2632+ 'fallback': 'public-address',
2633+ 'override': 'os-public-hostname',
2634+ },
2635+ INTERNAL: {
2636+ 'config': 'os-internal-network',
2637+ 'fallback': 'private-address',
2638+ 'override': 'os-internal-hostname',
2639+ },
2640+ ADMIN: {
2641+ 'config': 'os-admin-network',
2642+ 'fallback': 'private-address',
2643+ 'override': 'os-admin-hostname',
2644+ }
2645+}
2646+
2647+
2648+def canonical_url(configs, endpoint_type=PUBLIC):
2649+ """Returns the correct HTTP URL to this host given the state of HTTPS
2650+ configuration, hacluster and charm configuration.
2651+
2652+ :param configs: OSTemplateRenderer config templating object to inspect
2653+ for a complete https context.
2654+ :param endpoint_type: str endpoint type to resolve.
2655+ :param returns: str base URL for services on the current service unit.
2656+ """
2657+ scheme = _get_scheme(configs)
2658+
2659+ address = resolve_address(endpoint_type)
2660+ if is_ipv6(address):
2661+ address = "[{}]".format(address)
2662+
2663+ return '%s://%s' % (scheme, address)
2664+
2665+
2666+def _get_scheme(configs):
2667+ """Returns the scheme to use for the url (either http or https)
2668+ depending upon whether https is in the configs value.
2669+
2670+ :param configs: OSTemplateRenderer config templating object to inspect
2671+ for a complete https context.
2672+ :returns: either 'http' or 'https' depending on whether https is
2673+ configured within the configs context.
2674+ """
2675+ scheme = 'http'
2676+ if configs and 'https' in configs.complete_contexts():
2677+ scheme = 'https'
2678+ return scheme
2679+
2680+
2681+def _get_address_override(endpoint_type=PUBLIC):
2682+ """Returns any address overrides that the user has defined based on the
2683+ endpoint type.
2684+
2685+ Note: this function allows for the service name to be inserted into the
2686+ address if the user specifies {service_name}.somehost.org.
2687+
2688+ :param endpoint_type: the type of endpoint to retrieve the override
2689+ value for.
2690+ :returns: any endpoint address or hostname that the user has overridden
2691+ or None if an override is not present.
2692+ """
2693+ override_key = ADDRESS_MAP[endpoint_type]['override']
2694+ addr_override = config(override_key)
2695+ if not addr_override:
2696+ return None
2697+ else:
2698+ return addr_override.format(service_name=service_name())
2699+
2700+
2701+def resolve_address(endpoint_type=PUBLIC):
2702+ """Return unit address depending on net config.
2703+
2704+ If unit is clustered with vip(s) and has net splits defined, return vip on
2705+ correct network. If clustered with no nets defined, return primary vip.
2706+
2707+ If not clustered, return unit address ensuring address is on configured net
2708+ split if one is configured.
2709+
2710+ :param endpoint_type: Network endpoing type
2711+ """
2712+ resolved_address = _get_address_override(endpoint_type)
2713+ if resolved_address:
2714+ return resolved_address
2715+
2716+ vips = config('vip')
2717+ if vips:
2718+ vips = vips.split()
2719+
2720+ net_type = ADDRESS_MAP[endpoint_type]['config']
2721+ net_addr = config(net_type)
2722+ net_fallback = ADDRESS_MAP[endpoint_type]['fallback']
2723+ clustered = is_clustered()
2724+ if clustered:
2725+ if not net_addr:
2726+ # If no net-splits defined, we expect a single vip
2727+ resolved_address = vips[0]
2728+ else:
2729+ for vip in vips:
2730+ if is_address_in_network(net_addr, vip):
2731+ resolved_address = vip
2732+ break
2733+ else:
2734+ if config('prefer-ipv6'):
2735+ fallback_addr = get_ipv6_addr(exc_list=vips)[0]
2736+ else:
2737+ fallback_addr = unit_get(net_fallback)
2738+
2739+ resolved_address = get_address_in_network(net_addr, fallback_addr)
2740+
2741+ if resolved_address is None:
2742+ raise ValueError("Unable to resolve a suitable IP address based on "
2743+ "charm state and configuration. (net_type=%s, "
2744+ "clustered=%s)" % (net_type, clustered))
2745+
2746+ return resolved_address
2747
2748=== added file 'hooks/charmhelpers/contrib/openstack/neutron.py'
2749--- hooks/charmhelpers/contrib/openstack/neutron.py 1970-01-01 00:00:00 +0000
2750+++ hooks/charmhelpers/contrib/openstack/neutron.py 2016-02-04 15:46:27 +0000
2751@@ -0,0 +1,370 @@
2752+# Copyright 2014-2015 Canonical Limited.
2753+#
2754+# This file is part of charm-helpers.
2755+#
2756+# charm-helpers is free software: you can redistribute it and/or modify
2757+# it under the terms of the GNU Lesser General Public License version 3 as
2758+# published by the Free Software Foundation.
2759+#
2760+# charm-helpers is distributed in the hope that it will be useful,
2761+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2762+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2763+# GNU Lesser General Public License for more details.
2764+#
2765+# You should have received a copy of the GNU Lesser General Public License
2766+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2767+
2768+# Various utilies for dealing with Neutron and the renaming from Quantum.
2769+
2770+import six
2771+from subprocess import check_output
2772+
2773+from charmhelpers.core.hookenv import (
2774+ config,
2775+ log,
2776+ ERROR,
2777+)
2778+
2779+from charmhelpers.contrib.openstack.utils import os_release
2780+
2781+
2782+def headers_package():
2783+ """Ensures correct linux-headers for running kernel are installed,
2784+ for building DKMS package"""
2785+ kver = check_output(['uname', '-r']).decode('UTF-8').strip()
2786+ return 'linux-headers-%s' % kver
2787+
2788+QUANTUM_CONF_DIR = '/etc/quantum'
2789+
2790+
2791+def kernel_version():
2792+ """ Retrieve the current major kernel version as a tuple e.g. (3, 13) """
2793+ kver = check_output(['uname', '-r']).decode('UTF-8').strip()
2794+ kver = kver.split('.')
2795+ return (int(kver[0]), int(kver[1]))
2796+
2797+
2798+def determine_dkms_package():
2799+ """ Determine which DKMS package should be used based on kernel version """
2800+ # NOTE: 3.13 kernels have support for GRE and VXLAN native
2801+ if kernel_version() >= (3, 13):
2802+ return []
2803+ else:
2804+ return ['openvswitch-datapath-dkms']
2805+
2806+
2807+# legacy
2808+
2809+
2810+def quantum_plugins():
2811+ from charmhelpers.contrib.openstack import context
2812+ return {
2813+ 'ovs': {
2814+ 'config': '/etc/quantum/plugins/openvswitch/'
2815+ 'ovs_quantum_plugin.ini',
2816+ 'driver': 'quantum.plugins.openvswitch.ovs_quantum_plugin.'
2817+ 'OVSQuantumPluginV2',
2818+ 'contexts': [
2819+ context.SharedDBContext(user=config('neutron-database-user'),
2820+ database=config('neutron-database'),
2821+ relation_prefix='neutron',
2822+ ssl_dir=QUANTUM_CONF_DIR)],
2823+ 'services': ['quantum-plugin-openvswitch-agent'],
2824+ 'packages': [[headers_package()] + determine_dkms_package(),
2825+ ['quantum-plugin-openvswitch-agent']],
2826+ 'server_packages': ['quantum-server',
2827+ 'quantum-plugin-openvswitch'],
2828+ 'server_services': ['quantum-server']
2829+ },
2830+ 'nvp': {
2831+ 'config': '/etc/quantum/plugins/nicira/nvp.ini',
2832+ 'driver': 'quantum.plugins.nicira.nicira_nvp_plugin.'
2833+ 'QuantumPlugin.NvpPluginV2',
2834+ 'contexts': [
2835+ context.SharedDBContext(user=config('neutron-database-user'),
2836+ database=config('neutron-database'),
2837+ relation_prefix='neutron',
2838+ ssl_dir=QUANTUM_CONF_DIR)],
2839+ 'services': [],
2840+ 'packages': [],
2841+ 'server_packages': ['quantum-server',
2842+ 'quantum-plugin-nicira'],
2843+ 'server_services': ['quantum-server']
2844+ }
2845+ }
2846+
2847+NEUTRON_CONF_DIR = '/etc/neutron'
2848+
2849+
2850+def neutron_plugins():
2851+ from charmhelpers.contrib.openstack import context
2852+ release = os_release('nova-common')
2853+ plugins = {
2854+ 'ovs': {
2855+ 'config': '/etc/neutron/plugins/openvswitch/'
2856+ 'ovs_neutron_plugin.ini',
2857+ 'driver': 'neutron.plugins.openvswitch.ovs_neutron_plugin.'
2858+ 'OVSNeutronPluginV2',
2859+ 'contexts': [
2860+ context.SharedDBContext(user=config('neutron-database-user'),
2861+ database=config('neutron-database'),
2862+ relation_prefix='neutron',
2863+ ssl_dir=NEUTRON_CONF_DIR)],
2864+ 'services': ['neutron-plugin-openvswitch-agent'],
2865+ 'packages': [[headers_package()] + determine_dkms_package(),
2866+ ['neutron-plugin-openvswitch-agent']],
2867+ 'server_packages': ['neutron-server',
2868+ 'neutron-plugin-openvswitch'],
2869+ 'server_services': ['neutron-server']
2870+ },
2871+ 'nvp': {
2872+ 'config': '/etc/neutron/plugins/nicira/nvp.ini',
2873+ 'driver': 'neutron.plugins.nicira.nicira_nvp_plugin.'
2874+ 'NeutronPlugin.NvpPluginV2',
2875+ 'contexts': [
2876+ context.SharedDBContext(user=config('neutron-database-user'),
2877+ database=config('neutron-database'),
2878+ relation_prefix='neutron',
2879+ ssl_dir=NEUTRON_CONF_DIR)],
2880+ 'services': [],
2881+ 'packages': [],
2882+ 'server_packages': ['neutron-server',
2883+ 'neutron-plugin-nicira'],
2884+ 'server_services': ['neutron-server']
2885+ },
2886+ 'nsx': {
2887+ 'config': '/etc/neutron/plugins/vmware/nsx.ini',
2888+ 'driver': 'vmware',
2889+ 'contexts': [
2890+ context.SharedDBContext(user=config('neutron-database-user'),
2891+ database=config('neutron-database'),
2892+ relation_prefix='neutron',
2893+ ssl_dir=NEUTRON_CONF_DIR)],
2894+ 'services': [],
2895+ 'packages': [],
2896+ 'server_packages': ['neutron-server',
2897+ 'neutron-plugin-vmware'],
2898+ 'server_services': ['neutron-server']
2899+ },
2900+ 'n1kv': {
2901+ 'config': '/etc/neutron/plugins/cisco/cisco_plugins.ini',
2902+ 'driver': 'neutron.plugins.cisco.network_plugin.PluginV2',
2903+ 'contexts': [
2904+ context.SharedDBContext(user=config('neutron-database-user'),
2905+ database=config('neutron-database'),
2906+ relation_prefix='neutron',
2907+ ssl_dir=NEUTRON_CONF_DIR)],
2908+ 'services': [],
2909+ 'packages': [[headers_package()] + determine_dkms_package(),
2910+ ['neutron-plugin-cisco']],
2911+ 'server_packages': ['neutron-server',
2912+ 'neutron-plugin-cisco'],
2913+ 'server_services': ['neutron-server']
2914+ },
2915+ 'Calico': {
2916+ 'config': '/etc/neutron/plugins/ml2/ml2_conf.ini',
2917+ 'driver': 'neutron.plugins.ml2.plugin.Ml2Plugin',
2918+ 'contexts': [
2919+ context.SharedDBContext(user=config('neutron-database-user'),
2920+ database=config('neutron-database'),
2921+ relation_prefix='neutron',
2922+ ssl_dir=NEUTRON_CONF_DIR)],
2923+ 'services': ['calico-felix',
2924+ 'bird',
2925+ 'neutron-dhcp-agent',
2926+ 'nova-api-metadata',
2927+ 'etcd'],
2928+ 'packages': [[headers_package()] + determine_dkms_package(),
2929+ ['calico-compute',
2930+ 'bird',
2931+ 'neutron-dhcp-agent',
2932+ 'nova-api-metadata',
2933+ 'etcd']],
2934+ 'server_packages': ['neutron-server', 'calico-control', 'etcd'],
2935+ 'server_services': ['neutron-server', 'etcd']
2936+ },
2937+ 'vsp': {
2938+ 'config': '/etc/neutron/plugins/nuage/nuage_plugin.ini',
2939+ 'driver': 'neutron.plugins.nuage.plugin.NuagePlugin',
2940+ 'contexts': [
2941+ context.SharedDBContext(user=config('neutron-database-user'),
2942+ database=config('neutron-database'),
2943+ relation_prefix='neutron',
2944+ ssl_dir=NEUTRON_CONF_DIR)],
2945+ 'services': [],
2946+ 'packages': [],
2947+ 'server_packages': ['neutron-server', 'neutron-plugin-nuage'],
2948+ 'server_services': ['neutron-server']
2949+ },
2950+ 'plumgrid': {
2951+ 'config': '/etc/neutron/plugins/plumgrid/plumgrid.ini',
2952+ 'driver': 'neutron.plugins.plumgrid.plumgrid_plugin.plumgrid_plugin.NeutronPluginPLUMgridV2',
2953+ 'contexts': [
2954+ context.SharedDBContext(user=config('database-user'),
2955+ database=config('database'),
2956+ ssl_dir=NEUTRON_CONF_DIR)],
2957+ 'services': [],
2958+ 'packages': [['plumgrid-lxc'],
2959+ ['iovisor-dkms']],
2960+ 'server_packages': ['neutron-server',
2961+ 'neutron-plugin-plumgrid'],
2962+ 'server_services': ['neutron-server']
2963+ },
2964+ 'midonet': {
2965+ 'config': '/etc/neutron/plugins/midonet/midonet.ini',
2966+ 'driver': 'midonet.neutron.plugin.MidonetPluginV2',
2967+ 'contexts': [
2968+ context.SharedDBContext(user=config('neutron-database-user'),
2969+ database=config('neutron-database'),
2970+ relation_prefix='neutron',
2971+ ssl_dir=NEUTRON_CONF_DIR)],
2972+ 'services': [],
2973+ 'packages': [[headers_package()] + determine_dkms_package()],
2974+ 'server_packages': ['neutron-server',
2975+ 'python-neutron-plugin-midonet'],
2976+ 'server_services': ['neutron-server']
2977+ }
2978+ }
2979+ if release >= 'icehouse':
2980+ # NOTE: patch in ml2 plugin for icehouse onwards
2981+ plugins['ovs']['config'] = '/etc/neutron/plugins/ml2/ml2_conf.ini'
2982+ plugins['ovs']['driver'] = 'neutron.plugins.ml2.plugin.Ml2Plugin'
2983+ plugins['ovs']['server_packages'] = ['neutron-server',
2984+ 'neutron-plugin-ml2']
2985+ # NOTE: patch in vmware renames nvp->nsx for icehouse onwards
2986+ plugins['nvp'] = plugins['nsx']
2987+ return plugins
2988+
2989+
2990+def neutron_plugin_attribute(plugin, attr, net_manager=None):
2991+ manager = net_manager or network_manager()
2992+ if manager == 'quantum':
2993+ plugins = quantum_plugins()
2994+ elif manager == 'neutron':
2995+ plugins = neutron_plugins()
2996+ else:
2997+ log("Network manager '%s' does not support plugins." % (manager),
2998+ level=ERROR)
2999+ raise Exception
3000+
3001+ try:
3002+ _plugin = plugins[plugin]
3003+ except KeyError:
3004+ log('Unrecognised plugin for %s: %s' % (manager, plugin), level=ERROR)
3005+ raise Exception
3006+
3007+ try:
3008+ return _plugin[attr]
3009+ except KeyError:
3010+ return None
3011+
3012+
3013+def network_manager():
3014+ '''
3015+ Deals with the renaming of Quantum to Neutron in H and any situations
3016+ that require compatability (eg, deploying H with network-manager=quantum,
3017+ upgrading from G).
3018+ '''
3019+ release = os_release('nova-common')
3020+ manager = config('network-manager').lower()
3021+
3022+ if manager not in ['quantum', 'neutron']:
3023+ return manager
3024+
3025+ if release in ['essex']:
3026+ # E does not support neutron
3027+ log('Neutron networking not supported in Essex.', level=ERROR)
3028+ raise Exception
3029+ elif release in ['folsom', 'grizzly']:
3030+ # neutron is named quantum in F and G
3031+ return 'quantum'
3032+ else:
3033+ # ensure accurate naming for all releases post-H
3034+ return 'neutron'
3035+
3036+
3037+def parse_mappings(mappings, key_rvalue=False):
3038+ """By default mappings are lvalue keyed.
3039+
3040+ If key_rvalue is True, the mapping will be reversed to allow multiple
3041+ configs for the same lvalue.
3042+ """
3043+ parsed = {}
3044+ if mappings:
3045+ mappings = mappings.split()
3046+ for m in mappings:
3047+ p = m.partition(':')
3048+
3049+ if key_rvalue:
3050+ key_index = 2
3051+ val_index = 0
3052+ # if there is no rvalue skip to next
3053+ if not p[1]:
3054+ continue
3055+ else:
3056+ key_index = 0
3057+ val_index = 2
3058+
3059+ key = p[key_index].strip()
3060+ parsed[key] = p[val_index].strip()
3061+
3062+ return parsed
3063+
3064+
3065+def parse_bridge_mappings(mappings):
3066+ """Parse bridge mappings.
3067+
3068+ Mappings must be a space-delimited list of provider:bridge mappings.
3069+
3070+ Returns dict of the form {provider:bridge}.
3071+ """
3072+ return parse_mappings(mappings)
3073+
3074+
3075+def parse_data_port_mappings(mappings, default_bridge='br-data'):
3076+ """Parse data port mappings.
3077+
3078+ Mappings must be a space-delimited list of bridge:port.
3079+
3080+ Returns dict of the form {port:bridge} where ports may be mac addresses or
3081+ interface names.
3082+ """
3083+
3084+ # NOTE(dosaboy): we use rvalue for key to allow multiple values to be
3085+ # proposed for <port> since it may be a mac address which will differ
3086+ # across units this allowing first-known-good to be chosen.
3087+ _mappings = parse_mappings(mappings, key_rvalue=True)
3088+ if not _mappings or list(_mappings.values()) == ['']:
3089+ if not mappings:
3090+ return {}
3091+
3092+ # For backwards-compatibility we need to support port-only provided in
3093+ # config.
3094+ _mappings = {mappings.split()[0]: default_bridge}
3095+
3096+ ports = _mappings.keys()
3097+ if len(set(ports)) != len(ports):
3098+ raise Exception("It is not allowed to have the same port configured "
3099+ "on more than one bridge")
3100+
3101+ return _mappings
3102+
3103+
3104+def parse_vlan_range_mappings(mappings):
3105+ """Parse vlan range mappings.
3106+
3107+ Mappings must be a space-delimited list of provider:start:end mappings.
3108+
3109+ The start:end range is optional and may be omitted.
3110+
3111+ Returns dict of the form {provider: (start, end)}.
3112+ """
3113+ _mappings = parse_mappings(mappings)
3114+ if not _mappings:
3115+ return {}
3116+
3117+ mappings = {}
3118+ for p, r in six.iteritems(_mappings):
3119+ mappings[p] = tuple(r.split(':'))
3120+
3121+ return mappings
3122
3123=== added directory 'hooks/charmhelpers/contrib/openstack/templates'
3124=== added file 'hooks/charmhelpers/contrib/openstack/templates/__init__.py'
3125--- hooks/charmhelpers/contrib/openstack/templates/__init__.py 1970-01-01 00:00:00 +0000
3126+++ hooks/charmhelpers/contrib/openstack/templates/__init__.py 2016-02-04 15:46:27 +0000
3127@@ -0,0 +1,18 @@
3128+# Copyright 2014-2015 Canonical Limited.
3129+#
3130+# This file is part of charm-helpers.
3131+#
3132+# charm-helpers is free software: you can redistribute it and/or modify
3133+# it under the terms of the GNU Lesser General Public License version 3 as
3134+# published by the Free Software Foundation.
3135+#
3136+# charm-helpers is distributed in the hope that it will be useful,
3137+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3138+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3139+# GNU Lesser General Public License for more details.
3140+#
3141+# You should have received a copy of the GNU Lesser General Public License
3142+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3143+
3144+# dummy __init__.py to fool syncer into thinking this is a syncable python
3145+# module
3146
3147=== added file 'hooks/charmhelpers/contrib/openstack/templates/ceph.conf'
3148--- hooks/charmhelpers/contrib/openstack/templates/ceph.conf 1970-01-01 00:00:00 +0000
3149+++ hooks/charmhelpers/contrib/openstack/templates/ceph.conf 2016-02-04 15:46:27 +0000
3150@@ -0,0 +1,21 @@
3151+###############################################################################
3152+# [ WARNING ]
3153+# cinder configuration file maintained by Juju
3154+# local changes may be overwritten.
3155+###############################################################################
3156+[global]
3157+{% if auth -%}
3158+auth_supported = {{ auth }}
3159+keyring = /etc/ceph/$cluster.$name.keyring
3160+mon host = {{ mon_hosts }}
3161+{% endif -%}
3162+log to syslog = {{ use_syslog }}
3163+err to syslog = {{ use_syslog }}
3164+clog to syslog = {{ use_syslog }}
3165+
3166+[client]
3167+{% if rbd_client_cache_settings -%}
3168+{% for key, value in rbd_client_cache_settings.iteritems() -%}
3169+{{ key }} = {{ value }}
3170+{% endfor -%}
3171+{%- endif %}
3172\ No newline at end of file
3173
3174=== added file 'hooks/charmhelpers/contrib/openstack/templates/git.upstart'
3175--- hooks/charmhelpers/contrib/openstack/templates/git.upstart 1970-01-01 00:00:00 +0000
3176+++ hooks/charmhelpers/contrib/openstack/templates/git.upstart 2016-02-04 15:46:27 +0000
3177@@ -0,0 +1,17 @@
3178+description "{{ service_description }}"
3179+author "Juju {{ service_name }} Charm <juju@localhost>"
3180+
3181+start on runlevel [2345]
3182+stop on runlevel [!2345]
3183+
3184+respawn
3185+
3186+exec start-stop-daemon --start --chuid {{ user_name }} \
3187+ --chdir {{ start_dir }} --name {{ process_name }} \
3188+ --exec {{ executable_name }} -- \
3189+ {% for config_file in config_files -%}
3190+ --config-file={{ config_file }} \
3191+ {% endfor -%}
3192+ {% if log_file -%}
3193+ --log-file={{ log_file }}
3194+ {% endif -%}
3195
3196=== added file 'hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg'
3197--- hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg 1970-01-01 00:00:00 +0000
3198+++ hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg 2016-02-04 15:46:27 +0000
3199@@ -0,0 +1,58 @@
3200+global
3201+ log {{ local_host }} local0
3202+ log {{ local_host }} local1 notice
3203+ maxconn 20000
3204+ user haproxy
3205+ group haproxy
3206+ spread-checks 0
3207+
3208+defaults
3209+ log global
3210+ mode tcp
3211+ option tcplog
3212+ option dontlognull
3213+ retries 3
3214+ timeout queue 1000
3215+ timeout connect 1000
3216+{% if haproxy_client_timeout -%}
3217+ timeout client {{ haproxy_client_timeout }}
3218+{% else -%}
3219+ timeout client 30000
3220+{% endif -%}
3221+
3222+{% if haproxy_server_timeout -%}
3223+ timeout server {{ haproxy_server_timeout }}
3224+{% else -%}
3225+ timeout server 30000
3226+{% endif -%}
3227+
3228+listen stats {{ stat_port }}
3229+ mode http
3230+ stats enable
3231+ stats hide-version
3232+ stats realm Haproxy\ Statistics
3233+ stats uri /
3234+ stats auth admin:password
3235+
3236+{% if frontends -%}
3237+{% for service, ports in service_ports.items() -%}
3238+frontend tcp-in_{{ service }}
3239+ bind *:{{ ports[0] }}
3240+ {% if ipv6 -%}
3241+ bind :::{{ ports[0] }}
3242+ {% endif -%}
3243+ {% for frontend in frontends -%}
3244+ acl net_{{ frontend }} dst {{ frontends[frontend]['network'] }}
3245+ use_backend {{ service }}_{{ frontend }} if net_{{ frontend }}
3246+ {% endfor -%}
3247+ default_backend {{ service }}_{{ default_backend }}
3248+
3249+{% for frontend in frontends -%}
3250+backend {{ service }}_{{ frontend }}
3251+ balance leastconn
3252+ {% for unit, address in frontends[frontend]['backends'].items() -%}
3253+ server {{ unit }} {{ address }}:{{ ports[1] }} check
3254+ {% endfor %}
3255+{% endfor -%}
3256+{% endfor -%}
3257+{% endif -%}
3258
3259=== added file 'hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend'
3260--- hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend 1970-01-01 00:00:00 +0000
3261+++ hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend 2016-02-04 15:46:27 +0000
3262@@ -0,0 +1,24 @@
3263+{% if endpoints -%}
3264+{% for ext_port in ext_ports -%}
3265+Listen {{ ext_port }}
3266+{% endfor -%}
3267+{% for address, endpoint, ext, int in endpoints -%}
3268+<VirtualHost {{ address }}:{{ ext }}>
3269+ ServerName {{ endpoint }}
3270+ SSLEngine on
3271+ SSLCertificateFile /etc/apache2/ssl/{{ namespace }}/cert_{{ endpoint }}
3272+ SSLCertificateKeyFile /etc/apache2/ssl/{{ namespace }}/key_{{ endpoint }}
3273+ ProxyPass / http://localhost:{{ int }}/
3274+ ProxyPassReverse / http://localhost:{{ int }}/
3275+ ProxyPreserveHost on
3276+</VirtualHost>
3277+{% endfor -%}
3278+<Proxy *>
3279+ Order deny,allow
3280+ Allow from all
3281+</Proxy>
3282+<Location />
3283+ Order allow,deny
3284+ Allow from all
3285+</Location>
3286+{% endif -%}
3287
3288=== added file 'hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend.conf'
3289--- hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend.conf 1970-01-01 00:00:00 +0000
3290+++ hooks/charmhelpers/contrib/openstack/templates/openstack_https_frontend.conf 2016-02-04 15:46:27 +0000
3291@@ -0,0 +1,24 @@
3292+{% if endpoints -%}
3293+{% for ext_port in ext_ports -%}
3294+Listen {{ ext_port }}
3295+{% endfor -%}
3296+{% for address, endpoint, ext, int in endpoints -%}
3297+<VirtualHost {{ address }}:{{ ext }}>
3298+ ServerName {{ endpoint }}
3299+ SSLEngine on
3300+ SSLCertificateFile /etc/apache2/ssl/{{ namespace }}/cert_{{ endpoint }}
3301+ SSLCertificateKeyFile /etc/apache2/ssl/{{ namespace }}/key_{{ endpoint }}
3302+ ProxyPass / http://localhost:{{ int }}/
3303+ ProxyPassReverse / http://localhost:{{ int }}/
3304+ ProxyPreserveHost on
3305+</VirtualHost>
3306+{% endfor -%}
3307+<Proxy *>
3308+ Order deny,allow
3309+ Allow from all
3310+</Proxy>
3311+<Location />
3312+ Order allow,deny
3313+ Allow from all
3314+</Location>
3315+{% endif -%}
3316
3317=== added file 'hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken'
3318--- hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken 1970-01-01 00:00:00 +0000
3319+++ hooks/charmhelpers/contrib/openstack/templates/section-keystone-authtoken 2016-02-04 15:46:27 +0000
3320@@ -0,0 +1,9 @@
3321+{% if auth_host -%}
3322+[keystone_authtoken]
3323+identity_uri = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/{{ auth_admin_prefix }}
3324+auth_uri = {{ service_protocol }}://{{ service_host }}:{{ service_port }}/{{ service_admin_prefix }}
3325+admin_tenant_name = {{ admin_tenant_name }}
3326+admin_user = {{ admin_user }}
3327+admin_password = {{ admin_password }}
3328+signing_dir = {{ signing_dir }}
3329+{% endif -%}
3330
3331=== added file 'hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo'
3332--- hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo 1970-01-01 00:00:00 +0000
3333+++ hooks/charmhelpers/contrib/openstack/templates/section-rabbitmq-oslo 2016-02-04 15:46:27 +0000
3334@@ -0,0 +1,22 @@
3335+{% if rabbitmq_host or rabbitmq_hosts -%}
3336+[oslo_messaging_rabbit]
3337+rabbit_userid = {{ rabbitmq_user }}
3338+rabbit_virtual_host = {{ rabbitmq_virtual_host }}
3339+rabbit_password = {{ rabbitmq_password }}
3340+{% if rabbitmq_hosts -%}
3341+rabbit_hosts = {{ rabbitmq_hosts }}
3342+{% if rabbitmq_ha_queues -%}
3343+rabbit_ha_queues = True
3344+rabbit_durable_queues = False
3345+{% endif -%}
3346+{% else -%}
3347+rabbit_host = {{ rabbitmq_host }}
3348+{% endif -%}
3349+{% if rabbit_ssl_port -%}
3350+rabbit_use_ssl = True
3351+rabbit_port = {{ rabbit_ssl_port }}
3352+{% if rabbit_ssl_ca -%}
3353+kombu_ssl_ca_certs = {{ rabbit_ssl_ca }}
3354+{% endif -%}
3355+{% endif -%}
3356+{% endif -%}
3357
3358=== added file 'hooks/charmhelpers/contrib/openstack/templates/section-zeromq'
3359--- hooks/charmhelpers/contrib/openstack/templates/section-zeromq 1970-01-01 00:00:00 +0000
3360+++ hooks/charmhelpers/contrib/openstack/templates/section-zeromq 2016-02-04 15:46:27 +0000
3361@@ -0,0 +1,14 @@
3362+{% if zmq_host -%}
3363+# ZeroMQ configuration (restart-nonce: {{ zmq_nonce }})
3364+rpc_backend = zmq
3365+rpc_zmq_host = {{ zmq_host }}
3366+{% if zmq_redis_address -%}
3367+rpc_zmq_matchmaker = redis
3368+matchmaker_heartbeat_freq = 15
3369+matchmaker_heartbeat_ttl = 30
3370+[matchmaker_redis]
3371+host = {{ zmq_redis_address }}
3372+{% else -%}
3373+rpc_zmq_matchmaker = ring
3374+{% endif -%}
3375+{% endif -%}
3376
3377=== added file 'hooks/charmhelpers/contrib/openstack/templating.py'
3378--- hooks/charmhelpers/contrib/openstack/templating.py 1970-01-01 00:00:00 +0000
3379+++ hooks/charmhelpers/contrib/openstack/templating.py 2016-02-04 15:46:27 +0000
3380@@ -0,0 +1,323 @@
3381+# Copyright 2014-2015 Canonical Limited.
3382+#
3383+# This file is part of charm-helpers.
3384+#
3385+# charm-helpers is free software: you can redistribute it and/or modify
3386+# it under the terms of the GNU Lesser General Public License version 3 as
3387+# published by the Free Software Foundation.
3388+#
3389+# charm-helpers is distributed in the hope that it will be useful,
3390+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3391+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3392+# GNU Lesser General Public License for more details.
3393+#
3394+# You should have received a copy of the GNU Lesser General Public License
3395+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3396+
3397+import os
3398+
3399+import six
3400+
3401+from charmhelpers.fetch import apt_install, apt_update
3402+from charmhelpers.core.hookenv import (
3403+ log,
3404+ ERROR,
3405+ INFO
3406+)
3407+from charmhelpers.contrib.openstack.utils import OPENSTACK_CODENAMES
3408+
3409+try:
3410+ from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
3411+except ImportError:
3412+ apt_update(fatal=True)
3413+ apt_install('python-jinja2', fatal=True)
3414+ from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
3415+
3416+
3417+class OSConfigException(Exception):
3418+ pass
3419+
3420+
3421+def get_loader(templates_dir, os_release):
3422+ """
3423+ Create a jinja2.ChoiceLoader containing template dirs up to
3424+ and including os_release. If directory template directory
3425+ is missing at templates_dir, it will be omitted from the loader.
3426+ templates_dir is added to the bottom of the search list as a base
3427+ loading dir.
3428+
3429+ A charm may also ship a templates dir with this module
3430+ and it will be appended to the bottom of the search list, eg::
3431+
3432+ hooks/charmhelpers/contrib/openstack/templates
3433+
3434+ :param templates_dir (str): Base template directory containing release
3435+ sub-directories.
3436+ :param os_release (str): OpenStack release codename to construct template
3437+ loader.
3438+ :returns: jinja2.ChoiceLoader constructed with a list of
3439+ jinja2.FilesystemLoaders, ordered in descending
3440+ order by OpenStack release.
3441+ """
3442+ tmpl_dirs = [(rel, os.path.join(templates_dir, rel))
3443+ for rel in six.itervalues(OPENSTACK_CODENAMES)]
3444+
3445+ if not os.path.isdir(templates_dir):
3446+ log('Templates directory not found @ %s.' % templates_dir,
3447+ level=ERROR)
3448+ raise OSConfigException
3449+
3450+ # the bottom contains tempaltes_dir and possibly a common templates dir
3451+ # shipped with the helper.
3452+ loaders = [FileSystemLoader(templates_dir)]
3453+ helper_templates = os.path.join(os.path.dirname(__file__), 'templates')
3454+ if os.path.isdir(helper_templates):
3455+ loaders.append(FileSystemLoader(helper_templates))
3456+
3457+ for rel, tmpl_dir in tmpl_dirs:
3458+ if os.path.isdir(tmpl_dir):
3459+ loaders.insert(0, FileSystemLoader(tmpl_dir))
3460+ if rel == os_release:
3461+ break
3462+ log('Creating choice loader with dirs: %s' %
3463+ [l.searchpath for l in loaders], level=INFO)
3464+ return ChoiceLoader(loaders)
3465+
3466+
3467+class OSConfigTemplate(object):
3468+ """
3469+ Associates a config file template with a list of context generators.
3470+ Responsible for constructing a template context based on those generators.
3471+ """
3472+ def __init__(self, config_file, contexts):
3473+ self.config_file = config_file
3474+
3475+ if hasattr(contexts, '__call__'):
3476+ self.contexts = [contexts]
3477+ else:
3478+ self.contexts = contexts
3479+
3480+ self._complete_contexts = []
3481+
3482+ def context(self):
3483+ ctxt = {}
3484+ for context in self.contexts:
3485+ _ctxt = context()
3486+ if _ctxt:
3487+ ctxt.update(_ctxt)
3488+ # track interfaces for every complete context.
3489+ [self._complete_contexts.append(interface)
3490+ for interface in context.interfaces
3491+ if interface not in self._complete_contexts]
3492+ return ctxt
3493+
3494+ def complete_contexts(self):
3495+ '''
3496+ Return a list of interfaces that have satisfied contexts.
3497+ '''
3498+ if self._complete_contexts:
3499+ return self._complete_contexts
3500+ self.context()
3501+ return self._complete_contexts
3502+
3503+
3504+class OSConfigRenderer(object):
3505+ """
3506+ This class provides a common templating system to be used by OpenStack
3507+ charms. It is intended to help charms share common code and templates,
3508+ and ease the burden of managing config templates across multiple OpenStack
3509+ releases.
3510+
3511+ Basic usage::
3512+
3513+ # import some common context generates from charmhelpers
3514+ from charmhelpers.contrib.openstack import context
3515+
3516+ # Create a renderer object for a specific OS release.
3517+ configs = OSConfigRenderer(templates_dir='/tmp/templates',
3518+ openstack_release='folsom')
3519+ # register some config files with context generators.
3520+ configs.register(config_file='/etc/nova/nova.conf',
3521+ contexts=[context.SharedDBContext(),
3522+ context.AMQPContext()])
3523+ configs.register(config_file='/etc/nova/api-paste.ini',
3524+ contexts=[context.IdentityServiceContext()])
3525+ configs.register(config_file='/etc/haproxy/haproxy.conf',
3526+ contexts=[context.HAProxyContext()])
3527+ # write out a single config
3528+ configs.write('/etc/nova/nova.conf')
3529+ # write out all registered configs
3530+ configs.write_all()
3531+
3532+ **OpenStack Releases and template loading**
3533+
3534+ When the object is instantiated, it is associated with a specific OS
3535+ release. This dictates how the template loader will be constructed.
3536+
3537+ The constructed loader attempts to load the template from several places
3538+ in the following order:
3539+ - from the most recent OS release-specific template dir (if one exists)
3540+ - the base templates_dir
3541+ - a template directory shipped in the charm with this helper file.
3542+
3543+ For the example above, '/tmp/templates' contains the following structure::
3544+
3545+ /tmp/templates/nova.conf
3546+ /tmp/templates/api-paste.ini
3547+ /tmp/templates/grizzly/api-paste.ini
3548+ /tmp/templates/havana/api-paste.ini
3549+
3550+ Since it was registered with the grizzly release, it first seraches
3551+ the grizzly directory for nova.conf, then the templates dir.
3552+
3553+ When writing api-paste.ini, it will find the template in the grizzly
3554+ directory.
3555+
3556+ If the object were created with folsom, it would fall back to the
3557+ base templates dir for its api-paste.ini template.
3558+
3559+ This system should help manage changes in config files through
3560+ openstack releases, allowing charms to fall back to the most recently
3561+ updated config template for a given release
3562+
3563+ The haproxy.conf, since it is not shipped in the templates dir, will
3564+ be loaded from the module directory's template directory, eg
3565+ $CHARM/hooks/charmhelpers/contrib/openstack/templates. This allows
3566+ us to ship common templates (haproxy, apache) with the helpers.
3567+
3568+ **Context generators**
3569+
3570+ Context generators are used to generate template contexts during hook
3571+ execution. Doing so may require inspecting service relations, charm
3572+ config, etc. When registered, a config file is associated with a list
3573+ of generators. When a template is rendered and written, all context
3574+ generates are called in a chain to generate the context dictionary
3575+ passed to the jinja2 template. See context.py for more info.
3576+ """
3577+ def __init__(self, templates_dir, openstack_release):
3578+ if not os.path.isdir(templates_dir):
3579+ log('Could not locate templates dir %s' % templates_dir,
3580+ level=ERROR)
3581+ raise OSConfigException
3582+
3583+ self.templates_dir = templates_dir
3584+ self.openstack_release = openstack_release
3585+ self.templates = {}
3586+ self._tmpl_env = None
3587+
3588+ if None in [Environment, ChoiceLoader, FileSystemLoader]:
3589+ # if this code is running, the object is created pre-install hook.
3590+ # jinja2 shouldn't get touched until the module is reloaded on next
3591+ # hook execution, with proper jinja2 bits successfully imported.
3592+ apt_install('python-jinja2')
3593+
3594+ def register(self, config_file, contexts):
3595+ """
3596+ Register a config file with a list of context generators to be called
3597+ during rendering.
3598+ """
3599+ self.templates[config_file] = OSConfigTemplate(config_file=config_file,
3600+ contexts=contexts)
3601+ log('Registered config file: %s' % config_file, level=INFO)
3602+
3603+ def _get_tmpl_env(self):
3604+ if not self._tmpl_env:
3605+ loader = get_loader(self.templates_dir, self.openstack_release)
3606+ self._tmpl_env = Environment(loader=loader)
3607+
3608+ def _get_template(self, template):
3609+ self._get_tmpl_env()
3610+ template = self._tmpl_env.get_template(template)
3611+ log('Loaded template from %s' % template.filename, level=INFO)
3612+ return template
3613+
3614+ def render(self, config_file):
3615+ if config_file not in self.templates:
3616+ log('Config not registered: %s' % config_file, level=ERROR)
3617+ raise OSConfigException
3618+ ctxt = self.templates[config_file].context()
3619+
3620+ _tmpl = os.path.basename(config_file)
3621+ try:
3622+ template = self._get_template(_tmpl)
3623+ except exceptions.TemplateNotFound:
3624+ # if no template is found with basename, try looking for it
3625+ # using a munged full path, eg:
3626+ # /etc/apache2/apache2.conf -> etc_apache2_apache2.conf
3627+ _tmpl = '_'.join(config_file.split('/')[1:])
3628+ try:
3629+ template = self._get_template(_tmpl)
3630+ except exceptions.TemplateNotFound as e:
3631+ log('Could not load template from %s by %s or %s.' %
3632+ (self.templates_dir, os.path.basename(config_file), _tmpl),
3633+ level=ERROR)
3634+ raise e
3635+
3636+ log('Rendering from template: %s' % _tmpl, level=INFO)
3637+ return template.render(ctxt)
3638+
3639+ def write(self, config_file):
3640+ """
3641+ Write a single config file, raises if config file is not registered.
3642+ """
3643+ if config_file not in self.templates:
3644+ log('Config not registered: %s' % config_file, level=ERROR)
3645+ raise OSConfigException
3646+
3647+ _out = self.render(config_file)
3648+
3649+ with open(config_file, 'wb') as out:
3650+ out.write(_out)
3651+
3652+ log('Wrote template %s.' % config_file, level=INFO)
3653+
3654+ def write_all(self):
3655+ """
3656+ Write out all registered config files.
3657+ """
3658+ [self.write(k) for k in six.iterkeys(self.templates)]
3659+
3660+ def set_release(self, openstack_release):
3661+ """
3662+ Resets the template environment and generates a new template loader
3663+ based on a the new openstack release.
3664+ """
3665+ self._tmpl_env = None
3666+ self.openstack_release = openstack_release
3667+ self._get_tmpl_env()
3668+
3669+ def complete_contexts(self):
3670+ '''
3671+ Returns a list of context interfaces that yield a complete context.
3672+ '''
3673+ interfaces = []
3674+ [interfaces.extend(i.complete_contexts())
3675+ for i in six.itervalues(self.templates)]
3676+ return interfaces
3677+
3678+ def get_incomplete_context_data(self, interfaces):
3679+ '''
3680+ Return dictionary of relation status of interfaces and any missing
3681+ required context data. Example:
3682+ {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
3683+ 'zeromq-configuration': {'related': False}}
3684+ '''
3685+ incomplete_context_data = {}
3686+
3687+ for i in six.itervalues(self.templates):
3688+ for context in i.contexts:
3689+ for interface in interfaces:
3690+ related = False
3691+ if interface in context.interfaces:
3692+ related = context.get_related()
3693+ missing_data = context.missing_data
3694+ if missing_data:
3695+ incomplete_context_data[interface] = {'missing_data': missing_data}
3696+ if related:
3697+ if incomplete_context_data.get(interface):
3698+ incomplete_context_data[interface].update({'related': True})
3699+ else:
3700+ incomplete_context_data[interface] = {'related': True}
3701+ else:
3702+ incomplete_context_data[interface] = {'related': False}
3703+ return incomplete_context_data
3704
3705=== added file 'hooks/charmhelpers/contrib/openstack/utils.py'
3706--- hooks/charmhelpers/contrib/openstack/utils.py 1970-01-01 00:00:00 +0000
3707+++ hooks/charmhelpers/contrib/openstack/utils.py 2016-02-04 15:46:27 +0000
3708@@ -0,0 +1,982 @@
3709+# Copyright 2014-2015 Canonical Limited.
3710+#
3711+# This file is part of charm-helpers.
3712+#
3713+# charm-helpers is free software: you can redistribute it and/or modify
3714+# it under the terms of the GNU Lesser General Public License version 3 as
3715+# published by the Free Software Foundation.
3716+#
3717+# charm-helpers is distributed in the hope that it will be useful,
3718+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3719+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3720+# GNU Lesser General Public License for more details.
3721+#
3722+# You should have received a copy of the GNU Lesser General Public License
3723+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3724+
3725+# Common python helper functions used for OpenStack charms.
3726+from collections import OrderedDict
3727+from functools import wraps
3728+
3729+import subprocess
3730+import json
3731+import os
3732+import sys
3733+import re
3734+
3735+import six
3736+import traceback
3737+import yaml
3738+
3739+from charmhelpers.contrib.network import ip
3740+
3741+from charmhelpers.core import (
3742+ unitdata,
3743+)
3744+
3745+from charmhelpers.core.hookenv import (
3746+ action_fail,
3747+ action_set,
3748+ config,
3749+ log as juju_log,
3750+ charm_dir,
3751+ INFO,
3752+ relation_ids,
3753+ relation_set,
3754+ status_set,
3755+ hook_name
3756+)
3757+
3758+from charmhelpers.contrib.storage.linux.lvm import (
3759+ deactivate_lvm_volume_group,
3760+ is_lvm_physical_volume,
3761+ remove_lvm_physical_volume,
3762+)
3763+
3764+from charmhelpers.contrib.network.ip import (
3765+ get_ipv6_addr,
3766+ is_ipv6,
3767+)
3768+
3769+from charmhelpers.contrib.python.packages import (
3770+ pip_create_virtualenv,
3771+ pip_install,
3772+)
3773+
3774+from charmhelpers.core.host import lsb_release, mounts, umount
3775+from charmhelpers.fetch import apt_install, apt_cache, install_remote
3776+from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
3777+from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
3778+
3779+CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
3780+CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
3781+
3782+DISTRO_PROPOSED = ('deb http://archive.ubuntu.com/ubuntu/ %s-proposed '
3783+ 'restricted main multiverse universe')
3784+
3785+UBUNTU_OPENSTACK_RELEASE = OrderedDict([
3786+ ('oneiric', 'diablo'),
3787+ ('precise', 'essex'),
3788+ ('quantal', 'folsom'),
3789+ ('raring', 'grizzly'),
3790+ ('saucy', 'havana'),
3791+ ('trusty', 'icehouse'),
3792+ ('utopic', 'juno'),
3793+ ('vivid', 'kilo'),
3794+ ('wily', 'liberty'),
3795+])
3796+
3797+
3798+OPENSTACK_CODENAMES = OrderedDict([
3799+ ('2011.2', 'diablo'),
3800+ ('2012.1', 'essex'),
3801+ ('2012.2', 'folsom'),
3802+ ('2013.1', 'grizzly'),
3803+ ('2013.2', 'havana'),
3804+ ('2014.1', 'icehouse'),
3805+ ('2014.2', 'juno'),
3806+ ('2015.1', 'kilo'),
3807+ ('2015.2', 'liberty'),
3808+])
3809+
3810+# The ugly duckling
3811+SWIFT_CODENAMES = OrderedDict([
3812+ ('1.4.3', 'diablo'),
3813+ ('1.4.8', 'essex'),
3814+ ('1.7.4', 'folsom'),
3815+ ('1.8.0', 'grizzly'),
3816+ ('1.7.7', 'grizzly'),
3817+ ('1.7.6', 'grizzly'),
3818+ ('1.10.0', 'havana'),
3819+ ('1.9.1', 'havana'),
3820+ ('1.9.0', 'havana'),
3821+ ('1.13.1', 'icehouse'),
3822+ ('1.13.0', 'icehouse'),
3823+ ('1.12.0', 'icehouse'),
3824+ ('1.11.0', 'icehouse'),
3825+ ('2.0.0', 'juno'),
3826+ ('2.1.0', 'juno'),
3827+ ('2.2.0', 'juno'),
3828+ ('2.2.1', 'kilo'),
3829+ ('2.2.2', 'kilo'),
3830+ ('2.3.0', 'liberty'),
3831+ ('2.4.0', 'liberty'),
3832+ ('2.5.0', 'liberty'),
3833+])
3834+
3835+# >= Liberty version->codename mapping
3836+PACKAGE_CODENAMES = {
3837+ 'nova-common': OrderedDict([
3838+ ('12.0', 'liberty'),
3839+ ]),
3840+ 'neutron-common': OrderedDict([
3841+ ('7.0', 'liberty'),
3842+ ]),
3843+ 'cinder-common': OrderedDict([
3844+ ('7.0', 'liberty'),
3845+ ]),
3846+ 'keystone': OrderedDict([
3847+ ('8.0', 'liberty'),
3848+ ]),
3849+ 'horizon-common': OrderedDict([
3850+ ('8.0', 'liberty'),
3851+ ]),
3852+ 'ceilometer-common': OrderedDict([
3853+ ('5.0', 'liberty'),
3854+ ]),
3855+ 'heat-common': OrderedDict([
3856+ ('5.0', 'liberty'),
3857+ ]),
3858+ 'glance-common': OrderedDict([
3859+ ('11.0', 'liberty'),
3860+ ]),
3861+ 'openstack-dashboard': OrderedDict([
3862+ ('8.0', 'liberty'),
3863+ ]),
3864+}
3865+
3866+DEFAULT_LOOPBACK_SIZE = '5G'
3867+
3868+
3869+def error_out(msg):
3870+ juju_log("FATAL ERROR: %s" % msg, level='ERROR')
3871+ sys.exit(1)
3872+
3873+
3874+def get_os_codename_install_source(src):
3875+ '''Derive OpenStack release codename from a given installation source.'''
3876+ ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
3877+ rel = ''
3878+ if src is None:
3879+ return rel
3880+ if src in ['distro', 'distro-proposed']:
3881+ try:
3882+ rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel]
3883+ except KeyError:
3884+ e = 'Could not derive openstack release for '\
3885+ 'this Ubuntu release: %s' % ubuntu_rel
3886+ error_out(e)
3887+ return rel
3888+
3889+ if src.startswith('cloud:'):
3890+ ca_rel = src.split(':')[1]
3891+ ca_rel = ca_rel.split('%s-' % ubuntu_rel)[1].split('/')[0]
3892+ return ca_rel
3893+
3894+ # Best guess match based on deb string provided
3895+ if src.startswith('deb') or src.startswith('ppa'):
3896+ for k, v in six.iteritems(OPENSTACK_CODENAMES):
3897+ if v in src:
3898+ return v
3899+
3900+
3901+def get_os_version_install_source(src):
3902+ codename = get_os_codename_install_source(src)
3903+ return get_os_version_codename(codename)
3904+
3905+
3906+def get_os_codename_version(vers):
3907+ '''Determine OpenStack codename from version number.'''
3908+ try:
3909+ return OPENSTACK_CODENAMES[vers]
3910+ except KeyError:
3911+ e = 'Could not determine OpenStack codename for version %s' % vers
3912+ error_out(e)
3913+
3914+
3915+def get_os_version_codename(codename, version_map=OPENSTACK_CODENAMES):
3916+ '''Determine OpenStack version number from codename.'''
3917+ for k, v in six.iteritems(version_map):
3918+ if v == codename:
3919+ return k
3920+ e = 'Could not derive OpenStack version for '\
3921+ 'codename: %s' % codename
3922+ error_out(e)
3923+
3924+
3925+def get_os_codename_package(package, fatal=True):
3926+ '''Derive OpenStack release codename from an installed package.'''
3927+ import apt_pkg as apt
3928+
3929+ cache = apt_cache()
3930+
3931+ try:
3932+ pkg = cache[package]
3933+ except:
3934+ if not fatal:
3935+ return None
3936+ # the package is unknown to the current apt cache.
3937+ e = 'Could not determine version of package with no installation '\
3938+ 'candidate: %s' % package
3939+ error_out(e)
3940+
3941+ if not pkg.current_ver:
3942+ if not fatal:
3943+ return None
3944+ # package is known, but no version is currently installed.
3945+ e = 'Could not determine version of uninstalled package: %s' % package
3946+ error_out(e)
3947+
3948+ vers = apt.upstream_version(pkg.current_ver.ver_str)
3949+ if 'swift' in pkg.name:
3950+ # Fully x.y.z match for swift versions
3951+ match = re.match('^(\d+)\.(\d+)\.(\d+)', vers)
3952+ else:
3953+ # x.y match only for 20XX.X
3954+ # and ignore patch level for other packages
3955+ match = re.match('^(\d+)\.(\d+)', vers)
3956+
3957+ if match:
3958+ vers = match.group(0)
3959+
3960+ # >= Liberty independent project versions
3961+ if (package in PACKAGE_CODENAMES and
3962+ vers in PACKAGE_CODENAMES[package]):
3963+ return PACKAGE_CODENAMES[package][vers]
3964+ else:
3965+ # < Liberty co-ordinated project versions
3966+ try:
3967+ if 'swift' in pkg.name:
3968+ return SWIFT_CODENAMES[vers]
3969+ else:
3970+ return OPENSTACK_CODENAMES[vers]
3971+ except KeyError:
3972+ if not fatal:
3973+ return None
3974+ e = 'Could not determine OpenStack codename for version %s' % vers
3975+ error_out(e)
3976+
3977+
3978+def get_os_version_package(pkg, fatal=True):
3979+ '''Derive OpenStack version number from an installed package.'''
3980+ codename = get_os_codename_package(pkg, fatal=fatal)
3981+
3982+ if not codename:
3983+ return None
3984+
3985+ if 'swift' in pkg:
3986+ vers_map = SWIFT_CODENAMES
3987+ else:
3988+ vers_map = OPENSTACK_CODENAMES
3989+
3990+ for version, cname in six.iteritems(vers_map):
3991+ if cname == codename:
3992+ return version
3993+ # e = "Could not determine OpenStack version for package: %s" % pkg
3994+ # error_out(e)
3995+
3996+
3997+os_rel = None
3998+
3999+
4000+def os_release(package, base='essex'):
4001+ '''
4002+ Returns OpenStack release codename from a cached global.
4003+ If the codename can not be determined from either an installed package or
4004+ the installation source, the earliest release supported by the charm should
4005+ be returned.
4006+ '''
4007+ global os_rel
4008+ if os_rel:
4009+ return os_rel
4010+ os_rel = (get_os_codename_package(package, fatal=False) or
4011+ get_os_codename_install_source(config('openstack-origin')) or
4012+ base)
4013+ return os_rel
4014+
4015+
4016+def import_key(keyid):
4017+ cmd = "apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 " \
4018+ "--recv-keys %s" % keyid
4019+ try:
4020+ subprocess.check_call(cmd.split(' '))
4021+ except subprocess.CalledProcessError:
4022+ error_out("Error importing repo key %s" % keyid)
4023+
4024+
4025+def configure_installation_source(rel):
4026+ '''Configure apt installation source.'''
4027+ if rel == 'distro':
4028+ return
4029+ elif rel == 'distro-proposed':
4030+ ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
4031+ with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
4032+ f.write(DISTRO_PROPOSED % ubuntu_rel)
4033+ elif rel[:4] == "ppa:":
4034+ src = rel
4035+ subprocess.check_call(["add-apt-repository", "-y", src])
4036+ elif rel[:3] == "deb":
4037+ l = len(rel.split('|'))
4038+ if l == 2:
4039+ src, key = rel.split('|')
4040+ juju_log("Importing PPA key from keyserver for %s" % src)
4041+ import_key(key)
4042+ elif l == 1:
4043+ src = rel
4044+ with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
4045+ f.write(src)
4046+ elif rel[:6] == 'cloud:':
4047+ ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
4048+ rel = rel.split(':')[1]
4049+ u_rel = rel.split('-')[0]
4050+ ca_rel = rel.split('-')[1]
4051+
4052+ if u_rel != ubuntu_rel:
4053+ e = 'Cannot install from Cloud Archive pocket %s on this Ubuntu '\
4054+ 'version (%s)' % (ca_rel, ubuntu_rel)
4055+ error_out(e)
4056+
4057+ if 'staging' in ca_rel:
4058+ # staging is just a regular PPA.
4059+ os_rel = ca_rel.split('/')[0]
4060+ ppa = 'ppa:ubuntu-cloud-archive/%s-staging' % os_rel
4061+ cmd = 'add-apt-repository -y %s' % ppa
4062+ subprocess.check_call(cmd.split(' '))
4063+ return
4064+
4065+ # map charm config options to actual archive pockets.
4066+ pockets = {
4067+ 'folsom': 'precise-updates/folsom',
4068+ 'folsom/updates': 'precise-updates/folsom',
4069+ 'folsom/proposed': 'precise-proposed/folsom',
4070+ 'grizzly': 'precise-updates/grizzly',
4071+ 'grizzly/updates': 'precise-updates/grizzly',
4072+ 'grizzly/proposed': 'precise-proposed/grizzly',
4073+ 'havana': 'precise-updates/havana',
4074+ 'havana/updates': 'precise-updates/havana',
4075+ 'havana/proposed': 'precise-proposed/havana',
4076+ 'icehouse': 'precise-updates/icehouse',
4077+ 'icehouse/updates': 'precise-updates/icehouse',
4078+ 'icehouse/proposed': 'precise-proposed/icehouse',
4079+ 'juno': 'trusty-updates/juno',
4080+ 'juno/updates': 'trusty-updates/juno',
4081+ 'juno/proposed': 'trusty-proposed/juno',
4082+ 'kilo': 'trusty-updates/kilo',
4083+ 'kilo/updates': 'trusty-updates/kilo',
4084+ 'kilo/proposed': 'trusty-proposed/kilo',
4085+ 'liberty': 'trusty-updates/liberty',
4086+ 'liberty/updates': 'trusty-updates/liberty',
4087+ 'liberty/proposed': 'trusty-proposed/liberty',
4088+ }
4089+
4090+ try:
4091+ pocket = pockets[ca_rel]
4092+ except KeyError:
4093+ e = 'Invalid Cloud Archive release specified: %s' % rel
4094+ error_out(e)
4095+
4096+ src = "deb %s %s main" % (CLOUD_ARCHIVE_URL, pocket)
4097+ apt_install('ubuntu-cloud-keyring', fatal=True)
4098+
4099+ with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as f:
4100+ f.write(src)
4101+ else:
4102+ error_out("Invalid openstack-release specified: %s" % rel)
4103+
4104+
4105+def config_value_changed(option):
4106+ """
4107+ Determine if config value changed since last call to this function.
4108+ """
4109+ hook_data = unitdata.HookData()
4110+ with hook_data():
4111+ db = unitdata.kv()
4112+ current = config(option)
4113+ saved = db.get(option)
4114+ db.set(option, current)
4115+ if saved is None:
4116+ return False
4117+ return current != saved
4118+
4119+
4120+def save_script_rc(script_path="scripts/scriptrc", **env_vars):
4121+ """
4122+ Write an rc file in the charm-delivered directory containing
4123+ exported environment variables provided by env_vars. Any charm scripts run
4124+ outside the juju hook environment can source this scriptrc to obtain
4125+ updated config information necessary to perform health checks or
4126+ service changes.
4127+ """
4128+ juju_rc_path = "%s/%s" % (charm_dir(), script_path)
4129+ if not os.path.exists(os.path.dirname(juju_rc_path)):
4130+ os.mkdir(os.path.dirname(juju_rc_path))
4131+ with open(juju_rc_path, 'wb') as rc_script:
4132+ rc_script.write(
4133+ "#!/bin/bash\n")
4134+ [rc_script.write('export %s=%s\n' % (u, p))
4135+ for u, p in six.iteritems(env_vars) if u != "script_path"]
4136+
4137+
4138+def openstack_upgrade_available(package):
4139+ """
4140+ Determines if an OpenStack upgrade is available from installation
4141+ source, based on version of installed package.
4142+
4143+ :param package: str: Name of installed package.
4144+
4145+ :returns: bool: : Returns True if configured installation source offers
4146+ a newer version of package.
4147+
4148+ """
4149+
4150+ import apt_pkg as apt
4151+ src = config('openstack-origin')
4152+ cur_vers = get_os_version_package(package)
4153+ if "swift" in package:
4154+ codename = get_os_codename_install_source(src)
4155+ available_vers = get_os_version_codename(codename, SWIFT_CODENAMES)
4156+ else:
4157+ available_vers = get_os_version_install_source(src)
4158+ apt.init()
4159+ return apt.version_compare(available_vers, cur_vers) == 1
4160+
4161+
4162+def ensure_block_device(block_device):
4163+ '''
4164+ Confirm block_device, create as loopback if necessary.
4165+
4166+ :param block_device: str: Full path of block device to ensure.
4167+
4168+ :returns: str: Full path of ensured block device.
4169+ '''
4170+ _none = ['None', 'none', None]
4171+ if (block_device in _none):
4172+ error_out('prepare_storage(): Missing required input: block_device=%s.'
4173+ % block_device)
4174+
4175+ if block_device.startswith('/dev/'):
4176+ bdev = block_device
4177+ elif block_device.startswith('/'):
4178+ _bd = block_device.split('|')
4179+ if len(_bd) == 2:
4180+ bdev, size = _bd
4181+ else:
4182+ bdev = block_device
4183+ size = DEFAULT_LOOPBACK_SIZE
4184+ bdev = ensure_loopback_device(bdev, size)
4185+ else:
4186+ bdev = '/dev/%s' % block_device
4187+
4188+ if not is_block_device(bdev):
4189+ error_out('Failed to locate valid block device at %s' % bdev)
4190+
4191+ return bdev
4192+
4193+
4194+def clean_storage(block_device):
4195+ '''
4196+ Ensures a block device is clean. That is:
4197+ - unmounted
4198+ - any lvm volume groups are deactivated
4199+ - any lvm physical device signatures removed
4200+ - partition table wiped
4201+
4202+ :param block_device: str: Full path to block device to clean.
4203+ '''
4204+ for mp, d in mounts():
4205+ if d == block_device:
4206+ juju_log('clean_storage(): %s is mounted @ %s, unmounting.' %
4207+ (d, mp), level=INFO)
4208+ umount(mp, persist=True)
4209+
4210+ if is_lvm_physical_volume(block_device):
4211+ deactivate_lvm_volume_group(block_device)
4212+ remove_lvm_physical_volume(block_device)
4213+ else:
4214+ zap_disk(block_device)
4215+
4216+is_ip = ip.is_ip
4217+ns_query = ip.ns_query
4218+get_host_ip = ip.get_host_ip
4219+get_hostname = ip.get_hostname
4220+
4221+
4222+def get_matchmaker_map(mm_file='/etc/oslo/matchmaker_ring.json'):
4223+ mm_map = {}
4224+ if os.path.isfile(mm_file):
4225+ with open(mm_file, 'r') as f:
4226+ mm_map = json.load(f)
4227+ return mm_map
4228+
4229+
4230+def sync_db_with_multi_ipv6_addresses(database, database_user,
4231+ relation_prefix=None):
4232+ hosts = get_ipv6_addr(dynamic_only=False)
4233+
4234+ if config('vip'):
4235+ vips = config('vip').split()
4236+ for vip in vips:
4237+ if vip and is_ipv6(vip):
4238+ hosts.append(vip)
4239+
4240+ kwargs = {'database': database,
4241+ 'username': database_user,
4242+ 'hostname': json.dumps(hosts)}
4243+
4244+ if relation_prefix:
4245+ for key in list(kwargs.keys()):
4246+ kwargs["%s_%s" % (relation_prefix, key)] = kwargs[key]
4247+ del kwargs[key]
4248+
4249+ for rid in relation_ids('shared-db'):
4250+ relation_set(relation_id=rid, **kwargs)
4251+
4252+
4253+def os_requires_version(ostack_release, pkg):
4254+ """
4255+ Decorator for hook to specify minimum supported release
4256+ """
4257+ def wrap(f):
4258+ @wraps(f)
4259+ def wrapped_f(*args):
4260+ if os_release(pkg) < ostack_release:
4261+ raise Exception("This hook is not supported on releases"
4262+ " before %s" % ostack_release)
4263+ f(*args)
4264+ return wrapped_f
4265+ return wrap
4266+
4267+
4268+def git_install_requested():
4269+ """
4270+ Returns true if openstack-origin-git is specified.
4271+ """
4272+ return config('openstack-origin-git') is not None
4273+
4274+
4275+requirements_dir = None
4276+
4277+
4278+def _git_yaml_load(projects_yaml):
4279+ """
4280+ Load the specified yaml into a dictionary.
4281+ """
4282+ if not projects_yaml:
4283+ return None
4284+
4285+ return yaml.load(projects_yaml)
4286+
4287+
4288+def git_clone_and_install(projects_yaml, core_project, depth=1):
4289+ """
4290+ Clone/install all specified OpenStack repositories.
4291+
4292+ The expected format of projects_yaml is:
4293+
4294+ repositories:
4295+ - {name: keystone,
4296+ repository: 'git://git.openstack.org/openstack/keystone.git',
4297+ branch: 'stable/icehouse'}
4298+ - {name: requirements,
4299+ repository: 'git://git.openstack.org/openstack/requirements.git',
4300+ branch: 'stable/icehouse'}
4301+
4302+ directory: /mnt/openstack-git
4303+ http_proxy: squid-proxy-url
4304+ https_proxy: squid-proxy-url
4305+
4306+ The directory, http_proxy, and https_proxy keys are optional.
4307+
4308+ """
4309+ global requirements_dir
4310+ parent_dir = '/mnt/openstack-git'
4311+ http_proxy = None
4312+
4313+ projects = _git_yaml_load(projects_yaml)
4314+ _git_validate_projects_yaml(projects, core_project)
4315+
4316+ old_environ = dict(os.environ)
4317+
4318+ if 'http_proxy' in projects.keys():
4319+ http_proxy = projects['http_proxy']
4320+ os.environ['http_proxy'] = projects['http_proxy']
4321+ if 'https_proxy' in projects.keys():
4322+ os.environ['https_proxy'] = projects['https_proxy']
4323+
4324+ if 'directory' in projects.keys():
4325+ parent_dir = projects['directory']
4326+
4327+ pip_create_virtualenv(os.path.join(parent_dir, 'venv'))
4328+
4329+ # Upgrade setuptools and pip from default virtualenv versions. The default
4330+ # versions in trusty break master OpenStack branch deployments.
4331+ for p in ['pip', 'setuptools']:
4332+ pip_install(p, upgrade=True, proxy=http_proxy,
4333+ venv=os.path.join(parent_dir, 'venv'))
4334+
4335+ for p in projects['repositories']:
4336+ repo = p['repository']
4337+ branch = p['branch']
4338+ if p['name'] == 'requirements':
4339+ repo_dir = _git_clone_and_install_single(repo, branch, depth,
4340+ parent_dir, http_proxy,
4341+ update_requirements=False)
4342+ requirements_dir = repo_dir
4343+ else:
4344+ repo_dir = _git_clone_and_install_single(repo, branch, depth,
4345+ parent_dir, http_proxy,
4346+ update_requirements=True)
4347+
4348+ os.environ = old_environ
4349+
4350+
4351+def _git_validate_projects_yaml(projects, core_project):
4352+ """
4353+ Validate the projects yaml.
4354+ """
4355+ _git_ensure_key_exists('repositories', projects)
4356+
4357+ for project in projects['repositories']:
4358+ _git_ensure_key_exists('name', project.keys())
4359+ _git_ensure_key_exists('repository', project.keys())
4360+ _git_ensure_key_exists('branch', project.keys())
4361+
4362+ if projects['repositories'][0]['name'] != 'requirements':
4363+ error_out('{} git repo must be specified first'.format('requirements'))
4364+
4365+ if projects['repositories'][-1]['name'] != core_project:
4366+ error_out('{} git repo must be specified last'.format(core_project))
4367+
4368+
4369+def _git_ensure_key_exists(key, keys):
4370+ """
4371+ Ensure that key exists in keys.
4372+ """
4373+ if key not in keys:
4374+ error_out('openstack-origin-git key \'{}\' is missing'.format(key))
4375+
4376+
4377+def _git_clone_and_install_single(repo, branch, depth, parent_dir, http_proxy,
4378+ update_requirements):
4379+ """
4380+ Clone and install a single git repository.
4381+ """
4382+ dest_dir = os.path.join(parent_dir, os.path.basename(repo))
4383+
4384+ if not os.path.exists(parent_dir):
4385+ juju_log('Directory already exists at {}. '
4386+ 'No need to create directory.'.format(parent_dir))
4387+ os.mkdir(parent_dir)
4388+
4389+ if not os.path.exists(dest_dir):
4390+ juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
4391+ repo_dir = install_remote(repo, dest=parent_dir, branch=branch,
4392+ depth=depth)
4393+ else:
4394+ repo_dir = dest_dir
4395+
4396+ venv = os.path.join(parent_dir, 'venv')
4397+
4398+ if update_requirements:
4399+ if not requirements_dir:
4400+ error_out('requirements repo must be cloned before '
4401+ 'updating from global requirements.')
4402+ _git_update_requirements(venv, repo_dir, requirements_dir)
4403+
4404+ juju_log('Installing git repo from dir: {}'.format(repo_dir))
4405+ if http_proxy:
4406+ pip_install(repo_dir, proxy=http_proxy, venv=venv)
4407+ else:
4408+ pip_install(repo_dir, venv=venv)
4409+
4410+ return repo_dir
4411+
4412+
4413+def _git_update_requirements(venv, package_dir, reqs_dir):
4414+ """
4415+ Update from global requirements.
4416+
4417+ Update an OpenStack git directory's requirements.txt and
4418+ test-requirements.txt from global-requirements.txt.
4419+ """
4420+ orig_dir = os.getcwd()
4421+ os.chdir(reqs_dir)
4422+ python = os.path.join(venv, 'bin/python')
4423+ cmd = [python, 'update.py', package_dir]
4424+ try:
4425+ subprocess.check_call(cmd)
4426+ except subprocess.CalledProcessError:
4427+ package = os.path.basename(package_dir)
4428+ error_out("Error updating {} from "
4429+ "global-requirements.txt".format(package))
4430+ os.chdir(orig_dir)
4431+
4432+
4433+def git_pip_venv_dir(projects_yaml):
4434+ """
4435+ Return the pip virtualenv path.
4436+ """
4437+ parent_dir = '/mnt/openstack-git'
4438+
4439+ projects = _git_yaml_load(projects_yaml)
4440+
4441+ if 'directory' in projects.keys():
4442+ parent_dir = projects['directory']
4443+
4444+ return os.path.join(parent_dir, 'venv')
4445+
4446+
4447+def git_src_dir(projects_yaml, project):
4448+ """
4449+ Return the directory where the specified project's source is located.
4450+ """
4451+ parent_dir = '/mnt/openstack-git'
4452+
4453+ projects = _git_yaml_load(projects_yaml)
4454+
4455+ if 'directory' in projects.keys():
4456+ parent_dir = projects['directory']
4457+
4458+ for p in projects['repositories']:
4459+ if p['name'] == project:
4460+ return os.path.join(parent_dir, os.path.basename(p['repository']))
4461+
4462+ return None
4463+
4464+
4465+def git_yaml_value(projects_yaml, key):
4466+ """
4467+ Return the value in projects_yaml for the specified key.
4468+ """
4469+ projects = _git_yaml_load(projects_yaml)
4470+
4471+ if key in projects.keys():
4472+ return projects[key]
4473+
4474+ return None
4475+
4476+
4477+def os_workload_status(configs, required_interfaces, charm_func=None):
4478+ """
4479+ Decorator to set workload status based on complete contexts
4480+ """
4481+ def wrap(f):
4482+ @wraps(f)
4483+ def wrapped_f(*args, **kwargs):
4484+ # Run the original function first
4485+ f(*args, **kwargs)
4486+ # Set workload status now that contexts have been
4487+ # acted on
4488+ set_os_workload_status(configs, required_interfaces, charm_func)
4489+ return wrapped_f
4490+ return wrap
4491+
4492+
4493+def set_os_workload_status(configs, required_interfaces, charm_func=None):
4494+ """
4495+ Set workload status based on complete contexts.
4496+ status-set missing or incomplete contexts
4497+ and juju-log details of missing required data.
4498+ charm_func is a charm specific function to run checking
4499+ for charm specific requirements such as a VIP setting.
4500+ """
4501+ incomplete_rel_data = incomplete_relation_data(configs, required_interfaces)
4502+ state = 'active'
4503+ missing_relations = []
4504+ incomplete_relations = []
4505+ message = None
4506+ charm_state = None
4507+ charm_message = None
4508+
4509+ for generic_interface in incomplete_rel_data.keys():
4510+ related_interface = None
4511+ missing_data = {}
4512+ # Related or not?
4513+ for interface in incomplete_rel_data[generic_interface]:
4514+ if incomplete_rel_data[generic_interface][interface].get('related'):
4515+ related_interface = interface
4516+ missing_data = incomplete_rel_data[generic_interface][interface].get('missing_data')
4517+ # No relation ID for the generic_interface
4518+ if not related_interface:
4519+ juju_log("{} relation is missing and must be related for "
4520+ "functionality. ".format(generic_interface), 'WARN')
4521+ state = 'blocked'
4522+ if generic_interface not in missing_relations:
4523+ missing_relations.append(generic_interface)
4524+ else:
4525+ # Relation ID exists but no related unit
4526+ if not missing_data:
4527+ # Edge case relation ID exists but departing
4528+ if ('departed' in hook_name() or 'broken' in hook_name()) \
4529+ and related_interface in hook_name():
4530+ state = 'blocked'
4531+ if generic_interface not in missing_relations:
4532+ missing_relations.append(generic_interface)
4533+ juju_log("{} relation's interface, {}, "
4534+ "relationship is departed or broken "
4535+ "and is required for functionality."
4536+ "".format(generic_interface, related_interface), "WARN")
4537+ # Normal case relation ID exists but no related unit
4538+ # (joining)
4539+ else:
4540+ juju_log("{} relations's interface, {}, is related but has "
4541+ "no units in the relation."
4542+ "".format(generic_interface, related_interface), "INFO")
4543+ # Related unit exists and data missing on the relation
4544+ else:
4545+ juju_log("{} relation's interface, {}, is related awaiting "
4546+ "the following data from the relationship: {}. "
4547+ "".format(generic_interface, related_interface,
4548+ ", ".join(missing_data)), "INFO")
4549+ if state != 'blocked':
4550+ state = 'waiting'
4551+ if generic_interface not in incomplete_relations \
4552+ and generic_interface not in missing_relations:
4553+ incomplete_relations.append(generic_interface)
4554+
4555+ if missing_relations:
4556+ message = "Missing relations: {}".format(", ".join(missing_relations))
4557+ if incomplete_relations:
4558+ message += "; incomplete relations: {}" \
4559+ "".format(", ".join(incomplete_relations))
4560+ state = 'blocked'
4561+ elif incomplete_relations:
4562+ message = "Incomplete relations: {}" \
4563+ "".format(", ".join(incomplete_relations))
4564+ state = 'waiting'
4565+
4566+ # Run charm specific checks
4567+ if charm_func:
4568+ charm_state, charm_message = charm_func(configs)
4569+ if charm_state != 'active' and charm_state != 'unknown':
4570+ state = workload_state_compare(state, charm_state)
4571+ if message:
4572+ charm_message = charm_message.replace("Incomplete relations: ",
4573+ "")
4574+ message = "{}, {}".format(message, charm_message)
4575+ else:
4576+ message = charm_message
4577+
4578+ # Set to active if all requirements have been met
4579+ if state == 'active':
4580+ message = "Unit is ready"
4581+ juju_log(message, "INFO")
4582+
4583+ status_set(state, message)
4584+
4585+
4586+def workload_state_compare(current_workload_state, workload_state):
4587+ """ Return highest priority of two states"""
4588+ hierarchy = {'unknown': -1,
4589+ 'active': 0,
4590+ 'maintenance': 1,
4591+ 'waiting': 2,
4592+ 'blocked': 3,
4593+ }
4594+
4595+ if hierarchy.get(workload_state) is None:
4596+ workload_state = 'unknown'
4597+ if hierarchy.get(current_workload_state) is None:
4598+ current_workload_state = 'unknown'
4599+
4600+ # Set workload_state based on hierarchy of statuses
4601+ if hierarchy.get(current_workload_state) > hierarchy.get(workload_state):
4602+ return current_workload_state
4603+ else:
4604+ return workload_state
4605+
4606+
4607+def incomplete_relation_data(configs, required_interfaces):
4608+ """
4609+ Check complete contexts against required_interfaces
4610+ Return dictionary of incomplete relation data.
4611+
4612+ configs is an OSConfigRenderer object with configs registered
4613+
4614+ required_interfaces is a dictionary of required general interfaces
4615+ with dictionary values of possible specific interfaces.
4616+ Example:
4617+ required_interfaces = {'database': ['shared-db', 'pgsql-db']}
4618+
4619+ The interface is said to be satisfied if anyone of the interfaces in the
4620+ list has a complete context.
4621+
4622+ Return dictionary of incomplete or missing required contexts with relation
4623+ status of interfaces and any missing data points. Example:
4624+ {'message':
4625+ {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
4626+ 'zeromq-configuration': {'related': False}},
4627+ 'identity':
4628+ {'identity-service': {'related': False}},
4629+ 'database':
4630+ {'pgsql-db': {'related': False},
4631+ 'shared-db': {'related': True}}}
4632+ """
4633+ complete_ctxts = configs.complete_contexts()
4634+ incomplete_relations = []
4635+ for svc_type in required_interfaces.keys():
4636+ # Avoid duplicates
4637+ found_ctxt = False
4638+ for interface in required_interfaces[svc_type]:
4639+ if interface in complete_ctxts:
4640+ found_ctxt = True
4641+ if not found_ctxt:
4642+ incomplete_relations.append(svc_type)
4643+ incomplete_context_data = {}
4644+ for i in incomplete_relations:
4645+ incomplete_context_data[i] = configs.get_incomplete_context_data(required_interfaces[i])
4646+ return incomplete_context_data
4647+
4648+
4649+def do_action_openstack_upgrade(package, upgrade_callback, configs):
4650+ """Perform action-managed OpenStack upgrade.
4651+
4652+ Upgrades packages to the configured openstack-origin version and sets
4653+ the corresponding action status as a result.
4654+
4655+ If the charm was installed from source we cannot upgrade it.
4656+ For backwards compatibility a config flag (action-managed-upgrade) must
4657+ be set for this code to run, otherwise a full service level upgrade will
4658+ fire on config-changed.
4659+
4660+ @param package: package name for determining if upgrade available
4661+ @param upgrade_callback: function callback to charm's upgrade function
4662+ @param configs: templating object derived from OSConfigRenderer class
4663+
4664+ @return: True if upgrade successful; False if upgrade failed or skipped
4665+ """
4666+ ret = False
4667+
4668+ if git_install_requested():
4669+ action_set({'outcome': 'installed from source, skipped upgrade.'})
4670+ else:
4671+ if openstack_upgrade_available(package):
4672+ if config('action-managed-upgrade'):
4673+ juju_log('Upgrading OpenStack release')
4674+
4675+ try:
4676+ upgrade_callback(configs=configs)
4677+ action_set({'outcome': 'success, upgrade completed.'})
4678+ ret = True
4679+ except:
4680+ action_set({'outcome': 'upgrade failed, see traceback.'})
4681+ action_set({'traceback': traceback.format_exc()})
4682+ action_fail('do_openstack_upgrade resulted in an '
4683+ 'unexpected error')
4684+ else:
4685+ action_set({'outcome': 'action-managed-upgrade config is '
4686+ 'False, skipped upgrade.'})
4687+ else:
4688+ action_set({'outcome': 'no upgrade available.'})
4689+
4690+ return ret
4691
4692=== added directory 'hooks/charmhelpers/contrib/python'
4693=== added file 'hooks/charmhelpers/contrib/python/__init__.py'
4694--- hooks/charmhelpers/contrib/python/__init__.py 1970-01-01 00:00:00 +0000
4695+++ hooks/charmhelpers/contrib/python/__init__.py 2016-02-04 15:46:27 +0000
4696@@ -0,0 +1,15 @@
4697+# Copyright 2014-2015 Canonical Limited.
4698+#
4699+# This file is part of charm-helpers.
4700+#
4701+# charm-helpers is free software: you can redistribute it and/or modify
4702+# it under the terms of the GNU Lesser General Public License version 3 as
4703+# published by the Free Software Foundation.
4704+#
4705+# charm-helpers is distributed in the hope that it will be useful,
4706+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4707+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4708+# GNU Lesser General Public License for more details.
4709+#
4710+# You should have received a copy of the GNU Lesser General Public License
4711+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4712
4713=== added file 'hooks/charmhelpers/contrib/python/debug.py'
4714--- hooks/charmhelpers/contrib/python/debug.py 1970-01-01 00:00:00 +0000
4715+++ hooks/charmhelpers/contrib/python/debug.py 2016-02-04 15:46:27 +0000
4716@@ -0,0 +1,56 @@
4717+#!/usr/bin/env python
4718+# coding: utf-8
4719+
4720+# Copyright 2014-2015 Canonical Limited.
4721+#
4722+# This file is part of charm-helpers.
4723+#
4724+# charm-helpers is free software: you can redistribute it and/or modify
4725+# it under the terms of the GNU Lesser General Public License version 3 as
4726+# published by the Free Software Foundation.
4727+#
4728+# charm-helpers is distributed in the hope that it will be useful,
4729+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4730+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4731+# GNU Lesser General Public License for more details.
4732+#
4733+# You should have received a copy of the GNU Lesser General Public License
4734+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4735+
4736+from __future__ import print_function
4737+
4738+import atexit
4739+import sys
4740+
4741+from charmhelpers.contrib.python.rpdb import Rpdb
4742+from charmhelpers.core.hookenv import (
4743+ open_port,
4744+ close_port,
4745+ ERROR,
4746+ log
4747+)
4748+
4749+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
4750+
4751+DEFAULT_ADDR = "0.0.0.0"
4752+DEFAULT_PORT = 4444
4753+
4754+
4755+def _error(message):
4756+ log(message, level=ERROR)
4757+
4758+
4759+def set_trace(addr=DEFAULT_ADDR, port=DEFAULT_PORT):
4760+ """
4761+ Set a trace point using the remote debugger
4762+ """
4763+ atexit.register(close_port, port)
4764+ try:
4765+ log("Starting a remote python debugger session on %s:%s" % (addr,
4766+ port))
4767+ open_port(port)
4768+ debugger = Rpdb(addr=addr, port=port)
4769+ debugger.set_trace(sys._getframe().f_back)
4770+ except:
4771+ _error("Cannot start a remote debug session on %s:%s" % (addr,
4772+ port))
4773
4774=== added file 'hooks/charmhelpers/contrib/python/packages.py'
4775--- hooks/charmhelpers/contrib/python/packages.py 1970-01-01 00:00:00 +0000
4776+++ hooks/charmhelpers/contrib/python/packages.py 2016-02-04 15:46:27 +0000
4777@@ -0,0 +1,121 @@
4778+#!/usr/bin/env python
4779+# coding: utf-8
4780+
4781+# Copyright 2014-2015 Canonical Limited.
4782+#
4783+# This file is part of charm-helpers.
4784+#
4785+# charm-helpers is free software: you can redistribute it and/or modify
4786+# it under the terms of the GNU Lesser General Public License version 3 as
4787+# published by the Free Software Foundation.
4788+#
4789+# charm-helpers is distributed in the hope that it will be useful,
4790+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4791+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4792+# GNU Lesser General Public License for more details.
4793+#
4794+# You should have received a copy of the GNU Lesser General Public License
4795+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4796+
4797+import os
4798+import subprocess
4799+
4800+from charmhelpers.fetch import apt_install, apt_update
4801+from charmhelpers.core.hookenv import charm_dir, log
4802+
4803+try:
4804+ from pip import main as pip_execute
4805+except ImportError:
4806+ apt_update()
4807+ apt_install('python-pip')
4808+ from pip import main as pip_execute
4809+
4810+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
4811+
4812+
4813+def parse_options(given, available):
4814+ """Given a set of options, check if available"""
4815+ for key, value in sorted(given.items()):
4816+ if not value:
4817+ continue
4818+ if key in available:
4819+ yield "--{0}={1}".format(key, value)
4820+
4821+
4822+def pip_install_requirements(requirements, **options):
4823+ """Install a requirements file """
4824+ command = ["install"]
4825+
4826+ available_options = ('proxy', 'src', 'log', )
4827+ for option in parse_options(options, available_options):
4828+ command.append(option)
4829+
4830+ command.append("-r {0}".format(requirements))
4831+ log("Installing from file: {} with options: {}".format(requirements,
4832+ command))
4833+ pip_execute(command)
4834+
4835+
4836+def pip_install(package, fatal=False, upgrade=False, venv=None, **options):
4837+ """Install a python package"""
4838+ if venv:
4839+ venv_python = os.path.join(venv, 'bin/pip')
4840+ command = [venv_python, "install"]
4841+ else:
4842+ command = ["install"]
4843+
4844+ available_options = ('proxy', 'src', 'log', 'index-url', )
4845+ for option in parse_options(options, available_options):
4846+ command.append(option)
4847+
4848+ if upgrade:
4849+ command.append('--upgrade')
4850+
4851+ if isinstance(package, list):
4852+ command.extend(package)
4853+ else:
4854+ command.append(package)
4855+
4856+ log("Installing {} package with options: {}".format(package,
4857+ command))
4858+ if venv:
4859+ subprocess.check_call(command)
4860+ else:
4861+ pip_execute(command)
4862+
4863+
4864+def pip_uninstall(package, **options):
4865+ """Uninstall a python package"""
4866+ command = ["uninstall", "-q", "-y"]
4867+
4868+ available_options = ('proxy', 'log', )
4869+ for option in parse_options(options, available_options):
4870+ command.append(option)
4871+
4872+ if isinstance(package, list):
4873+ command.extend(package)
4874+ else:
4875+ command.append(package)
4876+
4877+ log("Uninstalling {} package with options: {}".format(package,
4878+ command))
4879+ pip_execute(command)
4880+
4881+
4882+def pip_list():
4883+ """Returns the list of current python installed packages
4884+ """
4885+ return pip_execute(["list"])
4886+
4887+
4888+def pip_create_virtualenv(path=None):
4889+ """Create an isolated Python environment."""
4890+ apt_install('python-virtualenv')
4891+
4892+ if path:
4893+ venv_path = path
4894+ else:
4895+ venv_path = os.path.join(charm_dir(), 'venv')
4896+
4897+ if not os.path.exists(venv_path):
4898+ subprocess.check_call(['virtualenv', venv_path])
4899
4900=== added file 'hooks/charmhelpers/contrib/python/rpdb.py'
4901--- hooks/charmhelpers/contrib/python/rpdb.py 1970-01-01 00:00:00 +0000
4902+++ hooks/charmhelpers/contrib/python/rpdb.py 2016-02-04 15:46:27 +0000
4903@@ -0,0 +1,58 @@
4904+# Copyright 2014-2015 Canonical Limited.
4905+#
4906+# This file is part of charm-helpers.
4907+#
4908+# charm-helpers is free software: you can redistribute it and/or modify
4909+# it under the terms of the GNU Lesser General Public License version 3 as
4910+# published by the Free Software Foundation.
4911+#
4912+# charm-helpers is distributed in the hope that it will be useful,
4913+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4914+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4915+# GNU Lesser General Public License for more details.
4916+#
4917+# You should have received a copy of the GNU Lesser General Public License
4918+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4919+
4920+"""Remote Python Debugger (pdb wrapper)."""
4921+
4922+import pdb
4923+import socket
4924+import sys
4925+
4926+__author__ = "Bertrand Janin <b@janin.com>"
4927+__version__ = "0.1.3"
4928+
4929+
4930+class Rpdb(pdb.Pdb):
4931+
4932+ def __init__(self, addr="127.0.0.1", port=4444):
4933+ """Initialize the socket and initialize pdb."""
4934+
4935+ # Backup stdin and stdout before replacing them by the socket handle
4936+ self.old_stdout = sys.stdout
4937+ self.old_stdin = sys.stdin
4938+
4939+ # Open a 'reusable' socket to let the webapp reload on the same port
4940+ self.skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
4941+ self.skt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
4942+ self.skt.bind((addr, port))
4943+ self.skt.listen(1)
4944+ (clientsocket, address) = self.skt.accept()
4945+ handle = clientsocket.makefile('rw')
4946+ pdb.Pdb.__init__(self, completekey='tab', stdin=handle, stdout=handle)
4947+ sys.stdout = sys.stdin = handle
4948+
4949+ def shutdown(self):
4950+ """Revert stdin and stdout, close the socket."""
4951+ sys.stdout = self.old_stdout
4952+ sys.stdin = self.old_stdin
4953+ self.skt.close()
4954+ self.set_continue()
4955+
4956+ def do_continue(self, arg):
4957+ """Stop all operation on ``continue``."""
4958+ self.shutdown()
4959+ return 1
4960+
4961+ do_EOF = do_quit = do_exit = do_c = do_cont = do_continue
4962
4963=== added file 'hooks/charmhelpers/contrib/python/version.py'
4964--- hooks/charmhelpers/contrib/python/version.py 1970-01-01 00:00:00 +0000
4965+++ hooks/charmhelpers/contrib/python/version.py 2016-02-04 15:46:27 +0000
4966@@ -0,0 +1,34 @@
4967+#!/usr/bin/env python
4968+# coding: utf-8
4969+
4970+# Copyright 2014-2015 Canonical Limited.
4971+#
4972+# This file is part of charm-helpers.
4973+#
4974+# charm-helpers is free software: you can redistribute it and/or modify
4975+# it under the terms of the GNU Lesser General Public License version 3 as
4976+# published by the Free Software Foundation.
4977+#
4978+# charm-helpers is distributed in the hope that it will be useful,
4979+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4980+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4981+# GNU Lesser General Public License for more details.
4982+#
4983+# You should have received a copy of the GNU Lesser General Public License
4984+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
4985+
4986+import sys
4987+
4988+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
4989+
4990+
4991+def current_version():
4992+ """Current system python version"""
4993+ return sys.version_info
4994+
4995+
4996+def current_version_string():
4997+ """Current system python version as string major.minor.micro"""
4998+ return "{0}.{1}.{2}".format(sys.version_info.major,
4999+ sys.version_info.minor,
5000+ sys.version_info.micro)
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: