Merge lp:~sil2100/ubuntu-system-image/server_script_testability into lp:ubuntu-system-image/server

Proposed by Łukasz Zemczak
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
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.

To post a comment you must log in.
Revision history for this message
Łukasz Zemczak (sil2100) wrote :

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.

Revision history for this message
Barry Warsaw (barry) wrote :

In general I really like the direction you're going in with this. More tests == better!

review: Approve
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

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

Subscribers

People subscribed via source and target branches