From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 022001FF0ED for ; Fri, 31 Jul 2026 16:40:13 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 7F23F2157B; Fri, 31 Jul 2026 16:40:12 +0200 (CEST) From: Christoph Heiss To: pdm-devel@lists.proxmox.com Subject: [PATCH installer 09/16] post-hook: support creating API token if requested in answer file Date: Fri, 31 Jul 2026 16:35:32 +0200 Message-ID: <20260731143910.936881-10-c.heiss@proxmox.com> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260731143910.936881-1-c.heiss@proxmox.com> References: <20260731143910.936881-1-c.heiss@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1785508797335 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.008 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_LOW -0.7 Sender listed at https://www.dnswl.org/, low trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: CAAOYQFVY3L327A46QEXF3XDNQ3B654P X-Message-ID-Hash: CAAOYQFVY3L327A46QEXF3XDNQ3B654P X-MailFrom: c.heiss@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: If `global.post-installation-webhook.api-token` is set and we're installing either PVE or PBS, create a new API token with the given name, and include it in the info sent back. Signed-off-by: Christoph Heiss --- Depends on the `proxmox-installer-types` changes and an accompanying dependency bump. Cargo.toml | 4 +- debian/control | 1 + proxmox-post-hook/Cargo.toml | 3 +- proxmox-post-hook/src/main.rs | 121 +++++++++++++++++++++++++++++++++- 4 files changed, 124 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7ba4a94..ad5294a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,12 +28,14 @@ serde_plain = "1.0" sha2 = "0.10" toml = "0.8" proxmox-auto-installer.path = "./proxmox-auto-installer" -proxmox-installer-common.path = "./proxmox-installer-common" proxmox-network-types = "1.1" +proxmox-installer-common.path = "./proxmox-installer-common" proxmox-installer-types = { version = "0.1.1", features = ["legacy"] } +proxmox-time = "2.1" # Local path overrides # NOTE: You must run `cargo update` after changing this for it to take effect! [patch.crates-io] # proxmox-network-types.path = "../proxmox/proxmox-network-types" # proxmox-installer-types.path = "../proxmox/proxmox-installer-types" +# proxmox-time.path = "../proxmox/proxmox-time" diff --git a/debian/control b/debian/control index 7565c2a..cc1f278 100644 --- a/debian/control +++ b/debian/control @@ -22,6 +22,7 @@ Build-Depends: cargo:native, librust-proxmox-installer-types-0.1+legacy-dev (>= 0.1.1-~~), librust-proxmox-network-types-1-dev (>= 1.1-~~), librust-proxmox-sys+crypt-dev, + librust-proxmox-time-dev, librust-regex-1+default-dev (>= 1.7~~), librust-rustls-0.23-dev, librust-rustls-native-certs-dev, diff --git a/proxmox-post-hook/Cargo.toml b/proxmox-post-hook/Cargo.toml index b6d9ce5..82a23c4 100644 --- a/proxmox-post-hook/Cargo.toml +++ b/proxmox-post-hook/Cargo.toml @@ -13,8 +13,9 @@ homepage = "https://www.proxmox.com" [dependencies] anyhow.workspace = true proxmox-installer-common = { workspace = true, features = ["http"] } -proxmox-network-types.workspace = true proxmox-installer-types.workspace = true +proxmox-network-types.workspace = true +proxmox-time.workspace = true rustls.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true diff --git a/proxmox-post-hook/src/main.rs b/proxmox-post-hook/src/main.rs index 760ab53..f7a41c9 100644 --- a/proxmox-post-hook/src/main.rs +++ b/proxmox-post-hook/src/main.rs @@ -49,8 +49,8 @@ mod detail { AutoInstallerConfig, FqdnConfig, FqdnFromDhcpConfig, FqdnSourceMode, SchemaVersion, }, post_hook::{ - BootInfo, CpuInfo, DiskInfo, KernelVersionInformation, NetworkInterfaceInfo, - PostHookInfo, PostHookInfoSchema, ProductInfo, SchemaVersion, SshPublicHostKeys, + BootInfo, CpuInfo, CreatedApiToken, DiskInfo, KernelVersionInformation, + NetworkInterfaceInfo, PostHookInfo, PostHookInfoSchema, ProductInfo, SshPublicHostKeys, }, }; @@ -148,6 +148,24 @@ mod detail { None }; + let api_token = if let Some(name) = answer + .post_installation_webhook + .as_ref() + .and_then(|p| p.api_token_name.as_ref()) + && schema_1_3_supported + && setup_info.config.product.supports_api_token_creation() + { + match create_api_token(name, target_path, setup_info.config.product, &run_cmd) { + Ok(token) => Some(token), + Err(err) => { + eprintln!("could not create API token: {err:#}"); + None + } + } + } else { + None + }; + Ok(PostHookInfo { schema: PostHookInfoSchema { version: super::POST_HOOK_SCHEMA_VERSION.to_owned(), @@ -220,7 +238,7 @@ mod detail { }, reboot_mode: answer.global.reboot_mode, cert_fingerprint, - api_token: None, + api_token, }) } @@ -638,6 +656,103 @@ mod detail { } } + pub fn create_api_token( + name: &str, + target_path: &str, + product: ProxmoxProduct, + run_cmd: &dyn Fn(&[&str]) -> Result, + ) -> Result { + const USER: &str = "root@pam"; + let date = proxmox_time::epoch_to_rfc2822(proxmox_time::epoch_i64())?; + let comment = format!("auto-generated during installation on {date}"); + + println!("Creating API token '{name}' for '{USER}' .."); + match product { + ProxmoxProduct::Pve => with_pmxcfs(target_path, |_| { + run_cmd(&[ + "pveum", + "user", + "token", + "add", + USER, + name, + "-privsep=0", + "-expire=0", + "-comment", + &comment, + "-output-format=json", + ]) + .map_err(|err| anyhow!(err)) + .and_then(|s| { + serde_json::from_str::(&s).map_err(|err| anyhow!(err)) + }) + .and_then(|v| { + Ok(CreatedApiToken { + id: v["full-tokenid"] + .as_str() + .ok_or_else(|| anyhow!("expected string for tokenid"))? + .parse()?, + secret: v["value"] + .as_str() + .ok_or_else(|| anyhow!("expected string for secret"))? + .to_owned(), + }) + }) + .context("generating API token") + }), + ProxmoxProduct::Pbs => { + let token = run_cmd(&[ + "proxmox-backup-manager", + "user", + "generate-token", + USER, + name, + "--comment", + &comment, + "--expire=0", + // TODO: once `user generate-token` supports it, switch to `--output-format + // json` and drop the rather ugly trim() hack below. + ]) + .map_err(|err| anyhow!(err)) + .and_then(|s| { + serde_json::from_str::( + s.trim_start_matches("Result:").trim(), + ) + .map_err(|err| anyhow!(err)) + }) + .and_then(|v| { + Ok(CreatedApiToken { + id: v["tokenid"] + .as_str() + .ok_or_else(|| anyhow!("expected string for tokenid"))? + .parse()?, + secret: v["value"] + .as_str() + .ok_or_else(|| anyhow!("expected string for secret"))? + .to_owned(), + }) + }) + .context("generating API token")?; + + // Give the token full privileges - same as the PDM wizard + run_cmd(&[ + "proxmox-backup-manager", + "acl", + "update", + "/", + "Admin", + "--auth-id", + &token.id.to_string(), + "--propagate=true", + ]) + .context("setting up API token ACL")?; + + Ok(token) + } + _ => bail!("cannot create API token for {}", product.full_name()), + } + } + fn is_path_a_mountpoint(path: impl AsRef) -> Result { let path = path.as_ref(); -- 2.54.0