ominfra 0.0.0.dev126__py3-none-any.whl → 0.0.0.dev128__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. ominfra/clouds/aws/auth.py +1 -1
  2. ominfra/deploy/_executor.py +1 -1
  3. ominfra/deploy/poly/_main.py +1 -1
  4. ominfra/pyremote/_runcommands.py +1 -1
  5. ominfra/scripts/journald2aws.py +2 -2
  6. ominfra/scripts/supervisor.py +4736 -4166
  7. ominfra/supervisor/configs.py +34 -11
  8. ominfra/supervisor/context.py +7 -345
  9. ominfra/supervisor/dispatchers.py +21 -324
  10. ominfra/supervisor/dispatchersimpl.py +343 -0
  11. ominfra/supervisor/groups.py +33 -111
  12. ominfra/supervisor/groupsimpl.py +86 -0
  13. ominfra/supervisor/inject.py +45 -20
  14. ominfra/supervisor/main.py +3 -3
  15. ominfra/supervisor/pipes.py +85 -0
  16. ominfra/supervisor/poller.py +42 -38
  17. ominfra/supervisor/privileges.py +65 -0
  18. ominfra/supervisor/process.py +6 -742
  19. ominfra/supervisor/processimpl.py +516 -0
  20. ominfra/supervisor/setup.py +38 -0
  21. ominfra/supervisor/setupimpl.py +262 -0
  22. ominfra/supervisor/spawning.py +32 -0
  23. ominfra/supervisor/spawningimpl.py +350 -0
  24. ominfra/supervisor/supervisor.py +67 -84
  25. ominfra/supervisor/types.py +101 -47
  26. ominfra/supervisor/utils/__init__.py +0 -0
  27. ominfra/supervisor/utils/collections.py +52 -0
  28. ominfra/supervisor/utils/diag.py +31 -0
  29. ominfra/supervisor/utils/fds.py +46 -0
  30. ominfra/supervisor/utils/fs.py +47 -0
  31. ominfra/supervisor/utils/os.py +45 -0
  32. ominfra/supervisor/utils/ostypes.py +9 -0
  33. ominfra/supervisor/utils/signals.py +60 -0
  34. ominfra/supervisor/utils/strings.py +105 -0
  35. ominfra/supervisor/utils/users.py +67 -0
  36. {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev128.dist-info}/METADATA +3 -3
  37. {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev128.dist-info}/RECORD +41 -25
  38. ominfra/supervisor/datatypes.py +0 -175
  39. ominfra/supervisor/signals.py +0 -52
  40. ominfra/supervisor/utils.py +0 -206
  41. {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev128.dist-info}/LICENSE +0 -0
  42. {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev128.dist-info}/WHEEL +0 -0
  43. {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev128.dist-info}/entry_points.txt +0 -0
  44. {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev128.dist-info}/top_level.txt +0 -0
@@ -7,11 +7,21 @@ import typing as ta
7
7
 
8
8
  from ..configs import ConfigMapping
9
9
  from ..configs import build_config_named_children
10
- from .datatypes import byte_size
11
- from .datatypes import existing_directory
12
- from .datatypes import existing_dirpath
13
- from .datatypes import logging_level
14
- from .datatypes import octal_type
10
+ from .utils.fs import check_existing_dir
11
+ from .utils.fs import check_path_with_existing_dir
12
+ from .utils.strings import parse_bytes_size
13
+ from .utils.strings import parse_octal
14
+
15
+
16
+ ##
17
+
18
+
19
+ class RestartWhenExitUnexpected:
20
+ pass
21
+
22
+
23
+ class RestartUnconditionally:
24
+ pass
15
25
 
16
26
 
17
27
  ##
@@ -104,12 +114,12 @@ class ServerConfig:
104
114
  **kwargs: ta.Any,
105
115
  ) -> 'ServerConfig':
