#
"""Sliver manager API.

This module exposes an XMLRPC interface that allows PlanetLab users to
create/destroy slivers with delegated instantiation, start and stop
slivers, make resource loans, and examine resource allocations.  The
XMLRPC is provided on a localhost-only TCP port as well as via a Unix
domain socket that is accessible by ssh-ing into a delegate account
with the forward_api_calls shell.
"""

import xmlrpc.server
import socketserver
import errno
import os
import pwd
import socket
import struct
import threading
import xmlrpc.client
import slivermanager

try:
    from PLC.Parameter import Parameter, Mixed
except:
    def Parameter(a = None, b = None): pass
    def Mixed(a = None, b = None, c = None): pass


import account
import logger

# TODO: These try/excepts are a hack to allow doc/DocBookLocal.py to
# import this file in order to extract the documentation from each
# exported function.
# A better approach will involve more extensive code splitting, I think.
try: import database
except: import logger as database
import ticket as ticket_module
import tools

deliver_ticket = None  # set in slivermanager.start()

api_method_dict = {}
nargs_dict = {}

def export_to_api(nargs):
    def export(method):
        nargs_dict[method.__name__] = nargs
        api_method_dict[method.__name__] = method
        return method
    return export

def export_to_docbook(**kwargs):

    keywords = {
        "group" : "NMAPI",
        "status" : "current",
        "name": None,
        "args": None,
        "roles": [],
        "accepts": [],
        "returns": [],
    }
    def export(method):
        def args():
            # Inspect method. Remove self from the argument list.
            max_args = method.__code__.co_varnames[0:method.__code__.co_argcount]
            defaults = method.__defaults__
            if defaults is None:
                defaults = ()
            min_args = max_args[0:len(max_args) - len(defaults)]

            defaults = tuple([None for arg in min_args]) + defaults
            return (min_args, max_args, defaults)

        keywords['name'] = method.__name__
        keywords['args'] = args
        for arg in keywords:
            method.__setattr__(arg, keywords[arg])

        for arg in kwargs:
            method.__setattr__(arg, kwargs[arg])
        return method

    return export


# status
# roles,
# accepts,
# returns

@export_to_docbook(roles=['self'],
                   accepts=[],
                   returns=Parameter([], 'A list of supported functions'))
@export_to_api(0)
def Help():
    """Get a list of functions currently supported by the Node Manager API"""
    names=list(api_method_dict.keys())
    names.sort()
    return ''.join(['**** ' + api_method_dict[name].__name__ + '\n' + api_method_dict[name].__doc__ + '\n'
                    for name in names])

@export_to_docbook(roles=['self'],
                   accepts=[Parameter(str, 'A ticket returned from GetSliceTicket()')],
                   returns=Parameter(int, '1 if successful'))
@export_to_api(1)
def Ticket(ticket):
    """The Node Manager periodically polls the PLC API for a list of all
    slices that are allowed to exist on the given node. Before
    actions are performed on a delegated slice (such as creation),
    a controller slice must deliver a valid slice ticket to NM.

    This ticket is the value retured by PLC's GetSliceTicket() API call."""
    try:
        data = ticket_module.verify(ticket)
        name = data['slivers'][0]['name']
        if data != None:
            deliver_ticket(data)
        logger.log('api_calls: Ticket delivered for %s' % name)
        Create(database.db.get(name))
    except Exception as err:
        raise xmlrpc.client.Fault(102, 'Ticket error: ' + str(err))

@export_to_docbook(roles=['self'],
                   accepts=[Parameter(str, 'A ticket returned from GetSlivers()')],
                   returns=Parameter(int, '1 if successful'))
@export_to_api(1)
def AdminTicket(ticket):
    """Admin interface to create slivers based on ticket returned by GetSlivers()."""
    try:
        data, = xmlrpc.client.loads(ticket)[0]
        name = data['slivers'][0]['name']
        if data != None:
            deliver_ticket(data)
        logger.log('api_calls: Admin Ticket delivered for %s' % name)
        Create(database.db.get(name))
    except Exception as err:
        raise xmlrpc.client.Fault(102, 'Ticket error: ' + str(err))


@export_to_docbook(roles=['self'],
                   accepts=[],
                   returns={'sliver_name' : Parameter(int, 'the associated xid')})
@export_to_api(0)
def GetXIDs():
    """Return an dictionary mapping Slice names to XIDs"""
    return dict([(pwent[0], pwent[2]) for pwent in pwd.getpwall() if pwent[6] == slivermanager.sliver_password_shell])

@export_to_docbook(roles=['self'],
                   accepts=[],
                   returns={ 'sliver_name' : Parameter(str, 'the associated SSHKey')})
@export_to_api(0)
def GetSSHKeys():
    """Return an dictionary mapping slice names to SSH keys"""
    keydict = {}
    for rec in database.db.values():
        if 'keys' in rec:
            keydict[rec['name']] = rec['keys']
    return keydict


@export_to_docbook(roles=['nm-controller', 'self'],
                    accepts=[Parameter(str, 'A sliver/slice name.')],
                   returns=Parameter(int, '1 if successful'))
@export_to_api(1)
def Create(sliver_name):
    """Create a non-PLC-instantiated sliver"""
    rec = sliver_name
    if rec['instantiation'] == 'delegated':
        account.get(rec['name']).ensure_created(rec)
        logger.log("api_calls: Create %s"%rec['name'])
    else:
        raise Exception("Only PLC can create non delegated slivers.")


@export_to_docbook(roles=['nm-controller', 'self'],
                    accepts=[Parameter(str, 'A sliver/slice name.')],
                   returns=Parameter(int, '1 if successful'))
@export_to_api(1)
def Destroy(sliver_name):
    """Destroy a non-PLC-instantiated sliver"""
    rec = sliver_name
    if rec['instantiation'] == 'delegated':
        account.get(rec['name']).ensure_destroyed()
        logger.log("api_calls: Destroy %s"%rec['name'])
    else:
        raise Exception("Only PLC can destroy non delegated slivers.")


@export_to_docbook(roles=['nm-controller', 'self'],
                    accepts=[Parameter(str, 'A sliver/slice name.')],
                   returns=Parameter(int, '1 if successful'))
@export_to_api(1)
def Start(sliver_name):
    """Configure and start sliver."""
    rec = sliver_name
    account.get(rec['name']).start(rec)
    logger.log("api_calls: Start %s"%rec['name'])


@export_to_docbook(roles=['nm-controller', 'self'],
                    accepts=[Parameter(str, 'A sliver/slice name.')],
                   returns=Parameter(int, '1 if successful'))
@export_to_api(1)
def Stop(sliver_name):
    """Kill all processes belonging to the specified sliver"""
    rec = sliver_name
    account.get(rec['name']).stop()
    logger.log("api_calls: Stop %s"%rec['name'])


@export_to_docbook(roles=['nm-controller', 'self'],
                    accepts=[Parameter(str, 'A sliver/slice name.')],
                   returns=Parameter(int, '1 if successful'))
@export_to_api(1)
def ReCreate(sliver_name):
    """Stop, Destroy, Create, Start sliver in order to reinstall it."""
    rec = sliver_name
    account.get(rec['name']).stop()
    account.get(rec['name']).ensure_created(rec)
    account.get(rec['name']).start(rec)
    logger.log("api_calls: ReCreate %s"%rec['name'])

@export_to_docbook(roles=['nm-controller', 'self'],
                    accepts=[Parameter(str, 'A sliver/slice name.')],
                   returns=Parameter(dict, "A resource specification"))
@export_to_api(1)
def GetEffectiveRSpec(sliver_name):
    """Return the RSpec allocated to the specified sliver, including loans"""
    rec = sliver_name
    return rec.get('_rspec', {}).copy()


@export_to_docbook(roles=['nm-controller', 'self'],
                    accepts=[Parameter(str, 'A sliver/slice name.')],
                    returns={"resource name" : Parameter(int, "amount")})
@export_to_api(1)
def GetRSpec(sliver_name):
    """Return the RSpec allocated to the specified sliver, excluding loans"""
    rec = sliver_name
    return rec.get('rspec', {}).copy()


@export_to_docbook(roles=['nm-controller', 'self'],
                    accepts=[Parameter(str, 'A sliver/slice name.')],
                    returns=[Mixed(Parameter(str, 'recipient slice name'),
                             Parameter(str, 'resource name'),
                             Parameter(int, 'resource amount'))])

@export_to_api(1)
def GetLoans(sliver_name):
    """Return the list of loans made by the specified sliver"""
    rec = sliver_name
    return rec.get('_loans', [])[:]

def validate_loans(loans):
    """Check that <obj> is a list of valid loan specifications."""
    def validate_loan(loan):