Merge lp:~johnsca/charms/trusty/cf-loggregator/refactor into lp:~cf-charmers/charms/trusty/cf-loggregator/trunk

Proposed by Cory Johns
Status: Merged
Merged at revision: 31
Proposed branch: lp:~johnsca/charms/trusty/cf-loggregator/refactor
Merge into: lp:~cf-charmers/charms/trusty/cf-loggregator/trunk
Diff against target: 1660 lines (+743/-433)
20 files modified
config.yaml (+19/-0)
hooks/charmhelpers/contrib/cloudfoundry/common.py (+3/-62)
hooks/charmhelpers/contrib/cloudfoundry/config_helper.py (+0/-11)
hooks/charmhelpers/contrib/cloudfoundry/contexts.py (+26/-54)
hooks/charmhelpers/contrib/cloudfoundry/install.py (+0/-35)
hooks/charmhelpers/contrib/cloudfoundry/services.py (+0/-118)
hooks/charmhelpers/contrib/cloudfoundry/upstart_helper.py (+0/-14)
hooks/charmhelpers/contrib/hahelpers/apache.py (+9/-8)
hooks/charmhelpers/contrib/openstack/context.py (+107/-26)
hooks/charmhelpers/contrib/openstack/neutron.py (+31/-5)
hooks/charmhelpers/contrib/openstack/utils.py (+9/-1)
hooks/charmhelpers/contrib/storage/linux/lvm.py (+1/-1)
hooks/charmhelpers/contrib/storage/linux/utils.py (+28/-5)
hooks/charmhelpers/core/hookenv.py (+98/-1)
hooks/charmhelpers/core/host.py (+47/-0)
hooks/charmhelpers/core/services.py (+84/-0)
hooks/charmhelpers/core/templating.py (+158/-0)
hooks/charmhelpers/fetch/__init__.py (+97/-65)
hooks/hooks.py (+16/-15)
hooks/install (+10/-12)
To merge this branch: bzr merge lp:~johnsca/charms/trusty/cf-loggregator/refactor
Reviewer Review Type Date Requested Status
Cloud Foundry Charmers Pending
Review via email: mp+219915@code.launchpad.net

Description of the change

Refactored to use refactored charm-helpers

https://codereview.appspot.com/91510047/

To post a comment you must log in.
Revision history for this message
Cory Johns (johnsca) wrote :

Reviewers: mp+219915_code.launchpad.net,

Message:
Please take a look.

Description:
Refactored to use refactored charm-helpers

https://code.launchpad.net/~johnsca/charms/trusty/cf-loggregator/refactor/+merge/219915

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/91510047/

Affected files (+485, -361 lines):
   A [revision details]
   M config.yaml
   M hooks/charmhelpers/contrib/cloudfoundry/common.py
   D hooks/charmhelpers/contrib/cloudfoundry/config_helper.py
   M hooks/charmhelpers/contrib/cloudfoundry/contexts.py
   D hooks/charmhelpers/contrib/cloudfoundry/install.py
   D hooks/charmhelpers/contrib/cloudfoundry/services.py
   D hooks/charmhelpers/contrib/cloudfoundry/upstart_helper.py
   M hooks/charmhelpers/contrib/hahelpers/apache.py
   M hooks/charmhelpers/contrib/openstack/context.py
   M hooks/charmhelpers/contrib/openstack/neutron.py
   M hooks/charmhelpers/contrib/openstack/utils.py
   M hooks/charmhelpers/contrib/storage/linux/utils.py
   M hooks/charmhelpers/core/host.py
   A hooks/charmhelpers/core/services.py
   A hooks/charmhelpers/core/templating.py
   M hooks/charmhelpers/fetch/__init__.py
   M hooks/hooks.py
   M hooks/install
   M templates/loggregator.conf

Revision history for this message
Benjamin Saller (bcsaller) wrote :

LGTM +1

Thanks again

https://codereview.appspot.com/91510047/diff/1/hooks/hooks.py
File hooks/hooks.py (right):

https://codereview.appspot.com/91510047/diff/1/hooks/hooks.py#newcode11
hooks/hooks.py:11: import config as lg_config
just config? or we could rename to common as in some of the others now.

https://codereview.appspot.com/91510047/

Revision history for this message
Cory Johns (johnsca) wrote :

https://codereview.appspot.com/91510047/diff/1/hooks/hooks.py
File hooks/hooks.py (right):

https://codereview.appspot.com/91510047/diff/1/hooks/hooks.py#newcode11
hooks/hooks.py:11: import config as lg_config
On 2014/05/19 18:58:39, benjamin.saller wrote:
> just config? or we could rename to common as in some of the others
now.

I was using xx_config to prevent potential conflicts with
hookenv.config, but those are all referenced indirectly anyway, so it's
probably not worth bothering with the prefix.

https://codereview.appspot.com/91510047/

32. By Cory Johns

Merged :parent

33. By Cory Johns

Resynced charm-helpers

34. By Cory Johns

Removed unnecessary prefix from config var

35. By Cory Johns

Slight addendum to previous commit

Revision history for this message
Cory Johns (johnsca) wrote :

*** Submitted:

Refactored to use refactored charm-helpers

R=benjamin.saller, cory.johns
CC=
https://codereview.appspot.com/91510047

https://codereview.appspot.com/91510047/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'config.yaml'
2--- config.yaml 2014-05-12 11:12:06 +0000
3+++ config.yaml 2014-05-20 21:57:20 +0000
4@@ -8,3 +8,22 @@
5 The shared-secret for log clients to use when publishing log
6 messages.
7 default: 17b6cc14-eea8-433e-b8b7-a65ff8941e3b
8+ source:
9+ type: string
10+ default: 'ppa:cf-charm/ppa'
11+ description: |
12+ Optional configuration to support use of additional sources such as:
13+ .
14+ - ppa:myteam/ppa
15+ - cloud:precise-proposed/folsom
16+ - http://my.archive.com/ubuntu main
17+ .
18+ The last option should be used in conjunction with the key configuration
19+ option.
20+ .
21+ key:
22+ type: string
23+ default: '4C430C3C2828E07D'
24+ description: |
25+ Key ID to import to the apt keyring to support use with arbitary source
26+ configuration from outside of Launchpad archives or PPA's.
27
28=== removed directory 'files/upstart'
29=== modified file 'hooks/charmhelpers/contrib/cloudfoundry/common.py'
30--- hooks/charmhelpers/contrib/cloudfoundry/common.py 2014-05-07 16:31:54 +0000
31+++ hooks/charmhelpers/contrib/cloudfoundry/common.py 2014-05-20 21:57:20 +0000
32@@ -1,11 +1,3 @@
33-import sys
34-import os
35-import pwd
36-import grp
37-import subprocess
38-
39-from contextlib import contextmanager
40-from charmhelpers.core.hookenv import log, ERROR, DEBUG
41 from charmhelpers.core import host
42
43 from charmhelpers.fetch import (
44@@ -13,59 +5,8 @@
45 )
46
47
48-def run(command, exit_on_error=True, quiet=False):
49- '''Run a command and return the output.'''
50- if not quiet:
51- log("Running {!r}".format(command), DEBUG)
52- p = subprocess.Popen(
53- command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
54- shell=isinstance(command, basestring))
55- p.stdin.close()
56- lines = []
57- for line in p.stdout:
58- if line:
59- if not quiet:
60- print line
61- lines.append(line)
62- elif p.poll() is not None:
63- break
64-
65- p.wait()
66-
67- if p.returncode == 0:
68- return '\n'.join(lines)
69-
70- if p.returncode != 0 and exit_on_error:
71- log("ERROR: {}".format(p.returncode), ERROR)
72- sys.exit(p.returncode)
73-
74- raise subprocess.CalledProcessError(
75- p.returncode, command, '\n'.join(lines))
76-
77-
78-def chownr(path, owner, group):
79- uid = pwd.getpwnam(owner).pw_uid
80- gid = grp.getgrnam(group).gr_gid
81- for root, dirs, files in os.walk(path):
82- for momo in dirs:
83- os.chown(os.path.join(root, momo), uid, gid)
84- for momo in files:
85- os.chown(os.path.join(root, momo), uid, gid)
86-
87-
88-@contextmanager
89-def chdir(d):
90- cur = os.getcwd()
91- try:
92- yield os.chdir(d)
93- finally:
94- os.chdir(cur)
95-
96-
97 def prepare_cloudfoundry_environment(config_data, packages):
98- if 'source' in config_data:
99- add_source(config_data['source'], config_data.get('key'))
100- apt_update(fatal=True)
101- if packages:
102- apt_install(packages=filter_installed_packages(packages), fatal=True)
103+ add_source(config_data['source'], config_data.get('key'))
104+ apt_update(fatal=True)
105+ apt_install(packages=filter_installed_packages(packages), fatal=True)
106 host.adduser('vcap')
107
108=== removed file 'hooks/charmhelpers/contrib/cloudfoundry/config_helper.py'
109--- hooks/charmhelpers/contrib/cloudfoundry/config_helper.py 2014-04-25 12:49:48 +0000
110+++ hooks/charmhelpers/contrib/cloudfoundry/config_helper.py 1970-01-01 00:00:00 +0000
111@@ -1,11 +0,0 @@
112-import jinja2
113-
114-TEMPLATES_DIR = 'templates'
115-
116-def render_template(template_name, context, template_dir=TEMPLATES_DIR):
117- templates = jinja2.Environment(
118- loader=jinja2.FileSystemLoader(template_dir))
119- template = templates.get_template(template_name)
120- return template.render(context)
121-
122-
123
124=== modified file 'hooks/charmhelpers/contrib/cloudfoundry/contexts.py'
125--- hooks/charmhelpers/contrib/cloudfoundry/contexts.py 2014-05-07 16:31:54 +0000
126+++ hooks/charmhelpers/contrib/cloudfoundry/contexts.py 2014-05-20 21:57:20 +0000
127@@ -1,54 +1,16 @@
128 import os
129-import yaml
130-
131-from charmhelpers.core import hookenv
132-from charmhelpers.contrib.openstack.context import OSContextGenerator
133-
134-
135-class RelationContext(OSContextGenerator):
136- def __call__(self):
137- if not hookenv.relation_ids(self.interface):
138- return {}
139-
140- ctx = {}
141- for rid in hookenv.relation_ids(self.interface):
142- for unit in hookenv.related_units(rid):
143- reldata = hookenv.relation_get(rid=rid, unit=unit)
144- required = set(self.required_keys)
145- if set(reldata.keys()).issuperset(required):
146- ns = ctx.setdefault(self.interface, {})
147- for k, v in reldata.items():
148- ns[k] = v
149- return ctx
150-
151- return {}
152-
153-
154-class ConfigContext(OSContextGenerator):
155- def __call__(self):
156- return hookenv.config()
157-
158-
159-class StorableContext(object):
160-
161- def store_context(self, file_name, config_data):
162- with open(file_name, 'w') as file_stream:
163- yaml.dump(config_data, file_stream)
164-
165- def read_context(self, file_name):
166- with open(file_name, 'r') as file_stream:
167- data = yaml.load(file_stream)
168- if not data:
169- raise OSError("%s is empty" % file_name)
170- return data
171+
172+from charmhelpers.core.templating import (
173+ ContextGenerator,
174+ RelationContext,
175+ StorableContext,
176+)
177
178
179 # Stores `config_data` hash into yaml file with `file_name` as a name
180 # if `file_name` already exists, then it loads data from `file_name`.
181-class StoredContext(OSContextGenerator, StorableContext):
182-
183+class StoredContext(ContextGenerator, StorableContext):
184 def __init__(self, file_name, config_data):
185- self.data = config_data
186 if os.path.exists(file_name):
187 self.data = self.read_context(file_name)
188 else:
189@@ -59,25 +21,35 @@
190 return self.data
191
192
193-class StaticContext(OSContextGenerator):
194- def __init__(self, data):
195- self.data = data
196-
197- def __call__(self):
198- return self.data
199-
200-
201 class NatsContext(RelationContext):
202 interface = 'nats'
203 required_keys = ['nats_port', 'nats_address', 'nats_user', 'nats_password']
204
205
206+class MysqlDSNContext(RelationContext):
207+ interface = 'db'
208+ required_keys = ['user', 'password', 'host', 'database']
209+ dsn_template = "mysql2://{user}:{password}@{host}:{port}/{database}"
210+
211+ def __call__(self):
212+ ctx = RelationContext.__call__(self)
213+ if ctx:
214+ if 'port' not in ctx:
215+ ctx['db']['port'] = '3306'
216+ ctx['db']['dsn'] = self.dsn_template.format(**ctx['db'])
217+ return ctx
218+
219+
220 class RouterContext(RelationContext):
221 interface = 'router'
222 required_keys = ['domain']
223
224
225+class LogRouterContext(RelationContext):
226+ interface = 'logrouter'
227+ required_keys = ['shared-secret', 'logrouter-address']
228+
229+
230 class LoggregatorContext(RelationContext):
231 interface = 'loggregator'
232 required_keys = ['shared_secret', 'loggregator_address']
233-
234
235=== removed file 'hooks/charmhelpers/contrib/cloudfoundry/install.py'
236--- hooks/charmhelpers/contrib/cloudfoundry/install.py 2014-04-25 12:49:48 +0000
237+++ hooks/charmhelpers/contrib/cloudfoundry/install.py 1970-01-01 00:00:00 +0000
238@@ -1,35 +0,0 @@
239-import os
240-import subprocess
241-
242-
243-def install(src, dest, fileprops=None, sudo=False):
244- """Install a file from src to dest. Dest can be a complete filename
245- or a target directory. fileprops is a dict with 'owner' (username of owner)
246- and mode (octal string) as keys, the defaults are 'ubuntu' and '400'
247-
248- When owner is passed or when access requires it sudo can be set to True and
249- sudo will be used to install the file.
250- """
251- if not fileprops:
252- fileprops = {}
253- mode = fileprops.get('mode', '400')
254- owner = fileprops.get('owner')
255- cmd = ['install']
256-
257- if not os.path.exists(src):
258- raise OSError(src)
259-
260- if not os.path.exists(dest) and not os.path.exists(os.path.dirname(dest)):
261- # create all but the last component as path
262- cmd.append('-D')
263-
264- if mode:
265- cmd.extend(['-m', mode])
266-
267- if owner:
268- cmd.extend(['-o', owner])
269-
270- if sudo:
271- cmd.insert(0, 'sudo')
272- cmd.extend([src, dest])
273- subprocess.check_call(cmd)
274
275=== removed file 'hooks/charmhelpers/contrib/cloudfoundry/services.py'
276--- hooks/charmhelpers/contrib/cloudfoundry/services.py 2014-04-25 12:49:48 +0000
277+++ hooks/charmhelpers/contrib/cloudfoundry/services.py 1970-01-01 00:00:00 +0000
278@@ -1,118 +0,0 @@
279-import os
280-import tempfile
281-from charmhelpers.core import host
282-
283-from charmhelpers.contrib.cloudfoundry.install import install
284-from charmhelpers.core.hookenv import log
285-from jinja2 import Environment, FileSystemLoader
286-
287-SERVICE_CONFIG = []
288-TEMPLATE_LOADER = None
289-
290-
291-def render_template(template_name, context):
292- """Render template to a tempfile returning the name"""
293- _, fn = tempfile.mkstemp()
294- template = load_template(template_name)
295- output = template.render(context)
296- with open(fn, "w") as fp:
297- fp.write(output)
298- return fn
299-
300-
301-def collect_contexts(context_providers):
302- ctx = {}
303- for provider in context_providers:
304- c = provider()
305- if not c:
306- return {}
307- ctx.update(c)
308- return ctx
309-
310-
311-def load_template(name):
312- return TEMPLATE_LOADER.get_template(name)
313-
314-
315-def configure_templates(template_dir):
316- global TEMPLATE_LOADER
317- TEMPLATE_LOADER = Environment(loader=FileSystemLoader(template_dir))
318-
319-
320-def register(service_configs, template_dir):
321- """Register a list of service configs.
322-
323- Service Configs are dicts in the following formats:
324-
325- {
326- "service": <service name>,
327- "templates": [ {
328- 'target': <render target of template>,
329- 'source': <optional name of template in passed in template_dir>
330- 'file_properties': <optional dict taking owner and octal mode>
331- 'contexts': [ context generators, see contexts.py ]
332- }
333- ] }
334-
335- If 'source' is not provided for a template the template_dir will
336- be consulted for ``basename(target).j2``.
337- """
338- global SERVICE_CONFIG
339- if template_dir:
340- configure_templates(template_dir)
341- SERVICE_CONFIG.extend(service_configs)
342-
343-
344-def reset():
345- global SERVICE_CONFIG
346- SERVICE_CONFIG = []
347-
348-
349-# def service_context(name):
350-# contexts = collect_contexts(template['contexts'])
351-
352-def reconfigure_service(service_name, restart=True):
353- global SERVICE_CONFIG
354- service = None
355- for service in SERVICE_CONFIG:
356- if service['service'] == service_name:
357- break
358- if not service or service['service'] != service_name:
359- raise KeyError('Service not registered: %s' % service_name)
360-
361- templates = service['templates']
362- for template in templates:
363- contexts = collect_contexts(template['contexts'])
364- if contexts:
365- template_target = template['target']
366- default_template = "%s.j2" % os.path.basename(template_target)
367- template_name = template.get('source', default_template)
368- output_file = render_template(template_name, contexts)
369- file_properties = template.get('file_properties')
370- install(output_file, template_target, file_properties)
371- os.unlink(output_file)
372- else:
373- restart = False
374-
375- if restart:
376- host.service_restart(service_name)
377-
378-
379-def stop_services():
380- global SERVICE_CONFIG
381- for service in SERVICE_CONFIG:
382- if host.service_running(service['service']):
383- host.service_stop(service['service'])
384-
385-
386-def get_service(service_name):
387- global SERVICE_CONFIG
388- for service in SERVICE_CONFIG:
389- if service_name == service['service']:
390- return service
391- return None
392-
393-
394-def reconfigure_services(restart=True):
395- for service in SERVICE_CONFIG:
396- reconfigure_service(service['service'], restart=restart)
397
398=== removed file 'hooks/charmhelpers/contrib/cloudfoundry/upstart_helper.py'
399--- hooks/charmhelpers/contrib/cloudfoundry/upstart_helper.py 2014-04-25 12:49:48 +0000
400+++ hooks/charmhelpers/contrib/cloudfoundry/upstart_helper.py 1970-01-01 00:00:00 +0000
401@@ -1,14 +0,0 @@
402-import os
403-import glob
404-from charmhelpers.core import hookenv
405-from charmhelpers.core.hookenv import charm_dir
406-from charmhelpers.contrib.cloudfoundry.install import install
407-
408-
409-def install_upstart_scripts(dirname=os.path.join(hookenv.charm_dir(),
410- 'files/upstart'),
411- pattern='*.conf'):
412- for script in glob.glob("%s/%s" % (dirname, pattern)):
413- filename = os.path.join(dirname, script)
414- hookenv.log('Installing upstart job:' + filename, hookenv.DEBUG)
415- install(filename, '/etc/init')
416
417=== modified file 'hooks/charmhelpers/contrib/hahelpers/apache.py'
418--- hooks/charmhelpers/contrib/hahelpers/apache.py 2014-04-25 12:49:48 +0000
419+++ hooks/charmhelpers/contrib/hahelpers/apache.py 2014-05-20 21:57:20 +0000
420@@ -39,14 +39,15 @@
421
422
423 def get_ca_cert():
424- ca_cert = None
425- log("Inspecting identity-service relations for CA SSL certificate.",
426- level=INFO)
427- for r_id in relation_ids('identity-service'):
428- for unit in relation_list(r_id):
429- if not ca_cert:
430- ca_cert = relation_get('ca_cert',
431- rid=r_id, unit=unit)
432+ ca_cert = config_get('ssl_ca')
433+ if ca_cert is None:
434+ log("Inspecting identity-service relations for CA SSL certificate.",
435+ level=INFO)
436+ for r_id in relation_ids('identity-service'):
437+ for unit in relation_list(r_id):
438+ if ca_cert is None:
439+ ca_cert = relation_get('ca_cert',
440+ rid=r_id, unit=unit)
441 return ca_cert
442
443
444
445=== modified file 'hooks/charmhelpers/contrib/openstack/context.py'
446--- hooks/charmhelpers/contrib/openstack/context.py 2014-04-25 12:49:48 +0000
447+++ hooks/charmhelpers/contrib/openstack/context.py 2014-05-20 21:57:20 +0000
448@@ -1,5 +1,6 @@
449 import json
450 import os
451+import time
452
453 from base64 import b64decode
454
455@@ -113,7 +114,8 @@
456 class SharedDBContext(OSContextGenerator):
457 interfaces = ['shared-db']
458
459- def __init__(self, database=None, user=None, relation_prefix=None):
460+ def __init__(self,
461+ database=None, user=None, relation_prefix=None, ssl_dir=None):
462 '''
463 Allows inspecting relation for settings prefixed with relation_prefix.
464 This is useful for parsing access for multiple databases returned via
465@@ -122,6 +124,7 @@
466 self.relation_prefix = relation_prefix
467 self.database = database
468 self.user = user
469+ self.ssl_dir = ssl_dir
470
471 def __call__(self):
472 self.database = self.database or config('database')
473@@ -139,17 +142,72 @@
474
475 for rid in relation_ids('shared-db'):
476 for unit in related_units(rid):
477- passwd = relation_get(password_setting, rid=rid, unit=unit)
478+ rdata = relation_get(rid=rid, unit=unit)
479 ctxt = {
480- 'database_host': relation_get('db_host', rid=rid,
481- unit=unit),
482+ 'database_host': rdata.get('db_host'),
483 'database': self.database,
484 'database_user': self.user,
485- 'database_password': passwd,
486- }
487- if context_complete(ctxt):
488- return ctxt
489- return {}
490+ 'database_password': rdata.get(password_setting),
491+ 'database_type': 'mysql'
492+ }
493+ if context_complete(ctxt):
494+ db_ssl(rdata, ctxt, self.ssl_dir)
495+ return ctxt
496+ return {}
497+
498+
499+class PostgresqlDBContext(OSContextGenerator):
500+ interfaces = ['pgsql-db']
501+
502+ def __init__(self, database=None):
503+ self.database = database
504+
505+ def __call__(self):
506+ self.database = self.database or config('database')
507+ if self.database is None:
508+ log('Could not generate postgresql_db context. '
509+ 'Missing required charm config options. '
510+ '(database name)')
511+ raise OSContextError
512+ ctxt = {}
513+
514+ for rid in relation_ids(self.interfaces[0]):
515+ for unit in related_units(rid):
516+ ctxt = {
517+ 'database_host': relation_get('host', rid=rid, unit=unit),
518+ 'database': self.database,
519+ 'database_user': relation_get('user', rid=rid, unit=unit),
520+ 'database_password': relation_get('password', rid=rid, unit=unit),
521+ 'database_type': 'postgresql',
522+ }
523+ if context_complete(ctxt):
524+ return ctxt
525+ return {}
526+
527+
528+def db_ssl(rdata, ctxt, ssl_dir):
529+ if 'ssl_ca' in rdata and ssl_dir:
530+ ca_path = os.path.join(ssl_dir, 'db-client.ca')
531+ with open(ca_path, 'w') as fh:
532+ fh.write(b64decode(rdata['ssl_ca']))
533+ ctxt['database_ssl_ca'] = ca_path
534+ elif 'ssl_ca' in rdata:
535+ log("Charm not setup for ssl support but ssl ca found")
536+ return ctxt
537+ if 'ssl_cert' in rdata:
538+ cert_path = os.path.join(
539+ ssl_dir, 'db-client.cert')
540+ if not os.path.exists(cert_path):
541+ log("Waiting 1m for ssl client cert validity")
542+ time.sleep(60)
543+ with open(cert_path, 'w') as fh:
544+ fh.write(b64decode(rdata['ssl_cert']))
545+ ctxt['database_ssl_cert'] = cert_path
546+ key_path = os.path.join(ssl_dir, 'db-client.key')
547+ with open(key_path, 'w') as fh:
548+ fh.write(b64decode(rdata['ssl_key']))
549+ ctxt['database_ssl_key'] = key_path
550+ return ctxt
551
552
553 class IdentityServiceContext(OSContextGenerator):
554@@ -161,24 +219,25 @@
555
556 for rid in relation_ids('identity-service'):
557 for unit in related_units(rid):
558+ rdata = relation_get(rid=rid, unit=unit)
559 ctxt = {
560- 'service_port': relation_get('service_port', rid=rid,
561- unit=unit),
562- 'service_host': relation_get('service_host', rid=rid,
563- unit=unit),
564- 'auth_host': relation_get('auth_host', rid=rid, unit=unit),
565- 'auth_port': relation_get('auth_port', rid=rid, unit=unit),
566- 'admin_tenant_name': relation_get('service_tenant',
567- rid=rid, unit=unit),
568- 'admin_user': relation_get('service_username', rid=rid,
569- unit=unit),
570- 'admin_password': relation_get('service_password', rid=rid,
571- unit=unit),
572- # XXX: Hard-coded http.
573- 'service_protocol': 'http',
574- 'auth_protocol': 'http',
575+ 'service_port': rdata.get('service_port'),
576+ 'service_host': rdata.get('service_host'),
577+ 'auth_host': rdata.get('auth_host'),
578+ 'auth_port': rdata.get('auth_port'),
579+ 'admin_tenant_name': rdata.get('service_tenant'),
580+ 'admin_user': rdata.get('service_username'),
581+ 'admin_password': rdata.get('service_password'),
582+ 'service_protocol':
583+ rdata.get('service_protocol') or 'http',
584+ 'auth_protocol':
585+ rdata.get('auth_protocol') or 'http',
586 }
587 if context_complete(ctxt):
588+ # NOTE(jamespage) this is required for >= icehouse
589+ # so a missing value just indicates keystone needs
590+ # upgrading
591+ ctxt['admin_tenant_id'] = rdata.get('service_tenant_id')
592 return ctxt
593 return {}
594
595@@ -186,6 +245,9 @@
596 class AMQPContext(OSContextGenerator):
597 interfaces = ['amqp']
598
599+ def __init__(self, ssl_dir=None):
600+ self.ssl_dir = ssl_dir
601+
602 def __call__(self):
603 log('Generating template context for amqp')
604 conf = config()
605@@ -196,7 +258,6 @@
606 log('Could not generate shared_db context. '
607 'Missing required charm config options: %s.' % e)
608 raise OSContextError
609-
610 ctxt = {}
611 for rid in relation_ids('amqp'):
612 ha_vip_only = False
613@@ -214,6 +275,14 @@
614 unit=unit),
615 'rabbitmq_virtual_host': vhost,
616 })
617+
618+ ssl_port = relation_get('ssl_port', rid=rid, unit=unit)
619+ if ssl_port:
620+ ctxt['rabbit_ssl_port'] = ssl_port
621+ ssl_ca = relation_get('ssl_ca', rid=rid, unit=unit)
622+ if ssl_ca:
623+ ctxt['rabbit_ssl_ca'] = ssl_ca
624+
625 if relation_get('ha_queues', rid=rid, unit=unit) is not None:
626 ctxt['rabbitmq_ha_queues'] = True
627
628@@ -221,6 +290,16 @@
629 rid=rid, unit=unit) is not None
630
631 if context_complete(ctxt):
632+ if 'rabbit_ssl_ca' in ctxt:
633+ if not self.ssl_dir:
634+ log(("Charm not setup for ssl support "
635+ "but ssl ca found"))
636+ break
637+ ca_path = os.path.join(
638+ self.ssl_dir, 'rabbit-client-ca.pem')
639+ with open(ca_path, 'w') as fh:
640+ fh.write(b64decode(ctxt['rabbit_ssl_ca']))
641+ ctxt['rabbit_ssl_ca'] = ca_path
642 # Sufficient information found = break out!
643 break
644 # Used for active/active rabbitmq >= grizzly
645@@ -391,6 +470,8 @@
646 'private_address': unit_get('private-address'),
647 'endpoints': []
648 }
649+ if is_clustered():
650+ ctxt['private_address'] = config('vip')
651 for api_port in self.external_ports:
652 ext_port = determine_apache_port(api_port)
653 int_port = determine_api_port(api_port)
654@@ -489,7 +570,7 @@
655
656 if self.plugin == 'ovs':
657 ctxt.update(self.ovs_ctxt())
658- elif self.plugin == 'nvp':
659+ elif self.plugin in ['nvp', 'nsx']:
660 ctxt.update(self.nvp_ctxt())
661
662 alchemy_flags = config('neutron-alchemy-flags')
663
664=== modified file 'hooks/charmhelpers/contrib/openstack/neutron.py'
665--- hooks/charmhelpers/contrib/openstack/neutron.py 2014-04-25 12:49:48 +0000
666+++ hooks/charmhelpers/contrib/openstack/neutron.py 2014-05-20 21:57:20 +0000
667@@ -17,6 +17,8 @@
668 kver = check_output(['uname', '-r']).strip()
669 return 'linux-headers-%s' % kver
670
671+QUANTUM_CONF_DIR = '/etc/quantum'
672+
673
674 def kernel_version():
675 """ Retrieve the current major kernel version as a tuple e.g. (3, 13) """
676@@ -35,6 +37,8 @@
677
678
679 # legacy
680+
681+
682 def quantum_plugins():
683 from charmhelpers.contrib.openstack import context
684 return {
685@@ -46,7 +50,8 @@
686 'contexts': [
687 context.SharedDBContext(user=config('neutron-database-user'),
688 database=config('neutron-database'),
689- relation_prefix='neutron')],
690+ relation_prefix='neutron',
691+ ssl_dir=QUANTUM_CONF_DIR)],
692 'services': ['quantum-plugin-openvswitch-agent'],
693 'packages': [[headers_package()] + determine_dkms_package(),
694 ['quantum-plugin-openvswitch-agent']],
695@@ -61,7 +66,8 @@
696 'contexts': [
697 context.SharedDBContext(user=config('neutron-database-user'),
698 database=config('neutron-database'),
699- relation_prefix='neutron')],
700+ relation_prefix='neutron',
701+ ssl_dir=QUANTUM_CONF_DIR)],
702 'services': [],
703 'packages': [],
704 'server_packages': ['quantum-server',
705@@ -70,6 +76,8 @@
706 }
707 }
708
709+NEUTRON_CONF_DIR = '/etc/neutron'
710+
711
712 def neutron_plugins():
713 from charmhelpers.contrib.openstack import context
714@@ -83,7 +91,8 @@
715 'contexts': [
716 context.SharedDBContext(user=config('neutron-database-user'),
717 database=config('neutron-database'),
718- relation_prefix='neutron')],
719+ relation_prefix='neutron',
720+ ssl_dir=NEUTRON_CONF_DIR)],
721 'services': ['neutron-plugin-openvswitch-agent'],
722 'packages': [[headers_package()] + determine_dkms_package(),
723 ['neutron-plugin-openvswitch-agent']],
724@@ -98,20 +107,37 @@
725 'contexts': [
726 context.SharedDBContext(user=config('neutron-database-user'),
727 database=config('neutron-database'),
728- relation_prefix='neutron')],
729+ relation_prefix='neutron',
730+ ssl_dir=NEUTRON_CONF_DIR)],
731 'services': [],
732 'packages': [],
733 'server_packages': ['neutron-server',
734 'neutron-plugin-nicira'],
735 'server_services': ['neutron-server']
736+ },
737+ 'nsx': {
738+ 'config': '/etc/neutron/plugins/vmware/nsx.ini',
739+ 'driver': 'vmware',
740+ 'contexts': [
741+ context.SharedDBContext(user=config('neutron-database-user'),
742+ database=config('neutron-database'),
743+ relation_prefix='neutron',
744+ ssl_dir=NEUTRON_CONF_DIR)],
745+ 'services': [],
746+ 'packages': [],
747+ 'server_packages': ['neutron-server',
748+ 'neutron-plugin-vmware'],
749+ 'server_services': ['neutron-server']
750 }
751 }
752- # NOTE: patch in ml2 plugin for icehouse onwards
753 if release >= 'icehouse':
754+ # NOTE: patch in ml2 plugin for icehouse onwards
755 plugins['ovs']['config'] = '/etc/neutron/plugins/ml2/ml2_conf.ini'
756 plugins['ovs']['driver'] = 'neutron.plugins.ml2.plugin.Ml2Plugin'
757 plugins['ovs']['server_packages'] = ['neutron-server',
758 'neutron-plugin-ml2']
759+ # NOTE: patch in vmware renames nvp->nsx for icehouse onwards
760+ plugins['nvp'] = plugins['nsx']
761 return plugins
762
763
764
765=== modified file 'hooks/charmhelpers/contrib/openstack/utils.py'
766--- hooks/charmhelpers/contrib/openstack/utils.py 2014-04-25 12:49:48 +0000
767+++ hooks/charmhelpers/contrib/openstack/utils.py 2014-05-20 21:57:20 +0000
768@@ -65,6 +65,7 @@
769 ('1.10.0', 'havana'),
770 ('1.9.1', 'havana'),
771 ('1.9.0', 'havana'),
772+ ('1.13.1', 'icehouse'),
773 ('1.13.0', 'icehouse'),
774 ('1.12.0', 'icehouse'),
775 ('1.11.0', 'icehouse'),
776@@ -130,6 +131,11 @@
777 def get_os_codename_package(package, fatal=True):
778 '''Derive OpenStack release codename from an installed package.'''
779 apt.init()
780+
781+ # Tell apt to build an in-memory cache to prevent race conditions (if
782+ # another process is already building the cache).
783+ apt.config.set("Dir::Cache::pkgcache", "")
784+
785 cache = apt.Cache()
786
787 try:
788@@ -182,7 +188,7 @@
789 if cname == codename:
790 return version
791 #e = "Could not determine OpenStack version for package: %s" % pkg
792- #error_out(e)
793+ # error_out(e)
794
795
796 os_rel = None
797@@ -400,6 +406,8 @@
798 rtype = 'PTR'
799 elif isinstance(address, basestring):
800 rtype = 'A'
801+ else:
802+ return None
803
804 answers = dns.resolver.query(address, rtype)
805 if answers:
806
807=== modified file 'hooks/charmhelpers/contrib/storage/linux/lvm.py'
808--- hooks/charmhelpers/contrib/storage/linux/lvm.py 2014-04-25 12:49:48 +0000
809+++ hooks/charmhelpers/contrib/storage/linux/lvm.py 2014-05-20 21:57:20 +0000
810@@ -62,7 +62,7 @@
811 pvd = check_output(['pvdisplay', block_device]).splitlines()
812 for l in pvd:
813 if l.strip().startswith('VG Name'):
814- vg = ' '.join(l.split()).split(' ').pop()
815+ vg = ' '.join(l.strip().split()[2:])
816 return vg
817
818
819
820=== modified file 'hooks/charmhelpers/contrib/storage/linux/utils.py'
821--- hooks/charmhelpers/contrib/storage/linux/utils.py 2014-04-25 12:49:48 +0000
822+++ hooks/charmhelpers/contrib/storage/linux/utils.py 2014-05-20 21:57:20 +0000
823@@ -1,8 +1,11 @@
824-from os import stat
825+import os
826+import re
827 from stat import S_ISBLK
828
829 from subprocess import (
830- check_call
831+ check_call,
832+ check_output,
833+ call
834 )
835
836
837@@ -12,7 +15,9 @@
838
839 :returns: boolean: True if path is a block device, False if not.
840 '''
841- return S_ISBLK(stat(path).st_mode)
842+ if not os.path.exists(path):
843+ return False
844+ return S_ISBLK(os.stat(path).st_mode)
845
846
847 def zap_disk(block_device):
848@@ -22,5 +27,23 @@
849
850 :param block_device: str: Full path of block device to clean.
851 '''
852- check_call(['sgdisk', '--zap-all', '--clear',
853- '--mbrtogpt', block_device])
854+ # sometimes sgdisk exits non-zero; this is OK, dd will clean up
855+ call(['sgdisk', '--zap-all', '--mbrtogpt',
856+ '--clear', block_device])
857+ dev_end = check_output(['blockdev', '--getsz', block_device])
858+ gpt_end = int(dev_end.split()[0]) - 100
859+ check_call(['dd', 'if=/dev/zero', 'of=%s' % (block_device),
860+ 'bs=1M', 'count=1'])
861+ check_call(['dd', 'if=/dev/zero', 'of=%s' % (block_device),
862+ 'bs=512', 'count=100', 'seek=%s' % (gpt_end)])
863+
864+def is_device_mounted(device):
865+ '''Given a device path, return True if that device is mounted, and False
866+ if it isn't.
867+
868+ :param device: str: Full path of the device to check.
869+ :returns: boolean: True if the path represents a mounted device, False if
870+ it doesn't.
871+ '''
872+ out = check_output(['mount'])
873+ return bool(re.search(device + r"[0-9]+\b", out))
874
875=== modified file 'hooks/charmhelpers/core/hookenv.py'
876--- hooks/charmhelpers/core/hookenv.py 2014-03-28 22:37:40 +0000
877+++ hooks/charmhelpers/core/hookenv.py 2014-05-20 21:57:20 +0000
878@@ -155,6 +155,100 @@
879 return os.path.basename(sys.argv[0])
880
881
882+class Config(dict):
883+ """A Juju charm config dictionary that can write itself to
884+ disk (as json) and track which values have changed since
885+ the previous hook invocation.
886+
887+ Do not instantiate this object directly - instead call
888+ ``hookenv.config()``
889+
890+ Example usage::
891+
892+ >>> # inside a hook
893+ >>> from charmhelpers.core import hookenv
894+ >>> config = hookenv.config()
895+ >>> config['foo']
896+ 'bar'
897+ >>> config['mykey'] = 'myval'
898+ >>> config.save()
899+
900+
901+ >>> # user runs `juju set mycharm foo=baz`
902+ >>> # now we're inside subsequent config-changed hook
903+ >>> config = hookenv.config()
904+ >>> config['foo']
905+ 'baz'
906+ >>> # test to see if this val has changed since last hook
907+ >>> config.changed('foo')
908+ True
909+ >>> # what was the previous value?
910+ >>> config.previous('foo')
911+ 'bar'
912+ >>> # keys/values that we add are preserved across hooks
913+ >>> config['mykey']
914+ 'myval'
915+ >>> # don't forget to save at the end of hook!
916+ >>> config.save()
917+
918+ """
919+ CONFIG_FILE_NAME = '.juju-persistent-config'
920+
921+ def __init__(self, *args, **kw):
922+ super(Config, self).__init__(*args, **kw)
923+ self._prev_dict = None
924+ self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
925+ if os.path.exists(self.path):
926+ self.load_previous()
927+
928+ def load_previous(self, path=None):
929+ """Load previous copy of config from disk so that current values
930+ can be compared to previous values.
931+
932+ :param path:
933+
934+ File path from which to load the previous config. If `None`,
935+ config is loaded from the default location. If `path` is
936+ specified, subsequent `save()` calls will write to the same
937+ path.
938+
939+ """
940+ self.path = path or self.path
941+ with open(self.path) as f:
942+ self._prev_dict = json.load(f)
943+
944+ def changed(self, key):
945+ """Return true if the value for this key has changed since
946+ the last save.
947+
948+ """
949+ if self._prev_dict is None:
950+ return True
951+ return self.previous(key) != self.get(key)
952+
953+ def previous(self, key):
954+ """Return previous value for this key, or None if there
955+ is no "previous" value.
956+
957+ """
958+ if self._prev_dict:
959+ return self._prev_dict.get(key)
960+ return None
961+
962+ def save(self):
963+ """Save this config to disk.
964+
965+ Preserves items in _prev_dict that do not exist in self.
966+
967+ """
968+ if self._prev_dict:
969+ for k, v in self._prev_dict.iteritems():
970+ if k not in self:
971+ self[k] = v
972+ with open(self.path, 'w') as f:
973+ json.dump(self, f)
974+
975+
976 @cached
977 def config(scope=None):
978 """Juju charm configuration"""
979@@ -163,7 +257,10 @@
980 config_cmd_line.append(scope)
981 config_cmd_line.append('--format=json')
982 try:
983- return json.loads(subprocess.check_output(config_cmd_line))
984+ config_data = json.loads(subprocess.check_output(config_cmd_line))
985+ if scope is not None:
986+ return config_data
987+ return Config(config_data)
988 except ValueError:
989 return None
990
991
992=== modified file 'hooks/charmhelpers/core/host.py'
993--- hooks/charmhelpers/core/host.py 2014-03-28 22:37:40 +0000
994+++ hooks/charmhelpers/core/host.py 2014-05-20 21:57:20 +0000
995@@ -12,6 +12,9 @@
996 import string
997 import subprocess
998 import hashlib
999+import shutil
1000+import apt_pkg
1001+from contextlib import contextmanager
1002
1003 from collections import OrderedDict
1004
1005@@ -143,6 +146,16 @@
1006 target.write(content)
1007
1008
1009+def copy_file(src, dst, owner='root', group='root', perms=0444):
1010+ """Create or overwrite a file with the contents of another file"""
1011+ log("Writing file {} {}:{} {:o} from {}".format(dst, owner, group, perms, src))
1012+ uid = pwd.getpwnam(owner).pw_uid
1013+ gid = grp.getgrnam(group).gr_gid
1014+ shutil.copyfile(src, dst)
1015+ os.chown(dst, uid, gid)
1016+ os.chmod(dst, perms)
1017+
1018+
1019 def mount(device, mountpoint, options=None, persist=False):
1020 """Mount a filesystem at a particular mountpoint"""
1021 cmd_args = ['mount']
1022@@ -295,3 +308,37 @@
1023 if 'link/ether' in words:
1024 hwaddr = words[words.index('link/ether') + 1]
1025 return hwaddr
1026+
1027+
1028+def cmp_pkgrevno(package, revno, pkgcache=None):
1029+ '''Compare supplied revno with the revno of the installed package
1030+ 1 => Installed revno is greater than supplied arg
1031+ 0 => Installed revno is the same as supplied arg
1032+ -1 => Installed revno is less than supplied arg
1033+ '''
1034+ if not pkgcache:
1035+ apt_pkg.init()
1036+ pkgcache = apt_pkg.Cache()
1037+ pkg = pkgcache[package]
1038+ return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
1039+
1040+
1041+@contextmanager
1042+def chdir(d):
1043+ cur = os.getcwd()
1044+ try:
1045+ yield os.chdir(d)
1046+ finally:
1047+ os.chdir(cur)
1048+
1049+
1050+def chownr(path, owner, group):
1051+ uid = pwd.getpwnam(owner).pw_uid
1052+ gid = grp.getgrnam(group).gr_gid
1053+
1054+ for root, dirs, files in os.walk(path):
1055+ for name in dirs + files:
1056+ full = os.path.join(root, name)
1057+ broken_symlink = os.path.lexists(full) and not os.path.exists(full)
1058+ if not broken_symlink:
1059+ os.chown(full, uid, gid)
1060
1061=== added file 'hooks/charmhelpers/core/services.py'
1062--- hooks/charmhelpers/core/services.py 1970-01-01 00:00:00 +0000
1063+++ hooks/charmhelpers/core/services.py 2014-05-20 21:57:20 +0000
1064@@ -0,0 +1,84 @@
1065+from charmhelpers.core import templating
1066+from charmhelpers.core import host
1067+
1068+
1069+SERVICES = {}
1070+
1071+
1072+def register(services, templates_dir=None):
1073+ """
1074+ Register a list of service configs.
1075+
1076+ Service Configs are dicts in the following formats:
1077+
1078+ {
1079+ "service": <service name>,
1080+ "templates": [ {
1081+ 'target': <render target of template>,
1082+ 'source': <optional name of template in passed in templates_dir>
1083+ 'file_properties': <optional dict taking owner and octal mode>
1084+ 'contexts': [ context generators, see contexts.py ]
1085+ }
1086+ ] }
1087+
1088+ Either `source` or `target` must be provided.
1089+
1090+ If 'source' is not provided for a template the templates_dir will
1091+ be consulted for ``basename(target).j2``.
1092+
1093+ If `target` is not provided, it will be assumed to be
1094+ ``/etc/init/<service name>.conf``.
1095+ """
1096+ for service in services:
1097+ service.setdefault('templates_dir', templates_dir)
1098+ SERVICES[service['service']] = service
1099+
1100+
1101+def reconfigure_services(restart=True):
1102+ """
1103+ Update all files for all services and optionally restart them, if ready.
1104+ """
1105+ for service_name in SERVICES.keys():
1106+ reconfigure_service(service_name, restart=restart)
1107+
1108+
1109+def reconfigure_service(service_name, restart=True):
1110+ """
1111+ Update all files for a single service and optionally restart it, if ready.
1112+ """
1113+ service = SERVICES.get(service_name)
1114+ if not service or service['service'] != service_name:
1115+ raise KeyError('Service not registered: %s' % service_name)
1116+
1117+ manager_type = service.get('type', UpstartService)
1118+ manager_type(service).reconfigure(restart)
1119+
1120+
1121+def stop_services():
1122+ for service_name in SERVICES.keys():
1123+ if host.service_running(service_name):
1124+ host.service_stop(service_name)
1125+
1126+
1127+class ServiceTypeManager(object):
1128+ def __init__(self, service_definition):
1129+ self.service_name = service_definition['service']
1130+ self.templates = service_definition['templates']
1131+ self.templates_dir = service_definition['templates_dir']
1132+
1133+ def reconfigure(self, restart=True):
1134+ raise NotImplementedError()
1135+
1136+
1137+class UpstartService(ServiceTypeManager):
1138+ def __init__(self, service_definition):
1139+ super(UpstartService, self).__init__(service_definition)
1140+ for tmpl in self.templates:
1141+ if 'target' not in tmpl:
1142+ tmpl['target'] = '/etc/init/%s.conf' % self.service_name
1143+
1144+ def reconfigure(self, restart):
1145+ complete = templating.render(self.templates, self.templates_dir)
1146+
1147+ if restart and complete:
1148+ host.service_restart(self.service_name)
1149
1150=== added file 'hooks/charmhelpers/core/templating.py'
1151--- hooks/charmhelpers/core/templating.py 1970-01-01 00:00:00 +0000
1152+++ hooks/charmhelpers/core/templating.py 2014-05-20 21:57:20 +0000
1153@@ -0,0 +1,158 @@
1154+import os
1155+import yaml
1156+
1157+from charmhelpers.core import host
1158+from charmhelpers.core import hookenv
1159+
1160+
1161+class ContextGenerator(object):
1162+ """
1163+ Base interface for template context container generators.
1164+
1165+ A template context is a dictionary that contains data needed to populate
1166+ the template. The generator instance should produce the context when
1167+ called (without arguments) by collecting information from juju (config-get,
1168+ relation-get, etc), the system, or whatever other sources are appropriate.
1169+
1170+ A context generator should only return any values if it has enough information
1171+ to provide all of its values. Any context that is missing data is considered
1172+ incomplete and will cause that template to not render until it has all of its
1173+ necessary data.
1174+
1175+ The template may receive several contexts, which will be merged together,
1176+ so care should be taken in the key names.
1177+ """
1178+ def __call__(self):
1179+ raise NotImplementedError
1180+
1181+
1182+class StorableContext(object):
1183+ """
1184+ A mixin for persisting a context to disk.
1185+ """
1186+ def store_context(self, file_name, config_data):
1187+ with open(file_name, 'w') as file_stream:
1188+ yaml.dump(config_data, file_stream)
1189+
1190+ def read_context(self, file_name):
1191+ with open(file_name, 'r') as file_stream:
1192+ data = yaml.load(file_stream)
1193+ if not data:
1194+ raise OSError("%s is empty" % file_name)
1195+ return data
1196+
1197+
1198+class ConfigContext(ContextGenerator):
1199+ """
1200+ A context generator that generates a context containing all of the
1201+ juju config values.
1202+ """
1203+ def __call__(self):
1204+ return hookenv.config()
1205+
1206+
1207+class RelationContext(ContextGenerator):
1208+ """
1209+ Base class for a context generator that gets relation data from juju.
1210+
1211+ Subclasses must provide `interface`, which is the interface type of interest,
1212+ and `required_keys`, which is the set of keys required for the relation to
1213+ be considered complete. The first relation for the interface that is complete
1214+ will be used to populate the data for template.
1215+
1216+ The generated context will be namespaced under the interface type, to prevent
1217+ potential naming conflicts.
1218+ """
1219+ interface = None
1220+ required_keys = []
1221+
1222+ def __call__(self):
1223+ if not hookenv.relation_ids(self.interface):
1224+ return {}
1225+
1226+ ctx = {}
1227+ for rid in hookenv.relation_ids(self.interface):
1228+ for unit in hookenv.related_units(rid):
1229+ reldata = hookenv.relation_get(rid=rid, unit=unit)
1230+ required = set(self.required_keys)
1231+ if set(reldata.keys()).issuperset(required):
1232+ ns = ctx.setdefault(self.interface, {})
1233+ for k, v in reldata.items():
1234+ ns[k] = v
1235+ return ctx
1236+
1237+ return {}
1238+
1239+
1240+class StaticContext(ContextGenerator):
1241+ def __init__(self, data):
1242+ self.data = data
1243+
1244+ def __call__(self):
1245+ return self.data
1246+
1247+
1248+def _collect_contexts(context_providers):
1249+ """
1250+ Helper function to collect and merge contexts from a list of providers.
1251+
1252+ If any of the contexts are incomplete (i.e., they return an empty dict),
1253+ the template is considered incomplete and will not render.
1254+ """
1255+ ctx = {}
1256+ for provider in context_providers:
1257+ c = provider()
1258+ if not c:
1259+ return False
1260+ ctx.update(c)
1261+ return ctx
1262+
1263+
1264+def render(template_definitions, templates_dir=None):
1265+ """
1266+ Render one or more templates, given a list of template definitions.
1267+
1268+ The template definitions should be dicts with the keys: `source`, `target`,
1269+ `file_properties`, and `contexts`.
1270+
1271+ The `source` path, if not absolute, is relative to the `templates_dir`
1272+ given when the rendered was created. If `source` is not provided
1273+ for a template the `template_dir` will be consulted for
1274+ ``basename(target).j2``.
1275+
1276+ The `target` path should be absolute.
1277+
1278+ The `file_properties` should be a dict optionally containing
1279+ `owner`, `group`, or `perms` options, to be passed to `write_file`.
1280+
1281+ The `contexts` should be a list containing zero or more ContextGenerators.
1282+
1283+ The `template_dir` defaults to `$CHARM_DIR/templates`
1284+
1285+ Returns True if all of the templates were "complete" (i.e., the context
1286+ generators were able to collect the information needed to render the
1287+ template) and were rendered.
1288+ """
1289+ # lazy import jinja2 in case templating is needed in install hook
1290+ from jinja2 import FileSystemLoader, Environment, exceptions
1291+ all_complete = True
1292+ if templates_dir is None:
1293+ templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
1294+ loader = Environment(loader=FileSystemLoader(templates_dir))
1295+ for tmpl in template_definitions:
1296+ ctx = _collect_contexts(tmpl.get('contexts', []))
1297+ if ctx is False:
1298+ all_complete = False
1299+ continue
1300+ try:
1301+ source = tmpl.get('source', os.path.basename(tmpl['target'])+'.j2')
1302+ template = loader.get_template(source)
1303+ except exceptions.TemplateNotFound as e:
1304+ hookenv.log('Could not load template %s from %s.' %
1305+ (tmpl['source'], templates_dir),
1306+ level=hookenv.ERROR)
1307+ raise e
1308+ content = template.render(ctx)
1309+ host.mkdir(os.path.dirname(tmpl['target']))
1310+ host.write_file(tmpl['target'], content, **tmpl.get('file_properties', {}))
1311+ return all_complete
1312
1313=== modified file 'hooks/charmhelpers/fetch/__init__.py'
1314--- hooks/charmhelpers/fetch/__init__.py 2014-04-25 12:49:48 +0000
1315+++ hooks/charmhelpers/fetch/__init__.py 2014-05-20 21:57:20 +0000
1316@@ -1,4 +1,5 @@
1317 import importlib
1318+import time
1319 from yaml import safe_load
1320 from charmhelpers.core.host import (
1321 lsb_release
1322@@ -15,6 +16,7 @@
1323 import apt_pkg
1324 import os
1325
1326+
1327 CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
1328 deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
1329 """
1330@@ -56,10 +58,62 @@
1331 'precise-proposed/icehouse': 'precise-proposed/icehouse',
1332 }
1333
1334+# The order of this list is very important. Handlers should be listed in from
1335+# least- to most-specific URL matching.
1336+FETCH_HANDLERS = (
1337+ 'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
1338+ 'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
1339+)
1340+
1341+APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
1342+APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
1343+APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
1344+
1345+
1346+class SourceConfigError(Exception):
1347+ pass
1348+
1349+
1350+class UnhandledSource(Exception):
1351+ pass
1352+
1353+
1354+class AptLockError(Exception):
1355+ pass
1356+
1357+
1358+class BaseFetchHandler(object):
1359+
1360+ """Base class for FetchHandler implementations in fetch plugins"""
1361+
1362+ def can_handle(self, source):
1363+ """Returns True if the source can be handled. Otherwise returns
1364+ a string explaining why it cannot"""
1365+ return "Wrong source type"
1366+
1367+ def install(self, source):
1368+ """Try to download and unpack the source. Return the path to the
1369+ unpacked files or raise UnhandledSource."""
1370+ raise UnhandledSource("Wrong source type {}".format(source))
1371+
1372+ def parse_url(self, url):
1373+ return urlparse(url)
1374+
1375+ def base_url(self, url):
1376+ """Return url without querystring or fragment"""
1377+ parts = list(self.parse_url(url))
1378+ parts[4:] = ['' for i in parts[4:]]
1379+ return urlunparse(parts)
1380+
1381
1382 def filter_installed_packages(packages):
1383 """Returns a list of packages that require installation"""
1384 apt_pkg.init()
1385+
1386+ # Tell apt to build an in-memory cache to prevent race conditions (if
1387+ # another process is already building the cache).
1388+ apt_pkg.config.set("Dir::Cache::pkgcache", "")
1389+
1390 cache = apt_pkg.Cache()
1391 _pkgs = []
1392 for package in packages:
1393@@ -87,14 +141,7 @@
1394 cmd.extend(packages)
1395 log("Installing {} with options: {}".format(packages,
1396 options))
1397- env = os.environ.copy()
1398- if 'DEBIAN_FRONTEND' not in env:
1399- env['DEBIAN_FRONTEND'] = 'noninteractive'
1400-
1401- if fatal:
1402- subprocess.check_call(cmd, env=env)
1403- else:
1404- subprocess.call(cmd, env=env)
1405+ _run_apt_command(cmd, fatal)
1406
1407
1408 def apt_upgrade(options=None, fatal=False, dist=False):
1409@@ -109,24 +156,13 @@
1410 else:
1411 cmd.append('upgrade')
1412 log("Upgrading with options: {}".format(options))
1413-
1414- env = os.environ.copy()
1415- if 'DEBIAN_FRONTEND' not in env:
1416- env['DEBIAN_FRONTEND'] = 'noninteractive'
1417-
1418- if fatal:
1419- subprocess.check_call(cmd, env=env)
1420- else:
1421- subprocess.call(cmd, env=env)
1422+ _run_apt_command(cmd, fatal)
1423
1424
1425 def apt_update(fatal=False):
1426 """Update local apt cache"""
1427 cmd = ['apt-get', 'update']
1428- if fatal:
1429- subprocess.check_call(cmd)
1430- else:
1431- subprocess.call(cmd)
1432+ _run_apt_command(cmd, fatal)
1433
1434
1435 def apt_purge(packages, fatal=False):
1436@@ -137,10 +173,7 @@
1437 else:
1438 cmd.extend(packages)
1439 log("Purging {}".format(packages))
1440- if fatal:
1441- subprocess.check_call(cmd)
1442- else:
1443- subprocess.call(cmd)
1444+ _run_apt_command(cmd, fatal)
1445
1446
1447 def apt_hold(packages, fatal=False):
1448@@ -151,6 +184,7 @@
1449 else:
1450 cmd.extend(packages)
1451 log("Holding {}".format(packages))
1452+
1453 if fatal:
1454 subprocess.check_call(cmd)
1455 else:
1456@@ -184,14 +218,10 @@
1457 apt.write(PROPOSED_POCKET.format(release))
1458 if key:
1459 subprocess.check_call(['apt-key', 'adv', '--keyserver',
1460- 'keyserver.ubuntu.com', '--recv',
1461+ 'hkp://keyserver.ubuntu.com:80', '--recv',
1462 key])
1463
1464
1465-class SourceConfigError(Exception):
1466- pass
1467-
1468-
1469 def configure_sources(update=False,
1470 sources_var='install_sources',
1471 keys_var='install_keys'):
1472@@ -224,17 +254,6 @@
1473 if update:
1474 apt_update(fatal=True)
1475
1476-# The order of this list is very important. Handlers should be listed in from
1477-# least- to most-specific URL matching.
1478-FETCH_HANDLERS = (
1479- 'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
1480- 'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
1481-)
1482-
1483-
1484-class UnhandledSource(Exception):
1485- pass
1486-
1487
1488 def install_remote(source):
1489 """
1490@@ -265,30 +284,6 @@
1491 return install_remote(source)
1492
1493
1494-class BaseFetchHandler(object):
1495-
1496- """Base class for FetchHandler implementations in fetch plugins"""
1497-
1498- def can_handle(self, source):
1499- """Returns True if the source can be handled. Otherwise returns
1500- a string explaining why it cannot"""
1501- return "Wrong source type"
1502-
1503- def install(self, source):
1504- """Try to download and unpack the source. Return the path to the
1505- unpacked files or raise UnhandledSource."""
1506- raise UnhandledSource("Wrong source type {}".format(source))
1507-
1508- def parse_url(self, url):
1509- return urlparse(url)
1510-
1511- def base_url(self, url):
1512- """Return url without querystring or fragment"""
1513- parts = list(self.parse_url(url))
1514- parts[4:] = ['' for i in parts[4:]]
1515- return urlunparse(parts)
1516-
1517-
1518 def plugins(fetch_handlers=None):
1519 if not fetch_handlers:
1520 fetch_handlers = FETCH_HANDLERS
1521@@ -306,3 +301,40 @@
1522 log("FetchHandler {} not found, skipping plugin".format(
1523 handler_name))
1524 return plugin_list
1525+
1526+
1527+def _run_apt_command(cmd, fatal=False):
1528+ """
1529+ Run an APT command, checking output and retrying if the fatal flag is set
1530+ to True.
1531+
1532+ :param: cmd: str: The apt command to run.
1533+ :param: fatal: bool: Whether the command's output should be checked and
1534+ retried.
1535+ """
1536+ env = os.environ.copy()
1537+
1538+ if 'DEBIAN_FRONTEND' not in env:
1539+ env['DEBIAN_FRONTEND'] = 'noninteractive'
1540+
1541+ if fatal:
1542+ retry_count = 0
1543+ result = None
1544+
1545+ # If the command is considered "fatal", we need to retry if the apt
1546+ # lock was not acquired.
1547+
1548+ while result is None or result == APT_NO_LOCK:
1549+ try:
1550+ result = subprocess.check_call(cmd, env=env)
1551+ except subprocess.CalledProcessError, e:
1552+ retry_count = retry_count + 1
1553+ if retry_count > APT_NO_LOCK_RETRY_COUNT:
1554+ raise
1555+ result = e.returncode
1556+ log("Couldn't acquire DPKG lock. Will retry in {} seconds."
1557+ "".format(APT_NO_LOCK_RETRY_DELAY))
1558+ time.sleep(APT_NO_LOCK_RETRY_DELAY)
1559+
1560+ else:
1561+ subprocess.call(cmd, env=env)
1562
1563=== modified file 'hooks/hooks.py'
1564--- hooks/hooks.py 2014-05-08 11:54:54 +0000
1565+++ hooks/hooks.py 2014-05-20 21:57:20 +0000
1566@@ -5,10 +5,10 @@
1567 import sys
1568 from charmhelpers.core.hookenv import log
1569 from charmhelpers.contrib.cloudfoundry import contexts
1570-from charmhelpers.contrib.cloudfoundry import services
1571+from charmhelpers.core import services
1572+from charmhelpers.core import templating
1573 from charmhelpers.core import hookenv
1574-from charmhelpers.core.hookenv import unit_get
1575-from config import *
1576+import config
1577
1578
1579 LOGSVC = "loggregator"
1580@@ -19,18 +19,19 @@
1581 fileproperties = {'owner': 'vcap'}
1582 services.register([
1583 {
1584- 'service': LOGGREGATOR_JOB_NAME,
1585- 'templates': [{
1586- 'source': 'loggregator.json',
1587- 'target': LOGGREGATOR_CONFIG_PATH,
1588- 'file_properties': fileproperties,
1589- 'contexts': [
1590- contexts.StaticContext({'service_name': service_name}),
1591- contexts.ConfigContext(),
1592- contexts.NatsContext()
1593- ]
1594- }]
1595- }], os.path.join(hookenv.charm_dir(), 'templates'))
1596+ 'service': config.LOGGREGATOR_JOB_NAME,
1597+ 'templates': [
1598+ {'source': 'loggregator.conf'},
1599+ {'source': 'loggregator.json',
1600+ 'target': config.LOGGREGATOR_CONFIG_PATH,
1601+ 'file_properties': fileproperties,
1602+ 'contexts': [
1603+ templating.StaticContext({'service_name': service_name}),
1604+ templating.ConfigContext(),
1605+ contexts.NatsContext()
1606+ ]},
1607+ ]
1608+ }])
1609
1610
1611 @hooks.hook()
1612
1613=== modified file 'hooks/install' (properties changed: -x to +x)
1614--- hooks/install 2014-05-08 11:47:19 +0000
1615+++ hooks/install 2014-05-20 21:57:20 +0000
1616@@ -2,21 +2,20 @@
1617 # vim: et ai ts=4 sw=4:
1618 import os
1619 import subprocess
1620-from config import *
1621 from charmhelpers.core import hookenv, host
1622 from charmhelpers.contrib.cloudfoundry.common import (
1623 prepare_cloudfoundry_environment
1624 )
1625-from charmhelpers.contrib.cloudfoundry.upstart_helper import (
1626- install_upstart_scripts
1627-)
1628-from charmhelpers.contrib.cloudfoundry.install import install
1629+
1630+import config
1631
1632
1633 def install_charm():
1634- prepare_cloudfoundry_environment(hookenv.config(), LOGGREGATOR_PACKAGES)
1635- install_upstart_scripts()
1636- dirs = [CF_DIR, LOGGREGATOR_DIR, LOGGREGATOR_CONFIG_DIR, '/var/log/vcap']
1637+ prepare_cloudfoundry_environment(hookenv.config(), config.LOGGREGATOR_PACKAGES)
1638+ dirs = [config.CF_DIR,
1639+ config.LOGGREGATOR_DIR,
1640+ config.LOGGREGATOR_CONFIG_DIR,
1641+ '/var/log/vcap']
1642
1643 for item in dirs:
1644 host.mkdir(item, owner='vcap', group='vcap', perms=0775)
1645@@ -24,10 +23,9 @@
1646 files_dir = os.path.join(hookenv.charm_dir(), "files")
1647
1648 subprocess.check_call(["gunzip", "-k", "loggregator.gz"], cwd=files_dir)
1649-
1650- install(os.path.join(files_dir, 'loggregator'),
1651- "/usr/bin/loggregator",
1652- fileprops={'mode': '0755', 'owner': 'vcap'})
1653+ host.copy_file(os.path.join(files_dir, 'loggregator'),
1654+ '/usr/bin/loggregator',
1655+ owner='vcap', perms=0755)
1656
1657
1658 if __name__ == '__main__':
1659
1660=== renamed file 'files/upstart/loggregator.conf' => 'templates/loggregator.conf'

Subscribers

People subscribed via source and target branches