#!/usr/bin/python2.6 # pylint: disable-msg=C0301,C0103,C0111,W0704 import subprocess import sys import os from optparse import OptionParser import univention.config_registry as ucr import tempfile import shutil import glob import datetime import yaml import atexit configRegistry = ucr.ConfigRegistry() configRegistry.load() sys.path.append("/usr/lib/repo-ng") import repo_lib as rp # All the package logic import gnupg os.umask(0002) # u+rwx,g+rwx,o+rx # This file contains the layout of a single errata announcement mail. It is updated through the repo-ng # source package mail_template = "/usr/share/repo-ng/templates/errata-template.txt" # The key used to sign the errata mail key_id = "F510AADA" key_file = "/etc/archive-keys/security-key.txt" key_id = "A7353B90" # FIXME: PHahn private test key on dimma key_file = "/home/phahn/univention" # This file contains the layout of a single errata HTML page. It is updated through the repo-ng # source package html_template = "/usr/share/repo-ng/templates/errata-template.html" # This file contains the layout for the overview page linking to all errata HTML page. It is updated through the repo-ng # source package overview_template = "/usr/share/repo-ng/templates/overview-template.html" TYPES = { 'bugfix': 'Bugfix', 'sec': 'Security bugfix', } # univention-errata-level needs to be auto-generated, this dict provides a mapping to the respective patch: ERRATALEVELPATCH = { '3.1': 'univention-errata-level/3.1-0-0-ucs/1.0.0-1-errata3.1-0/bump-version.patch', } # univention-errata-level needs to be auto-generated, this dict provides a mapping to the respective patch: ERRATABUILDSCRIPT = { '3.1': 'b31-scope', } # This directory holds the announcement mails, which are about to be sent out once passed through QA mail_staging_dir = "/var/univention/buildsystem2/errata/staging/" # This is the internal version of the errata website. It is mirrored to the public forge site once # passed through QA website_dir = "/mnt/omar/vmwares/mirror/ftp/download/errata" def remove_file(fn): try: os.remove( fn ) except OSError: pass # print 'ERROR: Deleting %s failed' % fn # print str(e) def generate_release_file(mirror_base, destname, keyfile, keyid): """ Create a signed Release file. mirror_base : The base directory of the mirror, e.g. /var/univention/buildsystem2/test_mirror/ftp/3.0 destname : destination directory, e.g. 3.0-0 or errata16 keyfile : A text file containing the passphrase of the archive key keyid : The archive, with which the Release file is signed """ print 'Generating Release files...' for state in ('maintained', 'unmaintained'): for arch in ( 'all', 'i386', 'amd64' ): try: p = os.path.join(mirror_base, state, destname, arch) os.chdir(p) except OSError: print >> sys.stderr, 'Skipping failed cd %s' % (p,) continue # Delete old Release file because apt-ftparchive includes it into the new Release file. remove_file('Release') f = open('Release.tmp', 'w') try: rv = subprocess.call(("apt-ftparchive", "-o", "APT::FTPArchive::Release::Origin=Univention", "-o", "APT::FTPArchive::Release::Label=Univention", "-o", "APT::FTPArchive::Release::Version=%s" % (destname), "-o", "APT::FTPArchive::Release::Codename=%s/%s" % (destname, arch), "release", "."), stdout=f) finally: f.close() if rv != 0: print >> sys.stderr, 'Failed to create %s/Release' % (p,) remove_file('Release.tmp') continue shutil.move('Release.tmp', 'Release') # Variante mit Debug: rv = subprocess.call(("repo-ng-sign-release-file", "-d", "-p", keyfile, "-k", keyid)) rv = subprocess.call(("repo-ng-sign-release-file", "-p", keyfile, "-k", keyid)) if rv != 0: print >> sys.stderr, 'Failed to sign %s/Release' % (p,) if os.path.exists('Release.gpg'): remove_file('Release.gpg') def append_template_overview(): o = open(overview_data_file, "a") if errata_31_style: print >> o, " ".join((errata_id, errata_type, adv['src'], str(adv['version']))) else: print >> o, " ".join((errata_id, errata_type, adv['src'])) o.close() def generate_errata_tgz(mirror_base, errata_id, arch): """Generate a .tar.gz file containing the updated packages and Packages files and return the name of the .tar.gz file.""" tgz_name = tempfile.mktemp('_%s.tar.gz' % (errata_id,)) tmpdir = tempfile.mkdtemp() try: for part in ('maintained', 'unmaintained'): for arch2 in ('all', arch): dst_dir = os.path.join(tmpdir, opt.ucsrelease, part, str(errata_id), arch2) os.makedirs(dst_dir) src_dir = os.path.join(mirror_base, part, str(errata_id), arch2) if os.path.exists(src_dir): subprocess.call(("cp", "-aT", src_dir, dst_dir)) cmd = ("tar", "cfzC", tgz_name, tmpdir, opt.ucsrelease) subprocess.call(cmd) finally: shutil.rmtree(tmpdir) return tgz_name def generate_errata_mail(): i = open(mail_template, "r") mail_file = os.path.join(mail_staging_dir, '%s-%s.txt' % (opt.ucsrelease, errata_id)) o = open(mail_file, "w") print "Generating errata mail in staging directory:", mail_file for a in i.readlines(): a = a.replace("SOURCEPACKAGE", adv['src']) a = a.replace("UCSVERSION", str(opt.ucsrelease)) a = a.replace("REFERENCE", adv['ref']) a = a.replace("ERRID", errata_id) a = a.replace("FIXED", adv['fix']) a = a.replace("DESC", adv['desc']) if adv['note']: a = a.replace("NOTE", adv['note']) else: a = a.replace("NOTE", "") o.write(a) o.close() gpg = gnupg.GPG(gpgbinary='/usr/bin/gpg') o = open(mail_file, "rb") signature = gpg.sign_file(o, clearsign=True, keyid=key_id, passphrase=sec_passphrase) o.close() if signature: sig_file = open(mail_file + ".asc", 'w') sig_file.write(str(signature)) sig_file.close() else: print >> sys.stderr, "Failed to generate advisory signature" sys.exit(1) def send_errata_test_mail(): """Send test advisory mail.""" adv_file = os.path.join(mail_staging_dir, '%s-%s.txt.asc' % (opt.ucsrelease, errata_id)) cmd = ('/usr/bin/errata-mailing', '-e', str(next_id), '-v', "Univention Corporate Server", '-t', adv_file, '-l', '/var/univention/buildsystem2/errata/mail/test.csv', '--really') print "Sending test advisory mail" subprocess.call(cmd) def parse_debs_from_changes_files(changes_file): pkgs = set() try: f = open(changes_file, "r") except IOError: print >> sys.stderr, "Failed to open changes file '%s'" % (changes_file,) sys.exit(1) for i in f.readlines(): l = i.replace("\n", "") if l.endswith(".deb"): pkgs.add(l.split(" ")[-1]) f.close() return pkgs def copy_single_source_to_mirror(package, srcdir, targetdir): src = os.listdir(srcdir) for j in src: if j.startswith(package): if j.endswith(".dsc") or j.endswith(".gz") or j.endswith(".bz2") or j.endswith(".xz"): if not j.endswith("Sources.gz"): print "Copying", j, "to source mirror" shutil.copy(os.path.join(srcdir, j), targetdir) def generate_errata_html(): """Generate errata web page.""" if errata_31_style: errata_html_file = os.path.join(website_dir, '%s-%s.html' % (opt.ucsrelease, errata_id)) else: errata_html_file = os.path.join(website_dir, errata_id + ".html") print "Generating errata web page:", errata_html_file i = open(html_template, "r") o = open(errata_html_file, "w") errata_header = "UCS " + opt.ucsrelease + " erratum" ref_blob = " \n No external references \n \n" if adv['ref'].count("CVE") > 0: ref_blob = "" errata_header = errata_header + " (security)" for a in adv['ref'].split(","): ref_blob += '\n' ref_blob += 'CVE ID' ref_blob += ' ' + a + '\n' ref_blob += '\n' else: ref_blob = "" errata_header = errata_header + " (bugfix)" for a in adv['ref'].split(","): bugnum = a.replace("Bug ", "") ref_blob += '\n' ref_blob += 'UCS Bug number' ref_blob += ' UCS bug ' + bugnum + '\n' ref_blob += '\n' currdate = datetime.date.isoformat(datetime.datetime.now()) for a in i.readlines(): a = a.replace("ERRATAHEADER", errata_header) a = a.replace("ERRATAID", errata_id) a = a.replace("CURRENTDATE", currdate) a = a.replace("SRCPKGNAME", adv['src']) a = a.replace("FIXEDVERSION", adv['fix']) a = a.replace("DESCRIPTION", adv['desc']) a = a.replace("REFERENCEBLOB", ref_blob) a = a.replace("NOTEADVISORY", adv['note'] or "") o.write(a) o.close() def generate_html_overview(): """Generate errata overview web page.""" print "Generating errata overview web page:", overview_html_file errata_header = "Security and bugfix errata for Univention Corporate Server " + opt.ucsrelease ref_blob = "" data_file = open(overview_data_file, "r") for line in data_file: if errata_31_style: eid, typ, src, version = line.strip().split(" ", 3) else: eid, typ, src = line.strip().split(" ", 2) version_blob = str(opt.ucsrelease) if errata_31_style: if int(version) == 0: version_blob = "%s (applicable to all releases)" % (opt.ucsrelease,) else: version_blob = "%s (applicable to %s-%s onwards)" % (opt.ucsrelease, opt.ucsrelease, version) if errata_31_style: ref_blob += '%s for UCS %s\n' % (opt.ucsrelease, eid, eid, opt.ucsrelease) else: ref_blob += '%s for UCS %s\n' % (eid, eid, opt.ucsrelease) ref_blob += '%s for UCS %s (%s)
\n' % (TYPES.get(typ, typ), opt.ucsrelease, src) data_file.close() template_file = open(overview_template, "r") overview_file = open(overview_html_file, "w") for a in template_file.readlines(): a = a.replace("OVERVIEWHEADER", errata_header) a = a.replace("ERRATABLOB", ref_blob) overview_file.write(a) template_file.close() overview_file.close() def update_errata_package(errata_level): """Increment version/errata_level in univention-errata-level package and build it.""" tmpdir = tempfile.mkdtemp('', 'apt-fetch-') atexit.register(shutil.rmtree, tmpdir, ignore_errors=True) os.chdir(tmpdir) build_password = open('/etc/build.secret').readline().strip('') svn_dir, svn_file = os.path.split(ERRATALEVELPATCH[opt.ucsrelease]) svn_path = 'svn+ssh://build@%s/%s' % (configRegistry['repong/patchrepo'], svn_dir) rp.run_expect('svn co %s svn' % (svn_path,), build_password, 10800) patchfile = os.path.join('svn', svn_file) if os.path.exists(patchfile): i = open(patchfile, "r") o = open(patchfile + ".tmp", "w") PREFIX = "+ucr set version/erratalevel=" for line in i: if line.startswith(PREFIX): line = '%s%d\n' % (PREFIX, errata_level) o.write(line) i.close() o.close() shutil.move(patchfile + ".tmp", patchfile) rp.run_expect("svn ci -m 'Package updated by announce-errata (Bug #28893)' %s" % (patchfile,), build_password, 10800) # Rebuilding the package cmd = (ERRATABUILDSCRIPT[opt.ucsrelease], opt.scope, "univention-errata-level") print ' '.join(cmd) ret = subprocess.call(cmd) if ret != 0: print >> sys.stderr, "Build failed!" sys.exit(ret) else: print >> sys.stderr, "Could not regenerate univention-errata-level package, bailing out" sys.exit(1) tag_svn = False p = OptionParser() p.add_option("-r", action="store", type="string", dest="ucsrelease", help="The UCS release on which this erratum is based") p.add_option("-s", action="store", type="string", dest="scope", help="The scope in which the errata packages are build") p.add_option("-a", action="store", type="string", dest="advisory", help="Advisory meta data") p.add_option("-p", action="store", type="string", dest="package", help="The package, for which the errata is issued") p.add_option("-k", "--keyfile", dest="keyfile", action="store", help="The path to the file containing the archive key") p.add_option("-K", "--keyid", dest="keyid", action="store", help="The key-ID of the PGP archive key") p.add_option("--announce-only", action="store_true", dest="announce_only", default=False, help="Only generate website / announce mail") p.add_option("--overview-only", action="store_true", dest="overview_only", help="Only generate overview website") opt, args = p.parse_args() if not opt.ucsrelease: p.error("You need to specify the release for which the erratum should be announced (e.g. -r 3.0)") if opt.ucsrelease == "3.0": errata_31_style = False else: errata_31_style = True if opt.ucsrelease not in ERRATALEVELPATCH: print >> sys.sdterr, "No patch mapping has been defined for this release in repo-ng" sys.exit(1) if opt.ucsrelease not in ERRATABUILDSCRIPT: print >> sys.sdterr, "No build script has been defined for this release in repo-ng" sys.exit(1) # This file contains the basic information on each errata and is used to generate the HTML overview page # It is created if it not yet exists overview_data_file = "/var/univention/buildsystem2/errata/status/overview-" + opt.ucsrelease + ".txt" # This file contains an overview for all errata for a UCS release overview_html_file = website_dir + "/overview-" + opt.ucsrelease + ".html" if opt.overview_only: generate_html_overview() sys.exit(0) if not opt.scope: p.error("You need to specify the scope for which an erratum should be announced (e.g. -s errata3.0)") if opt.ucsrelease.count(".") > 1 or opt.ucsrelease.count("-") > 0: p.error("You need to specify only the major UCS release, e.g. -r 3.0") if not opt.advisory: p.error("You need to specify advisory meta data (e.g. -a emacs.yaml)") if not opt.package: p.error("You need to specify the source package name for which an erratum will be issued (e.g. -p emacs)") if not opt.keyfile: p.error("You need to specifiy a keyfile (e.g. -k /etc/archive-keys/ucs3.0.txt)") if not os.path.exists(opt.keyfile): p.error("Key file not found") if not opt.keyid: p.error("You need to specifiy a key ID (e.g. -K 2CBDA4B0)") # This file contains the next errata ID to be used next_id_file = "/var/univention/buildsystem2/errata/status/next-" + opt.ucsrelease + ".txt" try: idfile = open(next_id_file, "r") except OSError, ex: print >> sys.stderr, "Error opering %s: %s" % (next_id_file, ex) sys.exit(1) next_id = int(idfile.readline()) idfile.close() errata_id = "errata%d" % (next_id,) stream = open(opt.advisory, "r") try: adv = yaml.load(stream) finally: stream.close() errata_type = "bugfix" if adv['ref'].count("Bug") > 0 and adv['ref'].count("CVE") > 0: print "Malformed advisory file, you can't mix security and bugfix errata updates" sys.exit(1) if errata_31_style: if not adv.has_key('version'): print "Malformed advisory file, no version specified" sys.exit(1) else: errata_base_version = int(adv['version']) if adv['ref'].count("CVE") > 0: errata_type = "sec" try: passfile = open(key_file, "r") except IOError, ex: print >> sys.stderr, "Failed to open passphrase file: %s" % (ex,) sys.exit(1) try: sec_passphrase = passfile.read() finally: passfile.close() if errata_31_style: update_errata_package(next_id) generate_errata_mail() send_errata_test_mail() generate_errata_html() append_template_overview() generate_html_overview() if opt.announce_only: print "Only generating announce HTML / mail, exiting" sys.exit(0) mirror_base = "/mnt/omar/vmwares/mirror/ftp/" + opt.ucsrelease mirror_base_tarballs = "/mnt/omar/vmwares/mirror/ftp/updates/errata" buildsystem_basedir = "/var/univention/buildsystem2/apt/" apt_base = "/var/univention/buildsystem2/apt/ucs_" + opt.ucsrelease + "-0-" + opt.scope archs = ["all", "i386", "amd64"] apt_server = "apt.univention.de" maintained_dir = mirror_base + "/maintained/" unmaintained_dir = mirror_base + "/unmaintained/" maintained_pkgs = [] for i in ['i386', 'amd64']: if opt.ucsrelease == "3.0": # Some packages moved to maintained in 3.0-2, so use a different list f = open("/var/univention/buildsystem2/cd-contents/ucs_" + str(opt.ucsrelease) + "-2_" + i + "_dvd.txt", "r") else: f = open("/var/univention/buildsystem2/cd-contents/ucs_" + str(opt.ucsrelease) + "-0_" + i + "_dvd.txt", "r") for j in f.readlines(): pkgname = j[2:].split("_")[0] # Format looks like this: ./apt-mirror_0.4.8-3.21.201109051552_all.deb maintained_pkgs.append(pkgname) if errata_31_style: for patch_level in range(errata_base_version, 10): comp = '%s-%s-errata' % (opt.ucsrelease, patch_level) errata_dir_maintained = os.path.join(mirror_base, "maintained", "component", comp) errata_dir_unmaintained = os.path.join(mirror_base, "unmaintained", "component", comp) else: errata_dir_maintained = os.path.join(mirror_base, "maintained", errata_id) errata_dir_unmaintained = os.path.join(mirror_base, "unmaintained", errata_id) try: os.makedirs(errata_dir_maintained) os.makedirs(errata_dir_unmaintained) except OSError: pass for i in archs: try: os.mkdir(os.path.join(errata_dir_maintained, i)) os.mkdir(os.path.join(errata_dir_unmaintained, i)) except OSError: pass try: os.mkdir(os.path.join(errata_dir_unmaintained, "source")) except OSError: pass # Parse the changes files for the given source package and copy the files according to their # maintenance status bdir = buildsystem_basedir + "ucs_" + opt.ucsrelease + "-0-" + opt.scope + "/" i386_changes_files = glob.glob(bdir + "/source/" + opt.package + "_*changes") if len(i386_changes_files) == 0: print "No i386 changes file found, aborting" sys.exit(1) if len(i386_changes_files) > 1: print "More than one changes file found, aborting" sys.exit(1) all_package = False amd64_changes_files = glob.glob(bdir + "/amd64/" + opt.package + "_*changes") if len(amd64_changes_files) == 0: print "No amd64 changes file found. This is okay if the updated package is an all package" all_package = True if len(amd64_changes_files) > 1: print "More than one changes file found, aborting" sys.exit(1) i386_changes_file = i386_changes_files[0] for j in parse_debs_from_changes_files(i386_changes_file): binary_name = j.split("_")[0].lower() arch = "i386" if j.endswith("all.deb"): arch = "all" if maintained_pkgs.count(binary_name) > 0: rp.copy_deb_to_mirror(os.path.join(apt_base, arch, j), True, errata_dir_maintained) else: rp.copy_deb_to_mirror(os.path.join(apt_base, arch, j), False, errata_dir_unmaintained) if all_package == False: amd64_changes_file = amd64_changes_files[0] for j in parse_debs_from_changes_files(amd64_changes_file): binary_name = j.split("_")[0].lower() arch = "amd64" if j.endswith("all.deb"): arch = "all" if maintained_pkgs.count(binary_name) > 0: rp.copy_deb_to_mirror(os.path.join(apt_base, arch, j), True, errata_dir_maintained) else: rp.copy_deb_to_mirror(os.path.join(apt_base, arch, j), False, errata_dir_unmaintained) print os.path.join(buildsystem_basedir, "ucs_" + opt.ucsrelease + "-" + opt.scope, "source") print os.path.join(errata_dir_unmaintained, "source") copy_single_source_to_mirror(opt.package, os.path.join(bdir, "source"), os.path.join(errata_dir_unmaintained, "source")) if errata_31_style: comp = '%s-%s-errata' % (opt.ucsrelease, 0) # FIXME: why always 0? rp.create_packages_files(os.path.join(mirror_base, "maintained", "component"), comp) rp.create_packages_files(os.path.join(mirror_base, "unmaintained", "component"), comp) rp.create_sources_files(os.path.join(mirror_base, "unmaintained", "component"), comp) generate_release_file(mirror_base, "component/" + comp, opt.keyfile, opt.keyid) else: rp.create_packages_files(os.path.join(mirror_base, "maintained"), errata_id) rp.create_packages_files(os.path.join(mirror_base, "unmaintained"), errata_id) rp.create_sources_files(os.path.join(mirror_base, "unmaintained"), errata_id) generate_release_file(mirror_base, errata_id, opt.keyfile, opt.keyid) if not errata_31_style: for arch in ('amd64', 'i386'): tgz = generate_errata_tgz(mirror_base, errata_id, arch) name = os.path.join(mirror_base_tarballs, "ucs-errata-%s-%s_%s.tar.gz" % (opt.ucsrelease, errata_id, arch)) shutil.move(tgz, name) else: pass # TODO # Update next ID last in case of failures idfile = open(next_id_file, "w") idfile.write(str(next_id + 1) idfile.close()