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

Proposed by Cory Johns
Status: Merged
Merged at revision: 30
Proposed branch: lp:~johnsca/charms/trusty/cf-uaa/refactor
Merge into: lp:~cf-charmers/charms/trusty/cf-uaa/trunk
Diff against target: 1692 lines (+749/-448)
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 (+24/-19)
hooks/install (+27/-23)
To merge this branch: bzr merge lp:~johnsca/charms/trusty/cf-uaa/refactor
Reviewer Review Type Date Requested Status
Cloud Foundry Charmers Pending
Review via email: mp+219918@code.launchpad.net

Description of the change

Refactored to use refactored charm-helpers

https://codereview.appspot.com/91450049/

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

Reviewers: mp+219918_code.launchpad.net,

Message:
Please take a look.

Description:
Refactored to use refactored charm-helpers

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

(do not edit description out of merge proposal)

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

Affected files (+491, -375 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/cf-registrar.conf
   M templates/cf-uaa.conf

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

LGTM

Thanks, I would consider just using config. or common. for the import
namespace of constants

https://codereview.appspot.com/91450049/diff/1/hooks/install
File hooks/install (right):

https://codereview.appspot.com/91450049/diff/1/hooks/install#newcode13
hooks/install:13: import config as uaa_config
just config. is fine again

https://codereview.appspot.com/91450049/diff/1/hooks/install#newcode43
hooks/install:43: 'sqlite-jdbc-3.7.2.jar'])
Hopefully we can drop this once we move to mysql

https://codereview.appspot.com/91450049/

31. By Cory Johns

Merged :parent

32. By Cory Johns

Resynced charm-helpers

33. By Cory Johns

Removed unnecessary prefix from config var

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

*** Submitted:

Refactored to use refactored charm-helpers

R=benjamin.saller
CC=
https://codereview.appspot.com/91450049

https://codereview.appspot.com/91450049/

Preview Diff

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

Subscribers

People subscribed via source and target branches