106
116
  return cls(
107
- umask=octal_type(umask),
108
- directory=existing_directory(directory) if directory is not None else None,
109
- logfile=existing_dirpath(logfile),
110
- logfile_maxbytes=byte_size(logfile_maxbytes),
111
- loglevel=logging_level(loglevel),
112
- pidfile=existing_dirpath(pidfile),
117
+ umask=parse_octal(umask),
118
+ directory=check_existing_dir(directory) if directory is not None else None,
119
+ logfile=check_path_with_existing_dir(logfile),
120
+ logfile_maxbytes=parse_bytes_size(logfile_maxbytes),
121
+ loglevel=parse_logging_level(loglevel),
122
+ pidfile=check_path_with_existing_dir(pidfile),
113
123
  child_logdir=child_logdir if child_logdir else tempfile.gettempdir(),
114
124
  **kwargs,
115
125
  )
@@ -129,3 +139,16 @@ def prepare_server_config(dct: ta.Mapping[str, ta.Any]) -> ta.Mapping[str, ta.An
129
139
  group_dcts = build_config_named_children(out.get('groups'))
130
140
  out['groups'] = [prepare_process_group_config(group_dct) for group_dct in group_dcts or []]
131
141
  return out
142
+
143
+
144
+ ##
145
+
146
+
147
+ def parse_logging_level(value: ta.Union[str, int]) -> int:
148
+ if isinstance(value, int):
149
+ return value
150
+ s = str(value).lower()
151
+ level = logging.getLevelNamesMapping().get(s.upper())
152
+ if level is None:
153
+ raise ValueError(f'bad logging level name {value!r}')
154
+ return level
@@ -1,34 +1,18 @@
1
1
  # ruff: noqa: UP006 UP007
2
2
  import errno
3
- import fcntl
4
- import grp
5
3
  import os
6
- import pwd
7
- import re
8
- import resource
9
- import stat
10
4
  import typing as ta
11
- import warnings
12
5
 
13
6
  from omlish.lite.logs import log
14
7
 
15
8
  from .configs import ServerConfig
16
- from .datatypes import gid_for_uid
17
- from .datatypes import name_to_uid
18
- from .exceptions import NoPermissionError
19
- from .exceptions import NotExecutableError
20
- from .exceptions import NotFoundError
21
9
  from .poller import Poller
22
10
  from .states import SupervisorState
23
- from .types import Process
24
11
  from .types import ServerContext
25
- from .utils import close_fd
26
- from .utils import mktempfile
27
- from .utils import real_exit
28
- from .utils import try_unlink
29
-
30
-
31
- ServerEpoch = ta.NewType('ServerEpoch', int)
12
+ from .types import ServerEpoch
13
+ from .utils.fs import mktempfile
14
+ from .utils.ostypes import Pid
15
+ from .utils.ostypes import Rc
32
16
 
33
17
 
34
18
  class ServerContextImpl(ServerContext):
@@ -45,19 +29,8 @@ class ServerContextImpl(ServerContext):
45
29
  self._poller = poller
46
30
  self._epoch = epoch
47
31
 
48
- self._pid_history: ta.Dict[int, Process] = {}
49
32
  self._state: SupervisorState = SupervisorState.RUNNING
50
33
 
51
- if config.user is not None:
52
- uid = name_to_uid(config.user)
53
- self._uid: ta.Optional[int] = uid
54
- self._gid: ta.Optional[int] = gid_for_uid(uid)
55
- else:
56
- self._uid = None
57
- self._gid = None
58
-
59
- self._unlink_pidfile = False
60
-
61
34
  @property
62
35
  def config(self) -> ServerConfig:
63
36
  return self._config
@@ -77,21 +50,9 @@ class ServerContextImpl(ServerContext):
77
50
  def set_state(self, state: SupervisorState) -> None:
78
51
  self._state = state
79
52
 
80
- @property
81
- def pid_history(self) -> ta.Dict[int, Process]:
82
- return self._pid_history
83
-
84
- @property
85
- def uid(self) -> ta.Optional[int]:
86
- return self._uid
87
-
88
- @property
89
- def gid(self) -> ta.Optional[int]:
90
- return self._gid
53
+ #
91
54
 
92
- ##
93
-
94
- def waitpid(self) -> ta.Tuple[ta.Optional[int], ta.Optional[int]]:
55
+ def waitpid(self) -> ta.Tuple[ta.Optional[Pid], ta.Optional[Rc]]:
95
56
  # Need pthread_sigmask here to avoid concurrent sigchld, but Python doesn't offer in Python < 3.4. There is
96
57
  # still a race condition here; we can get a sigchld while we're sitting in the waitpid call. However, AFAICT, if
97
58
  # waitpid is interrupted by SIGCHLD, as long as we call waitpid again (which happens every so often during the
@@ -107,167 +68,7 @@ class ServerContextImpl(ServerContext):
107
68
  if code == errno.EINTR:
108
69
  log.debug('EINTR during reap')
109
70
  pid, sts = None, None
110
- return pid, sts
111
-
112
- def set_uid_or_exit(self) -> None:
113
- """
114
- Set the uid of the supervisord process. Called during supervisord startup only. No return value. Exits the
115
- process via usage() if privileges could not be dropped.
116
- """
117
-
118
- if self.uid is None:
119
- if os.getuid() == 0:
120
- warnings.warn(
121
- 'Supervisor is running as root. Privileges were not dropped because no user is specified in the '
122
- 'config file. If you intend to run as root, you can set user=root in the config file to avoid '
123
- 'this message.',
124
- )
125
- else:
126
- msg = drop_privileges(self.uid)
127
- if msg is None:
128
- log.info('Set uid to user %s succeeded', self.uid)
129
- else: # failed to drop privileges
130
- raise RuntimeError(msg)
131
-
132
- def set_rlimits_or_exit(self) -> None:
133
- """
134
- Set the rlimits of the supervisord process. Called during supervisord startup only. No return value. Exits
135
- the process via usage() if any rlimits could not be set.
136
- """
137
-
138
- limits = []
139
-
140
- if hasattr(resource, 'RLIMIT_NOFILE'):
141
- limits.append({
142
- 'msg': (
143
- 'The minimum number of file descriptors required to run this process is %(min_limit)s as per the '
144
- '"minfds" command-line argument or config file setting. The current environment will only allow '
145
- 'you to open %(hard)s file descriptors. Either raise the number of usable file descriptors in '
146
- 'your environment (see README.rst) or lower the minfds setting in the config file to allow the '
147
- 'process to start.'
148
- ),
149
- 'min': self.config.minfds,
150
- 'resource': resource.RLIMIT_NOFILE,
151
- 'name': 'RLIMIT_NOFILE',
152
- })
153
-
154
- if hasattr(resource, 'RLIMIT_NPROC'):
155
- limits.append({
156
- 'msg': (
157
- 'The minimum number of available processes required to run this program is %(min_limit)s as per '
158
- 'the "minprocs" command-line argument or config file setting. The current environment will only '
159
- 'allow you to open %(hard)s processes. Either raise the number of usable processes in your '
160
- 'environment (see README.rst) or lower the minprocs setting in the config file to allow the '
161
- 'program to start.'
162
- ),
163
- 'min': self.config.minprocs,
164
- 'resource': resource.RLIMIT_NPROC,
165
- 'name': 'RLIMIT_NPROC',
166
- })
167
-
168
- for limit in limits:
169
- min_limit = limit['min']
170
- res = limit['resource']
171
- msg = limit['msg']
172
- name = limit['name']
173
-
174
- soft, hard = resource.getrlimit(res) # type: ignore
175
-
176
- # -1 means unlimited
177
- if soft < min_limit and soft != -1: # type: ignore
178
- if hard < min_limit and hard != -1: # type: ignore
179
- # setrlimit should increase the hard limit if we are root, if not then setrlimit raises and we print
180
- # usage
181
- hard = min_limit # type: ignore
182
-
183
- try:
184
- resource.setrlimit(res, (min_limit, hard)) # type: ignore
185
- log.info('Increased %s limit to %s', name, min_limit)
186
- except (resource.error, ValueError):
187
- raise RuntimeError(msg % dict( # type: ignore # noqa
188
- min_limit=min_limit,
189
- res=res,
190
- name=name,
191
- soft=soft,
192
- hard=hard,
193
- ))
194
-
195
- def cleanup(self) -> None:
196
- if self._unlink_pidfile:
197
- try_unlink(self.config.pidfile)
198
- self._poller.close()
199
-
200
- def cleanup_fds(self) -> None:
201
- # try to close any leaked file descriptors (for reload)
202
- start = 5
203
- os.closerange(start, self.config.minfds)
204
-
205
- def clear_auto_child_logdir(self) -> None:
206
- # must be called after realize()
207
- child_logdir = self.config.child_logdir
208
- fnre = re.compile(rf'.+?---{self.config.identifier}-\S+\.log\.?\d{{0,4}}')
209
- try:
210
- filenames = os.listdir(child_logdir)
211
- except OSError:
212
- log.warning('Could not clear child_log dir')
213
- return
214
-
215
- for filename in filenames:
216
- if fnre.match(filename):
217
- pathname = os.path.join(child_logdir, filename)
218
- try:
219
- os.remove(pathname)
220
- except OSError:
221
- log.warning('Failed to clean up %r', pathname)
222
-
223
- def daemonize(self) -> None:
224
- self._poller.before_daemonize()
225
- self._daemonize()
226
- self._poller.after_daemonize()
227
-
228
- def _daemonize(self) -> None:
229
- # To daemonize, we need to become the leader of our own session (process) group. If we do not, signals sent to
230
- # our parent process will also be sent to us. This might be bad because signals such as SIGINT can be sent to
231
- # our parent process during normal (uninteresting) operations such as when we press Ctrl-C in the parent
232
- # terminal window to escape from a logtail command. To disassociate ourselves from our parent's session group we
233
- # use os.setsid. It means "set session id", which has the effect of disassociating a process from is current
234
- # session and process group and setting itself up as a new session leader.
235
- #
236
- # Unfortunately we cannot call setsid if we're already a session group leader, so we use "fork" to make a copy
237
- # of ourselves that is guaranteed to not be a session group leader.
238
- #
239
- # We also change directories, set stderr and stdout to null, and change our umask.
240
- #
241
- # This explanation was (gratefully) garnered from
242
- # http://www.cems.uwe.ac.uk/~irjohnso/coursenotes/lrc/system/daemons/d3.htm
243
-
244
- pid = os.fork()
245
- if pid != 0:
246
- # Parent
247
- log.debug('supervisord forked; parent exiting')
248
- real_exit(0)
249
-
250
- # Child
251
- log.info('daemonizing the supervisord process')
252
- if self.config.directory:
253
- try:
254
- os.chdir(self.config.directory)
255
- except OSError as err:
256
- log.critical("can't chdir into %r: %s", self.config.directory, err)
257
- else:
258
- log.info('set current directory: %r', self.config.directory)
259
-
260
- os.dup2(0, os.open('/dev/null', os.O_RDONLY))
261
- os.dup2(1, os.open('/dev/null', os.O_WRONLY))
262
- os.dup2(2, os.open('/dev/null', os.O_WRONLY))
263
-
264
- os.setsid()
265
-
266
- os.umask(self.config.umask)
267
-
268
- # XXX Stevens, in his Advanced Unix book, section 13.3 (page 417) recommends calling umask(0) and closing unused
269
- # file descriptors. In his Network Programming book, he additionally recommends ignoring SIGHUP and forking
270
- # again after the setsid() call, for obscure SVR4 reasons.
71
+ return pid, sts # type: ignore
271
72
 
272
73
  def get_auto_child_log_name(self, name: str, identifier: str, channel: str) -> str:
273
74
  prefix = f'{name}-{channel}---{identifier}-'
@@ -277,142 +78,3 @@ class ServerContextImpl(ServerContext):
277
78
  dir=self.config.child_logdir,
278
79
  )
279
80
  return logfile
280
-
281
- def write_pidfile(self) -> None:
282
- pid = os.getpid()
283
- try:
284
- with open(self.config.pidfile, 'w') as f:
285
- f.write(f'{pid}\n')
286
- except OSError:
287
- log.critical('could not write pidfile %s', self.config.pidfile)
288
- else:
289
- self._unlink_pidfile = True
290
- log.info('supervisord started with pid %s', pid)
291
-
292
-
293
- def drop_privileges(user: ta.Union[int, str, None]) -> ta.Optional[str]:
294
- """
295
- Drop privileges to become the specified user, which may be a username or uid. Called for supervisord startup
296
- and when spawning subprocesses. Returns None on success or a string error message if privileges could not be
297
- dropped.
298
- """
299
-
300
- if user is None:
301
- return 'No user specified to setuid to!'
302
-
303
- # get uid for user, which can be a number or username
304
- try:
305
- uid = int(user)
306
- except ValueError:
307
- try:
308
- pwrec = pwd.getpwnam(user) # type: ignore
309
- except KeyError:
310
- return f"Can't find username {user!r}"
311
- uid = pwrec[2]
312
- else:
313
- try:
314
- pwrec = pwd.getpwuid(uid)
315
- except KeyError:
316
- return f"Can't find uid {uid!r}"
317
-
318
- current_uid = os.getuid()
319
-
320
- if current_uid == uid:
321
- # do nothing and return successfully if the uid is already the current one. this allows a supervisord
322
- # running as an unprivileged user "foo" to start a process where the config has "user=foo" (same user) in
323
- # it.
324
- return None
325
-
326
- if current_uid != 0:
327
- return "Can't drop privilege as nonroot user"
328
-
329
- gid = pwrec[3]
330
- if hasattr(os, 'setgroups'):
331
- user = pwrec[0]
332
- groups = [grprec[2] for grprec in grp.getgrall() if user in grprec[3]]
333
-
334
- # always put our primary gid first in this list, otherwise we can lose group info since sometimes the first
335
- # group in the setgroups list gets overwritten on the subsequent setgid call (at least on freebsd 9 with
336
- # python 2.7 - this will be safe though for all unix /python version combos)
337
- groups.insert(0, gid)
338
- try:
339
- os.setgroups(groups)
340
- except OSError:
341
- return 'Could not set groups of effective user'
342
-
343
- try:
344
- os.setgid(gid)
345
- except OSError:
346
- return 'Could not set group id of effective user'
347
-
348
- os.setuid(uid)
349
-
350
- return None
351
-
352
-
353
- def make_pipes(stderr=True) -> ta.Mapping[str, int]:
354
- """
355
- Create pipes for parent to child stdin/stdout/stderr communications. Open fd in non-blocking mode so we can
356
- read them in the mainloop without blocking. If stderr is False, don't create a pipe for stderr.
357
- """
358
-
359
- pipes: ta.Dict[str, ta.Optional[int]] = {
360
- 'child_stdin': None,
361
- 'stdin': None,
362
- 'stdout': None,
363
- 'child_stdout': None,
364
- 'stderr': None,
365
- 'child_stderr': None,
366
- }
367
-
368
- try:
369
- stdin, child_stdin = os.pipe()
370
- pipes['child_stdin'], pipes['stdin'] = stdin, child_stdin
371
-
372
- stdout, child_stdout = os.pipe()
373
- pipes['stdout'], pipes['child_stdout'] = stdout, child_stdout
374
-
375
- if stderr:
376
- stderr, child_stderr = os.pipe()
377
- pipes['stderr'], pipes['child_stderr'] = stderr, child_stderr
378
-
379
- for fd in (pipes['stdout'], pipes['stderr'], pipes['stdin']):
380
- if fd is not None:
381
- flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NDELAY
382
- fcntl.fcntl(fd, fcntl.F_SETFL, flags)
383
-
384
- return pipes # type: ignore
385
-
386
- except OSError:
387
- for fd in pipes.values():
388
- if fd is not None:
389
- close_fd(fd)
390
- raise
391
-
392
-
393
- def close_parent_pipes(pipes: ta.Mapping[str, int]) -> None:
394
- for fdname in ('stdin', 'stdout', 'stderr'):
395
- fd = pipes.get(fdname)
396
- if fd is not None:
397
- close_fd(fd)
398
-
399
-
400
- def close_child_pipes(pipes: ta.Mapping[str, int]) -> None:
401
- for fdname in ('child_stdin', 'child_stdout', 'child_stderr'):
402
- fd = pipes.get(fdname)
403
- if fd is not None:
404
- close_fd(fd)
405
-
406
-
407
- def check_execv_args(filename, argv, st) -> None:
408
- if st is None:
409
- raise NotFoundError(f"can't find command {filename!r}")
410
-
411
- elif stat.S_ISDIR(st[stat.ST_MODE]):
412
- raise NotExecutableError(f'command at {filename!r} is a directory')
413
-
414
- elif not (stat.S_IMODE(st[stat.ST_MODE]) & 0o111):
415
- raise NotExecutableError(f'command at {filename!r} is not executable')
416
-
417
- elif not os.access(filename, os.X_OK):
418
- raise NoPermissionError(f'no permission to run command {filename!r}')