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, HandlerResult, 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, HandlerResult};
21///
22/// fn example_async_fn<'a>(value: &'a i32) -> BoxFuture<'a, HandlerResult<GetResult>> {
23///     Box::pin(async move {
24///         // Async work here
25///         Ok(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/// - `Ok(`[`GetResult::Value`]`)` if the OID exists and has a value
54/// - `Ok(`[`GetResult::NoSuchObject`]`)` if the object type is not implemented
55/// - `Ok(`[`GetResult::NoSuchInstance`]`)` if the object exists but this instance doesn't
56/// - `Err(`[`HandlerError`](super::HandlerError)`)` if the handler failed to
57///   determine an answer (backing store unreachable, hardware fault, ...)
58///
59/// # GETNEXT and Lexicographic Ordering
60///
61/// The [`get_next`](MibHandler::get_next) method must return the lexicographically
62/// next OID after the requested one. OIDs are compared arc-by-arc as unsigned integers.
63/// For example: `1.3.6.1.2` < `1.3.6.1.2.1` < `1.3.6.1.3`.
64///
65/// Key considerations:
66/// - The returned OID must be strictly greater than the input OID
67/// - GETBULK uses GETNEXT repeatedly, so efficient implementation matters
68/// - Use [`OidTable`](super::OidTable) to simplify sorted OID management
69///
70/// # Error Handling
71///
72/// Both methods return [`HandlerResult`], so `?` works on any error type
73/// implementing [`std::error::Error`]. Return `Err` only for *processing
74/// failures* — "I could not find out" — never for "the object does not
75/// exist", which is expressed by the `Ok` variants above. On `Err`, the
76/// agent responds to the whole request with `genErr` and the error-index of
77/// the failing variable binding (RFC 3416 Section 4.2.1), and logs the
78/// error; the message is never sent to the manager.
79///
80/// # SET Two-Phase Commit (RFC 3416)
81///
82/// SET operations use a multi-phase protocol modeled after net-snmp's
83/// RESERVE1/RESERVE2/ACTION/COMMIT/FREE/UNDO phases:
84///
85/// 1. **Test phase**: [`test_set`](MibHandler::test_set) is called for ALL varbinds
86///    before any commits. If any test fails, [`free_set`](MibHandler::free_set)
87///    is called for all previously successful varbinds (in reverse order) to
88///    release resources allocated during the test phase.
89///
90/// 2. **Commit phase**: [`commit_set`](MibHandler::commit_set) is called for each
91///    varbind in order. If a commit fails, [`undo_set`](MibHandler::undo_set) is
92///    called for all previously committed varbinds (in reverse order).
93///
94/// By default, handlers are read-only and return [`SetResult::NotWritable`].
95///
96/// # Bounds
97///
98/// The `'static` bound is required because handlers are stored as
99/// `Arc<dyn MibHandler>` within the agent. This allows the agent to
100/// hold handlers for its entire lifetime without lifetime annotations.
101/// In practice, most handlers naturally satisfy this bound.
102///
103/// # Thread Safety
104///
105/// Handlers must be `Send + Sync` because the agent may process
106/// requests concurrently from multiple tasks.
107///
108/// # Example: Read-Only Handler
109///
110/// ```rust
111/// use async_snmp::handler::{
112///     MibHandler, RequestContext, GetResult, GetNextResult, HandlerResult, BoxFuture,
113/// };
114/// use async_snmp::{Oid, Value, VarBind, oid};
115///
116/// struct SystemInfoHandler {
117///     sys_descr: String,
118///     sys_uptime: u32,
119/// }
120///
121/// impl MibHandler for SystemInfoHandler {
122///     fn get<'a>(
123///         &'a self,
124///         _ctx: &'a RequestContext,
125///         oid: &'a Oid,
126///     ) -> BoxFuture<'a, HandlerResult<GetResult>> {
127///         Box::pin(async move {
128///             // sysDescr.0
129///             if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 1, 0) {
130///                 return Ok(GetResult::Value(Value::OctetString(self.sys_descr.clone().into())));
131///             }
132///             // sysUpTime.0
133///             if oid == &oid!(1, 3, 6, 1, 2, 1, 1, 3, 0) {
134///                 return Ok(GetResult::Value(Value::TimeTicks(self.sys_uptime)));
135///             }
136///             Ok(GetResult::NoSuchObject)
137///         })
138///     }
139///
140///     fn get_next<'a>(
141///         &'a self,
142///         _ctx: &'a RequestContext,
143///         oid: &'a Oid,
144///     ) -> BoxFuture<'a, HandlerResult<GetNextResult>> {
145///         Box::pin(async move {
146///             let sys_descr = oid!(1, 3, 6, 1, 2, 1, 1, 1, 0);
147///             let sys_uptime = oid!(1, 3, 6, 1, 2, 1, 1, 3, 0);
148///
149///             // Return the next OID in lexicographic order
150///             if oid < &sys_descr {
151///                 return Ok(GetNextResult::Value(VarBind::new(
152///                     sys_descr,
153///                     Value::OctetString("My System".into())
154///                 )));
155///             }
156///             if oid < &sys_uptime {
157///                 return Ok(GetNextResult::Value(VarBind::new(
158///                     sys_uptime,
159///                     Value::TimeTicks(12345)
160///                 )));
161///             }
162///             Ok(GetNextResult::EndOfMibView)
163///         })
164///     }
165/// }
166/// ```
167///
168/// # Example: Writable Handler
169///
170/// ```rust
171/// use async_snmp::handler::{
172///     MibHandler, RequestContext, GetResult, GetNextResult, HandlerResult, SetResult, BoxFuture
173/// };
174/// use async_snmp::{Oid, Value, VarBind, oid};
175/// use std::sync::atomic::{AtomicI32, Ordering};
176///
177/// struct WritableHandler {
178///     counter: AtomicI32,
179/// }
180///
181/// impl MibHandler for WritableHandler {
182///     fn get<'a>(
183///         &'a self,
184///         _ctx: &'a RequestContext,
185///         oid: &'a Oid,
186///     ) -> BoxFuture<'a, HandlerResult<GetResult>> {
187///         Box::pin(async move {
188///             if oid == &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
189///                 return Ok(GetResult::Value(Value::Integer(
190///                     self.counter.load(Ordering::Relaxed)
191///                 )));
192///             }
193///             Ok(GetResult::NoSuchObject)
194///         })
195///     }
196///
197///     fn get_next<'a>(
198///         &'a self,
199///         _ctx: &'a RequestContext,
200///         oid: &'a Oid,
201///     ) -> BoxFuture<'a, HandlerResult<GetNextResult>> {
202///         Box::pin(async move {
203///             let my_oid = oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0);
204///             if oid < &my_oid {
205///                 return Ok(GetNextResult::Value(VarBind::new(
206///                     my_oid,
207///                     Value::Integer(self.counter.load(Ordering::Relaxed))
208///                 )));
209///             }
210///             Ok(GetNextResult::EndOfMibView)
211///         })
212///     }
213///
214///     fn test_set<'a>(
215///         &'a self,
216///         _ctx: &'a RequestContext,
217///         oid: &'a Oid,
218///         value: &'a Value,
219///     ) -> BoxFuture<'a, SetResult> {
220///         Box::pin(async move {
221///             if oid != &oid!(1, 3, 6, 1, 4, 1, 99999, 1, 0) {
222///                 return SetResult::NotWritable;
223///             }
224///             // Validate the value type
225///             match value {
226///                 Value::Integer(_) => SetResult::Ok,
227///                 _ => SetResult::WrongType,
228///             }
229///         })
230///     }
231///
232///     fn commit_set<'a>(
233///         &'a self,
234///         _ctx: &'a RequestContext,
235///         _oid: &'a Oid,
236///         value: &'a Value,
237///     ) -> BoxFuture<'a, SetResult> {
238///         Box::pin(async move {
239///             if let Value::Integer(v) = value {
240///                 self.counter.store(*v, Ordering::Relaxed);
241///                 SetResult::Ok
242///             } else {
243///                 SetResult::CommitFailed
244///             }
245///         })
246///     }
247/// }
248/// ```
249pub trait MibHandler: Send + Sync + 'static {
250    /// Handle a GET request for a specific OID.
251    ///
252    /// Return `Ok(`[`GetResult::Value`]`)` if the OID exists,
253    /// `Ok(`[`GetResult::NoSuchObject`]`)` if the object type is not implemented,
254    /// or `Ok(`[`GetResult::NoSuchInstance`]`)` if the object type exists but
255    /// this specific instance doesn't.
256    ///
257    /// Return `Err(`[`HandlerError`](super::HandlerError)`)` only when the
258    /// handler failed to determine an answer (e.g. its backing store is
259    /// unreachable); the agent then responds to the whole request with
260    /// `genErr` per RFC 3416 Section 4.2.1.
261    ///
262    /// See [`GetResult`] documentation for details on when to use each variant.
263    fn get<'a>(
264        &'a self,
265        ctx: &'a RequestContext,
266        oid: &'a Oid,
267    ) -> BoxFuture<'a, HandlerResult<GetResult>>;
268
269    /// Handle a GETNEXT request.
270    ///
271    /// Return `Ok(`[`GetNextResult::Value`]`)` with the lexicographically next
272    /// OID and value after `oid`, or `Ok(`[`GetNextResult::EndOfMibView`]`)`
273    /// if there are no more OIDs in this handler's subtree.
274    ///
275    /// Return `Err(`[`HandlerError`](super::HandlerError)`)` only when the
276    /// handler failed to determine an answer; the agent then responds to the
277    /// whole request (including GETBULK) with `genErr` per RFC 3416
278    /// Section 4.2.1.
279    fn get_next<'a>(
280        &'a self,
281        ctx: &'a RequestContext,
282        oid: &'a Oid,
283    ) -> BoxFuture<'a, HandlerResult<GetNextResult>>;
284
285    /// Test if a SET operation would succeed (phase 1 of two-phase commit).
286    ///
287    /// Called for ALL varbinds before any commits. Must NOT modify state.
288    /// Return `SetResult::Ok` if the SET would succeed, or an appropriate
289    /// error otherwise.
290    ///
291    /// Default implementation returns `NotWritable` (read-only handler).
292    fn test_set<'a>(
293        &'a self,
294        _ctx: &'a RequestContext,
295        _oid: &'a Oid,
296        _value: &'a Value,
297    ) -> BoxFuture<'a, SetResult> {
298        Box::pin(async { SetResult::NotWritable })
299    }
300
301    /// Commit a SET operation (phase 2 of two-phase commit).
302    ///
303    /// Only called after ALL `test_set` calls succeed. Should apply the change.
304    /// If this fails, `undo_set` will be called for all previously committed
305    /// varbinds in this request.
306    ///
307    /// Default implementation returns `NotWritable` (read-only handler).
308    fn commit_set<'a>(
309        &'a self,
310        _ctx: &'a RequestContext,
311        _oid: &'a Oid,
312        _value: &'a Value,
313    ) -> BoxFuture<'a, SetResult> {
314        Box::pin(async { SetResult::NotWritable })
315    }
316
317    /// Undo a committed SET operation (rollback on partial failure).
318    ///
319    /// Called if a later `commit_set` fails. Should restore the previous value.
320    /// This is best-effort: if undo fails, log a warning but continue.
321    ///
322    /// Default implementation does nothing (no rollback support).
323    fn undo_set<'a>(
324        &'a self,
325        _ctx: &'a RequestContext,
326        _oid: &'a Oid,
327        _value: &'a Value,
328    ) -> BoxFuture<'a, SetResult> {
329        Box::pin(async { SetResult::Ok })
330    }
331
332    /// Free resources allocated during `test_set` (cleanup on test failure).
333    ///
334    /// Called for varbinds whose `test_set` succeeded when a later varbind's
335    /// `test_set` fails. This allows handlers to release any resources
336    /// (locks, temporary allocations) acquired during the test phase.
337    ///
338    /// Called in reverse order, matching the `undo_set` convention.
339    ///
340    /// Default implementation does nothing.
341    fn free_set<'a>(
342        &'a self,
343        _ctx: &'a RequestContext,
344        _oid: &'a Oid,
345        _value: &'a Value,
346    ) -> BoxFuture<'a, ()> {
347        Box::pin(async {})
348    }
349
350    /// Check if this handler handles the given OID.
351    ///
352    /// Default implementation returns true if the OID starts with
353    /// the registered prefix (i.e., the OID is within this handler's subtree).
354    /// Override for more complex matching.
355    ///
356    /// This method is used to route GET and SET requests. GETNEXT and GETBULK
357    /// consult all handlers regardless of this method.
358    fn handles(&self, registered_prefix: &Oid, oid: &Oid) -> bool {
359        oid.starts_with(registered_prefix)
360    }
361}