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
=== modified file '.bzrignore'
--- .bzrignore 2015-03-25 03:47:26 +0000
+++ .bzrignore 2015-03-26 20:44:41 +0000
@@ -1,1 +1,2 @@
1core_image_watcher.egg-info1core_image_watcher.egg-info
2ve
23
=== modified file 'README.rst'
--- README.rst 2015-03-24 22:58:34 +0000
+++ README.rst 2015-03-26 20:44:41 +0000
@@ -2,3 +2,67 @@
2##################2##################
33
4A micro-service that watches for new Ubuntu Core images.4A micro-service that watches for new Ubuntu Core images.
5
6Get the Source
7==============
8
9Branch the code::
10
11 $ bzr branch lp:core-image-watcher
12
13Install the Service
14========================
15
16Install system dependencies::
17
18 $ sudo apt-get install python3-dev
19
20Build and activate a virtualenv with python3::
21
22 $ virtualenv -p python3 --system-site-packages ve
23 $ . ve/bin/activate
24
25Install dependencies from pypi::
26
27 $ pip install -r requirements.txt
28
29...and install some dependencies from phablet-tools PPA::
30
31 $ sudo add-apt-repository ppa:phablet-team/tools
32 $ sudo apt-get update
33 $ sudo apt-get install ubuntu-device-flash
34
35Install the service itself::
36
37 $ python setup.py install
38
39...you may want to install it in 'development mode', which symlinks files,
40so you can edit/re-run without having to re-install the service. In that
41case, run::
42
43 $ python setup.py develop
44
45Run the tests!
46==============
47
48Install dependencies::
49
50 $ pip install -r test_requirements.txt
51
52Run those tests - with vigour!::
53
54 $ python setup.py test
55
56The config file
57===============
58
59The sample configuration file in 'core-service.conf'::
60
61 [amqp]
62 uris = amqp://guest:guest@localhost:5672//
63
64 [image]
65 channel = devel-proposed
66 device = generic_amd64
67 location = /tmp/latest-core-image-version
68 poll_period = 60
569
=== modified file 'core-image-watcher.py'
--- core-image-watcher.py 2015-03-25 02:36:54 +0000
+++ core-image-watcher.py 2015-03-26 20:44:41 +0000
@@ -1,3 +1,21 @@
1#!/usr/bin/env python3
2
3# core-image-watcher
4# Copyright (C) 2015 Canonical
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18#
119
2from core_image_watcher import main20from core_image_watcher import main
321
422
=== added file 'core-service.conf'
--- core-service.conf 1970-01-01 00:00:00 +0000
+++ core-service.conf 2015-03-26 20:44:41 +0000
@@ -0,0 +1,10 @@
1# `core-image-watcher` configuration file.
2
3[amqp]
4uris = amqp://guest:guest@localhost:5672//
5
6[image]
7channel = devel-proposed
8device = generic_amd64
9location = /tmp/latest-core-image-version
10poll_period = 60
011
=== modified file 'core_image_watcher/__init__.py'
--- core_image_watcher/__init__.py 2015-03-25 02:16:17 +0000
+++ core_image_watcher/__init__.py 2015-03-26 20:44:41 +0000
@@ -1,8 +1,174 @@
11# core-image-watcher
22# Copyright (C) 2015 Canonical
3import select3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16#
17
18"""Core Image Watcher functional code."""
19
20import argparse
21import configparser
22import logging
23import os
24import subprocess
25import time
26
27import kombu
28
29logger = logging.getLogger(__name__)
30
31API_VERSION = "v1"
32
33
34def _enqueue(body, amqp_uris):
35 """Enqueues a message of `body` to the rabbit queue"""
36 with kombu.Connection(amqp_uris) as connection:
37 logger.info('Triggering request: %s', body, extra=body)
38 try:
39 queue = connection.SimpleQueue(
40 'core.image.{}'.format(API_VERSION))
41 queue.put(body)
42 queue.close()
43 except Exception as exc:
44 logger.error(exc, exc_info=True)
45 logger.info('Done!', extra=body)
46
47
48def _get_version_string_output(channel, device):
49 """Obtains a bytestring of the images info from the core image server"""
50 cmd = ['ubuntu-device-flash',
51 'query ',
52 '--list-images',
53 '--channel',
54 'ubuntu-core/{}'.format(channel),
55 '--device={}'.format(device)]
56 images = None
57 try:
58 images = subprocess.check_output(cmd)
59 except subprocess.CalledProcessError as e:
60 logger.error(e, exc_info=True)
61 finally:
62 return images
63
64
65def _get_latest_image_version(channel,
66 device,
67 get_output=_get_version_string_output):
68 """Returns largest image version"""
69 images = get_output(channel, device).split()
70 if (len(images) >= 2):
71 return images[-2].decode('utf-8').replace(':', '')
72 else:
73 logger.info('Could not get the image list by u-d-f')
74 return None
75
76
77def _cache_version_to_disk(location, latest_version):
78 """Caches the version of the image if it's larger than a cached one"""
79 try:
80 if os.path.exists(location):
81 with open(location, 'r') as cache:
82 if (int(cache.read()) < int(latest_version)):
83 with open(location, 'w') as f:
84 f.write(latest_version)
85 else:
86 latest_version = None
87 else:
88 with open(location, 'w') as f:
89 f.write(latest_version)
90 except IOError as e:
91 latest_version = None
92 logger.error('Writing the latest image info failed: %s', (str(e)))
93 finally:
94 return latest_version
95
96
97def _check_for_new_image(location,
98 channel,
99 device,
100 latest_image_version=_get_latest_image_version,
101 cached_version=_cache_version_to_disk):
102 """Check if a new image is present in the core image server"""
103 latest_version = latest_image_version(channel, device)
104 try:
105 ret = cached_version(location, latest_version)
106 if not ret:
107 # There is no new image
108 # Do not progress
109 return None
110 except Exception as e:
111 logger.error(e, exc_info=True)
112 return None
113 body = {}
114 body['image_name'] = latest_version
115 body['channel'] = channel
116 body['device'] = device
117 return body
118
119
120def configure_logging(config):
121 root_logger = logging.getLogger()
122 root_logger.setLevel(logging.INFO)
123
124 requests_logger = logging.getLogger('requests')
125 requests_logger.setLevel(logging.WARNING)
126
127 # If there is no ./logs directory, fallback to stderr.
128 log_path = os.path.abspath(
129 os.path.join(__file__, '../../logs/core-image-watcher.log'))
130 log_dir = os.path.dirname(log_path)
131 if os.path.exists(log_dir):
132 handler = logging.FileHandler(log_path)
133 else:
134 print("'logs' directory '{}' does not exist, using stderr "
135 "for app log.".format(log_dir))
136 handler = logging.StreamHandler()
137
138 handler.setFormatter(
139 logging.Formatter(
140 '%(asctime)s %(name)s %(levelname)s: %(message)s'
141 )
142 )
143 root_logger.addHandler(handler)
4144
5145
6def main():146def main():
7 # for now, do nothing at all.147 parser = argparse.ArgumentParser(
8 select.select([], [], [])148 description='Core image watcher ...')
149 parser.add_argument('-c', '--conf', default='core-service.conf',
150 help='Configuration file path')
151 args = parser.parse_args()
152
153 # Load configuration options.
154 config = configparser.ConfigParser()
155 config.read(args.conf)
156
157 configure_logging(config)
158
159 amqp_uris = config.get('amqp', 'uris').split()
160 location = config.get('image', 'location')
161 channel = config.get('image', 'channel')
162 device = config.get('image', 'device')
163 poll_period = float(config.get('image', 'poll_period'))
164
165 try:
166 while True:
167 message_body = _check_for_new_image(location,
168 channel,
169 device)
170 if message_body:
171 _enqueue(message_body, amqp_uris)
172 time.sleep(poll_period)
173 except KeyboardInterrupt:
174 print('Bye!')
9175
=== added directory 'core_image_watcher/tests'
=== added file 'core_image_watcher/tests/__init__.py'
=== added file 'core_image_watcher/tests/test_image_watcher.py'
--- core_image_watcher/tests/test_image_watcher.py 1970-01-01 00:00:00 +0000
+++ core_image_watcher/tests/test_image_watcher.py 2015-03-26 20:44:41 +0000
@@ -0,0 +1,110 @@
1# Ubuntu CI Engine
2# Copyright 2015 Canonical Ltd.
3
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU Affero General Public License version 3, as
6# published by the Free Software Foundation.
7
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranties of
10# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11# PURPOSE. See the GNU Affero General Public License for more details.
12
13# You should have received a copy of the GNU Affero General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16import os
17import tempfile
18import testtools
19
20from core_image_watcher import (
21 _cache_version_to_disk,
22 _get_latest_image_version,
23 _check_for_new_image,
24)
25
26
27class TestWatchImage(testtools.TestCase):
28
29 def write_to_tempfile(self, version):
30 temp_file = tempfile.NamedTemporaryFile('w+', delete=False)
31 with temp_file as f:
32 f.write(version)
33 self.addCleanup(os.remove, temp_file.name)
34 return temp_file.name
35
36 def test_cache_version_to_disk(self):
37 temp_file_name = self.write_to_tempfile('111')
38 return_value = _cache_version_to_disk(temp_file_name, '112')
39 self.assertEqual(return_value, '112')
40
41 def test_cache_version_with_different_digits_to_disk(self):
42 temp_file_name = self.write_to_tempfile('23')
43 return_value = _cache_version_to_disk(temp_file_name, '112')
44 self.assertEqual(return_value, '112')
45
46 def test_do_not_cache_the_same_version_to_disk(self):
47 temp_file_name = self.write_to_tempfile('112')
48 return_value = _cache_version_to_disk(temp_file_name, '112')
49 self.assertEqual(return_value, None)
50
51 def test_do_not_proceed(self):
52 temp_file_name = self.write_to_tempfile('113')
53 return_value = _cache_version_to_disk(temp_file_name, '112')
54 self.assertEqual(return_value, None)
55
56 def test_freshly_cache_version_to_disk(self):
57 temp_file = tempfile.NamedTemporaryFile('w+', delete=False)
58 return_value = _cache_version_to_disk(temp_file.name, '112')
59 self.addCleanup(os.remove, temp_file.name)
60 self.assertEqual(return_value, '112')
61
62 def test_get_latest_version(self):
63 def get_output(channel, device):
64 return b"295: description='fake295'\n\
65 296: description='fake296'\n\
66 297: description='fake297'\n\
67 298: description='fake338'\n"
68 observed = _get_latest_image_version('fakechannel',
69 'fakedevice',
70 get_output)
71 self.assertEqual(observed, '298')
72
73 def test_dont_get_latest_version(self):
74 def get_output(channel, device):
75 return b"100: description='fake100'\n"
76 observed = _get_latest_image_version('fakechannel',
77 'fakedevice',
78 get_output)
79 self.assertEqual(observed, '100')
80
81 def test_check_for_new_image(self):
82 def _get_latest_image_version(channel, device):
83 return '100'
84
85 def _cache_version_to_disk(location, latest_version):
86 return '99'
87
88 body = _check_for_new_image('fakelocation',
89 'fakechannel',
90 'fakedevice',
91 _get_latest_image_version,
92 _cache_version_to_disk)
93 self.assertEqual(body,
94 {'device': 'fakedevice',
95 'image_name': '100',
96 'channel': 'fakechannel'})
97
98 def test_check_for_no_new_image(self):
99 def _get_latest_image_version(channel, device):
100 return '100'
101
102 def _cache_version_to_disk(location, latest_version):
103 return None
104
105 body = _check_for_new_image('fakelocation',
106 'fakechannel',
107 'fakedevice',
108 _get_latest_image_version,
109 _cache_version_to_disk)
110 self.assertEqual(body, None)
0111
=== modified file 'requirements.txt'
--- requirements.txt 2015-03-25 01:34:08 +0000
+++ requirements.txt 2015-03-26 20:44:41 +0000
@@ -0,0 +1,1 @@
1kombu==3.0.24
02
=== modified file 'setup.py'
--- setup.py 2015-03-25 03:47:26 +0000
+++ setup.py 2015-03-26 20:44:41 +0000
@@ -36,5 +36,6 @@
36 url='https://launchpad.net/core-image-watcher',36 url='https://launchpad.net/core-image-watcher',
37 license='GPLv3',37 license='GPLv3',
38 packages=find_packages(),38 packages=find_packages(),
39 test_suite='core_image_watcher.tests',
39 scripts=['core-image-watcher.py']40 scripts=['core-image-watcher.py']
40)41)
4142
=== modified file 'test_requirements.txt'
--- test_requirements.txt 2015-03-25 01:34:08 +0000
+++ test_requirements.txt 2015-03-26 20:44:41 +0000
@@ -0,0 +1,1 @@
1testtools==1.7.1

Subscribers

People subscribed via source and target branches