public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH proxmox/installer/datacenter-manager 00/16] auto-installer: add installed target systems as new remotes
@ 2026-07-31 14:35 Christoph Heiss
  2026-07-31 14:35 ` [PATCH proxmox 01/16] installer-types: drop unnecessary clippy attribute Christoph Heiss
                   ` (15 more replies)
  0 siblings, 16 replies; 17+ messages in thread
From: Christoph Heiss @ 2026-07-31 14:35 UTC (permalink / raw)
  To: pdm-devel

Adds a new checkbox to the "Authentication" tab of the wizard for
creating/editing prepared auto-installer answers.

When checked, the installer (in the post-hook) will create a new API token on
the freshly installed system - akin to how PDM does it when creating a new
token - and adds it a remote.

Of course; only supported when installing PVE or PBS, as these are the
supported targets of PDM.

The post-hook information schema (which is sent back to the answer server) is
expanded a bit to accommodate that with two new fields:

- `cert-fingerprint`: The SHA256 fingerprint for the API, needed due to the
  default self-signed certificates.
- `api-token`: Holds the ID of the created token and its secret.

Apply order
===========

Same as the repo order in the patch series, i.e. the `proxmox` part first, then
`pve-installer` and `proxmox-datacenter-manager` need their dependency on
`proxmox-installer-types` bumped accordingly.

Testing
=======

I've tested both following combinations; each with all products as
possible:

- Current PDM (1.1.7) with patched ISOs (i.e. w/ patches from this
  series)
- Patched PDM (i.e. w/ patches from this series) with current ISOs 
  (PVE 9.2-1, PBS 4.2-1, PMG 9.1-1, PDM 1.1-1)
- Patched PDM w/ patched ISOs

Diffstat
========

proxmox:

Christoph Heiss (5):
  installer-types: drop unnecessary clippy attribute
  installer-types: post-hook: factor schema version into proper struct
  installer-types: post-hook: allow additional properties on api schema
  installer-types: post-hook: add api-token and cert-fingerprint options
  installer-types: systeminfo: add check for API token creation
    capability

 proxmox-installer-types/Cargo.toml       |  1 +
 proxmox-installer-types/debian/control   |  4 ++
 proxmox-installer-types/src/answer.rs    | 68 ++++++++++++++++--
 proxmox-installer-types/src/lib.rs       | 92 +++++++++++++++++++++++-
 proxmox-installer-types/src/post_hook.rs | 50 +++++++++++--
 5 files changed, 202 insertions(+), 13 deletions(-)

pve-installer:

Christoph Heiss (6):
  chroot: print full error if bind-mounting fails
  post-hook: re-use low-level config retrieval from proxmox-chroot
  post-hook: generate and retrieve node certificate fingerprint
  post-hook: support creating API token if requested in answer file
  auto: enforce https for post hook when generating an API token
  assistant: validate-answer: also verify post-hook settings if set

 Cargo.toml                                 |   6 +-
 debian/control                             |   1 +
 proxmox-auto-install-assistant/src/main.rs |   7 +-
 proxmox-auto-installer/src/utils.rs        |  16 +-
 proxmox-chroot/src/main.rs                 |  16 +-
 proxmox-installer-common/Cargo.toml        |   4 +-
 proxmox-installer-common/src/setup.rs      |  12 +
 proxmox-post-hook/Cargo.toml               |   5 +-
 proxmox-post-hook/src/main.rs              | 292 ++++++++++++++++++++-
 9 files changed, 328 insertions(+), 31 deletions(-)

proxmox-datacenter-manager:

Christoph Heiss (5):
  ui: auto-installer: spell out Proxmox Datacenter Manager
  config: auto-install: add optional `post-hook-add-as-remote` field
  api: auto-installer: add option for adding new remotes to PDM
  ui: auto-installer: wizard: add checkbox to add target as new remote
  docs: auto-installer: document adding targets as remotes afterwards

 docs/automated-installations.rst              |  33 ++++
 lib/pdm-api-types/src/auto_installer.rs       |  10 ++
 lib/pdm-config/src/auto_install.rs            |  14 ++
 server/src/api/auto_installer/mod.rs          | 157 +++++++++++++++---
 .../prepared_answer_add_wizard.rs             |   1 +
 .../auto_installer/prepared_answer_form.rs    |   9 +-
 6 files changed, 202 insertions(+), 22 deletions(-)




^ permalink raw reply	[flat|nested] 17+ messages in thread

* [PATCH proxmox 01/16] installer-types: drop unnecessary clippy attribute
  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 ` Christoph Heiss
  2026-07-31 14:35 ` [PATCH proxmox 02/16] installer-types: post-hook: factor schema version into proper struct Christoph Heiss
                   ` (14 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2026-07-31 14:35 UTC (permalink / raw)
  To: pdm-devel

It's been superfluous since the rename of the enum members to PascalCase
in commit

  8f7a71bae ("installer-types: add common types used by the installer")

No functional changes.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 proxmox-installer-types/src/lib.rs | 1 -
 1 file changed, 1 deletion(-)

diff --git a/proxmox-installer-types/src/lib.rs b/proxmox-installer-types/src/lib.rs
index df1f7944..8d5ac4fc 100644
--- a/proxmox-installer-types/src/lib.rs
+++ b/proxmox-installer-types/src/lib.rs
@@ -151,7 +151,6 @@ pub struct NetworkInterface {
 }
 
 #[cfg_attr(feature = "api-types", api)]
-#[allow(clippy::upper_case_acronyms)]
 #[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, PartialOrd, Ord, Serialize)]
 #[serde(rename_all = "lowercase")]
 /// The name of the product.
-- 
2.54.0





^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH proxmox 02/16] installer-types: post-hook: factor schema version into proper struct
  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 ` Christoph Heiss
  2026-07-31 14:35 ` [PATCH proxmox 03/16] installer-types: post-hook: allow additional properties on api schema Christoph Heiss
                   ` (13 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2026-07-31 14:35 UTC (permalink / raw)
  To: pdm-devel

This simplifies first and foremost version comparison on consumer sites,
making the schema version actually useful.

No actual API changes, the version is still serialized to/deserialized
from a plain X.Y string.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 proxmox-installer-types/src/answer.rs    | 68 +++++++++++++++++++++---
 proxmox-installer-types/src/lib.rs       | 18 +++++++
 proxmox-installer-types/src/post_hook.rs | 13 +++--
 3 files changed, 90 insertions(+), 9 deletions(-)

diff --git a/proxmox-installer-types/src/answer.rs b/proxmox-installer-types/src/answer.rs
index 266d6a27..68cac026 100644
--- a/proxmox-installer-types/src/answer.rs
+++ b/proxmox-installer-types/src/answer.rs
@@ -9,6 +9,7 @@
 use anyhow::{Result, anyhow, bail};
 use serde::{Deserialize, Serialize};
 use std::{
+    cmp::Ordering,
     collections::{BTreeMap, HashMap},
     fmt::{self, Display},
     str::FromStr,
@@ -63,6 +64,38 @@ pub const SUBSCRIPTION_KEY_SCHEMA: proxmox_schema::Schema =
         .max_length(32)
         .schema();
 
+/// Represents a (major, minor) schema version tuple.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub struct SchemaVersion(pub u32, pub u32);
+
+impl PartialOrd for SchemaVersion {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        match (self.0.cmp(&other.0), self.1.cmp(&other.1)) {
+            (Ordering::Equal, ord) => Some(ord),
+            (ord, _) => Some(ord),
+        }
+    }
+}
+
+impl Display for SchemaVersion {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}.{}", self.0, self.1)
+    }
+}
+
+impl FromStr for SchemaVersion {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        crate::parse_version(s)
+            .map(|(maj, min)| SchemaVersion(maj, min))
+            .ok_or_else(|| anyhow!("invalid version"))
+    }
+}
+
+serde_plain::derive_serialize_from_display!(SchemaVersion);
+serde_plain::derive_deserialize_from_fromstr!(SchemaVersion, "valid version");
+
 /// Defines API types used by proxmox-fetch-answer, the first part of the
 /// auto-installer.
 pub mod fetch {
@@ -71,9 +104,16 @@ pub mod fetch {
     #[cfg(feature = "api-types")]
     use proxmox_schema::api;
 
-    use crate::SystemInfo;
+    use crate::{SystemInfo, answer::SchemaVersion};
 
-    #[cfg_attr(feature = "api-types", api)]
+    #[cfg_attr(feature = "api-types", api(
+        properties: {
+            version: {
+                type: String,
+            },
+        },
+        additional_properties: true,
+    ))]
     #[derive(Deserialize, Serialize)]
     #[serde(rename_all = "kebab-case")]
     /// Metadata of the HTTP POST payload, such as schema version of the document.
