0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-08-25 12:55:41 +00:00

add other zone types

Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
Pavel Boldyrev 2025-07-18 15:05:32 -04:00
parent 21059a6aa4
commit bf19edb12d
No known key found for this signature in database
GPG Key ID: 637146A2A6804C59
13 changed files with 1125 additions and 59 deletions

View File

@ -0,0 +1,191 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package zone
import (
"context"
"errors"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones"
"github.com/bpg/terraform-provider-proxmox/proxmox/helpers/ptr"
)
var (
_ resource.ResourceWithConfigure = &EVPNResource{}
_ resource.ResourceWithImportState = &EVPNResource{}
)
type EVPNResource struct {
client *zones.Client
}
func NewEVPNResource() resource.Resource {
return &EVPNResource{}
}
func (r *EVPNResource) Metadata(
_ context.Context,
req resource.MetadataRequest,
resp *resource.MetadataResponse,
) {
resp.TypeName = req.ProviderTypeName + "_sdn_zone_evpn"
}
func (r *EVPNResource) Configure(
_ context.Context,
req resource.ConfigureRequest,
resp *resource.ConfigureResponse,
) {
if req.ProviderData == nil {
return
}
cfg, ok := req.ProviderData.(config.Resource)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf(
"Expected config.Resource, got: %T",
req.ProviderData,
),
)
return
}
r.client = cfg.Client.Cluster().SDNZones()
}
func (r *EVPNResource) Create(
ctx context.Context,
req resource.CreateRequest,
resp *resource.CreateResponse,
) {
var plan evpnModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
reqData := plan.toAPIRequestBody(ctx, &resp.Diagnostics)
reqData.Type = ptr.Ptr(zones.TypeEVPN)
if err := r.client.CreateZone(ctx, reqData); err != nil {
resp.Diagnostics.AddError(
"Unable to Create SDN EVPN Zone",
err.Error(),
)
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *EVPNResource) Read(
ctx context.Context,
req resource.ReadRequest,
resp *resource.ReadResponse,
) {
var state evpnModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
zone, err := r.client.GetZone(ctx, state.ID.ValueString())
if err != nil {
if errors.Is(err, api.ErrResourceDoesNotExist) {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError(
"Unable to Read SDN EVPN Zone",
err.Error(),
)
return
}
readModel := &evpnModel{}
readModel.importFromAPI(zone.ID, zone, &resp.Diagnostics)
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
}
func (r *EVPNResource) Update(
ctx context.Context,
req resource.UpdateRequest,
resp *resource.UpdateResponse,
) {
var plan evpnModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
reqData := plan.toAPIRequestBody(ctx, &resp.Diagnostics)
if err := r.client.UpdateZone(ctx, reqData); err != nil {
resp.Diagnostics.AddError(
"Unable to Update SDN EVPN Zone",
err.Error(),
)
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *EVPNResource) Delete(
ctx context.Context,
req resource.DeleteRequest,
resp *resource.DeleteResponse,
) {
var state evpnModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if err := r.client.DeleteZone(ctx, state.ID.ValueString()); err != nil &&
!errors.Is(err, api.ErrResourceDoesNotExist) {
resp.Diagnostics.AddError(
"Unable to Delete SDN EVPN Zone",
err.Error(),
)
}
}
func (r *EVPNResource) ImportState(
ctx context.Context,
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
zone, err := r.client.GetZone(ctx, req.ID)
if err != nil {
if errors.Is(err, api.ErrResourceDoesNotExist) {
resp.Diagnostics.AddError(fmt.Sprintf("Zone %s does not exist", req.ID), err.Error())
return
}
resp.Diagnostics.AddError(fmt.Sprintf("Unable to Import SDN EVPN Zone %s", req.ID), err.Error())
return
}
readModel := &evpnModel{}
readModel.importFromAPI(zone.ID, zone, &resp.Diagnostics)
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
}

View File

@ -14,6 +14,8 @@ import (
"github.com/bpg/terraform-provider-proxmox/fwprovider/types/stringset" "github.com/bpg/terraform-provider-proxmox/fwprovider/types/stringset"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones" "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones"
proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types"
) )
type baseModel struct { type baseModel struct {
@ -51,18 +53,6 @@ func (m *baseModel) importFromAPI(name string, data *zones.ZoneData, diags *diag
m.MTU = types.Int64PointerValue(data.MTU) m.MTU = types.Int64PointerValue(data.MTU)
m.Nodes = stringset.NewValueString(data.Nodes, diags, stringset.WithSeparator(",")) m.Nodes = stringset.NewValueString(data.Nodes, diags, stringset.WithSeparator(","))
m.ReverseDNS = types.StringPointerValue(data.ReverseDNS) m.ReverseDNS = types.StringPointerValue(data.ReverseDNS)
// m.Bridge = types.StringPointerValue(data.Bridge)
// m.ServiceVLAN = types.Int64PointerValue(data.ServiceVLAN)
// m.ServiceVLANProtocol = types.StringPointerValue(data.ServiceVLANProtocol)
// m.Peers = stringset.NewValueString(data.Peers, diags, comaSeparated)
// m.Controller = types.StringPointerValue(data.Controller)
// m.ExitNodes = stringset.NewValueString(data.ExitNodes, diags, comaSeparated)
// m.PrimaryExitNode = types.StringPointerValue(data.ExitNodesPrimary)
// m.RouteTargetImport = types.StringPointerValue(data.RouteTargetImport)
// m.VRFVXLANID = types.Int64PointerValue(data.VRFVXLANID)
// m.ExitNodesLocalRouting = types.BoolPointerValue(ptrConversion.Int64ToBoolPtr(data.ExitNodesLocalRouting))
// m.AdvertiseSubnets = types.BoolPointerValue(ptrConversion.Int64ToBoolPtr(data.AdvertiseSubnets))
// m.DisableARPNDSuppression = types.BoolPointerValue(ptrConversion.Int64ToBoolPtr(data.DisableARPNDSuppression))
} }
func (m *baseModel) toAPIRequestBody(ctx context.Context, diags *diag.Diagnostics) *zones.ZoneRequestData { func (m *baseModel) toAPIRequestBody(ctx context.Context, diags *diag.Diagnostics) *zones.ZoneRequestData {
@ -76,18 +66,113 @@ func (m *baseModel) toAPIRequestBody(ctx context.Context, diags *diag.Diagnostic
data.DNSZone = m.DNSZone.ValueStringPointer() data.DNSZone = m.DNSZone.ValueStringPointer()
data.Nodes = m.Nodes.ValueStringPointer(ctx, diags, stringset.WithSeparator(",")) data.Nodes = m.Nodes.ValueStringPointer(ctx, diags, stringset.WithSeparator(","))
data.MTU = m.MTU.ValueInt64Pointer() data.MTU = m.MTU.ValueInt64Pointer()
// data.Bridge = m.Bridge.ValueStringPointer()
// data.ServiceVLAN = m.ServiceVLAN.ValueInt64Pointer() return data
// data.ServiceVLANProtocol = m.ServiceVLANProtocol.ValueStringPointer() }
// data.Peers = m.Peers.ValueStringPointer(ctx, diags, comaSeparated)
// data.Controller = m.Controller.ValueStringPointer() type simpleModel struct {
// data.ExitNodes = m.ExitNodes.ValueStringPointer(ctx, diags, comaSeparated) baseModel
// data.ExitNodesPrimary = m.PrimaryExitNode.ValueStringPointer() }
// data.RouteTargetImport = m.RouteTargetImport.ValueStringPointer()
// data.VRFVXLANID = m.VRFVXLANID.ValueInt64Pointer() type vlanModel struct {
// data.ExitNodesLocalRouting = ptrConversion.BoolToInt64Ptr(m.ExitNodesLocalRouting.ValueBoolPointer()) baseModel
// data.AdvertiseSubnets = ptrConversion.BoolToInt64Ptr(m.AdvertiseSubnets.ValueBoolPointer())
// data.DisableARPNDSuppression = ptrConversion.BoolToInt64Ptr(m.DisableARPNDSuppression.ValueBoolPointer()) Bridge types.String `tfsdk:"bridge"`
}
func (m *vlanModel) importFromAPI(name string, data *zones.ZoneData, diags *diag.Diagnostics) {
m.baseModel.importFromAPI(name, data, diags)
m.Bridge = types.StringPointerValue(data.Bridge)
}
func (m *vlanModel) toAPIRequestBody(ctx context.Context, diags *diag.Diagnostics) *zones.ZoneRequestData {
data := m.baseModel.toAPIRequestBody(ctx, diags)
data.Bridge = m.Bridge.ValueStringPointer()
return data
}
type qinqModel struct {
vlanModel
ServiceVLAN types.Int64 `tfsdk:"service_vlan"`
ServiceVLANProtocol types.String `tfsdk:"service_vlan_protocol"`
}
func (m *qinqModel) importFromAPI(name string, data *zones.ZoneData, diags *diag.Diagnostics) {
m.vlanModel.importFromAPI(name, data, diags)
m.ServiceVLAN = types.Int64PointerValue(data.ServiceVLAN)
m.ServiceVLANProtocol = types.StringPointerValue(data.ServiceVLANProtocol)
}
func (m *qinqModel) toAPIRequestBody(ctx context.Context, diags *diag.Diagnostics) *zones.ZoneRequestData {
data := m.vlanModel.toAPIRequestBody(ctx, diags)
data.ServiceVLAN = m.ServiceVLAN.ValueInt64Pointer()
data.ServiceVLANProtocol = m.ServiceVLANProtocol.ValueStringPointer()
return data
}
type vxlanModel struct {
baseModel
Peers stringset.Value `tfsdk:"peers"`
}
func (m *vxlanModel) importFromAPI(name string, data *zones.ZoneData, diags *diag.Diagnostics) {
m.baseModel.importFromAPI(name, data, diags)
m.Peers = stringset.NewValueString(data.Peers, diags, stringset.WithSeparator(","))
}
func (m *vxlanModel) toAPIRequestBody(ctx context.Context, diags *diag.Diagnostics) *zones.ZoneRequestData {
data := m.baseModel.toAPIRequestBody(ctx, diags)
data.Peers = m.Peers.ValueStringPointer(ctx, diags, stringset.WithSeparator(","))
return data
}
type evpnModel struct {
baseModel
AdvertiseSubnets types.Bool `tfsdk:"advertise_subnets"`
Controller types.String `tfsdk:"controller"`
DisableARPNDSuppression types.Bool `tfsdk:"disable_arp_nd_suppression"`
ExitNodes stringset.Value `tfsdk:"exit_nodes"`
ExitNodesLocalRouting types.Bool `tfsdk:"exit_nodes_local_routing"`
PrimaryExitNode types.String `tfsdk:"primary_exit_node"`
RouteTargetImport types.String `tfsdk:"rt_import"`
VRFVXLANID types.Int64 `tfsdk:"vrf_vxlan"`
}
func (m *evpnModel) importFromAPI(name string, data *zones.ZoneData, diags *diag.Diagnostics) {
m.baseModel.importFromAPI(name, data, diags)
m.AdvertiseSubnets = types.BoolPointerValue(data.AdvertiseSubnets.PointerBool())
m.Controller = types.StringPointerValue(data.Controller)
m.DisableARPNDSuppression = types.BoolPointerValue(data.DisableARPNDSuppression.PointerBool())
m.ExitNodes = stringset.NewValueString(data.ExitNodes, diags, stringset.WithSeparator(","))
m.ExitNodesLocalRouting = types.BoolPointerValue(data.ExitNodesLocalRouting.PointerBool())
m.PrimaryExitNode = types.StringPointerValue(data.ExitNodesPrimary)
m.RouteTargetImport = types.StringPointerValue(data.RouteTargetImport)
m.VRFVXLANID = types.Int64PointerValue(data.VRFVXLANID)
}
func (m *evpnModel) toAPIRequestBody(ctx context.Context, diags *diag.Diagnostics) *zones.ZoneRequestData {
data := m.baseModel.toAPIRequestBody(ctx, diags)
data.AdvertiseSubnets = proxmoxtypes.CustomBoolPtr(m.AdvertiseSubnets.ValueBoolPointer())
data.Controller = m.Controller.ValueStringPointer()
data.DisableARPNDSuppression = proxmoxtypes.CustomBoolPtr(m.DisableARPNDSuppression.ValueBoolPointer())
data.ExitNodes = m.ExitNodes.ValueStringPointer(ctx, diags, stringset.WithSeparator(","))
data.ExitNodesLocalRouting = proxmoxtypes.CustomBoolPtr(m.ExitNodesLocalRouting.ValueBoolPointer())
data.ExitNodesPrimary = m.PrimaryExitNode.ValueStringPointer()
data.RouteTargetImport = m.RouteTargetImport.ValueStringPointer()
data.VRFVXLANID = m.VRFVXLANID.ValueInt64Pointer()
return data return data
} }

View File

@ -0,0 +1,191 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package zone
import (
"context"
"errors"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones"
"github.com/bpg/terraform-provider-proxmox/proxmox/helpers/ptr"
)
var (
_ resource.ResourceWithConfigure = &QinQResource{}
_ resource.ResourceWithImportState = &QinQResource{}
)
type QinQResource struct {
client *zones.Client
}
func NewQinQResource() resource.Resource {
return &QinQResource{}
}
func (r *QinQResource) Metadata(
_ context.Context,
req resource.MetadataRequest,
resp *resource.MetadataResponse,
) {
resp.TypeName = req.ProviderTypeName + "_sdn_zone_qinq"
}
func (r *QinQResource) Configure(
_ context.Context,
req resource.ConfigureRequest,
resp *resource.ConfigureResponse,
) {
if req.ProviderData == nil {
return
}
cfg, ok := req.ProviderData.(config.Resource)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf(
"Expected config.Resource, got: %T",
req.ProviderData,
),
)
return
}
r.client = cfg.Client.Cluster().SDNZones()
}
func (r *QinQResource) Create(
ctx context.Context,
req resource.CreateRequest,
resp *resource.CreateResponse,
) {
var plan qinqModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
reqData := plan.toAPIRequestBody(ctx, &resp.Diagnostics)
reqData.Type = ptr.Ptr(zones.TypeQinQ)
if err := r.client.CreateZone(ctx, reqData); err != nil {
resp.Diagnostics.AddError(
"Unable to Create SDN QinQ Zone",
err.Error(),
)
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *QinQResource) Read(
ctx context.Context,
req resource.ReadRequest,
resp *resource.ReadResponse,
) {
var state qinqModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
zone, err := r.client.GetZone(ctx, state.ID.ValueString())
if err != nil {
if errors.Is(err, api.ErrResourceDoesNotExist) {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError(
"Unable to Read SDN QinQ Zone",
err.Error(),
)
return
}
readModel := &qinqModel{}
readModel.importFromAPI(zone.ID, zone, &resp.Diagnostics)
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
}
func (r *QinQResource) Update(
ctx context.Context,
req resource.UpdateRequest,
resp *resource.UpdateResponse,
) {
var plan qinqModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
reqData := plan.toAPIRequestBody(ctx, &resp.Diagnostics)
if err := r.client.UpdateZone(ctx, reqData); err != nil {
resp.Diagnostics.AddError(
"Unable to Update SDN QinQ Zone",
err.Error(),
)
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *QinQResource) Delete(
ctx context.Context,
req resource.DeleteRequest,
resp *resource.DeleteResponse,
) {
var state qinqModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if err := r.client.DeleteZone(ctx, state.ID.ValueString()); err != nil &&
!errors.Is(err, api.ErrResourceDoesNotExist) {
resp.Diagnostics.AddError(
"Unable to Delete SDN QinQ Zone",
err.Error(),
)
}
}
func (r *QinQResource) ImportState(
ctx context.Context,
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
zone, err := r.client.GetZone(ctx, req.ID)
if err != nil {
if errors.Is(err, api.ErrResourceDoesNotExist) {
resp.Diagnostics.AddError(fmt.Sprintf("Zone %s does not exist", req.ID), err.Error())
return
}
resp.Diagnostics.AddError(fmt.Sprintf("Unable to Import SDN QinQ Zone %s", req.ID), err.Error())
return
}
readModel := &qinqModel{}
readModel.importFromAPI(zone.ID, zone, &resp.Diagnostics)
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
}

View File

@ -0,0 +1,65 @@
//go:build acceptance || all
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package zone_test
import (
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/bpg/terraform-provider-proxmox/fwprovider/test"
)
func TestAccResourceSDNZoneQinQ(t *testing.T) {
t.Parallel()
te := test.InitEnvironment(t)
tests := []struct {
name string
steps []resource.TestStep
}{
{"create and update QinQ zone", []resource.TestStep{{
Config: te.RenderConfig(`
resource "proxmox_virtual_environment_sdn_zone_qinq" "zone_qinq" {
id = "zoneQ"
nodes = ["pve"]
mtu = 1496
bridge = "vmbr0"
service_vlan = 100
service_vlan_protocol = "802.1ad"
}
`),
}, {
Config: te.RenderConfig(`
resource "proxmox_virtual_environment_sdn_zone_qinq" "zone_qinq" {
id = "zoneQ"
nodes = ["pve"]
mtu = 1495
bridge = "vmbr0"
service_vlan = 200
service_vlan_protocol = "802.1q"
}
`),
ResourceName: "proxmox_virtual_environment_sdn_zone_qinq.zone_qinq",
ImportStateId: "zoneQ",
ImportState: true,
ImportStateVerify: true,
}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: te.AccProviders,
Steps: tt.steps,
})
})
}
}

View File

@ -22,16 +22,16 @@ import (
"github.com/bpg/terraform-provider-proxmox/fwprovider/types/stringset" "github.com/bpg/terraform-provider-proxmox/fwprovider/types/stringset"
) )
func commonAttributes(base ...map[string]schema.Attribute) map[string]schema.Attribute { func baseAttributesWith(extraAttributes ...map[string]schema.Attribute) map[string]schema.Attribute {
if len(base) > 1 { if len(extraAttributes) > 1 {
panic("commonAttributes expects at most one base map") panic("baseAttributesWith expects at most one extraAttributes map")
} }
if len(base) == 0 { if len(extraAttributes) == 0 {
base = append(base, make(map[string]schema.Attribute)) extraAttributes = append(extraAttributes, make(map[string]schema.Attribute))
} }
maps.Copy(base[0], map[string]schema.Attribute{ maps.Copy(extraAttributes[0], map[string]schema.Attribute{
"dns": schema.StringAttribute{ "dns": schema.StringAttribute{
Optional: true, Optional: true,
Description: "DNS API server address.", Description: "DNS API server address.",
@ -72,7 +72,7 @@ func commonAttributes(base ...map[string]schema.Attribute) map[string]schema.Att
}, },
}) })
return base[0] return extraAttributes[0]
} }
func (r *SimpleResource) Schema( func (r *SimpleResource) Schema(
@ -85,11 +85,11 @@ func (r *SimpleResource) Schema(
MarkdownDescription: "Simple Zone in Proxmox SDN. It will create an isolated VNet bridge. " + MarkdownDescription: "Simple Zone in Proxmox SDN. It will create an isolated VNet bridge. " +
"This bridge is not linked to a physical interface, and VM traffic is only local on each the node. " + "This bridge is not linked to a physical interface, and VM traffic is only local on each the node. " +
"It can be used in NAT or routed setups.", "It can be used in NAT or routed setups.",
Attributes: commonAttributes(), Attributes: baseAttributesWith(),
} }
} }
func (r *VLAN) Schema( func (r *VLANResource) Schema(
_ context.Context, _ context.Context,
_ resource.SchemaRequest, _ resource.SchemaRequest,
resp *resource.SchemaResponse, resp *resource.SchemaResponse,
@ -99,7 +99,7 @@ func (r *VLAN) Schema(
MarkdownDescription: "VLAN Zone in Proxmox SDN. It uses an existing local Linux or OVS bridge to connect to the " + MarkdownDescription: "VLAN Zone in Proxmox SDN. It uses an existing local Linux or OVS bridge to connect to the " +
"node's physical interface. It uses VLAN tagging defined in the VNet to isolate the network segments. " + "node's physical interface. It uses VLAN tagging defined in the VNet to isolate the network segments. " +
"This allows connectivity of VMs between different nodes.", "This allows connectivity of VMs between different nodes.",
Attributes: commonAttributes(map[string]schema.Attribute{ Attributes: baseAttributesWith(map[string]schema.Attribute{
"bridge": schema.StringAttribute{ "bridge": schema.StringAttribute{
Description: "Bridge interface for VLAN.", Description: "Bridge interface for VLAN.",
MarkdownDescription: "The local bridge or OVS switch, already configured on _each_ node that allows " + MarkdownDescription: "The local bridge or OVS switch, already configured on _each_ node that allows " +
@ -110,7 +110,7 @@ func (r *VLAN) Schema(
} }
} }
func (r *QinQ) Schema( func (r *QinQResource) Schema(
_ context.Context, _ context.Context,
_ resource.SchemaRequest, _ resource.SchemaRequest,
resp *resource.SchemaResponse, resp *resource.SchemaResponse,
@ -122,7 +122,7 @@ func (r *QinQ) Schema(
"VLAN tag is defined by the VNet. Your physical network switches must support stacked VLANs for this " + "VLAN tag is defined by the VNet. Your physical network switches must support stacked VLANs for this " +
"configuration. Due to the double stacking of tags, you need 4 more bytes for QinQ VLANs. " + "configuration. Due to the double stacking of tags, you need 4 more bytes for QinQ VLANs. " +
"For example, you must reduce the MTU to 1496 if you physical interface MTU is 1500.", "For example, you must reduce the MTU to 1496 if you physical interface MTU is 1500.",
Attributes: commonAttributes(map[string]schema.Attribute{ Attributes: baseAttributesWith(map[string]schema.Attribute{
"bridge": schema.StringAttribute{ "bridge": schema.StringAttribute{
Description: "A local, VLAN-aware bridge that is already configured on each local node", Description: "A local, VLAN-aware bridge that is already configured on each local node",
Optional: true, Optional: true,
@ -145,7 +145,7 @@ func (r *QinQ) Schema(
} }
} }
func (r *VXLAN) Schema( func (r *VXLANResource) Schema(
_ context.Context, _ context.Context,
_ resource.SchemaRequest, _ resource.SchemaRequest,
resp *resource.SchemaResponse, resp *resource.SchemaResponse,
@ -157,7 +157,7 @@ func (r *VXLAN) Schema(
"destination port 4789. You have to configure the underlay network yourself to enable UDP connectivity " + "destination port 4789. You have to configure the underlay network yourself to enable UDP connectivity " +
"between all peers. Because VXLAN encapsulation uses 50 bytes, the MTU needs to be 50 bytes lower than the " + "between all peers. Because VXLAN encapsulation uses 50 bytes, the MTU needs to be 50 bytes lower than the " +
"outgoing physical interface.", "outgoing physical interface.",
Attributes: commonAttributes(map[string]schema.Attribute{ Attributes: baseAttributesWith(map[string]schema.Attribute{
"peers": stringset.ResourceAttribute( "peers": stringset.ResourceAttribute(
"A list of IP addresses of each node in the VXLAN zone.", "A list of IP addresses of each node in the VXLAN zone.",
"A list of IP addresses of each node in the VXLAN zone. "+ "A list of IP addresses of each node in the VXLAN zone. "+
@ -168,7 +168,7 @@ func (r *VXLAN) Schema(
} }
} }
func (r *EVPN) Schema( func (r *EVPNResource) Schema(
_ context.Context, _ context.Context,
_ resource.SchemaRequest, _ resource.SchemaRequest,
resp *resource.SchemaResponse, resp *resource.SchemaResponse,
@ -177,7 +177,7 @@ func (r *EVPN) Schema(
Description: "EVPN Zone in Proxmox SDN.", Description: "EVPN Zone in Proxmox SDN.",
MarkdownDescription: "EVPN Zone in Proxmox SDN. The EVPN zone creates a routable Layer 3 network, capable of " + MarkdownDescription: "EVPN Zone in Proxmox SDN. The EVPN zone creates a routable Layer 3 network, capable of " +
"spanning across multiple clusters.", "spanning across multiple clusters.",
Attributes: commonAttributes(map[string]schema.Attribute{ Attributes: baseAttributesWith(map[string]schema.Attribute{
"advertise_subnets": schema.BoolAttribute{ "advertise_subnets": schema.BoolAttribute{
Optional: true, Optional: true,
Description: "Enable subnet advertisement for EVPN.", Description: "Enable subnet advertisement for EVPN.",
@ -202,6 +202,12 @@ func (r *EVPN) Schema(
"rt_import": schema.StringAttribute{ "rt_import": schema.StringAttribute{
Optional: true, Optional: true,
Description: "Route target import for EVPN.", Description: "Route target import for EVPN.",
Validators: []validator.String{
stringvalidator.RegexMatches(
regexp.MustCompile(`^(\d+):(\d+)$`),
"must be in the format '<ASN>:<number>' (e.g., '65000:65000')",
),
},
}, },
"vrf_vxlan": schema.Int64Attribute{ "vrf_vxlan": schema.Int64Attribute{
Optional: true, Optional: true,

View File

@ -28,12 +28,10 @@ type SimpleResource struct {
client *zones.Client client *zones.Client
} }
// NewSimpleResource creates a new instance of the Simple resource.
func NewSimpleResource() resource.Resource { func NewSimpleResource() resource.Resource {
return &SimpleResource{} return &SimpleResource{}
} }
// Metadata defines the name of the resource.
func (r *SimpleResource) Metadata( func (r *SimpleResource) Metadata(
_ context.Context, _ context.Context,
req resource.MetadataRequest, req resource.MetadataRequest,
@ -72,7 +70,7 @@ func (r *SimpleResource) Create(
req resource.CreateRequest, req resource.CreateRequest,
resp *resource.CreateResponse, resp *resource.CreateResponse,
) { ) {
var plan baseModel var plan simpleModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
@ -85,7 +83,7 @@ func (r *SimpleResource) Create(
if err := r.client.CreateZone(ctx, reqData); err != nil { if err := r.client.CreateZone(ctx, reqData); err != nil {
resp.Diagnostics.AddError( resp.Diagnostics.AddError(
"Unable to Create SDN Zone", "Unable to Create SDN SimpleZone",
err.Error(), err.Error(),
) )
@ -100,7 +98,7 @@ func (r *SimpleResource) Read(
req resource.ReadRequest, req resource.ReadRequest,
resp *resource.ReadResponse, resp *resource.ReadResponse,
) { ) {
var state baseModel var state simpleModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...) resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
@ -116,7 +114,7 @@ func (r *SimpleResource) Read(
} }
resp.Diagnostics.AddError( resp.Diagnostics.AddError(
"Unable to Read SDN Zone", "Unable to Read SDN SimpleZone",
err.Error(), err.Error(),
) )
@ -133,7 +131,7 @@ func (r *SimpleResource) Update(
req resource.UpdateRequest, req resource.UpdateRequest,
resp *resource.UpdateResponse, resp *resource.UpdateResponse,
) { ) {
var plan baseModel var plan simpleModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
@ -145,7 +143,7 @@ func (r *SimpleResource) Update(
if err := r.client.UpdateZone(ctx, reqData); err != nil { if err := r.client.UpdateZone(ctx, reqData); err != nil {
resp.Diagnostics.AddError( resp.Diagnostics.AddError(
"Unable to Update SDN Zone", "Unable to Update SDN Simple Zone",
err.Error(), err.Error(),
) )
@ -160,7 +158,7 @@ func (r *SimpleResource) Delete(
req resource.DeleteRequest, req resource.DeleteRequest,
resp *resource.DeleteResponse, resp *resource.DeleteResponse,
) { ) {
var state baseModel var state simpleModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...) resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
@ -171,7 +169,7 @@ func (r *SimpleResource) Delete(
if err := r.client.DeleteZone(ctx, state.ID.ValueString()); err != nil && if err := r.client.DeleteZone(ctx, state.ID.ValueString()); err != nil &&
!errors.Is(err, api.ErrResourceDoesNotExist) { !errors.Is(err, api.ErrResourceDoesNotExist) {
resp.Diagnostics.AddError( resp.Diagnostics.AddError(
"Unable to Delete SDN Zone", "Unable to Delete SDN Simple Zone",
err.Error(), err.Error(),
) )
} }
@ -182,4 +180,20 @@ func (r *SimpleResource) ImportState(
req resource.ImportStateRequest, req resource.ImportStateRequest,
resp *resource.ImportStateResponse, resp *resource.ImportStateResponse,
) { ) {
zone, err := r.client.GetZone(ctx, req.ID)
if err != nil {
if errors.Is(err, api.ErrResourceDoesNotExist) {
resp.Diagnostics.AddError(fmt.Sprintf("Zone %s does not exist", req.ID), err.Error())
return
}
resp.Diagnostics.AddError(fmt.Sprintf("Unable to Import SDN Simple Zone %s", req.ID), err.Error())
return
}
readModel := &simpleModel{}
readModel.importFromAPI(zone.ID, zone, &resp.Diagnostics)
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
} }

View File

@ -17,6 +17,8 @@ import (
) )
func TestAccResourceSDNZoneSimple(t *testing.T) { func TestAccResourceSDNZoneSimple(t *testing.T) {
t.Parallel()
te := test.InitEnvironment(t) te := test.InitEnvironment(t)
tests := []struct { tests := []struct {
@ -39,6 +41,10 @@ func TestAccResourceSDNZoneSimple(t *testing.T) {
mtu = 1495 mtu = 1495
} }
`), `),
ResourceName: "proxmox_virtual_environment_sdn_zone_simple.zone_simple",
ImportStateId: "zoneS",
ImportState: true,
ImportStateVerify: true,
}}}, }}},
} }

View File

@ -0,0 +1,191 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package zone
import (
"context"
"errors"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones"
"github.com/bpg/terraform-provider-proxmox/proxmox/helpers/ptr"
)
var (
_ resource.ResourceWithConfigure = &VLANResource{}
_ resource.ResourceWithImportState = &VLANResource{}
)
type VLANResource struct {
client *zones.Client
}
func NewVLANResource() resource.Resource {
return &VLANResource{}
}
func (r *VLANResource) Metadata(
_ context.Context,
req resource.MetadataRequest,
resp *resource.MetadataResponse,
) {
resp.TypeName = req.ProviderTypeName + "_sdn_zone_vlan"
}
func (r *VLANResource) Configure(
_ context.Context,
req resource.ConfigureRequest,
resp *resource.ConfigureResponse,
) {
if req.ProviderData == nil {
return
}
cfg, ok := req.ProviderData.(config.Resource)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf(
"Expected config.Resource, got: %T",
req.ProviderData,
),
)
return
}
r.client = cfg.Client.Cluster().SDNZones()
}
func (r *VLANResource) Create(
ctx context.Context,
req resource.CreateRequest,
resp *resource.CreateResponse,
) {
var plan vlanModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
reqData := plan.toAPIRequestBody(ctx, &resp.Diagnostics)
reqData.Type = ptr.Ptr(zones.TypeVLAN)
if err := r.client.CreateZone(ctx, reqData); err != nil {
resp.Diagnostics.AddError(
"Unable to Create SDN VLAN Zone",
err.Error(),
)
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *VLANResource) Read(
ctx context.Context,
req resource.ReadRequest,
resp *resource.ReadResponse,
) {
var state vlanModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
zone, err := r.client.GetZone(ctx, state.ID.ValueString())
if err != nil {
if errors.Is(err, api.ErrResourceDoesNotExist) {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError(
"Unable to Read SDN VLAN Zone",
err.Error(),
)
return
}
readModel := &vlanModel{}
readModel.importFromAPI(zone.ID, zone, &resp.Diagnostics)
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
}
func (r *VLANResource) Update(
ctx context.Context,
req resource.UpdateRequest,
resp *resource.UpdateResponse,
) {
var plan vlanModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
reqData := plan.toAPIRequestBody(ctx, &resp.Diagnostics)
if err := r.client.UpdateZone(ctx, reqData); err != nil {
resp.Diagnostics.AddError(
"Unable to Update SDN VLAN Zone",
err.Error(),
)
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *VLANResource) Delete(
ctx context.Context,
req resource.DeleteRequest,
resp *resource.DeleteResponse,
) {
var state vlanModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if err := r.client.DeleteZone(ctx, state.ID.ValueString()); err != nil &&
!errors.Is(err, api.ErrResourceDoesNotExist) {
resp.Diagnostics.AddError(
"Unable to Delete SDN VLAN Zone",
err.Error(),
)
}
}
func (r *VLANResource) ImportState(
ctx context.Context,
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
zone, err := r.client.GetZone(ctx, req.ID)
if err != nil {
if errors.Is(err, api.ErrResourceDoesNotExist) {
resp.Diagnostics.AddError(fmt.Sprintf("Zone %s does not exist", req.ID), err.Error())
return
}
resp.Diagnostics.AddError(fmt.Sprintf("Unable to Import SDN VLAN Zone %s", req.ID), err.Error())
return
}
readModel := &vlanModel{}
readModel.importFromAPI(zone.ID, zone, &resp.Diagnostics)
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
}

View File

@ -0,0 +1,61 @@
//go:build acceptance || all
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package zone_test
import (
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/bpg/terraform-provider-proxmox/fwprovider/test"
)
func TestAccResourceSDNZoneVLAN(t *testing.T) {
t.Parallel()
te := test.InitEnvironment(t)
tests := []struct {
name string
steps []resource.TestStep
}{
{"create and update VLAN zone", []resource.TestStep{{
Config: te.RenderConfig(`
resource "proxmox_virtual_environment_sdn_zone_vlan" "zone_vlan" {
id = "zoneV"
nodes = ["pve"]
mtu = 1496
bridge = "vmbr0"
}
`),
}, {
Config: te.RenderConfig(`
resource "proxmox_virtual_environment_sdn_zone_vlan" "zone_vlan" {
id = "zoneV"
nodes = ["pve"]
mtu = 1495
bridge = "vmbr0"
}
`),
ResourceName: "proxmox_virtual_environment_sdn_zone_vlan.zone_vlan",
ImportStateId: "zoneV",
ImportState: true,
ImportStateVerify: true,
}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: te.AccProviders,
Steps: tt.steps,
})
})
}
}

View File

@ -0,0 +1,191 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package zone
import (
"context"
"errors"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones"
"github.com/bpg/terraform-provider-proxmox/proxmox/helpers/ptr"
)
var (
_ resource.ResourceWithConfigure = &VXLANResource{}
_ resource.ResourceWithImportState = &VXLANResource{}
)
type VXLANResource struct {
client *zones.Client
}
func NewVXLANResource() resource.Resource {
return &VXLANResource{}
}
func (r *VXLANResource) Metadata(
_ context.Context,
req resource.MetadataRequest,
resp *resource.MetadataResponse,
) {
resp.TypeName = req.ProviderTypeName + "_sdn_zone_vxlan"
}
func (r *VXLANResource) Configure(
_ context.Context,
req resource.ConfigureRequest,
resp *resource.ConfigureResponse,
) {
if req.ProviderData == nil {
return
}
cfg, ok := req.ProviderData.(config.Resource)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf(
"Expected config.Resource, got: %T",
req.ProviderData,
),
)
return
}
r.client = cfg.Client.Cluster().SDNZones()
}
func (r *VXLANResource) Create(
ctx context.Context,
req resource.CreateRequest,
resp *resource.CreateResponse,
) {
var plan vxlanModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
reqData := plan.toAPIRequestBody(ctx, &resp.Diagnostics)
reqData.Type = ptr.Ptr(zones.TypeVXLAN)
if err := r.client.CreateZone(ctx, reqData); err != nil {
resp.Diagnostics.AddError(
"Unable to Create SDN VXLAN Zone",
err.Error(),
)
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *VXLANResource) Read(
ctx context.Context,
req resource.ReadRequest,
resp *resource.ReadResponse,
) {
var state vxlanModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
zone, err := r.client.GetZone(ctx, state.ID.ValueString())
if err != nil {
if errors.Is(err, api.ErrResourceDoesNotExist) {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError(
"Unable to Read SDN VXLAN Zone",
err.Error(),
)
return
}
readModel := &vxlanModel{}
readModel.importFromAPI(zone.ID, zone, &resp.Diagnostics)
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
}
func (r *VXLANResource) Update(
ctx context.Context,
req resource.UpdateRequest,
resp *resource.UpdateResponse,
) {
var plan vxlanModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
reqData := plan.toAPIRequestBody(ctx, &resp.Diagnostics)
if err := r.client.UpdateZone(ctx, reqData); err != nil {
resp.Diagnostics.AddError(
"Unable to Update SDN VXLAN Zone",
err.Error(),
)
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *VXLANResource) Delete(
ctx context.Context,
req resource.DeleteRequest,
resp *resource.DeleteResponse,
) {
var state vxlanModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
if err := r.client.DeleteZone(ctx, state.ID.ValueString()); err != nil &&
!errors.Is(err, api.ErrResourceDoesNotExist) {
resp.Diagnostics.AddError(
"Unable to Delete SDN VXLAN Zone",
err.Error(),
)
}
}
func (r *VXLANResource) ImportState(
ctx context.Context,
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
zone, err := r.client.GetZone(ctx, req.ID)
if err != nil {
if errors.Is(err, api.ErrResourceDoesNotExist) {
resp.Diagnostics.AddError(fmt.Sprintf("Zone %s does not exist", req.ID), err.Error())
return
}
resp.Diagnostics.AddError(fmt.Sprintf("Unable to Import SDN VXLAN Zone %s", req.ID), err.Error())
return
}
readModel := &vxlanModel{}
readModel.importFromAPI(zone.ID, zone, &resp.Diagnostics)
resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...)
}

View File

@ -0,0 +1,61 @@
//go:build acceptance || all
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package zone_test
import (
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/bpg/terraform-provider-proxmox/fwprovider/test"
)
func TestAccResourceSDNZoneVXLAN(t *testing.T) {
t.Parallel()
te := test.InitEnvironment(t)
tests := []struct {
name string
steps []resource.TestStep
}{
{"create and update VXLAN zone", []resource.TestStep{{
Config: te.RenderConfig(`
resource "proxmox_virtual_environment_sdn_zone_vxlan" "zone_vxlan" {
id = "zoneX"
nodes = ["pve"]
mtu = 1450
peers = ["10.0.0.1", "10.0.0.2"]
}
`),
}, {
Config: te.RenderConfig(`
resource "proxmox_virtual_environment_sdn_zone_vxlan" "zone_vxlan" {
id = "zoneX"
nodes = ["pve"]
mtu = 1440
peers = ["10.0.0.3", "10.0.0.4"]
}
`),
ResourceName: "proxmox_virtual_environment_sdn_zone_vxlan.zone_vxlan",
ImportStateId: "zoneX",
ImportState: true,
ImportStateVerify: true,
}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: te.AccProviders,
Steps: tt.steps,
})
})
}
}

View File

@ -352,8 +352,10 @@ func validateResponseCode(res *http.Response) error {
errList = append(errList, split...) errList = append(errList, split...)
} }
if len(errList) > 0 {
msg = fmt.Sprintf("%s (%s)", msg, strings.Join(errList, " - ")) msg = fmt.Sprintf("%s (%s)", msg, strings.Join(errList, " - "))
} }
}
httpError := &HTTPError{ httpError := &HTTPError{
Code: res.StatusCode, Code: res.StatusCode,

View File

@ -6,6 +6,8 @@
package zones package zones
import "github.com/bpg/terraform-provider-proxmox/proxmox/types"
const ( const (
TypeSimple = "simple" TypeSimple = "simple"
TypeVLAN = "vlan" TypeVLAN = "vlan"
@ -39,9 +41,9 @@ type ZoneData struct {
VRFVXLANID *int64 `json:"vrf-vxlan,omitempty" url:"vrf-vxlan,omitempty"` VRFVXLANID *int64 `json:"vrf-vxlan,omitempty" url:"vrf-vxlan,omitempty"`
ExitNodes *string `json:"exitnodes,omitempty" url:"exitnodes,omitempty"` ExitNodes *string `json:"exitnodes,omitempty" url:"exitnodes,omitempty"`
ExitNodesPrimary *string `json:"exitnodes-primary,omitempty" url:"exitnodes-primary,omitempty"` ExitNodesPrimary *string `json:"exitnodes-primary,omitempty" url:"exitnodes-primary,omitempty"`
ExitNodesLocalRouting *int64 `json:"exitnodes-local-routing,omitempty" url:"exitnodes-local-routing,omitempty"` ExitNodesLocalRouting *types.CustomBool `json:"exitnodes-local-routing,omitempty" url:"exitnodes-local-routing,omitempty,int"`
AdvertiseSubnets *int64 `json:"advertise-subnets,omitempty" url:"advertise-subnets,omitempty"` AdvertiseSubnets *types.CustomBool `json:"advertise-subnets,omitempty" url:"advertise-subnets,omitempty,int"`
DisableARPNDSuppression *int64 `json:"disable-arp-nd-suppression,omitempty" url:"disable-arp-nd-suppression,omitempty"` DisableARPNDSuppression *types.CustomBool `json:"disable-arp-nd-suppression,omitempty" url:"disable-arp-nd-suppression,omitempty,int"`
RouteTargetImport *string `json:"rt-import,omitempty" url:"rt-import,omitempty"` RouteTargetImport *string `json:"rt-import,omitempty" url:"rt-import,omitempty"`
} }