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()