From b2727af1d234bae3e64cf31a0af3dfee2d1c91f8 Mon Sep 17 00:00:00 2001 From: Mickael JEZEQUEL Date: Wed, 7 Mar 2018 17:19:48 +0100 Subject: implement rest API for ICE tests Change-Id: I1ccb630858907161c4e8b13986fe963bb309b608 Issue-ID: VNFSDK-213 Signed-off-by: Mickael JEZEQUEL --- ice-server/README.MD | 32 +++++++++++ ice-server/__init__.py | 0 ice-server/heat_test/__init__.py | 0 ice-server/heat_test/app.py | 65 ++++++++++++++++++++++ ice-server/heat_test/default_settings.py | 9 +++ ice-server/heat_test/heat_validator.py | 79 +++++++++++++++++++++++++++ ice-server/heat_test/swagger/ice_api.yaml | 85 +++++++++++++++++++++++++++++ ice-server/heat_test/test/fixture/test.zip | Bin 0 -> 166 bytes ice-server/heat_test/test/test_api.py | 62 +++++++++++++++++++++ ice-server/pom.xml | 38 +++++++++++++ ice-server/requirements.txt | 3 + ice-server/setup.py | 73 +++++++++++++++++++++++++ ice-server/tox.ini | 53 ++++++++++++++++++ pom.xml | 1 + 14 files changed, 500 insertions(+) create mode 100644 ice-server/README.MD create mode 100644 ice-server/__init__.py create mode 100644 ice-server/heat_test/__init__.py create mode 100644 ice-server/heat_test/app.py create mode 100644 ice-server/heat_test/default_settings.py create mode 100644 ice-server/heat_test/heat_validator.py create mode 100644 ice-server/heat_test/swagger/ice_api.yaml create mode 100644 ice-server/heat_test/test/fixture/test.zip create mode 100644 ice-server/heat_test/test/test_api.py create mode 100644 ice-server/pom.xml create mode 100644 ice-server/requirements.txt create mode 100644 ice-server/setup.py create mode 100644 ice-server/tox.ini diff --git a/ice-server/README.MD b/ice-server/README.MD new file mode 100644 index 0000000..87a5341 --- /dev/null +++ b/ice-server/README.MD @@ -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 index 0000000..e69de29 diff --git a/ice-server/heat_test/__init__.py b/ice-server/heat_test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ice-server/heat_test/app.py b/ice-server/heat_test/app.py new file mode 100644 index 0000000..e982c68 --- /dev/null +++ b/ice-server/heat_test/app.py @@ -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 index 0000000..d32d039 --- /dev/null +++ b/ice-server/heat_test/default_settings.py @@ -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 index 0000000..0cc32a1 --- /dev/null +++ b/ice-server/heat_test/heat_validator.py @@ -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 index 0000000..c12269f --- /dev/null +++ b/ice-server/heat_test/swagger/ice_api.yaml @@ -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 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 index 0000000..259a43b --- /dev/null +++ b/ice-server/heat_test/test/test_api.py @@ -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 index 0000000..3e6d2a5 --- /dev/null +++ b/ice-server/pom.xml @@ -0,0 +1,38 @@ + + + + 4.0.0 + + org.onap.oparent + oparent + 1.0.0-SNAPSHOT + ../../oparent + + + org.onap.vnfsdk.ice + vnf-sdk-ice-server + pom + + ice rest API + rest API for HEAT template validation + + py + Python + **/*.py + false + + diff --git a/ice-server/requirements.txt b/ice-server/requirements.txt new file mode 100644 index 0000000..4ea1f75 --- /dev/null +++ b/ice-server/requirements.txt @@ -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 index 0000000..b4c5aea --- /dev/null +++ b/ice-server/setup.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python + +# +# 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. +# + +import os +from setuptools import setup, find_packages +import sys + +if sys.version_info < (2, 7): + sys.exit('VNF SDK requires Python 2.7+') + +root_dir = os.path.dirname(__file__) +install_requires = [] +extras_require = {} + +with open(os.path.join(root_dir, 'requirements.txt')) as requirements: + for requirement in requirements.readlines(): + # get rid of comments or trailing comments + requirement = requirement.split('#')[0].strip() + if not requirement: + continue # skip empty and comment lines + # dependencies which use environment markers have to go in as + # conditional dependencies under "extra_require", see more at: + # https://wheel.readthedocs.io/en/latest/index.html#defining-conditional-dependencies + if ';' in requirement: + package, condition = requirement.split(';') + cond_name = ':{0}'.format(condition.strip()) + extras_require.setdefault(cond_name, []) + extras_require[cond_name].append(package.strip()) + else: + install_requires.append(requirement) + +setup( + name='vnfsdk-ice-server', + version='0.1', + description='VNF SDK Heat validation tool', + license='Apache License Version 2.0', + url='http://onap.org/', + + packages=find_packages('.'), +# packages=[ +# 'heat_test', +# 'heat_test.ice_validator', +# 'heat_test.ice_validator.tests', +# 'heat_test.ice_validator.tests.utils' +# ], + +package_dir={'heat_test': 'heat_test'}, +# package_dir={ +# 'heat_test': 'src/heat_test', +# 'heat_test.ice_validator': 'src/heat_test/ice_validator', +# 'heat_test.ice_validator.tests': 'src/heat_test/ice_validator/tests', +# 'heat_test.ice_validator.tests.utils': 'src/heat_test/ice_validator/tests/utils' +# }, + + include_package_data=True, + install_requires=install_requires, + extras_require=extras_require) + diff --git a/ice-server/tox.ini b/ice-server/tox.ini new file mode 100644 index 0000000..8da170d --- /dev/null +++ b/ice-server/tox.ini @@ -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 --- a/pom.xml +++ b/pom.xml @@ -32,5 +32,6 @@ under the License. validation-scripts + ice-server -- cgit 1.2.3-korg