1use std::{
2 fs::read_to_string,
3 net::SocketAddr,
4 os::unix::fs::PermissionsExt,
5 path::{Path, PathBuf},
6};
7
8use log::warn;
9use serde::{Deserialize, Deserializer};
10use statime::{
11 config::{ClockIdentity, DelayMechanism, PtpMinorVersion},
12 time::{Duration, Interval},
13};
14use timestamped_socket::interface::InterfaceName;
15
16use crate::tracing::LogLevel;
17
18#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
19#[serde(rename_all = "kebab-case", deny_unknown_fields)]
20pub struct Config {
21 #[serde(default)]
22 pub loglevel: LogLevel,
23 #[serde(default = "default_sdo_id")]
24 pub sdo_id: u16,
25 #[serde(default = "default_domain")]
26 pub domain: u8,
27 #[serde(default = "default_slave_only")]
28 pub slave_only: bool,
29 #[serde(default, deserialize_with = "deserialize_clock_identity")]
30 pub identity: Option<ClockIdentity>,
31 #[serde(default = "default_priority1")]
32 pub priority1: u8,
33 #[serde(default = "default_priority2")]
34 pub priority2: u8,
35 #[serde(default)]
36 pub path_trace: bool,
37 #[serde(rename = "port")]
38 pub ports: Vec<PortConfig>,
39 #[serde(default)]
40 pub observability: ObservabilityConfig,
41 #[serde(default)]
42 pub virtual_system_clock: bool,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Default)]
46pub enum HardwareClock {
47 #[default]
50 Auto,
51 Required,
55 Specific(u32),
57 None,
59}
60
61impl<'de> Deserialize<'de> for HardwareClock {
62 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63 where
64 D: Deserializer<'de>,
65 {
66 use serde::de::Error;
67
68 let raw: String = Deserialize::deserialize(deserializer)?;
69
70 if raw == "auto" {
71 Ok(HardwareClock::Auto)
72 } else if raw == "required" {
73 Ok(HardwareClock::Required)
74 } else if raw == "none" {
75 Ok(HardwareClock::None)
76 } else {
77 let clock = raw
78 .parse()
79 .map_err(|e| D::Error::custom(format!("Invalid hardware clock: {}", e)))?;
80 Ok(HardwareClock::Specific(clock))
81 }
82 }
83}
84
85#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
86#[serde(rename_all = "kebab-case", deny_unknown_fields)]
87pub struct PortConfig {
88 pub interface: InterfaceName,
89 #[serde(default, deserialize_with = "deserialize_acceptable_master_list")]
90 pub acceptable_master_list: Option<Vec<ClockIdentity>>,
91 #[serde(default)]
92 pub hardware_clock: HardwareClock,
93 #[serde(default)]
94 pub network_mode: NetworkMode,
95 #[serde(default = "default_announce_interval")]
96 pub announce_interval: i8,
97 #[serde(default = "default_sync_interval")]
98 pub sync_interval: i8,
99 #[serde(default = "default_announce_receipt_timeout")]
100 pub announce_receipt_timeout: u8,
101 #[serde(default)]
102 pub master_only: bool,
103 #[serde(default = "default_delay_asymmetry")]
104 pub delay_asymmetry: i64,
105 #[serde(default)]
106 pub delay_mechanism: DelayType,
107 #[serde(default = "default_delay_interval")]
108 pub delay_interval: i8,
109 #[serde(
110 default = "default_minor_ptp_version",
111 deserialize_with = "deserialize_minor_version"
112 )]
113 pub minor_ptp_version: PtpMinorVersion,
114}
115
116fn deserialize_minor_version<'de, D>(deserializer: D) -> Result<PtpMinorVersion, D::Error>
117where
118 D: Deserializer<'de>,
119{
120 use serde::de::Error;
121 let raw: u8 = Deserialize::deserialize(deserializer)?;
122 raw.try_into().map_err(D::Error::custom)
123}
124
125fn deserialize_acceptable_master_list<'de, D>(
126 deserializer: D,
127) -> Result<Option<Vec<ClockIdentity>>, D::Error>
128where
129 D: Deserializer<'de>,
130{
131 use hex::FromHex;
132 use serde::de::Error;
133
134 let raw: Vec<String> = Deserialize::deserialize(deserializer)?;
135 let mut result = Vec::with_capacity(raw.len());
136
137 for identity in raw {
138 result.push(ClockIdentity(<[u8; 8]>::from_hex(identity).map_err(
139 |e| D::Error::custom(format!("Invalid clock identifier: {}", e)),
140 )?));
141 }
142
143 Ok(Some(result))
144}
145
146fn deserialize_clock_identity<'de, D>(deserializer: D) -> Result<Option<ClockIdentity>, D::Error>
147where
148 D: Deserializer<'de>,
149{
150 use hex::FromHex;
151 use serde::de::Error;
152 let raw: String = Deserialize::deserialize(deserializer)?;
153 Ok(Some(ClockIdentity(<[u8; 8]>::from_hex(raw).map_err(
154 |e| D::Error::custom(format!("Invalid clock identifier: {}", e)),
155 )?)))
156}
157
158impl From<PortConfig> for statime::config::PortConfig<Option<Vec<ClockIdentity>>> {
159 fn from(pc: PortConfig) -> Self {
160 Self {
161 acceptable_master_list: pc.acceptable_master_list,
162 announce_interval: Interval::from_log_2(pc.announce_interval),
163 sync_interval: Interval::from_log_2(pc.sync_interval),
164 announce_receipt_timeout: pc.announce_receipt_timeout,
165 master_only: pc.master_only,
166 delay_asymmetry: Duration::from_nanos(pc.delay_asymmetry),
167 delay_mechanism: match pc.delay_mechanism {
168 DelayType::E2E => DelayMechanism::E2E {
169 interval: Interval::from_log_2(pc.delay_interval),
170 },
171 DelayType::P2P => DelayMechanism::P2P {
172 interval: Interval::from_log_2(pc.delay_interval),
173 },
174 },
175 minor_ptp_version: pc.minor_ptp_version,
176 }
177 }
178}
179
180#[derive(Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq)]
181#[serde(rename_all = "lowercase")]
182pub enum NetworkMode {
183 #[default]
184 Ipv4,
185 Ipv6,
186 Ethernet,
187}
188
189#[derive(Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq)]
190#[serde(rename_all = "UPPERCASE")]
191pub enum DelayType {
192 #[default]
193 E2E,
194 P2P,
195}
196
197impl Config {
198 pub fn from_file(file: &Path) -> Result<Config, ConfigError> {
200 let meta = std::fs::metadata(file).map_err(ConfigError::Io)?;
201 let perm = meta.permissions();
202
203 if perm.mode() as libc::mode_t & libc::S_IWOTH != 0 {
204 warn!("Unrestricted config file permissions: Others can write.");
205 }
206
207 let contents = read_to_string(file).map_err(ConfigError::Io)?;
208 let config: Config = toml::de::from_str(&contents).map_err(ConfigError::Toml)?;
209 config.warn_when_unreasonable();
210 Ok(config)
211 }
212
213 pub fn warn_when_unreasonable(&self) {
215 if self.ports.is_empty() {
216 warn!("No ports configured.");
217 }
218
219 if self.ports.len() > 16 {
220 warn!("Too many ports are configured.");
221 }
222 }
223}
224
225#[derive(Debug)]
226pub enum ConfigError {
227 Io(std::io::Error),
228 Toml(toml::de::Error),
229}
230
231impl std::fmt::Display for ConfigError {
232 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233 match self {
234 ConfigError::Io(e) => writeln!(f, "io error while reading config: {e}"),
235 ConfigError::Toml(e) => writeln!(f, "config toml parsing error: {e}"),
236 }
237 }
238}
239
240impl std::error::Error for ConfigError {}
241
242fn default_domain() -> u8 {
243 0
244}
245
246fn default_sdo_id() -> u16 {
247 0x000
248}
249
250fn default_slave_only() -> bool {
251 false
252}
253
254fn default_announce_interval() -> i8 {
255 1
256}
257
258fn default_sync_interval() -> i8 {
259 0
260}
261
262fn default_announce_receipt_timeout() -> u8 {
263 3
264}
265
266fn default_priority1() -> u8 {
267 128
268}
269
270fn default_priority2() -> u8 {
271 128
272}
273
274fn default_delay_asymmetry() -> i64 {
275 0
276}
277
278fn default_delay_interval() -> i8 {
279 0
280}
281
282fn default_minor_ptp_version() -> PtpMinorVersion {
283 PtpMinorVersion::One
284}
285
286#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
287#[serde(rename_all = "kebab-case", deny_unknown_fields)]
288pub struct ObservabilityConfig {
289 #[serde(default)]
290 pub observation_path: Option<PathBuf>,
291 #[serde(default = "default_observation_permissions")]
292 pub observation_permissions: u32,
293 #[serde(default = "default_metrics_exporter_listen")]
294 pub metrics_exporter_listen: SocketAddr,
295}
296
297impl Default for ObservabilityConfig {
298 fn default() -> Self {
299 Self {
300 observation_path: Default::default(),
301 observation_permissions: default_observation_permissions(),
302 metrics_exporter_listen: default_metrics_exporter_listen(),
303 }
304 }
305}
306
307const fn default_observation_permissions() -> u32 {
308 0o666
309}
310
311fn default_metrics_exporter_listen() -> SocketAddr {
312 "127.0.0.1:9975".parse().unwrap()
313}
314
315#[cfg(test)]
316mod tests {
317 use std::str::FromStr;
318
319 use statime::config::PtpMinorVersion;
320 use timestamped_socket::interface::InterfaceName;
321
322 use crate::{
323 config::{HardwareClock, ObservabilityConfig},
324 tracing::LogLevel,
325 };
326
327 #[test]
329 fn minimal_config() {
330 const MINIMAL_CONFIG: &str = r#"
331[[port]]
332interface = "enp0s31f6"
333"#;
334
335 let expected_port = crate::config::PortConfig {
336 interface: InterfaceName::from_str("enp0s31f6").unwrap(),
337 acceptable_master_list: None,
338 hardware_clock: HardwareClock::Auto,
339 network_mode: crate::config::NetworkMode::Ipv4,
340 announce_interval: 1,
341 sync_interval: 0,
342 announce_receipt_timeout: 3,
343 master_only: false,
344 delay_asymmetry: 0,
345 delay_mechanism: crate::config::DelayType::E2E,
346 delay_interval: 0,
347 minor_ptp_version: PtpMinorVersion::One,
348 };
349
350 let expected = crate::config::Config {
351 loglevel: LogLevel::Info,
352 sdo_id: 0x000,
353 domain: 0,
354 slave_only: false,
355 identity: None,
356 priority1: 128,
357 priority2: 128,
358 path_trace: false,
359 ports: vec![expected_port],
360 observability: ObservabilityConfig::default(),
361 virtual_system_clock: false,
362 };
363
364 let actual = toml::from_str(MINIMAL_CONFIG).unwrap();
365
366 assert_eq!(expected, actual);
367 }
368}