- Billing subscription and inventory item status fields are scalars, not objects - IP blocks use "block" key, not "subnet" - Strip nil variables from GraphQL pagination (Gaiia returns null for after:null) - Handle nil root keys in paginated responses gracefully - Handle empty string response from Preseem scores endpoint
132 lines
3.7 KiB
Elixir
132 lines
3.7 KiB
Elixir
defmodule Towerops.Preseem.Client do
|
|
@moduledoc """
|
|
HTTP client for the Preseem Model API.
|
|
|
|
Uses Req with built-in test support via `Req.Test`.
|
|
Auth: HTTP Basic (API key as username, empty password).
|
|
"""
|
|
|
|
require Logger
|
|
|
|
@base_url "https://api.preseem.com/model/v1"
|
|
|
|
@doc """
|
|
Tests the connection to the Preseem API by fetching a page of access points.
|
|
|
|
Returns `{:ok, body}` on success, `{:error, reason}` on failure.
|
|
"""
|
|
def test_connection(api_key) do
|
|
case request(:get, "#{@base_url}/access_points?limit=100", api_key) do
|
|
{:ok, %{status: status, body: body}} when status in 200..299 ->
|
|
{:ok, body}
|
|
|
|
{:ok, %{status: 401}} ->
|
|
{:error, :unauthorized}
|
|
|
|
{:ok, %{status: 403}} ->
|
|
{:error, :forbidden}
|
|
|
|
{:ok, %{status: status, body: body}} ->
|
|
Logger.warning("Preseem API unexpected status #{status}: #{inspect(body)}")
|
|
{:error, {:unexpected_status, status}}
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Preseem API connection error: #{inspect(reason)}")
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Lists all access points with scores from the Preseem API.
|
|
|
|
Fetches from `/access_point_scores` which returns rich data per AP.
|
|
Returns `{:ok, [access_point_score]}` on success.
|
|
"""
|
|
def list_access_points(api_key) do
|
|
case request(:get, "#{@base_url}/access_point_scores", api_key) do
|
|
{:ok, %{status: status, body: body}} when status in 200..299 ->
|
|
{:ok, normalize_scores_response(body)}
|
|
|
|
{:ok, %{status: 401}} ->
|
|
{:error, :unauthorized}
|
|
|
|
{:ok, %{status: 403}} ->
|
|
{:error, :forbidden}
|
|
|
|
{:ok, %{status: status, body: body}} ->
|
|
{:error, {:unexpected_status, status, body}}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Lists access points from the basic `/access_points` endpoint with pagination.
|
|
|
|
Returns `{:ok, [access_point]}` on success.
|
|
"""
|
|
def list_access_points_basic(api_key) do
|
|
fetch_all_pages(api_key, "#{@base_url}/access_points?limit=500", [])
|
|
end
|
|
|
|
defp fetch_all_pages(api_key, url, acc) do
|
|
case request(:get, url, api_key) do
|
|
{:ok, %{status: status, body: %{"data" => data, "paginator" => paginator}}}
|
|
when status in 200..299 ->
|
|
all = acc ++ data
|
|
|
|
if paginator["page"] < paginator["page_count"] do
|
|
next_page = paginator["page"] + 1
|
|
next_url = "#{@base_url}/access_points?limit=500&page=#{next_page}"
|
|
fetch_all_pages(api_key, next_url, all)
|
|
else
|
|
{:ok, all}
|
|
end
|
|
|
|
{:ok, %{status: 401}} ->
|
|
{:error, :unauthorized}
|
|
|
|
{:ok, %{status: 403}} ->
|
|
{:error, :forbidden}
|
|
|
|
{:ok, %{status: status, body: body}} ->
|
|
{:error, {:unexpected_status, status, body}}
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
# The scores endpoint may return an array, a wrapped object with "data" key,
|
|
# or a single object. Normalize to a list.
|
|
defp normalize_scores_response(body) when is_binary(body), do: []
|
|
|
|
defp normalize_scores_response(body) when is_list(body), do: body
|
|
|
|
defp normalize_scores_response(%{"data" => data}) when is_list(data), do: data
|
|
|
|
defp normalize_scores_response(body) when is_map(body), do: [body]
|
|
|
|
defp request(method, url, api_key) do
|
|
opts = [
|
|
method: method,
|
|
url: url,
|
|
auth: {:basic, "#{api_key}:"},
|
|
headers: [{"accept", "application/json"}]
|
|
]
|
|
|
|
# Only use Req.Test plug in test environment
|
|
opts =
|
|
if Application.get_env(:towerops, :env) == :test do
|
|
Keyword.put(opts, :plug, {Req.Test, __MODULE__})
|
|
else
|
|
opts
|
|
end
|
|
|
|
Req.request(opts)
|
|
rescue
|
|
exception ->
|
|
{:error, Exception.message(exception)}
|
|
end
|
|
end
|