Merge "Provide API for allowing full stack emulation"
[osm/vim-emu.git] / src / emuvim / api / osm / nbi.py
diff --git a/src/emuvim/api/osm/nbi.py b/src/emuvim/api/osm/nbi.py
new file mode 100644 (file)
index 0000000..0c5c914
--- /dev/null
@@ -0,0 +1,189 @@
+#!/usr/bin/env python2
+# Copyright (c) 2019 Erik Schilling
+# ALL RIGHTS RESERVED.
+#
+# 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 logging
+import os
+import subprocess
+
+import requests
+import time
+from pipes import quote
+from tempfile import NamedTemporaryFile
+
+import urllib3
+import yaml
+
+from emuvim.api.osm.osm_component_base import OSMComponentBase
+from emuvim.api.util.docker_utils import wrap_debian_like, DOCKER_HOST_IP
+from emuvim.api.util.path_utils import get_absolute_path
+from emuvim.api.util.process_utils import wait_until
+from mininet.log import debug
+
+# disables warnings about verify=False for TLS
+# since NBI runs with self-signed certificates this otherwise spams the log
+urllib3.disable_warnings()
+
+LOG = logging.getLogger(__name__)
+
+
+class NBI(OSMComponentBase):
+
+    def __init__(self, net, ip, mongo_ip, kafka_ip, version='latest', name_prefix=''):
+        OSMComponentBase.__init__(self)
+        self.instance = net.addDocker(
+            '{}nbi'.format(name_prefix), ip=ip, dimage=wrap_debian_like('opensourcemano/nbi:%s' % version),
+            volumes=['osm_packages:/app/storage'],
+            environment={'OSMNBI_DATABASE_URI': 'mongodb://%s:27017' % mongo_ip,
+                         'OSMNBI_MESSAGE_HOST': kafka_ip})
+        self._ip = self.instance.dcinfo['NetworkSettings']['IPAddress']
+
+    def start(self):
+        OSMComponentBase.start(self)
+        wait_until('nc -z %s 9999' % self._ip)
+
+    def _osm(self, command):
+        prefixed_command = 'osm %s' % command
+        debug('executing command: %s\n' % prefixed_command)
+        output = subprocess.check_output(prefixed_command, env={'OSM_HOSTNAME': self._ip}, shell=True)
+        debug('output: \n%s\n' % output)
+        return output
+
+    @staticmethod
+    def _create_archive(folder, tmp_file):
+        folder = get_absolute_path(folder)
+        parent_folder = os.path.dirname(folder)
+        basename = os.path.basename(folder)
+        subprocess.call('tar czf %s %s' % (quote(tmp_file.name), quote(basename)), cwd=parent_folder, shell=True)
+
+    def onboard_vnfd(self, folder):
+        try:
+            with NamedTemporaryFile() as tmp_archive:
+                self._create_archive(folder, tmp_archive)
+                return self._osm('vnfd-create %s' % quote(tmp_archive.name)).strip()
+        except subprocess.CalledProcessError as e:
+            raise RuntimeError('creating vnfd failed: %s' % e.output)
+
+    def onboard_nsd(self, folder):
+        try:
+            with NamedTemporaryFile() as tmp_archive:
+                self._create_archive(folder, tmp_archive)
+                return self._osm('nsd-create %s' % quote(tmp_archive.name)).strip()
+        except subprocess.CalledProcessError as e:
+            raise RuntimeError('creating nsd failed: %s' % e.output)
+
+    def _get_api_token(self):
+        token_request = requests.post('https://%s:9999/osm/admin/v1/tokens' % self._ip,
+                                      data={'username': 'admin', 'password': 'admin'}, verify=False)
+        if not token_request.ok:
+            raise RuntimeError('getting token failed with: %s' % token_request.text)
+        token = yaml.safe_load(token_request.text)
+        return token['id']
+
+    def _api_request_args(self):
+        return {'headers': {'Authorization': 'Bearer %s' % self._get_api_token()}, 'verify': False}
+
+    def _api_post_request(self, endpoint, data):
+        r = requests.post('https://%s:9999/%s' % (self._ip, endpoint),
+                          json=data,
+                          **self._api_request_args())
+        if not r.ok:
+            raise RuntimeError('POST request failed with: %s' % r.text)
+        result = yaml.safe_load(r.text)
+        return result
+
+    def _api_get_request(self, endpoint):
+        r = requests.get('https://%s:9999/%s' % (self._ip, endpoint),
+                         **self._api_request_args())
+        if not r.ok:
+            raise RuntimeError('GET request failed with: %s' % r.text)
+        result = yaml.safe_load(r.text)
+        return result
+
+    def _api_delete_request(self, endpoint):
+        r = requests.delete('https://%s:9999/%s' % (self._ip, endpoint),
+                            **self._api_request_args())
+        if not r.ok:
+            raise RuntimeError('DELETE request failed with: %s' % r.text)
+
+    def register_emulated_api(self, name, api):
+        output = self._osm('vim-create --name %s '
+                           '--user username '
+                           '--password password '
+                           '--auth_url http://%s:%d/v2.0 '
+                           '--tenant tenantName '
+                           '--account_type openstack' % (
+                               quote(name),
+                               quote(DOCKER_HOST_IP),
+                               api.port
+                           ))
+        vim_id = output.strip()
+        while self._api_get_request('osm/admin/v1/vim_accounts/%s' % vim_id)['_admin']['detailed-status'] != 'Done':
+            time.sleep(1)
+        return vim_id
+
+    def ns_create(self, ns_name, nsd_id, vim_id):
+        result = self._api_post_request('osm/nslcm/v1/ns_instances_content', {
+            'nsdId': nsd_id,
+            'nsName': ns_name,
+            # dummy text since this cannot be empty
+            'nsDescription': 'created by vim-emu',
+            'vimAccountId': vim_id,
+        })
+        return result['id']
+
+    def ns_get(self, ns_id):
+        return self._api_get_request('osm/nslcm/v1/ns_instances_content/%s' % ns_id)
+
+    def ns_action(self, ns_id, vnf_member_index, action, args=None):
+        if args is None:
+            args = {}
+        result = self._api_post_request('osm/nslcm/v1/ns_instances/%s/action' % ns_id, {
+            'vnf_member_index': str(vnf_member_index),
+            'primitive': action,
+            'primitive_params': args,
+        })
+        return result['id']
+
+    def ns_delete(self, ns_id):
+        self._api_delete_request('osm/nslcm/v1/ns_instances_content/%s' % ns_id)
+
+    def ns_list(self):
+        return self._api_get_request('osm/nslcm/v1/ns_instances_content')
+
+    @staticmethod
+    def _count_all_in_operational_status(ns_list, status):
+        return len(filter(lambda x: x['operational-status'] == status, ns_list))
+
+    def ns_wait_until_all_in_status(self, *statuses):
+        """Waits for all NSs to be in one of the specified stati.
+        Returns a tuple with the counts of the NSs in the invididual
+        stati in the same order as the specified stati"""
+
+        LOG.debug('Waiting for all NS to be in status {}'.format(statuses))
+        while True:
+            ns_list = self.ns_list()
+            state = ()
+            for status in statuses:
+                state += (self._count_all_in_operational_status(ns_list, status),)
+            number_correct = sum(state)
+            missing = len(ns_list) - number_correct
+            if missing == 0:
+                break
+            logging.debug('waiting for the status of %s services to change to %s' % (missing, statuses))
+            time.sleep(1)
+
+        logging.debug('all %d NSs in status %s' % (len(ns_list), statuses))
+        return state