Merge lp:~freyes/charms/trusty/memcached/py3-tox into lp:charms/trusty/memcached
- Trusty Tahr (14.04)
- py3-tox
- Merge into trunk
Proposed by Felipe Reyes
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 74 | ||||
Proposed branch: | lp:~freyes/charms/trusty/memcached/py3-tox | ||||
Merge into: | lp:charms/trusty/memcached | ||||
Diff against target: | 4386 lines (+2276/-937) 45 files modified .bzrignore (+1/-0) .coveragerc (+6/-0) charm-helpers.yaml (+1/-0) hooks/charmhelpers/__init__.py (+11/-13) hooks/charmhelpers/contrib/__init__.py (+11/-13) hooks/charmhelpers/contrib/hahelpers/__init__.py (+11/-13) hooks/charmhelpers/contrib/hahelpers/apache.py (+30/-17) hooks/charmhelpers/contrib/hahelpers/cluster.py (+70/-23) hooks/charmhelpers/contrib/network/__init__.py (+11/-13) hooks/charmhelpers/contrib/network/ip.py (+90/-43) hooks/charmhelpers/contrib/network/ovs/__init__.py (+17/-15) hooks/charmhelpers/contrib/network/ufw.py (+16/-19) hooks/charmhelpers/core/__init__.py (+11/-13) hooks/charmhelpers/core/decorators.py (+11/-13) hooks/charmhelpers/core/files.py (+43/-0) hooks/charmhelpers/core/fstab.py (+11/-13) hooks/charmhelpers/core/hookenv.py (+247/-27) hooks/charmhelpers/core/host.py (+396/-130) hooks/charmhelpers/core/host_factory/centos.py (+56/-0) hooks/charmhelpers/core/host_factory/ubuntu.py (+56/-0) hooks/charmhelpers/core/hugepage.py (+69/-0) hooks/charmhelpers/core/kernel.py (+72/-0) hooks/charmhelpers/core/kernel_factory/centos.py (+17/-0) hooks/charmhelpers/core/kernel_factory/ubuntu.py (+13/-0) hooks/charmhelpers/core/services/__init__.py (+11/-13) hooks/charmhelpers/core/services/base.py (+11/-13) hooks/charmhelpers/core/services/helpers.py (+41/-18) hooks/charmhelpers/core/strutils.py (+41/-13) hooks/charmhelpers/core/sysctl.py (+11/-13) hooks/charmhelpers/core/templating.py (+40/-24) hooks/charmhelpers/core/unitdata.py (+72/-31) hooks/charmhelpers/fetch/__init__.py (+45/-296) hooks/charmhelpers/fetch/archiveurl.py (+19/-15) hooks/charmhelpers/fetch/bzrurl.py (+48/-50) hooks/charmhelpers/fetch/centos.py (+171/-0) hooks/charmhelpers/fetch/giturl.py (+34/-38) hooks/charmhelpers/fetch/ubuntu.py (+313/-0) hooks/charmhelpers/osplatform.py (+19/-0) hooks/memcached_hooks.py (+1/-1) setup.cfg (+5/-0) test-requirements-py2.txt (+9/-0) test-requirements-py3.txt (+7/-0) tox.ini (+38/-0) unit_tests/test_memcached_hooks.py (+62/-46) unit_tests/test_utils.py (+1/-1) | ||||
To merge this branch: | bzr merge lp:~freyes/charms/trusty/memcached/py3-tox | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jorge Niedbalski (community) | Approve | ||
charmers | Pending | ||
Review via email: |
Commit message
Description of the change
This patch enables the charm to use python3, with this change it's possible to deploy trusty or xenial.
This change was tested with juju 2.0 and juju 1.0
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2014-12-09 21:43:34 +0000 |
3 | +++ .bzrignore 2016-09-16 19:19:06 +0000 |
4 | @@ -3,3 +3,4 @@ |
5 | bin/ |
6 | .coverage |
7 | .venv |
8 | +.tox |
9 | |
10 | === added file '.coveragerc' |
11 | --- .coveragerc 1970-01-01 00:00:00 +0000 |
12 | +++ .coveragerc 2016-09-16 19:19:06 +0000 |
13 | @@ -0,0 +1,6 @@ |
14 | +[report] |
15 | +# Regexes for lines to exclude from consideration |
16 | +exclude_lines = |
17 | + if __name__ == .__main__.: |
18 | +include= |
19 | + hooks/memcached* |
20 | |
21 | === modified file 'charm-helpers.yaml' |
22 | --- charm-helpers.yaml 2015-02-13 12:32:01 +0000 |
23 | +++ charm-helpers.yaml 2016-09-16 19:19:06 +0000 |
24 | @@ -5,3 +5,4 @@ |
25 | - core |
26 | - contrib.network |
27 | - contrib.hahelpers |
28 | + - osplatform |
29 | |
30 | === modified file 'hooks/charmhelpers/__init__.py' |
31 | --- hooks/charmhelpers/__init__.py 2015-02-03 18:59:19 +0000 |
32 | +++ hooks/charmhelpers/__init__.py 2016-09-16 19:19:06 +0000 |
33 | @@ -1,18 +1,16 @@ |
34 | # Copyright 2014-2015 Canonical Limited. |
35 | # |
36 | -# This file is part of charm-helpers. |
37 | -# |
38 | -# charm-helpers is free software: you can redistribute it and/or modify |
39 | -# it under the terms of the GNU Lesser General Public License version 3 as |
40 | -# published by the Free Software Foundation. |
41 | -# |
42 | -# charm-helpers is distributed in the hope that it will be useful, |
43 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
44 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
45 | -# GNU Lesser General Public License for more details. |
46 | -# |
47 | -# You should have received a copy of the GNU Lesser General Public License |
48 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
49 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
50 | +# you may not use this file except in compliance with the License. |
51 | +# You may obtain a copy of the License at |
52 | +# |
53 | +# http://www.apache.org/licenses/LICENSE-2.0 |
54 | +# |
55 | +# Unless required by applicable law or agreed to in writing, software |
56 | +# distributed under the License is distributed on an "AS IS" BASIS, |
57 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
58 | +# See the License for the specific language governing permissions and |
59 | +# limitations under the License. |
60 | |
61 | # Bootstrap charm-helpers, installing its dependencies if necessary using |
62 | # only standard libraries. |
63 | |
64 | === modified file 'hooks/charmhelpers/contrib/__init__.py' |
65 | --- hooks/charmhelpers/contrib/__init__.py 2015-02-03 18:59:19 +0000 |
66 | +++ hooks/charmhelpers/contrib/__init__.py 2016-09-16 19:19:06 +0000 |
67 | @@ -1,15 +1,13 @@ |
68 | # Copyright 2014-2015 Canonical Limited. |
69 | # |
70 | -# This file is part of charm-helpers. |
71 | -# |
72 | -# charm-helpers is free software: you can redistribute it and/or modify |
73 | -# it under the terms of the GNU Lesser General Public License version 3 as |
74 | -# published by the Free Software Foundation. |
75 | -# |
76 | -# charm-helpers is distributed in the hope that it will be useful, |
77 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
78 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
79 | -# GNU Lesser General Public License for more details. |
80 | -# |
81 | -# You should have received a copy of the GNU Lesser General Public License |
82 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
83 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
84 | +# you may not use this file except in compliance with the License. |
85 | +# You may obtain a copy of the License at |
86 | +# |
87 | +# http://www.apache.org/licenses/LICENSE-2.0 |
88 | +# |
89 | +# Unless required by applicable law or agreed to in writing, software |
90 | +# distributed under the License is distributed on an "AS IS" BASIS, |
91 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
92 | +# See the License for the specific language governing permissions and |
93 | +# limitations under the License. |
94 | |
95 | === modified file 'hooks/charmhelpers/contrib/hahelpers/__init__.py' |
96 | --- hooks/charmhelpers/contrib/hahelpers/__init__.py 2015-02-13 12:32:01 +0000 |
97 | +++ hooks/charmhelpers/contrib/hahelpers/__init__.py 2016-09-16 19:19:06 +0000 |
98 | @@ -1,15 +1,13 @@ |
99 | # Copyright 2014-2015 Canonical Limited. |
100 | # |
101 | -# This file is part of charm-helpers. |
102 | -# |
103 | -# charm-helpers is free software: you can redistribute it and/or modify |
104 | -# it under the terms of the GNU Lesser General Public License version 3 as |
105 | -# published by the Free Software Foundation. |
106 | -# |
107 | -# charm-helpers is distributed in the hope that it will be useful, |
108 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
109 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
110 | -# GNU Lesser General Public License for more details. |
111 | -# |
112 | -# You should have received a copy of the GNU Lesser General Public License |
113 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
114 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
115 | +# you may not use this file except in compliance with the License. |
116 | +# You may obtain a copy of the License at |
117 | +# |
118 | +# http://www.apache.org/licenses/LICENSE-2.0 |
119 | +# |
120 | +# Unless required by applicable law or agreed to in writing, software |
121 | +# distributed under the License is distributed on an "AS IS" BASIS, |
122 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
123 | +# See the License for the specific language governing permissions and |
124 | +# limitations under the License. |
125 | |
126 | === modified file 'hooks/charmhelpers/contrib/hahelpers/apache.py' |
127 | --- hooks/charmhelpers/contrib/hahelpers/apache.py 2015-02-13 12:32:01 +0000 |
128 | +++ hooks/charmhelpers/contrib/hahelpers/apache.py 2016-09-16 19:19:06 +0000 |
129 | @@ -1,18 +1,16 @@ |
130 | # Copyright 2014-2015 Canonical Limited. |
131 | # |
132 | -# This file is part of charm-helpers. |
133 | -# |
134 | -# charm-helpers is free software: you can redistribute it and/or modify |
135 | -# it under the terms of the GNU Lesser General Public License version 3 as |
136 | -# published by the Free Software Foundation. |
137 | -# |
138 | -# charm-helpers is distributed in the hope that it will be useful, |
139 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
140 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
141 | -# GNU Lesser General Public License for more details. |
142 | -# |
143 | -# You should have received a copy of the GNU Lesser General Public License |
144 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
145 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
146 | +# you may not use this file except in compliance with the License. |
147 | +# You may obtain a copy of the License at |
148 | +# |
149 | +# http://www.apache.org/licenses/LICENSE-2.0 |
150 | +# |
151 | +# Unless required by applicable law or agreed to in writing, software |
152 | +# distributed under the License is distributed on an "AS IS" BASIS, |
153 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
154 | +# See the License for the specific language governing permissions and |
155 | +# limitations under the License. |
156 | |
157 | # |
158 | # Copyright 2012 Canonical Ltd. |
159 | @@ -24,6 +22,7 @@ |
160 | # Adam Gandelman <adamg@ubuntu.com> |
161 | # |
162 | |
163 | +import os |
164 | import subprocess |
165 | |
166 | from charmhelpers.core.hookenv import ( |
167 | @@ -74,9 +73,23 @@ |
168 | return ca_cert |
169 | |
170 | |
171 | +def retrieve_ca_cert(cert_file): |
172 | + cert = None |
173 | + if os.path.isfile(cert_file): |
174 | + with open(cert_file, 'r') as crt: |
175 | + cert = crt.read() |
176 | + return cert |
177 | + |
178 | + |
179 | def install_ca_cert(ca_cert): |
180 | if ca_cert: |
181 | - with open('/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt', |
182 | - 'w') as crt: |
183 | - crt.write(ca_cert) |
184 | - subprocess.check_call(['update-ca-certificates', '--fresh']) |
185 | + cert_file = ('/usr/local/share/ca-certificates/' |
186 | + 'keystone_juju_ca_cert.crt') |
187 | + old_cert = retrieve_ca_cert(cert_file) |
188 | + if old_cert and old_cert == ca_cert: |
189 | + log("CA cert is the same as installed version", level=INFO) |
190 | + else: |
191 | + log("Installing new CA cert", level=INFO) |
192 | + with open(cert_file, 'w') as crt: |
193 | + crt.write(ca_cert) |
194 | + subprocess.check_call(['update-ca-certificates', '--fresh']) |
195 | |
196 | === modified file 'hooks/charmhelpers/contrib/hahelpers/cluster.py' |
197 | --- hooks/charmhelpers/contrib/hahelpers/cluster.py 2015-07-14 14:05:42 +0000 |
198 | +++ hooks/charmhelpers/contrib/hahelpers/cluster.py 2016-09-16 19:19:06 +0000 |
199 | @@ -1,18 +1,16 @@ |
200 | # Copyright 2014-2015 Canonical Limited. |
201 | # |
202 | -# This file is part of charm-helpers. |
203 | -# |
204 | -# charm-helpers is free software: you can redistribute it and/or modify |
205 | -# it under the terms of the GNU Lesser General Public License version 3 as |
206 | -# published by the Free Software Foundation. |
207 | -# |
208 | -# charm-helpers is distributed in the hope that it will be useful, |
209 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
210 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
211 | -# GNU Lesser General Public License for more details. |
212 | -# |
213 | -# You should have received a copy of the GNU Lesser General Public License |
214 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
215 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
216 | +# you may not use this file except in compliance with the License. |
217 | +# You may obtain a copy of the License at |
218 | +# |
219 | +# http://www.apache.org/licenses/LICENSE-2.0 |
220 | +# |
221 | +# Unless required by applicable law or agreed to in writing, software |
222 | +# distributed under the License is distributed on an "AS IS" BASIS, |
223 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
224 | +# See the License for the specific language governing permissions and |
225 | +# limitations under the License. |
226 | |
227 | # |
228 | # Copyright 2012 Canonical Ltd. |
229 | @@ -41,10 +39,11 @@ |
230 | relation_get, |
231 | config as config_get, |
232 | INFO, |
233 | - ERROR, |
234 | + DEBUG, |
235 | WARNING, |
236 | unit_get, |
237 | - is_leader as juju_is_leader |
238 | + is_leader as juju_is_leader, |
239 | + status_set, |
240 | ) |
241 | from charmhelpers.core.decorators import ( |
242 | retry_on_exception, |
243 | @@ -60,6 +59,10 @@ |
244 | pass |
245 | |
246 | |
247 | +class HAIncorrectConfig(Exception): |
248 | + pass |
249 | + |
250 | + |
251 | class CRMResourceNotFound(Exception): |
252 | pass |
253 | |
254 | @@ -274,27 +277,71 @@ |
255 | Obtains all relevant configuration from charm configuration required |
256 | for initiating a relation to hacluster: |
257 | |
258 | - ha-bindiface, ha-mcastport, vip |
259 | + ha-bindiface, ha-mcastport, vip, os-internal-hostname, |
260 | + os-admin-hostname, os-public-hostname, os-access-hostname |
261 | |
262 | param: exclude_keys: list of setting key(s) to be excluded. |
263 | returns: dict: A dict containing settings keyed by setting name. |
264 | - raises: HAIncompleteConfig if settings are missing. |
265 | + raises: HAIncompleteConfig if settings are missing or incorrect. |
266 | ''' |
267 | - settings = ['ha-bindiface', 'ha-mcastport', 'vip'] |
268 | + settings = ['ha-bindiface', 'ha-mcastport', 'vip', 'os-internal-hostname', |
269 | + 'os-admin-hostname', 'os-public-hostname', 'os-access-hostname'] |
270 | conf = {} |
271 | for setting in settings: |
272 | if exclude_keys and setting in exclude_keys: |
273 | continue |
274 | |
275 | conf[setting] = config_get(setting) |
276 | - missing = [] |
277 | - [missing.append(s) for s, v in six.iteritems(conf) if v is None] |
278 | - if missing: |
279 | - log('Insufficient config data to configure hacluster.', level=ERROR) |
280 | - raise HAIncompleteConfig |
281 | + |
282 | + if not valid_hacluster_config(): |
283 | + raise HAIncorrectConfig('Insufficient or incorrect config data to ' |
284 | + 'configure hacluster.') |
285 | return conf |
286 | |
287 | |
288 | +def valid_hacluster_config(): |
289 | + ''' |
290 | + Check that either vip or dns-ha is set. If dns-ha then one of os-*-hostname |
291 | + must be set. |
292 | + |
293 | + Note: ha-bindiface and ha-macastport both have defaults and will always |
294 | + be set. We only care that either vip or dns-ha is set. |
295 | + |
296 | + :returns: boolean: valid config returns true. |
297 | + raises: HAIncompatibileConfig if settings conflict. |
298 | + raises: HAIncompleteConfig if settings are missing. |
299 | + ''' |
300 | + vip = config_get('vip') |
301 | + dns = config_get('dns-ha') |
302 | + if not(bool(vip) ^ bool(dns)): |
303 | + msg = ('HA: Either vip or dns-ha must be set but not both in order to ' |
304 | + 'use high availability') |
305 | + status_set('blocked', msg) |
306 | + raise HAIncorrectConfig(msg) |
307 | + |
308 | + # If dns-ha then one of os-*-hostname must be set |
309 | + if dns: |
310 | + dns_settings = ['os-internal-hostname', 'os-admin-hostname', |
311 | + 'os-public-hostname', 'os-access-hostname'] |
312 | + # At this point it is unknown if one or all of the possible |
313 | + # network spaces are in HA. Validate at least one is set which is |
314 | + # the minimum required. |
315 | + for setting in dns_settings: |
316 | + if config_get(setting): |
317 | + log('DNS HA: At least one hostname is set {}: {}' |
318 | + ''.format(setting, config_get(setting)), |
319 | + level=DEBUG) |
320 | + return True |
321 | + |
322 | + msg = ('DNS HA: At least one os-*-hostname(s) must be set to use ' |
323 | + 'DNS HA') |
324 | + status_set('blocked', msg) |
325 | + raise HAIncompleteConfig(msg) |
326 | + |
327 | + log('VIP HA: VIP is set {}'.format(vip), level=DEBUG) |
328 | + return True |
329 | + |
330 | + |
331 | def canonical_url(configs, vip_setting='vip'): |
332 | ''' |
333 | Returns the correct HTTP URL to this host given the state of HTTPS |
334 | |
335 | === modified file 'hooks/charmhelpers/contrib/network/__init__.py' |
336 | --- hooks/charmhelpers/contrib/network/__init__.py 2015-02-03 18:59:19 +0000 |
337 | +++ hooks/charmhelpers/contrib/network/__init__.py 2016-09-16 19:19:06 +0000 |
338 | @@ -1,15 +1,13 @@ |
339 | # Copyright 2014-2015 Canonical Limited. |
340 | # |
341 | -# This file is part of charm-helpers. |
342 | -# |
343 | -# charm-helpers is free software: you can redistribute it and/or modify |
344 | -# it under the terms of the GNU Lesser General Public License version 3 as |
345 | -# published by the Free Software Foundation. |
346 | -# |
347 | -# charm-helpers is distributed in the hope that it will be useful, |
348 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
349 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
350 | -# GNU Lesser General Public License for more details. |
351 | -# |
352 | -# You should have received a copy of the GNU Lesser General Public License |
353 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
354 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
355 | +# you may not use this file except in compliance with the License. |
356 | +# You may obtain a copy of the License at |
357 | +# |
358 | +# http://www.apache.org/licenses/LICENSE-2.0 |
359 | +# |
360 | +# Unless required by applicable law or agreed to in writing, software |
361 | +# distributed under the License is distributed on an "AS IS" BASIS, |
362 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
363 | +# See the License for the specific language governing permissions and |
364 | +# limitations under the License. |
365 | |
366 | === modified file 'hooks/charmhelpers/contrib/network/ip.py' |
367 | --- hooks/charmhelpers/contrib/network/ip.py 2015-07-14 14:05:42 +0000 |
368 | +++ hooks/charmhelpers/contrib/network/ip.py 2016-09-16 19:19:06 +0000 |
369 | @@ -1,18 +1,16 @@ |
370 | # Copyright 2014-2015 Canonical Limited. |
371 | # |
372 | -# This file is part of charm-helpers. |
373 | -# |
374 | -# charm-helpers is free software: you can redistribute it and/or modify |
375 | -# it under the terms of the GNU Lesser General Public License version 3 as |
376 | -# published by the Free Software Foundation. |
377 | -# |
378 | -# charm-helpers is distributed in the hope that it will be useful, |
379 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
380 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
381 | -# GNU Lesser General Public License for more details. |
382 | -# |
383 | -# You should have received a copy of the GNU Lesser General Public License |
384 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
385 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
386 | +# you may not use this file except in compliance with the License. |
387 | +# You may obtain a copy of the License at |
388 | +# |
389 | +# http://www.apache.org/licenses/LICENSE-2.0 |
390 | +# |
391 | +# Unless required by applicable law or agreed to in writing, software |
392 | +# distributed under the License is distributed on an "AS IS" BASIS, |
393 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
394 | +# See the License for the specific language governing permissions and |
395 | +# limitations under the License. |
396 | |
397 | import glob |
398 | import re |
399 | @@ -23,7 +21,7 @@ |
400 | from functools import partial |
401 | |
402 | from charmhelpers.core.hookenv import unit_get |
403 | -from charmhelpers.fetch import apt_install |
404 | +from charmhelpers.fetch import apt_install, apt_update |
405 | from charmhelpers.core.hookenv import ( |
406 | log, |
407 | WARNING, |
408 | @@ -32,13 +30,15 @@ |
409 | try: |
410 | import netifaces |
411 | except ImportError: |
412 | - apt_install('python-netifaces') |
413 | + apt_update(fatal=True) |
414 | + apt_install('python-netifaces', fatal=True) |
415 | import netifaces |
416 | |
417 | try: |
418 | import netaddr |
419 | except ImportError: |
420 | - apt_install('python-netaddr') |
421 | + apt_update(fatal=True) |
422 | + apt_install('python-netaddr', fatal=True) |
423 | import netaddr |
424 | |
425 | |
426 | @@ -51,7 +51,7 @@ |
427 | |
428 | |
429 | def no_ip_found_error_out(network): |
430 | - errmsg = ("No IP address found in network: %s" % network) |
431 | + errmsg = ("No IP address found in network(s): %s" % network) |
432 | raise ValueError(errmsg) |
433 | |
434 | |
435 | @@ -59,7 +59,7 @@ |
436 | """Get an IPv4 or IPv6 address within the network from the host. |
437 | |
438 | :param network (str): CIDR presentation format. For example, |
439 | - '192.168.1.0/24'. |
440 | + '192.168.1.0/24'. Supports multiple networks as a space-delimited list. |
441 | :param fallback (str): If no address is found, return fallback. |
442 | :param fatal (boolean): If no address is found, fallback is not |
443 | set and fatal is True then exit(1). |
444 | @@ -73,24 +73,26 @@ |
445 | else: |
446 | return None |
447 | |
448 | - _validate_cidr(network) |
449 | - network = netaddr.IPNetwork(network) |
450 | - for iface in netifaces.interfaces(): |
451 | - addresses = netifaces.ifaddresses(iface) |
452 | - if network.version == 4 and netifaces.AF_INET in addresses: |
453 | - addr = addresses[netifaces.AF_INET][0]['addr'] |
454 | - netmask = addresses[netifaces.AF_INET][0]['netmask'] |
455 | - cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask)) |
456 | - if cidr in network: |
457 | - return str(cidr.ip) |
458 | + networks = network.split() or [network] |
459 | + for network in networks: |
460 | + _validate_cidr(network) |
461 | + network = netaddr.IPNetwork(network) |
462 | + for iface in netifaces.interfaces(): |
463 | + addresses = netifaces.ifaddresses(iface) |
464 | + if network.version == 4 and netifaces.AF_INET in addresses: |
465 | + addr = addresses[netifaces.AF_INET][0]['addr'] |
466 | + netmask = addresses[netifaces.AF_INET][0]['netmask'] |
467 | + cidr = netaddr.IPNetwork("%s/%s" % (addr, netmask)) |
468 | + if cidr in network: |
469 | + return str(cidr.ip) |
470 | |
471 | - if network.version == 6 and netifaces.AF_INET6 in addresses: |
472 | - for addr in addresses[netifaces.AF_INET6]: |
473 | - if not addr['addr'].startswith('fe80'): |
474 | - cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'], |
475 | - addr['netmask'])) |
476 | - if cidr in network: |
477 | - return str(cidr.ip) |
478 | + if network.version == 6 and netifaces.AF_INET6 in addresses: |
479 | + for addr in addresses[netifaces.AF_INET6]: |
480 | + if not addr['addr'].startswith('fe80'): |
481 | + cidr = netaddr.IPNetwork("%s/%s" % (addr['addr'], |
482 | + addr['netmask'])) |
483 | + if cidr in network: |
484 | + return str(cidr.ip) |
485 | |
486 | if fallback is not None: |
487 | return fallback |
488 | @@ -187,6 +189,15 @@ |
489 | get_netmask_for_address = partial(_get_for_address, key='netmask') |
490 | |
491 | |
492 | +def resolve_network_cidr(ip_address): |
493 | + ''' |
494 | + Resolves the full address cidr of an ip_address based on |
495 | + configured network interfaces |
496 | + ''' |
497 | + netmask = get_netmask_for_address(ip_address) |
498 | + return str(netaddr.IPNetwork("%s/%s" % (ip_address, netmask)).cidr) |
499 | + |
500 | + |
501 | def format_ipv6_addr(address): |
502 | """If address is IPv6, wrap it in '[]' otherwise return None. |
503 | |
504 | @@ -201,7 +212,16 @@ |
505 | |
506 | def get_iface_addr(iface='eth0', inet_type='AF_INET', inc_aliases=False, |
507 | fatal=True, exc_list=None): |
508 | - """Return the assigned IP address for a given interface, if any.""" |
509 | + """Return the assigned IP address for a given interface, if any. |
510 | + |
511 | + :param iface: network interface on which address(es) are expected to |
512 | + be found. |
513 | + :param inet_type: inet address family |
514 | + :param inc_aliases: include alias interfaces in search |
515 | + :param fatal: if True, raise exception if address not found |
516 | + :param exc_list: list of addresses to ignore |
517 | + :return: list of ip addresses |
518 | + """ |
519 | # Extract nic if passed /dev/ethX |
520 | if '/' in iface: |
521 | iface = iface.split('/')[-1] |
522 | @@ -302,6 +322,14 @@ |
523 | We currently only support scope global IPv6 addresses i.e. non-temporary |
524 | addresses. If no global IPv6 address is found, return the first one found |
525 | in the ipv6 address list. |
526 | + |
527 | + :param iface: network interface on which ipv6 address(es) are expected to |
528 | + be found. |
529 | + :param inc_aliases: include alias interfaces in search |
530 | + :param fatal: if True, raise exception if address not found |
531 | + :param exc_list: list of addresses to ignore |
532 | + :param dynamic_only: only recognise dynamic addresses |
533 | + :return: list of ipv6 addresses |
534 | """ |
535 | addresses = get_iface_addr(iface=iface, inet_type='AF_INET6', |
536 | inc_aliases=inc_aliases, fatal=fatal, |
537 | @@ -323,7 +351,7 @@ |
538 | cmd = ['ip', 'addr', 'show', iface] |
539 | out = subprocess.check_output(cmd).decode('UTF-8') |
540 | if dynamic_only: |
541 | - key = re.compile("inet6 (.+)/[0-9]+ scope global dynamic.*") |
542 | + key = re.compile("inet6 (.+)/[0-9]+ scope global.* dynamic.*") |
543 | else: |
544 | key = re.compile("inet6 (.+)/[0-9]+ scope global.*") |
545 | |
546 | @@ -375,10 +403,10 @@ |
547 | Returns True if address is a valid IP address. |
548 | """ |
549 | try: |
550 | - # Test to see if already an IPv4 address |
551 | - socket.inet_aton(address) |
552 | + # Test to see if already an IPv4/IPv6 address |
553 | + address = netaddr.IPAddress(address) |
554 | return True |
555 | - except socket.error: |
556 | + except netaddr.AddrFormatError: |
557 | return False |
558 | |
559 | |
560 | @@ -386,7 +414,7 @@ |
561 | try: |
562 | import dns.resolver |
563 | except ImportError: |
564 | - apt_install('python-dnspython') |
565 | + apt_install('python-dnspython', fatal=True) |
566 | import dns.resolver |
567 | |
568 | if isinstance(address, dns.name.Name): |
569 | @@ -430,13 +458,17 @@ |
570 | try: |
571 | import dns.reversename |
572 | except ImportError: |
573 | - apt_install("python-dnspython") |
574 | + apt_install("python-dnspython", fatal=True) |
575 | import dns.reversename |
576 | |
577 | rev = dns.reversename.from_address(address) |
578 | result = ns_query(rev) |
579 | + |
580 | if not result: |
581 | - return None |
582 | + try: |
583 | + result = socket.gethostbyaddr(address)[0] |
584 | + except: |
585 | + return None |
586 | else: |
587 | result = address |
588 | |
589 | @@ -448,3 +480,18 @@ |
590 | return result |
591 | else: |
592 | return result.split('.')[0] |
593 | + |
594 | + |
595 | +def port_has_listener(address, port): |
596 | + """ |
597 | + Returns True if the address:port is open and being listened to, |
598 | + else False. |
599 | + |
600 | + @param address: an IP address or hostname |
601 | + @param port: integer port |
602 | + |
603 | + Note calls 'zc' via a subprocess shell |
604 | + """ |
605 | + cmd = ['nc', '-z', address, str(port)] |
606 | + result = subprocess.call(cmd) |
607 | + return not(bool(result)) |
608 | |
609 | === modified file 'hooks/charmhelpers/contrib/network/ovs/__init__.py' |
610 | --- hooks/charmhelpers/contrib/network/ovs/__init__.py 2015-02-03 18:59:19 +0000 |
611 | +++ hooks/charmhelpers/contrib/network/ovs/__init__.py 2016-09-16 19:19:06 +0000 |
612 | @@ -1,18 +1,16 @@ |
613 | # Copyright 2014-2015 Canonical Limited. |
614 | # |
615 | -# This file is part of charm-helpers. |
616 | -# |
617 | -# charm-helpers is free software: you can redistribute it and/or modify |
618 | -# it under the terms of the GNU Lesser General Public License version 3 as |
619 | -# published by the Free Software Foundation. |
620 | -# |
621 | -# charm-helpers is distributed in the hope that it will be useful, |
622 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
623 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
624 | -# GNU Lesser General Public License for more details. |
625 | -# |
626 | -# You should have received a copy of the GNU Lesser General Public License |
627 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
628 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
629 | +# you may not use this file except in compliance with the License. |
630 | +# You may obtain a copy of the License at |
631 | +# |
632 | +# http://www.apache.org/licenses/LICENSE-2.0 |
633 | +# |
634 | +# Unless required by applicable law or agreed to in writing, software |
635 | +# distributed under the License is distributed on an "AS IS" BASIS, |
636 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
637 | +# See the License for the specific language governing permissions and |
638 | +# limitations under the License. |
639 | |
640 | ''' Helpers for interacting with OpenvSwitch ''' |
641 | import subprocess |
642 | @@ -25,10 +23,14 @@ |
643 | ) |
644 | |
645 | |
646 | -def add_bridge(name): |
647 | +def add_bridge(name, datapath_type=None): |
648 | ''' Add the named bridge to openvswitch ''' |
649 | log('Creating bridge {}'.format(name)) |
650 | - subprocess.check_call(["ovs-vsctl", "--", "--may-exist", "add-br", name]) |
651 | + cmd = ["ovs-vsctl", "--", "--may-exist", "add-br", name] |
652 | + if datapath_type is not None: |
653 | + cmd += ['--', 'set', 'bridge', name, |
654 | + 'datapath_type={}'.format(datapath_type)] |
655 | + subprocess.check_call(cmd) |
656 | |
657 | |
658 | def del_bridge(name): |
659 | |
660 | === modified file 'hooks/charmhelpers/contrib/network/ufw.py' |
661 | --- hooks/charmhelpers/contrib/network/ufw.py 2015-07-06 21:30:20 +0000 |
662 | +++ hooks/charmhelpers/contrib/network/ufw.py 2016-09-16 19:19:06 +0000 |
663 | @@ -1,18 +1,16 @@ |
664 | # Copyright 2014-2015 Canonical Limited. |
665 | # |
666 | -# This file is part of charm-helpers. |
667 | -# |
668 | -# charm-helpers is free software: you can redistribute it and/or modify |
669 | -# it under the terms of the GNU Lesser General Public License version 3 as |
670 | -# published by the Free Software Foundation. |
671 | -# |
672 | -# charm-helpers is distributed in the hope that it will be useful, |
673 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
674 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
675 | -# GNU Lesser General Public License for more details. |
676 | -# |
677 | -# You should have received a copy of the GNU Lesser General Public License |
678 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
679 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
680 | +# you may not use this file except in compliance with the License. |
681 | +# You may obtain a copy of the License at |
682 | +# |
683 | +# http://www.apache.org/licenses/LICENSE-2.0 |
684 | +# |
685 | +# Unless required by applicable law or agreed to in writing, software |
686 | +# distributed under the License is distributed on an "AS IS" BASIS, |
687 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
688 | +# See the License for the specific language governing permissions and |
689 | +# limitations under the License. |
690 | |
691 | """ |
692 | This module contains helpers to add and remove ufw rules. |
693 | @@ -40,7 +38,9 @@ |
694 | import re |
695 | import os |
696 | import subprocess |
697 | + |
698 | from charmhelpers.core import hookenv |
699 | +from charmhelpers.core.kernel import modprobe, is_module_loaded |
700 | |
701 | __author__ = "Felipe Reyes <felipe.reyes@canonical.com>" |
702 | |
703 | @@ -82,14 +82,11 @@ |
704 | # do we have IPv6 in the machine? |
705 | if os.path.isdir('/proc/sys/net/ipv6'): |
706 | # is ip6tables kernel module loaded? |
707 | - lsmod = subprocess.check_output(['lsmod'], universal_newlines=True) |
708 | - matches = re.findall('^ip6_tables[ ]+', lsmod, re.M) |
709 | - if len(matches) == 0: |
710 | + if not is_module_loaded('ip6_tables'): |
711 | # ip6tables support isn't complete, let's try to load it |
712 | try: |
713 | - subprocess.check_output(['modprobe', 'ip6_tables'], |
714 | - universal_newlines=True) |
715 | - # great, we could load the module |
716 | + modprobe('ip6_tables') |
717 | + # great, we can load the module |
718 | return True |
719 | except subprocess.CalledProcessError as ex: |
720 | hookenv.log("Couldn't load ip6_tables module: %s" % ex.output, |
721 | |
722 | === modified file 'hooks/charmhelpers/core/__init__.py' |
723 | --- hooks/charmhelpers/core/__init__.py 2015-02-03 18:59:19 +0000 |
724 | +++ hooks/charmhelpers/core/__init__.py 2016-09-16 19:19:06 +0000 |
725 | @@ -1,15 +1,13 @@ |
726 | # Copyright 2014-2015 Canonical Limited. |
727 | # |
728 | -# This file is part of charm-helpers. |
729 | -# |
730 | -# charm-helpers is free software: you can redistribute it and/or modify |
731 | -# it under the terms of the GNU Lesser General Public License version 3 as |
732 | -# published by the Free Software Foundation. |
733 | -# |
734 | -# charm-helpers is distributed in the hope that it will be useful, |
735 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
736 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
737 | -# GNU Lesser General Public License for more details. |
738 | -# |
739 | -# You should have received a copy of the GNU Lesser General Public License |
740 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
741 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
742 | +# you may not use this file except in compliance with the License. |
743 | +# You may obtain a copy of the License at |
744 | +# |
745 | +# http://www.apache.org/licenses/LICENSE-2.0 |
746 | +# |
747 | +# Unless required by applicable law or agreed to in writing, software |
748 | +# distributed under the License is distributed on an "AS IS" BASIS, |
749 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
750 | +# See the License for the specific language governing permissions and |
751 | +# limitations under the License. |
752 | |
753 | === modified file 'hooks/charmhelpers/core/decorators.py' |
754 | --- hooks/charmhelpers/core/decorators.py 2015-02-03 18:59:19 +0000 |
755 | +++ hooks/charmhelpers/core/decorators.py 2016-09-16 19:19:06 +0000 |
756 | @@ -1,18 +1,16 @@ |
757 | # Copyright 2014-2015 Canonical Limited. |
758 | # |
759 | -# This file is part of charm-helpers. |
760 | -# |
761 | -# charm-helpers is free software: you can redistribute it and/or modify |
762 | -# it under the terms of the GNU Lesser General Public License version 3 as |
763 | -# published by the Free Software Foundation. |
764 | -# |
765 | -# charm-helpers is distributed in the hope that it will be useful, |
766 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
767 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
768 | -# GNU Lesser General Public License for more details. |
769 | -# |
770 | -# You should have received a copy of the GNU Lesser General Public License |
771 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
772 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
773 | +# you may not use this file except in compliance with the License. |
774 | +# You may obtain a copy of the License at |
775 | +# |
776 | +# http://www.apache.org/licenses/LICENSE-2.0 |
777 | +# |
778 | +# Unless required by applicable law or agreed to in writing, software |
779 | +# distributed under the License is distributed on an "AS IS" BASIS, |
780 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
781 | +# See the License for the specific language governing permissions and |
782 | +# limitations under the License. |
783 | |
784 | # |
785 | # Copyright 2014 Canonical Ltd. |
786 | |
787 | === added file 'hooks/charmhelpers/core/files.py' |
788 | --- hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000 |
789 | +++ hooks/charmhelpers/core/files.py 2016-09-16 19:19:06 +0000 |
790 | @@ -0,0 +1,43 @@ |
791 | +#!/usr/bin/env python |
792 | +# -*- coding: utf-8 -*- |
793 | + |
794 | +# Copyright 2014-2015 Canonical Limited. |
795 | +# |
796 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
797 | +# you may not use this file except in compliance with the License. |
798 | +# You may obtain a copy of the License at |
799 | +# |
800 | +# http://www.apache.org/licenses/LICENSE-2.0 |
801 | +# |
802 | +# Unless required by applicable law or agreed to in writing, software |
803 | +# distributed under the License is distributed on an "AS IS" BASIS, |
804 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
805 | +# See the License for the specific language governing permissions and |
806 | +# limitations under the License. |
807 | + |
808 | +__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>' |
809 | + |
810 | +import os |
811 | +import subprocess |
812 | + |
813 | + |
814 | +def sed(filename, before, after, flags='g'): |
815 | + """ |
816 | + Search and replaces the given pattern on filename. |
817 | + |
818 | + :param filename: relative or absolute file path. |
819 | + :param before: expression to be replaced (see 'man sed') |
820 | + :param after: expression to replace with (see 'man sed') |
821 | + :param flags: sed-compatible regex flags in example, to make |
822 | + the search and replace case insensitive, specify ``flags="i"``. |
823 | + The ``g`` flag is always specified regardless, so you do not |
824 | + need to remember to include it when overriding this parameter. |
825 | + :returns: If the sed command exit code was zero then return, |
826 | + otherwise raise CalledProcessError. |
827 | + """ |
828 | + expression = r's/{0}/{1}/{2}'.format(before, |
829 | + after, flags) |
830 | + |
831 | + return subprocess.check_call(["sed", "-i", "-r", "-e", |
832 | + expression, |
833 | + os.path.expanduser(filename)]) |
834 | |
835 | === modified file 'hooks/charmhelpers/core/fstab.py' |
836 | --- hooks/charmhelpers/core/fstab.py 2015-07-14 14:05:42 +0000 |
837 | +++ hooks/charmhelpers/core/fstab.py 2016-09-16 19:19:06 +0000 |
838 | @@ -3,19 +3,17 @@ |
839 | |
840 | # Copyright 2014-2015 Canonical Limited. |
841 | # |
842 | -# This file is part of charm-helpers. |
843 | -# |
844 | -# charm-helpers is free software: you can redistribute it and/or modify |
845 | -# it under the terms of the GNU Lesser General Public License version 3 as |
846 | -# published by the Free Software Foundation. |
847 | -# |
848 | -# charm-helpers is distributed in the hope that it will be useful, |
849 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
850 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
851 | -# GNU Lesser General Public License for more details. |
852 | -# |
853 | -# You should have received a copy of the GNU Lesser General Public License |
854 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
855 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
856 | +# you may not use this file except in compliance with the License. |
857 | +# You may obtain a copy of the License at |
858 | +# |
859 | +# http://www.apache.org/licenses/LICENSE-2.0 |
860 | +# |
861 | +# Unless required by applicable law or agreed to in writing, software |
862 | +# distributed under the License is distributed on an "AS IS" BASIS, |
863 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
864 | +# See the License for the specific language governing permissions and |
865 | +# limitations under the License. |
866 | |
867 | import io |
868 | import os |
869 | |
870 | === modified file 'hooks/charmhelpers/core/hookenv.py' |
871 | --- hooks/charmhelpers/core/hookenv.py 2015-07-14 14:05:42 +0000 |
872 | +++ hooks/charmhelpers/core/hookenv.py 2016-09-16 19:19:06 +0000 |
873 | @@ -1,18 +1,16 @@ |
874 | # Copyright 2014-2015 Canonical Limited. |
875 | # |
876 | -# This file is part of charm-helpers. |
877 | -# |
878 | -# charm-helpers is free software: you can redistribute it and/or modify |
879 | -# it under the terms of the GNU Lesser General Public License version 3 as |
880 | -# published by the Free Software Foundation. |
881 | -# |
882 | -# charm-helpers is distributed in the hope that it will be useful, |
883 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
884 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
885 | -# GNU Lesser General Public License for more details. |
886 | -# |
887 | -# You should have received a copy of the GNU Lesser General Public License |
888 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
889 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
890 | +# you may not use this file except in compliance with the License. |
891 | +# You may obtain a copy of the License at |
892 | +# |
893 | +# http://www.apache.org/licenses/LICENSE-2.0 |
894 | +# |
895 | +# Unless required by applicable law or agreed to in writing, software |
896 | +# distributed under the License is distributed on an "AS IS" BASIS, |
897 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
898 | +# See the License for the specific language governing permissions and |
899 | +# limitations under the License. |
900 | |
901 | "Interactions with the Juju environment" |
902 | # Copyright 2013 Canonical Ltd. |
903 | @@ -21,6 +19,7 @@ |
904 | # Charm Helpers Developers <juju@lists.ubuntu.com> |
905 | |
906 | from __future__ import print_function |
907 | +import copy |
908 | from distutils.version import LooseVersion |
909 | from functools import wraps |
910 | import glob |
911 | @@ -73,6 +72,7 @@ |
912 | res = func(*args, **kwargs) |
913 | cache[key] = res |
914 | return res |
915 | + wrapper._wrapped = func |
916 | return wrapper |
917 | |
918 | |
919 | @@ -172,9 +172,19 @@ |
920 | return os.environ.get('JUJU_RELATION', None) |
921 | |
922 | |
923 | -def relation_id(): |
924 | - """The relation ID for the current relation hook""" |
925 | - return os.environ.get('JUJU_RELATION_ID', None) |
926 | +@cached |
927 | +def relation_id(relation_name=None, service_or_unit=None): |
928 | + """The relation ID for the current or a specified relation""" |
929 | + if not relation_name and not service_or_unit: |
930 | + return os.environ.get('JUJU_RELATION_ID', None) |
931 | + elif relation_name and service_or_unit: |
932 | + service_name = service_or_unit.split('/')[0] |
933 | + for relid in relation_ids(relation_name): |
934 | + remote_service = remote_service_name(relid) |
935 | + if remote_service == service_name: |
936 | + return relid |
937 | + else: |
938 | + raise ValueError('Must specify neither or both of relation_name and service_or_unit') |
939 | |
940 | |
941 | def local_unit(): |
942 | @@ -192,9 +202,20 @@ |
943 | return local_unit().split('/')[0] |
944 | |
945 | |
946 | +@cached |
947 | +def remote_service_name(relid=None): |
948 | + """The remote service name for a given relation-id (or the current relation)""" |
949 | + if relid is None: |
950 | + unit = remote_unit() |
951 | + else: |
952 | + units = related_units(relid) |
953 | + unit = units[0] if units else None |
954 | + return unit.split('/')[0] if unit else None |
955 | + |
956 | + |
957 | def hook_name(): |
958 | """The name of the currently executing hook""" |
959 | - return os.path.basename(sys.argv[0]) |
960 | + return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0])) |
961 | |
962 | |
963 | class Config(dict): |
964 | @@ -263,7 +284,7 @@ |
965 | self.path = path or self.path |
966 | with open(self.path) as f: |
967 | self._prev_dict = json.load(f) |
968 | - for k, v in self._prev_dict.items(): |
969 | + for k, v in copy.deepcopy(self._prev_dict).items(): |
970 | if k not in self: |
971 | self[k] = v |
972 | |
973 | @@ -468,6 +489,76 @@ |
974 | |
975 | |
976 | @cached |
977 | +def peer_relation_id(): |
978 | + '''Get the peers relation id if a peers relation has been joined, else None.''' |
979 | + md = metadata() |
980 | + section = md.get('peers') |
981 | + if section: |
982 | + for key in section: |
983 | + relids = relation_ids(key) |
984 | + if relids: |
985 | + return relids[0] |
986 | + return None |
987 | + |
988 | + |
989 | +@cached |
990 | +def relation_to_interface(relation_name): |
991 | + """ |
992 | + Given the name of a relation, return the interface that relation uses. |
993 | + |
994 | + :returns: The interface name, or ``None``. |
995 | + """ |
996 | + return relation_to_role_and_interface(relation_name)[1] |
997 | + |
998 | + |
999 | +@cached |
1000 | +def relation_to_role_and_interface(relation_name): |
1001 | + """ |
1002 | + Given the name of a relation, return the role and the name of the interface |
1003 | + that relation uses (where role is one of ``provides``, ``requires``, or ``peers``). |
1004 | + |
1005 | + :returns: A tuple containing ``(role, interface)``, or ``(None, None)``. |
1006 | + """ |
1007 | + _metadata = metadata() |
1008 | + for role in ('provides', 'requires', 'peers'): |
1009 | + interface = _metadata.get(role, {}).get(relation_name, {}).get('interface') |
1010 | + if interface: |
1011 | + return role, interface |
1012 | + return None, None |
1013 | + |
1014 | + |
1015 | +@cached |
1016 | +def role_and_interface_to_relations(role, interface_name): |
1017 | + """ |
1018 | + Given a role and interface name, return a list of relation names for the |
1019 | + current charm that use that interface under that role (where role is one |
1020 | + of ``provides``, ``requires``, or ``peers``). |
1021 | + |
1022 | + :returns: A list of relation names. |
1023 | + """ |
1024 | + _metadata = metadata() |
1025 | + results = [] |
1026 | + for relation_name, relation in _metadata.get(role, {}).items(): |
1027 | + if relation['interface'] == interface_name: |
1028 | + results.append(relation_name) |
1029 | + return results |
1030 | + |
1031 | + |
1032 | +@cached |
1033 | +def interface_to_relations(interface_name): |
1034 | + """ |
1035 | + Given an interface, return a list of relation names for the current |
1036 | + charm that use that interface. |
1037 | + |
1038 | + :returns: A list of relation names. |
1039 | + """ |
1040 | + results = [] |
1041 | + for role in ('provides', 'requires', 'peers'): |
1042 | + results.extend(role_and_interface_to_relations(role, interface_name)) |
1043 | + return results |
1044 | + |
1045 | + |
1046 | +@cached |
1047 | def charm_name(): |
1048 | """Get the name of the current charm as is specified on metadata.yaml""" |
1049 | return metadata().get('name') |
1050 | @@ -543,6 +634,38 @@ |
1051 | return unit_get('private-address') |
1052 | |
1053 | |
1054 | +@cached |
1055 | +def storage_get(attribute=None, storage_id=None): |
1056 | + """Get storage attributes""" |
1057 | + _args = ['storage-get', '--format=json'] |
1058 | + if storage_id: |
1059 | + _args.extend(('-s', storage_id)) |
1060 | + if attribute: |
1061 | + _args.append(attribute) |
1062 | + try: |
1063 | + return json.loads(subprocess.check_output(_args).decode('UTF-8')) |
1064 | + except ValueError: |
1065 | + return None |
1066 | + |
1067 | + |
1068 | +@cached |
1069 | +def storage_list(storage_name=None): |
1070 | + """List the storage IDs for the unit""" |
1071 | + _args = ['storage-list', '--format=json'] |
1072 | + if storage_name: |
1073 | + _args.append(storage_name) |
1074 | + try: |
1075 | + return json.loads(subprocess.check_output(_args).decode('UTF-8')) |
1076 | + except ValueError: |
1077 | + return None |
1078 | + except OSError as e: |
1079 | + import errno |
1080 | + if e.errno == errno.ENOENT: |
1081 | + # storage-list does not exist |
1082 | + return [] |
1083 | + raise |
1084 | + |
1085 | + |
1086 | class UnregisteredHookError(Exception): |
1087 | """Raised when an undefined hook is called""" |
1088 | pass |
1089 | @@ -643,6 +766,21 @@ |
1090 | subprocess.check_call(['action-fail', message]) |
1091 | |
1092 | |
1093 | +def action_name(): |
1094 | + """Get the name of the currently executing action.""" |
1095 | + return os.environ.get('JUJU_ACTION_NAME') |
1096 | + |
1097 | + |
1098 | +def action_uuid(): |
1099 | + """Get the UUID of the currently executing action.""" |
1100 | + return os.environ.get('JUJU_ACTION_UUID') |
1101 | + |
1102 | + |
1103 | +def action_tag(): |
1104 | + """Get the tag for the currently executing action.""" |
1105 | + return os.environ.get('JUJU_ACTION_TAG') |
1106 | + |
1107 | + |
1108 | def status_set(workload_state, message): |
1109 | """Set the workload state with a message |
1110 | |
1111 | @@ -672,25 +810,28 @@ |
1112 | |
1113 | |
1114 | def status_get(): |
1115 | - """Retrieve the previously set juju workload state |
1116 | - |
1117 | - If the status-set command is not found then assume this is juju < 1.23 and |
1118 | - return 'unknown' |
1119 | + """Retrieve the previously set juju workload state and message |
1120 | + |
1121 | + If the status-get command is not found then assume this is juju < 1.23 and |
1122 | + return 'unknown', "" |
1123 | + |
1124 | """ |
1125 | - cmd = ['status-get'] |
1126 | + cmd = ['status-get', "--format=json", "--include-data"] |
1127 | try: |
1128 | - raw_status = subprocess.check_output(cmd, universal_newlines=True) |
1129 | - status = raw_status.rstrip() |
1130 | - return status |
1131 | + raw_status = subprocess.check_output(cmd) |
1132 | except OSError as e: |
1133 | if e.errno == errno.ENOENT: |
1134 | - return 'unknown' |
1135 | + return ('unknown', "") |
1136 | else: |
1137 | raise |
1138 | + else: |
1139 | + status = json.loads(raw_status.decode("UTF-8")) |
1140 | + return (status["status"], status["message"]) |
1141 | |
1142 | |
1143 | def translate_exc(from_exc, to_exc): |
1144 | def inner_translate_exc1(f): |
1145 | + @wraps(f) |
1146 | def inner_translate_exc2(*args, **kwargs): |
1147 | try: |
1148 | return f(*args, **kwargs) |
1149 | @@ -702,6 +843,20 @@ |
1150 | return inner_translate_exc1 |
1151 | |
1152 | |
1153 | +def application_version_set(version): |
1154 | + """Charm authors may trigger this command from any hook to output what |
1155 | + version of the application is running. This could be a package version, |
1156 | + for instance postgres version 9.5. It could also be a build number or |
1157 | + version control revision identifier, for instance git sha 6fb7ba68. """ |
1158 | + |
1159 | + cmd = ['application-version-set'] |
1160 | + cmd.append(version) |
1161 | + try: |
1162 | + subprocess.check_call(cmd) |
1163 | + except OSError: |
1164 | + log("Application Version: {}".format(version)) |
1165 | + |
1166 | + |
1167 | @translate_exc(from_exc=OSError, to_exc=NotImplementedError) |
1168 | def is_leader(): |
1169 | """Does the current unit hold the juju leadership |
1170 | @@ -735,6 +890,58 @@ |
1171 | subprocess.check_call(cmd) |
1172 | |
1173 | |
1174 | +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) |
1175 | +def payload_register(ptype, klass, pid): |
1176 | + """ is used while a hook is running to let Juju know that a |
1177 | + payload has been started.""" |
1178 | + cmd = ['payload-register'] |
1179 | + for x in [ptype, klass, pid]: |
1180 | + cmd.append(x) |
1181 | + subprocess.check_call(cmd) |
1182 | + |
1183 | + |
1184 | +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) |
1185 | +def payload_unregister(klass, pid): |
1186 | + """ is used while a hook is running to let Juju know |
1187 | + that a payload has been manually stopped. The <class> and <id> provided |
1188 | + must match a payload that has been previously registered with juju using |
1189 | + payload-register.""" |
1190 | + cmd = ['payload-unregister'] |
1191 | + for x in [klass, pid]: |
1192 | + cmd.append(x) |
1193 | + subprocess.check_call(cmd) |
1194 | + |
1195 | + |
1196 | +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) |
1197 | +def payload_status_set(klass, pid, status): |
1198 | + """is used to update the current status of a registered payload. |
1199 | + The <class> and <id> provided must match a payload that has been previously |
1200 | + registered with juju using payload-register. The <status> must be one of the |
1201 | + follow: starting, started, stopping, stopped""" |
1202 | + cmd = ['payload-status-set'] |
1203 | + for x in [klass, pid, status]: |
1204 | + cmd.append(x) |
1205 | + subprocess.check_call(cmd) |
1206 | + |
1207 | + |
1208 | +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) |
1209 | +def resource_get(name): |
1210 | + """used to fetch the resource path of the given name. |
1211 | + |
1212 | + <name> must match a name of defined resource in metadata.yaml |
1213 | + |
1214 | + returns either a path or False if resource not available |
1215 | + """ |
1216 | + if not name: |
1217 | + return False |
1218 | + |
1219 | + cmd = ['resource-get', name] |
1220 | + try: |
1221 | + return subprocess.check_output(cmd).decode('UTF-8') |
1222 | + except subprocess.CalledProcessError: |
1223 | + return False |
1224 | + |
1225 | + |
1226 | @cached |
1227 | def juju_version(): |
1228 | """Full version string (eg. '1.23.3.1-trusty-amd64')""" |
1229 | @@ -799,3 +1006,16 @@ |
1230 | for callback, args, kwargs in reversed(_atexit): |
1231 | callback(*args, **kwargs) |
1232 | del _atexit[:] |
1233 | + |
1234 | + |
1235 | +@translate_exc(from_exc=OSError, to_exc=NotImplementedError) |
1236 | +def network_get_primary_address(binding): |
1237 | + ''' |
1238 | + Retrieve the primary network address for a named binding |
1239 | + |
1240 | + :param binding: string. The name of a relation of extra-binding |
1241 | + :return: string. The primary IP address for the named binding |
1242 | + :raise: NotImplementedError if run on Juju < 2.0 |
1243 | + ''' |
1244 | + cmd = ['network-get', '--primary-address', binding] |
1245 | + return subprocess.check_output(cmd).decode('UTF-8').strip() |
1246 | |
1247 | === modified file 'hooks/charmhelpers/core/host.py' |
1248 | --- hooks/charmhelpers/core/host.py 2015-07-14 14:05:42 +0000 |
1249 | +++ hooks/charmhelpers/core/host.py 2016-09-16 19:19:06 +0000 |
1250 | @@ -1,18 +1,16 @@ |
1251 | # Copyright 2014-2015 Canonical Limited. |
1252 | # |
1253 | -# This file is part of charm-helpers. |
1254 | -# |
1255 | -# charm-helpers is free software: you can redistribute it and/or modify |
1256 | -# it under the terms of the GNU Lesser General Public License version 3 as |
1257 | -# published by the Free Software Foundation. |
1258 | -# |
1259 | -# charm-helpers is distributed in the hope that it will be useful, |
1260 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1261 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1262 | -# GNU Lesser General Public License for more details. |
1263 | -# |
1264 | -# You should have received a copy of the GNU Lesser General Public License |
1265 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
1266 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
1267 | +# you may not use this file except in compliance with the License. |
1268 | +# You may obtain a copy of the License at |
1269 | +# |
1270 | +# http://www.apache.org/licenses/LICENSE-2.0 |
1271 | +# |
1272 | +# Unless required by applicable law or agreed to in writing, software |
1273 | +# distributed under the License is distributed on an "AS IS" BASIS, |
1274 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
1275 | +# See the License for the specific language governing permissions and |
1276 | +# limitations under the License. |
1277 | |
1278 | """Tools for working with the host system""" |
1279 | # Copyright 2012 Canonical Ltd. |
1280 | @@ -30,13 +28,31 @@ |
1281 | import string |
1282 | import subprocess |
1283 | import hashlib |
1284 | +import functools |
1285 | +import itertools |
1286 | +import six |
1287 | + |
1288 | from contextlib import contextmanager |
1289 | from collections import OrderedDict |
1290 | - |
1291 | -import six |
1292 | - |
1293 | from .hookenv import log |
1294 | from .fstab import Fstab |
1295 | +from charmhelpers.osplatform import get_platform |
1296 | + |
1297 | +__platform__ = get_platform() |
1298 | +if __platform__ == "ubuntu": |
1299 | + from charmhelpers.core.host_factory.ubuntu import ( |
1300 | + service_available, |
1301 | + add_new_group, |
1302 | + lsb_release, |
1303 | + cmp_pkgrevno, |
1304 | + ) # flake8: noqa -- ignore F401 for this import |
1305 | +elif __platform__ == "centos": |
1306 | + from charmhelpers.core.host_factory.centos import ( |
1307 | + service_available, |
1308 | + add_new_group, |
1309 | + lsb_release, |
1310 | + cmp_pkgrevno, |
1311 | + ) # flake8: noqa -- ignore F401 for this import |
1312 | |
1313 | |
1314 | def service_start(service_name): |
1315 | @@ -63,47 +79,138 @@ |
1316 | return service_result |
1317 | |
1318 | |
1319 | +def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"): |
1320 | + """Pause a system service. |
1321 | + |
1322 | + Stop it, and prevent it from starting again at boot.""" |
1323 | + stopped = True |
1324 | + if service_running(service_name): |
1325 | + stopped = service_stop(service_name) |
1326 | + upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) |
1327 | + sysv_file = os.path.join(initd_dir, service_name) |
1328 | + if init_is_systemd(): |
1329 | + service('disable', service_name) |
1330 | + elif os.path.exists(upstart_file): |
1331 | + override_path = os.path.join( |
1332 | + init_dir, '{}.override'.format(service_name)) |
1333 | + with open(override_path, 'w') as fh: |
1334 | + fh.write("manual\n") |
1335 | + elif os.path.exists(sysv_file): |
1336 | + subprocess.check_call(["update-rc.d", service_name, "disable"]) |
1337 | + else: |
1338 | + raise ValueError( |
1339 | + "Unable to detect {0} as SystemD, Upstart {1} or" |
1340 | + " SysV {2}".format( |
1341 | + service_name, upstart_file, sysv_file)) |
1342 | + return stopped |
1343 | + |
1344 | + |
1345 | +def service_resume(service_name, init_dir="/etc/init", |
1346 | + initd_dir="/etc/init.d"): |
1347 | + """Resume a system service. |
1348 | + |
1349 | + Reenable starting again at boot. Start the service""" |
1350 | + upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) |
1351 | + sysv_file = os.path.join(initd_dir, service_name) |
1352 | + if init_is_systemd(): |
1353 | + service('enable', service_name) |
1354 | + elif os.path.exists(upstart_file): |
1355 | + override_path = os.path.join( |
1356 | + init_dir, '{}.override'.format(service_name)) |
1357 | + if os.path.exists(override_path): |
1358 | + os.unlink(override_path) |
1359 | + elif os.path.exists(sysv_file): |
1360 | + subprocess.check_call(["update-rc.d", service_name, "enable"]) |
1361 | + else: |
1362 | + raise ValueError( |
1363 | + "Unable to detect {0} as SystemD, Upstart {1} or" |
1364 | + " SysV {2}".format( |
1365 | + service_name, upstart_file, sysv_file)) |
1366 | + |
1367 | + started = service_running(service_name) |
1368 | + if not started: |
1369 | + started = service_start(service_name) |
1370 | + return started |
1371 | + |
1372 | + |
1373 | def service(action, service_name): |
1374 | """Control a system service""" |
1375 | - cmd = ['service', service_name, action] |
1376 | + if init_is_systemd(): |
1377 | + cmd = ['systemctl', action, service_name] |
1378 | + else: |
1379 | + cmd = ['service', service_name, action] |
1380 | return subprocess.call(cmd) == 0 |
1381 | |
1382 | |
1383 | -def service_running(service): |
1384 | +_UPSTART_CONF = "/etc/init/{}.conf" |
1385 | +_INIT_D_CONF = "/etc/init.d/{}" |
1386 | + |
1387 | + |
1388 | +def service_running(service_name): |
1389 | """Determine whether a system service is running""" |
1390 | - try: |
1391 | - output = subprocess.check_output( |
1392 | - ['service', service, 'status'], |
1393 | - stderr=subprocess.STDOUT).decode('UTF-8') |
1394 | - except subprocess.CalledProcessError: |
1395 | + if init_is_systemd(): |
1396 | + return service('is-active', service_name) |
1397 | + else: |
1398 | + if os.path.exists(_UPSTART_CONF.format(service_name)): |
1399 | + try: |
1400 | + output = subprocess.check_output( |
1401 | + ['status', service_name], |
1402 | + stderr=subprocess.STDOUT).decode('UTF-8') |
1403 | + except subprocess.CalledProcessError: |
1404 | + return False |
1405 | + else: |
1406 | + # This works for upstart scripts where the 'service' command |
1407 | + # returns a consistent string to represent running |
1408 | + # 'start/running' |
1409 | + if ("start/running" in output or |
1410 | + "is running" in output or |
1411 | + "up and running" in output): |
1412 | + return True |
1413 | + elif os.path.exists(_INIT_D_CONF.format(service_name)): |
1414 | + # Check System V scripts init script return codes |
1415 | + return service('status', service_name) |
1416 | return False |
1417 | - else: |
1418 | - if ("start/running" in output or "is running" in output): |
1419 | - return True |
1420 | - else: |
1421 | - return False |
1422 | - |
1423 | - |
1424 | -def service_available(service_name): |
1425 | - """Determine whether a system service is available""" |
1426 | - try: |
1427 | - subprocess.check_output( |
1428 | - ['service', service_name, 'status'], |
1429 | - stderr=subprocess.STDOUT).decode('UTF-8') |
1430 | - except subprocess.CalledProcessError as e: |
1431 | - return b'unrecognized service' not in e.output |
1432 | - else: |
1433 | - return True |
1434 | - |
1435 | - |
1436 | -def adduser(username, password=None, shell='/bin/bash', system_user=False): |
1437 | - """Add a user to the system""" |
1438 | + |
1439 | + |
1440 | +SYSTEMD_SYSTEM = '/run/systemd/system' |
1441 | + |
1442 | + |
1443 | +def init_is_systemd(): |
1444 | + """Return True if the host system uses systemd, False otherwise.""" |
1445 | + return os.path.isdir(SYSTEMD_SYSTEM) |
1446 | + |
1447 | + |
1448 | +def adduser(username, password=None, shell='/bin/bash', |
1449 | + system_user=False, primary_group=None, |
1450 | + secondary_groups=None, uid=None, home_dir=None): |
1451 | + """Add a user to the system. |
1452 | + |
1453 | + Will log but otherwise succeed if the user already exists. |
1454 | + |
1455 | + :param str username: Username to create |
1456 | + :param str password: Password for user; if ``None``, create a system user |
1457 | + :param str shell: The default shell for the user |
1458 | + :param bool system_user: Whether to create a login or system user |
1459 | + :param str primary_group: Primary group for user; defaults to username |
1460 | + :param list secondary_groups: Optional list of additional groups |
1461 | + :param int uid: UID for user being created |
1462 | + :param str home_dir: Home directory for user |
1463 | + |
1464 | + :returns: The password database entry struct, as returned by `pwd.getpwnam` |
1465 | + """ |
1466 | try: |
1467 | user_info = pwd.getpwnam(username) |
1468 | log('user {0} already exists!'.format(username)) |
1469 | + if uid: |
1470 | + user_info = pwd.getpwuid(int(uid)) |
1471 | + log('user with uid {0} already exists!'.format(uid)) |
1472 | except KeyError: |
1473 | log('creating user {0}'.format(username)) |
1474 | cmd = ['useradd'] |
1475 | + if uid: |
1476 | + cmd.extend(['--uid', str(uid)]) |
1477 | + if home_dir: |
1478 | + cmd.extend(['--home', str(home_dir)]) |
1479 | if system_user or password is None: |
1480 | cmd.append('--system') |
1481 | else: |
1482 | @@ -112,39 +219,89 @@ |
1483 | '--shell', shell, |
1484 | '--password', password, |
1485 | ]) |
1486 | + if not primary_group: |
1487 | + try: |
1488 | + grp.getgrnam(username) |
1489 | + primary_group = username # avoid "group exists" error |
1490 | + except KeyError: |
1491 | + pass |
1492 | + if primary_group: |
1493 | + cmd.extend(['-g', primary_group]) |
1494 | + if secondary_groups: |
1495 | + cmd.extend(['-G', ','.join(secondary_groups)]) |
1496 | cmd.append(username) |
1497 | subprocess.check_call(cmd) |
1498 | user_info = pwd.getpwnam(username) |
1499 | return user_info |
1500 | |
1501 | |
1502 | -def add_group(group_name, system_group=False): |
1503 | - """Add a group to the system""" |
1504 | +def user_exists(username): |
1505 | + """Check if a user exists""" |
1506 | + try: |
1507 | + pwd.getpwnam(username) |
1508 | + user_exists = True |
1509 | + except KeyError: |
1510 | + user_exists = False |
1511 | + return user_exists |
1512 | + |
1513 | + |
1514 | +def uid_exists(uid): |
1515 | + """Check if a uid exists""" |
1516 | + try: |
1517 | + pwd.getpwuid(uid) |
1518 | + uid_exists = True |
1519 | + except KeyError: |
1520 | + uid_exists = False |
1521 | + return uid_exists |
1522 | + |
1523 | + |
1524 | +def group_exists(groupname): |
1525 | + """Check if a group exists""" |
1526 | + try: |
1527 | + grp.getgrnam(groupname) |
1528 | + group_exists = True |
1529 | + except KeyError: |
1530 | + group_exists = False |
1531 | + return group_exists |
1532 | + |
1533 | + |
1534 | +def gid_exists(gid): |
1535 | + """Check if a gid exists""" |
1536 | + try: |
1537 | + grp.getgrgid(gid) |
1538 | + gid_exists = True |
1539 | + except KeyError: |
1540 | + gid_exists = False |
1541 | + return gid_exists |
1542 | + |
1543 | + |
1544 | +def add_group(group_name, system_group=False, gid=None): |
1545 | + """Add a group to the system |
1546 | + |
1547 | + Will log but otherwise succeed if the group already exists. |
1548 | + |
1549 | + :param str group_name: group to create |
1550 | + :param bool system_group: Create system group |
1551 | + :param int gid: GID for user being created |
1552 | + |
1553 | + :returns: The password database entry struct, as returned by `grp.getgrnam` |
1554 | + """ |
1555 | try: |
1556 | group_info = grp.getgrnam(group_name) |
1557 | log('group {0} already exists!'.format(group_name)) |
1558 | + if gid: |
1559 | + group_info = grp.getgrgid(gid) |
1560 | + log('group with gid {0} already exists!'.format(gid)) |
1561 | except KeyError: |
1562 | log('creating group {0}'.format(group_name)) |
1563 | - cmd = ['addgroup'] |
1564 | - if system_group: |
1565 | - cmd.append('--system') |
1566 | - else: |
1567 | - cmd.extend([ |
1568 | - '--group', |
1569 | - ]) |
1570 | - cmd.append(group_name) |
1571 | - subprocess.check_call(cmd) |
1572 | + add_new_group(group_name, system_group, gid) |
1573 | group_info = grp.getgrnam(group_name) |
1574 | return group_info |
1575 | |
1576 | |
1577 | def add_user_to_group(username, group): |
1578 | """Add a user to a group""" |
1579 | - cmd = [ |
1580 | - 'gpasswd', '-a', |
1581 | - username, |
1582 | - group |
1583 | - ] |
1584 | + cmd = ['gpasswd', '-a', username, group] |
1585 | log("Adding user {} to group {}".format(username, group)) |
1586 | subprocess.check_call(cmd) |
1587 | |
1588 | @@ -203,14 +360,12 @@ |
1589 | |
1590 | |
1591 | def fstab_remove(mp): |
1592 | - """Remove the given mountpoint entry from /etc/fstab |
1593 | - """ |
1594 | + """Remove the given mountpoint entry from /etc/fstab""" |
1595 | return Fstab.remove_by_mountpoint(mp) |
1596 | |
1597 | |
1598 | def fstab_add(dev, mp, fs, options=None): |
1599 | - """Adds the given device entry to the /etc/fstab file |
1600 | - """ |
1601 | + """Adds the given device entry to the /etc/fstab file""" |
1602 | return Fstab.add(dev, mp, fs, options=options) |
1603 | |
1604 | |
1605 | @@ -254,9 +409,19 @@ |
1606 | return system_mounts |
1607 | |
1608 | |
1609 | +def fstab_mount(mountpoint): |
1610 | + """Mount filesystem using fstab""" |
1611 | + cmd_args = ['mount', mountpoint] |
1612 | + try: |
1613 | + subprocess.check_output(cmd_args) |
1614 | + except subprocess.CalledProcessError as e: |
1615 | + log('Error unmounting {}\n{}'.format(mountpoint, e.output)) |
1616 | + return False |
1617 | + return True |
1618 | + |
1619 | + |
1620 | def file_hash(path, hash_type='md5'): |
1621 | - """ |
1622 | - Generate a hash checksum of the contents of 'path' or None if not found. |
1623 | + """Generate a hash checksum of the contents of 'path' or None if not found. |
1624 | |
1625 | :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`, |
1626 | such as md5, sha1, sha256, sha512, etc. |
1627 | @@ -271,10 +436,9 @@ |
1628 | |
1629 | |
1630 | def path_hash(path): |
1631 | - """ |
1632 | - Generate a hash checksum of all files matching 'path'. Standard wildcards |
1633 | - like '*' and '?' are supported, see documentation for the 'glob' module for |
1634 | - more information. |
1635 | + """Generate a hash checksum of all files matching 'path'. Standard |
1636 | + wildcards like '*' and '?' are supported, see documentation for the 'glob' |
1637 | + module for more information. |
1638 | |
1639 | :return: dict: A { filename: hash } dictionary for all matched files. |
1640 | Empty if none found. |
1641 | @@ -286,8 +450,7 @@ |
1642 | |
1643 | |
1644 | def check_hash(path, checksum, hash_type='md5'): |
1645 | - """ |
1646 | - Validate a file using a cryptographic checksum. |
1647 | + """Validate a file using a cryptographic checksum. |
1648 | |
1649 | :param str checksum: Value of the checksum used to validate the file. |
1650 | :param str hash_type: Hash algorithm used to generate `checksum`. |
1651 | @@ -302,10 +465,11 @@ |
1652 | |
1653 | |
1654 | class ChecksumError(ValueError): |
1655 | + """A class derived from Value error to indicate the checksum failed.""" |
1656 | pass |
1657 | |
1658 | |
1659 | -def restart_on_change(restart_map, stopstart=False): |
1660 | +def restart_on_change(restart_map, stopstart=False, restart_functions=None): |
1661 | """Restart services based on configuration files changing |
1662 | |
1663 | This function is used a decorator, for example:: |
1664 | @@ -323,35 +487,56 @@ |
1665 | restarted if any file matching the pattern got changed, created |
1666 | or removed. Standard wildcards are supported, see documentation |
1667 | for the 'glob' module for more information. |
1668 | + |
1669 | + @param restart_map: {path_file_name: [service_name, ...] |
1670 | + @param stopstart: DEFAULT false; whether to stop, start OR restart |
1671 | + @param restart_functions: nonstandard functions to use to restart services |
1672 | + {svc: func, ...} |
1673 | + @returns result from decorated function |
1674 | """ |
1675 | def wrap(f): |
1676 | + @functools.wraps(f) |
1677 | def wrapped_f(*args, **kwargs): |
1678 | - checksums = {path: path_hash(path) for path in restart_map} |
1679 | - f(*args, **kwargs) |
1680 | - restarts = [] |
1681 | - for path in restart_map: |
1682 | - if path_hash(path) != checksums[path]: |
1683 | - restarts += restart_map[path] |
1684 | - services_list = list(OrderedDict.fromkeys(restarts)) |
1685 | - if not stopstart: |
1686 | - for service_name in services_list: |
1687 | - service('restart', service_name) |
1688 | - else: |
1689 | - for action in ['stop', 'start']: |
1690 | - for service_name in services_list: |
1691 | - service(action, service_name) |
1692 | + return restart_on_change_helper( |
1693 | + (lambda: f(*args, **kwargs)), restart_map, stopstart, |
1694 | + restart_functions) |
1695 | return wrapped_f |
1696 | return wrap |
1697 | |
1698 | |
1699 | -def lsb_release(): |
1700 | - """Return /etc/lsb-release in a dict""" |
1701 | - d = {} |
1702 | - with open('/etc/lsb-release', 'r') as lsb: |
1703 | - for l in lsb: |
1704 | - k, v = l.split('=') |
1705 | - d[k.strip()] = v.strip() |
1706 | - return d |
1707 | +def restart_on_change_helper(lambda_f, restart_map, stopstart=False, |
1708 | + restart_functions=None): |
1709 | + """Helper function to perform the restart_on_change function. |
1710 | + |
1711 | + This is provided for decorators to restart services if files described |
1712 | + in the restart_map have changed after an invocation of lambda_f(). |
1713 | + |
1714 | + @param lambda_f: function to call. |
1715 | + @param restart_map: {file: [service, ...]} |
1716 | + @param stopstart: whether to stop, start or restart a service |
1717 | + @param restart_functions: nonstandard functions to use to restart services |
1718 | + {svc: func, ...} |
1719 | + @returns result of lambda_f() |
1720 | + """ |
1721 | + if restart_functions is None: |
1722 | + restart_functions = {} |
1723 | + checksums = {path: path_hash(path) for path in restart_map} |
1724 | + r = lambda_f() |
1725 | + # create a list of lists of the services to restart |
1726 | + restarts = [restart_map[path] |
1727 | + for path in restart_map |
1728 | + if path_hash(path) != checksums[path]] |
1729 | + # create a flat list of ordered services without duplicates from lists |
1730 | + services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts))) |
1731 | + if services_list: |
1732 | + actions = ('stop', 'start') if stopstart else ('restart',) |
1733 | + for service_name in services_list: |
1734 | + if service_name in restart_functions: |
1735 | + restart_functions[service_name](service_name) |
1736 | + else: |
1737 | + for action in actions: |
1738 | + service(action, service_name) |
1739 | + return r |
1740 | |
1741 | |
1742 | def pwgen(length=None): |
1743 | @@ -370,36 +555,92 @@ |
1744 | return(''.join(random_chars)) |
1745 | |
1746 | |
1747 | -def list_nics(nic_type): |
1748 | - '''Return a list of nics of given type(s)''' |
1749 | +def is_phy_iface(interface): |
1750 | + """Returns True if interface is not virtual, otherwise False.""" |
1751 | + if interface: |
1752 | + sys_net = '/sys/class/net' |
1753 | + if os.path.isdir(sys_net): |
1754 | + for iface in glob.glob(os.path.join(sys_net, '*')): |
1755 | + if '/virtual/' in os.path.realpath(iface): |
1756 | + continue |
1757 | + |
1758 | + if interface == os.path.basename(iface): |
1759 | + return True |
1760 | + |
1761 | + return False |
1762 | + |
1763 | + |
1764 | +def get_bond_master(interface): |
1765 | + """Returns bond master if interface is bond slave otherwise None. |
1766 | + |
1767 | + NOTE: the provided interface is expected to be physical |
1768 | + """ |
1769 | + if interface: |
1770 | + iface_path = '/sys/class/net/%s' % (interface) |
1771 | + if os.path.exists(iface_path): |
1772 | + if '/virtual/' in os.path.realpath(iface_path): |
1773 | + return None |
1774 | + |
1775 | + master = os.path.join(iface_path, 'master') |
1776 | + if os.path.exists(master): |
1777 | + master = os.path.realpath(master) |
1778 | + # make sure it is a bond master |
1779 | + if os.path.exists(os.path.join(master, 'bonding')): |
1780 | + return os.path.basename(master) |
1781 | + |
1782 | + return None |
1783 | + |
1784 | + |
1785 | +def list_nics(nic_type=None): |
1786 | + """Return a list of nics of given type(s)""" |
1787 | if isinstance(nic_type, six.string_types): |
1788 | int_types = [nic_type] |
1789 | else: |
1790 | int_types = nic_type |
1791 | + |
1792 | interfaces = [] |
1793 | - for int_type in int_types: |
1794 | - cmd = ['ip', 'addr', 'show', 'label', int_type + '*'] |
1795 | + if nic_type: |
1796 | + for int_type in int_types: |
1797 | + cmd = ['ip', 'addr', 'show', 'label', int_type + '*'] |
1798 | + ip_output = subprocess.check_output(cmd).decode('UTF-8') |
1799 | + ip_output = ip_output.split('\n') |
1800 | + ip_output = (line for line in ip_output if line) |
1801 | + for line in ip_output: |
1802 | + if line.split()[1].startswith(int_type): |
1803 | + matched = re.search('.*: (' + int_type + |
1804 | + r'[0-9]+\.[0-9]+)@.*', line) |
1805 | + if matched: |
1806 | + iface = matched.groups()[0] |
1807 | + else: |
1808 | + iface = line.split()[1].replace(":", "") |
1809 | + |
1810 | + if iface not in interfaces: |
1811 | + interfaces.append(iface) |
1812 | + else: |
1813 | + cmd = ['ip', 'a'] |
1814 | ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n') |
1815 | - ip_output = (line for line in ip_output if line) |
1816 | + ip_output = (line.strip() for line in ip_output if line) |
1817 | + |
1818 | + key = re.compile('^[0-9]+:\s+(.+):') |
1819 | for line in ip_output: |
1820 | - if line.split()[1].startswith(int_type): |
1821 | - matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line) |
1822 | - if matched: |
1823 | - interface = matched.groups()[0] |
1824 | - else: |
1825 | - interface = line.split()[1].replace(":", "") |
1826 | - interfaces.append(interface) |
1827 | + matched = re.search(key, line) |
1828 | + if matched: |
1829 | + iface = matched.group(1) |
1830 | + iface = iface.partition("@")[0] |
1831 | + if iface not in interfaces: |
1832 | + interfaces.append(iface) |
1833 | |
1834 | return interfaces |
1835 | |
1836 | |
1837 | def set_nic_mtu(nic, mtu): |
1838 | - '''Set MTU on a network interface''' |
1839 | + """Set the Maximum Transmission Unit (MTU) on a network interface.""" |
1840 | cmd = ['ip', 'link', 'set', nic, 'mtu', mtu] |
1841 | subprocess.check_call(cmd) |
1842 | |
1843 | |
1844 | def get_nic_mtu(nic): |
1845 | + """Return the Maximum Transmission Unit (MTU) for a network interface.""" |
1846 | cmd = ['ip', 'addr', 'show', nic] |
1847 | ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n') |
1848 | mtu = "" |
1849 | @@ -411,6 +652,7 @@ |
1850 | |
1851 | |
1852 | def get_nic_hwaddr(nic): |
1853 | + """Return the Media Access Control (MAC) for a network interface.""" |
1854 | cmd = ['ip', '-o', '-0', 'addr', 'show', nic] |
1855 | ip_output = subprocess.check_output(cmd).decode('UTF-8') |
1856 | hwaddr = "" |
1857 | @@ -420,35 +662,31 @@ |
1858 | return hwaddr |
1859 | |
1860 | |
1861 | -def cmp_pkgrevno(package, revno, pkgcache=None): |
1862 | - '''Compare supplied revno with the revno of the installed package |
1863 | - |
1864 | - * 1 => Installed revno is greater than supplied arg |
1865 | - * 0 => Installed revno is the same as supplied arg |
1866 | - * -1 => Installed revno is less than supplied arg |
1867 | - |
1868 | - This function imports apt_cache function from charmhelpers.fetch if |
1869 | - the pkgcache argument is None. Be sure to add charmhelpers.fetch if |
1870 | - you call this function, or pass an apt_pkg.Cache() instance. |
1871 | - ''' |
1872 | - import apt_pkg |
1873 | - if not pkgcache: |
1874 | - from charmhelpers.fetch import apt_cache |
1875 | - pkgcache = apt_cache() |
1876 | - pkg = pkgcache[package] |
1877 | - return apt_pkg.version_compare(pkg.current_ver.ver_str, revno) |
1878 | - |
1879 | - |
1880 | @contextmanager |
1881 | -def chdir(d): |
1882 | +def chdir(directory): |
1883 | + """Change the current working directory to a different directory for a code |
1884 | + block and return the previous directory after the block exits. Useful to |
1885 | + run commands from a specificed directory. |
1886 | + |
1887 | + :param str directory: The directory path to change to for this context. |
1888 | + """ |
1889 | cur = os.getcwd() |
1890 | try: |
1891 | - yield os.chdir(d) |
1892 | + yield os.chdir(directory) |
1893 | finally: |
1894 | os.chdir(cur) |
1895 | |
1896 | |
1897 | -def chownr(path, owner, group, follow_links=True): |
1898 | +def chownr(path, owner, group, follow_links=True, chowntopdir=False): |
1899 | + """Recursively change user and group ownership of files and directories |
1900 | + in given path. Doesn't chown path itself by default, only its children. |
1901 | + |
1902 | + :param str path: The string path to start changing ownership. |
1903 | + :param str owner: The owner string to use when looking up the uid. |
1904 | + :param str group: The group string to use when looking up the gid. |
1905 | + :param bool follow_links: Also Chown links if True |
1906 | + :param bool chowntopdir: Also chown path itself if True |
1907 | + """ |
1908 | uid = pwd.getpwnam(owner).pw_uid |
1909 | gid = grp.getgrnam(group).gr_gid |
1910 | if follow_links: |
1911 | @@ -456,6 +694,10 @@ |
1912 | else: |
1913 | chown = os.lchown |
1914 | |
1915 | + if chowntopdir: |
1916 | + broken_symlink = os.path.lexists(path) and not os.path.exists(path) |
1917 | + if not broken_symlink: |
1918 | + chown(path, uid, gid) |
1919 | for root, dirs, files in os.walk(path): |
1920 | for name in dirs + files: |
1921 | full = os.path.join(root, name) |
1922 | @@ -465,4 +707,28 @@ |
1923 | |
1924 | |
1925 | def lchownr(path, owner, group): |
1926 | + """Recursively change user and group ownership of files and directories |
1927 | + in a given path, not following symbolic links. See the documentation for |
1928 | + 'os.lchown' for more information. |
1929 | + |
1930 | + :param str path: The string path to start changing ownership. |
1931 | + :param str owner: The owner string to use when looking up the uid. |
1932 | + :param str group: The group string to use when looking up the gid. |
1933 | + """ |
1934 | chownr(path, owner, group, follow_links=False) |
1935 | + |
1936 | + |
1937 | +def get_total_ram(): |
1938 | + """The total amount of system RAM in bytes. |
1939 | + |
1940 | + This is what is reported by the OS, and may be overcommitted when |
1941 | + there are multiple containers hosted on the same machine. |
1942 | + """ |
1943 | + with open('/proc/meminfo', 'r') as f: |
1944 | + for line in f.readlines(): |
1945 | + if line: |
1946 | + key, value, unit = line.split() |
1947 | + if key == 'MemTotal:': |
1948 | + assert unit == 'kB', 'Unknown unit' |
1949 | + return int(value) * 1024 # Classic, not KiB. |
1950 | + raise NotImplementedError() |
1951 | |
1952 | === added directory 'hooks/charmhelpers/core/host_factory' |
1953 | === added file 'hooks/charmhelpers/core/host_factory/__init__.py' |
1954 | === added file 'hooks/charmhelpers/core/host_factory/centos.py' |
1955 | --- hooks/charmhelpers/core/host_factory/centos.py 1970-01-01 00:00:00 +0000 |
1956 | +++ hooks/charmhelpers/core/host_factory/centos.py 2016-09-16 19:19:06 +0000 |
1957 | @@ -0,0 +1,56 @@ |
1958 | +import subprocess |
1959 | +import yum |
1960 | +import os |
1961 | + |
1962 | + |
1963 | +def service_available(service_name): |
1964 | + # """Determine whether a system service is available.""" |
1965 | + if os.path.isdir('/run/systemd/system'): |
1966 | + cmd = ['systemctl', 'is-enabled', service_name] |
1967 | + else: |
1968 | + cmd = ['service', service_name, 'is-enabled'] |
1969 | + return subprocess.call(cmd) == 0 |
1970 | + |
1971 | + |
1972 | +def add_new_group(group_name, system_group=False, gid=None): |
1973 | + cmd = ['groupadd'] |
1974 | + if gid: |
1975 | + cmd.extend(['--gid', str(gid)]) |
1976 | + if system_group: |
1977 | + cmd.append('-r') |
1978 | + cmd.append(group_name) |
1979 | + subprocess.check_call(cmd) |
1980 | + |
1981 | + |
1982 | +def lsb_release(): |
1983 | + """Return /etc/os-release in a dict.""" |
1984 | + d = {} |
1985 | + with open('/etc/os-release', 'r') as lsb: |
1986 | + for l in lsb: |
1987 | + s = l.split('=') |
1988 | + if len(s) != 2: |
1989 | + continue |
1990 | + d[s[0].strip()] = s[1].strip() |
1991 | + return d |
1992 | + |
1993 | + |
1994 | +def cmp_pkgrevno(package, revno, pkgcache=None): |
1995 | + """Compare supplied revno with the revno of the installed package. |
1996 | + |
1997 | + * 1 => Installed revno is greater than supplied arg |
1998 | + * 0 => Installed revno is the same as supplied arg |
1999 | + * -1 => Installed revno is less than supplied arg |
2000 | + |
2001 | + This function imports YumBase function if the pkgcache argument |
2002 | + is None. |
2003 | + """ |
2004 | + if not pkgcache: |
2005 | + y = yum.YumBase() |
2006 | + packages = y.doPackageLists() |
2007 | + pkgcache = {i.Name: i.version for i in packages['installed']} |
2008 | + pkg = pkgcache[package] |
2009 | + if pkg > revno: |
2010 | + return 1 |
2011 | + if pkg < revno: |
2012 | + return -1 |
2013 | + return 0 |
2014 | |
2015 | === added file 'hooks/charmhelpers/core/host_factory/ubuntu.py' |
2016 | --- hooks/charmhelpers/core/host_factory/ubuntu.py 1970-01-01 00:00:00 +0000 |
2017 | +++ hooks/charmhelpers/core/host_factory/ubuntu.py 2016-09-16 19:19:06 +0000 |
2018 | @@ -0,0 +1,56 @@ |
2019 | +import subprocess |
2020 | + |
2021 | + |
2022 | +def service_available(service_name): |
2023 | + """Determine whether a system service is available""" |
2024 | + try: |
2025 | + subprocess.check_output( |
2026 | + ['service', service_name, 'status'], |
2027 | + stderr=subprocess.STDOUT).decode('UTF-8') |
2028 | + except subprocess.CalledProcessError as e: |
2029 | + return b'unrecognized service' not in e.output |
2030 | + else: |
2031 | + return True |
2032 | + |
2033 | + |
2034 | +def add_new_group(group_name, system_group=False, gid=None): |
2035 | + cmd = ['addgroup'] |
2036 | + if gid: |
2037 | + cmd.extend(['--gid', str(gid)]) |
2038 | + if system_group: |
2039 | + cmd.append('--system') |
2040 | + else: |
2041 | + cmd.extend([ |
2042 | + '--group', |
2043 | + ]) |
2044 | + cmd.append(group_name) |
2045 | + subprocess.check_call(cmd) |
2046 | + |
2047 | + |
2048 | +def lsb_release(): |
2049 | + """Return /etc/lsb-release in a dict""" |
2050 | + d = {} |
2051 | + with open('/etc/lsb-release', 'r') as lsb: |
2052 | + for l in lsb: |
2053 | + k, v = l.split('=') |
2054 | + d[k.strip()] = v.strip() |
2055 | + return d |
2056 | + |
2057 | + |
2058 | +def cmp_pkgrevno(package, revno, pkgcache=None): |
2059 | + """Compare supplied revno with the revno of the installed package. |
2060 | + |
2061 | + * 1 => Installed revno is greater than supplied arg |
2062 | + * 0 => Installed revno is the same as supplied arg |
2063 | + * -1 => Installed revno is less than supplied arg |
2064 | + |
2065 | + This function imports apt_cache function from charmhelpers.fetch if |
2066 | + the pkgcache argument is None. Be sure to add charmhelpers.fetch if |
2067 | + you call this function, or pass an apt_pkg.Cache() instance. |
2068 | + """ |
2069 | + import apt_pkg |
2070 | + if not pkgcache: |
2071 | + from charmhelpers.fetch import apt_cache |
2072 | + pkgcache = apt_cache() |
2073 | + pkg = pkgcache[package] |
2074 | + return apt_pkg.version_compare(pkg.current_ver.ver_str, revno) |
2075 | |
2076 | === added file 'hooks/charmhelpers/core/hugepage.py' |
2077 | --- hooks/charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000 |
2078 | +++ hooks/charmhelpers/core/hugepage.py 2016-09-16 19:19:06 +0000 |
2079 | @@ -0,0 +1,69 @@ |
2080 | +# -*- coding: utf-8 -*- |
2081 | + |
2082 | +# Copyright 2014-2015 Canonical Limited. |
2083 | +# |
2084 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
2085 | +# you may not use this file except in compliance with the License. |
2086 | +# You may obtain a copy of the License at |
2087 | +# |
2088 | +# http://www.apache.org/licenses/LICENSE-2.0 |
2089 | +# |
2090 | +# Unless required by applicable law or agreed to in writing, software |
2091 | +# distributed under the License is distributed on an "AS IS" BASIS, |
2092 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2093 | +# See the License for the specific language governing permissions and |
2094 | +# limitations under the License. |
2095 | + |
2096 | +import yaml |
2097 | +from charmhelpers.core import fstab |
2098 | +from charmhelpers.core import sysctl |
2099 | +from charmhelpers.core.host import ( |
2100 | + add_group, |
2101 | + add_user_to_group, |
2102 | + fstab_mount, |
2103 | + mkdir, |
2104 | +) |
2105 | +from charmhelpers.core.strutils import bytes_from_string |
2106 | +from subprocess import check_output |
2107 | + |
2108 | + |
2109 | +def hugepage_support(user, group='hugetlb', nr_hugepages=256, |
2110 | + max_map_count=65536, mnt_point='/run/hugepages/kvm', |
2111 | + pagesize='2MB', mount=True, set_shmmax=False): |
2112 | + """Enable hugepages on system. |
2113 | + |
2114 | + Args: |
2115 | + user (str) -- Username to allow access to hugepages to |
2116 | + group (str) -- Group name to own hugepages |
2117 | + nr_hugepages (int) -- Number of pages to reserve |
2118 | + max_map_count (int) -- Number of Virtual Memory Areas a process can own |
2119 | + mnt_point (str) -- Directory to mount hugepages on |
2120 | + pagesize (str) -- Size of hugepages |
2121 | + mount (bool) -- Whether to Mount hugepages |
2122 | + """ |
2123 | + group_info = add_group(group) |
2124 | + gid = group_info.gr_gid |
2125 | + add_user_to_group(user, group) |
2126 | + if max_map_count < 2 * nr_hugepages: |
2127 | + max_map_count = 2 * nr_hugepages |
2128 | + sysctl_settings = { |
2129 | + 'vm.nr_hugepages': nr_hugepages, |
2130 | + 'vm.max_map_count': max_map_count, |
2131 | + 'vm.hugetlb_shm_group': gid, |
2132 | + } |
2133 | + if set_shmmax: |
2134 | + shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax'])) |
2135 | + shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages |
2136 | + if shmmax_minsize > shmmax_current: |
2137 | + sysctl_settings['kernel.shmmax'] = shmmax_minsize |
2138 | + sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf') |
2139 | + mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False) |
2140 | + lfstab = fstab.Fstab() |
2141 | + fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point) |
2142 | + if fstab_entry: |
2143 | + lfstab.remove_entry(fstab_entry) |
2144 | + entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs', |
2145 | + 'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0) |
2146 | + lfstab.add_entry(entry) |
2147 | + if mount: |
2148 | + fstab_mount(mnt_point) |
2149 | |
2150 | === added file 'hooks/charmhelpers/core/kernel.py' |
2151 | --- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000 |
2152 | +++ hooks/charmhelpers/core/kernel.py 2016-09-16 19:19:06 +0000 |
2153 | @@ -0,0 +1,72 @@ |
2154 | +#!/usr/bin/env python |
2155 | +# -*- coding: utf-8 -*- |
2156 | + |
2157 | +# Copyright 2014-2015 Canonical Limited. |
2158 | +# |
2159 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
2160 | +# you may not use this file except in compliance with the License. |
2161 | +# You may obtain a copy of the License at |
2162 | +# |
2163 | +# http://www.apache.org/licenses/LICENSE-2.0 |
2164 | +# |
2165 | +# Unless required by applicable law or agreed to in writing, software |
2166 | +# distributed under the License is distributed on an "AS IS" BASIS, |
2167 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2168 | +# See the License for the specific language governing permissions and |
2169 | +# limitations under the License. |
2170 | + |
2171 | +import re |
2172 | +import subprocess |
2173 | + |
2174 | +from charmhelpers.osplatform import get_platform |
2175 | +from charmhelpers.core.hookenv import ( |
2176 | + log, |
2177 | + INFO |
2178 | +) |
2179 | + |
2180 | +__platform__ = get_platform() |
2181 | +if __platform__ == "ubuntu": |
2182 | + from charmhelpers.core.kernel_factory.ubuntu import ( |
2183 | + persistent_modprobe, |
2184 | + update_initramfs, |
2185 | + ) # flake8: noqa -- ignore F401 for this import |
2186 | +elif __platform__ == "centos": |
2187 | + from charmhelpers.core.kernel_factory.centos import ( |
2188 | + persistent_modprobe, |
2189 | + update_initramfs, |
2190 | + ) # flake8: noqa -- ignore F401 for this import |
2191 | + |
2192 | +__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>" |
2193 | + |
2194 | + |
2195 | +def modprobe(module, persist=True): |
2196 | + """Load a kernel module and configure for auto-load on reboot.""" |
2197 | + cmd = ['modprobe', module] |
2198 | + |
2199 | + log('Loading kernel module %s' % module, level=INFO) |
2200 | + |
2201 | + subprocess.check_call(cmd) |
2202 | + if persist: |
2203 | + persistent_modprobe(module) |
2204 | + |
2205 | + |
2206 | +def rmmod(module, force=False): |
2207 | + """Remove a module from the linux kernel""" |
2208 | + cmd = ['rmmod'] |
2209 | + if force: |
2210 | + cmd.append('-f') |
2211 | + cmd.append(module) |
2212 | + log('Removing kernel module %s' % module, level=INFO) |
2213 | + return subprocess.check_call(cmd) |
2214 | + |
2215 | + |
2216 | +def lsmod(): |
2217 | + """Shows what kernel modules are currently loaded""" |
2218 | + return subprocess.check_output(['lsmod'], |
2219 | + universal_newlines=True) |
2220 | + |
2221 | + |
2222 | +def is_module_loaded(module): |
2223 | + """Checks if a kernel module is already loaded""" |
2224 | + matches = re.findall('^%s[ ]+' % module, lsmod(), re.M) |
2225 | + return len(matches) > 0 |
2226 | |
2227 | === added directory 'hooks/charmhelpers/core/kernel_factory' |
2228 | === added file 'hooks/charmhelpers/core/kernel_factory/__init__.py' |
2229 | === added file 'hooks/charmhelpers/core/kernel_factory/centos.py' |
2230 | --- hooks/charmhelpers/core/kernel_factory/centos.py 1970-01-01 00:00:00 +0000 |
2231 | +++ hooks/charmhelpers/core/kernel_factory/centos.py 2016-09-16 19:19:06 +0000 |
2232 | @@ -0,0 +1,17 @@ |
2233 | +import subprocess |
2234 | +import os |
2235 | + |
2236 | + |
2237 | +def persistent_modprobe(module): |
2238 | + """Load a kernel module and configure for auto-load on reboot.""" |
2239 | + if not os.path.exists('/etc/rc.modules'): |
2240 | + open('/etc/rc.modules', 'a') |
2241 | + os.chmod('/etc/rc.modules', 111) |
2242 | + with open('/etc/rc.modules', 'r+') as modules: |
2243 | + if module not in modules.read(): |
2244 | + modules.write('modprobe %s\n' % module) |
2245 | + |
2246 | + |
2247 | +def update_initramfs(version='all'): |
2248 | + """Updates an initramfs image.""" |
2249 | + return subprocess.check_call(["dracut", "-f", version]) |
2250 | |
2251 | === added file 'hooks/charmhelpers/core/kernel_factory/ubuntu.py' |
2252 | --- hooks/charmhelpers/core/kernel_factory/ubuntu.py 1970-01-01 00:00:00 +0000 |
2253 | +++ hooks/charmhelpers/core/kernel_factory/ubuntu.py 2016-09-16 19:19:06 +0000 |
2254 | @@ -0,0 +1,13 @@ |
2255 | +import subprocess |
2256 | + |
2257 | + |
2258 | +def persistent_modprobe(module): |
2259 | + """Load a kernel module and configure for auto-load on reboot.""" |
2260 | + with open('/etc/modules', 'r+') as modules: |
2261 | + if module not in modules.read(): |
2262 | + modules.write(module) |
2263 | + |
2264 | + |
2265 | +def update_initramfs(version='all'): |
2266 | + """Updates an initramfs image.""" |
2267 | + return subprocess.check_call(["update-initramfs", "-k", version, "-u"]) |
2268 | |
2269 | === modified file 'hooks/charmhelpers/core/services/__init__.py' |
2270 | --- hooks/charmhelpers/core/services/__init__.py 2015-02-03 18:59:19 +0000 |
2271 | +++ hooks/charmhelpers/core/services/__init__.py 2016-09-16 19:19:06 +0000 |
2272 | @@ -1,18 +1,16 @@ |
2273 | # Copyright 2014-2015 Canonical Limited. |
2274 | # |
2275 | -# This file is part of charm-helpers. |
2276 | -# |
2277 | -# charm-helpers is free software: you can redistribute it and/or modify |
2278 | -# it under the terms of the GNU Lesser General Public License version 3 as |
2279 | -# published by the Free Software Foundation. |
2280 | -# |
2281 | -# charm-helpers is distributed in the hope that it will be useful, |
2282 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2283 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2284 | -# GNU Lesser General Public License for more details. |
2285 | -# |
2286 | -# You should have received a copy of the GNU Lesser General Public License |
2287 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
2288 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
2289 | +# you may not use this file except in compliance with the License. |
2290 | +# You may obtain a copy of the License at |
2291 | +# |
2292 | +# http://www.apache.org/licenses/LICENSE-2.0 |
2293 | +# |
2294 | +# Unless required by applicable law or agreed to in writing, software |
2295 | +# distributed under the License is distributed on an "AS IS" BASIS, |
2296 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2297 | +# See the License for the specific language governing permissions and |
2298 | +# limitations under the License. |
2299 | |
2300 | from .base import * # NOQA |
2301 | from .helpers import * # NOQA |
2302 | |
2303 | === modified file 'hooks/charmhelpers/core/services/base.py' |
2304 | --- hooks/charmhelpers/core/services/base.py 2015-07-14 14:05:42 +0000 |
2305 | +++ hooks/charmhelpers/core/services/base.py 2016-09-16 19:19:06 +0000 |
2306 | @@ -1,18 +1,16 @@ |
2307 | # Copyright 2014-2015 Canonical Limited. |
2308 | # |
2309 | -# This file is part of charm-helpers. |
2310 | -# |
2311 | -# charm-helpers is free software: you can redistribute it and/or modify |
2312 | -# it under the terms of the GNU Lesser General Public License version 3 as |
2313 | -# published by the Free Software Foundation. |
2314 | -# |
2315 | -# charm-helpers is distributed in the hope that it will be useful, |
2316 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2317 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2318 | -# GNU Lesser General Public License for more details. |
2319 | -# |
2320 | -# You should have received a copy of the GNU Lesser General Public License |
2321 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
2322 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
2323 | +# you may not use this file except in compliance with the License. |
2324 | +# You may obtain a copy of the License at |
2325 | +# |
2326 | +# http://www.apache.org/licenses/LICENSE-2.0 |
2327 | +# |
2328 | +# Unless required by applicable law or agreed to in writing, software |
2329 | +# distributed under the License is distributed on an "AS IS" BASIS, |
2330 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2331 | +# See the License for the specific language governing permissions and |
2332 | +# limitations under the License. |
2333 | |
2334 | import os |
2335 | import json |
2336 | |
2337 | === modified file 'hooks/charmhelpers/core/services/helpers.py' |
2338 | --- hooks/charmhelpers/core/services/helpers.py 2015-07-14 14:05:42 +0000 |
2339 | +++ hooks/charmhelpers/core/services/helpers.py 2016-09-16 19:19:06 +0000 |
2340 | @@ -1,22 +1,22 @@ |
2341 | # Copyright 2014-2015 Canonical Limited. |
2342 | # |
2343 | -# This file is part of charm-helpers. |
2344 | -# |
2345 | -# charm-helpers is free software: you can redistribute it and/or modify |
2346 | -# it under the terms of the GNU Lesser General Public License version 3 as |
2347 | -# published by the Free Software Foundation. |
2348 | -# |
2349 | -# charm-helpers is distributed in the hope that it will be useful, |
2350 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2351 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2352 | -# GNU Lesser General Public License for more details. |
2353 | -# |
2354 | -# You should have received a copy of the GNU Lesser General Public License |
2355 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
2356 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
2357 | +# you may not use this file except in compliance with the License. |
2358 | +# You may obtain a copy of the License at |
2359 | +# |
2360 | +# http://www.apache.org/licenses/LICENSE-2.0 |
2361 | +# |
2362 | +# Unless required by applicable law or agreed to in writing, software |
2363 | +# distributed under the License is distributed on an "AS IS" BASIS, |
2364 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2365 | +# See the License for the specific language governing permissions and |
2366 | +# limitations under the License. |
2367 | |
2368 | import os |
2369 | import yaml |
2370 | + |
2371 | from charmhelpers.core import hookenv |
2372 | +from charmhelpers.core import host |
2373 | from charmhelpers.core import templating |
2374 | |
2375 | from charmhelpers.core.services.base import ManagerCallback |
2376 | @@ -240,27 +240,50 @@ |
2377 | |
2378 | :param str source: The template source file, relative to |
2379 | `$CHARM_DIR/templates` |
2380 | - :param str target: The target to write the rendered template to |
2381 | + |
2382 | + :param str target: The target to write the rendered template to (or None) |
2383 | :param str owner: The owner of the rendered file |
2384 | :param str group: The group of the rendered file |
2385 | :param int perms: The permissions of the rendered file |
2386 | + :param partial on_change_action: functools partial to be executed when |
2387 | + rendered file changes |
2388 | + :param jinja2 loader template_loader: A jinja2 template loader |
2389 | |
2390 | + :return str: The rendered template |
2391 | """ |
2392 | def __init__(self, source, target, |
2393 | - owner='root', group='root', perms=0o444): |
2394 | + owner='root', group='root', perms=0o444, |
2395 | + on_change_action=None, template_loader=None): |
2396 | self.source = source |
2397 | self.target = target |
2398 | self.owner = owner |
2399 | self.group = group |
2400 | self.perms = perms |
2401 | + self.on_change_action = on_change_action |
2402 | + self.template_loader = template_loader |
2403 | |
2404 | def __call__(self, manager, service_name, event_name): |
2405 | + pre_checksum = '' |
2406 | + if self.on_change_action and os.path.isfile(self.target): |
2407 | + pre_checksum = host.file_hash(self.target) |
2408 | service = manager.get_service(service_name) |
2409 | - context = {} |
2410 | + context = {'ctx': {}} |
2411 | for ctx in service.get('required_data', []): |
2412 | context.update(ctx) |
2413 | - templating.render(self.source, self.target, context, |
2414 | - self.owner, self.group, self.perms) |
2415 | + context['ctx'].update(ctx) |
2416 | + |
2417 | + result = templating.render(self.source, self.target, context, |
2418 | + self.owner, self.group, self.perms, |
2419 | + template_loader=self.template_loader) |
2420 | + if self.on_change_action: |
2421 | + if pre_checksum == host.file_hash(self.target): |
2422 | + hookenv.log( |
2423 | + 'No change detected: {}'.format(self.target), |
2424 | + hookenv.DEBUG) |
2425 | + else: |
2426 | + self.on_change_action() |
2427 | + |
2428 | + return result |
2429 | |
2430 | |
2431 | # Convenience aliases for templates |
2432 | |
2433 | === modified file 'hooks/charmhelpers/core/strutils.py' |
2434 | --- hooks/charmhelpers/core/strutils.py 2015-07-14 14:05:42 +0000 |
2435 | +++ hooks/charmhelpers/core/strutils.py 2016-09-16 19:19:06 +0000 |
2436 | @@ -3,21 +3,20 @@ |
2437 | |
2438 | # Copyright 2014-2015 Canonical Limited. |
2439 | # |
2440 | -# This file is part of charm-helpers. |
2441 | -# |
2442 | -# charm-helpers is free software: you can redistribute it and/or modify |
2443 | -# it under the terms of the GNU Lesser General Public License version 3 as |
2444 | -# published by the Free Software Foundation. |
2445 | -# |
2446 | -# charm-helpers is distributed in the hope that it will be useful, |
2447 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2448 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2449 | -# GNU Lesser General Public License for more details. |
2450 | -# |
2451 | -# You should have received a copy of the GNU Lesser General Public License |
2452 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
2453 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
2454 | +# you may not use this file except in compliance with the License. |
2455 | +# You may obtain a copy of the License at |
2456 | +# |
2457 | +# http://www.apache.org/licenses/LICENSE-2.0 |
2458 | +# |
2459 | +# Unless required by applicable law or agreed to in writing, software |
2460 | +# distributed under the License is distributed on an "AS IS" BASIS, |
2461 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2462 | +# See the License for the specific language governing permissions and |
2463 | +# limitations under the License. |
2464 | |
2465 | import six |
2466 | +import re |
2467 | |
2468 | |
2469 | def bool_from_string(value): |
2470 | @@ -40,3 +39,32 @@ |
2471 | |
2472 | msg = "Unable to interpret string value '%s' as boolean" % (value) |
2473 | raise ValueError(msg) |
2474 | + |
2475 | + |
2476 | +def bytes_from_string(value): |
2477 | + """Interpret human readable string value as bytes. |
2478 | + |
2479 | + Returns int |
2480 | + """ |
2481 | + BYTE_POWER = { |
2482 | + 'K': 1, |
2483 | + 'KB': 1, |
2484 | + 'M': 2, |
2485 | + 'MB': 2, |
2486 | + 'G': 3, |
2487 | + 'GB': 3, |
2488 | + 'T': 4, |
2489 | + 'TB': 4, |
2490 | + 'P': 5, |
2491 | + 'PB': 5, |
2492 | + } |
2493 | + if isinstance(value, six.string_types): |
2494 | + value = six.text_type(value) |
2495 | + else: |
2496 | + msg = "Unable to interpret non-string value '%s' as boolean" % (value) |
2497 | + raise ValueError(msg) |
2498 | + matches = re.match("([0-9]+)([a-zA-Z]+)", value) |
2499 | + if not matches: |
2500 | + msg = "Unable to interpret string value '%s' as bytes" % (value) |
2501 | + raise ValueError(msg) |
2502 | + return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)]) |
2503 | |
2504 | === modified file 'hooks/charmhelpers/core/sysctl.py' |
2505 | --- hooks/charmhelpers/core/sysctl.py 2015-02-12 20:10:50 +0000 |
2506 | +++ hooks/charmhelpers/core/sysctl.py 2016-09-16 19:19:06 +0000 |
2507 | @@ -3,19 +3,17 @@ |
2508 | |
2509 | # Copyright 2014-2015 Canonical Limited. |
2510 | # |
2511 | -# This file is part of charm-helpers. |
2512 | -# |
2513 | -# charm-helpers is free software: you can redistribute it and/or modify |
2514 | -# it under the terms of the GNU Lesser General Public License version 3 as |
2515 | -# published by the Free Software Foundation. |
2516 | -# |
2517 | -# charm-helpers is distributed in the hope that it will be useful, |
2518 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2519 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2520 | -# GNU Lesser General Public License for more details. |
2521 | -# |
2522 | -# You should have received a copy of the GNU Lesser General Public License |
2523 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
2524 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
2525 | +# you may not use this file except in compliance with the License. |
2526 | +# You may obtain a copy of the License at |
2527 | +# |
2528 | +# http://www.apache.org/licenses/LICENSE-2.0 |
2529 | +# |
2530 | +# Unless required by applicable law or agreed to in writing, software |
2531 | +# distributed under the License is distributed on an "AS IS" BASIS, |
2532 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2533 | +# See the License for the specific language governing permissions and |
2534 | +# limitations under the License. |
2535 | |
2536 | import yaml |
2537 | |
2538 | |
2539 | === modified file 'hooks/charmhelpers/core/templating.py' |
2540 | --- hooks/charmhelpers/core/templating.py 2015-02-04 12:16:14 +0000 |
2541 | +++ hooks/charmhelpers/core/templating.py 2016-09-16 19:19:06 +0000 |
2542 | @@ -1,33 +1,33 @@ |
2543 | # Copyright 2014-2015 Canonical Limited. |
2544 | # |
2545 | -# This file is part of charm-helpers. |
2546 | -# |
2547 | -# charm-helpers is free software: you can redistribute it and/or modify |
2548 | -# it under the terms of the GNU Lesser General Public License version 3 as |
2549 | -# published by the Free Software Foundation. |
2550 | -# |
2551 | -# charm-helpers is distributed in the hope that it will be useful, |
2552 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2553 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2554 | -# GNU Lesser General Public License for more details. |
2555 | -# |
2556 | -# You should have received a copy of the GNU Lesser General Public License |
2557 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
2558 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
2559 | +# you may not use this file except in compliance with the License. |
2560 | +# You may obtain a copy of the License at |
2561 | +# |
2562 | +# http://www.apache.org/licenses/LICENSE-2.0 |
2563 | +# |
2564 | +# Unless required by applicable law or agreed to in writing, software |
2565 | +# distributed under the License is distributed on an "AS IS" BASIS, |
2566 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2567 | +# See the License for the specific language governing permissions and |
2568 | +# limitations under the License. |
2569 | |
2570 | import os |
2571 | +import sys |
2572 | |
2573 | from charmhelpers.core import host |
2574 | from charmhelpers.core import hookenv |
2575 | |
2576 | |
2577 | def render(source, target, context, owner='root', group='root', |
2578 | - perms=0o444, templates_dir=None, encoding='UTF-8'): |
2579 | + perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None): |
2580 | """ |
2581 | Render a template. |
2582 | |
2583 | The `source` path, if not absolute, is relative to the `templates_dir`. |
2584 | |
2585 | - The `target` path should be absolute. |
2586 | + The `target` path should be absolute. It can also be `None`, in which |
2587 | + case no file will be written. |
2588 | |
2589 | The context should be a dict containing the values to be replaced in the |
2590 | template. |
2591 | @@ -36,8 +36,12 @@ |
2592 | |
2593 | If omitted, `templates_dir` defaults to the `templates` folder in the charm. |
2594 | |
2595 | - Note: Using this requires python-jinja2; if it is not installed, calling |
2596 | - this will attempt to use charmhelpers.fetch.apt_install to install it. |
2597 | + The rendered template will be written to the file as well as being returned |
2598 | + as a string. |
2599 | + |
2600 | + Note: Using this requires python-jinja2 or python3-jinja2; if it is not |
2601 | + installed, calling this will attempt to use charmhelpers.fetch.apt_install |
2602 | + to install it. |
2603 | """ |
2604 | try: |
2605 | from jinja2 import FileSystemLoader, Environment, exceptions |
2606 | @@ -49,20 +53,32 @@ |
2607 | 'charmhelpers.fetch to install it', |
2608 | level=hookenv.ERROR) |
2609 | raise |
2610 | - apt_install('python-jinja2', fatal=True) |
2611 | + if sys.version_info.major == 2: |
2612 | + apt_install('python-jinja2', fatal=True) |
2613 | + else: |
2614 | + apt_install('python3-jinja2', fatal=True) |
2615 | from jinja2 import FileSystemLoader, Environment, exceptions |
2616 | |
2617 | - if templates_dir is None: |
2618 | - templates_dir = os.path.join(hookenv.charm_dir(), 'templates') |
2619 | - loader = Environment(loader=FileSystemLoader(templates_dir)) |
2620 | + if template_loader: |
2621 | + template_env = Environment(loader=template_loader) |
2622 | + else: |
2623 | + if templates_dir is None: |
2624 | + templates_dir = os.path.join(hookenv.charm_dir(), 'templates') |
2625 | + template_env = Environment(loader=FileSystemLoader(templates_dir)) |
2626 | try: |
2627 | source = source |
2628 | - template = loader.get_template(source) |
2629 | + template = template_env.get_template(source) |
2630 | except exceptions.TemplateNotFound as e: |
2631 | hookenv.log('Could not load template %s from %s.' % |
2632 | (source, templates_dir), |
2633 | level=hookenv.ERROR) |
2634 | raise e |
2635 | content = template.render(context) |
2636 | - host.mkdir(os.path.dirname(target), owner, group, perms=0o755) |
2637 | - host.write_file(target, content.encode(encoding), owner, group, perms) |
2638 | + if target is not None: |
2639 | + target_dir = os.path.dirname(target) |
2640 | + if not os.path.exists(target_dir): |
2641 | + # This is a terrible default directory permission, as the file |
2642 | + # or its siblings will often contain secrets. |
2643 | + host.mkdir(os.path.dirname(target), owner, group, perms=0o755) |
2644 | + host.write_file(target, content.encode(encoding), owner, group, perms) |
2645 | + return content |
2646 | |
2647 | === modified file 'hooks/charmhelpers/core/unitdata.py' |
2648 | --- hooks/charmhelpers/core/unitdata.py 2015-07-14 14:05:42 +0000 |
2649 | +++ hooks/charmhelpers/core/unitdata.py 2016-09-16 19:19:06 +0000 |
2650 | @@ -3,20 +3,17 @@ |
2651 | # |
2652 | # Copyright 2014-2015 Canonical Limited. |
2653 | # |
2654 | -# This file is part of charm-helpers. |
2655 | -# |
2656 | -# charm-helpers is free software: you can redistribute it and/or modify |
2657 | -# it under the terms of the GNU Lesser General Public License version 3 as |
2658 | -# published by the Free Software Foundation. |
2659 | -# |
2660 | -# charm-helpers is distributed in the hope that it will be useful, |
2661 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2662 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2663 | -# GNU Lesser General Public License for more details. |
2664 | -# |
2665 | -# You should have received a copy of the GNU Lesser General Public License |
2666 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
2667 | -# |
2668 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
2669 | +# you may not use this file except in compliance with the License. |
2670 | +# You may obtain a copy of the License at |
2671 | +# |
2672 | +# http://www.apache.org/licenses/LICENSE-2.0 |
2673 | +# |
2674 | +# Unless required by applicable law or agreed to in writing, software |
2675 | +# distributed under the License is distributed on an "AS IS" BASIS, |
2676 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2677 | +# See the License for the specific language governing permissions and |
2678 | +# limitations under the License. |
2679 | # |
2680 | # Authors: |
2681 | # Kapil Thangavelu <kapil.foss@gmail.com> |
2682 | @@ -152,6 +149,7 @@ |
2683 | import collections |
2684 | import contextlib |
2685 | import datetime |
2686 | +import itertools |
2687 | import json |
2688 | import os |
2689 | import pprint |
2690 | @@ -164,8 +162,7 @@ |
2691 | class Storage(object): |
2692 | """Simple key value database for local unit state within charms. |
2693 | |
2694 | - Modifications are automatically committed at hook exit. That's |
2695 | - currently regardless of exit code. |
2696 | + Modifications are not persisted unless :meth:`flush` is called. |
2697 | |
2698 | To support dicts, lists, integer, floats, and booleans values |
2699 | are automatically json encoded/decoded. |
2700 | @@ -173,8 +170,11 @@ |
2701 | def __init__(self, path=None): |
2702 | self.db_path = path |
2703 | if path is None: |
2704 | - self.db_path = os.path.join( |
2705 | - os.environ.get('CHARM_DIR', ''), '.unit-state.db') |
2706 | + if 'UNIT_STATE_DB' in os.environ: |
2707 | + self.db_path = os.environ['UNIT_STATE_DB'] |
2708 | + else: |
2709 | + self.db_path = os.path.join( |
2710 | + os.environ.get('CHARM_DIR', ''), '.unit-state.db') |
2711 | self.conn = sqlite3.connect('%s' % self.db_path) |
2712 | self.cursor = self.conn.cursor() |
2713 | self.revision = None |
2714 | @@ -189,15 +189,8 @@ |
2715 | self.conn.close() |
2716 | self._closed = True |
2717 | |
2718 | - def _scoped_query(self, stmt, params=None): |
2719 | - if params is None: |
2720 | - params = [] |
2721 | - return stmt, params |
2722 | - |
2723 | def get(self, key, default=None, record=False): |
2724 | - self.cursor.execute( |
2725 | - *self._scoped_query( |
2726 | - 'select data from kv where key=?', [key])) |
2727 | + self.cursor.execute('select data from kv where key=?', [key]) |
2728 | result = self.cursor.fetchone() |
2729 | if not result: |
2730 | return default |
2731 | @@ -206,33 +199,81 @@ |
2732 | return json.loads(result[0]) |
2733 | |
2734 | def getrange(self, key_prefix, strip=False): |
2735 | - stmt = "select key, data from kv where key like '%s%%'" % key_prefix |
2736 | - self.cursor.execute(*self._scoped_query(stmt)) |
2737 | + """ |
2738 | + Get a range of keys starting with a common prefix as a mapping of |
2739 | + keys to values. |
2740 | + |
2741 | + :param str key_prefix: Common prefix among all keys |
2742 | + :param bool strip: Optionally strip the common prefix from the key |
2743 | + names in the returned dict |
2744 | + :return dict: A (possibly empty) dict of key-value mappings |
2745 | + """ |
2746 | + self.cursor.execute("select key, data from kv where key like ?", |
2747 | + ['%s%%' % key_prefix]) |
2748 | result = self.cursor.fetchall() |
2749 | |
2750 | if not result: |
2751 | - return None |
2752 | + return {} |
2753 | if not strip: |
2754 | key_prefix = '' |
2755 | return dict([ |
2756 | (k[len(key_prefix):], json.loads(v)) for k, v in result]) |
2757 | |
2758 | def update(self, mapping, prefix=""): |
2759 | + """ |
2760 | + Set the values of multiple keys at once. |
2761 | + |
2762 | + :param dict mapping: Mapping of keys to values |
2763 | + :param str prefix: Optional prefix to apply to all keys in `mapping` |
2764 | + before setting |
2765 | + """ |
2766 | for k, v in mapping.items(): |
2767 | self.set("%s%s" % (prefix, k), v) |
2768 | |
2769 | def unset(self, key): |
2770 | + """ |
2771 | + Remove a key from the database entirely. |
2772 | + """ |
2773 | self.cursor.execute('delete from kv where key=?', [key]) |
2774 | if self.revision and self.cursor.rowcount: |
2775 | self.cursor.execute( |
2776 | 'insert into kv_revisions values (?, ?, ?)', |
2777 | [key, self.revision, json.dumps('DELETED')]) |
2778 | |
2779 | + def unsetrange(self, keys=None, prefix=""): |
2780 | + """ |
2781 | + Remove a range of keys starting with a common prefix, from the database |
2782 | + entirely. |
2783 | + |
2784 | + :param list keys: List of keys to remove. |
2785 | + :param str prefix: Optional prefix to apply to all keys in ``keys`` |
2786 | + before removing. |
2787 | + """ |
2788 | + if keys is not None: |
2789 | + keys = ['%s%s' % (prefix, key) for key in keys] |
2790 | + self.cursor.execute('delete from kv where key in (%s)' % ','.join(['?'] * len(keys)), keys) |
2791 | + if self.revision and self.cursor.rowcount: |
2792 | + self.cursor.execute( |
2793 | + 'insert into kv_revisions values %s' % ','.join(['(?, ?, ?)'] * len(keys)), |
2794 | + list(itertools.chain.from_iterable((key, self.revision, json.dumps('DELETED')) for key in keys))) |
2795 | + else: |
2796 | + self.cursor.execute('delete from kv where key like ?', |
2797 | + ['%s%%' % prefix]) |
2798 | + if self.revision and self.cursor.rowcount: |
2799 | + self.cursor.execute( |
2800 | + 'insert into kv_revisions values (?, ?, ?)', |
2801 | + ['%s%%' % prefix, self.revision, json.dumps('DELETED')]) |
2802 | + |
2803 | def set(self, key, value): |
2804 | + """ |
2805 | + Set a value in the database. |
2806 | + |
2807 | + :param str key: Key to set the value for |
2808 | + :param value: Any JSON-serializable value to be set |
2809 | + """ |
2810 | serialized = json.dumps(value) |
2811 | |
2812 | - self.cursor.execute( |
2813 | - 'select data from kv where key=?', [key]) |
2814 | + self.cursor.execute('select data from kv where key=?', [key]) |
2815 | exists = self.cursor.fetchone() |
2816 | |
2817 | # Skip mutations to the same value |
2818 | |
2819 | === modified file 'hooks/charmhelpers/fetch/__init__.py' |
2820 | --- hooks/charmhelpers/fetch/__init__.py 2015-07-14 14:05:42 +0000 |
2821 | +++ hooks/charmhelpers/fetch/__init__.py 2016-09-16 19:19:06 +0000 |
2822 | @@ -1,32 +1,24 @@ |
2823 | # Copyright 2014-2015 Canonical Limited. |
2824 | # |
2825 | -# This file is part of charm-helpers. |
2826 | -# |
2827 | -# charm-helpers is free software: you can redistribute it and/or modify |
2828 | -# it under the terms of the GNU Lesser General Public License version 3 as |
2829 | -# published by the Free Software Foundation. |
2830 | -# |
2831 | -# charm-helpers is distributed in the hope that it will be useful, |
2832 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
2833 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
2834 | -# GNU Lesser General Public License for more details. |
2835 | -# |
2836 | -# You should have received a copy of the GNU Lesser General Public License |
2837 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
2838 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
2839 | +# you may not use this file except in compliance with the License. |
2840 | +# You may obtain a copy of the License at |
2841 | +# |
2842 | +# http://www.apache.org/licenses/LICENSE-2.0 |
2843 | +# |
2844 | +# Unless required by applicable law or agreed to in writing, software |
2845 | +# distributed under the License is distributed on an "AS IS" BASIS, |
2846 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
2847 | +# See the License for the specific language governing permissions and |
2848 | +# limitations under the License. |
2849 | |
2850 | import importlib |
2851 | -from tempfile import NamedTemporaryFile |
2852 | -import time |
2853 | +from charmhelpers.osplatform import get_platform |
2854 | from yaml import safe_load |
2855 | -from charmhelpers.core.host import ( |
2856 | - lsb_release |
2857 | -) |
2858 | -import subprocess |
2859 | from charmhelpers.core.hookenv import ( |
2860 | config, |
2861 | log, |
2862 | ) |
2863 | -import os |
2864 | |
2865 | import six |
2866 | if six.PY3: |
2867 | @@ -35,63 +27,6 @@ |
2868 | from urlparse import urlparse, urlunparse |
2869 | |
2870 | |
2871 | -CLOUD_ARCHIVE = """# Ubuntu Cloud Archive |
2872 | -deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main |
2873 | -""" |
2874 | -PROPOSED_POCKET = """# Proposed |
2875 | -deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted |
2876 | -""" |
2877 | -CLOUD_ARCHIVE_POCKETS = { |
2878 | - # Folsom |
2879 | - 'folsom': 'precise-updates/folsom', |
2880 | - 'precise-folsom': 'precise-updates/folsom', |
2881 | - 'precise-folsom/updates': 'precise-updates/folsom', |
2882 | - 'precise-updates/folsom': 'precise-updates/folsom', |
2883 | - 'folsom/proposed': 'precise-proposed/folsom', |
2884 | - 'precise-folsom/proposed': 'precise-proposed/folsom', |
2885 | - 'precise-proposed/folsom': 'precise-proposed/folsom', |
2886 | - # Grizzly |
2887 | - 'grizzly': 'precise-updates/grizzly', |
2888 | - 'precise-grizzly': 'precise-updates/grizzly', |
2889 | - 'precise-grizzly/updates': 'precise-updates/grizzly', |
2890 | - 'precise-updates/grizzly': 'precise-updates/grizzly', |
2891 | - 'grizzly/proposed': 'precise-proposed/grizzly', |
2892 | - 'precise-grizzly/proposed': 'precise-proposed/grizzly', |
2893 | - 'precise-proposed/grizzly': 'precise-proposed/grizzly', |
2894 | - # Havana |
2895 | - 'havana': 'precise-updates/havana', |
2896 | - 'precise-havana': 'precise-updates/havana', |
2897 | - 'precise-havana/updates': 'precise-updates/havana', |
2898 | - 'precise-updates/havana': 'precise-updates/havana', |
2899 | - 'havana/proposed': 'precise-proposed/havana', |
2900 | - 'precise-havana/proposed': 'precise-proposed/havana', |
2901 | - 'precise-proposed/havana': 'precise-proposed/havana', |
2902 | - # Icehouse |
2903 | - 'icehouse': 'precise-updates/icehouse', |
2904 | - 'precise-icehouse': 'precise-updates/icehouse', |
2905 | - 'precise-icehouse/updates': 'precise-updates/icehouse', |
2906 | - 'precise-updates/icehouse': 'precise-updates/icehouse', |
2907 | - 'icehouse/proposed': 'precise-proposed/icehouse', |
2908 | - 'precise-icehouse/proposed': 'precise-proposed/icehouse', |
2909 | - 'precise-proposed/icehouse': 'precise-proposed/icehouse', |
2910 | - # Juno |
2911 | - 'juno': 'trusty-updates/juno', |
2912 | - 'trusty-juno': 'trusty-updates/juno', |
2913 | - 'trusty-juno/updates': 'trusty-updates/juno', |
2914 | - 'trusty-updates/juno': 'trusty-updates/juno', |
2915 | - 'juno/proposed': 'trusty-proposed/juno', |
2916 | - 'trusty-juno/proposed': 'trusty-proposed/juno', |
2917 | - 'trusty-proposed/juno': 'trusty-proposed/juno', |
2918 | - # Kilo |
2919 | - 'kilo': 'trusty-updates/kilo', |
2920 | - 'trusty-kilo': 'trusty-updates/kilo', |
2921 | - 'trusty-kilo/updates': 'trusty-updates/kilo', |
2922 | - 'trusty-updates/kilo': 'trusty-updates/kilo', |
2923 | - 'kilo/proposed': 'trusty-proposed/kilo', |
2924 | - 'trusty-kilo/proposed': 'trusty-proposed/kilo', |
2925 | - 'trusty-proposed/kilo': 'trusty-proposed/kilo', |
2926 | -} |
2927 | - |
2928 | # The order of this list is very important. Handlers should be listed in from |
2929 | # least- to most-specific URL matching. |
2930 | FETCH_HANDLERS = ( |
2931 | @@ -100,10 +35,6 @@ |
2932 | 'charmhelpers.fetch.giturl.GitUrlFetchHandler', |
2933 | ) |
2934 | |
2935 | -APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT. |
2936 | -APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks. |
2937 | -APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times. |
2938 | - |
2939 | |
2940 | class SourceConfigError(Exception): |
2941 | pass |
2942 | @@ -141,180 +72,37 @@ |
2943 | return urlunparse(parts) |
2944 | |
2945 | |
2946 | -def filter_installed_packages(packages): |
2947 | - """Returns a list of packages that require installation""" |
2948 | - cache = apt_cache() |
2949 | - _pkgs = [] |
2950 | - for package in packages: |
2951 | - try: |
2952 | - p = cache[package] |
2953 | - p.current_ver or _pkgs.append(package) |
2954 | - except KeyError: |
2955 | - log('Package {} has no installation candidate.'.format(package), |
2956 | - level='WARNING') |
2957 | - _pkgs.append(package) |
2958 | - return _pkgs |
2959 | - |
2960 | - |
2961 | -def apt_cache(in_memory=True): |
2962 | - """Build and return an apt cache""" |
2963 | - from apt import apt_pkg |
2964 | - apt_pkg.init() |
2965 | - if in_memory: |
2966 | - apt_pkg.config.set("Dir::Cache::pkgcache", "") |
2967 | - apt_pkg.config.set("Dir::Cache::srcpkgcache", "") |
2968 | - return apt_pkg.Cache() |
2969 | - |
2970 | - |
2971 | -def apt_install(packages, options=None, fatal=False): |
2972 | - """Install one or more packages""" |
2973 | - if options is None: |
2974 | - options = ['--option=Dpkg::Options::=--force-confold'] |
2975 | - |
2976 | - cmd = ['apt-get', '--assume-yes'] |
2977 | - cmd.extend(options) |
2978 | - cmd.append('install') |
2979 | - if isinstance(packages, six.string_types): |
2980 | - cmd.append(packages) |
2981 | - else: |
2982 | - cmd.extend(packages) |
2983 | - log("Installing {} with options: {}".format(packages, |
2984 | - options)) |
2985 | - _run_apt_command(cmd, fatal) |
2986 | - |
2987 | - |
2988 | -def apt_upgrade(options=None, fatal=False, dist=False): |
2989 | - """Upgrade all packages""" |
2990 | - if options is None: |
2991 | - options = ['--option=Dpkg::Options::=--force-confold'] |
2992 | - |
2993 | - cmd = ['apt-get', '--assume-yes'] |
2994 | - cmd.extend(options) |
2995 | - if dist: |
2996 | - cmd.append('dist-upgrade') |
2997 | - else: |
2998 | - cmd.append('upgrade') |
2999 | - log("Upgrading with options: {}".format(options)) |
3000 | - _run_apt_command(cmd, fatal) |
3001 | - |
3002 | - |
3003 | -def apt_update(fatal=False): |
3004 | - """Update local apt cache""" |
3005 | - cmd = ['apt-get', 'update'] |
3006 | - _run_apt_command(cmd, fatal) |
3007 | - |
3008 | - |
3009 | -def apt_purge(packages, fatal=False): |
3010 | - """Purge one or more packages""" |
3011 | - cmd = ['apt-get', '--assume-yes', 'purge'] |
3012 | - if isinstance(packages, six.string_types): |
3013 | - cmd.append(packages) |
3014 | - else: |
3015 | - cmd.extend(packages) |
3016 | - log("Purging {}".format(packages)) |
3017 | - _run_apt_command(cmd, fatal) |
3018 | - |
3019 | - |
3020 | -def apt_mark(packages, mark, fatal=False): |
3021 | - """Flag one or more packages using apt-mark""" |
3022 | - cmd = ['apt-mark', mark] |
3023 | - if isinstance(packages, six.string_types): |
3024 | - cmd.append(packages) |
3025 | - else: |
3026 | - cmd.extend(packages) |
3027 | - log("Holding {}".format(packages)) |
3028 | - |
3029 | - if fatal: |
3030 | - subprocess.check_call(cmd, universal_newlines=True) |
3031 | - else: |
3032 | - subprocess.call(cmd, universal_newlines=True) |
3033 | - |
3034 | - |
3035 | -def apt_hold(packages, fatal=False): |
3036 | - return apt_mark(packages, 'hold', fatal=fatal) |
3037 | - |
3038 | - |
3039 | -def apt_unhold(packages, fatal=False): |
3040 | - return apt_mark(packages, 'unhold', fatal=fatal) |
3041 | - |
3042 | - |
3043 | -def add_source(source, key=None): |
3044 | - """Add a package source to this system. |
3045 | - |
3046 | - @param source: a URL or sources.list entry, as supported by |
3047 | - add-apt-repository(1). Examples:: |
3048 | - |
3049 | - ppa:charmers/example |
3050 | - deb https://stub:key@private.example.com/ubuntu trusty main |
3051 | - |
3052 | - In addition: |
3053 | - 'proposed:' may be used to enable the standard 'proposed' |
3054 | - pocket for the release. |
3055 | - 'cloud:' may be used to activate official cloud archive pockets, |
3056 | - such as 'cloud:icehouse' |
3057 | - 'distro' may be used as a noop |
3058 | - |
3059 | - @param key: A key to be added to the system's APT keyring and used |
3060 | - to verify the signatures on packages. Ideally, this should be an |
3061 | - ASCII format GPG public key including the block headers. A GPG key |
3062 | - id may also be used, but be aware that only insecure protocols are |
3063 | - available to retrieve the actual public key from a public keyserver |
3064 | - placing your Juju environment at risk. ppa and cloud archive keys |
3065 | - are securely added automtically, so sould not be provided. |
3066 | - """ |
3067 | - if source is None: |
3068 | - log('Source is not present. Skipping') |
3069 | - return |
3070 | - |
3071 | - if (source.startswith('ppa:') or |
3072 | - source.startswith('http') or |
3073 | - source.startswith('deb ') or |
3074 | - source.startswith('cloud-archive:')): |
3075 | - subprocess.check_call(['add-apt-repository', '--yes', source]) |
3076 | - elif source.startswith('cloud:'): |
3077 | - apt_install(filter_installed_packages(['ubuntu-cloud-keyring']), |
3078 | - fatal=True) |
3079 | - pocket = source.split(':')[-1] |
3080 | - if pocket not in CLOUD_ARCHIVE_POCKETS: |
3081 | - raise SourceConfigError( |
3082 | - 'Unsupported cloud: source option %s' % |
3083 | - pocket) |
3084 | - actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket] |
3085 | - with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt: |
3086 | - apt.write(CLOUD_ARCHIVE.format(actual_pocket)) |
3087 | - elif source == 'proposed': |
3088 | - release = lsb_release()['DISTRIB_CODENAME'] |
3089 | - with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt: |
3090 | - apt.write(PROPOSED_POCKET.format(release)) |
3091 | - elif source == 'distro': |
3092 | - pass |
3093 | - else: |
3094 | - log("Unknown source: {!r}".format(source)) |
3095 | - |
3096 | - if key: |
3097 | - if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key: |
3098 | - with NamedTemporaryFile('w+') as key_file: |
3099 | - key_file.write(key) |
3100 | - key_file.flush() |
3101 | - key_file.seek(0) |
3102 | - subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file) |
3103 | - else: |
3104 | - # Note that hkp: is in no way a secure protocol. Using a |
3105 | - # GPG key id is pointless from a security POV unless you |
3106 | - # absolutely trust your network and DNS. |
3107 | - subprocess.check_call(['apt-key', 'adv', '--keyserver', |
3108 | - 'hkp://keyserver.ubuntu.com:80', '--recv', |
3109 | - key]) |
3110 | +__platform__ = get_platform() |
3111 | +module = "charmhelpers.fetch.%s" % __platform__ |
3112 | +fetch = importlib.import_module(module) |
3113 | + |
3114 | +filter_installed_packages = fetch.filter_installed_packages |
3115 | +install = fetch.install |
3116 | +upgrade = fetch.upgrade |
3117 | +update = fetch.update |
3118 | +purge = fetch.purge |
3119 | +add_source = fetch.add_source |
3120 | + |
3121 | +if __platform__ == "ubuntu": |
3122 | + apt_cache = fetch.apt_cache |
3123 | + apt_install = fetch.install |
3124 | + apt_update = fetch.update |
3125 | + apt_upgrade = fetch.upgrade |
3126 | + apt_purge = fetch.purge |
3127 | + apt_mark = fetch.apt_mark |
3128 | + apt_hold = fetch.apt_hold |
3129 | + apt_unhold = fetch.apt_unhold |
3130 | +elif __platform__ == "centos": |
3131 | + yum_search = fetch.yum_search |
3132 | |
3133 | |
3134 | def configure_sources(update=False, |
3135 | sources_var='install_sources', |
3136 | keys_var='install_keys'): |
3137 | - """ |
3138 | - Configure multiple sources from charm configuration. |
3139 | + """Configure multiple sources from charm configuration. |
3140 | |
3141 | The lists are encoded as yaml fragments in the configuration. |
3142 | - The frament needs to be included as a string. Sources and their |
3143 | + The fragment needs to be included as a string. Sources and their |
3144 | corresponding keys are of the types supported by add_source(). |
3145 | |
3146 | Example config: |
3147 | @@ -346,12 +134,11 @@ |
3148 | for source, key in zip(sources, keys): |
3149 | add_source(source, key) |
3150 | if update: |
3151 | - apt_update(fatal=True) |
3152 | + fetch.update(fatal=True) |
3153 | |
3154 | |
3155 | def install_remote(source, *args, **kwargs): |
3156 | - """ |
3157 | - Install a file tree from a remote source |
3158 | + """Install a file tree from a remote source. |
3159 | |
3160 | The specified source should be a url of the form: |
3161 | scheme://[host]/path[#[option=value][&...]] |
3162 | @@ -374,18 +161,17 @@ |
3163 | # We ONLY check for True here because can_handle may return a string |
3164 | # explaining why it can't handle a given source. |
3165 | handlers = [h for h in plugins() if h.can_handle(source) is True] |
3166 | - installed_to = None |
3167 | for handler in handlers: |
3168 | try: |
3169 | - installed_to = handler.install(source, *args, **kwargs) |
3170 | - except UnhandledSource: |
3171 | - pass |
3172 | - if not installed_to: |
3173 | - raise UnhandledSource("No handler found for source {}".format(source)) |
3174 | - return installed_to |
3175 | + return handler.install(source, *args, **kwargs) |
3176 | + except UnhandledSource as e: |
3177 | + log('Install source attempt unsuccessful: {}'.format(e), |
3178 | + level='WARNING') |
3179 | + raise UnhandledSource("No handler found for source {}".format(source)) |
3180 | |
3181 | |
3182 | def install_from_config(config_var_name): |
3183 | + """Install a file from config.""" |
3184 | charm_config = config() |
3185 | source = charm_config[config_var_name] |
3186 | return install_remote(source) |
3187 | @@ -402,46 +188,9 @@ |
3188 | importlib.import_module(package), |
3189 | classname) |
3190 | plugin_list.append(handler_class()) |
3191 | - except (ImportError, AttributeError): |
3192 | + except NotImplementedError: |
3193 | # Skip missing plugins so that they can be ommitted from |
3194 | # installation if desired |
3195 | log("FetchHandler {} not found, skipping plugin".format( |
3196 | handler_name)) |
3197 | return plugin_list |
3198 | - |
3199 | - |
3200 | -def _run_apt_command(cmd, fatal=False): |
3201 | - """ |
3202 | - Run an APT command, checking output and retrying if the fatal flag is set |
3203 | - to True. |
3204 | - |
3205 | - :param: cmd: str: The apt command to run. |
3206 | - :param: fatal: bool: Whether the command's output should be checked and |
3207 | - retried. |
3208 | - """ |
3209 | - env = os.environ.copy() |
3210 | - |
3211 | - if 'DEBIAN_FRONTEND' not in env: |
3212 | - env['DEBIAN_FRONTEND'] = 'noninteractive' |
3213 | - |
3214 | - if fatal: |
3215 | - retry_count = 0 |
3216 | - result = None |
3217 | - |
3218 | - # If the command is considered "fatal", we need to retry if the apt |
3219 | - # lock was not acquired. |
3220 | - |
3221 | - while result is None or result == APT_NO_LOCK: |
3222 | - try: |
3223 | - result = subprocess.check_call(cmd, env=env) |
3224 | - except subprocess.CalledProcessError as e: |
3225 | - retry_count = retry_count + 1 |
3226 | - if retry_count > APT_NO_LOCK_RETRY_COUNT: |
3227 | - raise |
3228 | - result = e.returncode |
3229 | - log("Couldn't acquire DPKG lock. Will retry in {} seconds." |
3230 | - "".format(APT_NO_LOCK_RETRY_DELAY)) |
3231 | - time.sleep(APT_NO_LOCK_RETRY_DELAY) |
3232 | - |
3233 | - else: |
3234 | - subprocess.call(cmd, env=env) |
3235 | |
3236 | === modified file 'hooks/charmhelpers/fetch/archiveurl.py' |
3237 | --- hooks/charmhelpers/fetch/archiveurl.py 2015-02-12 20:10:50 +0000 |
3238 | +++ hooks/charmhelpers/fetch/archiveurl.py 2016-09-16 19:19:06 +0000 |
3239 | @@ -1,18 +1,16 @@ |
3240 | # Copyright 2014-2015 Canonical Limited. |
3241 | # |
3242 | -# This file is part of charm-helpers. |
3243 | -# |
3244 | -# charm-helpers is free software: you can redistribute it and/or modify |
3245 | -# it under the terms of the GNU Lesser General Public License version 3 as |
3246 | -# published by the Free Software Foundation. |
3247 | -# |
3248 | -# charm-helpers is distributed in the hope that it will be useful, |
3249 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
3250 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
3251 | -# GNU Lesser General Public License for more details. |
3252 | -# |
3253 | -# You should have received a copy of the GNU Lesser General Public License |
3254 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
3255 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
3256 | +# you may not use this file except in compliance with the License. |
3257 | +# You may obtain a copy of the License at |
3258 | +# |
3259 | +# http://www.apache.org/licenses/LICENSE-2.0 |
3260 | +# |
3261 | +# Unless required by applicable law or agreed to in writing, software |
3262 | +# distributed under the License is distributed on an "AS IS" BASIS, |
3263 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
3264 | +# See the License for the specific language governing permissions and |
3265 | +# limitations under the License. |
3266 | |
3267 | import os |
3268 | import hashlib |
3269 | @@ -77,6 +75,8 @@ |
3270 | def can_handle(self, source): |
3271 | url_parts = self.parse_url(source) |
3272 | if url_parts.scheme not in ('http', 'https', 'ftp', 'file'): |
3273 | + # XXX: Why is this returning a boolean and a string? It's |
3274 | + # doomed to fail since "bool(can_handle('foo://'))" will be True. |
3275 | return "Wrong source type" |
3276 | if get_archive_handler(self.base_url(source)): |
3277 | return True |
3278 | @@ -106,7 +106,7 @@ |
3279 | install_opener(opener) |
3280 | response = urlopen(source) |
3281 | try: |
3282 | - with open(dest, 'w') as dest_file: |
3283 | + with open(dest, 'wb') as dest_file: |
3284 | dest_file.write(response.read()) |
3285 | except Exception as e: |
3286 | if os.path.isfile(dest): |
3287 | @@ -155,7 +155,11 @@ |
3288 | else: |
3289 | algorithms = hashlib.algorithms_available |
3290 | if key in algorithms: |
3291 | - check_hash(dld_file, value, key) |
3292 | + if len(value) != 1: |
3293 | + raise TypeError( |
3294 | + "Expected 1 hash value, not %d" % len(value)) |
3295 | + expected = value[0] |
3296 | + check_hash(dld_file, expected, key) |
3297 | if checksum: |
3298 | check_hash(dld_file, checksum, hash_type) |
3299 | return extract(dld_file, dest) |
3300 | |
3301 | === modified file 'hooks/charmhelpers/fetch/bzrurl.py' |
3302 | --- hooks/charmhelpers/fetch/bzrurl.py 2015-02-03 18:59:19 +0000 |
3303 | +++ hooks/charmhelpers/fetch/bzrurl.py 2016-09-16 19:19:06 +0000 |
3304 | @@ -1,78 +1,76 @@ |
3305 | # Copyright 2014-2015 Canonical Limited. |
3306 | # |
3307 | -# This file is part of charm-helpers. |
3308 | -# |
3309 | -# charm-helpers is free software: you can redistribute it and/or modify |
3310 | -# it under the terms of the GNU Lesser General Public License version 3 as |
3311 | -# published by the Free Software Foundation. |
3312 | -# |
3313 | -# charm-helpers is distributed in the hope that it will be useful, |
3314 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
3315 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
3316 | -# GNU Lesser General Public License for more details. |
3317 | -# |
3318 | -# You should have received a copy of the GNU Lesser General Public License |
3319 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
3320 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
3321 | +# you may not use this file except in compliance with the License. |
3322 | +# You may obtain a copy of the License at |
3323 | +# |
3324 | +# http://www.apache.org/licenses/LICENSE-2.0 |
3325 | +# |
3326 | +# Unless required by applicable law or agreed to in writing, software |
3327 | +# distributed under the License is distributed on an "AS IS" BASIS, |
3328 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
3329 | +# See the License for the specific language governing permissions and |
3330 | +# limitations under the License. |
3331 | |
3332 | import os |
3333 | +from subprocess import check_call |
3334 | from charmhelpers.fetch import ( |
3335 | BaseFetchHandler, |
3336 | - UnhandledSource |
3337 | + UnhandledSource, |
3338 | + filter_installed_packages, |
3339 | + install, |
3340 | ) |
3341 | from charmhelpers.core.host import mkdir |
3342 | |
3343 | -import six |
3344 | -if six.PY3: |
3345 | - raise ImportError('bzrlib does not support Python3') |
3346 | |
3347 | -try: |
3348 | - from bzrlib.branch import Branch |
3349 | - from bzrlib import bzrdir, workingtree, errors |
3350 | -except ImportError: |
3351 | - from charmhelpers.fetch import apt_install |
3352 | - apt_install("python-bzrlib") |
3353 | - from bzrlib.branch import Branch |
3354 | - from bzrlib import bzrdir, workingtree, errors |
3355 | +if filter_installed_packages(['bzr']) != []: |
3356 | + install(['bzr']) |
3357 | + if filter_installed_packages(['bzr']) != []: |
3358 | + raise NotImplementedError('Unable to install bzr') |
3359 | |
3360 | |
3361 | class BzrUrlFetchHandler(BaseFetchHandler): |
3362 | - """Handler for bazaar branches via generic and lp URLs""" |
3363 | + """Handler for bazaar branches via generic and lp URLs.""" |
3364 | + |
3365 | def can_handle(self, source): |
3366 | url_parts = self.parse_url(source) |
3367 | - if url_parts.scheme not in ('bzr+ssh', 'lp'): |
3368 | + if url_parts.scheme not in ('bzr+ssh', 'lp', ''): |
3369 | return False |
3370 | + elif not url_parts.scheme: |
3371 | + return os.path.exists(os.path.join(source, '.bzr')) |
3372 | else: |
3373 | return True |
3374 | |
3375 | - def branch(self, source, dest): |
3376 | - url_parts = self.parse_url(source) |
3377 | - # If we use lp:branchname scheme we need to load plugins |
3378 | + def branch(self, source, dest, revno=None): |
3379 | if not self.can_handle(source): |
3380 | raise UnhandledSource("Cannot handle {}".format(source)) |
3381 | - if url_parts.scheme == "lp": |
3382 | - from bzrlib.plugin import load_plugins |
3383 | - load_plugins() |
3384 | - try: |
3385 | - local_branch = bzrdir.BzrDir.create_branch_convenience(dest) |
3386 | - except errors.AlreadyControlDirError: |
3387 | - local_branch = Branch.open(dest) |
3388 | - try: |
3389 | - remote_branch = Branch.open(source) |
3390 | - remote_branch.push(local_branch) |
3391 | - tree = workingtree.WorkingTree.open(dest) |
3392 | - tree.update() |
3393 | - except Exception as e: |
3394 | - raise e |
3395 | + cmd_opts = [] |
3396 | + if revno: |
3397 | + cmd_opts += ['-r', str(revno)] |
3398 | + if os.path.exists(dest): |
3399 | + cmd = ['bzr', 'pull'] |
3400 | + cmd += cmd_opts |
3401 | + cmd += ['--overwrite', '-d', dest, source] |
3402 | + else: |
3403 | + cmd = ['bzr', 'branch'] |
3404 | + cmd += cmd_opts |
3405 | + cmd += [source, dest] |
3406 | + check_call(cmd) |
3407 | |
3408 | - def install(self, source): |
3409 | + def install(self, source, dest=None, revno=None): |
3410 | url_parts = self.parse_url(source) |
3411 | branch_name = url_parts.path.strip("/").split("/")[-1] |
3412 | - dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", |
3413 | - branch_name) |
3414 | - if not os.path.exists(dest_dir): |
3415 | - mkdir(dest_dir, perms=0o755) |
3416 | + if dest: |
3417 | + dest_dir = os.path.join(dest, branch_name) |
3418 | + else: |
3419 | + dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", |
3420 | + branch_name) |
3421 | + |
3422 | + if dest and not os.path.exists(dest): |
3423 | + mkdir(dest, perms=0o755) |
3424 | + |
3425 | try: |
3426 | - self.branch(source, dest_dir) |
3427 | + self.branch(source, dest_dir, revno) |
3428 | except OSError as e: |
3429 | raise UnhandledSource(e.strerror) |
3430 | return dest_dir |
3431 | |
3432 | === added file 'hooks/charmhelpers/fetch/centos.py' |
3433 | --- hooks/charmhelpers/fetch/centos.py 1970-01-01 00:00:00 +0000 |
3434 | +++ hooks/charmhelpers/fetch/centos.py 2016-09-16 19:19:06 +0000 |
3435 | @@ -0,0 +1,171 @@ |
3436 | +# Copyright 2014-2015 Canonical Limited. |
3437 | +# |
3438 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
3439 | +# you may not use this file except in compliance with the License. |
3440 | +# You may obtain a copy of the License at |
3441 | +# |
3442 | +# http://www.apache.org/licenses/LICENSE-2.0 |
3443 | +# |
3444 | +# Unless required by applicable law or agreed to in writing, software |
3445 | +# distributed under the License is distributed on an "AS IS" BASIS, |
3446 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
3447 | +# See the License for the specific language governing permissions and |
3448 | +# limitations under the License. |
3449 | + |
3450 | +import subprocess |
3451 | +import os |
3452 | +import time |
3453 | +import six |
3454 | +import yum |
3455 | + |
3456 | +from tempfile import NamedTemporaryFile |
3457 | +from charmhelpers.core.hookenv import log |
3458 | + |
3459 | +YUM_NO_LOCK = 1 # The return code for "couldn't acquire lock" in YUM. |
3460 | +YUM_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks. |
3461 | +YUM_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times. |
3462 | + |
3463 | + |
3464 | +def filter_installed_packages(packages): |
3465 | + """Return a list of packages that require installation.""" |
3466 | + yb = yum.YumBase() |
3467 | + package_list = yb.doPackageLists() |
3468 | + temp_cache = {p.base_package_name: 1 for p in package_list['installed']} |
3469 | + |
3470 | + _pkgs = [p for p in packages if not temp_cache.get(p, False)] |
3471 | + return _pkgs |
3472 | + |
3473 | + |
3474 | +def install(packages, options=None, fatal=False): |
3475 | + """Install one or more packages.""" |
3476 | + cmd = ['yum', '--assumeyes'] |
3477 | + if options is not None: |
3478 | + cmd.extend(options) |
3479 | + cmd.append('install') |
3480 | + if isinstance(packages, six.string_types): |
3481 | + cmd.append(packages) |
3482 | + else: |
3483 | + cmd.extend(packages) |
3484 | + log("Installing {} with options: {}".format(packages, |
3485 | + options)) |
3486 | + _run_yum_command(cmd, fatal) |
3487 | + |
3488 | + |
3489 | +def upgrade(options=None, fatal=False, dist=False): |
3490 | + """Upgrade all packages.""" |
3491 | + cmd = ['yum', '--assumeyes'] |
3492 | + if options is not None: |
3493 | + cmd.extend(options) |
3494 | + cmd.append('upgrade') |
3495 | + log("Upgrading with options: {}".format(options)) |
3496 | + _run_yum_command(cmd, fatal) |
3497 | + |
3498 | + |
3499 | +def update(fatal=False): |
3500 | + """Update local yum cache.""" |
3501 | + cmd = ['yum', '--assumeyes', 'update'] |
3502 | + log("Update with fatal: {}".format(fatal)) |
3503 | + _run_yum_command(cmd, fatal) |
3504 | + |
3505 | + |
3506 | +def purge(packages, fatal=False): |
3507 | + """Purge one or more packages.""" |
3508 | + cmd = ['yum', '--assumeyes', 'remove'] |
3509 | + if isinstance(packages, six.string_types): |
3510 | + cmd.append(packages) |
3511 | + else: |
3512 | + cmd.extend(packages) |
3513 | + log("Purging {}".format(packages)) |
3514 | + _run_yum_command(cmd, fatal) |
3515 | + |
3516 | + |
3517 | +def yum_search(packages): |
3518 | + """Search for a package.""" |
3519 | + output = {} |
3520 | + cmd = ['yum', 'search'] |
3521 | + if isinstance(packages, six.string_types): |
3522 | + cmd.append(packages) |
3523 | + else: |
3524 | + cmd.extend(packages) |
3525 | + log("Searching for {}".format(packages)) |
3526 | + result = subprocess.check_output(cmd) |
3527 | + for package in list(packages): |
3528 | + output[package] = package in result |
3529 | + return output |
3530 | + |
3531 | + |
3532 | +def add_source(source, key=None): |
3533 | + """Add a package source to this system. |
3534 | + |
3535 | + @param source: a URL with a rpm package |
3536 | + |
3537 | + @param key: A key to be added to the system's keyring and used |
3538 | + to verify the signatures on packages. Ideally, this should be an |
3539 | + ASCII format GPG public key including the block headers. A GPG key |
3540 | + id may also be used, but be aware that only insecure protocols are |
3541 | + available to retrieve the actual public key from a public keyserver |
3542 | + placing your Juju environment at risk. |
3543 | + """ |
3544 | + if source is None: |
3545 | + log('Source is not present. Skipping') |
3546 | + return |
3547 | + |
3548 | + if source.startswith('http'): |
3549 | + directory = '/etc/yum.repos.d/' |
3550 | + for filename in os.listdir(directory): |
3551 | + with open(directory + filename, 'r') as rpm_file: |
3552 | + if source in rpm_file.read(): |
3553 | + break |
3554 | + else: |
3555 | + log("Add source: {!r}".format(source)) |
3556 | + # write in the charms.repo |
3557 | + with open(directory + 'Charms.repo', 'a') as rpm_file: |
3558 | + rpm_file.write('[%s]\n' % source[7:].replace('/', '_')) |
3559 | + rpm_file.write('name=%s\n' % source[7:]) |
3560 | + rpm_file.write('baseurl=%s\n\n' % source) |
3561 | + else: |
3562 | + log("Unknown source: {!r}".format(source)) |
3563 | + |
3564 | + if key: |
3565 | + if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key: |
3566 | + with NamedTemporaryFile('w+') as key_file: |
3567 | + key_file.write(key) |
3568 | + key_file.flush() |
3569 | + key_file.seek(0) |
3570 | + subprocess.check_call(['rpm', '--import', key_file]) |
3571 | + else: |
3572 | + subprocess.check_call(['rpm', '--import', key]) |
3573 | + |
3574 | + |
3575 | +def _run_yum_command(cmd, fatal=False): |
3576 | + """Run an YUM command. |
3577 | + |
3578 | + Checks the output and retry if the fatal flag is set to True. |
3579 | + |
3580 | + :param: cmd: str: The yum command to run. |
3581 | + :param: fatal: bool: Whether the command's output should be checked and |
3582 | + retried. |
3583 | + """ |
3584 | + env = os.environ.copy() |
3585 | + |
3586 | + if fatal: |
3587 | + retry_count = 0 |
3588 | + result = None |
3589 | + |
3590 | + # If the command is considered "fatal", we need to retry if the yum |
3591 | + # lock was not acquired. |
3592 | + |
3593 | + while result is None or result == YUM_NO_LOCK: |
3594 | + try: |
3595 | + result = subprocess.check_call(cmd, env=env) |
3596 | + except subprocess.CalledProcessError as e: |
3597 | + retry_count = retry_count + 1 |
3598 | + if retry_count > YUM_NO_LOCK_RETRY_COUNT: |
3599 | + raise |
3600 | + result = e.returncode |
3601 | + log("Couldn't acquire YUM lock. Will retry in {} seconds." |
3602 | + "".format(YUM_NO_LOCK_RETRY_DELAY)) |
3603 | + time.sleep(YUM_NO_LOCK_RETRY_DELAY) |
3604 | + |
3605 | + else: |
3606 | + subprocess.call(cmd, env=env) |
3607 | |
3608 | === modified file 'hooks/charmhelpers/fetch/giturl.py' |
3609 | --- hooks/charmhelpers/fetch/giturl.py 2015-07-14 14:05:42 +0000 |
3610 | +++ hooks/charmhelpers/fetch/giturl.py 2016-09-16 19:19:06 +0000 |
3611 | @@ -1,58 +1,56 @@ |
3612 | # Copyright 2014-2015 Canonical Limited. |
3613 | # |
3614 | -# This file is part of charm-helpers. |
3615 | -# |
3616 | -# charm-helpers is free software: you can redistribute it and/or modify |
3617 | -# it under the terms of the GNU Lesser General Public License version 3 as |
3618 | -# published by the Free Software Foundation. |
3619 | -# |
3620 | -# charm-helpers is distributed in the hope that it will be useful, |
3621 | -# but WITHOUT ANY WARRANTY; without even the implied warranty of |
3622 | -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
3623 | -# GNU Lesser General Public License for more details. |
3624 | -# |
3625 | -# You should have received a copy of the GNU Lesser General Public License |
3626 | -# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>. |
3627 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
3628 | +# you may not use this file except in compliance with the License. |
3629 | +# You may obtain a copy of the License at |
3630 | +# |
3631 | +# http://www.apache.org/licenses/LICENSE-2.0 |
3632 | +# |
3633 | +# Unless required by applicable law or agreed to in writing, software |
3634 | +# distributed under the License is distributed on an "AS IS" BASIS, |
3635 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
3636 | +# See the License for the specific language governing permissions and |
3637 | +# limitations under the License. |
3638 | |
3639 | import os |
3640 | +from subprocess import check_call, CalledProcessError |
3641 | from charmhelpers.fetch import ( |
3642 | BaseFetchHandler, |
3643 | - UnhandledSource |
3644 | + UnhandledSource, |
3645 | + filter_installed_packages, |
3646 | + install, |
3647 | ) |
3648 | -from charmhelpers.core.host import mkdir |
3649 | - |
3650 | -import six |
3651 | -if six.PY3: |
3652 | - raise ImportError('GitPython does not support Python 3') |
3653 | - |
3654 | -try: |
3655 | - from git import Repo |
3656 | -except ImportError: |
3657 | - from charmhelpers.fetch import apt_install |
3658 | - apt_install("python-git") |
3659 | - from git import Repo |
3660 | - |
3661 | -from git.exc import GitCommandError # noqa E402 |
3662 | + |
3663 | +if filter_installed_packages(['git']) != []: |
3664 | + install(['git']) |
3665 | + if filter_installed_packages(['git']) != []: |
3666 | + raise NotImplementedError('Unable to install git') |
3667 | |
3668 | |
3669 | class GitUrlFetchHandler(BaseFetchHandler): |
3670 | - """Handler for git branches via generic and github URLs""" |
3671 | + """Handler for git branches via generic and github URLs.""" |
3672 | + |
3673 | def can_handle(self, source): |
3674 | url_parts = self.parse_url(source) |
3675 | # TODO (mattyw) no support for ssh git@ yet |
3676 | - if url_parts.scheme not in ('http', 'https', 'git'): |
3677 | + if url_parts.scheme not in ('http', 'https', 'git', ''): |
3678 | return False |
3679 | + elif not url_parts.scheme: |
3680 | + return os.path.exists(os.path.join(source, '.git')) |
3681 | else: |
3682 | return True |
3683 | |
3684 | - def clone(self, source, dest, branch, depth=None): |
3685 | + def clone(self, source, dest, branch="master", depth=None): |
3686 | if not self.can_handle(source): |
3687 | raise UnhandledSource("Cannot handle {}".format(source)) |
3688 | |
3689 | - if depth: |
3690 | - Repo.clone_from(source, dest, branch=branch, depth=depth) |
3691 | + if os.path.exists(dest): |
3692 | + cmd = ['git', '-C', dest, 'pull', source, branch] |
3693 | else: |
3694 | - Repo.clone_from(source, dest, branch=branch) |
3695 | + cmd = ['git', 'clone', source, dest, '--branch', branch] |
3696 | + if depth: |
3697 | + cmd.extend(['--depth', depth]) |
3698 | + check_call(cmd) |
3699 | |
3700 | def install(self, source, branch="master", dest=None, depth=None): |
3701 | url_parts = self.parse_url(source) |
3702 | @@ -62,12 +60,10 @@ |
3703 | else: |
3704 | dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", |
3705 | branch_name) |
3706 | - if not os.path.exists(dest_dir): |
3707 | - mkdir(dest_dir, perms=0o755) |
3708 | try: |
3709 | self.clone(source, dest_dir, branch, depth) |
3710 | - except GitCommandError as e: |
3711 | - raise UnhandledSource(e.message) |
3712 | + except CalledProcessError as e: |
3713 | + raise UnhandledSource(e) |
3714 | except OSError as e: |
3715 | raise UnhandledSource(e.strerror) |
3716 | return dest_dir |
3717 | |
3718 | === added file 'hooks/charmhelpers/fetch/ubuntu.py' |
3719 | --- hooks/charmhelpers/fetch/ubuntu.py 1970-01-01 00:00:00 +0000 |
3720 | +++ hooks/charmhelpers/fetch/ubuntu.py 2016-09-16 19:19:06 +0000 |
3721 | @@ -0,0 +1,313 @@ |
3722 | +# Copyright 2014-2015 Canonical Limited. |
3723 | +# |
3724 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
3725 | +# you may not use this file except in compliance with the License. |
3726 | +# You may obtain a copy of the License at |
3727 | +# |
3728 | +# http://www.apache.org/licenses/LICENSE-2.0 |
3729 | +# |
3730 | +# Unless required by applicable law or agreed to in writing, software |
3731 | +# distributed under the License is distributed on an "AS IS" BASIS, |
3732 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
3733 | +# See the License for the specific language governing permissions and |
3734 | +# limitations under the License. |
3735 | + |
3736 | +import os |
3737 | +import six |
3738 | +import time |
3739 | +import subprocess |
3740 | + |
3741 | +from tempfile import NamedTemporaryFile |
3742 | +from charmhelpers.core.host import ( |
3743 | + lsb_release |
3744 | +) |
3745 | +from charmhelpers.core.hookenv import log |
3746 | +from charmhelpers.fetch import SourceConfigError |
3747 | + |
3748 | +CLOUD_ARCHIVE = ('# Ubuntu Cloud Archive deb' |
3749 | + ' http://ubuntu-cloud.archive.canonical.com/ubuntu' |
3750 | + ' {} main') |
3751 | +PROPOSED_POCKET = ('# Proposed deb http://archive.ubuntu.com/ubuntu' |
3752 | + ' {}-proposed main universe multiverse restricted') |
3753 | +CLOUD_ARCHIVE_POCKETS = { |
3754 | + # Folsom |
3755 | + 'folsom': 'precise-updates/folsom', |
3756 | + 'precise-folsom': 'precise-updates/folsom', |
3757 | + 'precise-folsom/updates': 'precise-updates/folsom', |
3758 | + 'precise-updates/folsom': 'precise-updates/folsom', |
3759 | + 'folsom/proposed': 'precise-proposed/folsom', |
3760 | + 'precise-folsom/proposed': 'precise-proposed/folsom', |
3761 | + 'precise-proposed/folsom': 'precise-proposed/folsom', |
3762 | + # Grizzly |
3763 | + 'grizzly': 'precise-updates/grizzly', |
3764 | + 'precise-grizzly': 'precise-updates/grizzly', |
3765 | + 'precise-grizzly/updates': 'precise-updates/grizzly', |
3766 | + 'precise-updates/grizzly': 'precise-updates/grizzly', |
3767 | + 'grizzly/proposed': 'precise-proposed/grizzly', |
3768 | + 'precise-grizzly/proposed': 'precise-proposed/grizzly', |
3769 | + 'precise-proposed/grizzly': 'precise-proposed/grizzly', |
3770 | + # Havana |
3771 | + 'havana': 'precise-updates/havana', |
3772 | + 'precise-havana': 'precise-updates/havana', |
3773 | + 'precise-havana/updates': 'precise-updates/havana', |
3774 | + 'precise-updates/havana': 'precise-updates/havana', |
3775 | + 'havana/proposed': 'precise-proposed/havana', |
3776 | + 'precise-havana/proposed': 'precise-proposed/havana', |
3777 | + 'precise-proposed/havana': 'precise-proposed/havana', |
3778 | + # Icehouse |
3779 | + 'icehouse': 'precise-updates/icehouse', |
3780 | + 'precise-icehouse': 'precise-updates/icehouse', |
3781 | + 'precise-icehouse/updates': 'precise-updates/icehouse', |
3782 | + 'precise-updates/icehouse': 'precise-updates/icehouse', |
3783 | + 'icehouse/proposed': 'precise-proposed/icehouse', |
3784 | + 'precise-icehouse/proposed': 'precise-proposed/icehouse', |
3785 | + 'precise-proposed/icehouse': 'precise-proposed/icehouse', |
3786 | + # Juno |
3787 | + 'juno': 'trusty-updates/juno', |
3788 | + 'trusty-juno': 'trusty-updates/juno', |
3789 | + 'trusty-juno/updates': 'trusty-updates/juno', |
3790 | + 'trusty-updates/juno': 'trusty-updates/juno', |
3791 | + 'juno/proposed': 'trusty-proposed/juno', |
3792 | + 'trusty-juno/proposed': 'trusty-proposed/juno', |
3793 | + 'trusty-proposed/juno': 'trusty-proposed/juno', |
3794 | + # Kilo |
3795 | + 'kilo': 'trusty-updates/kilo', |
3796 | + 'trusty-kilo': 'trusty-updates/kilo', |
3797 | + 'trusty-kilo/updates': 'trusty-updates/kilo', |
3798 | + 'trusty-updates/kilo': 'trusty-updates/kilo', |
3799 | + 'kilo/proposed': 'trusty-proposed/kilo', |
3800 | + 'trusty-kilo/proposed': 'trusty-proposed/kilo', |
3801 | + 'trusty-proposed/kilo': 'trusty-proposed/kilo', |
3802 | + # Liberty |
3803 | + 'liberty': 'trusty-updates/liberty', |
3804 | + 'trusty-liberty': 'trusty-updates/liberty', |
3805 | + 'trusty-liberty/updates': 'trusty-updates/liberty', |
3806 | + 'trusty-updates/liberty': 'trusty-updates/liberty', |
3807 | + 'liberty/proposed': 'trusty-proposed/liberty', |
3808 | + 'trusty-liberty/proposed': 'trusty-proposed/liberty', |
3809 | + 'trusty-proposed/liberty': 'trusty-proposed/liberty', |
3810 | + # Mitaka |
3811 | + 'mitaka': 'trusty-updates/mitaka', |
3812 | + 'trusty-mitaka': 'trusty-updates/mitaka', |
3813 | + 'trusty-mitaka/updates': 'trusty-updates/mitaka', |
3814 | + 'trusty-updates/mitaka': 'trusty-updates/mitaka', |
3815 | + 'mitaka/proposed': 'trusty-proposed/mitaka', |
3816 | + 'trusty-mitaka/proposed': 'trusty-proposed/mitaka', |
3817 | + 'trusty-proposed/mitaka': 'trusty-proposed/mitaka', |
3818 | + # Newton |
3819 | + 'newton': 'xenial-updates/newton', |
3820 | + 'xenial-newton': 'xenial-updates/newton', |
3821 | + 'xenial-newton/updates': 'xenial-updates/newton', |
3822 | + 'xenial-updates/newton': 'xenial-updates/newton', |
3823 | + 'newton/proposed': 'xenial-proposed/newton', |
3824 | + 'xenial-newton/proposed': 'xenial-proposed/newton', |
3825 | + 'xenial-proposed/newton': 'xenial-proposed/newton', |
3826 | +} |
3827 | + |
3828 | +APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT. |
3829 | +APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks. |
3830 | +APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times. |
3831 | + |
3832 | + |
3833 | +def filter_installed_packages(packages): |
3834 | + """Return a list of packages that require installation.""" |
3835 | + cache = apt_cache() |
3836 | + _pkgs = [] |
3837 | + for package in packages: |
3838 | + try: |
3839 | + p = cache[package] |
3840 | + p.current_ver or _pkgs.append(package) |
3841 | + except KeyError: |
3842 | + log('Package {} has no installation candidate.'.format(package), |
3843 | + level='WARNING') |
3844 | + _pkgs.append(package) |
3845 | + return _pkgs |
3846 | + |
3847 | + |
3848 | +def apt_cache(in_memory=True, progress=None): |
3849 | + """Build and return an apt cache.""" |
3850 | + from apt import apt_pkg |
3851 | + apt_pkg.init() |
3852 | + if in_memory: |
3853 | + apt_pkg.config.set("Dir::Cache::pkgcache", "") |
3854 | + apt_pkg.config.set("Dir::Cache::srcpkgcache", "") |
3855 | + return apt_pkg.Cache(progress) |
3856 | + |
3857 | + |
3858 | +def install(packages, options=None, fatal=False): |
3859 | + """Install one or more packages.""" |
3860 | + if options is None: |
3861 | + options = ['--option=Dpkg::Options::=--force-confold'] |
3862 | + |
3863 | + cmd = ['apt-get', '--assume-yes'] |
3864 | + cmd.extend(options) |
3865 | + cmd.append('install') |
3866 | + if isinstance(packages, six.string_types): |
3867 | + cmd.append(packages) |
3868 | + else: |
3869 | + cmd.extend(packages) |
3870 | + log("Installing {} with options: {}".format(packages, |
3871 | + options)) |
3872 | + _run_apt_command(cmd, fatal) |
3873 | + |
3874 | + |
3875 | +def upgrade(options=None, fatal=False, dist=False): |
3876 | + """Upgrade all packages.""" |
3877 | + if options is None: |
3878 | + options = ['--option=Dpkg::Options::=--force-confold'] |
3879 | + |
3880 | + cmd = ['apt-get', '--assume-yes'] |
3881 | + cmd.extend(options) |
3882 | + if dist: |
3883 | + cmd.append('dist-upgrade') |
3884 | + else: |
3885 | + cmd.append('upgrade') |
3886 | + log("Upgrading with options: {}".format(options)) |
3887 | + _run_apt_command(cmd, fatal) |
3888 | + |
3889 | + |
3890 | +def update(fatal=False): |
3891 | + """Update local apt cache.""" |
3892 | + cmd = ['apt-get', 'update'] |
3893 | + _run_apt_command(cmd, fatal) |
3894 | + |
3895 | + |
3896 | +def purge(packages, fatal=False): |
3897 | + """Purge one or more packages.""" |
3898 | + cmd = ['apt-get', '--assume-yes', 'purge'] |
3899 | + if isinstance(packages, six.string_types): |
3900 | + cmd.append(packages) |
3901 | + else: |
3902 | + cmd.extend(packages) |
3903 | + log("Purging {}".format(packages)) |
3904 | + _run_apt_command(cmd, fatal) |
3905 | + |
3906 | + |
3907 | +def apt_mark(packages, mark, fatal=False): |
3908 | + """Flag one or more packages using apt-mark.""" |
3909 | + log("Marking {} as {}".format(packages, mark)) |
3910 | + cmd = ['apt-mark', mark] |
3911 | + if isinstance(packages, six.string_types): |
3912 | + cmd.append(packages) |
3913 | + else: |
3914 | + cmd.extend(packages) |
3915 | + |
3916 | + if fatal: |
3917 | + subprocess.check_call(cmd, universal_newlines=True) |
3918 | + else: |
3919 | + subprocess.call(cmd, universal_newlines=True) |
3920 | + |
3921 | + |
3922 | +def apt_hold(packages, fatal=False): |
3923 | + return apt_mark(packages, 'hold', fatal=fatal) |
3924 | + |
3925 | + |
3926 | +def apt_unhold(packages, fatal=False): |
3927 | + return apt_mark(packages, 'unhold', fatal=fatal) |
3928 | + |
3929 | + |
3930 | +def add_source(source, key=None): |
3931 | + """Add a package source to this system. |
3932 | + |
3933 | + @param source: a URL or sources.list entry, as supported by |
3934 | + add-apt-repository(1). Examples:: |
3935 | + |
3936 | + ppa:charmers/example |
3937 | + deb https://stub:key@private.example.com/ubuntu trusty main |
3938 | + |
3939 | + In addition: |
3940 | + 'proposed:' may be used to enable the standard 'proposed' |
3941 | + pocket for the release. |
3942 | + 'cloud:' may be used to activate official cloud archive pockets, |
3943 | + such as 'cloud:icehouse' |
3944 | + 'distro' may be used as a noop |
3945 | + |
3946 | + @param key: A key to be added to the system's APT keyring and used |
3947 | + to verify the signatures on packages. Ideally, this should be an |
3948 | + ASCII format GPG public key including the block headers. A GPG key |
3949 | + id may also be used, but be aware that only insecure protocols are |
3950 | + available to retrieve the actual public key from a public keyserver |
3951 | + placing your Juju environment at risk. ppa and cloud archive keys |
3952 | + are securely added automtically, so sould not be provided. |
3953 | + """ |
3954 | + if source is None: |
3955 | + log('Source is not present. Skipping') |
3956 | + return |
3957 | + |
3958 | + if (source.startswith('ppa:') or |
3959 | + source.startswith('http') or |
3960 | + source.startswith('deb ') or |
3961 | + source.startswith('cloud-archive:')): |
3962 | + subprocess.check_call(['add-apt-repository', '--yes', source]) |
3963 | + elif source.startswith('cloud:'): |
3964 | + install(filter_installed_packages(['ubuntu-cloud-keyring']), |
3965 | + fatal=True) |
3966 | + pocket = source.split(':')[-1] |
3967 | + if pocket not in CLOUD_ARCHIVE_POCKETS: |
3968 | + raise SourceConfigError( |
3969 | + 'Unsupported cloud: source option %s' % |
3970 | + pocket) |
3971 | + actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket] |
3972 | + with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt: |
3973 | + apt.write(CLOUD_ARCHIVE.format(actual_pocket)) |
3974 | + elif source == 'proposed': |
3975 | + release = lsb_release()['DISTRIB_CODENAME'] |
3976 | + with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt: |
3977 | + apt.write(PROPOSED_POCKET.format(release)) |
3978 | + elif source == 'distro': |
3979 | + pass |
3980 | + else: |
3981 | + log("Unknown source: {!r}".format(source)) |
3982 | + |
3983 | + if key: |
3984 | + if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key: |
3985 | + with NamedTemporaryFile('w+') as key_file: |
3986 | + key_file.write(key) |
3987 | + key_file.flush() |
3988 | + key_file.seek(0) |
3989 | + subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file) |
3990 | + else: |
3991 | + # Note that hkp: is in no way a secure protocol. Using a |
3992 | + # GPG key id is pointless from a security POV unless you |
3993 | + # absolutely trust your network and DNS. |
3994 | + subprocess.check_call(['apt-key', 'adv', '--keyserver', |
3995 | + 'hkp://keyserver.ubuntu.com:80', '--recv', |
3996 | + key]) |
3997 | + |
3998 | + |
3999 | +def _run_apt_command(cmd, fatal=False): |
4000 | + """Run an APT command. |
4001 | + |
4002 | + Checks the output and retries if the fatal flag is set |
4003 | + to True. |
4004 | + |
4005 | + :param: cmd: str: The apt command to run. |
4006 | + :param: fatal: bool: Whether the command's output should be checked and |
4007 | + retried. |
4008 | + """ |
4009 | + env = os.environ.copy() |
4010 | + |
4011 | + if 'DEBIAN_FRONTEND' not in env: |
4012 | + env['DEBIAN_FRONTEND'] = 'noninteractive' |
4013 | + |
4014 | + if fatal: |
4015 | + retry_count = 0 |
4016 | + result = None |
4017 | + |
4018 | + # If the command is considered "fatal", we need to retry if the apt |
4019 | + # lock was not acquired. |
4020 | + |
4021 | + while result is None or result == APT_NO_LOCK: |
4022 | + try: |
4023 | + result = subprocess.check_call(cmd, env=env) |
4024 | + except subprocess.CalledProcessError as e: |
4025 | + retry_count = retry_count + 1 |
4026 | + if retry_count > APT_NO_LOCK_RETRY_COUNT: |
4027 | + raise |
4028 | + result = e.returncode |
4029 | + log("Couldn't acquire DPKG lock. Will retry in {} seconds." |
4030 | + "".format(APT_NO_LOCK_RETRY_DELAY)) |
4031 | + time.sleep(APT_NO_LOCK_RETRY_DELAY) |
4032 | + |
4033 | + else: |
4034 | + subprocess.call(cmd, env=env) |
4035 | |
4036 | === added file 'hooks/charmhelpers/osplatform.py' |
4037 | --- hooks/charmhelpers/osplatform.py 1970-01-01 00:00:00 +0000 |
4038 | +++ hooks/charmhelpers/osplatform.py 2016-09-16 19:19:06 +0000 |
4039 | @@ -0,0 +1,19 @@ |
4040 | +import platform |
4041 | + |
4042 | + |
4043 | +def get_platform(): |
4044 | + """Return the current OS platform. |
4045 | + |
4046 | + For example: if current os platform is Ubuntu then a string "ubuntu" |
4047 | + will be returned (which is the name of the module). |
4048 | + This string is used to decide which platform module should be imported. |
4049 | + """ |
4050 | + tuple_platform = platform.linux_distribution() |
4051 | + current_platform = tuple_platform[0] |
4052 | + if "Ubuntu" in current_platform: |
4053 | + return "ubuntu" |
4054 | + elif "CentOS" in current_platform: |
4055 | + return "centos" |
4056 | + else: |
4057 | + raise RuntimeError("This module is not supported on {}." |
4058 | + .format(current_platform)) |
4059 | |
4060 | === modified file 'hooks/memcached_hooks.py' |
4061 | --- hooks/memcached_hooks.py 2016-09-02 09:04:58 +0000 |
4062 | +++ hooks/memcached_hooks.py 2016-09-16 19:19:06 +0000 |
4063 | @@ -1,4 +1,4 @@ |
4064 | -#!/usr/bin/env python |
4065 | +#!/usr/bin/env python3 |
4066 | import re |
4067 | import os |
4068 | import shutil |
4069 | |
4070 | === added file 'setup.cfg' |
4071 | --- setup.cfg 1970-01-01 00:00:00 +0000 |
4072 | +++ setup.cfg 2016-09-16 19:19:06 +0000 |
4073 | @@ -0,0 +1,5 @@ |
4074 | +[nosetests] |
4075 | +verbosity=2 |
4076 | +with-coverage=1 |
4077 | +cover-erase=1 |
4078 | +cover-package=hooks |
4079 | |
4080 | === added file 'test-requirements-py2.txt' |
4081 | --- test-requirements-py2.txt 1970-01-01 00:00:00 +0000 |
4082 | +++ test-requirements-py2.txt 2016-09-16 19:19:06 +0000 |
4083 | @@ -0,0 +1,9 @@ |
4084 | +nose==1.3.1 |
4085 | +Jinja2==2.7 |
4086 | +mock==1.0.1 |
4087 | +PyYAML==3.10 |
4088 | +six==1.5.2 |
4089 | +coverage==3.7.1 |
4090 | +flake8==2.1.0 |
4091 | +simplejson |
4092 | +charm-tools |
4093 | |
4094 | === added file 'test-requirements-py3.txt' |
4095 | --- test-requirements-py3.txt 1970-01-01 00:00:00 +0000 |
4096 | +++ test-requirements-py3.txt 2016-09-16 19:19:06 +0000 |
4097 | @@ -0,0 +1,7 @@ |
4098 | +nose==1.3.7 |
4099 | +Jinja2==2.8 |
4100 | +mock==1.3.0 |
4101 | +PyYAML==3.11 |
4102 | +six==1.10.0 |
4103 | +coverage==3.7.1 |
4104 | +flake8==2.5.4 |
4105 | |
4106 | === added file 'tox.ini' |
4107 | --- tox.ini 1970-01-01 00:00:00 +0000 |
4108 | +++ tox.ini 2016-09-16 19:19:06 +0000 |
4109 | @@ -0,0 +1,38 @@ |
4110 | +[tox] |
4111 | +envlist = pep8,py27,py34,py35 |
4112 | +skipsdist = True |
4113 | + |
4114 | +[testenv] |
4115 | +setenv = VIRTUAL_ENV={envdir} |
4116 | + PYTHONHASHSEED=0 |
4117 | +install_command = |
4118 | + pip install --allow-unverified python-apt {opts} {packages} |
4119 | +commands = nosetests -s --nologcapture {posargs} unit_tests/ |
4120 | + |
4121 | +# trusty |
4122 | +[testenv:py27] |
4123 | +basepython = python2.7 |
4124 | +deps = -r{toxinidir}/test-requirements-py2.txt |
4125 | + |
4126 | +# trusty |
4127 | +[testenv:py34] |
4128 | +basepython = python3.4 |
4129 | +deps = -r{toxinidir}/test-requirements-py3.txt |
4130 | + |
4131 | +# xenial |
4132 | +[testenv:py35] |
4133 | +basepython = python3.5 |
4134 | +deps = -r{toxinidir}/test-requirements-py3.txt |
4135 | + |
4136 | +[testenv:pep8] |
4137 | +basepython = python2.7 |
4138 | +deps = -r{toxinidir}/test-requirements-py2.txt |
4139 | +commands = flake8 {posargs} hooks unit_tests tests |
4140 | + charm-proof |
4141 | + |
4142 | +[testenv:venv] |
4143 | +commands = {posargs} |
4144 | + |
4145 | +[flake8] |
4146 | +ignore = E402,E226 |
4147 | +exclude = hooks/charmhelpers |
4148 | |
4149 | === modified file 'unit_tests/test_memcached_hooks.py' |
4150 | --- unit_tests/test_memcached_hooks.py 2015-07-22 20:13:58 +0000 |
4151 | +++ unit_tests/test_memcached_hooks.py 2016-09-16 19:19:06 +0000 |
4152 | @@ -2,7 +2,7 @@ |
4153 | import os |
4154 | import shutil |
4155 | import tempfile |
4156 | -from test_utils import CharmTestCase |
4157 | +from unit_tests.test_utils import CharmTestCase, get_default_config |
4158 | import memcached_hooks |
4159 | |
4160 | __author__ = 'Felipe Reyes <felipe.reyes@canonical.com>' |
4161 | @@ -22,6 +22,7 @@ |
4162 | 'log', |
4163 | 'oldest_peer', |
4164 | 'peer_units', |
4165 | + 'open_port' |
4166 | ] |
4167 | FREE_MEM_SMALL = """ total used free shared \ |
4168 | buffers cached |
4169 | @@ -159,23 +160,28 @@ |
4170 | self.test_config.config['size']) |
4171 | open_port.assert_any_call(configs['tcp-port'], 'TCP') |
4172 | |
4173 | + @mock.patch('charmhelpers.contrib.network.ufw.modify_access') |
4174 | + @mock.patch('charmhelpers.contrib.network.ufw.grant_access') |
4175 | + @mock.patch('memcached_utils.config') |
4176 | @mock.patch('subprocess.Popen') |
4177 | @mock.patch('charmhelpers.core.templating.render') |
4178 | @mock.patch('subprocess.check_output') |
4179 | @mock.patch('memcached_utils.log') |
4180 | def test_config_changed_size_set_0_small(self, log, check_output, render, |
4181 | - popen): |
4182 | + popen, mucfg, *args): |
4183 | p = mock.Mock() |
4184 | p.configure_mock(**{'communicate.return_value': ('stdout', 'stderr'), |
4185 | 'returncode': 0}) |
4186 | popen.return_value = p |
4187 | |
4188 | - configs = {'size': 0} |
4189 | + configs = get_default_config() |
4190 | + configs.update({'size': 0}) |
4191 | |
4192 | def f(c): |
4193 | - return configs.get(c, None) |
4194 | + return configs[c] |
4195 | |
4196 | self.config.side_effect = f |
4197 | + mucfg.side_effect = f |
4198 | |
4199 | def g(*args, **kwargs): |
4200 | if args[0] == ['free', '-m']: |
4201 | @@ -191,30 +197,35 @@ |
4202 | self.assertEqual(passed_vars['mem_size'], 1596) |
4203 | self.assertFalse(passed_vars['large_pages_enabled']) |
4204 | |
4205 | + @mock.patch('charmhelpers.contrib.network.ufw.modify_access') |
4206 | + @mock.patch('charmhelpers.contrib.network.ufw.grant_access') |
4207 | + @mock.patch('memcached_utils.config') |
4208 | @mock.patch('subprocess.Popen') |
4209 | @mock.patch('charmhelpers.core.templating.render') |
4210 | @mock.patch('subprocess.check_output') |
4211 | @mock.patch('memcached_utils.log') |
4212 | def test_config_changed_size_set_0_big(self, log, check_output, render, |
4213 | - popen): |
4214 | + popen, mucfg, *args): |
4215 | p = mock.Mock() |
4216 | p.configure_mock(**{'communicate.return_value': ('stdout', 'stderr'), |
4217 | 'returncode': 0}) |
4218 | popen.return_value = p |
4219 | |
4220 | - configs = {'size': 0, |
4221 | - 'disable-large-pages': False} |
4222 | + configs = get_default_config() |
4223 | + configs.update({'size': 0, |
4224 | + 'disable-large-pages': False}) |
4225 | |
4226 | def f(c): |
4227 | - return configs.get(c, None) |
4228 | + return configs[c] |
4229 | |
4230 | self.config.side_effect = f |
4231 | + mucfg.side_effect = f |
4232 | |
4233 | def g(*args, **kwargs): |
4234 | if args[0] == ['free', '-m']: |
4235 | return FREE_MEM_BIG |
4236 | if args[0] == ['sysctl', '-n', 'vm.nr_hugepages']: |
4237 | - return int(16013 / 2) |
4238 | + return int(16013 * 2) |
4239 | else: |
4240 | return "" |
4241 | |
4242 | @@ -228,24 +239,30 @@ |
4243 | 'vm.nr_hugepages={}'.format(16013/2)]) |
4244 | self.assertTrue(passed_vars['large_pages_enabled']) |
4245 | |
4246 | + @mock.patch('charmhelpers.contrib.network.ufw.modify_access') |
4247 | + @mock.patch('charmhelpers.contrib.network.ufw.grant_access') |
4248 | + @mock.patch('memcached_utils.config') |
4249 | @mock.patch('subprocess.Popen') |
4250 | @mock.patch('charmhelpers.core.templating.render') |
4251 | @mock.patch('subprocess.check_output') |
4252 | @mock.patch('memcached_utils.log') |
4253 | def test_config_changed_size_failed_set_hpages(self, log, check_output, |
4254 | - render, popen): |
4255 | + render, popen, mucfg, |
4256 | + *args): |
4257 | p = mock.Mock() |
4258 | p.configure_mock(**{'communicate.return_value': ('stdout', 'stderr'), |
4259 | 'returncode': 0}) |
4260 | popen.return_value = p |
4261 | |
4262 | - configs = {'size': 0, |
4263 | - 'disable-large-pages': False} |
4264 | + configs = get_default_config() |
4265 | + configs.update({'size': 0, |
4266 | + 'disable-large-pages': False}) |
4267 | |
4268 | def f(c): |
4269 | - return configs.get(c, None) |
4270 | + return configs[c] |
4271 | |
4272 | self.config.side_effect = f |
4273 | + mucfg.side_effect = f |
4274 | |
4275 | def g(*args, **kwargs): |
4276 | if args[0] == ['free', '-m']: |
4277 | @@ -435,31 +452,31 @@ |
4278 | cache_joined, ufw, unit_get, replica, |
4279 | open_port): |
4280 | params = { |
4281 | - 'tcp_port': None, |
4282 | - 'disable_cas': None, |
4283 | - 'mem_size': None, |
4284 | - 'factor': None, |
4285 | - 'connection_limit': None, |
4286 | - 'udp_port': None, |
4287 | - 'slab_page_size': None, |
4288 | - 'threads': None, |
4289 | + 'tcp_port': 11211, |
4290 | + 'disable_cas': False, |
4291 | + 'mem_size': 768, |
4292 | + 'factor': 1.25, |
4293 | + 'connection_limit': 1024, |
4294 | + 'udp_port': 0, |
4295 | + 'slab_page_size': -1, |
4296 | + 'threads': -1, |
4297 | + 'replica': "10.0.0.2", |
4298 | + 'repcached_port': '11212', |
4299 | 'large_pages_enabled': False, |
4300 | - 'min_item_size': None, |
4301 | - 'repcached_port': '11212', |
4302 | - 'request_limit': None, |
4303 | - 'disable_auto_cleanup': None, |
4304 | + 'min_item_size': -1, |
4305 | + 'request_limit': -1, |
4306 | + 'disable_auto_cleanup': False, |
4307 | } |
4308 | |
4309 | dpkg.return_value = True |
4310 | replica.return_value = ('10.0.0.2') |
4311 | |
4312 | - configs = { |
4313 | - 'repcached': True, |
4314 | - 'memsize': 1, |
4315 | - } |
4316 | + configs = get_default_config() |
4317 | + configs.update({'repcached': True, |
4318 | + 'memsize': 1}) |
4319 | |
4320 | def f(c): |
4321 | - return configs.get(c, params.get(c)) |
4322 | + return configs[c] |
4323 | |
4324 | self.config.side_effect = f |
4325 | self.unit_get.return_value = '10.0.0.1' |
4326 | @@ -482,32 +499,31 @@ |
4327 | def test_config_changed_no_replica(self, dpkg, render, |
4328 | replica, ufw, cache, open_port): |
4329 | params = { |
4330 | - 'tcp_port': None, |
4331 | - 'disable_cas': None, |
4332 | - 'mem_size': None, |
4333 | - 'factor': None, |
4334 | - 'connection_limit': None, |
4335 | - 'udp_port': None, |
4336 | - 'slab_page_size': None, |
4337 | - 'threads': None, |
4338 | + 'tcp_port': 11211, |
4339 | + 'disable_cas': False, |
4340 | + 'mem_size': 768, |
4341 | + 'factor': 1.25, |
4342 | + 'connection_limit': 1024, |
4343 | + 'udp_port': 0, |
4344 | + 'slab_page_size': -1, |
4345 | + 'threads': -1, |
4346 | 'replica': "10.0.0.2", |
4347 | 'repcached_port': '11212', |
4348 | 'large_pages_enabled': False, |
4349 | - 'min_item_size': None, |
4350 | - 'request_limit': None, |
4351 | - 'disable_auto_cleanup': None, |
4352 | + 'min_item_size': -1, |
4353 | + 'request_limit': -1, |
4354 | + 'disable_auto_cleanup': False, |
4355 | } |
4356 | |
4357 | replica.return_value = "10.0.0.2" |
4358 | dpkg.return_value = True |
4359 | |
4360 | - configs = { |
4361 | - 'repcached': True, |
4362 | - 'memsize': 1, |
4363 | - } |
4364 | + configs = get_default_config() |
4365 | + configs.update({'repcached': True, |
4366 | + 'memsize': 1}) |
4367 | |
4368 | def f(c): |
4369 | - return configs.get(c, params.get(c)) |
4370 | + return configs[c] |
4371 | |
4372 | self.config.side_effect = f |
4373 | |
4374 | |
4375 | === modified file 'unit_tests/test_utils.py' |
4376 | --- unit_tests/test_utils.py 2014-12-01 19:11:26 +0000 |
4377 | +++ unit_tests/test_utils.py 2016-09-16 19:19:06 +0000 |
4378 | @@ -37,7 +37,7 @@ |
4379 | ''' |
4380 | default_config = {} |
4381 | config = load_config() |
4382 | - for k, v in config.iteritems(): |
4383 | + for k, v in config.items(): |
4384 | if 'default' in v: |
4385 | default_config[k] = v['default'] |
4386 | else: |
Hello,
I checked all the targets, including lint py27, py34, py35, all seems good.
Thanks for fixing @freyes
LGTM.