Skip to main content

network_device/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3
4//! A dependency-free, strongly typed eDSL and base contracts for network
5//! device operations and declarative infrastructure resources.
6//!
7//! The imperative API runs operations directly through [`Driver`] and
8//! [`Execute`]. The declarative API describes desired resources for an
9//! [`IacBackend`]. These models are intentionally separate.
10//!
11//! # Direct execution
12//!
13//! ```
14//! use network_device::{
15//!     AdministrativeState, Driver, Execute, InterfaceName, Run,
16//!     SetInterfaceState, operation,
17//! };
18//! use std::convert::Infallible;
19//! use std::future::Future;
20//!
21//! struct Device;
22//!
23//! impl Driver for Device {
24//!     type Error = Infallible;
25//! }
26//!
27//! impl Execute<SetInterfaceState> for Device {
28//!     type Output = ();
29//!
30//!     fn execute<'a>(
31//!         &'a self,
32//!         _operation: SetInterfaceState,
33//!     ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send + 'a {
34//!         async { Ok(()) }
35//!     }
36//! }
37//!
38//! # async fn example() -> Result<(), Infallible> {
39//! let name = InterfaceName::try_from("Ethernet1/1").expect("valid interface name");
40//! operation(SetInterfaceState::new(name, AdministrativeState::Enabled))
41//!     .run(&Device)
42//!     .await?;
43//! # Ok(())
44//! # }
45//! ```
46//!
47//! # IaC backend
48//!
49//! ```
50//! use network_device::{
51//!     AdministrativeState, IacBackend, InterfaceConfiguration, InterfaceName,
52//! };
53//! use std::convert::Infallible;
54//! use std::future::Future;
55//!
56//! struct LocalBackend;
57//!
58//! impl IacBackend<InterfaceConfiguration> for LocalBackend {
59//!     type Error = Infallible;
60//!     type Plan = usize;
61//!     type ApplyResult = ();
62//!
63//!     fn plan<'a>(
64//!         &'a self,
65//!         desired: &'a InterfaceConfiguration,
66//!     ) -> impl Future<Output = Result<Self::Plan, Self::Error>> + Send + 'a {
67//!         async move { Ok(usize::from(desired.administrative_state.is_some())) }
68//!     }
69//!
70//!     fn apply<'a>(
71//!         &'a self,
72//!         _desired: &'a InterfaceConfiguration,
73//!     ) -> impl Future<Output = Result<Self::ApplyResult, Self::Error>> + Send + 'a {
74//!         async { Ok(()) }
75//!     }
76//! }
77//!
78//! # async fn example() -> Result<(), Infallible> {
79//! let desired = InterfaceConfiguration::new(
80//!     InterfaceName::try_from("Ethernet1/1").expect("valid interface name"),
81//! )
82//! .administrative_state(AdministrativeState::Enabled);
83//! let backend = LocalBackend;
84//! let _plan = backend.plan(&desired).await?;
85//! backend.apply(&desired).await?;
86//! # Ok(())
87//! # }
88//! ```
89
90mod iac;
91mod interface;
92mod operation;
93mod routing;
94
95pub use iac::{IacBackend, InterfaceConfiguration, StaticRouteConfiguration};
96pub use interface::{
97    InterfaceName, InterfaceNameError, IpPrefix, IpPrefixError, IpPrefixParseError,
98};
99pub use operation::{
100    AdministrativeState, Driver, EnsureStaticRoute, Execute, OperationStep, RemoveStaticRoute, Run,
101    Sequence, SetInterfaceDescription, SetInterfaceState, operation,
102};
103pub use routing::{StaticRoute, StaticRouteError};
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use std::convert::Infallible;
109    use std::future::Future;
110    use std::net::{IpAddr, Ipv4Addr};
111    use std::pin::pin;
112    use std::sync::atomic::{AtomicBool, Ordering};
113    use std::sync::mpsc::{self, Receiver, Sender};
114    use std::task::{Context, Poll, Waker};
115
116    fn block_on<F: Future>(future: F) -> F::Output {
117        let mut future = pin!(future);
118        let waker = Waker::noop();
119        let mut context = Context::from_waker(waker);
120        match future.as_mut().poll(&mut context) {
121            Poll::Ready(output) => output,
122            Poll::Pending => panic!("test future unexpectedly yielded"),
123        }
124    }
125
126    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
127    enum TestError {
128        Failed,
129    }
130
131    impl std::fmt::Display for TestError {
132        fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133            formatter.write_str("test operation failed")
134        }
135    }
136
137    impl std::error::Error for TestError {}
138
139    struct TestDriver {
140        events: Sender<&'static str>,
141        fail_first: AtomicBool,
142    }
143
144    impl Driver for TestDriver {
145        type Error = TestError;
146    }
147
148    struct FirstOperation;
149    struct SecondOperation;
150
151    impl Execute<FirstOperation> for TestDriver {
152        type Output = u8;
153
154        #[allow(clippy::manual_async_fn)]
155        fn execute<'a>(
156            &'a self,
157            _operation: FirstOperation,
158        ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send + 'a {
159            async move {
160                self.events.send("first").expect("receiver is alive");
161                if self.fail_first.load(Ordering::Relaxed) {
162                    Err(TestError::Failed)
163                } else {
164                    Ok(7)
165                }
166            }
167        }
168    }
169
170    impl Execute<SecondOperation> for TestDriver {
171        type Output = String;
172
173        #[allow(clippy::manual_async_fn)]
174        fn execute<'a>(
175            &'a self,
176            _operation: SecondOperation,
177        ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send + 'a {
178            async move {
179                self.events.send("second").expect("receiver is alive");
180                Ok(String::from("completed"))
181            }
182        }
183    }
184
185    fn test_driver(fail_first: bool) -> (TestDriver, Receiver<&'static str>) {
186        let (events, receiver) = mpsc::channel();
187        (
188            TestDriver {
189                events,
190                fail_first: AtomicBool::new(fail_first),
191            },
192            receiver,
193        )
194    }
195
196    #[test]
197    fn operation_sequence_runs_in_order_and_returns_second_output() {
198        let (driver, events) = test_driver(false);
199        let output = block_on(
200            operation(FirstOperation)
201                .then(operation(SecondOperation))
202                .run(&driver),
203        )
204        .unwrap();
205
206        assert_eq!(output, "completed");
207        assert_eq!(events.try_iter().collect::<Vec<_>>(), ["first", "second"]);
208    }
209
210    #[test]
211    fn operation_sequence_stops_after_first_error() {
212        let (driver, events) = test_driver(true);
213        let result = block_on(
214            operation(FirstOperation)
215                .then(operation(SecondOperation))
216                .run(&driver),
217        );
218
219        assert_eq!(result, Err(TestError::Failed));
220        assert_eq!(events.try_iter().collect::<Vec<_>>(), ["first"]);
221    }
222
223    struct CustomOperation(&'static str);
224
225    impl Execute<CustomOperation> for TestDriver {
226        type Output = usize;
227
228        #[allow(clippy::manual_async_fn)]
229        fn execute<'a>(
230            &'a self,
231            operation: CustomOperation,
232        ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send + 'a {
233            async move {
234                self.events.send(operation.0).expect("receiver is alive");
235                Ok(operation.0.len())
236            }
237        }
238    }
239
240    #[test]
241    fn downstream_custom_operation_is_supported_without_core_changes() {
242        let (driver, events) = test_driver(false);
243        let result = block_on(operation(CustomOperation("extension")).run(&driver)).unwrap();
244
245        assert_eq!(result, "extension".len());
246        assert_eq!(events.try_iter().collect::<Vec<_>>(), ["extension"]);
247    }
248
249    struct TestIacBackend;
250
251    impl IacBackend<StaticRouteConfiguration> for TestIacBackend {
252        type Error = Infallible;
253        type Plan = bool;
254        type ApplyResult = usize;
255
256        #[allow(clippy::manual_async_fn)]
257        fn plan<'a>(
258            &'a self,
259            desired: &'a StaticRouteConfiguration,
260        ) -> impl Future<Output = Result<Self::Plan, Self::Error>> + Send + 'a {
261            async move { Ok(desired.route.destination().prefix_length() == 24) }
262        }
263
264        #[allow(clippy::manual_async_fn)]
265        fn apply<'a>(
266            &'a self,
267            desired: &'a StaticRouteConfiguration,
268        ) -> impl Future<Output = Result<Self::ApplyResult, Self::Error>> + Send + 'a {
269            async move { Ok(usize::from(desired.route.destination().prefix_length())) }
270        }
271    }
272
273    #[test]
274    fn iac_backend_can_plan_and_apply_a_standard_resource() {
275        let destination = "192.0.2.0/24".parse::<IpPrefix>().unwrap();
276        let route = StaticRoute::new(destination, IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1))).unwrap();
277        let desired = StaticRouteConfiguration::new(route);
278        let backend = TestIacBackend;
279
280        assert!(block_on(backend.plan(&desired)).unwrap());
281        assert_eq!(block_on(backend.apply(&desired)).unwrap(), 24);
282    }
283}