Current section
Files
Jump to
Current section
Files
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