runspec-linux 0.2.0__tar.gz → 0.3.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.
@@ -1,5 +1,35 @@
1
1
  # runspec-linux Changelog
2
2
 
3
+ ## [0.3.0] — 2026-06-20
4
+
5
+ Add 6 runnables for managing a fleet's packages and power state from
6
+ runspec-console — thin wrappers over the new `runspec-linux-core` 0.2.0 helpers
7
+ (dependency floor bumped to `>=0.2.0`). They work across the common distro
8
+ families (apt/dnf/yum/zypper/pacman).
9
+
10
+ **Packages**
11
+
12
+ - `list-packages` (autonomous) — list installed packages, optional `--filter`
13
+ substring and `--limit`.
14
+ - `list-upgrades` (autonomous) — list packages with an available upgrade;
15
+ refreshes the index first, or pass `--no-refresh` to read the cache without
16
+ root.
17
+ - `upgrade-packages` (confirm) — refresh the index and upgrade everything.
18
+ - `install-package` (confirm) — install one or more packages (`--package`,
19
+ space-separated for several).
20
+ - `remove-package` (confirm) — remove one or more packages.
21
+
22
+ **Power**
23
+
24
+ - `reboot-host` (confirm) — reboot the host now, or schedule with
25
+ `--delay-minutes`.
26
+
27
+ The read-only runnables are `autonomous`; the state-changing ones are `confirm`,
28
+ matching `restart-service` / `restart-container`. Missing package manager / a
29
+ non-root failure returns structured JSON (`{... false, "error": ...}`) and a
30
+ non-zero exit rather than a stack trace; package names are validated before they
31
+ reach the package manager.
32
+
3
33
  ## [0.2.0] — 2026-06-09
4
34
 
5
35
  Split the pure logic out into a new dependency-free package,
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runspec-linux
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Linux system admin runnables for runspec
5
5
  Requires-Python: >=3.10
6
- Requires-Dist: runspec-linux-core>=0.1.0
6
+ Requires-Dist: runspec-linux-core>=0.2.0
7
7
  Requires-Dist: runspec>=0.27.0
8
8
  Provides-Extra: dev
9
9
  Requires-Dist: mypy; extra == 'dev'
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "runspec-linux"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  requires-python = ">=3.10"
9
9
  description = "Linux system admin runnables for runspec"
10
10
  dependencies = [
11
11
  "runspec>=0.27.0",
12
- "runspec-linux-core>=0.1.0",
12
+ "runspec-linux-core>=0.2.0",
13
13
  ]
14
14
 
15
15
  [project.scripts]
@@ -33,6 +33,12 @@ who = "runspec_linux.security:main_who"
33
33
  list-containers = "runspec_linux.containers:main_list_containers"
34
34
  container-logs = "runspec_linux.containers:main_container_logs"
35
35
  restart-container = "runspec_linux.containers:main_restart_container"
36
+ list-packages = "runspec_linux.packages:main_list_packages"
37
+ list-upgrades = "runspec_linux.packages:main_list_upgrades"
38
+ upgrade-packages = "runspec_linux.packages:main_upgrade_packages"
39
+ install-package = "runspec_linux.packages:main_install_package"
40
+ remove-package = "runspec_linux.packages:main_remove_package"
41
+ reboot-host = "runspec_linux.power:main_reboot_host"
36
42
  nc-command = "runspec_linux.nc_command:main"
37
43
 
38
44
  [project.optional-dependencies]
@@ -13,21 +13,28 @@ from runspec_linux_core import (
13
13
  check_port,
14
14
  check_service,
15
15
  container_logs,
16
+ detect_manager,
16
17
  disk_usage,
17
18
  find_large_files,
19
+ install_package,
18
20
  journalctl,
19
21
  last_logins,
20
22
  list_containers,
23
+ list_packages,
21
24
  list_processes,
22
25
  list_services,
26
+ list_upgrades,
23
27
  nc_send,
24
28
  ping_host,
29
+ reboot_host,
30
+ remove_package,
25
31
  restart_container,
26
32
  restart_service,
27
33
  search_log,
28
34
  show_connections,
29
35
  system_info,
30
36
  tail_log,
37
+ upgrade_packages,
31
38
  who,
32
39
  )
33
40
 
@@ -37,20 +44,27 @@ __all__ = [
37
44
  "check_port",
38
45
  "check_service",
39
46
  "container_logs",
47
+ "detect_manager",
40
48
  "disk_usage",
41
49
  "find_large_files",
50
+ "install_package",
42
51
  "journalctl",
43
52
  "last_logins",
44
53
  "list_containers",
54
+ "list_packages",
45
55
  "list_processes",
46
56
  "list_services",
57
+ "list_upgrades",
47
58
  "nc_send",
48
59
  "ping_host",
60
+ "reboot_host",
61
+ "remove_package",
49
62
  "restart_container",
50
63
  "restart_service",
51
64
  "search_log",
52
65
  "show_connections",
53
66
  "system_info",
54
67
  "tail_log",
68
+ "upgrade_packages",
55
69
  "who",
56
70
  ]
@@ -8,7 +8,7 @@ from runspec_linux_core.errors import CommandError
8
8
 
9
9
  def main_list_containers() -> None:
10
10
  spec = rs.parse("list-containers")
11
- include_all = bool(spec.all)
11
+ include_all = spec.all.value
12
12
  try:
13
13
  print(json.dumps(list_containers(include_all)))
14
14
  except Exception as e:
@@ -19,8 +19,8 @@ def main_list_containers() -> None:
19
19
 
20
20
  def main_container_logs() -> None:
21
21
  spec = rs.parse("container-logs")
22
- container = str(spec.container)
23
- lines = int(spec.lines)
22
+ container = spec.container.value
23
+ lines = spec.lines.value
24
24
  try:
25
25
  print(json.dumps(container_logs(container, lines)))
26
26
  except Exception as e:
@@ -30,7 +30,7 @@ def main_container_logs() -> None:
30
30
 
31
31
  def main_restart_container() -> None:
32
32
  spec = rs.parse("restart-container")
33
- container = str(spec.container)
33
+ container = spec.container.value
34
34
  try:
35
35
  print(json.dumps(restart_container(container)))
36
36
  except CommandError as e:
@@ -7,9 +7,9 @@ from runspec_linux_core import backup_files, find_large_files
7
7
 
8
8
  def main_find_large_files() -> None:
9
9
  spec = rs.parse("find-large-files")
10
- search_path = str(spec.path)
11
- min_mb = float(spec.min_mb)
12
- limit = int(spec.limit)
10
+ search_path = str(spec.path.value)
11
+ min_mb = spec.min_mb.value
12
+ limit = spec.limit.value
13
13
  try:
14
14
  print(json.dumps(find_large_files(search_path, min_mb, limit)))
15
15
  except Exception as e:
@@ -19,8 +19,8 @@ def main_find_large_files() -> None:
19
19
 
20
20
  def main_backup_files() -> None:
21
21
  spec = rs.parse("backup-files")
22
- source = str(spec.source)
23
- destination = str(spec.destination)
22
+ source = str(spec.source.value)
23
+ destination = str(spec.destination.value)
24
24
  try:
25
25
  print(json.dumps(backup_files(source, destination)))
26
26
  except Exception as e:
@@ -10,8 +10,8 @@ from runspec_linux_core.errors import ToolNotFoundError
10
10
 
11
11
  def main_tail_log() -> None:
12
12
  spec = rs.parse("tail-log")
13
- file_path = str(spec.file)
14
- lines = int(spec.lines)
13
+ file_path = str(spec.file.value)
14
+ lines = spec.lines.value
15
15
  try:
16
16
  print(json.dumps(tail_log(file_path, lines)))
17
17
  except OSError as e:
@@ -21,9 +21,9 @@ def main_tail_log() -> None:
21
21
 
22
22
  def main_search_log() -> None:
23
23
  spec = rs.parse("search-log")
24
- file_path = str(spec.file)
25
- pattern = str(spec.pattern)
26
- limit = int(spec.limit)
24
+ file_path = str(spec.file.value)
25
+ pattern = spec.pattern.value
26
+ limit = spec.limit.value
27
27
  try:
28
28
  print(json.dumps(search_log(file_path, pattern, limit)))
29
29
  except OSError as e:
@@ -36,9 +36,9 @@ def main_search_log() -> None:
36
36
 
37
37
  def main_journalctl() -> None:
38
38
  spec = rs.parse("journalctl")
39
- unit = str(spec.unit)
40
- lines = int(spec.lines)
41
- since = str(spec.since) if spec.since is not None else None
39
+ unit = spec.unit.value
40
+ lines = spec.lines.value
41
+ since = spec.since.value
42
42
  try:
43
43
  print(json.dumps(journalctl(unit, lines, since)))
44
44
  except ToolNotFoundError as e:
@@ -13,11 +13,11 @@ __all__ = ["main", "nc_send"]
13
13
 
14
14
  def main() -> None:
15
15
  spec = rs.parse("nc-command")
