Add rudimentary support for resource unifi_device (#112)
This commit is contained in:
80
docs/resources/device.md
Normal file
80
docs/resources/device.md
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||
page_title: "unifi_device Resource - terraform-provider-unifi"
|
||||
subcategory: ""
|
||||
description: |-
|
||||
unifi_device manages a device of the network.
|
||||
Devices are adopted by the controller, so it is not possible for this resource to be created through Terraform.
|
||||
---
|
||||
|
||||
# unifi_device (Resource)
|
||||
|
||||
`unifi_device` manages a device of the network.
|
||||
|
||||
Devices are adopted by the controller, so it is not possible for this resource to be created through Terraform.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```terraform
|
||||
data "unifi_port_profile" "disabled" {
|
||||
# look up the built-in disabled port profile
|
||||
name = "Disabled"
|
||||
}
|
||||
|
||||
resource "unifi_port_profile" "poe" {
|
||||
name = "poe"
|
||||
forward = "customize"
|
||||
|
||||
native_networkconf_id = var.native_network_id
|
||||
tagged_networkconf_ids = [
|
||||
var.some_vlan_network_id,
|
||||
]
|
||||
|
||||
poe_mode = "auto"
|
||||
}
|
||||
|
||||
resource "unifi_device" "us_24_poe" {
|
||||
name = "Switch with POE"
|
||||
|
||||
port_override {
|
||||
number = 1
|
||||
name = "port w/ poe"
|
||||
port_profile_id = unifi_port_profile.poe.id
|
||||
}
|
||||
|
||||
port_override {
|
||||
number = 2
|
||||
name = "disabled"
|
||||
port_profile_id = data.unifi_port_profile.disabled.id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Optional
|
||||
|
||||
- **name** (String) The name of the device.
|
||||
- **port_override** (Block Set) Settings overrides for specific switch ports. (see [below for nested schema](#nestedblock--port_override))
|
||||
- **site** (String) The name of the site to associate the device with.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- **disabled** (Boolean) Specifies whether this device should be disabled.
|
||||
- **id** (String) The ID of the device.
|
||||
- **mac** (String) The MAC address of the device.
|
||||
|
||||
<a id="nestedblock--port_override"></a>
|
||||
### Nested Schema for `port_override`
|
||||
|
||||
Required:
|
||||
|
||||
- **number** (Number) Switch port number.
|
||||
|
||||
Optional:
|
||||
|
||||
- **name** (String) Human-readable name of the port.
|
||||
- **port_profile_id** (String) ID of the Port Profile used on this port.
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ resource "unifi_port_profile" "poe_disabled" {
|
||||
- **dot1x_idle_timeout** (Number) The timeout, in seconds, to use when using the MAC Based 802.1X control. Can be between 0 and 65535 Defaults to `300`.
|
||||
- **egress_rate_limit_kbps** (Number) The egress rate limit, in kpbs, for the port profile. Can be between `64` and `9999999`.
|
||||
- **egress_rate_limit_kbps_enabled** (Boolean) Enable egress rate limiting for the port profile. Defaults to `false`.
|
||||
- **forward** (String) The type forwarding to use for the port profile. Can be `all`, `native`, `customize` or `disabled`. Defaults to `native`.
|
||||
- **forward** (String) The type forwarding to use for the port profile. Can be `all`, `native`, `customize` or `disabled`. Defaults to `native`.
|
||||
- **full_duplex** (Boolean) Enable full duplex for the port profile. Defaults to `false`.
|
||||
- **isolation** (Boolean) Enable port isolation for the port profile. Defaults to `false`.
|
||||
- **lldpmed_enabled** (Boolean) Enable LLDP-MED for the port profile. Defaults to `true`.
|
||||
|
||||
32
examples/resources/unifi_device/resource.tf
Normal file
32
examples/resources/unifi_device/resource.tf
Normal file
@@ -0,0 +1,32 @@
|
||||
data "unifi_port_profile" "disabled" {
|
||||
# look up the built-in disabled port profile
|
||||
name = "Disabled"
|
||||
}
|
||||
|
||||
resource "unifi_port_profile" "poe" {
|
||||
name = "poe"
|
||||
forward = "customize"
|
||||
|
||||
native_networkconf_id = var.native_network_id
|
||||
tagged_networkconf_ids = [
|
||||
var.some_vlan_network_id,
|
||||
]
|
||||
|
||||
poe_mode = "auto"
|
||||
}
|
||||
|
||||
resource "unifi_device" "us_24_poe" {
|
||||
name = "Switch with POE"
|
||||
|
||||
port_override {
|
||||
number = 1
|
||||
name = "port w/ poe"
|
||||
port_profile_id = unifi_port_profile.poe.id
|
||||
}
|
||||
|
||||
port_override {
|
||||
number = 2
|
||||
name = "disabled"
|
||||
port_profile_id = data.unifi_port_profile.disabled.id
|
||||
}
|
||||
}
|
||||
2
go.mod
2
go.mod
@@ -29,7 +29,7 @@ require (
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/paultyng/go-unifi v1.12.0
|
||||
github.com/paultyng/go-unifi v1.13.0
|
||||
github.com/posener/complete v1.2.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
|
||||
|
||||
4
go.sum
4
go.sum
@@ -303,8 +303,8 @@ github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce/go.mod h1:uFMI8w+ref4
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
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/paultyng/go-unifi v1.12.0 h1:ENuIMHpV+ndSCi4xv5RCEw+uSrxN7fUmfTdY36Tcmhk=
|
||||
github.com/paultyng/go-unifi v1.12.0/go.mod h1:aF1ya3pylyb7RRCcFxaOB6oZSWYlAnmD8ycv4AMW2+I=
|
||||
github.com/paultyng/go-unifi v1.13.0 h1:j44gxcCqXkkJxdJ6NSrUdCBOLRiuLzkgWQtNDjk99qo=
|
||||
github.com/paultyng/go-unifi v1.13.0/go.mod h1:aF1ya3pylyb7RRCcFxaOB6oZSWYlAnmD8ycv4AMW2+I=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
|
||||
@@ -167,6 +167,36 @@ func (c *lazyClient) UpdateUserGroup(ctx context.Context, site string, d *unifi.
|
||||
}
|
||||
return c.inner.UpdateUserGroup(ctx, site, d)
|
||||
}
|
||||
func (c *lazyClient) GetDevice(ctx context.Context, site, id string) (*unifi.Device, error) {
|
||||
if err := c.init(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.inner.GetDevice(ctx, site, id)
|
||||
}
|
||||
func (c *lazyClient) CreateDevice(ctx context.Context, site string, d *unifi.Device) (*unifi.Device, error) {
|
||||
if err := c.init(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.inner.CreateDevice(ctx, site, d)
|
||||
}
|
||||
func (c *lazyClient) UpdateDevice(ctx context.Context, site string, d *unifi.Device) (*unifi.Device, error) {
|
||||
if err := c.init(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.inner.UpdateDevice(ctx, site, d)
|
||||
}
|
||||
func (c *lazyClient) DeleteDevice(ctx context.Context, site, id string) error {
|
||||
if err := c.init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.inner.DeleteDevice(ctx, site, id)
|
||||
}
|
||||
func (c *lazyClient) ListDevice(ctx context.Context, site string) ([]unifi.Device, error) {
|
||||
if err := c.init(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.inner.ListDevice(ctx, site)
|
||||
}
|
||||
func (c *lazyClient) GetUser(ctx context.Context, site, id string) (*unifi.User, error) {
|
||||
if err := c.init(ctx); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -9,8 +9,12 @@ import (
|
||||
|
||||
var macAddressRegexp = regexp.MustCompile("^([0-9a-fA-F][0-9a-fA-F][-:]){5}([0-9a-fA-F][0-9a-fA-F])$")
|
||||
|
||||
func cleanMAC(mac string) string {
|
||||
return strings.TrimSpace(strings.ReplaceAll(strings.ToLower(mac), "-", ":"))
|
||||
}
|
||||
|
||||
func macDiffSuppressFunc(k, old, new string, d *schema.ResourceData) bool {
|
||||
old = strings.TrimSpace(strings.ReplaceAll(strings.ToLower(old), "-", ":"))
|
||||
new = strings.TrimSpace(strings.ReplaceAll(strings.ToLower(new), "-", ":"))
|
||||
old = cleanMAC(old)
|
||||
new = cleanMAC(new)
|
||||
return old == new
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ func New(version string) func() *schema.Provider {
|
||||
},
|
||||
ResourcesMap: map[string]*schema.Resource{
|
||||
// TODO: "unifi_ap_group"
|
||||
"unifi_device": resourceDevice(),
|
||||
"unifi_firewall_group": resourceFirewallGroup(),
|
||||
"unifi_firewall_rule": resourceFirewallRule(),
|
||||
"unifi_network": resourceNetwork(),
|
||||
@@ -156,6 +157,12 @@ type unifiClient interface {
|
||||
GetWLAN(ctx context.Context, site, id string) (*unifi.WLAN, error)
|
||||
UpdateWLAN(ctx context.Context, site string, d *unifi.WLAN) (*unifi.WLAN, error)
|
||||
|
||||
GetDevice(ctx context.Context, site, id string) (*unifi.Device, error)
|
||||
CreateDevice(ctx context.Context, site string, d *unifi.Device) (*unifi.Device, error)
|
||||
UpdateDevice(ctx context.Context, site string, d *unifi.Device) (*unifi.Device, error)
|
||||
DeleteDevice(ctx context.Context, site, id string) error
|
||||
ListDevice(ctx context.Context, site string) ([]unifi.Device, error)
|
||||
|
||||
GetUser(ctx context.Context, site, id string) (*unifi.User, error)
|
||||
GetUserByMAC(ctx context.Context, site, mac string) (*unifi.User, error)
|
||||
CreateUser(ctx context.Context, site string, d *unifi.User) (*unifi.User, error)
|
||||
|
||||
273
internal/provider/resource_device.go
Normal file
273
internal/provider/resource_device.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||
"github.com/paultyng/go-unifi/unifi"
|
||||
)
|
||||
|
||||
func resourceDevice() *schema.Resource {
|
||||
return &schema.Resource{
|
||||
Description: "`unifi_device` manages a device of the network.\n\n" +
|
||||
"Devices are adopted by the controller, so it is not possible " +
|
||||
"for this resource to be created through Terraform.",
|
||||
|
||||
Create: resourceDeviceCreate,
|
||||
Read: resourceDeviceRead,
|
||||
Update: resourceDeviceUpdate,
|
||||
DeleteContext: resourceDeviceDelete,
|
||||
Importer: &schema.ResourceImporter{
|
||||
StateContext: resourceDeviceImport,
|
||||
},
|
||||
|
||||
Schema: map[string]*schema.Schema{
|
||||
"id": {
|
||||
Description: "The ID of the device.",
|
||||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
},
|
||||
"site": {
|
||||
Description: "The name of the site to associate the device with.",
|
||||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
Optional: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
"mac": {
|
||||
Description: "The MAC address of the device.",
|
||||
Type: schema.TypeString,
|
||||
Computed: true,
|
||||
},
|
||||
"name": {
|
||||
Description: "The name of the device.",
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"disabled": {
|
||||
Description: "Specifies whether this device should be disabled.",
|
||||
Type: schema.TypeBool,
|
||||
Computed: true,
|
||||
},
|
||||
"port_override": {
|
||||
Description: "Settings overrides for specific switch ports.",
|
||||
// TODO: this should really be a map or something when possible in the SDK
|
||||
Type: schema.TypeSet,
|
||||
Optional: true,
|
||||
Set: resourceDevicePortOverrideSet,
|
||||
Elem: &schema.Resource{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"number": {
|
||||
Description: "Switch port number.",
|
||||
Type: schema.TypeInt,
|
||||
Required: true,
|
||||
},
|
||||
"name": {
|
||||
Description: "Human-readable name of the port.",
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
},
|
||||
"port_profile_id": {
|
||||
Description: "ID of the Port Profile used on this port.",
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func resourceDevicePortOverrideSet(v interface{}) int {
|
||||
m := v.(map[string]interface{})
|
||||
return m["number"].(int)
|
||||
}
|
||||
|
||||
func resourceDeviceImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
|
||||
c := meta.(*client)
|
||||
id := d.Id()
|
||||
site := d.Get("site").(string)
|
||||
if site == "" {
|
||||
site = c.site
|
||||
}
|
||||
|
||||
if colons := strings.Count(id, ":"); colons == 1 || colons == 6 {
|
||||
importParts := strings.SplitN(id, ":", 2)
|
||||
site = importParts[0]
|
||||
id = importParts[1]
|
||||
}
|
||||
|
||||
if macAddressRegexp.MatchString(id) {
|
||||
// look up id by mac
|
||||
find := cleanMAC(id)
|
||||
|
||||
devices, err := c.c.ListDevice(ctx, site)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, d := range devices {
|
||||
if cleanMAC(d.MAC) == find {
|
||||
id = d.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if id != "" {
|
||||
d.SetId(id)
|
||||
}
|
||||
if site != "" {
|
||||
d.Set("site", site)
|
||||
}
|
||||
|
||||
return []*schema.ResourceData{d}, nil
|
||||
}
|
||||
|
||||
func resourceDeviceCreate(d *schema.ResourceData, meta interface{}) error {
|
||||
return errors.New("unifi_device can only be imported, not created")
|
||||
}
|
||||
|
||||
func resourceDeviceUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||
c := meta.(*client)
|
||||
|
||||
site := d.Get("site").(string)
|
||||
if site == "" {
|
||||
site = c.site
|
||||
}
|
||||
|
||||
req, err := resourceDeviceGetResourceData(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.ID = d.Id()
|
||||
req.SiteID = site
|
||||
|
||||
resp, err := c.c.UpdateDevice(context.TODO(), site, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return resourceDeviceSetResourceData(resp, d, site)
|
||||
}
|
||||
|
||||
func resourceDeviceDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
|
||||
return diag.Diagnostics{
|
||||
diag.Diagnostic{
|
||||
Severity: diag.Warning,
|
||||
Summary: "Deleting a device via Terraform is not supported, the device will just be removed from state.",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func resourceDeviceRead(d *schema.ResourceData, meta interface{}) error {
|
||||
c := meta.(*client)
|
||||
|
||||
id := d.Id()
|
||||
|
||||
site := d.Get("site").(string)
|
||||
if site == "" {
|
||||
site = c.site
|
||||
}
|
||||
|
||||
resp, err := c.c.GetDevice(context.TODO(), site, id)
|
||||
if _, ok := err.(*unifi.NotFoundError); ok {
|
||||
d.SetId("")
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return resourceDeviceSetResourceData(resp, d, site)
|
||||
}
|
||||
|
||||
func resourceDeviceSetResourceData(resp *unifi.Device, d *schema.ResourceData, site string) error {
|
||||
portOverrides, err := setFromPortOverrides(resp.PortOverrides)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.Set("site", site)
|
||||
d.Set("mac", resp.MAC)
|
||||
d.Set("name", resp.Name)
|
||||
d.Set("disabled", resp.Disabled)
|
||||
d.Set("port_override", portOverrides)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func resourceDeviceGetResourceData(d *schema.ResourceData) (*unifi.Device, error) {
|
||||
pos, err := setToPortOverrides(d.Get("port_override").(*schema.Set))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to process port_override block: %w", err)
|
||||
}
|
||||
|
||||
//TODO: pass Disabled once we figure out how to enable the device afterwards
|
||||
|
||||
return &unifi.Device{
|
||||
MAC: d.Get("mac").(string),
|
||||
Name: d.Get("name").(string),
|
||||
PortOverrides: pos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func setToPortOverrides(set *schema.Set) ([]unifi.DevicePortOverrides, error) {
|
||||
// use a map here to remove any duplication
|
||||
overrideMap := map[int]unifi.DevicePortOverrides{}
|
||||
for _, item := range set.List() {
|
||||
data, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected data in block")
|
||||
}
|
||||
po, err := toPortOverride(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create port override: %w", err)
|
||||
}
|
||||
overrideMap[po.PortIDX] = po
|
||||
}
|
||||
|
||||
pos := make([]unifi.DevicePortOverrides, 0, len(overrideMap))
|
||||
for _, item := range overrideMap {
|
||||
pos = append(pos, item)
|
||||
}
|
||||
return pos, nil
|
||||
}
|
||||
|
||||
func setFromPortOverrides(pos []unifi.DevicePortOverrides) (*schema.Set, error) {
|
||||
list := make([]interface{}, 0, len(pos))
|
||||
for _, po := range pos {
|
||||
v, err := fromPortOverride(po)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse port override: %w", err)
|
||||
}
|
||||
list = append(list, v)
|
||||
}
|
||||
return schema.NewSet(resourceDevicePortOverrideSet, list), nil
|
||||
}
|
||||
|
||||
func toPortOverride(data map[string]interface{}) (unifi.DevicePortOverrides, error) {
|
||||
// TODO: error check these?
|
||||
idx := data["number"].(int)
|
||||
name := data["name"].(string)
|
||||
profile_id := data["port_profile_id"].(string)
|
||||
return unifi.DevicePortOverrides{
|
||||
PortIDX: idx,
|
||||
Name: name,
|
||||
PortProfileID: profile_id,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func fromPortOverride(po unifi.DevicePortOverrides) (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"number": po.PortIDX,
|
||||
"name": po.Name,
|
||||
"port_profile_id": po.PortProfileID,
|
||||
}, nil
|
||||
}
|
||||
@@ -2,9 +2,9 @@ package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
|
||||
"github.com/paultyng/go-unifi/unifi"
|
||||
)
|
||||
|
||||
@@ -66,7 +66,7 @@ func resourcePortProfile() *schema.Resource {
|
||||
Default: false,
|
||||
},
|
||||
"forward": {
|
||||
Description: "The type forwarding to use for the port profile. Can be `all`, `native`, `customize` or `disabled`.",
|
||||
Description: "The type forwarding to use for the port profile. Can be `all`, `native`, `customize` or `disabled`.",
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Default: "native",
|
||||
@@ -96,6 +96,7 @@ func resourcePortProfile() *schema.Resource {
|
||||
Optional: true,
|
||||
//ValidateFunc: ,
|
||||
},
|
||||
// TODO: rename to native_network_id
|
||||
"native_networkconf_id": {
|
||||
Description: "The ID of network to use as the main network on the port profile.",
|
||||
Type: schema.TypeString,
|
||||
@@ -230,12 +231,14 @@ func resourcePortProfile() *schema.Resource {
|
||||
Optional: true,
|
||||
Default: true,
|
||||
},
|
||||
// TODO: renamed to tagged_network_ids
|
||||
"tagged_networkconf_ids": {
|
||||
Description: "The IDs of networks to tag traffic with for the port profile.",
|
||||
Type: schema.TypeSet,
|
||||
Optional: true,
|
||||
Elem: &schema.Schema{Type: schema.TypeString},
|
||||
},
|
||||
// TODO: rename to voice_network_id
|
||||
"voice_networkconf_id": {
|
||||
Description: "The ID of network to use as the voice network on the port profile.",
|
||||
Type: schema.TypeString,
|
||||
|
||||
Reference in New Issue
Block a user