]> git.proxmox.com Git - proxmox-offline-mirror.git/blame - src/medium.rs
add some more comments
[proxmox-offline-mirror.git] / src / medium.rs
CommitLineData
9ecde319
FG
1use std::{
2 collections::{HashMap, HashSet},
d035ecb5 3 path::{Path, PathBuf},
9ecde319
FG
4};
5
6use anyhow::{bail, format_err, Error};
7use nix::libc;
8b267808 8use proxmox_subscription::SubscriptionInfo;
9ecde319
FG
9use proxmox_sys::fs::{file_get_contents, replace_file, CreateOptions};
10use proxmox_time::{epoch_i64, epoch_to_rfc3339_utc};
11use serde::{Deserialize, Serialize};
12
13use crate::{
d035ecb5
FG
14 config::{self, ConfigLockGuard, MediaConfig, MirrorConfig},
15 generate_repo_file_line,
16 mirror::pool,
9ecde319 17 pool::Pool,
d035ecb5 18 types::{Snapshot, SNAPSHOT_REGEX},
9ecde319
FG
19};
20#[derive(Clone, Debug, Serialize, Deserialize)]
21#[serde(rename_all = "kebab-case")]
2d13dcfc
FG
22/// Information about a mirror on the medium.
23///
24/// Used to generate repository lines for accessing the synced mirror.
9ecde319 25pub struct MirrorInfo {
2d13dcfc 26 /// Original repository line
d035ecb5 27 pub repository: String,
2d13dcfc 28 /// Mirrored architectures
d035ecb5 29 pub architectures: Vec<String>,
9ecde319
FG
30}
31
32impl From<&MirrorConfig> for MirrorInfo {
33 fn from(config: &MirrorConfig) -> Self {
34 Self {
35 repository: config.repository.clone(),
36 architectures: config.architectures.clone(),
37 }
38 }
39}
40
41impl From<MirrorConfig> for MirrorInfo {
42 fn from(config: MirrorConfig) -> Self {
43 Self {
44 repository: config.repository,
45 architectures: config.architectures,
46 }
47 }
48}
49
50#[derive(Debug, Serialize, Deserialize)]
51#[serde(rename_all = "kebab-case")]
2d13dcfc 52/// State of mirrors on the medium
9ecde319 53pub struct MediumState {
2d13dcfc 54 /// Map of mirror ID to `MirrorInfo`.
9ecde319 55 pub mirrors: HashMap<String, MirrorInfo>,
2d13dcfc 56 /// Timestamp of last sync operation.
9ecde319 57 pub last_sync: i64,
8b267808
FG
58 /// Subscriptions
59 #[serde(skip_serializing_if = "Vec::is_empty", default)]
60 pub subscriptions: Vec<SubscriptionInfo>,
9ecde319
FG
61}
62
2d13dcfc
FG
63/// Information about the mirrors on a medium.
64///
65/// Derived from `MediaConfig` (supposed state) and `MediumState` (actual state)
d035ecb5 66pub struct MediumMirrorState {
2d13dcfc 67 /// Mirrors which are configured and synced
d035ecb5 68 pub synced: HashSet<String>,
2d13dcfc
FG
69 /// Mirrors which are configured
70 pub config: HashSet<String>,
71 /// Mirrors which are configured but not synced yet
d035ecb5 72 pub source_only: HashSet<String>,
2d13dcfc 73 /// Mirrors which are not configured but exist on medium
d035ecb5
FG
74 pub target_only: HashSet<String>,
75}
76
2d13dcfc 77// helper to derive `MediumMirrorState`
d035ecb5
FG
78fn get_mirror_state(config: &MediaConfig, state: &MediumState) -> MediumMirrorState {
79 let synced_mirrors: HashSet<String> = state
80 .mirrors
81 .iter()
82 .map(|(id, _mirror)| id.clone())
83 .collect();
84 let config_mirrors: HashSet<String> = config.mirrors.iter().cloned().collect();
85 let new_mirrors: HashSet<String> = config_mirrors
86 .difference(&synced_mirrors)
87 .cloned()
88 .collect();
89 let dropped_mirrors: HashSet<String> = synced_mirrors
90 .difference(&config_mirrors)
91 .cloned()
92 .collect();
93
94 MediumMirrorState {
95 synced: synced_mirrors,
2d13dcfc 96 config: config_mirrors,
d035ecb5
FG
97 source_only: new_mirrors,
98 target_only: dropped_mirrors,
99 }
100}
101
2d13dcfc 102// Helper to lock medium
d035ecb5
FG
103fn lock(base: &Path) -> Result<ConfigLockGuard, Error> {
104 let mut lockfile = base.to_path_buf();
105 lockfile.push("mirror-state");
106 let lockfile = lockfile
107 .to_str()
108 .ok_or_else(|| format_err!("Couldn't convert lockfile path {lockfile:?})"))?;
109 config::lock_config(lockfile)
110}
111
2d13dcfc 112// Helper to get statefile path
d035ecb5
FG
113fn statefile(base: &Path) -> PathBuf {
114 let mut statefile = base.to_path_buf();
115 statefile.push(".mirror-state");
116 statefile
117}
118
2d13dcfc 119// Helper to load statefile
d035ecb5
FG
120fn load_state(base: &Path) -> Result<Option<MediumState>, Error> {
121 let statefile = statefile(base);
122
123 if statefile.exists() {
124 let raw = file_get_contents(&statefile)?;
125 let state: MediumState = serde_json::from_slice(&raw)?;
126 Ok(Some(state))
127 } else {
128 Ok(None)
129 }
130}
131
2d13dcfc 132// Helper to write statefile
d035ecb5
FG
133fn write_state(_lock: &ConfigLockGuard, base: &Path, state: &MediumState) -> Result<(), Error> {
134 replace_file(
135 &statefile(base),
136 &serde_json::to_vec(&state)?,
137 CreateOptions::default(),
138 true,
139 )?;
140
141 Ok(())
142}
143
2d13dcfc 144/// List snapshots of a given mirror on a given medium.
d035ecb5 145pub fn list_snapshots(medium_base: &Path, mirror: &str) -> Result<Vec<Snapshot>, Error> {
9ecde319
FG
146 if !medium_base.exists() {
147 bail!("Medium mountpoint doesn't exist.");
148 }
149
150 let mut list = vec![];
151 let mut mirror_base = medium_base.to_path_buf();
152 mirror_base.push(Path::new(&mirror));
153
154 proxmox_sys::fs::scandir(
155 libc::AT_FDCWD,
156 &mirror_base,
157 &SNAPSHOT_REGEX,
158 |_l2_fd, snapshot, file_type| {
159 if file_type != nix::dir::Type::Directory {
160 return Ok(());
161 }
162
d035ecb5 163 list.push(snapshot.parse()?);
9ecde319
FG
164
165 Ok(())
166 },
167 )?;
168
169 list.sort();
170
171 Ok(list)
172}
173
2d13dcfc 174/// Generate a repository snippet for a selection of mirrors on a medium.
9ecde319
FG
175pub fn generate_repo_snippet(
176 medium_base: &Path,
d035ecb5 177 repositories: &HashMap<String, (&MirrorInfo, Snapshot)>,
9ecde319
FG
178) -> Result<Vec<String>, Error> {
179 let mut res = Vec::new();
180 for (mirror_id, (mirror_info, snapshot)) in repositories {
181 res.push(generate_repo_file_line(
182 medium_base,
183 mirror_id,
184 mirror_info,
185 snapshot,
186 )?);
187 }
188 Ok(res)
189}
190
2d13dcfc 191/// Run garbage collection on all mirrors on a medium.
9ecde319
FG
192pub fn gc(medium: &crate::config::MediaConfig) -> Result<(), Error> {
193 let medium_base = Path::new(&medium.mountpoint);
194 if !medium_base.exists() {
195 bail!("Medium mountpoint doesn't exist.");
196 }
197
d035ecb5 198 let _lock = lock(medium_base)?;
9ecde319 199
d035ecb5
FG
200 println!("Loading state..");
201 let state = load_state(medium_base)?
202 .ok_or_else(|| format_err!("Cannot GC empty medium - no statefile found."))?;
9ecde319 203
9ecde319
FG
204 println!(
205 "Last sync timestamp: {}",
206 epoch_to_rfc3339_utc(state.last_sync)?
207 );
208
209 let mut total_count = 0usize;
210 let mut total_bytes = 0_u64;
211
212 for (id, _info) in state.mirrors {
213 println!("\nGC for '{id}'");
214 let mut mirror_base = medium_base.to_path_buf();
215 mirror_base.push(Path::new(&id));
216
217 let mut mirror_pool = mirror_base.clone();
218 mirror_pool.push(".pool"); // TODO make configurable somehow?
219
220 if mirror_base.exists() {
221 let pool = Pool::open(&mirror_base, &mirror_pool)?;
222 let locked = pool.lock()?;
223 let (count, bytes) = locked.gc()?;
224 println!("removed {count} files ({bytes}b)");
225 total_count += count;
226 total_bytes += bytes;
227 } else {
228 println!("{mirror_base:?} doesn't exist, skipping '{}'", id);
229 };
230 }
231
232 println!("GC removed {total_count} files ({total_bytes}b)");
233
234 Ok(())
235}
236
2d13dcfc 237/// Get `MediumState` and `MediumMirrorState` for a given medium.
d035ecb5
FG
238pub fn status(
239 medium: &crate::config::MediaConfig,
240) -> Result<(MediumState, MediumMirrorState), Error> {
9ecde319
FG
241 let medium_base = Path::new(&medium.mountpoint);
242 if !medium_base.exists() {
243 bail!("Medium mountpoint doesn't exist.");
244 }
245
d035ecb5
FG
246 let state = load_state(medium_base)?
247 .ok_or_else(|| format_err!("No status available - statefile doesn't exist."))?;
248 let mirror_state = get_mirror_state(medium, &state);
9ecde319 249
d035ecb5 250 Ok((state, mirror_state))
9ecde319
FG
251}
252
8b267808
FG
253/// Sync only subscription keys to medium
254pub fn sync_keys(
255 medium: &crate::config::MediaConfig,
256 subscriptions: Vec<SubscriptionInfo>,
257) -> Result<(), Error> {
258 let medium_base = Path::new(&medium.mountpoint);
259 if !medium_base.exists() {
260 bail!("Medium mountpoint doesn't exist.");
261 }
262
263 let lock = lock(medium_base)?;
264
265 let mut state = match load_state(medium_base)? {
266 Some(state) => {
267 println!("Loaded existing statefile.");
268 println!(
269 "Last sync timestamp: {}",
270 epoch_to_rfc3339_utc(state.last_sync)?
271 );
272 state
273 }
274 None => {
275 println!("Creating new statefile..");
276 MediumState {
277 mirrors: HashMap::new(),
278 last_sync: 0,
279 subscriptions: vec![],
280 }
281 }
282 };
283
284 state.last_sync = epoch_i64();
285 println!("Sync timestamp: {}", epoch_to_rfc3339_utc(state.last_sync)?);
286
287 println!("Updating statefile..");
288 state.subscriptions = subscriptions;
289 write_state(&lock, medium_base, &state)?;
290
291 Ok(())
292}
293
2d13dcfc 294/// Sync medium's content according to config.
8b267808
FG
295pub fn sync(
296 medium: &crate::config::MediaConfig,
297 mirrors: Vec<MirrorConfig>,
298 subscriptions: Vec<SubscriptionInfo>,
299) -> Result<(), Error> {
9ecde319
FG
300 println!(
301 "Syncing {} mirrors {:?} to medium '{}' ({:?})",
302 &medium.mirrors.len(),
303 &medium.mirrors,
304 &medium.id,
305 &medium.mountpoint
306 );
307
2d13dcfc
FG
308 if mirrors.len() != medium.mirrors.len() {
309 bail!("Number of mirrors in config and sync request don't match.");
310 }
311
9ecde319
FG
312 let medium_base = Path::new(&medium.mountpoint);
313 if !medium_base.exists() {
314 bail!("Medium mountpoint doesn't exist.");
315 }
316
d035ecb5 317 let lock = lock(medium_base)?;
9ecde319 318
d035ecb5
FG
319 let mut state = match load_state(medium_base)? {
320 Some(state) => {
321 println!("Loaded existing statefile.");
322 println!(
323 "Last sync timestamp: {}",
324 epoch_to_rfc3339_utc(state.last_sync)?
325 );
326 state
327 }
328 None => {
329 println!("Creating new statefile..");
330 MediumState {
331 mirrors: HashMap::new(),
332 last_sync: 0,
8b267808 333 subscriptions: vec![],
d035ecb5 334 }
9ecde319
FG
335 }
336 };
337
338 state.last_sync = epoch_i64();
339 println!("Sync timestamp: {}", epoch_to_rfc3339_utc(state.last_sync)?);
340
d035ecb5
FG
341 let mirror_state = get_mirror_state(medium, &state);
342 println!("Previously synced mirrors: {:?}", &mirror_state.synced);
9ecde319 343
2d13dcfc
FG
344 let requested: HashSet<String> = mirrors.iter().map(|mirror| mirror.id.clone()).collect();
345 if requested != mirror_state.config {
346 bail!(
347 "Config and sync request don't use the same mirror list: {:?} / {:?}",
348 mirror_state.config,
349 requested
350 );
351 }
352
d035ecb5 353 if !mirror_state.source_only.is_empty() {
9ecde319 354 println!(
d035ecb5
FG
355 "Adding {} new mirror(s) to target medium: {:?}",
356 mirror_state.source_only.len(),
357 mirror_state.source_only,
9ecde319
FG
358 );
359 }
d035ecb5 360 if !mirror_state.target_only.is_empty() {
9ecde319 361 println!(
d035ecb5
FG
362 "Dropping {} removed mirror(s) from target medium (after syncing): {:?}",
363 mirror_state.target_only.len(),
364 mirror_state.target_only,
9ecde319
FG
365 );
366 }
367
368 println!("\nStarting sync now!");
369 state.mirrors = HashMap::new();
370
371 for mirror in mirrors.into_iter() {
372 let mut mirror_base = medium_base.to_path_buf();
373 mirror_base.push(Path::new(&mirror.id));
374
375 println!("\nSyncing '{}' to {mirror_base:?}..", mirror.id);
376
377 let mut mirror_pool = mirror_base.clone();
378 mirror_pool.push(".pool"); // TODO make configurable somehow?
379
380 let target_pool = if mirror_base.exists() {
381 Pool::open(&mirror_base, &mirror_pool)?
382 } else {
383 Pool::create(&mirror_base, &mirror_pool)?
384 };
385
d035ecb5 386 let source_pool: Pool = pool(&mirror)?;
9ecde319
FG
387 source_pool.lock()?.sync_pool(&target_pool, medium.verify)?;
388
389 state.mirrors.insert(mirror.id.clone(), mirror.into());
390 }
391
d035ecb5 392 if !mirror_state.target_only.is_empty() {
9ecde319
FG
393 println!();
394 }
d035ecb5 395 for dropped in mirror_state.target_only {
9ecde319
FG
396 let mut mirror_base = medium_base.to_path_buf();
397 mirror_base.push(Path::new(&dropped));
398
399 if mirror_base.exists() {
400 println!("Removing previously synced, but no longer configured mirror '{dropped}'..");
401 std::fs::remove_dir_all(&mirror_base)?;
402 }
403 }
404
405 println!("Updating statefile..");
8b267808 406 state.subscriptions = subscriptions;
d035ecb5 407 write_state(&lock, medium_base, &state)?;
9ecde319
FG
408
409 Ok(())
410}