From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 993C61FF17A for ; Tue, 9 Dec 2025 12:01:14 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 0E70724CBE; Tue, 9 Dec 2025 12:01:54 +0100 (CET) Mime-Version: 1.0 Date: Tue, 09 Dec 2025 12:01:36 +0100 Message-Id: From: "Lukas Wagner" To: "Proxmox Datacenter Manager development discussion" , "Christoph Heiss" X-Mailer: aerc 0.21.0-0-g5549850facc2-dirty References: <20251205112528.373387-1-c.heiss@proxmox.com> <20251205112528.373387-12-c.heiss@proxmox.com> In-Reply-To: <20251205112528.373387-12-c.heiss@proxmox.com> X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1765278090439 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.969 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_MAILER 2 Automated Mailer Tag Left in Email RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [p.id, p.country, proxmox.com, mod.rs, config.id, update.country, other.id] Subject: Re: [pdm-devel] [PATCH datacenter-manager v2 11/14] server: api: add auto-installer integration module X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Datacenter Manager development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" 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 > --- > 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 > + "#, > + &[&::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, > +) -> ApiResponseFuture { > + Box::pin(async move { > + let response = serde_json::from_value::(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 { > + 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> { > + 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> { > + let (prepared, digest) = pdm_config::auto_install::read_prepared_answers()?; > + > + rpcenv["digest"] = hex::encode(digest).into(); > + > + Ok(prepared > + .convert_to_typed_array::("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, > +) -> Result<()> { > + let _lock = pdm_config::auto_install::prepared_answers_write_lock(); > + let (mut prepared, _) = pdm_config::auto_install::read_prepared_answers()?; > + > + if prepared > + .lookup::("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::("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 { > + let (prepared, _) = pdm_config::auto_install::read_prepared_answers()?; > + > + if let Ok(mut p) = prepared.lookup::("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, > + delete: Option>, > + digest: Option, > +) -> 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::("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::("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> { > + 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::("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