implement rest API for ICE tests 45/34545/1
authorMickael JEZEQUEL <mickael.jezequel@orange.com>
Wed, 7 Mar 2018 16:19:48 +0000 (17:19 +0100)
committerMickael JEZEQUEL <mickael.jezequel@orange.com>
Wed, 7 Mar 2018 16:21:45 +0000 (17:21 +0100)
Change-Id: I1ccb630858907161c4e8b13986fe963bb309b608
Issue-ID: VNFSDK-213
Signed-off-by: Mickael JEZEQUEL <mickael.jezequel@orange.com>
14 files changed:
ice-server/README.MD [new file with mode: 0644]
ice-server/__init__.py [new file with mode: 0644]
ice-server/heat_test/__init__.py [new file with mode: 0644]
ice-server/heat_test/app.py [new file with mode: 0644]
ice-server/heat_test/default_settings.py [new file with mode: 0644]
ice-server/heat_test/heat_validator.py [new file with mode: 0644]
ice-server/heat_test/swagger/ice_api.yaml [new file with mode: 0644]
ice-server/heat_test/test/fixture/test.zip [new file with mode: 0644]
ice-server/heat_test/test/test_api.py [new file with mode: 0644]
ice-server/pom.xml [new file with mode: 0644]
ice-server/requirements.txt [new file with mode: 0644]
ice-server/setup.py [new file with mode: 0644]
ice-server/tox.ini [new file with mode: 0644]
pom.xml

diff --git a/ice-server/README.MD b/ice-server/README.MD
new file mode 100644 (file)
index 0000000..87a5341
--- /dev/null
@@ -0,0 +1,32 @@
+Introduction:
+=============
+This document provides the required steps for running the 
+heat validation rest service in a development environment.
+If you only want to run it, go to distribution directory.
+
+Installation steps:
+====================
+Install following software:
+- python 3.4 or latest
+
+Configuration steps:
+====================
+you can override the defaults settings (declared in default_settings.py)
+if needed by pointing the env variable ICE_SETTINGS to a file
+with the appropriate values :
+
+``$ export ICE_SETTINGS='/path/to/my/production/settings.py'``
+
+How to run?
+===========
+ - In command console, cd to directory where this service is installed 
+ - use virtualenv if needed
+ - ``$ pip install -r requirements.txt``
+ - check that everything is ok by running unit tests : ``$ tox``
+ - ``$ python app.py``
+ - Once service is started, from MSB service, verify that "ice-test"  is reported from GET request on "/openoapi/microservices/v1/services"
+
+Debug/development Mode
+==========
+ debug mode is activated by default
+ in debug mode, the swagger ui is available on http://127.0.0.1:5000/onapapi/ice/v1/ui
\ No newline at end of file
diff --git a/ice-server/__init__.py b/ice-server/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/ice-server/heat_test/__init__.py b/ice-server/heat_test/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/ice-server/heat_test/app.py b/ice-server/heat_test/app.py
new file mode 100644 (file)
index 0000000..e982c68
--- /dev/null
@@ -0,0 +1,65 @@
+#!flask/bin/python
+import argparse
+import logging
+import requests
+
+import connexion
+from connexion.resolver import RestyResolver
+
+
+def parse_args():
+    """
+    parse argument parameters
+    """
+    parser = argparse.ArgumentParser(description='start the heat validation rest server')
+    parser.add_argument("--debug", help="increase output verbosity")
+    parser.add_argument("-p", "--port", type=int, help="listen port (default 5000)", default=5000)
+    args = parser.parse_args()
+    if args.debug:
+        logging.info("debug mode")
+        debug = True
+    else:
+        debug = False
+    if args.port:
+        port = args.port
+    else:
+        port = 5000
+
+
+def create_app():
+    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+    app = connexion.App(__name__, specification_dir='swagger/')
+
+    # load default config
+    app.app.config.from_object("default_settings")
+    app.app.config.from_envvar('ICE_SETTINGS', silent=True)
+    app.add_api('ice_api.yaml', swagger_ui=app.app.config['DEBUG'], resolver=RestyResolver('ice_validator'))
+
+    return app
+
+def create_test_app():
+    print("create_test_app")
+    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+    app = connexion.App(__name__, specification_dir='swagger/')
+    app.add_api('ice_api.yaml', swagger_ui=app.app.config['DEBUG'], resolver=RestyResolver('ice_validator'))
+    app.app.testing = True
+    return app
+
+
+def start_app(app):
+    logging.info("######################################")
+    logging.info("starting server")
+    logging.info("######################################")
+    app.run(port=app.app.config['ICE_PORT'], debug=app.app.config['DEBUG'])
+
+    # register service in MSB
+    try:
+        msb_url = app.app.config['MSB_URL']
+        requests.get(msb_url)
+        logging.info("registration to %s done", msb_url)
+    except:
+        logging.info("registration to %s failed", msb_url)
+
+
+if __name__ == '__main__':
+    start_app(create_app())
diff --git a/ice-server/heat_test/default_settings.py b/ice-server/heat_test/default_settings.py
new file mode 100644 (file)
index 0000000..d32d039
--- /dev/null
@@ -0,0 +1,9 @@
+# ICE settings
+DEBUG = True
+ICE_PORT = 5000
+
+# MSB settings
+MSB_IP = "127.0.0.1"
+MSB_PORT = "80"
+MSB_URL = "http://"+MSB_IP+':'+MSB_PORT+"/api/microservices/v1/services"
+
diff --git a/ice-server/heat_test/heat_validator.py b/ice-server/heat_test/heat_validator.py
new file mode 100644 (file)
index 0000000..0cc32a1
--- /dev/null
@@ -0,0 +1,79 @@
+import logging
+import os
+import shutil
+import sys
+import tempfile
+import zipfile
+from io import StringIO
+
+import pytest
+from flask import (request, jsonify, abort)
+
+
+class HeatValidator(object):
+    """REST service for HEAT templates validation"""
+
+    # Customize messages for pytest exit codes...
+    msg = {0: 'OK',
+           1: 'Tests failed',
+           2: 'Interrupted',
+           3: 'Internal error',
+           4: 'Usage error',
+           5: 'No tests collected'}
+
+    def ping(self):
+        return "pong"
+
+    def validate(self):
+        """validate the heat template contained in the uploaded zipfile
+        by running the ice_validator scripts"""
+        logging.info("validate")
+
+        # check if the post request is valid
+        if 'file' not in request.files:
+            logging.error("invalid request: no file found")
+            abort(422, {'status': 1, 'message': 'no file found'})
+
+        try:
+            # extract the uploaded archive
+            zip_file = request.files['file']
+            tmp_dir = tempfile.mkdtemp()
+            zip_ref = zipfile.ZipFile(zip_file, 'r')
+            zip_ref.extractall(tmp_dir)
+            zip_ref.close()
+            debug = request.args.get('debug')
+
+            # execute the validation scripts with pytest
+            if debug == 'true':
+                # Save the original stream output, the console basically
+                original_output = sys.stdout
+                # Assign StringIO so the output is not sent anymore to the console
+                sys.stdout = StringIO()
+            exit_code = pytest.main(['../../validation-scripts/ice_validator',
+                                     '--resultlog=' + tmp_dir + '/result.txt',
+                                     '--template-dir', tmp_dir])
+            with open(tmp_dir + '/result.txt', 'r') as result_file:
+                result = result_file.read()
+            if debug == 'true':
+                output = sys.stdout.getvalue()
+                # close the stream and reset stdout to the original value (console)
+                sys.stdout.close()
+                sys.stdout = original_output
+        except zipfile.BadZipFile:
+            logging.exception("invalid file")
+            abort(422, {'status': 4, 'message': 'invalid file'})
+        except:
+            logging.exception("server error on file")
+            abort(500, {'status': 3, 'message': 'server error'})
+        finally:
+            if os.path.exists(tmp_dir):
+                shutil.rmtree(tmp_dir)
+
+        result = {'status': exit_code, 'message': self.msg[exit_code], 'result': result}
+        if debug == 'true':
+            result['debug'] = output
+
+        return jsonify(result), 200 if (exit_code == 0) else 422
+
+
+class_instance = HeatValidator()
diff --git a/ice-server/heat_test/swagger/ice_api.yaml b/ice-server/heat_test/swagger/ice_api.yaml
new file mode 100644 (file)
index 0000000..c12269f
--- /dev/null
@@ -0,0 +1,85 @@
+swagger: "2.0"
+
+info:
+  title: "ICE validation"
+  description: "Heat template validation rest API"
+  contact:
+    name: ONAP
+    url: https://www.onap.org
+    email: onap-discuss@lists.onap.org
+  license:
+    name: Apache 2.0
+    url: http://www.apache.org/licenses/LICENSE-2.0.html
+  version: "1.0.0"
+
+basePath: /onapapi/ice/v1
+
+paths:
+  /:
+    get:
+      summary: "list HEAT templates"
+      operationId: heat_validator.class_instance.ping
+      responses:
+        '200':
+          description: 'server is up and running'
+          schema:
+            type: string
+    post:
+      summary: "validate HEAT template"
+      operationId: heat_validator.class_instance.validate
+      consumes: [
+        "multipart/form-data"
+      ]
+      produces: [
+        "application/json"
+      ]
+      parameters: [
+         {
+            "name": "file",
+            "in": "formData",
+            "description": "file to upload",
+            "required": true,
+            "type": "file"
+        },
+        {
+            "name": "debug",
+            "in": "query",
+            "description": "debug mode",
+            "required": false,
+            "type": "boolean"
+        }
+      ]
+      responses:
+        "200":
+          description: 'validation success'
+          schema: {
+              "$ref": "#/definitions/Result"
+          }
+        "400":
+          description: 'validation error'
+          schema: {
+              "$ref": "#/definitions/Result"
+          }
+        "500":
+          description: 'validation error'
+          schema: {
+              "$ref": "#/definitions/Result"
+          }
+definitions:
+  Result:
+    required: [
+      "status",
+      "message"
+    ]
+    properties: {
+      "status": {
+        "type": "integer",
+        "format": "int64"
+      },
+      "message": {
+        "type": "string"
+      },
+      "debug": {
+        "type": "string"
+      }
+    }
\ No newline at end of file
diff --git a/ice-server/heat_test/test/fixture/test.zip b/ice-server/heat_test/test/fixture/test.zip
new file mode 100644 (file)
index 0000000..d1e6d99
Binary files /dev/null and b/ice-server/heat_test/test/fixture/test.zip differ
diff --git a/ice-server/heat_test/test/test_api.py b/ice-server/heat_test/test/test_api.py
new file mode 100644 (file)
index 0000000..259a43b
--- /dev/null
@@ -0,0 +1,62 @@
+import io
+import json
+import os
+import sys
+
+import pytest
+
+#sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir))
+
+# Make sure that the application source directory (this directory's parent) is
+# on sys.path.
+here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, here)
+print(sys.path)
+
+import app as myapp
+
+ICE_URL = '/ice/'
+
+flask_app = myapp.create_test_app()
+
+
+@pytest.fixture(scope='module')
+def client():
+    with flask_app.app.test_client() as c:
+        yield c
+
+
+def test_404(client):
+    response = client.get('/dummy')
+    assert response.status_code == 404
+
+
+def test_ping(client):
+    response = client.get(ICE_URL, content_type='application/json')
+    assert response.status_code == 200
+
+
+def test_validate_nofile(client):
+    response = client.post(ICE_URL)
+    assert response.status_code == 400
+    assert json_of_response(response) is not None
+
+
+def test_validate_not_zip(client):
+    data = {'file': (io.BytesIO(b'my file contents'), 'hello world.txt')}
+    response = client.post(ICE_URL, data=data)
+    assert response.status_code == 422
+
+
+@pytest.mark.skip(reason="no way of currently testing this")
+def test_validate_bad_zip(client):
+    zf = os.path.join(os.path.dirname(__file__), 'fixture/test.zip')
+    data = {'file': (zf, 'test.zip')}
+    response = client.post(ICE_URL, data=data)
+    print("##"+zf)
+    assert response.status_code == 200
+
+
+def json_of_response(response):
+    """Decode json from response"""
+    return json.loads(response.data.decode('utf8'))
diff --git a/ice-server/pom.xml b/ice-server/pom.xml
new file mode 100644 (file)
index 0000000..3e6d2a5
--- /dev/null
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<!--
+Copyright (c) 2018 Orange. 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.onap.oparent</groupId>
+        <artifactId>oparent</artifactId>
+        <version>1.0.0-SNAPSHOT</version>
+        <relativePath>../../oparent</relativePath>
+    </parent>
+
+    <groupId>org.onap.vnfsdk.ice</groupId>
+    <artifactId>vnf-sdk-ice-server</artifactId>
+    <packaging>pom</packaging>
+
+    <name>ice rest API</name>
+    <description>rest API for HEAT template validation</description>
+    <properties>
+      <sonar.language>py</sonar.language>
+      <sonar.pluginName>Python</sonar.pluginName>
+      <sonar.inclusions>**/*.py</sonar.inclusions>
+      <sonar.skip>false</sonar.skip>
+    </properties>
+</project>
diff --git a/ice-server/requirements.txt b/ice-server/requirements.txt
new file mode 100644 (file)
index 0000000..4ea1f75
--- /dev/null
@@ -0,0 +1,3 @@
+Flask
+connexion
+pytest
\ No newline at end of file
diff --git a/ice-server/setup.py b/ice-server/setup.py
new file mode 100644 (file)
index 0000000..b4c5aea
--- /dev/null
@@ -0,0 +1,73 @@
+#!/usr/bin/env python\r
+\r
+#\r
+# Copyright (c) 2018 Orange. All rights reserved.\r
+#\r
+# Licensed under the Apache License, Version 2.0 (the "License"); you may\r
+# not use this file except in compliance with the License. You may obtain\r
+# a copy of the License at\r
+#\r
+#      http://www.apache.org/licenses/LICENSE-2.0\r
+#\r
+# Unless required by applicable law or agreed to in writing, software\r
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT\r
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the\r
+# License for the specific language governing permissions and limitations\r
+# under the License.\r
+#\r
+\r
+import os\r
+from setuptools import setup, find_packages\r
+import sys\r
+\r
+if sys.version_info < (2, 7):\r
+    sys.exit('VNF SDK requires Python 2.7+')\r
+\r
+root_dir = os.path.dirname(__file__)\r
+install_requires = []\r
+extras_require = {}\r
+\r
+with open(os.path.join(root_dir, 'requirements.txt')) as requirements:\r
+    for requirement in requirements.readlines():\r
+        # get rid of comments or trailing comments\r
+        requirement = requirement.split('#')[0].strip()\r
+        if not requirement:\r
+            continue # skip empty and comment lines\r
+        # dependencies which use environment markers have to go in as\r
+        # conditional dependencies under "extra_require", see more at:\r
+        # https://wheel.readthedocs.io/en/latest/index.html#defining-conditional-dependencies\r
+        if ';' in requirement:\r
+            package, condition = requirement.split(';')\r
+            cond_name = ':{0}'.format(condition.strip())\r
+            extras_require.setdefault(cond_name, [])\r
+            extras_require[cond_name].append(package.strip())\r
+        else:\r
+            install_requires.append(requirement)\r
+\r
+setup(\r
+    name='vnfsdk-ice-server',\r
+    version='0.1',\r
+    description='VNF SDK Heat validation tool',\r
+    license='Apache License Version 2.0',\r
+    url='http://onap.org/',\r
+\r
+    packages=find_packages('.'),\r
+#    packages=[\r
+#        'heat_test',\r
+#        'heat_test.ice_validator',\r
+#        'heat_test.ice_validator.tests',\r
+#        'heat_test.ice_validator.tests.utils'\r
+#    ],\r
+\r
+package_dir={'heat_test': 'heat_test'},\r
+#    package_dir={\r
+#        'heat_test': 'src/heat_test',\r
+#        'heat_test.ice_validator': 'src/heat_test/ice_validator',\r
+#        'heat_test.ice_validator.tests': 'src/heat_test/ice_validator/tests',\r
+#        'heat_test.ice_validator.tests.utils': 'src/heat_test/ice_validator/tests/utils'\r
+#    },\r
+\r
+    include_package_data=True,\r
+    install_requires=install_requires,\r
+    extras_require=extras_require)\r
+\r
diff --git a/ice-server/tox.ini b/ice-server/tox.ini
new file mode 100644 (file)
index 0000000..8da170d
--- /dev/null
@@ -0,0 +1,53 @@
+# -*- coding: utf8 -*-
+# ============LICENSE_START=======================================================
+# org.onap.vvp/validation-scripts
+# ===================================================================
+# Copyright © 2017 AT&T Intellectual Property. All rights reserved.
+# ===================================================================
+#
+# Unless otherwise specified, all software contained herein is licensed
+# under the Apache License, Version 2.0 (the “License”);
+# you may not use this software 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.
+#
+#
+#
+# Unless otherwise specified, all documentation contained herein is licensed
+# under the Creative Commons License, Attribution 4.0 Intl. (the “License”);
+# you may not use this documentation except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#             https://creativecommons.org/licenses/by/4.0/
+#
+# Unless required by applicable law or agreed to in writing, documentation
+# 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.
+#
+# ============LICENSE_END============================================
+#
+# ECOMP is a trademark and service mark of AT&T Intellectual Property.
+#
+
+[tox]
+skipsdist=True
+envlist = py3
+
+[testenv]
+distribute = False
+commands =
+   {envpython} --version
+   pytest
+deps = -rrequirements.txt
+
+[testenv:py3]
+basepython=python3.5
diff --git a/pom.xml b/pom.xml
index 0632691..0a941ea 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -32,5 +32,6 @@ under the License.
 
     <modules>
         <module>validation-scripts</module>
+        <module>ice-server</module>
     </modules>
 </project>