use crate::inventory as inv;
use crate::inventory::options::Options;
use crate::inventory::value::{Key, Value};
use hashlink::LinkedHashMap;
use serde::ser::{Serialize, SerializeMap, Serializer};
use snafu::prelude::*;
use std::rc::Rc;
use yaml_rust2::Yaml;
use super::YamlOutput;
use super::ansible::{self, HostVars};
#[derive(Debug, Snafu)]
pub enum TopError {
#[snafu(transparent)]
TopInventoryLoad { source: inv::Error },
}
pub struct TopData {
environments: LinkedHashMap<String, LinkedHashMap<String, Vec<String>>>,
}
struct EnvEntry<'a> {
nodes: &'a LinkedHashMap<String, Vec<String>>,
}
impl<'a> Serialize for EnvEntry<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut node_keys: Vec<&String> = self.nodes.keys().collect();
node_keys.sort();
let mut map = serializer.serialize_map(Some(node_keys.len()))?;
for key in node_keys {
map.serialize_entry(key, &self.nodes[key])?;
}
map.end()
}
}
impl Serialize for TopData {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut env_keys: Vec<&String> = self.environments.keys().collect();
env_keys.sort();
let mut map = serializer.serialize_map(Some(env_keys.len()))?;
for env in &env_keys {
let entry = EnvEntry {
nodes: &self.environments[*env],
};
map.serialize_entry(*env, &entry)?;
}
map.end()
}
}
impl YamlOutput for TopData {
fn to_yaml_value(&self, sorted: bool) -> Yaml {
let mut env_keys: Vec<&String> = self.environments.keys().collect();
if sorted {
env_keys.sort();
}
let mut map = LinkedHashMap::new();
for env in &env_keys {
let nodes = &self.environments[*env];
let mut node_keys: Vec<&String> = nodes.keys().collect();
if sorted {
node_keys.sort();
}
let mut node_map = LinkedHashMap::new();
for key in &node_keys {
node_map.insert(
Yaml::String(key.to_string()),
Yaml::Array(
nodes[*key]
.iter()
.map(|s| Yaml::String(s.clone()))
.collect(),
),
);
}
map.insert(Yaml::String(env.to_string()), Yaml::Hash(node_map));
}
Yaml::Hash(map)
}
}
#[derive(Debug, Snafu)]
pub enum PillarError {
#[snafu(transparent)]
PillarInventoryLoad { source: inv::Error },
#[snafu(display("node '{node_name}' not found"))]
NodeNotFound { node_name: String },
#[snafu(display("error merging node '{node_name}'"))]
Merge {
source: inv::Error,
node_name: String,
},
}
fn inject_salt_reclass_fields(
parameters: &mut LinkedHashMap<Key, Value>,
nodename: &str,
classes: &[String],
applications: &[String],
environment: &dyn std::fmt::Display,
) {
let reclass_key = Key::String("_reclass_".to_string());
if let Some(Value::Hash(reclass_hash)) = parameters.remove(&reclass_key) {
let mut reclass_hash = Rc::try_unwrap(reclass_hash).unwrap_or_else(|rc| (*rc).clone());
reclass_hash.insert(
Key::String("nodename".to_string()),
Value::String(nodename.to_string()),
);
reclass_hash.insert(
Key::String("classes".to_string()),
Value::Array(Rc::new(
classes.iter().map(|s| Value::String(s.clone())).collect(),
)),
);
reclass_hash.insert(
Key::String("applications".to_string()),
Value::Array(Rc::new(
applications
.iter()
.map(|s| Value::String(s.clone()))
.collect(),
)),
);
reclass_hash.insert(
Key::String("environment".to_string()),
Value::String(environment.to_string()),
);
parameters.insert(reclass_key, Value::Hash(Rc::new(reclass_hash)));
}
}
pub fn build_top(config: &Options) -> Result<TopData, TopError> {
let merge_config = config.build_merge_config();
let ignore_failed_node = merge_config.inventory_ignore_failed_node;
let ignore_failed_render = merge_config.inventory_ignore_failed_render;
let mut inventory = inv::load(&config.storage_options).map_err(TopError::from)?;
inventory.set_class_mappings(config.class_mappings.clone());
inventory.set_class_mappings_match_path(config.class_mappings_match_path);
inventory.set_merge_config(merge_config);
let inv_map = inventory.build_inventory_map().map_err(TopError::from)?;
let mut environments: LinkedHashMap<String, LinkedHashMap<String, Vec<String>>> =
LinkedHashMap::new();
for node in inventory.nodes_iter() {
let merged = match inventory.merge_node(node.name()) {
Ok(n) => n,
Err(e) => {
if ignore_failed_node {
tracing::warn!("Skipping node '{}': {}", node.name(), e);
continue;
}
return Err(TopError::TopInventoryLoad { source: e });
}
};
let has_inv_query = {
let params = merged.parameters();
params.values().any(|v| v.has_inv_query())
|| merged.exports().values().any(|v| v.has_inv_query())
};
let merged = if has_inv_query {
match inventory.merge_node_with_inventory(node.name(), &inv_map) {
Ok(n) => n,
Err(e) => {
if ignore_failed_render {
tracing::warn!(
"Skipping node '{}': inv query render failed: {}",
node.name(),
e
);
continue;
}
return Err(TopError::TopInventoryLoad { source: e });
}
}
} else {
merged
};
let env = merged.environment().to_string();
let apps: Vec<String> = merged.applications().as_list().to_vec();
environments
.entry(env)
.or_insert_with(LinkedHashMap::new)
.insert(merged.name().to_string(), apps);
}
Ok(TopData { environments })
}
pub fn build_pillar(config: &Options, minion_id: &str) -> Result<HostVars, PillarError> {
let merge_config = config.build_merge_config();
let ignore_failed_render = merge_config.inventory_ignore_failed_render;
let mut inventory = inv::load(&config.storage_options).map_err(PillarError::from)?;
inventory.set_class_mappings(config.class_mappings.clone());
inventory.set_class_mappings_match_path(config.class_mappings_match_path);
inventory.set_merge_config(merge_config);
let _node = inventory
.get_node(minion_id)
.ok_or_else(|| PillarError::NodeNotFound {
node_name: minion_id.to_string(),
})?;
let merged = inventory
.merge_node(minion_id)
.map_err(|e| PillarError::Merge {
source: e,
node_name: minion_id.to_string(),
})?;
let has_inv_query = {
let params = merged.parameters();
params.values().any(|v| v.has_inv_query())
|| merged.exports().values().any(|v| v.has_inv_query())
};
let merged = if has_inv_query {
let inv_map = inventory.build_inventory_map().map_err(PillarError::from)?;
match inventory.merge_node_with_inventory(minion_id, &inv_map) {
Ok(n) => n,
Err(e) => {
if ignore_failed_render {
tracing::warn!(
"Failed to render inv queries for node '{}': {}",
minion_id,
e
);
}
return Err(PillarError::Merge {
source: e,
node_name: minion_id.to_string(),
});
}
}
} else {
merged
};
let mut parameters = LinkedHashMap::new();
for (k, v) in merged.parameters() {
parameters.insert(k.clone(), v.clone());
}
ansible::inject_classes_and_applications_into_reclass(
&mut parameters,
merged.classes(),
merged.applications().as_list(),
);
inject_salt_reclass_fields(
&mut parameters,
minion_id,
merged.classes(),
merged.applications().as_list(),
merged.environment(),
);
Ok(HostVars { parameters })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_inject_salt_reclass_fields() {
let mut params = LinkedHashMap::new();
let mut reclass_inner = LinkedHashMap::new();
reclass_inner.insert(Key::from("name"), Value::String("test".to_string()));
params.insert(Key::from("_reclass_"), Value::Hash(Rc::new(reclass_inner)));
params.insert(Key::from("hostname"), Value::String("web-01".to_string()));
inject_salt_reclass_fields(
&mut params,
"web-01",
&["all".to_string(), "env.prod".to_string()],
&["web".to_string()],
&"production",
);
let reclass = params.get(&Key::from("_reclass_")).unwrap();
if let Value::Hash(h) = reclass {
let h = Rc::try_unwrap(h.clone()).unwrap_or_else(|rc| (*rc).clone());
assert!(h.contains_key(&Key::from("nodename")));
assert!(h.contains_key(&Key::from("classes")));
assert!(h.contains_key(&Key::from("applications")));
assert!(h.contains_key(&Key::from("environment")));
let nodename = h.get(&Key::from("nodename")).unwrap();
assert_eq!(nodename, &Value::String("web-01".to_string()));
let env = h.get(&Key::from("environment")).unwrap();
assert_eq!(env, &Value::String("production".to_string()));
} else {
panic!("expected Hash");
}
}
#[test]
fn test_inject_salt_reclass_fields_no_reclass_key() {
let mut params = LinkedHashMap::new();
params.insert(Key::from("hostname"), Value::String("web-01".to_string()));
inject_salt_reclass_fields(
&mut params,
"web-01",
&["all".to_string()],
&["web".to_string()],
&"base",
);
assert!(!params.contains_key(&Key::from("_reclass_")));
}
}