Merge lp:~tribaal/charms/trusty/ntpmaster/use-charm-helpers-templating into lp:charms/ntpmaster

Proposed by Chris Glass
Status: Merged
Merged at revision: 10
Proposed branch: lp:~tribaal/charms/trusty/ntpmaster/use-charm-helpers-templating
Merge into: lp:charms/ntpmaster
Diff against target: 1337 lines (+668/-138)
10 files modified
charm-helpers-sync.yaml (+1/-0)
hooks/charmhelpers/contrib/templating/jinja.py (+23/-0)
hooks/charmhelpers/core/fstab.py (+116/-0)
hooks/charmhelpers/core/hookenv.py (+184/-25)
hooks/charmhelpers/core/host.py (+109/-19)
hooks/charmhelpers/fetch/__init__.py (+214/-66)
hooks/charmhelpers/fetch/archiveurl.py (+15/-0)
hooks/charmhelpers/fetch/bzrurl.py (+3/-2)
hooks/ntpmaster_hooks.py (+3/-5)
hooks/utils.py (+0/-21)
To merge this branch: bzr merge lp:~tribaal/charms/trusty/ntpmaster/use-charm-helpers-templating
Reviewer Review Type Date Requested Status
Matt Bruzek (community) Approve
Review via email: mp+229558@code.launchpad.net

Description of the change

This branch lets ntpmaster use the charmhelper version of jinja templating instead of maintaining its own.

To post a comment you must log in.
Revision history for this message
Matt Bruzek (mbruzek) wrote :

Hi Chris,

Thank you for this merge proposal to use the charm helpers. I deployed the ntpmaster charm and the /etc/ntp.conf looked as expected!

+1 LGTM

