all lists on lists.proxmox.com
 help / color / mirror / Atom feed
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


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