← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/custom-uefi into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/custom-uefi into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1016594 in Launchpad itself: "UEFI image signing"
  https://bugs.launchpad.net/launchpad/+bug/1016594

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/custom-uefi/+merge/111626

== Summary ==

Bug 1016594: Ubuntu Engineering needs Launchpad to support signing UEFI boot loaders and publishing them in the Ubuntu archive.

== Proposed fix ==

Create a new custom upload format for UEFI.  This provides a reasonable place for us to sign images on publication.

Make the key/certificate location configurable in launchpad-lazr.conf, for fairly obvious reasons.  Filesystem locations may differ, we're only going to want the real key on production, and developer machines don't need a key at all.

Never auto-approve uploads containing UEFI files for signing, since signed images are going to be rather high-value.  This means that we don't have to invent some new permissioning system for who's allowed to upload UEFI images or which packages they're allowed to go in; instead, we can just rely on them all being held for manual approval.

== LOC Rationale ==

+311.  This feature is critical to UE, and I have 3039 lines of credit right now, so I'd very much like to use some of that.  Most of my other work at the moment is coming out LoC-negative.

== Tests ==

bin/test -vvct test_uefi -t test_ftparchive -t test_generate_extra_overrides -t test_nascentuploadfile -t test_custom_uploads_copier

== Demo and Q/A ==

Once sbsigntool has been installed on dogfood along with a test key, we should change efilinux to add a raw-uefi custom tarball and upload that to dogfood.  It should be extracted to /dists/quantal/main/uefi/efilinux-<ARCH>/<VERSION>/ and the .efi file signed by the test key, and a current -> <VERSION> symlink should be created.

== Lint ==

Pre-existing lint in *-lazr.conf files, which I don't think I should touch:

./configs/development/launchpad-lazr.conf
      64: Line exceeds 80 characters.
      93: Line exceeds 80 characters.
./lib/lp/services/config/schema-lazr.conf
     457: Line exceeds 80 characters.
    1050: Line exceeds 80 characters.
    1057: Line exceeds 80 characters.
    1595: Line exceeds 80 characters.
-- 
https://code.launchpad.net/~cjwatson/launchpad/custom-uefi/+merge/111626
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/custom-uefi into lp:launchpad.
=== modified file 'configs/development/launchpad-lazr.conf'
--- configs/development/launchpad-lazr.conf	2012-05-24 01:43:42 +0000
+++ configs/development/launchpad-lazr.conf	2012-06-22 15:32:21 +0000
@@ -7,6 +7,8 @@
 
 [archivepublisher]
 run_parts_location: none
+uefi_key_location: none
+uefi_cert_location: none
 
 [builddmaster]
 root: /var/tmp/builddmaster/

=== modified file 'lib/lp/archivepublisher/model/ftparchive.py'
--- lib/lp/archivepublisher/model/ftparchive.py	2012-03-27 11:08:12 +0000
+++ lib/lp/archivepublisher/model/ftparchive.py	2012-06-22 15:32:21 +0000
@@ -21,6 +21,7 @@
 from lp.services.database.decoratedresultset import DecoratedResultSet
 from lp.services.database.stormexpr import Concatenate
 from lp.services.librarian.model import LibraryFileAlias
+from lp.services.osutils import write_file
 from lp.services.webapp.interfaces import (
     DEFAULT_FLAVOR,
     IStoreSelector,
@@ -42,12 +43,6 @@
     return (os.path.basename(filename).split("_"))[0]
 
 
-def f_touch(*parts):
-    """Touch the file named by the arguments concatenated as a path."""
-    fname = os.path.join(*parts)
-    open(fname, "w").close()
-
-
 def safe_mkdir(path):
     """Ensures the path exists, creating it if it doesn't."""
     if not os.path.exists(path):
@@ -215,15 +210,15 @@
             (comp, "debian-installer"),
             (comp, "src"),
             ):
-            f_touch(os.path.join(
+            write_file(os.path.join(
                 self._config.overrideroot,
-                ".".join(("override", suite) + path)))
+                ".".join(("override", suite) + path)), "")
 
         # Create empty file lists.
         def touch_list(*parts):
-            f_touch(os.path.join(
+            write_file(os.path.join(
                 self._config.overrideroot,
-                "_".join((suite, ) + parts)))
+                "_".join((suite, ) + parts)), "")
         touch_list(comp, "source")
 
         arch_tags = [

=== modified file 'lib/lp/archivepublisher/tests/test_ftparchive.py'
--- lib/lp/archivepublisher/tests/test_ftparchive.py	2012-03-27 12:07:15 +0000
+++ lib/lp/archivepublisher/tests/test_ftparchive.py	2012-06-22 15:32:21 +0000
@@ -16,7 +16,6 @@
 from lp.archivepublisher.diskpool import DiskPool
 from lp.archivepublisher.model.ftparchive import (
     AptFTPArchiveFailure,
-    f_touch,
     FTPArchiveHandler,
     )
 from lp.archivepublisher.publishing import Publisher
@@ -27,10 +26,7 @@
     BufferLogger,
     DevNullLogger,
     )
-from lp.testing import (
-    TestCase,
-    TestCaseWithFactory,
-    )
+from lp.testing import TestCaseWithFactory
 from lp.testing.dbuser import switch_dbuser
 from lp.testing.layers import (
     LaunchpadZopelessLayer,
@@ -493,26 +489,3 @@
         distro = distroarchseries.distroseries.distribution
         fa = FTPArchiveHandler(DevNullLogger(), None, None, distro, None)
         self.assertRaises(AptFTPArchiveFailure, fa.runApt, "bogus-config")
-
-
-class TestFTouch(TestCase):
-    """Tests for f_touch function."""
-
-    def setUp(self):
-        TestCase.setUp(self)
-        self.test_folder = self.useTempDir()
-
-    def test_f_touch_new_file(self):
-        # Test f_touch correctly creates a new file.
-        f_touch(self.test_folder, "file_to_touch")
-        self.assertTrue(os.path.exists("%s/file_to_touch" % self.test_folder))
-
-    def test_f_touch_existing_file(self):
-        # Test f_touch truncates existing files.
-        with open("%s/file_to_truncate" % self.test_folder, "w") as f:
-            f.write("I'm some test contents")
-
-        f_touch(self.test_folder, "file_to_leave_alone")
-
-        with open("%s/file_to_leave_alone" % self.test_folder, "r") as f:
-            self.assertEqual("", f.read())

=== modified file 'lib/lp/archivepublisher/tests/test_generate_extra_overrides.py'
--- lib/lp/archivepublisher/tests/test_generate_extra_overrides.py	2012-05-21 19:03:16 +0000
+++ lib/lp/archivepublisher/tests/test_generate_extra_overrides.py	2012-06-22 15:32:21 +0000
@@ -29,6 +29,7 @@
 from lp.services.osutils import (
     ensure_directory_exists,
     open_for_writing,
+    write_file,
     )
 from lp.services.scripts.base import LaunchpadScriptFailure
 from lp.services.utils import file_exists
@@ -47,12 +48,6 @@
         return handle.read()
 
 
-def touch(path):
-    """Create an empty file at path."""
-    with open_for_writing(path, "a"):
-        pass
-
-
 class TestAtomicFile(TestCaseWithFactory):
     """Tests for the AtomicFile helper class."""
 
@@ -562,7 +557,7 @@
         other_file = "other-file"
         output = partial(os.path.join, self.script.config.germinateroot)
         for base in (seed_old_file, seed_new_file, other_file):
-            touch(output(base))
+            write_file(output(base), "")
         self.script.removeStaleOutputs(series_name, set([seed_new_file]))
         self.assertFalse(os.path.exists(output(seed_old_file)))
         self.assertTrue(os.path.exists(output(seed_new_file)))

=== added file 'lib/lp/archivepublisher/tests/test_uefi.py'
--- lib/lp/archivepublisher/tests/test_uefi.py	1970-01-01 00:00:00 +0000
+++ lib/lp/archivepublisher/tests/test_uefi.py	2012-06-22 15:32:21 +0000
@@ -0,0 +1,137 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Test UEFI custom uploads."""
+
+import os
+from textwrap import dedent
+
+from lp.archivepublisher.customupload import (
+    CustomUploadAlreadyExists,
+    CustomUploadBadUmask,
+    )
+from lp.archivepublisher.uefi import (
+    UefiConfigurationError,
+    UefiNothingToSign,
+    UefiUpload,
+    )
+from lp.services.config import config
+from lp.services.osutils import write_file
+from lp.services.tarfile_helpers import LaunchpadWriteTarFile
+from lp.testing import TestCase
+from lp.testing.fakemethod import FakeMethod
+
+
+class TestUefi(TestCase):
+
+    def setUp(self):
+        super(TestUefi, self).setUp()
+        self.temp_dir = self.makeTemporaryDirectory()
+        self.suite = "distroseries"
+        # CustomUpload.installFiles requires a umask of 022.
+        old_umask = os.umask(022)
+        self.addCleanup(os.umask, old_umask)
+
+    def pushConfiguration(self, key_location, cert_location):
+        uefi_config = dedent("""
+            [archivepublisher]
+            uefi_key_location: %s
+            uefi_cert_location: %s
+            """ % (key_location, cert_location))
+        config.push("uefi_config", uefi_config)
+        self.addCleanup(config.pop, "uefi_config")
+
+    def setUpKeyAndCert(self):
+        self.key_location = os.path.join(self.temp_dir, "test.key")
+        self.cert_location = os.path.join(self.temp_dir, "test.cert")
+        write_file(self.key_location, "")
+        write_file(self.cert_location, "")
+        self.pushConfiguration(self.key_location, self.cert_location)
+
+    def openArchive(self, loader_type, version, arch):
+        self.path = os.path.join(
+            self.temp_dir, "%s_%s_%s.tar.gz" % (loader_type, version, arch))
+        self.buffer = open(self.path, "wb")
+        self.archive = LaunchpadWriteTarFile(self.buffer)
+
+    def process(self):
+        self.archive.close()
+        self.buffer.close()
+        upload = UefiUpload()
+        upload.sign = FakeMethod()
+        upload.process(self.temp_dir, self.path, self.suite)
+        return upload
+
+    def getUefiPath(self, loader_type, arch):
+        return os.path.join(
+            self.temp_dir, "dists", self.suite, "main", "uefi",
+            "%s-%s" % (loader_type, arch))
+
+    def test_unconfigured(self):
+        # If there is no key/cert configuration, processing fails.
+        self.pushConfiguration("none", "none")
+        self.openArchive("test", "1.0", "amd64")
+        self.assertRaises(UefiConfigurationError, self.process)
+
+    def test_missing_key_and_cert(self):
+        # If the configured key/cert are missing, processing fails.
+        self.pushConfiguration(
+            os.path.join(self.temp_dir, "key"),
+            os.path.join(self.temp_dir, "cert"))
+        self.openArchive("test", "1.0", "amd64")
+        self.archive.add_file("1.0/empty.efi", "")
+        self.assertRaises(UefiConfigurationError, self.process)
+
+    def test_no_efi_files(self):
+        # Tarballs containing no *.efi files are rejected.
+        self.setUpKeyAndCert()
+        self.openArchive("empty", "1.0", "amd64")
+        self.archive.add_file("hello", "world")
+        self.assertRaises(UefiNothingToSign, self.process)
+
+    def test_already_exists(self):
+        # If the target directory already exists, processing fails.
+        self.setUpKeyAndCert()
+        self.openArchive("test", "1.0", "amd64")
+        self.archive.add_file("1.0/empty.efi", "")
+        os.makedirs(os.path.join(self.getUefiPath("test", "amd64"), "1.0"))
+        self.assertRaises(CustomUploadAlreadyExists, self.process)
+
+    def test_bad_umask(self):
+        # The umask must be 022 to avoid incorrect permissions.
+        self.setUpKeyAndCert()
+        self.openArchive("test", "1.0", "amd64")
+        self.archive.add_file("1.0/dir/file.efi", "foo")
+        os.umask(002)  # cleanup already handled by setUp
+        self.assertRaises(CustomUploadBadUmask, self.process)
+
+    def test_correct_signing_command(self):
+        # getSigningCommand returns the correct command.
+        self.setUpKeyAndCert()
+        upload = UefiUpload()
+        upload.setTargetDirectory(
+            self.temp_dir, "test_1.0_amd64.tar.gz", "distroseries")
+        expected_command = [
+            "sbsign", "--key", self.key_location, "--cert", self.cert_location,
+            "t.efi"]
+        self.assertEqual(expected_command, upload.getSigningCommand("t.efi"))
+
+    def test_signs_image(self):
+        # Each image in the tarball is signed.
+        self.setUpKeyAndCert()
+        self.openArchive("test", "1.0", "amd64")
+        self.archive.add_file("1.0/empty.efi", "")
+        upload = self.process()
+        self.assertEqual(1, upload.sign.call_count)
+        self.assertEqual(1, len(upload.sign.calls[0][0]))
+        self.assertEqual(
+            "empty.efi", os.path.basename(upload.sign.calls[0][0][0]))
+
+    def test_installed(self):
+        # Files in the tarball are installed correctly.
+        self.setUpKeyAndCert()
+        self.openArchive("test", "1.0", "amd64")
+        self.archive.add_file("1.0/empty.efi", "")
+        self.process()
+        self.assertTrue(os.path.exists(os.path.join(
+            self.getUefiPath("test", "amd64"), "1.0", "empty.efi")))

=== added file 'lib/lp/archivepublisher/uefi.py'
--- lib/lp/archivepublisher/uefi.py	1970-01-01 00:00:00 +0000
+++ lib/lp/archivepublisher/uefi.py	2012-06-22 15:32:21 +0000
@@ -0,0 +1,142 @@
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""The processing of UEFI boot loader images.
+
+UEFI Secure Boot requires boot loader images to be signed, and we want to
+have signed images in the archive so that they can be used for upgrades.
+This cannot be done on the build daemons because they are insufficiently
+secure to hold signing keys, so we sign them as a custom upload instead.
+"""
+
+__metaclass__ = type
+
+__all__ = [
+    "UefiUpload",
+    "process_uefi",
+    ]
+
+import os
+import subprocess
+
+from lp.archivepublisher.customupload import (
+    CustomUpload,
+    CustomUploadError,
+    )
+from lp.services.config import config
+from lp.services.osutils import remove_if_exists
+
+
+class UefiConfigurationError(CustomUploadError):
+    """No signing key location is configured."""
+    def __init__(self, message):
+        CustomUploadError.__init__(
+            self, "UEFI signing configuration error: %s" % message)
+
+
+class UefiNothingToSign(CustomUploadError):
+    """The tarball contained no *.efi files."""
+    def __init__(self, tarfile_path):
+        CustomUploadError.__init__(
+            self, "UEFI upload '%s' contained no *.efi files" % tarfile_path)
+
+
+class UefiUpload(CustomUpload):
+    """UEFI boot loader custom upload.
+
+    The filename should be something like:
+
+        <TYPE>_<VERSION>_<ARCH>.tar.gz
+
+    where:
+
+      * TYPE: loader type (e.g. 'efilinux');
+      * VERSION: encoded version;
+      * ARCH: targeted architecture tag (e.g. 'amd64').
+
+    The contents are extracted in the archive in the following path:
+
+        <ARCHIVE>/dists/<SUITE>/main/uefi/<TYPE>-<ARCH>/<VERSION>
+
+    A 'current' symbolic link points to the most recent version.  The
+    tarfile must contain at least one file matching the wildcard *.efi, and
+    any such files are signed using the key configured in
+    config.archivepublisher.uefi_key_location.
+    """
+    custom_type = "UEFI"
+
+    def setTargetDirectory(self, archive_root, tarfile_path, distroseries):
+        self.uefi_key_location = config.archivepublisher.uefi_key_location
+        self.uefi_cert_location = config.archivepublisher.uefi_cert_location
+        if self.uefi_key_location is None:
+            raise UefiConfigurationError("no key configured")
+        if not os.access(self.uefi_key_location, os.R_OK):
+            raise UefiConfigurationError(
+                "configured key %s not readable" % self.uefi_key_location)
+        if self.uefi_cert_location is None:
+            raise UefiConfigurationError("no certificate configured")
+        if not os.access(self.uefi_cert_location, os.R_OK):
+            raise UefiConfigurationError(
+                "configured certificate %s not readable" %
+                self.uefi_cert_location)
+
+        tarfile_base = os.path.basename(tarfile_path)
+        self.loader_type, self.version, self.arch = tarfile_base.split("_")
+        self.arch = self.arch.split(".")[0]
+
+        self.targetdir = os.path.join(
+            archive_root, "dists", distroseries, "main", "uefi",
+            "%s-%s" % (self.loader_type, self.arch))
+
+    def getSeriesKey(self, tarfile_path):
+        try:
+            loader_type, _, arch = os.path.basename(tarfile_path).split("_")
+            arch = arch.split(".")[0]
+            return (loader_type, arch)
+        except ValueError:
+            return None
+
+    def findEfiFilenames(self):
+        """Find all the *.efi files in an extracted tarball."""
+        for dirpath, dirnames, filenames in os.walk(self.tmpdir):
+            for filename in filenames:
+                if filename.endswith(".efi"):
+                    yield os.path.join(dirpath, filename)
+
+    def getSigningCommand(self, image):
+        """Return the command used to sign an image."""
+        return [
+            "sbsign", "--key", self.uefi_key_location,
+            "--cert", self.uefi_cert_location, image,
+            ]
+
+    def sign(self, image):
+        """Sign an image."""
+        subprocess.check_call(self.getSigningCommand(image))
+
+    def extract(self):
+        """Copy the custom upload to a temporary directory, and sign it.
+
+        No actual extraction is required.
+        """
+        super(UefiUpload, self).extract()
+        efi_filenames = list(self.findEfiFilenames())
+        if not efi_filenames:
+            raise UefiNothingToSign(self.tarfile_path)
+        for efi_filename in efi_filenames:
+            remove_if_exists("%s.signed" % efi_filename)
+            self.sign(efi_filename)
+
+    def shouldInstall(self, filename):
+        return filename.startswith("%s/" % self.version)
+
+
+def process_uefi(archive_root, tarfile_path, distroseries):
+    """Process a raw-uefi tarfile.
+
+    Unpacking it into the given archive for the given distroseries.
+    Raises CustomUploadError (or some subclass thereof) if anything goes
+    wrong.
+    """
+    upload = UefiUpload()
+    upload.process(archive_root, tarfile_path, distroseries)

=== modified file 'lib/lp/archiveuploader/nascentuploadfile.py'
--- lib/lp/archiveuploader/nascentuploadfile.py	2012-05-25 15:31:50 +0000
+++ lib/lp/archiveuploader/nascentuploadfile.py	2012-06-22 15:32:21 +0000
@@ -268,6 +268,7 @@
             PackageUploadCustomFormat.STATIC_TRANSLATIONS,
         'raw-meta-data':
             PackageUploadCustomFormat.META_DATA,
+        'raw-uefi': PackageUploadCustomFormat.UEFI,
         }
 
     @property