Thanks, again.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charm-helpers-sync.yaml'
2--- charm-helpers-sync.yaml 2013-08-29 18:39:36 +0000
3+++ charm-helpers-sync.yaml 2014-08-05 05:52:40 +0000
4@@ -3,3 +3,4 @@
5 include:
6 - core
7 - fetch
8+ - contrib.templating.jinja
9
10=== added directory 'hooks/charmhelpers/contrib'
11=== added file 'hooks/charmhelpers/contrib/__init__.py'
12=== added directory 'hooks/charmhelpers/contrib/templating'
13=== added file 'hooks/charmhelpers/contrib/templating/__init__.py'
14=== added file 'hooks/charmhelpers/contrib/templating/jinja.py'
15--- hooks/charmhelpers/contrib/templating/jinja.py 1970-01-01 00:00:00 +0000
16+++ hooks/charmhelpers/contrib/templating/jinja.py 2014-08-05 05:52:40 +0000
17@@ -0,0 +1,23 @@
18+"""
19+Templating using the python-jinja2 package.
20+"""
21+from charmhelpers.fetch import (
22+ apt_install,
23+)
24+
25+
26+DEFAULT_TEMPLATES_DIR = 'templates'
27+
28+
29+try:
30+ import jinja2
31+except ImportError:
32+ apt_install(["python-jinja2"])
33+ import jinja2
34+
35+
36+def render(template_name, context, template_dir=DEFAULT_TEMPLATES_DIR):
37+ templates = jinja2.Environment(
38+ loader=jinja2.FileSystemLoader(template_dir))
39+ template = templates.get_template(template_name)
40+ return template.render(context)
41
42=== added file 'hooks/charmhelpers/core/fstab.py'
43--- hooks/charmhelpers/core/fstab.py 1970-01-01 00:00:00 +0000
44+++ hooks/charmhelpers/core/fstab.py 2014-08-05 05:52:40 +0000
45@@ -0,0 +1,116 @@
46+#!/usr/bin/env python
47+# -*- coding: utf-8 -*-
48+
49+__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
50+
51+import os
52+
53+
54+class Fstab(file):
55+ """This class extends file in order to implement a file reader/writer
56+ for file `/etc/fstab`
57+ """
58+
59+ class Entry(object):
60+ """Entry class represents a non-comment line on the `/etc/fstab` file
61+ """
62+ def __init__(self, device, mountpoint, filesystem,
63+ options, d=0, p=0):
64+ self.device = device
65+ self.mountpoint = mountpoint
66+ self.filesystem = filesystem
67+
68+ if not options:
69+ options = "defaults"
70+
71+ self.options = options
72+ self.d = d
73+ self.p = p
74+
75+ def __eq__(self, o):
76+ return str(self) == str(o)
77+
78+ def __str__(self):
79+ return "{} {} {} {} {} {}".format(self.device,
80+ self.mountpoint,
81+ self.filesystem,
82+ self.options,
83+ self.d,
84+ self.p)
85+
86+ DEFAULT_PATH = os.path.join(os.path.sep, 'etc', 'fstab')
87+
88+ def __init__(self, path=None):
89+ if path:
90+ self._path = path
91+ else:
92+ self._path = self.DEFAULT_PATH
93+ file.__init__(self, self._path, 'r+')
94+
95+ def _hydrate_entry(self, line):
96+ # NOTE: use split with no arguments to split on any
97+ # whitespace including tabs
98+ return Fstab.Entry(*filter(
99+ lambda x: x not in ('', None),
100+ line.strip("\n").split()))
101+
102+ @property
103+ def entries(self):
104+ self.seek(0)
105+ for line in self.readlines():
106+ try:
107+ if not line.startswith("#"):
108+ yield self._hydrate_entry(line)
109+ except ValueError:
110+ pass
111+
112+ def get_entry_by_attr(self, attr, value):
113+ for entry in self.entries:
114+ e_attr = getattr(entry, attr)
115+ if e_attr == value:
116+ return entry
117+ return None
118+
119+ def add_entry(self, entry):
120+ if self.get_entry_by_attr('device', entry.device):
121+ return False
122+
123+ self.write(str(entry) + '\n')
124+ self.truncate()
125+ return entry
126+
127+ def remove_entry(self, entry):
128+ self.seek(0)
129+
130+ lines = self.readlines()
131+
132+ found = False
133+ for index, line in enumerate(lines):
134+ if not line.startswith("#"):
135+ if self._hydrate_entry(line) == entry:
136+ found = True
137+ break
138+
139+ if not found:
140+ return False
141+
142+ lines.remove(line)
143+
144+ self.seek(0)
145+ self.write(''.join(lines))
146+ self.truncate()
147+ return True
148+
149+ @classmethod
150+ def remove_by_mountpoint(cls, mountpoint, path=None):
151+ fstab = cls(path=path)
152+ entry = fstab.get_entry_by_attr('mountpoint', mountpoint)
153+ if entry:
154+ return fstab.remove_entry(entry)
155+ return False
156+
157+ @classmethod
158+ def add(cls, device, mountpoint, filesystem, options=None, path=None):
159+ return cls(path=path).add_entry(Fstab.Entry(device,
160+ mountpoint, filesystem,
161+ options=options))
162
163=== modified file 'hooks/charmhelpers/core/hookenv.py'
164--- hooks/charmhelpers/core/hookenv.py 2013-08-29 18:39:36 +0000
165+++ hooks/charmhelpers/core/hookenv.py 2014-08-05 05:52:40 +0000
166@@ -8,7 +8,9 @@
167 import json
168 import yaml
169 import subprocess
170+import sys
171 import UserDict
172+from subprocess import CalledProcessError
173
174 CRITICAL = "CRITICAL"
175 ERROR = "ERROR"
176@@ -21,9 +23,9 @@
177
178
179 def cached(func):
180- ''' Cache return values for multiple executions of func + args
181+ """Cache return values for multiple executions of func + args
182
183- For example:
184+ For example::
185
186 @cached
187 def unit_get(attribute):
188@@ -32,7 +34,7 @@
189 unit_get('test')
190
191 will cache the result of unit_get + 'test' for future calls.
192- '''
193+ """
194 def wrapper(*args, **kwargs):
195 global cache
196 key = str((func, args, kwargs))
197@@ -46,8 +48,8 @@
198
199
200 def flush(key):
201- ''' Flushes any entries from function cache where the
202- key is found in the function+args '''
203+ """Flushes any entries from function cache where the
204+ key is found in the function+args """
205 flush_list = []
206 for item in cache:
207 if key in item:
208@@ -57,7 +59,7 @@
209
210
211 def log(message, level=None):
212- "Write a message to the juju log"
213+ """Write a message to the juju log"""
214 command = ['juju-log']
215 if level:
216 command += ['-l', level]
217@@ -66,7 +68,7 @@
218
219
220 class Serializable(UserDict.IterableUserDict):
221- "Wrapper, an object that can be serialized to yaml or json"
222+ """Wrapper, an object that can be serialized to yaml or json"""
223
224 def __init__(self, obj):
225 # wrap the object
226@@ -96,11 +98,11 @@
227 self.data = state
228
229 def json(self):
230- "Serialize the object to json"
231+ """Serialize the object to json"""
232 return json.dumps(self.data)
233
234 def yaml(self):
235- "Serialize the object to yaml"
236+ """Serialize the object to yaml"""
237 return yaml.dump(self.data)
238
239
240@@ -119,50 +121,153 @@
241
242
243 def in_relation_hook():
244- "Determine whether we're running in a relation hook"
245+ """Determine whether we're running in a relation hook"""
246 return 'JUJU_RELATION' in os.environ
247
248
249 def relation_type():
250- "The scope for the current relation hook"
251+ """The scope for the current relation hook"""
252 return os.environ.get('JUJU_RELATION', None)
253
254
255 def relation_id():
256- "The relation ID for the current relation hook"
257+ """The relation ID for the current relation hook"""
258 return os.environ.get('JUJU_RELATION_ID', None)
259
260
261 def local_unit():
262- "Local unit ID"
263+ """Local unit ID"""
264 return os.environ['JUJU_UNIT_NAME']
265
266
267 def remote_unit():
268- "The remote unit for the current relation hook"
269+ """The remote unit for the current relation hook"""
270 return os.environ['JUJU_REMOTE_UNIT']
271
272
273 def service_name():
274- "The name service group this unit belongs to"
275+ """The name service group this unit belongs to"""
276 return local_unit().split('/')[0]
277
278
279+def hook_name():
280+ """The name of the currently executing hook"""
281+ return os.path.basename(sys.argv[0])
282+
283+
284+class Config(dict):
285+ """A Juju charm config dictionary that can write itself to
286+ disk (as json) and track which values have changed since
287+ the previous hook invocation.
288+
289+ Do not instantiate this object directly - instead call
290+ ``hookenv.config()``
291+
292+ Example usage::
293+
294+ >>> # inside a hook
295+ >>> from charmhelpers.core import hookenv
296+ >>> config = hookenv.config()
297+ >>> config['foo']
298+ 'bar'
299+ >>> config['mykey'] = 'myval'
300+ >>> config.save()
301+
302+
303+ >>> # user runs `juju set mycharm foo=baz`
304+ >>> # now we're inside subsequent config-changed hook
305+ >>> config = hookenv.config()
306+ >>> config['foo']
307+ 'baz'
308+ >>> # test to see if this val has changed since last hook
309+ >>> config.changed('foo')
310+ True
311+ >>> # what was the previous value?
312+ >>> config.previous('foo')
313+ 'bar'
314+ >>> # keys/values that we add are preserved across hooks
315+ >>> config['mykey']
316+ 'myval'
317+ >>> # don't forget to save at the end of hook!
318+ >>> config.save()
319+
320+ """
321+ CONFIG_FILE_NAME = '.juju-persistent-config'
322+
323+ def __init__(self, *args, **kw):
324+ super(Config, self).__init__(*args, **kw)
325+ self._prev_dict = None
326+ self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
327+ if os.path.exists(self.path):
328+ self.load_previous()
329+
330+ def load_previous(self, path=None):
331+ """Load previous copy of config from disk so that current values
332+ can be compared to previous values.
333+
334+ :param path:
335+
336+ File path from which to load the previous config. If `None`,
337+ config is loaded from the default location. If `path` is
338+ specified, subsequent `save()` calls will write to the same
339+ path.
340+
341+ """
342+ self.path = path or self.path
343+ with open(self.path) as f:
344+ self._prev_dict = json.load(f)
345+
346+ def changed(self, key):
347+ """Return true if the value for this key has changed since
348+ the last save.
349+
350+ """
351+ if self._prev_dict is None:
352+ return True
353+ return self.previous(key) != self.get(key)
354+
355+ def previous(self, key):
356+ """Return previous value for this key, or None if there
357+ is no "previous" value.
358+
359+ """
360+ if self._prev_dict:
361+ return self._prev_dict.get(key)
362+ return None
363+
364+ def save(self):
365+ """Save this config to disk.
366+
367+ Preserves items in _prev_dict that do not exist in self.
368+
369+ """
370+ if self._prev_dict:
371+ for k, v in self._prev_dict.iteritems():
372+ if k not in self:
373+ self[k] = v
374+ with open(self.path, 'w') as f:
375+ json.dump(self, f)
376+
377+
378 @cached
379 def config(scope=None):
380- "Juju charm configuration"
381+ """Juju charm configuration"""
382 config_cmd_line = ['config-get']
383 if scope is not None:
384 config_cmd_line.append(scope)
385 config_cmd_line.append('--format=json')
386 try:
387- return json.loads(subprocess.check_output(config_cmd_line))
388+ config_data = json.loads(subprocess.check_output(config_cmd_line))
389+ if scope is not None:
390+ return config_data
391+ return Config(config_data)
392 except ValueError:
393 return None
394
395
396 @cached
397 def relation_get(attribute=None, unit=None, rid=None):
398+ """Get relation information"""
399 _args = ['relation-get', '--format=json']
400 if rid:
401 _args.append('-r')
402@@ -174,9 +279,14 @@
403 return json.loads(subprocess.check_output(_args))
404 except ValueError:
405 return None
406+ except CalledProcessError, e:
407+ if e.returncode == 2:
408+ return None
409+ raise
410
411
412 def relation_set(relation_id=None, relation_settings={}, **kwargs):
413+ """Set relation information for the current unit"""
414 relation_cmd_line = ['relation-set']
415 if relation_id is not None:
416 relation_cmd_line.extend(('-r', relation_id))
417@@ -192,7 +302,7 @@
418
419 @cached
420 def relation_ids(reltype=None):
421- "A list of relation_ids"
422+ """A list of relation_ids"""
423 reltype = reltype or relation_type()
424 relid_cmd_line = ['relation-ids', '--format=json']
425 if reltype is not None:
426@@ -203,7 +313,7 @@
427
428 @cached
429 def related_units(relid=None):
430- "A list of related units"
431+ """A list of related units"""
432 relid = relid or relation_id()
433 units_cmd_line = ['relation-list', '--format=json']
434 if relid is not None:
435@@ -213,7 +323,7 @@
436
437 @cached
438 def relation_for_unit(unit=None, rid=None):
439- "Get the json represenation of a unit's relation"
440+ """Get the json represenation of a unit's relation"""
441 unit = unit or remote_unit()
442 relation = relation_get(unit=unit, rid=rid)
443 for key in relation:
444@@ -225,7 +335,7 @@
445
446 @cached
447 def relations_for_id(relid=None):
448- "Get relations of a specific relation ID"
449+ """Get relations of a specific relation ID"""
450 relation_data = []
451 relid = relid or relation_ids()
452 for unit in related_units(relid):
453@@ -237,7 +347,7 @@
454
455 @cached
456 def relations_of_type(reltype=None):
457- "Get relations of a specific type"
458+ """Get relations of a specific type"""
459 relation_data = []
460 reltype = reltype or relation_type()
461 for relid in relation_ids(reltype):
462@@ -249,7 +359,7 @@
463
464 @cached
465 def relation_types():
466- "Get a list of relation types supported by this charm"
467+ """Get a list of relation types supported by this charm"""
468 charmdir = os.environ.get('CHARM_DIR', '')
469 mdf = open(os.path.join(charmdir, 'metadata.yaml'))
470 md = yaml.safe_load(mdf)
471@@ -264,6 +374,7 @@
472
473 @cached
474 def relations():
475+ """Get a nested dictionary of relation data for all related units"""
476 rels = {}
477 for reltype in relation_types():
478 relids = {}
479@@ -277,15 +388,35 @@
480 return rels
481
482
483+@cached
484+def is_relation_made(relation, keys='private-address'):
485+ '''
486+ Determine whether a relation is established by checking for
487+ presence of key(s). If a list of keys is provided, they
488+ must all be present for the relation to be identified as made
489+ '''
490+ if isinstance(keys, str):
491+ keys = [keys]
492+ for r_id in relation_ids(relation):
493+ for unit in related_units(r_id):
494+ context = {}
495+ for k in keys:
496+ context[k] = relation_get(k, rid=r_id,
497+ unit=unit)
498+ if None not in context.values():
499+ return True
500+ return False
501+
502+
503 def open_port(port, protocol="TCP"):
504- "Open a service network port"
505+ """Open a service network port"""
506 _args = ['open-port']
507 _args.append('{}/{}'.format(port, protocol))
508 subprocess.check_call(_args)
509
510
511 def close_port(port, protocol="TCP"):
512- "Close a service network port"
513+ """Close a service network port"""
514 _args = ['close-port']
515 _args.append('{}/{}'.format(port, protocol))
516 subprocess.check_call(_args)
517@@ -293,6 +424,7 @@
518
519 @cached
520 def unit_get(attribute):
521+ """Get the unit ID for the remote unit"""
522 _args = ['unit-get', '--format=json', attribute]
523 try:
524 return json.loads(subprocess.check_output(_args))
525@@ -301,22 +433,47 @@
526
527
528 def unit_private_ip():
529+ """Get this unit's private IP address"""
530 return unit_get('private-address')
531
532
533 class UnregisteredHookError(Exception):
534+ """Raised when an undefined hook is called"""
535 pass
536
537
538 class Hooks(object):
539+ """A convenient handler for hook functions.
540+
541+ Example::
542+
543+ hooks = Hooks()
544+
545+ # register a hook, taking its name from the function name
546+ @hooks.hook()
547+ def install():
548+ pass # your code here
549+
550+ # register a hook, providing a custom hook name
551+ @hooks.hook("config-changed")
552+ def config_changed():
553+ pass # your code here
554+
555+ if __name__ == "__main__":
556+ # execute a hook based on the name the program is called by
557+ hooks.execute(sys.argv)
558+ """
559+
560 def __init__(self):
561 super(Hooks, self).__init__()
562 self._hooks = {}
563
564 def register(self, name, function):
565+ """Register a hook"""
566 self._hooks[name] = function
567
568 def execute(self, args):
569+ """Execute a registered hook based on args[0]"""
570 hook_name = os.path.basename(args[0])
571 if hook_name in self._hooks:
572 self._hooks[hook_name]()
573@@ -324,6 +481,7 @@
574 raise UnregisteredHookError(hook_name)
575
576 def hook(self, *hook_names):
577+ """Decorator, registering them as hooks"""
578 def wrapper(decorated):
579 for hook_name in hook_names:
580 self.register(hook_name, decorated)
581@@ -337,4 +495,5 @@
582
583
584 def charm_dir():
585+ """Return the root directory of the current charm"""
586 return os.environ.get('CHARM_DIR')
587
588=== modified file 'hooks/charmhelpers/core/host.py'
589--- hooks/charmhelpers/core/host.py 2013-08-29 18:39:36 +0000
590+++ hooks/charmhelpers/core/host.py 2014-08-05 05:52:40 +0000
591@@ -16,21 +16,27 @@
592 from collections import OrderedDict
593
594 from hookenv import log
595+from fstab import Fstab
596
597
598 def service_start(service_name):
599+ """Start a system service"""
600 return service('start', service_name)
601
602
603 def service_stop(service_name):
604+ """Stop a system service"""
605 return service('stop', service_name)
606
607
608 def service_restart(service_name):
609+ """Restart a system service"""
610 return service('restart', service_name)
611
612
613 def service_reload(service_name, restart_on_failure=False):
614+ """Reload a system service, optionally falling back to restart if
615+ reload fails"""
616 service_result = service('reload', service_name)
617 if not service_result and restart_on_failure:
618 service_result = service('restart', service_name)
619@@ -38,11 +44,13 @@
620
621
622 def service(action, service_name):
623+ """Control a system service"""
624 cmd = ['service', service_name, action]
625 return subprocess.call(cmd) == 0
626
627
628 def service_running(service):
629+ """Determine whether a system service is running"""
630 try:
631 output = subprocess.check_output(['service', service, 'status'])
632 except subprocess.CalledProcessError:
633@@ -55,7 +63,7 @@
634
635
636 def adduser(username, password=None, shell='/bin/bash', system_user=False):
637- """Add a user"""
638+ """Add a user to the system"""
639 try:
640 user_info = pwd.getpwnam(username)
641 log('user {0} already exists!'.format(username))
642@@ -137,8 +145,20 @@
643 target.write(content)
644
645
646-def mount(device, mountpoint, options=None, persist=False):
647- '''Mount a filesystem'''
648+def fstab_remove(mp):
649+ """Remove the given mountpoint entry from /etc/fstab
650+ """
651+ return Fstab.remove_by_mountpoint(mp)
652+
653+
654+def fstab_add(dev, mp, fs, options=None):
655+ """Adds the given device entry to the /etc/fstab file
656+ """
657+ return Fstab.add(dev, mp, fs, options=options)
658+
659+
660+def mount(device, mountpoint, options=None, persist=False, filesystem="ext3"):
661+ """Mount a filesystem at a particular mountpoint"""
662 cmd_args = ['mount']
663 if options is not None:
664 cmd_args.extend(['-o', options])
665@@ -148,28 +168,28 @@
666 except subprocess.CalledProcessError, e:
667 log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
668 return False
669+
670 if persist:
671- # TODO: update fstab
672- pass
673+ return fstab_add(device, mountpoint, filesystem, options=options)
674 return True
675
676
677 def umount(mountpoint, persist=False):
678- '''Unmount a filesystem'''
679+ """Unmount a filesystem"""
680 cmd_args = ['umount', mountpoint]
681 try:
682 subprocess.check_output(cmd_args)
683 except subprocess.CalledProcessError, e:
684 log('Error unmounting {}\n{}'.format(mountpoint, e.output))
685 return False
686+
687 if persist:
688- # TODO: update fstab
689- pass
690+ return fstab_remove(mountpoint)
691 return True
692
693
694 def mounts():
695- '''List of all mounted volumes as [[mountpoint,device],[...]]'''
696+ """Get a list of all mounted volumes as [[mountpoint,device],[...]]"""
697 with open('/proc/mounts') as f:
698 # [['/mount/point','/dev/path'],[...]]
699 system_mounts = [m[1::-1] for m in [l.strip().split()
700@@ -178,7 +198,7 @@
701
702
703 def file_hash(path):
704- ''' Generate a md5 hash of the contents of 'path' or None if not found '''
705+ """Generate a md5 hash of the contents of 'path' or None if not found """
706 if os.path.exists(path):
707 h = hashlib.md5()
708 with open(path, 'r') as source:
709@@ -188,21 +208,21 @@
710 return None
711
712
713-def restart_on_change(restart_map):
714- ''' Restart services based on configuration files changing
715+def restart_on_change(restart_map, stopstart=False):
716+ """Restart services based on configuration files changing
717
718- This function is used a decorator, for example
719+ This function is used a decorator, for example::
720
721 @restart_on_change({
722 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
723 })
724 def ceph_client_changed():
725- ...
726+ pass # your code here
727
728 In this example, the cinder-api and cinder-volume services
729 would be restarted if /etc/ceph/ceph.conf is changed by the
730 ceph_client_changed function.
731- '''
732+ """
733 def wrap(f):
734 def wrapped_f(*args):
735 checksums = {}
736@@ -213,14 +233,20 @@
737 for path in restart_map:
738 if checksums[path] != file_hash(path):
739 restarts += restart_map[path]
740- for service_name in list(OrderedDict.fromkeys(restarts)):
741- service('restart', service_name)
742+ services_list = list(OrderedDict.fromkeys(restarts))
743+ if not stopstart:
744+ for service_name in services_list:
745+ service('restart', service_name)
746+ else:
747+ for action in ['stop', 'start']:
748+ for service_name in services_list:
749+ service(action, service_name)
750 return wrapped_f
751 return wrap
752
753
754 def lsb_release():
755- '''Return /etc/lsb-release in a dict'''
756+ """Return /etc/lsb-release in a dict"""
757 d = {}
758 with open('/etc/lsb-release', 'r') as lsb:
759 for l in lsb:
760@@ -230,7 +256,7 @@
761
762
763 def pwgen(length=None):
764- '''Generate a random pasword.'''
765+ """Generate a random pasword."""
766 if length is None:
767 length = random.choice(range(35, 45))
768 alphanumeric_chars = [
769@@ -239,3 +265,67 @@
770 random_chars = [
771 random.choice(alphanumeric_chars) for _ in range(length)]
772 return(''.join(random_chars))
773+
774+
775+def list_nics(nic_type):
776+ '''Return a list of nics of given type(s)'''
777+ if isinstance(nic_type, basestring):
778+ int_types = [nic_type]
779+ else:
780+ int_types = nic_type
781+ interfaces = []
782+ for int_type in int_types:
783+ cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
784+ ip_output = subprocess.check_output(cmd).split('\n')
785+ ip_output = (line for line in ip_output if line)
786+ for line in ip_output:
787+ if line.split()[1].startswith(int_type):
788+ interfaces.append(line.split()[1].replace(":", ""))
789+ return interfaces
790+
791+
792+def set_nic_mtu(nic, mtu):
793+ '''Set MTU on a network interface'''
794+ cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
795+ subprocess.check_call(cmd)
796+
797+
798+def get_nic_mtu(nic):
799+ cmd = ['ip', 'addr', 'show', nic]
800+ ip_output = subprocess.check_output(cmd).split('\n')
801+ mtu = ""
802+ for line in ip_output:
803+ words = line.split()
804+ if 'mtu' in words:
805+ mtu = words[words.index("mtu") + 1]
806+ return mtu
807+
808+
809+def get_nic_hwaddr(nic):
810+ cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
811+ ip_output = subprocess.check_output(cmd)
812+ hwaddr = ""
813+ words = ip_output.split()
814+ if 'link/ether' in words:
815+ hwaddr = words[words.index('link/ether') + 1]
816+ return hwaddr
817+
818+
819+def cmp_pkgrevno(package, revno, pkgcache=None):
820+ '''Compare supplied revno with the revno of the installed package
821+
822+ * 1 => Installed revno is greater than supplied arg
823+ * 0 => Installed revno is the same as supplied arg
824+ * -1 => Installed revno is less than supplied arg
825+
826+ '''
827+ import apt_pkg
828+ if not pkgcache:
829+ apt_pkg.init()
830+ # Force Apt to build its cache in memory. That way we avoid race
831+ # conditions with other applications building the cache in the same
832+ # place.
833+ apt_pkg.config.set("Dir::Cache::pkgcache", "")
834+ pkgcache = apt_pkg.Cache()
835+ pkg = pkgcache[package]
836+ return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
837
838=== modified file 'hooks/charmhelpers/fetch/__init__.py'
839--- hooks/charmhelpers/fetch/__init__.py 2013-08-29 18:41:54 +0000
840+++ hooks/charmhelpers/fetch/__init__.py 2014-08-05 05:52:40 +0000
841@@ -1,4 +1,5 @@
842 import importlib
843+import time
844 from yaml import safe_load
845 from charmhelpers.core.host import (
846 lsb_release
847@@ -12,7 +13,8 @@
848 config,
849 log,
850 )
851-import apt_pkg
852+import os
853+
854
855 CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
856 deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
857@@ -20,11 +22,107 @@
858 PROPOSED_POCKET = """# Proposed
859 deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
860 """
861+CLOUD_ARCHIVE_POCKETS = {
862+ # Folsom
863+ 'folsom': 'precise-updates/folsom',
864+ 'precise-folsom': 'precise-updates/folsom',
865+ 'precise-folsom/updates': 'precise-updates/folsom',
866+ 'precise-updates/folsom': 'precise-updates/folsom',
867+ 'folsom/proposed': 'precise-proposed/folsom',
868+ 'precise-folsom/proposed': 'precise-proposed/folsom',
869+ 'precise-proposed/folsom': 'precise-proposed/folsom',
870+ # Grizzly
871+ 'grizzly': 'precise-updates/grizzly',
872+ 'precise-grizzly': 'precise-updates/grizzly',
873+ 'precise-grizzly/updates': 'precise-updates/grizzly',
874+ 'precise-updates/grizzly': 'precise-updates/grizzly',
875+ 'grizzly/proposed': 'precise-proposed/grizzly',
876+ 'precise-grizzly/proposed': 'precise-proposed/grizzly',
877+ 'precise-proposed/grizzly': 'precise-proposed/grizzly',
878+ # Havana
879+ 'havana': 'precise-updates/havana',
880+ 'precise-havana': 'precise-updates/havana',
881+ 'precise-havana/updates': 'precise-updates/havana',
882+ 'precise-updates/havana': 'precise-updates/havana',
883+ 'havana/proposed': 'precise-proposed/havana',
884+ 'precise-havana/proposed': 'precise-proposed/havana',
885+ 'precise-proposed/havana': 'precise-proposed/havana',
886+ # Icehouse
887+ 'icehouse': 'precise-updates/icehouse',
888+ 'precise-icehouse': 'precise-updates/icehouse',
889+ 'precise-icehouse/updates': 'precise-updates/icehouse',
890+ 'precise-updates/icehouse': 'precise-updates/icehouse',
891+ 'icehouse/proposed': 'precise-proposed/icehouse',
892+ 'precise-icehouse/proposed': 'precise-proposed/icehouse',
893+ 'precise-proposed/icehouse': 'precise-proposed/icehouse',
894+ # Juno
895+ 'juno': 'trusty-updates/juno',
896+ 'trusty-juno': 'trusty-updates/juno',
897+ 'trusty-juno/updates': 'trusty-updates/juno',
898+ 'trusty-updates/juno': 'trusty-updates/juno',
899+ 'juno/proposed': 'trusty-proposed/juno',
900+ 'juno/proposed': 'trusty-proposed/juno',
901+ 'trusty-juno/proposed': 'trusty-proposed/juno',
902+ 'trusty-proposed/juno': 'trusty-proposed/juno',
903+}
904+
905+# The order of this list is very important. Handlers should be listed in from
906+# least- to most-specific URL matching.
907+FETCH_HANDLERS = (
908+ 'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
909+ 'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
910+)
911+
912+APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
913+APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
914+APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
915+
916+
917+class SourceConfigError(Exception):
918+ pass
919+
920+
921+class UnhandledSource(Exception):
922+ pass
923+
924+
925+class AptLockError(Exception):
926+ pass
927+
928+
929+class BaseFetchHandler(object):
930+
931+ """Base class for FetchHandler implementations in fetch plugins"""
932+
933+ def can_handle(self, source):
934+ """Returns True if the source can be handled. Otherwise returns
935+ a string explaining why it cannot"""
936+ return "Wrong source type"
937+
938+ def install(self, source):
939+ """Try to download and unpack the source. Return the path to the
940+ unpacked files or raise UnhandledSource."""
941+ raise UnhandledSource("Wrong source type {}".format(source))
942+
943+ def parse_url(self, url):
944+ return urlparse(url)
945+
946+ def base_url(self, url):
947+ """Return url without querystring or fragment"""
948+ parts = list(self.parse_url(url))
949+ parts[4:] = ['' for i in parts[4:]]
950+ return urlunparse(parts)
951
952
953 def filter_installed_packages(packages):
954 """Returns a list of packages that require installation"""
955+ import apt_pkg
956 apt_pkg.init()
957+
958+ # Tell apt to build an in-memory cache to prevent race conditions (if
959+ # another process is already building the cache).
960+ apt_pkg.config.set("Dir::Cache::pkgcache", "")
961+
962 cache = apt_pkg.Cache()
963 _pkgs = []
964 for package in packages:
965@@ -40,8 +138,10 @@
966
967 def apt_install(packages, options=None, fatal=False):
968 """Install one or more packages"""
969- options = options or []
970- cmd = ['apt-get', '-y']
971+ if options is None:
972+ options = ['--option=Dpkg::Options::=--force-confold']
973+
974+ cmd = ['apt-get', '--assume-yes']
975 cmd.extend(options)
976 cmd.append('install')
977 if isinstance(packages, basestring):
978@@ -50,29 +150,50 @@
979 cmd.extend(packages)
980 log("Installing {} with options: {}".format(packages,
981 options))
982- if fatal:
983- subprocess.check_call(cmd)
984+ _run_apt_command(cmd, fatal)
985+
986+
987+def apt_upgrade(options=None, fatal=False, dist=False):
988+ """Upgrade all packages"""
989+ if options is None:
990+ options = ['--option=Dpkg::Options::=--force-confold']
991+
992+ cmd = ['apt-get', '--assume-yes']
993+ cmd.extend(options)
994+ if dist:
995+ cmd.append('dist-upgrade')
996 else:
997- subprocess.call(cmd)
998+ cmd.append('upgrade')
999+ log("Upgrading with options: {}".format(options))
1000+ _run_apt_command(cmd, fatal)
1001
1002
1003 def apt_update(fatal=False):
1004 """Update local apt cache"""
1005 cmd = ['apt-get', 'update']
1006- if fatal:
1007- subprocess.check_call(cmd)
1008- else:
1009- subprocess.call(cmd)
1010+ _run_apt_command(cmd, fatal)
1011
1012
1013 def apt_purge(packages, fatal=False):
1014 """Purge one or more packages"""
1015- cmd = ['apt-get', '-y', 'purge']
1016+ cmd = ['apt-get', '--assume-yes', 'purge']
1017 if isinstance(packages, basestring):
1018 cmd.append(packages)
1019 else:
1020 cmd.extend(packages)
1021 log("Purging {}".format(packages))
1022+ _run_apt_command(cmd, fatal)
1023+
1024+
1025+def apt_hold(packages, fatal=False):
1026+ """Hold one or more packages"""
1027+ cmd = ['apt-mark', 'hold']
1028+ if isinstance(packages, basestring):
1029+ cmd.append(packages)
1030+ else:
1031+ cmd.extend(packages)
1032+ log("Holding {}".format(packages))
1033+
1034 if fatal:
1035 subprocess.check_call(cmd)
1036 else:
1037@@ -80,67 +201,76 @@
1038
1039
1040 def add_source(source, key=None):
1041- if ((source.startswith('ppa:') or
1042- source.startswith('http:'))):
1043+ if source is None:
1044+ log('Source is not present. Skipping')
1045+ return
1046+
1047+ if (source.startswith('ppa:') or
1048+ source.startswith('http') or
1049+ source.startswith('deb ') or
1050+ source.startswith('cloud-archive:')):
1051 subprocess.check_call(['add-apt-repository', '--yes', source])
1052 elif source.startswith('cloud:'):
1053 apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
1054 fatal=True)
1055 pocket = source.split(':')[-1]
1056+ if pocket not in CLOUD_ARCHIVE_POCKETS:
1057+ raise SourceConfigError(
1058+ 'Unsupported cloud: source option %s' %
1059+ pocket)
1060+ actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
1061 with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
1062- apt.write(CLOUD_ARCHIVE.format(pocket))
1063+ apt.write(CLOUD_ARCHIVE.format(actual_pocket))
1064 elif source == 'proposed':
1065 release = lsb_release()['DISTRIB_CODENAME']
1066 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
1067 apt.write(PROPOSED_POCKET.format(release))
1068 if key:
1069- subprocess.check_call(['apt-key', 'import', key])
1070-
1071-
1072-class SourceConfigError(Exception):
1073- pass
1074+ subprocess.check_call(['apt-key', 'adv', '--keyserver',
1075+ 'hkp://keyserver.ubuntu.com:80', '--recv',
1076+ key])
1077
1078
1079 def configure_sources(update=False,
1080 sources_var='install_sources',
1081 keys_var='install_keys'):
1082 """
1083- Configure multiple sources from charm configuration
1084+ Configure multiple sources from charm configuration.
1085+
1086+ The lists are encoded as yaml fragments in the configuration.
1087+ The frament needs to be included as a string.
1088
1089 Example config:
1090- install_sources:
1091+ install_sources: |
1092 - "ppa:foo"
1093 - "http://example.com/repo precise main"
1094- install_keys:
1095+ install_keys: |
1096 - null
1097 - "a1b2c3d4"
1098
1099 Note that 'null' (a.k.a. None) should not be quoted.
1100 """
1101- sources = safe_load(config(sources_var))
1102- keys = safe_load(config(keys_var))
1103- if isinstance(sources, basestring) and isinstance(keys, basestring):
1104- add_source(sources, keys)
1105+ sources = safe_load((config(sources_var) or '').strip()) or []
1106+ keys = safe_load((config(keys_var) or '').strip()) or None
1107+
1108+ if isinstance(sources, basestring):
1109+ sources = [sources]
1110+
1111+ if keys is None:
1112+ for source in sources:
1113+ add_source(source, None)
1114 else:
1115- if not len(sources) == len(keys):
1116- msg = 'Install sources and keys lists are different lengths'
1117- raise SourceConfigError(msg)
1118- for src_num in range(len(sources)):
1119- add_source(sources[src_num], keys[src_num])
1120+ if isinstance(keys, basestring):
1121+ keys = [keys]
1122+
1123+ if len(sources) != len(keys):
1124+ raise SourceConfigError(
1125+ 'Install sources and keys lists are different lengths')
1126+ for source, key in zip(sources, keys):
1127+ add_source(source, key)
1128 if update:
1129 apt_update(fatal=True)
1130
1131-# The order of this list is very important. Handlers should be listed in from
1132-# least- to most-specific URL matching.
1133-FETCH_HANDLERS = (
1134- 'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
1135- 'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
1136-)
1137-
1138-
1139-class UnhandledSource(Exception):
1140- pass
1141-
1142
1143 def install_remote(source):
1144 """
1145@@ -171,28 +301,6 @@
1146 return install_remote(source)
1147
1148
1149-class BaseFetchHandler(object):
1150- """Base class for FetchHandler implementations in fetch plugins"""
1151- def can_handle(self, source):
1152- """Returns True if the source can be handled. Otherwise returns
1153- a string explaining why it cannot"""
1154- return "Wrong source type"
1155-
1156- def install(self, source):
1157- """Try to download and unpack the source. Return the path to the
1158- unpacked files or raise UnhandledSource."""
1159- raise UnhandledSource("Wrong source type {}".format(source))
1160-
1161- def parse_url(self, url):
1162- return urlparse(url)
1163-
1164- def base_url(self, url):
1165- """Return url without querystring or fragment"""
1166- parts = list(self.parse_url(url))
1167- parts[4:] = ['' for i in parts[4:]]
1168- return urlunparse(parts)
1169-
1170-
1171 def plugins(fetch_handlers=None):
1172 if not fetch_handlers:
1173 fetch_handlers = FETCH_HANDLERS
1174@@ -200,10 +308,50 @@
1175 for handler_name in fetch_handlers:
1176 package, classname = handler_name.rsplit('.', 1)
1177 try:
1178- handler_class = getattr(importlib.import_module(package), classname)
1179+ handler_class = getattr(
1180+ importlib.import_module(package),
1181+ classname)
1182 plugin_list.append(handler_class())
1183 except (ImportError, AttributeError):
1184 # Skip missing plugins so that they can be ommitted from
1185 # installation if desired
1186- log("FetchHandler {} not found, skipping plugin".format(handler_name))
1187+ log("FetchHandler {} not found, skipping plugin".format(
1188+ handler_name))
1189 return plugin_list
1190+
1191+
1192+def _run_apt_command(cmd, fatal=False):
1193+ """
1194+ Run an APT command, checking output and retrying if the fatal flag is set
1195+ to True.
1196+
1197+ :param: cmd: str: The apt command to run.
1198+ :param: fatal: bool: Whether the command's output should be checked and
1199+ retried.
1200+ """
1201+ env = os.environ.copy()
1202+
1203+ if 'DEBIAN_FRONTEND' not in env:
1204+ env['DEBIAN_FRONTEND'] = 'noninteractive'
1205+
1206+ if fatal:
1207+ retry_count = 0
1208+ result = None
1209+
1210+ # If the command is considered "fatal", we need to retry if the apt
1211+ # lock was not acquired.
1212+
1213+ while result is None or result == APT_NO_LOCK:
1214+ try:
1215+ result = subprocess.check_call(cmd, env=env)
1216+ except subprocess.CalledProcessError, e:
1217+ retry_count = retry_count + 1
1218+ if retry_count > APT_NO_LOCK_RETRY_COUNT:
1219+ raise
1220+ result = e.returncode
1221+ log("Couldn't acquire DPKG lock. Will retry in {} seconds."
1222+ "".format(APT_NO_LOCK_RETRY_DELAY))
1223+ time.sleep(APT_NO_LOCK_RETRY_DELAY)
1224+
1225+ else:
1226+ subprocess.call(cmd, env=env)
1227
1228=== modified file 'hooks/charmhelpers/fetch/archiveurl.py'
1229--- hooks/charmhelpers/fetch/archiveurl.py 2013-08-29 18:41:54 +0000
1230+++ hooks/charmhelpers/fetch/archiveurl.py 2014-08-05 05:52:40 +0000
1231@@ -1,5 +1,7 @@
1232 import os
1233 import urllib2
1234+import urlparse
1235+
1236 from charmhelpers.fetch import (
1237 BaseFetchHandler,
1238 UnhandledSource
1239@@ -24,6 +26,19 @@
1240 def download(self, source, dest):
1241 # propogate all exceptions
1242 # URLError, OSError, etc
1243+ proto, netloc, path, params, query, fragment = urlparse.urlparse(source)
1244+ if proto in ('http', 'https'):
1245+ auth, barehost = urllib2.splituser(netloc)
1246+ if auth is not None:
1247+ source = urlparse.urlunparse((proto, barehost, path, params, query, fragment))
1248+ username, password = urllib2.splitpasswd(auth)
1249+ passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
1250+ # Realm is set to None in add_password to force the username and password
1251+ # to be used whatever the realm
1252+ passman.add_password(None, source, username, password)
1253+ authhandler = urllib2.HTTPBasicAuthHandler(passman)
1254+ opener = urllib2.build_opener(authhandler)
1255+ urllib2.install_opener(opener)
1256 response = urllib2.urlopen(source)
1257 try:
1258 with open(dest, 'w') as dest_file:
1259
1260=== modified file 'hooks/charmhelpers/fetch/bzrurl.py'
1261--- hooks/charmhelpers/fetch/bzrurl.py 2013-08-29 18:41:54 +0000
1262+++ hooks/charmhelpers/fetch/bzrurl.py 2014-08-05 05:52:40 +0000
1263@@ -12,6 +12,7 @@
1264 apt_install("python-bzrlib")
1265 from bzrlib.branch import Branch
1266
1267+
1268 class BzrUrlFetchHandler(BaseFetchHandler):
1269 """Handler for bazaar branches via generic and lp URLs"""
1270 def can_handle(self, source):
1271@@ -38,7 +39,8 @@
1272 def install(self, source):
1273 url_parts = self.parse_url(source)
1274 branch_name = url_parts.path.strip("/").split("/")[-1]
1275- dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", branch_name)
1276+ dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched",
1277+ branch_name)
1278 if not os.path.exists(dest_dir):
1279 mkdir(dest_dir, perms=0755)
1280 try:
1281@@ -46,4 +48,3 @@
1282 except OSError as e:
1283 raise UnhandledSource(e.strerror)
1284 return dest_dir
1285-
1286
1287=== modified file 'hooks/ntpmaster_hooks.py'
1288--- hooks/ntpmaster_hooks.py 2013-11-22 14:17:22 +0000
1289+++ hooks/ntpmaster_hooks.py 2014-08-05 05:52:40 +0000
1290@@ -7,9 +7,8 @@
1291 import charmhelpers.fetch as fetch
1292 import charmhelpers.core.host as host
1293 from charmhelpers.core.hookenv import UnregisteredHookError
1294-from utils import (
1295- render_template,
1296-)
1297+from charmhelpers.contrib.templating.jinja import render
1298+
1299
1300 NTP_CONF = '/etc/ntp.conf'
1301 NTP_CONF_ORIG = '{}.orig'.format(NTP_CONF)
1302@@ -55,8 +54,7 @@
1303 'peers': peers
1304 }
1305 with open(NTP_CONF, "w") as ntpconf:
1306- ntpconf.write(render_template(os.path.basename(NTP_CONF),
1307- ntp_context))
1308+ ntpconf.write(render(os.path.basename(NTP_CONF), ntp_context))
1309 else:
1310 shutil.copy(NTP_CONF_ORIG, NTP_CONF)
1311
1312
1313=== removed file 'hooks/utils.py'
1314--- hooks/utils.py 2013-08-29 18:39:36 +0000
1315+++ hooks/utils.py 1970-01-01 00:00:00 +0000
1316@@ -1,21 +0,0 @@
1317-from charmhelpers.fetch import (
1318- apt_install,
1319- filter_installed_packages
1320-)
1321-
1322-
1323-TEMPLATES_DIR = 'templates'
1324-
1325-try:
1326- import jinja2
1327-except ImportError:
1328- apt_install(filter_installed_packages(['python-jinja2']),
1329- fatal=True)
1330- import jinja2
1331-
1332-
1333-def render_template(template_name, context, template_dir=TEMPLATES_DIR):
1334- templates = jinja2.Environment(
1335- loader=jinja2.FileSystemLoader(template_dir))
1336- template = templates.get_template(template_name)
1337- return template.render(context)

Subscribers

People subscribed via source and target branches

to all changes: