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] cleanup Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- .golangci.yml | 2 + fwprovider/cluster/sdn/zone/resource_evpn.go | 289 ++++++++--------- .../cluster/sdn/zone/resource_generic.go | 294 ++++++++++++++++++ fwprovider/cluster/sdn/zone/resource_model.go | 178 ----------- fwprovider/cluster/sdn/zone/resource_qinq.go | 253 ++++++--------- .../cluster/sdn/zone/resource_qinq_test.go | 65 ---- .../cluster/sdn/zone/resource_schema.go | 219 ------------- .../cluster/sdn/zone/resource_simple.go | 216 +++---------- .../cluster/sdn/zone/resource_simple_test.go | 59 ---- fwprovider/cluster/sdn/zone/resource_vlan.go | 233 +++++--------- .../cluster/sdn/zone/resource_vlan_test.go | 61 ---- fwprovider/cluster/sdn/zone/resource_vxlan.go | 230 +++++--------- .../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, 925 insertions(+), 1458 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 3c6d3ae1..b91d3142 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 a7d3836b..f753bae3 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 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 (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 (m *evpnModel) toAPIRequestBody(ctx context.Context, diags *diag.Diagnostics) *zones.ZoneRequestData { + data := m.genericModel.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 +} + type EVPNResource struct { - client *zones.Client + generic *genericZoneResource } 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(), - ) + return &EVPNResource{ + generic: newGenericZoneResource(zoneResourceConfig{ + typeNameSuffix: "_sdn_zone_evpn", + zoneType: zones.TypeEVPN, + modelFunc: func() zoneModel { return &evpnModel{} }, + }).(*genericZoneResource), } } -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 +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.", + }, + }), } - readModel := &evpnModel{} - readModel.importFromAPI(zone.ID, zone, &resp.Diagnostics) - resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...) +} + +func (r *EVPNResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.generic.Metadata(ctx, req, resp) +} + +func (r *EVPNResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.generic.Configure(ctx, req, resp) +} + +func (r *EVPNResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + r.generic.Create(ctx, req, resp) +} + +func (r *EVPNResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + r.generic.Read(ctx, req, resp) +} + +func (r *EVPNResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + r.generic.Update(ctx, req, resp) +} + +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) { + 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 00000000..3c4e9005 --- /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 35969379..00000000 --- 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 1e91ec3a..04e083d6 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 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 QinQResource struct { - client *zones.Client + generic *genericZoneResource } 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(), - ) + return &QinQResource{ + generic: newGenericZoneResource(zoneResourceConfig{ + typeNameSuffix: "_sdn_zone_qinq", + zoneType: zones.TypeQinQ, + modelFunc: func() zoneModel { return &qinqModel{} }, + }).(*genericZoneResource), } } -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 +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"), + }, + }, + }), } - readModel := &qinqModel{} - readModel.importFromAPI(zone.ID, zone, &resp.Diagnostics) - resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...) +} + +func (r *QinQResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.generic.Metadata(ctx, req, resp) +} + +func (r *QinQResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.generic.Configure(ctx, req, resp) +} + +func (r *QinQResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + r.generic.Create(ctx, req, resp) +} + +func (r *QinQResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + r.generic.Read(ctx, req, resp) +} + +func (r *QinQResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + r.generic.Update(ctx, req, resp) +} + +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) { + 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 bb047a5c..00000000 --- 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 8aa78f35..00000000 --- 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 da15af95..d8de0ae5 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 simpleModel struct { + genericModel +} + type SimpleResource struct { - client *zones.Client + generic *genericZoneResource } func NewSimpleResource() resource.Resource { - return &SimpleResource{} -} - -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 simpleModel - - 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 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) 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) 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(), - ) + return &SimpleResource{ + generic: newGenericZoneResource(zoneResourceConfig{ + typeNameSuffix: "_sdn_zone_simple", + zoneType: zones.TypeSimple, + modelFunc: func() zoneModel { return &simpleModel{} }, + }).(*genericZoneResource), } } -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 - } - - resp.Diagnostics.AddError(fmt.Sprintf("Unable to Import SDN Simple Zone %s", req.ID), err.Error()) - - 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(), } - - readModel := &simpleModel{} - 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) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.generic.Configure(ctx, req, resp) +} + +func (r *SimpleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + r.generic.Create(ctx, req, resp) +} + +func (r *SimpleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + r.generic.Read(ctx, req, resp) +} + +func (r *SimpleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + r.generic.Update(ctx, req, resp) +} + +func (r *SimpleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + r.generic.Delete(ctx, req, resp) +} + +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 87bc9198..00000000 --- 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 e303f107..31c10c90 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 vlanModel struct { + genericModel + + Bridge types.String `tfsdk:"bridge"` +} + +func (m *vlanModel) importFromAPI(name string, data *zones.ZoneData, diags *diag.Diagnostics) { + m.genericModel.importFromAPI(name, data, diags) + + m.Bridge = types.StringPointerValue(data.Bridge) +} + +func (m *vlanModel) toAPIRequestBody(ctx context.Context, diags *diag.Diagnostics) *zones.ZoneRequestData { + data := m.genericModel.toAPIRequestBody(ctx, diags) + + data.Bridge = m.Bridge.ValueStringPointer() + + return data +} + type VLANResource struct { - client *zones.Client + generic *genericZoneResource } 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(), - ) + return &VLANResource{ + generic: newGenericZoneResource(zoneResourceConfig{ + typeNameSuffix: "_sdn_zone_vlan", + zoneType: zones.TypeVLAN, + modelFunc: func() zoneModel { return &vlanModel{} }, + }).(*genericZoneResource), } } -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 +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, + }, + }), } - readModel := &vlanModel{} - readModel.importFromAPI(zone.ID, zone, &resp.Diagnostics) - resp.Diagnostics.Append(resp.State.Set(ctx, readModel)...) +} + +func (r *VLANResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.generic.Metadata(ctx, req, resp) +} + +func (r *VLANResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.generic.Configure(ctx, req, resp) +} + +func (r *VLANResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + r.generic.Create(ctx, req, resp) +} + +func (r *VLANResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + r.generic.Read(ctx, req, resp) +} + +func (r *VLANResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + r.generic.Update(ctx, req, resp) +} + +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) { + 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 dc8b64eb..00000000 --- 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 703b9320..b0b0abdc 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 vxlanModel struct { + genericModel + + Peers stringset.Value `tfsdk:"peers"` +} + +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 (m *vxlanModel) toAPIRequestBody(ctx context.Context, diags *diag.Diagnostics) *zones.ZoneRequestData { + data := m.genericModel.toAPIRequestBody(ctx, diags) + + data.Peers = m.Peers.ValueStringPointer(ctx, diags, stringset.WithSeparator(",")) + + return data +} + type VXLANResource struct { - client *zones.Client + generic *genericZoneResource } 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 + return &VXLANResource{ + generic: newGenericZoneResource(zoneResourceConfig{ + typeNameSuffix: "_sdn_zone_vxlan", + zoneType: zones.TypeVXLAN, + modelFunc: func() zoneModel { return &vxlanModel{} }, + }).(*genericZoneResource), } +} - cfg, ok := req.ProviderData.(config.Resource) - if !ok { - resp.Diagnostics.AddError( - "Unexpected Resource Configure Type", - fmt.Sprintf( - "Expected config.Resource, got: %T", - req.ProviderData, +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", ), - ) - 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)...) +func (r *VXLANResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + r.generic.Metadata(ctx, req, resp) +} + +func (r *VXLANResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.generic.Configure(ctx, req, resp) +} + +func (r *VXLANResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + r.generic.Create(ctx, req, resp) +} + +func (r *VXLANResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + r.generic.Read(ctx, req, resp) +} + +func (r *VXLANResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + r.generic.Update(ctx, req, resp) +} + +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) { + 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 7bb2f30d..00000000 --- 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 00000000..3caaa64b --- /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 b6334e67..19c63209 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 f39426bc..442fbdec 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 b657c5cc..57098144 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 00000000..b61a318a --- /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 +}