From: Christoph Heiss <c.heiss@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [PATCH datacenter-manager 14/16] api: auto-installer: add option for adding new remotes to PDM
Date: Fri, 31 Jul 2026 16:35:37 +0200 [thread overview]
Message-ID: <20260731143910.936881-15-c.heiss@proxmox.com> (raw)
In-Reply-To: <20260731143910.936881-1-c.heiss@proxmox.com>
.. after the installation was successful.
Requires the post-hook machinery to be set up correctly.
If `post-hook-add-as-remote` is set on the prepared answer and the
target installer supports creating API tokens after the installation,
set `post-installation-webhook.api-token-name` in the answer file before
sending it.
The post-hook will then detect if an API token was created and add the
machine as a new remote.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Depends on the `proxmox-installer-types` changes and an accompanying
dependency bump.
lib/pdm-api-types/src/auto_installer.rs | 10 ++
server/src/api/auto_installer/mod.rs | 157 ++++++++++++++++++++----
2 files changed, 146 insertions(+), 21 deletions(-)
diff --git a/lib/pdm-api-types/src/auto_installer.rs b/lib/pdm-api-types/src/auto_installer.rs
index 4b5027fb..bd9540f6 100644
--- a/lib/pdm-api-types/src/auto_installer.rs
+++ b/lib/pdm-api-types/src/auto_installer.rs
@@ -168,6 +168,10 @@ pub const PREPARED_INSTALL_CONFIG_ID_SCHEMA: proxmox_schema::Schema =
schema: CERT_FINGERPRINT_SHA256_SCHEMA,
optional: true,
},
+ "post-hook-add-as-remote": {
+ default: true,
+ optional: true,
+ },
"template-counters": {
type: Object,
properties: {},
@@ -329,6 +333,12 @@ pub struct PreparedInstallationConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[updater(serde(default, skip_serializing_if = "Option::is_none"))]
pub post_hook_cert_fp: Option<String>,
+ /// Whether to add the target host as new remote to PDM after the installation.
+ ///
+ /// Only available with PVE and PBS.
+ #[serde(default)]
+ #[updater(serde(default, skip_serializing_if = "Option::is_none"))]
+ pub post_hook_add_as_remote: bool,
/// Key-value pairs of (auto-incrementing) counters.
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
diff --git a/server/src/api/auto_installer/mod.rs b/server/src/api/auto_installer/mod.rs
index 470f31b6..4b0a946c 100644
--- a/server/src/api/auto_installer/mod.rs
+++ b/server/src/api/auto_installer/mod.rs
@@ -1,12 +1,13 @@
//! Implements all the methods under `/api2/json/auto-install/`.
-use anyhow::{Context, Result, anyhow};
-use http::StatusCode;
+use anyhow::{Context, Result, anyhow, bail};
+use http::{StatusCode, Uri};
+use log::warn;
use std::collections::{BTreeMap, HashMap};
-use pdm_api_types::PROXMOX_TOKEN_NAME_SCHEMA;
use pdm_api_types::{
- Authid, ConfigDigest, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA,
+ ConfigDigest, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA,
+ PROXMOX_TOKEN_NAME_SCHEMA,
auto_installer::{
AnswerToken, AnswerTokenCreateResult, AnswerTokenUpdateResult, AnswerTokenUpdater,
DeletableAnswerTokenProperty, DeletablePreparedInstallationConfigProperty,
@@ -15,12 +16,14 @@ use pdm_api_types::{
PreparedInstallationConfigCreateResult, PreparedInstallationConfigUpdateResult,
PreparedInstallationConfigUpdater, TEMPLATE_COUNTER_NAME_REGEX, UDEV_FILTER_KEY_REGEX,
},
+ remotes::{NodeUrl, Remote, RemoteType},
};
use pdm_config::auto_install::types::PreparedInstallationSectionConfigWrapper;
+use proxmox_auth_api::types::Authid;
use proxmox_installer_types::{
- SystemInfo,
+ ProxmoxProduct, SystemInfo,
answer::{
- self, AutoInstallerConfig, PostNotificationHookInfo, ROOT_PASSWORD_SCHEMA,
+ self, AutoInstallerConfig, PostNotificationHookInfo, ROOT_PASSWORD_SCHEMA, SchemaVersion,
fetch::AnswerFetchData,
},
post_hook::PostHookInfo,
@@ -36,6 +39,11 @@ use proxmox_schema::{
use proxmox_sortable_macro::sortable;
use proxmox_uuid::Uuid;
+use crate::api;
+
+const ANSWER_FETCH_SUPPORTED_SCHEMA: SchemaVersion = SchemaVersion(1, 0);
+const POST_HOOK_SUPPORTED_SCHEMA: SchemaVersion = SchemaVersion(1, 3);
+
#[sortable]
const SUBDIR_INSTALLATION_PER_ID: SubdirMap = &sorted!([(
"post-hook",
@@ -139,6 +147,8 @@ fn api_function_new_installation(
}
};
+ check_compatible_fetch_answer_schema(¶m)?;
+
let response = serde_json::from_value::<AnswerFetchData>(param)
.map_err(|err| anyhow!("failed to deserialize body: {err:?}"))
.and_then(|data| new_installation(&token_id, data))
@@ -161,6 +171,21 @@ fn api_function_new_installation(
})
}
+fn check_compatible_fetch_answer_schema(data: &serde_json::Value) -> Result<()> {
+ let version: SchemaVersion = data
+ .get("$schema")
+ .and_then(|schema| schema.get("version"))
+ .and_then(|val| val.as_str())
+ .ok_or_else(|| anyhow!("answer fetch schema version not found"))
+ .and_then(|s| s.parse())?;
+
+ if version <= ANSWER_FETCH_SUPPORTED_SCHEMA {
+ Ok(())
+ } else {
+ bail!("unsupported answer fetch version")
+ }
+}
+
/// Verifies the given `Authorization` HTTP header value whether
/// a) It matches the required format, i.e. Bearer <token-id>:<secret>
/// b) The token secret is known and verifies successfully.
@@ -227,6 +252,30 @@ fn new_installation(token_id: &String, payload: AnswerFetchData) -> Result<AutoI
.then(|| -> Result<String> { Ok(hex::encode(proxmox_sys::linux::random_data(16)?)) })
.transpose()?;
+ // Inject our custom post hook if the user defined a base url
+ if let Some(base_url) = config.post_hook_base_url {
+ // The target needs to support creating an API token, as well as the user requesting it
+ // to add it as a remote.
+ let should_create_token =
+ payload.sysinfo.supports_api_token_creation() && config.post_hook_add_as_remote;
+
+ answer.post_installation_webhook = Some(PostNotificationHookInfo {
+ url: format!(
+ "{}/api2/json/auto-install/installations/{uuid}/post-hook",
+ base_url.trim_end_matches('/')
+ ),
+ cert_fingerprint: config.post_hook_cert_fp.clone(),
+ auth_token: post_hook_token.clone(),
+ // Anything below was introduced with API token support, so gate it on that to avoid
+ // breaking older ISOs in combination with newer PDM versions
+ max_schema_version: payload
+ .sysinfo
+ .supports_api_token_creation()
+ .then_some(POST_HOOK_SUPPORTED_SCHEMA),
+ api_token_name: should_create_token.then(|| "pdm-admin".to_owned()),
+ });
+ }
+
let installation = Installation {
uuid: uuid.clone(),
received_at: timestamp_now,
@@ -234,21 +283,9 @@ fn new_installation(token_id: &String, payload: AnswerFetchData) -> Result<AutoI
info: payload.sysinfo,
answer_id: Some(config.id.clone()),
post_hook_data: None,
- post_hook_token: post_hook_token.clone(),
+ post_hook_token,
};
- // Inject our custom post hook if the user defined a base url
- if let Some(base_url) = config.post_hook_base_url {
- answer.post_installation_webhook = Some(PostNotificationHookInfo {
- url: format!(
- "{}/api2/json/auto-install/installations/{uuid}/post-hook",
- base_url.trim_end_matches('/')
- ),
- cert_fingerprint: config.post_hook_cert_fp.clone(),
- auth_token: post_hook_token,
- });
- }
-
increment_template_counters(&config.id)?;
pdm_config::auto_install::save_installation(&installation)?;
Ok(answer)
@@ -596,6 +633,7 @@ async fn update_prepared_answer(
disk_filter_match,
post_hook_base_url,
post_hook_cert_fp,
+ post_hook_add_as_remote,
template_counters,
subscription_key,
} = update;
@@ -716,6 +754,10 @@ async fn update_prepared_answer(
p.post_hook_cert_fp = Some(fp);
}
+ if let Some(add) = post_hook_add_as_remote {
+ p.post_hook_add_as_remote = add;
+ }
+
if let Some(counters) = template_counters {
validate_template_map(&counters)?;
**p.template_counters = counters;
@@ -815,7 +857,7 @@ fn validate_template_map(map: &BTreeMap<String, i32>) -> Result<()> {
/// POST /auto-install/installations/{uuid}/post-hook
///
/// Handles the post-installation hook for all installations.
-async fn handle_post_hook(uuid: Uuid, token: String, info: PostHookInfo) -> Result<()> {
+async fn handle_post_hook(uuid: Uuid, token: String, mut info: PostHookInfo) -> Result<()> {
let _lock = pdm_config::auto_install::installations_write_lock();
let (mut installations, _) = pdm_config::auto_install::read_installations()?;
@@ -840,14 +882,87 @@ async fn handle_post_hook(uuid: Uuid, token: String, info: PostHookInfo) -> Resu
return not_found();
}
+ let token_secret = if let Some(token) = &mut info.api_token {
+ Some(std::mem::take(&mut token.secret))
+ } else {
+ None
+ };
+
install.status = InstallationStatus::Finished;
- install.post_hook_data = Some(info);
+ install.post_hook_data = Some(info.clone());
install.post_hook_token = None;
pdm_config::auto_install::save_installation(install)?;
+ if let (Some(token), Some(secret)) = (&info.api_token, token_secret) {
+ add_host_as_remote(install, &info, &token.id, secret).await?
+ }
+
Ok(())
}
+async fn add_host_as_remote(
+ install: &Installation,
+ info: &PostHookInfo,
+ authid: &Authid,
+ token: String,
+) -> Result<()> {
+ let ty = match info.product.short {
+ ProxmoxProduct::Pve => RemoteType::Pve,
+ ProxmoxProduct::Pbs => RemoteType::Pbs,
+ _ => {
+ let answer = install
+ .answer_id
+ .as_ref()
+ .map(|s| format!(" (answer: '{s}')"))
+ .unwrap_or_default();
+
+ warn!(
+ "auto-installer: cannot add {} target as remote{answer}!",
+ info.product.fullname,
+ );
+ return Ok(());
+ }
+ };
+
+ // Prefer the management interface address if available, otherwise fall back to the first
+ // interface with an address. As a last fallback, use the FQDN.
+ let hostname = info
+ .network_interfaces
+ .iter()
+ .find(|iface| iface.is_management)
+ .and_then(|iface| iface.address)
+ .or_else(|| {
+ info.network_interfaces
+ .iter()
+ .find(|iface| iface.address.is_some())
+ .and_then(|iface| iface.address)
+ })
+ .map_or_else(|| info.fqdn.to_owned(), |cidr| cidr.address().to_string());
+
+ let web_url = Uri::builder()
+ .scheme("https")
+ .authority(format!("{}:{}", info.fqdn, ty.default_port()))
+ .build()
+ .ok();
+
+ let remote = Remote {
+ ty,
+ id: info.fqdn.clone(),
+ nodes: vec![
+ NodeUrl {
+ hostname,
+ fingerprint: info.cert_fingerprint.clone(),
+ }
+ .into(),
+ ],
+ authid: authid.clone(),
+ token: token.to_owned(),
+ web_url,
+ };
+
+ api::remotes::add_remote(remote, None).await
+}
+
#[api(
returns: {
description: "List of tokens for authenticating automated installations requests.",
--
2.54.0
next prev parent reply other threads:[~2026-07-31 14:40 UTC|newest]
Thread overview: 17+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-31 14:35 [PATCH proxmox/installer/datacenter-manager 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
2026-07-31 14:35 ` [PATCH proxmox 01/16] installer-types: drop unnecessary clippy attribute Christoph Heiss
2026-07-31 14:35 ` [PATCH proxmox 02/16] installer-types: post-hook: factor schema version into proper struct Christoph Heiss
2026-07-31 14:35 ` [PATCH proxmox 03/16] installer-types: post-hook: allow additional properties on api schema Christoph Heiss
2026-07-31 14:35 ` [PATCH proxmox 04/16] installer-types: post-hook: add api-token and cert-fingerprint options Christoph Heiss
2026-07-31 14:35 ` [PATCH proxmox 05/16] installer-types: systeminfo: add check for API token creation capability Christoph Heiss
2026-07-31 14:35 ` [PATCH installer 06/16] chroot: print full error if bind-mounting fails Christoph Heiss
2026-07-31 14:35 ` [PATCH installer 07/16] post-hook: re-use low-level config retrieval from proxmox-chroot Christoph Heiss
2026-07-31 14:35 ` [PATCH installer 08/16] post-hook: generate and retrieve node certificate fingerprint Christoph Heiss
2026-07-31 14:35 ` [PATCH installer 09/16] post-hook: support creating API token if requested in answer file Christoph Heiss
2026-07-31 14:35 ` [PATCH installer 10/16] auto: enforce https for post hook when generating an API token Christoph Heiss
2026-07-31 14:35 ` [PATCH installer 11/16] assistant: validate-answer: also verify post-hook settings if set Christoph Heiss
2026-07-31 14:35 ` [PATCH datacenter-manager 12/16] ui: auto-installer: spell out Proxmox Datacenter Manager Christoph Heiss
2026-07-31 14:35 ` [PATCH datacenter-manager 13/16] config: auto-install: add optional `post-hook-add-as-remote` field Christoph Heiss
2026-07-31 14:35 ` Christoph Heiss [this message]
2026-07-31 14:35 ` [PATCH datacenter-manager 15/16] ui: auto-installer: wizard: add checkbox to add target as new remote Christoph Heiss
2026-07-31 14:35 ` [PATCH datacenter-manager 16/16] docs: auto-installer: document adding targets as remotes afterwards Christoph Heiss
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260731143910.936881-15-c.heiss@proxmox.com \
--to=c.heiss@proxmox.com \
--cc=pdm-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox