Merge lp:~stub/charms/trusty/cassandra/bug-1557769-update-charmhelpers into lp:charms/trusty/cassandra
- Trusty Tahr (14.04)
- bug-1557769-update-charmhelpers
- Merge into trunk
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Adam Israel (community) | Approve | ||
Review Queue (community) | automated testing | Approve | |
Review via email: mp+289346@code.launchpad.net |
Commit message
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 : | # |
review:
Approve
(automated testing)
Revision history for this message
Review Queue (review-queue) wrote : | # |
The results (PASS) are in and available here: http://
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') |
The results (PASS) are in and available here: http:// juju-ci. vapour. ws:8080/ job/charm- bundle- test-aws/ 3209/