#!python
# -*- coding: utf-8 -*-
"""
Elliott is a CLI tool for managing Red Hat release advisories using the Erratatool
web service.
"""

# -----------------------------------------------------------------------------
# Module dependencies
# -----------------------------------------------------------------------------

# Prepare for Python 3
# stdlib
from __future__ import print_function
from multiprocessing import cpu_count
from multiprocessing.dummy import Pool as ThreadPool
import datetime
import json
import os
import re
import sys
import time

# ours
from elliottlib import version, exectools
from elliottlib import Runtime
import elliottlib.constants
import elliottlib.bzutil
import elliottlib.brew
import elliottlib.errata
import elliottlib.exceptions
import elliottlib.openshiftclient

from elliottlib import cli_opts
from elliottlib.exceptions import ElliottFatalError
from elliottlib.util import exit_unauthenticated, green_prefix, YMD, override_product_version
from elliottlib.util import default_release_date, validate_release_date
from elliottlib.util import validate_email_address, red_print, major_from_branch
from elliottlib.util import green_print, red_prefix, minor_from_branch
from elliottlib.util import yellow_print, yellow_prefix, exit_unauthorized, release_from_branch
from elliottlib.util import progress_func, pbar_header
from elliottlib.cli.tarball_sources_cli import tarball_sources_cli

# 3rd party
import bugzilla
import click
import requests
import dotconfig
import errata_tool.build
from errata_tool import Erratum, ErrataException
from kerberos import GSSError

# -----------------------------------------------------------------------------
# Constants and defaults
# -----------------------------------------------------------------------------
pass_runtime = click.make_pass_decorator(Runtime)
context_settings = dict(help_option_names=['-h', '--help'])


def print_version(ctx, param, value):
    if not value or ctx.resilient_parsing:
        return
    click.echo('Elliott v{}'.format(version()))
    ctx.exit()


@click.group(context_settings=context_settings)
@click.option('--version', is_flag=True, callback=print_version,
              expose_value=False, is_eager=True)
@click.option("--working-dir", metavar='PATH', envvar="ELLIOTT_WORKING_DIR",
              default=None,
              help="Existing directory in which file operations should be performed.")
@click.option("--data-path", metavar='PATH', default=None,
              help="Git repo or directory containing groups metadata")
@click.option("--user", metavar='USERNAME', envvar="ELLIOTT_USER",
              default=None,
              help="Username for rhpkg.")
@click.option("--group", "-g", default=None, metavar='NAME',
              help="The group of images on which to operate.")
@click.option("--branch", default=None, metavar='BRANCH',
              help="Branch to override any default in group.yml.")
@click.option('--stage', default=False, is_flag=True, help='Force checkout stage branch for sources in group.yml.')
@click.option("-i", "--images", default=[], metavar='NAME', multiple=True,
              help="Name of group image member to include in operation (all by default). Can be comma delimited list.")
@click.option("-r", "--rpms", default=[], metavar='NAME', multiple=True,
              help="Name of group rpm member to include in operation (all by default). Can be comma delimited list.")
@click.option("-x", "--exclude", default=[], metavar='NAME', multiple=True,
              help="Name of group image or rpm member to exclude in operation (none by default). Can be comma delimited list.")
@click.option("--quiet", "-q", default=False, is_flag=True, help="Suppress non-critical output")
@click.option('--debug', default=False, is_flag=True, help='Show debug output on console.')
@click.pass_context
def cli(ctx, **kwargs):
    cfg = dotconfig.Config('elliott', 'settings',
                           template=cli_opts.CLI_CONFIG_TEMPLATE,
                           envvars=cli_opts.CLI_ENV_VARS,
                           cli_args=kwargs)
    ctx.obj = Runtime(cfg_obj=cfg, **cfg.to_dict())

# -----------------------------------------------------------------------------
# CLI Commands - Please keep these in alphabetical order
# -----------------------------------------------------------------------------

#
# Set advisory state
# change-state
#
@cli.command("change-state", short_help="Change ADVISORY state")
@click.option("--state", '-s', required=True,
              type=click.Choice(['NEW_FILES', 'QE', 'REL_PREP']),
              help="New state for the Advisory. NEW_FILES, QE, REL_PREP")
@click.option("--advisory", "-a", metavar='ADVISORY',
              help="Change state of ADVISORY")
@click.option("--use-default-advisory", 'default_advisory_type',
              metavar='ADVISORY_TYPE',
              type=click.Choice(['image', 'rpm', 'security']),
              help="Use the default value from ocp-build-data for ADVISORY_TYPE [image, rpm, security]")
@click.option("--noop", "--dry-run",
              required=False,
              default=False, is_flag=True,
              help="Do not actually change anything")
@pass_runtime
def change_state(runtime, state, advisory, default_advisory_type, noop):
    """Change the state of an ADVISORY. Additional permissions may be
required to change an advisory to certain states.

An advisory may not move between some states until all criteria have
been met. For example, an advisory can not move from NEW_FILES to QE
unless Bugzilla Bugs or JIRA Issues have been attached.

    NOTE: The two advisory options are mutually exclusive and can not
    be used together.

See the find-bugs help for additional information on adding
Bugzilla Bugs.

    Move the advisory 123456 from NEW_FILES to QE state:

    $ elliott change-state --state QE --advisory 123456

    Move the advisory 123456 back to NEW_FILES (short option flag):

    $ elliott change-state -s NEW_FILES -a 123456

    Do not actually change state, just check that the command could
    have ran (for example, when testing out pipelines)

    $ elliott change-state -s NEW_FILES -a 123456 --noop
"""
    runtime.initialize()

    if advisory and default_advisory_type:
        raise click.BadParameter("Use only one of --use-default-advisory or --advisory")

    if default_advisory_type is not None:
        advisory = find_default_advisory(runtime, default_advisory_type)

    if noop:
        prefix = "[NOOP] "
    else:
        prefix = ""

    try:
        e = Erratum(errata_id=advisory)

        if e.errata_state == state:
            green_prefix("{}No change to make: ".format(prefix))
            click.echo("Target state is same as current state")
            return
        else:
            if noop:
                green_prefix("{}Would have changed state: ".format(prefix))
                click.echo("{} ➔ {}".format(e.errata_state, state))
                return
            else:
                # Capture current state because `e.commit()` will
                # refresh the `e.errata_state` attribute
                old_state = e.errata_state
                e.setState(state)
                e.commit()
                green_prefix("Changed state: ")
                click.echo("{old_state} ➔ {new_state}".format(
                    old_state=old_state,
                    new_state=state))
    except ErrataException as ex:
        raise ElliottFatalError(getattr(ex, 'message', repr(ex)))

    green_print("Successfully changed advisory state")


#
# Create Advisory (RPM and image)
# advisory:create
#
@cli.command("create", short_help="Create a new advisory")
@click.option("--type", '-t', 'errata_type',
              type=click.Choice(['RHBA', 'RHSA', 'RHEA']),
              default='RHBA',
              help="Type of Advisory to create.")
@click.option("--kind", '-k', required=True,
              type=click.Choice(['rpm', 'image']),
              help="Kind of artifacts that will be attached to Advisory. Affects boilerplate text.")
@click.option("--impetus", default='standard',
              type=click.Choice(elliottlib.constants.errata_valid_impetus),
              help="Impetus for the advisory creation [{}]".format(
                ', '.join(elliottlib.constants.errata_valid_impetus)))
