Add support for port aggregation (#182)
* Add support for port aggregation Fixes #142 * Return `*unifi.Device` from `allocateDevice` * Merge `TestAccDevice_switch_portOverrides` and `TestAccDevice_remove_portOverrides` * Add tests Note that only switches with a Broadcom, Microsemi or Nephos chipset support both port mirroring and port aggregation. ```java package com.ubnt.data; public class Device extends X implements Sanitizable { // ... public int getMaxMirrorSession() { int n = 0; if (this.getModel() == Model.\u00f8\u00f50000) { n = 2; } else if (this.isBroadcomSwitch() || this.isMicrosemiSwitch() || this.isMediaTekSwitch() || this.isNephosSwitch()) { n = 1; } return this.thisforObject().getInt("max_mirror_sessions", n); } public int getMaxAggregation() { int n = 0; if (this.isBroadcomSwitch() || this.isMicrosemiSwitch() || this.isNephosSwitch()) { n = 6; } return this.thisforObject().getInt("max_aggregate_sessions", n); } // ... public boolean isBroadcomSwitch() { final Model model = this.getModel(); return model.getChipset().typeOf(Chipset.\u00f400000) && model.getType() == DeviceType.if; } public boolean isMicrosemiSwitch() { final Model model = this.getModel(); return model.getChipset().typeOf(Chipset.o00000) && model.getType() == DeviceType.if; } public boolean isMediaTekSwitch() { final Model model = this.getModel(); return model.getChipset().typeOf(Chipset.\u00d3O0000) && model.getType() == DeviceType.if; } public boolean isNephosSwitch() { final Model model = this.getModel(); return model.getChipset().typeOf(Chipset.\u00d5O0000) && model.getType() == DeviceType.if; } // ... } ``` To extract the list of models that use one of these chipsets I used the following script (executed as `java --class-path path/to/ace.jar main.java`): ```java import com.ubnt.data.Model; class UniFiModels { public static void main(String[] args) { /* for (Model model : Model.values()) { System.out.printf( "Model = %s\nSKU = %s\nType = %s\nFeatures = %s\nChipset = %s\nSysId = %s\nPortNum = %s\n\n", model, model.getSku(), model.getType(), model.getFeatures(), model.getChipset(), model.getSysId(), model.getPortNum()); } */ for (Model model : Model.values()) { System.out.printf("%s: %s (%s)\n", model.getChipset(), model, model.getSku()); } } } ``` --------- Co-authored-by: Joshua Spence <josh@spence.com.au>
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user