]>
Commit | Line | Data |
---|---|---|
9ecde319 FG |
1 | use std::path::Path; |
2 | ||
3 | use anyhow::{bail, Error}; | |
4 | use lazy_static::lazy_static; | |
8b267808 | 5 | use proxmox_subscription::{sign::ServerBlob, SubscriptionInfo}; |
9ecde319 | 6 | use serde::{Deserialize, Serialize}; |
f4f8dff0 | 7 | |
9ecde319 FG |
8 | use proxmox_schema::{api, ApiType, Schema, Updater}; |
9 | use proxmox_section_config::{SectionConfig, SectionConfigData, SectionConfigPlugin}; | |
d035ecb5 | 10 | use proxmox_sys::fs::{replace_file, CreateOptions}; |
9ecde319 | 11 | |
8b267808 | 12 | use 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 | 54 | pub 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 |
103 | pub 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. | |
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 | ||
c76900c9 FG |
154 | impl 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 | ||
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 | ||
9ecde319 | 186 | lazy_static! { |
fabdaf91 | 187 | pub static ref CONFIG: SectionConfig = init(); |
9ecde319 FG |
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 | ||
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 |
229 | pub struct ConfigLockGuard(std::fs::File); |
230 | ||
2d13dcfc | 231 | /// Get exclusive lock for config file (in order to make or protect against modifications). |
9ecde319 FG |
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 | ||
2d13dcfc | 250 | /// Read config |
9ecde319 FG |
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 | ||
2d13dcfc | 260 | /// Write config (and verify data matches schema!) |
9ecde319 FG |
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 | } |