mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-06-30 02:31:10 +00:00
Signed-off-by: fmendieta <fmendieta@eservicios.indra.es> Co-authored-by: fmendieta <fmendieta@eservicios.indra.es>
541 lines
15 KiB
Go
541 lines
15 KiB
Go
/*
|
|
* 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 network
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"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/booldefault"
|
|
"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"
|
|
|
|
"github.com/bpg/terraform-provider-proxmox/fwprovider/attribute"
|
|
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
|
|
customtypes "github.com/bpg/terraform-provider-proxmox/fwprovider/types"
|
|
|
|
"github.com/bpg/terraform-provider-proxmox/proxmox"
|
|
"github.com/bpg/terraform-provider-proxmox/proxmox/nodes"
|
|
proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types"
|
|
)
|
|
|
|
var (
|
|
_ resource.Resource = &linuxBridgeResource{}
|
|
_ resource.ResourceWithConfigure = &linuxBridgeResource{}
|
|
_ resource.ResourceWithImportState = &linuxBridgeResource{}
|
|
)
|
|
|
|
type linuxBridgeResourceModel struct {
|
|
// Base attributes
|
|
ID types.String `tfsdk:"id"`
|
|
NodeName types.String `tfsdk:"node_name"`
|
|
Name types.String `tfsdk:"name"`
|
|
Address customtypes.IPCIDRValue `tfsdk:"address"`
|
|
Gateway customtypes.IPAddrValue `tfsdk:"gateway"`
|
|
Address6 customtypes.IPCIDRValue `tfsdk:"address6"`
|
|
Gateway6 customtypes.IPAddrValue `tfsdk:"gateway6"`
|
|
Autostart types.Bool `tfsdk:"autostart"`
|
|
MTU types.Int64 `tfsdk:"mtu"`
|
|
Comment types.String `tfsdk:"comment"`
|
|
// Linux bridge attributes
|
|
Ports []types.String `tfsdk:"ports"`
|
|
VLANAware types.Bool `tfsdk:"vlan_aware"`
|
|
}
|
|
|
|
//nolint:lll
|
|
func (m *linuxBridgeResourceModel) exportToNetworkInterfaceCreateUpdateBody() *nodes.NetworkInterfaceCreateUpdateRequestBody {
|
|
body := &nodes.NetworkInterfaceCreateUpdateRequestBody{
|
|
Iface: m.Name.ValueString(),
|
|
Type: "bridge",
|
|
Autostart: proxmoxtypes.CustomBool(m.Autostart.ValueBool()).Pointer(),
|
|
}
|
|
|
|
body.CIDR = m.Address.ValueStringPointer()
|
|
body.Gateway = m.Gateway.ValueStringPointer()
|
|
body.CIDR6 = m.Address6.ValueStringPointer()
|
|
body.Gateway6 = m.Gateway6.ValueStringPointer()
|
|
|
|
if !m.MTU.IsUnknown() {
|
|
body.MTU = m.MTU.ValueInt64Pointer()
|
|
}
|
|
|
|
body.Comments = m.Comment.ValueStringPointer()
|
|
|
|
var sanitizedPorts []string
|
|
|
|
for _, port := range m.Ports {
|
|
port := strings.TrimSpace(port.ValueString())
|
|
if len(port) > 0 {
|
|
sanitizedPorts = append(sanitizedPorts, port)
|
|
}
|
|
}
|
|
|
|
sort.Strings(sanitizedPorts)
|
|
bridgePorts := strings.Join(sanitizedPorts, " ")
|
|
|
|
if len(bridgePorts) > 0 {
|
|
body.BridgePorts = &bridgePorts
|
|
}
|
|
|
|
if m.VLANAware.ValueBool() {
|
|
body.BridgeVLANAware = proxmoxtypes.CustomBool(true).Pointer()
|
|
}
|
|
|
|
return body
|
|
}
|
|
|
|
func (m *linuxBridgeResourceModel) importFromNetworkInterfaceList(
|
|
ctx context.Context,
|
|
iface *nodes.NetworkInterfaceListResponseData,
|
|
) error {
|
|
m.Address = customtypes.NewIPCIDRPointerValue(iface.CIDR)
|
|
m.Gateway = customtypes.NewIPAddrPointerValue(iface.Gateway)
|
|
m.Address6 = customtypes.NewIPCIDRPointerValue(iface.CIDR6)
|
|
m.Gateway6 = customtypes.NewIPAddrPointerValue(iface.Gateway6)
|
|
|
|
m.Autostart = types.BoolPointerValue(iface.Autostart.PointerBool())
|
|
if m.Autostart.IsNull() {
|
|
m.Autostart = types.BoolValue(false)
|
|
}
|
|
|
|
if iface.MTU != nil {
|
|
if v, err := strconv.Atoi(*iface.MTU); err == nil {
|
|
m.MTU = types.Int64Value(int64(v))
|
|
}
|
|
} else {
|
|
m.MTU = types.Int64Null()
|
|
}
|
|
|
|
// Comments can be set to an empty string in plant, which will translate to a "no value" in PVE
|
|
// So we don't want to set it to null if it's empty, as this will be indicated as a plan drift
|
|
if iface.Comments != nil {
|
|
m.Comment = types.StringValue(strings.TrimSpace(*iface.Comments))
|
|
}
|
|
|
|
if iface.BridgeVLANAware != nil {
|
|
m.VLANAware = types.BoolPointerValue(iface.BridgeVLANAware.PointerBool())
|
|
} else {
|
|
m.VLANAware = types.BoolValue(false)
|
|
}
|
|
|
|
if iface.BridgePorts != nil && len(*iface.BridgePorts) > 0 {
|
|
ports, diags := types.ListValueFrom(ctx, types.StringType, strings.Split(*iface.BridgePorts, " "))
|
|
if diags.HasError() {
|
|
return fmt.Errorf("failed to parse bridge ports: %s", *iface.BridgePorts)
|
|
}
|
|
|
|
diags = ports.ElementsAs(ctx, &m.Ports, false)
|
|
if diags.HasError() {
|
|
return fmt.Errorf("failed to build bridge ports list: %s", *iface.BridgePorts)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NewLinuxBridgeResource creates a new resource for managing Linux Bridge network interfaces.
|
|
func NewLinuxBridgeResource() resource.Resource {
|
|
return &linuxBridgeResource{}
|
|
}
|
|
|
|
type linuxBridgeResource struct {
|
|
client proxmox.Client
|
|
}
|
|
|
|
func (r *linuxBridgeResource) Metadata(
|
|
_ context.Context,
|
|
req resource.MetadataRequest,
|
|
resp *resource.MetadataResponse,
|
|
) {
|
|
resp.TypeName = req.ProviderTypeName + "_network_linux_bridge"
|
|
}
|
|
|
|
// Schema defines the schema for the resource.
|
|
func (r *linuxBridgeResource) Schema(
|
|
_ context.Context,
|
|
_ resource.SchemaRequest,
|
|
resp *resource.SchemaResponse,
|
|
) {
|
|
resp.Schema = schema.Schema{
|
|
Description: "Manages a Linux Bridge network interface in a Proxmox VE node.",
|
|
Attributes: map[string]schema.Attribute{
|
|
// Base attributes
|
|
"id": attribute.ResourceID("A unique identifier with format `<node name>:<iface>`"),
|
|
"node_name": schema.StringAttribute{
|
|
Description: "The name of the node.",
|
|
Required: true,
|
|
},
|
|
"name": schema.StringAttribute{
|
|
Description: "The interface name.",
|
|
MarkdownDescription: "The interface name. Commonly vmbr[N], where 0 ≤ N ≤ 4094 (vmbr0 - vmbr4094), but " +
|
|
"can be any alphanumeric string that starts with a character and is at most 10 characters long.",
|
|
Required: true,
|
|
Validators: []validator.String{
|
|
stringvalidator.RegexMatches(
|
|
regexp.MustCompile(`^[A-Za-z][A-Za-z0-9]{0,9}$`),
|
|
`must be an alphanumeric string that starts with a character and is at most 10 characters long`,
|
|
),
|
|
},
|
|
PlanModifiers: []planmodifier.String{
|
|
stringplanmodifier.RequiresReplace(),
|
|
},
|
|
},
|
|
"address": schema.StringAttribute{
|
|
Description: "The interface IPv4/CIDR address.",
|
|
CustomType: customtypes.IPCIDRType{},
|
|
Optional: true,
|
|
},
|
|
"gateway": schema.StringAttribute{
|
|
Description: "Default gateway address.",
|
|
CustomType: customtypes.IPAddrType{},
|
|
Optional: true,
|
|
},
|
|
"address6": schema.StringAttribute{
|
|
Description: "The interface IPv6/CIDR address.",
|
|
CustomType: customtypes.IPCIDRType{},
|
|
Optional: true,
|
|
},
|
|
"gateway6": schema.StringAttribute{
|
|
Description: "Default IPv6 gateway address.",
|
|
CustomType: customtypes.IPAddrType{},
|
|
Optional: true,
|
|
},
|
|
"autostart": schema.BoolAttribute{
|
|
Description: "Automatically start interface on boot (defaults to `true`).",
|
|
Optional: true,
|
|
Computed: true,
|
|
Default: booldefault.StaticBool(true),
|
|
},
|
|
"mtu": schema.Int64Attribute{
|
|
Description: "The interface MTU.",
|
|
Optional: true,
|
|
},
|
|
"comment": schema.StringAttribute{
|
|
Description: "Comment for the interface.",
|
|
Optional: true,
|
|
},
|
|
// Linux Bridge attributes
|
|
"ports": schema.ListAttribute{
|
|
Description: "The interface bridge ports.",
|
|
Optional: true,
|
|
ElementType: types.StringType,
|
|
},
|
|
"vlan_aware": schema.BoolAttribute{
|
|
Description: "Whether the interface bridge is VLAN aware (defaults to `false`).",
|
|
Optional: true,
|
|
Computed: true,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (r *linuxBridgeResource) 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
|
|
}
|
|
|
|
//nolint:dupl
|
|
func (r *linuxBridgeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
|
var plan linuxBridgeResourceModel
|
|
diags := req.Plan.Get(ctx, &plan)
|
|
resp.Diagnostics.Append(diags...)
|
|
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
body := plan.exportToNetworkInterfaceCreateUpdateBody()
|
|
|
|
err := r.client.Node(plan.NodeName.ValueString()).CreateNetworkInterface(ctx, body)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError(
|
|
"Error creating Linux Bridge interface",
|
|
"Could not create Linux Bridge, unexpected error: "+err.Error(),
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
plan.ID = types.StringValue(plan.NodeName.ValueString() + ":" + plan.Name.ValueString())
|
|
|
|
found := r.read(ctx, &plan, &resp.Diagnostics)
|
|
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
if !found {
|
|
resp.Diagnostics.AddError(
|
|
"Linux Bridge interface not found after creation",
|
|
fmt.Sprintf(
|
|
"Interface %q on node %q could not be read after creation",
|
|
plan.Name.ValueString(), plan.NodeName.ValueString()),
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
resp.State.Set(ctx, plan)
|
|
resp.Diagnostics.Append(diags...)
|
|
|
|
err = r.client.Node(plan.NodeName.ValueString()).ReloadNetworkConfiguration(ctx)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError(
|
|
"Error reloading network configuration",
|
|
fmt.Sprintf("Could not reload network configuration on node '%s', unexpected error: %s",
|
|
plan.NodeName.ValueString(), err.Error()),
|
|
)
|
|
}
|
|
}
|
|
|
|
func (r *linuxBridgeResource) read(ctx context.Context, model *linuxBridgeResourceModel, diags *diag.Diagnostics) bool {
|
|
ifaces, err := r.client.Node(model.NodeName.ValueString()).ListNetworkInterfaces(ctx)
|
|
if err != nil {
|
|
diags.AddError(
|
|
"Error listing network interfaces",
|
|
"Could not list network interfaces, unexpected error: "+err.Error(),
|
|
)
|
|
|
|
return false
|
|
}
|
|
|
|
for _, iface := range ifaces {
|
|
if iface.Iface != model.Name.ValueString() {
|
|
continue
|
|
}
|
|
|
|
err = model.importFromNetworkInterfaceList(ctx, iface)
|
|
if err != nil {
|
|
diags.AddError(
|
|
"Error converting network interface to a model",
|
|
"Could not import network interface from API response, unexpected error: "+err.Error(),
|
|
)
|
|
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Read reads a Linux Bridge interface.
|
|
func (r *linuxBridgeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
|
// Get current state
|
|
var state linuxBridgeResourceModel
|
|
diags := req.State.Get(ctx, &state)
|
|
resp.Diagnostics.Append(diags...)
|
|
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
found := r.read(ctx, &state, &resp.Diagnostics)
|
|
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
if !found {
|
|
resp.State.RemoveResource(ctx)
|
|
return
|
|
}
|
|
|
|
diags = resp.State.Set(ctx, state)
|
|
resp.Diagnostics.Append(diags...)
|
|
}
|
|
|
|
// Update updates a Linux Bridge interface.
|
|
func (r *linuxBridgeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
|
var plan, state linuxBridgeResourceModel
|
|
|
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
|
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
|
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
body := plan.exportToNetworkInterfaceCreateUpdateBody()
|
|
|
|
var toDelete []string
|
|
|
|
if !plan.MTU.Equal(state.MTU) && plan.MTU.ValueInt64() == 0 {
|
|
toDelete = append(toDelete, "mtu")
|
|
body.MTU = nil
|
|
}
|
|
|
|
if !plan.Gateway.Equal(state.Gateway) && plan.Gateway.ValueString() == "" {
|
|
toDelete = append(toDelete, "gateway")
|
|
body.Gateway = nil
|
|
}
|
|
|
|
if !plan.Gateway6.Equal(state.Gateway6) && plan.Gateway6.ValueString() == "" {
|
|
toDelete = append(toDelete, "gateway6")
|
|
body.Gateway6 = nil
|
|
}
|
|
|
|
// VLANAware is computed, will never be null
|
|
if !plan.VLANAware.Equal(state.VLANAware) && !plan.VLANAware.ValueBool() {
|
|
toDelete = append(toDelete, "bridge_vlan_aware")
|
|
body.BridgeVLANAware = nil
|
|
}
|
|
|
|
if len(toDelete) > 0 {
|
|
body.Delete = &toDelete
|
|
}
|
|
|
|
err := r.client.Node(plan.NodeName.ValueString()).UpdateNetworkInterface(ctx, plan.Name.ValueString(), body)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError(
|
|
"Error updating Linux Bridge interface",
|
|
"Could not update Linux Bridge, unexpected error: "+err.Error(),
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
found := r.read(ctx, &plan, &resp.Diagnostics)
|
|
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
if !found {
|
|
resp.Diagnostics.AddError(
|
|
"Linux Bridge interface not found after update",
|
|
fmt.Sprintf(
|
|
"Interface %q on node %q could not be read after update",
|
|
plan.Name.ValueString(), plan.NodeName.ValueString()),
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
|
|
|
|
err = r.client.Node(state.NodeName.ValueString()).ReloadNetworkConfiguration(ctx)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError(
|
|
"Error reloading network configuration",
|
|
fmt.Sprintf("Could not reload network configuration on node '%s', unexpected error: %s",
|
|
state.NodeName.ValueString(), err.Error()),
|
|
)
|
|
}
|
|
}
|
|
|
|
// Delete deletes a Linux Bridge interface.
|
|
//
|
|
//nolint:dupl
|
|
func (r *linuxBridgeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
|
var state linuxBridgeResourceModel
|
|
diags := req.State.Get(ctx, &state)
|
|
resp.Diagnostics.Append(diags...)
|
|
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
err := r.client.Node(state.NodeName.ValueString()).DeleteNetworkInterface(ctx, state.Name.ValueString())
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "interface does not exist") {
|
|
resp.Diagnostics.AddWarning(
|
|
"Linux Bridge interface does not exist",
|
|
fmt.Sprintf("Could not delete Linux Bridge '%s', interface does not exist, "+
|
|
"or has already been deleted outside of Terraform.", state.Name.ValueString()),
|
|
)
|
|
} else {
|
|
resp.Diagnostics.AddError(
|
|
"Error deleting Linux Bridge interface",
|
|
fmt.Sprintf("Could not delete Linux Bridge '%s', unexpected error: %s",
|
|
state.Name.ValueString(), err.Error()),
|
|
)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
err = r.client.Node(state.NodeName.ValueString()).ReloadNetworkConfiguration(ctx)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError(
|
|
"Error reloading network configuration",
|
|
fmt.Sprintf("Could not reload network configuration on node '%s', unexpected error: %s",
|
|
state.NodeName.ValueString(), err.Error()),
|
|
)
|
|
}
|
|
}
|
|
|
|
func (r *linuxBridgeResource) ImportState(
|
|
ctx context.Context,
|
|
req resource.ImportStateRequest,
|
|
resp *resource.ImportStateResponse,
|
|
) {
|
|
idParts := strings.Split(req.ID, ":")
|
|
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
|
|
resp.Diagnostics.AddError(
|
|
"Unexpected Import Identifier",
|
|
fmt.Sprintf("Expected import identifier with format: `node_name:iface`. Got: %q", req.ID),
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
nodeName := idParts[0]
|
|
iface := idParts[1]
|
|
|
|
state := linuxBridgeResourceModel{
|
|
ID: types.StringValue(req.ID),
|
|
NodeName: types.StringValue(nodeName),
|
|
Name: types.StringValue(iface),
|
|
}
|
|
found := r.read(ctx, &state, &resp.Diagnostics)
|
|
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
if !found {
|
|
resp.Diagnostics.AddError(
|
|
"Linux Bridge interface not found",
|
|
fmt.Sprintf("Interface %q on node %q could not be imported", iface, nodeName),
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
diags := resp.State.Set(ctx, state)
|
|
resp.Diagnostics.Append(diags...)
|
|
}
|