Skip to main content

async_snmp/handler/
traits.rs

1//! MibHandler trait and related types.
2
3use std::future::Future;
4use std::pin::Pin;
5
6use crate::oid::Oid;
7use crate::value::Value;
8
9use super::{GetNextResult, GetResult, RequestContext, SetResult};
10
11/// Type alias for boxed async return type (dyn-compatible).
12///
13/// This type is required because async trait methods cannot be object-safe.
14/// All handler methods return `BoxFuture` to allow handlers to be stored
15/// as trait objects in the agent.
16///
17/// # Example
18///
19/// ```rust
20/// use async_snmp::handler::{BoxFuture, GetResult};
21///
22/// fn example_async_fn<'a>(value: &'a i32) -> BoxFuture<'a, GetResult> {
23///     Box::pin(async move {
24///         // Async work here
25///         GetResult::Value(async_snmp::Value::Integer(*value))
26///     })
27/// }
28/// ```
29pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
30
31/// Handler for SNMP MIB operations.
32///
33/// Implement this trait to provide values for a subtree of OIDs.
34/// Register handlers with [`AgentBuilder::handler()`](crate::agent::AgentBuilder::handler)
35/// using a prefix OID.
36///
37/// # Required Methods
38///
39/// - [`get`](MibHandler::get): Handle GET requests for specific OIDs
40/// - [`get_next`](MibHandler::get_next): Handle GETNEXT/GETBULK requests
41///
42/// # Optional Methods
43///
44/// - [`test_set`](MibHandler::test_set): Validate SET operations (default: read-only)
45/// - [`commit_set`](MibHandler::commit_set): Apply SET operations (default: read-only)
46/// - [`undo_set`](MibHandler::undo_set): Rollback failed SET operations
47/// - [`free_set`](MibHandler::free_set): Cleanup resources on test failure
48/// - [`handles`](MibHandler::handles): Custom OID matching logic
49///
50/// # GET Implementation
51///
52/// The [`get`](MibHandler::get) method should return:
53/// - [`GetResult::Value`] if the OID exists and has a value
54/// - [`GetResult::NoSuchObject`] if the object type is not implemented
55/// - [`GetResult::NoSuchInstance`] if the object exists but this instance doesn't
56///
57/// # GETNEXT and Lexicographic Ordering
58///
59/// The [`get_next`](MibHandler::get_next) method must return the lexicographically
60/// next OID after the requested one. OIDs are compared arc-by-arc as unsigned integers.
61/// For example: `1.3.6.1.2` < `1.3.6.1.2.1` < `1.3.6.1.3`.
62///
63/// Key considerations:
64/// - The returned OID must be strictly greater than the input OID
65/// - GETBULK uses GETNEXT repeatedly, so efficient implementation matters
66/// - Use [`OidTable`](super::OidTable) to simplify sorted OID management
67///
68/// # SET Two-Phase Commit (RFC 3416)
69///
70/// SET operations use a multi-phase protocol modeled after net-snmp's
71/// RESERVE1/RESERVE2/ACTION/COMMIT/FREE/UNDO phases:
72///
73/// 1. **Test phase**: [`test_set`](MibHandler::test_set) is called for ALL varbinds
74///    before any commits. If any test fails, [`free_set`](MibHandler::free_set)
75///    is called for all previously successful varbinds (in reverse order) to
76///    release resources allocated during the test phase.
77///
78/// 2. **Commit phase**: [`commit_set`](MibHandler::commit_set) is called for each
79///    varbind in order. If a commit fails, [`undo_set`](MibHandler::undo_set) is
80///    called for all previously committed varbinds (in reverse order).
81///
82/// By default, handlers are read-only and return [`SetResult::NotWritable`].
83///
84/// # Bounds
85///
86/// The `'static` bound is required because handlers are stored as
87/// `Arc<dyn MibHandler>` within the agent. This allows the agent to
88/// hold handlers for its entire lifetime without lifetime annotations.
89/// In practice, most handlers naturally satisfy this bound.
90///
91/// # Thread Safety
92///
93/// Handlers must be `Send + Sync` because the agent may process
94/// requests concurrently from multiple tasks.
95///
96/// # Example: Read-Only Handler
97///
98/// ```rust
99/// use async_snmp::handler::{MibHandler, RequestContext, GetResult, GetNextResult, BoxFuture};
100/// use async_snmp::{Oid, Value, VarBind, oid};
101///
102/// struct SystemInfoHandler {
103///     sys_descr: String,
104///     sys_uptime: u32,
105/// }
106///
107/// impl MibHandler for SystemInfoHandler {
108///     fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
109///         Box::pin(async move {
110///             // sysDescr.0
111///             if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 1, 0) {
112///                 return GetResult::Value(Value::OctetString(self.sys_descr.clone().into()));
113///             }
114///             // sysUpTime.0
115///             if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 3, 0) {
116///                 return GetResult::Value(Value::TimeTicks(self.sys_uptime));
117///             }
118///             GetResult::NoSuchObject
119///         })
120///     }
121///
122///     fn get_next<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetNextResult> {
123///         Box::pin(async move {
124///             let sys_descr = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
125///             let sys_uptime = oid!(1, 3, 6, 1, 2, 1, 1, 3, 0);
126///
127///             // Return the next OID in lexicographic order
128///             if oid < &sys_descr {
129///                 return GetNextResult::Value(VarBind::new(
130///                     sys_descr,
131///                     Value::OctetString("My System".into())
132///                 ));
133///             }
134///             if oid < &sys_uptime {
135///                 return GetNextResult::Value(VarBind::new(
136///                     sys_uptime,
137///                     Value::TimeTicks(12345)
138///                 ));
139///             }
140///             GetNextResult::EndOfMibView
141///         })
142///     }
143/// }
144/// ```
145///
146/// # Example: Writable Handler
147///
148/// ```rust
149/// use async_snmp::handler::{
150///     MibHandler, RequestContext, GetResult, GetNextResult, SetResult, BoxFuture
151/// };
152/// use async_snmp::{Oid, Value, VarBind, oid};
153/// use std::sync::atomic::{AtomicI32, Ordering};
154///
155/// struct WritableHandler {
156///     counter: AtomicI32,
157/// }
158///
159/// impl MibHandler for WritableHandler {
160///     fn get<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult> {
161///         Box::pin(async move {
162///             if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
163///                 return GetResult::Value(Value::Integer(
164///                     self.counter.load(Ordering::Relaxed)
165///                 ));
166///             }
167///             GetResult::NoSuchObject
168///         })
169///     }
170///
171///     fn get_next<'a>(&'a self, _ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetNextResult> {
172///         Box::pin(async move {
173///             let my_oid = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
174///             if oid < &my_oid {
175///                 return GetNextResult::Value(VarBind::new(
176///                     my_oid,
177///                     Value::Integer(self.counter.load(Ordering::Relaxed))
178///                 ));
179///             }
180///             GetNextResult::EndOfMibView
181///         })
182///     }
183///
184///     fn test_set<'a>(
185///         &'a self,
186///         _ctx: &'a RequestContext,
187///         oid: &'a Oid,
188///         value: &'a Value,
189///     ) -> BoxFuture<'a, SetResult> {
190///         Box::pin(async move {
191///             if oid != &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
192///                 return SetResult::NotWritable;
193///             }
194///             // Validate the value type
195///             match value {
196///                 Value::Integer(_) => SetResult::Ok,
197///                 _ => SetResult::WrongType,
198///             }
199///         })
200///     }
201///
202///     fn commit_set<'a>(
203///         &'a self,
204///         _ctx: &'a RequestContext,
205///         _oid: &'a Oid,
206///         value: &'a Value,
207///     ) -> BoxFuture<'a, SetResult> {
208///         Box::pin(async move {
209///             if let Value::Integer(v) = value {
210///                 self.counter.store(*v, Ordering::Relaxed);
211///                 SetResult::Ok
212///             } else {
213///                 SetResult::CommitFailed
214///             }
215///         })
216///     }
217/// }
218/// ```
219pub trait MibHandler: Send + Sync + 'static {
220    /// Handle a GET request for a specific OID.
221    ///
222    /// Return [`GetResult::Value`] if the OID exists, [`GetResult::NoSuchObject`]
223    /// if the object type is not implemented, or [`GetResult::NoSuchInstance`]
224    /// if the object type exists but this specific instance doesn't.
225    ///
226    /// See [`GetResult`] documentation for details on when to use each variant.
227    fn get<'a>(&'a self, ctx: &'a RequestContext, oid: &'a Oid) -> BoxFuture<'a, GetResult>;
228
229    /// Handle a GETNEXT request.
230    ///
231    /// Return [`GetNextResult::Value`] with the lexicographically next OID and value
232    /// after `oid`, or [`GetNextResult::EndOfMibView`] if there are no more OIDs
233    /// in this handler's subtree.
234    fn get_next<'a>(
235        &'a self,
236        ctx: &'a RequestContext,
237        oid: &'a Oid,
238    ) -> BoxFuture<'a, GetNextResult>;
239
240    /// Test if a SET operation would succeed (phase 1 of two-phase commit).
241    ///
242    /// Called for ALL varbinds before any commits. Must NOT modify state.
243    /// Return `SetResult::Ok` if the SET would succeed, or an appropriate
244    /// error otherwise.
245    ///
246    /// Default implementation returns `NotWritable` (read-only handler).
247    fn test_set<'a>(
248        &'a self,
249        _ctx: &'a RequestContext,
250        _oid: &'a Oid,
251        _value: &'a Value,
252    ) -> BoxFuture<'a, SetResult> {
253        Box::pin(async { SetResult::NotWritable })
254    }
255
256    /// Commit a SET operation (phase 2 of two-phase commit).
257    ///
258    /// Only called after ALL `test_set` calls succeed. Should apply the change.
259    /// If this fails, `undo_set` will be called for all previously committed
260    /// varbinds in this request.
261    ///
262    /// Default implementation returns `NotWritable` (read-only handler).
263    fn commit_set<'a>(
264        &'a self,
265        _ctx: &'a RequestContext,
266        _oid: &'a Oid,
267        _value: &'a Value,
268    ) -> BoxFuture<'a, SetResult> {
269        Box::pin(async { SetResult::NotWritable })
270    }
271
272    /// Undo a committed SET operation (rollback on partial failure).
273    ///
274    /// Called if a later `commit_set` fails. Should restore the previous value.
275    /// This is best-effort: if undo fails, log a warning but continue.
276    ///
277    /// Default implementation does nothing (no rollback support).
278    fn undo_set<'a>(
279        &'a self,
280        _ctx: &'a RequestContext,
281        _oid: &'a Oid,
282        _value: &'a Value,
283    ) -> BoxFuture<'a, SetResult> {
284        Box::pin(async { SetResult::Ok })
285    }
286
287    /// Free resources allocated during test_set (cleanup on test failure).
288    ///
289    /// Called for varbinds whose test_set succeeded when a later varbind's
290    /// test_set fails. This allows handlers to release any resources
291    /// (locks, temporary allocations) acquired during the test phase.
292    ///
293    /// Called in reverse order, matching the undo_set convention.
294    ///
295    /// Default implementation does nothing.
296    fn free_set<'a>(
297        &'a self,
298        _ctx: &'a RequestContext,
299        _oid: &'a Oid,
300        _value: &'a Value,
301    ) -> BoxFuture<'a, ()> {
302        Box::pin(async {})
303    }
304
305    /// Check if this handler handles the given OID.
306    ///
307    /// Default implementation returns true if the OID starts with
308    /// the registered prefix (i.e., the OID is within this handler's subtree).
309    /// Override for more complex matching.
310    ///
311    /// This method is used to route GET and SET requests. GETNEXT and GETBULK
312    /// consult all handlers regardless of this method.
313    fn handles(&self, registered_prefix: &Oid, oid: &Oid) -> bool {
314        oid.starts_with(registered_prefix)
315    }
316}