Merge lp:~tribaal/charms/trusty/ntpmaster/use-charm-helpers-templating into lp:charms/ntpmaster
- Trusty Tahr (14.04)
- use-charm-helpers-templating
- Merge into trunk
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Matt Bruzek (community) | Approve | ||
Review via email: mp+229558@code.launchpad.net |
Commit message
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.
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) |
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.