• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

yaleman / maremma / #249

02 Feb 2025 10:09PM UTC coverage: 75.046% (+0.8%) from 74.233%
#249

push

web-flow
New fixes (#131)

14 of 25 new or added lines in 3 files covered. (56.0%)

2 existing lines in 1 file now uncovered.

2033 of 2709 relevant lines covered (75.05%)

2.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.92
/src/services/cli.rs
1
//! CLI-based service checks
2

3
use schemars::JsonSchema;
4

5
use super::prelude::*;
6
use crate::prelude::*;
7
use std::os::unix::process::ExitStatusExt;
8
use std::process::Stdio;
9

10
#[derive(Debug, Deserialize, Serialize, clap::Parser, JsonSchema)]
11
/// A service that runs on the command line, typically on the Maremma server
12
pub struct CliService {
13
    /// Name of the service
14
    pub name: String,
15
    /// Hostname for overlaying on the service
16
    pub hostname: Option<String>,
17
    /// Command line to run, you can use #HOSTNAME# to substitute the hostname
18
    pub command_line: String,
19
    #[serde(default)]
20
    /// If we should run the command in a shell
21
    pub run_in_shell: bool,
22
    #[serde(with = "crate::serde::cron")]
23
    #[schemars(with = "String")]
24
    /// Cron schedule for the service
25
    pub cron_schedule: Cron,
26
    /// Add random jitter in 0..n seconds to the check
27
    pub jitter: Option<u16>,
28
}
29

30
impl ConfigOverlay for CliService {
31
    fn overlay_host_config(&self, value: &Map<String, Json>) -> Result<Box<Self>, Error> {
1✔
32
        let cron_schedule = self.extract_cron(value, "cron_schedule", &self.cron_schedule)?;
1✔
33
        let hostname = self.extract_value(value, "hostname", &self.hostname)?;
2✔
34
        let name = self.extract_string(value, "name", &self.name);
2✔
35
        let command_line = self.extract_string(value, "command_line", &self.command_line);
2✔
36

37
        Ok(Box::new(Self {
2✔
38
            name,
1✔
39
            hostname,
1✔
40
            cron_schedule,
1✔
41
            command_line,
1✔
42
            run_in_shell: self.extract_bool(value, "run_in_shell", self.run_in_shell),
1✔
43
            jitter: self.extract_value(value, "jitter", &self.jitter)?,
1✔
44
        }))
45
    }
46
}
47

48
#[async_trait]
49
impl ServiceTrait for CliService {
50
    async fn run(&self, host: &entities::host::Model) -> Result<CheckResult, Error> {
3✔
51
        let start_time = chrono::Utc::now();
1✔
52
        // run the command line and capture the exit code and stdout
53

54
        let config = self.overlay_host_config(&self.get_host_config(&self.name, host)?)?;
1✔
55

56
        let hostname = match &config.hostname {
1✔
57
            Some(ref h) => h.to_owned(),
×
58
            None => host.hostname.to_owned(),
2✔
59
        };
60

61
        let command_line = config.command_line.replace("#HOSTNAME#", &hostname);
2✔
62

63
        let mut cmd_split = command_line.split(" ");
2✔
64
        let cmd = match cmd_split.next() {
1✔
65
            Some(c) => c,
1✔
66
            None => return Err(Error::Generic("No command specified!".to_string())),
×
67
        };
68

69
        let which_cmd = which::which(cmd).map_err(|err| Error::CommandNotFound(err.to_string()))?;
2✔
70

71
        if !which_cmd.exists() {
2✔
72
            // check if the command exists
NEW
73
            return Err(Error::CommandNotFound(format!(
×
74
                "Command not found: {}",
75
                cmd
76
            )));
77
        }
78

79
        let args = cmd_split.collect::<Vec<&str>>();
2✔
80

81
        let child = tokio::process::Command::new(cmd)
5✔
82
            .args(args)
1✔
83
            .kill_on_drop(true)
84
            .stdout(Stdio::piped())
1✔
85
            .stderr(Stdio::piped())
1✔
86
            .spawn()
87
            .map_err(|err| Error::Generic(err.to_string()))?;
1✔
88

89
        let res = child
4✔
90
            .wait_with_output()
91
            .await
4✔
92
            .map_err(|err| Error::Generic(err.to_string()))?;
×
93

94
        let time_elapsed = chrono::Utc::now() - start_time;
2✔
95

96
        if res.status != std::process::ExitStatus::from_raw(0) {
1✔
97
            let mut combined = res.stderr.to_vec();
×
98
            combined.extend(res.stdout);
×
99
            return Ok(CheckResult {
×
100
                timestamp: chrono::Utc::now(),
×
101
                result_text: String::from_utf8_lossy(&combined)
×
102
                    .to_string()
103
                    .replace(r#"\\n"#, " "),
104
                status: ServiceStatus::Critical,
×
105
                time_elapsed,
106
            });
107
        }
108

109
        Ok(CheckResult {
1✔
110
            timestamp: chrono::Utc::now(),
1✔
111
            result_text: String::from_utf8_lossy(&res.stdout)
4✔
112
                .to_string()
113
                .replace(r#"\\n"#, " "),
114
            status: ServiceStatus::Ok,
1✔
115
            time_elapsed,
116
        })
117
    }
118

119
    fn as_json_pretty(&self, host: &entities::host::Model) -> Result<String, Error> {
1✔
120
        let config = self.overlay_host_config(&self.get_host_config(&self.name, host)?)?;
1✔
121
        Ok(serde_json::to_string_pretty(&config)?)
1✔
122
    }
123

124
    fn jitter_value(&self) -> u32 {
×
125
        self.jitter.unwrap_or(0) as u32
×
126
    }
127
}
128

129
#[cfg(test)]
130
mod tests {
131
    use entities::host::test_host;
132

133
    use crate::prelude::*;
134

135
    #[tokio::test]
136
    async fn test_cliservice() {
137
        let service = super::CliService {
138
            name: "test".to_string(),
139
            hostname: None,
140
            command_line: "ls -lah .".to_string(),
141
            run_in_shell: false,
142
            cron_schedule: "@hourly".parse().expect("Failed to parse cron schedule"),
143
            jitter: None,
144
        };
145
        let host = entities::host::Model {
146
            check: crate::host::HostCheck::None,
147
            ..test_host()
148
        };
149

150
        let res = service.run(&host).await;
151
        assert_eq!(service.name, "test".to_string());
152
        assert!(res.is_ok());
153
    }
154

155
    #[test]
156
    fn test_parse_cliservice() {
157
        let service: super::CliService = match serde_json::from_str(
158
            r#" {
159
            "name": "local_lslah",
160
            "service_type": "cli",
161
            "host_groups": ["local_lslah"],
162
            "command_line": "ls -lah /tmp",
163
            "cron_schedule": "* * * * *"
164
        }"#,
165
        ) {
166
            Err(err) => panic!("Failed to parse service: {:?}", err),
167
            Ok(val) => val,
168
        };
169
        assert_eq!(service.name, "local_lslah".to_string());
170

171
        // test parsing broken service
172
        assert!(Service {
173
            name: Some("test".to_string()),
174
            service_type: ServiceType::Cli,
175
            id: Default::default(),
176
            description: None,
177
            host_groups: vec![],
178
            cron_schedule: Cron::new("@hourly").parse().expect("Failed to parse cron"),
179
            extra_config: HashMap::from_iter([("hello".to_string(), json!("world"))]),
180
            config: None
181
        }
182
        .parse_config()
183
        .is_err());
184
    }
185
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc