Merge lp:~stub/charms/trusty/cassandra/bug-1557769-update-charmhelpers into lp:charms/trusty/cassandra

Proposed by Stuart Bishop
Status: Merged
Merge reported by: Adam Israel
Merged at revision: not available
Proposed branch: lp:~stub/charms/trusty/cassandra/bug-1557769-update-charmhelpers
Merge into: lp:charms/trusty/cassandra
Diff against target: 2354 lines (+1119/-267)
23 files modified
Makefile (+10/-3)
hooks/actions.py (+5/-5)
hooks/charmhelpers/contrib/benchmark/__init__.py (+3/-1)
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+52/-14)
hooks/charmhelpers/contrib/network/ufw.py (+51/-9)
hooks/charmhelpers/contrib/templating/jinja.py (+4/-3)
hooks/charmhelpers/coordinator.py (+18/-18)
hooks/charmhelpers/core/files.py (+45/-0)
hooks/charmhelpers/core/hookenv.py (+244/-42)
hooks/charmhelpers/core/host.py (+307/-62)
hooks/charmhelpers/core/hugepage.py (+71/-0)
hooks/charmhelpers/core/kernel.py (+68/-0)
hooks/charmhelpers/core/services/helpers.py (+31/-6)
hooks/charmhelpers/core/strutils.py (+30/-0)
hooks/charmhelpers/core/templating.py (+21/-8)
hooks/charmhelpers/core/unitdata.py (+61/-17)
hooks/charmhelpers/fetch/__init__.py (+40/-15)
hooks/charmhelpers/fetch/archiveurl.py (+8/-2)
hooks/charmhelpers/fetch/bzrurl.py (+22/-32)
hooks/charmhelpers/fetch/giturl.py (+21/-24)
hooks/helpers.py (+2/-2)
tests/test_actions.py (+3/-2)
tests/test_helpers.py (+2/-2)
To merge this branch: bzr merge lp:~stub/charms/trusty/cassandra/bug-1557769-update-charmhelpers
Reviewer Review Type Date Requested Status
Adam Israel (community) Approve
Review Queue (community) automated testing Approve
Review via email: mp+289346@code.launchpad.net

Description of the change

Fix Bug #1557769 by updating to a version of charmhelpers with a fixed unit_private_ip() function.

Without this, services on some providers will fail to restart after being upgraded to 1.25.4.

To post a comment you must log in.
Revision history for this message
Review Queue (review-queue) wrote :

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

review: Approve (automated testing)
Revision history for this message
Review Queue (review-queue) wrote :

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

review: Approve (automated testing)
Revision history for this message
Adam Israel (aisrael) wrote :