@@ -294,6 +295,13 @@
             restricted=self.policy.archive.private)
         return libraryfile
 
+    def autoApprove(self):
+        """Return whether this custom upload can be automatically approved."""
+        # UEFI uploads are signed, and must therefore be approved by a human.
+        if self.custom_type == PackageUploadCustomFormat.UEFI:
+            return False
+        return True
+
 
 class PackageUploadFile(NascentUploadFile):
     """Base class to model sources and binary files contained in a upload. """

=== modified file 'lib/lp/archiveuploader/tests/test_nascentuploadfile.py'
--- lib/lp/archiveuploader/tests/test_nascentuploadfile.py	2012-04-27 14:20:20 +0000
+++ lib/lp/archiveuploader/tests/test_nascentuploadfile.py	2012-06-22 15:32:21 +0000
@@ -25,6 +25,7 @@
 from lp.buildmaster.enums import BuildStatus
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.services.log.logger import BufferLogger
+from lp.services.osutils import write_file
 from lp.soyuz.enums import (
     PackagePublishingStatus,
     PackageUploadCustomFormat,
@@ -92,6 +93,18 @@
         self.assertEquals("bla.txt", libraryfile.filename)
         self.assertEquals("application/octet-stream", libraryfile.mimetype)
 
+    def test_debian_installer_auto_approved(self):
+        # debian-installer uploads are auto-approved.
+        uploadfile = self.createCustomUploadFile(
+            "bla.txt", "data", "main/raw-installer", "extra")
+        self.assertTrue(uploadfile.autoApprove())
+
+    def test_uefi_not_auto_approved(self):
+        # UEFI uploads are auto-approved.
+        uploadfile = self.createCustomUploadFile(
+            "bla.txt", "data", "main/raw-uefi", "extra")
+        self.assertFalse(uploadfile.autoApprove())
+
 
 class PackageUploadFileTestCase(NascentUploadFileTestCase):
     """Base class for all tests of classes deriving from PackageUploadFile."""
@@ -286,8 +299,7 @@
             "data.tar.%s" % data_format,
             ]
         for member in members:
-            with open(os.path.join(tempdir, member), "w") as f:
-                pass
+            write_file(os.path.join(tempdir, member), "")
         retcode = subprocess.call(
             ["ar", "rc", filename] + members, cwd=tempdir)
         self.assertEqual(0, retcode)

=== modified file 'lib/lp/archiveuploader/uploadpolicy.py'
--- lib/lp/archiveuploader/uploadpolicy.py	2012-06-19 03:26:57 +0000
+++ lib/lp/archiveuploader/uploadpolicy.py	2012-06-22 15:32:21 +0000
@@ -317,6 +317,14 @@
             raise AssertionError(
                 "Upload is not sourceful, binaryful or mixed.")
 
+    def autoApprove(self, upload):
+        """Check that all custom files in this upload can be auto-approved."""
+        if self.binaryful:
+            for custom_file in upload.changes.custom_files:
+                if not custom_file.autoApprove():
+                    return False
+        return True
+
 
 class SyncUploadPolicy(AbstractUploadPolicy):
     """This policy is invoked when processing sync uploads."""

=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf	2012-06-15 00:42:38 +0000
+++ lib/lp/services/config/schema-lazr.conf	2012-06-22 15:32:21 +0000
@@ -32,6 +32,18 @@
 # datatype: string
 run_parts_location: none
 
+# Location of the UEFI private key.  Absolute path, or "none" to refuse
+# signing.
+#
+# datatype: string
+uefi_key_location: none
+
+# Location of the UEFI certificate.  Absolute path, or "none" to refuse
+# signing.
+#
+# datatype: string
+uefi_cert_location: none
+
 
 [binaryfile_expire]
 dbuser: binaryfile-expire

=== modified file 'lib/lp/services/osutils.py'
--- lib/lp/services/osutils.py	2012-05-31 11:46:17 +0000
+++ lib/lp/services/osutils.py	2012-06-22 15:32:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Utilities for doing the sort of thing the os module does."""
@@ -15,6 +15,7 @@
     'remove_tree',
     'two_stage_kill',
     'until_no_eintr',
+    'write_file',
     ]
 
 from contextlib import contextmanager
@@ -207,6 +208,5 @@
 
 
 def write_file(path, content):
-    f = open(path, 'w')
-    f.write(content)
-    f.close()
+    with open_for_writing(path, 'w') as f:
+        f.write(content)

=== modified file 'lib/lp/soyuz/browser/queue.py'
--- lib/lp/soyuz/browser/queue.py	2012-01-01 02:58:52 +0000
+++ lib/lp/soyuz/browser/queue.py	2012-06-22 15:32:21 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Browser views for package queue."""
@@ -578,6 +578,7 @@
             (self.contains_installer, ("Installer", 'ubuntu-icon')),
             (self.contains_upgrader, ("Upgrader", 'ubuntu-icon')),
             (self.contains_ddtp, (ddtp, 'ubuntu-icon')),
+            (self.contains_uefi, ("Signed UEFI boot loader", 'ubuntu-icon')),
             ]
         return [
             self.composeIcon(*details)

=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml	2012-06-22 05:25:35 +0000
+++ lib/lp/soyuz/configure.zcml	2012-06-22 15:32:21 +0000
@@ -173,6 +173,7 @@
                 contains_installer
                 contains_upgrader
                 contains_ddtp
+                contains_uefi
                 displayname
                 displayarchs
                 displayversion

=== modified file 'lib/lp/soyuz/enums.py'
--- lib/lp/soyuz/enums.py	2012-05-28 15:12:00 +0000
+++ lib/lp/soyuz/enums.py	2012-06-22 15:32:21 +0000
@@ -474,6 +474,12 @@
         the Software Center.
         """)
 
+    UEFI = DBItem(6, """
+        uefi
+
+        A signed UEFI boot loader image.
+        """)
+
 
 class PackageUploadStatus(DBEnumeratedType):
     """Distro Release Queue Status

=== modified file 'lib/lp/soyuz/interfaces/queue.py'
--- lib/lp/soyuz/interfaces/queue.py	2012-05-30 08:50:50 +0000
+++ lib/lp/soyuz/interfaces/queue.py	2012-06-22 15:32:21 +0000
@@ -210,6 +210,8 @@
         "wheter or not this upload contains upgrader images")
     contains_ddtp = Attribute(
         "wheter or not this upload contains DDTP images")
