ominfra 0.0.0.dev125__py3-none-any.whl → 0.0.0.dev127__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 (34) 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 +1825 -1217
  7. ominfra/supervisor/collections.py +52 -0
  8. ominfra/supervisor/context.py +2 -336
  9. ominfra/supervisor/datatypes.py +1 -63
  10. ominfra/supervisor/dispatchers.py +22 -338
  11. ominfra/supervisor/dispatchersimpl.py +342 -0
  12. ominfra/supervisor/groups.py +33 -110
  13. ominfra/supervisor/groupsimpl.py +86 -0
  14. ominfra/supervisor/inject.py +45 -13
  15. ominfra/supervisor/main.py +1 -1
  16. ominfra/supervisor/pipes.py +83 -0
  17. ominfra/supervisor/poller.py +6 -3
  18. ominfra/supervisor/privileges.py +65 -0
  19. ominfra/supervisor/processes.py +18 -0
  20. ominfra/supervisor/{process.py → processesimpl.py} +99 -317
  21. ominfra/supervisor/setup.py +38 -0
  22. ominfra/supervisor/setupimpl.py +261 -0
  23. ominfra/supervisor/signals.py +24 -16
  24. ominfra/supervisor/spawning.py +31 -0
  25. ominfra/supervisor/spawningimpl.py +347 -0
  26. ominfra/supervisor/supervisor.py +54 -78
  27. ominfra/supervisor/types.py +122 -39
  28. ominfra/supervisor/users.py +64 -0
  29. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/METADATA +3 -3
  30. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/RECORD +34 -23
  31. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/LICENSE +0 -0
  32. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/WHEEL +0 -0
  33. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/entry_points.txt +0 -0
  34. {ominfra-0.0.0.dev125.dist-info → ominfra-0.0.0.dev127.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,52 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import abc
3
+ import typing as ta
4
+
5
+
6
+ K = ta.TypeVar('K')
7
+ V = ta.TypeVar('V')
8
+
9
+
10
+ class KeyedCollectionAccessors(abc.ABC, ta.Generic[K, V]):
11
+ @property
12
+ @abc.abstractmethod
13
+ def _by_key(self) -> ta.Mapping[K, V]:
14
+ raise NotImplementedError
15
+
16
+ def __iter__(self) -> ta.Iterator[V]:
17
+ return iter(self._by_key.values())
18
+
19
+ def __len__(self) -> int:
20
+ return len(self._by_key)
21
+
22
+ def __contains__(self, key: K) -> bool:
23
+ return key in self._by_key
24
+
25
+ def __getitem__(self, key: K) -> V:
26
+ return self._by_key[key]
27
+
28
+ def get(self, key: K, default: ta.Optional[V] = None) -> ta.Optional[V]:
29
+ return self._by_key.get(key, default)
30
+
31
+ def items(self) -> ta.Iterator[ta.Tuple[K, V]]:
32
+ return iter(self._by_key.items())
33
+
34
+
35
+ class KeyedCollection(KeyedCollectionAccessors[K, V]):
36
+ def __init__(self, items: ta.Iterable[V]) -> None:
37
+ super().__init__()
38
+
39
+ by_key: ta.Dict[K, V] = {}
40
+ for v in items:
41
+ if (k := self._key(v)) in by_key:
42
+ raise KeyError(f'key {k} of {v} already registered by {by_key[k]}')
43
+ by_key[k] = v
44
+ self.__by_key = by_key
45
+
46
+ @property
47
+ def _by_key(self) -> ta.Mapping[K, V]:
48
+ return self.__by_key
49
+
50
+ @abc.abstractmethod
51
+ def _key(self, v: V) -> K:
52
+ raise NotImplementedError
@@ -1,34 +1,17 @@
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
11
  from .types import Process
24
12
  from .types import ServerContext
25
- from .utils import close_fd
13
+ from .types import ServerEpoch
26
14
  from .utils import mktempfile
27
- from .utils import real_exit
28
- from .utils import try_unlink
29
-
30
-
31
- ServerEpoch = ta.NewType('ServerEpoch', int)
32
15
 
33
16
 
34
17
  class ServerContextImpl(ServerContext):
@@ -48,16 +31,6 @@ class ServerContextImpl(ServerContext):
48
31
  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
@@ -81,15 +54,7 @@ class ServerContextImpl(ServerContext):
81
54
  def pid_history(self) -> ta.Dict[int, Process]:
82
55
  return self._pid_history
83
56
 
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
91
-
92
- ##
57
+ #
93
58
 
94
59
  def waitpid(self) -> ta.Tuple[ta.Optional[int], ta.Optional[int]]:
95
60
  # Need pthread_sigmask here to avoid concurrent sigchld, but Python doesn't offer in Python < 3.4. There is
@@ -109,166 +74,6 @@ class ServerContextImpl(ServerContext):
109
74
  pid, sts = None, None
110
75
  return pid, sts
111
76
 
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.
271
-
272
77
  def get_auto_child_log_name(self, name: str, identifier: str, channel: str) -> str:
273
78
  prefix = f'{name}-{channel}---{identifier}-'
274
79
  logfile = mktempfile(
@@ -277,142 +82,3 @@ class ServerContextImpl(ServerContext):
277
82
  dir=self.config.child_logdir,
278
83
  )
279
84
  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}')
@@ -1,9 +1,6 @@
1
1
  # ruff: noqa: UP007
2
- import grp
3
2
  import logging
4
3
  import os
5
- import pwd
6
- import signal
7
4
  import typing as ta
8
5
 
9
6
 
@@ -36,43 +33,7 @@ def logfile_name(val):
36
33
  return existing_dirpath(val)
37
34
 
38
35
 
39
- def name_to_uid(name: str) -> int:
40
- try:
41
- uid = int(name)
42
- except ValueError:
43
- try:
44
- pwdrec = pwd.getpwnam(name)
45
- except KeyError:
46
- raise ValueError(f'Invalid user name {name}') # noqa
47
- uid = pwdrec[2]
48
- else:
49
- try:
50
- pwd.getpwuid(uid) # check if uid is valid
51
- except KeyError:
52
- raise ValueError(f'Invalid user id {name}') # noqa
53
- return uid
54
-
55
-
56
- def name_to_gid(name: str) -> int:
57
- try:
58
- gid = int(name)
59
- except ValueError:
60
- try:
61
- grprec = grp.getgrnam(name)
62
- except KeyError:
63
- raise ValueError(f'Invalid group name {name}') # noqa
64
- gid = grprec[2]
65
- else:
66
- try:
67
- grp.getgrgid(gid) # check if gid is valid
68
- except KeyError:
69
- raise ValueError(f'Invalid group id {name}') # noqa
70
- return gid
71
-
72
-
73
- def gid_for_uid(uid: int) -> int:
74
- pwrec = pwd.getpwuid(uid)
75
- return pwrec[3]
36
+ ##
76
37
 
77
38
 
78
39
  def octal_type(arg: ta.Union[str, int]) -> int:
@@ -144,29 +105,6 @@ byte_size = SuffixMultiplier({
144
105
  })
145
106
 
146
107
 
147
- # all valid signal numbers
148
- SIGNUMS = [getattr(signal, k) for k in dir(signal) if k.startswith('SIG')]
149
-
150
-
151
- def signal_number(value: ta.Union[int, str]) -> int:
152
- try:
153
- num = int(value)
154
-
155
- except (ValueError, TypeError):
156
- name = value.strip().upper() # type: ignore
157
- if not name.startswith('SIG'):
158
- name = f'SIG{name}'
159
-
160
- num = getattr(signal, name, None) # type: ignore
161
- if num is None:
162
- raise ValueError(f'value {value!r} is not a valid signal name') # noqa
163
-
164
- if num not in SIGNUMS:
165
- raise ValueError(f'value {value!r} is not a valid signal number')
166
-
167
- return num
168
-
169
-
170
108
  class RestartWhenExitUnexpected:
171
109
  pass
172
110