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