Merge lp:~doanac/ubuntu-ci-services-itself/bsbuilder into lp:ubuntu-ci-services-itself

Proposed by Andy Doan
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
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-source-builder/run_worker that will wait for messages and print a TODO.

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-source-builder/run_worker that will wait for messages and print a TODO.

The MP itself is bigger than the service because of the groundwork I did for the restish service.

To post a comment you must log in.
Revision history for this message
Andy Doan (doanac) wrote :

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

23. By Andy Doan

work around a canonistack bug with seen with rabbitmq-server

Revision history for this message
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.

Revision history for this message
Andy Doan (doanac) wrote :

as of revno 24, things "just work".

Revision history for this message
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-source-builder/bsbuilder/tests/test_utils.py.
 - branch-source-builder/setup.py is not executable
 - 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.

25. By Andy Doan

address comments from fginther

make setup.py executable
use argparse for local wsgi server

Revision history for this message
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.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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]

Subscribers

People subscribed via source and target branches