#!/usr/bin/env python
# vi: sw=4 ts=4 expandtab :
"""
    Mnemonic:   iplex.py
    Abstract:   iplex is a cli to VF Daemon, used by nova (compute) and operators
                to manually add, delete, list VF's to a port.
    Date:       3 March 2016
    Author:     Dhanunjaya Naidu Ravada (dr3662@att.com)
    Mod:        2016 03 Mar - Created script
                2016 11 Mar - Add ping, verbose functionality
                2016 17 Mar - Add logging functionality
                2016 25 Apr - Cleanup code
                2016 03 May - Add dump action
                2016 11 July- Add update action
                2016 20 July - Add list pf's, per vf status functionality
                2017 13 Jan - Allow any argument to follow show to enable show pfn[:vfn]
                              where n:m supplies a pf and vf number rather than all or pfs.
                2017 09 Oct - Add mirror update command and support for config option.
                2018 21 Feb - Add support for live config directory
"""

__doc__ = """ iplex
    Usage:
    iplex [--conf=<config>] (add | update | delete | status) <port-id> [--loglevel=<value>] 
    iplex [--conf=<config>] mirror <pf> <vf> <dir> [<target>]  [--loglevel=<value>]
    iplex [--conf=<config>] show <what> [--loglevel=<value>] 
    iplex [--conf=<config>] verbose [--loglevel=<value>] 
    iplex [--conf=<config>] (ping | dump)
    iplex -h | --help
    iplex --version
    Options:
        -h, --help      show this help message and exit
        --version       show version and exit
        --loglevel=<value>  Default logvalue [default: 0]
        <what> may be one of:  all, pfs, extended, or <n> where <n> is a PF number.
        <dir> is the mirror direction: one of: {in | out | all | off}.
"""

from docopt import docopt
import os
import errno
import json
import logging
import sys
import ast
import time
from logging.handlers import RotatingFileHandler
import fcntl
import platform

VFD_CONFIG = '/etc/vfd/vfd.cfg'		# default; --conf= overrides from command line

# logging
def setup_logging(logfile, config_data):
    handler = RotatingFileHandler(os.path.join(config_data["log_dir"], logfile), maxBytes=200000, backupCount=20)
    log_formatter = logging.Formatter('%(asctime)s  %(process)s %(levelname)s %(name)s [-] %(message)s')
    handler.setFormatter(log_formatter)
    log = logging.getLogger('iplex')
    log.setLevel(logging.INFO)
    log.addHandler(handler)
    return log

def _errorMsg(msg):
    data = {}
    data['state'] = 'ERROR'
    data['msg'] = msg
    print data

# read vfd.cfg and validate
def read_config():
    with open(VFD_CONFIG) as data_file:
        try:
            data = json.load(data_file)
        except ValueError:
            _errorMsg(VFD_CONFIG + " is not a valid json")
            sys.exit(1)
        return data

class Iplex(object):

    PRIVATE_FIFO_PATH = "/tmp/IPLEX_"

    def __init__(self, config_data=None, options=None, log=None):
        self.options = options
        self.config_data = config_data
        self.log = log

    def add(self, port_id):
        self.filename = self.__validate_file(port_id)
        self.resp_fifo = self.__create_fifo()
        msg = self.__request_message('add')
        self.__write_read_fifo(msg)
        return

    def update(self, port_id):
        self.filename = self.__validate_file(port_id)
        self.resp_fifo = self.__create_fifo()
        msg = self.__request_message('update')
        self.__write_read_fifo(msg)
        return

    def delete(self, port_id):
        self.filename = self.__assert_live_vfconfig(port_id)
        self.resp_fifo = self.__create_fifo()
        msg = self.__request_message('delete')
        self.__write_read_fifo(msg)
        return

    def mirror( self ):
        self.filename = None
        self.resp_fifo = self.__create_fifo()
        msg = self.__request_message( 'mirror' )
        self.__write_read_fifo( msg )
        return
        

    def status(self, port_id):
        self.filename = self.__validate_file(port_id)
        self.resp_fifo = self.__create_fifo()
        msg = self.__request_message('status')
        self.__write_read_fifo(msg)
        return

    def show(self):
        self.filename = None
        self.resp_fifo = self.__create_fifo()
        msg = self.__request_message('show')
        self.__write_read_fifo(msg)
        return

    def verbose(self):
        self.filename=None
        self.resp_fifo = self.__create_fifo()
        msg = self.__request_message('verbose')
        self.__write_read_fifo(msg)
        return

    def ping(self):
        self.filename = None
        self.resp_fifo = self.__create_fifo()
        msg = self.__request_message('ping')
        self.__write_read_fifo(msg)
        return

    def dump(self):
        self.filename = None
        self.resp_fifo = self.__create_fifo()
        msg = self.__request_message('dump')
        self.__write_read_fifo(msg)
        return

    def __errMsg(self, msg=None):
        data = {}
        data['state'] = 'OK'
        if msg is not None:
            data['state'] = 'ERROR'
            data['msg'] = msg
        print data

    # validate file whether vf config resides at /var/lib/vfd/config
    def __validate_file(self, port_id):
        filename = None
        if os.path.isfile(os.path.join(self.config_data['config_dir'], port_id)+'.json'):
            filename = os.path.join(self.config_data['config_dir'], port_id)+'.json'
            self.log.info("valid vf config file: %s", filename)
            return filename
        else:
            self.__errMsg("VF config for {} doesn't exist".format(port_id))
            self.log.error("VF config for %s doesn't exist", port_id)
            sys.exit(1)

    # Verify that a VF config file in the live (active) directory exists.
    # returns the filename if valid, and aborts the whole process if it is not.
    #
    def __assert_live_vfconfig( self, port_id ) :
        filename = os.path.join(self.config_data['config_dir'] + "_live", port_id)+'.json'
        if os.path.isfile( filename ) :
            self.log.info("valid vf config file: %s", filename)
            return filename
        else:
            self.__errMsg( "VF live config for {} doesn't exist".format(port_id) )
            self.log.error( "VF live config for %s doesn't exist", port_id )
            sys.exit( 1 )

    # Create private fifo
    def __create_fifo(self):
        resp_fifo = Iplex.PRIVATE_FIFO_PATH+str(os.getpid())
        try:
            os.mkfifo(resp_fifo)
        except OSError, e:
            self.log.error("Failed to create FIFO: %s", e)
            sys.exit(1)
        self.log.info("Successfully created FIFO: %s", resp_fifo)
        return resp_fifo

    # Build the request message
    def __request_message(self, action):
        msg = {}
        msg["action"] = action
        msg["params"] = {}
        if self.filename is not None:
            msg["params"]["filename"] = self.filename
        
        if action == "show":
            msg["params"]["resource"] = self.options["<what>"]				# pick up generic option
        else:
            if action == "mirror":
                msg["params"]["resource"] = self.options["<pf>"] + " " + self.options["<vf>"] + " " + self.options["<dir>"]
                if self.options["<target>"] != None:
                    msg["params"]["resource"] +=  " " + self.options["<target>"]
                
        msg["params"]["loglevel"] = int(self.options["--loglevel"])
        msg["params"]["r_fifo"] = self.resp_fifo
        self.log.info("REQUEST MESSAGE: %s", msg)
        return json.dumps(msg)

    def __read_fd(self, fd, chunksize=2048):
        buffer = []
        running = True
        time.sleep(.2)
        while running:
            time.sleep(.1)
            chunk = os.read(fd, chunksize)
            buffer.append(chunk)
            running = len(chunk) == chunksize
        return ''.join(buffer).strip(' \n\t')

    # write data to public fifo and read from private fifo
    def __write_read_fifo(self, msg):
        readFd = None
        try:
            writeFd = os.open(self.config_data['fifo'], os.O_WRONLY | os.O_NONBLOCK)
            os.write(writeFd, str(msg)+'\n\n')
            os.close(writeFd)
            readFd = os.open(self.resp_fifo, os.O_RDONLY)
            if not hasattr(fcntl, 'F_SETPIPE_SZ'):
                if platform.system() == 'Linux':
                    fcntl.F_SETPIPE_SZ = 1031
                    fcntl.fcntl(readFd, fcntl.F_SETPIPE_SZ, 1024 * 60)
            buf = self.__read_fd(readFd)
            resp_data = json.dumps(buf.strip(' \n\t'))
            print(ast.literal_eval(resp_data))
            os.close(readFd)
            os.unlink(self.resp_fifo)
        except OSError as e:
            if errno.ENXIO:
                self.__errMsg("VFD is not running")
                self.log.error("VFD does not seem to be running, please start the service ")
            else:
                self.log.error("%s", e)
            if not (readFd == None):
                os.close(readFd)
            os.unlink(self.resp_fifo)
            sys.exit(1)


if __name__ == '__main__':
    options = docopt(__doc__, version='1.2')

    if options["--conf"] != None:
        VFD_CONFIG = options["--conf"]

    config_data = read_config()
    log = setup_logging('iplex.log', config_data)
    iplex = Iplex(config_data, options, log)

    if options['add']:
        iplex.add(options['<port-id>'])
    elif options['update']:
        iplex.update(options['<port_id>'])
    elif options['delete']:
        iplex.delete(options['<port-id>'])
    elif options['ping']:
        iplex.ping()
    elif options['verbose']:
        iplex.verbose()
    elif options['dump']:
        iplex.dump()
    elif options['mirror']:
        iplex.mirror()
    else:
        if options['show']:
            iplex.show()
