From 2b8a0f0caf676ba7b2944578fb07ce9ce7605f8a Mon Sep 17 00:00:00 2001 From: Thomas Bertschinger Date: Tue, 30 Apr 2024 18:32:07 -0600 Subject: remove redundant "cmd_" prefix from functions in "commands" It is more idiomatic to use `commands::mount` than `commands::cmd_mount`. No functionality changes intended by this patch. Signed-off-by: Thomas Bertschinger Signed-off-by: Kent Overstreet --- src/bcachefs.rs | 12 +- src/commands/cmd_completions.rs | 19 --- src/commands/cmd_list.rs | 169 --------------------- src/commands/cmd_mount.rs | 316 ---------------------------------------- src/commands/cmd_subvolume.rs | 70 --------- src/commands/completions.rs | 19 +++ src/commands/list.rs | 169 +++++++++++++++++++++ src/commands/mod.rs | 21 ++- src/commands/mount.rs | 316 ++++++++++++++++++++++++++++++++++++++++ src/commands/subvolume.rs | 70 +++++++++ 10 files changed, 591 insertions(+), 590 deletions(-) delete mode 100644 src/commands/cmd_completions.rs delete mode 100644 src/commands/cmd_list.rs delete mode 100644 src/commands/cmd_mount.rs delete mode 100644 src/commands/cmd_subvolume.rs create mode 100644 src/commands/completions.rs create mode 100644 src/commands/list.rs create mode 100644 src/commands/mount.rs create mode 100644 src/commands/subvolume.rs (limited to 'src') diff --git a/src/bcachefs.rs b/src/bcachefs.rs index 079a4155..e8099ffa 100644 --- a/src/bcachefs.rs +++ b/src/bcachefs.rs @@ -4,10 +4,6 @@ mod key; use std::ffi::{c_char, CString}; -use commands::cmd_completions::cmd_completions; -use commands::cmd_list::cmd_list; -use commands::cmd_mount::cmd_mount; -use commands::cmd_subvolume::cmd_subvolumes; use commands::logger::SimpleLogger; use bch_bindgen::c; @@ -110,10 +106,10 @@ fn main() { }; let ret = match cmd { - "completions" => cmd_completions(args[1..].to_vec()), - "list" => cmd_list(args[1..].to_vec()), - "mount" => cmd_mount(args, symlink_cmd), - "subvolume" => cmd_subvolumes(args[1..].to_vec()), + "completions" => commands::completions(args[1..].to_vec()), + "list" => commands::list(args[1..].to_vec()), + "mount" => commands::mount(args, symlink_cmd), + "subvolume" => commands::subvolume(args[1..].to_vec()), _ => handle_c_command(args, symlink_cmd), }; diff --git a/src/commands/cmd_completions.rs b/src/commands/cmd_completions.rs deleted file mode 100644 index 81ee719f..00000000 --- a/src/commands/cmd_completions.rs +++ /dev/null @@ -1,19 +0,0 @@ -use clap::{Command, CommandFactory, Parser}; -use clap_complete::{generate, Generator, Shell}; -use std::io; - -/// Generate shell completions -#[derive(Parser, Debug)] -pub struct Cli { - shell: Shell, -} - -fn print_completions(gen: G, cmd: &mut Command) { - generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout()); -} - -pub fn cmd_completions(argv: Vec) -> i32 { - let cli = Cli::parse_from(argv); - print_completions(cli.shell, &mut super::Cli::command()); - 0 -} diff --git a/src/commands/cmd_list.rs b/src/commands/cmd_list.rs deleted file mode 100644 index 8af075af..00000000 --- a/src/commands/cmd_list.rs +++ /dev/null @@ -1,169 +0,0 @@ -use log::{error}; -use bch_bindgen::bcachefs; -use bch_bindgen::opt_set; -use bch_bindgen::fs::Fs; -use bch_bindgen::bkey::BkeySC; -use bch_bindgen::btree::BtreeTrans; -use bch_bindgen::btree::BtreeIter; -use bch_bindgen::btree::BtreeNodeIter; -use bch_bindgen::btree::BtreeIterFlags; -use clap::{Parser}; -use std::io::{stdout, IsTerminal}; - -fn list_keys(fs: &Fs, opt: Cli) -> anyhow::Result<()> { - let trans = BtreeTrans::new(fs); - let mut iter = BtreeIter::new(&trans, opt.btree, opt.start, - BtreeIterFlags::ALL_SNAPSHOTS| - BtreeIterFlags::PREFETCH); - - while let Some(k) = iter.peek_and_restart()? { - if k.k.p > opt.end { - break; - } - - println!("{}", k.to_text(fs)); - iter.advance(); - } - - Ok(()) -} - -fn list_btree_formats(fs: &Fs, opt: Cli) -> anyhow::Result<()> { - let trans = BtreeTrans::new(fs); - let mut iter = BtreeNodeIter::new(&trans, opt.btree, opt.start, - 0, opt.level, - BtreeIterFlags::PREFETCH); - - while let Some(b) = iter.peek_and_restart()? { - if b.key.k.p > opt.end { - break; - } - - println!("{}", b.to_text(fs)); - iter.advance(); - } - - Ok(()) -} - -fn list_btree_nodes(fs: &Fs, opt: Cli) -> anyhow::Result<()> { - let trans = BtreeTrans::new(fs); - let mut iter = BtreeNodeIter::new(&trans, opt.btree, opt.start, - 0, opt.level, - BtreeIterFlags::PREFETCH); - - while let Some(b) = iter.peek_and_restart()? { - if b.key.k.p > opt.end { - break; - } - - println!("{}", BkeySC::from(&b.key).to_text(fs)); - iter.advance(); - } - - Ok(()) -} - -fn list_nodes_ondisk(fs: &Fs, opt: Cli) -> anyhow::Result<()> { - let trans = BtreeTrans::new(fs); - let mut iter = BtreeNodeIter::new(&trans, opt.btree, opt.start, - 0, opt.level, - BtreeIterFlags::PREFETCH); - - while let Some(b) = iter.peek_and_restart()? { - if b.key.k.p > opt.end { - break; - } - - println!("{}", b.ondisk_to_text(fs)); - iter.advance(); - } - - Ok(()) -} - -#[derive(Clone, clap::ValueEnum, Debug)] -enum Mode { - Keys, - Formats, - Nodes, - NodesOndisk, -} - -/// List filesystem metadata in textual form -#[derive(Parser, Debug)] -pub struct Cli { - /// Btree to list from - #[arg(short, long, default_value_t=bcachefs::btree_id::BTREE_ID_extents)] - btree: bcachefs::btree_id, - - /// Btree depth to descend to (0 == leaves) - #[arg(short, long, default_value_t=0)] - level: u32, - - /// Start position to list from - #[arg(short, long, default_value="POS_MIN")] - start: bcachefs::bpos, - - /// End position - #[arg(short, long, default_value="SPOS_MAX")] - end: bcachefs::bpos, - - #[arg(short, long, default_value="keys")] - mode: Mode, - - /// Check (fsck) the filesystem first - #[arg(short, long)] - fsck: bool, - - /// Force color on/off. Default: autodetect tty - #[arg(short, long, action = clap::ArgAction::Set, default_value_t=stdout().is_terminal())] - colorize: bool, - - /// Verbose mode - #[arg(short, long)] - verbose: bool, - - #[arg(required(true))] - devices: Vec, -} - -fn cmd_list_inner(opt: Cli) -> anyhow::Result<()> { - let mut fs_opts: bcachefs::bch_opts = Default::default(); - - opt_set!(fs_opts, nochanges, 1); - opt_set!(fs_opts, read_only, 1); - opt_set!(fs_opts, norecovery, 1); - opt_set!(fs_opts, degraded, 1); - opt_set!(fs_opts, very_degraded, 1); - opt_set!(fs_opts, errors, bcachefs::bch_error_actions::BCH_ON_ERROR_continue as u8); - - if opt.fsck { - opt_set!(fs_opts, fix_errors, bcachefs::fsck_err_opts::FSCK_FIX_yes as u8); - opt_set!(fs_opts, norecovery, 0); - } - - if opt.verbose { - opt_set!(fs_opts, verbose, 1); - } - - let fs = Fs::open(&opt.devices, fs_opts)?; - - match opt.mode { - Mode::Keys => list_keys(&fs, opt), - Mode::Formats => list_btree_formats(&fs, opt), - Mode::Nodes => list_btree_nodes(&fs, opt), - Mode::NodesOndisk => list_nodes_ondisk(&fs, opt), - } -} - -pub fn cmd_list(argv: Vec) -> i32 { - let opt = Cli::parse_from(argv); - colored::control::set_override(opt.colorize); - if let Err(e) = cmd_list_inner(opt) { - error!("Fatal error: {}", e); - 1 - } else { - 0 - } -} diff --git a/src/commands/cmd_mount.rs b/src/commands/cmd_mount.rs deleted file mode 100644 index 8d0e2e90..00000000 --- a/src/commands/cmd_mount.rs +++ /dev/null @@ -1,316 +0,0 @@ -use bch_bindgen::{path_to_cstr, bcachefs, bcachefs::bch_sb_handle, opt_set}; -use log::{info, debug, error, LevelFilter}; -use clap::Parser; -use uuid::Uuid; -use std::io::{stdout, IsTerminal}; -use std::path::PathBuf; -use std::fs; -use crate::key; -use crate::key::UnlockPolicy; -use std::ffi::{CString, c_char, c_void}; - -fn mount_inner( - src: String, - target: impl AsRef, - fstype: &str, - mountflags: libc::c_ulong, - data: Option, -) -> anyhow::Result<()> { - - // bind the CStrings to keep them alive - let src = CString::new(src)?; - let target = path_to_cstr(target); - let data = data.map(CString::new).transpose()?; - let fstype = CString::new(fstype)?; - - // convert to pointers for ffi - let src = src.as_c_str().to_bytes_with_nul().as_ptr() as *const c_char; - let target = target.as_c_str().to_bytes_with_nul().as_ptr() as *const c_char; - let data = data.as_ref().map_or(std::ptr::null(), |data| { - data.as_c_str().to_bytes_with_nul().as_ptr() as *const c_void - }); - let fstype = fstype.as_c_str().to_bytes_with_nul().as_ptr() as *const c_char; - - let ret = { - info!("mounting filesystem"); - // REQUIRES: CAP_SYS_ADMIN - unsafe { libc::mount(src, target, fstype, mountflags, data) } - }; - match ret { - 0 => Ok(()), - _ => Err(crate::ErrnoError(errno::errno()).into()), - } -} - -/// Parse a comma-separated mount options and split out mountflags and filesystem -/// specific options. -fn parse_mount_options(options: impl AsRef) -> (Option, libc::c_ulong) { - use either::Either::*; - debug!("parsing mount options: {}", options.as_ref()); - let (opts, flags) = options - .as_ref() - .split(",") - .map(|o| match o { - "dirsync" => Left(libc::MS_DIRSYNC), - "lazytime" => Left(1 << 25), // MS_LAZYTIME - "mand" => Left(libc::MS_MANDLOCK), - "noatime" => Left(libc::MS_NOATIME), - "nodev" => Left(libc::MS_NODEV), - "nodiratime" => Left(libc::MS_NODIRATIME), - "noexec" => Left(libc::MS_NOEXEC), - "nosuid" => Left(libc::MS_NOSUID), - "relatime" => Left(libc::MS_RELATIME), - "remount" => Left(libc::MS_REMOUNT), - "ro" => Left(libc::MS_RDONLY), - "rw" => Left(0), - "strictatime" => Left(libc::MS_STRICTATIME), - "sync" => Left(libc::MS_SYNCHRONOUS), - "" => Left(0), - o @ _ => Right(o), - }) - .fold((Vec::new(), 0), |(mut opts, flags), next| match next { - Left(f) => (opts, flags | f), - Right(o) => { - opts.push(o); - (opts, flags) - } - }); - - ( - if opts.len() == 0 { - None - } else { - Some(opts.join(",")) - }, - flags, - ) -} - -fn mount( - device: String, - target: impl AsRef, - options: impl AsRef, -) -> anyhow::Result<()> { - let (data, mountflags) = parse_mount_options(options); - - info!( - "mounting bcachefs filesystem, {}", - target.as_ref().display() - ); - mount_inner(device, target, "bcachefs", mountflags, data) -} - -fn read_super_silent(path: &std::path::PathBuf) -> anyhow::Result { - let mut opts = bcachefs::bch_opts::default(); - opt_set!(opts, noexcl, 1); - - bch_bindgen::sb_io::read_super_silent(&path, opts) -} - -fn get_devices_by_uuid(uuid: Uuid) -> anyhow::Result> { - debug!("enumerating udev devices"); - let mut udev = udev::Enumerator::new()?; - - udev.match_subsystem("block")?; - - let devs = udev - .scan_devices()? - .into_iter() - .filter_map(|dev| dev.devnode().map(ToOwned::to_owned)) - .map(|dev| (dev.clone(), read_super_silent(&dev))) - .filter_map(|(dev, sb)| sb.ok().map(|sb| (dev, sb))) - .filter(|(_, sb)| sb.sb().uuid() == uuid) - .collect(); - Ok(devs) -} - -fn get_uuid_for_dev_node(device: &std::path::PathBuf) -> anyhow::Result> { - let mut udev = udev::Enumerator::new()?; - let canonical = fs::canonicalize(device)?; - - udev.match_subsystem("block")?; - - for dev in udev.scan_devices()?.into_iter() { - if let Some(devnode) = dev.devnode() { - if devnode == canonical { - let devnode_owned = devnode.to_owned(); - let sb_result = read_super_silent(&devnode_owned); - if let Ok(sb) = sb_result { - return Ok(Some(sb.sb().uuid())); - } - } - } - } - Ok(None) -} - -/// Mount a bcachefs filesystem by its UUID. -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -pub struct Cli { - /// Path to passphrase/key file - /// - /// Precedes key_location/unlock_policy: if the filesystem can be decrypted - /// by the specified passphrase file; it is decrypted. (i.e. Regardless - /// if "fail" is specified for key_location/unlock_policy.) - #[arg(short = 'f', long)] - passphrase_file: Option, - - /// Password policy to use in case of encrypted filesystem. - /// - /// Possible values are: - /// "fail" - don't ask for password, fail if filesystem is encrypted; - /// "wait" - wait for password to become available before mounting; - /// "ask" - prompt the user for password; - #[arg(short = 'k', long = "key_location", default_value = "ask", verbatim_doc_comment)] - unlock_policy: UnlockPolicy, - - /// Device, or UUID=\ - dev: String, - - /// Where the filesystem should be mounted. If not set, then the filesystem - /// won't actually be mounted. But all steps preceeding mounting the - /// filesystem (e.g. asking for passphrase) will still be performed. - mountpoint: Option, - - /// Mount options - #[arg(short, default_value = "")] - options: String, - - /// Force color on/off. Autodetect tty is used to define default: - #[arg(short, long, action = clap::ArgAction::Set, default_value_t=stdout().is_terminal())] - colorize: bool, - - /// Verbose mode - #[arg(short, long, action = clap::ArgAction::Count)] - verbose: u8, -} - -fn devs_str_sbs_from_uuid(uuid: String) -> anyhow::Result<(String, Vec)> { - debug!("enumerating devices with UUID {}", uuid); - - let devs_sbs = Uuid::parse_str(&uuid) - .map(|uuid| get_devices_by_uuid(uuid))??; - - let devs_str = devs_sbs - .iter() - .map(|(dev, _)| dev.to_str().unwrap()) - .collect::>() - .join(":"); - - let sbs: Vec = devs_sbs.iter().map(|(_, sb)| *sb).collect(); - - Ok((devs_str, sbs)) - -} - -fn devs_str_sbs_from_device(device: &std::path::PathBuf) -> anyhow::Result<(String, Vec)> { - let uuid = get_uuid_for_dev_node(device)?; - - if let Some(bcache_fs_uuid) = uuid { - devs_str_sbs_from_uuid(bcache_fs_uuid.to_string()) - } else { - Ok((String::new(), Vec::new())) - } -} - -fn cmd_mount_inner(opt: Cli) -> anyhow::Result<()> { - let (devices, block_devices_to_mount) = if opt.dev.starts_with("UUID=") { - let uuid = opt.dev.replacen("UUID=", "", 1); - devs_str_sbs_from_uuid(uuid)? - } else if opt.dev.starts_with("OLD_BLKID_UUID=") { - let uuid = opt.dev.replacen("OLD_BLKID_UUID=", "", 1); - devs_str_sbs_from_uuid(uuid)? - } else { - // If the device string contains ":" we will assume the user knows the entire list. - // If they supply a single device it could be either the FS only has 1 device or it's - // only 1 of a number of devices which are part of the FS. This appears to be the case - // when we get called during fstab mount processing and the fstab specifies a UUID. - if opt.dev.contains(":") { - let mut block_devices_to_mount = Vec::new(); - - for dev in opt.dev.split(':') { - let dev = PathBuf::from(dev); - block_devices_to_mount.push(read_super_silent(&dev)?); - } - - (opt.dev, block_devices_to_mount) - } else { - devs_str_sbs_from_device(&PathBuf::from(opt.dev))? - } - }; - - if block_devices_to_mount.len() == 0 { - Err(anyhow::anyhow!("No device found from specified parameters"))?; - } - // Check if the filesystem's master key is encrypted - if unsafe { bcachefs::bch2_sb_is_encrypted_and_locked(block_devices_to_mount[0].sb) } { - // First by password_file, if available - let fallback_to_unlock_policy = if let Some(passphrase_file) = &opt.passphrase_file { - match key::read_from_passphrase_file(&block_devices_to_mount[0], passphrase_file.as_path()) { - Ok(()) => { - // Decryption succeeded - false - } - Err(err) => { - // Decryption failed, fall back to unlock_policy - error!("Failed to decrypt using passphrase_file: {}", err); - true - } - } - } else { - // No passphrase_file specified, fall back to unlock_policy - true - }; - // If decryption by key_file was unsuccesful, prompt for passphrase (or follow key_policy) - if fallback_to_unlock_policy { - key::apply_key_unlocking_policy(&block_devices_to_mount[0], opt.unlock_policy)?; - }; - } - - if let Some(mountpoint) = opt.mountpoint { - info!( - "mounting with params: device: {}, target: {}, options: {}", - devices, - mountpoint.to_string_lossy(), - &opt.options - ); - - mount(devices, mountpoint, &opt.options)?; - } else { - info!( - "would mount with params: device: {}, options: {}", - devices, - &opt.options - ); - } - - Ok(()) -} - -pub fn cmd_mount(mut argv: Vec, symlink_cmd: Option<&str>) -> i32 { - // If the bcachefs tool is being called as "bcachefs mount dev ..." (as opposed to via a - // symlink like "/usr/sbin/mount.bcachefs dev ...", then we need to pop the 0th argument - // ("bcachefs") since the CLI parser here expects the device at position 1. - if symlink_cmd.is_none() { - argv.remove(0); - } - - let opt = Cli::parse_from(argv); - - // @TODO : more granular log levels via mount option - log::set_max_level(match opt.verbose { - 0 => LevelFilter::Warn, - 1 => LevelFilter::Trace, - 2_u8..=u8::MAX => todo!(), - }); - - colored::control::set_override(opt.colorize); - if let Err(e) = cmd_mount_inner(opt) { - error!("Fatal error: {}", e); - 1 - } else { - info!("Successfully mounted"); - 0 - } -} diff --git a/src/commands/cmd_subvolume.rs b/src/commands/cmd_subvolume.rs deleted file mode 100644 index c77eaacd..00000000 --- a/src/commands/cmd_subvolume.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::path::PathBuf; - -use bch_bindgen::c::BCH_SUBVOL_SNAPSHOT_RO; -use clap::{Parser, Subcommand}; - -use crate::wrappers::handle::BcachefsHandle; - -#[derive(Parser, Debug)] -pub struct Cli { - #[command(subcommand)] - subcommands: Subcommands, -} - -/// Subvolumes-related commands -#[derive(Subcommand, Debug)] -enum Subcommands { - #[command(visible_aliases = ["new"])] - Create { - /// Paths - targets: Vec - }, - - #[command(visible_aliases = ["del"])] - Delete { - /// Path - target: PathBuf - }, - - #[command(allow_missing_positional = true, visible_aliases = ["snap"])] - Snapshot { - /// Make snapshot read only - #[arg(long, short)] - read_only: bool, - source: Option, - dest: PathBuf - } -} - -pub fn cmd_subvolumes(argv: Vec) -> i32 { - let args = Cli::parse_from(argv); - - match args.subcommands { - Subcommands::Create { targets } => { - for target in targets { - if let Some(dirname) = target.parent() { - let fs = unsafe { BcachefsHandle::open(dirname) }; - fs.create_subvolume(target).expect("Failed to create the subvolume"); - } - } - } - , - Subcommands::Delete { target } => { - if let Some(dirname) = target.parent() { - let fs = unsafe { BcachefsHandle::open(dirname) }; - fs.delete_subvolume(target).expect("Failed to delete the subvolume"); - } - }, - Subcommands::Snapshot { read_only, source, dest } => { - if let Some(dirname) = dest.parent() { - let dot = PathBuf::from("."); - let dir = if dirname.as_os_str().is_empty() { &dot } else { dirname }; - let fs = unsafe { BcachefsHandle::open(dir) }; - - fs.snapshot_subvolume(if read_only { BCH_SUBVOL_SNAPSHOT_RO } else { 0x0 }, source, dest).expect("Failed to snapshot the subvolume"); - } - } - } - - 0 -} diff --git a/src/commands/completions.rs b/src/commands/completions.rs new file mode 100644 index 00000000..d4e98569 --- /dev/null +++ b/src/commands/completions.rs @@ -0,0 +1,19 @@ +use clap::{Command, CommandFactory, Parser}; +use clap_complete::{generate, Generator, Shell}; +use std::io; + +/// Generate shell completions +#[derive(Parser, Debug)] +pub struct Cli { + shell: Shell, +} + +fn print_completions(gen: G, cmd: &mut Command) { + generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout()); +} + +pub fn completions(argv: Vec) -> i32 { + let cli = Cli::parse_from(argv); + print_completions(cli.shell, &mut super::Cli::command()); + 0 +} diff --git a/src/commands/list.rs b/src/commands/list.rs new file mode 100644 index 00000000..0ed6be05 --- /dev/null +++ b/src/commands/list.rs @@ -0,0 +1,169 @@ +use log::{error}; +use bch_bindgen::bcachefs; +use bch_bindgen::opt_set; +use bch_bindgen::fs::Fs; +use bch_bindgen::bkey::BkeySC; +use bch_bindgen::btree::BtreeTrans; +use bch_bindgen::btree::BtreeIter; +use bch_bindgen::btree::BtreeNodeIter; +use bch_bindgen::btree::BtreeIterFlags; +use clap::{Parser}; +use std::io::{stdout, IsTerminal}; + +fn list_keys(fs: &Fs, opt: Cli) -> anyhow::Result<()> { + let trans = BtreeTrans::new(fs); + let mut iter = BtreeIter::new(&trans, opt.btree, opt.start, + BtreeIterFlags::ALL_SNAPSHOTS| + BtreeIterFlags::PREFETCH); + + while let Some(k) = iter.peek_and_restart()? { + if k.k.p > opt.end { + break; + } + + println!("{}", k.to_text(fs)); + iter.advance(); + } + + Ok(()) +} + +fn list_btree_formats(fs: &Fs, opt: Cli) -> anyhow::Result<()> { + let trans = BtreeTrans::new(fs); + let mut iter = BtreeNodeIter::new(&trans, opt.btree, opt.start, + 0, opt.level, + BtreeIterFlags::PREFETCH); + + while let Some(b) = iter.peek_and_restart()? { + if b.key.k.p > opt.end { + break; + } + + println!("{}", b.to_text(fs)); + iter.advance(); + } + + Ok(()) +} + +fn list_btree_nodes(fs: &Fs, opt: Cli) -> anyhow::Result<()> { + let trans = BtreeTrans::new(fs); + let mut iter = BtreeNodeIter::new(&trans, opt.btree, opt.start, + 0, opt.level, + BtreeIterFlags::PREFETCH); + + while let Some(b) = iter.peek_and_restart()? { + if b.key.k.p > opt.end { + break; + } + + println!("{}", BkeySC::from(&b.key).to_text(fs)); + iter.advance(); + } + + Ok(()) +} + +fn list_nodes_ondisk(fs: &Fs, opt: Cli) -> anyhow::Result<()> { + let trans = BtreeTrans::new(fs); + let mut iter = BtreeNodeIter::new(&trans, opt.btree, opt.start, + 0, opt.level, + BtreeIterFlags::PREFETCH); + + while let Some(b) = iter.peek_and_restart()? { + if b.key.k.p > opt.end { + break; + } + + println!("{}", b.ondisk_to_text(fs)); + iter.advance(); + } + + Ok(()) +} + +#[derive(Clone, clap::ValueEnum, Debug)] +enum Mode { + Keys, + Formats, + Nodes, + NodesOndisk, +} + +/// List filesystem metadata in textual form +#[derive(Parser, Debug)] +pub struct Cli { + /// Btree to list from + #[arg(short, long, default_value_t=bcachefs::btree_id::BTREE_ID_extents)] + btree: bcachefs::btree_id, + + /// Btree depth to descend to (0 == leaves) + #[arg(short, long, default_value_t=0)] + level: u32, + + /// Start position to list from + #[arg(short, long, default_value="POS_MIN")] + start: bcachefs::bpos, + + /// End position + #[arg(short, long, default_value="SPOS_MAX")] + end: bcachefs::bpos, + + #[arg(short, long, default_value="keys")] + mode: Mode, + + /// Check (fsck) the filesystem first + #[arg(short, long)] + fsck: bool, + + /// Force color on/off. Default: autodetect tty + #[arg(short, long, action = clap::ArgAction::Set, default_value_t=stdout().is_terminal())] + colorize: bool, + + /// Verbose mode + #[arg(short, long)] + verbose: bool, + + #[arg(required(true))] + devices: Vec, +} + +fn cmd_list_inner(opt: Cli) -> anyhow::Result<()> { + let mut fs_opts: bcachefs::bch_opts = Default::default(); + + opt_set!(fs_opts, nochanges, 1); + opt_set!(fs_opts, read_only, 1); + opt_set!(fs_opts, norecovery, 1); + opt_set!(fs_opts, degraded, 1); + opt_set!(fs_opts, very_degraded, 1); + opt_set!(fs_opts, errors, bcachefs::bch_error_actions::BCH_ON_ERROR_continue as u8); + + if opt.fsck { + opt_set!(fs_opts, fix_errors, bcachefs::fsck_err_opts::FSCK_FIX_yes as u8); + opt_set!(fs_opts, norecovery, 0); + } + + if opt.verbose { + opt_set!(fs_opts, verbose, 1); + } + + let fs = Fs::open(&opt.devices, fs_opts)?; + + match opt.mode { + Mode::Keys => list_keys(&fs, opt), + Mode::Formats => list_btree_formats(&fs, opt), + Mode::Nodes => list_btree_nodes(&fs, opt), + Mode::NodesOndisk => list_nodes_ondisk(&fs, opt), + } +} + +pub fn list(argv: Vec) -> i32 { + let opt = Cli::parse_from(argv); + colored::control::set_override(opt.colorize); + if let Err(e) = cmd_list_inner(opt) { + error!("Fatal error: {}", e); + 1 + } else { + 0 + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 70fef82c..c7645926 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,10 +1,15 @@ use clap::Subcommand; pub mod logger; -pub mod cmd_mount; -pub mod cmd_list; -pub mod cmd_completions; -pub mod cmd_subvolume; +pub mod mount; +pub mod list; +pub mod completions; +pub mod subvolume; + +pub use mount::mount; +pub use list::list; +pub use completions::completions; +pub use subvolume::subvolume; #[derive(clap::Parser, Debug)] #[command(name = "bcachefs")] @@ -15,11 +20,11 @@ pub struct Cli { #[derive(Subcommand, Debug)] enum Subcommands { - List(cmd_list::Cli), - Mount(cmd_mount::Cli), - Completions(cmd_completions::Cli), + List(list::Cli), + Mount(mount::Cli), + Completions(completions::Cli), #[command(visible_aliases = ["subvol"])] - Subvolume(cmd_subvolume::Cli), + Subvolume(subvolume::Cli), } #[macro_export] diff --git a/src/commands/mount.rs b/src/commands/mount.rs new file mode 100644 index 00000000..3cca0a4c --- /dev/null +++ b/src/commands/mount.rs @@ -0,0 +1,316 @@ +use bch_bindgen::{path_to_cstr, bcachefs, bcachefs::bch_sb_handle, opt_set}; +use log::{info, debug, error, LevelFilter}; +use clap::Parser; +use uuid::Uuid; +use std::io::{stdout, IsTerminal}; +use std::path::PathBuf; +use std::fs; +use crate::key; +use crate::key::UnlockPolicy; +use std::ffi::{CString, c_char, c_void}; + +fn mount_inner( + src: String, + target: impl AsRef, + fstype: &str, + mountflags: libc::c_ulong, + data: Option, +) -> anyhow::Result<()> { + + // bind the CStrings to keep them alive + let src = CString::new(src)?; + let target = path_to_cstr(target); + let data = data.map(CString::new).transpose()?; + let fstype = CString::new(fstype)?; + + // convert to pointers for ffi + let src = src.as_c_str().to_bytes_with_nul().as_ptr() as *const c_char; + let target = target.as_c_str().to_bytes_with_nul().as_ptr() as *const c_char; + let data = data.as_ref().map_or(std::ptr::null(), |data| { + data.as_c_str().to_bytes_with_nul().as_ptr() as *const c_void + }); + let fstype = fstype.as_c_str().to_bytes_with_nul().as_ptr() as *const c_char; + + let ret = { + info!("mounting filesystem"); + // REQUIRES: CAP_SYS_ADMIN + unsafe { libc::mount(src, target, fstype, mountflags, data) } + }; + match ret { + 0 => Ok(()), + _ => Err(crate::ErrnoError(errno::errno()).into()), + } +} + +/// Parse a comma-separated mount options and split out mountflags and filesystem +/// specific options. +fn parse_mount_options(options: impl AsRef) -> (Option, libc::c_ulong) { + use either::Either::*; + debug!("parsing mount options: {}", options.as_ref()); + let (opts, flags) = options + .as_ref() + .split(",") + .map(|o| match o { + "dirsync" => Left(libc::MS_DIRSYNC), + "lazytime" => Left(1 << 25), // MS_LAZYTIME + "mand" => Left(libc::MS_MANDLOCK), + "noatime" => Left(libc::MS_NOATIME), + "nodev" => Left(libc::MS_NODEV), + "nodiratime" => Left(libc::MS_NODIRATIME), + "noexec" => Left(libc::MS_NOEXEC), + "nosuid" => Left(libc::MS_NOSUID), + "relatime" => Left(libc::MS_RELATIME), + "remount" => Left(libc::MS_REMOUNT), + "ro" => Left(libc::MS_RDONLY), + "rw" => Left(0), + "strictatime" => Left(libc::MS_STRICTATIME), + "sync" => Left(libc::MS_SYNCHRONOUS), + "" => Left(0), + o @ _ => Right(o), + }) + .fold((Vec::new(), 0), |(mut opts, flags), next| match next { + Left(f) => (opts, flags | f), + Right(o) => { + opts.push(o); + (opts, flags) + } + }); + + ( + if opts.len() == 0 { + None + } else { + Some(opts.join(",")) + }, + flags, + ) +} + +fn do_mount( + device: String, + target: impl AsRef, + options: impl AsRef, +) -> anyhow::Result<()> { + let (data, mountflags) = parse_mount_options(options); + + info!( + "mounting bcachefs filesystem, {}", + target.as_ref().display() + ); + mount_inner(device, target, "bcachefs", mountflags, data) +} + +fn read_super_silent(path: &std::path::PathBuf) -> anyhow::Result { + let mut opts = bcachefs::bch_opts::default(); + opt_set!(opts, noexcl, 1); + + bch_bindgen::sb_io::read_super_silent(&path, opts) +} + +fn get_devices_by_uuid(uuid: Uuid) -> anyhow::Result> { + debug!("enumerating udev devices"); + let mut udev = udev::Enumerator::new()?; + + udev.match_subsystem("block")?; + + let devs = udev + .scan_devices()? + .into_iter() + .filter_map(|dev| dev.devnode().map(ToOwned::to_owned)) + .map(|dev| (dev.clone(), read_super_silent(&dev))) + .filter_map(|(dev, sb)| sb.ok().map(|sb| (dev, sb))) + .filter(|(_, sb)| sb.sb().uuid() == uuid) + .collect(); + Ok(devs) +} + +fn get_uuid_for_dev_node(device: &std::path::PathBuf) -> anyhow::Result> { + let mut udev = udev::Enumerator::new()?; + let canonical = fs::canonicalize(device)?; + + udev.match_subsystem("block")?; + + for dev in udev.scan_devices()?.into_iter() { + if let Some(devnode) = dev.devnode() { + if devnode == canonical { + let devnode_owned = devnode.to_owned(); + let sb_result = read_super_silent(&devnode_owned); + if let Ok(sb) = sb_result { + return Ok(Some(sb.sb().uuid())); + } + } + } + } + Ok(None) +} + +/// Mount a bcachefs filesystem by its UUID. +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +pub struct Cli { + /// Path to passphrase/key file + /// + /// Precedes key_location/unlock_policy: if the filesystem can be decrypted + /// by the specified passphrase file; it is decrypted. (i.e. Regardless + /// if "fail" is specified for key_location/unlock_policy.) + #[arg(short = 'f', long)] + passphrase_file: Option, + + /// Password policy to use in case of encrypted filesystem. + /// + /// Possible values are: + /// "fail" - don't ask for password, fail if filesystem is encrypted; + /// "wait" - wait for password to become available before mounting; + /// "ask" - prompt the user for password; + #[arg(short = 'k', long = "key_location", default_value = "ask", verbatim_doc_comment)] + unlock_policy: UnlockPolicy, + + /// Device, or UUID=\ + dev: String, + + /// Where the filesystem should be mounted. If not set, then the filesystem + /// won't actually be mounted. But all steps preceeding mounting the + /// filesystem (e.g. asking for passphrase) will still be performed. + mountpoint: Option, + + /// Mount options + #[arg(short, default_value = "")] + options: String, + + /// Force color on/off. Autodetect tty is used to define default: + #[arg(short, long, action = clap::ArgAction::Set, default_value_t=stdout().is_terminal())] + colorize: bool, + + /// Verbose mode + #[arg(short, long, action = clap::ArgAction::Count)] + verbose: u8, +} + +fn devs_str_sbs_from_uuid(uuid: String) -> anyhow::Result<(String, Vec)> { + debug!("enumerating devices with UUID {}", uuid); + + let devs_sbs = Uuid::parse_str(&uuid) + .map(|uuid| get_devices_by_uuid(uuid))??; + + let devs_str = devs_sbs + .iter() + .map(|(dev, _)| dev.to_str().unwrap()) + .collect::>() + .join(":"); + + let sbs: Vec = devs_sbs.iter().map(|(_, sb)| *sb).collect(); + + Ok((devs_str, sbs)) + +} + +fn devs_str_sbs_from_device(device: &std::path::PathBuf) -> anyhow::Result<(String, Vec)> { + let uuid = get_uuid_for_dev_node(device)?; + + if let Some(bcache_fs_uuid) = uuid { + devs_str_sbs_from_uuid(bcache_fs_uuid.to_string()) + } else { + Ok((String::new(), Vec::new())) + } +} + +fn cmd_mount_inner(opt: Cli) -> anyhow::Result<()> { + let (devices, block_devices_to_mount) = if opt.dev.starts_with("UUID=") { + let uuid = opt.dev.replacen("UUID=", "", 1); + devs_str_sbs_from_uuid(uuid)? + } else if opt.dev.starts_with("OLD_BLKID_UUID=") { + let uuid = opt.dev.replacen("OLD_BLKID_UUID=", "", 1); + devs_str_sbs_from_uuid(uuid)? + } else { + // If the device string contains ":" we will assume the user knows the entire list. + // If they supply a single device it could be either the FS only has 1 device or it's + // only 1 of a number of devices which are part of the FS. This appears to be the case + // when we get called during fstab mount processing and the fstab specifies a UUID. + if opt.dev.contains(":") { + let mut block_devices_to_mount = Vec::new(); + + for dev in opt.dev.split(':') { + let dev = PathBuf::from(dev); + block_devices_to_mount.push(read_super_silent(&dev)?); + } + + (opt.dev, block_devices_to_mount) + } else { + devs_str_sbs_from_device(&PathBuf::from(opt.dev))? + } + }; + + if block_devices_to_mount.len() == 0 { + Err(anyhow::anyhow!("No device found from specified parameters"))?; + } + // Check if the filesystem's master key is encrypted + if unsafe { bcachefs::bch2_sb_is_encrypted_and_locked(block_devices_to_mount[0].sb) } { + // First by password_file, if available + let fallback_to_unlock_policy = if let Some(passphrase_file) = &opt.passphrase_file { + match key::read_from_passphrase_file(&block_devices_to_mount[0], passphrase_file.as_path()) { + Ok(()) => { + // Decryption succeeded + false + } + Err(err) => { + // Decryption failed, fall back to unlock_policy + error!("Failed to decrypt using passphrase_file: {}", err); + true + } + } + } else { + // No passphrase_file specified, fall back to unlock_policy + true + }; + // If decryption by key_file was unsuccesful, prompt for passphrase (or follow key_policy) + if fallback_to_unlock_policy { + key::apply_key_unlocking_policy(&block_devices_to_mount[0], opt.unlock_policy)?; + }; + } + + if let Some(mountpoint) = opt.mountpoint { + info!( + "mounting with params: device: {}, target: {}, options: {}", + devices, + mountpoint.to_string_lossy(), + &opt.options + ); + + do_mount(devices, mountpoint, &opt.options)?; + } else { + info!( + "would mount with params: device: {}, options: {}", + devices, + &opt.options + ); + } + + Ok(()) +} + +pub fn mount(mut argv: Vec, symlink_cmd: Option<&str>) -> i32 { + // If the bcachefs tool is being called as "bcachefs mount dev ..." (as opposed to via a + // symlink like "/usr/sbin/mount.bcachefs dev ...", then we need to pop the 0th argument + // ("bcachefs") since the CLI parser here expects the device at position 1. + if symlink_cmd.is_none() { + argv.remove(0); + } + + let opt = Cli::parse_from(argv); + + // @TODO : more granular log levels via mount option + log::set_max_level(match opt.verbose { + 0 => LevelFilter::Warn, + 1 => LevelFilter::Trace, + 2_u8..=u8::MAX => todo!(), + }); + + colored::control::set_override(opt.colorize); + if let Err(e) = cmd_mount_inner(opt) { + error!("Fatal error: {}", e); + 1 + } else { + info!("Successfully mounted"); + 0 + } +} diff --git a/src/commands/subvolume.rs b/src/commands/subvolume.rs new file mode 100644 index 00000000..5f7cdc76 --- /dev/null +++ b/src/commands/subvolume.rs @@ -0,0 +1,70 @@ +use std::path::PathBuf; + +use bch_bindgen::c::BCH_SUBVOL_SNAPSHOT_RO; +use clap::{Parser, Subcommand}; + +use crate::wrappers::handle::BcachefsHandle; + +#[derive(Parser, Debug)] +pub struct Cli { + #[command(subcommand)] + subcommands: Subcommands, +} + +/// Subvolumes-related commands +#[derive(Subcommand, Debug)] +enum Subcommands { + #[command(visible_aliases = ["new"])] + Create { + /// Paths + targets: Vec + }, + + #[command(visible_aliases = ["del"])] + Delete { + /// Path + target: PathBuf + }, + + #[command(allow_missing_positional = true, visible_aliases = ["snap"])] + Snapshot { + /// Make snapshot read only + #[arg(long, short)] + read_only: bool, + source: Option, + dest: PathBuf + } +} + +pub fn subvolume(argv: Vec) -> i32 { + let args = Cli::parse_from(argv); + + match args.subcommands { + Subcommands::Create { targets } => { + for target in targets { + if let Some(dirname) = target.parent() { + let fs = unsafe { BcachefsHandle::open(dirname) }; + fs.create_subvolume(target).expect("Failed to create the subvolume"); + } + } + } + , + Subcommands::Delete { target } => { + if let Some(dirname) = target.parent() { + let fs = unsafe { BcachefsHandle::open(dirname) }; + fs.delete_subvolume(target).expect("Failed to delete the subvolume"); + } + }, + Subcommands::Snapshot { read_only, source, dest } => { + if let Some(dirname) = dest.parent() { + let dot = PathBuf::from("."); + let dir = if dirname.as_os_str().is_empty() { &dot } else { dirname }; + let fs = unsafe { BcachefsHandle::open(dir) }; + + fs.snapshot_subvolume(if read_only { BCH_SUBVOL_SNAPSHOT_RO } else { 0x0 }, source, dest).expect("Failed to snapshot the subvolume"); + } + } + } + + 0 +} -- cgit v1.2.3