16
- host = str(spec.host)
17
- port = int(spec.port)
18
- command = str(spec.command)
19
- wait = float(spec.wait)
20
- read_timeout = float(spec.read_timeout)
16
+ host = spec.host.value
17
+ port = spec.port.value
18
+ command = spec.command.value
19
+ wait = spec.wait.value
20
+ read_timeout = spec.read_timeout.value
21
21
 
22
22
  try:
23
23
  response = nc_send(host, port, command, wait=wait, read_timeout=read_timeout)
@@ -7,8 +7,8 @@ from runspec_linux_core import check_port, ping_host, show_connections
7
7
 
8
8
  def main_ping_host() -> None:
9
9
  spec = rs.parse("ping-host")
10
- host = str(spec.host)
11
- count = int(spec.count)
10
+ host = spec.host.value
11
+ count = spec.count.value
12
12
  try:
13
13
  print(json.dumps(ping_host(host, count)))
14
14
  except Exception as e:
@@ -18,15 +18,15 @@ def main_ping_host() -> None:
18
18
 
19
19
  def main_check_port() -> None:
20
20
  spec = rs.parse("check-port")
21
- host = str(spec.host)
22
- port = int(spec.port)
23
- timeout = float(spec.timeout)
21
+ host = spec.host.value
22
+ port = spec.port.value
23
+ timeout = spec.timeout.value
24
24
  print(json.dumps(check_port(host, port, timeout)))
25
25
 
26
26
 
27
27
  def main_show_connections() -> None:
28
28
  spec = rs.parse("show-connections")
29
- state_filter = str(spec.state)
29
+ state_filter = spec.state.value
30
30
  try:
31
31
  print(json.dumps(show_connections(state_filter)))
32
32
  except Exception as e:
@@ -0,0 +1,74 @@
1
+ import json
2
+ import sys
3
+
4
+ import runspec as rs
5
+ from runspec_linux_core import (
6
+ install_package,
7
+ list_packages,
8
+ list_upgrades,
9
+ remove_package,
10
+ upgrade_packages,
11
+ )
12
+ from runspec_linux_core.errors import CommandError
13
+
14
+
15
+ def main_list_packages() -> None:
16
+ spec = rs.parse("list-packages")
17
+ name_filter = spec.filter.value
18
+ limit = spec.limit.value
19
+ try:
20
+ print(json.dumps(list_packages(name_filter, limit)))
21
+ except Exception as e:
22
+ # Includes ToolNotFoundError (no package manager) and CommandError.
23
+ print(json.dumps({"error": str(e)}))
24
+ sys.exit(1)
25
+
26
+
27
+ def main_list_upgrades() -> None:
28
+ spec = rs.parse("list-upgrades")
29
+ try:
30
+ print(json.dumps(list_upgrades(refresh=not spec.no_refresh.value)))
31
+ except Exception as e:
32
+ print(json.dumps({"error": str(e)}))
33
+ sys.exit(1)
34
+
35
+
36
+ def main_upgrade_packages() -> None:
37
+ rs.parse("upgrade-packages")
38
+ try:
39
+ print(json.dumps(upgrade_packages()))
40
+ except CommandError as e:
41
+ print(json.dumps({"upgraded": False, "error": str(e)}))
42
+ sys.exit(1)
43
+ except Exception as e:
44
+ # Includes ToolNotFoundError (no package manager).
45
+ print(json.dumps({"error": str(e)}))
46
+ sys.exit(1)
47
+
48
+
49
+ def main_install_package() -> None:
50
+ spec = rs.parse("install-package")
51
+ package = spec.package.value
52
+ try:
53
+ print(json.dumps(install_package(package)))
54
+ except CommandError as e:
55
+ print(json.dumps({"package": package, "installed": False, "error": str(e)}))
56
+ sys.exit(1)
57
+ except Exception as e:
58
+ # Includes ValueError (bad name) and ToolNotFoundError (no package manager).
59
+ print(json.dumps({"error": str(e)}))
60
+ sys.exit(1)
61
+
62
+
63
+ def main_remove_package() -> None:
64
+ spec = rs.parse("remove-package")
65
+ package = spec.package.value
66
+ try:
67
+ print(json.dumps(remove_package(package)))
68
+ except CommandError as e:
69
+ print(json.dumps({"package": package, "removed": False, "error": str(e)}))
70
+ sys.exit(1)
71
+ except Exception as e:
72
+ # Includes ValueError (bad name) and ToolNotFoundError (no package manager).
73
+ print(json.dumps({"error": str(e)}))
74
+ sys.exit(1)
@@ -0,0 +1,20 @@
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)
@@ -299,6 +299,79 @@ type = "str"
299
299
  short = "-c"
300
300
  description = "Container name or ID"
301
301
 
