Merge lp:~freyes/charms/trusty/memcached/py3-tox into lp:charms/trusty/memcached

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
Reviewer Review Type Date Requested Status
Jorge Niedbalski (community) Approve
charmers Pending
Review via email: mp+306002@code.launchpad.net

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.
Revision history for this message
Jorge Niedbalski (niedbalski) wrote :

Hello,

I checked all the targets, including lint py27, py34, py35, all seems good.

Thanks for fixing @freyes

LGTM.

review: Approve

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:

Subscribers

People subscribed via source and target branches