diff --git a/inventory_provider/__init__.py b/inventory_provider/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2918d68e2776b9ce8c3de4f05f80c839742755b2 100644 --- a/inventory_provider/__init__.py +++ b/inventory_provider/__init__.py @@ -0,0 +1,43 @@ +""" +automatically invoked app factory +""" +import logging +import os +from flask import Flask + + +def create_app(): + """ + overrides default settings with those found + in the file read from env var SETTINGS_FILENAME + + :return: a new flask app instance + """ + + app = Flask(__name__) + app.secret_key = "super secret session key" + + from inventory_provider import data_routes + app.register_blueprint(data_routes.routes, url_prefix='/data') + + if "SETTINGS_FILENAME" not in os.environ: + assert False, \ + "environment variable SETTINGS_FILENAME' must be defined" + app.config.from_envvar("SETTINGS_FILENAME") + + assert "INVENTORY_PROVIDER_CONFIG_FILENAME" in app.config, ( + "INVENTORY_PROVIDER_CONFIG_FILENAME not defined in %s" + % os.environ["SETTINGS_FILENAME"]) + + assert os.path.isfile(app.config["INVENTORY_PROVIDER_CONFIG_FILENAME"]), ( + "config file '%s' not found" % + app.config["INVENTORY_PROVIDER_CONFIG_FILENAME"]) + + from inventory_provider import config + with open(app.config["INVENTORY_PROVIDER_CONFIG_FILENAME"]) as f: + # test the config file can be loaded + config.load(f) + + logging.debug(app.config) + + return app diff --git a/inventory_provider/data_routes.py b/inventory_provider/data_routes.py new file mode 100644 index 0000000000000000000000000000000000000000..01b6f12cbcc84e9011a19851e43304c7ee4ccf5c --- /dev/null +++ b/inventory_provider/data_routes.py @@ -0,0 +1,40 @@ +import functools +import json + +from flask import Blueprint, request, Response +#render_template, url_for + +routes = Blueprint("python-utils-ui-routes", __name__) + +VERSION = { + "api": "0.1", + "module": "0.1" +} + + +def require_accepts_json(f): + """ + used as a route handler decorator to return an error + unless the request allows responses with type "application/json" + :param f: the function to be decorated + :return: the decorated function + """ + @functools.wraps(f) + def decorated_function(*args, **kwargs): + # TODO: use best_match to disallow */* ...? + if not request.accept_mimetypes.accept_json: + return Response( + response="response will be json", + status=406, + mimetype="text/html") + return f(*args, **kwargs) + return decorated_function + + +@routes.route("/version", methods=['GET', 'POST']) +@require_accepts_json +def version(): + return Response( + json.dumps(VERSION), + mimetype="application/json" + ) diff --git a/requirements.txt b/requirements.txt index 57746b4c8a1cafc94f5ccb363f4bd7cb1159b2fd..ea667eb9d92fa5f7855ca9f376c17a27a22e1686 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,6 @@ mysql-connector pysnmp jsonschema paramiko +flask pytest diff --git a/setup.py b/setup.py index c375511aead517d25b466a98df98935d15aaea7f..65e429b28e1424c732fa1a447ed49e7ee09e7cbc 100644 --- a/setup.py +++ b/setup.py @@ -14,6 +14,7 @@ setup( 'mysql-connector', 'pysnmp', 'jsonschema', - 'paramiko' + 'paramiko', + 'flask' ] ) diff --git a/test/test_data_routes.py b/test/test_data_routes.py new file mode 100644 index 0000000000000000000000000000000000000000..617e89e6ff5cb1fd425f04dae0e264b5fa06f46d --- /dev/null +++ b/test/test_data_routes.py @@ -0,0 +1,159 @@ +import json +import logging +import os +import tempfile + +import pytest +import jsonschema + +import inventory_provider + +# logging.basicConfig(level=logging.DEBUG) + +DEFAULT_REQUEST_HEADERS = { + "Content-type": "application/json", + "Accept": ["application/json"] +} + +MODULE_DIR = os.path.realpath(os.path.join( + os.path.dirname(__file__), + "..", + "inventory_provider")) + +OID_LIST_CONF = """ +# +# This file is located in dbupdates/conf and is used by scripts under dbupdates/scripts. +# It holds OID values for retrieving details of a router. +# + +## IPv4 +v4Address=.1.3.6.1.2.1.4.20.1.1 +v4InterfaceOID=.1.3.6.1.2.1.4.20.1.2 +v4InterfaceName=.1.3.6.1.2.1.31.1.1.1.1 +v4Mask=.1.3.6.1.2.1.4.20.1.3 + +## IPv6 +v6AddressAndMask=.1.3.6.1.2.1.55.1.8.1.2 +v6InterfaceName=.1.3.6.1.2.1.55.1.5.1.2 +""" + +ROUTERS_COMMUNITY_CONF = """ +###################################################################################################################################### +## ## +## This is a configuration file that stores router names and the SNMP community name in <router>=<community>,<IP address> format. ## +## ## +###################################################################################################################################### + +mx2.ath.gr.geant.net=0pBiFbD,62.40.114.59 +mx1.tal.ee.geant.net=0pBiFbD,62.40.96.1 +mx2.tal.ee.geant.net=0pBiFbD,62.40.96.2 +mx2.rig.lv.geant.net=0pBiFbD,62.40.96.4 +mx1.kau.lt.geant.net=0pBiFbD,62.40.96.6 +mx2.kau.lt.geant.net=0pBiFbD,62.40.96.5 +mx2.zag.hr.geant.net=0pBiFbD,62.40.96.8 +mx2.lju.si.geant.net=0pBiFbD,62.40.96.10 +mx1.bud.hu.geant.net=0pBiFbD,62.40.97.1 +mx1.pra.cz.geant.net=0pBiFbD,62.40.97.2 +mx2.bra.sk.geant.net=0pBiFbD,62.40.97.4 +mx1.lon.uk.geant.net=0pBiFbD,62.40.97.5 +mx1.vie.at.geant.net=0pBiFbD,62.40.97.7 +mx2.bru.be.geant.net=0pBiFbD,62.40.96.20 +mx1.poz.pl.geant.net=0pBiFbD,62.40.97.10 +mx1.ams.nl.geant.net=0pBiFbD,62.40.97.11 +mx1.fra.de.geant.net=0pBiFbD,62.40.97.12 +mx1.par.fr.geant.net=0pBiFbD,62.40.97.13 +mx1.gen.ch.geant.net=0pBiFbD,62.40.97.14 +mx1.mil2.it.geant.net=0pBiFbD,62.40.97.15 +mx1.lis.pt.geant.net=0pBiFbD,62.40.96.16 +mx2.lis.pt.geant.net=0pBiFbD,62.40.96.17 +mx1.mad.es.geant.net=0pBiFbD,62.40.97.16 +mx1.sof.bg.geant.net=0pBiFbD,62.40.96.21 +mx1.buc.ro.geant.net=0pBiFbD,62.40.96.19 +mx1.ham.de.geant.net=0pBiFbD,62.40.96.26 +mx1.dub.ie.geant.net=0pBiFbD,62.40.96.3 +mx1.dub2.ie.geant.net=0pBiFbD,62.40.96.25 +mx1.mar.fr.geant.net=0pBiFbD,62.40.96.12 +mx1.lon2.uk.geant.net=0pBiFbD,62.40.96.15 +# rt1.clpk.us.geant.net=GEANT_RO,10.200.64.128 +# rt1.denv.us.geant.net=GEANT_RO,10.200.67.128 +mx1.ath2.gr.geant.net=0pBiFbD,62.40.96.39 +# qfx.par.fr.geant.net=0pBiFbD,62.40.117.170 +# qfx.fra.de.geant.net=0pBiFbD,62.40.117.162 +""" + +def data_config_filename(tmp_dir_name): + config = { + "alarms-db": { + "hostname": "xxxxxxx.yyyyy.zzz", + "dbname": "xxxxxx", + "username": "xxxxxx", + "password": "xxxxxxxx" + }, + "oid_list.conf": os.path.join(tmp_dir_name, "oid_list.conf"), + "routers_community.conf": os.path.join(tmp_dir_name, "routers_community.conf"), + "ssh": { + "private-key": "private-key-filename", + "known-hosts": "known-hosts=filename" + } + } + + with open(config["oid_list.conf"], "w") as f: + f.write(OID_LIST_CONF) + + with open(config["routers_community.conf"], "w") as f: + f.write(ROUTERS_COMMUNITY_CONF) + + filename = os.path.join(tmp_dir_name, "config.json") + with open(filename, "w") as f: + f.write(json.dumps(config)) + + return filename + + +@pytest.fixture +def app_config(): + with tempfile.TemporaryDirectory() as tmpdir: + + app_config_filename = os.path.join(tmpdir, "app.config") + with open(app_config_filename, "w") as f: + f.write("%s = '%s'\n" % ( + "INVENTORY_PROVIDER_CONFIG_FILENAME", + data_config_filename(tmpdir))) + + yield app_config_filename + + +@pytest.fixture +def client(app_config): + os.environ["SETTINGS_FILENAME"] = app_config + # with release_webservice.create_app().test_client() as c: + # yield c + with inventory_provider.create_app().test_client() as c: + yield c + + +def test_version_request(client): + version_schema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "api": { + "type": "string", + "pattern": r'\d+\.\d+' + }, + "module": { + "type": "string", + "pattern": r'\d+\.\d+' + } + }, + "required": ["api", "module"], + "additionalProperties": False + } + + rv = client.post( + "data/version", + headers=DEFAULT_REQUEST_HEADERS) + assert rv.status_code == 200 + jsonschema.validate( + json.loads(rv.data.decode("utf-8")), + version_schema)