runspec-linux-core 0.3.3__tar.gz → 0.3.5__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.3.3 → runspec_linux_core-0.3.5}/CHANGELOG.md +37 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/PKG-INFO +2 -2
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/pyproject.toml +2 -2
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/runspec_linux_core/venvs.py +27 -6
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/tests/test_venvs.py +49 -3
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/.gitignore +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/runspec_linux_core/__init__.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/runspec_linux_core/_paths.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/runspec_linux_core/commands.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/runspec_linux_core/containers.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/runspec_linux_core/errors.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/runspec_linux_core/files.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/runspec_linux_core/logs.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/runspec_linux_core/nc.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/runspec_linux_core/network.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/runspec_linux_core/packages.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/runspec_linux_core/power.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/runspec_linux_core/security.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/runspec_linux_core/services.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/runspec_linux_core/sudoers.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/runspec_linux_core/system.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/tests/__init__.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/tests/test_commands.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/tests/test_nc_send.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/tests/test_network.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/tests/test_packages.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/tests/test_paths.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/tests/test_power.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/tests/test_preconditions.py +0 -0
- {runspec_linux_core-0.3.3 → runspec_linux_core-0.3.5}/tests/test_sudoers.py +0 -0
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# runspec-linux-core Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.5] — 2026-07-08
|
|
4
|
+
|
|
5
|
+
Apply the same umask-neutralising treatment to `install_into_venv` that `0.3.4`
|
|
6
|
+
added to `create_venv`. After the `pip install`, the venv tree is now
|
|
7
|
+
normalised with `chmod -R u=rwX,go=rX <venv>` so pip's new directories are
|
|
8
|
+
`755` regardless of the caller's umask.
|
|
9
|
+
|
|
10
|
+
The trigger was a `sudo pip install --upgrade runspec-linux` under a host
|
|
11
|
+
where sudo/PAM had switched to a `027` login umask: pip's new
|
|
12
|
+
`site-packages/runspec_linux/` and `site-packages/runspec_linux_core/`
|
|
13
|
+
directories landed at `750`, and `runspec local` (walking `.dist-info` via
|
|
14
|
+
`importlib.metadata`) stopped seeing the packs for any user but the owner.
|
|
15
|
+
The equivalent bug for the venv root was fixed at build time in `0.3.4`; this
|
|
16
|
+
release closes the same gap at upgrade time.
|
|
17
|
+
|
|
18
|
+
If `<venv>/logs` exists (created by `create_venv`), its `2775` setgid mode is
|
|
19
|
+
restored after the wide chmod — the `test -d` guard skips the restore for a
|
|
20
|
+
venv built some other way. When `run_as` is set the new steps escalate too.
|
|
21
|
+
|
|
22
|
+
## [0.3.4] — 2026-07-07
|
|
23
|
+
|
|
24
|
+
Fix `create_venv` producing an unreadable `750` venv directory when the caller's
|
|
25
|
+
umask is `027` (typical under `sudo -n -u <svc>` with a PAM-set login umask).
|
|
26
|
+
`python -m venv` honours the current umask, so a shared service-account venv
|
|
27
|
+
was landing owner-only instead of the `755` a stock `python -m venv <dest>`
|
|
28
|
+
produces under the default `022` umask, locking every non-owner console out —
|
|
29
|
+
contrary to the "shared between several consoles" use case the helper is built
|
|
30
|
+
for.
|
|
31
|
+
|
|
32
|
+
After the venv-creation step, `create_venv` now runs
|
|
33
|
+
`chmod -R u=rwX,go=rX <dest>` to normalise the tree to `755` on directories and
|
|
34
|
+
`644` on files while preserving the `+x` on real executables in `bin/` (capital
|
|
35
|
+
`X` only sets execute on directories and files that already have some execute
|
|
36
|
+
bit). The subsequent `chmod 2775 <dest>/logs` still gets the last word on the
|
|
37
|
+
logs directory, and `chgrp -R <group>` (when passed) still runs. When `run_as`
|
|
38
|
+
is set the new chmod runs as that user too.
|
|
39
|
+
|
|
3
40
|
## [0.3.3] — 2026-06-22
|
|
4
41
|
|
|
5
42
|
Add `commands.py` — the pure logic behind the new `runspec-linux` `which`
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: runspec-linux-core
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: Pure-Python Linux system-admin helpers — the importable core behind runspec-linux (no runspec dependency, no runnables)
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Provides-Extra: dev
|
|
7
7
|
Requires-Dist: mypy; extra == 'dev'
|
|
8
8
|
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
9
|
-
Requires-Dist: ruff; extra == 'dev'
|
|
9
|
+
Requires-Dist: ruff==0.15.20; extra == 'dev'
|
|
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "runspec-linux-core"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.5"
|
|
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 = []
|
|
11
11
|
|
|
12
12
|
[project.optional-dependencies]
|
|
13
13
|
dev = [
|
|
14
|
-
"ruff",
|
|
14
|
+
"ruff==0.15.20",
|
|
15
15
|
"mypy",
|
|
16
16
|
"pytest>=8.0",
|
|
17
17
|
]
|
|
@@ -92,12 +92,17 @@ def create_venv(
|
|
|
92
92
|
) -> dict:
|
|
93
93
|
"""Create a virtualenv at ``dest`` from a specific ``python`` interpreter.
|
|
94
94
|
|
|
95
|
-
Always uses ``--symlinks``. Then
|
|
96
|
-
|
|
97
|
-
``
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
Always uses ``--symlinks``. Then normalises the tree to ``u=rwX,go=rX`` so
|
|
96
|
+
directories are ``755`` and executables in ``bin/`` keep their ``+x`` — matching
|
|
97
|
+
what a plain ``python -m venv`` produces under a default ``022`` umask, regardless
|
|
98
|
+
of the caller's umask (``sudo -n -u <svc>`` often lands at ``027``, which would
|
|
99
|
+
otherwise leave the venv at ``750`` and lock other consoles out). Then creates
|
|
100
|
+
``<dest>/logs`` mode ``2775`` (setgid, so new audit-log files inherit the
|
|
101
|
+
directory's group) and, if ``group`` is set, ``chgrp -R`` the venv to it —
|
|
102
|
+
enabling a shared service-account venv that several consoles read/write. With
|
|
103
|
+
``run_as`` set every step runs as that user (so the venv is owned by it). Raises
|
|
104
|
+
ValueError on a non-executable interpreter / bad name, or CommandError on a
|
|
105
|
+
failed step.
|
|
101
106
|
"""
|
|
102
107
|
if not (os.path.isfile(python) and os.access(python, os.X_OK)):
|
|
103
108
|
raise ValueError(f"not an executable file: {python!r} (pick a real interpreter, not the system default)")
|
|
@@ -114,6 +119,8 @@ def create_venv(
|
|
|
114
119
|
|
|
115
120
|
_check(_run(_become([python, "-m", "venv", *flags, dest], run_as, become_method)), "venv creation", run_as)
|
|
116
121
|
|
|
122
|
+
_check(_run(_become(["chmod", "-R", "u=rwX,go=rX", dest], run_as, become_method)), "chmod venv", run_as)
|
|
123
|
+
|
|
117
124
|
logs_dir = os.path.join(dest, "logs")
|
|
118
125
|
_check(_run(_become(["mkdir", "-p", logs_dir], run_as, become_method)), "logs dir", run_as)
|
|
119
126
|
if group:
|
|
@@ -150,6 +157,14 @@ def install_into_venv(
|
|
|
150
157
|
path to a directory with a ``pyproject.toml`` (or a wheel/sdist), installed
|
|
151
158
|
editable when ``editable`` is set. At least one of ``packages``/``project`` is
|
|
152
159
|
required. Raises ValueError on bad input, CommandError on a failed install.
|
|
160
|
+
|
|
161
|
+
After the install, the venv tree is normalised to ``u=rwX,go=rX`` (the same
|
|
162
|
+
step ``create_venv`` runs at build time) so pip's new directories are ``755``
|
|
163
|
+
regardless of the caller's umask — a ``sudo`` upgrade under a PAM-set ``027``
|
|
164
|
+
login umask otherwise lands site-packages entries at ``750`` and hides the
|
|
165
|
+
packs from ``runspec local`` / ``importlib.metadata`` for anyone but the
|
|
166
|
+
owner. If ``<venv>/logs`` exists (``create_venv`` leaves it there), its
|
|
167
|
+
``2775`` setgid mode is restored after the wide chmod strips it.
|
|
153
168
|
"""
|
|
154
169
|
from runspec_linux_core.packages import _validate_packages
|
|
155
170
|
|
|
@@ -180,6 +195,12 @@ def install_into_venv(
|
|
|
180
195
|
cmd = _become([venv_python, "-m", "pip", "install", *opts, *target], run_as, become_method)
|
|
181
196
|
result = _run(cmd)
|
|
182
197
|
_check(result, "pip install", run_as)
|
|
198
|
+
|
|
199
|
+
_check(_run(_become(["chmod", "-R", "u=rwX,go=rX", venv], run_as, become_method)), "chmod venv", run_as)
|
|
200
|
+
logs_dir = os.path.join(venv, "logs")
|
|
201
|
+
if _run(_become(["test", "-d", logs_dir], run_as, become_method)).returncode == 0:
|
|
202
|
+
_check(_run(_become(["chmod", "2775", logs_dir], run_as, become_method)), "chmod logs", run_as)
|
|
203
|
+
|
|
183
204
|
return {
|
|
184
205
|
"venv": venv,
|
|
185
206
|
"packages": names,
|
|
@@ -69,9 +69,12 @@ def test_create_venv_uses_symlinks_and_logs(monkeypatch: pytest.MonkeyPatch) ->
|
|
|
69
69
|
venv_cmd = calls[0]
|
|
70
70
|
assert venv_cmd[:4] == [sys.executable, "-m", "venv", "--symlinks"]
|
|
71
71
|
assert venv_cmd[-1] == "/opt/v1"
|
|
72
|
+
# chmod -R u=rwX,go=rX normalises the tree to 755-dirs/644-files regardless of
|
|
73
|
+
# the caller's umask (a sudo -u <svc> shell often lands at 027 → 750).
|
|
74
|
+
assert calls[1] == ["chmod", "-R", "u=rwX,go=rX", "/opt/v1"]
|
|
72
75
|
# mkdir -p logs, then chmod 2775 logs (no group → no chgrp step).
|
|
73
|
-
assert calls[
|
|
74
|
-
assert calls[
|
|
76
|
+
assert calls[2] == ["mkdir", "-p", "/opt/v1/logs"]
|
|
77
|
+
assert calls[3] == ["chmod", "2775", "/opt/v1/logs"]
|
|
75
78
|
assert result == {
|
|
76
79
|
"python": sys.executable,
|
|
77
80
|
"dest": "/opt/v1",
|
|
@@ -100,7 +103,8 @@ def test_create_venv_run_as_escalates_every_step(monkeypatch: pytest.MonkeyPatch
|
|
|
100
103
|
venvs.create_venv(sys.executable, "/opt/v1", run_as="svc", group="consoles")
|
|
101
104
|
for cmd in calls:
|
|
102
105
|
assert cmd[:4] == ["sudo", "-n", "-u", "svc"]
|
|
103
|
-
#
|
|
106
|
+
# The umask-normalising chmod and the chgrp both run as svc too.
|
|
107
|
+
assert ["sudo", "-n", "-u", "svc", "chmod", "-R", "u=rwX,go=rX", "/opt/v1"] in calls
|
|
104
108
|
assert ["sudo", "-n", "-u", "svc", "chgrp", "-R", "consoles", "/opt/v1"] in calls
|
|
105
109
|
|
|
106
110
|
|
|
@@ -157,6 +161,48 @@ def test_install_into_venv_editable_project(tmp_path, monkeypatch: pytest.Monkey
|
|
|
157
161
|
assert cmd[-2:] == ["-e", str(proj)]
|
|
158
162
|
|
|
159
163
|
|
|
164
|
+
def test_install_into_venv_normalises_perms(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
165
|
+
"""After the pip install, the venv tree is chmodded so sudo's umask can't hide
|
|
166
|
+
site-packages entries at 750 from `runspec local` / `importlib.metadata`."""
|
|
167
|
+
venv = _fake_venv(tmp_path, monkeypatch)
|
|
168
|
+
calls = _record(monkeypatch)
|
|
169
|
+
|
|
170
|
+
venvs.install_into_venv(venv, "httpx", upgrade=True, run_as="svc")
|
|
171
|
+
|
|
172
|
+
assert ["sudo", "-n", "-u", "svc", "chmod", "-R", "u=rwX,go=rX", venv] in calls
|
|
173
|
+
# logs_dir exists (the default stub returns 0 for `test -d`), so the setgid
|
|
174
|
+
# mode is restored after the wide chmod would have stripped it.
|
|
175
|
+
assert ["sudo", "-n", "-u", "svc", "test", "-d", f"{venv}/logs"] in calls
|
|
176
|
+
assert ["sudo", "-n", "-u", "svc", "chmod", "2775", f"{venv}/logs"] in calls
|
|
177
|
+
# And the chmod steps run *after* the pip install (order matters — pip
|
|
178
|
+
# creates the dirs we're normalising).
|
|
179
|
+
pip_i = next(i for i, c in enumerate(calls) if "pip" in c and "install" in c)
|
|
180
|
+
chmod_i = next(i for i, c in enumerate(calls) if "chmod" in c and "u=rwX,go=rX" in c)
|
|
181
|
+
assert chmod_i > pip_i
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_install_into_venv_skips_logs_restore_when_absent(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
185
|
+
"""A venv without a logs/ dir (not built by create_venv) gets the wide chmod
|
|
186
|
+
but no 2775 restore — the test -d guard skips the logs step."""
|
|
187
|
+
venv = _fake_venv(tmp_path, monkeypatch)
|
|
188
|
+
calls: list[list[str]] = []
|
|
189
|
+
|
|
190
|
+
def fake_run(argv: list[str]) -> subprocess.CompletedProcess:
|
|
191
|
+
calls.append(argv)
|
|
192
|
+
# Only the `test -d <venv>/logs` probe fails; everything else succeeds.
|
|
193
|
+
if argv[:2] == ["test", "-d"]:
|
|
194
|
+
return _completed(returncode=1)
|
|
195
|
+
return _completed("Successfully installed foo\n")
|
|
196
|
+
|
|
197
|
+
monkeypatch.setattr(venvs, "_run", fake_run)
|
|
198
|
+
|
|
199
|
+
venvs.install_into_venv(venv, "httpx")
|
|
200
|
+
|
|
201
|
+
assert ["chmod", "-R", "u=rwX,go=rX", venv] in calls
|
|
202
|
+
assert ["test", "-d", f"{venv}/logs"] in calls
|
|
203
|
+
assert not any(c[:2] == ["chmod", "2775"] for c in calls)
|
|
204
|
+
|
|
205
|
+
|
|
160
206
|
def test_install_into_venv_requires_a_target(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
161
207
|
venv = _fake_venv(tmp_path, monkeypatch)
|
|
162
208
|
_record(monkeypatch)
|
|
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
|