]> git.proxmox.com Git - proxmox-offline-mirror.git/blob - src/config.rs
6c2f3e81b619fb3b84c12306678e881a65655a8c
[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 }
45 )]
46 #[derive(Clone, Debug, Serialize, Deserialize, Updater)]
47 #[serde(rename_all = "kebab-case")]
48 /// Configuration entry for a mirrored repository.
49 pub struct MirrorConfig {
50 #[updater(skip)]
51 /// Identifier for this entry.
52 pub id: String,
53 /// Single repository definition in sources.list format.
54 pub repository: String,
55 /// List of architectures that should be mirrored.
56 pub architectures: Vec<String>,
57 /// Path to directory containg mirrored repository pool. Can be shared by multiple mirrors.
58 pub base_dir: String,
59 /// Path to public key file for verifying repository integrity.
60 pub key_path: String,
61 /// Whether to verify existing files or assume they are valid (IO-intensive).
62 pub verify: bool,
63 /// Whether to write new files using FSYNC.
64 pub sync: bool,
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>,
68 }
69
70 #[api(
71 properties: {
72 id: {
73 schema: MEDIA_ID_SCHEMA,
74 },
75 mountpoint: {
76 type: String,
77 },
78 verify: {
79 type: bool,
80 },
81 sync: {
82 type: bool,
83 },
84 mirrors: {
85 type: Array,
86 items: {
87 schema: MIRROR_ID_SCHEMA,
88 },
89 },
90 }
91 )]
92 #[derive(Debug, Serialize, Deserialize, Updater)]
93 #[serde(rename_all = "kebab-case")]
94 /// Configuration entry for an external medium.
95 pub struct MediaConfig {
96 #[updater(skip)]
97 /// Identifier for this entry.
98 pub id: String,
99 /// Mountpoint where medium is available on mirroring system.
100 pub mountpoint: String,
101 /// List of [MirrorConfig] IDs which should be synced to medium.
102 pub mirrors: Vec<String>,
103 /// Whether to verify existing files or assume they are valid (IO-intensive).
104 pub verify: bool,
105 /// Whether to write new files using FSYNC.
106 pub sync: bool,
107 }
108
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
146 impl From<SubscriptionKey> for ServerBlob {
147 fn from(key: SubscriptionKey) -> Self {
148 Self {
149 key: key.key,
150 serverid: key.server_id,
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
178 lazy_static! {
179 pub static ref CONFIG: SectionConfig = init();
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
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
215 config
216 }
217
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.
221 pub struct ConfigLockGuard(std::fs::File);
222
223 /// Get exclusive lock for config file (in order to make or protect against modifications).
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
242 /// Read config
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
252 /// Write config (and verify data matches schema!)
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 }