diff --git a/docs/resources/device.md b/docs/resources/device.md index b834dca..8d9f926 100644 --- a/docs/resources/device.md +++ b/docs/resources/device.md @@ -51,6 +51,13 @@ resource "unifi_device" "us_24_poe" { name = "disabled" port_profile_id = data.unifi_port_profile.disabled.id } + + # port aggregation for ports 11 and 12 + port_override { + number = 11 + op_mode = "aggregate" + aggregate_num_ports = 2 + } } ``` @@ -80,7 +87,9 @@ Required: Optional: +- `aggregate_num_ports` (Number) Number of ports in the aggregate. - `name` (String) Human-readable name of the port. +- `op_mode` (String) Operating mode of the port, valid values are `switch`, `mirror`, and `aggregate`. - `port_profile_id` (String) ID of the Port Profile used on this port. diff --git a/examples/resources/unifi_device/resource.tf b/examples/resources/unifi_device/resource.tf index 16d64a6..5417c23 100644 --- a/examples/resources/unifi_device/resource.tf +++ b/examples/resources/unifi_device/resource.tf @@ -33,4 +33,11 @@ resource "unifi_device" "us_24_poe" { name = "disabled" port_profile_id = data.unifi_port_profile.disabled.id } + + # port aggregation for ports 11 and 12 + port_override { + number = 11 + op_mode = "aggregate" + aggregate_num_ports = 2 + } } diff --git a/internal/provider/resource_device.go b/internal/provider/resource_device.go index e2d85b6..7f31ee3 100644 --- a/internal/provider/resource_device.go +++ b/internal/provider/resource_device.go @@ -84,6 +84,19 @@ func resourceDevice() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "op_mode": { + Description: "Operating mode of the port, valid values are `switch`, `mirror`, and `aggregate`.", + Type: schema.TypeString, + Optional: true, + Default: "switch", + ValidateFunc: validation.StringInSlice([]string{"switch", "mirror", "aggregate"}, false), + }, + "aggregate_num_ports": { + Description: "Number of ports in the aggregate.", + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(2, 8), + }, }, }, }, @@ -326,22 +339,28 @@ func setFromPortOverrides(pos []unifi.DevicePortOverrides) ([]map[string]interfa } 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) + profileID := data["port_profile_id"].(string) + opMode := data["op_mode"].(string) + aggregateNumPorts := data["aggregate_num_ports"].(int) + return unifi.DevicePortOverrides{ - PortIDX: idx, - Name: name, - PortProfileID: profile_id, + PortIDX: idx, + Name: name, + PortProfileID: profileID, + OpMode: opMode, + AggregateNumPorts: aggregateNumPorts, }, 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, + "number": po.PortIDX, + "name": po.Name, + "port_profile_id": po.PortProfileID, + "op_mode": po.OpMode, + "aggregate_num_ports": po.AggregateNumPorts, }, nil } diff --git a/internal/provider/resource_device_test.go b/internal/provider/resource_device_test.go index ce8fb3e..465e3c7 100644 --- a/internal/provider/resource_device_test.go +++ b/internal/provider/resource_device_test.go @@ -19,7 +19,7 @@ var ( devicePool mapset.Set[*unifi.Device] = mapset.NewSet[*unifi.Device]() ) -func allocateDevice(t *testing.T) (string, func()) { +func allocateDevice(t *testing.T) (*unifi.Device, func()) { ctx := context.Background() deviceInit.Do(func() { @@ -49,6 +49,11 @@ func allocateDevice(t *testing.T) (string, func()) { continue } + // Only switches with these chipsets support both port mirroring ang aggregation. + if !(isBroadcomSwitch(device) || isMicrosemiSwitch(device) || isNephosSwitch(device)) { + continue + } + d := device if ok := devicePool.Add(&d); !ok { return resource.NonRetryableError(fmt.Errorf("Failed to add device to pool")) @@ -86,7 +91,77 @@ func allocateDevice(t *testing.T) (string, func()) { } } - return device.MAC, unallocate + return device, unallocate +} + +func isBroadcomSwitch(device unifi.Device) bool { + if device.Type != "usw" { + return false + } + + switch device.Model { + // US-8 variants + case "US8", "US8P60", "US8P150", "S28150": + return true + + // US-16 variants + case "US16P150", "S216150", "USXG": + return true + + // US-24 variants + case "US24", "US24P250", "S224250", "US24P500", "S224500", "US24PL2": + return true + + // US-48 variants + case "US48", "US48P500", "S248500", "US48P750", "S248750", "US48PL2": + return true + + // USW-Pro + case "US24PRO", "US24PRO2", "US48PRO", "US48PRO2", "USAGGPRO": + return true + + // USW-Enterprise + case "US624P", "US648P", "USXG24": + return true + + // US-XG-6PoE + case "US6XG150": + return true + } + + return false +} + +func isMicrosemiSwitch(device unifi.Device) bool { + if device.Type != "usw" { + return false + } + + switch device.Model { + // US-8 variants + case "USC8", "USC8P60", "USC8P150": + return true + + // USW-Industrial + case "USC8P450": + return true + } + + return false +} + +func isNephosSwitch(device unifi.Device) bool { + if device.Type != "usw" { + return false + } + + switch device.Model { + // USW-Leaf + case "UDC48X6": + return true + } + + return false } func preCheckDeviceExists(t *testing.T, site, mac string) { @@ -115,7 +190,7 @@ func TestAccDevice_switch_basic(t *testing.T) { resourceName := "unifi_device.test" site := "default" - switchMAC, unallocateDevice := allocateDevice(t) + device, unallocateDevice := allocateDevice(t) defer unallocateDevice() importStateVerifyIgnore := []string{"allow_adoption", "forget_on_destroy"} @@ -123,17 +198,17 @@ func TestAccDevice_switch_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { preCheck(t) - preCheckDeviceExists(t, site, switchMAC) + preCheckDeviceExists(t, site, device.MAC) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckDeviceDestroy, Steps: []resource.TestStep{ { - Config: testAccDeviceConfig(switchMAC), + Config: testAccDeviceConfig(device.MAC), Check: resource.ComposeTestCheckFunc( testAccCheckDeviceExists(resourceName), resource.TestCheckResourceAttr(resourceName, "site", site), - resource.TestCheckResourceAttr(resourceName, "mac", switchMAC), + resource.TestCheckResourceAttr(resourceName, "mac", device.MAC), resource.TestCheckResourceAttr(resourceName, "name", ""), ), }, @@ -150,13 +225,13 @@ func TestAccDevice_switch_basic(t *testing.T) { { ResourceName: resourceName, ImportState: true, - ImportStateId: switchMAC, + ImportStateId: device.MAC, ImportStateVerify: true, ImportStateVerifyIgnore: importStateVerifyIgnore, }, { - Config: testAccDeviceConfig_withName(switchMAC, "Test Switch"), + Config: testAccDeviceConfig_withName(device.MAC, "Test Switch"), Check: resource.ComposeTestCheckFunc( testAccCheckDeviceExists(resourceName), resource.TestCheckResourceAttr(resourceName, "name", "Test Switch"), @@ -170,52 +245,43 @@ func TestAccDevice_switch_portOverrides(t *testing.T) { resourceName := "unifi_device.test" site := "default" - switchMAC, unallocateDevice := allocateDevice(t) + device, unallocateDevice := allocateDevice(t) defer unallocateDevice() resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { preCheck(t) - preCheckDeviceExists(t, site, switchMAC) + preCheckDeviceExists(t, site, device.MAC) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckDeviceDestroy, Steps: []resource.TestStep{ { - Config: testAccDeviceConfig_withPortOverrides(switchMAC), + Config: testAccDeviceConfig_withPortOverrides(device.MAC), Check: resource.ComposeTestCheckFunc( testAccCheckDeviceExists(resourceName), - resource.TestCheckResourceAttr(resourceName, "port_override.#", "2"), - resource.TestCheckResourceAttr(resourceName, "port_override.0.number", "1"), - resource.TestCheckResourceAttr(resourceName, "port_override.0.name", "Port 1"), - resource.TestCheckResourceAttr(resourceName, "port_override.1.number", "2"), - resource.TestCheckResourceAttr(resourceName, "port_override.1.name", "Port 2"), + resource.TestCheckResourceAttr(resourceName, "port_override.#", "3"), + + // TODO: Why are these out of order? + resource.TestCheckResourceAttr(resourceName, "port_override.0.number", "3"), + resource.TestCheckResourceAttr(resourceName, "port_override.0.name", ""), + resource.TestCheckResourceAttr(resourceName, "port_override.0.port_profile_id", ""), + resource.TestCheckResourceAttr(resourceName, "port_override.0.op_mode", "aggregate"), + resource.TestCheckResourceAttr(resourceName, "port_override.0.aggregate_num_ports", "2"), + + resource.TestCheckResourceAttr(resourceName, "port_override.1.number", "1"), + resource.TestCheckResourceAttr(resourceName, "port_override.1.name", "Port 1"), + resource.TestCheckResourceAttr(resourceName, "port_override.1.port_profile_id", ""), + resource.TestCheckResourceAttr(resourceName, "port_override.1.op_mode", "switch"), + + resource.TestCheckResourceAttr(resourceName, "port_override.2.number", "2"), + resource.TestCheckResourceAttr(resourceName, "port_override.2.name", "Port 2"), + //resource.TestCheckResourceAttr(resourceName, "port_override.2.port_profile_id", ""), + resource.TestCheckResourceAttr(resourceName, "port_override.2.op_mode", "switch"), ), }, - }, - }) -} - -func TestAccDevice_remove_portOverrides(t *testing.T) { - resourceName := "unifi_device.test" - site := "default" - - switchMAC, unallocateDevice := allocateDevice(t) - defer unallocateDevice() - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { - preCheck(t) - preCheckDeviceExists(t, site, switchMAC) - }, - ProviderFactories: providerFactories, - CheckDestroy: testAccCheckDeviceDestroy, - Steps: []resource.TestStep{ { - Config: testAccDeviceConfig_withPortOverrides(switchMAC), - }, - { - Config: testAccDeviceConfig(switchMAC), + Config: testAccDeviceConfig(device.MAC), Check: resource.ComposeTestCheckFunc( testAccCheckDeviceExists(resourceName), resource.TestCheckResourceAttr(resourceName, "port_override.#", "0"), @@ -250,6 +316,8 @@ resource "unifi_device" "test" { func testAccDeviceConfig_withPortOverrides(mac string) string { return fmt.Sprintf(` +data "unifi_port_profile" "all" {} + resource "unifi_device" "test" { mac = %q @@ -259,8 +327,16 @@ resource "unifi_device" "test" { } port_override { - number = 2 - name = "Port 2" + number = 2 + name = "Port 2" + port_profile_id = data.unifi_port_profile.all.id + op_mode = "switch" + } + + port_override { + number = 3 + op_mode = "aggregate" + aggregate_num_ports = 2 } } `, mac)