diff --git a/libcitadel/src/mount.rs b/libcitadel/src/mount.rs deleted file mode 100644 index 46b6642..0000000 --- a/libcitadel/src/mount.rs +++ /dev/null @@ -1,64 +0,0 @@ - -use std::path::{PathBuf,Path}; -use std::fs; -use crate::Result; - -pub struct Mount { - source: String, - target: PathBuf, - fstype: String, - options: String, -} - -impl Mount { - /// - /// Returns `true` if `path` matches the source field (first field) - /// of any of the mount lines listed in /proc/mounts - /// - pub fn is_source_mounted>(path: P) -> Result { - let path_str = path.as_ref().to_string_lossy(); - let mounts = Mount::all_mounts()?; - Ok(mounts.into_iter().any(|m| m.source == path_str)) - } - - pub fn is_target_mounted>(path: P) -> Result { - let mounts = Mount::all_mounts()?; - Ok(mounts.into_iter().any(|m| m.target == path.as_ref())) - } - - pub fn all_mounts() -> Result> { - let s = fs::read_to_string("/proc/mounts")?; - Ok(s.lines().flat_map(Mount::parse_mount_line).collect()) - } - - fn parse_mount_line(line: &str) -> Option { - let parts = line.split_whitespace().collect::>(); - if parts.len() < 4 { - warn!("Failed to parse mount line: {}", line); - return None; - } - Some(Mount{ - source: parts[0].to_string(), - target: PathBuf::from(parts[1]), - fstype: parts[2].to_string(), - options: parts[3].to_string(), - }) - } - - pub fn source(&self) -> &str { - &self.source - } - - pub fn target(&self) -> &Path { - &self.target - } - - pub fn fstype(&self) -> &str { - &self.fstype - } - - pub fn options(&self) -> &str { - &self.options - } -} - diff --git a/libcitadel/src/system/lock.rs b/libcitadel/src/system/lock.rs new file mode 100644 index 0000000..0b6f764 --- /dev/null +++ b/libcitadel/src/system/lock.rs @@ -0,0 +1,78 @@ +use std::fs::{self,File,OpenOptions}; +use std::io::{Error,ErrorKind}; +use std::os::unix::io::AsRawFd; +use std::path::{Path,PathBuf}; + +use crate::Result; + +pub struct FileLock { + file: File, + path: PathBuf, +} + +impl FileLock { + + pub fn acquire>(path: P) -> Result { + let path = path.as_ref().to_path_buf(); + let file = Self::open_lockfile(&path)?; + let flock = FileLock { file, path }; + flock.lock()?; + Ok(flock) + } + + fn open_lockfile(path: &Path) -> Result { + if let Some(parent) = path.parent() { + if !parent.exists() { + fs::create_dir_all(parent)?; + } + } + + for _ in 0..3 { + if let Some(file) = Self::try_create_lockfile(path)? { + return Ok(file); + } + if let Some(file) = Self::try_open_lockfile(path)? { + return Ok(file); + } + } + Err(format_err!("unable to acquire lockfile {}", path.display() )) + } + + fn try_create_lockfile(path: &Path) -> Result> { + match OpenOptions::new().write(true).create_new(true).open(path) { + Ok(file) => Ok(Some(file)), + Err(ref e) if e.kind() == ErrorKind::AlreadyExists => Ok(None), + Err(e) => Err(e.into()), + } + } + + fn try_open_lockfile(path: &Path) -> Result> { + match File::open(path) { + Ok(file) => Ok(Some(file)), + Err(ref e) if e.kind() == ErrorKind::NotFound => Ok(None), + Err(e) => Err(e.into()), + } + } + + fn unlock(&self) -> Result<()> { + self.flock(libc::LOCK_UN) + } + + fn lock(&self) -> Result<()> { + self.flock(libc::LOCK_EX) + } + + fn flock(&self, flag: libc::c_int) -> Result<()> { + if unsafe { libc::flock(self.file.as_raw_fd(), flag) } < 0 { + return Err(Error::last_os_error().into()); + } + Ok(()) + } +} + +impl Drop for FileLock { + fn drop(&mut self) { + let _ = fs::remove_file(&self.path); + let _ = self.unlock(); + } +} diff --git a/libcitadel/src/system/loopdev.rs b/libcitadel/src/system/loopdev.rs new file mode 100644 index 0000000..44d2a04 --- /dev/null +++ b/libcitadel/src/system/loopdev.rs @@ -0,0 +1,120 @@ +use std::fmt; +use std::path::{Path,PathBuf}; + +use crate::Result; + +use super::mounts::Mounts; + +#[derive(Debug)] +pub struct LoopDevice(PathBuf); + +impl LoopDevice { + const LOSETUP: &'static str = "/usr/sbin/losetup"; + const MOUNT: &'static str = "/usr/bin/mount"; + + fn new>(device: P) -> LoopDevice { + let device = device.as_ref().to_path_buf(); + LoopDevice(device) + } + + pub fn create>(image: P, offset: Option, read_only: bool) -> Result { + let image = image.as_ref(); + let mut args = String::new(); + if let Some(offset) = offset { + args += &format!("--offset {} ", offset); + } + if read_only { + args += &format!("--read-only "); + } + args += &format!("-f --show {}", image.display()); + let output = cmd_with_output!(Self::LOSETUP, args)?; + Ok(LoopDevice::new(output)) + } + + pub fn with_loop(image: P, offset: Option, read_only: bool, f: F) -> Result + where P: AsRef, + F: FnOnce(&LoopDevice) -> Result, + { + let loopdev = Self::create(image, offset, read_only)?; + let result = f(&loopdev); + let detach_result = loopdev.detach(); + let r = result?; + detach_result.map_err(|e| format_err!("error detaching loop device: {}", e))?; + Ok(r) + } + + /// Search for an entry in /proc/mounts for a loop device which is mounted on the + /// specified mountpoint. + /// The relevant lines look like this: + /// + /// /dev/loop3 /run/citadel/realmfs/realmfs-name-rw.mountpoint ext4 rw,noatime,data=ordered 0 0 + /// + pub fn find_mounted_loop>(mount_target: P) -> Option { + let mount_target = mount_target.as_ref(); + Mounts::load().ok() + .and_then(|mounts| mounts.mounts() + .find(|m| m.target_path() == mount_target && + m.source().starts_with("/dev/loop")) + .map(|m| LoopDevice::new(m.source_path())) ) + } + + pub fn find_devices_for>(image: P) -> Result> { + let image = image.as_ref(); + // Output from losetup -j looks like this: + // /dev/loop1: [0036]:64845938 (/storage/resources/dev/citadel-extra-dev-001.img), offset 4096 + let output:String = cmd_with_output!(Self::LOSETUP, "-j {}", image.display())?; + Ok(output.lines() + .flat_map(|line| line.splitn(2, ":").next()) + .map(|s| LoopDevice::new(s)) + .collect()) + } + + pub fn detach(&self) -> Result<()> { + cmd!(Self::LOSETUP, format!("-d {}", self.0.display())) + } + + pub fn resize(&self) -> Result<()> { + cmd!(Self::LOSETUP, format!("-c {}", self.0.display())) + } + + pub fn device(&self) -> &Path { + &self.0 + } + + pub fn device_str(&self) -> &str { + self.device().to_str().unwrap() + } + + pub fn mount_ro>(&self, target: P) -> Result<()> { + let target = target.as_ref(); + cmd!(Self::MOUNT, "-oro,noatime {} {}", self, target.display()) + } + + pub fn mount>(&self, target: P) -> Result<()> { + let target = target.as_ref(); + cmd!(Self::MOUNT, "-orw,noatime {} {}", self, target.display()) + } + + pub fn mount_pair(&self, rw_target: P, ro_target: Q) -> Result<()> + where P: AsRef, + Q: AsRef + { + let rw = rw_target.as_ref(); + let ro = ro_target.as_ref(); + + self.mount(rw)?; + // From mount(8): + // + // mount --bind olddir newdir + // mount -o remount,bind,ro olddir newdir + cmd!(Self::MOUNT, "--bind {} {}", rw.display(), ro.display())?; + cmd!(Self::MOUNT, "-o remount,bind,ro {} {}", rw.display(), ro.display())?; + Ok(()) + } +} + +impl fmt::Display for LoopDevice { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.device().display()) + } +} diff --git a/libcitadel/src/system/mod.rs b/libcitadel/src/system/mod.rs new file mode 100644 index 0000000..91f9f49 --- /dev/null +++ b/libcitadel/src/system/mod.rs @@ -0,0 +1,9 @@ +mod lock; +mod loopdev; +mod mounts; +mod uname; + +pub use self::uname::UtsName; +pub use self::loopdev::LoopDevice; +pub use self::mounts::{Mounts,MountLine}; +pub use self::lock::FileLock; diff --git a/libcitadel/src/system/mounts.rs b/libcitadel/src/system/mounts.rs new file mode 100644 index 0000000..6cd820c --- /dev/null +++ b/libcitadel/src/system/mounts.rs @@ -0,0 +1,96 @@ +use std::fs; +use std::collections::HashMap; +use std::path::Path; + +use crate::Result; + +pub struct Mounts { + content: String, +} + +impl Mounts { + /// + /// Returns `true` if `path` matches the source field (first field) + /// of any of the mount lines listed in /proc/mounts + /// + pub fn is_source_mounted>(path: P) -> Result { + let path = path.as_ref(); + + let mounted = Self::load()? + .mounts() + .any(|m| m.source_path() == path); + + Ok(mounted) + } + + pub fn is_target_mounted>(path: P) -> Result { + let path = path.as_ref(); + + let mounted = Self::load()? + .mounts() + .any(|m| m.target_path() == path); + + Ok(mounted) + } + + pub fn load() -> Result { + let content = fs::read_to_string("/proc/mounts")?; + Ok(Mounts { content }) + } + + pub fn mounts(&self) -> impl Iterator { + self.content.lines().flat_map(MountLine::new) + } +} + +pub struct MountLine<'a> { + line: &'a str, +} + +impl <'a> MountLine<'a> { + + fn new(line: &str) -> Option { + if line.split_whitespace().count() >= 4 { + Some(MountLine { line }) + } else { + None + } + } + + fn field(&self, n: usize) -> &str { + self.line.split_whitespace().nth(n).unwrap() + } + + pub fn source(&self) -> &str { + self.field(0) + } + + pub fn source_path(&self) -> &Path { + Path::new(self.source()) + } + + pub fn target(&self) -> &str { + self.field(1) + } + + pub fn target_path(&self) -> &Path { + Path::new(self.target()) + } + + pub fn fstype(&self) -> &str { + self.field(2) + } + + pub fn options(&self) -> HashMap<&str,&str> { + self.field(3).split(',').map(Self::parse_key_val).collect() + } + + fn parse_key_val(option: &str) -> (&str,&str) { + let kv: Vec<&str> = option.splitn(2, '=').collect(); + if kv.len() == 2 { + (kv[0], kv[1]) + } else { + (kv[0], "") + } + } +} diff --git a/libcitadel/src/system/uname.rs b/libcitadel/src/system/uname.rs new file mode 100644 index 0000000..326c726 --- /dev/null +++ b/libcitadel/src/system/uname.rs @@ -0,0 +1,48 @@ +use std::ffi::CStr; +use std::mem; +use std::str; + +use libc::c_char; + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct UtsName(libc::utsname); + +#[allow(dead_code)] +impl UtsName { + pub fn uname() -> UtsName { + unsafe { + let mut ret: UtsName = mem::uninitialized(); + libc::uname(&mut ret.0); + ret + } + } + + pub fn sysname(&self) -> &str { + to_str(&(&self.0.sysname as *const c_char ) as *const *const c_char) + } + + pub fn nodename(&self) -> &str { + to_str(&(&self.0.nodename as *const c_char ) as *const *const c_char) + } + + pub fn release(&self) -> &str { + to_str(&(&self.0.release as *const c_char ) as *const *const c_char) + } + + pub fn version(&self) -> &str { + to_str(&(&self.0.version as *const c_char ) as *const *const c_char) + } + + pub fn machine(&self) -> &str { + to_str(&(&self.0.machine as *const c_char ) as *const *const c_char) + } +} + +#[inline] +fn to_str<'a>(s: *const *const c_char) -> &'a str { + unsafe { + let res = CStr::from_ptr(*s).to_bytes(); + str::from_utf8_unchecked(res) + } +} diff --git a/libcitadel/src/util.rs b/libcitadel/src/util.rs index 0b78efd..df6b838 100644 --- a/libcitadel/src/util.rs +++ b/libcitadel/src/util.rs @@ -1,14 +1,15 @@ use std::path::{Path,PathBuf}; -use std::process::{Command,ExitStatus,Stdio}; -use std::mem; -use libc::{self, c_char}; -use std::ffi::CStr; -use std::str::from_utf8_unchecked; +use std::process::{Command,Stdio}; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::fs::MetadataExt; use std::env; -use std::fs::File; -use std::io::{self,Seek,Read,BufReader,SeekFrom}; +use std::fs::{self,File}; +use std::ffi::CString; +use std::io::{self, Seek, Read, BufReader, SeekFrom}; use failure::ResultExt; +use walkdir::WalkDir; +use libc; use crate::Result; @@ -56,55 +57,11 @@ pub fn ensure_command_exists(cmd: &str) -> Result<()> { Err(format_err!("Cannot execute '{}': command does not exist", cmd)) } -pub fn exec_cmdline>(cmd_path: &str, args: S) -> Result<()> { - ensure_command_exists(cmd_path)?; - let args: Vec<&str> = args.as_ref().split_whitespace().collect::>(); - let status = Command::new(cmd_path) - .args(args) - .stderr(Stdio::inherit()) - .status()?; - - check_cmd_status(cmd_path, &status) -} - -pub fn exec_cmdline_quiet>(cmd_path: &str, args: S) -> Result<()> { - ensure_command_exists(cmd_path)?; - let args: Vec<&str> = args.as_ref().split_whitespace().collect::>(); - let status = Command::new(cmd_path) - .args(args) - .stderr(Stdio::null()) - .stdout(Stdio::null()) - .status()?; - - check_cmd_status(cmd_path, &status) -} - -pub fn exec_cmdline_with_output>(cmd_path: &str, args: S) -> Result { - ensure_command_exists(cmd_path)?; - let args: Vec<&str> = args.as_ref().split_whitespace().collect::>(); - let res = Command::new(cmd_path) - .args(args) - .stderr(Stdio::inherit()) - .output() - .context(format!("unable to execute {}", cmd_path))?; - - check_cmd_status(cmd_path, &res.status)?; - Ok(String::from_utf8(res.stdout).unwrap().trim().to_owned()) -} - -fn check_cmd_status(cmd_path: &str, status: &ExitStatus) -> Result<()> { - if !status.success() { - match status.code() { - Some(code) => bail!("command {} failed with exit code: {}", cmd_path, code), - None => bail!("command {} failed with no exit code", cmd_path), - } - } - Ok(()) -} pub fn sha256>(path: P) -> Result { - let output = exec_cmdline_with_output("/usr/bin/sha256sum", format!("{}", path.as_ref().display())) - .context(format!("failed to calculate sha256 on {}", path.as_ref().display()))?; + let path = path.as_ref(); + let output = cmd_with_output!("/usr/bin/256sum", "{}", path.display()) + .context(format!("failed to calculate sha256 on {}", path.display()))?; let v: Vec<&str> = output.split_whitespace().collect(); Ok(v[0].trim().to_owned()) @@ -158,71 +115,96 @@ pub fn exec_cmdline_pipe_input(cmd_path: &str, args: S, input: P, range: Fi } pub fn xz_compress>(path: P) -> Result<()> { - exec_cmdline("/usr/bin/xz", format!("-T0 {}", path.as_ref().display())) - .context(format!("failed to compress {}", path.as_ref().display()))?; + let path = path.as_ref(); + cmd!("/usr/bin/xz", "-T0 {}", path.display()) + .context(format!("failed to compress {}", path.display()))?; Ok(()) } pub fn xz_decompress>(path: P) -> Result<()> { - exec_cmdline("/usr/bin/xz", format!("-d {}", path.as_ref().display())) - .context(format!("failed to decompress {}", path.as_ref().display()))?; + let path = path.as_ref(); + cmd!("/usr/bin/xz", "-d {}", path.display()) + .context(format!("failed to decompress {}", path.display()))?; Ok(()) } -pub fn mount>(source: &str, target: P, options: Option<&str>) -> Result<()> { - let paths = format!("{} {}", source, target.as_ref().display()); - let args = match options { - Some(s) => format!("{} {}", s, paths), - None => paths, - }; - exec_cmdline("/usr/bin/mount", args) +pub fn mount>(source: impl AsRef, target: P, options: Option<&str>) -> Result<()> { + let source = source.as_ref(); + let target = target.as_ref(); + if let Some(options) = options { + cmd!("/usr/bin/mount", "{} {} {}", options, source, target.display()) + } else { + cmd!("/usr/bin/mount", "{} {}", source, target.display()) + } } pub fn umount>(path: P) -> Result<()> { - let args = format!("{}", path.as_ref().display()); - exec_cmdline("/usr/bin/umount", args) + let path = path.as_ref(); + cmd!("/usr/bin/umount", "{}", path.display()) } - -#[repr(C)] -#[derive(Clone, Copy)] -pub struct UtsName(libc::utsname); - -#[allow(dead_code)] -impl UtsName { - pub fn sysname(&self) -> &str { - to_str(&(&self.0.sysname as *const c_char ) as *const *const c_char) - } - - pub fn nodename(&self) -> &str { - to_str(&(&self.0.nodename as *const c_char ) as *const *const c_char) - } - - pub fn release(&self) -> &str { - to_str(&(&self.0.release as *const c_char ) as *const *const c_char) - } - - pub fn version(&self) -> &str { - to_str(&(&self.0.version as *const c_char ) as *const *const c_char) - } - - pub fn machine(&self) -> &str { - to_str(&(&self.0.machine as *const c_char ) as *const *const c_char) - } +pub fn chown_user>(path: P) -> io::Result<()> { + chown(path.as_ref(), 1000, 1000) } -pub fn uname() -> UtsName { +pub fn chown(path: &Path, uid: u32, gid: u32) -> io::Result<()> { + let cstr = CString::new(path.as_os_str().as_bytes())?; unsafe { - let mut ret: UtsName = mem::uninitialized(); - libc::uname(&mut ret.0); - ret + if libc::chown(cstr.as_ptr(), uid, gid) == -1 { + return Err(io::Error::last_os_error()); + } } + Ok(()) } -#[inline] -fn to_str<'a>(s: *const *const c_char) -> &'a str { - unsafe { - let res = CStr::from_ptr(*s).to_bytes(); - from_utf8_unchecked(res) +fn copy_path(from: &Path, to: &Path, chown_to: Option<(u32,u32)>) -> Result<()> { + if to.exists() { + bail!("destination path {} already exists which is not expected", to.display()); } + + let meta = from.metadata()?; + + if from.is_dir() { + fs::create_dir(to)?; + } else { + fs::copy(&from, &to)?; + } + + if let Some((uid,gid)) = chown_to { + chown(to, uid, gid)?; + } else { + chown(to, meta.uid(), meta.gid())?; + } + Ok(()) + +} + +pub fn copy_tree(from_base: &Path, to_base: &Path) -> Result<()> { + _copy_tree(from_base, to_base, None) +} + +pub fn copy_tree_with_chown(from_base: &Path, to_base: &Path, chown_to: (u32,u32)) -> Result<()> { + _copy_tree(from_base, to_base, Some(chown_to)) +} + +fn _copy_tree(from_base: &Path, to_base: &Path, chown_to: Option<(u32,u32)>) -> Result<()> { + for entry in WalkDir::new(from_base) { + let path = entry?.path().to_owned(); + let to = to_base.join(path.strip_prefix(from_base)?); + if &to != to_base { + copy_path(&path, &to, chown_to) + .map_err(|e| format_err!("failed to copy {} to {}: {}", path.display(), to.display(), e))?; + } + } + Ok(()) +} + +pub fn chown_tree(base: &Path, chown_to: (u32,u32), include_base: bool) -> Result<()> { + for entry in WalkDir::new(base) { + let entry = entry?; + if entry.path() != base || include_base { + chown(entry.path(), chown_to.0, chown_to.1)?; + } + } + Ok(()) }