#!/usr/bin/env python2
#
# Checks the kernel ABI versions to make sure there are no mismatches
# between -security and -updates, as well as reports whether any kernel
# USNs need to be published.
#
# Copyright (C) 2009-2021 Canonical, Ltd.
# Author: Kees Cook <kees@ubuntu.com>
# Author: Steve Beattie <sbeattie@ubuntu.com>

from __future__ import print_function

import argparse
import os
import subprocess
import sys

import apt_pkg

from cve_lib import (is_active_release, is_active_esm_release,
                     esm_releases, get_orig_rel_name)
from kernel_lib import (meta_kernels, lookup_glitch_version,
                        ignore_kernel_mabi, get_kernel_meta_alt_pkg,
                        kernel_package_abi, kernel_meta_abi)
from usn_lib import USNdb

try:
    import lpl_common
except ImportError:
    uqt_dir = os.getenv('UQT')
    sys.path.append(os.path.join(os.getenv('UQT'), 'common/'))
    try:
        import lpl_common
    except ImportError:
        print("lpl_common.py seems to be missing. Make sure $UQT environment variable is set properly", file=sys.stderr)
        sys.exit(1)

# cope with apt_pkg api changes.
if 'init_system' in dir(apt_pkg):
    apt_pkg.init_system()
else:
    apt_pkg.InitSystem()

parser = argparse.ArgumentParser()
parser.add_argument("-v", "--verbose", help="Show matches", action='store_true')
parser.add_argument("-d", "--debug", action='count', default=0,
                    help="Report debugging information")
parser.add_argument("--ignore-usn", help="Ignore need for USNs", action='store_true')
parser.add_argument("--ignore-abi", help="Ignore ABI mismatches", action='store_true')
parser.add_argument("--add-proposed", help="Check proposed for ABI mismatches", action='store_true')
parser.add_argument("--check-esm", help="Check esm ppa kernels as well", action='store_true')
parser.add_argument("--use-updates", '--include-updates', help="Check updates pocket for USNs to issue", action='store_true')
parser.add_argument("kernel", help="only check specific kernels", nargs="*")
opt = parser.parse_args()

# debugging; caution, can expose credentials
if opt.debug >= 2:
    import httplib2
    httplib2.debuglevel = 1

lp = lpl_common.connect()
ubuntu = lp.distributions['ubuntu']
archive = ubuntu.main_archive
pockets = ['Updates', 'Security']
if opt.add_proposed:
    pockets.append('Proposed')

usndb = None

# should move this to cve_lib or lp_lib
class LP_Series_Cache(object):
    def __init__(self, distro):
        self.distro = distro
        self.cache = dict()

    def series(self, release):
        if release not in self.cache:
            _series = self.distro.getSeries(name_or_version=release)
            self.cache[release] = _series

        return self.cache[release]


# should move this to cve_lib or lp_lib
class LP_PPA_Cache(object):
    def __init__(self, lp, debug=False):
        self.cache = dict()
        self.lp = lp
        self.debug = debug

    def get_archive_ref(self, ppa):
        if ppa not in self.cache:
            _archive, _, _ = lpl_common.get_archive(ppa, self.lp, self.debug)
            self.cache[ppa] = _archive

        return self.cache[ppa]

def get_usn_version(src, release):
    global usndb
    global opt

    orig_rel = get_orig_rel_name(release)

    if not usndb:
        usndb = USNdb([src], releases=[orig_rel], opt=opt)

    versions = usndb.get_usns(src, orig_rel)
    if versions:
        version = versions[0]
    else:
        version = '~'

    glitch_version = lookup_glitch_version(src, get_orig_rel_name(release), version)
    if glitch_version:
        if opt.verbose:
            print('Pretending that %s %s is really %s for %s' % (release, version, glitch_version, src), file=sys.stderr)
        version = glitch_version
    return version


def add_src_pkg(db, src, release, pockets, ppa=None):
    global archive
    global lp_series_cache
    global lp_ppa_cache

    db.setdefault(src, dict())
    # print('fetching %s in %s (%s)...' % (src, release, ppa))

    _release = get_orig_rel_name(release)
    series = lp_series_cache.series(_release)

    if ppa:
        ppa_archive = lp_ppa_cache.get_archive_ref(ppa)
        all_pubs = ppa_archive.getPublishedSources(exact_match=True,
                                       source_name=src,
                                       status="Published",
                                       distro_series=series)
    else:
        all_pubs = archive.getPublishedSources(exact_match=True,
                                       source_name=src,
                                       status="Published",
                                       distro_series=series)

    for pocket in pockets:
        db[src].setdefault(pocket, dict())
        db[src][pocket]['name'] = src

        pubs = sorted([p for p in all_pubs if p.pocket == pocket],
                      key=(lambda x: x.source_package_version))
        if pubs == []:
            if opt.verbose:
                print('%s missing in %s %s' % (src, series.name, pocket), file=sys.stderr)
            db[src][pocket]['version'] = '0.0.0.0'
            continue
        elif len(pubs) > 1:
            print('found multiple source versions for %s; using %s-%s' % (src, pubs[0].display_name, pocket), file=sys.stderr)
            print('(This is probably transient; publication of kernels is likely in progress)', file=sys.stderr)
        pub = pubs[0]
        db[src][pocket]['version'] = pub.source_package_version


# The "srcs" list must have the main kernel listed first, with ABI tracked
# packages coming after it.
def pocket_abis_match(release, srcs, meta_pkg, signed):
    global opt

    pkg = dict()
    meta = dict()
    signed_pkg = dict()
    ppa = None

    if opt.verbose:
        print("checking '%s/%s..." % (srcs, release), file=sys.stderr)
    if meta_kernels.get_ppa(srcs[0], release) is not None:
        ppa = meta_kernels.get_ppa(srcs[0], release)

    for src in srcs:
        add_src_pkg(pkg, src, release, pockets + ["Release"], ppa)

    # Add meta
    add_src_pkg(meta, meta_pkg, release, pockets + ["Release"], ppa)
    alt_src = get_kernel_meta_alt_pkg(meta_pkg)
    # ensure meta alt source packages are added to pkg dict
    if alt_src and (alt_src not in pkg):
        add_src_pkg(pkg, alt_src, release, pockets + ["Release"], ppa)

    if signed:
        add_src_pkg(signed_pkg, signed, release, pockets + ["Release"], ppa)

    rc = True

    # Check for primary kernel versions in -security that do not have a USN
    if not opt.ignore_usn:
        if is_active_esm_release(get_orig_rel_name(release)):
            pocket = 'Release'
        elif opt.use_updates:
            pocket = 'Updates'
        else:
            pocket = 'Security'

        src = srcs[0]
        last_usn = get_usn_version(src, release)
        if not pkg[src][pocket]['version'] in ['0.0.0.0', last_usn]:
            # if version in security pocket is older than last USN, skip
            if apt_pkg.version_compare(pkg[src][pocket]['version'], last_usn) < 0:
                if opt.verbose:
                    print('Version in %s pocket (%s) is less than the last USN version (%s) for %s' % (pocket, pkg[src][pocket]['version'], last_usn, src), file=sys.stderr)
            elif meta_kernels.ignore_usn(release, src):
                if opt.verbose:
                    print('Skipping kernel %s in release %s pocket %s' % (src, release, pocket), file=sys.stderr)
            else:
                rc = False
                print("USN needed: %s/%s: %s (last USN: %s)" % (release, pkg[src][pocket]['name'], pkg[src][pocket]['version'], last_usn), file=sys.stderr)

    if opt.ignore_abi:
        return rc

    # Check for ABI mismatches
    for src in srcs:
        if is_active_esm_release(get_orig_rel_name(release)):
            check_pockets = ['Release']
        else:
            check_pockets = pockets
        for pocket in check_pockets:
            if not pkg[src][pocket]['version']:
                raise ValueError('missing %s in %s %s' % (src, release, pocket))

            abis = [kernel_package_abi(pkg[src][pocket]['version']),
                    kernel_package_abi(pkg[src]["Release"]['version'])]

            for name in sorted(meta.keys()):
                mabi = kernel_meta_abi(meta[name][pocket]['version'])
                meta_ver = meta[name][pocket]['version']
                if meta[name][pocket]['version'] == "0.0.0.0":
                    meta_ver += " (DNE)"

                # Hack for different format and out of sync ABI on dapper
                if (release == 'dapper'):
                    mabi = kernel_meta_abi(meta[name][pocket]['version'], -1)
                    # no longer out of sync as of 2.6.15.57
                    if mabi < 57:
                        mabi -= 1

                # Allow certain ABIs to be ignored
                # if opt.verbose:
                #    print('Looking up kernel %s mabi %s' % (meta[name][pocket]['name'],  meta[name][pocket]['version']))
                if ignore_kernel_mabi(src, meta[name][pocket]['name'], release, meta[name][pocket]['version']):
                    if opt.verbose:
                        print("skip: ABI ignored for %s-%s: %d == %d (%s %s vs %s %s)" %
                              (release, pocket.lower(), mabi, abis[0], meta[name][pocket]['name'],
                              meta_ver, pkg[src][pocket]['name'], pkg[src][pocket]['version']))
                    continue

                if mabi not in abis:
                    abi = abis[0]
                    alt_src = get_kernel_meta_alt_pkg(name)
                    if alt_src:
                        alt_abi = kernel_package_abi(pkg[alt_src][pocket]['version'])
                        if alt_abi != mabi:
                            rc = False
                            print("FAIL: ABI mismatch in %s-%s: %d != %d (%s %s vs %s %s)" %
                                  (release, pocket.lower(), mabi, abi, meta[name][pocket]['name'],
                                  meta_ver, pkg[src][pocket]['name'], pkg[src][pocket]['version']),
                                  file=sys.stderr)
                            print("  alternate source ABI does not match either %s-%s: %d != %d (%s %s vs %s %s)" %
                                  (release, pocket.lower(), mabi, abi, meta[name][pocket]['name'], meta_ver,
                                  pkg[alt_src][pocket]['name'], pkg[alt_src][pocket]['version']), file=sys.stderr)
                        elif opt.verbose:
                            print("ok: ABI matches alternate abi in %s-%s: %d == %d (%s %s vs %s %s)" %
                                  (release, pocket.lower(), mabi, alt_abi, meta[name][pocket]['name'],
                                  meta_ver, pkg[alt_src][pocket]['name'], pkg[alt_src][pocket]['version']))
                    else:
                        rc = False
                        print("FAIL: ABI mismatch in %s-%s: %d != %d (%s %s vs %s %s)" %
                              (release, pocket.lower(), mabi, abi, meta[name][pocket]['name'],
                              meta_ver, pkg[src][pocket]['name'], pkg[src][pocket]['version']),
                              file=sys.stderr)
                elif opt.verbose:
                    print("ok: ABI matches in %s-%s: %d == %d (%s %s vs %s %s)" %
                          (release, pocket.lower(), mabi, abis[0], meta[name][pocket]['name'],
                          meta_ver, pkg[src][pocket]['name'], pkg[src][pocket]['version']))

            if signed:
                signed_abi = kernel_package_abi(signed_pkg[signed][pocket]['version'])
                if signed_abi not in abis:
                    rc = False
                    print("FAIL: ABI mismatch in %s-%s: %d != %d (%s %s vs %s %s)" %
                          (release, pocket.lower(), signed_abi, abis[0], signed_pkg[signed][pocket]['name'],
                          signed_pkg[signed][pocket]['version'], pkg[src][pocket]['name'], pkg[src][pocket]['version']),
                          file=sys.stderr)
                elif opt.verbose:
                    print("ok: ABI matches in %s-%s: %d == %d (%s %s vs %s %s)" %
                          (release, pocket.lower(), signed_abi, abis[0], signed_pkg[signed][pocket]['name'],
                          signed_pkg[signed][pocket]['version'], pkg[src][pocket]['name'], pkg[src][pocket]['version']))

    #print("pocket_abis_match(%s, %s, %s, %s, %s) returned %s" % (release, srcs, metas, signed, ignored, rc))
    return rc


ok = True

# The following table is loosely based on
# https://wiki.ubuntu.com/Kernel/Dev/ABIPackages and
# https://wiki.ubuntu.com/Kernel/Dev/TopicBranches
# the version number in the second parameter isn't currently used in the
# script.

wanted_kernels = set(opt.kernel)
lp_series_cache = LP_Series_Cache(ubuntu)
lp_ppa_cache = LP_PPA_Cache(lp, debug=opt.debug)

for (release, srcs, meta, signed) in meta_kernels.get_next_kernel():
    if not wanted_kernels.issubset(set()) and wanted_kernels.isdisjoint(set(srcs)):
        continue
    # FIXME - need to figure out lookups for esm kernel versions; until
    # then skip them to avoid email noise
    #if is_active_release(release) or is_active_esm_release(release):
    #
    # don't check end of life releases
    if is_active_release(release) and \
       (opt.check_esm or not is_active_esm_release(get_orig_rel_name(release))):
        if not pocket_abis_match(release, srcs, meta, signed):
            ok = False

if ok:
    sys.exit(0)
else:
    sys.exit(1)
