From: Christoph Heiss <c.heiss@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [PATCH datacenter-manager v4 16/40] config: add auto-installer configuration module
Date: Thu, 30 Apr 2026 14:46:45 +0200 [thread overview]
Message-ID: <20260430124712.1614305-17-c.heiss@proxmox.com> (raw)
In-Reply-To: <20260430124712.1614305-1-c.heiss@proxmox.com>
Provides some primitives for the auto-installer integration to save
state about
- individual installations (as plain JSON, as these aren't
configurations files) and
- prepared answer file configurations, as section config
The new files (including lock files) are placed under
`/etc/proxmox-datacenter-manager/autoinst`.
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
Changes v3 -> v4:
* fix schema for PreparedInstallationSectionConfig::disk_list
* rename `AnswerAuthToken*` -> `AnswerToken*`
Changes v2 -> v3:
* added answer authentication token interface
* added separate type from `PreparedInstallationConfig` api type for
use with configuration, mapping maps to propertystrings
* moved state files to /var/lib/proxmox-datacenter-manager
Changes v1 -> v2:
* no changes
lib/pdm-buildcfg/src/lib.rs | 10 +
lib/pdm-config/Cargo.toml | 3 +
lib/pdm-config/src/auto_install.rs | 575 +++++++++++++++++++++++++++++
lib/pdm-config/src/lib.rs | 1 +
lib/pdm-config/src/setup.rs | 7 +
5 files changed, 596 insertions(+)
create mode 100644 lib/pdm-config/src/auto_install.rs
diff --git a/lib/pdm-buildcfg/src/lib.rs b/lib/pdm-buildcfg/src/lib.rs
index 9380972..734e95d 100644
--- a/lib/pdm-buildcfg/src/lib.rs
+++ b/lib/pdm-buildcfg/src/lib.rs
@@ -106,3 +106,13 @@ macro_rules! rundir {
concat!($crate::PDM_RUN_DIR_M!(), $subdir)
};
}
+
+/// Prepend the state directory to a file name.
+///
+/// This is a simply way to get the full path for files in `/var/lib/`.
+#[macro_export]
+macro_rules! statedir {
+ ($subdir:expr) => {
+ concat!($crate::PDM_STATE_DIR_M!(), $subdir)
+ };
+}
diff --git a/lib/pdm-config/Cargo.toml b/lib/pdm-config/Cargo.toml
index d39c2ad..17ca27e 100644
--- a/lib/pdm-config/Cargo.toml
+++ b/lib/pdm-config/Cargo.toml
@@ -12,6 +12,7 @@ nix.workspace = true
once_cell.workspace = true
openssl.workspace = true
serde.workspace = true
+serde_json.workspace = true
proxmox-config-digest = { workspace = true, features = [ "openssl" ] }
proxmox-http = { workspace = true, features = [ "http-helpers" ] }
@@ -23,5 +24,7 @@ proxmox-shared-memory.workspace = true
proxmox-simple-config.workspace = true
proxmox-sys = { workspace = true, features = [ "acl", "crypt", "timer" ] }
proxmox-acme-api.workspace = true
+proxmox-serde.workspace = true
+proxmox-network-types.workspace = true
pdm-api-types.workspace = true
pdm-buildcfg.workspace = true
diff --git a/lib/pdm-config/src/auto_install.rs b/lib/pdm-config/src/auto_install.rs
new file mode 100644
index 0000000..e1d1dc9
--- /dev/null
+++ b/lib/pdm-config/src/auto_install.rs
@@ -0,0 +1,575 @@
+//! Implements configuration for the auto-installer integration.
+
+use anyhow::{anyhow, bail, Result};
+use std::collections::HashMap;
+
+use pdm_api_types::{
+ auto_installer::{AnswerToken, Installation},
+ ConfigDigest,
+};
+use proxmox_product_config::{open_api_lockfile, replace_config, ApiLockGuard};
+use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData};
+
+use crate::auto_install::types::AnswerTokenWrapper;
+
+pub const CONFIG_PATH: &str = pdm_buildcfg::configdir!("/autoinst");
+
+const PREPARED_CONF_FILE: &str = pdm_buildcfg::configdir!("/autoinst/prepared.cfg");
+const PREPARED_LOCK_FILE: &str = pdm_buildcfg::configdir!("/autoinst/.prepared.lock");
+
+const TOKENS_CONF_FILE: &str = pdm_buildcfg::configdir!("/autoinst/tokens.cfg");
+const TOKENS_SHADOW_FILE: &str = pdm_buildcfg::configdir!("/autoinst/tokens.shadow");
+const TOKENS_LOCK_FILE: &str = pdm_buildcfg::configdir!("/autoinst/.tokens.lock");
+
+const INSTALLATIONS_STATE_FILE: &str = pdm_buildcfg::statedir!("/automated-installations.json");
+const INSTALLATIONS_LOCK_FILE: &str = pdm_buildcfg::statedir!("/.automated-installations.lock");
+
+pub mod types {
+ use serde::{Deserialize, Serialize};
+ use std::{collections::BTreeMap, fmt::Debug};
+
+ use pdm_api_types::{
+ auto_installer::{
+ answer, AnswerToken, DiskSelectionMode, PreparedInstallationConfig,
+ PREPARED_INSTALL_CONFIG_ID_SCHEMA,
+ },
+ BLOCKDEVICE_NAME_SCHEMA, CERT_FINGERPRINT_SHA256_SCHEMA, HTTP_URL_SCHEMA,
+ PROXMOX_TOKEN_NAME_SCHEMA, SINGLE_LINE_COMMENT_FORMAT,
+ };
+ use proxmox_network_types::{api_types::IpAddr, Cidr};
+ use proxmox_schema::{api, ApiStringFormat, ApiType, PropertyString};
+
+ #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
+ /// API wrapper for a [`BTreeMap`].
+ pub struct BTreeMapWrapper<T>(BTreeMap<String, T>);
+
+ impl<T> std::ops::Deref for BTreeMapWrapper<T> {
+ type Target = BTreeMap<String, T>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+ }
+
+ impl<T> std::ops::DerefMut for BTreeMapWrapper<T> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+ }
+
+ impl<T: for<'de> Deserialize<'de> + Serialize> ApiType for BTreeMapWrapper<T> {
+ const API_SCHEMA: proxmox_schema::Schema =
+ proxmox_schema::ObjectSchema::new("Map of key-value pairs", &[])
+ .additional_properties(true)
+ .schema();
+ }
+
+ #[api(
+ "id-property": "id",
+ "id-schema": {
+ type: String,
+ description: "ID of prepared configuration for automated installations.",
+ min_length: 3,
+ max_length: 64
+ }
+ )]
+ #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
+ #[serde(rename_all = "kebab-case", tag = "type")]
+ /// Wrapper type for using [`PreparedInstallationConfig`] with
+ /// [`proxmox_schema::typed::SectionConfigData`].
+ pub enum PreparedInstallationSectionConfigWrapper {
+ /// Prepared Installation Configuration.
+ PreparedConfig(PreparedInstallationSectionConfig),
+ }
+
+ #[api(
+ properties: {
+ id: {
+ schema: PREPARED_INSTALL_CONFIG_ID_SCHEMA,
+ },
+ "authorized-tokens": {
+ type: Array,
+ optional: true,
+ items: {
+ schema: PROXMOX_TOKEN_NAME_SCHEMA,
+ },
+ },
+ "is-default": {
+ optional: true,
+ },
+ "target-filter": {
+ type: String,
+ optional: true,
+ },
+ country: {
+ format: &ApiStringFormat::Pattern(&answer::COUNTRY_CODE_REGEX),
+ min_length: 2,
+ max_length: 2,
+ },
+ mailto: {
+ min_length: 2,
+ max_length: 256,
+ format: &SINGLE_LINE_COMMENT_FORMAT,
+ },
+ "root-ssh-keys": {
+ type: Array,
+ optional: true,
+ items: {
+ type: String,
+ description: "SSH public key.",
+ },
+ },
+ "netdev-filter": {
+ type: String,
+ optional: true,
+ },
+ filesystem: {
+ type: String,
+ },
+ "disk-mode": {
+ type: String,
+ },
+ "disk-list": {
+ type: Array,
+ optional: true,
+ items: {
+ schema: BLOCKDEVICE_NAME_SCHEMA,
+ },
+ },
+ "disk-filter": {
+ type: String,
+ optional: true,
+ },
+ "post-hook-base-url": {
+ schema: HTTP_URL_SCHEMA,
+ optional: true,
+ },
+ "post-hook-cert-fp": {
+ schema: CERT_FINGERPRINT_SHA256_SCHEMA,
+ optional: true,
+ },
+ "template-counters": {
+ type: String,
+ optional: true,
+ },
+ },
+ )]
+ #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
+ #[serde(rename_all = "kebab-case")]
+ /// Configuration describing an automated installation.
+ ///
+ /// Certain fields support simple templating via [Handlebars]. Currently, following fields will
+ /// resolve handlebars expression upon instantiation of an answer:
+ ///
+ /// * `fqdn`
+ /// * `mailto`
+ /// * `cidr`
+ /// * `gateway`
+ /// * `dns
+ ///
+ /// [Handlebars]: https://handlebarsjs.com/guide/
+ pub struct PreparedInstallationSectionConfig {
+ pub id: String,
+
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ /// List of token IDs that are authoried to retrieve this answer.
+ pub authorized_tokens: Vec<String>,
+
+ /// Whether this is the default answer. There can only ever be one default answer.
+ /// `target_filter` below is ignored if this is `true`.
+ pub is_default: bool,
+
+ // Target filters
+ /// Map of filters for matching against a property in [`answer::fetch::AnswerFetchData`].
+ /// The keys are JSON Pointers as per [RFC6901], the values globs as accepted
+ /// by the [glob] crate.
+ ///
+ /// Used to check this configuration against incoming automated installation requests. If this
+ /// is unset, it will match any installation not matched "narrower" by other prepared
+ /// configurations, thus being the default.
+ ///
+ /// [RFC6901] https://datatracker.ietf.org/doc/html/rfc6901
+ /// [glob crate] https://docs.rs/glob/
+ #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
+ pub target_filter: PropertyString<BTreeMapWrapper<String>>,
+
+ // Keys from [`answer::GlobalOptions`], adapted to better fit the API and model of the UI.
+ /// Country to use for apt mirrors.
+ pub country: String,
+ /// FQDN to set for the installed system. Only used if `use_dhcp_fqdn` is true.
+ ///
+ /// Supports templating via Handlebars.
+ /// The [`proxmox_network_types::fqdn::Fqdn`] type cannot be used here
+ /// because of that, as curly brackets are not valid in hostnames.
+ pub fqdn: String,
+ /// Whether to use the FQDN from the DHCP lease or the user-provided one.
+ pub use_dhcp_fqdn: bool,
+ /// Keyboard layout to set.
+ pub keyboard: answer::KeyboardLayout,
+ /// Mail address for `root@pam`.
+ ///
+ /// Supports templating via Handlebars.
+ pub mailto: String,
+ /// Timezone to set on the new system.
+ pub timezone: String,
+ /// Pre-hashed password to set for the `root` PAM account.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub root_password_hashed: Option<String>,
+ /// Whether to reboot the machine if an error occurred during the
+ /// installation.
+ pub reboot_on_error: bool,
+ /// Action to take after the installation completed successfully.
+ pub reboot_mode: answer::RebootMode,
+ /// Newline-separated list of public SSH keys to set up for the `root` PAM account.
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub root_ssh_keys: Vec<String>,
+
+ // Keys from [`answer::NetworkConfig`], adapted to better fit the API and model of the UI.
+ /// Whether to use the network configuration from the DHCP lease or not.
+ pub use_dhcp_network: bool,
+ /// IP address and netmask if not using DHCP.
+ ///
+ /// Supports templating via Handlebars.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub cidr: Option<Cidr>,
+ /// Gateway if not using DHCP.
+ ///
+ /// Supports templating via Handlebars.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub gateway: Option<IpAddr>,
+ /// DNS server address if not using DHCP.
+ ///
+ /// Supports templating via Handlebars.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub dns: Option<IpAddr>,
+ /// Filter for network devices, to select a specific management interface.
+ #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
+ pub netdev_filter: PropertyString<BTreeMapWrapper<String>>,
+ /// Whether to enable network interface name pinning.
+ pub netif_name_pinning_enabled: bool,
+
+ /// Root filesystem options.
+ pub filesystem: PropertyString<answer::FilesystemOptions>,
+
+ /// Whether to use the fixed disk list or select disks dynamically by udev filters.
+ pub disk_mode: DiskSelectionMode,
+ /// List of raw disk identifiers to use for the root filesystem.
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub disk_list: Vec<String>,
+ /// Filter against udev properties to select the disks for the installation,
+ /// to allow dynamic selection of disks.
+ #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
+ pub disk_filter: PropertyString<BTreeMapWrapper<String>>,
+ /// Whether it is enough that any filter matches on a disk or all given
+ /// filters must match to select a disk. Only used if `disk_list` is unset.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub disk_filter_match: Option<answer::FilterMatch>,
+
+ /// Post installations hook base URL, i.e. host PDM is reachable as from
+ /// the target machine.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub post_hook_base_url: Option<String>,
+ /// Post hook certificate fingerprint, if needed.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub post_hook_cert_fp: Option<String>,
+
+ /// Key-value pairs of (auto-incrementing) counters.
+ #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
+ pub template_counters: PropertyString<BTreeMapWrapper<i32>>,
+ }
+
+ impl TryFrom<PreparedInstallationConfig> for PreparedInstallationSectionConfig {
+ type Error = anyhow::Error;
+
+ fn try_from(conf: PreparedInstallationConfig) -> Result<Self, Self::Error> {
+ Ok(Self {
+ id: conf.id,
+ authorized_tokens: conf.authorized_tokens,
+ // target filter
+ is_default: conf.is_default,
+ target_filter: PropertyString::new(BTreeMapWrapper(conf.target_filter)),
+ // global options
+ country: conf.country,
+ fqdn: conf.fqdn,
+ use_dhcp_fqdn: conf.use_dhcp_fqdn,
+ keyboard: conf.keyboard,
+ mailto: conf.mailto,
+ timezone: conf.timezone,
+ root_password_hashed: conf.root_password_hashed,
+ reboot_on_error: conf.reboot_on_error,
+ reboot_mode: conf.reboot_mode,
+ root_ssh_keys: conf.root_ssh_keys,
+ // network options
+ use_dhcp_network: conf.use_dhcp_network,
+ cidr: conf.cidr,
+ gateway: conf.gateway,
+ dns: conf.dns,
+ netdev_filter: PropertyString::new(BTreeMapWrapper(conf.netdev_filter)),
+ netif_name_pinning_enabled: conf.netif_name_pinning_enabled,
+ // disk options
+ filesystem: PropertyString::new(conf.filesystem),
+ disk_mode: conf.disk_mode,
+ disk_list: conf.disk_list,
+ disk_filter: PropertyString::new(BTreeMapWrapper(conf.disk_filter)),
+ disk_filter_match: conf.disk_filter_match,
+ // post hook
+ post_hook_base_url: conf.post_hook_base_url,
+ post_hook_cert_fp: conf.post_hook_cert_fp,
+ // templating
+ template_counters: PropertyString::new(BTreeMapWrapper(conf.template_counters)),
+ })
+ }
+ }
+
+ impl TryFrom<PreparedInstallationConfig> for PreparedInstallationSectionConfigWrapper {
+ type Error = anyhow::Error;
+
+ fn try_from(conf: PreparedInstallationConfig) -> Result<Self, Self::Error> {
+ Ok(Self::PreparedConfig(conf.try_into()?))
+ }
+ }
+
+ impl TryInto<PreparedInstallationConfig> for PreparedInstallationSectionConfig {
+ type Error = anyhow::Error;
+
+ fn try_into(self) -> Result<PreparedInstallationConfig, Self::Error> {
+ Ok(PreparedInstallationConfig {
+ id: self.id,
+ authorized_tokens: self.authorized_tokens,
+ // target filter
+ is_default: self.is_default,
+ target_filter: self.target_filter.into_inner().0,
+ // global options
+ country: self.country,
+ fqdn: self.fqdn,
+ use_dhcp_fqdn: self.use_dhcp_fqdn,
+ keyboard: self.keyboard,
+ mailto: self.mailto,
+ timezone: self.timezone,
+ root_password_hashed: self.root_password_hashed,
+ reboot_on_error: self.reboot_on_error,
+ reboot_mode: self.reboot_mode,
+ root_ssh_keys: self.root_ssh_keys,
+ // network options
+ use_dhcp_network: self.use_dhcp_network,
+ cidr: self.cidr,
+ gateway: self.gateway,
+ dns: self.dns,
+ netdev_filter: self.netdev_filter.into_inner().0,
+ netif_name_pinning_enabled: self.netif_name_pinning_enabled,
+ // disk options
+ filesystem: self.filesystem.into_inner(),
+ disk_mode: self.disk_mode,
+ disk_list: self.disk_list,
+ disk_filter: self.disk_filter.into_inner().0,
+ disk_filter_match: self.disk_filter_match,
+ // post hook
+ post_hook_base_url: self.post_hook_base_url,
+ post_hook_cert_fp: self.post_hook_cert_fp,
+ // templating
+ template_counters: self.template_counters.into_inner().0,
+ })
+ }
+ }
+
+ impl TryInto<PreparedInstallationConfig> for PreparedInstallationSectionConfigWrapper {
+ type Error = anyhow::Error;
+
+ fn try_into(self) -> Result<PreparedInstallationConfig, Self::Error> {
+ let PreparedInstallationSectionConfigWrapper::PreparedConfig(conf) = self;
+ conf.try_into()
+ }
+ }
+
+ #[api(
+ "id-property": "id",
+ "id-schema": {
+ type: String,
+ description: "Access token name.",
+ min_length: 3,
+ max_length: 64,
+ },
+ )]
+ #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
+ #[serde(rename_all = "kebab-case", tag = "type")]
+ /// Access token for authenticating against the /answer endpoint.
+ pub enum AnswerTokenWrapper {
+ /// Simple token with a secret, like an API token.
+ Token(AnswerToken),
+ }
+
+ impl From<AnswerTokenWrapper> for AnswerToken {
+ fn from(value: AnswerTokenWrapper) -> Self {
+ let AnswerTokenWrapper::Token(token) = value;
+ token
+ }
+ }
+
+ impl From<AnswerToken> for AnswerTokenWrapper {
+ fn from(value: AnswerToken) -> Self {
+ AnswerTokenWrapper::Token(value)
+ }
+ }
+}
+
+pub fn installations_read_lock() -> Result<ApiLockGuard> {
+ open_api_lockfile(INSTALLATIONS_LOCK_FILE, None, false)
+}
+
+pub fn installations_write_lock() -> Result<ApiLockGuard> {
+ open_api_lockfile(INSTALLATIONS_LOCK_FILE, None, true)
+}
+
+pub fn read_installations() -> Result<(Vec<Installation>, ConfigDigest)> {
+ let content: serde_json::Value = serde_json::from_str(
+ &proxmox_sys::fs::file_read_optional_string(INSTALLATIONS_STATE_FILE)?
+ .unwrap_or_else(|| "[]".to_owned()),
+ )?;
+
+ let digest = proxmox_serde::json::to_canonical_json(&content).map(ConfigDigest::from_slice)?;
+ let data = serde_json::from_value(content)?;
+
+ Ok((data, digest))
+}
+
+/// Write lock must be already held.
+pub fn save_installations(config: &[Installation]) -> Result<()> {
+ let raw = serde_json::to_string(&config)?;
+ replace_config(INSTALLATIONS_STATE_FILE, raw.as_bytes())
+}
+
+pub fn prepared_answers_read_lock() -> Result<ApiLockGuard> {
+ open_api_lockfile(PREPARED_LOCK_FILE, None, false)
+}
+
+pub fn prepared_answers_write_lock() -> Result<ApiLockGuard> {
+ open_api_lockfile(PREPARED_LOCK_FILE, None, true)
+}
+
+pub fn read_prepared_answers() -> Result<(
+ SectionConfigData<types::PreparedInstallationSectionConfigWrapper>,
+ ConfigDigest,
+)> {
+ let content =
+ proxmox_sys::fs::file_read_optional_string(PREPARED_CONF_FILE)?.unwrap_or_default();
+
+ let digest = ConfigDigest::from_slice(content.as_bytes());
+ let data = types::PreparedInstallationSectionConfigWrapper::parse_section_config(
+ PREPARED_CONF_FILE,
+ &content,
+ )?;
+
+ Ok((data, digest))
+}
+
+/// Write lock must be already held.
+pub fn save_prepared_answers(
+ config: &SectionConfigData<types::PreparedInstallationSectionConfigWrapper>,
+) -> Result<()> {
+ let raw = types::PreparedInstallationSectionConfigWrapper::write_section_config(
+ PREPARED_CONF_FILE,
+ config,
+ )?;
+ replace_config(PREPARED_CONF_FILE, raw.as_bytes())
+}
+
+pub fn tokens_read_lock() -> Result<ApiLockGuard> {
+ open_api_lockfile(TOKENS_LOCK_FILE, None, false)
+}
+
+pub fn tokens_write_lock() -> Result<ApiLockGuard> {
+ open_api_lockfile(TOKENS_LOCK_FILE, None, true)
+}
+
+pub fn read_tokens() -> Result<(SectionConfigData<types::AnswerTokenWrapper>, ConfigDigest)> {
+ let content = proxmox_sys::fs::file_read_optional_string(TOKENS_CONF_FILE)?.unwrap_or_default();
+
+ let digest = ConfigDigest::from_slice(content.as_bytes());
+ let data = types::AnswerTokenWrapper::parse_section_config(TOKENS_CONF_FILE, &content)?;
+
+ Ok((data, digest))
+}
+
+fn write_tokens(data: SectionConfigData<AnswerTokenWrapper>) -> Result<()> {
+ let raw = AnswerTokenWrapper::write_section_config(TOKENS_CONF_FILE, &data)?;
+ replace_config(TOKENS_CONF_FILE, raw.as_bytes())
+}
+
+/// Write lock must be already held.
+pub fn add_token(token: &AnswerToken, value: &str) -> Result<()> {
+ let (mut tokens, _) = read_tokens()?;
+
+ if tokens.contains_key(&token.id) {
+ bail!("token already exists");
+ }
+
+ tokens.insert(token.id.clone(), token.clone().into());
+ write_tokens(tokens)?;
+
+ let mut shadow = read_token_shadow()?;
+ let hashed = proxmox_sys::crypt::encrypt_pw(value)?;
+ shadow.insert(token.id.clone(), hashed);
+ write_token_shadow(shadow)
+}
+
+/// Write lock must be already held.
+pub fn update_token(token: &AnswerToken) -> Result<()> {
+ let (mut tokens, _) = read_tokens()?;
+
+ if tokens.contains_key(&token.id) {
+ bail!("unknown token: {}", token.id);
+ }
+
+ tokens.insert(token.id.clone(), token.clone().into());
+ write_tokens(tokens)?;
+ Ok(())
+}
+
+/// Write lock must be already held.
+pub fn update_token_shadow(id: &str, secret: &str) -> Result<()> {
+ let mut shadow = read_token_shadow()?;
+ if !shadow.contains_key(id) {
+ bail!("unknown token: {id}");
+ }
+
+ let hashed = proxmox_sys::crypt::encrypt_pw(secret)?;
+ shadow.insert(id.to_owned(), hashed);
+ write_token_shadow(shadow)
+}
+
+/// Write lock must be already held.
+pub fn delete_token(id: &str) -> Result<()> {
+ let (mut tokens, _) = read_tokens()?;
+
+ if !tokens.contains_key(id) {
+ bail!("unknown token: {id}");
+ }
+
+ tokens.remove(id);
+ write_tokens(tokens)?;
+
+ let mut shadow = read_token_shadow()?;
+ shadow.remove(id);
+ write_token_shadow(shadow)
+}
+
+/// At least read lock must be held.
+pub fn verify_secret(id: &str, secret: &str) -> Result<()> {
+ read_token_shadow()?
+ .get(id)
+ .and_then(|hashed| proxmox_sys::crypt::verify_crypt_pw(secret, hashed).ok())
+ .ok_or_else(|| anyhow!("invalid access secret"))
+}
+
+fn read_token_shadow() -> Result<HashMap<String, String>> {
+ Ok(serde_json::from_str(
+ &proxmox_sys::fs::file_read_optional_string(TOKENS_SHADOW_FILE)?
+ .unwrap_or_else(|| "{}".to_owned()),
+ )?)
+}
+
+/// Write lock must be already held.
+fn write_token_shadow(data: HashMap<String, String>) -> Result<()> {
+ let raw = serde_json::to_string(&data)?;
+ replace_config(TOKENS_SHADOW_FILE, raw.as_bytes())
+}
diff --git a/lib/pdm-config/src/lib.rs b/lib/pdm-config/src/lib.rs
index 4c49054..5b9bcca 100644
--- a/lib/pdm-config/src/lib.rs
+++ b/lib/pdm-config/src/lib.rs
@@ -2,6 +2,7 @@ use anyhow::{format_err, Error};
use nix::unistd::{Gid, Group, Uid, User};
pub use pdm_buildcfg::{BACKUP_GROUP_NAME, BACKUP_USER_NAME};
+pub mod auto_install;
pub mod certificate_config;
pub mod domains;
pub mod node;
diff --git a/lib/pdm-config/src/setup.rs b/lib/pdm-config/src/setup.rs
index 5f920c8..5adb05f 100644
--- a/lib/pdm-config/src/setup.rs
+++ b/lib/pdm-config/src/setup.rs
@@ -24,6 +24,13 @@ pub fn create_configdir() -> Result<(), Error> {
0o750,
)?;
+ mkdir_perms(
+ crate::auto_install::CONFIG_PATH,
+ api_user.uid,
+ api_user.gid,
+ 0o750,
+ )?;
+
Ok(())
}
--
2.53.0
next prev parent reply other threads:[~2026-04-30 12:49 UTC|newest]
Thread overview: 41+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-30 12:46 [PATCH datacenter-manager/installer/proxmox/yew-comp v4 00/40] add auto-installer integration Christoph Heiss
2026-04-30 12:46 ` [PATCH proxmox v4 01/40] api-macro: allow $ in identifier name Christoph Heiss
2026-04-30 12:46 ` [PATCH proxmox v4 02/40] schema: oneOf: allow single string variant Christoph Heiss
2026-04-30 12:46 ` [PATCH proxmox v4 03/40] schema: implement UpdaterType for HashMap and BTreeMap Christoph Heiss
2026-04-30 12:46 ` [PATCH proxmox v4 04/40] network-types: move `Fqdn` type from proxmox-installer-common Christoph Heiss
2026-04-30 12:46 ` [PATCH proxmox v4 05/40] network-types: implement api type for Fqdn Christoph Heiss
2026-04-30 12:46 ` [PATCH proxmox v4 06/40] network-types: add api wrapper type for std::net::IpAddr Christoph Heiss
2026-04-30 12:46 ` [PATCH proxmox v4 07/40] network-types: cidr: implement generic `IpAddr::new` constructor Christoph Heiss
2026-04-30 12:46 ` [PATCH proxmox v4 08/40] network-types: fqdn: implement standard library Error for Fqdn Christoph Heiss
2026-04-30 12:46 ` [PATCH proxmox v4 09/40] node-status: make KernelVersionInformation Clone + PartialEq Christoph Heiss
2026-04-30 12:46 ` [PATCH proxmox v4 10/40] installer-types: add common types used by the installer Christoph Heiss
2026-04-30 12:46 ` [PATCH proxmox v4 11/40] installer-types: add types used by the auto-installer Christoph Heiss
2026-04-30 12:46 ` [PATCH proxmox v4 12/40] installer-types: implement api type for all externally-used types Christoph Heiss
2026-04-30 12:46 ` [PATCH yew-comp v4 13/40] widget: kvlist: add widget for user-modifiable data tables Christoph Heiss
2026-04-30 12:46 ` [PATCH datacenter-manager v4 14/40] api-types, cli: use ReturnType::new() instead of constructing it manually Christoph Heiss
2026-04-30 12:46 ` [PATCH datacenter-manager v4 15/40] api-types: add api types for auto-installer integration Christoph Heiss
2026-04-30 12:46 ` Christoph Heiss [this message]
2026-04-30 12:46 ` [PATCH datacenter-manager v4 17/40] acl: wire up new /system/auto-installation acl path Christoph Heiss
2026-04-30 12:46 ` [PATCH datacenter-manager v4 18/40] server: api: add auto-installer integration module Christoph Heiss
2026-04-30 12:46 ` [PATCH datacenter-manager v4 19/40] server: api: auto-installer: add access token management endpoints Christoph Heiss
2026-04-30 12:46 ` [PATCH datacenter-manager v4 20/40] client: add bindings for auto-installer endpoints Christoph Heiss
2026-04-30 12:46 ` [PATCH datacenter-manager v4 21/40] ui: auto-installer: add installations overview panel Christoph Heiss
2026-04-30 12:46 ` [PATCH datacenter-manager v4 22/40] ui: auto-installer: add prepared answer configuration panel Christoph Heiss
2026-04-30 12:46 ` [PATCH datacenter-manager v4 23/40] ui: auto-installer: add access token " Christoph Heiss
2026-04-30 12:46 ` [PATCH datacenter-manager v4 24/40] docs: add documentation for auto-installer integration Christoph Heiss
2026-04-30 12:46 ` [PATCH installer v4 25/40] install: iso env: use JSON boolean literals for product config Christoph Heiss
2026-04-30 12:46 ` [PATCH installer v4 26/40] common: http: allow passing custom headers to post() Christoph Heiss
2026-04-30 12:46 ` [PATCH installer v4 27/40] common: http: retrieve error message from body on post() Christoph Heiss
2026-04-30 12:46 ` [PATCH installer v4 28/40] common: options: move regex construction out of loop Christoph Heiss
2026-04-30 12:46 ` [PATCH installer v4 29/40] assistant: support adding an authorization token for HTTP-based answers Christoph Heiss
2026-04-30 12:46 ` [PATCH installer v4 30/40] post-hook: run cargo fmt Christoph Heiss
2026-04-30 12:47 ` [PATCH installer v4 31/40] tree-wide: used moved `Fqdn` type to proxmox-network-types Christoph Heiss
2026-04-30 12:47 ` [PATCH installer v4 32/40] tree-wide: use `Cidr` type from proxmox-network-types Christoph Heiss
2026-04-30 12:47 ` [PATCH installer v4 33/40] tree-wide: switch to filesystem types from proxmox-installer-types Christoph Heiss
2026-04-30 12:47 ` [PATCH installer v4 34/40] auto: sysinfo: switch to " Christoph Heiss
2026-04-30 12:47 ` [PATCH installer v4 35/40] fetch-answer: " Christoph Heiss
2026-04-30 12:47 ` [PATCH installer v4 36/40] fetch-answer: http: prefer json over toml for answer format Christoph Heiss
2026-04-30 12:47 ` [PATCH installer v4 37/40] fetch-answer: send auto-installer HTTP authorization token if set Christoph Heiss
2026-04-30 12:47 ` [PATCH installer v4 38/40] fetch-answer: print full error messages when fetching failed Christoph Heiss
2026-04-30 12:47 ` [PATCH installer v4 39/40] tree-wide: switch out `Answer` -> `AutoInstallerConfig` types Christoph Heiss
2026-04-30 12:47 ` [PATCH installer v4 40/40] auto: drop now-dead answer file definitions Christoph Heiss
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260430124712.1614305-17-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.