302
+ # ── Packages ──────────────────────────────────────────────────────────────────
303
+
304
+ [list-packages]
305
+ serve = ["local"]
306
+ description = "List installed packages (apt/dnf/yum/zypper/pacman), optionally filtered by name"
307
+ autonomy = "autonomous"
308
+ output = "json"
309
+
310
+ [list-packages.args.filter]
311
+ type = "str"
312
+ short = "-f"
313
+ description = "Only list packages whose name contains this string"
314
+ required = false
315
+
316
+ [list-packages.args.limit]
317
+ type = "int"
318
+ short = "-n"
319
+ description = "Maximum number of packages to return (sorted by name)"
320
+ default = 100
321
+
322
+ [list-upgrades]
323
+ serve = ["local"]
324
+ description = "List packages with an available upgrade (refreshes the package index first)"
325
+ autonomy = "autonomous"
326
+ output = "json"
327
+
328
+ [list-upgrades.args.no-refresh]
329
+ type = "flag"
330
+ description = "Read the cached package index instead of refreshing it first (no root needed)"
331
+ default = false
332
+
333
+ [upgrade-packages]
334
+ serve = ["local"]
335
+ description = "Refresh the package index and upgrade all installed packages (needs root)"
336
+ autonomy = "confirm"
337
+ output = "json"
338
+
339
+ [install-package]
340
+ serve = ["local"]
341
+ description = "Install one or more packages by name (needs root)"
342
+ autonomy = "confirm"
343
+ output = "json"
344
+
345
+ [install-package.args.package]
346
+ type = "str"
347
+ short = "-p"
348
+ description = "Package name to install (space-separated for several)"
349
+
350
+ [remove-package]
351
+ serve = ["local"]
352
+ description = "Remove one or more packages by name (needs root)"
353
+ autonomy = "confirm"
354
+ output = "json"
355
+
356
+ [remove-package.args.package]
357
+ type = "str"
358
+ short = "-p"
359
+ description = "Package name to remove (space-separated for several)"
360
+
361
+ # ── Power ─────────────────────────────────────────────────────────────────────
362
+
363
+ [reboot-host]
364
+ serve = ["local"]
365
+ description = "Reboot the host, optionally after a delay in minutes (needs root)"
366
+ autonomy = "confirm"
367
+ output = "json"
368
+
369
+ [reboot-host.args.delay-minutes]
370
+ type = "int"
371
+ short = "-d"
372
+ description = "Minutes to wait before rebooting (0 = immediately)"
373
+ default = 0
374
+
302
375
  # ── TCP interfaces (nc-command) ────────────────────────────────────────────────
303
376
 
304
377
  [nc-command]
@@ -7,7 +7,7 @@ from runspec_linux_core import last_logins, who
7
7
 
8
8
  def main_last_logins() -> None:
9
9
  spec = rs.parse("last-logins")
10
- limit = int(spec.limit)
10
+ limit = spec.limit.value
11
11
  try:
12
12
  print(json.dumps(last_logins(limit)))
13
13
  except Exception as e:
@@ -8,7 +8,7 @@ from runspec_linux_core.errors import CommandError
8
8
 
9
9
  def main_check_service() -> None:
10
10
  spec = rs.parse("check-service")
11
- service = str(spec.service)
11
+ service = spec.service.value
12
12
  try:
13
13
  print(json.dumps(check_service(service)))
14
14
  except Exception as e:
@@ -18,7 +18,7 @@ def main_check_service() -> None:
18
18
 
19
19
  def main_list_services() -> None:
20
20
  spec = rs.parse("list-services")
21
- state_filter = str(spec.state)
21
+ state_filter = spec.state.value
22
22
  try:
23
23
  print(json.dumps(list_services(state_filter)))
24
24
  except Exception as e:
@@ -28,7 +28,7 @@ def main_list_services() -> None:
28
28
 
29
29
  def main_restart_service() -> None:
30
30
  spec = rs.parse("restart-service")
31
- service = str(spec.service)
31
+ service = spec.service.value
32
32
  try:
33
33
  print(json.dumps(restart_service(service)))
34
34
  except CommandError as e:
@@ -34,8 +34,8 @@ def main_check_memory() -> None:
34
34
 
35
35
  def main_list_processes() -> None:
36
36
  spec = rs.parse("list-processes")
37
- name_filter = str(spec.filter) if spec.filter is not None else None
38
- limit = int(spec.limit)
37
+ name_filter = spec.filter.value
38
+ limit = spec.limit.value
39
39
  try:
40
40
  print(json.dumps(list_processes(name_filter, limit)))
41
41
  except Exception as e:
File without changes