Merge lp:~stub/charms/precise/postgresql/charm-helpers into lp:charms/postgresql

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
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Approve
Review via email: mp+221852@code.launchpad.net

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.
Revision history for this message
Stuart Bishop (stub) wrote :

Noop review

review: Approve
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:

Subscribers

People subscribed via source and target branches