runspec-linux 0.4.0__tar.gz → 0.4.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.
@@ -1,5 +1,41 @@
1
1
  # runspec-linux Changelog
2
2
 
3
+ ## [0.4.2] — 2026-06-21
4
+
5
+ Add 3 runnables for managing Python virtualenvs on a fleet host — thin wrappers
6
+ over the new `runspec-linux-core` 0.3.2 helpers (dependency floor bumped to
7
+ `>=0.3.2`). All three are `autonomy = "confirm"`.
8
+
9
+ - **`create-venv`** — create a venv (always `--symlinks`) from a *specific*
10
+ interpreter (`--python` is a browsable `path` arg → pick the remote binary, not
11
+ the system default) at a free `--dest`. Creates a shared `<dest>/logs` dir mode
12
+ `2775` and can `--group` (chgrp) the venv to a shared group. Flags:
13
+ `--system-site-packages`, `--upgrade-deps`, `--clear`, `--prompt`.
14
+ - **`install-into-venv`** — `pip install` named `--package`(s) and/or a local
15
+ `--project` (a `pyproject.toml` directory or wheel/sdist; `--editable` for
16
+ `-e`) into a venv. Flags: `--upgrade`/`-U`, `--no-cache-dir`,
17
+ `--force-reinstall`.
18
+ - **`configure-pip`** — write a `pip.conf` (`--index-url` + `--extra-index-url`,
19
+ `--trusted-host`, `--cert`, `--timeout`, `--retries`, `--proxy`,
20
+ `--require-virtualenv`) at `--scope user` (`~/.config/pip/pip.conf`) or
21
+ `--scope venv` (`<venv>/pip.conf`) for private-PyPI access.
22
+
23
+ Unlike the package/power runnables, these declare **no `run_as`** in
24
+ `runspec.toml`. The service account that owns a shared venv is chosen per run, so
25
+ each takes a **`--run-as`** argument (and a `--become-method` of `sudo` or `su`)
26
+ and escalates internally — an empty `--run-as` runs as the SSH login user
27
+ (personal venv).
28
+
29
+ ## [0.4.1] — 2026-06-20
30
+
31
+ Give `list-upgrades` `run_as = "root"`. It refreshes the package index
32
+ (`apt-get update` / `dnf check-update` / …) by default, which needs root — but it
33
+ had no `run_as`, so over the console it ran as the SSH login user and failed with
34
+ `Could not open lock file /var/lib/apt/lists/lock — Permission denied`. It now
35
+ escalates like the rest of the package family (works password-free with the
36
+ `enable-passwordless-sudo` drop-in). `--no-refresh` still skips the index refresh
37
+ and lists from the cache (now just faster, rather than a non-root workaround).
38
+
3
39
  ## [0.4.0] — 2026-06-20
4
40
 
5
41
  Make the privileged package/power runnables escalate via runspec's `run_as`, and
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runspec-linux
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Linux system admin runnables for runspec
5
5
  Requires-Python: >=3.10
6
- Requires-Dist: runspec-linux-core>=0.3.0
6
+ Requires-Dist: runspec-linux-core>=0.3.2
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.4.0"
7
+ version = "0.4.2"
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.3.0",
12
+ "runspec-linux-core>=0.3.2",
13
13
  ]
14
14
 
15
15
  [project.scripts]
@@ -40,6 +40,9 @@ install-package = "runspec_linux.packages:main_install_package"
40
40
  remove-package = "runspec_linux.packages:main_remove_package"
41
41
  reboot-host = "runspec_linux.power:main_reboot_host"
42
42
  enable-passwordless-sudo = "runspec_linux.sudoers:main_enable_passwordless_sudo"
43
+ create-venv = "runspec_linux.venvs:main_create_venv"
44
+ install-into-venv = "runspec_linux.venvs:main_install_into_venv"
45
+ configure-pip = "runspec_linux.venvs:main_configure_pip"
43
46
  nc-command = "runspec_linux.nc_command:main"
44
47
 
45
48
  [project.optional-dependencies]
@@ -12,11 +12,14 @@ from runspec_linux_core import (
12
12
  check_memory,
13
13
  check_port,
14
14
  check_service,
15
+ configure_pip,
15
16
  container_logs,
17
+ create_venv,
16
18
  detect_manager,
17
19
  disk_usage,
18
20
  enable_passwordless_sudo,
19
21
  find_large_files,
22
+ install_into_venv,
20
23
  install_package,
21
24
  journalctl,
22
25
  last_logins,
@@ -44,11 +47,14 @@ __all__ = [
44
47
  "check_memory",
45
48
  "check_port",
46
49
  "check_service",
50
+ "configure_pip",
47
51
  "container_logs",
52
+ "create_venv",
48
53
  "detect_manager",
49
54
  "disk_usage",
50
55
  "enable_passwordless_sudo",
51
56
  "find_large_files",
57
+ "install_into_venv",
52
58
  "install_package",
53
59
  "journalctl",
54
60
  "last_logins",
@@ -324,10 +324,11 @@ serve = ["local"]
324
324
  description = "List packages with an available upgrade (refreshes the package index first)"
325
325
  autonomy = "autonomous"
326
326
  output = "json"
327
+ run_as = "root"
327
328
 
328
329
  [list-upgrades.args.no-refresh]
329
330
  type = "flag"
330
- description = "Read the cached package index instead of refreshing it first (no root needed)"
331
+ description = "Skip refreshing the package index — list from the cached index (faster)"
331
332
  default = false
332
333
 
333
334
  [upgrade-packages]
@@ -437,3 +438,190 @@ default = 0.3
437
438
  type = "float"
438
439
  description = "Per-chunk socket read timeout in seconds"
439
440
  default = 0.1
441
+
442
+ # ── Python venvs ──────────────────────────────────────────────────────────────
443
+
444
+ # These deliberately declare NO `run_as` key. The service account that owns a
445
+ # shared venv is chosen per invocation, so each runnable takes a `run-as`
446
+ # argument and escalates internally (`sudo -n -u <user>`, or `su` via
447
+ # `become-method`). An empty `run-as` runs as the SSH login user (personal venv).
448
+
449
+ [create-venv]
450
+ serve = ["local"]
451
+ description = "Create a Python virtualenv (always --symlinks) from a specific interpreter, optionally as a service account, with a shared 2775 logs/ dir"
452
+ autonomy = "confirm"
453
+ output = "json"
454
+
455
+ [create-venv.args.python]
456
+ type = "path"
457
+ description = "Path to the Python interpreter to build the venv from (browse to a real binary — not the system default)"
458
+
459
+ [create-venv.args.dest]
460
+ type = "path"
461
+ short = "-d"
462
+ description = "Full path of the venv to create (name and location are free)"
463
+
464
+ [create-venv.args.run-as]
465
+ type = "str"
466
+ short = "-u"
467
+ description = "Service account to create the venv as (escalates via sudo/su); empty = the SSH login user"
468
+ required = false
469
+
470
+ [create-venv.args.become-method]
471
+ type = "choice"
472
+ options = ["sudo", "su"]
473
+ default = "sudo"
474
+ description = "How to escalate to run-as: sudo (passwordless, -n) or su"
475
+
476
+ [create-venv.args.group]
477
+ type = "str"
478
+ short = "-g"
479
+ description = "Optional shared group to chgrp the venv to (e.g. consoles), so every connected console can read/write"
480
+ required = false
481
+
482
+ [create-venv.args.system-site-packages]
483
+ type = "flag"
484
+ description = "Give the venv access to the system site-packages"
485
+ default = false
486
+
487
+ [create-venv.args.upgrade-deps]
488
+ type = "flag"
489
+ description = "Upgrade pip/setuptools to the latest in the new venv (--upgrade-deps)"
490
+ default = false
491
+
492
+ [create-venv.args.clear]
493
+ type = "flag"
494
+ description = "Delete the contents of the venv directory if it already exists (--clear)"
495
+ default = false
496
+
497
+ [create-venv.args.prompt]
498
+ type = "str"
499
+ short = "-p"
500
+ description = "Prompt prefix shown when the venv is activated (--prompt)"
501
+ required = false
502
+
503
+ [install-into-venv]
504
+ serve = ["local"]
505
+ description = "pip install named packages and/or a local project (pyproject dir or wheel/sdist) into an existing venv"
506
+ autonomy = "confirm"
507
+ output = "json"
508
+
509
+ [install-into-venv.args.venv]
510
+ type = "path"
511
+ short = "-v"
512
+ description = "Path to the venv to install into (its bundled python runs pip)"
513
+
514
+ [install-into-venv.args.package]
515
+ type = "str"
516
+ short = "-p"
517
+ description = "Package spec(s) to install (space-separated for several)"
518
+ required = false
519
+
520
+ [install-into-venv.args.project]
521
+ type = "path"
522
+ description = "Path to a local project to install — a directory with pyproject.toml, or a wheel/sdist"
523
+ required = false
524
+
525
+ [install-into-venv.args.editable]
526
+ type = "flag"
527
+ short = "-e"
528
+ description = "Install the project in editable mode (pip install -e); requires --project"
529
+ default = false
530
+
531
+ [install-into-venv.args.upgrade]
532
+ type = "flag"
533
+ short = "-U"
534
+ description = "Upgrade packages to the newest available version (pip install -U)"
535
+ default = false
536
+
537
+ [install-into-venv.args.no-cache-dir]
538
+ type = "flag"
539
+ description = "Disable the pip cache for this install (--no-cache-dir)"
540
+ default = false
541
+
542
+ [install-into-venv.args.force-reinstall]
543
+ type = "flag"
544
+ description = "Reinstall packages even if already up to date (--force-reinstall)"
545
+ default = false
546
+
547
+ [install-into-venv.args.run-as]
548
+ type = "str"
549
+ short = "-u"
550
+ description = "Service account that owns the venv (escalates via sudo/su); empty = the SSH login user"
551
+ required = false
552
+
553
+ [install-into-venv.args.become-method]
554
+ type = "choice"
555
+ options = ["sudo", "su"]
556
+ default = "sudo"
557
+ description = "How to escalate to run-as: sudo (passwordless, -n) or su"
558
+
559
+ [configure-pip]
560
+ serve = ["local"]
561
+ description = "Write a pip.conf (index-url and friends) at user or venv scope for private-PyPI access"
562
+ autonomy = "confirm"
563
+ output = "json"
564
+
565
+ [configure-pip.args.index-url]
566
+ type = "str"
567
+ short = "-i"
568
+ description = "Primary package index URL (index-url)"
569
+
570
+ [configure-pip.args.extra-index-url]
571
+ type = "str"
572
+ description = "Additional package index URL (extra-index-url)"
573
+ required = false
574
+
575
+ [configure-pip.args.trusted-host]
576
+ type = "str"
577
+ description = "Host to trust without TLS verification (trusted-host)"
578
+ required = false
579
+
580
+ [configure-pip.args.cert]
581
+ type = "str"
582
+ description = "Path to a CA bundle to verify the index's TLS certificate (cert)"
583
+ required = false
584
+
585
+ [configure-pip.args.timeout]
586
+ type = "int"
587
+ description = "Socket timeout in seconds (timeout)"
588
+ required = false
589
+
590
+ [configure-pip.args.retries]
591
+ type = "int"
592
+ description = "Number of retries for a failed connection (retries)"
593
+ required = false
594
+
595
+ [configure-pip.args.proxy]
596
+ type = "str"
597
+ description = "Proxy URL in [user:passwd@]proxy.server:port form (proxy)"
598
+ required = false
599
+
600
+ [configure-pip.args.require-virtualenv]
601
+ type = "flag"
602
+ description = "Refuse to install outside a virtualenv and silence the pip-version check (require-virtualenv + disable-pip-version-check)"
603
+ default = false
604
+
605
+ [configure-pip.args.scope]
606
+ type = "choice"
607
+ options = ["user", "venv"]
608
+ default = "user"
609
+ short = "-s"
610
+ description = "Where to write pip.conf: user (~/.config/pip/pip.conf) or venv (<venv>/pip.conf)"
611
+
612
+ [configure-pip.args.venv]
613
+ type = "path"
614
+ description = "Venv path for --scope venv (the file is written to <venv>/pip.conf)"
615
+ required = false
616
+
617
+ [configure-pip.args.run-as]
618
+ type = "str"
619
+ short = "-u"
620
+ description = "User who owns the target file (escalates via sudo/su); empty = the SSH login user"
621
+ required = false
622
+
623
+ [configure-pip.args.become-method]
624
+ type = "choice"
625
+ options = ["sudo", "su"]
626
+ default = "sudo"
627
+ description = "How to escalate to run-as: sudo (passwordless, -n) or su"
@@ -0,0 +1,81 @@
1
+ import json
2
+ import sys
3
+
4
+ import runspec as rs
5
+ from runspec_linux_core import configure_pip, create_venv, install_into_venv
6
+ from runspec_linux_core.errors import CommandError
7
+
8
+
9
+ def main_create_venv() -> None:
10
+ spec = rs.parse("create-venv")
11
+ try:
12
+ result = create_venv(
13
+ str(spec.python.value),
14
+ str(spec.dest.value),
15
+ run_as=spec.run_as.value or None,
16
+ become_method=spec.become_method.value,
17
+ group=spec.group.value or None,
18
+ system_site_packages=spec.system_site_packages.value,
19
+ upgrade_deps=spec.upgrade_deps.value,
20
+ clear=spec.clear.value,
21
+ prompt=spec.prompt.value or None,
22
+ )
23
+ print(json.dumps(result))
24
+ except CommandError as e:
25
+ print(json.dumps({"dest": str(spec.dest.value), "created": False, "error": str(e)}))
26
+ sys.exit(1)
27
+ except Exception as e:
28
+ # Includes ValueError (non-executable interpreter / bad name).
29
+ print(json.dumps({"error": str(e)}))
30
+ sys.exit(1)
31
+
32
+
33
+ def main_install_into_venv() -> None:
34
+ spec = rs.parse("install-into-venv")
35
+ try:
36
+ result = install_into_venv(
37
+ str(spec.venv.value),
38
+ spec.package.value or "",
39
+ project=str(spec.project.value) if spec.project.value else None,
40
+ editable=spec.editable.value,
41
+ upgrade=spec.upgrade.value,
42
+ no_cache_dir=spec.no_cache_dir.value,
43
+ force_reinstall=spec.force_reinstall.value,
44
+ run_as=spec.run_as.value or None,
45
+ become_method=spec.become_method.value,
46
+ )
47
+ print(json.dumps(result))
48
+ except CommandError as e:
49
+ print(json.dumps({"venv": str(spec.venv.value), "installed": False, "error": str(e)}))
50
+ sys.exit(1)
51
+ except Exception as e:
52
+ # Includes ValueError (bad package name / missing venv / nothing to install).
53
+ print(json.dumps({"error": str(e)}))
54
+ sys.exit(1)
55
+
56
+
57
+ def main_configure_pip() -> None:
58
+ spec = rs.parse("configure-pip")
59
+ try:
60
+ result = configure_pip(
61
+ spec.index_url.value,
62
+ extra_index_url=spec.extra_index_url.value or None,
63
+ trusted_host=spec.trusted_host.value or None,
64
+ cert=spec.cert.value or None,
65
+ timeout=spec.timeout.value,
66
+ retries=spec.retries.value,
67
+ proxy=spec.proxy.value or None,
68
+ require_virtualenv=spec.require_virtualenv.value,
69
+ scope=spec.scope.value,
70
+ venv=str(spec.venv.value) if spec.venv.value else None,
71
+ run_as=spec.run_as.value or None,
72
+ become_method=spec.become_method.value,
73
+ )
74
+ print(json.dumps(result))
75
+ except CommandError as e:
76
+ print(json.dumps({"configured": False, "error": str(e)}))
77
+ sys.exit(1)
78
+ except Exception as e:
79
+ # Includes ValueError (bad scope / missing venv path).
80
+ print(json.dumps({"error": str(e)}))
81
+ sys.exit(1)
File without changes