← Back to team overview

ubuntu-server-iso-testing-dev team mailing list archive

lp:~jibel/ubuntu-server-iso-testing/download-latest-test-iso.fixes into lp:ubuntu-server-iso-testing

 

Jean-Baptiste Lallement has proposed merging lp:~jibel/ubuntu-server-iso-testing/download-latest-test-iso.fixes into lp:ubuntu-server-iso-testing.

Requested reviews:
  Ubuntu Server Iso Testing Developers (ubuntu-server-iso-testing-dev)
Related bugs:
  Bug #740853 in ubuntu-server-iso-testing: "download-latest-test-iso.py fails with KeyError when the image is not listed in SHA256SUMS "
  https://bugs.launchpad.net/ubuntu-server-iso-testing/+bug/740853
  Bug #751292 in ubuntu-server-iso-testing: "testing continues even if ISO is not available"
  https://bugs.launchpad.net/ubuntu-server-iso-testing/+bug/751292
  Bug #752321 in ubuntu-server-iso-testing: "download-latest-test-iso.py fails when launchpad is not available"
  https://bugs.launchpad.net/ubuntu-server-iso-testing/+bug/752321

For more details, see:
https://code.launchpad.net/~jibel/ubuntu-server-iso-testing/download-latest-test-iso.fixes/+merge/69967

Major diff due to code refactoring and some python style cleaning.

The logic is exactly the same the main differences being:
- Check if both ISO and Checksum files are available on the server before starting sync
- Validation of checksum _after_ download to be sure that what's in cache is really what is expected.
- More exception handling.
-- 
https://code.launchpad.net/~jibel/ubuntu-server-iso-testing/download-latest-test-iso.fixes/+merge/69967
Your team Ubuntu Server Iso Testing Developers is requested to review the proposed merge of lp:~jibel/ubuntu-server-iso-testing/download-latest-test-iso.fixes into lp:ubuntu-server-iso-testing.
=== modified file 'download-latest-test-iso.py'
--- download-latest-test-iso.py	2011-07-19 22:34:19 +0000
+++ download-latest-test-iso.py	2011-08-01 08:22:29 +0000
@@ -1,209 +1,238 @@
 #!/usr/bin/python
-# 
-# Copyright (C) 2010, Canonical Ltd (http://www.canonical.com/)
+#
+# Copyright (C) 2010-2011, Canonical Ltd (http://www.canonical.com/)
 #
 # This file is part of ubuntu-server-iso-testing.
-# 
-# ubuntu-server-iso-testing is free software: you can redistribute it 
-# and/or modify it under the terms of the GNU General Public License 
-# as published by the Free Software Foundation, either version 3 of 
+#
+# ubuntu-server-iso-testing is free software: you can redistribute it
+# and/or modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation, either version 3 of
 # the License, or (at your option) any later version.
-# 
-# ubuntu-server-iso-testing is distributed in the hope that it will 
-# be useful, but WITHOUT ANY WARRANTY; without even the implied warranty 
+#
+# ubuntu-server-iso-testing is distributed in the hope that it will
+# be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
 # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
-# 
+#
 # You should have received a copy of the GNU General Public License
-# along with ubuntu-server-iso-testing.  If not, see 
+# along with ubuntu-server-iso-testing.  If not, see
 # <http://www.gnu.org/licenses/>.
-# 
-
-from HTMLParser import HTMLParser
+#
 from lockfile import FileLock
 import os
 import optparse
 import sys
 import logging
-import urllib
-import urlparse
+import urllib2
 import subprocess
-import datetime
 import hashlib
+from time import sleep
 
 # Default options
 DEFAULT_VARIANT = 'server'
-DEFAULT_RELEASE = 'maverick'
+DEFAULT_RELEASE = 'oneiric'
 DEFAULT_ARCH = 'all'
-DEFAULT_FLAVOR= 'ubuntu-server'
+DEFAULT_FLAVOR = 'ubuntu-server'
 DEFAULT_ISOROOT = os.path.expanduser('~/isos')
 
+TIMEOUT = 1 # Timeout between retries when the image is not on the server
+RETRY_MAX = 4 # Number of retries
+
 # URLS
-BASE_URL="http://cdimage.ubuntu.com";
+BASE_URL = "http://cdimage.ubuntu.com";
 
 # List of 'all' architectures
-ALL_ARCHS =[ 'i386',  'amd64']
+ALL_ARCHS = [ 'i386',  'amd64']
+
+class HeadRequest(urllib2.Request):
+    """ Used to check to retrieve headers of a remote file """
+    def get_method(self):
+        return "HEAD"
 
 # Calculates the sha256 hash for a given file
