├── testdata ├── empty.json ├── only_failed_hosts2.json ├── number_as_ret.json ├── no_ret_array.json ├── string.json ├── only_failed_hosts.json ├── array.json ├── array_weird.json ├── bool.json ├── duplicate_keys_hosts.json ├── command_weird.json ├── highstate_is_already_running.json ├── command.json └── old_new_values_in_ret.json ├── .gitignore ├── .travis.yml ├── misc └── csh_alias ├── .rustfmt.toml ├── Cargo.toml ├── LICENSE ├── README.md ├── src ├── cli.yml ├── tests.rs └── main.rs └── Cargo.lock /testdata/empty.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.bk 3 | -------------------------------------------------------------------------------- /testdata/only_failed_hosts2.json: -------------------------------------------------------------------------------- 1 | {} 2 | ERROR: No return received 3 | -------------------------------------------------------------------------------- /testdata/number_as_ret.json: -------------------------------------------------------------------------------- 1 | { 2 | "null": { 3 | "ret": 0 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /testdata/no_ret_array.json: -------------------------------------------------------------------------------- 1 | { 2 | "minion": [ 3 | "line1", 4 | "line2", 5 | "line3" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /testdata/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "minion": { 3 | "retcode": 254, 4 | "ret": "'test' __virtual__ returned False" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | matrix: 7 | allow_failures: 8 | - rust: nightly 9 | -------------------------------------------------------------------------------- /testdata/only_failed_hosts.json: -------------------------------------------------------------------------------- 1 | Minion minion_fail_1 did not respond. No job will be sent. 2 | Minion minion_fail_2 did not respond. No job will be sent. 3 | {} 4 | -------------------------------------------------------------------------------- /testdata/array.json: -------------------------------------------------------------------------------- 1 | { 2 | "minion": { 3 | "retcode": 1, 4 | "ret": [ 5 | "line1", 6 | "line2", 7 | "line3" 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /testdata/array_weird.json: -------------------------------------------------------------------------------- 1 | { 2 | "minion": { 3 | "retcode": 1, 4 | "ret": [ 5 | 1, 6 | 2, 7 | 3, 8 | 4 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /testdata/bool.json: -------------------------------------------------------------------------------- 1 | { 2 | "minion": { 3 | "retcode": 0, 4 | "ret": true 5 | }, 6 | "minion_fail": { 7 | "retcode": 1, 8 | "ret": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /testdata/duplicate_keys_hosts.json: -------------------------------------------------------------------------------- 1 | minion minion_fail_1 was already deleted from tracker, probably a duplicate key 2 | minion minion_fail_2 was already deleted from tracker, probably a duplicate key 3 | {} 4 | -------------------------------------------------------------------------------- /misc/csh_alias: -------------------------------------------------------------------------------- 1 | alias pretty_salt 'salt \!* -b 20 --static --out json | /usr/local/bin/salt-compressor --input -' 2 | alias pretty_salt_onlychanged 'salt \!* -b 20 --static --out json | /usr/local/bin/salt-compressor --input - --changed' 3 | -------------------------------------------------------------------------------- /testdata/command_weird.json: -------------------------------------------------------------------------------- 1 | { 2 | "minion": { 3 | "retcode": 0, 4 | "ret": { 5 | "command_with_weird_diff": { 6 | "comment": "comment", 7 | "changes": { 8 | "diff": 0 9 | } 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | condense_wildcard_suffixes = true 2 | format_strings = true 3 | imports_indent = "Block" 4 | imports_layout = "Vertical" 5 | merge_imports = true 6 | normalize_comments = true 7 | reorder_imports = true 8 | use_try_shorthand = true 9 | wrap_comments = true 10 | -------------------------------------------------------------------------------- /testdata/highstate_is_already_running.json: -------------------------------------------------------------------------------- 1 | { 2 | "loadbalancer_datacenter_dca": [ 3 | "The function \"state.highstate\" is running as PID 19760 and was started at 2017, Jul 19 11:29:04.786325 with jid 20170719112904786325" 4 | ], 5 | "loadbalancer_datacenter_hkg": [ 6 | "The function \"state.highstate\" is running as PID 19759 and was started at 2017, Jul 19 11:29:04.786325 with jid 20170719112904786325" 7 | ], 8 | "loadbalancer_datacenter_sfo": [ 9 | "The function \"state.highstate\" is running as PID 19757 and was started at 2017, Jul 19 11:29:04.786325 with jid 20170719112904786325" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /testdata/command.json: -------------------------------------------------------------------------------- 1 | { 2 | "minion": { 3 | "retcode": 0, 4 | "ret": { 5 | "command_with_changes": { 6 | "comment": "comment", 7 | "changes": { 8 | "diff": "--- \n+++ \n@@ -0,0 +0,10 @@\n+ added\n- removed" 9 | } 10 | }, 11 | "command_with_changes2": { 12 | "comment": "comment", 13 | "changes": { 14 | "diff": "this is another change" 15 | } 16 | }, 17 | "command_without_changes": { 18 | "comment": "comment", 19 | "changes": {} 20 | }, 21 | "command_with_no_comment": { 22 | "changes": {} 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /testdata/old_new_values_in_ret.json: -------------------------------------------------------------------------------- 1 | { 2 | "minion1": { 3 | "retcode": 0, 4 | "ret": { 5 | "package": { 6 | "new": "", 7 | "old": "version1" 8 | } 9 | } 10 | }, 11 | "minion2": { 12 | "retcode": 0, 13 | "ret": { 14 | "package": { 15 | "new": "", 16 | "old": "version1" 17 | } 18 | } 19 | }, 20 | "minion3": { 21 | "retcode": 0, 22 | "ret": { 23 | "package": { 24 | "new": "", 25 | "old": "version2" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "salt-compressor" 3 | version = "0.4.2" 4 | authors = ["Alexander Thaller "] 5 | description = "Compress the output of saltstack-runs to make it easier to review changes that happen to a lot of servers." 6 | homepage = "https://github.com/AlexanderThaller/salt-compressor" 7 | repository = "https://github.com/AlexanderThaller/salt-compressor" 8 | readme = "README.md" 9 | keywords = ["saltstack", "ops", "opsworks", "automation", "tool"] 10 | license = "MIT" 11 | edition = "2018" 12 | 13 | [badges] 14 | travis-ci = { repository = "AlexanderThaller/salt-compressor", branch = "master" } 15 | 16 | [dependencies] 17 | colored = "1" 18 | log = "0.4" 19 | loggerv = "0.7" 20 | regex = "1" 21 | serde_json = "1" 22 | chrono = "0.4" 23 | 24 | [dependencies.clap] 25 | version = "2" 26 | features = ["yaml"] 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alexander Thaller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # salt-compressor 2 | Compress the output of saltstack-runs to make it easier to review changes that happen to a lot of servers. 3 | 4 | [![TravisCI Build Status](https://travis-ci.org/AlexanderThaller/salt-compressor.svg?branch=master)](https://travis-ci.org/AlexanderThaller/salt-compressor) 5 | 6 | # Usage 7 | ``` 8 | salt-compressor 0.4.0 9 | Alexander Thaller 10 | Compress the output of saltstack-runs to make it easier to review changes that happen to a lot of servers. 11 | 12 | USAGE: 13 | salt-compressor [FLAGS] [OPTIONS] --input 14 | 15 | FLAGS: 16 | -F, --filter_failed 17 | Only print states that failed 18 | 19 | -S, --filter_succeeded 20 | Only print states that succeeded 21 | 22 | -U, --filter_unchanged 23 | Only print states that have outputs 24 | 25 | -h, --help 26 | Prints help information 27 | 28 | -n, --no_save_file 29 | Do not write save file on error 30 | 31 | -V, --version 32 | Prints version information 33 | 34 | 35 | OPTIONS: 36 | -C, --filter_command 37 | Only print states that have commands that match the given regex [default: .*] 38 | 39 | -O, --filter_output 40 | Only print states that have outputs that match the given regex [default: .*] 41 | 42 | -R, --filter_result 43 | Only print states that have results that match the given regex [default: .*] 44 | 45 | -i, --input 46 | Path to the input file. If input is '-' read from stdin 47 | 48 | -l, --loglevel 49 | Loglevel to run under [default: info] [values: trace, debug, info, warn, error] 50 | ``` 51 | 52 | # Example 53 | ``` 54 | salt '*' state.highstate -b 10 --static --out json test=true | salt-compressor -i - 55 | ``` 56 | 57 | The `--static` and `--json` flags are important. `static` will output a much 58 | easier to parse format. `json` will of course output everything in the JSON 59 | format. 60 | -------------------------------------------------------------------------------- /src/cli.yml: -------------------------------------------------------------------------------- 1 | name: "salt-compressor" 2 | author: "Alexander Thaller " 3 | about: "Compress the output of saltstack-runs to make it easier to review changes that happen to a lot of servers." 4 | global_settings: 5 | - "ColoredHelp" 6 | - "GlobalVersion" 7 | - "NextLineHelp" 8 | - "VersionlessSubcommands" 9 | args: 10 | - loglevel: 11 | help: "Loglevel to run under" 12 | long: "loglevel" 13 | short: "l" 14 | takes_value: true 15 | default_value: "info" 16 | value_name: "level" 17 | possible_values: [ "trace", "debug", "info", "warn", "error" ] 18 | global: true 19 | - input: 20 | help: "Path to the input file. If input is '-' read from stdin" 21 | long: "input" 22 | short: "i" 23 | takes_value: true 24 | value_name: "path" 25 | required: true 26 | - no_save_file: 27 | help: "Do not write save file on error" 28 | long: "no_save_file" 29 | short: "n" 30 | - filter_unchanged: 31 | help: "Only print states that have outputs" 32 | long: "filter_unchanged" 33 | short: "U" 34 | - filter_command: 35 | help: "Only print states that have commands that match the given regex" 36 | long: "filter_command" 37 | short: "C" 38 | takes_value: true 39 | default_value: ".*" 40 | value_name: "regex" 41 | - filter_result: 42 | help: "Only print states that have results that match the given regex" 43 | long: "filter_result" 44 | short: "R" 45 | takes_value: true 46 | default_value: ".*" 47 | value_name: "regex" 48 | - filter_output: 49 | help: "Only print states that have outputs that match the given regex" 50 | long: "filter_output" 51 | short: "O" 52 | takes_value: true 53 | default_value: ".*" 54 | value_name: "regex" 55 | - filter_failed: 56 | help: "Only print states that failed" 57 | long: "filter_failed" 58 | short: "F" 59 | - filter_succeeded: 60 | help: "Only print states that succeeded" 61 | long: "filter_succeeded" 62 | short: "S" 63 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | mod test_retcode { 2 | use crate::Retcode; 3 | 4 | #[test] 5 | fn from_success() { 6 | assert_eq!(Retcode::Success, 0.into()) 7 | } 8 | 9 | #[test] 10 | fn from_failure() { 11 | for i in 1..10 { 12 | assert_eq!(Retcode::Failure, i.into()) 13 | } 14 | } 15 | } 16 | 17 | mod test_get_results { 18 | use crate::{ 19 | cleanup_input_data, 20 | get_results, 21 | MinionResult, 22 | Retcode, 23 | }; 24 | use log::trace; 25 | use serde_json::Value; 26 | use std::collections::BTreeMap as DataMap; 27 | 28 | #[test] 29 | #[should_panic(expected = "value it not an object")] 30 | fn value_not_an_object() { 31 | let value = Value::default(); 32 | 33 | match get_results(&value, DataMap::default()) { 34 | Ok(_) => {} 35 | Err(e) => panic!(format!("{}", e)), 36 | } 37 | } 38 | 39 | #[test] 40 | fn empty_results() { 41 | let value: Value = serde_json::from_str("{}").unwrap(); 42 | 43 | let got = match get_results(&value, DataMap::default()) { 44 | Ok(r) => r, 45 | Err(e) => panic!("unexpected error: {}", e), 46 | }; 47 | let expected = Vec::new(); 48 | 49 | trace!("got: {:#?}", got); 50 | trace!("expected: {:#?}", expected); 51 | 52 | assert_eq!(got, expected); 53 | } 54 | 55 | #[test] 56 | fn only_failed_hosts() { 57 | let input = include_str!("../testdata/only_failed_hosts.json"); 58 | let (input, _) = cleanup_input_data(input); 59 | 60 | let value: Value = 61 | serde_json::from_str(input.as_str()).expect("can not parse input to json"); 62 | 63 | let mut failed_hosts = DataMap::default(); 64 | failed_hosts.insert("minion_fail_1".into(), ""); 65 | failed_hosts.insert("minion_fail_1".into(), ""); 66 | 67 | let got = match get_results(&value, failed_hosts.clone()) { 68 | Ok(r) => r, 69 | Err(e) => panic!("unexpected error: {}", e), 70 | }; 71 | let mut expected = Vec::new(); 72 | for (host, message) in failed_hosts { 73 | expected.push(MinionResult { 74 | host: host, 75 | retcode: Retcode::Failure, 76 | output: Some(message.into()), 77 | ..MinionResult::default() 78 | }); 79 | } 80 | 81 | println!("got: {:#?}", got); 82 | println!("expected: {:#?}", expected); 83 | 84 | assert_eq!(got, expected); 85 | } 86 | 87 | #[test] 88 | fn duplicate_keys_hosts() { 89 | let input = include_str!("../testdata/duplicate_keys_hosts.json"); 90 | let (input, _) = cleanup_input_data(input); 91 | 92 | let value: Value = 93 | serde_json::from_str(input.as_str()).expect("can not parse input to json"); 94 | 95 | let mut failed_hosts = DataMap::default(); 96 | failed_hosts.insert("minion_fail_1".into(), ""); 97 | failed_hosts.insert("minion_fail_2".into(), ""); 98 | 99 | let got = match get_results(&value, failed_hosts.clone()) { 100 | Ok(r) => r, 101 | Err(e) => panic!("unexpected error: {}", e), 102 | }; 103 | 104 | let mut expected = Vec::new(); 105 | for (host, message) in failed_hosts { 106 | expected.push(MinionResult { 107 | host: host, 108 | retcode: Retcode::Failure, 109 | output: Some(message.to_string()), 110 | ..MinionResult::default() 111 | }); 112 | } 113 | 114 | println!("got: {:#?}", got); 115 | println!("expected: {:#?}", expected); 116 | 117 | assert_eq!(got, expected); 118 | } 119 | 120 | #[test] 121 | fn array() { 122 | let input = include_str!("../testdata/array.json"); 123 | let value: Value = serde_json::from_str(input).unwrap(); 124 | 125 | let got = match get_results(&value, DataMap::default()) { 126 | Ok(r) => r, 127 | Err(e) => panic!("unexpected error: {}", e), 128 | }; 129 | 130 | let mut expected = Vec::new(); 131 | expected.push(MinionResult { 132 | host: "minion".to_string(), 133 | retcode: Retcode::Failure, 134 | result: Some("line1\nline2\nline3".to_string()), 135 | ..MinionResult::default() 136 | }); 137 | 138 | trace!("got: {:#?}", got); 139 | trace!("expected: {:#?}", expected); 140 | 141 | assert_eq!(got, expected); 142 | } 143 | 144 | #[test] 145 | #[should_panic(expected = "can not convert the array value to a string")] 146 | fn array_weird() { 147 | let input = include_str!("../testdata/array_weird.json"); 148 | let value: Value = serde_json::from_str(input).unwrap(); 149 | 150 | match get_results(&value, DataMap::default()) { 151 | Ok(_) => {} 152 | Err(e) => panic!(e), 153 | }; 154 | } 155 | 156 | #[test] 157 | fn bool() { 158 | let input = include_str!("../testdata/bool.json"); 159 | let value: Value = serde_json::from_str(input).unwrap(); 160 | 161 | let got = match get_results(&value, DataMap::default()) { 162 | Ok(r) => r, 163 | Err(e) => panic!("unexpected error: {}", e), 164 | }; 165 | 166 | let mut expected = Vec::new(); 167 | expected.push(MinionResult { 168 | host: "minion".to_string(), 169 | retcode: Retcode::Success, 170 | result: Some("true".to_string()), 171 | ..MinionResult::default() 172 | }); 173 | expected.push(MinionResult { 174 | host: "minion_fail".to_string(), 175 | retcode: Retcode::Failure, 176 | result: Some("false".to_string()), 177 | ..MinionResult::default() 178 | }); 179 | expected.sort(); 180 | 181 | trace!("got: {:#?}", got); 182 | trace!("expected: {:#?}", expected); 183 | 184 | assert_eq!(got, expected); 185 | } 186 | 187 | #[test] 188 | fn not_ret_array() { 189 | let input = include_str!("../testdata/no_ret_array.json"); 190 | let value: Value = serde_json::from_str(input).unwrap(); 191 | 192 | let got = match get_results(&value, DataMap::default()) { 193 | Ok(r) => r, 194 | Err(e) => panic!("unexpected error: {}", e), 195 | }; 196 | 197 | let mut expected = Vec::new(); 198 | expected.push(MinionResult { 199 | host: "minion".to_string(), 200 | retcode: Retcode::Failure, 201 | result: Some("line1\nline2\nline3".to_string()), 202 | ..MinionResult::default() 203 | }); 204 | 205 | trace!("got: {:#?}", got); 206 | trace!("expected: {:#?}", expected); 207 | 208 | assert_eq!(got, expected); 209 | } 210 | 211 | #[test] 212 | fn old_new_values_in_ret() { 213 | let input = include_str!("../testdata/old_new_values_in_ret.json"); 214 | let value: Value = serde_json::from_str(input).unwrap(); 215 | 216 | let got = match get_results(&value, DataMap::default()) { 217 | Ok(r) => r, 218 | Err(e) => panic!("unexpected error: {}", e), 219 | }; 220 | 221 | let mut expected = Vec::new(); 222 | expected.push(MinionResult { 223 | host: "minion1".to_string(), 224 | retcode: Retcode::Success, 225 | output: Some("Old: version1\nNew: \n".to_string()), 226 | command: Some("package".into()), 227 | ..MinionResult::default() 228 | }); 229 | expected.push(MinionResult { 230 | host: "minion2".to_string(), 231 | retcode: Retcode::Success, 232 | output: Some("Old: version1\nNew: \n".to_string()), 233 | command: Some("package".into()), 234 | ..MinionResult::default() 235 | }); 236 | expected.push(MinionResult { 237 | host: "minion3".to_string(), 238 | retcode: Retcode::Success, 239 | output: Some("Old: version2\nNew: \n".to_string()), 240 | command: Some("package".into()), 241 | ..MinionResult::default() 242 | }); 243 | 244 | trace!("got: {:#?}", got); 245 | trace!("expected: {:#?}", expected); 246 | 247 | assert_eq!(got, expected); 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "aho-corasick" 5 | version = "0.7.10" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | dependencies = [ 8 | "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 9 | ] 10 | 11 | [[package]] 12 | name = "ansi_term" 13 | version = "0.11.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | dependencies = [ 16 | "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", 17 | ] 18 | 19 | [[package]] 20 | name = "ansi_term" 21 | version = "0.12.1" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | dependencies = [ 24 | "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", 25 | ] 26 | 27 | [[package]] 28 | name = "atty" 29 | version = "0.2.14" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | dependencies = [ 32 | "hermit-abi 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 33 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 34 | "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", 35 | ] 36 | 37 | [[package]] 38 | name = "autocfg" 39 | version = "1.0.0" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | 42 | [[package]] 43 | name = "bitflags" 44 | version = "1.2.1" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | 47 | [[package]] 48 | name = "cfg-if" 49 | version = "0.1.10" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | 52 | [[package]] 53 | name = "chrono" 54 | version = "0.4.11" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | dependencies = [ 57 | "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", 58 | "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 59 | "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", 60 | ] 61 | 62 | [[package]] 63 | name = "clap" 64 | version = "2.33.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | dependencies = [ 67 | "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", 68 | "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", 69 | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 70 | "strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", 71 | "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", 72 | "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 73 | "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 74 | "yaml-rust 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 75 | ] 76 | 77 | [[package]] 78 | name = "colored" 79 | version = "1.9.3" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | dependencies = [ 82 | "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", 83 | "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 84 | "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", 85 | ] 86 | 87 | [[package]] 88 | name = "hermit-abi" 89 | version = "0.1.10" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | dependencies = [ 92 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 93 | ] 94 | 95 | [[package]] 96 | name = "itoa" 97 | version = "0.4.5" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | 100 | [[package]] 101 | name = "lazy_static" 102 | version = "1.4.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | 105 | [[package]] 106 | name = "libc" 107 | version = "0.2.68" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | 110 | [[package]] 111 | name = "log" 112 | version = "0.4.8" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | dependencies = [ 115 | "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 116 | ] 117 | 118 | [[package]] 119 | name = "loggerv" 120 | version = "0.7.2" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | dependencies = [ 123 | "ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", 124 | "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", 125 | "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", 126 | ] 127 | 128 | [[package]] 129 | name = "memchr" 130 | version = "2.3.3" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | 133 | [[package]] 134 | name = "num-integer" 135 | version = "0.1.42" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | dependencies = [ 138 | "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 139 | "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 140 | ] 141 | 142 | [[package]] 143 | name = "num-traits" 144 | version = "0.2.11" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | dependencies = [ 147 | "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 148 | ] 149 | 150 | [[package]] 151 | name = "redox_syscall" 152 | version = "0.1.56" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | 155 | [[package]] 156 | name = "regex" 157 | version = "1.3.6" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | dependencies = [ 160 | "aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)", 161 | "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 162 | "regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)", 163 | "thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 164 | ] 165 | 166 | [[package]] 167 | name = "regex-syntax" 168 | version = "0.6.17" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | 171 | [[package]] 172 | name = "ryu" 173 | version = "1.0.3" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | 176 | [[package]] 177 | name = "salt-compressor" 178 | version = "0.4.2" 179 | dependencies = [ 180 | "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", 181 | "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", 182 | "colored 1.9.3 (registry+https://github.com/rust-lang/crates.io-index)", 183 | "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", 184 | "loggerv 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", 185 | "regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 186 | "serde_json 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", 187 | ] 188 | 189 | [[package]] 190 | name = "serde" 191 | version = "1.0.105" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | 194 | [[package]] 195 | name = "serde_json" 196 | version = "1.0.50" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | dependencies = [ 199 | "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", 200 | "ryu 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 201 | "serde 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)", 202 | ] 203 | 204 | [[package]] 205 | name = "strsim" 206 | version = "0.8.0" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | 209 | [[package]] 210 | name = "textwrap" 211 | version = "0.11.0" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | dependencies = [ 214 | "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 215 | ] 216 | 217 | [[package]] 218 | name = "thread_local" 219 | version = "1.0.1" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | dependencies = [ 222 | "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 223 | ] 224 | 225 | [[package]] 226 | name = "time" 227 | version = "0.1.42" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | dependencies = [ 230 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 231 | "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", 232 | "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", 233 | ] 234 | 235 | [[package]] 236 | name = "unicode-width" 237 | version = "0.1.7" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | 240 | [[package]] 241 | name = "vec_map" 242 | version = "0.8.1" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | 245 | [[package]] 246 | name = "winapi" 247 | version = "0.3.8" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | dependencies = [ 250 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 251 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 252 | ] 253 | 254 | [[package]] 255 | name = "winapi-i686-pc-windows-gnu" 256 | version = "0.4.0" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | 259 | [[package]] 260 | name = "winapi-x86_64-pc-windows-gnu" 261 | version = "0.4.0" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | 264 | [[package]] 265 | name = "yaml-rust" 266 | version = "0.3.5" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | 269 | [metadata] 270 | "checksum aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" 271 | "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 272 | "checksum ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 273 | "checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 274 | "checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 275 | "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 276 | "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 277 | "checksum chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" 278 | "checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" 279 | "checksum colored 1.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" 280 | "checksum hermit-abi 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "725cf19794cf90aa94e65050cb4191ff5d8fa87a498383774c47b332e3af952e" 281 | "checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" 282 | "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 283 | "checksum libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)" = "dea0c0405123bba743ee3f91f49b1c7cfb684eef0da0a50110f758ccf24cdff0" 284 | "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 285 | "checksum loggerv 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "60d8de15ae71e760bce7f05447f85f73624fe0d3b1e4c5a63ba5d4cb0748d374" 286 | "checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 287 | "checksum num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" 288 | "checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" 289 | "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" 290 | "checksum regex 1.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7f6946991529684867e47d86474e3a6d0c0ab9b82d5821e314b1ede31fa3a4b3" 291 | "checksum regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" 292 | "checksum ryu 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76" 293 | "checksum serde 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)" = "e707fbbf255b8fc8c3b99abb91e7257a622caeb20a9818cbadbeeede4e0932ff" 294 | "checksum serde_json 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)" = "78a7a12c167809363ec3bd7329fc0a3369056996de43c4b37ef3cd54a6ce4867" 295 | "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 296 | "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 297 | "checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 298 | "checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" 299 | "checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" 300 | "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" 301 | "checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 302 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 303 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 304 | "checksum yaml-rust 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e66366e18dc58b46801afbf2ca7661a9f59cc8c5962c29892b6039b4f86fa992" 305 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{ 2 | crate_version, 3 | load_yaml, 4 | value_t, 5 | App, 6 | }; 7 | use colored::*; 8 | use log::{ 9 | error, 10 | info, 11 | trace, 12 | warn, 13 | Level, 14 | }; 15 | use regex::Regex; 16 | use serde_json::Value; 17 | use std::{ 18 | collections::{ 19 | BTreeMap as DataMap, 20 | BTreeSet as DataSet, 21 | }, 22 | fmt, 23 | fs::File, 24 | io::{ 25 | self, 26 | Read, 27 | Write, 28 | }, 29 | process, 30 | }; 31 | 32 | #[cfg(test)] 33 | mod tests; 34 | 35 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default)] 36 | struct MinionResult { 37 | command: Option, 38 | retcode: Retcode, 39 | output: Option, 40 | result: Option, 41 | host: String, 42 | } 43 | 44 | type MinionResults = Vec; 45 | 46 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] 47 | enum Retcode { 48 | Success, 49 | Failure, 50 | } 51 | 52 | impl Retcode { 53 | fn is_success(&self) -> bool { 54 | self == &Retcode::Success 55 | } 56 | } 57 | 58 | impl Default for Retcode { 59 | fn default() -> Retcode { 60 | Retcode::Failure 61 | } 62 | } 63 | 64 | impl From for Retcode { 65 | fn from(input: u64) -> Self { 66 | match input { 67 | 0 => Retcode::Success, 68 | _ => Retcode::Failure, 69 | } 70 | } 71 | } 72 | 73 | #[derive(Debug)] 74 | struct Filter { 75 | command: Regex, 76 | failed: bool, 77 | output: Regex, 78 | result: Regex, 79 | succeeded: bool, 80 | unchanged: bool, 81 | } 82 | 83 | fn main() { 84 | let yaml = load_yaml!("cli.yml"); 85 | let matches = App::from_yaml(yaml).version(crate_version!()).get_matches(); 86 | trace!("matches: {:?}", matches); 87 | 88 | { 89 | let loglevel: Level = 90 | value_t!(matches, "loglevel", Level).expect("can not parse loglevel from args"); 91 | loggerv::init_with_level(loglevel).expect("can not initialize logger with parsed loglevel"); 92 | } 93 | 94 | let no_save_file = matches.is_present("no_save_file"); 95 | 96 | let filter_failed = matches.is_present("filter_failed"); 97 | let filter_succeeded = matches.is_present("filter_succeeded"); 98 | let filter_unchanged = matches.is_present("filter_unchanged"); 99 | let filter_command = value_t!(matches, "filter_command", Regex) 100 | .expect("can not parse regex from filter_command"); 101 | let filter_result = 102 | value_t!(matches, "filter_result", Regex).expect("can not parse regex from filter_result"); 103 | let filter_output = 104 | value_t!(matches, "filter_output", Regex).expect("can not parse regex from filter_output"); 105 | 106 | let filter = Filter { 107 | command: filter_command, 108 | failed: filter_failed, 109 | output: filter_output, 110 | result: filter_result, 111 | succeeded: filter_succeeded, 112 | unchanged: filter_unchanged, 113 | }; 114 | 115 | trace!("filter: {:#?}", filter); 116 | 117 | let input_data = { 118 | let input = matches 119 | .value_of("input") 120 | .expect("can not get input file from args"); 121 | 122 | match input { 123 | "-" => { 124 | let mut buffer = String::new(); 125 | io::stdin() 126 | .read_to_string(&mut buffer) 127 | .expect("can not read from stdin"); 128 | buffer 129 | } 130 | _ => std::fs::read_to_string(input).expect("can not read from input file"), 131 | } 132 | }; 133 | 134 | let (host_data, failed_minions) = cleanup_input_data(input_data.as_str()); 135 | 136 | trace!("input: {}", host_data); 137 | 138 | let value: Value = match serde_json::from_str(host_data.as_str()) { 139 | Ok(v) => v, 140 | Err(e) => { 141 | error!( 142 | "can not convert input data to value: {}\nhave you run the salt command with \ 143 | --static?", 144 | e 145 | ); 146 | if !no_save_file { 147 | write_save_file(host_data.as_str()); 148 | } 149 | process::exit(1) 150 | } 151 | }; 152 | 153 | trace!("value: {}", value); 154 | 155 | let results = match get_results(&value, failed_minions) { 156 | Ok(r) => r, 157 | Err(e) => { 158 | error!("can not get results from serde value: {}", e); 159 | if !no_save_file { 160 | write_save_file(host_data.as_str()); 161 | } 162 | process::exit(1) 163 | } 164 | }; 165 | 166 | trace!("results: {:#?}", results); 167 | 168 | let compressed = get_compressed(results); 169 | trace!("compressed: {:#?}", compressed); 170 | 171 | print_compressed(compressed, &filter); 172 | } 173 | 174 | #[derive(Debug)] 175 | enum ResultError { 176 | ConvertDiffToString, 177 | ConvertValueToString, 178 | ReturnCodeNotNumber, 179 | RetValueIsNull, 180 | RetValueIsNumber, 181 | ValueNotAnObject, 182 | OldIsNotAString, 183 | NewIsNotAString, 184 | } 185 | 186 | impl fmt::Display for ResultError { 187 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 188 | match *self { 189 | ResultError::ConvertDiffToString => write!(f, "can not convert diff to string"), 190 | ResultError::ConvertValueToString => write!(f, "can not convert value to string"), 191 | ResultError::ReturnCodeNotNumber => write!(f, "returncode is not a number"), 192 | ResultError::RetValueIsNull => write!(f, "ret value is null"), 193 | ResultError::RetValueIsNumber => write!(f, "ret value is number"), 194 | ResultError::ValueNotAnObject => write!(f, "value it not an object"), 195 | ResultError::OldIsNotAString => write!(f, "old is not a string"), 196 | ResultError::NewIsNotAString => write!(f, "new is not a string"), 197 | } 198 | } 199 | } 200 | 201 | fn get_results( 202 | value: &Value, 203 | failed_minions: DataMap, 204 | ) -> Result { 205 | if !value.is_object() { 206 | return Err(ResultError::ValueNotAnObject); 207 | } 208 | 209 | let mut results: MinionResults = Vec::new(); 210 | 211 | for (host, values) in value.as_object().unwrap().iter() { 212 | trace!("host: {:#?}", host); 213 | trace!("values: {:#?}", values); 214 | 215 | let retcode: Retcode = match values.get("retcode") { 216 | Some(o) => match o.as_u64() { 217 | Some(v) => v.into(), 218 | None => return Err(ResultError::ReturnCodeNotNumber), 219 | }, 220 | None => { 221 | warn!("host {} does not have a return code", host); 222 | Retcode::Failure 223 | } 224 | }; 225 | 226 | let ret = match values.get("ret") { 227 | Some(r) => r, 228 | None => values, 229 | }; 230 | 231 | match *ret { 232 | Value::Null => return Err(ResultError::RetValueIsNull), 233 | Value::Bool(r) => { 234 | let result = Some(r.to_string()); 235 | 236 | results.push(MinionResult { 237 | host: host.clone(), 238 | result, 239 | retcode, 240 | ..MinionResult::default() 241 | }); 242 | } 243 | Value::Number(_) => return Err(ResultError::RetValueIsNumber), 244 | Value::String(ref r) => { 245 | results.push(MinionResult { 246 | host: host.clone(), 247 | result: Some(r.clone()), 248 | retcode, 249 | ..MinionResult::default() 250 | }); 251 | } 252 | 253 | Value::Array(ref r) => { 254 | let values: Vec<_> = r 255 | .iter() 256 | .map(|v| { 257 | v.as_str() 258 | .expect("can not convert the array value to a string") 259 | }) 260 | .collect(); 261 | 262 | results.push(MinionResult { 263 | host: host.clone(), 264 | result: Some(values.join("\n").to_string()), 265 | retcode, 266 | ..MinionResult::default() 267 | }); 268 | } 269 | Value::Object(ref r) => { 270 | if r.is_empty() { 271 | results.push(MinionResult { 272 | host: host.clone(), 273 | retcode: retcode.clone(), 274 | ..MinionResult::default() 275 | }); 276 | } 277 | 278 | for (command, command_result) in r.iter() { 279 | trace!("command: {:#?}", command); 280 | trace!("command_result: {:#?}", command_result); 281 | 282 | let result = match command_result.get("comment") { 283 | Some(r) => match r.as_str() { 284 | Some(s) => Some(s.to_string()), 285 | None => return Err(ResultError::ConvertValueToString), 286 | }, 287 | None => None, 288 | }; 289 | 290 | let old = match command_result.get("old") { 291 | Some(r) => match r.as_str() { 292 | Some(s) => Some(s.to_string()), 293 | None => return Err(ResultError::OldIsNotAString), 294 | }, 295 | None => None, 296 | }; 297 | 298 | let new = match command_result.get("new") { 299 | Some(r) => match r.as_str() { 300 | Some(s) => Some(s.to_string()), 301 | None => return Err(ResultError::NewIsNotAString), 302 | }, 303 | None => None, 304 | }; 305 | 306 | let output = match command_result.get("changes") { 307 | Some(r) => match r.get("diff") { 308 | Some(d) => match d.as_str() { 309 | Some(i) => Some(i.to_string()), 310 | None => return Err(ResultError::ConvertDiffToString), 311 | }, 312 | None => None, 313 | }, 314 | None => None, 315 | }; 316 | 317 | let output = if old.is_some() { 318 | match output { 319 | Some(mut s) => { 320 | s.push_str(format!("Old: {}\n", old.unwrap()).as_str()); 321 | Some(s) 322 | } 323 | None => Some(format!("Old: {}\n", old.unwrap())), 324 | } 325 | } else { 326 | output 327 | }; 328 | 329 | let output = if new.is_some() { 330 | match output { 331 | Some(mut s) => { 332 | s.push_str(format!("New: {}\n", new.unwrap()).as_str()); 333 | Some(s) 334 | } 335 | None => Some(format!("New: {}\n", new.unwrap())), 336 | } 337 | } else { 338 | output 339 | }; 340 | 341 | results.push(MinionResult { 342 | command: Some(command.to_string()), 343 | host: host.clone(), 344 | output, 345 | result, 346 | retcode: retcode.clone(), 347 | }); 348 | } 349 | } 350 | }; 351 | } 352 | 353 | for (host, message) in failed_minions { 354 | results.push(MinionResult { 355 | host, 356 | retcode: Retcode::Failure, 357 | output: Some(message.into()), 358 | ..MinionResult::default() 359 | }); 360 | } 361 | 362 | Ok(results) 363 | } 364 | 365 | fn get_compressed(results: MinionResults) -> DataMap> { 366 | // compress output by changeing the hostname to the same value for all results 367 | // and then just 368 | // adding all hosts with that value to the map. 369 | let mut compressed: DataMap> = DataMap::new(); 370 | for result in results { 371 | let mut result_no_host = result.clone(); 372 | result_no_host.host = String::new(); 373 | 374 | compressed 375 | .entry(result_no_host) 376 | .or_insert_with(Vec::new) 377 | .push(result.host); 378 | } 379 | 380 | compressed 381 | } 382 | 383 | fn print_compressed(compressed: DataMap>, filter: &Filter) { 384 | let mut succeeded_hosts = DataSet::default(); 385 | let mut failed_hosts = DataSet::default(); 386 | 387 | let mut filter_command = DataSet::default(); 388 | let mut filter_failed = DataSet::default(); 389 | let mut filter_result = DataSet::default(); 390 | let mut filter_output = DataSet::default(); 391 | let mut filter_succeeded = DataSet::default(); 392 | let mut filter_unchanged = 0; 393 | 394 | for (result, hosts) in compressed { 395 | // continue if we only want to print out changes and there are none and the 396 | // command was a 397 | // success 398 | // TODO: make this a filter of the map 399 | if filter.succeeded && !result.retcode.is_success() { 400 | for host in hosts { 401 | filter_failed.insert(host); 402 | } 403 | continue; 404 | } 405 | 406 | if filter.failed && result.retcode.is_success() { 407 | for host in hosts { 408 | filter_succeeded.insert(host); 409 | } 410 | continue; 411 | } 412 | 413 | if filter.unchanged && result.output.is_none() && result.retcode.is_success() { 414 | filter_unchanged += 1; 415 | continue; 416 | } 417 | 418 | if result.command.is_some() 419 | && !filter 420 | .command 421 | .is_match(result.command.clone().unwrap().as_str()) 422 | { 423 | for host in hosts { 424 | filter_command.insert(host); 425 | } 426 | continue; 427 | } 428 | 429 | if result.result.is_some() 430 | && !filter 431 | .result 432 | .is_match(result.result.clone().unwrap().as_str()) 433 | { 434 | for host in hosts { 435 | filter_result.insert(host); 436 | } 437 | continue; 438 | } 439 | 440 | if result.output.is_some() 441 | && !filter 442 | .output 443 | .is_match(result.output.clone().unwrap().as_str()) 444 | { 445 | for host in hosts { 446 | filter_output.insert(host); 447 | } 448 | continue; 449 | } 450 | 451 | println!(); 452 | println!("{}", "----------".bold()); 453 | println!(); 454 | 455 | // state, command info 456 | { 457 | if result.command.is_some() { 458 | println!("{}", "------".purple()); 459 | 460 | if result.command.is_some() { 461 | println!( 462 | "{}", 463 | format!("COMMAND: {}", result.command.clone().unwrap()).purple() 464 | ); 465 | } 466 | 467 | println!("{}\n", "------".purple()); 468 | } 469 | } 470 | 471 | // hosts 472 | { 473 | println!("{}", "------".cyan()); 474 | println!("{}{}", "HOSTS: ".cyan(), hosts.join(", ").as_str()); 475 | println!("{}\n", "------".cyan()); 476 | } 477 | 478 | // output 479 | { 480 | println!("{}", "------".yellow()); 481 | 482 | match result.retcode { 483 | Retcode::Success => { 484 | for host in hosts { 485 | succeeded_hosts.insert(host); 486 | } 487 | println!("{}{}", "RETURN CODE: ".yellow(), "Success".green()) 488 | } 489 | Retcode::Failure => { 490 | for host in hosts { 491 | failed_hosts.insert(host); 492 | } 493 | println!("{}{}", "RETURN CODE: ".yellow(), "Failure".red()) 494 | } 495 | } 496 | 497 | if result.result.is_some() { 498 | println!("{}", "RESULT:".yellow()); 499 | println!("{}\n", result.result.unwrap()); 500 | } 501 | 502 | println!("{}", "OUTPUT:".yellow()); 503 | if result.output.is_some() { 504 | for line in result.output.unwrap().lines() { 505 | if line.starts_with('-') { 506 | println!("{}", line.red()); 507 | continue; 508 | } 509 | 510 | if line.starts_with('+') { 511 | println!("{}", line.green()); 512 | continue; 513 | } 514 | 515 | println!("{}", line); 516 | } 517 | } else { 518 | println!("No changes"); 519 | } 520 | println!("{}", "------".yellow()); 521 | } 522 | } 523 | 524 | println!(); 525 | 526 | print_filter_statistics("command", filter_command.len()); 527 | print_filter_statistics("result", filter_result.len()); 528 | print_filter_statistics("output", filter_output.len()); 529 | print_filter_statistics("failed", filter_failed.len()); 530 | print_filter_statistics("succeeded", filter_succeeded.len()); 531 | print_filter_statistics("changed", filter_unchanged); 532 | 533 | info!( 534 | "succeeded host{}: {}", 535 | if succeeded_hosts.len() > 1 || succeeded_hosts.is_empty() { 536 | "s" 537 | } else { 538 | "" 539 | }, 540 | succeeded_hosts.len() 541 | ); 542 | info!( 543 | "failed host{}: {}", 544 | if failed_hosts.len() > 1 || failed_hosts.is_empty() { 545 | "s" 546 | } else { 547 | "" 548 | }, 549 | failed_hosts.len() 550 | ); 551 | } 552 | 553 | fn print_filter_statistics(stats: &str, count: usize) { 554 | info!( 555 | "filtered {} state{}: {}", 556 | stats, 557 | if count > 1 || count == 0 { "s" } else { "" }, 558 | count 559 | ); 560 | } 561 | 562 | fn write_save_file(host_data: &str) { 563 | let save_filename = format!( 564 | "/tmp/salt-compressor_{}.json", 565 | chrono::Utc::now().timestamp() 566 | ); 567 | let mut save_file = File::create(save_filename.clone()).expect("can not create save_file"); 568 | save_file 569 | .write_all(host_data.as_bytes()) 570 | .expect("can not write host data to save_file"); 571 | info!( 572 | "please send me the save file under {} which contains the json data from salt", 573 | save_filename 574 | ); 575 | } 576 | 577 | fn cleanup_input_data<'a>( 578 | input_data: &str, 579 | ) -> ( 580 | String, 581 | std::collections::BTreeMap, 582 | ) { 583 | let mut failed_minions = DataMap::default(); 584 | 585 | // Cleanup input data from minions that either didnt return or had a duplicate 586 | // key 587 | let input_data = { 588 | // match all hosts that have not returned as they are not in the json data 589 | // format is normally like "Minion minionid did not respond. No job will be 590 | // sent." 591 | let catch_not_returned_minions = 592 | Regex::new(r"(?m)^Minion (\S*) did not respond\. No job will be sent\.$") 593 | .expect("regex for catching not returned minions is not valid"); 594 | 595 | let errmessage = "Minion did not respond. No job will be sent."; 596 | for host in catch_not_returned_minions.captures_iter(input_data) { 597 | failed_minions.insert(host[1].to_string(), errmessage); 598 | } 599 | 600 | let data = catch_not_returned_minions 601 | .replace_all(input_data, "") 602 | .into_owned(); 603 | 604 | // match all hosts that have a duplicate key in the system 605 | // like "minion minionid was already deleted from tracker, probably a duplicate 606 | // key" 607 | let catch_duplicate_key_minions = Regex::new( 608 | r"(?m)^minion (\S*) was already deleted from tracker, probably a duplicate key", 609 | ) 610 | .expect("regex for catching duplicate key minions is not valid"); 611 | 612 | let errmessage = "Minion was already deleted from tracker, probably a duplicate key."; 613 | for host in catch_duplicate_key_minions.captures_iter(input_data) { 614 | failed_minions.insert(host[1].to_string(), errmessage); 615 | } 616 | 617 | catch_duplicate_key_minions 618 | .replace_all(data.as_str(), "") 619 | .into_owned() 620 | }; 621 | 622 | let no_return_received = "ERROR: No return received"; 623 | let input_data = if input_data.contains(no_return_received) { 624 | failed_minions.insert('*'.to_string(), "ERROR: No return received."); 625 | input_data.replace(no_return_received, "") 626 | } else { 627 | input_data 628 | }; 629 | 630 | // clean up hosts that have not returned from the json data 631 | (input_data, failed_minions) 632 | } 633 | --------------------------------------------------------------------------------