Packages

A comprehensive SNMP toolkit for Elixir featuring a unified API, pure Elixir implementation, and powerful device simulation. Perfect for network monitoring, testing, and development with support for SNMP operations, MIB management, and realistic device simulation.

Current section

Files

Jump to
snmpkit lib snmpkit snmp_sim config.ex
Raw

lib/snmpkit/snmp_sim/config.ex

defmodule SnmpKit.SnmpSim.Config do
@moduledoc """
Configuration management for SnmpSim with support for JSON and YAML files.
This module provides a convenient way to configure SnmpSim using external
configuration files, making it especially useful for container deployments
and development environments.
## Supported formats
- JSON files
- YAML files (requires `yaml_elixir` dependency)
- Elixir configuration maps
## Usage
# Load from JSON file
{:ok, config} = SnmpKit.SnmpSim.Config.load_from_file("config/devices.json")
{:ok, devices} = SnmpKit.SnmpSim.Config.start_from_config(config)
# Load from YAML file
{:ok, config} = SnmpKit.SnmpSim.Config.load_yaml("config/devices.yaml")
{:ok, devices} = SnmpKit.SnmpSim.Config.start_from_config(config)
# Load from environment
{:ok, config} = SnmpKit.SnmpSim.Config.load_from_environment()
{:ok, devices} = SnmpKit.SnmpSim.Config.start_from_config(config)
"""
alias SnmpKit.SnmpSim.{Device, Performance.ResourceManager}
require Logger
@doc """
Loads configuration from a JSON file.
"""
def load_from_file(file_path) when is_binary(file_path) do
case File.read(file_path) do
{:ok, content} ->
case Jason.decode(content, keys: :atoms) do
{:ok, config} ->
validate_and_normalize_config(config)
{:error, error} ->
{:error, {:json_decode_error, error}}
end
{:error, error} ->
{:error, {:file_read_error, error}}
end
end
@doc """
Loads configuration from a YAML file.
Requires the `yaml_elixir` dependency to be added to your mix.exs:
{:yaml_elixir, "~> 2.9"}
"""
def load_yaml(file_path) when is_binary(file_path) do
if Code.ensure_loaded?(YamlElixir) do
case YamlElixir.read_from_file(file_path) do
{:ok, config} ->
# Convert string keys to atoms for consistency
atomized_config = atomize_keys(config)
validate_and_normalize_config(atomized_config)
{:error, error} ->
{:error, {:yaml_read_error, error}}
end
else
{:error,
{:missing_dependency,
"yaml_elixir package not found. Add {:yaml_elixir, \"~> 2.9\"} to your deps."}}
end
end
@doc """
Loads configuration from environment variables.
This function reads common environment variables and creates a configuration
map suitable for starting devices and configuring the system.
"""
def load_from_environment do
config = %{
snmp_sim: %{
global_settings: %{
max_devices: get_env_int("SNMP_SIM_EX_MAX_DEVICES", 1000),
max_memory_mb: get_env_int("SNMP_SIM_EX_MAX_MEMORY_MB", 512),
enable_telemetry: get_env_boolean("SNMP_SIM_EX_ENABLE_TELEMETRY", true),
enable_performance_monitoring:
get_env_boolean("SNMP_SIM_EX_ENABLE_PERFORMANCE_MONITORING", true),
host: System.get_env("SNMP_SIM_EX_HOST", "127.0.0.1"),
community: System.get_env("SNMP_SIM_EX_COMMUNITY", "public"),
worker_pool_size: get_env_int("SNMP_SIM_EX_WORKER_POOL_SIZE", 16),
socket_count: get_env_int("SNMP_SIM_EX_SOCKET_COUNT", 4)
},
device_groups: parse_device_groups_from_env(),
monitoring: %{
health_check: %{
enabled: get_env_boolean("SNMP_SIM_EX_ENABLE_HEALTH_ENDPOINT", true),
port: get_env_int("SNMP_SIM_EX_HEALTH_PORT", 4000),
path: System.get_env("SNMP_SIM_EX_HEALTH_PATH", "/health")
},
performance_monitor: %{
collection_interval_ms: get_env_int("SNMP_SIM_EX_PERF_COLLECTION_INTERVAL_MS", 30000),
alert_thresholds: %{
memory_usage_mb: get_env_int("SNMP_SIM_EX_ALERT_MEMORY_THRESHOLD_MB", 400),
response_time_ms: get_env_int("SNMP_SIM_EX_ALERT_RESPONSE_TIME_MS", 100),
error_rate_percent: get_env_float("SNMP_SIM_EX_ALERT_ERROR_RATE_PERCENT", 5.0)
}
}
}
}
}
validate_and_normalize_config(config)
end
@doc """
Starts devices and services based on the provided configuration.
"""
def start_from_config(%{snmp_sim: config}) do
Logger.info("Starting SnmpSim from configuration")
# Apply global settings
apply_global_settings(config[:global_settings] || %{})
# Start resource manager if configured
start_resource_manager(config[:global_settings])
# Start monitoring if configured
start_monitoring(config[:monitoring])
# Start device groups
devices = start_device_groups(config[:device_groups] || [])
Logger.info("SnmpSim configuration loaded successfully. Started #{length(devices)} devices.")
{:ok, devices}
end
@doc """
Validates a configuration map and provides helpful error messages.
"""
def validate_config(config) do
case validate_and_normalize_config(config) do
{:ok, _normalized} -> :ok
{:error, reason} -> {:error, reason}
end
end
@doc """
Creates a sample configuration for reference.
"""
def sample_config do
%{
snmp_sim: %{
global_settings: %{
max_devices: 1000,
max_memory_mb: 512,
enable_telemetry: true,
enable_performance_monitoring: true,
host: "127.0.0.1",
community: "public",
worker_pool_size: 16,
socket_count: 4
},
device_groups: [
%{
name: "cable_modems",
device_type: "cable_modem",
count: 100,
port_range: %{start: 30000, end: 30099},
community: "public",
walk_file: "priv/walks/cable_modem.walk",
behaviors: ["realistic_counters", "time_patterns"],
error_injection: %{
packet_loss_rate: 0.01,
timeout_rate: 0.005
}
},
%{
name: "switches",
device_type: "switch",
count: 20,
port_range: %{start: 31000, end: 31019},
community: "private",
walk_file: "priv/walks/switch.walk",
behaviors: ["realistic_counters", "correlations"]
}
],
monitoring: %{
health_check: %{
enabled: true,
port: 4000,
path: "/health"
},
performance_monitor: %{
collection_interval_ms: 30000,
alert_thresholds: %{
memory_usage_mb: 400,
response_time_ms: 100,
error_rate_percent: 5.0
}
}
}
}
}
end
@doc """
Writes a sample configuration to a file.
"""
def write_sample_config(file_path, format \\ :json) do
config = sample_config()
case format do
:json ->
json_content = Jason.encode!(config, pretty: true)
File.write(file_path, json_content)
:yaml ->
if Code.ensure_loaded?(YamlElixir) do
case Jason.encode!(config) |> then(&File.write(file_path, &1)) do
:ok -> :ok
error -> error
end
else
{:error, "yaml_elixir package not available"}
end
_ ->
{:error, "Unsupported format. Use :json or :yaml"}
end
end
# Private helper functions
defp validate_and_normalize_config(config) do
case config do
%{snmp_sim: snmp_config} ->
case validate_snmp_config(snmp_config) do
:ok -> {:ok, config}
{:error, reason} -> {:error, reason}
end
_ ->
{:error, "Configuration must contain a 'snmp_sim' key"}
end
end
defp validate_snmp_config(config) when is_map(config) do
# Validate global settings
case validate_global_settings(config[:global_settings]) do
:ok ->
# Validate device groups
case validate_device_groups(config[:device_groups]) do
:ok -> :ok
error -> error
end
error ->
error
end
end
defp validate_snmp_config(_), do: {:error, "snmp_sim configuration must be a map"}
defp validate_global_settings(nil), do: :ok
defp validate_global_settings(settings) when is_map(settings) do
# Basic validation of global settings
cond do
Map.has_key?(settings, :max_devices) and not is_integer(settings.max_devices) ->
{:error, "max_devices must be an integer"}
Map.has_key?(settings, :max_memory_mb) and not is_integer(settings.max_memory_mb) ->
{:error, "max_memory_mb must be an integer"}
true ->
:ok
end
end
defp validate_global_settings(_), do: {:error, "global_settings must be a map"}
defp validate_device_groups(nil), do: :ok
defp validate_device_groups(groups) when is_list(groups) do
# Validate each device group
Enum.reduce_while(groups, :ok, fn group, _acc ->
case validate_device_group(group) do
:ok -> {:cont, :ok}
error -> {:halt, error}
end
end)
end
defp validate_device_groups(_), do: {:error, "device_groups must be a list"}
defp validate_device_group(group) when is_map(group) do
required_fields = [:name, :count, :port_range]
case Enum.find(required_fields, fn field -> not Map.has_key?(group, field) end) do
nil ->
# Validate port range
case group.port_range do
%{start: start_port, end: end_port}
when is_integer(start_port) and is_integer(end_port) ->
if start_port <= end_port do
:ok
else
{:error, "port_range start must be <= end"}
end
_ ->
{:error, "port_range must have 'start' and 'end' integer fields"}
end
missing_field ->
{:error, "device_group missing required field: #{missing_field}"}
end
end
defp validate_device_group(_), do: {:error, "device_group must be a map"}
defp apply_global_settings(settings) do
Enum.each(settings, fn {key, value} ->
case key do
:max_devices ->
Application.put_env(:snmp_sim, :max_devices, value)
:max_memory_mb ->
Application.put_env(:snmp_sim, :max_memory_mb, value)
:enable_telemetry ->
Application.put_env(:snmp_sim, :enable_telemetry, value)
:enable_performance_monitoring ->
Application.put_env(:snmp_sim, :enable_performance_monitoring, value)
:worker_pool_size ->
Application.put_env(:snmp_sim, :worker_pool_size, value)
:socket_count ->
Application.put_env(:snmp_sim, :socket_count, value)
# Ignore unknown settings
_ ->
:ok
end
end)
end
defp start_resource_manager(settings) do
if settings[:max_devices] != nil or settings[:max_memory_mb] != nil do
resource_config = [
max_devices: settings[:max_devices] || 1000,
max_memory_mb: settings[:max_memory_mb] || 512
]
case ResourceManager.start_link(resource_config) do
{:ok, _pid} ->
Logger.info("Started ResourceManager with config: #{inspect(resource_config)}")
{:error, {:already_started, _pid}} ->
Logger.debug("ResourceManager already started")
{:error, reason} ->
Logger.warning("Failed to start ResourceManager: #{inspect(reason)}")
end
end
end
defp start_monitoring(nil), do: :ok
defp start_monitoring(monitoring_config) do
# Start health check if configured
if get_in(monitoring_config, [:health_check, :enabled]) do
health_config = monitoring_config[:health_check]
Logger.info("Health check enabled on port #{health_config[:port]}")
# Health check would be started here if we had that module
end
# Start performance monitoring if configured
if monitoring_config[:performance_monitor] do
Logger.info("Performance monitoring configured")
# Performance monitor configuration would be applied here
end
end
defp start_device_groups(device_groups) do
device_groups
|> Enum.flat_map(&start_device_group/1)
|> List.flatten()
end
defp start_device_group(group) do
Logger.info("Starting device group: #{group[:name]} (#{group[:count]} devices)")
port_range = group[:port_range]
start_port = port_range[:start]
count = group[:count]
behaviors = parse_behaviors(group[:behaviors] || [])
devices =
for i <- 0..(count - 1) do
port = start_port + i
device_config = %{
port: port,
device_type: String.to_atom(group[:device_type] || "cable_modem"),
device_id: "#{group[:name]}_#{port}",
community: group[:community] || "public"
}
case DynamicSupervisor.start_child(SnmpSim.DeviceSupervisor, {Device, device_config}) do
{:ok, device} ->
# Apply behaviors if specified
apply_device_behaviors(device, behaviors)
# Apply error injection if specified
apply_error_injection(device, group[:error_injection])
device
{:error, reason} ->
Logger.warning("Failed to start device on port #{port}: #{inspect(reason)}")
nil
end
end
# Filter out failed devices
successful_devices = Enum.filter(devices, &(&1 != nil))
Logger.info(
"Successfully started #{length(successful_devices)}/#{count} devices for group #{group[:name]}"
)
successful_devices
end
defp parse_behaviors(behaviors) when is_list(behaviors) do
Enum.map(behaviors, fn behavior ->
case behavior do
"realistic_counters" -> :realistic_counters
"time_patterns" -> :time_patterns
"correlations" -> :correlations
"seasonal_patterns" -> :seasonal_patterns
atom when is_atom(atom) -> atom
string when is_binary(string) -> String.to_atom(string)
_ -> nil
end
end)
|> Enum.filter(&(&1 != nil))
end
defp apply_device_behaviors(_device, []), do: :ok
defp apply_device_behaviors(device, behaviors) do
# Apply behaviors to device
# This would integrate with the existing behavior system
Logger.debug("Applied behaviors #{inspect(behaviors)} to device #{inspect(device)}")
end
defp apply_error_injection(_device, nil), do: :ok
defp apply_error_injection(device, error_config) when is_map(error_config) do
# Apply error injection configuration
if packet_loss_rate = error_config[:packet_loss_rate] do
# Would apply packet loss injection
Logger.debug("Applied packet loss rate #{packet_loss_rate} to device #{inspect(device)}")
end
if timeout_rate = error_config[:timeout_rate] do
# Would apply timeout injection
Logger.debug("Applied timeout rate #{timeout_rate} to device #{inspect(device)}")
end
end
defp parse_device_groups_from_env do
# Parse device groups from environment variables
# This is a simplified implementation - could be extended for more complex scenarios
device_count = get_env_int("SNMP_SIM_EX_DEVICE_COUNT", 0)
port_start = get_env_int("SNMP_SIM_EX_PORT_RANGE_START", 30000)
walk_file = System.get_env("SNMP_SIM_EX_WALK_FILE", "priv/walks/cable_modem.walk")
community = System.get_env("SNMP_SIM_EX_COMMUNITY", "public")
if device_count > 0 do
[
%{
name: "env_devices",
device_type: "generic",
count: device_count,
port_range: %{
start: port_start,
end: port_start + device_count - 1
},
community: community,
walk_file: walk_file,
behaviors: []
}
]
else
[]
end
end
defp atomize_keys(map) when is_map(map) do
map
|> Enum.map(fn {k, v} -> {atomize_key(k), atomize_keys(v)} end)
|> Enum.into(%{})
end
defp atomize_keys(list) when is_list(list) do
Enum.map(list, &atomize_keys/1)
end
defp atomize_keys(value), do: value
defp atomize_key(key) when is_binary(key), do: String.to_atom(key)
defp atomize_key(key), do: key
defp get_env_int(var_name, default) do
case System.get_env(var_name) do
nil -> default
value -> String.to_integer(value)
end
end
defp get_env_float(var_name, default) do
case System.get_env(var_name) do
nil -> default
value -> String.to_float(value)
end
end
defp get_env_boolean(var_name, default) do
case System.get_env(var_name) do
nil -> default
"true" -> true
"false" -> false
_ -> default
end
end
end