]> git.sesse.net Git - bcachefs-tools-debian/blob - rust-src/src/cmd_mount.rs
add command to generate Rust-part CLI completions
[bcachefs-tools-debian] / rust-src / src / cmd_mount.rs
1 use atty::Stream;
2 use bch_bindgen::{bcachefs, bcachefs::bch_sb_handle};
3 use log::{info, debug, error, LevelFilter};
4 use clap::{Parser, Subcommand};
5 use uuid::Uuid;
6 use std::path::PathBuf;
7 use crate::{key, transform_c_args};
8 use crate::key::KeyLoc;
9 use crate::logger::SimpleLogger;
10 use std::ffi::{CStr, CString, OsStr, c_int, c_char, c_void};
11 use std::os::unix::ffi::OsStrExt;
12
13 fn mount_inner(
14     src: String,
15     target: impl AsRef<std::path::Path>,
16     fstype: &str,
17     mountflags: libc::c_ulong,
18     data: Option<String>,
19 ) -> anyhow::Result<()> {
20
21     // bind the CStrings to keep them alive
22     let src = CString::new(src)?;
23     let target = CString::new(target.as_ref().as_os_str().as_bytes())?;
24     let data = data.map(CString::new).transpose()?;
25     let fstype = CString::new(fstype)?;
26
27     // convert to pointers for ffi
28     let src = src.as_c_str().to_bytes_with_nul().as_ptr() as *const c_char;
29     let target = target.as_c_str().to_bytes_with_nul().as_ptr() as *const c_char;
30     let data = data.as_ref().map_or(std::ptr::null(), |data| {
31         data.as_c_str().to_bytes_with_nul().as_ptr() as *const c_void
32     });
33     let fstype = fstype.as_c_str().to_bytes_with_nul().as_ptr() as *const c_char;
34
35     let ret = {
36         info!("mounting filesystem");
37         // REQUIRES: CAP_SYS_ADMIN
38         unsafe { libc::mount(src, target, fstype, mountflags, data) }
39     };
40     match ret {
41         0 => Ok(()),
42         _ => Err(crate::ErrnoError(errno::errno()).into()),
43     }
44 }
45
46 /// Parse a comma-separated mount options and split out mountflags and filesystem
47 /// specific options.
48 fn parse_mount_options(options: impl AsRef<str>) -> (Option<String>, libc::c_ulong) {
49     use either::Either::*;
50     debug!("parsing mount options: {}", options.as_ref());
51     let (opts, flags) = options
52         .as_ref()
53         .split(",")
54         .map(|o| match o {
55             "dirsync"       => Left(libc::MS_DIRSYNC),
56             "lazytime"      => Left(1 << 25), // MS_LAZYTIME
57             "mand"          => Left(libc::MS_MANDLOCK),
58             "noatime"       => Left(libc::MS_NOATIME),
59             "nodev"         => Left(libc::MS_NODEV),
60             "nodiratime"    => Left(libc::MS_NODIRATIME),
61             "noexec"        => Left(libc::MS_NOEXEC),
62             "nosuid"        => Left(libc::MS_NOSUID),
63             "relatime"      => Left(libc::MS_RELATIME),
64             "remount"       => Left(libc::MS_REMOUNT),
65             "ro"            => Left(libc::MS_RDONLY),
66             "rw"            => Left(0),
67             "strictatime"   => Left(libc::MS_STRICTATIME),
68             "sync"          => Left(libc::MS_SYNCHRONOUS),
69             ""              => Left(0),
70             o @ _           => Right(o),
71         })
72         .fold((Vec::new(), 0), |(mut opts, flags), next| match next {
73             Left(f) => (opts, flags | f),
74             Right(o) => {
75                 opts.push(o);
76                 (opts, flags)
77             }
78         });
79
80     use itertools::Itertools;
81     (
82         if opts.len() == 0 {
83             None
84         } else {
85             Some(opts.iter().join(","))
86         },
87         flags,
88     )
89 }
90
91 fn mount(
92     device: String,
93     target: impl AsRef<std::path::Path>,
94     options: impl AsRef<str>,
95 ) -> anyhow::Result<()> {
96     let (data, mountflags) = parse_mount_options(options);
97
98     info!(
99         "mounting bcachefs filesystem, {}",
100         target.as_ref().display()
101     );
102     mount_inner(device, target, "bcachefs", mountflags, data)
103 }
104
105 fn read_super_silent(path: &std::path::PathBuf) -> anyhow::Result<bch_sb_handle> {
106     // Stop libbcachefs from spamming the output
107     let _gag = gag::BufferRedirect::stdout().unwrap();
108
109     bch_bindgen::rs::read_super(&path)
110 }
111
112 fn get_devices_by_uuid(uuid: Uuid) -> anyhow::Result<Vec<(PathBuf, bch_sb_handle)>> {
113     debug!("enumerating udev devices");
114     let mut udev = udev::Enumerator::new()?;
115
116     udev.match_subsystem("block")?;
117
118     let devs = udev
119         .scan_devices()?
120         .into_iter()
121         .filter_map(|dev| dev.devnode().map(ToOwned::to_owned))
122         .map(|dev| (dev.clone(), read_super_silent(&dev)))
123         .filter_map(|(dev, sb)| sb.ok().map(|sb| (dev, sb)))
124         .filter(|(_, sb)| sb.sb().uuid() == uuid)
125         .collect();
126     Ok(devs)
127 }
128
129 /// Mount a bcachefs filesystem by its UUID.
130 #[derive(Parser, Debug)]
131 #[command(author, version, about, long_about = None)]
132 pub struct Cli {
133     /// Where the password would be loaded from.
134     ///
135     /// Possible values are:
136     /// "fail" - don't ask for password, fail if filesystem is encrypted;
137     /// "wait" - wait for password to become available before mounting;
138     /// "ask" -  prompt the user for password;
139     #[arg(short, long, default_value = "ask", verbatim_doc_comment)]
140     key_location:   KeyLoc,
141
142     /// Device, or UUID=<UUID>
143     dev:            String,
144
145     /// Where the filesystem should be mounted. If not set, then the filesystem
146     /// won't actually be mounted. But all steps preceeding mounting the
147     /// filesystem (e.g. asking for passphrase) will still be performed.
148     mountpoint:     Option<std::path::PathBuf>,
149
150     /// Mount options
151     #[arg(short, default_value = "")]
152     options:        String,
153
154     /// Force color on/off. Default: autodetect tty
155     #[arg(short, long, action = clap::ArgAction::Set, default_value_t=atty::is(Stream::Stdout))]
156     colorize:       bool,
157
158     /// Verbose mode
159     #[arg(short, long, action = clap::ArgAction::Count)]
160     verbose:        u8,
161 }
162
163 fn devs_str_sbs_from_uuid(uuid: String) -> anyhow::Result<(String, Vec<bch_sb_handle>)> {
164     debug!("enumerating devices with UUID {}", uuid);
165
166     let devs_sbs = Uuid::parse_str(&uuid)
167         .map(|uuid| get_devices_by_uuid(uuid))??;
168
169     let devs_str = devs_sbs
170         .iter()
171         .map(|(dev, _)| dev.to_str().unwrap())
172         .collect::<Vec<_>>()
173         .join(":");
174
175     let sbs: Vec<bch_sb_handle> = devs_sbs.iter().map(|(_, sb)| *sb).collect();
176
177     Ok((devs_str, sbs))
178
179 }
180
181 fn cmd_mount_inner(opt: Cli) -> anyhow::Result<()> {
182     let (devs, sbs) = if opt.dev.starts_with("UUID=") {
183         let uuid = opt.dev.replacen("UUID=", "", 1);
184         devs_str_sbs_from_uuid(uuid)?
185     } else if opt.dev.starts_with("OLD_BLKID_UUID=") {
186         let uuid = opt.dev.replacen("OLD_BLKID_UUID=", "", 1);
187         devs_str_sbs_from_uuid(uuid)?
188     } else {
189         let mut sbs = Vec::new();
190
191         for dev in opt.dev.split(':') {
192             let dev = PathBuf::from(dev);
193             sbs.push(bch_bindgen::rs::read_super(&dev)?);
194         }
195
196         (opt.dev, sbs)
197     };
198
199     if sbs.len() == 0 {
200         Err(anyhow::anyhow!("No device found from specified parameters"))?;
201     } else if unsafe { bcachefs::bch2_sb_is_encrypted(sbs[0].sb) } {
202         let key = opt
203             .key_location
204             .0
205             .ok_or_else(|| anyhow::anyhow!("no keyoption specified for locked filesystem"))?;
206
207         key::prepare_key(&sbs[0], key)?;
208     }
209
210     if let Some(mountpoint) = opt.mountpoint {
211         info!(
212             "mounting with params: device: {}, target: {}, options: {}",
213             devs,
214             mountpoint.to_string_lossy(),
215             &opt.options
216         );
217
218         mount(devs, mountpoint, &opt.options)?;
219     } else {
220         info!(
221             "would mount with params: device: {}, options: {}",
222             devs,
223             &opt.options
224         );
225     }
226
227     Ok(())
228 }
229
230 #[no_mangle]
231 #[allow(clippy::not_unsafe_ptr_arg_deref)]
232 pub extern "C" fn cmd_mount(argc: c_int, argv: *const *const c_char) -> c_int {
233     transform_c_args!(argv, argc, argv);
234     let opt = Cli::parse_from(argv);
235
236     log::set_boxed_logger(Box::new(SimpleLogger)).unwrap();
237
238     // @TODO : more granular log levels via mount option
239     log::set_max_level(match opt.verbose {
240         0 => LevelFilter::Warn,
241         1 => LevelFilter::Trace,
242         2_u8..=u8::MAX => todo!(),
243     });
244
245     colored::control::set_override(opt.colorize);
246     if let Err(e) = cmd_mount_inner(opt) {
247         error!("Fatal error: {}", e);
248         1
249     } else {
250         info!("Successfully mounted");
251         0
252     }
253 }