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