|  | #    Author: Alex Savatieiev (osavatieiev@mirantis.com; a.savex@gmail.com) | 
|  | #    Copyright 2019-2022 Mirantis, Inc. | 
|  | from gevent import monkey, pywsgi | 
|  | monkey.patch_all() | 
|  | import falcon # noqa E402 | 
|  | import os # noqa E402 | 
|  | import json # noqa E402 | 
|  |  | 
|  | from copy import deepcopy # noqa E402 | 
|  | from platform import system, release, node # noqa E402 | 
|  |  | 
|  | from cfg_checker.common.log import logger # noqa E402 | 
|  | from cfg_checker.common.settings import pkg_dir  # noqa E402 | 
|  | from cfg_checker.common.exception import CheckerException  # noqa E402 | 
|  | from cfg_checker.helpers.falcon_jinja2 import FalconTemplate  # noqa E402 | 
|  | from .fio_runner import FioProcessShellRun, get_time # noqa E402 | 
|  |  | 
|  | template = FalconTemplate( | 
|  | path=os.path.join(pkg_dir, "templates") | 
|  | ) | 
|  |  | 
|  | _module_status = { | 
|  | "status": "unknown", | 
|  | "healthcheck": {}, | 
|  | "actions": [], | 
|  | "options": {}, | 
|  | "uri": "<agent_uri>/api/<modile_name>", | 
|  | } | 
|  |  | 
|  | _action = { | 
|  | "module": "<name>", | 
|  | "action": "<action>", | 
|  | "options": "<options_dict>" | 
|  | } | 
|  |  | 
|  | _modules = { | 
|  | "fio": deepcopy(_module_status) | 
|  | } | 
|  |  | 
|  | _status = { | 
|  | "agent": { | 
|  | "started": get_time() | 
|  | }, | 
|  | "modules": list(_modules.keys()), | 
|  | "help": { | 
|  | ".../api": { | 
|  | "GET": "<this_status>", | 
|  | "POST": json.dumps(_action) | 
|  | }, | 
|  | ".../api/<module_name>": { | 
|  | "GET": "returns healthcheck and module help" | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | # Populate modules | 
|  | _fio = FioProcessShellRun() | 
|  |  | 
|  |  | 
|  | def _init_status(mod): | 
|  | _modules[mod]["uri"] = "<agent_uri>/api/fio" | 
|  | _modules[mod]["actions"] = list(_fio.actions.keys()) | 
|  |  | 
|  |  | 
|  | def _update_status(mod): | 
|  | _modules[mod]["healthcheck"] = _fio.hchk | 
|  | _modules[mod]["options"] = _fio.get_options() | 
|  | _modules[mod].update(_fio.status()) | 
|  |  | 
|  |  | 
|  | class FioStatus: | 
|  | _name = "fio" | 
|  |  | 
|  | def on_get(self, req, resp): | 
|  | # Hacky way to handle empty request | 
|  | _m = req.get_media(default_when_empty={}) | 
|  | if "fast" in _m and _m["fast"]: | 
|  | resp.status = falcon.HTTP_200 | 
|  | resp.content_type = "application/json" | 
|  | resp.text = json.dumps(_fio.status()) | 
|  | else: | 
|  | _update_status(self._name) | 
|  |  | 
|  | resp.status = falcon.HTTP_200 | 
|  | resp.content_type = "application/json" | 
|  | resp.text = json.dumps(_modules[self._name]) | 
|  |  | 
|  |  | 
|  | class Api: | 
|  | def on_get(self, req, resp): | 
|  | # return api status | 
|  | resp.status = falcon.HTTP_200 | 
|  | resp.content_type = "application/json" | 
|  | resp.text = json.dumps(_status) | 
|  |  | 
|  | def on_post(self, req, resp): | 
|  | def _resp(resp, code, msg, opt={}): | 
|  | resp.status = code | 
|  | resp.content_type = "application/json" | 
|  | resp.text = json.dumps({"error": msg, "options": opt}) | 
|  | # Handle actions | 
|  | logger.info("Getting media") | 
|  | try: | 
|  | _m = req.stream.read() | 
|  | _m = json.loads(_m) | 
|  | except json.JSONDecodeError: | 
|  | _msg = "Incorrect input data" | 
|  | logger.error(_msg) | 
|  | _resp(resp, falcon.HTTP_400, _msg) | 
|  | return | 
|  | if _m: | 
|  | logger.debug("got media object:\n{}".format(json.dumps(_m))) | 
|  | # Validate action structure | 
|  | _module = _m.get('module', "") | 
|  | _action = _m.get('action', "") | 
|  | _options = _m.get('options', {}) | 
|  |  | 
|  | if not _module or _module not in list(_modules.keys()): | 
|  | logger.error("invalid module '{}'".format(_module)) | 
|  | _resp( | 
|  | resp, | 
|  | falcon.HTTP_400, | 
|  | "Invalid module '{}'".format(_module) | 
|  | ) | 
|  | return | 
|  | elif not _action or _action not in _modules[_module]['actions']: | 
|  | logger.error("invalid action '{}'".format(_action)) | 
|  | _resp( | 
|  | resp, | 
|  | falcon.HTTP_400, | 
|  | "Invalid action '{}'".format(_action) | 
|  | ) | 
|  | return | 
|  | else: | 
|  | # Handle command | 
|  | _a = _fio.actions[_action] | 
|  | try: | 
|  | if _action == 'get_options': | 
|  | logger.info("returning options") | 
|  | resp.status = falcon.HTTP_200 | 
|  | resp.content_type = "application/json" | 
|  | resp.text = json.dumps({"options": _a()}) | 
|  | elif _action == 'get_resultlist': | 
|  | logger.info("getting results") | 
|  | resp.status = falcon.HTTP_200 | 
|  | resp.content_type = "application/json" | 
|  | resp.text = json.dumps({"resultlist": _a()}) | 
|  | elif _action == 'get_result': | 
|  | if 'time' not in _options: | 
|  | _msg = "No 'time' found for '{}'".format(_action) | 
|  | logger.error(_msg) | 
|  | _resp( | 
|  | resp, | 
|  | falcon.HTTP_400, | 
|  | _msg | 
|  | ) | 
|  | return | 
|  | _time = _options['time'] | 
|  | logger.info("getting results for '{}'".format(_time)) | 
|  | resp.status = falcon.HTTP_200 | 
|  | resp.content_type = "application/json" | 
|  | # TODO: get timeline too? | 
|  | resp.text = json.dumps({_time: _a(_time)}) | 
|  | elif _action == 'do_singlerun': | 
|  | logger.info("executing single run") | 
|  | _a(_options) | 
|  | resp.status = falcon.HTTP_200 | 
|  | resp.content_type = "application/json" | 
|  | resp.text = json.dumps({"ok": True}) | 
|  | elif _action == 'do_scheduledrun': | 
|  | logger.info("executing scheduled run") | 
|  | # prepare scheduled run | 
|  |  | 
|  | # Run it | 
|  | _a(_options) | 
|  | resp.status = falcon.HTTP_200 | 
|  | resp.content_type = "application/json" | 
|  | resp.text = json.dumps({"ok": True}) | 
|  | else: | 
|  | _msg = "Unknown error happened for '{}/{}/{}'".format( | 
|  | _module, | 
|  | _action, | 
|  | _options | 
|  | ) | 
|  | logger.error(_msg) | 
|  | _resp(resp, falcon.HTTP_500, _msg) | 
|  | except CheckerException as e: | 
|  | _msg = "Error for '{}/{}':\n{}".format( | 
|  | _module, | 
|  | _action, | 
|  | e | 
|  | ) | 
|  | logger.error(_msg) | 
|  | _resp(resp, falcon.HTTP_500, _msg, opt=_options) | 
|  | return | 
|  | else: | 
|  | _msg = "Empty request body" | 
|  | logger.error(_msg) | 
|  | _resp(resp, falcon.HTTP_400, _msg) | 
|  |  | 
|  |  | 
|  | class Index: | 
|  | @template.render("agent_index_html.j2") | 
|  | def on_get(self, request, response): | 
|  | # prepare template context | 
|  | _context = { | 
|  | "gen_date": get_time(), | 
|  | "system": system(), | 
|  | "release": release(), | 
|  | "hostname": node() | 
|  | } | 
|  | _context.update(_status) | 
|  | # creating response | 
|  | response.status = falcon.HTTP_200 | 
|  | response.content_type = "text/html" | 
|  | response.context = _context | 
|  |  | 
|  |  | 
|  | def agent_server(host="0.0.0.0", port=8765): | 
|  | # init api | 
|  | api = falcon.API() | 
|  | # populate pages | 
|  | api.add_route("/", Index()) | 
|  | api.add_route("/api/", Api()) | 
|  |  | 
|  | # Populate modules list | 
|  | _active_modules = [FioStatus] | 
|  | # init modules | 
|  | for mod in _active_modules: | 
|  | _init_status(mod._name) | 
|  | _update_status(mod._name) | 
|  |  | 
|  | api.add_route("/api/"+mod._name, mod()) | 
|  |  | 
|  | # init and start server | 
|  | server = pywsgi.WSGIServer((host, port), api) | 
|  | server.serve_forever() |