| #!/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._pkgs = None |
| |
| self._service_name = None |
| self._nsd_id = None |
| self._dc = None |
| self._account = 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". \ |
| format(header=self._headers, |
| user=self._user, |
| passwd=self._password, |
| ip=self._ip, |
| port=self._rport) |
| self._oper_url = "curl -k {header} --user \"{user}:{passwd}\" https://{ip}:{port}/api/operational". \ |
| 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, |
| 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+"/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 \"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 to see if datacenter is valid |
| if self._dc: |
| dc_url = "{url}/datacenters". format(url=self._oper_url) |
| output = self._exec_cmd(dc_url) |
| if (output is None) or (len(output) == 0): |
| # Account not found |
| raise OnboardPkgDcError("Datacenter {} provided is not valid". |
| format(self._dc)) |
| found = False |
| js = json.loads(output) |
| if "ro-accounts" in js["rw-launchpad:datacenters"]: |
| for ro in js["rw-launchpad:datacenters"]["ro-accounts"]: |
| if "datacenters" in ro: |
| for dc in ro["datacenters"]: |
| if dc["uuid"] == self._dc: |
| self.log.debug("Found datacenter {}".format(dc)) |
| found = True |
| break |
| if found: |
| break |
| |
| if found is False: |
| raise OnboardPkgDcError("Datacenter {} provided is not valid". |
| format(self._dc)) |
| |
| |
| # 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): |
| 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() |
| |
| |
| 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("-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) |
| |