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

Proposed by Cory Johns
Status: Merged
Merged at revision: 27
Proposed branch: lp:~johnsca/charms/trusty/cf-go-router/refactor
Merge into: lp:~cf-charmers/charms/trusty/cf-go-router/trunk
Diff against target: 1628 lines (+720/-430)
19 files modified
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/-14)
hooks/install (+6/-10)
To merge this branch: bzr merge lp:~johnsca/charms/trusty/cf-go-router/refactor
Reviewer Review Type Date Requested Status
Cloud Foundry Charmers Pending
Review via email: mp+219914@code.launchpad.net

Description of the change

Refactored to use refactored charm-helpers

https://codereview.appspot.com/96360049/

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

Reviewers: mp+219914_code.launchpad.net,

Message:
Please take a look.

Description:
Refactored to use refactored charm-helpers

https://code.launchpad.net/~johnsca/charms/trusty/cf-go-router/refactor/+merge/219914

(do not edit description out of merge proposal)

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

Affected files (+462, -357 lines):
   A [revision details]
   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/gorouter.conf

Revision history for this message
Benjamin Saller (bcsaller) wrote :
28. By Cory Johns

Merged :parent

29. By Cory Johns

Resynced charm-helpers

30. By Cory Johns

Removed unnecessary prefix from config var

31. 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
CC=
https://codereview.appspot.com/96360049

https://codereview.appspot.com/96360049/

Revision history for this message
Alex Lomov (lomov-as) wrote :

I think that it would be better to rename gorouter.conf upstart job to
cf-gorouter.conf or cf-go-router.conf (I mean this change
https://codereview.appspot.com/96360049/patch/20001/30021),
just to do it in the same way with other charms.

On 21 May 2014 01:03, Cory Johns <email address hidden> wrote:

> *** Submitted:
>
> Refactored to use refactored charm-helpers
>
> R=benjamin.saller
> CC=
> https://codereview.appspot.com/96360049
>
>
> https://codereview.appspot.com/96360049/
>
> --
>
> https://code.launchpad.net/~johnsca/charms/trusty/cf-go-router/refactor/+merge/219914
> Your team Cloud Foundry Charmers is requested to review the proposed merge
> of lp:~johnsca/charms/trusty/cf-go-router/refactor into
> lp:~cf-charmers/charms/trusty/cf-go-router/trunk.
>
> --
> Mailing list: https://launchpad.net/~cf-charmers
> Post to : <email address hidden>
> Unsubscribe : https://launchpad.net/~cf-charmers
> More help : https://help.launchpad.net/ListHelp
>

Preview Diff

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

Subscribers

People subscribed via source and target branches