1use std::collections::HashMap;
7use std::path::PathBuf;
8use std::str::FromStr;
9use std::sync::Arc;
10
11use anyhow::Result;
12use int_enum::IntEnum;
13use serde_repr::*;
14use strum_macros::EnumIter;
15use strum_macros::EnumString;
16use tokio::sync::RwLock;
17use tracing::error;
18
19use crate::MachineStats;
20
21#[allow(non_camel_case_types)]
23#[derive(
24 Serialize_repr,
25 Deserialize_repr,
26 Clone,
27 Copy,
28 Debug,
29 Default,
30 Eq,
31 PartialEq,
32 EnumIter,
33 EnumString,
34 IntEnum,
35 strum_macros::Display,
36)]
37#[repr(u8)]
38pub enum AddressState {
39 #[default]
40 unknown = 0,
41 off = 1,
42 degraded = 2,
43 routable = 3,
44}
45
46#[allow(non_camel_case_types)]
48#[derive(
49 Serialize_repr,
50 Deserialize_repr,
51 Clone,
52 Copy,
53 Debug,
54 Default,
55 Eq,
56 PartialEq,
57 EnumIter,
58 EnumString,
59 IntEnum,
60 strum_macros::Display,
61)]
62#[repr(u8)]
63pub enum AdminState {
64 #[default]
65 unknown = 0,
66 pending = 1,
67 failed = 2,
68 configuring = 3,
69 configured = 4,
70 unmanaged = 5,
71 linger = 6,
72}
73
74#[allow(non_camel_case_types)]
76#[derive(
77 Serialize_repr,
78 Deserialize_repr,
79 Clone,
80 Copy,
81 Debug,
82 Default,
83 Eq,
84 PartialEq,
85 EnumIter,
86 EnumString,
87 IntEnum,
88 strum_macros::Display,
89)]
90#[repr(u8)]
91pub enum BoolState {
92 #[default]
93 unknown = u8::MAX,
94 #[strum(
95 serialize = "false",
96 serialize = "False",
97 serialize = "no",
98 serialize = "No"
99 )]
100 False = 0,
101 #[strum(
102 serialize = "true",
103 serialize = "True",
104 serialize = "yes",
105 serialize = "Yes"
106 )]
107 True = 1,
108}
109
110#[allow(non_camel_case_types)]
112#[derive(
113 Serialize_repr,
114 Deserialize_repr,
115 Clone,
116 Copy,
117 Debug,
118 Default,
119 Eq,
120 PartialEq,
121 EnumIter,
122 EnumString,
123 IntEnum,
124 strum_macros::Display,
125)]
126#[repr(u8)]
127pub enum CarrierState {
128 #[default]
129 unknown = 0,
130 off = 1,
131 #[strum(serialize = "no-carrier", serialize = "no_carrier")]
132 no_carrier = 2,
133 dormant = 3,
134 #[strum(serialize = "degraded-carrier", serialize = "degraded_carrier")]
135 degraded_carrier = 4,
136 carrier = 5,
137 enslaved = 6,
138}
139
140#[allow(non_camel_case_types)]
142#[derive(
143 Serialize_repr,
144 Deserialize_repr,
145 Clone,
146 Copy,
147 Debug,
148 Default,
149 Eq,
150 PartialEq,
151 EnumIter,
152 EnumString,
153 IntEnum,
154 strum_macros::Display,
155)]
156#[repr(u8)]
157pub enum OnlineState {
158 #[default]
159 unknown = 0,
160 offline = 1,
161 partial = 2,
162 online = 3,
163}
164
165#[allow(non_camel_case_types)]
167#[derive(
168 Serialize_repr,
169 Deserialize_repr,
170 Clone,
171 Copy,
172 Debug,
173 Default,
174 Eq,
175 PartialEq,
176 EnumIter,
177 EnumString,
178 IntEnum,
179 strum_macros::Display,
180)]
181#[repr(u8)]
182pub enum OperState {
183 #[default]
184 unknown = 0,
185 missing = 1,
186 off = 2,
187 #[strum(serialize = "no-carrier", serialize = "no_carrier")]
188 no_carrier = 3,
189 dormant = 4,
190 #[strum(serialize = "degraded-carrier", serialize = "degraded_carrier")]
191 degraded_carrier = 5,
192 carrier = 6,
193 degraded = 7,
194 enslaved = 8,
195 routable = 9,
196}
197
198#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
200pub struct InterfaceState {
201 pub address_state: AddressState,
202 pub admin_state: AdminState,
203 pub carrier_state: CarrierState,
204 pub ipv4_address_state: AddressState,
205 pub ipv6_address_state: AddressState,
206 pub name: String,
207 pub network_file: String,
208 pub oper_state: OperState,
209 pub required_for_online: BoolState,
210}
211
212async fn get_interface_links(
214 connection: &zbus::Connection,
215) -> Result<HashMap<i32, String>, Box<dyn std::error::Error + Send + Sync>> {
216 let p = crate::dbus::zbus_networkd::ManagerProxy::new(connection).await?;
217 let links = p.list_links().await?;
218 let mut link_int_to_name: HashMap<i32, String> = HashMap::new();
219 for network_link in links {
220 link_int_to_name.insert(network_link.0, network_link.1);
221 }
222 Ok(link_int_to_name)
223}
224
225#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
227pub struct NetworkdState {
228 pub interfaces_state: Vec<InterfaceState>,
229 pub managed_interfaces: u64,
230}
231
232pub const NETWORKD_STATE_FILES: &str = "/run/systemd/netif/links";
233
234pub fn parse_interface_stats(
236 interface_state_str: String,
237 interface_id: i32,
238 interface_id_to_name: &HashMap<i32, String>,
239) -> Result<InterfaceState, String> {
240 let mut interface_state = InterfaceState::default();
241
242 for line in interface_state_str.lines() {
243 if !line.contains('=') {
245 continue;
246 }
247
248 if interface_id > 0 {
250 interface_state.name = interface_id_to_name
251 .get(&interface_id)
252 .unwrap_or(&String::from(""))
253 .to_string();
254 }
255
256 let (key, value) = line
257 .split_once('=')
258 .expect("Unable to split a network state line");
259 match key {
260 "ADDRESS_STATE" => {
261 interface_state.address_state =
262 AddressState::from_str(value).unwrap_or(AddressState::unknown)
263 }
264 "ADMIN_STATE" => {
265 interface_state.admin_state =
266 AdminState::from_str(value).unwrap_or(AdminState::unknown)
267 }
268 "CARRIER_STATE" => {
269 interface_state.carrier_state =
270 CarrierState::from_str(value).unwrap_or(CarrierState::unknown)
271 }
272 "IPV4_ADDRESS_STATE" => {
273 interface_state.ipv4_address_state =
274 AddressState::from_str(value).unwrap_or(AddressState::unknown)
275 }
276 "IPV6_ADDRESS_STATE" => {
277 interface_state.ipv6_address_state =
278 AddressState::from_str(value).unwrap_or(AddressState::unknown)
279 }
280 "NETWORK_FILE" => interface_state.network_file = value.to_string(),
281 "OPER_STATE" => {
282 interface_state.oper_state =
283 OperState::from_str(value).unwrap_or(OperState::unknown)
284 }
285 "REQUIRED_FOR_ONLINE" => {
286 interface_state.required_for_online =
287 BoolState::from_str(value).unwrap_or(BoolState::unknown)
288 }
289 _ => continue,
290 };
291 }
292
293 Ok(interface_state)
294}
295
296pub async fn parse_interface_state_files(
298 states_path: &PathBuf,
299 maybe_network_int_to_name: Option<HashMap<i32, String>>,
300 maybe_connection: Option<&zbus::Connection>,
301) -> Result<NetworkdState, std::io::Error> {
302 let mut managed_interface_count: u64 = 0;
303 let mut interfaces_state = vec![];
304
305 let network_int_to_name = match maybe_network_int_to_name {
306 None => {
307 if let Some(connection) = maybe_connection {
308 match get_interface_links(connection).await {
309 Ok(hashmap) => hashmap,
310 Err(err) => {
311 error!(
312 "Unable to get interface links via DBUS - is networkd running?: {:#?}",
313 err
314 );
315 return Ok(NetworkdState::default());
316 }
317 }
318 } else {
319 error!(
320 "Unable to get interface links via DBUS and no network_int_to_name supplied"
321 );
322 return Ok(NetworkdState::default());
323 }
324 }
325 Some(valid_hashmap) => valid_hashmap,
326 };
327
328 let mut state_file_dir_entries = tokio::fs::read_dir(states_path).await?;
329 while let Some(state_file) = state_file_dir_entries.next_entry().await? {
330 if !state_file.path().is_file() {
331 continue;
332 }
333 let interface_stats_file_str = tokio::fs::read_to_string(state_file.path()).await?;
334 if !interface_stats_file_str.contains("NETWORK_FILE") {
335 continue;
336 }
337 managed_interface_count += 1;
338 let fname = state_file.file_name();
339 let interface_id: i32 = i32::from_str(fname.to_str().unwrap_or("0")).unwrap_or(0);
340 match parse_interface_stats(interface_stats_file_str, interface_id, &network_int_to_name) {
341 Ok(interface_state) => interfaces_state.push(interface_state),
342 Err(err) => error!(
343 "Unable to parse interface statistics for {:?}: {}",
344 state_file.path().into_os_string(),
345 err
346 ),
347 }
348 }
349 Ok(NetworkdState {
350 interfaces_state,
351 managed_interfaces: managed_interface_count,
352 })
353}
354
355pub async fn update_networkd_stats(
357 states_path: PathBuf,
358 maybe_network_int_to_name: Option<HashMap<i32, String>>,
359 connection: zbus::Connection,
360 locked_machine_stats: Arc<RwLock<MachineStats>>,
361) -> anyhow::Result<()> {
362 match parse_interface_state_files(&states_path, maybe_network_int_to_name, Some(&connection))
363 .await
364 {
365 Ok(networkd_stats) => {
366 let mut machine_stats = locked_machine_stats.write().await;
367 machine_stats.networkd = networkd_stats;
368 }
369 Err(err) => error!("networkd stats failed: {:?}", err),
370 }
371 Ok(())
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377 use std::fs::File;
378 use std::io::Write;
379 use tempfile::tempdir;
380
381 const MOCK_INTERFACE_STATE: &str = r###"# This is private data. Do not parse.
382ADMIN_STATE=configured
383OPER_STATE=routable
384CARRIER_STATE=carrier
385ADDRESS_STATE=routable
386IPV4_ADDRESS_STATE=degraded
387IPV6_ADDRESS_STATE=routable
388ONLINE_STATE=online
389REQUIRED_FOR_ONLINE=yes
390REQUIRED_OPER_STATE_FOR_ONLINE=degraded:routable
391REQUIRED_FAMILY_FOR_ONLINE=any
392ACTIVATION_POLICY=up
393NETWORK_FILE=/etc/systemd/network/69-eno4.network
394NETWORK_FILE_DROPINS=""
395DNS=8.8.8.8 8.8.4.4
396NTP=
397SIP=
398DOMAINS=
399ROUTE_DOMAINS=
400LLMNR=yes
401MDNS=no
402"###;
403
404 fn return_expected_interface_state() -> InterfaceState {
405 InterfaceState {
406 address_state: AddressState::routable,
407 admin_state: AdminState::configured,
408 carrier_state: CarrierState::carrier,
409 ipv4_address_state: AddressState::degraded,
410 ipv6_address_state: AddressState::routable,
411 name: "eth0".to_string(),
412 network_file: "/etc/systemd/network/69-eno4.network".to_string(),
413 oper_state: OperState::routable,
414 required_for_online: BoolState::True,
415 }
416 }
417
418 fn return_mock_int_name_hashmap() -> Option<HashMap<i32, String>> {
419 let mut h: HashMap<i32, String> = HashMap::new();
420 h.insert(2, String::from("eth0"));
421 h.insert(69, String::from("eth69"));
422 Some(h)
423 }
424
425 #[test]
426 fn test_parse_interface_stats() {
427 assert_eq!(
428 return_expected_interface_state(),
429 parse_interface_stats(
430 MOCK_INTERFACE_STATE.to_string(),
431 2,
432 &return_mock_int_name_hashmap().expect("Failed to get a mock int name hashmap"),
433 )
434 .expect("Failed to parse interface stats"),
435 );
436 }
437
438 #[test]
439 fn test_parse_interface_stats_json() {
440 let expected_interface_state_json = r###"{"address_state":3,"admin_state":4,"carrier_state":5,"ipv4_address_state":2,"ipv6_address_state":3,"name":"","network_file":"/etc/systemd/network/69-eno4.network","oper_state":9,"required_for_online":1}"###;
442 let stats =
443 parse_interface_stats(MOCK_INTERFACE_STATE.to_string(), 0, &HashMap::new()).unwrap();
444 let stats_json = serde_json::to_string(&stats).unwrap();
445 assert_eq!(expected_interface_state_json.to_string(), stats_json);
446 }
447
448 #[tokio::test]
449 async fn test_parse_interface_state_files() -> Result<()> {
450 let expected_files = NetworkdState {
451 interfaces_state: vec![return_expected_interface_state()],
452 managed_interfaces: 1,
453 };
454
455 let temp_dir = tempdir()?;
456 let file_path = temp_dir.path().join("2");
458 let mut state_file = File::create(file_path)?;
459 writeln!(state_file, "{}", MOCK_INTERFACE_STATE)?;
460
461 let path = PathBuf::from(temp_dir.path());
462 assert_eq!(
463 expected_files,
464 parse_interface_state_files(
465 &path,
466 return_mock_int_name_hashmap(),
467 None, )
469 .await
470 .expect("Problem with parsing interface stte files")
471 );
472 Ok(())
473 }
474
475 #[test]
476 fn test_enums_to_ints() -> Result<()> {
477 assert_eq!(3, AddressState::routable as u64);
478 let carrier_state_int: u8 = u8::from(CarrierState::degraded_carrier);
479 assert_eq!(4, carrier_state_int);
480 assert_eq!(1, BoolState::True as i64);
481 let bool_state_false_int: u8 = u8::from(BoolState::False);
482 assert_eq!(0, bool_state_false_int);
483
484 Ok(())
485 }
486}