Merge lp:~ivoks/charms/trusty/elasticsearch/refresh into lp:charms/trusty/elasticsearch

Proposed by Ante Karamatić
Status: Needs review
Proposed branch: lp:~ivoks/charms/trusty/elasticsearch/refresh
Merge into: lp:charms/trusty/elasticsearch
Diff against target: 5480 lines (+3717/-626)
42 files modified
README.md (+3/-3)
charm-helpers.yaml (+1/-0)
config.yaml (+1/-1)
hooks/charmhelpers/__init__.py (+36/-0)
hooks/charmhelpers/contrib/__init__.py (+13/-0)
hooks/charmhelpers/contrib/ansible/__init__.py (+86/-5)
hooks/charmhelpers/contrib/templating/__init__.py (+13/-0)
hooks/charmhelpers/contrib/templating/contexts.py (+25/-4)
hooks/charmhelpers/core/__init__.py (+13/-0)
hooks/charmhelpers/core/decorators.py (+55/-0)
hooks/charmhelpers/core/files.py (+43/-0)
hooks/charmhelpers/core/fstab.py (+28/-12)
hooks/charmhelpers/core/hookenv.py (+594/-58)
hooks/charmhelpers/core/host.py (+677/-146)
hooks/charmhelpers/core/host_factory/centos.py (+72/-0)
hooks/charmhelpers/core/host_factory/ubuntu.py (+88/-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 (+14/-0)
hooks/charmhelpers/core/services/base.py (+57/-19)
hooks/charmhelpers/core/services/helpers.py (+64/-13)
hooks/charmhelpers/core/strutils.py (+123/-0)
hooks/charmhelpers/core/sysctl.py (+26/-6)
hooks/charmhelpers/core/templating.py (+44/-11)
hooks/charmhelpers/core/unitdata.py (+518/-0)
hooks/charmhelpers/fetch/__init__.py (+58/-275)
hooks/charmhelpers/fetch/archiveurl.py (+75/-18)
hooks/charmhelpers/fetch/bzrurl.py (+52/-26)
hooks/charmhelpers/fetch/centos.py (+171/-0)
hooks/charmhelpers/fetch/giturl.py (+45/-20)
hooks/charmhelpers/fetch/snap.py (+122/-0)
hooks/charmhelpers/fetch/ubuntu.py (+364/-0)
hooks/charmhelpers/osplatform.py (+25/-0)
hooks/charmhelpers/payload/__init__.py (+14/-0)
hooks/charmhelpers/payload/execd.py (+17/-2)
hooks/hooks.py (+1/-1)
metadata.yaml (+3/-0)
tasks/install-elasticsearch.yml (+1/-1)
templates/elasticsearch.yml (+2/-3)
unit_tests/test_hooks.py (+2/-2)
To merge this branch: bzr merge lp:~ivoks/charms/trusty/elasticsearch/refresh
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+321247@code.launchpad.net

Description of the change

Elasticsearch charm refreshment. Adds support for newer versions of Elasticsearch, drops obsolete workarounds and adds support for 16.04.

To post a comment you must log in.
Revision history for this message
Ante Karamatić (ivoks) wrote :

You might want to drop changes from README.md. This charm obviously doesn't work with outdated Kibana charm, so new Kibana charm is being proposed. That charm is available at https://code.launchpad.net/~ivoks/charms/xenial/kibana/kibana-xenial and is pending charm publication in charmstore.

57. By Ante Karamatić

Allow elasticsearch to start when there's only one unit

discovery.zen.ping.unicast.hosts cannot be empty, so it should not
be configured when there are no peers

58. By Ante Karamatić

len() doesn't exist in ansible

Check if relations.peer is not empty instead

Unmerged revisions

58. By Ante Karamatić

len() doesn't exist in ansible

Check if relations.peer is not empty instead

57. By Ante Karamatić

Allow elasticsearch to start when there's only one unit

discovery.zen.ping.unicast.hosts cannot be empty, so it should not
be configured when there are no peers

56. By Ante Karamatić

Update README.md

55. By Ante Karamatić

Drop support for precise

Precise will EOL in few weeks and it doesn't have python3. There's
no justification to invest time to workaround python2/python3 problem.

54. By Ante Karamatić

Adjust tests for new charmhelpers

53. By Ante Karamatić

Drop obsolete config options in the template

Removed two config options that are not needed any more and
only break newer versions of Elasticsearch

52. By Ante Karamatić

Allow distribution to select JRE

By switching to default-jre-headless we make sure available JRE
is always installable. This doesn't resolve the requirement
on the user to know which Elasticsearch works with wich version
of JRE

51. By Ante Karamatić

Add series in metadata.yaml and add support for xenial

Previous 2 commits enable successful deployment of Elasticsearch
on Xenial and this commit only declares that in metadata

50. By Ante Karamatić

Switch hooks to python3

This is a requirement for deployment in Ubuntu 16.04 or later

49. By Ante Karamatić

Change default repo for elasticsearch

New repo is in line with upstream's instructions and also pulls in
latest 5.x version, instead of old 2.x

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README.md'
2--- README.md 2014-10-31 03:43:31 +0000
3+++ README.md 2017-05-11 14:56:57 +0000
4@@ -14,11 +14,11 @@
5
6 You can simply deploy one node with:
7
8- juju deploy elasticsearch
9+ juju deploy cs:~ivoks/elasticsearch
10
11 You can also deploy and relate the Kibana dashboard:
12
13- juju deploy kibana
14+ juju deploy cs:~ivoks/kibana
15 juju add-relation kibana elasticsearch
16 juju expose kibana
17
18@@ -29,7 +29,7 @@
19
20 Deploy three or more units with:
21
22- juju deploy -n3 elasticsearch
23+ juju deploy -n3 cs:~ivoks/elasticsearch
24
25 And when they have started you can inspect the cluster health:
26
27
28=== modified file 'charm-helpers.yaml'
29--- charm-helpers.yaml 2014-04-08 14:58:22 +0000
30+++ charm-helpers.yaml 2017-05-11 14:56:57 +0000
31@@ -6,3 +6,4 @@
32 - contrib.ansible|inc=*
33 - contrib.templating.contexts
34 - payload.execd|inc=*
35+ - osplatform
36
37=== modified file 'config.yaml'
38--- config.yaml 2016-03-08 16:38:17 +0000
39+++ config.yaml 2017-05-11 14:56:57 +0000
40@@ -1,7 +1,7 @@
41 options:
42 apt-repository:
43 type: string
44- default: "deb http://packages.elastic.co/elasticsearch/2.x/debian stable main"
45+ default: "deb https://artifacts.elastic.co/packages/5.x/apt stable main"
46 description: |
47 A deb-line for the apt archive which contains the elasticsearch package.
48 This is necessary until elasticsearch gets into the debian/ubuntu archives.
49
50=== modified file 'hooks/charmhelpers/__init__.py'
51--- hooks/charmhelpers/__init__.py 2014-02-06 12:54:59 +0000
52+++ hooks/charmhelpers/__init__.py 2017-05-11 14:56:57 +0000
53@@ -0,0 +1,36 @@
54+# Copyright 2014-2015 Canonical Limited.
55+#
56+# Licensed under the Apache License, Version 2.0 (the "License");
57+# you may not use this file except in compliance with the License.
58+# You may obtain a copy of the License at
59+#
60+# http://www.apache.org/licenses/LICENSE-2.0
61+#
62+# Unless required by applicable law or agreed to in writing, software
63+# distributed under the License is distributed on an "AS IS" BASIS,
64+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
65+# See the License for the specific language governing permissions and
66+# limitations under the License.
67+
68+# Bootstrap charm-helpers, installing its dependencies if necessary using
69+# only standard libraries.
70+import subprocess
71+import sys
72+
73+try:
74+ import six # flake8: noqa
75+except ImportError:
76+ if sys.version_info.major == 2:
77+ subprocess.check_call(['apt-get', 'install', '-y', 'python-six'])
78+ else:
79+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-six'])
80+ import six # flake8: noqa
81+
82+try:
83+ import yaml # flake8: noqa
84+except ImportError:
85+ if sys.version_info.major == 2:
86+ subprocess.check_call(['apt-get', 'install', '-y', 'python-yaml'])
87+ else:
88+ subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
89+ import yaml # flake8: noqa
90
91=== modified file 'hooks/charmhelpers/contrib/__init__.py'
92--- hooks/charmhelpers/contrib/__init__.py 2014-02-06 12:54:59 +0000
93+++ hooks/charmhelpers/contrib/__init__.py 2017-05-11 14:56:57 +0000
94@@ -0,0 +1,13 @@
95+# Copyright 2014-2015 Canonical Limited.
96+#
97+# Licensed under the Apache License, Version 2.0 (the "License");
98+# you may not use this file except in compliance with the License.
99+# You may obtain a copy of the License at
100+#
101+# http://www.apache.org/licenses/LICENSE-2.0
102+#
103+# Unless required by applicable law or agreed to in writing, software
104+# distributed under the License is distributed on an "AS IS" BASIS,
105+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
106+# See the License for the specific language governing permissions and
107+# limitations under the License.
108
109=== modified file 'hooks/charmhelpers/contrib/ansible/__init__.py'
110--- hooks/charmhelpers/contrib/ansible/__init__.py 2014-10-24 16:35:00 +0000
111+++ hooks/charmhelpers/contrib/ansible/__init__.py 2017-05-11 14:56:57 +0000
112@@ -1,3 +1,17 @@
113+# Copyright 2014-2015 Canonical Limited.
114+#
115+# Licensed under the Apache License, Version 2.0 (the "License");
116+# you may not use this file except in compliance with the License.
117+# You may obtain a copy of the License at
118+#
119+# http://www.apache.org/licenses/LICENSE-2.0
120+#
121+# Unless required by applicable law or agreed to in writing, software
122+# distributed under the License is distributed on an "AS IS" BASIS,
123+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
124+# See the License for the specific language governing permissions and
125+# limitations under the License.
126+
127 # Copyright 2013 Canonical Ltd.
128 #
129 # Authors:
130@@ -59,9 +73,36 @@
131 .. _playbooks: http://www.ansibleworks.com/docs/playbooks.html
132 .. _modules: http://www.ansibleworks.com/docs/modules.html
133
134+A further feature os the ansible hooks is to provide a light weight "action"
135+scripting tool. This is a decorator that you apply to a function, and that
136+function can now receive cli args, and can pass extra args to the playbook.
137+
138+e.g.
139+
140+
141+@hooks.action()
142+def some_action(amount, force="False"):
143+ "Usage: some-action AMOUNT [force=True]" # <-- shown on error
144+ # process the arguments
145+ # do some calls
146+ # return extra-vars to be passed to ansible-playbook
147+ return {
148+ 'amount': int(amount),
149+ 'type': force,
150+ }
151+
152+You can now create a symlink to hooks.py that can be invoked like a hook, but
153+with cli params:
154+
155+# link actions/some-action to hooks/hooks.py
156+
157+actions/some-action amount=10 force=true
158+
159 """
160 import os
161+import stat
162 import subprocess
163+import functools
164
165 import charmhelpers.contrib.templating.contexts
166 import charmhelpers.core.host
167@@ -96,12 +137,13 @@
168 hosts_file.write('localhost ansible_connection=local')
169
170
171-def apply_playbook(playbook, tags=None):
172+def apply_playbook(playbook, tags=None, extra_vars=None):
173 tags = tags or []
174 tags = ",".join(tags)
175 charmhelpers.contrib.templating.contexts.juju_state_to_yaml(
176 ansible_vars_path, namespace_separator='__',
177- allow_hyphens_in_keys=False)
178+ allow_hyphens_in_keys=False, mode=(stat.S_IRUSR | stat.S_IWUSR))
179+
180 # we want ansible's log output to be unbuffered
181 env = os.environ.copy()
182 env['PYTHONUNBUFFERED'] = "1"
183@@ -113,6 +155,9 @@
184 ]
185 if tags:
186 call.extend(['--tags', '{}'.format(tags)])
187+ if extra_vars:
188+ extra = ["%s=%s" % (k, v) for k, v in extra_vars.items()]
189+ call.extend(['--extra-vars', " ".join(extra)])
190 subprocess.check_call(call, env=env)
191
192
193@@ -156,16 +201,52 @@
194 """Register any hooks handled by ansible."""
195 super(AnsibleHooks, self).__init__()
196
197+ self._actions = {}
198 self.playbook_path = playbook_path
199
200 default_hooks = default_hooks or []
201- noop = lambda *args, **kwargs: None
202+
203+ def noop(*args, **kwargs):
204+ pass
205+
206 for hook in default_hooks:
207 self.register(hook, noop)
208
209+ def register_action(self, name, function):
210+ """Register a hook"""
211+ self._actions[name] = function
212+
213 def execute(self, args):
214 """Execute the hook followed by the playbook using the hook as tag."""
215- super(AnsibleHooks, self).execute(args)
216 hook_name = os.path.basename(args[0])
217+ extra_vars = None
218+ if hook_name in self._actions:
219+ extra_vars = self._actions[hook_name](args[1:])
220+ else:
221+ super(AnsibleHooks, self).execute(args)
222+
223 charmhelpers.contrib.ansible.apply_playbook(
224- self.playbook_path, tags=[hook_name])
225+ self.playbook_path, tags=[hook_name], extra_vars=extra_vars)
226+
227+ def action(self, *action_names):
228+ """Decorator, registering them as actions"""
229+ def action_wrapper(decorated):
230+
231+ @functools.wraps(decorated)
232+ def wrapper(argv):
233+ kwargs = dict(arg.split('=') for arg in argv)
234+ try:
235+ return decorated(**kwargs)
236+ except TypeError as e:
237+ if decorated.__doc__:
238+ e.args += (decorated.__doc__,)
239+ raise
240+
241+ self.register_action(decorated.__name__, wrapper)
242+ if '_' in decorated.__name__:
243+ self.register_action(
244+ decorated.__name__.replace('_', '-'), wrapper)
245+
246+ return wrapper
247+
248+ return action_wrapper
249
250=== modified file 'hooks/charmhelpers/contrib/templating/__init__.py'
251--- hooks/charmhelpers/contrib/templating/__init__.py 2014-02-06 12:54:59 +0000
252+++ hooks/charmhelpers/contrib/templating/__init__.py 2017-05-11 14:56:57 +0000
253@@ -0,0 +1,13 @@
254+# Copyright 2014-2015 Canonical Limited.
255+#
256+# Licensed under the Apache License, Version 2.0 (the "License");
257+# you may not use this file except in compliance with the License.
258+# You may obtain a copy of the License at
259+#
260+# http://www.apache.org/licenses/LICENSE-2.0
261+#
262+# Unless required by applicable law or agreed to in writing, software
263+# distributed under the License is distributed on an "AS IS" BASIS,
264+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
265+# See the License for the specific language governing permissions and
266+# limitations under the License.
267
268=== modified file 'hooks/charmhelpers/contrib/templating/contexts.py'
269--- hooks/charmhelpers/contrib/templating/contexts.py 2014-10-24 16:35:00 +0000
270+++ hooks/charmhelpers/contrib/templating/contexts.py 2017-05-11 14:56:57 +0000
271@@ -1,3 +1,17 @@
272+# Copyright 2014-2015 Canonical Limited.
273+#
274+# Licensed under the Apache License, Version 2.0 (the "License");
275+# you may not use this file except in compliance with the License.
276+# You may obtain a copy of the License at
277+#
278+# http://www.apache.org/licenses/LICENSE-2.0
279+#
280+# Unless required by applicable law or agreed to in writing, software
281+# distributed under the License is distributed on an "AS IS" BASIS,
282+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
283+# See the License for the specific language governing permissions and
284+# limitations under the License.
285+
286 # Copyright 2013 Canonical Ltd.
287 #
288 # Authors:
289@@ -6,6 +20,8 @@
290 import os
291 import yaml
292
293+import six
294+
295 import charmhelpers.core.hookenv
296
297
298@@ -62,7 +78,7 @@
299
300
301 def juju_state_to_yaml(yaml_path, namespace_separator=':',
302- allow_hyphens_in_keys=True):
303+ allow_hyphens_in_keys=True, mode=None):
304 """Update the juju config and state in a yaml file.
305
306 This includes any current relation-get data, and the charm
307@@ -92,9 +108,9 @@
308
309 # Don't use non-standard tags for unicode which will not
310 # work when salt uses yaml.load_safe.
311- yaml.add_representer(unicode, lambda dumper,
312- value: dumper.represent_scalar(
313- u'tag:yaml.org,2002:str', value))
314+ yaml.add_representer(six.text_type,
315+ lambda dumper, value: dumper.represent_scalar(
316+ six.u('tag:yaml.org,2002:str'), value))
317
318 yaml_dir = os.path.dirname(yaml_path)
319 if not os.path.exists(yaml_dir):
320@@ -104,8 +120,13 @@
321 with open(yaml_path, "r") as existing_vars_file:
322 existing_vars = yaml.load(existing_vars_file.read())
323 else:
324+ with open(yaml_path, "w+"):
325+ pass
326 existing_vars = {}
327
328+ if mode is not None:
329+ os.chmod(yaml_path, mode)
330+
331 if not allow_hyphens_in_keys:
332 config = dict_keys_without_hyphens(config)
333 existing_vars.update(config)
334
335=== modified file 'hooks/charmhelpers/core/__init__.py'
336--- hooks/charmhelpers/core/__init__.py 2014-02-06 12:54:59 +0000
337+++ hooks/charmhelpers/core/__init__.py 2017-05-11 14:56:57 +0000
338@@ -0,0 +1,13 @@
339+# Copyright 2014-2015 Canonical Limited.
340+#
341+# Licensed under the Apache License, Version 2.0 (the "License");
342+# you may not use this file except in compliance with the License.
343+# You may obtain a copy of the License at
344+#
345+# http://www.apache.org/licenses/LICENSE-2.0
346+#
347+# Unless required by applicable law or agreed to in writing, software
348+# distributed under the License is distributed on an "AS IS" BASIS,
349+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
350+# See the License for the specific language governing permissions and
351+# limitations under the License.
352
353=== added file 'hooks/charmhelpers/core/decorators.py'
354--- hooks/charmhelpers/core/decorators.py 1970-01-01 00:00:00 +0000
355+++ hooks/charmhelpers/core/decorators.py 2017-05-11 14:56:57 +0000
356@@ -0,0 +1,55 @@
357+# Copyright 2014-2015 Canonical Limited.
358+#
359+# Licensed under the Apache License, Version 2.0 (the "License");
360+# you may not use this file except in compliance with the License.
361+# You may obtain a copy of the License at
362+#
363+# http://www.apache.org/licenses/LICENSE-2.0
364+#
365+# Unless required by applicable law or agreed to in writing, software
366+# distributed under the License is distributed on an "AS IS" BASIS,
367+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
368+# See the License for the specific language governing permissions and
369+# limitations under the License.
370+
371+#
372+# Copyright 2014 Canonical Ltd.
373+#
374+# Authors:
375+# Edward Hope-Morley <opentastic@gmail.com>
376+#
377+
378+import time
379+
380+from charmhelpers.core.hookenv import (
381+ log,
382+ INFO,
383+)
384+
385+
386+def retry_on_exception(num_retries, base_delay=0, exc_type=Exception):
387+ """If the decorated function raises exception exc_type, allow num_retries
388+ retry attempts before raise the exception.
389+ """
390+ def _retry_on_exception_inner_1(f):
391+ def _retry_on_exception_inner_2(*args, **kwargs):
392+ retries = num_retries
393+ multiplier = 1
394+ while True:
395+ try:
396+ return f(*args, **kwargs)
397+ except exc_type:
398+ if not retries:
399+ raise
400+
401+ delay = base_delay * multiplier
402+ multiplier += 1
403+ log("Retrying '%s' %d more times (delay=%s)" %
404+ (f.__name__, retries, delay), level=INFO)
405+ retries -= 1
406+ if delay:
407+ time.sleep(delay)
408+
409+ return _retry_on_exception_inner_2
410+
411+ return _retry_on_exception_inner_1
412
413=== added file 'hooks/charmhelpers/core/files.py'
414--- hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
415+++ hooks/charmhelpers/core/files.py 2017-05-11 14:56:57 +0000
416@@ -0,0 +1,43 @@
417+#!/usr/bin/env python
418+# -*- coding: utf-8 -*-
419+
420+# Copyright 2014-2015 Canonical Limited.
421+#
422+# Licensed under the Apache License, Version 2.0 (the "License");
423+# you may not use this file except in compliance with the License.
424+# You may obtain a copy of the License at
425+#
426+# http://www.apache.org/licenses/LICENSE-2.0
427+#
428+# Unless required by applicable law or agreed to in writing, software
429+# distributed under the License is distributed on an "AS IS" BASIS,
430+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
431+# See the License for the specific language governing permissions and
432+# limitations under the License.
433+
434+__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
435+
436+import os
437+import subprocess
438+
439+
440+def sed(filename, before, after, flags='g'):
441+ """
442+ Search and replaces the given pattern on filename.
443+
444+ :param filename: relative or absolute file path.
445+ :param before: expression to be replaced (see 'man sed')
446+ :param after: expression to replace with (see 'man sed')
447+ :param flags: sed-compatible regex flags in example, to make
448+ the search and replace case insensitive, specify ``flags="i"``.
449+ The ``g`` flag is always specified regardless, so you do not
450+ need to remember to include it when overriding this parameter.
451+ :returns: If the sed command exit code was zero then return,
452+ otherwise raise CalledProcessError.
453+ """
454+ expression = r's/{0}/{1}/{2}'.format(before,
455+ after, flags)
456+
457+ return subprocess.check_call(["sed", "-i", "-r", "-e",
458+ expression,
459+ os.path.expanduser(filename)])
460
461=== modified file 'hooks/charmhelpers/core/fstab.py'
462--- hooks/charmhelpers/core/fstab.py 2014-10-24 16:35:00 +0000
463+++ hooks/charmhelpers/core/fstab.py 2017-05-11 14:56:57 +0000
464@@ -1,12 +1,27 @@
465 #!/usr/bin/env python
466 # -*- coding: utf-8 -*-
467
468+# Copyright 2014-2015 Canonical Limited.
469+#
470+# Licensed under the Apache License, Version 2.0 (the "License");
471+# you may not use this file except in compliance with the License.
472+# You may obtain a copy of the License at
473+#
474+# http://www.apache.org/licenses/LICENSE-2.0
475+#
476+# Unless required by applicable law or agreed to in writing, software
477+# distributed under the License is distributed on an "AS IS" BASIS,
478+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
479+# See the License for the specific language governing permissions and
480+# limitations under the License.
481+
482+import io
483+import os
484+
485 __author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
486
487-import os
488-
489-
490-class Fstab(file):
491+
492+class Fstab(io.FileIO):
493 """This class extends file in order to implement a file reader/writer
494 for file `/etc/fstab`
495 """
496@@ -24,8 +39,8 @@
497 options = "defaults"
498
499 self.options = options
500- self.d = d
501- self.p = p
502+ self.d = int(d)
503+ self.p = int(p)
504
505 def __eq__(self, o):
506 return str(self) == str(o)
507@@ -45,7 +60,7 @@
508 self._path = path
509 else:
510 self._path = self.DEFAULT_PATH
511- file.__init__(self, self._path, 'r+')
512+ super(Fstab, self).__init__(self._path, 'rb+')
513
514 def _hydrate_entry(self, line):
515 # NOTE: use split with no arguments to split on any
516@@ -58,8 +73,9 @@
517 def entries(self):
518 self.seek(0)
519 for line in self.readlines():
520+ line = line.decode('us-ascii')
521 try:
522- if not line.startswith("#"):
523+ if line.strip() and not line.strip().startswith("#"):
524 yield self._hydrate_entry(line)
525 except ValueError:
526 pass
527@@ -75,18 +91,18 @@
528 if self.get_entry_by_attr('device', entry.device):
529 return False
530
531- self.write(str(entry) + '\n')
532+ self.write((str(entry) + '\n').encode('us-ascii'))
533 self.truncate()
534 return entry
535
536 def remove_entry(self, entry):
537 self.seek(0)
538
539- lines = self.readlines()
540+ lines = [l.decode('us-ascii') for l in self.readlines()]
541
542 found = False
543 for index, line in enumerate(lines):
544- if not line.startswith("#"):
545+ if line.strip() and not line.strip().startswith("#"):
546 if self._hydrate_entry(line) == entry:
547 found = True
548 break
549@@ -97,7 +113,7 @@
550 lines.remove(line)
551
552 self.seek(0)
553- self.write(''.join(lines))
554+ self.write(''.join(lines).encode('us-ascii'))
555 self.truncate()
556 return True
557
558
559=== modified file 'hooks/charmhelpers/core/hookenv.py'
560--- hooks/charmhelpers/core/hookenv.py 2014-10-24 16:35:00 +0000
561+++ hooks/charmhelpers/core/hookenv.py 2017-05-11 14:56:57 +0000
562@@ -1,17 +1,43 @@
563+# Copyright 2014-2015 Canonical Limited.
564+#
565+# Licensed under the Apache License, Version 2.0 (the "License");
566+# you may not use this file except in compliance with the License.
567+# You may obtain a copy of the License at
568+#
569+# http://www.apache.org/licenses/LICENSE-2.0
570+#
571+# Unless required by applicable law or agreed to in writing, software
572+# distributed under the License is distributed on an "AS IS" BASIS,
573+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
574+# See the License for the specific language governing permissions and
575+# limitations under the License.
576+
577 "Interactions with the Juju environment"
578 # Copyright 2013 Canonical Ltd.
579 #
580 # Authors:
581 # Charm Helpers Developers <juju@lists.ubuntu.com>
582
583+from __future__ import print_function
584+import copy
585+from distutils.version import LooseVersion
586+from functools import wraps
587+import glob
588 import os
589 import json
590 import yaml
591 import subprocess
592 import sys
593-import UserDict
594+import errno
595+import tempfile
596 from subprocess import CalledProcessError
597
598+import six
599+if not six.PY3:
600+ from UserDict import UserDict
601+else:
602+ from collections import UserDict
603+
604 CRITICAL = "CRITICAL"
605 ERROR = "ERROR"
606 WARNING = "WARNING"
607@@ -35,15 +61,18 @@
608
609 will cache the result of unit_get + 'test' for future calls.
610 """
611+ @wraps(func)
612 def wrapper(*args, **kwargs):
613 global cache
614 key = str((func, args, kwargs))
615 try:
616 return cache[key]
617 except KeyError:
618- res = func(*args, **kwargs)
619- cache[key] = res
620- return res
621+ pass # Drop out of the exception handler scope.
622+ res = func(*args, **kwargs)
623+ cache[key] = res
624+ return res
625+ wrapper._wrapped = func
626 return wrapper
627
628
629@@ -63,16 +92,29 @@
630 command = ['juju-log']
631 if level:
632 command += ['-l', level]
633+ if not isinstance(message, six.string_types):
634+ message = repr(message)
635 command += [message]
636- subprocess.call(command)
637-
638-
639-class Serializable(UserDict.IterableUserDict):
640+ # Missing juju-log should not cause failures in unit tests
641+ # Send log output to stderr
642+ try:
643+ subprocess.call(command)
644+ except OSError as e:
645+ if e.errno == errno.ENOENT:
646+ if level:
647+ message = "{}: {}".format(level, message)
648+ message = "juju-log: {}".format(message)
649+ print(message, file=sys.stderr)
650+ else:
651+ raise
652+
653+
654+class Serializable(UserDict):
655 """Wrapper, an object that can be serialized to yaml or json"""
656
657 def __init__(self, obj):
658 # wrap the object
659- UserDict.IterableUserDict.__init__(self)
660+ UserDict.__init__(self)
661 self.data = obj
662
663 def __getattr__(self, attr):
664@@ -130,9 +172,19 @@
665 return os.environ.get('JUJU_RELATION', None)
666
667
668-def relation_id():
669- """The relation ID for the current relation hook"""
670- return os.environ.get('JUJU_RELATION_ID', None)
671+@cached
672+def relation_id(relation_name=None, service_or_unit=None):
673+ """The relation ID for the current or a specified relation"""
674+ if not relation_name and not service_or_unit:
675+ return os.environ.get('JUJU_RELATION_ID', None)
676+ elif relation_name and service_or_unit:
677+ service_name = service_or_unit.split('/')[0]
678+ for relid in relation_ids(relation_name):
679+ remote_service = remote_service_name(relid)
680+ if remote_service == service_name:
681+ return relid
682+ else:
683+ raise ValueError('Must specify neither or both of relation_name and service_or_unit')
684
685
686 def local_unit():
687@@ -142,7 +194,7 @@
688
689 def remote_unit():
690 """The remote unit for the current relation hook"""
691- return os.environ['JUJU_REMOTE_UNIT']
692+ return os.environ.get('JUJU_REMOTE_UNIT', None)
693
694
695 def service_name():
696@@ -150,9 +202,20 @@
697 return local_unit().split('/')[0]
698
699
700+@cached
701+def remote_service_name(relid=None):
702+ """The remote service name for a given relation-id (or the current relation)"""
703+ if relid is None:
704+ unit = remote_unit()
705+ else:
706+ units = related_units(relid)
707+ unit = units[0] if units else None
708+ return unit.split('/')[0] if unit else None
709+
710+
711 def hook_name():
712 """The name of the currently executing hook"""
713- return os.path.basename(sys.argv[0])
714+ return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
715
716
717 class Config(dict):
718@@ -202,23 +265,7 @@
719 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
720 if os.path.exists(self.path):
721 self.load_previous()
722-
723- def __getitem__(self, key):
724- """For regular dict lookups, check the current juju config first,
725- then the previous (saved) copy. This ensures that user-saved values
726- will be returned by a dict lookup.
727-
728- """
729- try:
730- return dict.__getitem__(self, key)
731- except KeyError:
732- return (self._prev_dict or {})[key]
733-
734- def keys(self):
735- prev_keys = []
736- if self._prev_dict is not None:
737- prev_keys = self._prev_dict.keys()
738- return list(set(prev_keys + dict.keys(self)))
739+ atexit(self._implicit_save)
740
741 def load_previous(self, path=None):
742 """Load previous copy of config from disk.
743@@ -237,6 +284,9 @@
744 self.path = path or self.path
745 with open(self.path) as f:
746 self._prev_dict = json.load(f)
747+ for k, v in copy.deepcopy(self._prev_dict).items():
748+ if k not in self:
749+ self[k] = v
750
751 def changed(self, key):
752 """Return True if the current value for this key is different from
753@@ -268,13 +318,13 @@
754 instance.
755
756 """
757- if self._prev_dict:
758- for k, v in self._prev_dict.iteritems():
759- if k not in self:
760- self[k] = v
761 with open(self.path, 'w') as f:
762 json.dump(self, f)
763
764+ def _implicit_save(self):
765+ if self.implicit_save:
766+ self.save()
767+
768
769 @cached
770 def config(scope=None):
771@@ -282,9 +332,12 @@
772 config_cmd_line = ['config-get']
773 if scope is not None:
774 config_cmd_line.append(scope)
775+ else:
776+ config_cmd_line.append('--all')
777 config_cmd_line.append('--format=json')
778 try:
779- config_data = json.loads(subprocess.check_output(config_cmd_line))
780+ config_data = json.loads(
781+ subprocess.check_output(config_cmd_line).decode('UTF-8'))
782 if scope is not None:
783 return config_data
784 return Config(config_data)
785@@ -303,10 +356,10 @@
786 if unit:
787 _args.append(unit)
788 try:
789- return json.loads(subprocess.check_output(_args))
790+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
791 except ValueError:
792 return None
793- except CalledProcessError, e:
794+ except CalledProcessError as e:
795 if e.returncode == 2:
796 return None
797 raise
798@@ -316,18 +369,49 @@
799 """Set relation information for the current unit"""
800 relation_settings = relation_settings if relation_settings else {}
801 relation_cmd_line = ['relation-set']
802+ accepts_file = "--file" in subprocess.check_output(
803+ relation_cmd_line + ["--help"], universal_newlines=True)
804 if relation_id is not None:
805 relation_cmd_line.extend(('-r', relation_id))
806- for k, v in (relation_settings.items() + kwargs.items()):
807- if v is None:
808- relation_cmd_line.append('{}='.format(k))
809- else:
810- relation_cmd_line.append('{}={}'.format(k, v))
811- subprocess.check_call(relation_cmd_line)
812+ settings = relation_settings.copy()
813+ settings.update(kwargs)
814+ for key, value in settings.items():
815+ # Force value to be a string: it always should, but some call
816+ # sites pass in things like dicts or numbers.
817+ if value is not None:
818+ settings[key] = "{}".format(value)
819+ if accepts_file:
820+ # --file was introduced in Juju 1.23.2. Use it by default if
821+ # available, since otherwise we'll break if the relation data is
822+ # too big. Ideally we should tell relation-set to read the data from
823+ # stdin, but that feature is broken in 1.23.2: Bug #1454678.
824+ with tempfile.NamedTemporaryFile(delete=False) as settings_file:
825+ settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
826+ subprocess.check_call(
827+ relation_cmd_line + ["--file", settings_file.name])
828+ os.remove(settings_file.name)
829+ else:
830+ for key, value in settings.items():
831+ if value is None:
832+ relation_cmd_line.append('{}='.format(key))
833+ else:
834+ relation_cmd_line.append('{}={}'.format(key, value))
835+ subprocess.check_call(relation_cmd_line)
836 # Flush cache of any relation-gets for local unit
837 flush(local_unit())
838
839
840+def relation_clear(r_id=None):
841+ ''' Clears any relation data already set on relation r_id '''
842+ settings = relation_get(rid=r_id,
843+ unit=local_unit())
844+ for setting in settings:
845+ if setting not in ['public-address', 'private-address']:
846+ settings[setting] = None
847+ relation_set(relation_id=r_id,
848+ **settings)
849+
850+
851 @cached
852 def relation_ids(reltype=None):
853 """A list of relation_ids"""
854@@ -335,7 +419,8 @@
855 relid_cmd_line = ['relation-ids', '--format=json']
856 if reltype is not None:
857 relid_cmd_line.append(reltype)
858- return json.loads(subprocess.check_output(relid_cmd_line)) or []
859+ return json.loads(
860+ subprocess.check_output(relid_cmd_line).decode('UTF-8')) or []
861 return []
862
863
864@@ -346,7 +431,8 @@
865 units_cmd_line = ['relation-list', '--format=json']
866 if relid is not None:
867 units_cmd_line.extend(('-r', relid))
868- return json.loads(subprocess.check_output(units_cmd_line)) or []
869+ return json.loads(
870+ subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
871
872
873 @cached
874@@ -386,21 +472,101 @@
875
876
877 @cached
878+def metadata():
879+ """Get the current charm metadata.yaml contents as a python object"""
880+ with open(os.path.join(charm_dir(), 'metadata.yaml')) as md:
881+ return yaml.safe_load(md)
882+
883+
884+@cached
885 def relation_types():
886 """Get a list of relation types supported by this charm"""
887- charmdir = os.environ.get('CHARM_DIR', '')
888- mdf = open(os.path.join(charmdir, 'metadata.yaml'))
889- md = yaml.safe_load(mdf)
890 rel_types = []
891+ md = metadata()
892 for key in ('provides', 'requires', 'peers'):
893 section = md.get(key)
894 if section:
895 rel_types.extend(section.keys())
896- mdf.close()
897 return rel_types
898
899
900 @cached
901+def peer_relation_id():
902+ '''Get the peers relation id if a peers relation has been joined, else None.'''
903+ md = metadata()
904+ section = md.get('peers')
905+ if section:
906+ for key in section:
907+ relids = relation_ids(key)
908+ if relids:
909+ return relids[0]
910+ return None
911+
912+
913+@cached
914+def relation_to_interface(relation_name):
915+ """
916+ Given the name of a relation, return the interface that relation uses.
917+
918+ :returns: The interface name, or ``None``.
919+ """
920+ return relation_to_role_and_interface(relation_name)[1]
921+
922+
923+@cached
924+def relation_to_role_and_interface(relation_name):
925+ """
926+ Given the name of a relation, return the role and the name of the interface
927+ that relation uses (where role is one of ``provides``, ``requires``, or ``peers``).
928+
929+ :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
930+ """
931+ _metadata = metadata()
932+ for role in ('provides', 'requires', 'peers'):
933+ interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
934+ if interface:
935+ return role, interface
936+ return None, None
937+
938+
939+@cached
940+def role_and_interface_to_relations(role, interface_name):
941+ """
942+ Given a role and interface name, return a list of relation names for the
943+ current charm that use that interface under that role (where role is one
944+ of ``provides``, ``requires``, or ``peers``).
945+
946+ :returns: A list of relation names.
947+ """
948+ _metadata = metadata()
949+ results = []
950+ for relation_name, relation in _metadata.get(role, {}).items():
951+ if relation['interface'] == interface_name:
952+ results.append(relation_name)
953+ return results
954+
955+
956+@cached
957+def interface_to_relations(interface_name):
958+ """
959+ Given an interface, return a list of relation names for the current
960+ charm that use that interface.
961+
962+ :returns: A list of relation names.
963+ """
964+ results = []
965+ for role in ('provides', 'requires', 'peers'):
966+ results.extend(role_and_interface_to_relations(role, interface_name))
967+ return results
968+
969+
970+@cached
971+def charm_name():
972+ """Get the name of the current charm as is specified on metadata.yaml"""
973+ return metadata().get('name')
974+
975+
976+@cached
977 def relations():
978 """Get a nested dictionary of relation data for all related units"""
979 rels = {}
980@@ -450,21 +616,72 @@
981 subprocess.check_call(_args)
982
983
984+def open_ports(start, end, protocol="TCP"):
985+ """Opens a range of service network ports"""
986+ _args = ['open-port']
987+ _args.append('{}-{}/{}'.format(start, end, protocol))
988+ subprocess.check_call(_args)
989+
990+
991+def close_ports(start, end, protocol="TCP"):
992+ """Close a range of service network ports"""
993+ _args = ['close-port']
994+ _args.append('{}-{}/{}'.format(start, end, protocol))
995+ subprocess.check_call(_args)
996+
997+
998 @cached
999 def unit_get(attribute):
1000 """Get the unit ID for the remote unit"""
1001 _args = ['unit-get', '--format=json', attribute]
1002 try:
1003- return json.loads(subprocess.check_output(_args))
1004+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
1005 except ValueError:
1006 return None
1007
1008
1009+def unit_public_ip():
1010+ """Get this unit's public IP address"""
1011+ return unit_get('public-address')
1012+
1013+
1014 def unit_private_ip():
1015 """Get this unit's private IP address"""
1016 return unit_get('private-address')
1017
1018
1019+@cached
1020+def storage_get(attribute=None, storage_id=None):
1021+ """Get storage attributes"""
1022+ _args = ['storage-get', '--format=json']
1023+ if storage_id:
1024+ _args.extend(('-s', storage_id))
1025+ if attribute:
1026+ _args.append(attribute)
1027+ try:
1028+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
1029+ except ValueError:
1030+ return None
1031+
1032+
1033+@cached
1034+def storage_list(storage_name=None):
1035+ """List the storage IDs for the unit"""
1036+ _args = ['storage-list', '--format=json']
1037+ if storage_name:
1038+ _args.append(storage_name)
1039+ try:
1040+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
1041+ except ValueError:
1042+ return None
1043+ except OSError as e:
1044+ import errno
1045+ if e.errno == errno.ENOENT:
1046+ # storage-list does not exist
1047+ return []
1048+ raise
1049+
1050+
1051 class UnregisteredHookError(Exception):
1052 """Raised when an undefined hook is called"""
1053 pass
1054@@ -492,10 +709,14 @@
1055 hooks.execute(sys.argv)
1056 """
1057
1058- def __init__(self, config_save=True):
1059+ def __init__(self, config_save=None):
1060 super(Hooks, self).__init__()
1061 self._hooks = {}
1062- self._config_save = config_save
1063+
1064+ # For unknown reasons, we allow the Hooks constructor to override
1065+ # config().implicit_save.
1066+ if config_save is not None:
1067+ config().implicit_save = config_save
1068
1069 def register(self, name, function):
1070 """Register a hook"""
1071@@ -503,13 +724,16 @@
1072
1073 def execute(self, args):
1074 """Execute a registered hook based on args[0]"""
1075+ _run_atstart()
1076 hook_name = os.path.basename(args[0])
1077 if hook_name in self._hooks:
1078- self._hooks[hook_name]()
1079- if self._config_save:
1080- cfg = config()
1081- if cfg.implicit_save:
1082- cfg.save()
1083+ try:
1084+ self._hooks[hook_name]()
1085+ except SystemExit as x:
1086+ if x.code is None or x.code == 0:
1087+ _run_atexit()
1088+ raise
1089+ _run_atexit()
1090 else:
1091 raise UnregisteredHookError(hook_name)
1092
1093@@ -530,3 +754,315 @@
1094 def charm_dir():
1095 """Return the root directory of the current charm"""
1096 return os.environ.get('CHARM_DIR')
1097+
1098+
1099+@cached
1100+def action_get(key=None):
1101+ """Gets the value of an action parameter, or all key/value param pairs"""
1102+ cmd = ['action-get']
1103+ if key is not None:
1104+ cmd.append(key)
1105+ cmd.append('--format=json')
1106+ action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
1107+ return action_data
1108+
1109+
1110+def action_set(values):
1111+ """Sets the values to be returned after the action finishes"""
1112+ cmd = ['action-set']
1113+ for k, v in list(values.items()):
1114+ cmd.append('{}={}'.format(k, v))
1115+ subprocess.check_call(cmd)
1116+
1117+
1118+def action_fail(message):
1119+ """Sets the action status to failed and sets the error message.
1120+
1121+ The results set by action_set are preserved."""
1122+ subprocess.check_call(['action-fail', message])
1123+
1124+
1125+def action_name():
1126+ """Get the name of the currently executing action."""
1127+ return os.environ.get('JUJU_ACTION_NAME')
1128+
1129+
1130+def action_uuid():
1131+ """Get the UUID of the currently executing action."""
1132+ return os.environ.get('JUJU_ACTION_UUID')
1133+
1134+
1135+def action_tag():
1136+ """Get the tag for the currently executing action."""
1137+ return os.environ.get('JUJU_ACTION_TAG')
1138+
1139+
1140+def status_set(workload_state, message):
1141+ """Set the workload state with a message
1142+
1143+ Use status-set to set the workload state with a message which is visible
1144+ to the user via juju status. If the status-set command is not found then
1145+ assume this is juju < 1.23 and juju-log the message unstead.
1146+
1147+ workload_state -- valid juju workload state.
1148+ message -- status update message
1149+ """
1150+ valid_states = ['maintenance', 'blocked', 'waiting', 'active']
1151+ if workload_state not in valid_states:
1152+ raise ValueError(
1153+ '{!r} is not a valid workload state'.format(workload_state)
1154+ )
1155+ cmd = ['status-set', workload_state, message]
1156+ try:
1157+ ret = subprocess.call(cmd)
1158+ if ret == 0:
1159+ return
1160+ except OSError as e:
1161+ if e.errno != errno.ENOENT:
1162+ raise
1163+ log_message = 'status-set failed: {} {}'.format(workload_state,
1164+ message)
1165+ log(log_message, level='INFO')
1166+
1167+
1168+def status_get():
1169+ """Retrieve the previously set juju workload state and message
1170+
1171+ If the status-get command is not found then assume this is juju < 1.23 and
1172+ return 'unknown', ""
1173+
1174+ """
1175+ cmd = ['status-get', "--format=json", "--include-data"]
1176+ try:
1177+ raw_status = subprocess.check_output(cmd)
1178+ except OSError as e:
1179+ if e.errno == errno.ENOENT:
1180+ return ('unknown', "")
1181+ else:
1182+ raise
1183+ else:
1184+ status = json.loads(raw_status.decode("UTF-8"))
1185+ return (status["status"], status["message"])
1186+
1187+
1188+def translate_exc(from_exc, to_exc):
1189+ def inner_translate_exc1(f):
1190+ @wraps(f)
1191+ def inner_translate_exc2(*args, **kwargs):
1192+ try:
1193+ return f(*args, **kwargs)
1194+ except from_exc:
1195+ raise to_exc
1196+
1197+ return inner_translate_exc2
1198+
1199+ return inner_translate_exc1
1200+
1201+
1202+def application_version_set(version):
1203+ """Charm authors may trigger this command from any hook to output what
1204+ version of the application is running. This could be a package version,
1205+ for instance postgres version 9.5. It could also be a build number or
1206+ version control revision identifier, for instance git sha 6fb7ba68. """
1207+
1208+ cmd = ['application-version-set']
1209+ cmd.append(version)
1210+ try:
1211+ subprocess.check_call(cmd)
1212+ except OSError:
1213+ log("Application Version: {}".format(version))
1214+
1215+
1216+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1217+def is_leader():
1218+ """Does the current unit hold the juju leadership
1219+
1220+ Uses juju to determine whether the current unit is the leader of its peers
1221+ """
1222+ cmd = ['is-leader', '--format=json']
1223+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
1224+
1225+
1226+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1227+def leader_get(attribute=None):
1228+ """Juju leader get value(s)"""
1229+ cmd = ['leader-get', '--format=json'] + [attribute or '-']
1230+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
1231+
1232+
1233+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1234+def leader_set(settings=None, **kwargs):
1235+ """Juju leader set value(s)"""
1236+ # Don't log secrets.
1237+ # log("Juju leader-set '%s'" % (settings), level=DEBUG)
1238+ cmd = ['leader-set']
1239+ settings = settings or {}
1240+ settings.update(kwargs)
1241+ for k, v in settings.items():
1242+ if v is None:
1243+ cmd.append('{}='.format(k))
1244+ else:
1245+ cmd.append('{}={}'.format(k, v))
1246+ subprocess.check_call(cmd)
1247+
1248+
1249+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1250+def payload_register(ptype, klass, pid):
1251+ """ is used while a hook is running to let Juju know that a
1252+ payload has been started."""
1253+ cmd = ['payload-register']
1254+ for x in [ptype, klass, pid]:
1255+ cmd.append(x)
1256+ subprocess.check_call(cmd)
1257+
1258+
1259+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1260+def payload_unregister(klass, pid):
1261+ """ is used while a hook is running to let Juju know
1262+ that a payload has been manually stopped. The <class> and <id> provided
1263+ must match a payload that has been previously registered with juju using
1264+ payload-register."""
1265+ cmd = ['payload-unregister']
1266+ for x in [klass, pid]:
1267+ cmd.append(x)
1268+ subprocess.check_call(cmd)
1269+
1270+
1271+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1272+def payload_status_set(klass, pid, status):
1273+ """is used to update the current status of a registered payload.
1274+ The <class> and <id> provided must match a payload that has been previously
1275+ registered with juju using payload-register. The <status> must be one of the
1276+ follow: starting, started, stopping, stopped"""
1277+ cmd = ['payload-status-set']
1278+ for x in [klass, pid, status]:
1279+ cmd.append(x)
1280+ subprocess.check_call(cmd)
1281+
1282+
1283+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1284+def resource_get(name):
1285+ """used to fetch the resource path of the given name.
1286+
1287+ <name> must match a name of defined resource in metadata.yaml
1288+
1289+ returns either a path or False if resource not available
1290+ """
1291+ if not name:
1292+ return False
1293+
1294+ cmd = ['resource-get', name]
1295+ try:
1296+ return subprocess.check_output(cmd).decode('UTF-8')
1297+ except subprocess.CalledProcessError:
1298+ return False
1299+
1300+
1301+@cached
1302+def juju_version():
1303+ """Full version string (eg. '1.23.3.1-trusty-amd64')"""
1304+ # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
1305+ jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
1306+ return subprocess.check_output([jujud, 'version'],
1307+ universal_newlines=True).strip()
1308+
1309+
1310+@cached
1311+def has_juju_version(minimum_version):
1312+ """Return True if the Juju version is at least the provided version"""
1313+ return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
1314+
1315+
1316+_atexit = []
1317+_atstart = []
1318+
1319+
1320+def atstart(callback, *args, **kwargs):
1321+ '''Schedule a callback to run before the main hook.
1322+
1323+ Callbacks are run in the order they were added.
1324+
1325+ This is useful for modules and classes to perform initialization
1326+ and inject behavior. In particular:
1327+
1328+ - Run common code before all of your hooks, such as logging
1329+ the hook name or interesting relation data.
1330+ - Defer object or module initialization that requires a hook
1331+ context until we know there actually is a hook context,
1332+ making testing easier.
1333+ - Rather than requiring charm authors to include boilerplate to
1334+ invoke your helper's behavior, have it run automatically if
1335+ your object is instantiated or module imported.
1336+
1337+ This is not at all useful after your hook framework as been launched.
1338+ '''
1339+ global _atstart
1340+ _atstart.append((callback, args, kwargs))
1341+
1342+
1343+def atexit(callback, *args, **kwargs):
1344+ '''Schedule a callback to run on successful hook completion.
1345+
1346+ Callbacks are run in the reverse order that they were added.'''
1347+ _atexit.append((callback, args, kwargs))
1348+
1349+
1350+def _run_atstart():
1351+ '''Hook frameworks must invoke this before running the main hook body.'''
1352+ global _atstart
1353+ for callback, args, kwargs in _atstart:
1354+ callback(*args, **kwargs)
1355+ del _atstart[:]
1356+
1357+
1358+def _run_atexit():
1359+ '''Hook frameworks must invoke this after the main hook body has
1360+ successfully completed. Do not invoke it if the hook fails.'''
1361+ global _atexit
1362+ for callback, args, kwargs in reversed(_atexit):
1363+ callback(*args, **kwargs)
1364+ del _atexit[:]
1365+
1366+
1367+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1368+def network_get_primary_address(binding):
1369+ '''
1370+ Retrieve the primary network address for a named binding
1371+
1372+ :param binding: string. The name of a relation of extra-binding
1373+ :return: string. The primary IP address for the named binding
1374+ :raise: NotImplementedError if run on Juju < 2.0
1375+ '''
1376+ cmd = ['network-get', '--primary-address', binding]
1377+ return subprocess.check_output(cmd).decode('UTF-8').strip()
1378+
1379+
1380+def add_metric(*args, **kwargs):
1381+ """Add metric values. Values may be expressed with keyword arguments. For
1382+ metric names containing dashes, these may be expressed as one or more
1383+ 'key=value' positional arguments. May only be called from the collect-metrics
1384+ hook."""
1385+ _args = ['add-metric']
1386+ _kvpairs = []
1387+ _kvpairs.extend(args)
1388+ _kvpairs.extend(['{}={}'.format(k, v) for k, v in kwargs.items()])
1389+ _args.extend(sorted(_kvpairs))
1390+ try:
1391+ subprocess.check_call(_args)
1392+ return
1393+ except EnvironmentError as e:
1394+ if e.errno != errno.ENOENT:
1395+ raise
1396+ log_message = 'add-metric failed: {}'.format(' '.join(_kvpairs))
1397+ log(log_message, level='INFO')
1398+
1399+
1400+def meter_status():
1401+ """Get the meter status, if running in the meter-status-changed hook."""
1402+ return os.environ.get('JUJU_METER_STATUS')
1403+
1404+
1405+def meter_info():
1406+ """Get the meter status information, if running in the meter-status-changed
1407+ hook."""
1408+ return os.environ.get('JUJU_METER_INFO')
1409
1410=== modified file 'hooks/charmhelpers/core/host.py'
1411--- hooks/charmhelpers/core/host.py 2014-10-24 16:35:00 +0000
1412+++ hooks/charmhelpers/core/host.py 2017-05-11 14:56:57 +0000
1413@@ -1,3 +1,17 @@
1414+# Copyright 2014-2015 Canonical Limited.
1415+#
1416+# Licensed under the Apache License, Version 2.0 (the "License");
1417+# you may not use this file except in compliance with the License.
1418+# You may obtain a copy of the License at
1419+#
1420+# http://www.apache.org/licenses/LICENSE-2.0
1421+#
1422+# Unless required by applicable law or agreed to in writing, software
1423+# distributed under the License is distributed on an "AS IS" BASIS,
1424+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1425+# See the License for the specific language governing permissions and
1426+# limitations under the License.
1427+
1428 """Tools for working with the host system"""
1429 # Copyright 2012 Canonical Ltd.
1430 #
1431@@ -8,80 +22,328 @@
1432 import os
1433 import re
1434 import pwd
1435+import glob
1436 import grp
1437 import random
1438 import string
1439 import subprocess
1440 import hashlib
1441+import functools
1442+import itertools
1443+import six
1444+
1445 from contextlib import contextmanager
1446-
1447 from collections import OrderedDict
1448-
1449-from hookenv import log
1450-from fstab import Fstab
1451-
1452-
1453-def service_start(service_name):
1454- """Start a system service"""
1455- return service('start', service_name)
1456-
1457-
1458-def service_stop(service_name):
1459- """Stop a system service"""
1460- return service('stop', service_name)
1461-
1462-
1463-def service_restart(service_name):
1464- """Restart a system service"""
1465+from .hookenv import log
1466+from .fstab import Fstab
1467+from charmhelpers.osplatform import get_platform
1468+
1469+__platform__ = get_platform()
1470+if __platform__ == "ubuntu":
1471+ from charmhelpers.core.host_factory.ubuntu import (
1472+ service_available,
1473+ add_new_group,
1474+ lsb_release,
1475+ cmp_pkgrevno,
1476+ CompareHostReleases,
1477+ ) # flake8: noqa -- ignore F401 for this import
1478+elif __platform__ == "centos":
1479+ from charmhelpers.core.host_factory.centos import (
1480+ service_available,
1481+ add_new_group,
1482+ lsb_release,
1483+ cmp_pkgrevno,
1484+ CompareHostReleases,
1485+ ) # flake8: noqa -- ignore F401 for this import
1486+
1487+UPDATEDB_PATH = '/etc/updatedb.conf'
1488+
1489+def service_start(service_name, **kwargs):
1490+ """Start a system service.
1491+
1492+ The specified service name is managed via the system level init system.
1493+ Some init systems (e.g. upstart) require that additional arguments be
1494+ provided in order to directly control service instances whereas other init
1495+ systems allow for addressing instances of a service directly by name (e.g.
1496+ systemd).
1497+
1498+ The kwargs allow for the additional parameters to be passed to underlying
1499+ init systems for those systems which require/allow for them. For example,
1500+ the ceph-osd upstart script requires the id parameter to be passed along
1501+ in order to identify which running daemon should be reloaded. The follow-
1502+ ing example stops the ceph-osd service for instance id=4:
1503+
1504+ service_stop('ceph-osd', id=4)
1505+
1506+ :param service_name: the name of the service to stop
1507+ :param **kwargs: additional parameters to pass to the init system when
1508+ managing services. These will be passed as key=value
1509+ parameters to the init system's commandline. kwargs
1510+ are ignored for systemd enabled systems.
1511+ """
1512+ return service('start', service_name, **kwargs)
1513+
1514+
1515+def service_stop(service_name, **kwargs):
1516+ """Stop a system service.
1517+
1518+ The specified service name is managed via the system level init system.
1519+ Some init systems (e.g. upstart) require that additional arguments be
1520+ provided in order to directly control service instances whereas other init
1521+ systems allow for addressing instances of a service directly by name (e.g.
1522+ systemd).
1523+
1524+ The kwargs allow for the additional parameters to be passed to underlying
1525+ init systems for those systems which require/allow for them. For example,
1526+ the ceph-osd upstart script requires the id parameter to be passed along
1527+ in order to identify which running daemon should be reloaded. The follow-
1528+ ing example stops the ceph-osd service for instance id=4:
1529+
1530+ service_stop('ceph-osd', id=4)
1531+
1532+ :param service_name: the name of the service to stop
1533+ :param **kwargs: additional parameters to pass to the init system when
1534+ managing services. These will be passed as key=value
1535+ parameters to the init system's commandline. kwargs
1536+ are ignored for systemd enabled systems.
1537+ """
1538+ return service('stop', service_name, **kwargs)
1539+
1540+
1541+def service_restart(service_name, **kwargs):
1542+ """Restart a system service.
1543+
1544+ The specified service name is managed via the system level init system.
1545+ Some init systems (e.g. upstart) require that additional arguments be
1546+ provided in order to directly control service instances whereas other init
1547+ systems allow for addressing instances of a service directly by name (e.g.
1548+ systemd).
1549+
1550+ The kwargs allow for the additional parameters to be passed to underlying
1551+ init systems for those systems which require/allow for them. For example,
1552+ the ceph-osd upstart script requires the id parameter to be passed along
1553+ in order to identify which running daemon should be restarted. The follow-
1554+ ing example restarts the ceph-osd service for instance id=4:
1555+
1556+ service_restart('ceph-osd', id=4)
1557+
1558+ :param service_name: the name of the service to restart
1559+ :param **kwargs: additional parameters to pass to the init system when
1560+ managing services. These will be passed as key=value
1561+ parameters to the init system's commandline. kwargs
1562+ are ignored for init systems not allowing additional
1563+ parameters via the commandline (systemd).
1564+ """
1565 return service('restart', service_name)
1566
1567
1568-def service_reload(service_name, restart_on_failure=False):
1569+def service_reload(service_name, restart_on_failure=False, **kwargs):
1570 """Reload a system service, optionally falling back to restart if
1571- reload fails"""
1572- service_result = service('reload', service_name)
1573+ reload fails.
1574+
1575+ The specified service name is managed via the system level init system.
1576+ Some init systems (e.g. upstart) require that additional arguments be
1577+ provided in order to directly control service instances whereas other init
1578+ systems allow for addressing instances of a service directly by name (e.g.
1579+ systemd).
1580+
1581+ The kwargs allow for the additional parameters to be passed to underlying
1582+ init systems for those systems which require/allow for them. For example,
1583+ the ceph-osd upstart script requires the id parameter to be passed along
1584+ in order to identify which running daemon should be reloaded. The follow-
1585+ ing example restarts the ceph-osd service for instance id=4:
1586+
1587+ service_reload('ceph-osd', id=4)
1588+
1589+ :param service_name: the name of the service to reload
1590+ :param restart_on_failure: boolean indicating whether to fallback to a
1591+ restart if the reload fails.
1592+ :param **kwargs: additional parameters to pass to the init system when
1593+ managing services. These will be passed as key=value
1594+ parameters to the init system's commandline. kwargs
1595+ are ignored for init systems not allowing additional
1596+ parameters via the commandline (systemd).
1597+ """
1598+ service_result = service('reload', service_name, **kwargs)
1599 if not service_result and restart_on_failure:
1600- service_result = service('restart', service_name)
1601+ service_result = service('restart', service_name, **kwargs)
1602 return service_result
1603
1604
1605-def service(action, service_name):
1606- """Control a system service"""
1607- cmd = ['service', service_name, action]
1608+def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d",
1609+ **kwargs):
1610+ """Pause a system service.
1611+
1612+ Stop it, and prevent it from starting again at boot.
1613+
1614+ :param service_name: the name of the service to pause
1615+ :param init_dir: path to the upstart init directory
1616+ :param initd_dir: path to the sysv init directory
1617+ :param **kwargs: additional parameters to pass to the init system when
1618+ managing services. These will be passed as key=value
1619+ parameters to the init system's commandline. kwargs
1620+ are ignored for init systems which do not support
1621+ key=value arguments via the commandline.
1622+ """
1623+ stopped = True
1624+ if service_running(service_name, **kwargs):
1625+ stopped = service_stop(service_name, **kwargs)
1626+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
1627+ sysv_file = os.path.join(initd_dir, service_name)
1628+ if init_is_systemd():
1629+ service('disable', service_name)
1630+ elif os.path.exists(upstart_file):
1631+ override_path = os.path.join(
1632+ init_dir, '{}.override'.format(service_name))
1633+ with open(override_path, 'w') as fh:
1634+ fh.write("manual\n")
1635+ elif os.path.exists(sysv_file):
1636+ subprocess.check_call(["update-rc.d", service_name, "disable"])
1637+ else:
1638+ raise ValueError(
1639+ "Unable to detect {0} as SystemD, Upstart {1} or"
1640+ " SysV {2}".format(
1641+ service_name, upstart_file, sysv_file))
1642+ return stopped
1643+
1644+
1645+def service_resume(service_name, init_dir="/etc/init",
1646+ initd_dir="/etc/init.d", **kwargs):
1647+ """Resume a system service.
1648+
1649+ Reenable starting again at boot. Start the service.
1650+
1651+ :param service_name: the name of the service to resume
1652+ :param init_dir: the path to the init dir
1653+ :param initd dir: the path to the initd dir
1654+ :param **kwargs: additional parameters to pass to the init system when
1655+ managing services. These will be passed as key=value
1656+ parameters to the init system's commandline. kwargs
1657+ are ignored for systemd enabled systems.
1658+ """
1659+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
1660+ sysv_file = os.path.join(initd_dir, service_name)
1661+ if init_is_systemd():
1662+ service('enable', service_name)
1663+ elif os.path.exists(upstart_file):
1664+ override_path = os.path.join(
1665+ init_dir, '{}.override'.format(service_name))
1666+ if os.path.exists(override_path):
1667+ os.unlink(override_path)
1668+ elif os.path.exists(sysv_file):
1669+ subprocess.check_call(["update-rc.d", service_name, "enable"])
1670+ else:
1671+ raise ValueError(
1672+ "Unable to detect {0} as SystemD, Upstart {1} or"
1673+ " SysV {2}".format(
1674+ service_name, upstart_file, sysv_file))
1675+ started = service_running(service_name, **kwargs)
1676+
1677+ if not started:
1678+ started = service_start(service_name, **kwargs)
1679+ return started
1680+
1681+
1682+def service(action, service_name, **kwargs):
1683+ """Control a system service.
1684+
1685+ :param action: the action to take on the service
1686+ :param service_name: the name of the service to perform th action on
1687+ :param **kwargs: additional params to be passed to the service command in
1688+ the form of key=value.
1689+ """
1690+ if init_is_systemd():
1691+ cmd = ['systemctl', action, service_name]
1692+ else:
1693+ cmd = ['service', service_name, action]
1694+ for key, value in six.iteritems(kwargs):
1695+ parameter = '%s=%s' % (key, value)
1696+ cmd.append(parameter)
1697 return subprocess.call(cmd) == 0
1698
1699
1700-def service_running(service):
1701- """Determine whether a system service is running"""
1702- try:
1703- output = subprocess.check_output(['service', service, 'status'], stderr=subprocess.STDOUT)
1704- except subprocess.CalledProcessError:
1705- return False
1706- else:
1707- if ("start/running" in output or "is running" in output):
1708- return True
1709- else:
1710- return False
1711-
1712-
1713-def service_available(service_name):
1714- """Determine whether a system service is available"""
1715- try:
1716- subprocess.check_output(['service', service_name, 'status'], stderr=subprocess.STDOUT)
1717- except subprocess.CalledProcessError as e:
1718- return 'unrecognized service' not in e.output
1719- else:
1720- return True
1721-
1722-
1723-def adduser(username, password=None, shell='/bin/bash', system_user=False):
1724- """Add a user to the system"""
1725+_UPSTART_CONF = "/etc/init/{}.conf"
1726+_INIT_D_CONF = "/etc/init.d/{}"
1727+
1728+
1729+def service_running(service_name, **kwargs):
1730+ """Determine whether a system service is running.
1731+
1732+ :param service_name: the name of the service
1733+ :param **kwargs: additional args to pass to the service command. This is
1734+ used to pass additional key=value arguments to the
1735+ service command line for managing specific instance
1736+ units (e.g. service ceph-osd status id=2). The kwargs
1737+ are ignored in systemd services.
1738+ """
1739+ if init_is_systemd():
1740+ return service('is-active', service_name)
1741+ else:
1742+ if os.path.exists(_UPSTART_CONF.format(service_name)):
1743+ try:
1744+ cmd = ['status', service_name]
1745+ for key, value in six.iteritems(kwargs):
1746+ parameter = '%s=%s' % (key, value)
1747+ cmd.append(parameter)
1748+ output = subprocess.check_output(cmd,
1749+ stderr=subprocess.STDOUT).decode('UTF-8')
1750+ except subprocess.CalledProcessError:
1751+ return False
1752+ else:
1753+ # This works for upstart scripts where the 'service' command
1754+ # returns a consistent string to represent running
1755+ # 'start/running'
1756+ if ("start/running" in output or
1757+ "is running" in output or
1758+ "up and running" in output):
1759+ return True
1760+ elif os.path.exists(_INIT_D_CONF.format(service_name)):
1761+ # Check System V scripts init script return codes
1762+ return service('status', service_name)
1763+ return False
1764+
1765+
1766+SYSTEMD_SYSTEM = '/run/systemd/system'
1767+
1768+
1769+def init_is_systemd():
1770+ """Return True if the host system uses systemd, False otherwise."""
1771+ if lsb_release()['DISTRIB_CODENAME'] == 'trusty':
1772+ return False
1773+ return os.path.isdir(SYSTEMD_SYSTEM)
1774+
1775+
1776+def adduser(username, password=None, shell='/bin/bash',
1777+ system_user=False, primary_group=None,
1778+ secondary_groups=None, uid=None, home_dir=None):
1779+ """Add a user to the system.
1780+
1781+ Will log but otherwise succeed if the user already exists.
1782+
1783+ :param str username: Username to create
1784+ :param str password: Password for user; if ``None``, create a system user
1785+ :param str shell: The default shell for the user
1786+ :param bool system_user: Whether to create a login or system user
1787+ :param str primary_group: Primary group for user; defaults to username
1788+ :param list secondary_groups: Optional list of additional groups
1789+ :param int uid: UID for user being created
1790+ :param str home_dir: Home directory for user
1791+
1792+ :returns: The password database entry struct, as returned by `pwd.getpwnam`
1793+ """
1794 try:
1795 user_info = pwd.getpwnam(username)
1796 log('user {0} already exists!'.format(username))
1797+ if uid:
1798+ user_info = pwd.getpwuid(int(uid))
1799+ log('user with uid {0} already exists!'.format(uid))
1800 except KeyError:
1801 log('creating user {0}'.format(username))
1802 cmd = ['useradd']
1803+ if uid:
1804+ cmd.extend(['--uid', str(uid)])
1805+ if home_dir:
1806+ cmd.extend(['--home', str(home_dir)])
1807 if system_user or password is None:
1808 cmd.append('--system')
1809 else:
1810@@ -90,32 +352,104 @@
1811 '--shell', shell,
1812 '--password', password,
1813 ])
1814+ if not primary_group:
1815+ try:
1816+ grp.getgrnam(username)
1817+ primary_group = username # avoid "group exists" error
1818+ except KeyError:
1819+ pass
1820+ if primary_group:
1821+ cmd.extend(['-g', primary_group])
1822+ if secondary_groups:
1823+ cmd.extend(['-G', ','.join(secondary_groups)])
1824 cmd.append(username)
1825 subprocess.check_call(cmd)
1826 user_info = pwd.getpwnam(username)
1827 return user_info
1828
1829
1830+def user_exists(username):
1831+ """Check if a user exists"""
1832+ try:
1833+ pwd.getpwnam(username)
1834+ user_exists = True
1835+ except KeyError:
1836+ user_exists = False
1837+ return user_exists
1838+
1839+
1840+def uid_exists(uid):
1841+ """Check if a uid exists"""
1842+ try:
1843+ pwd.getpwuid(uid)
1844+ uid_exists = True
1845+ except KeyError:
1846+ uid_exists = False
1847+ return uid_exists
1848+
1849+
1850+def group_exists(groupname):
1851+ """Check if a group exists"""
1852+ try:
1853+ grp.getgrnam(groupname)
1854+ group_exists = True
1855+ except KeyError:
1856+ group_exists = False
1857+ return group_exists
1858+
1859+
1860+def gid_exists(gid):
1861+ """Check if a gid exists"""
1862+ try:
1863+ grp.getgrgid(gid)
1864+ gid_exists = True
1865+ except KeyError:
1866+ gid_exists = False
1867+ return gid_exists
1868+
1869+
1870+def add_group(group_name, system_group=False, gid=None):
1871+ """Add a group to the system
1872+
1873+ Will log but otherwise succeed if the group already exists.
1874+
1875+ :param str group_name: group to create
1876+ :param bool system_group: Create system group
1877+ :param int gid: GID for user being created
1878+
1879+ :returns: The password database entry struct, as returned by `grp.getgrnam`
1880+ """
1881+ try:
1882+ group_info = grp.getgrnam(group_name)
1883+ log('group {0} already exists!'.format(group_name))
1884+ if gid:
1885+ group_info = grp.getgrgid(gid)
1886+ log('group with gid {0} already exists!'.format(gid))
1887+ except KeyError:
1888+ log('creating group {0}'.format(group_name))
1889+ add_new_group(group_name, system_group, gid)
1890+ group_info = grp.getgrnam(group_name)
1891+ return group_info
1892+
1893+
1894 def add_user_to_group(username, group):
1895 """Add a user to a group"""
1896- cmd = [
1897- 'gpasswd', '-a',
1898- username,
1899- group
1900- ]
1901+ cmd = ['gpasswd', '-a', username, group]
1902 log("Adding user {} to group {}".format(username, group))
1903 subprocess.check_call(cmd)
1904
1905
1906-def rsync(from_path, to_path, flags='-r', options=None):
1907+def rsync(from_path, to_path, flags='-r', options=None, timeout=None):
1908 """Replicate the contents of a path"""
1909 options = options or ['--delete', '--executability']
1910 cmd = ['/usr/bin/rsync', flags]
1911+ if timeout:
1912+ cmd = ['timeout', str(timeout)] + cmd
1913 cmd.extend(options)
1914 cmd.append(from_path)
1915 cmd.append(to_path)
1916 log(" ".join(cmd))
1917- return subprocess.check_output(cmd).strip()
1918+ return subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('UTF-8').strip()
1919
1920
1921 def symlink(source, destination):
1922@@ -130,42 +464,43 @@
1923 subprocess.check_call(cmd)
1924
1925
1926-def mkdir(path, owner='root', group='root', perms=0555, force=False):
1927+def mkdir(path, owner='root', group='root', perms=0o555, force=False):
1928 """Create a directory"""
1929 log("Making dir {} {}:{} {:o}".format(path, owner, group,
1930 perms))
1931 uid = pwd.getpwnam(owner).pw_uid
1932 gid = grp.getgrnam(group).gr_gid
1933 realpath = os.path.abspath(path)
1934- if os.path.exists(realpath):
1935- if force and not os.path.isdir(realpath):
1936+ path_exists = os.path.exists(realpath)
1937+ if path_exists and force:
1938+ if not os.path.isdir(realpath):
1939 log("Removing non-directory file {} prior to mkdir()".format(path))
1940 os.unlink(realpath)
1941- else:
1942+ os.makedirs(realpath, perms)
1943+ elif not path_exists:
1944 os.makedirs(realpath, perms)
1945 os.chown(realpath, uid, gid)
1946-
1947-
1948-def write_file(path, content, owner='root', group='root', perms=0444):
1949- """Create or overwrite a file with the contents of a string"""
1950+ os.chmod(realpath, perms)
1951+
1952+
1953+def write_file(path, content, owner='root', group='root', perms=0o444):
1954+ """Create or overwrite a file with the contents of a byte string."""
1955 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
1956 uid = pwd.getpwnam(owner).pw_uid
1957 gid = grp.getgrnam(group).gr_gid
1958- with open(path, 'w') as target:
1959+ with open(path, 'wb') as target:
1960 os.fchown(target.fileno(), uid, gid)
1961 os.fchmod(target.fileno(), perms)
1962 target.write(content)
1963
1964
1965 def fstab_remove(mp):
1966- """Remove the given mountpoint entry from /etc/fstab
1967- """
1968+ """Remove the given mountpoint entry from /etc/fstab"""
1969 return Fstab.remove_by_mountpoint(mp)
1970
1971
1972 def fstab_add(dev, mp, fs, options=None):
1973- """Adds the given device entry to the /etc/fstab file
1974- """
1975+ """Adds the given device entry to the /etc/fstab file"""
1976 return Fstab.add(dev, mp, fs, options=options)
1977
1978
1979@@ -177,7 +512,7 @@
1980 cmd_args.extend([device, mountpoint])
1981 try:
1982 subprocess.check_output(cmd_args)
1983- except subprocess.CalledProcessError, e:
1984+ except subprocess.CalledProcessError as e:
1985 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
1986 return False
1987
1988@@ -191,7 +526,7 @@
1989 cmd_args = ['umount', mountpoint]
1990 try:
1991 subprocess.check_output(cmd_args)
1992- except subprocess.CalledProcessError, e:
1993+ except subprocess.CalledProcessError as e:
1994 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
1995 return False
1996
1997@@ -209,25 +544,48 @@
1998 return system_mounts
1999
2000
2001+def fstab_mount(mountpoint):
2002+ """Mount filesystem using fstab"""
2003+ cmd_args = ['mount', mountpoint]
2004+ try:
2005+ subprocess.check_output(cmd_args)
2006+ except subprocess.CalledProcessError as e:
2007+ log('Error unmounting {}\n{}'.format(mountpoint, e.output))
2008+ return False
2009+ return True
2010+
2011+
2012 def file_hash(path, hash_type='md5'):
2013- """
2014- Generate a hash checksum of the contents of 'path' or None if not found.
2015+ """Generate a hash checksum of the contents of 'path' or None if not found.
2016
2017 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
2018 such as md5, sha1, sha256, sha512, etc.
2019 """
2020 if os.path.exists(path):
2021 h = getattr(hashlib, hash_type)()
2022- with open(path, 'r') as source:
2023- h.update(source.read()) # IGNORE:E1101 - it does have update
2024+ with open(path, 'rb') as source:
2025+ h.update(source.read())
2026 return h.hexdigest()
2027 else:
2028 return None
2029
2030
2031+def path_hash(path):
2032+ """Generate a hash checksum of all files matching 'path'. Standard
2033+ wildcards like '*' and '?' are supported, see documentation for the 'glob'
2034+ module for more information.
2035+
2036+ :return: dict: A { filename: hash } dictionary for all matched files.
2037+ Empty if none found.
2038+ """
2039+ return {
2040+ filename: file_hash(filename)
2041+ for filename in glob.iglob(path)
2042+ }
2043+
2044+
2045 def check_hash(path, checksum, hash_type='md5'):
2046- """
2047- Validate a file using a cryptographic checksum.
2048+ """Validate a file using a cryptographic checksum.
2049
2050 :param str checksum: Value of the checksum used to validate the file.
2051 :param str hash_type: Hash algorithm used to generate `checksum`.
2052@@ -242,100 +600,184 @@
2053
2054
2055 class ChecksumError(ValueError):
2056+ """A class derived from Value error to indicate the checksum failed."""
2057 pass
2058
2059
2060-def restart_on_change(restart_map, stopstart=False):
2061+def restart_on_change(restart_map, stopstart=False, restart_functions=None):
2062 """Restart services based on configuration files changing
2063
2064 This function is used a decorator, for example::
2065
2066 @restart_on_change({
2067 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
2068+ '/etc/apache/sites-enabled/*': [ 'apache2' ]
2069 })
2070- def ceph_client_changed():
2071+ def config_changed():
2072 pass # your code here
2073
2074 In this example, the cinder-api and cinder-volume services
2075 would be restarted if /etc/ceph/ceph.conf is changed by the
2076- ceph_client_changed function.
2077+ ceph_client_changed function. The apache2 service would be
2078+ restarted if any file matching the pattern got changed, created
2079+ or removed. Standard wildcards are supported, see documentation
2080+ for the 'glob' module for more information.
2081+
2082+ @param restart_map: {path_file_name: [service_name, ...]
2083+ @param stopstart: DEFAULT false; whether to stop, start OR restart
2084+ @param restart_functions: nonstandard functions to use to restart services
2085+ {svc: func, ...}
2086+ @returns result from decorated function
2087 """
2088 def wrap(f):
2089- def wrapped_f(*args):
2090- checksums = {}
2091- for path in restart_map:
2092- checksums[path] = file_hash(path)
2093- f(*args)
2094- restarts = []
2095- for path in restart_map:
2096- if checksums[path] != file_hash(path):
2097- restarts += restart_map[path]
2098- services_list = list(OrderedDict.fromkeys(restarts))
2099- if not stopstart:
2100- for service_name in services_list:
2101- service('restart', service_name)
2102- else:
2103- for action in ['stop', 'start']:
2104- for service_name in services_list:
2105- service(action, service_name)
2106+ @functools.wraps(f)
2107+ def wrapped_f(*args, **kwargs):
2108+ return restart_on_change_helper(
2109+ (lambda: f(*args, **kwargs)), restart_map, stopstart,
2110+ restart_functions)
2111 return wrapped_f
2112 return wrap
2113
2114
2115-def lsb_release():
2116- """Return /etc/lsb-release in a dict"""
2117- d = {}
2118- with open('/etc/lsb-release', 'r') as lsb:
2119- for l in lsb:
2120- k, v = l.split('=')
2121- d[k.strip()] = v.strip()
2122- return d
2123+def restart_on_change_helper(lambda_f, restart_map, stopstart=False,
2124+ restart_functions=None):
2125+ """Helper function to perform the restart_on_change function.
2126+
2127+ This is provided for decorators to restart services if files described
2128+ in the restart_map have changed after an invocation of lambda_f().
2129+
2130+ @param lambda_f: function to call.
2131+ @param restart_map: {file: [service, ...]}
2132+ @param stopstart: whether to stop, start or restart a service
2133+ @param restart_functions: nonstandard functions to use to restart services
2134+ {svc: func, ...}
2135+ @returns result of lambda_f()
2136+ """
2137+ if restart_functions is None:
2138+ restart_functions = {}
2139+ checksums = {path: path_hash(path) for path in restart_map}
2140+ r = lambda_f()
2141+ # create a list of lists of the services to restart
2142+ restarts = [restart_map[path]
2143+ for path in restart_map
2144+ if path_hash(path) != checksums[path]]
2145+ # create a flat list of ordered services without duplicates from lists
2146+ services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
2147+ if services_list:
2148+ actions = ('stop', 'start') if stopstart else ('restart',)
2149+ for service_name in services_list:
2150+ if service_name in restart_functions:
2151+ restart_functions[service_name](service_name)
2152+ else:
2153+ for action in actions:
2154+ service(action, service_name)
2155+ return r
2156
2157
2158 def pwgen(length=None):
2159 """Generate a random pasword."""
2160 if length is None:
2161+ # A random length is ok to use a weak PRNG
2162 length = random.choice(range(35, 45))
2163 alphanumeric_chars = [
2164- l for l in (string.letters + string.digits)
2165+ l for l in (string.ascii_letters + string.digits)
2166 if l not in 'l0QD1vAEIOUaeiou']
2167+ # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
2168+ # actual password
2169+ random_generator = random.SystemRandom()
2170 random_chars = [
2171- random.choice(alphanumeric_chars) for _ in range(length)]
2172+ random_generator.choice(alphanumeric_chars) for _ in range(length)]
2173 return(''.join(random_chars))
2174
2175
2176-def list_nics(nic_type):
2177- '''Return a list of nics of given type(s)'''
2178- if isinstance(nic_type, basestring):
2179+def is_phy_iface(interface):
2180+ """Returns True if interface is not virtual, otherwise False."""
2181+ if interface:
2182+ sys_net = '/sys/class/net'
2183+ if os.path.isdir(sys_net):
2184+ for iface in glob.glob(os.path.join(sys_net, '*')):
2185+ if '/virtual/' in os.path.realpath(iface):
2186+ continue
2187+
2188+ if interface == os.path.basename(iface):
2189+ return True
2190+
2191+ return False
2192+
2193+
2194+def get_bond_master(interface):
2195+ """Returns bond master if interface is bond slave otherwise None.
2196+
2197+ NOTE: the provided interface is expected to be physical
2198+ """
2199+ if interface:
2200+ iface_path = '/sys/class/net/%s' % (interface)
2201+ if os.path.exists(iface_path):
2202+ if '/virtual/' in os.path.realpath(iface_path):
2203+ return None
2204+
2205+ master = os.path.join(iface_path, 'master')
2206+ if os.path.exists(master):
2207+ master = os.path.realpath(master)
2208+ # make sure it is a bond master
2209+ if os.path.exists(os.path.join(master, 'bonding')):
2210+ return os.path.basename(master)
2211+
2212+ return None
2213+
2214+
2215+def list_nics(nic_type=None):
2216+ """Return a list of nics of given type(s)"""
2217+ if isinstance(nic_type, six.string_types):
2218 int_types = [nic_type]
2219 else:
2220 int_types = nic_type
2221+
2222 interfaces = []
2223- for int_type in int_types:
2224- cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
2225- ip_output = subprocess.check_output(cmd).split('\n')
2226- ip_output = (line for line in ip_output if line)
2227+ if nic_type:
2228+ for int_type in int_types:
2229+ cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
2230+ ip_output = subprocess.check_output(cmd).decode('UTF-8')
2231+ ip_output = ip_output.split('\n')
2232+ ip_output = (line for line in ip_output if line)
2233+ for line in ip_output:
2234+ if line.split()[1].startswith(int_type):
2235+ matched = re.search('.*: (' + int_type +
2236+ r'[0-9]+\.[0-9]+)@.*', line)
2237+ if matched:
2238+ iface = matched.groups()[0]
2239+ else:
2240+ iface = line.split()[1].replace(":", "")
2241+
2242+ if iface not in interfaces:
2243+ interfaces.append(iface)
2244+ else:
2245+ cmd = ['ip', 'a']
2246+ ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
2247+ ip_output = (line.strip() for line in ip_output if line)
2248+
2249+ key = re.compile('^[0-9]+:\s+(.+):')
2250 for line in ip_output:
2251- if line.split()[1].startswith(int_type):
2252- matched = re.search('.*: (bond[0-9]+\.[0-9]+)@.*', line)
2253- if matched:
2254- interface = matched.groups()[0]
2255- else:
2256- interface = line.split()[1].replace(":", "")
2257- interfaces.append(interface)
2258+ matched = re.search(key, line)
2259+ if matched:
2260+ iface = matched.group(1)
2261+ iface = iface.partition("@")[0]
2262+ if iface not in interfaces:
2263+ interfaces.append(iface)
2264
2265 return interfaces
2266
2267
2268 def set_nic_mtu(nic, mtu):
2269- '''Set MTU on a network interface'''
2270+ """Set the Maximum Transmission Unit (MTU) on a network interface."""
2271 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
2272 subprocess.check_call(cmd)
2273
2274
2275 def get_nic_mtu(nic):
2276+ """Return the Maximum Transmission Unit (MTU) for a network interface."""
2277 cmd = ['ip', 'addr', 'show', nic]
2278- ip_output = subprocess.check_output(cmd).split('\n')
2279+ ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
2280 mtu = ""
2281 for line in ip_output:
2282 words = line.split()
2283@@ -345,8 +787,9 @@
2284
2285
2286 def get_nic_hwaddr(nic):
2287+ """Return the Media Access Control (MAC) for a network interface."""
2288 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
2289- ip_output = subprocess.check_output(cmd)
2290+ ip_output = subprocess.check_output(cmd).decode('UTF-8')
2291 hwaddr = ""
2292 words = ip_output.split()
2293 if 'link/ether' in words:
2294@@ -354,38 +797,126 @@
2295 return hwaddr
2296
2297
2298-def cmp_pkgrevno(package, revno, pkgcache=None):
2299- '''Compare supplied revno with the revno of the installed package
2300-
2301- * 1 => Installed revno is greater than supplied arg
2302- * 0 => Installed revno is the same as supplied arg
2303- * -1 => Installed revno is less than supplied arg
2304-
2305- '''
2306- import apt_pkg
2307- from charmhelpers.fetch import apt_cache
2308- if not pkgcache:
2309- pkgcache = apt_cache()
2310- pkg = pkgcache[package]
2311- return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
2312-
2313-
2314 @contextmanager
2315-def chdir(d):
2316+def chdir(directory):
2317+ """Change the current working directory to a different directory for a code
2318+ block and return the previous directory after the block exits. Useful to
2319+ run commands from a specificed directory.
2320+
2321+ :param str directory: The directory path to change to for this context.
2322+ """
2323 cur = os.getcwd()
2324 try:
2325- yield os.chdir(d)
2326+ yield os.chdir(directory)
2327 finally:
2328 os.chdir(cur)
2329
2330
2331-def chownr(path, owner, group):
2332+def chownr(path, owner, group, follow_links=True, chowntopdir=False):
2333+ """Recursively change user and group ownership of files and directories
2334+ in given path. Doesn't chown path itself by default, only its children.
2335+
2336+ :param str path: The string path to start changing ownership.
2337+ :param str owner: The owner string to use when looking up the uid.
2338+ :param str group: The group string to use when looking up the gid.
2339+ :param bool follow_links: Also follow and chown links if True
2340+ :param bool chowntopdir: Also chown path itself if True
2341+ """
2342 uid = pwd.getpwnam(owner).pw_uid
2343 gid = grp.getgrnam(group).gr_gid
2344+ if follow_links:
2345+ chown = os.chown
2346+ else:
2347+ chown = os.lchown
2348
2349- for root, dirs, files in os.walk(path):
2350+ if chowntopdir:
2351+ broken_symlink = os.path.lexists(path) and not os.path.exists(path)
2352+ if not broken_symlink:
2353+ chown(path, uid, gid)
2354+ for root, dirs, files in os.walk(path, followlinks=follow_links):
2355 for name in dirs + files:
2356 full = os.path.join(root, name)
2357 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
2358 if not broken_symlink:
2359- os.chown(full, uid, gid)
2360+ chown(full, uid, gid)
2361+
2362+
2363+def lchownr(path, owner, group):
2364+ """Recursively change user and group ownership of files and directories
2365+ in a given path, not following symbolic links. See the documentation for
2366+ 'os.lchown' for more information.
2367+
2368+ :param str path: The string path to start changing ownership.
2369+ :param str owner: The owner string to use when looking up the uid.
2370+ :param str group: The group string to use when looking up the gid.
2371+ """
2372+ chownr(path, owner, group, follow_links=False)
2373+
2374+
2375+def owner(path):
2376+ """Returns a tuple containing the username & groupname owning the path.
2377+
2378+ :param str path: the string path to retrieve the ownership
2379+ :return tuple(str, str): A (username, groupname) tuple containing the
2380+ name of the user and group owning the path.
2381+ :raises OSError: if the specified path does not exist
2382+ """
2383+ stat = os.stat(path)
2384+ username = pwd.getpwuid(stat.st_uid)[0]
2385+ groupname = grp.getgrgid(stat.st_gid)[0]
2386+ return username, groupname
2387+
2388+
2389+def get_total_ram():
2390+ """The total amount of system RAM in bytes.
2391+
2392+ This is what is reported by the OS, and may be overcommitted when
2393+ there are multiple containers hosted on the same machine.
2394+ """
2395+ with open('/proc/meminfo', 'r') as f:
2396+ for line in f.readlines():
2397+ if line:
2398+ key, value, unit = line.split()
2399+ if key == 'MemTotal:':
2400+ assert unit == 'kB', 'Unknown unit'
2401+ return int(value) * 1024 # Classic, not KiB.
2402+ raise NotImplementedError()
2403+
2404+
2405+UPSTART_CONTAINER_TYPE = '/run/container_type'
2406+
2407+
2408+def is_container():
2409+ """Determine whether unit is running in a container
2410+
2411+ @return: boolean indicating if unit is in a container
2412+ """
2413+ if init_is_systemd():
2414+ # Detect using systemd-detect-virt
2415+ return subprocess.call(['systemd-detect-virt',
2416+ '--container']) == 0
2417+ else:
2418+ # Detect using upstart container file marker
2419+ return os.path.exists(UPSTART_CONTAINER_TYPE)
2420+
2421+
2422+def add_to_updatedb_prunepath(path, updatedb_path=UPDATEDB_PATH):
2423+ with open(updatedb_path, 'r+') as f_id:
2424+ updatedb_text = f_id.read()
2425+ output = updatedb(updatedb_text, path)
2426+ f_id.seek(0)
2427+ f_id.write(output)
2428+ f_id.truncate()
2429+
2430+
2431+def updatedb(updatedb_text, new_path):
2432+ lines = [line for line in updatedb_text.split("\n")]
2433+ for i, line in enumerate(lines):
2434+ if line.startswith("PRUNEPATHS="):
2435+ paths_line = line.split("=")[1].replace('"', '')
2436+ paths = paths_line.split(" ")
2437+ if new_path not in paths:
2438+ paths.append(new_path)
2439+ lines[i] = 'PRUNEPATHS="{}"'.format(' '.join(paths))
2440+ output = "\n".join(lines)
2441+ return output
2442
2443=== added directory 'hooks/charmhelpers/core/host_factory'
2444=== added file 'hooks/charmhelpers/core/host_factory/__init__.py'
2445=== added file 'hooks/charmhelpers/core/host_factory/centos.py'
2446--- hooks/charmhelpers/core/host_factory/centos.py 1970-01-01 00:00:00 +0000
2447+++ hooks/charmhelpers/core/host_factory/centos.py 2017-05-11 14:56:57 +0000
2448@@ -0,0 +1,72 @@
2449+import subprocess
2450+import yum
2451+import os
2452+
2453+from charmhelpers.core.strutils import BasicStringComparator
2454+
2455+
2456+class CompareHostReleases(BasicStringComparator):
2457+ """Provide comparisons of Host releases.
2458+
2459+ Use in the form of
2460+
2461+ if CompareHostReleases(release) > 'trusty':
2462+ # do something with mitaka
2463+ """
2464+
2465+ def __init__(self, item):
2466+ raise NotImplementedError(
2467+ "CompareHostReleases() is not implemented for CentOS")
2468+
2469+
2470+def service_available(service_name):
2471+ # """Determine whether a system service is available."""
2472+ if os.path.isdir('/run/systemd/system'):
2473+ cmd = ['systemctl', 'is-enabled', service_name]
2474+ else:
2475+ cmd = ['service', service_name, 'is-enabled']
2476+ return subprocess.call(cmd) == 0
2477+
2478+
2479+def add_new_group(group_name, system_group=False, gid=None):
2480+ cmd = ['groupadd']
2481+ if gid:
2482+ cmd.extend(['--gid', str(gid)])
2483+ if system_group:
2484+ cmd.append('-r')
2485+ cmd.append(group_name)
2486+ subprocess.check_call(cmd)
2487+
2488+
2489+def lsb_release():
2490+ """Return /etc/os-release in a dict."""
2491+ d = {}
2492+ with open('/etc/os-release', 'r') as lsb:
2493+ for l in lsb:
2494+ s = l.split('=')
2495+ if len(s) != 2:
2496+ continue
2497+ d[s[0].strip()] = s[1].strip()
2498+ return d
2499+
2500+
2501+def cmp_pkgrevno(package, revno, pkgcache=None):
2502+ """Compare supplied revno with the revno of the installed package.
2503+
2504+ * 1 => Installed revno is greater than supplied arg
2505+ * 0 => Installed revno is the same as supplied arg
2506+ * -1 => Installed revno is less than supplied arg
2507+
2508+ This function imports YumBase function if the pkgcache argument
2509+ is None.
2510+ """
2511+ if not pkgcache:
2512+ y = yum.YumBase()
2513+ packages = y.doPackageLists()
2514+ pkgcache = {i.Name: i.version for i in packages['installed']}
2515+ pkg = pkgcache[package]
2516+ if pkg > revno:
2517+ return 1
2518+ if pkg < revno:
2519+ return -1
2520+ return 0
2521
2522=== added file 'hooks/charmhelpers/core/host_factory/ubuntu.py'
2523--- hooks/charmhelpers/core/host_factory/ubuntu.py 1970-01-01 00:00:00 +0000
2524+++ hooks/charmhelpers/core/host_factory/ubuntu.py 2017-05-11 14:56:57 +0000
2525@@ -0,0 +1,88 @@
2526+import subprocess
2527+
2528+from charmhelpers.core.strutils import BasicStringComparator
2529+
2530+
2531+UBUNTU_RELEASES = (
2532+ 'lucid',
2533+ 'maverick',
2534+ 'natty',
2535+ 'oneiric',
2536+ 'precise',
2537+ 'quantal',
2538+ 'raring',
2539+ 'saucy',
2540+ 'trusty',
2541+ 'utopic',
2542+ 'vivid',
2543+ 'wily',
2544+ 'xenial',
2545+ 'yakkety',
2546+ 'zesty',
2547+)
2548+
2549+
2550+class CompareHostReleases(BasicStringComparator):
2551+ """Provide comparisons of Ubuntu releases.
2552+
2553+ Use in the form of
2554+
2555+ if CompareHostReleases(release) > 'trusty':
2556+ # do something with mitaka
2557+ """
2558+ _list = UBUNTU_RELEASES
2559+
2560+
2561+def service_available(service_name):
2562+ """Determine whether a system service is available"""
2563+ try:
2564+ subprocess.check_output(
2565+ ['service', service_name, 'status'],
2566+ stderr=subprocess.STDOUT).decode('UTF-8')
2567+ except subprocess.CalledProcessError as e:
2568+ return b'unrecognized service' not in e.output
2569+ else:
2570+ return True
2571+
2572+
2573+def add_new_group(group_name, system_group=False, gid=None):
2574+ cmd = ['addgroup']
2575+ if gid:
2576+ cmd.extend(['--gid', str(gid)])
2577+ if system_group:
2578+ cmd.append('--system')
2579+ else:
2580+ cmd.extend([
2581+ '--group',
2582+ ])
2583+ cmd.append(group_name)
2584+ subprocess.check_call(cmd)
2585+
2586+
2587+def lsb_release():
2588+ """Return /etc/lsb-release in a dict"""
2589+ d = {}
2590+ with open('/etc/lsb-release', 'r') as lsb:
2591+ for l in lsb:
2592+ k, v = l.split('=')
2593+ d[k.strip()] = v.strip()
2594+ return d
2595+
2596+
2597+def cmp_pkgrevno(package, revno, pkgcache=None):
2598+ """Compare supplied revno with the revno of the installed package.
2599+
2600+ * 1 => Installed revno is greater than supplied arg
2601+ * 0 => Installed revno is the same as supplied arg
2602+ * -1 => Installed revno is less than supplied arg
2603+
2604+ This function imports apt_cache function from charmhelpers.fetch if
2605+ the pkgcache argument is None. Be sure to add charmhelpers.fetch if
2606+ you call this function, or pass an apt_pkg.Cache() instance.
2607+ """
2608+ import apt_pkg
2609+ if not pkgcache:
2610+ from charmhelpers.fetch import apt_cache
2611+ pkgcache = apt_cache()
2612+ pkg = pkgcache[package]
2613+ return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
2614
2615=== added file 'hooks/charmhelpers/core/hugepage.py'
2616--- hooks/charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000
2617+++ hooks/charmhelpers/core/hugepage.py 2017-05-11 14:56:57 +0000
2618@@ -0,0 +1,69 @@
2619+# -*- coding: utf-8 -*-
2620+
2621+# Copyright 2014-2015 Canonical Limited.
2622+#
2623+# Licensed under the Apache License, Version 2.0 (the "License");
2624+# you may not use this file except in compliance with the License.
2625+# You may obtain a copy of the License at
2626+#
2627+# http://www.apache.org/licenses/LICENSE-2.0
2628+#
2629+# Unless required by applicable law or agreed to in writing, software
2630+# distributed under the License is distributed on an "AS IS" BASIS,
2631+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2632+# See the License for the specific language governing permissions and
2633+# limitations under the License.
2634+
2635+import yaml
2636+from charmhelpers.core import fstab
2637+from charmhelpers.core import sysctl
2638+from charmhelpers.core.host import (
2639+ add_group,
2640+ add_user_to_group,
2641+ fstab_mount,
2642+ mkdir,
2643+)
2644+from charmhelpers.core.strutils import bytes_from_string
2645+from subprocess import check_output
2646+
2647+
2648+def hugepage_support(user, group='hugetlb', nr_hugepages=256,
2649+ max_map_count=65536, mnt_point='/run/hugepages/kvm',
2650+ pagesize='2MB', mount=True, set_shmmax=False):
2651+ """Enable hugepages on system.
2652+
2653+ Args:
2654+ user (str) -- Username to allow access to hugepages to
2655+ group (str) -- Group name to own hugepages
2656+ nr_hugepages (int) -- Number of pages to reserve
2657+ max_map_count (int) -- Number of Virtual Memory Areas a process can own
2658+ mnt_point (str) -- Directory to mount hugepages on
2659+ pagesize (str) -- Size of hugepages
2660+ mount (bool) -- Whether to Mount hugepages
2661+ """
2662+ group_info = add_group(group)
2663+ gid = group_info.gr_gid
2664+ add_user_to_group(user, group)
2665+ if max_map_count < 2 * nr_hugepages:
2666+ max_map_count = 2 * nr_hugepages
2667+ sysctl_settings = {
2668+ 'vm.nr_hugepages': nr_hugepages,
2669+ 'vm.max_map_count': max_map_count,
2670+ 'vm.hugetlb_shm_group': gid,
2671+ }
2672+ if set_shmmax:
2673+ shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax']))
2674+ shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages
2675+ if shmmax_minsize > shmmax_current:
2676+ sysctl_settings['kernel.shmmax'] = shmmax_minsize
2677+ sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
2678+ mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
2679+ lfstab = fstab.Fstab()
2680+ fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point)
2681+ if fstab_entry:
2682+ lfstab.remove_entry(fstab_entry)
2683+ entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs',
2684+ 'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0)
2685+ lfstab.add_entry(entry)
2686+ if mount:
2687+ fstab_mount(mnt_point)
2688
2689=== added file 'hooks/charmhelpers/core/kernel.py'
2690--- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
2691+++ hooks/charmhelpers/core/kernel.py 2017-05-11 14:56:57 +0000
2692@@ -0,0 +1,72 @@
2693+#!/usr/bin/env python
2694+# -*- coding: utf-8 -*-
2695+
2696+# Copyright 2014-2015 Canonical Limited.
2697+#
2698+# Licensed under the Apache License, Version 2.0 (the "License");
2699+# you may not use this file except in compliance with the License.
2700+# You may obtain a copy of the License at
2701+#
2702+# http://www.apache.org/licenses/LICENSE-2.0
2703+#
2704+# Unless required by applicable law or agreed to in writing, software
2705+# distributed under the License is distributed on an "AS IS" BASIS,
2706+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2707+# See the License for the specific language governing permissions and
2708+# limitations under the License.
2709+
2710+import re
2711+import subprocess
2712+
2713+from charmhelpers.osplatform import get_platform
2714+from charmhelpers.core.hookenv import (
2715+ log,
2716+ INFO
2717+)
2718+
2719+__platform__ = get_platform()
2720+if __platform__ == "ubuntu":
2721+ from charmhelpers.core.kernel_factory.ubuntu import (
2722+ persistent_modprobe,
2723+ update_initramfs,
2724+ ) # flake8: noqa -- ignore F401 for this import
2725+elif __platform__ == "centos":
2726+ from charmhelpers.core.kernel_factory.centos import (
2727+ persistent_modprobe,
2728+ update_initramfs,
2729+ ) # flake8: noqa -- ignore F401 for this import
2730+
2731+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
2732+
2733+
2734+def modprobe(module, persist=True):
2735+ """Load a kernel module and configure for auto-load on reboot."""
2736+ cmd = ['modprobe', module]
2737+
2738+ log('Loading kernel module %s' % module, level=INFO)
2739+
2740+ subprocess.check_call(cmd)
2741+ if persist:
2742+ persistent_modprobe(module)
2743+
2744+
2745+def rmmod(module, force=False):
2746+ """Remove a module from the linux kernel"""
2747+ cmd = ['rmmod']
2748+ if force:
2749+ cmd.append('-f')
2750+ cmd.append(module)
2751+ log('Removing kernel module %s' % module, level=INFO)
2752+ return subprocess.check_call(cmd)
2753+
2754+
2755+def lsmod():
2756+ """Shows what kernel modules are currently loaded"""
2757+ return subprocess.check_output(['lsmod'],
2758+ universal_newlines=True)
2759+
2760+
2761+def is_module_loaded(module):
2762+ """Checks if a kernel module is already loaded"""
2763+ matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
2764+ return len(matches) > 0
2765
2766=== added directory 'hooks/charmhelpers/core/kernel_factory'
2767=== added file 'hooks/charmhelpers/core/kernel_factory/__init__.py'
2768=== added file 'hooks/charmhelpers/core/kernel_factory/centos.py'
2769--- hooks/charmhelpers/core/kernel_factory/centos.py 1970-01-01 00:00:00 +0000
2770+++ hooks/charmhelpers/core/kernel_factory/centos.py 2017-05-11 14:56:57 +0000
2771@@ -0,0 +1,17 @@
2772+import subprocess
2773+import os
2774+
2775+
2776+def persistent_modprobe(module):
2777+ """Load a kernel module and configure for auto-load on reboot."""
2778+ if not os.path.exists('/etc/rc.modules'):
2779+ open('/etc/rc.modules', 'a')
2780+ os.chmod('/etc/rc.modules', 111)
2781+ with open('/etc/rc.modules', 'r+') as modules:
2782+ if module not in modules.read():
2783+ modules.write('modprobe %s\n' % module)
2784+
2785+
2786+def update_initramfs(version='all'):
2787+ """Updates an initramfs image."""
2788+ return subprocess.check_call(["dracut", "-f", version])
2789
2790=== added file 'hooks/charmhelpers/core/kernel_factory/ubuntu.py'
2791--- hooks/charmhelpers/core/kernel_factory/ubuntu.py 1970-01-01 00:00:00 +0000
2792+++ hooks/charmhelpers/core/kernel_factory/ubuntu.py 2017-05-11 14:56:57 +0000
2793@@ -0,0 +1,13 @@
2794+import subprocess
2795+
2796+
2797+def persistent_modprobe(module):
2798+ """Load a kernel module and configure for auto-load on reboot."""
2799+ with open('/etc/modules', 'r+') as modules:
2800+ if module not in modules.read():
2801+ modules.write(module + "\n")
2802+
2803+
2804+def update_initramfs(version='all'):
2805+ """Updates an initramfs image."""
2806+ return subprocess.check_call(["update-initramfs", "-k", version, "-u"])
2807
2808=== modified file 'hooks/charmhelpers/core/services/__init__.py'
2809--- hooks/charmhelpers/core/services/__init__.py 2014-10-24 16:35:00 +0000
2810+++ hooks/charmhelpers/core/services/__init__.py 2017-05-11 14:56:57 +0000
2811@@ -1,2 +1,16 @@
2812+# Copyright 2014-2015 Canonical Limited.
2813+#
2814+# Licensed under the Apache License, Version 2.0 (the "License");
2815+# you may not use this file except in compliance with the License.
2816+# You may obtain a copy of the License at
2817+#
2818+# http://www.apache.org/licenses/LICENSE-2.0
2819+#
2820+# Unless required by applicable law or agreed to in writing, software
2821+# distributed under the License is distributed on an "AS IS" BASIS,
2822+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2823+# See the License for the specific language governing permissions and
2824+# limitations under the License.
2825+
2826 from .base import * # NOQA
2827 from .helpers import * # NOQA
2828
2829=== modified file 'hooks/charmhelpers/core/services/base.py'
2830--- hooks/charmhelpers/core/services/base.py 2014-10-24 16:35:00 +0000
2831+++ hooks/charmhelpers/core/services/base.py 2017-05-11 14:56:57 +0000
2832@@ -1,7 +1,21 @@
2833+# Copyright 2014-2015 Canonical Limited.
2834+#
2835+# Licensed under the Apache License, Version 2.0 (the "License");
2836+# you may not use this file except in compliance with the License.
2837+# You may obtain a copy of the License at
2838+#
2839+# http://www.apache.org/licenses/LICENSE-2.0
2840+#
2841+# Unless required by applicable law or agreed to in writing, software
2842+# distributed under the License is distributed on an "AS IS" BASIS,
2843+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2844+# See the License for the specific language governing permissions and
2845+# limitations under the License.
2846+
2847 import os
2848-import re
2849 import json
2850-from collections import Iterable
2851+from inspect import getargspec
2852+from collections import Iterable, OrderedDict
2853
2854 from charmhelpers.core import host
2855 from charmhelpers.core import hookenv
2856@@ -103,7 +117,7 @@
2857 """
2858 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
2859 self._ready = None
2860- self.services = {}
2861+ self.services = OrderedDict()
2862 for service in services or []:
2863 service_name = service['service']
2864 self.services[service_name] = service
2865@@ -112,15 +126,18 @@
2866 """
2867 Handle the current hook by doing The Right Thing with the registered services.
2868 """
2869- hook_name = hookenv.hook_name()
2870- if hook_name == 'stop':
2871- self.stop_services()
2872- else:
2873- self.provide_data()
2874- self.reconfigure_services()
2875- cfg = hookenv.config()
2876- if cfg.implicit_save:
2877- cfg.save()
2878+ hookenv._run_atstart()
2879+ try:
2880+ hook_name = hookenv.hook_name()
2881+ if hook_name == 'stop':
2882+ self.stop_services()
2883+ else:
2884+ self.reconfigure_services()
2885+ self.provide_data()
2886+ except SystemExit as x:
2887+ if x.code is None or x.code == 0:
2888+ hookenv._run_atexit()
2889+ hookenv._run_atexit()
2890
2891 def provide_data(self):
2892 """
2893@@ -129,15 +146,36 @@
2894 A provider must have a `name` attribute, which indicates which relation
2895 to set data on, and a `provide_data()` method, which returns a dict of
2896 data to set.
2897+
2898+ The `provide_data()` method can optionally accept two parameters:
2899+
2900+ * ``remote_service`` The name of the remote service that the data will
2901+ be provided to. The `provide_data()` method will be called once
2902+ for each connected service (not unit). This allows the method to
2903+ tailor its data to the given service.
2904+ * ``service_ready`` Whether or not the service definition had all of
2905+ its requirements met, and thus the ``data_ready`` callbacks run.
2906+
2907+ Note that the ``provided_data`` methods are now called **after** the
2908+ ``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks
2909+ a chance to generate any data necessary for the providing to the remote
2910+ services.
2911 """
2912- hook_name = hookenv.hook_name()
2913- for service in self.services.values():
2914+ for service_name, service in self.services.items():
2915+ service_ready = self.is_ready(service_name)
2916 for provider in service.get('provided_data', []):
2917- if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
2918- data = provider.provide_data()
2919- _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data
2920- if _ready:
2921- hookenv.relation_set(None, data)
2922+ for relid in hookenv.relation_ids(provider.name):
2923+ units = hookenv.related_units(relid)
2924+ if not units:
2925+ continue
2926+ remote_service = units[0].split('/')[0]
2927+ argspec = getargspec(provider.provide_data)
2928+ if len(argspec.args) > 1:
2929+ data = provider.provide_data(remote_service, service_ready)
2930+ else:
2931+ data = provider.provide_data()
2932+ if data:
2933+ hookenv.relation_set(relid, data)
2934
2935 def reconfigure_services(self, *service_names):
2936 """
2937
2938=== modified file 'hooks/charmhelpers/core/services/helpers.py'
2939--- hooks/charmhelpers/core/services/helpers.py 2014-10-24 16:35:00 +0000
2940+++ hooks/charmhelpers/core/services/helpers.py 2017-05-11 14:56:57 +0000
2941@@ -1,6 +1,22 @@
2942+# Copyright 2014-2015 Canonical Limited.
2943+#
2944+# Licensed under the Apache License, Version 2.0 (the "License");
2945+# you may not use this file except in compliance with the License.
2946+# You may obtain a copy of the License at
2947+#
2948+# http://www.apache.org/licenses/LICENSE-2.0
2949+#
2950+# Unless required by applicable law or agreed to in writing, software
2951+# distributed under the License is distributed on an "AS IS" BASIS,
2952+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2953+# See the License for the specific language governing permissions and
2954+# limitations under the License.
2955+
2956 import os
2957 import yaml
2958+
2959 from charmhelpers.core import hookenv
2960+from charmhelpers.core import host
2961 from charmhelpers.core import templating
2962
2963 from charmhelpers.core.services.base import ManagerCallback
2964@@ -29,12 +45,14 @@
2965 """
2966 name = None
2967 interface = None
2968- required_keys = []
2969
2970 def __init__(self, name=None, additional_required_keys=None):
2971+ if not hasattr(self, 'required_keys'):
2972+ self.required_keys = []
2973+
2974 if name is not None:
2975 self.name = name
2976- if additional_required_keys is not None:
2977+ if additional_required_keys:
2978 self.required_keys.extend(additional_required_keys)
2979 self.get_data()
2980
2981@@ -118,7 +136,10 @@
2982 """
2983 name = 'db'
2984 interface = 'mysql'
2985- required_keys = ['host', 'user', 'password', 'database']
2986+
2987+ def __init__(self, *args, **kwargs):
2988+ self.required_keys = ['host', 'user', 'password', 'database']
2989+ RelationContext.__init__(self, *args, **kwargs)
2990
2991
2992 class HttpRelation(RelationContext):
2993@@ -130,7 +151,10 @@
2994 """
2995 name = 'website'
2996 interface = 'http'
2997- required_keys = ['host', 'port']
2998+
2999+ def __init__(self, *args, **kwargs):
3000+ self.required_keys = ['host', 'port']
3001+ RelationContext.__init__(self, *args, **kwargs)
3002
3003 def provide_data(self):
3004 return {
3005@@ -196,7 +220,7 @@
3006 if not os.path.isabs(file_name):
3007 file_name = os.path.join(hookenv.charm_dir(), file_name)
3008 with open(file_name, 'w') as file_stream:
3009- os.fchmod(file_stream.fileno(), 0600)
3010+ os.fchmod(file_stream.fileno(), 0o600)
3011 yaml.dump(config_data, file_stream)
3012
3013 def read_context(self, file_name):
3014@@ -211,28 +235,55 @@
3015
3016 class TemplateCallback(ManagerCallback):
3017 """
3018- Callback class that will render a Jinja2 template, for use as a ready action.
3019-
3020- :param str source: The template source file, relative to `$CHARM_DIR/templates`
3021- :param str target: The target to write the rendered template to
3022+ Callback class that will render a Jinja2 template, for use as a ready
3023+ action.
3024+
3025+ :param str source: The template source file, relative to
3026+ `$CHARM_DIR/templates`
3027+
3028+ :param str target: The target to write the rendered template to (or None)
3029 :param str owner: The owner of the rendered file
3030 :param str group: The group of the rendered file
3031 :param int perms: The permissions of the rendered file
3032+ :param partial on_change_action: functools partial to be executed when
3033+ rendered file changes
3034+ :param jinja2 loader template_loader: A jinja2 template loader
3035+
3036+ :return str: The rendered template
3037 """
3038- def __init__(self, source, target, owner='root', group='root', perms=0444):
3039+ def __init__(self, source, target,
3040+ owner='root', group='root', perms=0o444,
3041+ on_change_action=None, template_loader=None):
3042 self.source = source
3043 self.target = target
3044 self.owner = owner
3045 self.group = group
3046 self.perms = perms
3047+ self.on_change_action = on_change_action
3048+ self.template_loader = template_loader
3049
3050 def __call__(self, manager, service_name, event_name):
3051+ pre_checksum = ''
3052+ if self.on_change_action and os.path.isfile(self.target):
3053+ pre_checksum = host.file_hash(self.target)
3054 service = manager.get_service(service_name)
3055- context = {}
3056+ context = {'ctx': {}}
3057 for ctx in service.get('required_data', []):
3058 context.update(ctx)
3059- templating.render(self.source, self.target, context,
3060- self.owner, self.group, self.perms)
3061+ context['ctx'].update(ctx)
3062+
3063+ result = templating.render(self.source, self.target, context,
3064+ self.owner, self.group, self.perms,
3065+ template_loader=self.template_loader)
3066+ if self.on_change_action:
3067+ if pre_checksum == host.file_hash(self.target):
3068+ hookenv.log(
3069+ 'No change detected: {}'.format(self.target),
3070+ hookenv.DEBUG)
3071+ else:
3072+ self.on_change_action()
3073+
3074+ return result
3075
3076
3077 # Convenience aliases for templates
3078
3079=== added file 'hooks/charmhelpers/core/strutils.py'
3080--- hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000
3081+++ hooks/charmhelpers/core/strutils.py 2017-05-11 14:56:57 +0000
3082@@ -0,0 +1,123 @@
3083+#!/usr/bin/env python
3084+# -*- coding: utf-8 -*-
3085+
3086+# Copyright 2014-2015 Canonical Limited.
3087+#
3088+# Licensed under the Apache License, Version 2.0 (the "License");
3089+# you may not use this file except in compliance with the License.
3090+# You may obtain a copy of the License at
3091+#
3092+# http://www.apache.org/licenses/LICENSE-2.0
3093+#
3094+# Unless required by applicable law or agreed to in writing, software
3095+# distributed under the License is distributed on an "AS IS" BASIS,
3096+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3097+# See the License for the specific language governing permissions and
3098+# limitations under the License.
3099+
3100+import six
3101+import re
3102+
3103+
3104+def bool_from_string(value):
3105+ """Interpret string value as boolean.
3106+
3107+ Returns True if value translates to True otherwise False.
3108+ """
3109+ if isinstance(value, six.string_types):
3110+ value = six.text_type(value)
3111+ else:
3112+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
3113+ raise ValueError(msg)
3114+
3115+ value = value.strip().lower()
3116+
3117+ if value in ['y', 'yes', 'true', 't', 'on']:
3118+ return True
3119+ elif value in ['n', 'no', 'false', 'f', 'off']:
3120+ return False
3121+
3122+ msg = "Unable to interpret string value '%s' as boolean" % (value)
3123+ raise ValueError(msg)
3124+
3125+
3126+def bytes_from_string(value):
3127+ """Interpret human readable string value as bytes.
3128+
3129+ Returns int
3130+ """
3131+ BYTE_POWER = {
3132+ 'K': 1,
3133+ 'KB': 1,
3134+ 'M': 2,
3135+ 'MB': 2,
3136+ 'G': 3,
3137+ 'GB': 3,
3138+ 'T': 4,
3139+ 'TB': 4,
3140+ 'P': 5,
3141+ 'PB': 5,
3142+ }
3143+ if isinstance(value, six.string_types):
3144+ value = six.text_type(value)
3145+ else:
3146+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
3147+ raise ValueError(msg)
3148+ matches = re.match("([0-9]+)([a-zA-Z]+)", value)
3149+ if not matches:
3150+ msg = "Unable to interpret string value '%s' as bytes" % (value)
3151+ raise ValueError(msg)
3152+ return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
3153+
3154+
3155+class BasicStringComparator(object):
3156+ """Provides a class that will compare strings from an iterator type object.
3157+ Used to provide > and < comparisons on strings that may not necessarily be
3158+ alphanumerically ordered. e.g. OpenStack or Ubuntu releases AFTER the
3159+ z-wrap.
3160+ """
3161+
3162+ _list = None
3163+
3164+ def __init__(self, item):
3165+ if self._list is None:
3166+ raise Exception("Must define the _list in the class definition!")
3167+ try:
3168+ self.index = self._list.index(item)
3169+ except Exception:
3170+ raise KeyError("Item '{}' is not in list '{}'"
3171+ .format(item, self._list))
3172+
3173+ def __eq__(self, other):
3174+ assert isinstance(other, str) or isinstance(other, self.__class__)
3175+ return self.index == self._list.index(other)
3176+
3177+ def __ne__(self, other):
3178+ return not self.__eq__(other)
3179+
3180+ def __lt__(self, other):
3181+ assert isinstance(other, str) or isinstance(other, self.__class__)
3182+ return self.index < self._list.index(other)
3183+
3184+ def __ge__(self, other):
3185+ return not self.__lt__(other)
3186+
3187+ def __gt__(self, other):
3188+ assert isinstance(other, str) or isinstance(other, self.__class__)
3189+ return self.index > self._list.index(other)
3190+
3191+ def __le__(self, other):
3192+ return not self.__gt__(other)
3193+
3194+ def __str__(self):
3195+ """Always give back the item at the index so it can be used in
3196+ comparisons like:
3197+
3198+ s_mitaka = CompareOpenStack('mitaka')
3199+ s_newton = CompareOpenstack('newton')
3200+
3201+ assert s_newton > s_mitaka
3202+
3203+ @returns: <string>
3204+ """
3205+ return self._list[self.index]
3206
3207=== modified file 'hooks/charmhelpers/core/sysctl.py'
3208--- hooks/charmhelpers/core/sysctl.py 2014-10-24 16:35:00 +0000
3209+++ hooks/charmhelpers/core/sysctl.py 2017-05-11 14:56:57 +0000
3210@@ -1,7 +1,19 @@
3211 #!/usr/bin/env python
3212 # -*- coding: utf-8 -*-
3213
3214-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
3215+# Copyright 2014-2015 Canonical Limited.
3216+#
3217+# Licensed under the Apache License, Version 2.0 (the "License");
3218+# you may not use this file except in compliance with the License.
3219+# You may obtain a copy of the License at
3220+#
3221+# http://www.apache.org/licenses/LICENSE-2.0
3222+#
3223+# Unless required by applicable law or agreed to in writing, software
3224+# distributed under the License is distributed on an "AS IS" BASIS,
3225+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3226+# See the License for the specific language governing permissions and
3227+# limitations under the License.
3228
3229 import yaml
3230
3231@@ -10,25 +22,33 @@
3232 from charmhelpers.core.hookenv import (
3233 log,
3234 DEBUG,
3235+ ERROR,
3236 )
3237
3238+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
3239+
3240
3241 def create(sysctl_dict, sysctl_file):
3242 """Creates a sysctl.conf file from a YAML associative array
3243
3244- :param sysctl_dict: a dict of sysctl options eg { 'kernel.max_pid': 1337 }
3245- :type sysctl_dict: dict
3246+ :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
3247+ :type sysctl_dict: str
3248 :param sysctl_file: path to the sysctl file to be saved
3249 :type sysctl_file: str or unicode
3250 :returns: None
3251 """
3252- sysctl_dict = yaml.load(sysctl_dict)
3253+ try:
3254+ sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
3255+ except yaml.YAMLError:
3256+ log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
3257+ level=ERROR)
3258+ return
3259
3260 with open(sysctl_file, "w") as fd:
3261- for key, value in sysctl_dict.items():
3262+ for key, value in sysctl_dict_parsed.items():
3263 fd.write("{}={}\n".format(key, value))
3264
3265- log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict),
3266+ log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),
3267 level=DEBUG)
3268
3269 check_call(["sysctl", "-p", sysctl_file])
3270
3271=== modified file 'hooks/charmhelpers/core/templating.py'
3272--- hooks/charmhelpers/core/templating.py 2014-10-24 16:35:00 +0000
3273+++ hooks/charmhelpers/core/templating.py 2017-05-11 14:56:57 +0000
3274@@ -1,16 +1,33 @@
3275+# Copyright 2014-2015 Canonical Limited.
3276+#
3277+# Licensed under the Apache License, Version 2.0 (the "License");
3278+# you may not use this file except in compliance with the License.
3279+# You may obtain a copy of the License at
3280+#
3281+# http://www.apache.org/licenses/LICENSE-2.0
3282+#
3283+# Unless required by applicable law or agreed to in writing, software
3284+# distributed under the License is distributed on an "AS IS" BASIS,
3285+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3286+# See the License for the specific language governing permissions and
3287+# limitations under the License.
3288+
3289 import os
3290+import sys
3291
3292 from charmhelpers.core import host
3293 from charmhelpers.core import hookenv
3294
3295
3296-def render(source, target, context, owner='root', group='root', perms=0444, templates_dir=None):
3297+def render(source, target, context, owner='root', group='root',
3298+ perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None):
3299 """
3300 Render a template.
3301
3302 The `source` path, if not absolute, is relative to the `templates_dir`.
3303
3304- The `target` path should be absolute.
3305+ The `target` path should be absolute. It can also be `None`, in which
3306+ case no file will be written.
3307
3308 The context should be a dict containing the values to be replaced in the
3309 template.
3310@@ -19,8 +36,12 @@
3311
3312 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
3313
3314- Note: Using this requires python-jinja2; if it is not installed, calling
3315- this will attempt to use charmhelpers.fetch.apt_install to install it.
3316+ The rendered template will be written to the file as well as being returned
3317+ as a string.
3318+
3319+ Note: Using this requires python-jinja2 or python3-jinja2; if it is not
3320+ installed, calling this will attempt to use charmhelpers.fetch.apt_install
3321+ to install it.
3322 """
3323 try:
3324 from jinja2 import FileSystemLoader, Environment, exceptions
3325@@ -32,20 +53,32 @@
3326 'charmhelpers.fetch to install it',
3327 level=hookenv.ERROR)
3328 raise
3329- apt_install('python-jinja2', fatal=True)
3330+ if sys.version_info.major == 2:
3331+ apt_install('python-jinja2', fatal=True)
3332+ else:
3333+ apt_install('python3-jinja2', fatal=True)
3334 from jinja2 import FileSystemLoader, Environment, exceptions
3335
3336- if templates_dir is None:
3337- templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
3338- loader = Environment(loader=FileSystemLoader(templates_dir))
3339+ if template_loader:
3340+ template_env = Environment(loader=template_loader)
3341+ else:
3342+ if templates_dir is None:
3343+ templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
3344+ template_env = Environment(loader=FileSystemLoader(templates_dir))
3345 try:
3346 source = source
3347- template = loader.get_template(source)
3348+ template = template_env.get_template(source)
3349 except exceptions.TemplateNotFound as e:
3350 hookenv.log('Could not load template %s from %s.' %
3351 (source, templates_dir),
3352 level=hookenv.ERROR)
3353 raise e
3354 content = template.render(context)
3355- host.mkdir(os.path.dirname(target))
3356- host.write_file(target, content, owner, group, perms)
3357+ if target is not None:
3358+ target_dir = os.path.dirname(target)
3359+ if not os.path.exists(target_dir):
3360+ # This is a terrible default directory permission, as the file
3361+ # or its siblings will often contain secrets.
3362+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
3363+ host.write_file(target, content.encode(encoding), owner, group, perms)
3364+ return content
3365
3366=== added file 'hooks/charmhelpers/core/unitdata.py'
3367--- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
3368+++ hooks/charmhelpers/core/unitdata.py 2017-05-11 14:56:57 +0000
3369@@ -0,0 +1,518 @@
3370+#!/usr/bin/env python
3371+# -*- coding: utf-8 -*-
3372+#
3373+# Copyright 2014-2015 Canonical Limited.
3374+#
3375+# Licensed under the Apache License, Version 2.0 (the "License");
3376+# you may not use this file except in compliance with the License.
3377+# You may obtain a copy of the License at
3378+#
3379+# http://www.apache.org/licenses/LICENSE-2.0
3380+#
3381+# Unless required by applicable law or agreed to in writing, software
3382+# distributed under the License is distributed on an "AS IS" BASIS,
3383+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3384+# See the License for the specific language governing permissions and
3385+# limitations under the License.
3386+#
3387+# Authors:
3388+# Kapil Thangavelu <kapil.foss@gmail.com>
3389+#
3390+"""
3391+Intro
3392+-----
3393+
3394+A simple way to store state in units. This provides a key value
3395+storage with support for versioned, transactional operation,
3396+and can calculate deltas from previous values to simplify unit logic
3397+when processing changes.
3398+
3399+
3400+Hook Integration
3401+----------------
3402+
3403+There are several extant frameworks for hook execution, including
3404+
3405+ - charmhelpers.core.hookenv.Hooks
3406+ - charmhelpers.core.services.ServiceManager
3407+
3408+The storage classes are framework agnostic, one simple integration is
3409+via the HookData contextmanager. It will record the current hook
3410+execution environment (including relation data, config data, etc.),
3411+setup a transaction and allow easy access to the changes from
3412+previously seen values. One consequence of the integration is the
3413+reservation of particular keys ('rels', 'unit', 'env', 'config',
3414+'charm_revisions') for their respective values.
3415+
3416+Here's a fully worked integration example using hookenv.Hooks::
3417+
3418+ from charmhelper.core import hookenv, unitdata
3419+
3420+ hook_data = unitdata.HookData()
3421+ db = unitdata.kv()
3422+ hooks = hookenv.Hooks()
3423+
3424+ @hooks.hook
3425+ def config_changed():
3426+ # Print all changes to configuration from previously seen
3427+ # values.
3428+ for changed, (prev, cur) in hook_data.conf.items():
3429+ print('config changed', changed,
3430+ 'previous value', prev,
3431+ 'current value', cur)
3432+
3433+ # Get some unit specific bookeeping
3434+ if not db.get('pkg_key'):
3435+ key = urllib.urlopen('https://example.com/pkg_key').read()
3436+ db.set('pkg_key', key)
3437+
3438+ # Directly access all charm config as a mapping.
3439+ conf = db.getrange('config', True)
3440+
3441+ # Directly access all relation data as a mapping
3442+ rels = db.getrange('rels', True)
3443+
3444+ if __name__ == '__main__':
3445+ with hook_data():
3446+ hook.execute()
3447+
3448+
3449+A more basic integration is via the hook_scope context manager which simply
3450+manages transaction scope (and records hook name, and timestamp)::
3451+
3452+ >>> from unitdata import kv
3453+ >>> db = kv()
3454+ >>> with db.hook_scope('install'):
3455+ ... # do work, in transactional scope.
3456+ ... db.set('x', 1)
3457+ >>> db.get('x')
3458+ 1
3459+
3460+
3461+Usage
3462+-----
3463+
3464+Values are automatically json de/serialized to preserve basic typing
3465+and complex data struct capabilities (dicts, lists, ints, booleans, etc).
3466+
3467+Individual values can be manipulated via get/set::
3468+
3469+ >>> kv.set('y', True)
3470+ >>> kv.get('y')
3471+ True
3472+
3473+ # We can set complex values (dicts, lists) as a single key.
3474+ >>> kv.set('config', {'a': 1, 'b': True'})
3475+
3476+ # Also supports returning dictionaries as a record which
3477+ # provides attribute access.
3478+ >>> config = kv.get('config', record=True)
3479+ >>> config.b
3480+ True
3481+
3482+
3483+Groups of keys can be manipulated with update/getrange::
3484+
3485+ >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
3486+ >>> kv.getrange('gui.', strip=True)
3487+ {'z': 1, 'y': 2}
3488+
3489+When updating values, its very helpful to understand which values
3490+have actually changed and how have they changed. The storage
3491+provides a delta method to provide for this::
3492+
3493+ >>> data = {'debug': True, 'option': 2}
3494+ >>> delta = kv.delta(data, 'config.')
3495+ >>> delta.debug.previous
3496+ None
3497+ >>> delta.debug.current
3498+ True
3499+ >>> delta
3500+ {'debug': (None, True), 'option': (None, 2)}
3501+
3502+Note the delta method does not persist the actual change, it needs to
3503+be explicitly saved via 'update' method::
3504+
3505+ >>> kv.update(data, 'config.')
3506+
3507+Values modified in the context of a hook scope retain historical values
3508+associated to the hookname.
3509+
3510+ >>> with db.hook_scope('config-changed'):
3511+ ... db.set('x', 42)
3512+ >>> db.gethistory('x')
3513+ [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
3514+ (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
3515+
3516+"""
3517+
3518+import collections
3519+import contextlib
3520+import datetime
3521+import itertools
3522+import json
3523+import os
3524+import pprint
3525+import sqlite3
3526+import sys
3527+
3528+__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
3529+
3530+
3531+class Storage(object):
3532+ """Simple key value database for local unit state within charms.
3533+
3534+ Modifications are not persisted unless :meth:`flush` is called.
3535+
3536+ To support dicts, lists, integer, floats, and booleans values
3537+ are automatically json encoded/decoded.
3538+ """
3539+ def __init__(self, path=None):
3540+ self.db_path = path
3541+ if path is None:
3542+ if 'UNIT_STATE_DB' in os.environ:
3543+ self.db_path = os.environ['UNIT_STATE_DB']
3544+ else:
3545+ self.db_path = os.path.join(
3546+ os.environ.get('CHARM_DIR', ''), '.unit-state.db')
3547+ self.conn = sqlite3.connect('%s' % self.db_path)
3548+ self.cursor = self.conn.cursor()
3549+ self.revision = None
3550+ self._closed = False
3551+ self._init()
3552+
3553+ def close(self):
3554+ if self._closed:
3555+ return
3556+ self.flush(False)
3557+ self.cursor.close()
3558+ self.conn.close()
3559+ self._closed = True
3560+
3561+ def get(self, key, default=None, record=False):
3562+ self.cursor.execute('select data from kv where key=?', [key])
3563+ result = self.cursor.fetchone()
3564+ if not result:
3565+ return default
3566+ if record:
3567+ return Record(json.loads(result[0]))
3568+ return json.loads(result[0])
3569+
3570+ def getrange(self, key_prefix, strip=False):
3571+ """
3572+ Get a range of keys starting with a common prefix as a mapping of
3573+ keys to values.
3574+
3575+ :param str key_prefix: Common prefix among all keys
3576+ :param bool strip: Optionally strip the common prefix from the key
3577+ names in the returned dict
3578+ :return dict: A (possibly empty) dict of key-value mappings
3579+ """
3580+ self.cursor.execute("select key, data from kv where key like ?",
3581+ ['%s%%' % key_prefix])
3582+ result = self.cursor.fetchall()
3583+
3584+ if not result:
3585+ return {}
3586+ if not strip:
3587+ key_prefix = ''
3588+ return dict([
3589+ (k[len(key_prefix):], json.loads(v)) for k, v in result])
3590+
3591+ def update(self, mapping, prefix=""):
3592+ """
3593+ Set the values of multiple keys at once.
3594+
3595+ :param dict mapping: Mapping of keys to values
3596+ :param str prefix: Optional prefix to apply to all keys in `mapping`
3597+ before setting
3598+ """
3599+ for k, v in mapping.items():
3600+ self.set("%s%s" % (prefix, k), v)
3601+
3602+ def unset(self, key):
3603+ """
3604+ Remove a key from the database entirely.
3605+ """
3606+ self.cursor.execute('delete from kv where key=?', [key])
3607+ if self.revision and self.cursor.rowcount:
3608+ self.cursor.execute(
3609+ 'insert into kv_revisions values (?, ?, ?)',
3610+ [key, self.revision, json.dumps('DELETED')])
3611+
3612+ def unsetrange(self, keys=None, prefix=""):
3613+ """
3614+ Remove a range of keys starting with a common prefix, from the database
3615+ entirely.
3616+
3617+ :param list keys: List of keys to remove.
3618+ :param str prefix: Optional prefix to apply to all keys in ``keys``
3619+ before removing.
3620+ """
3621+ if keys is not None:
3622+ keys = ['%s%s' % (prefix, key) for key in keys]
3623+ self.cursor.execute('delete from kv where key in (%s)' % ','.join(['?'] * len(keys)), keys)
3624+ if self.revision and self.cursor.rowcount:
3625+ self.cursor.execute(
3626+ 'insert into kv_revisions values %s' % ','.join(['(?, ?, ?)'] * len(keys)),
3627+ list(itertools.chain.from_iterable((key, self.revision, json.dumps('DELETED')) for key in keys)))
3628+ else:
3629+ self.cursor.execute('delete from kv where key like ?',
3630+ ['%s%%' % prefix])
3631+ if self.revision and self.cursor.rowcount:
3632+ self.cursor.execute(
3633+ 'insert into kv_revisions values (?, ?, ?)',
3634+ ['%s%%' % prefix, self.revision, json.dumps('DELETED')])
3635+
3636+ def set(self, key, value):
3637+ """
3638+ Set a value in the database.
3639+
3640+ :param str key: Key to set the value for
3641+ :param value: Any JSON-serializable value to be set
3642+ """
3643+ serialized = json.dumps(value)
3644+
3645+ self.cursor.execute('select data from kv where key=?', [key])
3646+ exists = self.cursor.fetchone()
3647+
3648+ # Skip mutations to the same value
3649+ if exists:
3650+ if exists[0] == serialized:
3651+ return value
3652+
3653+ if not exists:
3654+ self.cursor.execute(
3655+ 'insert into kv (key, data) values (?, ?)',
3656+ (key, serialized))
3657+ else:
3658+ self.cursor.execute('''
3659+ update kv
3660+ set data = ?
3661+ where key = ?''', [serialized, key])
3662+
3663+ # Save
3664+ if not self.revision:
3665+ return value
3666+
3667+ self.cursor.execute(
3668+ 'select 1 from kv_revisions where key=? and revision=?',
3669+ [key, self.revision])
3670+ exists = self.cursor.fetchone()
3671+
3672+ if not exists:
3673+ self.cursor.execute(
3674+ '''insert into kv_revisions (
3675+ revision, key, data) values (?, ?, ?)''',
3676+ (self.revision, key, serialized))
3677+ else:
3678+ self.cursor.execute(
3679+ '''
3680+ update kv_revisions
3681+ set data = ?
3682+ where key = ?
3683+ and revision = ?''',
3684+ [serialized, key, self.revision])
3685+
3686+ return value
3687+
3688+ def delta(self, mapping, prefix):
3689+ """
3690+ return a delta containing values that have changed.
3691+ """
3692+ previous = self.getrange(prefix, strip=True)
3693+ if not previous:
3694+ pk = set()
3695+ else:
3696+ pk = set(previous.keys())
3697+ ck = set(mapping.keys())
3698+ delta = DeltaSet()
3699+
3700+ # added
3701+ for k in ck.difference(pk):
3702+ delta[k] = Delta(None, mapping[k])
3703+
3704+ # removed
3705+ for k in pk.difference(ck):
3706+ delta[k] = Delta(previous[k], None)
3707+
3708+ # changed
3709+ for k in pk.intersection(ck):
3710+ c = mapping[k]
3711+ p = previous[k]
3712+ if c != p:
3713+ delta[k] = Delta(p, c)
3714+
3715+ return delta
3716+
3717+ @contextlib.contextmanager
3718+ def hook_scope(self, name=""):
3719+ """Scope all future interactions to the current hook execution
3720+ revision."""
3721+ assert not self.revision
3722+ self.cursor.execute(
3723+ 'insert into hooks (hook, date) values (?, ?)',
3724+ (name or sys.argv[0],
3725+ datetime.datetime.utcnow().isoformat()))
3726+ self.revision = self.cursor.lastrowid
3727+ try:
3728+ yield self.revision
3729+ self.revision = None
3730+ except:
3731+ self.flush(False)
3732+ self.revision = None
3733+ raise
3734+ else:
3735+ self.flush()
3736+
3737+ def flush(self, save=True):
3738+ if save:
3739+ self.conn.commit()
3740+ elif self._closed:
3741+ return
3742+ else:
3743+ self.conn.rollback()
3744+
3745+ def _init(self):
3746+ self.cursor.execute('''
3747+ create table if not exists kv (
3748+ key text,
3749+ data text,
3750+ primary key (key)
3751+ )''')
3752+ self.cursor.execute('''
3753+ create table if not exists kv_revisions (
3754+ key text,
3755+ revision integer,
3756+ data text,
3757+ primary key (key, revision)
3758+ )''')
3759+ self.cursor.execute('''
3760+ create table if not exists hooks (
3761+ version integer primary key autoincrement,
3762+ hook text,
3763+ date text
3764+ )''')
3765+ self.conn.commit()
3766+
3767+ def gethistory(self, key, deserialize=False):
3768+ self.cursor.execute(
3769+ '''
3770+ select kv.revision, kv.key, kv.data, h.hook, h.date
3771+ from kv_revisions kv,
3772+ hooks h
3773+ where kv.key=?
3774+ and kv.revision = h.version
3775+ ''', [key])
3776+ if deserialize is False:
3777+ return self.cursor.fetchall()
3778+ return map(_parse_history, self.cursor.fetchall())
3779+
3780+ def debug(self, fh=sys.stderr):
3781+ self.cursor.execute('select * from kv')
3782+ pprint.pprint(self.cursor.fetchall(), stream=fh)
3783+ self.cursor.execute('select * from kv_revisions')
3784+ pprint.pprint(self.cursor.fetchall(), stream=fh)
3785+
3786+
3787+def _parse_history(d):
3788+ return (d[0], d[1], json.loads(d[2]), d[3],
3789+ datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
3790+
3791+
3792+class HookData(object):
3793+ """Simple integration for existing hook exec frameworks.
3794+
3795+ Records all unit information, and stores deltas for processing
3796+ by the hook.
3797+
3798+ Sample::
3799+
3800+ from charmhelper.core import hookenv, unitdata
3801+
3802+ changes = unitdata.HookData()
3803+ db = unitdata.kv()
3804+ hooks = hookenv.Hooks()
3805+
3806+ @hooks.hook
3807+ def config_changed():
3808+ # View all changes to configuration
3809+ for changed, (prev, cur) in changes.conf.items():
3810+ print('config changed', changed,
3811+ 'previous value', prev,
3812+ 'current value', cur)
3813+
3814+ # Get some unit specific bookeeping
3815+ if not db.get('pkg_key'):
3816+ key = urllib.urlopen('https://example.com/pkg_key').read()
3817+ db.set('pkg_key', key)
3818+
3819+ if __name__ == '__main__':
3820+ with changes():
3821+ hook.execute()
3822+
3823+ """
3824+ def __init__(self):
3825+ self.kv = kv()
3826+ self.conf = None
3827+ self.rels = None
3828+
3829+ @contextlib.contextmanager
3830+ def __call__(self):
3831+ from charmhelpers.core import hookenv
3832+ hook_name = hookenv.hook_name()
3833+
3834+ with self.kv.hook_scope(hook_name):
3835+ self._record_charm_version(hookenv.charm_dir())
3836+ delta_config, delta_relation = self._record_hook(hookenv)
3837+ yield self.kv, delta_config, delta_relation
3838+
3839+ def _record_charm_version(self, charm_dir):
3840+ # Record revisions.. charm revisions are meaningless
3841+ # to charm authors as they don't control the revision.
3842+ # so logic dependnent on revision is not particularly
3843+ # useful, however it is useful for debugging analysis.
3844+ charm_rev = open(
3845+ os.path.join(charm_dir, 'revision')).read().strip()
3846+ charm_rev = charm_rev or '0'
3847+ revs = self.kv.get('charm_revisions', [])
3848+ if charm_rev not in revs:
3849+ revs.append(charm_rev.strip() or '0')
3850+ self.kv.set('charm_revisions', revs)
3851+
3852+ def _record_hook(self, hookenv):
3853+ data = hookenv.execution_environment()
3854+ self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
3855+ self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
3856+ self.kv.set('env', dict(data['env']))
3857+ self.kv.set('unit', data['unit'])
3858+ self.kv.set('relid', data.get('relid'))
3859+ return conf_delta, rels_delta
3860+
3861+
3862+class Record(dict):
3863+
3864+ __slots__ = ()
3865+
3866+ def __getattr__(self, k):
3867+ if k in self:
3868+ return self[k]
3869+ raise AttributeError(k)
3870+
3871+
3872+class DeltaSet(Record):
3873+
3874+ __slots__ = ()
3875+
3876+
3877+Delta = collections.namedtuple('Delta', ['previous', 'current'])
3878+
3879+
3880+_KV = None
3881+
3882+
3883+def kv():
3884+ global _KV
3885+ if _KV is None:
3886+ _KV = Storage()
3887+ return _KV
3888
3889=== modified file 'hooks/charmhelpers/fetch/__init__.py'
3890--- hooks/charmhelpers/fetch/__init__.py 2014-10-24 16:35:00 +0000
3891+++ hooks/charmhelpers/fetch/__init__.py 2017-05-11 14:56:57 +0000
3892@@ -1,71 +1,31 @@
3893+# Copyright 2014-2015 Canonical Limited.
3894+#
3895+# Licensed under the Apache License, Version 2.0 (the "License");
3896+# you may not use this file except in compliance with the License.
3897+# You may obtain a copy of the License at
3898+#
3899+# http://www.apache.org/licenses/LICENSE-2.0
3900+#
3901+# Unless required by applicable law or agreed to in writing, software
3902+# distributed under the License is distributed on an "AS IS" BASIS,
3903+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3904+# See the License for the specific language governing permissions and
3905+# limitations under the License.
3906+
3907 import importlib
3908-from tempfile import NamedTemporaryFile
3909-import time
3910+from charmhelpers.osplatform import get_platform
3911 from yaml import safe_load
3912-from charmhelpers.core.host import (
3913- lsb_release
3914-)
3915-from urlparse import (
3916- urlparse,
3917- urlunparse,
3918-)
3919-import subprocess
3920 from charmhelpers.core.hookenv import (
3921 config,
3922 log,
3923 )
3924-import os
3925-
3926-
3927-CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
3928-deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
3929-"""
3930-PROPOSED_POCKET = """# Proposed
3931-deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
3932-"""
3933-CLOUD_ARCHIVE_POCKETS = {
3934- # Folsom
3935- 'folsom': 'precise-updates/folsom',
3936- 'precise-folsom': 'precise-updates/folsom',
3937- 'precise-folsom/updates': 'precise-updates/folsom',
3938- 'precise-updates/folsom': 'precise-updates/folsom',
3939- 'folsom/proposed': 'precise-proposed/folsom',
3940- 'precise-folsom/proposed': 'precise-proposed/folsom',
3941- 'precise-proposed/folsom': 'precise-proposed/folsom',
3942- # Grizzly
3943- 'grizzly': 'precise-updates/grizzly',
3944- 'precise-grizzly': 'precise-updates/grizzly',
3945- 'precise-grizzly/updates': 'precise-updates/grizzly',
3946- 'precise-updates/grizzly': 'precise-updates/grizzly',
3947- 'grizzly/proposed': 'precise-proposed/grizzly',
3948- 'precise-grizzly/proposed': 'precise-proposed/grizzly',
3949- 'precise-proposed/grizzly': 'precise-proposed/grizzly',
3950- # Havana
3951- 'havana': 'precise-updates/havana',
3952- 'precise-havana': 'precise-updates/havana',
3953- 'precise-havana/updates': 'precise-updates/havana',
3954- 'precise-updates/havana': 'precise-updates/havana',
3955- 'havana/proposed': 'precise-proposed/havana',
3956- 'precise-havana/proposed': 'precise-proposed/havana',
3957- 'precise-proposed/havana': 'precise-proposed/havana',
3958- # Icehouse
3959- 'icehouse': 'precise-updates/icehouse',
3960- 'precise-icehouse': 'precise-updates/icehouse',
3961- 'precise-icehouse/updates': 'precise-updates/icehouse',
3962- 'precise-updates/icehouse': 'precise-updates/icehouse',
3963- 'icehouse/proposed': 'precise-proposed/icehouse',
3964- 'precise-icehouse/proposed': 'precise-proposed/icehouse',
3965- 'precise-proposed/icehouse': 'precise-proposed/icehouse',
3966- # Juno
3967- 'juno': 'trusty-updates/juno',
3968- 'trusty-juno': 'trusty-updates/juno',
3969- 'trusty-juno/updates': 'trusty-updates/juno',
3970- 'trusty-updates/juno': 'trusty-updates/juno',
3971- 'juno/proposed': 'trusty-proposed/juno',
3972- 'juno/proposed': 'trusty-proposed/juno',
3973- 'trusty-juno/proposed': 'trusty-proposed/juno',
3974- 'trusty-proposed/juno': 'trusty-proposed/juno',
3975-}
3976+
3977+import six
3978+if six.PY3:
3979+ from urllib.parse import urlparse, urlunparse
3980+else:
3981+ from urlparse import urlparse, urlunparse
3982+
3983
3984 # The order of this list is very important. Handlers should be listed in from
3985 # least- to most-specific URL matching.
3986@@ -75,10 +35,6 @@
3987 'charmhelpers.fetch.giturl.GitUrlFetchHandler',
3988 )
3989
3990-APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
3991-APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
3992-APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
3993-
3994
3995 class SourceConfigError(Exception):
3996 pass
3997@@ -116,172 +72,38 @@
3998 return urlunparse(parts)
3999
4000
4001-def filter_installed_packages(packages):
4002- """Returns a list of packages that require installation"""
4003- cache = apt_cache()
4004- _pkgs = []
4005- for package in packages:
4006- try:
4007- p = cache[package]
4008- p.current_ver or _pkgs.append(package)
4009- except KeyError:
4010- log('Package {} has no installation candidate.'.format(package),
4011- level='WARNING')
4012- _pkgs.append(package)
4013- return _pkgs
4014-
4015-
4016-def apt_cache(in_memory=True):
4017- """Build and return an apt cache"""
4018- import apt_pkg
4019- apt_pkg.init()
4020- if in_memory:
4021- apt_pkg.config.set("Dir::Cache::pkgcache", "")
4022- apt_pkg.config.set("Dir::Cache::srcpkgcache", "")
4023- return apt_pkg.Cache()
4024-
4025-
4026-def apt_install(packages, options=None, fatal=False):
4027- """Install one or more packages"""
4028- if options is None:
4029- options = ['--option=Dpkg::Options::=--force-confold']
4030-
4031- cmd = ['apt-get', '--assume-yes']
4032- cmd.extend(options)
4033- cmd.append('install')
4034- if isinstance(packages, basestring):
4035- cmd.append(packages)
4036- else:
4037- cmd.extend(packages)
4038- log("Installing {} with options: {}".format(packages,
4039- options))
4040- _run_apt_command(cmd, fatal)
4041-
4042-
4043-def apt_upgrade(options=None, fatal=False, dist=False):
4044- """Upgrade all packages"""
4045- if options is None:
4046- options = ['--option=Dpkg::Options::=--force-confold']
4047-
4048- cmd = ['apt-get', '--assume-yes']
4049- cmd.extend(options)
4050- if dist:
4051- cmd.append('dist-upgrade')
4052- else:
4053- cmd.append('upgrade')
4054- log("Upgrading with options: {}".format(options))
4055- _run_apt_command(cmd, fatal)
4056-
4057-
4058-def apt_update(fatal=False):
4059- """Update local apt cache"""
4060- cmd = ['apt-get', 'update']
4061- _run_apt_command(cmd, fatal)
4062-
4063-
4064-def apt_purge(packages, fatal=False):
4065- """Purge one or more packages"""
4066- cmd = ['apt-get', '--assume-yes', 'purge']
4067- if isinstance(packages, basestring):
4068- cmd.append(packages)
4069- else:
4070- cmd.extend(packages)
4071- log("Purging {}".format(packages))
4072- _run_apt_command(cmd, fatal)
4073-
4074-
4075-def apt_hold(packages, fatal=False):
4076- """Hold one or more packages"""
4077- cmd = ['apt-mark', 'hold']
4078- if isinstance(packages, basestring):
4079- cmd.append(packages)
4080- else:
4081- cmd.extend(packages)
4082- log("Holding {}".format(packages))
4083-
4084- if fatal:
4085- subprocess.check_call(cmd)
4086- else:
4087- subprocess.call(cmd)
4088-
4089-
4090-def add_source(source, key=None):
4091- """Add a package source to this system.
4092-
4093- @param source: a URL or sources.list entry, as supported by
4094- add-apt-repository(1). Examples::
4095-
4096- ppa:charmers/example
4097- deb https://stub:key@private.example.com/ubuntu trusty main
4098-
4099- In addition:
4100- 'proposed:' may be used to enable the standard 'proposed'
4101- pocket for the release.
4102- 'cloud:' may be used to activate official cloud archive pockets,
4103- such as 'cloud:icehouse'
4104- 'distro' may be used as a noop
4105-
4106- @param key: A key to be added to the system's APT keyring and used
4107- to verify the signatures on packages. Ideally, this should be an
4108- ASCII format GPG public key including the block headers. A GPG key
4109- id may also be used, but be aware that only insecure protocols are
4110- available to retrieve the actual public key from a public keyserver
4111- placing your Juju environment at risk. ppa and cloud archive keys
4112- are securely added automtically, so sould not be provided.
4113- """
4114- if source is None:
4115- log('Source is not present. Skipping')
4116- return
4117-
4118- if (source.startswith('ppa:') or
4119- source.startswith('http') or
4120- source.startswith('deb ') or
4121- source.startswith('cloud-archive:')):
4122- subprocess.check_call(['add-apt-repository', '--yes', source])
4123- elif source.startswith('cloud:'):
4124- apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
4125- fatal=True)
4126- pocket = source.split(':')[-1]
4127- if pocket not in CLOUD_ARCHIVE_POCKETS:
4128- raise SourceConfigError(
4129- 'Unsupported cloud: source option %s' %
4130- pocket)
4131- actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
4132- with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
4133- apt.write(CLOUD_ARCHIVE.format(actual_pocket))
4134- elif source == 'proposed':
4135- release = lsb_release()['DISTRIB_CODENAME']
4136- with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
4137- apt.write(PROPOSED_POCKET.format(release))
4138- elif source == 'distro':
4139- pass
4140- else:
4141- log("Unknown source: {!r}".format(source))
4142-
4143- if key:
4144- if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
4145- with NamedTemporaryFile() as key_file:
4146- key_file.write(key)
4147- key_file.flush()
4148- key_file.seek(0)
4149- subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
4150- else:
4151- # Note that hkp: is in no way a secure protocol. Using a
4152- # GPG key id is pointless from a security POV unless you
4153- # absolutely trust your network and DNS.
4154- subprocess.check_call(['apt-key', 'adv', '--keyserver',
4155- 'hkp://keyserver.ubuntu.com:80', '--recv',
4156- key])
4157+__platform__ = get_platform()
4158+module = "charmhelpers.fetch.%s" % __platform__
4159+fetch = importlib.import_module(module)
4160+
4161+filter_installed_packages = fetch.filter_installed_packages
4162+install = fetch.install
4163+upgrade = fetch.upgrade
4164+update = fetch.update
4165+purge = fetch.purge
4166+add_source = fetch.add_source
4167+
4168+if __platform__ == "ubuntu":
4169+ apt_cache = fetch.apt_cache
4170+ apt_install = fetch.install
4171+ apt_update = fetch.update
4172+ apt_upgrade = fetch.upgrade
4173+ apt_purge = fetch.purge
4174+ apt_mark = fetch.apt_mark
4175+ apt_hold = fetch.apt_hold
4176+ apt_unhold = fetch.apt_unhold
4177+ get_upstream_version = fetch.get_upstream_version
4178+elif __platform__ == "centos":
4179+ yum_search = fetch.yum_search
4180
4181
4182 def configure_sources(update=False,
4183 sources_var='install_sources',
4184 keys_var='install_keys'):
4185- """
4186- Configure multiple sources from charm configuration.
4187+ """Configure multiple sources from charm configuration.
4188
4189 The lists are encoded as yaml fragments in the configuration.
4190- The frament needs to be included as a string. Sources and their
4191+ The fragment needs to be included as a string. Sources and their
4192 corresponding keys are of the types supported by add_source().
4193
4194 Example config:
4195@@ -297,14 +119,14 @@
4196 sources = safe_load((config(sources_var) or '').strip()) or []
4197 keys = safe_load((config(keys_var) or '').strip()) or None
4198
4199- if isinstance(sources, basestring):
4200+ if isinstance(sources, six.string_types):
4201 sources = [sources]
4202
4203 if keys is None:
4204 for source in sources:
4205 add_source(source, None)
4206 else:
4207- if isinstance(keys, basestring):
4208+ if isinstance(keys, six.string_types):
4209 keys = [keys]
4210
4211 if len(sources) != len(keys):
4212@@ -313,12 +135,11 @@
4213 for source, key in zip(sources, keys):
4214 add_source(source, key)
4215 if update:
4216- apt_update(fatal=True)
4217+ fetch.update(fatal=True)
4218
4219
4220 def install_remote(source, *args, **kwargs):
4221- """
4222- Install a file tree from a remote source
4223+ """Install a file tree from a remote source.
4224
4225 The specified source should be a url of the form:
4226 scheme://[host]/path[#[option=value][&...]]
4227@@ -341,18 +162,17 @@
4228 # We ONLY check for True here because can_handle may return a string
4229 # explaining why it can't handle a given source.
4230 handlers = [h for h in plugins() if h.can_handle(source) is True]
4231- installed_to = None
4232 for handler in handlers:
4233 try:
4234- installed_to = handler.install(source, *args, **kwargs)
4235- except UnhandledSource:
4236- pass
4237- if not installed_to:
4238- raise UnhandledSource("No handler found for source {}".format(source))
4239- return installed_to
4240+ return handler.install(source, *args, **kwargs)
4241+ except UnhandledSource as e:
4242+ log('Install source attempt unsuccessful: {}'.format(e),
4243+ level='WARNING')
4244+ raise UnhandledSource("No handler found for source {}".format(source))
4245
4246
4247 def install_from_config(config_var_name):
4248+ """Install a file from config."""
4249 charm_config = config()
4250 source = charm_config[config_var_name]
4251 return install_remote(source)
4252@@ -369,46 +189,9 @@
4253 importlib.import_module(package),
4254 classname)
4255 plugin_list.append(handler_class())
4256- except (ImportError, AttributeError):
4257+ except NotImplementedError:
4258 # Skip missing plugins so that they can be ommitted from
4259 # installation if desired
4260 log("FetchHandler {} not found, skipping plugin".format(
4261 handler_name))
4262 return plugin_list
4263-
4264-
4265-def _run_apt_command(cmd, fatal=False):
4266- """
4267- Run an APT command, checking output and retrying if the fatal flag is set
4268- to True.
4269-
4270- :param: cmd: str: The apt command to run.
4271- :param: fatal: bool: Whether the command's output should be checked and
4272- retried.
4273- """
4274- env = os.environ.copy()
4275-
4276- if 'DEBIAN_FRONTEND' not in env:
4277- env['DEBIAN_FRONTEND'] = 'noninteractive'
4278-
4279- if fatal:
4280- retry_count = 0
4281- result = None
4282-
4283- # If the command is considered "fatal", we need to retry if the apt
4284- # lock was not acquired.
4285-
4286- while result is None or result == APT_NO_LOCK:
4287- try:
4288- result = subprocess.check_call(cmd, env=env)
4289- except subprocess.CalledProcessError, e:
4290- retry_count = retry_count + 1
4291- if retry_count > APT_NO_LOCK_RETRY_COUNT:
4292- raise
4293- result = e.returncode
4294- log("Couldn't acquire DPKG lock. Will retry in {} seconds."
4295- "".format(APT_NO_LOCK_RETRY_DELAY))
4296- time.sleep(APT_NO_LOCK_RETRY_DELAY)
4297-
4298- else:
4299- subprocess.call(cmd, env=env)
4300
4301=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
4302--- hooks/charmhelpers/fetch/archiveurl.py 2014-10-24 16:35:00 +0000
4303+++ hooks/charmhelpers/fetch/archiveurl.py 2017-05-11 14:56:57 +0000
4304@@ -1,8 +1,20 @@
4305+# Copyright 2014-2015 Canonical Limited.
4306+#
4307+# Licensed under the Apache License, Version 2.0 (the "License");
4308+# you may not use this file except in compliance with the License.
4309+# You may obtain a copy of the License at
4310+#
4311+# http://www.apache.org/licenses/LICENSE-2.0
4312+#
4313+# Unless required by applicable law or agreed to in writing, software
4314+# distributed under the License is distributed on an "AS IS" BASIS,
4315+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4316+# See the License for the specific language governing permissions and
4317+# limitations under the License.
4318+
4319 import os
4320-import urllib2
4321-from urllib import urlretrieve
4322-import urlparse
4323 import hashlib
4324+import re
4325
4326 from charmhelpers.fetch import (
4327 BaseFetchHandler,
4328@@ -14,6 +26,41 @@
4329 )
4330 from charmhelpers.core.host import mkdir, check_hash
4331
4332+import six
4333+if six.PY3:
4334+ from urllib.request import (
4335+ build_opener, install_opener, urlopen, urlretrieve,
4336+ HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler,
4337+ )
4338+ from urllib.parse import urlparse, urlunparse, parse_qs
4339+ from urllib.error import URLError
4340+else:
4341+ from urllib import urlretrieve
4342+ from urllib2 import (
4343+ build_opener, install_opener, urlopen,
4344+ HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler,
4345+ URLError
4346+ )
4347+ from urlparse import urlparse, urlunparse, parse_qs
4348+
4349+
4350+def splituser(host):
4351+ '''urllib.splituser(), but six's support of this seems broken'''
4352+ _userprog = re.compile('^(.*)@(.*)$')
4353+ match = _userprog.match(host)
4354+ if match:
4355+ return match.group(1, 2)
4356+ return None, host
4357+
4358+
4359+def splitpasswd(user):
4360+ '''urllib.splitpasswd(), but six's support of this is missing'''
4361+ _passwdprog = re.compile('^([^:]*):(.*)$', re.S)
4362+ match = _passwdprog.match(user)
4363+ if match:
4364+ return match.group(1, 2)
4365+ return user, None
4366+
4367
4368 class ArchiveUrlFetchHandler(BaseFetchHandler):
4369 """
4370@@ -28,6 +75,8 @@
4371 def can_handle(self, source):
4372 url_parts = self.parse_url(source)
4373 if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
4374+ # XXX: Why is this returning a boolean and a string? It's
4375+ # doomed to fail since "bool(can_handle('foo://'))" will be True.
4376 return "Wrong source type"
4377 if get_archive_handler(self.base_url(source)):
4378 return True
4379@@ -42,22 +91,22 @@
4380 """
4381 # propogate all exceptions
4382 # URLError, OSError, etc
4383- proto, netloc, path, params, query, fragment = urlparse.urlparse(source)
4384+ proto, netloc, path, params, query, fragment = urlparse(source)
4385 if proto in ('http', 'https'):
4386- auth, barehost = urllib2.splituser(netloc)
4387+ auth, barehost = splituser(netloc)
4388 if auth is not None:
4389- source = urlparse.urlunparse((proto, barehost, path, params, query, fragment))
4390- username, password = urllib2.splitpasswd(auth)
4391- passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
4392+ source = urlunparse((proto, barehost, path, params, query, fragment))
4393+ username, password = splitpasswd(auth)
4394+ passman = HTTPPasswordMgrWithDefaultRealm()
4395 # Realm is set to None in add_password to force the username and password
4396 # to be used whatever the realm
4397 passman.add_password(None, source, username, password)
4398- authhandler = urllib2.HTTPBasicAuthHandler(passman)
4399- opener = urllib2.build_opener(authhandler)
4400- urllib2.install_opener(opener)
4401- response = urllib2.urlopen(source)
4402+ authhandler = HTTPBasicAuthHandler(passman)
4403+ opener = build_opener(authhandler)
4404+ install_opener(opener)
4405+ response = urlopen(source)
4406 try:
4407- with open(dest, 'w') as dest_file:
4408+ with open(dest, 'wb') as dest_file:
4409 dest_file.write(response.read())
4410 except Exception as e:
4411 if os.path.isfile(dest):
4412@@ -91,18 +140,26 @@
4413 url_parts = self.parse_url(source)
4414 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched')
4415 if not os.path.exists(dest_dir):
4416- mkdir(dest_dir, perms=0755)
4417+ mkdir(dest_dir, perms=0o755)
4418 dld_file = os.path.join(dest_dir, os.path.basename(url_parts.path))
4419 try:
4420 self.download(source, dld_file)
4421- except urllib2.URLError as e:
4422+ except URLError as e:
4423 raise UnhandledSource(e.reason)
4424 except OSError as e:
4425 raise UnhandledSource(e.strerror)
4426- options = urlparse.parse_qs(url_parts.fragment)
4427+ options = parse_qs(url_parts.fragment)
4428 for key, value in options.items():
4429- if key in hashlib.algorithms:
4430- check_hash(dld_file, value, key)
4431+ if not six.PY3:
4432+ algorithms = hashlib.algorithms
4433+ else:
4434+ algorithms = hashlib.algorithms_available
4435+ if key in algorithms:
4436+ if len(value) != 1:
4437+ raise TypeError(
4438+ "Expected 1 hash value, not %d" % len(value))
4439+ expected = value[0]
4440+ check_hash(dld_file, expected, key)
4441 if checksum:
4442 check_hash(dld_file, checksum, hash_type)
4443 return extract(dld_file, dest)
4444
4445=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
4446--- hooks/charmhelpers/fetch/bzrurl.py 2014-10-24 16:35:00 +0000
4447+++ hooks/charmhelpers/fetch/bzrurl.py 2017-05-11 14:56:57 +0000
4448@@ -1,50 +1,76 @@
4449+# Copyright 2014-2015 Canonical Limited.
4450+#
4451+# Licensed under the Apache License, Version 2.0 (the "License");
4452+# you may not use this file except in compliance with the License.
4453+# You may obtain a copy of the License at
4454+#
4455+# http://www.apache.org/licenses/LICENSE-2.0
4456+#
4457+# Unless required by applicable law or agreed to in writing, software
4458+# distributed under the License is distributed on an "AS IS" BASIS,
4459+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4460+# See the License for the specific language governing permissions and
4461+# limitations under the License.
4462+
4463 import os
4464+from subprocess import check_call
4465 from charmhelpers.fetch import (
4466 BaseFetchHandler,
4467- UnhandledSource
4468+ UnhandledSource,
4469+ filter_installed_packages,
4470+ install,
4471 )
4472 from charmhelpers.core.host import mkdir
4473
4474-try:
4475- from bzrlib.branch import Branch
4476-except ImportError:
4477- from charmhelpers.fetch import apt_install
4478- apt_install("python-bzrlib")
4479- from bzrlib.branch import Branch
4480+
4481+if filter_installed_packages(['bzr']) != []:
4482+ install(['bzr'])
4483+ if filter_installed_packages(['bzr']) != []:
4484+ raise NotImplementedError('Unable to install bzr')
4485
4486
4487 class BzrUrlFetchHandler(BaseFetchHandler):
4488- """Handler for bazaar branches via generic and lp URLs"""
4489+ """Handler for bazaar branches via generic and lp URLs."""
4490+
4491 def can_handle(self, source):
4492 url_parts = self.parse_url(source)
4493- if url_parts.scheme not in ('bzr+ssh', 'lp'):
4494+ if url_parts.scheme not in ('bzr+ssh', 'lp', ''):
4495 return False
4496+ elif not url_parts.scheme:
4497+ return os.path.exists(os.path.join(source, '.bzr'))
4498 else:
4499 return True
4500
4501- def branch(self, source, dest):
4502- url_parts = self.parse_url(source)
4503- # If we use lp:branchname scheme we need to load plugins
4504+ def branch(self, source, dest, revno=None):
4505 if not self.can_handle(source):
4506 raise UnhandledSource("Cannot handle {}".format(source))
4507- if url_parts.scheme == "lp":
4508- from bzrlib.plugin import load_plugins
4509- load_plugins()
4510- try:
4511- remote_branch = Branch.open(source)
4512- remote_branch.bzrdir.sprout(dest).open_branch()
4513- except Exception as e:
4514- raise e
4515+ cmd_opts = []
4516+ if revno:
4517+ cmd_opts += ['-r', str(revno)]
4518+ if os.path.exists(dest):
4519+ cmd = ['bzr', 'pull']
4520+ cmd += cmd_opts
4521+ cmd += ['--overwrite', '-d', dest, source]
4522+ else:
4523+ cmd = ['bzr', 'branch']
4524+ cmd += cmd_opts
4525+ cmd += [source, dest]
4526+ check_call(cmd)
4527
4528- def install(self, source):
4529+ def install(self, source, dest=None, revno=None):
4530 url_parts = self.parse_url(source)
4531 branch_name = url_parts.path.strip("/").split("/")[-1]
4532- dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
4533- branch_name)
4534- if not os.path.exists(dest_dir):
4535- mkdir(dest_dir, perms=0755)
4536+ if dest:
4537+ dest_dir = os.path.join(dest, branch_name)
4538+ else:
4539+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
4540+ branch_name)
4541+
4542+ if dest and not os.path.exists(dest):
4543+ mkdir(dest, perms=0o755)
4544+
4545 try:
4546- self.branch(source, dest_dir)
4547+ self.branch(source, dest_dir, revno)
4548 except OSError as e:
4549 raise UnhandledSource(e.strerror)
4550 return dest_dir
4551
4552=== added file 'hooks/charmhelpers/fetch/centos.py'
4553--- hooks/charmhelpers/fetch/centos.py 1970-01-01 00:00:00 +0000
4554+++ hooks/charmhelpers/fetch/centos.py 2017-05-11 14:56:57 +0000
4555@@ -0,0 +1,171 @@
4556+# Copyright 2014-2015 Canonical Limited.
4557+#
4558+# Licensed under the Apache License, Version 2.0 (the "License");
4559+# you may not use this file except in compliance with the License.
4560+# You may obtain a copy of the License at
4561+#
4562+# http://www.apache.org/licenses/LICENSE-2.0
4563+#
4564+# Unless required by applicable law or agreed to in writing, software
4565+# distributed under the License is distributed on an "AS IS" BASIS,
4566+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4567+# See the License for the specific language governing permissions and
4568+# limitations under the License.
4569+
4570+import subprocess
4571+import os
4572+import time
4573+import six
4574+import yum
4575+
4576+from tempfile import NamedTemporaryFile
4577+from charmhelpers.core.hookenv import log
4578+
4579+YUM_NO_LOCK = 1 # The return code for "couldn't acquire lock" in YUM.
4580+YUM_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
4581+YUM_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
4582+
4583+
4584+def filter_installed_packages(packages):
4585+ """Return a list of packages that require installation."""
4586+ yb = yum.YumBase()
4587+ package_list = yb.doPackageLists()
4588+ temp_cache = {p.base_package_name: 1 for p in package_list['installed']}
4589+
4590+ _pkgs = [p for p in packages if not temp_cache.get(p, False)]
4591+ return _pkgs
4592+
4593+
4594+def install(packages, options=None, fatal=False):
4595+ """Install one or more packages."""
4596+ cmd = ['yum', '--assumeyes']
4597+ if options is not None:
4598+ cmd.extend(options)
4599+ cmd.append('install')
4600+ if isinstance(packages, six.string_types):
4601+ cmd.append(packages)
4602+ else:
4603+ cmd.extend(packages)
4604+ log("Installing {} with options: {}".format(packages,
4605+ options))
4606+ _run_yum_command(cmd, fatal)
4607+
4608+
4609+def upgrade(options=None, fatal=False, dist=False):
4610+ """Upgrade all packages."""
4611+ cmd = ['yum', '--assumeyes']
4612+ if options is not None:
4613+ cmd.extend(options)
4614+ cmd.append('upgrade')
4615+ log("Upgrading with options: {}".format(options))
4616+ _run_yum_command(cmd, fatal)
4617+
4618+
4619+def update(fatal=False):
4620+ """Update local yum cache."""
4621+ cmd = ['yum', '--assumeyes', 'update']
4622+ log("Update with fatal: {}".format(fatal))
4623+ _run_yum_command(cmd, fatal)
4624+
4625+
4626+def purge(packages, fatal=False):
4627+ """Purge one or more packages."""
4628+ cmd = ['yum', '--assumeyes', 'remove']
4629+ if isinstance(packages, six.string_types):
4630+ cmd.append(packages)
4631+ else:
4632+ cmd.extend(packages)
4633+ log("Purging {}".format(packages))
4634+ _run_yum_command(cmd, fatal)
4635+
4636+
4637+def yum_search(packages):
4638+ """Search for a package."""
4639+ output = {}
4640+ cmd = ['yum', 'search']
4641+ if isinstance(packages, six.string_types):
4642+ cmd.append(packages)
4643+ else:
4644+ cmd.extend(packages)
4645+ log("Searching for {}".format(packages))
4646+ result = subprocess.check_output(cmd)
4647+ for package in list(packages):
4648+ output[package] = package in result
4649+ return output
4650+
4651+
4652+def add_source(source, key=None):
4653+ """Add a package source to this system.
4654+
4655+ @param source: a URL with a rpm package
4656+
4657+ @param key: A key to be added to the system's keyring and used
4658+ to verify the signatures on packages. Ideally, this should be an
4659+ ASCII format GPG public key including the block headers. A GPG key
4660+ id may also be used, but be aware that only insecure protocols are
4661+ available to retrieve the actual public key from a public keyserver
4662+ placing your Juju environment at risk.
4663+ """
4664+ if source is None:
4665+ log('Source is not present. Skipping')
4666+ return
4667+
4668+ if source.startswith('http'):
4669+ directory = '/etc/yum.repos.d/'
4670+ for filename in os.listdir(directory):
4671+ with open(directory + filename, 'r') as rpm_file:
4672+ if source in rpm_file.read():
4673+ break
4674+ else:
4675+ log("Add source: {!r}".format(source))
4676+ # write in the charms.repo
4677+ with open(directory + 'Charms.repo', 'a') as rpm_file:
4678+ rpm_file.write('[%s]\n' % source[7:].replace('/', '_'))
4679+ rpm_file.write('name=%s\n' % source[7:])
4680+ rpm_file.write('baseurl=%s\n\n' % source)
4681+ else:
4682+ log("Unknown source: {!r}".format(source))
4683+
4684+ if key:
4685+ if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
4686+ with NamedTemporaryFile('w+') as key_file:
4687+ key_file.write(key)
4688+ key_file.flush()
4689+ key_file.seek(0)
4690+ subprocess.check_call(['rpm', '--import', key_file])
4691+ else:
4692+ subprocess.check_call(['rpm', '--import', key])
4693+
4694+
4695+def _run_yum_command(cmd, fatal=False):
4696+ """Run an YUM command.
4697+
4698+ Checks the output and retry if the fatal flag is set to True.
4699+
4700+ :param: cmd: str: The yum command to run.
4701+ :param: fatal: bool: Whether the command's output should be checked and
4702+ retried.
4703+ """
4704+ env = os.environ.copy()
4705+
4706+ if fatal:
4707+ retry_count = 0
4708+ result = None
4709+
4710+ # If the command is considered "fatal", we need to retry if the yum
4711+ # lock was not acquired.
4712+
4713+ while result is None or result == YUM_NO_LOCK:
4714+ try:
4715+ result = subprocess.check_call(cmd, env=env)
4716+ except subprocess.CalledProcessError as e:
4717+ retry_count = retry_count + 1
4718+ if retry_count > YUM_NO_LOCK_RETRY_COUNT:
4719+ raise
4720+ result = e.returncode
4721+ log("Couldn't acquire YUM lock. Will retry in {} seconds."
4722+ "".format(YUM_NO_LOCK_RETRY_DELAY))
4723+ time.sleep(YUM_NO_LOCK_RETRY_DELAY)
4724+
4725+ else:
4726+ subprocess.call(cmd, env=env)
4727
4728=== modified file 'hooks/charmhelpers/fetch/giturl.py'
4729--- hooks/charmhelpers/fetch/giturl.py 2014-10-24 16:35:00 +0000
4730+++ hooks/charmhelpers/fetch/giturl.py 2017-05-11 14:56:57 +0000
4731@@ -1,44 +1,69 @@
4732+# Copyright 2014-2015 Canonical Limited.
4733+#
4734+# Licensed under the Apache License, Version 2.0 (the "License");
4735+# you may not use this file except in compliance with the License.
4736+# You may obtain a copy of the License at
4737+#
4738+# http://www.apache.org/licenses/LICENSE-2.0
4739+#
4740+# Unless required by applicable law or agreed to in writing, software
4741+# distributed under the License is distributed on an "AS IS" BASIS,
4742+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4743+# See the License for the specific language governing permissions and
4744+# limitations under the License.
4745+
4746 import os
4747+from subprocess import check_call, CalledProcessError
4748 from charmhelpers.fetch import (
4749 BaseFetchHandler,
4750- UnhandledSource
4751+ UnhandledSource,
4752+ filter_installed_packages,
4753+ install,
4754 )
4755-from charmhelpers.core.host import mkdir
4756
4757-try:
4758- from git import Repo
4759-except ImportError:
4760- from charmhelpers.fetch import apt_install
4761- apt_install("python-git")
4762- from git import Repo
4763+if filter_installed_packages(['git']) != []:
4764+ install(['git'])
4765+ if filter_installed_packages(['git']) != []:
4766+ raise NotImplementedError('Unable to install git')
4767
4768
4769 class GitUrlFetchHandler(BaseFetchHandler):
4770- """Handler for git branches via generic and github URLs"""
4771+ """Handler for git branches via generic and github URLs."""
4772+
4773 def can_handle(self, source):
4774 url_parts = self.parse_url(source)
4775- #TODO (mattyw) no support for ssh git@ yet
4776- if url_parts.scheme not in ('http', 'https', 'git'):
4777+ # TODO (mattyw) no support for ssh git@ yet
4778+ if url_parts.scheme not in ('http', 'https', 'git', ''):
4779 return False
4780+ elif not url_parts.scheme:
4781+ return os.path.exists(os.path.join(source, '.git'))
4782 else:
4783 return True
4784
4785- def clone(self, source, dest, branch):
4786+ def clone(self, source, dest, branch="master", depth=None):
4787 if not self.can_handle(source):
4788 raise UnhandledSource("Cannot handle {}".format(source))
4789
4790- repo = Repo.clone_from(source, dest)
4791- repo.git.checkout(branch)
4792+ if os.path.exists(dest):
4793+ cmd = ['git', '-C', dest, 'pull', source, branch]
4794+ else:
4795+ cmd = ['git', 'clone', source, dest, '--branch', branch]
4796+ if depth:
4797+ cmd.extend(['--depth', depth])
4798+ check_call(cmd)
4799
4800- def install(self, source, branch="master"):
4801+ def install(self, source, branch="master", dest=None, depth=None):
4802 url_parts = self.parse_url(source)
4803 branch_name = url_parts.path.strip("/").split("/")[-1]
4804- dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
4805- branch_name)
4806- if not os.path.exists(dest_dir):
4807- mkdir(dest_dir, perms=0755)
4808+ if dest:
4809+ dest_dir = os.path.join(dest, branch_name)
4810+ else:
4811+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
4812+ branch_name)
4813 try:
4814- self.clone(source, dest_dir, branch)
4815+ self.clone(source, dest_dir, branch, depth)
4816+ except CalledProcessError as e:
4817+ raise UnhandledSource(e)
4818 except OSError as e:
4819 raise UnhandledSource(e.strerror)
4820 return dest_dir
4821
4822=== added file 'hooks/charmhelpers/fetch/snap.py'
4823--- hooks/charmhelpers/fetch/snap.py 1970-01-01 00:00:00 +0000
4824+++ hooks/charmhelpers/fetch/snap.py 2017-05-11 14:56:57 +0000
4825@@ -0,0 +1,122 @@
4826+# Copyright 2014-2017 Canonical Limited.
4827+#
4828+# Licensed under the Apache License, Version 2.0 (the "License");
4829+# you may not use this file except in compliance with the License.
4830+# You may obtain a copy of the License at
4831+#
4832+# http://www.apache.org/licenses/LICENSE-2.0
4833+#
4834+# Unless required by applicable law or agreed to in writing, software
4835+# distributed under the License is distributed on an "AS IS" BASIS,
4836+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4837+# See the License for the specific language governing permissions and
4838+# limitations under the License.
4839+"""
4840+Charm helpers snap for classic charms.
4841+
4842+If writing reactive charms, use the snap layer:
4843+https://lists.ubuntu.com/archives/snapcraft/2016-September/001114.html
4844+"""
4845+import subprocess
4846+from os import environ
4847+from time import sleep
4848+from charmhelpers.core.hookenv import log
4849+
4850+__author__ = 'Joseph Borg <joseph.borg@canonical.com>'
4851+
4852+SNAP_NO_LOCK = 1 # The return code for "couldn't acquire lock" in Snap (hopefully this will be improved).
4853+SNAP_NO_LOCK_RETRY_DELAY = 10 # Wait X seconds between Snap lock checks.
4854+SNAP_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
4855+
4856+
4857+class CouldNotAcquireLockException(Exception):
4858+ pass
4859+
4860+
4861+def _snap_exec(commands):
4862+ """
4863+ Execute snap commands.
4864+
4865+ :param commands: List commands
4866+ :return: Integer exit code
4867+ """
4868+ assert type(commands) == list
4869+
4870+ retry_count = 0
4871+ return_code = None
4872+
4873+ while return_code is None or return_code == SNAP_NO_LOCK:
4874+ try:
4875+ return_code = subprocess.check_call(['snap'] + commands, env=environ)
4876+ except subprocess.CalledProcessError as e:
4877+ retry_count += + 1
4878+ if retry_count > SNAP_NO_LOCK_RETRY_COUNT:
4879+ raise CouldNotAcquireLockException('Could not aquire lock after %s attempts' % SNAP_NO_LOCK_RETRY_COUNT)
4880+ return_code = e.returncode
4881+ log('Snap failed to acquire lock, trying again in %s seconds.' % SNAP_NO_LOCK_RETRY_DELAY, level='WARN')
4882+ sleep(SNAP_NO_LOCK_RETRY_DELAY)
4883+
4884+ return return_code
4885+
4886+
4887+def snap_install(packages, *flags):
4888+ """
4889+ Install a snap package.
4890+
4891+ :param packages: String or List String package name
4892+ :param flags: List String flags to pass to install command
4893+ :return: Integer return code from snap
4894+ """
4895+ if type(packages) is not list:
4896+ packages = [packages]
4897+
4898+ flags = list(flags)
4899+
4900+ message = 'Installing snap(s) "%s"' % ', '.join(packages)
4901+ if flags:
4902+ message += ' with option(s) "%s"' % ', '.join(flags)
4903+
4904+ log(message, level='INFO')
4905+ return _snap_exec(['install'] + flags + packages)
4906+
4907+
4908+def snap_remove(packages, *flags):
4909+ """
4910+ Remove a snap package.
4911+
4912+ :param packages: String or List String package name
4913+ :param flags: List String flags to pass to remove command
4914+ :return: Integer return code from snap
4915+ """
4916+ if type(packages) is not list:
4917+ packages = [packages]
4918+
4919+ flags = list(flags)
4920+
4921+ message = 'Removing snap(s) "%s"' % ', '.join(packages)
4922+ if flags:
4923+ message += ' with options "%s"' % ', '.join(flags)
4924+
4925+ log(message, level='INFO')
4926+ return _snap_exec(['remove'] + flags + packages)
4927+
4928+
4929+def snap_refresh(packages, *flags):
4930+ """
4931+ Refresh / Update snap package.
4932+
4933+ :param packages: String or List String package name
4934+ :param flags: List String flags to pass to refresh command
4935+ :return: Integer return code from snap
4936+ """
4937+ if type(packages) is not list:
4938+ packages = [packages]
4939+
4940+ flags = list(flags)
4941+
4942+ message = 'Refreshing snap(s) "%s"' % ', '.join(packages)
4943+ if flags:
4944+ message += ' with options "%s"' % ', '.join(flags)
4945+
4946+ log(message, level='INFO')
4947+ return _snap_exec(['refresh'] + flags + packages)
4948
4949=== added file 'hooks/charmhelpers/fetch/ubuntu.py'
4950--- hooks/charmhelpers/fetch/ubuntu.py 1970-01-01 00:00:00 +0000
4951+++ hooks/charmhelpers/fetch/ubuntu.py 2017-05-11 14:56:57 +0000
4952@@ -0,0 +1,364 @@
4953+# Copyright 2014-2015 Canonical Limited.
4954+#
4955+# Licensed under the Apache License, Version 2.0 (the "License");
4956+# you may not use this file except in compliance with the License.
4957+# You may obtain a copy of the License at
4958+#
4959+# http://www.apache.org/licenses/LICENSE-2.0
4960+#
4961+# Unless required by applicable law or agreed to in writing, software
4962+# distributed under the License is distributed on an "AS IS" BASIS,
4963+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4964+# See the License for the specific language governing permissions and
4965+# limitations under the License.
4966+
4967+import os
4968+import six
4969+import time
4970+import subprocess
4971+
4972+from tempfile import NamedTemporaryFile
4973+from charmhelpers.core.host import (
4974+ lsb_release
4975+)
4976+from charmhelpers.core.hookenv import log
4977+from charmhelpers.fetch import SourceConfigError
4978+
4979+CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
4980+deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
4981+"""
4982+
4983+PROPOSED_POCKET = """# Proposed
4984+deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
4985+"""
4986+
4987+CLOUD_ARCHIVE_POCKETS = {
4988+ # Folsom
4989+ 'folsom': 'precise-updates/folsom',
4990+ 'precise-folsom': 'precise-updates/folsom',
4991+ 'precise-folsom/updates': 'precise-updates/folsom',
4992+ 'precise-updates/folsom': 'precise-updates/folsom',
4993+ 'folsom/proposed': 'precise-proposed/folsom',
4994+ 'precise-folsom/proposed': 'precise-proposed/folsom',
4995+ 'precise-proposed/folsom': 'precise-proposed/folsom',
4996+ # Grizzly
4997+ 'grizzly': 'precise-updates/grizzly',
4998+ 'precise-grizzly': 'precise-updates/grizzly',
4999+ 'precise-grizzly/updates': 'precise-updates/grizzly',
5000+ 'precise-updates/grizzly': 'precise-updates/grizzly',
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches