#!/usr/bin/env python3
# Copyright 2011-2013, Canonical, Ltd.
# Author: Jamie Strandboge <jamie@canonical.com>
# License: GPLv3
#
# Report security bugs for the security team to EOL. By default, query
# Launchpad for any Public Security bugs with EOL tasks. Can optionally
# query by team or structural_subscriber (team subscribed to package bugs).
#
# $ report-bugs-for-eol
# $ report-bugs-for-eol --bug-cache=~/tmp/bugs/bug.cache
#

import pickle
import cve_lib
import optparse
import os
import re
import sys

parser = optparse.OptionParser()
parser.add_option("--debug", help="Verbose processing output", action='store_true')
parser.add_option("--team", help="Find bugs for team", metavar="TEAM", action='store', default=None)
parser.add_option("--structural-subscriber", help="Find bugs for packages subscribed to by team", metavar="TEAM", action='store', default=None)
parser.add_option("--bug-cache", help="Cached pickle file for bugs (warning-- private bug data stored)", metavar="FILE", action='store')
parser.add_option("--bug-status", help="Specify bug status", metavar="STATUS", action='store', default=None)

(opt, args) = parser.parse_args()

def debug(s):
    '''Print debug message'''
    if opt.debug:
        print("DEBUG: %s" % (s), file=sys.stderr)

def print_bugs(bugs, status=None, tags=[]):
    '''Output a collection of bugs'''
    keys = sorted(list(bugs.keys()))
    is_eol_out = []
    is_eol_skipped = []
    for id in keys:
        bugid = bugs[id]['bug_id']

        bug_target_name = bugs[id]['bug_target_name']
        if not ' (' in bug_target_name:
            print("Skipping target name=%s (LP: #%d)" % (bug_target_name, bugid), file=sys.stderr)
            continue

        release = id.split(':')[1]

        pkg, target = bug_target_name.split(' (', 1)

        # Only consider non-linux packages in EOL releases. Note, this also
        # allows release == 'ubuntu' with linux packages (fine, since we want
        # to EOL 'ubuntu' there too
        if not pkg.startswith('linux') and release not in cve_lib.eol_releases:
            debug("Skipping LP: #%d ('%s' is not EOL on '%s')" % (bugid, pkg, release))
            continue

        tstatus = bugs[id]['bug_status']
        if status:
           if tstatus != status:
               debug("Skipping LP: #%d (%s != %s)" % (bugid, tstatus, status))
               continue

        target = target.split(')')[0]
        if ' ' in target:
            target, targeted_to = target.split(' ', 1)

        if target and target.lower() != 'ubuntu':
            debug('skipping target "%s" (%s) (LP: #%d)' % (target, pkg, bugid))
            continue

        if tstatus in ['Fix Released', 'Invalid', "Won't Fix"]:
            debug('skipping (pkg:%s status:%s LP: #%d)' % (pkg, tstatus, bugid))
            continue

        if not re.match(r'^[a-z0-9][a-z0-9+\.\-]+$', pkg):
            print("Bad package name '%s' (LP: #%d)" % (pkg, bugid), file=sys.stderr)
            continue

        if pkg.startswith('linux'):
            if '-lts-' in pkg and pkg.split('-')[-1] in cve_lib.eol_releases:
                if release == 'ubuntu': # is-eol doesn't use release for
                                        # non-series tasks
                    is_eol_out.append("%s:%s" % (bugid, pkg))
                else:
                    # all backport kernels will be supported for precise
                    if release == "precise" and \
                       pkg.split('-')[-1] in ["quantal", "raring", "saucy"]:
                        is_eol_skipped.append("%s:%s:%s (%s)" % (bugid, pkg, release, tstatus))
                    else:
                        is_eol_out.append("%s:%s:%s" % (bugid, pkg, release))
            elif release in cve_lib.eol_releases:
                is_eol_out.append("%s:%s:%s" % (bugid, pkg, release))
            else:
                is_eol_skipped.append("%s:%s:%s (%s)" % (bugid, pkg, release, tstatus))
        else:
            is_eol_out.append("%s:%s:%s" % (bugid, pkg, release))

    return (is_eol_out, is_eol_skipped)


