ominfra 0.0.0.dev118__py3-none-any.whl → 0.0.0.dev120__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- ominfra/deploy/_executor.py +6 -2
- ominfra/deploy/poly/_main.py +6 -2
- ominfra/journald/fields.py +187 -0
- ominfra/journald/tailer.py +375 -312
- ominfra/pyremote/_runcommands.py +6 -2
- ominfra/scripts/journald2aws.py +381 -314
- ominfra/scripts/supervisor.py +697 -464
- ominfra/supervisor/__main__.py +1 -1
- ominfra/supervisor/context.py +50 -25
- ominfra/supervisor/dispatchers.py +12 -13
- ominfra/supervisor/events.py +4 -7
- ominfra/supervisor/main.py +68 -0
- ominfra/supervisor/poller.py +1 -3
- ominfra/supervisor/process.py +10 -11
- ominfra/supervisor/supervisor.py +161 -193
- {ominfra-0.0.0.dev118.dist-info → ominfra-0.0.0.dev120.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev118.dist-info → ominfra-0.0.0.dev120.dist-info}/RECORD +21 -19
- {ominfra-0.0.0.dev118.dist-info → ominfra-0.0.0.dev120.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev118.dist-info → ominfra-0.0.0.dev120.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev118.dist-info → ominfra-0.0.0.dev120.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev118.dist-info → ominfra-0.0.0.dev120.dist-info}/top_level.txt +0 -0
ominfra/supervisor/__main__.py
CHANGED
ominfra/supervisor/context.py
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
import errno
|
3
3
|
import fcntl
|
4
4
|
import grp
|
5
|
-
import logging
|
6
5
|
import os
|
7
6
|
import pwd
|
8
7
|
import re
|
@@ -12,6 +11,8 @@ import stat
|
|
12
11
|
import typing as ta
|
13
12
|
import warnings
|
14
13
|
|
14
|
+
from omlish.lite.logs import log
|
15
|
+
|
15
16
|
from .compat import SignalReceiver
|
16
17
|
from .compat import close_fd
|
17
18
|
from .compat import mktempfile
|
@@ -23,6 +24,7 @@ from .datatypes import name_to_uid
|
|
23
24
|
from .exceptions import NoPermissionError
|
24
25
|
from .exceptions import NotExecutableError
|
25
26
|
from .exceptions import NotFoundError
|
27
|
+
from .poller import BasePoller
|
26
28
|
from .poller import Poller
|
27
29
|
from .states import SupervisorState
|
28
30
|
from .states import SupervisorStates
|
@@ -30,41 +32,47 @@ from .types import AbstractServerContext
|
|
30
32
|
from .types import AbstractSubprocess
|
31
33
|
|
32
34
|
|
33
|
-
log = logging.getLogger(__name__)
|
34
|
-
|
35
|
-
|
36
35
|
class ServerContext(AbstractServerContext):
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
36
|
+
def __init__(
|
37
|
+
self,
|
38
|
+
config: ServerConfig,
|
39
|
+
*,
|
40
|
+
epoch: int = 0,
|
41
|
+
) -> None:
|
43
42
|
super().__init__()
|
44
43
|
|
45
44
|
self._config = config
|
45
|
+
self._epoch = epoch
|
46
46
|
|
47
47
|
self._pid_history: ta.Dict[int, AbstractSubprocess] = {}
|
48
48
|
self._state: SupervisorState = SupervisorStates.RUNNING
|
49
49
|
|
50
|
-
self.
|
50
|
+
self._signal_receiver = SignalReceiver()
|
51
51
|
|
52
|
-
self.
|
52
|
+
self._poller: BasePoller = Poller()
|
53
53
|
|
54
|
-
if
|
55
|
-
uid = name_to_uid(
|
56
|
-
self.
|
57
|
-
self.
|
54
|
+
if config.user is not None:
|
55
|
+
uid = name_to_uid(config.user)
|
56
|
+
self._uid: ta.Optional[int] = uid
|
57
|
+
self._gid: ta.Optional[int] = gid_for_uid(uid)
|
58
58
|
else:
|
59
|
-
self.
|
60
|
-
self.
|
59
|
+
self._uid = None
|
60
|
+
self._gid = None
|
61
61
|
|
62
|
-
self.
|
62
|
+
self._unlink_pidfile = False
|
63
63
|
|
64
64
|
@property
|
65
65
|
def config(self) -> ServerConfig:
|
66
66
|
return self._config
|
67
67
|
|
68
|
+
@property
|
69
|
+
def epoch(self) -> int:
|
70
|
+
return self._epoch
|
71
|
+
|
72
|
+
@property
|
73
|
+
def first(self) -> bool:
|
74
|
+
return not self._epoch
|
75
|
+
|
68
76
|
@property
|
69
77
|
def state(self) -> SupervisorState:
|
70
78
|
return self._state
|
@@ -72,17 +80,26 @@ class ServerContext(AbstractServerContext):
|
|
72
80
|
def set_state(self, state: SupervisorState) -> None:
|
73
81
|
self._state = state
|
74
82
|
|
83
|
+
@property
|
84
|
+
def poller(self) -> BasePoller:
|
85
|
+
return self._poller
|
86
|
+
|
75
87
|
@property
|
76
88
|
def pid_history(self) -> ta.Dict[int, AbstractSubprocess]:
|
77
89
|
return self._pid_history
|
78
90
|
|
79
|
-
|
80
|
-
|
91
|
+
@property
|
92
|
+
def uid(self) -> ta.Optional[int]:
|
93
|
+
return self._uid
|
94
|
+
|
95
|
+
@property
|
96
|
+
def gid(self) -> ta.Optional[int]:
|
97
|
+
return self._gid
|
81
98
|
|
82
99
|
##
|
83
100
|
|
84
101
|
def set_signals(self) -> None:
|
85
|
-
self.
|
102
|
+
self._signal_receiver.install(
|
86
103
|
signal.SIGTERM,
|
87
104
|
signal.SIGINT,
|
88
105
|
signal.SIGQUIT,
|
@@ -193,7 +210,7 @@ class ServerContext(AbstractServerContext):
|
|
193
210
|
))
|
194
211
|
|
195
212
|
def cleanup(self) -> None:
|
196
|
-
if self.
|
213
|
+
if self._unlink_pidfile:
|
197
214
|
try_unlink(self.config.pidfile)
|
198
215
|
self.poller.close()
|
199
216
|
|
@@ -246,6 +263,7 @@ class ServerContext(AbstractServerContext):
|
|
246
263
|
# Parent
|
247
264
|
log.debug('supervisord forked; parent exiting')
|
248
265
|
real_exit(0)
|
266
|
+
|
249
267
|
# Child
|
250
268
|
log.info('daemonizing the supervisord process')
|
251
269
|
if self.config.directory:
|
@@ -255,11 +273,15 @@ class ServerContext(AbstractServerContext):
|
|
255
273
|
log.critical("can't chdir into %r: %s", self.config.directory, err)
|
256
274
|
else:
|
257
275
|
log.info('set current directory: %r', self.config.directory)
|
276
|
+
|
258
277
|
os.dup2(0, os.open('/dev/null', os.O_RDONLY))
|
259
278
|
os.dup2(1, os.open('/dev/null', os.O_WRONLY))
|
260
279
|
os.dup2(2, os.open('/dev/null', os.O_WRONLY))
|
280
|
+
|
261
281
|
os.setsid()
|
282
|
+
|
262
283
|
os.umask(self.config.umask)
|
284
|
+
|
263
285
|
# XXX Stevens, in his Advanced Unix book, section 13.3 (page 417) recommends calling umask(0) and closing unused
|
264
286
|
# file descriptors. In his Network Programming book, he additionally recommends ignoring SIGHUP and forking
|
265
287
|
# again after the setsid() call, for obscure SVR4 reasons.
|
@@ -274,7 +296,7 @@ class ServerContext(AbstractServerContext):
|
|
274
296
|
return logfile
|
275
297
|
|
276
298
|
def get_signal(self) -> ta.Optional[int]:
|
277
|
-
return self.
|
299
|
+
return self._signal_receiver.get_signal()
|
278
300
|
|
279
301
|
def write_pidfile(self) -> None:
|
280
302
|
pid = os.getpid()
|
@@ -284,7 +306,7 @@ class ServerContext(AbstractServerContext):
|
|
284
306
|
except OSError:
|
285
307
|
log.critical('could not write pidfile %s', self.config.pidfile)
|
286
308
|
else:
|
287
|
-
self.
|
309
|
+
self._unlink_pidfile = True
|
288
310
|
log.info('supervisord started with pid %s', pid)
|
289
311
|
|
290
312
|
|
@@ -337,11 +359,14 @@ def drop_privileges(user: ta.Union[int, str, None]) -> ta.Optional[str]:
|
|
337
359
|
os.setgroups(groups)
|
338
360
|
except OSError:
|
339
361
|
return 'Could not set groups of effective user'
|
362
|
+
|
340
363
|
try:
|
341
364
|
os.setgid(gid)
|
342
365
|
except OSError:
|
343
366
|
return 'Could not set group id of effective user'
|
367
|
+
|
344
368
|
os.setuid(uid)
|
369
|
+
|
345
370
|
return None
|
346
371
|
|
347
372
|
|
@@ -5,21 +5,20 @@ import logging
|
|
5
5
|
import os
|
6
6
|
import typing as ta
|
7
7
|
|
8
|
+
from omlish.lite.logs import log
|
9
|
+
|
8
10
|
from .compat import as_bytes
|
9
11
|
from .compat import compact_traceback
|
10
12
|
from .compat import find_prefix_at_end
|
11
13
|
from .compat import readfd
|
12
14
|
from .compat import strip_escapes
|
13
15
|
from .configs import ProcessConfig
|
16
|
+
from .events import EVENT_CALLBACKS
|
14
17
|
from .events import ProcessLogStderrEvent
|
15
18
|
from .events import ProcessLogStdoutEvent
|
16
|
-
from .events import notify_event
|
17
19
|
from .types import AbstractSubprocess
|
18
20
|
|
19
21
|
|
20
|
-
log = logging.getLogger(__name__)
|
21
|
-
|
22
|
-
|
23
22
|
class Dispatcher(abc.ABC):
|
24
23
|
|
25
24
|
def __init__(self, process: AbstractSubprocess, channel: str, fd: int) -> None:
|
@@ -176,16 +175,16 @@ class OutputDispatcher(Dispatcher):
|
|
176
175
|
# )
|
177
176
|
|
178
177
|
def remove_logs(self):
|
179
|
-
for
|
180
|
-
if
|
181
|
-
for handler in
|
178
|
+
for l in (self._normal_log, self._capture_log):
|
179
|
+
if l is not None:
|
180
|
+
for handler in l.handlers:
|
182
181
|
handler.remove() # type: ignore
|
183
182
|
handler.reopen() # type: ignore
|
184
183
|
|
185
184
|
def reopen_logs(self):
|
186
|
-
for
|
187
|
-
if
|
188
|
-
for handler in
|
185
|
+
for l in (self._normal_log, self._capture_log):
|
186
|
+
if l is not None:
|
187
|
+
for handler in l.handlers:
|
189
188
|
handler.reopen() # type: ignore
|
190
189
|
|
191
190
|
def _log(self, data):
|
@@ -208,10 +207,10 @@ class OutputDispatcher(Dispatcher):
|
|
208
207
|
|
209
208
|
if self._channel == 'stdout':
|
210
209
|
if self._stdout_events_enabled:
|
211
|
-
|
210
|
+
EVENT_CALLBACKS.notify(ProcessLogStdoutEvent(self._process, self._process.pid, data))
|
212
211
|
|
213
212
|
elif self._stderr_events_enabled:
|
214
|
-
|
213
|
+
EVENT_CALLBACKS.notify(ProcessLogStderrEvent(self._process, self._process.pid, data))
|
215
214
|
|
216
215
|
def record_output(self):
|
217
216
|
if self._capture_log is None:
|
@@ -262,7 +261,7 @@ class OutputDispatcher(Dispatcher):
|
|
262
261
|
channel = self._channel
|
263
262
|
procname = self._process.config.name
|
264
263
|
event = self.event_type(self._process, self._process.pid, data)
|
265
|
-
|
264
|
+
EVENT_CALLBACKS.notify(event)
|
266
265
|
|
267
266
|
log.debug('%r %s emitted a comm event', procname, channel)
|
268
267
|
for handler in self._capture_log.handlers:
|
ominfra/supervisor/events.py
CHANGED
@@ -29,12 +29,9 @@ class EventCallbacks:
|
|
29
29
|
|
30
30
|
EVENT_CALLBACKS = EventCallbacks()
|
31
31
|
|
32
|
-
notify_event = EVENT_CALLBACKS.notify
|
33
|
-
clear_events = EVENT_CALLBACKS.clear
|
34
|
-
|
35
32
|
|
36
33
|
class Event(abc.ABC): # noqa
|
37
|
-
"""Abstract event type
|
34
|
+
"""Abstract event type."""
|
38
35
|
|
39
36
|
|
40
37
|
class ProcessLogEvent(Event, abc.ABC):
|
@@ -114,7 +111,7 @@ class RemoteCommunicationEvent(Event):
|
|
114
111
|
|
115
112
|
|
116
113
|
class SupervisorStateChangeEvent(Event):
|
117
|
-
"""
|
114
|
+
"""Abstract class."""
|
118
115
|
|
119
116
|
def payload(self):
|
120
117
|
return ''
|
@@ -136,7 +133,7 @@ class EventRejectedEvent: # purposely does not subclass Event
|
|
136
133
|
|
137
134
|
|
138
135
|
class ProcessStateEvent(Event):
|
139
|
-
"""
|
136
|
+
"""Abstract class, never raised directly."""
|
140
137
|
frm = None
|
141
138
|
to = None
|
142
139
|
|
@@ -225,7 +222,7 @@ class ProcessGroupRemovedEvent(ProcessGroupEvent):
|
|
225
222
|
|
226
223
|
|
227
224
|
class TickEvent(Event):
|
228
|
-
"""
|
225
|
+
"""Abstract."""
|
229
226
|
|
230
227
|
def __init__(self, when, supervisord):
|
231
228
|
super().__init__()
|
@@ -0,0 +1,68 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
# ruff: noqa: UP006 UP007
|
3
|
+
# @omlish-amalg ../scripts/supervisor.py
|
4
|
+
import itertools
|
5
|
+
import json
|
6
|
+
import typing as ta
|
7
|
+
|
8
|
+
from omlish.lite.journald import journald_log_handler_factory
|
9
|
+
from omlish.lite.logs import configure_standard_logging
|
10
|
+
from omlish.lite.marshal import unmarshal_obj
|
11
|
+
|
12
|
+
from .compat import ExitNow
|
13
|
+
from .configs import ServerConfig
|
14
|
+
from .context import ServerContext
|
15
|
+
from .states import SupervisorStates
|
16
|
+
from .supervisor import Supervisor
|
17
|
+
|
18
|
+
|
19
|
+
def main(
|
20
|
+
argv: ta.Optional[ta.Sequence[str]] = None,
|
21
|
+
*,
|
22
|
+
no_logging: bool = False,
|
23
|
+
) -> None:
|
24
|
+
import argparse
|
25
|
+
|
26
|
+
parser = argparse.ArgumentParser()
|
27
|
+
parser.add_argument('config_file', metavar='config-file')
|
28
|
+
parser.add_argument('--no-journald', action='store_true')
|
29
|
+
args = parser.parse_args(argv)
|
30
|
+
|
31
|
+
#
|
32
|
+
|
33
|
+
if not (cf := args.config_file):
|
34
|
+
raise RuntimeError('No config file specified')
|
35
|
+
|
36
|
+
if not no_logging:
|
37
|
+
configure_standard_logging(
|
38
|
+
'INFO',
|
39
|
+
handler_factory=journald_log_handler_factory if not args.no_journald else None,
|
40
|
+
)
|
41
|
+
|
42
|
+
#
|
43
|
+
|
44
|
+
# if we hup, restart by making a new Supervisor()
|
45
|
+
for epoch in itertools.count():
|
46
|
+
with open(cf) as f:
|
47
|
+
config_src = f.read()
|
48
|
+
|
49
|
+
config_dct = json.loads(config_src)
|
50
|
+
config: ServerConfig = unmarshal_obj(config_dct, ServerConfig)
|
51
|
+
|
52
|
+
context = ServerContext(
|
53
|
+
config,
|
54
|
+
epoch=epoch,
|
55
|
+
)
|
56
|
+
|
57
|
+
supervisor = Supervisor(context)
|
58
|
+
try:
|
59
|
+
supervisor.main()
|
60
|
+
except ExitNow:
|
61
|
+
pass
|
62
|
+
|
63
|
+
if context.state < SupervisorStates.RESTARTING:
|
64
|
+
break
|
65
|
+
|
66
|
+
|
67
|
+
if __name__ == '__main__':
|
68
|
+
main()
|
ominfra/supervisor/poller.py
CHANGED
ominfra/supervisor/process.py
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007
|
2
2
|
import errno
|
3
3
|
import functools
|
4
|
-
import
|
5
|
-
import os
|
4
|
+
import os.path
|
6
5
|
import shlex
|
7
6
|
import signal
|
8
7
|
import time
|
9
8
|
import traceback
|
10
9
|
import typing as ta
|
11
10
|
|
11
|
+
from omlish.lite.logs import log
|
12
|
+
|
12
13
|
from .compat import as_bytes
|
13
14
|
from .compat import as_string
|
14
15
|
from .compat import close_fd
|
@@ -29,6 +30,7 @@ from .datatypes import RestartUnconditionally
|
|
29
30
|
from .dispatchers import Dispatcher
|
30
31
|
from .dispatchers import InputDispatcher
|
31
32
|
from .dispatchers import OutputDispatcher
|
33
|
+
from .events import EVENT_CALLBACKS
|
32
34
|
from .events import EventRejectedEvent
|
33
35
|
from .events import ProcessCommunicationEvent
|
34
36
|
from .events import ProcessCommunicationStderrEvent
|
@@ -42,7 +44,6 @@ from .events import ProcessStateStartingEvent
|
|
42
44
|
from .events import ProcessStateStoppedEvent
|
43
45
|
from .events import ProcessStateStoppingEvent
|
44
46
|
from .events import ProcessStateUnknownEvent
|
45
|
-
from .events import notify_event
|
46
47
|
from .exceptions import BadCommandError
|
47
48
|
from .exceptions import ProcessError
|
48
49
|
from .states import STOPPED_STATES
|
@@ -54,9 +55,6 @@ from .types import AbstractServerContext
|
|
54
55
|
from .types import AbstractSubprocess
|
55
56
|
|
56
57
|
|
57
|
-
log = logging.getLogger(__name__)
|
58
|
-
|
59
|
-
|
60
58
|
@functools.total_ordering
|
61
59
|
class Subprocess(AbstractSubprocess):
|
62
60
|
"""A class to manage a subprocess."""
|
@@ -209,7 +207,7 @@ class Subprocess(AbstractSubprocess):
|
|
209
207
|
event_class = self.event_map.get(new_state)
|
210
208
|
if event_class is not None:
|
211
209
|
event = event_class(self, old_state, expected)
|
212
|
-
|
210
|
+
EVENT_CALLBACKS.notify(event)
|
213
211
|
|
214
212
|
return True
|
215
213
|
|
@@ -326,6 +324,7 @@ class Subprocess(AbstractSubprocess):
|
|
326
324
|
os.dup2(self._pipes['child_stdout'], 2)
|
327
325
|
else:
|
328
326
|
os.dup2(self._pipes['child_stderr'], 2)
|
327
|
+
# FIXME: leave debugger fds
|
329
328
|
for i in range(3, self.context.config.minfds):
|
330
329
|
close_fd(i)
|
331
330
|
|
@@ -361,7 +360,7 @@ class Subprocess(AbstractSubprocess):
|
|
361
360
|
cwd = self.config.directory
|
362
361
|
try:
|
363
362
|
if cwd is not None:
|
364
|
-
os.chdir(cwd)
|
363
|
+
os.chdir(os.path.expanduser(cwd))
|
365
364
|
except OSError as why:
|
366
365
|
code = errno.errorcode.get(why.args[0], why.args[0])
|
367
366
|
msg = f"couldn't chdir to {cwd}: {code}\n"
|
@@ -417,7 +416,7 @@ class Subprocess(AbstractSubprocess):
|
|
417
416
|
return self.kill(self.config.stopsignal)
|
418
417
|
|
419
418
|
def stop_report(self) -> None:
|
420
|
-
"""
|
419
|
+
"""Log a 'waiting for x to stop' message with throttling."""
|
421
420
|
if self.state == ProcessStates.STOPPING:
|
422
421
|
now = time.time()
|
423
422
|
|
@@ -547,7 +546,7 @@ class Subprocess(AbstractSubprocess):
|
|
547
546
|
return None
|
548
547
|
|
549
548
|
def finish(self, sts: int) -> None:
|
550
|
-
"""
|
549
|
+
"""The process was reaped and we need to report and manage its state."""
|
551
550
|
|
552
551
|
self.drain()
|
553
552
|
|
@@ -628,7 +627,7 @@ class Subprocess(AbstractSubprocess):
|
|
628
627
|
# system that this event was rejected so it can be processed again.
|
629
628
|
if self.event is not None:
|
630
629
|
# Note: this should only be true if we were in the BUSY state when finish() was called.
|
631
|
-
|
630
|
+
EVENT_CALLBACKS.notify(EventRejectedEvent(self, self.event)) # type: ignore
|
632
631
|
self.event = None
|
633
632
|
|
634
633
|
def set_uid(self) -> ta.Optional[str]:
|