185 lines
5.2 KiB
Elixir
185 lines
5.2 KiB
Elixir
defmodule Towerops.Sonar.Client do
|
|
@moduledoc """
|
|
GraphQL client for the Sonar API.
|
|
|
|
Uses Req with page-based pagination for all list endpoints.
|
|
"""
|
|
|
|
require Logger
|
|
|
|
@accounts_query """
|
|
query ListAccounts($page: Int!, $records_per_page: Int!) {
|
|
accounts(paginator: {page: $page, records_per_page: $records_per_page}) {
|
|
entities {
|
|
id
|
|
name
|
|
account_status { name }
|
|
account_type { name }
|
|
addresses { line1 city state zip country latitude longitude }
|
|
account_services {
|
|
name
|
|
service { name }
|
|
price_override
|
|
next_bill_date
|
|
}
|
|
}
|
|
page_info { page total_pages records_per_page total_count }
|
|
}
|
|
}
|
|
"""
|
|
|
|
@network_sites_query """
|
|
query ListNetworkSites($page: Int!, $records_per_page: Int!) {
|
|
network_sites(paginator: {page: $page, records_per_page: $records_per_page}) {
|
|
entities {
|
|
id
|
|
name
|
|
latitude
|
|
longitude
|
|
ip_assignments { ip_address subnet description }
|
|
}
|
|
page_info { page total_pages records_per_page total_count }
|
|
}
|
|
}
|
|
"""
|
|
|
|
@inventory_items_query """
|
|
query ListInventoryItems($page: Int!, $records_per_page: Int!) {
|
|
inventory_items(paginator: {page: $page, records_per_page: $records_per_page}) {
|
|
entities {
|
|
id
|
|
name
|
|
inventory_model { name manufacturer { name } }
|
|
assignee { id __typename }
|
|
}
|
|
page_info { page total_pages records_per_page total_count }
|
|
}
|
|
}
|
|
"""
|
|
|
|
@doc "Test the connection to Sonar by fetching a single account."
|
|
def test_connection(instance_url, api_token) do
|
|
test_query = """
|
|
{ accounts(paginator: {page: 1, records_per_page: 1}) {
|
|
entities { id }
|
|
}
|
|
}
|
|
"""
|
|
|
|
case query(instance_url, api_token, test_query) do
|
|
{:ok, _data} -> {:ok, %{}}
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc "List all accounts with automatic pagination."
|
|
def list_accounts(instance_url, api_token) do
|
|
paginate(instance_url, api_token, @accounts_query, "accounts")
|
|
end
|
|
|
|
@doc "List all network sites with automatic pagination."
|
|
def list_network_sites(instance_url, api_token) do
|
|
paginate(instance_url, api_token, @network_sites_query, "network_sites")
|
|
end
|
|
|
|
@doc "List all inventory items with automatic pagination."
|
|
def list_inventory_items(instance_url, api_token) do
|
|
paginate(instance_url, api_token, @inventory_items_query, "inventory_items")
|
|
end
|
|
|
|
@doc "Execute a raw GraphQL query against the Sonar API."
|
|
def query(instance_url, api_token, query_string, variables \\ %{}) do
|
|
case request(instance_url, api_token, query_string, variables) do
|
|
{:ok, %{status: status, body: %{"data" => data}}} when status in 200..299 ->
|
|
{:ok, data}
|
|
|
|
{:ok, %{status: status, body: %{"errors" => errors}}} when status in [200, 400] ->
|
|
{:error, {:graphql_errors, errors}}
|
|
|
|
{:ok, %{status: 401}} ->
|
|
{:error, :unauthorized}
|
|
|
|
{:ok, %{status: 403}} ->
|
|
{:error, :forbidden}
|
|
|
|
{:ok, %{status: 429, headers: headers}} ->
|
|
retry_after = get_retry_after(headers)
|
|
{:error, {:rate_limited, retry_after}}
|
|
|
|
{:ok, %{status: status, body: body}} ->
|
|
Logger.warning("Sonar API unexpected status #{status}: #{inspect(body)}")
|
|
{:error, {:unexpected_status, status}}
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Sonar API connection error: #{inspect(reason)}")
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp paginate(instance_url, api_token, query_string, root_key, page \\ 1, acc \\ []) do
|
|
variables = %{"page" => page, "records_per_page" => 100}
|
|
|
|
case query(instance_url, api_token, query_string, variables) do
|
|
{:ok, data} ->
|
|
root = data[root_key]
|
|
entities = root["entities"] || []
|
|
all_entities = acc ++ entities
|
|
|
|
page_info = root["page_info"]
|
|
|
|
if page < page_info["total_pages"] do
|
|
paginate(instance_url, api_token, query_string, root_key, page + 1, all_entities)
|
|
else
|
|
{:ok, all_entities}
|
|
end
|
|
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp request(instance_url, api_token, query_string, variables) do
|
|
url = String.trim_trailing(instance_url, "/") <> "/api/graphql"
|
|
|
|
req_opts = [
|
|
method: :post,
|
|
url: url,
|
|
headers: [
|
|
{"authorization", "Bearer #{api_token}"},
|
|
{"content-type", "application/json"}
|
|
],
|
|
json: %{"query" => query_string, "variables" => variables}
|
|
]
|
|
|
|
req_opts =
|
|
if Application.get_env(:towerops, :env) == :test do
|
|
Keyword.put(req_opts, :plug, {Req.Test, __MODULE__})
|
|
else
|
|
req_opts
|
|
end
|
|
|
|
Req.request(req_opts)
|
|
rescue
|
|
exception ->
|
|
{:error, Exception.message(exception)}
|
|
end
|
|
|
|
defp get_retry_after(headers) when is_map(headers) do
|
|
case Map.get(headers, "retry-after") do
|
|
[value | _] -> parse_retry_after(value)
|
|
_ -> 60
|
|
end
|
|
end
|
|
|
|
defp get_retry_after(_headers), do: 60
|
|
|
|
defp parse_retry_after(value) when is_binary(value) do
|
|
case Integer.parse(value) do
|
|
{seconds, _} -> seconds
|
|
:error -> 60
|
|
end
|
|
end
|
|
|
|
defp parse_retry_after(value) when is_integer(value), do: value
|
|
defp parse_retry_after(_), do: 60
|
|
end
|