Merge lp:~sil2100/ubuntu-system-image/server_script_testability into lp:ubuntu-system-image/server
- server_script_testability
- Merge into server
Status: | Needs review |
---|---|
Proposed branch: | lp:~sil2100/ubuntu-system-image/server_script_testability |
Merge into: | lp:ubuntu-system-image/server |
Diff against target: |
2040 lines (+1340/-531) 12 files modified
bin/copy-image (+11/-277) bin/set-phased-percentage (+8/-72) bin/tag-image (+4/-178) lib/systemimage/script.py (+269/-0) lib/systemimage/scripts/script_copy_image.py (+187/-0) lib/systemimage/scripts/script_set_phased_percentage.py (+83/-0) lib/systemimage/scripts/script_tag_image.py (+127/-0) lib/systemimage/tests/test_scripts.py (+590/-0) lib/systemimage/tests/test_tools.py (+41/-0) lib/systemimage/tests/test_tree.py (+1/-1) lib/systemimage/tools.py (+15/-0) lib/systemimage/tree.py (+4/-3) |
To merge this branch: | bzr merge lp:~sil2100/ubuntu-system-image/server_script_testability |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Barry Warsaw (community) | Approve | ||
Review via email: mp+285041@code.launchpad.net |
Commit message
Make scripts implemented as part of a Script class, making them more easily testable. Add some basic tests for the first two migrated - copy-image and tag-image.
Description of the change
The general script-testability branch. It's something I'm working on part-time between all other things - so far only two scripts are being migrated. Submitting for early review.
Barry Warsaw (barry) wrote : | # |
In general I really like the direction you're going in with this. More tests == better!
- 287. By Łukasz Zemczak
-
Add test for the script execute for tag-image. Fix a bug in setting the phased percentage after tagging (detected thanks to the unit test).
- 288. By Łukasz Zemczak
-
Add more tests for scripts.
- 289. By Łukasz Zemczak
-
Move the script classes to a separate script directory. Update copyright.
- 290. By Łukasz Zemczak
-
Fix script tests. Port the phased percentage script to the new framework, add tests.
- 291. By Łukasz Zemczak
-
Actually use the new script in the bin/ script of phased percentage.
- 292. By Łukasz Zemczak
-
Merge trunk.
- 293. By Łukasz Zemczak
-
Backport the new per-device redirect checks to scripts, add tests for those cases. Fix a few existing test failures.
- 294. By Łukasz Zemczak
-
pep8 fixes.
Unmerged revisions
- 294. By Łukasz Zemczak
-
pep8 fixes.
- 293. By Łukasz Zemczak
-
Backport the new per-device redirect checks to scripts, add tests for those cases. Fix a few existing test failures.
- 292. By Łukasz Zemczak
-
Merge trunk.
- 291. By Łukasz Zemczak
-
Actually use the new script in the bin/ script of phased percentage.
- 290. By Łukasz Zemczak
-
Fix script tests. Port the phased percentage script to the new framework, add tests.
- 289. By Łukasz Zemczak
-
Move the script classes to a separate script directory. Update copyright.
- 288. By Łukasz Zemczak
-
Add more tests for scripts.
- 287. By Łukasz Zemczak
-
Add test for the script execute for tag-image. Fix a bug in setting the phased percentage after tagging (detected thanks to the unit test).
- 286. By Łukasz Zemczak
-
PEP8 fixes to scripts.py.
- 285. By Łukasz Zemczak
-
Add arg parsing tests for the tag script as well. Do some PEP8 syntax fixes.
Preview Diff
1 | === modified file 'bin/copy-image' (properties changed: +x to -x) |
2 | --- bin/copy-image 2016-06-24 21:02:18 +0000 |
3 | +++ bin/copy-image 2016-07-20 12:34:06 +0000 |
4 | @@ -1,8 +1,8 @@ |
5 | #!/usr/bin/python |
6 | # -*- coding: utf-8 -*- |
7 | |
8 | -# Copyright (C) 2013 Canonical Ltd. |
9 | -# Author: Stéphane Graber <stgraber@ubuntu.com> |
10 | +# Copyright (C) 2015-2016 Canonical Ltd. |
11 | +# Author: Łukasz 'sil2100' Zemczak <lukasz.zemczak@ubuntu.com> |
12 | |
13 | # This program is free software: you can redistribute it and/or modify |
14 | # it under the terms of the GNU General Public License as published by |
15 | @@ -16,284 +16,18 @@ |
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 | |
19 | -import json |
20 | import os |
21 | import sys |
22 | + |
23 | sys.path.insert(0, os.path.join(sys.path[0], os.pardir, "lib")) |
24 | |
25 | -from systemimage import config, generators, tools, tree |
26 | - |
27 | -import argparse |
28 | -import fcntl |
29 | -import logging |
30 | +from systemimage.scripts import script_copy_image |
31 | + |
32 | + |
33 | +def main(): |
34 | + script = script_copy_image.CopyImageScript() |
35 | + script.run(sys.argv) |
36 | + |
37 | |
38 | if __name__ == '__main__': |
39 | - parser = argparse.ArgumentParser(description="image copier") |
40 | - parser.add_argument("source_channel", metavar="SOURCE-CHANNEL") |
41 | - parser.add_argument("destination_channel", metavar="DESTINATION-CHANNEL") |
42 | - parser.add_argument("device", metavar="DEVICE") |
43 | - parser.add_argument("version", metavar="VERSION", type=int) |
44 | - parser.add_argument("-k", "--keep-version", action="store_true", |
45 | - help="Keep the original version number") |
46 | - parser.add_argument("-p", "--phased-percentage", type=int, |
47 | - help="Set the phased percentage for the copied image", |
48 | - default=100) |
49 | - parser.add_argument("-t", "--tag", type=str, |
50 | - help="Set a version tag on the new image") |
51 | - parser.add_argument("--verbose", "-v", action="count", default=0) |
52 | - |
53 | - args = parser.parse_args() |
54 | - |
55 | - # Setup logging |
56 | - formatter = logging.Formatter( |
57 | - "%(asctime)s %(levelname)s %(message)s") |
58 | - |
59 | - levels = {1: logging.ERROR, |
60 | - 2: logging.WARNING, |
61 | - 3: logging.INFO, |
62 | - 4: logging.DEBUG} |
63 | - |
64 | - if args.verbose > 0: |
65 | - stdoutlogger = logging.StreamHandler(sys.stdout) |
66 | - stdoutlogger.setFormatter(formatter) |
67 | - logging.root.setLevel(levels[min(4, args.verbose)]) |
68 | - logging.root.addHandler(stdoutlogger) |
69 | - else: |
70 | - logging.root.addHandler(logging.NullHandler()) |
71 | - |
72 | - # Load the configuration |
73 | - conf = config.Config() |
74 | - |
75 | - # Try to acquire a global lock |
76 | - lock_file = os.path.join(conf.state_path, "global.lock") |
77 | - lock_fd = open(lock_file, 'w') |
78 | - |
79 | - try: |
80 | - fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) |
81 | - except IOError: |
82 | - print("Something else holds the global lock. exiting.") |
83 | - sys.exit(0) |
84 | - |
85 | - # Load the tree |
86 | - pub = tree.Tree(conf) |
87 | - |
88 | - # Do some checks |
89 | - channels = pub.list_channels() |
90 | - if args.source_channel not in channels: |
91 | - parser.error("Invalid source channel: %s" % args.source_channel) |
92 | - |
93 | - if args.destination_channel not in channels: |
94 | - parser.error("Invalid destination channel: %s" % |
95 | - args.destination_channel) |
96 | - |
97 | - if args.device not in channels[args.source_channel]['devices']: |
98 | - parser.error("Invalid device for source channel: %s" % |
99 | - args.device) |
100 | - |
101 | - if args.device not in \ |
102 | - channels[args.destination_channel]['devices']: |
103 | - parser.error("Invalid device for destination channel: %s" % |
104 | - args.device) |
105 | - |
106 | - if "alias" in channels[args.source_channel] and \ |
107 | - channels[args.source_channel]['alias'] \ |
108 | - != args.source_channel: |
109 | - parser.error("Source channel is an alias.") |
110 | - |
111 | - if "alias" in channels[args.destination_channel] and \ |
112 | - channels[args.destination_channel]['alias'] \ |
113 | - != args.destination_channel: |
114 | - parser.error("Destination channel is an alias.") |
115 | - |
116 | - if "redirect" in channels[args.source_channel]: |
117 | - parser.error("Source channel is a redirect.") |
118 | - |
119 | - if "redirect" in channels[args.destination_channel]: |
120 | - parser.error("Destination channel is a redirect.") |
121 | - |
122 | - src_device = channels[args.source_channel]['devices'][args.device] |
123 | - if "redirect" in src_device: |
124 | - parser.error("Source device is a redirect. Use original channel " |
125 | - "%s instead." % src_device['redirect']) |
126 | - |
127 | - dest_device = channels[args.destination_channel]['devices'][args.device] |
128 | - if "redirect" in dest_device: |
129 | - parser.error("Destination device is a redirect. Use original channel " |
130 | - "%s instead." % dest_device['redirect']) |
131 | - |
132 | - if args.tag and "," in args.tag: |
133 | - parser.error("Image tag cannot include the character ','.") |
134 | - |
135 | - source_device = pub.get_device(args.source_channel, args.device) |
136 | - destination_device = pub.get_device(args.destination_channel, args.device) |
137 | - |
138 | - if args.keep_version: |
139 | - images = [image for image in destination_device.list_images() |
140 | - if image['version'] == args.version] |
141 | - if images: |
142 | - parser.error("Version number is already used: %s" % args.version) |
143 | - |
144 | - # Assign a new version number |
145 | - new_version = args.version |
146 | - if not args.keep_version: |
147 | - # Find the next available version |
148 | - new_version = 1 |
149 | - for image in destination_device.list_images(): |
150 | - if image['version'] >= new_version: |
151 | - new_version = image['version'] + 1 |
152 | - logging.debug("Version for next image: %s" % new_version) |
153 | - |
154 | - # Extract the build we want to copy |
155 | - images = [image for image in source_device.list_images() |
156 | - if image['type'] == "full" and image['version'] == args.version] |
157 | - if not images: |
158 | - parser.error("Can't find version: %s" % args.version) |
159 | - source_image = images[0] |
160 | - |
161 | - # Extract the list of existing full images |
162 | - full_images = {image['version']: image |
163 | - for image in destination_device.list_images() |
164 | - if image['type'] == "full"} |
165 | - |
166 | - # Check that the last full and the new image aren't one and the same |
167 | - source_files = [entry['path'].split("/")[-1] |
168 | - for entry in source_image['files'] |
169 | - if not entry['path'].split("/")[-1].startswith("version-")] |
170 | - destination_files = [] |
171 | - if full_images: |
172 | - latest_full = sorted(full_images.values(), |
173 | - key=lambda image: image['version'])[-1] |
174 | - destination_files = [entry['path'].split("/")[-1] |
175 | - for entry in latest_full['files'] |
176 | - if not entry['path'].split( |
177 | - "/")[-1].startswith("version-")] |
178 | - if source_files == destination_files: |
179 | - parser.error("Source image is already latest full in " |
180 | - "destination channel.") |
181 | - |
182 | - # Generate a list of required deltas |
183 | - delta_base = tools.get_required_deltas( |
184 | - conf, pub, args.destination_channel, args.device) |
185 | - |
186 | - # Create new empty entries |
187 | - new_images = {'full': {'files': []}} |
188 | - for delta in delta_base: |
189 | - new_images['delta_%s' % delta['version']] = {'files': []} |
190 | - |
191 | - # Extract current version_detail and files |
192 | - version_detail = tools.extract_files_and_version( |
193 | - conf, source_image['files'], args.version, new_images['full']['files']) |
194 | - |
195 | - # Generate new version tarball |
196 | - environment = {} |
197 | - environment['channel_name'] = args.destination_channel |
198 | - environment['device'] = destination_device |
199 | - environment['device_name'] = args.device |
200 | - environment['version'] = new_version |
201 | - environment['version_detail'] = [entry |
202 | - for entry in version_detail.split(",") |
203 | - if not entry.startswith("version=")] |
204 | - environment['new_files'] = new_images['full']['files'] |
205 | - |
206 | - # Add new tag if requested |
207 | - if args.tag: |
208 | - tools.set_tag_on_version_detail( |
209 | - environment['version_detail'], args.tag) |
210 | - logging.info("Setting tag for image to '%s'" % args.tag) |
211 | - |
212 | - logging.info("Generating new version tarball for '%s' (%s)" |
213 | - % (new_version, ",".join(environment['version_detail']))) |
214 | - version_path = generators.generate_file(conf, "version", [], environment) |
215 | - if version_path: |
216 | - new_images['full']['files'].append(version_path) |
217 | - |
218 | - # Generate deltas |
219 | - for abspath in new_images['full']['files']: |
220 | - prefix = abspath.split("/")[-1].rsplit("-", 1)[0] |
221 | - for delta in delta_base: |
222 | - # Extract the source |
223 | - src_path = None |
224 | - for file_dict in delta['files']: |
225 | - if (file_dict['path'].split("/")[-1] |
226 | - .startswith(prefix)): |
227 | - src_path = "%s/%s" % (conf.publish_path, |
228 | - file_dict['path']) |
229 | - break |
230 | - |
231 | - # Check that it's not the current file |
232 | - if src_path: |
233 | - src_path = os.path.realpath(src_path) |
234 | - |
235 | - # FIXME: the keyring- is a big hack... |
236 | - if src_path == abspath and "keyring-" not in src_path: |
237 | - continue |
238 | - |
239 | - # Generators are allowed to return None when no delta |
240 | - # exists at all. |
241 | - logging.info("Generating delta from '%s' for '%s'" % |
242 | - (delta['version'], |
243 | - prefix)) |
244 | - delta_path = generators.generate_delta(conf, src_path, |
245 | - abspath) |
246 | - else: |
247 | - delta_path = abspath |
248 | - |
249 | - if not delta_path: |
250 | - continue |
251 | - |
252 | - # Get the full and relative paths |
253 | - delta_abspath, delta_relpath = tools.expand_path( |
254 | - delta_path, conf.publish_path) |
255 | - |
256 | - new_images['delta_%s' % delta['version']]['files'] \ |
257 | - .append(delta_abspath) |
258 | - |
259 | - # Add full image |
260 | - logging.info("Publishing new image '%s' (%s) with %s files." |
261 | - % (new_version, ",".join(environment['version_detail']), |
262 | - len(new_images['full']['files']))) |
263 | - destination_device.create_image( |
264 | - "full", new_version, ",".join(environment['version_detail']), |
265 | - new_images['full']['files'], |
266 | - version_detail=",".join(environment['version_detail'])) |
267 | - |
268 | - # Add delta images |
269 | - for delta in delta_base: |
270 | - files = new_images['delta_%s' % delta['version']]['files'] |
271 | - logging.info("Publishing new delta from '%s' (%s)" |
272 | - " to '%s' (%s) with %s files" % |
273 | - (delta['version'], delta.get("description", ""), |
274 | - new_version, ",".join(environment['version_detail']), |
275 | - len(files))) |
276 | - |
277 | - destination_device.create_image( |
278 | - "delta", new_version, |
279 | - ",".join(environment['version_detail']), |
280 | - files, |
281 | - base=delta['version'], |
282 | - version_detail=",".join(environment['version_detail'])) |
283 | - |
284 | - # Set the phased percentage value for the new image |
285 | - logging.info("Setting phased percentage of the new image to %s%%" % |
286 | - args.phased_percentage) |
287 | - destination_device.set_phased_percentage( |
288 | - new_version, args.phased_percentage) |
289 | - |
290 | - # Expire images |
291 | - if args.destination_channel in conf.channels: |
292 | - if conf.channels[args.destination_channel].fullcount > 0: |
293 | - logging.info("Expiring old images") |
294 | - destination_device.expire_images( |
295 | - conf.channels[args.destination_channel].fullcount) |
296 | - |
297 | - # Sync all channel aliases |
298 | - logging.info("Syncing any existing alias") |
299 | - pub.sync_aliases(args.destination_channel) |
300 | - |
301 | - # Remove any orphaned file |
302 | - logging.info("Removing orphaned files from the pool") |
303 | - pub.cleanup_tree() |
304 | - |
305 | - # Sync the mirrors |
306 | - logging.info("Triggering a mirror sync") |
307 | - tools.sync_mirrors(conf) |
308 | + main() |
309 | |
310 | === modified file 'bin/set-phased-percentage' (properties changed: +x to -x) |
311 | --- bin/set-phased-percentage 2016-06-24 21:02:18 +0000 |
312 | +++ bin/set-phased-percentage 2016-07-20 12:34:06 +0000 |
313 | @@ -20,77 +20,13 @@ |
314 | import sys |
315 | sys.path.insert(0, os.path.join(sys.path[0], os.pardir, "lib")) |
316 | |
317 | -from systemimage import config, tools, tree |
318 | - |
319 | -import argparse |
320 | -import logging |
321 | +from systemimage.scripts import script_set_phased_percentage |
322 | + |
323 | + |
324 | +def main(): |
325 | + script = script_set_phased_percentage.SetPhasedPercentageScript() |
326 | + script.run(sys.argv) |
327 | + |
328 | |
329 | if __name__ == '__main__': |
330 | - parser = argparse.ArgumentParser(description="set phased percentage") |
331 | - parser.add_argument("channel", metavar="CHANNEL") |
332 | - parser.add_argument("device", metavar="DEVICE") |
333 | - parser.add_argument("version", metavar="VERSION", type=int) |
334 | - parser.add_argument("percentage", metavar="PERCENTAGE", type=int) |
335 | - parser.add_argument("--verbose", "-v", action="count") |
336 | - |
337 | - args = parser.parse_args() |
338 | - |
339 | - # Setup logging |
340 | - formatter = logging.Formatter( |
341 | - "%(asctime)s %(levelname)s %(message)s") |
342 | - |
343 | - levels = {1: logging.ERROR, |
344 | - 2: logging.WARNING, |
345 | - 3: logging.INFO, |
346 | - 4: logging.DEBUG} |
347 | - |
348 | - if args.verbose > 0: |
349 | - stdoutlogger = logging.StreamHandler(sys.stdout) |
350 | - stdoutlogger.setFormatter(formatter) |
351 | - logging.root.setLevel(levels[min(4, args.verbose)]) |
352 | - logging.root.addHandler(stdoutlogger) |
353 | - else: |
354 | - logging.root.addHandler(logging.NullHandler()) |
355 | - |
356 | - # Load the configuration |
357 | - conf = config.Config() |
358 | - |
359 | - # Load the tree |
360 | - pub = tree.Tree(conf) |
361 | - |
362 | - # Do some checks |
363 | - channels = pub.list_channels() |
364 | - if args.channel not in channels: |
365 | - parser.error("Invalid channel: %s" % args.channel) |
366 | - |
367 | - if args.device not in channels[args.channel]['devices']: |
368 | - parser.error("Invalid device for source channel: %s" % |
369 | - args.device) |
370 | - |
371 | - if args.percentage < 0 or args.percentage > 100: |
372 | - parser.error("Invalid value: %s" % args.percentage) |
373 | - |
374 | - if "alias" in channels[args.channel] and \ |
375 | - channels[args.channel]['alias'] != args.channel: |
376 | - parser.error("Channel is an alias.") |
377 | - |
378 | - if "redirect" in channels[args.channel]: |
379 | - parser.error("Channel is a redirect.") |
380 | - |
381 | - target_device = channels[args.channel]['devices'][args.device] |
382 | - if "redirect" in target_device: |
383 | - parser.error("Target device is a redirect. Use original channel " |
384 | - "%s instead." % target_device['redirect']) |
385 | - |
386 | - dev = pub.get_device(args.channel, args.device) |
387 | - logging.info("Setting phased-percentage of '%s' to %s%%" % |
388 | - (args.version, args.percentage)) |
389 | - dev.set_phased_percentage(args.version, args.percentage) |
390 | - |
391 | - # Sync all channel aliases |
392 | - logging.info("Syncing any existing alias") |
393 | - pub.sync_aliases(args.channel) |
394 | - |
395 | - # Sync the mirrors |
396 | - logging.info("Triggering a mirror sync") |
397 | - tools.sync_mirrors(conf) |
398 | + main() |
399 | |
400 | === modified file 'bin/tag-image' (properties changed: +x to -x) |
401 | --- bin/tag-image 2016-06-24 21:02:18 +0000 |
402 | +++ bin/tag-image 2016-07-20 12:34:06 +0000 |
403 | @@ -1,7 +1,7 @@ |
404 | #!/usr/bin/python |
405 | # -*- coding: utf-8 -*- |
406 | |
407 | -# Copyright (C) 2015 Canonical Ltd. |
408 | +# Copyright (C) 2015-2016 Canonical Ltd. |
409 | # Author: Łukasz 'sil2100' Zemczak <lukasz.zemczak@ubuntu.com> |
410 | |
411 | # This program is free software: you can redistribute it and/or modify |
412 | @@ -16,191 +16,17 @@ |
413 | # You should have received a copy of the GNU General Public License |
414 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
415 | |
416 | -import json |
417 | import os |
418 | import sys |
419 | -import argparse |
420 | -import fcntl |
421 | -import logging |
422 | |
423 | sys.path.insert(0, os.path.join(sys.path[0], os.pardir, "lib")) |
424 | |
425 | -from operator import itemgetter |
426 | -from systemimage import config, generators, tools, tree |
427 | +from systemimage.scripts import script_tag_image |
428 | |
429 | |
430 | def main(): |
431 | - parser = argparse.ArgumentParser(description="image tagger") |
432 | - parser.add_argument("channel", metavar="CHANNEL") |
433 | - parser.add_argument("device", metavar="DEVICE") |
434 | - parser.add_argument("version", metavar="VERSION", type=int) |
435 | - parser.add_argument("tag", metavar="TAG") |
436 | - parser.add_argument("--verbose", "-v", action="count", default=0) |
437 | - |
438 | - args = parser.parse_args() |
439 | - |
440 | - # Setup logging |
441 | - formatter = logging.Formatter( |
442 | - "%(asctime)s %(levelname)s %(message)s") |
443 | - |
444 | - levels = {1: logging.ERROR, |
445 | - 2: logging.WARNING, |
446 | - 3: logging.INFO, |
447 | - 4: logging.DEBUG} |
448 | - |
449 | - if args.verbose > 0: |
450 | - stdoutlogger = logging.StreamHandler(sys.stdout) |
451 | - stdoutlogger.setFormatter(formatter) |
452 | - logging.root.setLevel(levels[min(4, args.verbose)]) |
453 | - logging.root.addHandler(stdoutlogger) |
454 | - else: |
455 | - logging.root.addHandler(logging.NullHandler()) |
456 | - |
457 | - # Load the configuration |
458 | - conf = config.Config() |
459 | - |
460 | - # Try to acquire a global lock |
461 | - lock_file = os.path.join(conf.state_path, "global.lock") |
462 | - lock_fd = open(lock_file, 'w') |
463 | - |
464 | - try: |
465 | - fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) |
466 | - except IOError: |
467 | - print("Something else holds the global lock. exiting.") |
468 | - sys.exit(0) |
469 | - |
470 | - # Load the tree |
471 | - pub = tree.Tree(conf) |
472 | - |
473 | - # Do some checks |
474 | - channels = pub.list_channels() |
475 | - if args.channel not in channels: |
476 | - parser.error("Invalid channel: %s" % args.channel) |
477 | - |
478 | - if args.device not in channels[args.channel]['devices']: |
479 | - parser.error("Invalid device for channel: %s" % args.device) |
480 | - |
481 | - if "alias" in channels[args.channel] and \ |
482 | - channels[args.channel]['alias'] \ |
483 | - != args.channel: |
484 | - parser.error("Channel is an alias.") |
485 | - |
486 | - if "redirect" in channels[args.channel]: |
487 | - parser.error("Channel is a redirect.") |
488 | - |
489 | - target_device = channels[args.channel]['devices'][args.device] |
490 | - if "redirect" in target_device: |
491 | - parser.error("Target device is a redirect. Use original channel " |
492 | - "%s instead." % target_device['redirect']) |
493 | - |
494 | - if "," in args.tag: |
495 | - parser.error("Image tag cannot include the character ','.") |
496 | - |
497 | - device = pub.get_device(args.channel, args.device) |
498 | - |
499 | - device_images = device.list_images() |
500 | - if not device_images: |
501 | - parser.error("No images in selected channel/device.") |
502 | - |
503 | - sorted_images = sorted(device_images, key=itemgetter('version')) |
504 | - if args.version != sorted_images[-1]['version']: |
505 | - parser.error("You can only tag the latest image.") |
506 | - |
507 | - # Extract the build we want to copy |
508 | - images = [image for image in device_images |
509 | - if image['type'] == "full" and image['version'] == args.version] |
510 | - if not images: |
511 | - parser.error("Can't find version: %s" % args.version) |
512 | - image = images[0] |
513 | - |
514 | - # Assign a new version number |
515 | - new_version = args.version + 1 |
516 | - |
517 | - # Generate a list of required deltas |
518 | - delta_base = tools.get_required_deltas( |
519 | - conf, pub, args.channel, args.device) |
520 | - |
521 | - # Create a new empty file list |
522 | - new_files = [] |
523 | - |
524 | - # Extract current version_detail and image files |
525 | - version_detail = tools.extract_files_and_version( |
526 | - conf, image['files'], args.version, new_files) |
527 | - logging.debug("Original version_detail is: %s" % version_detail) |
528 | - |
529 | - # Generate new version tarball environment |
530 | - environment = {} |
531 | - environment['channel_name'] = args.channel |
532 | - environment['device'] = device |
533 | - environment['device_name'] = args.device |
534 | - environment['version'] = new_version |
535 | - environment['version_detail'] = [entry |
536 | - for entry in version_detail.split(",") |
537 | - if not entry.startswith("version=")] |
538 | - environment['new_files'] = new_files |
539 | - |
540 | - # Set the tag for the image |
541 | - tools.set_tag_on_version_detail(environment['version_detail'], args.tag) |
542 | - logging.info("Setting tag for image to '%s'" % args.tag) |
543 | - |
544 | - # Generate the new version tarball |
545 | - logging.info("Generating new version tarball for '%s' (%s)" |
546 | - % (new_version, ",".join(environment['version_detail']))) |
547 | - version_path = generators.generate_file(conf, "version", [], environment) |
548 | - if version_path: |
549 | - new_files.append(version_path) |
550 | - |
551 | - # Add full image |
552 | - logging.info("Publishing new image '%s' (%s) with %s files." |
553 | - % (new_version, ",".join(environment['version_detail']), |
554 | - len(new_files))) |
555 | - device.create_image( |
556 | - "full", new_version, ",".join(environment['version_detail']), |
557 | - new_files, version_detail=",".join(environment['version_detail'])) |
558 | - |
559 | - # Add delta images |
560 | - delta_files = [] |
561 | - for path in new_files: |
562 | - filename = path.split("/")[-1] |
563 | - if filename.startswith("version-") or filename.startswith("keyring-"): |
564 | - delta_files.append(path) |
565 | - |
566 | - for delta in delta_base: |
567 | - logging.info("Publishing new delta from '%s' (%s)" |
568 | - " to '%s' (%s)" % |
569 | - (delta['version'], delta.get("description", ""), |
570 | - new_version, ",".join(environment['version_detail']))) |
571 | - |
572 | - device.create_image( |
573 | - "delta", new_version, |
574 | - ",".join(environment['version_detail']), |
575 | - delta_files, |
576 | - base=delta['version'], |
577 | - version_detail=",".join(environment['version_detail'])) |
578 | - |
579 | - # Set the phased percentage value for the new image |
580 | - percentage = device.get_phased_percentage(args.version) |
581 | - logging.info("Setting phased percentage of the new image to %s%%" % |
582 | - percentage) |
583 | - device.set_phased_percentage(new_version, percentage) |
584 | - |
585 | - # Expire images |
586 | - if args.channel in conf.channels: |
587 | - if conf.channels[args.channel].fullcount > 0: |
588 | - logging.info("Expiring old images") |
589 | - device.expire_images(conf.channels[args.channel].fullcount) |
590 | - |
591 | - # Sync all channel aliases |
592 | - logging.info("Syncing any existing alias") |
593 | - pub.sync_aliases(args.channel) |
594 | - |
595 | - # Remove any orphaned file |
596 | - logging.info("Removing orphaned files from the pool") |
597 | - pub.cleanup_tree() |
598 | - |
599 | - # Sync the mirrors |
600 | - logging.info("Triggering a mirror sync") |
601 | - tools.sync_mirrors(conf) |
602 | + script = script_tag_image.TagImageScript() |
603 | + script.run(sys.argv) |
604 | |
605 | |
606 | if __name__ == '__main__': |
607 | |
608 | === added file 'lib/systemimage/script.py' |
609 | --- lib/systemimage/script.py 1970-01-01 00:00:00 +0000 |
610 | +++ lib/systemimage/script.py 2016-07-20 12:34:06 +0000 |
611 | @@ -0,0 +1,269 @@ |
612 | +# -*- coding: utf-8 -*- |
613 | + |
614 | +# Copyright (C) 2015-2016 Canonical Ltd. |
615 | +# Authors: Łukasz 'sil2100' Zemczak <lukasz.zemczak@ubuntu.com> |
616 | +# Stéphane Graber <stgraber@ubuntu.com> |
617 | + |
618 | +# This program is free software: you can redistribute it and/or modify |
619 | +# it under the terms of the GNU General Public License as published by |
620 | +# the Free Software Foundation; version 3 of the License. |
621 | +# |
622 | +# This program is distributed in the hope that it will be useful, |
623 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
624 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
625 | +# GNU General Public License for more details. |
626 | +# |
627 | +# You should have received a copy of the GNU General Public License |
628 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
629 | + |
630 | +import json |
631 | +import os |
632 | +import sys |
633 | +import argparse |
634 | +import fcntl |
635 | +import logging |
636 | + |
637 | +sys.path.insert(0, os.path.join(sys.path[0], os.pardir, "lib")) |
638 | + |
639 | +from operator import itemgetter |
640 | +from systemimage import config, generators, tools, tree |
641 | + |
642 | + |
643 | +class ScriptError(Exception): |
644 | + """Exception used to indicate error on any stage of a script.""" |
645 | + pass |
646 | + |
647 | + |
648 | +class Script: |
649 | + """Base common class for all system-image scripts.""" |
650 | + |
651 | + def __init__(self, path=None): |
652 | + # Load the configuration |
653 | + self.conf = config.Config(path) if path else config.Config() |
654 | + self.pub = None # system-image tree, defined during run() |
655 | + |
656 | + # Variables to be defined during setup()/execute() |
657 | + self.channel = None # Channel name |
658 | + self.device = None # Device object |
659 | + |
660 | + self.lock_fd = None |
661 | + |
662 | + def setup_logging(self, verbosity): |
663 | + """Helper function setting up common logging scheme.""" |
664 | + |
665 | + formatter = logging.Formatter( |
666 | + "%(asctime)s %(levelname)s %(message)s") |
667 | + |
668 | + levels = {1: logging.ERROR, |
669 | + 2: logging.WARNING, |
670 | + 3: logging.INFO, |
671 | + 4: logging.DEBUG} |
672 | + |
673 | + if verbosity > 0: |
674 | + stdoutlogger = logging.StreamHandler(sys.stdout) |
675 | + stdoutlogger.setFormatter(formatter) |
676 | + logging.root.setLevel(levels[min(4, verbosity)]) |
677 | + logging.root.addHandler(stdoutlogger) |
678 | + else: |
679 | + logging.root.addHandler(logging.NullHandler()) |
680 | + |
681 | + def sync_trees(self): |
682 | + """Helper function for syncing changes to all trees.""" |
683 | + # Sync all channel aliases |
684 | + logging.info("Syncing any existing alias") |
685 | + self.pub.sync_aliases(self.channel) |
686 | + |
687 | + # Sync the mirrors |
688 | + logging.info("Triggering a mirror sync") |
689 | + tools.sync_mirrors(self.conf) |
690 | + |
691 | + def lock(self): |
692 | + """Aquire the global lock, protecting from races.""" |
693 | + |
694 | + # Try to acquire a global lock |
695 | + lock_file = os.path.join(self.conf.state_path, "global.lock") |
696 | + self.lock_fd = open(lock_file, 'w') |
697 | + |
698 | + try: |
699 | + fcntl.lockf(self.lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) |
700 | + except IOError: |
701 | + raise ScriptError("Something else holds the global lock. exiting.") |
702 | + |
703 | + def presetup(self): |
704 | + """All low-level pre-setup to be done before the script can be run.""" |
705 | + # Load the tree |
706 | + self.pub = tree.Tree(self.conf) |
707 | + |
708 | + def setup(self, argv): |
709 | + """Implementable in script. Perform script setup, parse cmdline.""" |
710 | + pass |
711 | + |
712 | + def execute(self): |
713 | + """Implementable in script. All script execution code goes here.""" |
714 | + pass |
715 | + |
716 | + def cleanup(self): |
717 | + """Called at the end - by default provides image cleanup.""" |
718 | + |
719 | + # Expire images, if a device was used in the script |
720 | + if self.device and self.channel in self.conf.channels: |
721 | + if self.conf.channels[self.channel].fullcount > 0: |
722 | + logging.info("Expiring old images") |
723 | + self.device.expire_images( |
724 | + self.conf.channels[ |
725 | + self.channel].fullcount) |
726 | + |
727 | + # Remove any orphaned file |
728 | + logging.info("Removing orphaned files from the pool") |
729 | + self.pub.cleanup_tree() |
730 | + |
731 | + self.sync_trees() |
732 | + |
733 | + def run(self, argv): |
734 | + """Run the script.""" |
735 | + |
736 | + try: |
737 | + self.lock() |
738 | + self.presetup() |
739 | + self.setup(argv) |
740 | + self.execute() |
741 | + self.cleanup() |
742 | + except ScriptError as err: |
743 | + print(str(err)) |
744 | + return False |
745 | + |
746 | + return True |
747 | + |
748 | + |
749 | +class CommonImageManipulationScript(Script): |
750 | + """A common base for scripts that manipulate images, providing helpers.""" |
751 | + |
752 | + def generate_version(self, source_files, version, tag=None): |
753 | + """ |
754 | + Generate the version tarball for the version and files. |
755 | + Requires the self.target_version, self.channel and self.device |
756 | + variables: for the new version number, destination channel and |
757 | + device object respectively. |
758 | + """ |
759 | + |
760 | + # Extract current version_detail and files |
761 | + version_detail = tools.extract_files_and_version( |
762 | + self.conf, |
763 | + source_files, |
764 | + version, |
765 | + self.new_images['full']['files']) |
766 | + |
767 | + # Generate new version tarball |
768 | + self.environment = {} |
769 | + self.environment['channel_name'] = self.channel |
770 | + self.environment['device'] = self.device |
771 | + self.environment['device_name'] = self.device.name |
772 | + self.environment['version'] = self.target_version |
773 | + self.environment['version_detail'] = [ |
774 | + entry for entry in version_detail.split(",") |
775 | + if not entry.startswith("version=")] |
776 | + self.environment['new_files'] = self.new_images['full']['files'] |
777 | + |
778 | + # Add new tag if requested |
779 | + if tag: |
780 | + tools.set_tag_on_version_detail( |
781 | + self.environment['version_detail'], tag) |
782 | + logging.info("Setting tag for image to '%s'" % tag) |
783 | + |
784 | + logging.info("Generating new version tarball for '%s' (%s)" |
785 | + % (self.target_version, ",".join( |
786 | + self.environment['version_detail']))) |
787 | + version_path = generators.generate_file( |
788 | + self.conf, "version", [], self.environment) |
789 | + if version_path: |
790 | + self.new_images['full']['files'].append(version_path) |
791 | + |
792 | + def generate_deltas(self): |
793 | + """ |
794 | + Generates deltas from the list in self.delta_base. |
795 | + To generate the deltas the self.new_images dictionary is used. All |
796 | + the newly generated delta files are then appended to the new_images |
797 | + structure. |
798 | + """ |
799 | + |
800 | + for delta in self.delta_base: |
801 | + self.new_images['delta_%s' % delta['version']] = {'files': []} |
802 | + |
803 | + # Generate deltas |
804 | + for abspath in self.new_images['full']['files']: |
805 | + prefix = abspath.split("/")[-1].rsplit("-", 1)[0] |
806 | + for delta in self.delta_base: |
807 | + # Extract the source |
808 | + src_path = None |
809 | + for file_dict in delta['files']: |
810 | + if (file_dict['path'].split("/")[-1] |
811 | + .startswith(prefix)): |
812 | + src_path = "%s/%s" % (self.conf.publish_path, |
813 | + file_dict['path']) |
814 | + break |
815 | + |
816 | + # Check that it's not the current file |
817 | + if src_path: |
818 | + src_path = os.path.realpath(src_path) |
819 | + |
820 | + # FIXME: the keyring- is a big hack... |
821 | + if src_path == abspath and "keyring-" not in src_path: |
822 | + continue |
823 | + |
824 | + # Generators are allowed to return None when no delta |
825 | + # exists at all. |
826 | + logging.info("Generating delta from '%s' for '%s'" % |
827 | + (delta['version'], |
828 | + prefix)) |
829 | + delta_path = generators.generate_delta(self.conf, src_path, |
830 | + abspath) |
831 | + else: |
832 | + delta_path = abspath |
833 | + |
834 | + if not delta_path: |
835 | + continue |
836 | + |
837 | + # Get the full and relative paths |
838 | + delta_abspath, delta_relpath = tools.expand_path( |
839 | + delta_path, self.conf.publish_path) |
840 | + |
841 | + self.new_images['delta_%s' % delta['version']]['files'] \ |
842 | + .append(delta_abspath) |
843 | + |
844 | + def add_images_to_index(self): |
845 | + """ |
846 | + Adds all newly created image files to the index. |
847 | + This basically takes all the new entries from the self.new_images |
848 | + dictionary (full and delta entries) and adds them, calling |
849 | + create_image on the device object. |
850 | + """ |
851 | + |
852 | + # Add full image |
853 | + logging.info("Publishing new image '%s' (%s) with %s files." |
854 | + % (self.target_version, |
855 | + ",".join(self.environment['version_detail']), |
856 | + len(self.new_images['full']['files']))) |
857 | + self.device.create_image( |
858 | + "full", |
859 | + self.target_version, |
860 | + ",".join(self.environment['version_detail']), |
861 | + self.new_images['full']['files'], |
862 | + version_detail=",".join(self.environment['version_detail'])) |
863 | + |
864 | + # Add delta images |
865 | + for delta in self.delta_base: |
866 | + files = self.new_images['delta_%s' % delta['version']]['files'] |
867 | + logging.info("Publishing new delta from '%s' (%s)" |
868 | + " to '%s' (%s) with %s files" % |
869 | + (delta['version'], |
870 | + delta.get("description", ""), |
871 | + self.target_version, |
872 | + ",".join(self.environment['version_detail']), |
873 | + len(files))) |
874 | + |
875 | + self.device.create_image( |
876 | + "delta", self.target_version, |
877 | + ",".join(self.environment['version_detail']), |
878 | + files, |
879 | + base=delta['version'], |
880 | + version_detail=",".join(self.environment['version_detail'])) |
881 | |
882 | === added directory 'lib/systemimage/scripts' |
883 | === added file 'lib/systemimage/scripts/__init__.py' |
884 | === added file 'lib/systemimage/scripts/script_copy_image.py' |
885 | --- lib/systemimage/scripts/script_copy_image.py 1970-01-01 00:00:00 +0000 |
886 | +++ lib/systemimage/scripts/script_copy_image.py 2016-07-20 12:34:06 +0000 |
887 | @@ -0,0 +1,187 @@ |
888 | +# -*- coding: utf-8 -*- |
889 | + |
890 | +# Copyright (C) 2016 Canonical Ltd. |
891 | +# Authors: Łukasz 'sil2100' Zemczak <lukasz.zemczak@ubuntu.com> |
892 | +# Stéphane Graber <stgraber@ubuntu.com> |
893 | + |
894 | +# This program is free software: you can redistribute it and/or modify |
895 | +# it under the terms of the GNU General Public License as published by |
896 | +# the Free Software Foundation; version 3 of the License. |
897 | +# |
898 | +# This program is distributed in the hope that it will be useful, |
899 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
900 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
901 | +# GNU General Public License for more details. |
902 | +# |
903 | +# You should have received a copy of the GNU General Public License |
904 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
905 | + |
906 | +import json |
907 | +import os |
908 | +import sys |
909 | +import argparse |
910 | +import fcntl |
911 | +import logging |
912 | + |
913 | +from systemimage import config, generators, tools, tree, script |
914 | +from systemimage.script import ScriptError |
915 | + |
916 | + |
917 | +class CopyImageScript(script.CommonImageManipulationScript): |
918 | + """Script used to copy images from channel to channel.""" |
919 | + |
920 | + def setup(self, argv): |
921 | + parser = argparse.ArgumentParser(description="image copier") |
922 | + parser.add_argument("source_channel", metavar="SOURCE-CHANNEL") |
923 | + parser.add_argument("destination_channel", |
924 | + metavar="DESTINATION-CHANNEL") |
925 | + parser.add_argument("device", metavar="DEVICE") |
926 | + parser.add_argument("version", metavar="VERSION", type=int) |
927 | + parser.add_argument("-k", "--keep-version", action="store_true", |
928 | + help="Keep the original version number") |
929 | + parser.add_argument("-p", "--phased-percentage", type=int, |
930 | + help="Set the phased percentage for the copied " |
931 | + "image", |
932 | + default=100) |
933 | + parser.add_argument("-t", "--tag", type=str, |
934 | + help="Set a version tag on the new image") |
935 | + parser.add_argument("--verbose", "-v", action="count", default=0) |
936 | + |
937 | + argv.pop(0) # To fix issues with parse_args() shifting arguments |
938 | + args = parser.parse_args(argv) |
939 | + |
940 | + # Setup logging |
941 | + self.setup_logging(args.verbose) |
942 | + |
943 | + # Do some checks |
944 | + channels = self.pub.list_channels() |
945 | + if args.source_channel not in channels: |
946 | + raise ScriptError("Invalid source channel: %s" % |
947 | + args.source_channel) |
948 | + |
949 | + if args.destination_channel not in channels: |
950 | + raise ScriptError("Invalid destination channel: %s" % |
951 | + args.destination_channel) |
952 | + |
953 | + if args.device not in \ |
954 | + channels[args.source_channel]['devices']: |
955 | + raise ScriptError("Invalid device for source channel: %s" % |
956 | + args.device) |
957 | + |
958 | + if args.device not in \ |
959 | + channels[args.destination_channel]['devices']: |
960 | + raise ScriptError("Invalid device for destination channel: %s" % |
961 | + args.device) |
962 | + |
963 | + if "alias" in channels[args.source_channel] and \ |
964 | + channels[args.source_channel]['alias'] \ |
965 | + != args.source_channel: |
966 | + raise ScriptError("Source channel is an alias.") |
967 | + |
968 | + if "alias" in channels[args.destination_channel] and \ |
969 | + channels[args.destination_channel]['alias'] \ |
970 | + != args.destination_channel: |
971 | + raise ScriptError("Destination channel is an alias.") |
972 | + |
973 | + if "redirect" in channels[args.source_channel]: |
974 | + raise ScriptError("Source channel is a redirect.") |
975 | + |
976 | + if "redirect" in channels[args.destination_channel]: |
977 | + raise ScriptError("Destination channel is a redirect.") |
978 | + |
979 | + src_device = channels[args.source_channel]['devices'][args.device] |
980 | + if "redirect" in src_device: |
981 | + raise ScriptError("Source device is a redirect. Use original " |
982 | + "channel %s instead." % src_device['redirect']) |
983 | + |
984 | + dest_device = \ |
985 | + channels[args.destination_channel]['devices'][args.device] |
986 | + if "redirect" in dest_device: |
987 | + raise ScriptError("Destination device is a redirect. Use original " |
988 | + "channel %s instead." % dest_device['redirect']) |
989 | + |
990 | + if args.tag and "," in args.tag: |
991 | + raise ScriptError("Image tag cannot include the character ','.") |
992 | + |
993 | + self.args = args |
994 | + self.channel = args.destination_channel |
995 | + |
996 | + def assign_new_version(self, version, keep_version=False): |
997 | + if keep_version: |
998 | + images = [image for image in self.device.list_images() |
999 | + if image['version'] == version] |
1000 | + if images: |
1001 | + raise ScriptError( |
1002 | + "Version number is already used: %s" % version) |
1003 | + |
1004 | + # Assign a new version number |
1005 | + new_version = version |
1006 | + if not keep_version: |
1007 | + # Find the next available version |
1008 | + new_version = 1 |
1009 | + for image in self.device.list_images(): |
1010 | + if image['version'] >= new_version: |
1011 | + new_version = image['version'] + 1 |
1012 | + logging.debug("Version for next image: %s" % new_version) |
1013 | + |
1014 | + self.target_version = new_version |
1015 | + |
1016 | + def execute(self): |
1017 | + source_device = self.pub.get_device( |
1018 | + self.args.source_channel, self.args.device) |
1019 | + self.device = self.pub.get_device( |
1020 | + self.channel, self.args.device) |
1021 | + |
1022 | + # Assign a version number to the new image |
1023 | + self.assign_new_version(self.args.version, self.args.keep_version) |
1024 | + |
1025 | + # Extract the build we want to copy |
1026 | + source_image = tools.extract_image( |
1027 | + source_device.list_images(), self.args.version) |
1028 | + if not source_image: |
1029 | + raise ScriptError("Can't find version: %s" % version) |
1030 | + |
1031 | + # Extract the list of existing full images |
1032 | + full_images = {image['version']: image |
1033 | + for image in self.device.list_images() |
1034 | + if image['type'] == "full"} |
1035 | + |
1036 | + # Check that the last full and the new image aren't one and the same |
1037 | + source_files = [entry['path'].split("/")[-1] |
1038 | + for entry in source_image['files'] |
1039 | + if not entry['path'].split("/")[-1].startswith( |
1040 | + "version-")] |
1041 | + destination_files = [] |
1042 | + if full_images: |
1043 | + latest_full = sorted(full_images.values(), |
1044 | + key=lambda image: image['version'])[-1] |
1045 | + destination_files = [entry['path'].split("/")[-1] |
1046 | + for entry in latest_full['files'] |
1047 | + if not entry['path'].split( |
1048 | + "/")[-1].startswith("version-")] |
1049 | + if source_files == destination_files: |
1050 | + raise ScriptError("Source image is already latest full in " |
1051 | + "destination channel.") |
1052 | + |
1053 | + # Generate a list of required deltas |
1054 | + self.delta_base = tools.get_required_deltas( |
1055 | + self.conf, self.pub, self.channel, self.args.device) |
1056 | + |
1057 | + # Create new empty entries |
1058 | + self.new_images = {'full': {'files': []}} |
1059 | + |
1060 | + # Generate the version tarball |
1061 | + self.generate_version( |
1062 | + source_image['files'], self.args.version, self.args.tag) |
1063 | + |
1064 | + # Generate all required deltas |
1065 | + self.generate_deltas() |
1066 | + |
1067 | + # Add both full and delta images to the index |
1068 | + self.add_images_to_index() |
1069 | + |
1070 | + # Set the phased percentage value for the new image |
1071 | + logging.info("Setting phased percentage of the new image to %s%%" % |
1072 | + self.args.phased_percentage) |
1073 | + self.device.set_phased_percentage( |
1074 | + self.target_version, self.args.phased_percentage) |
1075 | |
1076 | === added file 'lib/systemimage/scripts/script_set_phased_percentage.py' |
1077 | --- lib/systemimage/scripts/script_set_phased_percentage.py 1970-01-01 00:00:00 +0000 |
1078 | +++ lib/systemimage/scripts/script_set_phased_percentage.py 2016-07-20 12:34:06 +0000 |
1079 | @@ -0,0 +1,83 @@ |
1080 | +# -*- coding: utf-8 -*- |
1081 | + |
1082 | +# Copyright (C) 2016 Canonical Ltd. |
1083 | +# Authors: Łukasz 'sil2100' Zemczak <lukasz.zemczak@ubuntu.com> |
1084 | +# Stéphane Graber <stgraber@ubuntu.com> |
1085 | + |
1086 | +# This program is free software: you can redistribute it and/or modify |
1087 | +# it under the terms of the GNU General Public License as published by |
1088 | +# the Free Software Foundation; version 3 of the License. |
1089 | +# |
1090 | +# This program is distributed in the hope that it will be useful, |
1091 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1092 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1093 | +# GNU General Public License for more details. |
1094 | +# |
1095 | +# You should have received a copy of the GNU General Public License |
1096 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1097 | + |
1098 | +import json |
1099 | +import os |
1100 | +import sys |
1101 | +import argparse |
1102 | +import fcntl |
1103 | +import logging |
1104 | + |
1105 | +from systemimage import config, generators, tools, tree, script |
1106 | +from systemimage.script import ScriptError |
1107 | + |
1108 | + |
1109 | +class SetPhasedPercentageScript(script.CommonImageManipulationScript): |
1110 | + """Script used to set phased percentages of images.""" |
1111 | + |
1112 | + def setup(self, argv): |
1113 | + parser = argparse.ArgumentParser(description="set phased percentage") |
1114 | + parser.add_argument("channel", metavar="CHANNEL") |
1115 | + parser.add_argument("device", metavar="DEVICE") |
1116 | + parser.add_argument("version", metavar="VERSION", type=int) |
1117 | + parser.add_argument("percentage", metavar="PERCENTAGE", type=int) |
1118 | + parser.add_argument("--verbose", "-v", action="count", default=0) |
1119 | + |
1120 | + argv.pop(0) # To fix issues with parse_args() shifting arguments |
1121 | + args = parser.parse_args(argv) |
1122 | + |
1123 | + # Setup logging |
1124 | + self.setup_logging(args.verbose) |
1125 | + |
1126 | + # Do some checks |
1127 | + channels = self.pub.list_channels() |
1128 | + if args.channel not in channels: |
1129 | + raise ScriptError("Invalid channel: %s" % args.channel) |
1130 | + |
1131 | + if args.device not in channels[args.channel]['devices']: |
1132 | + raise ScriptError("Invalid device for source channel: %s" % |
1133 | + args.device) |
1134 | + |
1135 | + if args.percentage < 0 or args.percentage > 100: |
1136 | + raise ScriptError("Invalid value: %s" % args.percentage) |
1137 | + |
1138 | + if "alias" in channels[args.channel] and \ |
1139 | + channels[args.channel]['alias'] != args.channel: |
1140 | + raise ScriptError("Channel is an alias.") |
1141 | + |
1142 | + if "redirect" in channels[args.channel]: |
1143 | + raise ScriptError("Channel is a redirect.") |
1144 | + |
1145 | + target_device = channels[args.channel]['devices'][args.device] |
1146 | + if "redirect" in target_device: |
1147 | + raise ScriptError( |
1148 | + "Target device is a redirect. Use original channel %s " |
1149 | + "instead." % target_device['redirect']) |
1150 | + |
1151 | + self.args = args |
1152 | + |
1153 | + def execute(self): |
1154 | + dev = self.pub.get_device(self.args.channel, self.args.device) |
1155 | + logging.info("Setting phased-percentage of '%s' to %s%%" % |
1156 | + (self.args.version, self.args.percentage)) |
1157 | + dev.set_phased_percentage(self.args.version, self.args.percentage) |
1158 | + |
1159 | + def cleanup(self): |
1160 | + # We're overwriting this method as there's no need to expire images |
1161 | + # after running this command |
1162 | + self.sync_trees() |
1163 | |
1164 | === added file 'lib/systemimage/scripts/script_tag_image.py' |
1165 | --- lib/systemimage/scripts/script_tag_image.py 1970-01-01 00:00:00 +0000 |
1166 | +++ lib/systemimage/scripts/script_tag_image.py 2016-07-20 12:34:06 +0000 |
1167 | @@ -0,0 +1,127 @@ |
1168 | +# -*- coding: utf-8 -*- |
1169 | + |
1170 | +# Copyright (C) 2016 Canonical Ltd. |
1171 | +# Authors: Łukasz 'sil2100' Zemczak <lukasz.zemczak@ubuntu.com> |
1172 | +# Stéphane Graber <stgraber@ubuntu.com> |
1173 | + |
1174 | +# This program is free software: you can redistribute it and/or modify |
1175 | +# it under the terms of the GNU General Public License as published by |
1176 | +# the Free Software Foundation; version 3 of the License. |
1177 | +# |
1178 | +# This program is distributed in the hope that it will be useful, |
1179 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1180 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1181 | +# GNU General Public License for more details. |
1182 | +# |
1183 | +# You should have received a copy of the GNU General Public License |
1184 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1185 | + |
1186 | +import json |
1187 | +import os |
1188 | +import sys |
1189 | +import argparse |
1190 | +import fcntl |
1191 | +import logging |
1192 | + |
1193 | +from operator import itemgetter |
1194 | +from systemimage import config, generators, tools, tree, script |
1195 | +from systemimage.script import ScriptError |
1196 | + |
1197 | + |
1198 | +class TagImageScript(script.CommonImageManipulationScript): |
1199 | + """Script used to append/modify tags in images.""" |
1200 | + |
1201 | + def setup(self, argv): |
1202 | + parser = argparse.ArgumentParser(description="image tagger") |
1203 | + parser.add_argument("channel", metavar="CHANNEL") |
1204 | + parser.add_argument("device", metavar="DEVICE") |
1205 | + parser.add_argument("version", metavar="VERSION", type=int) |
1206 | + parser.add_argument("tag", metavar="TAG") |
1207 | + parser.add_argument("--verbose", "-v", action="count", default=0) |
1208 | + |
1209 | + argv.pop(0) # To fix issues with parse_args() shifting arguments |
1210 | + args = parser.parse_args(argv) |
1211 | + |
1212 | + # Setup logging |
1213 | + self.setup_logging(args.verbose) |
1214 | + |
1215 | + # Do some checks |
1216 | + channels = self.pub.list_channels() |
1217 | + if args.channel not in channels: |
1218 | + raise ScriptError("Invalid channel: %s" % args.channel) |
1219 | + |
1220 | + if args.device not in \ |
1221 | + channels[args.channel]['devices']: |
1222 | + raise ScriptError("Invalid device for channel: %s" % args.device) |
1223 | + |
1224 | + if "alias" in channels[args.channel] and \ |
1225 | + channels[args.channel]['alias'] \ |
1226 | + != args.channel: |
1227 | + raise ScriptError("Channel is an alias.") |
1228 | + |
1229 | + if "redirect" in channels[args.channel]: |
1230 | + raise ScriptError("Channel is a redirect.") |
1231 | + |
1232 | + target_device = channels[args.channel]['devices'][args.device] |
1233 | + if "redirect" in target_device: |
1234 | + raise ScriptError( |
1235 | + "Target device is a redirect. Use original channel %s " |
1236 | + "instead." % target_device['redirect']) |
1237 | + |
1238 | + if "," in args.tag: |
1239 | + raise ScriptError("Image tag cannot include the character ','.") |
1240 | + |
1241 | + self.args = args |
1242 | + self.channel = args.channel |
1243 | + |
1244 | + def execute(self): |
1245 | + self.device = self.pub.get_device(self.args.channel, self.args.device) |
1246 | + |
1247 | + device_images = self.device.list_images() |
1248 | + if not device_images: |
1249 | + raise ScriptError("No images in selected channel/device.") |
1250 | + |
1251 | + sorted_images = sorted(device_images, key=itemgetter('version')) |
1252 | + if self.args.version != sorted_images[-1]['version']: |
1253 | + raise ScriptError("You can only tag the latest image.") |
1254 | + |
1255 | + # Extract the build we want to copy |
1256 | + self.image = tools.extract_image(device_images, self.args.version) |
1257 | + if not self.image: |
1258 | + raise ScriptError("Can't find version: %s" % version) |
1259 | + |
1260 | + # Remember the current image's phased percentage |
1261 | + percentage = self.device.get_phased_percentage(self.args.version) |
1262 | + |
1263 | + # Assign a new version number |
1264 | + self.target_version = self.args.version + 1 |
1265 | + |
1266 | + # Generate a list of required deltas |
1267 | + self.delta_base = tools.get_required_deltas( |
1268 | + self.conf, self.pub, self.channel, self.args.device) |
1269 | + |
1270 | + # Create new empty entries |
1271 | + self.new_images = {'full': {'files': []}} |
1272 | + |
1273 | + self.generate_version( |
1274 | + self.image['files'], self.args.version, self.args.tag) |
1275 | + |
1276 | + # Add delta images |
1277 | + delta_files = [] |
1278 | + for path in self.new_images['full']['files']: |
1279 | + filename = path.split("/")[-1] |
1280 | + if (filename.startswith("version-") or |
1281 | + filename.startswith("keyring-")): |
1282 | + delta_files.append(path) |
1283 | + |
1284 | + for delta in self.delta_base: |
1285 | + self.new_images['delta_%s' % delta['version']] = \ |
1286 | + {'files': delta_files} |
1287 | + |
1288 | + # Add both full and delta images to the index |
1289 | + self.add_images_to_index() |
1290 | + |
1291 | + # Set the phased percentage of the image to the previous value |
1292 | + logging.info("Setting phased percentage of the new image to %s%%" % |
1293 | + percentage) |
1294 | + self.device.set_phased_percentage(self.target_version, percentage) |
1295 | |
1296 | === added file 'lib/systemimage/tests/test_scripts.py' |
1297 | --- lib/systemimage/tests/test_scripts.py 1970-01-01 00:00:00 +0000 |
1298 | +++ lib/systemimage/tests/test_scripts.py 2016-07-20 12:34:06 +0000 |
1299 | @@ -0,0 +1,590 @@ |
1300 | +# -*- coding: utf-8 -*- |
1301 | + |
1302 | +# Copyright (C) 2015-2016 Canonical Ltd. |
1303 | +# Author: Łukasz 'sil2100' Zemczak <lukasz.zemczak@ubuntu.com> |
1304 | + |
1305 | +# This program is free software: you can redistribute it and/or modify |
1306 | +# it under the terms of the GNU General Public License as published by |
1307 | +# the Free Software Foundation; version 3 of the License. |
1308 | +# |
1309 | +# This program is distributed in the hope that it will be useful, |
1310 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1311 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1312 | +# GNU General Public License for more details. |
1313 | +# |
1314 | +# You should have received a copy of the GNU General Public License |
1315 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1316 | + |
1317 | +import os |
1318 | +import shutil |
1319 | +import stat |
1320 | +import subprocess |
1321 | +import tarfile |
1322 | +import tempfile |
1323 | +import unittest |
1324 | +import json |
1325 | +import six |
1326 | +import fcntl |
1327 | + |
1328 | +from mock import Mock, patch |
1329 | +from systemimage import config, tools, tree, gpg |
1330 | +from systemimage import script as script_base |
1331 | +from systemimage.scripts import ( |
1332 | + script_copy_image, |
1333 | + script_tag_image, |
1334 | + script_set_phased_percentage, |
1335 | + ) |
1336 | +from systemimage.testing.helpers import HAS_TEST_KEYS, MISSING_KEYS_WARNING |
1337 | +from systemimage.tools import xz_uncompress |
1338 | + |
1339 | + |
1340 | +class ScriptTests(unittest.TestCase): |
1341 | + def setUp(self): |
1342 | + temp_directory = tempfile.mkdtemp() |
1343 | + self.temp_directory = temp_directory |
1344 | + self.old_path = os.environ.get("PATH", None) |
1345 | + |
1346 | + os.mkdir(os.path.join(self.temp_directory, "etc")) |
1347 | + state_dir = os.path.join(self.temp_directory, "state") |
1348 | + os.mkdir(state_dir) |
1349 | + lock_file = os.path.join(state_dir, "global.lock") |
1350 | + open(lock_file, 'w+') |
1351 | + |
1352 | + def tearDown(self): |
1353 | + shutil.rmtree(self.temp_directory) |
1354 | + if self.old_path: |
1355 | + os.environ['PATH'] = self.old_path |
1356 | + |
1357 | + |
1358 | +class BaseScriptTests(ScriptTests): |
1359 | + def setUp(self): |
1360 | + super().setUp() |
1361 | + config_path = os.path.join(self.temp_directory, "etc", "config") |
1362 | + with open(config_path, "w+") as fd: |
1363 | + fd.write("""[global] |
1364 | +base_path = %s |
1365 | +gpg_key_path = %s |
1366 | +public_fqdn = system-image.example.net |
1367 | +public_http_port = 880 |
1368 | +public_https_port = 8443 |
1369 | +""" % (self.temp_directory, os.path.join(os.getcwd(), "tools", "keys"))) |
1370 | + self.config_path = config_path |
1371 | + |
1372 | + def test_lock(self): |
1373 | + """Make sure locking works.""" |
1374 | + script = script_base.Script(self.config_path) |
1375 | + script.lock() |
1376 | + lock_file = os.path.join(script.conf.state_path, "global.lock") |
1377 | + lock_fd = open(lock_file, 'r') |
1378 | + with self.assertRaises(IOError): |
1379 | + fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) |
1380 | + |
1381 | + def test_lock_already_locked(self): |
1382 | + """Make sure locking twice rises an error.""" |
1383 | + script = script_base.Script(self.config_path) |
1384 | + script2 = script_base.Script(self.config_path) |
1385 | + script.lock() |
1386 | + with self.assertRaises(script_base.ScriptError): |
1387 | + script2.lock() |
1388 | + |
1389 | + @patch('systemimage.tools.sync_mirrors') |
1390 | + def test_sync_trees(self, sync_mock): |
1391 | + """Check if calling sync_trees synces what we need.""" |
1392 | + script = script_base.Script(self.config_path) |
1393 | + script.pub = Mock() |
1394 | + script.sync_trees() |
1395 | + self.assertEqual(script.pub.sync_aliases.call_count, 1) |
1396 | + self.assertEqual(sync_mock.call_count, 1) |
1397 | + |
1398 | + @patch('systemimage.tools.sync_mirrors') |
1399 | + def test_cleanup(self, sync_mock): |
1400 | + """Check if cleanup performs proper steps.""" |
1401 | + script = script_base.Script(self.config_path) |
1402 | + script.pub = Mock() |
1403 | + script.cleanup() |
1404 | + self.assertEqual(script.pub.sync_aliases.call_count, 1) |
1405 | + self.assertEqual(script.pub.cleanup_tree.call_count, 1) |
1406 | + self.assertEqual(sync_mock.call_count, 1) |
1407 | + |
1408 | + def test_run(self): |
1409 | + """Check if all required class methods are called.""" |
1410 | + script = script_base.Script(self.config_path) |
1411 | + script.lock = Mock() |
1412 | + script.presetup = Mock() |
1413 | + script.setup = Mock() |
1414 | + script.execute = Mock() |
1415 | + script.cleanup = Mock() |
1416 | + script.run([]) |
1417 | + self.assertEqual(script.lock.call_count, 1) |
1418 | + self.assertEqual(script.presetup.call_count, 1) |
1419 | + self.assertEqual(script.setup.call_count, 1) |
1420 | + self.assertEqual(script.execute.call_count, 1) |
1421 | + self.assertEqual(script.cleanup.call_count, 1) |
1422 | + |
1423 | + def test_error_handled(self): |
1424 | + """Check if error on any stage causes the script to fail.""" |
1425 | + script = script_base.Script(self.config_path) |
1426 | + script.lock = Mock() |
1427 | + script.presetup = Mock() |
1428 | + script.setup = Mock() |
1429 | + script.execute = Mock() |
1430 | + script.cleanup = Mock() |
1431 | + |
1432 | + for name in ["lock", "presetup", "setup", "execute", "cleanup"]: |
1433 | + setattr(script, name, Mock( |
1434 | + side_effect=script_base.ScriptError('foo'))) |
1435 | + self.assertFalse(script.run([])) |
1436 | + setattr(script, name, Mock()) |
1437 | + |
1438 | + |
1439 | +class CommonScriptTests(ScriptTests): |
1440 | + def setUp(self): |
1441 | + super().setUp() |
1442 | + config_path = os.path.join(self.temp_directory, "etc", "config") |
1443 | + with open(config_path, "w+") as fd: |
1444 | + fd.write("""[global] |
1445 | +base_path = %s |
1446 | +gpg_key_path = %s |
1447 | +public_fqdn = system-image.example.net |
1448 | +public_http_port = 880 |
1449 | +public_https_port = 8443 |
1450 | +""" % (self.temp_directory, os.path.join(os.getcwd(), "tools", "keys"))) |
1451 | + self.config_path = config_path |
1452 | + |
1453 | + os.mkdir(os.path.join(self.temp_directory, "www")) |
1454 | + script = script_base.CommonImageManipulationScript(self.config_path) |
1455 | + script.presetup() |
1456 | + script.pub.create_channel("test") |
1457 | + script.pub.create_device("test", "test") |
1458 | + self.script = script |
1459 | + self.device = script.pub.get_device("test", "test") |
1460 | + |
1461 | + # An image helper file that can be used for contents |
1462 | + self.image_file = os.path.join( |
1463 | + self.script.conf.publish_path, "test_file") |
1464 | + open(self.image_file, "w+").close() |
1465 | + gpg.sign_file(self.script.conf, "image-signing", self.image_file) |
1466 | + |
1467 | + @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING) |
1468 | + @patch('systemimage.tools.extract_files_and_version') |
1469 | + def test_generate_version(self, extract_mock): |
1470 | + """Check if script parts for generating version tarballs work""" |
1471 | + |
1472 | + version = 1 |
1473 | + tag = "Test-1" |
1474 | + |
1475 | + # Emulate environment of the script run |
1476 | + self.script.target_version = version + 1 |
1477 | + self.script.channel = "ubuntu-test/test" |
1478 | + self.script.device = self.device |
1479 | + self.script.new_images = {'full': {'files': ["some-file.tar.xz"]}} |
1480 | + |
1481 | + self.script.generate_version([], version, tag) |
1482 | + |
1483 | + self.assertEqual( |
1484 | + self.script.new_images['full']['files'], |
1485 | + ["some-file.tar.xz", |
1486 | + os.path.realpath(os.path.join( |
1487 | + self.device.path, |
1488 | + "version-%s.tar.xz" % self.script.target_version))]) |
1489 | + |
1490 | + target_obj = None |
1491 | + xz_path = self.script.new_images['full']['files'][-1] |
1492 | + unxz_path = os.path.join(self.temp_directory, "temp-unpack.tar") |
1493 | + channel_path = "system/etc/system-image/channel.ini" |
1494 | + extracted_path = os.path.join(self.temp_directory, channel_path) |
1495 | + try: |
1496 | + xz_uncompress(xz_path, unxz_path) |
1497 | + target_obj = tarfile.open(unxz_path, "r") |
1498 | + target_obj.extract( |
1499 | + channel_path, self.temp_directory) |
1500 | + with open(extracted_path, "r") as f: |
1501 | + contents = f.read() |
1502 | + self.assertIn( |
1503 | + "tag=%s" % tag, contents) |
1504 | + self.assertIn( |
1505 | + "channel: %s" % self.script.channel, contents) |
1506 | + self.assertIn( |
1507 | + "build_number: %s" % self.script.target_version, contents) |
1508 | + finally: |
1509 | + target_obj.close() |
1510 | + os.remove(unxz_path) |
1511 | + os.remove(extracted_path) |
1512 | + |
1513 | + @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING) |
1514 | + @patch('systemimage.generators.generate_delta') |
1515 | + def test_generate_deltas(self, delta_mock): |
1516 | + """Check if we generate proper delta files.""" |
1517 | + |
1518 | + # Emulate environment of the script run |
1519 | + self.script.version = 2 |
1520 | + self.script.channel = "ubuntu-test/test" |
1521 | + self.script.device = self.device |
1522 | + self.script.new_images = {'full': {'files': [ |
1523 | + os.path.join(self.script.conf.publish_path, "file-source.tar.xz"), |
1524 | + os.path.join(self.script.conf.publish_path, "file2-source.tar.xz"), |
1525 | + os.path.join(self.script.conf.publish_path, |
1526 | + "keyring-source.tar.xz")]}} |
1527 | + self.script.delta_base = [ |
1528 | + {'version': 1, 'files': [{'path': "file-other.tar.xz"}]}, |
1529 | + {'version': 2, 'files': [{'path': "file-other2.tar.xz"}, |
1530 | + {'path': "file2-other3.tar.xz"}]}, |
1531 | + {'version': 3, 'files': [{'path': "file-other4.tar.xz"}, |
1532 | + {'path': "keyring-source.tar.xz"}, |
1533 | + {'path': "file2-other5.tar.xz"}]}, |
1534 | + {'version': 4, 'files': [{'path': "file-other6.tar.xz"}, |
1535 | + {'path': "keyring-other.tar.xz"}, |
1536 | + {'path': "file-source.tar.xz"}]}, ] |
1537 | + |
1538 | + expected_lengths = {1: 1, 2: 2, 3: 3, 4: 2} |
1539 | + delta_mock.return_value = "delta.tar.xz" |
1540 | + |
1541 | + self.script.generate_deltas() |
1542 | + |
1543 | + for delta in self.script.delta_base: |
1544 | + version = delta['version'] |
1545 | + self.assertIn('delta_%s' % version, self.script.new_images) |
1546 | + delta_files = self.script.new_images['delta_%s' % version]['files'] |
1547 | + self.assertEqual( |
1548 | + len(delta_files), |
1549 | + len(self.script.new_images['full']['files'])) |
1550 | + self.assertEqual( |
1551 | + len([x for x in delta_files if "delta" in x]), |
1552 | + expected_lengths[version]) |
1553 | + |
1554 | + @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING) |
1555 | + def test_add_images_to_index(self): |
1556 | + """Check if a full image upload is properly added to the index.""" |
1557 | + |
1558 | + self.script.target_version = 1 |
1559 | + self.script.environment = {'version_detail': [ |
1560 | + "ubuntu=1", "device=1", "tag=Foo"]} |
1561 | + self.script.new_images = {'full': {'files': [self.image_file]}} |
1562 | + self.script.delta_base = [] |
1563 | + self.script.device = self.device |
1564 | + |
1565 | + self.script.add_images_to_index() |
1566 | + image = tools.extract_image(self.script.device.list_images(), 1) |
1567 | + |
1568 | + self.assertIn('version', image) |
1569 | + self.assertEqual(image['version'], 1) |
1570 | + self.assertIn('version_detail', image) |
1571 | + self.assertEqual(image['version_detail'], "ubuntu=1,device=1,tag=Foo") |
1572 | + |
1573 | + @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING) |
1574 | + def test_add_images_to_index_with_delta(self): |
1575 | + """Check if a full + delta upload is properly added.""" |
1576 | + |
1577 | + self.script.target_version = 1 |
1578 | + self.script.environment = {'version_detail': [ |
1579 | + "ubuntu=1", "device=1", "tag=Foo"]} |
1580 | + self.script.new_images = { |
1581 | + 'full': {'files': [self.image_file]}, |
1582 | + 'delta_4': {'files': [self.image_file]}} |
1583 | + self.script.delta_base = [{ |
1584 | + 'version': 4, 'files': [{'path': "other-file.tar.xz"}]}] |
1585 | + self.script.device = self.device |
1586 | + |
1587 | + self.script.add_images_to_index() |
1588 | + images = self.script.device.list_images() |
1589 | + |
1590 | + self.assertEqual(len(images), 2) |
1591 | + |
1592 | + has_delta = False |
1593 | + has_full = False |
1594 | + for image in images: |
1595 | + self.assertIn('type', image) |
1596 | + if image['type'] == "delta": |
1597 | + has_delta = True |
1598 | + self.assertIn('base', image) |
1599 | + self.assertEqual(image['base'], 4) |
1600 | + elif image['type'] == "full": |
1601 | + has_full = True |
1602 | + self.assertIn('version', image) |
1603 | + self.assertEqual(image['version'], 1) |
1604 | + self.assertIn('version_detail', image) |
1605 | + self.assertEqual( |
1606 | + image['version_detail'], "ubuntu=1,device=1,tag=Foo") |
1607 | + |
1608 | + self.assertTrue(has_delta) |
1609 | + self.assertTrue(has_full) |
1610 | + |
1611 | + |
1612 | +class ImageScriptBaseTests(ScriptTests): |
1613 | + scriptClass = script_base.CommonImageManipulationScript |
1614 | + |
1615 | + def setUp(self): |
1616 | + super().setUp() |
1617 | + config_path = os.path.join(self.temp_directory, "etc", "config") |
1618 | + with open(config_path, "w+") as fd: |
1619 | + fd.write("""[global] |
1620 | +base_path = %s |
1621 | +gpg_key_path = %s |
1622 | +public_fqdn = system-image.example.net |
1623 | +public_http_port = 880 |
1624 | +public_https_port = 8443 |
1625 | +""" % (self.temp_directory, os.path.join(os.getcwd(), "tools", "keys"))) |
1626 | + self.config_path = config_path |
1627 | + |
1628 | + os.mkdir(os.path.join(self.temp_directory, "www")) |
1629 | + script = self.scriptClass(self.config_path) |
1630 | + script.presetup() |
1631 | + script.pub.create_channel("empty") |
1632 | + script.pub.create_channel("from") |
1633 | + script.pub.create_channel("to") |
1634 | + script.pub.create_device("empty", "test") |
1635 | + script.pub.create_device("from", "test") |
1636 | + script.pub.create_device("to", "test") |
1637 | + |
1638 | + self.image_file = os.path.join(script.conf.publish_path, "test_file") |
1639 | + open(self.image_file, "w+").close() |
1640 | + gpg.sign_file(script.conf, "image-signing", self.image_file) |
1641 | + |
1642 | + device = script.pub.get_device("from", "test") |
1643 | + device.create_image("full", 1, "abc", [self.image_file]) |
1644 | + device.create_image("full", 2, "def", [self.image_file]) |
1645 | + device = script.pub.get_device("to", "test") |
1646 | + device.create_image("full", 1, "abc", [self.image_file]) |
1647 | + |
1648 | + self.script = script |
1649 | + |
1650 | + |
1651 | +class CopyImageScriptTests(ImageScriptBaseTests): |
1652 | + scriptClass = script_copy_image.CopyImageScript |
1653 | + |
1654 | + def test_copy_setup_correct(self): |
1655 | + """Check if setup doesn't fail with good arguments.""" |
1656 | + argv = ["copy-image", "from", "to", "test", "1"] |
1657 | + self.script.setup(argv) |
1658 | + |
1659 | + def test_copy_setup_invalid_channel(self): |
1660 | + """Check if setup fails on invalid channel arguments.""" |
1661 | + argv = ["copy-image", "none", "to", "test", "1"] |
1662 | + with self.assertRaises(script_base.ScriptError): |
1663 | + self.script.setup(argv) |
1664 | + argv = ["copy-image", "from", "none", "test", "1"] |
1665 | + with self.assertRaises(script_base.ScriptError): |
1666 | + self.script.setup(argv) |
1667 | + |
1668 | + self.script.pub.create_channel_alias("alias1", "from") |
1669 | + argv = ["copy-image", "alias1", "to", "test", "1"] |
1670 | + with self.assertRaises(script_base.ScriptError): |
1671 | + self.script.setup(argv) |
1672 | + self.script.pub.create_channel_alias("alias2", "to") |
1673 | + argv = ["copy-image", "from", "alias2", "test", "1"] |
1674 | + with self.assertRaises(script_base.ScriptError): |
1675 | + self.script.setup(argv) |
1676 | + |
1677 | + self.script.pub.create_channel_redirect("re1", "from") |
1678 | + argv = ["copy-image", "re1", "to", "test", "1"] |
1679 | + with self.assertRaises(script_base.ScriptError): |
1680 | + self.script.setup(argv) |
1681 | + self.script.pub.create_channel_alias("re2", "to") |
1682 | + argv = ["copy-image", "from", "re2", "test", "1"] |
1683 | + with self.assertRaises(script_base.ScriptError): |
1684 | + self.script.setup(argv) |
1685 | + |
1686 | + self.script.pub.create_channel("redirects") |
1687 | + self.script.pub.create_per_device_channel_redirect( |
1688 | + "test", "redirects", "empty") |
1689 | + argv = ["copy-image", "redirects", "to", "test", "1"] |
1690 | + with self.assertRaises(script_base.ScriptError): |
1691 | + self.script.setup(argv) |
1692 | + |
1693 | + argv = ["copy-image", "from", "redirects", "test", "1"] |
1694 | + with self.assertRaises(script_base.ScriptError): |
1695 | + self.script.setup(argv) |
1696 | + |
1697 | + def test_copy_setup_invalid_tag(self): |
1698 | + """Check if setup fails on invalid tag arguments.""" |
1699 | + argv = ["copy-image", "from", "to", "test", "1", "--tag=\"tag,tag\""] |
1700 | + with self.assertRaises(script_base.ScriptError): |
1701 | + self.script.setup(argv) |
1702 | + |
1703 | + def test_copy_setup_invalid_device(self): |
1704 | + """Check if setup fails on invalid device selection.""" |
1705 | + argv = ["copy-image", "from", "to", "none", "1"] |
1706 | + with self.assertRaises(script_base.ScriptError): |
1707 | + self.script.setup(argv) |
1708 | + |
1709 | + self.script.pub.create_device("to", "to_only") |
1710 | + argv = ["copy-image", "from", "to", "to_only", "1"] |
1711 | + with self.assertRaises(script_base.ScriptError): |
1712 | + self.script.setup(argv) |
1713 | + |
1714 | + self.script.pub.create_device("from", "from_only") |
1715 | + device = self.script.pub.get_device("from", "from_only") |
1716 | + device.create_image("full", 1, "abc", [self.image_file]) |
1717 | + argv = ["copy-image", "from", "to", "from_only", "1"] |
1718 | + with self.assertRaises(script_base.ScriptError): |
1719 | + self.script.setup(argv) |
1720 | + |
1721 | + def test_assign_new_version(self): |
1722 | + """Check if new version assignment logic is correct.""" |
1723 | + self.script.device = self.script.pub.get_device("to", "test") |
1724 | + |
1725 | + self.script.assign_new_version(1, False) |
1726 | + self.assertEqual(self.script.target_version, 2) |
1727 | + self.script.assign_new_version(4, False) |
1728 | + self.assertEqual(self.script.target_version, 2) |
1729 | + self.script.assign_new_version(4, True) |
1730 | + self.assertEqual(self.script.target_version, 4) |
1731 | + with self.assertRaises(script_base.ScriptError): |
1732 | + self.script.assign_new_version(1, True) |
1733 | + |
1734 | + |
1735 | +class TagImageScriptTests(ImageScriptBaseTests): |
1736 | + scriptClass = script_tag_image.TagImageScript |
1737 | + |
1738 | + def test_tag_setup_correct(self): |
1739 | + """Check if setup doesn't fail with good arguments.""" |
1740 | + argv = ["tag-image", "to", "test", "1", "TAG1"] |
1741 | + self.script.setup(argv) |
1742 | + |
1743 | + def test_tag_setup_invalid_channel(self): |
1744 | + """Check if setup fails on invalid channel arguments.""" |
1745 | + argv = ["tag-image", "none", "test", "1", "TAG1"] |
1746 | + with self.assertRaises(script_base.ScriptError): |
1747 | + self.script.setup(argv) |
1748 | + |
1749 | + self.script.pub.create_channel_alias("alias", "from") |
1750 | + argv = ["tag-image", "alias", "test", "1", "TAG1"] |
1751 | + with self.assertRaises(script_base.ScriptError): |
1752 | + self.script.setup(argv) |
1753 | + |
1754 | + self.script.pub.create_channel_redirect("re", "from") |
1755 | + argv = ["tag-image", "re", "test", "1", "TAG1"] |
1756 | + with self.assertRaises(script_base.ScriptError): |
1757 | + self.script.setup(argv) |
1758 | + |
1759 | + self.script.pub.create_channel("redirects") |
1760 | + self.script.pub.create_per_device_channel_redirect( |
1761 | + "test", "redirects", "empty") |
1762 | + argv = ["tag-image", "redirects", "test", "1", "TAG1"] |
1763 | + with self.assertRaises(script_base.ScriptError): |
1764 | + self.script.setup(argv) |
1765 | + |
1766 | + def test_tag_setup_invalid_tag(self): |
1767 | + """Check if setup fails on invalid tag arguments.""" |
1768 | + argv = ["tag-image", "to", "test", "1", "TAG1,TAG2"] |
1769 | + with self.assertRaises(script_base.ScriptError): |
1770 | + self.script.setup(argv) |
1771 | + |
1772 | + def test_tag_setup_invalid_device(self): |
1773 | + """Check if setup fails on invalid device selection.""" |
1774 | + argv = ["tag-image", "to", "none", "1", "TAG1"] |
1775 | + with self.assertRaises(script_base.ScriptError): |
1776 | + self.script.setup(argv) |
1777 | + |
1778 | + def test_tag_execute_invalid_image(self): |
1779 | + """Make sure execute fails on invalid image scenarios.""" |
1780 | + argv = ["tag-image", "empty", "test", "1", "TAG1"] |
1781 | + self.script.setup(argv) |
1782 | + with self.assertRaises(script_base.ScriptError): |
1783 | + self.script.execute() |
1784 | + |
1785 | + argv = ["tag-image", "from", "test", "1", "TAG1"] |
1786 | + self.script.setup(argv) |
1787 | + with self.assertRaises(script_base.ScriptError): |
1788 | + self.script.execute() |
1789 | + |
1790 | + argv = ["tag-image", "from", "test", "3", "TAG1"] |
1791 | + self.script.setup(argv) |
1792 | + with self.assertRaises(script_base.ScriptError): |
1793 | + self.script.execute() |
1794 | + |
1795 | + def test_tag_execute(self): |
1796 | + """Check that we can correctly tag an image.""" |
1797 | + device = self.script.pub.get_device("from", "test") |
1798 | + device.set_phased_percentage(2, 60) |
1799 | + |
1800 | + argv = ["tag-image", "from", "test", "2", "TAG1"] |
1801 | + self.script.setup(argv) |
1802 | + self.script.execute() |
1803 | + |
1804 | + image = tools.extract_image(self.script.device.list_images(), 3) |
1805 | + self.assertTrue(image) |
1806 | + self.assertIn("version_detail", image) |
1807 | + self.assertIn("tag=TAG1", image['version_detail']) |
1808 | + self.assertEqual(device.get_phased_percentage(3), 60) |
1809 | + |
1810 | + previous = tools.extract_image(self.script.device.list_images(), 2) |
1811 | + self.assertIn("files", image) |
1812 | + for f in previous['files']: |
1813 | + self.assertIn("path", f) |
1814 | + if "version-" not in f['path']: |
1815 | + self.assertIn(f, image['files']) |
1816 | + |
1817 | + |
1818 | +class SetPhasedPercentageScriptTests(ImageScriptBaseTests): |
1819 | + scriptClass = script_set_phased_percentage.SetPhasedPercentageScript |
1820 | + |
1821 | + def test_phased_setup_correct(self): |
1822 | + """Check if setup doesn't fail with good arguments.""" |
1823 | + argv = ["set-phased-percentage", "to", "test", "1", "50"] |
1824 | + self.script.setup(argv) |
1825 | + |
1826 | + def test_phased_setup_invalid_percentage(self): |
1827 | + """Check if setup fails with an invalid phase percentage value.""" |
1828 | + argv = ["set-phased-percentage", "to", "test", "1", "foo"] |
1829 | + with self.assertRaises(SystemExit): |
1830 | + self.script.setup(argv) |
1831 | + |
1832 | + argv = ["set-phased-percentage", "to", "test", "1", "9000"] |
1833 | + with self.assertRaises(script_base.ScriptError): |
1834 | + self.script.setup(argv) |
1835 | + |
1836 | + argv = ["set-phased-percentage", "to", "test", "1", "-10"] |
1837 | + with self.assertRaises(Exception): |
1838 | + self.script.setup(argv) |
1839 | + |
1840 | + def test_phased_setup_invalid_channel(self): |
1841 | + """Check if setup fails on invalid channel arguments.""" |
1842 | + argv = ["set-phased-percentage", "none", "test", "1", "50"] |
1843 | + with self.assertRaises(script_base.ScriptError): |
1844 | + self.script.setup(argv) |
1845 | + |
1846 | + self.script.pub.create_channel_alias("alias", "from") |
1847 | + argv = ["set-phased-percentage", "alias", "test", "1", "50"] |
1848 | + with self.assertRaises(script_base.ScriptError): |
1849 | + self.script.setup(argv) |
1850 | + |
1851 | + self.script.pub.create_channel_redirect("re", "from") |
1852 | + argv = ["set-phased-percentage", "re", "test", "1", "50"] |
1853 | + with self.assertRaises(script_base.ScriptError): |
1854 | + self.script.setup(argv) |
1855 | + |
1856 | + self.script.pub.create_channel("redirects") |
1857 | + self.script.pub.create_per_device_channel_redirect( |
1858 | + "test", "redirects", "empty") |
1859 | + argv = ["set-phased-percentage", "redirects", "test", "1", "50"] |
1860 | + with self.assertRaises(script_base.ScriptError): |
1861 | + self.script.setup(argv) |
1862 | + |
1863 | + def test_phased_execute(self): |
1864 | + """Check if we can change the percentage forwards and backwards.""" |
1865 | + device = self.script.pub.get_device("to", "test") |
1866 | + |
1867 | + argv = ["set-phased-percentage", "to", "test", "1", "50"] |
1868 | + self.script.setup(argv) |
1869 | + self.script.execute() |
1870 | + self.assertEqual(device.get_phased_percentage(1), 50) |
1871 | + |
1872 | + argv = ["set-phased-percentage", "to", "test", "1", "60"] |
1873 | + self.script.setup(argv) |
1874 | + self.script.execute() |
1875 | + self.assertEqual(device.get_phased_percentage(1), 60) |
1876 | + |
1877 | + argv = ["set-phased-percentage", "to", "test", "1", "15"] |
1878 | + self.script.setup(argv) |
1879 | + self.script.execute() |
1880 | + self.assertEqual(device.get_phased_percentage(1), 15) |
1881 | + |
1882 | + def test_phased_execute_invalid_image(self): |
1883 | + """Check if we fail on trying to phase an older image.""" |
1884 | + device = self.script.pub.get_device("to", "test") |
1885 | + |
1886 | + argv = ["set-phased-percentage", "from", "test", "1", "50"] |
1887 | + with self.assertRaises(Exception): |
1888 | + self.script.setup(argv) |
1889 | + self.script.execute() |
1890 | |
1891 | === modified file 'lib/systemimage/tests/test_tools.py' |
1892 | --- lib/systemimage/tests/test_tools.py 2016-03-16 18:52:39 +0000 |
1893 | +++ lib/systemimage/tests/test_tools.py 2016-07-20 12:34:06 +0000 |
1894 | @@ -432,6 +432,7 @@ |
1895 | |
1896 | def test_set_tag_on_version_detail(self): |
1897 | """Set a basic tag.""" |
1898 | + |
1899 | version_detail_list = [ |
1900 | "device=20150821-736d127", |
1901 | "custom=20150925-901-35-40-vivid", |
1902 | @@ -444,6 +445,7 @@ |
1903 | |
1904 | def test_set_tag_on_version_detail_rewrite(self): |
1905 | """Make sure tags can be rewritten.""" |
1906 | + |
1907 | version_detail_list = [ |
1908 | "device=20150821-736d127", |
1909 | "custom=20150925-901-35-40-vivid", |
1910 | @@ -458,6 +460,7 @@ |
1911 | |
1912 | def test_set_tag_on_version_detail_clear(self): |
1913 | """Clear the tag.""" |
1914 | + |
1915 | version_detail_list = [ |
1916 | "device=20150821-736d127", |
1917 | "custom=20150925-901-35-40-vivid", |
1918 | @@ -470,6 +473,7 @@ |
1919 | |
1920 | def test_extract_files_and_version(self): |
1921 | """Check if version_detail is correctly extracted""" |
1922 | + |
1923 | os.mkdir(self.config.publish_path) |
1924 | |
1925 | version = 12 |
1926 | @@ -501,6 +505,7 @@ |
1927 | @unittest.skip("Current deltabase handling is broken") |
1928 | def test_get_required_deltas(self): |
1929 | """Check if a proper list of valid deltabases is found.""" |
1930 | + |
1931 | config_path = os.path.join(self.temp_directory, "etc", "config") |
1932 | with open(config_path, "w+") as fd: |
1933 | fd.write("""[global] |
1934 | @@ -550,6 +555,42 @@ |
1935 | six.assertCountEqual( |
1936 | self, [base_image1, base_image2], delta_base) |
1937 | |
1938 | + def test_extract_image(self): |
1939 | + """Extracting single images from devices.""" |
1940 | + |
1941 | + config_path = os.path.join(self.temp_directory, "etc", "config") |
1942 | + with open(config_path, "w+") as fd: |
1943 | + fd.write("""[global] |
1944 | +base_path = %s |
1945 | +gpg_key_path = %s |
1946 | +public_fqdn = system-image.example.net |
1947 | +public_http_port = 880 |
1948 | +public_https_port = 8443 |
1949 | +""" % (self.temp_directory, os.path.join(os.getcwd(), "tools", "keys"))) |
1950 | + test_config = config.Config(config_path) |
1951 | + os.makedirs(test_config.publish_path) |
1952 | + |
1953 | + test_tree = tree.Tree(test_config) |
1954 | + test_tree.create_channel("testing") |
1955 | + test_tree.create_device("testing", "test") |
1956 | + |
1957 | + image_file = os.path.join(self.config.publish_path, "test_file") |
1958 | + open(image_file, "w+").close() |
1959 | + gpg.sign_file(test_config, "image-signing", image_file) |
1960 | + |
1961 | + device = test_tree.get_device("testing", "test") |
1962 | + device.create_image("full", 1, "abc", [image_file]) |
1963 | + device.create_image("delta", 2, "abc", [image_file], base=1) |
1964 | + device.create_image("full", 4, "abc", [image_file]) |
1965 | + |
1966 | + image = tools.extract_image(device.list_images(), 1) |
1967 | + self.assertIsNotNone(image) |
1968 | + self.assertEqual(image['version'], 1) |
1969 | + self.assertIsNone(tools.extract_image(device.list_images(), 2)) |
1970 | + image = tools.extract_image(device.list_images(), 4) |
1971 | + self.assertIsNotNone(image) |
1972 | + self.assertEqual(image['version'], 4) |
1973 | + |
1974 | def test_guess_file_compression(self): |
1975 | """Check if we can correctly guess compression algorithms.""" |
1976 | test_string = "test-string" |
1977 | |
1978 | === modified file 'lib/systemimage/tests/test_tree.py' |
1979 | --- lib/systemimage/tests/test_tree.py 2016-06-24 19:10:46 +0000 |
1980 | +++ lib/systemimage/tests/test_tree.py 2016-07-20 12:34:06 +0000 |
1981 | @@ -648,7 +648,7 @@ |
1982 | |
1983 | @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING) |
1984 | def test_index(self): |
1985 | - device = tree.Device(self.config, self.temp_directory) |
1986 | + device = tree.Device("test", self.config, self.temp_directory) |
1987 | |
1988 | # Check without files |
1989 | self.assertRaises(Exception, device.create_image, "full", 1234, |
1990 | |
1991 | === modified file 'lib/systemimage/tools.py' |
1992 | --- lib/systemimage/tools.py 2016-03-16 18:52:39 +0000 |
1993 | +++ lib/systemimage/tools.py 2016-07-20 12:34:06 +0000 |
1994 | @@ -603,6 +603,21 @@ |
1995 | return version_detail |
1996 | |
1997 | |
1998 | +def extract_image(images, version): |
1999 | + """ |
2000 | + Extracts the image file from an device image list for the selected |
2001 | + version number. |
2002 | + """ |
2003 | + |
2004 | + images = [image for image in images |
2005 | + if image['type'] == "full" and |
2006 | + image['version'] == version] |
2007 | + if not images: |
2008 | + return None |
2009 | + |
2010 | + return images[0] |
2011 | + |
2012 | + |
2013 | def set_tag_on_version_detail(version_detail_list, tag): |
2014 | """ |
2015 | Append a tag to the version_detail array. |
2016 | |
2017 | === modified file 'lib/systemimage/tree.py' |
2018 | --- lib/systemimage/tree.py 2016-06-24 19:10:46 +0000 |
2019 | +++ lib/systemimage/tree.py 2016-07-20 12:34:06 +0000 |
2020 | @@ -429,8 +429,8 @@ |
2021 | device_path = os.path.dirname(channels[channel_name]['devices'] |
2022 | [device_name]['index']) |
2023 | |
2024 | - return Device(self.config, os.path.normpath("%s/%s" % (self.path, |
2025 | - device_path))) |
2026 | + return Device(device_name, self.config, |
2027 | + os.path.normpath("%s/%s" % (self.path, device_path))) |
2028 | |
2029 | def hide_channel(self, channel_name): |
2030 | """ |
2031 | @@ -874,7 +874,8 @@ |
2032 | |
2033 | |
2034 | class Device: |
2035 | - def __init__(self, config, path): |
2036 | + def __init__(self, name, config, path): |
2037 | + self.name = name |
2038 | self.config = config |
2039 | self.pub_path = self.config.publish_path |
2040 | self.path = path |
I wouldn't land this right now even without writing more tests, but I'd like to gather some feedback in the meantime when I'm busy with other duties.