]> git.proxmox.com Git - proxmox-offline-mirror.git/blobdiff - src/config.rs
fix #4259: mirror: add ignore-errors option
[proxmox-offline-mirror.git] / src / config.rs
index 99c5592308fb157fd9ec1c87e6eb6b5b9079395d..cb9a22b5c6a79529d0be4a2092912a97735f2fed 100644 (file)
-use std::fmt::Debug;
+use std::path::Path;
 
-use proxmox_apt::repositories::APTRepository;
+use anyhow::{bail, Error};
+use lazy_static::lazy_static;
+use proxmox_subscription::{sign::ServerBlob, SubscriptionInfo};
+use serde::{Deserialize, Serialize};
 
-#[derive(Debug)]
+use proxmox_schema::{api, ApiType, Schema, Updater};
+use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin};
+use proxmox_sys::fs::{replace_file, CreateOptions};
+
+use crate::types::{
+    ProductType, MEDIA_ID_SCHEMA, MIRROR_ID_SCHEMA, PROXMOX_SERVER_ID_SCHEMA,
+    PROXMOX_SUBSCRIPTION_KEY_SCHEMA,
+};
+
+#[api(
+    properties: {
+        id: {
+            schema: MIRROR_ID_SCHEMA,
+        },
+        repository: {
+            type: String,
+        },
+        architectures: {
+            type: Array,
+            items: {
+                type: String,
+                description: "Architecture specifier.",
+            },
+        },
+        "base-dir": {
+            type: String,
+        },
+        "key-path": {
+            type: String,
+        },
+        verify: {
+            type: bool,
+        },
+        sync: {
+            type: bool,
+        },
+        "ignore-errors": {
+            type: bool,
+            optional: true,
+            default: false,
+        },
+    }
+)]
+#[derive(Clone, Debug, Serialize, Deserialize, Updater)]
+#[serde(rename_all = "kebab-case")]
+/// Configuration entry for a mirrored repository.
 pub struct MirrorConfig {
-    pub repository: APTRepository,
+    #[updater(skip)]
+    /// Identifier for this entry.
+    pub id: String,
+    /// Single repository definition in sources.list format.
+    pub repository: String,
+    /// List of architectures that should be mirrored.
     pub architectures: Vec<String>,
-    pub path: String,
-    pub key: Vec<u8>,
+    /// Path to directory containg mirrored repository pool. Can be shared by multiple mirrors.
+    pub base_dir: String,
+    /// Path to public key file for verifying repository integrity.
+    pub key_path: String,
+    /// Whether to verify existing files or assume they are valid (IO-intensive).
+    pub verify: bool,
+    /// Whether to write new files using FSYNC.
+    pub sync: bool,
+    /// Use subscription key to access (required for Proxmox Enterprise repositories).
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub use_subscription: Option<ProductType>,
+    /// Whether to downgrade download errors to warnings
+    #[serde(default)]
+    pub ignore_errors: bool,
+}
+
+#[api(
+    properties: {
+        id: {
+            schema: MEDIA_ID_SCHEMA,
+        },
+        mountpoint: {
+            type: String,
+        },
+        verify: {
+            type: bool,
+        },
+        sync: {
+            type: bool,
+        },
+        mirrors: {
+            type: Array,
+            items: {
+                schema: MIRROR_ID_SCHEMA,
+            },
+        },
+    }
+)]
+#[derive(Debug, Serialize, Deserialize, Updater)]
+#[serde(rename_all = "kebab-case")]
+/// Configuration entry for an external medium.
+pub struct MediaConfig {
+    #[updater(skip)]
+    /// Identifier for this entry.
+    pub id: String,
+    /// Mountpoint where medium is available on mirroring system.
+    pub mountpoint: String,
+    /// List of [MirrorConfig] IDs which should be synced to medium.
+    pub mirrors: Vec<String>,
+    /// Whether to verify existing files or assume they are valid (IO-intensive).
+    pub verify: bool,
+    /// Whether to write new files using FSYNC.
+    pub sync: bool,
+}
+
+#[api(
+    properties: {
+        key: {
+            schema: PROXMOX_SUBSCRIPTION_KEY_SCHEMA,
+        },
+        "server-id": {
+            schema: PROXMOX_SERVER_ID_SCHEMA,
+        },
+        description: {
+            type: String,
+            optional: true,
+        },
+        info: {
+            type: String,
+            description: "base64 encoded subscription info - update with 'refresh' command.",
+            optional: true,
+        },
+    },
+)]
+#[derive(Clone, Debug, Serialize, Deserialize, Updater)]
+#[serde(rename_all = "kebab-case")]
+/// Subscription key used for accessing enterprise repositories and for offline subscription activation/renewal.
+pub struct SubscriptionKey {
+    /// Subscription key
+    #[updater(skip)]
+    pub key: String,
+    /// Server ID for this subscription key
+    pub server_id: String,
+    /// Description, e.g. which system this key is deployed on
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub description: Option<String>,
+    /// Last Subscription Key state
+    #[serde(skip_serializing_if = "Option::is_none")]
+    #[updater(skip)]
+    pub info: Option<String>,
+}
+
+impl From<SubscriptionKey> for ServerBlob {
+    fn from(key: SubscriptionKey) -> Self {
+        Self {
+            key: key.key,
+            serverid: key.server_id,
+        }
+    }
+}
+
+impl SubscriptionKey {
+    pub fn product(&self) -> ProductType {
+        match &self.key[..3] {
+            "pve" => ProductType::Pve,
+            "pmg" => ProductType::Pmg,
+            "pbs" => ProductType::Pbs,
+            "pom" => ProductType::Pom, // TODO replace with actual key prefix
+            _ => unimplemented!(),
+        }
+    }
+
+    pub fn info(&self) -> Result<Option<SubscriptionInfo>, Error> {
+        match self.info.as_ref() {
+            Some(info) => {
+                let info = base64::decode(info)?;
+                let info = serde_json::from_slice(&info)?;
+                Ok(Some(info))
+            }
+            None => Ok(None),
+        }
+    }
+}
+
+lazy_static! {
+    pub static ref CONFIG: SectionConfig = init();
+}
+
+fn init() -> SectionConfig {
+    let mut config = SectionConfig::new(&MIRROR_ID_SCHEMA);
+
+    let mirror_schema = match MirrorConfig::API_SCHEMA {
+        Schema::Object(ref obj_schema) => obj_schema,
+        _ => unreachable!(),
+    };
+    let mirror_plugin = SectionConfigPlugin::new(
+        "mirror".to_string(),
+        Some(String::from("id")),
+        mirror_schema,
+    );
+    config.register_plugin(mirror_plugin);
+
+    let media_schema = match MediaConfig::API_SCHEMA {
+        Schema::Object(ref obj_schema) => obj_schema,
+        _ => unreachable!(),
+    };
+    let media_plugin =
+        SectionConfigPlugin::new("medium".to_string(), Some(String::from("id")), media_schema);
+    config.register_plugin(media_plugin);
+
+    let key_schema = match SubscriptionKey::API_SCHEMA {
+        Schema::Object(ref obj_schema) => obj_schema,
+        _ => unreachable!(),
+    };
+    let key_plugin = SectionConfigPlugin::new(
+        "subscription".to_string(),
+        Some(String::from("key")),
+        key_schema,
+    );
+    config.register_plugin(key_plugin);
+
+    config
+}
+
+/// Lock guard for guarding modifications of config file.
+///
+/// Obtained via [lock_config], should only be dropped once config file should no longer be locked.
+pub struct ConfigLockGuard(std::fs::File);
+
+/// Get exclusive lock for config file (in order to make or protect against modifications).
+pub fn lock_config(path: &str) -> Result<ConfigLockGuard, Error> {
+    let path = Path::new(path);
+
+    let (mut path, file) = match (path.parent(), path.file_name()) {
+        (Some(parent), Some(file)) => (parent.to_path_buf(), file.to_string_lossy()),
+        _ => bail!("Unable to derive lock file name for {path:?}"),
+    };
+    path.push(format!(".{file}.lock"));
+
+    let file = proxmox_sys::fs::open_file_locked(
+        &path,
+        std::time::Duration::new(10, 0),
+        true,
+        CreateOptions::default(),
+    )?;
+    Ok(ConfigLockGuard(file))
+}
+
+/// Read config
+pub fn config(path: &str) -> Result<(SectionConfigData, [u8; 32]), Error> {
+    let content =
+        proxmox_sys::fs::file_read_optional_string(path)?.unwrap_or_else(|| "".to_string());
+
+    let digest = openssl::sha::sha256(content.as_bytes());
+    let data = CONFIG.parse(path, &content)?;
+    Ok((data, digest))
+}
+
+/// Write config (and verify data matches schema!)
+pub fn save_config(path: &str, data: &SectionConfigData) -> Result<(), Error> {
+    let raw = CONFIG.write(path, data)?;
+    replace_file(path, raw.as_bytes(), CreateOptions::default(), true)
 }