"""Calxeda: tspackage.py"""


# Copyright (c) 2012-2013, Calxeda Inc.
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of Calxeda Inc. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
# DAMAGE.


#
# A cxmanage command to collect information about a node and archive it.
#
# Example:
# cxmanage tspackage 10.10.10.10
#


import os
import pkg_resources
import shutil
import subprocess
import sys
import tarfile
import tempfile
import time

from cxmanage_api.cli import get_tftp, get_nodes, run_command, COMPONENTS


def tspackage_command(args):
    """Get information pertaining to each node.
    This includes:
    Version info (like cxmanage info)
    MAC addresses
    Sensor readings
    Sensor data records
    Firmware info
    Boot order
    SELs (System Event Logs),
    Depth charts
    Routing Tables

    This data will be written to a set of files. Each node will get its own
    file. All of these files will be archived and saved to the user's current
    directory.

    Internally, this command is called from:
    ~/virtual_testenv/workspace/cx_manage_util/scripts/cxmanage

    """
    tftp = get_tftp(args)
    nodes = get_nodes(args, tftp)

    # Make a temporary directory to store the node information files
    original_dir = os.getcwd()
    temp_dir = tempfile.mkdtemp()
    os.chdir(temp_dir)
    tspackage_dir = "tspackage.%s" % time.strftime("%Y%m%d%H%M%S")
    os.mkdir(tspackage_dir)
    os.chdir(tspackage_dir)

    quiet = args.quiet

    write_client_info()

    if not quiet:
        print("Getting version information...")
    write_version_info(args, nodes)

    if not quiet:
        print("Getting boot order...")
    write_boot_order(args, nodes)

    if not quiet:
        print("Getting MAC addresses...")
    write_mac_addrs(args, nodes)

    if not quiet:
        print("Getting sensor information...")
    write_sensor_info(args, nodes)

    if not quiet:
        print("Getting firmware information...")
    write_fwinfo(args, nodes)

    if not quiet:
        print("Getting system event logs...")
    write_sel(args, nodes)

    if not quiet:
        print("Getting depth charts...")
    write_depth_chart(args, nodes)

    if not quiet:
        print("Getting routing tables...")
    write_routing_table(args, nodes)

    if not quiet:
        print("Getting serial log...")
    write_serial_log(args, nodes)

    if not quiet:
        print("Getting crash log...")
    write_crash_log(args, nodes)

    # Archive the files
    archive(os.getcwd(), original_dir)

    # The original files are already archived, so we can delete them.
    shutil.rmtree(temp_dir)


def write_client_info():
    """ Write client-side info """
    with open("client.txt", "w") as fout:
        def write_command(command):
            """ Safely write output from a single command to the file """
            try:
                fout.write(subprocess.check_output(
                    command, stderr=subprocess.STDOUT, shell=True
                ))
            except subprocess.CalledProcessError:
                pass

        fout.write("[ Operating System ]\n")
        fout.write("Operating system: %s\n" % sys.platform)
        write_command("lsb_release -a")
        write_command("uname -a")

        fout.write("\n[ Tool versions ]\n")
        fout.write("Python %s\n" % sys.version.replace("\n", ""))
        cxmanage_version = pkg_resources.require("cxmanage")[0].version
        fout.write("cxmanage version %s\n" % cxmanage_version)
        pyipmi_version = pkg_resources.require("pyipmi")[0].version
        fout.write("pyipmi version %s\n" % pyipmi_version)
        ipmitool_path = os.environ.get('IPMITOOL_PATH', 'ipmitool')
        write_command("%s -V" % ipmitool_path)

        fout.write("\n[ Python packages ]\n")
        write_command("pip freeze")


def write_version_info(args, nodes):
    """Write the version info (like cxmanage info) for each node
    to their respective files.

    """
    info_results, _ = run_command(args, nodes, "get_versions")


    for node in nodes:
        lines = [
            "[ Version Info for Node %d ]" % node.node_id,
            "ECME IP Address     : %s" % node.ip_address
        ]

        if node in info_results:
            info_result = info_results[node]
            lines.append(
                "Hardware version    : %s" %
                info_result.hardware_version
            )
            lines.append(
                "Firmware version    : %s" %
                info_result.firmware_version
            )

            # Get mappings between attributes and formatted strings
            components = COMPONENTS
            for var, description in components:
                if hasattr(info_result, var):
                    version = getattr(info_result, var)
                    lines.append("%s: %s" % (description.ljust(20), version))
        else:
            lines.append("No version information could be found.")

        write_to_file(node, lines)

def write_mac_addrs(args, nodes):
    """Write the MAC addresses for each node to their respective files."""
    mac_addr_results, _ = run_command(
        args,
        nodes,
        "get_fabric_macaddrs"
    )

    for node in nodes:
        lines = []  # Lines of text to write to file
        # \n is used here to give a blank line before this section
        lines.append("\n[ MAC Addresses for Node %d ]" % node.node_id)

        if node in mac_addr_results:
            for port in mac_addr_results[node][node.node_id]:
                for mac_address in mac_addr_results[node][node.node_id][port]:
                    lines.append(
                        "Node %i, Port %i: %s" %
                        (node.node_id, port, mac_address)
                    )
        else:
            lines.append("\nWARNING: No MAC addresses found!")

        write_to_file(node, lines)

# pylint: disable=R0914
def write_sensor_info(args, nodes):
    """Write sensor information for each node to their respective files."""
    args.sensor_name = ""

    results, _ = run_command(args, nodes, "get_sensors",
                             args.sensor_name)

    sensors = {}
    for node in nodes:
        lines = []  # Lines of text to write to file
        # \n is used here to give a blank line before this section
        lines.append("\n[ Sensors for Node %d ]" % node.node_id)

        if node in results:
            for sensor_name, sensor in results[node].iteritems():
                if not sensor_name in sensors:
                    sensors[sensor_name] = []

                reading = sensor.sensor_reading.replace("(+/- 0) ", "")
                try:
                    value = float(reading.split()[0])
                    suffix = reading.lstrip("%f " % value)
                    sensors[sensor_name].append((node, value, suffix))
                except ValueError:
                    sensors[sensor_name].append((node, reading, ""))
        else:
            print("Could not get sensor info!")
            lines.append("Could not get sensor info!")

        for sensor_name, readings in sensors.iteritems():
            for reading_node, reading, suffix in readings:
                if reading_node.ip_address == node.ip_address:
                    left_side = "{:<18}".format(sensor_name)
                    right_side = ": %.2f %s" % (reading, suffix)
                    lines.append(left_side + right_side)

        write_to_file(node, lines)


def write_fwinfo(args, nodes):
    """Write information about each node's firware partitions
    to its respective file.

    """
    results, _ = run_command(args, nodes, "get_firmware_info")

    for node in nodes:
        lines = []  # Lines of text to write to file
        # \n is used here to give a blank line before this section
        lines.append("\n[ Firmware Info for Node %d ]" % node.node_id)

        if node in results:
            first_partition = True  # The first partiton doesn't need \n

            for partition in results[node]:
                if first_partition:
                    lines.append("Partition : %s" % partition.partition)
                    first_partition = False
                else:
                    lines.append("\nPartition : %s" % partition.partition)
                lines.append("Type      : %s" % partition.type)
                lines.append("Offset    : %s" % partition.offset)
                lines.append("Size      : %s" % partition.size)
                lines.append("Priority  : %s" % partition.priority)
                lines.append("Daddr     : %s" % partition.daddr)
                lines.append("Flags     : %s" % partition.flags)
                lines.append("Version   : %s" % partition.version)
                lines.append("In Use    : %s" % partition.in_use)
        else:
            lines.append("Could not get firmware info!")
        write_to_file(node, lines)


def write_boot_order(args, nodes):
    """Write the boot order of each node to their respective files."""
    results, _ = run_command(args, nodes, "get_boot_order")

    for node in nodes:
        lines = []  # Lines of text to write to file
        # \n is used here to give a blank line before this section
        lines.append("\n[ Boot Order for Node %d ]" % node.node_id)

        if node in results:
            lines.append(", ".join(results[node]))
        else:
            lines.append("Could not get boot order!")

        write_to_file(node, lines)


