Files

219 lines
7.7 KiB
Go

// Port of PortConfiguration.html -- see client/port_config.go's package doc
// for why this page needed dedicated (non-generic) client-side handling.
//
// This is a materially riskier write target than Locator/Green Features: a
// wrong write can disable the port a management session, or the switch's
// only uplink, depends on. See the admin-port guard below.
package provider
import (
"context"
"fmt"
"strconv"
"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/types"
"terraform-provider-hpe1810/internal/provider/client"
)
var (
_ resource.Resource = &portResource{}
_ resource.ResourceWithConfigure = &portResource{}
)
func NewPortResource() resource.Resource {
return &portResource{}
}
type portResource struct {
client *client.Client
}
type portResourceModel struct {
ID types.String `tfsdk:"id"`
Interface types.String `tfsdk:"interface"`
AdminMode types.Bool `tfsdk:"admin_mode"`
LinkSpeed types.String `tfsdk:"link_speed"`
PhysicalType types.String `tfsdk:"physical_type"`
LinkStatus types.String `tfsdk:"link_status"`
}
func (r *portResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_port"
}
func (r *portResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Controls one switch port's Admin Mode and Link Speed. Intended to be driven with for_each over a map keyed by port number. The provider's admin_port (default 24) can never be written by this resource, regardless of what's in the for_each map -- see the provider schema.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
Description: "Same as interface.",
},
"interface": schema.StringAttribute{
Required: true,
Description: "Port number, \"1\"..\"24\". Changing this replaces the resource (it's the for_each key in normal usage).",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"admin_mode": schema.BoolAttribute{
Required: true,
Description: "Whether the port is enabled.",
},
"link_speed": schema.StringAttribute{
Required: true,
Description: "e.g. \"Auto\", \"100 Mbps Full Duplex\". Not enum-validated client-side -- the switch's accepted values for gigabit copper ports aren't fully confirmed, so invalid values are caught by reading the value back rather than guessed at up front.",
},
"physical_type": schema.StringAttribute{
Computed: true,
Description: "Read-only, e.g. \"No SFP\" for RJ45 copper.",
},
"link_status": schema.StringAttribute{
Computed: true,
Description: "Read-only, \"Link Up\" or \"Link Down\".",
},
},
}
}
func (r *portResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
c, ok := req.ProviderData.(*client.Client)
if !ok {
resp.Diagnostics.AddError("Unexpected resource configure type", fmt.Sprintf("expected *client.Client, got %T", req.ProviderData))
return
}
r.client = c
}
// guardAdminPort refuses any write targeting the provider's protected
// admin_port (default 24 -- the switch's uplink), and refuses a
// non-numeric interface value outright. This is the safety net requested
// explicitly: even if the admin port ends up in a for_each map by mistake,
// the write never reaches the switch.
func (r *portResource) guardAdminPort(interfaceStr string) error {
n, err := strconv.Atoi(interfaceStr)
if err != nil {
return fmt.Errorf("interface %q is not a valid port number: %w", interfaceStr, err)
}
if int64(n) == r.client.AdminPort {
return fmt.Errorf("refusing to write to interface %q: it's the provider's admin_port (%d), protected from all writes by this resource", interfaceStr, r.client.AdminPort)
}
return nil
}
func portConfigToModel(pc client.PortConfig) portResourceModel {
return portResourceModel{
ID: types.StringValue(pc.Interface),
Interface: types.StringValue(pc.Interface),
AdminMode: types.BoolValue(pc.AdminMode),
LinkSpeed: types.StringValue(pc.LinkSpeed),
PhysicalType: types.StringValue(pc.PhysicalType),
LinkStatus: types.StringValue(pc.LinkStatus),
}
}
func (r *portResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan portResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
iface := plan.Interface.ValueString()
if err := r.guardAdminPort(iface); err != nil {
resp.Diagnostics.AddError("Refusing to write to admin_port", err.Error())
return
}
if err := r.client.SetPortConfig(iface, plan.AdminMode.ValueBool(), plan.LinkSpeed.ValueString()); err != nil {
resp.Diagnostics.AddError("Unable to set port config", err.Error())
return
}
pc, err := r.client.ReadPortConfig(iface)
if err != nil {
resp.Diagnostics.AddError("Unable to read back port config", err.Error())
return
}
state := portConfigToModel(pc)
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (r *portResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state portResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
pc, err := r.client.ReadPortConfig(state.Interface.ValueString())
if err != nil {
resp.Diagnostics.AddError("Unable to read port config", err.Error())
return
}
newState := portConfigToModel(pc)
resp.Diagnostics.Append(resp.State.Set(ctx, &newState)...)
}
func (r *portResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan portResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
iface := plan.Interface.ValueString()
if err := r.guardAdminPort(iface); err != nil {
resp.Diagnostics.AddError("Refusing to write to admin_port", err.Error())
return
}
if err := r.client.SetPortConfig(iface, plan.AdminMode.ValueBool(), plan.LinkSpeed.ValueString()); err != nil {
resp.Diagnostics.AddError("Unable to set port config", err.Error())
return
}
pc, err := r.client.ReadPortConfig(iface)
if err != nil {
resp.Diagnostics.AddError("Unable to read back port config", err.Error())
return
}
state := portConfigToModel(pc)
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
// Delete disables the port and resets Link Speed to Auto -- matches
// AGENT.md's Port Configuration risk-triage decision: "no longer managed by
// Terraform" should fail toward safer (closed), not toward whatever it
// happened to be. Safe to apply unconditionally here because the one port
// that must never be disabled (admin_port) is structurally excluded by the
// guard below -- it can never have been Created in the first place.
func (r *portResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state portResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
iface := state.Interface.ValueString()
if err := r.guardAdminPort(iface); err != nil {
resp.Diagnostics.AddError("Refusing to write to admin_port", err.Error())
return
}
if err := r.client.SetPortConfig(iface, false, "Auto"); err != nil {
resp.Diagnostics.AddError("Unable to reset port config on delete", err.Error())
}
}