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

Proposed by Cory Johns
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
Reviewer Review Type Date Requested Status
Cloud Foundry Charmers Pending
Review via email: mp+219919@code.launchpad.net

Description of the change

Refactored to use refactored charm-helpers

https://codereview.appspot.com/91450050/

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

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):
   A [revision details]
   D files/config/cloud_controller.yml
   M hooks/charmhelpers/contrib/cloudfoundry/common.py
   D hooks/charmhelpers/contrib/cloudfoundry/config_helper.py
   M hooks/charmhelpers/contrib/cloudfoundry/contexts.py
   D hooks/charmhelpers/contrib/cloudfoundry/install.py
   D hooks/charmhelpers/contrib/cloudfoundry/services.py
   D hooks/charmhelpers/contrib/cloudfoundry/upstart_helper.py
   M hooks/charmhelpers/contrib/hahelpers/apache.py
   M hooks/charmhelpers/contrib/openstack/context.py
   M hooks/charmhelpers/contrib/openstack/neutron.py
   M hooks/charmhelpers/contrib/openstack/utils.py
   M hooks/charmhelpers/contrib/storage/linux/utils.py
   M hooks/charmhelpers/core/host.py
   A hooks/charmhelpers/core/services.py
   A hooks/charmhelpers/core/templating.py
   M hooks/charmhelpers/fetch/__init__.py
   M hooks/hooks.py
   M hooks/install
   M templates/cf-cloudcontroller.conf
   M templates/cf-cloudcontroller-job.conf

29. By Cory Johns

Merged require-mysql branch

30. By Cory Johns

Synced refactored charm-helpers

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

https://codereview.appspot.com/91450050/diff/20001/hooks/install
File hooks/install (right):

https://codereview.appspot.com/91450050/diff/20001/hooks/install#newcode60
hooks/install:60: common.FOG_CONNECTION,
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://codereview.appspot.com/91450050/diff/20001/hooks/install#newcode69
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://codereview.appspot.com/91450050/diff/20001/templates/cloud_controller.yml
File templates/cloud_controller.yml (right):

https://codereview.appspot.com/91450050/diff/20001/templates/cloud_controller.yml#newcode57
templates/cloud_controller.yml:57: database: {{db['dsn']}}
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?

https://codereview.appspot.com/91450050/

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

On 2014/05/19 10:19:34, lomov.as wrote:

https://codereview.appspot.com/91450050/diff/20001/hooks/install#newcode60
> hooks/install:60: common.FOG_CONNECTION,
> 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://codereview.appspot.com/91450050/diff/20001/hooks/install#newcode69
> 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://codereview.appspot.com/91450050/diff/20001/templates/cloud_controller.yml#newcode57
> templates/cloud_controller.yml:57: database: {{db['dsn']}}
> 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://codereview.appspot.com/91450050/diff/20001/hooks/charmhelpers/contrib/cloudfoundry/contexts.py#newcode29.
  It builds the MySQL DSN from the relation data and the context is used
(https://codereview.appspot.com/91450050/diff/20001/hooks/hooks.py#newcode26)
to populate the DSN in the template.

https://codereview.appspot.com/91450050/

Revision history for this message
Cory Johns (johnsca) wrote :
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

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

LGTM, thanks

https://codereview.appspot.com/91450050/diff/20001/hooks/install
File hooks/install (right):

https://codereview.appspot.com/91450050/diff/20001/hooks/install#newcode60
hooks/install:60: common.FOG_CONNECTION,
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://codereview.appspot.com/91450050/diff/40001/hooks/common.py
File hooks/common.py (right):

https://codereview.appspot.com/91450050/diff/40001/hooks/common.py#newcode34
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://codereview.appspot.com/91450050/diff/40001/hooks/hooks.py
File hooks/hooks.py (left):

https://codereview.appspot.com/91450050/diff/40001/hooks/hooks.py#oldcode44
hooks/hooks.py:44:
This will make more sense w/o sqlite as you said on IRC, today its lossy

https://codereview.appspot.com/91450050/diff/40001/metadata.yaml
File metadata.yaml (right):

https://codereview.appspot.com/91450050/diff/40001/metadata.yaml#newcode22
metadata.yaml:22: optional: true
Thanks for adding this

https://codereview.appspot.com/91450050/

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

https://codereview.appspot.com/91450050/diff/20001/hooks/install
File hooks/install (right):

https://codereview.appspot.com/91450050/diff/20001/hooks/install#newcode60
hooks/install:60: common.FOG_CONNECTION,
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.

https://codereview.appspot.com/91450050/

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

On 2014/05/19 19:20:32, cory.johns wrote:
> https://codereview.appspot.com/91450050/diff/20001/hooks/install
> File hooks/install (right):

https://codereview.appspot.com/91450050/diff/20001/hooks/install#newcode60
> hooks/install:60: common.FOG_CONNECTION,
> 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

https://codereview.appspot.com/91450050/

32. By Cory Johns

Resynced charm-helpers

33. By Cory Johns

Inlined db_migrate and renamed common -> config, per review

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

*** Submitted:

Refactored to use refactored charm-helpers

R=lomov.as, cory.johns, benjamin.saller
CC=
https://codereview.appspot.com/91450050

https://codereview.appspot.com/91450050/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed directory 'files'
2=== removed directory 'files/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

Subscribers

People subscribed via source and target branches