blob: ee72a427b52107f49bf7c9e8261d05af584deb1e [file] [log] [blame]
Alex0989ecf2022-03-29 13:43:21 -05001# Author: Alex Savatieiev (osavatieiev@mirantis.com; a.savex@gmail.com)
2# Copyright 2019-2022 Mirantis, Inc.
Alexb78191f2021-11-02 16:35:46 -05003from gevent import monkey, pywsgi
4monkey.patch_all()
5import falcon # noqa E402
6import os # noqa E402
7import json # noqa E402
8
9from copy import deepcopy # noqa E402
10from platform import system, release, node # noqa E402
11
Alex2a7657c2021-11-10 20:51:34 -060012from cfg_checker.common.log import logger # noqa E402
Alexb78191f2021-11-02 16:35:46 -050013from cfg_checker.common.settings import pkg_dir # noqa E402
Alex2a7657c2021-11-10 20:51:34 -060014from cfg_checker.common.exception import CheckerException # noqa E402
Alexb78191f2021-11-02 16:35:46 -050015from cfg_checker.helpers.falcon_jinja2 import FalconTemplate # noqa E402
16from .fio_runner import FioProcessShellRun, get_time # noqa E402
17
18template = 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
60def _init_status(mod):
61 _modules[mod]["uri"] = "<agent_uri>/api/fio"
62 _modules[mod]["actions"] = list(_fio.actions.keys())
63
64
65def _update_status(mod):
66 _modules[mod]["healthcheck"] = _fio.hchk
67 _modules[mod]["options"] = _fio.get_options()
68 _modules[mod].update(_fio.status())
69
70
71class 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
89class 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):
Alex2a7657c2021-11-10 20:51:34 -060097 def _resp(resp, code, msg, opt={}):
Alexb78191f2021-11-02 16:35:46 -050098 resp.status = code
99 resp.content_type = "application/json"
Alex2a7657c2021-11-10 20:51:34 -0600100 resp.text = json.dumps({"error": msg, "options": opt})
Alexb78191f2021-11-02 16:35:46 -0500101 # Handle actions
Alex2a7657c2021-11-10 20:51:34 -0600102 logger.info("Getting media")
103 try:
Alexbfa947c2021-11-11 18:14:28 -0600104 _m = req.stream.read()
105 _m = json.loads(_m)
106 except json.JSONDecodeError:
Alex2a7657c2021-11-10 20:51:34 -0600107 _msg = "Incorrect input data"
108 logger.error(_msg)
109 _resp(resp, falcon.HTTP_400, _msg)
110 return
Alexb78191f2021-11-02 16:35:46 -0500111 if _m:
Alex2a7657c2021-11-10 20:51:34 -0600112 logger.debug("got media object:\n{}".format(json.dumps(_m)))
Alexb78191f2021-11-02 16:35:46 -0500113 # 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()):
Alex2a7657c2021-11-10 20:51:34 -0600119 logger.error("invalid module '{}'".format(_module))
Alexb78191f2021-11-02 16:35:46 -0500120 _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']:
Alex2a7657c2021-11-10 20:51:34 -0600127 logger.error("invalid action '{}'".format(_action))
Alexb78191f2021-11-02 16:35:46 -0500128 _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]
Alex2a7657c2021-11-10 20:51:34 -0600137 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"
Alex3034ba52021-11-13 17:06:45 -0600162 # TODO: get timeline too?
Alex2a7657c2021-11-10 20:51:34 -0600163 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
Alexb78191f2021-11-02 16:35:46 -0500173
Alex2a7657c2021-11-10 20:51:34 -0600174 # 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)
Alexb78191f2021-11-02 16:35:46 -0500195 return
196 else:
Alex2a7657c2021-11-10 20:51:34 -0600197 _msg = "Empty request body"
198 logger.error(_msg)
199 _resp(resp, falcon.HTTP_400, _msg)
Alexb78191f2021-11-02 16:35:46 -0500200
201
202class 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
219def 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()