From: Christoph Heiss <c.heiss@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager v2 11/14] server: api: add auto-installer integration module
Date: Fri, 5 Dec 2025 12:25:13 +0100 [thread overview]
Message-ID: <20251205112528.373387-12-c.heiss@proxmox.com> (raw)
In-Reply-To: <20251205112528.373387-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.
Changes v1 -> v2:
* fixed compilation error due to leftover, unresolved type
Cargo.toml | 2 +
debian/control | 2 +
server/Cargo.toml | 4 +
server/src/api/auto_installer/mod.rs | 653 +++++++++++++++++++++++++++
server/src/api/mod.rs | 2 +
5 files changed, 663 insertions(+)
create mode 100644 server/src/api/auto_installer/mod.rs
diff --git a/Cargo.toml b/Cargo.toml
index 5021531..5893db5 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..ec5752a
--- /dev/null
+++ b/server/src/api/auto_installer/mod.rs
@@ -0,0 +1,653 @@
+//! 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, 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<AutoInstallerConfig> {
+ 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(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-05 11:26 UTC|newest]
Thread overview: 18+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-12-05 11:25 [pdm-devel] [PATCH proxmox/datacenter-manager v2 00/14] initial auto-installer integration Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 01/14] api-macro: allow $ in identifier name Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 02/14] network-types: move `Fqdn` type from proxmox-installer-common Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 03/14] network-types: implement api type for Fqdn Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 04/14] network-types: add api wrapper type for std::net::IpAddr Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 05/14] installer-types: add common types used by the installer Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 06/14] installer-types: add types used by the auto-installer Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH proxmox v2 07/14] installer-types: implement api type for all externally-used types Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 08/14] api-types: add api types for auto-installer integration Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 09/14] config: add auto-installer configuration module Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 10/14] acl: wire up new /system/auto-installation acl path Christoph Heiss
2025-12-05 11:25 ` Christoph Heiss [this message]
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 12/14] ui: auto-installer: add installations overview panel Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 13/14] ui: auto-installer: add prepared answer configuration panel Christoph Heiss
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 14/14] docs: add documentation for auto-installer integration Christoph Heiss
2025-12-05 11:53 ` [pdm-devel] [PATCH proxmox/datacenter-manager v2 00/14] initial " Thomas Lamprecht
2025-12-05 15:50 ` Christoph Heiss
2025-12-05 15:57 ` Thomas Lamprecht
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=20251205112528.373387-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 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.