#!/usr/libexec/platform-python
# Script to check for known gotchas that occur between
# major versions of htcondor. This script will only check
# for gotchas to the next version.

# Imports Always needed
import os
import sys
import subprocess
import textwrap
import argparse
from enum import Enum

# Temporary needed imports (for a check)
import stat
import pwd
import grp
import platform
import re
# End imports

try:
    import htcondor2 as htcondor
    import classad2 as classad
except ImportError:
    print(
        textwrap.dedent(
            """
            Failed to find HTCondor Python bindings.
            Please check your current Python environment or install the bindings if needed:
            https://htcondor.readthedocs.io/en/latest/apis/python-bindings/install.html
            """
        ),
        file=sys.stderr,
    )
    sys.exit(1)


class Debug(Enum):
    WARNING = 0
    BASE = 1
    DETAILED = 2
    DEBUGGING = 3
    MAX = 4

    def __gt__(self, other):
        if self.__class__ is other.__class__:
            return self.value > other.value
        return NotImplemented

    def __ge__(self, other):
        if self.__class__ is other.__class__:
            return self.value >= other.value
        return NotImplemented


class System(Enum):
    STANDARD = 0
    CE = 1
    BOTH = 2


MAX_TAG_LEN = 10


class Check:
    def __init__(self, **kwargs):
        self.version = kwargs["version"]
        self.test = kwargs["test"]
        self.name = kwargs["name"]
        self.tag = kwargs["tag"]
        self.synopsis = kwargs.get("synopsis", "No check synopsis provided.")
        self.desc = kwargs.get("desc", "No detailed check description provided.")
        self.is_warning = kwargs.get("warning", False)
        self.daemons = kwargs.get("daemons", [])
        self.needs_root = kwargs.get("root", False)
        self.system = kwargs.get("system", System.BOTH)

        assert len(self.tag) <= MAX_TAG_LEN and len(self.tag) > 0
        assert type(self.version) is tuple
        assert type(self.daemons) is list

        self.desc = textwrap.dedent(self.desc)
    def __lt__(self, other):
        return self.version > other.version


NEXT_MAJOR_VERSION = (26, 0, 0)
CURRENT_VERSION = tuple(int(x) for x in htcondor.param["CONDOR_VERSION"].split("."))
IS_WINDOWS = os.name == "nt"
VERBOSITY = Debug.BASE


def format_print(msg, offset=0, newline=False, err=False, debug=Debug.BASE):
    """Custom print function to help with output spacing"""
    if debug > VERBOSITY:
        return

    f = sys.stderr if err else sys.stdout
    if newline:
        print(file=f)

    for line in textwrap.dedent(msg).split("\n"):
        if line.strip() == "":
            continue
        if "$(<EMPTY_LINE>)" in line:
            print(file=f)
            continue
        line_offset = offset + len(line)
        print(f"{line:>{line_offset}s}", file=f)


def make_version_str(version):
    """Convert Version Tuple into period seperated string"""
    return ".".join([f"{n}" for n in version])


def iter_list(l: list):
    """Recursively yield each item in a list of lists"""
    for item in l:
        if type(item) is list:
            yield from iter_list(item)
        else:
            yield item


# ===============================Incompatibility Check Functions===============================
def warn_multi_q_deprecation(*args, **kwargs):
    """Warn about multi-queue statement deprecation"""
    format_print(
        """
        Warning:
            The use of multiple queue statements in a single submit
            description file is now deprecated. This functionality
            will be removed during the lifetime of the V25 feature
            series.
        """,
        offset=4,
        newline=True,
        debug=Debug.WARNING,
    )


def warn_python_api_v2(*args, **kwargs):
    """Warn about removal of V1 Python API"""
    format_print(
        """
        Warning:
            The original Python API is no longer supported.  The `htcondor`
            and `classad` modules have been removed from HTCondor 25, and
            replaced with the `htcondor2` and `classad2` modules.
        $(<EMPTY_LINE>)
            The modules are almost entirely compatible, but scripts using
            the original API will not work until switched to the new API;
            see the following URL for details.
        $(<EMPTY_LINE>)
            https://htcondor.readthedocs.io/en/latest/apis/python-bindings/api/version2/migration-guide.html
        """,
        offset=4,
        newline=True,
        debug=Debug.WARNING,
    )


def check_job_router_syntax(*args, **kwargs):
    """Check for old job router configuration syntax."""
    deprecated_knobs = ["DEFAULTS", "ENTRIES", "ENTRIES_CMD", "ENTRIES_FILE"]
    found_knobs = []

    for knob in deprecated_knobs:
        full_name = "JOB_ROUTER_" + knob
        if full_name in htcondor.param:
            found_knobs.append(full_name)

    if len(found_knobs) > 0:
        format_print(
            """
            Warning:
                The following configuration macro(s):
            """,
            offset=4,
            newline=True,
            debug=Debug.WARNING,
        )
        for knob in found_knobs:
            format_print(f"- {knob}", offset=12, debug=Debug.WARNING)
        format_print(
            """
            Are no longer supported due to use of old syntax.
            The new configuration syntax for the job router is defined
            using JOB_ROUTER_ROUTE_NAMES and JOB_ROUTER_ROUTE_<name>.
            For more information visit:
                https://htcondor.readthedocs.io/en/latest/grid-computing/job-router.html#the-htcondor-job-router
            For assistance converting from old to new syntax use:
                condor_transfrom_ads -help convert
            """,
            offset=8,
            debug=Debug.WARNING,
        )
        return False
    return True


def check_job_transform_old_syntax(*args, **kwargs):
    """Check for obsolete syntax in schedd job transforms"""
    transforms = htcondor.param.get("JOB_TRANSFORM_NAMES")
    if transforms is not None:
        bad_syntax_xforms = []
        for name in transforms.replace(" ", ",").split(","):
            name = name.strip()
            xform = htcondor.param.get(f"JOB_TRANSFORM_{name}")
            if xform is not None:
                if xform.strip().startswith("["):
                    bad_syntax_xforms.append(f"JOB_TRANSFORM_{name}")

        l = len(bad_syntax_xforms)
        if l != 0:
            s = "s" if l > 1 else ""
            format_print(
                f"""
                Found {l} job transform{s} using obsolete syntax that is no longer
                supported. Please convert the following transforms to use the
                proper syntax:
                """,
                offset=4,
                newline=True,
            )
            for xform in bad_syntax_xforms:
                format_print(f"- {xform}", offset=8)
            format_print(
                """
                $(<EMPTY_LINE>)
                Proper Syntax Documentation:
                https://htcondor.readthedocs.io/en/latest/admin-manual/ap-policy-configuration.html#schedd-job-transforms
                """,
                offset=4
            )
            return False

    return True


def check_pslot_default(*args, **kwargs):
    """Check for use of RANK in startd config due to incompatibility with
    P-Slots on by default in V23.0.0"""
    for key in htcondor.param.keys():
        if key.lower().startswith("slot_type_"):
            return True
    rank = htcondor.param.get("RANK")
    if rank is None:
        pass
    else:
        try:
            temp = float(rank)
        except ValueError:
            format_print(
                f"""
                Issue found with StartD Rank:
                $(<EMPTY_LINE>)
                    Partitionable Slot is turned on by default in V23.0.0 of
                    HTCondor. This makes the configuration 'RANK = {rank}'
                    inconsequential. To continue having RANK be applied to jobs
                    ran on this EP configure the use of static slots by adding
                    either custom slot types or 'use FEATURE:StaticSlots'.
                """,
                newline=True,
            )
            return False
    return True


def uid2user(uid: int):
    udata = pwd.getpwuid(uid)
    return udata.pw_name if uid != 0 else "root"


def validate_token_permissions(info: os.stat_result, uid: int, perms: dict):
    """Function to verify token directory/file ownership and permissions"""

    errors = []

    if info.st_uid != uid:
        errors.append(f"Owner is not '{uid2user(uid)}'")

    keys = list(perms.keys())
    keys.sort()
    perms_str = "|".join(keys)

    fs_perm = info.st_mode & (stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)

    valid = False
    for perm in perms.values():
        if fs_perm == perm:
            valid = True
            break

    if not valid:
            errors.append(f"Invalid permissions ({oct(fs_perm)[2:]}): Requires ({perms_str})")

    return errors if len(errors) > 0 else None


def check_token_permissions(*args, **kwargs):
    """Check the token directories and stored tokens for the apropriate ownership and access permissions"""

    # Does not apply to Windows hosts
    if IS_WINDOWS:
        return True

    hostname = htcondor.param["FULL_HOSTNAME"]
    format_print(f"Hostname: {hostname}", newline=True, debug=Debug.DEBUGGING)

    try:
        ads = htcondor.Collector().query(
            htcondor.AdTypes.Master,
            constraint=f'Machine=="{hostname}"',
            projection=["RealUid"]
        )
        if len(ads) != 1:
            format_print("Error: No Master Ad returned for this host", newline=True, err=True)
            return False
    except Exception as e:
        format_print(f"Error: Failed to contact pool Collector: {repr(e)}", newline=True, err=True)
        return False

    master_ad = ads[0]

    if "RealUid" not in master_ad:
        format_print("Error: Master Ad missing 'RealUid'", newline=True, err=True)
        return False

    uid = master_ad["RealUid"]

    if uid == 0 and os.getuid() != 0:
        format_print("HTCondor is running as root on system so test requires root", newline=True)
        return False

    format_print("", newline=True, debug=Debug.DEBUGGING)

    TOKEN_DIRS = [
        htcondor.param["SEC_TOKEN_SYSTEM_DIRECTORY"],
        htcondor.param.get("SEC_TOKEN_DIRECTORY", "~/.condor/tokens.d"),
    ]

    issues = {}
    extra_msg = None

    for directory in TOKEN_DIRS:
        if len(directory) == 0:
            continue

        if directory[0] == "~":
            extra_msg = """
                        Warning:
                            SEC_TOKEN_DIRECTORY is relative to a home directory '~'
                            which is applied on a per user basis. This directory and
                            stored tokens are expected to be owned and only readable
                            by the user. This tool is not verifying this behavior.
                        """
            continue

        valid_perms = {"700" : stat.S_IRWXU}

        format_print(f"Validating: {directory}", offset=4, debug=Debug.DEBUGGING)
        errors = validate_token_permissions(os.stat(directory), uid, valid_perms)

        if errors is not None:
            issues[directory] = errors

        valid_perms.update({
            "400" : stat.S_IRUSR,
            "600" : stat.S_IRUSR | stat.S_IWUSR,
        })

        for f in os.listdir(directory):
            token_file = os.path.join(directory, f)
            format_print(f"Validating: {token_file}", offset=4, debug=Debug.DEBUGGING)
            errors = validate_token_permissions(os.stat(token_file), uid, valid_perms)

            if errors is not None:
                issues[token_file] = errors

    if len(issues) > 0:
        user = uid2user(uid)

        format_print(
            f"""
            Found {len(issues)} token related thing(s) with incorrect ownership
            or file permissions. Expected:
                - Token Directories-> {user}:700
                - Token Files-------> {user}:400|600|700
            """,
            newline=True,
            offset=4,
        )

        if extra_msg is not None:
            format_print(extra_msg, offset=4)

        if VERBOSITY < Debug.DETAILED:
            format_print(
                """
                For a complete list of files and their issues, increase
                the tool output verbosity with -v
                """,
                newline=True,
                offset=4,
            )

        format_print("", newline=True, debug=Debug.DETAILED)

        for item, errors in issues.items():
            i_type = "DIR" if item in TOKEN_DIRS else "FILE"
            format_print(f"{i_type:<4s}> {item}", offset=4, debug=Debug.DETAILED)
            for err in errors:
                format_print(f"* {err}", offset=10, debug=Debug.DETAILED)

        return False

    return True


def iter_dir(path: str):
    """Recursively iterate over directory returning files"""
    for item in os.listdir(path):
        full_path = os.path.join(path, item)
        if os.path.isdir(full_path):
            yield from iter_dir(full_path)
        else:
            yield full_path


def check_for_classad_unit_specifier(*args, **kwargs):
    """Warn about ClassAds no longer parsing unit specifiers"""
    SUFFIXED_NUM = re.compile("([^A-Za-z_]\\d+[BbKkMmGgTt])")
    CHECK_GANGLIA = False
    warn_macros = list()

    cron_period = re.compile(r".*_CRON_.*_PERIOD", re.IGNORECASE)
    benchmark_period = re.compile(r"BENCHMARKS_.*_PERIOD", re.IGNORECASE)

    # Check Configuration for numbers with unit suffixes
    # Note: false positives are possible
    for macro, value in htcondor.param.items():
        # Skip if value is not a string literal
        if type(value) is not str:
            continue

        # Well known false positives we can ignore
        if macro in ["DCSTATISTICS_TIMESPANS", "TRANSFER_IO_REPORT_TIMESPANS", "JAVA_MAXHEAP_ARGUMENT", "IPV6_ADDRESS"]:
            continue
        if macro.upper().startswith("FILE_TRANSFER_DISK_LOAD_THROTTLE_"):
            continue
        if cron_period.match(macro) or benchmark_period.match(macro):
            continue

        # Check daemon list for GangliaD
        if macro == "DAEMON_LIST":
            CHECK_GANGLIA = "GANGLIAD" in value.upper()
        # Search value for pattern
        if SUFFIXED_NUM.search(str(value)):
            warn_macros.append(macro)

    warn_ganglia = dict()
    METRICS_DIR = htcondor.param.get("GANGLIAD_METRICS_CONFIG_DIR", "/lItErAl-GaRbAgE")
    # Check GangliaD metrics config if in daemon list and metrics dir exists
    if CHECK_GANGLIA and os.path.exists(METRICS_DIR):
        for metric_file in iter_dir(METRICS_DIR):
            warn_lines = list()
            with open(metric_file, "r") as f:
                line_no = 0
                for line in f:
                    if SUFFIXED_NUM.search(line):
                        warn_lines.append(str(line_no))
                    line_no += 1
                if len(warn_lines) > 0:
                    warn_ganglia[metric_file] = warn_lines

    if len(warn_macros) > 0 or len(warn_ganglia) > 0:
        end = " and/or the Ganglia metrics configuration." if CHECK_GANGLIA else "."
        format_print(
            f"""
            Found potential uses of numeric literals containing suffixes in
            the configuration{end}
            """,
            newline=True,
            offset=4,
        )

        if len(warn_macros) > 0:
            format_print(
                """
                Potential Configuration Macros:
                """,
                newline=False,
                offset=6,
            )
            for macro in warn_macros:
                format_print(
                    f"- {macro}",
                    newline=False,
                    offset=8,
                )

        if len(warn_ganglia) > 0:
            format_print(
                """
                Potential Ganglia Metric Config Files:
                """,
                newline=False,
                offset=6,
            )
            for metric_file, warn_lines in warn_ganglia.items():
                lines = ",".join(warn_lines)
                format_print(
                    f"Lines {lines} in {metric_file}",
                    newline=False,
                    offset=8,
                )

        return False

    return True


def check_password_identity(*args, **kwargs):
    """Warn about pool PASSWORD identity change"""

    # Check Configuration for PASSWORD authentication
    name_re = re.compile('SEC_[a-zA-Z]*_AUTHENTICATION_METHODS', re.I)
    value_re = re.compile('PASSWORD', re.I)
    for macro, value in htcondor.param.items():
        if name_re.match(macro) and value_re.search(str(value)):
            format_print(
                """
                For pool PASSWORD authentication, the authenicated identity
                has changed from condor_pool@$(UID_DOMAIN) to condor@password.
                Your ALLOW_* and DENY_* configuration parameters may need to
                be changed accordingly.
                """,
                offset=4,
            )

            return False

    return True


# =============================================================================================
INCOMPATIBILITY_CHECKS = [
    Check(
        test=warn_multi_q_deprecation,
        name="Multiple Queue Statment Deprecation",
        tag="MULTI-Q",
        synopsis="The use of multiple queue statements in one submit file is deprecated",
        version=(23, 7, 1),
        daemons=["SCHEDD"],
        warning=True,
    ),
    Check(
        test=check_job_router_syntax,
        name="Old Job Router Syntax",
        tag="OLD-JRC",
        synopsis="Old Job Router Configuration Syntax has been removed",
        version=(24, 10, 2),
        daemons=["JOB_ROUTER"],
    ),
    Check(
        test=check_job_transform_old_syntax,
        name="Obsolete Syntax For Schedd Job Transforms",
        tag="JOB-XFORM",
        synopsis="""
                 This check will verify Schedd job transforms are not
                 using obsolete configuration syntax. This syntax is
                 the same as the old job router syntax.
                 """,
        version=(25, 8, 0),
        daemons=["SCHEDD"]
    ),
    Check(
        test=check_pslot_default,
        name="Enable P-Slots as default",
        tag="P-SLOT",
        synopsis="Use of StartD RANK doesn't function with new Partitionable Slot default",
        version=(23, 0, 0),
        daemons=["STARTD"],
    ),
    Check(
        test=check_token_permissions,
        name="Token Permissions",
        tag="TOK-PERM",
        synopsis="Verify ownership and permissions of stored tokens",
        desc="""
             This check will verify that the SEC_TOKEN_SYSTEM_DIRECTORY
             along with all stored tokens have the correct owner and
             file system permissions.
             $(<EMPTY_LINE>)
             Note: Requires root if HTCondor is running as root
             """,
        version=(23, 7, 1),
    ),
    Check(
        test=check_for_classad_unit_specifier,
        name="ClassAd Unit Specifiers",
        tag="AD-UNITS",
        synopsis="ClassAds no longer parses unit specifiers",
        desc="""
             This check will see if the configuration uses unit
             suffixes for numeric literals used in ClassAd expressions.
             If configured to use a GangliaD, then all metric
             configuration will also be checked for unit suffixes.
             $(<EMPTY_LINE>)
             Note: Unit suffixes are sill allowed in the Job Description
                   Language (JDL) for commands such as `request_disk` and
                   `request_memory`.
             """,
        version=(23, 9, 6),
    ),
    Check(
        test=check_password_identity,
        name="Pool PASSWORD identity changed",
        tag="PASSWORD",
        synopsis="Pool PASSWORD identity changed from condor_pool@UID_DOMAIN to condor@password",
        version=(23, 9, 6),
    ),
    Check(
        test=warn_python_api_v2,
        name="Python API Deprecation",
        tag="PY-API",
        synopsis="HTCondor Python API is no longer available",
        version=(25, 0, 0),
    ),
]

# Sort checks by version
INCOMPATIBILITY_CHECKS = sorted(INCOMPATIBILITY_CHECKS)

def list_tags():
    """Function to list all available check TAGs for filtering purposes"""
    format_print(
        """
        Incompatibility Check Tags:
               TAGS   |  Check
            ====================
        """,
        newline=True,
    )
    for check in INCOMPATIBILITY_CHECKS:
        format_print(f"{check.tag.upper():<{MAX_TAG_LEN}}| '{check.name}'", offset=4)


def list_checks(is_ce, ignore_list, only_list):
    format_print(
        f"""
        Incompatibility Checks for upgrading to HTCondor V{NEXT_MAJOR_VERSION[0]}:
            (For detailed descriptions use -v/--verbose)
        """,
        newline=True,
    )

    i = 0
    total = len(INCOMPATIBILITY_CHECKS)
    cnt_fmt = len(str(total))
    pad_len = (2 * cnt_fmt) + 4

    for check in INCOMPATIBILITY_CHECKS:
        i += 1
        if check.tag.upper() in ignore_list:
            continue

        if len(only_list) > 0:
            check_tag = True
            if "WARNINGS" in only_list and check.is_warning:
                check_tag = False
            if check_tag and check.tag.upper() not in only_list:
                continue

        if is_ce and check.system == System.STANDARD:
            continue
        elif not is_ce and check.system == System.CE:
            continue

        if IS_WINDOWS and check.needs_root:
            continue

        root = "Requires ROOT" if check.needs_root else ""
        ver = make_version_str(check.version)
        padding = " " * pad_len

        format_print(
            f"""
            ({i:>{cnt_fmt}}/{total}) [{check.tag:<{MAX_TAG_LEN}s}] '{check.name}'
            {padding}{root}
            {padding}Version : V{ver}
            {padding}Synopsis: {check.synopsis}
            """,
            offset=4,
            newline=(i == 1),
        )

        format_print(
            check.desc, offset=(4 + pad_len), newline=True, debug=Debug.DETAILED
        )
        format_print("", newline=True, debug=Debug.DETAILED)


def run_checks(args, ignore_list, only_list, daemons):
    """Function that runs all checks make it through filtering"""
    num_failed_checks = 0
    num_checks_ran = 0

    for check in INCOMPATIBILITY_CHECKS:
        if check.tag.upper() in ignore_list:
            continue

        if len(only_list) > 0:
            check_tag = True
            if "WARNINGS" in only_list and check.is_warning:
                check_tag = False
            if check_tag and check.tag.upper() not in only_list:
                continue

        if args.ce and check.system == System.STANDARD:
            continue
        elif not args.ce and check.system == System.CE:
            continue

        if IS_WINDOWS and check.needs_root:
            continue

        if check.needs_root and os.geteuid() != 0:
            format_print(
                f"Warning: Skipping check '{check.name}' because it requires root.",
            )
            continue

        if not args.all and check.version <= CURRENT_VERSION:
            continue

        if not args.warnings and check.is_warning:
            continue

        missing_daemon = False
        for daemon in check.daemons:
            if daemon.upper() not in daemons:
                missing_daemon = True
                break
        if missing_daemon:
            continue

        num_checks_ran += 1

        format_print(
            f"Checking {check.name} [{check.tag}]:", newline=True, debug=Debug.DETAILED
        )

        if not check.test(args):
            num_failed_checks += 1
            format_print(f"Check {check.name} [{check.tag}] failed", offset=8, debug=Debug.DETAILED)
        else:
            format_print("No issue found with check", offset=8, debug=Debug.DETAILED)

    return (num_failed_checks, num_checks_ran)


class NegateAction(argparse.Action):
    def __call__(self, parser, args, values, option_string=None):
        setattr(args, self.dest, option_string.strip("-").startswith("no") != True)


def parse_args(has_extend_action: bool):
    """Function to parse tool command line arguments"""

    list_action = "extend" if has_extend_action else "append"

    parser = argparse.ArgumentParser(
        prog="condor_upgrade_check",
        description=textwrap.dedent(
            """
            Tool to help check for known incompatabilites with a current
            HTCondor install when upgrading to a new major version.

            This should be ran on a host with HTCondor installed prior
            to upgrading to new major version (V23 -> V24) to check current
            installation for potential issues that may occur during an
            upgrade.

            Examples:
                condor_upgrade_check
                condor_upgrade_check --tags
                condor_upgrade_check --dump
                condor_upgrade_check -i BAR -vv
                condor_upgrade_check -ce --only FOO BAR BAZ --no-warnings
            """
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )

    parser.add_argument(
        "-ce",
        "--ce",
        "-CE",
        "--CE",
        dest="ce",
        action="store_true",
        help="Do checks for HTCondor-CE environment.",
    )

    parser.add_argument(
        "-a",
        "--all",
        action="store_true",
        help="Run all available checks including ones for issues introduced in versions older than the installed system.",
    )

    parser.add_argument(
        "-i",
        "--ignore",
        nargs="+",
        action=list_action,
        metavar="TAG",
        default=[],
        help="Skip the provided TAGs. This option takes precedence over the --only option.",
    )

    parser.add_argument(
        "-o",
        "--only",
        nargs="+",
        action=list_action,
        metavar="TAG",
        default=[],
        help="Check only the provided TAGs. Use the tag WARNINGS to only check warnings",
    )

    parser.add_argument(
        "-w",
        "--warnings",
        "--no-warnings",
        dest="warnings",
        action=NegateAction,
        nargs=0,
        default=True,
        help="Enable or disable persistent upgrade warnings (i.e. issues that can't be tested).",
    )

    parser.add_argument(
        "-d",
        "--dump",
        action="store_true",
        help="Display available incompatibility checks.",
    )

    parser.add_argument(
        "-v",
        "--verbose",
        action="count",
        default=1,
        help="Increase tool output verbosity.",
    )

    parser.add_argument(
        "-t",
        "--tags",
        action="store_true",
        help="Display the incompatibility check TAGs which can be used with --only & --ignore",
    )

    return parser.parse_args()


def main():
    if len(INCOMPATIBILITY_CHECKS) == 0:
        ver = make_version_str(NEXT_MAJOR_VERSION)
        format_print(
            f"Currently no known incompatibilities to check for in preparation for V{ver}",
            newline=True,
            offset=4,
        )
        sys.exit(0)

    has_extend_action = sys.version_info[0] >= 3 and sys.version_info[1] >= 8
    args = parse_args(has_extend_action)

    global VERBOSITY
    try:
        VERBOSITY = Debug(args.verbose)
    except ValueError:
        VERBOSITY = Debug.MAX

    ignore_list = (
        [tag.upper() for tag in args.ignore]
        if has_extend_action
        else [tag.upper() for tag in iter_list(args.ignore)]
    )
    only_list = (
        [tag.upper() for tag in args.only]
        if has_extend_action
        else [tag.upper() for tag in iter_list(args.only)]
    )

    if args.tags:
        list_tags()
    elif args.dump:
        list_checks(args.ce, ignore_list, only_list)
    else:
        system = "HTCondor"

        if args.ce:
            system += "-CE"
            global CURRENT_VERSION
            config = "/etc/condor-ce/condor_config"
            if not os.path.exists(config):
                format_print(
                    f"""Error: {system} Specified but failed to locate {config}""",
                    err=True,
                    newline=True,
                )
                sys.exit(1)
            os.environ["CONDOR_CONFIG"] = config
            htcondor.reload_config()
            CURRENT_VERSION = tuple(
                int(x) for x in htcondor.param["CONDOR_VERSION"].split(".")
            )

        format_print(
            f"Checking {system} for possible issues with known incompatibilities upgrading to V{NEXT_MAJOR_VERSION[0]}",
            newline=True,
        )

        daemons = htcondor.param["DAEMON_LIST"].upper()

        num_failed, num_ran = run_checks(args, ignore_list, only_list, daemons)
        format_print(
            f"""
            Final Report:
                {num_ran} checks tested against currently installed {system}.
            """,
            newline=True,
            debug=Debug.DETAILED,
        )
        if num_failed == 0:
            format_print(
                f"""
                No issues found with installed {system} for known incompatibilities.
                Upgrade to V{NEXT_MAJOR_VERSION[0]} should be safe.
                """,
                newline=(Debug.DETAILED > VERBOSITY),
                offset=4,
            )
        else:
            format_print(
                f"Current install failed {num_failed} checks.",
                offset=4,
                debug=Debug.DETAILED,
            )

    format_print(
        """
        To ask any questions regarding incompatibilities email:
            htcondor-users@g-groups.wisc.edu
        """,
        newline=True,
    )


if __name__ == "__main__":
    main()
