#!/usr/bin/env python
# -*- coding: utf-8 -*-


##
# Copyright 2015 Telefónica Investigación y Desarrollo, S.A.U.
# This file is part of openvim
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# For those usages not covered by the Apache License, Version 2.0 please
# contact with: nfvlabs@tid.es
##

'''
This is a client tester for openvim.
It is almost DEPRECATED by the openvim client

The reason for keeping is because it is used for some scripts
and it contain the -r option (delete recursive)  
that it is very useful for deleting content of database.
Another difference from openvim is that it is more verbose
and so more suitable for the developers
'''

__author__="Alfonso Tierno"
__date__ ="$5-oct-2014 11:09:29$"

import requests
import json
import yaml
import sys
import getopt
from jsonschema import validate as js_v, exceptions as js_e

version="0.0.2"
global global_config


def get_elements(url):
    headers_req = {'content-type': 'application/json'}
    try:
        vim_response = requests.get(url, headers = headers_req)
        #print vim_response
        #print vim_response.status_code
        if vim_response.status_code == 200:
        #print vim_response.json()
        #print json.dumps(vim_response.json(), indent=4)
            content = vim_response.json()
            return 1, content
            #print http_content
        else:
            text = " Error. VIM response '%s': not possible to GET %s" % (vim_response.status_code, url)
            text += "\n " + vim_response.text
            #print text
            return -vim_response.status_code,text
    except requests.exceptions.RequestException, e:
        return -1, " Exception "+ str(e.message)

def delete_elements(url):
    headers_req = {'content-type': 'application/json'}
    
    try:
        vim_response = requests.delete(url, headers = headers_req)
        #print vim_response
        #print vim_response.status_code
        if vim_response.status_code == 200:
            pass
            #print vim_response.json()
            #print json.dumps(vim_response.json(), indent=4)
        else:
            #print vim_response.text
            text = " Error. VIM response '%s': not possible to DELETE %s" % (vim_response.status_code, url)
            text += "\n " + vim_response.text
            #print text
            return -vim_response.status_code,text
    except requests.exceptions.RequestException, e:
        return -1, " Exception "+ str(e.message)
    return 1, None


def new_elements(url, payload):
    headers_req = {'Accept': 'application/json', 'content-type': 'application/json'}
    #print str(payload)
    try:
        vim_response = requests.post(url, data=json.dumps(payload), headers=headers_req)
        #print vim_response
        #print vim_response.status_code
        if vim_response.status_code == 200:
            #print vim_response.json()
            #print json.dumps(vim_response.json(), indent=4)
            return 1, vim_response.text
        else:
            #print vim_response.text
            text = "Error. VIM response '%s': not possible to ADD %s" % (vim_response.status_code, url)
            text += "\n" + vim_response.text
            #print text
            return -vim_response.status_code,text
    except requests.exceptions.RequestException, e:
        return -1, " Exception "+ str(e.message)


def get_details(url, what, c):
    item_list = []
    return_dict = {what+'s': []}
    
    item = c.get(what,None)
    if item is None: item = c.get(what+'s',None)
    if item is None:
        error_text= " Internal error, not found '" + what +"[s]' in content"
        print 'get_details()', error_text, c
        return -1, error_text 
    if type(item) is list: 
        item_list = item
    else:
        item_list.append(item)
    if len(item_list)==0:
        print what, "not found"
        return 1
    for item in item_list: 
        uuid = item.get('id',None)
        if uuid is None: uuid = item.get('uuid',None)
        if uuid is None:
            error_text= " Internal error, not found 'id/uuid' in item"
            print 'get_details()', error_text, item
            return -1, error_text 
        #print " get", what, uuid, "     >>>>>>>> ",
        r,c = get_elements(url + "/" + uuid)
        if r<0:              
        #    print "fail"
            print " get", what, uuid, "fail", c
            return -1, c
        #else:
        #    print 'ok'
        return_dict[what+'s'].append(c[what])
    return 1, return_dict


def action_details(url, what, c, force, payload):
    item_list = []
    return_dict = {what+'s': []}
    headers_req = {'Accept': 'application/json', 'content-type': 'application/json'}
    fail=0
    ok=0
    
    #Allows for payload both keypairs inside a 'server','port' ... or directly. In later case, put keypairs inside what
        
    item = c.get(what,None)
    if item is None: item = c.get(what+'s',None)
    if item is None:
        error_text= " Internal error, not found '" + what +"[s]' in content"
        print 'get_details()', error_text, c
        return -1, error_text 
    if type(item) is list: 
        item_list = item
    else:
        item_list.append(item)
    if len(item_list)==0:
        print what, "not found"
        return 1
    for item in item_list: 
        name = item.get('name',None)
        uuid = item.get('id',None)
        if uuid is None: uuid = item.get('uuid',None)
        if uuid is None:
            error_text= " Internal error, not found 'id/uuid' in item"
            print 'get_details()', error_text, item
            return -1, error_text 
        if not force:
            r = raw_input("Action on  " + what + " " + uuid + " " + name + " (y/N)? ")
            if  len(r)>0  and r[0].lower()=="y":
                print " put", what, uuid, "     >>>>>>>> ",
            else:
                continue

        #print str(payload)
        try:
            vim_response = requests.post(url + "/" + uuid + "/action", data=json.dumps(payload), headers=headers_req)
            if vim_response.status_code == 200:
                print 'ok'
                ok += 1
                return_dict[what+'s'].append(vim_response.json())
                return_dict[what+'s'][-1]['uuid'] = uuid
                return_dict[what+'s'][-1]['name'] = name
            else:
                fail += 1
                print "fail"
                #print vim_response.text
                #text = "Error. VIM response '%s': not possible to PUT %s" % (vim_response.status_code, url)
                #text += "\n" + vim_response.text
                #print text
                error_dict = vim_response.json()
                error_dict['error']['uuid']=uuid
                error_dict['error']['name']=name
                return_dict[what+'s'].append(error_dict)
        except requests.exceptions.RequestException, e:
            return -1, " Exception "+ str(e.message)
    if ok>0 and fail>0: return 0, return_dict
    elif fail==0 :      return 1, return_dict
    else:               return -1, return_dict



def edit_details(url, what, c, force, payload):
    item_list = []
    return_dict = {what+'s': []}
    headers_req = {'Accept': 'application/json', 'content-type': 'application/json'}
    fail=0
    ok=0
    
    #Allows for payload both keypairs inside a 'server','port' ... or directly. In later case, put keypairs inside what
    if what not in payload:
        payload = {what:payload}
        
    item = c.get(what,None)
    if item is None: item = c.get(what+'s',None)
    if item is None:
        error_text= " Internal error, not found '" + what +"[s]' in content"
        print 'get_details()', error_text, c
        return -1, error_text 
    if type(item) is list: 
        item_list = item
    else:
        item_list.append(item)
    if len(item_list)==0:
        print what, "not found"
        return 1
    for item in item_list: 
        name = item.get('name',None)
        uuid = item.get('id',None)
        if uuid is None: uuid = item.get('uuid',None)
        if uuid is None:
            error_text= " Internal error, not found 'id/uuid' in item"
            print 'get_details()', error_text, item
            return -1, error_text 
        if not force:
            r = raw_input("Edit " + what + " " + uuid + " " + name + " (y/N)? ")
            if  len(r)>0  and r[0].lower()=="y":
                print " put", what, uuid, "     >>>>>>>> ",
            else:
                continue

        #print str(payload)
        try:
            vim_response = requests.put(url + "/" + uuid, data=json.dumps(payload), headers=headers_req)
            if vim_response.status_code == 200:
                print 'ok'
                ok += 1
                return_dict[what+'s'].append( vim_response.json()[what] )
            else:
                fail += 1
                print "fail"
                #print vim_response.text
                #text = "Error. VIM response '%s': not possible to PUT %s" % (vim_response.status_code, url)
                #text += "\n" + vim_response.text
                #print text
                error_dict = vim_response.json()
                error_dict['error']['uuid']=uuid
                error_dict['error']['name']=name
                return_dict[what+'s'].append(error_dict)
        except requests.exceptions.RequestException, e:
            return -1, " Exception "+ str(e.message)
    if ok>0 and fail>0: return 0, return_dict
    elif fail==0 :      return 1, return_dict
    else:               return -1, return_dict

