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