* feat: initialize Terraform Plugin Framework * fix docker-compose path for tests * fix: ensure documentation can be generated with old provider SDK and new plugin framework * lint
419 lines
12 KiB
Go
419 lines
12 KiB
Go
package v1
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/filipowm/terraform-provider-unifi/internal/provider"
|
|
"strings"
|
|
|
|
"github.com/filipowm/go-unifi/unifi"
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
|
|
)
|
|
|
|
func resourceRadiusProfile() *schema.Resource {
|
|
return &schema.Resource{
|
|
Description: "`unifi_radius_profile` manages RADIUS profiles.",
|
|
|
|
CreateContext: resourceRadiusProfileCreate,
|
|
ReadContext: resourceRadiusProfileRead,
|
|
UpdateContext: resourceRadiusProfileUpdate,
|
|
DeleteContext: resourceRadiusProfileDelete,
|
|
Importer: &schema.ResourceImporter{
|
|
StateContext: importRadiusProfile,
|
|
},
|
|
|
|
Schema: map[string]*schema.Schema{
|
|
"id": {
|
|
Description: "The ID of the settings.",
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
"site": {
|
|
Description: "The name of the site to associate the settings with.",
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
Optional: true,
|
|
ForceNew: true,
|
|
},
|
|
"name": {
|
|
Description: "The name of the profile.",
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
},
|
|
"accounting_enabled": {
|
|
Description: "Specifies whether to use RADIUS accounting.",
|
|
Type: schema.TypeBool,
|
|
Default: false,
|
|
Optional: true,
|
|
},
|
|
"interim_update_enabled": {
|
|
Description: "Specifies whether to use interim_update.",
|
|
Type: schema.TypeBool,
|
|
Default: false,
|
|
Optional: true,
|
|
},
|
|
"interim_update_interval": {
|
|
Description: "Specifies interim_update interval.",
|
|
Type: schema.TypeInt,
|
|
Default: 3600,
|
|
Optional: true,
|
|
},
|
|
"use_usg_acct_server": {
|
|
Description: "Specifies whether to use usg as a RADIUS accounting server.",
|
|
Type: schema.TypeBool,
|
|
Default: false,
|
|
Optional: true,
|
|
},
|
|
"use_usg_auth_server": {
|
|
Description: "Specifies whether to use usg as a RADIUS authentication server.",
|
|
Type: schema.TypeBool,
|
|
Default: false,
|
|
Optional: true,
|
|
},
|
|
"vlan_enabled": {
|
|
Description: "Specifies whether to use vlan on wired connections.",
|
|
Type: schema.TypeBool,
|
|
Default: false,
|
|
Optional: true,
|
|
},
|
|
"vlan_wlan_mode": {
|
|
Description: "Specifies whether to use vlan on wireless connections. Must be one of `disabled`, `optional`, or `required`.",
|
|
Type: schema.TypeString,
|
|
Default: "",
|
|
Optional: true,
|
|
ValidateFunc: validation.StringInSlice([]string{"disabled", "optional", "required"}, false),
|
|
},
|
|
"auth_server": {
|
|
Description: "RADIUS authentication servers.",
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"ip": {
|
|
Description: "IP address of authentication service server.",
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ValidateFunc: validation.IsIPAddress,
|
|
},
|
|
"port": {
|
|
Description: "Port of authentication service.",
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
Default: 1812,
|
|
ValidateFunc: validation.IsPortNumber,
|
|
},
|
|
"xsecret": {
|
|
Description: "RADIUS secret.",
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
Sensitive: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"acct_server": {
|
|
Description: "RADIUS accounting servers.",
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"ip": {
|
|
Description: "IP address of accounting service server.",
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ValidateFunc: validation.IsIPAddress,
|
|
},
|
|
"port": {
|
|
Description: "Port of accounting service.",
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
Default: 1813,
|
|
ValidateFunc: validation.IsPortNumber,
|
|
},
|
|
"xsecret": {
|
|
Description: "RADIUS secret.",
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
Sensitive: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func setToAuthServers(set []interface{}) ([]unifi.RADIUSProfileAuthServers, error) {
|
|
var authServers []unifi.RADIUSProfileAuthServers
|
|
for _, item := range set {
|
|
data, ok := item.(map[string]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected data in block")
|
|
}
|
|
authServer, err := toAuthServer(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to create port override: %w", err)
|
|
}
|
|
authServers = append(authServers, authServer)
|
|
}
|
|
return authServers, nil
|
|
}
|
|
|
|
func setToAcctServers(set []interface{}) ([]unifi.RADIUSProfileAcctServers, error) {
|
|
var acctServers []unifi.RADIUSProfileAcctServers
|
|
for _, item := range set {
|
|
data, ok := item.(map[string]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected data in block")
|
|
}
|
|
accServer, err := toAcctServer(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to create port override: %w", err)
|
|
}
|
|
acctServers = append(acctServers, accServer)
|
|
}
|
|
return acctServers, nil
|
|
}
|
|
|
|
func toAuthServer(data map[string]interface{}) (unifi.RADIUSProfileAuthServers, error) {
|
|
return unifi.RADIUSProfileAuthServers{
|
|
IP: data["ip"].(string),
|
|
Port: data["port"].(int),
|
|
XSecret: data["xsecret"].(string),
|
|
}, nil
|
|
}
|
|
|
|
func toAcctServer(data map[string]interface{}) (unifi.RADIUSProfileAcctServers, error) {
|
|
return unifi.RADIUSProfileAcctServers{
|
|
IP: data["ip"].(string),
|
|
Port: data["port"].(int),
|
|
XSecret: data["xsecret"].(string),
|
|
}, nil
|
|
}
|
|
|
|
func setFromAuthServers(authServers []unifi.RADIUSProfileAuthServers) ([]map[string]interface{}, error) {
|
|
list := make([]map[string]interface{}, 0, len(authServers))
|
|
for _, authServer := range authServers {
|
|
v, err := fromAuthServer(authServer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse ssh key: %w", err)
|
|
}
|
|
list = append(list, v)
|
|
}
|
|
return list, nil
|
|
}
|
|
|
|
func setFromAcctServers(acctServers []unifi.RADIUSProfileAcctServers) ([]map[string]interface{}, error) {
|
|
list := make([]map[string]interface{}, 0, len(acctServers))
|
|
for _, acctServer := range acctServers {
|
|
v, err := fromAcctServer(acctServer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse ssh key: %w", err)
|
|
}
|
|
list = append(list, v)
|
|
}
|
|
return list, nil
|
|
}
|
|
|
|
func fromAuthServer(sshKey unifi.RADIUSProfileAuthServers) (map[string]interface{}, error) {
|
|
return map[string]interface{}{
|
|
"ip": sshKey.IP,
|
|
"port": sshKey.Port,
|
|
"xsecret": sshKey.XSecret,
|
|
}, nil
|
|
}
|
|
|
|
func fromAcctServer(sshKey unifi.RADIUSProfileAcctServers) (map[string]interface{}, error) {
|
|
return map[string]interface{}{
|
|
"ip": sshKey.IP,
|
|
"port": sshKey.Port,
|
|
"xsecret": sshKey.XSecret,
|
|
}, nil
|
|
}
|
|
|
|
func resourceRadiusProfileCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
|
|
c := meta.(*provider.Client)
|
|
req, err := resourceRadiusProfileGetResourceData(d)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
site := d.Get("site").(string)
|
|
if site == "" {
|
|
site = c.Site
|
|
}
|
|
resp, err := c.CreateRADIUSProfile(ctx, site, req)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
d.SetId(resp.ID)
|
|
|
|
return resourceRadiusProfileSetResourceData(resp, d, site)
|
|
}
|
|
|
|
func resourceRadiusProfileGetResourceData(d *schema.ResourceData) (*unifi.RADIUSProfile, error) {
|
|
authServers, err := setToAuthServers(d.Get("auth_server").([]interface{}))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to auth_server ssh_key block: %w", err)
|
|
}
|
|
acctServers, err := setToAcctServers(d.Get("acct_server").([]interface{}))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to acct_server ssh_key block: %w", err)
|
|
}
|
|
return &unifi.RADIUSProfile{
|
|
Name: d.Get("name").(string),
|
|
InterimUpdateEnabled: d.Get("interim_update_enabled").(bool),
|
|
InterimUpdateInterval: d.Get("interim_update_interval").(int),
|
|
AccountingEnabled: d.Get("accounting_enabled").(bool),
|
|
UseUsgAcctServer: d.Get("use_usg_acct_server").(bool),
|
|
UseUsgAuthServer: d.Get("use_usg_auth_server").(bool),
|
|
VLANEnabled: d.Get("vlan_enabled").(bool),
|
|
VLANWLANMode: d.Get("vlan_wlan_mode").(string),
|
|
AuthServers: authServers,
|
|
AcctServers: acctServers,
|
|
}, nil
|
|
}
|
|
|
|
func resourceRadiusProfileSetResourceData(resp *unifi.RADIUSProfile, d *schema.ResourceData, site string) diag.Diagnostics {
|
|
authServers, err := setFromAuthServers(resp.AuthServers)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
acctServers, err := setFromAcctServers(resp.AcctServers)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
d.Set("site", site)
|
|
d.Set("name", resp.Name)
|
|
|
|
d.Set("interim_update_enabled", resp.InterimUpdateEnabled)
|
|
d.Set("interim_update_interval", resp.InterimUpdateInterval)
|
|
d.Set("accounting_enabled", resp.AccountingEnabled)
|
|
d.Set("use_usg_acct_server", resp.UseUsgAcctServer)
|
|
d.Set("use_usg_auth_server", resp.UseUsgAuthServer)
|
|
d.Set("vlan_enabled", resp.VLANEnabled)
|
|
d.Set("vlan_wlan_mode", resp.VLANWLANMode)
|
|
d.Set("auth_server", authServers)
|
|
d.Set("acct_server", acctServers)
|
|
return nil
|
|
}
|
|
|
|
func resourceRadiusProfileRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
|
|
c := meta.(*provider.Client)
|
|
|
|
id := d.Id()
|
|
|
|
site := d.Get("site").(string)
|
|
if site == "" {
|
|
site = c.Site
|
|
}
|
|
resp, err := c.GetRADIUSProfile(ctx, site, id)
|
|
if errors.Is(err, unifi.ErrNotFound) {
|
|
d.SetId("")
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
return resourceRadiusProfileSetResourceData(resp, d, site)
|
|
}
|
|
|
|
func resourceRadiusProfileUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
|
|
c := meta.(*provider.Client)
|
|
|
|
req, err := resourceRadiusProfileGetResourceData(d)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
req.ID = d.Id()
|
|
|
|
site := d.Get("site").(string)
|
|
if site == "" {
|
|
site = c.Site
|
|
}
|
|
req.SiteID = site
|
|
|
|
resp, err := c.UpdateRADIUSProfile(ctx, site, req)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
return resourceRadiusProfileSetResourceData(resp, d, site)
|
|
}
|
|
|
|
func resourceRadiusProfileDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
|
|
c := meta.(*provider.Client)
|
|
|
|
id := d.Id()
|
|
|
|
site := d.Get("site").(string)
|
|
if site == "" {
|
|
site = c.Site
|
|
}
|
|
|
|
err := c.DeleteRADIUSProfile(ctx, site, id)
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
func importRadiusProfile(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
|
|
c := meta.(*provider.Client)
|
|
id := d.Id()
|
|
site := d.Get("site").(string)
|
|
if site == "" {
|
|
site = c.Site
|
|
}
|
|
|
|
if strings.Contains(id, ":") {
|
|
importParts := strings.SplitN(id, ":", 2)
|
|
site = importParts[0]
|
|
id = importParts[1]
|
|
}
|
|
|
|
if strings.HasPrefix(id, "name=") {
|
|
targetName := strings.TrimPrefix(id, "name=")
|
|
var err error
|
|
if id, err = getRadiusProfileIDByName(ctx, c.Client, targetName, site); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if id != "" {
|
|
d.SetId(id)
|
|
}
|
|
if site != "" {
|
|
d.Set("site", site)
|
|
}
|
|
|
|
return []*schema.ResourceData{d}, nil
|
|
}
|
|
|
|
func getRadiusProfileIDByName(ctx context.Context, client unifi.Client, profileName, site string) (string, error) {
|
|
radiusProfiles, err := client.ListRADIUSProfile(ctx, site)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
idMatchingName := ""
|
|
allNames := []string{}
|
|
for _, profile := range radiusProfiles {
|
|
allNames = append(allNames, profile.Name)
|
|
if profile.Name != profileName {
|
|
continue
|
|
}
|
|
if idMatchingName != "" {
|
|
return "", fmt.Errorf("Found multiple RADIUS profiles with name '%s'", profileName)
|
|
}
|
|
idMatchingName = profile.ID
|
|
}
|
|
if idMatchingName == "" {
|
|
return "", fmt.Errorf("Found no RADIUS profile with name '%s', found: %s", profileName, strings.Join(allNames, ", "))
|
|
}
|
|
return idMatchingName, nil
|
|
}
|