From c1016a96eb366bde15b9ec6f385788d67194bc1d Mon Sep 17 00:00:00 2001 From: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Date: Mon, 14 Jul 2025 21:43:36 -0400 Subject: [PATCH 1/9] add zones api client Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- proxmox/cluster/sdn/zones/client.go | 23 +++++++ proxmox/cluster/sdn/zones/doc.go | 11 ++++ proxmox/cluster/sdn/zones/zones.go | 82 ++++++++++++++++++++++++ proxmox/cluster/sdn/zones/zones_types.go | 54 ++++++++++++++++ 4 files changed, 170 insertions(+) create mode 100644 proxmox/cluster/sdn/zones/client.go create mode 100644 proxmox/cluster/sdn/zones/doc.go create mode 100644 proxmox/cluster/sdn/zones/zones.go create mode 100644 proxmox/cluster/sdn/zones/zones_types.go diff --git a/proxmox/cluster/sdn/zones/client.go b/proxmox/cluster/sdn/zones/client.go new file mode 100644 index 000000000..1adb4c0ff --- /dev/null +++ b/proxmox/cluster/sdn/zones/client.go @@ -0,0 +1,23 @@ +/* + * 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 zones + +import ( + "fmt" + + "github.com/bpg/terraform-provider-proxmox/proxmox/api" +) + +// Client is a client for accessing the Proxmox SDN Zones API. +type Client struct { + api.Client +} + +// ExpandPath returns the API path for SDN zones. +func (c *Client) ExpandPath(path string) string { + return fmt.Sprintf("cluster/sdn/zones/%s", path) +} diff --git a/proxmox/cluster/sdn/zones/doc.go b/proxmox/cluster/sdn/zones/doc.go new file mode 100644 index 000000000..4afeea944 --- /dev/null +++ b/proxmox/cluster/sdn/zones/doc.go @@ -0,0 +1,11 @@ +/* + * 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 `zones` provides types and structures for managing Proxmox VE Software Defined Network (SDN) zones. +// SDN zones are logical network segments that can be configured with different network types including +// VLAN, QinQ, VXLAN, and EVPN. This package contains the data structures used for creating, updating, +// and managing SDN zones through the Proxmox API. +package zones diff --git a/proxmox/cluster/sdn/zones/zones.go b/proxmox/cluster/sdn/zones/zones.go new file mode 100644 index 000000000..23fd19cca --- /dev/null +++ b/proxmox/cluster/sdn/zones/zones.go @@ -0,0 +1,82 @@ +/* + * 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 zones + +import ( + "context" + "fmt" + "net/http" + + "github.com/bpg/terraform-provider-proxmox/proxmox/api" +) + +// GetZone retrieves a single SDN zone by ID. +func (c *Client) GetZone(ctx context.Context, id string) (*ZoneData, error) { + resBody := &ZoneResponseBody{} + + err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath(id), nil, resBody) + if err != nil { + return nil, fmt.Errorf("error reading SDN zone %s: %w", id, err) + } + + if resBody.Data == nil { + return nil, api.ErrNoDataObjectInResponse + } + + return resBody.Data, nil +} + +// GetZones lists all SDN zones. +func (c *Client) GetZones(ctx context.Context) ([]ZoneData, error) { + resBody := &ZonesResponseBody{} + + err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath(""), nil, resBody) + if err != nil { + return nil, fmt.Errorf("error listing SDN zones: %w", err) + } + + if resBody.Data == nil { + return nil, api.ErrNoDataObjectInResponse + } + + return *resBody.Data, nil +} + +// CreateZone creates a new SDN zone. +func (c *Client) CreateZone(ctx context.Context, data *ZoneRequestData) error { + err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath(""), data, nil) + if err != nil { + return fmt.Errorf("error creating SDN zone: %w", err) + } + + return nil +} + +// UpdateZone updates an existing SDN zone. +func (c *Client) UpdateZone(ctx context.Context, data *ZoneRequestData) error { + /* PVE API does not allow to pass "type" in PUT requests, this doesn't makes any sense + since other required params like port, server must still be there + while we could spawn another struct, let's just fix it silently */ + data.Type = nil + + err := c.DoRequest(ctx, http.MethodPut, c.ExpandPath(data.ID), data, nil) + if err != nil { + return fmt.Errorf("error updating SDN zone: %w", err) + } + + return nil +} + +// DeleteZone deletes an SDN zone by ID. +func (c *Client) DeleteZone(ctx context.Context, id string) error { + err := c.DoRequest(ctx, http.MethodDelete, c.ExpandPath(id), nil, nil) + if err != nil { + return fmt.Errorf("error deleting SDN zone: %w", err) + } + + return nil +} diff --git a/proxmox/cluster/sdn/zones/zones_types.go b/proxmox/cluster/sdn/zones/zones_types.go new file mode 100644 index 000000000..f463cd262 --- /dev/null +++ b/proxmox/cluster/sdn/zones/zones_types.go @@ -0,0 +1,54 @@ +/* + * 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 zones + +type ZoneData struct { + ID string `json:"zone,omitempty" url:"zone,omitempty"` + Type *string `json:"type,omitempty" url:"type,omitempty"` + IPAM *string `json:"ipam,omitempty" url:"ipam,omitempty"` + DNS *string `json:"dns,omitempty" url:"dns,omitempty"` + ReverseDNS *string `json:"reversedns,omitempty" url:"reversedns,omitempty"` + DNSZone *string `json:"dnszone,omitempty" url:"dnszone,omitempty"` + Nodes *string `json:"nodes,omitempty" url:"nodes,omitempty"` + MTU *int64 `json:"mtu,omitempty" url:"mtu,omitempty"` + + // VLAN. + Bridge *string `json:"bridge,omitempty" url:"bridge,omitempty"` + + // QinQ. + ServiceVLAN *int64 `json:"tag,omitempty" url:"tag,omitempty"` + ServiceVLANProtocol *string `json:"vlan-protocol,omitempty" url:"vlan-protocol,omitempty"` + + // VXLAN. + Peers *string `json:"peers,omitempty" url:"peers,omitempty"` + + // EVPN. + Controller *string `json:"controller,omitempty" url:"controller,omitempty"` + VRFVXLANID *int64 `json:"vrf-vxlan,omitempty" url:"vrf-vxlan,omitempty"` + ExitNodes *string `json:"exitnodes,omitempty" url:"exitnodes,omitempty"` + ExitNodesPrimary *string `json:"exitnodes-primary,omitempty" url:"exitnodes-primary,omitempty"` + ExitNodesLocalRouting *int64 `json:"exitnodes-local-routing,omitempty" url:"exitnodes-local-routing,omitempty"` + AdvertiseSubnets *int64 `json:"advertise-subnets,omitempty" url:"advertise-subnets,omitempty"` + DisableARPNDSuppression *int64 `json:"disable-arp-nd-suppression,omitempty" url:"disable-arp-nd-suppression,omitempty"` + RouteTargetImport *string `json:"rt-import,omitempty" url:"rt-import,omitempty"` +} + +// ZoneRequestData wraps a ZoneData struct with optional delete instructions. +type ZoneRequestData struct { + ZoneData + Delete []string `url:"delete,omitempty"` +} + +// ZoneResponseBody represents the response for a single zone. +type ZoneResponseBody struct { + Data *ZoneData `json:"data"` +} + +// ZonesResponseBody represents the response for a list of zones. +type ZonesResponseBody struct { + Data *[]ZoneData `json:"data"` +} From 21059a6aa4c7454f79b154f7412f296f99555238 Mon Sep 17 00:00:00 2001 From: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Date: Tue, 15 Jul 2025 22:58:44 -0400 Subject: [PATCH 2/9] add simple zone resource Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- .../virtual_environment_sdn_zone_simple.md | 30 +++ fwprovider/cluster/sdn/zone/resource_model.go | 93 ++++++++ .../cluster/sdn/zone/resource_schema.go | 213 ++++++++++++++++++ .../cluster/sdn/zone/resource_simple.go | 185 +++++++++++++++ .../cluster/sdn/zone/resource_simple_test.go | 53 +++++ fwprovider/provider.go | 9 + main.go | 1 + proxmox/cluster/client.go | 16 ++ proxmox/cluster/sdn/zones/zones_types.go | 11 +- testacc | 20 +- 10 files changed, 628 insertions(+), 3 deletions(-) create mode 100644 docs/resources/virtual_environment_sdn_zone_simple.md create mode 100644 fwprovider/cluster/sdn/zone/resource_model.go create mode 100644 fwprovider/cluster/sdn/zone/resource_schema.go create mode 100644 fwprovider/cluster/sdn/zone/resource_simple.go create mode 100644 fwprovider/cluster/sdn/zone/resource_simple_test.go diff --git a/docs/resources/virtual_environment_sdn_zone_simple.md b/docs/resources/virtual_environment_sdn_zone_simple.md new file mode 100644 index 000000000..ab922e64b --- /dev/null +++ b/docs/resources/virtual_environment_sdn_zone_simple.md @@ -0,0 +1,30 @@ +--- +layout: page +title: proxmox_virtual_environment_sdn_zone_simple +parent: Resources +subcategory: Virtual Environment +description: |- + 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. It can be used in NAT or routed setups. +--- + +# Resource: proxmox_virtual_environment_sdn_zone_simple + +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. It can be used in NAT or routed setups. + + + + +## Schema + +### Required + +- `id` (String) The unique identifier of the SDN zone. + +### Optional + +- `dns` (String) DNS API server address. +- `dns_zone` (String) DNS domain name. Used to register hostnames, such as `.`. The DNS zone must already exist on the DNS server. +- `ipam` (String) IP Address Management system. +- `mtu` (Number) MTU value for the zone. +- `nodes` (Set of String) Proxmox node names. +- `reverse_dns` (String) Reverse DNS API server address. diff --git a/fwprovider/cluster/sdn/zone/resource_model.go b/fwprovider/cluster/sdn/zone/resource_model.go new file mode 100644 index 000000000..4368b7fca --- /dev/null +++ b/fwprovider/cluster/sdn/zone/resource_model.go @@ -0,0 +1,93 @@ +/* + * 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" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/bpg/terraform-provider-proxmox/fwprovider/types/stringset" + "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones" +) + +type baseModel struct { + ID types.String `tfsdk:"id"` + IPAM types.String `tfsdk:"ipam"` + DNS types.String `tfsdk:"dns"` + ReverseDNS types.String `tfsdk:"reverse_dns"` + DNSZone types.String `tfsdk:"dns_zone"` + Nodes stringset.Value `tfsdk:"nodes"` + MTU types.Int64 `tfsdk:"mtu"` + // // VLAN. + // Bridge types.String `tfsdk:"bridge"` + // // QinQ. + // ServiceVLAN types.Int64 `tfsdk:"service_vlan"` + // ServiceVLANProtocol types.String `tfsdk:"service_vlan_protocol"` + // // VXLAN. + // Peers stringset.Value `tfsdk:"peers"` + // // EVPN. + // Controller types.String `tfsdk:"controller"` + // ExitNodes stringset.Value `tfsdk:"exit_nodes"` + // PrimaryExitNode types.String `tfsdk:"primary_exit_node"` + // RouteTargetImport types.String `tfsdk:"rt_import"` + // VRFVXLANID types.Int64 `tfsdk:"vrf_vxlan"` + // ExitNodesLocalRouting types.Bool `tfsdk:"exit_nodes_local_routing"` + // AdvertiseSubnets types.Bool `tfsdk:"advertise_subnets"` + // DisableARPNDSuppression types.Bool `tfsdk:"disable_arp_nd_suppression"` +} + +func (m *baseModel) importFromAPI(name string, data *zones.ZoneData, diags *diag.Diagnostics) { + m.ID = types.StringValue(name) + + m.DNS = types.StringPointerValue(data.DNS) + m.DNSZone = types.StringPointerValue(data.DNSZone) + m.IPAM = types.StringPointerValue(data.IPAM) + m.MTU = types.Int64PointerValue(data.MTU) + m.Nodes = stringset.NewValueString(data.Nodes, diags, stringset.WithSeparator(",")) + 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 { + data := &zones.ZoneRequestData{} + + data.ID = m.ID.ValueString() + + data.IPAM = m.IPAM.ValueStringPointer() + data.DNS = m.DNS.ValueStringPointer() + data.ReverseDNS = m.ReverseDNS.ValueStringPointer() + data.DNSZone = m.DNSZone.ValueStringPointer() + data.Nodes = m.Nodes.ValueStringPointer(ctx, diags, stringset.WithSeparator(",")) + data.MTU = m.MTU.ValueInt64Pointer() + // data.Bridge = m.Bridge.ValueStringPointer() + // data.ServiceVLAN = m.ServiceVLAN.ValueInt64Pointer() + // data.ServiceVLANProtocol = m.ServiceVLANProtocol.ValueStringPointer() + // data.Peers = m.Peers.ValueStringPointer(ctx, diags, comaSeparated) + // data.Controller = m.Controller.ValueStringPointer() + // data.ExitNodes = m.ExitNodes.ValueStringPointer(ctx, diags, comaSeparated) + // data.ExitNodesPrimary = m.PrimaryExitNode.ValueStringPointer() + // data.RouteTargetImport = m.RouteTargetImport.ValueStringPointer() + // data.VRFVXLANID = m.VRFVXLANID.ValueInt64Pointer() + // data.ExitNodesLocalRouting = ptrConversion.BoolToInt64Ptr(m.ExitNodesLocalRouting.ValueBoolPointer()) + // data.AdvertiseSubnets = ptrConversion.BoolToInt64Ptr(m.AdvertiseSubnets.ValueBoolPointer()) + // data.DisableARPNDSuppression = ptrConversion.BoolToInt64Ptr(m.DisableARPNDSuppression.ValueBoolPointer()) + + return data +} diff --git a/fwprovider/cluster/sdn/zone/resource_schema.go b/fwprovider/cluster/sdn/zone/resource_schema.go new file mode 100644 index 000000000..e26864ddb --- /dev/null +++ b/fwprovider/cluster/sdn/zone/resource_schema.go @@ -0,0 +1,213 @@ +/* + * 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" + "maps" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/bpg/terraform-provider-proxmox/fwprovider/types/stringset" +) + +func commonAttributes(base ...map[string]schema.Attribute) map[string]schema.Attribute { + if len(base) > 1 { + panic("commonAttributes expects at most one base map") + } + + if len(base) == 0 { + base = append(base, make(map[string]schema.Attribute)) + } + + maps.Copy(base[0], map[string]schema.Attribute{ + "dns": schema.StringAttribute{ + Optional: true, + Description: "DNS API server address.", + }, + "dns_zone": schema.StringAttribute{ + Optional: true, + Description: "DNS domain name. The DNS zone must already exist on the DNS server.", + MarkdownDescription: "DNS domain name. Used to register hostnames, such as `.`. " + + "The DNS zone must already exist on the DNS server.", + }, + "id": schema.StringAttribute{ + Description: "The unique identifier of the SDN zone.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + // https://github.com/proxmox/pve-network/blob/faaf96a8378a3e41065018562c09c3de0aa434f5/src/PVE/Network/SDN/Zones/Plugin.pm#L34 + stringvalidator.RegexMatches( + regexp.MustCompile(`^[A-Za-z][A-Za-z0-9]*[A-Za-z0-9]$`), + "must be a valid zone identifier", + ), + stringvalidator.LengthAtMost(8), + }, + }, + "ipam": schema.StringAttribute{ + Optional: true, + Description: "IP Address Management system.", + }, + "mtu": schema.Int64Attribute{ + Optional: true, + Description: "MTU value for the zone.", + }, + "nodes": stringset.ResourceAttribute("Proxmox node names.", ""), + "reverse_dns": schema.StringAttribute{ + Optional: true, + Description: "Reverse DNS API server address.", + }, + }) + + return base[0] +} + +func (r *SimpleResource) Schema( + _ context.Context, + _ resource.SchemaRequest, + resp *resource.SchemaResponse, +) { + resp.Schema = schema.Schema{ + Description: "Simple Zone in Proxmox SDN.", + 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. " + + "It can be used in NAT or routed setups.", + Attributes: commonAttributes(), + } +} + +func (r *VLAN) Schema( + _ context.Context, + _ resource.SchemaRequest, + resp *resource.SchemaResponse, +) { + resp.Schema = schema.Schema{ + Description: "VLAN Zone in Proxmox SDN.", + 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. " + + "This allows connectivity of VMs between different nodes.", + Attributes: commonAttributes(map[string]schema.Attribute{ + "bridge": schema.StringAttribute{ + Description: "Bridge interface for VLAN.", + MarkdownDescription: "The local bridge or OVS switch, already configured on _each_ node that allows " + + "node-to-node connection.", + Optional: true, + }, + }), + } +} + +func (r *QinQ) Schema( + _ context.Context, + _ resource.SchemaRequest, + resp *resource.SchemaResponse, +) { + resp.Schema = schema.Schema{ + Description: "QinQ Zone in Proxmox SDN.", + MarkdownDescription: "QinQ Zone in Proxmox SDN. QinQ also known as VLAN stacking, that uses multiple layers of " + + "VLAN tags for isolation. The QinQ zone defines the outer VLAN tag (the Service VLAN) whereas the inner " + + "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. " + + "For example, you must reduce the MTU to 1496 if you physical interface MTU is 1500.", + Attributes: commonAttributes(map[string]schema.Attribute{ + "bridge": schema.StringAttribute{ + Description: "A local, VLAN-aware bridge that is already configured on each local node", + Optional: true, + }, + "service_vlan": schema.Int64Attribute{ + Optional: true, + Description: "Service VLAN tag for QinQ.", + Validators: []validator.Int64{ + int64validator.Between(int64(1), int64(4094)), + }, + }, + "service_vlan_protocol": schema.StringAttribute{ + Optional: true, + Description: "Service VLAN protocol for QinQ.", + Validators: []validator.String{ + stringvalidator.OneOf("802.1ad", "802.1q"), + }, + }, + }), + } +} + +func (r *VXLAN) Schema( + _ context.Context, + _ resource.SchemaRequest, + resp *resource.SchemaResponse, +) { + resp.Schema = schema.Schema{ + Description: "VXLAN Zone in Proxmox SDN.", + MarkdownDescription: "VXLAN Zone in Proxmox SDN. It establishes a tunnel (overlay) on top of an existing network " + + "(underlay). This encapsulates layer 2 Ethernet frames within layer 4 UDP datagrams using the default " + + "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 " + + "outgoing physical interface.", + Attributes: commonAttributes(map[string]schema.Attribute{ + "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. "+ + "This can be external nodes reachable at this IP address. All nodes in the cluster need to be "+ + "mentioned here", + ), + }), + } +} + +func (r *EVPN) Schema( + _ context.Context, + _ resource.SchemaRequest, + resp *resource.SchemaResponse, +) { + resp.Schema = schema.Schema{ + Description: "EVPN Zone in Proxmox SDN.", + MarkdownDescription: "EVPN Zone in Proxmox SDN. The EVPN zone creates a routable Layer 3 network, capable of " + + "spanning across multiple clusters.", + Attributes: commonAttributes(map[string]schema.Attribute{ + "advertise_subnets": schema.BoolAttribute{ + Optional: true, + Description: "Enable subnet advertisement for EVPN.", + }, + "controller": schema.StringAttribute{ + Optional: true, + Description: "EVPN controller address.", + }, + "disable_arp_nd_suppression": schema.BoolAttribute{ + Optional: true, + Description: "Disable ARP/ND suppression for EVPN.", + }, + "exit_nodes": stringset.ResourceAttribute("List of exit nodes for EVPN.", ""), + "exit_nodes_local_routing": schema.BoolAttribute{ + Optional: true, + Description: "Enable local routing for EVPN exit nodes.", + }, + "primary_exit_node": schema.StringAttribute{ + Optional: true, + Description: "Primary exit node for EVPN.", + }, + "rt_import": schema.StringAttribute{ + Optional: true, + Description: "Route target import for EVPN.", + }, + "vrf_vxlan": schema.Int64Attribute{ + Optional: true, + Description: "VRF VXLAN-ID used for dedicated routing interconnect between VNets. It must be different " + + "than the VXLAN-ID of the VNets.", + }, + }), + } +} diff --git a/fwprovider/cluster/sdn/zone/resource_simple.go b/fwprovider/cluster/sdn/zone/resource_simple.go new file mode 100644 index 000000000..a40883d05 --- /dev/null +++ b/fwprovider/cluster/sdn/zone/resource_simple.go @@ -0,0 +1,185 @@ +/* + * 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 = &SimpleResource{} + _ resource.ResourceWithImportState = &SimpleResource{} +) + +type SimpleResource struct { + client *zones.Client +} + +// NewSimpleResource creates a new instance of the Simple resource. +func NewSimpleResource() resource.Resource { + return &SimpleResource{} +} + +// Metadata defines the name of the resource. +func (r *SimpleResource) Metadata( + _ context.Context, + req resource.MetadataRequest, + resp *resource.MetadataResponse, +) { + resp.TypeName = req.ProviderTypeName + "_sdn_zone_simple" +} + +func (r *SimpleResource) 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 *SimpleResource) Create( + ctx context.Context, + req resource.CreateRequest, + resp *resource.CreateResponse, +) { + var plan baseModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + if resp.Diagnostics.HasError() { + return + } + + reqData := plan.toAPIRequestBody(ctx, &resp.Diagnostics) + reqData.Type = ptr.Ptr(zones.TypeSimple) + + if err := r.client.CreateZone(ctx, reqData); err != nil { + resp.Diagnostics.AddError( + "Unable to Create SDN Zone", + err.Error(), + ) + + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *SimpleResource) Read( + ctx context.Context, + req resource.ReadRequest, + resp *resource.ReadResponse, +) { + var state baseModel + + 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 Zone", + err.Error(), + ) + + return + } + + readModel := &baseModel{} + readModel.importFromAPI(zone.ID, zone, &resp.Diagnostics) + resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...) +} + +func (r *SimpleResource) Update( + ctx context.Context, + req resource.UpdateRequest, + resp *resource.UpdateResponse, +) { + var plan baseModel + + 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 Zone", + err.Error(), + ) + + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *SimpleResource) Delete( + ctx context.Context, + req resource.DeleteRequest, + resp *resource.DeleteResponse, +) { + var state baseModel + + 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 Zone", + err.Error(), + ) + } +} + +func (r *SimpleResource) ImportState( + ctx context.Context, + req resource.ImportStateRequest, + resp *resource.ImportStateResponse, +) { +} diff --git a/fwprovider/cluster/sdn/zone/resource_simple_test.go b/fwprovider/cluster/sdn/zone/resource_simple_test.go new file mode 100644 index 000000000..8ce5c3071 --- /dev/null +++ b/fwprovider/cluster/sdn/zone/resource_simple_test.go @@ -0,0 +1,53 @@ +//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 TestAccResourceSDNZoneSimple(t *testing.T) { + te := test.InitEnvironment(t) + + tests := []struct { + name string + steps []resource.TestStep + }{ + {"create and update zones", []resource.TestStep{{ + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_sdn_zone_simple" "zone_simple" { + id = "zoneS" + nodes = ["pve"] + mtu = 1496 + } + `), + }, { + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_sdn_zone_simple" "zone_simple" { + id = "zoneS" + nodes = ["pve"] + mtu = 1495 + } + `), + }}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: te.AccProviders, + Steps: tt.steps, + }) + }) + } +} diff --git a/fwprovider/provider.go b/fwprovider/provider.go index c578e2e87..b657c5cce 100644 --- a/fwprovider/provider.go +++ b/fwprovider/provider.go @@ -30,6 +30,7 @@ import ( "github.com/bpg/terraform-provider-proxmox/fwprovider/cluster/hardwaremapping" "github.com/bpg/terraform-provider-proxmox/fwprovider/cluster/metrics" "github.com/bpg/terraform-provider-proxmox/fwprovider/cluster/options" + sdnzone "github.com/bpg/terraform-provider-proxmox/fwprovider/cluster/sdn/zone" "github.com/bpg/terraform-provider-proxmox/fwprovider/config" "github.com/bpg/terraform-provider-proxmox/fwprovider/nodes" "github.com/bpg/terraform-provider-proxmox/fwprovider/nodes/apt" @@ -527,6 +528,11 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc nodes.NewDownloadFileResource, options.NewClusterOptionsResource, vm.NewResource, + sdnzone.NewSimpleResource, + // + // sdn.NewSDNZoneResource, + // sdn.NewSDNVnetResource, + //sdn.NewSDNSubnetResource, } } @@ -550,6 +556,9 @@ func (p *proxmoxProvider) DataSources(_ context.Context) []func() datasource.Dat hardwaremapping.NewUSBDataSource, metrics.NewMetricsServerDatasource, vm.NewDataSource, + // sdn.NewSDNZoneDataSource, + // sdn.NewSDNVnetDataSource, + // sdn.NewSDNSubnetDataSource, } } diff --git a/main.go b/main.go index 0395120ef..c71c3b4ed 100644 --- a/main.go +++ b/main.go @@ -65,6 +65,7 @@ import ( //go:generate cp ./build/docs-gen/resources/virtual_environment_haresource.md ./docs/resources/ //go:generate cp ./build/docs-gen/resources/virtual_environment_network_linux_bridge.md ./docs/resources/ //go:generate cp ./build/docs-gen/resources/virtual_environment_network_linux_vlan.md ./docs/resources/ +//go:generate cp ./build/docs-gen/resources/virtual_environment_sdn_zone_simple.md ./docs/resources/ //go:generate cp ./build/docs-gen/resources/virtual_environment_user_token.md ./docs/resources/ //go:generate cp ./build/docs-gen/resources/virtual_environment_vm2.md ./docs/resources/ //go:generate cp ./build/docs-gen/resources/virtual_environment_metrics_server.md ./docs/resources/ diff --git a/proxmox/cluster/client.go b/proxmox/cluster/client.go index e4f2314a7..f0accdf17 100644 --- a/proxmox/cluster/client.go +++ b/proxmox/cluster/client.go @@ -15,6 +15,7 @@ import ( "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/ha" "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/mapping" "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/metrics" + "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones" "github.com/bpg/terraform-provider-proxmox/proxmox/firewall" ) @@ -54,3 +55,18 @@ func (c *Client) ACME() *acme.Client { func (c *Client) Metrics() *metrics.Client { return &metrics.Client{Client: c} } + +// SDNZones returns a client for managing the cluster's SDN zones. +func (c *Client) SDNZones() *zones.Client { + return &zones.Client{Client: c} +} + +// // SDNVnets returns a client for managing the cluster's SDN Vnets. +// func (c *Client) SDNVnets() *vnets.Client { +// return &vnets.Client{Client: c} +// } + +// // SDNSubnets returns a client for managing the cluster's SDN Subnets. +// func (c *Client) SDNSubnets() *subnets.Client { +// return &subnets.Client{Client: c} +// } diff --git a/proxmox/cluster/sdn/zones/zones_types.go b/proxmox/cluster/sdn/zones/zones_types.go index f463cd262..d753bc15f 100644 --- a/proxmox/cluster/sdn/zones/zones_types.go +++ b/proxmox/cluster/sdn/zones/zones_types.go @@ -6,8 +6,16 @@ package zones +const ( + TypeSimple = "simple" + TypeVLAN = "vlan" + TypeQinQ = "qinq" + TypeVXLAN = "vxlan" + TypeEVPN = "evpn" +) + type ZoneData struct { - ID string `json:"zone,omitempty" url:"zone,omitempty"` + ID string `json:"zone" url:"zone"` Type *string `json:"type,omitempty" url:"type,omitempty"` IPAM *string `json:"ipam,omitempty" url:"ipam,omitempty"` DNS *string `json:"dns,omitempty" url:"dns,omitempty"` @@ -40,6 +48,7 @@ type ZoneData struct { // ZoneRequestData wraps a ZoneData struct with optional delete instructions. type ZoneRequestData struct { ZoneData + Delete []string `url:"delete,omitempty"` } diff --git a/testacc b/testacc index b022b53b6..0959e6318 100755 --- a/testacc +++ b/testacc @@ -6,5 +6,21 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. # -# shellcheck disable=SC2046 -TF_ACC=1 env $(xargs < testacc.env) go test -count 1 --tags=acceptance -timeout 360s -run "$1" github.com/bpg/terraform-provider-proxmox/fwprovider/... $2 +BASE_PKG="github.com/bpg/terraform-provider-proxmox" + +find_test_package() { + test_name="$1" + [ -z "$test_name" ] && echo "${BASE_PKG}/fwprovider/..." && return + + test_file=$(find . -name "*.go" -type f -exec grep -l "func ${test_name}(" {} \; | head -1) + [ -z "$test_file" ] && echo "${BASE_PKG}/fwprovider/..." && return + + package_dir=$(dirname "$test_file") + package_path=$(echo "$package_dir" | sed 's|^\./||') + echo "${BASE_PKG}/${package_path}" +} + +PACKAGE_PATH=$(find_test_package "$1") + +# shellcheck disable=SC2046,SC2086 +TF_ACC=1 env $(xargs < testacc.env) go test -count 1 --tags=acceptance -timeout 360s -run "$1" "$PACKAGE_PATH" $2 From bf19edb12d36fd9b9e2c29fe9c05bf4d251a9743 Mon Sep 17 00:00:00 2001 From: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Date: Fri, 18 Jul 2025 15:05:32 -0400 Subject: [PATCH 3/9] add other zone types Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- fwprovider/cluster/sdn/zone/resource_evpn.go | 191 ++++++++++++++++++ fwprovider/cluster/sdn/zone/resource_model.go | 133 +++++++++--- fwprovider/cluster/sdn/zone/resource_qinq.go | 191 ++++++++++++++++++ .../cluster/sdn/zone/resource_qinq_test.go | 65 ++++++ .../cluster/sdn/zone/resource_schema.go | 38 ++-- .../cluster/sdn/zone/resource_simple.go | 34 +++- .../cluster/sdn/zone/resource_simple_test.go | 6 + fwprovider/cluster/sdn/zone/resource_vlan.go | 191 ++++++++++++++++++ .../cluster/sdn/zone/resource_vlan_test.go | 61 ++++++ fwprovider/cluster/sdn/zone/resource_vxlan.go | 191 ++++++++++++++++++ .../cluster/sdn/zone/resource_vxlan_test.go | 61 ++++++ proxmox/api/client.go | 4 +- proxmox/cluster/sdn/zones/zones_types.go | 18 +- 13 files changed, 1125 insertions(+), 59 deletions(-) create mode 100644 fwprovider/cluster/sdn/zone/resource_evpn.go create mode 100644 fwprovider/cluster/sdn/zone/resource_qinq.go create mode 100644 fwprovider/cluster/sdn/zone/resource_qinq_test.go create mode 100644 fwprovider/cluster/sdn/zone/resource_vlan.go create mode 100644 fwprovider/cluster/sdn/zone/resource_vlan_test.go create mode 100644 fwprovider/cluster/sdn/zone/resource_vxlan.go create mode 100644 fwprovider/cluster/sdn/zone/resource_vxlan_test.go diff --git a/fwprovider/cluster/sdn/zone/resource_evpn.go b/fwprovider/cluster/sdn/zone/resource_evpn.go new file mode 100644 index 000000000..a7d3836b6 --- /dev/null +++ b/fwprovider/cluster/sdn/zone/resource_evpn.go @@ -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)...) +} diff --git a/fwprovider/cluster/sdn/zone/resource_model.go b/fwprovider/cluster/sdn/zone/resource_model.go index 4368b7fca..359693790 100644 --- a/fwprovider/cluster/sdn/zone/resource_model.go +++ b/fwprovider/cluster/sdn/zone/resource_model.go @@ -14,6 +14,8 @@ import ( "github.com/bpg/terraform-provider-proxmox/fwprovider/types/stringset" "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones" + + proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types" ) 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.Nodes = stringset.NewValueString(data.Nodes, diags, stringset.WithSeparator(",")) 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 { @@ -76,18 +66,113 @@ func (m *baseModel) toAPIRequestBody(ctx context.Context, diags *diag.Diagnostic data.DNSZone = m.DNSZone.ValueStringPointer() data.Nodes = m.Nodes.ValueStringPointer(ctx, diags, stringset.WithSeparator(",")) data.MTU = m.MTU.ValueInt64Pointer() - // data.Bridge = m.Bridge.ValueStringPointer() - // data.ServiceVLAN = m.ServiceVLAN.ValueInt64Pointer() - // data.ServiceVLANProtocol = m.ServiceVLANProtocol.ValueStringPointer() - // data.Peers = m.Peers.ValueStringPointer(ctx, diags, comaSeparated) - // data.Controller = m.Controller.ValueStringPointer() - // data.ExitNodes = m.ExitNodes.ValueStringPointer(ctx, diags, comaSeparated) - // data.ExitNodesPrimary = m.PrimaryExitNode.ValueStringPointer() - // data.RouteTargetImport = m.RouteTargetImport.ValueStringPointer() - // data.VRFVXLANID = m.VRFVXLANID.ValueInt64Pointer() - // data.ExitNodesLocalRouting = ptrConversion.BoolToInt64Ptr(m.ExitNodesLocalRouting.ValueBoolPointer()) - // data.AdvertiseSubnets = ptrConversion.BoolToInt64Ptr(m.AdvertiseSubnets.ValueBoolPointer()) - // data.DisableARPNDSuppression = ptrConversion.BoolToInt64Ptr(m.DisableARPNDSuppression.ValueBoolPointer()) + + return data +} + +type simpleModel struct { + baseModel +} + +type vlanModel struct { + baseModel + + 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 } diff --git a/fwprovider/cluster/sdn/zone/resource_qinq.go b/fwprovider/cluster/sdn/zone/resource_qinq.go new file mode 100644 index 000000000..1e91ec3a8 --- /dev/null +++ b/fwprovider/cluster/sdn/zone/resource_qinq.go @@ -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)...) +} diff --git a/fwprovider/cluster/sdn/zone/resource_qinq_test.go b/fwprovider/cluster/sdn/zone/resource_qinq_test.go new file mode 100644 index 000000000..bb047a5c1 --- /dev/null +++ b/fwprovider/cluster/sdn/zone/resource_qinq_test.go @@ -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, + }) + }) + } +} diff --git a/fwprovider/cluster/sdn/zone/resource_schema.go b/fwprovider/cluster/sdn/zone/resource_schema.go index e26864ddb..8aa78f351 100644 --- a/fwprovider/cluster/sdn/zone/resource_schema.go +++ b/fwprovider/cluster/sdn/zone/resource_schema.go @@ -22,16 +22,16 @@ import ( "github.com/bpg/terraform-provider-proxmox/fwprovider/types/stringset" ) -func commonAttributes(base ...map[string]schema.Attribute) map[string]schema.Attribute { - if len(base) > 1 { - panic("commonAttributes expects at most one base map") +func baseAttributesWith(extraAttributes ...map[string]schema.Attribute) map[string]schema.Attribute { + if len(extraAttributes) > 1 { + panic("baseAttributesWith expects at most one extraAttributes map") } - if len(base) == 0 { - base = append(base, make(map[string]schema.Attribute)) + if len(extraAttributes) == 0 { + 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{ Optional: true, 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( @@ -85,11 +85,11 @@ func (r *SimpleResource) Schema( 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. " + "It can be used in NAT or routed setups.", - Attributes: commonAttributes(), + Attributes: baseAttributesWith(), } } -func (r *VLAN) Schema( +func (r *VLANResource) Schema( _ context.Context, _ resource.SchemaRequest, 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 " + "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.", - Attributes: commonAttributes(map[string]schema.Attribute{ + Attributes: baseAttributesWith(map[string]schema.Attribute{ "bridge": schema.StringAttribute{ Description: "Bridge interface for VLAN.", 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, _ resource.SchemaRequest, 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 " + "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.", - Attributes: commonAttributes(map[string]schema.Attribute{ + Attributes: baseAttributesWith(map[string]schema.Attribute{ "bridge": schema.StringAttribute{ Description: "A local, VLAN-aware bridge that is already configured on each local node", Optional: true, @@ -145,7 +145,7 @@ func (r *QinQ) Schema( } } -func (r *VXLAN) Schema( +func (r *VXLANResource) Schema( _ context.Context, _ resource.SchemaRequest, 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 " + "between all peers. Because VXLAN encapsulation uses 50 bytes, the MTU needs to be 50 bytes lower than the " + "outgoing physical interface.", - Attributes: commonAttributes(map[string]schema.Attribute{ + Attributes: baseAttributesWith(map[string]schema.Attribute{ "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. "+ @@ -168,7 +168,7 @@ func (r *VXLAN) Schema( } } -func (r *EVPN) Schema( +func (r *EVPNResource) Schema( _ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse, @@ -177,7 +177,7 @@ func (r *EVPN) Schema( Description: "EVPN Zone in Proxmox SDN.", MarkdownDescription: "EVPN Zone in Proxmox SDN. The EVPN zone creates a routable Layer 3 network, capable of " + "spanning across multiple clusters.", - Attributes: commonAttributes(map[string]schema.Attribute{ + Attributes: baseAttributesWith(map[string]schema.Attribute{ "advertise_subnets": schema.BoolAttribute{ Optional: true, Description: "Enable subnet advertisement for EVPN.", @@ -202,6 +202,12 @@ func (r *EVPN) Schema( "rt_import": schema.StringAttribute{ Optional: true, Description: "Route target import for EVPN.", + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile(`^(\d+):(\d+)$`), + "must be in the format ':' (e.g., '65000:65000')", + ), + }, }, "vrf_vxlan": schema.Int64Attribute{ Optional: true, diff --git a/fwprovider/cluster/sdn/zone/resource_simple.go b/fwprovider/cluster/sdn/zone/resource_simple.go index a40883d05..da15af956 100644 --- a/fwprovider/cluster/sdn/zone/resource_simple.go +++ b/fwprovider/cluster/sdn/zone/resource_simple.go @@ -28,12 +28,10 @@ type SimpleResource struct { client *zones.Client } -// NewSimpleResource creates a new instance of the Simple resource. func NewSimpleResource() resource.Resource { return &SimpleResource{} } -// Metadata defines the name of the resource. func (r *SimpleResource) Metadata( _ context.Context, req resource.MetadataRequest, @@ -72,7 +70,7 @@ func (r *SimpleResource) Create( req resource.CreateRequest, resp *resource.CreateResponse, ) { - var plan baseModel + var plan simpleModel 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 { resp.Diagnostics.AddError( - "Unable to Create SDN Zone", + "Unable to Create SDN SimpleZone", err.Error(), ) @@ -100,7 +98,7 @@ func (r *SimpleResource) Read( req resource.ReadRequest, resp *resource.ReadResponse, ) { - var state baseModel + var state simpleModel resp.Diagnostics.Append(req.State.Get(ctx, &state)...) @@ -116,7 +114,7 @@ func (r *SimpleResource) Read( } resp.Diagnostics.AddError( - "Unable to Read SDN Zone", + "Unable to Read SDN SimpleZone", err.Error(), ) @@ -133,7 +131,7 @@ func (r *SimpleResource) Update( req resource.UpdateRequest, resp *resource.UpdateResponse, ) { - var plan baseModel + var plan simpleModel 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 { resp.Diagnostics.AddError( - "Unable to Update SDN Zone", + "Unable to Update SDN Simple Zone", err.Error(), ) @@ -160,7 +158,7 @@ func (r *SimpleResource) Delete( req resource.DeleteRequest, resp *resource.DeleteResponse, ) { - var state baseModel + var state simpleModel 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 && !errors.Is(err, api.ErrResourceDoesNotExist) { resp.Diagnostics.AddError( - "Unable to Delete SDN Zone", + "Unable to Delete SDN Simple Zone", err.Error(), ) } @@ -182,4 +180,20 @@ func (r *SimpleResource) ImportState( 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 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)...) } diff --git a/fwprovider/cluster/sdn/zone/resource_simple_test.go b/fwprovider/cluster/sdn/zone/resource_simple_test.go index 8ce5c3071..87bc91985 100644 --- a/fwprovider/cluster/sdn/zone/resource_simple_test.go +++ b/fwprovider/cluster/sdn/zone/resource_simple_test.go @@ -17,6 +17,8 @@ import ( ) func TestAccResourceSDNZoneSimple(t *testing.T) { + t.Parallel() + te := test.InitEnvironment(t) tests := []struct { @@ -39,6 +41,10 @@ func TestAccResourceSDNZoneSimple(t *testing.T) { mtu = 1495 } `), + ResourceName: "proxmox_virtual_environment_sdn_zone_simple.zone_simple", + ImportStateId: "zoneS", + ImportState: true, + ImportStateVerify: true, }}}, } diff --git a/fwprovider/cluster/sdn/zone/resource_vlan.go b/fwprovider/cluster/sdn/zone/resource_vlan.go new file mode 100644 index 000000000..e303f107b --- /dev/null +++ b/fwprovider/cluster/sdn/zone/resource_vlan.go @@ -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)...) +} diff --git a/fwprovider/cluster/sdn/zone/resource_vlan_test.go b/fwprovider/cluster/sdn/zone/resource_vlan_test.go new file mode 100644 index 000000000..dc8b64ebe --- /dev/null +++ b/fwprovider/cluster/sdn/zone/resource_vlan_test.go @@ -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, + }) + }) + } +} diff --git a/fwprovider/cluster/sdn/zone/resource_vxlan.go b/fwprovider/cluster/sdn/zone/resource_vxlan.go new file mode 100644 index 000000000..703b93206 --- /dev/null +++ b/fwprovider/cluster/sdn/zone/resource_vxlan.go @@ -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)...) +} diff --git a/fwprovider/cluster/sdn/zone/resource_vxlan_test.go b/fwprovider/cluster/sdn/zone/resource_vxlan_test.go new file mode 100644 index 000000000..7bb2f30db --- /dev/null +++ b/fwprovider/cluster/sdn/zone/resource_vxlan_test.go @@ -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, + }) + }) + } +} diff --git a/proxmox/api/client.go b/proxmox/api/client.go index 663dc9c99..8cc89d5fc 100644 --- a/proxmox/api/client.go +++ b/proxmox/api/client.go @@ -352,7 +352,9 @@ func validateResponseCode(res *http.Response) error { errList = append(errList, split...) } - msg = fmt.Sprintf("%s (%s)", msg, strings.Join(errList, " - ")) + if len(errList) > 0 { + msg = fmt.Sprintf("%s (%s)", msg, strings.Join(errList, " - ")) + } } httpError := &HTTPError{ diff --git a/proxmox/cluster/sdn/zones/zones_types.go b/proxmox/cluster/sdn/zones/zones_types.go index d753bc15f..6b40695db 100644 --- a/proxmox/cluster/sdn/zones/zones_types.go +++ b/proxmox/cluster/sdn/zones/zones_types.go @@ -6,6 +6,8 @@ package zones +import "github.com/bpg/terraform-provider-proxmox/proxmox/types" + const ( TypeSimple = "simple" TypeVLAN = "vlan" @@ -35,14 +37,14 @@ type ZoneData struct { Peers *string `json:"peers,omitempty" url:"peers,omitempty"` // EVPN. - Controller *string `json:"controller,omitempty" url:"controller,omitempty"` - VRFVXLANID *int64 `json:"vrf-vxlan,omitempty" url:"vrf-vxlan,omitempty"` - ExitNodes *string `json:"exitnodes,omitempty" url:"exitnodes,omitempty"` - ExitNodesPrimary *string `json:"exitnodes-primary,omitempty" url:"exitnodes-primary,omitempty"` - ExitNodesLocalRouting *int64 `json:"exitnodes-local-routing,omitempty" url:"exitnodes-local-routing,omitempty"` - AdvertiseSubnets *int64 `json:"advertise-subnets,omitempty" url:"advertise-subnets,omitempty"` - DisableARPNDSuppression *int64 `json:"disable-arp-nd-suppression,omitempty" url:"disable-arp-nd-suppression,omitempty"` - RouteTargetImport *string `json:"rt-import,omitempty" url:"rt-import,omitempty"` + Controller *string `json:"controller,omitempty" url:"controller,omitempty"` + VRFVXLANID *int64 `json:"vrf-vxlan,omitempty" url:"vrf-vxlan,omitempty"` + ExitNodes *string `json:"exitnodes,omitempty" url:"exitnodes,omitempty"` + ExitNodesPrimary *string `json:"exitnodes-primary,omitempty" url:"exitnodes-primary,omitempty"` + ExitNodesLocalRouting *types.CustomBool `json:"exitnodes-local-routing,omitempty" url:"exitnodes-local-routing,omitempty,int"` + AdvertiseSubnets *types.CustomBool `json:"advertise-subnets,omitempty" url:"advertise-subnets,omitempty,int"` + 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"` } // ZoneRequestData wraps a ZoneData struct with optional delete instructions. From 12cc3298e9c5a806a629df015d3798322691a553 Mon Sep 17 00:00:00 2001 From: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:16:20 -0400 Subject: [PATCH 4/9] cleanup Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- .golangci.yml | 2 + fwprovider/cluster/sdn/zone/resource_evpn.go | 259 +++++++-------- .../cluster/sdn/zone/resource_generic.go | 294 ++++++++++++++++++ fwprovider/cluster/sdn/zone/resource_model.go | 178 ----------- fwprovider/cluster/sdn/zone/resource_qinq.go | 221 +++++-------- .../cluster/sdn/zone/resource_qinq_test.go | 65 ---- .../cluster/sdn/zone/resource_schema.go | 219 ------------- .../cluster/sdn/zone/resource_simple.go | 194 +++--------- .../cluster/sdn/zone/resource_simple_test.go | 59 ---- fwprovider/cluster/sdn/zone/resource_vlan.go | 201 ++++-------- .../cluster/sdn/zone/resource_vlan_test.go | 61 ---- fwprovider/cluster/sdn/zone/resource_vxlan.go | 202 ++++-------- .../cluster/sdn/zone/resource_vxlan_test.go | 61 ---- .../cluster/sdn/zone/resource_zones_test.go | 195 ++++++++++++ .../nodes/network/resource_linux_bridge.go | 1 - .../nodes/network/resource_linux_vlan.go | 1 - fwprovider/provider.go | 7 +- proxmox/cluster/sdn/zones/api.go | 19 ++ 18 files changed, 853 insertions(+), 1386 deletions(-) create mode 100644 fwprovider/cluster/sdn/zone/resource_generic.go delete mode 100644 fwprovider/cluster/sdn/zone/resource_model.go delete mode 100644 fwprovider/cluster/sdn/zone/resource_qinq_test.go delete mode 100644 fwprovider/cluster/sdn/zone/resource_schema.go delete mode 100644 fwprovider/cluster/sdn/zone/resource_simple_test.go delete mode 100644 fwprovider/cluster/sdn/zone/resource_vlan_test.go delete mode 100644 fwprovider/cluster/sdn/zone/resource_vxlan_test.go create mode 100644 fwprovider/cluster/sdn/zone/resource_zones_test.go create mode 100644 proxmox/cluster/sdn/zones/api.go diff --git a/.golangci.yml b/.golangci.yml index 3c6d3ae15..b91d3142c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -44,6 +44,8 @@ linters: gosec: excludes: - G115 + lll: + line-length: 150 revive: rules: - name: "package-comments" diff --git a/fwprovider/cluster/sdn/zone/resource_evpn.go b/fwprovider/cluster/sdn/zone/resource_evpn.go index a7d3836b6..f753bae3b 100644 --- a/fwprovider/cluster/sdn/zone/resource_evpn.go +++ b/fwprovider/cluster/sdn/zone/resource_evpn.go @@ -8,15 +8,19 @@ package zone import ( "context" - "errors" - "fmt" + "regexp" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/bpg/terraform-provider-proxmox/fwprovider/config" - "github.com/bpg/terraform-provider-proxmox/proxmox/api" + "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/helpers/ptr" + + proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types" ) var ( @@ -24,168 +28,131 @@ var ( _ resource.ResourceWithImportState = &EVPNResource{} ) -type EVPNResource struct { - client *zones.Client +type evpnModel struct { + genericModel + + 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 NewEVPNResource() resource.Resource { - return &EVPNResource{} +func (m *evpnModel) importFromAPI(name string, data *zones.ZoneData, diags *diag.Diagnostics) { + m.genericModel.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 (r *EVPNResource) Metadata( - _ context.Context, - req resource.MetadataRequest, - resp *resource.MetadataResponse, -) { - resp.TypeName = req.ProviderTypeName + "_sdn_zone_evpn" -} +func (m *evpnModel) toAPIRequestBody(ctx context.Context, diags *diag.Diagnostics) *zones.ZoneRequestData { + data := m.genericModel.toAPIRequestBody(ctx, diags) -func (r *EVPNResource) Configure( - _ context.Context, - req resource.ConfigureRequest, - resp *resource.ConfigureResponse, -) { - if req.ProviderData == nil { - return - } + 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() - 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() + return data } -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)...) +type EVPNResource struct { + generic *genericZoneResource } -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 +func NewEVPNResource() resource.Resource { + return &EVPNResource{ + generic: newGenericZoneResource(zoneResourceConfig{ + typeNameSuffix: "_sdn_zone_evpn", + zoneType: zones.TypeEVPN, + modelFunc: func() zoneModel { return &evpnModel{} }, + }).(*genericZoneResource), } - - 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 +func (r *EVPNResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "EVPN Zone in Proxmox SDN.", + MarkdownDescription: "EVPN Zone in Proxmox SDN. The EVPN zone creates a routable Layer 3 network, capable of " + + "spanning across multiple clusters.", + Attributes: genericAttributesWith(map[string]schema.Attribute{ + "advertise_subnets": schema.BoolAttribute{ + Optional: true, + Description: "Enable subnet advertisement for EVPN.", + }, + "controller": schema.StringAttribute{ + Optional: true, + Description: "EVPN controller address.", + }, + "disable_arp_nd_suppression": schema.BoolAttribute{ + Optional: true, + Description: "Disable ARP/ND suppression for EVPN.", + }, + "exit_nodes": stringset.ResourceAttribute("List of exit nodes for EVPN.", ""), + "exit_nodes_local_routing": schema.BoolAttribute{ + Optional: true, + Description: "Enable local routing for EVPN exit nodes.", + }, + "primary_exit_node": schema.StringAttribute{ + Optional: true, + Description: "Primary exit node for EVPN.", + }, + "rt_import": schema.StringAttribute{ + Optional: true, + Description: "Route target import for EVPN.", + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile(`^(\d+):(\d+)$`), + "must be in the format ':' (e.g., '65000:65000')", + ), + }, + }, + "vrf_vxlan": schema.Int64Attribute{ + Optional: true, + Description: "VRF VXLAN-ID used for dedicated routing interconnect between VNets. It must be different " + + "than the VXLAN-ID of the VNets.", + }, + }), } +} - 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 - } +func (r *EVPNResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.generic.Metadata(ctx, req, resp) +} - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +func (r *EVPNResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.generic.Configure(ctx, req, resp) } -func (r *EVPNResource) Delete( - ctx context.Context, - req resource.DeleteRequest, - resp *resource.DeleteResponse, -) { - var state evpnModel +func (r *EVPNResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + r.generic.Create(ctx, req, resp) +} - resp.Diagnostics.Append(req.State.Get(ctx, &state)...) +func (r *EVPNResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + r.generic.Read(ctx, req, resp) +} - if resp.Diagnostics.HasError() { - return - } +func (r *EVPNResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + r.generic.Update(ctx, req, resp) +} - 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) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + r.generic.Delete(ctx, req, resp) } -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)...) +func (r *EVPNResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.generic.ImportState(ctx, req, resp) } diff --git a/fwprovider/cluster/sdn/zone/resource_generic.go b/fwprovider/cluster/sdn/zone/resource_generic.go new file mode 100644 index 000000000..3c4e9005c --- /dev/null +++ b/fwprovider/cluster/sdn/zone/resource_generic.go @@ -0,0 +1,294 @@ +/* + * 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" + "maps" + "regexp" + + "github.com/bpg/terraform-provider-proxmox/fwprovider/config" + "github.com/bpg/terraform-provider-proxmox/fwprovider/types/stringset" + "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" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type genericModel struct { + ID types.String `tfsdk:"id"` + IPAM types.String `tfsdk:"ipam"` + DNS types.String `tfsdk:"dns"` + ReverseDNS types.String `tfsdk:"reverse_dns"` + DNSZone types.String `tfsdk:"dns_zone"` + Nodes stringset.Value `tfsdk:"nodes"` + MTU types.Int64 `tfsdk:"mtu"` +} + +func (m *genericModel) importFromAPI(name string, data *zones.ZoneData, diags *diag.Diagnostics) { + m.ID = types.StringValue(name) + + m.DNS = types.StringPointerValue(data.DNS) + m.DNSZone = types.StringPointerValue(data.DNSZone) + m.IPAM = types.StringPointerValue(data.IPAM) + m.MTU = types.Int64PointerValue(data.MTU) + m.Nodes = stringset.NewValueString(data.Nodes, diags, stringset.WithSeparator(",")) + m.ReverseDNS = types.StringPointerValue(data.ReverseDNS) +} + +func (m *genericModel) toAPIRequestBody(ctx context.Context, diags *diag.Diagnostics) *zones.ZoneRequestData { + data := &zones.ZoneRequestData{} + + data.ID = m.ID.ValueString() + + data.IPAM = m.IPAM.ValueStringPointer() + data.DNS = m.DNS.ValueStringPointer() + data.ReverseDNS = m.ReverseDNS.ValueStringPointer() + data.DNSZone = m.DNSZone.ValueStringPointer() + data.Nodes = m.Nodes.ValueStringPointer(ctx, diags, stringset.WithSeparator(",")) + data.MTU = m.MTU.ValueInt64Pointer() + + return data +} + +func (m *genericModel) getID() string { + return m.ID.ValueString() +} + +func genericAttributesWith(extraAttributes ...map[string]schema.Attribute) map[string]schema.Attribute { + if len(extraAttributes) > 1 { + panic("genericAttributesWith expects at most one extraAttributes map") + } + + if len(extraAttributes) == 0 { + extraAttributes = append(extraAttributes, make(map[string]schema.Attribute)) + } + + maps.Copy(extraAttributes[0], map[string]schema.Attribute{ + "dns": schema.StringAttribute{ + Optional: true, + Description: "DNS API server address.", + }, + "dns_zone": schema.StringAttribute{ + Optional: true, + Description: "DNS domain name. The DNS zone must already exist on the DNS server.", + MarkdownDescription: "DNS domain name. Used to register hostnames, such as `.`. " + + "The DNS zone must already exist on the DNS server.", + }, + "id": schema.StringAttribute{ + Description: "The unique identifier of the SDN zone.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + // https://github.com/proxmox/pve-network/blob/faaf96a8378a3e41065018562c09c3de0aa434f5/src/PVE/Network/SDN/Zones/Plugin.pm#L34 + stringvalidator.RegexMatches( + regexp.MustCompile(`^[A-Za-z][A-Za-z0-9]*[A-Za-z0-9]$`), + "must be a valid zone identifier", + ), + stringvalidator.LengthAtMost(8), + }, + }, + "ipam": schema.StringAttribute{ + Optional: true, + Description: "IP Address Management system.", + }, + "mtu": schema.Int64Attribute{ + Optional: true, + Description: "MTU value for the zone.", + }, + "nodes": stringset.ResourceAttribute("Proxmox node names.", ""), + "reverse_dns": schema.StringAttribute{ + Optional: true, + Description: "Reverse DNS API server address.", + }, + }) + + return extraAttributes[0] +} + +type zoneModel interface { + importFromAPI(name string, data *zones.ZoneData, diags *diag.Diagnostics) + toAPIRequestBody(ctx context.Context, diags *diag.Diagnostics) *zones.ZoneRequestData + getID() string +} + +type zoneResourceConfig struct { + typeNameSuffix string + zoneType string + modelFunc func() zoneModel +} + +type genericZoneResource struct { + client *zones.Client + config zoneResourceConfig +} + +func newGenericZoneResource(cfg zoneResourceConfig) resource.Resource { + return &genericZoneResource{config: cfg} +} + +func (r *genericZoneResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + r.config.typeNameSuffix +} + +func (r *genericZoneResource) 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 *genericZoneResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + plan := r.config.modelFunc() + resp.Diagnostics.Append(req.Plan.Get(ctx, plan)...) + + if resp.Diagnostics.HasError() { + return + } + + diags := &diag.Diagnostics{} + reqData := plan.toAPIRequestBody(ctx, diags) + resp.Diagnostics.Append(*diags...) + + reqData.Type = ptr.Ptr(r.config.zoneType) + + if err := r.client.CreateZone(ctx, reqData); err != nil { + resp.Diagnostics.AddError( + "Unable to Create SDN Zone", + err.Error(), + ) + + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *genericZoneResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + state := r.config.modelFunc() + resp.Diagnostics.Append(req.State.Get(ctx, state)...) + + if resp.Diagnostics.HasError() { + return + } + + zone, err := r.client.GetZone(ctx, state.getID()) + if err != nil { + if errors.Is(err, api.ErrResourceDoesNotExist) { + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.AddError( + "Unable to Read SDN Zone", + err.Error(), + ) + + return + } + + readModel := r.config.modelFunc() + diags := &diag.Diagnostics{} + readModel.importFromAPI(zone.ID, zone, diags) + resp.Diagnostics.Append(*diags...) + resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...) +} + +func (r *genericZoneResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + plan := r.config.modelFunc() + resp.Diagnostics.Append(req.Plan.Get(ctx, plan)...) + + if resp.Diagnostics.HasError() { + return + } + + diags := &diag.Diagnostics{} + reqData := plan.toAPIRequestBody(ctx, diags) + resp.Diagnostics.Append(*diags...) + + if err := r.client.UpdateZone(ctx, reqData); err != nil { + resp.Diagnostics.AddError( + "Unable to Update SDN Zone", + err.Error(), + ) + + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *genericZoneResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + state := r.config.modelFunc() + resp.Diagnostics.Append(req.State.Get(ctx, state)...) + + if resp.Diagnostics.HasError() { + return + } + + if err := r.client.DeleteZone(ctx, state.getID()); err != nil && + !errors.Is(err, api.ErrResourceDoesNotExist) { + resp.Diagnostics.AddError( + "Unable to Delete SDN Zone", + err.Error(), + ) + } +} + +func (r *genericZoneResource) 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 Zone %s", req.ID), err.Error()) + + return + } + + readModel := r.config.modelFunc() + diags := &diag.Diagnostics{} + readModel.importFromAPI(zone.ID, zone, diags) + resp.Diagnostics.Append(*diags...) + resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...) +} + +// Schema is required to satisfy the resource.Resource interface. It should be implemented by the specific resource. +func (r *genericZoneResource) Schema(_ context.Context, _ resource.SchemaRequest, _ *resource.SchemaResponse) { + // Intentionally left blank. Should be set by the specific resource. +} diff --git a/fwprovider/cluster/sdn/zone/resource_model.go b/fwprovider/cluster/sdn/zone/resource_model.go deleted file mode 100644 index 359693790..000000000 --- a/fwprovider/cluster/sdn/zone/resource_model.go +++ /dev/null @@ -1,178 +0,0 @@ -/* - * 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" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/types" - - "github.com/bpg/terraform-provider-proxmox/fwprovider/types/stringset" - "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones" - - proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types" -) - -type baseModel struct { - ID types.String `tfsdk:"id"` - IPAM types.String `tfsdk:"ipam"` - DNS types.String `tfsdk:"dns"` - ReverseDNS types.String `tfsdk:"reverse_dns"` - DNSZone types.String `tfsdk:"dns_zone"` - Nodes stringset.Value `tfsdk:"nodes"` - MTU types.Int64 `tfsdk:"mtu"` - // // VLAN. - // Bridge types.String `tfsdk:"bridge"` - // // QinQ. - // ServiceVLAN types.Int64 `tfsdk:"service_vlan"` - // ServiceVLANProtocol types.String `tfsdk:"service_vlan_protocol"` - // // VXLAN. - // Peers stringset.Value `tfsdk:"peers"` - // // EVPN. - // Controller types.String `tfsdk:"controller"` - // ExitNodes stringset.Value `tfsdk:"exit_nodes"` - // PrimaryExitNode types.String `tfsdk:"primary_exit_node"` - // RouteTargetImport types.String `tfsdk:"rt_import"` - // VRFVXLANID types.Int64 `tfsdk:"vrf_vxlan"` - // ExitNodesLocalRouting types.Bool `tfsdk:"exit_nodes_local_routing"` - // AdvertiseSubnets types.Bool `tfsdk:"advertise_subnets"` - // DisableARPNDSuppression types.Bool `tfsdk:"disable_arp_nd_suppression"` -} - -func (m *baseModel) importFromAPI(name string, data *zones.ZoneData, diags *diag.Diagnostics) { - m.ID = types.StringValue(name) - - m.DNS = types.StringPointerValue(data.DNS) - m.DNSZone = types.StringPointerValue(data.DNSZone) - m.IPAM = types.StringPointerValue(data.IPAM) - m.MTU = types.Int64PointerValue(data.MTU) - m.Nodes = stringset.NewValueString(data.Nodes, diags, stringset.WithSeparator(",")) - m.ReverseDNS = types.StringPointerValue(data.ReverseDNS) -} - -func (m *baseModel) toAPIRequestBody(ctx context.Context, diags *diag.Diagnostics) *zones.ZoneRequestData { - data := &zones.ZoneRequestData{} - - data.ID = m.ID.ValueString() - - data.IPAM = m.IPAM.ValueStringPointer() - data.DNS = m.DNS.ValueStringPointer() - data.ReverseDNS = m.ReverseDNS.ValueStringPointer() - data.DNSZone = m.DNSZone.ValueStringPointer() - data.Nodes = m.Nodes.ValueStringPointer(ctx, diags, stringset.WithSeparator(",")) - data.MTU = m.MTU.ValueInt64Pointer() - - return data -} - -type simpleModel struct { - baseModel -} - -type vlanModel struct { - baseModel - - 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 -} diff --git a/fwprovider/cluster/sdn/zone/resource_qinq.go b/fwprovider/cluster/sdn/zone/resource_qinq.go index 1e91ec3a8..04e083d6a 100644 --- a/fwprovider/cluster/sdn/zone/resource_qinq.go +++ b/fwprovider/cluster/sdn/zone/resource_qinq.go @@ -8,15 +8,16 @@ package zone import ( "context" - "errors" - "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" - "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 ( @@ -24,168 +25,98 @@ var ( _ resource.ResourceWithImportState = &QinQResource{} ) -type QinQResource struct { - client *zones.Client -} - -func NewQinQResource() resource.Resource { - return &QinQResource{} -} +type qinqModel struct { + vlanModel -func (r *QinQResource) Metadata( - _ context.Context, - req resource.MetadataRequest, - resp *resource.MetadataResponse, -) { - resp.TypeName = req.ProviderTypeName + "_sdn_zone_qinq" + ServiceVLAN types.Int64 `tfsdk:"service_vlan"` + ServiceVLANProtocol types.String `tfsdk:"service_vlan_protocol"` } -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 - } +func (m *qinqModel) importFromAPI(name string, data *zones.ZoneData, diags *diag.Diagnostics) { + m.vlanModel.importFromAPI(name, data, diags) - r.client = cfg.Client.Cluster().SDNZones() + m.ServiceVLAN = types.Int64PointerValue(data.ServiceVLAN) + m.ServiceVLANProtocol = types.StringPointerValue(data.ServiceVLANProtocol) } -func (r *QinQResource) Create( - ctx context.Context, - req resource.CreateRequest, - resp *resource.CreateResponse, -) { - var plan qinqModel +func (m *qinqModel) toAPIRequestBody(ctx context.Context, diags *diag.Diagnostics) *zones.ZoneRequestData { + data := m.vlanModel.toAPIRequestBody(ctx, diags) - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + data.ServiceVLAN = m.ServiceVLAN.ValueInt64Pointer() + data.ServiceVLANProtocol = m.ServiceVLANProtocol.ValueStringPointer() - 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)...) + return data } -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 - } +type QinQResource struct { + generic *genericZoneResource +} - 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 +func NewQinQResource() resource.Resource { + return &QinQResource{ + generic: newGenericZoneResource(zoneResourceConfig{ + typeNameSuffix: "_sdn_zone_qinq", + zoneType: zones.TypeQinQ, + modelFunc: func() zoneModel { return &qinqModel{} }, + }).(*genericZoneResource), } - - 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 +func (r *QinQResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "QinQ Zone in Proxmox SDN.", + MarkdownDescription: "QinQ Zone in Proxmox SDN. QinQ also known as VLAN stacking, that uses multiple layers of " + + "VLAN tags for isolation. The QinQ zone defines the outer VLAN tag (the Service VLAN) whereas the inner " + + "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. " + + "For example, you must reduce the MTU to 1496 if you physical interface MTU is 1500.", + Attributes: genericAttributesWith(map[string]schema.Attribute{ + "bridge": schema.StringAttribute{ + Description: "A local, VLAN-aware bridge that is already configured on each local node", + Optional: true, + }, + "service_vlan": schema.Int64Attribute{ + Optional: true, + Description: "Service VLAN tag for QinQ.", + Validators: []validator.Int64{ + int64validator.Between(int64(1), int64(4094)), + }, + }, + "service_vlan_protocol": schema.StringAttribute{ + Optional: true, + Description: "Service VLAN protocol for QinQ.", + Validators: []validator.String{ + stringvalidator.OneOf("802.1ad", "802.1q"), + }, + }, + }), } +} - 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 - } +func (r *QinQResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.generic.Metadata(ctx, req, resp) +} - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +func (r *QinQResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.generic.Configure(ctx, req, resp) } -func (r *QinQResource) Delete( - ctx context.Context, - req resource.DeleteRequest, - resp *resource.DeleteResponse, -) { - var state qinqModel +func (r *QinQResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + r.generic.Create(ctx, req, resp) +} - resp.Diagnostics.Append(req.State.Get(ctx, &state)...) +func (r *QinQResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + r.generic.Read(ctx, req, resp) +} - if resp.Diagnostics.HasError() { - return - } +func (r *QinQResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + r.generic.Update(ctx, req, resp) +} - 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) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + r.generic.Delete(ctx, req, resp) } -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)...) +func (r *QinQResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.generic.ImportState(ctx, req, resp) } diff --git a/fwprovider/cluster/sdn/zone/resource_qinq_test.go b/fwprovider/cluster/sdn/zone/resource_qinq_test.go deleted file mode 100644 index bb047a5c1..000000000 --- a/fwprovider/cluster/sdn/zone/resource_qinq_test.go +++ /dev/null @@ -1,65 +0,0 @@ -//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, - }) - }) - } -} diff --git a/fwprovider/cluster/sdn/zone/resource_schema.go b/fwprovider/cluster/sdn/zone/resource_schema.go deleted file mode 100644 index 8aa78f351..000000000 --- a/fwprovider/cluster/sdn/zone/resource_schema.go +++ /dev/null @@ -1,219 +0,0 @@ -/* - * 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" - "maps" - "regexp" - - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - - "github.com/bpg/terraform-provider-proxmox/fwprovider/types/stringset" -) - -func baseAttributesWith(extraAttributes ...map[string]schema.Attribute) map[string]schema.Attribute { - if len(extraAttributes) > 1 { - panic("baseAttributesWith expects at most one extraAttributes map") - } - - if len(extraAttributes) == 0 { - extraAttributes = append(extraAttributes, make(map[string]schema.Attribute)) - } - - maps.Copy(extraAttributes[0], map[string]schema.Attribute{ - "dns": schema.StringAttribute{ - Optional: true, - Description: "DNS API server address.", - }, - "dns_zone": schema.StringAttribute{ - Optional: true, - Description: "DNS domain name. The DNS zone must already exist on the DNS server.", - MarkdownDescription: "DNS domain name. Used to register hostnames, such as `.`. " + - "The DNS zone must already exist on the DNS server.", - }, - "id": schema.StringAttribute{ - Description: "The unique identifier of the SDN zone.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - // https://github.com/proxmox/pve-network/blob/faaf96a8378a3e41065018562c09c3de0aa434f5/src/PVE/Network/SDN/Zones/Plugin.pm#L34 - stringvalidator.RegexMatches( - regexp.MustCompile(`^[A-Za-z][A-Za-z0-9]*[A-Za-z0-9]$`), - "must be a valid zone identifier", - ), - stringvalidator.LengthAtMost(8), - }, - }, - "ipam": schema.StringAttribute{ - Optional: true, - Description: "IP Address Management system.", - }, - "mtu": schema.Int64Attribute{ - Optional: true, - Description: "MTU value for the zone.", - }, - "nodes": stringset.ResourceAttribute("Proxmox node names.", ""), - "reverse_dns": schema.StringAttribute{ - Optional: true, - Description: "Reverse DNS API server address.", - }, - }) - - return extraAttributes[0] -} - -func (r *SimpleResource) Schema( - _ context.Context, - _ resource.SchemaRequest, - resp *resource.SchemaResponse, -) { - resp.Schema = schema.Schema{ - Description: "Simple Zone in Proxmox SDN.", - 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. " + - "It can be used in NAT or routed setups.", - Attributes: baseAttributesWith(), - } -} - -func (r *VLANResource) Schema( - _ context.Context, - _ resource.SchemaRequest, - resp *resource.SchemaResponse, -) { - resp.Schema = schema.Schema{ - Description: "VLAN Zone in Proxmox SDN.", - 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. " + - "This allows connectivity of VMs between different nodes.", - Attributes: baseAttributesWith(map[string]schema.Attribute{ - "bridge": schema.StringAttribute{ - Description: "Bridge interface for VLAN.", - MarkdownDescription: "The local bridge or OVS switch, already configured on _each_ node that allows " + - "node-to-node connection.", - Optional: true, - }, - }), - } -} - -func (r *QinQResource) Schema( - _ context.Context, - _ resource.SchemaRequest, - resp *resource.SchemaResponse, -) { - resp.Schema = schema.Schema{ - Description: "QinQ Zone in Proxmox SDN.", - MarkdownDescription: "QinQ Zone in Proxmox SDN. QinQ also known as VLAN stacking, that uses multiple layers of " + - "VLAN tags for isolation. The QinQ zone defines the outer VLAN tag (the Service VLAN) whereas the inner " + - "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. " + - "For example, you must reduce the MTU to 1496 if you physical interface MTU is 1500.", - Attributes: baseAttributesWith(map[string]schema.Attribute{ - "bridge": schema.StringAttribute{ - Description: "A local, VLAN-aware bridge that is already configured on each local node", - Optional: true, - }, - "service_vlan": schema.Int64Attribute{ - Optional: true, - Description: "Service VLAN tag for QinQ.", - Validators: []validator.Int64{ - int64validator.Between(int64(1), int64(4094)), - }, - }, - "service_vlan_protocol": schema.StringAttribute{ - Optional: true, - Description: "Service VLAN protocol for QinQ.", - Validators: []validator.String{ - stringvalidator.OneOf("802.1ad", "802.1q"), - }, - }, - }), - } -} - -func (r *VXLANResource) Schema( - _ context.Context, - _ resource.SchemaRequest, - resp *resource.SchemaResponse, -) { - resp.Schema = schema.Schema{ - Description: "VXLAN Zone in Proxmox SDN.", - MarkdownDescription: "VXLAN Zone in Proxmox SDN. It establishes a tunnel (overlay) on top of an existing network " + - "(underlay). This encapsulates layer 2 Ethernet frames within layer 4 UDP datagrams using the default " + - "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 " + - "outgoing physical interface.", - Attributes: baseAttributesWith(map[string]schema.Attribute{ - "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. "+ - "This can be external nodes reachable at this IP address. All nodes in the cluster need to be "+ - "mentioned here", - ), - }), - } -} - -func (r *EVPNResource) Schema( - _ context.Context, - _ resource.SchemaRequest, - resp *resource.SchemaResponse, -) { - resp.Schema = schema.Schema{ - Description: "EVPN Zone in Proxmox SDN.", - MarkdownDescription: "EVPN Zone in Proxmox SDN. The EVPN zone creates a routable Layer 3 network, capable of " + - "spanning across multiple clusters.", - Attributes: baseAttributesWith(map[string]schema.Attribute{ - "advertise_subnets": schema.BoolAttribute{ - Optional: true, - Description: "Enable subnet advertisement for EVPN.", - }, - "controller": schema.StringAttribute{ - Optional: true, - Description: "EVPN controller address.", - }, - "disable_arp_nd_suppression": schema.BoolAttribute{ - Optional: true, - Description: "Disable ARP/ND suppression for EVPN.", - }, - "exit_nodes": stringset.ResourceAttribute("List of exit nodes for EVPN.", ""), - "exit_nodes_local_routing": schema.BoolAttribute{ - Optional: true, - Description: "Enable local routing for EVPN exit nodes.", - }, - "primary_exit_node": schema.StringAttribute{ - Optional: true, - Description: "Primary exit node for EVPN.", - }, - "rt_import": schema.StringAttribute{ - Optional: true, - Description: "Route target import for EVPN.", - Validators: []validator.String{ - stringvalidator.RegexMatches( - regexp.MustCompile(`^(\d+):(\d+)$`), - "must be in the format ':' (e.g., '65000:65000')", - ), - }, - }, - "vrf_vxlan": schema.Int64Attribute{ - Optional: true, - Description: "VRF VXLAN-ID used for dedicated routing interconnect between VNets. It must be different " + - "than the VXLAN-ID of the VNets.", - }, - }), - } -} diff --git a/fwprovider/cluster/sdn/zone/resource_simple.go b/fwprovider/cluster/sdn/zone/resource_simple.go index da15af956..d8de0ae57 100644 --- a/fwprovider/cluster/sdn/zone/resource_simple.go +++ b/fwprovider/cluster/sdn/zone/resource_simple.go @@ -8,15 +8,11 @@ package zone import ( "context" - "errors" - "fmt" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "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 ( @@ -24,176 +20,58 @@ var ( _ resource.ResourceWithImportState = &SimpleResource{} ) -type SimpleResource struct { - client *zones.Client -} - -func NewSimpleResource() resource.Resource { - return &SimpleResource{} +type simpleModel struct { + genericModel } -func (r *SimpleResource) Metadata( - _ context.Context, - req resource.MetadataRequest, - resp *resource.MetadataResponse, -) { - resp.TypeName = req.ProviderTypeName + "_sdn_zone_simple" +type SimpleResource struct { + generic *genericZoneResource } -func (r *SimpleResource) 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 +func NewSimpleResource() resource.Resource { + return &SimpleResource{ + generic: newGenericZoneResource(zoneResourceConfig{ + typeNameSuffix: "_sdn_zone_simple", + zoneType: zones.TypeSimple, + modelFunc: func() zoneModel { return &simpleModel{} }, + }).(*genericZoneResource), } - - r.client = cfg.Client.Cluster().SDNZones() } -func (r *SimpleResource) Create( - ctx context.Context, - req resource.CreateRequest, - resp *resource.CreateResponse, -) { - var plan simpleModel - - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) - - if resp.Diagnostics.HasError() { - return +func (r *SimpleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Simple Zone in Proxmox SDN.", + 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. " + + "It can be used in NAT or routed setups.", + Attributes: genericAttributesWith(), } - - reqData := plan.toAPIRequestBody(ctx, &resp.Diagnostics) - reqData.Type = ptr.Ptr(zones.TypeSimple) - - if err := r.client.CreateZone(ctx, reqData); err != nil { - resp.Diagnostics.AddError( - "Unable to Create SDN SimpleZone", - err.Error(), - ) - - return - } - - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } -func (r *SimpleResource) Read( - ctx context.Context, - req resource.ReadRequest, - resp *resource.ReadResponse, -) { - var state simpleModel - - 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 SimpleZone", - err.Error(), - ) - - return - } - - readModel := &baseModel{} - readModel.importFromAPI(zone.ID, zone, &resp.Diagnostics) - resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...) +func (r *SimpleResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.generic.Metadata(ctx, req, resp) } -func (r *SimpleResource) Update( - ctx context.Context, - req resource.UpdateRequest, - resp *resource.UpdateResponse, -) { - var plan simpleModel - - 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 Simple Zone", - err.Error(), - ) - - return - } - - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +func (r *SimpleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.generic.Configure(ctx, req, resp) } -func (r *SimpleResource) Delete( - ctx context.Context, - req resource.DeleteRequest, - resp *resource.DeleteResponse, -) { - var state simpleModel - - 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 Simple Zone", - err.Error(), - ) - } +func (r *SimpleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + r.generic.Create(ctx, req, resp) } -func (r *SimpleResource) 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 - } +func (r *SimpleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + r.generic.Read(ctx, req, resp) +} - resp.Diagnostics.AddError(fmt.Sprintf("Unable to Import SDN Simple Zone %s", req.ID), err.Error()) +func (r *SimpleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + r.generic.Update(ctx, req, resp) +} - return - } +func (r *SimpleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + r.generic.Delete(ctx, req, resp) +} - readModel := &simpleModel{} - readModel.importFromAPI(zone.ID, zone, &resp.Diagnostics) - resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...) +func (r *SimpleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.generic.ImportState(ctx, req, resp) } diff --git a/fwprovider/cluster/sdn/zone/resource_simple_test.go b/fwprovider/cluster/sdn/zone/resource_simple_test.go deleted file mode 100644 index 87bc91985..000000000 --- a/fwprovider/cluster/sdn/zone/resource_simple_test.go +++ /dev/null @@ -1,59 +0,0 @@ -//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 TestAccResourceSDNZoneSimple(t *testing.T) { - t.Parallel() - - te := test.InitEnvironment(t) - - tests := []struct { - name string - steps []resource.TestStep - }{ - {"create and update zones", []resource.TestStep{{ - Config: te.RenderConfig(` - resource "proxmox_virtual_environment_sdn_zone_simple" "zone_simple" { - id = "zoneS" - nodes = ["pve"] - mtu = 1496 - } - `), - }, { - Config: te.RenderConfig(` - resource "proxmox_virtual_environment_sdn_zone_simple" "zone_simple" { - id = "zoneS" - nodes = ["pve"] - mtu = 1495 - } - `), - ResourceName: "proxmox_virtual_environment_sdn_zone_simple.zone_simple", - ImportStateId: "zoneS", - 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, - }) - }) - } -} diff --git a/fwprovider/cluster/sdn/zone/resource_vlan.go b/fwprovider/cluster/sdn/zone/resource_vlan.go index e303f107b..31c10c908 100644 --- a/fwprovider/cluster/sdn/zone/resource_vlan.go +++ b/fwprovider/cluster/sdn/zone/resource_vlan.go @@ -8,15 +8,13 @@ package zone import ( "context" - "errors" - "fmt" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" - "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 ( @@ -24,168 +22,81 @@ var ( _ resource.ResourceWithImportState = &VLANResource{} ) -type VLANResource struct { - client *zones.Client -} - -func NewVLANResource() resource.Resource { - return &VLANResource{} -} +type vlanModel struct { + genericModel -func (r *VLANResource) Metadata( - _ context.Context, - req resource.MetadataRequest, - resp *resource.MetadataResponse, -) { - resp.TypeName = req.ProviderTypeName + "_sdn_zone_vlan" + Bridge types.String `tfsdk:"bridge"` } -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 - } +func (m *vlanModel) importFromAPI(name string, data *zones.ZoneData, diags *diag.Diagnostics) { + m.genericModel.importFromAPI(name, data, diags) - r.client = cfg.Client.Cluster().SDNZones() + m.Bridge = types.StringPointerValue(data.Bridge) } -func (r *VLANResource) Create( - ctx context.Context, - req resource.CreateRequest, - resp *resource.CreateResponse, -) { - var plan vlanModel +func (m *vlanModel) toAPIRequestBody(ctx context.Context, diags *diag.Diagnostics) *zones.ZoneRequestData { + data := m.genericModel.toAPIRequestBody(ctx, diags) - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + data.Bridge = m.Bridge.ValueStringPointer() - 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)...) + return data } -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 - } +type VLANResource struct { + generic *genericZoneResource +} - 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 +func NewVLANResource() resource.Resource { + return &VLANResource{ + generic: newGenericZoneResource(zoneResourceConfig{ + typeNameSuffix: "_sdn_zone_vlan", + zoneType: zones.TypeVLAN, + modelFunc: func() zoneModel { return &vlanModel{} }, + }).(*genericZoneResource), } - - 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 +func (r *VLANResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "VLAN Zone in Proxmox SDN.", + 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. " + + "This allows connectivity of VMs between different nodes.", + Attributes: genericAttributesWith(map[string]schema.Attribute{ + "bridge": schema.StringAttribute{ + Description: "Bridge interface for VLAN.", + MarkdownDescription: "The local bridge or OVS switch, already configured on _each_ node that allows " + + "node-to-node connection.", + Optional: true, + }, + }), } +} - 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 - } +func (r *VLANResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.generic.Metadata(ctx, req, resp) +} - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +func (r *VLANResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.generic.Configure(ctx, req, resp) } -func (r *VLANResource) Delete( - ctx context.Context, - req resource.DeleteRequest, - resp *resource.DeleteResponse, -) { - var state vlanModel +func (r *VLANResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + r.generic.Create(ctx, req, resp) +} - resp.Diagnostics.Append(req.State.Get(ctx, &state)...) +func (r *VLANResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + r.generic.Read(ctx, req, resp) +} - if resp.Diagnostics.HasError() { - return - } +func (r *VLANResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + r.generic.Update(ctx, req, resp) +} - 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) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + r.generic.Delete(ctx, req, resp) } -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)...) +func (r *VLANResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.generic.ImportState(ctx, req, resp) } diff --git a/fwprovider/cluster/sdn/zone/resource_vlan_test.go b/fwprovider/cluster/sdn/zone/resource_vlan_test.go deleted file mode 100644 index dc8b64ebe..000000000 --- a/fwprovider/cluster/sdn/zone/resource_vlan_test.go +++ /dev/null @@ -1,61 +0,0 @@ -//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, - }) - }) - } -} diff --git a/fwprovider/cluster/sdn/zone/resource_vxlan.go b/fwprovider/cluster/sdn/zone/resource_vxlan.go index 703b93206..b0b0abdc7 100644 --- a/fwprovider/cluster/sdn/zone/resource_vxlan.go +++ b/fwprovider/cluster/sdn/zone/resource_vxlan.go @@ -8,15 +8,13 @@ package zone import ( "context" - "errors" - "fmt" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/bpg/terraform-provider-proxmox/fwprovider/config" - "github.com/bpg/terraform-provider-proxmox/proxmox/api" + "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/helpers/ptr" ) var ( @@ -24,168 +22,82 @@ var ( _ resource.ResourceWithImportState = &VXLANResource{} ) -type VXLANResource struct { - client *zones.Client -} +type vxlanModel struct { + genericModel -func NewVXLANResource() resource.Resource { - return &VXLANResource{} + Peers stringset.Value `tfsdk:"peers"` } -func (r *VXLANResource) Metadata( - _ context.Context, - req resource.MetadataRequest, - resp *resource.MetadataResponse, -) { - resp.TypeName = req.ProviderTypeName + "_sdn_zone_vxlan" +func (m *vxlanModel) importFromAPI(name string, data *zones.ZoneData, diags *diag.Diagnostics) { + m.genericModel.importFromAPI(name, data, diags) + m.Peers = stringset.NewValueString(data.Peers, diags, stringset.WithSeparator(",")) } -func (r *VXLANResource) Configure( - _ context.Context, - req resource.ConfigureRequest, - resp *resource.ConfigureResponse, -) { - if req.ProviderData == nil { - return - } +func (m *vxlanModel) toAPIRequestBody(ctx context.Context, diags *diag.Diagnostics) *zones.ZoneRequestData { + data := m.genericModel.toAPIRequestBody(ctx, diags) - 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 - } + data.Peers = m.Peers.ValueStringPointer(ctx, diags, stringset.WithSeparator(",")) - r.client = cfg.Client.Cluster().SDNZones() + return data } -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)...) +type VXLANResource struct { + generic *genericZoneResource } -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 +func NewVXLANResource() resource.Resource { + return &VXLANResource{ + generic: newGenericZoneResource(zoneResourceConfig{ + typeNameSuffix: "_sdn_zone_vxlan", + zoneType: zones.TypeVXLAN, + modelFunc: func() zoneModel { return &vxlanModel{} }, + }).(*genericZoneResource), } - - 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 +func (r *VXLANResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "VXLAN Zone in Proxmox SDN.", + MarkdownDescription: "VXLAN Zone in Proxmox SDN. It establishes a tunnel (overlay) on top of an existing network " + + "(underlay). This encapsulates layer 2 Ethernet frames within layer 4 UDP datagrams using the default " + + "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 " + + "outgoing physical interface.", + Attributes: genericAttributesWith(map[string]schema.Attribute{ + "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. "+ + "This can be external nodes reachable at this IP address. All nodes in the cluster need to be "+ + "mentioned here", + ), + }), } +} - 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 - } +func (r *VXLANResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.generic.Metadata(ctx, req, resp) +} - resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +func (r *VXLANResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.generic.Configure(ctx, req, resp) } -func (r *VXLANResource) Delete( - ctx context.Context, - req resource.DeleteRequest, - resp *resource.DeleteResponse, -) { - var state vxlanModel +func (r *VXLANResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + r.generic.Create(ctx, req, resp) +} - resp.Diagnostics.Append(req.State.Get(ctx, &state)...) +func (r *VXLANResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + r.generic.Read(ctx, req, resp) +} - if resp.Diagnostics.HasError() { - return - } +func (r *VXLANResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + r.generic.Update(ctx, req, resp) +} - 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) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + r.generic.Delete(ctx, req, resp) } -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)...) +func (r *VXLANResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + r.generic.ImportState(ctx, req, resp) } diff --git a/fwprovider/cluster/sdn/zone/resource_vxlan_test.go b/fwprovider/cluster/sdn/zone/resource_vxlan_test.go deleted file mode 100644 index 7bb2f30db..000000000 --- a/fwprovider/cluster/sdn/zone/resource_vxlan_test.go +++ /dev/null @@ -1,61 +0,0 @@ -//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, - }) - }) - } -} diff --git a/fwprovider/cluster/sdn/zone/resource_zones_test.go b/fwprovider/cluster/sdn/zone/resource_zones_test.go new file mode 100644 index 000000000..3caaa64bc --- /dev/null +++ b/fwprovider/cluster/sdn/zone/resource_zones_test.go @@ -0,0 +1,195 @@ +//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 TestAccResourceSDNZoneSimple(t *testing.T) { + t.Parallel() + + te := test.InitEnvironment(t) + + tests := []struct { + name string + steps []resource.TestStep + }{ + {"create and update zones", []resource.TestStep{{ + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_sdn_zone_simple" "zone_simple" { + id = "zoneS" + nodes = ["pve"] + mtu = 1496 + } + `), + }, { + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_sdn_zone_simple" "zone_simple" { + id = "zoneS" + nodes = ["pve"] + mtu = 1495 + } + `), + ResourceName: "proxmox_virtual_environment_sdn_zone_simple.zone_simple", + ImportStateId: "zoneS", + 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, + }) + }) + } +} + +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, + }) + }) + } +} + +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, + }) + }) + } +} + +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, + }) + }) + } +} diff --git a/fwprovider/nodes/network/resource_linux_bridge.go b/fwprovider/nodes/network/resource_linux_bridge.go index b6334e678..19c63209b 100644 --- a/fwprovider/nodes/network/resource_linux_bridge.go +++ b/fwprovider/nodes/network/resource_linux_bridge.go @@ -56,7 +56,6 @@ type linuxBridgeResourceModel struct { VLANAware types.Bool `tfsdk:"vlan_aware"` } -//nolint:lll func (m *linuxBridgeResourceModel) exportToNetworkInterfaceCreateUpdateBody() *nodes.NetworkInterfaceCreateUpdateRequestBody { body := &nodes.NetworkInterfaceCreateUpdateRequestBody{ Iface: m.Name.ValueString(), diff --git a/fwprovider/nodes/network/resource_linux_vlan.go b/fwprovider/nodes/network/resource_linux_vlan.go index f39426bc5..442fbdec6 100644 --- a/fwprovider/nodes/network/resource_linux_vlan.go +++ b/fwprovider/nodes/network/resource_linux_vlan.go @@ -54,7 +54,6 @@ type linuxVLANResourceModel struct { VLAN types.Int64 `tfsdk:"vlan"` } -//nolint:lll func (m *linuxVLANResourceModel) exportToNetworkInterfaceCreateUpdateBody() *nodes.NetworkInterfaceCreateUpdateRequestBody { body := &nodes.NetworkInterfaceCreateUpdateRequestBody{ Iface: m.Name.ValueString(), diff --git a/fwprovider/provider.go b/fwprovider/provider.go index b657c5cce..570981445 100644 --- a/fwprovider/provider.go +++ b/fwprovider/provider.go @@ -529,10 +529,13 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc options.NewClusterOptionsResource, vm.NewResource, sdnzone.NewSimpleResource, + sdnzone.NewVLANResource, + sdnzone.NewQinQResource, + sdnzone.NewVXLANResource, + sdnzone.NewEVPNResource, // - // sdn.NewSDNZoneResource, // sdn.NewSDNVnetResource, - //sdn.NewSDNSubnetResource, + // sdn.NewSDNSubnetResource, } } diff --git a/proxmox/cluster/sdn/zones/api.go b/proxmox/cluster/sdn/zones/api.go new file mode 100644 index 000000000..b61a318a7 --- /dev/null +++ b/proxmox/cluster/sdn/zones/api.go @@ -0,0 +1,19 @@ +/* + * 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 zones + +import ( + "context" +) + +type API interface { + GetZones(ctx context.Context) ([]ZoneData, error) + GetZone(ctx context.Context, id string) (*ZoneData, error) + CreateZone(ctx context.Context, req *ZoneRequestData) error + UpdateZone(ctx context.Context, req *ZoneRequestData) error + DeleteZone(ctx context.Context, id string) error +} From 520e7b45944232561e6eb78157560eafb66f74fd Mon Sep 17 00:00:00 2001 From: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:20:09 -0400 Subject: [PATCH 5/9] more docs Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- .../virtual_environment_sdn_zone_evpn.md | 38 +++++++++++++++++++ .../virtual_environment_sdn_zone_qinq.md | 33 ++++++++++++++++ .../virtual_environment_sdn_zone_vlan.md | 31 +++++++++++++++ .../virtual_environment_sdn_zone_vxlan.md | 31 +++++++++++++++ main.go | 4 ++ 5 files changed, 137 insertions(+) create mode 100644 docs/resources/virtual_environment_sdn_zone_evpn.md create mode 100644 docs/resources/virtual_environment_sdn_zone_qinq.md create mode 100644 docs/resources/virtual_environment_sdn_zone_vlan.md create mode 100644 docs/resources/virtual_environment_sdn_zone_vxlan.md diff --git a/docs/resources/virtual_environment_sdn_zone_evpn.md b/docs/resources/virtual_environment_sdn_zone_evpn.md new file mode 100644 index 000000000..fc2b9efcc --- /dev/null +++ b/docs/resources/virtual_environment_sdn_zone_evpn.md @@ -0,0 +1,38 @@ +--- +layout: page +title: proxmox_virtual_environment_sdn_zone_evpn +parent: Resources +subcategory: Virtual Environment +description: |- + EVPN Zone in Proxmox SDN. The EVPN zone creates a routable Layer 3 network, capable of spanning across multiple clusters. +--- + +# Resource: proxmox_virtual_environment_sdn_zone_evpn + +EVPN Zone in Proxmox SDN. The EVPN zone creates a routable Layer 3 network, capable of spanning across multiple clusters. + + + + +## Schema + +### Required + +- `id` (String) The unique identifier of the SDN zone. + +### Optional + +- `advertise_subnets` (Boolean) Enable subnet advertisement for EVPN. +- `controller` (String) EVPN controller address. +- `disable_arp_nd_suppression` (Boolean) Disable ARP/ND suppression for EVPN. +- `dns` (String) DNS API server address. +- `dns_zone` (String) DNS domain name. Used to register hostnames, such as `.`. The DNS zone must already exist on the DNS server. +- `exit_nodes` (Set of String) List of exit nodes for EVPN. +- `exit_nodes_local_routing` (Boolean) Enable local routing for EVPN exit nodes. +- `ipam` (String) IP Address Management system. +- `mtu` (Number) MTU value for the zone. +- `nodes` (Set of String) Proxmox node names. +- `primary_exit_node` (String) Primary exit node for EVPN. +- `reverse_dns` (String) Reverse DNS API server address. +- `rt_import` (String) Route target import for EVPN. +- `vrf_vxlan` (Number) VRF VXLAN-ID used for dedicated routing interconnect between VNets. It must be different than the VXLAN-ID of the VNets. diff --git a/docs/resources/virtual_environment_sdn_zone_qinq.md b/docs/resources/virtual_environment_sdn_zone_qinq.md new file mode 100644 index 000000000..7cad37b54 --- /dev/null +++ b/docs/resources/virtual_environment_sdn_zone_qinq.md @@ -0,0 +1,33 @@ +--- +layout: page +title: proxmox_virtual_environment_sdn_zone_qinq +parent: Resources +subcategory: Virtual Environment +description: |- + QinQ Zone in Proxmox SDN. QinQ also known as VLAN stacking, that uses multiple layers of VLAN tags for isolation. The QinQ zone defines the outer VLAN tag (the Service VLAN) whereas the inner 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. For example, you must reduce the MTU to 1496 if you physical interface MTU is 1500. +--- + +# Resource: proxmox_virtual_environment_sdn_zone_qinq + +QinQ Zone in Proxmox SDN. QinQ also known as VLAN stacking, that uses multiple layers of VLAN tags for isolation. The QinQ zone defines the outer VLAN tag (the Service VLAN) whereas the inner 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. For example, you must reduce the MTU to 1496 if you physical interface MTU is 1500. + + + + +## Schema + +### Required + +- `id` (String) The unique identifier of the SDN zone. + +### Optional + +- `bridge` (String) A local, VLAN-aware bridge that is already configured on each local node +- `dns` (String) DNS API server address. +- `dns_zone` (String) DNS domain name. Used to register hostnames, such as `.`. The DNS zone must already exist on the DNS server. +- `ipam` (String) IP Address Management system. +- `mtu` (Number) MTU value for the zone. +- `nodes` (Set of String) Proxmox node names. +- `reverse_dns` (String) Reverse DNS API server address. +- `service_vlan` (Number) Service VLAN tag for QinQ. +- `service_vlan_protocol` (String) Service VLAN protocol for QinQ. diff --git a/docs/resources/virtual_environment_sdn_zone_vlan.md b/docs/resources/virtual_environment_sdn_zone_vlan.md new file mode 100644 index 000000000..921f41877 --- /dev/null +++ b/docs/resources/virtual_environment_sdn_zone_vlan.md @@ -0,0 +1,31 @@ +--- +layout: page +title: proxmox_virtual_environment_sdn_zone_vlan +parent: Resources +subcategory: Virtual Environment +description: |- + 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. This allows connectivity of VMs between different nodes. +--- + +# Resource: proxmox_virtual_environment_sdn_zone_vlan + +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. This allows connectivity of VMs between different nodes. + + + + +## Schema + +### Required + +- `id` (String) The unique identifier of the SDN zone. + +### Optional + +- `bridge` (String) The local bridge or OVS switch, already configured on _each_ node that allows node-to-node connection. +- `dns` (String) DNS API server address. +- `dns_zone` (String) DNS domain name. Used to register hostnames, such as `.`. The DNS zone must already exist on the DNS server. +- `ipam` (String) IP Address Management system. +- `mtu` (Number) MTU value for the zone. +- `nodes` (Set of String) Proxmox node names. +- `reverse_dns` (String) Reverse DNS API server address. diff --git a/docs/resources/virtual_environment_sdn_zone_vxlan.md b/docs/resources/virtual_environment_sdn_zone_vxlan.md new file mode 100644 index 000000000..9879f5c39 --- /dev/null +++ b/docs/resources/virtual_environment_sdn_zone_vxlan.md @@ -0,0 +1,31 @@ +--- +layout: page +title: proxmox_virtual_environment_sdn_zone_vxlan +parent: Resources +subcategory: Virtual Environment +description: |- + VXLAN Zone in Proxmox SDN. It establishes a tunnel (overlay) on top of an existing network (underlay). This encapsulates layer 2 Ethernet frames within layer 4 UDP datagrams using the default 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 outgoing physical interface. +--- + +# Resource: proxmox_virtual_environment_sdn_zone_vxlan + +VXLAN Zone in Proxmox SDN. It establishes a tunnel (overlay) on top of an existing network (underlay). This encapsulates layer 2 Ethernet frames within layer 4 UDP datagrams using the default 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 outgoing physical interface. + + + + +## Schema + +### Required + +- `id` (String) The unique identifier of the SDN zone. + +### Optional + +- `dns` (String) DNS API server address. +- `dns_zone` (String) DNS domain name. Used to register hostnames, such as `.`. The DNS zone must already exist on the DNS server. +- `ipam` (String) IP Address Management system. +- `mtu` (Number) MTU value for the zone. +- `nodes` (Set of String) Proxmox node names. +- `peers` (Set of String) A list of IP addresses of each node in the VXLAN zone. This can be external nodes reachable at this IP address. All nodes in the cluster need to be mentioned here +- `reverse_dns` (String) Reverse DNS API server address. diff --git a/main.go b/main.go index c71c3b4ed..ed7742221 100644 --- a/main.go +++ b/main.go @@ -66,6 +66,10 @@ import ( //go:generate cp ./build/docs-gen/resources/virtual_environment_network_linux_bridge.md ./docs/resources/ //go:generate cp ./build/docs-gen/resources/virtual_environment_network_linux_vlan.md ./docs/resources/ //go:generate cp ./build/docs-gen/resources/virtual_environment_sdn_zone_simple.md ./docs/resources/ +//go:generate cp ./build/docs-gen/resources/virtual_environment_sdn_zone_vlan.md ./docs/resources/ +//go:generate cp ./build/docs-gen/resources/virtual_environment_sdn_zone_qinq.md ./docs/resources/ +//go:generate cp ./build/docs-gen/resources/virtual_environment_sdn_zone_vxlan.md ./docs/resources/ +//go:generate cp ./build/docs-gen/resources/virtual_environment_sdn_zone_evpn.md ./docs/resources/ //go:generate cp ./build/docs-gen/resources/virtual_environment_user_token.md ./docs/resources/ //go:generate cp ./build/docs-gen/resources/virtual_environment_vm2.md ./docs/resources/ //go:generate cp ./build/docs-gen/resources/virtual_environment_metrics_server.md ./docs/resources/ From 03203dc4f34a2620700d7d537006d6a47c347548 Mon Sep 17 00:00:00 2001 From: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Date: Wed, 30 Jul 2025 08:05:51 -0400 Subject: [PATCH 6/9] refactor: simplify genericAttributesWith function signature and logic - Updated the genericAttributesWith function to accept a single map of attributes instead of a variadic argument. - Improved the logic to initialize and merge attributes, ensuring that extra attributes can override the generic ones. - Adjusted the SimpleResource schema to call genericAttributesWith with nil for clarity. Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- .../cluster/sdn/zone/resource_generic.go | 22 +++++++++---------- .../cluster/sdn/zone/resource_simple.go | 2 +- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/fwprovider/cluster/sdn/zone/resource_generic.go b/fwprovider/cluster/sdn/zone/resource_generic.go index 3c4e9005c..94efe9100 100644 --- a/fwprovider/cluster/sdn/zone/resource_generic.go +++ b/fwprovider/cluster/sdn/zone/resource_generic.go @@ -68,16 +68,9 @@ func (m *genericModel) getID() string { return m.ID.ValueString() } -func genericAttributesWith(extraAttributes ...map[string]schema.Attribute) map[string]schema.Attribute { - if len(extraAttributes) > 1 { - panic("genericAttributesWith expects at most one extraAttributes map") - } - - if len(extraAttributes) == 0 { - extraAttributes = append(extraAttributes, make(map[string]schema.Attribute)) - } - - maps.Copy(extraAttributes[0], map[string]schema.Attribute{ +func genericAttributesWith(extraAttributes map[string]schema.Attribute) map[string]schema.Attribute { + // Start with generic attributes as the base + result := map[string]schema.Attribute{ "dns": schema.StringAttribute{ Optional: true, Description: "DNS API server address.", @@ -116,9 +109,14 @@ func genericAttributesWith(extraAttributes ...map[string]schema.Attribute) map[s Optional: true, Description: "Reverse DNS API server address.", }, - }) + } + + // Add extra attributes, allowing them to override generic ones if needed + if extraAttributes != nil { + maps.Copy(result, extraAttributes) + } - return extraAttributes[0] + return result } type zoneModel interface { diff --git a/fwprovider/cluster/sdn/zone/resource_simple.go b/fwprovider/cluster/sdn/zone/resource_simple.go index d8de0ae57..71866b7de 100644 --- a/fwprovider/cluster/sdn/zone/resource_simple.go +++ b/fwprovider/cluster/sdn/zone/resource_simple.go @@ -44,7 +44,7 @@ func (r *SimpleResource) Schema(_ context.Context, _ resource.SchemaRequest, res 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. " + "It can be used in NAT or routed setups.", - Attributes: genericAttributesWith(), + Attributes: genericAttributesWith(nil), } } From c63aa98d4b794be653de1b4e4eb70e4144932f00 Mon Sep 17 00:00:00 2001 From: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:57:37 -0400 Subject: [PATCH 7/9] update required vs optional attributes, update docs Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- .../virtual_environment_sdn_zone_evpn.md | 40 +++++++++++++++++-- .../virtual_environment_sdn_zone_qinq.md | 37 ++++++++++++++--- .../virtual_environment_sdn_zone_simple.md | 28 ++++++++++++- .../virtual_environment_sdn_zone_vlan.md | 31 ++++++++++++-- .../virtual_environment_sdn_zone_vxlan.md | 31 ++++++++++++-- .../import.sh | 3 ++ .../resource.tf | 21 ++++++++++ .../import.sh | 3 ++ .../resource.tf | 14 +++++++ .../import.sh | 3 ++ .../resource.tf | 11 +++++ .../import.sh | 3 ++ .../resource.tf | 12 ++++++ .../import.sh | 3 ++ .../resource.tf | 12 ++++++ fwprovider/cluster/sdn/zone/resource_evpn.go | 14 +++---- .../cluster/sdn/zone/resource_generic.go | 2 +- fwprovider/cluster/sdn/zone/resource_qinq.go | 12 +++--- fwprovider/cluster/sdn/zone/resource_vlan.go | 2 +- fwprovider/cluster/sdn/zone/resource_vxlan.go | 1 + fwprovider/types/stringset/attribute.go | 29 ++++++++++++-- 21 files changed, 279 insertions(+), 33 deletions(-) create mode 100644 examples/resources/proxmox_virtual_environment_sdn_zone_evpn/import.sh create mode 100644 examples/resources/proxmox_virtual_environment_sdn_zone_evpn/resource.tf create mode 100644 examples/resources/proxmox_virtual_environment_sdn_zone_qinq/import.sh create mode 100644 examples/resources/proxmox_virtual_environment_sdn_zone_qinq/resource.tf create mode 100644 examples/resources/proxmox_virtual_environment_sdn_zone_simple/import.sh create mode 100644 examples/resources/proxmox_virtual_environment_sdn_zone_simple/resource.tf create mode 100644 examples/resources/proxmox_virtual_environment_sdn_zone_vlan/import.sh create mode 100644 examples/resources/proxmox_virtual_environment_sdn_zone_vlan/resource.tf create mode 100644 examples/resources/proxmox_virtual_environment_sdn_zone_vxlan/import.sh create mode 100644 examples/resources/proxmox_virtual_environment_sdn_zone_vxlan/resource.tf diff --git a/docs/resources/virtual_environment_sdn_zone_evpn.md b/docs/resources/virtual_environment_sdn_zone_evpn.md index fc2b9efcc..1b94d6372 100644 --- a/docs/resources/virtual_environment_sdn_zone_evpn.md +++ b/docs/resources/virtual_environment_sdn_zone_evpn.md @@ -11,19 +11,45 @@ description: |- EVPN Zone in Proxmox SDN. The EVPN zone creates a routable Layer 3 network, capable of spanning across multiple clusters. +## Example Usage +```terraform +resource "proxmox_virtual_environment_sdn_zone_evpn" "example" { + id = "evpn1" + nodes = ["pve"] + controller = "evpn-controller1" + vrf_vxlan = 4000 + + # Optional attributes + advertise_subnets = true + disable_arp_nd_suppression = false + exit_nodes = ["pve-exit1", "pve-exit2"] + exit_nodes_local_routing = true + primary_exit_node = "pve-exit1" + rt_import = "65000:65000" + mtu = 1450 + + # Generic optional attributes + dns = "1.1.1.1" + dns_zone = "example.com" + ipam = "pve" + reverse_dns = "1.1.1.1" +} +``` ## Schema ### Required +- `controller` (String) EVPN controller address. - `id` (String) The unique identifier of the SDN zone. +- `nodes` (Set of String) The Proxmox nodes which the zone and associated VNets should be deployed on +- `vrf_vxlan` (Number) VRF VXLAN-ID used for dedicated routing interconnect between VNets. It must be different than the VXLAN-ID of the VNets. ### Optional - `advertise_subnets` (Boolean) Enable subnet advertisement for EVPN. -- `controller` (String) EVPN controller address. - `disable_arp_nd_suppression` (Boolean) Disable ARP/ND suppression for EVPN. - `dns` (String) DNS API server address. - `dns_zone` (String) DNS domain name. Used to register hostnames, such as `.`. The DNS zone must already exist on the DNS server. @@ -31,8 +57,16 @@ EVPN Zone in Proxmox SDN. The EVPN zone creates a routable Layer 3 network, capa - `exit_nodes_local_routing` (Boolean) Enable local routing for EVPN exit nodes. - `ipam` (String) IP Address Management system. - `mtu` (Number) MTU value for the zone. -- `nodes` (Set of String) Proxmox node names. - `primary_exit_node` (String) Primary exit node for EVPN. - `reverse_dns` (String) Reverse DNS API server address. - `rt_import` (String) Route target import for EVPN. -- `vrf_vxlan` (Number) VRF VXLAN-ID used for dedicated routing interconnect between VNets. It must be different than the VXLAN-ID of the VNets. + +## Import + +Import is supported using the following syntax: + +```shell +#!/usr/bin/env sh +# EVPN SDN zone can be imported using its unique identifier (zone ID) +terraform import proxmox_virtual_environment_sdn_zone_evpn.example evpn1 +``` diff --git a/docs/resources/virtual_environment_sdn_zone_qinq.md b/docs/resources/virtual_environment_sdn_zone_qinq.md index 7cad37b54..7b72b785d 100644 --- a/docs/resources/virtual_environment_sdn_zone_qinq.md +++ b/docs/resources/virtual_environment_sdn_zone_qinq.md @@ -11,23 +11,50 @@ description: |- QinQ Zone in Proxmox SDN. QinQ also known as VLAN stacking, that uses multiple layers of VLAN tags for isolation. The QinQ zone defines the outer VLAN tag (the Service VLAN) whereas the inner 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. For example, you must reduce the MTU to 1496 if you physical interface MTU is 1500. - +## Example Usage + +```terraform +resource "proxmox_virtual_environment_sdn_zone_qinq" "example" { + id = "qinq1" + nodes = ["pve"] + bridge = "vmbr0" + service_vlan = 100 + service_vlan_protocol = "802.1ad" + mtu = 1496 + + # Optional attributes + dns = "1.1.1.1" + dns_zone = "example.com" + ipam = "pve" + reverse_dns = "1.1.1.1" +} +``` ## Schema ### Required +- `bridge` (String) A local, VLAN-aware bridge that is already configured on each local node - `id` (String) The unique identifier of the SDN zone. +- `nodes` (Set of String) The Proxmox nodes which the zone and associated VNets should be deployed on +- `service_vlan` (Number) Service VLAN tag for QinQ. The tag must be between `1` and `4094`. ### Optional -- `bridge` (String) A local, VLAN-aware bridge that is already configured on each local node - `dns` (String) DNS API server address. - `dns_zone` (String) DNS domain name. Used to register hostnames, such as `.`. The DNS zone must already exist on the DNS server. - `ipam` (String) IP Address Management system. - `mtu` (Number) MTU value for the zone. -- `nodes` (Set of String) Proxmox node names. - `reverse_dns` (String) Reverse DNS API server address. -- `service_vlan` (Number) Service VLAN tag for QinQ. -- `service_vlan_protocol` (String) Service VLAN protocol for QinQ. +- `service_vlan_protocol` (String) Service VLAN protocol for QinQ. The protocol must be `802.1ad` or `802.1q`. + +## Import + +Import is supported using the following syntax: + +```shell +#!/usr/bin/env sh +# QinQ SDN zone can be imported using its unique identifier (zone ID) +terraform import proxmox_virtual_environment_sdn_zone_qinq.example qinq1 +``` diff --git a/docs/resources/virtual_environment_sdn_zone_simple.md b/docs/resources/virtual_environment_sdn_zone_simple.md index ab922e64b..92600448a 100644 --- a/docs/resources/virtual_environment_sdn_zone_simple.md +++ b/docs/resources/virtual_environment_sdn_zone_simple.md @@ -11,7 +11,21 @@ description: |- 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. It can be used in NAT or routed setups. - +## Example Usage + +```terraform +resource "proxmox_virtual_environment_sdn_zone_simple" "example" { + id = "simple1" + nodes = ["pve"] + mtu = 1500 + + # Optional attributes + dns = "1.1.1.1" + dns_zone = "example.com" + ipam = "pve" + reverse_dns = "1.1.1.1" +} +``` ## Schema @@ -19,6 +33,7 @@ Simple Zone in Proxmox SDN. It will create an isolated VNet bridge. This bridge ### Required - `id` (String) The unique identifier of the SDN zone. +- `nodes` (Set of String) The Proxmox nodes which the zone and associated VNets should be deployed on ### Optional @@ -26,5 +41,14 @@ Simple Zone in Proxmox SDN. It will create an isolated VNet bridge. This bridge - `dns_zone` (String) DNS domain name. Used to register hostnames, such as `.`. The DNS zone must already exist on the DNS server. - `ipam` (String) IP Address Management system. - `mtu` (Number) MTU value for the zone. -- `nodes` (Set of String) Proxmox node names. - `reverse_dns` (String) Reverse DNS API server address. + +## Import + +Import is supported using the following syntax: + +```shell +#!/usr/bin/env sh +# Simple SDN zone can be imported using its unique identifier (zone ID) +terraform import proxmox_virtual_environment_sdn_zone_simple.example simple1 +``` diff --git a/docs/resources/virtual_environment_sdn_zone_vlan.md b/docs/resources/virtual_environment_sdn_zone_vlan.md index 921f41877..3cb0b5eb0 100644 --- a/docs/resources/virtual_environment_sdn_zone_vlan.md +++ b/docs/resources/virtual_environment_sdn_zone_vlan.md @@ -11,21 +11,46 @@ description: |- 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. This allows connectivity of VMs between different nodes. - +## Example Usage + +```terraform +resource "proxmox_virtual_environment_sdn_zone_vlan" "example" { + id = "vlan1" + nodes = ["pve"] + bridge = "vmbr0" + mtu = 1500 + + # Optional attributes + dns = "1.1.1.1" + dns_zone = "example.com" + ipam = "pve" + reverse_dns = "1.1.1.1" +} +``` ## Schema ### Required +- `bridge` (String) The local bridge or OVS switch, already configured on _each_ node that allows node-to-node connection. - `id` (String) The unique identifier of the SDN zone. +- `nodes` (Set of String) The Proxmox nodes which the zone and associated VNets should be deployed on ### Optional -- `bridge` (String) The local bridge or OVS switch, already configured on _each_ node that allows node-to-node connection. - `dns` (String) DNS API server address. - `dns_zone` (String) DNS domain name. Used to register hostnames, such as `.`. The DNS zone must already exist on the DNS server. - `ipam` (String) IP Address Management system. - `mtu` (Number) MTU value for the zone. -- `nodes` (Set of String) Proxmox node names. - `reverse_dns` (String) Reverse DNS API server address. + +## Import + +Import is supported using the following syntax: + +```shell +#!/usr/bin/env sh +# VLAN SDN zone can be imported using its unique identifier (zone ID) +terraform import proxmox_virtual_environment_sdn_zone_vlan.example vlan1 +``` diff --git a/docs/resources/virtual_environment_sdn_zone_vxlan.md b/docs/resources/virtual_environment_sdn_zone_vxlan.md index 9879f5c39..630f2a3a8 100644 --- a/docs/resources/virtual_environment_sdn_zone_vxlan.md +++ b/docs/resources/virtual_environment_sdn_zone_vxlan.md @@ -11,7 +11,22 @@ description: |- VXLAN Zone in Proxmox SDN. It establishes a tunnel (overlay) on top of an existing network (underlay). This encapsulates layer 2 Ethernet frames within layer 4 UDP datagrams using the default 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 outgoing physical interface. - +## Example Usage + +```terraform +resource "proxmox_virtual_environment_sdn_zone_vxlan" "example" { + id = "vxlan1" + nodes = ["pve"] + peers = ["10.0.0.1", "10.0.0.2", "10.0.0.3"] + mtu = 1450 + + # Optional attributes + dns = "1.1.1.1" + dns_zone = "example.com" + ipam = "pve" + reverse_dns = "1.1.1.1" +} +``` ## Schema @@ -19,6 +34,8 @@ VXLAN Zone in Proxmox SDN. It establishes a tunnel (overlay) on top of an existi ### Required - `id` (String) The unique identifier of the SDN zone. +- `nodes` (Set of String) The Proxmox nodes which the zone and associated VNets should be deployed on +- `peers` (Set of String) A list of IP addresses of each node in the VXLAN zone. This can be external nodes reachable at this IP address. All nodes in the cluster need to be mentioned here ### Optional @@ -26,6 +43,14 @@ VXLAN Zone in Proxmox SDN. It establishes a tunnel (overlay) on top of an existi - `dns_zone` (String) DNS domain name. Used to register hostnames, such as `.`. The DNS zone must already exist on the DNS server. - `ipam` (String) IP Address Management system. - `mtu` (Number) MTU value for the zone. -- `nodes` (Set of String) Proxmox node names. -- `peers` (Set of String) A list of IP addresses of each node in the VXLAN zone. This can be external nodes reachable at this IP address. All nodes in the cluster need to be mentioned here - `reverse_dns` (String) Reverse DNS API server address. + +## Import + +Import is supported using the following syntax: + +```shell +#!/usr/bin/env sh +# VXLAN SDN zone can be imported using its unique identifier (zone ID) +terraform import proxmox_virtual_environment_sdn_zone_vxlan.example vxlan1 +``` diff --git a/examples/resources/proxmox_virtual_environment_sdn_zone_evpn/import.sh b/examples/resources/proxmox_virtual_environment_sdn_zone_evpn/import.sh new file mode 100644 index 000000000..77ec54de1 --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_sdn_zone_evpn/import.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +# EVPN SDN zone can be imported using its unique identifier (zone ID) +terraform import proxmox_virtual_environment_sdn_zone_evpn.example evpn1 diff --git a/examples/resources/proxmox_virtual_environment_sdn_zone_evpn/resource.tf b/examples/resources/proxmox_virtual_environment_sdn_zone_evpn/resource.tf new file mode 100644 index 000000000..1993be2bc --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_sdn_zone_evpn/resource.tf @@ -0,0 +1,21 @@ +resource "proxmox_virtual_environment_sdn_zone_evpn" "example" { + id = "evpn1" + nodes = ["pve"] + controller = "evpn-controller1" + vrf_vxlan = 4000 + + # Optional attributes + advertise_subnets = true + disable_arp_nd_suppression = false + exit_nodes = ["pve-exit1", "pve-exit2"] + exit_nodes_local_routing = true + primary_exit_node = "pve-exit1" + rt_import = "65000:65000" + mtu = 1450 + + # Generic optional attributes + dns = "1.1.1.1" + dns_zone = "example.com" + ipam = "pve" + reverse_dns = "1.1.1.1" +} diff --git a/examples/resources/proxmox_virtual_environment_sdn_zone_qinq/import.sh b/examples/resources/proxmox_virtual_environment_sdn_zone_qinq/import.sh new file mode 100644 index 000000000..ee146ff57 --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_sdn_zone_qinq/import.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +# QinQ SDN zone can be imported using its unique identifier (zone ID) +terraform import proxmox_virtual_environment_sdn_zone_qinq.example qinq1 diff --git a/examples/resources/proxmox_virtual_environment_sdn_zone_qinq/resource.tf b/examples/resources/proxmox_virtual_environment_sdn_zone_qinq/resource.tf new file mode 100644 index 000000000..3b5f1998d --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_sdn_zone_qinq/resource.tf @@ -0,0 +1,14 @@ +resource "proxmox_virtual_environment_sdn_zone_qinq" "example" { + id = "qinq1" + nodes = ["pve"] + bridge = "vmbr0" + service_vlan = 100 + service_vlan_protocol = "802.1ad" + mtu = 1496 + + # Optional attributes + dns = "1.1.1.1" + dns_zone = "example.com" + ipam = "pve" + reverse_dns = "1.1.1.1" +} diff --git a/examples/resources/proxmox_virtual_environment_sdn_zone_simple/import.sh b/examples/resources/proxmox_virtual_environment_sdn_zone_simple/import.sh new file mode 100644 index 000000000..72b735c0a --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_sdn_zone_simple/import.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +# Simple SDN zone can be imported using its unique identifier (zone ID) +terraform import proxmox_virtual_environment_sdn_zone_simple.example simple1 diff --git a/examples/resources/proxmox_virtual_environment_sdn_zone_simple/resource.tf b/examples/resources/proxmox_virtual_environment_sdn_zone_simple/resource.tf new file mode 100644 index 000000000..ebe019e2a --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_sdn_zone_simple/resource.tf @@ -0,0 +1,11 @@ +resource "proxmox_virtual_environment_sdn_zone_simple" "example" { + id = "simple1" + nodes = ["pve"] + mtu = 1500 + + # Optional attributes + dns = "1.1.1.1" + dns_zone = "example.com" + ipam = "pve" + reverse_dns = "1.1.1.1" +} diff --git a/examples/resources/proxmox_virtual_environment_sdn_zone_vlan/import.sh b/examples/resources/proxmox_virtual_environment_sdn_zone_vlan/import.sh new file mode 100644 index 000000000..72dd74cdb --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_sdn_zone_vlan/import.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +# VLAN SDN zone can be imported using its unique identifier (zone ID) +terraform import proxmox_virtual_environment_sdn_zone_vlan.example vlan1 diff --git a/examples/resources/proxmox_virtual_environment_sdn_zone_vlan/resource.tf b/examples/resources/proxmox_virtual_environment_sdn_zone_vlan/resource.tf new file mode 100644 index 000000000..1cd733da6 --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_sdn_zone_vlan/resource.tf @@ -0,0 +1,12 @@ +resource "proxmox_virtual_environment_sdn_zone_vlan" "example" { + id = "vlan1" + nodes = ["pve"] + bridge = "vmbr0" + mtu = 1500 + + # Optional attributes + dns = "1.1.1.1" + dns_zone = "example.com" + ipam = "pve" + reverse_dns = "1.1.1.1" +} diff --git a/examples/resources/proxmox_virtual_environment_sdn_zone_vxlan/import.sh b/examples/resources/proxmox_virtual_environment_sdn_zone_vxlan/import.sh new file mode 100644 index 000000000..fb9cad685 --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_sdn_zone_vxlan/import.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +# VXLAN SDN zone can be imported using its unique identifier (zone ID) +terraform import proxmox_virtual_environment_sdn_zone_vxlan.example vxlan1 diff --git a/examples/resources/proxmox_virtual_environment_sdn_zone_vxlan/resource.tf b/examples/resources/proxmox_virtual_environment_sdn_zone_vxlan/resource.tf new file mode 100644 index 000000000..833397bb5 --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_sdn_zone_vxlan/resource.tf @@ -0,0 +1,12 @@ +resource "proxmox_virtual_environment_sdn_zone_vxlan" "example" { + id = "vxlan1" + nodes = ["pve"] + peers = ["10.0.0.1", "10.0.0.2", "10.0.0.3"] + mtu = 1450 + + # Optional attributes + dns = "1.1.1.1" + dns_zone = "example.com" + ipam = "pve" + reverse_dns = "1.1.1.1" +} diff --git a/fwprovider/cluster/sdn/zone/resource_evpn.go b/fwprovider/cluster/sdn/zone/resource_evpn.go index f753bae3b..fff5f8e92 100644 --- a/fwprovider/cluster/sdn/zone/resource_evpn.go +++ b/fwprovider/cluster/sdn/zone/resource_evpn.go @@ -90,28 +90,27 @@ func (r *EVPNResource) Schema(_ context.Context, _ resource.SchemaRequest, resp "spanning across multiple clusters.", Attributes: genericAttributesWith(map[string]schema.Attribute{ "advertise_subnets": schema.BoolAttribute{ - Optional: true, Description: "Enable subnet advertisement for EVPN.", + Optional: true, }, "controller": schema.StringAttribute{ - Optional: true, Description: "EVPN controller address.", + Required: true, }, "disable_arp_nd_suppression": schema.BoolAttribute{ - Optional: true, Description: "Disable ARP/ND suppression for EVPN.", + Optional: true, }, "exit_nodes": stringset.ResourceAttribute("List of exit nodes for EVPN.", ""), "exit_nodes_local_routing": schema.BoolAttribute{ - Optional: true, Description: "Enable local routing for EVPN exit nodes.", + Optional: true, }, "primary_exit_node": schema.StringAttribute{ - Optional: true, Description: "Primary exit node for EVPN.", + Optional: true, }, "rt_import": schema.StringAttribute{ - Optional: true, Description: "Route target import for EVPN.", Validators: []validator.String{ stringvalidator.RegexMatches( @@ -119,11 +118,12 @@ func (r *EVPNResource) Schema(_ context.Context, _ resource.SchemaRequest, resp "must be in the format ':' (e.g., '65000:65000')", ), }, + Optional: true, }, "vrf_vxlan": schema.Int64Attribute{ - Optional: true, Description: "VRF VXLAN-ID used for dedicated routing interconnect between VNets. It must be different " + "than the VXLAN-ID of the VNets.", + Required: true, }, }), } diff --git a/fwprovider/cluster/sdn/zone/resource_generic.go b/fwprovider/cluster/sdn/zone/resource_generic.go index 94efe9100..3ade630ad 100644 --- a/fwprovider/cluster/sdn/zone/resource_generic.go +++ b/fwprovider/cluster/sdn/zone/resource_generic.go @@ -104,7 +104,7 @@ func genericAttributesWith(extraAttributes map[string]schema.Attribute) map[stri Optional: true, Description: "MTU value for the zone.", }, - "nodes": stringset.ResourceAttribute("Proxmox node names.", ""), + "nodes": stringset.ResourceAttribute("The Proxmox nodes which the zone and associated VNets should be deployed on", "", stringset.WithRequired()), "reverse_dns": schema.StringAttribute{ Optional: true, Description: "Reverse DNS API server address.", diff --git a/fwprovider/cluster/sdn/zone/resource_qinq.go b/fwprovider/cluster/sdn/zone/resource_qinq.go index 04e083d6a..ca89398d4 100644 --- a/fwprovider/cluster/sdn/zone/resource_qinq.go +++ b/fwprovider/cluster/sdn/zone/resource_qinq.go @@ -73,21 +73,23 @@ func (r *QinQResource) Schema(_ context.Context, _ resource.SchemaRequest, resp Attributes: genericAttributesWith(map[string]schema.Attribute{ "bridge": schema.StringAttribute{ Description: "A local, VLAN-aware bridge that is already configured on each local node", - Optional: true, + Required: true, }, "service_vlan": schema.Int64Attribute{ - Optional: true, - Description: "Service VLAN tag for QinQ.", + Description: "Service VLAN tag for QinQ.", + MarkdownDescription: "Service VLAN tag for QinQ. The tag must be between `1` and `4094`.", Validators: []validator.Int64{ int64validator.Between(int64(1), int64(4094)), }, + Required: true, }, "service_vlan_protocol": schema.StringAttribute{ - Optional: true, - Description: "Service VLAN protocol for QinQ.", + Description: "Service VLAN protocol for QinQ.", + MarkdownDescription: "Service VLAN protocol for QinQ. The protocol must be `802.1ad` or `802.1q`.", Validators: []validator.String{ stringvalidator.OneOf("802.1ad", "802.1q"), }, + Optional: true, }, }), } diff --git a/fwprovider/cluster/sdn/zone/resource_vlan.go b/fwprovider/cluster/sdn/zone/resource_vlan.go index 31c10c908..c3f61312b 100644 --- a/fwprovider/cluster/sdn/zone/resource_vlan.go +++ b/fwprovider/cluster/sdn/zone/resource_vlan.go @@ -67,7 +67,7 @@ func (r *VLANResource) Schema(_ context.Context, _ resource.SchemaRequest, resp Description: "Bridge interface for VLAN.", MarkdownDescription: "The local bridge or OVS switch, already configured on _each_ node that allows " + "node-to-node connection.", - Optional: true, + Required: true, }, }), } diff --git a/fwprovider/cluster/sdn/zone/resource_vxlan.go b/fwprovider/cluster/sdn/zone/resource_vxlan.go index b0b0abdc7..824d43837 100644 --- a/fwprovider/cluster/sdn/zone/resource_vxlan.go +++ b/fwprovider/cluster/sdn/zone/resource_vxlan.go @@ -69,6 +69,7 @@ func (r *VXLANResource) Schema(_ context.Context, _ resource.SchemaRequest, resp "A list of IP addresses of each node in the VXLAN zone. "+ "This can be external nodes reachable at this IP address. All nodes in the cluster need to be "+ "mentioned here", + stringset.WithRequired(), ), }), } diff --git a/fwprovider/types/stringset/attribute.go b/fwprovider/types/stringset/attribute.go index ca7f89d75..2b5a2ba6c 100644 --- a/fwprovider/types/stringset/attribute.go +++ b/fwprovider/types/stringset/attribute.go @@ -16,9 +16,27 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) +type ResourceAttributeOption func(*schema.SetAttribute) + +func WithRequired() ResourceAttributeOption { + return func(attribute *schema.SetAttribute) { + attribute.Required = true + attribute.Optional = false + attribute.Computed = false + } +} + +func WithOptional() ResourceAttributeOption { + return func(attribute *schema.SetAttribute) { + attribute.Optional = true + attribute.Required = false + attribute.Computed = true + } +} + // ResourceAttribute returns a resource schema attribute for string set. -func ResourceAttribute(desc, markdownDesc string) schema.SetAttribute { - return schema.SetAttribute{ +func ResourceAttribute(desc, markdownDesc string, options ...ResourceAttributeOption) schema.SetAttribute { + attribute := schema.SetAttribute{ CustomType: Type{ SetType: types.SetType{ ElemType: types.StringType, @@ -30,7 +48,6 @@ func ResourceAttribute(desc, markdownDesc string) schema.SetAttribute { Computed: true, ElementType: types.StringType, Validators: []validator.Set{ - // NOTE: we allow empty list to remove all previously set values setvalidator.ValueStringsAre( stringvalidator.RegexMatches( regexp.MustCompile(`(.|\s)*\S(.|\s)*`), @@ -40,6 +57,12 @@ func ResourceAttribute(desc, markdownDesc string) schema.SetAttribute { ), }, } + + for _, option := range options { + option(&attribute) + } + + return attribute } // DataSourceAttribute returns a data source schema attribute for string set. From 67a548104746065595d0e7fc90e3b019dc43af1e Mon Sep 17 00:00:00 2001 From: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Date: Wed, 30 Jul 2025 22:42:33 -0400 Subject: [PATCH 8/9] add datasources Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- .../virtual_environment_sdn_zone_evpn.md | 64 ++++ .../virtual_environment_sdn_zone_qinq.md | 54 ++++ .../virtual_environment_sdn_zone_simple.md | 48 +++ .../virtual_environment_sdn_zone_vlan.md | 50 ++++ .../virtual_environment_sdn_zone_vxlan.md | 50 ++++ .../virtual_environment_sdn_zones.md | 79 +++++ .../data-source.tf | 23 ++ .../data-source.tf | 18 ++ .../data-source.tf | 15 + .../data-source.tf | 16 + .../data-source.tf | 16 + .../data-source.tf | 25 ++ .../cluster/sdn/zone/datasource_evpn.go | 99 +++++++ .../cluster/sdn/zone/datasource_generic.go | 164 ++++++++++ .../cluster/sdn/zone/datasource_qinq.go | 74 +++++ .../cluster/sdn/zone/datasource_simple.go | 57 ++++ .../cluster/sdn/zone/datasource_vlan.go | 64 ++++ .../cluster/sdn/zone/datasource_vxlan.go | 75 +++++ .../cluster/sdn/zone/datasource_zones.go | 279 ++++++++++++++++++ .../cluster/sdn/zone/datasource_zones_test.go | 259 ++++++++++++++++ fwprovider/provider.go | 12 +- main.go | 6 + .../data-source.tf | 23 ++ .../data-source.tf | 18 ++ .../data-source.tf | 15 + .../data-source.tf | 16 + .../data-source.tf | 16 + .../data-source.tf | 25 ++ 28 files changed, 1654 insertions(+), 6 deletions(-) create mode 100644 docs/data-sources/virtual_environment_sdn_zone_evpn.md create mode 100644 docs/data-sources/virtual_environment_sdn_zone_qinq.md create mode 100644 docs/data-sources/virtual_environment_sdn_zone_simple.md create mode 100644 docs/data-sources/virtual_environment_sdn_zone_vlan.md create mode 100644 docs/data-sources/virtual_environment_sdn_zone_vxlan.md create mode 100644 docs/data-sources/virtual_environment_sdn_zones.md create mode 100644 examples/data-sources/proxmox_virtual_environment_sdn_zone_evpn/data-source.tf create mode 100644 examples/data-sources/proxmox_virtual_environment_sdn_zone_qinq/data-source.tf create mode 100644 examples/data-sources/proxmox_virtual_environment_sdn_zone_simple/data-source.tf create mode 100644 examples/data-sources/proxmox_virtual_environment_sdn_zone_vlan/data-source.tf create mode 100644 examples/data-sources/proxmox_virtual_environment_sdn_zone_vxlan/data-source.tf create mode 100644 examples/data-sources/proxmox_virtual_environment_sdn_zones/data-source.tf create mode 100644 fwprovider/cluster/sdn/zone/datasource_evpn.go create mode 100644 fwprovider/cluster/sdn/zone/datasource_generic.go create mode 100644 fwprovider/cluster/sdn/zone/datasource_qinq.go create mode 100644 fwprovider/cluster/sdn/zone/datasource_simple.go create mode 100644 fwprovider/cluster/sdn/zone/datasource_vlan.go create mode 100644 fwprovider/cluster/sdn/zone/datasource_vxlan.go create mode 100644 fwprovider/cluster/sdn/zone/datasource_zones.go create mode 100644 fwprovider/cluster/sdn/zone/datasource_zones_test.go create mode 100644 proxmox_virtual_environment_sdn_zone_evpn/data-source.tf create mode 100644 proxmox_virtual_environment_sdn_zone_qinq/data-source.tf create mode 100644 proxmox_virtual_environment_sdn_zone_simple/data-source.tf create mode 100644 proxmox_virtual_environment_sdn_zone_vlan/data-source.tf create mode 100644 proxmox_virtual_environment_sdn_zone_vxlan/data-source.tf create mode 100644 proxmox_virtual_environment_sdn_zones/data-source.tf diff --git a/docs/data-sources/virtual_environment_sdn_zone_evpn.md b/docs/data-sources/virtual_environment_sdn_zone_evpn.md new file mode 100644 index 000000000..14ed2a7a3 --- /dev/null +++ b/docs/data-sources/virtual_environment_sdn_zone_evpn.md @@ -0,0 +1,64 @@ +--- +layout: page +title: proxmox_virtual_environment_sdn_zone_evpn +parent: Data Sources +subcategory: Virtual Environment +description: |- + Retrieves information about an EVPN Zone in Proxmox SDN. The EVPN zone creates a routable Layer 3 network, capable of spanning across multiple clusters. +--- + +# Data Source: proxmox_virtual_environment_sdn_zone_evpn + +Retrieves information about an EVPN Zone in Proxmox SDN. The EVPN zone creates a routable Layer 3 network, capable of spanning across multiple clusters. + +## Example Usage + +```terraform +data "proxmox_virtual_environment_sdn_zone_evpn" "example" { + id = "evpn1" +} + +output "data_proxmox_virtual_environment_sdn_zone_evpn" { + value = { + id = data.proxmox_virtual_environment_sdn_zone_evpn.example.id + nodes = data.proxmox_virtual_environment_sdn_zone_evpn.example.nodes + controller = data.proxmox_virtual_environment_sdn_zone_evpn.example.controller + vrf_vxlan = data.proxmox_virtual_environment_sdn_zone_evpn.example.vrf_vxlan + advertise_subnets = data.proxmox_virtual_environment_sdn_zone_evpn.example.advertise_subnets + disable_arp_nd_suppression = data.proxmox_virtual_environment_sdn_zone_evpn.example.disable_arp_nd_suppression + exit_nodes = data.proxmox_virtual_environment_sdn_zone_evpn.example.exit_nodes + exit_nodes_local_routing = data.proxmox_virtual_environment_sdn_zone_evpn.example.exit_nodes_local_routing + primary_exit_node = data.proxmox_virtual_environment_sdn_zone_evpn.example.primary_exit_node + rt_import = data.proxmox_virtual_environment_sdn_zone_evpn.example.rt_import + mtu = data.proxmox_virtual_environment_sdn_zone_evpn.example.mtu + dns = data.proxmox_virtual_environment_sdn_zone_evpn.example.dns + dns_zone = data.proxmox_virtual_environment_sdn_zone_evpn.example.dns_zone + ipam = data.proxmox_virtual_environment_sdn_zone_evpn.example.ipam + reverse_dns = data.proxmox_virtual_environment_sdn_zone_evpn.example.reverse_dns + } +} +``` + + +## Schema + +### Required + +- `id` (String) The unique identifier of the SDN zone. + +### Read-Only + +- `advertise_subnets` (Boolean) Enable subnet advertisement for EVPN. +- `controller` (String) EVPN controller address. +- `disable_arp_nd_suppression` (Boolean) Disable ARP/ND suppression for EVPN. +- `dns` (String) DNS API server address. +- `dns_zone` (String) DNS domain name. Used to register hostnames, such as `.`. The DNS zone must already exist on the DNS server. +- `exit_nodes` (Set of String) List of exit nodes for EVPN. +- `exit_nodes_local_routing` (Boolean) Enable local routing for EVPN exit nodes. +- `ipam` (String) IP Address Management system. +- `mtu` (Number) MTU value for the zone. +- `nodes` (Set of String) The Proxmox nodes which the zone and associated VNets are deployed on +- `primary_exit_node` (String) Primary exit node for EVPN. +- `reverse_dns` (String) Reverse DNS API server address. +- `rt_import` (String) Route target import for EVPN. Must be in the format ':' (e.g., '65000:65000'). +- `vrf_vxlan` (Number) VRF VXLAN-ID used for dedicated routing interconnect between VNets. It must be different than the VXLAN-ID of the VNets. diff --git a/docs/data-sources/virtual_environment_sdn_zone_qinq.md b/docs/data-sources/virtual_environment_sdn_zone_qinq.md new file mode 100644 index 000000000..b3ad6ebaa --- /dev/null +++ b/docs/data-sources/virtual_environment_sdn_zone_qinq.md @@ -0,0 +1,54 @@ +--- +layout: page +title: proxmox_virtual_environment_sdn_zone_qinq +parent: Data Sources +subcategory: Virtual Environment +description: |- + Retrieves information about a QinQ Zone in Proxmox SDN. QinQ also known as VLAN stacking, that uses multiple layers of VLAN tags for isolation. The QinQ zone defines the outer VLAN tag (the Service VLAN) whereas the inner 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. For example, you must reduce the MTU to 1496 if you physical interface MTU is 1500. +--- + +# Data Source: proxmox_virtual_environment_sdn_zone_qinq + +Retrieves information about a QinQ Zone in Proxmox SDN. QinQ also known as VLAN stacking, that uses multiple layers of VLAN tags for isolation. The QinQ zone defines the outer VLAN tag (the Service VLAN) whereas the inner 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. For example, you must reduce the MTU to 1496 if you physical interface MTU is 1500. + +## Example Usage + +```terraform +data "proxmox_virtual_environment_sdn_zone_qinq" "example" { + id = "qinq1" +} + +output "data_proxmox_virtual_environment_sdn_zone_qinq" { + value = { + id = data.proxmox_virtual_environment_sdn_zone_qinq.example.id + nodes = data.proxmox_virtual_environment_sdn_zone_qinq.example.nodes + bridge = data.proxmox_virtual_environment_sdn_zone_qinq.example.bridge + service_vlan = data.proxmox_virtual_environment_sdn_zone_qinq.example.service_vlan + service_vlan_protocol = data.proxmox_virtual_environment_sdn_zone_qinq.example.service_vlan_protocol + mtu = data.proxmox_virtual_environment_sdn_zone_qinq.example.mtu + dns = data.proxmox_virtual_environment_sdn_zone_qinq.example.dns + dns_zone = data.proxmox_virtual_environment_sdn_zone_qinq.example.dns_zone + ipam = data.proxmox_virtual_environment_sdn_zone_qinq.example.ipam + reverse_dns = data.proxmox_virtual_environment_sdn_zone_qinq.example.reverse_dns + } +} +``` + + +## Schema + +### Required + +- `id` (String) The unique identifier of the SDN zone. + +### Read-Only + +- `bridge` (String) A local, VLAN-aware bridge that is already configured on each local node +- `dns` (String) DNS API server address. +- `dns_zone` (String) DNS domain name. Used to register hostnames, such as `.`. The DNS zone must already exist on the DNS server. +- `ipam` (String) IP Address Management system. +- `mtu` (Number) MTU value for the zone. +- `nodes` (Set of String) The Proxmox nodes which the zone and associated VNets are deployed on +- `reverse_dns` (String) Reverse DNS API server address. +- `service_vlan` (Number) Service VLAN tag for QinQ. The tag must be between `1` and `4094`. +- `service_vlan_protocol` (String) Service VLAN protocol for QinQ. The protocol must be `802.1ad` or `802.1q`. diff --git a/docs/data-sources/virtual_environment_sdn_zone_simple.md b/docs/data-sources/virtual_environment_sdn_zone_simple.md new file mode 100644 index 000000000..79382c0e0 --- /dev/null +++ b/docs/data-sources/virtual_environment_sdn_zone_simple.md @@ -0,0 +1,48 @@ +--- +layout: page +title: proxmox_virtual_environment_sdn_zone_simple +parent: Data Sources +subcategory: Virtual Environment +description: |- + Retrieves information about a 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. It can be used in NAT or routed setups. +--- + +# Data Source: proxmox_virtual_environment_sdn_zone_simple + +Retrieves information about a 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. It can be used in NAT or routed setups. + +## Example Usage + +```terraform +data "proxmox_virtual_environment_sdn_zone_simple" "example" { + id = "simple1" +} + +output "data_proxmox_virtual_environment_sdn_zone_simple" { + value = { + id = data.proxmox_virtual_environment_sdn_zone_simple.example.id + nodes = data.proxmox_virtual_environment_sdn_zone_simple.example.nodes + mtu = data.proxmox_virtual_environment_sdn_zone_simple.example.mtu + dns = data.proxmox_virtual_environment_sdn_zone_simple.example.dns + dns_zone = data.proxmox_virtual_environment_sdn_zone_simple.example.dns_zone + ipam = data.proxmox_virtual_environment_sdn_zone_simple.example.ipam + reverse_dns = data.proxmox_virtual_environment_sdn_zone_simple.example.reverse_dns + } +} +``` + + +## Schema + +### Required + +- `id` (String) The unique identifier of the SDN zone. + +### Read-Only + +- `dns` (String) DNS API server address. +- `dns_zone` (String) DNS domain name. Used to register hostnames, such as `.`. The DNS zone must already exist on the DNS server. +- `ipam` (String) IP Address Management system. +- `mtu` (Number) MTU value for the zone. +- `nodes` (Set of String) The Proxmox nodes which the zone and associated VNets are deployed on +- `reverse_dns` (String) Reverse DNS API server address. diff --git a/docs/data-sources/virtual_environment_sdn_zone_vlan.md b/docs/data-sources/virtual_environment_sdn_zone_vlan.md new file mode 100644 index 000000000..83377df2c --- /dev/null +++ b/docs/data-sources/virtual_environment_sdn_zone_vlan.md @@ -0,0 +1,50 @@ +--- +layout: page +title: proxmox_virtual_environment_sdn_zone_vlan +parent: Data Sources +subcategory: Virtual Environment +description: |- + Retrieves information about a 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. This allows connectivity of VMs between different nodes. +--- + +# Data Source: proxmox_virtual_environment_sdn_zone_vlan + +Retrieves information about a 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. This allows connectivity of VMs between different nodes. + +## Example Usage + +```terraform +data "proxmox_virtual_environment_sdn_zone_vlan" "example" { + id = "vlan1" +} + +output "data_proxmox_virtual_environment_sdn_zone_vlan" { + value = { + id = data.proxmox_virtual_environment_sdn_zone_vlan.example.id + nodes = data.proxmox_virtual_environment_sdn_zone_vlan.example.nodes + bridge = data.proxmox_virtual_environment_sdn_zone_vlan.example.bridge + mtu = data.proxmox_virtual_environment_sdn_zone_vlan.example.mtu + dns = data.proxmox_virtual_environment_sdn_zone_vlan.example.dns + dns_zone = data.proxmox_virtual_environment_sdn_zone_vlan.example.dns_zone + ipam = data.proxmox_virtual_environment_sdn_zone_vlan.example.ipam + reverse_dns = data.proxmox_virtual_environment_sdn_zone_vlan.example.reverse_dns + } +} +``` + + +## Schema + +### Required + +- `id` (String) The unique identifier of the SDN zone. + +### Read-Only + +- `bridge` (String) The local bridge or OVS switch, already configured on _each_ node that allows node-to-node connection. +- `dns` (String) DNS API server address. +- `dns_zone` (String) DNS domain name. Used to register hostnames, such as `.`. The DNS zone must already exist on the DNS server. +- `ipam` (String) IP Address Management system. +- `mtu` (Number) MTU value for the zone. +- `nodes` (Set of String) The Proxmox nodes which the zone and associated VNets are deployed on +- `reverse_dns` (String) Reverse DNS API server address. diff --git a/docs/data-sources/virtual_environment_sdn_zone_vxlan.md b/docs/data-sources/virtual_environment_sdn_zone_vxlan.md new file mode 100644 index 000000000..a4346333c --- /dev/null +++ b/docs/data-sources/virtual_environment_sdn_zone_vxlan.md @@ -0,0 +1,50 @@ +--- +layout: page +title: proxmox_virtual_environment_sdn_zone_vxlan +parent: Data Sources +subcategory: Virtual Environment +description: |- + Retrieves information about a VXLAN Zone in Proxmox SDN. It establishes a tunnel (overlay) on top of an existing network (underlay). This encapsulates layer 2 Ethernet frames within layer 4 UDP datagrams using the default 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 outgoing physical interface. +--- + +# Data Source: proxmox_virtual_environment_sdn_zone_vxlan + +Retrieves information about a VXLAN Zone in Proxmox SDN. It establishes a tunnel (overlay) on top of an existing network (underlay). This encapsulates layer 2 Ethernet frames within layer 4 UDP datagrams using the default 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 outgoing physical interface. + +## Example Usage + +```terraform +data "proxmox_virtual_environment_sdn_zone_vxlan" "example" { + id = "vxlan1" +} + +output "data_proxmox_virtual_environment_sdn_zone_vxlan" { + value = { + id = data.proxmox_virtual_environment_sdn_zone_vxlan.example.id + nodes = data.proxmox_virtual_environment_sdn_zone_vxlan.example.nodes + peers = data.proxmox_virtual_environment_sdn_zone_vxlan.example.peers + mtu = data.proxmox_virtual_environment_sdn_zone_vxlan.example.mtu + dns = data.proxmox_virtual_environment_sdn_zone_vxlan.example.dns + dns_zone = data.proxmox_virtual_environment_sdn_zone_vxlan.example.dns_zone + ipam = data.proxmox_virtual_environment_sdn_zone_vxlan.example.ipam + reverse_dns = data.proxmox_virtual_environment_sdn_zone_vxlan.example.reverse_dns + } +} +``` + + +## Schema + +### Required + +- `id` (String) The unique identifier of the SDN zone. + +### Read-Only + +- `dns` (String) DNS API server address. +- `dns_zone` (String) DNS domain name. Used to register hostnames, such as `.`. The DNS zone must already exist on the DNS server. +- `ipam` (String) IP Address Management system. +- `mtu` (Number) MTU value for the zone. +- `nodes` (Set of String) The Proxmox nodes which the zone and associated VNets are deployed on +- `peers` (Set of String) A list of IP addresses of each node in the VXLAN zone. This can be external nodes reachable at this IP address. All nodes in the cluster need to be mentioned here +- `reverse_dns` (String) Reverse DNS API server address. diff --git a/docs/data-sources/virtual_environment_sdn_zones.md b/docs/data-sources/virtual_environment_sdn_zones.md new file mode 100644 index 000000000..2997d01b0 --- /dev/null +++ b/docs/data-sources/virtual_environment_sdn_zones.md @@ -0,0 +1,79 @@ +--- +layout: page +title: proxmox_virtual_environment_sdn_zones +parent: Data Sources +subcategory: Virtual Environment +description: |- + Retrieves information about all SDN Zones in Proxmox. This data source can optionally filter zones by type. +--- + +# Data Source: proxmox_virtual_environment_sdn_zones + +Retrieves information about all SDN Zones in Proxmox. This data source can optionally filter zones by type. + +## Example Usage + +```terraform +# List all SDN zones +data "proxmox_virtual_environment_sdn_zones" "all" {} + +# List only EVPN zones +data "proxmox_virtual_environment_sdn_zones" "evpn_only" { + type = "evpn" +} + +# List only Simple zones +data "proxmox_virtual_environment_sdn_zones" "simple_only" { + type = "simple" +} + +output "data_proxmox_virtual_environment_sdn_zones_all" { + value = { + zones = data.proxmox_virtual_environment_sdn_zones.all.zones + } +} + +output "data_proxmox_virtual_environment_sdn_zones_filtered" { + value = { + evpn_zones = data.proxmox_virtual_environment_sdn_zones.evpn_only.zones + simple_zones = data.proxmox_virtual_environment_sdn_zones.simple_only.zones + } +} +``` + + +## Schema + +### Optional + +- `type` (String) Filter zones by type (simple, vlan, qinq, vxlan, evpn). + +### Read-Only + +- `zones` (List of Object) List of SDN zones. (see [below for nested schema](#nestedatt--zones)) + + +### Nested Schema for `zones` + +Read-Only: + +- `advertise_subnets` (Boolean) +- `bridge` (String) +- `controller` (String) +- `disable_arp_nd_suppression` (Boolean) +- `dns` (String) +- `dns_zone` (String) +- `exit_nodes` (Set of String) +- `exit_nodes_local_routing` (Boolean) +- `id` (String) +- `ipam` (String) +- `mtu` (Number) +- `nodes` (Set of String) +- `peers` (Set of String) +- `primary_exit_node` (String) +- `reverse_dns` (String) +- `rt_import` (String) +- `service_vlan` (Number) +- `service_vlan_protocol` (String) +- `type` (String) +- `vrf_vxlan` (Number) diff --git a/examples/data-sources/proxmox_virtual_environment_sdn_zone_evpn/data-source.tf b/examples/data-sources/proxmox_virtual_environment_sdn_zone_evpn/data-source.tf new file mode 100644 index 000000000..2b70ab68d --- /dev/null +++ b/examples/data-sources/proxmox_virtual_environment_sdn_zone_evpn/data-source.tf @@ -0,0 +1,23 @@ +data "proxmox_virtual_environment_sdn_zone_evpn" "example" { + id = "evpn1" +} + +output "data_proxmox_virtual_environment_sdn_zone_evpn" { + value = { + id = data.proxmox_virtual_environment_sdn_zone_evpn.example.id + nodes = data.proxmox_virtual_environment_sdn_zone_evpn.example.nodes + controller = data.proxmox_virtual_environment_sdn_zone_evpn.example.controller + vrf_vxlan = data.proxmox_virtual_environment_sdn_zone_evpn.example.vrf_vxlan + advertise_subnets = data.proxmox_virtual_environment_sdn_zone_evpn.example.advertise_subnets + disable_arp_nd_suppression = data.proxmox_virtual_environment_sdn_zone_evpn.example.disable_arp_nd_suppression + exit_nodes = data.proxmox_virtual_environment_sdn_zone_evpn.example.exit_nodes + exit_nodes_local_routing = data.proxmox_virtual_environment_sdn_zone_evpn.example.exit_nodes_local_routing + primary_exit_node = data.proxmox_virtual_environment_sdn_zone_evpn.example.primary_exit_node + rt_import = data.proxmox_virtual_environment_sdn_zone_evpn.example.rt_import + mtu = data.proxmox_virtual_environment_sdn_zone_evpn.example.mtu + dns = data.proxmox_virtual_environment_sdn_zone_evpn.example.dns + dns_zone = data.proxmox_virtual_environment_sdn_zone_evpn.example.dns_zone + ipam = data.proxmox_virtual_environment_sdn_zone_evpn.example.ipam + reverse_dns = data.proxmox_virtual_environment_sdn_zone_evpn.example.reverse_dns + } +} diff --git a/examples/data-sources/proxmox_virtual_environment_sdn_zone_qinq/data-source.tf b/examples/data-sources/proxmox_virtual_environment_sdn_zone_qinq/data-source.tf new file mode 100644 index 000000000..26f448c1a --- /dev/null +++ b/examples/data-sources/proxmox_virtual_environment_sdn_zone_qinq/data-source.tf @@ -0,0 +1,18 @@ +data "proxmox_virtual_environment_sdn_zone_qinq" "example" { + id = "qinq1" +} + +output "data_proxmox_virtual_environment_sdn_zone_qinq" { + value = { + id = data.proxmox_virtual_environment_sdn_zone_qinq.example.id + nodes = data.proxmox_virtual_environment_sdn_zone_qinq.example.nodes + bridge = data.proxmox_virtual_environment_sdn_zone_qinq.example.bridge + service_vlan = data.proxmox_virtual_environment_sdn_zone_qinq.example.service_vlan + service_vlan_protocol = data.proxmox_virtual_environment_sdn_zone_qinq.example.service_vlan_protocol + mtu = data.proxmox_virtual_environment_sdn_zone_qinq.example.mtu + dns = data.proxmox_virtual_environment_sdn_zone_qinq.example.dns + dns_zone = data.proxmox_virtual_environment_sdn_zone_qinq.example.dns_zone + ipam = data.proxmox_virtual_environment_sdn_zone_qinq.example.ipam + reverse_dns = data.proxmox_virtual_environment_sdn_zone_qinq.example.reverse_dns + } +} diff --git a/examples/data-sources/proxmox_virtual_environment_sdn_zone_simple/data-source.tf b/examples/data-sources/proxmox_virtual_environment_sdn_zone_simple/data-source.tf new file mode 100644 index 000000000..452cb88fc --- /dev/null +++ b/examples/data-sources/proxmox_virtual_environment_sdn_zone_simple/data-source.tf @@ -0,0 +1,15 @@ +data "proxmox_virtual_environment_sdn_zone_simple" "example" { + id = "simple1" +} + +output "data_proxmox_virtual_environment_sdn_zone_simple" { + value = { + id = data.proxmox_virtual_environment_sdn_zone_simple.example.id + nodes = data.proxmox_virtual_environment_sdn_zone_simple.example.nodes + mtu = data.proxmox_virtual_environment_sdn_zone_simple.example.mtu + dns = data.proxmox_virtual_environment_sdn_zone_simple.example.dns + dns_zone = data.proxmox_virtual_environment_sdn_zone_simple.example.dns_zone + ipam = data.proxmox_virtual_environment_sdn_zone_simple.example.ipam + reverse_dns = data.proxmox_virtual_environment_sdn_zone_simple.example.reverse_dns + } +} diff --git a/examples/data-sources/proxmox_virtual_environment_sdn_zone_vlan/data-source.tf b/examples/data-sources/proxmox_virtual_environment_sdn_zone_vlan/data-source.tf new file mode 100644 index 000000000..a1f0f9e4d --- /dev/null +++ b/examples/data-sources/proxmox_virtual_environment_sdn_zone_vlan/data-source.tf @@ -0,0 +1,16 @@ +data "proxmox_virtual_environment_sdn_zone_vlan" "example" { + id = "vlan1" +} + +output "data_proxmox_virtual_environment_sdn_zone_vlan" { + value = { + id = data.proxmox_virtual_environment_sdn_zone_vlan.example.id + nodes = data.proxmox_virtual_environment_sdn_zone_vlan.example.nodes + bridge = data.proxmox_virtual_environment_sdn_zone_vlan.example.bridge + mtu = data.proxmox_virtual_environment_sdn_zone_vlan.example.mtu + dns = data.proxmox_virtual_environment_sdn_zone_vlan.example.dns + dns_zone = data.proxmox_virtual_environment_sdn_zone_vlan.example.dns_zone + ipam = data.proxmox_virtual_environment_sdn_zone_vlan.example.ipam + reverse_dns = data.proxmox_virtual_environment_sdn_zone_vlan.example.reverse_dns + } +} diff --git a/examples/data-sources/proxmox_virtual_environment_sdn_zone_vxlan/data-source.tf b/examples/data-sources/proxmox_virtual_environment_sdn_zone_vxlan/data-source.tf new file mode 100644 index 000000000..2ca035e49 --- /dev/null +++ b/examples/data-sources/proxmox_virtual_environment_sdn_zone_vxlan/data-source.tf @@ -0,0 +1,16 @@ +data "proxmox_virtual_environment_sdn_zone_vxlan" "example" { + id = "vxlan1" +} + +output "data_proxmox_virtual_environment_sdn_zone_vxlan" { + value = { + id = data.proxmox_virtual_environment_sdn_zone_vxlan.example.id + nodes = data.proxmox_virtual_environment_sdn_zone_vxlan.example.nodes + peers = data.proxmox_virtual_environment_sdn_zone_vxlan.example.peers + mtu = data.proxmox_virtual_environment_sdn_zone_vxlan.example.mtu + dns = data.proxmox_virtual_environment_sdn_zone_vxlan.example.dns + dns_zone = data.proxmox_virtual_environment_sdn_zone_vxlan.example.dns_zone + ipam = data.proxmox_virtual_environment_sdn_zone_vxlan.example.ipam + reverse_dns = data.proxmox_virtual_environment_sdn_zone_vxlan.example.reverse_dns + } +} diff --git a/examples/data-sources/proxmox_virtual_environment_sdn_zones/data-source.tf b/examples/data-sources/proxmox_virtual_environment_sdn_zones/data-source.tf new file mode 100644 index 000000000..a84abd4c5 --- /dev/null +++ b/examples/data-sources/proxmox_virtual_environment_sdn_zones/data-source.tf @@ -0,0 +1,25 @@ +# List all SDN zones +data "proxmox_virtual_environment_sdn_zones" "all" {} + +# List only EVPN zones +data "proxmox_virtual_environment_sdn_zones" "evpn_only" { + type = "evpn" +} + +# List only Simple zones +data "proxmox_virtual_environment_sdn_zones" "simple_only" { + type = "simple" +} + +output "data_proxmox_virtual_environment_sdn_zones_all" { + value = { + zones = data.proxmox_virtual_environment_sdn_zones.all.zones + } +} + +output "data_proxmox_virtual_environment_sdn_zones_filtered" { + value = { + evpn_zones = data.proxmox_virtual_environment_sdn_zones.evpn_only.zones + simple_zones = data.proxmox_virtual_environment_sdn_zones.simple_only.zones + } +} diff --git a/fwprovider/cluster/sdn/zone/datasource_evpn.go b/fwprovider/cluster/sdn/zone/datasource_evpn.go new file mode 100644 index 000000000..c504360a1 --- /dev/null +++ b/fwprovider/cluster/sdn/zone/datasource_evpn.go @@ -0,0 +1,99 @@ +/* + * 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" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/bpg/terraform-provider-proxmox/fwprovider/types/stringset" + "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones" +) + +var ( + _ datasource.DataSource = &EVPNDataSource{} + _ datasource.DataSourceWithConfigure = &EVPNDataSource{} +) + +type EVPNDataSource struct { + generic *genericZoneDataSource +} + +func NewEVPNDataSource() datasource.DataSource { + return &EVPNDataSource{ + generic: newGenericZoneDataSource(zoneDataSourceConfig{ + typeNameSuffix: "_sdn_zone_evpn", + zoneType: zones.TypeEVPN, + modelFunc: func() zoneModel { return &evpnModel{} }, + }), + } +} + +func (d *EVPNDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Retrieves information about an EVPN Zone in Proxmox SDN.", + MarkdownDescription: "Retrieves information about an EVPN Zone in Proxmox SDN. The EVPN zone creates a routable Layer 3 network, capable of " + + "spanning across multiple clusters.", + Attributes: genericDataSourceAttributesWith(map[string]schema.Attribute{ + "advertise_subnets": schema.BoolAttribute{ + Description: "Enable subnet advertisement for EVPN.", + Computed: true, + }, + "controller": schema.StringAttribute{ + Description: "EVPN controller address.", + Computed: true, + }, + "disable_arp_nd_suppression": schema.BoolAttribute{ + Description: "Disable ARP/ND suppression for EVPN.", + Computed: true, + }, + "exit_nodes": schema.SetAttribute{ + CustomType: stringset.Type{ + SetType: types.SetType{ + ElemType: types.StringType, + }, + }, + Description: "List of exit nodes for EVPN.", + ElementType: types.StringType, + Computed: true, + }, + "exit_nodes_local_routing": schema.BoolAttribute{ + Description: "Enable local routing for EVPN exit nodes.", + Computed: true, + }, + "primary_exit_node": schema.StringAttribute{ + Description: "Primary exit node for EVPN.", + Computed: true, + }, + "rt_import": schema.StringAttribute{ + Description: "Route target import for EVPN.", + MarkdownDescription: "Route target import for EVPN. Must be in the format ':' (e.g., '65000:65000').", + Computed: true, + }, + "vrf_vxlan": schema.Int64Attribute{ + Description: "VRF VXLAN-ID used for dedicated routing interconnect between VNets. It must be different " + + "than the VXLAN-ID of the VNets.", + Computed: true, + }, + }), + } +} + +func (d *EVPNDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + d.generic.Metadata(ctx, req, resp) +} + +func (d *EVPNDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + d.generic.Configure(ctx, req, resp) +} + +func (d *EVPNDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + d.generic.Read(ctx, req, resp) +} diff --git a/fwprovider/cluster/sdn/zone/datasource_generic.go b/fwprovider/cluster/sdn/zone/datasource_generic.go new file mode 100644 index 000000000..3547ed87e --- /dev/null +++ b/fwprovider/cluster/sdn/zone/datasource_generic.go @@ -0,0 +1,164 @@ +/* + * 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" + "maps" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/bpg/terraform-provider-proxmox/fwprovider/config" + "github.com/bpg/terraform-provider-proxmox/fwprovider/types/stringset" + "github.com/bpg/terraform-provider-proxmox/proxmox/api" + "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones" +) + +type zoneDataSourceConfig struct { + typeNameSuffix string + zoneType string + modelFunc func() zoneModel +} + +type genericZoneDataSource struct { + client *zones.Client + config zoneDataSourceConfig +} + +func newGenericZoneDataSource(cfg zoneDataSourceConfig) *genericZoneDataSource { + return &genericZoneDataSource{config: cfg} +} + +func (d *genericZoneDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + d.config.typeNameSuffix +} + +func (d *genericZoneDataSource) Configure( + _ context.Context, + req datasource.ConfigureRequest, + resp *datasource.ConfigureResponse, +) { + if req.ProviderData == nil { + return + } + + cfg, ok := req.ProviderData.(config.DataSource) + if !ok { + resp.Diagnostics.AddError( + "Unexpected DataSource Configure Type", + fmt.Sprintf( + "Expected config.DataSource, got: %T", + req.ProviderData, + ), + ) + + return + } + + d.client = cfg.Client.Cluster().SDNZones() +} + +func genericDataSourceAttributesWith(extraAttributes map[string]schema.Attribute) map[string]schema.Attribute { + // Start with generic attributes as the base + result := map[string]schema.Attribute{ + "dns": schema.StringAttribute{ + Computed: true, + Description: "DNS API server address.", + }, + "dns_zone": schema.StringAttribute{ + Computed: true, + Description: "DNS domain name. The DNS zone must already exist on the DNS server.", + MarkdownDescription: "DNS domain name. Used to register hostnames, such as `.`. " + + "The DNS zone must already exist on the DNS server.", + }, + "id": schema.StringAttribute{ + Description: "The unique identifier of the SDN zone.", + Required: true, + }, + "ipam": schema.StringAttribute{ + Computed: true, + Description: "IP Address Management system.", + }, + "mtu": schema.Int64Attribute{ + Computed: true, + Description: "MTU value for the zone.", + }, + "nodes": schema.SetAttribute{ + CustomType: stringset.Type{ + SetType: types.SetType{ + ElemType: types.StringType, + }, + }, + Description: "The Proxmox nodes which the zone and associated VNets are deployed on", + ElementType: types.StringType, + Computed: true, + }, + "reverse_dns": schema.StringAttribute{ + Computed: true, + Description: "Reverse DNS API server address.", + }, + } + + // Add extra attributes, allowing them to override generic ones if needed + if extraAttributes != nil { + maps.Copy(result, extraAttributes) + } + + return result +} + +func (d *genericZoneDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + state := d.config.modelFunc() + resp.Diagnostics.Append(req.Config.Get(ctx, state)...) + + if resp.Diagnostics.HasError() { + return + } + + zone, err := d.client.GetZone(ctx, state.getID()) + if err != nil { + if errors.Is(err, api.ErrResourceDoesNotExist) { + resp.Diagnostics.AddError( + "SDN Zone Not Found", + fmt.Sprintf("SDN zone with ID '%s' was not found", state.getID()), + ) + return + } + + resp.Diagnostics.AddError( + "Unable to Read SDN Zone", + err.Error(), + ) + + return + } + + // Verify the zone type matches what this datasource expects + if zone.Type != nil && *zone.Type != d.config.zoneType { + resp.Diagnostics.AddError( + "SDN Zone Type Mismatch", + fmt.Sprintf( + "Expected zone type '%s' but found '%s' for zone '%s'", + d.config.zoneType, + *zone.Type, + zone.ID, + ), + ) + return + } + + readModel := d.config.modelFunc() + diags := &diag.Diagnostics{} + readModel.importFromAPI(zone.ID, zone, diags) + resp.Diagnostics.Append(*diags...) + resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...) +} diff --git a/fwprovider/cluster/sdn/zone/datasource_qinq.go b/fwprovider/cluster/sdn/zone/datasource_qinq.go new file mode 100644 index 000000000..2060e5738 --- /dev/null +++ b/fwprovider/cluster/sdn/zone/datasource_qinq.go @@ -0,0 +1,74 @@ +/* + * 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" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + + "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones" +) + +var ( + _ datasource.DataSource = &QinQDataSource{} + _ datasource.DataSourceWithConfigure = &QinQDataSource{} +) + +type QinQDataSource struct { + generic *genericZoneDataSource +} + +func NewQinQDataSource() datasource.DataSource { + return &QinQDataSource{ + generic: newGenericZoneDataSource(zoneDataSourceConfig{ + typeNameSuffix: "_sdn_zone_qinq", + zoneType: zones.TypeQinQ, + modelFunc: func() zoneModel { return &qinqModel{} }, + }), + } +} + +func (d *QinQDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Retrieves information about a QinQ Zone in Proxmox SDN.", + MarkdownDescription: "Retrieves information about a QinQ Zone in Proxmox SDN. QinQ also known as VLAN stacking, that uses multiple layers of " + + "VLAN tags for isolation. The QinQ zone defines the outer VLAN tag (the Service VLAN) whereas the inner " + + "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. " + + "For example, you must reduce the MTU to 1496 if you physical interface MTU is 1500.", + Attributes: genericDataSourceAttributesWith(map[string]schema.Attribute{ + "bridge": schema.StringAttribute{ + Description: "A local, VLAN-aware bridge that is already configured on each local node", + Computed: true, + }, + "service_vlan": schema.Int64Attribute{ + Description: "Service VLAN tag for QinQ.", + MarkdownDescription: "Service VLAN tag for QinQ. The tag must be between `1` and `4094`.", + Computed: true, + }, + "service_vlan_protocol": schema.StringAttribute{ + Description: "Service VLAN protocol for QinQ.", + MarkdownDescription: "Service VLAN protocol for QinQ. The protocol must be `802.1ad` or `802.1q`.", + Computed: true, + }, + }), + } +} + +func (d *QinQDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + d.generic.Metadata(ctx, req, resp) +} + +func (d *QinQDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + d.generic.Configure(ctx, req, resp) +} + +func (d *QinQDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + d.generic.Read(ctx, req, resp) +} diff --git a/fwprovider/cluster/sdn/zone/datasource_simple.go b/fwprovider/cluster/sdn/zone/datasource_simple.go new file mode 100644 index 000000000..04a718c3b --- /dev/null +++ b/fwprovider/cluster/sdn/zone/datasource_simple.go @@ -0,0 +1,57 @@ +/* + * 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" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + + "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones" +) + +var ( + _ datasource.DataSource = &SimpleDataSource{} + _ datasource.DataSourceWithConfigure = &SimpleDataSource{} +) + +type SimpleDataSource struct { + generic *genericZoneDataSource +} + +func NewSimpleDataSource() datasource.DataSource { + return &SimpleDataSource{ + generic: newGenericZoneDataSource(zoneDataSourceConfig{ + typeNameSuffix: "_sdn_zone_simple", + zoneType: zones.TypeSimple, + modelFunc: func() zoneModel { return &simpleModel{} }, + }), + } +} + +func (d *SimpleDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Retrieves information about a Simple Zone in Proxmox SDN.", + MarkdownDescription: "Retrieves information about a 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. " + + "It can be used in NAT or routed setups.", + Attributes: genericDataSourceAttributesWith(nil), + } +} + +func (d *SimpleDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + d.generic.Metadata(ctx, req, resp) +} + +func (d *SimpleDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + d.generic.Configure(ctx, req, resp) +} + +func (d *SimpleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + d.generic.Read(ctx, req, resp) +} diff --git a/fwprovider/cluster/sdn/zone/datasource_vlan.go b/fwprovider/cluster/sdn/zone/datasource_vlan.go new file mode 100644 index 000000000..7cb466f92 --- /dev/null +++ b/fwprovider/cluster/sdn/zone/datasource_vlan.go @@ -0,0 +1,64 @@ +/* + * 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" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + + "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones" +) + +var ( + _ datasource.DataSource = &VLANDataSource{} + _ datasource.DataSourceWithConfigure = &VLANDataSource{} +) + +type VLANDataSource struct { + generic *genericZoneDataSource +} + +func NewVLANDataSource() datasource.DataSource { + return &VLANDataSource{ + generic: newGenericZoneDataSource(zoneDataSourceConfig{ + typeNameSuffix: "_sdn_zone_vlan", + zoneType: zones.TypeVLAN, + modelFunc: func() zoneModel { return &vlanModel{} }, + }), + } +} + +func (d *VLANDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Retrieves information about a VLAN Zone in Proxmox SDN.", + MarkdownDescription: "Retrieves information about a 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. " + + "This allows connectivity of VMs between different nodes.", + Attributes: genericDataSourceAttributesWith(map[string]schema.Attribute{ + "bridge": schema.StringAttribute{ + Description: "Bridge interface for VLAN.", + MarkdownDescription: "The local bridge or OVS switch, already configured on _each_ node that allows " + + "node-to-node connection.", + Computed: true, + }, + }), + } +} + +func (d *VLANDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + d.generic.Metadata(ctx, req, resp) +} + +func (d *VLANDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + d.generic.Configure(ctx, req, resp) +} + +func (d *VLANDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + d.generic.Read(ctx, req, resp) +} diff --git a/fwprovider/cluster/sdn/zone/datasource_vxlan.go b/fwprovider/cluster/sdn/zone/datasource_vxlan.go new file mode 100644 index 000000000..5c8d6b3ca --- /dev/null +++ b/fwprovider/cluster/sdn/zone/datasource_vxlan.go @@ -0,0 +1,75 @@ +/* + * 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" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/bpg/terraform-provider-proxmox/fwprovider/types/stringset" + "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones" +) + +var ( + _ datasource.DataSource = &VXLANDataSource{} + _ datasource.DataSourceWithConfigure = &VXLANDataSource{} +) + +type VXLANDataSource struct { + generic *genericZoneDataSource +} + +func NewVXLANDataSource() datasource.DataSource { + return &VXLANDataSource{ + generic: newGenericZoneDataSource(zoneDataSourceConfig{ + typeNameSuffix: "_sdn_zone_vxlan", + zoneType: zones.TypeVXLAN, + modelFunc: func() zoneModel { return &vxlanModel{} }, + }), + } +} + +func (d *VXLANDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Retrieves information about a VXLAN Zone in Proxmox SDN.", + MarkdownDescription: "Retrieves information about a VXLAN Zone in Proxmox SDN. It establishes a tunnel (overlay) on top of an existing network " + + "(underlay). This encapsulates layer 2 Ethernet frames within layer 4 UDP datagrams using the default " + + "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 " + + "outgoing physical interface.", + Attributes: genericDataSourceAttributesWith(map[string]schema.Attribute{ + "peers": schema.SetAttribute{ + CustomType: stringset.Type{ + SetType: types.SetType{ + ElemType: types.StringType, + }, + }, + Description: "A list of IP addresses of each node in the VXLAN zone.", + MarkdownDescription: "A list of IP addresses of each node in the VXLAN zone. " + + "This can be external nodes reachable at this IP address. All nodes in the cluster need to be " + + "mentioned here", + ElementType: types.StringType, + Computed: true, + }, + }), + } +} + +func (d *VXLANDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + d.generic.Metadata(ctx, req, resp) +} + +func (d *VXLANDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + d.generic.Configure(ctx, req, resp) +} + +func (d *VXLANDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + d.generic.Read(ctx, req, resp) +} diff --git a/fwprovider/cluster/sdn/zone/datasource_zones.go b/fwprovider/cluster/sdn/zone/datasource_zones.go new file mode 100644 index 000000000..c82707c12 --- /dev/null +++ b/fwprovider/cluster/sdn/zone/datasource_zones.go @@ -0,0 +1,279 @@ +/* + * 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" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/bpg/terraform-provider-proxmox/fwprovider/config" + "github.com/bpg/terraform-provider-proxmox/fwprovider/types/stringset" + "github.com/bpg/terraform-provider-proxmox/proxmox/cluster/sdn/zones" +) + +// Ensure the implementation satisfies the required interfaces. +var ( + _ datasource.DataSource = &zonesDataSource{} + _ datasource.DataSourceWithConfigure = &zonesDataSource{} +) + +// zonesDataSource is the data source implementation for SDN zones. +type zonesDataSource struct { + client *zones.Client +} + +// zonesDataSourceModel represents the data source model for listing zones. +type zonesDataSourceModel struct { + Type types.String `tfsdk:"type"` + Zones types.List `tfsdk:"zones"` +} + +// zoneDataModel represents individual zone data in the list. +type zoneDataModel struct { + ID types.String `tfsdk:"id"` + Type types.String `tfsdk:"type"` + IPAM types.String `tfsdk:"ipam"` + DNS types.String `tfsdk:"dns"` + ReverseDNS types.String `tfsdk:"reverse_dns"` + DNSZone types.String `tfsdk:"dns_zone"` + Nodes stringset.Value `tfsdk:"nodes"` + MTU types.Int64 `tfsdk:"mtu"` + Bridge types.String `tfsdk:"bridge"` + ServiceVLAN types.Int64 `tfsdk:"service_vlan"` + ServiceVLANProtocol types.String `tfsdk:"service_vlan_protocol"` + Peers stringset.Value `tfsdk:"peers"` + 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"` +} + +// Configure adds the provider-configured client to the data source. +func (d *zonesDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + cfg, ok := req.ProviderData.(config.DataSource) + if !ok { + resp.Diagnostics.AddError( + "Unexpected DataSource Configure Type", + fmt.Sprintf("Expected config.DataSource, got: %T", req.ProviderData), + ) + + return + } + + d.client = cfg.Client.Cluster().SDNZones() +} + +// Metadata returns the data source type name. +func (d *zonesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_sdn_zones" +} + +// Schema defines the schema for the data source. +func (d *zonesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Retrieves information about all SDN Zones in Proxmox.", + MarkdownDescription: "Retrieves information about all SDN Zones in Proxmox. " + + "This data source can optionally filter zones by type.", + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Description: "Filter zones by type (simple, vlan, qinq, vxlan, evpn).", + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOf("simple", "vlan", "qinq", "vxlan", "evpn"), + }, + }, + "zones": schema.ListAttribute{ + Description: "List of SDN zones.", + Computed: true, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "type": types.StringType, + "ipam": types.StringType, + "dns": types.StringType, + "reverse_dns": types.StringType, + "dns_zone": types.StringType, + "nodes": types.SetType{ + ElemType: types.StringType, + }, + "mtu": types.Int64Type, + "bridge": types.StringType, + "service_vlan": types.Int64Type, + "service_vlan_protocol": types.StringType, + "peers": types.SetType{ + ElemType: types.StringType, + }, + "advertise_subnets": types.BoolType, + "controller": types.StringType, + "disable_arp_nd_suppression": types.BoolType, + "exit_nodes": types.SetType{ + ElemType: types.StringType, + }, + "exit_nodes_local_routing": types.BoolType, + "primary_exit_node": types.StringType, + "rt_import": types.StringType, + "vrf_vxlan": types.Int64Type, + }, + }, + }, + }, + } +} + +// Read fetches all SDN zones from the Proxmox VE API. +func (d *zonesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data zonesDataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + zonesList, err := d.client.GetZones(ctx) + if err != nil { + resp.Diagnostics.AddError( + "Unable to Read SDN Zones", + err.Error(), + ) + return + } + + filteredZones := zonesList + if !data.Type.IsNull() && !data.Type.IsUnknown() { + filterType := data.Type.ValueString() + filteredZones = make([]zones.ZoneData, 0) + for _, zone := range zonesList { + if zone.Type != nil && *zone.Type == filterType { + filteredZones = append(filteredZones, zone) + } + } + } + + // Convert zones to list elements + zoneElements := make([]attr.Value, len(filteredZones)) + for i, zone := range filteredZones { + diags := &resp.Diagnostics + + zoneData := zoneDataModel{ + ID: types.StringValue(zone.ID), + Type: types.StringPointerValue(zone.Type), + IPAM: types.StringPointerValue(zone.IPAM), + DNS: types.StringPointerValue(zone.DNS), + ReverseDNS: types.StringPointerValue(zone.ReverseDNS), + DNSZone: types.StringPointerValue(zone.DNSZone), + Nodes: stringset.NewValueString(zone.Nodes, diags, stringset.WithSeparator(",")), + MTU: types.Int64PointerValue(zone.MTU), + Bridge: types.StringPointerValue(zone.Bridge), + ServiceVLAN: types.Int64PointerValue(zone.ServiceVLAN), + ServiceVLANProtocol: types.StringPointerValue(zone.ServiceVLANProtocol), + Peers: stringset.NewValueString(zone.Peers, diags, stringset.WithSeparator(",")), + AdvertiseSubnets: types.BoolPointerValue(zone.AdvertiseSubnets.PointerBool()), + Controller: types.StringPointerValue(zone.Controller), + DisableARPNDSuppression: types.BoolPointerValue(zone.DisableARPNDSuppression.PointerBool()), + ExitNodes: stringset.NewValueString(zone.ExitNodes, diags, stringset.WithSeparator(",")), + ExitNodesLocalRouting: types.BoolPointerValue(zone.ExitNodesLocalRouting.PointerBool()), + PrimaryExitNode: types.StringPointerValue(zone.ExitNodesPrimary), + RouteTargetImport: types.StringPointerValue(zone.RouteTargetImport), + VRFVXLANID: types.Int64PointerValue(zone.VRFVXLANID), + } + + objValue, objDiag := types.ObjectValueFrom(ctx, map[string]attr.Type{ + "id": types.StringType, + "type": types.StringType, + "ipam": types.StringType, + "dns": types.StringType, + "reverse_dns": types.StringType, + "dns_zone": types.StringType, + "nodes": types.SetType{ + ElemType: types.StringType, + }, + "mtu": types.Int64Type, + "bridge": types.StringType, + "service_vlan": types.Int64Type, + "service_vlan_protocol": types.StringType, + "peers": types.SetType{ + ElemType: types.StringType, + }, + "advertise_subnets": types.BoolType, + "controller": types.StringType, + "disable_arp_nd_suppression": types.BoolType, + "exit_nodes": types.SetType{ + ElemType: types.StringType, + }, + "exit_nodes_local_routing": types.BoolType, + "primary_exit_node": types.StringType, + "rt_import": types.StringType, + "vrf_vxlan": types.Int64Type, + }, zoneData) + resp.Diagnostics.Append(objDiag...) + + if resp.Diagnostics.HasError() { + return + } + + zoneElements[i] = objValue + } + + listValue, listDiag := types.ListValue(types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "type": types.StringType, + "ipam": types.StringType, + "dns": types.StringType, + "reverse_dns": types.StringType, + "dns_zone": types.StringType, + "nodes": types.SetType{ + ElemType: types.StringType, + }, + "mtu": types.Int64Type, + "bridge": types.StringType, + "service_vlan": types.Int64Type, + "service_vlan_protocol": types.StringType, + "peers": types.SetType{ + ElemType: types.StringType, + }, + "advertise_subnets": types.BoolType, + "controller": types.StringType, + "disable_arp_nd_suppression": types.BoolType, + "exit_nodes": types.SetType{ + ElemType: types.StringType, + }, + "exit_nodes_local_routing": types.BoolType, + "primary_exit_node": types.StringType, + "rt_import": types.StringType, + "vrf_vxlan": types.Int64Type, + }, + }, zoneElements) + resp.Diagnostics.Append(listDiag...) + + if resp.Diagnostics.HasError() { + return + } + + data.Zones = listValue + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +// NewZonesDataSource returns a new data source for SDN zones. +func NewZonesDataSource() datasource.DataSource { + return &zonesDataSource{} +} diff --git a/fwprovider/cluster/sdn/zone/datasource_zones_test.go b/fwprovider/cluster/sdn/zone/datasource_zones_test.go new file mode 100644 index 000000000..6c2140fb2 --- /dev/null +++ b/fwprovider/cluster/sdn/zone/datasource_zones_test.go @@ -0,0 +1,259 @@ +//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 TestAccDataSourceSDNZoneSimple(t *testing.T) { + t.Parallel() + + te := test.InitEnvironment(t) + + tests := []struct { + name string + steps []resource.TestStep + }{ + {"create simple zone and read with datasource", []resource.TestStep{{ + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_sdn_zone_simple" "test" { + id = "dstest1" + nodes = ["pve"] + mtu = 1500 + } + + data "proxmox_virtual_environment_sdn_zone_simple" "test" { + id = proxmox_virtual_environment_sdn_zone_simple.test.id + } + `), + Check: resource.ComposeTestCheckFunc( + test.ResourceAttributes("data.proxmox_virtual_environment_sdn_zone_simple.test", map[string]string{ + "id": "dstest1", + "mtu": "1500", + }), + test.ResourceAttributes("data.proxmox_virtual_environment_sdn_zone_simple.test", map[string]string{ + "nodes.#": "1", + "nodes.0": "pve", + }), + ), + }}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: te.AccProviders, + Steps: tt.steps, + }) + }) + } +} + +func TestAccDataSourceSDNZoneVLAN(t *testing.T) { + t.Parallel() + + te := test.InitEnvironment(t) + + tests := []struct { + name string + steps []resource.TestStep + }{ + {"create VLAN zone and read with datasource", []resource.TestStep{{ + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_sdn_zone_vlan" "test" { + id = "dstest2" + nodes = ["pve"] + bridge = "vmbr0" + mtu = 1496 + } + + data "proxmox_virtual_environment_sdn_zone_vlan" "test" { + id = proxmox_virtual_environment_sdn_zone_vlan.test.id + } + `), + Check: resource.ComposeTestCheckFunc( + test.ResourceAttributes("data.proxmox_virtual_environment_sdn_zone_vlan.test", map[string]string{ + "id": "dstest2", + "bridge": "vmbr0", + "mtu": "1496", + "nodes.#": "1", + "nodes.0": "pve", + }), + ), + }}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: te.AccProviders, + Steps: tt.steps, + }) + }) + } +} + +func TestAccDataSourceSDNZoneQinQ(t *testing.T) { + t.Parallel() + + te := test.InitEnvironment(t) + + tests := []struct { + name string + steps []resource.TestStep + }{ + {"create QinQ zone and read with datasource", []resource.TestStep{{ + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_sdn_zone_qinq" "test" { + id = "dstest3" + nodes = ["pve"] + bridge = "vmbr0" + service_vlan = 100 + service_vlan_protocol = "802.1ad" + mtu = 1492 + } + + data "proxmox_virtual_environment_sdn_zone_qinq" "test" { + id = proxmox_virtual_environment_sdn_zone_qinq.test.id + } + `), + Check: resource.ComposeTestCheckFunc( + test.ResourceAttributes("data.proxmox_virtual_environment_sdn_zone_qinq.test", map[string]string{ + "id": "dstest3", + "bridge": "vmbr0", + "service_vlan": "100", + "service_vlan_protocol": "802.1ad", + "mtu": "1492", + "nodes.#": "1", + "nodes.0": "pve", + }), + ), + }}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: te.AccProviders, + Steps: tt.steps, + }) + }) + } +} + +func TestAccDataSourceSDNZoneVXLAN(t *testing.T) { + t.Parallel() + + te := test.InitEnvironment(t) + + tests := []struct { + name string + steps []resource.TestStep + }{ + {"create VXLAN zone and read with datasource", []resource.TestStep{{ + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_sdn_zone_vxlan" "test" { + id = "dstest4" + nodes = ["pve"] + peers = ["10.0.0.1", "10.0.0.2"] + mtu = 1450 + } + + data "proxmox_virtual_environment_sdn_zone_vxlan" "test" { + id = proxmox_virtual_environment_sdn_zone_vxlan.test.id + } + `), + Check: resource.ComposeTestCheckFunc( + test.ResourceAttributes("data.proxmox_virtual_environment_sdn_zone_vxlan.test", map[string]string{ + "id": "dstest4", + "mtu": "1450", + "nodes.#": "1", + "nodes.0": "pve", + "peers.#": "2", + "peers.0": "10.0.0.1", + "peers.1": "10.0.0.2", + }), + ), + }}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: te.AccProviders, + Steps: tt.steps, + }) + }) + } +} + +func TestAccDataSourceSDNZones(t *testing.T) { + t.Parallel() + + te := test.InitEnvironment(t) + + tests := []struct { + name string + steps []resource.TestStep + }{ + {"create multiple zones and read with zones datasource", []resource.TestStep{{ + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_sdn_zone_simple" "test1" { + id = "dstest6" + nodes = ["pve"] + mtu = 1500 + } + + resource "proxmox_virtual_environment_sdn_zone_vlan" "test2" { + id = "dstest7" + nodes = ["pve"] + bridge = "vmbr0" + mtu = 1496 + } + + data "proxmox_virtual_environment_sdn_zones" "all" { + depends_on = [ + proxmox_virtual_environment_sdn_zone_simple.test1, + proxmox_virtual_environment_sdn_zone_vlan.test2 + ] + } + + data "proxmox_virtual_environment_sdn_zones" "simple_only" { + type = "simple" + depends_on = [ + proxmox_virtual_environment_sdn_zone_simple.test1, + proxmox_virtual_environment_sdn_zone_vlan.test2 + ] + } + `), + Check: resource.ComposeTestCheckFunc( + // Check that all zones datasource returns multiple zones + resource.TestCheckResourceAttrSet("data.proxmox_virtual_environment_sdn_zones.all", "zones.#"), + + // Check that filtered datasource works + resource.TestCheckResourceAttr("data.proxmox_virtual_environment_sdn_zones.simple_only", "type", "simple"), + resource.TestCheckResourceAttrSet("data.proxmox_virtual_environment_sdn_zones.simple_only", "zones.#"), + ), + }}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: te.AccProviders, + Steps: tt.steps, + }) + }) + } +} diff --git a/fwprovider/provider.go b/fwprovider/provider.go index 570981445..98faa4ebf 100644 --- a/fwprovider/provider.go +++ b/fwprovider/provider.go @@ -533,9 +533,6 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc sdnzone.NewQinQResource, sdnzone.NewVXLANResource, sdnzone.NewEVPNResource, - // - // sdn.NewSDNVnetResource, - // sdn.NewSDNSubnetResource, } } @@ -558,10 +555,13 @@ func (p *proxmoxProvider) DataSources(_ context.Context) []func() datasource.Dat hardwaremapping.NewPCIDataSource, hardwaremapping.NewUSBDataSource, metrics.NewMetricsServerDatasource, + sdnzone.NewSimpleDataSource, + sdnzone.NewVLANDataSource, + sdnzone.NewQinQDataSource, + sdnzone.NewVXLANDataSource, + sdnzone.NewEVPNDataSource, + sdnzone.NewZonesDataSource, vm.NewDataSource, - // sdn.NewSDNZoneDataSource, - // sdn.NewSDNVnetDataSource, - // sdn.NewSDNSubnetDataSource, } } diff --git a/main.go b/main.go index ed7742221..7ab7d54a5 100644 --- a/main.go +++ b/main.go @@ -48,6 +48,12 @@ import ( //go:generate cp ./build/docs-gen/data-sources/virtual_environment_hardware_mappings.md ./docs/data-sources/ //go:generate cp ./build/docs-gen/data-sources/virtual_environment_haresource.md ./docs/data-sources/ //go:generate cp ./build/docs-gen/data-sources/virtual_environment_haresources.md ./docs/data-sources/ +//go:generate cp ./build/docs-gen/data-sources/virtual_environment_sdn_zones.md ./docs/data-sources/ +//go:generate cp ./build/docs-gen/data-sources/virtual_environment_sdn_zone_simple.md ./docs/data-sources/ +//go:generate cp ./build/docs-gen/data-sources/virtual_environment_sdn_zone_vlan.md ./docs/data-sources/ +//go:generate cp ./build/docs-gen/data-sources/virtual_environment_sdn_zone_qinq.md ./docs/data-sources/ +//go:generate cp ./build/docs-gen/data-sources/virtual_environment_sdn_zone_vxlan.md ./docs/data-sources/ +//go:generate cp ./build/docs-gen/data-sources/virtual_environment_sdn_zone_evpn.md ./docs/data-sources/ //go:generate cp ./build/docs-gen/data-sources/virtual_environment_version.md ./docs/data-sources/ //go:generate cp ./build/docs-gen/data-sources/virtual_environment_vm2.md ./docs/data-sources/ //go:generate cp ./build/docs-gen/data-sources/virtual_environment_metrics_server.md ./docs/data-sources/ diff --git a/proxmox_virtual_environment_sdn_zone_evpn/data-source.tf b/proxmox_virtual_environment_sdn_zone_evpn/data-source.tf new file mode 100644 index 000000000..2b70ab68d --- /dev/null +++ b/proxmox_virtual_environment_sdn_zone_evpn/data-source.tf @@ -0,0 +1,23 @@ +data "proxmox_virtual_environment_sdn_zone_evpn" "example" { + id = "evpn1" +} + +output "data_proxmox_virtual_environment_sdn_zone_evpn" { + value = { + id = data.proxmox_virtual_environment_sdn_zone_evpn.example.id + nodes = data.proxmox_virtual_environment_sdn_zone_evpn.example.nodes + controller = data.proxmox_virtual_environment_sdn_zone_evpn.example.controller + vrf_vxlan = data.proxmox_virtual_environment_sdn_zone_evpn.example.vrf_vxlan + advertise_subnets = data.proxmox_virtual_environment_sdn_zone_evpn.example.advertise_subnets + disable_arp_nd_suppression = data.proxmox_virtual_environment_sdn_zone_evpn.example.disable_arp_nd_suppression + exit_nodes = data.proxmox_virtual_environment_sdn_zone_evpn.example.exit_nodes + exit_nodes_local_routing = data.proxmox_virtual_environment_sdn_zone_evpn.example.exit_nodes_local_routing + primary_exit_node = data.proxmox_virtual_environment_sdn_zone_evpn.example.primary_exit_node + rt_import = data.proxmox_virtual_environment_sdn_zone_evpn.example.rt_import + mtu = data.proxmox_virtual_environment_sdn_zone_evpn.example.mtu + dns = data.proxmox_virtual_environment_sdn_zone_evpn.example.dns + dns_zone = data.proxmox_virtual_environment_sdn_zone_evpn.example.dns_zone + ipam = data.proxmox_virtual_environment_sdn_zone_evpn.example.ipam + reverse_dns = data.proxmox_virtual_environment_sdn_zone_evpn.example.reverse_dns + } +} diff --git a/proxmox_virtual_environment_sdn_zone_qinq/data-source.tf b/proxmox_virtual_environment_sdn_zone_qinq/data-source.tf new file mode 100644 index 000000000..26f448c1a --- /dev/null +++ b/proxmox_virtual_environment_sdn_zone_qinq/data-source.tf @@ -0,0 +1,18 @@ +data "proxmox_virtual_environment_sdn_zone_qinq" "example" { + id = "qinq1" +} + +output "data_proxmox_virtual_environment_sdn_zone_qinq" { + value = { + id = data.proxmox_virtual_environment_sdn_zone_qinq.example.id + nodes = data.proxmox_virtual_environment_sdn_zone_qinq.example.nodes + bridge = data.proxmox_virtual_environment_sdn_zone_qinq.example.bridge + service_vlan = data.proxmox_virtual_environment_sdn_zone_qinq.example.service_vlan + service_vlan_protocol = data.proxmox_virtual_environment_sdn_zone_qinq.example.service_vlan_protocol + mtu = data.proxmox_virtual_environment_sdn_zone_qinq.example.mtu + dns = data.proxmox_virtual_environment_sdn_zone_qinq.example.dns + dns_zone = data.proxmox_virtual_environment_sdn_zone_qinq.example.dns_zone + ipam = data.proxmox_virtual_environment_sdn_zone_qinq.example.ipam + reverse_dns = data.proxmox_virtual_environment_sdn_zone_qinq.example.reverse_dns + } +} diff --git a/proxmox_virtual_environment_sdn_zone_simple/data-source.tf b/proxmox_virtual_environment_sdn_zone_simple/data-source.tf new file mode 100644 index 000000000..452cb88fc --- /dev/null +++ b/proxmox_virtual_environment_sdn_zone_simple/data-source.tf @@ -0,0 +1,15 @@ +data "proxmox_virtual_environment_sdn_zone_simple" "example" { + id = "simple1" +} + +output "data_proxmox_virtual_environment_sdn_zone_simple" { + value = { + id = data.proxmox_virtual_environment_sdn_zone_simple.example.id + nodes = data.proxmox_virtual_environment_sdn_zone_simple.example.nodes + mtu = data.proxmox_virtual_environment_sdn_zone_simple.example.mtu + dns = data.proxmox_virtual_environment_sdn_zone_simple.example.dns + dns_zone = data.proxmox_virtual_environment_sdn_zone_simple.example.dns_zone + ipam = data.proxmox_virtual_environment_sdn_zone_simple.example.ipam + reverse_dns = data.proxmox_virtual_environment_sdn_zone_simple.example.reverse_dns + } +} diff --git a/proxmox_virtual_environment_sdn_zone_vlan/data-source.tf b/proxmox_virtual_environment_sdn_zone_vlan/data-source.tf new file mode 100644 index 000000000..a1f0f9e4d --- /dev/null +++ b/proxmox_virtual_environment_sdn_zone_vlan/data-source.tf @@ -0,0 +1,16 @@ +data "proxmox_virtual_environment_sdn_zone_vlan" "example" { + id = "vlan1" +} + +output "data_proxmox_virtual_environment_sdn_zone_vlan" { + value = { + id = data.proxmox_virtual_environment_sdn_zone_vlan.example.id + nodes = data.proxmox_virtual_environment_sdn_zone_vlan.example.nodes + bridge = data.proxmox_virtual_environment_sdn_zone_vlan.example.bridge + mtu = data.proxmox_virtual_environment_sdn_zone_vlan.example.mtu + dns = data.proxmox_virtual_environment_sdn_zone_vlan.example.dns + dns_zone = data.proxmox_virtual_environment_sdn_zone_vlan.example.dns_zone + ipam = data.proxmox_virtual_environment_sdn_zone_vlan.example.ipam + reverse_dns = data.proxmox_virtual_environment_sdn_zone_vlan.example.reverse_dns + } +} diff --git a/proxmox_virtual_environment_sdn_zone_vxlan/data-source.tf b/proxmox_virtual_environment_sdn_zone_vxlan/data-source.tf new file mode 100644 index 000000000..2ca035e49 --- /dev/null +++ b/proxmox_virtual_environment_sdn_zone_vxlan/data-source.tf @@ -0,0 +1,16 @@ +data "proxmox_virtual_environment_sdn_zone_vxlan" "example" { + id = "vxlan1" +} + +output "data_proxmox_virtual_environment_sdn_zone_vxlan" { + value = { + id = data.proxmox_virtual_environment_sdn_zone_vxlan.example.id + nodes = data.proxmox_virtual_environment_sdn_zone_vxlan.example.nodes + peers = data.proxmox_virtual_environment_sdn_zone_vxlan.example.peers + mtu = data.proxmox_virtual_environment_sdn_zone_vxlan.example.mtu + dns = data.proxmox_virtual_environment_sdn_zone_vxlan.example.dns + dns_zone = data.proxmox_virtual_environment_sdn_zone_vxlan.example.dns_zone + ipam = data.proxmox_virtual_environment_sdn_zone_vxlan.example.ipam + reverse_dns = data.proxmox_virtual_environment_sdn_zone_vxlan.example.reverse_dns + } +} diff --git a/proxmox_virtual_environment_sdn_zones/data-source.tf b/proxmox_virtual_environment_sdn_zones/data-source.tf new file mode 100644 index 000000000..a84abd4c5 --- /dev/null +++ b/proxmox_virtual_environment_sdn_zones/data-source.tf @@ -0,0 +1,25 @@ +# List all SDN zones +data "proxmox_virtual_environment_sdn_zones" "all" {} + +# List only EVPN zones +data "proxmox_virtual_environment_sdn_zones" "evpn_only" { + type = "evpn" +} + +# List only Simple zones +data "proxmox_virtual_environment_sdn_zones" "simple_only" { + type = "simple" +} + +output "data_proxmox_virtual_environment_sdn_zones_all" { + value = { + zones = data.proxmox_virtual_environment_sdn_zones.all.zones + } +} + +output "data_proxmox_virtual_environment_sdn_zones_filtered" { + value = { + evpn_zones = data.proxmox_virtual_environment_sdn_zones.evpn_only.zones + simple_zones = data.proxmox_virtual_environment_sdn_zones.simple_only.zones + } +} From e86fce1c13a602ad33c74f8b5cce2367f71257cf Mon Sep 17 00:00:00 2001 From: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Date: Wed, 30 Jul 2025 22:43:59 -0400 Subject: [PATCH 9/9] linter Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- fwprovider/cluster/sdn/zone/datasource_generic.go | 2 ++ fwprovider/cluster/sdn/zone/datasource_zones.go | 3 +++ 2 files changed, 5 insertions(+) diff --git a/fwprovider/cluster/sdn/zone/datasource_generic.go b/fwprovider/cluster/sdn/zone/datasource_generic.go index 3547ed87e..0406030fb 100644 --- a/fwprovider/cluster/sdn/zone/datasource_generic.go +++ b/fwprovider/cluster/sdn/zone/datasource_generic.go @@ -131,6 +131,7 @@ func (d *genericZoneDataSource) Read(ctx context.Context, req datasource.ReadReq "SDN Zone Not Found", fmt.Sprintf("SDN zone with ID '%s' was not found", state.getID()), ) + return } @@ -153,6 +154,7 @@ func (d *genericZoneDataSource) Read(ctx context.Context, req datasource.ReadReq zone.ID, ), ) + return } diff --git a/fwprovider/cluster/sdn/zone/datasource_zones.go b/fwprovider/cluster/sdn/zone/datasource_zones.go index c82707c12..f412c50f3 100644 --- a/fwprovider/cluster/sdn/zone/datasource_zones.go +++ b/fwprovider/cluster/sdn/zone/datasource_zones.go @@ -154,13 +154,16 @@ func (d *zonesDataSource) Read(ctx context.Context, req datasource.ReadRequest, "Unable to Read SDN Zones", err.Error(), ) + return } filteredZones := zonesList + if !data.Type.IsNull() && !data.Type.IsUnknown() { filterType := data.Type.ValueString() filteredZones = make([]zones.ZoneData, 0) + for _, zone := range zonesList { if zone.Type != nil && *zone.Type == filterType { filteredZones = append(filteredZones, zone)