#!/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)