public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
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(&param)?;
+
         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





  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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal