ominfra 0.0.0.dev126__py3-none-any.whl → 0.0.0.dev128__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.
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}')