netzwork_api/
lib.rs

1pub mod agent;
2pub mod error;
3pub mod host;
4pub mod id;
5pub mod interface;
6
7use std::fs::File;
8use std::io::{BufRead, BufReader};
9use std::time::{Duration, SystemTime};
10
11use agent::{AgentInfo, AgentStatus};
12use chrono::DateTime;
13use error::NetzworkApiError;
14use id::agent_uuid;
15use interface::{NetInterface, NetInterfaceAddr};
16use network_interface::{NetworkInterface, NetworkInterfaceConfig};
17use serde::{Deserialize, Serialize};
18use uuid::Uuid;
19
20use crate::host::HostInfo;
21use crate::id::{host_uuid, interface_addr_uuid, interface_uuid};
22
23#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
24pub struct Heartbeat {
25    /// Heartbeat type
26    pub hb_type: HeartbeatType,
27    /// Current timestamp
28    pub timestamp: DateTime<chrono::Utc>,
29    /// UUID (v4) of this heartbeat
30    pub hb_uuid: Uuid,
31    /// Payload
32    pub payload: Vec<HeartbeatPayload>,
33}
34
35impl Heartbeat {
36    pub fn new(
37        hb_type: HeartbeatType,
38        name: String,
39        agent_status: Option<AgentStatus>,
40    ) -> Heartbeat {
41        let status = match agent_status {
42            Some(s) => s,
43            None => AgentStatus::Undefined,
44        };
45        let host_info = HostInfo::new();
46        let mut hb = Heartbeat {
47            hb_type: hb_type.clone(),
48            timestamp: DateTime::from(SystemTime::now()),
49            hb_uuid: Uuid::new_v4(),
50            payload: vec![
51                HeartbeatPayload::AgentInfo(AgentInfo {
52                    host_uuid: host_info.host_uuid,
53                    uuid: agent_uuid(&host_info.host_uuid, &name),
54                    name,
55                    status,
56                }),
57                HeartbeatPayload::HostInfo(host_info.clone()),
58            ],
59        };
60        if hb_type == HeartbeatType::Full {
61            hb.payload
62                .push(HeartbeatPayload::BuildInfo(BuildInfo::new()));
63            #[cfg(target_os = "linux")]
64            {
65                hb.payload
66                    .push(HeartbeatPayload::PSIInfo(PSIInfoResource::CPU));
67                hb.payload
68                    .push(HeartbeatPayload::PSIInfo(PSIInfoResource::Memory));
69                hb.payload
70                    .push(HeartbeatPayload::PSIInfo(PSIInfoResource::IO));
71            }
72            hb.payload
73                .push(HeartbeatPayload::OSInfo(OSInfo::new(host_uuid().unwrap())));
74            match NetworkInterface::show() {
75                Ok(network_interfaces) => {
76                    for itf in network_interfaces.iter() {
77                        println!("{:?}", itf);
78                        if let Some(mac_addr_string) = &itf.mac_addr {
79                            let maybe_mac_addr: Result<Vec<u8>, _> = mac_addr_string
80                                .split(':')
81                                .map(|s| u8::from_str_radix(s, 16))
82                                .collect();
83                            match maybe_mac_addr {
84                                Ok(mac_addr) => {
85                                let ni_info = NetInterface {
86                                    host_uuid: host_info.host_uuid,
87                                    uuid: interface_uuid(&host_info.host_uuid, &mac_addr),
88                                    name: itf.name.clone(),
89                                    mac_addr,
90                                };
91                                hb.payload
92                                    .push(HeartbeatPayload::NetInterface(ni_info.clone()));
93                                for a in itf.addr.iter() {
94                                    let nia_info = NetInterfaceAddr {
95                                        iface_uuid: ni_info.uuid,
96                                        uuid: interface_addr_uuid(&ni_info.uuid, a.ip()),
97                                        ip_addr: a.ip().to_string(),
98                                        active: true,
99                                    };
100                                    hb.payload
101                                        .push(HeartbeatPayload::NetInterfaceAddr(nia_info));
102                                }
103                                },
104                                Err(_) => eprintln!("Interface {:?} produced an error retrieving the MAC address, skipping for now (open issue)", itf),
105                            };
106                        } else {
107                            eprintln!("Interface {:?} has an empty MAC address, skipping for now (open issue)", itf);
108                        };
109                    }
110                }
111                Err(_) => {
112                    eprintln!("Could not retrieve network interfaces");
113                }
114            };
115        }
116        hb
117        // //let mut ni_info = NetInterfaceInfo::new();
118        // for (if_name, ip_addr) in network_interfaces.iter() {
119        //     let mut found_if = false;
120        //     for ifs in ni_info.interfaces.iter_mut() {
121        //         // the interface is known
122        //         if ifs.if_name.eq(if_name) {
123        //             found_if = true;
124        //             // the address is not stored yet
125        //             if !ifs.ip_addr.contains(ip_addr) {
126        //                 // store address
127        //                 ifs.ip_addr.push(*ip_addr);
128        //             }
129        //         };
130        //     }
131        //     if !found_if {
132        //         // we had no interface matches
133        //         ni_info.interfaces.push(NetInterfaceId {
134        //             machine_id: get_secure_machine_id(None).unwrap(),
135        //             if_name: if_name.clone(),
136        //             ip_addr: vec![*ip_addr],
137        //         })
138        //     }
139        // }
140    }
141    pub fn compact(payload: Vec<HeartbeatPayloadType>) -> Heartbeat {
142        let mut hb = Heartbeat {
143            hb_type: HeartbeatType::Compact,
144            timestamp: DateTime::from(SystemTime::now()),
145            hb_uuid: Uuid::new_v4(),
146            payload: vec![],
147        };
148        for p in payload.iter() {
149            hb.payload.push(p.to_payload())
150        }
151        hb
152    }
153}
154
155#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
156pub enum HeartbeatPayloadType {
157    AgentInfo(AgentStatus, String),
158    HostInfo,
159    BuildInfo,
160    NetInterface(NetInterface),
161    NetInterfaceAddr(NetInterfaceAddr),
162    PSIInfo(PSIInfoResource),
163    OSInfo,
164}
165impl HeartbeatPayloadType {
166    pub fn to_payload(&self) -> HeartbeatPayload {
167        match self {
168            HeartbeatPayloadType::AgentInfo(status, name) => {
169                HeartbeatPayload::AgentInfo(AgentInfo {
170                    status: status.to_owned(),
171                    name: name.to_string(),
172                    host_uuid: host_uuid().unwrap(),
173                    uuid: agent_uuid(&host_uuid().unwrap(), name),
174                })
175            }
176            HeartbeatPayloadType::HostInfo => HeartbeatPayload::HostInfo(HostInfo::new()),
177            HeartbeatPayloadType::BuildInfo => HeartbeatPayload::BuildInfo(BuildInfo::new()),
178            HeartbeatPayloadType::NetInterface(ni) => HeartbeatPayload::NetInterface(ni.clone()),
179            HeartbeatPayloadType::NetInterfaceAddr(nia) => {
180                HeartbeatPayload::NetInterfaceAddr(nia.clone())
181            }
182            HeartbeatPayloadType::PSIInfo(resource) => {
183                HeartbeatPayload::PSIInfo(PSIInfo::new(resource))
184            }
185            HeartbeatPayloadType::OSInfo => {
186                HeartbeatPayload::OSInfo(OSInfo::new(host_uuid().unwrap()))
187            }
188        }
189    }
190}
191
192#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
193pub enum HeartbeatPayload {
194    AgentInfo(AgentInfo),
195    HostInfo(HostInfo),
196    BuildInfo(BuildInfo),
197    NetInterface(NetInterface),
198    NetInterfaceAddr(NetInterfaceAddr),
199    PSIInfo(PSIInfo),
200    OSInfo(OSInfo),
201}
202
203#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
204pub enum HeartbeatType {
205    /// A full heartbeat contains information in all data structures
206    Full,
207    /// A compact heartbeat contains a minimal amount of information,
208    /// indicating that information is unchanged compared to the previous heartbeat.
209    Compact,
210}
211
212#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
213pub struct BuildInfo {
214    // TODO: Include API crate version here
215    // See: https://linear.app/netzwork/issue/NET-31/api-include-api-crate-version-in-the-buildinfo-struct
216    build_timestamp: String,
217    build_date: String,
218    git_branch: String,
219    git_timestamp: String,
220    git_date: String,
221    git_hash: String,
222    git_describe: String,
223    rustc_host_triple: String,
224    rustc_version: String,
225    cargo_target_triple: String,
226}
227
228impl BuildInfo {
229    fn new() -> BuildInfo {
230        BuildInfo {
231            build_timestamp: String::from(env!("VERGEN_BUILD_TIMESTAMP")),
232            build_date: String::from(env!("VERGEN_BUILD_DATE")),
233            git_branch: String::from(env!("VERGEN_GIT_BRANCH")),
234            git_timestamp: String::from(env!("VERGEN_GIT_COMMIT_TIMESTAMP")),
235            git_date: String::from(env!("VERGEN_GIT_COMMIT_DATE")),
236            git_hash: String::from(env!("VERGEN_GIT_SHA")),
237            git_describe: String::from(env!("VERGEN_GIT_DESCRIBE")),
238            rustc_host_triple: String::from(env!("VERGEN_RUSTC_HOST_TRIPLE")),
239            rustc_version: String::from(env!("VERGEN_RUSTC_SEMVER")),
240            cargo_target_triple: String::from(env!("VERGEN_CARGO_TARGET_TRIPLE")),
241        }
242    }
243}
244
245#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
246pub struct ConnectivityMeasurementSample {
247    timestamp: SystemTime,
248    rtt: Duration,
249    fingerprint: [u8; 32],
250}
251
252#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
253pub struct PSIInfo {
254    resource: String,
255    lines: Vec<PSIInfoLine>,
256}
257
258#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
259pub enum PSIInfoResource {
260    CPU,
261    IO,
262    Memory,
263}
264
265impl PSIInfoResource {
266    fn to_string(&self) -> String {
267        match self {
268            PSIInfoResource::CPU => "cpu".to_string(),
269            PSIInfoResource::IO => "io".to_string(),
270            PSIInfoResource::Memory => "memory".to_string(),
271        }
272    }
273    fn to_proc_path(&self) -> String {
274        match self {
275            PSIInfoResource::CPU => "/proc/pressure/cpu".to_string(),
276            PSIInfoResource::IO => "/proc/pressure/io".to_string(),
277            PSIInfoResource::Memory => "/proc/pressure/memory".to_string(),
278        }
279    }
280}
281
282impl PSIInfo {
283    fn new(rs: &PSIInfoResource) -> PSIInfo {
284        let resource = rs.to_string();
285        let psifile_path = rs.to_proc_path();
286        let psifile = File::open(psifile_path).expect("could not open PSI file path");
287        let reader = BufReader::new(psifile);
288        let mut lines = Vec::<PSIInfoLine>::new();
289        for maybe_line in reader.lines() {
290            if let Ok(line) = maybe_line {
291                if let Ok(psi_info_line) = PSIInfoLine::from_line(&line) {
292                    lines.push(psi_info_line);
293                }
294            }
295        }
296        PSIInfo { resource, lines }
297    }
298}
299
300#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
301pub struct PSIInfoLine {
302    pub share: String,
303    pub avg10: f64,
304    pub avg60: f64,
305    pub avg300: f64,
306    pub total: u64,
307}
308
309impl PSIInfoLine {
310    fn from_line(line: &str) -> Result<PSIInfoLine, NetzworkApiError> {
311        let mut line_elements = line.split_whitespace();
312        let share = match line_elements.next() {
313            Some(s) => match s {
314                "some" => Ok("some".to_string()),
315                "full" => Ok("full".to_string()),
316                &_ => Err(NetzworkApiError::SomeProblem(
317                    "Error compiling PSI information: parsing share failed".to_string(),
318                )),
319            },
320            None => Err(NetzworkApiError::SomeProblem(
321                "Error compiling PSI information: parsing share failed".to_string(),
322            )),
323        }?;
324        let avg10: f64 = match line_elements.next() {
325            Some(s) => match s.replace("avg10=", "").parse::<f64>() {
326                Ok(a) => Ok(a),
327                Err(_) => Err(NetzworkApiError::SomeProblem(
328                    "Error compiling PSI information: parsing avg10 failed".to_string(),
329                )),
330            },
331            None => Err(NetzworkApiError::SomeProblem(
332                "Error compiling PSI information: parsing avg10 failed".to_string(),
333            )),
334        }?;
335        let avg60: f64 = match line_elements.next() {
336            Some(s) => match s.replace("avg60=", "").parse::<f64>() {
337                Ok(a) => Ok(a),
338                Err(_) => Err(NetzworkApiError::SomeProblem(
339                    "Error compiling PSI information: parsing avg60 failed".to_string(),
340                )),
341            },
342            None => Err(NetzworkApiError::SomeProblem(
343                "Error compiling PSI information: parsing avg60 failed".to_string(),
344            )),
345        }?;
346        let avg300: f64 = match line_elements.next() {
347            Some(s) => match s.replace("avg300=", "").parse::<f64>() {
348                Ok(a) => Ok(a),
349                Err(_) => Err(NetzworkApiError::SomeProblem(
350                    "Error compiling PSI information: parsing avg300 failed".to_string(),
351                )),
352            },
353            None => Err(NetzworkApiError::SomeProblem(
354                "Error compiling PSI information: parsing avg300 failed".to_string(),
355            )),
356        }?;
357        let total: u64 = match line_elements.next() {
358            Some(s) => match s.replace("total=", "").parse::<u64>() {
359                Ok(a) => Ok(a),
360                Err(_) => Err(NetzworkApiError::SomeProblem(
361                    "Error compiling PSI information: parsing total failed".to_string(),
362                )),
363            },
364            None => Err(NetzworkApiError::SomeProblem(
365                "Error compiling PSI information: parsing total failed".to_string(),
366            )),
367        }?;
368        Ok(PSIInfoLine {
369            share,
370            avg10,
371            avg60,
372            avg300,
373            total,
374        })
375    }
376}
377
378// #[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
379// pub struct NetInterfaceConnectivityMeasurement {
380//     interface: NetInterfaceId,
381//     remote_socket: SocketAddr,
382//     samples: Vec<ConnectivityMeasurementSample>,
383// }
384//
385// #[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
386// pub struct NetInterfaceInfo {
387//     pub interfaces: Vec<NetInterfaceId>,
388//     pub measurements: Vec<NetInterfaceConnectivityMeasurement>,
389// }
390// impl NetInterfaceInfo {
391//     pub fn new() -> NetInterfaceInfo {
392//         NetInterfaceInfo {
393//             interfaces: vec![],
394//             measurements: vec![],
395//         }
396//     }
397//
398//     pub fn from_network_interfaces() -> Result<NetInterfaceInfo, local_ip_address::Error> {
399//         let network_interfaces = list_afinet_netifas()?;
400//         let mut ni_info = NetInterfaceInfo::new();
401//         for (if_name, ip_addr) in network_interfaces.iter() {
402//             let mut found_if = false;
403//             for ifs in ni_info.interfaces.iter_mut() {
404//                 // the interface is known
405//                 if ifs.if_name.eq(if_name) {
406//                     found_if = true;
407//                     // the address is not stored yet
408//                     if !ifs.ip_addr.contains(ip_addr) {
409//                         // store address
410//                         ifs.ip_addr.push(*ip_addr);
411//                     }
412//                 };
413//             }
414//             if !found_if {
415//                 // we had no interface matches
416//                 ni_info.interfaces.push(NetInterfaceId {
417//                     machine_id: get_secure_machine_id(None).unwrap(),
418//                     if_name: if_name.clone(),
419//                     ip_addr: vec![*ip_addr],
420//                 })
421//             }
422//         }
423//         Ok(ni_info)
424//     }
425// }
426// impl Default for NetInterfaceInfo {
427//     fn default() -> Self {
428//         Self::new()
429//     }
430// }
431
432#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
433pub struct OSInfo {
434    host_uuid: Uuid,
435    os_type: String,
436    version: String,
437    bitness: String,
438    architecture: String,
439}
440impl OSInfo {
441    fn new(host_uuid: Uuid) -> OSInfo {
442        let info = os_info::get();
443        OSInfo {
444            host_uuid,
445            os_type: info.os_type().to_string(),
446            version: info.version().to_string(),
447            bitness: info.bitness().to_string(),
448            architecture: info.architecture().unwrap_or("unknown").to_string(),
449        }
450    }
451}