runspec-linux-core 0.6.2__tar.gz → 0.7.0__tar.gz
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.
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/CHANGELOG.md +17 -1
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/PKG-INFO +1 -1
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/pyproject.toml +1 -1
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/__init__.py +2 -1
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/venvs.py +53 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/tests/test_venvs.py +87 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/.gitignore +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/_paths.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/become.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/commands.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/containers.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/cron.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/errors.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/files.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/investigate.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/logs.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/nc.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/network.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/packages.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/perf.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/power.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/security.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/services.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/sudoers.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/runspec_linux_core/system.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/tests/__init__.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/tests/test_become.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/tests/test_commands.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/tests/test_containers_runas.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/tests/test_cron.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/tests/test_investigate.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/tests/test_logs_services_runas.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/tests/test_nc_send.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/tests/test_network.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/tests/test_packages.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/tests/test_paths.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/tests/test_perf.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/tests/test_power.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/tests/test_preconditions.py +0 -0
- {runspec_linux_core-0.6.2 → runspec_linux_core-0.7.0}/tests/test_sudoers.py +0 -0
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
# runspec-linux-core Changelog
|
|
2
2
|
|
|
3
|
-
## [0.
|
|
3
|
+
## [0.7.0] — 2026-09-25
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`share_venv_logs(venv, *, group=None, run_as=None, become_method="sudo")`** —
|
|
8
|
+
the repair path for a venv **not** built by `create-venv --group` (hand-made or
|
|
9
|
+
legacy). Sets `<venv>/logs` to `2775` (setgid + group-writable) so new files —
|
|
10
|
+
including the per-venv runs index `runspec_runs.db` and the audit-log files —
|
|
11
|
+
inherit the directory's group and can be written across a shared console group,
|
|
12
|
+
and, with `group`, `chgrp`s the `logs/` **directory** to it. The chgrp is on the
|
|
13
|
+
directory only (not `-R`): `logs/` already holds per-run files owned by *other*
|
|
14
|
+
users (a `run_as="root"` runnable leaves root-owned logs there) which a non-root
|
|
15
|
+
caller can't chgrp, and the setgid bit means only new files need the group. This
|
|
16
|
+
is exactly what `create_venv` does at build time, applied to an existing venv
|
|
17
|
+
without recreating it. Raises `ValueError` for a bad group name or a `<venv>`
|
|
18
|
+
that isn't a directory, `CommandError` on a failed step. Backs the new
|
|
19
|
+
`share-venv-logs` runnable in `runspec-linux`.
|
|
4
20
|
|
|
5
21
|
### Fixed
|
|
6
22
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "runspec-linux-core"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0"
|
|
8
8
|
requires-python = ">=3.10"
|
|
9
9
|
description = "Pure-Python Linux system-admin helpers — the importable core behind runspec-linux (no runspec dependency, no runnables)"
|
|
10
10
|
dependencies = []
|
|
@@ -63,7 +63,7 @@ from runspec_linux_core.security import last_logins, who
|
|
|
63
63
|
from runspec_linux_core.services import check_service, list_services, restart_service
|
|
64
64
|
from runspec_linux_core.sudoers import enable_passwordless_sudo
|
|
65
65
|
from runspec_linux_core.system import check_memory, disk_usage, list_processes, system_info
|
|
66
|
-
from runspec_linux_core.venvs import configure_pip, create_venv, install_into_venv
|
|
66
|
+
from runspec_linux_core.venvs import configure_pip, create_venv, install_into_venv, share_venv_logs
|
|
67
67
|
|
|
68
68
|
__all__ = [
|
|
69
69
|
# errors
|
|
@@ -139,6 +139,7 @@ __all__ = [
|
|
|
139
139
|
"create_venv",
|
|
140
140
|
"install_into_venv",
|
|
141
141
|
"configure_pip",
|
|
142
|
+
"share_venv_logs",
|
|
142
143
|
# tcp
|
|
143
144
|
"nc_send",
|
|
144
145
|
# commands
|
|
@@ -115,6 +115,54 @@ def create_venv(
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
|
|
118
|
+
def share_venv_logs(
|
|
119
|
+
venv: str,
|
|
120
|
+
*,
|
|
121
|
+
group: str | None = None,
|
|
122
|
+
run_as: str | None = None,
|
|
123
|
+
become_method: str = "sudo",
|
|
124
|
+
) -> dict:
|
|
125
|
+
"""Make an existing venv's ``logs/`` dir shared — the repair path for a venv
|
|
126
|
+
**not** built by ``create-venv --group`` (hand-made or legacy).
|
|
127
|
+
|
|
128
|
+
Sets ``<venv>/logs`` to ``2775`` (setgid + group-writable) so new files —
|
|
129
|
+
including the per-venv runs index ``runspec_runs.db`` and the audit-log
|
|
130
|
+
files — inherit the directory's group and can be written by every console /
|
|
131
|
+
service account in a shared group. When ``group`` is given it also
|
|
132
|
+
``chgrp``s the ``logs/`` **directory** to it.
|
|
133
|
+
|
|
134
|
+
The chgrp is on the directory only, **not** ``-R``: ``logs/`` already holds
|
|
135
|
+
per-run files owned by *other* users (a ``run_as="root"`` runnable leaves
|
|
136
|
+
root-owned logs there), which a non-root caller can't chgrp — and the setgid
|
|
137
|
+
bit means only *new* files need the group anyway (the DB is created fresh).
|
|
138
|
+
``create-venv --group`` does exactly this at build time; this brings a
|
|
139
|
+
pre-existing venv up to the same state without recreating it. With
|
|
140
|
+
``run_as`` every step runs as that user (the venv's owner).
|
|
141
|
+
|
|
142
|
+
Raises ValueError if ``group`` is malformed or ``<venv>`` isn't an existing
|
|
143
|
+
directory, CommandError on a failed step.
|
|
144
|
+
"""
|
|
145
|
+
if group is not None:
|
|
146
|
+
_validate_name(group, "group")
|
|
147
|
+
if not _dir_exists_as(venv, run_as, become_method):
|
|
148
|
+
raise ValueError(f"not a directory: {venv!r} (point at an existing venv)")
|
|
149
|
+
|
|
150
|
+
logs_dir = os.path.join(venv, "logs")
|
|
151
|
+
_check(_run(build_become(["mkdir", "-p", logs_dir], run_as, become_method)), "logs dir", run_as)
|
|
152
|
+
if group:
|
|
153
|
+
_check(_run(build_become(["chgrp", group, logs_dir], run_as, become_method)), "chgrp logs", run_as)
|
|
154
|
+
_check(_run(build_become(["chmod", "2775", logs_dir], run_as, become_method)), "chmod logs", run_as)
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
"venv": venv,
|
|
158
|
+
"logs_dir": logs_dir,
|
|
159
|
+
"group": group or "",
|
|
160
|
+
"run_as": run_as or "",
|
|
161
|
+
"mode": "2775",
|
|
162
|
+
"shared": True,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
118
166
|
def install_into_venv(
|
|
119
167
|
venv: str,
|
|
120
168
|
packages: str = "",
|
|
@@ -344,3 +392,8 @@ def configure_pip(
|
|
|
344
392
|
def _path_exists_as(path: str, run_as: str | None, become_method: str) -> bool:
|
|
345
393
|
"""True if ``path`` exists from the run-as user's view (``test -e``)."""
|
|
346
394
|
return _run(build_become(["test", "-e", path], run_as, become_method)).returncode == 0
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _dir_exists_as(path: str, run_as: str | None, become_method: str) -> bool:
|
|
398
|
+
"""True if ``path`` is a directory from the run-as user's view (``test -d``)."""
|
|
399
|
+
return _run(build_become(["test", "-d", path], run_as, become_method)).returncode == 0
|
|
@@ -101,6 +101,93 @@ def test_create_venv_raises_on_failure(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
101
101
|
venvs.create_venv(sys.executable, "/opt/v1")
|
|
102
102
|
|
|
103
103
|
|
|
104
|
+
# ── share_venv_logs (repair a legacy venv's logs/ perms) ───────────────────────
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_share_venv_logs_builds_commands(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
108
|
+
calls = _record(monkeypatch) # default stub: `test -d` returns 0
|
|
109
|
+
|
|
110
|
+
result = venvs.share_venv_logs("/opt/v1")
|
|
111
|
+
|
|
112
|
+
# Verify the venv is a dir, then mkdir -p logs, then chmod 2775 logs. No chgrp
|
|
113
|
+
# without a group; the chmod is the last step.
|
|
114
|
+
assert calls == [
|
|
115
|
+
["test", "-d", "/opt/v1"],
|
|
116
|
+
["mkdir", "-p", "/opt/v1/logs"],
|
|
117
|
+
["chmod", "2775", "/opt/v1/logs"],
|
|
118
|
+
]
|
|
119
|
+
assert result == {
|
|
120
|
+
"venv": "/opt/v1",
|
|
121
|
+
"logs_dir": "/opt/v1/logs",
|
|
122
|
+
"group": "",
|
|
123
|
+
"run_as": "",
|
|
124
|
+
"mode": "2775",
|
|
125
|
+
"shared": True,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_share_venv_logs_with_group_chgrps_dir_only(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
130
|
+
calls = _record(monkeypatch)
|
|
131
|
+
|
|
132
|
+
venvs.share_venv_logs("/opt/v1", group="consoles")
|
|
133
|
+
|
|
134
|
+
# chgrp targets the logs *directory* only — never `-R` (logs/ holds per-run
|
|
135
|
+
# files owned by other users a non-root caller can't chgrp; setgid covers new
|
|
136
|
+
# files). Order: test -d, mkdir, chgrp, chmod.
|
|
137
|
+
assert calls == [
|
|
138
|
+
["test", "-d", "/opt/v1"],
|
|
139
|
+
["mkdir", "-p", "/opt/v1/logs"],
|
|
140
|
+
["chgrp", "consoles", "/opt/v1/logs"],
|
|
141
|
+
["chmod", "2775", "/opt/v1/logs"],
|
|
142
|
+
]
|
|
143
|
+
assert not any(c[:2] == ["chgrp", "-R"] for c in calls)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_share_venv_logs_run_as_escalates_every_step(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
147
|
+
calls = _record(monkeypatch)
|
|
148
|
+
|
|
149
|
+
venvs.share_venv_logs("/opt/v1", group="consoles", run_as="svc")
|
|
150
|
+
|
|
151
|
+
for cmd in calls:
|
|
152
|
+
assert cmd[:4] == ["sudo", "-n", "-u", "svc"]
|
|
153
|
+
assert ["sudo", "-n", "-u", "svc", "chgrp", "consoles", "/opt/v1/logs"] in calls
|
|
154
|
+
assert ["sudo", "-n", "-u", "svc", "chmod", "2775", "/opt/v1/logs"] in calls
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_share_venv_logs_rejects_missing_venv(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
158
|
+
calls: list[list[str]] = []
|
|
159
|
+
|
|
160
|
+
def fake_run(argv: list[str]) -> subprocess.CompletedProcess:
|
|
161
|
+
calls.append(argv)
|
|
162
|
+
return _completed(returncode=1) # `test -d` fails → not a directory
|
|
163
|
+
|
|
164
|
+
monkeypatch.setattr(venvs, "_run", fake_run)
|
|
165
|
+
|
|
166
|
+
with pytest.raises(ValueError):
|
|
167
|
+
venvs.share_venv_logs("/opt/nope")
|
|
168
|
+
# Bailed after the probe — no mkdir/chmod attempted.
|
|
169
|
+
assert calls == [["test", "-d", "/opt/nope"]]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_share_venv_logs_rejects_bad_group(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
173
|
+
calls = _record(monkeypatch)
|
|
174
|
+
with pytest.raises(ValueError):
|
|
175
|
+
venvs.share_venv_logs("/opt/v1", group="bad group")
|
|
176
|
+
# Group is validated before any subprocess runs (not even the `test -d` probe).
|
|
177
|
+
assert calls == []
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_share_venv_logs_raises_on_failure(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
181
|
+
def fake_run(argv: list[str]) -> subprocess.CompletedProcess:
|
|
182
|
+
if argv[:2] == ["test", "-d"]:
|
|
183
|
+
return _completed(returncode=0) # dir exists…
|
|
184
|
+
return _completed("", returncode=1, stderr="boom") # …but a step fails
|
|
185
|
+
|
|
186
|
+
monkeypatch.setattr(venvs, "_run", fake_run)
|
|
187
|
+
with pytest.raises(CommandError):
|
|
188
|
+
venvs.share_venv_logs("/opt/v1")
|
|
189
|
+
|
|
190
|
+
|
|
104
191
|
# ── install_into_venv ─────────────────────────────────────────────────────────
|
|
105
192
|
|
|
106
193
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|