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

Proposed by Łukasz Zemczak
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
Reviewer Review Type Date Requested Status
Barry Warsaw (community) Approve
Registry Administrators Pending
Review via email: mp+297904@code.launchpad.net

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.

To post a comment you must log in.
Revision history for this message
Barry Warsaw (barry) wrote :

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.

review: Approve
Revision history for this message
Ł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.

Revision history for this message
Ł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-download-manager to accept my local self-signed key (even when using the -self-signed-certs <DIR> parameter). But at least the channels.json modification parts are working and a fresh flash of a channel with the redirected device results in pulling in the right files and channel info (from the target channel).

Merging this in and then proceeding with testing on completely new test-related channels.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/systemimage/tests/test_tree.py'
--- lib/systemimage/tests/test_tree.py 2015-05-14 22:33:12 +0000
+++ lib/systemimage/tests/test_tree.py 2016-06-20 12:44:28 +0000
@@ -389,6 +389,103 @@
389 self.assertEqual(device.list_images(), target.list_images())389 self.assertEqual(device.list_images(), target.list_images())
390390
391 @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING)391 @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING)
392 def test_per_device_redirect(self):
393 """ """
394 test_tree = tree.Tree(self.config)
395
396 # Create some channels
397 test_tree.create_channel("parent")
398 test_tree.create_channel("redirect")
399
400 # Create the required devices
401 test_tree.create_device("parent", "device")
402 test_tree.create_device("parent", "other")
403 test_tree.create_device("redirect", "other")
404
405 # Test standard failure cases
406 self.assertRaises(KeyError,
407 test_tree.create_per_device_channel_redirect,
408 "device", "redirect1", "parent")
409 self.assertRaises(KeyError,
410 test_tree.create_per_device_channel_redirect,
411 "other", "redirect", "parent")
412 self.assertRaises(KeyError,
413 test_tree.create_per_device_channel_redirect,
414 "device", "redirect", "parent1")
415 self.assertRaises(KeyError,
416 test_tree.create_per_device_channel_redirect,
417 "device2", "redirect", "parent")
418
419 # Create the device channel redirect
420 test_tree.create_per_device_channel_redirect(
421 "device", "redirect", "parent")
422
423 # Publish an image
424
425 # # First file
426 first = os.path.join(self.config.publish_path, "parent/device/full")
427 open(first, "w+").close()
428 gpg.sign_file(self.config, "image-signing", first)
429
430 # # Second file
431 second = os.path.join(self.config.publish_path,
432 "parent/device/version-1234.tar.xz")
433
434 tools.generate_version_tarball(self.config, "parent", "test", "1234",
435 second.replace(".xz", ""))
436 tools.xz_compress(second.replace(".xz", ""))
437 os.remove(second.replace(".xz", ""))
438 gpg.sign_file(self.config, "image-signing", second)
439
440 with open(second.replace(".tar.xz", ".json"), "w+") as fd:
441 metadata = {}
442 metadata['channel.ini'] = {}
443 metadata['channel.ini']['version_detail'] = "test"
444 fd.write(json.dumps(metadata))
445 gpg.sign_file(self.config, "image-signing",
446 second.replace(".tar.xz", ".json"))
447
448 # # Adding the entry
449 device = test_tree.get_device("parent", "device")
450 device.create_image("full", 1234, "abc",
451 ["parent/device/full",
452 "parent/device/version-1234.tar.xz"])
453 device.set_phased_percentage(1234, 50)
454
455 # # Get the target
456 target = test_tree.get_device("redirect", "device")
457
458 # Confirm the fs layout
459 self.assertTrue(os.path.exists(os.path.join(
460 self.config.publish_path, "redirect")))
461 self.assertFalse(os.path.exists(os.path.join(
462 self.config.publish_path, "redirect", "device")))
463 self.assertTrue(os.path.exists(os.path.join(
464 self.config.publish_path, "parent", "device")))
465 self.assertEqual(device.list_images(), target.list_images())
466
467 # Check the channels index
468 channels = test_tree.list_channels()
469 self.assertIn("other", channels['redirect']['devices'])
470 self.assertEqual(
471 channels['redirect']['devices']['other']['index'],
472 "/redirect/other/index.json")
473 self.assertIn("device", channels['redirect']['devices'])
474 self.assertEqual(
475 channels['redirect']['devices']['device']['index'],
476 "/parent/device/index.json")
477 self.assertIn(
478 "redirect",
479 channels['redirect']['devices']['device'])
480
481 # Try removing a per-channel redirect
482 self.assertTrue(test_tree.remove_device("redirect", "device"))
483
484 # Confirm files are not removed
485 self.assertTrue(os.path.exists(os.path.join(
486 self.config.publish_path, "parent", "device", "index.json")))
487
488 @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING)
392 def test_redirect_alias(self):489 def test_redirect_alias(self):
393 # LP: #1455119 - a channel which is both an alias and a redirect is490 # LP: #1455119 - a channel which is both an alias and a redirect is
394 # culled from sync_aliases().491 # culled from sync_aliases().
@@ -412,6 +509,57 @@
412 self.assertFalse(mock.called)509 self.assertFalse(mock.called)
413510
414 @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING)511 @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING)
512 def test_cleanup_device_redirects(self):
513 test_tree = tree.Tree(self.config)
514
515 # Create some channels and devices
516 test_tree.create_channel("parent")
517 test_tree.create_channel("redirect1")
518 test_tree.create_channel("redirect2")
519 test_tree.create_channel("ignore")
520
521 test_tree.create_device("parent", "device")
522 test_tree.create_device("parent", "other")
523 test_tree.create_device("parent", "ignore")
524
525 test_tree.create_device("redirect1", "other")
526
527 # Create per-device redirects
528 test_tree.create_per_device_channel_redirect(
529 "device", "redirect1", "parent")
530 test_tree.create_per_device_channel_redirect(
531 "other", "redirect2", "parent")
532
533 # Check if removal of an unrelated channel does nothing
534 channels = test_tree.list_channels()
535 devices_before1 = dict(channels['redirect1']['devices'])
536 devices_before2 = dict(channels['redirect2']['devices'])
537 test_tree.remove_channel("ignore")
538 channels = test_tree.list_channels()
539 self.assertEqual(devices_before1, channels['redirect1']['devices'])
540 self.assertEqual(devices_before2, channels['redirect2']['devices'])
541
542 # Check if removal of an unrelated device does nothing
543 channels = test_tree.list_channels()
544 devices_before1 = dict(channels['redirect1']['devices'])
545 devices_before2 = dict(channels['redirect2']['devices'])
546 test_tree.remove_device("parent", "ignore")
547 channels = test_tree.list_channels()
548 self.assertEqual(devices_before1, channels['redirect1']['devices'])
549 self.assertEqual(devices_before2, channels['redirect2']['devices'])
550
551 # Check cleanup after single device removal
552 test_tree.remove_device("parent", "device")
553 channels = test_tree.list_channels()
554 self.assertNotIn("device", channels['redirect1']['devices'])
555 self.assertIn("other", channels['redirect1']['devices'])
556
557 # Check cleanup after whole channel removal
558 test_tree.remove_channel("parent")
559 channels = test_tree.list_channels()
560 self.assertNotIn("other", channels['redirect2']['devices'])
561
562 @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING)
415 def test_rename(self):563 def test_rename(self):
416 test_tree = tree.Tree(self.config)564 test_tree = tree.Tree(self.config)
417565
@@ -472,6 +620,33 @@
472 {'devices': {'device': {'index': '/test/new/device/index.json'}}})620 {'devices': {'device': {'index': '/test/new/device/index.json'}}})
473621
474 @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING)622 @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING)
623 def test_rename_with_redirects(self):
624 test_tree = tree.Tree(self.config)
625
626 # Create some channels
627 test_tree.create_channel("old")
628 test_tree.create_channel("redirect")
629
630 # Create basic devices
631 test_tree.create_device("old", "device")
632 test_tree.create_device("redirect", "other")
633
634 # Create a redirect
635 test_tree.create_per_device_channel_redirect(
636 "device", "redirect", "old")
637
638 # Rename
639 self.assertTrue(test_tree.rename_channel("old", "new"))
640
641 channels = test_tree.list_channels()
642 self.assertIn("device", channels['redirect']['devices'])
643 self.assertIn("other", channels['redirect']['devices'])
644 device = channels['redirect']['devices']['device']
645 self.assertIn("redirect", device)
646 self.assertEqual(device['redirect'], "new")
647 self.assertEqual(device['index'], "/new/device/index.json")
648
649 @unittest.skipUnless(HAS_TEST_KEYS, MISSING_KEYS_WARNING)
475 def test_index(self):650 def test_index(self):
476 device = tree.Device(self.config, self.temp_directory)651 device = tree.Device(self.config, self.temp_directory)
477652
478653
=== modified file 'lib/systemimage/tree.py'
--- lib/systemimage/tree.py 2015-10-27 15:25:38 +0000
+++ lib/systemimage/tree.py 2016-06-20 12:44:28 +0000
@@ -306,6 +306,37 @@
306306
307 return True307 return True
308308
309 def create_per_device_channel_redirect(self, device_name, channel_name,
310 target_name):
311 """
312 Creates a device-specific channel redirect, redirecting that device
313 to point to a different channel.
314 """
315
316 with channels_json(self.config, self.indexpath, True) as channels:
317 if channel_name not in channels:
318 raise KeyError("Couldn't find channel: %s" % channel_name)
319
320 if device_name in channels[channel_name]['devices']:
321 raise KeyError("Device already exists: %s" % device_name)
322
323 if target_name not in channels:
324 raise KeyError("Couldn't find target channel: %s" %
325 target_name)
326
327 if device_name not in channels[target_name]['devices']:
328 raise KeyError("Couldn't find device on target channel: "
329 "%s, %s" %
330 (target_name, device_name))
331
332 channels[channel_name]['devices'][device_name] = \
333 dict(channels[target_name]['devices'][device_name])
334
335 channels[channel_name]['devices'][device_name]['redirect'] = \
336 target_name
337
338 return True
339
309 def create_device(self, channel_name, device_name, keyring_path=None):340 def create_device(self, channel_name, device_name, keyring_path=None):
310 """341 """
311 Creates a new device entry in the tree.342 Creates a new device entry in the tree.
@@ -514,6 +545,9 @@
514 shutil.rmtree(channel_path)545 shutil.rmtree(channel_path)
515 channels.pop(channel_name)546 channels.pop(channel_name)
516547
548 # Remove all redirect device channels pointing at this channel
549 self.cleanup_device_redirects(channel_name)
550
517 return True551 return True
518552
519 def remove_device(self, channel_name, device_name):553 def remove_device(self, channel_name, device_name):
@@ -528,14 +562,21 @@
528 if device_name not in channels[channel_name]['devices']:562 if device_name not in channels[channel_name]['devices']:
529 raise KeyError("Couldn't find device: %s" % device_name)563 raise KeyError("Couldn't find device: %s" % device_name)
530564
531 device_path = os.path.join(self.path, channel_name, device_name)565 # Do not remove the device files for per-device redirects
532 if os.path.exists(device_path):566 device = channels[channel_name]['devices'][device_name]
533 shutil.rmtree(device_path)567 if "redirect" not in device:
568 device_path = os.path.join(
569 self.path, channel_name, device_name)
570 if os.path.exists(device_path):
571 shutil.rmtree(device_path)
534 channels[channel_name]['devices'].pop(device_name)572 channels[channel_name]['devices'].pop(device_name)
535573
536 self.sync_aliases(channel_name)574 self.sync_aliases(channel_name)
537 self.sync_redirects(channel_name)575 self.sync_redirects(channel_name)
538576
577 # Remove all redirect channels pointing at this device
578 self.cleanup_device_redirects(channel_name, device_name)
579
539 return True580 return True
540581
541 def rename_channel(self, old_name, new_name):582 def rename_channel(self, old_name, new_name):
@@ -581,6 +622,15 @@
581 .replace("/%s/" % old_name,622 .replace("/%s/" % old_name,
582 "/%s/" % new_name)623 "/%s/" % new_name)
583624
625 # Handle any device-specific channel redirects
626 for channel_name, channel in channels.items():
627 for device_name, device in channel['devices'].items():
628 if "redirect" in device and device['redirect'] == old_name:
629 index_path = "/%s/%s/index.json" % (new_name,
630 device_name)
631 device['redirect'] = new_name
632 device['index'] = index_path
633
584 channels.pop(old_name)634 channels.pop(old_name)
585635
586 return True636 return True
@@ -804,6 +854,24 @@
804854
805 return True855 return True
806856
857 def cleanup_device_redirects(self, channel_name,
858 redirect_device_name=None):
859 """
860 Cleanup any dangling device-specific channel redirects.
861 """
862
863 with channels_json(self.config, self.indexpath, True) as channels:
864 for target_name, channel in channels.items():
865 devices = dict(channel['devices'])
866 for device_name, device in devices.items():
867 if ("redirect" in device and
868 device['redirect'] == channel_name and
869 (not redirect_device_name or
870 redirect_device_name == device_name)):
871 channels[target_name]['devices'].pop(device_name)
872
873 return True
874
807875
808class Device:876class Device:
809 def __init__(self, config, path):877 def __init__(self, config, path):

Subscribers

People subscribed via source and target branches