#
# Main
#

lts_releases = []
for r in cve_lib.releases:
    if r not in cve_lib.eol_releases and 'LTS' in cve_lib.release_name(r):
        lts_releases.append(r)

bugs = None
bug_database_fn = ""
if opt.bug_cache:
    bug_database_fn = os.path.expanduser(opt.bug_cache)
    if not os.path.isfile(bug_database_fn):
        print("'%s' does not exist. Skipping load" % bug_database_fn, file=sys.stderr)
    else:
        # Load pickle database
        print("Loading bugs from cache...", file=sys.stderr)
        bugs = pickle.load(open(bug_database_fn))
        print("done", file=sys.stderr)


# Connect to LP only if cached db not available
if bugs == None:
    if opt.bug_cache and os.path.exists(bug_database_fn):
        print("'%s' exists, aborting" % opt.bug_cache, file=sys.stderr)
        sys.exit(1)

    #
    # Connect to Launchpad
    #
    try:
        import lpl_common
    except:
        print("lpl_common.py seems to be missing.  Please create a symlink from $UQT/common/lpl_common.py to $UCT/scripts/", file=sys.stderr)
        sys.exit(1)

    # Load configuration
    cve_lib.read_config()

    # API interface
    print("Connecting to LP ...", end=' ', file=sys.stderr)
    lp = lpl_common.connect()

    # Get authenticated URL fetcher
    opener = lpl_common.opener_with_cookie(cve_lib.config["plb_authentication"])
    if not opener:
        raise ValueError("Could not open cookies")

    ubuntu = lp.distributions['ubuntu']
    debug("Distribution: %s" % ubuntu)
    team = None
    structural_subscriber = None
    if opt.team is not None:
        team = lp.people[opt.team]
        debug("Team: %s" % team)
    elif opt.structural_subscriber is not None:
        structural_subscriber = lp.people["ubuntu-security"]
        debug("Package subscriber (structural_subscriber): %s" % structural_subscriber)

    print("done", file=sys.stderr)

    search_releases = cve_lib.releases
    print("Loading bugs for all releases (%s)..." % (", ".join(search_releases)), end=' ', file=sys.stderr)

    bugs = {}
    for rel in search_releases + ['ubuntu']:
        if rel == "ubuntu":
            obj = ubuntu
        else:
            series = ubuntu.getSeries(name_or_version=rel)
            obj = series

        if team is not None:
            task_collection = obj.searchTasks(bug_subscriber=team, omit_targeted=False)
        elif structural_subscriber is not None:
            task_collection = obj.searchTasks(structural_subscriber=structural_subscriber, omit_targeted=False)
        else: # just show all public security bugs by default. TODO: Private Security
            task_collection = obj.searchTasks(information_type="Public Security", omit_targeted=False)

        for task in task_collection:
            bugid = task.bug.id
            bug_target_name = task.bug_target_name
            key = "%d:%s:%s" % (bugid, rel, bug_target_name.split()[0])
            if key not in bugs:
                data = {}
                data['bug_id'] = bugid
                data['bug_status'] = task.status
                data['bug_target_name'] = bug_target_name
                bugs[key] = data
    print("done", file=sys.stderr)


    if opt.bug_cache and not os.path.exists(bug_database_fn):
        print("Saving cache to '%s'. To update cache, remove this file" % (opt.bug_cache), file=sys.stderr)
        pickle.dump(bugs, open(bug_database_fn, 'w'), -1)

#print bugs

(out, skipped) = print_bugs(bugs, status=opt.bug_status)
print('''
The following were skipped (not EOL):
 %s

You can retire these bugs for EOL releases automatically with:
$UQT/responses/security/is-eol --no-comment %s
''' % ("\n ".join(skipped), " ".join(out)))

sys.exit(0)

