mcp-agent mode for mcp-checker with web-info and REST API
New:
- agent index page serving on 0.0.0.0:8765
- REST API with modular approach to modules
- 'fio' module working via thread-safe Thread able to return
real-time info on its status
- 'fio' module scheduled run option
- ability to preserve multiple testrun results while active
- dockerfile for agent image
Fixed:
- Network report fixes to work on Kube envs
- Fixed function for running commands inside daemonset pods
Related-PROD: PROD-36669
Change-Id: I57e73001247af9187680bfc5744590eef219d93c
diff --git a/cfg_checker/agent/webserver.py b/cfg_checker/agent/webserver.py
new file mode 100644
index 0000000..8b8466c
--- /dev/null
+++ b/cfg_checker/agent/webserver.py
@@ -0,0 +1,206 @@
+# author: Alex Savatieiev (osavatieiev@mirantis.com)
+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.settings import pkg_dir # 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):
+ resp.status = code
+ resp.content_type = "application/json"
+ resp.text = json.dumps({"error": msg})
+ # Handle actions
+ _m = req.get_media(default_when_empty={})
+ if _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()):
+ _resp(
+ resp,
+ falcon.HTTP_400,
+ "Invalid module '{}'".format(_module)
+ )
+ return
+ elif not _action or _action not in _modules[_module]['actions']:
+ _resp(
+ resp,
+ falcon.HTTP_400,
+ "Invalid action '{}'".format(_action)
+ )
+ return
+ else:
+ # Handle command
+ _a = _fio.actions[_action]
+ if _action == 'get_options':
+ resp.status = falcon.HTTP_200
+ resp.content_type = "application/json"
+ resp.text = json.dumps({"options": _a()})
+ elif _action == 'get_resultlist':
+ 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:
+ _resp(
+ resp,
+ falcon.HTTP_400,
+ "No 'time' found for '{}'".format(_action)
+ )
+ return
+ _time = _options['time']
+ resp.status = falcon.HTTP_200
+ resp.content_type = "application/json"
+ resp.text = json.dumps({_time: _a(_time)})
+ elif _action == 'do_singlerun':
+ _a(_options)
+ resp.status = falcon.HTTP_200
+ resp.content_type = "application/json"
+ resp.text = json.dumps({"ok": True})
+ elif _action == 'do_scheduledrun':
+ # prepare scheduled run
+
+ # Run it
+ _a(_options)
+ resp.status = falcon.HTTP_200
+ resp.content_type = "application/json"
+ resp.text = json.dumps({"ok": True})
+ else:
+ _resp(
+ resp,
+ falcon.HTTP_500,
+ "Unknown error happened for '{}/{}/{}'".format(
+ _module,
+ _action,
+ _options
+ )
+ )
+ return
+ else:
+ _resp(falcon.HTTP_400, "Empty request body")
+
+
+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()