From: Christoph Heiss <c.heiss@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager 11/13] server: api: add auto-installer integration module
Date: Thu, 4 Dec 2025 13:51:20 +0100 [thread overview]
Message-ID: <20251204125122.945961-12-c.heiss@proxmox.com> (raw)
In-Reply-To: <20251204125122.945961-1-c.heiss@proxmox.com>
Adds the required API surface for managing prepared answer files and
viewing past installations via the UI, as well as serving answer files
to proxmox-auto-installer.
Short overview:
POST /auto-install/answer
serves answer files based on target filters, if any
GET /auto-install/installations
list all in-progress and past installations
POST /auto-install/installations/{id}/post-hook
endpoint for integrating the post-installation notification webhook
GET /auto-install/prepared
list all prepared answer file configurations
POST /auto-install/prepared
create a new prepared answer file configuration
DELETE /auto-install/prepared
delete an existing prepared answer file configuration
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
The auto-installer (currently) only supports TOML as input format for
the answer, so we need to "hack" around a bit to serve TOML in the API.
See also the cover letter for a bit more discussion.
Cargo.toml | 2 +
debian/control | 2 +
server/Cargo.toml | 4 +
server/src/api/auto_installer/mod.rs | 654 +++++++++++++++++++++++++++
server/src/api/mod.rs | 2 +
5 files changed, 664 insertions(+)
create mode 100644 server/src/api/auto_installer/mod.rs
diff --git a/Cargo.toml b/Cargo.toml
index 9c1f0c2..c897eda 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -105,6 +105,7 @@ async-trait = "0.1"
bitflags = "2.4"
const_format = "0.2"
futures = "0.3"
+glob = "0.3"
h2 = { version = "0.4", features = [ "stream" ] }
handlebars = "5.1"
hex = "0.4.3"
@@ -131,6 +132,7 @@ tokio = "1.6"
tokio-openssl = "0.6.1"
tokio-stream = "0.1.0"
tokio-util = { version = "0.7", features = [ "io" ] }
+toml.version = "0.8"
tower-service = "0.3.0"
tracing = "0.1"
url = "2.1"
diff --git a/debian/control b/debian/control
index b1ce92a..59d5f63 100644
--- a/debian/control
+++ b/debian/control
@@ -17,6 +17,7 @@ Build-Depends: debhelper-compat (= 13),
librust-async-trait-0.1+default-dev,
librust-const-format-0.2+default-dev,
librust-futures-0.3+default-dev,
+ librust-glob-0.3-dev,
librust-hex-0.4+default-dev (>= 0.4.3-~~),
librust-http-1+default-dev,
librust-http-body-util-0.1+default-dev (>= 0.1.2-~~),
@@ -130,6 +131,7 @@ Build-Depends: debhelper-compat (= 13),
librust-tokio-1+signal-dev (>= 1.6-~~),
librust-tokio-1+time-dev (>= 1.6-~~),
librust-tokio-stream-0.1+default-dev,
+ librust-toml-0.8+default-dev,
librust-tracing-0.1+default-dev,
librust-url-2+default-dev (>= 2.1-~~),
librust-webauthn-rs-core-0.5+default-dev,
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 6969549..4501a15 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -14,6 +14,7 @@ async-stream.workspace = true
async-trait.workspace = true
const_format.workspace = true
futures.workspace = true
+glob.workspace = true
hex.workspace = true
http.workspace = true
http-body-util.workspace = true
@@ -31,6 +32,7 @@ serde_plain.workspace = true
syslog.workspace = true
tokio = { workspace = true, features = [ "fs", "io-util", "io-std", "macros", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "time" ] }
tokio-stream.workspace = true
+toml.workspace = true
tracing.workspace = true
url.workspace = true
zstd.workspace = true
@@ -42,10 +44,12 @@ proxmox-base64.workspace = true
proxmox-daemon.workspace = true
proxmox-docgen.workspace = true
proxmox-http = { workspace = true, features = [ "client-trait", "proxmox-async" ] } # pbs-client doesn't use these
+proxmox-installer-types.workspace = true
proxmox-lang.workspace = true
proxmox-ldap.workspace = true
proxmox-log.workspace = true
proxmox-login.workspace = true
+proxmox-network-types.workspace = true
proxmox-openid.workspace = true
proxmox-rest-server = { workspace = true, features = [ "templates" ] }
proxmox-router = { workspace = true, features = [ "cli", "server"] }
diff --git a/server/src/api/auto_installer/mod.rs b/server/src/api/auto_installer/mod.rs
new file mode 100644
index 0000000..1412e99
--- /dev/null
+++ b/server/src/api/auto_installer/mod.rs
@@ -0,0 +1,654 @@
+//! Implements all the methods under `/api2/*/access/auto-install/`.
+
+use anyhow::{anyhow, Result};
+use std::time::SystemTime;
+
+use pdm_api_types::{
+ auto_installer::{
+ Installation, InstallationStatus, PreparedInstallationConfig,
+ PreparedInstallationConfigDeletableProperty, PreparedInstallationConfigUpdater,
+ INSTALLATION_UUID_SCHEMA, PREPARED_INSTALL_CONFIG_ID_SCHEMA,
+ },
+ ConfigDigest, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA,
+};
+use proxmox_installer_types::{
+ answer::{
+ fetch::{AnswerFetchData, AnswerFetchResponse},
+ AutoInstallerConfig, PostNotificationHookInfo, ROOT_PASSWORD_SCHEMA,
+ },
+ post_hook::PostHookInfo,
+};
+use proxmox_router::{
+ http_bail, http_err, list_subdirs_api_method, ApiHandler, ApiMethod, ApiResponseFuture,
+ Permission, Router, RpcEnvironment, SubdirMap,
+};
+use proxmox_schema::{api, AllOfSchema, ApiType, ParameterSchema, ReturnType, StringSchema};
+use proxmox_sortable_macro::sortable;
+use proxmox_uuid::Uuid;
+
+#[sortable]
+const SUBDIR_INSTALLATION_PER_ID: SubdirMap = &sorted!([(
+ "post-hook",
+ &Router::new().post(&API_METHOD_HANDLE_POST_HOOK)
+)]);
+
+#[sortable]
+const SUBDIRS: SubdirMap = &sorted!([
+ ("answer", &Router::new().post(&API_METHOD_NEW_INSTALLATION)),
+ (
+ "prepared",
+ &Router::new()
+ .get(&API_METHOD_LIST_PREPARED)
+ .post(&API_METHOD_CREATE_PREPARED)
+ .match_all(
+ "id",
+ &Router::new()
+ .get(&API_METHOD_GET_PREPARED)
+ .put(&API_METHOD_UPDATE_PREPARED)
+ .delete(&API_METHOD_DELETE_PREPARED)
+ )
+ ),
+ (
+ "installations",
+ &Router::new().get(&API_METHOD_LIST_INSTALLATIONS).match_all(
+ "uuid",
+ &Router::new()
+ .delete(&API_METHOD_DELETE_INSTALLATION)
+ .subdirs(SUBDIR_INSTALLATION_PER_ID)
+ )
+ ),
+]);
+
+pub const ROUTER: Router = Router::new()
+ .get(&list_subdirs_api_method!(SUBDIRS))
+ .subdirs(SUBDIRS);
+
+const API_METHOD_NEW_INSTALLATION: ApiMethod = ApiMethod::new_full(
+ &ApiHandler::AsyncHttpBodyParameters(&api_function_new_installation),
+ ParameterSchema::AllOf(&AllOfSchema::new(
+ r#"\
+ Handles the system information of a new machine to install.
+
+ See also
+ <https://pve.proxmox.com/wiki/Automated_Installation#Answer_Fetched_via_HTTP>"#,
+ &[&<AnswerFetchData as ApiType>::API_SCHEMA],
+ )),
+)
+.returns(ReturnType::new(
+ false,
+ &StringSchema::new(
+ "either a auto-installation configuration or a request to wait, in JSON format",
+ )
+ .schema(),
+))
+.access(
+ Some("proxmox-fetch-answer only sends unauthenticated requests."),
+ &Permission::World,
+)
+.protected(false);
+
+fn api_function_new_installation(
+ _parts: http::request::Parts,
+ param: serde_json::Value,
+ _info: &ApiMethod,
+ _rpcenv: Box<dyn RpcEnvironment>,
+) -> ApiResponseFuture {
+ Box::pin(async move {
+ let response = serde_json::from_value::<AnswerFetchData>(param)
+ .map_err(|err| anyhow!("failed to deserialize body: {err:?}"))
+ .and_then(new_installation)
+ .and_then(|x| {
+ toml::to_string(&x).map_err(|err| anyhow!("failed to deserialize body: {err:?}"))
+ });
+
+ match response {
+ Ok(value) => Ok(http::Response::builder()
+ .status(200)
+ .header(http::header::CONTENT_TYPE, "application/toml")
+ .body(value.into())?),
+ Err(err) => Ok(http::Response::builder()
+ .status(401)
+ .header(http::header::CONTENT_TYPE, "text/plain")
+ .body(format!("{err:?}").into())?),
+ }
+ })
+}
+
+/// POST /auto-install/answer
+///
+/// Either returns a auto-installer configuration if a matching one is found or errors out.
+/// The system information data is saved in any case to make them easily inspectable.
+fn new_installation(payload: AnswerFetchData) -> Result<AnswerFetchResponse> {
+ let _lock = pdm_config::auto_install::installations_write_lock();
+
+ let uuid = Uuid::generate();
+ let (mut installations, _) = pdm_config::auto_install::read_installations()?;
+
+ if installations.iter().any(|p| p.uuid == uuid) {
+ http_bail!(CONFLICT, "already exists");
+ }
+
+ let timestamp_now = SystemTime::now()
+ .duration_since(SystemTime::UNIX_EPOCH)?
+ .as_secs() as i64;
+
+ if let Some(config) = find_config(&payload.sysinfo)? {
+ let status = if config.post_hook_base_url.is_some() {
+ InstallationStatus::InProgress
+ } else {
+ InstallationStatus::Finished
+ };
+
+ installations.push(Installation {
+ uuid: uuid.clone(),
+ received_at: timestamp_now,
+ status,
+ info: payload.sysinfo,
+ answer_id: Some(config.id.clone()),
+ post_hook_data: None,
+ });
+
+ // "Inject" our custom post hook if possible
+ let mut answer: AutoInstallerConfig = config.clone().try_into()?;
+ if let Some(base_url) = config.post_hook_base_url {
+ answer.post_installation_webhook = Some(PostNotificationHookInfo {
+ url: format!("{base_url}/auto-install/installations/{uuid}/post-hook"),
+ cert_fingerprint: config.post_hook_cert_fp.clone(),
+ });
+ }
+
+ pdm_config::auto_install::save_installations(&installations)?;
+ Ok(AnswerFetchResponse::Configuration(answer))
+ } else {
+ installations.push(Installation {
+ uuid: uuid.clone(),
+ received_at: timestamp_now,
+ status: InstallationStatus::NoAnswerFound,
+ info: payload.sysinfo,
+ answer_id: None,
+ post_hook_data: None,
+ });
+
+ pdm_config::auto_install::save_installations(&installations)?;
+ http_bail!(NOT_FOUND, "no answer file found");
+ }
+}
+
+#[api(
+ returns: {
+ description: "List of all automated installations.",
+ type: Array,
+ items: { type: Installation },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "auto-installation"], PRIV_SYS_AUDIT, false),
+ },
+)]
+/// GET /auto-install/installations
+///
+/// Get all automated installations.
+fn list_installations(rpcenv: &mut dyn RpcEnvironment) -> Result<Vec<Installation>> {
+ let _lock = pdm_config::auto_install::installations_read_lock();
+
+ let (config, digest) = pdm_config::auto_install::read_installations()?;
+
+ rpcenv["digest"] = hex::encode(digest).into();
+ Ok(config)
+}
+
+#[api(
+ input: {
+ properties: {
+ uuid: {
+ schema: INSTALLATION_UUID_SCHEMA,
+ }
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "auto-installation"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// DELETE /auto-install/installations/{uuid}
+///
+/// Remove an installation entry.
+fn delete_installation(uuid: Uuid) -> Result<()> {
+ let _lock = pdm_config::auto_install::installations_write_lock();
+
+ let (mut installations, _) = pdm_config::auto_install::read_installations()?;
+ if installations
+ .extract_if(.., |inst| inst.uuid == uuid)
+ .count()
+ == 0
+ {
+ http_bail!(NOT_FOUND, "no such entry {uuid:?}");
+ }
+
+ pdm_config::auto_install::save_installations(&installations)
+}
+
+#[api(
+ returns: {
+ description: "List of prepared auto-installer answer configurations.",
+ type: Array,
+ items: { type: PreparedInstallationConfig },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "auto-installation"], PRIV_SYS_AUDIT, false),
+ },
+)]
+/// GET /auto-install/prepared
+///
+/// Get all prepared auto-installer answer configurations.
+async fn list_prepared(rpcenv: &mut dyn RpcEnvironment) -> Result<Vec<PreparedInstallationConfig>> {
+ let (prepared, digest) = pdm_config::auto_install::read_prepared_answers()?;
+
+ rpcenv["digest"] = hex::encode(digest).into();
+
+ Ok(prepared
+ .convert_to_typed_array::<PreparedInstallationConfig>("prepared-answer")?
+ .into_iter()
+ .map(|mut p| {
+ p.root_password_hashed = None;
+ p
+ })
+ .collect())
+}
+
+#[api(
+ input: {
+ properties: {
+ config: {
+ type: PreparedInstallationConfig,
+ flatten: true,
+ },
+ "root-password": {
+ schema: ROOT_PASSWORD_SCHEMA,
+ optional: true,
+ },
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "auto-installation"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// POST /auto-install/prepared
+///
+/// Creates a new prepared answer file.
+async fn create_prepared(
+ mut config: PreparedInstallationConfig,
+ root_password: Option<String>,
+) -> Result<()> {
+ let _lock = pdm_config::auto_install::prepared_answers_write_lock();
+ let (mut prepared, _) = pdm_config::auto_install::read_prepared_answers()?;
+
+ if prepared
+ .lookup::<PreparedInstallationConfig>("prepared-answer", &config.id)
+ .is_ok()
+ {
+ http_bail!(
+ CONFLICT,
+ "configuration with ID {} already exists",
+ config.id
+ );
+ }
+
+ if config.is_default {
+ if let Some(p) = prepared
+ .convert_to_typed_array::<PreparedInstallationConfig>("prepared-answer")?
+ .iter()
+ .find(|p| p.is_default)
+ {
+ http_bail!(
+ CONFLICT,
+ "configuration '{}' is already the default answer",
+ p.id
+ );
+ }
+ }
+
+ if let Some(password) = root_password {
+ config.root_password_hashed = Some(proxmox_sys::crypt::encrypt_pw(&password)?);
+ } else if config.root_password_hashed.is_none() {
+ http_bail!(
+ BAD_REQUEST,
+ "either `root-password` or `root-password-hashed` must be set"
+ );
+ }
+
+ prepared.set_data(&config.id.clone(), "prepared-answer", config)?;
+ pdm_config::auto_install::save_prepared_answers(&prepared)?;
+ Ok(())
+}
+
+#[api(
+ input: {
+ properties: {
+ id: {
+ schema: PREPARED_INSTALL_CONFIG_ID_SCHEMA,
+ },
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "auto-installation"], PRIV_SYS_AUDIT, false),
+ },
+)]
+/// GET /auto-install/prepared/{id}
+///
+/// Retrieves a prepared auto-installer answer configuration.
+async fn get_prepared(id: String) -> Result<PreparedInstallationConfig> {
+ let (prepared, _) = pdm_config::auto_install::read_prepared_answers()?;
+
+ if let Ok(mut p) = prepared.lookup::<PreparedInstallationConfig>("prepared-answer", &id) {
+ // Don't send the hashed password, the user cannot do anything with it anyway
+ p.root_password_hashed = None;
+ Ok(p)
+ } else {
+ http_bail!(NOT_FOUND, "no such prepared answer configuration: {id}");
+ }
+}
+
+#[api(
+ input: {
+ properties: {
+ id: {
+ schema: PREPARED_INSTALL_CONFIG_ID_SCHEMA,
+ },
+ update: {
+ type: PreparedInstallationConfigUpdater,
+ flatten: true,
+ },
+ "root-password": {
+ schema: ROOT_PASSWORD_SCHEMA,
+ optional: true,
+ },
+ delete: {
+ description: "List of properties to delete.",
+ type: Array,
+ optional: true,
+ items: {
+ type: PreparedInstallationConfigDeletableProperty,
+ }
+ },
+ digest: {
+ optional: true,
+ schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
+ },
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "auto-installation"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// PUT /auto-install/prepared/{id}
+///
+/// Updates a prepared auto-installer answer configuration.
+async fn update_prepared(
+ id: String,
+ update: PreparedInstallationConfigUpdater,
+ root_password: Option<String>,
+ delete: Option<Vec<PreparedInstallationConfigDeletableProperty>>,
+ digest: Option<ConfigDigest>,
+) -> Result<()> {
+ let _lock = pdm_config::auto_install::prepared_answers_write_lock();
+
+ let (mut prepared, config_digest) = pdm_config::auto_install::read_prepared_answers()?;
+ config_digest.detect_modification(digest.as_ref())?;
+
+ let mut p = prepared
+ .lookup::<PreparedInstallationConfig>("prepared-answer", &id)
+ .map_err(|_| http_err!(NOT_FOUND, "no such prepared answer configuration: {id}"))?;
+
+ if let Some(delete) = delete {
+ for prop in delete {
+ match prop {
+ PreparedInstallationConfigDeletableProperty::TargetFilter => {
+ p.target_filter = Vec::new();
+ }
+ PreparedInstallationConfigDeletableProperty::NetdevFilter => {
+ p.netdev_filter = Vec::new();
+ }
+ PreparedInstallationConfigDeletableProperty::DiskFilter => {
+ p.disk_filter = Vec::new()
+ }
+ PreparedInstallationConfigDeletableProperty::RootSshKeys => {
+ p.root_ssh_keys = Vec::new();
+ }
+ PreparedInstallationConfigDeletableProperty::PostHookBaseUrl => {
+ p.post_hook_base_url = None;
+ }
+ PreparedInstallationConfigDeletableProperty::PostHookCertFp => {
+ p.post_hook_cert_fp = None;
+ }
+ }
+ }
+ }
+
+ if let Some(target_filter) = update.target_filter {
+ p.target_filter = target_filter;
+ }
+
+ if let Some(is_default) = update.is_default {
+ if is_default {
+ if let Some(other) = prepared
+ .convert_to_typed_array::<PreparedInstallationConfig>("prepared-answer")?
+ .iter()
+ .find(|p| p.is_default)
+ {
+ http_bail!(
+ CONFLICT,
+ "configuration '{}' is already the default answer",
+ other.id
+ );
+ }
+ }
+
+ p.is_default = is_default;
+ }
+
+ if let Some(country) = update.country {
+ p.country = country;
+ }
+
+ if let Some(fqdn) = update.fqdn {
+ p.fqdn = fqdn;
+ }
+
+ if let Some(use_dhcp) = update.use_dhcp_fqdn {
+ p.use_dhcp_fqdn = use_dhcp;
+ }
+
+ if let Some(keyboard) = update.keyboard {
+ p.keyboard = keyboard;
+ }
+
+ if let Some(mailto) = update.mailto {
+ p.mailto = mailto;
+ }
+
+ if let Some(timezone) = update.timezone {
+ p.timezone = timezone;
+ }
+
+ if let Some(password) = root_password {
+ p.root_password_hashed = Some(proxmox_sys::crypt::encrypt_pw(&password)?);
+ } else if let Some(password) = update.root_password_hashed {
+ p.root_password_hashed = Some(password);
+ }
+
+ if let Some(reboot_on_error) = update.reboot_on_error {
+ p.reboot_on_error = reboot_on_error;
+ }
+
+ if let Some(reboot_mode) = update.reboot_mode {
+ p.reboot_mode = reboot_mode;
+ }
+
+ if let Some(ssh_keys) = update.root_ssh_keys {
+ p.root_ssh_keys = if ssh_keys.is_empty() {
+ Vec::new()
+ } else {
+ ssh_keys
+ };
+ }
+
+ if let Some(use_dhcp) = update.use_dhcp_network {
+ p.use_dhcp_network = use_dhcp;
+ }
+
+ if let Some(cidr) = update.cidr {
+ p.cidr = Some(cidr);
+ }
+
+ if let Some(gateway) = update.gateway {
+ p.gateway = Some(gateway);
+ }
+
+ if let Some(dns) = update.dns {
+ p.dns = Some(dns);
+ }
+
+ if let Some(filter) = update.netdev_filter {
+ p.netdev_filter = filter;
+ }
+
+ if let Some(enabled) = update.netif_name_pinning_enabled {
+ p.netif_name_pinning_enabled = enabled;
+ }
+
+ if let Some(typ) = update.filesystem_type {
+ p.filesystem_type = typ;
+ }
+
+ if let Some(options) = update.filesystem_options {
+ p.filesystem_options = options;
+ }
+
+ if let Some(mode) = update.disk_mode {
+ p.disk_mode = mode;
+ }
+
+ if let Some(list) = update.disk_list {
+ p.disk_list = if list.is_empty() { None } else { Some(list) };
+ }
+
+ if let Some(filter) = update.disk_filter {
+ p.disk_filter = filter;
+ }
+
+ if let Some(filter_match) = update.disk_filter_match {
+ p.disk_filter_match = Some(filter_match);
+ }
+
+ if let Some(url) = update.post_hook_base_url {
+ p.post_hook_base_url = Some(url);
+ }
+
+ if let Some(fp) = update.post_hook_cert_fp {
+ p.post_hook_cert_fp = Some(fp);
+ }
+
+ prepared.set_data(&id, "prepared-answer", p)?;
+ pdm_config::auto_install::save_prepared_answers(&prepared)?;
+
+ Ok(())
+}
+
+#[api(
+ input: {
+ properties: {
+ id: {
+ schema: PREPARED_INSTALL_CONFIG_ID_SCHEMA,
+ },
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["system", "auto-installation"], PRIV_SYS_MODIFY, false),
+ },
+)]
+/// DELETE /auto-install/prepared/{id}
+///
+/// Deletes a prepared auto-installer answer configuration.
+async fn delete_prepared(id: String) -> Result<()> {
+ let _lock = pdm_config::auto_install::prepared_answers_write_lock();
+
+ let (mut prepared, _) = pdm_config::auto_install::read_prepared_answers()?;
+ if prepared.sections.remove(&id).is_none() {
+ http_bail!(NOT_FOUND, "no such entry '{id:?}'");
+ }
+
+ pdm_config::auto_install::save_prepared_answers(&prepared)
+}
+
+#[api(
+ input: {
+ properties: {
+ uuid: {
+ schema: INSTALLATION_UUID_SCHEMA,
+ },
+ info: {
+ type: PostHookInfo,
+ flatten: true,
+ }
+ },
+ },
+ access: {
+ permission: &Permission::World,
+ },
+)]
+/// POST /auto-install/installations/{uuid}/post-hook
+///
+/// Handles the post-installation hook for all installations.
+async fn handle_post_hook(uuid: Uuid, info: PostHookInfo) -> Result<()> {
+ let _lock = pdm_config::auto_install::installations_write_lock();
+ let (mut installations, _) = pdm_config::auto_install::read_installations()?;
+
+ if let Some(install) = installations.iter_mut().find(|p| p.uuid == uuid) {
+ install.status = InstallationStatus::Finished;
+ install.post_hook_data = Some(info);
+ pdm_config::auto_install::save_installations(&installations)?;
+ } else {
+ http_bail!(NOT_FOUND, "installation {uuid} not found");
+ }
+
+ Ok(())
+}
+
+fn find_config(
+ info: &proxmox_installer_types::SystemInfo,
+) -> Result<Option<PreparedInstallationConfig>> {
+ let info = serde_json::to_value(info)?;
+ let (prepared, _) = pdm_config::auto_install::read_prepared_answers()?;
+
+ let mut default_answer = None;
+ for p in prepared.convert_to_typed_array::<PreparedInstallationConfig>("prepared-answer")? {
+ if p.is_default {
+ default_answer = Some(p.clone());
+ continue;
+ }
+
+ if p.target_filter.is_empty() {
+ // Not default answer and empty target filter, can never match
+ continue;
+ }
+
+ let matched_all = p.target_filter.iter().all(|filter| {
+ let pattern = match glob::Pattern::new(&filter.value) {
+ Ok(pattern) => pattern,
+ _ => return false,
+ };
+
+ if let Some(value) = info.pointer(&filter.key).and_then(|v| v.as_str()) {
+ pattern.matches(value)
+ } else {
+ false
+ }
+ });
+
+ if matched_all {
+ return Ok(Some(p.clone()));
+ }
+ }
+
+ // If no specific target filter(s) matched, return the default answer, if there is one
+ Ok(default_answer)
+}
diff --git a/server/src/api/mod.rs b/server/src/api/mod.rs
index 5688871..0fa26da 100644
--- a/server/src/api/mod.rs
+++ b/server/src/api/mod.rs
@@ -9,6 +9,7 @@ use proxmox_schema::api;
use proxmox_sortable_macro::sortable;
pub mod access;
+pub mod auto_installer;
pub mod config;
pub mod metric_collection;
pub mod nodes;
@@ -25,6 +26,7 @@ pub mod sdn;
#[sortable]
const SUBDIRS: SubdirMap = &sorted!([
("access", &access::ROUTER),
+ ("auto-install", &auto_installer::ROUTER),
("config", &config::ROUTER),
("ping", &Router::new().get(&API_METHOD_PING)),
("pve", &pve::ROUTER),
--
2.51.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
next prev parent reply other threads:[~2025-12-04 12:52 UTC|newest]
Thread overview: 17+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-12-04 12:51 [pdm-devel] [PATCH proxmox/datacenter-manager 00/13] initial auto-installer integration Christoph Heiss
2025-12-04 12:51 ` [pdm-devel] [PATCH proxmox 01/13] api-macro: allow $ in identifier name Christoph Heiss
2025-12-04 12:51 ` [pdm-devel] [PATCH proxmox 02/13] network-types: move `Fqdn` type from proxmox-installer-common Christoph Heiss
2025-12-04 12:51 ` [pdm-devel] [PATCH proxmox 03/13] network-types: implement api type for Fqdn Christoph Heiss
2025-12-04 12:51 ` [pdm-devel] [PATCH proxmox 04/13] network-types: add api wrapper type for std::net::IpAddr Christoph Heiss
2025-12-04 12:51 ` [pdm-devel] [PATCH proxmox 05/13] installer-types: add common types used by the installer Christoph Heiss
2025-12-04 12:51 ` [pdm-devel] [PATCH proxmox 06/13] installer-types: add types used by the auto-installer Christoph Heiss
2025-12-04 12:51 ` [pdm-devel] [PATCH proxmox 07/13] installer-types: implement api type for all externally-used types Christoph Heiss
2025-12-04 12:51 ` [pdm-devel] [PATCH datacenter-manager 08/13] api-types: add api types for auto-installer integration Christoph Heiss
2025-12-04 12:51 ` [pdm-devel] [PATCH datacenter-manager 09/13] config: add auto-installer configuration module Christoph Heiss
2025-12-04 12:51 ` [pdm-devel] [PATCH datacenter-manager 10/13] acl: wire up new /system/auto-installation acl path Christoph Heiss
2025-12-04 12:51 ` Christoph Heiss [this message]
2025-12-04 12:51 ` [pdm-devel] [PATCH datacenter-manager 12/13] ui: auto-installer: add installations overview panel Christoph Heiss
2025-12-04 12:51 ` [pdm-devel] [PATCH datacenter-manager 13/13] ui: auto-installer: add prepared answer configuration panel Christoph Heiss
2025-12-04 14:17 ` [pdm-devel] [PATCH proxmox/datacenter-manager 00/13] initial auto-installer integration Lukas Wagner
2025-12-04 15:06 ` Christoph Heiss
2025-12-05 11:26 ` Christoph Heiss
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20251204125122.945961-12-c.heiss@proxmox.com \
--to=c.heiss@proxmox.com \
--cc=pdm-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox