1use clap::{Parser, ValueEnum};
6use std::time::Duration;
7
8use crate::Version;
9use crate::client::{Auth, Backoff, Retry};
10use crate::format::hex;
11use crate::v3::{AuthProtocol, PrivProtocol};
12
13#[derive(Debug, Clone, Copy, Default, ValueEnum)]
15pub enum SnmpVersion {
16 #[value(name = "1")]
18 V1,
19 #[default]
21 #[value(name = "2c")]
22 V2c,
23 #[value(name = "3")]
25 V3,
26}
27
28impl From<SnmpVersion> for Version {
29 fn from(v: SnmpVersion) -> Self {
30 match v {
31 SnmpVersion::V1 => Version::V1,
32 SnmpVersion::V2c => Version::V2c,
33 SnmpVersion::V3 => Version::V3,
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, Default, ValueEnum)]
40pub enum OutputFormat {
41 #[default]
43 Human,
44 Json,
46 Raw,
48}
49
50#[derive(Debug, Clone, Copy, Default, ValueEnum)]
52pub enum BackoffStrategy {
53 #[default]
55 None,
56 Fixed,
58 Exponential,
60}
61
62#[derive(Debug, Parser)]
64pub struct CommonArgs {
65 #[arg(value_name = "TARGET")]
67 pub target: String,
68
69 #[arg(short = 'v', long = "snmp-version", default_value = "2c")]
71 pub snmp_version: SnmpVersion,
72
73 #[arg(short = 'c', long = "community", default_value = "public")]
75 pub community: String,
76
77 #[arg(short = 't', long = "timeout", default_value = "5")]
79 pub timeout: f64,
80
81 #[arg(short = 'r', long = "retries", default_value = "3")]
83 pub retries: u32,
84
85 #[arg(long = "backoff", default_value = "none")]
87 pub backoff: BackoffStrategy,
88
89 #[arg(long = "backoff-delay", default_value = "1000")]
91 pub backoff_delay: u64,
92
93 #[arg(long = "backoff-max", default_value = "5000")]
95 pub backoff_max: u64,
96
97 #[arg(long = "backoff-jitter", default_value = "0.25")]
99 pub backoff_jitter: f64,
100}
101
102impl CommonArgs {
103 pub fn timeout_duration(&self) -> Duration {
105 Duration::from_secs_f64(self.timeout)
106 }
107
108 pub fn effective_version(&self, v3: &V3Args) -> SnmpVersion {
110 if v3.is_v3() {
111 SnmpVersion::V3
112 } else {
113 self.snmp_version
114 }
115 }
116
117 pub fn retry_config(&self) -> Retry {
119 let backoff = match self.backoff {
120 BackoffStrategy::None => Backoff::None,
121 BackoffStrategy::Fixed => Backoff::Fixed {
122 delay: Duration::from_millis(self.backoff_delay),
123 },
124 BackoffStrategy::Exponential => Backoff::Exponential {
125 initial: Duration::from_millis(self.backoff_delay),
126 max: Duration::from_millis(self.backoff_max),
127 jitter: self.backoff_jitter.clamp(0.0, 1.0),
128 },
129 };
130 Retry {
131 max_attempts: self.retries,
132 backoff,
133 }
134 }
135}
136
137#[derive(Debug, Parser)]
139pub struct V3Args {
140 #[arg(short = 'u', long = "username")]
142 pub username: Option<String>,
143
144 #[arg(short = 'a', long = "auth-protocol")]
146 pub auth_protocol: Option<AuthProtocol>,
147
148 #[arg(short = 'A', long = "auth-password")]
150 pub auth_password: Option<String>,
151
152 #[arg(short = 'x', long = "priv-protocol")]
154 pub priv_protocol: Option<PrivProtocol>,
155
156 #[arg(short = 'X', long = "priv-password")]
158 pub priv_password: Option<String>,
159}
160
161impl V3Args {
162 pub fn is_v3(&self) -> bool {
164 self.username.is_some()
165 }
166
167 pub fn auth(&self, common: &CommonArgs) -> Result<Auth, String> {
172 if let Some(ref username) = self.username {
173 if self.auth_protocol.is_none() && self.auth_password.is_some() {
174 return Err("authentication protocol required when using auth password".into());
175 }
176 if self.priv_protocol.is_none() && self.priv_password.is_some() {
177 return Err("privacy protocol required when using priv password".into());
178 }
179
180 let config = match (
181 self.auth_protocol,
182 self.auth_password.as_deref(),
183 self.priv_protocol,
184 self.priv_password.as_deref(),
185 ) {
186 (None, _, None, _) => Auth::usm(username),
187 (Some(auth_protocol), Some(auth_password), None, _) => {
188 Auth::usm(username).auth(auth_protocol, auth_password)
189 }
190 (
191 Some(auth_protocol),
192 Some(auth_password),
193 Some(priv_protocol),
194 Some(priv_password),
195 ) => Auth::usm(username).auth_priv(
196 auth_protocol,
197 auth_password,
198 priv_protocol,
199 priv_password,
200 ),
201 (None, _, Some(_), _) => {
202 return Err("authentication protocol required when using privacy".into());
203 }
204 (Some(_), None, _, _) => return Err("auth password required".into()),
205 (Some(_), Some(_), Some(_), None) => {
206 return Err("priv password required".into());
207 }
208 };
209 Ok(config.into())
210 } else {
211 let community = &common.community;
212 Ok(match common.snmp_version {
213 SnmpVersion::V1 => Auth::v1(community),
214 _ => Auth::v2c(community),
215 })
216 }
217 }
218
219 pub fn validate(&self) -> Result<(), String> {
221 if let Some(ref _username) = self.username {
222 if self.auth_protocol.is_none() && self.auth_password.is_some() {
223 return Err(
224 "authentication protocol (-a) required when using auth password".into(),
225 );
226 }
227
228 if self.priv_protocol.is_none() && self.priv_password.is_some() {
229 return Err("privacy protocol (-x) required when using priv password".into());
230 }
231
232 if self.auth_protocol.is_some() && self.auth_password.is_none() {
234 return Err(
235 "authentication password (-A) required when using auth protocol".into(),
236 );
237 }
238
239 if self.priv_protocol.is_some() && self.priv_password.is_none() {
241 return Err("privacy password (-X) required when using priv protocol".into());
242 }
243
244 if self.priv_protocol.is_some() && self.auth_protocol.is_none() {
246 return Err("authentication protocol (-a) required when using privacy".into());
247 }
248 }
249 Ok(())
250 }
251}
252
253#[derive(Debug, Parser)]
255pub struct OutputArgs {
256 #[arg(short = 'O', long = "output", default_value = "human")]
258 pub format: OutputFormat,
259
260 #[arg(long = "verbose")]
262 pub verbose: bool,
263
264 #[arg(long = "hex")]
266 pub hex: bool,
267
268 #[arg(long = "timing")]
270 pub timing: bool,
271
272 #[arg(long = "no-hints")]
274 pub no_hints: bool,
275
276 #[arg(short = 'd', long = "debug")]
278 pub debug: bool,
279
280 #[arg(short = 'D', long = "trace")]
282 pub trace: bool,
283}
284
285impl OutputArgs {
286 pub fn elapsed(&self, elapsed: Duration) -> Option<Duration> {
288 if self.timing { Some(elapsed) } else { None }
289 }
290
291 pub fn init_tracing(&self) {
296 use tracing_subscriber::EnvFilter;
297
298 let filter = if self.trace {
299 "async_snmp=trace"
300 } else if self.debug {
301 "async_snmp=debug"
302 } else {
303 "async_snmp=warn"
304 };
305
306 let _ = tracing_subscriber::fmt()
307 .with_env_filter(EnvFilter::new(filter))
308 .with_writer(std::io::stderr)
309 .try_init();
310 }
311}
312
313#[derive(Debug, Parser)]
315pub struct WalkArgs {
316 #[arg(long = "getnext")]
318 pub getnext: bool,
319
320 #[arg(long = "max-rep", default_value = "10")]
322 pub max_repetitions: u32,
323}
324
325#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
327pub enum ValueType {
328 #[value(name = "i")]
330 Integer,
331 #[value(name = "u")]
333 Unsigned,
334 #[value(name = "s")]
336 String,
337 #[value(name = "x")]
339 HexString,
340 #[value(name = "o")]
342 Oid,
343 #[value(name = "a")]
345 IpAddress,
346 #[value(name = "t")]
348 TimeTicks,
349 #[value(name = "c")]
351 Counter32,
352 #[value(name = "C")]
354 Counter64,
355}
356
357impl std::str::FromStr for ValueType {
358 type Err = String;
359
360 fn from_str(s: &str) -> Result<Self, Self::Err> {
361 match s {
362 "i" => Ok(ValueType::Integer),
363 "u" => Ok(ValueType::Unsigned),
364 "s" => Ok(ValueType::String),
365 "x" => Ok(ValueType::HexString),
366 "o" => Ok(ValueType::Oid),
367 "a" => Ok(ValueType::IpAddress),
368 "t" => Ok(ValueType::TimeTicks),
369 "c" => Ok(ValueType::Counter32),
370 "C" => Ok(ValueType::Counter64),
371 _ => Err(format!("invalid type specifier: {}", s)),
372 }
373 }
374}
375
376impl ValueType {
377 pub fn parse_value(&self, s: &str) -> Result<crate::Value, String> {
379 use crate::{Oid, Value};
380
381 match self {
382 ValueType::Integer => {
383 let v: i32 = s
384 .parse()
385 .map_err(|_| format!("invalid integer value: {}", s))?;
386 Ok(Value::Integer(v))
387 }
388 ValueType::Unsigned => {
389 let v: u32 = s
390 .parse()
391 .map_err(|_| format!("invalid unsigned value: {}", s))?;
392 Ok(Value::Gauge32(v))
393 }
394 ValueType::String => Ok(Value::OctetString(s.as_bytes().to_vec().into())),
395 ValueType::HexString => {
396 let bytes = hex::decode_relaxed(s)
397 .map_err(|_| "hex string must have even number of hex digits".to_string())?;
398 Ok(Value::OctetString(bytes.into()))
399 }
400 ValueType::Oid => {
401 let oid = Oid::parse(s).map_err(|e| format!("invalid OID value: {}", e))?;
402 Ok(Value::ObjectIdentifier(oid))
403 }
404 ValueType::IpAddress => {
405 let parts: Vec<&str> = s.split('.').collect();
406 if parts.len() != 4 {
407 return Err(format!("invalid IP address: {}", s));
408 }
409 let mut bytes = [0u8; 4];
410 for (i, part) in parts.iter().enumerate() {
411 bytes[i] = part
412 .parse()
413 .map_err(|_| format!("invalid IP address octet: {}", part))?;
414 }
415 Ok(Value::IpAddress(bytes))
416 }
417 ValueType::TimeTicks => {
418 let v: u32 = s
419 .parse()
420 .map_err(|_| format!("invalid timeticks value: {}", s))?;
421 Ok(Value::TimeTicks(v))
422 }
423 ValueType::Counter32 => {
424 let v: u32 = s
425 .parse()
426 .map_err(|_| format!("invalid counter32 value: {}", s))?;
427 Ok(Value::Counter32(v))
428 }
429 ValueType::Counter64 => {
430 let v: u64 = s
431 .parse()
432 .map_err(|_| format!("invalid counter64 value: {}", s))?;
433 Ok(Value::Counter64(v))
434 }
435 }
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 fn common_args() -> CommonArgs {
444 CommonArgs {
445 target: "192.168.1.1".to_string(),
446 snmp_version: SnmpVersion::V3,
447 community: "public".to_string(),
448 timeout: 5.0,
449 retries: 3,
450 backoff: BackoffStrategy::None,
451 backoff_delay: 100,
452 backoff_max: 5000,
453 backoff_jitter: 0.25,
454 }
455 }
456
457 #[test]
458 fn test_retry_config_none() {
459 let args = CommonArgs {
460 target: "192.168.1.1".to_string(),
461 snmp_version: SnmpVersion::V2c,
462 community: "public".to_string(),
463 timeout: 5.0,
464 retries: 3,
465 backoff: BackoffStrategy::None,
466 backoff_delay: 100,
467 backoff_max: 5000,
468 backoff_jitter: 0.25,
469 };
470 let retry = args.retry_config();
471 assert_eq!(retry.max_attempts, 3);
472 assert!(matches!(retry.backoff, Backoff::None));
473 }
474
475 #[test]
476 fn test_retry_config_fixed() {
477 let args = CommonArgs {
478 target: "192.168.1.1".to_string(),
479 snmp_version: SnmpVersion::V2c,
480 community: "public".to_string(),
481 timeout: 5.0,
482 retries: 5,
483 backoff: BackoffStrategy::Fixed,
484 backoff_delay: 200,
485 backoff_max: 5000,
486 backoff_jitter: 0.25,
487 };
488 let retry = args.retry_config();
489 assert_eq!(retry.max_attempts, 5);
490 assert!(matches!(
491 retry.backoff,
492 Backoff::Fixed { delay } if delay == Duration::from_millis(200)
493 ));
494 }
495
496 #[test]
497 fn test_retry_config_exponential() {
498 let args = CommonArgs {
499 target: "192.168.1.1".to_string(),
500 snmp_version: SnmpVersion::V2c,
501 community: "public".to_string(),
502 timeout: 5.0,
503 retries: 4,
504 backoff: BackoffStrategy::Exponential,
505 backoff_delay: 50,
506 backoff_max: 2000,
507 backoff_jitter: 0.1,
508 };
509 let retry = args.retry_config();
510 assert_eq!(retry.max_attempts, 4);
511 match retry.backoff {
512 Backoff::Exponential {
513 initial,
514 max,
515 jitter,
516 } => {
517 assert_eq!(initial, Duration::from_millis(50));
518 assert_eq!(max, Duration::from_millis(2000));
519 assert!((jitter - 0.1).abs() < f64::EPSILON);
520 }
521 _ => panic!("expected Exponential"),
522 }
523 }
524
525 #[test]
526 fn test_v3_args_validation() {
527 let args = V3Args {
529 username: None,
530 auth_protocol: None,
531 auth_password: None,
532 priv_protocol: None,
533 priv_password: None,
534 };
535 assert!(args.validate().is_ok());
536
537 let args = V3Args {
539 username: Some("admin".to_string()),
540 auth_protocol: None,
541 auth_password: None,
542 priv_protocol: None,
543 priv_password: None,
544 };
545 assert!(args.validate().is_ok());
546
547 let args = V3Args {
549 username: Some("admin".to_string()),
550 auth_protocol: None,
551 auth_password: Some("pass".to_string()),
552 priv_protocol: None,
553 priv_password: None,
554 };
555 assert!(args.validate().is_err());
556 assert!(args.auth(&common_args()).is_err());
557
558 let args = V3Args {
560 username: Some("admin".to_string()),
561 auth_protocol: Some(AuthProtocol::Sha256),
562 auth_password: None,
563 priv_protocol: None,
564 priv_password: None,
565 };
566 assert!(args.validate().is_err());
567 assert!(args.auth(&common_args()).is_err());
568
569 let args = V3Args {
571 username: Some("admin".to_string()),
572 auth_protocol: Some(AuthProtocol::Sha256),
573 auth_password: Some("authpass".to_string()),
574 priv_protocol: None,
575 priv_password: Some("privpass".to_string()),
576 };
577 assert!(args.validate().is_err());
578 assert!(args.auth(&common_args()).is_err());
579
580 let args = V3Args {
582 username: Some("admin".to_string()),
583 auth_protocol: Some(AuthProtocol::Sha256),
584 auth_password: Some("authpass".to_string()),
585 priv_protocol: Some(PrivProtocol::Aes128),
586 priv_password: None,
587 };
588 assert!(args.validate().is_err());
589 assert!(args.auth(&common_args()).is_err());
590
591 let args = V3Args {
593 username: Some("admin".to_string()),
594 auth_protocol: None,
595 auth_password: None,
596 priv_protocol: Some(PrivProtocol::Aes128),
597 priv_password: Some("pass".to_string()),
598 };
599 assert!(args.validate().is_err());
600 assert!(args.auth(&common_args()).is_err());
601
602 let args = V3Args {
604 username: Some("admin".to_string()),
605 auth_protocol: Some(AuthProtocol::Sha1),
606 auth_password: Some("pass".to_string()),
607 priv_protocol: Some(PrivProtocol::Aes256),
608 priv_password: Some("pass".to_string()),
609 };
610 assert!(args.validate().is_ok());
611 }
612
613 #[test]
614 fn test_v3_args_auth_constructs_valid_security_levels() {
615 let no_auth = V3Args {
616 username: Some("user".to_string()),
617 auth_protocol: None,
618 auth_password: None,
619 priv_protocol: None,
620 priv_password: None,
621 }
622 .auth(&common_args())
623 .unwrap();
624 let Auth::Usm(no_auth) = no_auth else {
625 panic!("expected USM config");
626 };
627 assert_eq!(no_auth.auth_protocol(), None);
628 assert_eq!(no_auth.priv_protocol(), None);
629
630 let auth = V3Args {
631 username: Some("user".to_string()),
632 auth_protocol: Some(AuthProtocol::Sha256),
633 auth_password: Some("authpass".to_string()),
634 priv_protocol: None,
635 priv_password: None,
636 }
637 .auth(&common_args())
638 .unwrap();
639 let Auth::Usm(auth) = auth else {
640 panic!("expected USM config");
641 };
642 assert_eq!(auth.auth_protocol(), Some(AuthProtocol::Sha256));
643 assert_eq!(auth.priv_protocol(), None);
644
645 let auth_priv = V3Args {
646 username: Some("user".to_string()),
647 auth_protocol: Some(AuthProtocol::Sha1),
648 auth_password: Some("authpass".to_string()),
649 priv_protocol: Some(PrivProtocol::Aes128),
650 priv_password: Some("privpass".to_string()),
651 }
652 .auth(&common_args())
653 .unwrap();
654 let Auth::Usm(auth_priv) = auth_priv else {
655 panic!("expected USM config");
656 };
657 assert_eq!(auth_priv.auth_protocol(), Some(AuthProtocol::Sha1));
658 assert_eq!(auth_priv.priv_protocol(), Some(PrivProtocol::Aes128));
659 }
660
661 #[test]
662 fn test_value_type_parse_integer() {
663 use crate::Value;
664 let v = ValueType::Integer.parse_value("42").unwrap();
665 assert!(matches!(v, Value::Integer(42)));
666
667 let v = ValueType::Integer.parse_value("-100").unwrap();
668 assert!(matches!(v, Value::Integer(-100)));
669
670 assert!(ValueType::Integer.parse_value("not_a_number").is_err());
671 }
672
673 #[test]
674 fn test_value_type_parse_unsigned() {
675 use crate::Value;
676 let v = ValueType::Unsigned.parse_value("42").unwrap();
677 assert!(matches!(v, Value::Gauge32(42)));
678
679 assert!(ValueType::Unsigned.parse_value("-1").is_err());
680 }
681
682 #[test]
683 fn test_value_type_parse_string() {
684 use crate::Value;
685 let v = ValueType::String.parse_value("hello world").unwrap();
686 if let Value::OctetString(bytes) = v {
687 assert_eq!(&*bytes, b"hello world");
688 } else {
689 panic!("expected OctetString");
690 }
691 }
692
693 #[test]
694 fn test_value_type_parse_hex_string() {
695 use crate::Value;
696
697 let v = ValueType::HexString.parse_value("001a2b").unwrap();
699 if let Value::OctetString(bytes) = v {
700 assert_eq!(&*bytes, &[0x00, 0x1a, 0x2b]);
701 } else {
702 panic!("expected OctetString");
703 }
704
705 let v = ValueType::HexString.parse_value("00 1A 2B").unwrap();
707 if let Value::OctetString(bytes) = v {
708 assert_eq!(&*bytes, &[0x00, 0x1a, 0x2b]);
709 } else {
710 panic!("expected OctetString");
711 }
712
713 assert!(ValueType::HexString.parse_value("001").is_err());
715 }
716
717 #[test]
718 fn test_value_type_parse_ip_address() {
719 use crate::Value;
720 let v = ValueType::IpAddress.parse_value("192.168.1.1").unwrap();
721 assert!(matches!(v, Value::IpAddress([192, 168, 1, 1])));
722
723 assert!(ValueType::IpAddress.parse_value("192.168.1").is_err());
724 assert!(ValueType::IpAddress.parse_value("256.1.1.1").is_err());
725 }
726
727 #[test]
728 fn test_value_type_parse_timeticks() {
729 use crate::Value;
730 let v = ValueType::TimeTicks.parse_value("12345678").unwrap();
731 assert!(matches!(v, Value::TimeTicks(12345678)));
732 }
733
734 #[test]
735 fn test_value_type_parse_counters() {
736 use crate::Value;
737
738 let v = ValueType::Counter32.parse_value("4294967295").unwrap();
739 assert!(matches!(v, Value::Counter32(4294967295)));
740
741 let v = ValueType::Counter64
742 .parse_value("18446744073709551615")
743 .unwrap();
744 assert!(matches!(v, Value::Counter64(18446744073709551615)));
745 }
746}