Merge lp:~johnsca/charm-helpers/services-framework into lp:charm-helpers

Proposed by Cory Johns
Status: Merged
Merged at revision: 191
Proposed branch: lp:~johnsca/charm-helpers/services-framework
Merge into: lp:charm-helpers
Diff against target: 1537 lines (+1445/-2)
12 files modified
charmhelpers/core/host.py (+34/-1)
charmhelpers/core/services/__init__.py (+2/-0)
charmhelpers/core/services/base.py (+305/-0)
charmhelpers/core/services/helpers.py (+125/-0)
charmhelpers/core/templating.py (+51/-0)
test_requirements.txt (+0/-1)
tests/core/templates/cloud_controller_ng.yml (+173/-0)
tests/core/templates/fake_cc.yml (+3/-0)
tests/core/templates/nginx.conf (+154/-0)
tests/core/templates/test.conf (+3/-0)
tests/core/test_services.py (+531/-0)
tests/core/test_templating.py (+64/-0)
To merge this branch: bzr merge lp:~johnsca/charm-helpers/services-framework
Reviewer Review Type Date Requested Status
Charles Butler (community) Approve
Tim Van Steenburgh Approve
charmers Pending
Review via email: mp+226161@code.launchpad.net

Description of the change

Split services framework off into separate merge proposal, and combined commits, for easier review.

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

This MP includes the most recent version of the Services framework, which changes the focus of charms from handling charm events (hooks) to describing the data (and sources of that data) required to set up the software and the actions to take when all of the required data is available. It also creates a standard for rendering config and services jobs based on Jinja templates, and manages re-rendering the files and restarting the services when the data changes.

This relieves the charm author of having to do things like keep track of a bunch of .foo flag files for indicating whether or not such-and-such file has been written or such-and-such service has been started.

The docstrings below are fairly complete, and some (somewhat simple) real-world example usages can be found in the Apache Allura charm (http://bazaar.launchpad.net/~johnsca/charms/precise/apache-allura/refactoring-with-tests/files) and the RethinkDB Docker charm (https://github.com/bcsaller/juju-docker/).

Revision history for this message
James Troup (elmo) wrote :

Comments inline

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

Replies / comments inline.

180. By Cory Johns

Cleanup based on review

Revision history for this message
Tim Van Steenburgh (tvansteenburgh) wrote :

+1 LGTM.

This is awesome, I can't wait to try it out! I found a few typos (see inline diff comments), but other than that, this is good-to-go. Tests all pass.

Looking forward to implementing a charm with this to get a better feel for it. You might consider adding more examples to the published docs (http://pythonhosted.org/charmhelpers/) if you get time.

Very nice work!

review: Approve
181. By Cory Johns

Fixed documentation typos, per review

Revision history for this message
Charles Butler (lazypower) wrote :

Looks good to me. I'm on teh band wagon for using this as well.

I see nothing truly heinous in the code base, and considering there are already a slew of CF charms relying on this - I'd like to get more traction. Merging so we can pilot this officially in charms and get a charm-school published on this.

Make sure you're watching the charm-helpers bug tracker for any incoming bugs related to the services framework, and thank you for helping solve some long running issues with a very unique approach to service declaration, implementation, and constraints (on a per service level via relation even!)

+1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charmhelpers/core/host.py'
--- charmhelpers/core/host.py 2014-07-23 11:18:54 +0000
+++ charmhelpers/core/host.py 2014-08-05 13:03:33 +0000
@@ -12,6 +12,8 @@
12import string12import string
13import subprocess13import subprocess
14import hashlib14import hashlib
15import shutil
16from contextlib import contextmanager
1517
16from collections import OrderedDict18from collections import OrderedDict
1719
@@ -52,7 +54,7 @@
52def service_running(service):54def service_running(service):
53 """Determine whether a system service is running"""55 """Determine whether a system service is running"""
54 try:56 try:
55 output = subprocess.check_output(['service', service, 'status'])57 output = subprocess.check_output(['service', service, 'status'], stderr=subprocess.STDOUT)
56 except subprocess.CalledProcessError:58 except subprocess.CalledProcessError:
57 return False59 return False
58 else:60 else:
@@ -62,6 +64,16 @@
62 return False64 return False
6365
6466
67def service_available(service_name):
68 """Determine whether a system service is available"""
69 try:
70 subprocess.check_output(['service', service_name, 'status'], stderr=subprocess.STDOUT)
71 except subprocess.CalledProcessError:
72 return False
73 else:
74 return True
75
76
65def adduser(username, password=None, shell='/bin/bash', system_user=False):77def adduser(username, password=None, shell='/bin/bash', system_user=False):
66 """Add a user to the system"""78 """Add a user to the system"""
67 try:79 try:
@@ -329,3 +341,24 @@
329 pkgcache = apt_pkg.Cache()341 pkgcache = apt_pkg.Cache()
330 pkg = pkgcache[package]342 pkg = pkgcache[package]
331 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)343 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)
344
345
346@contextmanager
347def chdir(d):
348 cur = os.getcwd()
349 try:
350 yield os.chdir(d)
351 finally:
352 os.chdir(cur)
353
354
355def chownr(path, owner, group):
356 uid = pwd.getpwnam(owner).pw_uid
357 gid = grp.getgrnam(group).gr_gid
358
359 for root, dirs, files in os.walk(path):
360 for name in dirs + files:
361 full = os.path.join(root, name)
362 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
363 if not broken_symlink:
364 os.chown(full, uid, gid)
332365
=== added directory 'charmhelpers/core/services'
=== added file 'charmhelpers/core/services/__init__.py'
--- charmhelpers/core/services/__init__.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/core/services/__init__.py 2014-08-05 13:03:33 +0000
@@ -0,0 +1,2 @@
1from .base import *
2from .helpers import *
03
=== added file 'charmhelpers/core/services/base.py'
--- charmhelpers/core/services/base.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/core/services/base.py 2014-08-05 13:03:33 +0000
@@ -0,0 +1,305 @@
1import os
2import re
3import json
4from collections import Iterable
5
6from charmhelpers.core import host
7from charmhelpers.core import hookenv
8
9
10__all__ = ['ServiceManager', 'ManagerCallback',
11 'PortManagerCallback', 'open_ports', 'close_ports', 'manage_ports',
12 'service_restart', 'service_stop']
13
14
15class ServiceManager(object):
16 def __init__(self, services=None):
17 """
18 Register a list of services, given their definitions.
19
20 Traditional charm authoring is focused on implementing hooks. That is,
21 the charm author is thinking in terms of "What hook am I handling; what
22 does this hook need to do?" However, in most cases, the real question
23 should be "Do I have the information I need to configure and start this
24 piece of software and, if so, what are the steps for doing so?" The
25 ServiceManager framework tries to bring the focus to the data and the
26 setup tasks, in the most declarative way possible.
27
28 Service definitions are dicts in the following formats (all keys except
29 'service' are optional):
30
31 {
32 "service": <service name>,
33 "required_data": <list of required data contexts>,
34 "data_ready": <one or more callbacks>,
35 "data_lost": <one or more callbacks>,
36 "start": <one or more callbacks>,
37 "stop": <one or more callbacks>,
38 "ports": <list of ports to manage>,
39 }
40
41 The 'required_data' list should contain dicts of required data (or
42 dependency managers that act like dicts and know how to collect the data).
43 Only when all items in the 'required_data' list are populated are the list
44 of 'data_ready' and 'start' callbacks executed. See `is_ready()` for more
45 information.
46
47 The 'data_ready' value should be either a single callback, or a list of
48 callbacks, to be called when all items in 'required_data' pass `is_ready()`.
49 Each callback will be called with the service name as the only parameter.
50 After all of the 'data_ready' callbacks are called, the 'start' callbacks
51 are fired.
52
53 The 'data_lost' value should be either a single callback, or a list of
54 callbacks, to be called when a 'required_data' item no longer passes
55 `is_ready()`. Each callback will be called with the service name as the
56 only parameter. After all of the 'data_lost' callbacks are called,
57 the 'stop' callbacks are fired.
58
59 The 'start' value should be either a single callback, or a list of
60 callbacks, to be called when starting the service, after the 'data_ready'
61 callbacks are complete. Each callback will be called with the service
62 name as the only parameter. This defaults to
63 `[host.service_start, services.open_ports]`.
64
65 The 'stop' value should be either a single callback, or a list of
66 callbacks, to be called when stopping the service. If the service is
67 being stopped because it no longer has all of its 'required_data', this
68 will be called after all of the 'data_lost' callbacks are complete.
69 Each callback will be called with the service name as the only parameter.
70 This defaults to `[services.close_ports, host.service_stop]`.
71
72 The 'ports' value should be a list of ports to manage. The default
73 'start' handler will open the ports after the service is started,
74 and the default 'stop' handler will close the ports prior to stopping
75 the service.
76
77
78 Examples:
79
80 The following registers an Upstart service called bingod that depends on
81 a mongodb relation and which runs a custom `db_migrate` function prior to
82 restarting the service, and a Runit service called spadesd.
83
84 manager = services.ServiceManager([
85 {
86 'service': 'bingod',
87 'ports': [80, 443],
88 'required_data': [MongoRelation(), config(), {'my': 'data'}],
89 'data_ready': [
90 services.template(source='bingod.conf'),
91 services.template(source='bingod.ini',
92 target='/etc/bingod.ini',
93 owner='bingo', perms=0400),
94 ],
95 },
96 {
97 'service': 'spadesd',
98 'data_ready': services.template(source='spadesd_run.j2',
99 target='/etc/sv/spadesd/run',
100 perms=0555),
101 'start': runit_start,
102 'stop': runit_stop,
103 },
104 ])
105 manager.manage()
106 """
107 self._ready_file = os.path.join(hookenv.charm_dir(), 'READY-SERVICES.json')
108 self._ready = None
109 self.services = {}
110 for service in services or []:
111 service_name = service['service']
112 self.services[service_name] = service
113
114 def manage(self):
115 """
116 Handle the current hook by doing The Right Thing with the registered services.
117 """
118 hook_name = hookenv.hook_name()
119 if hook_name == 'stop':
120 self.stop_services()
121 else:
122 self.provide_data()
123 self.reconfigure_services()
124
125 def provide_data(self):
126 hook_name = hookenv.hook_name()
127 for service in self.services.values():
128 for provider in service.get('provided_data', []):
129 if re.match(r'{}-relation-(joined|changed)'.format(provider.name), hook_name):
130 data = provider.provide_data()
131 if provider._is_ready(data):
132 hookenv.relation_set(None, data)
133
134 def reconfigure_services(self, *service_names):
135 """
136 Update all files for one or more registered services, and,
137 if ready, optionally restart them.
138
139 If no service names are given, reconfigures all registered services.
140 """
141 for service_name in service_names or self.services.keys():
142 if self.is_ready(service_name):
143 self.fire_event('data_ready', service_name)
144 self.fire_event('start', service_name, default=[
145 service_restart,
146 manage_ports])
147 self.save_ready(service_name)
148 else:
149 if self.was_ready(service_name):
150 self.fire_event('data_lost', service_name)
151 self.fire_event('stop', service_name, default=[
152 manage_ports,
153 service_stop])
154 self.save_lost(service_name)
155
156 def stop_services(self, *service_names):
157 """
158 Stop one or more registered services, by name.
159
160 If no service names are given, stops all registered services.
161 """
162 for service_name in service_names or self.services.keys():
163 self.fire_event('stop', service_name, default=[
164 manage_ports,
165 service_stop])
166
167 def get_service(self, service_name):
168 """
169 Given the name of a registered service, return its service definition.
170 """
171 service = self.services.get(service_name)
172 if not service:
173 raise KeyError('Service not registered: %s' % service_name)
174 return service
175
176 def fire_event(self, event_name, service_name, default=None):
177 """
178 Fire a data_ready, data_lost, start, or stop event on a given service.
179 """
180 service = self.get_service(service_name)
181 callbacks = service.get(event_name, default)
182 if not callbacks:
183 return
184 if not isinstance(callbacks, Iterable):
185 callbacks = [callbacks]
186 for callback in callbacks:
187 if isinstance(callback, ManagerCallback):
188 callback(self, service_name, event_name)
189 else:
190 callback(service_name)
191
192 def is_ready(self, service_name):
193 """
194 Determine if a registered service is ready, by checking its 'required_data'.
195
196 A 'required_data' item can be any mapping type, and is considered ready
197 if `bool(item)` evaluates as True.
198 """
199 service = self.get_service(service_name)
200 reqs = service.get('required_data', [])
201 return all(bool(req) for req in reqs)
202
203 def _load_ready_file(self):
204 if self._ready is not None:
205 return
206 if os.path.exists(self._ready_file):
207 with open(self._ready_file) as fp:
208 self._ready = set(json.load(fp))
209 else:
210 self._ready = set()
211
212 def _save_ready_file(self):
213 if self._ready is None:
214 return
215 with open(self._ready_file, 'w') as fp:
216 json.dump(list(self._ready), fp)
217
218 def save_ready(self, service_name):
219 """
220 Save an indicator that the given service is now data_ready.
221 """
222 self._load_ready_file()
223 self._ready.add(service_name)
224 self._save_ready_file()
225
226 def save_lost(self, service_name):
227 """
228 Save an indicator that the given service is no longer data_ready.
229 """
230 self._load_ready_file()
231 self._ready.discard(service_name)
232 self._save_ready_file()
233
234 def was_ready(self, service_name):
235 """
236 Determine if the given service was previously data_ready.
237 """
238 self._load_ready_file()
239 return service_name in self._ready
240
241
242class ManagerCallback(object):
243 """
244 Special case of a callback that takes the `ServiceManager` instance
245 in addition to the service name.
246
247 Subclasses should implement `__call__` which should accept three parameters:
248
249 * `manager` The `ServiceManager` instance
250 * `service_name` The name of the service it's being triggered for
251 * `event_name` The name of the event that this callback is handling
252 """
253 def __call__(self, manager, service_name, event_name):
254 raise NotImplementedError()
255
256
257class PortManagerCallback(ManagerCallback):
258 """
259 Callback class that will open or close ports, for use as either
260 a start or stop action.
261 """
262 def __call__(self, manager, service_name, event_name):
263 service = manager.get_service(service_name)
264 new_ports = service.get('ports', [])
265 port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
266 if os.path.exists(port_file):
267 with open(port_file) as fp:
268 old_ports = fp.read().split(',')
269 for old_port in old_ports:
270 if bool(old_port):
271 old_port = int(old_port)
272 if old_port not in new_ports:
273 hookenv.close_port(old_port)
274 with open(port_file, 'w') as fp:
275 fp.write(','.join(str(port) for port in new_ports))
276 for port in new_ports:
277 if event_name == 'start':
278 hookenv.open_port(port)
279 elif event_name == 'stop':
280 hookenv.close_port(port)
281
282
283def service_stop(service_name):
284 """
285 Wrapper around host.service_stop to prevent spurious "unknown service"
286 messages in the logs.
287 """
288 if host.service_running(service_name):
289 host.service_stop(service_name)
290
291
292def service_restart(service_name):
293 """
294 Wrapper around host.service_restart to prevent spurious "unknown service"
295 messages in the logs.
296 """
297 if host.service_available(service_name):
298 if host.service_running(service_name):
299 host.service_restart(service_name)
300 else:
301 host.service_start(service_name)
302
303
304# Convenience aliases
305open_ports = close_ports = manage_ports = PortManagerCallback()
0306
=== added file 'charmhelpers/core/services/helpers.py'
--- charmhelpers/core/services/helpers.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/core/services/helpers.py 2014-08-05 13:03:33 +0000
@@ -0,0 +1,125 @@
1from charmhelpers.core import hookenv
2from charmhelpers.core import templating
3
4from charmhelpers.core.services.base import ManagerCallback
5
6
7__all__ = ['RelationContext', 'TemplateCallback',
8 'render_template', 'template']
9
10
11class RelationContext(dict):
12 """
13 Base class for a context generator that gets relation data from juju.
14
15 Subclasses must provide the attributes `name`, which is the name of the
16 interface of interest, `interface`, which is the type of the interface of
17 interest, and `required_keys`, which is the set of keys required for the
18 relation to be considered complete. The data for all interfaces matching
19 the `name` attribute that are complete will used to populate the dictionary
20 values (see `get_data`, below).
21
22 The generated context will be namespaced under the interface type, to prevent
23 potential naming conflicts.
24 """
25 name = None
26 interface = None
27 required_keys = []
28
29 def __init__(self, *args, **kwargs):
30 super(RelationContext, self).__init__(*args, **kwargs)
31 self.get_data()
32
33 def __bool__(self):
34 """
35 Returns True if all of the required_keys are available.
36 """
37 return self.is_ready()
38
39 __nonzero__ = __bool__
40
41 def __repr__(self):
42 return super(RelationContext, self).__repr__()
43
44 def is_ready(self):
45 """
46 Returns True if all of the `required_keys` are available from any units.
47 """
48 ready = len(self.get(self.name, [])) > 0
49 if not ready:
50 hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG)
51 return ready
52
53 def _is_ready(self, unit_data):
54 """
55 Helper method that tests a set of relation data and returns True if
56 all of the `required_keys` are present.
57 """
58 return set(unit_data.keys()).issuperset(set(self.required_keys))
59
60 def get_data(self):
61 """
62 Retrieve the relation data for each unit involved in a relation and,
63 if complete, store it in a list under `self[self.name]`. This
64 is automatically called when the RelationContext is instantiated.
65
66 The units are sorted lexographically first by the service ID, then by
67 the unit ID. Thus, if an interface has two other services, 'db:1'
68 and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1',
69 and 'db:2' having one unit, 'mediawiki/0', all of which have a complete
70 set of data, the relation data for the units will be stored in the
71 order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'.
72
73 If you only care about a single unit on the relation, you can just
74 access it as `{{ interface[0]['key'] }}`. However, if you can at all
75 support multiple units on a relation, you should iterate over the list,
76 like:
77
78 {% for unit in interface -%}
79 {{ unit['key'] }}{% if not loop.last %},{% endif %}
80 {%- endfor %}
81
82 Note that since all sets of relation data from all related services and
83 units are in a single list, if you need to know which service or unit a
84 set of data came from, you'll need to extend this class to preserve
85 that information.
86 """
87 if not hookenv.relation_ids(self.name):
88 return
89
90 ns = self.setdefault(self.name, [])
91 for rid in sorted(hookenv.relation_ids(self.name)):
92 for unit in sorted(hookenv.related_units(rid)):
93 reldata = hookenv.relation_get(rid=rid, unit=unit)
94 if self._is_ready(reldata):
95 ns.append(reldata)
96
97 def provide_data(self):
98 """
99 Return data to be relation_set for this interface.
100 """
101 return {}
102
103
104class TemplateCallback(ManagerCallback):
105 """
106 Callback class that will render a template, for use as a ready action.
107 """
108 def __init__(self, source, target, owner='root', group='root', perms=0444):
109 self.source = source
110 self.target = target
111 self.owner = owner
112 self.group = group
113 self.perms = perms
114
115 def __call__(self, manager, service_name, event_name):
116 service = manager.get_service(service_name)
117 context = {}
118 for ctx in service.get('required_data', []):
119 context.update(ctx)
120 templating.render(self.source, self.target, context,
121 self.owner, self.group, self.perms)
122
123
124# Convenience aliases for templates
125render_template = template = TemplateCallback
0126
=== added file 'charmhelpers/core/templating.py'
--- charmhelpers/core/templating.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/core/templating.py 2014-08-05 13:03:33 +0000
@@ -0,0 +1,51 @@
1import os
2
3from charmhelpers.core import host
4from charmhelpers.core import hookenv
5
6
7def render(source, target, context, owner='root', group='root', perms=0444, templates_dir=None):
8 """
9 Render a template.
10
11 The `source` path, if not absolute, is relative to the `templates_dir`.
12
13 The `target` path should be absolute.
14
15 The context should be a dict containing the values to be replaced in the
16 template.
17
18 The `owner`, `group`, and `perms` options will be passed to `write_file`.
19
20 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
21
22 Note: Using this requires python-jinja2; if it is not installed, calling
23 this will attempt to use charmhelpers.fetch.apt_install to install it.
24 """
25 try:
26 from jinja2 import FileSystemLoader, Environment, exceptions
27 except ImportError:
28 try:
29 from charmhelpers.fetch import apt_install
30 except ImportError:
31 hookenv.log('Could not import jinja2, and could not import '
32 'charmhelpers.fetch to install it',
33 level=hookenv.ERROR)
34 raise
35 apt_install('python-jinja2', fatal=True)
36 from jinja2 import FileSystemLoader, Environment, exceptions
37
38 if templates_dir is None:
39 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
40 loader = Environment(loader=FileSystemLoader(templates_dir))
41 try:
42 source = source
43 template = loader.get_template(source)
44 except exceptions.TemplateNotFound as e:
45 hookenv.log('Could not load template %s from %s.' %
46 (source, templates_dir),
47 level=hookenv.ERROR)
48 raise e
49 content = template.render(context)
50 host.mkdir(os.path.dirname(target))
51 host.write_file(target, content, owner, group, perms)
052
=== modified file 'test_requirements.txt'
--- test_requirements.txt 2014-06-02 12:18:22 +0000
+++ test_requirements.txt 2014-08-05 13:03:33 +0000
@@ -17,6 +17,5 @@
17Tempita==0.5.117Tempita==0.5.1
18bzr+http://bazaar.launchpad.net/~yellow/python-shelltoolbox/trunk@17#egg=shelltoolbox18bzr+http://bazaar.launchpad.net/~yellow/python-shelltoolbox/trunk@17#egg=shelltoolbox
19http://alastairs-place.net/projects/netifaces/netifaces-0.6.tar.gz19http://alastairs-place.net/projects/netifaces/netifaces-0.6.tar.gz
20netaddr==0.7.5
21bzr==2.6.020bzr==2.6.0
22Jinja2==2.7.221Jinja2==2.7.2
2322
=== added directory 'tests/core/templates'
=== added file 'tests/core/templates/cloud_controller_ng.yml'
--- tests/core/templates/cloud_controller_ng.yml 1970-01-01 00:00:00 +0000
+++ tests/core/templates/cloud_controller_ng.yml 2014-08-05 13:03:33 +0000
@@ -0,0 +1,173 @@
1---
2# TODO cc_ip cc public ip
3local_route: {{ domain }}
4port: {{ cc_port }}
5pid_filename: /var/vcap/sys/run/cloud_controller_ng/cloud_controller_ng.pid
6development_mode: false
7
8message_bus_servers:
9 - nats://{{ nats['user'] }}:{{ nats['password'] }}@{{ nats['address'] }}:{{ nats['port'] }}
10
11external_domain:
12 - api.{{ domain }}
13
14system_domain_organization: {{ default_organization }}
15system_domain: {{ domain }}
16app_domains: [ {{ domain }} ]
17srv_api_uri: http://api.{{ domain }}
18
19default_app_memory: 1024
20
21cc_partition: default
22
23bootstrap_admin_email: admin@{{ default_organization }}
24
25bulk_api:
26 auth_user: bulk_api
27 auth_password: "Password"
28
29nginx:
30 use_nginx: false
31 instance_socket: "/var/vcap/sys/run/cloud_controller_ng/cloud_controller.sock"
32
33index: 1
34name: cloud_controller_ng
35
36info:
37 name: vcap
38 build: "2222"
39 version: 2
40 support_address: http://support.cloudfoundry.com
41 description: Cloud Foundry sponsored by Pivotal
42 api_version: 2.0.0
43
44
45directories:
46 tmpdir: /var/vcap/data/cloud_controller_ng/tmp
47
48
49logging:
50 file: /var/vcap/sys/log/cloud_controller_ng/cloud_controller_ng.log
51
52 syslog: vcap.cloud_controller_ng
53
54 level: debug2
55 max_retries: 1
56
57
58
59
60
61db: &db
62 database: sqlite:///var/lib/cloudfoundry/cfcloudcontroller/db/cc.db
63 max_connections: 25
64 pool_timeout: 10
65 log_level: debug2
66
67
68login:
69 url: http://uaa.{{ domain }}
70
71uaa:
72 url: http://uaa.{{ domain }}
73 resource_id: cloud_controller
74 #symmetric_secret: cc-secret
75 verification_key: |
76 -----BEGIN PUBLIC KEY-----
77 MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHFr+KICms+tuT1OXJwhCUmR2d
78 KVy7psa8xzElSyzqx7oJyfJ1JZyOzToj9T5SfTIq396agbHJWVfYphNahvZ/7uMX
79 qHxf+ZH9BL1gk9Y6kCnbM5R60gfwjyW1/dQPjOzn9N394zd2FJoFHwdq9Qs0wBug
80 spULZVNRxq7veq/fzwIDAQAB
81 -----END PUBLIC KEY-----
82
83# App staging parameters
84staging:
85 max_staging_runtime: 900
86 auth:
87 user:
88 password: "Password"
89
90maximum_health_check_timeout: 180
91
92runtimes_file: /var/lib/cloudfoundry/cfcloudcontroller/jobs/config/runtimes.yml
93stacks_file: /var/lib/cloudfoundry/cfcloudcontroller/jobs/config/stacks.yml
94
95quota_definitions:
96 free:
97 non_basic_services_allowed: false
98 total_services: 2
99 total_routes: 1000
100 memory_limit: 1024
101 paid:
102 non_basic_services_allowed: true
103 total_services: 32
104 total_routes: 1000
105 memory_limit: 204800
106 runaway:
107 non_basic_services_allowed: true
108 total_services: 500
109 total_routes: 1000
110 memory_limit: 204800
111 trial:
112 non_basic_services_allowed: false
113 total_services: 10
114 memory_limit: 2048
115 total_routes: 1000
116 trial_db_allowed: true
117
118default_quota_definition: free
119
120resource_pool:
121 minimum_size: 65536
122 maximum_size: 536870912
123 resource_directory_key: cc-resources
124
125 cdn:
126 uri:
127 key_pair_id:
128 private_key: ""
129
130 fog_connection: {"provider":"Local","local_root":"/var/vcap/nfs/store"}
131
132packages:
133 app_package_directory_key: cc-packages
134
135 cdn:
136 uri:
137 key_pair_id:
138 private_key: ""
139
140 fog_connection: {"provider":"Local","local_root":"/var/vcap/nfs/store"}
141
142droplets:
143 droplet_directory_key: cc-droplets
144
145 cdn:
146 uri:
147 key_pair_id:
148 private_key: ""
149
150 fog_connection: {"provider":"Local","local_root":"/var/vcap/nfs/store"}
151
152buildpacks:
153 buildpack_directory_key: cc-buildpacks
154
155 cdn:
156 uri:
157 key_pair_id:
158 private_key: ""
159
160 fog_connection: {"provider":"Local","local_root":"/var/vcap/nfs/store"}
161
162db_encryption_key: Password
163
164trial_db:
165 guid: "78ad16cf-3c22-4427-a982-b9d35d746914"
166
167tasks_disabled: false
168hm9000_noop: true
169flapping_crash_count_threshold: 3
170
171disable_custom_buildpacks: false
172
173broker_client_timeout_seconds: 60
0174
=== added file 'tests/core/templates/fake_cc.yml'
--- tests/core/templates/fake_cc.yml 1970-01-01 00:00:00 +0000
+++ tests/core/templates/fake_cc.yml 2014-08-05 13:03:33 +0000
@@ -0,0 +1,3 @@
1host: {{nats['host']}}
2port: {{nats['port']}}
3domain: {{router['domain']}}
04
=== added file 'tests/core/templates/nginx.conf'
--- tests/core/templates/nginx.conf 1970-01-01 00:00:00 +0000
+++ tests/core/templates/nginx.conf 2014-08-05 13:03:33 +0000
@@ -0,0 +1,154 @@
1# deployment cloudcontroller nginx.conf
2#user vcap vcap;
3
4error_log /var/vcap/sys/log/nginx_ccng/nginx.error.log;
5pid /var/vcap/sys/run/nginx_ccng/nginx.pid;
6
7events {
8 worker_connections 8192;
9 use epoll;
10}
11
12http {
13 include mime.types;
14 default_type text/html;
15 server_tokens off;
16 variables_hash_max_size 1024;
17
18 log_format main '$host - [$time_local] '
19 '"$request" $status $bytes_sent '
20 '"$http_referer" "$http_#user_agent" '
21 '$proxy_add_x_forwarded_for response_time:$upstream_response_time';
22
23 access_log /var/vcap/sys/log/nginx_ccng/nginx.access.log main;
24
25 sendfile on; #enable use of sendfile()
26 tcp_nopush on;
27 tcp_nodelay on; #disable nagel's algorithm
28
29 keepalive_timeout 75 20; #inherited from router
30
31 client_max_body_size 256M; #already enforced upstream/but doesn't hurt.
32
33 upstream cloud_controller {
34 server unix:/var/vcap/sys/run/cloud_controller_ng/cloud_controller.sock;
35 }
36
37 server {
38 listen {{ nginx_port }};
39 server_name _;
40 server_name_in_redirect off;
41 proxy_send_timeout 300;
42 proxy_read_timeout 300;
43
44 # proxy and log all CC traffic
45 location / {
46 access_log /var/vcap/sys/log/nginx_ccng/nginx.access.log main;
47 proxy_buffering off;
48 proxy_set_header Host $host;
49 proxy_set_header X-Real_IP $remote_addr;
50 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
51 proxy_redirect off;
52 proxy_connect_timeout 10;
53 proxy_pass http://cloud_controller;
54 }
55
56
57 # used for x-accel-redirect uri://location/foo.txt
58 # nginx will serve the file root || location || foo.txt
59 location /droplets/ {
60 internal;
61 root /var/vcap/nfs/store;
62 }
63
64
65
66 # used for x-accel-redirect uri://location/foo.txt
67 # nginx will serve the file root || location || foo.txt
68 location /cc-packages/ {
69 internal;
70 root /var/vcap/nfs/store;
71 }
72
73
74 # used for x-accel-redirect uri://location/foo.txt
75 # nginx will serve the file root || location || foo.txt
76 location /cc-droplets/ {
77 internal;
78 root /var/vcap/nfs/store;
79 }
80
81
82 location ~ (/apps/.*/application|/v2/apps/.*/bits|/services/v\d+/configurations/.*/serialized/data|/v2/buildpacks/.*/bits) {
83 # Pass altered request body to this location
84 upload_pass @cc_uploads;
85 upload_pass_args on;
86
87 # Store files to this directory
88 upload_store /var/vcap/data/cloud_controller_ng/tmp/uploads;
89
90 # No limit for output body forwarded to CC
91 upload_max_output_body_len 0;
92
93 # Allow uploaded files to be read only by #user
94 #upload_store_access #user:r;
95
96 # Set specified fields in request body
97 upload_set_form_field "${upload_field_name}_name" $upload_file_name;
98 upload_set_form_field "${upload_field_name}_path" $upload_tmp_path;
99
100 #forward the following fields from existing body
101 upload_pass_form_field "^resources$";
102 upload_pass_form_field "^_method$";
103
104 #on any error, delete uploaded files.
105 upload_cleanup 400-505;
106 }
107
108 location ~ /staging/(buildpack_cache|droplets)/.*/upload {
109
110 # Allow download the droplets and buildpacks
111 if ($request_method = GET){
112 proxy_pass http://cloud_controller;
113 }
114
115 # Pass along auth header
116 set $auth_header $upstream_http_x_auth;
117 proxy_set_header Authorization $auth_header;
118
119 # Pass altered request body to this location
120 upload_pass @cc_uploads;
121
122 # Store files to this directory
123 upload_store /var/vcap/data/cloud_controller_ng/tmp/staged_droplet_uploads;
124
125 # Allow uploaded files to be read only by #user
126 upload_store_access user:r;
127
128 # Set specified fields in request body
129 upload_set_form_field "droplet_path" $upload_tmp_path;
130
131 #on any error, delete uploaded files.
132 upload_cleanup 400-505;
133 }
134
135 # Pass altered request body to a backend
136 location @cc_uploads {
137 proxy_pass http://unix:/var/vcap/sys/run/cloud_controller_ng/cloud_controller.sock;
138 }
139
140 location ~ ^/internal_redirect/(.*){
141 # only allow internal redirects
142 internal;
143
144 set $download_url $1;
145
146 #have to manualy pass along auth header
147 set $auth_header $upstream_http_x_auth;
148 proxy_set_header Authorization $auth_header;
149
150 # Download the file and send it to client
151 proxy_pass $download_url;
152 }
153 }
154}
0155
=== added file 'tests/core/templates/test.conf'
--- tests/core/templates/test.conf 1970-01-01 00:00:00 +0000
+++ tests/core/templates/test.conf 2014-08-05 13:03:33 +0000
@@ -0,0 +1,3 @@
1something
2listen {{nginx_port}}
3something else
04
=== added file 'tests/core/test_services.py'
--- tests/core/test_services.py 1970-01-01 00:00:00 +0000
+++ tests/core/test_services.py 2014-08-05 13:03:33 +0000
@@ -0,0 +1,531 @@
1import mock
2import unittest
3from charmhelpers.core import hookenv
4from charmhelpers.core import services
5
6
7class TestServiceManager(unittest.TestCase):
8 def setUp(self):
9 self.pcharm_dir = mock.patch.object(hookenv, 'charm_dir')
10 self.mcharm_dir = self.pcharm_dir.start()
11 self.mcharm_dir.return_value = 'charm_dir'
12
13 def tearDown(self):
14 self.pcharm_dir.stop()
15
16 def test_register(self):
17 manager = services.ServiceManager([
18 {'service': 'service1',
19 'foo': 'bar'},
20 {'service': 'service2',
21 'qux': 'baz'},
22 ])
23 self.assertEqual(manager.services, {
24 'service1': {'service': 'service1',
25 'foo': 'bar'},
26 'service2': {'service': 'service2',
27 'qux': 'baz'},
28 })
29
30 @mock.patch.object(services.ServiceManager, 'reconfigure_services')
31 @mock.patch.object(services.ServiceManager, 'stop_services')
32 @mock.patch.object(hookenv, 'hook_name')
33 def test_manage_stop(self, hook_name, stop_services, reconfigure_services):
34 manager = services.ServiceManager()
35 hook_name.return_value = 'stop'
36 manager.manage()
37 stop_services.assert_called_once_with()
38 assert not reconfigure_services.called
39
40 @mock.patch.object(services.ServiceManager, 'provide_data')
41 @mock.patch.object(services.ServiceManager, 'reconfigure_services')
42 @mock.patch.object(services.ServiceManager, 'stop_services')
43 @mock.patch.object(hookenv, 'hook_name')
44 def test_manage_other(self, hook_name, stop_services, reconfigure_services, provide_data):
45 manager = services.ServiceManager()
46 hook_name.return_value = 'config-changed'
47 manager.manage()
48 assert not stop_services.called
49 reconfigure_services.assert_called_once_with()
50 provide_data.assert_called_once_with()
51
52 @mock.patch.object(services.ServiceManager, 'save_ready')
53 @mock.patch.object(services.ServiceManager, 'fire_event')
54 @mock.patch.object(services.ServiceManager, 'is_ready')
55 def test_reconfigure_ready(self, is_ready, fire_event, save_ready):
56 manager = services.ServiceManager([
57 {'service': 'service1'}, {'service': 'service2'}])
58 is_ready.return_value = True
59 manager.reconfigure_services()
60 is_ready.assert_has_calls([
61 mock.call('service1'),
62 mock.call('service2'),
63 ], any_order=True)
64 fire_event.assert_has_calls([
65 mock.call('data_ready', 'service1'),
66 mock.call('start', 'service1', default=[
67 services.service_restart,
68 services.manage_ports]),
69 ], any_order=False)
70 fire_event.assert_has_calls([
71 mock.call('data_ready', 'service2'),
72 mock.call('start', 'service2', default=[
73 services.service_restart,
74 services.manage_ports]),
75 ], any_order=False)
76 save_ready.assert_has_calls([
77 mock.call('service1'),
78 mock.call('service2'),
79 ], any_order=True)
80
81 @mock.patch.object(services.ServiceManager, 'save_ready')
82 @mock.patch.object(services.ServiceManager, 'fire_event')
83 @mock.patch.object(services.ServiceManager, 'is_ready')
84 def test_reconfigure_ready_list(self, is_ready, fire_event, save_ready):
85 manager = services.ServiceManager([
86 {'service': 'service1'}, {'service': 'service2'}])
87 is_ready.return_value = True
88 manager.reconfigure_services('service3', 'service4')
89 self.assertEqual(is_ready.call_args_list, [
90 mock.call('service3'),
91 mock.call('service4'),
92 ])
93 self.assertEqual(fire_event.call_args_list, [
94 mock.call('data_ready', 'service3'),
95 mock.call('start', 'service3', default=[
96 services.service_restart,
97 services.open_ports]),
98 mock.call('data_ready', 'service4'),
99 mock.call('start', 'service4', default=[
100 services.service_restart,
101 services.open_ports]),
102 ])
103 self.assertEqual(save_ready.call_args_list, [
104 mock.call('service3'),
105 mock.call('service4'),
106 ])
107
108 @mock.patch.object(services.ServiceManager, 'save_lost')
109 @mock.patch.object(services.ServiceManager, 'fire_event')
110 @mock.patch.object(services.ServiceManager, 'was_ready')
111 @mock.patch.object(services.ServiceManager, 'is_ready')
112 def test_reconfigure_not_ready(self, is_ready, was_ready, fire_event, save_lost):
113 manager = services.ServiceManager([
114 {'service': 'service1'}, {'service': 'service2'}])
115 is_ready.return_value = False
116 was_ready.return_value = False
117 manager.reconfigure_services()
118 is_ready.assert_has_calls([
119 mock.call('service1'),
120 mock.call('service2'),
121 ], any_order=True)
122 fire_event.assert_has_calls([
123 mock.call('stop', 'service1', default=[
124 services.close_ports,
125 services.service_stop]),
126 mock.call('stop', 'service2', default=[
127 services.close_ports,
128 services.service_stop]),
129 ], any_order=True)
130 save_lost.assert_has_calls([
131 mock.call('service1'),
132 mock.call('service2'),
133 ], any_order=True)
134
135 @mock.patch.object(services.ServiceManager, 'save_lost')
136 @mock.patch.object(services.ServiceManager, 'fire_event')
137 @mock.patch.object(services.ServiceManager, 'was_ready')
138 @mock.patch.object(services.ServiceManager, 'is_ready')
139 def test_reconfigure_no_longer_ready(self, is_ready, was_ready, fire_event, save_lost):
140 manager = services.ServiceManager([
141 {'service': 'service1'}, {'service': 'service2'}])
142 is_ready.return_value = False
143 was_ready.return_value = True
144 manager.reconfigure_services()
145 is_ready.assert_has_calls([
146 mock.call('service1'),
147 mock.call('service2'),
148 ], any_order=True)
149 fire_event.assert_has_calls([
150 mock.call('data_lost', 'service1'),
151 mock.call('stop', 'service1', default=[
152 services.close_ports,
153 services.service_stop]),
154 ], any_order=False)
155 fire_event.assert_has_calls([
156 mock.call('data_lost', 'service2'),
157 mock.call('stop', 'service2', default=[
158 services.close_ports,
159 services.service_stop]),
160 ], any_order=False)
161 save_lost.assert_has_calls([
162 mock.call('service1'),
163 mock.call('service2'),
164 ], any_order=True)
165
166 @mock.patch.object(services.ServiceManager, 'fire_event')
167 def test_stop_services(self, fire_event):
168 manager = services.ServiceManager([
169 {'service': 'service1'}, {'service': 'service2'}])
170 manager.stop_services()
171 fire_event.assert_has_calls([
172 mock.call('stop', 'service1', default=[
173 services.close_ports,
174 services.service_stop]),
175 mock.call('stop', 'service2', default=[
176 services.close_ports,
177 services.service_stop]),
178 ], any_order=True)
179
180 @mock.patch.object(services.ServiceManager, 'fire_event')
181 def test_stop_services_list(self, fire_event):
182 manager = services.ServiceManager([
183 {'service': 'service1'}, {'service': 'service2'}])
184 manager.stop_services('service3', 'service4')
185 self.assertEqual(fire_event.call_args_list, [
186 mock.call('stop', 'service3', default=[
187 services.close_ports,
188 services.service_stop]),
189 mock.call('stop', 'service4', default=[
190 services.close_ports,
191 services.service_stop]),
192 ])
193
194 def test_get_service(self):
195 service = {'service': 'test', 'test': 'test_service'}
196 manager = services.ServiceManager([service])
197 self.assertEqual(manager.get_service('test'), service)
198
199 def test_get_service_not_registered(self):
200 service = {'service': 'test', 'test': 'test_service'}
201 manager = services.ServiceManager([service])
202 self.assertRaises(KeyError, manager.get_service, 'foo')
203
204 @mock.patch.object(services.ServiceManager, 'get_service')
205 def test_fire_event_default(self, get_service):
206 get_service.return_value = {}
207 cb = mock.Mock()
208 manager = services.ServiceManager()
209 manager.fire_event('event', 'service', cb)
210 cb.assert_called_once_with('service')
211
212 @mock.patch.object(services.ServiceManager, 'get_service')
213 def test_fire_event_default_list(self, get_service):
214 get_service.return_value = {}
215 cb = mock.Mock()
216 manager = services.ServiceManager()
217 manager.fire_event('event', 'service', [cb])
218 cb.assert_called_once_with('service')
219
220 @mock.patch.object(services.ServiceManager, 'get_service')
221 def test_fire_event_simple_callback(self, get_service):
222 cb = mock.Mock()
223 dcb = mock.Mock()
224 get_service.return_value = {'event': cb}
225 manager = services.ServiceManager()
226 manager.fire_event('event', 'service', dcb)
227 assert not dcb.called
228 cb.assert_called_once_with('service')
229
230 @mock.patch.object(services.ServiceManager, 'get_service')
231 def test_fire_event_simple_callback_list(self, get_service):
232 cb = mock.Mock()
233 dcb = mock.Mock()
234 get_service.return_value = {'event': [cb]}
235 manager = services.ServiceManager()
236 manager.fire_event('event', 'service', dcb)
237 assert not dcb.called
238 cb.assert_called_once_with('service')
239
240 @mock.patch.object(services.ManagerCallback, '__call__')
241 @mock.patch.object(services.ServiceManager, 'get_service')
242 def test_fire_event_manager_callback(self, get_service, mcall):
243 cb = services.ManagerCallback()
244 dcb = mock.Mock()
245 get_service.return_value = {'event': cb}
246 manager = services.ServiceManager()
247 manager.fire_event('event', 'service', dcb)
248 assert not dcb.called
249 mcall.assert_called_once_with(manager, 'service', 'event')
250
251 @mock.patch.object(services.ManagerCallback, '__call__')
252 @mock.patch.object(services.ServiceManager, 'get_service')
253 def test_fire_event_manager_callback_list(self, get_service, mcall):
254 cb = services.ManagerCallback()
255 dcb = mock.Mock()
256 get_service.return_value = {'event': [cb]}
257 manager = services.ServiceManager()
258 manager.fire_event('event', 'service', dcb)
259 assert not dcb.called
260 mcall.assert_called_once_with(manager, 'service', 'event')
261
262 @mock.patch.object(services.ServiceManager, 'get_service')
263 def test_is_ready(self, get_service):
264 get_service.side_effect = [
265 {},
266 {'required_data': [True]},
267 {'required_data': [False]},
268 {'required_data': [True, False]},
269 ]
270 manager = services.ServiceManager()
271 assert manager.is_ready('foo')
272 assert manager.is_ready('bar')
273 assert not manager.is_ready('foo')
274 assert not manager.is_ready('foo')
275 get_service.assert_has_calls([mock.call('foo'), mock.call('bar')])
276
277 def test_load_ready_file_short_circuit(self):
278 manager = services.ServiceManager()
279 manager._ready = 'foo'
280 manager._load_ready_file()
281 self.assertEqual(manager._ready, 'foo')
282
283 @mock.patch('os.path.exists')
284 @mock.patch.object(services.base, 'open', create=True)
285 def test_load_ready_file_new(self, mopen, exists):
286 manager = services.ServiceManager()
287 exists.return_value = False
288 manager._load_ready_file()
289 self.assertEqual(manager._ready, set())
290 assert not mopen.called
291
292 @mock.patch('json.load')
293 @mock.patch('os.path.exists')
294 @mock.patch.object(services.base, 'open', create=True)
295 def test_load_ready_file(self, mopen, exists, jload):
296 manager = services.ServiceManager()
297 exists.return_value = True
298 jload.return_value = ['bar']
299 manager._load_ready_file()
300 self.assertEqual(manager._ready, set(['bar']))
301 exists.assert_called_once_with('charm_dir/READY-SERVICES.json')
302 mopen.assert_called_once_with('charm_dir/READY-SERVICES.json')
303
304 @mock.patch('json.dump')
305 @mock.patch.object(services.base, 'open', create=True)
306 def test_save_ready_file(self, mopen, jdump):
307 manager = services.ServiceManager()
308 manager._save_ready_file()
309 assert not mopen.called
310 manager._ready = set(['foo'])
311 manager._save_ready_file()
312 mopen.assert_called_once_with('charm_dir/READY-SERVICES.json', 'w')
313 jdump.assert_called_once_with(['foo'], mopen.return_value.__enter__())
314
315 @mock.patch.object(services.base.ServiceManager, '_save_ready_file')
316 @mock.patch.object(services.base.ServiceManager, '_load_ready_file')
317 def test_save_ready(self, _lrf, _srf):
318 manager = services.ServiceManager()
319 manager._ready = set(['foo'])
320 manager.save_ready('bar')
321 _lrf.assert_called_once_with()
322 self.assertEqual(manager._ready, set(['foo', 'bar']))
323 _srf.assert_called_once_with()
324
325 @mock.patch.object(services.base.ServiceManager, '_save_ready_file')
326 @mock.patch.object(services.base.ServiceManager, '_load_ready_file')
327 def test_save_lost(self, _lrf, _srf):
328 manager = services.ServiceManager()
329 manager._ready = set(['foo', 'bar'])
330 manager.save_lost('bar')
331 _lrf.assert_called_once_with()
332 self.assertEqual(manager._ready, set(['foo']))
333 _srf.assert_called_once_with()
334 manager.save_lost('bar')
335 self.assertEqual(manager._ready, set(['foo']))
336
337 @mock.patch.object(services.base.ServiceManager, '_save_ready_file')
338 @mock.patch.object(services.base.ServiceManager, '_load_ready_file')
339 def test_was_ready(self, _lrf, _srf):
340 manager = services.ServiceManager()
341 manager._ready = set()
342 manager.save_ready('foo')
343 manager.save_ready('bar')
344 assert manager.was_ready('foo')
345 assert manager.was_ready('bar')
346 manager.save_lost('bar')
347 assert manager.was_ready('foo')
348 assert not manager.was_ready('bar')
349
350 @mock.patch.object(services.base.hookenv, 'relation_set')
351 @mock.patch.object(services.base.hookenv, 'hook_name')
352 def test_provide_data_no_match(self, hook_name, relation_set):
353 provider = mock.Mock()
354 provider.name = 'provided'
355 manager = services.ServiceManager([
356 {'service': 'service', 'provided_data': [provider]}
357 ])
358 hook_name.return_value = 'not-provided-relation-joined'
359 manager.provide_data()
360 assert not provider.provide_data.called
361
362 hook_name.return_value = 'provided-relation-broken'
363 manager.provide_data()
364 assert not provider.provide_data.called
365
366 @mock.patch.object(services.base.hookenv, 'relation_set')
367 @mock.patch.object(services.base.hookenv, 'hook_name')
368 def test_provide_data_not_ready(self, hook_name, relation_set):
369 provider = mock.Mock()
370 provider.name = 'provided'
371 data = provider.provide_data.return_value = {'data': True}
372 provider._is_ready.return_value = False
373 manager = services.ServiceManager([
374 {'service': 'service', 'provided_data': [provider]}
375 ])
376 hook_name.return_value = 'provided-relation-joined'
377 manager.provide_data()
378 assert not relation_set.called
379 provider._is_ready.assert_called_once_with(data)
380
381 @mock.patch.object(services.base.hookenv, 'relation_set')
382 @mock.patch.object(services.base.hookenv, 'hook_name')
383 def test_provide_data_ready(self, hook_name, relation_set):
384 provider = mock.Mock()
385 provider.name = 'provided'
386 data = provider.provide_data.return_value = {'data': True}
387 provider._is_ready.return_value = True
388 manager = services.ServiceManager([
389 {'service': 'service', 'provided_data': [provider]}
390 ])
391 hook_name.return_value = 'provided-relation-changed'
392 manager.provide_data()
393 relation_set.assert_called_once_with(None, data)
394
395
396class TestRelationContext(unittest.TestCase):
397 def setUp(self):
398 self.phookenv = mock.patch.object(services.helpers, 'hookenv')
399 self.mhookenv = self.phookenv.start()
400 self.mhookenv.relation_ids.return_value = []
401 self.context = services.RelationContext()
402 self.context.name = 'http'
403 self.context.interface = 'http'
404 self.context.required_keys = ['foo', 'bar']
405 self.mhookenv.reset_mock()
406
407 def tearDown(self):
408 self.phookenv.stop()
409
410 def test_no_relations(self):
411 self.context.get_data()
412 self.assertFalse(self.context.is_ready())
413 self.assertEqual(self.context, {})
414 self.mhookenv.relation_ids.assert_called_once_with('http')
415
416 def test_no_units(self):
417 self.mhookenv.relation_ids.return_value = ['nginx']
418 self.mhookenv.related_units.return_value = []
419 self.context.get_data()
420 self.assertFalse(self.context.is_ready())
421 self.assertEqual(self.context, {'http': []})
422
423 def test_incomplete(self):
424 self.mhookenv.relation_ids.return_value = ['nginx', 'apache']
425 self.mhookenv.related_units.side_effect = lambda i: [i+'/0']
426 self.mhookenv.relation_get.side_effect = [{}, {'foo': '1'}]
427 self.context.get_data()
428 self.assertFalse(bool(self.context))
429 self.assertEqual(self.mhookenv.relation_get.call_args_list, [
430 mock.call(rid='apache', unit='apache/0'),
431 mock.call(rid='nginx', unit='nginx/0'),
432 ])
433
434 def test_complete(self):
435 self.mhookenv.relation_ids.return_value = ['nginx', 'apache', 'tomcat']
436 self.mhookenv.related_units.side_effect = lambda i: [i+'/0']
437 self.mhookenv.relation_get.side_effect = [{'foo': '1'}, {'foo': '2', 'bar': '3'}, {}]
438 self.context.get_data()
439 self.assertTrue(self.context.is_ready())
440 self.assertEqual(self.context, {'http': [
441 {
442 'foo': '2',
443 'bar': '3',
444 },
445 ]})
446 self.mhookenv.relation_ids.assert_called_with('http')
447 self.assertEqual(self.mhookenv.relation_get.call_args_list, [
448 mock.call(rid='apache', unit='apache/0'),
449 mock.call(rid='nginx', unit='nginx/0'),
450 mock.call(rid='tomcat', unit='tomcat/0'),
451 ])
452
453 def test_provide(self):
454 self.assertEqual(self.context.provide_data(), {})
455
456
457class TestTemplateCallback(unittest.TestCase):
458 @mock.patch.object(services.helpers, 'templating')
459 def test_template_defaults(self, mtemplating):
460 manager = mock.Mock(**{'get_service.return_value': {
461 'required_data': [{'foo': 'bar'}]}})
462 self.assertRaises(TypeError, services.template, source='foo.yml')
463 callback = services.template(source='foo.yml', target='bar.yml')
464 assert isinstance(callback, services.ManagerCallback)
465 assert not mtemplating.render.called
466 callback(manager, 'test', 'event')
467 mtemplating.render.assert_called_once_with(
468 'foo.yml', 'bar.yml', {'foo': 'bar'},
469 'root', 'root', 0444)
470
471 @mock.patch.object(services.helpers, 'templating')
472 def test_template_explicit(self, mtemplating):
473 manager = mock.Mock(**{'get_service.return_value': {
474 'required_data': [{'foo': 'bar'}]}})
475 callback = services.template(
476 source='foo.yml', target='bar.yml',
477 owner='user', group='group', perms=0555
478 )
479 assert isinstance(callback, services.ManagerCallback)
480 assert not mtemplating.render.called
481 callback(manager, 'test', 'event')
482 mtemplating.render.assert_called_once_with(
483 'foo.yml', 'bar.yml', {'foo': 'bar'},
484 'user', 'group', 0555)
485
486
487class TestPortsCallback(unittest.TestCase):
488 def setUp(self):
489 self.phookenv = mock.patch.object(services.base, 'hookenv')
490 self.mhookenv = self.phookenv.start()
491 self.mhookenv.relation_ids.return_value = []
492 self.mhookenv.charm_dir.return_value = 'charm_dir'
493 self.popen = mock.patch.object(services.base, 'open', create=True)
494 self.mopen = self.popen.start()
495
496 def tearDown(self):
497 self.phookenv.stop()
498 self.popen.stop()
499
500 def test_no_ports(self):
501 manager = mock.Mock(**{'get_service.return_value': {}})
502 services.PortManagerCallback()(manager, 'service', 'event')
503 assert not self.mhookenv.open_port.called
504 assert not self.mhookenv.close_port.called
505
506 def test_open_ports(self):
507 manager = mock.Mock(**{'get_service.return_value': {'ports': [1, 2]}})
508 services.open_ports(manager, 'service', 'start')
509 self.mhookenv.open_port.has_calls([mock.call(1), mock.call(2)])
510 assert not self.mhookenv.close_port.called
511
512 def test_close_ports(self):
513 manager = mock.Mock(**{'get_service.return_value': {'ports': [1, 2]}})
514 services.close_ports(manager, 'service', 'stop')
515 assert not self.mhookenv.open_port.called
516 self.mhookenv.close_port.has_calls([mock.call(1), mock.call(2)])
517
518 def test_close_old_ports(self):
519 self.mopen.return_value.read.return_value = '10,20'
520 manager = mock.Mock(**{'get_service.return_value': {'ports': [1, 2]}})
521 services.close_ports(manager, 'service', 'stop')
522 assert not self.mhookenv.open_port.called
523 self.mhookenv.close_port.has_calls([
524 mock.call(10),
525 mock.call(20),
526 mock.call(1),
527 mock.call(2)])
528
529
530if __name__ == '__main__':
531 unittest.main()
0532
=== added file 'tests/core/test_templating.py'
--- tests/core/test_templating.py 1970-01-01 00:00:00 +0000
+++ tests/core/test_templating.py 2014-08-05 13:03:33 +0000
@@ -0,0 +1,64 @@
1import os
2import pkg_resources
3import tempfile
4import unittest
5import jinja2
6
7import mock
8from charmhelpers.core import templating
9
10
11TEMPLATES_DIR = pkg_resources.resource_filename(__name__, 'templates')
12
13
14class TestTemplating(unittest.TestCase):
15 def setUp(self):
16 self.charm_dir = pkg_resources.resource_filename(__name__, '')
17 self._charm_dir_patch = mock.patch.object(templating.hookenv, 'charm_dir')
18 self._charm_dir_mock = self._charm_dir_patch.start()
19 self._charm_dir_mock.side_effect = lambda: self.charm_dir
20
21 def tearDown(self):
22 self._charm_dir_patch.stop()
23
24 @mock.patch.object(templating.host.os, 'fchown')
25 @mock.patch.object(templating.host, 'mkdir')
26 @mock.patch.object(templating.host, 'log')
27 def test_render(self, log, mkdir, fchown):
28 _, fn1 = tempfile.mkstemp()
29 _, fn2 = tempfile.mkstemp()
30 try:
31 context = {
32 'nats': {
33 'port': '1234',
34 'host': 'example.com',
35 },
36 'router': {
37 'domain': 'api.foo.com'
38 },
39 'nginx_port': 80,
40 }
41 templating.render('fake_cc.yml', fn1, context, templates_dir=TEMPLATES_DIR)
42 contents = open(fn1).read()
43 self.assertRegexpMatches(contents, 'port: 1234')
44 self.assertRegexpMatches(contents, 'host: example.com')
45 self.assertRegexpMatches(contents, 'domain: api.foo.com')
46
47 templating.render('test.conf', fn2, context, templates_dir=TEMPLATES_DIR)
48 contents = open(fn2).read()
49 self.assertRegexpMatches(contents, 'listen 80')
50 self.assertEqual(fchown.call_count, 2)
51 self.assertEqual(mkdir.call_count, 2)
52 finally:
53 for fn in (fn1, fn2):
54 if os.path.exists(fn):
55 os.remove(fn)
56
57 @mock.patch.object(templating, 'hookenv')
58 @mock.patch('jinja2.Environment')
59 def test_load_error(self, Env, hookenv):
60 Env().get_template.side_effect = jinja2.exceptions.TemplateNotFound('fake_cc.yml')
61 self.assertRaises(
62 jinja2.exceptions.TemplateNotFound, templating.render,
63 'fake.src', 'fake.tgt', {}, templates_dir='tmpl')
64 hookenv.log.assert_called_once_with('Could not load template fake.src from tmpl.', level=hookenv.ERROR)

Subscribers

People subscribed via source and target branches