Now works - more or less fully
This commit is contained in:
1 parent
fb8d468943
commit
92701aab92
13 files changed
+1938
-12
No files matched your search
@@ -0,0 +1,123 @@
|
||||
// Summer_Time_Configuration.html has THREE DST modes (Disable, Recurring,
|
||||
// NonRecurring), each with its own full set of fields, ALL present in the
|
||||
// DOM simultaneously (JS shows/hides based on the selected mode) -- same
|
||||
// shape of problem as PortConfiguration.html's duplicate Link Speed fields,
|
||||
// except here several labels collide WITHIN one table (Month/Hours/Minutes/
|
||||
// Offset each appear once in the NonRecurring block and once in the
|
||||
// Recurring block). Rather than lean on generic xepage.go parsing and its
|
||||
// document-order last-wins behavior, this reads/writes via hardcoded field
|
||||
// names extracted directly with regex -- unambiguous regardless of DOM
|
||||
// order.
|
||||
//
|
||||
// Only Disable and Recurring are supported; NonRecurring is rejected
|
||||
// outright rather than guessed at (it uses a different, absolute-date field
|
||||
// set: Month/Date/Year/Hours/Minutes, v_3_1_1..v_3_10_1).
|
||||
//
|
||||
// Confirmed live before this was wired into a resource: submitting mode +
|
||||
// only the Recurring block's fields (omitting the NonRecurring block
|
||||
// entirely) was accepted with no err_flag and no FILTER_MISSING -- unlike
|
||||
// PortConfiguration.html, this page does NOT require hidden hold_all_fields.
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
dstConfigPath = "/Summer_Time_Configuration.html"
|
||||
dstModeField = "v_1_1_1"
|
||||
dstStartWeekField = "v_3_12_1"
|
||||
dstStartDayField = "v_3_13_1"
|
||||
dstStartMonthField = "v_3_14_1"
|
||||
dstStartHourField = "v_3_15_1"
|
||||
dstStartMinuteField = "v_3_399_1"
|
||||
dstEndWeekField = "v_3_16_1"
|
||||
dstEndDayField = "v_3_17_1"
|
||||
dstEndMonthField = "v_3_18_1"
|
||||
dstEndHourField = "v_3_19_1"
|
||||
dstEndMinuteField = "v_3_498_1"
|
||||
dstOffsetField = "v_3_20_1"
|
||||
dstOffsetAcronymField = "v_3_21_1"
|
||||
|
||||
DSTModeDisable = "Disable"
|
||||
DSTModeRecurring = "Recurring"
|
||||
)
|
||||
|
||||
type DSTConfig struct {
|
||||
Mode string
|
||||
StartWeek int
|
||||
StartDay string
|
||||
StartMonth string
|
||||
StartHour int
|
||||
StartMinute int
|
||||
EndWeek int
|
||||
EndDay string
|
||||
EndMonth string
|
||||
EndHour int
|
||||
EndMinute int
|
||||
OffsetMinutes int
|
||||
OffsetAcronym string
|
||||
}
|
||||
|
||||
func extractHiddenFieldValue(html, name string) string {
|
||||
re := regexp.MustCompile(`(?i)NAME=` + regexp.QuoteMeta(name) + `\b[^>]*VALUE="([^"]*)"`)
|
||||
m := re.FindStringSubmatch(html)
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
return m[1]
|
||||
}
|
||||
|
||||
func (c *Client) ReadDSTConfig() (DSTConfig, error) {
|
||||
html, err := c.Fetch(dstConfigPath)
|
||||
if err != nil {
|
||||
return DSTConfig{}, err
|
||||
}
|
||||
|
||||
atoi := func(name string) int {
|
||||
n, _ := strconv.Atoi(extractHiddenFieldValue(html, name))
|
||||
return n
|
||||
}
|
||||
|
||||
return DSTConfig{
|
||||
Mode: extractHiddenFieldValue(html, dstModeField),
|
||||
StartWeek: atoi(dstStartWeekField),
|
||||
StartDay: extractHiddenFieldValue(html, dstStartDayField),
|
||||
StartMonth: extractHiddenFieldValue(html, dstStartMonthField),
|
||||
StartHour: atoi(dstStartHourField),
|
||||
StartMinute: atoi(dstStartMinuteField),
|
||||
EndWeek: atoi(dstEndWeekField),
|
||||
EndDay: extractHiddenFieldValue(html, dstEndDayField),
|
||||
EndMonth: extractHiddenFieldValue(html, dstEndMonthField),
|
||||
EndHour: atoi(dstEndHourField),
|
||||
EndMinute: atoi(dstEndMinuteField),
|
||||
OffsetMinutes: atoi(dstOffsetField),
|
||||
OffsetAcronym: extractHiddenFieldValue(html, dstOffsetAcronymField),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) SetDSTConfig(desired DSTConfig) error {
|
||||
if desired.Mode != DSTModeDisable && desired.Mode != DSTModeRecurring {
|
||||
return fmt.Errorf("unsupported Daylight Saving Time mode %q -- this resource only supports %q and %q (NonRecurring is not supported)", desired.Mode, DSTModeDisable, DSTModeRecurring)
|
||||
}
|
||||
|
||||
fields := map[string]string{
|
||||
dstModeField: desired.Mode,
|
||||
dstStartWeekField: strconv.Itoa(desired.StartWeek),
|
||||
dstStartDayField: desired.StartDay,
|
||||
dstStartMonthField: desired.StartMonth,
|
||||
dstStartHourField: strconv.Itoa(desired.StartHour),
|
||||
dstStartMinuteField: strconv.Itoa(desired.StartMinute),
|
||||
dstEndWeekField: strconv.Itoa(desired.EndWeek),
|
||||
dstEndDayField: desired.EndDay,
|
||||
dstEndMonthField: desired.EndMonth,
|
||||
dstEndHourField: strconv.Itoa(desired.EndHour),
|
||||
dstEndMinuteField: strconv.Itoa(desired.EndMinute),
|
||||
dstOffsetField: strconv.Itoa(desired.OffsetMinutes),
|
||||
dstOffsetAcronymField: desired.OffsetAcronym,
|
||||
}
|
||||
_, err := c.SubmitForm(dstConfigPath, fields)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
// Trunking (link aggregation / LAG) spans two pages with a genuinely
|
||||
// different shape from everything else ported so far: TrunkConfig.html is a
|
||||
// dynamic list of named entities (create/delete trunks, each gets a
|
||||
// switch-assigned "TrunkN" id), and TrunkMembership.html assigns member
|
||||
// ports to one of them. Every other resource so far is a singleton or a
|
||||
// fixed for_each set (24 ports); this is the first with real create/delete
|
||||
// of sub-entities.
|
||||
//
|
||||
// All three operations (Create, set members, Delete) were verified live
|
||||
// before being wired into a Terraform resource, using a throwaway test
|
||||
// trunk on ports 5 and 6 (never port 24), cleaned up afterward:
|
||||
//
|
||||
// - Create: POST TrunkConfig.html with v_1_1_1=Enable (the "Create"
|
||||
// field), v_1_2_1=<name>, v_1_4_1=Add. The switch assigns the next
|
||||
// "TrunkN" id automatically.
|
||||
// - Set members: POST TrunkMembership.html with the selected trunk id in
|
||||
// v_1_1_1, plus ALL 24 ports' v_2_3_1 (port number), v_2_3_2 (per-port
|
||||
// LACP mode), v_2_3_3 (membership: the trunk id string if a member,
|
||||
// "1" if not -- the exact sentinel the page's own JS uses for "off").
|
||||
// Two gotchas confirmed live: (1) the page's other hidden context
|
||||
// fields (v_1_2_1, v_1_2_2, v_1_3_1, v_5_1_1, v_6_1_1, v_2_1_2, v_2_1_3,
|
||||
// v_2_1_4, v_3_1_1, v_3_2_1, v_3_3_1, v_3_4_1) must be resubmitted
|
||||
// unchanged or the switch rejects with FILTER_MISSING; (2) v_2_3_1 is
|
||||
// marked DISABLED in the raw HTML (a real browser wouldn't normally
|
||||
// submit a disabled field), but the page's own JS
|
||||
// (lagViewAutoGen.js's showvalues()) explicitly un-disables it right
|
||||
// before submit -- omitting it also fails with FILTER_MISSING.
|
||||
// - Delete: POST TrunkConfig.html with that trunk's row fields
|
||||
// (v_1_5_1..v_1_5_6) resubmitted unchanged, plus v_1_5_7=Enable (the
|
||||
// "Delete" trigger) and v_1_5_8=Delete (a fixed action-name constant).
|
||||
// Omitting the row's current-value fields fails with "Error! Failed to
|
||||
// remove LAG." Critically, including the page's top-level Create
|
||||
// section (v_1_1_1/v_1_2_1/v_1_4_1) in the SAME request as a delete
|
||||
// causes the switch to create ANOTHER trunk as a side effect instead --
|
||||
// confirmed live (deleting "Trunk1" while resending v_1_4_1=Add
|
||||
// produced a brand new empty "Trunk2"). So a delete request must omit
|
||||
// those fields entirely.
|
||||
//
|
||||
// The per-row/per-trunk field name prefix ("1.<index>.1.") uses this
|
||||
// trunk's zero-based position in ListTrunks()'s order, the same positional
|
||||
// pattern used throughout this firmware (PortConfiguration, Port
|
||||
// Mirroring, LLDP, Trunk Membership). Verified live only for the
|
||||
// single-trunk case (index 0); multi-trunk indexing is inferred by pattern,
|
||||
// not independently confirmed against a live switch with 2+ trunks.
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
trunkConfigPath = "/TrunkConfig.html"
|
||||
trunkMembershipPath = "/TrunkMembership.html"
|
||||
trunkSummaryTableID = "1_5"
|
||||
|
||||
trunkInterfaceLabel = "Interface"
|
||||
trunkNameLabel = "Trunk Name"
|
||||
trunkMembersLabel = "Trunk Members"
|
||||
trunkAdminModeLabel = "Admin Mode"
|
||||
trunkStaticCapLabel = "Static Capability"
|
||||
|
||||
trunkCreateField = "v_1_1_1"
|
||||
trunkNameField = "v_1_2_1"
|
||||
trunkAddActionField = "v_1_4_1"
|
||||
|
||||
trunkRowInterfaceField = "v_1_5_1"
|
||||
trunkRowNameField = "v_1_5_2"
|
||||
trunkRowMembersField = "v_1_5_5"
|
||||
trunkRowAdminField = "v_1_5_3"
|
||||
trunkRowStaticField = "v_1_5_4"
|
||||
trunkRowModifyField = "v_1_5_6"
|
||||
trunkRowDeleteField = "v_1_5_7"
|
||||
trunkRowDeleteActionField = "v_1_5_8"
|
||||
)
|
||||
|
||||
type Trunk struct {
|
||||
ID string
|
||||
Name string
|
||||
Members []string
|
||||
AdminMode bool
|
||||
StaticCapability bool
|
||||
}
|
||||
|
||||
// ListTrunks returns every configured trunk, in the switch's own display
|
||||
// order -- that order also determines the zero-based row index used in
|
||||
// per-row field names, needed by DeleteTrunk.
|
||||
func (c *Client) ListTrunks() ([]Trunk, error) {
|
||||
tables, err := c.FetchAllTables(trunkConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var summary *Table
|
||||
for i := range tables {
|
||||
if tables[i].ID == trunkSummaryTableID {
|
||||
summary = &tables[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if summary == nil {
|
||||
return nil, fmt.Errorf("table id %q not found on %s", trunkSummaryTableID, trunkConfigPath)
|
||||
}
|
||||
|
||||
trunks := make([]Trunk, 0, len(summary.Rows))
|
||||
for _, row := range summary.Rows {
|
||||
var members []string
|
||||
if m := row[trunkMembersLabel]; m != "" {
|
||||
members = strings.Split(m, ",")
|
||||
}
|
||||
trunks = append(trunks, Trunk{
|
||||
ID: row[trunkInterfaceLabel],
|
||||
Name: row[trunkNameLabel],
|
||||
Members: members,
|
||||
AdminMode: row[trunkAdminModeLabel] == "Enable",
|
||||
StaticCapability: row[trunkStaticCapLabel] == "Enable",
|
||||
})
|
||||
}
|
||||
return trunks, nil
|
||||
}
|
||||
|
||||
// FindTrunkByName returns the trunk with the given name, its zero-based
|
||||
// row index (for DeleteTrunk), or (nil, -1, nil) if no trunk has that name.
|
||||
func (c *Client) FindTrunkByName(name string) (*Trunk, int, error) {
|
||||
trunks, err := c.ListTrunks()
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
}
|
||||
for i, t := range trunks {
|
||||
if t.Name == name {
|
||||
found := t
|
||||
return &found, i, nil
|
||||
}
|
||||
}
|
||||
return nil, -1, nil
|
||||
}
|
||||
|
||||
// CreateTrunk creates a new trunk with the given name (1-15 alphanumeric
|
||||
// characters) and returns it once created, including its switch-assigned
|
||||
// "TrunkN" id.
|
||||
func (c *Client) CreateTrunk(name string) (Trunk, error) {
|
||||
_, err := c.SubmitForm(trunkConfigPath, map[string]string{
|
||||
trunkCreateField: "Enable",
|
||||
trunkNameField: name,
|
||||
trunkAddActionField: "Add",
|
||||
})
|
||||
if err != nil {
|
||||
return Trunk{}, err
|
||||
}
|
||||
trunk, _, err := c.FindTrunkByName(name)
|
||||
if err != nil {
|
||||
return Trunk{}, err
|
||||
}
|
||||
if trunk == nil {
|
||||
return Trunk{}, fmt.Errorf("trunk %q not found after creation", name)
|
||||
}
|
||||
return *trunk, nil
|
||||
}
|
||||
|
||||
// DeleteTrunk removes the named trunk. See the package doc comment for the
|
||||
// two confirmed-live gotchas (must resend the row's current values, must
|
||||
// NOT include the page's Create-section fields).
|
||||
func (c *Client) DeleteTrunk(name string) error {
|
||||
trunk, index, err := c.FindTrunkByName(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if trunk == nil {
|
||||
return nil // already gone
|
||||
}
|
||||
|
||||
adminValue := "Disable"
|
||||
if trunk.AdminMode {
|
||||
adminValue = "Enable"
|
||||
}
|
||||
staticValue := "Disable"
|
||||
if trunk.StaticCapability {
|
||||
staticValue = "Enable"
|
||||
}
|
||||
|
||||
prefix := fmt.Sprintf("1.%d.1.", index)
|
||||
_, err = c.SubmitForm(trunkConfigPath, map[string]string{
|
||||
prefix + trunkRowInterfaceField: trunk.ID,
|
||||
prefix + trunkRowNameField: trunk.Name,
|
||||
prefix + trunkRowMembersField: strings.Join(trunk.Members, ","),
|
||||
prefix + trunkRowAdminField: adminValue,
|
||||
prefix + trunkRowStaticField: staticValue,
|
||||
prefix + trunkRowModifyField: "Disable",
|
||||
prefix + trunkRowDeleteField: "Enable",
|
||||
prefix + trunkRowDeleteActionField: "Delete",
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
const trunkMembershipTrunkIDField = "v_1_1_1"
|
||||
|
||||
// trunkMembershipHiddenFields must be resubmitted unchanged on every
|
||||
// SetTrunkMembers call or the switch rejects the write with FILTER_MISSING
|
||||
// -- confirmed live.
|
||||
var trunkMembershipHiddenFields = []string{
|
||||
"v_1_2_1", "v_1_2_2", "v_1_3_1", "v_5_1_1", "v_6_1_1",
|
||||
"v_2_1_2", "v_2_1_3", "v_2_1_4", "v_3_1_1", "v_3_2_1", "v_3_3_1", "v_3_4_1",
|
||||
}
|
||||
|
||||
// SetTrunkMembers sets exactly which of the switch's 24 ports belong to
|
||||
// trunkID (e.g. "Trunk1"), replacing any previous membership entirely.
|
||||
func (c *Client) SetTrunkMembers(trunkID string, members map[string]bool) error {
|
||||
// Select the trunk first (submit_flag=1, reload -- doesn't apply
|
||||
// anything, just like PortConfiguration.html's port selector) so the
|
||||
// hidden context fields we read back reflect this specific trunk, not
|
||||
// whatever was last selected.
|
||||
html, err := c.postForm(trunkMembershipPath, map[string]string{
|
||||
"submit_flag": "1",
|
||||
"submit_target": "TrunkMembership.html",
|
||||
"err_flag": "0",
|
||||
"err_msg": "",
|
||||
"clazz_information": "TrunkMembership.html",
|
||||
trunkMembershipTrunkIDField: trunkID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fields := map[string]string{trunkMembershipTrunkIDField: trunkID}
|
||||
for _, name := range trunkMembershipHiddenFields {
|
||||
fields[name] = extractHiddenFieldValue(html, name)
|
||||
}
|
||||
|
||||
for i := 0; i < 24; i++ {
|
||||
port := strconv.Itoa(i + 1)
|
||||
prefix := fmt.Sprintf("1.%d.24.", i)
|
||||
fields[prefix+"v_2_3_1"] = port
|
||||
fields[prefix+"v_2_3_2"] = "Enable"
|
||||
if members[port] {
|
||||
fields[prefix+"v_2_3_3"] = trunkID
|
||||
} else {
|
||||
fields[prefix+"v_2_3_3"] = "1"
|
||||
}
|
||||
}
|
||||
|
||||
_, err = c.SubmitForm(trunkMembershipPath, fields)
|
||||
return err
|
||||
}
|
||||
Reference in new issue
Block a user