* [PATCH datacenter-manager/installer/proxmox v2 00/16] auto-installer: add installed target systems as new remotes
@ 2026-08-26 11:04 Christoph Heiss
2026-08-26 11:04 ` [PATCH proxmox v2 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-08-26 11:04 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
History
=======
v1: https://lore.proxmox.com/pdm-devel/20260731143910.936881-1-c.heiss@proxmox.com/
Notable changes v1 -> v2:
* rebased all on latest masters
* addressed review comments from Lukas
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 | 65 +++++++++++++++--
proxmox-installer-types/src/lib.rs | 89 +++++++++++++++++++++++-
proxmox-installer-types/src/post_hook.rs | 50 +++++++++++--
5 files changed, 196 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 | 291 ++++++++++++++++++++-
9 files changed, 327 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 | 39 ++++-
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, 205 insertions(+), 25 deletions(-)
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH proxmox v2 01/16] installer-types: drop unnecessary clippy attribute
2026-08-26 11:04 [PATCH datacenter-manager/installer/proxmox v2 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
@ 2026-08-26 11:04 ` Christoph Heiss
2026-08-26 11:04 ` [PATCH proxmox v2 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-08-26 11:04 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>
---
Changes v1 -> v2:
* no changes
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.55.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH proxmox v2 02/16] installer-types: post-hook: factor schema version into proper struct
2026-08-26 11:04 [PATCH datacenter-manager/installer/proxmox v2 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
2026-08-26 11:04 ` [PATCH proxmox v2 01/16] installer-types: drop unnecessary clippy attribute Christoph Heiss
@ 2026-08-26 11:04 ` Christoph Heiss
2026-08-26 11:04 ` [PATCH proxmox v2 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-08-26 11:04 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>
---
Changes v1 -> v2:
* chain .cmp() calls instead of manually `match`ing on them
proxmox-installer-types/src/answer.rs | 65 +++++++++++++++++++++---
proxmox-installer-types/src/lib.rs | 18 +++++++
proxmox-installer-types/src/post_hook.rs | 13 +++--
3 files changed, 87 insertions(+), 9 deletions(-)
diff --git a/proxmox-installer-types/src/answer.rs b/proxmox-installer-types/src/answer.rs
index e2e9b8e4..d6a99ba3 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,35 @@ 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> {
+ Some(self.0.cmp(&other.0).then(self.1.cmp(&other.1)))
+ }
+}
+
+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 +101,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 +122,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,
}
}
}
@@ -335,7 +372,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
@@ -352,6 +396,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.55.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH proxmox v2 03/16] installer-types: post-hook: allow additional properties on api schema
2026-08-26 11:04 [PATCH datacenter-manager/installer/proxmox v2 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
2026-08-26 11:04 ` [PATCH proxmox v2 01/16] installer-types: drop unnecessary clippy attribute Christoph Heiss
2026-08-26 11:04 ` [PATCH proxmox v2 02/16] installer-types: post-hook: factor schema version into proper struct Christoph Heiss
@ 2026-08-26 11:04 ` Christoph Heiss
2026-08-26 11:04 ` [PATCH proxmox v2 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-08-26 11:04 UTC (permalink / raw)
To: pdm-devel
See the accompanying comment for context.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Changes v1 -> v2:
* no changes
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.55.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH proxmox v2 04/16] installer-types: post-hook: add api-token and cert-fingerprint options
2026-08-26 11:04 [PATCH datacenter-manager/installer/proxmox v2 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
` (2 preceding siblings ...)
2026-08-26 11:04 ` [PATCH proxmox v2 03/16] installer-types: post-hook: allow additional properties on api schema Christoph Heiss
@ 2026-08-26 11:04 ` Christoph Heiss
2026-08-26 11:04 ` [PATCH proxmox v2 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-08-26 11:04 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>
---
Changes v1 -> v2:
* no changes
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 45f436e9..43200c74 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 e6f2ad6a..fc917dfa 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.55.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH proxmox v2 05/16] installer-types: systeminfo: add check for API token creation capability
2026-08-26 11:04 [PATCH datacenter-manager/installer/proxmox v2 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
` (3 preceding siblings ...)
2026-08-26 11:04 ` [PATCH proxmox v2 04/16] installer-types: post-hook: add api-token and cert-fingerprint options Christoph Heiss
@ 2026-08-26 11:04 ` Christoph Heiss
2026-08-26 11:04 ` [PATCH installer v2 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-08-26 11:04 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>
---
Changes v1 -> v2:
* chain .cmp() calls instead of manually `match`ing on them
proxmox-installer-types/src/lib.rs | 70 +++++++++++++++++++++++++++++-
1 file changed, 69 insertions(+), 1 deletion(-)
diff --git a/proxmox-installer-types/src/lib.rs b/proxmox-installer-types/src/lib.rs
index 718867be..0ecc5d70 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,26 @@ 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> {
+ Some(
+ self.0
+ .cmp(&other.0)
+ .then(self.1.cmp(&other.1))
+ .then(self.2.cmp(&other.2)),
+ )
+ }
}
pub(crate) fn parse_version(s: &str) -> Option<(u32, u32)> {
@@ -196,4 +243,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.55.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH installer v2 06/16] chroot: print full error if bind-mounting fails
2026-08-26 11:04 [PATCH datacenter-manager/installer/proxmox v2 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
` (4 preceding siblings ...)
2026-08-26 11:04 ` [PATCH proxmox v2 05/16] installer-types: systeminfo: add check for API token creation capability Christoph Heiss
@ 2026-08-26 11:04 ` Christoph Heiss
2026-08-26 11:04 ` [PATCH installer v2 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-08-26 11:04 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Changes v1 -> v2:
* no changes
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.55.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH installer v2 07/16] post-hook: re-use low-level config retrieval from proxmox-chroot
2026-08-26 11:04 [PATCH datacenter-manager/installer/proxmox v2 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
` (5 preceding siblings ...)
2026-08-26 11:04 ` [PATCH installer v2 06/16] chroot: print full error if bind-mounting fails Christoph Heiss
@ 2026-08-26 11:04 ` Christoph Heiss
2026-08-26 11:04 ` [PATCH installer v2 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-08-26 11:04 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>
---
Changes v1 -> v2:
* no changes
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 749fd0e..7d73306 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.55.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH installer v2 08/16] post-hook: generate and retrieve node certificate fingerprint
2026-08-26 11:04 [PATCH datacenter-manager/installer/proxmox v2 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
` (6 preceding siblings ...)
2026-08-26 11:04 ` [PATCH installer v2 07/16] post-hook: re-use low-level config retrieval from proxmox-chroot Christoph Heiss
@ 2026-08-26 11:04 ` Christoph Heiss
2026-08-26 11:04 ` [PATCH installer v2 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-08-26 11:04 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.
Changes v1 -> v2:
* rename gather_node_cert_fingerprint() -> generate_node_certificate()
* use Result::inspect_err() for simplifying error handling
Cargo.toml | 2 +
proxmox-installer-common/Cargo.toml | 4 +-
proxmox-post-hook/Cargo.toml | 2 +
proxmox-post-hook/src/main.rs | 167 ++++++++++++++++++++++++++--
4 files changed, 164 insertions(+), 11 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index c33219c..a134ffb 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 7d73306..e4f1fec 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.as_ref() {
@@ -110,6 +124,22 @@ 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 {
+ generate_node_certificate(target_path, setup_info.config.product, &open_file, &run_cmd)
+ .inspect_err(|err| eprintln!("could not retrieve node certificate: {err:#}"))
+ .ok()
+ .flatten()
+ } else {
+ None
+ };
+
Ok(PostHookInfo {
schema: PostHookInfoSchema {
version: super::POST_HOOK_SCHEMA_VERSION.to_owned(),
@@ -181,6 +211,8 @@ mod detail {
}),
},
reboot_mode: answer.global.reboot_mode,
+ cert_fingerprint,
+ api_token: None,
})
}
@@ -568,6 +600,122 @@ mod detail {
result
}
+ /// Creates the initial node certificate for the API/web proxy.
+ ///
+ /// # Returns
+ ///
+ /// The fingerprint of the newly generated certificate.
+ fn generate_node_certificate(
+ 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 +1067,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.55.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH installer v2 09/16] post-hook: support creating API token if requested in answer file
2026-08-26 11:04 [PATCH datacenter-manager/installer/proxmox v2 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
` (7 preceding siblings ...)
2026-08-26 11:04 ` [PATCH installer v2 08/16] post-hook: generate and retrieve node certificate fingerprint Christoph Heiss
@ 2026-08-26 11:04 ` Christoph Heiss
2026-08-26 11:04 ` [PATCH installer v2 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-08-26 11:04 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.
Changes v1 -> v2:
* document create_api_token()
* use Result::inspect_err() for simplifying error handling
Cargo.toml | 4 +-
debian/control | 1 +
proxmox-post-hook/Cargo.toml | 3 +-
proxmox-post-hook/src/main.rs | 125 +++++++++++++++++++++++++++++++++-
4 files changed, 128 insertions(+), 5 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index a134ffb..55d2192 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 e4f1fec..6455838 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,
},
};
@@ -140,6 +140,20 @@ 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()
+ {
+ create_api_token(name, target_path, setup_info.config.product, &run_cmd)
+ .inspect_err(|err| eprintln!("could not create API token: {err:#}"))
+ .ok()
+ } else {
+ None
+ };
+
Ok(PostHookInfo {
schema: PostHookInfoSchema {
version: super::POST_HOOK_SCHEMA_VERSION.to_owned(),
@@ -212,7 +226,7 @@ mod detail {
},
reboot_mode: answer.global.reboot_mode,
cert_fingerprint,
- api_token: None,
+ api_token,
})
}
@@ -633,6 +647,111 @@ mod detail {
}
}
+ /// Creates a new, fully privileged API token with the given `name` for the target system.
+ ///
+ /// The token is set to never expire, and the comment will include that it was created during
+ /// installation, as well as the creation time.
+ ///
+ /// # Returns
+ ///
+ /// The ID and secret of the created API token.
+ 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.55.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH installer v2 10/16] auto: enforce https for post hook when generating an API token
2026-08-26 11:04 [PATCH datacenter-manager/installer/proxmox v2 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
` (8 preceding siblings ...)
2026-08-26 11:04 ` [PATCH installer v2 09/16] post-hook: support creating API token if requested in answer file Christoph Heiss
@ 2026-08-26 11:04 ` Christoph Heiss
2026-08-26 11:04 ` [PATCH installer v2 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-08-26 11:04 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.
Changes v1 -> v2:
* no changes
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 7205ae3..03253b7 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.55.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH installer v2 11/16] assistant: validate-answer: also verify post-hook settings if set
2026-08-26 11:04 [PATCH datacenter-manager/installer/proxmox v2 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
` (9 preceding siblings ...)
2026-08-26 11:04 ` [PATCH installer v2 10/16] auto: enforce https for post hook when generating an API token Christoph Heiss
@ 2026-08-26 11:04 ` Christoph Heiss
2026-08-26 11:04 ` [PATCH datacenter-manager v2 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-08-26 11:04 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.
Changes v1 -> v2:
* no changes
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.55.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH datacenter-manager v2 12/16] ui: auto-installer: spell out Proxmox Datacenter Manager
2026-08-26 11:04 [PATCH datacenter-manager/installer/proxmox v2 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
` (10 preceding siblings ...)
2026-08-26 11:04 ` [PATCH installer v2 11/16] assistant: validate-answer: also verify post-hook settings if set Christoph Heiss
@ 2026-08-26 11:04 ` Christoph Heiss
2026-08-26 11:04 ` [PATCH datacenter-manager v2 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-08-26 11:04 UTC (permalink / raw)
To: pdm-devel
Better fits our brand guidelines.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Changes v1 -> v2:
* no changes
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.55.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH datacenter-manager v2 13/16] config: auto-install: add optional `post-hook-add-as-remote` field
2026-08-26 11:04 [PATCH datacenter-manager/installer/proxmox v2 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
` (11 preceding siblings ...)
2026-08-26 11:04 ` [PATCH datacenter-manager v2 12/16] ui: auto-installer: spell out Proxmox Datacenter Manager Christoph Heiss
@ 2026-08-26 11:04 ` Christoph Heiss
2026-08-26 11:04 ` [PATCH datacenter-manager v2 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-08-26 11:04 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Changes v1 -> v2:
* no changes
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.55.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH datacenter-manager v2 14/16] api: auto-installer: add option for adding new remotes to PDM
2026-08-26 11:04 [PATCH datacenter-manager/installer/proxmox v2 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
` (12 preceding siblings ...)
2026-08-26 11:04 ` [PATCH datacenter-manager v2 13/16] config: auto-install: add optional `post-hook-add-as-remote` field Christoph Heiss
@ 2026-08-26 11:04 ` Christoph Heiss
2026-08-26 11:04 ` [PATCH datacenter-manager v2 15/16] ui: auto-installer: wizard: add checkbox to add target as new remote Christoph Heiss
2026-08-26 11:04 ` [PATCH datacenter-manager v2 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-08-26 11:04 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.
Changes v1 -> v2:
* no changes
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 7c8ef0fb..744863dc 100644
--- a/server/src/api/auto_installer/mod.rs
+++ b/server/src/api/auto_installer/mod.rs
@@ -1,12 +1,13 @@
//! Implements all the methods under `/api2/json/auto-install/`.
-use anyhow::{Context, Result, anyhow};
-use http::StatusCode;
+use anyhow::{Context, Result, anyhow, bail};
+use http::{StatusCode, Uri};
+use log::warn;
use std::collections::{BTreeMap, HashMap};
-use pdm_api_types::PROXMOX_TOKEN_NAME_SCHEMA;
use pdm_api_types::{
- Authid, ConfigDigest, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA,
+ ConfigDigest, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA,
+ PROXMOX_TOKEN_NAME_SCHEMA,
auto_installer::{
AnswerToken, AnswerTokenCreateResult, AnswerTokenUpdateResult, AnswerTokenUpdater,
DeletableAnswerTokenProperty, DeletablePreparedInstallationConfigProperty,
@@ -15,12 +16,14 @@ use pdm_api_types::{
PreparedInstallationConfigCreateResult, PreparedInstallationConfigUpdateResult,
PreparedInstallationConfigUpdater, TEMPLATE_COUNTER_NAME_REGEX, UDEV_FILTER_KEY_REGEX,
},
+ remotes::{NodeUrl, Remote, RemoteType},
};
use pdm_config::auto_install::types::PreparedInstallationSectionConfigWrapper;
+use proxmox_auth_api::types::Authid;
use proxmox_installer_types::{
- SystemInfo,
+ ProxmoxProduct, SystemInfo,
answer::{
- self, AutoInstallerConfig, PostNotificationHookInfo, ROOT_PASSWORD_SCHEMA,
+ self, AutoInstallerConfig, PostNotificationHookInfo, ROOT_PASSWORD_SCHEMA, SchemaVersion,
fetch::AnswerFetchData,
},
post_hook::PostHookInfo,
@@ -36,6 +39,11 @@ use proxmox_schema::{
use proxmox_sortable_macro::sortable;
use proxmox_uuid::Uuid;
+use crate::api;
+
+const ANSWER_FETCH_SUPPORTED_SCHEMA: SchemaVersion = SchemaVersion(1, 0);
+const POST_HOOK_SUPPORTED_SCHEMA: SchemaVersion = SchemaVersion(1, 3);
+
#[sortable]
const SUBDIR_INSTALLATION_PER_ID: SubdirMap = &sorted!([(
"post-hook",
@@ -139,6 +147,8 @@ fn api_function_new_installation(
}
};
+ check_compatible_fetch_answer_schema(¶m)?;
+
let response = serde_json::from_value::<AnswerFetchData>(param)
.map_err(|err| anyhow!("failed to deserialize body: {err:?}"))
.and_then(|data| new_installation(&token_id, data))
@@ -161,6 +171,21 @@ fn api_function_new_installation(
})
}
+fn check_compatible_fetch_answer_schema(data: &serde_json::Value) -> Result<()> {
+ let version: SchemaVersion = data
+ .get("$schema")
+ .and_then(|schema| schema.get("version"))
+ .and_then(|val| val.as_str())
+ .ok_or_else(|| anyhow!("answer fetch schema version not found"))
+ .and_then(|s| s.parse())?;
+
+ if version <= ANSWER_FETCH_SUPPORTED_SCHEMA {
+ Ok(())
+ } else {
+ bail!("unsupported answer fetch version")
+ }
+}
+
/// Verifies the given `Authorization` HTTP header value whether
/// a) It matches the required format, i.e. Bearer <token-id>:<secret>
/// b) The token secret is known and verifies successfully.
@@ -227,6 +252,30 @@ fn new_installation(token_id: &String, payload: AnswerFetchData) -> Result<AutoI
.then(|| -> Result<String> { Ok(hex::encode(proxmox_sys::linux::random_data(16)?)) })
.transpose()?;
+ // Inject our custom post hook if the user defined a base url
+ if let Some(base_url) = config.post_hook_base_url {
+ // The target needs to support creating an API token, as well as the user requesting it
+ // to add it as a remote.
+ let should_create_token =
+ payload.sysinfo.supports_api_token_creation() && config.post_hook_add_as_remote;
+
+ answer.post_installation_webhook = Some(PostNotificationHookInfo {
+ url: format!(
+ "{}/api2/json/auto-install/installations/{uuid}/post-hook",
+ base_url.trim_end_matches('/')
+ ),
+ cert_fingerprint: config.post_hook_cert_fp.clone(),
+ auth_token: post_hook_token.clone(),
+ // Anything below was introduced with API token support, so gate it on that to avoid
+ // breaking older ISOs in combination with newer PDM versions
+ max_schema_version: payload
+ .sysinfo
+ .supports_api_token_creation()
+ .then_some(POST_HOOK_SUPPORTED_SCHEMA),
+ api_token_name: should_create_token.then(|| "pdm-admin".to_owned()),
+ });
+ }
+
let installation = Installation {
uuid: uuid.clone(),
received_at: timestamp_now,
@@ -234,21 +283,9 @@ fn new_installation(token_id: &String, payload: AnswerFetchData) -> Result<AutoI
info: payload.sysinfo,
answer_id: Some(config.id.clone()),
post_hook_data: None,
- post_hook_token: post_hook_token.clone(),
+ post_hook_token,
};
- // Inject our custom post hook if the user defined a base url
- if let Some(base_url) = config.post_hook_base_url {
- answer.post_installation_webhook = Some(PostNotificationHookInfo {
- url: format!(
- "{}/api2/json/auto-install/installations/{uuid}/post-hook",
- base_url.trim_end_matches('/')
- ),
- cert_fingerprint: config.post_hook_cert_fp.clone(),
- auth_token: post_hook_token,
- });
- }
-
increment_template_counters(&config.id)?;
pdm_config::auto_install::save_installation(&installation)?;
Ok(answer)
@@ -596,6 +633,7 @@ async fn update_prepared_answer(
disk_filter_match,
post_hook_base_url,
post_hook_cert_fp,
+ post_hook_add_as_remote,
template_counters,
subscription_key,
} = update;
@@ -716,6 +754,10 @@ async fn update_prepared_answer(
p.post_hook_cert_fp = Some(fp);
}
+ if let Some(add) = post_hook_add_as_remote {
+ p.post_hook_add_as_remote = add;
+ }
+
if let Some(counters) = template_counters {
validate_template_map(&counters)?;
**p.template_counters = counters;
@@ -815,7 +857,7 @@ fn validate_template_map(map: &BTreeMap<String, i32>) -> Result<()> {
/// POST /auto-install/installations/{uuid}/post-hook
///
/// Handles the post-installation hook for all installations.
-async fn handle_post_hook(uuid: Uuid, token: String, info: PostHookInfo) -> Result<()> {
+async fn handle_post_hook(uuid: Uuid, token: String, mut info: PostHookInfo) -> Result<()> {
let _lock = pdm_config::auto_install::installations_write_lock();
let (mut installations, _) = pdm_config::auto_install::read_installations()?;
@@ -840,14 +882,87 @@ async fn handle_post_hook(uuid: Uuid, token: String, info: PostHookInfo) -> Resu
return not_found();
}
+ let token_secret = if let Some(token) = &mut info.api_token {
+ Some(std::mem::take(&mut token.secret))
+ } else {
+ None
+ };
+
install.status = InstallationStatus::Finished;
- install.post_hook_data = Some(info);
+ install.post_hook_data = Some(info.clone());
install.post_hook_token = None;
pdm_config::auto_install::save_installation(install)?;
+ if let (Some(token), Some(secret)) = (&info.api_token, token_secret) {
+ add_host_as_remote(install, &info, &token.id, secret).await?
+ }
+
Ok(())
}
+async fn add_host_as_remote(
+ install: &Installation,
+ info: &PostHookInfo,
+ authid: &Authid,
+ token: String,
+) -> Result<()> {
+ let ty = match info.product.short {
+ ProxmoxProduct::Pve => RemoteType::Pve,
+ ProxmoxProduct::Pbs => RemoteType::Pbs,
+ _ => {
+ let answer = install
+ .answer_id
+ .as_ref()
+ .map(|s| format!(" (answer: '{s}')"))
+ .unwrap_or_default();
+
+ warn!(
+ "auto-installer: cannot add {} target as remote{answer}!",
+ info.product.fullname,
+ );
+ return Ok(());
+ }
+ };
+
+ // Prefer the management interface address if available, otherwise fall back to the first
+ // interface with an address. As a last fallback, use the FQDN.
+ let hostname = info
+ .network_interfaces
+ .iter()
+ .find(|iface| iface.is_management)
+ .and_then(|iface| iface.address)
+ .or_else(|| {
+ info.network_interfaces
+ .iter()
+ .find(|iface| iface.address.is_some())
+ .and_then(|iface| iface.address)
+ })
+ .map_or_else(|| info.fqdn.to_owned(), |cidr| cidr.address().to_string());
+
+ let web_url = Uri::builder()
+ .scheme("https")
+ .authority(format!("{}:{}", info.fqdn, ty.default_port()))
+ .build()
+ .ok();
+
+ let remote = Remote {
+ ty,
+ id: info.fqdn.clone(),
+ nodes: vec![
+ NodeUrl {
+ hostname,
+ fingerprint: info.cert_fingerprint.clone(),
+ }
+ .into(),
+ ],
+ authid: authid.clone(),
+ token: token.to_owned(),
+ web_url,
+ };
+
+ api::remotes::add_remote(remote, None).await
+}
+
#[api(
returns: {
description: "List of tokens for authenticating automated installations requests.",
--
2.55.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH datacenter-manager v2 15/16] ui: auto-installer: wizard: add checkbox to add target as new remote
2026-08-26 11:04 [PATCH datacenter-manager/installer/proxmox v2 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
` (13 preceding siblings ...)
2026-08-26 11:04 ` [PATCH datacenter-manager v2 14/16] api: auto-installer: add option for adding new remotes to PDM Christoph Heiss
@ 2026-08-26 11:04 ` Christoph Heiss
2026-08-26 11:04 ` [PATCH datacenter-manager v2 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-08-26 11:04 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Changes v1 -> v2:
* drop confusing tooltip hint on supported products
.../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..6257bc11 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!("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.55.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
* [PATCH datacenter-manager v2 16/16] docs: auto-installer: document adding targets as remotes afterwards
2026-08-26 11:04 [PATCH datacenter-manager/installer/proxmox v2 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
` (14 preceding siblings ...)
2026-08-26 11:04 ` [PATCH datacenter-manager v2 15/16] ui: auto-installer: wizard: add checkbox to add target as new remote Christoph Heiss
@ 2026-08-26 11:04 ` Christoph Heiss
15 siblings, 0 replies; 17+ messages in thread
From: Christoph Heiss @ 2026-08-26 11:04 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 committed beforehand.
I will also update the wiki page accordingly on time.
Changes v1 -> v2:
* adopt title case for all headings in the file
docs/automated-installations.rst | 39 +++++++++++++++++++++++++++++---
1 file changed, 36 insertions(+), 3 deletions(-)
diff --git a/docs/automated-installations.rst b/docs/automated-installations.rst
index ede613dc..f9a7ef88 100644
--- a/docs/automated-installations.rst
+++ b/docs/automated-installations.rst
@@ -30,7 +30,7 @@ copying into new answers and deleting them. For a quick overview, it shows
whether an answer is the default and what target filters have been defined for
that particular configuration.
-Target filter
+Target Filter
^^^^^^^^^^^^^
Target filter allow you to control what systems should match.
@@ -110,7 +110,7 @@ valid subscription.
.. _autoinst_token:
-Authentication token management
+Authentication Token Management
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To use the automated installer integration of Proxmox Datacenter Manager, an
@@ -121,9 +121,42 @@ 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
+Preparing An ISO
~~~~~~~~~~~~~~~~
To use an installation ISO of a Proxmox product with the Proxmox Datacenter
--
2.55.0
^ permalink raw reply related [flat|nested] 17+ messages in thread
end of thread, other threads:[~2026-08-26 11:06 UTC | newest]
Thread overview: 17+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-26 11:04 [PATCH datacenter-manager/installer/proxmox v2 00/16] auto-installer: add installed target systems as new remotes Christoph Heiss
2026-08-26 11:04 ` [PATCH proxmox v2 01/16] installer-types: drop unnecessary clippy attribute Christoph Heiss
2026-08-26 11:04 ` [PATCH proxmox v2 02/16] installer-types: post-hook: factor schema version into proper struct Christoph Heiss
2026-08-26 11:04 ` [PATCH proxmox v2 03/16] installer-types: post-hook: allow additional properties on api schema Christoph Heiss
2026-08-26 11:04 ` [PATCH proxmox v2 04/16] installer-types: post-hook: add api-token and cert-fingerprint options Christoph Heiss
2026-08-26 11:04 ` [PATCH proxmox v2 05/16] installer-types: systeminfo: add check for API token creation capability Christoph Heiss
2026-08-26 11:04 ` [PATCH installer v2 06/16] chroot: print full error if bind-mounting fails Christoph Heiss
2026-08-26 11:04 ` [PATCH installer v2 07/16] post-hook: re-use low-level config retrieval from proxmox-chroot Christoph Heiss
2026-08-26 11:04 ` [PATCH installer v2 08/16] post-hook: generate and retrieve node certificate fingerprint Christoph Heiss
2026-08-26 11:04 ` [PATCH installer v2 09/16] post-hook: support creating API token if requested in answer file Christoph Heiss
2026-08-26 11:04 ` [PATCH installer v2 10/16] auto: enforce https for post hook when generating an API token Christoph Heiss
2026-08-26 11:04 ` [PATCH installer v2 11/16] assistant: validate-answer: also verify post-hook settings if set Christoph Heiss
2026-08-26 11:04 ` [PATCH datacenter-manager v2 12/16] ui: auto-installer: spell out Proxmox Datacenter Manager Christoph Heiss
2026-08-26 11:04 ` [PATCH datacenter-manager v2 13/16] config: auto-install: add optional `post-hook-add-as-remote` field Christoph Heiss
2026-08-26 11:04 ` [PATCH datacenter-manager v2 14/16] api: auto-installer: add option for adding new remotes to PDM Christoph Heiss
2026-08-26 11:04 ` [PATCH datacenter-manager v2 15/16] ui: auto-installer: wizard: add checkbox to add target as new remote Christoph Heiss
2026-08-26 11:04 ` [PATCH datacenter-manager v2 16/16] docs: auto-installer: document adding targets as remotes afterwards Christoph Heiss
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.