Skip to content

Commit 5b3adff

Browse files
committed
bugy#83: added simple admin panel with the possibility to view executions history and logs
1 parent 5b260c2 commit 5b3adff

31 files changed

Lines changed: 1798 additions & 164 deletions

src/alerts/alerts_service.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import logging
2+
import threading
3+
4+
LOGGER = logging.getLogger('script_server.alerts_service')
5+
6+
7+
class AlertsService:
8+
def __init__(self, alerts_config):
9+
if alerts_config:
10+
self._destinations = alerts_config.get_destinations()
11+
else:
12+
self._destinations = []
13+
14+
self._running_threads = []
15+
16+
def send_alert(self, title, body, logs=None):
17+
if not self._destinations:
18+
return
19+
20+
def _send():
21+
for destination in self._destinations:
22+
try:
23+
destination.send(title, body, logs)
24+
except:
25+
LOGGER.exception("Couldn't send alert to " + str(destination))
26+
27+
thread = threading.Thread(target=_send, name='AlertThread-' + title)
28+
thread.start()
29+
30+
@staticmethod
31+
def _wait():
32+
for thread in threading.enumerate():
33+
if thread.name.startswith('AlertThread-'):
34+
thread.join()

src/auth/authorization.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,31 @@
1-
class ListBasedAuthorizer:
1+
ANY_USER = 'ANY_USER'
2+
3+
4+
class Authorizer:
5+
def __init__(self, app_allowed_users, admin_users):
6+
if ANY_USER in app_allowed_users:
7+
self._app_auth_check = AnyUserAuthorizationCheck()
8+
else:
9+
self._app_auth_check = ListBasedAuthorizationCheck(app_allowed_users)
10+
11+
self._admin_users = admin_users
12+
13+
def is_allowed_in_app(self, username):
14+
return self._app_auth_check.is_allowed(username)
15+
16+
def is_admin(self, username):
17+
return username in self._admin_users
18+
19+
20+
class ListBasedAuthorizationCheck:
221
def __init__(self, allowed_users) -> None:
322
self.allowed_users = allowed_users
423

524
def is_allowed(self, username):
625
return username in self.allowed_users
726

827

9-
class AnyUserAuthorizer:
28+
class AnyUserAuthorizationCheck:
1029
@staticmethod
1130
def is_allowed(username):
1231
return True

src/auth/tornado_auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def is_authorized(self, request_handler):
3131
return True
3232

3333
username = self._get_current_user(request_handler)
34-
return self.authorizer.is_allowed(username)
34+
return self.authorizer.is_allowed_in_app(username)
3535

3636
@staticmethod
3737
def _get_current_user(request_handler):
@@ -81,7 +81,7 @@ def authenticate(self, request_handler):
8181

8282
LOGGER.info('Authenticated user ' + username)
8383

84-
if not self.authorizer.is_allowed(username):
84+
if not self.authorizer.is_allowed_in_app(username):
8585
LOGGER.info('User ' + username + ' have no access')
8686
respond_error(request_handler, 403, 'Access is prohibited. Please contact system administrator')
8787
return

