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

Proposed by Greg Lutostanski
Status: Rejected
Rejected by: Ryan Beisner
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 (community) Needs Information
Review Queue (community) automated testing Approve
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.
Revision history for this message
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)
Revision history for this message
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)
Revision history for this message
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

only run python3 if we are at or above xenial

45. By Greg Lutostanski

import six after charmhelpers has run to install it

44. By Greg Lutostanski

install python3-jenkins if necessary

43. By Greg Lutostanski

default to python3 first over python2

42. By Greg Lutostanski

fix for subprocess check_output bytes in python3

41. By Greg Lutostanski

try to utf-8 encode for password

40. By Greg Lutostanski

allow to run with any python

39. By Greg Lutostanski

allow charm to work with python3 as well as python2

38. By Greg Lutostanski

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: