#!/usr/bin/env python

# Copyright (c) 2012, 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.

import argparse
import os
import pkg_resources
import subprocess
import sys

from cxmanage.commands.power import power_command, power_status_command, \
        power_policy_command, power_policy_status_command
from cxmanage.commands.mc import mcreset_command
from cxmanage.commands.fw import fwupdate_command, fwinfo_command
from cxmanage.commands.sensor import sensor_command
from cxmanage.commands.fabric import ipinfo_command, macaddrs_command
from cxmanage.commands.config import config_reset_command, config_boot_command
from cxmanage.commands.info import info_command
from cxmanage.commands.ipmitool import ipmitool_command
from cxmanage.commands.ipdiscover import ipdiscover_command


PYIPMI_VERSION = '0.7.1'
IPMITOOL_VERSION = '1.8.11.0-cx5'


PARSER_EPILOG = """examples:
  cxmanage power status 192.168.1.1             # single host
  cxmanage power on 192.168.1.1,192.168.1.2     # comma-separated hosts
  cxmanage info 192.168.1.1-192.168.1.5         # IP range (5 hosts)
  cxmanage -a sensor temp 192.168.1.1           # all nodes on a fabric
  cxmanage -a fwupdate package ECX-1000_update.tar.gz 192.168.1.1"""

FWUPDATE_EPILOG = """examples:
  cxmanage -a fwupdate package ECX-1000_update.tar.gz 192.168.1.1
  cxmanage -a fwupdate --full package ECX-1000_update.tar.gz 192.168.1.1"""

FWUPDATE_IMAGE_TYPES = ['PACKAGE'] + sorted([
    'DEL',
    'DEL1',
    'S2_ELF',
    'SOC_ELF',
    'A9_UEFI',
    'A9_UBOOT',
    'A9_EXEC',
    'A9_ELF',
    'SOCDATA',
    'DTB',
    'CDB',
    'UBOOTENV',
    'SEL',
    'BOOT_LOG',
    'UEFI_ENV',
    'DIAG_ELF',
])



def build_parser():
    """setup the argparse parser"""
    parser = argparse.ArgumentParser(
            description='Calxeda Server Management Utility',
            formatter_class=argparse.RawDescriptionHelpFormatter,
            epilog=PARSER_EPILOG)

    #global arguments
    parser.add_argument('-V', '--version', action='store_true',
            help='Show version information')
    parser.add_argument('-u', '--user', default='admin',
            help='Username for login')
    parser.add_argument('-p', '--password', default='admin',
            help='Password for login')
    parser.add_argument('-a', '--all-nodes', action='store_true',
            help='Send command to all nodes reported by fabric')
    parser.add_argument('--threads', type=int, metavar='THREAD_COUNT',
            help='Number of threads to use')
    parser.add_argument('--command_delay', type=float,
            metavar='SECONDS', default=0.0,
            help='Per thread time to delay between issuing commands')
    parser.add_argument('--force', action='store_true',
            help='Force the command to run')
    parser.add_argument('--retry', help='Retry command on multiple times',
            type=int, default=None, metavar='COUNT')
    parser.add_argument('--ipmipath', help='Path to ipmitool command',
            default=None)
    parser.add_argument('-n', '--nodes', metavar='COUNT', type=int,
            help='Expected number of nodes')
    parser.add_argument('-i', '--ids', action='store_true',
            help='Display node IDs in addition to IP addresses')
    verbosity = parser.add_mutually_exclusive_group()
    verbosity.add_argument('-v', '--verbose', action='store_true',
            help='Verbose output')
    verbosity.add_argument('-q', '--quiet', action='store_true',
            help='Quiet output')
    tftp_type = parser.add_mutually_exclusive_group()
    tftp_type.add_argument('--internal-tftp', metavar='IP:PORT',
            help='Host an internal TFTP server listening on ip:port')
    tftp_type.add_argument('--external-tftp', metavar='IP:PORT',
            help='Connect to remote TFTP server at ip:port')
    parser.add_argument('--ecme-tftp-port', type=int, default=5001,
            metavar='PORT', help='TFTP port of the ECME')

    subparsers = parser.add_subparsers()

    #power command
    power = subparsers.add_parser('power',
        help='control server power')
    power_subs = power.add_subparsers()

    power_on = power_subs.add_parser('on', help='boot the server')
    power_on.set_defaults(power_mode='on', func=power_command)

    power_off = power_subs.add_parser('off', help='shut the server off')
    power_off.set_defaults(power_mode='off', func=power_command)

    power_reset = power_subs.add_parser('reset', help='reset the server')
    power_reset.set_defaults(power_mode='reset', func=power_command)

    power_status = power_subs.add_parser('status',
            help='get server power status')
    power_status.set_defaults(func=power_status_command)

    power_policy = power_subs.add_parser('policy',
            help='set server power policy')
    power_policy_subs = power_policy.add_subparsers()

    power_policy_always_on = power_policy_subs.add_parser(
            'always-on', help='always boot the server by default')
    power_policy_always_on.set_defaults(policy='always-on',
            func=power_policy_command)
    power_policy_always_off = power_policy_subs.add_parser(
            'always-off', help='never boot the server by default')
    power_policy_always_off.set_defaults(policy='always-off',
            func=power_policy_command)
    power_policy_previous = power_policy_subs.add_parser(
            'previous', help='return to previous power state by default')
    power_policy_previous.set_defaults(policy='previous',
            func=power_policy_command)
    power_policy_status = power_policy_subs.add_parser(
            'status', help='get the current power policy')
    power_policy_status.set_defaults(func=power_policy_status_command)

    #mcreset command
    mcreset = subparsers.add_parser('mcreset',
            help='reset the management controller')
    mcreset.set_defaults(func=mcreset_command)

    #fwupdate command
    fwupdate = subparsers.add_parser('fwupdate', help='update firmware',
            formatter_class=argparse.RawDescriptionHelpFormatter,
            epilog=FWUPDATE_EPILOG)
    fwupdate.add_argument('image_type', metavar='IMAGE_TYPE',
            help='image type to use (%s)' % ", ".join(FWUPDATE_IMAGE_TYPES),
            type=lambda string: string.upper(),
            choices = FWUPDATE_IMAGE_TYPES)
    fwupdate.add_argument('filename', help='path to file to upload')
    fwupdate.add_argument('--full', action='store_true', default=False,
            help='Update primary AND backup partitions (will reset MC)')
    fwupdate.add_argument('--partition',
            help='Specify partition to update', default='INACTIVE',
            type=lambda string: string.upper(),
            choices = list([
                'FIRST',
                'SECOND',
                'BOTH',
                'OLDEST',
                'NEWEST',
                'INACTIVE'
            ]))
    simg_args = fwupdate.add_mutually_exclusive_group()
    simg_args.add_argument('--force-simg',
            help='Force addition of SIMG header',
            default=False, action='store_true')
    simg_args.add_argument('--skip-simg',
            help='Skip addition of SIMG header',
            default=False, action='store_true')
    fwupdate.add_argument('--priority',
            help='Priority for SIMG header', default=None, type=int)
    fwupdate.add_argument('-d', '--daddr',
            help='Destination address for SIMG',
            default=None, type=lambda x : int(x, 16))
    fwupdate.add_argument('--skip-crc32',
            help='Skip crc32 calculation for SIMG',
            default=False, action='store_true')
    fwupdate.add_argument('--version', dest='fw_version',
            help='Version for SIMG header', default=None)
    fwupdate.set_defaults(func=fwupdate_command)

    #fwinfo command
    fwinfo = subparsers.add_parser('fwinfo', help='get FW info')
    fwinfo.set_defaults(func=fwinfo_command)

    #sensor command
    sensor = subparsers.add_parser('sensor',
            help='read sensor value')
    sensor.add_argument('sensor_name', help='Sensor name to read',
            nargs='?', default='')
    sensor.set_defaults(func=sensor_command)

    #ipinfo command
    ipinfo = subparsers.add_parser('ipinfo', help='get IP info')
    ipinfo.set_defaults(func=ipinfo_command)

    #macaddrs command
    macaddrs = subparsers.add_parser('macaddrs',
            help='get mac addresses')
    macaddrs.set_defaults(func=macaddrs_command)

    #config command
    config = subparsers.add_parser('config', help='configure hosts')
    config_subs = config.add_subparsers()

    reset = config_subs.add_parser('reset',
            help='reset to factory default')
    reset.set_defaults(func=config_reset_command)

    boot = config_subs.add_parser('boot',
            help='set server boot order')
    boot.add_argument('boot_order', help='boot order to use', default=[],
            type=lambda x: [] if x == 'none' else x.split(','))
    boot.set_defaults(func=config_boot_command)

    #info command
    info = subparsers.add_parser('info', help='get host info')
    info.add_argument('info_type', nargs='?',
            type=lambda string: string.lower(),
            choices=['basic', 'ubootenv'])
    info.set_defaults(func=info_command)

    #ipmitool command
    ipmitool = subparsers.add_parser('ipmitool',
            help='run an arbitrary ipmitool command')
    ipmitool.add_argument('-l', '--lanplus',
            action='store_true', default=False,
            help='use lanplus')
    ipmitool.add_argument('ipmitool_args', nargs='+',
            help='ipmitool arguments')
    ipmitool.set_defaults(func=ipmitool_command)

    #ipdiscover command
    ipdiscover = subparsers.add_parser('ipdiscover',
            help='discover server-side IP addresses')
    ipdiscover.add_argument('-A', '--aggressive', action='store_true',
            help='discover IPs aggressively')
    ipdiscover.add_argument('-U', '--server-user', type=str, default='user1',
            metavar='USER', help='Server-side Linux username')
    ipdiscover.add_argument('-P', '--server-password', type=str,
            default='1Password', metavar='PASSWORD',
            help='Server-side Linux password')
    ipdiscover.add_argument('-6', '--ipv6', action='store_true',
            help='Discover IPv6 addresses')
    ipdiscover.add_argument('-I', '--interface', type=str, default=None,
            help='Network interface to check')
    ipdiscover.set_defaults(func=ipdiscover_command)

    parser.add_argument('hostname',
            help='nodes to operate on (see examples below)')

    return parser


def validate_args(args):
    """ Bail out if the arguments don't make sense"""
    if args.threads != None and args.threads < 1:
        sys.exit('ERROR: --threads must be at least 1')
    if args.func == fwupdate_command:
        if args.skip_simg and args.priority:
            sys.exit('Invalid argument --priority when supplied with --skip-simg')
        if args.skip_simg and args.daddr:
            sys.exit('Invalid argument --daddr when supplied with --skip-simg')
        if args.skip_simg and args.skip_crc32:
            sys.exit('Invalid argument --skip-crc32 when supplied with --skip-simg')
        if args.skip_simg and args.fw_version:
            sys.exit('Invalid argument --version when supplied with --skip-simg')


def print_version():
    """ Print the current version of cxmanage """
    version = pkg_resources.require('cxmanage')[0].version
    print "cxmanage version %s" % version


def check_versions():
    """Check versions of dependencies"""
    # Check pyipmi version
    try:
        pkg_resources.require('pyipmi>=%s' % PYIPMI_VERSION)
    except pkg_resources.DistributionNotFound:
        print 'ERROR: cxmanage requires pyipmi version %s'\
                % PYIPMI_VERSION
        print 'No existing version was found.'
        sys.exit(1)
    except pkg_resources.VersionConflict:
        version = pkg_resources.require('pyipmi')[0].version
        print 'ERROR: cxmanage requires pyipmi version %s' % PYIPMI_VERSION
        print 'Current pyipmi version is %s' % version
        sys.exit(1)


    # Check ipmitool version
    if 'IPMITOOL_PATH' in os.environ:
        args = [os.environ['IPMITOOL_PATH'], '-V']
    else:
        args = ['ipmitool', '-V']

    try:
        ipmitool_process = subprocess.Popen(args, stdout=subprocess.PIPE)
        ipmitool_version = ipmitool_process.communicate()[0].split()[2]
        if pkg_resources.parse_version(ipmitool_version) < \
                pkg_resources.parse_version(IPMITOOL_VERSION):
            print 'ERROR: cxmanage requires IPMItool %s or later' \
                    % IPMITOOL_VERSION
            print 'Current IPMItool version is %s' % ipmitool_version
            sys.exit(1)
    except OSError:
        print 'ERROR: cxmanage requires IPMItool %s or later' \
                % IPMITOOL_VERSION
        print 'No existing version was found.'
        sys.exit(1)


def main():
    """Get args and go"""
    for arg in sys.argv[1:]:
        if arg in ['-V', '--version']:
            print_version()
            sys.exit(0)
        elif arg[0] != '-':
            break

    parser = build_parser()
    args = parser.parse_args()
    validate_args(args)

    if args.ipmipath:
        if os.path.isdir(args.ipmipath):
            args.ipmipath = args.ipmipath.rstrip('/') + '/ipmitool'
        os.environ['IPMITOOL_PATH'] = args.ipmipath

    check_versions()

    sys.exit(args.func(args))


if __name__ == '__main__':
    main()
