]> git.proxmox.com Git - proxmox-backup.git/blame - pbs-datastore/src/datastore.rs
datastore: group: return basic stats on backup group destroy
[proxmox-backup.git] / pbs-datastore / src / datastore.rs
CommitLineData
42c2b5be 1use std::collections::{HashMap, HashSet};
54552dda 2use std::io::{self, Write};
4c7cc5b3 3use std::os::unix::io::AsRawFd;
367f002e 4use std::path::{Path, PathBuf};
42c2b5be 5use std::sync::{Arc, Mutex};
367f002e 6
f7d4e4b5 7use anyhow::{bail, format_err, Error};
2c32fdde 8use lazy_static::lazy_static;
4c7cc5b3 9use nix::unistd::{unlinkat, UnlinkatFlags};
e4439025 10
08f8a3e5 11use proxmox_human_byte::HumanByte;
fef61684
DC
12use proxmox_schema::ApiType;
13
857f346c 14use proxmox_sys::error::SysError;
42c2b5be
TL
15use proxmox_sys::fs::{file_read_optional_string, replace_file, CreateOptions};
16use proxmox_sys::fs::{lock_dir_noblock, DirLockGuard};
d5790a9f 17use proxmox_sys::process_locker::ProcessLockSharedGuard;
25877d05 18use proxmox_sys::WorkerTaskContext;
d5790a9f 19use proxmox_sys::{task_log, task_warn};
529de6c7 20
fef61684 21use pbs_api_types::{
647186dd 22 Authid, BackupNamespace, BackupType, ChunkOrder, DataStoreConfig, DatastoreFSyncLevel,
08f8a3e5 23 DatastoreTuning, GarbageCollectionStatus, Operation, UPID,
fef61684 24};
529de6c7 25
524ed404 26use crate::backup_info::{BackupDir, BackupGroup, BackupGroupDeleteStats};
6d5d305d
DM
27use crate::chunk_store::ChunkStore;
28use crate::dynamic_index::{DynamicIndexReader, DynamicIndexWriter};
29use crate::fixed_index::{FixedIndexReader, FixedIndexWriter};
5444b914 30use crate::hierarchy::{ListGroups, ListGroupsType, ListNamespaces, ListNamespacesRecursive};
6d5d305d 31use crate::index::IndexFile;
9ccf933b 32use crate::manifest::{archive_type, ArchiveType};
857f346c 33use crate::task_tracking::{self, update_active_operations};
42c2b5be 34use crate::DataBlob;
6d5d305d 35
367f002e 36lazy_static! {
42c2b5be
TL
37 static ref DATASTORE_MAP: Mutex<HashMap<String, Arc<DataStoreImpl>>> =
38 Mutex::new(HashMap::new());
b3483782 39}
ff3d3100 40
9751ef4b
DC
41/// checks if auth_id is owner, or, if owner is a token, if
42/// auth_id is the user of the token
42c2b5be
TL
43pub fn check_backup_owner(owner: &Authid, auth_id: &Authid) -> Result<(), Error> {
44 let correct_owner =
45 owner == auth_id || (owner.is_token() && &Authid::from(owner.user().clone()) == auth_id);
9751ef4b
DC
46 if !correct_owner {
47 bail!("backup owner check failed ({} != {})", auth_id, owner);
48 }
49 Ok(())
50}
51
e5064ba6
DM
52/// Datastore Management
53///
54/// A Datastore can store severals backups, and provides the
55/// management interface for backup.
4bc84a65 56pub struct DataStoreImpl {
1629d2ad 57 chunk_store: Arc<ChunkStore>,
81b2a872 58 gc_mutex: Mutex<()>,
f2b99c34 59 last_gc_status: Mutex<GarbageCollectionStatus>,
0698f78d 60 verify_new: bool,
fef61684 61 chunk_order: ChunkOrder,
51d900d1 62 last_digest: Option<[u8; 32]>,
647186dd 63 sync_level: DatastoreFSyncLevel,
529de6c7
DM
64}
65
6da20161
WB
66impl DataStoreImpl {
67 // This one just panics on everything
68 #[doc(hidden)]
615a50c1 69 pub(crate) unsafe fn new_test() -> Arc<Self> {
6da20161
WB
70 Arc::new(Self {
71 chunk_store: Arc::new(unsafe { ChunkStore::panic_store() }),
72 gc_mutex: Mutex::new(()),
73 last_gc_status: Mutex::new(GarbageCollectionStatus::default()),
74 verify_new: false,
c40a2f8b 75 chunk_order: Default::default(),
51d900d1 76 last_digest: None,
647186dd 77 sync_level: Default::default(),
6da20161
WB
78 })
79 }
80}
81
4bc84a65
HL
82pub struct DataStore {
83 inner: Arc<DataStoreImpl>,
84 operation: Option<Operation>,
85}
86
87impl Clone for DataStore {
88 fn clone(&self) -> Self {
89 let mut new_operation = self.operation;
90 if let Some(operation) = self.operation {
91 if let Err(e) = update_active_operations(self.name(), operation, 1) {
92 log::error!("could not update active operations - {}", e);
93 new_operation = None;
94 }
95 }
96
97 DataStore {
98 inner: self.inner.clone(),
99 operation: new_operation,
100 }
101 }
102}
103
104impl Drop for DataStore {
105 fn drop(&mut self) {
106 if let Some(operation) = self.operation {
9cba51ac
HL
107 let mut last_task = false;
108 match update_active_operations(self.name(), operation, -1) {
109 Err(e) => log::error!("could not update active operations - {}", e),
110 Ok(updated_operations) => {
111 last_task = updated_operations.read + updated_operations.write == 0;
112 }
113 }
114
115 // remove datastore from cache iff
116 // - last task finished, and
117 // - datastore is in a maintenance mode that mandates it
118 let remove_from_cache = last_task
119 && pbs_config::datastore::config()
120 .and_then(|(s, _)| s.lookup::<DataStoreConfig>("datastore", self.name()))
121 .map_or(false, |c| {
122 c.get_maintenance_mode().map_or(false, |m| m.is_offline())
123 });
124
125 if remove_from_cache {
126 DATASTORE_MAP.lock().unwrap().remove(self.name());
4bc84a65
HL
127 }
128 }
129 }
130}
131
529de6c7 132impl DataStore {
6da20161
WB
133 // This one just panics on everything
134 #[doc(hidden)]
615a50c1 135 pub(crate) unsafe fn new_test() -> Arc<Self> {
6da20161
WB
136 Arc::new(Self {
137 inner: unsafe { DataStoreImpl::new_test() },
138 operation: None,
139 })
140 }
141
e9d2fc93
HL
142 pub fn lookup_datastore(
143 name: &str,
144 operation: Option<Operation>,
145 ) -> Result<Arc<DataStore>, Error> {
857f346c
WB
146 // Avoid TOCTOU between checking maintenance mode and updating active operation counter, as
147 // we use it to decide whether it is okay to delete the datastore.
148 let config_lock = pbs_config::datastore::lock_config()?;
149
51d900d1
TL
150 // we could use the ConfigVersionCache's generation for staleness detection, but we load
151 // the config anyway -> just use digest, additional benefit: manual changes get detected
152 let (config, digest) = pbs_config::datastore::config()?;
e9d2fc93 153 let config: DataStoreConfig = config.lookup("datastore", name)?;
e9d2fc93
HL
154
155 if let Some(maintenance_mode) = config.get_maintenance_mode() {
156 if let Err(error) = maintenance_mode.check(operation) {
0408f60b 157 bail!("datastore '{name}' is in {error}");
e9d2fc93
HL
158 }
159 }
160
4bc84a65
HL
161 if let Some(operation) = operation {
162 update_active_operations(name, operation, 1)?;
163 }
164
857f346c
WB
165 // Our operation is registered, unlock the config.
166 drop(config_lock);
167
33a1ef7a
TL
168 let mut datastore_cache = DATASTORE_MAP.lock().unwrap();
169 let entry = datastore_cache.get(name);
2c32fdde 170
c7f7236b 171 // reuse chunk store so that we keep using the same process locker instance!
0bd9c870 172 let chunk_store = if let Some(datastore) = &entry {
51d900d1
TL
173 let last_digest = datastore.last_digest.as_ref();
174 if let Some(true) = last_digest.map(|last_digest| last_digest == &digest) {
4bc84a65
HL
175 return Ok(Arc::new(Self {
176 inner: Arc::clone(datastore),
177 operation,
178 }));
2c32fdde 179 }
0bd9c870
DC
180 Arc::clone(&datastore.chunk_store)
181 } else {
647186dd
DC
182 let tuning: DatastoreTuning = serde_json::from_value(
183 DatastoreTuning::API_SCHEMA
184 .parse_property_string(config.tuning.as_deref().unwrap_or(""))?,
185 )?;
186 Arc::new(ChunkStore::open(
187 name,
188 &config.path,
189 tuning.sync_level.unwrap_or_default(),
190 )?)
0bd9c870 191 };
2c32fdde 192
51d900d1 193 let datastore = DataStore::with_store_and_config(chunk_store, config, Some(digest))?;
f0a61124
DM
194
195 let datastore = Arc::new(datastore);
33a1ef7a 196 datastore_cache.insert(name.to_string(), datastore.clone());
2c32fdde 197
4bc84a65
HL
198 Ok(Arc::new(Self {
199 inner: datastore,
200 operation,
201 }))
2c32fdde
DM
202 }
203
062cf75c 204 /// removes all datastores that are not configured anymore
42c2b5be 205 pub fn remove_unused_datastores() -> Result<(), Error> {
e7d4be9d 206 let (config, _digest) = pbs_config::datastore::config()?;
062cf75c
DC
207
208 let mut map = DATASTORE_MAP.lock().unwrap();
209 // removes all elements that are not in the config
42c2b5be 210 map.retain(|key, _| config.sections.contains_key(key));
062cf75c
DC
211 Ok(())
212 }
213
9cba51ac
HL
214 /// trigger clearing cache entry based on maintenance mode. Entry will only
215 /// be cleared iff there is no other task running, if there is, the end of the
216 /// last running task will trigger the clearing of the cache entry.
217 pub fn update_datastore_cache(name: &str) -> Result<(), Error> {
218 let (config, _digest) = pbs_config::datastore::config()?;
219 let datastore: DataStoreConfig = config.lookup("datastore", name)?;
220 if datastore
221 .get_maintenance_mode()
222 .map_or(false, |m| m.is_offline())
223 {
224 // the datastore drop handler does the checking if tasks are running and clears the
225 // cache entry, so we just have to trigger it here
226 let _ = DataStore::lookup_datastore(name, Some(Operation::Lookup));
227 }
228
229 Ok(())
230 }
231
6da20161 232 /// Open a raw database given a name and a path.
c66fa32c
TL
233 ///
234 /// # Safety
235 /// See the safety section in `open_from_config`
519ca9d0 236 pub unsafe fn open_path(
6da20161
WB
237 name: &str,
238 path: impl AsRef<Path>,
239 operation: Option<Operation>,
240 ) -> Result<Arc<Self>, Error> {
241 let path = path
242 .as_ref()
243 .to_str()
244 .ok_or_else(|| format_err!("non-utf8 paths not supported"))?
245 .to_owned();
246 unsafe { Self::open_from_config(DataStoreConfig::new(name.to_owned(), path), operation) }
247 }
248
249 /// Open a datastore given a raw configuration.
c66fa32c
TL
250 ///
251 /// # Safety
74cad4a8 252 /// There's no memory safety implication, but as this is opening a new ChunkStore it will
c66fa32c
TL
253 /// create a new process locker instance, potentially on the same path as existing safely
254 /// created ones. This is dangerous as dropping the reference of this and thus the underlying
255 /// chunkstore's process locker will close all locks from our process on the config.path,
256 /// breaking guarantees we need to uphold for safe long backup + GC interaction on newer/older
257 /// process instances (from package update).
519ca9d0 258 unsafe fn open_from_config(
6da20161
WB
259 config: DataStoreConfig,
260 operation: Option<Operation>,
261 ) -> Result<Arc<Self>, Error> {
262 let name = config.name.clone();
263
647186dd
DC
264 let tuning: DatastoreTuning = serde_json::from_value(
265 DatastoreTuning::API_SCHEMA
266 .parse_property_string(config.tuning.as_deref().unwrap_or(""))?,
267 )?;
268 let chunk_store =
269 ChunkStore::open(&name, &config.path, tuning.sync_level.unwrap_or_default())?;
0bd9c870
DC
270 let inner = Arc::new(Self::with_store_and_config(
271 Arc::new(chunk_store),
272 config,
51d900d1 273 None,
0bd9c870 274 )?);
6da20161
WB
275
276 if let Some(operation) = operation {
277 update_active_operations(&name, operation, 1)?;
278 }
279
280 Ok(Arc::new(Self { inner, operation }))
281 }
282
283 fn with_store_and_config(
0bd9c870 284 chunk_store: Arc<ChunkStore>,
118deb4d 285 config: DataStoreConfig,
51d900d1 286 last_digest: Option<[u8; 32]>,
4bc84a65 287 ) -> Result<DataStoreImpl, Error> {
b683fd58
DC
288 let mut gc_status_path = chunk_store.base_path();
289 gc_status_path.push(".gc-status");
290
291 let gc_status = if let Some(state) = file_read_optional_string(gc_status_path)? {
292 match serde_json::from_str(&state) {
293 Ok(state) => state,
294 Err(err) => {
dce4b540 295 log::error!("error reading gc-status: {}", err);
b683fd58
DC
296 GarbageCollectionStatus::default()
297 }
298 }
299 } else {
300 GarbageCollectionStatus::default()
301 };
f2b99c34 302
fef61684 303 let tuning: DatastoreTuning = serde_json::from_value(
42c2b5be
TL
304 DatastoreTuning::API_SCHEMA
305 .parse_property_string(config.tuning.as_deref().unwrap_or(""))?,
fef61684 306 )?;
fef61684 307
4bc84a65 308 Ok(DataStoreImpl {
0bd9c870 309 chunk_store,
81b2a872 310 gc_mutex: Mutex::new(()),
f2b99c34 311 last_gc_status: Mutex::new(gc_status),
0698f78d 312 verify_new: config.verify_new.unwrap_or(false),
c40a2f8b 313 chunk_order: tuning.chunk_order.unwrap_or_default(),
51d900d1 314 last_digest,
647186dd 315 sync_level: tuning.sync_level.unwrap_or_default(),
529de6c7
DM
316 })
317 }
318
d59397e6
WB
319 pub fn get_chunk_iterator(
320 &self,
321 ) -> Result<
25877d05 322 impl Iterator<Item = (Result<proxmox_sys::fs::ReadDirEntry, Error>, usize, bool)>,
42c2b5be 323 Error,
d59397e6 324 > {
4bc84a65 325 self.inner.chunk_store.get_chunk_iterator()
d59397e6
WB
326 }
327
42c2b5be
TL
328 pub fn create_fixed_writer<P: AsRef<Path>>(
329 &self,
330 filename: P,
331 size: usize,
332 chunk_size: usize,
333 ) -> Result<FixedIndexWriter, Error> {
334 let index = FixedIndexWriter::create(
335 self.inner.chunk_store.clone(),
336 filename.as_ref(),
337 size,
338 chunk_size,
339 )?;
529de6c7
DM
340
341 Ok(index)
342 }
343
42c2b5be
TL
344 pub fn open_fixed_reader<P: AsRef<Path>>(
345 &self,
346 filename: P,
347 ) -> Result<FixedIndexReader, Error> {
348 let full_path = self.inner.chunk_store.relative_path(filename.as_ref());
a7c72ad9
DM
349
350 let index = FixedIndexReader::open(&full_path)?;
529de6c7
DM
351
352 Ok(index)
353 }
3d5c11e5 354
93d5d779 355 pub fn create_dynamic_writer<P: AsRef<Path>>(
42c2b5be
TL
356 &self,
357 filename: P,
93d5d779 358 ) -> Result<DynamicIndexWriter, Error> {
42c2b5be 359 let index = DynamicIndexWriter::create(self.inner.chunk_store.clone(), filename.as_ref())?;
0433db19
DM
360
361 Ok(index)
362 }
ff3d3100 363
42c2b5be
TL
364 pub fn open_dynamic_reader<P: AsRef<Path>>(
365 &self,
366 filename: P,
367 ) -> Result<DynamicIndexReader, Error> {
368 let full_path = self.inner.chunk_store.relative_path(filename.as_ref());
d48a9955
DM
369
370 let index = DynamicIndexReader::open(&full_path)?;
77703d95
DM
371
372 Ok(index)
373 }
374
5de2bced
WB
375 pub fn open_index<P>(&self, filename: P) -> Result<Box<dyn IndexFile + Send>, Error>
376 where
377 P: AsRef<Path>,
378 {
379 let filename = filename.as_ref();
42c2b5be
TL
380 let out: Box<dyn IndexFile + Send> = match archive_type(filename)? {
381 ArchiveType::DynamicIndex => Box::new(self.open_dynamic_reader(filename)?),
382 ArchiveType::FixedIndex => Box::new(self.open_fixed_reader(filename)?),
383 _ => bail!("cannot open index file of unknown type: {:?}", filename),
384 };
5de2bced
WB
385 Ok(out)
386 }
387
1369bcdb 388 /// Fast index verification - only check if chunks exists
28570d19
DM
389 pub fn fast_index_verification(
390 &self,
391 index: &dyn IndexFile,
42c2b5be 392 checked: &mut HashSet<[u8; 32]>,
28570d19 393 ) -> Result<(), Error> {
1369bcdb
DM
394 for pos in 0..index.index_count() {
395 let info = index.chunk_info(pos).unwrap();
28570d19
DM
396 if checked.contains(&info.digest) {
397 continue;
398 }
399
42c2b5be
TL
400 self.stat_chunk(&info.digest).map_err(|err| {
401 format_err!(
402 "fast_index_verification error, stat_chunk {} failed - {}",
16f6766a 403 hex::encode(info.digest),
42c2b5be
TL
404 err,
405 )
406 })?;
28570d19
DM
407
408 checked.insert(info.digest);
1369bcdb
DM
409 }
410
411 Ok(())
412 }
413
60f9a6ea 414 pub fn name(&self) -> &str {
4bc84a65 415 self.inner.chunk_store.name()
60f9a6ea
DM
416 }
417
ff3d3100 418 pub fn base_path(&self) -> PathBuf {
4bc84a65 419 self.inner.chunk_store.base_path()
ff3d3100
DM
420 }
421
133d718f 422 /// Returns the absolute path for a backup namespace on this datastore
8c74349b
WB
423 pub fn namespace_path(&self, ns: &BackupNamespace) -> PathBuf {
424 let mut path = self.base_path();
425 path.reserve(ns.path_len());
426 for part in ns.components() {
427 path.push("ns");
428 path.push(part);
429 }
430 path
431 }
432
5444b914
WB
433 /// Returns the absolute path for a backup_type
434 pub fn type_path(&self, ns: &BackupNamespace, backup_type: BackupType) -> PathBuf {
435 let mut full_path = self.namespace_path(ns);
436 full_path.push(backup_type.to_string());
437 full_path
438 }
439
41b373ec 440 /// Returns the absolute path for a backup_group
133d718f
WB
441 pub fn group_path(
442 &self,
443 ns: &BackupNamespace,
444 backup_group: &pbs_api_types::BackupGroup,
445 ) -> PathBuf {
446 let mut full_path = self.namespace_path(ns);
db87d93e 447 full_path.push(backup_group.to_string());
41b373ec
DM
448 full_path
449 }
450
451 /// Returns the absolute path for backup_dir
133d718f
WB
452 pub fn snapshot_path(
453 &self,
454 ns: &BackupNamespace,
455 backup_dir: &pbs_api_types::BackupDir,
456 ) -> PathBuf {
457 let mut full_path = self.namespace_path(ns);
db87d93e 458 full_path.push(backup_dir.to_string());
41b373ec
DM
459 full_path
460 }
461
dc3d716b
TL
462 /// Create a backup namespace.
463 pub fn create_namespace(
464 self: &Arc<Self>,
465 parent: &BackupNamespace,
466 name: String,
467 ) -> Result<BackupNamespace, Error> {
fc99c279 468 if !self.namespace_exists(parent) {
dc3d716b
TL
469 bail!("cannot create new namespace, parent {parent} doesn't already exists");
470 }
471
472 // construct ns before mkdir to enforce max-depth and name validity
473 let ns = BackupNamespace::from_parent_ns(parent, name)?;
474
ca3f8757 475 let mut ns_full_path = self.base_path();
dc3d716b
TL
476 ns_full_path.push(ns.path());
477
478 std::fs::create_dir_all(ns_full_path)?;
479
480 Ok(ns)
481 }
482
fc99c279
DC
483 /// Returns if the given namespace exists on the datastore
484 pub fn namespace_exists(&self, ns: &BackupNamespace) -> bool {
ca3f8757 485 let mut path = self.base_path();
fc99c279
DC
486 path.push(ns.path());
487 path.exists()
488 }
489
4c7cc5b3
TL
490 /// Remove all backup groups of a single namespace level but not the namespace itself.
491 ///
492 /// Does *not* descends into child-namespaces and doesn't remoes the namespace itself either.
493 ///
494 /// Returns true if all the groups were removed, and false if some were protected.
495 pub fn remove_namespace_groups(self: &Arc<Self>, ns: &BackupNamespace) -> Result<bool, Error> {
496 // FIXME: locking? The single groups/snapshots are already protected, so may not be
74cad4a8 497 // necessary (depends on what we all allow to do with namespaces)
4c7cc5b3
TL
498 log::info!("removing all groups in namespace {}:/{ns}", self.name());
499
500 let mut removed_all_groups = true;
501
502 for group in self.iter_backup_groups(ns.to_owned())? {
524ed404
CE
503 let delete_stats = group?.destroy()?;
504 removed_all_groups = removed_all_groups && delete_stats.all_removed();
4c7cc5b3
TL
505 }
506
507 let base_file = std::fs::File::open(self.base_path())?;
508 let base_fd = base_file.as_raw_fd();
509 for ty in BackupType::iter() {
510 let mut ty_dir = ns.path();
511 ty_dir.push(ty.to_string());
512 // best effort only, but we probably should log the error
513 if let Err(err) = unlinkat(Some(base_fd), &ty_dir, UnlinkatFlags::RemoveDir) {
11ca8343 514 if err != nix::errno::Errno::ENOENT {
4c7cc5b3
TL
515 log::error!("failed to remove backup type {ty} in {ns} - {err}");
516 }
517 }
518 }
519
520 Ok(removed_all_groups)
521 }
522
d1f9ccea
TL
523 /// Remove a complete backup namespace optionally including all it's, and child namespaces',
524 /// groups. If `removed_groups` is false this only prunes empty namespaces.
4c7cc5b3 525 ///
d1f9ccea
TL
526 /// Returns true if everything requested, and false if some groups were protected or if some
527 /// namespaces weren't empty even though all groups were deleted (race with new backup)
4c7cc5b3
TL
528 pub fn remove_namespace_recursive(
529 self: &Arc<Self>,
530 ns: &BackupNamespace,
d1f9ccea 531 delete_groups: bool,
4c7cc5b3 532 ) -> Result<bool, Error> {
d1f9ccea
TL
533 let store = self.name();
534 let mut removed_all_requested = true;
535 if delete_groups {
536 log::info!("removing whole namespace recursively below {store}:/{ns}",);
537 for ns in self.recursive_iter_backup_ns(ns.to_owned())? {
538 let removed_ns_groups = self.remove_namespace_groups(&ns?)?;
539 removed_all_requested = removed_all_requested && removed_ns_groups;
540 }
541 } else {
542 log::info!("pruning empty namespace recursively below {store}:/{ns}");
4c7cc5b3
TL
543 }
544
545 // now try to delete the actual namespaces, bottom up so that we can use safe rmdir that
546 // will choke if a new backup/group appeared in the meantime (but not on an new empty NS)
547 let mut children = self
548 .recursive_iter_backup_ns(ns.to_owned())?
549 .collect::<Result<Vec<BackupNamespace>, Error>>()?;
550
bc001e12 551 children.sort_by_key(|b| std::cmp::Reverse(b.depth()));
4c7cc5b3
TL
552
553 let base_file = std::fs::File::open(self.base_path())?;
554 let base_fd = base_file.as_raw_fd();
555
556 for ns in children.iter() {
557 let mut ns_dir = ns.path();
558 ns_dir.push("ns");
559 let _ = unlinkat(Some(base_fd), &ns_dir, UnlinkatFlags::RemoveDir);
560
561 if !ns.is_root() {
562 match unlinkat(Some(base_fd), &ns.path(), UnlinkatFlags::RemoveDir) {
d1f9ccea 563 Ok(()) => log::debug!("removed namespace {ns}"),
11ca8343 564 Err(nix::errno::Errno::ENOENT) => {
d1f9ccea
TL
565 log::debug!("namespace {ns} already removed")
566 }
11ca8343 567 Err(nix::errno::Errno::ENOTEMPTY) if !delete_groups => {
3aafa613 568 removed_all_requested = false;
d1f9ccea
TL
569 log::debug!("skip removal of non-empty namespace {ns}")
570 }
571 Err(err) => {
572 removed_all_requested = false;
573 log::warn!("failed to remove namespace {ns} - {err}")
574 }
4c7cc5b3
TL
575 }
576 }
577 }
578
d1f9ccea 579 Ok(removed_all_requested)
4c7cc5b3
TL
580 }
581
f03649b8
TL
582 /// Remove a complete backup group including all snapshots.
583 ///
524ed404
CE
584 /// Returns `BackupGroupDeleteStats`, containing the number of deleted snapshots
585 /// and number of protected snaphsots, which therefore were not removed.
db87d93e 586 pub fn remove_backup_group(
6da20161 587 self: &Arc<Self>,
133d718f 588 ns: &BackupNamespace,
db87d93e 589 backup_group: &pbs_api_types::BackupGroup,
524ed404 590 ) -> Result<BackupGroupDeleteStats, Error> {
133d718f 591 let backup_group = self.backup_group(ns.clone(), backup_group.clone());
db87d93e 592
f03649b8 593 backup_group.destroy()
4b4eba0b
DM
594 }
595
8f579717 596 /// Remove a backup directory including all content
db87d93e 597 pub fn remove_backup_dir(
6da20161 598 self: &Arc<Self>,
133d718f 599 ns: &BackupNamespace,
db87d93e
WB
600 backup_dir: &pbs_api_types::BackupDir,
601 force: bool,
602 ) -> Result<(), Error> {
133d718f 603 let backup_dir = self.backup_dir(ns.clone(), backup_dir.clone())?;
db87d93e 604
f03649b8 605 backup_dir.destroy(force)
8f579717
DM
606 }
607
41b373ec
DM
608 /// Returns the time of the last successful backup
609 ///
610 /// Or None if there is no backup in the group (or the group dir does not exist).
db87d93e 611 pub fn last_successful_backup(
6da20161 612 self: &Arc<Self>,
133d718f 613 ns: &BackupNamespace,
db87d93e
WB
614 backup_group: &pbs_api_types::BackupGroup,
615 ) -> Result<Option<i64>, Error> {
133d718f 616 let backup_group = self.backup_group(ns.clone(), backup_group.clone());
db87d93e 617
4b77d300 618 let group_path = backup_group.full_group_path();
41b373ec
DM
619
620 if group_path.exists() {
6da20161 621 backup_group.last_successful_backup()
41b373ec
DM
622 } else {
623 Ok(None)
624 }
625 }
626
133d718f
WB
627 /// Return the path of the 'owner' file.
628 fn owner_path(&self, ns: &BackupNamespace, group: &pbs_api_types::BackupGroup) -> PathBuf {
629 self.group_path(ns, group).join("owner")
630 }
631
54552dda
DM
632 /// Returns the backup owner.
633 ///
e6dc35ac 634 /// The backup owner is the entity who first created the backup group.
133d718f
WB
635 pub fn get_owner(
636 &self,
637 ns: &BackupNamespace,
638 backup_group: &pbs_api_types::BackupGroup,
639 ) -> Result<Authid, Error> {
640 let full_path = self.owner_path(ns, backup_group);
25877d05 641 let owner = proxmox_sys::fs::file_read_firstline(full_path)?;
de6b0ea3
CE
642 owner
643 .trim_end() // remove trailing newline
644 .parse()
fa86b05d 645 .map_err(|err| format_err!("parsing owner for {backup_group} failed: {err}"))
54552dda
DM
646 }
647
db87d93e
WB
648 pub fn owns_backup(
649 &self,
133d718f 650 ns: &BackupNamespace,
db87d93e
WB
651 backup_group: &pbs_api_types::BackupGroup,
652 auth_id: &Authid,
653 ) -> Result<bool, Error> {
133d718f 654 let owner = self.get_owner(ns, backup_group)?;
9751ef4b 655
8e0b852f 656 Ok(check_backup_owner(&owner, auth_id).is_ok())
9751ef4b
DC
657 }
658
54552dda 659 /// Set the backup owner.
e7cb4dc5
WB
660 pub fn set_owner(
661 &self,
133d718f 662 ns: &BackupNamespace,
db87d93e 663 backup_group: &pbs_api_types::BackupGroup,
e6dc35ac 664 auth_id: &Authid,
e7cb4dc5
WB
665 force: bool,
666 ) -> Result<(), Error> {
133d718f 667 let path = self.owner_path(ns, backup_group);
54552dda
DM
668
669 let mut open_options = std::fs::OpenOptions::new();
670 open_options.write(true);
671 open_options.truncate(true);
672
673 if force {
674 open_options.create(true);
675 } else {
676 open_options.create_new(true);
677 }
678
42c2b5be
TL
679 let mut file = open_options
680 .open(&path)
54552dda
DM
681 .map_err(|err| format_err!("unable to create owner file {:?} - {}", path, err))?;
682
e6dc35ac 683 writeln!(file, "{}", auth_id)
54552dda
DM
684 .map_err(|err| format_err!("unable to write owner file {:?} - {}", path, err))?;
685
686 Ok(())
687 }
688
1fc82c41 689 /// Create (if it does not already exists) and lock a backup group
54552dda
DM
690 ///
691 /// And set the owner to 'userid'. If the group already exists, it returns the
692 /// current owner (instead of setting the owner).
1fc82c41 693 ///
1ffe0301 694 /// This also acquires an exclusive lock on the directory and returns the lock guard.
e7cb4dc5
WB
695 pub fn create_locked_backup_group(
696 &self,
133d718f 697 ns: &BackupNamespace,
db87d93e 698 backup_group: &pbs_api_types::BackupGroup,
e6dc35ac
FG
699 auth_id: &Authid,
700 ) -> Result<(Authid, DirLockGuard), Error> {
8731e40a 701 // create intermediate path first:
44288184 702 let mut full_path = self.base_path();
133d718f 703 for ns in ns.components() {
8c74349b
WB
704 full_path.push("ns");
705 full_path.push(ns);
706 }
db87d93e 707 full_path.push(backup_group.ty.as_str());
8731e40a
WB
708 std::fs::create_dir_all(&full_path)?;
709
db87d93e 710 full_path.push(&backup_group.id);
54552dda
DM
711
712 // create the last component now
713 match std::fs::create_dir(&full_path) {
714 Ok(_) => {
42c2b5be
TL
715 let guard = lock_dir_noblock(
716 &full_path,
717 "backup group",
718 "another backup is already running",
719 )?;
133d718f
WB
720 self.set_owner(ns, backup_group, auth_id, false)?;
721 let owner = self.get_owner(ns, backup_group)?; // just to be sure
1fc82c41 722 Ok((owner, guard))
54552dda
DM
723 }
724 Err(ref err) if err.kind() == io::ErrorKind::AlreadyExists => {
42c2b5be
TL
725 let guard = lock_dir_noblock(
726 &full_path,
727 "backup group",
728 "another backup is already running",
729 )?;
133d718f 730 let owner = self.get_owner(ns, backup_group)?; // just to be sure
1fc82c41 731 Ok((owner, guard))
54552dda
DM
732 }
733 Err(err) => bail!("unable to create backup group {:?} - {}", full_path, err),
734 }
735 }
736
737 /// Creates a new backup snapshot inside a BackupGroup
738 ///
739 /// The BackupGroup directory needs to exist.
42c2b5be
TL
740 pub fn create_locked_backup_dir(
741 &self,
133d718f 742 ns: &BackupNamespace,
db87d93e 743 backup_dir: &pbs_api_types::BackupDir,
42c2b5be 744 ) -> Result<(PathBuf, bool, DirLockGuard), Error> {
133d718f
WB
745 let full_path = self.snapshot_path(ns, backup_dir);
746 let relative_path = full_path.strip_prefix(self.base_path()).map_err(|err| {
747 format_err!(
748 "failed to produce correct path for backup {backup_dir} in namespace {ns}: {err}"
749 )
750 })?;
ff3d3100 751
42c2b5be
TL
752 let lock = || {
753 lock_dir_noblock(
754 &full_path,
755 "snapshot",
756 "internal error - tried creating snapshot that's already in use",
757 )
758 };
f23f7543 759
8731e40a 760 match std::fs::create_dir(&full_path) {
133d718f 761 Ok(_) => Ok((relative_path.to_owned(), true, lock()?)),
42c2b5be 762 Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => {
133d718f 763 Ok((relative_path.to_owned(), false, lock()?))
42c2b5be
TL
764 }
765 Err(e) => Err(e.into()),
8731e40a 766 }
ff3d3100
DM
767 }
768
90e38696
TL
769 /// Get a streaming iter over single-level backup namespaces of a datatstore
770 ///
771 /// The iterated item is still a Result that can contain errors from rather unexptected FS or
772 /// parsing errors.
773 pub fn iter_backup_ns(
774 self: &Arc<DataStore>,
775 ns: BackupNamespace,
776 ) -> Result<ListNamespaces, Error> {
777 ListNamespaces::new(Arc::clone(self), ns)
778 }
779
780 /// Get a streaming iter over single-level backup namespaces of a datatstore, filtered by Ok
781 ///
782 /// The iterated item's result is already unwrapped, if it contained an error it will be
783 /// logged. Can be useful in iterator chain commands
784 pub fn iter_backup_ns_ok(
785 self: &Arc<DataStore>,
786 ns: BackupNamespace,
787 ) -> Result<impl Iterator<Item = BackupNamespace> + 'static, Error> {
788 let this = Arc::clone(self);
789 Ok(
ca3f8757 790 ListNamespaces::new(Arc::clone(self), ns)?.filter_map(move |ns| match ns {
90e38696
TL
791 Ok(ns) => Some(ns),
792 Err(err) => {
793 log::error!("list groups error on datastore {} - {}", this.name(), err);
794 None
795 }
796 }),
797 )
798 }
799
800 /// Get a streaming iter over single-level backup namespaces of a datatstore
801 ///
802 /// The iterated item is still a Result that can contain errors from rather unexptected FS or
803 /// parsing errors.
804 pub fn recursive_iter_backup_ns(
805 self: &Arc<DataStore>,
806 ns: BackupNamespace,
807 ) -> Result<ListNamespacesRecursive, Error> {
808 ListNamespacesRecursive::new(Arc::clone(self), ns)
809 }
810
811 /// Get a streaming iter over single-level backup namespaces of a datatstore, filtered by Ok
812 ///
813 /// The iterated item's result is already unwrapped, if it contained an error it will be
814 /// logged. Can be useful in iterator chain commands
815 pub fn recursive_iter_backup_ns_ok(
816 self: &Arc<DataStore>,
817 ns: BackupNamespace,
15a92724 818 max_depth: Option<usize>,
90e38696
TL
819 ) -> Result<impl Iterator<Item = BackupNamespace> + 'static, Error> {
820 let this = Arc::clone(self);
15a92724 821 Ok(if let Some(depth) = max_depth {
ca3f8757 822 ListNamespacesRecursive::new_max_depth(Arc::clone(self), ns, depth)?
15a92724 823 } else {
ca3f8757 824 ListNamespacesRecursive::new(Arc::clone(self), ns)?
15a92724
TL
825 }
826 .filter_map(move |ns| match ns {
827 Ok(ns) => Some(ns),
828 Err(err) => {
829 log::error!("list groups error on datastore {} - {}", this.name(), err);
830 None
831 }
832 }))
90e38696
TL
833 }
834
5444b914
WB
835 /// Get a streaming iter over top-level backup groups of a datatstore of a particular type.
836 ///
837 /// The iterated item is still a Result that can contain errors from rather unexptected FS or
838 /// parsing errors.
839 pub fn iter_backup_type(
840 self: &Arc<DataStore>,
841 ns: BackupNamespace,
842 ty: BackupType,
843 ) -> Result<ListGroupsType, Error> {
844 ListGroupsType::new(Arc::clone(self), ns, ty)
845 }
846
847 /// Get a streaming iter over top-level backup groups of a datatstore of a particular type,
848 /// filtered by `Ok` results
849 ///
850 /// The iterated item's result is already unwrapped, if it contained an error it will be
851 /// logged. Can be useful in iterator chain commands
852 pub fn iter_backup_type_ok(
853 self: &Arc<DataStore>,
854 ns: BackupNamespace,
855 ty: BackupType,
856 ) -> Result<impl Iterator<Item = BackupGroup> + 'static, Error> {
857 Ok(self.iter_backup_type(ns, ty)?.ok())
858 }
859
7b125de3
TL
860 /// Get a streaming iter over top-level backup groups of a datatstore
861 ///
862 /// The iterated item is still a Result that can contain errors from rather unexptected FS or
863 /// parsing errors.
8c74349b
WB
864 pub fn iter_backup_groups(
865 self: &Arc<DataStore>,
866 ns: BackupNamespace,
867 ) -> Result<ListGroups, Error> {
868 ListGroups::new(Arc::clone(self), ns)
7b125de3
TL
869 }
870
871 /// Get a streaming iter over top-level backup groups of a datatstore, filtered by Ok results
872 ///
873 /// The iterated item's result is already unwrapped, if it contained an error it will be
874 /// logged. Can be useful in iterator chain commands
6da20161
WB
875 pub fn iter_backup_groups_ok(
876 self: &Arc<DataStore>,
8c74349b 877 ns: BackupNamespace,
6da20161 878 ) -> Result<impl Iterator<Item = BackupGroup> + 'static, Error> {
5444b914 879 Ok(self.iter_backup_groups(ns)?.ok())
7b125de3
TL
880 }
881
c90dbb5c 882 /// Get a in-memory vector for all top-level backup groups of a datatstore
7b125de3
TL
883 ///
884 /// NOTE: using the iterator directly is most often more efficient w.r.t. memory usage
8c74349b
WB
885 pub fn list_backup_groups(
886 self: &Arc<DataStore>,
887 ns: BackupNamespace,
888 ) -> Result<Vec<BackupGroup>, Error> {
889 ListGroups::new(Arc::clone(self), ns)?.collect()
c90dbb5c
TL
890 }
891
3d5c11e5 892 pub fn list_images(&self) -> Result<Vec<PathBuf>, Error> {
ff3d3100 893 let base = self.base_path();
3d5c11e5
DM
894
895 let mut list = vec![];
896
95cea65b
DM
897 use walkdir::WalkDir;
898
a57413a5 899 let walker = WalkDir::new(base).into_iter();
95cea65b
DM
900
901 // make sure we skip .chunks (and other hidden files to keep it simple)
902 fn is_hidden(entry: &walkdir::DirEntry) -> bool {
42c2b5be
TL
903 entry
904 .file_name()
95cea65b 905 .to_str()
d8d8af98 906 .map(|s| s.starts_with('.'))
95cea65b
DM
907 .unwrap_or(false)
908 }
c3b090ac 909 let handle_entry_err = |err: walkdir::Error| {
5b16dffc
TL
910 // first, extract the actual IO error and the affected path
911 let (inner, path) = match (err.io_error(), err.path()) {
912 (None, _) => return Ok(()), // not an IO-error
913 (Some(inner), Some(path)) => (inner, path),
914 (Some(inner), None) => bail!("unexpected error on datastore traversal: {inner}"),
915 };
916 if inner.kind() == io::ErrorKind::PermissionDenied {
0d69dcb4 917 if err.depth() <= 1 && path.ends_with("lost+found") {
8fcd709c 918 // allow skipping of (root-only) ext4 fsck-directory on EPERM ..
5b16dffc 919 return Ok(());
c3b090ac 920 }
8fcd709c
FG
921 // .. but do not ignore EPERM in general, otherwise we might prune too many chunks.
922 // E.g., if users messed up with owner/perms on a rsync
5b16dffc 923 bail!("cannot continue garbage-collection safely, permission denied on: {path:?}");
9d1ba51d
GG
924 } else if inner.kind() == io::ErrorKind::NotFound {
925 log::info!("ignoring vanished file: {path:?}");
926 return Ok(());
5b16dffc
TL
927 } else {
928 bail!("unexpected error on datastore traversal: {inner} - {path:?}");
c3b090ac 929 }
c3b090ac 930 };
95cea65b 931 for entry in walker.filter_entry(|e| !is_hidden(e)) {
c3b090ac
TL
932 let path = match entry {
933 Ok(entry) => entry.into_path(),
934 Err(err) => {
935 handle_entry_err(err)?;
42c2b5be
TL
936 continue;
937 }
c3b090ac 938 };
1e8da0a7 939 if let Ok(archive_type) = archive_type(&path) {
42c2b5be
TL
940 if archive_type == ArchiveType::FixedIndex
941 || archive_type == ArchiveType::DynamicIndex
942 {
95cea65b 943 list.push(path);
3d5c11e5
DM
944 }
945 }
946 }
947
948 Ok(list)
949 }
950
a660978c
DM
951 // mark chunks used by ``index`` as used
952 fn index_mark_used_chunks<I: IndexFile>(
953 &self,
954 index: I,
955 file_name: &Path, // only used for error reporting
956 status: &mut GarbageCollectionStatus,
c8449217 957 worker: &dyn WorkerTaskContext,
a660978c 958 ) -> Result<(), Error> {
a660978c
DM
959 status.index_file_count += 1;
960 status.index_data_bytes += index.index_bytes();
961
962 for pos in 0..index.index_count() {
f6b1d1cc 963 worker.check_abort()?;
0fd55b08 964 worker.fail_on_shutdown()?;
a660978c 965 let digest = index.index_digest(pos).unwrap();
4bc84a65 966 if !self.inner.chunk_store.cond_touch_chunk(digest, false)? {
ca3f8757 967 let hex = hex::encode(digest);
c23192d3 968 task_warn!(
f6b1d1cc 969 worker,
ca3f8757 970 "warning: unable to access non-existent chunk {hex}, required by {file_name:?}"
f6b1d1cc 971 );
fd192564
SR
972
973 // touch any corresponding .bad files to keep them around, meaning if a chunk is
974 // rewritten correctly they will be removed automatically, as well as if no index
975 // file requires the chunk anymore (won't get to this loop then)
976 for i in 0..=9 {
977 let bad_ext = format!("{}.bad", i);
978 let mut bad_path = PathBuf::new();
979 bad_path.push(self.chunk_path(digest).0);
980 bad_path.set_extension(bad_ext);
4bc84a65 981 self.inner.chunk_store.cond_touch_path(&bad_path, false)?;
fd192564 982 }
a660978c
DM
983 }
984 }
985 Ok(())
986 }
987
f6b1d1cc
WB
988 fn mark_used_chunks(
989 &self,
990 status: &mut GarbageCollectionStatus,
c8449217 991 worker: &dyn WorkerTaskContext,
f6b1d1cc 992 ) -> Result<(), Error> {
3d5c11e5 993 let image_list = self.list_images()?;
8317873c
DM
994 let image_count = image_list.len();
995
8317873c
DM
996 let mut last_percentage: usize = 0;
997
cb4b721c
FG
998 let mut strange_paths_count: u64 = 0;
999
ea368a06 1000 for (i, img) in image_list.into_iter().enumerate() {
f6b1d1cc 1001 worker.check_abort()?;
0fd55b08 1002 worker.fail_on_shutdown()?;
92da93b2 1003
cb4b721c
FG
1004 if let Some(backup_dir_path) = img.parent() {
1005 let backup_dir_path = backup_dir_path.strip_prefix(self.base_path())?;
1006 if let Some(backup_dir_str) = backup_dir_path.to_str() {
161a8864 1007 if pbs_api_types::parse_ns_and_snapshot(backup_dir_str).is_err() {
cb4b721c
FG
1008 strange_paths_count += 1;
1009 }
1010 }
1011 }
1012
efcac39d 1013 match std::fs::File::open(&img) {
e0762002 1014 Ok(file) => {
788d82d9 1015 if let Ok(archive_type) = archive_type(&img) {
e0762002 1016 if archive_type == ArchiveType::FixedIndex {
788d82d9 1017 let index = FixedIndexReader::new(file).map_err(|e| {
efcac39d 1018 format_err!("can't read index '{}' - {}", img.to_string_lossy(), e)
2f0b9235 1019 })?;
788d82d9 1020 self.index_mark_used_chunks(index, &img, status, worker)?;
e0762002 1021 } else if archive_type == ArchiveType::DynamicIndex {
788d82d9 1022 let index = DynamicIndexReader::new(file).map_err(|e| {
efcac39d 1023 format_err!("can't read index '{}' - {}", img.to_string_lossy(), e)
2f0b9235 1024 })?;
788d82d9 1025 self.index_mark_used_chunks(index, &img, status, worker)?;
e0762002
DM
1026 }
1027 }
1028 }
788d82d9 1029 Err(err) if err.kind() == io::ErrorKind::NotFound => (), // ignore vanished files
efcac39d 1030 Err(err) => bail!("can't open index {} - {}", img.to_string_lossy(), err),
77703d95 1031 }
8317873c 1032
ea368a06 1033 let percentage = (i + 1) * 100 / image_count;
8317873c 1034 if percentage > last_percentage {
c23192d3 1035 task_log!(
f6b1d1cc 1036 worker,
7956877f 1037 "marked {}% ({} of {} index files)",
f6b1d1cc 1038 percentage,
ea368a06 1039 i + 1,
f6b1d1cc
WB
1040 image_count,
1041 );
8317873c
DM
1042 last_percentage = percentage;
1043 }
3d5c11e5
DM
1044 }
1045
cb4b721c 1046 if strange_paths_count > 0 {
c23192d3 1047 task_log!(
cb4b721c
FG
1048 worker,
1049 "found (and marked) {} index files outside of expected directory scheme",
1050 strange_paths_count,
1051 );
1052 }
1053
3d5c11e5 1054 Ok(())
f2b99c34
DM
1055 }
1056
1057 pub fn last_gc_status(&self) -> GarbageCollectionStatus {
4bc84a65 1058 self.inner.last_gc_status.lock().unwrap().clone()
f2b99c34 1059 }
3d5c11e5 1060
8545480a 1061 pub fn garbage_collection_running(&self) -> bool {
1c3f1e7c 1062 self.inner.gc_mutex.try_lock().is_err()
8545480a
DM
1063 }
1064
42c2b5be
TL
1065 pub fn garbage_collection(
1066 &self,
1067 worker: &dyn WorkerTaskContext,
1068 upid: &UPID,
1069 ) -> Result<(), Error> {
4bc84a65 1070 if let Ok(ref mut _mutex) = self.inner.gc_mutex.try_lock() {
c6772c92
TL
1071 // avoids that we run GC if an old daemon process has still a
1072 // running backup writer, which is not save as we have no "oldest
1073 // writer" information and thus no safe atime cutoff
42c2b5be 1074 let _exclusive_lock = self.inner.chunk_store.try_exclusive_lock()?;
43b13033 1075
6ef1b649 1076 let phase1_start_time = proxmox_time::epoch_i64();
42c2b5be
TL
1077 let oldest_writer = self
1078 .inner
1079 .chunk_store
1080 .oldest_writer()
1081 .unwrap_or(phase1_start_time);
11861a48 1082
bc001e12
TL
1083 let mut gc_status = GarbageCollectionStatus {
1084 upid: Some(upid.to_string()),
1085 ..Default::default()
1086 };
f6b1d1cc 1087
c23192d3 1088 task_log!(worker, "Start GC phase1 (mark used chunks)");
f6b1d1cc
WB
1089
1090 self.mark_used_chunks(&mut gc_status, worker)?;
1091
c23192d3 1092 task_log!(worker, "Start GC phase2 (sweep unused chunks)");
4bc84a65 1093 self.inner.chunk_store.sweep_unused_chunks(
f6b1d1cc
WB
1094 oldest_writer,
1095 phase1_start_time,
1096 &mut gc_status,
1097 worker,
1098 )?;
1099
c23192d3 1100 task_log!(
f6b1d1cc
WB
1101 worker,
1102 "Removed garbage: {}",
1103 HumanByte::from(gc_status.removed_bytes),
1104 );
c23192d3 1105 task_log!(worker, "Removed chunks: {}", gc_status.removed_chunks);
cf459b19 1106 if gc_status.pending_bytes > 0 {
c23192d3 1107 task_log!(
f6b1d1cc
WB
1108 worker,
1109 "Pending removals: {} (in {} chunks)",
1110 HumanByte::from(gc_status.pending_bytes),
1111 gc_status.pending_chunks,
1112 );
cf459b19 1113 }
a9767cf7 1114 if gc_status.removed_bad > 0 {
c23192d3 1115 task_log!(worker, "Removed bad chunks: {}", gc_status.removed_bad);
a9767cf7 1116 }
cf459b19 1117
b4fb2623 1118 if gc_status.still_bad > 0 {
c23192d3 1119 task_log!(worker, "Leftover bad chunks: {}", gc_status.still_bad);
b4fb2623
DM
1120 }
1121
c23192d3 1122 task_log!(
f6b1d1cc
WB
1123 worker,
1124 "Original data usage: {}",
1125 HumanByte::from(gc_status.index_data_bytes),
1126 );
868c5852
DM
1127
1128 if gc_status.index_data_bytes > 0 {
42c2b5be
TL
1129 let comp_per =
1130 (gc_status.disk_bytes as f64 * 100.) / gc_status.index_data_bytes as f64;
c23192d3 1131 task_log!(
f6b1d1cc
WB
1132 worker,
1133 "On-Disk usage: {} ({:.2}%)",
1134 HumanByte::from(gc_status.disk_bytes),
1135 comp_per,
1136 );
868c5852
DM
1137 }
1138
c23192d3 1139 task_log!(worker, "On-Disk chunks: {}", gc_status.disk_chunks);
868c5852 1140
d6373f35 1141 let deduplication_factor = if gc_status.disk_bytes > 0 {
42c2b5be 1142 (gc_status.index_data_bytes as f64) / (gc_status.disk_bytes as f64)
d6373f35
DM
1143 } else {
1144 1.0
1145 };
1146
c23192d3 1147 task_log!(worker, "Deduplication factor: {:.2}", deduplication_factor);
d6373f35 1148
868c5852 1149 if gc_status.disk_chunks > 0 {
42c2b5be 1150 let avg_chunk = gc_status.disk_bytes / (gc_status.disk_chunks as u64);
c23192d3 1151 task_log!(worker, "Average chunk size: {}", HumanByte::from(avg_chunk));
868c5852 1152 }
64e53b28 1153
b683fd58
DC
1154 if let Ok(serialized) = serde_json::to_string(&gc_status) {
1155 let mut path = self.base_path();
1156 path.push(".gc-status");
1157
21211748 1158 let backup_user = pbs_config::backup_user()?;
b683fd58
DC
1159 let mode = nix::sys::stat::Mode::from_bits_truncate(0o0644);
1160 // set the correct owner/group/permissions while saving file
1161 // owner(rw) = backup, group(r)= backup
1162 let options = CreateOptions::new()
1163 .perm(mode)
1164 .owner(backup_user.uid)
1165 .group(backup_user.gid);
1166
1167 // ignore errors
e0a19d33 1168 let _ = replace_file(path, serialized.as_bytes(), options, false);
b683fd58
DC
1169 }
1170
4bc84a65 1171 *self.inner.last_gc_status.lock().unwrap() = gc_status;
64e53b28 1172 } else {
d4b59ae0 1173 bail!("Start GC failed - (already running/locked)");
64e53b28 1174 }
3d5c11e5
DM
1175
1176 Ok(())
1177 }
3b7ade9e 1178
ccc3896f 1179 pub fn try_shared_chunk_store_lock(&self) -> Result<ProcessLockSharedGuard, Error> {
4bc84a65 1180 self.inner.chunk_store.try_shared_lock()
1cf5178a
DM
1181 }
1182
42c2b5be 1183 pub fn chunk_path(&self, digest: &[u8; 32]) -> (PathBuf, String) {
4bc84a65 1184 self.inner.chunk_store.chunk_path(digest)
d48a9955
DM
1185 }
1186
b298e9f1 1187 pub fn cond_touch_chunk(&self, digest: &[u8; 32], assert_exists: bool) -> Result<bool, Error> {
42c2b5be
TL
1188 self.inner
1189 .chunk_store
b298e9f1 1190 .cond_touch_chunk(digest, assert_exists)
42c2b5be
TL
1191 }
1192
1193 pub fn insert_chunk(&self, chunk: &DataBlob, digest: &[u8; 32]) -> Result<(bool, u64), Error> {
4bc84a65 1194 self.inner.chunk_store.insert_chunk(chunk, digest)
3b7ade9e 1195 }
60f9a6ea 1196
7f394c80 1197 pub fn stat_chunk(&self, digest: &[u8; 32]) -> Result<std::fs::Metadata, Error> {
4bc84a65 1198 let (chunk_path, _digest_str) = self.inner.chunk_store.chunk_path(digest);
7f394c80
DC
1199 std::fs::metadata(chunk_path).map_err(Error::from)
1200 }
1201
39f18b30 1202 pub fn load_chunk(&self, digest: &[u8; 32]) -> Result<DataBlob, Error> {
4bc84a65 1203 let (chunk_path, digest_str) = self.inner.chunk_store.chunk_path(digest);
39f18b30 1204
6ef1b649 1205 proxmox_lang::try_block!({
39f18b30
DM
1206 let mut file = std::fs::File::open(&chunk_path)?;
1207 DataBlob::load_from_reader(&mut file)
42c2b5be
TL
1208 })
1209 .map_err(|err| {
1210 format_err!(
1211 "store '{}', unable to load chunk '{}' - {}",
1212 self.name(),
1213 digest_str,
1214 err,
1215 )
1216 })
1a374fcf
SR
1217 }
1218
8292d3d2 1219 /// Updates the protection status of the specified snapshot.
42c2b5be 1220 pub fn update_protection(&self, backup_dir: &BackupDir, protection: bool) -> Result<(), Error> {
6da20161 1221 let full_path = backup_dir.full_path();
8292d3d2 1222
c78437e3
FG
1223 if !full_path.exists() {
1224 bail!("snapshot {} does not exist!", backup_dir.dir());
1225 }
1226
8292d3d2
DC
1227 let _guard = lock_dir_noblock(&full_path, "snapshot", "possibly running or in use")?;
1228
6da20161 1229 let protected_path = backup_dir.protected_file();
8292d3d2
DC
1230 if protection {
1231 std::fs::File::create(protected_path)
1232 .map_err(|err| format_err!("could not create protection file: {}", err))?;
1233 } else if let Err(err) = std::fs::remove_file(protected_path) {
1234 // ignore error for non-existing file
1235 if err.kind() != std::io::ErrorKind::NotFound {
1236 bail!("could not remove protection file: {}", err);
1237 }
1238 }
1239
1240 Ok(())
1241 }
1242
0698f78d 1243 pub fn verify_new(&self) -> bool {
4bc84a65 1244 self.inner.verify_new
0698f78d 1245 }
4921a411 1246
bc001e12
TL
1247 /// returns a list of chunks sorted by their inode number on disk chunks that couldn't get
1248 /// stat'ed are placed at the end of the list
4921a411
DC
1249 pub fn get_chunks_in_order<F, A>(
1250 &self,
e1db0670 1251 index: &(dyn IndexFile + Send),
4921a411
DC
1252 skip_chunk: F,
1253 check_abort: A,
1254 ) -> Result<Vec<(usize, u64)>, Error>
1255 where
1256 F: Fn(&[u8; 32]) -> bool,
1257 A: Fn(usize) -> Result<(), Error>,
1258 {
1259 let index_count = index.index_count();
1260 let mut chunk_list = Vec::with_capacity(index_count);
1261 use std::os::unix::fs::MetadataExt;
1262 for pos in 0..index_count {
1263 check_abort(pos)?;
1264
1265 let info = index.chunk_info(pos).unwrap();
1266
1267 if skip_chunk(&info.digest) {
1268 continue;
1269 }
1270
4bc84a65 1271 let ino = match self.inner.chunk_order {
fef61684
DC
1272 ChunkOrder::Inode => {
1273 match self.stat_chunk(&info.digest) {
1274 Err(_) => u64::MAX, // could not stat, move to end of list
1275 Ok(metadata) => metadata.ino(),
1276 }
1277 }
1278 ChunkOrder::None => 0,
4921a411
DC
1279 };
1280
1281 chunk_list.push((pos, ino));
1282 }
1283
4bc84a65 1284 match self.inner.chunk_order {
fef61684
DC
1285 // sorting by inode improves data locality, which makes it lots faster on spinners
1286 ChunkOrder::Inode => {
1287 chunk_list.sort_unstable_by(|(_, ino_a), (_, ino_b)| ino_a.cmp(ino_b))
1288 }
1289 ChunkOrder::None => {}
1290 }
4921a411
DC
1291
1292 Ok(chunk_list)
1293 }
db87d93e 1294
6b0c6492 1295 /// Open a backup group from this datastore.
133d718f
WB
1296 pub fn backup_group(
1297 self: &Arc<Self>,
1298 ns: BackupNamespace,
1299 group: pbs_api_types::BackupGroup,
1300 ) -> BackupGroup {
ca3f8757 1301 BackupGroup::new(Arc::clone(self), ns, group)
db87d93e
WB
1302 }
1303
6b0c6492 1304 /// Open a backup group from this datastore.
8c74349b
WB
1305 pub fn backup_group_from_parts<T>(
1306 self: &Arc<Self>,
1307 ns: BackupNamespace,
1308 ty: BackupType,
1309 id: T,
1310 ) -> BackupGroup
6b0c6492
WB
1311 where
1312 T: Into<String>,
1313 {
133d718f 1314 self.backup_group(ns, (ty, id.into()).into())
6b0c6492
WB
1315 }
1316
133d718f 1317 /*
6b0c6492
WB
1318 /// Open a backup group from this datastore by backup group path such as `vm/100`.
1319 ///
1320 /// Convenience method for `store.backup_group(path.parse()?)`
6da20161 1321 pub fn backup_group_from_path(self: &Arc<Self>, path: &str) -> Result<BackupGroup, Error> {
133d718f 1322 todo!("split out the namespace");
db87d93e 1323 }
133d718f 1324 */
db87d93e 1325
6b0c6492 1326 /// Open a snapshot (backup directory) from this datastore.
133d718f
WB
1327 pub fn backup_dir(
1328 self: &Arc<Self>,
1329 ns: BackupNamespace,
1330 dir: pbs_api_types::BackupDir,
1331 ) -> Result<BackupDir, Error> {
1332 BackupDir::with_group(self.backup_group(ns, dir.group), dir.time)
6b0c6492
WB
1333 }
1334
1335 /// Open a snapshot (backup directory) from this datastore.
db87d93e 1336 pub fn backup_dir_from_parts<T>(
6da20161 1337 self: &Arc<Self>,
8c74349b 1338 ns: BackupNamespace,
db87d93e
WB
1339 ty: BackupType,
1340 id: T,
1341 time: i64,
1342 ) -> Result<BackupDir, Error>
1343 where
1344 T: Into<String>,
1345 {
133d718f 1346 self.backup_dir(ns, (ty, id.into(), time).into())
db87d93e
WB
1347 }
1348
6b0c6492 1349 /// Open a snapshot (backup directory) from this datastore with a cached rfc3339 time string.
db87d93e 1350 pub fn backup_dir_with_rfc3339<T: Into<String>>(
6da20161 1351 self: &Arc<Self>,
db87d93e
WB
1352 group: BackupGroup,
1353 time_string: T,
1354 ) -> Result<BackupDir, Error> {
1355 BackupDir::with_rfc3339(group, time_string.into())
1356 }
1357
133d718f 1358 /*
6b0c6492 1359 /// Open a snapshot (backup directory) from this datastore by a snapshot path.
6da20161 1360 pub fn backup_dir_from_path(self: &Arc<Self>, path: &str) -> Result<BackupDir, Error> {
133d718f 1361 todo!("split out the namespace");
db87d93e 1362 }
133d718f 1363 */
647186dd
DC
1364
1365 /// Syncs the filesystem of the datastore if 'sync_level' is set to
1366 /// [`DatastoreFSyncLevel::Filesystem`]. Uses syncfs(2).
1367 pub fn try_ensure_sync_level(&self) -> Result<(), Error> {
1368 if self.inner.sync_level != DatastoreFSyncLevel::Filesystem {
1369 return Ok(());
1370 }
1371 let file = std::fs::File::open(self.base_path())?;
1372 let fd = file.as_raw_fd();
7be0a3fd 1373 log::info!("syncing filesystem");
647186dd
DC
1374 if unsafe { libc::syncfs(fd) } < 0 {
1375 bail!("error during syncfs: {}", std::io::Error::last_os_error());
1376 }
1377 Ok(())
1378 }
857f346c
WB
1379
1380 /// Destroy a datastore. This requires that there are no active operations on the datastore.
1381 ///
1382 /// This is a synchronous operation and should be run in a worker-thread.
1383 pub fn destroy(
1384 name: &str,
1385 destroy_data: bool,
1386 worker: &dyn WorkerTaskContext,
1387 ) -> Result<(), Error> {
1388 let config_lock = pbs_config::datastore::lock_config()?;
1389
1390 let (mut config, _digest) = pbs_config::datastore::config()?;
1391 let mut datastore_config: DataStoreConfig = config.lookup("datastore", name)?;
1392
1393 datastore_config.maintenance_mode = Some("type=delete".to_string());
1394 config.set_data(name, "datastore", &datastore_config)?;
1395 pbs_config::datastore::save_config(&config)?;
1396 drop(config_lock);
1397
1398 let (operations, _lock) = task_tracking::get_active_operations_locked(name)?;
1399
1400 if operations.read != 0 || operations.write != 0 {
1401 bail!("datastore is currently in use");
1402 }
1403
1404 let base = PathBuf::from(&datastore_config.path);
1405
1406 let mut ok = true;
1407 if destroy_data {
1408 let remove = |subdir, ok: &mut bool| {
1409 if let Err(err) = std::fs::remove_dir_all(base.join(subdir)) {
1410 if err.kind() != io::ErrorKind::NotFound {
1411 task_warn!(worker, "failed to remove {subdir:?} subdirectory: {err}");
1412 *ok = false;
1413 }
1414 }
1415 };
1416
1417 task_log!(worker, "Deleting datastore data...");
1418 remove("ns", &mut ok); // ns first
1419 remove("ct", &mut ok);
1420 remove("vm", &mut ok);
1421 remove("host", &mut ok);
1422
1423 if ok {
1424 if let Err(err) = std::fs::remove_file(base.join(".gc-status")) {
1425 if err.kind() != io::ErrorKind::NotFound {
1426 task_warn!(worker, "failed to remove .gc-status file: {err}");
1427 ok = false;
1428 }
1429 }
1430 }
1431
1432 // chunks get removed last and only if the backups were successfully deleted
1433 if ok {
1434 remove(".chunks", &mut ok);
1435 }
1436 }
1437
1438 // now the config
1439 if ok {
1440 task_log!(worker, "Removing datastore from config...");
1441 let _lock = pbs_config::datastore::lock_config()?;
1442 let _ = config.sections.remove(name);
1443 pbs_config::datastore::save_config(&config)?;
1444 }
1445
1446 // finally the lock & toplevel directory
1447 if destroy_data {
1448 if ok {
1449 if let Err(err) = std::fs::remove_file(base.join(".lock")) {
1450 if err.kind() != io::ErrorKind::NotFound {
1451 task_warn!(worker, "failed to remove .lock file: {err}");
1452 ok = false;
1453 }
1454 }
1455 }
1456
1457 if ok {
1458 task_log!(worker, "Finished deleting data.");
1459
1460 match std::fs::remove_dir(base) {
1461 Ok(()) => task_log!(worker, "Removed empty datastore directory."),
1462 Err(err) if err.kind() == io::ErrorKind::NotFound => {
1463 // weird, but ok
1464 }
1465 Err(err) if err.is_errno(nix::errno::Errno::EBUSY) => {
1466 task_warn!(
1467 worker,
1468 "Cannot delete datastore directory (is it a mount point?)."
1469 )
1470 }
1471 Err(err) if err.is_errno(nix::errno::Errno::ENOTEMPTY) => {
1472 task_warn!(worker, "Datastore directory not empty, not deleting.")
1473 }
1474 Err(err) => {
1475 task_warn!(worker, "Failed to remove datastore directory: {err}");
1476 }
1477 }
1478 } else {
1479 task_log!(worker, "There were errors deleting data.");
1480 }
1481 }
1482
1483 Ok(())
1484 }
529de6c7 1485}