Merge lp:~bloodearnest/charms/trusty/gunicorn/config-rework into lp:charms/trusty/gunicorn

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
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 }}

Subscribers

People subscribed via source and target branches