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:
Paul Tyng
2023-03-08 02:02:34 -05:00
committed by GitHub
parent ef6aa3bd9b
commit 0401ae6913
4 changed files with 160 additions and 49 deletions

View File

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

View File

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

View File

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

View File

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