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