Bug 1239778 - release sanity: check en-US binaries. r=rail
authorMihai Tabara <mtabara@mozilla.com>
Fri, 26 Feb 2016 14:23:38 -0800
changeset 6530 aa178bedc23302a243118a73facdd5e98f0d176b
parent 6529 2c882403a743a9b687685265d769b3ed28626b45
child 6531 45b5fb1a99beb85bcd1565aa9aa0d8099d139125
push id4874
push usermtabara@mozilla.com
push dateFri, 26 Feb 2016 22:26:12 +0000
reviewersrail
bugs1239778
Bug 1239778 - release sanity: check en-US binaries. r=rail
buildfarm/release/release-runner.py
scripts/release/FAKE_KEY
--- a/buildfarm/release/release-runner.py
+++ b/buildfarm/release/release-runner.py
@@ -1,36 +1,45 @@
 #!/usr/bin/env python
 
 import site
 import time
 import logging
 import sys
 import os
+import subprocess
+import hashlib
+import functools
+import shutil
+import tempfile
+import requests
 from os import path
 from optparse import OptionParser
 from twisted.python.lockfile import FilesystemLock
 
 site.addsitedir(path.join(path.dirname(__file__), "../../lib/python"))
 
-import requests
 from kickoff.api import Releases, Release, ReleaseL10n
 from release.info import readBranchConfig
 from release.l10n import parsePlainL10nChangesets
 from release.versions import getAppVersion
 from releasetasks import make_task_graph
-from taskcluster import Scheduler, Index
+from taskcluster import Scheduler, Index, Queue
 from taskcluster.utils import slugId
 from util.hg import mercurial
 from util.retry import retry
 from util.file import load_config, get_config
 
 log = logging.getLogger(__name__)
 
 
+class SanityException(Exception):
+    pass
+
+
 # FIXME: the following function should be removed and we should use
 # next_version provided by ship-it
 def bump_version(version):
     """Bump last digit"""
     split_by = "."
     digit_index = 2
     if "b" in version:
         split_by = "b"
@@ -189,24 +198,153 @@ def get_en_US_config(release, branchConf
             "task_id": task["taskId"],
         }
 
     return {
         "platforms": platforms,
     }
 
 
-def validate_graph_kwargs(**kwargs):
+def validate_signatures(checksums, signature, dir_path, gpg_key_path):
+    try:
+        cmd = ['gpg', '--batch', '--homedir', dir_path, '--import',
+               gpg_key_path]
+        subprocess.check_call(cmd)
+        cmd = ['gpg', '--homedir', dir_path, '--verify', signature, checksums]
+        subprocess.check_call(cmd)
+    except subprocess.CalledProcessError:
+        log.exception("GPG signature check failed")
+        raise SanityException("GPG signature check failed")
+
+
+def parse_sha512(checksums):
+    # parse the checksums file and store all sha512 digests
+    _dict = dict()
+    with open(checksums, 'rb') as fd:
+        lines = fd.readlines()
+        for line in lines:
+            digest, alg, _, name = line.split()
+            if alg != 'sha512':
+                continue
+            _dict[os.path.basename(name)] = digest
+    return _dict
+
+
+def download_all_artifacts(queue, artifacts, task_id, dir_path):
+    failed_downloads = False
+
+    for artifact in artifacts:
+        name = os.path.basename(artifact['name'])
+        build_url = queue.buildSignedUrl(
+            'getLatestArtifact',
+            task_id,
+            artifact['name']
+        )
+        if name.endswith(".checksums"):
+            continue
+
+        log.debug('Downloading %s', name)
+        try:
+            r = requests.get(build_url, timeout=60)
+            r.raise_for_status()
+        except requests.HTTPError:
+            log.exception("Failed to download %s", name)
+            failed_downloads = True
+        else:
+            filepath = os.path.join(dir_path, name)
+            with open(filepath, 'wb') as fd:
+                for chunk in r.iter_content(1024):
+                    fd.write(chunk)
+
+    if failed_downloads:
+        raise SanityException('Downloading artifacts failed')
+
+
+def validate_checksums(_dict, dir_path):
+    for name in _dict.keys():
+        filepath = os.path.join(dir_path, name)
+        computed_hash = get_hash(filepath)
+        correct_hash = _dict[name]
+        if computed_hash != correct_hash:
+            log.error("failed to validate checksum for %s", name, exc_info=True)
+            raise SanityException("Failed to check digest for %s" % name)
+
+
+def sanitize_en_US_binary(queue, task_id, gpg_key_path):
+    # each platform en-US gets its own tempdir workground
+    tempdir = tempfile.mkdtemp()
+    log.debug('Temporary playground is %s', tempdir)
+
+    artifacts = queue.listLatestArtifacts(task_id)['artifacts']
+    # iterate in artifacts and grab checksums and its signature only
+    log.info("Retrieve the checksums file and its signature ...")
+    for artifact in artifacts:
+        name = os.path.basename(artifact['name'])
+        if not (name.endswith(".checksums") or name.endswith(".checksums.asc")):
+            continue
+        build_url = queue.buildSignedUrl(
+            'getLatestArtifact',
+            task_id,
+            artifact['name']
+        )
+        try:
+            r = requests.get(build_url, timeout=60)
+            r.raise_for_status()
+        except requests.HTTPError:
+            log.exception("Failed to download %s file", name)
+            raise SanityException("Failed to download %s file" % name)
+        filepath = os.path.join(tempdir, name)
+        with open(filepath, 'wb') as fd:
+            for chunk in r.iter_content(1024):
+                fd.write(chunk)
+        if name.endswith(".checksums.asc"):
+            signature = filepath
+        else:
+            checksums = filepath
+
+    # perform the signatures validation test
+    log.info("Attempt to validate signatures ...")
+    validate_signatures(checksums, signature, tempdir, gpg_key_path)
+    log.info("Signatures validated correctly!")
+
+    log.info("Download all artifacts ...")
+    download_all_artifacts(queue, artifacts, task_id, tempdir)
+    log.info("All downloads completed!")
+
+    log.info("Retrieve all sha512 from checksums file...")
+    sha512_dict = parse_sha512(checksums)
+    log.info("All sha512 digests retrieved")
+
+    log.info("Validating checksums for each artifact ...")
+    validate_checksums(sha512_dict, tempdir)
+    log.info("All checksums validated!")
+
+    # remove entire playground before moving forward
+    log.debug("Deleting the temporary playground ...")
+    shutil.rmtree(tempdir)
+
+
+def get_hash(path, hash_type="sha512"):
+    h = hashlib.new(hash_type)
+    with open(path, "rb") as f:
+        for chunk in iter(functools.partial(f.read, 4096), ''):
+            h.update(chunk)
+    return h.hexdigest()
+
+
+def validate_graph_kwargs(queue, gpg_key_path, **kwargs):
     # TODO: validate partials
     # TODO: validate l10n changesets
