]> git.proxmox.com Git - proxmox-offline-mirror.git/blob - src/config.rs
fix #4259: mirror: add ignore-errors option
[proxmox-offline-mirror.git] / src / config.rs
1 use std::path::Path;
2
3 use anyhow::{bail, Error};
4 use lazy_static::lazy_static;
5 use proxmox_subscription::{sign::ServerBlob, SubscriptionInfo};
6 use serde::{Deserialize, Serialize};
7
8 use proxmox_schema::{api, ApiType, Schema, Updater};
9 use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin};
10 use proxmox_sys::fs::{replace_file, CreateOptions};
11
12 use crate::types::{
13 ProductType, MEDIA_ID_SCHEMA, MIRROR_ID_SCHEMA, PROXMOX_SERVER_ID_SCHEMA,
14 PROXMOX_SUBSCRIPTION_KEY_SCHEMA,
15 };
16
17 #[api(
18 properties: {
19 id: {
20 schema: MIRROR_ID_SCHEMA,
21 },
22 repository: {
23 type: String,
24 },
25 architectures: {
26 type: Array,
27 items: {
28 type: String,
29 description: "Architecture specifier.",
30 },
31 },
32 "base-dir": {
33 type: String,
34 },
35 "key-path": {
36 type: String,
37 },
38 verify: {
39 type: bool,
40 },
41 sync: {
42 type: bool,
43 },
44 "ignore-errors": {
45 type: bool,
46 optional: true,
47 default: false,
48 },
49 }
50 )]
51 #[derive(Clone, Debug, Serialize, Deserialize, Updater)]
52 #[serde(rename_all = "kebab-case")]
53 /// Configuration entry for a mirrored repository.
54 pub struct MirrorConfig {
55 #[updater(skip)]
56 /// Identifier for this entry.
57 pub id: String,
58 /// Single repository definition in sources.list format.
59 pub repository: String,
60 /// List of architectures that should be mirrored.
61 pub architectures: Vec<String>,
62 /// Path to directory containg mirrored repository pool. Can be shared by multiple mirrors.
63 pub base_dir: String,
64 /// Path to public key file for verifying repository integrity.
65 pub key_path: String,
66 /// Whether to verify existing files or assume they are valid (IO-intensive).
67 pub verify: bool,
68 /// Whether to write new files using FSYNC.
69 pub sync: bool,
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>,
73 /// Whether to downgrade download errors to warnings
74 #[serde(default)]
75 pub ignore_errors: bool,
76 }
77
78 #[api(
79 properties: {
80 id: {
81 schema: MEDIA_ID_SCHEMA,
82 },
83 mountpoint: {
84 type: String,
85 },
86 verify: {
87 type: bool,
88 },
89 sync: {
90 type: bool,
91 },
92 mirrors: {
93 type: Array,
94 items: {
95 schema: MIRROR_ID_SCHEMA,
96 },
97 },
98 }
99 )]
100 #[derive(Debug, Serialize, Deserialize, Updater)]
101 #[serde(rename_all = "kebab-case")]
102 /// Configuration entry for an external medium.
103 pub struct MediaConfig {
104 #[updater(skip)]
105 /// Identifier for this entry.
106 pub id: String,
107 /// Mountpoint where medium is available on mirroring system.
108 pub mountpoint: String,
109 /// List of [MirrorConfig] IDs which should be synced to medium.
110 pub mirrors: Vec<String>,
111 /// Whether to verify existing files or assume they are valid (IO-intensive).
112 pub verify: bool,
113 /// Whether to write new files using FSYNC.
114 pub sync: bool,
115 }
116
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.
139 pub 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
154 impl From<SubscriptionKey> for ServerBlob {
155 fn from(key: SubscriptionKey) -> Self {
156 Self {
157 key: key.key,
158 serverid: key.server_id,
159 }
160 }
161 }
162
163 impl 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
186 lazy_static! {
187 pub static ref CONFIG: SectionConfig = init();
188 }
189
190 fn 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
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
223 config
224 }
225
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.
229 pub struct ConfigLockGuard(std::fs::File);
230
231 /// Get exclusive lock for config file (in order to make or protect against modifications).
232 pub 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
250 /// Read config
251 pub 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
260 /// Write config (and verify data matches schema!)
261 pub 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 }