towerops/docs/terraform-coverages.md
Graham McIntire edb82bcc40 feat(coverage): public REST API + KMZ export + email-on-complete
cnHeat-2.0-style automation hooks. Coverage prediction is now a
first-class API resource that Terraform / Ansible / cron can drive.

REST API (under /api/v1, Bearer-token auth scoped to one org)
- GET    /coverages              list
- POST   /coverages               create + auto-queue compute
- GET    /coverages/:id           read (includes status / progress_pct
                                  for polling-based providers)
- PATCH  /coverages/:id           update editable fields
- DELETE /coverages/:id           delete + cleanup
- POST   /coverages/:id/recompute requeue, returns 202
- GET    /coverages/:id/kmz       Google Earth bundle (KML + PNG)

Each response includes the full resource — Terraform read-after-write
reconciles cleanly; status polling hits a stable JSON shape.

Email-on-complete (Towerops.Coverages.Notifier)
- Worker fires deliver_compute_complete on both :ready and :failed
  paths. Sends a short text email to every member of the owning
  organisation with a link to the show page (or the failure
  reason). Mirrors cnHeat 2.0's per-project email alerts.
- Failures are non-fatal: a stuck SMTP relay never blocks the
  underlying coverage record from transitioning state.

KMZ export
- Builds a flat KMZ in-memory via :zip.create — doc.kml +
  overlay.png, with the LatLonBox set from the coverage's bbox.
  XML special chars are escaped. 409 if not yet ready.

Tests (11 new, all green)
- Bearer-token auth happy path + 401 missing
- index / show / create / update / delete + 404 cross-org
- create returns status:'queued' confirming the auto-enqueue
- recompute returns 202 with status:'queued'

Plus docs/terraform-coverages.md showing how to drive the new API
from a third-party restapi provider until the first-party
terraform-provider-towerops gets a coverage resource.
2026-05-06 17:15:57 -05:00

3.2 KiB

Coverages over the Towerops API (Terraform-friendly)

Towerops exposes a resourceful REST API for RF coverage predictions that's stable enough to drive from Terraform, Ansible, or any infrastructure-as-code tool. All endpoints live under /api/v1/ and use Bearer-token auth scoped to a single organization.

Authentication

Mint a token from Settings → API tokens. Send it as:

Authorization: Bearer towerops_<token>

The token is org-scoped — every endpoint reads/writes inside that org only.

Resources

Method Path Purpose
GET /api/v1/coverages List all coverages in the org.
POST /api/v1/coverages Create a coverage. The compute pipeline is queued automatically.
GET /api/v1/coverages/:id Read a single coverage including current status / progress_pct.
PATCH /api/v1/coverages/:id Update editable fields (does not auto-recompute).
DELETE /api/v1/coverages/:id Delete a coverage and its on-disk rasters.
POST /api/v1/coverages/:id/recompute Re-run the compute pipeline. Returns 202 with status: "queued".
GET /api/v1/coverages/:id/kmz Download a KMZ overlay for Google Earth (returns 409 until status: "ready").

Create payload

{
  "coverage": {
    "name": "North sector 5 GHz",
    "site_id": "1f4...uuid",
    "antenna_slug": "rf-elements-twistport-symmetrical-horn-30",
    "frequency_ghz": 5.8,
    "tx_power_dbm": 22.0,
    "height_agl_ft": 100,
    "azimuth_deg": 0,
    "downtilt_deg": 0,
    "radius_mi": 4
  }
}

Imperial inputs are accepted via the virtual fields shown above; the schema converts them to SI before validation.

Polling for :ready

Compute is asynchronous. After POST you'll receive status: "queued"; the worker transitions through "computing" to "ready" (or "failed"). Terraform providers can poll with GET /api/v1/coverages/:id until status == "ready" before declaring the resource healthy.

Terraform example

A Terraform provider for Towerops isn't published yet, but a hand-rolled restapi provider works today:

terraform {
  required_providers {
    restapi = { source = "Mastercard/restapi", version = "~> 1.18" }
  }
}

provider "restapi" {
  uri                  = "https://app.towerops.com"
  write_returns_object = true
  headers = {
    Authorization = "Bearer ${var.towerops_token}"
    Content-Type  = "application/json"
  }
}

resource "restapi_object" "north_sector" {
  path         = "/api/v1/coverages"
  id_attribute = "id"

  data = jsonencode({
    coverage = {
      name           = "North sector 5 GHz"
      site_id        = var.site_id
      antenna_slug   = "rf-elements-twistport-symmetrical-horn-30"
      frequency_ghz  = 5.8
      tx_power_dbm   = 22.0
      height_agl_ft  = 100
      azimuth_deg    = 0
      downtilt_deg   = 0
      radius_mi      = 4
    }
  })
}

terraform apply creates the coverage; terraform destroy deletes it. Updates flow through PATCH. Use terraform_data + a provisioner if you need to wait on status = "ready" before proceeding.