]> git.proxmox.com Git - proxmox-offline-mirror.git/blame - src/config.rs
fix #4259: mirror: add ignore-errors option
[proxmox-offline-mirror.git] / src / config.rs
CommitLineData
9ecde319
FG
1use std::path::Path;
2
3use anyhow::{bail, Error};
4use lazy_static::lazy_static;
8b267808 5use proxmox_subscription::{sign::ServerBlob, SubscriptionInfo};
9ecde319 6use serde::{Deserialize, Serialize};
f4f8dff0 7
9ecde319
FG
8use proxmox_schema::{api, ApiType, Schema, Updater};
9use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin};
d035ecb5 10use proxmox_sys::fs::{replace_file, CreateOptions};
9ecde319 11
8b267808 12use crate::types::{
b42cad3b
FG
13 ProductType, MEDIA_ID_SCHEMA, MIRROR_ID_SCHEMA, PROXMOX_SERVER_ID_SCHEMA,
14 PROXMOX_SUBSCRIPTION_KEY_SCHEMA,
8b267808 15};
f4f8dff0 16
9ecde319
FG
17#[api(
18 properties: {
19 id: {
20 schema: MIRROR_ID_SCHEMA,
21 },
22 repository: {
23 type: String,
9ecde319
FG
24 },
25 architectures: {
26 type: Array,
9ecde319
FG
27 items: {
28 type: String,
29 description: "Architecture specifier.",
30 },
31 },
c598cb15 32 "base-dir": {
9ecde319 33 type: String,
9ecde319
FG
34 },
35 "key-path": {
36 type: String,
9ecde319
FG
37 },
38 verify: {
39 type: bool,
9ecde319
FG
40 },
41 sync: {
42 type: bool,
9ecde319 43 },
96a80415
FG
44 "ignore-errors": {
45 type: bool,
46 optional: true,
47 default: false,
48 },
9ecde319
FG
49 }
50)]
51#[derive(Clone, Debug, Serialize, Deserialize, Updater)]
52#[serde(rename_all = "kebab-case")]
d035ecb5 53/// Configuration entry for a mirrored repository.
f4f8dff0 54pub struct MirrorConfig {
9ecde319 55 #[updater(skip)]
d035ecb5 56 /// Identifier for this entry.
9ecde319 57 pub id: String,
d035ecb5 58 /// Single repository definition in sources.list format.
9ecde319 59 pub repository: String,
d035ecb5 60 /// List of architectures that should be mirrored.
9ecde319 61 pub architectures: Vec<String>,
c598cb15
FG
62 /// Path to directory containg mirrored repository pool. Can be shared by multiple mirrors.
63 pub base_dir: String,
d035ecb5 64 /// Path to public key file for verifying repository integrity.
9ecde319 65 pub key_path: String,
d035ecb5 66 /// Whether to verify existing files or assume they are valid (IO-intensive).
9ecde319 67 pub verify: bool,
d035ecb5 68 /// Whether to write new files using FSYNC.
9ecde319 69 pub sync: bool,
8b267808
FG
70 /// Use subscription key to access (required for Proxmox Enterprise repositories).
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub use_subscription: Option<ProductType>,
96a80415
FG
73 /// Whether to downgrade download errors to warnings
74 #[serde(default)]
75 pub ignore_errors: bool,
9ecde319
FG
76}
77
9ecde319
FG
78#[api(
79 properties: {
80 id: {
b42cad3b 81 schema: MEDIA_ID_SCHEMA,
9ecde319
FG
82 },
83 mountpoint: {
84 type: String,
9ecde319
FG
85 },
86 verify: {
87 type: bool,
9ecde319
FG
88 },
89 sync: {
90 type: bool,
9ecde319
FG
91 },
92 mirrors: {
93 type: Array,
9ecde319
FG
94 items: {
95 schema: MIRROR_ID_SCHEMA,
96 },
97 },
98 }
99)]
100#[derive(Debug, Serialize, Deserialize, Updater)]
101#[serde(rename_all = "kebab-case")]
2d13dcfc 102/// Configuration entry for an external medium.
9ecde319
FG
103pub struct MediaConfig {
104 #[updater(skip)]
2d13dcfc 105 /// Identifier for this entry.
9ecde319 106 pub id: String,
2d13dcfc 107 /// Mountpoint where medium is available on mirroring system.
9ecde319 108 pub mountpoint: String,
2d13dcfc 109 /// List of [MirrorConfig] IDs which should be synced to medium.
9ecde319 110 pub mirrors: Vec<String>,
2d13dcfc 111 /// Whether to verify existing files or assume they are valid (IO-intensive).
9ecde319 112 pub verify: bool,
2d13dcfc 113 /// Whether to write new files using FSYNC.
9ecde319
FG
114 pub sync: bool,
115}
116
8b267808
FG
117#[api(
118 properties: {
119 key: {
120 schema: PROXMOX_SUBSCRIPTION_KEY_SCHEMA,
121 },
122 "server-id": {
123 schema: PROXMOX_SERVER_ID_SCHEMA,
124 },
125 description: {
126 type: String,
127 optional: true,
128 },
129 info: {
130 type: String,
131 description: "base64 encoded subscription info - update with 'refresh' command.",
132 optional: true,
133 },
134 },
135)]
136#[derive(Clone, Debug, Serialize, Deserialize, Updater)]
137#[serde(rename_all = "kebab-case")]
138/// Subscription key used for accessing enterprise repositories and for offline subscription activation/renewal.
139pub struct SubscriptionKey {
140 /// Subscription key
141 #[updater(skip)]
142 pub key: String,
143 /// Server ID for this subscription key
144 pub server_id: String,
145 /// Description, e.g. which system this key is deployed on
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub description: Option<String>,
148 /// Last Subscription Key state
149 #[serde(skip_serializing_if = "Option::is_none")]
150 #[updater(skip)]
151 pub info: Option<String>,
152}
153
c76900c9
FG
154impl From<SubscriptionKey> for ServerBlob {
155 fn from(key: SubscriptionKey) -> Self {
156 Self {
157 key: key.key,
158 serverid: key.server_id,
8b267808
FG
159 }
160 }
161}
162
163impl SubscriptionKey {
164 pub fn product(&self) -> ProductType {
165 match &self.key[..3] {
166 "pve" => ProductType::Pve,
167 "pmg" => ProductType::Pmg,
168 "pbs" => ProductType::Pbs,
169 "pom" => ProductType::Pom, // TODO replace with actual key prefix
170 _ => unimplemented!(),
171 }
172 }
173
174 pub fn info(&self) -> Result<Option<SubscriptionInfo>, Error> {
175 match self.info.as_ref() {
176 Some(info) => {
177 let info = base64::decode(info)?;
178 let info = serde_json::from_slice(&info)?;
179 Ok(Some(info))
180 }
181 None => Ok(None),
182 }
183 }
184}
185
9ecde319 186lazy_static! {
fabdaf91 187 pub static ref CONFIG: SectionConfig = init();
9ecde319
FG
188}
189
190fn init() -> SectionConfig {
191 let mut config = SectionConfig::new(&MIRROR_ID_SCHEMA);
192
193 let mirror_schema = match MirrorConfig::API_SCHEMA {
194 Schema::Object(ref obj_schema) => obj_schema,
195 _ => unreachable!(),
196 };
197 let mirror_plugin = SectionConfigPlugin::new(
198 "mirror".to_string(),
199 Some(String::from("id")),
200 mirror_schema,
201 );
202 config.register_plugin(mirror_plugin);
203
204 let media_schema = match MediaConfig::API_SCHEMA {
205 Schema::Object(ref obj_schema) => obj_schema,
206 _ => unreachable!(),
207 };
208 let media_plugin =
209 SectionConfigPlugin::new("medium".to_string(), Some(String::from("id")), media_schema);
210 config.register_plugin(media_plugin);
211
8b267808
FG
212 let key_schema = match SubscriptionKey::API_SCHEMA {
213 Schema::Object(ref obj_schema) => obj_schema,
214 _ => unreachable!(),
215 };
216 let key_plugin = SectionConfigPlugin::new(
217 "subscription".to_string(),
218 Some(String::from("key")),
219 key_schema,
220 );
221 config.register_plugin(key_plugin);
222
9ecde319
FG
223 config
224}
225
2d13dcfc
FG
226/// Lock guard for guarding modifications of config file.
227///
228/// Obtained via [lock_config], should only be dropped once config file should no longer be locked.
9ecde319
FG
229pub struct ConfigLockGuard(std::fs::File);
230
2d13dcfc 231/// Get exclusive lock for config file (in order to make or protect against modifications).
9ecde319
FG
232pub fn lock_config(path: &str) -> Result<ConfigLockGuard, Error> {
233 let path = Path::new(path);
234
235 let (mut path, file) = match (path.parent(), path.file_name()) {
236 (Some(parent), Some(file)) => (parent.to_path_buf(), file.to_string_lossy()),
237 _ => bail!("Unable to derive lock file name for {path:?}"),
238 };
239 path.push(format!(".{file}.lock"));
240
241 let file = proxmox_sys::fs::open_file_locked(
242 &path,
243 std::time::Duration::new(10, 0),
244 true,
245 CreateOptions::default(),
246 )?;
247 Ok(ConfigLockGuard(file))
248}
249
2d13dcfc 250/// Read config
9ecde319
FG
251pub fn config(path: &str) -> Result<(SectionConfigData, [u8; 32]), Error> {
252 let content =
253 proxmox_sys::fs::file_read_optional_string(path)?.unwrap_or_else(|| "".to_string());
254
255 let digest = openssl::sha::sha256(content.as_bytes());
256 let data = CONFIG.parse(path, &content)?;
257 Ok((data, digest))
258}
259
2d13dcfc 260/// Write config (and verify data matches schema!)
9ecde319
FG
261pub fn save_config(path: &str, data: &SectionConfigData) -> Result<(), Error> {
262 let raw = CONFIG.write(path, data)?;
263 replace_file(path, raw.as_bytes(), CreateOptions::default(), true)
264}