defmodule SnmpKit.SnmpMgr.Config do @moduledoc """ Configuration management for SnmpKit.SnmpMgr. Provides global defaults and configuration options that can be set application-wide and used by all SNMP operations. """ use GenServer @default_config %{ community: "public", timeout: 5000, retries: 1, port: 161, version: :v2c, mib_paths: [], include_names: true, include_formatted: true, auto_start_services: true } ## Public API @doc """ Starts the configuration GenServer. """ def start_link(opts \\ []) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end @doc """ Sets the default community string for SNMP requests. ## Examples iex> SnmpKit.SnmpMgr.Config.set_default_community("private") :ok """ def set_default_community(community) when is_binary(community) do GenServer.call(__MODULE__, {:set, :community, community}) end @doc """ Gets the default community string. ## Examples iex> SnmpKit.SnmpMgr.Config.get_default_community() "public" """ def get_default_community do GenServer.call(__MODULE__, {:get, :community}) end @doc """ Sets the default timeout for SNMP requests in milliseconds. ## Examples iex> SnmpKit.SnmpMgr.Config.set_default_timeout(10000) :ok """ def set_default_timeout(timeout) when is_integer(timeout) and timeout > 0 do GenServer.call(__MODULE__, {:set, :timeout, timeout}) end @doc """ Gets the default timeout. """ def get_default_timeout do GenServer.call(__MODULE__, {:get, :timeout}) end @doc """ Sets the default number of retries for SNMP requests. ## Examples iex> SnmpKit.SnmpMgr.Config.set_default_retries(3) :ok """ def set_default_retries(retries) when is_integer(retries) and retries >= 0 do GenServer.call(__MODULE__, {:set, :retries, retries}) end @doc """ Gets the default number of retries. """ def get_default_retries do GenServer.call(__MODULE__, {:get, :retries}) end @doc """ Sets the default port for SNMP requests. ## Examples iex> SnmpKit.SnmpMgr.Config.set_default_port(1161) :ok """ def set_default_port(port) when is_integer(port) and port > 0 and port <= 65_535 do GenServer.call(__MODULE__, {:set, :port, port}) end @doc """ Gets the default port. """ def get_default_port do GenServer.call(__MODULE__, {:get, :port}) end @doc """ Sets the default SNMP version. ## Examples iex> SnmpKit.SnmpMgr.Config.set_default_version(:v2c) :ok """ def set_default_version(version) when version in [:v1, :v2c] do GenServer.call(__MODULE__, {:set, :version, version}) end @doc """ Gets the default SNMP version. """ def get_default_version do GenServer.call(__MODULE__, {:get, :version}) end @doc """ Sets the default include_names flag (controls reverse MIB lookups in results). """ def set_default_include_names(flag) when is_boolean(flag) do GenServer.call(__MODULE__, {:set, :include_names, flag}) end @doc """ Gets the default include_names flag. """ def get_default_include_names do GenServer.call(__MODULE__, {:get, :include_names}) end @doc """ Sets the default include_formatted flag (controls formatted field in results). """ def set_default_include_formatted(flag) when is_boolean(flag) do GenServer.call(__MODULE__, {:set, :include_formatted, flag}) end @doc """ Sets whether manager services auto-start on demand. """ def set_default_auto_start_services(flag) when is_boolean(flag) do GenServer.call(__MODULE__, {:set, :auto_start_services, flag}) end @doc """ Gets whether manager services auto-start on demand. """ def get_default_auto_start_services do GenServer.call(__MODULE__, {:get, :auto_start_services}) end @doc """ Gets the default include_formatted flag. """ def get_default_include_formatted do GenServer.call(__MODULE__, {:get, :include_formatted}) end @doc """ Adds a directory to the MIB search paths. ## Examples iex> SnmpKit.SnmpMgr.Config.add_mib_path("/usr/share/snmp/mibs") :ok """ def add_mib_path(path) when is_binary(path) do GenServer.call(__MODULE__, {:add_mib_path, path}) end @doc """ Sets the MIB search paths (replaces existing paths). ## Examples iex> SnmpKit.SnmpMgr.Config.set_mib_paths(["/usr/share/snmp/mibs", "./mibs"]) :ok """ def set_mib_paths(paths) when is_list(paths) do GenServer.call(__MODULE__, {:set, :mib_paths, paths}) end @doc """ Gets the current MIB search paths. """ def get_mib_paths do GenServer.call(__MODULE__, {:get, :mib_paths}) end @doc """ Gets all current configuration as a map. ## Examples iex> SnmpKit.SnmpMgr.Config.get_all() %{ community: "public", timeout: 5000, retries: 1, port: 161, version: :v2c, mib_paths: [] } """ def get_all do GenServer.call(__MODULE__, :get_all) end @doc """ Resets configuration to defaults. ## Examples iex> SnmpKit.SnmpMgr.Config.reset() :ok """ def reset do GenServer.call(__MODULE__, :reset) end @doc """ Gets configuration value with fallback to application config. This is the main function used by other modules to get configuration values. It first checks the GenServer state, then falls back to application config, then to module defaults. ## Examples iex> SnmpKit.SnmpMgr.Config.get(:community) "public" iex> SnmpKit.SnmpMgr.Config.get(:timeout) 5000 """ def get(key) do case GenServer.whereis(__MODULE__) do nil -> # GenServer not started, use application config or defaults get_from_app_config(key) _pid -> GenServer.call(__MODULE__, {:get, key}) end end @doc """ Merges the current configuration with provided opts, giving priority to opts. This is useful for functions that want to use global defaults but allow per-request overrides. ## Examples iex> SnmpKit.SnmpMgr.Config.merge_opts(community: "private", timeout: 10000) [community: "private", timeout: 10000, retries: 1, port: 161] """ def merge_opts(opts) do config = case GenServer.whereis(__MODULE__) do nil -> # Config server not running, use defaults @default_config pid when is_pid(pid) -> # Config server running, get current config with timeout case GenServer.call(pid, :get_all, 1000) do config when is_map(config) -> config _ -> @default_config end end merged = config |> Map.to_list() |> Keyword.merge(opts) merged end ## GenServer Implementation @impl true def init(opts) do # Start with defaults, then merge application environment, then passed options app_env_config = %{ community: Application.get_env(:snmp_mgr, :community, @default_config.community), timeout: Application.get_env(:snmp_mgr, :timeout, @default_config.timeout), retries: Application.get_env(:snmp_mgr, :retries, @default_config.retries), port: Application.get_env(:snmp_mgr, :port, @default_config.port), version: Application.get_env(:snmp_mgr, :version, @default_config.version), mib_paths: Application.get_env(:snmp_mgr, :mib_paths, @default_config.mib_paths), include_names: Application.get_env(:snmp_mgr, :include_names, @default_config.include_names), include_formatted: Application.get_env(:snmp_mgr, :include_formatted, @default_config.include_formatted), auto_start_services: Application.get_env(:snmp_mgr, :auto_start_services, @default_config.auto_start_services) } config = @default_config |> Map.merge(app_env_config) |> Map.merge(Map.new(opts)) {:ok, config} end @impl true def handle_call({:set, key, value}, _from, config) do new_config = Map.put(config, key, value) {:reply, :ok, new_config} end @impl true def handle_call({:get, key}, _from, config) do value = Map.get(config, key, get_from_app_config(key)) {:reply, value, config} end @impl true def handle_call({:add_mib_path, path}, _from, config) do current_paths = Map.get(config, :mib_paths, []) new_paths = if path in current_paths do current_paths else current_paths ++ [path] end new_config = Map.put(config, :mib_paths, new_paths) {:reply, :ok, new_config} end @impl true def handle_call(:get_all, _from, config) do {:reply, config, config} end @impl true def handle_call(:reset, _from, _config) do {:reply, :ok, @default_config} end @impl true def handle_call(msg, _from, config) do {:reply, {:error, {:unknown_call, msg}}, config} end ## Private Functions defp get_from_app_config(key) do case Application.get_env(:snmp_mgr, key) do nil -> Map.get(@default_config, key) value -> value end end end