Merge lp:~stub/charms/precise/postgresql/charm-helpers into lp:charms/postgresql
- Precise Pangolin (12.04)
- charm-helpers
- Merge into trunk
Proposed by
Stuart Bishop
Status: | Merged |
---|---|
Approved by: | Stuart Bishop |
Approved revision: | 94 |
Merged at revision: | 99 |
Proposed branch: | lp:~stub/charms/precise/postgresql/charm-helpers |
Merge into: | lp:charms/postgresql |
Prerequisite: | lp:~stub/charms/precise/postgresql/trunk-dev |
Diff against target: |
677 lines (+381/-89) 7 files modified
charm-helpers.yaml (+2/-1) config.yaml (+2/-2) hooks/charmhelpers/core/fstab.py (+114/-0) hooks/charmhelpers/core/hookenv.py (+98/-1) hooks/charmhelpers/core/host.py (+34/-6) hooks/charmhelpers/fetch/__init__.py (+129/-78) hooks/charmhelpers/fetch/bzrurl.py (+2/-1) |
To merge this branch: | bzr merge lp:~stub/charms/precise/postgresql/charm-helpers |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Stuart Bishop (community) | Approve | ||
Review via email: mp+221852@code.launchpad.net |
Commit message
Description of the change
Update charm-helpers from trunk.
Fix a default in config.yaml to work with updated charm-helpers.
To post a comment you must log in.
- 95. By Stuart Bishop
-
Update charm-helpers, from bugfix branch
- 96. By Stuart Bishop
-
Add new charmhelpers file
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'charm-helpers.yaml' |
2 | --- charm-helpers.yaml 2013-10-10 10:02:24 +0000 |
3 | +++ charm-helpers.yaml 2014-06-05 12:35:09 +0000 |
4 | @@ -1,5 +1,6 @@ |
5 | destination: hooks/charmhelpers |
6 | -branch: lp:charm-helpers |
7 | +#branch: lp:charm-helpers |
8 | +branch: lp:~stub/charm-helpers/fix-configure_sources |
9 | include: |
10 | - core |
11 | - fetch |
12 | |
13 | === modified file 'config.yaml' |
14 | --- config.yaml 2014-05-29 13:08:35 +0000 |
15 | +++ config.yaml 2014-06-05 12:35:09 +0000 |
16 | @@ -358,13 +358,13 @@ |
17 | List of extra package sources, per charm-helpers standard. |
18 | YAML format. |
19 | type: string |
20 | - default: '' |
21 | + default: null |
22 | install_keys: |
23 | description: | |
24 | List of signing keys for install_sources package sources, per |
25 | charmhelpers standard. YAML format. |
26 | type: string |
27 | - default: '' |
28 | + default: null |
29 | extra_archives: |
30 | default: "" |
31 | type: string |
32 | |
33 | === added file 'hooks/charmhelpers/core/fstab.py' |
34 | --- hooks/charmhelpers/core/fstab.py 1970-01-01 00:00:00 +0000 |
35 | +++ hooks/charmhelpers/core/fstab.py 2014-06-05 12:35:09 +0000 |
36 | @@ -0,0 +1,114 @@ |
37 | +#!/usr/bin/env python |
38 | +# -*- coding: utf-8 -*- |
39 | + |
40 | +__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>' |
41 | + |
42 | +import os |
43 | + |
44 | + |
45 | +class Fstab(file): |
46 | + """This class extends file in order to implement a file reader/writer |
47 | + for file `/etc/fstab` |
48 | + """ |
49 | + |
50 | + class Entry(object): |
51 | + """Entry class represents a non-comment line on the `/etc/fstab` file |
52 | + """ |
53 | + def __init__(self, device, mountpoint, filesystem, |
54 | + options, d=0, p=0): |
55 | + self.device = device |
56 | + self.mountpoint = mountpoint |
57 | + self.filesystem = filesystem |
58 | + |
59 | + if not options: |
60 | + options = "defaults" |
61 | + |
62 | + self.options = options |
63 | + self.d = d |
64 | + self.p = p |
65 | + |
66 | + def __eq__(self, o): |
67 | + return str(self) == str(o) |
68 | + |
69 | + def __str__(self): |
70 | + return "{} {} {} {} {} {}".format(self.device, |
71 | + self.mountpoint, |
72 | + self.filesystem, |
73 | + self.options, |
74 | + self.d, |
75 | + self.p) |
76 | + |
77 | + DEFAULT_PATH = os.path.join(os.path.sep, 'etc', 'fstab') |
78 | + |
79 | + def __init__(self, path=None): |
80 | + if path: |
81 | + self._path = path |
82 | + else: |
83 | + self._path = self.DEFAULT_PATH |
84 | + file.__init__(self, self._path, 'r+') |
85 | + |
86 | + def _hydrate_entry(self, line): |
87 | + return Fstab.Entry(*filter( |
88 | + lambda x: x not in ('', None), |
89 | + line.strip("\n").split(" "))) |
90 | + |
91 | + @property |
92 | + def entries(self): |
93 | + self.seek(0) |
94 | + for line in self.readlines(): |
95 | + try: |
96 | + if not line.startswith("#"): |
97 | + yield self._hydrate_entry(line) |
98 | + except ValueError: |
99 | + pass |
100 | + |
101 | + def get_entry_by_attr(self, attr, value): |
102 | + for entry in self.entries: |
103 | + e_attr = getattr(entry, attr) |
104 | + if e_attr == value: |
105 | + return entry |
106 | + return None |
107 | + |
108 | + def add_entry(self, entry): |
109 | + if self.get_entry_by_attr('device', entry.device): |
110 | + return False |
111 | + |
112 | + self.write(str(entry) + '\n') |
113 | + self.truncate() |
114 | + return entry |
115 | + |
116 | + def remove_entry(self, entry): |
117 | + self.seek(0) |
118 | + |
119 | + lines = self.readlines() |
120 | + |
121 | + found = False |
122 | + for index, line in enumerate(lines): |
123 | + if not line.startswith("#"): |
124 | + if self._hydrate_entry(line) == entry: |
125 | + found = True |
126 | + break |
127 | + |
128 | + if not found: |
129 | + return False |
130 | + |
131 | + lines.remove(line) |
132 | + |
133 | + self.seek(0) |
134 | + self.write(''.join(lines)) |
135 | + self.truncate() |
136 | + return True |
137 | + |
138 | + @classmethod |
139 | + def remove_by_mountpoint(cls, mountpoint, path=None): |
140 | + fstab = cls(path=path) |
141 | + entry = fstab.get_entry_by_attr('mountpoint', mountpoint) |
142 | + if entry: |
143 | + return fstab.remove_entry(entry) |
144 | + return False |
145 | + |
146 | + @classmethod |
147 | + def add(cls, device, mountpoint, filesystem, options=None, path=None): |
148 | + return cls(path=path).add_entry(Fstab.Entry(device, |
149 | + mountpoint, filesystem, |
150 | + options=options)) |
151 | |
152 | === modified file 'hooks/charmhelpers/core/hookenv.py' |
153 | --- hooks/charmhelpers/core/hookenv.py 2014-03-31 12:37:26 +0000 |
154 | +++ hooks/charmhelpers/core/hookenv.py 2014-06-05 12:35:09 +0000 |
155 | @@ -155,6 +155,100 @@ |
156 | return os.path.basename(sys.argv[0]) |
157 | |
158 | |
159 | +class Config(dict): |
160 | + """A Juju charm config dictionary that can write itself to |
161 | + disk (as json) and track which values have changed since |
162 | + the previous hook invocation. |
163 | + |
164 | + Do not instantiate this object directly - instead call |
165 | + ``hookenv.config()`` |
166 | + |
167 | + Example usage:: |
168 | + |
169 | + >>> # inside a hook |
170 | + >>> from charmhelpers.core import hookenv |
171 | + >>> config = hookenv.config() |
172 | + >>> config['foo'] |
173 | + 'bar' |
174 | + >>> config['mykey'] = 'myval' |
175 | + >>> config.save() |
176 | + |
177 | + |
178 | + >>> # user runs `juju set mycharm foo=baz` |
179 | + >>> # now we're inside subsequent config-changed hook |
180 | + >>> config = hookenv.config() |
181 | + >>> config['foo'] |
182 | + 'baz' |
183 | + >>> # test to see if this val has changed since last hook |
184 | + >>> config.changed('foo') |
185 | + True |
186 | + >>> # what was the previous value? |
187 | + >>> config.previous('foo') |
188 | + 'bar' |
189 | + >>> # keys/values that we add are preserved across hooks |
190 | + >>> config['mykey'] |
191 | + 'myval' |
192 | + >>> # don't forget to save at the end of hook! |
193 | + >>> config.save() |
194 | + |
195 | + """ |
196 | + CONFIG_FILE_NAME = '.juju-persistent-config' |
197 | + |
198 | + def __init__(self, *args, **kw): |
199 | + super(Config, self).__init__(*args, **kw) |
200 | + self._prev_dict = None |
201 | + self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME) |
202 | + if os.path.exists(self.path): |
203 | + self.load_previous() |
204 | + |
205 | + def load_previous(self, path=None): |
206 | + """Load previous copy of config from disk so that current values |
207 | + can be compared to previous values. |
208 | + |
209 | + :param path: |
210 | + |
211 | + File path from which to load the previous config. If `None`, |
212 | + config is loaded from the default location. If `path` is |
213 | + specified, subsequent `save()` calls will write to the same |
214 | + path. |
215 | + |
216 | + """ |
217 | + self.path = path or self.path |
218 | + with open(self.path) as f: |
219 | + self._prev_dict = json.load(f) |
220 | + |
221 | + def changed(self, key): |
222 | + """Return true if the value for this key has changed since |
223 | + the last save. |
224 | + |
225 | + """ |
226 | + if self._prev_dict is None: |
227 | + return True |
228 | + return self.previous(key) != self.get(key) |
229 | + |
230 | + def previous(self, key): |
231 | + """Return previous value for this key, or None if there |
232 | + is no "previous" value. |
233 | + |
234 | + """ |
235 | + if self._prev_dict: |
236 | + return self._prev_dict.get(key) |
237 | + return None |
238 | + |
239 | + def save(self): |
240 | + """Save this config to disk. |
241 | + |
242 | + Preserves items in _prev_dict that do not exist in self. |
243 | + |
244 | + """ |
245 | + if self._prev_dict: |
246 | + for k, v in self._prev_dict.iteritems(): |
247 | + if k not in self: |
248 | + self[k] = v |
249 | + with open(self.path, 'w') as f: |
250 | + json.dump(self, f) |
251 | + |
252 | + |
253 | @cached |
254 | def config(scope=None): |
255 | """Juju charm configuration""" |
256 | @@ -163,7 +257,10 @@ |
257 | config_cmd_line.append(scope) |
258 | config_cmd_line.append('--format=json') |
259 | try: |
260 | - return json.loads(subprocess.check_output(config_cmd_line)) |
261 | + config_data = json.loads(subprocess.check_output(config_cmd_line)) |
262 | + if scope is not None: |
263 | + return config_data |
264 | + return Config(config_data) |
265 | except ValueError: |
266 | return None |
267 | |
268 | |
269 | === modified file 'hooks/charmhelpers/core/host.py' |
270 | --- hooks/charmhelpers/core/host.py 2014-03-31 12:37:26 +0000 |
271 | +++ hooks/charmhelpers/core/host.py 2014-06-05 12:35:09 +0000 |
272 | @@ -12,10 +12,12 @@ |
273 | import string |
274 | import subprocess |
275 | import hashlib |
276 | +import apt_pkg |
277 | |
278 | from collections import OrderedDict |
279 | |
280 | from hookenv import log |
281 | +from fstab import Fstab |
282 | |
283 | |
284 | def service_start(service_name): |
285 | @@ -34,7 +36,8 @@ |
286 | |
287 | |
288 | def service_reload(service_name, restart_on_failure=False): |
289 | - """Reload a system service, optionally falling back to restart if reload fails""" |
290 | + """Reload a system service, optionally falling back to restart if |
291 | + reload fails""" |
292 | service_result = service('reload', service_name) |
293 | if not service_result and restart_on_failure: |
294 | service_result = service('restart', service_name) |
295 | @@ -143,7 +146,19 @@ |
296 | target.write(content) |
297 | |
298 | |
299 | -def mount(device, mountpoint, options=None, persist=False): |
300 | +def fstab_remove(mp): |
301 | + """Remove the given mountpoint entry from /etc/fstab |
302 | + """ |
303 | + return Fstab.remove_by_mountpoint(mp) |
304 | + |
305 | + |
306 | +def fstab_add(dev, mp, fs, options=None): |
307 | + """Adds the given device entry to the /etc/fstab file |
308 | + """ |
309 | + return Fstab.add(dev, mp, fs, options=options) |
310 | + |
311 | + |
312 | +def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"): |
313 | """Mount a filesystem at a particular mountpoint""" |
314 | cmd_args = ['mount'] |
315 | if options is not None: |
316 | @@ -154,9 +169,9 @@ |
317 | except subprocess.CalledProcessError, e: |
318 | log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output)) |
319 | return False |
320 | + |
321 | if persist: |
322 | - # TODO: update fstab |
323 | - pass |
324 | + return fstab_add(device, mountpoint, filesystem, options=options) |
325 | return True |
326 | |
327 | |
328 | @@ -168,9 +183,9 @@ |
329 | except subprocess.CalledProcessError, e: |
330 | log('Error unmounting {}\n{}'.format(mountpoint, e.output)) |
331 | return False |
332 | + |
333 | if persist: |
334 | - # TODO: update fstab |
335 | - pass |
336 | + return fstab_remove(mountpoint) |
337 | return True |
338 | |
339 | |
340 | @@ -295,3 +310,16 @@ |
341 | if 'link/ether' in words: |
342 | hwaddr = words[words.index('link/ether') + 1] |
343 | return hwaddr |
344 | + |
345 | + |
346 | +def cmp_pkgrevno(package, revno, pkgcache=None): |
347 | + '''Compare supplied revno with the revno of the installed package |
348 | + 1 => Installed revno is greater than supplied arg |
349 | + 0 => Installed revno is the same as supplied arg |
350 | + -1 => Installed revno is less than supplied arg |
351 | + ''' |
352 | + if not pkgcache: |
353 | + apt_pkg.init() |
354 | + pkgcache = apt_pkg.Cache() |
355 | + pkg = pkgcache[package] |
356 | + return apt_pkg.version_compare(pkg.current_ver.ver_str, revno) |
357 | |
358 | === modified file 'hooks/charmhelpers/fetch/__init__.py' |
359 | --- hooks/charmhelpers/fetch/__init__.py 2014-04-01 13:53:02 +0000 |
360 | +++ hooks/charmhelpers/fetch/__init__.py 2014-06-05 12:35:09 +0000 |
361 | @@ -1,4 +1,5 @@ |
362 | import importlib |
363 | +import time |
364 | from yaml import safe_load |
365 | from charmhelpers.core.host import ( |
366 | lsb_release |
367 | @@ -15,6 +16,7 @@ |
368 | import apt_pkg |
369 | import os |
370 | |
371 | + |
372 | CLOUD_ARCHIVE = """# Ubuntu Cloud Archive |
373 | deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main |
374 | """ |
375 | @@ -54,12 +56,73 @@ |
376 | 'icehouse/proposed': 'precise-proposed/icehouse', |
377 | 'precise-icehouse/proposed': 'precise-proposed/icehouse', |
378 | 'precise-proposed/icehouse': 'precise-proposed/icehouse', |
379 | + # Juno |
380 | + 'juno': 'trusty-updates/juno', |
381 | + 'trusty-juno': 'trusty-updates/juno', |
382 | + 'trusty-juno/updates': 'trusty-updates/juno', |
383 | + 'trusty-updates/juno': 'trusty-updates/juno', |
384 | + 'juno/proposed': 'trusty-proposed/juno', |
385 | + 'juno/proposed': 'trusty-proposed/juno', |
386 | + 'trusty-juno/proposed': 'trusty-proposed/juno', |
387 | + 'trusty-proposed/juno': 'trusty-proposed/juno', |
388 | } |
389 | |
390 | +# The order of this list is very important. Handlers should be listed in from |
391 | +# least- to most-specific URL matching. |
392 | +FETCH_HANDLERS = ( |
393 | + 'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler', |
394 | + 'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler', |
395 | +) |
396 | + |
397 | +APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT. |
398 | +APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks. |
399 | +APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times. |
400 | + |
401 | + |
402 | +class SourceConfigError(Exception): |
403 | + pass |
404 | + |
405 | + |
406 | +class UnhandledSource(Exception): |
407 | + pass |
408 | + |
409 | + |
410 | +class AptLockError(Exception): |
411 | + pass |
412 | + |
413 | + |
414 | +class BaseFetchHandler(object): |
415 | + |
416 | + """Base class for FetchHandler implementations in fetch plugins""" |
417 | + |
418 | + def can_handle(self, source): |
419 | + """Returns True if the source can be handled. Otherwise returns |
420 | + a string explaining why it cannot""" |
421 | + return "Wrong source type" |
422 | + |
423 | + def install(self, source): |
424 | + """Try to download and unpack the source. Return the path to the |
425 | + unpacked files or raise UnhandledSource.""" |
426 | + raise UnhandledSource("Wrong source type {}".format(source)) |
427 | + |
428 | + def parse_url(self, url): |
429 | + return urlparse(url) |
430 | + |
431 | + def base_url(self, url): |
432 | + """Return url without querystring or fragment""" |
433 | + parts = list(self.parse_url(url)) |
434 | + parts[4:] = ['' for i in parts[4:]] |
435 | + return urlunparse(parts) |
436 | + |
437 | |
438 | def filter_installed_packages(packages): |
439 | """Returns a list of packages that require installation""" |
440 | apt_pkg.init() |
441 | + |
442 | + # Tell apt to build an in-memory cache to prevent race conditions (if |
443 | + # another process is already building the cache). |
444 | + apt_pkg.config.set("Dir::Cache::pkgcache", "") |
445 | + |
446 | cache = apt_pkg.Cache() |
447 | _pkgs = [] |
448 | for package in packages: |
449 | @@ -87,14 +150,7 @@ |
450 | cmd.extend(packages) |
451 | log("Installing {} with options: {}".format(packages, |
452 | options)) |
453 | - env = os.environ.copy() |
454 | - if 'DEBIAN_FRONTEND' not in env: |
455 | - env['DEBIAN_FRONTEND'] = 'noninteractive' |
456 | - |
457 | - if fatal: |
458 | - subprocess.check_call(cmd, env=env) |
459 | - else: |
460 | - subprocess.call(cmd, env=env) |
461 | + _run_apt_command(cmd, fatal) |
462 | |
463 | |
464 | def apt_upgrade(options=None, fatal=False, dist=False): |
465 | @@ -109,24 +165,13 @@ |
466 | else: |
467 | cmd.append('upgrade') |
468 | log("Upgrading with options: {}".format(options)) |
469 | - |
470 | - env = os.environ.copy() |
471 | - if 'DEBIAN_FRONTEND' not in env: |
472 | - env['DEBIAN_FRONTEND'] = 'noninteractive' |
473 | - |
474 | - if fatal: |
475 | - subprocess.check_call(cmd, env=env) |
476 | - else: |
477 | - subprocess.call(cmd, env=env) |
478 | + _run_apt_command(cmd, fatal) |
479 | |
480 | |
481 | def apt_update(fatal=False): |
482 | """Update local apt cache""" |
483 | cmd = ['apt-get', 'update'] |
484 | - if fatal: |
485 | - subprocess.check_call(cmd) |
486 | - else: |
487 | - subprocess.call(cmd) |
488 | + _run_apt_command(cmd, fatal) |
489 | |
490 | |
491 | def apt_purge(packages, fatal=False): |
492 | @@ -137,10 +182,7 @@ |
493 | else: |
494 | cmd.extend(packages) |
495 | log("Purging {}".format(packages)) |
496 | - if fatal: |
497 | - subprocess.check_call(cmd) |
498 | - else: |
499 | - subprocess.call(cmd) |
500 | + _run_apt_command(cmd, fatal) |
501 | |
502 | |
503 | def apt_hold(packages, fatal=False): |
504 | @@ -151,6 +193,7 @@ |
505 | else: |
506 | cmd.extend(packages) |
507 | log("Holding {}".format(packages)) |
508 | + |
509 | if fatal: |
510 | subprocess.check_call(cmd) |
511 | else: |
512 | @@ -184,55 +227,50 @@ |
513 | apt.write(PROPOSED_POCKET.format(release)) |
514 | if key: |
515 | subprocess.check_call(['apt-key', 'adv', '--keyserver', |
516 | - 'keyserver.ubuntu.com', '--recv', |
517 | + 'hkp://keyserver.ubuntu.com:80', '--recv', |
518 | key]) |
519 | |
520 | |
521 | -class SourceConfigError(Exception): |
522 | - pass |
523 | - |
524 | - |
525 | def configure_sources(update=False, |
526 | sources_var='install_sources', |
527 | keys_var='install_keys'): |
528 | """ |
529 | - Configure multiple sources from charm configuration |
530 | + Configure multiple sources from charm configuration. |
531 | + |
532 | + The lists are encoded as yaml fragments in the configuration. |
533 | + The frament needs to be included as a string. |
534 | |
535 | Example config: |
536 | - install_sources: |
537 | + install_sources: | |
538 | - "ppa:foo" |
539 | - "http://example.com/repo precise main" |
540 | - install_keys: |
541 | + install_keys: | |
542 | - null |
543 | - "a1b2c3d4" |
544 | |
545 | Note that 'null' (a.k.a. None) should not be quoted. |
546 | """ |
547 | - sources = safe_load(config(sources_var) or '') or [] |
548 | - keys = safe_load(config(keys_var) or '') or [] |
549 | - if isinstance(sources, basestring) and ( |
550 | - keys is None or isinstance(keys, basestring)): |
551 | - add_source(sources, keys) |
552 | + sources = safe_load((config(sources_var) or '').strip()) or [] |
553 | + keys = safe_load((config(keys_var) or '').strip()) or None |
554 | + |
555 | + if isinstance(sources, basestring): |
556 | + sources = [sources] |
557 | + |
558 | + if keys is None: |
559 | + for source in sources: |
560 | + add_source(source, None) |
561 | else: |
562 | - if not len(sources) == len(keys): |
563 | - msg = 'Install sources and keys lists are different lengths' |
564 | - raise SourceConfigError(msg) |
565 | - for src_num in range(len(sources)): |
566 | - add_source(sources[src_num], keys[src_num]) |
567 | + if isinstance(keys, basestring): |
568 | + keys = [keys] |
569 | + |
570 | + if len(sources) != len(keys): |
571 | + raise SourceConfigError( |
572 | + 'Install sources and keys lists are different lengths') |
573 | + for source, key in zip(sources, keys): |
574 | + add_source(source, key) |
575 | if update: |
576 | apt_update(fatal=True) |
577 | |
578 | -# The order of this list is very important. Handlers should be listed in from |
579 | -# least- to most-specific URL matching. |
580 | -FETCH_HANDLERS = ( |
581 | - 'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler', |
582 | - 'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler', |
583 | -) |
584 | - |
585 | - |
586 | -class UnhandledSource(Exception): |
587 | - pass |
588 | - |
589 | |
590 | def install_remote(source): |
591 | """ |
592 | @@ -263,30 +301,6 @@ |
593 | return install_remote(source) |
594 | |
595 | |
596 | -class BaseFetchHandler(object): |
597 | - |
598 | - """Base class for FetchHandler implementations in fetch plugins""" |
599 | - |
600 | - def can_handle(self, source): |
601 | - """Returns True if the source can be handled. Otherwise returns |
602 | - a string explaining why it cannot""" |
603 | - return "Wrong source type" |
604 | - |
605 | - def install(self, source): |
606 | - """Try to download and unpack the source. Return the path to the |
607 | - unpacked files or raise UnhandledSource.""" |
608 | - raise UnhandledSource("Wrong source type {}".format(source)) |
609 | - |
610 | - def parse_url(self, url): |
611 | - return urlparse(url) |
612 | - |
613 | - def base_url(self, url): |
614 | - """Return url without querystring or fragment""" |
615 | - parts = list(self.parse_url(url)) |
616 | - parts[4:] = ['' for i in parts[4:]] |
617 | - return urlunparse(parts) |
618 | - |
619 | - |
620 | def plugins(fetch_handlers=None): |
621 | if not fetch_handlers: |
622 | fetch_handlers = FETCH_HANDLERS |
623 | @@ -304,3 +318,40 @@ |
624 | log("FetchHandler {} not found, skipping plugin".format( |
625 | handler_name)) |
626 | return plugin_list |
627 | + |
628 | + |
629 | +def _run_apt_command(cmd, fatal=False): |
630 | + """ |
631 | + Run an APT command, checking output and retrying if the fatal flag is set |
632 | + to True. |
633 | + |
634 | + :param: cmd: str: The apt command to run. |
635 | + :param: fatal: bool: Whether the command's output should be checked and |
636 | + retried. |
637 | + """ |
638 | + env = os.environ.copy() |
639 | + |
640 | + if 'DEBIAN_FRONTEND' not in env: |
641 | + env['DEBIAN_FRONTEND'] = 'noninteractive' |
642 | + |
643 | + if fatal: |
644 | + retry_count = 0 |
645 | + result = None |
646 | + |
647 | + # If the command is considered "fatal", we need to retry if the apt |
648 | + # lock was not acquired. |
649 | + |
650 | + while result is None or result == APT_NO_LOCK: |
651 | + try: |
652 | + result = subprocess.check_call(cmd, env=env) |
653 | + except subprocess.CalledProcessError, e: |
654 | + retry_count = retry_count + 1 |
655 | + if retry_count > APT_NO_LOCK_RETRY_COUNT: |
656 | + raise |
657 | + result = e.returncode |
658 | + log("Couldn't acquire DPKG lock. Will retry in {} seconds." |
659 | + "".format(APT_NO_LOCK_RETRY_DELAY)) |
660 | + time.sleep(APT_NO_LOCK_RETRY_DELAY) |
661 | + |
662 | + else: |
663 | + subprocess.call(cmd, env=env) |
664 | |
665 | === modified file 'hooks/charmhelpers/fetch/bzrurl.py' |
666 | --- hooks/charmhelpers/fetch/bzrurl.py 2014-03-31 12:37:26 +0000 |
667 | +++ hooks/charmhelpers/fetch/bzrurl.py 2014-06-05 12:35:09 +0000 |
668 | @@ -39,7 +39,8 @@ |
669 | def install(self, source): |
670 | url_parts = self.parse_url(source) |
671 | branch_name = url_parts.path.strip("/").split("/")[-1] |
672 | - dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", branch_name) |
673 | + dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", |
674 | + branch_name) |
675 | if not os.path.exists(dest_dir): |
676 | mkdir(dest_dir, perms=0755) |
677 | try: |
Noop review