-    # TODO: go through release sanity for other validations to do
-    for url in kwargs.get("l10n_platforms", {}).values():
-        ret = requests.head(url, allow_redirects=True)
-        if not ret.ok():
-            log.error("en_us_binary url (%s) not accessible (got http %s)", url, ret.status_code)
+    platforms = kwargs.get('en_US_config', {}).get('platforms', {})
+    for platform in platforms.keys():
+        task_id = platforms.get(platform).get('task_id', {})
+        log.info('Performing release sanity for %s en-US binary', platform)
+        sanitize_en_US_binary(queue, task_id, gpg_key_path)
+
+    log.info("Release sanity for all en-US is now completed!")
 
 
 def main(options):
     log.info('Loading config from %s' % options.config)
     config = load_config(options.config)
 
     if config.getboolean('release-runner', 'verbose'):
         log_level = logging.DEBUG
@@ -241,22 +379,24 @@ def main(options):
         }
     }
     configs_workdir = 'buildbot-configs'
     balrog_username = get_config(config, "balrog", "username", None)
     balrog_password = get_config(config, "balrog", "password", None)
     extra_balrog_submitter_params = get_config(config, "balrog", "extra_balrog_submitter_params", None)
     beetmover_aws_access_key_id = get_config(config, "beetmover", "aws_access_key_id", None)
     beetmover_aws_secret_access_key = get_config(config, "beetmover", "aws_secret_access_key", None)
+    gpg_key_path = get_config(config, "signing", "gpg_key_path", None)
 
     # TODO: replace release sanity with direct checks of en-US and l10n revisions (and other things if needed)
 
     rr = ReleaseRunner(api_root=api_root, username=username, password=password)
     scheduler = Scheduler(tc_config)
     index = Index(tc_config)
+    queue = Queue(tc_config)
 
     # Main loop waits for new releases, processes them and exits.
     while True:
         try:
             log.debug('Fetching release requests')
             rr.get_release_requests()
             if rr.new_releases:
                 for release in rr.new_releases:
@@ -326,17 +466,17 @@ def main(options):
                 "postrelease_version_bump_enabled": branchConfig['postrelease_version_bump_enabled'],
                 "push_to_releases_enabled": True,
                 "push_to_releases_automatic": branchConfig['push_to_releases_automatic'],
                 "beetmover_candidates_bucket": branchConfig["beetmover_candidates_bucket"],
             }
             if extra_balrog_submitter_params:
                 kwargs["extra_balrog_submitter_params"] = extra_balrog_submitter_params
 
