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

Sign up to get free protection for your applications and to get access to all the features.
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 +1796 -1218
  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 +20 -324
  11. ominfra/supervisor/dispatchersimpl.py +342 -0
  12. ominfra/supervisor/groups.py +33 -111
  13. ominfra/supervisor/groupsimpl.py +86 -0
  14. ominfra/supervisor/inject.py +44 -19
  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} +96 -330
  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 +52 -77
  27. ominfra/supervisor/types.py +101 -45
  28. ominfra/supervisor/users.py +64 -0
  29. {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev127.dist-info}/METADATA +3 -3
  30. {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev127.dist-info}/RECORD +34 -23
  31. {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev127.dist-info}/LICENSE +0 -0
  32. {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev127.dist-info}/WHEEL +0 -0
  33. {ominfra-0.0.0.dev126.dist-info → ominfra-0.0.0.dev127.dist-info}/entry_points.txt +0 -0
  34. {ominfra-0.0.0.dev126.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