meerschaum 2.2.6__py3-none-any.whl → 2.3.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meerschaum/__init__.py +4 -1
- meerschaum/__main__.py +10 -5
- meerschaum/_internal/arguments/_parser.py +44 -15
- meerschaum/_internal/entry.py +35 -14
- meerschaum/_internal/shell/Shell.py +155 -53
- meerschaum/_internal/shell/updates.py +175 -0
- meerschaum/actions/api.py +12 -12
- meerschaum/actions/attach.py +95 -0
- meerschaum/actions/delete.py +35 -26
- meerschaum/actions/register.py +19 -5
- meerschaum/actions/show.py +119 -148
- meerschaum/actions/start.py +85 -75
- meerschaum/actions/stop.py +68 -39
- meerschaum/actions/sync.py +3 -3
- meerschaum/actions/upgrade.py +28 -36
- meerschaum/api/_events.py +18 -1
- meerschaum/api/_oauth2.py +2 -0
- meerschaum/api/_websockets.py +2 -2
- meerschaum/api/dash/jobs.py +5 -2
- meerschaum/api/routes/__init__.py +1 -0
- meerschaum/api/routes/_actions.py +122 -44
- meerschaum/api/routes/_jobs.py +340 -0
- meerschaum/api/routes/_pipes.py +25 -25
- meerschaum/config/_default.py +1 -0
- meerschaum/config/_formatting.py +1 -0
- meerschaum/config/_paths.py +5 -0
- meerschaum/config/_shell.py +84 -67
- meerschaum/config/_version.py +1 -1
- meerschaum/config/static/__init__.py +9 -0
- meerschaum/connectors/__init__.py +9 -11
- meerschaum/connectors/api/APIConnector.py +18 -1
- meerschaum/connectors/api/_actions.py +60 -71
- meerschaum/connectors/api/_jobs.py +260 -0
- meerschaum/connectors/api/_misc.py +1 -1
- meerschaum/connectors/api/_request.py +13 -9
- meerschaum/connectors/parse.py +23 -7
- meerschaum/core/Pipe/_sync.py +3 -0
- meerschaum/plugins/__init__.py +89 -5
- meerschaum/utils/daemon/Daemon.py +333 -149
- meerschaum/utils/daemon/FileDescriptorInterceptor.py +19 -10
- meerschaum/utils/daemon/RotatingFile.py +18 -7
- meerschaum/utils/daemon/StdinFile.py +110 -0
- meerschaum/utils/daemon/__init__.py +40 -27
- meerschaum/utils/formatting/__init__.py +83 -37
- meerschaum/utils/formatting/_jobs.py +118 -51
- meerschaum/utils/formatting/_shell.py +6 -0
- meerschaum/utils/jobs/_Job.py +684 -0
- meerschaum/utils/jobs/__init__.py +245 -0
- meerschaum/utils/misc.py +18 -17
- meerschaum/utils/packages/__init__.py +21 -15
- meerschaum/utils/packages/_packages.py +2 -2
- meerschaum/utils/prompt.py +20 -7
- meerschaum/utils/schedule.py +21 -15
- {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/METADATA +9 -9
- {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/RECORD +61 -54
- {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/WHEEL +1 -1
- {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/LICENSE +0 -0
- {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/NOTICE +0 -0
- {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/top_level.txt +0 -0
- {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/zip-safe +0 -0
meerschaum/actions/stop.py
CHANGED
@@ -50,11 +50,13 @@ def _complete_stop(
|
|
50
50
|
|
51
51
|
def _stop_jobs(
|
52
52
|
action: Optional[List[str]] = None,
|
53
|
+
executor_keys: Optional[str] = None,
|
53
54
|
timeout_seconds: Optional[int] = None,
|
54
55
|
noask: bool = False,
|
55
56
|
force: bool = False,
|
56
57
|
yes: bool = False,
|
57
58
|
nopretty: bool = False,
|
59
|
+
debug: bool = False,
|
58
60
|
**kw
|
59
61
|
) -> SuccessTuple:
|
60
62
|
"""
|
@@ -62,66 +64,93 @@ def _stop_jobs(
|
|
62
64
|
|
63
65
|
To see running processes, run `show jobs`.
|
64
66
|
"""
|
67
|
+
from meerschaum.utils.jobs import (
|
68
|
+
get_filtered_jobs,
|
69
|
+
get_running_jobs,
|
70
|
+
get_paused_jobs,
|
71
|
+
get_stopped_jobs,
|
72
|
+
get_restart_jobs,
|
73
|
+
)
|
65
74
|
from meerschaum.utils.formatting._jobs import pprint_jobs
|
66
75
|
from meerschaum.utils.daemon import (
|
67
76
|
get_filtered_daemons, get_running_daemons, get_stopped_daemons, get_paused_daemons,
|
68
77
|
)
|
69
78
|
from meerschaum.utils.warnings import warn
|
70
79
|
from meerschaum.utils.prompt import yes_no
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
80
|
+
|
81
|
+
jobs = get_filtered_jobs(executor_keys, action, warn=(not nopretty))
|
82
|
+
running_jobs = get_running_jobs(executor_keys, jobs)
|
83
|
+
paused_jobs = get_paused_jobs(executor_keys, jobs)
|
84
|
+
restart_jobs = get_restart_jobs(executor_keys, jobs)
|
85
|
+
stopped_jobs = {
|
86
|
+
name: job
|
87
|
+
for name, job in get_stopped_jobs(executor_keys, jobs).items()
|
88
|
+
if name not in restart_jobs
|
89
|
+
}
|
90
|
+
|
91
|
+
jobs_to_stop = {
|
92
|
+
**running_jobs,
|
93
|
+
**paused_jobs,
|
94
|
+
**restart_jobs,
|
95
|
+
}
|
96
|
+
|
97
|
+
if action and stopped_jobs and not nopretty:
|
76
98
|
warn(
|
77
|
-
|
78
|
-
|
79
|
-
|
99
|
+
"Skipping stopped job"
|
100
|
+
+ ("s" if len(stopped_jobs) != 1 else '')
|
101
|
+
+ " '"
|
102
|
+
+ ("', '".join(name for name in stopped_jobs)) + "'.",
|
103
|
+
stack=False,
|
80
104
|
)
|
81
105
|
|
82
|
-
|
83
|
-
|
84
|
-
return False, "No running or paused jobs to stop."
|
106
|
+
if not jobs_to_stop:
|
107
|
+
return False, "No running, paused or restarting jobs to stop."
|
85
108
|
|
86
109
|
if not action:
|
87
110
|
if not force:
|
88
|
-
pprint_jobs(
|
111
|
+
pprint_jobs(jobs_to_stop)
|
89
112
|
if not yes_no(
|
90
113
|
"Stop the above jobs?",
|
91
114
|
noask=noask, yes=yes, default='n'
|
92
115
|
):
|
93
116
|
return False, "No jobs were stopped."
|
94
117
|
|
95
|
-
|
96
|
-
for
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
warn(
|
103
|
-
f"Failed to gracefully quit job '{d.daemon_id}', attempting to terminate:\n "
|
104
|
-
+ f"{quit_msg}",
|
105
|
-
stack = False,
|
106
|
-
)
|
107
|
-
|
108
|
-
kill_success, kill_msg = d.kill(timeout=timeout_seconds)
|
109
|
-
if kill_success:
|
110
|
-
_kill_daemons.append(d)
|
111
|
-
continue
|
112
|
-
if not nopretty:
|
113
|
-
warn(f"Failed to kill job '{d.daemon_id}' (PID {d.pid}):\n{kill_msg}", stack=False)
|
118
|
+
job_success_tuples = {}
|
119
|
+
for name, job in jobs_to_stop.items():
|
120
|
+
stop_success, stop_msg = job.stop(
|
121
|
+
timeout_seconds=timeout_seconds,
|
122
|
+
debug=debug,
|
123
|
+
)
|
124
|
+
job_success_tuples[name] = (stop_success, stop_msg)
|
114
125
|
|
126
|
+
num_success = sum(
|
127
|
+
(
|
128
|
+
1
|
129
|
+
for name, (stop_success, stop_msg) in job_success_tuples.items()
|
130
|
+
if stop_success
|
131
|
+
)
|
132
|
+
)
|
133
|
+
num_fail = sum(
|
134
|
+
(
|
135
|
+
1
|
136
|
+
for name, (stop_success, stop_msg) in job_success_tuples.items()
|
137
|
+
if not stop_success
|
138
|
+
)
|
139
|
+
)
|
140
|
+
success = num_success > 0
|
115
141
|
msg = (
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
+ (("\n" if _quit_daemons else "")
|
120
|
-
+ ("Killed job" + ("s" if len(_kill_daemons) != 1 else '') +
|
121
|
-
" '" + "', '".join([d.daemon_id for d in _kill_daemons]) + "'.")
|
122
|
-
if _kill_daemons else '')
|
142
|
+
f"Stopped {num_success} job"
|
143
|
+
+ ('s' if num_success != 1 else '')
|
144
|
+
+ '.'
|
123
145
|
)
|
124
|
-
|
146
|
+
if num_fail > 0:
|
147
|
+
msg += (
|
148
|
+
f"\nFailed to stop {num_fail} job"
|
149
|
+
+ ('s' if num_fail != 1 else '')
|
150
|
+
+ '.'
|
151
|
+
)
|
152
|
+
|
153
|
+
return success, msg
|
125
154
|
|
126
155
|
|
127
156
|
### NOTE: This must be the final statement of the module.
|
meerschaum/actions/sync.py
CHANGED
@@ -14,9 +14,9 @@ import meerschaum as mrsm
|
|
14
14
|
from meerschaum.utils.typing import SuccessTuple, Any, List, Optional, Tuple, Union
|
15
15
|
|
16
16
|
def sync(
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
action: Optional[List[str]] = None,
|
18
|
+
**kw: Any
|
19
|
+
) -> SuccessTuple:
|
20
20
|
"""
|
21
21
|
Fetch and sync data for pipes.
|
22
22
|
|
meerschaum/actions/upgrade.py
CHANGED
@@ -10,9 +10,9 @@ from __future__ import annotations
|
|
10
10
|
from meerschaum.utils.typing import SuccessTuple, Any, List, Optional, Union
|
11
11
|
|
12
12
|
def upgrade(
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
action: Optional[List[str]] = None,
|
14
|
+
**kw: Any
|
15
|
+
) -> SuccessTuple:
|
16
16
|
"""
|
17
17
|
Upgrade Meerschaum, plugins, or packages.
|
18
18
|
|
@@ -34,13 +34,13 @@ def upgrade(
|
|
34
34
|
|
35
35
|
|
36
36
|
def _upgrade_meerschaum(
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
37
|
+
action: Optional[List[str]] = None,
|
38
|
+
yes: bool = False,
|
39
|
+
force: bool = False,
|
40
|
+
noask: bool = False,
|
41
|
+
debug: bool = False,
|
42
|
+
**kw: Any
|
43
|
+
) -> SuccessTuple:
|
44
44
|
"""
|
45
45
|
Upgrade the current Meerschaum instance.
|
46
46
|
Optionally specify dependency groups.
|
@@ -109,26 +109,21 @@ class NoVenv:
|
|
109
109
|
pass
|
110
110
|
|
111
111
|
def _upgrade_packages(
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
112
|
+
action: Optional[List[str]] = None,
|
113
|
+
venv: Union[str, None, NoVenv] = NoVenv,
|
114
|
+
yes: bool = False,
|
115
|
+
force: bool = False,
|
116
|
+
noask: bool = False,
|
117
|
+
debug: bool = False,
|
118
|
+
**kw: Any
|
119
|
+
) -> SuccessTuple:
|
120
120
|
"""
|
121
121
|
Upgrade and install dependencies.
|
122
122
|
If provided, upgrade only a dependency group, otherwise default to `full`.
|
123
123
|
|
124
124
|
Examples:
|
125
|
-
```
|
126
125
|
upgrade packages
|
127
|
-
|
128
|
-
|
129
|
-
```
|
130
|
-
upgrade packages docs
|
131
|
-
```
|
126
|
+
upgrade packages full
|
132
127
|
"""
|
133
128
|
from meerschaum.utils.packages import packages, pip_install, get_prerelease_dependencies
|
134
129
|
from meerschaum.utils.warnings import info, warn
|
@@ -178,14 +173,15 @@ def _upgrade_packages(
|
|
178
173
|
)
|
179
174
|
return success, msg
|
180
175
|
|
176
|
+
|
181
177
|
def _upgrade_plugins(
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
178
|
+
action: Optional[List[str]] = None,
|
179
|
+
yes: bool = False,
|
180
|
+
force: bool = False,
|
181
|
+
noask: bool = False,
|
182
|
+
debug: bool = False,
|
183
|
+
**kw: Any
|
184
|
+
) -> SuccessTuple:
|
189
185
|
"""
|
190
186
|
Upgrade all installed plugins to the latest versions.
|
191
187
|
If no plugins are specified, attempt to upgrade all,
|
@@ -193,12 +189,8 @@ def _upgrade_plugins(
|
|
193
189
|
|
194
190
|
Examples:
|
195
191
|
|
196
|
-
```
|
197
192
|
upgrade plugins
|
198
|
-
|
199
|
-
```
|
200
|
-
upgrade plugins testing
|
201
|
-
```
|
193
|
+
upgrade plugins noaa
|
202
194
|
"""
|
203
195
|
from meerschaum.actions import actions
|
204
196
|
from meerschaum.plugins import get_plugins_names
|
meerschaum/api/_events.py
CHANGED
@@ -19,14 +19,20 @@ from meerschaum.utils.debug import dprint
|
|
19
19
|
from meerschaum.connectors.poll import retry_connect
|
20
20
|
from meerschaum.utils.warnings import warn
|
21
21
|
from meerschaum._internal.term.tools import is_webterm_running
|
22
|
+
from meerschaum.utils.jobs import start_check_jobs_thread, stop_check_jobs_thread
|
23
|
+
|
24
|
+
_check_jobs_thread = None
|
22
25
|
|
23
26
|
@app.on_event("startup")
|
24
27
|
async def startup():
|
25
|
-
|
28
|
+
"""
|
29
|
+
Connect to the instance database and begin monitoring jobs.
|
30
|
+
"""
|
26
31
|
try:
|
27
32
|
if not no_dash:
|
28
33
|
from meerschaum.api.dash.webterm import start_webterm
|
29
34
|
start_webterm()
|
35
|
+
|
30
36
|
connected = retry_connect(
|
31
37
|
get_api_connector(),
|
32
38
|
workers = get_uvicorn_config().get('workers', None),
|
@@ -36,18 +42,29 @@ async def startup():
|
|
36
42
|
import traceback
|
37
43
|
traceback.print_exc()
|
38
44
|
connected = False
|
45
|
+
|
39
46
|
if not connected:
|
40
47
|
await shutdown()
|
41
48
|
os._exit(1)
|
42
49
|
|
50
|
+
start_check_jobs_thread()
|
51
|
+
|
43
52
|
|
44
53
|
@app.on_event("shutdown")
|
45
54
|
async def shutdown():
|
55
|
+
"""
|
56
|
+
Close the database connection and stop monitoring jobs.
|
57
|
+
"""
|
46
58
|
if debug:
|
47
59
|
dprint("Closing connection...")
|
48
60
|
if get_api_connector().type == 'sql':
|
49
61
|
get_api_connector().engine.dispose()
|
50
62
|
|
63
|
+
stop_check_jobs_thread()
|
64
|
+
from meerschaum.api.routes._actions import _temp_jobs
|
65
|
+
for name, job in _temp_jobs.items():
|
66
|
+
job.delete()
|
67
|
+
|
51
68
|
### Terminate any running jobs left over.
|
52
69
|
if 'meerschaum.api.dash' in sys.modules:
|
53
70
|
from meerschaum.api.dash.actions import running_jobs, stop_action
|
meerschaum/api/_oauth2.py
CHANGED
@@ -30,6 +30,7 @@ class CustomOAuth2PasswordRequestForm:
|
|
30
30
|
self.client_id = client_id
|
31
31
|
self.client_secret = client_secret
|
32
32
|
|
33
|
+
|
33
34
|
LoginManager = fastapi_login.LoginManager
|
34
35
|
def generate_secret_key() -> str:
|
35
36
|
"""
|
@@ -46,5 +47,6 @@ def generate_secret_key() -> str:
|
|
46
47
|
|
47
48
|
return secret_key.encode('utf-8')
|
48
49
|
|
50
|
+
|
49
51
|
SECRET = generate_secret_key()
|
50
52
|
manager = LoginManager(SECRET, token_url=endpoints['login'])
|
meerschaum/api/_websockets.py
CHANGED
@@ -23,8 +23,8 @@ sessions = {}
|
|
23
23
|
@app.websocket('/dash/ws')
|
24
24
|
@app.websocket(_websocket_endpoint)
|
25
25
|
async def websocket_endpoint(
|
26
|
-
|
27
|
-
|
26
|
+
websocket: fastapi.WebSocket,
|
27
|
+
):
|
28
28
|
"""
|
29
29
|
Communicate with the Web Interface over a websocket.
|
30
30
|
"""
|
meerschaum/api/dash/jobs.py
CHANGED
@@ -16,6 +16,7 @@ from meerschaum.api import CHECK_UPDATE
|
|
16
16
|
dbc = attempt_import('dash_bootstrap_components', lazy=False, check_update=CHECK_UPDATE)
|
17
17
|
html, dcc = import_html(), import_dcc()
|
18
18
|
dateutil_parser = attempt_import('dateutil.parser', check_update=CHECK_UPDATE)
|
19
|
+
from meerschaum.utils.jobs import get_jobs, Job
|
19
20
|
from meerschaum.utils.daemon import (
|
20
21
|
get_daemons,
|
21
22
|
get_running_daemons,
|
@@ -38,13 +39,15 @@ def get_jobs_cards(state: WebState):
|
|
38
39
|
Build cards and alerts lists for jobs.
|
39
40
|
"""
|
40
41
|
daemons = get_daemons()
|
42
|
+
jobs = get_jobs()
|
41
43
|
session_id = state['session-store.data'].get('session-id', None)
|
42
44
|
is_authenticated = is_session_authenticated(session_id)
|
43
45
|
|
44
46
|
alert = alert_from_success_tuple(daemons)
|
45
47
|
cards = []
|
46
48
|
|
47
|
-
for
|
49
|
+
for name, job in jobs.items():
|
50
|
+
d = job.daemon
|
48
51
|
footer_children = html.Div(
|
49
52
|
build_process_timestamps_children(d),
|
50
53
|
id = {'type': 'process-timestamps-div', 'index': d.daemon_id},
|
@@ -80,7 +83,7 @@ def get_jobs_cards(state: WebState):
|
|
80
83
|
]
|
81
84
|
|
82
85
|
body_children = [
|
83
|
-
html.H4(html.B(
|
86
|
+
html.H4(html.B(name), className="card-title"),
|
84
87
|
html.Div(
|
85
88
|
html.P(
|
86
89
|
d.label,
|
@@ -8,6 +8,7 @@ Import all routes from other modules in package
|
|
8
8
|
|
9
9
|
from meerschaum.api.routes._login import *
|
10
10
|
from meerschaum.api.routes._actions import *
|
11
|
+
from meerschaum.api.routes._jobs import *
|
11
12
|
from meerschaum.api.routes._connectors import *
|
12
13
|
from meerschaum.api.routes._index import *
|
13
14
|
from meerschaum.api.routes._misc import *
|
@@ -7,65 +7,143 @@ Execute Meerschaum Actions via the API
|
|
7
7
|
"""
|
8
8
|
|
9
9
|
from __future__ import annotations
|
10
|
-
|
10
|
+
import asyncio
|
11
|
+
import traceback
|
12
|
+
import shlex
|
13
|
+
from functools import partial
|
14
|
+
from datetime import datetime, timezone
|
11
15
|
|
16
|
+
from fastapi import WebSocket, WebSocketDisconnect
|
17
|
+
|
18
|
+
from meerschaum.utils.misc import generate_password
|
19
|
+
from meerschaum.utils.jobs import Job
|
20
|
+
from meerschaum.utils.warnings import warn
|
21
|
+
from meerschaum.utils.typing import SuccessTuple, Union, List, Dict
|
12
22
|
from meerschaum.api import (
|
13
23
|
fastapi, app, endpoints, get_api_connector, debug, manager, private, no_auth
|
14
24
|
)
|
15
25
|
from meerschaum.actions import actions
|
16
26
|
import meerschaum.core
|
27
|
+
from meerschaum.config import get_config
|
28
|
+
from meerschaum._internal.arguments._parse_arguments import parse_dict_to_sysargs, parse_arguments
|
29
|
+
|
17
30
|
actions_endpoint = endpoints['actions']
|
18
31
|
|
32
|
+
def is_user_allowed_to_execute(user) -> SuccessTuple:
|
33
|
+
if user is None:
|
34
|
+
return False, "Could not load user."
|
35
|
+
|
36
|
+
if user.type == 'admin':
|
37
|
+
return True, "Success"
|
38
|
+
|
39
|
+
allow_non_admin = get_config(
|
40
|
+
'system', 'api', 'permissions', 'actions', 'non_admin', patch=True
|
41
|
+
)
|
42
|
+
if not allow_non_admin:
|
43
|
+
return False, (
|
44
|
+
"The administrator for this server has not allowed users to perform actions.\n\n"
|
45
|
+
+ "Please contact the system administrator, or if you are running this server, "
|
46
|
+
+ "open the configuration file with `edit config system` "
|
47
|
+
+ "and search for 'permissions'. "
|
48
|
+
+ "\nUnder the keys 'api:permissions:actions', "
|
49
|
+
+ "you can allow non-admin users to perform actions."
|
50
|
+
)
|
51
|
+
|
52
|
+
return True, "Success"
|
53
|
+
|
54
|
+
|
19
55
|
@app.get(actions_endpoint, tags=['Actions'])
|
20
56
|
def get_actions(
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
57
|
+
curr_user = (
|
58
|
+
fastapi.Depends(manager) if private else None
|
59
|
+
),
|
60
|
+
) -> List[str]:
|
61
|
+
"""
|
62
|
+
Return a list of the available actions.
|
63
|
+
"""
|
25
64
|
return list(actions)
|
26
65
|
|
27
|
-
|
28
|
-
def
|
29
|
-
|
30
|
-
|
31
|
-
curr_user = (
|
32
|
-
fastapi.Depends(manager) if not no_auth else None
|
33
|
-
),
|
34
|
-
) -> SuccessTuple:
|
66
|
+
|
67
|
+
async def notify_client(client, content: str):
|
68
|
+
"""
|
69
|
+
Send a line of text to a client.
|
35
70
|
"""
|
36
|
-
|
71
|
+
try:
|
72
|
+
await client.send_text(content)
|
73
|
+
except WebSocketDisconnect:
|
74
|
+
pass
|
37
75
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
76
|
+
_temp_jobs = {}
|
77
|
+
@app.websocket(actions_endpoint + '/ws')
|
78
|
+
async def do_action_websocket(websocket: WebSocket):
|
79
|
+
"""
|
80
|
+
Execute an action and stream the output to the client.
|
81
|
+
"""
|
82
|
+
await websocket.accept()
|
45
83
|
|
46
|
-
|
47
|
-
-------
|
48
|
-
A `SuccessTuple` of success, message.
|
84
|
+
stop_event = asyncio.Event()
|
49
85
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
if not allow_non_admin:
|
57
|
-
return False, (
|
58
|
-
"The administrator for this server has not allowed users to perform actions.\n\n"
|
59
|
-
+ "Please contact the system administrator, or if you are running this server, "
|
60
|
-
+ "open the configuration file with `edit config system` "
|
61
|
-
+ "and search for 'permissions'. "
|
62
|
-
+ "\nUnder the keys 'api:permissions:actions', "
|
63
|
-
+ "you can allow non-admin users to perform actions."
|
86
|
+
async def monitor_logs(job):
|
87
|
+
success, msg = job.start()
|
88
|
+
await job.monitor_logs_async(
|
89
|
+
partial(notify_client, websocket),
|
90
|
+
stop_event=stop_event,
|
91
|
+
stop_on_exit=True,
|
64
92
|
)
|
65
93
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
94
|
+
job = None
|
95
|
+
try:
|
96
|
+
token = await websocket.receive_text()
|
97
|
+
user = await manager.get_current_user(token) if not no_auth else None
|
98
|
+
if user is None and not no_auth:
|
99
|
+
raise fastapi.HTTPException(
|
100
|
+
status_code=401,
|
101
|
+
detail="Invalid credentials.",
|
102
|
+
)
|
103
|
+
|
104
|
+
auth_success, auth_msg = is_user_allowed_to_execute(user)
|
105
|
+
auth_payload = {
|
106
|
+
'is_authenticated': auth_success,
|
107
|
+
'timestamp': datetime.now(timezone.utc).isoformat(),
|
108
|
+
}
|
109
|
+
await websocket.send_json(auth_payload)
|
110
|
+
if not auth_success:
|
111
|
+
await websocket.close()
|
112
|
+
|
113
|
+
sysargs = await websocket.receive_json()
|
114
|
+
kwargs = parse_arguments(sysargs)
|
115
|
+
_ = kwargs.pop('executor_keys', None)
|
116
|
+
_ = kwargs.pop('shell', None)
|
117
|
+
sysargs = parse_dict_to_sysargs(kwargs)
|
118
|
+
|
119
|
+
job_name = '.' + generate_password(12)
|
120
|
+
job = Job(
|
121
|
+
job_name,
|
122
|
+
sysargs,
|
123
|
+
_properties={
|
124
|
+
'logs': {
|
125
|
+
'write_timestamps': False,
|
126
|
+
},
|
127
|
+
},
|
128
|
+
)
|
129
|
+
_temp_jobs[job_name] = job
|
130
|
+
monitor_task = asyncio.create_task(monitor_logs(job))
|
131
|
+
await monitor_task
|
132
|
+
try:
|
133
|
+
await websocket.close()
|
134
|
+
except RuntimeError:
|
135
|
+
pass
|
136
|
+
except fastapi.HTTPException:
|
137
|
+
await websocket.send_text("Invalid credentials.")
|
138
|
+
await websocket.close()
|
139
|
+
except WebSocketDisconnect:
|
140
|
+
pass
|
141
|
+
except asyncio.CancelledError:
|
142
|
+
pass
|
143
|
+
except Exception:
|
144
|
+
warn(f"Error in logs websocket:\n{traceback.format_exc()}")
|
145
|
+
finally:
|
146
|
+
if job is not None:
|
147
|
+
job.delete()
|
148
|
+
_ = _temp_jobs.pop(job_name, None)
|
149
|
+
stop_event.set()
|