def write_sel(args, nodes):
    """Write the SEL for each node to their respective files."""
    results, _ = run_command(args, nodes, "get_sel")

    for node in nodes:
        lines = []  # Lines of text to write to file
        # \n is used here to give a blank line before this section
        lines.append("\n[ System Event Log for Node %d ]" % node.node_id)

        try:
            if node in results:
                for event in results[node]:
                    lines.append(event)

        # pylint: disable=W0703
        except Exception as error:
            lines.append("Could not get SEL! " + str(error))
            if not args.quiet:
                print("Failed to get system event log for " + node.ip_address)

        write_to_file(node, lines)


def write_depth_chart(args, nodes):
    """Write the depth chart for each node to their respective files."""
    depth_results, _ = run_command(args, nodes, "get_depth_chart")

    for node in nodes:
        lines = []  # Lines of text to write to file
        # \n is used here to give a blank line before this section
        lines.append("\n[ Depth Chart for Node %d ]" % node.node_id)

        if node in depth_results:
            depth_chart = depth_results[node]
            for key in depth_chart:
                subchart = depth_chart[key]
                lines.append("To node " + str(key))

                # The 'shortest' entry is one tuple, but
                # the 'others' are a list.
                for subkey in subchart:
                    if str(subkey) == "shortest":
                        lines.append(
                            "  " + str(subkey) +
                            " : " + str(subchart[subkey])
                        )
                    else:
                        for entry in subchart[subkey]:
                            lines.append(
                                "  " + str(subkey) +
                                "   : " + str(entry)
                            )

        else:
            lines.append("Could not get depth chart!")

        write_to_file(node, lines)


def write_routing_table(args, nodes):
    """Write the routing table for each node to their respective files."""
    routing_results, _ = run_command(args, nodes, "get_routing_table")

    for node in nodes:
        lines = []  # Lines of text to write to file
        # \n is used here to give a blank line before this section
        lines.append("\n[ Routing Table for Node %d ]" % node.node_id)

        if node in routing_results:
            table = routing_results[node]
            for node_to in table:
                lines.append(str(node_to) + " : " + str(table[node_to]))
        else:
            lines.append("Could not get routing table!")

        write_to_file(node, lines)


def write_serial_log(args, nodes):
    """Write the serial log for each node"""
    results, errors = run_command(args, nodes, "read_fru", 98)
    for node in nodes:
        lines = ["\n[ Serial log for Node %d ]" % node.node_id]
        if node in results:
            lines.append(results[node].strip())
        else:
            lines.append(str(errors[node]))
        write_to_file(node, lines)


def write_crash_log(args, nodes):
    """Write the crash log for each node"""
    results, errors = run_command(args, nodes, "read_fru", 99)
    for node in nodes:
        lines = ["\n[ Crash log for Node %d ]" % node.node_id]
        if node in results:
            lines.append(results[node].strip())
        else:
            lines.append(str(errors[node]))
        write_to_file(node, lines)


def write_to_file(node, to_write, add_newlines=True):
    """Append to_write to an info file for every node in nodes.

    :param node: Node object to write about
    :type node: Node object
    :param to_write: Text to write to the files
    :type to_write: List of strings
    :param add_newlines: Whether to add newline characters before
    every item in to_write. True by default. True will add newline
    characters.
    :type add_newlines: bool

    """
    with open("node" + str(node.node_id) + ".txt", 'a') as node_file:
        if add_newlines:
            node_file.write("%s\n" % "\n".join(to_write))
        else:
            node_file.write("".join(to_write))


def archive(directory_to_archive, destination):
    """Creates a .tar containing everything in the directory_to_archive.
    The .tar is saved to destination with the same name as the original
    directory_to_archive, but with .tar appended.

    :param directory_to_archive: A path to the directory to be archived.
    :type directory_to_archive: string

    :param destination: A path to the location the .tar should be saved
    :type destination: string

    """
    os.chdir(os.path.dirname(directory_to_archive))

    tar_name = os.path.basename(directory_to_archive) + ".tar"
    tar_name = os.path.join(destination, tar_name)

    with tarfile.open(tar_name, "w") as tar:
        tar.add(os.path.basename(directory_to_archive))

    print(
        "Finished! One archive created:\n" +
        os.path.join(destination, tar_name)
    )
