Merge lp:~paulgear/charms/trusty/ntp/sync-charm-helpers into lp:charms/trusty/ntp

Proposed by Paul Gear on 2015-11-05
Status: Merged
Merged at revision: 24
Proposed branch: lp:~paulgear/charms/trusty/ntp/sync-charm-helpers
Merge into: lp:charms/trusty/ntp
Diff against target: 1784 lines (+1036/-151)
15 files modified
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+47/-9)
hooks/charmhelpers/contrib/templating/jinja.py (+4/-3)
hooks/charmhelpers/core/files.py (+45/-0)
hooks/charmhelpers/core/hookenv.py (+393/-43)
hooks/charmhelpers/core/host.py (+187/-24)
hooks/charmhelpers/core/hugepage.py (+71/-0)
hooks/charmhelpers/core/kernel.py (+68/-0)
hooks/charmhelpers/core/services/base.py (+43/-19)
hooks/charmhelpers/core/services/helpers.py (+24/-5)
hooks/charmhelpers/core/strutils.py (+32/-2)
hooks/charmhelpers/core/templating.py (+13/-6)
hooks/charmhelpers/core/unitdata.py (+62/-18)
hooks/charmhelpers/fetch/__init__.py (+32/-15)
hooks/charmhelpers/fetch/archiveurl.py (+7/-1)
hooks/charmhelpers/fetch/giturl.py (+8/-6)
To merge this branch: bzr merge lp:~paulgear/charms/trusty/ntp/sync-charm-helpers
Reviewer Review Type Date Requested Status
Marco Ceppi 2015-11-05 Approve on 2015-11-09
Review via email: mp+276826@code.launchpad.net

Description of the Change

This is a straight sync of charmhelpers upstream, in preparation for a future MP which depends on it.

To post a comment you must log in.
25. By Paul Gear on 2015-11-05

Add missing files from charm helpers sync

Marco Ceppi (marcoceppi) wrote :

lgtm

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
2--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-03-17 04:10:30 +0000
3+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-11-05 23:29:10 +0000
4@@ -148,6 +148,13 @@
5 self.description = description
6 self.check_cmd = self._locate_cmd(check_cmd)
7
8+ def _get_check_filename(self):
9+ return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command))
10+
11+ def _get_service_filename(self, hostname):
12+ return os.path.join(NRPE.nagios_exportdir,
13+ 'service__{}_{}.cfg'.format(hostname, self.command))
14+
15 def _locate_cmd(self, check_cmd):
16 search_path = (
17 '/usr/lib/nagios/plugins',
18@@ -163,9 +170,21 @@
19 log('Check command not found: {}'.format(parts[0]))
20 return ''
21
22+ def _remove_service_files(self):
23+ if not os.path.exists(NRPE.nagios_exportdir):
24+ return
25+ for f in os.listdir(NRPE.nagios_exportdir):
26+ if f.endswith('_{}.cfg'.format(self.command)):
27+ os.remove(os.path.join(NRPE.nagios_exportdir, f))
28+
29+ def remove(self, hostname):
30+ nrpe_check_file = self._get_check_filename()
31+ if os.path.exists(nrpe_check_file):
32+ os.remove(nrpe_check_file)
33+ self._remove_service_files()
34+
35 def write(self, nagios_context, hostname, nagios_servicegroups):
36- nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
37- self.command)
38+ nrpe_check_file = self._get_check_filename()
39 with open(nrpe_check_file, 'w') as nrpe_check_config:
40 nrpe_check_config.write("# check {}\n".format(self.shortname))
41 nrpe_check_config.write("command[{}]={}\n".format(
42@@ -180,9 +199,7 @@
43
44 def write_service_config(self, nagios_context, hostname,
45 nagios_servicegroups):
46- for f in os.listdir(NRPE.nagios_exportdir):
47- if re.search('.*{}.cfg'.format(self.command), f):
48- os.remove(os.path.join(NRPE.nagios_exportdir, f))
49+ self._remove_service_files()
50
51 templ_vars = {
52 'nagios_hostname': hostname,
53@@ -192,8 +209,7 @@
54 'command': self.command,
55 }
56 nrpe_service_text = Check.service_template.format(**templ_vars)
57- nrpe_service_file = '{}/service__{}_{}.cfg'.format(
58- NRPE.nagios_exportdir, hostname, self.command)
59+ nrpe_service_file = self._get_service_filename(hostname)
60 with open(nrpe_service_file, 'w') as nrpe_service_config:
61 nrpe_service_config.write(str(nrpe_service_text))
62
63@@ -218,12 +234,32 @@
64 if hostname:
65 self.hostname = hostname
66 else:
67- self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
68+ nagios_hostname = get_nagios_hostname()
69+ if nagios_hostname:
70+ self.hostname = nagios_hostname
71+ else:
72+ self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
73 self.checks = []
74
75 def add_check(self, *args, **kwargs):
76 self.checks.append(Check(*args, **kwargs))
77
78+ def remove_check(self, *args, **kwargs):
79+ if kwargs.get('shortname') is None:
80+ raise ValueError('shortname of check must be specified')
81+
82+ # Use sensible defaults if they're not specified - these are not
83+ # actually used during removal, but they're required for constructing
84+ # the Check object; check_disk is chosen because it's part of the
85+ # nagios-plugins-basic package.
86+ if kwargs.get('check_cmd') is None:
87+ kwargs['check_cmd'] = 'check_disk'
88+ if kwargs.get('description') is None:
89+ kwargs['description'] = ''
90+
91+ check = Check(*args, **kwargs)
92+ check.remove(self.hostname)
93+
94 def write(self):
95 try:
96 nagios_uid = pwd.getpwnam('nagios').pw_uid
97@@ -247,7 +283,9 @@
98
99 service('restart', 'nagios-nrpe-server')
100
101- for rid in relation_ids("local-monitors"):
102+ monitor_ids = relation_ids("local-monitors") + \
103+ relation_ids("nrpe-external-master")
104+ for rid in monitor_ids:
105 relation_set(relation_id=rid, monitors=yaml.dump(monitors))
106
107
108
109=== modified file 'hooks/charmhelpers/contrib/templating/jinja.py'
110--- hooks/charmhelpers/contrib/templating/jinja.py 2015-03-17 04:10:30 +0000
111+++ hooks/charmhelpers/contrib/templating/jinja.py 2015-11-05 23:29:10 +0000
112@@ -18,14 +18,15 @@
113 Templating using the python-jinja2 package.
114 """
115 import six
116-from charmhelpers.fetch import apt_install
117+from charmhelpers.fetch import apt_install, apt_update
118 try:
119 import jinja2
120 except ImportError:
121+ apt_update(fatal=True)
122 if six.PY3:
123- apt_install(["python3-jinja2"])
124+ apt_install(["python3-jinja2"], fatal=True)
125 else:
126- apt_install(["python-jinja2"])
127+ apt_install(["python-jinja2"], fatal=True)
128 import jinja2
129
130
131
132=== added file 'hooks/charmhelpers/core/files.py'
133--- hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
134+++ hooks/charmhelpers/core/files.py 2015-11-05 23:29:10 +0000
135@@ -0,0 +1,45 @@
136+#!/usr/bin/env python
137+# -*- coding: utf-8 -*-
138+
139+# Copyright 2014-2015 Canonical Limited.
140+#
141+# This file is part of charm-helpers.
142+#
143+# charm-helpers is free software: you can redistribute it and/or modify
144+# it under the terms of the GNU Lesser General Public License version 3 as
145+# published by the Free Software Foundation.
146+#
147+# charm-helpers is distributed in the hope that it will be useful,
148+# but WITHOUT ANY WARRANTY; without even the implied warranty of
149+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
150+# GNU Lesser General Public License for more details.
151+#
152+# You should have received a copy of the GNU Lesser General Public License
153+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
154+
155+__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
156+
157+import os
158+import subprocess
159+
160+
161+def sed(filename, before, after, flags='g'):
162+ """
163+ Search and replaces the given pattern on filename.
164+
165+ :param filename: relative or absolute file path.
166+ :param before: expression to be replaced (see 'man sed')
167+ :param after: expression to replace with (see 'man sed')
168+ :param flags: sed-compatible regex flags in example, to make
169+ the search and replace case insensitive, specify ``flags="i"``.
170+ The ``g`` flag is always specified regardless, so you do not
171+ need to remember to include it when overriding this parameter.
172+ :returns: If the sed command exit code was zero then return,
173+ otherwise raise CalledProcessError.
174+ """
175+ expression = r's/{0}/{1}/{2}'.format(before,
176+ after, flags)
177+
178+ return subprocess.check_call(["sed", "-i", "-r", "-e",
179+ expression,
180+ os.path.expanduser(filename)])
181
182=== modified file 'hooks/charmhelpers/core/hookenv.py'
183--- hooks/charmhelpers/core/hookenv.py 2015-03-17 04:10:30 +0000
184+++ hooks/charmhelpers/core/hookenv.py 2015-11-05 23:29:10 +0000
185@@ -20,11 +20,18 @@
186 # Authors:
187 # Charm Helpers Developers <juju@lists.ubuntu.com>
188
189+from __future__ import print_function
190+import copy
191+from distutils.version import LooseVersion
192+from functools import wraps
193+import glob
194 import os
195 import json
196 import yaml
197 import subprocess
198 import sys
199+import errno
200+import tempfile
201 from subprocess import CalledProcessError
202
203 import six
204@@ -56,15 +63,18 @@
205
206 will cache the result of unit_get + 'test' for future calls.
207 """
208+ @wraps(func)
209 def wrapper(*args, **kwargs):
210 global cache
211 key = str((func, args, kwargs))
212 try:
213 return cache[key]
214 except KeyError:
215- res = func(*args, **kwargs)
216- cache[key] = res
217- return res
218+ pass # Drop out of the exception handler scope.
219+ res = func(*args, **kwargs)
220+ cache[key] = res
221+ return res
222+ wrapper._wrapped = func
223 return wrapper
224
225
226@@ -87,7 +97,18 @@
227 if not isinstance(message, six.string_types):
228 message = repr(message)
229 command += [message]
230- subprocess.call(command)
231+ # Missing juju-log should not cause failures in unit tests
232+ # Send log output to stderr
233+ try:
234+ subprocess.call(command)
235+ except OSError as e:
236+ if e.errno == errno.ENOENT:
237+ if level:
238+ message = "{}: {}".format(level, message)
239+ message = "juju-log: {}".format(message)
240+ print(message, file=sys.stderr)
241+ else:
242+ raise
243
244
245 class Serializable(UserDict):
246@@ -153,9 +174,19 @@
247 return os.environ.get('JUJU_RELATION', None)
248
249
250-def relation_id():
251- """The relation ID for the current relation hook"""
252- return os.environ.get('JUJU_RELATION_ID', None)
253+@cached
254+def relation_id(relation_name=None, service_or_unit=None):
255+ """The relation ID for the current or a specified relation"""
256+ if not relation_name and not service_or_unit:
257+ return os.environ.get('JUJU_RELATION_ID', None)
258+ elif relation_name and service_or_unit:
259+ service_name = service_or_unit.split('/')[0]
260+ for relid in relation_ids(relation_name):
261+ remote_service = remote_service_name(relid)
262+ if remote_service == service_name:
263+ return relid
264+ else:
265+ raise ValueError('Must specify neither or both of relation_name and service_or_unit')
266
267
268 def local_unit():
269@@ -165,7 +196,7 @@
270
271 def remote_unit():
272 """The remote unit for the current relation hook"""
273- return os.environ['JUJU_REMOTE_UNIT']
274+ return os.environ.get('JUJU_REMOTE_UNIT', None)
275
276
277 def service_name():
278@@ -173,9 +204,20 @@
279 return local_unit().split('/')[0]
280
281
282+@cached
283+def remote_service_name(relid=None):
284+ """The remote service name for a given relation-id (or the current relation)"""
285+ if relid is None:
286+ unit = remote_unit()
287+ else:
288+ units = related_units(relid)
289+ unit = units[0] if units else None
290+ return unit.split('/')[0] if unit else None
291+
292+
293 def hook_name():
294 """The name of the currently executing hook"""
295- return os.path.basename(sys.argv[0])
296+ return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
297
298
299 class Config(dict):
300@@ -225,23 +267,7 @@
301 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
302 if os.path.exists(self.path):
303 self.load_previous()
304-
305- def __getitem__(self, key):
306- """For regular dict lookups, check the current juju config first,
307- then the previous (saved) copy. This ensures that user-saved values
308- will be returned by a dict lookup.
309-
310- """
311- try:
312- return dict.__getitem__(self, key)
313- except KeyError:
314- return (self._prev_dict or {})[key]
315-
316- def keys(self):
317- prev_keys = []
318- if self._prev_dict is not None:
319- prev_keys = self._prev_dict.keys()
320- return list(set(prev_keys + list(dict.keys(self))))
321+ atexit(self._implicit_save)
322
323 def load_previous(self, path=None):
324 """Load previous copy of config from disk.
325@@ -260,6 +286,9 @@
326 self.path = path or self.path
327 with open(self.path) as f:
328 self._prev_dict = json.load(f)
329+ for k, v in copy.deepcopy(self._prev_dict).items():
330+ if k not in self:
331+ self[k] = v
332
333 def changed(self, key):
334 """Return True if the current value for this key is different from
335@@ -291,13 +320,13 @@
336 instance.
337
338 """
339- if self._prev_dict:
340- for k, v in six.iteritems(self._prev_dict):
341- if k not in self:
342- self[k] = v
343 with open(self.path, 'w') as f:
344 json.dump(self, f)
345
346+ def _implicit_save(self):
347+ if self.implicit_save:
348+ self.save()
349+
350
351 @cached
352 def config(scope=None):
353@@ -340,18 +369,49 @@
354 """Set relation information for the current unit"""
355 relation_settings = relation_settings if relation_settings else {}
356 relation_cmd_line = ['relation-set']
357+ accepts_file = "--file" in subprocess.check_output(
358+ relation_cmd_line + ["--help"], universal_newlines=True)
359 if relation_id is not None:
360 relation_cmd_line.extend(('-r', relation_id))
361- for k, v in (list(relation_settings.items()) + list(kwargs.items())):
362- if v is None:
363- relation_cmd_line.append('{}='.format(k))
364- else:
365- relation_cmd_line.append('{}={}'.format(k, v))
366- subprocess.check_call(relation_cmd_line)
367+ settings = relation_settings.copy()
368+ settings.update(kwargs)
369+ for key, value in settings.items():
370+ # Force value to be a string: it always should, but some call
371+ # sites pass in things like dicts or numbers.
372+ if value is not None:
373+ settings[key] = "{}".format(value)
374+ if accepts_file:
375+ # --file was introduced in Juju 1.23.2. Use it by default if
376+ # available, since otherwise we'll break if the relation data is
377+ # too big. Ideally we should tell relation-set to read the data from
378+ # stdin, but that feature is broken in 1.23.2: Bug #1454678.
379+ with tempfile.NamedTemporaryFile(delete=False) as settings_file:
380+ settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
381+ subprocess.check_call(
382+ relation_cmd_line + ["--file", settings_file.name])
383+ os.remove(settings_file.name)
384+ else:
385+ for key, value in settings.items():
386+ if value is None:
387+ relation_cmd_line.append('{}='.format(key))
388+ else:
389+ relation_cmd_line.append('{}={}'.format(key, value))
390+ subprocess.check_call(relation_cmd_line)
391 # Flush cache of any relation-gets for local unit
392 flush(local_unit())
393
394
395+def relation_clear(r_id=None):
396+ ''' Clears any relation data already set on relation r_id '''
397+ settings = relation_get(rid=r_id,
398+ unit=local_unit())
399+ for setting in settings:
400+ if setting not in ['public-address', 'private-address']:
401+ settings[setting] = None
402+ relation_set(relation_id=r_id,
403+ **settings)
404+
405+
406 @cached
407 def relation_ids(reltype=None):
408 """A list of relation_ids"""
409@@ -431,6 +491,76 @@
410
411
412 @cached
413+def peer_relation_id():
414+ '''Get a peer relation id if a peer relation has been joined, else None.'''
415+ md = metadata()
416+ section = md.get('peers')
417+ if section:
418+ for key in section:
419+ relids = relation_ids(key)
420+ if relids:
421+ return relids[0]
422+ return None
423+
424+
425+@cached
426+def relation_to_interface(relation_name):
427+ """
428+ Given the name of a relation, return the interface that relation uses.
429+
430+ :returns: The interface name, or ``None``.
431+ """
432+ return relation_to_role_and_interface(relation_name)[1]
433+
434+
435+@cached
436+def relation_to_role_and_interface(relation_name):
437+ """
438+ Given the name of a relation, return the role and the name of the interface
439+ that relation uses (where role is one of ``provides``, ``requires``, or ``peer``).
440+
441+ :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
442+ """
443+ _metadata = metadata()
444+ for role in ('provides', 'requires', 'peer'):
445+ interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
446+ if interface:
447+ return role, interface
448+ return None, None
449+
450+
451+@cached
452+def role_and_interface_to_relations(role, interface_name):
453+ """
454+ Given a role and interface name, return a list of relation names for the
455+ current charm that use that interface under that role (where role is one
456+ of ``provides``, ``requires``, or ``peer``).
457+
458+ :returns: A list of relation names.
459+ """
460+ _metadata = metadata()
461+ results = []
462+ for relation_name, relation in _metadata.get(role, {}).items():
463+ if relation['interface'] == interface_name:
464+ results.append(relation_name)
465+ return results
466+
467+
468+@cached
469+def interface_to_relations(interface_name):
470+ """
471+ Given an interface, return a list of relation names for the current
472+ charm that use that interface.
473+
474+ :returns: A list of relation names.
475+ """
476+ results = []
477+ for role in ('provides', 'requires', 'peer'):
478+ results.extend(role_and_interface_to_relations(role, interface_name))
479+ return results
480+
481+
482+@cached
483 def charm_name():
484 """Get the name of the current charm as is specified on metadata.yaml"""
485 return metadata().get('name')
486@@ -496,11 +626,48 @@
487 return None
488
489
490+def unit_public_ip():
491+ """Get this unit's public IP address"""
492+ return unit_get('public-address')
493+
494+
495 def unit_private_ip():
496 """Get this unit's private IP address"""
497 return unit_get('private-address')
498
499
500+@cached
501+def storage_get(attribute="", storage_id=""):
502+ """Get storage attributes"""
503+ _args = ['storage-get', '--format=json']
504+ if storage_id:
505+ _args.extend(('-s', storage_id))
506+ if attribute:
507+ _args.append(attribute)
508+ try:
509+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
510+ except ValueError:
511+ return None
512+
513+
514+@cached
515+def storage_list(storage_name=""):
516+ """List the storage IDs for the unit"""
517+ _args = ['storage-list', '--format=json']
518+ if storage_name:
519+ _args.append(storage_name)
520+ try:
521+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
522+ except ValueError:
523+ return None
524+ except OSError as e:
525+ import errno
526+ if e.errno == errno.ENOENT:
527+ # storage-list does not exist
528+ return []
529+ raise
530+
531+
532 class UnregisteredHookError(Exception):
533 """Raised when an undefined hook is called"""
534 pass
535@@ -528,10 +695,14 @@
536 hooks.execute(sys.argv)
537 """
538
539- def __init__(self, config_save=True):
540+ def __init__(self, config_save=None):
541 super(Hooks, self).__init__()
542 self._hooks = {}
543- self._config_save = config_save
544+
545+ # For unknown reasons, we allow the Hooks constructor to override
546+ # config().implicit_save.
547+ if config_save is not None:
548+ config().implicit_save = config_save
549
550 def register(self, name, function):
551 """Register a hook"""
552@@ -539,13 +710,16 @@
553
554 def execute(self, args):
555 """Execute a registered hook based on args[0]"""
556+ _run_atstart()
557 hook_name = os.path.basename(args[0])
558 if hook_name in self._hooks:
559- self._hooks[hook_name]()
560- if self._config_save:
561- cfg = config()
562- if cfg.implicit_save:
563- cfg.save()
564+ try:
565+ self._hooks[hook_name]()
566+ except SystemExit as x:
567+ if x.code is None or x.code == 0:
568+ _run_atexit()
569+ raise
570+ _run_atexit()
571 else:
572 raise UnregisteredHookError(hook_name)
573
574@@ -592,3 +766,179 @@
575
576 The results set by action_set are preserved."""
577 subprocess.check_call(['action-fail', message])
578+
579+
580+def action_name():
581+ """Get the name of the currently executing action."""
582+ return os.environ.get('JUJU_ACTION_NAME')
583+
584+
585+def action_uuid():
586+ """Get the UUID of the currently executing action."""
587+ return os.environ.get('JUJU_ACTION_UUID')
588+
589+
590+def action_tag():
591+ """Get the tag for the currently executing action."""
592+ return os.environ.get('JUJU_ACTION_TAG')
593+
594+
595+def status_set(workload_state, message):
596+ """Set the workload state with a message
597+
598+ Use status-set to set the workload state with a message which is visible
599+ to the user via juju status. If the status-set command is not found then
600+ assume this is juju < 1.23 and juju-log the message unstead.
601+
602+ workload_state -- valid juju workload state.
603+ message -- status update message
604+ """
605+ valid_states = ['maintenance', 'blocked', 'waiting', 'active']
606+ if workload_state not in valid_states:
607+ raise ValueError(
608+ '{!r} is not a valid workload state'.format(workload_state)
609+ )
610+ cmd = ['status-set', workload_state, message]
611+ try:
612+ ret = subprocess.call(cmd)
613+ if ret == 0:
614+ return
615+ except OSError as e:
616+ if e.errno != errno.ENOENT:
617+ raise
618+ log_message = 'status-set failed: {} {}'.format(workload_state,
619+ message)
620+ log(log_message, level='INFO')
621+
622+
623+def status_get():
624+ """Retrieve the previously set juju workload state and message
625+
626+ If the status-get command is not found then assume this is juju < 1.23 and
627+ return 'unknown', ""
628+
629+ """
630+ cmd = ['status-get', "--format=json", "--include-data"]
631+ try:
632+ raw_status = subprocess.check_output(cmd)
633+ except OSError as e:
634+ if e.errno == errno.ENOENT:
635+ return ('unknown', "")
636+ else:
637+ raise
638+ else:
639+ status = json.loads(raw_status.decode("UTF-8"))
640+ return (status["status"], status["message"])
641+
642+
643+def translate_exc(from_exc, to_exc):
644+ def inner_translate_exc1(f):
645+ @wraps(f)
646+ def inner_translate_exc2(*args, **kwargs):
647+ try:
648+ return f(*args, **kwargs)
649+ except from_exc:
650+ raise to_exc
651+
652+ return inner_translate_exc2
653+
654+ return inner_translate_exc1
655+
656+
657+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
658+def is_leader():
659+ """Does the current unit hold the juju leadership
660+
661+ Uses juju to determine whether the current unit is the leader of its peers
662+ """
663+ cmd = ['is-leader', '--format=json']
664+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
665+
666+
667+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
668+def leader_get(attribute=None):
669+ """Juju leader get value(s)"""
670+ cmd = ['leader-get', '--format=json'] + [attribute or '-']
671+ return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
672+
673+
674+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
675+def leader_set(settings=None, **kwargs):
676+ """Juju leader set value(s)"""
677+ # Don't log secrets.
678+ # log("Juju leader-set '%s'" % (settings), level=DEBUG)
679+ cmd = ['leader-set']
680+ settings = settings or {}
681+ settings.update(kwargs)
682+ for k, v in settings.items():
683+ if v is None:
684+ cmd.append('{}='.format(k))
685+ else:
686+ cmd.append('{}={}'.format(k, v))
687+ subprocess.check_call(cmd)
688+
689+
690+@cached
691+def juju_version():
692+ """Full version string (eg. '1.23.3.1-trusty-amd64')"""
693+ # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
694+ jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
695+ return subprocess.check_output([jujud, 'version'],
696+ universal_newlines=True).strip()
697+
698+
699+@cached
700+def has_juju_version(minimum_version):
701+ """Return True if the Juju version is at least the provided version"""
702+ return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
703+
704+
705+_atexit = []
706+_atstart = []
707+
708+
709+def atstart(callback, *args, **kwargs):
710+ '''Schedule a callback to run before the main hook.
711+
712+ Callbacks are run in the order they were added.
713+
714+ This is useful for modules and classes to perform initialization
715+ and inject behavior. In particular:
716+
717+ - Run common code before all of your hooks, such as logging
718+ the hook name or interesting relation data.
719+ - Defer object or module initialization that requires a hook
720+ context until we know there actually is a hook context,
721+ making testing easier.
722+ - Rather than requiring charm authors to include boilerplate to
723+ invoke your helper's behavior, have it run automatically if
724+ your object is instantiated or module imported.
725+
726+ This is not at all useful after your hook framework as been launched.
727+ '''
728+ global _atstart
729+ _atstart.append((callback, args, kwargs))
730+
731+
732+def atexit(callback, *args, **kwargs):
733+ '''Schedule a callback to run on successful hook completion.
734+
735+ Callbacks are run in the reverse order that they were added.'''
736+ _atexit.append((callback, args, kwargs))
737+
738+
739+def _run_atstart():
740+ '''Hook frameworks must invoke this before running the main hook body.'''
741+ global _atstart
742+ for callback, args, kwargs in _atstart:
743+ callback(*args, **kwargs)
744+ del _atstart[:]
745+
746+
747+def _run_atexit():
748+ '''Hook frameworks must invoke this after the main hook body has
749+ successfully completed. Do not invoke it if the hook fails.'''
750+ global _atexit
751+ for callback, args, kwargs in reversed(_atexit):
752+ callback(*args, **kwargs)
753+ del _atexit[:]
754
755=== modified file 'hooks/charmhelpers/core/host.py'
756--- hooks/charmhelpers/core/host.py 2015-03-17 04:10:30 +0000
757+++ hooks/charmhelpers/core/host.py 2015-11-05 23:29:10 +0000
758@@ -24,6 +24,7 @@
759 import os
760 import re
761 import pwd
762+import glob
763 import grp
764 import random
765 import string
766@@ -62,6 +63,52 @@
767 return service_result
768
769
770+def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
771+ """Pause a system service.
772+
773+ Stop it, and prevent it from starting again at boot."""
774+ stopped = service_stop(service_name)
775+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
776+ sysv_file = os.path.join(initd_dir, service_name)
777+ if os.path.exists(upstart_file):
778+ override_path = os.path.join(
779+ init_dir, '{}.override'.format(service_name))
780+ with open(override_path, 'w') as fh:
781+ fh.write("manual\n")
782+ elif os.path.exists(sysv_file):
783+ subprocess.check_call(["update-rc.d", service_name, "disable"])
784+ else:
785+ # XXX: Support SystemD too
786+ raise ValueError(
787+ "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
788+ service_name, upstart_file, sysv_file))
789+ return stopped
790+
791+
792+def service_resume(service_name, init_dir="/etc/init",
793+ initd_dir="/etc/init.d"):
794+ """Resume a system service.
795+
796+ Reenable starting again at boot. Start the service"""
797+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
798+ sysv_file = os.path.join(initd_dir, service_name)
799+ if os.path.exists(upstart_file):
800+ override_path = os.path.join(
801+ init_dir, '{}.override'.format(service_name))
802+ if os.path.exists(override_path):
803+ os.unlink(override_path)
804+ elif os.path.exists(sysv_file):
805+ subprocess.check_call(["update-rc.d", service_name, "enable"])
806+ else:
807+ # XXX: Support SystemD too
808+ raise ValueError(
809+ "Unable to detect {0} as either Upstart {1} or SysV {2}".format(
810+ service_name, upstart_file, sysv_file))
811+
812+ started = service_start(service_name)
813+ return started
814+
815+
816 def service(action, service_name):
817 """Control a system service"""
818 cmd = ['service', service_name, action]
819@@ -90,7 +137,7 @@
820 ['service', service_name, 'status'],
821 stderr=subprocess.STDOUT).decode('UTF-8')
822 except subprocess.CalledProcessError as e:
823- return 'unrecognized service' not in e.output
824+ return b'unrecognized service' not in e.output
825 else:
826 return True
827
828@@ -117,6 +164,16 @@
829 return user_info
830
831
832+def user_exists(username):
833+ """Check if a user exists"""
834+ try:
835+ pwd.getpwnam(username)
836+ user_exists = True
837+ except KeyError:
838+ user_exists = False
839+ return user_exists
840+
841+
842 def add_group(group_name, system_group=False):
843 """Add a group to the system"""
844 try:
845@@ -139,11 +196,7 @@
846
847 def add_user_to_group(username, group):
848 """Add a user to a group"""
849- cmd = [
850- 'gpasswd', '-a',
851- username,
852- group
853- ]
854+ cmd = ['gpasswd', '-a', username, group]
855 log("Adding user {} to group {}".format(username, group))
856 subprocess.check_call(cmd)
857
858@@ -253,6 +306,17 @@
859 return system_mounts
860
861
862+def fstab_mount(mountpoint):
863+ """Mount filesystem using fstab"""
864+ cmd_args = ['mount', mountpoint]
865+ try:
866+ subprocess.check_output(cmd_args)
867+ except subprocess.CalledProcessError as e:
868+ log('Error unmounting {}\n{}'.format(mountpoint, e.output))
869+ return False
870+ return True
871+
872+
873 def file_hash(path, hash_type='md5'):
874 """
875 Generate a hash checksum of the contents of 'path' or None if not found.
876@@ -269,6 +333,21 @@
877 return None
878
879
880+def path_hash(path):
881+ """
882+ Generate a hash checksum of all files matching 'path'. Standard wildcards
883+ like '*' and '?' are supported, see documentation for the 'glob' module for
884+ more information.
885+
886+ :return: dict: A { filename: hash } dictionary for all matched files.
887+ Empty if none found.
888+ """
889+ return {
890+ filename: file_hash(filename)
891+ for filename in glob.iglob(path)
892+ }
893+
894+
895 def check_hash(path, checksum, hash_type='md5'):
896 """
897 Validate a file using a cryptographic checksum.
898@@ -296,23 +375,25 @@
899
900 @restart_on_change({
901 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
902+ '/etc/apache/sites-enabled/*': [ 'apache2' ]
903 })
904- def ceph_client_changed():
905+ def config_changed():
906 pass # your code here
907
908 In this example, the cinder-api and cinder-volume services
909 would be restarted if /etc/ceph/ceph.conf is changed by the
910- ceph_client_changed function.
911+ ceph_client_changed function. The apache2 service would be
912+ restarted if any file matching the pattern got changed, created
913+ or removed. Standard wildcards are supported, see documentation
914+ for the 'glob' module for more information.
915 """
916 def wrap(f):
917 def wrapped_f(*args, **kwargs):
918- checksums = {}
919- for path in restart_map:
920- checksums[path] = file_hash(path)
921+ checksums = {path: path_hash(path) for path in restart_map}
922 f(*args, **kwargs)
923 restarts = []
924 for path in restart_map:
925- if checksums[path] != file_hash(path):
926+ if path_hash(path) != checksums[path]:
927 restarts += restart_map[path]
928 services_list = list(OrderedDict.fromkeys(restarts))
929 if not stopstart:
930@@ -352,25 +433,80 @@
931 return(''.join(random_chars))
932
933
934-def list_nics(nic_type):
935+def is_phy_iface(interface):
936+ """Returns True if interface is not virtual, otherwise False."""
937+ if interface:
938+ sys_net = '/sys/class/net'
939+ if os.path.isdir(sys_net):
940+ for iface in glob.glob(os.path.join(sys_net, '*')):
941+ if '/virtual/' in os.path.realpath(iface):
942+ continue
943+
944+ if interface == os.path.basename(iface):
945+ return True
946+
947+ return False
948+
949+
950+def get_bond_master(interface):
951+ """Returns bond master if interface is bond slave otherwise None.
952+
953+ NOTE: the provided interface is expected to be physical
954+ """
955+ if interface:
956+ iface_path = '/sys/class/net/%s' % (interface)
957+ if os.path.exists(iface_path):
958+ if '/virtual/' in os.path.realpath(iface_path):
959+ return None
960+
961+ master = os.path.join(iface_path, 'master')
962+ if os.path.exists(master):
963+ master = os.path.realpath(master)
964+ # make sure it is a bond master
965+ if os.path.exists(os.path.join(master, 'bonding')):
966+ return os.path.basename(master)
967+
968+ return None
969+
970+
971+def list_nics(nic_type=None):
972 '''Return a list of nics of given type(s)'''
973 if isinstance(nic_type, six.string_types):
974 int_types = [nic_type]
975 else:
976 int_types = nic_type
977+
978 interfaces = []
979- for int_type in int_types:
980- cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
981+ if nic_type:
982+ for int_type in int_types:
983+ cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
984+ ip_output = subprocess.check_output(cmd).decode('UTF-8')
985+ ip_output = ip_output.split('\n')
986+ ip_output = (line for line in ip_output if line)
987+ for line in ip_output:
988+ if line.split()[1].startswith(int_type):
989+ matched = re.search('.*: (' + int_type +
990+ r'[0-9]+\.[0-9]+)@.*', line)
991+ if matched:
992+ iface = matched.groups()[0]
993+ else:
994+ iface = line.split()[1].replace(":", "")
995+
996+ if iface not in interfaces:
997+ interfaces.append(iface)
998+ else:
999+ cmd = ['ip', 'a']
1000 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
1001- ip_output = (line for line in ip_output if line)
1002+ ip_output = (line.strip() for line in ip_output if line)
1003+
1004+ key = re.compile('^[0-9]+:\s+(.+):')
1005 for line in ip_output:
1006- if line.split()[1].startswith(int_type):
1007- matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
1008- if matched:
1009- interface = matched.groups()[0]
1010- else:
1011- interface = line.split()[1].replace(":", "")
1012- interfaces.append(interface)
1013+ matched = re.search(key, line)
1014+ if matched:
1015+ iface = matched.group(1)
1016+ iface = iface.partition("@")[0]
1017+ if iface not in interfaces:
1018+ interfaces.append(iface)
1019
1020 return interfaces
1021
1022@@ -430,7 +566,14 @@
1023 os.chdir(cur)
1024
1025
1026-def chownr(path, owner, group, follow_links=True):
1027+def chownr(path, owner, group, follow_links=True, chowntopdir=False):
1028+ """
1029+ Recursively change user and group ownership of files and directories
1030+ in given path. Doesn't chown path itself by default, only its children.
1031+
1032+ :param bool follow_links: Also Chown links if True
1033+ :param bool chowntopdir: Also chown path itself if True
1034+ """
1035 uid = pwd.getpwnam(owner).pw_uid
1036 gid = grp.getgrnam(group).gr_gid
1037 if follow_links:
1038@@ -438,6 +581,10 @@
1039 else:
1040 chown = os.lchown
1041
1042+ if chowntopdir:
1043+ broken_symlink = os.path.lexists(path) and not os.path.exists(path)
1044+ if not broken_symlink:
1045+ chown(path, uid, gid)
1046 for root, dirs, files in os.walk(path):
1047 for name in dirs + files:
1048 full = os.path.join(root, name)
1049@@ -448,3 +595,19 @@
1050
1051 def lchownr(path, owner, group):
1052 chownr(path, owner, group, follow_links=False)
1053+
1054+
1055+def get_total_ram():
1056+ '''The total amount of system RAM in bytes.
1057+
1058+ This is what is reported by the OS, and may be overcommitted when
1059+ there are multiple containers hosted on the same machine.
1060+ '''
1061+ with open('/proc/meminfo', 'r') as f:
1062+ for line in f.readlines():
1063+ if line:
1064+ key, value, unit = line.split()
1065+ if key == 'MemTotal:':
1066+ assert unit == 'kB', 'Unknown unit'
1067+ return int(value) * 1024 # Classic, not KiB.
1068+ raise NotImplementedError()
1069
1070=== added file 'hooks/charmhelpers/core/hugepage.py'
1071--- hooks/charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000
1072+++ hooks/charmhelpers/core/hugepage.py 2015-11-05 23:29:10 +0000
1073@@ -0,0 +1,71 @@
1074+# -*- coding: utf-8 -*-
1075+
1076+# Copyright 2014-2015 Canonical Limited.
1077+#
1078+# This file is part of charm-helpers.
1079+#
1080+# charm-helpers is free software: you can redistribute it and/or modify
1081+# it under the terms of the GNU Lesser General Public License version 3 as
1082+# published by the Free Software Foundation.
1083+#
1084+# charm-helpers is distributed in the hope that it will be useful,
1085+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1086+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1087+# GNU Lesser General Public License for more details.
1088+#
1089+# You should have received a copy of the GNU Lesser General Public License
1090+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1091+
1092+import yaml
1093+from charmhelpers.core import fstab
1094+from charmhelpers.core import sysctl
1095+from charmhelpers.core.host import (
1096+ add_group,
1097+ add_user_to_group,
1098+ fstab_mount,
1099+ mkdir,
1100+)
1101+from charmhelpers.core.strutils import bytes_from_string
1102+from subprocess import check_output
1103+
1104+
1105+def hugepage_support(user, group='hugetlb', nr_hugepages=256,
1106+ max_map_count=65536, mnt_point='/run/hugepages/kvm',
1107+ pagesize='2MB', mount=True, set_shmmax=False):
1108+ """Enable hugepages on system.
1109+
1110+ Args:
1111+ user (str) -- Username to allow access to hugepages to
1112+ group (str) -- Group name to own hugepages
1113+ nr_hugepages (int) -- Number of pages to reserve
1114+ max_map_count (int) -- Number of Virtual Memory Areas a process can own
1115+ mnt_point (str) -- Directory to mount hugepages on
1116+ pagesize (str) -- Size of hugepages
1117+ mount (bool) -- Whether to Mount hugepages
1118+ """
1119+ group_info = add_group(group)
1120+ gid = group_info.gr_gid
1121+ add_user_to_group(user, group)
1122+ if max_map_count < 2 * nr_hugepages:
1123+ max_map_count = 2 * nr_hugepages
1124+ sysctl_settings = {
1125+ 'vm.nr_hugepages': nr_hugepages,
1126+ 'vm.max_map_count': max_map_count,
1127+ 'vm.hugetlb_shm_group': gid,
1128+ }
1129+ if set_shmmax:
1130+ shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax']))
1131+ shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages
1132+ if shmmax_minsize > shmmax_current:
1133+ sysctl_settings['kernel.shmmax'] = shmmax_minsize
1134+ sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
1135+ mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
1136+ lfstab = fstab.Fstab()
1137+ fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point)
1138+ if fstab_entry:
1139+ lfstab.remove_entry(fstab_entry)
1140+ entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs',
1141+ 'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0)
1142+ lfstab.add_entry(entry)
1143+ if mount:
1144+ fstab_mount(mnt_point)
1145
1146=== added file 'hooks/charmhelpers/core/kernel.py'
1147--- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
1148+++ hooks/charmhelpers/core/kernel.py 2015-11-05 23:29:10 +0000
1149@@ -0,0 +1,68 @@
1150+#!/usr/bin/env python
1151+# -*- coding: utf-8 -*-
1152+
1153+# Copyright 2014-2015 Canonical Limited.
1154+#
1155+# This file is part of charm-helpers.
1156+#
1157+# charm-helpers is free software: you can redistribute it and/or modify
1158+# it under the terms of the GNU Lesser General Public License version 3 as
1159+# published by the Free Software Foundation.
1160+#
1161+# charm-helpers is distributed in the hope that it will be useful,
1162+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1163+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1164+# GNU Lesser General Public License for more details.
1165+#
1166+# You should have received a copy of the GNU Lesser General Public License
1167+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1168+
1169+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
1170+
1171+from charmhelpers.core.hookenv import (
1172+ log,
1173+ INFO
1174+)
1175+
1176+from subprocess import check_call, check_output
1177+import re
1178+
1179+
1180+def modprobe(module, persist=True):
1181+ """Load a kernel module and configure for auto-load on reboot."""
1182+ cmd = ['modprobe', module]
1183+
1184+ log('Loading kernel module %s' % module, level=INFO)
1185+
1186+ check_call(cmd)
1187+ if persist:
1188+ with open('/etc/modules', 'r+') as modules:
1189+ if module not in modules.read():
1190+ modules.write(module)
1191+
1192+
1193+def rmmod(module, force=False):
1194+ """Remove a module from the linux kernel"""
1195+ cmd = ['rmmod']
1196+ if force:
1197+ cmd.append('-f')
1198+ cmd.append(module)
1199+ log('Removing kernel module %s' % module, level=INFO)
1200+ return check_call(cmd)
1201+
1202+
1203+def lsmod():
1204+ """Shows what kernel modules are currently loaded"""
1205+ return check_output(['lsmod'],
1206+ universal_newlines=True)
1207+
1208+
1209+def is_module_loaded(module):
1210+ """Checks if a kernel module is already loaded"""
1211+ matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
1212+ return len(matches) > 0
1213+
1214+
1215+def update_initramfs(version='all'):
1216+ """Updates an initramfs image"""
1217+ return check_call(["update-initramfs", "-k", version, "-u"])
1218
1219=== modified file 'hooks/charmhelpers/core/services/base.py'
1220--- hooks/charmhelpers/core/services/base.py 2015-03-17 04:10:30 +0000
1221+++ hooks/charmhelpers/core/services/base.py 2015-11-05 23:29:10 +0000
1222@@ -15,9 +15,9 @@
1223 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1224
1225 import os
1226-import re
1227 import json
1228-from collections import Iterable
1229+from inspect import getargspec
1230+from collections import Iterable, OrderedDict
1231
1232 from charmhelpers.core import host
1233 from charmhelpers.core import hookenv
1234@@ -119,7 +119,7 @@
1235 """
1236 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
1237 self._ready = None
1238- self.services = {}
1239+ self.services = OrderedDict()
1240 for service in services or []:
1241 service_name = service['service']
1242 self.services[service_name] = service
1243@@ -128,15 +128,18 @@
1244 """
1245 Handle the current hook by doing The Right Thing with the registered services.
1246 """
1247- hook_name = hookenv.hook_name()
1248- if hook_name == 'stop':
1249- self.stop_services()
1250- else:
1251- self.provide_data()
1252- self.reconfigure_services()
1253- cfg = hookenv.config()
1254- if cfg.implicit_save:
1255- cfg.save()
1256+ hookenv._run_atstart()
1257+ try:
1258+ hook_name = hookenv.hook_name()
1259+ if hook_name == 'stop':
1260+ self.stop_services()
1261+ else:
1262+ self.reconfigure_services()
1263+ self.provide_data()
1264+ except SystemExit as x:
1265+ if x.code is None or x.code == 0:
1266+ hookenv._run_atexit()
1267+ hookenv._run_atexit()
1268
1269 def provide_data(self):
1270 """
1271@@ -145,15 +148,36 @@
1272 A provider must have a `name` attribute, which indicates which relation
1273 to set data on, and a `provide_data()` method, which returns a dict of
1274 data to set.
1275+
1276+ The `provide_data()` method can optionally accept two parameters:
1277+
1278+ * ``remote_service`` The name of the remote service that the data will
1279+ be provided to. The `provide_data()` method will be called once
1280+ for each connected service (not unit). This allows the method to
1281+ tailor its data to the given service.
1282+ * ``service_ready`` Whether or not the service definition had all of
1283+ its requirements met, and thus the ``data_ready`` callbacks run.
1284+
1285+ Note that the ``provided_data`` methods are now called **after** the
1286+ ``data_ready`` callbacks are run. This gives the ``data_ready`` callbacks
1287+ a chance to generate any data necessary for the providing to the remote
1288+ services.
1289 """
1290- hook_name = hookenv.hook_name()
1291- for service in self.services.values():
1292+ for service_name, service in self.services.items():
1293+ service_ready = self.is_ready(service_name)
1294 for provider in service.get('provided_data', []):
1295- if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
1296- data = provider.provide_data()
1297- _ready = provider._is_ready(data) if hasattr(provider, '_is_ready') else data
1298- if _ready:
1299- hookenv.relation_set(None, data)
1300+ for relid in hookenv.relation_ids(provider.name):
1301+ units = hookenv.related_units(relid)
1302+ if not units:
1303+ continue
1304+ remote_service = units[0].split('/')[0]
1305+ argspec = getargspec(provider.provide_data)
1306+ if len(argspec.args) > 1:
1307+ data = provider.provide_data(remote_service, service_ready)
1308+ else:
1309+ data = provider.provide_data()
1310+ if data:
1311+ hookenv.relation_set(relid, data)
1312
1313 def reconfigure_services(self, *service_names):
1314 """
1315
1316=== modified file 'hooks/charmhelpers/core/services/helpers.py'
1317--- hooks/charmhelpers/core/services/helpers.py 2015-03-17 04:10:30 +0000
1318+++ hooks/charmhelpers/core/services/helpers.py 2015-11-05 23:29:10 +0000
1319@@ -16,7 +16,9 @@
1320
1321 import os
1322 import yaml
1323+
1324 from charmhelpers.core import hookenv
1325+from charmhelpers.core import host
1326 from charmhelpers.core import templating
1327
1328 from charmhelpers.core.services.base import ManagerCallback
1329@@ -139,7 +141,7 @@
1330
1331 def __init__(self, *args, **kwargs):
1332 self.required_keys = ['host', 'user', 'password', 'database']
1333- super(HttpRelation).__init__(self, *args, **kwargs)
1334+ RelationContext.__init__(self, *args, **kwargs)
1335
1336
1337 class HttpRelation(RelationContext):
1338@@ -154,7 +156,7 @@
1339
1340 def __init__(self, *args, **kwargs):
1341 self.required_keys = ['host', 'port']
1342- super(HttpRelation).__init__(self, *args, **kwargs)
1343+ RelationContext.__init__(self, *args, **kwargs)
1344
1345 def provide_data(self):
1346 return {
1347@@ -239,28 +241,45 @@
1348 action.
1349
1350 :param str source: The template source file, relative to
1351- `$CHARM_DIR/templates`
1352+ `$CHARM_DIR/templates`
1353
1354 :param str target: The target to write the rendered template to
1355 :param str owner: The owner of the rendered file
1356 :param str group: The group of the rendered file
1357 :param int perms: The permissions of the rendered file
1358+ :param partial on_change_action: functools partial to be executed when
1359+ rendered file changes
1360+ :param jinja2 loader template_loader: A jinja2 template loader
1361 """
1362 def __init__(self, source, target,
1363- owner='root', group='root', perms=0o444):
1364+ owner='root', group='root', perms=0o444,
1365+ on_change_action=None, template_loader=None):
1366 self.source = source
1367 self.target = target
1368 self.owner = owner
1369 self.group = group
1370 self.perms = perms
1371+ self.on_change_action = on_change_action
1372+ self.template_loader = template_loader
1373
1374 def __call__(self, manager, service_name, event_name):
1375+ pre_checksum = ''
1376+ if self.on_change_action and os.path.isfile(self.target):
1377+ pre_checksum = host.file_hash(self.target)
1378 service = manager.get_service(service_name)
1379 context = {}
1380 for ctx in service.get('required_data', []):
1381 context.update(ctx)
1382 templating.render(self.source, self.target, context,
1383- self.owner, self.group, self.perms)
1384+ self.owner, self.group, self.perms,
1385+ template_loader=self.template_loader)
1386+ if self.on_change_action:
1387+ if pre_checksum == host.file_hash(self.target):
1388+ hookenv.log(
1389+ 'No change detected: {}'.format(self.target),
1390+ hookenv.DEBUG)
1391+ else:
1392+ self.on_change_action()
1393
1394
1395 # Convenience aliases for templates
1396
1397=== modified file 'hooks/charmhelpers/core/strutils.py'
1398--- hooks/charmhelpers/core/strutils.py 2015-03-17 04:10:30 +0000
1399+++ hooks/charmhelpers/core/strutils.py 2015-11-05 23:29:10 +0000
1400@@ -18,6 +18,7 @@
1401 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1402
1403 import six
1404+import re
1405
1406
1407 def bool_from_string(value):
1408@@ -33,10 +34,39 @@
1409
1410 value = value.strip().lower()
1411
1412- if value in ['y', 'yes', 'true', 't']:
1413+ if value in ['y', 'yes', 'true', 't', 'on']:
1414 return True
1415- elif value in ['n', 'no', 'false', 'f']:
1416+ elif value in ['n', 'no', 'false', 'f', 'off']:
1417 return False
1418
1419 msg = "Unable to interpret string value '%s' as boolean" % (value)
1420 raise ValueError(msg)
1421+
1422+
1423+def bytes_from_string(value):
1424+ """Interpret human readable string value as bytes.
1425+
1426+ Returns int
1427+ """
1428+ BYTE_POWER = {
1429+ 'K': 1,
1430+ 'KB': 1,
1431+ 'M': 2,
1432+ 'MB': 2,
1433+ 'G': 3,
1434+ 'GB': 3,
1435+ 'T': 4,
1436+ 'TB': 4,
1437+ 'P': 5,
1438+ 'PB': 5,
1439+ }
1440+ if isinstance(value, six.string_types):
1441+ value = six.text_type(value)
1442+ else:
1443+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
1444+ raise ValueError(msg)
1445+ matches = re.match("([0-9]+)([a-zA-Z]+)", value)
1446+ if not matches:
1447+ msg = "Unable to interpret string value '%s' as bytes" % (value)
1448+ raise ValueError(msg)
1449+ return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
1450
1451=== modified file 'hooks/charmhelpers/core/templating.py'
1452--- hooks/charmhelpers/core/templating.py 2015-03-17 04:10:30 +0000
1453+++ hooks/charmhelpers/core/templating.py 2015-11-05 23:29:10 +0000
1454@@ -21,7 +21,7 @@
1455
1456
1457 def render(source, target, context, owner='root', group='root',
1458- perms=0o444, templates_dir=None, encoding='UTF-8'):
1459+ perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None):
1460 """
1461 Render a template.
1462
1463@@ -52,17 +52,24 @@
1464 apt_install('python-jinja2', fatal=True)
1465 from jinja2 import FileSystemLoader, Environment, exceptions
1466
1467- if templates_dir is None:
1468- templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
1469- loader = Environment(loader=FileSystemLoader(templates_dir))
1470+ if template_loader:
1471+ template_env = Environment(loader=template_loader)
1472+ else:
1473+ if templates_dir is None:
1474+ templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
1475+ template_env = Environment(loader=FileSystemLoader(templates_dir))
1476 try:
1477 source = source
1478- template = loader.get_template(source)
1479+ template = template_env.get_template(source)
1480 except exceptions.TemplateNotFound as e:
1481 hookenv.log('Could not load template %s from %s.' %
1482 (source, templates_dir),
1483 level=hookenv.ERROR)
1484 raise e
1485 content = template.render(context)
1486- host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
1487+ target_dir = os.path.dirname(target)
1488+ if not os.path.exists(target_dir):
1489+ # This is a terrible default directory permission, as the file
1490+ # or its siblings will often contain secrets.
1491+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
1492 host.write_file(target, content.encode(encoding), owner, group, perms)
1493
1494=== modified file 'hooks/charmhelpers/core/unitdata.py'
1495--- hooks/charmhelpers/core/unitdata.py 2015-03-17 04:10:30 +0000
1496+++ hooks/charmhelpers/core/unitdata.py 2015-11-05 23:29:10 +0000
1497@@ -152,6 +152,7 @@
1498 import collections
1499 import contextlib
1500 import datetime
1501+import itertools
1502 import json
1503 import os
1504 import pprint
1505@@ -164,8 +165,7 @@
1506 class Storage(object):
1507 """Simple key value database for local unit state within charms.
1508
1509- Modifications are automatically committed at hook exit. That's
1510- currently regardless of exit code.
1511+ Modifications are not persisted unless :meth:`flush` is called.
1512
1513 To support dicts, lists, integer, floats, and booleans values
1514 are automatically json encoded/decoded.
1515@@ -173,8 +173,11 @@
1516 def __init__(self, path=None):
1517 self.db_path = path
1518 if path is None:
1519- self.db_path = os.path.join(
1520- os.environ.get('CHARM_DIR', ''), '.unit-state.db')
1521+ if 'UNIT_STATE_DB' in os.environ:
1522+ self.db_path = os.environ['UNIT_STATE_DB']
1523+ else:
1524+ self.db_path = os.path.join(
1525+ os.environ.get('CHARM_DIR', ''), '.unit-state.db')
1526 self.conn = sqlite3.connect('%s' % self.db_path)
1527 self.cursor = self.conn.cursor()
1528 self.revision = None
1529@@ -189,15 +192,8 @@
1530 self.conn.close()
1531 self._closed = True
1532
1533- def _scoped_query(self, stmt, params=None):
1534- if params is None:
1535- params = []
1536- return stmt, params
1537-
1538 def get(self, key, default=None, record=False):
1539- self.cursor.execute(
1540- *self._scoped_query(
1541- 'select data from kv where key=?', [key]))
1542+ self.cursor.execute('select data from kv where key=?', [key])
1543 result = self.cursor.fetchone()
1544 if not result:
1545 return default
1546@@ -206,33 +202,81 @@
1547 return json.loads(result[0])
1548
1549 def getrange(self, key_prefix, strip=False):
1550- stmt = "select key, data from kv where key like '%s%%'" % key_prefix
1551- self.cursor.execute(*self._scoped_query(stmt))
1552+ """
1553+ Get a range of keys starting with a common prefix as a mapping of
1554+ keys to values.
1555+
1556+ :param str key_prefix: Common prefix among all keys
1557+ :param bool strip: Optionally strip the common prefix from the key
1558+ names in the returned dict
1559+ :return dict: A (possibly empty) dict of key-value mappings
1560+ """
1561+ self.cursor.execute("select key, data from kv where key like ?",
1562+ ['%s%%' % key_prefix])
1563 result = self.cursor.fetchall()
1564
1565 if not result:
1566- return None
1567+ return {}
1568 if not strip:
1569 key_prefix = ''
1570 return dict([
1571 (k[len(key_prefix):], json.loads(v)) for k, v in result])
1572
1573 def update(self, mapping, prefix=""):
1574+ """
1575+ Set the values of multiple keys at once.
1576+
1577+ :param dict mapping: Mapping of keys to values
1578+ :param str prefix: Optional prefix to apply to all keys in `mapping`
1579+ before setting
1580+ """
1581 for k, v in mapping.items():
1582 self.set("%s%s" % (prefix, k), v)
1583
1584 def unset(self, key):
1585+ """
1586+ Remove a key from the database entirely.
1587+ """
1588 self.cursor.execute('delete from kv where key=?', [key])
1589 if self.revision and self.cursor.rowcount:
1590 self.cursor.execute(
1591 'insert into kv_revisions values (?, ?, ?)',
1592 [key, self.revision, json.dumps('DELETED')])
1593
1594+ def unsetrange(self, keys=None, prefix=""):
1595+ """
1596+ Remove a range of keys starting with a common prefix, from the database
1597+ entirely.
1598+
1599+ :param list keys: List of keys to remove.
1600+ :param str prefix: Optional prefix to apply to all keys in ``keys``
1601+ before removing.
1602+ """
1603+ if keys is not None:
1604+ keys = ['%s%s' % (prefix, key) for key in keys]
1605+ self.cursor.execute('delete from kv where key in (%s)' % ','.join(['?'] * len(keys)), keys)
1606+ if self.revision and self.cursor.rowcount:
1607+ self.cursor.execute(
1608+ 'insert into kv_revisions values %s' % ','.join(['(?, ?, ?)'] * len(keys)),
1609+ list(itertools.chain.from_iterable((key, self.revision, json.dumps('DELETED')) for key in keys)))
1610+ else:
1611+ self.cursor.execute('delete from kv where key like ?',
1612+ ['%s%%' % prefix])
1613+ if self.revision and self.cursor.rowcount:
1614+ self.cursor.execute(
1615+ 'insert into kv_revisions values (?, ?, ?)',
1616+ ['%s%%' % prefix, self.revision, json.dumps('DELETED')])
1617+
1618 def set(self, key, value):
1619+ """
1620+ Set a value in the database.
1621+
1622+ :param str key: Key to set the value for
1623+ :param value: Any JSON-serializable value to be set
1624+ """
1625 serialized = json.dumps(value)
1626
1627- self.cursor.execute(
1628- 'select data from kv where key=?', [key])
1629+ self.cursor.execute('select data from kv where key=?', [key])
1630 exists = self.cursor.fetchone()
1631
1632 # Skip mutations to the same value
1633@@ -443,7 +487,7 @@
1634 data = hookenv.execution_environment()
1635 self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
1636 self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
1637- self.kv.set('env', data['env'])
1638+ self.kv.set('env', dict(data['env']))
1639 self.kv.set('unit', data['unit'])
1640 self.kv.set('relid', data.get('relid'))
1641 return conf_delta, rels_delta
1642
1643=== modified file 'hooks/charmhelpers/fetch/__init__.py'
1644--- hooks/charmhelpers/fetch/__init__.py 2015-03-17 04:10:30 +0000
1645+++ hooks/charmhelpers/fetch/__init__.py 2015-11-05 23:29:10 +0000
1646@@ -90,6 +90,14 @@
1647 'kilo/proposed': 'trusty-proposed/kilo',
1648 'trusty-kilo/proposed': 'trusty-proposed/kilo',
1649 'trusty-proposed/kilo': 'trusty-proposed/kilo',
1650+ # Liberty
1651+ 'liberty': 'trusty-updates/liberty',
1652+ 'trusty-liberty': 'trusty-updates/liberty',
1653+ 'trusty-liberty/updates': 'trusty-updates/liberty',
1654+ 'trusty-updates/liberty': 'trusty-updates/liberty',
1655+ 'liberty/proposed': 'trusty-proposed/liberty',
1656+ 'trusty-liberty/proposed': 'trusty-proposed/liberty',
1657+ 'trusty-proposed/liberty': 'trusty-proposed/liberty',
1658 }
1659
1660 # The order of this list is very important. Handlers should be listed in from
1661@@ -158,7 +166,7 @@
1662
1663 def apt_cache(in_memory=True):
1664 """Build and return an apt cache"""
1665- import apt_pkg
1666+ from apt import apt_pkg
1667 apt_pkg.init()
1668 if in_memory:
1669 apt_pkg.config.set("Dir::Cache::pkgcache", "")
1670@@ -215,19 +223,27 @@
1671 _run_apt_command(cmd, fatal)
1672
1673
1674+def apt_mark(packages, mark, fatal=False):
1675+ """Flag one or more packages using apt-mark"""
1676+ log("Marking {} as {}".format(packages, mark))
1677+ cmd = ['apt-mark', mark]
1678+ if isinstance(packages, six.string_types):
1679+ cmd.append(packages)
1680+ else:
1681+ cmd.extend(packages)
1682+
1683+ if fatal:
1684+ subprocess.check_call(cmd, universal_newlines=True)
1685+ else:
1686+ subprocess.call(cmd, universal_newlines=True)
1687+
1688+
1689 def apt_hold(packages, fatal=False):
1690- """Hold one or more packages"""
1691- cmd = ['apt-mark', 'hold']
1692- if isinstance(packages, six.string_types):
1693- cmd.append(packages)
1694- else:
1695- cmd.extend(packages)
1696- log("Holding {}".format(packages))
1697-
1698- if fatal:
1699- subprocess.check_call(cmd)
1700- else:
1701- subprocess.call(cmd)
1702+ return apt_mark(packages, 'hold', fatal=fatal)
1703+
1704+
1705+def apt_unhold(packages, fatal=False):
1706+ return apt_mark(packages, 'unhold', fatal=fatal)
1707
1708
1709 def add_source(source, key=None):
1710@@ -370,8 +386,9 @@
1711 for handler in handlers:
1712 try:
1713 installed_to = handler.install(source, *args, **kwargs)
1714- except UnhandledSource:
1715- pass
1716+ except UnhandledSource as e:
1717+ log('Install source attempt unsuccessful: {}'.format(e),
1718+ level='WARNING')
1719 if not installed_to:
1720 raise UnhandledSource("No handler found for source {}".format(source))
1721 return installed_to
1722
1723=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
1724--- hooks/charmhelpers/fetch/archiveurl.py 2015-03-17 04:10:30 +0000
1725+++ hooks/charmhelpers/fetch/archiveurl.py 2015-11-05 23:29:10 +0000
1726@@ -77,6 +77,8 @@
1727 def can_handle(self, source):
1728 url_parts = self.parse_url(source)
1729 if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
1730+ # XXX: Why is this returning a boolean and a string? It's
1731+ # doomed to fail since "bool(can_handle('foo://'))" will be True.
1732 return "Wrong source type"
1733 if get_archive_handler(self.base_url(source)):
1734 return True
1735@@ -155,7 +157,11 @@
1736 else:
1737 algorithms = hashlib.algorithms_available
1738 if key in algorithms:
1739- check_hash(dld_file, value, key)
1740+ if len(value) != 1:
1741+ raise TypeError(
1742+ "Expected 1 hash value, not %d" % len(value))
1743+ expected = value[0]
1744+ check_hash(dld_file, expected, key)
1745 if checksum:
1746 check_hash(dld_file, checksum, hash_type)
1747 return extract(dld_file, dest)
1748
1749=== modified file 'hooks/charmhelpers/fetch/giturl.py'
1750--- hooks/charmhelpers/fetch/giturl.py 2015-03-17 04:10:30 +0000
1751+++ hooks/charmhelpers/fetch/giturl.py 2015-11-05 23:29:10 +0000
1752@@ -45,14 +45,16 @@
1753 else:
1754 return True
1755
1756- def clone(self, source, dest, branch):
1757+ def clone(self, source, dest, branch, depth=None):
1758 if not self.can_handle(source):
1759 raise UnhandledSource("Cannot handle {}".format(source))
1760
1761- repo = Repo.clone_from(source, dest)
1762- repo.git.checkout(branch)
1763+ if depth:
1764+ Repo.clone_from(source, dest, branch=branch, depth=depth)
1765+ else:
1766+ Repo.clone_from(source, dest, branch=branch)
1767
1768- def install(self, source, branch="master", dest=None):
1769+ def install(self, source, branch="master", dest=None, depth=None):
1770 url_parts = self.parse_url(source)
1771 branch_name = url_parts.path.strip("/").split("/")[-1]
1772 if dest:
1773@@ -63,9 +65,9 @@
1774 if not os.path.exists(dest_dir):
1775 mkdir(dest_dir, perms=0o755)
1776 try:
1777- self.clone(source, dest_dir, branch)
1778+ self.clone(source, dest_dir, branch, depth)
1779 except GitCommandError as e:
1780- raise UnhandledSource(e.message)
1781+ raise UnhandledSource(e)
1782 except OSError as e:
1783 raise UnhandledSource(e.strerror)
1784 return dest_dir

Subscribers

People subscribed via source and target branches

to all changes: