Merge lp:~psivaa/core-image-watcher/image-watcher-udf into lp:core-image-watcher

Proposed by Para Siva
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
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.

To post a comment you must log in.
Revision history for this message
Joe Talbott (joetalbott) wrote :

A few in-line questions.

review: Needs Information
Revision history for this message
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!

review: Needs Fixing
Revision history for this message
Celso Providelo (cprov) wrote :

Psivaa,

Quick adjustment before review, the configuration file on the charms is called "core-service.conf".

review: Needs Fixing
9. By Para Siva

Review comment fixes

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

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

review: Needs Fixing
10. By Para Siva

Fixes for thomi's phase 2 review comments

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

11. By Para Siva

Sleep after finding the latest version

12. By Para Siva

testtools for test requirements

13. By Para Siva

Use uniform naming test variable

14. By Para Siva

Pin testtool version to 1.7.1

Revision history for this message
Thomi Richards (thomir-deactivatedaccount) wrote :

Looks great - thanks!

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

review: Needs Fixing
15. By Para Siva

Create connection to rabbit only when there is a new image

16. By Para Siva

Add unittest to check_for_new_image

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

Revision history for this message
Celso Providelo (cprov) wrote :

Nice one! I liked the procedural version. Looking forward to see it in production.

review: Approve

Preview Diff

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

Subscribers

People subscribed via source and target branches