diff --git a/lib/towerops/gaiia/actions.ex b/lib/towerops/gaiia/actions.ex new file mode 100644 index 00000000..869721be --- /dev/null +++ b/lib/towerops/gaiia/actions.ex @@ -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 diff --git a/lib/towerops/gaiia/client.ex b/lib/towerops/gaiia/client.ex index 40ae2eaa..1d469f13 100644 --- a/lib/towerops/gaiia/client.ex +++ b/lib/towerops/gaiia/client.ex @@ -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 diff --git a/test/towerops/gaiia/actions_test.exs b/test/towerops/gaiia/actions_test.exs new file mode 100644 index 00000000..89403193 --- /dev/null +++ b/test/towerops/gaiia/actions_test.exs @@ -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