Hey Stub, this looks good to me. Thanks for the updates! +1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2016-02-26 16:47:03 +0000
3+++ Makefile 2016-03-17 11:00:44 +0000
4@@ -24,6 +24,13 @@
5 # Only trusty supported, but xenial expected soon.
6 SERIES := $(shell juju get-environment default-series)
7
8+HOST_SERIES := $(shell lsb_release -sc)
9+ifeq ($(HOST_SERIES),trusty)
10+ PYVER := 3.4
11+else
12+ PYVER := 3.5
13+endif
14+
15
16 # /!\ Ensure that errors early in pipes cause failures, rather than
17 # overridden by the last stage of the pipe. cf. 'test.py | ts'
18@@ -41,8 +48,8 @@
19
20 SITE_PACKAGES=$(wildcard $(VENV3)/lib/python*/site-packages)
21
22-PIP=.venv3/bin/pip3.4 -q
23-NOSETESTS=.venv3/bin/nosetests-3.4 -sv
24+PIP=.venv3/bin/pip$(PYVER) -q
25+NOSETESTS=.venv3/bin/nosetests-3.4 -sv # Yes, even with 3.5
26
27 # Set pipefail so we can get sane error codes while tagging test output
28 # with ts(1)
29@@ -177,7 +184,7 @@
30 # Create a .pth so our tests can locate everything without
31 # sys.path hacks.
32 (echo ${CHARM_DIR}/hooks; echo ${CHARM_DIR}) \
33- > ${VENV3}/lib/python3.4/site-packages/tests.pth
34+ > ${VENV3}/lib/python${PYVER}/site-packages/tests.pth
35
36 echo 'pip: ' `which pip`
37
38
39=== modified file 'hooks/actions.py'
40--- hooks/actions.py 2016-02-26 16:47:03 +0000
41+++ hooks/actions.py 2016-03-17 11:00:44 +0000
42@@ -298,7 +298,7 @@
43 helpers.status_set('blocked',
44 'Invalid private_jre_url {}'.format(url))
45 raise SystemExit(0)
46- helpers.status_set(hookenv.status_get(),
47+ helpers.status_set(hookenv.status_get()[0],
48 'Downloading Oracle JRE')
49 hookenv.log('Oracle JRE URL is {}'.format(url))
50 urllib.request.urlretrieve(url, filename)
51@@ -507,7 +507,7 @@
52 # during a restart.
53 storage = relations.StorageRelation()
54 if storage.needs_remount():
55- helpers.status_set(hookenv.status_get(),
56+ helpers.status_set(hookenv.status_get()[0],
57 'New mounts. Waiting for restart permission')
58 return True
59
60@@ -517,7 +517,7 @@
61 hookenv.log('{} changed. Restart required.'.format(key))
62 for key in RESTART_REQUIRED_KEYS:
63 if config.changed(key):
64- helpers.status_set(hookenv.status_get(),
65+ helpers.status_set(hookenv.status_get()[0],
66 'Config changes. '
67 'Waiting for restart permission.')
68 return True
69@@ -530,7 +530,7 @@
70 # We don't care about the local node in the changes.
71 changed.discard(hookenv.unit_private_ip())
72 if changed:
73- helpers.status_set(hookenv.status_get(),
74+ helpers.status_set(hookenv.status_get()[0],
75 'Updated seeds {!r}. '
76 'Waiting for restart permission.'
77 ''.format(new_seeds))
78@@ -924,7 +924,7 @@
79 # not already active. We don't do this unconditionally, as the charm
80 # may be active but doing stuff, like active but waiting for restart
81 # permission.
82- if hookenv.status_get() != 'active':
83+ if hookenv.status_get()[0] != 'active':
84 helpers.set_active()
85 else:
86 hookenv.log('Unit status already active', DEBUG)
87
88=== modified file 'hooks/charmhelpers/contrib/benchmark/__init__.py'
89--- hooks/charmhelpers/contrib/benchmark/__init__.py 2015-05-07 11:11:42 +0000
90+++ hooks/charmhelpers/contrib/benchmark/__init__.py 2016-03-17 11:00:44 +0000
91@@ -63,6 +63,8 @@
92
93 """
94
95+ BENCHMARK_CONF = '/etc/benchmark.conf' # Replaced in testing
96+
97 required_keys = [
98 'hostname',
99 'port',
100@@ -91,7 +93,7 @@
101 break
102
103 if len(config):
104- with open('/etc/benchmark.conf', 'w') as f:
105+ with open(self.BENCHMARK_CONF, 'w') as f:
106 for key, val in iter(config.items()):
107 f.write("%s=%s\n" % (key, val))
108
109
110=== modified file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
111--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 2015-04-29 13:21:29 +0000
112+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2016-03-17 11:00:44 +0000
113@@ -148,6 +148,13 @@
114 self.description = description
115 self.check_cmd = self._locate_cmd(check_cmd)
116
117+ def _get_check_filename(self):
118+ return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command))
119+
120+ def _get_service_filename(self, hostname):
121+ return os.path.join(NRPE.nagios_exportdir,
122+ 'service__{}_{}.cfg'.format(hostname, self.command))
123+
124 def _locate_cmd(self, check_cmd):
125 search_path = (
126 '/usr/lib/nagios/plugins',
127@@ -163,9 +170,21 @@
128 log('Check command not found: {}'.format(parts[0]))
129 return ''
130
131+ def _remove_service_files(self):
132+ if not os.path.exists(NRPE.nagios_exportdir):
133+ return
134+ for f in os.listdir(NRPE.nagios_exportdir):
135+ if f.endswith('_{}.cfg'.format(self.command)):
136+ os.remove(os.path.join(NRPE.nagios_exportdir, f))
137+
138+ def remove(self, hostname):
139+ nrpe_check_file = self._get_check_filename()
140+ if os.path.exists(nrpe_check_file):
141+ os.remove(nrpe_check_file)
142+ self._remove_service_files()
143+
144 def write(self, nagios_context, hostname, nagios_servicegroups):
145- nrpe_check_file = '/etc/nagios/nrpe.d/{}.cfg'.format(
146- self.command)
147+ nrpe_check_file = self._get_check_filename()
148 with open(nrpe_check_file, 'w') as nrpe_check_config:
149 nrpe_check_config.write("# check {}\n".format(self.shortname))
150 nrpe_check_config.write("command[{}]={}\n".format(
151@@ -180,9 +199,7 @@
152
153 def write_service_config(self, nagios_context, hostname,
154 nagios_servicegroups):
155- for f in os.listdir(NRPE.nagios_exportdir):
156- if re.search('.*{}.cfg'.format(self.command), f):
157- os.remove(os.path.join(NRPE.nagios_exportdir, f))
158+ self._remove_service_files()
159
160 templ_vars = {
161 'nagios_hostname': hostname,
162@@ -192,8 +209,7 @@
163 'command': self.command,
164 }
165 nrpe_service_text = Check.service_template.format(**templ_vars)
166- nrpe_service_file = '{}/service__{}_{}.cfg'.format(
167- NRPE.nagios_exportdir, hostname, self.command)
168+ nrpe_service_file = self._get_service_filename(hostname)
169 with open(nrpe_service_file, 'w') as nrpe_service_config:
170 nrpe_service_config.write(str(nrpe_service_text))
171
172@@ -218,12 +234,32 @@
173 if hostname:
174 self.hostname = hostname
175 else:
176- self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
177+ nagios_hostname = get_nagios_hostname()
178+ if nagios_hostname:
179+ self.hostname = nagios_hostname
180+ else:
181+ self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
182 self.checks = []
183
184 def add_check(self, *args, **kwargs):
185 self.checks.append(Check(*args, **kwargs))
186
187+ def remove_check(self, *args, **kwargs):
188+ if kwargs.get('shortname') is None:
189+ raise ValueError('shortname of check must be specified')
190+
191+ # Use sensible defaults if they're not specified - these are not
192+ # actually used during removal, but they're required for constructing
193+ # the Check object; check_disk is chosen because it's part of the
194+ # nagios-plugins-basic package.
195+ if kwargs.get('check_cmd') is None:
196+ kwargs['check_cmd'] = 'check_disk'
197+ if kwargs.get('description') is None:
198+ kwargs['description'] = ''
199+
200+ check = Check(*args, **kwargs)
201+ check.remove(self.hostname)
202+
203 def write(self):
204 try:
205 nagios_uid = pwd.getpwnam('nagios').pw_uid
206@@ -260,7 +296,7 @@
207 :param str relation_name: Name of relation nrpe sub joined to
208 """
209 for rel in relations_of_type(relation_name):
210- if 'nagios_hostname' in rel:
211+ if 'nagios_host_context' in rel:
212 return rel['nagios_host_context']
213
214
215@@ -301,11 +337,13 @@
216 upstart_init = '/etc/init/%s.conf' % svc
217 sysv_init = '/etc/init.d/%s' % svc
218 if os.path.exists(upstart_init):
219- nrpe.add_check(
220- shortname=svc,
221- description='process check {%s}' % unit_name,
222- check_cmd='check_upstart_job %s' % svc
223- )
224+ # Don't add a check for these services from neutron-gateway
225+ if svc not in ['ext-port', 'os-charm-phy-nic-mtu']:
226+ nrpe.add_check(
227+ shortname=svc,
228+ description='process check {%s}' % unit_name,
229+ check_cmd='check_upstart_job %s' % svc
230+ )
231 elif os.path.exists(sysv_init):
232 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
233 cron_file = ('*/5 * * * * root '
234
235=== modified file 'hooks/charmhelpers/contrib/network/ufw.py'
236--- hooks/charmhelpers/contrib/network/ufw.py 2015-02-18 14:24:32 +0000
237+++ hooks/charmhelpers/contrib/network/ufw.py 2016-03-17 11:00:44 +0000
238@@ -40,7 +40,9 @@
239 import re
240 import os
241 import subprocess
242+
243 from charmhelpers.core import hookenv
244+from charmhelpers.core.kernel import modprobe, is_module_loaded
245
246 __author__ = "Felipe Reyes <felipe.reyes@canonical.com>"
247
248@@ -82,14 +84,11 @@
249 # do we have IPv6 in the machine?
250 if os.path.isdir('/proc/sys/net/ipv6'):
251 # is ip6tables kernel module loaded?
252- lsmod = subprocess.check_output(['lsmod'], universal_newlines=True)
253- matches = re.findall('^ip6_tables[ ]+', lsmod, re.M)
254- if len(matches) == 0:
255+ if not is_module_loaded('ip6_tables'):
256 # ip6tables support isn't complete, let's try to load it
257 try:
258- subprocess.check_output(['modprobe', 'ip6_tables'],
259- universal_newlines=True)
260- # great, we could load the module
261+ modprobe('ip6_tables')
262+ # great, we can load the module
263 return True
264 except subprocess.CalledProcessError as ex:
265 hookenv.log("Couldn't load ip6_tables module: %s" % ex.output,
266@@ -180,7 +179,43 @@
267 return True
268
269
270-def modify_access(src, dst='any', port=None, proto=None, action='allow'):
271+def default_policy(policy='deny', direction='incoming'):
272+ """
273+ Changes the default policy for traffic `direction`
274+
275+ :param policy: allow, deny or reject
276+ :param direction: traffic direction, possible values: incoming, outgoing,
277+ routed
278+ """
279+ if policy not in ['allow', 'deny', 'reject']:
280+ raise UFWError(('Unknown policy %s, valid values: '
281+ 'allow, deny, reject') % policy)
282+
283+ if direction not in ['incoming', 'outgoing', 'routed']:
284+ raise UFWError(('Unknown direction %s, valid values: '
285+ 'incoming, outgoing, routed') % direction)
286+
287+ output = subprocess.check_output(['ufw', 'default', policy, direction],
288+ universal_newlines=True,
289+ env={'LANG': 'en_US',
290+ 'PATH': os.environ['PATH']})
291+ hookenv.log(output, level='DEBUG')
292+
293+ m = re.findall("^Default %s policy changed to '%s'\n" % (direction,
294+ policy),
295+ output, re.M)
296+ if len(m) == 0:
297+ hookenv.log("ufw couldn't change the default policy to %s for %s"
298+ % (policy, direction), level='WARN')
299+ return False
300+ else:
301+ hookenv.log("ufw default policy for %s changed to %s"
302+ % (direction, policy), level='INFO')
303+ return True
304+
305+
306+def modify_access(src, dst='any', port=None, proto=None, action='allow',
307+ index=None):
308 """
309 Grant access to an address or subnet
310
311@@ -192,6 +227,8 @@
312 :param port: destiny port
313 :param proto: protocol (tcp or udp)
314 :param action: `allow` or `delete`
315+ :param index: if different from None the rule is inserted at the given
316+ `index`.
317 """
318 if not is_enabled():
319 hookenv.log('ufw is disabled, skipping modify_access()', level='WARN')
320@@ -199,6 +236,8 @@
321
322 if action == 'delete':
323 cmd = ['ufw', 'delete', 'allow']
324+ elif index is not None:
325+ cmd = ['ufw', 'insert', str(index), action]
326 else:
327 cmd = ['ufw', action]
328
329@@ -227,7 +266,7 @@
330 level='ERROR')
331
332
333-def grant_access(src, dst='any', port=None, proto=None):
334+def grant_access(src, dst='any', port=None, proto=None, index=None):
335 """
336 Grant access to an address or subnet
337
338@@ -238,8 +277,11 @@
339 field has to be set.
340 :param port: destiny port
341 :param proto: protocol (tcp or udp)
342+ :param index: if different from None the rule is inserted at the given
343+ `index`.
344 """
345- return modify_access(src, dst=dst, port=port, proto=proto, action='allow')
346+ return modify_access(src, dst=dst, port=port, proto=proto, action='allow',
347+ index=index)
348
349
350 def revoke_access(src, dst='any', port=None, proto=None):
351
352=== modified file 'hooks/charmhelpers/contrib/templating/jinja.py'
353--- hooks/charmhelpers/contrib/templating/jinja.py 2015-01-26 13:07:31 +0000
354+++ hooks/charmhelpers/contrib/templating/jinja.py 2016-03-17 11:00:44 +0000
355@@ -18,14 +18,15 @@
356 Templating using the python-jinja2 package.
357 """
358 import six
359-from charmhelpers.fetch import apt_install
360+from charmhelpers.fetch import apt_install, apt_update
361 try:
362 import jinja2
363 except ImportError:
364+ apt_update(fatal=True)
365 if six.PY3:
366- apt_install(["python3-jinja2"])
367+ apt_install(["python3-jinja2"], fatal=True)
368 else:
369- apt_install(["python-jinja2"])
370+ apt_install(["python-jinja2"], fatal=True)
371 import jinja2
372
373
374
375=== modified file 'hooks/charmhelpers/coordinator.py'
376--- hooks/charmhelpers/coordinator.py 2015-06-19 10:21:21 +0000
377+++ hooks/charmhelpers/coordinator.py 2016-03-17 11:00:44 +0000
378@@ -29,10 +29,10 @@
379 Services Framework Usage
380 ========================
381
382-Ensure a peer relation is defined in metadata.yaml. Instantiate a
383+Ensure a peers relation is defined in metadata.yaml. Instantiate a
384 BaseCoordinator subclass before invoking ServiceManager.manage().
385 Ensure that ServiceManager.manage() is wired up to the leader-elected,
386-leader-settings-changed, peer relation-changed and peer
387+leader-settings-changed, peers relation-changed and peers
388 relation-departed hooks in addition to any other hooks you need, or your
389 service will deadlock.
390
391@@ -90,7 +90,7 @@
392 Traditional Usage
393 =================
394
395-Ensure a peer relationis defined in metadata.yaml.
396+Ensure a peers relation is defined in metadata.yaml.
397
398 If you are using charmhelpers.core.hookenv.Hooks, ensure that a
399 BaseCoordinator subclass is instantiated before calling Hooks.execute.
400@@ -151,7 +151,7 @@
401 hookenv.service_restart('myservice')
402
403 @hooks.hook('install', 'config-changed', 'upgrade-charm',
404- # Peer and leader hooks must be wired up.
405+ # Peers and leader hooks must be wired up.
406 'cluster-relation-changed', 'cluster-relation-departed',
407 'leader-elected', 'leader-settings-changed')
408 def default_hook():
409@@ -174,7 +174,7 @@
410 Locks are released at the end of the hook they are acquired in. This may
411 be the current hook if the unit is leader and the lock is free. It is
412 more likely a future hook (probably leader-settings-changed, possibly
413-the peer relation-changed or departed hook, potentially any hook).
414+the peers relation-changed or departed hook, potentially any hook).
415
416 Whenever a charm needs to perform a coordinated action it will acquire()
417 the lock and perform the action immediately if acquisition is
418@@ -189,16 +189,16 @@
419 If the unit is the leader, then it may be able to grant its own lock
420 and perform the action immediately in the source hook. If the unit is
421 the leader and cannot immediately grant the lock, then its only
422-guaranteed chance of acquiring the lock is in the peer relation-joined,
423-relation-changed or peer relation-departed hooks when another unit has
424-released it (the only channel to communicate to the leader is the peer
425+guaranteed chance of acquiring the lock is in the peers relation-joined,
426+relation-changed or peers relation-departed hooks when another unit has
427+released it (the only channel to communicate to the leader is the peers
428 relation). If the unit is not the leader, then it is unlikely the lock
429 is granted in the source hook (a previous hook must have also made the
430 request for this to happen). A non-leader is notified about the lock via
431 leader settings. These changes may be visible in any hook, even before
432 the leader-settings-changed hook has been invoked. Or the requesting
433 unit may be promoted to leader after making a request, in which case the
434-lock may be granted in leader-elected or in a future peer
435+lock may be granted in leader-elected or in a future peers
436 relation-changed or relation-departed hook.
437
438 This could be simpler if leader-settings-changed was invoked on the
439@@ -255,10 +255,10 @@
440 def __init__(self, relation_key='coordinator', peer_relation_name=None):
441 '''Instatiate a Coordinator.
442
443- Data is stored on the peer relation and in leadership storage
444+ Data is stored on the peers relation and in leadership storage
445 under the provided relation_key.
446
447- The peer relation is identified by peer_relation_name, and defaults
448+ The peers relation is identified by peer_relation_name, and defaults
449 to the first one found in metadata.yaml.
450 '''
451 # Most initialization is deferred, since invoking hook tools from
452@@ -310,13 +310,13 @@
453
454 Do not mindlessly call this method, as it triggers a cascade of
455 hooks. For example, if you call acquire() every time in your
456- peer relation-changed hook you will end up with an infinite loop
457+ peers relation-changed hook you will end up with an infinite loop
458 of hooks. It should almost always be guarded by some condition.
459 '''
460 unit = hookenv.local_unit()
461 ts = self.requests[unit].get(lock)
462 if not ts:
463- # If there is no outstanding request on the peer relation,
464+ # If there is no outstanding request on the peers relation,
465 # create one.
466 self.requests.setdefault(lock, {})
467 self.requests[unit][lock] = _timestamp()
468@@ -329,7 +329,7 @@
469
470 # If the unit making the request also happens to be the
471 # leader, it must handle the request now. Even though the
472- # request has been stored on the peer relation, the peer
473+ # request has been stored on the peers relation, the peers
474 # relation-changed hook will not be triggered.
475 if hookenv.is_leader():
476 return self.grant(lock, unit)
477@@ -476,13 +476,13 @@
478
479 local_unit = hookenv.local_unit()
480
481- # All requests must be stored on the peer relation. This is
482+ # All requests must be stored on the peers relation. This is
483 # the only channel units have to communicate with the leader.
484 # Even the leader needs to store its requests here, as a
485 # different unit may be leader by the time the request can be
486 # granted.
487 if self.relid is None:
488- # The peer relation is not available. Maybe we are early in
489+ # The peers relation is not available. Maybe we are early in
490 # the units's lifecycle. Maybe this unit is standalone.
491 # Fallback to using local state.
492 self.msg('No peer relation. Loading local state')
493@@ -490,7 +490,7 @@
494 else:
495 self.requests = self._load_peer_state()
496 if local_unit not in self.requests:
497- # The peer relation has just been joined. Update any state
498+ # The peers relation has just been joined. Update any state
499 # loaded from our peers with our local state.
500 self.msg('New peer relation. Merging local state')
501 self.requests[local_unit] = self._load_local_state()
502@@ -513,7 +513,7 @@
503 local_unit = hookenv.local_unit()
504
505 if self.relid is None:
506- # No peer relation yet. Fallback to local state.
507+ # No peers relation yet. Fallback to local state.
508 self.msg('No peer relation. Saving local state')
509 self._save_local_state(self.requests[local_unit])
510 else:
511
512=== added file 'hooks/charmhelpers/core/files.py'
513--- hooks/charmhelpers/core/files.py 1970-01-01 00:00:00 +0000
514+++ hooks/charmhelpers/core/files.py 2016-03-17 11:00:44 +0000
515@@ -0,0 +1,45 @@
516+#!/usr/bin/env python
517+# -*- coding: utf-8 -*-
518+
519+# Copyright 2014-2015 Canonical Limited.
520+#
521+# This file is part of charm-helpers.
522+#
523+# charm-helpers is free software: you can redistribute it and/or modify
524+# it under the terms of the GNU Lesser General Public License version 3 as
525+# published by the Free Software Foundation.
526+#
527+# charm-helpers is distributed in the hope that it will be useful,
528+# but WITHOUT ANY WARRANTY; without even the implied warranty of
529+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
530+# GNU Lesser General Public License for more details.
531+#
532+# You should have received a copy of the GNU Lesser General Public License
533+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
534+
535+__author__ = 'Jorge Niedbalski <niedbalski@ubuntu.com>'
536+
537+import os
538+import subprocess
539+
540+
541+def sed(filename, before, after, flags='g'):
542+ """
543+ Search and replaces the given pattern on filename.
544+
545+ :param filename: relative or absolute file path.
546+ :param before: expression to be replaced (see 'man sed')
547+ :param after: expression to replace with (see 'man sed')
548+ :param flags: sed-compatible regex flags in example, to make
549+ the search and replace case insensitive, specify ``flags="i"``.
550+ The ``g`` flag is always specified regardless, so you do not
551+ need to remember to include it when overriding this parameter.
552+ :returns: If the sed command exit code was zero then return,
553+ otherwise raise CalledProcessError.
554+ """
555+ expression = r's/{0}/{1}/{2}'.format(before,
556+ after, flags)
557+
558+ return subprocess.check_call(["sed", "-i", "-r", "-e",
559+ expression,
560+ os.path.expanduser(filename)])
561
562=== modified file 'hooks/charmhelpers/core/hookenv.py'
563--- hooks/charmhelpers/core/hookenv.py 2015-06-12 13:53:43 +0000
564+++ hooks/charmhelpers/core/hookenv.py 2016-03-17 11:00:44 +0000
565@@ -21,12 +21,14 @@
566 # Charm Helpers Developers <juju@lists.ubuntu.com>
567
568 from __future__ import print_function
569+import copy
570 from distutils.version import LooseVersion
571 from functools import wraps
572 import glob
573 import os
574 import json
575 import yaml
576+import socket
577 import subprocess
578 import sys
579 import errno
580@@ -73,6 +75,7 @@
581 res = func(*args, **kwargs)
582 cache[key] = res
583 return res
584+ wrapper._wrapped = func
585 return wrapper
586
587
588@@ -172,9 +175,19 @@
589 return os.environ.get('JUJU_RELATION', None)
590
591
592-def relation_id():
593- """The relation ID for the current relation hook"""
594- return os.environ.get('JUJU_RELATION_ID', None)
595+@cached
596+def relation_id(relation_name=None, service_or_unit=None):
597+ """The relation ID for the current or a specified relation"""
598+ if not relation_name and not service_or_unit:
599+ return os.environ.get('JUJU_RELATION_ID', None)
600+ elif relation_name and service_or_unit:
601+ service_name = service_or_unit.split('/')[0]
602+ for relid in relation_ids(relation_name):
603+ remote_service = remote_service_name(relid)
604+ if remote_service == service_name:
605+ return relid
606+ else:
607+ raise ValueError('Must specify neither or both of relation_name and service_or_unit')
608
609
610 def local_unit():
611@@ -192,9 +205,20 @@
612 return local_unit().split('/')[0]
613
614
615+@cached
616+def remote_service_name(relid=None):
617+ """The remote service name for a given relation-id (or the current relation)"""
618+ if relid is None:
619+ unit = remote_unit()
620+ else:
621+ units = related_units(relid)
622+ unit = units[0] if units else None
623+ return unit.split('/')[0] if unit else None
624+
625+
626 def hook_name():
627 """The name of the currently executing hook"""
628- return os.path.basename(sys.argv[0])
629+ return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
630
631
632 class Config(dict):
633@@ -246,29 +270,6 @@
634 self.load_previous()
635 atexit(self._implicit_save)
636
637- def __getitem__(self, key):
638- """For regular dict lookups, check the current juju config first,
639- then the previous (saved) copy. This ensures that user-saved values
640- will be returned by a dict lookup.
641-
642- """
643- try:
644- return dict.__getitem__(self, key)
645- except KeyError:
646- return (self._prev_dict or {})[key]
647-
648- def get(self, key, default=None):
649- try:
650- return self[key]
651- except KeyError:
652- return default
653-
654- def keys(self):
655- prev_keys = []
656- if self._prev_dict is not None:
657- prev_keys = self._prev_dict.keys()
658- return list(set(prev_keys + list(dict.keys(self))))
659-
660 def load_previous(self, path=None):
661 """Load previous copy of config from disk.
662
663@@ -286,6 +287,9 @@
664 self.path = path or self.path
665 with open(self.path) as f:
666 self._prev_dict = json.load(f)
667+ for k, v in copy.deepcopy(self._prev_dict).items():
668+ if k not in self:
669+ self[k] = v
670
671 def changed(self, key):
672 """Return True if the current value for this key is different from
673@@ -317,10 +321,6 @@
674 instance.
675
676 """
677- if self._prev_dict:
678- for k, v in six.iteritems(self._prev_dict):
679- if k not in self:
680- self[k] = v
681 with open(self.path, 'w') as f:
682 json.dump(self, f)
683
684@@ -492,6 +492,76 @@
685
686
687 @cached
688+def peer_relation_id():
689+ '''Get the peers relation id if a peers relation has been joined, else None.'''
690+ md = metadata()
691+ section = md.get('peers')
692+ if section:
693+ for key in section:
694+ relids = relation_ids(key)
695+ if relids:
696+ return relids[0]
697+ return None
698+
699+
700+@cached
701+def relation_to_interface(relation_name):
702+ """
703+ Given the name of a relation, return the interface that relation uses.
704+
705+ :returns: The interface name, or ``None``.
706+ """
707+ return relation_to_role_and_interface(relation_name)[1]
708+
709+
710+@cached
711+def relation_to_role_and_interface(relation_name):
712+ """
713+ Given the name of a relation, return the role and the name of the interface
714+ that relation uses (where role is one of ``provides``, ``requires``, or ``peers``).
715+
716+ :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
717+ """
718+ _metadata = metadata()
719+ for role in ('provides', 'requires', 'peers'):
720+ interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
721+ if interface:
722+ return role, interface
723+ return None, None
724+
725+
726+@cached
727+def role_and_interface_to_relations(role, interface_name):
728+ """
729+ Given a role and interface name, return a list of relation names for the
730+ current charm that use that interface under that role (where role is one
731+ of ``provides``, ``requires``, or ``peers``).
732+
733+ :returns: A list of relation names.
734+ """
735+ _metadata = metadata()
736+ results = []
737+ for relation_name, relation in _metadata.get(role, {}).items():
738+ if relation['interface'] == interface_name:
739+ results.append(relation_name)
740+ return results
741+
742+
743+@cached
744+def interface_to_relations(interface_name):
745+ """
746+ Given an interface, return a list of relation names for the current
747+ charm that use that interface.
748+
749+ :returns: A list of relation names.
750+ """
751+ results = []
752+ for role in ('provides', 'requires', 'peers'):
753+ results.extend(role_and_interface_to_relations(role, interface_name))
754+ return results
755+
756+
757+@cached
758 def charm_name():
759 """Get the name of the current charm as is specified on metadata.yaml"""
760 return metadata().get('name')
761@@ -559,12 +629,60 @@
762
763 def unit_public_ip():
764 """Get this unit's public IP address"""
765- return unit_get('public-address')
766+ return _ensure_ip(unit_get('public-address'))
767
768
769 def unit_private_ip():
770 """Get this unit's private IP address"""
771- return unit_get('private-address')
772+ return _ensure_ip(unit_get('private-address'))
773+
774+
775+def _ensure_ip(addr):
776+ """If addr is a hostname, resolve it to an IP address"""
777+ if not addr:
778+ return None
779+ # We need to use socket.getaddrinfo for IPv6 support.
780+ info = socket.getaddrinfo(addr, None)
781+ if info is None:
782+ # Should never happen
783+ raise ValueError("Invalid result None from getaddinfo")
784+ try:
785+ return info[0][4][0]
786+ except IndexError:
787+ # Should never happen
788+ raise ValueError("Invalid result {!r} from getaddinfo".format(info))
789+
790+
791+@cached
792+def storage_get(attribute=None, storage_id=None):
793+ """Get storage attributes"""
794+ _args = ['storage-get', '--format=json']
795+ if storage_id:
796+ _args.extend(('-s', storage_id))
797+ if attribute:
798+ _args.append(attribute)
799+ try:
800+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
801+ except ValueError:
802+ return None
803+
804+
805+@cached
806+def storage_list(storage_name=None):
807+ """List the storage IDs for the unit"""
808+ _args = ['storage-list', '--format=json']
809+ if storage_name:
810+ _args.append(storage_name)
811+ try:
812+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
813+ except ValueError:
814+ return None
815+ except OSError as e:
816+ import errno
817+ if e.errno == errno.ENOENT:
818+ # storage-list does not exist
819+ return []
820+ raise
821
822
823 class UnregisteredHookError(Exception):
824@@ -667,6 +785,21 @@
825 subprocess.check_call(['action-fail', message])
826
827
828+def action_name():
829+ """Get the name of the currently executing action."""
830+ return os.environ.get('JUJU_ACTION_NAME')
831+
832+
833+def action_uuid():
834+ """Get the UUID of the currently executing action."""
835+ return os.environ.get('JUJU_ACTION_UUID')
836+
837+
838+def action_tag():
839+ """Get the tag for the currently executing action."""
840+ return os.environ.get('JUJU_ACTION_TAG')
841+
842+
843 def status_set(workload_state, message):
844 """Set the workload state with a message
845
846@@ -696,25 +829,28 @@
847
848
849 def status_get():
850- """Retrieve the previously set juju workload state
851-
852- If the status-set command is not found then assume this is juju < 1.23 and
853- return 'unknown'
854+ """Retrieve the previously set juju workload state and message
855+
856+ If the status-get command is not found then assume this is juju < 1.23 and
857+ return 'unknown', ""
858+
859 """
860- cmd = ['status-get']
861+ cmd = ['status-get', "--format=json", "--include-data"]
862 try:
863- raw_status = subprocess.check_output(cmd, universal_newlines=True)
864- status = raw_status.rstrip()
865- return status
866+ raw_status = subprocess.check_output(cmd)
867 except OSError as e:
868 if e.errno == errno.ENOENT:
869- return 'unknown'
870+ return ('unknown', "")
871 else:
872 raise
873+ else:
874+ status = json.loads(raw_status.decode("UTF-8"))
875+ return (status["status"], status["message"])
876
877
878 def translate_exc(from_exc, to_exc):
879 def inner_translate_exc1(f):
880+ @wraps(f)
881 def inner_translate_exc2(*args, **kwargs):
882 try:
883 return f(*args, **kwargs)
884@@ -759,6 +895,58 @@
885 subprocess.check_call(cmd)
886
887
888+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
889+def payload_register(ptype, klass, pid):
890+ """ is used while a hook is running to let Juju know that a
891+ payload has been started."""
892+ cmd = ['payload-register']
893+ for x in [ptype, klass, pid]:
894+ cmd.append(x)
895+ subprocess.check_call(cmd)
896+
897+
898+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
899+def payload_unregister(klass, pid):
900+ """ is used while a hook is running to let Juju know
901+ that a payload has been manually stopped. The <class> and <id> provided
902+ must match a payload that has been previously registered with juju using
903+ payload-register."""
904+ cmd = ['payload-unregister']
905+ for x in [klass, pid]:
906+ cmd.append(x)
907+ subprocess.check_call(cmd)
908+
909+
910+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
911+def payload_status_set(klass, pid, status):
912+ """is used to update the current status of a registered payload.
913+ The <class> and <id> provided must match a payload that has been previously
914+ registered with juju using payload-register. The <status> must be one of the
915+ follow: starting, started, stopping, stopped"""
916+ cmd = ['payload-status-set']
917+ for x in [klass, pid, status]:
918+ cmd.append(x)
919+ subprocess.check_call(cmd)
920+
921+
922+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
923+def resource_get(name):
924+ """used to fetch the resource path of the given name.
925+
926+ <name> must match a name of defined resource in metadata.yaml
927+
928+ returns either a path or False if resource not available
929+ """
930+ if not name:
931+ return False
932+
933+ cmd = ['resource-get', name]
934+ try:
935+ return subprocess.check_output(cmd).decode('UTF-8')
936+ except subprocess.CalledProcessError:
937+ return False
938+
939+
940 @cached
941 def juju_version():
942 """Full version string (eg. '1.23.3.1-trusty-amd64')"""
943@@ -785,6 +973,7 @@
944
945 This is useful for modules and classes to perform initialization
946 and inject behavior. In particular:
947+
948 - Run common code before all of your hooks, such as logging
949 the hook name or interesting relation data.
950 - Defer object or module initialization that requires a hook
951@@ -822,3 +1011,16 @@
952 for callback, args, kwargs in reversed(_atexit):
953 callback(*args, **kwargs)
954 del _atexit[:]
955+
956+
957+@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
958+def network_get_primary_address(binding):
959+ '''
960+ Retrieve the primary network address for a named binding
961+
962+ :param binding: string. The name of a relation of extra-binding
963+ :return: string. The primary IP address for the named binding
964+ :raise: NotImplementedError if run on Juju < 2.0
965+ '''
966+ cmd = ['network-get', '--primary-address', binding]
967+ return subprocess.check_output(cmd).strip()
968
969=== modified file 'hooks/charmhelpers/core/host.py'
970--- hooks/charmhelpers/core/host.py 2015-04-29 13:21:29 +0000
971+++ hooks/charmhelpers/core/host.py 2016-03-17 11:00:44 +0000
972@@ -24,11 +24,14 @@
973 import os
974 import re
975 import pwd
976+import glob
977 import grp
978 import random
979 import string
980 import subprocess
981 import hashlib
982+import functools
983+import itertools
984 from contextlib import contextmanager
985 from collections import OrderedDict
986
987@@ -62,25 +65,86 @@
988 return service_result
989
990
991+def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
992+ """Pause a system service.
993+
994+ Stop it, and prevent it from starting again at boot."""
995+ stopped = True
996+ if service_running(service_name):
997+ stopped = service_stop(service_name)
998+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
999+ sysv_file = os.path.join(initd_dir, service_name)
1000+ if init_is_systemd():
1001+ service('disable', service_name)
1002+ elif os.path.exists(upstart_file):
1003+ override_path = os.path.join(
1004+ init_dir, '{}.override'.format(service_name))
1005+ with open(override_path, 'w') as fh:
1006+ fh.write("manual\n")
1007+ elif os.path.exists(sysv_file):
1008+ subprocess.check_call(["update-rc.d", service_name, "disable"])
1009+ else:
1010+ raise ValueError(
1011+ "Unable to detect {0} as SystemD, Upstart {1} or"
1012+ " SysV {2}".format(
1013+ service_name, upstart_file, sysv_file))
1014+ return stopped
1015+
1016+
1017+def service_resume(service_name, init_dir="/etc/init",
1018+ initd_dir="/etc/init.d"):
1019+ """Resume a system service.
1020+
1021+ Reenable starting again at boot. Start the service"""
1022+ upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
1023+ sysv_file = os.path.join(initd_dir, service_name)
1024+ if init_is_systemd():
1025+ service('enable', service_name)
1026+ elif os.path.exists(upstart_file):
1027+ override_path = os.path.join(
1028+ init_dir, '{}.override'.format(service_name))
1029+ if os.path.exists(override_path):
1030+ os.unlink(override_path)
1031+ elif os.path.exists(sysv_file):
1032+ subprocess.check_call(["update-rc.d", service_name, "enable"])
1033+ else:
1034+ raise ValueError(
1035+ "Unable to detect {0} as SystemD, Upstart {1} or"
1036+ " SysV {2}".format(
1037+ service_name, upstart_file, sysv_file))
1038+
1039+ started = service_running(service_name)
1040+ if not started:
1041+ started = service_start(service_name)
1042+ return started
1043+
1044+
1045 def service(action, service_name):
1046 """Control a system service"""
1047- cmd = ['service', service_name, action]
1048+ if init_is_systemd():
1049+ cmd = ['systemctl', action, service_name]
1050+ else:
1051+ cmd = ['service', service_name, action]
1052 return subprocess.call(cmd) == 0
1053
1054
1055-def service_running(service):
1056+def service_running(service_name):
1057 """Determine whether a system service is running"""
1058- try:
1059- output = subprocess.check_output(
1060- ['service', service, 'status'],
1061- stderr=subprocess.STDOUT).decode('UTF-8')
1062- except subprocess.CalledProcessError:
1063- return False
1064+ if init_is_systemd():
1065+ return service('is-active', service_name)
1066 else:
1067- if ("start/running" in output or "is running" in output):
1068- return True
1069- else:
1070+ try:
1071+ output = subprocess.check_output(
1072+ ['service', service_name, 'status'],
1073+ stderr=subprocess.STDOUT).decode('UTF-8')
1074+ except subprocess.CalledProcessError:
1075 return False
1076+ else:
1077+ if ("start/running" in output or "is running" in output or
1078+ "up and running" in output):
1079+ return True
1080+ else:
1081+ return False
1082
1083
1084 def service_available(service_name):
1085@@ -95,8 +159,29 @@
1086 return True
1087
1088
1089-def adduser(username, password=None, shell='/bin/bash', system_user=False):
1090- """Add a user to the system"""
1091+SYSTEMD_SYSTEM = '/run/systemd/system'
1092+
1093+
1094+def init_is_systemd():
1095+ """Return True if the host system uses systemd, False otherwise."""
1096+ return os.path.isdir(SYSTEMD_SYSTEM)
1097+
1098+
1099+def adduser(username, password=None, shell='/bin/bash', system_user=False,
1100+ primary_group=None, secondary_groups=None):
1101+ """Add a user to the system.
1102+
1103+ Will log but otherwise succeed if the user already exists.
1104+
1105+ :param str username: Username to create
1106+ :param str password: Password for user; if ``None``, create a system user
1107+ :param str shell: The default shell for the user
1108+ :param bool system_user: Whether to create a login or system user
1109+ :param str primary_group: Primary group for user; defaults to username
1110+ :param list secondary_groups: Optional list of additional groups
1111+
1112+ :returns: The password database entry struct, as returned by `pwd.getpwnam`
1113+ """
1114 try:
1115 user_info = pwd.getpwnam(username)
1116 log('user {0} already exists!'.format(username))
1117@@ -111,12 +196,32 @@
1118 '--shell', shell,
1119 '--password', password,
1120 ])
1121+ if not primary_group:
1122+ try:
1123+ grp.getgrnam(username)
1124+ primary_group = username # avoid "group exists" error
1125+ except KeyError:
1126+ pass
1127+ if primary_group:
1128+ cmd.extend(['-g', primary_group])
1129+ if secondary_groups:
1130+ cmd.extend(['-G', ','.join(secondary_groups)])
1131 cmd.append(username)
1132 subprocess.check_call(cmd)
1133 user_info = pwd.getpwnam(username)
1134 return user_info
1135
1136
1137+def user_exists(username):
1138+ """Check if a user exists"""
1139+ try:
1140+ pwd.getpwnam(username)
1141+ user_exists = True
1142+ except KeyError:
1143+ user_exists = False
1144+ return user_exists
1145+
1146+
1147 def add_group(group_name, system_group=False):
1148 """Add a group to the system"""
1149 try:
1150@@ -139,11 +244,7 @@
1151
1152 def add_user_to_group(username, group):
1153 """Add a user to a group"""
1154- cmd = [
1155- 'gpasswd', '-a',
1156- username,
1157- group
1158- ]
1159+ cmd = ['gpasswd', '-a', username, group]
1160 log("Adding user {} to group {}".format(username, group))
1161 subprocess.check_call(cmd)
1162
1163@@ -202,14 +303,12 @@
1164
1165
1166 def fstab_remove(mp):
1167- """Remove the given mountpoint entry from /etc/fstab
1168- """
1169+ """Remove the given mountpoint entry from /etc/fstab"""
1170 return Fstab.remove_by_mountpoint(mp)
1171
1172
1173 def fstab_add(dev, mp, fs, options=None):
1174- """Adds the given device entry to the /etc/fstab file
1175- """
1176+ """Adds the given device entry to the /etc/fstab file"""
1177 return Fstab.add(dev, mp, fs, options=options)
1178
1179
1180@@ -253,9 +352,19 @@
1181 return system_mounts
1182
1183
1184+def fstab_mount(mountpoint):
1185+ """Mount filesystem using fstab"""
1186+ cmd_args = ['mount', mountpoint]
1187+ try:
1188+ subprocess.check_output(cmd_args)
1189+ except subprocess.CalledProcessError as e:
1190+ log('Error unmounting {}\n{}'.format(mountpoint, e.output))
1191+ return False
1192+ return True
1193+
1194+
1195 def file_hash(path, hash_type='md5'):
1196- """
1197- Generate a hash checksum of the contents of 'path' or None if not found.
1198+ """Generate a hash checksum of the contents of 'path' or None if not found.
1199
1200 :param str hash_type: Any hash alrgorithm supported by :mod:`hashlib`,
1201 such as md5, sha1, sha256, sha512, etc.
1202@@ -269,9 +378,22 @@
1203 return None
1204
1205
1206+def path_hash(path):
1207+ """Generate a hash checksum of all files matching 'path'. Standard
1208+ wildcards like '*' and '?' are supported, see documentation for the 'glob'
1209+ module for more information.
1210+
1211+ :return: dict: A { filename: hash } dictionary for all matched files.
1212+ Empty if none found.
1213+ """
1214+ return {
1215+ filename: file_hash(filename)
1216+ for filename in glob.iglob(path)
1217+ }
1218+
1219+
1220 def check_hash(path, checksum, hash_type='md5'):
1221- """
1222- Validate a file using a cryptographic checksum.
1223+ """Validate a file using a cryptographic checksum.
1224
1225 :param str checksum: Value of the checksum used to validate the file.
1226 :param str hash_type: Hash algorithm used to generate `checksum`.
1227@@ -286,6 +408,7 @@
1228
1229
1230 class ChecksumError(ValueError):
1231+ """A class derived from Value error to indicate the checksum failed."""
1232 pass
1233
1234
1235@@ -296,36 +419,58 @@
1236
1237 @restart_on_change({
1238 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
1239+ '/etc/apache/sites-enabled/*': [ 'apache2' ]
1240 })
1241- def ceph_client_changed():
1242+ def config_changed():
1243 pass # your code here
1244
1245 In this example, the cinder-api and cinder-volume services
1246 would be restarted if /etc/ceph/ceph.conf is changed by the
1247- ceph_client_changed function.
1248+ ceph_client_changed function. The apache2 service would be
1249+ restarted if any file matching the pattern got changed, created
1250+ or removed. Standard wildcards are supported, see documentation
1251+ for the 'glob' module for more information.
1252+
1253+ @param restart_map: {path_file_name: [service_name, ...]
1254+ @param stopstart: DEFAULT false; whether to stop, start OR restart
1255+ @returns result from decorated function
1256 """
1257 def wrap(f):
1258+ @functools.wraps(f)
1259 def wrapped_f(*args, **kwargs):
1260- checksums = {}
1261- for path in restart_map:
1262- checksums[path] = file_hash(path)
1263- f(*args, **kwargs)
1264- restarts = []
1265- for path in restart_map:
1266- if checksums[path] != file_hash(path):
1267- restarts += restart_map[path]
1268- services_list = list(OrderedDict.fromkeys(restarts))
1269- if not stopstart:
1270- for service_name in services_list:
1271- service('restart', service_name)
1272- else:
1273- for action in ['stop', 'start']:
1274- for service_name in services_list:
1275- service(action, service_name)
1276+ return restart_on_change_helper(
1277+ (lambda: f(*args, **kwargs)), restart_map, stopstart)
1278 return wrapped_f
1279 return wrap
1280
1281
1282+def restart_on_change_helper(lambda_f, restart_map, stopstart=False):
1283+ """Helper function to perform the restart_on_change function.
1284+
1285+ This is provided for decorators to restart services if files described
1286+ in the restart_map have changed after an invocation of lambda_f().
1287+
1288+ @param lambda_f: function to call.
1289+ @param restart_map: {file: [service, ...]}
1290+ @param stopstart: whether to stop, start or restart a service
1291+ @returns result of lambda_f()
1292+ """
1293+ checksums = {path: path_hash(path) for path in restart_map}
1294+ r = lambda_f()
1295+ # create a list of lists of the services to restart
1296+ restarts = [restart_map[path]
1297+ for path in restart_map
1298+ if path_hash(path) != checksums[path]]
1299+ # create a flat list of ordered services without duplicates from lists
1300+ services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
1301+ if services_list:
1302+ actions = ('stop', 'start') if stopstart else ('restart',)
1303+ for action in actions:
1304+ for service_name in services_list:
1305+ service(action, service_name)
1306+ return r
1307+
1308+
1309 def lsb_release():
1310 """Return /etc/lsb-release in a dict"""
1311 d = {}
1312@@ -352,36 +497,92 @@
1313 return(''.join(random_chars))
1314
1315
1316-def list_nics(nic_type):
1317- '''Return a list of nics of given type(s)'''
1318+def is_phy_iface(interface):
1319+ """Returns True if interface is not virtual, otherwise False."""
1320+ if interface:
1321+ sys_net = '/sys/class/net'
1322+ if os.path.isdir(sys_net):
1323+ for iface in glob.glob(os.path.join(sys_net, '*')):
1324+ if '/virtual/' in os.path.realpath(iface):
1325+ continue
1326+
1327+ if interface == os.path.basename(iface):
1328+ return True
1329+
1330+ return False
1331+
1332+
1333+def get_bond_master(interface):
1334+ """Returns bond master if interface is bond slave otherwise None.
1335+
1336+ NOTE: the provided interface is expected to be physical
1337+ """
1338+ if interface:
1339+ iface_path = '/sys/class/net/%s' % (interface)
1340+ if os.path.exists(iface_path):
1341+ if '/virtual/' in os.path.realpath(iface_path):
1342+ return None
1343+
1344+ master = os.path.join(iface_path, 'master')
1345+ if os.path.exists(master):
1346+ master = os.path.realpath(master)
1347+ # make sure it is a bond master
1348+ if os.path.exists(os.path.join(master, 'bonding')):
1349+ return os.path.basename(master)
1350+
1351+ return None
1352+
1353+
1354+def list_nics(nic_type=None):
1355+ """Return a list of nics of given type(s)"""
1356 if isinstance(nic_type, six.string_types):
1357 int_types = [nic_type]
1358 else:
1359 int_types = nic_type
1360+
1361 interfaces = []
1362- for int_type in int_types:
1363- cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
1364+ if nic_type:
1365+ for int_type in int_types:
1366+ cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
1367+ ip_output = subprocess.check_output(cmd).decode('UTF-8')
1368+ ip_output = ip_output.split('\n')
1369+ ip_output = (line for line in ip_output if line)
1370+ for line in ip_output:
1371+ if line.split()[1].startswith(int_type):
1372+ matched = re.search('.*: (' + int_type +
1373+ r'[0-9]+\.[0-9]+)@.*', line)
1374+ if matched:
1375+ iface = matched.groups()[0]
1376+ else:
1377+ iface = line.split()[1].replace(":", "")
1378+
1379+ if iface not in interfaces:
1380+ interfaces.append(iface)
1381+ else:
1382+ cmd = ['ip', 'a']
1383 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
1384- ip_output = (line for line in ip_output if line)
1385+ ip_output = (line.strip() for line in ip_output if line)
1386+
1387+ key = re.compile('^[0-9]+:\s+(.+):')
1388 for line in ip_output:
1389- if line.split()[1].startswith(int_type):
1390- matched = re.search('.*: (' + int_type + r'[0-9]+\.[0-9]+)@.*', line)
1391- if matched:
1392- interface = matched.groups()[0]
1393- else:
1394- interface = line.split()[1].replace(":", "")
1395- interfaces.append(interface)
1396+ matched = re.search(key, line)
1397+ if matched:
1398+ iface = matched.group(1)
1399+ iface = iface.partition("@")[0]
1400+ if iface not in interfaces:
1401+ interfaces.append(iface)
1402
1403 return interfaces
1404
1405
1406 def set_nic_mtu(nic, mtu):
1407- '''Set MTU on a network interface'''
1408+ """Set the Maximum Transmission Unit (MTU) on a network interface."""
1409 cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
1410 subprocess.check_call(cmd)
1411
1412
1413 def get_nic_mtu(nic):
1414+ """Return the Maximum Transmission Unit (MTU) for a network interface."""
1415 cmd = ['ip', 'addr', 'show', nic]
1416 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')
1417 mtu = ""
1418@@ -393,6 +594,7 @@
1419
1420
1421 def get_nic_hwaddr(nic):
1422+ """Return the Media Access Control (MAC) for a network interface."""
1423 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
1424 ip_output = subprocess.check_output(cmd).decode('UTF-8')
1425 hwaddr = ""
1426@@ -403,7 +605,7 @@
1427
1428
1429 def cmp_pkgrevno(package, revno, pkgcache=None):
1430- '''Compare supplied revno with the revno of the installed package
1431+ """Compare supplied revno with the revno of the installed package
1432
1433 * 1 => Installed revno is greater than supplied arg
1434 * 0 => Installed revno is the same as supplied arg
1435@@ -412,7 +614,7 @@
1436 This function imports apt_cache function from charmhelpers.fetch if
1437 the pkgcache argument is None. Be sure to add charmhelpers.fetch if
1438 you call this function, or pass an apt_pkg.Cache() instance.
1439- '''
1440+ """
1441 import apt_pkg
1442 if not pkgcache:
1443 from charmhelpers.fetch import apt_cache
1444@@ -422,15 +624,30 @@
1445
1446
1447 @contextmanager
1448-def chdir(d):
1449+def chdir(directory):
1450+ """Change the current working directory to a different directory for a code
1451+ block and return the previous directory after the block exits. Useful to
1452+ run commands from a specificed directory.
1453+
1454+ :param str directory: The directory path to change to for this context.
1455+ """
1456 cur = os.getcwd()
1457 try:
1458- yield os.chdir(d)
1459+ yield os.chdir(directory)
1460 finally:
1461 os.chdir(cur)
1462
1463
1464-def chownr(path, owner, group, follow_links=True):
1465+def chownr(path, owner, group, follow_links=True, chowntopdir=False):
1466+ """Recursively change user and group ownership of files and directories
1467+ in given path. Doesn't chown path itself by default, only its children.
1468+
1469+ :param str path: The string path to start changing ownership.
1470+ :param str owner: The owner string to use when looking up the uid.
1471+ :param str group: The group string to use when looking up the gid.
1472+ :param bool follow_links: Also Chown links if True
1473+ :param bool chowntopdir: Also chown path itself if True
1474+ """
1475 uid = pwd.getpwnam(owner).pw_uid
1476 gid = grp.getgrnam(group).gr_gid
1477 if follow_links:
1478@@ -438,6 +655,10 @@
1479 else:
1480 chown = os.lchown
1481
1482+ if chowntopdir:
1483+ broken_symlink = os.path.lexists(path) and not os.path.exists(path)
1484+ if not broken_symlink:
1485+ chown(path, uid, gid)
1486 for root, dirs, files in os.walk(path):
1487 for name in dirs + files:
1488 full = os.path.join(root, name)
1489@@ -447,4 +668,28 @@
1490
1491
1492 def lchownr(path, owner, group):
1493+ """Recursively change user and group ownership of files and directories
1494+ in a given path, not following symbolic links. See the documentation for
1495+ 'os.lchown' for more information.
1496+
1497+ :param str path: The string path to start changing ownership.
1498+ :param str owner: The owner string to use when looking up the uid.
1499+ :param str group: The group string to use when looking up the gid.
1500+ """
1501 chownr(path, owner, group, follow_links=False)
1502+
1503+
1504+def get_total_ram():
1505+ """The total amount of system RAM in bytes.
1506+
1507+ This is what is reported by the OS, and may be overcommitted when
1508+ there are multiple containers hosted on the same machine.
1509+ """
1510+ with open('/proc/meminfo', 'r') as f:
1511+ for line in f.readlines():
1512+ if line:
1513+ key, value, unit = line.split()
1514+ if key == 'MemTotal:':
1515+ assert unit == 'kB', 'Unknown unit'
1516+ return int(value) * 1024 # Classic, not KiB.
1517+ raise NotImplementedError()
1518
1519=== added file 'hooks/charmhelpers/core/hugepage.py'
1520--- hooks/charmhelpers/core/hugepage.py 1970-01-01 00:00:00 +0000
1521+++ hooks/charmhelpers/core/hugepage.py 2016-03-17 11:00:44 +0000
1522@@ -0,0 +1,71 @@
1523+# -*- coding: utf-8 -*-
1524+
1525+# Copyright 2014-2015 Canonical Limited.
1526+#
1527+# This file is part of charm-helpers.
1528+#
1529+# charm-helpers is free software: you can redistribute it and/or modify
1530+# it under the terms of the GNU Lesser General Public License version 3 as
1531+# published by the Free Software Foundation.
1532+#
1533+# charm-helpers is distributed in the hope that it will be useful,
1534+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1535+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1536+# GNU Lesser General Public License for more details.
1537+#
1538+# You should have received a copy of the GNU Lesser General Public License
1539+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1540+
1541+import yaml
1542+from charmhelpers.core import fstab
1543+from charmhelpers.core import sysctl
1544+from charmhelpers.core.host import (
1545+ add_group,
1546+ add_user_to_group,
1547+ fstab_mount,
1548+ mkdir,
1549+)
1550+from charmhelpers.core.strutils import bytes_from_string
1551+from subprocess import check_output
1552+
1553+
1554+def hugepage_support(user, group='hugetlb', nr_hugepages=256,
1555+ max_map_count=65536, mnt_point='/run/hugepages/kvm',
1556+ pagesize='2MB', mount=True, set_shmmax=False):
1557+ """Enable hugepages on system.
1558+
1559+ Args:
1560+ user (str) -- Username to allow access to hugepages to
1561+ group (str) -- Group name to own hugepages
1562+ nr_hugepages (int) -- Number of pages to reserve
1563+ max_map_count (int) -- Number of Virtual Memory Areas a process can own
1564+ mnt_point (str) -- Directory to mount hugepages on
1565+ pagesize (str) -- Size of hugepages
1566+ mount (bool) -- Whether to Mount hugepages
1567+ """
1568+ group_info = add_group(group)
1569+ gid = group_info.gr_gid
1570+ add_user_to_group(user, group)
1571+ if max_map_count < 2 * nr_hugepages:
1572+ max_map_count = 2 * nr_hugepages
1573+ sysctl_settings = {
1574+ 'vm.nr_hugepages': nr_hugepages,
1575+ 'vm.max_map_count': max_map_count,
1576+ 'vm.hugetlb_shm_group': gid,
1577+ }
1578+ if set_shmmax:
1579+ shmmax_current = int(check_output(['sysctl', '-n', 'kernel.shmmax']))
1580+ shmmax_minsize = bytes_from_string(pagesize) * nr_hugepages
1581+ if shmmax_minsize > shmmax_current:
1582+ sysctl_settings['kernel.shmmax'] = shmmax_minsize
1583+ sysctl.create(yaml.dump(sysctl_settings), '/etc/sysctl.d/10-hugepage.conf')
1584+ mkdir(mnt_point, owner='root', group='root', perms=0o755, force=False)
1585+ lfstab = fstab.Fstab()
1586+ fstab_entry = lfstab.get_entry_by_attr('mountpoint', mnt_point)
1587+ if fstab_entry:
1588+ lfstab.remove_entry(fstab_entry)
1589+ entry = lfstab.Entry('nodev', mnt_point, 'hugetlbfs',
1590+ 'mode=1770,gid={},pagesize={}'.format(gid, pagesize), 0, 0)
1591+ lfstab.add_entry(entry)
1592+ if mount:
1593+ fstab_mount(mnt_point)
1594
1595=== added file 'hooks/charmhelpers/core/kernel.py'
1596--- hooks/charmhelpers/core/kernel.py 1970-01-01 00:00:00 +0000
1597+++ hooks/charmhelpers/core/kernel.py 2016-03-17 11:00:44 +0000
1598@@ -0,0 +1,68 @@
1599+#!/usr/bin/env python
1600+# -*- coding: utf-8 -*-
1601+
1602+# Copyright 2014-2015 Canonical Limited.
1603+#
1604+# This file is part of charm-helpers.
1605+#
1606+# charm-helpers is free software: you can redistribute it and/or modify
1607+# it under the terms of the GNU Lesser General Public License version 3 as
1608+# published by the Free Software Foundation.
1609+#
1610+# charm-helpers is distributed in the hope that it will be useful,
1611+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1612+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1613+# GNU Lesser General Public License for more details.
1614+#
1615+# You should have received a copy of the GNU Lesser General Public License
1616+# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1617+
1618+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
1619+
1620+from charmhelpers.core.hookenv import (
1621+ log,
1622+ INFO
1623+)
1624+
1625+from subprocess import check_call, check_output
1626+import re
1627+
1628+
1629+def modprobe(module, persist=True):
1630+ """Load a kernel module and configure for auto-load on reboot."""
1631+ cmd = ['modprobe', module]
1632+
1633+ log('Loading kernel module %s' % module, level=INFO)
1634+
1635+ check_call(cmd)
1636+ if persist:
1637+ with open('/etc/modules', 'r+') as modules:
1638+ if module not in modules.read():
1639+ modules.write(module)
1640+
1641+
1642+def rmmod(module, force=False):
1643+ """Remove a module from the linux kernel"""
1644+ cmd = ['rmmod']
1645+ if force:
1646+ cmd.append('-f')
1647+ cmd.append(module)
1648+ log('Removing kernel module %s' % module, level=INFO)
1649+ return check_call(cmd)
1650+
1651+
1652+def lsmod():
1653+ """Shows what kernel modules are currently loaded"""
1654+ return check_output(['lsmod'],
1655+ universal_newlines=True)
1656+
1657+
1658+def is_module_loaded(module):
1659+ """Checks if a kernel module is already loaded"""
1660+ matches = re.findall('^%s[ ]+' % module, lsmod(), re.M)
1661+ return len(matches) > 0
1662+
1663+
1664+def update_initramfs(version='all'):
1665+ """Updates an initramfs image"""
1666+ return check_call(["update-initramfs", "-k", version, "-u"])
1667
1668=== modified file 'hooks/charmhelpers/core/services/helpers.py'
1669--- hooks/charmhelpers/core/services/helpers.py 2015-04-03 15:42:21 +0000
1670+++ hooks/charmhelpers/core/services/helpers.py 2016-03-17 11:00:44 +0000
1671@@ -16,7 +16,9 @@
1672
1673 import os
1674 import yaml
1675+
1676 from charmhelpers.core import hookenv
1677+from charmhelpers.core import host
1678 from charmhelpers.core import templating
1679
1680 from charmhelpers.core.services.base import ManagerCallback
1681@@ -239,28 +241,51 @@
1682 action.
1683
1684 :param str source: The template source file, relative to
1685- `$CHARM_DIR/templates`
1686+ `$CHARM_DIR/templates`
1687
1688- :param str target: The target to write the rendered template to
1689+ :param str target: The target to write the rendered template to (or None)
1690 :param str owner: The owner of the rendered file
1691 :param str group: The group of the rendered file
1692 :param int perms: The permissions of the rendered file
1693+ :param partial on_change_action: functools partial to be executed when
1694+ rendered file changes
1695+ :param jinja2 loader template_loader: A jinja2 template loader
1696+
1697+ :return str: The rendered template
1698 """
1699 def __init__(self, source, target,
1700- owner='root', group='root', perms=0o444):
1701+ owner='root', group='root', perms=0o444,
1702+ on_change_action=None, template_loader=None):
1703 self.source = source
1704 self.target = target
1705 self.owner = owner
1706 self.group = group
1707 self.perms = perms
1708+ self.on_change_action = on_change_action
1709+ self.template_loader = template_loader
1710
1711 def __call__(self, manager, service_name, event_name):
1712+ pre_checksum = ''
1713+ if self.on_change_action and os.path.isfile(self.target):
1714+ pre_checksum = host.file_hash(self.target)
1715 service = manager.get_service(service_name)
1716- context = {}
1717+ context = {'ctx': {}}
1718 for ctx in service.get('required_data', []):
1719 context.update(ctx)
1720- templating.render(self.source, self.target, context,
1721- self.owner, self.group, self.perms)
1722+ context['ctx'].update(ctx)
1723+
1724+ result = templating.render(self.source, self.target, context,
1725+ self.owner, self.group, self.perms,
1726+ template_loader=self.template_loader)
1727+ if self.on_change_action:
1728+ if pre_checksum == host.file_hash(self.target):
1729+ hookenv.log(
1730+ 'No change detected: {}'.format(self.target),
1731+ hookenv.DEBUG)
1732+ else:
1733+ self.on_change_action()
1734+
1735+ return result
1736
1737
1738 # Convenience aliases for templates
1739
1740=== modified file 'hooks/charmhelpers/core/strutils.py'
1741--- hooks/charmhelpers/core/strutils.py 2015-04-29 13:21:29 +0000
1742+++ hooks/charmhelpers/core/strutils.py 2016-03-17 11:00:44 +0000
1743@@ -18,6 +18,7 @@
1744 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1745
1746 import six
1747+import re
1748
1749
1750 def bool_from_string(value):
1751@@ -40,3 +41,32 @@
1752
1753 msg = "Unable to interpret string value '%s' as boolean" % (value)
1754 raise ValueError(msg)
1755+
1756+
1757+def bytes_from_string(value):
1758+ """Interpret human readable string value as bytes.
1759+
1760+ Returns int
1761+ """
1762+ BYTE_POWER = {
1763+ 'K': 1,
1764+ 'KB': 1,
1765+ 'M': 2,
1766+ 'MB': 2,
1767+ 'G': 3,
1768+ 'GB': 3,
1769+ 'T': 4,
1770+ 'TB': 4,
1771+ 'P': 5,
1772+ 'PB': 5,
1773+ }
1774+ if isinstance(value, six.string_types):
1775+ value = six.text_type(value)
1776+ else:
1777+ msg = "Unable to interpret non-string value '%s' as boolean" % (value)
1778+ raise ValueError(msg)
1779+ matches = re.match("([0-9]+)([a-zA-Z]+)", value)
1780+ if not matches:
1781+ msg = "Unable to interpret string value '%s' as bytes" % (value)
1782+ raise ValueError(msg)
1783+ return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
1784
1785=== modified file 'hooks/charmhelpers/core/templating.py'
1786--- hooks/charmhelpers/core/templating.py 2015-02-18 14:24:32 +0000
1787+++ hooks/charmhelpers/core/templating.py 2016-03-17 11:00:44 +0000
1788@@ -21,13 +21,14 @@
1789
1790
1791 def render(source, target, context, owner='root', group='root',
1792- perms=0o444, templates_dir=None, encoding='UTF-8'):
1793+ perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None):
1794 """
1795 Render a template.
1796
1797 The `source` path, if not absolute, is relative to the `templates_dir`.
1798
1799- The `target` path should be absolute.
1800+ The `target` path should be absolute. It can also be `None`, in which
1801+ case no file will be written.
1802
1803 The context should be a dict containing the values to be replaced in the
1804 template.
1805@@ -36,6 +37,9 @@
1806
1807 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
1808
1809+ The rendered template will be written to the file as well as being returned
1810+ as a string.
1811+
1812 Note: Using this requires python-jinja2; if it is not installed, calling
1813 this will attempt to use charmhelpers.fetch.apt_install to install it.
1814 """
1815@@ -52,17 +56,26 @@
1816 apt_install('python-jinja2', fatal=True)
1817 from jinja2 import FileSystemLoader, Environment, exceptions
1818
1819- if templates_dir is None:
1820- templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
1821- loader = Environment(loader=FileSystemLoader(templates_dir))
1822+ if template_loader:
1823+ template_env = Environment(loader=template_loader)
1824+ else:
1825+ if templates_dir is None:
1826+ templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
1827+ template_env = Environment(loader=FileSystemLoader(templates_dir))
1828 try:
1829 source = source
1830- template = loader.get_template(source)
1831+ template = template_env.get_template(source)
1832 except exceptions.TemplateNotFound as e:
1833 hookenv.log('Could not load template %s from %s.' %
1834 (source, templates_dir),
1835 level=hookenv.ERROR)
1836 raise e
1837 content = template.render(context)
1838- host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
1839- host.write_file(target, content.encode(encoding), owner, group, perms)
1840+ if target is not None:
1841+ target_dir = os.path.dirname(target)
1842+ if not os.path.exists(target_dir):
1843+ # This is a terrible default directory permission, as the file
1844+ # or its siblings will often contain secrets.
1845+ host.mkdir(os.path.dirname(target), owner, group, perms=0o755)
1846+ host.write_file(target, content.encode(encoding), owner, group, perms)
1847+ return content
1848
1849=== modified file 'hooks/charmhelpers/core/unitdata.py'
1850--- hooks/charmhelpers/core/unitdata.py 2015-04-03 15:42:21 +0000
1851+++ hooks/charmhelpers/core/unitdata.py 2016-03-17 11:00:44 +0000
1852@@ -152,6 +152,7 @@
1853 import collections
1854 import contextlib
1855 import datetime
1856+import itertools
1857 import json
1858 import os
1859 import pprint
1860@@ -164,8 +165,7 @@
1861 class Storage(object):
1862 """Simple key value database for local unit state within charms.
1863
1864- Modifications are automatically committed at hook exit. That's
1865- currently regardless of exit code.
1866+ Modifications are not persisted unless :meth:`flush` is called.
1867
1868 To support dicts, lists, integer, floats, and booleans values
1869 are automatically json encoded/decoded.
1870@@ -173,8 +173,11 @@
1871 def __init__(self, path=None):
1872 self.db_path = path
1873 if path is None:
1874- self.db_path = os.path.join(
1875- os.environ.get('CHARM_DIR', ''), '.unit-state.db')
1876+ if 'UNIT_STATE_DB' in os.environ:
1877+ self.db_path = os.environ['UNIT_STATE_DB']
1878+ else:
1879+ self.db_path = os.path.join(
1880+ os.environ.get('CHARM_DIR', ''), '.unit-state.db')
1881 self.conn = sqlite3.connect('%s' % self.db_path)
1882 self.cursor = self.conn.cursor()
1883 self.revision = None
1884@@ -189,15 +192,8 @@
1885 self.conn.close()
1886 self._closed = True
1887
1888- def _scoped_query(self, stmt, params=None):
1889- if params is None:
1890- params = []
1891- return stmt, params
1892-
1893 def get(self, key, default=None, record=False):
1894- self.cursor.execute(
1895- *self._scoped_query(
1896- 'select data from kv where key=?', [key]))
1897+ self.cursor.execute('select data from kv where key=?', [key])
1898 result = self.cursor.fetchone()
1899 if not result:
1900 return default
1901@@ -206,33 +202,81 @@
1902 return json.loads(result[0])
1903
1904 def getrange(self, key_prefix, strip=False):
1905- stmt = "select key, data from kv where key like '%s%%'" % key_prefix
1906- self.cursor.execute(*self._scoped_query(stmt))
1907+ """
1908+ Get a range of keys starting with a common prefix as a mapping of
1909+ keys to values.
1910+
1911+ :param str key_prefix: Common prefix among all keys
1912+ :param bool strip: Optionally strip the common prefix from the key
1913+ names in the returned dict
1914+ :return dict: A (possibly empty) dict of key-value mappings
1915+ """
1916+ self.cursor.execute("select key, data from kv where key like ?",
1917+ ['%s%%' % key_prefix])
1918 result = self.cursor.fetchall()
1919
1920 if not result:
1921- return None
1922+ return {}
1923 if not strip:
1924 key_prefix = ''
1925 return dict([
1926 (k[len(key_prefix):], json.loads(v)) for k, v in result])
1927
1928 def update(self, mapping, prefix=""):
1929+ """
1930+ Set the values of multiple keys at once.
1931+
1932+ :param dict mapping: Mapping of keys to values
1933+ :param str prefix: Optional prefix to apply to all keys in `mapping`
1934+ before setting
1935+ """
1936 for k, v in mapping.items():
1937 self.set("%s%s" % (prefix, k), v)
1938
1939 def unset(self, key):
1940+ """
1941+ Remove a key from the database entirely.
1942+ """
1943 self.cursor.execute('delete from kv where key=?', [key])
1944 if self.revision and self.cursor.rowcount:
1945 self.cursor.execute(
1946 'insert into kv_revisions values (?, ?, ?)',
1947 [key, self.revision, json.dumps('DELETED')])
1948
1949+ def unsetrange(self, keys=None, prefix=""):
1950+ """
1951+ Remove a range of keys starting with a common prefix, from the database
1952+ entirely.
1953+
1954+ :param list keys: List of keys to remove.
1955+ :param str prefix: Optional prefix to apply to all keys in ``keys``
1956+ before removing.
1957+ """
1958+ if keys is not None:
1959+ keys = ['%s%s' % (prefix, key) for key in keys]
1960+ self.cursor.execute('delete from kv where key in (%s)' % ','.join(['?'] * len(keys)), keys)
1961+ if self.revision and self.cursor.rowcount:
1962+ self.cursor.execute(
1963+ 'insert into kv_revisions values %s' % ','.join(['(?, ?, ?)'] * len(keys)),
1964+ list(itertools.chain.from_iterable((key, self.revision, json.dumps('DELETED')) for key in keys)))
1965+ else:
1966+ self.cursor.execute('delete from kv where key like ?',
1967+ ['%s%%' % prefix])
1968+ if self.revision and self.cursor.rowcount:
1969+ self.cursor.execute(
1970+ 'insert into kv_revisions values (?, ?, ?)',
1971+ ['%s%%' % prefix, self.revision, json.dumps('DELETED')])
1972+
1973 def set(self, key, value):
1974+ """
1975+ Set a value in the database.
1976+
1977+ :param str key: Key to set the value for
1978+ :param value: Any JSON-serializable value to be set
1979+ """
1980 serialized = json.dumps(value)
1981
1982- self.cursor.execute(
1983- 'select data from kv where key=?', [key])
1984+ self.cursor.execute('select data from kv where key=?', [key])
1985 exists = self.cursor.fetchone()
1986
1987 # Skip mutations to the same value
1988
1989=== modified file 'hooks/charmhelpers/fetch/__init__.py'
1990--- hooks/charmhelpers/fetch/__init__.py 2015-01-26 13:07:31 +0000
1991+++ hooks/charmhelpers/fetch/__init__.py 2016-03-17 11:00:44 +0000
1992@@ -90,6 +90,22 @@
1993 'kilo/proposed': 'trusty-proposed/kilo',
1994 'trusty-kilo/proposed': 'trusty-proposed/kilo',
1995 'trusty-proposed/kilo': 'trusty-proposed/kilo',
1996+ # Liberty
1997+ 'liberty': 'trusty-updates/liberty',
1998+ 'trusty-liberty': 'trusty-updates/liberty',
1999+ 'trusty-liberty/updates': 'trusty-updates/liberty',
2000+ 'trusty-updates/liberty': 'trusty-updates/liberty',
2001+ 'liberty/proposed': 'trusty-proposed/liberty',
2002+ 'trusty-liberty/proposed': 'trusty-proposed/liberty',
2003+ 'trusty-proposed/liberty': 'trusty-proposed/liberty',
2004+ # Mitaka
2005+ 'mitaka': 'trusty-updates/mitaka',
2006+ 'trusty-mitaka': 'trusty-updates/mitaka',
2007+ 'trusty-mitaka/updates': 'trusty-updates/mitaka',
2008+ 'trusty-updates/mitaka': 'trusty-updates/mitaka',
2009+ 'mitaka/proposed': 'trusty-proposed/mitaka',
2010+ 'trusty-mitaka/proposed': 'trusty-proposed/mitaka',
2011+ 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
2012 }
2013
2014 # The order of this list is very important. Handlers should be listed in from
2015@@ -215,19 +231,27 @@
2016 _run_apt_command(cmd, fatal)
2017
2018
2019+def apt_mark(packages, mark, fatal=False):
2020+ """Flag one or more packages using apt-mark"""
2021+ log("Marking {} as {}".format(packages, mark))
2022+ cmd = ['apt-mark', mark]
2023+ if isinstance(packages, six.string_types):
2024+ cmd.append(packages)
2025+ else:
2026+ cmd.extend(packages)
2027+
2028+ if fatal:
2029+ subprocess.check_call(cmd, universal_newlines=True)
2030+ else:
2031+ subprocess.call(cmd, universal_newlines=True)
2032+
2033+
2034 def apt_hold(packages, fatal=False):
2035- """Hold one or more packages"""
2036- cmd = ['apt-mark', 'hold']
2037- if isinstance(packages, six.string_types):
2038- cmd.append(packages)
2039- else:
2040- cmd.extend(packages)
2041- log("Holding {}".format(packages))
2042-
2043- if fatal:
2044- subprocess.check_call(cmd)
2045- else:
2046- subprocess.call(cmd)
2047+ return apt_mark(packages, 'hold', fatal=fatal)
2048+
2049+
2050+def apt_unhold(packages, fatal=False):
2051+ return apt_mark(packages, 'unhold', fatal=fatal)
2052
2053
2054 def add_source(source, key=None):
2055@@ -370,8 +394,9 @@
2056 for handler in handlers:
2057 try:
2058 installed_to = handler.install(source, *args, **kwargs)
2059- except UnhandledSource:
2060- pass
2061+ except UnhandledSource as e:
2062+ log('Install source attempt unsuccessful: {}'.format(e),
2063+ level='WARNING')
2064 if not installed_to:
2065 raise UnhandledSource("No handler found for source {}".format(source))
2066 return installed_to
2067@@ -394,7 +419,7 @@
2068 importlib.import_module(package),
2069 classname)
2070 plugin_list.append(handler_class())
2071- except (ImportError, AttributeError):
2072+ except NotImplementedError:
2073 # Skip missing plugins so that they can be ommitted from
2074 # installation if desired
2075 log("FetchHandler {} not found, skipping plugin".format(
2076
2077=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
2078--- hooks/charmhelpers/fetch/archiveurl.py 2015-02-18 14:24:32 +0000
2079+++ hooks/charmhelpers/fetch/archiveurl.py 2016-03-17 11:00:44 +0000
2080@@ -77,6 +77,8 @@
2081 def can_handle(self, source):
2082 url_parts = self.parse_url(source)
2083 if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
2084+ # XXX: Why is this returning a boolean and a string? It's
2085+ # doomed to fail since "bool(can_handle('foo://'))" will be True.
2086 return "Wrong source type"
2087 if get_archive_handler(self.base_url(source)):
2088 return True
2089@@ -106,7 +108,7 @@
2090 install_opener(opener)
2091 response = urlopen(source)
2092 try:
2093- with open(dest, 'w') as dest_file:
2094+ with open(dest, 'wb') as dest_file:
2095 dest_file.write(response.read())
2096 except Exception as e:
2097 if os.path.isfile(dest):
2098@@ -155,7 +157,11 @@
2099 else:
2100 algorithms = hashlib.algorithms_available
2101 if key in algorithms:
2102- check_hash(dld_file, value, key)
2103+ if len(value) != 1:
2104+ raise TypeError(
2105+ "Expected 1 hash value, not %d" % len(value))
2106+ expected = value[0]
2107+ check_hash(dld_file, expected, key)
2108 if checksum:
2109 check_hash(dld_file, checksum, hash_type)
2110 return extract(dld_file, dest)
2111
2112=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
2113--- hooks/charmhelpers/fetch/bzrurl.py 2015-01-26 13:07:31 +0000
2114+++ hooks/charmhelpers/fetch/bzrurl.py 2016-03-17 11:00:44 +0000
2115@@ -15,60 +15,50 @@
2116 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2117
2118 import os
2119+from subprocess import check_call
2120 from charmhelpers.fetch import (
2121 BaseFetchHandler,
2122- UnhandledSource
2123+ UnhandledSource,
2124+ filter_installed_packages,
2125+ apt_install,
2126 )
2127 from charmhelpers.core.host import mkdir
2128
2129-import six
2130-if six.PY3:
2131- raise ImportError('bzrlib does not support Python3')
2132
2133-try:
2134- from bzrlib.branch import Branch
2135- from bzrlib import bzrdir, workingtree, errors
2136-except ImportError:
2137- from charmhelpers.fetch import apt_install
2138- apt_install("python-bzrlib")
2139- from bzrlib.branch import Branch
2140- from bzrlib import bzrdir, workingtree, errors
2141+if filter_installed_packages(['bzr']) != []:
2142+ apt_install(['bzr'])
2143+ if filter_installed_packages(['bzr']) != []:
2144+ raise NotImplementedError('Unable to install bzr')
2145
2146
2147 class BzrUrlFetchHandler(BaseFetchHandler):
2148 """Handler for bazaar branches via generic and lp URLs"""
2149 def can_handle(self, source):
2150 url_parts = self.parse_url(source)
2151- if url_parts.scheme not in ('bzr+ssh', 'lp'):
2152+ if url_parts.scheme not in ('bzr+ssh', 'lp', ''):
2153 return False
2154+ elif not url_parts.scheme:
2155+ return os.path.exists(os.path.join(source, '.bzr'))
2156 else:
2157 return True
2158
2159 def branch(self, source, dest):
2160- url_parts = self.parse_url(source)
2161- # If we use lp:branchname scheme we need to load plugins
2162 if not self.can_handle(source):
2163 raise UnhandledSource("Cannot handle {}".format(source))
2164- if url_parts.scheme == "lp":
2165- from bzrlib.plugin import load_plugins
2166- load_plugins()
2167- try:
2168- local_branch = bzrdir.BzrDir.create_branch_convenience(dest)
2169- except errors.AlreadyControlDirError:
2170- local_branch = Branch.open(dest)
2171- try:
2172- remote_branch = Branch.open(source)
2173- remote_branch.push(local_branch)
2174- tree = workingtree.WorkingTree.open(dest)
2175- tree.update()
2176- except Exception as e:
2177- raise e
2178+ if os.path.exists(dest):
2179+ check_call(['bzr', 'pull', '--overwrite', '-d', dest, source])
2180+ else:
2181+ check_call(['bzr', 'branch', source, dest])
2182
2183- def install(self, source):
2184+ def install(self, source, dest=None):
2185 url_parts = self.parse_url(source)
2186 branch_name = url_parts.path.strip("/").split("/")[-1]
2187- dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
2188- branch_name)
2189+ if dest:
2190+ dest_dir = os.path.join(dest, branch_name)
2191+ else:
2192+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
2193+ branch_name)
2194+
2195 if not os.path.exists(dest_dir):
2196 mkdir(dest_dir, perms=0o755)
2197 try:
2198
2199=== modified file 'hooks/charmhelpers/fetch/giturl.py'
2200--- hooks/charmhelpers/fetch/giturl.py 2015-06-08 08:24:59 +0000
2201+++ hooks/charmhelpers/fetch/giturl.py 2016-03-17 11:00:44 +0000
2202@@ -15,24 +15,18 @@
2203 # along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
2204
2205 import os
2206+from subprocess import check_call, CalledProcessError
2207 from charmhelpers.fetch import (
2208 BaseFetchHandler,
2209- UnhandledSource
2210+ UnhandledSource,
2211+ filter_installed_packages,
2212+ apt_install,
2213 )
2214-from charmhelpers.core.host import mkdir
2215-
2216-import six
2217-if six.PY3:
2218- raise ImportError('GitPython does not support Python 3')
2219-
2220-try:
2221- from git import Repo
2222-except ImportError:
2223- from charmhelpers.fetch import apt_install
2224- apt_install("python-git")
2225- from git import Repo
2226-
2227-from git.exc import GitCommandError # noqa E402
2228+
2229+if filter_installed_packages(['git']) != []:
2230+ apt_install(['git'])
2231+ if filter_installed_packages(['git']) != []:
2232+ raise NotImplementedError('Unable to install git')
2233
2234
2235 class GitUrlFetchHandler(BaseFetchHandler):
2236@@ -40,19 +34,24 @@
2237 def can_handle(self, source):
2238 url_parts = self.parse_url(source)
2239 # TODO (mattyw) no support for ssh git@ yet
2240- if url_parts.scheme not in ('http', 'https', 'git'):
2241+ if url_parts.scheme not in ('http', 'https', 'git', ''):
2242 return False
2243+ elif not url_parts.scheme:
2244+ return os.path.exists(os.path.join(source, '.git'))
2245 else:
2246 return True
2247
2248- def clone(self, source, dest, branch, depth=None):
2249+ def clone(self, source, dest, branch="master", depth=None):
2250 if not self.can_handle(source):
2251 raise UnhandledSource("Cannot handle {}".format(source))
2252
2253- if depth:
2254- Repo.clone_from(source, dest, branch=branch, depth=depth)
2255+ if os.path.exists(dest):
2256+ cmd = ['git', '-C', dest, 'pull', source, branch]
2257 else:
2258- Repo.clone_from(source, dest, branch=branch)
2259+ cmd = ['git', 'clone', source, dest, '--branch', branch]
2260+ if depth:
2261+ cmd.extend(['--depth', depth])
2262+ check_call(cmd)
2263
2264 def install(self, source, branch="master", dest=None, depth=None):
2265 url_parts = self.parse_url(source)
2266@@ -62,12 +61,10 @@
2267 else:
2268 dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
2269 branch_name)
2270- if not os.path.exists(dest_dir):
2271- mkdir(dest_dir, perms=0o755)
2272 try:
2273 self.clone(source, dest_dir, branch, depth)
2274- except GitCommandError as e:
2275- raise UnhandledSource(e.message)
2276+ except CalledProcessError as e:
2277+ raise UnhandledSource(e)
2278 except OSError as e:
2279 raise UnhandledSource(e.strerror)
2280 return dest_dir
2281
2282=== modified file 'hooks/helpers.py'
2283--- hooks/helpers.py 2016-02-26 16:47:03 +0000
2284+++ hooks/helpers.py 2016-03-17 11:00:44 +0000
2285@@ -824,7 +824,7 @@
2286 @logged
2287 def set_auth_keyspace_replication(session, settings):
2288 # Live operation, so keep status the same.
2289- status_set(hookenv.status_get(),
2290+ status_set(hookenv.status_get()[0],
2291 'Updating system_auth rf to {!r}'.format(settings))
2292 statement = 'ALTER KEYSPACE system_auth WITH REPLICATION = %s'
2293 query(session, statement, ConsistencyLevel.ALL, (settings,))
2294@@ -835,7 +835,7 @@
2295 # Repair takes a long time, and may need to be retried due to 'snapshot
2296 # creation' errors, but should certainly complete within an hour since
2297 # the keyspace is tiny.
2298- status_set(hookenv.status_get(),
2299+ status_set(hookenv.status_get()[0],
2300 'Repairing system_auth keyspace')
2301 nodetool('repair', 'system_auth', timeout=3600)
2302
2303
2304=== modified file 'tests/test_actions.py'
2305--- tests/test_actions.py 2016-02-26 16:47:03 +0000
2306+++ tests/test_actions.py 2016-03-17 11:00:44 +0000
2307@@ -1095,7 +1095,7 @@
2308 @patch('charmhelpers.core.hookenv.is_leader')
2309 def test_set_active(self, is_leader, status_get, status_set, seed_ips):
2310 is_leader.return_value = False
2311- status_get.return_value = 'waiting'
2312+ status_get.return_value = ('waiting', '')
2313 seed_ips.return_value = set()
2314 actions.set_active('')
2315 status_set.assert_called_once_with('active', 'Live node')
2316@@ -1107,7 +1107,7 @@
2317 def test_set_active_seed(self, is_leader,
2318 status_get, status_set, seed_ips):
2319 is_leader.return_value = False
2320- status_get.return_value = 'waiting'
2321+ status_get.return_value = ('waiting', '')
2322 seed_ips.return_value = set([hookenv.unit_private_ip()])
2323 actions.set_active('')
2324 status_set.assert_called_once_with('active', 'Live seed')
2325@@ -1121,6 +1121,7 @@
2326 def test_set_active_service(self, is_leader,
2327 status_get, status_set, service_status_set,
2328 seed_ips, num_nodes):
2329+ status_get.return_value = ('waiting', '')
2330 is_leader.return_value = True
2331 seed_ips.return_value = set([hookenv.unit_private_ip()])
2332 num_nodes.return_value = 1
2333
2334=== modified file 'tests/test_helpers.py'
2335--- tests/test_helpers.py 2016-02-26 16:47:03 +0000
2336+++ tests/test_helpers.py 2016-03-17 11:00:44 +0000
2337@@ -1339,7 +1339,7 @@
2338 @patch('helpers.query')
2339 def test_set_auth_keyspace_replication(self, query,
2340 status_get, status_set):
2341- status_get.return_value = 'active'
2342+ status_get.return_value = ('active', '')
2343 settings = dict(json=True)
2344 helpers.set_auth_keyspace_replication(sentinel.session, settings)
2345 query.assert_called_once_with(sentinel.session,
2346@@ -1351,7 +1351,7 @@
2347 @patch('charmhelpers.core.hookenv.status_get')
2348 @patch('helpers.nodetool')
2349 def test_repair_auth_keyspace(self, nodetool, status_get, status_set):
2350- status_get.return_value = sentinel.status
2351+ status_get.return_value = (sentinel.status, '')
2352 helpers.repair_auth_keyspace()
2353 status_set.assert_called_once_with(sentinel.status,
2354 'Repairing system_auth keyspace')

Subscribers

People subscribed via source and target branches

to all changes: