Plenty of writes now works and an expanded example exists in the terraform directory

This commit is contained in:
Joachim Friberg committed 2026-08-25 20:25:08 +02:00
1 parent a6f1019175
commit fb8d468943
15 files changed
+1344 -74

No files matched your search

@@ -0,0 +1,253 @@
// First resource ported straight from a saved snapshot (no Python
// extractors/actions equivalent existed yet) -- see
// ~/code/experiments/hpe/page_snapshots/greenmode.html.html. Safest write
// target after Locator per AGENT.md's risk triage: EEE power-saving, no
// traffic impact.
//
// The page has two tables in one <FORM>: "Green Features Configuration"
// (Green Mode, Mode LED Time) and "Phy Auto Power-Down" (Mode). All three
// fields are always submitted together on any change -- the firmware's form
// includes all of them at once in a real browser submission, and there's no
// evidence omitted fields are left alone rather than reset, so this plays it
// safe and always sends current-or-desired values for all three.
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/types"
"terraform-provider-hpe1810/internal/provider/client"
)
const (
greenFeaturesPath = "/greenmode.html"
greenFeaturesCaption = "Green Features Configuration"
greenFeaturesGreenModeField = "Green Mode"
greenFeaturesLEDTimeField = "Mode LED Time"
phyAutoPowerDownCaption = "Phy Auto Power-Down"
phyAutoPowerDownModeField = "Mode"
)
var (
_ resource.Resource = &greenFeaturesResource{}
_ resource.ResourceWithConfigure = &greenFeaturesResource{}
)
func NewGreenFeaturesResource() resource.Resource {
return &greenFeaturesResource{}
}
type greenFeaturesResource struct {
client *client.Client
}
type greenFeaturesResourceModel struct {
ID types.String `tfsdk:"id"`
GreenMode types.Bool `tfsdk:"green_mode"`
ModeLEDTime types.Int64 `tfsdk:"mode_led_time"`
PhyAutoPowerDown types.Bool `tfsdk:"phy_auto_power_down"`
}
func (r *greenFeaturesResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_green_features"
}
func (r *greenFeaturesResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Controls the switch's Green Features (EEE power-saving) page. Singleton -- there is only ever one of these per switch. Delete resets all three fields to their documented factory defaults (Green Mode/Phy Auto Power-Down Enable, Mode LED Time 10).",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
Description: "Always \"green_features\" -- this is a singleton resource.",
},
"green_mode": schema.BoolAttribute{
Required: true,
Description: "Whether EEE (Energy Efficient Ethernet) is enabled.",
},
"mode_led_time": schema.Int64Attribute{
Required: true,
Description: "Minutes of link-up activity before the port LED dims in green mode. Range 1-30 (switch default: 10).",
},
"phy_auto_power_down": schema.BoolAttribute{
Required: true,
Description: "Whether PHY Auto Power-Down (link-partner-absent power saving) is enabled.",
},
},
}
}
func (r *greenFeaturesResource) 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
}
type greenFeaturesState struct {
GreenMode bool
ModeLEDTime int64
PhyAutoPowerDown bool
}
func (r *greenFeaturesResource) readState() (greenFeaturesState, error) {
tables, err := r.client.FetchTables(greenFeaturesPath)
if err != nil {
return greenFeaturesState{}, err
}
green, ok := tables[greenFeaturesCaption]
if !ok {
return greenFeaturesState{}, fmt.Errorf("table captioned %q not found on %s", greenFeaturesCaption, greenFeaturesPath)
}
phy, ok := tables[phyAutoPowerDownCaption]
if !ok {
return greenFeaturesState{}, fmt.Errorf("table captioned %q not found on %s", phyAutoPowerDownCaption, greenFeaturesPath)
}
ledTime, err := strconv.ParseInt(green.Record[greenFeaturesLEDTimeField], 10, 64)
if err != nil {
return greenFeaturesState{}, fmt.Errorf("parsing %q as int: %w", green.Record[greenFeaturesLEDTimeField], err)
}
return greenFeaturesState{
GreenMode: green.Record[greenFeaturesGreenModeField] == "Enable",
ModeLEDTime: ledTime,
PhyAutoPowerDown: phy.Record[phyAutoPowerDownModeField] == "Enable",
}, nil
}
func (r *greenFeaturesResource) setState(desired greenFeaturesState) error {
tables, err := r.client.FetchTables(greenFeaturesPath)
if err != nil {
return err
}
green, ok := tables[greenFeaturesCaption]
if !ok {
return fmt.Errorf("table captioned %q not found on %s", greenFeaturesCaption, greenFeaturesPath)
}
phy, ok := tables[phyAutoPowerDownCaption]
if !ok {
return fmt.Errorf("table captioned %q not found on %s", phyAutoPowerDownCaption, greenFeaturesPath)
}
greenModeField, ok := green.FieldNames[greenFeaturesGreenModeField]
if !ok {
return fmt.Errorf("field %q not found in table %q", greenFeaturesGreenModeField, greenFeaturesCaption)
}
ledTimeField, ok := green.FieldNames[greenFeaturesLEDTimeField]
if !ok {
return fmt.Errorf("field %q not found in table %q", greenFeaturesLEDTimeField, greenFeaturesCaption)
}
phyModeField, ok := phy.FieldNames[phyAutoPowerDownModeField]
if !ok {
return fmt.Errorf("field %q not found in table %q", phyAutoPowerDownModeField, phyAutoPowerDownCaption)
}
enableDisable := func(b bool) string {
if b {
return "Enable"
}
return "Disable"
}
_, err = r.client.SubmitForm(greenFeaturesPath, map[string]string{
greenModeField: enableDisable(desired.GreenMode),
ledTimeField: strconv.FormatInt(desired.ModeLEDTime, 10),
phyModeField: enableDisable(desired.PhyAutoPowerDown),
})
return err
}
func stateToModel(s greenFeaturesState) greenFeaturesResourceModel {
return greenFeaturesResourceModel{
ID: types.StringValue("green_features"),
GreenMode: types.BoolValue(s.GreenMode),
ModeLEDTime: types.Int64Value(s.ModeLEDTime),
PhyAutoPowerDown: types.BoolValue(s.PhyAutoPowerDown),
}
}
func (r *greenFeaturesResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan greenFeaturesResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
desired := greenFeaturesState{
GreenMode: plan.GreenMode.ValueBool(),
ModeLEDTime: plan.ModeLEDTime.ValueInt64(),
PhyAutoPowerDown: plan.PhyAutoPowerDown.ValueBool(),
}
if err := r.setState(desired); err != nil {
resp.Diagnostics.AddError("Unable to set Green Features state", err.Error())
return
}
actual, err := r.readState()
if err != nil {
resp.Diagnostics.AddError("Unable to read back Green Features state", err.Error())
return
}
state := stateToModel(actual)
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (r *greenFeaturesResource) Read(ctx context.Context, _ resource.ReadRequest, resp *resource.ReadResponse) {
actual, err := r.readState()
if err != nil {
resp.Diagnostics.AddError("Unable to read Green Features state", err.Error())
return
}
state := stateToModel(actual)
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (r *greenFeaturesResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan greenFeaturesResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
desired := greenFeaturesState{
GreenMode: plan.GreenMode.ValueBool(),
ModeLEDTime: plan.ModeLEDTime.ValueInt64(),
PhyAutoPowerDown: plan.PhyAutoPowerDown.ValueBool(),
}
if err := r.setState(desired); err != nil {
resp.Diagnostics.AddError("Unable to set Green Features state", err.Error())
return
}
actual, err := r.readState()
if err != nil {
resp.Diagnostics.AddError("Unable to read back Green Features state", err.Error())
return
}
state := stateToModel(actual)
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
// Delete resets all three fields to their documented factory defaults --
// safe here since this is a pure power-saving feature with no traffic
// impact (Safe tier in AGENT.md's risk triage), unlike e.g. Network Setup
// where touching anything on delete would be the risky move.
func (r *greenFeaturesResource) Delete(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) {
defaults := greenFeaturesState{GreenMode: true, ModeLEDTime: 10, PhyAutoPowerDown: true}
if err := r.setState(defaults); err != nil {
resp.Diagnostics.AddError("Unable to reset Green Features state on delete", err.Error())
}
}