diff --git a/provider/provider.go b/provider/provider.go index b4f9188..7a327b2 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -38,6 +38,7 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ "unifi_network": resourceNetwork(), + "unifi_wlan": resourceWLAN(), }, } p.ConfigureFunc = configure(p) diff --git a/provider/resource_wlan.go b/provider/resource_wlan.go new file mode 100644 index 0000000..8f1c0b8 --- /dev/null +++ b/provider/resource_wlan.go @@ -0,0 +1,153 @@ +package provider + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + + "github.com/paultyng/terraform-provider-unifi/unifi" +) + +func resourceWLAN() *schema.Resource { + return &schema.Resource{ + Create: resourceWLANCreate, + Read: resourceWLANRead, + Update: resourceWLANUpdate, + Delete: resourceWLANDelete, + + // TODO: handle site + ID (or name) + // Importer: &schema.ResourceImporter{ + // State: schema.ImportStatePassthrough, + // }, + + Schema: map[string]*schema.Schema{ + "site": { + Type: schema.TypeString, + Optional: true, + Default: "default", + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "vlan_id": { + Type: schema.TypeInt, + Optional: true, + Default: 1, + }, + "passphrase": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + }, + }, + } +} + +func resourceWLANCreate(d *schema.ResourceData, meta interface{}) error { + c := meta.(*client) + + site := d.Get("site").(string) + + // TODO: allow passing these defaults + wlanGroups, err := c.c.ListWLANGroup(site) + if err != nil { + return err + } + var defaultWLANGroup *unifi.WLANGroup + for _, wg := range wlanGroups { + if wg.HiddenID == "Default" { + defaultWLANGroup = &wg + break + } + } + if defaultWLANGroup == nil { + return fmt.Errorf("unable to find default WLAN group") + } + + userGroups, err := c.c.ListUserGroup(site) + if err != nil { + return err + } + var defaultUserGroup *unifi.UserGroup + for _, ug := range userGroups { + if ug.HiddenID == "Default" { + defaultUserGroup = &ug + break + } + } + if defaultUserGroup == nil { + return fmt.Errorf("unable to find default user group") + } + + req := &unifi.WLAN{ + Name: d.Get("name").(string), + VLAN: fmt.Sprintf("%d", d.Get("vlan_id").(int)), + XPassphrase: d.Get("passphrase").(string), + + WLANGroupID: defaultWLANGroup.ID, + UserGroupID: defaultUserGroup.ID, + + Enabled: true, + VLANEnabled: true, + WPAEnc: "ccmp", + Security: "wpapsk", + WPAMode: "wpa2", + NameCombineEnabled: true, + GroupRekey: 3600, + DTIMMode: "default", + No2GhzOui: true, + MinrateNaBeaconRateKbps: 6000, + MinrateNaDataRateKbps: 6000, + MinrateNaMgmtRateKbps: 6000, + MinrateNgBeaconRateKbps: 1000, + MinrateNgCckRatesEnabled: true, + MinrateNgDataRateKbps: 1000, + MinrateNgMgmtRateKbps: 1000, + } + + resp, err := c.c.CreateWLAN(site, req) + if err != nil { + return err + } + + d.SetId(resp.ID) + + return nil +} + +func resourceWLANRead(d *schema.ResourceData, meta interface{}) error { + c := meta.(*client) + + site := d.Get("site").(string) + id := d.Id() + + _, err := c.c.GetWLAN(site, id) + if _, ok := err.(*unifi.NotFoundError); ok { + d.SetId("") + return nil + } + if err != nil { + return err + } + + return nil +} + +func resourceWLANUpdate(d *schema.ResourceData, meta interface{}) error { + panic("not implemented") +} + +func resourceWLANDelete(d *schema.ResourceData, meta interface{}) error { + c := meta.(*client) + + site := d.Get("site").(string) + id := d.Id() + + err := c.c.DeleteWLAN(site, id) + if _, ok := err.(*unifi.NotFoundError); ok { + return nil + } + return err +} diff --git a/provider/resource_wlan_test.go b/provider/resource_wlan_test.go new file mode 100644 index 0000000..cb7dc3e --- /dev/null +++ b/provider/resource_wlan_test.go @@ -0,0 +1,31 @@ +package provider + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccWLAN_basic(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + Providers: providers, + // TODO: CheckDestroy: , + Steps: []resource.TestStep{ + { + Config: testAccWLANConfig, + Check: resource.ComposeTestCheckFunc( + // testCheckNetworkExists(t, "name"), + ), + }, + // importStep("unifi_wlan.test"), + }, + }) +} + +const testAccWLANConfig = ` +resource "unifi_wlan" "test" { + name = "foo" + vlan_id = 202 + passphrase = "12345678" +} +` diff --git a/unifi/networks.go b/unifi/networks.go index 1e43b2f..e507526 100644 --- a/unifi/networks.go +++ b/unifi/networks.go @@ -5,9 +5,12 @@ import ( ) type Network struct { - ID string `json:"_id,omitempty"` - HiddenID string `json:"attr_hidden_id,omitempty"` - NoDelete bool `json:"attr_no_delete,omitempty"` + ID string `json:"_id,omitempty"` + + // Hidden bool `json:"attr_hidden,omitempty"` + // HiddenID string `json:"attr_hidden_id,omitempty"` + // NoDelete bool `json:"attr_no_delete,omitempty"` + // NoEdit bool `json:"attr_no_edit,omitempty"` Purpose string `json:"purpose"` // "corporate" NetworkGroup string `json:"networkgroup"` // "LAN" @@ -34,11 +37,9 @@ type Network struct { IPV6PDStop string `json:"ipv6_pd_stop"` // "::7d1" } -func (c *Client) ListNetworks(site string) ([]Network, error) { +func (c *Client) ListNetwork(site string) ([]Network, error) { var respBody struct { - Meta struct { - RC string `json:"rc"` - } `json:"meta"` + Meta meta `json:"meta"` Data []Network `json:"data"` } @@ -51,35 +52,22 @@ func (c *Client) ListNetworks(site string) ([]Network, error) { } func (c *Client) GetNetwork(site, id string) (*Network, error) { - list, err := c.ListNetworks(site) + var respBody struct { + Meta meta `json:"meta"` + Data []Network `json:"data"` + } + + err := c.do("GET", fmt.Sprintf("s/%s/rest/networkconf/%s", site, id), nil, &respBody) if err != nil { return nil, err } - for _, net := range list { - if net.ID == id { - return &net, nil - } + + if len(respBody.Data) != 1 { + return nil, &NotFoundError{} } - return nil, &NotFoundError{} - // var respBody struct { - // Meta struct { - // RC string `json:"rc"` - // } `json:"meta"` - // Data []Network `json:"data"` - // } - - // err := c.do("GET", fmt.Sprintf("s/%s/rest/networkconf/%s", site, id), nil, &respBody) - // if err != nil { - // return nil, err - // } - - // if len(respBody.Data) != 1 { - // return nil, &NotFoundError{} - // } - - // net := respBody.Data[0] - // return &net, nil + d := respBody.Data[0] + return &d, nil } func (c *Client) DeleteNetwork(site, id, name string) error { @@ -94,15 +82,13 @@ func (c *Client) DeleteNetwork(site, id, name string) error { return nil } -func (c *Client) CreateNetwork(site string, network *Network) (*Network, error) { +func (c *Client) CreateNetwork(site string, d *Network) (*Network, error) { var respBody struct { - Meta struct { - RC string `json:"rc"` - } `json:"meta"` + Meta meta `json:"meta"` Data []Network `json:"data"` } - err := c.do("POST", fmt.Sprintf("s/%s/rest/networkconf", site), network, &respBody) + err := c.do("POST", fmt.Sprintf("s/%s/rest/networkconf", site), d, &respBody) if err != nil { return nil, err } @@ -111,7 +97,7 @@ func (c *Client) CreateNetwork(site string, network *Network) (*Network, error) return nil, &NotFoundError{} } - newNetwork := respBody.Data[0] + new := respBody.Data[0] - return &newNetwork, nil + return &new, nil } diff --git a/unifi/sites.go b/unifi/sites.go index f3b3541..226f6b0 100644 --- a/unifi/sites.go +++ b/unifi/sites.go @@ -1,9 +1,12 @@ package unifi type Site struct { - ID string `json:"_id,omitempty"` - HiddenID string `json:"attr_hidden_id,omitempty"` - NoDelete bool `json:"attr_no_delete,omitempty"` + ID string `json:"_id,omitempty"` + + // Hidden bool `json:"attr_hidden,omitempty"` + // HiddenID string `json:"attr_hidden_id,omitempty"` + // NoDelete bool `json:"attr_no_delete,omitempty"` + // NoEdit bool `json:"attr_no_edit,omitempty"` Name string `json:"name"` Description string `json:"desc"` @@ -13,9 +16,7 @@ type Site struct { func (c *Client) ListSites() ([]Site, error) { var respBody struct { - Meta struct { - RC string `json:"rc"` - } `json:"meta"` + Meta meta `json:"meta"` Data []Site `json:"data"` } diff --git a/unifi/unifi.go b/unifi/unifi.go index a95f353..930f1f6 100644 --- a/unifi/unifi.go +++ b/unifi/unifi.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "io" - // "io/ioutil" "net" "net/http" "net/http/cookiejar" @@ -73,9 +72,14 @@ func (c *Client) Login(user, pass string) error { } func (c *Client) do(method, relativeURL string, reqBody interface{}, respBody interface{}) error { - var reqReader io.Reader + var ( + reqReader io.Reader + err error + reqBytes []byte + ) if reqBody != nil { - reqBytes, err := json.Marshal(reqBody) + + reqBytes, err = json.Marshal(reqBody) if err != nil { return err } @@ -101,11 +105,12 @@ func (c *Client) do(method, relativeURL string, reqBody interface{}, respBody in } if resp.StatusCode != 200 { - // body, _ := ioutil.ReadAll(resp.Body) - //TODO: debug logging? - // fmt.Printf("%s %s\nStatus: %s\n%s", method, url.String(), resp.Status, string(body)) - - return fmt.Errorf("error from API %s", resp.Status) + fmt.Printf("Request Body:\n%s\n", string(reqBytes)) + errBody := struct { + Meta meta `json:"meta"` + }{} + err = json.NewDecoder(resp.Body).Decode(&errBody) + return fmt.Errorf("%s %s (%s) for %s %s", errBody.Meta.RC, errBody.Meta.Message, resp.Status, method, url.String()) } if respBody == nil || resp.ContentLength == 0 { @@ -121,3 +126,8 @@ func (c *Client) do(method, relativeURL string, reqBody interface{}, respBody in return nil } + +type meta struct { + RC string `json:"rc"` + Message string `json:"msg"` +} diff --git a/unifi/user_group.go b/unifi/user_group.go new file mode 100644 index 0000000..6915231 --- /dev/null +++ b/unifi/user_group.go @@ -0,0 +1,33 @@ +package unifi + +import ( + "fmt" +) + +type UserGroup struct { + ID string `json:"_id"` + SiteID string `json:"site_id"` + Name string `json:"name"` + + //Hidden bool `json:"attr_hidden,omitempty"` + HiddenID string `json:"attr_hidden_id,omitempty"` + NoDelete bool `json:"attr_no_delete,omitempty"` + //NoEdit bool `json:"attr_no_edit,omitempty"` + + QOSRateMaxDown int `json:"qos_rate_max_down"` + QOSRateMaxUp int `json:"qos_rate_max_up"` +} + +func (c *Client) ListUserGroup(site string) ([]UserGroup, error) { + var respBody struct { + Meta meta `json:"meta"` + Data []UserGroup `json:"data"` + } + + err := c.do("GET", fmt.Sprintf("s/%s/rest/usergroup", site), nil, &respBody) + if err != nil { + return nil, err + } + + return respBody.Data, nil +} diff --git a/unifi/wlan.go b/unifi/wlan.go new file mode 100644 index 0000000..fc0ffea --- /dev/null +++ b/unifi/wlan.go @@ -0,0 +1,259 @@ +package unifi + +import ( + "fmt" +) + +/* +{ + "meta": { + "rc": "ok" + }, + "data": [ + { + "_id": "5deeabfc439adf048407dcf3", + "enabled": true, + "name": "fred", + "security": "wpapsk", + "wpa_enc": "ccmp", + "wpa_mode": "wpa2", + "x_passphrase": "Liebeskind", + "wlangroup_id": "5d6d8b0c439adf048407dce9", + "name_combine_enabled": true, + "site_id": "5d6d8b07439adf048407dcd9", + "x_iapp_key": "a199e6f225c01127e4135211236f9767", + "minrate_ng_enabled": true, + "minrate_ng_beacon_rate_kbps": 6000, + "minrate_ng_data_rate_kbps": 6000, + "no2ghz_oui": true, + "wep_idx": 1, + "usergroup_id": "5d6d8b0c439adf048407dce8", + "dtim_mode": "default", + "dtim_ng": 1, + "dtim_na": 1, + "minrate_ng_advertising_rates": false, + "minrate_ng_cck_rates_enabled": true, + "minrate_na_enabled": false, + "minrate_na_advertising_rates": false, + "minrate_na_data_rate_kbps": 6000, + "mac_filter_enabled": false, + "mac_filter_policy": "allow", + "mac_filter_list": [], + "bc_filter_enabled": false, + "bc_filter_list": [], + "group_rekey": 3600, + "vlan_enabled": true, + "vlan": "20", + "radius_das_enabled": false, + "schedule": [], + "minrate_ng_mgmt_rate_kbps": 6000, + "minrate_na_mgmt_rate_kbps": 6000, + "minrate_na_beacon_rate_kbps": 6000 + }, + { + "_id": "5deecfa31e801c052a1a5f5a", + "enabled": true, + "is_guest": true, + "name": "fred-guest", + "security": "wpapsk", + "usergroup_id": "5d6d8b0c439adf048407dce8", + "vlan_enabled": true, + "wlangroup_id": "5d6d8b0c439adf048407dce9", + "x_passphrase": "Enzo and Alice", + "site_id": "5d6d8b07439adf048407dcd9", + "x_iapp_key": "3e6b24dda0d11ce3c097107fc429596e", + "minrate_ng_enabled": true, + "minrate_ng_beacon_rate_kbps": 6000, + "minrate_ng_data_rate_kbps": 6000, + "vlan": "30", + "wep_idx": 1, + "wpa_mode": "wpa2", + "wpa_enc": "ccmp", + "dtim_mode": "default", + "dtim_ng": 1, + "dtim_na": 1, + "minrate_ng_advertising_rates": false, + "minrate_ng_cck_rates_enabled": true, + "minrate_na_enabled": false, + "minrate_na_advertising_rates": false, + "minrate_na_data_rate_kbps": 6000, + "mac_filter_enabled": false, + "mac_filter_policy": "allow", + "mac_filter_list": [], + "name_combine_enabled": true, + "bc_filter_enabled": false, + "bc_filter_list": [], + "group_rekey": 3600, + "radius_das_enabled": false, + "schedule": [], + "minrate_ng_mgmt_rate_kbps": 6000, + "minrate_na_mgmt_rate_kbps": 6000, + "minrate_na_beacon_rate_kbps": 6000 + }, + { + "_id": "5deed0d51e801c052a1a5f64", + "enabled": true, + "name": "patpat", + "security": "wpapsk", + "usergroup_id": "5d6d8b0c439adf048407dce8", + "wlangroup_id": "5d6d8b0c439adf048407dce9", + "x_passphrase": "forever home 23", + "site_id": "5d6d8b07439adf048407dcd9", + "x_iapp_key": "5aa68d6ab2ddedf45230f7f87bf11921", + "minrate_ng_enabled": true, + "minrate_ng_beacon_rate_kbps": 6000, + "minrate_ng_data_rate_kbps": 6000, + "vlan": "40", + "vlan_enabled": true, + "wep_idx": 1, + "wpa_mode": "wpa2", + "wpa_enc": "ccmp", + "dtim_mode": "default", + "dtim_ng": 1, + "dtim_na": 1, + "minrate_ng_advertising_rates": false, + "minrate_ng_cck_rates_enabled": true, + "minrate_na_enabled": false, + "minrate_na_advertising_rates": false, + "minrate_na_data_rate_kbps": 6000, + "mac_filter_enabled": false, + "mac_filter_policy": "allow", + "mac_filter_list": [], + "name_combine_enabled": true, + "bc_filter_enabled": false, + "bc_filter_list": [], + "group_rekey": 3600, + "radius_das_enabled": false, + "schedule": [], + "minrate_ng_mgmt_rate_kbps": 6000, + "minrate_na_mgmt_rate_kbps": 6000, + "minrate_na_beacon_rate_kbps": 6000 + } + ] +} +*/ + +type WLAN struct { + ID string `json:"_id,omitempty"` + SiteID string `json:"site_id,omitempty"` + Enabled bool `json:"enabled"` + Name string `json:"name"` + + Security string `json:"security"` // "wpapsk", "wpaeap", "open" + WPAEnc string `json:"wpa_enc"` // "ccmp", "tkip"? + WPAMode string `json:"wpa_mode"` // "wpa2" + XPassphrase string `json:"x_passphrase"` + + // create only? + FastRoamingEnabled bool `json:"fast_roaming_enabled,omitempty"` + HideSSID bool `json:"hide_ssid,omitempty"` + IsGuest bool `json:"is_guest,omitempty"` + MulticastEnhanceEnabled bool `json:"mcastenhance_enabled,omitempty"` + + RADIUSDasEnabled bool `json:"radius_das_enabled"` + + WLANGroupID string `json:"wlangroup_id"` + + NameCombineEnabled bool `json:"name_combine_enabled"` + NameCombineSuffix string `json:"name_combine_suffix"` + + XIappKey string `json:"x_iapp_key,omitempty"` + + No2GhzOui bool `json:"no2ghz_oui"` + WEPIdx int `json:"wep_idx,omitempty"` + UserGroupID string `json:"usergroup_id"` + DTIMMode string `json:"dtim_mode"` + DTIMNg int `json:"dtim_ng,omitempty"` + DTIMNa int `json:"dtim_na,omitempty"` + + MinrateNgEnabled bool `json:"minrate_ng_enabled"` + MinrateNgBeaconRateKbps int `json:"minrate_ng_beacon_rate_kbps"` + MinrateNgDataRateKbps int `json:"minrate_ng_data_rate_kbps"` + MinrateNgAdvertisingRates bool `json:"minrate_ng_advertising_rates"` + MinrateNgCckRatesEnabled bool `json:"minrate_ng_cck_rates_enabled"` + MinrateNaEnabled bool `json:"minrate_na_enabled"` + MinrateNaAdvertisingRates bool `json:"minrate_na_advertising_rates"` + MinrateNaDataRateKbps int `json:"minrate_na_data_rate_kbps"` + MinrateNgMgmtRateKbps int `json:"minrate_ng_mgmt_rate_kbps"` + MinrateNaMgmtRateKbps int `json:"minrate_na_mgmt_rate_kbps"` + MinrateNaBeaconRateKbps int `json:"minrate_na_beacon_rate_kbps"` + + MACFilterEnabled bool `json:"mac_filter_enabled"` + MACFilterPolicy string `json:"mac_filter_policy,omitempty"` + MACFilterList []string `json:"mac_filter_list,omitempty"` + + BroadcastFilterEnabled bool `json:"bc_filter_enabled"` + BroadcastFilterList []string `json:"bc_filter_list,omitempty"` + + GroupRekey int `json:"group_rekey"` + + VLANEnabled bool `json:"vlan_enabled"` + VLAN string `json:"vlan"` + + Schedule []string `json:"schedule"` +} + +func (c *Client) ListWLAN(site string) ([]WLAN, error) { + var respBody struct { + Meta meta `json:"meta"` + Data []WLAN `json:"data"` + } + + err := c.do("GET", fmt.Sprintf("s/%s/rest/wlanconf", site), nil, &respBody) + if err != nil { + return nil, err + } + + return respBody.Data, nil +} + +func (c *Client) GetWLAN(site, id string) (*WLAN, error) { + var respBody struct { + Meta meta `json:"meta"` + Data []WLAN `json:"data"` + } + + err := c.do("GET", fmt.Sprintf("s/%s/rest/wlanconf/%s", site, id), nil, &respBody) + if err != nil { + return nil, err + } + + if len(respBody.Data) != 1 { + return nil, &NotFoundError{} + } + + d := respBody.Data[0] + return &d, nil +} + +func (c *Client) DeleteWLAN(site, id string) error { + err := c.do("DELETE", fmt.Sprintf("s/%s/rest/wlanconf/%s", site, id), struct{}{}, nil) + if err != nil { + return err + } + return nil +} + +func (c *Client) CreateWLAN(site string, d *WLAN) (*WLAN, error) { + if d.Schedule == nil { + d.Schedule = []string{} + } + + var respBody struct { + Meta meta `json:"meta"` + Data []WLAN `json:"data"` + } + + err := c.do("POST", fmt.Sprintf("s/%s/rest/wlanconf", site), d, &respBody) + if err != nil { + return nil, err + } + + if len(respBody.Data) != 1 { + return nil, &NotFoundError{} + } + + new := respBody.Data[0] + + return &new, nil +} diff --git a/unifi/wlan_group.go b/unifi/wlan_group.go new file mode 100644 index 0000000..8529211 --- /dev/null +++ b/unifi/wlan_group.go @@ -0,0 +1,34 @@ +package unifi + +import ( + "fmt" +) + +type WLANGroup struct { + ID string `json:"_id"` + SiteID string `json:"site_id"` + Name string `json:"name"` + + Hidden bool `json:"attr_hidden,omitempty"` + HiddenID string `json:"attr_hidden_id,omitempty"` + NoDelete bool `json:"attr_no_delete,omitempty"` + NoEdit bool `json:"attr_no_edit,omitempty"` + + BSupported bool `json:"b_supported"` + LoadBalanceEnabled bool `json:"loadbalance_enabled"` + PMFMode string `json:"pmf_mode"` +} + +func (c *Client) ListWLANGroup(site string) ([]WLANGroup, error) { + var respBody struct { + Meta meta `json:"meta"` + Data []WLANGroup `json:"data"` + } + + err := c.do("GET", fmt.Sprintf("s/%s/rest/wlangroup", site), nil, &respBody) + if err != nil { + return nil, err + } + + return respBody.Data, nil +}