add address, latitude, longitude to site resource

Support geographic coordinates and street address in towerops_site.
All three fields are optional; server validates lat/long ranges.
This commit is contained in:
Graham McIntire 2026-03-13 17:35:16 -05:00
parent bbe1babdf7
commit 4960adaf37
No known key found for this signature in database
4 changed files with 117 additions and 72 deletions

View file

@ -31,8 +31,11 @@ provider "towerops" {
} }
resource "towerops_site" "example" { resource "towerops_site" "example" {
name = "Main Office" name = "Main Office"
location = "New York, NY" location = "New York, NY"
address = "350 5th Ave, New York, NY 10118"
latitude = 40.7484
longitude = -73.9857
} }
resource "towerops_device" "router" { resource "towerops_device" "router" {

View file

@ -10,6 +10,8 @@ Manages a TowerOps site. Sites represent physical locations that contain devices
## Example Usage ## Example Usage
### Basic
```terraform ```terraform
resource "towerops_site" "example" { resource "towerops_site" "example" {
name = "Main Office" name = "Main Office"
@ -18,6 +20,18 @@ resource "towerops_site" "example" {
} }
``` ```
### With Geographic Coordinates
```terraform
resource "towerops_site" "tower_site" {
name = "Verona Tower"
location = "Verona, TX"
address = "123 Main St, Verona, TX 75482"
latitude = 33.4356
longitude = -96.0028
}
```
## Schema ## Schema
### Required ### Required
@ -26,7 +40,10 @@ resource "towerops_site" "example" {
### Optional ### Optional
- `location` (String) - The physical location or address of the site. - `location` (String) - A short description of the physical location.
- `address` (String) - The street address of the site. Maximum 500 characters.
- `latitude` (Float) - The latitude of the site. Must be between -90 and 90.
- `longitude` (Float) - The longitude of the site. Must be between -180 and 180.
- `snmp_community` (String, Sensitive) - The default SNMP community string for devices at this site. - `snmp_community` (String, Sensitive) - The default SNMP community string for devices at this site.
### Read-Only ### Read-Only

View file

@ -38,11 +38,14 @@ func NewClient(token, baseURL string) *Client {
// Site represents a TowerOps site. // Site represents a TowerOps site.
type Site struct { type Site struct {
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`
Name string `json:"name"` Name string `json:"name"`
Location *string `json:"location,omitempty"` Location *string `json:"location,omitempty"`
SNMPCommunity *string `json:"snmp_community,omitempty"` Address *string `json:"address,omitempty"`
InsertedAt string `json:"inserted_at,omitempty"` Latitude *float64 `json:"latitude,omitempty"`
Longitude *float64 `json:"longitude,omitempty"`
SNMPCommunity *string `json:"snmp_community,omitempty"`
InsertedAt string `json:"inserted_at,omitempty"`
} }
// Device represents a TowerOps device. // Device represents a TowerOps device.

View file

@ -23,11 +23,14 @@ type SiteResource struct {
// SiteResourceModel describes the resource data model. // SiteResourceModel describes the resource data model.
type SiteResourceModel struct { type SiteResourceModel struct {
ID types.String `tfsdk:"id"` ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"` Name types.String `tfsdk:"name"`
Location types.String `tfsdk:"location"` Location types.String `tfsdk:"location"`
SNMPCommunity types.String `tfsdk:"snmp_community"` Address types.String `tfsdk:"address"`
InsertedAt types.String `tfsdk:"inserted_at"` Latitude types.Float64 `tfsdk:"latitude"`
Longitude types.Float64 `tfsdk:"longitude"`
SNMPCommunity types.String `tfsdk:"snmp_community"`
InsertedAt types.String `tfsdk:"inserted_at"`
} }
// NewSiteResource creates a new site resource. // NewSiteResource creates a new site resource.
@ -55,7 +58,19 @@ func (r *SiteResource) Schema(ctx context.Context, req resource.SchemaRequest, r
Required: true, Required: true,
}, },
"location": schema.StringAttribute{ "location": schema.StringAttribute{
Description: "The physical location or address of the site.", Description: "A short description of the physical location.",
Optional: true,
},
"address": schema.StringAttribute{
Description: "The street address of the site.",
Optional: true,
},
"latitude": schema.Float64Attribute{
Description: "The latitude of the site (-90 to 90).",
Optional: true,
},
"longitude": schema.Float64Attribute{
Description: "The longitude of the site (-180 to 180).",
Optional: true, Optional: true,
}, },
"snmp_community": schema.StringAttribute{ "snmp_community": schema.StringAttribute{
@ -99,19 +114,7 @@ func (r *SiteResource) Create(ctx context.Context, req resource.CreateRequest, r
return return
} }
site := Site{ site := buildSiteFromModel(&data)
Name: data.Name.ValueString(),
}
if !data.Location.IsNull() {
location := data.Location.ValueString()
site.Location = &location
}
if !data.SNMPCommunity.IsNull() {
community := data.SNMPCommunity.ValueString()
site.SNMPCommunity = &community
}
created, err := r.client.CreateSite(site) created, err := r.client.CreateSite(site)
if err != nil { if err != nil {
@ -121,13 +124,7 @@ func (r *SiteResource) Create(ctx context.Context, req resource.CreateRequest, r
data.ID = types.StringValue(created.ID) data.ID = types.StringValue(created.ID)
data.InsertedAt = types.StringValue(created.InsertedAt) data.InsertedAt = types.StringValue(created.InsertedAt)
setSiteOptionalFields(&data, created)
if created.Location != nil {
data.Location = types.StringValue(*created.Location)
}
if created.SNMPCommunity != nil {
data.SNMPCommunity = types.StringValue(*created.SNMPCommunity)
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
} }
@ -153,18 +150,7 @@ func (r *SiteResource) Read(ctx context.Context, req resource.ReadRequest, resp
data.Name = types.StringValue(site.Name) data.Name = types.StringValue(site.Name)
data.InsertedAt = types.StringValue(site.InsertedAt) data.InsertedAt = types.StringValue(site.InsertedAt)
setSiteOptionalFields(&data, site)
if site.Location != nil {
data.Location = types.StringValue(*site.Location)
} else {
data.Location = types.StringNull()
}
if site.SNMPCommunity != nil {
data.SNMPCommunity = types.StringValue(*site.SNMPCommunity)
} else {
data.SNMPCommunity = types.StringNull()
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
} }
@ -177,19 +163,7 @@ func (r *SiteResource) Update(ctx context.Context, req resource.UpdateRequest, r
return return
} }
site := Site{ site := buildSiteFromModel(&data)
Name: data.Name.ValueString(),
}
if !data.Location.IsNull() {
location := data.Location.ValueString()
site.Location = &location
}
if !data.SNMPCommunity.IsNull() {
community := data.SNMPCommunity.ValueString()
site.SNMPCommunity = &community
}
updated, err := r.client.UpdateSite(data.ID.ValueString(), site) updated, err := r.client.UpdateSite(data.ID.ValueString(), site)
if err != nil { if err != nil {
@ -203,12 +177,7 @@ func (r *SiteResource) Update(ctx context.Context, req resource.UpdateRequest, r
data.ID = types.StringValue(created.ID) data.ID = types.StringValue(created.ID)
data.InsertedAt = types.StringValue(created.InsertedAt) data.InsertedAt = types.StringValue(created.InsertedAt)
data.Name = types.StringValue(created.Name) data.Name = types.StringValue(created.Name)
if created.Location != nil { setSiteOptionalFields(&data, created)
data.Location = types.StringValue(*created.Location)
}
if created.SNMPCommunity != nil {
data.SNMPCommunity = types.StringValue(*created.SNMPCommunity)
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
return return
} }
@ -217,13 +186,7 @@ func (r *SiteResource) Update(ctx context.Context, req resource.UpdateRequest, r
} }
data.Name = types.StringValue(updated.Name) data.Name = types.StringValue(updated.Name)
setSiteOptionalFields(&data, updated)
if updated.Location != nil {
data.Location = types.StringValue(*updated.Location)
}
if updated.SNMPCommunity != nil {
data.SNMPCommunity = types.StringValue(*updated.SNMPCommunity)
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
} }
@ -246,3 +209,62 @@ func (r *SiteResource) Delete(ctx context.Context, req resource.DeleteRequest, r
func (r *SiteResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { func (r *SiteResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
} }
// buildSiteFromModel converts the Terraform model to an API Site struct.
func buildSiteFromModel(data *SiteResourceModel) Site {
site := Site{
Name: data.Name.ValueString(),
}
if !data.Location.IsNull() {
v := data.Location.ValueString()
site.Location = &v
}
if !data.Address.IsNull() {
v := data.Address.ValueString()
site.Address = &v
}
if !data.Latitude.IsNull() {
v := data.Latitude.ValueFloat64()
site.Latitude = &v
}
if !data.Longitude.IsNull() {
v := data.Longitude.ValueFloat64()
site.Longitude = &v
}
if !data.SNMPCommunity.IsNull() {
v := data.SNMPCommunity.ValueString()
site.SNMPCommunity = &v
}
return site
}
// setSiteOptionalFields maps API response optional fields back to the Terraform model.
func setSiteOptionalFields(data *SiteResourceModel, site *Site) {
if site.Location != nil {
data.Location = types.StringValue(*site.Location)
} else {
data.Location = types.StringNull()
}
if site.Address != nil {
data.Address = types.StringValue(*site.Address)
} else {
data.Address = types.StringNull()
}
if site.Latitude != nil {
data.Latitude = types.Float64Value(*site.Latitude)
} else {
data.Latitude = types.Float64Null()
}
if site.Longitude != nil {
data.Longitude = types.Float64Value(*site.Longitude)
} else {
data.Longitude = types.Float64Null()
}
if site.SNMPCommunity != nil {
data.SNMPCommunity = types.StringValue(*site.SNMPCommunity)
} else {
data.SNMPCommunity = types.StringNull()
}
}