#!/usr/bin/python

"""
Interface Rename
"""

import sys, logging
from optparse import OptionParser, OptionGroup
from os.path import join as joinpath, normpath, exists as pathexists
from subprocess import Popen

try:
    import cStringIO as StringIO
except ImportError:
    import StringIO

import xcp.logger as LOG
from xcp.pci import PCI
from xcp.net.biosdevname import all_devices_all_names
from xcp.net.ip import ip_link_set_name
from xcp.net.ifrename.logic import rename
from xcp.net.ifrename.macpci import MACPCI
from xcp.net.ifrename.static import StaticRules
from xcp.net.ifrename.dynamic import DynamicRules
from xcp.net.ifrename.util import niceformat
from xcp.net.mac import MAC

__version__ = "2.0.0"

DATA_DIR = normpath("/etc/sysconfig/network-scripts/interface-rename-data")
BACKUP_DIR = joinpath(DATA_DIR, ".from_install")

LOG_PATH = normpath("/var/log/interface-rename.log")
SRULE_FILE = "static-rules.conf"
DRULE_FILE = "dynamic-rules.json"

def run(dryrun, update, args):
    """
    Run the main logic
    """

    # Grab the current state from biosdevname
    current_eths = all_devices_all_names()
    current_state = []

    for nic in current_eths.keys():
        eth = current_eths[nic]

        if not ( "BIOS device" in eth and
                 "Kernel name" in eth and
                 "Assigned MAC" in eth and
                 "Bus Info" in eth and
                 "all_ethN" in eth["BIOS device"] and
                 "physical" in eth["BIOS device"]
                  ):
            LOG.error("Interface information for '%s' from biosdevname is "
                      "incomplete; Discarding."
                      % (eth.get("Kernel name", "Unknown"),))

        try:
            current_state.append(
                MACPCI(eth["Assigned MAC"],
                       eth["Bus Info"],
                       kname = eth["Kernel name"],
                       order = int(eth["BIOS device"]["all_ethN"][3:]),
                       ppn = eth["BIOS device"]["physical"],
                       label = eth.get("SMBIOS Label", "")
                       ))
        except Exception, e:
            LOG.error("Can't generate current state for interface '%s' - "
                      "%s" % (eth, e))
    current_state.sort()

    LOG.debug("Current state = %s" % (niceformat(current_state),))

    # Parse the static rules
    sr = StaticRules(joinpath(DATA_DIR, SRULE_FILE))

    if not sr.load_and_parse():
        LOG.warning("Failed to parse the static rules.  Attempting to continue "
                    "without any")

    sr.generate(current_state)

    LOG.debug("StaticRules Formulae = %s" %(niceformat(sr.formulae),))
    LOG.debug("StaticRules Rules = %s" %(niceformat(sr.rules),))

    # Parse the dynamic rules
    dr = DynamicRules(joinpath(DATA_DIR, DRULE_FILE))

    if not dr.load_and_parse():
        LOG.warning("Failed to parse the dynamic rules.  Attempting to continue"
                    " without any")

    LOG.debug("DynamicRules Lastboot = %s" % (niceformat(dr.lastboot),))
    LOG.debug("DynamicRules Old = %s" % (niceformat(dr.old),))

    # If we are attempting a manual update
    if update:
        # Parse the args as static rules
        ur = StaticRules(fd=StringIO.StringIO('\n'.join(args)))

        if not ur.load_and_parse():
            LOG.error("Failed to parse the update rules")
            return

        LOG.debug("UpdateRules Formulae = %s" %(niceformat(ur.formulae),))
        ur.generate(current_state)
        LOG.debug("UpdateRules Rules = %s" %(niceformat(ur.rules),))

        if len(ur.rules) < 1:
            LOG.error("No valid update rules after processing.  Doing nothing")
            return

        all_srule_eths = sr.formulae.keys() + ur.formulae.keys()
        all_srules = sr.rules + ur.rules
        if ( len(all_srule_eths) != len(set(all_srule_eths)) or
             len(all_srules) != len(set(all_srules)) ):
            LOG.error("Update rules and static rules overlap.  Doing nothing")
            return
        else:
            sr.rules.extend(ur.rules)


    # Invoke the renaming logic
    try:
        transactions = rename(static_rules = sr.rules,
                              cur_state = current_state,
                              last_state = dr.lastboot,
                              old_state = dr.old)
    except Exception, e:
        LOG.critical("Problem from rename logic: %s.  Giving up" % (e,))
        return

    # If we are performing an manual update and not already logging to stdout,
    # start logging so the user sees messages regarding needing to reboot
    if update and not dryrun:
        LOG.logToStdout()

    # Apply transactions, or explicitly state that there are none
    if len (transactions):
        if update:
            LOG.info("Performing manual update of rules.  Not actually "
                     "renaming interfaces")
        else:
            for src, dst in transactions:
                if dryrun:
                    LOG.info("Would rename '%s' to '%s' if not dry run"
                             % (src, dst))
                else:
                    ip_link_set_name(src, dst)
    else:
        LOG.info("No transactions.  No need to rename any nics")

    # Regenerate dynamic configuration
    def macpci_as_list(x):
        return [str(x.mac), str(x.pci), x.tname]

    new_lastboot = map(macpci_as_list, current_state)
    new_macs = frozenset( (x.mac for x in current_state) )
    new_old = map(macpci_as_list, filter( lambda x: x.mac not in new_macs,
                                          dr.lastboot + dr.old ))

    LOG.debug("New lastboot data=\n%s" % (niceformat(new_lastboot),))
    LOG.debug("New old data=\n%s" % (niceformat(new_old),))

    if dryrun:
        LOG.info("Would update the dynamic configuration if not dry run")
    else:
        dr.lastboot = new_lastboot
        dr.old = new_old
        dr.save()

    if update and not len(transactions):
        LOG.info("Done - Please reboot to safely rename interfaces")
    else:
        LOG.info("All done")

def listdevs():
    """
    List physical devices and associated information.
    """

    # Grab the current state from biosdevname
    current_eths = all_devices_all_names()

    # Column titles
    eths = [("Name", "MAC", "PCI", "ethN", "Phys", "SMBios", "Driver",
             "Version", "Firmware")]

    # Sort keys, ethN first in ascending N, followed by others
    keys = current_eths.keys()
    order = ( sorted( (k for k in keys if k.startswith('eth')),
                      key=lambda x : int(x[3:])) +
              sorted( (k for k in keys if not k.startswith('eth'))))

    for nic in order:
        eth = current_eths[nic]

        if not ( "BIOS device" in eth and
                 "Kernel name" in eth and
                 "Assigned MAC" in eth and
                 "Bus Info" in eth and
                 "all_ethN" in eth["BIOS device"] and
                 "physical" in eth["BIOS device"] and
                 "Driver" in eth
                 ):
            LOG.error("Interface information for '%s' from biosdevname is "
                      "incomplete; Discarding."
                      % (eth.get("Kernel name", "Unknown"),))

        try:
            eths.append(
                ( eth["Kernel name"], str(MAC(eth["Assigned MAC"])),
                  str(PCI(eth["Bus Info"])), eth["BIOS device"]["all_ethN"],
                  eth["BIOS device"]["physical"],
                  eth.get("SMBIOS Label", ""), eth["Driver"],
                  eth.get("Driver version", ""),
                  eth.get("Firmware version", "")
                  ))
        except Exception, e:
            LOG.error("Can't generate current state for interface '%s' - "
                      "%s" % (eth, e))

    # Calculate maximum widths of each column in the table
    widths = []
    for x in xrange(len(eths[0])):
        widths.append( max((len(row[x]) for row in eths)) )

    # Create a format string based on calculated widths
    fmt_str = ('  '.join(["%%-%ds"]*len(widths)) % tuple(widths))

    # Print table
    for eth in eths:
        print fmt_str % eth


def reset(dryrun):
    """
    Reset interface configuration to how it was on boot.  This is required for
    xe pool-eject
    """
    import shutil

    srule_src = joinpath(BACKUP_DIR, SRULE_FILE)
    srule_dst = joinpath(DATA_DIR, SRULE_FILE)
    drule_src = joinpath(BACKUP_DIR, DRULE_FILE)
    drule_dst = joinpath(DATA_DIR, DRULE_FILE)

    # Revert static rules file if possible
    if pathexists(srule_src):
        if dryrun:
            LOG.info("Would copy '%s' to '%s' if not dry run"
                     % (srule_src, srule_dst))
        else:
            shutil.copy2(srule_src, srule_dst)
            Popen(['sed', r's/pci\([0-9]\+p[0-9]\+\)/p\1/g', '-i',
                   srule_dst]).communicate()
            LOG.debug("Copied '%s' to '%s'" % (srule_src, srule_dst))
    else:
        LOG.warning("Installer file '%s' not found.  Ignoring reset"
                    % (srule_src,))

    # Revert dynamic rules file if possible
    if pathexists(drule_src):
        if dryrun:
            LOG.info("Would copy '%s' to '%s' if not dry run"
                     % (drule_src, drule_dst))
        else:
            shutil.copy2(drule_src, drule_dst)
            LOG.debug("Copied '%s' to '%s'" % (drule_src, drule_dst))
    else:
        LOG.warning("Installer file '%s' not found.  Ignoring reset"
                    % (drule_src,))

    LOG.info("All Done")


def main(argv = sys.argv):
    """
    Parse command line arguments and set up basic logging
    """

    parser = OptionParser(
        usage =
        ("usage: %prog --rename|--list|--update <args>|"
         "--reset-to-install [-v] [-d]"),
        description =
        ("Utility for managing the naming of physical network interfaces.  It "
         "is used "
         "to undo the damage of race condition for device drivers grabbing "
         "eth names on boot, taking into account naming policies provided at "
         "install time.  In addition, it implements sensible policies when "
         "network hardware changes, and the ability for manual alteration of "
         "the policies after install."),
        version = "%%prog %s" % (__version__, )
        )

    # Misc options
    parser.add_option("-v", "--verbose", action = "store_true",
                      dest = "verbose", default = False,
                      help = "increase logging")
    parser.add_option("-d", "--dry-run", action = "store_true",
                      dest = "dryrun", default = False,
                      help = "dry run - don't write any state back to disk")

    # Actions
    actions = OptionGroup(parser, "Actions",
                          "Exactly one action is expected")
    actions.add_option("-r", "--rename", action = "store_true",
                       dest = "rename", default = False,
                       help = "rename physical interfaces.  It is not safe to "
                       "rename interfaces which have traffic passing, or "
                       "higher level networking constructs on them (bonds/"
                       "bridges/etc).  Use at your own risk after boot"
                       )
    actions.add_option("-l", "--list", action = "store_true",
                       dest = "listdevs", default = False,
                       help = "list current physical device information in a "
                       "concise manner as a reference for --update"
                       )
    actions.add_option("-u", "--update", action = "store_true",
                       dest = "update", default = False,
                       help = "manually update the order of devices.  <args> "
                       "should be one or more <target eth name>=MAC|PCI|Phys|"
                       "\"SMBios\""
                       )
    actions.add_option("--reset-to-install", action = "store_true",
                       dest = "reset", default = False,
                       help = "reset configuration to install state")
    parser.add_option_group(actions)

    if len(argv) == 1:
        parser.print_help()
        return

    options, args = parser.parse_args()

    all_actions = [options.rename, options.listdevs, options.update,
                   options.reset ]
    selected_actions = [ x for x in all_actions if x ]

    if len(selected_actions) == 0:
        parser.error("Action expected")

    if len(selected_actions) > 1:
        parser.error("Expected only 1 action")


    # Choose logging level based on verboseness
    if options.verbose:
        loglvl = logging.DEBUG
    else:
        loglvl = logging.INFO

    # Choose logging destination based on dryrun or not
    if options.dryrun:
        LOG.closeLogs()
        LOG.logToStdout(loglvl)
        LOG.info("Dry Run - logging to stdout instead of '%s'"
                 % ( LOG_PATH, ))
    else:
        LOG.openLog(LOG_PATH, loglvl)

    LOG.debug("Started script with command line '%s'" % ' '.join(argv))

    # Conditionally log verbosity
    if options.verbose:
        LOG.debug("Verbose logging enabled")

    # Actually do some work
    if options.reset:
        reset(options.dryrun)
    elif options.listdevs:
        listdevs()
    else:
        run(options.dryrun, options.update, args)

    LOG.closeLogs()

    return 0

if __name__ == "__main__":
    import traceback
    ret = 255

    LOG.logToStderr(logging.ERROR)

    try:
        ret = main()

    except SystemExit, e:
        # parser --help raises SystemExit - let it pass
        ret = e.code

    except:
        LOG.critical(traceback.format_exc())
    sys.exit(ret)
