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.

Files changed (40) hide show
  1. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/CHANGELOG.md +33 -0
  2. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/PKG-INFO +1 -1
  3. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/pyproject.toml +1 -1
  4. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/venvs.py +52 -3
  5. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_venvs.py +51 -8
  6. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/.gitignore +0 -0
  7. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/__init__.py +0 -0
  8. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/_paths.py +0 -0
  9. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/become.py +0 -0
  10. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/commands.py +0 -0
  11. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/containers.py +0 -0
  12. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/cron.py +0 -0
  13. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/errors.py +0 -0
  14. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/files.py +0 -0
  15. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/investigate.py +0 -0
  16. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/logs.py +0 -0
  17. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/nc.py +0 -0
  18. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/network.py +0 -0
  19. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/packages.py +0 -0
  20. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/perf.py +0 -0
  21. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/power.py +0 -0
  22. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/security.py +0 -0
  23. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/services.py +0 -0
  24. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/sudoers.py +0 -0
  25. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/runspec_linux_core/system.py +0 -0
  26. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/__init__.py +0 -0
  27. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_become.py +0 -0
  28. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_commands.py +0 -0
  29. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_containers_runas.py +0 -0
  30. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_cron.py +0 -0
  31. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_investigate.py +0 -0
  32. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_logs_services_runas.py +0 -0
  33. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_nc_send.py +0 -0
  34. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_network.py +0 -0
  35. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_packages.py +0 -0
  36. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_paths.py +0 -0
  37. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_perf.py +0 -0
  38. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_power.py +0 -0
  39. {runspec_linux_core-0.6.0 → runspec_linux_core-0.6.2}/tests/test_preconditions.py +0 -0
  40. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: runspec-linux-core
3
- Version: 0.6.0
3
+ Version: 0.6.2
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "runspec-linux-core"
7
- version = "0.6.0"
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. If ``<venv>/logs`` exists (``create_venv`` leaves it there), its
144
- ``2775`` setgid mode is restored after the wide chmod strips it.
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
- _check(_run(build_become(["chmod", "-R", "u=rwX,go=rX", venv], run_as, become_method)), "chmod venv", run_as)
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
- assert ["sudo", "-n", "-u", "svc", "chmod", "-R", "u=rwX,go=rX", venv] in calls
152
- # logs_dir exists (the default stub returns 0 for `test -d`), so the setgid
153
- # mode is restored after the wide chmod would have stripped it.
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 chmod steps run *after* the pip install (order matters — pip
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 "chmod" in c and "u=rwX,go=rX" in c)
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 wide chmod
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 ["chmod", "-R", "u=rwX,go=rX", venv] in calls
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