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