Enhance ISO selection logic and update documentation for improved clarity and usage instructions
This commit is contained in:
parent
b6886cb34a
commit
9a7bd81d17
12 changed files with 287 additions and 16 deletions
47
.env.example
47
.env.example
|
|
@ -1,14 +1,61 @@
|
|||
# Base Proxmox endpoint, including scheme and port.
|
||||
# Required for live API access.
|
||||
PROXMOX_URL=https://proxmox.example.invalid:8006
|
||||
|
||||
# Login realm used for authentication.
|
||||
# Required for live API access.
|
||||
PROXMOX_REALM=pam
|
||||
|
||||
# Proxmox username. If it does not already include @realm, the app appends PROXMOX_REALM.
|
||||
# Required for live API access.
|
||||
PROXMOX_USER=root
|
||||
|
||||
# Password for the configured user.
|
||||
# Required for live API access.
|
||||
PROXMOX_PASSWORD=replace-me
|
||||
|
||||
# Verify TLS certificates for API requests.
|
||||
# Recommended: true for trusted certificates, false only for known self-signed/internal setups.
|
||||
PROXMOX_VERIFY_TLS=false
|
||||
|
||||
# Usually leave this at the default Proxmox API base.
|
||||
PROXMOX_API_BASE=/api2/json
|
||||
|
||||
# Optional ISO auto-selection rule for the OS step.
|
||||
# Uses glob syntax by default. Prefix with "regex:" to use a regular expression.
|
||||
# Examples:
|
||||
# PROXMOX_DEFAULT_ISO_SELECTOR=*ubuntu*
|
||||
# PROXMOX_DEFAULT_ISO_SELECTOR=regex:nixos-minimal-\d{2}\.\d{2}\..*-x86_64-linux\.iso$
|
||||
PROXMOX_DEFAULT_ISO_SELECTOR=
|
||||
|
||||
# Global create safety switch.
|
||||
# false = allows creates
|
||||
# true = blocks creates
|
||||
PROXMOX_PREVENT_CREATE=false
|
||||
|
||||
# Restrict live creates to a dedicated test scope.
|
||||
# When true, the PROXMOX_TEST_* values below become required.
|
||||
PROXMOX_ENABLE_TEST_MODE=false
|
||||
|
||||
# Required only when PROXMOX_ENABLE_TEST_MODE=true.
|
||||
# Creates are restricted to this node.
|
||||
PROXMOX_TEST_NODE=
|
||||
|
||||
# Required only when PROXMOX_ENABLE_TEST_MODE=true.
|
||||
# Creates are restricted to this resource pool.
|
||||
PROXMOX_TEST_POOL=
|
||||
|
||||
# Required only when PROXMOX_ENABLE_TEST_MODE=true.
|
||||
# Automatically added to created VMs in test mode.
|
||||
PROXMOX_TEST_TAG=codex-e2e
|
||||
|
||||
# Required only when PROXMOX_ENABLE_TEST_MODE=true.
|
||||
# Automatically prefixed to VM names in test mode.
|
||||
PROXMOX_TEST_VM_NAME_PREFIX=codex-e2e-
|
||||
|
||||
# Reserved for future failed-create cleanup behavior.
|
||||
# Parsed today, but not yet acted on by the workflow.
|
||||
PROXMOX_KEEP_FAILED_VM=true
|
||||
|
||||
# Request timeout used for API calls and task polling.
|
||||
PROXMOX_REQUEST_TIMEOUT_SECONDS=15
|
||||
|
|
|
|||
172
README.md
172
README.md
|
|
@ -1,25 +1,173 @@
|
|||
# pve-vm-setup
|
||||
|
||||
Textual TUI for creating Proxmox VMs with live reference data, guarded create controls, and a guided multi-step wizard.
|
||||
|
||||
## What it does
|
||||
|
||||
- logs into a Proxmox VE API endpoint
|
||||
- loads nodes, pools, storages, bridges, tags, and ISO images from the live cluster
|
||||
- walks through VM creation in a step-by-step wizard
|
||||
- can run in a safe read-only mode, a normal live-create mode, or a restricted test mode
|
||||
|
||||
## Commands
|
||||
|
||||
- Install: `uv sync`
|
||||
- Run app: `uv run python -m pve_vm_setup`
|
||||
- Run live diagnostics: `uv run python -m pve_vm_setup --doctor-live`
|
||||
- Run directly from this repo (without cloning): `uvx git+https://git.s1q.dev/phg/pve-vm-setup.git`
|
||||
- Install dependencies: `uv sync`
|
||||
- Run the app from the checkout repository: `uv run -m pve_vm_setup`
|
||||
- Run live diagnostics: `uv run -m pve_vm_setup --doctor-live`
|
||||
- Run tests: `uv run pytest`
|
||||
- Run read-only live tests: `uv run pytest -m live`
|
||||
- Run create-gated live tests: `uv run pytest -m live_create`
|
||||
- Run live create tests: `uv run pytest -m live_create`
|
||||
- Lint: `uv run ruff check .`
|
||||
- Format: `uv run ruff format .`
|
||||
|
||||
## Live configuration
|
||||
## Typical usage
|
||||
|
||||
Start from `.env.example` and provide the Proxmox credentials in `.env`.
|
||||
1. Copy `.env.example` to `.env`.
|
||||
2. Fill in the Proxmox connection settings.
|
||||
3. Decide whether this machine should be allowed to create VMs at all.
|
||||
4. Optionally enable test mode if you want live creates restricted to a known node/pool and auto-tagged.
|
||||
5. Start the app with `uv run python -m pve_vm_setup`.
|
||||
6. Log in and complete the wizard.
|
||||
|
||||
Additional live-access controls:
|
||||
Before the final create request, the app asks whether the VM should be started automatically after creation.
|
||||
|
||||
- `PROXMOX_VERIFY_TLS=false` disables certificate verification for internal/self-signed installs
|
||||
- `PROXMOX_API_BASE=/api2/json` makes the API base explicit
|
||||
- `PROXMOX_PREVENT_CREATE=false` allows VM creation by default; set it to `true` to block creates
|
||||
- `PROXMOX_ENABLE_TEST_MODE=true` enables scoped test mode for live creates
|
||||
- When test mode is enabled, `PROXMOX_TEST_NODE`, `PROXMOX_TEST_POOL`, `PROXMOX_TEST_TAG`, and `PROXMOX_TEST_VM_NAME_PREFIX` are required and are used to constrain and mark created VMs
|
||||
## Operating modes
|
||||
|
||||
### Read-only mode
|
||||
|
||||
Use this when you want to browse live data and validate the setup without creating anything.
|
||||
|
||||
- Set `PROXMOX_PREVENT_CREATE=true`
|
||||
- Recommended for first-time setup
|
||||
- `--doctor-live` is useful here
|
||||
|
||||
### Normal live-create mode
|
||||
|
||||
Use this when you want to create real VMs without the extra test-mode restrictions.
|
||||
|
||||
- Set `PROXMOX_PREVENT_CREATE=false` or leave it unset
|
||||
- Leave `PROXMOX_ENABLE_TEST_MODE=false`
|
||||
- Recommended only when you are comfortable with the target cluster and defaults
|
||||
|
||||
### Restricted test mode
|
||||
|
||||
Use this when you want live creates, but only inside a constrained sandbox.
|
||||
|
||||
- Set `PROXMOX_PREVENT_CREATE=false`
|
||||
- Set `PROXMOX_ENABLE_TEST_MODE=true`
|
||||
- `PROXMOX_TEST_NODE`, `PROXMOX_TEST_POOL`, `PROXMOX_TEST_TAG`, and `PROXMOX_TEST_VM_NAME_PREFIX` become required
|
||||
- The app restricts creates to the configured node and pool
|
||||
- The app automatically adds the configured tag and name prefix
|
||||
|
||||
## Environment variables
|
||||
|
||||
Start from `.env.example`. The table below describes every supported variable that is currently parsed by the app.
|
||||
|
||||
| Variable | Required | Default | Recommended | Purpose |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `PROXMOX_URL` | Required for live access | none | Yes | Base Proxmox URL, for example `https://pve.example.com:8006`. |
|
||||
| `PROXMOX_REALM` | Required for live access | none | Yes | Proxmox auth realm, for example `pam`, `pve`, or `ldap`. |
|
||||
| `PROXMOX_USER` | Required for live access | none | Yes | Username used for API login. If it does not contain `@realm`, the configured realm is appended automatically. |
|
||||
| `PROXMOX_PASSWORD` | Required for live access | none | Yes | Password for the Proxmox user. |
|
||||
| `PROXMOX_VERIFY_TLS` | Optional | `false` | Yes, if your certificates are valid | Controls TLS certificate verification for API calls. Set to `true` for properly trusted certificates. Set to `false` only for internal or self-signed setups you explicitly trust. |
|
||||
| `PROXMOX_API_BASE` | Optional | `/api2/json` | Usually leave as-is | API base path appended to `PROXMOX_URL`. Only change this if your deployment needs a different base path. |
|
||||
| `PROXMOX_REQUEST_TIMEOUT_SECONDS` | Optional | `15` | Usually yes | Request timeout used for API calls and task polling. Increase it if your environment is slow. |
|
||||
| `PROXMOX_DEFAULT_ISO_SELECTOR` | Optional | unset | Optional | Controls which ISO image is auto-selected in the OS step. Uses glob matching by default. If prefixed with `regex:`, the remainder is treated as a regular expression. |
|
||||
| `PROXMOX_PREVENT_CREATE` | Optional | `false` | Yes | Global create safety switch. Set to `true` to block VM creation completely. Leave unset or set to `false` to allow creates. |
|
||||
| `PROXMOX_ENABLE_TEST_MODE` | Optional | `false` | Yes for shared or risky environments | Enables restricted live-create mode. When enabled, the `PROXMOX_TEST_*` scope settings become mandatory. |
|
||||
| `PROXMOX_TEST_NODE` | Required only in test mode | none | Yes in test mode | Node that live creates are restricted to. |
|
||||
| `PROXMOX_TEST_POOL` | Required only in test mode | none | Yes in test mode | Pool that live creates are restricted to. |
|
||||
| `PROXMOX_TEST_TAG` | Required only in test mode | `codex-e2e` | Yes in test mode | Tag added automatically to created VMs in test mode. |
|
||||
| `PROXMOX_TEST_VM_NAME_PREFIX` | Required only in test mode | `codex-e2e-` | Yes in test mode | Prefix added automatically to VM names in test mode. |
|
||||
| `PROXMOX_KEEP_FAILED_VM` | Optional | `true` | Leave as-is for now | Parsed by settings, but currently not acted on by the create workflow yet. Treat it as reserved for future cleanup behavior. |
|
||||
|
||||
## ISO selector syntax
|
||||
|
||||
`PROXMOX_DEFAULT_ISO_SELECTOR` supports two forms:
|
||||
|
||||
- Glob syntax, used by default
|
||||
- Regex syntax, enabled with a `regex:` prefix
|
||||
|
||||
Examples:
|
||||
|
||||
- `PROXMOX_DEFAULT_ISO_SELECTOR=*ubuntu*`
|
||||
- `PROXMOX_DEFAULT_ISO_SELECTOR=*debian-12*`
|
||||
- `PROXMOX_DEFAULT_ISO_SELECTOR=regex:nixos-minimal-\d{2}\.\d{2}\..*-x86_64-linux\.iso$`
|
||||
|
||||
Behavior:
|
||||
|
||||
- if the selector matches one or more ISOs, the app picks from those matches
|
||||
- if multiple matching NixOS-style ISOs exist, it prefers the latest one by release naming
|
||||
- if nothing matches, the app falls back to the built-in default picker
|
||||
|
||||
## Recommended `.env` setups
|
||||
|
||||
### Safe initial setup
|
||||
|
||||
```dotenv
|
||||
PROXMOX_URL=https://pve.example.com:8006
|
||||
PROXMOX_REALM=pam
|
||||
PROXMOX_USER=root
|
||||
PROXMOX_PASSWORD=replace-me
|
||||
PROXMOX_VERIFY_TLS=true
|
||||
PROXMOX_PREVENT_CREATE=true
|
||||
PROXMOX_ENABLE_TEST_MODE=false
|
||||
```
|
||||
|
||||
### Normal live-create setup
|
||||
|
||||
```dotenv
|
||||
PROXMOX_URL=https://pve.example.com:8006
|
||||
PROXMOX_REALM=pam
|
||||
PROXMOX_USER=root
|
||||
PROXMOX_PASSWORD=replace-me
|
||||
PROXMOX_VERIFY_TLS=true
|
||||
PROXMOX_PREVENT_CREATE=false
|
||||
PROXMOX_ENABLE_TEST_MODE=false
|
||||
PROXMOX_DEFAULT_ISO_SELECTOR=*nixos*
|
||||
```
|
||||
|
||||
### Restricted test setup
|
||||
|
||||
```dotenv
|
||||
PROXMOX_URL=https://pve.example.com:8006
|
||||
PROXMOX_REALM=pam
|
||||
PROXMOX_USER=root
|
||||
PROXMOX_PASSWORD=replace-me
|
||||
PROXMOX_VERIFY_TLS=true
|
||||
PROXMOX_PREVENT_CREATE=false
|
||||
PROXMOX_ENABLE_TEST_MODE=true
|
||||
PROXMOX_TEST_NODE=pve-test-01
|
||||
PROXMOX_TEST_POOL=sandbox
|
||||
PROXMOX_TEST_TAG=codex-e2e
|
||||
PROXMOX_TEST_VM_NAME_PREFIX=codex-e2e-
|
||||
```
|
||||
|
||||
## Notes and caveats
|
||||
|
||||
- Live access requires `PROXMOX_URL`, `PROXMOX_USER`, `PROXMOX_PASSWORD`, and `PROXMOX_REALM`.
|
||||
- Test mode is not the same as read-only mode. Test mode still performs real creates when creation is allowed.
|
||||
- If `PROXMOX_PREVENT_CREATE=true`, the confirm step validates but actual creation is blocked.
|
||||
- `PROXMOX_TEST_POOL` is currently required when test mode is enabled.
|
||||
- `PROXMOX_KEEP_FAILED_VM` is currently reserved and not yet implemented in the workflow logic.
|
||||
|
||||
## Live diagnostics
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
uv run python -m pve_vm_setup --doctor-live
|
||||
```
|
||||
|
||||
This verifies:
|
||||
|
||||
- transport reachability
|
||||
- API base access
|
||||
- visible nodes
|
||||
- configured test node and pool, when test mode is enabled
|
||||
|
||||
Use this before enabling creates against a real cluster.
|
||||
|
||||
## Engineering rules
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import fnmatch
|
||||
import re
|
||||
from dataclasses import replace
|
||||
|
||||
|
|
@ -10,6 +11,7 @@ from .settings import AppSettings
|
|||
_NIXOS_ISO_PATTERN = re.compile(
|
||||
r"nixos-minimal-(?P<year>\d{2})[.-](?P<month>\d{2})\.[A-Za-z0-9]+-[A-Za-z0-9_]+-linux\.iso$"
|
||||
)
|
||||
_REGEX_SELECTOR_PREFIX = "regex:"
|
||||
|
||||
|
||||
def select_latest_nixos_iso(isos: list[str]) -> str | None:
|
||||
|
|
@ -23,6 +25,21 @@ def select_latest_nixos_iso(isos: list[str]) -> str | None:
|
|||
return max(candidates)[2]
|
||||
|
||||
|
||||
def _matches_iso_selector(iso: str, selector: str) -> bool:
|
||||
if selector.startswith(_REGEX_SELECTOR_PREFIX):
|
||||
pattern = selector.removeprefix(_REGEX_SELECTOR_PREFIX)
|
||||
return re.search(pattern, iso) is not None
|
||||
return fnmatch.fnmatch(iso, selector)
|
||||
|
||||
|
||||
def select_preferred_iso(isos: list[str], selector: str | None = None) -> str | None:
|
||||
if selector:
|
||||
matches = sorted(iso for iso in isos if _matches_iso_selector(iso, selector))
|
||||
if matches:
|
||||
return select_latest_nixos_iso(matches) or matches[0]
|
||||
return select_latest_nixos_iso(isos)
|
||||
|
||||
|
||||
def build_startup_value(order: str, up: str, down: str) -> str:
|
||||
parts: list[str] = []
|
||||
if order.strip():
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -12,7 +12,7 @@ from textual.widgets import Button, Checkbox, Input, Select, Static
|
|||
|
||||
from ..domain import (
|
||||
build_confirmation_text,
|
||||
select_latest_nixos_iso,
|
||||
select_preferred_iso,
|
||||
validate_all_steps,
|
||||
validate_step,
|
||||
)
|
||||
|
|
@ -55,10 +55,13 @@ class AutoStartConfirmModal(ModalScreen[bool | None]):
|
|||
#auto-start-actions {
|
||||
margin-top: 1;
|
||||
height: auto;
|
||||
width: 1fr;
|
||||
align-horizontal: center;
|
||||
}
|
||||
|
||||
#auto-start-actions Button {
|
||||
min-width: 8;
|
||||
width: 12;
|
||||
min-width: 12;
|
||||
margin-right: 1;
|
||||
}
|
||||
"""
|
||||
|
|
@ -676,7 +679,10 @@ class WizardView(Vertical):
|
|||
self._workflow.reference_data.isos = iso_values
|
||||
self._loaded_iso_source = (node, storage)
|
||||
self._set_select_options("os-iso", iso_values)
|
||||
preferred = select_latest_nixos_iso(iso_values) or (iso_values[0] if iso_values else "")
|
||||
preferred = select_preferred_iso(
|
||||
iso_values,
|
||||
self._settings.default_iso_selector,
|
||||
) or (iso_values[0] if iso_values else "")
|
||||
if preferred:
|
||||
self.query_one("#os-iso", Select).value = preferred
|
||||
self._workflow.config.os.iso = preferred
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
|
@ -82,6 +83,7 @@ class AppSettings:
|
|||
proxmox_realm: str | None
|
||||
proxmox_verify_tls: bool
|
||||
request_timeout_seconds: int
|
||||
default_iso_selector: str | None
|
||||
safety_policy: LiveSafetyPolicy
|
||||
|
||||
@classmethod
|
||||
|
|
@ -122,6 +124,14 @@ class AppSettings:
|
|||
proxmox_url = (raw.get("PROXMOX_URL") or "").strip() or None
|
||||
if proxmox_url is not None:
|
||||
proxmox_url = proxmox_url.rstrip("/")
|
||||
default_iso_selector = (raw.get("PROXMOX_DEFAULT_ISO_SELECTOR") or "").strip() or None
|
||||
if default_iso_selector and default_iso_selector.startswith("regex:"):
|
||||
try:
|
||||
re.compile(default_iso_selector.removeprefix("regex:"))
|
||||
except re.error as exc:
|
||||
raise SettingsError(
|
||||
"Invalid PROXMOX_DEFAULT_ISO_SELECTOR regex."
|
||||
) from exc
|
||||
|
||||
return cls(
|
||||
proxmox_url=proxmox_url,
|
||||
|
|
@ -133,6 +143,7 @@ class AppSettings:
|
|||
request_timeout_seconds=_parse_int(
|
||||
raw.get("PROXMOX_REQUEST_TIMEOUT_SECONDS"), default=15
|
||||
),
|
||||
default_iso_selector=default_iso_selector,
|
||||
safety_policy=safety_policy,
|
||||
)
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -1,4 +1,9 @@
|
|||
from pve_vm_setup.domain import build_create_payload, select_latest_nixos_iso, validate_all_steps
|
||||
from pve_vm_setup.domain import (
|
||||
build_create_payload,
|
||||
select_latest_nixos_iso,
|
||||
select_preferred_iso,
|
||||
validate_all_steps,
|
||||
)
|
||||
from pve_vm_setup.models.workflow import VmConfig
|
||||
from pve_vm_setup.settings import AppSettings
|
||||
|
||||
|
|
@ -15,6 +20,32 @@ def test_select_latest_nixos_iso_prefers_latest_year_month() -> None:
|
|||
assert choice == "cephfs:iso/nixos-minimal-25.05.ffffeeee-x86_64-linux.iso"
|
||||
|
||||
|
||||
def test_select_preferred_iso_uses_glob_selector_when_configured() -> None:
|
||||
choice = select_preferred_iso(
|
||||
[
|
||||
"cephfs:iso/debian-12.iso",
|
||||
"cephfs:iso/nixos-minimal-24.11.1234abcd-x86_64-linux.iso",
|
||||
"cephfs:iso/ubuntu-24.04.iso",
|
||||
],
|
||||
"*ubuntu*",
|
||||
)
|
||||
|
||||
assert choice == "cephfs:iso/ubuntu-24.04.iso"
|
||||
|
||||
|
||||
def test_select_preferred_iso_uses_regex_selector_when_prefixed() -> None:
|
||||
choice = select_preferred_iso(
|
||||
[
|
||||
"cephfs:iso/debian-12.iso",
|
||||
"cephfs:iso/nixos-minimal-24.11.1234abcd-x86_64-linux.iso",
|
||||
"cephfs:iso/nixos-graphical-25.05.iso",
|
||||
],
|
||||
r"regex:nixos-graphical-\d{2}\.\d{2}\.iso$",
|
||||
)
|
||||
|
||||
assert choice == "cephfs:iso/nixos-graphical-25.05.iso"
|
||||
|
||||
|
||||
def test_build_create_payload_applies_safety_name_tag_and_key_settings() -> None:
|
||||
settings = AppSettings.from_env(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ def test_settings_load_defaults_and_normalize_api_base() -> None:
|
|||
assert settings.proxmox_api_base == "/api2/json"
|
||||
assert settings.proxmox_verify_tls is False
|
||||
assert settings.request_timeout_seconds == 15
|
||||
assert settings.default_iso_selector is None
|
||||
assert settings.effective_username == "root@pam"
|
||||
assert settings.safety_policy.prevent_create is False
|
||||
assert settings.safety_policy.enable_test_mode is False
|
||||
|
|
@ -54,3 +55,13 @@ def test_settings_allow_create_by_default_when_prevent_flag_is_unset() -> None:
|
|||
|
||||
assert settings.safety_policy.prevent_create is False
|
||||
assert settings.safety_policy.allow_create is True
|
||||
|
||||
|
||||
def test_settings_reject_invalid_default_iso_regex_selector() -> None:
|
||||
with pytest.raises(SettingsError):
|
||||
AppSettings.from_env(
|
||||
{
|
||||
"PROXMOX_DEFAULT_ISO_SELECTOR": "regex:[unterminated",
|
||||
},
|
||||
load_dotenv_file=False,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue