1use async_trait::async_trait;
2use serde::{Deserialize, Serialize};
3use soma_fleet::{HostId, HostRecord, TopologyRevision};
4use soma_ops::Timestamp;
5use tokio_util::sync::CancellationToken;
6
7use crate::{InfraError, InfraResult};
8
9const MAX_FILTER_CHARS: usize = 256;
10const MAX_PORT_ROWS: u32 = 5000;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14pub struct ServiceListRequest {
15 service: Option<String>,
16 state: Option<String>,
17 deadline: Timestamp,
18}
19
20impl ServiceListRequest {
21 #[must_use]
23 pub const fn new(deadline: Timestamp) -> Self {
24 Self {
25 service: None,
26 state: None,
27 deadline,
28 }
29 }
30 pub fn with_service(mut self, value: impl Into<String>) -> InfraResult<Self> {
32 self.service = Some(validate_filter("service", value.into())?);
33 Ok(self)
34 }
35 pub fn with_state(mut self, value: impl Into<String>) -> InfraResult<Self> {
37 self.state = Some(validate_filter("state", value.into())?);
38 Ok(self)
39 }
40 #[must_use]
42 pub fn service(&self) -> Option<&str> {
43 self.service.as_deref()
44 }
45 #[must_use]
47 pub fn state(&self) -> Option<&str> {
48 self.state.as_deref()
49 }
50 #[must_use]
52 pub const fn deadline(&self) -> Timestamp {
53 self.deadline
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct ServiceStatus {
60 pub unit: String,
62 pub load: String,
64 pub active: String,
66 pub sub: String,
68 pub description: String,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct NetworkAddress {
75 pub family: String,
77 pub address: String,
79 pub prefix_len: u8,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct NetworkInterface {
86 pub index: u64,
88 pub name: String,
90 pub state: Option<String>,
92 pub mtu: Option<u64>,
94 pub addresses: Vec<NetworkAddress>,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub struct MountInfo {
101 pub target: String,
103 pub source: Option<String>,
105 pub filesystem: Option<String>,
107 pub options: Option<String>,
109 pub size_bytes: Option<u64>,
111 pub used_bytes: Option<u64>,
113 pub available_bytes: Option<u64>,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(rename_all = "snake_case")]
120pub enum PortProtocol {
121 Tcp,
123 Udp,
125}
126
127impl PortProtocol {
128 #[cfg(feature = "process-driver")]
129 pub(crate) const fn as_ss_filter(self) -> &'static str {
130 match self {
131 Self::Tcp => "-t",
132 Self::Udp => "-u",
133 }
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139pub struct PortListRequest {
140 protocol: Option<PortProtocol>,
141 offset: u32,
142 limit: u32,
143 deadline: Timestamp,
144}
145
146impl PortListRequest {
147 #[must_use]
149 pub const fn new(deadline: Timestamp) -> Self {
150 Self {
151 protocol: None,
152 offset: 0,
153 limit: 500,
154 deadline,
155 }
156 }
157 #[must_use]
159 pub const fn with_protocol(mut self, protocol: PortProtocol) -> Self {
160 self.protocol = Some(protocol);
161 self
162 }
163 pub fn with_page(mut self, offset: u32, limit: u32) -> InfraResult<Self> {
165 if limit == 0 || limit > MAX_PORT_ROWS {
166 return Err(InfraError::InvalidRequest {
167 domain: "host",
168 message: format!("port limit must be 1-{MAX_PORT_ROWS}"),
169 });
170 }
171 self.offset = offset;
172 self.limit = limit;
173 Ok(self)
174 }
175 #[must_use]
177 pub const fn protocol(&self) -> Option<PortProtocol> {
178 self.protocol
179 }
180 #[must_use]
182 pub const fn offset(&self) -> u32 {
183 self.offset
184 }
185 #[must_use]
187 pub const fn limit(&self) -> u32 {
188 self.limit
189 }
190 #[must_use]
192 pub const fn deadline(&self) -> Timestamp {
193 self.deadline
194 }
195}
196
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
199pub struct PortInfo {
200 pub protocol: String,
202 pub state: String,
204 pub local_address: String,
206 pub peer_address: String,
208 pub process: Option<String>,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214pub struct FilesystemUsage {
215 pub source: String,
217 pub filesystem: String,
219 pub size_bytes: u64,
221 pub used_bytes: u64,
223 pub available_bytes: u64,
225 pub usage_percent: u8,
227 pub target: String,
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
233pub struct DoctorCheck {
234 pub name: String,
236 pub ok: bool,
238 pub summary: String,
240}
241
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244pub struct DoctorReport {
245 pub host: HostId,
247 pub topology_revision: TopologyRevision,
249 pub overall: String,
251 pub checks: Vec<DoctorCheck>,
253}
254
255#[async_trait]
257pub trait HostSystemInspector: Send + Sync {
258 async fn services(
260 &self,
261 host: &HostRecord,
262 request: &ServiceListRequest,
263 cancellation: &CancellationToken,
264 ) -> InfraResult<Vec<ServiceStatus>>;
265 async fn network(
267 &self,
268 host: &HostRecord,
269 deadline: Timestamp,
270 cancellation: &CancellationToken,
271 ) -> InfraResult<Vec<NetworkInterface>>;
272 async fn mounts(
274 &self,
275 host: &HostRecord,
276 deadline: Timestamp,
277 cancellation: &CancellationToken,
278 ) -> InfraResult<Vec<MountInfo>>;
279 async fn ports(
281 &self,
282 host: &HostRecord,
283 request: &PortListRequest,
284 cancellation: &CancellationToken,
285 ) -> InfraResult<Vec<PortInfo>>;
286 async fn filesystem_usage(
288 &self,
289 host: &HostRecord,
290 path: Option<&str>,
291 deadline: Timestamp,
292 cancellation: &CancellationToken,
293 ) -> InfraResult<FilesystemUsage>;
294 async fn doctor(
296 &self,
297 host: &HostRecord,
298 deadline: Timestamp,
299 cancellation: &CancellationToken,
300 ) -> InfraResult<DoctorReport>;
301}
302
303fn validate_filter(field: &'static str, value: String) -> InfraResult<String> {
304 if value.is_empty()
305 || value.chars().count() > MAX_FILTER_CHARS
306 || value.chars().any(char::is_control)
307 {
308 Err(InfraError::InvalidRequest {
309 domain: "host",
310 message: format!("invalid {field} filter"),
311 })
312 } else {
313 Ok(value)
314 }
315}
316
317#[cfg(test)]
318#[path = "host_system_tests.rs"]
319mod tests;