]> git.sesse.net Git - bcachefs-tools-debian/blob - rust-src/src/cmd_mount.rs
3f8253f5e1f56781a22fdf6ee7cf6b0ccacdd85b
[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};
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     use itertools::Itertools;
80     (
81         if opts.len() == 0 {
82             None
83         } else {
84             Some(opts.iter().join(","))
85         },
86         flags,
87     )
88 }
89
90 fn mount(
91     device: String,
92     target: impl AsRef<std::path::Path>,
93     options: impl AsRef<str>,
94 ) -> anyhow::Result<()> {
95     let (data, mountflags) = parse_mount_options(options);
96
97     info!(
98         "mounting bcachefs filesystem, {}",
99         target.as_ref().display()
100     );
101     mount_inner(device, target, "bcachefs", mountflags, data)
102 }
103
104 fn read_super_silent(path: &std::path::PathBuf) -> anyhow::Result<bch_sb_handle> {
105     // Stop libbcachefs from spamming the output
106     let _gag = gag::BufferRedirect::stdout().unwrap();
107
108     bch_bindgen::rs::read_super(&path)
109 }
110
111 fn get_devices_by_uuid(uuid: Uuid) -> anyhow::Result<Vec<(PathBuf, bch_sb_handle)>> {
112     debug!("enumerating udev devices");
113     let mut udev = udev::Enumerator::new()?;
114
115     udev.match_subsystem("block")?;
116
117     let devs = udev
118         .scan_devices()?
119         .into_iter()
120         .filter_map(|dev| dev.devnode().map(ToOwned::to_owned))
121         .map(|dev| (dev.clone(), read_super_silent(&dev)))
122         .filter_map(|(dev, sb)| sb.ok().map(|sb| (dev, sb)))
123         .filter(|(_, sb)| sb.sb().uuid() == uuid)
124         .collect();
125     Ok(devs)
126 }
127
128 /// Mount a bcachefs filesystem by its UUID.
129 #[derive(Parser, Debug)]
130 #[command(author, version, about, long_about = None)]
131 pub struct Cli {
132     /// Where the password would be loaded from.
133     ///
134     /// Possible values are:
135     /// "fail" - don't ask for password, fail if filesystem is encrypted;
136     /// "wait" - wait for password to become available before mounting;
137     /// "ask" -  prompt the user for password;
138     #[arg(short, long, default_value = "ask", verbatim_doc_comment)]
139     key_location:   KeyLocation,
140
141     /// Device, or UUID=<UUID>
142     dev:            String,
143
144     /// Where the filesystem should be mounted. If not set, then the filesystem
145     /// won't actually be mounted. But all steps preceeding mounting the
146     /// filesystem (e.g. asking for passphrase) will still be performed.
147     mountpoint:     Option<std::path::PathBuf>,
148
149     /// Mount options
150     #[arg(short, default_value = "")]
151     options:        String,
152
153     /// Force color on/off. Default: autodetect tty
154     #[arg(short, long, action = clap::ArgAction::Set, default_value_t=atty::is(Stream::Stdout))]
155     colorize:       bool,
156
157     /// Verbose mode
158     #[arg(short, long, action = clap::ArgAction::Count)]
159     verbose:        u8,
160 }
161
162 fn devs_str_sbs_from_uuid(uuid: String) -> anyhow::Result<(String, Vec<bch_sb_handle>)> {
163     debug!("enumerating devices with UUID {}", uuid);
164
165     let devs_sbs = Uuid::parse_str(&uuid)
166         .map(|uuid| get_devices_by_uuid(uuid))??;
167
168     let devs_str = devs_sbs
169         .iter()
170         .map(|(dev, _)| dev.to_str().unwrap())
171         .collect::<Vec<_>>()
172         .join(":");
173
174     let sbs: Vec<bch_sb_handle> = devs_sbs.iter().map(|(_, sb)| *sb).collect();
175
176     Ok((devs_str, sbs))
177
178 }
179
180 fn cmd_mount_inner(opt: Cli) -> anyhow::Result<()> {
181     let (devs, sbs) = if opt.dev.starts_with("UUID=") {
182         let uuid = opt.dev.replacen("UUID=", "", 1);
183         devs_str_sbs_from_uuid(uuid)?
184     } else if opt.dev.starts_with("OLD_BLKID_UUID=") {
185         let uuid = opt.dev.replacen("OLD_BLKID_UUID=", "", 1);
186         devs_str_sbs_from_uuid(uuid)?
187     } else {
188         let mut sbs = Vec::new();
189
190         for dev in opt.dev.split(':') {
191             let dev = PathBuf::from(dev);
192             sbs.push(bch_bindgen::rs::read_super(&dev)?);
193         }
194
195         (opt.dev, sbs)
196     };
197
198     if sbs.len() == 0 {
199         Err(anyhow::anyhow!("No device found from specified parameters"))?;
200     } else if unsafe { bcachefs::bch2_sb_is_encrypted(sbs[0].sb) } {
201         key::prepare_key(&sbs[0], opt.key_location)?;
202     }
203
204     if let Some(mountpoint) = opt.mountpoint {
205         info!(
206             "mounting with params: device: {}, target: {}, options: {}",
207             devs,
208             mountpoint.to_string_lossy(),
209             &opt.options
210         );
211
212         mount(devs, mountpoint, &opt.options)?;
213     } else {
214         info!(
215             "would mount with params: device: {}, options: {}",
216             devs,
217             &opt.options
218         );
219     }
220
221     Ok(())
222 }
223
224 pub fn cmd_mount(argv: Vec<&OsStr>) -> c_int {
225     let opt = Cli::parse_from(argv);
226
227     // @TODO : more granular log levels via mount option
228     log::set_max_level(match opt.verbose {
229         0 => LevelFilter::Warn,
230         1 => LevelFilter::Trace,
231         2_u8..=u8::MAX => todo!(),
232     });
233
234     colored::control::set_override(opt.colorize);
235     if let Err(e) = cmd_mount_inner(opt) {
236         error!("Fatal error: {}", e);
237         1
238     } else {
239         info!("Successfully mounted");
240         0
241     }
242 }