New Feature : Update CLI for OSM to support onbaording of VNF/NS
[osm/SO.git] / rwlaunchpad / plugins / rwlaunchpadtasklet / scripts / onboard_pkg
diff --git a/rwlaunchpad/plugins/rwlaunchpadtasklet/scripts/onboard_pkg b/rwlaunchpad/plugins/rwlaunchpadtasklet/scripts/onboard_pkg
new file mode 100644 (file)
index 0000000..b616ccf
--- /dev/null
@@ -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()