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