statime_linux/config/
mod.rs

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    /// Automatically use the (default) hardware clock for the interface
48    /// specified if available
49    #[default]
50    Auto,
51    /// Require the use of the (default) hardware clock for the interface
52    /// specified. If a hardware clock is not available, statime will refuse
53    /// to start.
54    Required,
55    /// Use the specified hardware clock
56    Specific(u32),
57    /// Do not use a hardware clock
58    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    /// Parse config from file
199    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    /// Warns about unreasonable config values
214    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    // Minimal amount of config results in default values
328    #[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}