Skip to main content

btrfs_cli/quota/
status.rs

1use crate::{Format, RunContext, Runnable, util::open_path};
2use anyhow::{Context, Result};
3use btrfs_uapi::{quota::QuotaRescanStatus, sysfs::SysfsBtrfs};
4use clap::Parser;
5use cols::Cols;
6use std::{os::unix::io::AsFd, path::PathBuf};
7use uuid::Uuid;
8
9/// Show status information about quota on the filesystem
10///
11/// Displays the current quota configuration and accounting state. The
12/// fields shown when quotas are enabled:
13///
14/// Enabled: whether quota accounting is active on this filesystem.
15///
16/// Mode: the accounting mode. "qgroup" (full accounting) tracks every
17/// extent backref and provides accurate shared/exclusive byte counts.
18/// "squota" (simple quotas, kernel 6.7+) uses lighter-weight lifetime
19/// tracking that avoids the overhead of full backref walking but does
20/// not distinguish shared vs exclusive usage.
21///
22/// Inconsistent: if "yes", the qgroup numbers are stale and a rescan
23/// is needed (btrfs quota rescan). This happens after unclean shutdowns
24/// or when qgroups are first enabled on an existing filesystem.
25///
26/// Override limits: when enabled, qgroup limits are not enforced for
27/// the current mount. Writes will succeed even if they exceed the
28/// configured limits. Useful for emergency recovery.
29///
30/// Drop subtree threshold: controls how deep the kernel tracks qgroup
31/// changes when deleting subvolumes. Levels below this threshold skip
32/// detailed per-extent accounting during heavy delete workloads,
33/// trading accuracy for performance. A value of 0 means full tracking.
34///
35/// Total count: the total number of qgroup entries in the quota tree.
36///
37/// Level 0: the number of level-0 qgroups. Each subvolume automatically
38/// gets a level-0 qgroup (e.g. 0/256, 0/257). Higher-level qgroups
39/// (1/0, 2/0, ...) are user-created containers for hierarchical limits.
40#[derive(Parser, Debug)]
41pub struct QuotaStatusCommand {
42    /// Path to a mounted btrfs filesystem
43    pub path: PathBuf,
44
45    /// Only check if quotas are enabled, without printing full status
46    #[clap(long)]
47    pub is_enabled: bool,
48}
49
50fn describe_mode(mode: &str) -> &str {
51    match mode {
52        "qgroup" => "full accounting",
53        "squota" => "simplified accounting",
54        other => other,
55    }
56}
57
58impl Runnable for QuotaStatusCommand {
59    fn run(&self, ctx: &RunContext) -> Result<()> {
60        let file = open_path(&self.path)?;
61        let fd = file.as_fd();
62
63        let fs =
64            btrfs_uapi::filesystem::filesystem_info(fd).with_context(|| {
65                format!(
66                    "failed to get filesystem info for '{}'",
67                    self.path.display()
68                )
69            })?;
70
71        let status =
72            SysfsBtrfs::new(&fs.uuid).quota_status().with_context(|| {
73                format!(
74                    "failed to read quota status for '{}'",
75                    self.path.display()
76                )
77            })?;
78
79        if self.is_enabled {
80            if !status.enabled {
81                // Exit with a non-zero code the way the C tool does, without
82                // printing anything.
83                std::process::exit(1);
84            }
85            return Ok(());
86        }
87
88        let rescan = if status.enabled {
89            Some(btrfs_uapi::quota::quota_rescan_status(fd))
90        } else {
91            None
92        };
93
94        match ctx.format {
95            Format::Modern => {
96                print_status_modern(
97                    &self.path,
98                    &fs.uuid,
99                    &status,
100                    rescan.as_ref(),
101                );
102            }
103            Format::Text => {
104                print_status_text(&self.path, &status);
105            }
106            Format::Json => unreachable!(),
107        }
108
109        Ok(())
110    }
111}
112
113fn print_status_text(
114    path: &std::path::Path,
115    status: &btrfs_uapi::sysfs::QuotaStatus,
116) {
117    println!("Quotas on {}:", path.display());
118
119    if !status.enabled {
120        println!("  Enabled:                 no");
121        return;
122    }
123
124    println!("  Enabled:                 yes");
125
126    if let Some(ref mode) = status.mode {
127        println!(
128            "  Mode:                    {} ({})",
129            mode,
130            describe_mode(mode)
131        );
132    }
133
134    if let Some(inconsistent) = status.inconsistent {
135        println!(
136            "  Inconsistent:            {}{}",
137            if inconsistent { "yes" } else { "no" },
138            if inconsistent { " (rescan needed)" } else { "" }
139        );
140    }
141
142    if let Some(override_limits) = status.override_limits {
143        println!(
144            "  Override limits:         {}",
145            if override_limits { "yes" } else { "no" }
146        );
147    }
148
149    if let Some(threshold) = status.drop_subtree_threshold {
150        println!("  Drop subtree threshold:  {threshold}");
151    }
152
153    if let Some(total) = status.total_count {
154        println!("  Total count:             {total}");
155    }
156
157    if let Some(level0) = status.level0_count {
158        println!("  Level 0:                 {level0}");
159    }
160}
161
162#[derive(Cols)]
163struct StatusRow {
164    #[column(header = "PROPERTY")]
165    label: String,
166    #[column(header = "VALUE")]
167    value: String,
168}
169
170fn print_status_modern(
171    path: &std::path::Path,
172    uuid: &Uuid,
173    status: &btrfs_uapi::sysfs::QuotaStatus,
174    rescan: Option<&nix::Result<QuotaRescanStatus>>,
175) {
176    println!("Quotas on {}:", path.display());
177
178    let mut rows: Vec<StatusRow> = Vec::new();
179
180    rows.push(StatusRow {
181        label: "UUID".to_string(),
182        value: uuid.as_hyphenated().to_string(),
183    });
184
185    rows.push(StatusRow {
186        label: "Enabled".to_string(),
187        value: if status.enabled { "yes" } else { "no" }.to_string(),
188    });
189
190    if !status.enabled {
191        let mut out = std::io::stdout().lock();
192        let _ = StatusRow::print_table(&rows, &mut out);
193        return;
194    }
195
196    if let Some(ref mode) = status.mode {
197        rows.push(StatusRow {
198            label: "Mode".to_string(),
199            value: format!("{mode} ({})", describe_mode(mode)),
200        });
201    }
202
203    if let Some(inconsistent) = status.inconsistent {
204        rows.push(StatusRow {
205            label: "Inconsistent".to_string(),
206            value: if inconsistent {
207                "yes (rescan needed)"
208            } else {
209                "no"
210            }
211            .to_string(),
212        });
213    }
214
215    if let Some(override_limits) = status.override_limits {
216        rows.push(StatusRow {
217            label: "Override limits".to_string(),
218            value: if override_limits { "yes" } else { "no" }.to_string(),
219        });
220    }
221
222    if let Some(threshold) = status.drop_subtree_threshold {
223        rows.push(StatusRow {
224            label: "Drop subtree threshold".to_string(),
225            value: threshold.to_string(),
226        });
227    }
228
229    if let Some(total) = status.total_count {
230        rows.push(StatusRow {
231            label: "Total count".to_string(),
232            value: total.to_string(),
233        });
234    }
235
236    if let Some(level0) = status.level0_count {
237        rows.push(StatusRow {
238            label: "Level 0".to_string(),
239            value: level0.to_string(),
240        });
241    }
242
243    if let Some(rescan_result) = rescan {
244        rows.push(StatusRow {
245            label: "Rescan".to_string(),
246            value: match rescan_result {
247                Ok(rs) if rs.running => {
248                    format!("in progress (objectid {})", rs.progress)
249                }
250                Ok(_) => "not in progress".to_string(),
251                Err(_) => "unknown (insufficient privileges)".to_string(),
252            },
253        });
254    }
255
256    let mut out = std::io::stdout().lock();
257    let _ = StatusRow::print_table(&rows, &mut out);
258}