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