#!/bin/env python
"""
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
import datetime
from multiprocessing.dummy import Pool as ThreadPool
from multiprocessing import cpu_count
import os
import sys

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

from elliottlib import cli_opts
from elliottlib.exceptions import ElliottFatalError
from elliottlib.util import *

# 3rd party
import bugzilla
import click
import requests
import dotconfig
from errata_tool import Erratum

# -----------------------------------------------------------------------------
# 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
# advisory:state
#
@cli.command("change-state", short_help="Change ADVISORY state")
@click.option("--state", '-s', type=click.Choice(['NEW_FILES', 'QE', 'REL_PREP']),
              help="New state for the Advisory. NEW_FILES, QE, REL_PREP.")
@click.argument('advisory', type=int)
@click.pass_context
def change_state(ctx, state, advisory):
    """Change the state of 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.

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

    Move the advisory 123456 from NEW_FILES to QE state:

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

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

    $ elliott advisory:change-state -s NEW_FILES 123456
    """
    
    click.echo("Changing state for {id} to {state}".format(id=advisory, state=state))
    
    e = Erratum(errata_id=id)
    e.setState(state)
    e.commit()

    green_prefix("Changed advisory state:")
    click.echo(e)


#
# Create Advisory (RPM and image)
# advisory:create
#
@cli.command("create", short_help="Create a new advisory")
@click.option("--kind", '-k', required=True,
              type=click.Choice(['rpm', 'image']),
              help="Kind of Advisory to create. Affects boilerplate text.")
@click.option("--impetus", default='standard',
              type=click.Choice(elliottlib.constants.errata_valid_impetus),
              help="Impetus for the advisory creation [standard, cve, ga, test]")
@click.option("--date", required=False,
              default=default_release_date.strftime(YMD),
              callback=validate_release_date,
              help="Release date for the advisory. Optional. Format: YYYY-MM-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('--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, kind, impetus, date, assigned_to, manager, package_owner, 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-MM-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.

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 advisory:create

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

\b
    $ elliott --group openshift-3.5 advisory:create --yes -k image --date 2018-03-05
"""
    runtime.initialize()
    major = major_from_branch(runtime.group_config.branch)
    minor = minor_from_branch(runtime.group_config.branch)

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

    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 elliottlib.exceptions.ErrataToolUnauthenticatedException:
            exit_unauthenticated()
        except elliottlib.exceptions.ErrataToolUnauthorizedException:
            exit_unauthorized()
        except elliottlib.exceptions.ErrataToolError as err:
            red_prefix("Error searching advisories: ")
            click.echo(str(err))
            exit(1)
        else:
            if latest_advisory is None:
                red_prefix("No metadata discovered: ")
                click.echo("No advisory for {x}.{y} has been released in recent history, can not auto determine next release date".format(
                    x=major, y=minor))
                exit(1)

        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)
            click.secho("Adjusted release date to land on a Tuesday", fg='yellow')

        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)

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

    try:
        erratum = elliottlib.errata.new_erratum(
            et_data,
            kind=kind,
            release_date=release_date.strftime(YMD),
            create=yes,
            assigned_to=assigned_to,
            manager=manager,
            package_owner=package_owner
        )
    except elliottlib.exceptions.ErrataToolUnauthorizedException:
        exit_unauthorized()
    except elliottlib.exceptions.ErrataToolError as err:
        red_prefix("Error creating advisory: ")
        click.echo(str(err))
        exit(1)

    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))
    else:
        green_prefix("Would have created advisory: ")
        click.echo("")
        click.echo(erratum)


#
# 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("--auto",
              required=False,
              default=False, is_flag=True,
              help="AUTO mode, adds bugs based on --group")
@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", type=int, metavar='BUGID',
              multiple=True, required=False,
              help="Bugzilla IDs to add, conflicts with --auto [MULTIPLE]")
@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, auto, status, id, flag):
    """Find Red Hat Bugzilla bugs or add them to ADVISORY. Bugs can be
"swept" into the advisory either automatically (--auto), or by
manually specifying one or more bugs using the --id option. Mixing
--auto with --id is an invalid use-case. The two use cases are
described below:

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

AUTOMATIC: For this use-case the --group option MUST be provided. The
--group automatically determines the correct target-releases to search

for MODIFIED bugs in.

MANUAL: The --group option is not required if you are specifying bugs
manually. Provide one or more --id's for manual bug addition.

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

\b
    $ elliott --group openshift-3.7 advisory:find-bugs --auto --add 123456

    List bugs that would be added to advisory 123456 and set the bro_ok flag on the bugs (NOOP):

\b
    $ elliott --group openshift-3.7 advisory:find-bugs --auto --flag bro_ok 123456

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

\b
    $ elliott advisory:find-bugs --id 8675309 --id 7001337 --add 123456
"""
    if auto and len(id) > 0:
        raise click.BadParameter("Combining the automatic and manual bug attachment options is not supported")

    if auto:
        # Initialization ensures a valid group was provided
        runtime.initialize()
        bz_data = runtime.gitdata.load_data(key='bugzilla').data
    elif len(id) == 0:
        # No bugs were provided
        raise click.BadParameter("If not using --auto then one or more --id's must be provided")

    if auto:
        bug_ids = elliottlib.bzutil.search_for_bugs(bz_data, status)
    else:
        bzapi = elliottlib.bzutil.get_bzapi(bz_data)
        bug_ids = [bzapi.getbug(i) for i in id]

    bug_count = len(bug_ids)

    if advisory is not False:
        try:
            advs = Erratum(errata_id=advisory)
        except elliottlib.exceptions.ErrataToolUnauthenticatedException:
            exit_unauthenticated()

        if advs is False:
            red_prefix("Error: ")
            click.echo("Could not locate advisory {advs}".format(advs=advisory))
            exit(1)

        if auto:
            click.echo("Adding bugs to {advs} for target releases: {tr}".format(advs=advisory, tr=", ".join(bz_data['target_release'])))
        else:
            click.echo("Adding {count} bugs to {advs}".format(count=bug_count, advs=advisory))

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

        advs.addBugs([bug.id for bug in bug_ids])
        advs.commit()
    else: # Add bug is false (noop)
        green_prefix("Would have added {n} bugs: ".format(n=bug_count))
        click.echo(", ".join([str(b.bug_id) for b in bug_ids]))


#
# 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('--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=True, type=click.Choice(['rpm', 'image']),
              help='Find builds of the given KIND [rpm, image]')
@pass_runtime
def find_builds(runtime, advisory, builds, kind):
    """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 advisory:find-builds -k image

    ATTACH the latest OSE 3.6 rpm builds to advisory 123456:

\b
    $ elliott --group openshift-3.6 advisory: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 advisory:find-builds -k rpm -b megafrobber-1.0.1-2.el7 -b 93170
"""
    runtime.initialize()
    et_data = runtime.gitdata.load_data(key='erratatool').data
    product_version = et_data.get('product_version')
    base_tag = et_data.get('brew_tag')

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

    session = requests.Session()

    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 e:
            red_prefix("Error: ")
            click.echo(e)
            exit(1)
    else:
        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
            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]

    build_count = len(unshipped_builds)

    if advisory is not False:
        # Search and attach
        try:
            erratum = Erratum(errata_id=advisory)
            erratum.addBuilds([build.nvr for build in unshipped_builds], release=product_version)
            erratum.commit()
            click.secho("Attached build(s) successfully", fg='green', bold=True)
        except elliottlib.exceptions.ErrataToolUnauthenticatedException:
            exit_unauthenticated()
        except elliottlib.exceptions.BrewBuildException as e:
            red_prefix("Error attaching builds: ")
            click.echo(str(e))
            exit(1)
    else:
        click.echo("The following {n} builds ".format(n=build_count), 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)
@click.option('--details', is_flag=True, default=False,
              help="Print the full object of the advisory")
@click.pass_context
def get(ctx, details, 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.

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

    Basic one-line output for advisory 123456:

\b
    $ elliott advisory: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 advisory:get --json 123456 | jq '.errata'
    {
      "rhba": {
        "actual_ship_date": null,
        "assigned_to_id": 3002255,
        "batch_id": null,
        ...
"""
    try:
        advisory = Erratum(errata_id=advisory)
    except elliottlib.exceptions.ErrataToolUnauthenticatedException:
        exit_unauthenticated()

    if details:
        click.echo(advisory)
    else:
        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)


#
# 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 advisory: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 elliottlib.exceptions.ErrataToolUnauthenticatedException:
        exit_unauthenticated()
    except elliottlib.exceptions.ErrataToolError as err:
        red_prefix("Error: ")
        click.echo(str(err))
        exit(1)

#
# 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
advisory: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 advisory:add-metadata --impetus standard --kind image
"""
    runtime.initialize()
    release = release_from_branch(runtime.group_config.branch)

    try:
        advisory = Erratum(errata_id=advisory)
    except elliottlib.exceptions.ErrataToolUnauthenticatedException:
        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_prefix("Something weird may have happened: ")
        click.echo("Unexpected response from ET API: {code}".format(
            code=result.status_code))
        exit(1)


# -----------------------------------------------------------------------------
# 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(ex.message)
        sys.exit(1)


if __name__ == '__main__':
    main()