-def sha256_for_file(f, block_size=2**20):
+def validate_sha256(fname, sha256_hash, block_size=2**20):
+    """ Validate checksum of file fname against sha256 hash """
+    logging.debug("Calculating hash for file '%s'", fname)
     sha256 = hashlib.sha256()
-    while True:
-        data = f.read(block_size)
-        if not data:
-            break
-        sha256.update(data)
-    return sha256.hexdigest()
-
-
-# HTML Parser for parsing ISO directory listing
-class ISOHTMLParser(HTMLParser):
-    in_a = False
-    builds = []
-    year = str(datetime.datetime.utcnow().year)
-    
-    def handle_starttag(self, tag, attrs):
-        if tag == 'a':
-            self.in_a = True
-        else:
-            self.in_a = False
-
-    def handle_endtag(self, tag):
-        if tag == 'a':
-            self.in_a = False
-        else:
-            self.in_a = True
-            
-    def handle_data(self,  data):
-        if self.in_a:
-            l_stripped = data.strip().rstrip('/')
-            if self.year in l_stripped:
-                self.builds.append(l_stripped)
-                
-    def getbuilds(self):
-        return self.builds
-        
-    def getlatestbuild(self):
-        self.builds.sort()
-        return self.builds.pop()
-
-usage="usage: %prog [options]"
-parser = optparse.OptionParser(usage=usage)
-parser.add_option("-d", "--debug", dest="debug", action="store_true",  default=False,
-                                help="enable debugging")
-parser.add_option("-r", "--release",  dest="release", default=DEFAULT_RELEASE, 
-                                help="release of Ubuntu to download (default=%s)" % DEFAULT_RELEASE)
-parser.add_option("-v", "--variant",  dest="variant", default=DEFAULT_VARIANT, 
-                                help="variant of Ubuntu to download (default=%s)" % DEFAULT_VARIANT)
-parser.add_option("-a", "--arch",  dest="arch", default=DEFAULT_ARCH, 
-                                help="arch of Ubuntu to download (default=%s)" % DEFAULT_ARCH)
-parser.add_option("-i", "--isoroot",  dest="isoroot", default=DEFAULT_ISOROOT, 
-                                help="location to store iso images (default=%s)" % DEFAULT_ISOROOT)
-parser.add_option("-f", "--flavor",  dest="flavor", default=DEFAULT_FLAVOR, 
-                                help="flavor of Ubuntu to download (default=%s)" % DEFAULT_FLAVOR)
-parser.add_option('-n', '--no-act', default=False, action='store_true',
-                  dest='no_act', help='compute everything but don\'t actually download')
-parser.add_option('-u', '--url', dest="base_url", default=BASE_URL, 
-                                help="Base URL for cdimage retrieval website (default=%s)" % BASE_URL)
-parser.add_option('-c', '--custom-url', dest="custom_url", default=False,
-                                help="Full URL for cdimage retrieval website")
-
-(options, args) = parser.parse_args()
-
-if options.debug:
-    logging.basicConfig(level=logging.DEBUG)
-else:
-    logging.basicConfig(level=logging.INFO)
-
-# Setup URLS
-ALTERNATE_URL= '%s/daily/' % options.base_url
-DESKTOP_URL='%s/daily-live/' % options.base_url
-SERVER_URL='%s/ubuntu-server/daily/' % options.base_url
-
-# Setup Parser to parse CDIMAGE pages.
-parser = ISOHTMLParser()
-l_url = SERVER_URL
-if options.variant == 'server':
-    l_url= SERVER_URL
-elif options.variant == 'desktop':
-    l_url = DESKTOP_URL
-elif options.variant == 'alternate':
-    l_url = ALTERNATE_URL
-
-# override url setting if custom url is provided
-if options.custom_url:
-    l_url = options.custom_url
-
-# Check options for architectures
-l_archs = []
-if options.arch == 'all':
-    l_archs = ALL_ARCHS
-else:
-    l_archs.append(options.arch)
-
-# Retrieve page and parse list
-fh = urllib.urlopen(l_url)
-page = fh.read()
-fh.close()
-
-# Parse the page for information
-parser.feed(page)
-logging.debug(parser.getbuilds())
-l_build =  parser.getlatestbuild()
-
-logging.info("Checking/Downloading ISO build %s for variant %s from %s" % (l_build, options.variant, l_url))
-
-# Download and store sha256 digest for required ISO images
-l_current_iso_digest = {}
-fh = urllib.urlopen(l_url + l_build + '/SHA256SUMS')
-# Process the file (will contain a number of entries
-for digest in fh:
-    (id, sep,  iso) = digest.partition(' ')
-    l_current_iso_digest[iso.rstrip('\n').lstrip('*')] = id
-fh.close()
-
-logging.debug(l_current_iso_digest)
-    
-# Check to see if version on disk is already the latest
-for arch in l_archs:
-    l_download = False
-    l_iso_name = options.release + '-' + options.variant + '-' + arch + '.iso'
-    lock = FileLock('/tmp/' + l_iso_name)
-    with lock:
-        l_iso_dir = options.flavor
-        l_iso_location = os.path.join(options.isoroot, l_iso_dir, l_iso_name)
-        # If iso does not exists then mark for retrieval
-        if os.path.exists(l_iso_location):
-            # Check to see if latest version
-            f = open(l_iso_location)
-            l_sha256 =  sha256_for_file(f)
-            f.close()
-            # If digests don't match then mark for retrieval
-            if l_current_iso_digest[l_iso_name] != l_sha256:
-                logging.debug("ISO is not the latest")
-                l_download = True
-            else:
-                logging.info("Required ISO version (%s,%s) already in cache" % (l_iso_name, l_build))
-        else:
-            # ISO does not exist need to download
-            logging.info("Downloading ISO (%s,%s) to local cache" % (l_iso_name, l_build))
-            l_download = True
-            
-        if l_download:
-            # Call dl-ubunut-test-iso with correct arguments
-            logging.debug("Downloading ISO as local cache is stale/empty")
-            cmd = ['dl-ubuntu-test-iso', '--variant=%s' % options.variant,  
-                            '--arch=%s' % arch, '--release=%s' % options.release,  
-                            '--build=%s' % l_build,'--isoroot=%s' % options.isoroot, '-P']
-            if options.flavor:
-                cmd.append('--flavor=%s' % options.flavor)
-
-            if options.no_act:
-                cmd.append('-n')
-
-            logging.debug("Cmd: %s" % (cmd))
-            subprocess.check_call(cmd)
-            # Write out build version to file for later use - needs to be refactored
-            f = open(l_iso_location + '.build-version', 'w')
-            f.write(l_build)
-            f.close()
-        
+    with open(fname) as f:
+        while True:
+            data = f.read(block_size)
+            if not data:
+                break
+            sha256.update(data)
+
+    l_sha256 = sha256.hexdigest()
+    logging.debug("Expected Checksum: '%s'", sha256_hash)
+    logging.debug("Local File Checksum: '%s'", l_sha256)
+    return sha256_hash == l_sha256
+
+def download_iso(isoroot, release, variant, arch, flavor=None, no_act=False):
+    """ Download an iso """
+    logging.info("Downloading ISO...")
+    cmd = ['dl-ubuntu-test-iso', '--variant=%s' % variant,
+                    '--arch=%s' % arch, '--release=%s' % release,
+                    '--build=current' ,'--isoroot=%s' % isoroot, '-P']
+    if flavor:
+        cmd.append('--flavor=%s' % flavor)
+
+    if no_act:
+        cmd.append('-n')
+
+    logging.debug("Cmd: %s", cmd)
+    try:
+        subprocess.check_call(cmd)
+    except subprocess.CalledProcessError, e:
+        logging.error(e)
+        return e.returncode
+    return 0
+
+def remote_file_exists(file_url):
+    """ Check if the remote file exists
+
+    Return file info on success false otherwise """
+    logging.debug("Checking if remote file exists: '%s'", file_url)
+    try:
+        res = urllib2.urlopen(HeadRequest(file_url))
+        logging.debug('URL Found:\n%s', res.info())
+        return res.info()
+    except (urllib2.HTTPError, urllib2.URLError), e:
+        logging.debug("Check failed: %s", e)
+        return False
+
+def main():
+    """ Main routine """
+    usage = "Usage: %prog [options]"
+    parser = optparse.OptionParser(usage=usage)
+    parser.add_option("-d", "--debug", dest="debug", action="store_true",
+                      default=False, help="enable debugging")
+    parser.add_option("-r", "--release",  dest="release",
+                      default=DEFAULT_RELEASE,
+                      help="release of Ubuntu to download (default=%s)"
+                      % DEFAULT_RELEASE)
+    parser.add_option("-v", "--variant",  dest="variant",
+                      default=DEFAULT_VARIANT,
+                      help="variant of Ubuntu to download (default=%s)"
+                      % DEFAULT_VARIANT)
+    parser.add_option("-a", "--arch",  dest="arch", default=DEFAULT_ARCH,
+                      help="arch of Ubuntu to download (default=%s)"
+                      % DEFAULT_ARCH)
+    parser.add_option("-i", "--isoroot",  dest="isoroot",
+                      default=DEFAULT_ISOROOT,
+                      help="location to store iso images (default=%s)"
+                      % DEFAULT_ISOROOT)
+    parser.add_option("-f", "--flavor",  dest="flavor", default=DEFAULT_FLAVOR,
+                      help="flavor of Ubuntu to download (default=%s)"
+                      % DEFAULT_FLAVOR)
+    parser.add_option('-n', '--no-act', default=False, action='store_true',
+                      dest='no_act',
+                      help='compute everything but don\'t actually download')
+    parser.add_option('-u', '--url', dest="base_url", default=BASE_URL,
+                      help="Base URL for cdimage retrieval website (default=%s)"
+                      % BASE_URL)
+
+    (options, args) = parser.parse_args()
+
+    if options.debug:
+        logging.basicConfig(level=logging.DEBUG)
+    else:
+        logging.basicConfig(level=logging.INFO)
+
+    # Setup URLS
+    release_part = options.release
+    if options.release == DEFAULT_RELEASE:
+        release_part = ''
+    if options.variant == 'desktop':
+        l_url = os.path.join(options.base_url, release_part, 'daily-live')
+    elif options.variant == 'alternate':
+        l_url = os.path.join(options.base_url, release_part, 'daily')
+    else:
+        # Default to server
+        l_url = os.path.join(options.base_url, 'ubuntu-server', release_part,
+                             'daily')
+
+    # Check options for architectures
+    l_archs = []
+    if options.arch == 'all':
+        l_archs = ALL_ARCHS
+    else:
+        l_archs.append(options.arch)
+
+    logging.info("Checking/Downloading current build for variant %s from %s",
+                 options.variant, l_url)
+
+    # Download and store sha256 digest for required ISO images
+    l_current_iso_digest = {}
+    sha256sum_url = os.path.join(l_url, 'current', 'SHA256SUMS')
+    try:
+        fh = urllib2.urlopen(sha256sum_url)
+    except urllib2.HTTPError, e:
+        logging.error("Failed to fetch URL '%s': %s . Aborting!",
+                      sha256sum_url, e)
+        sys.exit(1)
+
+    # Process the file (will contain a number of entries
+    for digest in fh:
+        (chksum, sep,  iso) = digest.partition(' *')
+        l_current_iso_digest[iso.rstrip('\n')] = chksum
+    fh.close()
+
+    # Check to see if version on disk is already the latest
+    for arch in l_archs:
+        l_iso_name = '-'.join((options.release, options.variant, arch)) + '.iso'
+        l_iso_url = os.path.join(l_url, 'current', l_iso_name)
+
+        # Check ISO availability on remote server
+        for retry in range(1, RETRY_MAX+1):
+            logging.debug('Try %d', retry)
+            if remote_file_exists(l_iso_url):
+                break
+
+            if retry == RETRY_MAX:
+                logging.info('Max number of retries reached. Aborting!')
+                sys.exit(1)
+            logging.info("Remote file not available. Retrying in %d minutes",
+                         l_iso_url, TIMEOUT)
+            sleep(TIMEOUT * 60)
+
+        try:
+            iso_sha256 = l_current_iso_digest[l_iso_name]
+        except KeyError:
+            logging.error("No digest found for image '%s'", l_iso_name)
+
+        lock = FileLock('/tmp/' + l_iso_name)
+        with lock:
+            rc = 0
+            l_iso_dir = options.flavor
+            l_iso_location = os.path.join(options.isoroot, l_iso_dir,
+                                          l_iso_name)
+            logging.debug("Checking state of local cache '%s'", l_iso_location)
+            # If iso does not exists then mark for retrieval
+            if os.path.exists(l_iso_location):
+                # Check to see if latest version
+                # If digests don't match then mark for retrieval
+                if not validate_sha256(l_iso_location, iso_sha256):
+                    logging.debug("ISO '%s' is not the latest", l_iso_name)
+                    rc = download_iso(options.isoroot, options.release,
+                                      options.variant, arch, options.flavor,
+                                      options.no_act)
+                else:
+                    logging.info("Required ISO version '%s' already in cache",
+                                 l_iso_name)
+            else:
+                # ISO does not exist need to download
+                logging.info("Downloading ISO '%s' to local cache", l_iso_name)
+                rc = download_iso(options.isoroot, options.release,
+                                  options.variant, arch, options.flavor,
+                                  options.no_act)
+
+            if rc != 0:
+                logging.error("Failed to download ISO. Aborting!")
+                sys.exit(1)
+            else:
+                # We downloaded something, let see if that is what we expected
+                if not validate_sha256(l_iso_location, iso_sha256):
+                    logging.info("Local file doesn't match checksum. Aborting!")
+                    sys.exit(1)
+
+if __name__ == '__main__':
+    main()


Follow ups