systemd-pydantic 0.1.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.
- systemd_pydantic/__init__.py +17 -0
- systemd_pydantic/client/__init__.py +19 -0
- systemd_pydantic/client/systemctl.py +165 -0
- systemd_pydantic/config/__init__.py +10 -0
- systemd_pydantic/config/convenience.py +45 -0
- systemd_pydantic/config/hydra/base.yaml +5 -0
- systemd_pydantic/config/systemd.py +182 -0
- systemd_pydantic/convenience/__init__.py +21 -0
- systemd_pydantic/convenience/commands.py +109 -0
- systemd_pydantic/convenience/common.py +12 -0
- systemd_pydantic/models.py +367 -0
- systemd_pydantic/tests/hydra/config/systemd.yaml +8 -0
- systemd_pydantic/tests/test_client.py +77 -0
- systemd_pydantic/tests/test_commands.py +53 -0
- systemd_pydantic/tests/test_config.py +65 -0
- systemd_pydantic/tests/test_load.py +20 -0
- systemd_pydantic/tests/test_models.py +111 -0
- systemd_pydantic-0.1.0.dist-info/METADATA +140 -0
- systemd_pydantic-0.1.0.dist-info/RECORD +22 -0
- systemd_pydantic-0.1.0.dist-info/WHEEL +4 -0
- systemd_pydantic-0.1.0.dist-info/entry_points.txt +2 -0
- systemd_pydantic-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .client import *
|
|
2
|
+
from .config import *
|
|
3
|
+
from .convenience import *
|
|
4
|
+
from .models import (
|
|
5
|
+
InstallConfiguration,
|
|
6
|
+
KillMode,
|
|
7
|
+
RestartPolicy,
|
|
8
|
+
ServiceConfiguration,
|
|
9
|
+
ServiceType,
|
|
10
|
+
ServiceUnitConfiguration,
|
|
11
|
+
TimerConfiguration,
|
|
12
|
+
TimerUnitConfiguration,
|
|
13
|
+
TimeSpan,
|
|
14
|
+
UnitConfiguration,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .systemctl import (
|
|
2
|
+
CommandResult,
|
|
3
|
+
CommandRunner,
|
|
4
|
+
SSHCommandRunner,
|
|
5
|
+
SubprocessCommandRunner,
|
|
6
|
+
SystemdClient,
|
|
7
|
+
SystemdCommandError,
|
|
8
|
+
UnitInfo,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
"CommandResult",
|
|
13
|
+
"CommandRunner",
|
|
14
|
+
"SSHCommandRunner",
|
|
15
|
+
"SubprocessCommandRunner",
|
|
16
|
+
"SystemdClient",
|
|
17
|
+
"SystemdCommandError",
|
|
18
|
+
"UnitInfo",
|
|
19
|
+
)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from subprocess import run
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ..config import SystemdConfiguration
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class CommandResult:
|
|
15
|
+
command: list[str]
|
|
16
|
+
returncode: int
|
|
17
|
+
stdout: str = ""
|
|
18
|
+
stderr: str = ""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CommandRunner(Protocol):
|
|
22
|
+
def run(self, command: list[str], timeout: int) -> CommandResult: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SubprocessCommandRunner:
|
|
26
|
+
def run(self, command: list[str], timeout: int) -> CommandResult:
|
|
27
|
+
result = run(command, capture_output=True, text=True, timeout=timeout, check=False)
|
|
28
|
+
return CommandResult(command=command, returncode=result.returncode, stdout=result.stdout, stderr=result.stderr)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SSHCommandRunner:
|
|
32
|
+
def __init__(self, host: str, runner: CommandRunner | None = None):
|
|
33
|
+
self.host = host
|
|
34
|
+
self.runner = runner or SubprocessCommandRunner()
|
|
35
|
+
|
|
36
|
+
def run(self, command: list[str], timeout: int) -> CommandResult:
|
|
37
|
+
return self.runner.run(["ssh", self.host, *command], timeout)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SystemdCommandError(RuntimeError):
|
|
41
|
+
def __init__(self, result: CommandResult):
|
|
42
|
+
self.result = result
|
|
43
|
+
detail = result.stderr.strip() or result.stdout.strip()
|
|
44
|
+
super().__init__(f"command failed ({result.returncode}): {' '.join(result.command)}{': ' + detail if detail else ''}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class UnitInfo(BaseModel):
|
|
48
|
+
model_config = ConfigDict(extra="ignore")
|
|
49
|
+
|
|
50
|
+
name: str
|
|
51
|
+
load_state: str = ""
|
|
52
|
+
active_state: str = ""
|
|
53
|
+
sub_state: str = ""
|
|
54
|
+
result: str = ""
|
|
55
|
+
exec_main_code: str = ""
|
|
56
|
+
exec_main_status: int | None = None
|
|
57
|
+
|
|
58
|
+
def running(self) -> bool:
|
|
59
|
+
return self.active_state in {"active", "activating", "reloading"}
|
|
60
|
+
|
|
61
|
+
def stopped(self) -> bool:
|
|
62
|
+
return self.active_state in {"inactive", "deactivating"}
|
|
63
|
+
|
|
64
|
+
def done(self, ok_exitstatuses: list[int | str] | None = None) -> bool:
|
|
65
|
+
statuses = {str(value) for value in (ok_exitstatuses or [0])}
|
|
66
|
+
return self.stopped() and self.result in {"", "success"} and str(self.exec_main_status or 0) in statuses
|
|
67
|
+
|
|
68
|
+
def ok(self, ok_exitstatuses: list[int | str] | None = None) -> bool:
|
|
69
|
+
return self.running() or self.done(ok_exitstatuses)
|
|
70
|
+
|
|
71
|
+
def bad(self, ok_exitstatuses: list[int | str] | None = None) -> bool:
|
|
72
|
+
return self.active_state == "failed" or (self.stopped() and not self.done(ok_exitstatuses))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class SystemdClient:
|
|
76
|
+
_SHOW_PROPERTIES = ("LoadState", "ActiveState", "SubState", "Result", "ExecMainCode", "ExecMainStatus")
|
|
77
|
+
|
|
78
|
+
def __init__(self, cfg: SystemdConfiguration, runner: CommandRunner | None = None):
|
|
79
|
+
self.cfg = cfg
|
|
80
|
+
self.runner = runner or SubprocessCommandRunner()
|
|
81
|
+
self.timeout = getattr(cfg, "command_timeout", 60)
|
|
82
|
+
|
|
83
|
+
def _run(self, *args: str, check: bool = True) -> CommandResult:
|
|
84
|
+
command = ["systemctl"]
|
|
85
|
+
if self.cfg.scope == "user":
|
|
86
|
+
command.append("--user")
|
|
87
|
+
result = self.runner.run([*command, *args], self.timeout)
|
|
88
|
+
if check and result.returncode != 0:
|
|
89
|
+
raise SystemdCommandError(result)
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
def daemon_reload(self) -> CommandResult:
|
|
93
|
+
return self._run("daemon-reload")
|
|
94
|
+
|
|
95
|
+
def enable_units(self, names: list[str] | None = None) -> CommandResult:
|
|
96
|
+
return self._run("enable", *(names if names is not None else [*self.cfg.service_names, *self.cfg.timer_names]))
|
|
97
|
+
|
|
98
|
+
def disable_units(self, names: list[str] | None = None) -> CommandResult:
|
|
99
|
+
return self._run("disable", *(names if names is not None else [*self.cfg.service_names, *self.cfg.timer_names]))
|
|
100
|
+
|
|
101
|
+
def start_units(self, names: list[str]) -> dict[str, UnitInfo]:
|
|
102
|
+
if names:
|
|
103
|
+
self._run("start", *names)
|
|
104
|
+
return self.get_units_info(names)
|
|
105
|
+
|
|
106
|
+
def stop_units(self, names: list[str]) -> dict[str, UnitInfo]:
|
|
107
|
+
if names:
|
|
108
|
+
self._run("stop", *names)
|
|
109
|
+
return self.get_units_info(names)
|
|
110
|
+
|
|
111
|
+
def restart_units(self, names: list[str]) -> dict[str, UnitInfo]:
|
|
112
|
+
if names:
|
|
113
|
+
self._run("restart", *names)
|
|
114
|
+
return self.get_units_info(names)
|
|
115
|
+
|
|
116
|
+
def kill_units(self, names: list[str]) -> dict[str, UnitInfo]:
|
|
117
|
+
if names:
|
|
118
|
+
self._run("kill", "--signal=SIGKILL", *names)
|
|
119
|
+
return self.get_units_info(names)
|
|
120
|
+
|
|
121
|
+
def start_services(self) -> dict[str, UnitInfo]:
|
|
122
|
+
return self.start_units(self.cfg.service_names)
|
|
123
|
+
|
|
124
|
+
def stop_services(self) -> dict[str, UnitInfo]:
|
|
125
|
+
return self.stop_units(self.cfg.service_names)
|
|
126
|
+
|
|
127
|
+
def restart_services(self) -> dict[str, UnitInfo]:
|
|
128
|
+
return self.restart_units(self.cfg.service_names)
|
|
129
|
+
|
|
130
|
+
def kill_services(self) -> dict[str, UnitInfo]:
|
|
131
|
+
return self.kill_units(self.cfg.service_names)
|
|
132
|
+
|
|
133
|
+
def start_timers(self) -> dict[str, UnitInfo]:
|
|
134
|
+
return self.start_units(self.cfg.timer_names)
|
|
135
|
+
|
|
136
|
+
def stop_timers(self) -> dict[str, UnitInfo]:
|
|
137
|
+
return self.stop_units(self.cfg.timer_names)
|
|
138
|
+
|
|
139
|
+
def get_unit_info(self, name: str) -> UnitInfo:
|
|
140
|
+
properties = [f"--property={value}" for value in self._SHOW_PROPERTIES]
|
|
141
|
+
result = self._run("show", name, *properties, check=False)
|
|
142
|
+
values = {}
|
|
143
|
+
for line in result.stdout.splitlines():
|
|
144
|
+
key, separator, value = line.partition("=")
|
|
145
|
+
if separator:
|
|
146
|
+
values[key] = value
|
|
147
|
+
status = values.get("ExecMainStatus", "")
|
|
148
|
+
return UnitInfo(
|
|
149
|
+
name=name,
|
|
150
|
+
load_state=values.get("LoadState", "not-found" if result.returncode else ""),
|
|
151
|
+
active_state=values.get("ActiveState", "failed" if result.returncode else ""),
|
|
152
|
+
sub_state=values.get("SubState", ""),
|
|
153
|
+
result=values.get("Result", ""),
|
|
154
|
+
exec_main_code=values.get("ExecMainCode", ""),
|
|
155
|
+
exec_main_status=int(status) if status.lstrip("-").isdigit() else None,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def get_units_info(self, names: list[str]) -> dict[str, UnitInfo]:
|
|
159
|
+
return {name: self.get_unit_info(name) for name in names}
|
|
160
|
+
|
|
161
|
+
def get_all_service_info(self) -> dict[str, UnitInfo]:
|
|
162
|
+
return self.get_units_info(self.cfg.service_names)
|
|
163
|
+
|
|
164
|
+
def get_all_timer_info(self) -> dict[str, UnitInfo]:
|
|
165
|
+
return self.get_units_info(self.cfg.timer_names)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .convenience import SystemdConvenienceConfiguration, load_convenience_config
|
|
2
|
+
from .systemd import SystemdConfiguration, SystemdScope, load_config
|
|
3
|
+
|
|
4
|
+
__all__ = (
|
|
5
|
+
"SystemdConfiguration",
|
|
6
|
+
"SystemdConvenienceConfiguration",
|
|
7
|
+
"SystemdScope",
|
|
8
|
+
"load_config",
|
|
9
|
+
"load_convenience_config",
|
|
10
|
+
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, PrivateAttr, model_validator
|
|
4
|
+
|
|
5
|
+
from ..models import KillMode, RestartPolicy, TimeSpan
|
|
6
|
+
from .systemd import SystemdConfiguration
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SystemdConvenienceConfiguration(SystemdConfiguration):
|
|
10
|
+
"""Systemd defaults and persisted state used by convenience commands."""
|
|
11
|
+
|
|
12
|
+
_pydantic_path: Path = PrivateAttr(default=Path("pydantic.json"))
|
|
13
|
+
|
|
14
|
+
restart: RestartPolicy = "no"
|
|
15
|
+
timeout_stop_sec: TimeSpan = "30s"
|
|
16
|
+
kill_mode: KillMode = "control-group"
|
|
17
|
+
kill_signal: str = "SIGTERM"
|
|
18
|
+
success_exit_status: list[int | str] = [0]
|
|
19
|
+
command_timeout: int = Field(default=60, ge=1)
|
|
20
|
+
|
|
21
|
+
@model_validator(mode="after")
|
|
22
|
+
def _set_convenience_defaults(self) -> "SystemdConvenienceConfiguration":
|
|
23
|
+
for unit in self.service.values():
|
|
24
|
+
service = unit.service
|
|
25
|
+
service.restart = service.restart or self.restart
|
|
26
|
+
service.timeout_stop_sec = service.timeout_stop_sec or self.timeout_stop_sec
|
|
27
|
+
service.kill_mode = service.kill_mode or self.kill_mode
|
|
28
|
+
service.kill_signal = service.kill_signal or self.kill_signal
|
|
29
|
+
service.success_exit_status = service.success_exit_status or self.success_exit_status
|
|
30
|
+
assert self.working_dir is not None
|
|
31
|
+
self._pydantic_path = self.working_dir / "pydantic.json"
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
def _write_self(self) -> None:
|
|
35
|
+
assert self.working_dir is not None
|
|
36
|
+
self.working_dir.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
self.write()
|
|
38
|
+
self._pydantic_path.write_text(self.model_dump_json(exclude_unset=True))
|
|
39
|
+
|
|
40
|
+
def rmdir(self) -> None:
|
|
41
|
+
self._pydantic_path.unlink(missing_ok=True)
|
|
42
|
+
super().rmdir()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
load_convenience_config = SystemdConvenienceConfiguration.load
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import os
|
|
5
|
+
from getpass import getuser
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from tempfile import gettempdir
|
|
8
|
+
from typing import TYPE_CHECKING, Literal, Self
|
|
9
|
+
|
|
10
|
+
from hydra import compose, initialize_config_dir
|
|
11
|
+
from hydra.utils import instantiate
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
13
|
+
|
|
14
|
+
from ..models import ServiceUnitConfiguration, TimerUnitConfiguration
|
|
15
|
+
|
|
16
|
+
SystemdScope = Literal["system", "user"]
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from ..client import UnitInfo
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _calling_file(offset: int) -> str:
|
|
23
|
+
return inspect.stack()[offset].filename
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _unit_filename(name: str, suffix: str) -> str:
|
|
27
|
+
if "/" in name or name in {"", ".", ".."}:
|
|
28
|
+
raise ValueError(f"invalid unit name: {name!r}")
|
|
29
|
+
if name.endswith(suffix):
|
|
30
|
+
return name
|
|
31
|
+
if "." in name:
|
|
32
|
+
raise ValueError(f"unit {name!r} must end in {suffix}")
|
|
33
|
+
return f"{name}{suffix}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SystemdConfiguration(BaseModel):
|
|
37
|
+
"""Named collection of systemd service and timer unit files."""
|
|
38
|
+
|
|
39
|
+
model_config = ConfigDict(extra="forbid", validate_assignment=True)
|
|
40
|
+
|
|
41
|
+
service: dict[str, ServiceUnitConfiguration]
|
|
42
|
+
timer: dict[str, TimerUnitConfiguration] = Field(default_factory=dict)
|
|
43
|
+
unit_dir: Path | None = None
|
|
44
|
+
working_dir: Path | None = None
|
|
45
|
+
scope: SystemdScope = "system"
|
|
46
|
+
|
|
47
|
+
@model_validator(mode="after")
|
|
48
|
+
def _set_paths(self) -> SystemdConfiguration:
|
|
49
|
+
names = sorted([*self.service, *self.timer])
|
|
50
|
+
label = "-".join(names) or "units"
|
|
51
|
+
if self.working_dir is None:
|
|
52
|
+
object.__setattr__(self, "working_dir", Path(gettempdir()).resolve() / f"systemd-{getuser()}-{label}")
|
|
53
|
+
if self.unit_dir is None:
|
|
54
|
+
object.__setattr__(
|
|
55
|
+
self,
|
|
56
|
+
"unit_dir",
|
|
57
|
+
Path("/etc/systemd/system") if self.scope == "system" else Path.home() / ".config/systemd/user",
|
|
58
|
+
)
|
|
59
|
+
for name in self.service:
|
|
60
|
+
_unit_filename(name, ".service")
|
|
61
|
+
for name in self.timer:
|
|
62
|
+
_unit_filename(name, ".timer")
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def to_cfg(self) -> dict[str, str]:
|
|
66
|
+
rendered = {_unit_filename(name, ".service"): config.to_cfg() for name, config in self.service.items()}
|
|
67
|
+
rendered.update({_unit_filename(name, ".timer"): config.to_cfg() for name, config in self.timer.items()})
|
|
68
|
+
return rendered
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def service_names(self) -> list[str]:
|
|
72
|
+
return [_unit_filename(name, ".service") for name in self.service]
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def timer_names(self) -> list[str]:
|
|
76
|
+
return [_unit_filename(name, ".timer") for name in self.timer]
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def unit_paths(self) -> list[Path]:
|
|
80
|
+
assert self.unit_dir is not None
|
|
81
|
+
return [self.unit_dir / name for name in self.to_cfg()]
|
|
82
|
+
|
|
83
|
+
def write(self) -> list[Path]:
|
|
84
|
+
assert self.unit_dir is not None
|
|
85
|
+
self.unit_dir.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
paths = []
|
|
87
|
+
for filename, contents in self.to_cfg().items():
|
|
88
|
+
path = self.unit_dir / filename
|
|
89
|
+
path.write_text(contents)
|
|
90
|
+
paths.append(path)
|
|
91
|
+
return paths
|
|
92
|
+
|
|
93
|
+
def rmdir(self) -> None:
|
|
94
|
+
for path in self.unit_paths:
|
|
95
|
+
path.unlink(missing_ok=True)
|
|
96
|
+
assert self.working_dir is not None
|
|
97
|
+
if self.working_dir.exists() and not any(self.working_dir.iterdir()):
|
|
98
|
+
self.working_dir.rmdir()
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def _find_parent_config_folder(
|
|
102
|
+
cls,
|
|
103
|
+
config_dir: str = "config",
|
|
104
|
+
config_name: str = "",
|
|
105
|
+
*,
|
|
106
|
+
basepath: str = "",
|
|
107
|
+
_offset: int = 2,
|
|
108
|
+
) -> tuple[Path, Path, Path | str]:
|
|
109
|
+
if basepath:
|
|
110
|
+
calling_file = Path(basepath) if basepath.endswith((".py", ".yml", ".yaml")) else Path(basepath) / "dummy.py"
|
|
111
|
+
else:
|
|
112
|
+
calling_file = Path(_calling_file(_offset))
|
|
113
|
+
folder = calling_file.parent.resolve()
|
|
114
|
+
|
|
115
|
+
while True:
|
|
116
|
+
candidate = folder / config_dir
|
|
117
|
+
if not config_name and candidate.exists():
|
|
118
|
+
return folder, candidate.resolve(), ""
|
|
119
|
+
for suffix in (".yml", ".yaml"):
|
|
120
|
+
path = candidate / f"{config_name}{suffix}"
|
|
121
|
+
if config_name and path.exists():
|
|
122
|
+
return folder, candidate.resolve(), path.resolve()
|
|
123
|
+
if str(folder) == os.path.abspath(os.sep):
|
|
124
|
+
raise FileNotFoundError(f"could not find {config_name or config_dir!r} from {calling_file}")
|
|
125
|
+
folder = folder.parent
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def load(
|
|
129
|
+
cls,
|
|
130
|
+
config_dir: str = "config",
|
|
131
|
+
config_name: str = "",
|
|
132
|
+
overrides: list[str] | None = None,
|
|
133
|
+
*,
|
|
134
|
+
basepath: str = "",
|
|
135
|
+
_offset: int = 3,
|
|
136
|
+
) -> Self:
|
|
137
|
+
overrides = overrides or []
|
|
138
|
+
hydra_dir = Path(__file__).resolve().parent / "hydra"
|
|
139
|
+
with initialize_config_dir(config_dir=str(hydra_dir), version_base=None):
|
|
140
|
+
if config_dir:
|
|
141
|
+
hydra_folder, resolved_config_dir, _ = cls._find_parent_config_folder(
|
|
142
|
+
config_dir=config_dir,
|
|
143
|
+
config_name=config_name,
|
|
144
|
+
basepath=basepath,
|
|
145
|
+
_offset=_offset,
|
|
146
|
+
)
|
|
147
|
+
base = compose(config_name="base", return_hydra_config=True)
|
|
148
|
+
searchpaths = list(base["hydra"]["searchpath"])
|
|
149
|
+
searchpaths.extend([str(hydra_folder), str(resolved_config_dir)])
|
|
150
|
+
if config_name:
|
|
151
|
+
overrides = [f"+config={config_name}", *overrides, f"hydra.searchpath=[{','.join(searchpaths)}]"]
|
|
152
|
+
else:
|
|
153
|
+
overrides = [*overrides, f"hydra.searchpath=[{','.join(searchpaths)}]"]
|
|
154
|
+
config = instantiate(compose(config_name="base", overrides=overrides))
|
|
155
|
+
if isinstance(config, cls):
|
|
156
|
+
return config
|
|
157
|
+
if isinstance(config, BaseModel):
|
|
158
|
+
config = config.model_dump(exclude_unset=True)
|
|
159
|
+
return cls.model_validate(config)
|
|
160
|
+
|
|
161
|
+
def start(self) -> dict[str, UnitInfo]:
|
|
162
|
+
from ..client import SystemdClient
|
|
163
|
+
|
|
164
|
+
return SystemdClient(self).start_services()
|
|
165
|
+
|
|
166
|
+
def running(self) -> bool:
|
|
167
|
+
from ..client import SystemdClient
|
|
168
|
+
|
|
169
|
+
return all(info.running() for info in SystemdClient(self).get_all_service_info().values())
|
|
170
|
+
|
|
171
|
+
def stop(self) -> dict[str, UnitInfo]:
|
|
172
|
+
from ..client import SystemdClient
|
|
173
|
+
|
|
174
|
+
return SystemdClient(self).stop_services()
|
|
175
|
+
|
|
176
|
+
def kill(self) -> dict[str, UnitInfo]:
|
|
177
|
+
from ..client import SystemdClient
|
|
178
|
+
|
|
179
|
+
return SystemdClient(self).kill_services()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
load_config = SystemdConfiguration.load
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .commands import (
|
|
2
|
+
check_services,
|
|
3
|
+
main,
|
|
4
|
+
remove_systemd_config,
|
|
5
|
+
restart_services,
|
|
6
|
+
start_services,
|
|
7
|
+
stop_services,
|
|
8
|
+
write_systemd_config,
|
|
9
|
+
)
|
|
10
|
+
from .common import SystemdTaskStep
|
|
11
|
+
|
|
12
|
+
__all__ = (
|
|
13
|
+
"SystemdTaskStep",
|
|
14
|
+
"check_services",
|
|
15
|
+
"main",
|
|
16
|
+
"remove_systemd_config",
|
|
17
|
+
"restart_services",
|
|
18
|
+
"start_services",
|
|
19
|
+
"stop_services",
|
|
20
|
+
"write_systemd_config",
|
|
21
|
+
)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from typer import Argument, Exit, Option, Typer
|
|
4
|
+
from typing_extensions import Annotated
|
|
5
|
+
|
|
6
|
+
from ..client import SystemdClient
|
|
7
|
+
from ..config import SystemdConvenienceConfiguration
|
|
8
|
+
from .common import SystemdTaskStep
|
|
9
|
+
|
|
10
|
+
ConfigInput = str | Path | SystemdConvenienceConfiguration
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _raise_or_exit(value: bool, exit: bool):
|
|
14
|
+
if exit:
|
|
15
|
+
raise Exit(int(not value))
|
|
16
|
+
return value
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _load_or_pass(cfg: ConfigInput) -> SystemdConvenienceConfiguration:
|
|
20
|
+
if isinstance(cfg, Path):
|
|
21
|
+
cfg = cfg.read_text()
|
|
22
|
+
if isinstance(cfg, str):
|
|
23
|
+
return SystemdConvenienceConfiguration.model_validate_json(cfg)
|
|
24
|
+
if not isinstance(cfg, SystemdConvenienceConfiguration):
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
return cfg
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def write_systemd_config(cfg_json: str, _exit: Annotated[bool, Argument(hidden=True)] = True):
|
|
30
|
+
cfg = _load_or_pass(cfg_json)
|
|
31
|
+
cfg._write_self()
|
|
32
|
+
client = SystemdClient(cfg)
|
|
33
|
+
client.daemon_reload()
|
|
34
|
+
return _raise_or_exit(True, _exit)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def start_services(
|
|
38
|
+
cfg: Annotated[Path, Option(exists=True, file_okay=True, dir_okay=False, readable=True)] = Path("pydantic.json"),
|
|
39
|
+
restart: bool = False,
|
|
40
|
+
_exit: Annotated[bool, Argument(hidden=True)] = True,
|
|
41
|
+
):
|
|
42
|
+
config = _load_or_pass(cfg)
|
|
43
|
+
client = SystemdClient(config)
|
|
44
|
+
result = client.restart_services() if restart else client.start_services()
|
|
45
|
+
ok = all(info.ok(config.success_exit_status) for info in result.values())
|
|
46
|
+
return _raise_or_exit(ok, _exit)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check_services(
|
|
50
|
+
cfg: Annotated[Path, Option(exists=True, file_okay=True, dir_okay=False, readable=True)] = Path("pydantic.json"),
|
|
51
|
+
check_running: bool = False,
|
|
52
|
+
check_done: bool = False,
|
|
53
|
+
_exit: Annotated[bool, Argument(hidden=True)] = True,
|
|
54
|
+
):
|
|
55
|
+
config = _load_or_pass(cfg)
|
|
56
|
+
infos = SystemdClient(config).get_all_service_info().values()
|
|
57
|
+
if check_running:
|
|
58
|
+
ok = all(info.running() for info in infos)
|
|
59
|
+
elif check_done:
|
|
60
|
+
ok = all(info.done(config.success_exit_status) for info in infos)
|
|
61
|
+
else:
|
|
62
|
+
ok = all(info.ok(config.success_exit_status) for info in infos)
|
|
63
|
+
return _raise_or_exit(ok, _exit)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def stop_services(
|
|
67
|
+
cfg: Annotated[Path, Option(exists=True, file_okay=True, dir_okay=False, readable=True)] = Path("pydantic.json"),
|
|
68
|
+
_exit: Annotated[bool, Argument(hidden=True)] = True,
|
|
69
|
+
):
|
|
70
|
+
config = _load_or_pass(cfg)
|
|
71
|
+
infos = SystemdClient(config).stop_services().values()
|
|
72
|
+
return _raise_or_exit(all(info.stopped() for info in infos), _exit)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def restart_services(
|
|
76
|
+
cfg: Annotated[Path, Option(exists=True, file_okay=True, dir_okay=False, readable=True)] = Path("pydantic.json"),
|
|
77
|
+
_exit: Annotated[bool, Argument(hidden=True)] = True,
|
|
78
|
+
):
|
|
79
|
+
config = _load_or_pass(cfg)
|
|
80
|
+
infos = SystemdClient(config).restart_services().values()
|
|
81
|
+
return _raise_or_exit(all(info.ok(config.success_exit_status) for info in infos), _exit)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def remove_systemd_config(
|
|
85
|
+
cfg: Annotated[Path, Option(exists=True, file_okay=True, dir_okay=False, readable=True)] = Path("pydantic.json"),
|
|
86
|
+
_exit: Annotated[bool, Argument(hidden=True)] = True,
|
|
87
|
+
):
|
|
88
|
+
config = _load_or_pass(cfg)
|
|
89
|
+
client = SystemdClient(config)
|
|
90
|
+
client.stop_timers()
|
|
91
|
+
client.stop_services()
|
|
92
|
+
config.rmdir()
|
|
93
|
+
client.daemon_reload()
|
|
94
|
+
return _raise_or_exit(True, _exit)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _add_to_typer(app: Typer, command: SystemdTaskStep, function) -> None:
|
|
98
|
+
app.command(command)(function)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def main() -> None:
|
|
102
|
+
app = Typer()
|
|
103
|
+
_add_to_typer(app, "configure-systemd", write_systemd_config)
|
|
104
|
+
_add_to_typer(app, "start-services", start_services)
|
|
105
|
+
_add_to_typer(app, "check-services", check_services)
|
|
106
|
+
_add_to_typer(app, "restart-services", restart_services)
|
|
107
|
+
_add_to_typer(app, "stop-services", stop_services)
|
|
108
|
+
_add_to_typer(app, "unconfigure-systemd", remove_systemd_config)
|
|
109
|
+
app()
|