@@ -85,17 +125,17 @@ pub mod fetch {
         /// field.
         /// minor: Incremented when adding functionality in a backwards-compatible matter, e.g.
         /// adding a new field.
-        pub version: String,
+        pub version: SchemaVersion,
     }
 
     impl AnswerFetchDataSchema {
-        const SCHEMA_VERSION: &str = "1.0";
+        const SCHEMA_VERSION: SchemaVersion = SchemaVersion(1, 0);
     }
 
     impl Default for AnswerFetchDataSchema {
         fn default() -> Self {
             Self {
-                version: Self::SCHEMA_VERSION.to_owned(),
+                version: Self::SCHEMA_VERSION,
             }
         }
     }
@@ -311,7 +351,14 @@ pub enum FqdnSourceMode {
     FromDhcp,
 }
 
-#[cfg_attr(feature = "api-types", api)]
+#[cfg_attr(feature = "api-types", api(
+    properties: {
+        "max-schema-version": {
+            type: String,
+            optional: true,
+        },
+    },
+))]
 #[derive(Clone, Deserialize, Debug, Serialize, PartialEq)]
 #[serde(rename_all = "kebab-case", deny_unknown_fields)]
 /// Configuration for the post-installation hook, which runs after an
@@ -328,6 +375,15 @@ pub struct PostNotificationHookInfo {
     #[serde(skip_serializing_if = "Option::is_none")]
     #[cfg_attr(feature = "legacy", serde(alias = "auth_token"))]
     pub auth_token: Option<String>,
+
+    /// Maximum supported schema version by the post-hook target implementation.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub max_schema_version: Option<SchemaVersion>,
+
+    /// If set, create an API token with the given name and POST its name and secret back with the
+    /// hook.
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub api_token_name: Option<String>,
 }
 
 #[cfg_attr(feature = "api-types", api)]
diff --git a/proxmox-installer-types/src/lib.rs b/proxmox-installer-types/src/lib.rs
index 8d5ac4fc..718867be 100644
--- a/proxmox-installer-types/src/lib.rs
+++ b/proxmox-installer-types/src/lib.rs
@@ -179,3 +179,21 @@ impl ProxmoxProduct {
         }
     }
 }
+
+pub(crate) fn parse_version(s: &str) -> Option<(u32, u32)> {
+    s.split_once('.')
+        .and_then(|(maj, min)| Some((maj.parse().ok()?, min.parse().ok()?)))
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::ProductVersion;
+
+    use super::{IsoInfo, parse_version};
+
+    #[test]
+    fn parse_version_works() {
+        assert_eq!(parse_version("42.1"), Some((42, 1)));
+        assert_eq!(parse_version("a.1"), None);
+    }
+}
diff --git a/proxmox-installer-types/src/post_hook.rs b/proxmox-installer-types/src/post_hook.rs
index 77cbef48..03a12bdf 100644
--- a/proxmox-installer-types/src/post_hook.rs
+++ b/proxmox-installer-types/src/post_hook.rs
@@ -8,7 +8,7 @@ use proxmox_schema::api;
 
 use crate::{
     BootType, IsoInfo, ProxmoxProduct, SystemDMI, UdevProperties,
-    answer::{FilesystemType, RebootMode},
+    answer::{FilesystemType, RebootMode, SchemaVersion},
 };
 
 /// Re-export for convenience, since this is public API
@@ -137,7 +137,14 @@ pub struct CpuInfo {
     pub sockets: usize,
 }
 
-#[cfg_attr(feature = "api-types", api)]
+#[cfg_attr(feature = "api-types", api(
+    properties: {
+        version: {
+            type: String,
+        },
+    },
+    additional_properties: true,
+))]
 #[derive(Clone, Serialize, Deserialize, PartialEq)]
 #[serde(rename_all = "kebab-case")]
 /// Metadata of the hook, such as schema version of the document.
@@ -149,7 +156,7 @@ pub struct PostHookInfoSchema {
     /// field.
     /// minor: Incremented when adding functionality in a backwards-compatible matter, e.g.
     /// adding a new field.
-    pub version: String,
+    pub version: SchemaVersion,
 }
 
 #[cfg_attr(feature = "api-types", api(
-- 
2.54.0





^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH proxmox 03/16] installer-types: post-hook: allow additional properties on api schema
  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 ` Christoph Heiss
  2026-07-31 14:35 ` [PATCH proxmox 04/16] installer-types: post-hook: add api-token and cert-fingerprint options Christoph Heiss
                   ` (12 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2026-07-31 14:35 UTC (permalink / raw)
  To: pdm-devel

See the accompanying comment for context.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 proxmox-installer-types/src/post_hook.rs | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/proxmox-installer-types/src/post_hook.rs b/proxmox-installer-types/src/post_hook.rs
index 03a12bdf..828452e6 100644
--- a/proxmox-installer-types/src/post_hook.rs
+++ b/proxmox-installer-types/src/post_hook.rs
@@ -173,6 +173,10 @@ pub struct PostHookInfoSchema {
             items: { type: NetworkInterfaceInfo },
         }
     },
+    // Make it forward-compatible, allowing sending additional properties to older implementations,
+    // making it a bit more graceful in fallback.
+    // Targets can differentiate struct versions based on the $schema object.
+    additional_properties: true,
 ))]
 #[derive(Clone, Serialize, Deserialize, PartialEq)]
 #[serde(rename_all = "kebab-case")]
-- 
2.54.0





^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH proxmox 04/16] installer-types: post-hook: add api-token and cert-fingerprint options
  2026-07-31 14:35 [PATCH proxmox/installer/datacenter-manager 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
                   ` (2 preceding siblings ...)
  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 ` Christoph Heiss
  2026-07-31 14:35 ` [PATCH proxmox 05/16] installer-types: systeminfo: add check for API token creation capability Christoph Heiss
                   ` (11 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2026-07-31 14:35 UTC (permalink / raw)
  To: pdm-devel

Adds two new (optional) fields to `PostHookInfo`; `cert-fingerprint` and
`api-token`.
As well as a corresponding optional field in the answer file for
requesting to create an API token.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 proxmox-installer-types/Cargo.toml       |  1 +
 proxmox-installer-types/debian/control   |  4 +++
 proxmox-installer-types/src/post_hook.rs | 33 ++++++++++++++++++++++--
 3 files changed, 36 insertions(+), 2 deletions(-)

diff --git a/proxmox-installer-types/Cargo.toml b/proxmox-installer-types/Cargo.toml
index ff0dc330..3c64aaa7 100644
--- a/proxmox-installer-types/Cargo.toml
+++ b/proxmox-installer-types/Cargo.toml
@@ -16,6 +16,7 @@ anyhow.workspace = true
 serde = { workspace = true, features = ["derive"] }
 serde_plain.workspace = true
 regex = { workspace = true }
+proxmox-auth-api = { workspace = true, features = ["api-types"] }
 proxmox-network-types.workspace = true
 proxmox-schema = { workspace = true, features = ["api-macro"] }
 proxmox-section-config = { workspace = true, optional = true }
diff --git a/proxmox-installer-types/debian/control b/proxmox-installer-types/debian/control
index a880c18a..0dfd26f0 100644
--- a/proxmox-installer-types/debian/control
+++ b/proxmox-installer-types/debian/control
@@ -7,6 +7,8 @@ Build-Depends-Arch: cargo:native <!nocheck>,
  rustc:native (>= 1.85) <!nocheck>,
  libstd-rust-dev <!nocheck>,
  librust-anyhow-1+default-dev <!nocheck>,
+ librust-proxmox-auth-api-1+api-types-dev (>= 1.0.5-~~) <!nocheck>,
+ librust-proxmox-auth-api-1+default-dev (>= 1.0.5-~~) <!nocheck>,
  librust-proxmox-network-types-1+default-dev (>= 1.0.2-~~) <!nocheck>,
  librust-proxmox-node-status-1+default-dev <!nocheck>,
  librust-proxmox-schema-5+api-macro-dev (>= 5.3.0-~~) <!nocheck>,
@@ -28,6 +30,8 @@ Multi-Arch: same
 Depends:
  ${misc:Depends},
  librust-anyhow-1+default-dev,
+ librust-proxmox-auth-api-1+api-types-dev (>= 1.0.5-~~),
+ librust-proxmox-auth-api-1+default-dev (>= 1.0.5-~~),
  librust-proxmox-network-types-1+default-dev (>= 1.0.2-~~),
  librust-proxmox-node-status-1+default-dev,
  librust-proxmox-schema-5+api-macro-dev (>= 5.3.0-~~),
diff --git a/proxmox-installer-types/src/post_hook.rs b/proxmox-installer-types/src/post_hook.rs
index 828452e6..bb4f8d9a 100644
--- a/proxmox-installer-types/src/post_hook.rs
+++ b/proxmox-installer-types/src/post_hook.rs
@@ -2,9 +2,10 @@
 
 use serde::{Deserialize, Serialize};
 
+use proxmox_auth_api::types::Authid;
 use proxmox_network_types::ip_address::Cidr;
 #[cfg(feature = "api-types")]
-use proxmox_schema::api;
+use proxmox_schema::{api, api_types::FINGERPRINT_SHA256_FORMAT};
 
 use crate::{
     BootType, IsoInfo, ProxmoxProduct, SystemDMI, UdevProperties,
@@ -119,6 +120,18 @@ pub struct ProductInfo {
     pub version: String,
 }
 
+#[cfg_attr(feature = "api-types", api)]
+#[derive(Clone, Serialize, Deserialize, PartialEq)]
+#[serde(rename_all = "kebab-case")]
+/// Information about the API token created after the installation, if requested.
+/// Available since schema 1.3.
+pub struct CreatedApiToken {
+    /// The generated API token added to the new installation.
+    pub id: Authid,
+    /// The generated API token secret added to the new installation.
+    pub secret: String,
+}
+
 #[cfg_attr(feature = "api-types", api)]
 #[derive(Clone, Serialize, Deserialize, PartialEq)]
 /// Information about the CPU(s) installed in the system
@@ -171,7 +184,15 @@ pub struct PostHookInfoSchema {
         "network-interfaces": {
             type: Array,
             items: { type: NetworkInterfaceInfo },
-        }
+        },
+        "cert-fingerprint": {
+            type: String,
+            format: &FINGERPRINT_SHA256_FORMAT,
+            optional: true,
+        },
+        "api-token": {
+            optional: true,
+        },
     },
     // Make it forward-compatible, allowing sending additional properties to older implementations,
     // making it a bit more graceful in fallback.
@@ -219,6 +240,14 @@ pub struct PostHookInfo {
     pub ssh_public_host_keys: SshPublicHostKeys,
     /// Action to will be performed, i.e. either reboot or power off the machine.
     pub reboot_mode: RebootMode,
+    /// Certificate fingerprint.
+    /// New with schema 1.3
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub cert_fingerprint: Option<String>,
+    /// If it was requested, contains the information about the created API token.
+    /// New with schema 1.3
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub api_token: Option<CreatedApiToken>,
 }
 
 fn bool_is_false(value: &bool) -> bool {
-- 
2.54.0





^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH proxmox 05/16] installer-types: systeminfo: add check for API token creation capability
  2026-07-31 14:35 [PATCH proxmox/installer/datacenter-manager 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
                   ` (3 preceding siblings ...)
  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 ` Christoph Heiss
  2026-07-31 14:35 ` [PATCH installer 06/16] chroot: print full error if bind-mounting fails Christoph Heiss
                   ` (10 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2026-07-31 14:35 UTC (permalink / raw)
  To: pdm-devel

Only newer installers support creating API tokens on (supported)
products, i.e. currently PVE and PBS only, for usage with PDM.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 proxmox-installer-types/src/lib.rs | 73 +++++++++++++++++++++++++++++-
 1 file changed, 72 insertions(+), 1 deletion(-)

diff --git a/proxmox-installer-types/src/lib.rs b/proxmox-installer-types/src/lib.rs
index 718867be..b04de27e 100644
--- a/proxmox-installer-types/src/lib.rs
+++ b/proxmox-installer-types/src/lib.rs
@@ -14,7 +14,10 @@ pub mod post_hook;
 use proxmox_schema::api;
 
 use serde::{Deserialize, Serialize};
-use std::collections::{BTreeMap, HashMap};
+use std::{
+    cmp::Ordering,
+    collections::{BTreeMap, HashMap},
+};
 
 use proxmox_network_types::mac_address::MacAddress;
 
@@ -67,6 +70,17 @@ pub struct SystemInfo {
     pub network_interfaces: Vec<NetworkInterface>,
 }
 
+impl SystemInfo {
+    /// Determines whether the product supports creating an API token during installation.
+    pub fn supports_api_token_creation(&self) -> bool {
+        match self.product.product {
+            ProxmoxProduct::Pve => self.iso.product_version() >= ProductVersion(9, 2, 2),
+            ProxmoxProduct::Pbs => self.iso.product_version() >= ProductVersion(4, 2, 2),
+            _ => false,
+        }
+    }
+}
+
 #[cfg_attr(feature = "api-types", api)]
 #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
 /// The per-product configuration of the installer.
@@ -108,6 +122,19 @@ impl IsoInfo {
             isorelease: String::from("mocked-1"),
         }
     }
+
+    /// Returns the product version contained in the ISO.
+    pub fn product_version(&self) -> ProductVersion {
+        let rel = self
+            .isorelease
+            .parse()
+            // some ALPHA-X, BETA-X release, treat them as 0
+            .unwrap_or(0);
+
+        parse_version(&self.release)
+            .map(|(maj, min)| ProductVersion(maj, min, rel))
+            .unwrap_or_else(|| ProductVersion(0, 0, 0))
+    }
 }
 
 #[cfg_attr(feature = "api-types", api(
@@ -178,6 +205,29 @@ impl ProxmoxProduct {
             Self::Pdm => "Proxmox Datacenter Manager",
         }
     }
+
+    /// Determines whether the product supports creating an API token during installation.
+    pub fn supports_api_token_creation(&self) -> bool {
+        matches!(self, ProxmoxProduct::Pve | ProxmoxProduct::Pbs)
+    }
+}
+
+/// Represents a (major, minor, patch) product version tuple.
+#[derive(Debug, PartialEq, Eq)]
+pub struct ProductVersion(pub u32, pub u32, pub u32);
+
+impl PartialOrd for ProductVersion {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        match (
+            self.0.cmp(&other.0),
+            self.1.cmp(&other.1),
+            self.2.cmp(&other.2),
+        ) {
+            (Ordering::Equal, Ordering::Equal, ord) => Some(ord),
+            (Ordering::Equal, ord, _) => Some(ord),
+            (ord, _, _) => Some(ord),
+        }
+    }
 }
 
 pub(crate) fn parse_version(s: &str) -> Option<(u32, u32)> {
@@ -196,4 +246,25 @@ mod tests {
         assert_eq!(parse_version("42.1"), Some((42, 1)));
         assert_eq!(parse_version("a.1"), None);
     }
+
+    #[test]
+    fn iso_is_version_at_least_works() {
+        let iso = IsoInfo {
+            release: "9.1".into(),
+            isorelease: "1".into(),
+        };
+
+        assert!(iso.product_version() >= ProductVersion(9, 0, 2));
+        assert!(iso.product_version() < ProductVersion(9, 2, 1));
+        assert!(iso.product_version() >= ProductVersion(9, 1, 1));
+        assert!(iso.product_version() < ProductVersion(9, 1, 2));
+
+        let iso = IsoInfo {
+            release: "9.1".into(),
+            isorelease: "ALPHA-1".into(),
+        };
+
+        assert!(iso.product_version() == ProductVersion(9, 1, 0));
+        assert!(iso.product_version() < ProductVersion(9, 1, 1));
+    }
 }
-- 
2.54.0





^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH installer 06/16] chroot: print full error if bind-mounting fails
  2026-07-31 14:35 [PATCH proxmox/installer/datacenter-manager 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
                   ` (4 preceding siblings ...)
  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 ` Christoph Heiss
  2026-07-31 14:35 ` [PATCH installer 07/16] post-hook: re-use low-level config retrieval from proxmox-chroot Christoph Heiss
                   ` (9 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2026-07-31 14:35 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 proxmox-chroot/src/main.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/proxmox-chroot/src/main.rs b/proxmox-chroot/src/main.rs
index 5f087bb..c329f1a 100644
--- a/proxmox-chroot/src/main.rs
+++ b/proxmox-chroot/src/main.rs
@@ -144,7 +144,7 @@ fn prepare(args: &CommandPrepareArgs) -> Result<()> {
     }
 
     if let Err(e) = bindmount() {
-        eprintln!("{e}")
+        eprintln!("{e:#}")
     }
 
     println!("Done. You can now use 'chroot /target /bin/bash'!");
-- 
2.54.0





^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH installer 07/16] post-hook: re-use low-level config retrieval from proxmox-chroot
  2026-07-31 14:35 [PATCH proxmox/installer/datacenter-manager 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
                   ` (5 preceding siblings ...)
  2026-07-31 14:35 ` [PATCH installer 06/16] chroot: print full error if bind-mounting fails Christoph Heiss
@ 2026-07-31 14:35 ` Christoph Heiss
  2026-07-31 14:35 ` [PATCH installer 08/16] post-hook: generate and retrieve node certificate fingerprint Christoph Heiss
                   ` (8 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2026-07-31 14:35 UTC (permalink / raw)
  To: pdm-devel

We load the file in both tools, so simpfify it a bit for the
proxmox-post-hook side.

While at it, add some context to its errors, as they don't include the
path by default, making debugging unnecessary hard.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 proxmox-chroot/src/main.rs            | 14 ++------------
 proxmox-installer-common/src/setup.rs | 12 ++++++++++++
 proxmox-post-hook/src/main.rs         |  3 +--
 3 files changed, 15 insertions(+), 14 deletions(-)

diff --git a/proxmox-chroot/src/main.rs b/proxmox-chroot/src/main.rs
index c329f1a..387bdf4 100644
--- a/proxmox-chroot/src/main.rs
+++ b/proxmox-chroot/src/main.rs
@@ -13,10 +13,7 @@ use std::{
     process::{self, Command},
 };
 
-use proxmox_installer_common::{
-    RUNTIME_DIR, cli,
-    setup::{InstallConfig, SetupInfo},
-};
+use proxmox_installer_common::{RUNTIME_DIR, cli, setup::SetupInfo};
 use proxmox_installer_types::answer::Filesystem;
 
 const ANSWER_MP: &str = "answer";
@@ -170,7 +167,7 @@ fn cleanup(args: &CommandCleanupArgs) -> Result<()> {
 fn get_fs(filesystem: Option<Filesystem>) -> Result<Filesystem> {
     let fs = match filesystem {
         None => {
-            let low_level_config = match get_low_level_config() {
+            let low_level_config = match proxmox_installer_common::setup::get_low_level_config() {
                 Ok(c) => c,
                 Err(_) => bail!(
                     "Could not fetch config from previous installation. Please specify file system with -f."
@@ -184,13 +181,6 @@ fn get_fs(filesystem: Option<Filesystem>) -> Result<Filesystem> {
     Ok(fs)
 }
 
-fn get_low_level_config() -> Result<InstallConfig> {
-    let file = fs::File::open("/tmp/low-level-config.json")?;
-    let reader = io::BufReader::new(file);
-    let config: InstallConfig = serde_json::from_reader(reader)?;
-    Ok(config)
-}
-
 fn get_iso_info() -> Result<SetupInfo> {
     let path = PathBuf::from(RUNTIME_DIR).join("iso-info.json");
     let reader = io::BufReader::new(fs::File::open(path)?);
diff --git a/proxmox-installer-common/src/setup.rs b/proxmox-installer-common/src/setup.rs
index b8af4e3..a5733bf 100644
--- a/proxmox-installer-common/src/setup.rs
+++ b/proxmox-installer-common/src/setup.rs
@@ -1,3 +1,4 @@
+use anyhow::{Context, Result};
 use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
 use std::{
     cmp,
@@ -469,6 +470,17 @@ pub struct InstallFirstBootSetup {
     pub ordering_target: Option<String>,
 }
 
+pub fn get_low_level_config() -> Result<InstallConfig> {
+    const PATH: &str = "/tmp/low-level-config.json";
+    let file = File::open(PATH).with_context(|| format!("while opening {PATH}"))?;
+
+    let reader = io::BufReader::new(file);
+    let config: InstallConfig =
+        serde_json::from_reader(reader).with_context(|| format!("while parsing {PATH}"))?;
+
+    Ok(config)
+}
+
 pub fn spawn_low_level_installer(test_mode: bool) -> io::Result<process::Child> {
     let (path, args, envs): (&str, &[&str], Vec<(&str, &str)>) = if test_mode {
         (
diff --git a/proxmox-post-hook/src/main.rs b/proxmox-post-hook/src/main.rs
index ec9ab74..d0fb297 100644
--- a/proxmox-post-hook/src/main.rs
+++ b/proxmox-post-hook/src/main.rs
@@ -60,8 +60,7 @@ mod detail {
     pub fn gather(target_path: &str, answer: &AutoInstallerConfig) -> Result<PostHookInfo> {
         println!("Gathering installed system data ...");
 
-        let config: InstallConfig =
-            serde_json::from_reader(BufReader::new(File::open("/tmp/low-level-config.json")?))?;
+        let config = proxmox_installer_common::setup::get_low_level_config()?;
 
         let (setup_info, _, run_env) =
             load_installer_setup_files(proxmox_installer_common::RUNTIME_DIR)
-- 
2.54.0





^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH installer 08/16] post-hook: generate and retrieve node certificate fingerprint
  2026-07-31 14:35 [PATCH proxmox/installer/datacenter-manager 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
                   ` (6 preceding siblings ...)
  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 ` Christoph Heiss
  2026-07-31 14:35 ` [PATCH installer 09/16] post-hook: support creating API token if requested in answer file Christoph Heiss
                   ` (7 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2026-07-31 14:35 UTC (permalink / raw)
  To: pdm-devel

The node certificate is created unconditionally created for all
products, as having the fingerprint is useful in general.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Depends on the `proxmox-installer-types` changes and an accompanying
dependency bump.

 Cargo.toml                          |   2 +
 proxmox-installer-common/Cargo.toml |   4 +-
 proxmox-post-hook/Cargo.toml        |   2 +
 proxmox-post-hook/src/main.rs       | 172 ++++++++++++++++++++++++++--
 4 files changed, 169 insertions(+), 11 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index af0655d..7ba4a94 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,9 +21,11 @@ anyhow = "1.0"
 log = "0.4.20"
 pico-args = "0.5"
 regex = "1.7"
+rustls = "0.23"
 serde = "1.0"
 serde_json = "1.0"
 serde_plain = "1.0"
+sha2 = "0.10"
 toml = "0.8"
 proxmox-auto-installer.path = "./proxmox-auto-installer"
 proxmox-installer-common.path = "./proxmox-installer-common"
diff --git a/proxmox-installer-common/Cargo.toml b/proxmox-installer-common/Cargo.toml
index 7682680..ec4cdb8 100644
--- a/proxmox-installer-common/Cargo.toml
+++ b/proxmox-installer-common/Cargo.toml
@@ -19,9 +19,9 @@ proxmox-installer-types.workspace = true
 # `http` feature
 hex = { version = "0.4", optional = true }
 native-tls = { version = "0.2", optional = true }
-rustls = { version = "0.23", optional = true }
+rustls = { workspace = true, optional = true }
 rustls-native-certs = { version = "0.6", optional = true }
-sha2 = { version = "0.10", optional = true }
+sha2 = { workspace = true, optional = true }
 ureq = { version = "3", features = [ "platform-verifier"  ], optional = true }
 
 # `cli` feature
diff --git a/proxmox-post-hook/Cargo.toml b/proxmox-post-hook/Cargo.toml
index 748b922..b6d9ce5 100644
--- a/proxmox-post-hook/Cargo.toml
+++ b/proxmox-post-hook/Cargo.toml
@@ -15,6 +15,8 @@ anyhow.workspace = true
 proxmox-installer-common = { workspace = true, features = ["http"] }
 proxmox-network-types.workspace = true
 proxmox-installer-types.workspace = true
+rustls.workspace = true
 serde = { workspace = true, features = ["derive"] }
 serde_json.workspace = true
+sha2.workspace = true
 toml.workspace = true
diff --git a/proxmox-post-hook/src/main.rs b/proxmox-post-hook/src/main.rs
index d0fb297..760ab53 100644
--- a/proxmox-post-hook/src/main.rs
+++ b/proxmox-post-hook/src/main.rs
@@ -17,21 +17,26 @@ use std::{
 };
 
 use proxmox_installer_common::http::{self, header::HeaderMap};
-use proxmox_installer_types::answer::{AutoInstallerConfig, PostNotificationHookInfo};
+use proxmox_installer_types::answer::{
+    AutoInstallerConfig, PostNotificationHookInfo, SchemaVersion,
+};
 
 /// Current version of the schema sent by this implementation.
-const POST_HOOK_SCHEMA_VERSION: &str = "1.2";
+const POST_HOOK_SCHEMA_VERSION: SchemaVersion = SchemaVersion(1, 3);
 
 mod detail {
     use anyhow::{Context, Result, anyhow, bail};
+    use rustls::pki_types::{CertificateDer, pem::PemObject};
+    use sha2::{Digest, Sha256};
     use std::{
         collections::{HashMap, HashSet},
         ffi::CStr,
         fs::{self, File},
-        io::BufReader,
-        os::unix::fs::FileExt,
-        path::PathBuf,
-        process::Command,
+        io::{BufReader, ErrorKind},
+        os::unix::fs::{FileExt, MetadataExt},
+        path::{Path, PathBuf},
+        process::{Command, Stdio},
+        time::Duration,
     };
 
     use proxmox_installer_common::{
@@ -40,10 +45,12 @@ mod detail {
     };
     use proxmox_installer_types::{
         ProxmoxProduct, SystemDMI, UdevInfo,
-        answer::{AutoInstallerConfig, FqdnConfig, FqdnFromDhcpConfig, FqdnSourceMode},
+        answer::{
+            AutoInstallerConfig, FqdnConfig, FqdnFromDhcpConfig, FqdnSourceMode, SchemaVersion,
+        },
         post_hook::{
             BootInfo, CpuInfo, DiskInfo, KernelVersionInformation, NetworkInterfaceInfo,
-            PostHookInfo, PostHookInfoSchema, ProductInfo, SshPublicHostKeys,
+            PostHookInfo, PostHookInfoSchema, ProductInfo, SchemaVersion, SshPublicHostKeys,
         },
     };
 
@@ -93,8 +100,15 @@ mod detail {
                 .arg(target_path)
                 .args(cmd)
                 .output()
+                .map_err(|err| anyhow!(err))
+                .and_then(|r| {
+                    if r.status.success() {
+                        Ok(String::from_utf8(r.stdout)?)
+                    } else {
+                        Err(anyhow!("{}", String::from_utf8(r.stderr)?))
+                    }
+                })
                 .with_context(|| format!("failed to run '{cmd:?}'"))
-                .and_then(|r| Ok(String::from_utf8(r.stdout)?))
         };
 
         let fqdn = match &answer.global.fqdn {
@@ -110,6 +124,30 @@ mod detail {
             .to_string(),
         };
 
+        let schema_1_3_supported = answer
+            .post_installation_webhook
+            .as_ref()
+            .and_then(|p| p.max_schema_version)
+            .map(|v| v >= SchemaVersion(1, 3))
+            .unwrap_or_default();
+
+        let cert_fingerprint = if schema_1_3_supported {
+            match gather_node_cert_fingerprint(
+                target_path,
+                setup_info.config.product,
+                &open_file,
+                &run_cmd,
+            ) {
+                Ok(fp) => fp,
+                Err(err) => {
+                    eprintln!("could not retrieve node certificate: {err:#}");
+                    None
+                }
+            }
+        } else {
+            None
+        };
+
         Ok(PostHookInfo {
             schema: PostHookInfoSchema {
                 version: super::POST_HOOK_SCHEMA_VERSION.to_owned(),
@@ -181,6 +219,8 @@ mod detail {
                 }),
             },
             reboot_mode: answer.global.reboot_mode,
+            cert_fingerprint,
+            api_token: None,
         })
     }
 
@@ -568,6 +608,119 @@ mod detail {
         result
     }
 
+    /// First creates the initial node certificate, then computes the fingerprint from the freshly
+    /// initialized proxy certificate.
+    fn gather_node_cert_fingerprint(
+        target_path: &str,
+        product: ProxmoxProduct,
+        open_file: &dyn Fn(&str) -> Result<File>,
+        run_cmd: &dyn Fn(&[&str]) -> Result<String>,
+    ) -> Result<Option<String>> {
+        println!("Generating node certificates ..");
+
+        match product {
+            ProxmoxProduct::Pve => with_pmxcfs(target_path, |_| {
+                run_cmd(&["pvecm", "updatecerts"])
+                    .context("failed to generate node certificates")?;
+                retrieve_cert_fingerprint(open_file("/etc/pve/local/pve-ssl.pem")?).map(Some)
+            }),
+            ProxmoxProduct::Pbs => {
+                run_cmd(&["proxmox-backup-manager", "cert", "update"])
+                    .context("failed to generate node certificates")?;
+                retrieve_cert_fingerprint(open_file("/etc/proxmox-backup/proxy.pem")?).map(Some)
+            }
+            ProxmoxProduct::Pmg => {
+                run_cmd(&["pmgconfig", "apicert"])
+                    .context("failed to generate node certificates")?;
+                retrieve_cert_fingerprint(open_file("/etc/pmg/pmg-api.pem")?).map(Some)
+            }
+            _ => Ok(None),
+        }
+    }
+
+    fn is_path_a_mountpoint(path: impl AsRef<Path>) -> Result<bool> {
+        let path = path.as_ref();
+
+        let parent = match path.parent() {
+            Some(p) => p,
+            None => return Ok(true), // / is always a mount
+        };
+
+        let dev = match fs::metadata(path).map(|md| md.dev()) {
+            Ok(dev) => dev,
+            Err(err) if err.kind() == ErrorKind::NotFound => return Ok(false),
+            Err(err) => return Err(err).with_context(|| format!("stat {}", path.display())),
+        };
+
+        let parent_dev = match fs::metadata(parent).map(|md| md.dev()) {
+            Ok(dev) => dev,
+            Err(err) if err.kind() == ErrorKind::NotFound => return Ok(false),
+            Err(err) => return Err(err).with_context(|| format!("stat {}", parent.display())),
+        };
+
+        // If the devices differ, it's a filesystem boundary
+        Ok(dev != parent_dev)
+    }
+
+    fn with_pmxcfs<R, F: FnOnce(&str) -> Result<R>>(target_path: &str, callback: F) -> Result<R> {
+        // Don't consider failing to start pmxcfs fatal, as detail::gather() will handle unavailable
+        // files and commands gracefully.
+
+        let mut pmxcfs = Command::new("chroot")
+            .args([target_path, "/usr/bin/pmxcfs", "--local", "--foreground"])
+            .stdin(Stdio::piped())
+            .spawn()
+            .context("starting pmxcfs")?;
+
+        // Wait until the cluster filesystem is mounted under /etc/pve
+        // Typically takes at least 1-2 seconds, so give it some time beforehand to avoid superfluous
+        // polling
+        std::thread::sleep(Duration::from_secs(1));
+
+        let pmxcfs_path = Path::new(target_path).join("etc/pve");
+        let result = loop {
+            match is_path_a_mountpoint(&pmxcfs_path) {
+                Ok(true) => break callback(target_path),
+                Ok(false) => {
+                    std::thread::sleep(Duration::from_millis(500));
+                }
+                Err(err) => break Err(err),
+            }
+        };
+
+        // TODO: Switch to std::os::unix::process::ChildExt::send_signal() once that becomes stable
+        let kill_cmd = Command::new("/usr/bin/kill")
+            .arg("-TERM")
+            .arg(pmxcfs.id().to_string())
+            .status();
+
+        // gracefully shut down pmxcfs
+        if let Err(err) = kill_cmd {
+            eprintln!("failed to gracefully terminate pmxcfs ({err}), killing ..");
+            let _ = pmxcfs.kill();
+        }
+
+        if let Err(err) = pmxcfs.wait() {
+            eprintln!("failed to wait for pmxcfs to terminate: {err}");
+        }
+
+        result
+    }
+
+    fn retrieve_cert_fingerprint(file: File) -> Result<String> {
+        let reader = CertificateDer::from_pem_reader(file)?;
+
+        let mut hasher = Sha256::new();
+        hasher.update(reader);
+
+        Ok(hasher
+            .finalize()
+            .iter()
+            .fold(String::new(), |acc, b| format!("{acc}:{b:02X}"))
+            .trim_start_matches(':')
+            .to_owned())
+    }
+
     #[cfg(test)]
     mod tests {
         use super::{
@@ -919,6 +1072,7 @@ fn do_main() -> Result<()> {
         url,
         cert_fingerprint,
         auth_token,
+        ..
     }) = &answer.post_installation_webhook
     {
         println!("Found post-installation-webhook; sending POST request to '{url}'.");
-- 
2.54.0





^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH installer 09/16] post-hook: support creating API token if requested in answer file
  2026-07-31 14:35 [PATCH proxmox/installer/datacenter-manager 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
                   ` (7 preceding siblings ...)
  2026-07-31 14:35 ` [PATCH installer 08/16] post-hook: generate and retrieve node certificate fingerprint Christoph Heiss
@ 2026-07-31 14:35 ` Christoph Heiss
  2026-07-31 14:35 ` [PATCH installer 10/16] auto: enforce https for post hook when generating an API token Christoph Heiss
                   ` (6 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2026-07-31 14:35 UTC (permalink / raw)
  To: pdm-devel

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 <c.heiss@proxmox.com>
---
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<String>,
+    ) -> Result<CreatedApiToken> {
+        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::<serde_json::Value>(&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::<serde_json::Value>(
+                        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<Path>) -> Result<bool> {
         let path = path.as_ref();
 
-- 
2.54.0





^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH installer 10/16] auto: enforce https for post hook when generating an API token
  2026-07-31 14:35 [PATCH proxmox/installer/datacenter-manager 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
                   ` (8 preceding siblings ...)
  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 ` Christoph Heiss
  2026-07-31 14:35 ` [PATCH installer 11/16] assistant: validate-answer: also verify post-hook settings if set Christoph Heiss
                   ` (5 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2026-07-31 14:35 UTC (permalink / raw)
  To: pdm-devel

As we include the token secret in the post-hook information, and (in
this particular case) the token is scoped on root@pam, enforce a minimum
level of security.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Depends on the `proxmox-installer-types` changes and an accompanying
dependency bump.

 proxmox-auto-installer/src/utils.rs | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/proxmox-auto-installer/src/utils.rs b/proxmox-auto-installer/src/utils.rs
index 710af21..7762738 100644
--- a/proxmox-auto-installer/src/utils.rs
+++ b/proxmox-auto-installer/src/utils.rs
@@ -11,7 +11,7 @@ use proxmox_installer_types::{
     answer::{
         AutoInstallerConfig, DiskSelection, Filesystem, FilesystemOptions, FilesystemType,
         FilterMatch, FirstBootHookSourceMode, FqdnConfig, FqdnFromDhcpConfig, FqdnSourceMode,
-        NetworkConfig,
+        NetworkConfig, PostNotificationHookInfo,
     },
 };
 
@@ -496,6 +496,16 @@ pub fn verify_network_settings(
     Ok(())
 }
 
+pub fn verify_post_hook_settings(hook: &PostNotificationHookInfo) -> Result<()> {
+    info!("Verifying post-installation hook settings");
+
+    if hook.api_token_name.is_some() && !hook.url.starts_with("https://") {
+        bail!("`post-installation-webhook.api-token-name` can only be used with HTTPS");
+    }
+
+    Ok(())
+}
+
 pub fn parse_answer(
     answer: &AutoInstallerConfig,
     udev_info: &UdevInfo,
@@ -518,6 +528,10 @@ pub fn parse_answer(
     verify_first_boot_settings(answer)?;
     verify_network_settings(&answer.network, Some(runtime_info))?;
 
+    if let Some(info) = &answer.post_installation_webhook {
+        verify_post_hook_settings(info)?;
+    }
+
     let root_password = match (
         &answer.global.root_password,
         &answer.global.root_password_hashed,
-- 
2.54.0





^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH installer 11/16] assistant: validate-answer: also verify post-hook settings if set
  2026-07-31 14:35 [PATCH proxmox/installer/datacenter-manager 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
                   ` (9 preceding siblings ...)
  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 ` Christoph Heiss
  2026-07-31 14:35 ` [PATCH datacenter-manager 12/16] ui: auto-installer: spell out Proxmox Datacenter Manager Christoph Heiss
                   ` (4 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2026-07-31 14:35 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Depends on the `proxmox-installer-types` changes and an accompanying
dependency bump.

 proxmox-auto-install-assistant/src/main.rs | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/proxmox-auto-install-assistant/src/main.rs b/proxmox-auto-install-assistant/src/main.rs
index 730d7d8..41a17d6 100644
--- a/proxmox-auto-install-assistant/src/main.rs
+++ b/proxmox-auto-install-assistant/src/main.rs
@@ -23,7 +23,7 @@ use proxmox_auto_installer::{
         AutoInstSettings, FetchAnswerFrom, HttpOptions, default_partition_label,
         get_matched_udev_indexes, get_nic_list, get_single_udev_index, verify_disks_settings,
         verify_email_and_root_password_settings, verify_first_boot_settings,
-        verify_locale_settings, verify_network_settings,
+        verify_locale_settings, verify_network_settings, verify_post_hook_settings,
     },
 };
 use proxmox_installer_common::{FIRST_BOOT_EXEC_MAX_SIZE, FIRST_BOOT_EXEC_NAME, cli};
@@ -1596,6 +1596,11 @@ fn parse_answer(path: impl AsRef<Path> + fmt::Debug) -> Result<AutoInstallerConf
             verify_first_boot_settings(&answer)?;
             verify_email_and_root_password_settings(&answer)?;
             verify_network_settings(&answer.network, None)?;
+
+            if let Some(info) = &answer.post_installation_webhook {
+                verify_post_hook_settings(info)?;
+            }
+
             Ok(answer)
         }
         Err(err) => bail!("Error parsing answer file: {err}"),
-- 
2.54.0





^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH datacenter-manager 12/16] ui: auto-installer: spell out Proxmox Datacenter Manager
  2026-07-31 14:35 [PATCH proxmox/installer/datacenter-manager 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
                   ` (10 preceding siblings ...)
  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 ` 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
                   ` (3 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2026-07-31 14:35 UTC (permalink / raw)
  To: pdm-devel

Better fits our brand guidelines.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 ui/src/remotes/auto_installer/prepared_answer_form.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ui/src/remotes/auto_installer/prepared_answer_form.rs b/ui/src/remotes/auto_installer/prepared_answer_form.rs
index 960decae..6ee4b15a 100644
--- a/ui/src/remotes/auto_installer/prepared_answer_form.rs
+++ b/ui/src/remotes/auto_installer/prepared_answer_form.rs
@@ -973,7 +973,7 @@ pub fn render_auth_form(
             Field::new()
                 .name("post-hook-base-url")
                 .tip(tr!(
-                    "Base URL this PDM instance is reachable from the target host"
+                    "Base URL this Proxmox Datacenter Manager instance is reachable from the target host."
                 ))
                 .placeholder(pdm_origin())
                 .value(config.post_hook_base_url.clone()),
-- 
2.54.0





^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH datacenter-manager 13/16] config: auto-install: add optional `post-hook-add-as-remote` field
  2026-07-31 14:35 [PATCH proxmox/installer/datacenter-manager 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
                   ` (11 preceding siblings ...)
  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 ` Christoph Heiss
  2026-07-31 14:35 ` [PATCH datacenter-manager 14/16] api: auto-installer: add option for adding new remotes to PDM Christoph Heiss
                   ` (2 subsequent siblings)
  15 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2026-07-31 14:35 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 lib/pdm-config/src/auto_install.rs | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/lib/pdm-config/src/auto_install.rs b/lib/pdm-config/src/auto_install.rs
index 8ac48fb6..68bc63a0 100644
--- a/lib/pdm-config/src/auto_install.rs
+++ b/lib/pdm-config/src/auto_install.rs
@@ -149,6 +149,9 @@ pub mod types {
                 schema: CERT_FINGERPRINT_SHA256_SCHEMA,
                 optional: true,
             },
+            "post-hook-add-as-remote": {
+                optional: true,
+            },
             "template-counters": {
                 type: String,
                 optional: true,
@@ -280,6 +283,11 @@ pub mod types {
         /// Post hook certificate fingerprint, if needed.
         #[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, skip_serializing_if = "bool_is_false")]
+        pub post_hook_add_as_remote: bool,
 
         /// Key-value pairs of (auto-incrementing) counters.
         #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
@@ -328,6 +336,7 @@ pub mod types {
                 // post hook
                 post_hook_base_url: conf.post_hook_base_url,
                 post_hook_cert_fp: conf.post_hook_cert_fp,
+                post_hook_add_as_remote: conf.post_hook_add_as_remote,
                 // templating
                 template_counters: PropertyString::new(BTreeMapWrapper(conf.template_counters)),
                 // subscription
@@ -381,6 +390,7 @@ pub mod types {
                 // post hook
                 post_hook_base_url: self.post_hook_base_url,
                 post_hook_cert_fp: self.post_hook_cert_fp,
+                post_hook_add_as_remote: self.post_hook_add_as_remote,
                 // templating
                 template_counters: self.template_counters.into_inner().0,
                 // subscription
@@ -427,6 +437,10 @@ pub mod types {
             AnswerTokenWrapper::Token(value)
         }
     }
+
+    fn bool_is_false(b: &bool) -> bool {
+        !b
+    }
 }
 
 pub fn installations_read_lock() -> Result<ApiLockGuard> {
-- 
2.54.0





^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH datacenter-manager 14/16] api: auto-installer: add option for adding new remotes to PDM
  2026-07-31 14:35 [PATCH proxmox/installer/datacenter-manager 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
                   ` (12 preceding siblings ...)
  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
  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
  15 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2026-07-31 14:35 UTC (permalink / raw)
  To: pdm-devel

.. 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





^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH datacenter-manager 15/16] ui: auto-installer: wizard: add checkbox to add target as new remote
  2026-07-31 14:35 [PATCH proxmox/installer/datacenter-manager 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
                   ` (13 preceding siblings ...)
  2026-07-31 14:35 ` [PATCH datacenter-manager 14/16] api: auto-installer: add option for adding new remotes to PDM Christoph Heiss
@ 2026-07-31 14:35 ` Christoph Heiss
  2026-07-31 14:35 ` [PATCH datacenter-manager 16/16] docs: auto-installer: document adding targets as remotes afterwards Christoph Heiss
  15 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2026-07-31 14:35 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 .../remotes/auto_installer/prepared_answer_add_wizard.rs   | 1 +
 ui/src/remotes/auto_installer/prepared_answer_form.rs      | 7 +++++++
 2 files changed, 8 insertions(+)

diff --git a/ui/src/remotes/auto_installer/prepared_answer_add_wizard.rs b/ui/src/remotes/auto_installer/prepared_answer_add_wizard.rs
index 03d78be8..a4ea66e0 100644
--- a/ui/src/remotes/auto_installer/prepared_answer_add_wizard.rs
+++ b/ui/src/remotes/auto_installer/prepared_answer_add_wizard.rs
@@ -81,6 +81,7 @@ impl AddAnswerWizardProperties {
             // post hook
             post_hook_base_url: pdm_origin(),
             post_hook_cert_fp: fingerprint,
+            post_hook_add_as_remote: true,
             // templating
             template_counters,
             // subscription
diff --git a/ui/src/remotes/auto_installer/prepared_answer_form.rs b/ui/src/remotes/auto_installer/prepared_answer_form.rs
index 6ee4b15a..e6eace8f 100644
--- a/ui/src/remotes/auto_installer/prepared_answer_form.rs
+++ b/ui/src/remotes/auto_installer/prepared_answer_form.rs
@@ -985,6 +985,13 @@ pub fn render_auth_form(
                 .tip(tr!("Optional certificate fingerprint"))
                 .value(config.post_hook_cert_fp.clone()),
         )
+        .with_field(
+            tr!("Provision As New Remote"),
+            Checkbox::new()
+                .name("post-hook-add-as-remote")
+                .tip(tr!("Only available for Virtual Environment and Backup Server. Add the target host as new remote to this Datacenter Manager instance."))
+                .default(config.post_hook_add_as_remote),
+        )
         .with_large_custom_child(
             Container::from_tag("p")
                 .key("post-hook-hint")
-- 
2.54.0





^ permalink raw reply related	[flat|nested] 17+ messages in thread

* [PATCH datacenter-manager 16/16] docs: auto-installer: document adding targets as remotes afterwards
  2026-07-31 14:35 [PATCH proxmox/installer/datacenter-manager 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
                   ` (14 preceding siblings ...)
  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 ` Christoph Heiss
  15 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2026-07-31 14:35 UTC (permalink / raw)
  To: pdm-devel

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Re. compatibility section, I've listed all products with their next
minor version as compatible with this feature in prospect, since they
should all receive that support with each respective next release, if
all pve-installer parts are comitted beforehand.

I will also update the wiki page accordingly on time.

 docs/automated-installations.rst | 33 ++++++++++++++++++++++++++++++++
 1 file changed, 33 insertions(+)

diff --git a/docs/automated-installations.rst b/docs/automated-installations.rst
index ede613dc..30feeb65 100644
--- a/docs/automated-installations.rst
+++ b/docs/automated-installations.rst
@@ -121,6 +121,39 @@ The automated installer integration uses a dedicated token mechanism, separate
 from the normal API tokens. See the example under :ref:`autoinst_preparing_iso`
 on how to include such a token in the ISO when preparing it.
 
+.. _autoinst_provision_as_remote:
+
+Provisioning targets as new remotes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The automated installer supports adding target systems to Proxmox Datacenter
+Manager as new remotes after successful installations.
+
+To enable this functionality, enable the **Provision As New Remote** checkbox
+in the *Authentication* tab when creating new prepared answer or editing
+existing ones.
+
+.. _autoinst_compatibility:
+
+Compatibility
+~~~~~~~~~~~~~
+
+Using Proxmox Datacenter Manager for automated installations is supported for
+the following product ISOs:
+
+* **Proxmox Virtual Environment 9.2-1** or higher
+* **Proxmox Backup Server 4.3-1** or higher
+* **Proxmox Mail Gateway 9.1-1** or higher
+* **Proxmox Datacenter Manager 1.1-1** or higher
+
+:ref:`_autoinst_provision_as_remote` is supported for the following product
+ISOs:
+
+* **Proxmox Virtual Environment 9.3-1** or higher
+* **Proxmox Backup Server 4.3-1** or higher
+* **Proxmox Mail Gateway 9.2-1** or higher
+* **Proxmox Datacenter Manager 1.2-1** or higher
+
 .. _autoinst_preparing_iso:
 
 Preparing an ISO
-- 
2.54.0





^ permalink raw reply related	[flat|nested] 17+ messages in thread

end of thread, other threads:[~2026-07-31 14:40 UTC | newest]

Thread overview: 17+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH datacenter-manager 14/16] api: auto-installer: add option for adding new remotes to PDM Christoph Heiss
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

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