Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

SnmpKit Interactive Tour 🚀

livebooks/snmpkit_tour.livemd

SnmpKit Interactive Tour 🚀

Section

Welcome to SnmpKit v0.3.5!

This interactive Livebook will take you on a comprehensive tour of SnmpKit’s new unified API. We’ll start by creating a simulated SNMP device and then demonstrate all the powerful features against our own simulation - no external network required!

What you’ll learn:

  • 🎯 Unified API - Clean, context-based modules
  • 📡 SNMP Operations - get, walk, bulk, multi-target
  • 📚 MIB Management - resolution, compilation, tree navigation
  • 🧪 Device Simulation - realistic testing environments
  • Advanced Features - streaming, performance, analytics

Let’s get started! 🚀

Setup

First, let’s install SnmpKit and configure our environment:

Mix.install([
  {:snmpkit, "~> 0.3.5"}
])

# Configure logging for our tour
Logger.configure(level: :info)

# Import the unified API modules for convenience
alias SnmpKit.{SNMP, MIB, Sim}

IO.puts("🎉 SnmpKit v0.3.5 loaded successfully!")
IO.puts("📚 Ready to explore the unified API!")

Chapter 1: Start Our Simulated Network 🖥️

Before we can demonstrate SNMP operations, let’s create our own simulated network! This is one of SnmpKit’s most powerful features - realistic device simulation for testing and development.

Create a Cable Modem Simulation

# Create a realistic DOCSIS cable modem with essential OIDs
cable_modem_oids = %{
  # System Group
  "1.3.6.1.2.1.1.1.0" => "ARRIS SURFboard SB8200 DOCSIS 3.1 Cable Modem",
  "1.3.6.1.2.1.1.2.0" => "1.3.6.1.4.1.4115.1.20.1.1.2.25",
  "1.3.6.1.2.1.1.3.0" => %{type: "TimeTicks", value: 0},
  "1.3.6.1.2.1.1.4.0" => "admin@example.com",
  "1.3.6.1.2.1.1.5.0" => "cm-001",
  "1.3.6.1.2.1.1.6.0" => "Home Network",

  # Interface Group
  "1.3.6.1.2.1.2.1.0" => 2,
  "1.3.6.1.2.1.2.2.1.1.1" => 1,
  "1.3.6.1.2.1.2.2.1.1.2" => 2,
  "1.3.6.1.2.1.2.2.1.2.1" => "cable-downstream0",
  "1.3.6.1.2.1.2.2.1.2.2" => "cable-upstream0",
  "1.3.6.1.2.1.2.2.1.3.1" => 127,  # docsCableMaclayer
  "1.3.6.1.2.1.2.2.1.3.2" => 127,

  # DOCSIS Specific OIDs
  "1.3.6.1.2.1.10.127.1.1.1.1.3.2" => %{type: "INTEGER", value: 3},  # docsIfCmtsUpChannelId
  "1.3.6.1.2.1.10.127.1.1.1.1.6.2" => %{type: "INTEGER", value: 6400000},  # docsIfCmtsUpChannelFrequency
  "1.3.6.1.2.1.10.127.1.2.2.1.1.2" => %{type: "INTEGER", value: 1},  # docsIfCmStatusValue
  "1.3.6.1.2.1.10.127.1.2.2.1.12.2" => %{type: "Counter32", value: 1000},  # docsIfCmStatusUnerroreds

  # Cable Modem specific counters
  "1.3.6.1.2.1.2.2.1.10.1" => %{type: "Counter32", value: 1500000},  # ifInOctets
  "1.3.6.1.2.1.2.2.1.16.1" => %{type: "Counter32", value: 900000},   # ifOutOctets
  "1.3.6.1.2.1.2.2.1.11.1" => %{type: "Counter32", value: 12000},    # ifInUcastPkts
  "1.3.6.1.2.1.2.2.1.17.1" => %{type: "Counter32", value: 8000},     # ifOutUcastPkts
}

# Create the cable modem profile
{:ok, cable_modem_profile} = SnmpKit.SnmpSim.ProfileLoader.load_profile(
  :cable_modem,
  {:manual, cable_modem_oids},
  behaviors: [:counter_increment, :time_based_changes]
)

# Start our simulated cable modem on port 1161
{:ok, cable_modem} = Sim.start_device(cable_modem_profile, [
  port: 1161,
  community: "public"
])

# Define our target for easy reference
cable_modem_target = "127.0.0.1:1161"

IO.puts("✅ Cable modem simulation started on #{cable_modem_target}")
IO.puts("🎯 Ready for SNMP operations!")

# Make target available to other cells
cable_modem_target

Create a Router Simulation

Let’s also create a router simulation to demonstrate multi-target operations:

# Create a realistic enterprise router with essential OIDs
router_oids = %{
  # System Group
  "1.3.6.1.2.1.1.1.0" => "Cisco IOS Software, C2900 Software, Version 15.1(4)M12a",
  "1.3.6.1.2.1.1.2.0" => "1.3.6.1.4.1.9.1.576",
  "1.3.6.1.2.1.1.3.0" => %{type: "TimeTicks", value: 0},
  "1.3.6.1.2.1.1.4.0" => "IT Department ",
  "1.3.6.1.2.1.1.5.0" => "router-001",
  "1.3.6.1.2.1.1.6.0" => "Main Office - Server Room",

  # Interface Group (more interfaces for a router)
  "1.3.6.1.2.1.2.1.0" => 5,  # ifNumber (more interfaces)

  # FastEthernet0/0 (WAN)
  "1.3.6.1.2.1.2.2.1.1.1" => 1,
  "1.3.6.1.2.1.2.2.1.2.1" => "FastEthernet0/0",
  "1.3.6.1.2.1.2.2.1.3.1" => 6,  # ethernetCsmacd
  "1.3.6.1.2.1.2.2.1.5.1" => %{type: "Gauge32", value: 100000000},  # 100Mbps
  "1.3.6.1.2.1.2.2.1.8.1" => 1,  # up

  # FastEthernet0/1 (LAN)
  "1.3.6.1.2.1.2.2.1.1.2" => 2,
  "1.3.6.1.2.1.2.2.1.2.2" => "FastEthernet0/1",
  "1.3.6.1.2.1.2.2.1.3.2" => 6,
  "1.3.6.1.2.1.2.2.1.5.2" => %{type: "Gauge32", value: 100000000},
  "1.3.6.1.2.1.2.2.1.8.2" => 1,

  # Serial0/0/0 (WAN backup)
  "1.3.6.1.2.1.2.2.1.1.3" => 3,
  "1.3.6.1.2.1.2.2.1.2.3" => "Serial0/0/0",
  "1.3.6.1.2.1.2.2.1.3.3" => 22,  # propPointToPointSerial
  "1.3.6.1.2.1.2.2.1.5.3" => %{type: "Gauge32", value: 1544000},  # T1 speed
  "1.3.6.1.2.1.2.2.1.8.3" => 2,  # down (backup interface)

  # Loopback0
  "1.3.6.1.2.1.2.2.1.1.4" => 4,
  "1.3.6.1.2.1.2.2.1.2.4" => "Loopback0",
  "1.3.6.1.2.1.2.2.1.3.4" => 24,  # softwareLoopback
  "1.3.6.1.2.1.2.2.1.8.4" => 1,

  # Traffic counters for active interfaces
  "1.3.6.1.2.1.2.2.1.10.1" => %{type: "Counter32", value: 0},  # ifInOctets WAN
  "1.3.6.1.2.1.2.2.1.16.1" => %{type: "Counter32", value: 0},  # ifOutOctets WAN
  "1.3.6.1.2.1.2.2.1.10.2" => %{type: "Counter32", value: 0},  # ifInOctets LAN
  "1.3.6.1.2.1.2.2.1.16.2" => %{type: "Counter32", value: 0},  # ifOutOctets LAN

  # IP routing info
  "1.3.6.1.2.1.4.1.0" => 1,  # ipForwarding (enabled)
  "1.3.6.1.2.1.4.2.0" => 30, # ipDefaultTTL

  # SNMP community info
  "1.3.6.1.2.1.11.1.0" => %{type: "Counter32", value: 0},  # snmpInPkts
  "1.3.6.1.2.1.11.2.0" => %{type: "Counter32", value: 0},  # snmpOutPkts
}

# Create the router profile
{:ok, router_profile} = SnmpKit.SnmpSim.ProfileLoader.load_profile(
  :router,
  {:manual, router_oids},
  behaviors: [:counter_increment, :time_based_changes]
)

