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