From: "Lukas Wagner" <l.wagner@proxmox.com>
To: "Proxmox Datacenter Manager development discussion"
<pdm-devel@lists.proxmox.com>,
"Christoph Heiss" <c.heiss@proxmox.com>
Subject: Re: [pdm-devel] [PATCH datacenter-manager v2 11/14] server: api: add auto-installer integration module
Date: Tue, 09 Dec 2025 12:01:36 +0100 [thread overview]
Message-ID: <DETMUXY1Q877.32G593TWC52WW@proxmox.com> (raw)
In-Reply-To: <20251205112528.373387-12-c.heiss@proxmox.com>
Some notes inline.
On Fri Dec 5, 2025 at 12:25 PM CET, Christoph Heiss wrote:
> 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,
> +)
I think this is dangerous.
While the answer-file does not leak any passwords (seems like the root
password is hashed), it still contains semi-sensitive data (email address,
SSH key, FQDN, etc.). Also, since each POST creates a new entry in the
installations JSON file, an unauthenticated user could abuse this to
make the PDM system run out of disk space (or simply disturb operations
by creating bogus entries).
I think we need some sort of shared secret between PDM and
autoinstaller ISO. I briefly talked to Thomas about this; some ideas
that came to mind where:
- just use the API token system with a distinct ACL object/privilege
(allowing nothing else than retrieving the answer file and then
updating the status via the post-hook)
- Use some separate secret - the handler would still keep
Permission::World, but there would need to be some separate parameter
(or header) with a key. While this might seem a bad idea initially,
since there are already API tokens which integrate nicely with out
stack, this would solve a particular problem:
User error might lead to one to generate a 'more powerful' token than needed
(worst case, a root@pam token with full privileges) which is then
embedded into the ISO. Users might not be fully aware or simply
forget that this particular ISO contains a secret; it could be
stored unprotected (e.g. some kind of network storage).
Using a secret that *only* works with the autoinstaller integration somewhat
mitigates the risk.
- mutual TLS / client certificates - seems to be not uncommon in the
sysadmin/devops space - could be evaluated here.
> +.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;
proxmox_time::epoch_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;
> + }
> + }
> + }
> + }
One pattern that I've found very useful for these PUT handlers is the
one I used in [1]. It's a nice guarantee that one does not 'forget' to
handle any fields in the updater (either it fails to compile, or at
least shows a warning).
[1] https://git.proxmox.com/?p=proxmox.git;a=blob;f=proxmox-notify/src/api/webhook.rs;h=9d904d0bf57f9f789bb6723e1d8ca710fcf0cb96;hb=HEAD#l175
> +
> + 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,
Same thing here with regards to the use of a secret.
> + },
> +)]
> +/// 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),
_______________________________________________
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-09 11:01 UTC|newest]
Thread overview: 35+ 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-09 9:13 ` Lukas Wagner
2025-12-09 12:26 ` 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-09 9:13 ` Lukas Wagner
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-09 9:16 ` Lukas Wagner
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-09 9:35 ` Lukas Wagner
2025-12-09 12:17 ` 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-09 9:44 ` Lukas Wagner
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-09 9:52 ` Lukas Wagner
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-09 10:03 ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 09/14] config: add auto-installer configuration module Christoph Heiss
2025-12-09 10:22 ` Lukas Wagner
2025-12-09 12:10 ` 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-09 10:23 ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 11/14] server: api: add auto-installer integration module Christoph Heiss
2025-12-09 11:01 ` Lukas Wagner [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-09 12:35 ` Lukas Wagner
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-09 13:01 ` Lukas Wagner
2025-12-05 11:25 ` [pdm-devel] [PATCH datacenter-manager v2 14/14] docs: add documentation for auto-installer integration Christoph Heiss
2025-12-09 13:12 ` Lukas Wagner
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
2025-12-09 13:38 ` Lukas Wagner
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=DETMUXY1Q877.32G593TWC52WW@proxmox.com \
--to=l.wagner@proxmox.com \
--cc=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