supervisor-pydantic 1.0.0__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.
- supervisor_pydantic/__init__.py +5 -0
- supervisor_pydantic/client/__init__.py +1 -0
- supervisor_pydantic/client/xmlrpc.py +212 -0
- supervisor_pydantic/config/__init__.py +18 -0
- supervisor_pydantic/config/base.py +92 -0
- supervisor_pydantic/config/convenience.py +102 -0
- supervisor_pydantic/config/eventlistener.py +48 -0
- supervisor_pydantic/config/fcgiprogram.py +26 -0
- supervisor_pydantic/config/group.py +29 -0
- supervisor_pydantic/config/hydra/base.yaml +18 -0
- supervisor_pydantic/config/hydra/eventlistener/default.yaml +2 -0
- supervisor_pydantic/config/hydra/fcgiprogram/default.yaml +2 -0
- supervisor_pydantic/config/hydra/group/default.yaml +2 -0
- supervisor_pydantic/config/hydra/hydra/hydra_logging/custom.yaml +24 -0
- supervisor_pydantic/config/hydra/hydra/job_logging/custom.yaml +24 -0
- supervisor_pydantic/config/hydra/include/default.yaml +2 -0
- supervisor_pydantic/config/hydra/inet_http_server/default.yaml +2 -0
- supervisor_pydantic/config/hydra/program/default.yaml +2 -0
- supervisor_pydantic/config/hydra/rpcinterface/default.yaml +2 -0
- supervisor_pydantic/config/hydra/supervisorctl/default.yaml +2 -0
- supervisor_pydantic/config/hydra/supervisord/default.yaml +3 -0
- supervisor_pydantic/config/hydra/unix_http_server/default.yaml +2 -0
- supervisor_pydantic/config/include.py +26 -0
- supervisor_pydantic/config/inet_http_server.py +33 -0
- supervisor_pydantic/config/program.py +163 -0
- supervisor_pydantic/config/rpcinterface.py +19 -0
- supervisor_pydantic/config/supervisor.py +300 -0
- supervisor_pydantic/config/supervisorctl.py +32 -0
- supervisor_pydantic/config/supervisord.py +82 -0
- supervisor_pydantic/config/unix_http_server.py +32 -0
- supervisor_pydantic/config/utils/__init__.py +1 -0
- supervisor_pydantic/config/utils/relativedelta.py +76 -0
- supervisor_pydantic/convenience/__init__.py +2 -0
- supervisor_pydantic/convenience/__main__.py +4 -0
- supervisor_pydantic/convenience/commands.py +342 -0
- supervisor_pydantic/convenience/common.py +15 -0
- supervisor_pydantic/exceptions.py +6 -0
- supervisor_pydantic/tests/client/test_client_e2e.py +37 -0
- supervisor_pydantic/tests/client/test_client_structs.py +30 -0
- supervisor_pydantic/tests/config/test_config.py +143 -0
- supervisor_pydantic/tests/config/test_eventlistener.py +26 -0
- supervisor_pydantic/tests/config/test_fcgiprogram.py +19 -0
- supervisor_pydantic/tests/config/test_group.py +15 -0
- supervisor_pydantic/tests/config/test_include.py +15 -0
- supervisor_pydantic/tests/config/test_inethttpserver.py +23 -0
- supervisor_pydantic/tests/config/test_program.py +31 -0
- supervisor_pydantic/tests/config/test_rpcinterface.py +11 -0
- supervisor_pydantic/tests/config/test_supervisorctl.py +10 -0
- supervisor_pydantic/tests/config/test_supervisord.py +10 -0
- supervisor_pydantic/tests/config/test_toplevel.py +11 -0
- supervisor_pydantic/tests/config/test_types.py +29 -0
- supervisor_pydantic/tests/config/test_unixhttpserver.py +31 -0
- supervisor_pydantic/tests/conftest.py +88 -0
- supervisor_pydantic/tests/convenience/test_commands.py +24 -0
- supervisor_pydantic/tests/gen_test_pydantic.py +18 -0
- supervisor_pydantic/tests/helpers.py +5 -0
- supervisor_pydantic/tests/hydra/config/inet_http_server/local.yaml +1 -0
- supervisor_pydantic/tests/hydra/config/program/echo.yaml +3 -0
- supervisor_pydantic/tests/hydra/config/program/sleep.yaml +3 -0
- supervisor_pydantic/tests/hydra/config/rpcinterface/standard.yaml +2 -0
- supervisor_pydantic/tests/hydra/test_hydra.py +40 -0
- supervisor_pydantic/tests/test_all.py +6 -0
- supervisor_pydantic/tests/test_nolocal.py +7 -0
- supervisor_pydantic/tests/test_utils.py +7 -0
- supervisor_pydantic/utils.py +11 -0
- supervisor_pydantic-1.0.0.dist-info/METADATA +80 -0
- supervisor_pydantic-1.0.0.dist-info/RECORD +70 -0
- supervisor_pydantic-1.0.0.dist-info/WHEEL +4 -0
- supervisor_pydantic-1.0.0.dist-info/entry_points.txt +2 -0
- supervisor_pydantic-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .xmlrpc import *
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
from xmlrpc.client import Fault, ServerProxy
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from ..config import SupervisorConvenienceConfiguration
|
|
9
|
+
|
|
10
|
+
__all__ = ("SupervisorRemoteXMLRPCClient", "ProcessState", "SupervisorState", "SupervisorMethodResult", "ProcessInfo")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProcessState(Enum):
|
|
14
|
+
STOPPED = 0
|
|
15
|
+
STARTING = 10
|
|
16
|
+
RUNNING = 20
|
|
17
|
+
BACKOFF = 30
|
|
18
|
+
STOPPING = 40
|
|
19
|
+
EXITED = 100
|
|
20
|
+
FATAL = 200
|
|
21
|
+
UNKNOWN = 1000
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def _missing_(cls, code):
|
|
25
|
+
if isinstance(code, str):
|
|
26
|
+
return getattr(cls, code)
|
|
27
|
+
if code not in (0, 10, 20, 30, 40, 100, 200):
|
|
28
|
+
return super().__init__(1000)
|
|
29
|
+
raise ValueError(code)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SupervisorState(Enum):
|
|
33
|
+
FATAL = 2
|
|
34
|
+
RUNNING = 1
|
|
35
|
+
RESTARTING = 0
|
|
36
|
+
SHUTDOWN = -1
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def _missing_(cls, code):
|
|
40
|
+
if isinstance(code, str):
|
|
41
|
+
return getattr(cls, code)
|
|
42
|
+
raise ValueError(code)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SupervisorMethodResult(Enum):
|
|
46
|
+
# duplicated from https://github.com/Supervisor/supervisor/blob/29eeb9dd55c55da2e83c5497d01f3a859998ecf9/supervisor/xmlrpc.py
|
|
47
|
+
UNKNOWN_METHOD = 1
|
|
48
|
+
INCORRECT_PARAMETERS = 2
|
|
49
|
+
BAD_ARGUMENTS = 3
|
|
50
|
+
SIGNATURE_UNSUPPORTED = 4
|
|
51
|
+
SHUTDOWN_STATE = 6
|
|
52
|
+
BAD_NAME = 10
|
|
53
|
+
BAD_SIGNAL = 11
|
|
54
|
+
NO_FILE = 20
|
|
55
|
+
NOT_EXECUTABLE = 21
|
|
56
|
+
FAILED = 30
|
|
57
|
+
ABNORMAL_TERMINATION = 40
|
|
58
|
+
SPAWN_ERROR = 50
|
|
59
|
+
ALREADY_STARTED = 60
|
|
60
|
+
NOT_RUNNING = 70
|
|
61
|
+
SUCCESS = 80
|
|
62
|
+
ALREADY_ADDED = 90
|
|
63
|
+
STILL_RUNNING = 91
|
|
64
|
+
CANT_REREAD = 92
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ProcessInfo(BaseModel):
|
|
68
|
+
name: str
|
|
69
|
+
group: str
|
|
70
|
+
state: ProcessState
|
|
71
|
+
description: str
|
|
72
|
+
start: datetime
|
|
73
|
+
stop: datetime
|
|
74
|
+
now: datetime
|
|
75
|
+
spawner: str = ""
|
|
76
|
+
exitstatus: int
|
|
77
|
+
logfile: str
|
|
78
|
+
stdout_logfile: str
|
|
79
|
+
stderr_logfile: str
|
|
80
|
+
pid: int
|
|
81
|
+
|
|
82
|
+
def running(self):
|
|
83
|
+
return self.state in (ProcessState.RUNNING, ProcessState.STOPPING)
|
|
84
|
+
|
|
85
|
+
def stopped(self):
|
|
86
|
+
return self.state in (ProcessState.STOPPED, ProcessState.EXITED, ProcessState.FATAL)
|
|
87
|
+
|
|
88
|
+
def done(self, ok_exitstatuses=None):
|
|
89
|
+
ok_exitstatuses = ok_exitstatuses or (0,)
|
|
90
|
+
return self.state in (ProcessState.STOPPED,) or (self.state == ProcessState.EXITED and self.exitstatus in ok_exitstatuses)
|
|
91
|
+
|
|
92
|
+
def ok(self, ok_exitstatuses=None):
|
|
93
|
+
ok_exitstatuses = ok_exitstatuses or (0,)
|
|
94
|
+
return self.state in (
|
|
95
|
+
# ProcessState.STARTING,
|
|
96
|
+
ProcessState.RUNNING,
|
|
97
|
+
ProcessState.STOPPING,
|
|
98
|
+
ProcessState.STOPPED,
|
|
99
|
+
) or (self.state == ProcessState.EXITED and self.exitstatus in ok_exitstatuses)
|
|
100
|
+
|
|
101
|
+
def bad(self, ok_exitstatuses=None):
|
|
102
|
+
ok_exitstatuses = ok_exitstatuses or (0,)
|
|
103
|
+
return self.state in (ProcessState.FATAL, ProcessState.UNKNOWN) or (
|
|
104
|
+
self.state == ProcessState.EXITED and self.exitstatus not in ok_exitstatuses
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class SupervisorRemoteXMLRPCClient(object):
|
|
109
|
+
"""A light wrapper over the supervisor xmlrpc api: http://supervisord.org/api.html"""
|
|
110
|
+
|
|
111
|
+
def __init__(self, cfg: SupervisorConvenienceConfiguration):
|
|
112
|
+
self._cfg = cfg
|
|
113
|
+
self._host = cfg.airflow.host
|
|
114
|
+
self._port = int(cfg.airflow.port.split(":")[-1])
|
|
115
|
+
self._protocol = cfg.airflow.protocol
|
|
116
|
+
self._rpcpath = "/" + cfg.airflow.rpcpath if not cfg.airflow.rpcpath.startswith("/") else cfg.airflow.rpcpath
|
|
117
|
+
self._rpcurl = self._build_rpcurl(username=cfg.airflow.username, password=cfg.airflow.password)
|
|
118
|
+
self._client = ServerProxy(self._rpcurl)
|
|
119
|
+
|
|
120
|
+
def _build_rpcurl(self, username: Optional[str], password: Optional[str]) -> str:
|
|
121
|
+
# Forces http or https based on port, otherwise resolves to given protocol
|
|
122
|
+
protocol = {80: "http", 443: "https"}.get(self._port, self._protocol)
|
|
123
|
+
port = "" if self._port in {80, 443} else f":{self._port}"
|
|
124
|
+
authentication = f"{username}:{password.get_secret_value()}@" if username and password else ""
|
|
125
|
+
return f"{protocol}://{authentication}{self._host}{port}{self._rpcpath}"
|
|
126
|
+
|
|
127
|
+
#######################
|
|
128
|
+
# supervisord methods #
|
|
129
|
+
#######################
|
|
130
|
+
def getAllProcessInfo(self) -> List[ProcessInfo]:
|
|
131
|
+
return [ProcessInfo(**_) for _ in self._client.supervisor.getAllProcessInfo()]
|
|
132
|
+
|
|
133
|
+
def getState(self):
|
|
134
|
+
return SupervisorState(self._client.supervisor.getState()["statecode"])
|
|
135
|
+
|
|
136
|
+
# def readLog(self):
|
|
137
|
+
# return self._client.supervisor.readLog(0, 0)
|
|
138
|
+
|
|
139
|
+
def restart(self):
|
|
140
|
+
return self._client.supervisor.restart()
|
|
141
|
+
|
|
142
|
+
def shutdown(self):
|
|
143
|
+
return self._client.supervisor.shutdown()
|
|
144
|
+
|
|
145
|
+
###################
|
|
146
|
+
# process methods #
|
|
147
|
+
###################
|
|
148
|
+
def getProcessInfo(self, name: str):
|
|
149
|
+
if name not in self._cfg.program:
|
|
150
|
+
raise RuntimeError(f"Unknown process: {name}")
|
|
151
|
+
return ProcessInfo(**self._client.supervisor.getProcessInfo(name))
|
|
152
|
+
|
|
153
|
+
def readProcessLog(self, name: str):
|
|
154
|
+
if name not in self._cfg.program:
|
|
155
|
+
raise RuntimeError(f"Unknown process: {name}")
|
|
156
|
+
return self._client.supervisor.readProcessLog(name, 0, 0)
|
|
157
|
+
|
|
158
|
+
def readProcessStderrLog(self, name: str):
|
|
159
|
+
if name not in self._cfg.program:
|
|
160
|
+
raise RuntimeError(f"Unknown process: {name}")
|
|
161
|
+
|
|
162
|
+
return self._client.supervisor.readProcessStderrLog()
|
|
163
|
+
|
|
164
|
+
def readProcessStdoutLog(self, name: str):
|
|
165
|
+
return self._client.supervisor.readProcessStdoutLog()
|
|
166
|
+
|
|
167
|
+
def startAllProcesses(self) -> Dict[str, ProcessInfo]:
|
|
168
|
+
# start all
|
|
169
|
+
self._client.supervisor.startAllProcesses()
|
|
170
|
+
return {name: self.getProcessInfo(name) for name in self._cfg.program}
|
|
171
|
+
|
|
172
|
+
def startProcess(self, name: str) -> ProcessInfo:
|
|
173
|
+
if name not in self._cfg.program:
|
|
174
|
+
raise RuntimeError(f"Unknown process: {name}")
|
|
175
|
+
try:
|
|
176
|
+
if self._client.supervisor.startProcess(name):
|
|
177
|
+
return self.getProcessInfo(name)
|
|
178
|
+
except Fault as f:
|
|
179
|
+
if f.faultCode == SupervisorMethodResult.ALREADY_STARTED.value:
|
|
180
|
+
return self.getProcessInfo(name)
|
|
181
|
+
if f.faultCode == SupervisorMethodResult.SPAWN_ERROR.value:
|
|
182
|
+
return self.getProcessInfo(name)
|
|
183
|
+
raise f
|
|
184
|
+
return self.getProcessInfo(name)
|
|
185
|
+
|
|
186
|
+
def stopAllProcesses(self) -> Dict[str, ProcessInfo]:
|
|
187
|
+
# start all
|
|
188
|
+
self._client.supervisor.stopAllProcesses()
|
|
189
|
+
return {name: self.getProcessInfo(name) for name in self._cfg.program}
|
|
190
|
+
|
|
191
|
+
def stopProcess(self, name: str) -> ProcessInfo:
|
|
192
|
+
if name not in self._cfg.program:
|
|
193
|
+
raise RuntimeError(f"Unknown process: {name}")
|
|
194
|
+
try:
|
|
195
|
+
if self._client.supervisor.stopProcess(name):
|
|
196
|
+
return self.getProcessInfo(name)
|
|
197
|
+
except Fault as f:
|
|
198
|
+
if f.faultCode == SupervisorMethodResult.NOT_RUNNING.value:
|
|
199
|
+
return self.getProcessInfo(name)
|
|
200
|
+
raise f
|
|
201
|
+
return self.getProcessInfo(name)
|
|
202
|
+
|
|
203
|
+
# def reloadConfig(self):
|
|
204
|
+
# return self._client.supervisor.reloadConfig()
|
|
205
|
+
|
|
206
|
+
# def signalAllProcesses(self, signal):
|
|
207
|
+
# return self._client.supervisor.signalAllProcesses()
|
|
208
|
+
|
|
209
|
+
def signalProcess(self, name: str, signal):
|
|
210
|
+
if name not in self._cfg.program:
|
|
211
|
+
raise RuntimeError(f"Unknown process: {name}")
|
|
212
|
+
return self._client.supervisor.signalProcess()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .base import HostPort, LogLevel, Octal, OctalUmask, Signal, SupervisorLocation, UnixUserName, UnixUserNameOrGroup
|
|
2
|
+
from .convenience import ConvenienceConfiguration
|
|
3
|
+
from .eventlistener import EventListenerConfiguration
|
|
4
|
+
from .fcgiprogram import FcgiProgramConfiguration
|
|
5
|
+
from .group import GroupConfiguration
|
|
6
|
+
from .include import IncludeConfiguration
|
|
7
|
+
from .inet_http_server import InetHttpServerConfiguration
|
|
8
|
+
from .program import ProgramConfiguration
|
|
9
|
+
from .rpcinterface import RpcInterfaceConfiguration
|
|
10
|
+
from .supervisor import (
|
|
11
|
+
SupervisorConfiguration,
|
|
12
|
+
SupervisorConvenienceConfiguration,
|
|
13
|
+
load_config,
|
|
14
|
+
load_convenience_config,
|
|
15
|
+
)
|
|
16
|
+
from .supervisorctl import SupervisorctlConfiguration
|
|
17
|
+
from .supervisord import SupervisordConfiguration
|
|
18
|
+
from .unix_http_server import UnixHttpServerConfiguration
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from json import loads
|
|
3
|
+
from typing import Callable, Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from pydantic.functional_validators import AfterValidator
|
|
7
|
+
from typing_extensions import Annotated
|
|
8
|
+
|
|
9
|
+
_un_regex = re.compile(r"^[a-z_]([a-z0-9_-]{0,31}|[a-z0-9_-]{0,30}\$)$")
|
|
10
|
+
_snake_regex = re.compile(r"(?<!^)(?=[A-Z])")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _is_octal(count: int) -> Callable[..., str]:
|
|
14
|
+
def _check_is_octal(v: str) -> str:
|
|
15
|
+
assert len(v) == count
|
|
16
|
+
assert v[0] == "0"
|
|
17
|
+
assert v
|
|
18
|
+
for _ in range(count):
|
|
19
|
+
assert 0 <= int(v[_]) <= 7
|
|
20
|
+
return v
|
|
21
|
+
|
|
22
|
+
return _check_is_octal
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _is_username(v: str) -> str:
|
|
26
|
+
assert re.match(_un_regex, v)
|
|
27
|
+
return v
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _is_username_or_usernamegroup(v: str) -> str:
|
|
31
|
+
splits = v.split(":")
|
|
32
|
+
assert len(splits) == 1 or len(splits) == 2
|
|
33
|
+
assert re.match(_un_regex, splits[0])
|
|
34
|
+
return v
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _is_host_port(v: str) -> str:
|
|
38
|
+
splits = v.split(":")
|
|
39
|
+
assert 0 < int(splits[1]) < 65_535
|
|
40
|
+
return v
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
Octal = Annotated[str, AfterValidator(_is_octal(4))]
|
|
44
|
+
OctalUmask = Annotated[str, AfterValidator(_is_octal(3))]
|
|
45
|
+
UnixUserNameOrGroup = Annotated[str, AfterValidator(_is_username_or_usernamegroup)]
|
|
46
|
+
UnixUserName = Annotated[str, AfterValidator(_is_username)]
|
|
47
|
+
HostPort = Annotated[str, AfterValidator(_is_host_port)]
|
|
48
|
+
LogLevel = Literal["critical", "error", "warn", "info", "debug", "trace", "blather"]
|
|
49
|
+
Signal = Literal["TERM", "HUP", "INT", "QUIT", "KILL", "USR1", "USR2"]
|
|
50
|
+
EventType = Literal[
|
|
51
|
+
"PROCESS_STATE",
|
|
52
|
+
"PROCESS_STATE_STARTING",
|
|
53
|
+
"PROCESS_STATE_RUNNING",
|
|
54
|
+
"PROCESS_STATE_BACKOFF",
|
|
55
|
+
"PROCESS_STATE_STOPPING",
|
|
56
|
+
"PROCESS_STATE_EXITED",
|
|
57
|
+
"PROCESS_STATE_STOPPED",
|
|
58
|
+
"PROCESS_STATE_FATAL",
|
|
59
|
+
"PROCESS_STATE_UNKNOWN",
|
|
60
|
+
"REMOTE_COMMUNICATION",
|
|
61
|
+
"PROCESS_LOG",
|
|
62
|
+
"PROCESS_LOG_STDOUT",
|
|
63
|
+
"PROCESS_LOG_STDERR",
|
|
64
|
+
"PROCESS_COMMUNICATION",
|
|
65
|
+
"PROCESS_COMMUNICATION_STDOUT",
|
|
66
|
+
"PROCESS_COMMUNICATION_STDERR",
|
|
67
|
+
"SUPERVISOR_STATE_CHANGE",
|
|
68
|
+
"SUPERVISOR_STATE_CHANGE_RUNNING",
|
|
69
|
+
"SUPERVISOR_STATE_CHANGE_STOPPING",
|
|
70
|
+
"TICK",
|
|
71
|
+
"TICK_5",
|
|
72
|
+
"TICK_60",
|
|
73
|
+
"TICK_3600",
|
|
74
|
+
"PROCESS_GROUP",
|
|
75
|
+
"PROCESS_GROUP_ADDED",
|
|
76
|
+
"PROCESS_GROUP_REMOVED",
|
|
77
|
+
]
|
|
78
|
+
SupervisorLocation = Literal["local", "remote"]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class _BaseCfgModel(BaseModel):
|
|
82
|
+
def to_cfg(self, key: str = "") -> str:
|
|
83
|
+
ret = f"[{_snake_regex.sub('_', self.__class__.__name__.replace('Configuration', '')).lower()}{':' + key if key else ''}]"
|
|
84
|
+
# round trip to json so we're fully
|
|
85
|
+
# cfg-compatible
|
|
86
|
+
for k, v in loads(self.model_dump_json()).items():
|
|
87
|
+
if v is not None:
|
|
88
|
+
if isinstance(v, bool):
|
|
89
|
+
ret += f"\n{k}={str(v).lower()}"
|
|
90
|
+
else:
|
|
91
|
+
ret += f"\n{k}={v}"
|
|
92
|
+
return ret.strip() + "\n"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, SecretStr, field_serializer, field_validator, model_validator
|
|
4
|
+
|
|
5
|
+
from .base import HostPort, Signal, SupervisorLocation, UnixUserName, _BaseCfgModel
|
|
6
|
+
|
|
7
|
+
__all__ = ("ConvenienceConfiguration",)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConvenienceConfiguration(_BaseCfgModel):
|
|
11
|
+
"""Convenience layer, settings that MUST be set when running via convenience API"""
|
|
12
|
+
|
|
13
|
+
############
|
|
14
|
+
# programs #
|
|
15
|
+
############
|
|
16
|
+
# autostart = False
|
|
17
|
+
# autorestart = False
|
|
18
|
+
startsecs: Optional[int] = Field(
|
|
19
|
+
default=1,
|
|
20
|
+
description="The total number of seconds which the program needs to stay running after a startup to consider the start successful (moving the process from the STARTING state to the RUNNING state). Set to 0 to indicate that the program needn’t stay running for any particular amount of time. Even if a process exits with an “expected” exit code (see exitcodes), the start will still be considered a failure if the process exits quicker than startsecs.",
|
|
21
|
+
)
|
|
22
|
+
startretries: Optional[int] = Field(
|
|
23
|
+
default=None,
|
|
24
|
+
description="The number of serial failure attempts that supervisord will allow when attempting to start the program before giving up and putting the process into an FATAL state. After each failed restart, process will be put in BACKOFF state and each retry attempt will take increasingly more time.",
|
|
25
|
+
)
|
|
26
|
+
exitcodes: Optional[List[int]] = Field(
|
|
27
|
+
default=[0],
|
|
28
|
+
description="The list of “expected” exit codes for this program used with autorestart. If the autorestart parameter is set to unexpected, and the process exits in any other way than as a result of a supervisor stop request, supervisord will restart the process if it exits with an exit code that is not defined in this list.",
|
|
29
|
+
)
|
|
30
|
+
stopsignal: Optional[Signal] = Field(
|
|
31
|
+
default="TERM",
|
|
32
|
+
description="The signal used to kill the program when a stop is requested. This can be specified using the signal’s name or its number. It is normally one of: TERM, HUP, INT, QUIT, KILL, USR1, or USR2.",
|
|
33
|
+
)
|
|
34
|
+
stopwaitsecs: Optional[int] = Field(
|
|
35
|
+
default=30,
|
|
36
|
+
description="The number of seconds to wait for the OS to return a SIGCHLD to supervisord after the program has been sent a stopsignal. If this number of seconds elapses before supervisord receives a SIGCHLD from the process, supervisord will attempt to kill it with a final SIGKILL.",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
####################
|
|
40
|
+
# inet_http_server #
|
|
41
|
+
####################
|
|
42
|
+
# port not optional
|
|
43
|
+
port: HostPort = Field(
|
|
44
|
+
default="*:9001",
|
|
45
|
+
description="A TCP host:port value or (e.g. 127.0.0.1:9001) on which supervisor will listen for HTTP/XML-RPC requests. supervisorctl will use XML-RPC to communicate with supervisord over this port. To listen on all interfaces in the machine, use :9001 or *:9001. Please read the security warning above.",
|
|
46
|
+
)
|
|
47
|
+
username: Optional[UnixUserName] = Field(default=None, description="The username required for authentication to the HTTP/Unix Server.")
|
|
48
|
+
password: Optional[SecretStr] = Field(
|
|
49
|
+
default=None,
|
|
50
|
+
description="The password required for authentication to the HTTP/Unix server. This can be a cleartext password, or can be specified as a SHA-1 hash if prefixed by the string {SHA}. For example, {SHA}82ab876d1387bfafe46cc1c8a2ef074eae50cb1d is the SHA-stored version of the password “thepassword”. Note that hashed password must be in hex format.",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
#################
|
|
54
|
+
# rpc_interface #
|
|
55
|
+
###################
|
|
56
|
+
rpcinterface_factory: str = Field(
|
|
57
|
+
default="supervisor.rpcinterface:make_main_rpcinterface",
|
|
58
|
+
description="pkg_resources “entry point” dotted name to your RPC interface’s factory function.",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
#########
|
|
62
|
+
# Other #
|
|
63
|
+
#########
|
|
64
|
+
local_or_remote: Optional[SupervisorLocation] = Field(
|
|
65
|
+
default="local",
|
|
66
|
+
description="Location of supervisor, either local for same-machine or remote. If same-machine, communicates via Unix sockets by default, if remote, communicates via inet http server",
|
|
67
|
+
)
|
|
68
|
+
host: str = Field(
|
|
69
|
+
default="localhost",
|
|
70
|
+
description="Hostname of the supervisor host. Used by the XMLRPC client",
|
|
71
|
+
)
|
|
72
|
+
protocol: str = Field(
|
|
73
|
+
default="http",
|
|
74
|
+
description="Protocol of the supervisor XMLRPC HTTP API. Used by the XMLRPC client",
|
|
75
|
+
)
|
|
76
|
+
rpcpath: str = Field(
|
|
77
|
+
default="/RPC2",
|
|
78
|
+
description="Path for supervisor XMLRPC HTTP API. Used by the XMLRPC client",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@model_validator(mode="after")
|
|
82
|
+
def validate_remote_accessible(self):
|
|
83
|
+
if self.local_or_remote == "remote" and self.port.startswith(("localhost", "127.0.0.1")):
|
|
84
|
+
raise ValueError("Supervisor binds only to loopback (localhost/127.0.0.1), but asked for remote")
|
|
85
|
+
if self.local_or_remote == "remote" and self.host.startswith(("localhost", "127.0.0.1")):
|
|
86
|
+
raise ValueError("Supervisor client expecting hostname, got localhost/127.0.0.1")
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
@field_serializer("exitcodes", when_used="json")
|
|
90
|
+
def _dump_exitcodes(self, v):
|
|
91
|
+
if v:
|
|
92
|
+
return ",".join(str(_) for _ in v)
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
@field_validator("exitcodes", mode="before")
|
|
96
|
+
@classmethod
|
|
97
|
+
def _load_exitcodes(cls, v):
|
|
98
|
+
if isinstance(v, str):
|
|
99
|
+
v = v.split(",")
|
|
100
|
+
if isinstance(v, list):
|
|
101
|
+
return [int(_) for _ in v]
|
|
102
|
+
return v
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, field_serializer, field_validator
|
|
4
|
+
|
|
5
|
+
from .base import EventType
|
|
6
|
+
from .program import ProgramConfiguration
|
|
7
|
+
|
|
8
|
+
__all__ = ("EventListenerConfiguration",)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EventListenerConfiguration(ProgramConfiguration):
|
|
12
|
+
def to_cfg(self, key: str) -> str:
|
|
13
|
+
# Overload to require key
|
|
14
|
+
return super().to_cfg(key=key).replace("[event_listener", "[eventlistener")
|
|
15
|
+
|
|
16
|
+
buffer_size: Optional[int] = Field(
|
|
17
|
+
default=None,
|
|
18
|
+
description="The event listener pool’s event queue buffer size. When a listener pool’s event buffer is overflowed (as can happen when an event listener pool cannot keep up with all of the events sent to it), the oldest event in the buffer is discarded.",
|
|
19
|
+
)
|
|
20
|
+
events: Optional[List[EventType]] = Field(
|
|
21
|
+
default=None,
|
|
22
|
+
description="A comma-separated list of event type names that this listener is “interested” in receiving notifications for (see Event Types for a list of valid event type names).",
|
|
23
|
+
)
|
|
24
|
+
result_handler: Optional[str] = Field(
|
|
25
|
+
default=None,
|
|
26
|
+
description="A pkg_resources entry point string that resolves to a Python callable. The default value is supervisor.dispatchers:default_handler. Specifying an alternate result handler is a very uncommon thing to need to do, and as a result, how to create one is not documented.",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# TODO can't inherit stdout_capture_maxbytes
|
|
30
|
+
@field_validator("stdout_capture_maxbytes")
|
|
31
|
+
@classmethod
|
|
32
|
+
def _event_listener_cant_use_stdout_capture_maxbytes(cls, v: str) -> str:
|
|
33
|
+
if v:
|
|
34
|
+
raise ValueError("eventlistener cannot use stdout_capture_maxbytes")
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
@field_serializer("events", when_used="json")
|
|
38
|
+
def _dump_events(self, v):
|
|
39
|
+
if v:
|
|
40
|
+
return ",".join(v)
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
@field_validator("events", mode="before")
|
|
44
|
+
@classmethod
|
|
45
|
+
def _load_events(cls, v):
|
|
46
|
+
if isinstance(v, str):
|
|
47
|
+
return v.split(",")
|
|
48
|
+
return v
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from .base import Octal, UnixUserNameOrGroup
|
|
6
|
+
from .program import ProgramConfiguration
|
|
7
|
+
|
|
8
|
+
__all__ = ("FcgiProgramConfiguration",)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FcgiProgramConfiguration(ProgramConfiguration):
|
|
12
|
+
def to_cfg(self, key: str) -> str:
|
|
13
|
+
# Overload to require key
|
|
14
|
+
return super().to_cfg(key=key).replace("[fcgi_program", "[fcgi-program")
|
|
15
|
+
|
|
16
|
+
socket: str = Field(
|
|
17
|
+
description="The FastCGI socket for this program, either TCP or UNIX domain socket. For TCP sockets, use this format: tcp://localhost:9002. For UNIX domain sockets, use unix:///absolute/path/to/file.sock. String expressions are evaluated against a dictionary containing the keys “program_name” and “here” (the directory of the supervisord config file)."
|
|
18
|
+
)
|
|
19
|
+
socket_backlog: Optional[str] = Field(default=None, description="Sets socket listen(2) backlog.")
|
|
20
|
+
socket_owner: Optional[UnixUserNameOrGroup] = Field(
|
|
21
|
+
default=None,
|
|
22
|
+
description="For UNIX domain sockets, this parameter can be used to specify the user and group for the FastCGI socket. May be a UNIX username (e.g. chrism) or a UNIX username and group separated by a colon (e.g. chrism:wheel).",
|
|
23
|
+
)
|
|
24
|
+
socket_mode: Optional[Octal] = Field(
|
|
25
|
+
default=None, description="For UNIX domain sockets, this parameter can be used to specify the permission mode."
|
|
26
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, field_serializer, field_validator
|
|
4
|
+
|
|
5
|
+
from .base import _BaseCfgModel
|
|
6
|
+
|
|
7
|
+
__all__ = ("GroupConfiguration",)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GroupConfiguration(_BaseCfgModel):
|
|
11
|
+
def to_cfg(self, key: str) -> str:
|
|
12
|
+
# Overload to require key
|
|
13
|
+
return super().to_cfg(key=key)
|
|
14
|
+
|
|
15
|
+
programs: List[str] = Field(description="A comma-separated list of program names. The programs which are listed become members of the group.")
|
|
16
|
+
priority: Optional[int] = Field(default=None, description="A priority number analogous to a [program:x] priority value assigned to the group.")
|
|
17
|
+
|
|
18
|
+
@field_serializer("programs", when_used="json")
|
|
19
|
+
def _dump_programs(self, v):
|
|
20
|
+
if v:
|
|
21
|
+
return ",".join(v)
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
@field_validator("programs", mode="before")
|
|
25
|
+
@classmethod
|
|
26
|
+
def _load_programs(cls, v):
|
|
27
|
+
if isinstance(v, str):
|
|
28
|
+
return v.split(",")
|
|
29
|
+
return v
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @package _global_
|
|
2
|
+
|
|
3
|
+
defaults:
|
|
4
|
+
# https://hydra.cc/docs/configure_hydra/logging/
|
|
5
|
+
- supervisord/default
|
|
6
|
+
- override hydra/job_logging: custom
|
|
7
|
+
- override hydra/hydra_logging: custom
|
|
8
|
+
- _self_
|
|
9
|
+
|
|
10
|
+
_target_: supervisor_pydantic.SupervisorConfiguration
|
|
11
|
+
unix_http_server:
|
|
12
|
+
pinet_http_server:
|
|
13
|
+
include:
|
|
14
|
+
program:
|
|
15
|
+
group:
|
|
16
|
+
fcgiprogram:
|
|
17
|
+
eventlistener:
|
|
18
|
+
rpcinterface:
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
formatters:
|
|
3
|
+
simple:
|
|
4
|
+
format: '[%(levelname)s] - %(message)s'
|
|
5
|
+
colorlog:
|
|
6
|
+
'()': 'colorlog.ColoredFormatter'
|
|
7
|
+
format: '[%(cyan)s%(asctime)s%(reset)s][%(threadName)s][%(blue)s%(name)s%(reset)s][%(log_color)s%(levelname)s%(reset)s]: %(message)s'
|
|
8
|
+
log_colors:
|
|
9
|
+
DEBUG: cyan
|
|
10
|
+
INFO: green
|
|
11
|
+
WARNING: yellow
|
|
12
|
+
ERROR: red
|
|
13
|
+
CRITICAL: red
|
|
14
|
+
handlers:
|
|
15
|
+
console:
|
|
16
|
+
class: logging.StreamHandler
|
|
17
|
+
formatter: colorlog
|
|
18
|
+
level: INFO
|
|
19
|
+
stream: ext://sys.stdout
|
|
20
|
+
root:
|
|
21
|
+
handlers: [console]
|
|
22
|
+
level: INFO
|
|
23
|
+
|
|
24
|
+
disable_existing_loggers: false
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
formatters:
|
|
3
|
+
simple:
|
|
4
|
+
format: '[%(levelname)s] - %(message)s'
|
|
5
|
+
colorlog:
|
|
6
|
+
'()': 'colorlog.ColoredFormatter'
|
|
7
|
+
format: '[%(cyan)s%(asctime)s%(reset)s][%(threadName)s][%(blue)s%(name)s%(reset)s][%(log_color)s%(levelname)s%(reset)s]: %(message)s'
|
|
8
|
+
log_colors:
|
|
9
|
+
DEBUG: cyan
|
|
10
|
+
INFO: green
|
|
11
|
+
WARNING: yellow
|
|
12
|
+
ERROR: red
|
|
13
|
+
CRITICAL: red
|
|
14
|
+
handlers:
|
|
15
|
+
console:
|
|
16
|
+
class: logging.StreamHandler
|
|
17
|
+
formatter: colorlog
|
|
18
|
+
level: INFO
|
|
19
|
+
stream: ext://sys.stdout
|
|
20
|
+
root:
|
|
21
|
+
handlers: [console]
|
|
22
|
+
level: INFO
|
|
23
|
+
|
|
24
|
+
disable_existing_loggers: false
|