-            validate_graph_kwargs(**kwargs)
+            validate_graph_kwargs(queue, gpg_key_path, **kwargs)
 
             graph_id = slugId()
             graph = make_task_graph(**kwargs)
 
             rr.update_status(release, "Submitting task graph")
 
             log.info("Task graph generated!")
             import pprint
new file mode 100644
--- /dev/null
+++ b/scripts/release/FAKE_KEY
@@ -0,0 +1,37 @@
+pub   1024D/50FA58BC 2010-07-23
+      Key fingerprint = BD0B 4CB5 24D4 3A1A 6A5E  9306 448F 27B4 50FA 58BC
+uid                  releases (This is a fake gpg key.) <releases@mozilla.com>
+sig 3        50FA58BC 2010-07-23  releases (This is a fake gpg key.) <releases@mozilla.com>
+sub   2048g/45D02959 2010-07-23
+sig          50FA58BC 2010-07-23  releases (This is a fake gpg key.) <releases@mozilla.com>
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1.4.9 (Cygwin)
+
+mQGiBExJGWURBACPaLXmmpiOhdmahACTGen2BhZJNC8ZLo48i20RTSOsh9QMzdSU
+Vck6V9bhLJhZhAVFQwA1US+UsDYwuE5n4BtYi7ohygMbIQhVYJXyjgJnhK/TLnZS
+cXBPKASj2U4vKgluipnB3kxWRTbinRW7A2u9Zf6ByG0UOftTFfeuh9Mz3wCgz9SS
+iazyZ7ZbeV2WYNqSv4geJzED/j6S3b5soFrGVF526eL4IfTjnGCQmNYTn4qHCRmX
+fn267CNBuZO6EBtYaD0I3+0EJRQGssNpqbah52JGPeSNwpMgenCX2PJeH3dRdJZ6
+3/quwY9pvUSLzCK6w++1k4oCWm0mbBIjK1YaE9RlAmpMf+Gw06RVE8VjJ9WEpK++
+vY/LA/4p9s7aDIgoilE6Yp+UQuuHGWj74MsBfrFGwdf/xQCMQXj7AfigGE3MVBOl
+Z5zfJCCiNyq2ShHnF1o/sRperl9KxuiHkT3o5iBtiLhmrVV5NqZqtEJzvZOg/ggE
+zrSq57kvR1uRslm8TJCFKcwQxg6iG++RNSSbrq5e7wvwCDwEIbQ5cmVsZWFzZXMg
+KFRoaXMgaXMgYSBmYWtlIGdwZyBrZXkuKSA8cmVsZWFzZXNAbW96aWxsYS5jb20+
+iGAEExECACAFAkxJGWUCGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRBEjye0
+UPpYvLzhAJwMCw5t2TL2hWa397/kq9BH2+cdwACgjV0joKYlBm8xYMkabYUjIq14
+F6G5Ag0ETEkZZRAIANpkO4CaaPVmwHtR8fU5GW0OaIcsNu9NA3KLZE0OZuN9T3UT
+02mvyBGPQ6GgDMkYOQazLBY9gmlcVmH33Qrn+5XOYMPCAyZM4J/RQIUvSJ9HlxNy
+DSyfj4Q94BpseGnxVhB4hdzyGgo0RFgmGtoFrdMxenF/TIKDhLY6Ige2XmBGZ8/Q
+1U4AafGzVWEgLjxy8FHHY2cH/yOS3ZpDS3Dj0sBoALUNrl13MYaUvwS6+uSX0OiB
+nl2KgnFI34H0fLcyCwMFNokM8BEHg1Wd6H3+Vnc1l7zxlmo9fqAXpYiyOX2+TKJX
+rJ5iHN5kC0gHGn+LTSohGuQ7vAUTcdMAmtKKvlsAAwUH/A3D8kfYzwnewEVX+MM/
+7tL5Ahsg1GOhueZQSV/I2aai2ks90rnj1X/zimxKXgI0nw0TBsIGrnKDbhdta2c0
+Vp5WxbRl2CG+zmkaKHGirjhh1dwqJeDUXifFAfDfV+Ohwcu21r8tkyqCWc6AkyQA
+WbYTN/+G8R9cJVzctFw17n9CqpM+LO4htcDiQ/1ltV6cSWgFIfIoPR0SXx4LPXa6
+H16H9YyaCq6sCJrP8fqw7RWWLgVM00H1cQVJCL4ZSUWX8NJL3+IGgh6znwkkOiiT
+XlT54kr8LPzlZfIWysI66RAssOAsYbt28owCliVwIBedyRHQ5lZuzXjkXd6uFoMe
+8fGISQQYEQIACQUCTEkZZQIbDAAKCRBEjye0UPpYvGXuAKDDSN9UVeJJPKop61H0
+opWXnb8bVACghdENVmVzAYYrsdQ4uW+scxmGMHw=
+=mqJn
+-----END PGP PUBLIC KEY BLOCK-----
\ No newline at end of file