src/execution/execution_service.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import logging
2+
import time
3+
import uuid
4+
5+
from alerts.alerts_service import AlertsService
6+
from execution.executor import ScriptExecutor
7+
from execution.logging import ExecutionLoggingService
8+
from react.observable import Observable
9+
from utils.date_utils import get_current_millis
10+
11+
LOGGER = logging.getLogger('script_server.execution_service')
12+
13+
14+
class ExecutionService:
15+
def __init__(self, execution_logging_service, alerts_service, id_generator):
16+
self._execution_logging_service = execution_logging_service # type: ExecutionLoggingService
17+
self._alerts_service = alerts_service # type: AlertsService
18+
self._id_generator = id_generator
19+
20+
self._running_scripts = {}
21+
22+
def get_active_executor(self, execution_id):
23+
return self._running_scripts.get(execution_id)
24+
25+
def start_script(self, config, values, audit_name):
26+
script_name = config.name
27+
28+
executor = ScriptExecutor(config, values, audit_name)
29+
execution_id = self._id_generator.next_id()
30+
31+
audit_command = executor.get_secure_command()
32+
LOGGER.info('Calling script #%s: %s', execution_id, audit_command)
33+
34+
executor.start()
35+
self._running_scripts[execution_id] = executor
36+
37+
secure_output_stream = executor.get_secure_output_stream()
38+
39+
self._execution_logging_service.start_logging(
40+
execution_id, audit_name, script_name, audit_command, secure_output_stream, self)
41+
42+
self.subscribe_fail_alerter(
43+
script_name,
44+
self._alerts_service,
45+
audit_name,
46+
executor,
47+
secure_output_stream)
48+
49+
return execution_id
50+
51+
def stop_script(self, execution_id):
52+
if execution_id in self._running_scripts:
53+
self._running_scripts[execution_id].stop()
54+
55+
def get_exit_code(self, execution_id):
56+
executor = self._running_scripts.get(execution_id) # type: ScriptExecutor
57+
if executor is None:
58+
return None
59+
60+
return executor.get_return_code()
61+
62+
def is_running(self, execution_id):
63+
executor = self._running_scripts.get(execution_id) # type: ScriptExecutor
64+
if executor is None:
65+
return False
66+
67+
return not executor.is_finished()
68+
69+
@staticmethod
70+
def subscribe_fail_alerter(
71+
script_name,
72+
alerts_service,
73+
audit_name,
74+
executor: ScriptExecutor,
75+
output_stream: Observable):
76+
77+
class Alerter(object):
78+
def finished(self):
79+
return_code = executor.get_return_code()
80+
81+
if return_code != 0:
82+
script = str(script_name)
83+
84+
title = script + ' FAILED'
85+
body = 'The script "' + script + '", started by ' + audit_name + \
86+
' exited with return code ' + str(return_code) + '.' + \
87+
' Usually this means an error, occurred during the execution.' + \
88+
' Please check the corresponding logs'
89+
90+
output_stream.wait_close()
91+
script_output = ''.join(output_stream.get_old_data())
92+
93+
alerts_service.send_alert(title, body, script_output)
94+
95+
executor.add_finish_listener(Alerter())

src/execution/executor.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def __init__(self, config, parameter_values, audit_name):
1919
self.parameter_values = parameter_values
2020
self.audit_name = audit_name
2121

22-
self.working_directory = self.get_working_directory()
22+
self.working_directory = self._get_working_directory()
2323
self.script_base_command = process_utils.split_command(
2424
self.config.get_script_command(),
2525
self.working_directory)
@@ -29,7 +29,7 @@ def __init__(self, config, parameter_values, audit_name):
2929
self.output_stream = None
3030
self.secure_output_stream = None
3131

32-
def get_working_directory(self):
32+
def _get_working_directory(self):
3333
working_directory = self.config.get_working_directory()
3434
if working_directory is not None:
3535
working_directory = file_utils.normalize_path(working_directory)
@@ -70,8 +70,6 @@ def start(self, process_constructor=None):
7070
else:
7171
self.secure_output_stream = self.output_stream
7272

73-
return process_wrapper.get_process_id()
74-
7573
def __init_secure_replacements(self):
7674
word_replacements = {}
7775
for parameter in self.config.parameters:
@@ -120,6 +118,9 @@ def get_unsecure_output_stream(self):
120118
def get_return_code(self):
121119
return self.process_wrapper.get_return_code()
122120

121+
def is_finished(self):
122+
return self.process_wrapper.is_finished()
123+
123124
def add_finish_listener(self, listener):
124125
self.process_wrapper.add_finish_listener(listener)
125126

src/execution/id_generator.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import threading
2+
3+
4+
class IdGenerator:
5+
def __init__(self, existing_ids):
6+
self._next_id = self._calc_next_id(existing_ids)
7+
self.lock = threading.Lock()
8+
9+
@staticmethod
10+
def _calc_next_id(existing_ids):
11+
max_id = 0
12+
for existing_id in existing_ids:
13+
try:
14+
numeric_id = int(existing_id)
15+
if numeric_id > max_id:
16+
max_id = numeric_id
17+
except ValueError:
18+
continue
19+
return max_id + 1
20+
21+
def next_id(self):
22+
with self.lock:
23+
id = self._next_id
24+
self._next_id += 1
25+
26+
return str(id)

0 commit comments

Comments
 (0)