Merge lp:~sil2100/ubuntu-system-image/server-per_device_redirect into lp:ubuntu-system-image/server
- server-per_device_redirect
- Merge into server
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 291 | ||||
Proposed branch: | lp:~sil2100/ubuntu-system-image/server-per_device_redirect | ||||
Merge into: | lp:ubuntu-system-image/server | ||||
Diff against target: |
317 lines (+246/-3) 2 files modified
lib/systemimage/tests/test_tree.py (+175/-0) lib/systemimage/tree.py (+71/-3) |
||||
To merge this branch: | bzr merge lp:~sil2100/ubuntu-system-image/server-per_device_redirect | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Barry Warsaw (community) | Approve | ||
Registry Administrators | Pending | ||
Review via email:
|
Commit message
Allow adding per-device channel redirects, allowing redirecting single devices out of a channel to a different one - similar to the normal channel redirects. This required some additional care-handling in multiple places.
Description of the change
Allow adding per-device channel redirects, allowing redirecting single devices out of a channel to a different one - similar to the normal channel redirects. This required some additional care-handling in multiple places.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Łukasz Zemczak (sil2100) wrote : | # |
Yeah, agreed on that! Will not merge this just yet, will test it locally on a device here first, then I'll deploy in production and do some more testing on a completely detached set of channels.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Łukasz Zemczak (sil2100) wrote : | # |
Ok, so I did as many tests as possible locally. Sadly I couldn't do an upgrade-test since I could not force ubuntu-
Merging this in and then proceeding with testing on completely new test-related channels.
Preview Diff
1 | === modified file 'lib/systemimage/tests/test_tree.py' | |||
2 | --- lib/systemimage/tests/test_tree.py 2015-05-14 22:33:12 +0000 | |||
3 | +++ lib/systemimage/tests/test_tree.py 2016-06-20 12:44:28 +0000 | |||
4 | @@ -389,6 +389,103 @@ | |||
5 | 389 | self.assertEqual(device.list_images(), target.list_images()) | 389 | self.assertEqual(device.list_images(), target.list_images()) |
6 | 390 | 390 | ||
7 | 391 | @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING) | 391 | @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING) |
8 | 392 | def test_per_device_redirect(self): | ||
9 | 393 | """ """ | ||
10 | 394 | test_tree = tree.Tree(self.config) | ||
11 | 395 | |||
12 | 396 | # Create some channels | ||
13 | 397 | test_tree.create_channel("parent") | ||
14 | 398 | test_tree.create_channel("redirect") | ||
15 | 399 | |||
16 | 400 | # Create the required devices | ||
17 | 401 | test_tree.create_device("parent", "device") | ||
18 | 402 | test_tree.create_device("parent", "other") | ||
19 | 403 | test_tree.create_device("redirect", "other") | ||
20 | 404 | |||
21 | 405 | # Test standard failure cases | ||
22 | 406 | self.assertRaises(KeyError, | ||
23 | 407 | test_tree.create_per_device_channel_redirect, | ||
24 | 408 | "device", "redirect1", "parent") | ||
25 | 409 | self.assertRaises(KeyError, | ||
26 | 410 | test_tree.create_per_device_channel_redirect, | ||
27 | 411 | "other", "redirect", "parent") | ||
28 | 412 | self.assertRaises(KeyError, | ||
29 | 413 | test_tree.create_per_device_channel_redirect, | ||
30 | 414 | "device", "redirect", "parent1") | ||
31 | 415 | self.assertRaises(KeyError, | ||
32 | 416 | test_tree.create_per_device_channel_redirect, | ||
33 | 417 | "device2", "redirect", "parent") | ||
34 | 418 | |||
35 | 419 | # Create the device channel redirect | ||
36 | 420 | test_tree.create_per_device_channel_redirect( | ||
37 | 421 | "device", "redirect", "parent") | ||
38 | 422 | |||
39 | 423 | # Publish an image | ||
40 | 424 | |||
41 | 425 | # # First file | ||
42 | 426 | first = os.path.join(self.config.publish_path, "parent/device/full") | ||
43 | 427 | open(first, "w+").close() | ||
44 | 428 | gpg.sign_file(self.config, "image-signing", first) | ||
45 | 429 | |||
46 | 430 | # # Second file | ||
47 | 431 | second = os.path.join(self.config.publish_path, | ||
48 | 432 | "parent/device/version-1234.tar.xz") | ||
49 | 433 | |||
50 | 434 | tools.generate_version_tarball(self.config, "parent", "test", "1234", | ||
51 | 435 | second.replace(".xz", "")) | ||
52 | 436 | tools.xz_compress(second.replace(".xz", "")) | ||
53 | 437 | os.remove(second.replace(".xz", "")) | ||
54 | 438 | gpg.sign_file(self.config, "image-signing", second) | ||
55 | 439 | |||
56 | 440 | with open(second.replace(".tar.xz", ".json"), "w+") as fd: | ||
57 | 441 | metadata = {} | ||
58 | 442 | metadata['channel.ini'] = {} | ||
59 | 443 | metadata['channel.ini']['version_detail'] = "test" | ||
60 | 444 | fd.write(json.dumps(metadata)) | ||
61 | 445 | gpg.sign_file(self.config, "image-signing", | ||
62 | 446 | second.replace(".tar.xz", ".json")) | ||
63 | 447 | |||
64 | 448 | # # Adding the entry | ||
65 | 449 | device = test_tree.get_device("parent", "device") | ||
66 | 450 | device.create_image("full", 1234, "abc", | ||
67 | 451 | ["parent/device/full", | ||
68 | 452 | "parent/device/version-1234.tar.xz"]) | ||
69 | 453 | device.set_phased_percentage(1234, 50) | ||
70 | 454 | |||
71 | 455 | # # Get the target | ||
72 | 456 | target = test_tree.get_device("redirect", "device") | ||
73 | 457 | |||
74 | 458 | # Confirm the fs layout | ||
75 | 459 | self.assertTrue(os.path.exists(os.path.join( | ||
76 | 460 | self.config.publish_path, "redirect"))) | ||
77 | 461 | self.assertFalse(os.path.exists(os.path.join( | ||
78 | 462 | self.config.publish_path, "redirect", "device"))) | ||
79 | 463 | self.assertTrue(os.path.exists(os.path.join( | ||
80 | 464 | self.config.publish_path, "parent", "device"))) | ||
81 | 465 | self.assertEqual(device.list_images(), target.list_images()) | ||
82 | 466 | |||
83 | 467 | # Check the channels index | ||
84 | 468 | channels = test_tree.list_channels() | ||
85 | 469 | self.assertIn("other", channels['redirect']['devices']) | ||
86 | 470 | self.assertEqual( | ||
87 | 471 | channels['redirect']['devices']['other']['index'], | ||
88 | 472 | "/redirect/other/index.json") | ||
89 | 473 | self.assertIn("device", channels['redirect']['devices']) | ||
90 | 474 | self.assertEqual( | ||
91 | 475 | channels['redirect']['devices']['device']['index'], | ||
92 | 476 | "/parent/device/index.json") | ||
93 | 477 | self.assertIn( | ||
94 | 478 | "redirect", | ||
95 | 479 | channels['redirect']['devices']['device']) | ||
96 | 480 | |||
97 | 481 | # Try removing a per-channel redirect | ||
98 | 482 | self.assertTrue(test_tree.remove_device("redirect", "device")) | ||
99 | 483 | |||
100 | 484 | # Confirm files are not removed | ||
101 | 485 | self.assertTrue(os.path.exists(os.path.join( | ||
102 | 486 | self.config.publish_path, "parent", "device", "index.json"))) | ||
103 | 487 | |||
104 | 488 | @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING) | ||
105 | 392 | def test_redirect_alias(self): | 489 | def test_redirect_alias(self): |
106 | 393 | # LP: #1455119 - a channel which is both an alias and a redirect is | 490 | # LP: #1455119 - a channel which is both an alias and a redirect is |
107 | 394 | # culled from sync_aliases(). | 491 | # culled from sync_aliases(). |
108 | @@ -412,6 +509,57 @@ | |||
109 | 412 | self.assertFalse(mock.called) | 509 | self.assertFalse(mock.called) |
110 | 413 | 510 | ||
111 | 414 | @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING) | 511 | @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING) |
112 | 512 | def test_cleanup_device_redirects(self): | ||
113 | 513 | test_tree = tree.Tree(self.config) | ||
114 | 514 | |||
115 | 515 | # Create some channels and devices | ||
116 | 516 | test_tree.create_channel("parent") | ||
117 | 517 | test_tree.create_channel("redirect1") | ||
118 | 518 | test_tree.create_channel("redirect2") | ||
119 | 519 | test_tree.create_channel("ignore") | ||
120 | 520 | |||
121 | 521 | test_tree.create_device("parent", "device") | ||
122 | 522 | test_tree.create_device("parent", "other") | ||
123 | 523 | test_tree.create_device("parent", "ignore") | ||
124 | 524 | |||
125 | 525 | test_tree.create_device("redirect1", "other") | ||
126 | 526 | |||
127 | 527 | # Create per-device redirects | ||
128 | 528 | test_tree.create_per_device_channel_redirect( | ||
129 | 529 | "device", "redirect1", "parent") | ||
130 | 530 | test_tree.create_per_device_channel_redirect( | ||
131 | 531 | "other", "redirect2", "parent") | ||
132 | 532 | |||
133 | 533 | # Check if removal of an unrelated channel does nothing | ||
134 | 534 | channels = test_tree.list_channels() | ||
135 | 535 | devices_before1 = dict(channels['redirect1']['devices']) | ||
136 | 536 | devices_before2 = dict(channels['redirect2']['devices']) | ||
137 | 537 | test_tree.remove_channel("ignore") | ||
138 | 538 | channels = test_tree.list_channels() | ||
139 | 539 | self.assertEqual(devices_before1, channels['redirect1']['devices']) | ||
140 | 540 | self.assertEqual(devices_before2, channels['redirect2']['devices']) | ||
141 | 541 | |||
142 | 542 | # Check if removal of an unrelated device does nothing | ||
143 | 543 | channels = test_tree.list_channels() | ||
144 | 544 | devices_before1 = dict(channels['redirect1']['devices']) | ||
145 | 545 | devices_before2 = dict(channels['redirect2']['devices']) | ||
146 | 546 | test_tree.remove_device("parent", "ignore") | ||
147 | 547 | channels = test_tree.list_channels() | ||
148 | 548 | self.assertEqual(devices_before1, channels['redirect1']['devices']) | ||
149 | 549 | self.assertEqual(devices_before2, channels['redirect2']['devices']) | ||
150 | 550 | |||
151 | 551 | # Check cleanup after single device removal | ||
152 | 552 | test_tree.remove_device("parent", "device") | ||
153 | 553 | channels = test_tree.list_channels() | ||
154 | 554 | self.assertNotIn("device", channels['redirect1']['devices']) | ||
155 | 555 | self.assertIn("other", channels['redirect1']['devices']) | ||
156 | 556 | |||
157 | 557 | # Check cleanup after whole channel removal | ||
158 | 558 | test_tree.remove_channel("parent") | ||
159 | 559 | channels = test_tree.list_channels() | ||
160 | 560 | self.assertNotIn("other", channels['redirect2']['devices']) | ||
161 | 561 | |||
162 | 562 | @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING) | ||
163 | 415 | def test_rename(self): | 563 | def test_rename(self): |
164 | 416 | test_tree = tree.Tree(self.config) | 564 | test_tree = tree.Tree(self.config) |
165 | 417 | 565 | ||
166 | @@ -472,6 +620,33 @@ | |||
167 | 472 | {'devices': {'device': {'index': '/test/new/device/index.json'}}}) | 620 | {'devices': {'device': {'index': '/test/new/device/index.json'}}}) |
168 | 473 | 621 | ||
169 | 474 | @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING) | 622 | @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING) |
170 | 623 | def test_rename_with_redirects(self): | ||
171 | 624 | test_tree = tree.Tree(self.config) | ||
172 | 625 | |||
173 | 626 | # Create some channels | ||
174 | 627 | test_tree.create_channel("old") | ||
175 | 628 | test_tree.create_channel("redirect") | ||
176 | 629 | |||
177 | 630 | # Create basic devices | ||
178 | 631 | test_tree.create_device("old", "device") | ||
179 | 632 | test_tree.create_device("redirect", "other") | ||
180 | 633 | |||
181 | 634 | # Create a redirect | ||
182 | 635 | test_tree.create_per_device_channel_redirect( | ||
183 | 636 | "device", "redirect", "old") | ||
184 | 637 | |||
185 | 638 | # Rename | ||
186 | 639 | self.assertTrue(test_tree.rename_channel("old", "new")) | ||
187 | 640 | |||
188 | 641 | channels = test_tree.list_channels() | ||
189 | 642 | self.assertIn("device", channels['redirect']['devices']) | ||
190 | 643 | self.assertIn("other", channels['redirect']['devices']) | ||
191 | 644 | device = channels['redirect']['devices']['device'] | ||
192 | 645 | self.assertIn("redirect", device) | ||
193 | 646 | self.assertEqual(device['redirect'], "new") | ||
194 | 647 | self.assertEqual(device['index'], "/new/device/index.json") | ||
195 | 648 | |||
196 | 649 | @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING) | ||
197 | 475 | def test_index(self): | 650 | def test_index(self): |
198 | 476 | device = tree.Device(self.config, self.temp_directory) | 651 | device = tree.Device(self.config, self.temp_directory) |
199 | 477 | 652 | ||
200 | 478 | 653 | ||
201 | === modified file 'lib/systemimage/tree.py' | |||
202 | --- lib/systemimage/tree.py 2015-10-27 15:25:38 +0000 | |||
203 | +++ lib/systemimage/tree.py 2016-06-20 12:44:28 +0000 | |||
204 | @@ -306,6 +306,37 @@ | |||
205 | 306 | 306 | ||
206 | 307 | return True | 307 | return True |
207 | 308 | 308 | ||
208 | 309 | def create_per_device_channel_redirect(self, device_name, channel_name, | ||
209 | 310 | target_name): | ||
210 | 311 | """ | ||
211 | 312 | Creates a device-specific channel redirect, redirecting that device | ||
212 | 313 | to point to a different channel. | ||
213 | 314 | """ | ||
214 | 315 | |||
215 | 316 | with channels_json(self.config, self.indexpath, True) as channels: | ||
216 | 317 | if channel_name not in channels: | ||
217 | 318 | raise KeyError("Couldn't find channel: %s" % channel_name) | ||
218 | 319 | |||
219 | 320 | if device_name in channels[channel_name]['devices']: | ||
220 | 321 | raise KeyError("Device already exists: %s" % device_name) | ||
221 | 322 | |||
222 | 323 | if target_name not in channels: | ||
223 | 324 | raise KeyError("Couldn't find target channel: %s" % | ||
224 | 325 | target_name) | ||
225 | 326 | |||
226 | 327 | if device_name not in channels[target_name]['devices']: | ||
227 | 328 | raise KeyError("Couldn't find device on target channel: " | ||
228 | 329 | "%s, %s" % | ||
229 | 330 | (target_name, device_name)) | ||
230 | 331 | |||
231 | 332 | channels[channel_name]['devices'][device_name] = \ | ||
232 | 333 | dict(channels[target_name]['devices'][device_name]) | ||
233 | 334 | |||
234 | 335 | channels[channel_name]['devices'][device_name]['redirect'] = \ | ||
235 | 336 | target_name | ||
236 | 337 | |||
237 | 338 | return True | ||
238 | 339 | |||
239 | 309 | def create_device(self, channel_name, device_name, keyring_path=None): | 340 | def create_device(self, channel_name, device_name, keyring_path=None): |
240 | 310 | """ | 341 | """ |
241 | 311 | Creates a new device entry in the tree. | 342 | Creates a new device entry in the tree. |
242 | @@ -514,6 +545,9 @@ | |||
243 | 514 | shutil.rmtree(channel_path) | 545 | shutil.rmtree(channel_path) |
244 | 515 | channels.pop(channel_name) | 546 | channels.pop(channel_name) |
245 | 516 | 547 | ||
246 | 548 | # Remove all redirect device channels pointing at this channel | ||
247 | 549 | self.cleanup_device_redirects(channel_name) | ||
248 | 550 | |||
249 | 517 | return True | 551 | return True |
250 | 518 | 552 | ||
251 | 519 | def remove_device(self, channel_name, device_name): | 553 | def remove_device(self, channel_name, device_name): |
252 | @@ -528,14 +562,21 @@ | |||
253 | 528 | if device_name not in channels[channel_name]['devices']: | 562 | if device_name not in channels[channel_name]['devices']: |
254 | 529 | raise KeyError("Couldn't find device: %s" % device_name) | 563 | raise KeyError("Couldn't find device: %s" % device_name) |
255 | 530 | 564 | ||
259 | 531 | device_path = os.path.join(self.path, channel_name, device_name) | 565 | # Do not remove the device files for per-device redirects |
260 | 532 | if os.path.exists(device_path): | 566 | device = channels[channel_name]['devices'][device_name] |
261 | 533 | shutil.rmtree(device_path) | 567 | if "redirect" not in device: |
262 | 568 | device_path = os.path.join( | ||
263 | 569 | self.path, channel_name, device_name) | ||
264 | 570 | if os.path.exists(device_path): | ||
265 | 571 | shutil.rmtree(device_path) | ||
266 | 534 | channels[channel_name]['devices'].pop(device_name) | 572 | channels[channel_name]['devices'].pop(device_name) |
267 | 535 | 573 | ||
268 | 536 | self.sync_aliases(channel_name) | 574 | self.sync_aliases(channel_name) |
269 | 537 | self.sync_redirects(channel_name) | 575 | self.sync_redirects(channel_name) |
270 | 538 | 576 | ||
271 | 577 | # Remove all redirect channels pointing at this device | ||
272 | 578 | self.cleanup_device_redirects(channel_name, device_name) | ||
273 | 579 | |||
274 | 539 | return True | 580 | return True |
275 | 540 | 581 | ||
276 | 541 | def rename_channel(self, old_name, new_name): | 582 | def rename_channel(self, old_name, new_name): |
277 | @@ -581,6 +622,15 @@ | |||
278 | 581 | .replace("/%s/" % old_name, | 622 | .replace("/%s/" % old_name, |
279 | 582 | "/%s/" % new_name) | 623 | "/%s/" % new_name) |
280 | 583 | 624 | ||
281 | 625 | # Handle any device-specific channel redirects | ||
282 | 626 | for channel_name, channel in channels.items(): | ||
283 | 627 | for device_name, device in channel['devices'].items(): | ||
284 | 628 | if "redirect" in device and device['redirect'] == old_name: | ||
285 | 629 | index_path = "/%s/%s/index.json" % (new_name, | ||
286 | 630 | device_name) | ||
287 | 631 | device['redirect'] = new_name | ||
288 | 632 | device['index'] = index_path | ||
289 | 633 | |||
290 | 584 | channels.pop(old_name) | 634 | channels.pop(old_name) |
291 | 585 | 635 | ||
292 | 586 | return True | 636 | return True |
293 | @@ -804,6 +854,24 @@ | |||
294 | 804 | 854 | ||
295 | 805 | return True | 855 | return True |
296 | 806 | 856 | ||
297 | 857 | def cleanup_device_redirects(self, channel_name, | ||
298 | 858 | redirect_device_name=None): | ||
299 | 859 | """ | ||
300 | 860 | Cleanup any dangling device-specific channel redirects. | ||
301 | 861 | """ | ||
302 | 862 | |||
303 | 863 | with channels_json(self.config, self.indexpath, True) as channels: | ||
304 | 864 | for target_name, channel in channels.items(): | ||
305 | 865 | devices = dict(channel['devices']) | ||
306 | 866 | for device_name, device in devices.items(): | ||
307 | 867 | if ("redirect" in device and | ||
308 | 868 | device['redirect'] == channel_name and | ||
309 | 869 | (not redirect_device_name or | ||
310 | 870 | redirect_device_name == device_name)): | ||
311 | 871 | channels[target_name]['devices'].pop(device_name) | ||
312 | 872 | |||
313 | 873 | return True | ||
314 | 874 | |||
315 | 807 | 875 | ||
316 | 808 | class Device: | 876 | class Device: |
317 | 809 | def __init__(self, config, path): | 877 | def __init__(self, config, path): |
LGTM, though I'd really like to do some real-world testing with a device to make sure that the redirect is doing what it's supposed to do on the phone.