#!/usr/bin/env python3

############################################################################
# Copyright 2016 RIFT.io Inc                                               #
#                                                                          #
# 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.                                           #
############################################################################

import argparse
from contextlib import closing
import logging
import os.path
import socket
import subprocess
import sys
import uuid

import json


class OnboardPkgError(Exception):
    pass


class OnboardPkgInputError(OnboardPkgError):
    pass


class OnboardPkgMissingPkg(OnboardPkgError):
    pass


class OnboardPkgFileError(OnboardPkgError):
    pass


class OnboardPkgMissingDescId(OnboardPkgError):
    pass


class OnboardPkgInvalidDescId(OnboardPkgError):
    pass


class OnboardPkgMissingAcct(OnboardPkgError):
    pass


class OnboardPkgSoConnError(OnboardPkgError):
    pass


class OnboardPkgCmdError(OnboardPkgError):
    pass


class OnboardPkgUploadError(OnboardPkgError):
    pass


class OnboardPkgRcConnError(OnboardPkgError):
    pass


class OnboardPkgDcError(OnboardPkgError):
    pass


class OnboardPkgAcctError(OnboardPkgError):
    pass


class OnboardPkgNsdError(OnboardPkgError):
    pass


class OnboardPkgInstError(OnboardPkgError):
    pass


class OnboardPkgInvalidPort(OnboardPkgError):
    pass


class OnboardPackage:

    def __init__(self,
                 log,
                 args):
        self._log = log
        self._args = args

        self._project = args.project

        self._pkgs = None

        self._service_name = None
        self._nsd_id = None
        self._dc = None
        self._ro = None

        self._ip = args.so_ip
        self._api_server_ip = "localhost"

        self._uport = args.upload_port
        self._onboard_port = args.onboard_port
        self._rport = args.restconf_port
        self._user = args.restconf_user
        self._password = args.restconf_password
        self._onboard_url = "curl -k --user \"{user}:{passwd}\" \"https://{ip}:{port}/composer/upload?api_server=https://{api_server_ip}&upload_server=https://{ip}\"". \
                             format(ip=self._ip,
                                    port=self._onboard_port,
                                    user=self._user,
                                    passwd=self._password,
                                    api_server_ip=self._api_server_ip)

        self._upload_url = "curl -k https://{ip}:{port}/api/upload". \
                            format(ip=self._ip,
                                   port=self._uport)

        self._headers = '-H "accept: application/json"' + \
                        ' -H "content-type: application/json"'

        self._conf_url = "curl -k {header} --user \"{user}:{passwd}\" https://{ip}:{port}/api/config/project/{project}". \
                       format(header=self._headers,
                              user=self._user,
                              passwd=self._password,
                              ip=self._ip,
                              port=self._rport,
                              project=self._project)

        self._oper_url = "curl -k {header} --user \"{user}:{passwd}\" https://{ip}:{port}/api/operational/project/{project}". \
                       format(header=self._headers,
                              user=self._user,
                              passwd=self._password,
                              ip=self._ip,
                              port=self._rport,
                              project=self._project)

    @property
    def log(self):
        return self._log

    def validate_args(self):
        args = self._args
        if args.upload_pkg is not None:
            self._pkgs = args.upload_pkg
            self.log.debug("Packages to upload: {}".format(self._pkgs))
            if len(self._pkgs) == 0:
                raise OnboardPkgMissingPkg('Need to specify atleast one package to upload')

            for pkg in self._pkgs:
                self.log.debug("Check pkg: {}".format(pkg))
                if os.path.isfile(pkg) is False:
                    raise OnboardPkgFileError("Unable to access file: {}".format(pkg))

        if args.instantiate:
            if args.nsd_id is None:
                raise OnboardPkgMissingDescId("NS Descriptor ID required for instantiation")

            if args.datacenter:
                self._dc = args.datacenter

            if args.resource_orchestrator:
                self._ro = args.resource_orchestrator
            
            self._service_name = args.instantiate
            self._nsd_id = args.nsd_id

            self.log.debug("Instantiate NSD {} as {} on {}".format(self._nsd_id,
                                                                   self._service_name,
                                                                   self._dc))

        if (self._pkgs is None) and (self._nsd_id is None) and (not args.list_nsds):
            raise OnboardPkgInputError("Need to specify either upload-pkg or instantiate or list options")

        # Validate the port numbers are correct
        def valid_port(port):
            if 1 <= port <= 65535:
                return True
            return False

        if not valid_port(self._uport):
            raise OnboardPkgInvalidPort("Invalid upload port: {}".format(self._uport))

        if not valid_port(self._rport):
            raise OnboardPkgInvalidPort("Invalid Restconf port: {}".format(self._rport))

    def _exec_cmd(self, cmd):
        self.log.debug("Execute command: {}".format(cmd))
        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE, shell=True)
        (output, err) = proc.communicate()
        rc = proc.returncode
        self.log.debug("Command exec status: {}\nSTDOUT: {}\nSTDERR: {}".
                       format(rc, output, err))
        if rc != 0:
            raise OnboardPkgCmdError("Command {} failed ({}): {}".
                                            format(cmd, rc, err))
        return output.decode("utf-8")

    def validate_connectivity(self):
        if self._pkgs:
            self.log.debug("Check connectivity to SO at {}:{}".
                           format(self._ip, self._uport))

            with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
                if sock.connect_ex((self._ip, self._uport)) != 0:
                    raise OnboardPkgSoConnError("Connection error to SO for upload at {}:{}".
                                                format(self._ip, self._uport))
            self.log.debug("Connection to SO upload port succeeded")

        if self._nsd_id:
            self.log.debug("Check connectivity to SO at {}:{}, with credentials {}:{}".
                           format(self._ip, self._rport, self._user, self._password))

            rest_url = self._conf_url+"/ro-account"
            try:
                output = self._exec_cmd(rest_url)
                self.log.debug("Output of restconf validation: {}".
                               format(output))
                if len(output) != 0:
                    js = json.loads(output)
                    if "error" in js:
                        raise OnboardPkgRcConnError("SO Restconf connect error: {}".
                                                    format(js["error"]))

                self.log.debug("Connection to SO restconf port succeeded")

            except OnboardPkgCmdError as e:
                self.log.error("SO restconf connect failed: {}".format(e))
                raise OnboardPkgRcConnError("SO Restconf connect error: {}".
                                            format(e))


    def _upload_package(self, pkg):
        upload_cmd = "{url} -F \"package=@{pkg}\" ". \
                                          format(url=self._onboard_url,
                                                 pkg=pkg)
        self.log.debug("Upload pkg {} cmd: {}".format(pkg, upload_cmd))

        output = self._exec_cmd(upload_cmd)

        # Get the transaction id and wait for upload to complete
        tx_id = json.loads(output)['transaction_id']

        upload_status_url = "{url}/{id}/state". \
                            format(url=self._upload_url,
                                   id=tx_id)
        status = ""
        while status not in ['success', 'failure']:
            output = self._exec_cmd(upload_status_url)
            js = json.loads(output)
            self.log.debug("Upload status of pkg {}: {}".format(pkg, js))
            status = js['status']

        if status != 'success':
            raise OnboardPkgUploadError("Package {} upload failed: {}".
                                        format(pkg, js['errors']))

        self.log.info("Upload of package {} succeeded".format(pkg))

    def upload_packages(self):
        if self._pkgs is None:
            self.log.debug("Upload packages not provided")
            return

        for pkg in self._pkgs:
            self._upload_package(pkg)

    def instantiate(self):
        if self._nsd_id is None:
            self.log.debug("No NSD ID provided for instantiation")
            return

        # Check id NSD ID is valid
        nsd_url = "{url}/nsd-catalog/nsd/{nsd_id}". \
                  format(url=self._conf_url, nsd_id=self._nsd_id)
        output = self._exec_cmd(nsd_url)
        if (output is None) or (len(output) == 0):
            # NSD not found
            raise OnboardPkgNsdError("NSD ID {} provided is not valid".
                                     format(self._nsd_id))

        js = json.loads(output)
        if "error" in js:
            raise OnboardPkgNsdError("NSD ID {} error: {}".
                                     format(self._nsd_id,
                                            js['error']))

        try:
            nsd = js['project-nsd:nsd']
        except KeyError as e:
            raise OnboardPkgNsdError("NSD ID {} provided is not valid".
                                     format(self._nsd_id))

        self.log.debug("NSD to instantiate: {}".format(nsd))

        # Generate a UUID for NS
        ns_id = str(uuid.uuid4())
        self.log.debug("NS instance uuid: {}".format(ns_id))

        # Build the nsr post data
        nsr = {"id": ns_id,
               'name': self._service_name,
               "nsd": nsd,}
        if self._dc:
            nsr['datacenter'] = self._dc
         
        if self._ro:
            nsr['resource-orchestrator'] = self._ro  

        data = {'nsr': [nsr]}

        data_str = json.dumps(data)
        self.log.debug("NSR post data: {}".format(data_str))

        inst_url = "{url}/ns-instance-config -X POST -d '{data}'". \
                   format(url=self._conf_url, data=data_str)
        output = self._exec_cmd(inst_url)
        self.log.debug("Instantiate output: {}".format(output))

        js = json.loads(output)

        if "last-error" in js:
            msg = "Error instantiating NS as {} with NSD {}: ". \
                  format(self._service_name, self._nsd_id,
                         js["last-error"])
            self.log.error(msg)
            raise OnboardPkgInstError(msg)

        elif "rpc-reply" in js:
            reply = js["rpc-reply"]
            if "rpc-error" in reply:
                msg = "Error instantiating NS as {} with NSD {}: ". \
                  format(self._service_name, self._nsd_id,
                         reply["rpc-error"])
                # self.log.error(msg)
                raise OnboardPkgInstError(msg)

        self.log.info("Successfully initiated instantiation of NS as {} ({})".
                      format(self._service_name, ns_id))

    def list_nsds(self):
        if self._args.list_nsds:
            self.log.debug("Check NSDS at {}:{}, with credentials {}:{}".
                           format(self._ip, self._rport, self._user, self._password))

            rest_url = self._conf_url+"/nsd-catalog/nsd"
            try:
                output = self._exec_cmd(rest_url)
                self.log.debug("Output of NSD list: {}".
                               format(output))
                if output:
                    js = json.loads(output)
                    if "error" in js:
                        raise OnboardPkgRcConnError("SO Restconf connect error: {}".
                                                    format(js["error"]))
                else:
                    print("No NSDs found on SO")
                    return

                self.log.debug("NSD list: {}".format(js))
                print('List of NSDs on SO:\nName\tID')
                for nsd in js['project-nsd:nsd']:
                    print('{}\t{}'.format(nsd['name'], nsd['id']))

            except OnboardPkgCmdError as e:
                self.log.error("SO restconf connect failed: {}".format(e))
                raise OnboardPkgRcConnError("SO Restconf connect error: {}".
                                            format(e))

    def process(self):
        try:
            self.validate_args()
        except Exception as e:
            if args.verbose:
                log.exception(e)

            print("\nERROR:", e)
            print("\n")
            parser.print_help()
            sys.exit(2)

        self.validate_connectivity()
        self.upload_packages()
        self.instantiate()
        self.list_nsds()


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Upload and instantiate NS')
    parser.add_argument("-s", "--so-ip", default='localhost',
                        help="SO Launchpad IP")

    parser.add_argument("-u", "--upload-pkg", action='append',
                        help="Descriptor packages to upload. " + \
                        "If multiple descriptors are provided, they are uploaded in the same sequence.")

    parser.add_argument("-l", "--list-nsds", action='store_true',
                        help="List available network service descriptors")

    parser.add_argument("-i", "--instantiate",
                        help="Instantiate a network service with the name")
    parser.add_argument("-d", "--nsd-id",
                        help="Network descriptor ID to instantiate")
    parser.add_argument("-D", "--datacenter",
                        help="OpenMano datacenter to instantiate on")
    parser.add_argument("-r", "--resource-orchestrator",
                        help="RO account to instantiate on")

    parser.add_argument("--project", default='default',
                        help="Project to use, default 'default'")
    parser.add_argument("-o", "--onboard-port", default=8443, type=int,
                        help="Onboarding port number - node port number, default 8443")
    parser.add_argument("-p", "--upload-port", default=4567, type=int,
                        help="Upload port number, default 4567")
    parser.add_argument("-P", "--restconf-port", default=8008, type=int,
                        help="RESTconf port number, default 8008")
    parser.add_argument("--restconf-user", default='admin',
                        help="RESTconf user name, default admin")
    parser.add_argument("--restconf-password", default='admin',
                        help="RESTconf password, default admin")

    parser.add_argument("-v", "--verbose", action='store_true',
                        help="Show more logs")

    args = parser.parse_args()

    fmt = logging.Formatter(
        '%(asctime)-23s %(levelname)-5s  (%(name)s@%(process)d:' \
        '%(filename)s:%(lineno)d) - %(message)s')
    log = logging.getLogger('onboard-pkg')
    log.setLevel(logging.INFO)
    if args.verbose:
        log.setLevel(logging.DEBUG)
    ch = logging.StreamHandler()
    ch.setLevel(logging.DEBUG)
    ch.setFormatter(fmt)
    log.addHandler(ch)

    log.debug("Input arguments: {}".format(args))

    try:
    	ob = OnboardPackage(log, args)
    	ob.process()
    except Exception as e:
        if args.verbose:
            log.exception(e)

        print("\nERROR:", e)
        sys.exit(1)

