Skip to content
Snippets Groups Projects
Commit 756470ac authored by Erik Reid's avatar Erik Reid
Browse files

init /api/version, /api/update flask api

parent 20acf019
No related branches found
No related tags found
No related merge requests found
# """ """
# automatically invoked app factory automatically invoked app factory
# """ """
# import logging import logging
# import os import os
#
# from flask import Flask from flask import Flask
#
# import dashboard_v3_webapp.config as config from brian_polling_manager import configuration
#
# CONFIG_KEY = 'CONFIG_PARAMS' CONFIG_KEY = 'CONFIG_PARAMS'
# SESSION_SECRET = 'super-secret' SESSION_SECRET = 'super-secret'
#
#
# def create_app(): def create_app():
# """ """
# overrides default settings with those found overrides default settings with those found
# in the file read from env var CONFIG_FILENAME in the file read from env var CONFIG_FILENAME
#
# :return: a new flask app instance :return: a new flask app instance
# """ """
#
# app_config = config.DEFAULT_PARAMS app = Flask(__name__)
# if 'CONFIG_FILENAME' in os.environ: app.secret_key = SESSION_SECRET
# with open(os.environ['CONFIG_FILENAME']) as f:
# app_config.update(config.load(f)) if 'CONFIG_FILENAME' in os.environ:
# with open(os.environ['CONFIG_FILENAME']) as f:
# app = Flask(__name__) app.config[CONFIG_KEY] = configuration.load_config(f)
# app.secret_key = SESSION_SECRET else:
# app.config[CONFIG_KEY] = app_config # this inits with the defaults
# app.config[CONFIG_KEY] = configuration.load_config()
# from dashboard_v3_webapp import api
# app.register_blueprint(api.routes, url_prefix='/api') from brian_polling_manager import api
# app.register_blueprint(api.routes, url_prefix='/api')
# logging.info('Flask app initialized')
# # environment.setup_logging() logging.info('Flask app initialized')
# return app return app
"""
These endpoints are used to update BRIAN snmp polling checks to Sensu
.. contents:: :local:
/api/version
---------------------
.. autofunction:: brian_polling_manager.api.version
/api/update
-----------------------------
.. autofunction:: brian_polling_manager.api.update
"""
import functools
import logging
import pkg_resources
from flask import Blueprint, current_app, request, Response, jsonify
from brian_polling_manager import CONFIG_KEY
from brian_polling_manager.main import refresh
routes = Blueprint("api-routes", __name__)
logger = logging.getLogger(__name__)
API_VERSION = '0.1'
VERSION_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'definitions': {
'latch': {
'type': 'object',
'properties': {
'current': {'type': 'integer'},
'next': {'type': 'integer'},
'this': {'type': 'integer'},
'failure': {'type': 'boolean'},
'pending': {'type': 'boolean'},
'timestamp': {'type': 'number'}
},
'required': ['current', 'next', 'this', 'pending', 'failure'],
'additionalProperties': False
}
},
'type': 'object',
'properties': {
'api': {
'type': 'string',
'pattern': r'\d+\.\d+'
},
'module': {
'type': 'string',
'pattern': r'\d+\.\d+'
},
'latch': {'$ref': '#/definitions/latch'}
},
'required': ['api', 'module'],
'additionalProperties': False
}
class APIUpstreamError(Exception):
def __init__(self, message, status_code=504):
super().__init__()
self.message = message
self.status_code = status_code
@routes.errorhandler(APIUpstreamError)
def handle_request_error(error):
logger.error(f'api error: {error.message}')
return Response(
response=error.message,
status=error.status_code,
mimetype='text/html')
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.after_request
def after_request(response):
"""
Generic function to do additional logging of requests & responses.
:param response:
:return:
"""
if response.status_code != 200:
try:
data = response.data.decode('utf-8')
except Exception:
# never expected to happen, but we don't want any failures here
logging.exception('INTERNAL DECODING ERROR')
data = 'decoding error (see logs)'
logger.warning(
f'[{response.status_code}] {request.method} {request.path} {data}')
else:
logger.info(
f'[{response.status_code}] {request.method} {request.path}')
return response
@routes.route('/version', methods=['GET', 'POST'])
@require_accepts_json
def version():
"""
Returns a json object with information about the module version.
The response will be formatted according to the following schema:
.. asjson:: brian_polling_manager.api.VERSION_SCHEMA
:return:
"""
version_params = {
'api': API_VERSION,
'module':
pkg_resources.get_distribution('brian_polling_manager').version
}
return jsonify(version_params)
@routes.route('/update', methods=['GET', 'POST'])
@require_accepts_json
def update():
"""
Update checks based on current inventory provider state.
The response will be formatted according to the following schema:
.. asjson:: brian_polling_manager.main.REFRESH_RESULT_SCHEMA
:return:
"""
response = refresh(config=current_app.config[CONFIG_KEY])
return jsonify(response)
...@@ -204,7 +204,7 @@ def _setup_logging(filename=None): ...@@ -204,7 +204,7 @@ def _setup_logging(filename=None):
# logging.config.dictConfig(json.loads(f.read())) # logging.config.dictConfig(json.loads(f.read()))
def load_config(file): def load_config(file=None):
""" """
loads, validates and returns configuration parameters loads, validates and returns configuration parameters
...@@ -213,7 +213,7 @@ def load_config(file): ...@@ -213,7 +213,7 @@ def load_config(file):
:raises: json.JSONDecodeError, jsonschema.ValidationError, :raises: json.JSONDecodeError, jsonschema.ValidationError,
OSError, AttributeError, ValueError, TypeError, ImportError OSError, AttributeError, ValueError, TypeError, ImportError
""" """
if file is None: if not file:
config = _DEFAULT_CONFIG config = _DEFAULT_CONFIG
else: else:
config = json.loads(file.read()) config = json.loads(file.read())
......
...@@ -97,6 +97,7 @@ def refresh(sensu_params, state): ...@@ -97,6 +97,7 @@ def refresh(sensu_params, state):
for name in extra_checks: for name in extra_checks:
sensu.delete_check(sensu_params, name) sensu.delete_check(sensu_params, name)
# cf. main.REFRESH_RESULT_SCHEMA
return { return {
'checks': len(ifc_checks), 'checks': len(ifc_checks),
'input': interfaces, 'input': interfaces,
......
...@@ -33,14 +33,43 @@ from brian_polling_manager import inventory, interfaces, configuration ...@@ -33,14 +33,43 @@ from brian_polling_manager import inventory, interfaces, configuration
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
REFRESH_RESULT_SCHEMA = {
'$schema': 'http://json-schema.org/draft-07/schema#',
'definitions': {
'refresh-result': {
'type': 'object',
'properties': {
'checks': {'type': 'integer'},
'input': {'type': 'integer'},
'created': {'type': 'integer'},
'updated': {'type': 'integer'},
'deleted': {'type': 'integer'}
},
'required': ['checks', 'input', 'created', 'updated', 'deleted'],
'additionalProperties': False
}
},
'type': 'object',
'properties': {
'interfaces': {'$ref': '#/definitions/refresh-result'}
},
'required': ['interfaces'],
'additionalProperties': False
}
def refresh(config, force=False): def refresh(config, force=False):
""" """
reload inventory data & update sensu checks reload inventory data & update sensu checks
The output will be a dict formatted according to the following schema:
.. asjson::
brian_polling_manager.main.REFRESH_RESULT_SCHEMA
:param config: a dict returned by configuration.load_config :param config: a dict returned by configuration.load_config
:param force: if True, reload inventory data even if timestamp is same :param force: if True, reload inventory data even if timestamp is same
:return: :return: a dict, formatted as above
""" """
state = configuration.State(config['statedir']) state = configuration.State(config['statedir'])
last = inventory.last_update_timestamp(config['inventory']) last = inventory.last_update_timestamp(config['inventory'])
...@@ -48,7 +77,9 @@ def refresh(config, force=False): ...@@ -48,7 +77,9 @@ def refresh(config, force=False):
state.last = last state.last = last
state.interfaces = inventory.load_interfaces(config['inventory']) state.interfaces = inventory.load_interfaces(config['inventory'])
result = interfaces.refresh(config['sensu'], state) result = {
'interfaces': interfaces.refresh(config['sensu'], state)
}
statsd_config = config.get('statsd', None) statsd_config = config.get('statsd', None)
if statsd_config: if statsd_config:
...@@ -56,13 +87,14 @@ def refresh(config, force=False): ...@@ -56,13 +87,14 @@ def refresh(config, force=False):
host=statsd_config['hostname'], host=statsd_config['hostname'],
port=statsd_config['port'], port=statsd_config['port'],
prefix=f'{statsd_config["prefix"]}_interfaces') prefix=f'{statsd_config["prefix"]}_interfaces')
statsd.gauge('checks', result['checks']) statsd.gauge('checks', result['interfaces']['checks'])
statsd.gauge('input', result['input']) statsd.gauge('input', result['interfaces']['input'])
statsd.gauge('created', result['created']) statsd.gauge('created', result['interfaces']['created'])
statsd.gauge('updated', result['updated']) statsd.gauge('updated', result['interfaces']['updated'])
statsd.gauge('deleted', result['deleted']) statsd.gauge('deleted', result['interfaces']['deleted'])
return statsd_config jsonschema.validate(result, REFRESH_RESULT_SCHEMA) # sanity
return result
def _validate_config(_ctx, _param, file): def _validate_config(_ctx, _param, file):
""" """
...@@ -95,7 +127,7 @@ def cli(config, force): ...@@ -95,7 +127,7 @@ def cli(config, force):
""" """
Update BRIAN snmp checks based on Inventory Provider data. Update BRIAN snmp checks based on Inventory Provider data.
""" """
refresh(config, force) logger.info(json.dumps(refresh(config, force)))
if __name__ == '__main__': if __name__ == '__main__':
......
HTTP API
===================================================
.. automodule:: brian_polling_manager.api
...@@ -17,3 +17,4 @@ Sensu checks for polling the data required by BRIAN. ...@@ -17,3 +17,4 @@ Sensu checks for polling the data required by BRIAN.
:caption: Contents: :caption: Contents:
main main
api
...@@ -2,6 +2,7 @@ click ...@@ -2,6 +2,7 @@ click
jsonschema jsonschema
requests requests
statsd statsd
flask
pytest pytest
responses responses
......
...@@ -12,11 +12,12 @@ setup( ...@@ -12,11 +12,12 @@ setup(
'click', 'click',
'requests', 'requests',
'jsonschema', 'jsonschema',
'statsd' 'statsd',
'flask'
], ],
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
'brian-polling-manager=brian_polling_manager.cli:main' 'brian-polling-manager=brian_polling_manager.main:cli'
] ]
}, },
include_package_data=True, include_package_data=True,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment