diff --git a/apstra/blueprint/datacenter_virtual_network.go b/apstra/blueprint/datacenter_virtual_network.go index 43c7aab7..9cf0cb8c 100644 --- a/apstra/blueprint/datacenter_virtual_network.go +++ b/apstra/blueprint/datacenter_virtual_network.go @@ -12,6 +12,7 @@ import ( apiversions "github.com/Juniper/terraform-provider-apstra/apstra/api_versions" "github.com/Juniper/terraform-provider-apstra/apstra/compatibility" "github.com/Juniper/terraform-provider-apstra/apstra/constants" + "github.com/Juniper/terraform-provider-apstra/apstra/design" apstraregexp "github.com/Juniper/terraform-provider-apstra/apstra/regexp" "github.com/Juniper/terraform-provider-apstra/apstra/utils" apstravalidator "github.com/Juniper/terraform-provider-apstra/apstra/validator" @@ -42,6 +43,7 @@ type DatacenterVirtualNetwork struct { Vni types.Int64 `tfsdk:"vni"` HadPriorVniConfig types.Bool `tfsdk:"had_prior_vni_config"` ReserveVlan types.Bool `tfsdk:"reserve_vlan"` + ReservedVlanId types.Int64 `tfsdk:"reserved_vlan_id"` Bindings types.Map `tfsdk:"bindings"` DhcpServiceEnabled types.Bool `tfsdk:"dhcp_service_enabled"` IPv4ConnectivityEnabled types.Bool `tfsdk:"ipv4_connectivity_enabled"` @@ -107,6 +109,10 @@ func (o DatacenterVirtualNetwork) DataSourceAttributes() map[string]dataSourceSc "which the Virtual Network has not yet been deployed.", enum.VnTypeVxlan), Computed: true, }, + "reserved_vlan_id": dataSourceSchema.Int64Attribute{ + MarkdownDescription: "Reserved VLAN ID, if any.", + Computed: true, + }, "bindings": dataSourceSchema.MapNestedAttribute{ MarkdownDescription: "Details availability of the virtual network on leaf and access switches", Computed: true, @@ -212,10 +218,12 @@ func (o DatacenterVirtualNetwork) DataSourceFilterAttributes() map[string]dataSo Computed: true, }, "reserve_vlan": dataSourceSchema.BoolAttribute{ - MarkdownDescription: fmt.Sprintf("For use only with `%s` type Virtual networks when all `bindings` "+ - "use the same VLAN ID. This option reserves the VLAN fabric-wide, even on switches to "+ - "which the Virtual Network has not yet been deployed.", enum.VnTypeVxlan), - Optional: true, + MarkdownDescription: "Selects only virtual networks with the *Reserve across blueprint* box checked.", + Optional: true, + }, + "reserved_vlan_id": dataSourceSchema.StringAttribute{ + MarkdownDescription: "Selects only virtual networks with the *Reserve across blueprint* box checked and this value selected.", + Optional: true, }, "bindings": dataSourceSchema.MapNestedAttribute{ MarkdownDescription: "Not applicable in filter context. Ignore.", @@ -384,6 +392,21 @@ func (o DatacenterVirtualNetwork) ResourceAttributes() map[string]resourceSchema false, ), ), + apstravalidator.AlsoRequiresNOf(1, + path.MatchRoot("bindings"), + path.MatchRoot("reserved_vlan_id"), + ), + }, + }, + "reserved_vlan_id": resourceSchema.Int64Attribute{ + MarkdownDescription: "Used to specify the reserved VLAN ID without specifying any *bindings*.", + Optional: true, + Computed: true, + Validators: []validator.Int64{ + apstravalidator.ForbiddenWhenValueIs(path.MatchRoot("reserve_vlan"), types.BoolNull()), + apstravalidator.ForbiddenWhenValueIs(path.MatchRoot("reserve_vlan"), types.BoolValue(false)), + int64validator.ConflictsWith(path.MatchRoot("bindings")), + int64validator.Between(design.VlanMin, design.VlanMax), }, }, "bindings": resourceSchema.MapNestedAttribute{ @@ -601,7 +624,11 @@ func (o *DatacenterVirtualNetwork) Request(ctx context.Context, diags *diag.Diag var reservedVlanId *apstra.Vlan if o.ReserveVlan.ValueBool() { - reservedVlanId = vnBindings[0].VlanId + if !o.ReservedVlanId.IsNull() { + reservedVlanId = utils.ToPtr(apstra.Vlan(o.ReservedVlanId.ValueInt64())) + } else { + reservedVlanId = vnBindings[0].VlanId + } } var ipv4Subnet, ipv6Subnet *net.IPNet @@ -687,6 +714,11 @@ func (o *DatacenterVirtualNetwork) LoadApiData(ctx context.Context, in *apstra.V o.IPv4ConnectivityEnabled = types.BoolValue(in.Ipv4Enabled) o.IPv6ConnectivityEnabled = types.BoolValue(in.Ipv6Enabled) o.ReserveVlan = types.BoolValue(in.ReservedVlanId != nil) + if in.ReservedVlanId == nil { + o.ReservedVlanId = types.Int64Null() + } else { + o.ReservedVlanId = types.Int64Value(int64(*in.ReservedVlanId)) + } if in.Ipv4Subnet == nil { o.IPv4Subnet = types.StringNull() } else { @@ -753,6 +785,13 @@ func (o *DatacenterVirtualNetwork) Query(resultName string) apstra.QEQuery { }) } + if !o.ReservedVlanId.IsNull() { + nodeAttributes = append(nodeAttributes, apstra.QEEAttribute{ + Key: "reserved_vlan_id", + Value: apstra.QEIntVal(o.ReservedVlanId.ValueInt64()), + }) + } + if !o.IPv4ConnectivityEnabled.IsNull() { nodeAttributes = append(nodeAttributes, apstra.QEEAttribute{ Key: "ipv4_enabled", @@ -925,7 +964,7 @@ func (o DatacenterVirtualNetwork) ValidateConfigBindingsReservation(ctx context. } if binding.VlanId.IsNull() { invalidConfigDueToNullVlan = true - continue // todo: should this be 'break' instead? + break } reservedVlanIds[binding.VlanId.ValueInt64()] = struct{}{} } diff --git a/apstra/resource_datacenter_virtual_network_test.go b/apstra/resource_datacenter_virtual_network_integration_test.go similarity index 93% rename from apstra/resource_datacenter_virtual_network_test.go rename to apstra/resource_datacenter_virtual_network_integration_test.go index 48b9102d..8d1c4f0a 100644 --- a/apstra/resource_datacenter_virtual_network_test.go +++ b/apstra/resource_datacenter_virtual_network_integration_test.go @@ -26,14 +26,16 @@ import ( const ( resourceDatacenterVirtualNetworkTemplateHCL = ` resource %q %q { - blueprint_id = %q - name = %q - description = %s - type = %s - vni = %s - routing_zone_id = %s - l3_mtu = %s - bindings = %s + blueprint_id = %q + name = %q + description = %s + type = %s + vni = %s + routing_zone_id = %s + l3_mtu = %s + bindings = %s + reserve_vlan = %s + reserved_vlan_id = %s } ` resourceDatacenterVirtualNetworkTemplateBindingHCL = ` @@ -45,14 +47,16 @@ resource %q %q { ) type resourceDatacenterVirtualNetworkTemplate struct { - blueprintId apstra.ObjectId - name string - description string - vnType string - vni *int - routingZoneId apstra.ObjectId - l3Mtu *int - bindings []resourceDatacenterVirtualNetworkTemplateBinding + blueprintId apstra.ObjectId + name string + description string + vnType string + vni *int + routingZoneId apstra.ObjectId + l3Mtu *int + bindings []resourceDatacenterVirtualNetworkTemplateBinding + reserveVlan *bool + reservedVlanId *int } func (o resourceDatacenterVirtualNetworkTemplate) render(rType, rName string) string { @@ -78,6 +82,8 @@ func (o resourceDatacenterVirtualNetworkTemplate) render(rType, rName string) st stringOrNull(o.routingZoneId.String()), intPtrOrNull(o.l3Mtu), bindings.String(), + boolPtrOrNull(o.reserveVlan), + intPtrOrNull(o.reservedVlanId), ) } @@ -597,6 +603,31 @@ func TestAccDatacenterVirtualNetwork(t *testing.T) { }, }, }, + "no_bindings_reserved_vlan_id": { + apiVersionConstraints: compatibility.VnEmptyBindingsOk, + steps: []testStep{ + { + config: resourceDatacenterVirtualNetworkTemplate{ + blueprintId: bp.Id(), + name: acctest.RandString(6), + vnType: enum.VnTypeVxlan.String(), + routingZoneId: szId, + reserveVlan: utils.ToPtr(true), + reservedVlanId: utils.ToPtr(1100), + }, + }, + { + config: resourceDatacenterVirtualNetworkTemplate{ + blueprintId: bp.Id(), + name: acctest.RandString(6), + vnType: enum.VnTypeVxlan.String(), + routingZoneId: szId, + reserveVlan: utils.ToPtr(true), + reservedVlanId: utils.ToPtr(1101), + }, + }, + }, + }, } resourceType := tfapstra.ResourceName(ctx, &tfapstra.ResourceDatacenterVirtualNetwork) diff --git a/apstra/validator/also_requires_n_of.go b/apstra/validator/also_requires_n_of.go index 170d701e..4128e874 100644 --- a/apstra/validator/also_requires_n_of.go +++ b/apstra/validator/also_requires_n_of.go @@ -43,6 +43,10 @@ func (o AlsoRequiresNOfValidator) MarkdownDescription(_ context.Context) string } func (o AlsoRequiresNOfValidator) Validate(ctx context.Context, req AlsoRequiresNOfValidatorRequest, resp *AlsoRequiresNOfValidatorResponse) { + if req.ConfigValue.IsNull() { + return + } + expressions := req.PathExpression.MergeExpressions(o.PathExpressions...) for i := range expressions { expressions[i] = expressions[i].Resolve() diff --git a/docs/data-sources/datacenter_virtual_network.md b/docs/data-sources/datacenter_virtual_network.md index 15cc91ee..f2db626e 100644 --- a/docs/data-sources/datacenter_virtual_network.md +++ b/docs/data-sources/datacenter_virtual_network.md @@ -58,6 +58,7 @@ locals { - `ipv6_virtual_gateway_enabled` (Boolean) Controls and indicates whether the IPv6 gateway within the Virtual Network is enabled. - `l3_mtu` (Number) L3 MTU used by the L3 switch interfaces participating in the Virtual Network. Requires Apstra 4.2 or later. - `reserve_vlan` (Boolean) For use only with `vxlan` type Virtual networks when all `bindings` use the same VLAN ID. This option reserves the VLAN fabric-wide, even on switches to which the Virtual Network has not yet been deployed. +- `reserved_vlan_id` (Number) Reserved VLAN ID, if any. - `routing_zone_id` (String) Routing Zone ID (only applies when `type == vxlan` - `type` (String) Virtual Network Type - `vni` (Number) EVPN Virtual Network ID to be associated with this Virtual Network. diff --git a/docs/data-sources/datacenter_virtual_networks.md b/docs/data-sources/datacenter_virtual_networks.md index cbd20f5e..ea617529 100644 --- a/docs/data-sources/datacenter_virtual_networks.md +++ b/docs/data-sources/datacenter_virtual_networks.md @@ -78,7 +78,8 @@ Optional: - `ipv6_virtual_gateway_enabled` (Boolean) Controls and indicates whether the IPv6 gateway within the Virtual Network is enabled. - `l3_mtu` (Number) L3 MTU used by the L3 switch interfaces participating in the Virtual Network. Requires Apstra 4.2 or later. - `name` (String) Virtual Network Name -- `reserve_vlan` (Boolean) For use only with `vxlan` type Virtual networks when all `bindings` use the same VLAN ID. This option reserves the VLAN fabric-wide, even on switches to which the Virtual Network has not yet been deployed. +- `reserve_vlan` (Boolean) Selects only virtual networks with the *Reserve across blueprint* box checked. +- `reserved_vlan_id` (String) Selects only virtual networks with the *Reserve across blueprint* box checked and this value selected. - `routing_zone_id` (String) Routing Zone ID (required when `type == vxlan`) - `type` (String) Virtual Network Type - `vni` (Number) EVPN Virtual Network ID to be associated with this Virtual Network. @@ -114,7 +115,8 @@ Optional: - `ipv6_virtual_gateway_enabled` (Boolean) Controls and indicates whether the IPv6 gateway within the Virtual Network is enabled. - `l3_mtu` (Number) L3 MTU used by the L3 switch interfaces participating in the Virtual Network. Requires Apstra 4.2 or later. - `name` (String) Virtual Network Name -- `reserve_vlan` (Boolean) For use only with `vxlan` type Virtual networks when all `bindings` use the same VLAN ID. This option reserves the VLAN fabric-wide, even on switches to which the Virtual Network has not yet been deployed. +- `reserve_vlan` (Boolean) Selects only virtual networks with the *Reserve across blueprint* box checked. +- `reserved_vlan_id` (String) Selects only virtual networks with the *Reserve across blueprint* box checked and this value selected. - `routing_zone_id` (String) Routing Zone ID (required when `type == vxlan`) - `type` (String) Virtual Network Type - `vni` (Number) EVPN Virtual Network ID to be associated with this Virtual Network. diff --git a/docs/resources/datacenter_virtual_network.md b/docs/resources/datacenter_virtual_network.md index 9db63a9e..fc18f438 100644 --- a/docs/resources/datacenter_virtual_network.md +++ b/docs/resources/datacenter_virtual_network.md @@ -70,6 +70,7 @@ resource "apstra_datacenter_virtual_network" "test" { - `ipv6_virtual_gateway_enabled` (Boolean) Controls and indicates whether the IPv6 gateway within the Virtual Network is enabled. Requires `ipv6_connectivity_enabled` to be `true` - `l3_mtu` (Number) L3 MTU used by the L3 switch interfaces participating in the Virtual Network. Must be an even number between 1280 and 9216. Requires Apstra 4.2.0 or later. - `reserve_vlan` (Boolean) For use only with `vxlan` type Virtual networks when all `bindings` use the same VLAN ID. This option reserves the VLAN fabric-wide, even on switches to which the Virtual Network has not yet been deployed. The only accepted values is `true`. +- `reserved_vlan_id` (Number) Used to specify the reserved VLAN ID without specifying any *bindings*. - `routing_zone_id` (String) Routing Zone ID (required when `type == vxlan` - `type` (String) Virtual Network Type - `vni` (Number) EVPN Virtual Network ID to be associated with this Virtual Network. When omitted, Apstra chooses a VNI from the Resource Pool [allocated](../resources/datacenter_resource_pool_allocation) to role `vni_virtual_network_ids`.