{:ok, router} = Sim.start_device(router_profile, [
  port: 1162,
  community: "public"
])

router_target = "127.0.0.1:1162"

IO.puts("✅ Router simulation started on #{router_target}")
IO.puts("🌐 Now we have a complete simulated network!")
IO.puts("📊 Loaded #{map_size(router_oids)} router OIDs")

# Make targets available to other cells
{cable_modem_target, router_target}

Quick Connectivity Test

Let’s verify our simulated devices are responding:

# Get targets from previous cells
cable_modem_target = "127.0.0.1:1161"
router_target = "127.0.0.1:1162"

# Test cable modem
case SNMP.get(cable_modem_target, "sysDescr.0") do
  {:ok, description} ->
    IO.puts("📡 Cable Modem: #{description}")
  {:error, reason} ->
    IO.puts("❌ Cable modem error: #{inspect(reason)}")
end

# Test router
case SNMP.get(router_target, "sysDescr.0") do
  {:ok, description} ->
    IO.puts("🔀 Router: #{description}")
  {:error, reason} ->
    IO.puts("❌ Router error: #{inspect(reason)}")
end

IO.puts("\n🎉 Both devices are responding! Let's explore the API...")

Chapter 2: SnmpKit.SNMP - Protocol Operations 📡

Now let’s explore the comprehensive SNMP operations available through SnmpKit.SNMP. All operations will work against our simulated devices!

Basic GET Operations

# Set targets for this cell
cable_modem_target = "127.0.0.1:1161"

IO.puts("=== Basic SNMP GET Operations ===\n")

# Standard GET operation
{:ok, system_desc} = SNMP.get(cable_modem_target, "sysDescr.0")
IO.puts("System Description: #{system_desc}")

# GET with type information
{:ok, {oid, type, value}} = SNMP.get_with_type(cable_modem_target, "sysUpTime.0")
IO.puts("System Uptime: #{value} (#{type}) at OID #{oid}")  # Remove Enum.join

# GET with pretty formatting
{:ok, formatted_uptime} = SNMP.get_pretty(cable_modem_target, "sysUpTime.0")
IO.puts("Formatted Uptime: #{formatted_uptime}")

# GET system contact
{:ok, contact} = SNMP.get(cable_modem_target, "sysContact.0")
IO.puts("System Contact: #{contact}")

WALK Operations

WALK operations traverse the SNMP tree to get multiple related values:

# Set targets for this cell
cable_modem_target = "127.0.0.1:1161"
router_target = "127.0.0.1:1162"

IO.puts("\n=== SNMP WALK Operations ===\n")

# Walk the system group
{:ok, system_info} = SNMP.walk(cable_modem_target, "1.3.6.1.2.1.1.1")
IO.puts("System group contains #{length(system_info)} objects:")

# Display first few system objects
system_info
|> Enum.take(5)
|> Enum.each(fn {oid, type, value} ->
  IO.puts("  #{oid} (#{type}) = #{inspect(value)}")
end)

# Walk with pretty formatting
{:ok, pretty_system} = SNMP.walk_pretty(cable_modem_target, "system")
IO.puts("\nPretty formatted system info:")
pretty_system
|> Enum.take(3)
|> Enum.each(fn {name, value} ->
  IO.puts("  #{name}: #{value}")
end)

Interface Information

Let’s explore interface data, which is crucial for network monitoring:

# Set targets for this cell
cable_modem_target = "127.0.0.1:1161"
router_target = "127.0.0.1:1162"

IO.puts("\n=== Interface Information ===\n")

# Get interface count from cable modem
{:ok, cm_if_count} = SNMP.get(cable_modem_target, "ifNumber.0")
IO.puts("Cable Modem interfaces: #{cm_if_count}")

# Get interface count from router
{:ok, router_if_count} = SNMP.get(router_target, "ifNumber.0")
IO.puts("Router interfaces: #{router_if_count}")

# Get interface descriptions
{:ok, cm_if1_desc} = SNMP.get(cable_modem_target, "ifDescr.1")
{:ok, cm_if2_desc} = SNMP.get(cable_modem_target, "ifDescr.2")
IO.puts("\nCable Modem Interface Details:")
IO.puts("  Interface 1: #{cm_if1_desc}")
IO.puts("  Interface 2: #{cm_if2_desc}")

{:ok, router_if1_desc} = SNMP.get(router_target, "ifDescr.1")
{:ok, router_if2_desc} = SNMP.get(router_target, "ifDescr.2")
IO.puts("\nRouter Interface Details:")
IO.puts("  Interface 1: #{router_if1_desc}")
IO.puts("  Interface 2: #{router_if2_desc}")

Bulk Operations

For large amounts of data, bulk operations are much more efficient:

# Set targets for this cell
cable_modem_target = "127.0.0.1:1161"
router_target = "127.0.0.1:1162"

IO.puts("\n=== Bulk Operations ===\n")

# Standard bulk walk
{:ok, bulk_results} = SNMP.bulk_walk(cable_modem_target, "interfaces")
IO.puts("Bulk walk of interfaces returned #{length(bulk_results)} objects")

# Adaptive bulk walk (auto-optimizes performance)
{:ok, adaptive_results} = SNMP.adaptive_walk(cable_modem_target, "interfaces")
IO.puts("Adaptive walk returned #{length(adaptive_results)} objects")

# Get bulk with specific parameters
{:ok, bulk_specific} = SNMP.get_bulk(cable_modem_target, "interfaces", [
  max_repetitions: 5,
  timeout: 2000
])
IO.puts("Targeted bulk operation returned #{length(bulk_specific)} objects")

# Show some bulk results
bulk_results
|> Enum.take(3)
|> Enum.each(fn {oid, type, value} ->
  IO.puts("  #{oid} (#{type}): #{inspect(value)}")
end)

Multi-Target Operations

One of SnmpKit’s powerful features is querying multiple devices simultaneously:

# Set targets for this cell
cable_modem_target = "127.0.0.1:1161"
router_target = "127.0.0.1:1162"

IO.puts("\n=== Multi-Target Operations ===\n")

# Query both devices for system information
multi_targets = [
  {cable_modem_target, "sysDescr.0"},
  {router_target, "sysDescr.0"},
  {cable_modem_target, "sysUpTime.0"},
  {router_target, "sysContact.0"}
]

multi_results = SNMP.get_multi(multi_targets)
IO.puts("Multi-target query results:")
IO.puts("Raw results: #{inspect(multi_results)}")

# Process results based on actual format
multi_results
|> Enum.with_index()
|> Enum.each(fn {result, index} ->
  {target, oid} = Enum.at(multi_targets, index)
  case result do
    {:ok, value} ->
      IO.puts("  ✅ #{target} #{oid}: #{inspect(value)}")
    {:error, reason} ->
      IO.puts("  ❌ #{target} #{oid}: #{inspect(reason)}")
    _ ->
      IO.puts("  ? #{target} #{oid}: #{inspect(result)}")
  end
end)

# Multi-target walk operations
walk_targets = [
  {cable_modem_target, "system"},
  {router_target, "system"}
]

multi_walk_results = SNMP.walk_multi(walk_targets)
IO.puts("\nMulti-target walk completed for #{length(multi_walk_results)} targets")

# Display walk results
multi_walk_results
|> Enum.with_index()
|> Enum.each(fn {result, index} ->
  {target, oid} = Enum.at(walk_targets, index)
  case result do
    {:ok, walk_data} ->
      IO.puts("  ✅ #{target} #{oid}: #{length(walk_data)} objects")
      # Show first few objects
      walk_data
      |> Enum.take(3)
      |> Enum.each(fn {obj_oid, type, value} ->
        oid_str = if is_list(obj_oid), do: Enum.join(obj_oid, "."), else: obj_oid
        IO.puts("    #{oid_str} (#{type}) = #{inspect(value)}")
      end)
    {:error, reason} ->
      IO.puts("  ❌ #{target} #{oid}: #{inspect(reason)}")
    _ ->
      IO.puts("  ? #{target} #{oid}: #{inspect(result)}")
  end
end)

Chapter 3: SnmpKit.MIB - MIB Management 📚

The MIB (Management Information Base) system is the heart of SNMP. It defines the structure and meaning of SNMP data. Let’s explore SnmpKit’s powerful MIB capabilities!

OID Name Resolution