+    contains_uefi = Attribute(
+        "whether or not this upload contains a signed UEFI boot loader image")
     isPPA = Attribute(
         "Return True if this PackageUpload is a PPA upload.")
     is_delayed_copy = Attribute(

=== modified file 'lib/lp/soyuz/model/queue.py'
--- lib/lp/soyuz/model/queue.py	2012-06-19 03:26:57 +0000
+++ lib/lp/soyuz/model/queue.py	2012-06-22 15:32:21 +0000
@@ -629,6 +629,12 @@
         return (PackageUploadCustomFormat.DDTP_TARBALL
                 in self._customFormats)
 
+    @cachedproperty
+    def contains_uefi(self):
+        """See `IPackageUpload`."""
+        return (PackageUploadCustomFormat.UEFI
+                in self._customFormats)
+
     @property
     def package_name(self):
         """See `IPackageUpload`."""
@@ -1377,6 +1383,14 @@
         self.libraryfilealias.open()
         copy_and_close(self.libraryfilealias, file_obj)
 
+    def publishUefi(self, logger=None):
+        """See `IPackageUploadCustom`."""
+        # XXX cprov 2005-03-03: We need to use the Zope Component Lookup
+        # to instantiate the object in question and avoid circular imports
+        from lp.archivepublisher.uefi import process_uefi
+
+        self._publishCustom(process_uefi)
+
     publisher_dispatch = {
         PackageUploadCustomFormat.DEBIAN_INSTALLER: publishDebianInstaller,
         PackageUploadCustomFormat.ROSETTA_TRANSLATIONS:
@@ -1386,6 +1400,7 @@
         PackageUploadCustomFormat.STATIC_TRANSLATIONS:
             publishStaticTranslations,
         PackageUploadCustomFormat.META_DATA: publishMetaData,
+        PackageUploadCustomFormat.UEFI: publishUefi,
         }
 
     # publisher_dispatch must have an entry for each value of

=== modified file 'lib/lp/soyuz/scripts/custom_uploads_copier.py'
--- lib/lp/soyuz/scripts/custom_uploads_copier.py	2012-05-30 10:25:43 +0000
+++ lib/lp/soyuz/scripts/custom_uploads_copier.py	2012-06-22 15:32:21 +0000
@@ -18,6 +18,7 @@
 
 from lp.archivepublisher.debian_installer import DebianInstallerUpload
 from lp.archivepublisher.dist_upgrader import DistUpgraderUpload
+from lp.archivepublisher.uefi import UefiUpload
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.services.database.bulk import load_referencing
 from lp.soyuz.enums import PackageUploadCustomFormat
@@ -40,6 +41,7 @@
     copyable_types = {
         PackageUploadCustomFormat.DEBIAN_INSTALLER: DebianInstallerUpload,
         PackageUploadCustomFormat.DIST_UPGRADER: DistUpgraderUpload,
+        PackageUploadCustomFormat.UEFI: UefiUpload,
         }
 
     def __init__(self, target_series):


Follow ups