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