Alex | 0989ecf | 2022-03-29 13:43:21 -0500 | [diff] [blame] | 1 | # Author: Alex Savatieiev (osavatieiev@mirantis.com; a.savex@gmail.com) |
| 2 | # Copyright 2019-2022 Mirantis, Inc. |
Alex | b78191f | 2021-11-02 16:35:46 -0500 | [diff] [blame] | 3 | from gevent import monkey, pywsgi |
| 4 | monkey.patch_all() |
| 5 | import falcon # noqa E402 |
| 6 | import os # noqa E402 |
| 7 | import json # noqa E402 |
| 8 | |
| 9 | from copy import deepcopy # noqa E402 |
| 10 | from platform import system, release, node # noqa E402 |
| 11 | |
Alex | 2a7657c | 2021-11-10 20:51:34 -0600 | [diff] [blame] | 12 | from cfg_checker.common.log import logger # noqa E402 |
Alex | b78191f | 2021-11-02 16:35:46 -0500 | [diff] [blame] | 13 | from cfg_checker.common.settings import pkg_dir # noqa E402 |
Alex | 2a7657c | 2021-11-10 20:51:34 -0600 | [diff] [blame] | 14 | from cfg_checker.common.exception import CheckerException # noqa E402 |
Alex | b78191f | 2021-11-02 16:35:46 -0500 | [diff] [blame] | 15 | from cfg_checker.helpers.falcon_jinja2 import FalconTemplate # noqa E402 |
| 16 | from .fio_runner import FioProcessShellRun, get_time # noqa E402 |
| 17 | |
| 18 | template = FalconTemplate( |
| 19 | path=os.path.join(pkg_dir, "templates") |
| 20 | ) |
| 21 | |
| 22 | _module_status = { |
| 23 | "status": "unknown", |
| 24 | "healthcheck": {}, |
| 25 | "actions": [], |
| 26 | "options": {}, |
| 27 | "uri": "<agent_uri>/api/<modile_name>", |
| 28 | } |
| 29 | |
| 30 | _action = { |
| 31 | "module": "<name>", |
| 32 | "action": "<action>", |
| 33 | "options": "<options_dict>" |
| 34 | } |
| 35 | |
| 36 | _modules = { |
| 37 | "fio": deepcopy(_module_status) |
| 38 | } |
| 39 | |
| 40 | _status = { |
| 41 | "agent": { |
| 42 | "started": get_time() |
| 43 | }, |
| 44 | "modules": list(_modules.keys()), |
| 45 | "help": { |
| 46 | ".../api": { |
| 47 | "GET": "<this_status>", |
| 48 | "POST": json.dumps(_action) |
| 49 | }, |
| 50 | ".../api/<module_name>": { |
| 51 | "GET": "returns healthcheck and module help" |
| 52 | } |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | # Populate modules |
| 57 | _fio = FioProcessShellRun() |
| 58 | |
| 59 | |
| 60 | def _init_status(mod): |
| 61 | _modules[mod]["uri"] = "<agent_uri>/api/fio" |
| 62 | _modules[mod]["actions"] = list(_fio.actions.keys()) |
| 63 | |
| 64 | |
| 65 | def _update_status(mod): |
| 66 | _modules[mod]["healthcheck"] = _fio.hchk |
| 67 | _modules[mod]["options"] = _fio.get_options() |
| 68 | _modules[mod].update(_fio.status()) |
| 69 | |
| 70 | |
| 71 | class FioStatus: |
| 72 | _name = "fio" |
| 73 | |
| 74 | def on_get(self, req, resp): |
| 75 | # Hacky way to handle empty request |
| 76 | _m = req.get_media(default_when_empty={}) |
| 77 | if "fast" in _m and _m["fast"]: |
| 78 | resp.status = falcon.HTTP_200 |
| 79 | resp.content_type = "application/json" |
| 80 | resp.text = json.dumps(_fio.status()) |
| 81 | else: |
| 82 | _update_status(self._name) |
| 83 | |
| 84 | resp.status = falcon.HTTP_200 |
| 85 | resp.content_type = "application/json" |
| 86 | resp.text = json.dumps(_modules[self._name]) |
| 87 | |
| 88 | |
| 89 | class Api: |
| 90 | def on_get(self, req, resp): |
| 91 | # return api status |
| 92 | resp.status = falcon.HTTP_200 |
| 93 | resp.content_type = "application/json" |
| 94 | resp.text = json.dumps(_status) |
| 95 | |
| 96 | def on_post(self, req, resp): |
Alex | 2a7657c | 2021-11-10 20:51:34 -0600 | [diff] [blame] | 97 | def _resp(resp, code, msg, opt={}): |
Alex | b78191f | 2021-11-02 16:35:46 -0500 | [diff] [blame] | 98 | resp.status = code |
| 99 | resp.content_type = "application/json" |
Alex | 2a7657c | 2021-11-10 20:51:34 -0600 | [diff] [blame] | 100 | resp.text = json.dumps({"error": msg, "options": opt}) |
Alex | b78191f | 2021-11-02 16:35:46 -0500 | [diff] [blame] | 101 | # Handle actions |
Alex | 2a7657c | 2021-11-10 20:51:34 -0600 | [diff] [blame] | 102 | logger.info("Getting media") |
| 103 | try: |
Alex | bfa947c | 2021-11-11 18:14:28 -0600 | [diff] [blame] | 104 | _m = req.stream.read() |
| 105 | _m = json.loads(_m) |
| 106 | except json.JSONDecodeError: |
Alex | 2a7657c | 2021-11-10 20:51:34 -0600 | [diff] [blame] | 107 | _msg = "Incorrect input data" |
| 108 | logger.error(_msg) |
| 109 | _resp(resp, falcon.HTTP_400, _msg) |
| 110 | return |
Alex | b78191f | 2021-11-02 16:35:46 -0500 | [diff] [blame] | 111 | if _m: |
Alex | 2a7657c | 2021-11-10 20:51:34 -0600 | [diff] [blame] | 112 | logger.debug("got media object:\n{}".format(json.dumps(_m))) |
Alex | b78191f | 2021-11-02 16:35:46 -0500 | [diff] [blame] | 113 | # Validate action structure |
| 114 | _module = _m.get('module', "") |
| 115 | _action = _m.get('action', "") |
| 116 | _options = _m.get('options', {}) |
| 117 | |
| 118 | if not _module or _module not in list(_modules.keys()): |
Alex | 2a7657c | 2021-11-10 20:51:34 -0600 | [diff] [blame] | 119 | logger.error("invalid module '{}'".format(_module)) |
Alex | b78191f | 2021-11-02 16:35:46 -0500 | [diff] [blame] | 120 | _resp( |
| 121 | resp, |
| 122 | falcon.HTTP_400, |
| 123 | "Invalid module '{}'".format(_module) |
| 124 | ) |
| 125 | return |
| 126 | elif not _action or _action not in _modules[_module]['actions']: |
Alex | 2a7657c | 2021-11-10 20:51:34 -0600 | [diff] [blame] | 127 | logger.error("invalid action '{}'".format(_action)) |
Alex | b78191f | 2021-11-02 16:35:46 -0500 | [diff] [blame] | 128 | _resp( |
| 129 | resp, |
| 130 | falcon.HTTP_400, |
| 131 | "Invalid action '{}'".format(_action) |
| 132 | ) |
| 133 | return |
| 134 | else: |
| 135 | # Handle command |
| 136 | _a = _fio.actions[_action] |
Alex | 2a7657c | 2021-11-10 20:51:34 -0600 | [diff] [blame] | 137 | try: |
| 138 | if _action == 'get_options': |
| 139 | logger.info("returning options") |
| 140 | resp.status = falcon.HTTP_200 |
| 141 | resp.content_type = "application/json" |
| 142 | resp.text = json.dumps({"options": _a()}) |
| 143 | elif _action == 'get_resultlist': |
| 144 | logger.info("getting results") |
| 145 | resp.status = falcon.HTTP_200 |
| 146 | resp.content_type = "application/json" |
| 147 | resp.text = json.dumps({"resultlist": _a()}) |
| 148 | elif _action == 'get_result': |
| 149 | if 'time' not in _options: |
| 150 | _msg = "No 'time' found for '{}'".format(_action) |
| 151 | logger.error(_msg) |
| 152 | _resp( |
| 153 | resp, |
| 154 | falcon.HTTP_400, |
| 155 | _msg |
| 156 | ) |
| 157 | return |
| 158 | _time = _options['time'] |
| 159 | logger.info("getting results for '{}'".format(_time)) |
| 160 | resp.status = falcon.HTTP_200 |
| 161 | resp.content_type = "application/json" |
Alex | 3034ba5 | 2021-11-13 17:06:45 -0600 | [diff] [blame] | 162 | # TODO: get timeline too? |
Alex | 2a7657c | 2021-11-10 20:51:34 -0600 | [diff] [blame] | 163 | resp.text = json.dumps({_time: _a(_time)}) |
| 164 | elif _action == 'do_singlerun': |
| 165 | logger.info("executing single run") |
| 166 | _a(_options) |
| 167 | resp.status = falcon.HTTP_200 |
| 168 | resp.content_type = "application/json" |
| 169 | resp.text = json.dumps({"ok": True}) |
| 170 | elif _action == 'do_scheduledrun': |
| 171 | logger.info("executing scheduled run") |
| 172 | # prepare scheduled run |
Alex | b78191f | 2021-11-02 16:35:46 -0500 | [diff] [blame] | 173 | |
Alex | 2a7657c | 2021-11-10 20:51:34 -0600 | [diff] [blame] | 174 | # Run it |
| 175 | _a(_options) |
| 176 | resp.status = falcon.HTTP_200 |
| 177 | resp.content_type = "application/json" |
| 178 | resp.text = json.dumps({"ok": True}) |
| 179 | else: |
| 180 | _msg = "Unknown error happened for '{}/{}/{}'".format( |
| 181 | _module, |
| 182 | _action, |
| 183 | _options |
| 184 | ) |
| 185 | logger.error(_msg) |
| 186 | _resp(resp, falcon.HTTP_500, _msg) |
| 187 | except CheckerException as e: |
| 188 | _msg = "Error for '{}/{}':\n{}".format( |
| 189 | _module, |
| 190 | _action, |
| 191 | e |
| 192 | ) |
| 193 | logger.error(_msg) |
| 194 | _resp(resp, falcon.HTTP_500, _msg, opt=_options) |
Alex | b78191f | 2021-11-02 16:35:46 -0500 | [diff] [blame] | 195 | return |
| 196 | else: |
Alex | 2a7657c | 2021-11-10 20:51:34 -0600 | [diff] [blame] | 197 | _msg = "Empty request body" |
| 198 | logger.error(_msg) |
| 199 | _resp(resp, falcon.HTTP_400, _msg) |
Alex | b78191f | 2021-11-02 16:35:46 -0500 | [diff] [blame] | 200 | |
| 201 | |
| 202 | class Index: |
| 203 | @template.render("agent_index_html.j2") |
| 204 | def on_get(self, request, response): |
| 205 | # prepare template context |
| 206 | _context = { |
| 207 | "gen_date": get_time(), |
| 208 | "system": system(), |
| 209 | "release": release(), |
| 210 | "hostname": node() |
| 211 | } |
| 212 | _context.update(_status) |
| 213 | # creating response |
| 214 | response.status = falcon.HTTP_200 |
| 215 | response.content_type = "text/html" |
| 216 | response.context = _context |
| 217 | |
| 218 | |
| 219 | def agent_server(host="0.0.0.0", port=8765): |
| 220 | # init api |
| 221 | api = falcon.API() |
| 222 | # populate pages |
| 223 | api.add_route("/", Index()) |
| 224 | api.add_route("/api/", Api()) |
| 225 | |
| 226 | # Populate modules list |
| 227 | _active_modules = [FioStatus] |
| 228 | # init modules |
| 229 | for mod in _active_modules: |
| 230 | _init_status(mod._name) |
| 231 | _update_status(mod._name) |
| 232 | |
| 233 | api.add_route("/api/"+mod._name, mod()) |
| 234 | |
| 235 | # init and start server |
| 236 | server = pywsgi.WSGIServer((host, port), api) |
| 237 | server.serve_forever() |