IO.puts("=== MIB Name Resolution ===\n")

# Resolve common SNMP object names to OIDs
common_objects = [
  "sysDescr.0",
  "sysUpTime.0",
  "sysContact.0",
  "ifNumber.0",
  "ifDescr.1",
  "ifInOctets.1",
  "system",
  "interfaces"
]

IO.puts("Common SNMP objects and their OIDs:")
Enum.each(common_objects, fn name ->
  case MIB.resolve(name) do
    {:ok, oid} ->
      IO.puts("  #{name}#{Enum.join(oid, ".")}")
    {:error, reason} ->
      IO.puts("  #{name} → Error: #{reason}")
  end
end)

Reverse OID Lookup

IO.puts("\n=== Reverse OID Lookup ===\n")

# Convert OIDs back to names
test_oids = [
  [1, 3, 6, 1, 2, 1, 1, 1, 0],
  [1, 3, 6, 1, 2, 1, 1, 3, 0],
  [1, 3, 6, 1, 2, 1, 2, 1, 0],
  [1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 1]
]

IO.puts("OID to name resolution:")
Enum.each(test_oids, fn oid ->
  case MIB.reverse_lookup(oid) do
    {:ok, name} ->
      IO.puts("  #{Enum.join(oid, ".")}#{name}")
    {:error, reason} ->
      IO.puts("  #{Enum.join(oid, ".")}#{reason}")
  end
end)

MIB Tree Navigation

IO.puts("\n=== MIB Tree Navigation ===\n")

# Get children of the system group
{:ok, system_oid} = MIB.resolve("system")
{:ok, system_children} = MIB.children(system_oid)

system_oid_str = if is_list(system_oid), do: Enum.join(system_oid, "."), else: inspect(system_oid)
IO.puts("System group (#{system_oid_str}) has #{length(system_children)} children:")
system_children
|> Enum.take(5)
|> Enum.each(fn child_oid ->
  oid_str = if is_list(child_oid), do: Enum.join(child_oid, "."), else: inspect(child_oid)
  case MIB.reverse_lookup(child_oid) do
    {:ok, name} ->
      IO.puts("  #{oid_str} (#{name})")
    {:error, _} ->
      IO.puts("  #{oid_str}")
  end
end)

# Get parent of a specific OID
{:ok, sys_descr_oid} = MIB.resolve("sysDescr.0")
{:ok, parent_oid} = MIB.parent(sys_descr_oid)
{:ok, parent_name} = MIB.reverse_lookup(parent_oid)
parent_oid_str = if is_list(parent_oid), do: Enum.join(parent_oid, "."), else: inspect(parent_oid)
IO.puts("\nParent of sysDescr.0: #{parent_oid_str} (#{parent_name})")

Chapter 4: Creating Custom Device Simulations 🛠️

One of the most powerful features of SnmpKit is creating realistic device simulations without needing walk files. Let’s explore different approaches:

Enterprise Switch Simulation

IO.puts("=== Creating Enterprise Switch Simulation ===\n")

# Define a realistic 24-port enterprise switch
switch_oids = %{
  # System Group
  "1.3.6.1.2.1.1.1.0" => "Cisco IOS Software, C3560CX Software, Version 15.2(4)E10",
  "1.3.6.1.2.1.1.2.0" => "1.3.6.1.4.1.9.1.1208",
  "1.3.6.1.2.1.1.3.0" => %{type: "TimeTicks", value: 0},
  "1.3.6.1.2.1.1.4.0" => "Network Operations ",
  "1.3.6.1.2.1.1.5.0" => "switch-core-01",
  "1.3.6.1.2.1.1.6.0" => "Main Office - Network Closet A",

  # Switch has many interfaces (24 ports + management)
  "1.3.6.1.2.1.2.1.0" => 25,  # ifNumber
}

# Add interfaces programmatically
switch_oids = Enum.reduce(1..24, switch_oids, fn port, acc ->
  Map.merge(acc, %{
    # Interface descriptions
    "1.3.6.1.2.1.2.2.1.2.#{port}" => "GigabitEthernet0/#{port}",
    "1.3.6.1.2.1.2.2.1.3.#{port}" => 6,  # ethernetCsmacd
    "1.3.6.1.2.1.2.2.1.5.#{port}" => %{type: "Gauge32", value: 1000000000},  # 1Gbps
    "1.3.6.1.2.1.2.2.1.8.#{port}" => if(port <= 12, do: 1, else: 2),  # First 12 up, rest down
    # Traffic counters for active ports
    "1.3.6.1.2.1.2.2.1.10.#{port}" => %{type: "Counter32", value: 0},
    "1.3.6.1.2.1.2.2.1.16.#{port}" => %{type: "Counter32", value: 0},
  })
end)

# Add management interface
switch_oids = Map.merge(switch_oids, %{
  "1.3.6.1.2.1.2.2.1.2.25" => "Management0",
  "1.3.6.1.2.1.2.2.1.3.25" => 6,
  "1.3.6.1.2.1.2.2.1.5.25" => %{type: "Gauge32", value: 100000000},  # 100Mbps
  "1.3.6.1.2.1.2.2.1.8.25" => 1,
})

# Create and start the switch
{:ok, switch_profile} = SnmpKit.SnmpSim.ProfileLoader.load_profile(
  :switch,
  {:manual, switch_oids},
  behaviors: [:counter_increment, :time_based_changes]
)

{:ok, switch_device} = Sim.start_device(switch_profile, port: 1163)
switch_target = "127.0.0.1:1163"

IO.puts("✅ Enterprise switch simulation started on #{switch_target}")

# Test the switch
{:ok, switch_desc} = SNMP.get(switch_target, "sysDescr.0")
{:ok, switch_interfaces} = SNMP.get(switch_target, "ifNumber.0")
IO.puts("Switch: #{switch_desc}")
IO.puts("Interfaces: #{switch_interfaces}")

# Check a few interface statuses
Enum.each([1, 5, 15, 25], fn port ->
  case SNMP.get(switch_target, "ifDescr.#{port}") do
    {:ok, desc} ->
      case SNMP.get(switch_target, "ifOperStatus.#{port}") do
        {:ok, status} ->
          status_text = if status == 1, do: "UP", else: "DOWN"
          IO.puts("  Port #{port}: #{desc} - #{status_text}")
        _ -> nil
      end
    _ -> nil
  end
end)

Wireless Access Point Simulation

IO.puts("\n=== Creating Wireless Access Point Simulation ===\n")

# Define a realistic dual-band wireless AP
wireless_ap_oids = %{
  # System Group
  "1.3.6.1.2.1.1.1.0" => "Ubiquiti UniFi AP AC Pro, Version 4.3.21.11325",
  "1.3.6.1.2.1.1.2.0" => "1.3.6.1.4.1.41112.1.4.7",
  "1.3.6.1.2.1.1.3.0" => %{type: "TimeTicks", value: 0},
  "1.3.6.1.2.1.1.4.0" => "Wireless Admin ",
  "1.3.6.1.2.1.1.5.0" => "ap-lobby-01",
  "1.3.6.1.2.1.1.6.0" => "Main Building - Lobby",

  # Wireless interfaces: Management + 2.4GHz + 5GHz
  "1.3.6.1.2.1.2.1.0" => 3,

  # Management interface (Ethernet)
  "1.3.6.1.2.1.2.2.1.2.1" => "eth0 (Management)",
  "1.3.6.1.2.1.2.2.1.3.1" => 6,  # ethernetCsmacd
  "1.3.6.1.2.1.2.2.1.5.1" => %{type: "Gauge32", value: 1000000000},  # 1Gbps
  "1.3.6.1.2.1.2.2.1.8.1" => 1,

  # 2.4GHz radio
  "1.3.6.1.2.1.2.2.1.2.2" => "wlan0 (2.4GHz)",
  "1.3.6.1.2.1.2.2.1.3.2" => 71,  # ieee80211
  "1.3.6.1.2.1.2.2.1.5.2" => %{type: "Gauge32", value: 300000000},  # 300Mbps
  "1.3.6.1.2.1.2.2.1.8.2" => 1,

  # 5GHz radio
  "1.3.6.1.2.1.2.2.1.2.3" => "wlan1 (5GHz)",
  "1.3.6.1.2.1.2.2.1.3.3" => 71,  # ieee80211
  "1.3.6.1.2.1.2.2.1.5.3" => %{type: "Gauge32", value: 1300000000},  # 1.3Gbps
  "1.3.6.1.2.1.2.2.1.8.3" => 1,

  # Wireless-specific OIDs (simplified)
  "1.3.6.1.4.1.41112.1.4.1.1.4.1" => 6,    # 2.4GHz channel
  "1.3.6.1.4.1.41112.1.4.1.1.4.2" => 36,   # 5GHz channel
  "1.3.6.1.4.1.41112.1.4.1.1.5.1" => 20,   # TX power (dBm)
  "1.3.6.1.4.1.41112.1.4.1.1.5.2" => 23,   # TX power (dBm)
  "1.3.6.1.4.1.41112.1.4.1.1.6.1" => 15,   # Connected clients 2.4GHz
  "1.3.6.1.4.1.41112.1.4.1.1.6.2" => 8,    # Connected clients 5GHz

  # Traffic counters
  "1.3.6.1.2.1.2.2.1.10.2" => %{type: "Counter32", value: 0},  # 2.4GHz RX
  "1.3.6.1.2.1.2.2.1.16.2" => %{type: "Counter32", value: 0},  # 2.4GHz TX
  "1.3.6.1.2.1.2.2.1.10.3" => %{type: "Counter32", value: 0},  # 5GHz RX
  "1.3.6.1.2.1.2.2.1.16.3" => %{type: "Counter32", value: 0}   # 5GHz TX
}

