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
=== modified file 'bin/copy-image' (properties changed: +x to -x)
--- bin/copy-image 2016-06-24 21:02:18 +0000
+++ bin/copy-image 2016-07-20 12:34:06 +0000
@@ -1,8 +1,8 @@
1#!/usr/bin/python1#!/usr/bin/python
2# -*- coding: utf-8 -*-2# -*- coding: utf-8 -*-
33
4# Copyright (C) 2013 Canonical Ltd.4# Copyright (C) 2015-2016 Canonical Ltd.
5# Author: Stéphane Graber <stgraber@ubuntu.com>5# Author: Łukasz 'sil2100' Zemczak <lukasz.zemczak@ubuntu.com>
66
7# This program is free software: you can redistribute it and/or modify7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by8# it under the terms of the GNU General Public License as published by
@@ -16,284 +16,18 @@
16# You should have received a copy of the GNU General Public License16# 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/>.17# along with this program. If not, see <http://www.gnu.org/licenses/>.
1818
19import json
20import os19import os
21import sys20import sys
21
22sys.path.insert(0, os.path.join(sys.path[0], os.pardir, "lib"))22sys.path.insert(0, os.path.join(sys.path[0], os.pardir, "lib"))
2323
24from systemimage import config, generators, tools, tree24from systemimage.scripts import script_copy_image
2525
26import argparse26
27import fcntl27def main():
28import logging28 script = script_copy_image.CopyImageScript()
29 script.run(sys.argv)
30
2931
30if __name__ == '__main__':32if __name__ == '__main__':
31 parser = argparse.ArgumentParser(description="image copier")33 main()
32 parser.add_argument("source_channel", metavar="SOURCE-CHANNEL")
33 parser.add_argument("destination_channel", metavar="DESTINATION-CHANNEL")
34 parser.add_argument("device", metavar="DEVICE")
35 parser.add_argument("version", metavar="VERSION", type=int)
36 parser.add_argument("-k", "--keep-version", action="store_true",
37 help="Keep the original version number")
38 parser.add_argument("-p", "--phased-percentage", type=int,
39 help="Set the phased percentage for the copied image",
40 default=100)
41 parser.add_argument("-t", "--tag", type=str,
42 help="Set a version tag on the new image")
43 parser.add_argument("--verbose", "-v", action="count", default=0)
44
45 args = parser.parse_args()
46
47 # Setup logging
48 formatter = logging.Formatter(
49 "%(asctime)s %(levelname)s %(message)s")
50
51 levels = {1: logging.ERROR,
52 2: logging.WARNING,
53 3: logging.INFO,
54 4: logging.DEBUG}
55
56 if args.verbose > 0:
57 stdoutlogger = logging.StreamHandler(sys.stdout)
58 stdoutlogger.setFormatter(formatter)
59 logging.root.setLevel(levels[min(4, args.verbose)])
60 logging.root.addHandler(stdoutlogger)
61 else:
62 logging.root.addHandler(logging.NullHandler())
63
64 # Load the configuration
65 conf = config.Config()
66
67 # Try to acquire a global lock
68 lock_file = os.path.join(conf.state_path, "global.lock")
69 lock_fd = open(lock_file, 'w')
70
71 try:
72 fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
73 except IOError:
74 print("Something else holds the global lock. exiting.")
75 sys.exit(0)
76
77 # Load the tree
78 pub = tree.Tree(conf)
79
80 # Do some checks
81 channels = pub.list_channels()
82 if args.source_channel not in channels:
83 parser.error("Invalid source channel: %s" % args.source_channel)
84
85 if args.destination_channel not in channels:
86 parser.error("Invalid destination channel: %s" %
87 args.destination_channel)
88
89 if args.device not in channels[args.source_channel]['devices']:
90 parser.error("Invalid device for source channel: %s" %
91 args.device)
92
93 if args.device not in \
94 channels[args.destination_channel]['devices']:
95 parser.error("Invalid device for destination channel: %s" %
96 args.device)
97
98 if "alias" in channels[args.source_channel] and \
99 channels[args.source_channel]['alias'] \
100 != args.source_channel:
101 parser.error("Source channel is an alias.")
102
103 if "alias" in channels[args.destination_channel] and \
104 channels[args.destination_channel]['alias'] \
105 != args.destination_channel:
106 parser.error("Destination channel is an alias.")
107
108 if "redirect" in channels[args.source_channel]:
109 parser.error("Source channel is a redirect.")
110
111 if "redirect" in channels[args.destination_channel]:
112 parser.error("Destination channel is a redirect.")
113
114 src_device = channels[args.source_channel]['devices'][args.device]
115 if "redirect" in src_device:
116 parser.error("Source device is a redirect. Use original channel "
117 "%s instead." % src_device['redirect'])
118
119 dest_device = channels[args.destination_channel]['devices'][args.device]
120 if "redirect" in dest_device:
121 parser.error("Destination device is a redirect. Use original channel "
122 "%s instead." % dest_device['redirect'])
123
124 if args.tag and "," in args.tag:
125 parser.error("Image tag cannot include the character ','.")
126
127 source_device = pub.get_device(args.source_channel, args.device)
128 destination_device = pub.get_device(args.destination_channel, args.device)
129
130 if args.keep_version:
131 images = [image for image in destination_device.list_images()
132 if image['version'] == args.version]
133 if images:
134 parser.error("Version number is already used: %s" % args.version)
135
136 # Assign a new version number
137 new_version = args.version
138 if not args.keep_version:
139 # Find the next available version
140 new_version = 1
141 for image in destination_device.list_images():
142 if image['version'] >= new_version:
143 new_version = image['version'] + 1
144 logging.debug("Version for next image: %s" % new_version)
145
146 # Extract the build we want to copy
147 images = [image for image in source_device.list_images()
148 if image['type'] == "full" and image['version'] == args.version]
149 if not images:
150 parser.error("Can't find version: %s" % args.version)
151 source_image = images[0]
152
153 # Extract the list of existing full images
154 full_images = {image['version']: image
155 for image in destination_device.list_images()
156 if image['type'] == "full"}
157
158 # Check that the last full and the new image aren't one and the same
159 source_files = [entry['path'].split("/")[-1]
160 for entry in source_image['files']
161 if not entry['path'].split("/")[-1].startswith("version-")]
162 destination_files = []
163 if full_images:
164 latest_full = sorted(full_images.values(),
165 key=lambda image: image['version'])[-1]
166 destination_files = [entry['path'].split("/")[-1]
167 for entry in latest_full['files']
168 if not entry['path'].split(
169 "/")[-1].startswith("version-")]
170 if source_files == destination_files:
171 parser.error("Source image is already latest full in "
172 "destination channel.")
173
174 # Generate a list of required deltas
175 delta_base = tools.get_required_deltas(
176 conf, pub, args.destination_channel, args.device)
177
178 # Create new empty entries
179 new_images = {'full': {'files': []}}
180 for delta in delta_base:
181 new_images['delta_%s' % delta['version']] = {'files': []}
182
183 # Extract current version_detail and files
184 version_detail = tools.extract_files_and_version(
185 conf, source_image['files'], args.version, new_images['full']['files'])
186
187 # Generate new version tarball
188 environment = {}
189 environment['channel_name'] = args.destination_channel
190 environment['device'] = destination_device
191 environment['device_name'] = args.device
192 environment['version'] = new_version
193 environment['version_detail'] = [entry
194 for entry in version_detail.split(",")
195 if not entry.startswith("version=")]
196 environment['new_files'] = new_images['full']['files']
197
198 # Add new tag if requested
199 if args.tag:
200 tools.set_tag_on_version_detail(
201 environment['version_detail'], args.tag)
202 logging.info("Setting tag for image to '%s'" % args.tag)
203
204 logging.info("Generating new version tarball for '%s' (%s)"
205 % (new_version, ",".join(environment['version_detail'])))
206 version_path = generators.generate_file(conf, "version", [], environment)
207 if version_path:
208 new_images['full']['files'].append(version_path)
209
210 # Generate deltas
211 for abspath in new_images['full']['files']:
212 prefix = abspath.split("/")[-1].rsplit("-", 1)[0]
213 for delta in delta_base:
214 # Extract the source
215 src_path = None
216 for file_dict in delta['files']:
217 if (file_dict['path'].split("/")[-1]
218 .startswith(prefix)):
219 src_path = "%s/%s" % (conf.publish_path,
220 file_dict['path'])
221 break
222
223 # Check that it's not the current file
224 if src_path:
225 src_path = os.path.realpath(src_path)
226
227 # FIXME: the keyring- is a big hack...
228 if src_path == abspath and "keyring-" not in src_path:
229 continue
230
231 # Generators are allowed to return None when no delta
232 # exists at all.
233 logging.info("Generating delta from '%s' for '%s'" %
234 (delta['version'],
235 prefix))
236 delta_path = generators.generate_delta(conf, src_path,
237 abspath)
238 else:
239 delta_path = abspath
240
241 if not delta_path:
242 continue
243
244 # Get the full and relative paths
245 delta_abspath, delta_relpath = tools.expand_path(
246 delta_path, conf.publish_path)
247
248 new_images['delta_%s' % delta['version']]['files'] \
249 .append(delta_abspath)
250
251 # Add full image
252 logging.info("Publishing new image '%s' (%s) with %s files."
253 % (new_version, ",".join(environment['version_detail']),
254 len(new_images['full']['files'])))
255 destination_device.create_image(
256 "full", new_version, ",".join(environment['version_detail']),
257 new_images['full']['files'],
258 version_detail=",".join(environment['version_detail']))
259
260 # Add delta images
261 for delta in delta_base:
262 files = new_images['delta_%s' % delta['version']]['files']
263 logging.info("Publishing new delta from '%s' (%s)"
264 " to '%s' (%s) with %s files" %
265 (delta['version'], delta.get("description", ""),
266 new_version, ",".join(environment['version_detail']),
267 len(files)))
268
269 destination_device.create_image(
270 "delta", new_version,
271 ",".join(environment['version_detail']),
272 files,
273 base=delta['version'],
274 version_detail=",".join(environment['version_detail']))
275
276 # Set the phased percentage value for the new image
277 logging.info("Setting phased percentage of the new image to %s%%" %
278 args.phased_percentage)
279 destination_device.set_phased_percentage(
280 new_version, args.phased_percentage)
281
282 # Expire images
283 if args.destination_channel in conf.channels:
284 if conf.channels[args.destination_channel].fullcount > 0:
285 logging.info("Expiring old images")
286 destination_device.expire_images(
287 conf.channels[args.destination_channel].fullcount)
288
289 # Sync all channel aliases
290 logging.info("Syncing any existing alias")
291 pub.sync_aliases(args.destination_channel)
292
293 # Remove any orphaned file
294 logging.info("Removing orphaned files from the pool")
295 pub.cleanup_tree()
296
297 # Sync the mirrors
298 logging.info("Triggering a mirror sync")
299 tools.sync_mirrors(conf)
30034
=== modified file 'bin/set-phased-percentage' (properties changed: +x to -x)
--- bin/set-phased-percentage 2016-06-24 21:02:18 +0000
+++ bin/set-phased-percentage 2016-07-20 12:34:06 +0000
@@ -20,77 +20,13 @@
20import sys20import sys
21sys.path.insert(0, os.path.join(sys.path[0], os.pardir, "lib"))21sys.path.insert(0, os.path.join(sys.path[0], os.pardir, "lib"))
2222
23from systemimage import config, tools, tree23from systemimage.scripts import script_set_phased_percentage
2424
25import argparse25
26import logging26def main():
27 script = script_set_phased_percentage.SetPhasedPercentageScript()
28 script.run(sys.argv)
29
2730
28if __name__ == '__main__':31if __name__ == '__main__':
29 parser = argparse.ArgumentParser(description="set phased percentage")32 main()
30 parser.add_argument("channel", metavar="CHANNEL")
31 parser.add_argument("device", metavar="DEVICE")
32 parser.add_argument("version", metavar="VERSION", type=int)
33 parser.add_argument("percentage", metavar="PERCENTAGE", type=int)
34 parser.add_argument("--verbose", "-v", action="count")
35
36 args = parser.parse_args()
37
38 # Setup logging
39 formatter = logging.Formatter(
40 "%(asctime)s %(levelname)s %(message)s")
41
42 levels = {1: logging.ERROR,
43 2: logging.WARNING,
44 3: logging.INFO,
45 4: logging.DEBUG}
46
47 if args.verbose > 0:
48 stdoutlogger = logging.StreamHandler(sys.stdout)
49 stdoutlogger.setFormatter(formatter)
50 logging.root.setLevel(levels[min(4, args.verbose)])
51 logging.root.addHandler(stdoutlogger)
52 else:
53 logging.root.addHandler(logging.NullHandler())
54
55 # Load the configuration
56 conf = config.Config()
57
58 # Load the tree
59 pub = tree.Tree(conf)
60
61 # Do some checks
62 channels = pub.list_channels()
63 if args.channel not in channels:
64 parser.error("Invalid channel: %s" % args.channel)
65
66 if args.device not in channels[args.channel]['devices']:
67 parser.error("Invalid device for source channel: %s" %
68 args.device)
69
70 if args.percentage < 0 or args.percentage > 100:
71 parser.error("Invalid value: %s" % args.percentage)
72
73 if "alias" in channels[args.channel] and \
74 channels[args.channel]['alias'] != args.channel:
75 parser.error("Channel is an alias.")
76
77 if "redirect" in channels[args.channel]:
78 parser.error("Channel is a redirect.")
79
80 target_device = channels[args.channel]['devices'][args.device]
81 if "redirect" in target_device:
82 parser.error("Target device is a redirect. Use original channel "
83 "%s instead." % target_device['redirect'])
84
85 dev = pub.get_device(args.channel, args.device)
86 logging.info("Setting phased-percentage of '%s' to %s%%" %
87 (args.version, args.percentage))
88 dev.set_phased_percentage(args.version, args.percentage)
89
90 # Sync all channel aliases
91 logging.info("Syncing any existing alias")
92 pub.sync_aliases(args.channel)
93
94 # Sync the mirrors
95 logging.info("Triggering a mirror sync")
96 tools.sync_mirrors(conf)
9733
=== modified file 'bin/tag-image' (properties changed: +x to -x)
--- bin/tag-image 2016-06-24 21:02:18 +0000
+++ bin/tag-image 2016-07-20 12:34:06 +0000
@@ -1,7 +1,7 @@
1#!/usr/bin/python1#!/usr/bin/python
2# -*- coding: utf-8 -*-2# -*- coding: utf-8 -*-
33
4# Copyright (C) 2015 Canonical Ltd.4# Copyright (C) 2015-2016 Canonical Ltd.
5# Author: Łukasz 'sil2100' Zemczak <lukasz.zemczak@ubuntu.com>5# Author: Łukasz 'sil2100' Zemczak <lukasz.zemczak@ubuntu.com>
66
7# This program is free software: you can redistribute it and/or modify7# This program is free software: you can redistribute it and/or modify
@@ -16,191 +16,17 @@
16# You should have received a copy of the GNU General Public License16# 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/>.17# along with this program. If not, see <http://www.gnu.org/licenses/>.
1818
19import json
20import os19import os
21import sys20import sys
22import argparse
23import fcntl
24import logging
2521
26sys.path.insert(0, os.path.join(sys.path[0], os.pardir, "lib"))22sys.path.insert(0, os.path.join(sys.path[0], os.pardir, "lib"))
2723
28from operator import itemgetter24from systemimage.scripts import script_tag_image
29from systemimage import config, generators, tools, tree
3025
3126
32def main():27def main():
33 parser = argparse.ArgumentParser(description="image tagger")28 script = script_tag_image.TagImageScript()
34 parser.add_argument("channel", metavar="CHANNEL")29 script.run(sys.argv)
35 parser.add_argument("device", metavar="DEVICE")
36 parser.add_argument("version", metavar="VERSION", type=int)
37 parser.add_argument("tag", metavar="TAG")
38 parser.add_argument("--verbose", "-v", action="count", default=0)
39
40 args = parser.parse_args()
41
42 # Setup logging
43 formatter = logging.Formatter(
44 "%(asctime)s %(levelname)s %(message)s")
45
46 levels = {1: logging.ERROR,
47 2: logging.WARNING,
48 3: logging.INFO,
49 4: logging.DEBUG}
50
51 if args.verbose > 0:
52 stdoutlogger = logging.StreamHandler(sys.stdout)
53 stdoutlogger.setFormatter(formatter)
54 logging.root.setLevel(levels[min(4, args.verbose)])
55 logging.root.addHandler(stdoutlogger)
56 else:
57 logging.root.addHandler(logging.NullHandler())
58
59 # Load the configuration
60 conf = config.Config()
61
62 # Try to acquire a global lock
63 lock_file = os.path.join(conf.state_path, "global.lock")
64 lock_fd = open(lock_file, 'w')
65
66 try:
67 fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
68 except IOError:
69 print("Something else holds the global lock. exiting.")
70 sys.exit(0)
71
72 # Load the tree
73 pub = tree.Tree(conf)
74
75 # Do some checks
76 channels = pub.list_channels()
77 if args.channel not in channels:
78 parser.error("Invalid channel: %s" % args.channel)
79
80 if args.device not in channels[args.channel]['devices']:
81 parser.error("Invalid device for channel: %s" % args.device)
82
83 if "alias" in channels[args.channel] and \
84 channels[args.channel]['alias'] \
85 != args.channel:
86 parser.error("Channel is an alias.")
87
88 if "redirect" in channels[args.channel]:
89 parser.error("Channel is a redirect.")
90
91 target_device = channels[args.channel]['devices'][args.device]
92 if "redirect" in target_device:
93 parser.error("Target device is a redirect. Use original channel "
94 "%s instead." % target_device['redirect'])
95
96 if "," in args.tag:
97 parser.error("Image tag cannot include the character ','.")
98
99 device = pub.get_device(args.channel, args.device)
100
101 device_images = device.list_images()
102 if not device_images:
103 parser.error("No images in selected channel/device.")
104
105 sorted_images = sorted(device_images, key=itemgetter('version'))
106 if args.version != sorted_images[-1]['version']:
107 parser.error("You can only tag the latest image.")
108
109 # Extract the build we want to copy
110 images = [image for image in device_images
111 if image['type'] == "full" and image['version'] == args.version]
112 if not images:
113 parser.error("Can't find version: %s" % args.version)
114 image = images[0]
115
116 # Assign a new version number
117 new_version = args.version + 1
118
119 # Generate a list of required deltas
120 delta_base = tools.get_required_deltas(
121 conf, pub, args.channel, args.device)
122
123 # Create a new empty file list
124 new_files = []
125
126 # Extract current version_detail and image files
127 version_detail = tools.extract_files_and_version(
128 conf, image['files'], args.version, new_files)
129 logging.debug("Original version_detail is: %s" % version_detail)
130
131 # Generate new version tarball environment
132 environment = {}
133 environment['channel_name'] = args.channel
134 environment['device'] = device
135 environment['device_name'] = args.device
136 environment['version'] = new_version
137 environment['version_detail'] = [entry
138 for entry in version_detail.split(",")
139 if not entry.startswith("version=")]
140 environment['new_files'] = new_files
141
142 # Set the tag for the image
143 tools.set_tag_on_version_detail(environment['version_detail'], args.tag)
144 logging.info("Setting tag for image to '%s'" % args.tag)
145
146 # Generate the new version tarball
147 logging.info("Generating new version tarball for '%s' (%s)"
148 % (new_version, ",".join(environment['version_detail'])))
149 version_path = generators.generate_file(conf, "version", [], environment)
150 if version_path:
151 new_files.append(version_path)
152
153 # Add full image
154 logging.info("Publishing new image '%s' (%s) with %s files."
155 % (new_version, ",".join(environment['version_detail']),
156 len(new_files)))
157 device.create_image(
158 "full", new_version, ",".join(environment['version_detail']),
159 new_files, version_detail=",".join(environment['version_detail']))
160
161 # Add delta images
162 delta_files = []
163 for path in new_files:
164 filename = path.split("/")[-1]
165 if filename.startswith("version-") or filename.startswith("keyring-"):
166 delta_files.append(path)
167
168 for delta in delta_base:
169 logging.info("Publishing new delta from '%s' (%s)"
170 " to '%s' (%s)" %
171 (delta['version'], delta.get("description", ""),
172 new_version, ",".join(environment['version_detail'])))
173
174 device.create_image(
175 "delta", new_version,
176 ",".join(environment['version_detail']),
177 delta_files,
178 base=delta['version'],
179 version_detail=",".join(environment['version_detail']))
180
181 # Set the phased percentage value for the new image
182 percentage = device.get_phased_percentage(args.version)
183 logging.info("Setting phased percentage of the new image to %s%%" %
184 percentage)
185 device.set_phased_percentage(new_version, percentage)
186
187 # Expire images
188 if args.channel in conf.channels:
189 if conf.channels[args.channel].fullcount > 0:
190 logging.info("Expiring old images")
191 device.expire_images(conf.channels[args.channel].fullcount)
192
193 # Sync all channel aliases
194 logging.info("Syncing any existing alias")
195 pub.sync_aliases(args.channel)
196
197 # Remove any orphaned file
198 logging.info("Removing orphaned files from the pool")
199 pub.cleanup_tree()
200
201 # Sync the mirrors
202 logging.info("Triggering a mirror sync")
203 tools.sync_mirrors(conf)
20430
20531
206if __name__ == '__main__':32if __name__ == '__main__':
20733
=== added file 'lib/systemimage/script.py'
--- lib/systemimage/script.py 1970-01-01 00:00:00 +0000
+++ lib/systemimage/script.py 2016-07-20 12:34:06 +0000
@@ -0,0 +1,269 @@
1# -*- coding: utf-8 -*-
2
3# Copyright (C) 2015-2016 Canonical Ltd.
4# Authors: Łukasz 'sil2100' Zemczak <lukasz.zemczak@ubuntu.com>
5# Stéphane Graber <stgraber@ubuntu.com>
6
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; version 3 of the License.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19import json
20import os
21import sys
22import argparse
23import fcntl
24import logging
25
26sys.path.insert(0, os.path.join(sys.path[0], os.pardir, "lib"))
27
28from operator import itemgetter
29from systemimage import config, generators, tools, tree
30
31
32class ScriptError(Exception):
33 """Exception used to indicate error on any stage of a script."""
34 pass
35
36
37class Script:
38 """Base common class for all system-image scripts."""
39
40 def __init__(self, path=None):
41 # Load the configuration
42 self.conf = config.Config(path) if path else config.Config()
43 self.pub = None # system-image tree, defined during run()
44
45 # Variables to be defined during setup()/execute()
46 self.channel = None # Channel name
47 self.device = None # Device object
48
49 self.lock_fd = None
50
51 def setup_logging(self, verbosity):
52 """Helper function setting up common logging scheme."""
53
54 formatter = logging.Formatter(
55 "%(asctime)s %(levelname)s %(message)s")
56
57 levels = {1: logging.ERROR,
58 2: logging.WARNING,
59 3: logging.INFO,
60 4: logging.DEBUG}
61
62 if verbosity > 0:
63 stdoutlogger = logging.StreamHandler(sys.stdout)
64 stdoutlogger.setFormatter(formatter)
65 logging.root.setLevel(levels[min(4, verbosity)])
66 logging.root.addHandler(stdoutlogger)
67 else:
68 logging.root.addHandler(logging.NullHandler())
69
70 def sync_trees(self):
71 """Helper function for syncing changes to all trees."""
72 # Sync all channel aliases
73 logging.info("Syncing any existing alias")
74 self.pub.sync_aliases(self.channel)
75
76 # Sync the mirrors
77 logging.info("Triggering a mirror sync")
78 tools.sync_mirrors(self.conf)
79
80 def lock(self):
81 """Aquire the global lock, protecting from races."""
82
83 # Try to acquire a global lock
84 lock_file = os.path.join(self.conf.state_path, "global.lock")
85 self.lock_fd = open(lock_file, 'w')
86
87 try:
88 fcntl.lockf(self.lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
89 except IOError:
90 raise ScriptError("Something else holds the global lock. exiting.")
91
92 def presetup(self):
93 """All low-level pre-setup to be done before the script can be run."""
94 # Load the tree
95 self.pub = tree.Tree(self.conf)
96
97 def setup(self, argv):
98 """Implementable in script. Perform script setup, parse cmdline."""
99 pass
100
101 def execute(self):
102 """Implementable in script. All script execution code goes here."""
103 pass
104
105 def cleanup(self):
106 """Called at the end - by default provides image cleanup."""
107
108 # Expire images, if a device was used in the script
109 if self.device and self.channel in self.conf.channels:
110 if self.conf.channels[self.channel].fullcount > 0:
111 logging.info("Expiring old images")
112 self.device.expire_images(
113 self.conf.channels[
114 self.channel].fullcount)
115
116 # Remove any orphaned file
117 logging.info("Removing orphaned files from the pool")
118 self.pub.cleanup_tree()
119
120 self.sync_trees()
121
122 def run(self, argv):
123 """Run the script."""
124
125 try:
126 self.lock()
127 self.presetup()
128 self.setup(argv)
129 self.execute()
130 self.cleanup()
131 except ScriptError as err:
132 print(str(err))
133 return False
134
135 return True
136
137
138class CommonImageManipulationScript(Script):
139 """A common base for scripts that manipulate images, providing helpers."""
140
141 def generate_version(self, source_files, version, tag=None):
142 """
143 Generate the version tarball for the version and files.
144 Requires the self.target_version, self.channel and self.device
145 variables: for the new version number, destination channel and
146 device object respectively.
147 """
148
149 # Extract current version_detail and files
150 version_detail = tools.extract_files_and_version(
151 self.conf,
152 source_files,
153 version,
154 self.new_images['full']['files'])
155
156 # Generate new version tarball
157 self.environment = {}
158 self.environment['channel_name'] = self.channel
159 self.environment['device'] = self.device
160 self.environment['device_name'] = self.device.name
161 self.environment['version'] = self.target_version
162 self.environment['version_detail'] = [
163 entry for entry in version_detail.split(",")
164 if not entry.startswith("version=")]
165 self.environment['new_files'] = self.new_images['full']['files']
166
167 # Add new tag if requested
168 if tag:
169 tools.set_tag_on_version_detail(
170 self.environment['version_detail'], tag)
171 logging.info("Setting tag for image to '%s'" % tag)
172
173 logging.info("Generating new version tarball for '%s' (%s)"
174 % (self.target_version, ",".join(
175 self.environment['version_detail'])))
176 version_path = generators.generate_file(
177 self.conf, "version", [], self.environment)
178 if version_path:
179 self.new_images['full']['files'].append(version_path)
180
181 def generate_deltas(self):
182 """
183 Generates deltas from the list in self.delta_base.
184 To generate the deltas the self.new_images dictionary is used. All
185 the newly generated delta files are then appended to the new_images
186 structure.
187 """
188
189 for delta in self.delta_base:
190 self.new_images['delta_%s' % delta['version']] = {'files': []}
191
192 # Generate deltas
193 for abspath in self.new_images['full']['files']:
194 prefix = abspath.split("/")[-1].rsplit("-", 1)[0]
195 for delta in self.delta_base:
196 # Extract the source
197 src_path = None
198 for file_dict in delta['files']:
199 if (file_dict['path'].split("/")[-1]
200 .startswith(prefix)):
201 src_path = "%s/%s" % (self.conf.publish_path,
202 file_dict['path'])
203 break
204
205 # Check that it's not the current file
206 if src_path:
207 src_path = os.path.realpath(src_path)
208
209 # FIXME: the keyring- is a big hack...
210 if src_path == abspath and "keyring-" not in src_path:
211 continue
212
213 # Generators are allowed to return None when no delta
214 # exists at all.
215 logging.info("Generating delta from '%s' for '%s'" %
216 (delta['version'],
217 prefix))
218 delta_path = generators.generate_delta(self.conf, src_path,
219 abspath)
220 else:
221 delta_path = abspath
222
223 if not delta_path:
224 continue
225
226 # Get the full and relative paths
227 delta_abspath, delta_relpath = tools.expand_path(
228 delta_path, self.conf.publish_path)
229
230 self.new_images['delta_%s' % delta['version']]['files'] \
231 .append(delta_abspath)
232
233 def add_images_to_index(self):
234 """
235 Adds all newly created image files to the index.
236 This basically takes all the new entries from the self.new_images
237 dictionary (full and delta entries) and adds them, calling
238 create_image on the device object.
239 """
240
241 # Add full image
242 logging.info("Publishing new image '%s' (%s) with %s files."
243 % (self.target_version,
244 ",".join(self.environment['version_detail']),
245 len(self.new_images['full']['files'])))
246 self.device.create_image(
247 "full",
248 self.target_version,
249 ",".join(self.environment['version_detail']),
250 self.new_images['full']['files'],
251 version_detail=",".join(self.environment['version_detail']))
252
253 # Add delta images
254 for delta in self.delta_base:
255 files = self.new_images['delta_%s' % delta['version']]['files']
256 logging.info("Publishing new delta from '%s' (%s)"
257 " to '%s' (%s) with %s files" %
258 (delta['version'],
259 delta.get("description", ""),
260 self.target_version,
261 ",".join(self.environment['version_detail']),
262 len(files)))
263
264 self.device.create_image(
265 "delta", self.target_version,
266 ",".join(self.environment['version_detail']),
267 files,
268 base=delta['version'],
269 version_detail=",".join(self.environment['version_detail']))
0270
=== added directory 'lib/systemimage/scripts'
=== added file 'lib/systemimage/scripts/__init__.py'
=== added file 'lib/systemimage/scripts/script_copy_image.py'
--- lib/systemimage/scripts/script_copy_image.py 1970-01-01 00:00:00 +0000
+++ lib/systemimage/scripts/script_copy_image.py 2016-07-20 12:34:06 +0000
@@ -0,0 +1,187 @@
1# -*- coding: utf-8 -*-
2
3# Copyright (C) 2016 Canonical Ltd.
4# Authors: Łukasz 'sil2100' Zemczak <lukasz.zemczak@ubuntu.com>
5# Stéphane Graber <stgraber@ubuntu.com>
6
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; version 3 of the License.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19import json
20import os
21import sys
22import argparse
23import fcntl
24import logging
25
26from systemimage import config, generators, tools, tree, script
27from systemimage.script import ScriptError
28
29
30class CopyImageScript(script.CommonImageManipulationScript):
31 """Script used to copy images from channel to channel."""
32
33 def setup(self, argv):
34 parser = argparse.ArgumentParser(description="image copier")
35 parser.add_argument("source_channel", metavar="SOURCE-CHANNEL")
36 parser.add_argument("destination_channel",
37 metavar="DESTINATION-CHANNEL")
38 parser.add_argument("device", metavar="DEVICE")
39 parser.add_argument("version", metavar="VERSION", type=int)
40 parser.add_argument("-k", "--keep-version", action="store_true",
41 help="Keep the original version number")
42 parser.add_argument("-p", "--phased-percentage", type=int,
43 help="Set the phased percentage for the copied "
44 "image",
45 default=100)
46 parser.add_argument("-t", "--tag", type=str,
47 help="Set a version tag on the new image")
48 parser.add_argument("--verbose", "-v", action="count", default=0)
49
50 argv.pop(0) # To fix issues with parse_args() shifting arguments
51 args = parser.parse_args(argv)
52
53 # Setup logging
54 self.setup_logging(args.verbose)
55
56 # Do some checks
57 channels = self.pub.list_channels()
58 if args.source_channel not in channels:
59 raise ScriptError("Invalid source channel: %s" %
60 args.source_channel)
61
62 if args.destination_channel not in channels:
63 raise ScriptError("Invalid destination channel: %s" %
64 args.destination_channel)
65
66 if args.device not in \
67 channels[args.source_channel]['devices']:
68 raise ScriptError("Invalid device for source channel: %s" %
69 args.device)
70
71 if args.device not in \
72 channels[args.destination_channel]['devices']:
73 raise ScriptError("Invalid device for destination channel: %s" %
74 args.device)
75
76 if "alias" in channels[args.source_channel] and \
77 channels[args.source_channel]['alias'] \
78 != args.source_channel:
79 raise ScriptError("Source channel is an alias.")
80
81 if "alias" in channels[args.destination_channel] and \
82 channels[args.destination_channel]['alias'] \
83 != args.destination_channel:
84 raise ScriptError("Destination channel is an alias.")
85
86 if "redirect" in channels[args.source_channel]:
87 raise ScriptError("Source channel is a redirect.")
88
89 if "redirect" in channels[args.destination_channel]:
90 raise ScriptError("Destination channel is a redirect.")
91
92 src_device = channels[args.source_channel]['devices'][args.device]
93 if "redirect" in src_device:
94 raise ScriptError("Source device is a redirect. Use original "
95 "channel %s instead." % src_device['redirect'])
96
97 dest_device = \
98 channels[args.destination_channel]['devices'][args.device]
99 if "redirect" in dest_device:
100 raise ScriptError("Destination device is a redirect. Use original "
101 "channel %s instead." % dest_device['redirect'])
102
103 if args.tag and "," in args.tag:
104 raise ScriptError("Image tag cannot include the character ','.")
105
106 self.args = args
107 self.channel = args.destination_channel
108
109 def assign_new_version(self, version, keep_version=False):
110 if keep_version:
111 images = [image for image in self.device.list_images()
112 if image['version'] == version]
113 if images:
114 raise ScriptError(
115 "Version number is already used: %s" % version)
116
117 # Assign a new version number
118 new_version = version
119 if not keep_version:
120 # Find the next available version
121 new_version = 1
122 for image in self.device.list_images():
123 if image['version'] >= new_version:
124 new_version = image['version'] + 1
125 logging.debug("Version for next image: %s" % new_version)
126
127 self.target_version = new_version
128
129 def execute(self):
130 source_device = self.pub.get_device(
131 self.args.source_channel, self.args.device)
132 self.device = self.pub.get_device(
133 self.channel, self.args.device)
134
135 # Assign a version number to the new image
136 self.assign_new_version(self.args.version, self.args.keep_version)
137
138 # Extract the build we want to copy
139 source_image = tools.extract_image(
140 source_device.list_images(), self.args.version)
141 if not source_image:
142 raise ScriptError("Can't find version: %s" % version)
143
144 # Extract the list of existing full images
145 full_images = {image['version']: image
146 for image in self.device.list_images()
147 if image['type'] == "full"}
148
149 # Check that the last full and the new image aren't one and the same
150 source_files = [entry['path'].split("/")[-1]
151 for entry in source_image['files']
152 if not entry['path'].split("/")[-1].startswith(
153 "version-")]
154 destination_files = []
155 if full_images:
156 latest_full = sorted(full_images.values(),
157 key=lambda image: image['version'])[-1]
158 destination_files = [entry['path'].split("/")[-1]
159 for entry in latest_full['files']
160 if not entry['path'].split(
161 "/")[-1].startswith("version-")]
162 if source_files == destination_files:
163 raise ScriptError("Source image is already latest full in "
164 "destination channel.")
165
166 # Generate a list of required deltas
167 self.delta_base = tools.get_required_deltas(
168 self.conf, self.pub, self.channel, self.args.device)
169
170 # Create new empty entries
171 self.new_images = {'full': {'files': []}}
172
173 # Generate the version tarball
174 self.generate_version(
175 source_image['files'], self.args.version, self.args.tag)
176
177 # Generate all required deltas
178 self.generate_deltas()
179
180 # Add both full and delta images to the index
181 self.add_images_to_index()
182
183 # Set the phased percentage value for the new image
184 logging.info("Setting phased percentage of the new image to %s%%" %
185 self.args.phased_percentage)
186 self.device.set_phased_percentage(
187 self.target_version, self.args.phased_percentage)
0188
=== added file 'lib/systemimage/scripts/script_set_phased_percentage.py'
--- lib/systemimage/scripts/script_set_phased_percentage.py 1970-01-01 00:00:00 +0000
+++ lib/systemimage/scripts/script_set_phased_percentage.py 2016-07-20 12:34:06 +0000
@@ -0,0 +1,83 @@
1# -*- coding: utf-8 -*-
2
3# Copyright (C) 2016 Canonical Ltd.
4# Authors: Łukasz 'sil2100' Zemczak <lukasz.zemczak@ubuntu.com>
5# Stéphane Graber <stgraber@ubuntu.com>
6
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; version 3 of the License.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19import json
20import os
21import sys
22import argparse
23import fcntl
24import logging
25
26from systemimage import config, generators, tools, tree, script
27from systemimage.script import ScriptError
28
29
30class SetPhasedPercentageScript(script.CommonImageManipulationScript):
31 """Script used to set phased percentages of images."""
32
33 def setup(self, argv):
34 parser = argparse.ArgumentParser(description="set phased percentage")
35 parser.add_argument("channel", metavar="CHANNEL")
36 parser.add_argument("device", metavar="DEVICE")
37 parser.add_argument("version", metavar="VERSION", type=int)
38 parser.add_argument("percentage", metavar="PERCENTAGE", type=int)
39 parser.add_argument("--verbose", "-v", action="count", default=0)
40
41 argv.pop(0) # To fix issues with parse_args() shifting arguments
42 args = parser.parse_args(argv)
43
44 # Setup logging
45 self.setup_logging(args.verbose)
46
47 # Do some checks
48 channels = self.pub.list_channels()
49 if args.channel not in channels:
50 raise ScriptError("Invalid channel: %s" % args.channel)
51
52 if args.device not in channels[args.channel]['devices']:
53 raise ScriptError("Invalid device for source channel: %s" %
54 args.device)
55
56 if args.percentage < 0 or args.percentage > 100:
57 raise ScriptError("Invalid value: %s" % args.percentage)
58
59 if "alias" in channels[args.channel] and \
60 channels[args.channel]['alias'] != args.channel:
61 raise ScriptError("Channel is an alias.")
62
63 if "redirect" in channels[args.channel]:
64 raise ScriptError("Channel is a redirect.")
65
66 target_device = channels[args.channel]['devices'][args.device]
67 if "redirect" in target_device:
68 raise ScriptError(
69 "Target device is a redirect. Use original channel %s "
70 "instead." % target_device['redirect'])
71
72 self.args = args
73
74 def execute(self):
75 dev = self.pub.get_device(self.args.channel, self.args.device)
76 logging.info("Setting phased-percentage of '%s' to %s%%" %
77 (self.args.version, self.args.percentage))
78 dev.set_phased_percentage(self.args.version, self.args.percentage)
79
80 def cleanup(self):
81 # We're overwriting this method as there's no need to expire images
82 # after running this command
83 self.sync_trees()
084
=== added file 'lib/systemimage/scripts/script_tag_image.py'
--- lib/systemimage/scripts/script_tag_image.py 1970-01-01 00:00:00 +0000
+++ lib/systemimage/scripts/script_tag_image.py 2016-07-20 12:34:06 +0000
@@ -0,0 +1,127 @@
1# -*- coding: utf-8 -*-
2
3# Copyright (C) 2016 Canonical Ltd.
4# Authors: Łukasz 'sil2100' Zemczak <lukasz.zemczak@ubuntu.com>
5# Stéphane Graber <stgraber@ubuntu.com>
6
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; version 3 of the License.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19import json
20import os
21import sys
22import argparse
23import fcntl
24import logging
25
26from operator import itemgetter
27from systemimage import config, generators, tools, tree, script
28from systemimage.script import ScriptError
29
30
31class TagImageScript(script.CommonImageManipulationScript):
32 """Script used to append/modify tags in images."""
33
34 def setup(self, argv):
35 parser = argparse.ArgumentParser(description="image tagger")
36 parser.add_argument("channel", metavar="CHANNEL")
37 parser.add_argument("device", metavar="DEVICE")
38 parser.add_argument("version", metavar="VERSION", type=int)
39 parser.add_argument("tag", metavar="TAG")
40 parser.add_argument("--verbose", "-v", action="count", default=0)
41
42 argv.pop(0) # To fix issues with parse_args() shifting arguments
43 args = parser.parse_args(argv)
44
45 # Setup logging
46 self.setup_logging(args.verbose)
47
48 # Do some checks
49 channels = self.pub.list_channels()
50 if args.channel not in channels:
51 raise ScriptError("Invalid channel: %s" % args.channel)
52
53 if args.device not in \
54 channels[args.channel]['devices']:
55 raise ScriptError("Invalid device for channel: %s" % args.device)
56
57 if "alias" in channels[args.channel] and \
58 channels[args.channel]['alias'] \
59 != args.channel:
60 raise ScriptError("Channel is an alias.")
61
62 if "redirect" in channels[args.channel]:
63 raise ScriptError("Channel is a redirect.")
64
65 target_device = channels[args.channel]['devices'][args.device]
66 if "redirect" in target_device:
67 raise ScriptError(
68 "Target device is a redirect. Use original channel %s "
69 "instead." % target_device['redirect'])
70
71 if "," in args.tag:
72 raise ScriptError("Image tag cannot include the character ','.")
73
74 self.args = args
75 self.channel = args.channel
76
77 def execute(self):
78 self.device = self.pub.get_device(self.args.channel, self.args.device)
79
80 device_images = self.device.list_images()
81 if not device_images:
82 raise ScriptError("No images in selected channel/device.")
83
84 sorted_images = sorted(device_images, key=itemgetter('version'))
85 if self.args.version != sorted_images[-1]['version']:
86 raise ScriptError("You can only tag the latest image.")
87
88 # Extract the build we want to copy
89 self.image = tools.extract_image(device_images, self.args.version)
90 if not self.image:
91 raise ScriptError("Can't find version: %s" % version)
92
93 # Remember the current image's phased percentage
94 percentage = self.device.get_phased_percentage(self.args.version)
95
96 # Assign a new version number
97 self.target_version = self.args.version + 1
98
99 # Generate a list of required deltas
100 self.delta_base = tools.get_required_deltas(
101 self.conf, self.pub, self.channel, self.args.device)
102
103 # Create new empty entries
104 self.new_images = {'full': {'files': []}}
105
106 self.generate_version(
107 self.image['files'], self.args.version, self.args.tag)
108
109 # Add delta images
110 delta_files = []
111 for path in self.new_images['full']['files']:
112 filename = path.split("/")[-1]
113 if (filename.startswith("version-") or
114 filename.startswith("keyring-")):
115 delta_files.append(path)
116
117 for delta in self.delta_base:
118 self.new_images['delta_%s' % delta['version']] = \
119 {'files': delta_files}
120
121 # Add both full and delta images to the index
122 self.add_images_to_index()
123
124 # Set the phased percentage of the image to the previous value
125 logging.info("Setting phased percentage of the new image to %s%%" %
126 percentage)
127 self.device.set_phased_percentage(self.target_version, percentage)
0128
=== added file 'lib/systemimage/tests/test_scripts.py'
--- lib/systemimage/tests/test_scripts.py 1970-01-01 00:00:00 +0000
+++ lib/systemimage/tests/test_scripts.py 2016-07-20 12:34:06 +0000
@@ -0,0 +1,590 @@
1# -*- coding: utf-8 -*-
2
3# Copyright (C) 2015-2016 Canonical Ltd.
4# Author: Łukasz 'sil2100' Zemczak <lukasz.zemczak@ubuntu.com>
5
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; version 3 of the License.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18import os
19import shutil
20import stat
21import subprocess
22import tarfile
23import tempfile
24import unittest
25import json
26import six
27import fcntl
28
29from mock import Mock, patch
30from systemimage import config, tools, tree, gpg
31from systemimage import script as script_base
32from systemimage.scripts import (
33 script_copy_image,
34 script_tag_image,
35 script_set_phased_percentage,
36 )
37from systemimage.testing.helpers import HAS_TEST_KEYS, MISSING_KEYS_WARNING
38from systemimage.tools import xz_uncompress
39
40
41class ScriptTests(unittest.TestCase):
42 def setUp(self):
43 temp_directory = tempfile.mkdtemp()
44 self.temp_directory = temp_directory
45 self.old_path = os.environ.get("PATH", None)
46
47 os.mkdir(os.path.join(self.temp_directory, "etc"))
48 state_dir = os.path.join(self.temp_directory, "state")
49 os.mkdir(state_dir)
50 lock_file = os.path.join(state_dir, "global.lock")
51 open(lock_file, 'w+')
52
53 def tearDown(self):
54 shutil.rmtree(self.temp_directory)
55 if self.old_path:
56 os.environ['PATH'] = self.old_path
57
58
59class BaseScriptTests(ScriptTests):
60 def setUp(self):
61 super().setUp()
62 config_path = os.path.join(self.temp_directory, "etc", "config")
63 with open(config_path, "w+") as fd:
64 fd.write("""[global]
65base_path = %s
66gpg_key_path = %s
67public_fqdn = system-image.example.net
68public_http_port = 880
69public_https_port = 8443
70""" % (self.temp_directory, os.path.join(os.getcwd(), "tools", "keys")))
71 self.config_path = config_path
72
73 def test_lock(self):
74 """Make sure locking works."""
75 script = script_base.Script(self.config_path)
76 script.lock()
77 lock_file = os.path.join(script.conf.state_path, "global.lock")
78 lock_fd = open(lock_file, 'r')
79 with self.assertRaises(IOError):
80 fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
81
82 def test_lock_already_locked(self):
83 """Make sure locking twice rises an error."""
84 script = script_base.Script(self.config_path)
85 script2 = script_base.Script(self.config_path)
86 script.lock()
87 with self.assertRaises(script_base.ScriptError):
88 script2.lock()
89
90 @patch('systemimage.tools.sync_mirrors')
91 def test_sync_trees(self, sync_mock):
92 """Check if calling sync_trees synces what we need."""
93 script = script_base.Script(self.config_path)
94 script.pub = Mock()
95 script.sync_trees()
96 self.assertEqual(script.pub.sync_aliases.call_count, 1)
97 self.assertEqual(sync_mock.call_count, 1)
98
99 @patch('systemimage.tools.sync_mirrors')
100 def test_cleanup(self, sync_mock):
101 """Check if cleanup performs proper steps."""
102 script = script_base.Script(self.config_path)
103 script.pub = Mock()
104 script.cleanup()
105 self.assertEqual(script.pub.sync_aliases.call_count, 1)
106 self.assertEqual(script.pub.cleanup_tree.call_count, 1)
107 self.assertEqual(sync_mock.call_count, 1)
108
109 def test_run(self):
110 """Check if all required class methods are called."""
111 script = script_base.Script(self.config_path)
112 script.lock = Mock()
113 script.presetup = Mock()
114 script.setup = Mock()
115 script.execute = Mock()
116 script.cleanup = Mock()
117 script.run([])
118 self.assertEqual(script.lock.call_count, 1)
119 self.assertEqual(script.presetup.call_count, 1)
120 self.assertEqual(script.setup.call_count, 1)
121 self.assertEqual(script.execute.call_count, 1)
122 self.assertEqual(script.cleanup.call_count, 1)
123
124 def test_error_handled(self):
125 """Check if error on any stage causes the script to fail."""
126 script = script_base.Script(self.config_path)
127 script.lock = Mock()
128 script.presetup = Mock()
129 script.setup = Mock()
130 script.execute = Mock()
131 script.cleanup = Mock()
132
133 for name in ["lock", "presetup", "setup", "execute", "cleanup"]:
134 setattr(script, name, Mock(
135 side_effect=script_base.ScriptError('foo')))
136 self.assertFalse(script.run([]))
137 setattr(script, name, Mock())
138
139
140class CommonScriptTests(ScriptTests):
141 def setUp(self):
142 super().setUp()
143 config_path = os.path.join(self.temp_directory, "etc", "config")
144 with open(config_path, "w+") as fd:
145 fd.write("""[global]
146base_path = %s
147gpg_key_path = %s
148public_fqdn = system-image.example.net
149public_http_port = 880
150public_https_port = 8443
151""" % (self.temp_directory, os.path.join(os.getcwd(), "tools", "keys")))
152 self.config_path = config_path
153
154 os.mkdir(os.path.join(self.temp_directory, "www"))
155 script = script_base.CommonImageManipulationScript(self.config_path)
156 script.presetup()
157 script.pub.create_channel("test")
158 script.pub.create_device("test", "test")
159 self.script = script
160 self.device = script.pub.get_device("test", "test")
161
162 # An image helper file that can be used for contents
163 self.image_file = os.path.join(
164 self.script.conf.publish_path, "test_file")
165 open(self.image_file, "w+").close()
166 gpg.sign_file(self.script.conf, "image-signing", self.image_file)
167
168 @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING)
169 @patch('systemimage.tools.extract_files_and_version')
170 def test_generate_version(self, extract_mock):
171 """Check if script parts for generating version tarballs work"""
172
173 version = 1
174 tag = "Test-1"
175
176 # Emulate environment of the script run
177 self.script.target_version = version + 1
178 self.script.channel = "ubuntu-test/test"
179 self.script.device = self.device
180 self.script.new_images = {'full': {'files': ["some-file.tar.xz"]}}
181
182 self.script.generate_version([], version, tag)
183
184 self.assertEqual(
185 self.script.new_images['full']['files'],
186 ["some-file.tar.xz",
187 os.path.realpath(os.path.join(
188 self.device.path,
189 "version-%s.tar.xz" % self.script.target_version))])
190
191 target_obj = None
192 xz_path = self.script.new_images['full']['files'][-1]
193 unxz_path = os.path.join(self.temp_directory, "temp-unpack.tar")
194 channel_path = "system/etc/system-image/channel.ini"
195 extracted_path = os.path.join(self.temp_directory, channel_path)
196 try:
197 xz_uncompress(xz_path, unxz_path)
198 target_obj = tarfile.open(unxz_path, "r")
199 target_obj.extract(
200 channel_path, self.temp_directory)
201 with open(extracted_path, "r") as f:
202 contents = f.read()
203 self.assertIn(
204 "tag=%s" % tag, contents)
205 self.assertIn(
206 "channel: %s" % self.script.channel, contents)
207 self.assertIn(
208 "build_number: %s" % self.script.target_version, contents)
209 finally:
210 target_obj.close()
211 os.remove(unxz_path)
212 os.remove(extracted_path)
213
214 @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING)
215 @patch('systemimage.generators.generate_delta')
216 def test_generate_deltas(self, delta_mock):
217 """Check if we generate proper delta files."""
218
219 # Emulate environment of the script run
220 self.script.version = 2
221 self.script.channel = "ubuntu-test/test"
222 self.script.device = self.device
223 self.script.new_images = {'full': {'files': [
224 os.path.join(self.script.conf.publish_path, "file-source.tar.xz"),
225 os.path.join(self.script.conf.publish_path, "file2-source.tar.xz"),
226 os.path.join(self.script.conf.publish_path,
227 "keyring-source.tar.xz")]}}
228 self.script.delta_base = [
229 {'version': 1, 'files': [{'path': "file-other.tar.xz"}]},
230 {'version': 2, 'files': [{'path': "file-other2.tar.xz"},
231 {'path': "file2-other3.tar.xz"}]},
232 {'version': 3, 'files': [{'path': "file-other4.tar.xz"},
233 {'path': "keyring-source.tar.xz"},
234 {'path': "file2-other5.tar.xz"}]},
235 {'version': 4, 'files': [{'path': "file-other6.tar.xz"},
236 {'path': "keyring-other.tar.xz"},
237 {'path': "file-source.tar.xz"}]}, ]
238
239 expected_lengths = {1: 1, 2: 2, 3: 3, 4: 2}
240 delta_mock.return_value = "delta.tar.xz"
241
242 self.script.generate_deltas()
243
244 for delta in self.script.delta_base:
245 version = delta['version']
246 self.assertIn('delta_%s' % version, self.script.new_images)
247 delta_files = self.script.new_images['delta_%s' % version]['files']
248 self.assertEqual(
249 len(delta_files),
250 len(self.script.new_images['full']['files']))
251 self.assertEqual(
252 len([x for x in delta_files if "delta" in x]),
253 expected_lengths[version])
254
255 @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING)
256 def test_add_images_to_index(self):
257 """Check if a full image upload is properly added to the index."""
258
259 self.script.target_version = 1
260 self.script.environment = {'version_detail': [
261 "ubuntu=1", "device=1", "tag=Foo"]}
262 self.script.new_images = {'full': {'files': [self.image_file]}}
263 self.script.delta_base = []
264 self.script.device = self.device
265
266 self.script.add_images_to_index()
267 image = tools.extract_image(self.script.device.list_images(), 1)
268
269 self.assertIn('version', image)
270 self.assertEqual(image['version'], 1)
271 self.assertIn('version_detail', image)
272 self.assertEqual(image['version_detail'], "ubuntu=1,device=1,tag=Foo")
273
274 @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING)
275 def test_add_images_to_index_with_delta(self):
276 """Check if a full + delta upload is properly added."""
277
278 self.script.target_version = 1
279 self.script.environment = {'version_detail': [
280 "ubuntu=1", "device=1", "tag=Foo"]}
281 self.script.new_images = {
282 'full': {'files': [self.image_file]},
283 'delta_4': {'files': [self.image_file]}}
284 self.script.delta_base = [{
285 'version': 4, 'files': [{'path': "other-file.tar.xz"}]}]
286 self.script.device = self.device
287
288 self.script.add_images_to_index()
289 images = self.script.device.list_images()
290
291 self.assertEqual(len(images), 2)
292
293 has_delta = False
294 has_full = False
295 for image in images:
296 self.assertIn('type', image)
297 if image['type'] == "delta":
298 has_delta = True
299 self.assertIn('base', image)
300 self.assertEqual(image['base'], 4)
301 elif image['type'] == "full":
302 has_full = True
303 self.assertIn('version', image)
304 self.assertEqual(image['version'], 1)
305 self.assertIn('version_detail', image)
306 self.assertEqual(
307 image['version_detail'], "ubuntu=1,device=1,tag=Foo")
308
309 self.assertTrue(has_delta)
310 self.assertTrue(has_full)
311
312
313class ImageScriptBaseTests(ScriptTests):
314 scriptClass = script_base.CommonImageManipulationScript
315
316 def setUp(self):
317 super().setUp()
318 config_path = os.path.join(self.temp_directory, "etc", "config")
319 with open(config_path, "w+") as fd:
320 fd.write("""[global]
321base_path = %s
322gpg_key_path = %s
323public_fqdn = system-image.example.net
324public_http_port = 880
325public_https_port = 8443
326""" % (self.temp_directory, os.path.join(os.getcwd(), "tools", "keys")))
327 self.config_path = config_path
328
329 os.mkdir(os.path.join(self.temp_directory, "www"))
330 script = self.scriptClass(self.config_path)
331 script.presetup()
332 script.pub.create_channel("empty")
333 script.pub.create_channel("from")
334 script.pub.create_channel("to")
335 script.pub.create_device("empty", "test")
336 script.pub.create_device("from", "test")
337 script.pub.create_device("to", "test")
338
339 self.image_file = os.path.join(script.conf.publish_path, "test_file")
340 open(self.image_file, "w+").close()
341 gpg.sign_file(script.conf, "image-signing", self.image_file)
342
343 device = script.pub.get_device("from", "test")
344 device.create_image("full", 1, "abc", [self.image_file])
345 device.create_image("full", 2, "def", [self.image_file])
346 device = script.pub.get_device("to", "test")
347 device.create_image("full", 1, "abc", [self.image_file])
348
349 self.script = script
350
351
352class CopyImageScriptTests(ImageScriptBaseTests):
353 scriptClass = script_copy_image.CopyImageScript
354
355 def test_copy_setup_correct(self):
356 """Check if setup doesn't fail with good arguments."""
357 argv = ["copy-image", "from", "to", "test", "1"]
358 self.script.setup(argv)
359
360 def test_copy_setup_invalid_channel(self):
361 """Check if setup fails on invalid channel arguments."""
362 argv = ["copy-image", "none", "to", "test", "1"]
363 with self.assertRaises(script_base.ScriptError):
364 self.script.setup(argv)
365 argv = ["copy-image", "from", "none", "test", "1"]
366 with self.assertRaises(script_base.ScriptError):
367 self.script.setup(argv)
368
369 self.script.pub.create_channel_alias("alias1", "from")
370 argv = ["copy-image", "alias1", "to", "test", "1"]
371 with self.assertRaises(script_base.ScriptError):
372 self.script.setup(argv)
373 self.script.pub.create_channel_alias("alias2", "to")
374 argv = ["copy-image", "from", "alias2", "test", "1"]
375 with self.assertRaises(script_base.ScriptError):
376 self.script.setup(argv)
377
378 self.script.pub.create_channel_redirect("re1", "from")
379 argv = ["copy-image", "re1", "to", "test", "1"]
380 with self.assertRaises(script_base.ScriptError):
381 self.script.setup(argv)
382 self.script.pub.create_channel_alias("re2", "to")
383 argv = ["copy-image", "from", "re2", "test", "1"]
384 with self.assertRaises(script_base.ScriptError):
385 self.script.setup(argv)
386
387 self.script.pub.create_channel("redirects")
388 self.script.pub.create_per_device_channel_redirect(
389 "test", "redirects", "empty")
390 argv = ["copy-image", "redirects", "to", "test", "1"]
391 with self.assertRaises(script_base.ScriptError):
392 self.script.setup(argv)
393
394 argv = ["copy-image", "from", "redirects", "test", "1"]
395 with self.assertRaises(script_base.ScriptError):
396 self.script.setup(argv)
397
398 def test_copy_setup_invalid_tag(self):
399 """Check if setup fails on invalid tag arguments."""
400 argv = ["copy-image", "from", "to", "test", "1", "--tag=\"tag,tag\""]
401 with self.assertRaises(script_base.ScriptError):
402 self.script.setup(argv)
403
404 def test_copy_setup_invalid_device(self):
405 """Check if setup fails on invalid device selection."""
406 argv = ["copy-image", "from", "to", "none", "1"]
407 with self.assertRaises(script_base.ScriptError):
408 self.script.setup(argv)
409
410 self.script.pub.create_device("to", "to_only")
411 argv = ["copy-image", "from", "to", "to_only", "1"]
412 with self.assertRaises(script_base.ScriptError):
413 self.script.setup(argv)
414
415 self.script.pub.create_device("from", "from_only")
416 device = self.script.pub.get_device("from", "from_only")
417 device.create_image("full", 1, "abc", [self.image_file])
418 argv = ["copy-image", "from", "to", "from_only", "1"]
419 with self.assertRaises(script_base.ScriptError):
420 self.script.setup(argv)
421
422 def test_assign_new_version(self):
423 """Check if new version assignment logic is correct."""
424 self.script.device = self.script.pub.get_device("to", "test")
425
426 self.script.assign_new_version(1, False)
427 self.assertEqual(self.script.target_version, 2)
428 self.script.assign_new_version(4, False)
429 self.assertEqual(self.script.target_version, 2)
430 self.script.assign_new_version(4, True)
431 self.assertEqual(self.script.target_version, 4)
432 with self.assertRaises(script_base.ScriptError):
433 self.script.assign_new_version(1, True)
434
435
436class TagImageScriptTests(ImageScriptBaseTests):
437 scriptClass = script_tag_image.TagImageScript
438
439 def test_tag_setup_correct(self):
440 """Check if setup doesn't fail with good arguments."""
441 argv = ["tag-image", "to", "test", "1", "TAG1"]
442 self.script.setup(argv)
443
444 def test_tag_setup_invalid_channel(self):
445 """Check if setup fails on invalid channel arguments."""
446 argv = ["tag-image", "none", "test", "1", "TAG1"]
447 with self.assertRaises(script_base.ScriptError):
448 self.script.setup(argv)
449
450 self.script.pub.create_channel_alias("alias", "from")
451 argv = ["tag-image", "alias", "test", "1", "TAG1"]
452 with self.assertRaises(script_base.ScriptError):
453 self.script.setup(argv)
454
455 self.script.pub.create_channel_redirect("re", "from")
456 argv = ["tag-image", "re", "test", "1", "TAG1"]
457 with self.assertRaises(script_base.ScriptError):
458 self.script.setup(argv)
459
460 self.script.pub.create_channel("redirects")
461 self.script.pub.create_per_device_channel_redirect(
462 "test", "redirects", "empty")
463 argv = ["tag-image", "redirects", "test", "1", "TAG1"]
464 with self.assertRaises(script_base.ScriptError):
465 self.script.setup(argv)
466
467 def test_tag_setup_invalid_tag(self):
468 """Check if setup fails on invalid tag arguments."""
469 argv = ["tag-image", "to", "test", "1", "TAG1,TAG2"]
470 with self.assertRaises(script_base.ScriptError):
471 self.script.setup(argv)
472
473 def test_tag_setup_invalid_device(self):
474 """Check if setup fails on invalid device selection."""
475 argv = ["tag-image", "to", "none", "1", "TAG1"]
476 with self.assertRaises(script_base.ScriptError):
477 self.script.setup(argv)
478
479 def test_tag_execute_invalid_image(self):
480 """Make sure execute fails on invalid image scenarios."""
481 argv = ["tag-image", "empty", "test", "1", "TAG1"]
482 self.script.setup(argv)
483 with self.assertRaises(script_base.ScriptError):
484 self.script.execute()
485
486 argv = ["tag-image", "from", "test", "1", "TAG1"]
487 self.script.setup(argv)
488 with self.assertRaises(script_base.ScriptError):
489 self.script.execute()
490
491 argv = ["tag-image", "from", "test", "3", "TAG1"]
492 self.script.setup(argv)
493 with self.assertRaises(script_base.ScriptError):
494 self.script.execute()
495
496 def test_tag_execute(self):
497 """Check that we can correctly tag an image."""
498 device = self.script.pub.get_device("from", "test")
499 device.set_phased_percentage(2, 60)
500
501 argv = ["tag-image", "from", "test", "2", "TAG1"]
502 self.script.setup(argv)
503 self.script.execute()
504
505 image = tools.extract_image(self.script.device.list_images(), 3)
506 self.assertTrue(image)
507 self.assertIn("version_detail", image)
508 self.assertIn("tag=TAG1", image['version_detail'])
509 self.assertEqual(device.get_phased_percentage(3), 60)
510
511 previous = tools.extract_image(self.script.device.list_images(), 2)
512 self.assertIn("files", image)
513 for f in previous['files']:
514 self.assertIn("path", f)
515 if "version-" not in f['path']:
516 self.assertIn(f, image['files'])
517
518
519class SetPhasedPercentageScriptTests(ImageScriptBaseTests):
520 scriptClass = script_set_phased_percentage.SetPhasedPercentageScript
521
522 def test_phased_setup_correct(self):
523 """Check if setup doesn't fail with good arguments."""
524 argv = ["set-phased-percentage", "to", "test", "1", "50"]
525 self.script.setup(argv)
526
527 def test_phased_setup_invalid_percentage(self):
528 """Check if setup fails with an invalid phase percentage value."""
529 argv = ["set-phased-percentage", "to", "test", "1", "foo"]
530 with self.assertRaises(SystemExit):
531 self.script.setup(argv)
532
533 argv = ["set-phased-percentage", "to", "test", "1", "9000"]
534 with self.assertRaises(script_base.ScriptError):
535 self.script.setup(argv)
536
537 argv = ["set-phased-percentage", "to", "test", "1", "-10"]
538 with self.assertRaises(Exception):
539 self.script.setup(argv)
540
541 def test_phased_setup_invalid_channel(self):
542 """Check if setup fails on invalid channel arguments."""
543 argv = ["set-phased-percentage", "none", "test", "1", "50"]
544 with self.assertRaises(script_base.ScriptError):
545 self.script.setup(argv)
546
547 self.script.pub.create_channel_alias("alias", "from")
548 argv = ["set-phased-percentage", "alias", "test", "1", "50"]
549 with self.assertRaises(script_base.ScriptError):
550 self.script.setup(argv)
551
552 self.script.pub.create_channel_redirect("re", "from")
553 argv = ["set-phased-percentage", "re", "test", "1", "50"]
554 with self.assertRaises(script_base.ScriptError):
555 self.script.setup(argv)
556
557 self.script.pub.create_channel("redirects")
558 self.script.pub.create_per_device_channel_redirect(
559 "test", "redirects", "empty")
560 argv = ["set-phased-percentage", "redirects", "test", "1", "50"]
561 with self.assertRaises(script_base.ScriptError):
562 self.script.setup(argv)
563
564 def test_phased_execute(self):
565 """Check if we can change the percentage forwards and backwards."""
566 device = self.script.pub.get_device("to", "test")
567
568 argv = ["set-phased-percentage", "to", "test", "1", "50"]
569 self.script.setup(argv)
570 self.script.execute()
571 self.assertEqual(device.get_phased_percentage(1), 50)
572
573 argv = ["set-phased-percentage", "to", "test", "1", "60"]
574 self.script.setup(argv)
575 self.script.execute()
576 self.assertEqual(device.get_phased_percentage(1), 60)
577
578 argv = ["set-phased-percentage", "to", "test", "1", "15"]
579 self.script.setup(argv)
580 self.script.execute()
581 self.assertEqual(device.get_phased_percentage(1), 15)
582
583 def test_phased_execute_invalid_image(self):
584 """Check if we fail on trying to phase an older image."""
585 device = self.script.pub.get_device("to", "test")
586
587 argv = ["set-phased-percentage", "from", "test", "1", "50"]
588 with self.assertRaises(Exception):
589 self.script.setup(argv)
590 self.script.execute()
0591
=== modified file 'lib/systemimage/tests/test_tools.py'
--- lib/systemimage/tests/test_tools.py 2016-03-16 18:52:39 +0000
+++ lib/systemimage/tests/test_tools.py 2016-07-20 12:34:06 +0000
@@ -432,6 +432,7 @@
432432
433 def test_set_tag_on_version_detail(self):433 def test_set_tag_on_version_detail(self):
434 """Set a basic tag."""434 """Set a basic tag."""
435
435 version_detail_list = [436 version_detail_list = [
436 "device=20150821-736d127",437 "device=20150821-736d127",
437 "custom=20150925-901-35-40-vivid",438 "custom=20150925-901-35-40-vivid",
@@ -444,6 +445,7 @@
444445
445 def test_set_tag_on_version_detail_rewrite(self):446 def test_set_tag_on_version_detail_rewrite(self):
446 """Make sure tags can be rewritten."""447 """Make sure tags can be rewritten."""
448
447 version_detail_list = [449 version_detail_list = [
448 "device=20150821-736d127",450 "device=20150821-736d127",
449 "custom=20150925-901-35-40-vivid",451 "custom=20150925-901-35-40-vivid",
@@ -458,6 +460,7 @@
458460
459 def test_set_tag_on_version_detail_clear(self):461 def test_set_tag_on_version_detail_clear(self):
460 """Clear the tag."""462 """Clear the tag."""
463
461 version_detail_list = [464 version_detail_list = [
462 "device=20150821-736d127",465 "device=20150821-736d127",
463 "custom=20150925-901-35-40-vivid",466 "custom=20150925-901-35-40-vivid",
@@ -470,6 +473,7 @@
470473
471 def test_extract_files_and_version(self):474 def test_extract_files_and_version(self):
472 """Check if version_detail is correctly extracted"""475 """Check if version_detail is correctly extracted"""
476
473 os.mkdir(self.config.publish_path)477 os.mkdir(self.config.publish_path)
474478
475 version = 12479 version = 12
@@ -501,6 +505,7 @@
501 @unittest.skip("Current deltabase handling is broken")505 @unittest.skip("Current deltabase handling is broken")
502 def test_get_required_deltas(self):506 def test_get_required_deltas(self):
503 """Check if a proper list of valid deltabases is found."""507 """Check if a proper list of valid deltabases is found."""
508
504 config_path = os.path.join(self.temp_directory, "etc", "config")509 config_path = os.path.join(self.temp_directory, "etc", "config")
505 with open(config_path, "w+") as fd:510 with open(config_path, "w+") as fd:
506 fd.write("""[global]511 fd.write("""[global]
@@ -550,6 +555,42 @@
550 six.assertCountEqual(555 six.assertCountEqual(
551 self, [base_image1, base_image2], delta_base)556 self, [base_image1, base_image2], delta_base)
552557
558 def test_extract_image(self):
559 """Extracting single images from devices."""
560
561 config_path = os.path.join(self.temp_directory, "etc", "config")
562 with open(config_path, "w+") as fd:
563 fd.write("""[global]
564base_path = %s
565gpg_key_path = %s
566public_fqdn = system-image.example.net
567public_http_port = 880
568public_https_port = 8443
569""" % (self.temp_directory, os.path.join(os.getcwd(), "tools", "keys")))
570 test_config = config.Config(config_path)
571 os.makedirs(test_config.publish_path)
572
573 test_tree = tree.Tree(test_config)
574 test_tree.create_channel("testing")
575 test_tree.create_device("testing", "test")
576
577 image_file = os.path.join(self.config.publish_path, "test_file")
578 open(image_file, "w+").close()
579 gpg.sign_file(test_config, "image-signing", image_file)
580
581 device = test_tree.get_device("testing", "test")
582 device.create_image("full", 1, "abc", [image_file])
583 device.create_image("delta", 2, "abc", [image_file], base=1)
584 device.create_image("full", 4, "abc", [image_file])
585
586 image = tools.extract_image(device.list_images(), 1)
587 self.assertIsNotNone(image)
588 self.assertEqual(image['version'], 1)
589 self.assertIsNone(tools.extract_image(device.list_images(), 2))
590 image = tools.extract_image(device.list_images(), 4)
591 self.assertIsNotNone(image)
592 self.assertEqual(image['version'], 4)
593
553 def test_guess_file_compression(self):594 def test_guess_file_compression(self):
554 """Check if we can correctly guess compression algorithms."""595 """Check if we can correctly guess compression algorithms."""
555 test_string = "test-string"596 test_string = "test-string"
556597
=== modified file 'lib/systemimage/tests/test_tree.py'
--- lib/systemimage/tests/test_tree.py 2016-06-24 19:10:46 +0000
+++ lib/systemimage/tests/test_tree.py 2016-07-20 12:34:06 +0000
@@ -648,7 +648,7 @@
648648
649 @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING)649 @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING)
650 def test_index(self):650 def test_index(self):
651 device = tree.Device(self.config, self.temp_directory)651 device = tree.Device("test", self.config, self.temp_directory)
652652
653 # Check without files653 # Check without files
654 self.assertRaises(Exception, device.create_image, "full", 1234,654 self.assertRaises(Exception, device.create_image, "full", 1234,
655655
=== modified file 'lib/systemimage/tools.py'
--- lib/systemimage/tools.py 2016-03-16 18:52:39 +0000
+++ lib/systemimage/tools.py 2016-07-20 12:34:06 +0000
@@ -603,6 +603,21 @@
603 return version_detail603 return version_detail
604604
605605
606def extract_image(images, version):
607 """
608 Extracts the image file from an device image list for the selected
609 version number.
610 """
611
612 images = [image for image in images
613 if image['type'] == "full" and
614 image['version'] == version]
615 if not images:
616 return None
617
618 return images[0]
619
620
606def set_tag_on_version_detail(version_detail_list, tag):621def set_tag_on_version_detail(version_detail_list, tag):
607 """622 """
608 Append a tag to the version_detail array.623 Append a tag to the version_detail array.
609624
=== modified file 'lib/systemimage/tree.py'
--- lib/systemimage/tree.py 2016-06-24 19:10:46 +0000
+++ lib/systemimage/tree.py 2016-07-20 12:34:06 +0000
@@ -429,8 +429,8 @@
429 device_path = os.path.dirname(channels[channel_name]['devices']429 device_path = os.path.dirname(channels[channel_name]['devices']
430 [device_name]['index'])430 [device_name]['index'])
431431
432 return Device(self.config, os.path.normpath("%s/%s" % (self.path,432 return Device(device_name, self.config,
433 device_path)))433 os.path.normpath("%s/%s" % (self.path, device_path)))
434434
435 def hide_channel(self, channel_name):435 def hide_channel(self, channel_name):
436 """436 """
@@ -874,7 +874,8 @@
874874
875875
876class Device:876class Device:
877 def __init__(self, config, path):877 def __init__(self, name, config, path):
878 self.name = name
878 self.config = config879 self.config = config
879 self.pub_path = self.config.publish_path880 self.pub_path = self.config.publish_path
880 self.path = path881 self.path = path

Subscribers

People subscribed via source and target branches