runspec-linux 0.18.0__tar.gz → 0.19.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-0.18.0 → runspec_linux-0.19.0}/CHANGELOG.md +35 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/PKG-INFO +15 -12
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/README.md +13 -10
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/pyproject.toml +2 -10
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/__init__.py +2 -13
- runspec_linux-0.19.0/runspec_linux/_escalation.py +42 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/cron.py +3 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/fs/cli.py +2 -0
- runspec_linux-0.19.0/runspec_linux/packages.py +27 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/perf.py +1 -20
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/runspec.toml +20 -250
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/venvs.py +6 -0
- runspec_linux-0.19.0/tests/test_escalation.py +56 -0
- runspec_linux-0.18.0/runspec_linux/_escalation.py +0 -20
- runspec_linux-0.18.0/runspec_linux/packages.py +0 -99
- runspec_linux-0.18.0/runspec_linux/power.py +0 -20
- runspec_linux-0.18.0/runspec_linux/sudoers.py +0 -23
- runspec_linux-0.18.0/tests/test_escalation.py +0 -34
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/.gitignore +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/commands.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/containers.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/files.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/fs/__init__.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/fs/_dispatch.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/fs/_worker.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/investigate.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/logs.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/nc_command.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/network.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/security.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/services.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/system_info.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/runspec_linux/webops.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/tests/__init__.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/tests/test_files.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/tests/test_fs.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/tests/test_inventory.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/tests/test_nc_send.py +0 -0
- {runspec_linux-0.18.0 → runspec_linux-0.19.0}/tests/test_webops.py +0 -0
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# runspec-linux Changelog
|
|
2
2
|
|
|
3
|
+
## [0.19.0] — 2026-09-30
|
|
4
|
+
|
|
5
|
+
**No root in runspec-linux.** Everything here now runs as an unprivileged
|
|
6
|
+
account: you, or the service account that owns a shared venv (runspec >= 0.68.0
|
|
7
|
+
runs a shared venv's tools as its owner). The tools that need root moved to the
|
|
8
|
+
new **`runspec-linux-root`** package, for a root-owned admin venv.
|
|
9
|
+
|
|
10
|
+
### Removed (moved to `runspec-linux-root`)
|
|
11
|
+
|
|
12
|
+
- `list-upgrades`, `upgrade-packages`, `install-package`, `remove-package`,
|
|
13
|
+
`autoremove-packages`, `set-sysctl`, `reboot-host`, `enable-passwordless-sudo`.
|
|
14
|
+
Their helpers stay in `runspec-linux-core`. The `runspec_linux` re-exports of
|
|
15
|
+
`install_package`, `list_upgrades`, `reboot_host`, `remove_package`,
|
|
16
|
+
`upgrade_packages` and `enable_passwordless_sudo` are gone: import them from
|
|
17
|
+
`runspec_linux_core`.
|
|
18
|
+
- **`--sudo`** ("run as root") on `restart-service`, `tail-log`, `search-log`,
|
|
19
|
+
`journalctl`, `list-containers`, `container-logs`, `restart-container`,
|
|
20
|
+
`service-detail` and `open-ports`.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **`--run-as root` is refused** on every tool that takes `--run-as` (the ones
|
|
25
|
+
above, the venv tools, `filesystem` and `cron`). It prints a JSON error naming
|
|
26
|
+
`runspec-linux-root` and exits 2. `--run-as` still selects any unprivileged
|
|
27
|
+
account.
|
|
28
|
+
- **`--run-as` naming the account you already are does nothing** (from
|
|
29
|
+
runspec-linux-core 0.13.0). A service account running its own venv's tools with
|
|
30
|
+
`--run-as <itself>` used to need a sudoers rule letting it sudo to itself.
|
|
31
|
+
|
|
32
|
+
### Upgrading
|
|
33
|
+
|
|
34
|
+
Install `runspec-linux-root` into a separate root-owned venv (see its README).
|
|
35
|
+
If an existing venv has both packages, upgrade `runspec-linux` first: pip
|
|
36
|
+
removes the old copies of the moved scripts when it upgrades it.
|
|
37
|
+
|
|
3
38
|
## [0.18.0] — 2026-09-29
|
|
4
39
|
|
|
5
40
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: runspec-linux
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.0
|
|
4
4
|
Summary: Linux system admin runnables for runspec
|
|
5
5
|
Project-URL: Documentation, https://runspec.app/
|
|
6
6
|
Project-URL: Source, https://github.com/jasonfinestone/runspec/tree/main/packages/python/runspec-linux
|
|
@@ -9,7 +9,7 @@ Project-URL: Issues, https://github.com/jasonfinestone/runspec/issues
|
|
|
9
9
|
Keywords: linux,ops,runnable,runspec,ssh,sysadmin,venv
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
11
|
Requires-Dist: runspec-fs-core>=0.5.0
|
|
12
|
-
Requires-Dist: runspec-linux-core>=0.
|
|
12
|
+
Requires-Dist: runspec-linux-core>=0.13.0
|
|
13
13
|
Requires-Dist: runspec-webops-core>=0.3.0
|
|
14
14
|
Requires-Dist: runspec>=0.50.1
|
|
15
15
|
Provides-Extra: dev
|
|
@@ -27,10 +27,15 @@ the runnables are discoverable by `runspec local`, `runspec serve` (MCP), and
|
|
|
27
27
|
runspec-console.
|
|
28
28
|
|
|
29
29
|
All runnables emit JSON on stdout. Read-only runnables are `autonomy =
|
|
30
|
-
"autonomous"`; state-changing ones are `autonomy = "confirm"`.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
"autonomous"`; state-changing ones are `autonomy = "confirm"`.
|
|
31
|
+
|
|
32
|
+
**Nothing here runs as root.** A tool runs as whoever owns its venv (runspec
|
|
33
|
+
>= 0.68.0): you in a personal venv, or the service account in a shared one.
|
|
34
|
+
`--run-as` picks another unprivileged account and refuses `root`. The tools that
|
|
35
|
+
need root (package upgrades/installs, `reboot-host`, `set-sysctl`,
|
|
36
|
+
`enable-passwordless-sudo`) are in
|
|
37
|
+
[`runspec-linux-root`](https://pypi.org/project/runspec-linux-root/), for a
|
|
38
|
+
root-owned admin venv.
|
|
34
39
|
|
|
35
40
|
## Install
|
|
36
41
|
|
|
@@ -47,13 +52,11 @@ pip install runspec-linux
|
|
|
47
52
|
| Services | `list-services`, `check-service`, `restart-service`, `service-detail`, `failed-units` |
|
|
48
53
|
| Logs | `tail-log`, `search-log`, `journalctl` |
|
|
49
54
|
| Network | `network-interfaces`, `ping-host`, `check-port`, `show-connections`, `trace-route`, `route-table`, `socket-stats`, `dns-config`, `open-ports` |
|
|
50
|
-
| Kernel params | `get-sysctl
|
|
55
|
+
| Kernel params | `get-sysctl` |
|
|
51
56
|
| Files | `find-large-files`, `backup-files`, `owning-package` |
|
|
52
57
|
| Sessions | `last-logins`, `who` |
|
|
53
58
|
| Containers | `list-containers`, `container-logs`, `restart-container` |
|
|
54
|
-
| Packages | `list-packages`, `list-
|
|
55
|
-
| Power | `reboot-host` |
|
|
56
|
-
| Sudo bootstrap | `enable-passwordless-sudo` |
|
|
59
|
+
| Packages (read-only) | `list-packages`, `list-autoremovable` |
|
|
57
60
|
| Python venvs | `create-venv`, `install-into-venv`, `configure-pip` |
|
|
58
61
|
| TCP | `nc-command` |
|
|
59
62
|
| Commands | `which` |
|
|
@@ -61,13 +64,13 @@ pip install runspec-linux
|
|
|
61
64
|
| Filesystem | `filesystem` (multi-verb, backed by `runspec-fs-core`) |
|
|
62
65
|
| Web / TLS / API | `cert-check`, `cert-chain`, `tls-info`, `http-check`, `http-headers`, `redirect-trace`, `dns-lookup`, `api-spec` |
|
|
63
66
|
|
|
64
|
-
The package
|
|
67
|
+
The package runnables auto-detect apt / dnf / yum / zypper / pacman.
|
|
65
68
|
The web/TLS/API probes are pure HTTP (shared with `runspec-windows` via
|
|
66
69
|
`runspec-webops-core`) and run anywhere.
|
|
67
70
|
|
|
68
71
|
```
|
|
69
72
|
system-info
|
|
70
|
-
list-
|
|
73
|
+
list-packages --filter nginx
|
|
71
74
|
check-port --host db-01 --port 5432
|
|
72
75
|
cert-check --url https://internal.example.com
|
|
73
76
|
```
|
|
@@ -7,10 +7,15 @@ the runnables are discoverable by `runspec local`, `runspec serve` (MCP), and
|
|
|
7
7
|
runspec-console.
|
|
8
8
|
|
|
9
9
|
All runnables emit JSON on stdout. Read-only runnables are `autonomy =
|
|
10
|
-
"autonomous"`; state-changing ones are `autonomy = "confirm"`.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
"autonomous"`; state-changing ones are `autonomy = "confirm"`.
|
|
11
|
+
|
|
12
|
+
**Nothing here runs as root.** A tool runs as whoever owns its venv (runspec
|
|
13
|
+
>= 0.68.0): you in a personal venv, or the service account in a shared one.
|
|
14
|
+
`--run-as` picks another unprivileged account and refuses `root`. The tools that
|
|
15
|
+
need root (package upgrades/installs, `reboot-host`, `set-sysctl`,
|
|
16
|
+
`enable-passwordless-sudo`) are in
|
|
17
|
+
[`runspec-linux-root`](https://pypi.org/project/runspec-linux-root/), for a
|
|
18
|
+
root-owned admin venv.
|
|
14
19
|
|
|
15
20
|
## Install
|
|
16
21
|
|
|
@@ -27,13 +32,11 @@ pip install runspec-linux
|
|
|
27
32
|
| Services | `list-services`, `check-service`, `restart-service`, `service-detail`, `failed-units` |
|
|
28
33
|
| Logs | `tail-log`, `search-log`, `journalctl` |
|
|
29
34
|
| Network | `network-interfaces`, `ping-host`, `check-port`, `show-connections`, `trace-route`, `route-table`, `socket-stats`, `dns-config`, `open-ports` |
|
|
30
|
-
| Kernel params | `get-sysctl
|
|
35
|
+
| Kernel params | `get-sysctl` |
|
|
31
36
|
| Files | `find-large-files`, `backup-files`, `owning-package` |
|
|
32
37
|
| Sessions | `last-logins`, `who` |
|
|
33
38
|
| Containers | `list-containers`, `container-logs`, `restart-container` |
|
|
34
|
-
| Packages | `list-packages`, `list-
|
|
35
|
-
| Power | `reboot-host` |
|
|
36
|
-
| Sudo bootstrap | `enable-passwordless-sudo` |
|
|
39
|
+
| Packages (read-only) | `list-packages`, `list-autoremovable` |
|
|
37
40
|
| Python venvs | `create-venv`, `install-into-venv`, `configure-pip` |
|
|
38
41
|
| TCP | `nc-command` |
|
|
39
42
|
| Commands | `which` |
|
|
@@ -41,13 +44,13 @@ pip install runspec-linux
|
|
|
41
44
|
| Filesystem | `filesystem` (multi-verb, backed by `runspec-fs-core`) |
|
|
42
45
|
| Web / TLS / API | `cert-check`, `cert-chain`, `tls-info`, `http-check`, `http-headers`, `redirect-trace`, `dns-lookup`, `api-spec` |
|
|
43
46
|
|
|
44
|
-
The package
|
|
47
|
+
The package runnables auto-detect apt / dnf / yum / zypper / pacman.
|
|
45
48
|
The web/TLS/API probes are pure HTTP (shared with `runspec-windows` via
|
|
46
49
|
`runspec-webops-core`) and run anywhere.
|
|
47
50
|
|
|
48
51
|
```
|
|
49
52
|
system-info
|
|
50
|
-
list-
|
|
53
|
+
list-packages --filter nginx
|
|
51
54
|
check-port --host db-01 --port 5432
|
|
52
55
|
cert-check --url https://internal.example.com
|
|
53
56
|
```
|
|
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "runspec-linux"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.19.0"
|
|
8
8
|
requires-python = ">=3.10"
|
|
9
9
|
description = "Linux system admin runnables for runspec"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
keywords = ["runspec", "runnable", "linux", "sysadmin", "ops", "ssh", "venv"]
|
|
12
12
|
dependencies = [
|
|
13
13
|
"runspec>=0.50.1",
|
|
14
|
-
"runspec-linux-core>=0.
|
|
14
|
+
"runspec-linux-core>=0.13.0",
|
|
15
15
|
"runspec-webops-core>=0.3.0",
|
|
16
16
|
"runspec-fs-core>=0.5.0",
|
|
17
17
|
]
|
|
@@ -43,7 +43,6 @@ socket-stats = "runspec_linux.network:main_socket_stats"
|
|
|
43
43
|
dns-config = "runspec_linux.network:main_dns_config"
|
|
44
44
|
network-interfaces = "runspec_linux.network:main_network_interfaces"
|
|
45
45
|
get-sysctl = "runspec_linux.perf:main_get_sysctl"
|
|
46
|
-
set-sysctl = "runspec_linux.perf:main_set_sysctl"
|
|
47
46
|
cpu-info = "runspec_linux.perf:main_cpu_info"
|
|
48
47
|
pressure = "runspec_linux.perf:main_pressure"
|
|
49
48
|
service-detail = "runspec_linux.investigate:main_service_detail"
|
|
@@ -58,14 +57,7 @@ list-containers = "runspec_linux.containers:main_list_containers"
|
|
|
58
57
|
container-logs = "runspec_linux.containers:main_container_logs"
|
|
59
58
|
restart-container = "runspec_linux.containers:main_restart_container"
|
|
60
59
|
list-packages = "runspec_linux.packages:main_list_packages"
|
|
61
|
-
list-upgrades = "runspec_linux.packages:main_list_upgrades"
|
|
62
|
-
upgrade-packages = "runspec_linux.packages:main_upgrade_packages"
|
|
63
|
-
install-package = "runspec_linux.packages:main_install_package"
|
|
64
|
-
remove-package = "runspec_linux.packages:main_remove_package"
|
|
65
|
-
autoremove-packages = "runspec_linux.packages:main_autoremove_packages"
|
|
66
60
|
list-autoremovable = "runspec_linux.packages:main_list_autoremovable"
|
|
67
|
-
reboot-host = "runspec_linux.power:main_reboot_host"
|
|
68
|
-
enable-passwordless-sudo = "runspec_linux.sudoers:main_enable_passwordless_sudo"
|
|
69
61
|
create-venv = "runspec_linux.venvs:main_create_venv"
|
|
70
62
|
install-into-venv = "runspec_linux.venvs:main_install_into_venv"
|
|
71
63
|
configure-pip = "runspec_linux.venvs:main_configure_pip"
|
|
@@ -4,7 +4,8 @@ The pure logic lives in the dependency-free ``runspec-linux-core`` package; the
|
|
|
4
4
|
modules here wrap each helper in a runspec runnable. The helper functions are
|
|
5
5
|
re-exported for convenience and back-compat (e.g. ``from runspec_linux import
|
|
6
6
|
nc_send``). To import the helpers *without* surfacing any runnables in a venv,
|
|
7
|
-
depend on ``runspec-linux-core`` directly instead.
|
|
7
|
+
depend on ``runspec-linux-core`` directly instead. Tools that need root live in
|
|
8
|
+
``runspec-linux-root``; their helpers are in ``runspec-linux-core``.
|
|
8
9
|
"""
|
|
9
10
|
|
|
10
11
|
from runspec_linux_core import (
|
|
@@ -19,10 +20,8 @@ from runspec_linux_core import (
|
|
|
19
20
|
create_venv,
|
|
20
21
|
detect_manager,
|
|
21
22
|
disk_usage,
|
|
22
|
-
enable_passwordless_sudo,
|
|
23
23
|
find_large_files,
|
|
24
24
|
install_into_venv,
|
|
25
|
-
install_package,
|
|
26
25
|
journalctl,
|
|
27
26
|
last_logins,
|
|
28
27
|
list_containers,
|
|
@@ -30,19 +29,15 @@ from runspec_linux_core import (
|
|
|
30
29
|
list_packages,
|
|
31
30
|
list_processes,
|
|
32
31
|
list_services,
|
|
33
|
-
list_upgrades,
|
|
34
32
|
nc_send,
|
|
35
33
|
ping_host,
|
|
36
|
-
reboot_host,
|
|
37
34
|
remove_cron,
|
|
38
|
-
remove_package,
|
|
39
35
|
restart_container,
|
|
40
36
|
restart_service,
|
|
41
37
|
search_log,
|
|
42
38
|
show_connections,
|
|
43
39
|
system_info,
|
|
44
40
|
tail_log,
|
|
45
|
-
upgrade_packages,
|
|
46
41
|
which,
|
|
47
42
|
who,
|
|
48
43
|
)
|
|
@@ -59,10 +54,8 @@ __all__ = [
|
|
|
59
54
|
"create_venv",
|
|
60
55
|
"detect_manager",
|
|
61
56
|
"disk_usage",
|
|
62
|
-
"enable_passwordless_sudo",
|
|
63
57
|
"find_large_files",
|
|
64
58
|
"install_into_venv",
|
|
65
|
-
"install_package",
|
|
66
59
|
"journalctl",
|
|
67
60
|
"last_logins",
|
|
68
61
|
"list_containers",
|
|
@@ -70,19 +63,15 @@ __all__ = [
|
|
|
70
63
|
"list_packages",
|
|
71
64
|
"list_processes",
|
|
72
65
|
"list_services",
|
|
73
|
-
"list_upgrades",
|
|
74
66
|
"nc_send",
|
|
75
67
|
"ping_host",
|
|
76
|
-
"reboot_host",
|
|
77
68
|
"remove_cron",
|
|
78
|
-
"remove_package",
|
|
79
69
|
"restart_container",
|
|
80
70
|
"restart_service",
|
|
81
71
|
"search_log",
|
|
82
72
|
"show_connections",
|
|
83
73
|
"system_info",
|
|
84
74
|
"tail_log",
|
|
85
|
-
"upgrade_packages",
|
|
86
75
|
"which",
|
|
87
76
|
"who",
|
|
88
77
|
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Shared wrapper helper for the ``--run-as`` / ``--become-method`` escalation args.
|
|
2
|
+
|
|
3
|
+
runspec-linux never escalates to root: tools that need root live in the separate
|
|
4
|
+
``runspec-linux-root`` package, installed into a root-owned venv. ``--run-as``
|
|
5
|
+
picks an unprivileged account (typically the service account that owns a shared
|
|
6
|
+
venv); a root target is refused before anything runs.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
ROOT_REFUSAL = "runspec-linux does not run anything as root — root tools live in runspec-linux-root"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_root(name: str) -> bool:
|
|
19
|
+
"""True when ``name`` names uid 0 (``root``, ``0`` or any alias)."""
|
|
20
|
+
if name in ("root", "0"):
|
|
21
|
+
return True
|
|
22
|
+
try:
|
|
23
|
+
import pwd
|
|
24
|
+
|
|
25
|
+
return pwd.getpwnam(name).pw_uid == 0
|
|
26
|
+
except (ImportError, KeyError):
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def refuse_root(run_as: str | None) -> None:
|
|
31
|
+
"""Print a JSON error and exit 2 when ``run_as`` targets root."""
|
|
32
|
+
if run_as and is_root(run_as):
|
|
33
|
+
print(json.dumps({"error": f"--run-as {run_as}: {ROOT_REFUSAL}"}))
|
|
34
|
+
sys.exit(2)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def resolve_escalation(spec: Any) -> tuple[str | None, str]:
|
|
38
|
+
"""Return ``(run_as, become_method)`` from a spec carrying the escalation args
|
|
39
|
+
(``None`` = no escalation, runs as the caller). Exits on a root target."""
|
|
40
|
+
run_as = spec.run_as.value or None
|
|
41
|
+
refuse_root(run_as)
|
|
42
|
+
return run_as, spec.become_method.value
|
|
@@ -23,6 +23,8 @@ import runspec as rs
|
|
|
23
23
|
from runspec_linux_core.cron import add_cron, clear_cron, list_cron, remove_cron
|
|
24
24
|
from runspec_linux_core.errors import CommandError, ToolNotFoundError
|
|
25
25
|
|
|
26
|
+
from runspec_linux._escalation import refuse_root
|
|
27
|
+
|
|
26
28
|
|
|
27
29
|
def _emit(payload: dict[str, Any]) -> None:
|
|
28
30
|
print(json.dumps(payload, default=str))
|
|
@@ -31,6 +33,7 @@ def _emit(payload: dict[str, Any]) -> None:
|
|
|
31
33
|
def _run(spec: rs.RunSpec) -> dict[str, Any]:
|
|
32
34
|
command = spec.runspec_command
|
|
33
35
|
run_as = spec.run_as.value or None
|
|
36
|
+
refuse_root(run_as)
|
|
34
37
|
become_method = spec.become_method.value
|
|
35
38
|
|
|
36
39
|
if command == "list":
|
|
@@ -26,6 +26,7 @@ from typing import Any
|
|
|
26
26
|
import runspec as rs
|
|
27
27
|
from runspec_fs_core import BecomeError, FsCoreError, build_become, current_user, escalation_supported
|
|
28
28
|
|
|
29
|
+
from runspec_linux._escalation import refuse_root
|
|
29
30
|
from runspec_linux.fs._dispatch import run_verb
|
|
30
31
|
|
|
31
32
|
|
|
@@ -115,6 +116,7 @@ def _run_as_worker(run_as: str, method: str, verb: str, kwargs: dict[str, Any])
|
|
|
115
116
|
def main() -> None:
|
|
116
117
|
spec = rs.parse("filesystem")
|
|
117
118
|
run_as = spec.run_as.value
|
|
119
|
+
refuse_root(run_as or None)
|
|
118
120
|
method = spec.become_method.value
|
|
119
121
|
try:
|
|
120
122
|
kwargs = _verb_kwargs(spec)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import runspec as rs
|
|
5
|
+
from runspec_linux_core import list_autoremovable, list_packages
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main_list_packages() -> None:
|
|
9
|
+
spec = rs.parse("list-packages")
|
|
10
|
+
name_filter = spec.filter.value
|
|
11
|
+
limit = spec.limit.value
|
|
12
|
+
try:
|
|
13
|
+
print(json.dumps(list_packages(name_filter, limit, reason=spec.reason.value)))
|
|
14
|
+
except Exception as e:
|
|
15
|
+
# Includes ToolNotFoundError (no package manager) and CommandError.
|
|
16
|
+
print(json.dumps({"error": str(e)}))
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main_list_autoremovable() -> None:
|
|
21
|
+
spec = rs.parse("list-autoremovable")
|
|
22
|
+
try:
|
|
23
|
+
print(json.dumps(list_autoremovable(purge=spec.purge.value)))
|
|
24
|
+
except Exception as e:
|
|
25
|
+
# Includes CommandError (yum, failed listing) and ToolNotFoundError.
|
|
26
|
+
print(json.dumps({"error": str(e)}))
|
|
27
|
+
sys.exit(1)
|
|
@@ -4,10 +4,7 @@ import json
|
|
|
4
4
|
import sys
|
|
5
5
|
|
|
6
6
|
import runspec as rs
|
|
7
|
-
from runspec_linux_core import cpu_info, get_sysctl, pressure
|
|
8
|
-
from runspec_linux_core.errors import CommandError
|
|
9
|
-
|
|
10
|
-
from runspec_linux._escalation import resolve_escalation
|
|
7
|
+
from runspec_linux_core import cpu_info, get_sysctl, pressure
|
|
11
8
|
|
|
12
9
|
|
|
13
10
|
def main_get_sysctl() -> None:
|
|
@@ -20,22 +17,6 @@ def main_get_sysctl() -> None:
|
|
|
20
17
|
sys.exit(1)
|
|
21
18
|
|
|
22
19
|
|
|
23
|
-
def main_set_sysctl() -> None:
|
|
24
|
-
spec = rs.parse("set-sysctl")
|
|
25
|
-
key = spec.key.value
|
|
26
|
-
value = spec.value.value
|
|
27
|
-
persist = spec.persist.value
|
|
28
|
-
run_as, method = resolve_escalation(spec)
|
|
29
|
-
try:
|
|
30
|
-
print(json.dumps(set_sysctl(key, value, persist=persist, run_as=run_as, become_method=method)))
|
|
31
|
-
except CommandError as e:
|
|
32
|
-
print(json.dumps({"key": key, "applied": False, "error": str(e)}))
|
|
33
|
-
sys.exit(1)
|
|
34
|
-
except Exception as e:
|
|
35
|
-
print(json.dumps({"error": str(e)}))
|
|
36
|
-
sys.exit(1)
|
|
37
|
-
|
|
38
|
-
|
|
39
20
|
def main_cpu_info() -> None:
|
|
40
21
|
rs.parse("cpu-info")
|
|
41
22
|
try:
|
|
@@ -96,24 +96,15 @@ description = "Service name to restart"
|
|
|
96
96
|
# Escalation: restarting a system service needs privilege.
|
|
97
97
|
[restart-service.args.run-as]
|
|
98
98
|
type = "str"
|
|
99
|
-
description = "Run as this specific OS user (via sudo -n -u / su).
|
|
99
|
+
description = "Run as this specific OS user (via sudo -n -u / su). Never root — root tools live in runspec-linux-root."
|
|
100
100
|
required = false
|
|
101
101
|
|
|
102
|
-
[restart-service.args.sudo]
|
|
103
|
-
type = "flag"
|
|
104
|
-
description = "Run as root (via sudo). Mutually exclusive with --run-as."
|
|
105
|
-
default = false
|
|
106
|
-
|
|
107
102
|
[restart-service.args.become-method]
|
|
108
103
|
type = "choice"
|
|
109
104
|
options = ["sudo", "su"]
|
|
110
|
-
description = "How to escalate for --run-as
|
|
105
|
+
description = "How to escalate for --run-as: sudo (passwordless, -n) or su."
|
|
111
106
|
default = "sudo"
|
|
112
107
|
|
|
113
|
-
[restart-service.groups.escalation]
|
|
114
|
-
exclusive = true
|
|
115
|
-
args = ["run-as", "sudo"]
|
|
116
|
-
|
|
117
108
|
# ── Logs ──────────────────────────────────────────────────────────────────────
|
|
118
109
|
|
|
119
110
|
[tail-log]
|
|
@@ -133,27 +124,18 @@ short = "-n"
|
|
|
133
124
|
description = "Number of lines to return"
|
|
134
125
|
default = 50
|
|
135
126
|
|
|
136
|
-
# Escalation: read a
|
|
127
|
+
# Escalation: read a service-account-owned log (tail -n under sudo/su).
|
|
137
128
|
[tail-log.args.run-as]
|
|
138
129
|
type = "str"
|
|
139
|
-
description = "Read as this specific OS user (via sudo -n -u / su).
|
|
130
|
+
description = "Read as this specific OS user (via sudo -n -u / su). Never root — root tools live in runspec-linux-root."
|
|
140
131
|
required = false
|
|
141
132
|
|
|
142
|
-
[tail-log.args.sudo]
|
|
143
|
-
type = "flag"
|
|
144
|
-
description = "Read as root (via sudo). Mutually exclusive with --run-as."
|
|
145
|
-
default = false
|
|
146
|
-
|
|
147
133
|
[tail-log.args.become-method]
|
|
148
134
|
type = "choice"
|
|
149
135
|
options = ["sudo", "su"]
|
|
150
|
-
description = "How to escalate for --run-as
|
|
136
|
+
description = "How to escalate for --run-as: sudo (passwordless, -n) or su."
|
|
151
137
|
default = "sudo"
|
|
152
138
|
|
|
153
|
-
[tail-log.groups.escalation]
|
|
154
|
-
exclusive = true
|
|
155
|
-
args = ["run-as", "sudo"]
|
|
156
|
-
|
|
157
139
|
[search-log]
|
|
158
140
|
serve = ["local"]
|
|
159
141
|
description = "Search a log file for lines matching a pattern"
|
|
@@ -176,27 +158,18 @@ short = "-n"
|
|
|
176
158
|
description = "Maximum number of matching lines to return"
|
|
177
159
|
default = 100
|
|
178
160
|
|
|
179
|
-
# Escalation: search a
|
|
161
|
+
# Escalation: search a service-account-owned log (cat under sudo/su).
|
|
180
162
|
[search-log.args.run-as]
|
|
181
163
|
type = "str"
|
|
182
|
-
description = "Read as this specific OS user (via sudo -n -u / su).
|
|
164
|
+
description = "Read as this specific OS user (via sudo -n -u / su). Never root — root tools live in runspec-linux-root."
|
|
183
165
|
required = false
|
|
184
166
|
|
|
185
|
-
[search-log.args.sudo]
|
|
186
|
-
type = "flag"
|
|
187
|
-
description = "Read as root (via sudo). Mutually exclusive with --run-as."
|
|
188
|
-
default = false
|
|
189
|
-
|
|
190
167
|
[search-log.args.become-method]
|
|
191
168
|
type = "choice"
|
|
192
169
|
options = ["sudo", "su"]
|
|
193
|
-
description = "How to escalate for --run-as
|
|
170
|
+
description = "How to escalate for --run-as: sudo (passwordless, -n) or su."
|
|
194
171
|
default = "sudo"
|
|
195
172
|
|
|
196
|
-
[search-log.groups.escalation]
|
|
197
|
-
exclusive = true
|
|
198
|
-
args = ["run-as", "sudo"]
|
|
199
|
-
|
|
200
173
|
[journalctl]
|
|
201
174
|
serve = ["local"]
|
|
202
175
|
description = "Fetch recent journal entries for a systemd unit (systemd-only)"
|
|
@@ -222,24 +195,15 @@ required = false
|
|
|
222
195
|
# Escalation: read a protected unit's journal (outside the systemd-journal group).
|
|
223
196
|
[journalctl.args.run-as]
|
|
224
197
|
type = "str"
|
|
225
|
-
description = "Read as this specific OS user (via sudo -n -u / su).
|
|
198
|
+
description = "Read as this specific OS user (via sudo -n -u / su). Never root — root tools live in runspec-linux-root."
|
|
226
199
|
required = false
|
|
227
200
|
|
|
228
|
-
[journalctl.args.sudo]
|
|
229
|
-
type = "flag"
|
|
230
|
-
description = "Read as root (via sudo). Mutually exclusive with --run-as."
|
|
231
|
-
default = false
|
|
232
|
-
|
|
233
201
|
[journalctl.args.become-method]
|
|
234
202
|
type = "choice"
|
|
235
203
|
options = ["sudo", "su"]
|
|
236
|
-
description = "How to escalate for --run-as
|
|
204
|
+
description = "How to escalate for --run-as: sudo (passwordless, -n) or su."
|
|
237
205
|
default = "sudo"
|
|
238
206
|
|
|
239
|
-
[journalctl.groups.escalation]
|
|
240
|
-
exclusive = true
|
|
241
|
-
args = ["run-as", "sudo"]
|
|
242
|
-
|
|
243
207
|
# ── Network ───────────────────────────────────────────────────────────────────
|
|
244
208
|
|
|
245
209
|
[ping-host]
|
|
@@ -375,24 +339,15 @@ default = false
|
|
|
375
339
|
# Escalation: the Docker daemon socket is usually root/docker-group owned.
|
|
376
340
|
[list-containers.args.run-as]
|
|
377
341
|
type = "str"
|
|
378
|
-
description = "Run docker as this specific OS user (via sudo -n -u / su).
|
|
342
|
+
description = "Run docker as this specific OS user (via sudo -n -u / su). Never root — root tools live in runspec-linux-root."
|
|
379
343
|
required = false
|
|
380
344
|
|
|
381
|
-
[list-containers.args.sudo]
|
|
382
|
-
type = "flag"
|
|
383
|
-
description = "Run docker as root (via sudo). Mutually exclusive with --run-as."
|
|
384
|
-
default = false
|
|
385
|
-
|
|
386
345
|
[list-containers.args.become-method]
|
|
387
346
|
type = "choice"
|
|
388
347
|
options = ["sudo", "su"]
|
|
389
|
-
description = "How to escalate for --run-as
|
|
348
|
+
description = "How to escalate for --run-as: sudo (passwordless, -n) or su."
|
|
390
349
|
default = "sudo"
|
|
391
350
|
|
|
392
|
-
[list-containers.groups.escalation]
|
|
393
|
-
exclusive = true
|
|
394
|
-
args = ["run-as", "sudo"]
|
|
395
|
-
|
|
396
351
|
[container-logs]
|
|
397
352
|
serve = ["local"]
|
|
398
353
|
description = "Fetch recent log output from a Docker container"
|
|
@@ -413,24 +368,15 @@ default = 50
|
|
|
413
368
|
# Escalation: the Docker daemon socket is usually root/docker-group owned.
|
|
414
369
|
[container-logs.args.run-as]
|
|
415
370
|
type = "str"
|
|
416
|
-
description = "Run docker as this specific OS user (via sudo -n -u / su).
|
|
371
|
+
description = "Run docker as this specific OS user (via sudo -n -u / su). Never root — root tools live in runspec-linux-root."
|
|
417
372
|
required = false
|
|
418
373
|
|
|
419
|
-
[container-logs.args.sudo]
|
|
420
|
-
type = "flag"
|
|
421
|
-
description = "Run docker as root (via sudo). Mutually exclusive with --run-as."
|
|
422
|
-
default = false
|
|
423
|
-
|
|
424
374
|
[container-logs.args.become-method]
|
|
425
375
|
type = "choice"
|
|
426
376
|
options = ["sudo", "su"]
|
|
427
|
-
description = "How to escalate for --run-as
|
|
377
|
+
description = "How to escalate for --run-as: sudo (passwordless, -n) or su."
|
|
428
378
|
default = "sudo"
|
|
429
379
|
|
|
430
|
-
[container-logs.groups.escalation]
|
|
431
|
-
exclusive = true
|
|
432
|
-
args = ["run-as", "sudo"]
|
|
433
|
-
|
|
434
380
|
[restart-container]
|
|
435
381
|
serve = ["local"]
|
|
436
382
|
description = "Restart a Docker container"
|
|
@@ -445,24 +391,15 @@ description = "Container name or ID"
|
|
|
445
391
|
# Escalation: the Docker daemon socket is usually root/docker-group owned.
|
|
446
392
|
[restart-container.args.run-as]
|
|
447
393
|
type = "str"
|
|
448
|
-
description = "Run docker as this specific OS user (via sudo -n -u / su).
|
|
394
|
+
description = "Run docker as this specific OS user (via sudo -n -u / su). Never root — root tools live in runspec-linux-root."
|
|
449
395
|
required = false
|
|
450
396
|
|
|
451
|
-
[restart-container.args.sudo]
|
|
452
|
-
type = "flag"
|
|
453
|
-
description = "Run docker as root (via sudo). Mutually exclusive with --run-as."
|
|
454
|
-
default = false
|
|
455
|
-
|
|
456
397
|
[restart-container.args.become-method]
|
|
457
398
|
type = "choice"
|
|
458
399
|
options = ["sudo", "su"]
|
|
459
|
-
description = "How to escalate for --run-as
|
|
400
|
+
description = "How to escalate for --run-as: sudo (passwordless, -n) or su."
|
|
460
401
|
default = "sudo"
|
|
461
402
|
|
|
462
|
-
[restart-container.groups.escalation]
|
|
463
|
-
exclusive = true
|
|
464
|
-
args = ["run-as", "sudo"]
|
|
465
|
-
|
|
466
403
|
# ── Packages ──────────────────────────────────────────────────────────────────
|
|
467
404
|
|
|
468
405
|
[list-packages]
|
|
@@ -488,54 +425,6 @@ type = "flag"
|
|
|
488
425
|
description = "Also say whether each package was installed by name (manual) or as a dependency (auto) — autoremove only ever removes auto packages"
|
|
489
426
|
default = false
|
|
490
427
|
|
|
491
|
-
[list-upgrades]
|
|
492
|
-
serve = ["local"]
|
|
493
|
-
description = "List packages with an available upgrade (refreshes the package index first)"
|
|
494
|
-
autonomy = "autonomous"
|
|
495
|
-
output = "json"
|
|
496
|
-
run_as = "root"
|
|
497
|
-
|
|
498
|
-
[list-upgrades.args.no-refresh]
|
|
499
|
-
type = "flag"
|
|
500
|
-
description = "Skip refreshing the package index — list from the cached index (faster)"
|
|
501
|
-
default = false
|
|
502
|
-
|
|
503
|
-
[upgrade-packages]
|
|
504
|
-
serve = ["local"]
|
|
505
|
-
description = "Refresh the package index and upgrade all installed packages (needs root)"
|
|
506
|
-
autonomy = "confirm"
|
|
507
|
-
output = "json"
|
|
508
|
-
run_as = "root"
|
|
509
|
-
|
|
510
|
-
[install-package]
|
|
511
|
-
serve = ["local"]
|
|
512
|
-
description = "Install one or more packages by name (needs root)"
|
|
513
|
-
autonomy = "confirm"
|
|
514
|
-
output = "json"
|
|
515
|
-
run_as = "root"
|
|
516
|
-
|
|
517
|
-
[install-package.args.package]
|
|
518
|
-
type = "str"
|
|
519
|
-
short = "-p"
|
|
520
|
-
description = "Package name to install (space-separated for several)"
|
|
521
|
-
|
|
522
|
-
[remove-package]
|
|
523
|
-
serve = ["local"]
|
|
524
|
-
description = "Remove one or more packages by name (needs root)"
|
|
525
|
-
autonomy = "confirm"
|
|
526
|
-
output = "json"
|
|
527
|
-
run_as = "root"
|
|
528
|
-
|
|
529
|
-
[remove-package.args.package]
|
|
530
|
-
type = "str"
|
|
531
|
-
short = "-p"
|
|
532
|
-
description = "Package name to remove (space-separated for several)"
|
|
533
|
-
|
|
534
|
-
[remove-package.args.purge]
|
|
535
|
-
type = "flag"
|
|
536
|
-
description = "Also delete the packages' configuration files (apt purge / pacman -Rn; a plain remove on dnf/yum/zypper)"
|
|
537
|
-
default = false
|
|
538
|
-
|
|
539
428
|
[list-autoremovable]
|
|
540
429
|
serve = ["local"]
|
|
541
430
|
description = "List the packages autoremove-packages would remove (read-only preview; not supported on yum)"
|
|
@@ -547,66 +436,6 @@ type = "flag"
|
|
|
547
436
|
description = "Preview a purge (apt shows config files would go too)"
|
|
548
437
|
default = false
|
|
549
438
|
|
|
550
|
-
[autoremove-packages]
|
|
551
|
-
serve = ["local"]
|
|
552
|
-
description = "Remove packages nothing depends on any more, e.g. old kernels (apt autoremove and equivalents; needs root)"
|
|
553
|
-
autonomy = "confirm"
|
|
554
|
-
output = "json"
|
|
555
|
-
run_as = "root"
|
|
556
|
-
|
|
557
|
-
[autoremove-packages.args.purge]
|
|
558
|
-
type = "flag"
|
|
559
|
-
description = "Also delete the removed packages' configuration files (apt --purge / pacman -n)"
|
|
560
|
-
default = false
|
|
561
|
-
|
|
562
|
-
[autoremove-packages.args.dry-run]
|
|
563
|
-
type = "flag"
|
|
564
|
-
description = "Only list what would be removed; remove nothing (not supported on yum). list-autoremovable does the same without needing approval"
|
|
565
|
-
default = false
|
|
566
|
-
|
|
567
|
-
# ── Power ─────────────────────────────────────────────────────────────────────
|
|
568
|
-
|
|
569
|
-
[reboot-host]
|
|
570
|
-
serve = ["local"]
|
|
571
|
-
description = "Reboot the host, optionally after a delay in minutes (needs root)"
|
|
572
|
-
autonomy = "confirm"
|
|
573
|
-
output = "json"
|
|
574
|
-
run_as = "root"
|
|
575
|
-
|
|
576
|
-
[reboot-host.args.delay-minutes]
|
|
577
|
-
type = "int"
|
|
578
|
-
short = "-d"
|
|
579
|
-
description = "Minutes to wait before rebooting (0 = immediately)"
|
|
580
|
-
default = 0
|
|
581
|
-
|
|
582
|
-
# ── Privilege bootstrap ───────────────────────────────────────────────────────
|
|
583
|
-
|
|
584
|
-
# One-time setup so the run_as="root" runnables above need no password. Itself
|
|
585
|
-
# escalates with the supplied password via `sudo -S` (read from stdin, never
|
|
586
|
-
# argv), so it must NOT declare run_as. Validated with visudo before + after
|
|
587
|
-
# install and rolled back on failure.
|
|
588
|
-
[enable-passwordless-sudo]
|
|
589
|
-
serve = ["local"]
|
|
590
|
-
description = "Install a validated /etc/sudoers.d drop-in so run_as='root' runnables need no password (one-time bootstrap)"
|
|
591
|
-
autonomy = "confirm"
|
|
592
|
-
output = "json"
|
|
593
|
-
|
|
594
|
-
[enable-passwordless-sudo.args.password]
|
|
595
|
-
type = "password"
|
|
596
|
-
description = "Your sudo password — sent over the secret channel and piped to 'sudo -S'; never appears in argv, ps, or logs"
|
|
597
|
-
|
|
598
|
-
[enable-passwordless-sudo.args.user]
|
|
599
|
-
type = "str"
|
|
600
|
-
short = "-u"
|
|
601
|
-
description = "User to grant passwordless sudo (defaults to the current login user)"
|
|
602
|
-
required = false
|
|
603
|
-
|
|
604
|
-
[enable-passwordless-sudo.args.scope]
|
|
605
|
-
type = "choice"
|
|
606
|
-
options = ["scoped", "all"]
|
|
607
|
-
default = "scoped"
|
|
608
|
-
description = "scoped = NOPASSWD only for the runspec venv runnables (manual/CLI runs); all = NOPASSWD: ALL (also covers agent-driven runs)"
|
|
609
|
-
|
|
610
439
|
# ── TCP interfaces (nc-command) ────────────────────────────────────────────────
|
|
611
440
|
|
|
612
441
|
[nc-command]
|
|
@@ -1237,47 +1066,6 @@ short = "-p"
|
|
|
1237
1066
|
description = "Only return keys containing this substring (e.g. 'net.core')"
|
|
1238
1067
|
required = false
|
|
1239
1068
|
|
|
1240
|
-
[set-sysctl]
|
|
1241
|
-
serve = ["local"]
|
|
1242
|
-
description = "Set a kernel parameter (sysctl -w), optionally persisting it under /etc/sysctl.d"
|
|
1243
|
-
autonomy = "confirm"
|
|
1244
|
-
output = "json"
|
|
1245
|
-
|
|
1246
|
-
[set-sysctl.args.key]
|
|
1247
|
-
type = "str"
|
|
1248
|
-
short = "-k"
|
|
1249
|
-
description = "Dotted sysctl key (e.g. net.core.somaxconn)"
|
|
1250
|
-
|
|
1251
|
-
[set-sysctl.args.value]
|
|
1252
|
-
type = "str"
|
|
1253
|
-
description = "New value to set"
|
|
1254
|
-
|
|
1255
|
-
[set-sysctl.args.persist]
|
|
1256
|
-
type = "flag"
|
|
1257
|
-
description = "Also append the setting to /etc/sysctl.d/99-runspec.conf so it survives a reboot"
|
|
1258
|
-
default = false
|
|
1259
|
-
|
|
1260
|
-
# Escalation: writing a kernel parameter is privileged.
|
|
1261
|
-
[set-sysctl.args.run-as]
|
|
1262
|
-
type = "str"
|
|
1263
|
-
description = "Apply as this specific OS user (via sudo -n -u / su). Mutually exclusive with --sudo."
|
|
1264
|
-
required = false
|
|
1265
|
-
|
|
1266
|
-
[set-sysctl.args.sudo]
|
|
1267
|
-
type = "flag"
|
|
1268
|
-
description = "Apply as root (via sudo). Mutually exclusive with --run-as."
|
|
1269
|
-
default = false
|
|
1270
|
-
|
|
1271
|
-
[set-sysctl.args.become-method]
|
|
1272
|
-
type = "choice"
|
|
1273
|
-
options = ["sudo", "su"]
|
|
1274
|
-
description = "How to escalate for --run-as/--sudo: sudo (passwordless, -n) or su."
|
|
1275
|
-
default = "sudo"
|
|
1276
|
-
|
|
1277
|
-
[set-sysctl.groups.escalation]
|
|
1278
|
-
exclusive = true
|
|
1279
|
-
args = ["run-as", "sudo"]
|
|
1280
|
-
|
|
1281
1069
|
[cpu-info]
|
|
1282
1070
|
serve = ["local"]
|
|
1283
1071
|
description = "Report CPU model, core count, and per-CPU scaling governor and frequency"
|
|
@@ -1306,24 +1094,15 @@ description = "Systemd unit name (e.g. nginx or nginx.service)"
|
|
|
1306
1094
|
# Escalation: some properties on protected units need privilege.
|
|
1307
1095
|
[service-detail.args.run-as]
|
|
1308
1096
|
type = "str"
|
|
1309
|
-
description = "Read as this specific OS user (via sudo -n -u / su).
|
|
1097
|
+
description = "Read as this specific OS user (via sudo -n -u / su). Never root — root tools live in runspec-linux-root."
|
|
1310
1098
|
required = false
|
|
1311
1099
|
|
|
1312
|
-
[service-detail.args.sudo]
|
|
1313
|
-
type = "flag"
|
|
1314
|
-
description = "Read as root (via sudo). Mutually exclusive with --run-as."
|
|
1315
|
-
default = false
|
|
1316
|
-
|
|
1317
1100
|
[service-detail.args.become-method]
|
|
1318
1101
|
type = "choice"
|
|
1319
1102
|
options = ["sudo", "su"]
|
|
1320
|
-
description = "How to escalate for --run-as
|
|
1103
|
+
description = "How to escalate for --run-as: sudo (passwordless, -n) or su."
|
|
1321
1104
|
default = "sudo"
|
|
1322
1105
|
|
|
1323
|
-
[service-detail.groups.escalation]
|
|
1324
|
-
exclusive = true
|
|
1325
|
-
args = ["run-as", "sudo"]
|
|
1326
|
-
|
|
1327
1106
|
[open-ports]
|
|
1328
1107
|
serve = ["local"]
|
|
1329
1108
|
description = "List listening sockets mapped to the owning process (port to pid to process name)"
|
|
@@ -1339,24 +1118,15 @@ default = "tcp"
|
|
|
1339
1118
|
# Escalation: the full pid/process map needs privilege (ss shows other users' processes only as root).
|
|
1340
1119
|
[open-ports.args.run-as]
|
|
1341
1120
|
type = "str"
|
|
1342
|
-
description = "Probe as this specific OS user (via sudo -n -u / su).
|
|
1121
|
+
description = "Probe as this specific OS user (via sudo -n -u / su). Never root — root tools live in runspec-linux-root."
|
|
1343
1122
|
required = false
|
|
1344
1123
|
|
|
1345
|
-
[open-ports.args.sudo]
|
|
1346
|
-
type = "flag"
|
|
1347
|
-
description = "Probe as root (via sudo). Mutually exclusive with --run-as."
|
|
1348
|
-
default = false
|
|
1349
|
-
|
|
1350
1124
|
[open-ports.args.become-method]
|
|
1351
1125
|
type = "choice"
|
|
1352
1126
|
options = ["sudo", "su"]
|
|
1353
|
-
description = "How to escalate for --run-as
|
|
1127
|
+
description = "How to escalate for --run-as: sudo (passwordless, -n) or su."
|
|
1354
1128
|
default = "sudo"
|
|
1355
1129
|
|
|
1356
|
-
[open-ports.groups.escalation]
|
|
1357
|
-
exclusive = true
|
|
1358
|
-
args = ["run-as", "sudo"]
|
|
1359
|
-
|
|
1360
1130
|
[owning-package]
|
|
1361
1131
|
serve = ["local"]
|
|
1362
1132
|
description = "Report which installed package owns a file path (dpkg / rpm)"
|
|
@@ -5,9 +5,12 @@ import runspec as rs
|
|
|
5
5
|
from runspec_linux_core import configure_pip, create_venv, install_into_venv, share_venv_logs
|
|
6
6
|
from runspec_linux_core.errors import CommandError
|
|
7
7
|
|
|
8
|
+
from runspec_linux._escalation import refuse_root
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
def main_create_venv() -> None:
|
|
10
12
|
spec = rs.parse("create-venv")
|
|
13
|
+
refuse_root(spec.run_as.value or None)
|
|
11
14
|
try:
|
|
12
15
|
result = create_venv(
|
|
13
16
|
str(spec.python.value),
|
|
@@ -33,6 +36,7 @@ def main_create_venv() -> None:
|
|
|
33
36
|
|
|
34
37
|
def main_install_into_venv() -> None:
|
|
35
38
|
spec = rs.parse("install-into-venv")
|
|
39
|
+
refuse_root(spec.run_as.value or None)
|
|
36
40
|
try:
|
|
37
41
|
result = install_into_venv(
|
|
38
42
|
str(spec.venv.value),
|
|
@@ -57,6 +61,7 @@ def main_install_into_venv() -> None:
|
|
|
57
61
|
|
|
58
62
|
def main_share_venv_logs() -> None:
|
|
59
63
|
spec = rs.parse("share-venv-logs")
|
|
64
|
+
refuse_root(spec.run_as.value or None)
|
|
60
65
|
try:
|
|
61
66
|
result = share_venv_logs(
|
|
62
67
|
str(spec.venv.value),
|
|
@@ -77,6 +82,7 @@ def main_share_venv_logs() -> None:
|
|
|
77
82
|
|
|
78
83
|
def main_configure_pip() -> None:
|
|
79
84
|
spec = rs.parse("configure-pip")
|
|
85
|
+
refuse_root(spec.run_as.value or None)
|
|
80
86
|
try:
|
|
81
87
|
result = configure_pip(
|
|
82
88
|
spec.index_url.value,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""The wrapper-side escalation resolver: --run-as / --become-method →
|
|
2
|
+
(run_as, become_method), refusing any root target.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from types import SimpleNamespace
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from runspec_linux._escalation import is_root, refuse_root, resolve_escalation
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _spec(run_as: str, method: str = "sudo") -> SimpleNamespace:
|
|
16
|
+
# A parsed spec exposes each arg as an object with a .value.
|
|
17
|
+
return SimpleNamespace(run_as=SimpleNamespace(value=run_as), become_method=SimpleNamespace(value=method))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_run_as_user() -> None:
|
|
21
|
+
assert resolve_escalation(_spec("deploy")) == ("deploy", "sudo")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_empty_is_none() -> None:
|
|
25
|
+
assert resolve_escalation(_spec("")) == (None, "sudo")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_become_method_passthrough() -> None:
|
|
29
|
+
assert resolve_escalation(_spec("svc", method="su")) == ("svc", "su")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parametrize("target", ["root", "0"])
|
|
33
|
+
def test_root_is_refused(target: str, capsys: pytest.CaptureFixture[str]) -> None:
|
|
34
|
+
with pytest.raises(SystemExit) as exc:
|
|
35
|
+
resolve_escalation(_spec(target))
|
|
36
|
+
assert exc.value.code == 2
|
|
37
|
+
assert "runspec-linux-root" in json.loads(capsys.readouterr().out)["error"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_is_root() -> None:
|
|
41
|
+
assert is_root("root") and is_root("0")
|
|
42
|
+
assert not is_root("svc-no-such-user")
|
|
43
|
+
refuse_root(None)
|
|
44
|
+
refuse_root("svc")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_no_root_tools_left() -> None:
|
|
48
|
+
from importlib.resources import files
|
|
49
|
+
|
|
50
|
+
tomllib = pytest.importorskip("tomllib")
|
|
51
|
+
spec = tomllib.loads((files("runspec_linux") / "runspec.toml").read_text())
|
|
52
|
+
for name, body in spec.items():
|
|
53
|
+
if name == "config":
|
|
54
|
+
continue
|
|
55
|
+
assert body.get("run_as") != "root", name
|
|
56
|
+
assert "sudo" not in (body.get("args") or {}), name
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
"""Shared wrapper helper for the ``--run-as`` / ``--sudo`` / ``--become-method``
|
|
2
|
-
escalation args.
|
|
3
|
-
|
|
4
|
-
The privileged runnables declare a mutually-exclusive ``escalation`` group with
|
|
5
|
-
``--run-as <user>`` and ``--sudo`` (a flag = "run as root"), plus
|
|
6
|
-
``--become-method sudo|su``. The group guarantees at most one of run-as/sudo is
|
|
7
|
-
set, so this resolves them to the ``(run_as, become_method)`` the core helpers
|
|
8
|
-
take: ``--sudo`` → ``run_as="root"``, ``--run-as x`` → ``run_as="x"``, neither →
|
|
9
|
-
``run_as=None`` (no escalation, runs as the login user).
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
import runspec as rs
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def resolve_escalation(spec: rs.RunSpec) -> tuple[str | None, str]:
|
|
18
|
-
"""Return ``(run_as, become_method)`` from a spec carrying the escalation args."""
|
|
19
|
-
run_as = "root" if spec.sudo.value else (spec.run_as.value or None)
|
|
20
|
-
return run_as, spec.become_method.value
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import sys
|
|
3
|
-
|
|
4
|
-
import runspec as rs
|
|
5
|
-
from runspec_linux_core import (
|
|
6
|
-
autoremove_packages,
|
|
7
|
-
install_package,
|
|
8
|
-
list_autoremovable,
|
|
9
|
-
list_packages,
|
|
10
|
-
list_upgrades,
|
|
11
|
-
remove_package,
|
|
12
|
-
upgrade_packages,
|
|
13
|
-
)
|
|
14
|
-
from runspec_linux_core.errors import CommandError
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def main_list_packages() -> None:
|
|
18
|
-
spec = rs.parse("list-packages")
|
|
19
|
-
name_filter = spec.filter.value
|
|
20
|
-
limit = spec.limit.value
|
|
21
|
-
try:
|
|
22
|
-
print(json.dumps(list_packages(name_filter, limit, reason=spec.reason.value)))
|
|
23
|
-
except Exception as e:
|
|
24
|
-
# Includes ToolNotFoundError (no package manager) and CommandError.
|
|
25
|
-
print(json.dumps({"error": str(e)}))
|
|
26
|
-
sys.exit(1)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def main_list_upgrades() -> None:
|
|
30
|
-
spec = rs.parse("list-upgrades")
|
|
31
|
-
try:
|
|
32
|
-
print(json.dumps(list_upgrades(refresh=not spec.no_refresh.value)))
|
|
33
|
-
except Exception as e:
|
|
34
|
-
print(json.dumps({"error": str(e)}))
|
|
35
|
-
sys.exit(1)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def main_upgrade_packages() -> None:
|
|
39
|
-
rs.parse("upgrade-packages")
|
|
40
|
-
try:
|
|
41
|
-
print(json.dumps(upgrade_packages()))
|
|
42
|
-
except CommandError as e:
|
|
43
|
-
print(json.dumps({"upgraded": False, "error": str(e)}))
|
|
44
|
-
sys.exit(1)
|
|
45
|
-
except Exception as e:
|
|
46
|
-
# Includes ToolNotFoundError (no package manager).
|
|
47
|
-
print(json.dumps({"error": str(e)}))
|
|
48
|
-
sys.exit(1)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def main_install_package() -> None:
|
|
52
|
-
spec = rs.parse("install-package")
|
|
53
|
-
package = spec.package.value
|
|
54
|
-
try:
|
|
55
|
-
print(json.dumps(install_package(package)))
|
|
56
|
-
except CommandError as e:
|
|
57
|
-
print(json.dumps({"package": package, "installed": False, "error": str(e)}))
|
|
58
|
-
sys.exit(1)
|
|
59
|
-
except Exception as e:
|
|
60
|
-
# Includes ValueError (bad name) and ToolNotFoundError (no package manager).
|
|
61
|
-
print(json.dumps({"error": str(e)}))
|
|
62
|
-
sys.exit(1)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def main_remove_package() -> None:
|
|
66
|
-
spec = rs.parse("remove-package")
|
|
67
|
-
package = spec.package.value
|
|
68
|
-
try:
|
|
69
|
-
print(json.dumps(remove_package(package, purge=spec.purge.value)))
|
|
70
|
-
except CommandError as e:
|
|
71
|
-
print(json.dumps({"package": package, "removed": False, "error": str(e)}))
|
|
72
|
-
sys.exit(1)
|
|
73
|
-
except Exception as e:
|
|
74
|
-
# Includes ValueError (bad name) and ToolNotFoundError (no package manager).
|
|
75
|
-
print(json.dumps({"error": str(e)}))
|
|
76
|
-
sys.exit(1)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def main_list_autoremovable() -> None:
|
|
80
|
-
spec = rs.parse("list-autoremovable")
|
|
81
|
-
try:
|
|
82
|
-
print(json.dumps(list_autoremovable(purge=spec.purge.value)))
|
|
83
|
-
except Exception as e:
|
|
84
|
-
# Includes CommandError (yum, failed listing) and ToolNotFoundError.
|
|
85
|
-
print(json.dumps({"error": str(e)}))
|
|
86
|
-
sys.exit(1)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def main_autoremove_packages() -> None:
|
|
90
|
-
spec = rs.parse("autoremove-packages")
|
|
91
|
-
try:
|
|
92
|
-
print(json.dumps(autoremove_packages(purge=spec.purge.value, dry_run=spec.dry_run.value)))
|
|
93
|
-
except CommandError as e:
|
|
94
|
-
print(json.dumps({"removed": False, "error": str(e)}))
|
|
95
|
-
sys.exit(1)
|
|
96
|
-
except Exception as e:
|
|
97
|
-
# Includes ToolNotFoundError (no package manager).
|
|
98
|
-
print(json.dumps({"error": str(e)}))
|
|
99
|
-
sys.exit(1)
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import sys
|
|
3
|
-
|
|
4
|
-
import runspec as rs
|
|
5
|
-
from runspec_linux_core import reboot_host
|
|
6
|
-
from runspec_linux_core.errors import CommandError
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def main_reboot_host() -> None:
|
|
10
|
-
spec = rs.parse("reboot-host")
|
|
11
|
-
delay_minutes = spec.delay_minutes.value
|
|
12
|
-
try:
|
|
13
|
-
print(json.dumps(reboot_host(delay_minutes)))
|
|
14
|
-
except CommandError as e:
|
|
15
|
-
print(json.dumps({"scheduled": False, "error": str(e)}))
|
|
16
|
-
sys.exit(1)
|
|
17
|
-
except Exception as e:
|
|
18
|
-
# Includes ToolNotFoundError (no shutdown/systemctl).
|
|
19
|
-
print(json.dumps({"error": str(e)}))
|
|
20
|
-
sys.exit(1)
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import sys
|
|
3
|
-
|
|
4
|
-
import runspec as rs
|
|
5
|
-
from runspec_linux_core import enable_passwordless_sudo
|
|
6
|
-
from runspec_linux_core.errors import CommandError
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def main_enable_passwordless_sudo() -> None:
|
|
10
|
-
spec = rs.parse("enable-passwordless-sudo")
|
|
11
|
-
password = spec.password.value
|
|
12
|
-
user = spec.user.value
|
|
13
|
-
scope = spec.scope.value
|
|
14
|
-
try:
|
|
15
|
-
print(json.dumps(enable_passwordless_sudo(password, user, scope)))
|
|
16
|
-
except CommandError as e:
|
|
17
|
-
# Auth failure / sudoers validation failure (rolled back).
|
|
18
|
-
print(json.dumps({"installed": False, "error": str(e)}))
|
|
19
|
-
sys.exit(1)
|
|
20
|
-
except Exception as e:
|
|
21
|
-
# Includes ValueError (bad scope/user) and ToolNotFoundError (no sudo/visudo).
|
|
22
|
-
print(json.dumps({"error": str(e)}))
|
|
23
|
-
sys.exit(1)
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
"""The wrapper-side escalation resolver: --sudo / --run-as / --become-method →
|
|
2
|
-
(run_as, become_method).
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
from types import SimpleNamespace
|
|
8
|
-
|
|
9
|
-
from runspec_linux._escalation import resolve_escalation
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def _spec(sudo: bool, run_as: str, method: str = "sudo") -> SimpleNamespace:
|
|
13
|
-
# A parsed spec exposes each arg as an object with a .value.
|
|
14
|
-
return SimpleNamespace(
|
|
15
|
-
sudo=SimpleNamespace(value=sudo),
|
|
16
|
-
run_as=SimpleNamespace(value=run_as),
|
|
17
|
-
become_method=SimpleNamespace(value=method),
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def test_sudo_maps_to_root() -> None:
|
|
22
|
-
assert resolve_escalation(_spec(sudo=True, run_as="")) == ("root", "sudo")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def test_run_as_user() -> None:
|
|
26
|
-
assert resolve_escalation(_spec(sudo=False, run_as="deploy")) == ("deploy", "sudo")
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def test_neither_is_none() -> None:
|
|
30
|
-
assert resolve_escalation(_spec(sudo=False, run_as="")) == (None, "sudo")
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_become_method_passthrough() -> None:
|
|
34
|
-
assert resolve_escalation(_spec(sudo=False, run_as="svc", method="su")) == ("svc", "su")
|
|
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
|