# Create the wireless AP profile
{:ok, wireless_ap_profile} = SnmpKit.SnmpSim.ProfileLoader.load_profile(
  :wireless_ap,
  {:manual, wireless_ap_oids},
  behaviors: [:counter_increment, :time_based_changes]
)

# NOTE: If you get port conflicts, restart your Livebook runtime to clean up previous devices

# Try to find an available port starting from 1164
{wireless_ap, wireless_ap_target} =
  Enum.reduce_while(1164..1170, nil, fn port, _acc ->
    case Sim.start_device(wireless_ap_profile, [port: port, community: "public"]) do
      {:ok, device} ->
        target = "127.0.0.1:#{port}"
        {:halt, {device, target}}
      {:error, :eaddrinuse} ->
        IO.puts("Port #{port} in use, trying next...")
        {:cont, nil}
      {:error, reason} ->
        IO.puts("Port #{port} failed: #{inspect(reason)}")
        {:cont, nil}
    end
  end) ||
  raise "Could not find available port for wireless AP. Try restarting runtime."

IO.puts("✅ Wireless AP simulation started on #{wireless_ap_target}")

# Test the wireless AP
{:ok, ap_desc} = SNMP.get(wireless_ap_target, "sysDescr.0")
{:ok, ap_interfaces} = SNMP.get(wireless_ap_target, "ifNumber.0")
IO.puts("Wireless AP: #{ap_desc}")
IO.puts("Interfaces: #{ap_interfaces}")

# Check wireless-specific data
case SNMP.get(wireless_ap_target, "1.3.6.1.4.1.41112.1.4.1.1.6.1") do
  {:ok, clients_24} -> IO.puts("2.4GHz Clients: #{clients_24}")
  _ -> IO.puts("2.4GHz Clients: Not available")
end

case SNMP.get(wireless_ap_target, "1.3.6.1.4.1.41112.1.4.1.1.6.2") do
  {:ok, clients_5} -> IO.puts("5GHz Clients: #{clients_5}")
  _ -> IO.puts("5GHz Clients: Not available")
end

wireless_ap_target

Congratulations! 🎉

You’ve completed the SnmpKit Interactive Tour! You’ve learned how to:

  • 🎯 Use the Unified API - Clean, context-based modules for different operations
  • 📡 Perform SNMP Operations - GET, WALK, bulk operations, and multi-target queries
  • 📚 Work with MIBs - Resolve OIDs, navigate the MIB tree, and understand SNMP data
  • 🧪 Create Device Simulations - Build realistic test environments without real hardware
  • Leverage Advanced Features - Streaming, performance optimization, and analytics

Next Steps

  1. Explore the Documentation: https://hexdocs.pm/snmpkit
  2. Try the Examples: Check out the examples/ directory for more practical use cases
  3. Read the Guides:
  4. Build Something Cool: Use SnmpKit in your own projects!

Community

Happy SNMP monitoring with SnmpKit! 🚀