add gaiia write-back actions (stage 6)
Three write-back capabilities: ticket creation from alerts, subscriber notes during outages, and inventory item updates. Adds GraphQL mutation methods to the client and an Actions module that handles credential retrieval and API call orchestration.
This commit is contained in:
parent
934ee5bb86
commit
6da05d461b
3 changed files with 291 additions and 0 deletions
103
lib/towerops/gaiia/actions.ex
Normal file
103
lib/towerops/gaiia/actions.ex
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
defmodule Towerops.Gaiia.Actions do
|
||||
@moduledoc """
|
||||
Write-back actions from Towerops to Gaiia.
|
||||
|
||||
Handles:
|
||||
- Ticket creation from alerts
|
||||
- Subscriber notes during outages
|
||||
- Inventory item updates (IP, firmware, serial)
|
||||
"""
|
||||
|
||||
alias Towerops.Gaiia.Client
|
||||
alias Towerops.Integrations
|
||||
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Create a Gaiia ticket for an alert.
|
||||
|
||||
Returns `{:ok, ticket_id}` on success, or `{:error, reason}`.
|
||||
"""
|
||||
def create_ticket_for_alert(organization_id, alert) do
|
||||
with {:ok, api_key} <- get_api_key(organization_id) do
|
||||
subject = "Device Down: #{alert.device.name}"
|
||||
|
||||
description =
|
||||
"Alert: #{alert.message}\nTriggered: #{DateTime.to_iso8601(alert.triggered_at)}"
|
||||
|
||||
variables = %{
|
||||
"subject" => subject,
|
||||
"description" => description
|
||||
}
|
||||
|
||||
case Client.create_ticket(api_key, variables) do
|
||||
{:ok, %{"createExternalTicket" => %{"id" => ticket_id}}} ->
|
||||
{:ok, ticket_id}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("Failed to create Gaiia ticket for alert #{alert.id}: #{inspect(reason)}")
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create a note on a Gaiia account (e.g., during an outage).
|
||||
|
||||
Returns `{:ok, note_id}` on success, or `{:error, reason}`.
|
||||
"""
|
||||
def create_subscriber_note(organization_id, account_gaiia_id, content) do
|
||||
with {:ok, api_key} <- get_api_key(organization_id) do
|
||||
case Client.create_note(api_key, "ACCOUNT", account_gaiia_id, content) do
|
||||
{:ok, %{"createNote" => %{"id" => note_id}}} ->
|
||||
{:ok, note_id}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("Failed to create Gaiia note for account #{account_gaiia_id}: #{inspect(reason)}")
|
||||
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Update a Gaiia inventory item with field changes.
|
||||
|
||||
Accepts a map of fields to update (e.g., `%{ip_address: "10.0.0.5"}`).
|
||||
Returns `{:ok, data}` on success, or `{:error, reason}`.
|
||||
"""
|
||||
def update_inventory_item(organization_id, item_gaiia_id, fields) do
|
||||
with {:ok, api_key} <- get_api_key(organization_id) do
|
||||
input = build_inventory_input(fields)
|
||||
|
||||
case Client.update_inventory_item(api_key, item_gaiia_id, input) do
|
||||
{:ok, %{"updateInventoryItem" => data}} ->
|
||||
{:ok, data}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("Failed to update Gaiia inventory item #{item_gaiia_id}: #{inspect(reason)}")
|
||||
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp get_api_key(organization_id) do
|
||||
case Integrations.get_integration(organization_id, "gaiia") do
|
||||
{:ok, %{credentials: %{"api_key" => api_key}}} when is_binary(api_key) ->
|
||||
{:ok, api_key}
|
||||
|
||||
_ ->
|
||||
{:error, :no_integration}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_inventory_input(fields) do
|
||||
Enum.reduce(fields, %{}, fn
|
||||
{:ip_address, value}, acc -> Map.put(acc, "ipAddress", value)
|
||||
{:serial_number, value}, acc -> Map.put(acc, "serialNumber", value)
|
||||
{:mac_address, value}, acc -> Map.put(acc, "macAddress", value)
|
||||
{key, value}, acc -> Map.put(acc, to_string(key), value)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
@ -141,6 +141,67 @@ defmodule Towerops.Gaiia.Client do
|
|||
}
|
||||
"""
|
||||
|
||||
# --- Mutations ---
|
||||
|
||||
@create_ticket_mutation """
|
||||
mutation CreateExternalTicket($subject: String!, $description: String!, $entityId: ID, $entityType: String) {
|
||||
createExternalTicket(input: {
|
||||
subject: $subject,
|
||||
description: $description,
|
||||
entityId: $entityId,
|
||||
entityType: $entityType
|
||||
}) {
|
||||
id
|
||||
subject
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@create_note_mutation """
|
||||
mutation CreateNote($entityId: ID!, $entityType: String!, $body: String!) {
|
||||
createNote(input: {
|
||||
entityId: $entityId,
|
||||
entityType: $entityType,
|
||||
body: $body
|
||||
}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@update_inventory_item_mutation """
|
||||
mutation UpdateInventoryItem($id: ID!, $input: UpdateInventoryItemInput!) {
|
||||
updateInventoryItem(id: $id, input: $input) {
|
||||
id
|
||||
ipAddress
|
||||
serialNumber
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@doc "Create an external ticket in Gaiia."
|
||||
def create_ticket(api_key, attrs, opts \\ []) do
|
||||
query(api_key, @create_ticket_mutation, attrs, opts)
|
||||
end
|
||||
|
||||
@doc "Create a note on a Gaiia entity (account, inventory item, etc)."
|
||||
def create_note(api_key, entity_type, entity_id, body, opts \\ []) do
|
||||
variables = %{
|
||||
"entityId" => entity_id,
|
||||
"entityType" => entity_type,
|
||||
"body" => body
|
||||
}
|
||||
|
||||
query(api_key, @create_note_mutation, variables, opts)
|
||||
end
|
||||
|
||||
@doc "Update an inventory item in Gaiia."
|
||||
def update_inventory_item(api_key, item_id, input, opts \\ []) do
|
||||
query(api_key, @update_inventory_item_mutation, %{"id" => item_id, "input" => input}, opts)
|
||||
end
|
||||
|
||||
# --- Queries ---
|
||||
|
||||
@doc "Test the connection to Gaiia by running a simple viewer query."
|
||||
def test_connection(api_key, opts \\ []) do
|
||||
case query(api_key, "{ viewer { id } }", %{}, opts) do
|
||||
|
|
|
|||
127
test/towerops/gaiia/actions_test.exs
Normal file
127
test/towerops/gaiia/actions_test.exs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
defmodule Towerops.Gaiia.ActionsTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
import Towerops.DevicesFixtures
|
||||
import Towerops.OrganizationsFixtures
|
||||
|
||||
alias Towerops.Gaiia.Actions
|
||||
alias Towerops.Gaiia.Client
|
||||
alias Towerops.Integrations
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
org = organization_fixture(user.id)
|
||||
|
||||
{:ok, integration} =
|
||||
Integrations.create_integration(org.id, %{
|
||||
provider: "gaiia",
|
||||
enabled: true,
|
||||
credentials: %{"api_key" => "test-key"}
|
||||
})
|
||||
|
||||
%{org: org, integration: integration}
|
||||
end
|
||||
|
||||
describe "create_ticket_for_alert/3" do
|
||||
test "creates a ticket via Gaiia API and returns ticket ID", %{org: org} do
|
||||
device = device_fixture(%{organization_id: org.id, name: "Tower Router"})
|
||||
|
||||
alert = %{
|
||||
id: Ecto.UUID.generate(),
|
||||
device: device,
|
||||
alert_type: :device_down,
|
||||
message: "Device is not responding to SNMP",
|
||||
triggered_at: DateTime.utc_now()
|
||||
}
|
||||
|
||||
Req.Test.stub(Client, fn conn ->
|
||||
{:ok, body, conn} = Plug.Conn.read_body(conn)
|
||||
decoded = Jason.decode!(body)
|
||||
assert decoded["query"] =~ "createExternalTicket"
|
||||
|
||||
Req.Test.json(conn, %{
|
||||
"data" => %{
|
||||
"createExternalTicket" => %{
|
||||
"id" => "gaiia-ticket-123",
|
||||
"subject" => "Device Down: Tower Router"
|
||||
}
|
||||
}
|
||||
})
|
||||
end)
|
||||
|
||||
assert {:ok, "gaiia-ticket-123"} = Actions.create_ticket_for_alert(org.id, alert)
|
||||
end
|
||||
|
||||
test "returns error when no integration exists" do
|
||||
org_id = Ecto.UUID.generate()
|
||||
|
||||
alert = %{
|
||||
id: Ecto.UUID.generate(),
|
||||
alert_type: :device_down,
|
||||
device: %{name: "Test"},
|
||||
message: "Down",
|
||||
triggered_at: DateTime.utc_now()
|
||||
}
|
||||
|
||||
assert {:error, :no_integration} = Actions.create_ticket_for_alert(org_id, alert)
|
||||
end
|
||||
end
|
||||
|
||||
describe "create_subscriber_note/4" do
|
||||
test "creates a note on a Gaiia account", %{org: org} do
|
||||
Req.Test.stub(Client, fn conn ->
|
||||
{:ok, body, conn} = Plug.Conn.read_body(conn)
|
||||
decoded = Jason.decode!(body)
|
||||
assert decoded["query"] =~ "createNote"
|
||||
|
||||
Req.Test.json(conn, %{
|
||||
"data" => %{
|
||||
"createNote" => %{
|
||||
"id" => "gaiia-note-456"
|
||||
}
|
||||
}
|
||||
})
|
||||
end)
|
||||
|
||||
assert {:ok, "gaiia-note-456"} =
|
||||
Actions.create_subscriber_note(org.id, "gaiia-acct-1", "Outage affecting Tower 3")
|
||||
end
|
||||
|
||||
test "returns error when no integration exists" do
|
||||
org_id = Ecto.UUID.generate()
|
||||
|
||||
assert {:error, :no_integration} =
|
||||
Actions.create_subscriber_note(org_id, "gaiia-acct-1", "note")
|
||||
end
|
||||
end
|
||||
|
||||
describe "update_inventory_item/4" do
|
||||
test "pushes field updates to Gaiia inventory item", %{org: org} do
|
||||
Req.Test.stub(Client, fn conn ->
|
||||
{:ok, body, conn} = Plug.Conn.read_body(conn)
|
||||
decoded = Jason.decode!(body)
|
||||
assert decoded["query"] =~ "updateInventoryItem"
|
||||
|
||||
Req.Test.json(conn, %{
|
||||
"data" => %{
|
||||
"updateInventoryItem" => %{
|
||||
"id" => "gaiia-inv-1",
|
||||
"ipAddress" => "10.0.0.5"
|
||||
}
|
||||
}
|
||||
})
|
||||
end)
|
||||
|
||||
assert {:ok, _data} =
|
||||
Actions.update_inventory_item(org.id, "gaiia-inv-1", %{ip_address: "10.0.0.5"})
|
||||
end
|
||||
|
||||
test "returns error when no integration exists" do
|
||||
org_id = Ecto.UUID.generate()
|
||||
|
||||
assert {:error, :no_integration} =
|
||||
Actions.update_inventory_item(org_id, "gaiia-inv-1", %{ip_address: "10.0.0.5"})
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue