Add rudimentary support for resource unifi_device (#112)

This commit is contained in:
wolf-cosmose
2021-03-21 03:38:32 +01:00
committed by GitHub
parent e08c2acdc2
commit 051ed9875e
10 changed files with 437 additions and 8 deletions

80
docs/resources/device.md Normal file
View 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.

View File

@@ -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`.

View 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
View File

@@ -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
View File

@@ -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=

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)

View 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
}

View File

@@ -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,