From 376e6f5631d14110df9866f2d0068586857d1b25 Mon Sep 17 00:00:00 2001 From: phg Date: Sun, 8 Mar 2026 19:25:45 +0100 Subject: [PATCH] Refactor Proxmox settings validation and enhance dotenv loading logic; update README for clarity on configuration requirements --- .env.example | 15 +-- README.md | 22 +++-- .../__pycache__/settings.cpython-313.pyc | Bin 9895 -> 10656 bytes .../__pycache__/proxmox.cpython-313.pyc | Bin 22065 -> 22074 bytes src/pve_vm_setup/services/proxmox.py | 2 +- src/pve_vm_setup/settings.py | 31 +++++-- .../test_factory.cpython-313-pytest-8.4.2.pyc | Bin 3385 -> 3305 bytes ...test_settings.cpython-313-pytest-8.4.2.pyc | Bin 12231 -> 17871 bytes tests/test_factory.py | 3 - tests/test_settings.py | 87 ++++++++++++++++++ 10 files changed, 135 insertions(+), 25 deletions(-) diff --git a/.env.example b/.env.example index be671eb..482aa68 100644 --- a/.env.example +++ b/.env.example @@ -2,16 +2,19 @@ # Required for live API access. PROXMOX_URL=https://proxmox.example.invalid:8006 -# Login realm used for authentication. -# Required for live API access. +# Default login realm used for authentication. +# Optional for the interactive app, because realms can be loaded and selected manually. +# Still required for non-interactive doctor login checks. PROXMOX_REALM=pam -# Proxmox username. If it does not already include @realm, the app appends PROXMOX_REALM. -# Required for live API access. +# Default Proxmox username. If it does not already include @realm, the app appends PROXMOX_REALM. +# Optional for the interactive app, because it can be entered manually. +# Still required for non-interactive doctor login checks. PROXMOX_USER=root -# Password for the configured user. -# Required for live API access. +# Default password for the configured user. +# Optional for the interactive app, because it can be entered manually. +# Still required for non-interactive doctor login checks. PROXMOX_PASSWORD=replace-me # Verify TLS certificates for API requests. diff --git a/README.md b/README.md index 7d1012c..5ed14a7 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Textual TUI for creating Proxmox VMs with live reference data, guarded create co ## Typical usage -1. Copy `.env.example` to `.env`. +1. Copy `.env.example` to `~/.config/pve-vm-setup/.env` or `.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. @@ -64,12 +64,20 @@ Use this when you want live creates, but only inside a constrained sandbox. Start from `.env.example`. The table below describes every supported variable that is currently parsed by the app. +Configuration source order: + +1. Process environment variables +2. `~/.config/pve-vm-setup/.env` +3. `.env` in the current working directory + +Higher entries override lower ones. + | 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_URL` | Required for live access | none | Yes | Base Proxmox URL, for example `https://pve.example.com:8006`. This is the only setting required to make the interactive app use the real Proxmox backend instead of fake mode. | +| `PROXMOX_REALM` | Optional | none | Recommended | Default realm for login. If unset, the login view loads realms from Proxmox and lets you choose one manually. Still required for non-interactive doctor login checks. | +| `PROXMOX_USER` | Optional | none | Recommended | Default username shown in the login form. If unset, you can type it manually. Still required for non-interactive doctor login checks. | +| `PROXMOX_PASSWORD` | Optional | none | Usually no | Default password shown in the login form. If unset, you can type it manually. Still required for non-interactive doctor login checks. | | `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. | @@ -146,7 +154,9 @@ PROXMOX_TEST_VM_NAME_PREFIX=codex-e2e- ## Notes and caveats -- Live access requires `PROXMOX_URL`, `PROXMOX_USER`, `PROXMOX_PASSWORD`, and `PROXMOX_REALM`. +- Interactive live access only requires `PROXMOX_URL`. +- `PROXMOX_USER`, `PROXMOX_PASSWORD`, and `PROXMOX_REALM` act as login defaults for the UI. +- `--doctor-live` still requires `PROXMOX_URL`, `PROXMOX_USER`, `PROXMOX_PASSWORD`, and `PROXMOX_REALM` because it performs a full non-interactive login. - 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. diff --git a/src/pve_vm_setup/__pycache__/settings.cpython-313.pyc b/src/pve_vm_setup/__pycache__/settings.cpython-313.pyc index da9bd3d35306f3a661c2531fec3ce1b49c846879..f6bac1bd6c25db55885b4e4575dc667cc183b264 100644 GIT binary patch delta 4019 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 delta 3174 zcmZ1wyxf=XGcPX}0|NsC)0VZFIZ_k(Bp6RjRL@El3g(hx$YLoH2o?_J2D1cX*p(Q9 zd4hQ@86_B$7=rnv7=rmtnLv6O7)&7)Ly-`gDgh~mU_lI3!s$$!A`|}=ZhpbU$|!S- zwJb5GH1!sHN@`kSX->&4uDtxB+{B#Bs??Op;!FwR-0=m8Ma8M{N%{FXMbZom4EziX z48?(y8(BpsJF;A3!N9<9i#fF-xtN)Ofk8n*p-2iO!UZ)UGq0pb zk%56hYI6;%3L~TP=7ntgm~=HjI<-IqC@6}wK`dPmp~t|$u#)i>OL1yWS}`NYz#^&1 zJ)9bhx|6qZwlK;}w&HRWU&-jF$ylTgQf~-y7fjt0E&~}ykbocq149GD17Y#&!fF?V z)voZXUEokFl9~K~Yn6f-0|SF5a}me|x44Q+5{pWT%QH(dia<7NG8Gw3Ud1iVC^h*Q zw-ux7WJVr8Mw!V0Jd!dFAluYHdRdCBL2Mlm0T$Jn+{)wQX$XEY412JA zusDhs7g$UJMT|RGAy^V5Qp6K1Rm7Vvps9F^+p(Y^IJKlCGcUdPr7y_2njr6S7Zl}J zJ}m)S}F^%J`C;;v%`pO8mxoAb;KBElx~JEvbwz z$j`}4uJjA3V$n;@E7N4UC6JS!m=d3oUy_g6DVdunIqAWfgzM7h;eeFfUqbiZGr?DLYYlL$%TP|A&7Od zA+uDXFhdw?7+W#30Yfp92M+^7DH8)jL0Bjo$Rb}Z28M{RcqU&y1_o{h1%_hgJhnVm zm;nl4Pd9(yz>WrNL)76wxcU_XM)6_J6tE0{Hy&5{X2 zf$-!5f?S+nxlpK67&#^v3QMwr`~fmTn2{5e&&w6ejpkHtm|k9LNE8(KY~Uc|o-8OVGr3Y&kXZ;49=r^BoS~daf(#6yoI&ECFyn`Z zOCBf80gS>R$HMdoGvslFa=}6^(T0H`lnWH1lM947jo`v+3=DD%(V!v=9C|^ZtOsTx z2qlJ4t{_>Er68?hFy|{Ugfb{GgmQ+m!|b2@OIQkKJ5msegItBgmjLl$>i8KjgGg2q zW(PFllwpp6*<>sQS0N1w#89qKj$j!}P8Eh?CRn0OV^Bo)nA~JrF-aZnXi(7$HZE8c zW=1G?DpN2+9=9WR8ewu};!?}#lm1edI!Fe?HV(Ws?!kv=%r z2L$;?`1(i0hX(l+@l4(%ekcoEB=~{K1%D6`03rfGL=cDw1`#13A{11J3f*EWEl5c$ zNiE_48ONGglA2q5iz&bO7JF)5S!Pjw-Yw?z)RH1mkPa&l0V*`WRZ)@gbc`@K57zOXT?XtJGx6r4IbfszfQjywpVyGC(l&L5dt?B!0ice409BfPL@-Xs|RNY zaAbpWMHN?ZNs&U0LO^0kMiIE(;tg`46DT(_K?Fe2tqEm=n>Y3#NxmXykU|#_;R+($ zK!iIeqj4waBo-Ivrj}&nrxaC!ggrn+4Txv}xuvKR#OwhPQ$U0#hyc43>}F8$TLg9n zhyX=)@$Jcc>aO8dtXv;B7?jj*C~17;wqoUaz^nR!6(T4t|A7<4;$jd}_yCgoz{9{L z`jyv;mFqJz1DoInCXj3eBLf@XXC_A0Fh-Cl-v=;r@>X@5`tMvktU-)LUf%W0Lz0|nj*Kj;^WgwOG=AUSTXS6WIbr#vsNI3}EUrGXs;vM?;7_Ie20K diff --git a/src/pve_vm_setup/services/__pycache__/proxmox.cpython-313.pyc b/src/pve_vm_setup/services/__pycache__/proxmox.cpython-313.pyc index 3ae649000dae8e1dc73fd223e812de1b14c07f87..b84dc719dfd4582e9ec27c65b3fe0773a04150d1 100644 GIT binary patch delta 65 zcmdnEhH=*#M&8f7yj%#`~c%B7CZm| delta 56 zcmdnBhH>K>M&8f7yj% None: - settings.validate_live_requirements() + settings.validate_live_endpoint_requirements() self._settings = settings factory = client_factory or httpx.Client self._client = factory( diff --git a/src/pve_vm_setup/settings.py b/src/pve_vm_setup/settings.py index 972f65e..9e9a17c 100644 --- a/src/pve_vm_setup/settings.py +++ b/src/pve_vm_setup/settings.py @@ -12,6 +12,14 @@ from dotenv import dotenv_values from .errors import SettingsError +def _load_dotenv(path: Path) -> dict[str, str]: + return { + key: value + for key, value in dotenv_values(path).items() + if value is not None + } + + def _parse_bool(value: str | None, *, default: bool) -> bool: if value is None or value == "": return default @@ -93,16 +101,18 @@ class AppSettings: *, load_dotenv_file: bool = True, dotenv_path: str | Path = ".env", + config_dotenv_path: str | Path | None = None, ) -> AppSettings: raw: dict[str, str] = {} if load_dotenv_file: - raw.update( - { - key: value - for key, value in dotenv_values(dotenv_path).items() - if value is not None - } + cwd_dotenv = Path(dotenv_path) + home_config_dotenv = ( + Path(config_dotenv_path).expanduser() + if config_dotenv_path is not None + else Path.home() / ".config" / "pve-vm-setup" / ".env" ) + raw.update(_load_dotenv(cwd_dotenv)) + raw.update(_load_dotenv(home_config_dotenv)) raw.update(os.environ if env is None else env) api_base = raw.get("PROXMOX_API_BASE", "/api2/json").strip() or "/api2/json" @@ -149,7 +159,7 @@ class AppSettings: @property def is_live_configured(self) -> bool: - return bool(self.proxmox_url and self.proxmox_user and self.proxmox_password) + return bool(self.proxmox_url) @property def effective_username(self) -> str | None: @@ -176,9 +186,8 @@ class AppSettings: return f"{self.proxmox_url}{self.proxmox_api_base}" def validate_live_requirements(self) -> None: + self.validate_live_endpoint_requirements() missing: list[str] = [] - if not self.proxmox_url: - missing.append("PROXMOX_URL") if not self.proxmox_user: missing.append("PROXMOX_USER") if not self.proxmox_password: @@ -188,3 +197,7 @@ class AppSettings: if missing: joined = ", ".join(missing) raise SettingsError(f"Missing live Proxmox configuration: {joined}.") + + def validate_live_endpoint_requirements(self) -> None: + if not self.proxmox_url: + raise SettingsError("Missing live Proxmox configuration: PROXMOX_URL.") diff --git a/tests/__pycache__/test_factory.cpython-313-pytest-8.4.2.pyc b/tests/__pycache__/test_factory.cpython-313-pytest-8.4.2.pyc index 1fb5b0c3e30ec6e86f7f4eec290c90eea2f23621..593cac001c31f8c806c37bab0da4c925897c36e8 100644 GIT binary patch delta 206 zcmdlf^-_}eGcPX}0|NuY-aTtGQ#bPVu`wAjPhwQz3T6yuGGGj54rZBrku8y#HJEL3 zAxGfkyNp51Y{Bf43psQr$1|ldvj=lbF63Cu$T8WGLveC1GZ!Q0WJk{F{9JywxC4Ux zBYgcM;zNUcCg0*zn;gis(@>Rxfq|cafuZ;@0|P??!ySI9>-@@>_>~{2PB0e<8=N1o~6YLsvO8~|Ta10I( z_YZQp#S0S&a&`3aoy^X)(_EW@fq|cafuZ;%0|P??!ySI9>-@@>_>~_>JKm5{x}l(P zLssb{Co3n{2Ob6~`H%b@0&EQ)UqmK<1Kf8QwI|QzkzzEP Ryn{!RgP&2NNRfeo0RT5bP|5%R diff --git a/tests/__pycache__/test_settings.cpython-313-pytest-8.4.2.pyc b/tests/__pycache__/test_settings.cpython-313-pytest-8.4.2.pyc index 81e221aefac1dcf89b844d782ecb546dade65ee5..db64f308d9ae7ba91953a60e0dbda7adf4f5d446 100644 GIT binary patch delta 5193 zcmX>ef4-Yh{WC8w7Xt$WL)*T!nV*Ch7#@Q-Fw6pFd_Fc&U8$bQo6U>8h+Tmpm?4FQ^cdd5G)wXCdH7&Qp6i97|af4@dXP8bAVa=!Ggh@ zV3t6zU@#Y$B^WFi%nfD<1q%lAfLX%9yfN&-tig;XEWvyrk%S@VBp+5NAd-$o&W;_!%Eg$ zYz37isl_F?*oqP}i&Kk>Kp|bk3NlFzq=2Ew1;lomyh+}g%ZGu1L6L!hq1b$Kx1P*q zVTBqNK9DgXMY15Zj8);2?~B<@zO5ovF9y;DQd^`9VyS=#RR#tIO9UlZz9E{;M1{MZ}TMm;as!C6OuC2xz#=yW}zF9~|7vV~# zdyJE#byX(I%JWQaSCC-IV+>}U+{I!l$`;CO!WhgRA!Nd23W`An28Ljc$qoFI8962w zaE0=6>M`rGF)-w@1ak#*PcC2zl22#Q7l30=oNf$+_g{7HAsky0nCB>SI;N(!u2#OX+jQF9(2zI^oAkFc`C7C%n z@p-AKDaG;NsEAKVO-n4zDJcfWR7!qwNq$jL3O?J_3wQGK4Y)fdT^*&`3Cx!4xX008+t>Bn4##Ge(2TUobC}A(%;*n*pxJlnE@%puiBy z5X=mc2?7;w5J4Ep#SqLA%o@yQ$)qs(jDjMc0Yfl*Foz{m9#be&5YJ=*K~YK0eCJRW zD-Q;S{7_J#1=5+ai-94UZ?XWBoJ25JD60u$Fn5FrQgOl)Ej0N7tK8%T{DO>plP#6x zC-ZW0PVV5~<=_KpXJBBMT*x9ic>^mCuRmijPc(|CpAf1tD`P(NBH_j#D@m?*k+WJ6ck(O>nE3|=%rR9<`(3n>SgAEQ>2xJfq@y< zFV@nMG+m38jJG&a^OEyZGV{_yz(rz}0#3d4nJHk+MZ%!uAqpbEH3nLV$bXA9IU^;r z=oVXXYDsEd*)8VclA>E&9l zS&&$goB_^+MK}vh4p8od)P)L=OlpamNptcOQ;Or0ON)w9^Gf1VGK*3{Ij=H4B_HJU z`0|X@y!e8m)Z)~9St;{mtub#D2K-0~M#TrViQLdc7P+^*MIpy(2d>jiH4$v$$rn(`3A%V4Dt zHZpmI#TDG9f#%g6ki5ze%m~U&pezE(agf}V#~jLxEf<3_m?a~w9E_I7;kg-{mcc|Y zTQnr!CWA^6CRk2D$=>WB2L*A0)uRx>9KoEyT$W4<457@yxK(k(R84N+SFkYv6$(6- zOnEG!EJ3imiDYaZYcOwqYA74HP$&l_L69W4P!O2>R#8qOm@kywgfW;uA`Gce5Qr9; zY^ek;6gWV|B}fmb7+?YA@5v1uqTDc1h9D_W&Xq!Bdw<4YfoL@8$^TfncrXMFG`aMG z1)+rmN3c+^a3}|~kl>64H9bMrG9YP_16jos$_XkYSZx^#%F-D$Mf{3Df&GhJ57M0Z z#Zyq0s#}(;Tbx=_S`f05@fLeQA}G}t7bSsmVG4*y1rcc=BAtPO;gfrxAnkpm(?DXa+8UN6EZK^Mfk$LQ+C^@e2A3OxqBA%z3Mww}yC`VX;B|wA?>dXf zMHZ16As0Zj61c=cFMsNx1(3&P#X}+yNKXjMN)kkY#5r(>nt8u zSUix6ustKA2y+PL3Fb9m3}y-Dqof$)2Nz=kD8-l{WyP2vQqco0Svelx=R0Qhy`e};a;z%mZ%qhvtD=yNS{6x=K z1XO2(>adcGoXn&mP{*RkbF!?yWIed+CR$LI8ef(R>L8aE=)t`d-29Z%oK(A_W(EcZQ2(a5k%58X z12ZEd<9$vBh8Bja3=)qSCNI>NiT=XJ!KnX5jFVCCiv&9(*9Q?UMmNS2EFTy^^o6*j uFJQ(8DPcxW#v6P>ADBUm2dtc5SV4>rIueX-5OEOW0W0TMRuBVR9s&TA36mxO delta 854 zcmX@#&3HUs{WC8w7Xt$WgZ-AZnL9Zc7#@Q-Fw6vHd@h)%t`y1W%~r$~%n;1v&F;lf z#G$|t%n;1%&FRHe#HGLx%o)rg#gN5P#2w5T%nD}l1ak(nfmyu4oWbm17GE%DFb9~$ zpU$Ys^%7)(-y}wb%^HkXg&3!;reL6A}@5FrC1WI==+0|SF5ZxP(YB0Z1;c@lGS z^2_6si&7IyQi~Kp3Y9>FGKf$C5vm|U4MeDe2n`Tn0CEv~5!f1Ikbo(OumBO33=9ks zMdl!;6$1l9F*^eTg8~E;Sx^3?>dop4a>!;UH5*1o+s*ar1}ya;rx%HVv`B&ou(7DF z4=M5j=|XpYkv>R0v99(8DfYo|6vJda9amO=1_lPx%@sPjjEuIE^9>~@=NVrU1v|_U zq|F6HKw}`%#FjOPfq}th@@x}FSvQcwm~OEZrIwTy<@sp}+~PPO4o|6axbTBgjiS3=9k(m>C%v?=vv8Ff>n&aFyZu%E!*g1x_~r DI%TU{ diff --git a/tests/test_factory.py b/tests/test_factory.py index e5fb38d..55ee220 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -16,9 +16,6 @@ def test_factory_returns_live_service_when_live_env_is_present() -> None: settings = AppSettings.from_env( { "PROXMOX_URL": "https://proxmox.example.invalid:8006", - "PROXMOX_USER": "root", - "PROXMOX_PASSWORD": "secret", - "PROXMOX_REALM": "pam", }, load_dotenv_file=False, ) diff --git a/tests/test_settings.py b/tests/test_settings.py index ec9788d..4dc58fc 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,3 +1,5 @@ +from pathlib import Path + import pytest from pve_vm_setup.errors import SettingsError @@ -57,6 +59,29 @@ def test_settings_allow_create_by_default_when_prevent_flag_is_unset() -> None: assert settings.safety_policy.allow_create is True +def test_settings_treat_url_only_as_live_capable_for_interactive_login() -> None: + settings = AppSettings.from_env( + { + "PROXMOX_URL": "https://proxmox.example.invalid:8006", + }, + load_dotenv_file=False, + ) + + assert settings.is_live_configured is True + + +def test_settings_validate_live_requirements_still_needs_login_defaults_for_doctor() -> None: + settings = AppSettings.from_env( + { + "PROXMOX_URL": "https://proxmox.example.invalid:8006", + }, + load_dotenv_file=False, + ) + + with pytest.raises(SettingsError): + settings.validate_live_requirements() + + def test_settings_reject_invalid_default_iso_regex_selector() -> None: with pytest.raises(SettingsError): AppSettings.from_env( @@ -65,3 +90,65 @@ def test_settings_reject_invalid_default_iso_regex_selector() -> None: }, load_dotenv_file=False, ) + + +def test_settings_loads_current_directory_dotenv_when_present(tmp_path: Path, monkeypatch) -> None: + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("HOME", str(tmp_path / "home")) + (tmp_path / ".env").write_text( + "PROXMOX_URL=https://cwd.example.invalid:8006\n", + encoding="utf-8", + ) + + settings = AppSettings.from_env({}, load_dotenv_file=True) + + assert settings.proxmox_url == "https://cwd.example.invalid:8006" + + +def test_settings_prefers_config_dotenv_over_current_directory_dotenv( + tmp_path: Path, monkeypatch +) -> None: + home = tmp_path / "home" + config_dir = home / ".config" / "pve-vm-setup" + config_dir.mkdir(parents=True) + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("HOME", str(home)) + (tmp_path / ".env").write_text( + "PROXMOX_URL=https://cwd.example.invalid:8006\n", + encoding="utf-8", + ) + (config_dir / ".env").write_text( + "PROXMOX_URL=https://config.example.invalid:8006\n", + encoding="utf-8", + ) + + settings = AppSettings.from_env({}, load_dotenv_file=True) + + assert settings.proxmox_url == "https://config.example.invalid:8006" + + +def test_settings_prefers_environment_over_config_and_current_directory_dotenv( + tmp_path: Path, monkeypatch +) -> None: + home = tmp_path / "home" + config_dir = home / ".config" / "pve-vm-setup" + config_dir.mkdir(parents=True) + monkeypatch.chdir(tmp_path) + monkeypatch.setenv("HOME", str(home)) + (tmp_path / ".env").write_text( + "PROXMOX_URL=https://cwd.example.invalid:8006\n", + encoding="utf-8", + ) + (config_dir / ".env").write_text( + "PROXMOX_URL=https://config.example.invalid:8006\n", + encoding="utf-8", + ) + + settings = AppSettings.from_env( + { + "PROXMOX_URL": "https://env.example.invalid:8006", + }, + load_dotenv_file=True, + ) + + assert settings.proxmox_url == "https://env.example.invalid:8006"