From: Philip Joseph Date: Wed, 14 Sep 2016 05:36:15 +0000 (+0000) Subject: New Feature : Update CLI for OSM to support onbaording of VNF/NS X-Git-Tag: v1.0.0~29 X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=refs%2Fchanges%2F20%2F320%2F1;p=osm%2FSO.git New Feature : Update CLI for OSM to support onbaording of VNF/NS Signed-off-by: Philip Joseph --- diff --git a/rwlaunchpad/plugins/rwlaunchpadtasklet/scripts/CMakeLists.txt b/rwlaunchpad/plugins/rwlaunchpadtasklet/scripts/CMakeLists.txt index 549af430..40f9465d 100644 --- a/rwlaunchpad/plugins/rwlaunchpadtasklet/scripts/CMakeLists.txt +++ b/rwlaunchpad/plugins/rwlaunchpadtasklet/scripts/CMakeLists.txt @@ -19,7 +19,7 @@ cmake_minimum_required(VERSION 2.8) -install(PROGRAMS rwlaunchpad +install(PROGRAMS onboard_pkg DESTINATION usr/bin COMPONENT ${INSTALL_COMPONENT} ) diff --git a/rwlaunchpad/plugins/rwlaunchpadtasklet/scripts/onboard_pkg b/rwlaunchpad/plugins/rwlaunchpadtasklet/scripts/onboard_pkg new file mode 100644 index 00000000..b616ccf3 --- /dev/null +++ b/rwlaunchpad/plugins/rwlaunchpadtasklet/scripts/onboard_pkg @@ -0,0 +1,393 @@ +#!/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 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._pkgs = None + + self._service_name = None + self._nsd_id = None + self._dc = None + self._account = None + + self._ip = args.so_ip + + self._uport = args.upload_port + self._upload_url = "curl -k https://{ip}:{port}/api/upload". \ + format(ip=self._ip, + port=self._uport) + + self._rport = args.restconf_port + self._user = args.restconf_user + self._password = args.restconf_password + 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". \ + format(header=self._headers, + user=self._user, + passwd=self._password, + ip=self._ip, + port=self._rport) + + @property + def log(self): + return self._log + + def validate_args(self): + 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: + try: + uuid.UUID(args.datacenter) + self._dc = args.datacenter + except ValueError as e: + raise OnboardPkgInvalidDescId("Invalid UUID for datacenter: {}". + format(args.datacenter)) + + elif args.vim_account: + self._account = args.vim_account + + else: + raise OnboardPkgMissingAcct("Datacenter or VIM account required for instantiation") + + 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._account)) + + if (self._pkgs is None) and (self._nsd_id is None): + raise OnboardPkgInputError("Need to specify either upload-pkg or instantiate 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, shell=True) + (output, err) = proc.communicate() + rc = proc.returncode + self.log.debug("Command exec status: {}, {}, {}".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+"/resource-orchestrator" + 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 \"descriptor=@{pkg}\" ". \ + format(url=self._upload_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 + + # TODO: Add check to see if datacenter is valid + + # Check cloud account is valid, if provided + if self._account: + acct_url = "{url}/cloud/account/{acct}". \ + format(url=self._conf_url, acct=self._account) + output = self._exec_cmd(acct_url) + if (output is None) or (len(output) == 0): + # Account not found + raise OnboardPkgAcctError("VIM/Cloud account {} provided is not valid". + format(self._account)) + + # 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'])) + + nsd = js['nsd:nsd'] + 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['om-datacenter'] = self._dc + else: + nsr['cloud-account'] = self._account + + 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 process(self): + self.validate_args() + self.validate_connectivity() + self.upload_packages() + self.instantiate() + + +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("-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("-c", "--vim-account", + help="Cloud/VIM account to instantiate on") + + parser.add_argument("-p", "--upload-port", default=4567, type=int, + help="Upload port number, default 4567") + parser.add_argument("-P", "--restconf-port", default=8888, type=int, + help="RESTconf port number, default 8888") + 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') + stderr_handler = logging.StreamHandler(stream=sys.stderr) + stderr_handler.setFormatter(fmt) + logging.basicConfig(level=logging.INFO) + log = logging.getLogger('onboard-pkg') + log.addHandler(stderr_handler) + if args.verbose: + log.setLevel(logging.DEBUG) + + log.debug("Input arguments: {}".format(args)) + + ob = OnboardPackage(log, args) + ob.process() diff --git a/rwlaunchpad/plugins/rwlaunchpadtasklet/scripts/rwlaunchpad b/rwlaunchpad/plugins/rwlaunchpadtasklet/scripts/rwlaunchpad deleted file mode 100755 index 21a06b7f..00000000 --- a/rwlaunchpad/plugins/rwlaunchpadtasklet/scripts/rwlaunchpad +++ /dev/null @@ -1,144 +0,0 @@ -#!/bin/bash - -# Script details -SCRIPTNAME=`basename $0` -SCRIPT=$0 -SCRIPT_ARGS=${@} - -# Initialize some of the variables -if [ "$RIFT_LP_ADDR" = "" ]; then - RIFT_LP_ADDR="localhost" -fi -PKGS=() -INSTANTIATE=0 -DESC_ID="" -NS_NAME="" -LOGGING=0 -RIFT_LP_PKG_UPLOAD_URL="https://${RIFT_LP_ADDR}:4567/api/upload" - -###################################################################### -# Function:usage # -# Prints usage # -###################################################################### -function usage() { - cat < ) and stderr to file - # and store the STDOUT and STDERR for later use - exec 3>&1 - exec 4>&2 - exec >$LOGFILE - exec 2>&1 -fi - -echo "Started at $DATE" - -# Iterate through the packages and upload them -for PKG in "${PKGS[@]}" -do - echo "Uploading package $PKG" - upload_package $PKG - echo "" -done - -if [ "${INSTANTIATE}" -eq 1 ]; then - instantiate_ns $DESC_ID -fi - -echo "Ended at $DATE"