Merge lp:~doanac/ubuntu-ci-services-itself/bsbuilder into lp:ubuntu-ci-services-itself
- bsbuilder
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Francis Ginther |
Approved revision: | 25 |
Merged at revision: | 28 |
Proposed branch: | lp:~doanac/ubuntu-ci-services-itself/bsbuilder |
Merge into: | lp:ubuntu-ci-services-itself |
Diff against target: |
800 lines (+720/-2) 12 files modified
README (+31/-2) branch-source-builder/bsbuilder/__init__.py (+17/-0) branch-source-builder/bsbuilder/amqp_utils.py (+108/-0) branch-source-builder/bsbuilder/resources/root.py (+30/-0) branch-source-builder/bsbuilder/resources/v1.py (+53/-0) branch-source-builder/bsbuilder/tests/test_utils.py (+165/-0) branch-source-builder/bsbuilder/tests/test_v1.py (+70/-0) branch-source-builder/bsbuilder/utils.py (+78/-0) branch-source-builder/bsbuilder/wsgi.py (+48/-0) branch-source-builder/run_worker (+47/-0) branch-source-builder/setup.py (+44/-0) juju-deployer/branch-source-builder.yaml (+29/-0) |
To merge this branch: | bzr merge lp:~doanac/ubuntu-ci-services-itself/bsbuilder |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Francis Ginther | Approve | ||
Review via email: mp+198308@code.launchpad.net |
Commit message
Implements the webservice portion of the branch source builder
This creates a simple app using restish that takes a build_source request and sends it to rabbitmq. On the other end we have a simple worker script, branch-
The MP itself is bigger than the service because of the groundwork I did for the restish service.
Description of the change
Implements the webservice portion of the branch source builder
This creates a simple app using restish that takes a build_source request and sends it to rabbitmq. On the other end we have a simple worker script, branch-
The MP itself is bigger than the service because of the groundwork I did for the restish service.
Andy Doan (doanac) wrote : | # |
Andy Doan (doanac) wrote : | # |
I've fixed the rabbitmq-server issue mentioned above by creating our own rabbitmq-server charm with a fix.
I've identified the solution to the 2nd problem and should have a minor fix for that ready soon.
- 24. By Andy Doan
-
move queue helpers to their own module
This makes the queue helpers more accessible by the run_worker
script w/o making it have to pull in python-restish dependencies.It also uses the new common configuration file mechanism thats
been changed in the rabbitmq-worker charm to be like the
restish charm.
Andy Doan (doanac) wrote : | # |
as of revno 24, things "just work".
Francis Ginther (fginther) wrote : | # |
In addition to eventually refactoring the queue helpers into a common utils directory as you mentioned:
- when refactoring, be sure to split the tests in branch-
- branch-
- wsgi.py starts the server on port 8080, but prints '8000'. Is there a guideline for choosing ports? My local jenkins runs on 8080 also so I can't start this locally as is.
Not having done a REST app before, I don't have a lot of input. I'm in favor of how you're doing this, but not familiar with any alternatives. I did spend extra time reviewing the queue setup and like what's going on there.
Want to do some local testing before approving.
Francis Ginther (fginther) wrote : | # |
Was able to deploy successfully after adjusting the juju-deployer file to point to this branch.
Let's go with this and tweak it over time.
Preview Diff
1 | === modified file 'README' | |||
2 | --- README 2013-11-25 17:45:20 +0000 | |||
3 | +++ README 2013-12-11 16:52:29 +0000 | |||
4 | @@ -4,8 +4,14 @@ | |||
5 | 4 | Development environments can be set of using the setup.py files in the | 4 | Development environments can be set of using the setup.py files in the |
6 | 5 | projects you wish to work on. The easiest approach is to use python-virtualenv. | 5 | projects you wish to work on. The easiest approach is to use python-virtualenv. |
7 | 6 | Since most projects require the ci-utils project, that should almost always | 6 | Since most projects require the ci-utils project, that should almost always |
10 | 7 | get setup first:: | 7 | get setup first. |
11 | 8 | 8 | ||
12 | 9 | There are two types of services written in this project, Django and Restish. | ||
13 | 10 | Development varies slightly between the two. | ||
14 | 11 | |||
15 | 12 | Django | ||
16 | 13 | ~~~~~~ | ||
17 | 14 | :: | ||
18 | 9 | # setup the ppa-assigner project | 15 | # setup the ppa-assigner project |
19 | 10 | virtualenv /tmp/venv | 16 | virtualenv /tmp/venv |
20 | 11 | . /tmp/venv/bin/activate | 17 | . /tmp/venv/bin/activate |
21 | @@ -25,6 +31,29 @@ | |||
22 | 25 | ./ppa-assigner/manage.py test ppa_assigner.TestApi #test one class | 31 | ./ppa-assigner/manage.py test ppa_assigner.TestApi #test one class |
23 | 26 | ./ppa-assigner/manage.py test ppa_assigner.TestApi.testFree # test one method | 32 | ./ppa-assigner/manage.py test ppa_assigner.TestApi.testFree # test one method |
24 | 27 | 33 | ||
25 | 34 | Restish | ||
26 | 35 | ~~~~~~~ | ||
27 | 36 | :: | ||
28 | 37 | virtualenv /tmp/venv | ||
29 | 38 | . /tmp/venv/bin/activate | ||
30 | 39 | ./branch-source-builder/setup.py develop | ||
31 | 40 | |||
32 | 41 | Unit-testing can be done with:: | ||
33 | 42 | |||
34 | 43 | python -m unittest bsbuilder.tests.test_example | ||
35 | 44 | |||
36 | 45 | Running under the python wsgi server can be done with:: | ||
37 | 46 | |||
38 | 47 | ./branch-source-builder/bsbuilder/wsgi.py | ||
39 | 48 | |||
40 | 49 | Running under gunicorn can be done with:: | ||
41 | 50 | |||
42 | 51 | # need gunicorn: | ||
43 | 52 | pip install gunicorn | ||
44 | 53 | |||
45 | 54 | # TIP: by setting max-requests=1 you can make changes to the source code | ||
46 | 55 | # and they'll be picked up in the next http request you make!! | ||
47 | 56 | gunicorn --max-requests 1 bsbuilder.wsgi:app | ||
48 | 28 | 57 | ||
49 | 29 | Juju Testing | 58 | Juju Testing |
50 | 30 | ------------ | 59 | ------------ |
51 | 31 | 60 | ||
52 | === added directory 'branch-source-builder' | |||
53 | === added directory 'branch-source-builder/bsbuilder' | |||
54 | === added file 'branch-source-builder/bsbuilder/__init__.py' | |||
55 | --- branch-source-builder/bsbuilder/__init__.py 1970-01-01 00:00:00 +0000 | |||
56 | +++ branch-source-builder/bsbuilder/__init__.py 2013-12-11 16:52:29 +0000 | |||
57 | @@ -0,0 +1,17 @@ | |||
58 | 1 | # Ubuntu CI Services | ||
59 | 2 | # Copyright 2013 Canonical Ltd. | ||
60 | 3 | |||
61 | 4 | # This program is free software: you can redistribute it and/or modify it | ||
62 | 5 | # under the terms of the GNU Affero General Public License version 3, as | ||
63 | 6 | # published by the Free Software Foundation. | ||
64 | 7 | |||
65 | 8 | # This program is distributed in the hope that it will be useful, but | ||
66 | 9 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
67 | 10 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
68 | 11 | # PURPOSE. See the GNU Affero General Public License for more details. | ||
69 | 12 | |||
70 | 13 | # You should have received a copy of the GNU Affero General Public License | ||
71 | 14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
72 | 15 | |||
73 | 16 | # TODO make this probe from changelog or bzr | ||
74 | 17 | __version__ = '0.1' | ||
75 | 0 | 18 | ||
76 | === added file 'branch-source-builder/bsbuilder/amqp_utils.py' | |||
77 | --- branch-source-builder/bsbuilder/amqp_utils.py 1970-01-01 00:00:00 +0000 | |||
78 | +++ branch-source-builder/bsbuilder/amqp_utils.py 2013-12-11 16:52:29 +0000 | |||
79 | @@ -0,0 +1,108 @@ | |||
80 | 1 | # Ubuntu CI Services | ||
81 | 2 | # Copyright 2013 Canonical Ltd. | ||
82 | 3 | |||
83 | 4 | # This program is free software: you can redistribute it and/or modify it | ||
84 | 5 | # under the terms of the GNU Affero General Public License version 3, as | ||
85 | 6 | # published by the Free Software Foundation. | ||
86 | 7 | |||
87 | 8 | # This program is distributed in the hope that it will be useful, but | ||
88 | 9 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
89 | 10 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
90 | 11 | # PURPOSE. See the GNU Affero General Public License for more details. | ||
91 | 12 | |||
92 | 13 | # You should have received a copy of the GNU Affero General Public License | ||
93 | 14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
94 | 15 | |||
95 | 16 | import logging | ||
96 | 17 | import os | ||
97 | 18 | import socket | ||
98 | 19 | import time | ||
99 | 20 | |||
100 | 21 | from amqplib import client_0_8 as amqp | ||
101 | 22 | |||
102 | 23 | log = logging.getLogger(__name__) | ||
103 | 24 | |||
104 | 25 | |||
105 | 26 | def get_config(): | ||
106 | 27 | '''Load the rabbit config created by the restish charm''' | ||
107 | 28 | config = None | ||
108 | 29 | try: | ||
109 | 30 | # try and find the config file, should be in the root of our bzr branch | ||
110 | 31 | f = os.path.join(os.path.dirname(__file__), '../../amqp_config.py') | ||
111 | 32 | if os.path.exists(f): | ||
112 | 33 | import imp | ||
113 | 34 | config = imp.load_source('amqp_config', f) | ||
114 | 35 | else: | ||
115 | 36 | log.warn('No amqp_config found at: %s' % os.path.abspath(f)) | ||
116 | 37 | except: | ||
117 | 38 | log.exception('ERROR detecting rabbit args') | ||
118 | 39 | return config | ||
119 | 40 | |||
120 | 41 | |||
121 | 42 | def connection(rabbit_config): | ||
122 | 43 | return amqp.Connection( | ||
123 | 44 | userid=rabbit_config.AMQP_USER, | ||
124 | 45 | virtual_host=rabbit_config.AMQP_VHOST, | ||
125 | 46 | host=rabbit_config.AMQP_HOST, | ||
126 | 47 | password=rabbit_config.AMQP_PASSWORD | ||
127 | 48 | ) | ||
128 | 49 | |||
129 | 50 | |||
130 | 51 | def send(queue, msg): | ||
131 | 52 | config = get_config() | ||
132 | 53 | if not config: | ||
133 | 54 | return 'rabbitmq settings not available.' | ||
134 | 55 | con = channel = None | ||
135 | 56 | try: | ||
136 | 57 | con = connection(config) | ||
137 | 58 | channel = con.channel() | ||
138 | 59 | channel.queue_declare(queue=queue, durable=True, auto_delete=False) | ||
139 | 60 | body = amqp.Message(msg) | ||
140 | 61 | body.properties['delivery_mode'] = 2 # Persistent | ||
141 | 62 | channel.basic_publish(body, routing_key=queue) | ||
142 | 63 | except Exception as e: | ||
143 | 64 | logging.exception('unable to queue up build_source request') | ||
144 | 65 | return str(e) | ||
145 | 66 | finally: | ||
146 | 67 | if channel: | ||
147 | 68 | channel.close() | ||
148 | 69 | if con: | ||
149 | 70 | con.close() | ||
150 | 71 | |||
151 | 72 | |||
152 | 73 | def _run_forever(channel, queue, callback, retry_period=120): | ||
153 | 74 | tag = channel.basic_consume(callback=callback, queue=queue) | ||
154 | 75 | try: | ||
155 | 76 | timeout = time.time() | ||
156 | 77 | while time.time() < timeout + retry_period: | ||
157 | 78 | try: | ||
158 | 79 | channel.wait() | ||
159 | 80 | timeout = time.time() | ||
160 | 81 | except (amqp.AMQPConnectionException, socket.error): | ||
161 | 82 | logging.error('lost connection to Rabbit') | ||
162 | 83 | # TODO metrics.meter('lost_rabbit_connection') | ||
163 | 84 | # Don't probe immediately, give the network/process | ||
164 | 85 | # time to come back. | ||
165 | 86 | time.sleep(0.1) | ||
166 | 87 | logging.error('Rabbit did not reappear quickly enough.') | ||
167 | 88 | except KeyboardInterrupt: | ||
168 | 89 | pass | ||
169 | 90 | finally: | ||
170 | 91 | if channel and channel.is_open: | ||
171 | 92 | channel.basic_cancel(tag) | ||
172 | 93 | |||
173 | 94 | |||
174 | 95 | def process_queue(config, queue, callback): | ||
175 | 96 | conn = channel = None | ||
176 | 97 | try: | ||
177 | 98 | conn = connection(config) | ||
178 | 99 | channel = conn.channel() | ||
179 | 100 | channel.queue_declare(queue=queue, durable=True, auto_delete=False) | ||
180 | 101 | channel.basic_qos(0, 1, False) | ||
181 | 102 | logging.info('Waiting for messages. ^C to exit.') | ||
182 | 103 | _run_forever(channel, queue, callback) | ||
183 | 104 | finally: | ||
184 | 105 | if channel: | ||
185 | 106 | channel.close() | ||
186 | 107 | if conn: | ||
187 | 108 | conn.close() | ||
188 | 0 | 109 | ||
189 | === added directory 'branch-source-builder/bsbuilder/resources' | |||
190 | === added file 'branch-source-builder/bsbuilder/resources/__init__.py' | |||
191 | === added file 'branch-source-builder/bsbuilder/resources/root.py' | |||
192 | --- branch-source-builder/bsbuilder/resources/root.py 1970-01-01 00:00:00 +0000 | |||
193 | +++ branch-source-builder/bsbuilder/resources/root.py 2013-12-11 16:52:29 +0000 | |||
194 | @@ -0,0 +1,30 @@ | |||
195 | 1 | # Ubuntu CI Services | ||
196 | 2 | # Copyright 2013 Canonical Ltd. | ||
197 | 3 | |||
198 | 4 | # This program is free software: you can redistribute it and/or modify it | ||
199 | 5 | # under the terms of the GNU Affero General Public License version 3, as | ||
200 | 6 | # published by the Free Software Foundation. | ||
201 | 7 | |||
202 | 8 | # This program is distributed in the hope that it will be useful, but | ||
203 | 9 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
204 | 10 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
205 | 11 | # PURPOSE. See the GNU Affero General Public License for more details. | ||
206 | 12 | |||
207 | 13 | # You should have received a copy of the GNU Affero General Public License | ||
208 | 14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
209 | 15 | |||
210 | 16 | import logging | ||
211 | 17 | |||
212 | 18 | from restish import resource | ||
213 | 19 | |||
214 | 20 | from bsbuilder.resources import v1 | ||
215 | 21 | |||
216 | 22 | |||
217 | 23 | log = logging.getLogger(__name__) | ||
218 | 24 | |||
219 | 25 | |||
220 | 26 | class Root(resource.Resource): | ||
221 | 27 | @resource.child('api/v1') | ||
222 | 28 | def api(self, request, segments): | ||
223 | 29 | log.debug('[api]: %s %s', request.url, str(segments)) | ||
224 | 30 | return v1.API() | ||
225 | 0 | 31 | ||
226 | === added file 'branch-source-builder/bsbuilder/resources/v1.py' | |||
227 | --- branch-source-builder/bsbuilder/resources/v1.py 1970-01-01 00:00:00 +0000 | |||
228 | +++ branch-source-builder/bsbuilder/resources/v1.py 2013-12-11 16:52:29 +0000 | |||
229 | @@ -0,0 +1,53 @@ | |||
230 | 1 | # Ubuntu CI Services | ||
231 | 2 | # Copyright 2013 Canonical Ltd. | ||
232 | 3 | |||
233 | 4 | # This program is free software: you can redistribute it and/or modify it | ||
234 | 5 | # under the terms of the GNU Affero General Public License version 3, as | ||
235 | 6 | # published by the Free Software Foundation. | ||
236 | 7 | |||
237 | 8 | # This program is distributed in the hope that it will be useful, but | ||
238 | 9 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
239 | 10 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
240 | 11 | # PURPOSE. See the GNU Affero General Public License for more details. | ||
241 | 12 | |||
242 | 13 | # You should have received a copy of the GNU Affero General Public License | ||
243 | 14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
244 | 15 | |||
245 | 16 | import json | ||
246 | 17 | import logging | ||
247 | 18 | |||
248 | 19 | from restish import http, resource | ||
249 | 20 | |||
250 | 21 | from bsbuilder import amqp_utils, utils | ||
251 | 22 | |||
252 | 23 | log = logging.getLogger(__name__) | ||
253 | 24 | |||
254 | 25 | |||
255 | 26 | def _status(): | ||
256 | 27 | return utils.json_ok({ | ||
257 | 28 | 'rabbit_configured': amqp_utils.get_config() is not None, | ||
258 | 29 | }) | ||
259 | 30 | |||
260 | 31 | |||
261 | 32 | def _build_source(source_packages, ppa, progress_trigger): | ||
262 | 33 | msg = json.dumps({ | ||
263 | 34 | 'source_packages': source_packages, | ||
264 | 35 | 'ppa': ppa, | ||
265 | 36 | 'progress_trigger': progress_trigger, | ||
266 | 37 | }) | ||
267 | 38 | r = amqp_utils.send('bsbuilder', msg) | ||
268 | 39 | if r: | ||
269 | 40 | # send only returns something if it an error message | ||
270 | 41 | r = http.service_unavailable(body=r) | ||
271 | 42 | return r | ||
272 | 43 | |||
273 | 44 | |||
274 | 45 | class API(resource.Resource): | ||
275 | 46 | @resource.child() | ||
276 | 47 | def status(self, request, segments): | ||
277 | 48 | log.debug('[status]: %s %s', request.url, str(segments)) | ||
278 | 49 | return utils.http_get_resource(_status) | ||
279 | 50 | |||
280 | 51 | @resource.child() | ||
281 | 52 | def build_source(self, request, segments): | ||
282 | 53 | return utils.http_post_resource(_build_source) | ||
283 | 0 | 54 | ||
284 | === added directory 'branch-source-builder/bsbuilder/tests' | |||
285 | === added file 'branch-source-builder/bsbuilder/tests/__init__.py' | |||
286 | === added file 'branch-source-builder/bsbuilder/tests/test_utils.py' | |||
287 | --- branch-source-builder/bsbuilder/tests/test_utils.py 1970-01-01 00:00:00 +0000 | |||
288 | +++ branch-source-builder/bsbuilder/tests/test_utils.py 2013-12-11 16:52:29 +0000 | |||
289 | @@ -0,0 +1,165 @@ | |||
290 | 1 | # Ubuntu CI Services | ||
291 | 2 | # Copyright 2013 Canonical Ltd. | ||
292 | 3 | |||
293 | 4 | # This program is free software: you can redistribute it and/or modify it | ||
294 | 5 | # under the terms of the GNU Affero General Public License version 3, as | ||
295 | 6 | # published by the Free Software Foundation. | ||
296 | 7 | |||
297 | 8 | # This program is distributed in the hope that it will be useful, but | ||
298 | 9 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
299 | 10 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
300 | 11 | # PURPOSE. See the GNU Affero General Public License for more details. | ||
301 | 12 | |||
302 | 13 | # You should have received a copy of the GNU Affero General Public License | ||
303 | 14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
304 | 15 | |||
305 | 16 | import json | ||
306 | 17 | import socket | ||
307 | 18 | import time | ||
308 | 19 | import unittest | ||
309 | 20 | |||
310 | 21 | import mock | ||
311 | 22 | import webtest | ||
312 | 23 | |||
313 | 24 | from restish import resource | ||
314 | 25 | from restish.app import RestishApp | ||
315 | 26 | |||
316 | 27 | from bsbuilder import amqp_utils, utils | ||
317 | 28 | |||
318 | 29 | |||
319 | 30 | def _no_param_action(): | ||
320 | 31 | return utils.json_ok({'func': _no_param_action.__name__}) | ||
321 | 32 | |||
322 | 33 | |||
323 | 34 | def _two_param_action(p1, p2): | ||
324 | 35 | return utils.json_ok({'func': _two_param_action.__name__}) | ||
325 | 36 | |||
326 | 37 | |||
327 | 38 | class TestGetResource(unittest.TestCase): | ||
328 | 39 | '''Test to ensure the http_get_resource works properly''' | ||
329 | 40 | |||
330 | 41 | def setUp(self): | ||
331 | 42 | super(TestGetResource, self).setUp() | ||
332 | 43 | |||
333 | 44 | class TR(resource.Resource): | ||
334 | 45 | @resource.child() | ||
335 | 46 | def empty(self, request, segments): | ||
336 | 47 | return utils.http_get_resource(_no_param_action) | ||
337 | 48 | |||
338 | 49 | @resource.child() | ||
339 | 50 | def two_param_get(self, request, segments): | ||
340 | 51 | return utils.http_get_resource(_two_param_action) | ||
341 | 52 | |||
342 | 53 | app = RestishApp(TR()) | ||
343 | 54 | self.app = webtest.TestApp(app, relative_to='.') | ||
344 | 55 | |||
345 | 56 | def testGetNoParam(self): | ||
346 | 57 | resp = self.app.get('/empty', status=200) | ||
347 | 58 | data = json.loads(resp.body) | ||
348 | 59 | self.assertEqual(data['func'], _no_param_action.__name__) | ||
349 | 60 | |||
350 | 61 | def testGetTwoParam(self): | ||
351 | 62 | resp = self.app.get('/two_param_get', {'p1': 1, 'p2': 2}, status=200) | ||
352 | 63 | data = json.loads(resp.body) | ||
353 | 64 | self.assertEqual(data['func'], _two_param_action.__name__) | ||
354 | 65 | |||
355 | 66 | # ensure a missing parameter returns a 400 error | ||
356 | 67 | resp = self.app.get('/two_param_get', {'p1': 1}, status=400) | ||
357 | 68 | self.assertTrue('p2' in resp.body) | ||
358 | 69 | |||
359 | 70 | def testLeafEnforced(self): | ||
360 | 71 | '''ensure we only respond to the leaf and not additional segments''' | ||
361 | 72 | self.app.get('/empty/', status=404) | ||
362 | 73 | |||
363 | 74 | def testAllowedMethods(self): | ||
364 | 75 | self.app.post_json('/empty', {'key': 'val'}, status=405) | ||
365 | 76 | |||
366 | 77 | |||
367 | 78 | class TestPostResource(unittest.TestCase): | ||
368 | 79 | '''Test to ensure the http_post_resource works properly''' | ||
369 | 80 | |||
370 | 81 | def setUp(self): | ||
371 | 82 | super(TestPostResource, self).setUp() | ||
372 | 83 | |||
373 | 84 | class TR(resource.Resource): | ||
374 | 85 | @resource.child() | ||
375 | 86 | def empty(self, request, segments): | ||
376 | 87 | return utils.http_post_resource(_no_param_action) | ||
377 | 88 | |||
378 | 89 | @resource.child() | ||
379 | 90 | def two_param_get(self, request, segments): | ||
380 | 91 | return utils.http_post_resource(_two_param_action) | ||
381 | 92 | |||
382 | 93 | app = RestishApp(TR()) | ||
383 | 94 | self.app = webtest.TestApp(app, relative_to='.') | ||
384 | 95 | |||
385 | 96 | def testPostNoParam(self): | ||
386 | 97 | resp = self.app.post_json('/empty', {}, status=200) | ||
387 | 98 | data = json.loads(resp.body) | ||
388 | 99 | self.assertEqual(data['func'], _no_param_action.__name__) | ||
389 | 100 | |||
390 | 101 | def testPostTwoParam(self): | ||
391 | 102 | resp = self.app.post_json('/two_param_get', {'p1': 1, 'p2': 2}) | ||
392 | 103 | data = json.loads(resp.body) | ||
393 | 104 | self.assertEqual(data['func'], _two_param_action.__name__) | ||
394 | 105 | |||
395 | 106 | # ensure a missing parameter returns a 400 error | ||
396 | 107 | resp = self.app.post_json('/two_param_get', {'p1': 1}, status=400) | ||
397 | 108 | self.assertTrue('p2' in resp.body) | ||
398 | 109 | |||
399 | 110 | def testLeafEnforced(self): | ||
400 | 111 | '''ensure we only respond to the leaf and not additional segments''' | ||
401 | 112 | self.app.post_json('/empty/', {}, status=404) | ||
402 | 113 | |||
403 | 114 | def testAllowedMethods(self): | ||
404 | 115 | self.app.get('/empty', {'p1': 1, 'p2': 2}, status=405) | ||
405 | 116 | |||
406 | 117 | |||
407 | 118 | class TestAMQP(unittest.TestCase): | ||
408 | 119 | @mock.patch('bsbuilder.amqp_utils.connection') | ||
409 | 120 | @mock.patch('bsbuilder.amqp_utils.get_config') | ||
410 | 121 | def testConnectFailed(self, get_config, connect): | ||
411 | 122 | '''Ensure a failed queue connection returns an HTTP 503 error''' | ||
412 | 123 | get_config.return_value = mock.Mock() | ||
413 | 124 | error = 'mocked test exception' | ||
414 | 125 | connect.side_effect = RuntimeError(error) | ||
415 | 126 | r = amqp_utils.send('fake_queue', 'fake_message') | ||
416 | 127 | self.assertIsNotNone(r) | ||
417 | 128 | self.assertEqual(error, r) | ||
418 | 129 | |||
419 | 130 | @mock.patch('bsbuilder.amqp_utils.connection') | ||
420 | 131 | @mock.patch('bsbuilder.amqp_utils.get_config') | ||
421 | 132 | def testSent(self, get_config, connect): | ||
422 | 133 | '''Test a successful send returns nothing | ||
423 | 134 | |||
424 | 135 | There's not much you can test in isolation here, but this gives | ||
425 | 136 | a bit of a sanity check. | ||
426 | 137 | ''' | ||
427 | 138 | get_config.return_value = mock.Mock() | ||
428 | 139 | connect.return_value = mock.Mock() | ||
429 | 140 | r = amqp_utils.send('fake_queue', 'fake_message') | ||
430 | 141 | self.assertIsNone(r) | ||
431 | 142 | |||
432 | 143 | @mock.patch('bsbuilder.amqp_utils._run_forever') | ||
433 | 144 | @mock.patch('bsbuilder.amqp_utils.connection') | ||
434 | 145 | def testProcessQueue(self, connection, run_forever): | ||
435 | 146 | '''Ensure we close the connection if something fails''' | ||
436 | 147 | conn = mock.Mock() | ||
437 | 148 | connection.return_value = conn | ||
438 | 149 | run_forever.side_effect = RuntimeError | ||
439 | 150 | with self.assertRaises(RuntimeError): | ||
440 | 151 | amqp_utils.process_queue(None, None, None) | ||
441 | 152 | self.assertTrue(conn.close.called) | ||
442 | 153 | |||
443 | 154 | def testRunForever(self): | ||
444 | 155 | '''Ensure this times out after the right amount of time''' | ||
445 | 156 | channel = mock.Mock() | ||
446 | 157 | callback = mock.Mock() | ||
447 | 158 | #wait = mock.Mock() | ||
448 | 159 | #wait.side_effect = socket.error | ||
449 | 160 | channel.wait.side_effect = socket.error() | ||
450 | 161 | start = time.time() | ||
451 | 162 | retry_period = 2 | ||
452 | 163 | amqp_utils._run_forever(channel, 'foo', callback, retry_period) | ||
453 | 164 | # see if it was within 1/10 of a second of the retry_period | ||
454 | 165 | self.assertAlmostEqual(start + retry_period, time.time(), places=1) | ||
455 | 0 | 166 | ||
456 | === added file 'branch-source-builder/bsbuilder/tests/test_v1.py' | |||
457 | --- branch-source-builder/bsbuilder/tests/test_v1.py 1970-01-01 00:00:00 +0000 | |||
458 | +++ branch-source-builder/bsbuilder/tests/test_v1.py 2013-12-11 16:52:29 +0000 | |||
459 | @@ -0,0 +1,70 @@ | |||
460 | 1 | # Ubuntu CI Services | ||
461 | 2 | # Copyright 2013 Canonical Ltd. | ||
462 | 3 | |||
463 | 4 | # This program is free software: you can redistribute it and/or modify it | ||
464 | 5 | # under the terms of the GNU Affero General Public License version 3, as | ||
465 | 6 | # published by the Free Software Foundation. | ||
466 | 7 | |||
467 | 8 | # This program is distributed in the hope that it will be useful, but | ||
468 | 9 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
469 | 10 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
470 | 11 | # PURPOSE. See the GNU Affero General Public License for more details. | ||
471 | 12 | |||
472 | 13 | # You should have received a copy of the GNU Affero General Public License | ||
473 | 14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
474 | 15 | |||
475 | 16 | import json | ||
476 | 17 | import unittest | ||
477 | 18 | |||
478 | 19 | import mock | ||
479 | 20 | import webtest | ||
480 | 21 | |||
481 | 22 | from bsbuilder import wsgi | ||
482 | 23 | |||
483 | 24 | |||
484 | 25 | class TestAPI(unittest.TestCase): | ||
485 | 26 | '''Test to ensure the v1 API works.''' | ||
486 | 27 | |||
487 | 28 | def setUp(self): | ||
488 | 29 | super(TestAPI, self).setUp() | ||
489 | 30 | self.app = webtest.TestApp(wsgi.app, relative_to='.') | ||
490 | 31 | |||
491 | 32 | @mock.patch('bsbuilder.amqp_utils.get_config') | ||
492 | 33 | def testStatus(self, get_config): | ||
493 | 34 | get_config.return_value = None | ||
494 | 35 | resp = self.app.get('/api/v1/status', status=200) | ||
495 | 36 | data = json.loads(resp.body) | ||
496 | 37 | self.assertEqual(False, data['rabbit_configured']) | ||
497 | 38 | |||
498 | 39 | get_config.return_value = {'foo': 'bar'} | ||
499 | 40 | resp = self.app.get('/api/v1/status', status=200) | ||
500 | 41 | data = json.loads(resp.body) | ||
501 | 42 | self.assertEqual(True, data['rabbit_configured']) | ||
502 | 43 | |||
503 | 44 | @mock.patch('bsbuilder.amqp_utils.get_config') | ||
504 | 45 | def testBuildSourceUnconfigured(self, get_config): | ||
505 | 46 | # ensure it fails when not configured | ||
506 | 47 | get_config.return_value = None | ||
507 | 48 | params = { | ||
508 | 49 | 'source_packages': 'foo', | ||
509 | 50 | 'ppa': 'foo', | ||
510 | 51 | 'progress_trigger': 'foo', | ||
511 | 52 | } | ||
512 | 53 | resp = self.app.post_json('/api/v1/build_source', params, status=503) | ||
513 | 54 | self.assertTrue('rabbitmq settings not available' in resp.body) | ||
514 | 55 | |||
515 | 56 | def testBuildSourceBadParams(self): | ||
516 | 57 | '''Ensure proper error message is returned for incorrect params.''' | ||
517 | 58 | resp = self.app.post_json('/api/v1/build_source', {}, status=400) | ||
518 | 59 | self.assertTrue('Missing required parameters' in resp.body) | ||
519 | 60 | |||
520 | 61 | @mock.patch('bsbuilder.amqp_utils.send') | ||
521 | 62 | def testBuildSource(self, send): | ||
522 | 63 | send.return_value = None | ||
523 | 64 | params = { | ||
524 | 65 | 'source_packages': 'foo', | ||
525 | 66 | 'ppa': 'foo', | ||
526 | 67 | 'progress_trigger': 'foo', | ||
527 | 68 | } | ||
528 | 69 | r = self.app.post_json('/api/v1/build_source', params, status=204) | ||
529 | 70 | self.assertEqual('', r.body) | ||
530 | 0 | 71 | ||
531 | === added file 'branch-source-builder/bsbuilder/utils.py' | |||
532 | --- branch-source-builder/bsbuilder/utils.py 1970-01-01 00:00:00 +0000 | |||
533 | +++ branch-source-builder/bsbuilder/utils.py 2013-12-11 16:52:29 +0000 | |||
534 | @@ -0,0 +1,78 @@ | |||
535 | 1 | # Ubuntu CI Services | ||
536 | 2 | # Copyright 2013 Canonical Ltd. | ||
537 | 3 | |||
538 | 4 | # This program is free software: you can redistribute it and/or modify it | ||
539 | 5 | # under the terms of the GNU Affero General Public License version 3, as | ||
540 | 6 | # published by the Free Software Foundation. | ||
541 | 7 | |||
542 | 8 | # This program is distributed in the hope that it will be useful, but | ||
543 | 9 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
544 | 10 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
545 | 11 | # PURPOSE. See the GNU Affero General Public License for more details. | ||
546 | 12 | |||
547 | 13 | # You should have received a copy of the GNU Affero General Public License | ||
548 | 14 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
549 | 15 | |||
550 | 16 | import inspect | ||
551 | 17 | import logging | ||
552 | 18 | import json | ||
553 | 19 | |||
554 | 20 | from restish import http, resource | ||
555 | 21 | |||
556 | 22 | log = logging.getLogger(__name__) | ||
557 | 23 | |||
558 | 24 | |||
559 | 25 | def json_ok(data): | ||
560 | 26 | '''Simple wrapper to return JSON data as an OK response''' | ||
561 | 27 | data = json.dumps(data) + '\n' # \n is handy when using curl from CLI | ||
562 | 28 | return http.ok([('Content-Type', 'application/json')], data) | ||
563 | 29 | |||
564 | 30 | |||
565 | 31 | class _BaseResource(resource.Resource): | ||
566 | 32 | '''Creates a resource that validates arguments by introspcection. | ||
567 | 33 | |||
568 | 34 | This class makes it easy to supply a single function with parameters | ||
569 | 35 | to handle HTTP operations. The GET/POST parameters will be compared to | ||
570 | 36 | the callback function's parameter list to validate it can be called. | ||
571 | 37 | ''' | ||
572 | 38 | def __init__(self, callback_func, allowed): | ||
573 | 39 | super(_BaseResource, self).__init__() | ||
574 | 40 | self._cb = callback_func | ||
575 | 41 | self._allowed = allowed | ||
576 | 42 | |||
577 | 43 | def _handle(self, req_args): | ||
578 | 44 | args, vargs, keywords, defaults = inspect.getargspec(self._cb) | ||
579 | 45 | missing = set(args) - set(req_args.keys()) | ||
580 | 46 | unknown = set(req_args.keys()) - set(args) | ||
581 | 47 | if unknown: | ||
582 | 48 | log.warning('Unsupported arguments: %r' % unknown) | ||
583 | 49 | if missing: | ||
584 | 50 | body = 'Missing required parameters\n %s\n' % '\n '.join(missing) | ||
585 | 51 | return http.bad_request([('Content-Type', 'text/plain')], body) | ||
586 | 52 | |||
587 | 53 | params = {k: req_args[k] for k in args} | ||
588 | 54 | return self._cb(**params) | ||
589 | 55 | |||
590 | 56 | @resource.GET() | ||
591 | 57 | def _do_get(self, request): | ||
592 | 58 | if resource.GET not in self._allowed: | ||
593 | 59 | return http.method_not_allowed([request.method]) | ||
594 | 60 | return self._handle(request.GET) | ||
595 | 61 | |||
596 | 62 | @resource.POST(accept='json') | ||
597 | 63 | def _do_post(self, request): | ||
598 | 64 | if resource.POST not in self._allowed: | ||
599 | 65 | return http.method_not_allowed([request.method]) | ||
600 | 66 | params = json.loads(request.body) | ||
601 | 67 | resp = self._handle(params) | ||
602 | 68 | if not resp: | ||
603 | 69 | resp = http.Response('204 No Content', [], None) | ||
604 | 70 | return resp | ||
605 | 71 | |||
606 | 72 | |||
607 | 73 | def http_get_resource(callback_func): | ||
608 | 74 | return _BaseResource(callback_func, (resource.GET,)) | ||
609 | 75 | |||
610 | 76 | |||
611 | 77 | def http_post_resource(callback_func): | ||
612 | 78 | return _BaseResource(callback_func, (resource.POST,)) | ||
613 | 0 | 79 | ||
614 | === added file 'branch-source-builder/bsbuilder/wsgi.py' | |||
615 | --- branch-source-builder/bsbuilder/wsgi.py 1970-01-01 00:00:00 +0000 | |||
616 | +++ branch-source-builder/bsbuilder/wsgi.py 2013-12-11 16:52:29 +0000 | |||
617 | @@ -0,0 +1,48 @@ | |||
618 | 1 | #!/usr/bin/env python | ||
619 | 2 | # Ubuntu CI Services | ||
620 | 3 | # Copyright 2013 Canonical Ltd. | ||
621 | 4 | |||
622 | 5 | # This program is free software: you can redistribute it and/or modify it | ||
623 | 6 | # under the terms of the GNU Affero General Public License version 3, as | ||
624 | 7 | # published by the Free Software Foundation. | ||
625 | 8 | |||
626 | 9 | # This program is distributed in the hope that it will be useful, but | ||
627 | 10 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
628 | 11 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
629 | 12 | # PURPOSE. See the GNU Affero General Public License for more details. | ||
630 | 13 | |||
631 | 14 | # You should have received a copy of the GNU Affero General Public License | ||
632 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
633 | 16 | |||
634 | 17 | import logging | ||
635 | 18 | from restish.app import RestishApp | ||
636 | 19 | |||
637 | 20 | from bsbuilder.resources.root import Root | ||
638 | 21 | |||
639 | 22 | logging.basicConfig(level=logging.DEBUG) | ||
640 | 23 | |||
641 | 24 | |||
642 | 25 | def make_app(): | ||
643 | 26 | """Build the wsgi app object.""" | ||
644 | 27 | app = RestishApp(Root()) | ||
645 | 28 | return app | ||
646 | 29 | |||
647 | 30 | app = make_app() | ||
648 | 31 | |||
649 | 32 | if __name__ == '__main__': | ||
650 | 33 | import argparse | ||
651 | 34 | from wsgiref.simple_server import make_server | ||
652 | 35 | |||
653 | 36 | parser = argparse.ArgumentParser( | ||
654 | 37 | description='Run webservice in python\'s wsgi reference server.') | ||
655 | 38 | parser.add_argument('-p', '--port', type=int, default=8080, | ||
656 | 39 | help='Port to use. Default=%(default)d') | ||
657 | 40 | args = parser.parse_args() | ||
658 | 41 | |||
659 | 42 | httpd = make_server('', args.port, app) | ||
660 | 43 | try: | ||
661 | 44 | print('Running server on port %d...' % args.port) | ||
662 | 45 | httpd.serve_forever() | ||
663 | 46 | except KeyboardInterrupt: | ||
664 | 47 | print('exiting') | ||
665 | 48 | pass | ||
666 | 0 | 49 | ||
667 | === added file 'branch-source-builder/run_worker' | |||
668 | --- branch-source-builder/run_worker 1970-01-01 00:00:00 +0000 | |||
669 | +++ branch-source-builder/run_worker 2013-12-11 16:52:29 +0000 | |||
670 | @@ -0,0 +1,47 @@ | |||
671 | 1 | #!/usr/bin/env python | ||
672 | 2 | # Ubuntu CI Services | ||
673 | 3 | # Copyright 2013 Canonical Ltd. | ||
674 | 4 | |||
675 | 5 | # This program is free software: you can redistribute it and/or modify it | ||
676 | 6 | # under the terms of the GNU Affero General Public License version 3, as | ||
677 | 7 | # published by the Free Software Foundation. | ||
678 | 8 | |||
679 | 9 | # This program is distributed in the hope that it will be useful, but | ||
680 | 10 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
681 | 11 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
682 | 12 | # PURPOSE. See the GNU Affero General Public License for more details. | ||
683 | 13 | |||
684 | 14 | # You should have received a copy of the GNU Affero General Public License | ||
685 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
686 | 16 | |||
687 | 17 | import json | ||
688 | 18 | import logging | ||
689 | 19 | import os | ||
690 | 20 | import sys | ||
691 | 21 | |||
692 | 22 | |||
693 | 23 | logging.basicConfig(level=logging.INFO) | ||
694 | 24 | log = logging.getLogger(__name__) | ||
695 | 25 | |||
696 | 26 | # the worker might not have installed this module, so determine the path | ||
697 | 27 | # and add it, so we can always safely import stuff | ||
698 | 28 | sys.path.append(os.path.join(os.path.dirname(__file__), 'bsbuilder')) | ||
699 | 29 | import amqp_utils | ||
700 | 30 | |||
701 | 31 | |||
702 | 32 | def on_message(msg): | ||
703 | 33 | params = json.loads(msg.body) | ||
704 | 34 | sources = params['source_packages'] | ||
705 | 35 | ppa = params['ppa'] | ||
706 | 36 | trigger = params['progress_trigger'] | ||
707 | 37 | print('TODO handler message: sources(%s) ppa(%s) trigger(%s)' % ( | ||
708 | 38 | sources, ppa, trigger)) | ||
709 | 39 | # remove from queue so request becomes completed | ||
710 | 40 | msg.channel.basic_ack(msg.delivery_tag) | ||
711 | 41 | |||
712 | 42 | |||
713 | 43 | if __name__ == '__main__': | ||
714 | 44 | config = amqp_utils.get_config() | ||
715 | 45 | if not config: | ||
716 | 46 | exit(1) # the get_config code prints an error | ||
717 | 47 | amqp_utils.process_queue(config, 'bsbuilder', on_message) | ||
718 | 0 | 48 | ||
719 | === added file 'branch-source-builder/setup.py' | |||
720 | --- branch-source-builder/setup.py 1970-01-01 00:00:00 +0000 | |||
721 | +++ branch-source-builder/setup.py 2013-12-11 16:52:29 +0000 | |||
722 | @@ -0,0 +1,44 @@ | |||
723 | 1 | #!/usr/bin/env python | ||
724 | 2 | # Ubuntu CI Services | ||
725 | 3 | # Copyright 2013 Canonical Ltd. | ||
726 | 4 | |||
727 | 5 | # This program is free software: you can redistribute it and/or modify it | ||
728 | 6 | # under the terms of the GNU Affero General Public License version 3, as | ||
729 | 7 | # published by the Free Software Foundation. | ||
730 | 8 | |||
731 | 9 | # This program is distributed in the hope that it will be useful, but | ||
732 | 10 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
733 | 11 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
734 | 12 | # PURPOSE. See the GNU Affero General Public License for more details. | ||
735 | 13 | |||
736 | 14 | # You should have received a copy of the GNU Affero General Public License | ||
737 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
738 | 16 | |||
739 | 17 | import os | ||
740 | 18 | |||
741 | 19 | from setuptools import find_packages, setup | ||
742 | 20 | |||
743 | 21 | # ensure find_packages works if our current directory isn't this project | ||
744 | 22 | basedir = os.path.abspath(os.path.dirname(__file__)) | ||
745 | 23 | os.chdir(basedir) | ||
746 | 24 | packages = find_packages(basedir) | ||
747 | 25 | |||
748 | 26 | import bsbuilder | ||
749 | 27 | |||
750 | 28 | requires = [ | ||
751 | 29 | 'restish==0.12.1', | ||
752 | 30 | 'amqplib==1.0.0', | ||
753 | 31 | 'mock==1.0.1', | ||
754 | 32 | 'WebTest==2.0.10', | ||
755 | 33 | ] | ||
756 | 34 | |||
757 | 35 | setup( | ||
758 | 36 | name='branch-source-builder', | ||
759 | 37 | version=bsbuilder.__version__, | ||
760 | 38 | description='Branch/Source Builder component of Ubuntu CI Services', | ||
761 | 39 | author='Canonical CI Engineering Team', | ||
762 | 40 | license='AGPL', | ||
763 | 41 | packages=packages, | ||
764 | 42 | test_suite='tests', | ||
765 | 43 | install_requires=requires, | ||
766 | 44 | ) | ||
767 | 0 | 45 | ||
768 | === added file 'juju-deployer/branch-source-builder.yaml' | |||
769 | --- juju-deployer/branch-source-builder.yaml 1970-01-01 00:00:00 +0000 | |||
770 | +++ juju-deployer/branch-source-builder.yaml 2013-12-11 16:52:29 +0000 | |||
771 | @@ -0,0 +1,29 @@ | |||
772 | 1 | branch-source-builder-staging: | ||
773 | 2 | series: precise | ||
774 | 3 | services: | ||
775 | 4 | bsb-restish: | ||
776 | 5 | charm: restish | ||
777 | 6 | branch: lp:~canonical-ci-engineering/charms/precise/ubuntu-ci-services-itself/restish | ||
778 | 7 | options: | ||
779 | 8 | branch: lp:ubuntu-ci-services-itself | ||
780 | 9 | python_path: ./branch-source-builder | ||
781 | 10 | # need non-default package python-amqplib for this service | ||
782 | 11 | packages: "python-webtest python-mock python-jinja2 python-amqplib" | ||
783 | 12 | bsb-gunicorn: | ||
784 | 13 | charm: gunicorn | ||
785 | 14 | branch: lp:charms/precise/gunicorn | ||
786 | 15 | options: | ||
787 | 16 | wsgi_wsgi_file: bsbuilder.wsgi:app | ||
788 | 17 | bsb-worker: | ||
789 | 18 | charm: rabbitmq-worker | ||
790 | 19 | branch: lp:~canonical-ci-engineering/charms/precise/ubuntu-ci-services-itself/rabbitmq-worker | ||
791 | 20 | options: | ||
792 | 21 | branch: lp:ubuntu-ci-services-itself | ||
793 | 22 | main: ./branch-source-builder/run_worker | ||
794 | 23 | bsb-rabbit: | ||
795 | 24 | branch: lp:~canonical-ci-engineering/charms/precise/ubuntu-ci-services-itself/rabbitmq-server | ||
796 | 25 | charm: rabbitmq | ||
797 | 26 | relations: | ||
798 | 27 | - [bsb-restish, bsb-gunicorn] | ||
799 | 28 | - [bsb-worker, bsb-rabbit] | ||
800 | 29 | - [bsb-rabbit, bsb-restish] |
There are some caveats to getting the juju-deployer portion of this working:
1) the rabbitmq-server charm is broke. It seems to be the .deb itself, but you have to edit /etc/hosts and add the host name to the 127.0.0.1 entry so the install will work. If you are fast and do that before juju-deployer gets to the bsb-rabbit unit the deployer will work. Otherwise you have to make that change, run "juju resolved --retry bsb-rabbit/0". then re-run the deployer script.
2) the bsb-worker script doesn't work out the first time. For some reason even with upstart respawing it, it won't start until you run: sudo stop bsb_worker; sudo start bsb_worker
At this point the service should be running and you can test with:
curl --dump-header - -H "Content-Type: application/json" -X POST --data '{"source_ packages" : "todo_source", "ppa": "todo_ppa", "progress_trigger": "todo_trigger"}' http://<BSB RESTISH IP>:8080/ api/v1/ build_source
then on the bsb_worker node, you'll see a TODO printed in /var/log/ upstart/ bsb_worker. log