use crate::error::{Error, ErrorKind, Result};
use crate::logger::diff;
use crate::modules::{parse_params, Module, ModuleResult};
use crate::utils::parse_octal;
use crate::vars::Vars;
#[cfg(feature = "docs")]
use rash_derive::DocJsonSchema;
use std::fs::{
create_dir_all, metadata, remove_dir_all, remove_file, set_permissions, File as StdFile,
Metadata,
};
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
#[cfg(feature = "docs")]
use schemars::schema::RootSchema;
#[cfg(feature = "docs")]
use schemars::JsonSchema;
use serde::Deserialize;
use serde_yaml::Value;
#[cfg(feature = "docs")]
use strum_macros::{Display, EnumString};
#[derive(Debug, PartialEq, Deserialize)]
#[cfg_attr(feature = "docs", derive(JsonSchema, DocJsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Params {
mode: Option<String>,
path: String,
state: Option<State>,
}
#[derive(Debug, PartialEq, Deserialize)]
#[cfg_attr(feature = "docs", derive(EnumString, Display, JsonSchema))]
#[serde(rename_all = "lowercase")]
enum State {
Absent,
Directory,
File,
Touch,
}
fn fail_if_not_exist(params: Params) -> Result<ModuleResult> {
match metadata(¶ms.path) {
Ok(_) => Ok(ModuleResult {
changed: false,
output: Some(params.path),
extra: None,
}),
Err(_) => Err(Error::new(
ErrorKind::NotFound,
format!("file {} is absent, cannot continue", ¶ms.path),
)),
}
}
fn apply_permissions_if_necessary(
meta: Metadata,
octal_mode: u32,
params: Params,
check_mode: bool,
) -> Result<ModuleResult> {
let mut permissions = meta.permissions();
let original_mode = permissions.mode() & 0o7777;
match original_mode != octal_mode {
true => {
diff(
format!("mode: {:o}", &original_mode),
format!("mode: {:o}", &octal_mode),
);
if !check_mode {
permissions.set_mode(octal_mode);
set_permissions(¶ms.path, permissions)?;
}
Ok(ModuleResult {
changed: true,
output: Some(params.path),
extra: None,
})
}
false => Ok(ModuleResult {
changed: false,
output: Some(params.path),
extra: None,
}),
}
}
fn find_first_existing_directory(path: &Path) -> Result<&Path> {
match path.is_dir() {
true => Ok(path),
false => find_first_existing_directory(path.parent().ok_or_else(|| {
Error::new(
ErrorKind::InvalidData,
format!(
"Parent of {} cannot be accessed",
path.to_str().unwrap_or("Parent cannot be accessed")
),
)
})?),
}
}
fn apply_permissions_recursively(octal_mode: u32, path: &Path, until: &Path) -> Result<()> {
match path == until {
true => Ok(()),
false => {
let meta = metadata(path)?;
let mut permissions = meta.permissions();
permissions.set_mode(octal_mode);
set_permissions(path, permissions)?;
apply_permissions_recursively(
octal_mode,
path.parent().ok_or_else(|| {
Error::new(
ErrorKind::InvalidData,
format!(
"Parent of {} cannot be accessed",
path.to_str().unwrap_or("Parent cannot be accessed")
),
)
})?,
until,
)
}
}
}
fn define_file(params: Params, check_mode: bool) -> Result<ModuleResult> {
match ¶ms.state {
Some(State::File) | None => match ¶ms.mode {
Some(mode) => {
let octal_mode = parse_octal(mode)?;
match metadata(¶ms.path) {
Ok(meta) => {
apply_permissions_if_necessary(meta, octal_mode, params, check_mode)
}
Err(_not_exists) => fail_if_not_exist(params),
}
}
None => fail_if_not_exist(params),
},
Some(State::Absent) => match metadata(¶ms.path) {
Ok(meta) => {
if meta.is_file() {
diff("state: file\n", "state: absent\n");
if !check_mode {
remove_file(¶ms.path)?;
}
} else if meta.is_dir() {
diff("state: directory\n", "state: absent\n");
if !check_mode {
remove_dir_all(¶ms.path)?;
}
} else {
return Err(Error::new(
ErrorKind::InvalidData,
format!(
"file {} is unknown type and cannot be removed",
¶ms.path
),
));
}
Ok(ModuleResult {
changed: true,
output: Some(params.path),
extra: None,
})
}
Err(_not_exists) => Ok(ModuleResult {
changed: false,
output: Some(params.path),
extra: None,
}),
},
Some(State::Directory) => match ¶ms.mode {
Some(mode) => {
let octal_mode = parse_octal(mode)?;
match metadata(¶ms.path) {
Ok(meta) => {
apply_permissions_if_necessary(meta, octal_mode, params, check_mode)
}
Err(_not_exists) => {
diff("state: absent\n", "state: directory\n");
if !check_mode {
let first_existing_parent =
find_first_existing_directory(Path::new(¶ms.path))?;
create_dir_all(¶ms.path)?;
apply_permissions_recursively(
octal_mode,
Path::new(¶ms.path),
first_existing_parent,
)?;
}
Ok(ModuleResult {
changed: true,
output: Some(params.path),
extra: None,
})
}
}
}
None => match metadata(¶ms.path) {
Ok(_exists) => Ok(ModuleResult {
changed: false,
output: Some(params.path),
extra: None,
}),
Err(_not_exists) => {
diff("state: absent\n", "state: directory\n");
if !check_mode {
create_dir_all(¶ms.path)?;
}
Ok(ModuleResult {
changed: true,
output: Some(params.path),
extra: None,
})
}
},
},
Some(State::Touch) => match ¶ms.mode {
Some(mode) => {
let octal_mode = parse_octal(mode)?;
match metadata(¶ms.path) {
Ok(meta) => {
apply_permissions_if_necessary(meta, octal_mode, params, check_mode)
}
Err(_not_exists) => {
diff("state: absent\n", "state: file\n");
if !check_mode {
let file = StdFile::create(¶ms.path)?;
let mut permissions = file.metadata()?.permissions();
permissions.set_mode(octal_mode);
set_permissions(¶ms.path, permissions)?;
}
Ok(ModuleResult {
changed: true,
output: Some(params.path),
extra: None,
})
}
}
}
None => match metadata(¶ms.path) {
Ok(_) => Ok(ModuleResult {
changed: false,
output: Some(params.path),
extra: None,
}),
Err(_not_exists) => {
diff("state: absent\n", "state: file\n");
if !check_mode {
StdFile::create(¶ms.path)?;
}
Ok(ModuleResult {
changed: true,
output: Some(params.path),
extra: None,
})
}
},
},
}
}
#[derive(Debug)]
pub struct File;
impl Module for File {
fn get_name(&self) -> &str {
"file"
}
fn exec(
&self,
optional_params: Value,
vars: Vars,
check_mode: bool,
) -> Result<(ModuleResult, Vars)> {
Ok((
define_file(parse_params(optional_params)?, check_mode)?,
vars,
))
}
#[cfg(feature = "docs")]
fn get_json_schema(&self) -> Option<RootSchema> {
Some(Params::get_json_schema())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::create_dir;
use std::os::unix::fs::PermissionsExt;
use tempfile::tempdir;
#[test]
fn test_parse_params() {
let yaml: Value = serde_yaml::from_str(
r#"
path: /yea
state: file
mode: "0644"
"#,
)
.unwrap();
let params: Params = parse_params(yaml).unwrap();
assert_eq!(
params,
Params {
mode: Some("0644".to_string()),
path: "/yea".to_string(),
state: Some(State::File),
}
);
}
#[test]
fn test_parse_params_no_path() {
let yaml: Value = serde_yaml::from_str(
r#"
mode: "0644"
state: file
"#,
)
.unwrap();
let error = parse_params::<Params>(yaml).unwrap_err();
assert_eq!(error.kind(), ErrorKind::InvalidData);
}
#[test]
fn test_parse_params_no_mode() {
let yaml: Value = serde_yaml::from_str(
r#"
path: foo
state: directory
"#,
)
.unwrap();
let params: Params = parse_params(yaml).unwrap();
assert_eq!(
params,
Params {
mode: None,
path: "foo".to_string(),
state: Some(State::Directory),
}
);
}
#[test]
fn test_parse_params_invalid_mode() {
let yaml: Value = serde_yaml::from_str(
r#"
mode:
yea: boo
path: foo
state: directory
"#,
)
.unwrap();
let error = parse_params::<Params>(yaml).unwrap_err();
assert_eq!(error.kind(), ErrorKind::InvalidData);
}
#[test]
fn test_parse_params_no_state() {
let yaml: Value = serde_yaml::from_str(
r#"
path: foo
"#,
)
.unwrap();
let params: Params = parse_params(yaml).unwrap();
assert_eq!(
params,
Params {
mode: None,
path: "foo".to_string(),
state: None,
}
);
}
#[test]
fn test_define_file_no_change() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("no_change");
let file = StdFile::create(file_path.clone()).unwrap();
let mut permissions = file.metadata().unwrap().permissions();
permissions.set_mode(0o400);
set_permissions(&file_path, permissions).unwrap();
let output = define_file(
Params {
path: file_path.to_str().unwrap().to_string(),
state: None,
mode: None,
},
false,
)
.unwrap();
let permissions = metadata(&file_path).unwrap().permissions();
assert_eq!(
format!("{:o}", permissions.mode() & 0o7777),
format!("{:o}", 0o400)
);
assert_eq!(
output,
ModuleResult {
changed: false,
output: Some(file_path.to_str().unwrap().to_string()),
extra: None,
}
);
}
#[test]
fn test_define_file_no_exists() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("no_exists");
let error = define_file(
Params {
path: file_path.to_str().unwrap().to_string(),
state: None,
mode: None,
},
false,
)
.unwrap_err();
assert_eq!(error.kind(), ErrorKind::NotFound);
}
#[test]
fn test_define_file_created() {
let dir = tempdir().unwrap();
let dir_path = dir.path().join("created");
let output = define_file(
Params {
path: dir_path.to_str().unwrap().to_string(),
state: Some(State::Touch),
mode: None,
},
false,
)
.unwrap();
let dir_metadata = metadata(&dir_path).unwrap();
let dir_permissions = dir_metadata.permissions();
assert!(dir_metadata.is_file());
assert_eq!(
format!("{:o}", dir_permissions.mode() & 0o7777),
format!("{:o}", 0o644)
);
assert_eq!(
output,
ModuleResult {
changed: true,
output: Some(dir_path.to_str().unwrap().to_string()),
extra: None,
}
);
}
#[test]
fn test_define_file_created_check_mode() {
let dir = tempdir().unwrap();
let dir_path = dir.path().join("created");
let output = define_file(
Params {
path: dir_path.to_str().unwrap().to_string(),
state: Some(State::Touch),
mode: None,
},
true,
)
.unwrap();
let dir_metadata = metadata(&dir_path);
assert!(dir_metadata.is_err());
assert_eq!(
output,
ModuleResult {
changed: true,
output: Some(dir_path.to_str().unwrap().to_string()),
extra: None,
}
);
}
#[test]
fn test_define_file_created_directory_and_subdirectories() {
let dir = tempdir().unwrap();
let parent_path = dir.path().join("parent");
let dir_path = parent_path
.join("foo")
.join("created_directory_and_subdirectories");
let output = define_file(
Params {
path: dir_path.to_str().unwrap().to_string(),
state: Some(State::Directory),
mode: Some("0750".to_string()),
},
false,
)
.unwrap();
let parent_metadata = metadata(&parent_path).unwrap();
let parent_permissions = parent_metadata.permissions();
assert!(parent_metadata.is_dir());
assert_eq!(
format!("{:o}", parent_permissions.mode() & 0o7777),
format!("{:o}", 0o750)
);
let dir_metadata = metadata(&dir_path).unwrap();
let dir_permissions = dir_metadata.permissions();
assert!(dir_metadata.is_dir());
assert_eq!(
format!("{:o}", dir_permissions.mode() & 0o7777),
format!("{:o}", 0o750)
);
assert_eq!(
output,
ModuleResult {
changed: true,
output: Some(dir_path.to_str().unwrap().to_string()),
extra: None,
}
);
}
#[test]
fn test_define_file_created_directory_and_subdirectories_check_mode() {
let dir = tempdir().unwrap();
let parent_path = dir.path().join("parent");
let dir_path = parent_path
.join("foo")
.join("created_directory_and_subdirectories");
let output = define_file(
Params {
path: dir_path.to_str().unwrap().to_string(),
state: Some(State::Directory),
mode: Some("0750".to_string()),
},
true,
)
.unwrap();
let parent_metadata = metadata(&parent_path);
let dir_metadata = metadata(&dir_path);
assert!(parent_metadata.is_err());
assert!(dir_metadata.is_err());
assert_eq!(
output,
ModuleResult {
changed: true,
output: Some(dir_path.to_str().unwrap().to_string()),
extra: None,
}
);
}
#[test]
fn test_define_file_modify_permissions() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("modify_permissions");
let file = StdFile::create(file_path.clone()).unwrap();
let mut permissions = file.metadata().unwrap().permissions();
permissions.set_mode(0o400);
set_permissions(&file_path, permissions).unwrap();
let output = define_file(
Params {
path: file_path.to_str().unwrap().to_string(),
state: Some(State::File),
mode: Some("0604".to_string()),
},
false,
)
.unwrap();
let file_metadata = metadata(&file_path).unwrap();
let permissions = file_metadata.permissions();
assert!(file_metadata.is_file());
assert_eq!(
format!("{:o}", permissions.mode() & 0o7777),
format!("{:o}", 0o604)
);
assert_eq!(
output,
ModuleResult {
changed: true,
output: Some(file_path.to_str().unwrap().to_string()),
extra: None,
}
);
}
#[test]
fn test_define_file_modify_permissions_check_mode() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("modify_permissions");
let file = StdFile::create(file_path.clone()).unwrap();
let mut permissions = file.metadata().unwrap().permissions();
permissions.set_mode(0o400);
set_permissions(&file_path, permissions).unwrap();
let output = define_file(
Params {
path: file_path.to_str().unwrap().to_string(),
state: Some(State::File),
mode: Some("0604".to_string()),
},
true,
)
.unwrap();
let file_metadata = metadata(&file_path).unwrap();
let permissions = file_metadata.permissions();
assert!(file_metadata.is_file());
assert_eq!(
format!("{:o}", permissions.mode() & 0o7777),
format!("{:o}", 0o400)
);
assert_eq!(
output,
ModuleResult {
changed: true,
output: Some(file_path.to_str().unwrap().to_string()),
extra: None,
}
);
}
#[test]
fn test_define_file_remove_file() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("remove_file");
let file = StdFile::create(file_path.clone()).unwrap();
let mut permissions = file.metadata().unwrap().permissions();
permissions.set_mode(0o400);
set_permissions(&file_path, permissions).unwrap();
let output = define_file(
Params {
path: file_path.to_str().unwrap().to_string(),
state: Some(State::Absent),
mode: None,
},
false,
)
.unwrap();
let file_metadata = metadata(&file_path);
assert!(file_metadata.is_err());
assert_eq!(
output,
ModuleResult {
changed: true,
output: Some(file_path.to_str().unwrap().to_string()),
extra: None,
}
);
}
#[test]
fn test_define_file_remove_file_check_mode() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("remove_file");
let file = StdFile::create(file_path.clone()).unwrap();
let mut permissions = file.metadata().unwrap().permissions();
permissions.set_mode(0o400);
set_permissions(&file_path, permissions).unwrap();
let output = define_file(
Params {
path: file_path.to_str().unwrap().to_string(),
state: Some(State::Absent),
mode: None,
},
true,
)
.unwrap();
let file_metadata = metadata(&file_path);
assert!(file_metadata.is_ok());
assert_eq!(
output,
ModuleResult {
changed: true,
output: Some(file_path.to_str().unwrap().to_string()),
extra: None,
}
);
}
#[test]
fn test_define_file_remove_directory() {
let dir = tempdir().unwrap();
let dir_path = dir.path().join("remove_directory");
create_dir(&dir_path).unwrap();
let dir_metadata = metadata(&dir_path).unwrap();
let mut permissions = dir_metadata.permissions();
permissions.set_mode(0o700);
set_permissions(&dir_path, permissions).unwrap();
let output = define_file(
Params {
path: dir_path.to_str().unwrap().to_string(),
state: Some(State::Absent),
mode: None,
},
false,
)
.unwrap();
let dir_metadata = metadata(&dir_path);
assert!(dir_metadata.is_err());
assert_eq!(
output,
ModuleResult {
changed: true,
output: Some(dir_path.to_str().unwrap().to_string()),
extra: None,
}
);
}
#[test]
fn test_define_file_remove_directory_check_mode() {
let dir = tempdir().unwrap();
let dir_path = dir.path().join("remove_directory");
create_dir(&dir_path).unwrap();
let dir_metadata = metadata(&dir_path).unwrap();
let mut permissions = dir_metadata.permissions();
permissions.set_mode(0o700);
set_permissions(&dir_path, permissions).unwrap();
let output = define_file(
Params {
path: dir_path.to_str().unwrap().to_string(),
state: Some(State::Absent),
mode: None,
},
true,
)
.unwrap();
let dir_metadata = metadata(&dir_path);
assert!(dir_metadata.is_ok());
assert_eq!(
output,
ModuleResult {
changed: true,
output: Some(dir_path.to_str().unwrap().to_string()),
extra: None,
}
);
}
#[test]
fn test_define_file_remove_directory_and_subdirectories() {
let dir = tempdir().unwrap();
let dir_path = dir.path().join("remove_directory_and_subdirectories");
create_dir(&dir_path).unwrap();
create_dir(dir_path.join("one_dir")).unwrap();
StdFile::create(dir_path.join("one_file")).unwrap();
let dir_metadata = metadata(&dir_path).unwrap();
let mut permissions = dir_metadata.permissions();
permissions.set_mode(0o700);
set_permissions(&dir_path, permissions).unwrap();
let output = define_file(
Params {
path: dir_path.to_str().unwrap().to_string(),
state: Some(State::Absent),
mode: None,
},
false,
)
.unwrap();
let dir_metadata = metadata(&dir_path);
assert!(dir_metadata.is_err());
assert_eq!(
output,
ModuleResult {
changed: true,
output: Some(dir_path.to_str().unwrap().to_string()),
extra: None,
}
);
}
#[test]
fn test_define_file_remove_directory_and_subdirectories_check_mode() {
let dir = tempdir().unwrap();
let dir_path = dir.path().join("remove_directory_and_subdirectories");
create_dir(&dir_path).unwrap();
create_dir(dir_path.join("one_dir")).unwrap();
StdFile::create(dir_path.join("one_file")).unwrap();
let dir_metadata = metadata(&dir_path).unwrap();
let mut permissions = dir_metadata.permissions();
permissions.set_mode(0o700);
set_permissions(&dir_path, permissions).unwrap();
let output = define_file(
Params {
path: dir_path.to_str().unwrap().to_string(),
state: Some(State::Absent),
mode: None,
},
true,
)
.unwrap();
let dir_metadata = metadata(&dir_path);
assert!(dir_metadata.is_ok());
assert_eq!(
output,
ModuleResult {
changed: true,
output: Some(dir_path.to_str().unwrap().to_string()),
extra: None,
}
);
}
}