Merge lp:~1chb1n/charms/trusty/mongodb/sync-fetch-helpers-liberty into lp:charms/trusty/mongodb

Proposed by Ryan Beisner
Status: Merged
Merged at revision: 76
Proposed branch: lp:~1chb1n/charms/trusty/mongodb/sync-fetch-helpers-liberty
Merge into: lp:charms/trusty/mongodb
Diff against target: 1620 lines (+890/-143)
14 files modified
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+3/-1)
hooks/charmhelpers/contrib/hahelpers/cluster.py (+47/-3)
hooks/charmhelpers/contrib/python/packages.py (+30/-5)
hooks/charmhelpers/core/files.py (+45/-0)
hooks/charmhelpers/core/hookenv.py (+371/-43)
hooks/charmhelpers/core/host.py (+148/-24)
hooks/charmhelpers/core/hugepage.py (+62/-0)
hooks/charmhelpers/core/services/base.py (+43/-19)
hooks/charmhelpers/core/services/helpers.py (+30/-6)
hooks/charmhelpers/core/strutils.py (+2/-2)
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:~1chb1n/charms/trusty/mongodb/sync-fetch-helpers-liberty
Reviewer Review Type Date Requested Status
Liam Young (community) Approve
Review via email: mp+268413@code.launchpad.net

Description of the change

Sync hooks/charmhelpers for liberty cloud archive enablement.

Resolves:
2015-08-26 13:00:47 INFO install charmhelpers.fetch.SourceConfigError: Unsupported cloud: source option trusty-liberty/proposed

To post a comment you must log in.
Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #8336 mongodb for 1chb1n mp268413
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/8336/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #7733 mongodb for 1chb1n mp268413
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/7733/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #5878 mongodb for 1chb1n mp268413
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/5878/

77. By Ryan Beisner

resync all hooks/charmhelpers

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_lint_check #8364 mongodb for 1chb1n mp268413
    LINT OK: passed

Build: http://10.245.162.77:8080/job/charm_lint_check/8364/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_unit_test #7762 mongodb for 1chb1n mp268413
    UNIT OK: passed

Build: http://10.245.162.77:8080/job/charm_unit_test/7762/

Revision history for this message
uosci-testing-bot (uosci-testing-bot) wrote :

charm_amulet_test #5908 mongodb for 1chb1n mp268413
    AMULET OK: passed

Build: http://10.245.162.77:8080/job/charm_amulet_test/5908/

Revision history for this message
Liam Young (gnuoy) wrote :

Approve

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

Subscribers

People subscribed via source and target branches