runspec-linux-core 0.6.0__tar.gz → 0.6.2__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.
Potentially problematic release.
This version of runspec-linux-core might be problematic. Click here for more details.
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/CHANGELOG.md +33 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/PKG-INFO +1 -1
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/pyproject.toml +1 -1
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/venvs.py +52 -3
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_venvs.py +51 -8
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/.gitignore +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/__init__.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/_paths.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/become.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/commands.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/containers.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/cron.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/errors.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/files.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/investigate.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/logs.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/nc.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/network.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/packages.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/perf.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/power.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/security.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/services.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/sudoers.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/system.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/__init__.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_become.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_commands.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_containers_runas.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_cron.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_investigate.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_logs_services_runas.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_nc_send.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_network.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_packages.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_paths.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_perf.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_power.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_preconditions.py +0 -0
- {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_sudoers.py +0 -0
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# runspec-linux-core Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.2] — 2026-09-17
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`install_into_venv` regression (0.6.1): `chmod: … Operation not permitted` on
|
|
8
|
+
a `--symlinks` venv's `bin/python*`.** 0.6.1 replaced the `chmod -R` normalise
|
|
9
|
+
sweep with `find … -exec chmod {} +` to prune the `logs/` dir. But `find -exec
|
|
10
|
+
chmod` **dereferences a symlink argument**, and a venv built with `--symlinks`
|
|
11
|
+
(what `create_venv` always does) has `bin/python`, `bin/python3`,
|
|
12
|
+
`bin/python3.X` pointing at the **root-owned system interpreter outside the
|
|
13
|
+
venv** — so the sweep tried to `chmod /usr/bin/python3.X` and failed for a
|
|
14
|
+
non-root caller (or, under sudo, would have silently repermissioned the system
|
|
15
|
+
Python). `chmod -R` skipped symlinks for free; the `find` sweep now prunes them
|
|
16
|
+
explicitly too (`-type l -prune`), matching the old behaviour. A symlink's own
|
|
17
|
+
mode is unused on Linux, so nothing is lost. The `logs/` prune from 0.6.1 is
|
|
18
|
+
unchanged. No API change.
|
|
19
|
+
|
|
20
|
+
## [0.6.1] — 2026-09-17
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **`install_into_venv` no longer fails with `chmod: … Operation not permitted`
|
|
25
|
+
on a venv with root-owned logs.** The post-install permission-normalisation
|
|
26
|
+
step ran a blanket `chmod -R u=rwX,go=rX <venv>` over the *whole* tree —
|
|
27
|
+
including `<venv>/logs`, the group-shared (`2775`) audit directory. A
|
|
28
|
+
`run_as="root"` runnable (`list-upgrades` / `upgrade-packages`) leaves
|
|
29
|
+
**root-owned** per-run log files there, so a non-root caller's `chmod` hit them
|
|
30
|
+
with "Operation not permitted" and the install reported failure. The sweep now
|
|
31
|
+
uses `find <venv> -path <venv>/logs -prune -o -exec chmod u=rwX,go=rX {} +`,
|
|
32
|
+
which skips the `logs` subtree entirely (its `2775` mode is left intact and
|
|
33
|
+
still restored afterwards as a guard). Site-packages normalisation is
|
|
34
|
+
unchanged. No API change.
|
|
35
|
+
|
|
3
36
|
## [0.6.0] — 2026-08-24
|
|
4
37
|
|
|
5
38
|
New `runspec_linux_core.cron` module — pure helpers for managing a user's
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "runspec-linux-core"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.2"
|
|
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 = []
|
|
@@ -140,8 +140,15 @@ def install_into_venv(
|
|
|
140
140
|
regardless of the caller's umask — a ``sudo`` upgrade under a PAM-set ``027``
|
|
141
141
|
login umask otherwise lands site-packages entries at ``750`` and hides the
|
|
142
142
|
packs from ``runspec local`` / ``importlib.metadata`` for anyone but the
|
|
143
|
-
owner.
|
|
144
|
-
|
|
143
|
+
owner. The sweep **prunes** two things: ``<venv>/logs`` (the group-shared
|
|
144
|
+
audit dir, which holds per-run log files owned by other users — a
|
|
145
|
+
``run_as="root"`` runnable leaves root-owned logs there — so its ``2775``
|
|
146
|
+
setgid mode is left intact and restored afterwards as a guard), and
|
|
147
|
+
**symlinks** (a ``--symlinks`` venv's ``bin/python*`` point at the root-owned
|
|
148
|
+
system interpreter *outside* the venv; ``find -exec chmod`` would dereference
|
|
149
|
+
them and fail — or, under sudo, silently repermission the system Python —
|
|
150
|
+
where ``chmod -R`` skipped symlinks for free). A symlink's own mode is unused
|
|
151
|
+
on Linux, so pruning them changes nothing.
|
|
145
152
|
"""
|
|
146
153
|
from runspec_linux_core.packages import _validate_packages
|
|
147
154
|
|
|
@@ -173,8 +180,50 @@ def install_into_venv(
|
|
|
173
180
|
result = _run(cmd)
|
|
174
181
|
_check(result, "pip install", run_as)
|
|
175
182
|
|
|
176
|
-
|
|
183
|
+
# Normalise pip's freshly-created site-packages entries to u=rwX,go=rX (the
|
|
184
|
+
# same step create_venv runs) so a sudo install under a restrictive login
|
|
185
|
+
# umask can't land them at 750 and hide the packs from `runspec local` /
|
|
186
|
+
# importlib.metadata. Two subtrees are pruned:
|
|
187
|
+
# * <venv>/logs — the group-shared (2775) audit dir, which accrues per-run
|
|
188
|
+
# log files owned by *other* users (a run_as="root" runnable such as
|
|
189
|
+
# list-upgrades / upgrade-packages leaves root-owned logs there), so a
|
|
190
|
+
# non-root caller's chmod on them dies with "Operation not permitted".
|
|
191
|
+
# * symlinks (-type l) — a `--symlinks` venv's bin/python* point at the
|
|
192
|
+
# system interpreter (root-owned, *outside* the venv). `find -exec chmod`
|
|
193
|
+
# dereferences a symlink argument, so without this it would chmod the
|
|
194
|
+
# system python: "Operation not permitted" as a normal user, or a silent
|
|
195
|
+
# repermission of /usr/bin/python* under sudo. `chmod -R` skipped symlinks
|
|
196
|
+
# for free; the find sweep must skip them explicitly to match. A symlink's
|
|
197
|
+
# own mode is irrelevant on Linux, so nothing is lost.
|
|
198
|
+
# The logs dir keeps its 2775 mode (restored below as a belt-and-braces guard).
|
|
177
199
|
logs_dir = os.path.join(venv, "logs")
|
|
200
|
+
_check(
|
|
201
|
+
_run(
|
|
202
|
+
build_become(
|
|
203
|
+
[
|
|
204
|
+
"find",
|
|
205
|
+
venv,
|
|
206
|
+
"-path",
|
|
207
|
+
logs_dir,
|
|
208
|
+
"-prune",
|
|
209
|
+
"-o",
|
|
210
|
+
"-type",
|
|
211
|
+
"l",
|
|
212
|
+
"-prune",
|
|
213
|
+
"-o",
|
|
214
|
+
"-exec",
|
|
215
|
+
"chmod",
|
|
216
|
+
"u=rwX,go=rX",
|
|
217
|
+
"{}",
|
|
218
|
+
"+",
|
|
219
|
+
],
|
|
220
|
+
run_as,
|
|
221
|
+
become_method,
|
|
222
|
+
)
|
|
223
|
+
),
|
|
224
|
+
"chmod venv",
|
|
225
|
+
run_as,
|
|
226
|
+
)
|
|
178
227
|
if _run(build_become(["test", "-d", logs_dir], run_as, become_method)).returncode == 0:
|
|
179
228
|
_check(_run(build_become(["chmod", "2775", logs_dir], run_as, become_method)), "chmod logs", run_as)
|
|
180
229
|
|
|
@@ -148,21 +148,48 @@ def test_install_into_venv_normalises_perms(tmp_path, monkeypatch: pytest.Monkey
|
|
|
148
148
|
|
|
149
149
|
venvs.install_into_venv(venv, "httpx", upgrade=True, run_as="svc")
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
#
|
|
153
|
-
#
|
|
151
|
+
# The normalise sweep prunes <venv>/logs (root-owned per-run logs a
|
|
152
|
+
# run_as="root" runnable leaves there) AND symlinks (a --symlinks venv's
|
|
153
|
+
# bin/python* point at the root-owned system interpreter; find -exec chmod
|
|
154
|
+
# would dereference and fail / repermission it). Otherwise: chmod "Operation
|
|
155
|
+
# not permitted", and the install reports failure.
|
|
156
|
+
assert [
|
|
157
|
+
"sudo",
|
|
158
|
+
"-n",
|
|
159
|
+
"-u",
|
|
160
|
+
"svc",
|
|
161
|
+
"find",
|
|
162
|
+
venv,
|
|
163
|
+
"-path",
|
|
164
|
+
f"{venv}/logs",
|
|
165
|
+
"-prune",
|
|
166
|
+
"-o",
|
|
167
|
+
"-type",
|
|
168
|
+
"l",
|
|
169
|
+
"-prune",
|
|
170
|
+
"-o",
|
|
171
|
+
"-exec",
|
|
172
|
+
"chmod",
|
|
173
|
+
"u=rwX,go=rX",
|
|
174
|
+
"{}",
|
|
175
|
+
"+",
|
|
176
|
+
] in calls
|
|
177
|
+
# No blanket `chmod -R` over the whole tree any more.
|
|
178
|
+
assert not any(c[-4:] == ["chmod", "-R", "u=rwX,go=rX", venv] for c in calls)
|
|
179
|
+
# logs_dir exists (the default stub returns 0 for `test -d`), so its 2775 mode
|
|
180
|
+
# is restored as a guard.
|
|
154
181
|
assert ["sudo", "-n", "-u", "svc", "test", "-d", f"{venv}/logs"] in calls
|
|
155
182
|
assert ["sudo", "-n", "-u", "svc", "chmod", "2775", f"{venv}/logs"] in calls
|
|
156
|
-
# And the
|
|
183
|
+
# And the normalise sweep runs *after* the pip install (order matters — pip
|
|
157
184
|
# creates the dirs we're normalising).
|
|
158
185
|
pip_i = next(i for i, c in enumerate(calls) if "pip" in c and "install" in c)
|
|
159
|
-
chmod_i = next(i for i, c in enumerate(calls) if "
|
|
186
|
+
chmod_i = next(i for i, c in enumerate(calls) if "find" in c and "u=rwX,go=rX" in c)
|
|
160
187
|
assert chmod_i > pip_i
|
|
161
188
|
|
|
162
189
|
|
|
163
190
|
def test_install_into_venv_skips_logs_restore_when_absent(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
164
|
-
"""A venv without a logs/ dir (not built by create_venv) gets the
|
|
165
|
-
but no 2775 restore — the test -d guard skips the logs step."""
|
|
191
|
+
"""A venv without a logs/ dir (not built by create_venv) gets the normalise
|
|
192
|
+
sweep but no 2775 restore — the test -d guard skips the logs step."""
|
|
166
193
|
venv = _fake_venv(tmp_path, monkeypatch)
|
|
167
194
|
calls: list[list[str]] = []
|
|
168
195
|
|
|
@@ -177,7 +204,23 @@ def test_install_into_venv_skips_logs_restore_when_absent(tmp_path, monkeypatch:
|
|
|
177
204
|
|
|
178
205
|
venvs.install_into_venv(venv, "httpx")
|
|
179
206
|
|
|
180
|
-
assert [
|
|
207
|
+
assert [
|
|
208
|
+
"find",
|
|
209
|
+
venv,
|
|
210
|
+
"-path",
|
|
211
|
+
f"{venv}/logs",
|
|
212
|
+
"-prune",
|
|
213
|
+
"-o",
|
|
214
|
+
"-type",
|
|
215
|
+
"l",
|
|
216
|
+
"-prune",
|
|
217
|
+
"-o",
|
|
218
|
+
"-exec",
|
|
219
|
+
"chmod",
|
|
220
|
+
"u=rwX,go=rX",
|
|
221
|
+
"{}",
|
|
222
|
+
"+",
|
|
223
|
+
] in calls
|
|
181
224
|
assert ["test", "-d", f"{venv}/logs"] in calls
|
|
182
225
|
assert not any(c[:2] == ["chmod", "2775"] for c in calls)
|
|
183
226
|
|
|
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
|
|
File without changes
|