init
This commit is contained in:
commit
8fed566e22
11 changed files with 1211 additions and 0 deletions
1
.tool-versions
Normal file
1
.tool-versions
Normal file
|
|
@ -0,0 +1 @@
|
|||
golang 1.25.6
|
||||
180
docs/plans/2026-01-26-terraform-provider-design.md
Normal file
180
docs/plans/2026-01-26-terraform-provider-design.md
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
# TowerOps Terraform Provider Design
|
||||
|
||||
## Overview
|
||||
|
||||
A Terraform provider for managing TowerOps resources (Sites and Devices) via the existing REST API.
|
||||
|
||||
## Scope
|
||||
|
||||
- **Resources**: Sites, Devices
|
||||
- **Authentication**: API token in provider config
|
||||
- **Import**: Supported for existing resources
|
||||
- **Endpoint**: Fixed production URL
|
||||
|
||||
## Provider Configuration
|
||||
|
||||
```hcl
|
||||
terraform {
|
||||
required_providers {
|
||||
towerops = {
|
||||
source = "towerops/towerops"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "towerops" {
|
||||
token = var.towerops_api_token # Required, scopes all operations to one org
|
||||
}
|
||||
```
|
||||
|
||||
The token determines which organization's resources are accessible via the existing API token authentication.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
towerops-tf-provider/
|
||||
├── main.go # Entry point
|
||||
├── go.mod / go.sum
|
||||
├── internal/
|
||||
│ └── provider/
|
||||
│ ├── provider.go # Provider schema + config
|
||||
│ ├── client.go # HTTP client for TowerOps API
|
||||
│ ├── site_resource.go
|
||||
│ ├── device_resource.go
|
||||
├── examples/
|
||||
│ └── main.tf # Usage example
|
||||
└── docs/ # Generated documentation
|
||||
```
|
||||
|
||||
Uses the Terraform Plugin Framework (modern approach).
|
||||
|
||||
## Resource Schemas
|
||||
|
||||
### towerops_site
|
||||
|
||||
```hcl
|
||||
resource "towerops_site" "main" {
|
||||
name = "Main Office"
|
||||
location = "New York, NY" # Optional
|
||||
snmp_community = "public" # Optional
|
||||
}
|
||||
|
||||
output "site_id" {
|
||||
value = towerops_site.main.id
|
||||
}
|
||||
```
|
||||
|
||||
**Attributes:**
|
||||
- `name` (Required, string) - Site name, 2-200 chars
|
||||
- `location` (Optional, string) - Physical location description
|
||||
- `snmp_community` (Optional, string) - Default SNMP community for devices
|
||||
|
||||
**Computed:**
|
||||
- `id` (string) - UUID assigned by TowerOps
|
||||
- `inserted_at` (string) - Creation timestamp
|
||||
|
||||
### towerops_device
|
||||
|
||||
```hcl
|
||||
resource "towerops_device" "router" {
|
||||
site_id = towerops_site.main.id
|
||||
name = "Core Router"
|
||||
ip_address = "192.168.1.1"
|
||||
|
||||
monitoring_enabled = true # Optional, default true
|
||||
snmp_enabled = true # Optional, default true
|
||||
snmp_version = "2c" # Optional, default "2c"
|
||||
snmp_port = 161 # Optional, default 161
|
||||
}
|
||||
```
|
||||
|
||||
**Attributes:**
|
||||
- `site_id` (Required, string) - UUID of parent site
|
||||
- `name` (Required, string) - Device name
|
||||
- `ip_address` (Required, string) - Device IP address
|
||||
- `description` (Optional, string) - Device description
|
||||
- `monitoring_enabled` (Optional, bool) - Enable monitoring, default true
|
||||
- `snmp_enabled` (Optional, bool) - Enable SNMP polling, default true
|
||||
- `snmp_version` (Optional, string) - SNMP version (1, 2c, 3), default "2c"
|
||||
- `snmp_port` (Optional, int) - SNMP port, default 161
|
||||
|
||||
**Computed:**
|
||||
- `id` (string) - UUID assigned by TowerOps
|
||||
- `inserted_at` (string) - Creation timestamp
|
||||
|
||||
## HTTP Client
|
||||
|
||||
```go
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
Token string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// Site operations
|
||||
func (c *Client) CreateSite(site Site) (*Site, error)
|
||||
func (c *Client) GetSite(id string) (*Site, error)
|
||||
func (c *Client) UpdateSite(id string, site Site) (*Site, error)
|
||||
func (c *Client) DeleteSite(id string) error
|
||||
|
||||
// Device operations
|
||||
func (c *Client) CreateDevice(device Device) (*Device, error)
|
||||
func (c *Client) GetDevice(id string) (*Device, error)
|
||||
func (c *Client) UpdateDevice(id string, device Device) (*Device, error)
|
||||
func (c *Client) DeleteDevice(id string) error
|
||||
```
|
||||
|
||||
### API Mapping
|
||||
|
||||
| Terraform Operation | HTTP Method | Endpoint |
|
||||
|---------------------|-------------|----------|
|
||||
| Create | POST | `/api/v1/sites` or `/api/v1/devices` |
|
||||
| Read | GET | `/api/v1/sites/:id` or `/api/v1/devices/:id` |
|
||||
| Update | PATCH | `/api/v1/sites/:id` or `/api/v1/devices/:id` |
|
||||
| Delete | DELETE | `/api/v1/sites/:id` or `/api/v1/devices/:id` |
|
||||
| Import | GET | Same as Read |
|
||||
|
||||
## Import Support
|
||||
|
||||
```bash
|
||||
# Import existing site by UUID
|
||||
terraform import towerops_site.main 550e8400-e29b-41d4-a716-446655440000
|
||||
|
||||
# Import existing device by UUID
|
||||
terraform import towerops_device.router 7c9e6679-7425-40de-944b-e07fc1f90ae7
|
||||
```
|
||||
|
||||
Import uses the same `GetSite`/`GetDevice` client methods as Read.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. **Unit tests** - Mock HTTP responses, test schema validation
|
||||
2. **Acceptance tests** - Run against real TowerOps instance (skipped in CI without credentials)
|
||||
|
||||
```go
|
||||
func TestAccSiteResource(t *testing.T) {
|
||||
resource.Test(t, resource.TestCase{
|
||||
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: `resource "towerops_site" "test" { name = "Test Site" }`,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("towerops_site.test", "name", "Test Site"),
|
||||
resource.TestCheckResourceAttrSet("towerops_site.test", "id"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. Project scaffolding (go.mod, main.go)
|
||||
2. HTTP client with Site CRUD
|
||||
3. Site resource implementation
|
||||
4. HTTP client Device CRUD
|
||||
5. Device resource implementation
|
||||
6. Import support for both resources
|
||||
7. Acceptance tests
|
||||
8. Documentation and examples
|
||||
54
examples/main.tf
Normal file
54
examples/main.tf
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
terraform {
|
||||
required_providers {
|
||||
towerops = {
|
||||
source = "towerops/towerops"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "towerops_api_token" {
|
||||
description = "TowerOps API token"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
provider "towerops" {
|
||||
token = var.towerops_api_token
|
||||
}
|
||||
|
||||
# Create a site
|
||||
resource "towerops_site" "main_office" {
|
||||
name = "Main Office"
|
||||
location = "New York, NY"
|
||||
snmp_community = "public"
|
||||
}
|
||||
|
||||
# Create devices at the site
|
||||
resource "towerops_device" "core_router" {
|
||||
site_id = towerops_site.main_office.id
|
||||
name = "Core Router"
|
||||
ip_address = "192.168.1.1"
|
||||
|
||||
monitoring_enabled = true
|
||||
snmp_enabled = true
|
||||
snmp_version = "2c"
|
||||
snmp_port = 161
|
||||
}
|
||||
|
||||
resource "towerops_device" "access_switch" {
|
||||
site_id = towerops_site.main_office.id
|
||||
name = "Access Switch"
|
||||
ip_address = "192.168.1.2"
|
||||
|
||||
monitoring_enabled = true
|
||||
snmp_enabled = true
|
||||
}
|
||||
|
||||
# Output the site ID
|
||||
output "site_id" {
|
||||
value = towerops_site.main_office.id
|
||||
}
|
||||
|
||||
output "router_id" {
|
||||
value = towerops_device.core_router.id
|
||||
}
|
||||
30
go.mod
Normal file
30
go.mod
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
module github.com/towerops/terraform-provider-towerops
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/hashicorp/terraform-plugin-framework v1.4.2
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.2 // indirect
|
||||
github.com/hashicorp/go-plugin v1.6.0 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/terraform-plugin-go v0.19.1 // indirect
|
||||
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
|
||||
github.com/hashicorp/terraform-registry-address v0.2.3 // indirect
|
||||
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect
|
||||
google.golang.org/grpc v1.60.1 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
)
|
||||
81
go.sum
Normal file
81
go.sum
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
|
||||
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/hashicorp/go-hclog v1.6.2 h1:NOtoftovWkDheyUM/8JW3QMiXyxJK3uHRK7wV04nD2I=
|
||||
github.com/hashicorp/go-hclog v1.6.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A=
|
||||
github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/terraform-plugin-framework v1.4.2 h1:P7a7VP1GZbjc4rv921Xy5OckzhoiO3ig6SGxwelD2sI=
|
||||
github.com/hashicorp/terraform-plugin-framework v1.4.2/go.mod h1:GWl3InPFZi2wVQmdVnINPKys09s9mLmTZr95/ngLnbY=
|
||||
github.com/hashicorp/terraform-plugin-go v0.19.1 h1:lf/jTGTeELcz5IIbn/94mJdmnTjRYm6S6ct/JqCSr50=
|
||||
github.com/hashicorp/terraform-plugin-go v0.19.1/go.mod h1:5NMIS+DXkfacX6o5HCpswda5yjkSYfKzn1Nfl9l+qRs=
|
||||
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
|
||||
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
|
||||
github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI=
|
||||
github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM=
|
||||
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
|
||||
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
|
||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
|
||||
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
|
||||
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
|
||||
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
|
||||
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
212
internal/provider/client.go
Normal file
212
internal/provider/client.go
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultBaseURL = "https://app.towerops.io"
|
||||
|
||||
// Client is the TowerOps API client.
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
Token string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new TowerOps API client.
|
||||
func NewClient(token string) *Client {
|
||||
return &Client{
|
||||
BaseURL: defaultBaseURL,
|
||||
Token: token,
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Site represents a TowerOps site.
|
||||
type Site struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Location *string `json:"location,omitempty"`
|
||||
SNMPCommunity *string `json:"snmp_community,omitempty"`
|
||||
InsertedAt string `json:"inserted_at,omitempty"`
|
||||
}
|
||||
|
||||
// Device represents a TowerOps device.
|
||||
type Device struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
SiteID string `json:"site_id"`
|
||||
Name string `json:"name"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
MonitoringEnabled *bool `json:"monitoring_enabled,omitempty"`
|
||||
SNMPEnabled *bool `json:"snmp_enabled,omitempty"`
|
||||
SNMPVersion *string `json:"snmp_version,omitempty"`
|
||||
SNMPPort *int `json:"snmp_port,omitempty"`
|
||||
CheckIntervalSeconds *int `json:"check_interval_seconds,omitempty"`
|
||||
InsertedAt string `json:"inserted_at,omitempty"`
|
||||
}
|
||||
|
||||
// APIError represents an error response from the API.
|
||||
type APIError struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
Errors map[string]string `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) doRequest(method, path string, body interface{}) ([]byte, error) {
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
reqBody = bytes.NewBuffer(jsonBody)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, c.BaseURL+path, reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.Token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
var apiErr APIError
|
||||
if err := json.Unmarshal(respBody, &apiErr); err == nil {
|
||||
if apiErr.Error != "" {
|
||||
return nil, fmt.Errorf("API error (%d): %s", resp.StatusCode, apiErr.Error)
|
||||
}
|
||||
if len(apiErr.Errors) > 0 {
|
||||
return nil, fmt.Errorf("API validation error (%d): %v", resp.StatusCode, apiErr.Errors)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("API error (%d): %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
return respBody, nil
|
||||
}
|
||||
|
||||
// CreateSite creates a new site.
|
||||
func (c *Client) CreateSite(site Site) (*Site, error) {
|
||||
body := map[string]Site{"site": site}
|
||||
respBody, err := c.doRequest(http.MethodPost, "/api/v1/sites", body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result Site
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetSite retrieves a site by ID.
|
||||
func (c *Client) GetSite(id string) (*Site, error) {
|
||||
respBody, err := c.doRequest(http.MethodGet, "/api/v1/sites/"+id, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result Site
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// UpdateSite updates an existing site.
|
||||
func (c *Client) UpdateSite(id string, site Site) (*Site, error) {
|
||||
body := map[string]Site{"site": site}
|
||||
respBody, err := c.doRequest(http.MethodPatch, "/api/v1/sites/"+id, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result Site
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// DeleteSite deletes a site.
|
||||
func (c *Client) DeleteSite(id string) error {
|
||||
_, err := c.doRequest(http.MethodDelete, "/api/v1/sites/"+id, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateDevice creates a new device.
|
||||
func (c *Client) CreateDevice(device Device) (*Device, error) {
|
||||
body := map[string]Device{"device": device}
|
||||
respBody, err := c.doRequest(http.MethodPost, "/api/v1/devices", body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result Device
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetDevice retrieves a device by ID.
|
||||
func (c *Client) GetDevice(id string) (*Device, error) {
|
||||
respBody, err := c.doRequest(http.MethodGet, "/api/v1/devices/"+id, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result Device
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// UpdateDevice updates an existing device.
|
||||
func (c *Client) UpdateDevice(id string, device Device) (*Device, error) {
|
||||
body := map[string]Device{"device": device}
|
||||
respBody, err := c.doRequest(http.MethodPatch, "/api/v1/devices/"+id, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result Device
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// DeleteDevice deletes a device.
|
||||
func (c *Client) DeleteDevice(id string) error {
|
||||
_, err := c.doRequest(http.MethodDelete, "/api/v1/devices/"+id, nil)
|
||||
return err
|
||||
}
|
||||
317
internal/provider/device_resource.go
Normal file
317
internal/provider/device_resource.go
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var _ resource.Resource = &DeviceResource{}
|
||||
var _ resource.ResourceWithImportState = &DeviceResource{}
|
||||
|
||||
// DeviceResource defines the resource implementation.
|
||||
type DeviceResource struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// DeviceResourceModel describes the resource data model.
|
||||
type DeviceResourceModel struct {
|
||||
ID types.String `tfsdk:"id"`
|
||||
SiteID types.String `tfsdk:"site_id"`
|
||||
Name types.String `tfsdk:"name"`
|
||||
IPAddress types.String `tfsdk:"ip_address"`
|
||||
Description types.String `tfsdk:"description"`
|
||||
MonitoringEnabled types.Bool `tfsdk:"monitoring_enabled"`
|
||||
SNMPEnabled types.Bool `tfsdk:"snmp_enabled"`
|
||||
SNMPVersion types.String `tfsdk:"snmp_version"`
|
||||
SNMPPort types.Int64 `tfsdk:"snmp_port"`
|
||||
InsertedAt types.String `tfsdk:"inserted_at"`
|
||||
}
|
||||
|
||||
// NewDeviceResource creates a new device resource.
|
||||
func NewDeviceResource() resource.Resource {
|
||||
return &DeviceResource{}
|
||||
}
|
||||
|
||||
func (r *DeviceResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_device"
|
||||
}
|
||||
|
||||
func (r *DeviceResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages a TowerOps device. Devices represent network equipment at a site.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "The unique identifier of the device.",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"site_id": schema.StringAttribute{
|
||||
Description: "The ID of the site this device belongs to.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The name of the device.",
|
||||
Required: true,
|
||||
},
|
||||
"ip_address": schema.StringAttribute{
|
||||
Description: "The IP address of the device.",
|
||||
Required: true,
|
||||
},
|
||||
"description": schema.StringAttribute{
|
||||
Description: "A description of the device.",
|
||||
Optional: true,
|
||||
},
|
||||
"monitoring_enabled": schema.BoolAttribute{
|
||||
Description: "Whether monitoring is enabled for this device.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: booldefault.StaticBool(true),
|
||||
},
|
||||
"snmp_enabled": schema.BoolAttribute{
|
||||
Description: "Whether SNMP polling is enabled for this device.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: booldefault.StaticBool(true),
|
||||
},
|
||||
"snmp_version": schema.StringAttribute{
|
||||
Description: "The SNMP version to use (1, 2c, or 3).",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: stringdefault.StaticString("2c"),
|
||||
},
|
||||
"snmp_port": schema.Int64Attribute{
|
||||
Description: "The SNMP port to use.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Default: int64default.StaticInt64(161),
|
||||
},
|
||||
"inserted_at": schema.StringAttribute{
|
||||
Description: "The timestamp when the device was created.",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *DeviceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
client, ok := req.ProviderData.(*Client)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *Client, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
r.client = client
|
||||
}
|
||||
|
||||
func (r *DeviceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var data DeviceResourceModel
|
||||
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
device := Device{
|
||||
SiteID: data.SiteID.ValueString(),
|
||||
Name: data.Name.ValueString(),
|
||||
IPAddress: data.IPAddress.ValueString(),
|
||||
}
|
||||
|
||||
if !data.Description.IsNull() {
|
||||
desc := data.Description.ValueString()
|
||||
device.Description = &desc
|
||||
}
|
||||
|
||||
if !data.MonitoringEnabled.IsNull() {
|
||||
enabled := data.MonitoringEnabled.ValueBool()
|
||||
device.MonitoringEnabled = &enabled
|
||||
}
|
||||
|
||||
if !data.SNMPEnabled.IsNull() {
|
||||
enabled := data.SNMPEnabled.ValueBool()
|
||||
device.SNMPEnabled = &enabled
|
||||
}
|
||||
|
||||
if !data.SNMPVersion.IsNull() {
|
||||
version := data.SNMPVersion.ValueString()
|
||||
device.SNMPVersion = &version
|
||||
}
|
||||
|
||||
if !data.SNMPPort.IsNull() {
|
||||
port := int(data.SNMPPort.ValueInt64())
|
||||
device.SNMPPort = &port
|
||||
}
|
||||
|
||||
created, err := r.client.CreateDevice(device)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Failed to create device", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data.ID = types.StringValue(created.ID)
|
||||
data.InsertedAt = types.StringValue(created.InsertedAt)
|
||||
|
||||
if created.MonitoringEnabled != nil {
|
||||
data.MonitoringEnabled = types.BoolValue(*created.MonitoringEnabled)
|
||||
}
|
||||
if created.SNMPEnabled != nil {
|
||||
data.SNMPEnabled = types.BoolValue(*created.SNMPEnabled)
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||
}
|
||||
|
||||
func (r *DeviceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var data DeviceResourceModel
|
||||
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
device, err := r.client.GetDevice(data.ID.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Failed to read device", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data.SiteID = types.StringValue(device.SiteID)
|
||||
data.Name = types.StringValue(device.Name)
|
||||
data.IPAddress = types.StringValue(device.IPAddress)
|
||||
data.InsertedAt = types.StringValue(device.InsertedAt)
|
||||
|
||||
if device.Description != nil {
|
||||
data.Description = types.StringValue(*device.Description)
|
||||
} else {
|
||||
data.Description = types.StringNull()
|
||||
}
|
||||
|
||||
if device.MonitoringEnabled != nil {
|
||||
data.MonitoringEnabled = types.BoolValue(*device.MonitoringEnabled)
|
||||
}
|
||||
|
||||
if device.SNMPEnabled != nil {
|
||||
data.SNMPEnabled = types.BoolValue(*device.SNMPEnabled)
|
||||
}
|
||||
|
||||
if device.SNMPVersion != nil {
|
||||
data.SNMPVersion = types.StringValue(*device.SNMPVersion)
|
||||
}
|
||||
|
||||
if device.SNMPPort != nil {
|
||||
data.SNMPPort = types.Int64Value(int64(*device.SNMPPort))
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||
}
|
||||
|
||||
func (r *DeviceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var data DeviceResourceModel
|
||||
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
device := Device{
|
||||
SiteID: data.SiteID.ValueString(),
|
||||
Name: data.Name.ValueString(),
|
||||
IPAddress: data.IPAddress.ValueString(),
|
||||
}
|
||||
|
||||
if !data.Description.IsNull() {
|
||||
desc := data.Description.ValueString()
|
||||
device.Description = &desc
|
||||
}
|
||||
|
||||
if !data.MonitoringEnabled.IsNull() {
|
||||
enabled := data.MonitoringEnabled.ValueBool()
|
||||
device.MonitoringEnabled = &enabled
|
||||
}
|
||||
|
||||
if !data.SNMPEnabled.IsNull() {
|
||||
enabled := data.SNMPEnabled.ValueBool()
|
||||
device.SNMPEnabled = &enabled
|
||||
}
|
||||
|
||||
if !data.SNMPVersion.IsNull() {
|
||||
version := data.SNMPVersion.ValueString()
|
||||
device.SNMPVersion = &version
|
||||
}
|
||||
|
||||
if !data.SNMPPort.IsNull() {
|
||||
port := int(data.SNMPPort.ValueInt64())
|
||||
device.SNMPPort = &port
|
||||
}
|
||||
|
||||
updated, err := r.client.UpdateDevice(data.ID.ValueString(), device)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Failed to update device", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data.Name = types.StringValue(updated.Name)
|
||||
data.IPAddress = types.StringValue(updated.IPAddress)
|
||||
|
||||
if updated.Description != nil {
|
||||
data.Description = types.StringValue(*updated.Description)
|
||||
}
|
||||
if updated.MonitoringEnabled != nil {
|
||||
data.MonitoringEnabled = types.BoolValue(*updated.MonitoringEnabled)
|
||||
}
|
||||
if updated.SNMPEnabled != nil {
|
||||
data.SNMPEnabled = types.BoolValue(*updated.SNMPEnabled)
|
||||
}
|
||||
if updated.SNMPVersion != nil {
|
||||
data.SNMPVersion = types.StringValue(*updated.SNMPVersion)
|
||||
}
|
||||
if updated.SNMPPort != nil {
|
||||
data.SNMPPort = types.Int64Value(int64(*updated.SNMPPort))
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||
}
|
||||
|
||||
func (r *DeviceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var data DeviceResourceModel
|
||||
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
err := r.client.DeleteDevice(data.ID.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Failed to delete device", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *DeviceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
|
||||
}
|
||||
91
internal/provider/provider.go
Normal file
91
internal/provider/provider.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/provider"
|
||||
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var _ provider.Provider = &ToweropsProvider{}
|
||||
|
||||
// ToweropsProvider defines the provider implementation.
|
||||
type ToweropsProvider struct {
|
||||
version string
|
||||
}
|
||||
|
||||
// ToweropsProviderModel describes the provider data model.
|
||||
type ToweropsProviderModel struct {
|
||||
Token types.String `tfsdk:"token"`
|
||||
}
|
||||
|
||||
// New creates a new provider instance.
|
||||
func New(version string) func() provider.Provider {
|
||||
return func() provider.Provider {
|
||||
return &ToweropsProvider{
|
||||
version: version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ToweropsProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
|
||||
resp.TypeName = "towerops"
|
||||
resp.Version = p.version
|
||||
}
|
||||
|
||||
func (p *ToweropsProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "The TowerOps provider allows you to manage TowerOps resources such as sites and devices.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"token": schema.StringAttribute{
|
||||
Description: "The API token for authenticating with TowerOps. This token determines which organization's resources are accessible.",
|
||||
Required: true,
|
||||
Sensitive: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ToweropsProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
|
||||
var config ToweropsProviderModel
|
||||
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
if config.Token.IsUnknown() {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unknown TowerOps API Token",
|
||||
"The provider cannot create the TowerOps API client as there is an unknown configuration value for the API token.",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if config.Token.IsNull() || config.Token.ValueString() == "" {
|
||||
resp.Diagnostics.AddError(
|
||||
"Missing TowerOps API Token",
|
||||
"The provider requires a token to authenticate with the TowerOps API.",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
client := NewClient(config.Token.ValueString())
|
||||
|
||||
resp.DataSourceData = client
|
||||
resp.ResourceData = client
|
||||
}
|
||||
|
||||
func (p *ToweropsProvider) Resources(ctx context.Context) []func() resource.Resource {
|
||||
return []func() resource.Resource{
|
||||
NewSiteResource,
|
||||
NewDeviceResource,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ToweropsProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
|
||||
return []func() datasource.DataSource{}
|
||||
}
|
||||
223
internal/provider/site_resource.go
Normal file
223
internal/provider/site_resource.go
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var _ resource.Resource = &SiteResource{}
|
||||
var _ resource.ResourceWithImportState = &SiteResource{}
|
||||
|
||||
// SiteResource defines the resource implementation.
|
||||
type SiteResource struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// SiteResourceModel describes the resource data model.
|
||||
type SiteResourceModel struct {
|
||||
ID types.String `tfsdk:"id"`
|
||||
Name types.String `tfsdk:"name"`
|
||||
Location types.String `tfsdk:"location"`
|
||||
SNMPCommunity types.String `tfsdk:"snmp_community"`
|
||||
InsertedAt types.String `tfsdk:"inserted_at"`
|
||||
}
|
||||
|
||||
// NewSiteResource creates a new site resource.
|
||||
func NewSiteResource() resource.Resource {
|
||||
return &SiteResource{}
|
||||
}
|
||||
|
||||
func (r *SiteResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_site"
|
||||
}
|
||||
|
||||
func (r *SiteResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Manages a TowerOps site. Sites represent physical locations that contain devices.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "The unique identifier of the site.",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The name of the site. Must be between 2 and 200 characters.",
|
||||
Required: true,
|
||||
},
|
||||
"location": schema.StringAttribute{
|
||||
Description: "The physical location or address of the site.",
|
||||
Optional: true,
|
||||
},
|
||||
"snmp_community": schema.StringAttribute{
|
||||
Description: "The default SNMP community string for devices at this site.",
|
||||
Optional: true,
|
||||
Sensitive: true,
|
||||
},
|
||||
"inserted_at": schema.StringAttribute{
|
||||
Description: "The timestamp when the site was created.",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *SiteResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
client, ok := req.ProviderData.(*Client)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *Client, got: %T", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
r.client = client
|
||||
}
|
||||
|
||||
func (r *SiteResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var data SiteResourceModel
|
||||
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
site := Site{
|
||||
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)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Failed to create site", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data.ID = types.StringValue(created.ID)
|
||||
data.InsertedAt = types.StringValue(created.InsertedAt)
|
||||
|
||||
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)...)
|
||||
}
|
||||
|
||||
func (r *SiteResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var data SiteResourceModel
|
||||
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
site, err := r.client.GetSite(data.ID.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Failed to read site", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data.Name = types.StringValue(site.Name)
|
||||
data.InsertedAt = types.StringValue(site.InsertedAt)
|
||||
|
||||
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)...)
|
||||
}
|
||||
|
||||
func (r *SiteResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var data SiteResourceModel
|
||||
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
site := Site{
|
||||
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)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Failed to update site", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data.Name = types.StringValue(updated.Name)
|
||||
|
||||
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)...)
|
||||
}
|
||||
|
||||
func (r *SiteResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var data SiteResourceModel
|
||||
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
err := r.client.DeleteSite(data.ID.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Failed to delete site", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *SiteResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
|
||||
}
|
||||
22
main.go
Normal file
22
main.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/providerserver"
|
||||
"github.com/towerops/terraform-provider-towerops/internal/provider"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
opts := providerserver.ServeOpts{
|
||||
Address: "registry.terraform.io/towerops/towerops",
|
||||
}
|
||||
|
||||
err := providerserver.Serve(context.Background(), provider.New(version), opts)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
BIN
terraform-provider-towerops
Executable file
BIN
terraform-provider-towerops
Executable file
Binary file not shown.
Loading…
Add table
Reference in a new issue