- Three sync modes: NetBox→TowerOps (pull), TowerOps→NetBox (push), Bidirectional - Configurable entity sync: devices, sites, IP addresses, interfaces - Optional filters by device role, site, and tag (comma-separated) - NetBox client with test connection, CRUD operations - Radio card UI for sync direction, checkbox cards for entity selection - Credentials stored encrypted (URL, API token, sync config)
145 lines
3.6 KiB
Elixir
145 lines
3.6 KiB
Elixir
defmodule Towerops.NetBox.Client do
|
|
@moduledoc """
|
|
HTTP client for the NetBox REST API.
|
|
|
|
Handles authentication, pagination, and common CRUD operations
|
|
against a user-provided NetBox instance.
|
|
"""
|
|
|
|
require Logger
|
|
|
|
@doc """
|
|
Test connectivity and authentication against a NetBox instance.
|
|
Returns `{:ok, info}` with the NetBox version or `{:error, reason}`.
|
|
"""
|
|
def test_connection(url, token) do
|
|
case get(url, "/api/status/", token) do
|
|
{:ok, %{"netbox-version" => version}} ->
|
|
{:ok, "Connected to NetBox #{version}"}
|
|
|
|
{:ok, %{"django-version" => _}} ->
|
|
{:ok, "Connected to NetBox"}
|
|
|
|
{:ok, _body} ->
|
|
{:ok, "Connected to NetBox"}
|
|
|
|
{:error, :unauthorized} ->
|
|
{:error, "Authentication failed — check your API token"}
|
|
|
|
{:error, :not_found} ->
|
|
{:error, "NetBox API not found at this URL — check the URL"}
|
|
|
|
{:error, reason} ->
|
|
{:error, "Connection failed: #{reason}"}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
List devices from NetBox with optional filters.
|
|
"""
|
|
def list_devices(url, token, params \\ %{}) do
|
|
get(url, "/api/dcim/devices/", token, params)
|
|
end
|
|
|
|
@doc """
|
|
List sites from NetBox with optional filters.
|
|
"""
|
|
def list_sites(url, token, params \\ %{}) do
|
|
get(url, "/api/dcim/sites/", token, params)
|
|
end
|
|
|
|
@doc """
|
|
List IP addresses from NetBox with optional filters.
|
|
"""
|
|
def list_ip_addresses(url, token, params \\ %{}) do
|
|
get(url, "/api/ipam/ip-addresses/", token, params)
|
|
end
|
|
|
|
@doc """
|
|
List interfaces from NetBox with optional filters.
|
|
"""
|
|
def list_interfaces(url, token, params \\ %{}) do
|
|
get(url, "/api/dcim/interfaces/", token, params)
|
|
end
|
|
|
|
@doc """
|
|
Create or update a device in NetBox.
|
|
"""
|
|
def upsert_device(url, token, attrs) do
|
|
post(url, "/api/dcim/devices/", token, attrs)
|
|
end
|
|
|
|
@doc """
|
|
Create or update a site in NetBox.
|
|
"""
|
|
def upsert_site(url, token, attrs) do
|
|
post(url, "/api/dcim/sites/", token, attrs)
|
|
end
|
|
|
|
# -- HTTP helpers --
|
|
|
|
defp get(base_url, path, token, params \\ %{}) do
|
|
url = normalize_url(base_url) <> path
|
|
|
|
case Req.get(url,
|
|
headers: auth_headers(token),
|
|
params: params,
|
|
connect_options: [timeout: 10_000],
|
|
receive_timeout: 15_000
|
|
) do
|
|
{:ok, %Req.Response{status: 200, body: body}} ->
|
|
{:ok, body}
|
|
|
|
{:ok, %Req.Response{status: 401}} ->
|
|
{:error, :unauthorized}
|
|
|
|
{:ok, %Req.Response{status: 403}} ->
|
|
{:error, :unauthorized}
|
|
|
|
{:ok, %Req.Response{status: 404}} ->
|
|
{:error, :not_found}
|
|
|
|
{:ok, %Req.Response{status: status}} ->
|
|
{:error, "HTTP #{status}"}
|
|
|
|
{:error, %Req.TransportError{reason: reason}} ->
|
|
{:error, "#{reason}"}
|
|
|
|
{:error, reason} ->
|
|
{:error, inspect(reason)}
|
|
end
|
|
end
|
|
|
|
defp post(base_url, path, token, body) do
|
|
url = normalize_url(base_url) <> path
|
|
|
|
case Req.post(url,
|
|
headers: auth_headers(token),
|
|
json: body,
|
|
connect_options: [timeout: 10_000],
|
|
receive_timeout: 15_000
|
|
) do
|
|
{:ok, %Req.Response{status: status, body: body}} when status in [200, 201] ->
|
|
{:ok, body}
|
|
|
|
{:ok, %Req.Response{status: 401}} ->
|
|
{:error, :unauthorized}
|
|
|
|
{:ok, %Req.Response{status: status, body: body}} ->
|
|
{:error, "HTTP #{status}: #{inspect(body)}"}
|
|
|
|
{:error, reason} ->
|
|
{:error, inspect(reason)}
|
|
end
|
|
end
|
|
|
|
defp auth_headers(token) do
|
|
[{"authorization", "Token #{token}"}, {"accept", "application/json"}]
|
|
end
|
|
|
|
defp normalize_url(url) do
|
|
url
|
|
|> String.trim()
|
|
|> String.trim_trailing("/")
|
|
end
|
|
end
|