Merge lp:~joetalbott/core-image-builder/add_build_logic into lp:core-image-builder
- add_build_logic
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Joe Talbott |
Approved revision: | 5 |
Merged at revision: | 3 |
Proposed branch: | lp:~joetalbott/core-image-builder/add_build_logic |
Merge into: | lp:core-image-builder |
Diff against target: |
304 lines (+169/-37) 5 files modified
core_image_builder/__init__.py (+3/-3) core_image_builder/queue.py (+2/-2) core_image_builder/worker.py (+151/-32) requirements.txt (+11/-0) test_requirements.txt (+2/-0) |
To merge this branch: | bzr merge lp:~joetalbott/core-image-builder/add_build_logic |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Joe Talbott (community) | Approve | ||
Francis Ginther | Approve | ||
Review via email: mp+259834@code.launchpad.net |
Commit message
Add first pass at image building logic.
Description of the change
Add first pass at image building logic.
Para Siva (psivaa) wrote : | # |
Francis Ginther (fginther) wrote : | # |
I've logged several comments, but nothing that should block this current MP. In fact, I would feel better landing this and then iterating on the comments in other MPs.
Also, it is my opinion that the channel and release input needs to come from the core-tester-agent. This is to solve the problem of needing to test both vivid and wily packages that land into two different core channels. Perhaps there is another solution that I've overlooked.
Ubuntu CI Bot (uci-bot) wrote : | # |
The attempt to merge lp:~joetalbott/core-image-builder/add_build_logic into lp:core-image-builder failed. Below is the output from the failed tests.
Using base prefix '/usr'
New python executable in /tmp/venv-
Also creating executable in /tmp/venv-
Installing setuptools, pip...done.
Running virtualenv with interpreter /usr/bin/python3
Ignoring indexes: https:/
Downloading/
Running setup.py (path:/
Downloading/
Running setup.py (path:/
Downloading/
Downloading/
Running setup.py (path:/
Downloading/
Downloading/
Downloading/
Downloading/
Running setup.py (path:/
warning: no previously-included files matching 'yacctab.*' found under directory 'tests'
warning: no previously-included files matching 'lextab.*' found under directory 'tests'
warning: no previously-included files matching 'yacctab.*' found under directory 'examples'
warning: no previously-included files matching 'lextab.*' found under directory 'examples'
Downloading/
Running setup.py (path:/
Downloading/
Running setup.py (path:/
Downloading/
Downloading/
Downloading/
Running setup.py (path:/
Downloading/
Running setup.py (path:/
warning: no previously-included files matching '*' found under directory 'docs/_build'
warning: no previously-included files matching '*.pyc' found under directory 'tests'
warning: no previously-...
- 5. By Joe Talbott
-
Fix flake8 warning
Joe Talbott (joetalbott) wrote : | # |
One more commit to fix a silly flake8 warning. I'm self-approving since it's so minor.
Preview Diff
1 | === modified file 'core_image_builder/__init__.py' |
2 | --- core_image_builder/__init__.py 2015-05-19 21:37:40 +0000 |
3 | +++ core_image_builder/__init__.py 2015-05-26 18:29:22 +0000 |
4 | @@ -28,7 +28,7 @@ |
5 | |
6 | from core_image_builder.queue import ( |
7 | create_connection_from_config, |
8 | - CoreImageResultBuilder, |
9 | + CoreImageBuilder, |
10 | ) |
11 | from core_image_builder import ( |
12 | constants, |
13 | @@ -67,14 +67,14 @@ |
14 | connection = create_connection_from_config(config) |
15 | try: |
16 | with connection: |
17 | - builder = CoreImageResultBuilder(connection) |
18 | + builder = CoreImageBuilder(connection) |
19 | retry_policy = DefaultRetryPolicy( |
20 | max_retries=3, |
21 | dead_queue='core.deadletters.{}'.format(constants.API_VERSION) |
22 | ) |
23 | monitor = SimpleRabbitQueueWorker( |
24 | connection, |
25 | - 'core.image.{}'.format(constants.API_VERSION), |
26 | + 'core.package.{}'.format(constants.API_VERSION), |
27 | worker.ImageBuilderWorker(config, builder), |
28 | retry_policy, |
29 | ) |
30 | |
31 | === modified file 'core_image_builder/queue.py' |
32 | --- core_image_builder/queue.py 2015-05-19 21:37:40 +0000 |
33 | +++ core_image_builder/queue.py 2015-05-26 18:29:22 +0000 |
34 | @@ -24,7 +24,7 @@ |
35 | logger = logging.getLogger(__name__) |
36 | |
37 | __all__ = [ |
38 | - 'CoreImageResultBuilder', |
39 | + 'CoreImageBuilder', |
40 | 'create_connection_from_config', |
41 | ] |
42 | |
43 | @@ -39,7 +39,7 @@ |
44 | return kombu.Connection(amqp_uris) |
45 | |
46 | |
47 | -class CoreImageResultBuilder(object): |
48 | +class CoreImageBuilder(object): |
49 | |
50 | """A callable that knows how to publish results from this service. |
51 | |
52 | |
53 | === modified file 'core_image_builder/worker.py' |
54 | --- core_image_builder/worker.py 2015-05-19 21:37:40 +0000 |
55 | +++ core_image_builder/worker.py 2015-05-26 18:29:22 +0000 |
56 | @@ -17,6 +17,7 @@ |
57 | |
58 | """Business logic or the service lives here.""" |
59 | |
60 | +import glob |
61 | import logging |
62 | import os |
63 | import subprocess |
64 | @@ -26,7 +27,10 @@ |
65 | |
66 | from core_image_builder.constants import LOGGING_EXTRA |
67 | from core_image_builder.cloud import get_glance_client |
68 | -from core_image_builder.utils import check_call |
69 | +from core_image_builder.utils import ( |
70 | + check_call, |
71 | + BetterCalledProcessError, |
72 | +) |
73 | |
74 | |
75 | logger = logging.getLogger(__name__) |
76 | @@ -47,6 +51,7 @@ |
77 | image_name = payload['image_name'] |
78 | channel = payload['channel'] |
79 | device = payload['device'] |
80 | + package_name = payload['package_name'] |
81 | except KeyError as e: |
82 | logger.error( |
83 | "Unable to deserialize message payload - " |
84 | @@ -57,9 +62,9 @@ |
85 | return MessageActions.Retry |
86 | |
87 | with tempfile.TemporaryDirectory() as tmpdir: |
88 | - logger.info("Beginning image download.", extra=extra) |
89 | + logger.info("Beginning rootfs download.", extra=extra) |
90 | try: |
91 | - image_path = download_image( |
92 | + rootfs_path = download_rootfs( |
93 | image_name, |
94 | channel, |
95 | device, |
96 | @@ -72,7 +77,61 @@ |
97 | extra=extra |
98 | ) |
99 | return MessageActions.Retry |
100 | - logger.info("Ubuntu Core image downloaded OK.", extra=extra) |
101 | + |
102 | + logger.info("Adding packages from proposed") |
103 | + MAX_RETRIES = 3 |
104 | + count = MAX_RETRIES |
105 | + while count > 0: |
106 | + new_rootfs_path = add_proposed_packages(rootfs_path, |
107 | + package_name) |
108 | + if new_rootfs_path is not None: |
109 | + break |
110 | + |
111 | + # try removing the rootfs as it sometimes is corrupted. |
112 | + # XXX: find out why it's getting corrupted. |
113 | + logger.info("Retrying rootfs download.", extra=extra) |
114 | + os.remove(rootfs_path) |
115 | + try: |
116 | + rootfs_path = download_rootfs( |
117 | + image_name, |
118 | + channel, |
119 | + device, |
120 | + tmpdir |
121 | + ) |
122 | + except subprocess.CalledProcessError as e: |
123 | + logger.error( |
124 | + "Unable to download core image: %s", |
125 | + e, |
126 | + extra=extra |
127 | + ) |
128 | + return MessageActions.Retry |
129 | + |
130 | + count -= 1 |
131 | + if count == 0: |
132 | + logger.error( |
133 | + "Unable to add packages after %d tries: %s", |
134 | + MAX_RETRIES, |
135 | + package_name, |
136 | + ) |
137 | + return MessageActions.Retry |
138 | + |
139 | + try: |
140 | + logger.info("Building the image") |
141 | + image_path = build_image( |
142 | + image_name, |
143 | + new_rootfs_path, |
144 | + channel, |
145 | + device, |
146 | + tmpdir |
147 | + ) |
148 | + except subprocess.CalledProcessError as e: |
149 | + logger.error( |
150 | + "Unable to build the image: %s", |
151 | + e, |
152 | + extra=extra |
153 | + ) |
154 | + return MessageActions.Retry |
155 | + logger.info("Ubuntu Core image build OK.", extra=extra) |
156 | |
157 | try: |
158 | nova_image_path = convert_nova_image(image_path) |
159 | @@ -106,34 +165,94 @@ |
160 | return MessageActions.Acknowledge |
161 | |
162 | |
163 | -def download_image(name, channel, device, tmpdir): |
164 | - """Download the ubuntu code image, return a path to it on disk.""" |
165 | - image_path = os.path.join(tmpdir, 'core-{}.img'.format(name)) |
166 | - udf_env = { |
167 | - # u-d-f fills '~/.cache/ubuntuimages/' on downloads, |
168 | - # not particulary useful to us, unless we only have one |
169 | - # worker and perform testing retries. |
170 | - 'HOME': tmpdir, |
171 | - } |
172 | - # The folling uses hardcoded parameters for '--channel' and 'release'. |
173 | - # It is ignoring the channel value passed in. |
174 | - cmd = ['sudo', |
175 | - 'ubuntu-device-flash', |
176 | - '--revision', name, |
177 | - 'core', |
178 | - '--device', device, |
179 | - '--channel', 'edge', |
180 | - '--size', '3', |
181 | - '-o', image_path, |
182 | - '--developer-mode', |
183 | - '--cloud', |
184 | - 'rolling'] |
185 | - check_call(cmd, env=udf_env) |
186 | - cmd = ['sudo', |
187 | - 'rm', |
188 | - '-rf', |
189 | - os.path.join(tmpdir, '.gnupg')] |
190 | - check_call(cmd, env=udf_env) |
191 | +def download_rootfs(name, channel, device, tmpdir): |
192 | + """Download the ubuntu code image, return a path to it on disk.""" |
193 | + image_path = os.path.join(tmpdir, 'core-{}.img'.format(name)) |
194 | + |
195 | + # The following uses hardcoded parameters for '--channel' and 'release'. |
196 | + # It is ignoring the channel value passed in. |
197 | + cmd = ['sudo', |
198 | + './ubuntu-device-flash', |
199 | + '--revision', name, |
200 | + '--download-only', |
201 | + 'core', |
202 | + '--device', device, |
203 | + '--channel', 'edge', |
204 | + '--size', '3', |
205 | + '-o', image_path, |
206 | + '--developer-mode', |
207 | + '--cloud', |
208 | + 'rolling'] |
209 | + check_call(cmd) |
210 | + |
211 | + cmd = ['sudo', |
212 | + 'rm', |
213 | + '-rf', |
214 | + os.path.join(tmpdir, '.gnupg')] |
215 | + check_call(cmd) |
216 | + output = glob.glob('ubuntu-*.tar.xz') |
217 | + return output[0] |
218 | + |
219 | + |
220 | +def add_proposed_packages(rootfs_path, package_name): |
221 | + |
222 | + new_rootfs_path = rootfs_path.replace('.tar.xz', '-modified.tar.xz') |
223 | + |
224 | + ROOTFS_BASE_DIR = 'system' |
225 | + |
226 | + # untar the rootfs |
227 | + cmd = ['sudo', 'tar', 'xJf', rootfs_path] |
228 | + try: |
229 | + check_call(cmd) |
230 | + except BetterCalledProcessError as e: |
231 | + logger.error(e) |
232 | + return None |
233 | + |
234 | + # sanity check that the base directory exists |
235 | + if not os.path.exists('system'): |
236 | + logger.error("Failed to untar the rootfs: %s", rootfs_path) |
237 | + return None |
238 | + |
239 | + # chroot into the rootfs and install packages |
240 | + cmd = ['sudo', 'chroot', os.path.join(os.getcwd(), ROOTFS_BASE_DIR), |
241 | + 'apt-get', ' update'] |
242 | + check_call(cmd) |
243 | + cmd = ['sudo', 'chroot', os.path.join(os.getcwd(), ROOTFS_BASE_DIR), |
244 | + 'apt-get', ' install ', package_name] |
245 | + check_call(cmd) |
246 | + |
247 | + # tar the rootfs back up. |
248 | + cmd = ['sudo', 'tar', 'cJf', new_rootfs_path, ROOTFS_BASE_DIR] |
249 | + check_call(cmd) |
250 | + |
251 | + return new_rootfs_path |
252 | + |
253 | + |
254 | +def build_image(name, rootfs_path, channel, device, tmpdir): |
255 | + """Download the ubuntu code image, return a path to it on disk.""" |
256 | + image_path = os.path.join(tmpdir, 'core-{}.img'.format(name)) |
257 | + # The following uses hardcoded parameters for '--channel' and 'release'. |
258 | + # It is ignoring the channel value passed in. |
259 | + cmd = ['sudo', |
260 | + './ubuntu-device-flash', |
261 | + '--revision', name, |
262 | + 'core', |
263 | + '--device', device, |
264 | + '--channel', 'edge', |
265 | + '--size', '3', |
266 | + '--image-part', rootfs_path, |
267 | + '-o', image_path, |
268 | + '--developer-mode', |
269 | + '--cloud', |
270 | + 'rolling'] |
271 | + check_call(cmd) |
272 | + |
273 | + cmd = ['sudo', |
274 | + 'rm', |
275 | + '-rf', |
276 | + os.path.join(tmpdir, '.gnupg')] |
277 | + check_call(cmd) |
278 | + |
279 | return image_path |
280 | |
281 | |
282 | |
283 | === added file 'requirements.txt' |
284 | --- requirements.txt 1970-01-01 00:00:00 +0000 |
285 | +++ requirements.txt 2015-05-26 18:29:22 +0000 |
286 | @@ -0,0 +1,11 @@ |
287 | +# XXX cprov 20150330: cffi and pyasn1 are cryptography (for glanceclient) |
288 | +# dependencies and if not available it uses easy_install for installing |
289 | +# and it doesn't respect local pip-cache setup, it ends up hitting pypi |
290 | +# on the internet. |
291 | +cffi |
292 | +pyasn1 |
293 | +kombu==3.0.24 |
294 | +python-logstash==0.4.2 |
295 | +python-glanceclient==0.17.0 |
296 | +uservice-utils==1.0.3 |
297 | +traceback2==1.4.0 |
298 | |
299 | === added file 'test_requirements.txt' |
300 | --- test_requirements.txt 1970-01-01 00:00:00 +0000 |
301 | +++ test_requirements.txt 2015-05-26 18:29:22 +0000 |
302 | @@ -0,0 +1,2 @@ |
303 | +testtools==1.7.1 |
304 | +flake8==2.4.0 |
Could not find any issues except a question inline.