use crate::data::paths::{PathData, PathDeconstruction};
use crate::filesystem::mounts::FilesystemType;
use crate::library::results::{HttmError, HttmResult};
use crate::library::utility::user_has_effective_root;
use crate::roll_forward::exec::RollForward;
use std::path::{Path, PathBuf};
use std::process::{Child, Command as ExecProcess, Stdio};
use which::which;
pub struct RunZFSCommand {
zfs_command: PathBuf,
}
impl RunZFSCommand {
pub fn new() -> HttmResult<Self> {
let zfs_command = which("zfs").map_err(|_err| {
HttmError::new("'zfs' command not found. Make sure the command 'zfs' is in your path.")
})?;
Ok(Self { zfs_command })
}
pub fn version(&self) -> HttmResult<String> {
let process_output = ExecProcess::new(&self.zfs_command).arg("-V").output()?;
if !process_output.stderr.is_empty() {
return HttmError::new(std::str::from_utf8(&process_output.stderr)?).into();
}
Ok(std::string::String::from_utf8(process_output.stdout)?)
}
pub fn snapshot(&self, snapshot_names: &[String]) -> HttmResult<()> {
let mut process_args = vec!["snapshot".to_owned()];
process_args.extend_from_slice(snapshot_names);
let process_output = ExecProcess::new(&self.zfs_command)
.args(&process_args)
.output()?;
let stderr_string = std::str::from_utf8(&process_output.stderr)?.trim();
if !stderr_string.is_empty() {
let description = if stderr_string
.contains("cannot create snapshots : permission denied")
{
"httm must have root privileges to snapshot a filesystem".to_owned()
} else {
"httm was unable to take snapshots. The 'zfs' command issued the following error: "
.to_owned()
+ stderr_string
};
return HttmError::from(description).into();
}
Ok(())
}
pub fn rollback(&self, snapshot_names: &[String]) -> HttmResult<()> {
let mut process_args = vec!["rollback".to_owned(), "-r".to_owned()];
process_args.extend_from_slice(snapshot_names);
let process_output = ExecProcess::new(&self.zfs_command)
.args(&process_args)
.output()?;
let stderr_string = std::str::from_utf8(&process_output.stderr)?.trim();
if !stderr_string.is_empty() {
let description = if stderr_string
.contains("cannot destroy snapshots: permission denied")
{
"httm may need root privileges to 'zfs rollback' a filesystem".to_owned()
} else {
"httm was unable to rollback the snapshot name. The 'zfs' command issued the following error: ".to_owned() + stderr_string
};
return HttmError::from(description).into();
}
Ok(())
}
pub fn prune(&self, snapshot_names: &[String]) -> HttmResult<()> {
let mut process_args = vec!["destroy".to_owned(), "-r".to_owned()];
process_args.extend_from_slice(snapshot_names);
let process_output = ExecProcess::new(&self.zfs_command)
.args(&process_args)
.output()?;
let stderr_string = std::str::from_utf8(&process_output.stderr)?.trim();
if !stderr_string.is_empty() {
let description = if stderr_string
.contains("cannot destroy snapshots: permission denied")
{
"httm must have root privileges to destroy a snapshot filesystem".to_owned()
} else {
"httm was unable to destroy snapshots. The 'zfs' command issued the following error: "
.to_owned()
+ stderr_string
};
return HttmError::from(description).into();
}
Ok(())
}
pub fn allow(&self, fs_name: &str, allow_type: &ZfsAllowPriv) -> HttmResult<()> {
let process_args = vec!["allow", fs_name];
let process_output = ExecProcess::new(&self.zfs_command)
.args(&process_args)
.output()?;
let stderr_string = std::str::from_utf8(&process_output.stderr)?.trim();
let stdout_string: &str = std::str::from_utf8(&process_output.stdout)?.trim();
if !stderr_string.is_empty() {
let description = "httm was unable to determine 'zfs allow' for the path given. The 'zfs' command issued the following error: ".to_owned() + stderr_string;
return HttmError::from(description).into();
}
let user_name = std::env::var("USER")?;
if !stdout_string.contains(&user_name)
|| !allow_type
.as_zfs_cmd_strings()
.iter()
.all(|p| stdout_string.contains(p))
{
let description = "User does not have 'zfs allow' privileges for the path given.";
return HttmError::new(description).into();
}
Ok(())
}
pub fn diff(&self, roll_forward: &RollForward) -> HttmResult<Child> {
let full_name = roll_forward.full_name();
let process_args = vec!["diff", "-H", "-t", "-h", &full_name];
let process_handle = ExecProcess::new(&self.zfs_command)
.args(&process_args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
Ok(process_handle)
}
}
pub enum ZfsAllowPriv {
Snapshot,
Rollback,
}
impl ZfsAllowPriv {
pub fn from_path(&self, path: &Path) -> HttmResult<Box<Path>> {
let path_data = PathData::from(path);
ZfsAllowPriv::from_opt_proximate_dataset(&self, &path_data, None)
}
pub fn from_opt_proximate_dataset(
&self,
path_data: &PathData,
opt_proximate_dataset: Option<&Path>,
) -> HttmResult<Box<Path>> {
let Some(fs_name) = path_data.source(opt_proximate_dataset) else {
let description = format!(
"Could not determine dataset name from path given: {:?}",
path_data.path()
);
return HttmError::from(description).into();
};
match path_data.fs_type(opt_proximate_dataset) {
Some(FilesystemType::Zfs) => {}
_ => {
let description = format!(
"httm only supports snapshot guards for ZFS paths. Path is not located on a ZFS dataset: {:?}",
path_data.path()
);
return HttmError::from(description).into();
}
}
Self::from_fs_name(&self, &fs_name.to_string_lossy())?;
Ok(fs_name)
}
pub fn from_fs_name(&self, fs_name: &str) -> HttmResult<()> {
let description = match self {
ZfsAllowPriv::Rollback => "A rollback after a restore action",
ZfsAllowPriv::Snapshot => "A snapshot guard before restore action",
};
if let Err(root_error) = user_has_effective_root(description) {
if let Err(_allow_priv_error) = self.user_has_zfs_allow_priv(fs_name) {
return Err(root_error);
}
}
Ok(())
}
fn as_zfs_cmd_strings(&self) -> &[&str] {
match self {
ZfsAllowPriv::Rollback => &["rollback"],
ZfsAllowPriv::Snapshot => &["snapshot", "mount"],
}
}
fn user_has_zfs_allow_priv(&self, fs_name: &str) -> HttmResult<()> {
let run_zfs = RunZFSCommand::new()?;
run_zfs.allow(fs_name, self)
}
}