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