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

yaleman / maremma / #272

02 Nov 2025 10:24AM UTC coverage: 77.069% (+2.2%) from 74.826%
#272

push

web-flow
chore(deps): bump actions/download-artifact from 5 to 6 (#187)

Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

2198 of 2852 relevant lines covered (77.07%)

2.19 hits per line

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

14.29
/src/host/ssh.rs
1
use std::net::ToSocketAddrs;
2
use std::num::NonZeroU16;
3
use std::time::Duration;
4

5
use chrono::Utc;
6
use serde::Deserialize;
7

8
use crate::prelude::*;
9

10
/// The default timeout
11
pub const DEFAULT_SSH_TIMEOUT_SECONDS: u16 = 30;
12
/// Guess?
13
pub const DEFAULT_SSH_PORT: u16 = 22;
14

15
fn default_ssh_port() -> NonZeroU16 {
×
16
    #[allow(clippy::expect_used)]
17
    NonZeroU16::new(DEFAULT_SSH_PORT).expect("Failed to parse 22 as non-zero u16!")
×
18
}
19

20
fn default_ssh_timeout_seconds() -> u16 {
21
    DEFAULT_SSH_TIMEOUT_SECONDS
22
}
23

24
#[derive(Default, Deserialize, Serialize, Debug)]
25
/// An SSH-connected host
26
pub struct SshHost {
27
    /// The hostname
28
    pub hostname: String,
29
    /// Defaults to [DEFAULT_SSH_PORT] (22)
30
    pub port: Option<NonZeroU16>,
31
    /// If you want to connect via IP address instead
32
    #[serde(skip_serializing_if = "Option::is_none")]
33
    pub ip_address: Option<std::net::IpAddr>,
34
    /// Defaults to [DEFAULT_SSH_TIMEOUT_SECONDS]
35
    #[serde(default = "default_ssh_timeout_seconds")]
36
    pub timeout_seconds: u16,
37
    /// If you're not just connecting as "you"
38
    #[serde(skip_serializing_if = "Option::is_none")]
39
    pub remote_user: Option<String>,
40

41
    #[serde(default)]
42
    /// Groups that this host is part of
43
    pub host_groups: Vec<String>,
44

45
    #[serde(default)]
46
    /// If this host is disabled
47
    pub disabled: bool,
48

49
    #[serde(skip)]
50
    /// The last time we checked this host
51
    #[serde(skip_serializing_if = "Option::is_none")]
52
    pub last_check: Option<DateTime<Utc>>,
53

54
    #[serde(skip)]
55
    /// The list of service checks for this host
56
    pub service_checks: Vec<Box<dyn ServiceTrait>>,
57
}
58

59
impl SshHost {
60
    /// Create a new SshHost from a hostname
61
    pub fn from_hostname(hostname: &str) -> Self {
×
62
        Self {
63
            hostname: hostname.to_string(),
×
64
            ..Default::default()
65
        }
66
    }
67
    /// Update the timeout
68
    pub fn with_timeout(self, timeout_seconds: u16) -> Self {
×
69
        Self {
70
            timeout_seconds,
71
            ..self
72
        }
73
    }
74
}
75

76
#[async_trait]
77
impl GenericHost for SshHost {
78
    async fn check_up(&self) -> Result<bool, Error> {
×
79
        let socket_address = match self.ip_address {
×
80
            Some(ip) => match (ip, u16::from(self.port.unwrap_or(default_ssh_port())))
×
81
                .to_socket_addrs()
×
82
                .map_err(|_err| Error::DnsFailed)?
×
83
                .next()
×
84
            {
85
                Some(sock) => sock,
×
86
                None => return Err(Error::DnsFailed),
×
87
            },
88
            None => match format!(
×
89
                "{}:{}",
90
                self.hostname,
91
                self.port.unwrap_or(default_ssh_port())
×
92
            )
93
            .to_socket_addrs()
×
94
            .map_err(|_err| Error::DnsFailed)?
×
95
            .next()
×
96
            {
97
                Some(val) => val,
×
98
                None => return Err(Error::DnsFailed),
×
99
            },
100
        };
101
        let result = std::net::TcpStream::connect_timeout(
102
            &socket_address,
103
            Duration::from_secs(self.timeout_seconds as u64),
×
104
        );
105
        match result {
×
106
            Ok(_) => Ok(true),
×
107
            Err(_) => Ok(false),
×
108
        }
109
    }
110

111
    fn try_from_config(config: serde_json::Value) -> Result<Self, Error>
1✔
112
    where
113
        Self: Sized,
114
    {
115
        Self::try_from(&config)
1✔
116
    }
117
}
118

119
impl TryFrom<&Value> for SshHost {
120
    type Error = Error;
121

122
    fn try_from(value: &Value) -> Result<Self, Self::Error> {
1✔
123
        serde_json::from_value(value.clone()).map_err(|e| Error::Deserialization(e.to_string()))
1✔
124
    }
125
}
126

127
#[cfg(test)]
128
mod test {
129
    use super::*;
130

131
    #[tokio::test]
132
    async fn test_check_up() {
133
        // check we have a test host defined in the MAREMMA_TEST_SSH_HOST env var
134
        let hostname = match std::env::var("MAREMMA_TEST_SSH_HOST") {
135
            Ok(val) => val,
136
            Err(_) => {
137
                eprintln!("MAREMMA_TEST_SSH_HOST not set, skipping test");
138
                return;
139
            }
140
        };
141
        let host = SshHost::from_hostname(&hostname).with_timeout(5);
142
        assert!(host.check_up().await.is_ok());
143

144
        let example_com = SshHost::from_hostname("example.com").with_timeout(1);
145
        let res = example_com.check_up().await;
146

147
        assert!(res.is_ok());
148
        assert!(!res.expect("Failed to check example.com"));
149
    }
150

151
    #[test]
152
    fn test_config_parse() {
153
        let config = r#"
154
            {
155
                "hostname": "example.com",
156
                "timeout_seconds": 1234
157
            }
158
        "#;
159
        let host: SshHost = serde_json::from_str(config).expect("Failed to parse config");
160
        assert_eq!(host.hostname, "example.com");
161
        assert_eq!(host.port, None);
162
        assert_eq!(host.timeout_seconds, 1234);
163
    }
164
    #[test]
165
    fn test_try_from_value() {
166
        let config = serde_json::json! {
167
                {
168
                    "hostname": "example.com",
169
                    "port" : 123
170
                }
171
        };
172
        let host = SshHost::try_from(&config).expect("Failed to parse from value");
173
        assert_eq!(host.hostname, "example.com");
174
        assert_eq!(
175
            host.port,
176
            Some(NonZeroU16::new(123).expect("failed to parse 123 as a non-zero u16"))
177
        );
178
        assert_eq!(host.timeout_seconds, default_ssh_timeout_seconds());
179
        assert_eq!(
180
            SshHost::try_from_config(config)
181
                .expect("Failed to parse from config")
182
                .hostname,
183
            host.hostname
184
        );
185
    }
186
}
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