Skip to main content

btrfs_cli/balance/
start.rs

1use super::{filters::parse_filters, open_path};
2use crate::{Format, Runnable};
3use anyhow::{Context, Result};
4use btrfs_uapi::balance::{BalanceFlags, balance};
5use clap::Parser;
6use nix::errno::Errno;
7use std::{os::unix::io::AsFd, path::PathBuf, thread, time::Duration};
8
9type Filters = String;
10
11/// Balance chunks across the devices
12///
13/// Balance and/or convert (change allocation profile of) chunks that
14/// passed all filters in a comma-separated list of filters for a
15/// particular chunk type.  If filter list is not given balance all
16/// chunks of that type.  In case none of the -d, -m or -s options is
17/// given balance all chunks in a filesystem. This is potentially
18/// long operation and the user is warned before this start, with
19/// a delay to stop it.
20#[derive(Parser, Debug)]
21pub struct BalanceStartCommand {
22    /// Act on data chunks with optional filters
23    #[clap(long, short)]
24    pub data_filters: Option<Filters>,
25    /// Act on metadata chunks with optional filters
26    #[clap(long, short)]
27    pub metadata_filters: Option<Filters>,
28    /// Act on system chunks (requires force) with optional filters
29    #[clap(long, short, requires = "force")]
30    pub system_filters: Option<Filters>,
31
32    /// Force a reduction of metadata integrity, or skip timeout when converting to RAID56 profiles
33    #[clap(long, short)]
34    pub force: bool,
35
36    /// Do not print warning and do not delay start
37    #[clap(long)]
38    pub full_balance: bool,
39
40    /// Run the balance as a background process
41    #[clap(long, short, alias = "bg")]
42    pub background: bool,
43
44    /// Wait if there's another exclusive operation running, otherwise continue
45    #[clap(long)]
46    pub enqueue: bool,
47
48    pub path: PathBuf,
49}
50
51impl Runnable for BalanceStartCommand {
52    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
53        // TODO: background mode (requires daemonizing the process)
54        if self.background {
55            anyhow::bail!("--background is not yet implemented");
56        }
57
58        let has_filters = self.data_filters.is_some()
59            || self.metadata_filters.is_some()
60            || self.system_filters.is_some();
61
62        let mut flags = BalanceFlags::empty();
63
64        let data_args = match &self.data_filters {
65            Some(f) => {
66                flags |= BalanceFlags::DATA;
67                Some(parse_filters(f).context("invalid data filter")?)
68            }
69            None => None,
70        };
71
72        // When metadata is balanced, system is always included with the same
73        // args, matching the behaviour of the C tool.
74        let meta_args = match &self.metadata_filters {
75            Some(f) => {
76                flags |= BalanceFlags::METADATA | BalanceFlags::SYSTEM;
77                Some(parse_filters(f).context("invalid metadata filter")?)
78            }
79            None => None,
80        };
81
82        // System args: explicitly requested, OR copied from meta if meta was
83        // given but system was not explicitly specified (see C reference).
84        let sys_args = match &self.system_filters {
85            Some(f) => {
86                flags |= BalanceFlags::SYSTEM;
87                Some(parse_filters(f).context("invalid system filter")?)
88            }
89            None => meta_args.clone(),
90        };
91
92        if !has_filters {
93            // No type filters specified — relocate everything.
94            flags |= BalanceFlags::DATA
95                | BalanceFlags::METADATA
96                | BalanceFlags::SYSTEM;
97        }
98
99        if self.force {
100            flags |= BalanceFlags::FORCE;
101        }
102
103        // Warn the user about a full (unfiltered) balance and give them a
104        // chance to abort, unless --full-balance was passed.
105        if !has_filters && !self.full_balance {
106            eprintln!("WARNING:\n");
107            eprintln!(
108                "\tFull balance without filters requested. This operation is very"
109            );
110            eprintln!(
111                "\tintense and takes potentially very long. It is recommended to"
112            );
113            eprintln!(
114                "\tuse the balance filters to narrow down the scope of balance."
115            );
116            eprintln!(
117                "\tUse 'btrfs balance start --full-balance' to skip this warning."
118            );
119            eprintln!(
120                "\tThe operation will start in 10 seconds. Use Ctrl-C to stop it."
121            );
122            thread::sleep(Duration::from_secs(10));
123            eprintln!("\nStarting balance without any filters.");
124        }
125
126        let file = open_path(&self.path)?;
127
128        match balance(file.as_fd(), flags, data_args, meta_args, sys_args) {
129            Ok(progress) => {
130                println!(
131                    "Done, had to relocate {} out of {} chunks",
132                    progress.completed, progress.considered
133                );
134                Ok(())
135            }
136            Err(e) if e == Errno::ECANCELED => {
137                // The kernel sets ECANCELED when the balance was paused or
138                // cancelled mid-run; this is not an error from the user's
139                // perspective.
140                eprintln!("Balance was paused or cancelled by user.");
141                Ok(())
142            }
143            Err(e) => Err(e).with_context(|| {
144                format!("error during balancing '{}'", self.path.display())
145            }),
146        }
147    }
148}