def get_del_recursive(url, what, url_suffix, force=False, recursive=False):
    #print
    #print " get", what, a, "     >>>>>>>> ",
    r,c = get_elements(url + what + 's' + url_suffix)
    if r<0:
        print c, "when getting", what, url_suffix
        return -1 
    #    print "ok"

    list_todelete = c.get(what, None)
    if list_todelete is None: list_todelete = c.get(what+'s', None)
    if list_todelete is None:
        print " Internal error, not found '" + what +"[s]' in", c
        return -3,  " Internal error, not found a valid dictionary"
    if type(list_todelete) == dict:
        list_todelete = (list_todelete, )
    
    if len(list_todelete)==0:
        print what, url_suffix, "not found"
        return 1
    for c in list_todelete:
        uuid=c.get('id', None)
        if uuid is None:
            uuid=c.get('uuid', None)
        if uuid is None:
            print "Id not found"
            continue
        name = c.get("name","")
        if recursive:
            if what=='tenant' :
                get_del_recursive(url + uuid + "/", 'server', "", force, recursive)
                get_del_recursive(url + uuid + "/", 'flavor', "", force, recursive)
                get_del_recursive(url + uuid + "/", 'image', "", force, recursive)
                get_del_recursive(url, 'network', "?tenant_id="+uuid, force, recursive)
            elif what=='flavors' :
                #get_del_recursive(url, 'servers', "?flavorRef="+uuid, force, recursive)
                pass
            elif what=='image' :
                get_del_recursive(url, 'server', "?imageRef="+uuid, force, recursive)
            elif what=='hosts' :
                get_del_recursive(url, 'server', "?hostId="+uuid, force, recursive)
                
        if not force:
            r = raw_input("Delete " + what + " " + uuid + " " + name + " (y/N)? ")
            if  len(r)>0  and r[0].lower()=="y":
                pass
            else:
                continue
        r,c = delete_elements(url + what + "s/" + uuid)
        if r<0:
            #print "Error deleting", vimURI, -r
            print c
        else:
            print what, uuid, name, "deleted"
    return 1

def check_valid_uuid(uuid):
    id_schema = {"type" : "string", "pattern": "^[a-fA-F0-9]{8}(-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$"}
    try:
        js_v(uuid, id_schema)
        return True
    except js_e.ValidationError:
        return False

def change_string(text, var_list):
    end=0
    type_=None
    while True:
        ini = text.find("${", end)
        if ini<0: return text
        end = text.find("}", ini) 
        if end<0: return text
        end+=1
        
        var = text[ini:end]
        if ' ' in var:
            kk=var.split(" ")
            var=kk[0]+"}"
            type_=kk[-1][:-1]
        var = var_list.get(var, None)
        if var==None: return text
        
        text =  text[:ini] + var + text[end:]
        if type_ != None:
            if 'null' in type_ and text=="null":
                return None
            if 'int' in type_ : #and text.isnumeric():
                return int(text)
    return text

def chage_var_recursively(data, var_list):
    '''Check recursively the conent of data, and look for "*${*}*" variables and changes  
    It assumes that this variables are not in the key of dictionary,
    Attributes:
        'data': dictionary, or list. None or empty is consideted valid
        'var_list': dictionary (name:change) pairs
    Return:
        None, data is modified
    '''
        
    if type(data) is dict:
        for k in data.keys():
            if type(data[k]) is dict or type(data[k]) is tuple or type(data[k]) is list:
                chage_var_recursively(data[k], var_list)
            elif type(data[k]) is str:
                data[k] = change_string(data[k], var_list)
    if type(data) is list:
        for k in range(0,len(data)):
            if type(data[k]) is dict or type(data[k]) is list:
                chage_var_recursively(data[k], var_list)
            elif type(data[k]) is str:
                data[k] = change_string(data[k], var_list)

def change_var(data):
    if type(data) is not dict:
        return -1, "Format error, not a object (dictionary)"
    if "${}" not in data:
        return 0, data

    var_list={}
    for var in data["${}"]:
        r = var.find("}",) + 1
        if r<=2 or var[:2] != '${':
            return -1, "Format error at '${}':" + var
        #change variables inside description text
        if "${" in var[r:]:
            var = var[:r] + change_string(var[r:], var_list)
        d_start = var.rfind("(",) + 1
        d_end   = var.rfind(")",) 
        if d_start>0 and d_end>=d_start:
            default = var[d_start:d_end]
        else: default=None
        v = raw_input(var[r:] + "? ")
        if v=="":
            if default != None:
                v = default
            else:
                v = raw_input("  empty string? try again: ")
        var_list[ var[:r] ] = str(v) 
    
    del data["${}"]
    chage_var_recursively(data, var_list)
    return 0, data

def parse_yaml_json(text):
    try:
        data = yaml.load(text)
        return 0, data
    except yaml.YAMLError, exc:
        error_pos = ""
        if hasattr(exc, 'problem_mark'):
            mark = exc.problem_mark
            error_pos = " at position: (%s:%s)" % (mark.line+1, mark.column+1)
        return -1, " Error yaml/json format error at " + error_pos

def load_file(file_, parse=False):
    try:
        f = open(file_, 'r')
        read_data = f.read()
        f.close()
        if not parse:
            return 0, read_data
    except IOError, e:
        return -1, " Error opening file '" + file_ + "': " + e.args[1]
    
    try:
        data = yaml.load(read_data)
        return change_var(data)
    except yaml.YAMLError, exc:
        error_pos = ""
        if hasattr(exc, 'problem_mark'):
            mark = exc.problem_mark
            error_pos = " at position: (%s:%s)" % (mark.line+1, mark.column+1)
        return -2, " Error yaml/json format error at '"+ file_ +"'"+error_pos

def load_configuration(configuration_file):
    default_tokens ={'http_port':8080, 'http_host':'localhost', 'test_mode':False, 'of_controller_nets_with_same_vlan':True}
    
    r, config = load_file(configuration_file, parse=True)
    if r < 0:
        return False, config

    #Check default values tokens
    for k,v in default_tokens.items():
        if k not in config: config[k]=v
    
    return (True, config)

items_list = ('server','host','tenant','image','flavor','network','port')
action_list = ('list','get','new','del','edit','action')


def usage(complete=False):
    global items_list
    global action_list
    print "Usage: ", sys.argv[0], "[options]", " [" + ",".join(action_list) +"] ", "<item>   [<other>] "
    print "   Perform an test action over openvim"
    print "      "+",".join(action_list)+": List (by default), GET detais, Creates, Deletes, Edit"
    print "      <item>: can be one of " + ",".join(items_list)
    print "      <other>: list of uuid|name for 'get|del'; list of json/yaml files for 'new' or 'edit'"
    if not complete:
        print "   Type -h or --help for a complete list of options"
        return
    print "   Options:"
    print "      -v|--version: prints current version"
    print "      -c|--config [configuration_file]: loads the configuration file (default: openvimd.cfg)"
    print "      -h|--help: shows this help"
    print "      -u|--url [URL]: url to use instead of the one loaded from configuration file"
    print "      -t|--tenant [tenant uuid]: tenant to be used for some comands. IF mising it will use the default obtained in configuration file"
    print "      -F|--filter [A=B[&C=D...]: URL query string used for 'get' or 'del' commands"
    print "      -f|--force : Do not ask for confirmation when deleting. Also remove dependent objects."
    print "      -r|--recursive : Delete also dependency elements, (from tenants: images, flavors,server; from hosts: instances; ..."
    print "   Examples:"
    print "     ",sys.argv[0]," tenant                                #list tenants "
    print "     ",sys.argv[0]," -F'device_owner=external' get port    #get details of all external ports"
    print "     ",sys.argv[0]," del server ses pan                    #delete server names 'ses' and 'pan'. Do not ask for confirmation"
    print "     ",sys.argv[0]," -r -f del host                        #delete all host and all the dependencies "
    print "     ",sys.argv[0]," new host ./Host/nfv100.json           #add a host which information is in this file"
    print "     ",sys.argv[0]," edit network f348faf8-59ef-11e4-b4c7-52540030594e  '{\"network\":{\"admin_state_up\":false}}'"
    print "                             #change the admin status of this network"
    return


if __name__=="__main__":
    global vimURI
    global vimURI_admin
    
    global what
    global query_string
#init variables    
    action="list"
    what=None
    url=None
    query_string = ""
    force = False
    recursive = False
    tenant = None
    additional = []
    #look for parent dir
    config_file = '../openvimd.cfg'
    pos = sys.argv[0].rfind("/")
    if pos<0:
        base_dir="./"
    else:
        base_dir = sys.argv[0] [:pos+1]
    if pos>=0:
        config_file = base_dir + config_file

#get params    
    try:
        opts, args = getopt.getopt(sys.argv[1:], "hvrfc:u:t:F:", 
            ["config", "help", "version", "force", "filter","tenant","url","recursive"])
    except getopt.GetoptError, err:
        print " Error:", err # will print something like "option -a not recognized"
        usage()
        sys.exit(-2)

    for o, a in opts:
        if o in ("-v", "--version"):
            print "test_openvim version", version, "Oct 2014"
            print "(c) Copyright Telefonica"
            sys.exit(0)
        elif o in ("-h", "--help"):
            usage(True)
            sys.exit(0)
        elif o in ("-c", "--config"):  config_file = a
        elif o in ("-f", "--force"):   force = True
        elif o in ("-r", "--recursive"):   recursive = True
        elif o in ("-F", "--filter"):  query_string = "?"+a
        elif o in ("-u", "--url"):     url = a
        elif o in ("-t", "--tenant"):  tenant = a
        else:
            assert False, "Unhandled option"

    for a in args:
        if len(a) == 0:
            print " Warning!!! Found an empty parameter?"
        elif a[0]=="-":
            print " Error!!! Put options parameter at the beginning"
            sys.exit(-2)
        elif what is not None:
            additional.append(a)
        elif a in items_list:
            what=a
        elif a[:-1] in items_list and a[-1]=='s':
            what=a[:-1]
        elif a in action_list:
            action=a
        else:
            print " Missing <item>", ",".join(items_list)
            sys.exit(-2)
    if what is None:
        usage()
        sys.exit(-1)
    #Load configuration file
    r, config_dic = load_configuration(config_file)
    #print config_dic
    if not r:
        print config_dic
        config_dic={}
        #exit(-1)
        
    #override parameters obtained by command line
    try:
        if url is not None:
            vimURI = vimURI_admin = url
        else:
            vimURI = "http://" + config_dic['http_host'] +":"+ str(config_dic['http_port']) + "/openvim/"
            if 'http_admin_port' in config_dic:
                vimURI_admin = "http://" + config_dic['http_host'] +":"+ str(config_dic['http_admin_port']) + "/openvim/"
    except: #key error 
        print " Error: can not get URL; neither option --u,-url, nor reading configuration file"
        exit(-1)
    if tenant is None:
        tenant = config_dic.get('tenant_id', None)
    
#check enough parameters
    URI=vimURI
    if (what in ('host','port') and action in ('del','new')) or (what=='host' and action=='edit' ):
        if vimURI_admin is None:
            print " Error: Can not get admin URL; neither option -t,--tenant, nor reading configuration file"
            exit(-1)
        else:
            URI=vimURI_admin
    if URI[-1] != "/": URI+="/"
    if what in ('server','image','flavor'):
        if tenant is None:
            print " Error: Can not get tenant; neither option -t,--tenant, nor reading configuration file"
            exit(-1)
        URI += tenant + "/"
    
    
    exit_code=0
    try:
#load file for new/edit
        payload_list=[]
        if action=='new' or action=='edit' or action=='action':
            if len(additional)==0:
                if action=='new' : 
                    additional.append(base_dir+what+"s/new_"+what+".yaml")
                    #print " New what? Missing additional parameters to complete action"
                else:
                    print " What must be edited? Missing additional parameters to complete action"
                    exit(-1)
            if action=='edit'or action=='action':
                #obtain only last element
                additional_temp = additional[:-1]
                additional = additional[-1:]
                
            for a in additional:
                r,payload = load_file(a, parse=True)
                if r<0:
                    if r==-1 and "{" in a or ":" in a:
                        #try to parse directly
                        r,payload = parse_yaml_json(a)
                        if r<0:
                            print payload
                            exit (-1)
                    else:
                        print payload
                        exit (-1)
                payload_list.append(payload)
            if action=='edit'or action=='action':
                additional = additional_temp

                
#perform actions NEW
        if action=='new':
            for payload in payload_list:
                print "\n new", what, a, "     >>>>>>>> ",
                r,c = new_elements(URI+what+'s', payload)
                if r>0:
                    print "ok"
                else:
                    print "fail"
                    exit_code = -1
                print c
                #try to decode
            exit(exit_code)
            
    #perform actions GET LIST EDIT DEL
        if len(additional)==0:
            additional=[""]
        for a in additional:
            filter_qs = query_string 
            if a != "" :
                if check_valid_uuid(a):
                    if len(filter_qs) > 0:  filter_qs += "&" + "id=" + str(a)
                    else:                   filter_qs += "?" + "id=" + str(a)
                else:
                    if len(filter_qs) > 0:  filter_qs += "&" + "name=" + str(a)
                    else:                   filter_qs += "?" + "name=" + str(a)

            if action=='list' or action=='get' or action=='edit'or action=='action':
                url = URI + what+'s'
                print url + filter_qs
                #print " get", what, a, "     >>>>>>>> ",
                r,c = get_elements(url + filter_qs)
                if r<0:              
                    #print "fail"
                    exit_code = -1
                    print c
                else:             
                    #print "ok"
                    if action=='list':
                        print json.dumps(c, indent=4)
                        continue
                    
                    if action=='get':
                        r1,c1 = get_details(url, what, c)
                    elif action=='action':
                        r1,c1 = action_details(url, what, c, force, payload_list[0])
                    else: # action=='edit':
                        r1,c1 = edit_details(url, what, c, force, payload_list[0])
                    if r1<0:              
                        exit_code = -1
                    else:             
                        if r>0: print "ok"
                        else: print "ok with some fails"
                    print json.dumps(c1, indent=4)

            elif action=='del':
                r = get_del_recursive(URI, what, filter_qs, force, recursive)
                if r<0:              
                    exit_code = -1
        exit(exit_code)
            
    except KeyboardInterrupt:
        print " Canceled"
    

