use crate::service::Services;
use crate::{error, Result};
use itertools::join;
use snafu::{ensure, ResultExt};
use std::collections::HashSet;
use std::fs::{self, OpenOptions};
use std::io::prelude::*;
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use std::process::Command;
const SYSTEMCTL_DAEMON_RELOAD: &str = "systemctl daemon-reload";
const DEFAULT_FILE_MODE: u32 = 0o644;
/// Query the API for ConfigurationFile data
#[allow(clippy::implicit_hasher)]
pub async fn get_affected_config_files
(
socket_path: P,
files_limit: Option>,
) -> Result
where
P: AsRef,
{
// Only want a query parameter if we had specific affected files, otherwise we want all
let query = files_limit.map(|files| ("names", join(&files, ",")));
debug!("Querying API for configuration file metadata");
let uri = "/configuration-files";
let config_files: model::ConfigurationFiles = schnauzer::get_json(socket_path, uri, query)
.await
.context(error::GetJsonSnafu { uri })?;
Ok(config_files)
}
/// Given a map of Service objects, return a HashSet of
/// affected configuration file names
pub fn get_config_file_names(services: &Services) -> HashSet {
debug!("Building set of affected configuration file names");
let mut config_file_set = HashSet::new();
for service in services.0.values() {
for file in &service.model.configuration_files {
config_file_set.insert(file.to_string());
}
}
trace!("Config file names: {:?}", config_file_set);
config_file_set
}
/// Render the configuration files
// If strict is True, return an error if we fail to render any template.
// If strict is False, ignore failures, always returning an Ok value
// containing any successfully rendered templates.
pub fn render_config_files(
registry: &handlebars::Handlebars<'_>,
config_files: model::ConfigurationFiles,
settings: model::Model,
strict: bool,
) -> Result> {
// Go write all the configuration files from template
let mut rendered_configs = Vec::new();
for (name, metadata) in config_files {
debug!("Rendering {}", &name);
let try_rendered = registry.render(&name, &settings);
if strict {
let rendered = try_rendered.context(error::TemplateRenderSnafu { template: name })?;
rendered_configs.push(RenderedConfigFile::new(
&metadata.path,
rendered,
&metadata.mode,
));
} else {
match try_rendered {
Ok(rendered) => rendered_configs.push(RenderedConfigFile::new(
&metadata.path,
rendered,
&metadata.mode,
)),
Err(err) => warn!("Unable to render template '{}': {}", &name, err),
}
}
}
trace!("Rendered configs: {:?}", &rendered_configs);
Ok(rendered_configs)
}
/// Write all the configuration files to disk
pub fn write_config_files(rendered_configs: &[RenderedConfigFile]) -> Result<()> {
for cfg in rendered_configs {
debug!("Writing {:?}", &cfg.path);
cfg.write_to_disk()?;
}
Ok(())
}
/// Run `systemd daemon-reload` if any modified config file requires it.
pub fn reload_config_files(rendered_configs: &[RenderedConfigFile]) -> Result<()> {
if rendered_configs
.iter()
.any(RenderedConfigFile::needs_reload)
{
let mut args = SYSTEMCTL_DAEMON_RELOAD.split(' ');
let program = args.next().expect("failed to split on space");
trace!("Command: {}", &program);
trace!("Args: {:?}", &args);
let result = Command::new(program).args(args).output().context(
error::CommandExecutionFailureSnafu {
command: SYSTEMCTL_DAEMON_RELOAD,
},
)?;
// If the reload command exited nonzero, call it a failure
ensure!(
result.status.success(),
error::FailedReloadCommandSnafu {
command: SYSTEMCTL_DAEMON_RELOAD,
stderr: String::from_utf8_lossy(&result.stderr),
}
);
trace!(
"Command stdout: {}",
String::from_utf8_lossy(&result.stdout)
);
trace!(
"Command stderr: {}",
String::from_utf8_lossy(&result.stderr)
);
}
Ok(())
}
/// RenderedConfigFile contains both the path to the config file
/// and the rendered data to write.
#[derive(Debug)]
pub struct RenderedConfigFile {
path: PathBuf,
rendered: String,
mode: Option,
}
impl RenderedConfigFile {
fn new(path: &str, rendered: String, mode: &Option) -> RenderedConfigFile {
RenderedConfigFile {
path: PathBuf::from(&path),
rendered,
mode: mode.to_owned(),
}
}
/// Writes the rendered template at the proper location
fn write_to_disk(&self) -> Result<()> {
if let Some(dirname) = self.path.parent() {
fs::create_dir_all(dirname).context(error::TemplateWriteSnafu {
path: dirname,
pathtype: "directory",
})?;
};
let mut binding = OpenOptions::new();
let options = binding
.write(true)
.create(true)
.truncate(true)
.mode(DEFAULT_FILE_MODE);
// See if this file has a config setting for a specific mode
if let Some(mode) = &self.mode {
let mode_int =
u32::from_str_radix(mode.as_str(), 8).context(error::TemplateModeSnafu {
path: &self.path,
mode,
})?;
options.mode(mode_int);
}
let mut file = options
.open(&self.path)
.context(error::TemplateWriteSnafu {
path: &self.path,
pathtype: "file",
})?;
file.write_all(self.rendered.as_bytes())
.context(error::TemplateWriteSnafu {
path: &self.path,
pathtype: "file",
})
}
/// Checks whether the config file needs `systemd` to reload.
fn needs_reload(&self) -> bool {
self.path.to_string_lossy().starts_with("/etc/systemd/")
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::service::Services;
use maplit::{hashmap, hashset};
use std::convert::TryInto;
#[test]
fn test_get_config_file_names() {
let input_map = hashmap!(
"foo".to_string() => model::Service {
configuration_files: vec!["file1".try_into().unwrap()],
restart_commands: vec!["echo hi".to_string()]
},
"bar".to_string() => model::Service {
configuration_files: vec!["file1".try_into().unwrap(), "file2".try_into().unwrap()],
restart_commands: vec!["echo hi".to_string()]
},
);
let services = Services::from_model_services(input_map, None);
let expected_output = hashset! {"file1".to_string(), "file2".to_string() };
assert_eq!(get_config_file_names(&services), expected_output)
}
}