Merge lp:~bloodearnest/charms/trusty/gunicorn/config-rework into lp:charms/trusty/gunicorn
- Trusty Tahr (14.04)
- config-rework
- Merge into trunk
Proposed by
Simon Davy
Status: | Rejected |
---|---|
Rejected by: | Haw Loeung |
Proposed branch: | lp:~bloodearnest/charms/trusty/gunicorn/config-rework |
Merge into: | lp:charms/trusty/gunicorn |
Diff against target: |
806 lines (+340/-181) 12 files modified
.bzrignore (+2/-0) Makefile (+10/-4) config.yaml (+29/-1) hooks/hooks.py (+56/-27) hooks/tests/test_hooks.py (+39/-26) hooks/tests/test_template.py (+147/-89) metadata.yaml (+3/-3) requirements.txt (+4/-0) revision (+1/-1) templates/config.tmpl (+41/-0) templates/runner.tmpl (+6/-0) templates/upstart.tmpl (+2/-30) |
To merge this branch: | bzr merge lp:~bloodearnest/charms/trusty/gunicorn/config-rework |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
charmers | Pending | ||
Review via email: mp+305623@code.launchpad.net |
Commit message
Use a config file rather than cli args, and some tweaks to wsgi config.
You can now supply config as python config to be included in the config file, or as cli args, which is backwards compatible with config for previous charm version.
Description of the change
Use a config file rather than cli args.
This is complete rework of the way gunicorn is configured. It's been extensively used in this form for a good while, as we were using a branch.
There are some changes to config options here, but they are all backwards compatible, unit tested, and documented.
To post a comment you must log in.
Unmerged revisions
- 36. By Simon Davy
-
merging config rework from lp:~bloodearnest/charms/gunicorn/trunk
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2014-02-11 14:15:13 +0000 |
3 | +++ .bzrignore 2016-09-13 16:09:18 +0000 |
4 | @@ -1,1 +1,3 @@ |
5 | bin |
6 | +.coverage |
7 | +.venv |
8 | |
9 | === modified file 'Makefile' |
10 | --- Makefile 2014-04-28 09:56:32 +0000 |
11 | +++ Makefile 2016-09-13 16:09:18 +0000 |
12 | @@ -1,8 +1,14 @@ |
13 | #!/usr/bin/make |
14 | -PYTHON := /usr/bin/env python |
15 | - |
16 | -unit_test: |
17 | - nosetests -s --with-coverage --cover-package=hooks hooks |
18 | +VENV = .venv |
19 | +PYTHON = $(VENV)/bin/python |
20 | + |
21 | +$(VENV): requirements.txt |
22 | + virtualenv --system-site-packages $@ |
23 | + $(VENV)/bin/pip install -r requirements.txt |
24 | + |
25 | + |
26 | +unit_test: .venv |
27 | + $(VENV)/bin/nosetests -s --with-coverage --cover-package=hooks hooks |
28 | |
29 | sync-charm-helpers: bin/charm_helpers_sync.py |
30 | @mkdir -p bin |
31 | |
32 | === modified file 'config.yaml' |
33 | --- config.yaml 2016-01-28 09:14:23 +0000 |
34 | +++ config.yaml 2016-09-13 16:09:18 +0000 |
35 | @@ -59,10 +59,32 @@ |
36 | type: string |
37 | default: "" |
38 | description: "The Access log format. Don't forget to escape all quotes and round brackets." |
39 | + wsgi_error_logfile: |
40 | + type: string |
41 | + default: "" |
42 | + description: "The error log file to write to." |
43 | + wsgi_cli: |
44 | + type: string |
45 | + default: "" |
46 | + description: > |
47 | + Extra cli arguments to gunicorn. |
48 | + e.g. '--debug --timeouts 2'. |
49 | + DEPRECATED: use wsgi_extra_config |
50 | wsgi_extra: |
51 | type: string |
52 | default: "" |
53 | - description: "Space separated extra settings. For example: --debug" |
54 | + description: > |
55 | + Backwards compatibility name for wsgi_cli |
56 | + DEPRECATED: use wsgi_extra_config |
57 | + wsgi_extra_config: |
58 | + type: string |
59 | + default: "" |
60 | + description: > |
61 | + Extra config, as newline seperated python statements. |
62 | + e.g. |
63 | + |
64 | + debug = True |
65 | + timeouts = 2 |
66 | wsgi_timestamp: |
67 | type: string |
68 | default: "" |
69 | @@ -89,3 +111,9 @@ |
70 | description: > |
71 | List of environment variables for the wsgi process. |
72 | e.g. FOO="bar" BAZ="1 2 3" |
73 | + gunicorn_path: |
74 | + type: string |
75 | + default: gunicorn |
76 | + description: > |
77 | + Path to gunicorn. Defaults to system. |
78 | + Use if run in venv. |
79 | |
80 | === modified file 'hooks/hooks.py' |
81 | --- hooks/hooks.py 2014-05-01 13:14:53 +0000 |
82 | +++ hooks/hooks.py 2016-09-13 16:09:18 +0000 |
83 | @@ -58,7 +58,7 @@ |
84 | |
85 | |
86 | def remove_old_services(): |
87 | - """Clean up older charm config if we are upgrading to new python based charm""" |
88 | + "Clean up older charm config if we are upgrading to new python based charm" |
89 | |
90 | if not os.path.exists(GUNICORN_INITD_SCRIPT_DISABLED): |
91 | # ensure the sysv service is disabled |
92 | @@ -67,7 +67,7 @@ |
93 | # ensure the sysv init is stopped |
94 | subprocess.call([GUNICORN_INITD_SCRIPT, 'stop']) |
95 | |
96 | - # ensure any old charm config removed. |
97 | + # ensure any old charm config removed. |
98 | for conf in glob.glob('/etc/gunicorn.d/*.conf'): |
99 | try: |
100 | hookenv.log('removing old guncorn config: %s' % conf) |
101 | @@ -75,13 +75,13 @@ |
102 | except: # pragma: no cover |
103 | pass |
104 | |
105 | - # rename /etc/init.d/gunicorn |
106 | + # rename /etc/init.d/gunicorn |
107 | if os.path.exists("/etc/init.d/gunicorn"): |
108 | shutil.move(GUNICORN_INITD_SCRIPT, GUNICORN_INITD_SCRIPT_DISABLED) |
109 | |
110 | |
111 | def write_initd_proxy(): |
112 | - """Some charms/packages may use hardcoded path of /etc/init.d/gunicorn, so |
113 | + """Some charms/packages may use hardcoded path of /etc/init.d/gunicorn, so |
114 | add a wrapper there that proxies to the upstart job.""" |
115 | with open(GUNICORN_INITD_SCRIPT, 'w') as initd: |
116 | initd.write("#!/bin/sh\nservice gunicorn $*\n") |
117 | @@ -97,6 +97,7 @@ |
118 | def install(): |
119 | ensure_packages() |
120 | write_initd_proxy() |
121 | + host.mkdir('/srv/gunicorn', force=True) |
122 | |
123 | |
124 | @hooks.hook('upgrade-charm') |
125 | @@ -104,6 +105,7 @@ |
126 | ensure_packages() |
127 | remove_old_services() # TODO: remove later |
128 | write_initd_proxy() |
129 | + host.mkdir('/srv/gunicorn', force=True) |
130 | |
131 | |
132 | @hooks.hook( |
133 | @@ -118,19 +120,22 @@ |
134 | hookenv.log("No wsgi-file relation, nothing to do") |
135 | return |
136 | |
137 | + # this is a subordinate, so only ever one relation |
138 | relation_data = relations[0] |
139 | - |
140 | - service_name = sanitized_service_name() |
141 | - wsgi_config['unit_name'] = service_name |
142 | - |
143 | - project_conf = "/etc/init/gunicorn.conf" |
144 | - |
145 | working_dir = relation_data.get('working_dir', None) |
146 | if not working_dir: |
147 | return |
148 | |
149 | wsgi_config['working_dir'] = working_dir |
150 | - wsgi_config['project_name'] = service_name |
151 | + service_name = sanitized_service_name() |
152 | + wsgi_config['service_name'] = service_name |
153 | + |
154 | + # hardcoded job for now, we might want to use something else in future |
155 | + upstart_file = "/etc/init/gunicorn.conf" |
156 | + # can't use /etc/gunicorn.d/ as that is for debians method |
157 | + config_file = "/srv/gunicorn/{}.py".format(service_name) |
158 | + runner_file = "/srv/gunicorn/run_{}.sh".format(service_name) |
159 | + wsgi_config['wsgi_config_file'] = config_file |
160 | |
161 | # any valid config item can be overidden by a relation item |
162 | for key, relation_value in relation_data.items(): |
163 | @@ -145,47 +150,71 @@ |
164 | fetch.apt_install('python-tornado') |
165 | |
166 | if str(wsgi_config['wsgi_workers']) == '0': |
167 | - wsgi_config['wsgi_workers'] = cpu_count() + 1 |
168 | + wsgi_config['wsgi_workers'] = (cpu_count() * 2) + 1 |
169 | |
170 | + # env_extra was originally a partial python dict, to templated into |
171 | + # a python dict for debian's gunicorn config |
172 | + # e.g env_extra = "'FOO': 'BAR', 'A': '1'" |
173 | + # |
174 | + # Now it is just a standard env string, e.g. |
175 | + # e.g. env_extra = "FOO=BAR A=1" |
176 | env_extra = wsgi_config.get('env_extra', '') |
177 | - |
178 | - # support old python dict format for env_extra for upgrade path |
179 | extra = [] |
180 | - # attempt dict parsing |
181 | + |
182 | try: |
183 | + # old style dict parsing |
184 | dict_str = '{' + env_extra + '}' |
185 | extra = [[k, str(v)] for k, v in ast.literal_eval(dict_str).items()] |
186 | except (SyntaxError, ValueError): |
187 | pass |
188 | |
189 | if not extra: |
190 | + # new style string |
191 | extra = [ |
192 | v.split('=', 1) for v in shlex.split(env_extra) if '=' in v |
193 | ] |
194 | |
195 | wsgi_config['env_extra'] = extra |
196 | |
197 | - |
198 | - # support old python list format for wsgi_extra |
199 | - # it will be a partial tuple of strings |
200 | - # e.g. wsgi_extra = "'foo', 'bar'," |
201 | - |
202 | - wsgi_extra = wsgi_config.get('wsgi_extra', '') |
203 | + # wsgi_cli (formely just wsgi_extra) used to be a list of cli args in |
204 | + # a partial python list like format |
205 | + # e.g. wsgi_cli = "'--foo=bar', '-a'" |
206 | + # |
207 | + # Now it is a simple string of of cli params |
208 | + # e.g wsgi_cli = "--foo=bar -a" |
209 | + |
210 | + wsgi_cli = wsgi_config.get('wsgi_cli', None) |
211 | + if not wsgi_cli: |
212 | + wsgi_cli = wsgi_config.get('wsgi_extra', '') |
213 | + |
214 | # attempt tuple parsing |
215 | try: |
216 | - tuple_str = '(' + wsgi_extra + ')' |
217 | - wsgi_extra = " ".join(ast.literal_eval(tuple_str)) |
218 | + tuple_str = '(' + wsgi_cli + ')' |
219 | + wsgi_cli = " ".join(ast.literal_eval(tuple_str)) |
220 | except (SyntaxError, ValueError): |
221 | pass |
222 | |
223 | - wsgi_config['wsgi_extra'] = wsgi_extra |
224 | - |
225 | - process_template('upstart.tmpl', wsgi_config, project_conf) |
226 | - hookenv.log('written gunicorn upstart config to %s' % project_conf) |
227 | + wsgi_config['wsgi_cli'] = wsgi_cli |
228 | + |
229 | + extra_config = wsgi_config.get('wsgi_extra_config', '') |
230 | + extra_config_lines = extra_config.split('\n') if extra_config else [] |
231 | + wsgi_config['wsgi_extra_config_lines'] = extra_config_lines |
232 | + |
233 | + process_template('config.tmpl', wsgi_config, config_file) |
234 | + hookenv.log( |
235 | + 'written gunicorn config for %s to %s' % (service_name, config_file)) |
236 | + process_template('upstart.tmpl', wsgi_config, upstart_file) |
237 | + hookenv.log('written gunicorn upstart config to %s' % upstart_file) |
238 | + process_template('runner.tmpl', wsgi_config, runner_file) |
239 | + os.chmod(runner_file, 0755) |
240 | |
241 | # We need this because when the contained charm configuration or code |
242 | # changed Gunicorn needs to restart to run the new code. |
243 | host.service_restart("gunicorn") |
244 | + try: |
245 | + hookenv.open_port(wsgi_config['port']) |
246 | + except subprocess.CalledProcessError: # pragma: no cover |
247 | + pass # already open |
248 | |
249 | |
250 | @hooks.hook("wsgi_file_relation_broken") |
251 | |
252 | === modified file 'hooks/tests/test_hooks.py' |
253 | --- hooks/tests/test_hooks.py 2014-05-01 13:14:53 +0000 |
254 | +++ hooks/tests/test_hooks.py 2016-09-13 16:09:18 +0000 |
255 | @@ -67,20 +67,32 @@ |
256 | 'hooks.sanitized_service_name', return_value=self.SERVICE_NAME) |
257 | self.apply_patch('hooks.cpu_count', return_value=1) |
258 | |
259 | - def assert_wsgi_config_applied(self, expected): |
260 | - tmpl, config, path = self.process_template.call_args[0] |
261 | - self.assertEqual(tmpl, 'upstart.tmpl') |
262 | - self.assertEqual(path, '/etc/init/gunicorn.conf') |
263 | - self.assertEqual(config, expected) |
264 | + def assert_config_applied(self, expected): |
265 | + |
266 | + cfg_tmpl, cfg_env, cfg_path = self.process_template.mock_calls[0][1] |
267 | + self.assertEqual(cfg_tmpl, 'config.tmpl') |
268 | + self.assertEqual(cfg_env, expected) |
269 | + self.assertEqual( |
270 | + cfg_path, '/srv/gunicorn/{}.py'.format(self.SERVICE_NAME)) |
271 | + |
272 | + up_tmpl, up_env, up_path = self.process_template.mock_calls[1][1] |
273 | + self.assertEqual(up_tmpl, 'upstart.tmpl') |
274 | + self.assertEqual(up_path, '/etc/init/gunicorn.conf') |
275 | + self.assertEqual(up_env, expected) |
276 | + |
277 | self.host.service_restart.assert_called_once_with("gunicorn") |
278 | + self.hookenv.open_port.assert_called_once_with(expected['port']) |
279 | |
280 | def get_default_context(self): |
281 | expected = DEFAULTS.copy() |
282 | - expected['unit_name'] = self.SERVICE_NAME |
283 | + # extra config added by the hook that's not in the charm config |
284 | expected['working_dir'] = self.WORKING_DIR |
285 | - expected['project_name'] = self.SERVICE_NAME |
286 | - expected['wsgi_workers'] = 2 |
287 | + expected['service_name'] = self.SERVICE_NAME |
288 | + expected['wsgi_workers'] = 3 |
289 | expected['env_extra'] = [] |
290 | + expected['wsgi_extra_config_lines'] = [] |
291 | + expected['wsgi_config_file'] = '/srv/gunicorn/' + self.SERVICE_NAME + '.py' |
292 | + expected['wsgi_config_file'] = '/srv/gunicorn/' + self.SERVICE_NAME + '.py' |
293 | fmt = expected['wsgi_access_logformat'].replace('"', '\\"') |
294 | expected['wsgi_access_logformat'] = fmt |
295 | return expected |
296 | @@ -109,13 +121,13 @@ |
297 | mock_exists.side_effect = [False, True] |
298 | |
299 | hooks.remove_old_services() |
300 | - |
301 | + |
302 | self.assertEqual( |
303 | - self.subprocess.call.mock_calls[0][1][0], |
304 | + self.subprocess.call.mock_calls[0][1][0], |
305 | ['update-rc.d', '-f', 'gunicorn', 'disable'] |
306 | ) |
307 | self.assertEqual( |
308 | - self.subprocess.call.mock_calls[1][1][0], |
309 | + self.subprocess.call.mock_calls[1][1][0], |
310 | ['/etc/init.d/gunicorn', 'stop'] |
311 | ) |
312 | mock_remove.assert_called_once_with(path) |
313 | @@ -125,12 +137,12 @@ |
314 | def test_default_configure_gunicorn(self): |
315 | hooks.configure_gunicorn() |
316 | expected = self.get_default_context() |
317 | - self.assert_wsgi_config_applied(expected) |
318 | + self.assert_config_applied(expected) |
319 | |
320 | def test_configure_gunicorn_no_relations(self): |
321 | self.hookenv.relations_of_type.return_value = [] |
322 | hooks.configure_gunicorn() |
323 | - self.hookenv.log.assert_called_once_With("No wsgi-file relation, nothing to do") |
324 | + self.hookenv.log.assert_called_once_with("No wsgi-file relation, nothing to do") |
325 | self.assertFalse(self.process_template.called) |
326 | self.assertFalse(self.host.service_restart.called) |
327 | |
328 | @@ -153,7 +165,7 @@ |
329 | expected['wsgi_workers'] = 1 |
330 | expected['port'] = 9999 |
331 | |
332 | - self.assert_wsgi_config_applied(expected) |
333 | + self.assert_config_applied(expected) |
334 | |
335 | def test_env_extra_parsing(self): |
336 | self.relation_data['env_extra'] = 'A=1 B="2" C="3 4" D= E' |
337 | @@ -169,7 +181,7 @@ |
338 | # no E |
339 | ] |
340 | |
341 | - self.assert_wsgi_config_applied(expected) |
342 | + self.assert_config_applied(expected) |
343 | |
344 | def test_env_extra_old_style_parsing(self): |
345 | self.relation_data['env_extra'] = "'A': '1', 'B': 2" |
346 | @@ -182,31 +194,31 @@ |
347 | ['B', '2'], |
348 | ] |
349 | |
350 | - self.assert_wsgi_config_applied(expected) |
351 | + self.assert_config_applied(expected) |
352 | |
353 | def test_wsgi_extra_old_style_parsing_single_param(self): |
354 | - self.relation_data['wsgi_extra'] = "'--some-option'," |
355 | + self.relation_data['wsgi_cli'] = "'--some-option'," |
356 | hooks.configure_gunicorn() |
357 | expected = self.get_default_context() |
358 | - expected['wsgi_extra'] = "--some-option" |
359 | + expected['wsgi_cli'] = "--some-option" |
360 | |
361 | - self.assert_wsgi_config_applied(expected) |
362 | + self.assert_config_applied(expected) |
363 | |
364 | def test_wsgi_extra_old_style_parsing_mulit_param(self): |
365 | - self.relation_data['wsgi_extra'] = "'--some-option', '--other-option'," |
366 | + self.relation_data['wsgi_cli'] = "'--some-option', '--other-option'," |
367 | hooks.configure_gunicorn() |
368 | expected = self.get_default_context() |
369 | - expected['wsgi_extra'] = "--some-option --other-option" |
370 | + expected['wsgi_cli'] = "--some-option --other-option" |
371 | |
372 | - self.assert_wsgi_config_applied(expected) |
373 | + self.assert_config_applied(expected) |
374 | |
375 | def test_wsgi_extra_old_style_parsing_bad_value(self): |
376 | - self.relation_data['wsgi_extra'] = "BAD PYTHON" |
377 | + self.relation_data['wsgi_cli'] = "BAD PYTHON" |
378 | hooks.configure_gunicorn() |
379 | expected = self.get_default_context() |
380 | - expected['wsgi_extra'] = "BAD PYTHON" |
381 | + expected['wsgi_cli'] = "BAD PYTHON" |
382 | |
383 | - self.assert_wsgi_config_applied(expected) |
384 | + self.assert_config_applied(expected) |
385 | |
386 | def do_worker_class(self, worker_class): |
387 | self.relation_data['wsgi_worker_class'] = worker_class |
388 | @@ -215,7 +227,7 @@ |
389 | 'python-%s' % worker_class) |
390 | expected = self.get_default_context() |
391 | expected['wsgi_worker_class'] = worker_class |
392 | - self.assert_wsgi_config_applied(expected) |
393 | + self.assert_config_applied(expected) |
394 | |
395 | def test_configure_worker_class_eventlet(self): |
396 | self.do_worker_class('eventlet') |
397 | @@ -231,3 +243,4 @@ |
398 | hooks.wsgi_file_relation_broken() |
399 | self.host.service_stop.assert_called_once_with("gunicorn") |
400 | remove.assert_called_once_with('/etc/init/gunicorn.conf') |
401 | + |
402 | |
403 | === modified file 'hooks/tests/test_template.py' |
404 | --- hooks/tests/test_template.py 2016-01-28 09:14:23 +0000 |
405 | +++ hooks/tests/test_template.py 2016-09-13 16:09:18 +0000 |
406 | @@ -1,60 +1,17 @@ |
407 | import os |
408 | +import textwrap |
409 | +from types import ModuleType |
410 | + |
411 | from unittest import TestCase |
412 | from mock import patch, MagicMock |
413 | |
414 | import hooks |
415 | |
416 | -EXPECTED = """ |
417 | -#-------------------------------------------------------------- |
418 | -# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN |
419 | -#-------------------------------------------------------------- |
420 | - |
421 | -description "Gunicorn daemon for the PROJECT_NAME project" |
422 | - |
423 | -start on (local-filesystems and net-device-up IFACE=eth0) |
424 | -stop on runlevel [!12345] |
425 | - |
426 | -# If the process quits unexpectadly trigger a respawn |
427 | -respawn |
428 | -respawn limit 10 5 |
429 | - |
430 | -setuid WSGI_USER |
431 | -setgid WSGI_GROUP |
432 | -chdir WORKING_DIR |
433 | - |
434 | -# This line can be removed and replace with the --pythonpath PYTHON_PATH \\ |
435 | -# option with Gunicorn>1.17 |
436 | -env PYTHONPATH=PYTHON_PATH |
437 | -env A="1" |
438 | -env B="1 2" |
439 | - |
440 | - |
441 | -exec gunicorn \\ |
442 | - --name=PROJECT_NAME \\ |
443 | - --workers=WSGI_WORKERS \\ |
444 | - --worker-class=WSGI_WORKER_CLASS \\ |
445 | - --worker-connections=WSGI_WORKER_CONNECTIONS \\ |
446 | - --max-requests=WSGI_MAX_REQUESTS \\ |
447 | - --backlog=WSGI_BACKLOG \\ |
448 | - --timeout=WSGI_TIMEOUT \\ |
449 | - --keep-alive=WSGI_KEEP_ALIVE \\ |
450 | - --umask=WSGI_UMASK \\ |
451 | - --bind=LISTEN_IP:PORT \\ |
452 | - --log-file=WSGI_LOG_FILE \\ |
453 | - --log-level=WSGI_LOG_LEVEL \\ |
454 | - --pid=PID_FILE \\ |
455 | - --access-logfile=WSGI_ACCESS_LOGFILE \\ |
456 | - --access-logformat=WSGI_ACCESS_LOGFORMAT \\ |
457 | - WSGI_EXTRA \\ |
458 | - WSGI_WSGI_FILE |
459 | -""".strip() |
460 | - |
461 | - |
462 | -class TemplateTestCase(TestCase): |
463 | +class BaseTemplateTestCase(TestCase): |
464 | maxDiff = None |
465 | |
466 | def setUp(self): |
467 | - super(TemplateTestCase, self).setUp() |
468 | + super(BaseTemplateTestCase, self).setUp() |
469 | patch_open = patch('hooks.open', create=True) |
470 | self.open = patch_open.start() |
471 | self.addCleanup(patch_open.stop) |
472 | @@ -71,57 +28,158 @@ |
473 | self.addCleanup(patch_hookenv.stop) |
474 | |
475 | def get_test_context(self): |
476 | - keys = [ |
477 | - 'project_name', |
478 | + string_keys = [ |
479 | + 'service_name', |
480 | 'wsgi_user', |
481 | 'wsgi_group', |
482 | 'working_dir', |
483 | - 'python_path', |
484 | + 'wsgi_worker_class', |
485 | + 'wsgi_log_file', |
486 | + 'wsgi_log_level', |
487 | + 'pid_file', |
488 | + 'listen_ip', |
489 | + 'port', |
490 | + 'wsgi_cli', |
491 | + 'wsgi_wsgi_file', |
492 | + 'wsgi_config_file', |
493 | + 'gunicorn_path', |
494 | + ] |
495 | + int_keys = [ |
496 | 'wsgi_workers', |
497 | - 'wsgi_worker_class', |
498 | 'wsgi_worker_connections', |
499 | 'wsgi_max_requests', |
500 | 'wsgi_backlog', |
501 | 'wsgi_timeout', |
502 | 'wsgi_keep_alive', |
503 | 'wsgi_umask', |
504 | - 'wsgi_log_file', |
505 | - 'wsgi_log_level', |
506 | - 'pid_file', |
507 | - 'wsgi_access_logfile', |
508 | - 'wsgi_access_logformat', |
509 | - 'listen_ip', |
510 | - 'port', |
511 | - 'wsgi_extra', |
512 | - 'wsgi_wsgi_file', |
513 | ] |
514 | - ctx = dict((k, k.upper()) for k in keys) |
515 | - ctx['env_extra'] = [["A", "1"], ["B", "1 2"]] |
516 | + ctx = dict((k, k.upper()) for k in string_keys) |
517 | + ctx.update(dict((k, 1) for k in int_keys)) |
518 | return ctx |
519 | |
520 | - def test_template(self): |
521 | - |
522 | - ctx = self.get_test_context() |
523 | - |
524 | - hooks.process_template('upstart.tmpl', ctx, 'path') |
525 | - output = self.file.write.call_args[0][0] |
526 | - |
527 | - self.assertMultiLineEqual(EXPECTED, output) |
528 | - |
529 | - def test_no_access_logfile(self): |
530 | - ctx = self.get_test_context() |
531 | - ctx['wsgi_access_logfile'] = "" |
532 | - |
533 | - hooks.process_template('upstart.tmpl', ctx, 'path') |
534 | - output = self.file.write.call_args[0][0] |
535 | - |
536 | - self.assertNotIn('--access-logfile', output) |
537 | - |
538 | - def test_no_access_logformat(self): |
539 | - ctx = self.get_test_context() |
540 | - ctx['wsgi_access_logformat'] = "" |
541 | - |
542 | - hooks.process_template('upstart.tmpl', ctx, 'path') |
543 | - output = self.file.write.call_args[0][0] |
544 | - |
545 | - self.assertNotIn('--access-logformat', output) |
546 | + |
547 | +class UpstartTemplateTestCase(BaseTemplateTestCase): |
548 | + UPSTART_EXPECTED = textwrap.dedent(""" |
549 | + #-------------------------------------------------------------- |
550 | + # This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN |
551 | + #-------------------------------------------------------------- |
552 | + |
553 | + description "Gunicorn daemon for SERVICE_NAME" |
554 | + |
555 | + start on (local-filesystems and net-device-up IFACE=eth0) |
556 | + stop on runlevel [!12345] |
557 | + |
558 | + # If the process quits unexpectadly trigger a respawn |
559 | + respawn |
560 | + respawn limit 10 5 |
561 | + |
562 | + setuid WSGI_USER |
563 | + setgid WSGI_GROUP |
564 | + chdir WORKING_DIR |
565 | + |
566 | + exec GUNICORN_PATH WSGI_CLI --config WSGI_CONFIG_FILE WSGI_WSGI_FILE |
567 | + """).strip() |
568 | + |
569 | + def test_upstart_template(self): |
570 | + ctx = self.get_test_context() |
571 | + hooks.process_template('upstart.tmpl', ctx, 'path') |
572 | + output = self.file.write.call_args[0][0] |
573 | + self.assertMultiLineEqual(self.UPSTART_EXPECTED, output) |
574 | + |
575 | + |
576 | +class RunnerTemplateTestCase(BaseTemplateTestCase): |
577 | + RUNNER_EXPECTED = textwrap.dedent(""" |
578 | + #!/bin/bash |
579 | + #-------------------------------------------------------------- |
580 | + # This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN |
581 | + #-------------------------------------------------------------- |
582 | + cd WORKING_DIR |
583 | + GUNICORN_PATH WSGI_CLI --config WSGI_CONFIG_FILE $@ WSGI_WSGI_FILE |
584 | + """).strip() |
585 | + |
586 | + def test_runner_template(self): |
587 | + ctx = self.get_test_context() |
588 | + hooks.process_template('runner.tmpl', ctx, 'path') |
589 | + output = self.file.write.call_args[0][0] |
590 | + self.assertMultiLineEqual(self.RUNNER_EXPECTED, output) |
591 | + |
592 | + |
593 | +class ConfigTemplateTestCase(BaseTemplateTestCase): |
594 | + |
595 | + def get_module(self, ctx): |
596 | + hooks.process_template('config.tmpl', ctx, 'path') |
597 | + config = self.file.write.call_args[0][0] |
598 | + mod = ModuleType('config') |
599 | + exec(config, mod.__dict__) |
600 | + return mod |
601 | + |
602 | + def assert_basic(self, cfg): |
603 | + self.assertEqual(cfg.name, "SERVICE_NAME") |
604 | + self.assertEqual(cfg.workers, 1) |
605 | + self.assertEqual(cfg.worker_class, "WSGI_WORKER_CLASS") |
606 | + self.assertEqual(cfg.worker_connections, 1) |
607 | + self.assertEqual(cfg.max_requests, 1) |
608 | + self.assertEqual(cfg.backlog, 1) |
609 | + self.assertEqual(cfg.timeout, 1) |
610 | + self.assertEqual(cfg.keep_alive, 1) |
611 | + self.assertEqual(cfg.umask, 1) |
612 | + self.assertEqual(cfg.bind, ['LISTEN_IP:PORT']) |
613 | + self.assertEqual(cfg.log_file, "WSGI_LOG_FILE") |
614 | + self.assertEqual(cfg.log_level, "WSGI_LOG_LEVEL") |
615 | + self.assertEqual(cfg.pidfile, "PID_FILE") |
616 | + |
617 | + def test_config_template(self): |
618 | + ctx = self.get_test_context() |
619 | + mod = self.get_module(ctx) |
620 | + self.assert_basic(mod) |
621 | + # optional config should not be there |
622 | + self.assertFalse(hasattr(mod, 'accesslog')) |
623 | + self.assertFalse(hasattr(mod, 'access_log_format')) |
624 | + self.assertFalse(hasattr(mod, 'pythonpath')) |
625 | + self.assertFalse(hasattr(mod, 'raw_env')) |
626 | + |
627 | + def test_access_logfile(self): |
628 | + ctx = self.get_test_context() |
629 | + ctx['wsgi_access_logfile'] = "WSGI_ACCESS_LOGFILE" |
630 | + mod = self.get_module(ctx) |
631 | + self.assert_basic(mod) |
632 | + self.assertEqual(mod.accesslog, 'WSGI_ACCESS_LOGFILE') |
633 | + self.assertFalse(hasattr(mod, 'access_log_format')) |
634 | + |
635 | + def test_access_logformat(self): |
636 | + ctx = self.get_test_context() |
637 | + ctx['wsgi_access_logformat'] = "WSGI_ACCESS_LOGFORMAT" |
638 | + mod = self.get_module(ctx) |
639 | + self.assert_basic(mod) |
640 | + self.assertEqual(mod.access_log_format, 'WSGI_ACCESS_LOGFORMAT') |
641 | + self.assertFalse(hasattr(mod, 'accesslog')) |
642 | + |
643 | + def test_errorlog(self): |
644 | + ctx = self.get_test_context() |
645 | + ctx['wsgi_error_logfile'] = "WSGI_ERROR_LOGFILE" |
646 | + mod = self.get_module(ctx) |
647 | + self.assert_basic(mod) |
648 | + self.assertEqual(mod.errorlog, 'WSGI_ERROR_LOGFILE') |
649 | + self.assertFalse(hasattr(mod, 'access_log_format')) |
650 | + |
651 | + def test_pythonpath(self): |
652 | + ctx = self.get_test_context() |
653 | + ctx['python_path'] = "PYTHONPATH" |
654 | + mod = self.get_module(ctx) |
655 | + self.assert_basic(mod) |
656 | + self.assertEqual(mod.pythonpath, 'PYTHONPATH') |
657 | + |
658 | + def test_env_extra(self): |
659 | + ctx = self.get_test_context() |
660 | + ctx['env_extra'] = [['A', '1'], ['B', '2 3']] |
661 | + mod = self.get_module(ctx) |
662 | + self.assert_basic(mod) |
663 | + self.assertEqual(mod.raw_env, ['A=1', 'B=2 3']) |
664 | + |
665 | + def test_config_extra(self): |
666 | + ctx = self.get_test_context() |
667 | + ctx['wsgi_extra_config_lines'] = ["x=1", "y='2'"] |
668 | + mod = self.get_module(ctx) |
669 | + self.assert_basic(mod) |
670 | + self.assertEqual(mod.x, 1) |
671 | + self.assertEqual(mod.y, '2') |
672 | |
673 | === modified file 'metadata.yaml' |
674 | --- metadata.yaml 2014-02-27 10:20:51 +0000 |
675 | +++ metadata.yaml 2016-09-13 16:09:18 +0000 |
676 | @@ -1,7 +1,7 @@ |
677 | name: gunicorn |
678 | -summary: Gunicorn |
679 | -maintainer: Patrick Hetu <patrick.hetu@gmail.com> |
680 | -categories: ["misc"] |
681 | +summary: Gunicorn http python wsgi server |
682 | +maintainers: ['Patrick Hetu <patrick.hetu@gmail.com>', 'Simon Davy <simon.davy@canonical.com>'] |
683 | +tags: ['python', 'django', 'wsgi', 'web'] |
684 | description: | |
685 | Gunicorn or Green Unicorn is a Python WSGI HTTP Server for UNIX. It's a |
686 | pre-fork worker model ported from Ruby's Unicorn project. The Gunicorn server |
687 | |
688 | === added file 'requirements.txt' |
689 | --- requirements.txt 1970-01-01 00:00:00 +0000 |
690 | +++ requirements.txt 2016-09-13 16:09:18 +0000 |
691 | @@ -0,0 +1,4 @@ |
692 | +mock |
693 | +nose |
694 | +pyyaml |
695 | +coverage |
696 | |
697 | === modified file 'revision' |
698 | --- revision 2014-04-22 10:02:51 +0000 |
699 | +++ revision 2016-09-13 16:09:18 +0000 |
700 | @@ -1,1 +1,1 @@ |
701 | -4 |
702 | +5 |
703 | |
704 | === added file 'templates/config.tmpl' |
705 | --- templates/config.tmpl 1970-01-01 00:00:00 +0000 |
706 | +++ templates/config.tmpl 2016-09-13 16:09:18 +0000 |
707 | @@ -0,0 +1,41 @@ |
708 | +#-------------------------------------------------------------- |
709 | +# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN |
710 | +#-------------------------------------------------------------- |
711 | +# |
712 | +# config for gunicorn service {{ service_name }} |
713 | + |
714 | +name = "{{ service_name }}" |
715 | +workers = {{ wsgi_workers }} |
716 | +worker_class = "{{ wsgi_worker_class }}" |
717 | +worker_connections = {{ wsgi_worker_connections }} |
718 | +max_requests = {{ wsgi_max_requests }} |
719 | +backlog = {{ wsgi_backlog }} |
720 | +timeout = {{ wsgi_timeout }} |
721 | +keep_alive = {{ wsgi_keep_alive }} |
722 | +umask = {{ wsgi_umask }} |
723 | +bind = ['{{ listen_ip }}:{{ port }}'] |
724 | +log_file = "{{ wsgi_log_file }}" |
725 | +log_level = "{{ wsgi_log_level }}" |
726 | +pidfile = "{{ pid_file }}" |
727 | +{% if wsgi_access_logfile %} |
728 | +accesslog = "{{ wsgi_access_logfile }}" |
729 | +{% endif %} |
730 | +{% if wsgi_access_logformat %} |
731 | +access_log_format = "{{ wsgi_access_logformat }}" |
732 | +{% endif %} |
733 | +{% if wsgi_error_logfile %} |
734 | +errorlog = "{{ wsgi_error_logfile }}" |
735 | +{% endif %} |
736 | +{% if python_path %} |
737 | +pythonpath = "{{ python_path }}" |
738 | +{% endif %} |
739 | +{% for line in wsgi_extra_config_lines %} |
740 | +{{ line }} |
741 | +{% endfor %} |
742 | +{% if env_extra %} |
743 | +raw_env = [ |
744 | + {% for k,v in env_extra -%} |
745 | + '{{k }}={{ v }}', |
746 | + {% endfor -%} |
747 | +] |
748 | +{% endif %} |
749 | |
750 | === added file 'templates/runner.tmpl' |
751 | --- templates/runner.tmpl 1970-01-01 00:00:00 +0000 |
752 | +++ templates/runner.tmpl 2016-09-13 16:09:18 +0000 |
753 | @@ -0,0 +1,6 @@ |
754 | +#!/bin/bash |
755 | +#-------------------------------------------------------------- |
756 | +# This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN |
757 | +#-------------------------------------------------------------- |
758 | +cd {{ working_dir }} |
759 | +{{ gunicorn_path }} {{ wsgi_cli }} --config {{ wsgi_config_file }} $@ {{ wsgi_wsgi_file }} |
760 | |
761 | === modified file 'templates/upstart.tmpl' |
762 | --- templates/upstart.tmpl 2016-01-28 09:14:23 +0000 |
763 | +++ templates/upstart.tmpl 2016-09-13 16:09:18 +0000 |
764 | @@ -2,7 +2,7 @@ |
765 | # This file is managed by Juju; ANY CHANGES WILL BE OVERWRITTEN |
766 | #-------------------------------------------------------------- |
767 | |
768 | -description "Gunicorn daemon for the {{ project_name }} project" |
769 | +description "Gunicorn daemon for {{ service_name }}" |
770 | |
771 | start on (local-filesystems and net-device-up IFACE=eth0) |
772 | stop on runlevel [!12345] |
773 | @@ -15,32 +15,4 @@ |
774 | setgid {{ wsgi_group }} |
775 | chdir {{ working_dir }} |
776 | |
777 | -# This line can be removed and replace with the --pythonpath {{ python_path }} \ |
778 | -# option with Gunicorn>1.17 |
779 | -env PYTHONPATH={{ python_path }} |
780 | -{% for name, value in env_extra -%} |
781 | -env {{ name }}="{{ value }}" |
782 | -{% endfor %} |
783 | - |
784 | -exec gunicorn \ |
785 | - --name={{ project_name }} \ |
786 | - --workers={{ wsgi_workers }} \ |
787 | - --worker-class={{ wsgi_worker_class }} \ |
788 | - --worker-connections={{ wsgi_worker_connections }} \ |
789 | - --max-requests={{ wsgi_max_requests }} \ |
790 | - --backlog={{ wsgi_backlog }} \ |
791 | - --timeout={{ wsgi_timeout }} \ |
792 | - --keep-alive={{ wsgi_keep_alive }} \ |
793 | - --umask={{ wsgi_umask }} \ |
794 | - --bind={{ listen_ip }}:{{ port }} \ |
795 | - --log-file={{ wsgi_log_file }} \ |
796 | - --log-level={{ wsgi_log_level }} \ |
797 | - --pid={{ pid_file }} \ |
798 | - {%- if wsgi_access_logfile %} |
799 | - --access-logfile={{ wsgi_access_logfile }} \ |
800 | - {%- endif %} |
801 | - {%- if wsgi_access_logformat %} |
802 | - --access-logformat={{ wsgi_access_logformat }} \ |
803 | - {%- endif %} |
804 | - {{ wsgi_extra }} \ |
805 | - {{ wsgi_wsgi_file }} |
806 | +exec {{ gunicorn_path }} {{ wsgi_cli }} --config {{ wsgi_config_file }} {{ wsgi_wsgi_file }} |