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