@click.option("--cve", required=False,
              help="CVE to associate with the Advisory. Required for RHSA.")
@click.option("--date", required=False,
              default=default_release_date.strftime(YMD),
              callback=validate_release_date,
              help="Release date for the advisory. Optional. Format: YYYY-Mon-DD. Defaults to 3 weeks after the release with the highest date for that series")
@click.option('--assigned-to', metavar="EMAIL_ADDR", required=True,
              envvar="ELLIOTT_ASSIGNED_TO_EMAIL",
              callback=validate_email_address,
              help="The email address group to review and approve the advisory.")
@click.option('--manager', metavar="EMAIL_ADDR", required=True,
              envvar="ELLIOTT_MANAGER_EMAIL",
              callback=validate_email_address,
              help="The email address of the manager monitoring the advisory status.")
@click.option('--package-owner', metavar="EMAIL_ADDR", required=True,
              envvar="ELLIOTT_PACKAGE_OWNER_EMAIL",
              callback=validate_email_address,
              help="The email address of the person responsible managing the advisory.")
@click.option('--with-placeholder', is_flag=True,
              default=False, type=bool,
              help="Create a placeholder bug and attach it to the advisory. Only valid if also using --yes.")
@click.option('--yes', '-y', is_flag=True,
              default=False, type=bool,
              help="Create the advisory (by default only a preview is displayed)")
@pass_runtime
@click.pass_context
def create(ctx, runtime, errata_type, kind, impetus, date, assigned_to, manager, package_owner, cve, with_placeholder, yes):
    """Create a new advisory. The kind of advisory must be specified with
'--kind'. Valid choices are 'rpm' and 'image'.

    You MUST specify a group (ex: "openshift-3.9") manually using the
    --group option. See examples below.

New advisories will be created with a Release Date set to 3 weeks (21
days) from now. You may customize this (especially if that happens to
fall on a weekend) by providing a YYYY-Mon-DD formatted string to the
--date option.

The default behavior for this command is to show what the generated
advisory would look like. The raw JSON used to create the advisory
will be printed to the screen instead of posted to the Errata Tool
API.

The impetus option only effects the metadata added to the new
advisory and its synopsis.

The --assigned-to, --manager and --package-owner options are required.
They are the email addresses of the parties responsible for managing and
approving the advisory.

Provide the '--yes' or '-y' option to confirm creation of the
advisory.

    PREVIEW an RPM Advisory 21 days from now (the default release date) for OSE 3.9:

    $ elliott --group openshift-3.9 create

    CREATE Image Advisory for the 3.5 series on the first Monday in March:

\b
    $ elliott --group openshift-3.5 create --yes -k image --date 2018-Mar-05
"""
    runtime.initialize()

    if errata_type == 'RHSA' and not cve:
        raise ElliottFatalError("When creating an RHSA, you must provide a --cve value.")

    et_data = runtime.gitdata.load_data(key='erratatool').data
    bz_data = runtime.gitdata.load_data(key='bugzilla').data

    major = major_from_branch(runtime.group_config.branch)
    minor = minor_from_branch(runtime.group_config.branch)
    impact = None

    if date == default_release_date.strftime(YMD):
        # User did not enter a value for --date, default is determined
        # by looking up the latest erratum in a series
        try:
            latest_advisory = elliottlib.errata.find_latest_erratum(kind, major, minor)
        except GSSError:
            exit_unauthenticated()
        except elliottlib.exceptions.ErrataToolUnauthorizedException:
            exit_unauthorized()
        except elliottlib.exceptions.ErrataToolError as ex:
            raise ElliottFatalError(getattr(ex, 'message', repr(ex)))
        else:
            if latest_advisory is None:
                red_print("No metadata discovered")
                raise ElliottFatalError("No advisory for {x}.{y} has been released in recent history, can not auto "
                                        "determine next release date".format(x=major, y=minor))

        green_prefix("Found an advisory to calculate new release date from: ")
        click.echo("{synopsis} - {rel_date}".format(
            synopsis=latest_advisory.synopsis,
            rel_date=str(latest_advisory.release_date)))
        release_date = latest_advisory.release_date + datetime.timedelta(days=21)

        # We want advisories to issue on Tuesdays. Using strftime
        # Tuesdays are '2' with Sunday indexed as '0'
        day_of_week = int(release_date.strftime('%w'))
        if day_of_week != 2:
            # How far from our target day of the week?
            delta = day_of_week - 2
            release_date = release_date - datetime.timedelta(days=delta)
            yellow_print("Adjusted release date to land on a Tuesday")

        green_prefix("Calculated release date: ")
        click.echo("{}".format(str(release_date)))
    else:
        # User entered a valid value for --date, set the release date
        release_date = datetime.datetime.strptime(date, YMD)

    ######################################################################

    if errata_type == 'RHSA':
        # grab CVE trackers and set Impact automatically
        cve_trackers = elliottlib.bzutil.search_for_security_bugs(bz_data, cve=cve)

        severity_indexes = []
        for t in cve_trackers:
            tracker_severity = elliottlib.bzutil.get_bug_severity(bz_data, t.id)
            severity_indexes.append(elliottlib.constants.BUG_SEVERITY.index(tracker_severity))

        if len(severity_indexes) == 0:
            raise ElliottFatalError("No security bugs found, make sure you run `bugzilla login`.")

        impact = elliottlib.constants.SECURITY_IMPACT[max(severity_indexes)]

    ######################################################################

    try:
        erratum = elliottlib.errata.new_erratum(
            et_data,
            errata_type=errata_type,
            kind=(impetus if impetus in ['extras', 'metadata'] else kind),
            release_date=release_date.strftime(YMD),
            create=yes,
            assigned_to=assigned_to,
            manager=manager,
            package_owner=package_owner,
            impact=impact,
            cve=cve
        )
    except elliottlib.exceptions.ErrataToolUnauthorizedException:
        exit_unauthorized()
    except elliottlib.exceptions.ErrataToolError as ex:
        raise ElliottFatalError(getattr(ex, 'message', repr(ex)))

    if yes:
        green_prefix("Created new advisory: ")
        click.echo(str(erratum.synopsis))

        # This is a little strange, I grant you that. For reference you
        # may wish to review the click docs
        #
        # http://click.pocoo.org/5/advanced/#invoking-other-commands
        #
        # You may be thinking, "But, add_metadata doesn't take keyword
        # arguments!" and that would be correct. However, we're not
        # calling that function directly. We actually use the context
        # 'invoke' method to call the _command_ (remember, it's wrapped
        # with click to create a 'command'). 'invoke' ensures the correct
        # options/arguments are mapped to the right parameters.
        ctx.invoke(add_metadata, kind=kind, impetus=impetus, advisory=erratum.errata_id)
        click.echo(str(erratum))

        if errata_type == 'RHSA':
            click.echo("Automatically attaching CVE trackers...")
            erratum.addBugs([bug.bug_id for bug in cve_trackers])
            erratum.commit()

            yellow_print("Remember to manually set the Security Reviewer in the Errata Tool Web UI")

        if with_placeholder:
            click.echo("Creating and attaching placeholder bug...")

            ctx.invoke(create_placeholder, kind=kind, advisory=erratum.errata_id)
    else:
        green_prefix("Would have created advisory: ")
        click.echo("")
        click.echo(erratum)


#
# Look up a default advisory specified for the branch in ocp-build-data
# Advisory types are in ['rpm', 'image', 'cve']
#
def find_default_advisory(runtime, default_advisory_type, quiet=False):
    """The `quiet` parameter will disable printing the informational message"""
    default_advisory = runtime.group_config.advisories.get(default_advisory_type, None)
    if default_advisory is None:
        red_prefix("No value defined for default advisory:")
        click.echo(" The key advisories.{} is not defined for group {} in group.yml".format(
            default_advisory_type, runtime.group))
        exit(1)
    if not quiet:
        green_prefix("Default advisory detected: ")
        click.echo(default_advisory)
    return default_advisory


#
# Collect bugs
# advisory:find-bugs
#
@cli.command("find-bugs", short_help="Find or add MODIFED bugs to ADVISORY")
@click.option("--add", "-a", 'advisory',
              default=False, metavar='ADVISORY',
              help="Add found bugs to ADVISORY. Applies to bug flags as well (by default only a list of discovered bugs are displayed)")
@click.option("--use-default-advisory", 'default_advisory_type',
              metavar='ADVISORY_TYPE',
              type=click.Choice(['image', 'rpm', 'security']),
              help="Use the default value from ocp-build-data for ADVISORY_TYPE [image, rpm, security]")
@click.option("--mode",
              required=True,
              type=click.Choice(['list', 'sweep', 'diff']),
              default='list',
              help='Mode to use to find bugs')
@click.option("--status", 'status',
              multiple=True,
              required=False,
              default=['MODIFIED', 'VERIFIED'],
              type=click.Choice(elliottlib.constants.VALID_BUG_STATES),
              help="Status of the bugs")
@click.option("--id", metavar='BUGID', default=None,
              multiple=True, required=False,
              help="Bugzilla IDs to add, required for LIST mode.")
@click.option("--from-diff", "--between",
              required=False,
              nargs=2,
              help="Two payloads to compare against")
@click.option("--flag", metavar='FLAG',
              required=False, multiple=True,
              help="Optional flag to apply to found bugs [MULTIPLE]")
@pass_runtime
def find_bugs(runtime, advisory, default_advisory_type, mode, status, id, from_diff, flag):
    """Find Red Hat Bugzilla bugs or add them to ADVISORY. Bugs can be
"swept" into the advisory either automatically (--mode sweep), or by
manually specifying one or more bugs using --mode list and the --id option.
Use cases are described below:

    Note: Using --id without --add is basically pointless

SWEEP: For this use-case the --group option MUST be provided. The
--group automatically determines the correct target-releases to search
for MODIFIED bugs in.

LIST: The --group option is not required if you are specifying bugs
manually. Provide one or more --id's for manual bug addition. In LIST
mode you must provide a list of IDs to attach with the --id option.

DIFF: For this use case, you must provide the --between option using two
URLs to payloads.

Using --use-default-advisory without a value set for the matching key
in the build-data will cause an error and elliott will exit in a
non-zero state. Use of this option silently overrides providing an
advisory with the --add option.

    Automatically add bugs with target-release matching 3.7.Z or 3.7.0
    to advisory 123456:

\b
    $ elliott --group openshift-3.7 find-bugs --mode sweep --add 123456

    List bugs that WOULD be added to an advisory and have set the bro_ok flag on them (NOOP):

\b
    $ elliott --group openshift-3.7 find-bugs --mode sweep --flag bro_ok

    Add two bugs to advisory 123456. Note that --group is not required
    because we're not auto searching:

\b
    $ elliott find-bugs --mode list --id 8675309 --id 7001337 --add 123456

    Automatically find bugs for openshift-4.1 and attach them to the
    rpm advisory defined in ocp-build-data:

\b
    $ elliott --group=openshift-4.1 --mode sweep --use-default-advisory rpm
"""
    if mode != 'list' and len(id) > 0:
        raise click.BadParameter("Combining the automatic and manual bug attachment options is not supported")

    if mode == 'list' and len(id) == 0:
        raise click.BadParameter("When using mode=list, you must provide a list of bug IDs")

    if mode == 'payload' and not len(from_diff) == 2 :
        raise click.BadParameter("If using mode=payload, you must provide two payloads to compare")

    if advisory and default_advisory_type:
        raise click.BadParameter("Use only one of --use-default-advisory or --add")

    runtime.initialize()
    bz_data = runtime.gitdata.load_data(key='bugzilla').data
    bzapi = elliottlib.bzutil.get_bzapi(bz_data)

    if default_advisory_type is not None:
        advisory = find_default_advisory(runtime, default_advisory_type)

    if mode == 'sweep':
        green_prefix("Searching for bugs with target release(s):")
        click.echo(" {tr}".format(tr=", ".join(bz_data['target_release'])))
        bug_ids = elliottlib.bzutil.search_for_bugs(bz_data, status)
    elif mode == 'list':
        bug_ids = [bzapi.getbug(i) for i in cli_opts.id_convert(id)]
    elif mode == "diff":
        click.echo(runtime.working_dir)
        bug_id_strings = elliottlib.openshiftclient.get_bug_list(runtime.working_dir, from_diff[0], from_diff[1])
        bug_ids = [bzapi.getbug(i) for i in bug_id_strings]

    green_prefix("Found {} bugs:".format(len(bug_ids)))
    click.echo(" {}".format(", ".join([str(b.bug_id) for b in bug_ids])))

    if len(flag) > 0:
        for bug in bug_ids:
            for f in flag:
                bug.updateflags({f: "+"})

    if advisory is not False:
        elliottlib.errata.add_bugs_with_retry(advisory, [bug.id for bug in bug_ids], False)


#
# Remove bugs
#
@cli.command("remove-bugs", short_help="Remove provided BUGS from ADVISORY")
@click.option('--advisory', '-a', 'advisory',
              default=False, metavar='ADVISORY',
              help='Remove found bugs from ADVISORY')
@click.option("--use-default-advisory", 'default_advisory_type',
              metavar='ADVISORY_TYPE',
              type=click.Choice(['image', 'rpm', 'security']),
              help="Use the default value from ocp-build-data for ADVISORY_TYPE [image, rpm, security]")
@click.option("--id", metavar='BUGID', default=[],
              multiple=True, required=True,
              help="Bugzilla IDs to add.")
@pass_runtime
def remove_bugs(runtime, advisory, default_advisory_type, id):
    """Remove given BUGS from ADVISORY.

    Remove bugs that have been attached an advisory:

\b
    $ elliott --group openshift-3.7 remove-bugs --id 123456 --advisory 1234123

    Remove two bugs from default rpm advisory. Note that --group is required
    because default advisory is from ocp-build-data:

\b
    $ elliott --group openshift-3.7 remove-bugs --id 123456 --id 3412311 --use-default-advisory rpm


"""
    if bool(advisory) == bool(default_advisory_type):
        raise click.BadParameter("Specify exactly one of --use-default-advisory or advisory arg")

    runtime.initialize()
    bz_data = runtime.gitdata.load_data(key='bugzilla').data
    bzapi = elliottlib.bzutil.get_bzapi(bz_data)

    bug_ids = [bzapi.getbug(i) for i in cli_opts.id_convert(id)]

    green_prefix("Found {} bugs:".format(len(bug_ids)))
    click.echo(" {}".format(", ".join([str(b.bug_id) for b in bug_ids])))

    if default_advisory_type is not None:
        advisory = find_default_advisory(runtime, default_advisory_type)

    if advisory is not False:
        try:
            advs = Erratum(errata_id=advisory)
        except GSSError:
            exit_unauthenticated()

        if advs is False:
            raise ElliottFatalError("Error: Could not locate advisory {advs}".format(advs=advisory))

        try:
            green_prefix("Removing {count} bugs from advisory:".format(count=len(bug_ids)))
            click.echo(" {advs}".format(advs=advisory))
            advs.removeBugs([bug.id for bug in bug_ids])
            advs.commit()
        except ErrataException as ex:
            raise ElliottFatalError(getattr(ex, 'message', repr(ex)))


#
# Attach Builds
# advisory:find-builds
#
@cli.command('find-builds',
             short_help='Find or attach builds to ADVISORY')
@click.option('--attach', '-a', 'advisory',
              default=False, metavar='ADVISORY',
              help='Attach the builds to ADVISORY (by default only a list of builds are displayed)')
@click.option("--use-default-advisory", 'default_advisory_type',
              metavar='ADVISORY_TYPE',
              type=click.Choice(['image', 'rpm', 'security']),
              help="Use the default value from ocp-build-data for ADVISORY_TYPE [image, rpm, security]")
@click.option('--build', '-b', 'builds',
              multiple=True, metavar='NVR_OR_ID',
              help='Add build NVR_OR_ID to ADVISORY [MULTIPLE]')
@click.option('--kind', '-k', metavar='KIND',
              required=False, type=click.Choice(['rpm', 'image']),
              help='Find builds of the given KIND [rpm, image]')
@click.option("--from-diff", "--between",
              required=False,
              nargs=2,
              help="Two payloads to compare against")
@click.option('--json', 'as_json', metavar="FILE_NAME",
              help="Dump new builds as JSON array to a file (or '-' for stdout)")
@pass_runtime
def find_builds(runtime, advisory, default_advisory_type, builds, kind, from_diff, as_json):
    """Automatically or manually find or attach viable rpm or image builds
to ADVISORY. Default behavior searches Brew for viable builds in the
given group. Provide builds manually by giving one or more --build
(-b) options. Manually provided builds are verified against the Errata
Tool API.

\b
  * Attach the builds to ADVISORY by giving --attach
  * Specify the build type using --kind KIND

Example: Assuming --group=openshift-3.7, then a build is a VIABLE
BUILD IFF it meets ALL of the following criteria:

\b
  * HAS the tag in brew: rhaos-3.7-rhel7-candidate
  * DOES NOT have the tag in brew: rhaos-3.7-rhel7
  * IS NOT attached to ANY existing RHBA, RHSA, or RHEA

That is to say, a viable build is tagged as a "candidate", has NOT
received the "shipped" tag yet, and is NOT attached to any PAST or
PRESENT advisory. Here are some examples:

    SHOW the latest OSE 3.6 image builds that would be attached to a
    3.6 advisory:

    $ elliott --group openshift-3.6 find-builds -k image

    ATTACH the latest OSE 3.6 rpm builds to advisory 123456:

\b
    $ elliott --group openshift-3.6 find-builds -k rpm --attach 123456

    VERIFY (no --attach) that the manually provided RPM NVR and build
    ID are viable builds:

\b
    $ elliott --group openshift-3.6 find-builds -k rpm -b megafrobber-1.0.1-2.el7 -b 93170
"""

    if (from_diff and kind) or (from_diff and builds) or (builds and kind):
        raise ElliottFatalError("Use only one of --kind or --build or --from-diff.")

    if not builds and not from_diff and not kind:
        raise ElliottFatalError("Must use one of --kind or --build or --from-diff.")

    if advisory and default_advisory_type:
        raise click.BadParameter("Use only one of --use-default-advisory or --attach")

    runtime.initialize()

    if not runtime.branch:
        raise ElliottFatalError("Need to specify a branch either in group.yml or with --branch option")
    base_tag = runtime.branch

    et_data = runtime.gitdata.load_data(key='erratatool').data
    product_version = override_product_version(et_data.get('product_version'), runtime.branch)

    if default_advisory_type is not None:
        advisory = find_default_advisory(runtime, default_advisory_type)

    # Test authentication
    try:
        elliottlib.errata.get_filtered_list(elliottlib.constants.errata_live_advisory_filter)
    except GSSError:
        exit_unauthenticated()

    session = requests.Session()
    unshipped_builds = []

    if len(builds) > 0:
        green_prefix("Build NVRs provided: ")
        click.echo("Manually verifying the builds exist")
        try:
            unshipped_builds = [elliottlib.brew.get_brew_build(b, product_version, session=session) for b in builds]
        except elliottlib.exceptions.BrewBuildException as ex:
            raise ElliottFatalError(getattr(ex, 'message', repr(ex)))
    elif kind:
        if kind == 'image':
            initial_builds = runtime.image_metas()
            pbar_header("Generating list of {kind}s: ".format(kind=kind),
                        "Hold on a moment, fetching Brew buildinfo",
                        initial_builds)
            pool = ThreadPool(cpu_count())
            # Look up builds concurrently
            click.secho("[", nl=False)

            # Returns a list of (n, v, r) tuples of each build
            potential_builds = pool.map(
                lambda build: progress_func(lambda: build.get_latest_build_info(), '*'),
                initial_builds)
            # Wait for results
            pool.close()
            pool.join()
            click.echo(']')

            pbar_header("Generating build metadata: ",
                        "Fetching data for {n} builds ".format(n=len(potential_builds)),
                        potential_builds)
            click.secho("[", nl=False)

            # Reassign variable contents, filter out remove non_release builds
            potential_builds = [i for i in potential_builds
                                if i[0] not in runtime.group_config.get('non_release', [])]

            # By 'meta' I mean the lil bits of meta data given back from
            # get_latest_build_info
            #
            # TODO: Update the ImageMetaData class to include the NVR as
            # an object attribute.
            pool = ThreadPool(cpu_count())
            unshipped_builds = pool.map(
                lambda meta: progress_func(
                    lambda: elliottlib.brew.get_brew_build("{}-{}-{}".format(meta[0], meta[1], meta[2]),
                                                           product_version,
                                                           session=session),
                    '*'),
                potential_builds)
            # Wait for results

            # filter out 'openshift-enterprise-base-container' since it's not needed in advisory
            unshipped_builds = [b for b in unshipped_builds
                                if 'openshift-enterprise-base-container' not in b.nvr]
            pool.close()
            pool.join()
            click.echo(']')
        elif kind == 'rpm':
            green_prefix("Generating list of {kind}s: ".format(kind=kind))
            click.echo("Hold on a moment, fetching Brew builds")
            unshipped_build_candidates = elliottlib.brew.find_unshipped_build_candidates(
                base_tag,
                product_version,
                kind=kind)

            pbar_header("Gathering additional information: ", "Brew buildinfo is required to continue", unshipped_build_candidates)
            click.secho("[", nl=False)

            # We could easily be making scores of requests, one for each build
            # we need information about. May as well do it in parallel.
            pool = ThreadPool(cpu_count())
            results = pool.map(
                lambda nvr: progress_func(
                    lambda: elliottlib.brew.get_brew_build(nvr, product_version, session=session),
                    '*'),
                unshipped_build_candidates)
            # Wait for results
            pool.close()
            pool.join()
            click.echo(']')

            # We only want builds not attached to an existing open advisory
            unshipped_builds = [b for b in results if not b.attached_to_open_erratum]
    elif from_diff:
        green_print("Fetching changed images between payloads...")
        changed_builds = elliottlib.openshiftclient.get_build_list(from_diff[0], from_diff[1])
        unshipped_builds = [elliottlib.brew.get_brew_build(b, product_version, session=session) for b in changed_builds]

    build_nvrs = sorted(build.nvr for build in unshipped_builds)
    json_data = dict(builds=build_nvrs, base_tag=base_tag, kind=kind)
    if as_json == "-":
        click.echo(json.dumps(json_data, indent=4, sort_keys=True))
    elif as_json:
        with open(as_json, "w") as json_file:
            json.dump(json_data, json_file, indent=4, sort_keys=True)

    if not unshipped_builds:
        green_print("No builds needed to be attached.")
        return

    if advisory is not False:
        # Search and attach
        file_type = None
        try:
            erratum = Erratum(errata_id=advisory)
            if kind == 'image':
                file_type = 'tar'
            elif kind == 'rpm':
                file_type = 'rpm'

            if file_type is None:
                raise ElliottFatalError("Need to specify with --kind=image or --kind=rpm with packages: {}".format(unshipped_builds))

            erratum.addBuilds(build_nvrs,
                              release=product_version,
                              file_types={build.nvr: [file_type] for build in unshipped_builds})
            erratum.commit()
            green_print("Attached build(s) successfully:")
            for b in sorted(unshipped_builds):
                click.echo(" " + b.nvr)
        except GSSError:
            exit_unauthenticated()
        except elliottlib.exceptions.BrewBuildException as ex:
            raise ElliottFatalError("Error attaching builds: {}".format(getattr(ex, 'message', repr(ex))))
    else:
        click.echo("The following {n} builds ".format(n=len(unshipped_builds)), nl=False)
        click.secho("may be attached ", bold=True, nl=False)
        click.echo("to an advisory:")
        for b in sorted(unshipped_builds):
            click.echo(" " + b.nvr)


#
# Get an Advisory
# advisory:get
#
@cli.command("get", short_help="Get information for an ADVISORY")
@click.argument('advisory', type=int, required=False)
@click.option("--use-default-advisory", 'default_advisory_type',
              metavar='ADVISORY_TYPE',
              type=click.Choice(['image', 'rpm', 'security']),
              help="Use the default value from ocp-build-data for ADVISORY_TYPE [image, rpm, security]")
@click.option('--details', is_flag=True, default=False,
              help="Print the full object of the advisory")
@click.option('--id-only', is_flag=True, default=False,
              help="Print only the ID of the default advisory")
@click.option('--json', 'as_json', metavar="FILE_NAME",
              help="Dump the advisory as JSON to a file (or '-' for stdout)")
@pass_runtime
@click.pass_context
def get(ctx, runtime, default_advisory_type, details, id_only, as_json, advisory):
    """Get details about a specific advisory from the Errata Tool. By
default a brief one-line informational string is printed. Use the
--details option to fetch and print the full details of the advisory.

Use of --id-only will override all other printing options. Requires
using --use-default-advisory. Only the ID of the advisory is printed
to standard out.

Fields for the short format: Release date, State, Synopsys, URL

    Basic one-line output for advisory 123456:

\b
    $ elliott get 123456
    2018-02-23T18:34:40 NEW_FILES OpenShift Container Platform 3.9 bug fix and enhancement update - https://errata.devel.redhat.com/advisory/123456

    Get the full JSON advisory object, use `jq` to print just the
    errata portion of the advisory:

\b
    $ elliott get --json - 123456 | jq '.errata'
    {
      "rhba": {
        "actual_ship_date": null,
        "assigned_to_id": 3002255,
        "batch_id": null,
        ...
"""

    runtime.initialize(no_group=default_advisory_type is None)

    if bool(advisory) == bool(default_advisory_type):
        raise click.BadParameter("Specify exactly one of --use-default-advisory or advisory arg")
    if default_advisory_type is not None:
        advisory = find_default_advisory(runtime, default_advisory_type, quiet=True)

    if id_only:
        click.echo(advisory)
        return

    try:
        advisory = Erratum(errata_id=advisory)
    except GSSError:
        exit_unauthenticated()

    if details:
        click.echo(advisory)
        return

    if not as_json:
        advisory_string = "{date} {state} {synopsis} {url}".format(
            date=advisory.publish_date_override,
            state=advisory.errata_state,
            synopsis=advisory.synopsis,
            url=advisory.url())
        click.echo(advisory_string)
        return

    json_data = advisory.get_erratum_data()
    json_data['errata_builds'] = advisory.errata_builds
    json_data['current_flags'] = advisory.current_flags
    json_data['rpmdiffs'] = advisory.externalTests(test_type='rpmdiff')

    if as_json == "-":
        click.echo(json.dumps(json_data, indent=4, sort_keys=True))
        return

    with open(as_json, "w") as json_file:
        json.dump(json_data, json_file, indent=4, sort_keys=True)


#
# List Advisories (RPM and image)
# advisory:list
#
@cli.command("list", short_help="List filtered RHOSE advisories")
@click.option("--filter-id", '-f',
              default=elliottlib.constants.errata_default_filter,
              help="A custom filter id to list from")
@click.option("-n", default=6,
              help="Return only N latest results (default: 6)")
@click.pass_context
def list(ctx, filter_id, n):
    """Print a list of one-line informational strings of RHOSE
advisories. By default the 5 most recently created advisories are
printed. Note, they are NOT sorted by release date.

    NOTE: new filters must be created in the Errata Tool web
    interface.

Default filter definition: RHBA; Active; Product: RHOSE; Devel Group:
ENG OpenShift Enterprise; sorted by newest. Browse this filter
yourself online: https://errata.devel.redhat.com/filter/1965

    List 10 advisories instead of the default 6 with your custom
    filter #1337:

    $ elliott list -n 10 -f 1337
"""
    try:
        for erratum in elliottlib.errata.get_filtered_list(filter_id, limit=n):
            click.echo("{release_date:11s} {state:15s} {synopsis:80s} {url}".format(
                       release_date=erratum.publish_date_override,
                       state=erratum.errata_state,
                       synopsis=erratum.synopsis,
                       url=erratum.url()))
    except GSSError:
        exit_unauthenticated()
    except elliottlib.exceptions.ErrataToolError as ex:
        raise ElliottFatalError(getattr(ex, 'message', repr(ex)))

#
# Get advisory numbers for making puddles
#
@cli.command("puddle-advisories", short_help="Get advisory numbers for making puddles")
@click.option("--filter-id", '-f',
              # default=elliottlib.constants.errata_default_filter,
              default=2507,
              help="A custom filter id to list from")
@click.option('--details', '-d', is_flag=True, default=False,
              help="Print details about the found advisories to STDERR")
# @click.option("-n", default=6,
#               help="Return only N latest results (default: 6)")
@pass_runtime
def puddle_advisories(runtime, filter_id, details):
    """Print a comma separated list of advisory numbers which can be used
when filling in the 'errata_whitelist' parameter in a signed puddle
config.

Uses an Errata Tool filter to find in-progress and being-released
advisories for OpenShift. This list is trimmed down to only advisories
matching the given --group by parsing the ART metadata embedded in the
first comment.

    List advisories required to create a signed 4.2 puddle:

\b
    $ elliott --group=openshift-4.1 puddle-advisories
    44849, 44740
"""
    use_in_puddle_conf = []
    runtime.initialize()
    major = major_from_branch(runtime.group_config.branch)
    minor = minor_from_branch(runtime.group_config.branch)
    release = "{}.{}".format(major, minor)

    try:
        for erratum in elliottlib.errata.get_filtered_list(filter_id, limit=20):
            try:
                c = elliottlib.errata.get_comments(erratum.errata_id)[-1]
                metadata = json.loads(c['attributes']['text'])
            except ValueError:
                # Does not contain ART metadata, skip it
                sys.stderr.write("Does not contain ART metadata: {}\n".format(erratum.errata_id))
                continue

            if str(metadata['release']) == str(release) and (metadata['impetus'] != 'test'):
                use_in_puddle_conf.append(str(erratum.errata_id))
                if details:
                    sys.stderr.write(str(erratum))
                    sys.stderr.flush()

        click.echo(", ".join(use_in_puddle_conf))
    except GSSError:
        exit_unauthenticated()
    except elliottlib.exceptions.ErrataToolError as ex:
        raise ElliottFatalError(getattr(ex, 'message', repr(ex)))

#
# Add metadata comment to an Advisory
# advisory:add-metadata
#
@cli.command("add-metadata", short_help="Add metadata comment to an advisory")
@click.argument('advisory', type=int)
@click.option('--kind', '-k', required=True,
              type=click.Choice(['rpm', 'image']),
              help="KIND of advisory [rpm, image]")
@click.option('--impetus', default='standard',
              type=click.Choice(elliottlib.constants.errata_valid_impetus),
              help="Impetus for the advisory creation [standard, cve, ga, test]")
@pass_runtime
def add_metadata(runtime, kind, impetus, advisory):
    """Add metadata to an advisory. This is usually called by
create immediately after creation. It is only useful to you if
you are going back and adding metadata to older advisories.

    Note: Requires you provide a --group

Example to add standard metadata to a 3.10 images release

\b
    $ elliott --group=openshift-3.10 add-metadata --impetus standard --kind image
"""
    runtime.initialize()
    release = release_from_branch(runtime.group_config.branch)

    try:
        advisory = Erratum(errata_id=advisory)
    except GSSError:
        exit_unauthenticated()

    result = elliottlib.errata.add_comment(advisory.errata_id, {'release': release, 'kind': kind, 'impetus': impetus})

    if result.status_code == 201:
        green_prefix("Added metadata successfully")
        click.echo()
    elif result.status_code == 403:
        exit_unauthorized()
    else:
        red_print("Something weird may have happened")
        raise ElliottFatalError("Unexpected response from ET API: {code}".format(code=result.status_code))

#
# Repair bugs
# advisory:repair-bugs
#
@cli.command("repair-bugs", short_help="Move bugs attached to ADVISORY from one state to another")
@click.option("--advisory", "-a",
              metavar='ADVISORY',
              help="Repair bugs attached to ADVISORY.")
@click.option("--auto",
              required=False,
              default=False, is_flag=True,
              help="AUTO mode, check all bugs attached to ADVISORY")
@click.option("--id", default=None, metavar='BUGID',
              multiple=True, required=False,
              help="Bugzilla IDs to modify, conflicts with --auto [MULTIPLE]")
@click.option("--from", "original_state",
              multiple=True,
              default=['MODIFIED'],
              type=click.Choice(elliottlib.constants.VALID_BUG_STATES),
              help="Current state of the bugs (default: MODIFIED)")
@click.option("--to", "new_state",
              default='ON_QA',
              type=click.Choice(elliottlib.constants.VALID_BUG_STATES),
              help="Final state of the bugs (default: ON_QA)")
@click.option("--noop", "--dry-run",
              required=False,
              default=False, is_flag=True,
              help="Check bugs attached, print what would change, but don't change anything")
@click.option("--use-default-advisory", 'default_advisory_type',
              metavar='ADVISORY_TYPE',
              type=click.Choice(['image', 'rpm', 'security']),
              help="Use the default value from ocp-build-data for ADVISORY_TYPE [image, rpm, security]")
@pass_runtime
def repair_bugs(runtime, advisory, auto, id, original_state, new_state, noop, default_advisory_type):
    """Move bugs attached to the advisory from one state to another
state. This is useful if the bugs have changed states *after* they
were attached. Similar to `find-bugs` but in reverse. `repair-bugs`
begins by reading bugs from an advisory, whereas `find-bugs` reads
from bugzilla.

This looks at attached bugs in the provided --from state and moves
them to the provided --to state.

\b
    Background: This is intended for bugs which went to MODIFIED, were
    attached to advisories, set to ON_QA, and then failed
    testing. When this happens their state is reset back to ASSIGNED.

Using --use-default-advisory without a value set for the matching key
in the build-data will cause an error and elliott will exit in a
non-zero state. Most likely you will only want to use the `rpm` state,
but that could change in the future. Use of this option conflicts with
providing an advisory with the -a/--advisory option.

    Move bugs on 123456 FROM the MODIFIED state back TO ON_QA state:

\b
    $ elliott --group=openshift-4.1 repair-bugs --auto --advisory 123456 --from MODIFIED --to ON_QA

    As above, but using the default RPM advisory defined in ocp-build-data:

\b
    $ elliott --group=openshift-4.1 repair-bugs --auto --use-default-advisory rpm --from MODIFIED --to ON_QA

    The previous examples could also be ran like this (MODIFIED and ON_QA are both defaults):

\b
    $ elliott --group=openshift-4.1 repair-bugs --auto --use-default-advisory rpm

    Bug ids may be given manually instead of using --auto:

\b
    $ elliott --group=openshift-4.1 repair-bugs --id 170899 --id 8675309 --use-default-advisory rpm
"""
    if auto and len(id) > 0:
        raise click.BadParameter("Combining the automatic and manual bug modification options is not supported")

    if not auto and len(id) == 0:
        # No bugs were provided
        raise click.BadParameter("If not using --auto then one or more --id's must be provided")

    if advisory and default_advisory_type:
        raise click.BadParameter("Use only one of --use-default-advisory or --advisory")

    if len(id) == 0 and advisory is None and default_advisory_type is None:
        # error, no bugs, advisory, or default selected
        raise click.BadParameter("No input provided: Must use one of --id, --advisory, or --use-default-advisory")

    # Load bugzilla infomation and get a reference to the api
    runtime.initialize()
    bz_data = runtime.gitdata.load_data(key='bugzilla').data
    bzapi = elliottlib.bzutil.get_bzapi(bz_data)
    changed_bug_count = 0
    attached_bugs = []

    if default_advisory_type is not None:
        advisory = find_default_advisory(runtime, default_advisory_type)

    raw_bug_list = []
    if auto:
        click.echo("Fetching Erratum(errata_id={})".format(advisory))
        e = Erratum(errata_id=advisory)
        raw_bug_list = e.errata_bugs
    else:
        click.echo("Bypassed fetching erratum, using provided BZs")
        raw_bug_list = cli_opts.id_convert(id)

    green_print("Getting bugs for advisory")

    # Fetch bugs in parallel because it can be really slow doing it
    # one-by-one when you have hundreds of bugs
    pbar_header("Fetching data for {} bugs: ".format(len(raw_bug_list)),
                "Hold on a moment, we have to grab each one",
                raw_bug_list)
    pool = ThreadPool(cpu_count())
    click.secho("[", nl=False)

    attached_bugs = pool.map(
        lambda bug: progress_func(lambda: bzapi.getbug(bug), '*'),
        raw_bug_list)
    # Wait for results
    pool.close()
    pool.join()
    click.echo(']')

    green_print("Got bugs for advisory")
    for bug in attached_bugs:
        if(bug.status in original_state):
            changed_bug_count += 1
            if noop:
                click.echo("Would have changed BZ#{bug_id} from {initial} to {final}".format(
                    bug_id=bug.bug_id,
                    initial=bug.status,
                    final=new_state))
            else:
                click.echo("Changing BZ#{bug_id} from {initial} to {final}".format(
                    bug_id=bug.bug_id,
                    initial=bug.status,
                    final=new_state))
                bug.setstatus(status=new_state,
                              comment="Elliott changed bug status from {initial} to {final}.".format(initial=original_state, final=new_state),
                              private=True)


    green_print("{} bugs successfullly modified (or would have been)".format(changed_bug_count))


#
# Search for CVE tracker bugs
#
@cli.command("find-cve-trackers", short_help="Find and list CVE tracker bugs for Security issues.")
@click.option('--cve',
              required=False,
              default=None,
              help="CVE number to filter on. (ex. CVE-2011-1000000)")
@click.option('--status', 'status',
              multiple=True,
              required=False,
              default=['NEW', 'ASSIGNED', 'POST', 'MODIFIED', 'ON_QA', 'VERIFIED', 'RELEASE_PENDING'],
              type=click.Choice(elliottlib.constants.VALID_BUG_STATES),
              help="Status the bugs can be in.")
@pass_runtime
def find_cve_trackers(runtime, cve, status):
    """Find Red Hat Bugzilla security bugs and list them out. Automatic attachment of these
    bugs is not supported because security issues generally need to be hand crafted to make
    sure all requirements are met.

    Usage:
\b
    $ elliott --group openshift-3.7 find-cve-trackers
"""
    runtime.initialize()
    bz_data = runtime.gitdata.load_data(key='bugzilla').data

    click.echo("Searching for bugs with target release(s): {tr}".format(tr=", ".join(bz_data['target_release'])))

    bug_list = elliottlib.bzutil.search_for_security_bugs(bz_data, status, cve=cve)

    click.echo("Found {} bugs:".format(len(bug_list)))
    for b in bug_list:
        click.echo("{}\t{:15s}\t{}".format(b.bug_id, b.status, b.summary))

#
# Verify images in a payload match the corresponding advisory
# verify-payload
#
@cli.command("verify-payload", short_help="Verify payload contents match advisory builds")
@click.argument("payload")
@click.argument('advisory', type=int)
@click.pass_context
def verify_payload(ctx, payload, advisory):
    """Cross-check that the builds present in PAYLOAD match the builds
attached to ADVISORY. The payload is treated as the source of
truth. If something is absent or different in the advisory it is
treated as an error with the advisory.

\b
    PAYLOAD - Full pullspec of the payload to verify
    ADVISORY - Numerical ID of the advisory

Two checks are made:

\b
 1. Missing in Advisory - No payload components are absent from the given advisory

 2. Payload Advisory Mismatch - The version-release of each payload item match what is in the advisory

Results are summarily printed at the end of the run. They are also
written out to summary_results.json.

     Verify builds in the given payload match the builds attached to
     advisory 41567

 \b
    $ elliott verify-payload quay.io/openshift-release-dev/ocp-release:4.1.0-rc.6 41567

    """
    try:
        green_prefix("Fetching advisory builds: ")
        click.echo("Advisory - {}".format(advisory))
        builds = elliottlib.errata.get_builds(advisory)
    except GSSError:
        exit_unauthenticated()
    except elliottlib.exceptions.ErrataToolError as ex:
        raise ElliottFatalError(getattr(ex, 'message', repr(ex)))

    all_advisory_nvrs = {}
    # Results come back with top level keys which are brew tags
    green_prefix("Looping over tags: ")
    click.echo("{} tags to check".format(len(builds.keys())))
    for tag in builds.keys():
        # Each top level has a key 'builds' which is a list of dicts
        green_prefix("Looping over builds in tag: ")
        click.echo("{} with {} builds".format(tag, len(builds[tag]['builds'])))
        for build in builds[tag]['builds']:
            # Each dict has a top level key which might be the actual
            # 'nvr' but I don't have enough data to know for sure
            # yet. Also I don't know when there might be more than one
            # key in the build dict. We'll loop over it to be sure.
            for name in build.keys():
                n, v, r = name.rsplit('-', 2)
                version_release = "{}-{}".format(v, r)
                all_advisory_nvrs[n] = version_release

    click.echo("Found {} builds".format(len(all_advisory_nvrs)))

    all_payload_nvrs = {}
    click.echo("Fetching release info")
    release_export_cmd = 'oc adm release info {} -o json'.format(payload)

    rc, stdout, stderr = exectools.cmd_gather(release_export_cmd)
    if rc != 0:
        # Probably no point in continuing.. can't contact brew?
        print("Unable to run oc release info: out={}  ; err={}".format(stdout, stderr))
        exit(1)
    else:
        click.echo("Got release info")

    payload_json = json.loads(stdout)

    green_prefix("Looping over payload images: ")
    click.echo("{} images to check".format(len(payload_json['references']['spec']['tags'])))
    for image in payload_json['references']['spec']['tags']:
        click.echo("----")
        green_prefix("Getting payload image metadata: ")
        click.echo("{}".format(image['from']['name']))
        pullspec = image['from']['name']
        image_name = image['name']
        pullspec_cmd = 'oc image info {} -o json'.format(pullspec)
        rc, stdout, stderr = exectools.cmd_gather(pullspec_cmd)
        if rc != 0:
            # Probably no point in continuing.. can't contact brew?
            red_prefix("Unable to run oc image info: ")
            click.echo("out={}  ; err={}".format(stdout, stderr))
            exit(1)

        image_info = json.loads(stdout)
        labels = image_info['config']['config']['Labels']

        # The machine-os-content image doesn't follow the standard
        # pattern. We need to skip that image when we find it, it is
        # not attached to advisories.
        if 'com.coreos.ostree-commit' in labels:
            yellow_prefix("Skipping machine-os-content image: ")
            click.echo("Not required for checks")
            continue

        component = labels['com.redhat.component']
        n = image_name
        click.echo("Payload name: {}".format(n))
        click.echo("Brew name: {}".format(component))
        if labels:
            v = labels['version']
            r = labels['release']
            all_payload_nvrs[component] = "{}-{}".format(v, r)
        else:
            print("For image {} Labels doesn't exist, image_info: {}".format(image_name, image_info))

    missing_in_errata = {}
    payload_doesnt_match_errata = {}
    output = {
        'missing_in_advisory': missing_in_errata,
        'payload_advisory_mismatch': payload_doesnt_match_errata,
    }
    green_prefix("Analyzing data: ")
    click.echo("{} images to consider from payload".format(len(all_payload_nvrs)))

    for image, vr in all_payload_nvrs.items():
        yellow_prefix("Cross-checking from payload: ")
        click.echo("{}-{}".format(image, vr))
        if image not in all_advisory_nvrs:
            missing_in_errata[image] = "{}-{}".format(image, vr)
            click.echo("{} in payload not found in advisory".format("{}-{}".format(image, vr)))
        elif image in all_advisory_nvrs and vr != all_advisory_nvrs[image]:
            click.echo("{} from payload has version {} which does not match {} from advisory".format(
                image, vr, all_advisory_nvrs[image]))
            payload_doesnt_match_errata[image] = {
                'payload': vr,
                'errata': all_advisory_nvrs[image]
            }

    green_print("Summary results:")
    click.echo(json.dumps(output, indent=4))
    with open('summary_results.json', 'w') as fp:
        json.dump(output, fp, indent=4)
    green_prefix("Wrote out summary results: ")
    click.echo("summary_results.json")

#
# Create Placeholder BZ
# bugzilla:create-placeholder
#
@cli.command('create-placeholder',
             short_help='Create a placeholder BZ')
@click.option('--kind', '-k', metavar='KIND',
              required=False, type=click.Choice(elliottlib.constants.placeholder_valid_types),
              help='KIND [{}] of placeholder bug to create. Affects BZ title.'.format(
                  ', '.join(elliottlib.constants.placeholder_valid_types)))
@click.option('--attach', '-a', 'advisory',
              default=False, metavar='ADVISORY',
              help='Attach the bug to ADVISORY')
@click.option("--use-default-advisory", 'default_advisory_type',
              metavar='ADVISORY_TYPE',
              type=click.Choice(elliottlib.constants.placeholder_valid_types),
              help="Use the default value from ocp-build-data for ADVISORY_TYPE [{}]".format(
                  ', '.join(elliottlib.constants.placeholder_valid_types)))
@pass_runtime
def create_placeholder(runtime, kind, advisory, default_advisory_type):
    """Create a placeholder bug for attaching to an advisory.

    KIND - The kind of placeholder to create ({}).
    ADVISORY - Optional. The advisory to attach the bug to.

    $ elliott --group openshift-4.1 create-placeholder --kind rpm --attach 12345
""".format('/'.join(elliottlib.constants.placeholder_valid_types))

    runtime.initialize()
    if advisory and default_advisory_type:
        raise click.BadParameter("Use only one of --use-default-advisory or --advisory")

    if default_advisory_type is not None:
        advisory = find_default_advisory(runtime, default_advisory_type)
        kind = default_advisory_type

    if kind is None:
        raise click.BadParameter("--kind must be specified when not using --use-default-advisory")

    bz_data = runtime.gitdata.load_data(key='bugzilla').data
    target_release = bz_data['target_release'][0]
    newbug = elliottlib.bzutil.create_placeholder(bz_data, kind, target_release)

    click.echo("Created BZ: {} {}".format(newbug.id, newbug.weburl))

    if advisory is not False:
        click.echo("Attaching to advisory...")

        try:
            advs = Erratum(errata_id=advisory)
        except GSSError:
            exit_unauthenticated()

        if advs is False:
            raise ElliottFatalError("Error: Could not locate advisory {advs}".format(advs=advisory))

        try:
            green_prefix("Adding placeholder bug to advisory:")
            click.echo(" {advs}".format(advs=advisory))
            advs.addBugs([newbug.id])
            advs.commit()
        except ErrataException as ex:
            raise ElliottFatalError(getattr(ex, 'message', repr(ex)))

#
# Poll for rpm-signed state change
# poll-signed
#
@cli.command("poll-signed", short_help="Poll for RPM build 'signed' status")
@click.option("--minutes", "-m", required=False,
              default=15, type=int,
              help="How long to poll before quitting")
@click.option("--advisory", "-a",
              metavar='ADVISORY',
              help="Advisory to watch")
@click.option("--use-default-advisory", 'default_advisory_type',
              metavar='ADVISORY_TYPE',
              type=click.Choice(['image', 'rpm', 'security']),
              help="Use the default value from ocp-build-data for ADVISORY_TYPE [image, rpm, security]")
@click.option("--noop", "--dry-run",
              required=False,
              default=False, is_flag=True,
              help="Don't actually poll, just print the signed status of each build")
@pass_runtime
def poll_signed(runtime, minutes, advisory, default_advisory_type, noop):
    """Poll for the signed-status of RPM builds attached to
ADVISORY. Returns rc=0 when all builds have been signed. Returns non-0
after MINUTES have passed and all builds have not been signed. This
non-0 return code is the number of unsigned builds remaining. All
builds must show 'signed' for this command to succeed.

    NOTE: The two advisory options are mutually exclusive.

For testing in pipeline scripts this sub-command accepts a --noop
option. When --noop is used the value of --minutes is irrelevant. This
command will print out the signed state of all attached builds and
then exit with rc=0 if all builds are signed and non-0 if builds are
still unsigned. In the non-0 case the return code is the number of
unsigned builds.

    Wait 15 minutes for the default 4.2 advisory to show all RPMS have
    been signed:

    $ elliott -g openshift-4.2 poll-signed --use-default-advisory rpm

    Wait 5 mintes for the provided 4.2 advisory to show all RPMs have
    been signed:

    $ elliott -g openshift-4.2 poll-signed -m 5 --advisory 123456

    Print the signed status of all attached builds, exit
    immediately. Return code is the number of unsigned builds.

\b
    $ elliott -g openshift-4.2 poll-signed --noop --use-default-advisory rpm
"""
    runtime.initialize()

    if advisory and default_advisory_type:
        raise click.BadParameter("Use only one of --use-default-advisory or --advisory")

    if default_advisory_type is not None:
        advisory = find_default_advisory(runtime, default_advisory_type)

    if not noop:
        click.echo("Polling up to {} minutes for all RPMs to be signed".format(minutes))

    try:
        e = Erratum(errata_id=advisory)
        all_builds = set([])
        all_signed = False
        # `errata_builds` is a dict with brew tags as keys, values are
        # lists of builds on the advisory with that tag
        for k, v in e.errata_builds.iteritems():
            all_builds = all_builds.union(set(v))
        green_prefix("Fetching initial states: ")
        click.echo("{} builds to check".format(len(all_builds)))
        start_time = datetime.datetime.now()
        while datetime.datetime.now() - start_time < datetime.timedelta(minutes=minutes):
            pbar_header("Getting build signatures: ",
                        "Should be pretty quick",
                        all_builds)
            pool = ThreadPool(cpu_count())
            # Look up builds concurrently
            click.secho("[", nl=False)

            build_sigs = pool.map(
                lambda build: progress_func(
                    lambda: elliottlib.errata.build_signed(build),
                    '*'),
                all_builds)
            # Wait for results
            pool.close()
            pool.join()
            click.echo(']')

            if all(build_sigs):
                all_signed = True
                break
            elif noop:
                # Escape the time-loop
                break
            else:
                yellow_prefix("Not all builds signed: ")
                click.echo("re-checking")
                continue

        if not all_signed:
            red_prefix("Signing incomplete: ")
            if noop:
                click.echo("All builds not signed. ")
            else:
                click.echo("All builds not signed in given window ({} minutes). ".format(minutes))
                exit(1)
        else:
            green_prefix("All builds signed: ")
            click.echo("Enjoy!")
    except ErrataException as ex:
        raise ElliottFatalError(getattr(ex, 'message', repr(ex)))

## Register additional command groups
cli.add_command(tarball_sources_cli)

# -----------------------------------------------------------------------------
# CLI Entry point
# -----------------------------------------------------------------------------
def main():
    try:
        if 'REQUESTS_CA_BUNDLE' not in os.environ:
            os.environ['REQUESTS_CA_BUNDLE'] = '/etc/pki/tls/certs/ca-bundle.crt'

        cli(obj={})
    except ElliottFatalError as ex:
        # Allow capturing actual tool errors and print them
        # nicely instead of a gross stack-trace.
        # All internal errors that should simply cause the app
        # to exit with an error code should use ElliottFatalError
        red_print(getattr(ex, 'message', repr(ex)))
        sys.exit(1)

if __name__ == '__main__':
    main()
