Merge lp:~lutostag/charms/trusty/jenkins/xenial into lp:charms/trusty/jenkins

Proposed by Greg Lutostanski on 2016-06-01
Status: Rejected
Rejected by: Ryan Beisner on 2016-06-16
Proposed branch: lp:~lutostag/charms/trusty/jenkins/xenial
Merge into: lp:charms/trusty/jenkins
Diff against target: 3378 lines (+2244/-264)
27 files modified
hooks/charmhelpers/__init__.py (+16/-0)
hooks/charmhelpers/contrib/__init__.py (+15/-0)
hooks/charmhelpers/contrib/python/__init__.py (+15/-0)
hooks/charmhelpers/contrib/python/packages.py (+82/-17)
hooks/charmhelpers/core/__init__.py (+15/-0)
hooks/charmhelpers/core/decorators.py (+16/-0)
hooks/charmhelpers/core/files.py (+45/-0)
hooks/charmhelpers/core/fstab.py (+19/-3)
hooks/charmhelpers/core/hookenv.py (+500/-43)
hooks/charmhelpers/core/host.py (+368/-70)
hooks/charmhelpers/core/hugepage.py (+71/-0)
hooks/charmhelpers/core/kernel.py (+68/-0)
hooks/charmhelpers/core/services/__init__.py (+16/-0)
hooks/charmhelpers/core/services/base.py (+59/-19)
hooks/charmhelpers/core/services/helpers.py (+59/-10)
hooks/charmhelpers/core/strutils.py (+72/-0)
hooks/charmhelpers/core/sysctl.py (+28/-6)
hooks/charmhelpers/core/templating.py (+37/-8)
hooks/charmhelpers/core/unitdata.py (+521/-0)
hooks/charmhelpers/fetch/__init__.py (+57/-16)
hooks/charmhelpers/fetch/archiveurl.py (+34/-12)
hooks/charmhelpers/fetch/bzrurl.py (+38/-24)
hooks/charmhelpers/fetch/giturl.py (+40/-21)
hooks/charmhelpers/payload/__init__.py (+16/-0)
hooks/charmhelpers/payload/execd.py (+16/-0)
hooks/jenkins_hooks.py (+20/-13)
hooks/jenkins_utils.py (+1/-2)
To merge this branch: bzr merge lp:~lutostag/charms/trusty/jenkins/xenial
Reviewer Review Type Date Requested Status
Ryan Beisner 2016-06-01 Needs Information on 2016-06-16
Review Queue (community) automated testing Approve on 2016-06-09
Review via email: mp+296222@code.launchpad.net

Description of the Change

Fixes to work on xenial as well as trusty. Welcome to any feedback. This is just a hold over till someone takes the initiative to rewrite the whole charm reactive-style.

To post a comment you must log in.
Review Queue (review-queue) wrote :

The results (PASS) are in and available here: http://juju-ci.vapour.ws:8080/job/charm-bundle-test-lxc/4514/

review: Approve (automated testing)
Review Queue (review-queue) wrote :

The results (PASS) are in and available here: http://juju-ci.vapour.ws:8080/job/charm-bundle-test-aws/4534/

review: Approve (automated testing)
Ryan Beisner (1chb1n) wrote :

I believe the source of truth for the Jenkins charm dev is now:
https://github.com/jenkinsci/jenkins-charm

review: Needs Information

Unmerged revisions

46. By Greg Lutostanski on 2016-05-18

only run python3 if we are at or above xenial

45. By Greg Lutostanski on 2016-05-09

import six after charmhelpers has run to install it

44. By Greg Lutostanski on 2016-05-09

install python3-jenkins if necessary

43. By Greg Lutostanski on 2016-05-09

default to python3 first over python2

42. By Greg Lutostanski on 2016-05-09

fix for subprocess check_output bytes in python3

41. By Greg Lutostanski on 2016-05-06

try to utf-8 encode for password

40. By Greg Lutostanski on 2016-05-04

allow to run with any python

39. By Greg Lutostanski on 2016-05-03

allow charm to work with python3 as well as python2

38. By Greg Lutostanski on 2016-05-03

