Merge lp:~psivaa/core-image-watcher/image-watcher-udf into lp:core-image-watcher
- image-watcher-udf
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Thomi Richards |
Approved revision: | 16 |
Merged at revision: | 5 |
Proposed branch: | lp:~psivaa/core-image-watcher/image-watcher-udf |
Merge into: | lp:core-image-watcher |
Diff against target: |
443 lines (+377/-5) 9 files modified
.bzrignore (+1/-0) README.rst (+64/-0) core-image-watcher.py (+18/-0) core-service.conf (+10/-0) core_image_watcher/__init__.py (+171/-5) core_image_watcher/tests/test_image_watcher.py (+110/-0) requirements.txt (+1/-0) setup.py (+1/-0) test_requirements.txt (+1/-0) |
To merge this branch: | bzr merge lp:~psivaa/core-image-watcher/image-watcher-udf |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Celso Providelo (community) | Approve | ||
Thomi Richards (community) | Approve | ||
Joe Talbott (community) | Needs Information | ||
Review via email: mp+254122@code.launchpad.net |
Commit message
Image watcher component to publish messages to a rabbit queue when there is a new image present in the core image server.
Description of the change
Image watcher component to publish messages to a rabbit queue when there is a new image present in the core image server.
The message does not include the test branch details since it could be picked up at a later stage in the workflow. But if that has also to be taken from the same config as its used here, we could include here.
Thomi Richards (thomir-deactivatedaccount) wrote : | # |
A few things need fixing - please don't hesitate to ping me if anything doesn't make sense here.
Overall this is looking really nice - thank you!
Celso Providelo (cprov) wrote : | # |
Psivaa,
Quick adjustment before review, the configuration file on the charms is called "core-service.
Para Siva (psivaa) wrote : | # |
Thanks Thomi, Cprov and Joe for the comments. I've addressed as much as possible. Would you be able to take another look please?
Thanks
Thomi Richards (thomir-deactivatedaccount) wrote : | # |
Hi Siva,
I made a new review, I'm afraid there are still several things to change. Let me know if you have any questions.
Cheers,
Para Siva (psivaa) wrote : | # |
Thanks again Thomi for the review. I need to mention that I really enjoyed fixing these comments, especially when you relate them to the fundamental principles.
I have addressed all your comments. Would be great for another look. Thanks
Thomi Richards (thomir-deactivatedaccount) wrote : | # |
Looks great - thanks!
Celso Providelo (cprov) wrote : | # |
Psivaa,
There is only one remaining point about the kombu connection context, it should have a more restricted and coherent lifetime (inside enqueue())
It might impact badly in tests, lets see how it goes.
Para Siva (psivaa) wrote : | # |
Cprove, Thomi,
Thanks a lot again for the reviews and help. Moved kombu connection bit inside enqueue and got rid of the CoreImageWatcher class.
Added a couple of unittests for _check_
Celso Providelo (cprov) wrote : | # |
Nice one! I liked the procedural version. Looking forward to see it in production.
Preview Diff
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2015-03-25 03:47:26 +0000 |
3 | +++ .bzrignore 2015-03-26 20:44:41 +0000 |
4 | @@ -1,1 +1,2 @@ |
5 | core_image_watcher.egg-info |
6 | +ve |
7 | |
8 | === modified file 'README.rst' |
9 | --- README.rst 2015-03-24 22:58:34 +0000 |
10 | +++ README.rst 2015-03-26 20:44:41 +0000 |
11 | @@ -2,3 +2,67 @@ |
12 | ################## |
13 | |
14 | A micro-service that watches for new Ubuntu Core images. |
15 | + |
16 | +Get the Source |
17 | +============== |
18 | + |
19 | +Branch the code:: |
20 | + |
21 | + $ bzr branch lp:core-image-watcher |
22 | + |
23 | +Install the Service |
24 | +======================== |
25 | + |
26 | +Install system dependencies:: |
27 | + |
28 | + $ sudo apt-get install python3-dev |
29 | + |
30 | +Build and activate a virtualenv with python3:: |
31 | + |
32 | + $ virtualenv -p python3 --system-site-packages ve |
33 | + $ . ve/bin/activate |
34 | + |
35 | +Install dependencies from pypi:: |
36 | + |
37 | + $ pip install -r requirements.txt |
38 | + |
39 | +...and install some dependencies from phablet-tools PPA:: |
40 | + |
41 | + $ sudo add-apt-repository ppa:phablet-team/tools |
42 | + $ sudo apt-get update |
43 | + $ sudo apt-get install ubuntu-device-flash |
44 | + |
45 | +Install the service itself:: |
46 | + |
47 | + $ python setup.py install |
48 | + |
49 | +...you may want to install it in 'development mode', which symlinks files, |
50 | +so you can edit/re-run without having to re-install the service. In that |
51 | +case, run:: |
52 | + |
53 | + $ python setup.py develop |
54 | + |
55 | +Run the tests! |
56 | +============== |
57 | + |
58 | +Install dependencies:: |
59 | + |
60 | + $ pip install -r test_requirements.txt |
61 | + |
62 | +Run those tests - with vigour!:: |
63 | + |
64 | + $ python setup.py test |
65 | + |
66 | +The config file |
67 | +=============== |
68 | + |
69 | +The sample configuration file in 'core-service.conf':: |
70 | + |
71 | + [amqp] |
72 | + uris = amqp://guest:guest@localhost:5672// |
73 | + |
74 | + [image] |
75 | + channel = devel-proposed |
76 | + device = generic_amd64 |
77 | + location = /tmp/latest-core-image-version |
78 | + poll_period = 60 |
79 | |
80 | === modified file 'core-image-watcher.py' |
81 | --- core-image-watcher.py 2015-03-25 02:36:54 +0000 |
82 | +++ core-image-watcher.py 2015-03-26 20:44:41 +0000 |
83 | @@ -1,3 +1,21 @@ |
84 | +#!/usr/bin/env python3 |
85 | + |
86 | +# core-image-watcher |
87 | +# Copyright (C) 2015 Canonical |
88 | +# |
89 | +# This program is free software: you can redistribute it and/or modify |
90 | +# it under the terms of the GNU General Public License as published by |
91 | +# the Free Software Foundation, either version 3 of the License, or |
92 | +# (at your option) any later version. |
93 | +# |
94 | +# This program is distributed in the hope that it will be useful, |
95 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
96 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
97 | +# GNU General Public License for more details. |
98 | +# |
99 | +# You should have received a copy of the GNU General Public License |
100 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
101 | +# |
102 | |
103 | from core_image_watcher import main |
104 | |
105 | |
106 | === added file 'core-service.conf' |
107 | --- core-service.conf 1970-01-01 00:00:00 +0000 |
108 | +++ core-service.conf 2015-03-26 20:44:41 +0000 |
109 | @@ -0,0 +1,10 @@ |
110 | +# `core-image-watcher` configuration file. |
111 | + |
112 | +[amqp] |
113 | +uris = amqp://guest:guest@localhost:5672// |
114 | + |
115 | +[image] |
116 | +channel = devel-proposed |
117 | +device = generic_amd64 |
118 | +location = /tmp/latest-core-image-version |
119 | +poll_period = 60 |
120 | |
121 | === modified file 'core_image_watcher/__init__.py' |
122 | --- core_image_watcher/__init__.py 2015-03-25 02:16:17 +0000 |
123 | +++ core_image_watcher/__init__.py 2015-03-26 20:44:41 +0000 |
124 | @@ -1,8 +1,174 @@ |
125 | - |
126 | - |
127 | -import select |
128 | +# core-image-watcher |
129 | +# Copyright (C) 2015 Canonical |
130 | +# |
131 | +# This program is free software: you can redistribute it and/or modify |
132 | +# it under the terms of the GNU General Public License as published by |
133 | +# the Free Software Foundation, either version 3 of the License, or |
134 | +# (at your option) any later version. |
135 | +# |
136 | +# This program is distributed in the hope that it will be useful, |
137 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
138 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
139 | +# GNU General Public License for more details. |
140 | +# |
141 | +# You should have received a copy of the GNU General Public License |
142 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
143 | +# |
144 | + |
145 | +"""Core Image Watcher functional code.""" |
146 | + |
147 | +import argparse |
148 | +import configparser |
149 | +import logging |
150 | +import os |
151 | +import subprocess |
152 | +import time |
153 | + |
154 | +import kombu |
155 | + |
156 | +logger = logging.getLogger(__name__) |
157 | + |
158 | +API_VERSION = "v1" |
159 | + |
160 | + |
161 | +def _enqueue(body, amqp_uris): |
162 | + """Enqueues a message of `body` to the rabbit queue""" |
163 | + with kombu.Connection(amqp_uris) as connection: |
164 | + logger.info('Triggering request: %s', body, extra=body) |
165 | + try: |
166 | + queue = connection.SimpleQueue( |
167 | + 'core.image.{}'.format(API_VERSION)) |
168 | + queue.put(body) |
169 | + queue.close() |
170 | + except Exception as exc: |
171 | + logger.error(exc, exc_info=True) |
172 | + logger.info('Done!', extra=body) |
173 | + |
174 | + |
175 | +def _get_version_string_output(channel, device): |
176 | + """Obtains a bytestring of the images info from the core image server""" |
177 | + cmd = ['ubuntu-device-flash', |
178 | + 'query ', |
179 | + '--list-images', |
180 | + '--channel', |
181 | + 'ubuntu-core/{}'.format(channel), |
182 | + '--device={}'.format(device)] |
183 | + images = None |
184 | + try: |
185 | + images = subprocess.check_output(cmd) |
186 | + except subprocess.CalledProcessError as e: |
187 | + logger.error(e, exc_info=True) |
188 | + finally: |
189 | + return images |
190 | + |
191 | + |
192 | +def _get_latest_image_version(channel, |
193 | + device, |
194 | + get_output=_get_version_string_output): |
195 | + """Returns largest image version""" |
196 | + images = get_output(channel, device).split() |
197 | + if (len(images) >= 2): |
198 | + return images[-2].decode('utf-8').replace(':', '') |
199 | + else: |
200 | + logger.info('Could not get the image list by u-d-f') |
201 | + return None |
202 | + |
203 | + |
204 | +def _cache_version_to_disk(location, latest_version): |
205 | + """Caches the version of the image if it's larger than a cached one""" |
206 | + try: |
207 | + if os.path.exists(location): |
208 | + with open(location, 'r') as cache: |
209 | + if (int(cache.read()) < int(latest_version)): |
210 | + with open(location, 'w') as f: |
211 | + f.write(latest_version) |
212 | + else: |
213 | + latest_version = None |
214 | + else: |
215 | + with open(location, 'w') as f: |
216 | + f.write(latest_version) |
217 | + except IOError as e: |
218 | + latest_version = None |
219 | + logger.error('Writing the latest image info failed: %s', (str(e))) |
220 | + finally: |
221 | + return latest_version |
222 | + |
223 | + |
224 | +def _check_for_new_image(location, |
225 | + channel, |
226 | + device, |
227 | + latest_image_version=_get_latest_image_version, |
228 | + cached_version=_cache_version_to_disk): |
229 | + """Check if a new image is present in the core image server""" |
230 | + latest_version = latest_image_version(channel, device) |
231 | + try: |
232 | + ret = cached_version(location, latest_version) |
233 | + if not ret: |
234 | + # There is no new image |
235 | + # Do not progress |
236 | + return None |
237 | + except Exception as e: |
238 | + logger.error(e, exc_info=True) |
239 | + return None |
240 | + body = {} |
241 | + body['image_name'] = latest_version |
242 | + body['channel'] = channel |
243 | + body['device'] = device |
244 | + return body |
245 | + |
246 | + |
247 | +def configure_logging(config): |
248 | + root_logger = logging.getLogger() |
249 | + root_logger.setLevel(logging.INFO) |
250 | + |
251 | + requests_logger = logging.getLogger('requests') |
252 | + requests_logger.setLevel(logging.WARNING) |
253 | + |
254 | + # If there is no ./logs directory, fallback to stderr. |
255 | + log_path = os.path.abspath( |
256 | + os.path.join(__file__, '../../logs/core-image-watcher.log')) |
257 | + log_dir = os.path.dirname(log_path) |
258 | + if os.path.exists(log_dir): |
259 | + handler = logging.FileHandler(log_path) |
260 | + else: |
261 | + print("'logs' directory '{}' does not exist, using stderr " |
262 | + "for app log.".format(log_dir)) |
263 | + handler = logging.StreamHandler() |
264 | + |
265 | + handler.setFormatter( |
266 | + logging.Formatter( |
267 | + '%(asctime)s %(name)s %(levelname)s: %(message)s' |
268 | + ) |
269 | + ) |
270 | + root_logger.addHandler(handler) |
271 | |
272 | |
273 | def main(): |
274 | - # for now, do nothing at all. |
275 | - select.select([], [], []) |
276 | + parser = argparse.ArgumentParser( |
277 | + description='Core image watcher ...') |
278 | + parser.add_argument('-c', '--conf', default='core-service.conf', |
279 | + help='Configuration file path') |
280 | + args = parser.parse_args() |
281 | + |
282 | + # Load configuration options. |
283 | + config = configparser.ConfigParser() |
284 | + config.read(args.conf) |
285 | + |
286 | + configure_logging(config) |
287 | + |
288 | + amqp_uris = config.get('amqp', 'uris').split() |
289 | + location = config.get('image', 'location') |
290 | + channel = config.get('image', 'channel') |
291 | + device = config.get('image', 'device') |
292 | + poll_period = float(config.get('image', 'poll_period')) |
293 | + |
294 | + try: |
295 | + while True: |
296 | + message_body = _check_for_new_image(location, |
297 | + channel, |
298 | + device) |
299 | + if message_body: |
300 | + _enqueue(message_body, amqp_uris) |
301 | + time.sleep(poll_period) |
302 | + except KeyboardInterrupt: |
303 | + print('Bye!') |
304 | |
305 | === added directory 'core_image_watcher/tests' |
306 | === added file 'core_image_watcher/tests/__init__.py' |
307 | === added file 'core_image_watcher/tests/test_image_watcher.py' |
308 | --- core_image_watcher/tests/test_image_watcher.py 1970-01-01 00:00:00 +0000 |
309 | +++ core_image_watcher/tests/test_image_watcher.py 2015-03-26 20:44:41 +0000 |
310 | @@ -0,0 +1,110 @@ |
311 | +# Ubuntu CI Engine |
312 | +# Copyright 2015 Canonical Ltd. |
313 | + |
314 | +# This program is free software: you can redistribute it and/or modify it |
315 | +# under the terms of the GNU Affero General Public License version 3, as |
316 | +# published by the Free Software Foundation. |
317 | + |
318 | +# This program is distributed in the hope that it will be useful, but |
319 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
320 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
321 | +# PURPOSE. See the GNU Affero General Public License for more details. |
322 | + |
323 | +# You should have received a copy of the GNU Affero General Public License |
324 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
325 | + |
326 | +import os |
327 | +import tempfile |
328 | +import testtools |
329 | + |
330 | +from core_image_watcher import ( |
331 | + _cache_version_to_disk, |
332 | + _get_latest_image_version, |
333 | + _check_for_new_image, |
334 | +) |
335 | + |
336 | + |
337 | +class TestWatchImage(testtools.TestCase): |
338 | + |
339 | + def write_to_tempfile(self, version): |
340 | + temp_file = tempfile.NamedTemporaryFile('w+', delete=False) |
341 | + with temp_file as f: |
342 | + f.write(version) |
343 | + self.addCleanup(os.remove, temp_file.name) |
344 | + return temp_file.name |
345 | + |
346 | + def test_cache_version_to_disk(self): |
347 | + temp_file_name = self.write_to_tempfile('111') |
348 | + return_value = _cache_version_to_disk(temp_file_name, '112') |
349 | + self.assertEqual(return_value, '112') |
350 | + |
351 | + def test_cache_version_with_different_digits_to_disk(self): |
352 | + temp_file_name = self.write_to_tempfile('23') |
353 | + return_value = _cache_version_to_disk(temp_file_name, '112') |
354 | + self.assertEqual(return_value, '112') |
355 | + |
356 | + def test_do_not_cache_the_same_version_to_disk(self): |
357 | + temp_file_name = self.write_to_tempfile('112') |
358 | + return_value = _cache_version_to_disk(temp_file_name, '112') |
359 | + self.assertEqual(return_value, None) |
360 | + |
361 | + def test_do_not_proceed(self): |
362 | + temp_file_name = self.write_to_tempfile('113') |
363 | + return_value = _cache_version_to_disk(temp_file_name, '112') |
364 | + self.assertEqual(return_value, None) |
365 | + |
366 | + def test_freshly_cache_version_to_disk(self): |
367 | + temp_file = tempfile.NamedTemporaryFile('w+', delete=False) |
368 | + return_value = _cache_version_to_disk(temp_file.name, '112') |
369 | + self.addCleanup(os.remove, temp_file.name) |
370 | + self.assertEqual(return_value, '112') |
371 | + |
372 | + def test_get_latest_version(self): |
373 | + def get_output(channel, device): |
374 | + return b"295: description='fake295'\n\ |
375 | + 296: description='fake296'\n\ |
376 | + 297: description='fake297'\n\ |
377 | + 298: description='fake338'\n" |
378 | + observed = _get_latest_image_version('fakechannel', |
379 | + 'fakedevice', |
380 | + get_output) |
381 | + self.assertEqual(observed, '298') |
382 | + |
383 | + def test_dont_get_latest_version(self): |
384 | + def get_output(channel, device): |
385 | + return b"100: description='fake100'\n" |
386 | + observed = _get_latest_image_version('fakechannel', |
387 | + 'fakedevice', |
388 | + get_output) |
389 | + self.assertEqual(observed, '100') |
390 | + |
391 | + def test_check_for_new_image(self): |
392 | + def _get_latest_image_version(channel, device): |
393 | + return '100' |
394 | + |
395 | + def _cache_version_to_disk(location, latest_version): |
396 | + return '99' |
397 | + |
398 | + body = _check_for_new_image('fakelocation', |
399 | + 'fakechannel', |
400 | + 'fakedevice', |
401 | + _get_latest_image_version, |
402 | + _cache_version_to_disk) |
403 | + self.assertEqual(body, |
404 | + {'device': 'fakedevice', |
405 | + 'image_name': '100', |
406 | + 'channel': 'fakechannel'}) |
407 | + |
408 | + def test_check_for_no_new_image(self): |
409 | + def _get_latest_image_version(channel, device): |
410 | + return '100' |
411 | + |
412 | + def _cache_version_to_disk(location, latest_version): |
413 | + return None |
414 | + |
415 | + body = _check_for_new_image('fakelocation', |
416 | + 'fakechannel', |
417 | + 'fakedevice', |
418 | + _get_latest_image_version, |
419 | + _cache_version_to_disk) |
420 | + self.assertEqual(body, None) |
421 | |
422 | === modified file 'requirements.txt' |
423 | --- requirements.txt 2015-03-25 01:34:08 +0000 |
424 | +++ requirements.txt 2015-03-26 20:44:41 +0000 |
425 | @@ -0,0 +1,1 @@ |
426 | +kombu==3.0.24 |
427 | |
428 | === modified file 'setup.py' |
429 | --- setup.py 2015-03-25 03:47:26 +0000 |
430 | +++ setup.py 2015-03-26 20:44:41 +0000 |
431 | @@ -36,5 +36,6 @@ |
432 | url='https://launchpad.net/core-image-watcher', |
433 | license='GPLv3', |
434 | packages=find_packages(), |
435 | + test_suite='core_image_watcher.tests', |
436 | scripts=['core-image-watcher.py'] |
437 | ) |
438 | |
439 | === modified file 'test_requirements.txt' |
440 | --- test_requirements.txt 2015-03-25 01:34:08 +0000 |
441 | +++ test_requirements.txt 2015-03-26 20:44:41 +0000 |
442 | @@ -0,0 +1,1 @@ |
443 | +testtools==1.7.1 |
A few in-line questions.