diff --git a/agent/lib/proxmox_agent/config.ex b/agent/lib/proxmox_agent/config.ex new file mode 100644 index 0000000..7761f3f --- /dev/null +++ b/agent/lib/proxmox_agent/config.ex @@ -0,0 +1,77 @@ +defmodule ProxmoxAgent.Config do + @moduledoc "Loads and validates the TOML agent config." + + defstruct [ + :server_url, + :token, + :host_id, + fast_seconds: 30, + medium_seconds: 300, + slow_seconds: 1800 + ] + + @type t :: %__MODULE__{ + server_url: String.t(), + token: String.t(), + host_id: String.t(), + fast_seconds: pos_integer(), + medium_seconds: pos_integer(), + slow_seconds: pos_integer() + } + + @required ~w(server_url token)a + + @spec load(Path.t()) :: + {:ok, t()} + | {:error, {:file_read, term()} | {:parse, term()} | {:missing_key, atom()}} + def load(path) do + with {:ok, body} <- read_file(path), + {:ok, parsed} <- parse_toml(body), + :ok <- validate_required(parsed) do + {:ok, build(parsed)} + end + end + + defp read_file(path) do + case File.read(path) do + {:ok, body} -> {:ok, body} + {:error, reason} -> {:error, {:file_read, reason}} + end + end + + defp parse_toml(body) do + case Toml.decode(body) do + {:ok, map} -> {:ok, map} + {:error, reason} -> {:error, {:parse, reason}} + end + end + + defp validate_required(map) do + Enum.find_value(@required, :ok, fn key -> + case Map.get(map, Atom.to_string(key)) do + v when is_binary(v) and v != "" -> nil + _ -> {:error, {:missing_key, key}} + end + end) + end + + defp build(map) do + intervals = Map.get(map, "intervals", %{}) + + %__MODULE__{ + server_url: map["server_url"], + token: map["token"], + host_id: map["host_id"] || hostname(), + fast_seconds: Map.get(intervals, "fast_seconds", 30), + medium_seconds: Map.get(intervals, "medium_seconds", 300), + slow_seconds: Map.get(intervals, "slow_seconds", 1800) + } + end + + defp hostname do + case :inet.gethostname() do + {:ok, name} -> List.to_string(name) + _ -> "unknown-host" + end + end +end diff --git a/agent/test/fixtures/agent.toml b/agent/test/fixtures/agent.toml new file mode 100644 index 0000000..9498b39 --- /dev/null +++ b/agent/test/fixtures/agent.toml @@ -0,0 +1,8 @@ +server_url = "wss://monitor.example.com/socket/websocket" +token = "test_token_123" +host_id = "pve-test-01" + +[intervals] +fast_seconds = 15 +medium_seconds = 120 +slow_seconds = 600 diff --git a/agent/test/proxmox_agent/config_test.exs b/agent/test/proxmox_agent/config_test.exs new file mode 100644 index 0000000..55b0bc5 --- /dev/null +++ b/agent/test/proxmox_agent/config_test.exs @@ -0,0 +1,62 @@ +defmodule ProxmoxAgent.ConfigTest do + use ExUnit.Case, async: true + + alias ProxmoxAgent.Config + + @fixture Path.expand("../fixtures/agent.toml", __DIR__) + + describe "load/1" do + test "parses required fields" do + assert {:ok, cfg} = Config.load(@fixture) + assert cfg.server_url == "wss://monitor.example.com/socket/websocket" + assert cfg.token == "test_token_123" + assert cfg.host_id == "pve-test-01" + assert cfg.fast_seconds == 15 + assert cfg.medium_seconds == 120 + assert cfg.slow_seconds == 600 + end + + test "returns error for missing file" do + assert {:error, {:file_read, _}} = Config.load("/does/not/exist.toml") + end + + test "defaults host_id to system hostname when absent" do + tmp = Path.join(System.tmp_dir!(), "agent_nohost.toml") + + File.write!(tmp, """ + server_url = "wss://x/socket/websocket" + token = "t" + """) + + on_exit(fn -> File.rm(tmp) end) + + assert {:ok, cfg} = Config.load(tmp) + assert is_binary(cfg.host_id) + assert cfg.host_id != "" + end + + test "applies default intervals when [intervals] is absent" do + tmp = Path.join(System.tmp_dir!(), "agent_nointervals.toml") + + File.write!(tmp, """ + server_url = "wss://x/socket/websocket" + token = "t" + host_id = "h" + """) + + on_exit(fn -> File.rm(tmp) end) + + assert {:ok, cfg} = Config.load(tmp) + assert cfg.fast_seconds == 30 + assert cfg.medium_seconds == 300 + assert cfg.slow_seconds == 1800 + end + + test "returns error when required keys missing" do + tmp = Path.join(System.tmp_dir!(), "agent_bad.toml") + File.write!(tmp, "token = \"t\"\n") + on_exit(fn -> File.rm(tmp) end) + assert {:error, {:missing_key, :server_url}} = Config.load(tmp) + end + end +end