update charmhelpers

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/charmhelpers/__init__.py'
2--- hooks/charmhelpers/__init__.py 2014-12-11 10:50:44 +0000
3+++ hooks/charmhelpers/__init__.py 2016-06-01 15:03:50 +0000
4@@ -1,3 +1,19 @@
5+# Copyright 2014-2015 Canonical Limited.
6+#
7+# This file is part of charm-helpers.
8+#
9+# charm-helpers is free software: you can redistribute it and/or modify
10+# it under the terms of the GNU Lesser General Public License version 3 as
11+# published by the Free Software Foundation.
12+#
13+# charm-helpers is distributed in the hope that it will be useful,
14+# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+# GNU Lesser General Public License for more details.
17+#
18+# You should have received a copy of the GNU Lesser General Public License
19+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
20+
21 # Bootstrap charm-helpers, installing its dependencies if necessary using
22 # only standard libraries.
23 import subprocess
24
25=== modified file 'hooks/charmhelpers/contrib/__init__.py'
26--- hooks/charmhelpers/contrib/__init__.py 2014-11-18 23:06:36 +0000
27+++ hooks/charmhelpers/contrib/__init__.py 2016-06-01 15:03:50 +0000
28@@ -0,0 +1,15 @@
29+# Copyright 2014-2015 Canonical Limited.
30+#
31+# This file is part of charm-helpers.
32+#
33+# charm-helpers is free software: you can redistribute it and/or modify
34+# it under the terms of the GNU Lesser General Public License version 3 as
35+# published by the Free Software Foundation.
36+#
37+# charm-helpers is distributed in the hope that it will be useful,
38+# but WITHOUT ANY WARRANTY; without even the implied warranty of
39+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
40+# GNU Lesser General Public License for more details.
41+#
42+# You should have received a copy of the GNU Lesser General Public License
43+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
44
45=== modified file 'hooks/charmhelpers/contrib/python/__init__.py'
46--- hooks/charmhelpers/contrib/python/__init__.py 2015-01-21 00:04:26 +0000
47+++ hooks/charmhelpers/contrib/python/__init__.py 2016-06-01 15:03:50 +0000
48@@ -0,0 +1,15 @@
49+# Copyright 2014-2015 Canonical Limited.
50+#
51+# This file is part of charm-helpers.
52+#
53+# charm-helpers is free software: you can redistribute it and/or modify
54+# it under the terms of the GNU Lesser General Public License version 3 as
55+# published by the Free Software Foundation.
56+#
57+# charm-helpers is distributed in the hope that it will be useful,
58+# but WITHOUT ANY WARRANTY; without even the implied warranty of
59+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
60+# GNU Lesser General Public License for more details.
61+#
62+# You should have received a copy of the GNU Lesser General Public License
63+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
64
65=== modified file 'hooks/charmhelpers/contrib/python/packages.py'
66--- hooks/charmhelpers/contrib/python/packages.py 2015-01-21 00:04:26 +0000
67+++ hooks/charmhelpers/contrib/python/packages.py 2016-06-01 15:03:50 +0000
68@@ -1,28 +1,68 @@
69 #!/usr/bin/env python
70 # coding: utf-8
71
72+# Copyright 2014-2015 Canonical Limited.
73+#
74+# This file is part of charm-helpers.
75+#
76+# charm-helpers is free software: you can redistribute it and/or modify
77+# it under the terms of the GNU Lesser General Public License version 3 as
78+# published by the Free Software Foundation.
79+#
80+# charm-helpers is distributed in the hope that it will be useful,
81+# but WITHOUT ANY WARRANTY; without even the implied warranty of
82+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
83+# GNU Lesser General Public License for more details.
84+#
85+# You should have received a copy of the GNU Lesser General Public License
86+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
87+
88+import os
89+import subprocess
90+import sys
91+
92+from charmhelpers.fetch import apt_install, apt_update
93+from charmhelpers.core.hookenv import charm_dir, log
94+
95 __author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
96
97-from charmhelpers.fetch import apt_install, apt_update
98-from charmhelpers.core.hookenv import log
99-
100-try:
101- from pip import main as pip_execute
102-except ImportError:
103- apt_update()
104- apt_install('python-pip')
105- from pip import main as pip_execute
106+
107+def pip_execute(*args, **kwargs):
108+ """Overriden pip_execute() to stop sys.path being changed.
109+
110+ The act of importing main from the pip module seems to cause add wheels
111+ from the /usr/share/python-wheels which are installed by various tools.
112+ This function ensures that sys.path remains the same after the call is
113+ executed.
114+ """
115+ try:
116+ _path = sys.path
117+ try:
118+ from pip import main as _pip_execute
119+ except ImportError:
120+ apt_update()
121+ apt_install('python-pip')
122+ from pip import main as _pip_execute
123+ _pip_execute(*args, **kwargs)
124+ finally:
125+ sys.path = _path
126
127
128 def parse_options(given, available):
129 """Given a set of options, check if available"""
130 for key, value in sorted(given.items()):
131+ if not value:
132+ continue
133 if key in available:
134 yield "--{0}={1}".format(key, value)
135
136
137-def pip_install_requirements(requirements, **options):
138- """Install a requirements file """
139+def pip_install_requirements(requirements, constraints=None, **options):
140+ """Install a requirements file.
141+
142+ :param constraints: Path to pip constraints file.
143+ http://pip.readthedocs.org/en/stable/user_guide/#constraints-files
144+ """
145 command = ["install"]
146
147 available_options = ('proxy', 'src', 'log', )
148@@ -30,16 +70,25 @@
149 command.append(option)
150
151 command.append("-r {0}".format(requirements))
152- log("Installing from file: {} with options: {}".format(requirements,
153- command))
154+ if constraints:
155+ command.append("-c {0}".format(constraints))
156+ log("Installing from file: {} with constraints {} "
157+ "and options: {}".format(requirements, constraints, command))
158+ else:
159+ log("Installing from file: {} with options: {}".format(requirements,
160+ command))
161 pip_execute(command)
162
163
164-def pip_install(package, fatal=False, upgrade=False, **options):
165+def pip_install(package, fatal=False, upgrade=False, venv=None, **options):
166 """Install a python package"""
167- command = ["install"]
168+ if venv:
169+ venv_python = os.path.join(venv, 'bin/pip')
170+ command = [venv_python, "install"]
171+ else:
172+ command = ["install"]
173
174- available_options = ('proxy', 'src', 'log', "index-url", )
175+ available_options = ('proxy', 'src', 'log', 'index-url', )
176 for option in parse_options(options, available_options):
177 command.append(option)
178
179@@ -53,7 +102,10 @@
180
181 log("Installing {} package with options: {}".format(package,
182 command))
183- pip_execute(command)
184+ if venv:
185+ subprocess.check_call(command)
186+ else:
187+ pip_execute(command)
188
189
190 def pip_uninstall(package, **options):
191@@ -78,3 +130,16 @@
192 """Returns the list of current python installed packages
193 """
194 return pip_execute(["list"])
195+
196+
197+def pip_create_virtualenv(path=None):
198+ """Create an isolated Python environment."""
199+ apt_install('python-virtualenv')
200+
201+ if path:
202+ venv_path = path
203+ else:
204+ venv_path = os.path.join(charm_dir(), 'venv')
205+
206+ if not os.path.exists(venv_path):
207+ subprocess.check_call(['virtualenv', venv_path])
208
209=== modified file 'hooks/charmhelpers/core/__init__.py'
210--- hooks/charmhelpers/core/__init__.py 2014-11-18 23:06:36 +0000
211+++ hooks/charmhelpers/core/__init__.py 2016-06-01 15:03:50 +0000
212@@ -0,0 +1,15 @@
213+# Copyright 2014-2015 Canonical Limited.
214+#
215+# This file is part of charm-helpers.
216+#
217+# charm-helpers is free software: you can redistribute it and/or modify
218+# it under the terms of the GNU Lesser General Public License version 3 as
219+# published by the Free Software Foundation.
220+#
221+# charm-helpers is distributed in the hope that it will be useful,
222+# but WITHOUT ANY WARRANTY; without even the implied warranty of
223+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
224+# GNU Lesser General Public License for more details.
225+#
226+# You should have received a copy of the GNU Lesser General Public License
227+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
228
229=== modified file 'hooks/charmhelpers/core/decorators.py'
230--- hooks/charmhelpers/core/decorators.py 2015-01-20 18:22:29 +0000
231+++ hooks/charmhelpers/core/decorators.py 2016-06-01 15:03:50 +0000
232@@ -1,3 +1,19 @@
233+# Copyright 2014-2015 Canonical Limited.
234+#
235+# This file is part of charm-helpers.
236+#
237+# charm-helpers is free software: you can redistribute it and/or modify
238+# it under the terms of the GNU Lesser General Public License version 3 as
239+# published by the Free Software Foundation.
240+#
241+# charm-helpers is distributed in the hope that it will be useful,
242+# but WITHOUT ANY WARRANTY; without even the implied warranty of
243+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
244+# GNU Lesser General Public License for more details.
245+#
246+# You should have received a copy of the GNU Lesser General Public License
247+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
248+
249 #
250 # Copyright 2014 Canonical Ltd.
251 #
252
253=== added file 'hooks/charmhelpers/core/files.py'
254--- hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
255+++ hooks/charmhelpers/core/files.py 2016-06-01 15:03:50 +0000
256@@ -0,0 +1,45 @@
257+#!/usr/bin/env python
258+# -*- coding: utf-8 -*-
259+
260+# Copyright 2014-2015 Canonical Limited.
261+#
262+# This file is part of charm-helpers.
263+#
264+# charm-helpers is free software: you can redistribute it and/or modify
265+# it under the terms of the GNU Lesser General Public License version 3 as
266+# published by the Free Software Foundation.
267+#
268+# charm-helpers is distributed in the hope that it will be useful,
269+# but WITHOUT ANY WARRANTY; without even the implied warranty of
270+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
271+# GNU Lesser General Public License for more details.
272+#
273+# You should have received a copy of the GNU Lesser General Public License
274+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
275+
276+__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
277+
278+import os
279+import subprocess
280+
281+
282+def sed(filename, before, after, flags='g'):
283+ """
284+ Search and replaces the given pattern on filename.
285+
286+ :param filename: relative or absolute file path.
287+ :param before: expression to be replaced (see 'man sed')
288+ :param after: expression to replace with (see 'man sed')
289+ :param flags: sed-compatible regex flags in example, to make
290+ the search and replace case insensitive, specify ``flags="i"``.
291+ The ``g`` flag is always specified regardless, so you do not
292+ need to remember to include it when overriding this parameter.
293+ :returns: If the sed command exit code was zero then return,
294+ otherwise raise CalledProcessError.
295+ """
296+ expression = r's/{0}/{1}/{2}'.format(before,
297+ after, flags)
298+
299+ return subprocess.check_call(["sed", "-i", "-r", "-e",
300+ expression,
301+ os.path.expanduser(filename)])
302
303=== modified file 'hooks/charmhelpers/core/fstab.py'
304--- hooks/charmhelpers/core/fstab.py 2014-12-11 10:35:04 +0000
305+++ hooks/charmhelpers/core/fstab.py 2016-06-01 15:03:50 +0000
306@@ -1,11 +1,27 @@
307 #!/usr/bin/env python
308 # -*- coding: utf-8 -*-
309
310-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
311+# Copyright 2014-2015 Canonical Limited.
312+#
313+# This file is part of charm-helpers.
314+#
315+# charm-helpers is free software: you can redistribute it and/or modify
316+# it under the terms of the GNU Lesser General Public License version 3 as
317+# published by the Free Software Foundation.
318+#
319+# charm-helpers is distributed in the hope that it will be useful,
320+# but WITHOUT ANY WARRANTY; without even the implied warranty of
321+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
322+# GNU Lesser General Public License for more details.
323+#
324+# You should have received a copy of the GNU Lesser General Public License
325+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
326
327 import io
328 import os
329
330+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
331+
332
333 class Fstab(io.FileIO):
334 """This class extends file in order to implement a file reader/writer
335@@ -61,7 +77,7 @@
336 for line in self.readlines():
337 line = line.decode('us-ascii')
338 try:
339- if line.strip() and not line.startswith("#"):
340+ if line.strip() and not line.strip().startswith("#"):
341 yield self._hydrate_entry(line)
342 except ValueError:
343 pass
344@@ -88,7 +104,7 @@
345
346 found = False
347 for index, line in enumerate(lines):
348- if not line.startswith("#"):
349+ if line.strip() and not line.strip().startswith("#"):
350 if self._hydrate_entry(line) == entry:
351 found = True
352 break
353
354=== modified file 'hooks/charmhelpers/core/hookenv.py'
355--- hooks/charmhelpers/core/hookenv.py 2014-12-11 10:35:04 +0000
356+++ hooks/charmhelpers/core/hookenv.py 2016-06-01 15:03:50 +0000
357@@ -1,14 +1,37 @@
358+# Copyright 2014-2015 Canonical Limited.
359+#
360+# This file is part of charm-helpers.
361+#
362+# charm-helpers is free software: you can redistribute it and/or modify
363+# it under the terms of the GNU Lesser General Public License version 3 as
364+# published by the Free Software Foundation.
365+#
366+# charm-helpers is distributed in the hope that it will be useful,
367+# but WITHOUT ANY WARRANTY; without even the implied warranty of
368+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
369+# GNU Lesser General Public License for more details.
370+#
371+# You should have received a copy of the GNU Lesser General Public License
372+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
373+
374 "Interactions with the Juju environment"
375 # Copyright 2013 Canonical Ltd.
376 #
377 # Authors:
378 # Charm Helpers Developers <juju@lists.ubuntu.com>
379
380+from __future__ import print_function
381+import copy
382+from distutils.version import LooseVersion
383+from functools import wraps
384+import glob
385 import os
386 import json
387 import yaml
388 import subprocess
389 import sys
390+import errno
391+import tempfile
392 from subprocess import CalledProcessError
393
394 import six
395@@ -40,15 +63,18 @@
396
397 will cache the result of unit_get + 'test' for future calls.
398 """
399+ @wraps(func)
400 def wrapper(*args, **kwargs):
401 global cache
402 key = str((func, args, kwargs))
403 try:
404 return cache[key]
405 except KeyError:
406- res = func(*args, **kwargs)
407- cache[key] = res
408- return res
409+ pass # Drop out of the exception handler scope.
410+ res = func(*args, **kwargs)
411+ cache[key] = res
412+ return res
413+ wrapper._wrapped = func
414 return wrapper
415
416
417@@ -71,7 +97,18 @@
418 if not isinstance(message, six.string_types):
419 message = repr(message)
420 command += [message]
421- subprocess.call(command)
422+ # Missing juju-log should not cause failures in unit tests
423+ # Send log output to stderr
424+ try:
425+ subprocess.call(command)
426+ except OSError as e:
427+ if e.errno == errno.ENOENT:
428+ if level:
429+ message = "{}: {}".format(level, message)
430+ message = "juju-log: {}".format(message)
431+ print(message, file=sys.stderr)
432+ else:
433+ raise
434
435
436 class Serializable(UserDict):
437@@ -137,9 +174,19 @@
438 return os.environ.get('JUJU_RELATION', None)
439
440
441-def relation_id():
442- """The relation ID for the current relation hook"""
443- return os.environ.get('JUJU_RELATION_ID', None)
444+@cached
445+def relation_id(relation_name=None, service_or_unit=None):
446+ """The relation ID for the current or a specified relation"""
447+ if not relation_name and not service_or_unit:
448+ return os.environ.get('JUJU_RELATION_ID', None)
449+ elif relation_name and service_or_unit:
450+ service_name = service_or_unit.split('/')[0]
451+ for relid in relation_ids(relation_name):
452+ remote_service = remote_service_name(relid)
453+ if remote_service == service_name:
454+ return relid
455+ else:
456+ raise ValueError('Must specify neither or both of relation_name and service_or_unit')
457
458
459 def local_unit():
460@@ -149,7 +196,7 @@
461
462 def remote_unit():
463 """The remote unit for the current relation hook"""
464- return os.environ['JUJU_REMOTE_UNIT']
465+ return os.environ.get('JUJU_REMOTE_UNIT', None)
466
467
468 def service_name():
469@@ -157,9 +204,20 @@
470 return local_unit().split('/')[0]
471
472
473+@cached
474+def remote_service_name(relid=None):
475+ """The remote service name for a given relation-id (or the current relation)"""
476+ if relid is None:
477+ unit = remote_unit()
478+ else:
479+ units = related_units(relid)
480+ unit = units[0] if units else None
481+ return unit.split('/')[0] if unit else None
482+
483+
484 def hook_name():
485 """The name of the currently executing hook"""
486- return os.path.basename(sys.argv[0])
487+ return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
488
489
490 class Config(dict):
491@@ -209,23 +267,7 @@
492 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
493 if os.path.exists(self.path):
494 self.load_previous()
495-
496- def __getitem__(self, key):
497- """For regular dict lookups, check the current juju config first,
498- then the previous (saved) copy. This ensures that user-saved values
499- will be returned by a dict lookup.
500-
501- """
502- try:
503- return dict.__getitem__(self, key)
504- except KeyError:
505- return (self._prev_dict or {})[key]
506-
507- def keys(self):
508- prev_keys = []
509- if self._prev_dict is not None:
510- prev_keys = self._prev_dict.keys()
511- return list(set(prev_keys + list(dict.keys(self))))
512+ atexit(self._implicit_save)
513
514 def load_previous(self, path=None):
515 """Load previous copy of config from disk.
516@@ -244,6 +286,9 @@
517 self.path = path or self.path
518 with open(self.path) as f:
519 self._prev_dict = json.load(f)
520+ for k, v in copy.deepcopy(self._prev_dict).items():
521+ if k not in self:
522+ self[k] = v
523
524 def changed(self, key):
525 """Return True if the current value for this key is different from
526@@ -275,13 +320,13 @@
527 instance.
528
529 """
530- if self._prev_dict:
531- for k, v in six.iteritems(self._prev_dict):
532- if k not in self:
533- self[k] = v
534 with open(self.path, 'w') as f:
535 json.dump(self, f)
536
537+ def _implicit_save(self):
538+ if self.implicit_save:
539+ self.save()
540+
541
542 @cached
543 def config(scope=None):
544@@ -324,18 +369,49 @@
545 """Set relation information for the current unit"""
546 relation_settings = relation_settings if relation_settings else {}
547 relation_cmd_line = ['relation-set']
548+ accepts_file = "--file" in subprocess.check_output(
549+ relation_cmd_line + ["--help"], universal_newlines=True)
550 if relation_id is not None:
551 relation_cmd_line.extend(('-r', relation_id))
552- for k, v in (list(relation_settings.items()) + list(kwargs.items())):
553- if v is None:
554- relation_cmd_line.append('{}='.format(k))
555- else:
556- relation_cmd_line.append('{}={}'.format(k, v))
557- subprocess.check_call(relation_cmd_line)
558+ settings = relation_settings.copy()
559+ settings.update(kwargs)
560+ for key, value in settings.items():
561+ # Force value to be a string: it always should, but some call
562+ # sites pass in things like dicts or numbers.
563+ if value is not None:
564+ settings[key] = "{}".format(value)
565+ if accepts_file:
566+ # --file was introduced in Juju 1.23.2. Use it by default if
567+ # available, since otherwise we'll break if the relation data is
568+ # too big. Ideally we should tell relation-set to read the data from
569+ # stdin, but that feature is broken in 1.23.2: Bug #1454678.
570+ with tempfile.NamedTemporaryFile(delete=False) as settings_file:
571+ settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
572+ subprocess.check_call(
573+ relation_cmd_line + ["--file", settings_file.name])
574+ os.remove(settings_file.name)
575+ else:
576+ for key, value in settings.items():
577+ if value is None:
578+ relation_cmd_line.append('{}='.format(key))
579+ else:
580+ relation_cmd_line.append('{}={}'.format(key, value))
581+ subprocess.check_call(relation_cmd_line)
582 # Flush cache of any relation-gets for local unit
583 flush(local_unit())
584
585
586+def relation_clear(r_id=None):
587+ ''' Clears any relation data already set on relation r_id '''
588+ settings = relation_get(rid=r_id,
589+ unit=local_unit())
590+ for setting in settings:
591+ if setting not in ['public-address', 'private-address']:
592+ settings[setting] = None
593+ relation_set(relation_id=r_id,
594+ **settings)
595+
596+
597 @cached
598 def relation_ids(reltype=None):
599 """A list of relation_ids"""
600@@ -415,6 +491,76 @@
601
602
603 @cached
604+def peer_relation_id():
605+ '''Get the peers relation id if a peers relation has been joined, else None.'''
606+ md = metadata()
607+ section = md.get('peers')
608+ if section:
609+ for key in section:
610+ relids = relation_ids(key)
611+ if relids:
612+ return relids[0]
613+ return None
614+
615+
616+@cached
617+def relation_to_interface(relation_name):
618+ """
619+ Given the name of a relation, return the interface that relation uses.
620+
621+ :returns: The interface name, or ``None``.
622+ """
623+ return relation_to_role_and_interface(relation_name)[1]
624+
625+
626+@cached
627+def relation_to_role_and_interface(relation_name):
628+ """
629+ Given the name of a relation, return the role and the name of the interface
630+ that relation uses (where role is one of ``provides``, ``requires``, or ``peers``).
631+
632+ :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
633+ """
634+ _metadata = metadata()
635+ for role in ('provides', 'requires', 'peers'):
636+ interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
637+ if interface:
638+ return role, interface
639+ return None, None
640+
641+
642+@cached
643+def role_and_interface_to_relations(role, interface_name):
644+ """
645+ Given a role and interface name, return a list of relation names for the
646+ current charm that use that interface under that role (where role is one
647+ of ``provides``, ``requires``, or ``peers``).
648+
649+ :returns: A list of relation names.
650+ """
651+ _metadata = metadata()
652+ results = []
653+ for relation_name, relation in _metadata.get(role, {}).items():
654+ if relation['interface'] == interface_name:
655+ results.append(relation_name)
656+ return results
657+
658+
659+@cached
660+def interface_to_relations(interface_name):
661+ """
662+ Given an interface, return a list of relation names for the current
663+ charm that use that interface.
664+
665+ :returns: A list of relation names.
666+ """
667+ results = []
668+ for role in ('provides', 'requires', 'peers'):
669+ results.extend(role_and_interface_to_relations(role, interface_name))
670+ return results
671+
672+
673+@cached
674 def charm_name():
675 """Get the name of the current charm as is specified on metadata.yaml"""
676 return metadata().get('name')
677@@ -480,11 +626,48 @@
678 return None
679
680
681+def unit_public_ip():
682+ """Get this unit's public IP address"""
683+ return unit_get('public-address')
684+
685+
686 def unit_private_ip():
687 """Get this unit's private IP address"""
688 return unit_get('private-address')
689
690
691+@cached
692+def storage_get(attribute=None, storage_id=None):
693+ """Get storage attributes"""
694+ _args = ['storage-get', '--format=json']
695+ if storage_id:
696+ _args.extend(('-s', storage_id))
697+ if attribute:
698+ _args.append(attribute)
699+ try:
700+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
701+ except ValueError:
702+ return None
703+
704+
705+@cached
706+def storage_list(storage_name=None):
707+ """List the storage IDs for the unit"""
708+ _args = ['storage-list', '--format=json']
709+ if storage_name:
710+ _args.append(storage_name)
711+ try:
712+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
713+ except ValueError:
714+ return None
715+ except OSError as e:
716+ import errno
717+ if e.errno == errno.ENOENT:
718+ # storage-list does not exist
719+ return []
720+ raise
721+
722+
723 class UnregisteredHookError(Exception):
724 """Raised when an undefined hook is called"""
725 pass
726@@ -512,10 +695,14 @@
727 hooks.execute(sys.argv)
728 """
729
730- def __init__(self, config_save=True):
731+ def __init__(self, config_save=None):
732 super(Hooks, self).__init__()
733 self._hooks = {}
734- self._config_save = config_save
735+
736+ # For unknown reasons, we allow the Hooks constructor to override
737+ # config().implicit_save.
738+ if config_save is not None:
739+ config().implicit_save = config_save
740
741 def register(self, name, function):
742 """Register a hook"""
743@@ -523,13 +710,16 @@
744
745 def execute(self, args):
746 """Execute a registered hook based on args[0]"""
747+ _run_atstart()
748 hook_name = os.path.basename(args[0])
749 if hook_name in self._hooks:
750- self._hooks[hook_name]()
751- if self._config_save:
752- cfg = config()
753- if cfg.implicit_save:
754- cfg.save()
755+ try:
756+ self._hooks[hook_name]()
757+ except SystemExit as x:
758+ if x.code is None or x.code == 0:
759+ _run_atexit()
760+ raise
761+ _run_atexit()
762 else:
763 raise UnregisteredHookError(hook_name)
764
765@@ -550,3 +740,270 @@
766 def charm_dir():
767 """Return the root directory of the current charm"""
768 return os.environ.get('CHARM_DIR')
769+
770+
771+@cached
772+def action_get(key=None):
773+ """Gets the value of an action parameter, or all key/value param pairs"""
774+ cmd = ['action-get']
775+ if key is not None:
776+ cmd.append(key)
777+ cmd.append('--format=json')
778+ action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
779+ return action_data
780+
781+
782+def action_set(values):
783+ """Sets the values to be returned after the action finishes"""
784+ cmd = ['action-set']
785+ for k, v in list(values.items()):
786+ cmd.append('{}={}'.format(k, v))
787+ subprocess.check_call(cmd)
788+
789+
790+def action_fail(message):
791+ """Sets the action status to failed and sets the error message.
792+
793+ The results set by action_set are preserved."""
794+ subprocess.check_call(['action-fail', message])
795+
796+
797+def action_name():
798+ """Get the name of the currently executing action."""
799+ return os.environ.get('JUJU_ACTION_NAME')
800+
801+
802+def action_uuid():
803+ """Get the UUID of the currently executing action."""
804+ return os.environ.get('JUJU_ACTION_UUID')
805+
806+
807+def action_tag():
808+ """Get the tag for the currently executing action."""
809+ return os.environ.get('JUJU_ACTION_TAG')
810+
811+
812+def status_set(workload_state, message):
813+ """Set the workload state with a message
814+
815+ Use status-set to set the workload state with a message which is visible
816+ to the user via juju status. If the status-set command is not found then
817+ assume this is juju < 1.23 and juju-log the message unstead.
818+
819+ workload_state -- valid juju workload state.
820+ message -- status update message
821+ """
822+ valid_states = ['maintenance', 'blocked', 'waiting', 'active']
823+ if workload_state not in valid_states:
824+ raise ValueError(
825+ '{!r} is not a valid workload state'.format(workload_state)
826+ )
827+ cmd = ['status-set', workload_state, message]
828+ try:
829+ ret = subprocess.call(cmd)
830+ if ret == 0:
831+ return
832+ except OSError as e:
833+ if e.errno != errno.ENOENT:
834+ raise
835+ log_message = 'status-set failed: {} {}'.format(workload_state,
836+ message)
837+ log(log_message, level='INFO')
838+
839+
840+def status_get():
841+ """Retrieve the previously set juju workload state and message
842+
843+ If the status-get command is not found then assume this is juju < 1.23 and
844+ return 'unknown', ""
845+
846+ """
847+ cmd = ['status-get', "--format=json", "--include-data"]
848+ try:
849+ raw_status = subprocess.check_output(cmd)
850+ except OSError as e:
851+ if e.errno == errno.ENOENT:
852+ return ('unknown', "")
853+ else:
854+ raise
855+ else:
856+ status = json.loads(raw_status.decode("UTF-8"))
857+ return (status["status"], status["message"])
858+
859+
860+def translate_exc(from_exc, to_exc):
861+ def inner_translate_exc1(f):
862+ @wraps(f)
863+ def inner_translate_exc2(*args, **kwargs):
864+ try:
865+ return f(*args, **kwargs)
866+ except from_exc:
867+ raise to_exc
868+
869+ return inner_translate_exc2
870+
871+ return inner_translate_exc1
872+
873+
874+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
875+def is_leader():
876+ """Does the current unit hold the juju leadership
877+
878+ Uses juju to determine whether the current unit is the leader of its peers
879+ """
880+ cmd = ['is-leader', '--format=json']
881+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
882+
883+
884+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
885+def leader_get(attribute=None):
886+ """Juju leader get value(s)"""
887+ cmd = ['leader-get', '--format=json'] + [attribute or '-']
888+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
889+
890+
891+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
892+def leader_set(settings=None, **kwargs):
893+ """Juju leader set value(s)"""
894+ # Don't log secrets.
895+ # log("Juju leader-set '%s'" % (settings), level=DEBUG)
896+ cmd = ['leader-set']
897+ settings = settings or {}
898+ settings.update(kwargs)
899+ for k, v in settings.items():
900+ if v is None:
901+ cmd.append('{}='.format(k))
902+ else:
903+ cmd.append('{}={}'.format(k, v))
904+ subprocess.check_call(cmd)
905+
906+
907+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
908+def payload_register(ptype, klass, pid):
909+ """ is used while a hook is running to let Juju know that a
910+ payload has been started."""
911+ cmd = ['payload-register']
912+ for x in [ptype, klass, pid]:
913+ cmd.append(x)
914+ subprocess.check_call(cmd)
915+
916+
917+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
918+def payload_unregister(klass, pid):
919+ """ is used while a hook is running to let Juju know
920+ that a payload has been manually stopped. The <class> and <id> provided
921+ must match a payload that has been previously registered with juju using
922+ payload-register."""
923+ cmd = ['payload-unregister']
924+ for x in [klass, pid]:
925+ cmd.append(x)
926+ subprocess.check_call(cmd)
927+
928+
929+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
930+def payload_status_set(klass, pid, status):
931+ """is used to update the current status of a registered payload.
932+ The <class> and <id> provided must match a payload that has been previously
933+ registered with juju using payload-register. The <status> must be one of the
934+ follow: starting, started, stopping, stopped"""
935+ cmd = ['payload-status-set']
936+ for x in [klass, pid, status]:
937+ cmd.append(x)
938+ subprocess.check_call(cmd)
939+
940+
941+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
942+def resource_get(name):
943+ """used to fetch the resource path of the given name.
944+
945+ <name> must match a name of defined resource in metadata.yaml
946+
947+ returns either a path or False if resource not available
948+ """
949+ if not name:
950+ return False
951+
952+ cmd = ['resource-get', name]
953+ try:
954+ return subprocess.check_output(cmd).decode('UTF-8')
955+ except subprocess.CalledProcessError:
956+ return False
957+
958+
959+@cached
960+def juju_version():
961+ """Full version string (eg. '1.23.3.1-trusty-amd64')"""
962+ # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
963+ jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
964+ return subprocess.check_output([jujud, 'version'],
965+ universal_newlines=True).strip()
966+
967+
968+@cached
969+def has_juju_version(minimum_version):
970+ """Return True if the Juju version is at least the provided version"""
971+ return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
972+
973+
974+_atexit = []
975+_atstart = []
976+
977+
978+def atstart(callback, *args, **kwargs):
979+ '''Schedule a callback to run before the main hook.
980+
981+ Callbacks are run in the order they were added.
982+
983+ This is useful for modules and classes to perform initialization
984+ and inject behavior. In particular:
985+
986+ - Run common code before all of your hooks, such as logging
987+ the hook name or interesting relation data.
988+ - Defer object or module initialization that requires a hook
989+ context until we know there actually is a hook context,
990+ making testing easier.
991+ - Rather than requiring charm authors to include boilerplate to
992+ invoke your helper's behavior, have it run automatically if
993+ your object is instantiated or module imported.
994+
995+ This is not at all useful after your hook framework as been launched.
996+ '''
997+ global _atstart
998+ _atstart.append((callback, args, kwargs))
999+
1000+
1001+def atexit(callback, *args, **kwargs):
1002+ '''Schedule a callback to run on successful hook completion.
1003+
1004+ Callbacks are run in the reverse order that they were added.'''
1005+ _atexit.append((callback, args, kwargs))
1006+
1007+
1008+def _run_atstart():
1009+ '''Hook frameworks must invoke this before running the main hook body.'''
1010+ global _atstart
1011+ for callback, args, kwargs in _atstart:
1012+ callback(*args, **kwargs)
1013+ del _atstart[:]
1014+
1015+
1016+def _run_atexit():
1017+ '''Hook frameworks must invoke this after the main hook body has
1018+ successfully completed. Do not invoke it if the hook fails.'''
1019+ global _atexit
1020+ for callback, args, kwargs in reversed(_atexit):
1021+ callback(*args, **kwargs)
1022+ del _atexit[:]
1023+
1024+
1025+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
1026+def network_get_primary_address(binding):
1027+ '''
1028+ Retrieve the primary network address for a named binding
1029+
1030+ :param binding: string. The name of a relation of extra-binding
1031+ :return: string. The primary IP address for the named binding
1032+ :raise: NotImplementedError if run on Juju < 2.0
1033+ '''
1034+ cmd = ['network-get', '--primary-address', binding]
1035+ return subprocess.check_output(cmd).strip()
1036
1037=== modified file 'hooks/charmhelpers/core/host.py'
1038--- hooks/charmhelpers/core/host.py 2015-01-20 18:22:29 +0000
1039+++ hooks/charmhelpers/core/host.py 2016-06-01 15:03:50 +0000
1040@@ -1,3 +1,19 @@
1041+# Copyright 2014-2015 Canonical Limited.
1042+#
1043+# This file is part of charm-helpers.
1044+#
1045+# charm-helpers is free software: you can redistribute it and/or modify
1046+# it under the terms of the GNU Lesser General Public License version 3 as
1047+# published by the Free Software Foundation.
1048+#
1049+# charm-helpers is distributed in the hope that it will be useful,
1050+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1051+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1052+# GNU Lesser General Public License for more details.
1053+#
1054+# You should have received a copy of the GNU Lesser General Public License
1055+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1056+
1057 """Tools for working with the host system"""
1058 # Copyright 2012 Canonical Ltd.
1059 #
1060@@ -8,11 +24,14 @@
1061 import os
1062 import re
1063 import pwd
1064+import glob
1065 import grp
1066 import random
1067 import string
1068 import subprocess
1069 import hashlib
1070+import functools
1071+import itertools
1072 from contextlib import contextmanager
1073 from collections import OrderedDict
1074
1075@@ -46,24 +65,96 @@
1076 return service_result
1077
1078
1079+def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
1080+ """Pause a system service.
1081+
1082+ Stop it, and prevent it from starting again at boot."""
1083+ stopped = True
1084+ if service_running(service_name):
1085+ stopped = service_stop(service_name)
1086+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
1087+ sysv_file = os.path.join(initd_dir, service_name)
1088+ if init_is_systemd():
1089+ service('disable', service_name)
1090+ elif os.path.exists(upstart_file):
1091+ override_path = os.path.join(
1092+ init_dir, '{}.override'.format(service_name))
1093+ with open(override_path, 'w') as fh:
1094+ fh.write("manual\n")
1095+ elif os.path.exists(sysv_file):
1096+ subprocess.check_call(["update-rc.d", service_name, "disable"])
1097+ else:
1098+ raise ValueError(
1099+ "Unable to detect {0} as SystemD, Upstart {1} or"
1100+ " SysV {2}".format(
1101+ service_name, upstart_file, sysv_file))
1102+ return stopped
1103+
1104+
1105+def service_resume(service_name, init_dir="/etc/init",
1106+ initd_dir="/etc/init.d"):
1107+ """Resume a system service.
1108+
1109+ Reenable starting again at boot. Start the service"""
1110+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
1111+ sysv_file = os.path.join(initd_dir, service_name)
1112+ if init_is_systemd():
1113+ service('enable', service_name)
1114+ elif os.path.exists(upstart_file):
1115+ override_path = os.path.join(
1116+ init_dir, '{}.override'.format(service_name))
1117+ if os.path.exists(override_path):
1118+ os.unlink(override_path)
1119+ elif os.path.exists(sysv_file):
1120+ subprocess.check_call(["update-rc.d", service_name, "enable"])
1121+ else:
1122+ raise ValueError(
1123+ "Unable to detect {0} as SystemD, Upstart {1} or"
1124+ " SysV {2}".format(
1125+ service_name, upstart_file, sysv_file))
1126+
1127+ started = service_running(service_name)
1128+ if not started:
1129+ started = service_start(service_name)
1130+ return started
1131+
1132+
1133 def service(action, service_name):
1134 """Control a system service"""
1135- cmd = ['service', service_name, action]
1136+ if init_is_systemd():
1137+ cmd = ['systemctl', action, service_name]
1138+ else:
1139+ cmd = ['service', service_name, action]
1140 return subprocess.call(cmd) == 0
1141
1142
1143-def service_running(service):
1144+def systemv_services_running():
1145+ output = subprocess.check_output(
1146+ ['service', '--status-all'],
1147+ stderr=subprocess.STDOUT).decode('UTF-8')
1148+ return [row.split()[-1] for row in output.split('\n') if '[ + ]' in row]
1149+
1150+
1151+def service_running(service_name):
1152 """Determine whether a system service is running"""
1153- try:
1154- output = subprocess.check_output(
1155- ['service', service, 'status'],
1156- stderr=subprocess.STDOUT).decode('UTF-8')
1157- except subprocess.CalledProcessError:
1158- return False
1159+ if init_is_systemd():
1160+ return service('is-active', service_name)
1161 else:
1162- if ("start/running" in output or "is running" in output):
1163- return True
1164+ try:
1165+ output = subprocess.check_output(
1166+ ['service', service_name, 'status'],
1167+ stderr=subprocess.STDOUT).decode('UTF-8')
1168+ except subprocess.CalledProcessError:
1169+ return False
1170 else:
1171+ # This works for upstart scripts where the 'service' command
1172+ # returns a consistent string to represent running 'start/running'
1173+ if ("start/running" in output or "is running" in output or
1174+ "up and running" in output):
1175+ return True
1176+ # Check System V scripts init script return codes
1177+ if service_name in systemv_services_running():
1178+ return True
1179 return False
1180
1181
1182@@ -74,13 +165,34 @@
1183 ['service', service_name, 'status'],
1184 stderr=subprocess.STDOUT).decode('UTF-8')
1185 except subprocess.CalledProcessError as e:
1186- return 'unrecognized service' not in e.output
1187+ return b'unrecognized service' not in e.output
1188 else:
1189 return True
1190
1191
1192-def adduser(username, password=None, shell='/bin/bash', system_user=False):
1193- """Add a user to the system"""
1194+SYSTEMD_SYSTEM = '/run/systemd/system'
1195+
1196+
1197+def init_is_systemd():
1198+ """Return True if the host system uses systemd, False otherwise."""
1199+ return os.path.isdir(SYSTEMD_SYSTEM)
1200+
1201+
1202+def adduser(username, password=None, shell='/bin/bash', system_user=False,
1203+ primary_group=None, secondary_groups=None):
1204+ """Add a user to the system.
1205+
1206+ Will log but otherwise succeed if the user already exists.
1207+
1208+ :param str username: Username to create
1209+ :param str password: Password for user; if ``None``, create a system user
1210+ :param str shell: The default shell for the user
1211+ :param bool system_user: Whether to create a login or system user
1212+ :param str primary_group: Primary group for user; defaults to username
1213+ :param list secondary_groups: Optional list of additional groups
1214+
1215+ :returns: The password database entry struct, as returned by `pwd.getpwnam`
1216+ """
1217 try:
1218 user_info = pwd.getpwnam(username)
1219 log('user {0} already exists!'.format(username))
1220@@ -95,12 +207,32 @@
1221 '--shell', shell,
1222 '--password', password,
1223 ])
1224+ if not primary_group:
1225+ try:
1226+ grp.getgrnam(username)
1227+ primary_group = username # avoid "group exists" error
1228+ except KeyError:
1229+ pass
1230+ if primary_group:
1231+ cmd.extend(['-g', primary_group])
1232+ if secondary_groups:
1233+ cmd.extend(['-G', ','.join(secondary_groups)])
1234 cmd.append(username)
1235 subprocess.check_call(cmd)
1236 user_info = pwd.getpwnam(username)
1237 return user_info
1238
1239
1240+def user_exists(username):
1241+ """Check if a user exists"""
1242+ try:
1243+ pwd.getpwnam(username)
1244+ user_exists = True
1245+ except KeyError:
1246+ user_exists = False
1247+ return user_exists
1248+
1249+
1250 def add_group(group_name, system_group=False):
1251 """Add a group to the system"""
1252 try:
1253@@ -123,11 +255,7 @@
1254
1255 def add_user_to_group(username, group):
1256 """Add a user to a group"""
1257- cmd = [
1258- 'gpasswd', '-a',
1259- username,
1260- group
1261- ]
1262+ cmd = ['gpasswd', '-a', username, group]
1263 log("Adding user {} to group {}".format(username, group))
1264 subprocess.check_call(cmd)
1265
1266@@ -168,32 +296,30 @@
1267 log("Removing non-directory file {} prior to mkdir()".format(path))
1268 os.unlink(realpath)
1269 os.makedirs(realpath, perms)
1270- os.chown(realpath, uid, gid)
1271 elif not path_exists:
1272 os.makedirs(realpath, perms)
1273- os.chown(realpath, uid, gid)
1274+ os.chown(realpath, uid, gid)
1275+ os.chmod(realpath, perms)
1276
1277
1278 def write_file(path, content, owner='root', group='root', perms=0o444):
1279- """Create or overwrite a file with the contents of a string"""
1280+ """Create or overwrite a file with the contents of a byte string."""
1281 log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
1282 uid = pwd.getpwnam(owner).pw_uid
1283 gid = grp.getgrnam(group).gr_gid
1284- with open(path, 'w') as target:
1285+ with open(path, 'wb') as target:
1286 os.fchown(target.fileno(), uid, gid)
1287 os.fchmod(target.fileno(), perms)
1288 target.write(content)
1289
1290
1291 def fstab_remove(mp):
1292- """Remove the given mountpoint entry from /etc/fstab
1293- """
1294+ """Remove the given mountpoint entry from /etc/fstab"""
1295 return Fstab.remove_by_mountpoint(mp)
1296
1297
1298 def fstab_add(dev, mp, fs, options=None):
1299- """Adds the given device entry to the /etc/fstab file
1300- """
1301+ """Adds the given device entry to the /etc/fstab file"""
1302 return Fstab.add(dev, mp, fs, options=options)
1303
1304
1305@@ -237,9 +363,19 @@
1306 return system_mounts
1307
1308
1309+def fstab_mount(mountpoint):
1310+ """Mount filesystem using fstab"""
1311+ cmd_args = ['mount', mountpoint]
1312+ try:
1313+ subprocess.check_output(cmd_args)
1314+ except subprocess.CalledProcessError as e:
1315+ log('Error unmounting {}\n{}'.format(mountpoint, e.output))
1316+ return False
1317+ return True
1318+
1319+
1320 def file_hash(path, hash_type='md5'):
1321- """
1322- Generate a hash checksum of the contents of 'path' or None if not found.
1323+ """Generate a hash checksum of the contents of 'path' or None if not found.
1324
1325 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
1326 such as md5, sha1, sha256, sha512, etc.
1327@@ -253,9 +389,22 @@
1328 return None
1329
1330
1331+def path_hash(path):
1332+ """Generate a hash checksum of all files matching 'path'. Standard
1333+ wildcards like '*' and '?' are supported, see documentation for the 'glob'
1334+ module for more information.
1335+
1336+ :return: dict: A { filename: hash } dictionary for all matched files.
1337+ Empty if none found.
1338+ """
1339+ return {
1340+ filename: file_hash(filename)
1341+ for filename in glob.iglob(path)
1342+ }
1343+
1344+
1345 def check_hash(path, checksum, hash_type='md5'):
1346- """
1347- Validate a file using a cryptographic checksum.
1348+ """Validate a file using a cryptographic checksum.
1349
1350 :param str checksum: Value of the checksum used to validate the file.
1351 :param str hash_type: Hash algorithm used to generate `checksum`.
1352@@ -270,46 +419,80 @@
1353
1354
1355 class ChecksumError(ValueError):
1356+ """A class derived from Value error to indicate the checksum failed."""
1357 pass
1358
1359
1360-def restart_on_change(restart_map, stopstart=False):
1361+def restart_on_change(restart_map, stopstart=False, restart_functions=None):
1362 """Restart services based on configuration files changing
1363
1364 This function is used a decorator, for example::
1365
1366 @restart_on_change({
1367 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
1368+ '/etc/apache/sites-enabled/*': [ 'apache2' ]
1369 })
1370- def ceph_client_changed():
1371+ def config_changed():
1372 pass # your code here
1373
1374 In this example, the cinder-api and cinder-volume services
1375 would be restarted if /etc/ceph/ceph.conf is changed by the
1376- ceph_client_changed function.
1377+ ceph_client_changed function. The apache2 service would be
1378+ restarted if any file matching the pattern got changed, created
1379+ or removed. Standard wildcards are supported, see documentation
1380+ for the 'glob' module for more information.
1381+
1382+ @param restart_map: {path_file_name: [service_name, ...]
1383+ @param stopstart: DEFAULT false; whether to stop, start OR restart
1384+ @param restart_functions: nonstandard functions to use to restart services
1385+ {svc: func, ...}
1386+ @returns result from decorated function
1387 """
1388 def wrap(f):
1389- def wrapped_f(*args):
1390- checksums = {}
1391- for path in restart_map:
1392- checksums[path] = file_hash(path)
1393- f(*args)
1394- restarts = []
1395- for path in restart_map:
1396- if checksums[path] != file_hash(path):
1397- restarts += restart_map[path]
1398- services_list = list(OrderedDict.fromkeys(restarts))
1399- if not stopstart:
1400- for service_name in services_list:
1401- service('restart', service_name)
1402- else:
1403- for action in ['stop', 'start']:
1404- for service_name in services_list:
1405- service(action, service_name)
1406+ @functools.wraps(f)
1407+ def wrapped_f(*args, **kwargs):
1408+ return restart_on_change_helper(
1409+ (lambda: f(*args, **kwargs)), restart_map, stopstart,
1410+ restart_functions)
1411 return wrapped_f
1412 return wrap
1413
1414
1415+def restart_on_change_helper(lambda_f, restart_map, stopstart=False,
1416+ restart_functions=None):
1417+ """Helper function to perform the restart_on_change function.
1418+
1419+ This is provided for decorators to restart services if files described
1420+ in the restart_map have changed after an invocation of lambda_f().
1421+
1422+ @param lambda_f: function to call.
1423+ @param restart_map: {file: [service, ...]}
1424+ @param stopstart: whether to stop, start or restart a service
1425+ @param restart_functions: nonstandard functions to use to restart services
1426+ {svc: func, ...}
1427+ @returns result of lambda_f()
1428+ """
1429+ if restart_functions is None:
1430+ restart_functions = {}
1431+ checksums = {path: path_hash(path) for path in restart_map}
1432+ r = lambda_f()
1433+ # create a list of lists of the services to restart
1434+ restarts = [restart_map[path]
1435+ for path in restart_map
1436+ if path_hash(path) != checksums[path]]
1437+ # create a flat list of ordered services without duplicates from lists
1438+ services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
1439+ if services_list:
1440+ actions = ('stop', 'start') if stopstart else ('restart',)
1441+ for service_name in services_list:
1442+ if service_name in restart_functions:
1443+ restart_functions[service_name](service_name)
1444+ else:
1445+ for action in actions:
1446+ service(action, service_name)
1447+ return r
1448+
1449+
1450 def lsb_release():
1451 """Return /etc/lsb-release in a dict"""
1452 d = {}
1453@@ -323,45 +506,105 @@
1454 def pwgen(length=None):
1455 """Generate a random pasword."""
1456 if length is None:
1457+ # A random length is ok to use a weak PRNG
1458 length = random.choice(range(35, 45))
1459 alphanumeric_chars = [
1460 l for l in (string.ascii_letters + string.digits)
1461 if l not in 'l0QD1vAEIOUaeiou']
1462+ # Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
1463+ # actual password
1464+ random_generator = random.SystemRandom()
1465 random_chars = [
1466- random.choice(alphanumeric_chars) for _ in range(length)]
1467+ random_generator.choice(alphanumeric_chars) for _ in range(length)]
1468 return(''.join(random_chars))
1469
1470
1471-def list_nics(nic_type):
1472- '''Return a list of nics of given type(s)'''
1473+def is_phy_iface(interface):
1474+ """Returns True if interface is not virtual, otherwise False."""
1475+ if interface:
1476+ sys_net = '/sys/class/net'
1477+ if os.path.isdir(sys_net):
1478+ for iface in glob.glob(os.path.join(sys_net, '*')):
1479+ if '/virtual/' in os.path.realpath(iface):
1480+ continue
1481+
1482+ if interface == os.path.basename(iface):
1483+ return True
1484+
1485+ return False
1486+
1487+
1488+def get_bond_master(interface):
1489+ """Returns bond master if interface is bond slave otherwise None.
1490+
1491+ NOTE: the provided interface is expected to be physical
1492+ """
1493+ if interface:
1494+ iface_path = '/sys/class/net/%s' % (interface)
1495+ if os.path.exists(iface_path):
1496+ if '/virtual/' in os.path.realpath(iface_path):
1497+ return None
1498+
1499+ master = os.path.join(iface_path, 'master')
1500+ if os.path.exists(master):
1501+ master = os.path.realpath(master)
1502+ # make sure it is a bond master
1503+ if os.path.exists(os.path.join(master, 'bonding')):
1504+ return os.path.basename(master)
1505+
1506+ return None
1507+
1508+
1509+def list_nics(nic_type=None):
1510+ """Return a list of nics of given type(s)"""
1511 if isinstance(nic_type, six.string_types):
1512 int_types = [nic_type]
1513 else:
1514 int_types = nic_type
1515+
1516 interfaces = []
1517- for int_type in int_types:
1518- cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
1519+ if nic_type:
1520+ for int_type in int_types:
1521+ cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
1522+ ip_output = subprocess.check_output(cmd).decode('UTF-8')
1523+ ip_output = ip_output.split('\n')
1524+ ip_output = (line for line in ip_output if line)
1525+ for line in ip_output:
1526+ if line.split()[1].startswith(int_type):
1527+ matched = re.search('.*: (' + int_type +
1528+ r'[0-9]+\.[0-9]+)@.*', line)
1529+ if matched:
1530+ iface = matched.groups()[0]
1531+ else:
1532+ iface = line.split()[1].replace(":", "")
1533+
1534+ if iface not in interfaces:
1535+ interfaces.append(iface)
1536+ else:
1537+ cmd = ['ip', 'a']
1538 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
1539- ip_output = (line for line in ip_output if line)
1540+ ip_output = (line.strip() for line in ip_output if line)
1541+
1542+ key = re.compile('^[0-9]+:\s+(.+):')
1543 for line in ip_output:
1544- if line.split()[1].startswith(int_type):
1545- matched = re.search('.*: (bond[0-9]+\.[0-9]+)@.*', line)
1546- if matched:
1547- interface = matched.groups()[0]
1548- else:
1549- interface = line.split()[1].replace(":", "")
1550- interfaces.append(interface)
1551+ matched = re.search(key, line)
1552+ if matched:
1553+ iface = matched.group(1)
1554+ iface = iface.partition("@")[0]
1555+ if iface not in interfaces:
1556+ interfaces.append(iface)
1557
1558 return interfaces
1559
1560
1561 def set_nic_mtu(nic, mtu):
1562- '''Set MTU on a network interface'''
1563+ """Set the Maximum Transmission Unit (MTU) on a network interface."""
1564 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
1565 subprocess.check_call(cmd)
1566
1567
1568 def get_nic_mtu(nic):
1569+ """Return the Maximum Transmission Unit (MTU) for a network interface."""
1570 cmd = ['ip', 'addr', 'show', nic]
1571 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
1572 mtu = ""
1573@@ -373,6 +616,7 @@
1574
1575
1576 def get_nic_hwaddr(nic):
1577+ """Return the Media Access Control (MAC) for a network interface."""
1578 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
1579 ip_output = subprocess.check_output(cmd).decode('UTF-8')
1580 hwaddr = ""
1581@@ -383,13 +627,16 @@
1582
1583
1584 def cmp_pkgrevno(package, revno, pkgcache=None):
1585- '''Compare supplied revno with the revno of the installed package
1586+ """Compare supplied revno with the revno of the installed package
1587
1588 * 1 => Installed revno is greater than supplied arg
1589 * 0 => Installed revno is the same as supplied arg
1590 * -1 => Installed revno is less than supplied arg
1591
1592- '''
1593+ This function imports apt_cache function from charmhelpers.fetch if
1594+ the pkgcache argument is None. Be sure to add charmhelpers.fetch if
1595+ you call this function, or pass an apt_pkg.Cache() instance.
1596+ """
1597 import apt_pkg
1598 if not pkgcache:
1599 from charmhelpers.fetch import apt_cache
1600@@ -399,21 +646,72 @@
1601
1602
1603 @contextmanager
1604-def chdir(d):
1605+def chdir(directory):
1606+ """Change the current working directory to a different directory for a code
1607+ block and return the previous directory after the block exits. Useful to
1608+ run commands from a specificed directory.
1609+
1610+ :param str directory: The directory path to change to for this context.
1611+ """
1612 cur = os.getcwd()
1613 try:
1614- yield os.chdir(d)
1615+ yield os.chdir(directory)
1616 finally:
1617 os.chdir(cur)
1618
1619
1620-def chownr(path, owner, group):
1621+def chownr(path, owner, group, follow_links=True, chowntopdir=False):
1622+ """Recursively change user and group ownership of files and directories
1623+ in given path. Doesn't chown path itself by default, only its children.
1624+
1625+ :param str path: The string path to start changing ownership.
1626+ :param str owner: The owner string to use when looking up the uid.
1627+ :param str group: The group string to use when looking up the gid.
1628+ :param bool follow_links: Also Chown links if True
1629+ :param bool chowntopdir: Also chown path itself if True
1630+ """
1631 uid = pwd.getpwnam(owner).pw_uid
1632 gid = grp.getgrnam(group).gr_gid
1633+ if follow_links:
1634+ chown = os.chown
1635+ else:
1636+ chown = os.lchown
1637
1638+ if chowntopdir:
1639+ broken_symlink = os.path.lexists(path) and not os.path.exists(path)
1640+ if not broken_symlink:
1641+ chown(path, uid, gid)
1642 for root, dirs, files in os.walk(path):
1643 for name in dirs + files:
1644 full = os.path.join(root, name)
1645 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
1646 if not broken_symlink:
1647- os.chown(full, uid, gid)
1648+ chown(full, uid, gid)
1649+
1650+
1651+def lchownr(path, owner, group):
1652+ """Recursively change user and group ownership of files and directories
1653+ in a given path, not following symbolic links. See the documentation for
1654+ 'os.lchown' for more information.
1655+
1656+ :param str path: The string path to start changing ownership.
1657+ :param str owner: The owner string to use when looking up the uid.
1658+ :param str group: The group string to use when looking up the gid.
1659+ """
1660+ chownr(path, owner, group, follow_links=False)
1661+
1662+
1663+def get_total_ram():
1664+ """The total amount of system RAM in bytes.
1665+
1666+ This is what is reported by the OS, and may be overcommitted when
1667+ there are multiple containers hosted on the same machine.
1668+ """
1669+ with open('/proc/meminfo', 'r') as f:
1670+ for line in f.readlines():
1671+ if line:
1672+ key, value, unit = line.split()
1673+ if key == 'MemTotal:':
1674+ assert unit == 'kB', 'Unknown unit'
1675+ return int(value) * 1024 # Classic, not KiB.
1676+ raise NotImplementedError()
1677
1678=== added file 'hooks/charmhelpers/core/hugepage.py'
1679--- hooks/charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000
1680+++ hooks/charmhelpers/core/hugepage.py 2016-06-01 15:03:50 +0000
1681@@ -0,0 +1,71 @@
1682+# -*- coding: utf-8 -*-
1683+
1684+# Copyright 2014-2015 Canonical Limited.
1685+#
1686+# This file is part of charm-helpers.
1687+#
1688+# charm-helpers is free software: you can redistribute it and/or modify
1689+# it under the terms of the GNU Lesser General Public License version 3 as
1690+# published by the Free Software Foundation.
1691+#
1692+# charm-helpers is distributed in the hope that it will be useful,
1693+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1694+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1695+# GNU Lesser General Public License for more details.
1696+#
1697+# You should have received a copy of the GNU Lesser General Public License
1698+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1699+
1700+import yaml
1701+from charmhelpers.core import fstab
1702+from charmhelpers.core import sysctl
1703+from charmhelpers.core.host import (
1704+ add_group,
1705+ add_user_to_group,
1706+ fstab_mount,
1707+ mkdir,
1708+)
1709+from charmhelpers.core.strutils import bytes_from_string
1710+from subprocess import check_output
1711+
1712+
1713+def hugepage_support(user, group='hugetlb', nr_hugepages=256,
1714+ max_map_count=65536, mnt_point='/run/hugepages/kvm',
1715+ pagesize='2MB', mount=True, set_shmmax=False):
1716+ """Enable hugepages on system.
1717+
1718+ Args:
1719+ user (str) -- Username to allow access to hugepages to
1720+ group (str) -- Group name to own hugepages
1721+ nr_hugepages (int) -- Number of pages to reserve
1722+ max_map_count (int) -- Number of Virtual Memory Areas a process can own
1723+ mnt_point (str) -- Directory to mount hugepages on
1724+ pagesize (str) -- Size of hugepages
1725+ mount (bool) -- Whether to Mount hugepages
1726+ """
1727+ group_info = add_group(group)
1728+ gid = group_info.gr_gid
1729+ add_user_to_group(user, group)
1730+ if max_map_count < 2 * nr_hugepages:
1731+ max_map_count = 2 * nr_hugepages
1732+ sysctl_settings = {
1733+ 'vm.nr_hugepages': nr_hugepages,
1734+ 'vm.max_map_count': max_map_count,
1735+ 'vm.hugetlb_shm_group': gid,
1736+ }
1737+ if set_shmmax:
1738+ shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax']))
1739+ shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages
1740+ if shmmax_minsize > shmmax_current:
1741+ sysctl_settings['kernel.shmmax'] = shmmax_minsize
1742+ sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
1743+ mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
1744+ lfstab = fstab.Fstab()
1745+ fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point)
1746+ if fstab_entry:
1747+ lfstab.remove_entry(fstab_entry)
1748+ entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs',
1749+ 'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0)
1750+ lfstab.add_entry(entry)
1751+ if mount:
1752+ fstab_mount(mnt_point)
1753
1754=== added file 'hooks/charmhelpers/core/kernel.py'
1755--- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
1756+++ hooks/charmhelpers/core/kernel.py 2016-06-01 15:03:50 +0000
1757@@ -0,0 +1,68 @@
1758+#!/usr/bin/env python
1759+# -*- coding: utf-8 -*-
1760+
1761+# Copyright 2014-2015 Canonical Limited.
1762+#
1763+# This file is part of charm-helpers.
1764+#
1765+# charm-helpers is free software: you can redistribute it and/or modify
1766+# it under the terms of the GNU Lesser General Public License version 3 as
1767+# published by the Free Software Foundation.
1768+#
1769+# charm-helpers is distributed in the hope that it will be useful,
1770+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1771+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1772+# GNU Lesser General Public License for more details.
1773+#
1774+# You should have received a copy of the GNU Lesser General Public License
1775+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1776+
1777+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
1778+
1779+from charmhelpers.core.hookenv import (
1780+ log,
1781+ INFO
1782+)
1783+
1784+from subprocess import check_call, check_output
1785+import re
1786+
1787+
1788+def modprobe(module, persist=True):
1789+ """Load a kernel module and configure for auto-load on reboot."""
1790+ cmd = ['modprobe', module]
1791+
1792+ log('Loading kernel module %s' % module, level=INFO)
1793+
1794+ check_call(cmd)
1795+ if persist:
1796+ with open('/etc/modules', 'r+') as modules:
1797+ if module not in modules.read():
1798+ modules.write(module)
1799+
1800+
1801+def rmmod(module, force=False):
1802+ """Remove a module from the linux kernel"""
1803+ cmd = ['rmmod']
1804+ if force:
1805+ cmd.append('-f')
1806+ cmd.append(module)
1807+ log('Removing kernel module %s' % module, level=INFO)
1808+ return check_call(cmd)
1809+
1810+
1811+def lsmod():
1812+ """Shows what kernel modules are currently loaded"""
1813+ return check_output(['lsmod'],
1814+ universal_newlines=True)
1815+
1816+
1817+def is_module_loaded(module):
1818+ """Checks if a kernel module is already loaded"""
1819+ matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
1820+ return len(matches) > 0
1821+
1822+
1823+def update_initramfs(version='all'):
1824+ """Updates an initramfs image"""
1825+ return check_call(["update-initramfs", "-k", version, "-u"])
1826
1827=== modified file 'hooks/charmhelpers/core/services/__init__.py'
1828--- hooks/charmhelpers/core/services/__init__.py 2014-11-18 23:06:36 +0000
1829+++ hooks/charmhelpers/core/services/__init__.py 2016-06-01 15:03:50 +0000
1830@@ -1,2 +1,18 @@
1831+# Copyright 2014-2015 Canonical Limited.
1832+#
1833+# This file is part of charm-helpers.
1834+#
1835+# charm-helpers is free software: you can redistribute it and/or modify
1836+# it under the terms of the GNU Lesser General Public License version 3 as
1837+# published by the Free Software Foundation.
1838+#
1839+# charm-helpers is distributed in the hope that it will be useful,
1840+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1841+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1842+# GNU Lesser General Public License for more details.
1843+#
1844+# You should have received a copy of the GNU Lesser General Public License
1845+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1846+
1847 from .base import * # NOQA
1848 from .helpers import * # NOQA
1849
1850=== modified file 'hooks/charmhelpers/core/services/base.py'
1851--- hooks/charmhelpers/core/services/base.py 2014-11-18 23:06:36 +0000
1852+++ hooks/charmhelpers/core/services/base.py 2016-06-01 15:03:50 +0000
1853@@ -1,7 +1,23 @@
1854+# Copyright 2014-2015 Canonical Limited.
1855+#
1856+# This file is part of charm-helpers.
1857+#
1858+# charm-helpers is free software: you can redistribute it and/or modify
1859+# it under the terms of the GNU Lesser General Public License version 3 as
1860+# published by the Free Software Foundation.
1861+#
1862+# charm-helpers is distributed in the hope that it will be useful,
1863+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1864+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1865+# GNU Lesser General Public License for more details.
1866+#
1867+# You should have received a copy of the GNU Lesser General Public License
1868+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1869+
1870 import os
1871-import re
1872 import json
1873-from collections import Iterable
1874+from inspect import getargspec
1875+from collections import Iterable, OrderedDict
1876
1877 from charmhelpers.core import host
1878 from charmhelpers.core import hookenv
1879@@ -103,7 +119,7 @@
1880 """
1881 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
1882 self._ready = None
1883- self.services = {}
1884+ self.services = OrderedDict()
1885 for service in services or []:
1886 service_name = service['service']
1887 self.services[service_name] = service
1888@@ -112,15 +128,18 @@
1889 """
1890 Handle the current hook by doing The Right Thing with the registered services.
1891 """
1892- hook_name = hookenv.hook_name()
1893- if hook_name == 'stop':
1894- self.stop_services()
1895- else:
1896- self.provide_data()
1897- self.reconfigure_services()
1898- cfg = hookenv.config()
1899- if cfg.implicit_save:
1900- cfg.save()
1901+ hookenv._run_atstart()
1902+ try:
1903+ hook_name = hookenv.hook_name()
1904+ if hook_name == 'stop':
1905+ self.stop_services()
1906+ else:
1907+ self.reconfigure_services()
1908+ self.provide_data()
1909+ except SystemExit as x:
1910+ if x.code is None or x.code == 0:
1911+ hookenv._run_atexit()
1912+ hookenv._run_atexit()
1913
1914 def provide_data(self):
1915 """
1916@@ -129,15 +148,36 @@
1917 A provider must have a `name` attribute, which indicates which relation
1918 to set data on, and a `provide_data()` method, which returns a dict of
1919 data to set.
1920+
1921+ The `provide_data()` method can optionally accept two parameters:
1922+
1923+ * ``remote_service`` The name of the remote service that the data will
1924+ be provided to. The `provide_data()` method will be called once
1925+ for each connected service (not unit). This allows the method to
1926+ tailor its data to the given service.
1927+ * ``service_ready`` Whether or not the service definition had all of
1928+ its requirements met, and thus the ``data_ready`` callbacks run.
1929+
1930+ Note that the ``provided_data`` methods are now called **after** the
1931+ ``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks
1932+ a chance to generate any data necessary for the providing to the remote
1933+ services.
1934 """
1935- hook_name = hookenv.hook_name()
1936- for service in self.services.values():
1937+ for service_name, service in self.services.items():
1938+ service_ready = self.is_ready(service_name)
1939 for provider in service.get('provided_data', []):
1940- if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
1941- data = provider.provide_data()
1942- _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data
1943- if _ready:
1944- hookenv.relation_set(None, data)
1945+ for relid in hookenv.relation_ids(provider.name):
1946+ units = hookenv.related_units(relid)
1947+ if not units:
1948+ continue
1949+ remote_service = units[0].split('/')[0]
1950+ argspec = getargspec(provider.provide_data)
1951+ if len(argspec.args) > 1:
1952+ data = provider.provide_data(remote_service, service_ready)
1953+ else:
1954+ data = provider.provide_data()
1955+ if data:
1956+ hookenv.relation_set(relid, data)
1957
1958 def reconfigure_services(self, *service_names):
1959 """
1960
1961=== modified file 'hooks/charmhelpers/core/services/helpers.py'
1962--- hooks/charmhelpers/core/services/helpers.py 2014-12-11 10:35:04 +0000
1963+++ hooks/charmhelpers/core/services/helpers.py 2016-06-01 15:03:50 +0000
1964@@ -1,6 +1,24 @@
1965+# Copyright 2014-2015 Canonical Limited.
1966+#
1967+# This file is part of charm-helpers.
1968+#
1969+# charm-helpers is free software: you can redistribute it and/or modify
1970+# it under the terms of the GNU Lesser General Public License version 3 as
1971+# published by the Free Software Foundation.
1972+#
1973+# charm-helpers is distributed in the hope that it will be useful,
1974+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1975+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1976+# GNU Lesser General Public License for more details.
1977+#
1978+# You should have received a copy of the GNU Lesser General Public License
1979+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1980+
1981 import os
1982 import yaml
1983+
1984 from charmhelpers.core import hookenv
1985+from charmhelpers.core import host
1986 from charmhelpers.core import templating
1987
1988 from charmhelpers.core.services.base import ManagerCallback
1989@@ -29,12 +47,14 @@
1990 """
1991 name = None
1992 interface = None
1993- required_keys = []
1994
1995 def __init__(self, name=None, additional_required_keys=None):
1996+ if not hasattr(self, 'required_keys'):
1997+ self.required_keys = []
1998+
1999 if name is not None:
2000 self.name = name
2001- if additional_required_keys is not None:
2002+ if additional_required_keys:
2003 self.required_keys.extend(additional_required_keys)
2004 self.get_data()
2005
2006@@ -118,7 +138,10 @@
2007 """
2008 name = 'db'
2009 interface = 'mysql'
2010- required_keys = ['host', 'user', 'password', 'database']
2011+
2012+ def __init__(self, *args, **kwargs):
2013+ self.required_keys = ['host', 'user', 'password', 'database']
2014+ RelationContext.__init__(self, *args, **kwargs)
2015
2016
2017 class HttpRelation(RelationContext):
2018@@ -130,7 +153,10 @@
2019 """
2020 name = 'website'
2021 interface = 'http'
2022- required_keys = ['host', 'port']
2023+
2024+ def __init__(self, *args, **kwargs):
2025+ self.required_keys = ['host', 'port']
2026+ RelationContext.__init__(self, *args, **kwargs)
2027
2028 def provide_data(self):
2029 return {
2030@@ -215,28 +241,51 @@
2031 action.
2032
2033 :param str source: The template source file, relative to
2034- `$CHARM_DIR/templates`
2035+ `$CHARM_DIR/templates`
2036
2037- :param str target: The target to write the rendered template to
2038+ :param str target: The target to write the rendered template to (or None)
2039 :param str owner: The owner of the rendered file
2040 :param str group: The group of the rendered file
2041 :param int perms: The permissions of the rendered file
2042+ :param partial on_change_action: functools partial to be executed when
2043+ rendered file changes
2044+ :param jinja2 loader template_loader: A jinja2 template loader
2045+
2046+ :return str: The rendered template
2047 """
2048 def __init__(self, source, target,
2049- owner='root', group='root', perms=0o444):
2050+ owner='root', group='root', perms=0o444,
2051+ on_change_action=None, template_loader=None):
2052 self.source = source
2053 self.target = target
2054 self.owner = owner
2055 self.group = group
2056 self.perms = perms
2057+ self.on_change_action = on_change_action
2058+ self.template_loader = template_loader
2059
2060 def __call__(self, manager, service_name, event_name):
2061+ pre_checksum = ''
2062+ if self.on_change_action and os.path.isfile(self.target):
2063+ pre_checksum = host.file_hash(self.target)
2064 service = manager.get_service(service_name)
2065- context = {}
2066+ context = {'ctx': {}}
2067 for ctx in service.get('required_data', []):
2068 context.update(ctx)
2069- templating.render(self.source, self.target, context,
2070- self.owner, self.group, self.perms)
2071+ context['ctx'].update(ctx)
2072+
2073+ result = templating.render(self.source, self.target, context,
2074+ self.owner, self.group, self.perms,
2075+ template_loader=self.template_loader)
2076+ if self.on_change_action:
2077+ if pre_checksum == host.file_hash(self.target):
2078+ hookenv.log(
2079+ 'No change detected: {}'.format(self.target),
2080+ hookenv.DEBUG)
2081+ else:
2082+ self.on_change_action()
2083+
2084+ return result
2085
2086
2087 # Convenience aliases for templates
2088
2089=== added file 'hooks/charmhelpers/core/strutils.py'
2090--- hooks/charmhelpers/core/strutils.py 1970-01-01 00:00:00 +0000
2091+++ hooks/charmhelpers/core/strutils.py 2016-06-01 15:03:50 +0000
2092@@ -0,0 +1,72 @@
2093+#!/usr/bin/env python
2094+# -*- coding: utf-8 -*-
2095+
2096+# Copyright 2014-2015 Canonical Limited.
2097+#
2098+# This file is part of charm-helpers.
2099+#
2100+# charm-helpers is free software: you can redistribute it and/or modify
2101+# it under the terms of the GNU Lesser General Public License version 3 as
2102+# published by the Free Software Foundation.
2103+#
2104+# charm-helpers is distributed in the hope that it will be useful,
2105+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2106+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2107+# GNU Lesser General Public License for more details.
2108+#
2109+# You should have received a copy of the GNU Lesser General Public License
2110+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2111+
2112+import six
2113+import re
2114+
2115+
2116+def bool_from_string(value):
2117+ """Interpret string value as boolean.
2118+
2119+ Returns True if value translates to True otherwise False.
2120+ """
2121+ if isinstance(value, six.string_types):
2122+ value = six.text_type(value)
2123+ else:
2124+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
2125+ raise ValueError(msg)
2126+
2127+ value = value.strip().lower()
2128+
2129+ if value in ['y', 'yes', 'true', 't', 'on']:
2130+ return True
2131+ elif value in ['n', 'no', 'false', 'f', 'off']:
2132+ return False
2133+
2134+ msg = "Unable to interpret string value '%s' as boolean" % (value)
2135+ raise ValueError(msg)
2136+
2137+
2138+def bytes_from_string(value):
2139+ """Interpret human readable string value as bytes.
2140+
2141+ Returns int
2142+ """
2143+ BYTE_POWER = {
2144+ 'K': 1,
2145+ 'KB': 1,
2146+ 'M': 2,
2147+ 'MB': 2,
2148+ 'G': 3,
2149+ 'GB': 3,
2150+ 'T': 4,
2151+ 'TB': 4,
2152+ 'P': 5,
2153+ 'PB': 5,
2154+ }
2155+ if isinstance(value, six.string_types):
2156+ value = six.text_type(value)
2157+ else:
2158+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
2159+ raise ValueError(msg)
2160+ matches = re.match("([0-9]+)([a-zA-Z]+)", value)
2161+ if not matches:
2162+ msg = "Unable to interpret string value '%s' as bytes" % (value)
2163+ raise ValueError(msg)
2164+ return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
2165
2166=== modified file 'hooks/charmhelpers/core/sysctl.py'
2167--- hooks/charmhelpers/core/sysctl.py 2014-11-18 23:06:36 +0000
2168+++ hooks/charmhelpers/core/sysctl.py 2016-06-01 15:03:50 +0000
2169@@ -1,7 +1,21 @@
2170 #!/usr/bin/env python
2171 # -*- coding: utf-8 -*-
2172
2173-__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
2174+# Copyright 2014-2015 Canonical Limited.
2175+#
2176+# This file is part of charm-helpers.
2177+#
2178+# charm-helpers is free software: you can redistribute it and/or modify
2179+# it under the terms of the GNU Lesser General Public License version 3 as
2180+# published by the Free Software Foundation.
2181+#
2182+# charm-helpers is distributed in the hope that it will be useful,
2183+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2184+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2185+# GNU Lesser General Public License for more details.
2186+#
2187+# You should have received a copy of the GNU Lesser General Public License
2188+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2189
2190 import yaml
2191
2192@@ -10,25 +24,33 @@
2193 from charmhelpers.core.hookenv import (
2194 log,
2195 DEBUG,
2196+ ERROR,
2197 )
2198
2199+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
2200+
2201
2202 def create(sysctl_dict, sysctl_file):
2203 """Creates a sysctl.conf file from a YAML associative array
2204
2205- :param sysctl_dict: a dict of sysctl options eg { 'kernel.max_pid': 1337 }
2206- :type sysctl_dict: dict
2207+ :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }"
2208+ :type sysctl_dict: str
2209 :param sysctl_file: path to the sysctl file to be saved
2210 :type sysctl_file: str or unicode
2211 :returns: None
2212 """
2213- sysctl_dict = yaml.load(sysctl_dict)
2214+ try:
2215+ sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
2216+ except yaml.YAMLError:
2217+ log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
2218+ level=ERROR)
2219+ return
2220
2221 with open(sysctl_file, "w") as fd:
2222- for key, value in sysctl_dict.items():
2223+ for key, value in sysctl_dict_parsed.items():
2224 fd.write("{}={}\n".format(key, value))
2225
2226- log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict),
2227+ log("Updating sysctl_file: %s values: %s" % (sysctl_file, sysctl_dict_parsed),
2228 level=DEBUG)
2229
2230 check_call(["sysctl", "-p", sysctl_file])
2231
2232=== modified file 'hooks/charmhelpers/core/templating.py'
2233--- hooks/charmhelpers/core/templating.py 2015-01-20 18:22:29 +0000
2234+++ hooks/charmhelpers/core/templating.py 2016-06-01 15:03:50 +0000
2235@@ -1,3 +1,19 @@
2236+# Copyright 2014-2015 Canonical Limited.
2237+#
2238+# This file is part of charm-helpers.
2239+#
2240+# charm-helpers is free software: you can redistribute it and/or modify
2241+# it under the terms of the GNU Lesser General Public License version 3 as
2242+# published by the Free Software Foundation.
2243+#
2244+# charm-helpers is distributed in the hope that it will be useful,
2245+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2246+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2247+# GNU Lesser General Public License for more details.
2248+#
2249+# You should have received a copy of the GNU Lesser General Public License
2250+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2251+
2252 import os
2253
2254 from charmhelpers.core import host
2255@@ -5,13 +21,14 @@
2256
2257
2258 def render(source, target, context, owner='root', group='root',
2259- perms=0o444, templates_dir=None):
2260+ perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None):
2261 """
2262 Render a template.
2263
2264 The `source` path, if not absolute, is relative to the `templates_dir`.
2265
2266- The `target` path should be absolute.
2267+ The `target` path should be absolute. It can also be `None`, in which
2268+ case no file will be written.
2269
2270 The context should be a dict containing the values to be replaced in the
2271 template.
2272@@ -20,6 +37,9 @@
2273
2274 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
2275
2276+ The rendered template will be written to the file as well as being returned
2277+ as a string.
2278+
2279 Note: Using this requires python-jinja2; if it is not installed, calling
2280 this will attempt to use charmhelpers.fetch.apt_install to install it.
2281 """
2282@@ -36,17 +56,26 @@
2283 apt_install('python-jinja2', fatal=True)
2284 from jinja2 import FileSystemLoader, Environment, exceptions
2285
2286- if templates_dir is None:
2287- templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
2288- loader = Environment(loader=FileSystemLoader(templates_dir))
2289+ if template_loader:
2290+ template_env = Environment(loader=template_loader)
2291+ else:
2292+ if templates_dir is None:
2293+ templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
2294+ template_env = Environment(loader=FileSystemLoader(templates_dir))
2295 try:
2296 source = source
2297- template = loader.get_template(source)
2298+ template = template_env.get_template(source)
2299 except exceptions.TemplateNotFound as e:
2300 hookenv.log('Could not load template %s from %s.' %
2301 (source, templates_dir),
2302 level=hookenv.ERROR)
2303 raise e
2304 content = template.render(context)
2305- host.mkdir(os.path.dirname(target), owner, group)
2306- host.write_file(target, content, owner, group, perms)
2307+ if target is not None:
2308+ target_dir = os.path.dirname(target)
2309+ if not os.path.exists(target_dir):
2310+ # This is a terrible default directory permission, as the file
2311+ # or its siblings will often contain secrets.
2312+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
2313+ host.write_file(target, content.encode(encoding), owner, group, perms)
2314+ return content
2315
2316=== added file 'hooks/charmhelpers/core/unitdata.py'
2317--- hooks/charmhelpers/core/unitdata.py 1970-01-01 00:00:00 +0000
2318+++ hooks/charmhelpers/core/unitdata.py 2016-06-01 15:03:50 +0000
2319@@ -0,0 +1,521 @@
2320+#!/usr/bin/env python
2321+# -*- coding: utf-8 -*-
2322+#
2323+# Copyright 2014-2015 Canonical Limited.
2324+#
2325+# This file is part of charm-helpers.
2326+#
2327+# charm-helpers is free software: you can redistribute it and/or modify
2328+# it under the terms of the GNU Lesser General Public License version 3 as
2329+# published by the Free Software Foundation.
2330+#
2331+# charm-helpers is distributed in the hope that it will be useful,
2332+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2333+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2334+# GNU Lesser General Public License for more details.
2335+#
2336+# You should have received a copy of the GNU Lesser General Public License
2337+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2338+#
2339+#
2340+# Authors:
2341+# Kapil Thangavelu <kapil.foss@gmail.com>
2342+#
2343+"""
2344+Intro
2345+-----
2346+
2347+A simple way to store state in units. This provides a key value
2348+storage with support for versioned, transactional operation,
2349+and can calculate deltas from previous values to simplify unit logic
2350+when processing changes.
2351+
2352+
2353+Hook Integration
2354+----------------
2355+
2356+There are several extant frameworks for hook execution, including
2357+
2358+ - charmhelpers.core.hookenv.Hooks
2359+ - charmhelpers.core.services.ServiceManager
2360+
2361+The storage classes are framework agnostic, one simple integration is
2362+via the HookData contextmanager. It will record the current hook
2363+execution environment (including relation data, config data, etc.),
2364+setup a transaction and allow easy access to the changes from
2365+previously seen values. One consequence of the integration is the
2366+reservation of particular keys ('rels', 'unit', 'env', 'config',
2367+'charm_revisions') for their respective values.
2368+
2369+Here's a fully worked integration example using hookenv.Hooks::
2370+
2371+ from charmhelper.core import hookenv, unitdata
2372+
2373+ hook_data = unitdata.HookData()
2374+ db = unitdata.kv()
2375+ hooks = hookenv.Hooks()
2376+
2377+ @hooks.hook
2378+ def config_changed():
2379+ # Print all changes to configuration from previously seen
2380+ # values.
2381+ for changed, (prev, cur) in hook_data.conf.items():
2382+ print('config changed', changed,
2383+ 'previous value', prev,
2384+ 'current value', cur)
2385+
2386+ # Get some unit specific bookeeping
2387+ if not db.get('pkg_key'):
2388+ key = urllib.urlopen('https://example.com/pkg_key').read()
2389+ db.set('pkg_key', key)
2390+
2391+ # Directly access all charm config as a mapping.
2392+ conf = db.getrange('config', True)
2393+
2394+ # Directly access all relation data as a mapping
2395+ rels = db.getrange('rels', True)
2396+
2397+ if __name__ == '__main__':
2398+ with hook_data():
2399+ hook.execute()
2400+
2401+
2402+A more basic integration is via the hook_scope context manager which simply
2403+manages transaction scope (and records hook name, and timestamp)::
2404+
2405+ >>> from unitdata import kv
2406+ >>> db = kv()
2407+ >>> with db.hook_scope('install'):
2408+ ... # do work, in transactional scope.
2409+ ... db.set('x', 1)
2410+ >>> db.get('x')
2411+ 1
2412+
2413+
2414+Usage
2415+-----
2416+
2417+Values are automatically json de/serialized to preserve basic typing
2418+and complex data struct capabilities (dicts, lists, ints, booleans, etc).
2419+
2420+Individual values can be manipulated via get/set::
2421+
2422+ >>> kv.set('y', True)
2423+ >>> kv.get('y')
2424+ True
2425+
2426+ # We can set complex values (dicts, lists) as a single key.
2427+ >>> kv.set('config', {'a': 1, 'b': True'})
2428+
2429+ # Also supports returning dictionaries as a record which
2430+ # provides attribute access.
2431+ >>> config = kv.get('config', record=True)
2432+ >>> config.b
2433+ True
2434+
2435+
2436+Groups of keys can be manipulated with update/getrange::
2437+
2438+ >>> kv.update({'z': 1, 'y': 2}, prefix="gui.")
2439+ >>> kv.getrange('gui.', strip=True)
2440+ {'z': 1, 'y': 2}
2441+
2442+When updating values, its very helpful to understand which values
2443+have actually changed and how have they changed. The storage
2444+provides a delta method to provide for this::
2445+
2446+ >>> data = {'debug': True, 'option': 2}
2447+ >>> delta = kv.delta(data, 'config.')
2448+ >>> delta.debug.previous
2449+ None
2450+ >>> delta.debug.current
2451+ True
2452+ >>> delta
2453+ {'debug': (None, True), 'option': (None, 2)}
2454+
2455+Note the delta method does not persist the actual change, it needs to
2456+be explicitly saved via 'update' method::
2457+
2458+ >>> kv.update(data, 'config.')
2459+
2460+Values modified in the context of a hook scope retain historical values
2461+associated to the hookname.
2462+
2463+ >>> with db.hook_scope('config-changed'):
2464+ ... db.set('x', 42)
2465+ >>> db.gethistory('x')
2466+ [(1, u'x', 1, u'install', u'2015-01-21T16:49:30.038372'),
2467+ (2, u'x', 42, u'config-changed', u'2015-01-21T16:49:30.038786')]
2468+
2469+"""
2470+
2471+import collections
2472+import contextlib
2473+import datetime
2474+import itertools
2475+import json
2476+import os
2477+import pprint
2478+import sqlite3
2479+import sys
2480+
2481+__author__ = 'Kapil Thangavelu <kapil.foss@gmail.com>'
2482+
2483+
2484+class Storage(object):
2485+ """Simple key value database for local unit state within charms.
2486+
2487+ Modifications are not persisted unless :meth:`flush` is called.
2488+
2489+ To support dicts, lists, integer, floats, and booleans values
2490+ are automatically json encoded/decoded.
2491+ """
2492+ def __init__(self, path=None):
2493+ self.db_path = path
2494+ if path is None:
2495+ if 'UNIT_STATE_DB' in os.environ:
2496+ self.db_path = os.environ['UNIT_STATE_DB']
2497+ else:
2498+ self.db_path = os.path.join(
2499+ os.environ.get('CHARM_DIR', ''), '.unit-state.db')
2500+ self.conn = sqlite3.connect('%s' % self.db_path)
2501+ self.cursor = self.conn.cursor()
2502+ self.revision = None
2503+ self._closed = False
2504+ self._init()
2505+
2506+ def close(self):
2507+ if self._closed:
2508+ return
2509+ self.flush(False)
2510+ self.cursor.close()
2511+ self.conn.close()
2512+ self._closed = True
2513+
2514+ def get(self, key, default=None, record=False):
2515+ self.cursor.execute('select data from kv where key=?', [key])
2516+ result = self.cursor.fetchone()
2517+ if not result:
2518+ return default
2519+ if record:
2520+ return Record(json.loads(result[0]))
2521+ return json.loads(result[0])
2522+
2523+ def getrange(self, key_prefix, strip=False):
2524+ """
2525+ Get a range of keys starting with a common prefix as a mapping of
2526+ keys to values.
2527+
2528+ :param str key_prefix: Common prefix among all keys
2529+ :param bool strip: Optionally strip the common prefix from the key
2530+ names in the returned dict
2531+ :return dict: A (possibly empty) dict of key-value mappings
2532+ """
2533+ self.cursor.execute("select key, data from kv where key like ?",
2534+ ['%s%%' % key_prefix])
2535+ result = self.cursor.fetchall()
2536+
2537+ if not result:
2538+ return {}
2539+ if not strip:
2540+ key_prefix = ''
2541+ return dict([
2542+ (k[len(key_prefix):], json.loads(v)) for k, v in result])
2543+
2544+ def update(self, mapping, prefix=""):
2545+ """
2546+ Set the values of multiple keys at once.
2547+
2548+ :param dict mapping: Mapping of keys to values
2549+ :param str prefix: Optional prefix to apply to all keys in `mapping`
2550+ before setting
2551+ """
2552+ for k, v in mapping.items():
2553+ self.set("%s%s" % (prefix, k), v)
2554+
2555+ def unset(self, key):
2556+ """
2557+ Remove a key from the database entirely.
2558+ """
2559+ self.cursor.execute('delete from kv where key=?', [key])
2560+ if self.revision and self.cursor.rowcount:
2561+ self.cursor.execute(
2562+ 'insert into kv_revisions values (?, ?, ?)',
2563+ [key, self.revision, json.dumps('DELETED')])
2564+
2565+ def unsetrange(self, keys=None, prefix=""):
2566+ """
2567+ Remove a range of keys starting with a common prefix, from the database
2568+ entirely.
2569+
2570+ :param list keys: List of keys to remove.
2571+ :param str prefix: Optional prefix to apply to all keys in ``keys``
2572+ before removing.
2573+ """
2574+ if keys is not None:
2575+ keys = ['%s%s' % (prefix, key) for key in keys]
2576+ self.cursor.execute('delete from kv where key in (%s)' % ','.join(['?'] * len(keys)), keys)
2577+ if self.revision and self.cursor.rowcount:
2578+ self.cursor.execute(
2579+ 'insert into kv_revisions values %s' % ','.join(['(?, ?, ?)'] * len(keys)),
2580+ list(itertools.chain.from_iterable((key, self.revision, json.dumps('DELETED')) for key in keys)))
2581+ else:
2582+ self.cursor.execute('delete from kv where key like ?',
2583+ ['%s%%' % prefix])
2584+ if self.revision and self.cursor.rowcount:
2585+ self.cursor.execute(
2586+ 'insert into kv_revisions values (?, ?, ?)',
2587+ ['%s%%' % prefix, self.revision, json.dumps('DELETED')])
2588+
2589+ def set(self, key, value):
2590+ """
2591+ Set a value in the database.
2592+
2593+ :param str key: Key to set the value for
2594+ :param value: Any JSON-serializable value to be set
2595+ """
2596+ serialized = json.dumps(value)
2597+
2598+ self.cursor.execute('select data from kv where key=?', [key])
2599+ exists = self.cursor.fetchone()
2600+
2601+ # Skip mutations to the same value
2602+ if exists:
2603+ if exists[0] == serialized:
2604+ return value
2605+
2606+ if not exists:
2607+ self.cursor.execute(
2608+ 'insert into kv (key, data) values (?, ?)',
2609+ (key, serialized))
2610+ else:
2611+ self.cursor.execute('''
2612+ update kv
2613+ set data = ?
2614+ where key = ?''', [serialized, key])
2615+
2616+ # Save
2617+ if not self.revision:
2618+ return value
2619+
2620+ self.cursor.execute(
2621+ 'select 1 from kv_revisions where key=? and revision=?',
2622+ [key, self.revision])
2623+ exists = self.cursor.fetchone()
2624+
2625+ if not exists:
2626+ self.cursor.execute(
2627+ '''insert into kv_revisions (
2628+ revision, key, data) values (?, ?, ?)''',
2629+ (self.revision, key, serialized))
2630+ else:
2631+ self.cursor.execute(
2632+ '''
2633+ update kv_revisions
2634+ set data = ?
2635+ where key = ?
2636+ and revision = ?''',
2637+ [serialized, key, self.revision])
2638+
2639+ return value
2640+
2641+ def delta(self, mapping, prefix):
2642+ """
2643+ return a delta containing values that have changed.
2644+ """
2645+ previous = self.getrange(prefix, strip=True)
2646+ if not previous:
2647+ pk = set()
2648+ else:
2649+ pk = set(previous.keys())
2650+ ck = set(mapping.keys())
2651+ delta = DeltaSet()
2652+
2653+ # added
2654+ for k in ck.difference(pk):
2655+ delta[k] = Delta(None, mapping[k])
2656+
2657+ # removed
2658+ for k in pk.difference(ck):
2659+ delta[k] = Delta(previous[k], None)
2660+
2661+ # changed
2662+ for k in pk.intersection(ck):
2663+ c = mapping[k]
2664+ p = previous[k]
2665+ if c != p:
2666+ delta[k] = Delta(p, c)
2667+
2668+ return delta
2669+
2670+ @contextlib.contextmanager
2671+ def hook_scope(self, name=""):
2672+ """Scope all future interactions to the current hook execution
2673+ revision."""
2674+ assert not self.revision
2675+ self.cursor.execute(
2676+ 'insert into hooks (hook, date) values (?, ?)',
2677+ (name or sys.argv[0],
2678+ datetime.datetime.utcnow().isoformat()))
2679+ self.revision = self.cursor.lastrowid
2680+ try:
2681+ yield self.revision
2682+ self.revision = None
2683+ except:
2684+ self.flush(False)
2685+ self.revision = None
2686+ raise
2687+ else:
2688+ self.flush()
2689+
2690+ def flush(self, save=True):
2691+ if save:
2692+ self.conn.commit()
2693+ elif self._closed:
2694+ return
2695+ else:
2696+ self.conn.rollback()
2697+
2698+ def _init(self):
2699+ self.cursor.execute('''
2700+ create table if not exists kv (
2701+ key text,
2702+ data text,
2703+ primary key (key)
2704+ )''')
2705+ self.cursor.execute('''
2706+ create table if not exists kv_revisions (
2707+ key text,
2708+ revision integer,
2709+ data text,
2710+ primary key (key, revision)
2711+ )''')
2712+ self.cursor.execute('''
2713+ create table if not exists hooks (
2714+ version integer primary key autoincrement,
2715+ hook text,
2716+ date text
2717+ )''')
2718+ self.conn.commit()
2719+
2720+ def gethistory(self, key, deserialize=False):
2721+ self.cursor.execute(
2722+ '''
2723+ select kv.revision, kv.key, kv.data, h.hook, h.date
2724+ from kv_revisions kv,
2725+ hooks h
2726+ where kv.key=?
2727+ and kv.revision = h.version
2728+ ''', [key])
2729+ if deserialize is False:
2730+ return self.cursor.fetchall()
2731+ return map(_parse_history, self.cursor.fetchall())
2732+
2733+ def debug(self, fh=sys.stderr):
2734+ self.cursor.execute('select * from kv')
2735+ pprint.pprint(self.cursor.fetchall(), stream=fh)
2736+ self.cursor.execute('select * from kv_revisions')
2737+ pprint.pprint(self.cursor.fetchall(), stream=fh)
2738+
2739+
2740+def _parse_history(d):
2741+ return (d[0], d[1], json.loads(d[2]), d[3],
2742+ datetime.datetime.strptime(d[-1], "%Y-%m-%dT%H:%M:%S.%f"))
2743+
2744+
2745+class HookData(object):
2746+ """Simple integration for existing hook exec frameworks.
2747+
2748+ Records all unit information, and stores deltas for processing
2749+ by the hook.
2750+
2751+ Sample::
2752+
2753+ from charmhelper.core import hookenv, unitdata
2754+
2755+ changes = unitdata.HookData()
2756+ db = unitdata.kv()
2757+ hooks = hookenv.Hooks()
2758+
2759+ @hooks.hook
2760+ def config_changed():
2761+ # View all changes to configuration
2762+ for changed, (prev, cur) in changes.conf.items():
2763+ print('config changed', changed,
2764+ 'previous value', prev,
2765+ 'current value', cur)
2766+
2767+ # Get some unit specific bookeeping
2768+ if not db.get('pkg_key'):
2769+ key = urllib.urlopen('https://example.com/pkg_key').read()
2770+ db.set('pkg_key', key)
2771+
2772+ if __name__ == '__main__':
2773+ with changes():
2774+ hook.execute()
2775+
2776+ """
2777+ def __init__(self):
2778+ self.kv = kv()
2779+ self.conf = None
2780+ self.rels = None
2781+
2782+ @contextlib.contextmanager
2783+ def __call__(self):
2784+ from charmhelpers.core import hookenv
2785+ hook_name = hookenv.hook_name()
2786+
2787+ with self.kv.hook_scope(hook_name):
2788+ self._record_charm_version(hookenv.charm_dir())
2789+ delta_config, delta_relation = self._record_hook(hookenv)
2790+ yield self.kv, delta_config, delta_relation
2791+
2792+ def _record_charm_version(self, charm_dir):
2793+ # Record revisions.. charm revisions are meaningless
2794+ # to charm authors as they don't control the revision.
2795+ # so logic dependnent on revision is not particularly
2796+ # useful, however it is useful for debugging analysis.
2797+ charm_rev = open(
2798+ os.path.join(charm_dir, 'revision')).read().strip()
2799+ charm_rev = charm_rev or '0'
2800+ revs = self.kv.get('charm_revisions', [])
2801+ if charm_rev not in revs:
2802+ revs.append(charm_rev.strip() or '0')
2803+ self.kv.set('charm_revisions', revs)
2804+
2805+ def _record_hook(self, hookenv):
2806+ data = hookenv.execution_environment()
2807+ self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
2808+ self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
2809+ self.kv.set('env', dict(data['env']))
2810+ self.kv.set('unit', data['unit'])
2811+ self.kv.set('relid', data.get('relid'))
2812+ return conf_delta, rels_delta
2813+
2814+
2815+class Record(dict):
2816+
2817+ __slots__ = ()
2818+
2819+ def __getattr__(self, k):
2820+ if k in self:
2821+ return self[k]
2822+ raise AttributeError(k)
2823+
2824+
2825+class DeltaSet(Record):
2826+
2827+ __slots__ = ()
2828+
2829+
2830+Delta = collections.namedtuple('Delta', ['previous', 'current'])
2831+
2832+
2833+_KV = None
2834+
2835+
2836+def kv():
2837+ global _KV
2838+ if _KV is None:
2839+ _KV = Storage()
2840+ return _KV
2841
2842=== modified file 'hooks/charmhelpers/fetch/__init__.py'
2843--- hooks/charmhelpers/fetch/__init__.py 2015-01-20 18:22:29 +0000
2844+++ hooks/charmhelpers/fetch/__init__.py 2016-06-01 15:03:50 +0000
2845@@ -1,3 +1,19 @@
2846+# Copyright 2014-2015 Canonical Limited.
2847+#
2848+# This file is part of charm-helpers.
2849+#
2850+# charm-helpers is free software: you can redistribute it and/or modify
2851+# it under the terms of the GNU Lesser General Public License version 3 as
2852+# published by the Free Software Foundation.
2853+#
2854+# charm-helpers is distributed in the hope that it will be useful,
2855+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2856+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2857+# GNU Lesser General Public License for more details.
2858+#
2859+# You should have received a copy of the GNU Lesser General Public License
2860+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2861+
2862 import importlib
2863 from tempfile import NamedTemporaryFile
2864 import time
2865@@ -74,6 +90,22 @@
2866 'kilo/proposed': 'trusty-proposed/kilo',
2867 'trusty-kilo/proposed': 'trusty-proposed/kilo',
2868 'trusty-proposed/kilo': 'trusty-proposed/kilo',
2869+ # Liberty
2870+ 'liberty': 'trusty-updates/liberty',
2871+ 'trusty-liberty': 'trusty-updates/liberty',
2872+ 'trusty-liberty/updates': 'trusty-updates/liberty',
2873+ 'trusty-updates/liberty': 'trusty-updates/liberty',
2874+ 'liberty/proposed': 'trusty-proposed/liberty',
2875+ 'trusty-liberty/proposed': 'trusty-proposed/liberty',
2876+ 'trusty-proposed/liberty': 'trusty-proposed/liberty',
2877+ # Mitaka
2878+ 'mitaka': 'trusty-updates/mitaka',
2879+ 'trusty-mitaka': 'trusty-updates/mitaka',
2880+ 'trusty-mitaka/updates': 'trusty-updates/mitaka',
2881+ 'trusty-updates/mitaka': 'trusty-updates/mitaka',
2882+ 'mitaka/proposed': 'trusty-proposed/mitaka',
2883+ 'trusty-mitaka/proposed': 'trusty-proposed/mitaka',
2884+ 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
2885 }
2886
2887 # The order of this list is very important. Handlers should be listed in from
2888@@ -142,7 +174,7 @@
2889
2890 def apt_cache(in_memory=True):
2891 """Build and return an apt cache"""
2892- import apt_pkg
2893+ from apt import apt_pkg
2894 apt_pkg.init()
2895 if in_memory:
2896 apt_pkg.config.set("Dir::Cache::pkgcache", "")
2897@@ -199,19 +231,27 @@
2898 _run_apt_command(cmd, fatal)
2899
2900
2901+def apt_mark(packages, mark, fatal=False):
2902+ """Flag one or more packages using apt-mark"""
2903+ log("Marking {} as {}".format(packages, mark))
2904+ cmd = ['apt-mark', mark]
2905+ if isinstance(packages, six.string_types):
2906+ cmd.append(packages)
2907+ else:
2908+ cmd.extend(packages)
2909+
2910+ if fatal:
2911+ subprocess.check_call(cmd, universal_newlines=True)
2912+ else:
2913+ subprocess.call(cmd, universal_newlines=True)
2914+
2915+
2916 def apt_hold(packages, fatal=False):
2917- """Hold one or more packages"""
2918- cmd = ['apt-mark', 'hold']
2919- if isinstance(packages, six.string_types):
2920- cmd.append(packages)
2921- else:
2922- cmd.extend(packages)
2923- log("Holding {}".format(packages))
2924-
2925- if fatal:
2926- subprocess.check_call(cmd)
2927- else:
2928- subprocess.call(cmd)
2929+ return apt_mark(packages, 'hold', fatal=fatal)
2930+
2931+
2932+def apt_unhold(packages, fatal=False):
2933+ return apt_mark(packages, 'unhold', fatal=fatal)
2934
2935
2936 def add_source(source, key=None):
2937@@ -354,8 +394,9 @@
2938 for handler in handlers:
2939 try:
2940 installed_to = handler.install(source, *args, **kwargs)
2941- except UnhandledSource:
2942- pass
2943+ except UnhandledSource as e:
2944+ log('Install source attempt unsuccessful: {}'.format(e),
2945+ level='WARNING')
2946 if not installed_to:
2947 raise UnhandledSource("No handler found for source {}".format(source))
2948 return installed_to
2949@@ -378,7 +419,7 @@
2950 importlib.import_module(package),
2951 classname)
2952 plugin_list.append(handler_class())
2953- except (ImportError, AttributeError):
2954+ except NotImplementedError:
2955 # Skip missing plugins so that they can be ommitted from
2956 # installation if desired
2957 log("FetchHandler {} not found, skipping plugin".format(
2958
2959=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
2960--- hooks/charmhelpers/fetch/archiveurl.py 2014-12-11 10:35:04 +0000
2961+++ hooks/charmhelpers/fetch/archiveurl.py 2016-06-01 15:03:50 +0000
2962@@ -1,7 +1,33 @@
2963+# Copyright 2014-2015 Canonical Limited.
2964+#
2965+# This file is part of charm-helpers.
2966+#
2967+# charm-helpers is free software: you can redistribute it and/or modify
2968+# it under the terms of the GNU Lesser General Public License version 3 as
2969+# published by the Free Software Foundation.
2970+#
2971+# charm-helpers is distributed in the hope that it will be useful,
2972+# but WITHOUT ANY WARRANTY; without even the implied warranty of
2973+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2974+# GNU Lesser General Public License for more details.
2975+#
2976+# You should have received a copy of the GNU Lesser General Public License
2977+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2978+
2979 import os
2980 import hashlib
2981 import re
2982
2983+from charmhelpers.fetch import (
2984+ BaseFetchHandler,
2985+ UnhandledSource
2986+)
2987+from charmhelpers.payload.archive import (
2988+ get_archive_handler,
2989+ extract,
2990+)
2991+from charmhelpers.core.host import mkdir, check_hash
2992+
2993 import six
2994 if six.PY3:
2995 from urllib.request import (
2996@@ -19,16 +45,6 @@
2997 )
2998 from urlparse import urlparse, urlunparse, parse_qs
2999
3000-from charmhelpers.fetch import (
3001- BaseFetchHandler,
3002- UnhandledSource
3003-)
3004-from charmhelpers.payload.archive import (
3005- get_archive_handler,
3006- extract,
3007-)
3008-from charmhelpers.core.host import mkdir, check_hash
3009-
3010
3011 def splituser(host):
3012 '''urllib.splituser(), but six's support of this seems broken'''
3013@@ -61,6 +77,8 @@
3014 def can_handle(self, source):
3015 url_parts = self.parse_url(source)
3016 if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
3017+ # XXX: Why is this returning a boolean and a string? It's
3018+ # doomed to fail since "bool(can_handle('foo://'))" will be True.
3019 return "Wrong source type"
3020 if get_archive_handler(self.base_url(source)):
3021 return True
3022@@ -90,7 +108,7 @@
3023 install_opener(opener)
3024 response = urlopen(source)
3025 try:
3026- with open(dest, 'w') as dest_file:
3027+ with open(dest, 'wb') as dest_file:
3028 dest_file.write(response.read())
3029 except Exception as e:
3030 if os.path.isfile(dest):
3031@@ -139,7 +157,11 @@
3032 else:
3033 algorithms = hashlib.algorithms_available
3034 if key in algorithms:
3035- check_hash(dld_file, value, key)
3036+ if len(value) != 1:
3037+ raise TypeError(
3038+ "Expected 1 hash value, not %d" % len(value))
3039+ expected = value[0]
3040+ check_hash(dld_file, expected, key)
3041 if checksum:
3042 check_hash(dld_file, checksum, hash_type)
3043 return extract(dld_file, dest)
3044
3045=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
3046--- hooks/charmhelpers/fetch/bzrurl.py 2014-12-11 10:35:04 +0000
3047+++ hooks/charmhelpers/fetch/bzrurl.py 2016-06-01 15:03:50 +0000
3048@@ -1,50 +1,64 @@
3049+# Copyright 2014-2015 Canonical Limited.
3050+#
3051+# This file is part of charm-helpers.
3052+#
3053+# charm-helpers is free software: you can redistribute it and/or modify
3054+# it under the terms of the GNU Lesser General Public License version 3 as
3055+# published by the Free Software Foundation.
3056+#
3057+# charm-helpers is distributed in the hope that it will be useful,
3058+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3059+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3060+# GNU Lesser General Public License for more details.
3061+#
3062+# You should have received a copy of the GNU Lesser General Public License
3063+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3064+
3065 import os
3066+from subprocess import check_call
3067 from charmhelpers.fetch import (
3068 BaseFetchHandler,
3069- UnhandledSource
3070+ UnhandledSource,
3071+ filter_installed_packages,
3072+ apt_install,
3073 )
3074 from charmhelpers.core.host import mkdir
3075
3076-import six
3077-if six.PY3:
3078- raise ImportError('bzrlib does not support Python3')
3079
3080-try:
3081- from bzrlib.branch import Branch
3082-except ImportError:
3083- from charmhelpers.fetch import apt_install
3084- apt_install("python-bzrlib")
3085- from bzrlib.branch import Branch
3086+if filter_installed_packages(['bzr']) != []:
3087+ apt_install(['bzr'])
3088+ if filter_installed_packages(['bzr']) != []:
3089+ raise NotImplementedError('Unable to install bzr')
3090
3091
3092 class BzrUrlFetchHandler(BaseFetchHandler):
3093 """Handler for bazaar branches via generic and lp URLs"""
3094 def can_handle(self, source):
3095 url_parts = self.parse_url(source)
3096- if url_parts.scheme not in ('bzr+ssh', 'lp'):
3097+ if url_parts.scheme not in ('bzr+ssh', 'lp', ''):
3098 return False
3099+ elif not url_parts.scheme:
3100+ return os.path.exists(os.path.join(source, '.bzr'))
3101 else:
3102 return True
3103
3104 def branch(self, source, dest):
3105- url_parts = self.parse_url(source)
3106- # If we use lp:branchname scheme we need to load plugins
3107 if not self.can_handle(source):
3108 raise UnhandledSource("Cannot handle {}".format(source))
3109- if url_parts.scheme == "lp":
3110- from bzrlib.plugin import load_plugins
3111- load_plugins()
3112- try:
3113- remote_branch = Branch.open(source)
3114- remote_branch.bzrdir.sprout(dest).open_branch()
3115- except Exception as e:
3116- raise e
3117+ if os.path.exists(dest):
3118+ check_call(['bzr', 'pull', '--overwrite', '-d', dest, source])
3119+ else:
3120+ check_call(['bzr', 'branch', source, dest])
3121
3122- def install(self, source):
3123+ def install(self, source, dest=None):
3124 url_parts = self.parse_url(source)
3125 branch_name = url_parts.path.strip("/").split("/")[-1]
3126- dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
3127- branch_name)
3128+ if dest:
3129+ dest_dir = os.path.join(dest, branch_name)
3130+ else:
3131+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
3132+ branch_name)
3133+
3134 if not os.path.exists(dest_dir):
3135 mkdir(dest_dir, perms=0o755)
3136 try:
3137
3138=== modified file 'hooks/charmhelpers/fetch/giturl.py'
3139--- hooks/charmhelpers/fetch/giturl.py 2014-12-11 10:35:04 +0000
3140+++ hooks/charmhelpers/fetch/giturl.py 2016-06-01 15:03:50 +0000
3141@@ -1,20 +1,32 @@
3142+# Copyright 2014-2015 Canonical Limited.
3143+#
3144+# This file is part of charm-helpers.
3145+#
3146+# charm-helpers is free software: you can redistribute it and/or modify
3147+# it under the terms of the GNU Lesser General Public License version 3 as
3148+# published by the Free Software Foundation.
3149+#
3150+# charm-helpers is distributed in the hope that it will be useful,
3151+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3152+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3153+# GNU Lesser General Public License for more details.
3154+#
3155+# You should have received a copy of the GNU Lesser General Public License
3156+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3157+
3158 import os
3159+from subprocess import check_call, CalledProcessError
3160 from charmhelpers.fetch import (
3161 BaseFetchHandler,
3162- UnhandledSource
3163+ UnhandledSource,
3164+ filter_installed_packages,
3165+ apt_install,
3166 )
3167-from charmhelpers.core.host import mkdir
3168-
3169-import six
3170-if six.PY3:
3171- raise ImportError('GitPython does not support Python 3')
3172-
3173-try:
3174- from git import Repo
3175-except ImportError:
3176- from charmhelpers.fetch import apt_install
3177- apt_install("python-git")
3178- from git import Repo
3179+
3180+if filter_installed_packages(['git']) != []:
3181+ apt_install(['git'])
3182+ if filter_installed_packages(['git']) != []:
3183+ raise NotImplementedError('Unable to install git')
3184
3185
3186 class GitUrlFetchHandler(BaseFetchHandler):
3187@@ -22,19 +34,26 @@
3188 def can_handle(self, source):
3189 url_parts = self.parse_url(source)
3190 # TODO (mattyw) no support for ssh git@ yet
3191- if url_parts.scheme not in ('http', 'https', 'git'):
3192+ if url_parts.scheme not in ('http', 'https', 'git', ''):
3193 return False
3194+ elif not url_parts.scheme:
3195+ return os.path.exists(os.path.join(source, '.git'))
3196 else:
3197 return True
3198
3199- def clone(self, source, dest, branch):
3200+ def clone(self, source, dest, branch="master", depth=None):
3201 if not self.can_handle(source):
3202 raise UnhandledSource("Cannot handle {}".format(source))
3203
3204- repo = Repo.clone_from(source, dest)
3205- repo.git.checkout(branch)
3206+ if os.path.exists(dest):
3207+ cmd = ['git', '-C', dest, 'pull', source, branch]
3208+ else:
3209+ cmd = ['git', 'clone', source, dest, '--branch', branch]
3210+ if depth:
3211+ cmd.extend(['--depth', depth])
3212+ check_call(cmd)
3213
3214- def install(self, source, branch="master", dest=None):
3215+ def install(self, source, branch="master", dest=None, depth=None):
3216 url_parts = self.parse_url(source)
3217 branch_name = url_parts.path.strip("/").split("/")[-1]
3218 if dest:
3219@@ -42,10 +61,10 @@
3220 else:
3221 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
3222 branch_name)
3223- if not os.path.exists(dest_dir):
3224- mkdir(dest_dir, perms=0o755)
3225 try:
3226- self.clone(source, dest_dir, branch)
3227+ self.clone(source, dest_dir, branch, depth)
3228+ except CalledProcessError as e:
3229+ raise UnhandledSource(e)
3230 except OSError as e:
3231 raise UnhandledSource(e.strerror)
3232 return dest_dir
3233
3234=== modified file 'hooks/charmhelpers/payload/__init__.py'
3235--- hooks/charmhelpers/payload/__init__.py 2014-11-18 23:06:36 +0000
3236+++ hooks/charmhelpers/payload/__init__.py 2016-06-01 15:03:50 +0000
3237@@ -1,1 +1,17 @@
3238+# Copyright 2014-2015 Canonical Limited.
3239+#
3240+# This file is part of charm-helpers.
3241+#
3242+# charm-helpers is free software: you can redistribute it and/or modify
3243+# it under the terms of the GNU Lesser General Public License version 3 as
3244+# published by the Free Software Foundation.
3245+#
3246+# charm-helpers is distributed in the hope that it will be useful,
3247+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3248+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3249+# GNU Lesser General Public License for more details.
3250+#
3251+# You should have received a copy of the GNU Lesser General Public License
3252+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3253+
3254 "Tools for working with files injected into a charm just before deployment."
3255
3256=== modified file 'hooks/charmhelpers/payload/execd.py'
3257--- hooks/charmhelpers/payload/execd.py 2014-11-18 23:06:36 +0000
3258+++ hooks/charmhelpers/payload/execd.py 2016-06-01 15:03:50 +0000
3259@@ -1,5 +1,21 @@
3260 #!/usr/bin/env python
3261
3262+# Copyright 2014-2015 Canonical Limited.
3263+#
3264+# This file is part of charm-helpers.
3265+#
3266+# charm-helpers is free software: you can redistribute it and/or modify
3267+# it under the terms of the GNU Lesser General Public License version 3 as
3268+# published by the Free Software Foundation.
3269+#
3270+# charm-helpers is distributed in the hope that it will be useful,
3271+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3272+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3273+# GNU Lesser General Public License for more details.
3274+#
3275+# You should have received a copy of the GNU Lesser General Public License
3276+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
3277+
3278 import os
3279 import sys
3280 import subprocess
3281
3282=== modified file 'hooks/jenkins_hooks.py'
3283--- hooks/jenkins_hooks.py 2016-01-12 17:51:52 +0000
3284+++ hooks/jenkins_hooks.py 2016-06-01 15:03:50 +0000
3285@@ -1,11 +1,7 @@
3286-#!/usr/bin/python
3287-import grp
3288-import hashlib
3289-import os
3290-import pwd
3291-import shutil
3292-import subprocess
3293-import sys
3294+#!/bin/sh
3295+''''. /etc/os-release; dpkg --compare-versions $VERSION_ID ge "16.04" && exec /usr/bin/env python3 "$0" "$@" # '''
3296+''''which python2 >/dev/null 2>&1 && exec /usr/bin/env python2 "$0" "$@" # '''
3297+''''exec echo "Error: I can't find python anywhere" # '''
3298
3299 from charmhelpers.core.hookenv import (
3300 Hooks,
3301@@ -45,6 +41,15 @@
3302 install_from_remote_deb,
3303 install_jenkins_plugins,
3304 )
3305+import grp
3306+import os
3307+import pwd
3308+import shutil
3309+import subprocess
3310+import sys
3311+import six
3312+from hashlib import sha256
3313+
3314
3315 hooks = Hooks()
3316
3317@@ -80,21 +85,21 @@
3318 # Generate a random one for security. User can then override using juju
3319 # set.
3320 admin_passwd = subprocess.check_output(['pwgen', '-N1', '15'])
3321- admin_passwd = admin_passwd.strip()
3322+ admin_passwd = admin_passwd.decode().strip()
3323
3324 passwd_file = os.path.join(JENKINS_HOME, '.admin_password')
3325 with open(passwd_file, 'w+') as fd:
3326 fd.write(admin_passwd)
3327
3328- os.chmod(passwd_file, 0600)
3329+ os.chmod(passwd_file, 0o600)
3330
3331 jenkins_uid = pwd.getpwnam('jenkins').pw_uid
3332 jenkins_gid = grp.getgrnam('jenkins').gr_gid
3333 nogroup_gid = grp.getgrnam('nogroup').gr_gid
3334
3335 # Generate Salt and Hash Password for Jenkins
3336- salt = subprocess.check_output(['pwgen', '-N1', '6']).strip()
3337- csum = hashlib.sha256("%s{%s}" % (admin_passwd, salt)).hexdigest()
3338+ salt = subprocess.check_output(['pwgen', '-N1', '6']).decode().strip()
3339+ csum = sha256(("%s{%s}" % (admin_passwd, salt)).encode()).hexdigest()
3340 salty_password = "%s:%s" % (salt, csum)
3341
3342 admin_username = config('username')
3343@@ -113,7 +118,7 @@
3344 kvs = {'__USERNAME__': admin_username,
3345 '__PASSWORD__': salty_password}
3346
3347- for key, val in kvs.iteritems():
3348+ for key, val in kvs.items():
3349 if key in line:
3350 line = line.replace(key, val)
3351
3352@@ -141,6 +146,8 @@
3353 service_start('jenkins')
3354
3355 apt_install(['python-jenkins'], fatal=True)
3356+ if six.PY3:
3357+ apt_install(['python3-jenkins'], fatal=True)
3358 tools = config('tools')
3359 if tools:
3360 log("Installing tools.", level=DEBUG)
3361
3362=== modified file 'hooks/jenkins_utils.py'
3363--- hooks/jenkins_utils.py 2015-12-18 15:22:28 +0000
3364+++ hooks/jenkins_utils.py 2016-06-01 15:03:50 +0000
3365@@ -1,4 +1,3 @@
3366-#!/usr/bin/python
3367 import glob
3368 import os
3369 import shutil
3370@@ -180,7 +179,7 @@
3371 cmd = ['wget'] + opts + ['--timestamping', url, '-O',
3372 plugin_path]
3373 subprocess.check_call(cmd)
3374- os.chmod(plugin_path, 0744)
3375+ os.chmod(plugin_path, 0o744)
3376 os.chown(plugin_path, jenkins_uid, jenkins_gid)
3377
3378 else:

Subscribers

People subscribed via source and target branches

to all changes: