#!/usr/bin/env python3

# Copyright (c) 2016 Cisco and/or its affiliates.
# Copyright (c) 2018 Vinci Consulting Corp.  All rights reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""VPP Configuration Main Entry"""
from __future__ import absolute_import, division, print_function

import re
import os
import sys
import logging
import argparse

from vpplib.AutoConfig import AutoConfig
from vpplib.VPPUtil import VPPUtil

#  Python2/3 compatible
try:
    input = raw_input  # noqa
except NameError:
    pass

VPP_DRYRUNDIR = "/vpp/vpp-config/dryrun"
VPP_AUTO_CONFIGURATION_FILE = "/vpp/vpp-config/configs/auto-config.yaml"
VPP_HUGE_PAGE_FILE = "/vpp/vpp-config/dryrun/sysctl.d/80-vpp.conf"
VPP_STARTUP_FILE = "/vpp/vpp-config/dryrun/vpp/startup.conf"
VPP_GRUB_FILE = "/vpp/vpp-config/dryrun/default/grub"
VPP_REAL_HUGE_PAGE_FILE = "/etc/sysctl.d/80-vpp.conf"
VPP_REAL_STARTUP_FILE = "/etc/vpp/startup.conf"
VPP_REAL_GRUB_FILE = "/etc/default/grub"

rootdir = ""


def autoconfig_yn(question, default):
    """
    Ask the user a yes or no question.

    :param question: The text of the question
    :param default: Value to be returned if '\n' is entered
    :type question: string
    :type default: string
    :returns: The Answer
    :rtype: string
    """
    input_valid = False
    default = default.lower()
    answer = ""
    while not input_valid:
        answer = input(question)
        if len(answer) == 0:
            answer = default
        if re.findall(r"[YyNn]", answer):
            input_valid = True
            answer = answer[0].lower()
        else:
            print("Please answer Y, N or Return.")

    return answer


def autoconfig_cp(node, src, dst):
    """
    Copies a file, saving the original if needed.

    :param node: Node dictionary with cpuinfo.
    :param src: Source File
    :param dst: Destination file
    :type node: dict
    :type src: string
    :type dst: string
    :raises RuntimeError: If command fails
    """

    # If the destination file exist, create a copy if one does not already
    # exist
    ofile = dst + ".orig"
    (ret, stdout, stderr) = VPPUtil.exec_command("ls {}".format(dst))
    if ret == 0:
        cmd = "cp {} {}".format(dst, ofile)
        (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
        if ret != 0:
            raise RuntimeError(
                "{} failed on node {} {} {}".format(cmd, node["host"], stdout, stderr)
            )

    # Copy the source file
    cmd = "cp {} {}".format(src, os.path.dirname(dst))
    (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
    if ret != 0:
        raise RuntimeError("{} failed on node {} {}".format(cmd, node["host"], stderr))


def autoconfig_diff(node, src, dst):
    """
    Returns the diffs of 2 files.

    :param node: Node dictionary with cpuinfo.
    :param src: Source File
    :param dst: Destination file
    :type node: dict
    :type src: string
    :type dst: string
    :returns: The Answer
    :rtype: string
    :raises RuntimeError: If command fails
    """

    # Diff the files and return the output
    cmd = "diff {} {}".format(src, dst)
    (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
    if stderr != "":
        raise RuntimeError(
            "{} failed on node {} {} {}".format(cmd, node["host"], ret, stderr)
        )

    return stdout


def autoconfig_show_system():
    """
    Shows the system information.

    """

    acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)

    acfg.discover()

    acfg.sys_info()


def autoconfig_hugepage_apply(node, ask_questions=True):
    """
    Apply the huge page configuration.
    :param node: The node structure
    :type node: dict
    :param ask_questions: When True ask the user questions
    :type ask_questions: bool
    :returns: -1 if the caller should return, 0 if not
    :rtype: int

    """

    diffs = autoconfig_diff(node, VPP_REAL_HUGE_PAGE_FILE, rootdir + VPP_HUGE_PAGE_FILE)
    if diffs != "":
        print("These are the changes we will apply to")
        print("the huge page file ({}).\n".format(VPP_REAL_HUGE_PAGE_FILE))
        print(diffs)
        if ask_questions:
            answer = autoconfig_yn(
                "\nAre you sure you want to apply these changes [Y/n]? ", "y"
            )
            if answer == "n":
                return -1

        # Copy and sysctl
        autoconfig_cp(node, rootdir + VPP_HUGE_PAGE_FILE, VPP_REAL_HUGE_PAGE_FILE)
        cmd = "sysctl -p {}".format(VPP_REAL_HUGE_PAGE_FILE)
        (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
        if ret != 0:
            raise RuntimeError(
                "{} failed on node {} {} {}".format(cmd, node["host"], stdout, stderr)
            )
    else:
        print("\nThere are no changes to the huge page configuration.")

    return 0


def autoconfig_vpp_apply(node, ask_questions=True):
    """
    Apply the vpp configuration.

    :param node: The node structure
    :type node: dict
    :param ask_questions: When True ask the user questions
    :type ask_questions: bool
    :returns: -1 if the caller should return, 0 if not
    :rtype: int

    """

    diffs = autoconfig_diff(node, VPP_REAL_STARTUP_FILE, rootdir + VPP_STARTUP_FILE)
    if diffs != "":
        print("These are the changes we will apply to")
        print("the VPP startup file ({}).\n".format(VPP_REAL_STARTUP_FILE))
        print(diffs)
        if ask_questions:
            answer = autoconfig_yn(
                "\nAre you sure you want to apply these changes [Y/n]? ", "y"
            )
            if answer == "n":
                return -1

        # Copy the VPP startup
        autoconfig_cp(node, rootdir + VPP_STARTUP_FILE, VPP_REAL_STARTUP_FILE)
    else:
        print("\nThere are no changes to VPP startup.")

    return 0


def autoconfig_grub_apply(node, ask_questions=True):
    """
    Apply the grub configuration.

    :param node: The node structure
    :type node: dict
    :param ask_questions: When True ask the user questions
    :type ask_questions: bool
    :returns: -1 if the caller should return, 0 if not
    :rtype: int

    """

    print("\nThe configured grub cmdline looks like this:")
    configured_cmdline = node["grub"]["default_cmdline"]
    current_cmdline = node["grub"]["current_cmdline"]
    print(configured_cmdline)
    print("\nThe current boot cmdline looks like this:")
    print(current_cmdline)
    if ask_questions:
        question = "\nDo you want to keep the current boot cmdline [Y/n]? "
        answer = autoconfig_yn(question, "y")
        if answer == "y":
            return

    node["grub"]["keep_cmdline"] = False

    # Diff the file
    diffs = autoconfig_diff(node, VPP_REAL_GRUB_FILE, rootdir + VPP_GRUB_FILE)
    if diffs != "":
        print("These are the changes we will apply to")
        print("the GRUB file ({}).\n".format(VPP_REAL_GRUB_FILE))
        print(diffs)
        if ask_questions:
            answer = autoconfig_yn(
                "\nAre you sure you want to apply these changes [y/N]? ", "n"
            )
            if answer == "n":
                return -1

        # Copy and update grub
        autoconfig_cp(node, rootdir + VPP_GRUB_FILE, VPP_REAL_GRUB_FILE)
        distro = VPPUtil.get_linux_distro()
        if distro[0] == "Ubuntu":
            cmd = "update-grub"
        else:
            cmd = "grub2-mkconfig -o /boot/grub2/grub.cfg"

        (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
        if ret != 0:
            raise RuntimeError(
                "{} failed on node {} {} {}".format(cmd, node["host"], stdout, stderr)
            )

        print("There have been changes to the GRUB config a", end=" ")
        print("reboot will be required.")
        return -1
    else:
        print("\nThere are no changes to the GRUB config.")

    return 0


def autoconfig_apply(ask_questions=True):
    """
    Apply the configuration.

    Show the diff of the dryrun file and the actual configuration file
    Copy the files from the dryrun directory to the actual file.
    Peform the system function

    :param ask_questions: When true ask the user questions
    :type ask_questions: bool

    """

    vutil = VPPUtil()
    pkgs = vutil.get_installed_vpp_pkgs()
    if len(pkgs) == 0:
        print("\nVPP is not installed, Install VPP with option 4.")
        return

    acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)

    if ask_questions:
        print("\nWe are now going to configure your system(s).\n")
        answer = autoconfig_yn("Are you sure you want to do this [Y/n]? ", "y")
        if answer == "n":
            return

    nodes = acfg.get_nodes()
    for i in nodes.items():
        node = i[1]

        # Check the system resources
        if not acfg.min_system_resources(node):
            return

        # Stop VPP
        VPPUtil.stop(node)

        # Huge Pages
        ret = autoconfig_hugepage_apply(node, ask_questions)
        if ret != 0:
            return

        # VPP
        ret = autoconfig_vpp_apply(node, ask_questions)
        if ret != 0:
            return

        # Grub
        ret = autoconfig_grub_apply(node, ask_questions)
        if ret != 0:
            # We can still start VPP, even if we haven't configured grub
            VPPUtil.start(node)
            return

        # Everything is configured start vpp
        VPPUtil.start(node)


def autoconfig_dryrun(ask_questions=True):
    """
    Execute the dryrun function.

    :param ask_questions: When true ask the user for paraameters
    :type ask_questions: bool

    """

    acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE, clean=True)

    # Stop VPP on each node
    nodes = acfg.get_nodes()
    for i in nodes.items():
        node = i[1]
        VPPUtil.stop(node)

    # Discover
    acfg.discover()

    # Check the system resources
    nodes = acfg.get_nodes()
    for i in nodes.items():
        node = i[1]
        if not acfg.min_system_resources(node):
            return

    # Modify the devices
    if ask_questions:
        acfg.modify_devices()
    else:
        acfg.update_interfaces_config()

    # If there are no interfaces, just return
    for i in nodes.items():
        node = i[1]
        if not acfg.has_interfaces(node):
            print(
                "\nThere are no VPP interfaces configured, please configure at least 1."
            )
            return

    # Modify CPU
    acfg.modify_cpu(ask_questions)

    # Calculate the cpu parameters
    acfg.calculate_cpu_parameters()

    # Acquire TCP stack parameters
    if ask_questions:
        acfg.acquire_tcp_params()

    # Apply the startup
    acfg.apply_vpp_startup()

    # Apply the grub configuration
    acfg.apply_grub_cmdline()

    # Huge Pages
    if ask_questions:
        acfg.modify_huge_pages()
    acfg.apply_huge_pages()


def autoconfig_install():
    """
    Install or Uninstall VPP.

    """

    # Since these commands will take a while, we
    # want to see the progress
    logger = logging.getLogger()

    acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)
    vutil = VPPUtil()

    nodes = acfg.get_nodes()
    for i in nodes.items():
        node = i[1]

        pkgs = vutil.get_installed_vpp_pkgs()

        if len(pkgs) > 0:
            print("\nThese packages are installed on node {}".format(node["host"]))
            print("{:25} {}".format("Name", "Version"))
            for pkg in pkgs:
                try:
                    print("{:25} {}".format(pkg["name"], pkg["version"]))
                except KeyError:
                    print("{}".format(pkg["name"]))

            question = "\nDo you want to uninstall these "
            question += "packages [y/N]? "
            answer = autoconfig_yn(question, "n")
            if answer == "y":
                logger.setLevel(logging.INFO)
                vutil.uninstall_vpp(node)
        else:
            print("\nThere are no VPP packages on node {}.".format(node["host"]))
            question = "Do you want to install VPP [Y/n]? "
            answer = autoconfig_yn(question, "y")
            if answer == "y":
                question = "Do you want to install the release version [Y/n]? "
                answer = autoconfig_yn(question, "y")
                if answer == "y":
                    branch = "release"
                else:
                    branch = "master"
                logger.setLevel(logging.INFO)
                vutil.install_vpp(node, branch)

    # Set the logging level back
    logger.setLevel(logging.ERROR)


def autoconfig_patch_qemu():
    """
    Patch the correct qemu version that is needed for openstack

    """

    # Since these commands will take a while, we
    # want to see the progress
    logger = logging.getLogger()

    acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)

    nodes = acfg.get_nodes()
    for i in nodes.items():
        node = i[1]

        logger.setLevel(logging.INFO)
        acfg.patch_qemu(node)


def autoconfig_ipv4_setup():
    """
    Setup IPv4 interfaces

    """

    acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)
    acfg.ipv4_interface_setup()


def autoconfig_create_iperf_vm():
    """
    Setup IPv4 interfaces

    """

    acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)
    acfg.destroy_iperf_vm("iperf-server")
    acfg.create_and_bridge_iperf_virtual_interface()
    acfg.create_iperf_vm("iperf-server")


def autoconfig_not_implemented():
    """
    This feature is not implemented

    """

    print("\nThis Feature is not implemented yet....")


def autoconfig_basic_test_menu():
    """
    The auto configuration basic test menu

    """

    basic_menu_text = "\nWhat would you like to do?\n\n\
1) List/Create Simple IPv4 Setup\n\
2) Create an iperf VM and Connect to VPP an interface\n\
9 or q) Back to main menu."

    print("{}".format(basic_menu_text))

    input_valid = False
    answer = ""
    while not input_valid:
        answer = input("\nCommand: ")
        if len(answer) > 1:
            print("Please enter only 1 character.")
            continue
        if re.findall(r"[Qq1-29]", answer):
            input_valid = True
            answer = answer[0].lower()
        else:
            print("Please enter a character between 1 and 2 or 9.")

        if answer == "9":
            answer = "q"

    return answer


def autoconfig_basic_test():
    """
    The auto configuration basic test menu

    """
    vutil = VPPUtil()
    pkgs = vutil.get_installed_vpp_pkgs()
    if len(pkgs) == 0:
        print("\nVPP is not installed, install VPP with option 4.")
        return

    answer = ""
    while answer != "q":
        answer = autoconfig_basic_test_menu()
        if answer == "1":
            autoconfig_ipv4_setup()
        elif answer == "2":
            autoconfig_create_iperf_vm()
        elif answer == "9" or answer == "q":
            return
        else:
            autoconfig_not_implemented()


def autoconfig_main_menu():
    """
    The auto configuration main menu

    """

    main_menu_text = "\nWhat would you like to do?\n\n\
1) Show basic system information\n\
2) Dry Run (Saves the configuration files in {}/vpp/vpp-config/dryrun.\n\
3) Full configuration (WARNING: This will change the system configuration)\n\
4) List/Install/Uninstall VPP.\n\
q) Quit".format(
        rootdir, rootdir
    )

    # 5) Dry Run from {}/vpp/vpp-config/auto-config.yaml (will not ask questions).\n\
    # 6) Install QEMU patch (Needed when running openstack).\n\

    print("{}".format(main_menu_text))

    input_valid = False
    answer = ""
    while not input_valid:
        answer = input("\nCommand: ")
        if len(answer) > 1:
            print("Please enter only 1 character.")
            continue
        if re.findall(r"[Qq1-4]", answer):
            input_valid = True
            answer = answer[0].lower()
        else:
            print("Please enter a character between 1 and 4 or q.")

    return answer


def autoconfig_main():
    """
    The auto configuration main entry point

    """

    # Setup
    autoconfig_setup()

    answer = ""
    while answer != "q":
        answer = autoconfig_main_menu()
        if answer == "1":
            autoconfig_show_system()
        elif answer == "2":
            autoconfig_dryrun()
        elif answer == "3":
            autoconfig_apply()
        elif answer == "4":
            autoconfig_install()
        elif answer == "q":
            return
        else:
            autoconfig_not_implemented()


def autoconfig_setup(ask_questions=True):
    """
    The auto configuration setup function.

    We will copy the configuration files to the dryrun directory.

    """

    global rootdir

    distro = VPPUtil.get_linux_distro()
    if distro[0] == "Ubuntu":
        rootdir = "/usr/local"
    else:
        rootdir = "/usr"

    # If there is a system configuration file use that, if not use the initial auto-config file
    filename = rootdir + VPP_AUTO_CONFIGURATION_FILE
    if os.path.isfile(filename) is True:
        acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)
    else:
        raise RuntimeError(
            "The Auto configuration file does not exist {}".format(filename)
        )

    if ask_questions:
        print("\nWelcome to the VPP system configuration utility")

        print("\nThese are the files we will modify:")
        print("    /etc/vpp/startup.conf")
        print("    /etc/sysctl.d/80-vpp.conf")
        print("    /etc/default/grub")

        print(
            "\nBefore we change them, we'll create working copies in "
            "{}".format(rootdir + VPP_DRYRUNDIR)
        )
        print(
            "Please inspect them carefully before applying the actual "
            "configuration (option 3)!"
        )

    nodes = acfg.get_nodes()
    for i in nodes.items():
        node = i[1]

        if (os.path.isfile(rootdir + VPP_STARTUP_FILE) is not True) and (
            os.path.isfile(VPP_REAL_STARTUP_FILE) is True
        ):
            autoconfig_cp(
                node, VPP_REAL_STARTUP_FILE, "{}".format(rootdir + VPP_STARTUP_FILE)
            )
        if (os.path.isfile(rootdir + VPP_HUGE_PAGE_FILE) is not True) and (
            os.path.isfile(VPP_REAL_HUGE_PAGE_FILE) is True
        ):
            autoconfig_cp(
                node, VPP_REAL_HUGE_PAGE_FILE, "{}".format(rootdir + VPP_HUGE_PAGE_FILE)
            )
        if (os.path.isfile(rootdir + VPP_GRUB_FILE) is not True) and (
            os.path.isfile(VPP_REAL_GRUB_FILE) is True
        ):
            autoconfig_cp(
                node, VPP_REAL_GRUB_FILE, "{}".format(rootdir + VPP_GRUB_FILE)
            )

        # Be sure the uio_pci_generic driver is installed
        cmd = "modprobe uio_pci_generic"
        (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
        if ret != 0:
            logging.warning("{} failed on node {} {}".format(cmd, node["host"], stderr))


# noinspection PyUnresolvedReferences
def execute_with_args(args):
    """
    Execute the configuration utility with agruments.

    :param args: The Command line arguments
    :type args: tuple
    """

    # Setup
    autoconfig_setup(ask_questions=False)

    # Execute the command
    if args.show:
        autoconfig_show_system()
    elif args.dry_run:
        autoconfig_dryrun(ask_questions=False)
    elif args.apply:
        autoconfig_apply(ask_questions=False)
    else:
        autoconfig_not_implemented()


def config_main():
    """
    The vpp configuration utility main entry point.

    """

    # Check for root
    if not os.geteuid() == 0:
        sys.exit("\nPlease run the VPP Configuration Utility as root.")

    if len(sys.argv) > 1 and ((sys.argv[1] == "-d") or (sys.argv[1] == "--debug")):
        logging.basicConfig(level=logging.DEBUG)
    else:
        logging.basicConfig(level=logging.ERROR)

    # If no arguments were entered, ask the user questions to
    # get the main parameters
    if len(sys.argv) == 1:
        autoconfig_main()
        return
    elif len(sys.argv) == 2 and ((sys.argv[1] == "-d") or (sys.argv[1] == "--debug")):
        autoconfig_main()
        return

    # There were arguments specified, so execute the utility using
    # command line arguments
    description = "The VPP configuration utility allows the user to "
    "configure VPP in a simple and safe manner. The utility takes input "
    "from the user or the specified .yaml file. The user should then "
    "examine these files to be sure they are correct and then actually "
    "apply the configuration. When run without arguments the utility run "
    "in an interactive mode"

    main_parser = argparse.ArgumentParser(
        prog="arg-test",
        description=description,
        epilog='See "%(prog)s help COMMAND" for help on a specific command.',
    )
    main_parser.add_argument(
        "--apply", "-a", action="store_true", help="Apply the cofiguration."
    )
    main_parser.add_argument(
        "--dry-run",
        "-dr",
        action="store_true",
        help="Create the dryrun configuration files.",
    )
    main_parser.add_argument(
        "--show", "-s", action="store_true", help="Shows basic system information"
    )
    main_parser.add_argument(
        "--debug", "-d", action="count", help="Print debug output (multiple levels)"
    )

    args = main_parser.parse_args()

    return execute_with_args(args)


if __name__ == "__main__":
    config_main()