diff --git a/.gitignore b/.gitignore
index 12370ae..18f0c8a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
__pycache__/
*.pyc
.session_cookie
+terraform-provider-hpe1810/terraform-provider-hpe1810
diff --git a/AGENT.md b/AGENT.md
index fdba2e0..c7e6355 100644
--- a/AGENT.md
+++ b/AGENT.md
@@ -5,6 +5,9 @@
A Python PoC that scrapes and writes to an HP ProCurve/OfficeConnect 1810 (J9450A)
switch's built-in web UI at `192.168.2.10` (no auth configured on the switch).
+- `config.py` — connection config from env vars (`SWITCH_HOST`, `SWITCH_HTTPS`,
+ `SWITCH_PASSWORD`). These three map directly onto what a future Terraform provider's
+ config block would need per-switch.
- `switch_client.py` — session/login (`get_session()`), generic `fetch()` (GET) and
`submit_form()` (POST, for writes).
- `extractors/xe_page.py` — generic parser for the firmware's auto-generated ("XE") pages;
@@ -16,41 +19,166 @@ switch's built-in web UI at `192.168.2.10` (no auth configured on the switch).
setting (currently just `locator.py`).
- `todo.py` / `TODO.md` — tracks which menu items have a read extractor implemented.
-## Long-term goal: become a Terraform module
+## Long-term goal: become a Terraform provider (full Go rewrite)
-The intended end state is a Terraform provider/module for this switch (and possibly the
-pattern generalized to similar switches). Terraform's core flow — as far as understood
-right now, exact provider-SDK details not yet researched — is: **read current state →
-diff against declared (desired) state → apply only the difference**. Each switch setting
-(Locator, System Name, VLAN config, port config, ...) should eventually become a Terraform
-resource with that Read/Diff/Apply semantics.
+The intended end state is a real Terraform provider for this switch. Researched and
+decided: Terraform's plugin protocol requires Go (HashiCorp's Terraform Plugin Framework
+is the current recommended SDK — see sources below), and the plan is a **full rewrite in
+Go**, not a thin Go shell calling out to this Python code. This Python project stays as-is
+as the reference implementation the Go port gets verified against.
-This is **not** an active task — no provider scaffolding, no SDK choice, no `ensure()`
-helper exists yet. It's noted here so code written in the meantime doesn't need reshaping
-later.
+**Correction to an earlier assumption**: Terraform **Core** does the diffing itself
+(compares saved state vs. declared config via the resource's schema) — the provider does
+NOT need its own `ensure()`/diff mechanism. It only implements:
+- **Read**: fetch real current state → write into Terraform state.
+- **Update**: called by Core only when Core already determined there's a difference.
-### What this means for code written now
+This still supports the existing guideline below (keep `get_x`/`set_x` separate, `set_x`
+idempotent) — just for a different reason than originally assumed: not because the
+provider diffs, but as a safety net against config drift (someone changes the switch via
+its web UI between `terraform plan` and `apply`).
-When adding a new `get_x`/`set_x` pair in `actions/`:
+Sources: [Terraform Plugin Framework (pkg.go.dev)](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework),
+[Resources — HashiCorp Developer](https://developer.hashicorp.com/terraform/plugin/framework/resources),
+[Implement a provider with the Plugin Framework (tutorial)](https://developer.hashicorp.com/terraform/tutorials/providers-plugin-framework/providers-plugin-framework-provider).
+
+### CLI tooling: OpenTofu, not Terraform
+
+The user is installing **OpenTofu** (`tofu` CLI) rather than the Terraform CLI. This
+doesn't change any of the Go code below — OpenTofu speaks the same plugin protocol as
+Terraform, and a provider built with `terraform-plugin-framework`/`terraform-plugin-go`
+(there's no separate "OpenTofu SDK") works unmodified against it. Practical differences
+only: the CLI binary is `tofu`, and local dev-override config lives in `~/.tofurc`
+(env var `TF_CLI_CONFIG_FILE` also works) instead of `~/.terraformrc`. "Terraform
+state"/"Terraform Core" below refers to the shared plugin-protocol concepts, which apply
+identically under OpenTofu.
+
+### The plan
+
+New sibling Go module: `~/code/experiments/terraform-provider-hpe1810/` (separate
+toolchain/`go.mod`, not nested in this Python project), scaffolded from HashiCorp's
+`terraform-provider-scaffolding-framework` template.
+
+- `internal/provider/client/client.go` — port of `switch_client.py` (`net/http.Client` +
+ `net/http/cookiejar` for session cookies; same login POST fields, same GET-before-POST
+ workaround, same `submit_flag=8` fix in `SubmitForm`). **Simplification vs. Python**: the
+ provider process lives for the whole `terraform plan`/`apply` run, so the disk-cached
+ session (`.session_cookie`) mostly goes away — `Configure()` logs in once, the same
+ client is reused for every resource/data source in that run. The switch's single-session
+ limit still means don't run two `terraform` processes against it concurrently.
+- `internal/provider/client/xepage.go` — port of `extractors/xe_page.py`, using `goquery`
+ (closest Go equivalent to BeautifulSoup). Same two table shapes (scalar
+ `defleft`/`defright`, tabular via `
`), same duplicate-header caveat as a comment.
+- `provider.go` schema maps 1:1 onto `config.py`'s three fields (`host`/`https`/`password`),
+ with the same env var fallback pattern already established there.
+- One data source per current `extractors/*.py` (`system_description`, `network_setup`,
+ `port_summary`, `lldp_statistics`, `buffered_log`, `mac_table`, `trunk_status`,
+ `loop_protection_status`, `dual_image_status`, `clock`, `sntp`, `log_configuration`,
+ `backup_manager`).
+- One resource per current `actions/*.py` (just `locator` today) — singleton-setting
+ resource with a fixed state ID. `Delete` semantics are decided **per resource**, not by
+ one blanket rule — the guiding question is "what's the safest state for *this* setting
+ to end up in", which sometimes means actively changing something, not just leaving it or
+ resetting to a factory default:
+ - **Locator**: reset to `Disable` — harmless, and leaving a physical LED blinking after
+ `terraform destroy` would be confusing.
+ - **Port Configuration**: `Delete` should disable/close the port — an unmanaged port is
+ a security exposure, so "no longer managed by Terraform" should fail toward *safer*
+ (closed), not toward "whatever it happened to be".
+ - **Network Setup** (IP, gateway, SNMP, management VLAN): `Delete` should be a pure
+ no-op that only drops it from Terraform state — there's no safe "unset" state for
+ the switch's own management network config; touching it on delete risks the same
+ lockout the risk triage below already warns about.
+ Decide this explicitly for each resource as it's built, using this "what does *safe*
+ mean for this specific setting" question — don't default to a single project-wide rule.
+- Testing: unit tests for `xepage.go` against `page_snapshots/*.html` copied into
+ `testdata/` (already-collected golden fixtures, runnable offline); acceptance tests via
+ `terraform-plugin-testing`, gated behind `TF_ACC=1`, only runnable against the real
+ switch.
+- Suggested build order: scaffold + `Configure()` → one vertical slice end-to-end
+ (`data.hpe1810_system_description`) → remaining data sources → `resource_locator` →
+ further write resources in the risk-triage order below (Safe category first).
+- **Status (2026-08-25)**: all 13 data sources are ported and verified with a live
+ `tofu plan` against the real switch (`~/code/experiments/hpe/terraform/`) — confirmed
+ port 24 (the active uplink) reads `Link Up`, all other ports `Link Down`. Only
+ `resource_locator` (and further write resources) remain unbuilt.
+
+### What this means for code written in this Python project meanwhile
+
+Even though the Go rewrite won't call this code directly, keep the same shape so the port
+stays a straight translation:
- `get_x_state(session)` must be a pure read — no side effects, safe to call anytime.
- `set_x(session, value)` should be idempotent — calling it again with the same `value`
- it already holds should be harmless, not something the caller has to guard against
- externally. (Not yet enforced anywhere; `actions/locator.py` always writes regardless of
- current state — fine for a manual on/off toggle, but a future idempotent version would
- read first and skip the write if already at `value`.)
-- Keep `get_x_state` and `set_x` as separate, independently callable functions rather than
- a combined "set and forget" call — that split is exactly what a Terraform resource's
- Read and Update map onto.
+ it already holds should be harmless. (Not yet enforced anywhere; `actions/locator.py`
+ always writes regardless of current state — fine for a manual on/off toggle.)
+- Keep `get_x_state` and `set_x` as separate, independently callable functions — that
+ split is exactly what a Terraform resource's Read and Update map onto.
### Explicit non-goals right now
-- No Terraform provider code, scaffolding, or SDK/language decision (Go provider vs.
- something else) exists yet.
-- No generic `ensure(desired) -> read, diff, apply-if-different` helper exists yet either —
- that's a natural next step once more `actions/` modules exist, but shouldn't be built
- speculatively ahead of need.
+- No Go module has been created yet — this is still just a plan.
+- No generic `ensure(desired) -> read, diff, apply-if-different` helper needed in Python
+ either now, given Core does the diffing in the real provider.
-Next concrete step towards this goal, when picked up: look into what a Terraform
-provider's Read/Diff/Apply contract actually requires (e.g. Terraform Plugin Framework)
-before designing anything here around it.
+## Offline snapshots
+
+`page_snapshots/` holds raw HTML + per-page `_xe_*.js` (field names/types/enums/OIDs) for
+every menu page, captured while the switch was reachable, plus `current_state.json` (a
+dump of every extractor's output at capture time) and `manifest.json` (HTML/JS file
+mapping). Use these to write and offline-verify new `extractors/`/`actions/` modules
+(parse the saved HTML directly, no live switch needed) when the switch isn't reachable —
+just re-verify against the real switch once it's back online, since a snapshot can go
+stale (config changes, firmware updates).
+
+## Write-target risk triage (for future `actions/` modules)
+
+Assessed from the snapshotted HTML/JS, not yet attempted live. Categorized by how bad a
+mistake would be and how easy it is to recover from.
+
+**Safe** (cosmetic or trivially reversible, like `actions/locator.py`):
+- Green Features (`greenmode.html`) — EEE power-saving toggle, no traffic impact.
+- Ping Test (`Ping.html`) — triggers a diagnostic ping, doesn't change config.
+- Jumbo Frames (`JumboFrames.html`) — single global MTU toggle, reversible.
+
+**Moderate** (affects real behavior, recoverable if you're paying attention, but a bad
+value has a real consequence — treat these with a real read-before-write / dry-run, not
+a blind toggle):
+- LLDP Configuration, Loop Protection Config, Port Mirroring (`FDBConfig.html`),
+ Flow Control (`SwitchConfig.html`), Trunk Configuration/Membership.
+- Time Zone / Daylight Saving Time config — wrong values just show wrong time, not
+ dangerous, but affects log timestamps everywhere.
+
+**High risk — plan for a lockout, not just "recoverable"**:
+- **Port Configuration** (`PortConfiguration.html`): disabling/misconfiguring the port
+ your management traffic actually flows over locks you out over the network (physical
+ access to the switch would be needed to recover).
+- **VLAN Configuration / VLAN Ports / Participation** (`VlanCreateConfig.html`,
+ `VLANPortConfig.html`, `VLANPortParticipation.html`): misconfiguring the management
+ VLAN or tagging is one of the most common ways to lock yourself out of a switch
+ entirely.
+- **Advanced Security / Secure Connection** (`security.html`, `SSLCfg.html`): e.g.
+ forcing HTTPS-only or an access-control change could cut off the very HTTP session
+ this project depends on.
+
+**Dangerous — do not automate without explicit, per-run human confirmation, ever**:
+- Reboot Switch (`SystemReset.html`) — drops all connections, however briefly.
+- Factory Defaults (`ResetConfigToDefaults.html`) — wipes all configuration.
+- Password Manager (`UserAccounts.html`) — setting/losing the admin password on a switch
+ that currently has none configured is exactly the kind of mistake that requires a
+ factory reset (or worse) to undo.
+- Update Manager / firmware upload (`http_file_download.html`) — a bad image can brick
+ the switch.
+- Dual Image Configuration (`dual_image_cfg.html`) — changes which firmware image boots
+ next; combined with a reboot this has the same blast radius as a firmware update gone
+ wrong.
+- Save Configuration (`SaveAllChanges.html`) — not destructive by itself (persists
+ running-config to startup-config, standard practice on this class of switch), but it's
+ what turns every other write above from "revert on next reboot" into permanent —
+ never call this automatically as a side effect of something else.
+
+Recommended order for future write targets, safest-value first: Green Features → Jumbo
+Frames → Ping Test → LLDP/Loop Protection/Trunk config → Port Configuration → VLANs. Treat
+everything in "Dangerous" as out of scope for automation entirely unless the user
+explicitly asks for that specific action, and even then prefer a human-confirmed,
+one-off script over a reusable `actions/` module.
diff --git a/CHEATSHEET.md b/CHEATSHEET.md
index 118dabd..4a2b479 100644
--- a/CHEATSHEET.md
+++ b/CHEATSHEET.md
@@ -17,6 +17,18 @@ export NO_PROXY=192.168.2.10
**Stäng switchens webb-UI i webbläsaren** innan du kör skript — den tillåter bara en inloggad session åt gången.
+## Konfiguration (`config.py`)
+
+Läses från miljövariabler, med defaults för dagens switch:
+
+```bash
+export SWITCH_HOST=192.168.2.10 # default
+export SWITCH_HTTPS=false # default; sätt till "true" om switchen kräver HTTPS
+export SWITCH_PASSWORD="" # default (ingen); sätt om switchen får ett lösenord
+```
+
+Bara sätta de du behöver ändra — resten faller tillbaka på nuvarande värden.
+
## Köra en extraktor
```bash
diff --git a/TODO.md b/TODO.md
index 1e7083f..24f7501 100644
--- a/TODO.md
+++ b/TODO.md
@@ -12,10 +12,10 @@ Regenerate with `python discover.py`. Edit the "Want it?" column by hand;
| Log | http://192.168.2.10/BufferedLogs.html | y | yes |
| Port Summary | http://192.168.2.10/PortSummary.html | y | yes |
| LLDP Statistics | http://192.168.2.10/LLDPStats.html | y | yes |
-| Trunk | http://192.168.2.10/TrunkStatus.html | | |
-| MAC Table | http://192.168.2.10/FDBSearch.html | | |
-| Loop Protection | http://192.168.2.10/LoopProtectionStatus.html | | |
-| Dual Image | http://192.168.2.10/dual_image_status.html | | |
+| Trunk | http://192.168.2.10/TrunkStatus.html | y | yes (offline, verify live) |
+| MAC Table | http://192.168.2.10/FDBSearch.html | y | yes (offline, verify live) |
+| Loop Protection | http://192.168.2.10/LoopProtectionStatus.html | y | yes (offline, verify live) |
+| Dual Image | http://192.168.2.10/dual_image_status.html | y | yes (offline, verify live) |
| Clock | http://192.168.2.10/Clock_Detail.html | y | yes |
| Network Setup | http://192.168.2.10/tree1.html#NetworkSetup | y | |
| Get Connected | http://192.168.2.10/Network.html | | yes |
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..4396e5f
--- /dev/null
+++ b/config.py
@@ -0,0 +1,13 @@
+"""Switch connection config, read from environment variables.
+
+These three settings (host, https, password) are exactly what a future
+Terraform provider config block would need per-switch -- see AGENT.md.
+"""
+
+import os
+
+SWITCH_HOST = os.environ.get("SWITCH_HOST", "192.168.2.10")
+SWITCH_HTTPS = os.environ.get("SWITCH_HTTPS", "false").strip().lower() in ("1", "true", "yes")
+SWITCH_PASSWORD = os.environ.get("SWITCH_PASSWORD", "")
+
+SWITCH_BASE_URL = f"{'https' if SWITCH_HTTPS else 'http'}://{SWITCH_HOST}"
diff --git a/extractors/dual_image_status.py b/extractors/dual_image_status.py
new file mode 100644
index 0000000..19d5ab9
--- /dev/null
+++ b/extractors/dual_image_status.py
@@ -0,0 +1,24 @@
+import json
+
+import switch_client
+import todo
+from extractors.xe_page import fetch_tables
+
+PATH = "/dual_image_status.html"
+
+
+def get_dual_image_status(session) -> list[dict]:
+ """Returns one dict per unit (Active image, Next-Active image).
+
+ Note: the page also has a "Dual Image Descriptions" table (per-image
+ version/description) that uses a header shape (TD.tableHeaderright +
+ plain TD.defright rows, no defleft labels) our generic scalar/tabular
+ parser doesn't handle -- skipped for now, not needed for this data."""
+ tables = fetch_tables(session, PATH)
+ return tables["Dual Image Status"]["rows"]
+
+
+if __name__ == "__main__":
+ session = switch_client.get_session()
+ print(json.dumps(get_dual_image_status(session), indent=2))
+ todo.mark_implemented(switch_client.BASE_URL + PATH)
diff --git a/extractors/loop_protection_status.py b/extractors/loop_protection_status.py
new file mode 100644
index 0000000..711fafc
--- /dev/null
+++ b/extractors/loop_protection_status.py
@@ -0,0 +1,20 @@
+import json
+
+import switch_client
+import todo
+from extractors.xe_page import fetch_tables
+
+PATH = "/LoopProtectionStatus.html"
+
+
+def get_loop_protection_status(session) -> list[dict]:
+ """Returns one dict per port (Interface, Configured Action Taken,
+ Protection Feature Mode, Tx Mode, counters, Loop Detected)."""
+ tables = fetch_tables(session, PATH)
+ return tables["Loop Protection Status"]["rows"]
+
+
+if __name__ == "__main__":
+ session = switch_client.get_session()
+ print(json.dumps(get_loop_protection_status(session), indent=2))
+ todo.mark_implemented(switch_client.BASE_URL + PATH)
diff --git a/extractors/mac_table.py b/extractors/mac_table.py
new file mode 100644
index 0000000..3e5b4d3
--- /dev/null
+++ b/extractors/mac_table.py
@@ -0,0 +1,25 @@
+import json
+
+import switch_client
+import todo
+from extractors.xe_page import fetch_all_tables
+
+PATH = "/FDBSearch.html"
+
+
+def get_mac_table(session) -> dict:
+ """Returns {"summary": {...}, "entries": [...]}.
+
+ The per-entry table on this page has no
(just an empty one),
+ so it can't be looked up by caption like other pages -- fetch_all_tables()
+ + table id "1_1" is used instead."""
+ tables = fetch_all_tables(session, PATH)
+ summary = next(t["record"] for t in tables if t.get("caption") == "MAC Table")
+ entries = next(t["rows"] for t in tables if t.get("id") == "1_1")
+ return {"summary": summary, "entries": entries}
+
+
+if __name__ == "__main__":
+ session = switch_client.get_session()
+ print(json.dumps(get_mac_table(session), indent=2))
+ todo.mark_implemented(switch_client.BASE_URL + PATH)
diff --git a/extractors/trunk_status.py b/extractors/trunk_status.py
new file mode 100644
index 0000000..76d82fb
--- /dev/null
+++ b/extractors/trunk_status.py
@@ -0,0 +1,19 @@
+import json
+
+import switch_client
+import todo
+from extractors.xe_page import fetch_tables
+
+PATH = "/TrunkStatus.html"
+
+
+def get_trunk_status(session) -> list[dict]:
+ """Returns one dict per configured trunk. Empty list if no trunks exist."""
+ tables = fetch_tables(session, PATH)
+ return tables["Trunk Status"]["rows"]
+
+
+if __name__ == "__main__":
+ session = switch_client.get_session()
+ print(json.dumps(get_trunk_status(session), indent=2))
+ todo.mark_implemented(switch_client.BASE_URL + PATH)
diff --git a/extractors/xe_page.py b/extractors/xe_page.py
index 138d2e9..f497c8c 100644
--- a/extractors/xe_page.py
+++ b/extractors/xe_page.py
@@ -28,19 +28,27 @@ def extract_cell_value(td) -> str:
def parse_table(table) -> dict:
+ table_id = table.get("id")
caption_tag = table.find("caption")
caption = caption_tag.get_text(strip=True) if caption_tag else None
headers = [th.get_text(strip=True) for th in table.find_all("th", recursive=False)]
trs = table.find_all("tr", recursive=False)
if headers:
+ # Some pages repeat a label across a hidden raw-value column and a
+ # visible display column (e.g. PortSummary's/MAC Table's two
+ # "Interface"/"MAC Address" headers). Building the row dict via zip()
+ # means the later (visible) column wins on a duplicate label -- that
+ # has matched the raw column's value in every case seen so far, but
+ # if a future page's hidden/visible pair ever genuinely differs,
+ # only the visible one survives here.
rows = []
for tr in trs:
tds = tr.find_all("td", recursive=False)
if len(tds) != len(headers):
continue
rows.append({h: extract_cell_value(td) for h, td in zip(headers, tds)})
- return {"caption": caption, "kind": "tabular", "rows": rows}
+ return {"caption": caption, "kind": "tabular", "rows": rows, "id": table_id}
record = {}
field_names = {}
@@ -58,7 +66,13 @@ def parse_table(table) -> dict:
field_names[label] = inp["name"]
# field_names lets a write action look up the real for a
# label instead of hardcoding a guessed "v_R_C_1"-style field name.
- return {"caption": caption, "kind": "scalar", "record": record, "field_names": field_names}
+ return {
+ "caption": caption,
+ "kind": "scalar",
+ "record": record,
+ "field_names": field_names,
+ "id": table_id,
+ }
def parse_xe_tables(html: str) -> list[dict]:
@@ -69,8 +83,18 @@ def parse_xe_tables(html: str) -> list[dict]:
def fetch_tables(session, path: str) -> dict[str, dict]:
"""Fetch path and return {caption: table} for every captioned table.
- Tables with no/blank caption (usually just a submit button row) are
- dropped since there's nothing useful to key them by."""
+ Tables with no/blank caption (usually just a submit button row, but
+ occasionally a real data table -- e.g. FDBSearch.html's MAC entries
+ table has no caption) are dropped since there's nothing useful to key
+ them by. Use fetch_all_tables() + table["id"] for those instead."""
html = switch_client.fetch(session, path)
tables = parse_xe_tables(html)
return {t["caption"]: t for t in tables if t["caption"]}
+
+
+def fetch_all_tables(session, path: str) -> list[dict]:
+ """Fetch path and return every table, including uncaptioned ones, in
+ document order. Look up by table["id"] (the
attribute)
+ when a table has no caption to key it by."""
+ html = switch_client.fetch(session, path)
+ return parse_xe_tables(html)
diff --git a/install-opentofu.sh b/install-opentofu.sh
new file mode 100755
index 0000000..1ea2a04
--- /dev/null
+++ b/install-opentofu.sh
@@ -0,0 +1,1362 @@
+#!/bin/sh
+# Copyright (c) The OpenTofu Authors
+# SPDX-License-Identifier: Apache-2.0
+
+# OpenTofu Installer
+#
+# This script installs OpenTofu via any of the supported methods.
+# License: https://github.com/opentofu/get.opentofu.org/blob/main/LICENSE
+
+export TOFU_INSTALL_EXIT_CODE_OK=0
+export TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET=1
+export TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED=2
+export TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT=3
+
+export TOFU_INSTALL_RETURN_CODE_COMMAND_NOT_FOUND=11
+export TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED=13
+
+bold=""
+normal=""
+red=""
+green=""
+yellow=""
+blue=""
+magenta=""
+cyan=""
+gray=""
+if [ -t 1 ]; then
+ if command -v "tput" >/dev/null 2>&1; then
+ colors=$(tput colors)
+ else
+ colors=2
+ fi
+
+ if [ "${colors}" -ge 8 ]; then
+ bold="$(tput bold)"
+ normal="$(tput sgr0)"
+ red="$(tput setaf 1)"
+ green="$(tput setaf 2)"
+ yellow="$(tput setaf 3)"
+ blue="$(tput setaf 4)"
+ magenta="$(tput setaf 5)"
+ cyan="$(tput setaf 6)"
+ gray="$(tput setaf 245)"
+ fi
+fi
+
+ROOT_METHOD="auto"
+INSTALL_METHOD=""
+DEFAULT_INSTALL_PATH="/opt/opentofu"
+INSTALL_PATH="${DEFAULT_INSTALL_PATH}"
+DEFAULT_SYMLINK_PATH="/usr/local/bin"
+SYMLINK_PATH="${DEFAULT_SYMLINK_PATH}"
+DEFAULT_OPENTOFU_VERSION="latest"
+OPENTOFU_VERSION="${DEFAULT_OPENTOFU_VERSION}"
+DEFAULT_DEB_GPG_URL="https://get.opentofu.org/opentofu.gpg"
+DEB_GPG_URL="${DEFAULT_DEB_GPG_URL}"
+DEFAULT_DEB_REPO_GPG_URL="https://packages.opentofu.org/opentofu/tofu/gpgkey"
+DEB_REPO_GPG_URL="${DEFAULT_DEB_REPO_GPG_URL}"
+DEFAULT_DEB_REPO_URL="https://packages.opentofu.org/opentofu/tofu/any/"
+DEB_REPO_URL="${DEFAULT_DEB_REPO_URL}"
+DEFAULT_DEB_REPO_SUITE="any"
+DEB_REPO_SUITE="${DEFAULT_DEB_REPO_SUITE}"
+DEFAULT_DEB_REPO_COMPONENTS="main"
+DEB_REPO_COMPONENTS="${DEFAULT_DEB_REPO_COMPONENTS}"
+DEFAULT_RPM_REPO_URL="https://packages.opentofu.org/opentofu/tofu/rpm_any/rpm_any/"
+
+RPM_REPO_URL="${DEFAULT_RPM_REPO_URL}"
+DEFAULT_RPM_REPO_GPG_URL="https://packages.opentofu.org/opentofu/tofu/gpgkey"
+DEFAULT_RPM_GPG_URL="https://get.opentofu.org/opentofu.asc"
+RPM_GPG_URL="${DEFAULT_RPM_GPG_URL}"
+RPM_REPO_GPG_URL="${DEFAULT_RPM_REPO_GPG_URL}"
+DEFAULT_APK_REPO_URL="@community https://dl-cdn.alpinelinux.org/alpine/edge/community"
+APK_REPO_URL="${DEFAULT_APK_REPO_URL}"
+DEFAULT_APK_PACKAGE="opentofu@community"
+APK_PACKAGE="${DEFAULT_APK_PACKAGE}"
+DEFAULT_GPG_PATH="gpg"
+GPG_PATH="${DEFAULT_GPG_PATH}"
+DEFAULT_GPG_URL="https://get.opentofu.org/opentofu.asc"
+GPG_URL="https://get.opentofu.org/opentofu.asc"
+DEFAULT_GPG_KEY_ID="E3E6E43D84CB852EADB0051D0C0AF313E5FD9F80"
+GPG_KEY_ID="${DEFAULT_GPG_KEY_ID}"
+DEFAULT_COSIGN_PATH="cosign"
+COSIGN_PATH="${DEFAULT_COSIGN_PATH}"
+DEFAULT_COSIGN_IDENTITY="autodetect"
+COSIGN_IDENTITY="${DEFAULT_COSIGN_IDENTITY}"
+DEFAULT_COSIGN_OIDC_ISSUER="https://token.actions.githubusercontent.com"
+COSIGN_OIDC_ISSUER="${DEFAULT_COSIGN_OIDC_ISSUER}"
+SKIP_VERIFY=0
+
+# region ZSH
+if [ -n "${ZSH_VERSION}" ]; then
+ ## Enable POSIX-style word splitting:
+ setopt SH_WORD_SPLIT >/dev/null 2>&1
+fi
+# endregion
+
+log_success() {
+ if [ -z "$1" ]; then
+ return
+ fi
+ echo "${green}$1${normal}" 1>&2
+}
+
+log_warning() {
+ if [ -z "$1" ]; then
+ return
+ fi
+ echo "${yellow}$1${normal}" 1>&2
+}
+
+log_info() {
+ if [ -z "$1" ]; then
+ return
+ fi
+ echo "${cyan}$1${normal}" 1>&2
+}
+
+log_debug() {
+ if [ -z "$1" ]; then
+ return
+ fi
+ if [ -z "${LOG_DEBUG}" ]; then
+ return
+ fi
+ echo "${gray}$1${normal}" 1>&2
+}
+
+log_error() {
+ if [ -z "$1" ]; then
+ return
+ fi
+ echo "${red}$1${normal}" 1>&2
+}
+
+# This function checks if the command specified in $1 exists.
+command_exists() {
+ log_debug "Determining if the ${1} command is available..."
+ if [ -z "$1" ]; then
+ log_error "Bug: no command supplied to command_exists()"
+ return "${TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT}"
+ fi
+ if ! command -v "$1" >/dev/null 2>&1; then
+ log_debug "The ${1} command is not available."
+ return "${TOFU_INSTALL_RETURN_CODE_COMMAND_NOT_FOUND}"
+ fi
+ log_debug "The ${1} command is available."
+ return "${TOFU_INSTALL_EXIT_CODE_OK}"
+}
+
+is_root() {
+ if [ "$(id -u || true)" -eq 0 ]; then
+ return 0
+ fi
+ return 1
+}
+
+# This function runs the specified command as root.
+as_root() {
+ # shellcheck disable=SC2145
+ log_debug "Running command as root: $*"
+ case "${ROOT_METHOD}" in
+ auto)
+ log_debug "Automatically determining root method..."
+ if is_root; then
+ log_debug "We are already root, no user change needed."
+ "$@"
+ elif command_exists "sudo"; then
+ log_debug "Running command using sudo."
+ sudo "$@"
+ elif command_exists "su"; then
+ log_debug "Running command using su."
+ su root "$@"
+ else
+ log_error "Neither su nor sudo is installed, cannot obtain root privileges."
+ return "${TOFU_INSTALL_RETURN_CODE_COMMAND_NOT_FOUND}"
+ fi
+ return $?
+ ;;
+ none)
+ log_debug "Using manual root method 'none'."
+ "$@"
+ return $?
+ ;;
+ sudo)
+ log_debug "Using manual root method 'sudo'."
+ sudo "$@"
+ return $?
+ ;;
+ su)
+ log_debug "Using manual root method 'su'."
+ su root "$@"
+ return $?
+ ;;
+ *)
+ log_error "Bug: invalid root method value: $1"
+ return "${TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT}"
+ esac
+}
+
+# This function attempts to execute a function as the current user and switches to root if it fails.
+maybe_root() {
+ if ! "$@" >/dev/null 2>&1; then
+ if ! as_root "$@"; then
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+ fi
+ return "${TOFU_INSTALL_EXIT_CODE_OK}"
+}
+
+# This function verifies if one of the supported download tools is installed and returns with
+# $TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET if that is not th ecase.
+download_tool_exists() {
+ log_debug "Determining if a supported download tool is installed..."
+ if command_exists "wget"; then
+ log_debug "wget is installed."
+ return "${TOFU_INSTALL_EXIT_CODE_OK}"
+ elif command_exists "curl"; then
+ log_debug "curl is installed."
+ return "${TOFU_INSTALL_EXIT_CODE_OK}"
+ else
+ log_debug "No supported download tool is installed."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET}"
+ fi
+}
+
+# This function downloads the URL specified in $1 into the file specified in $2.
+# It returns $TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET if no supported download tool is installed, or $TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED
+# if the download failed.
+download_file() {
+ if [ -z "$1" ]; then
+ log_error "Bug: no URL supplied to download_file()"
+ return "${TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT}"
+ fi
+ if [ -z "$2" ]; then
+ log_error "Bug: no destination file supplied to download_file()"
+ return "${TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT}"
+ fi
+ log_debug "Downloading URL ${1} to ${2}..."
+ IS_GITHUB=0
+ if [ -n "${GITHUB_TOKEN}" ]; then
+ if [ "$(echo "$1" | grep -c "api.github.com" || true)" -ne 0 ]; then
+ IS_GITHUB=1
+ fi
+ fi
+ if command_exists "wget"; then
+ if [ "${IS_GITHUB}" -eq 1 ]; then
+ log_debug "Downloading using wget with GITHUB_TOKEN..."
+ if ! wget -q --header="Authorization: token ${GITHUB_TOKEN}" -O "$2" "$1"; then
+ log_debug "Download failed."
+ return "${TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED}"
+ fi
+ else
+ log_debug "Downloading using wget without GITHUB_TOKEN, this may lead to rate limit issues..."
+ if ! wget -q -O "$2" "$1"; then
+ log_debug "Download failed, please try specifying the GITHUB_TOKEN environment variable."
+ return "${TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED}"
+ fi
+ fi
+ elif command_exists "curl"; then
+ if [ "${IS_GITHUB}" -eq 1 ]; then
+ log_debug "Downloading using curl with GITHUB_TOKEN..."
+ if ! curl --proto '=https' --tlsv1.2 -fsSL -H "Authorization: token ${GITHUB_TOKEN}" -o "$2" "$1"; then
+ log_debug "Download failed."
+ return "${TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED}"
+ fi
+ else
+ log_debug "Downloading using curl without GITHUB_TOKEN, this may lead to rate limit issues..."
+ if ! curl --proto '=https' --tlsv1.2 -fsSL -o "$2" "$1"; then
+ log_debug "Download failed, please try specifying the GITHUB_TOKEN environment variable."
+ return "${TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED}"
+ fi
+ fi
+ else
+ log_error "Neither wget nor curl are available on your system. Please install one of them to proceed."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET}"
+ fi
+ log_debug "Download successful."
+ return "${TOFU_INSTALL_EXIT_CODE_OK}"
+}
+
+# This function downloads the OpenTofu GPG key from the specified URL to the specified location. Setting the third
+# parameter to 1 causes the file to be moved as root. It returns $TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED if the
+# download fails, or $TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET if no download tool is available.
+download_gpg() {
+ if [ -z "$1" ]; then
+ log_error "Bug: no URL passed to download_gpg."
+ return "${TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT}"
+ fi
+ if [ -z "$2" ]; then
+ log_error "Bug: no destination passed to download_gpg."
+ return "${TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT}"
+ fi
+ if ! command_exists "gpg"; then
+ log_error "Missing gpg binary."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET}"
+ fi
+ log_debug "Downloading GPG key from ${1} to ${2}..."
+ if ! download_tool_exists; then
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET}"
+ fi
+ log_debug "Creating temporary directory..."
+ TEMPDIR=$(mktemp -d)
+ if [ -z "${TEMPDIR}" ]; then
+ log_error "Failed to create temporary directory for GPG download."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+ TEMPFILE="${TEMPDIR}/opentofu.gpg"
+
+ if ! download_file "${1}" "${TEMPFILE}"; then
+ log_debug "Removing temporary directory..."
+ rm -rf "${TEMPFILE}"
+ return "${TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED}"
+ fi
+ if [ "$(grep 'BEGIN PGP PUBLIC KEY BLOCK' -c "${TEMPFILE}" || true)" -ne 0 ]; then
+ log_debug "Performing GPG dearmor on ${TEMPFILE}"
+ if ! gpg --no-tty --batch --dearmor -o "${TEMPFILE}.tmp" <"${TEMPFILE}"; then
+ log_error "Failed to GPG dearmor ${TEMPFILE}."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+ if ! mv "${TEMPFILE}.tmp" "${TEMPFILE}"; then
+ log_error "Failed to move ${TEMPFILE}.tmp to ${TEMPFILE}."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+ fi
+ if [ "$3" = "1" ]; then
+ log_debug "Moving GPG file as root..."
+ if ! as_root mv "${TEMPFILE}" "${2}"; then
+ log_error "Failed to move ${TEMPFILE} to ${2}."
+ rm -rf "${TEMPFILE}"
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+ else
+ log_debug "Moving GPG file as the current user..."
+ if ! mv "${TEMPFILE}" "${2}"; then
+ log_error "Failed to move ${TEMPFILE} to ${2}."
+ rm -rf "${TEMPFILE}"
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+ fi
+
+ log_debug "Removing temporary directory..."
+ rm -rf "${TEMPFILE}"
+ return "${TOFU_INSTALL_EXIT_CODE_OK}"
+}
+
+# This is a helper function that downloads a GPG URL to the specified file.
+deb_download_gpg() {
+ DEB_GPG_URL="${1}"
+ GPG_FILE="${2}"
+ if [ -z "${DEB_GPG_URL}" ]; then
+ log_error "Bug: no GPG URL specified for deb_download_gpg."
+ return "${TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT}"
+ fi
+ if [ -z "${GPG_FILE}" ]; then
+ log_error "Bug: no destination path specified for deb_download_gpg."
+ return "${TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT}"
+ fi
+ if ! download_gpg "${DEB_GPG_URL}" "${GPG_FILE}" 1; then
+ log_error "Failed to download GPG key from ${DEB_GPG_URL}."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+ log_debug "Changing ownership and permissions of ${GPG_FILE}..."
+ if ! as_root chown root:root "${GPG_FILE}"; then
+ log_error "Failed to chown ${GPG_FILE}."
+ rm -rf "${GPG_FILE}"
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+ if ! as_root chmod a+r "${GPG_FILE}"; then
+ log_error "Failed to chmod ${GPG_FILE}."
+ rm -rf "${GPG_FILE}"
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+ return "${TOFU_INSTALL_EXIT_CODE_OK}"
+}
+
+# This function installs OpenTofu via a Debian repository. It returns
+# $TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET if this is not a Debian system.
+install_deb() {
+ log_info "Attempting installation via Debian repository..."
+ if ! command_exists apt-get; then
+ log_info "The apt-get command is not available, skipping Debian repository installation."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET}"
+ fi
+
+ if ! is_root; then
+ log_info "Root privileges are required to install OpenTofu as a Debian package."
+ log_info "The installer will now verify if it can correctly assume root privileges."
+ log_info "${bold}You may be asked to enter your password.${normal}"
+ if ! as_root echo -n ""; then
+ log_error "Cannot assume root privileges."
+ log_info "Please set up either '${bold}su${normal}' or '${bold}sudo${normal}'."
+ log_info "Alternatively, run this script with ${bold}-h${normal} for other installation methods."
+ fi
+ fi
+
+ log_info "Updating package list..."
+ if ! as_root apt-get update; then
+ log_error "Failed to update apt package list."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+
+ log_debug "Determining packages to install..."
+ PACKAGE_LIST="apt-transport-https ca-certificates"
+ if [ "${SKIP_VERIFY}" -ne "1" ]; then
+ PACKAGE_LIST="${PACKAGE_LIST} gnupg"
+ fi
+ if ! download_tool_exists; then
+ log_debug "No download tool present, adding curl to the package list..."
+ PACKAGE_LIST="${PACKAGE_LIST} curl"
+ fi
+
+ log_info "Installing necessary packages for installation..."
+ log_debug "Installing ${PACKAGE_LIST}..."
+ # shellcheck disable=SC2086
+ if ! as_root apt-get install -y ${PACKAGE_LIST}; then
+ log_error "Failed to install requisite packages for Debian repository installation."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+ log_debug "Necessary packages installed."
+
+ if [ "${SKIP_VERIFY}" -ne "1" ]; then
+ log_info "Installing the OpenTofu GPG keys..."
+ log_debug "Creating /etc/apt/keyrings..."
+ if ! as_root install -m 0755 -d /etc/apt/keyrings; then
+ log_error "Failed to create /etc/apt/keyrings."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+ log_debug "Created /etc/apt/keyrings."
+
+ PACKAGE_GPG_FILE=/etc/apt/keyrings/opentofu.gpg
+ log_debug "Downloading the GPG key from ${DEB_GPG_URL}.."
+ if ! deb_download_gpg "${DEB_GPG_URL}" "${PACKAGE_GPG_FILE}"; then
+ log_error "Failed to download GPG key from ${DEB_GPG_URL}."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+ if [ -n "${DEB_REPO_GPG_URL}" ] && [ "${DEB_REPO_GPG_URL}" != "-" ]; then
+ log_debug "Downloading the repo GPG key from ${DEB_REPO_GPG_URL}.."
+ REPO_GPG_FILE=/etc/apt/keyrings/opentofu-repo.gpg
+ if ! deb_download_gpg "${DEB_REPO_GPG_URL}" "${REPO_GPG_FILE}" 1; then
+ log_error "Failed to download GPG key from ${DEB_REPO_GPG_URL}."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+ fi
+ fi
+
+ log_info "Creating OpenTofu sources list..."
+ if [ "${SKIP_VERIFY}" -ne "1" ]; then
+ if [ -n "${REPO_GPG_FILE}" ]; then
+ if ! as_root tee /etc/apt/sources.list.d/opentofu.list; then
+ log_error "Failed to create /etc/apt/sources.list.d/opentofu.list."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi < /dev/null; then
+ log_error "Failed to run tofu after installation."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+ return "${TOFU_INSTALL_EXIT_CODE_OK}"
+}
+
+# This function installs OpenTofu via the zypper command line utility. It returns
+# $TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET if zypper is not available.
+install_zypper() {
+ if ! command_exists "zypper"; then
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET}"
+ fi
+ log_info "Installing OpenTofu using zypper..."
+ if [ "${SKIP_VERIFY}" -ne "1" ]; then
+ GPGCHECK=1
+ FINAL_GPG_URL="${RPM_GPG_URL}"
+ if [ "${RPM_REPO_GPG_URL}" != "-" ]; then
+ FINAL_GPG_URL=$(cat <&1 | grep "Primary key fingerprint: " | sed -e 's/^Primary key fingerprint: //' -e 's/ //g')
+ if [ "${FINGERPRINT}" != "${GPG_KEY_ID}" ]; then
+ log_error "The release is signed with the incorrect key: ${FINGERPRINT}."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+ log_info "Signature verified."
+ elif [ "${VERIFY_METHOD}" != "-" ]; then
+ log_error "Bug: unsupported verification method: ${VERIFY_METHOD}"
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+
+ log_info "Unpacking OpenTofu..."
+ if ! unzip -d "${ZIPDIR}" "${TEMPDIR}/${ZIPFILE}"; then
+ log_error "Failed to unzip ${TEMPDIR}/${ZIPFILE} to /"
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+
+ log_info "Moving OpenTofu installation to ${INSTALL_PATH}..."
+ if ! maybe_root mkdir -p "${INSTALL_PATH}"; then
+ log_error "Cannot create installation path at ${INSTALL_PATH}."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+
+ if ! maybe_root mv "${ZIPDIR}"/* "${INSTALL_PATH}" >/dev/null 2>&1; then
+ log_error "Cannot move ${ZIPDIR} contents to ${INSTALL_PATH}. Please check the permissions on the target directory."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+
+ if [ "${SYMLINK_PATH}" != "-" ]; then
+ log_info "Creating tofu symlink at ${SYMLINK_PATH}/tofu..."
+ if ! maybe_root ln -sf "${INSTALL_PATH}/tofu" "${SYMLINK_PATH}/tofu"; then
+ log_error "Failed to create symlink at ${INSTALL_PATH}/tofu."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+ fi
+ log_info "Checking if OpenTofu is installed correctly..."
+ if [ "${SYMLINK_PATH}" != "-" ]; then
+ if ! "${SYMLINK_PATH}/tofu" --version; then
+ log_error "Failed to run ${SYMLINK_PATH}/tofu after installation."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+ else
+ if ! "${INSTALL_PATH}/tofu" --version; then
+ log_error "Failed to run ${INSTALL_PATH}/tofu after installation."
+ return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}"
+ fi
+ fi
+ log_success "Installation complete."
+ return "${TOFU_INSTALL_EXIT_CODE_OK}"
+}
+
+usage() {
+ if [ -n "$1" ]; then
+ log_error "Error: $1"
+ fi
+ cat < requests.Session:
+def login(password: str = SWITCH_PASSWORD, session: requests.Session | None = None) -> requests.Session:
"""POST to /hp_login.html, return a Session with the SID cookie set."""
session = session or requests.Session()
# A bare POST occasionally comes back without setting SID (the switch
@@ -58,7 +60,7 @@ def _cache_session(session: requests.Session) -> None:
f.write(sid)
-def get_session(password: str = "", force_login: bool = False) -> requests.Session:
+def get_session(password: str = SWITCH_PASSWORD, force_login: bool = False) -> requests.Session:
"""Reuse a still-valid cached session if one exists, otherwise log in.
The switch only allows one active web session at a time and briefly
diff --git a/terraform-provider-hpe1810/.gitignore b/terraform-provider-hpe1810/.gitignore
new file mode 100644
index 0000000..3c73a46
--- /dev/null
+++ b/terraform-provider-hpe1810/.gitignore
@@ -0,0 +1 @@
+/terraform-provider-hpe1810
diff --git a/terraform-provider-hpe1810/go.mod b/terraform-provider-hpe1810/go.mod
new file mode 100644
index 0000000..09e5c37
--- /dev/null
+++ b/terraform-provider-hpe1810/go.mod
@@ -0,0 +1,34 @@
+module terraform-provider-hpe1810
+
+go 1.25.0
+
+require (
+ github.com/PuerkitoBio/goquery v1.12.0
+ github.com/hashicorp/terraform-plugin-framework v1.19.0
+)
+
+require (
+ github.com/andybalholm/cascadia v1.3.3 // indirect
+ github.com/fatih/color v1.18.0 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/hashicorp/go-hclog v1.6.3 // indirect
+ github.com/hashicorp/go-plugin v1.7.0 // indirect
+ github.com/hashicorp/go-uuid v1.0.3 // indirect
+ github.com/hashicorp/terraform-plugin-go v0.31.0 // indirect
+ github.com/hashicorp/terraform-plugin-log v0.10.0 // indirect
+ github.com/hashicorp/terraform-registry-address v0.4.0 // indirect
+ github.com/hashicorp/terraform-svchost v0.1.1 // indirect
+ github.com/hashicorp/yamux v0.1.2 // indirect
+ github.com/mattn/go-colorable v0.1.14 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mitchellh/go-testing-interface v1.14.1 // indirect
+ github.com/oklog/run v1.1.0 // indirect
+ github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
+ github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
+ golang.org/x/net v0.52.0 // indirect
+ golang.org/x/sys v0.42.0 // indirect
+ golang.org/x/text v0.35.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
+ google.golang.org/grpc v1.79.2 // indirect
+ google.golang.org/protobuf v1.36.11 // indirect
+)
diff --git a/terraform-provider-hpe1810/go.sum b/terraform-provider-hpe1810/go.sum
new file mode 100644
index 0000000..4566c67
--- /dev/null
+++ b/terraform-provider-hpe1810/go.sum
@@ -0,0 +1,168 @@
+github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo=
+github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ=
+github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
+github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
+github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
+github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
+github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
+github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
+github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
+github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
+github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
+github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
+github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/terraform-plugin-framework v1.19.0 h1:q0bwyhxAOR3vfdgbk9iplv3MlTv/dhBHTXjQOtQDoBA=
+github.com/hashicorp/terraform-plugin-framework v1.19.0/go.mod h1:YRXOBu0jvs7xp4AThBbX4mAzYaMJ1JgtFH//oGKxwLc=
+github.com/hashicorp/terraform-plugin-go v0.31.0 h1:0Fz2r9DQ+kNNl6bx8HRxFd1TfMKUvnrOtvJPmp3Z0q8=
+github.com/hashicorp/terraform-plugin-go v0.31.0/go.mod h1:A88bDhd/cW7FnwqxQRz3slT+QY6yzbHKc6AOTtmdeS8=
+github.com/hashicorp/terraform-plugin-log v0.10.0 h1:eu2kW6/QBVdN4P3Ju2WiB2W3ObjkAsyfBsL3Wh1fj3g=
+github.com/hashicorp/terraform-plugin-log v0.10.0/go.mod h1:/9RR5Cv2aAbrqcTSdNmY1NRHP4E3ekrXRGjqORpXyB0=
+github.com/hashicorp/terraform-registry-address v0.4.0 h1:S1yCGomj30Sao4l5BMPjTGZmCNzuv7/GDTDX99E9gTk=
+github.com/hashicorp/terraform-registry-address v0.4.0/go.mod h1:LRS1Ay0+mAiRkUyltGT+UHWkIqTFvigGn/LbMshfflE=
+github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
+github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
+github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
+github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
+github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=
+github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=
+github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
+github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
+github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
+github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
+github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
+github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
+github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
+go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
+go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
+go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
+go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
+go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
+go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
+go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
+go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
+go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
+golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
+golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
+golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
+gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
+google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
+google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/terraform-provider-hpe1810/internal/provider/client/client.go b/terraform-provider-hpe1810/internal/provider/client/client.go
new file mode 100644
index 0000000..6a1c032
--- /dev/null
+++ b/terraform-provider-hpe1810/internal/provider/client/client.go
@@ -0,0 +1,160 @@
+// Package client is a Go port of the reference Python implementation's
+// switch_client.py (see ~/code/experiments/hpe/switch_client.py).
+package client
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/cookiejar"
+ "net/url"
+ "regexp"
+ "strings"
+)
+
+type Client struct {
+ BaseURL string
+ httpClient *http.Client
+ password string
+}
+
+func NewClient(host string, https bool, password string) (*Client, error) {
+ scheme := "http"
+ if https {
+ scheme = "https"
+ }
+ jar, err := cookiejar.New(nil)
+ if err != nil {
+ return nil, fmt.Errorf("creating cookie jar: %w", err)
+ }
+ return &Client{
+ BaseURL: fmt.Sprintf("%s://%s", scheme, host),
+ httpClient: &http.Client{Jar: jar},
+ password: password,
+ }, nil
+}
+
+func (c *Client) resolveURL(path string) string {
+ if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
+ return path
+ }
+ if strings.HasPrefix(path, "/") {
+ return c.BaseURL + path
+ }
+ return c.BaseURL + "/" + path
+}
+
+// Login POSTs to /hp_login.html and establishes the session cookie.
+//
+// A bare POST occasionally comes back without setting SID (the switch seems
+// to want a session cookie already present, even an empty one, before it
+// will issue a real one) -- a GET first makes login reliable.
+//
+// Unlike the Python version, there's no on-disk session cache here: this
+// provider process lives for the whole plan/apply run, so Configure() logs
+// in once and every resource/data source reuses the same *Client.
+func (c *Client) Login() error {
+ if _, err := c.httpClient.Get(c.resolveURL("/")); err != nil {
+ return fmt.Errorf("priming session: %w", err)
+ }
+
+ form := url.Values{
+ "pwd": {c.password},
+ "login": {"Login"},
+ "err_flag": {""},
+ "err_msg": {""},
+ }
+ resp, err := c.httpClient.PostForm(c.resolveURL("/hp_login.html"), form)
+ if err != nil {
+ return fmt.Errorf("login request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return fmt.Errorf("reading login response: %w", err)
+ }
+ if resp.StatusCode != http.StatusOK {
+ return fmt.Errorf("login request returned status %d", resp.StatusCode)
+ }
+ if strings.Contains(strings.ToLower(string(body)), "login") {
+ snippet := string(body)
+ if len(snippet) > 500 {
+ snippet = snippet[:500]
+ }
+ return fmt.Errorf("login failed, response still looks like the login page: %q", snippet)
+ }
+ return nil
+}
+
+// Fetch GETs BaseURL+path (path may be relative or absolute) and returns the body.
+func (c *Client) Fetch(path string) (string, error) {
+ resp, err := c.httpClient.Get(c.resolveURL(path))
+ if err != nil {
+ return "", fmt.Errorf("fetching %s: %w", path, err)
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", fmt.Errorf("reading response for %s: %w", path, err)
+ }
+ if resp.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("fetching %s returned status %d", path, resp.StatusCode)
+ }
+ return string(body), nil
+}
+
+var errFlagRejectedRe = regexp.MustCompile(`(?i)name="err_flag"[^>]*value="1"`)
+var errMsgRe = regexp.MustCompile(`(?i)name="err_msg"[^>]*value="([^"]*)"`)
+
+// SubmitForm POSTs fields to path, merged with the firmware's standard
+// bookkeeping fields (submit_flag, submit_target, err_flag, err_msg,
+// clazz_information), and returns the response body.
+//
+// Returns an error if the response's err_flag comes back "1" -- the
+// firmware's own validation-failure signal. NOTE: most value validation in
+// this firmware happens client-side in JS, not on the server -- an invalid
+// enum value, for example, is just silently ignored (state left unchanged)
+// rather than reported via err_flag. So this check catches *some* rejected
+// writes, but a caller that needs certainty should read the value back
+// afterwards and compare, rather than trusting the absence of an error.
+func (c *Client) SubmitForm(path string, fields map[string]string) (string, error) {
+ target := strings.TrimPrefix(path, "/")
+ form := url.Values{
+ // 8 == xui_operation_submit (per the firmware's own _xe_jsvars.js);
+ // 1 is xui_operation_reload and silently applies nothing.
+ "submit_flag": {"8"},
+ "submit_target": {target},
+ "err_flag": {"0"},
+ "err_msg": {""},
+ "clazz_information": {target},
+ }
+ for k, v := range fields {
+ form.Set(k, v)
+ }
+
+ resp, err := c.httpClient.PostForm(c.resolveURL(path), form)
+ if err != nil {
+ return "", fmt.Errorf("submitting form to %s: %w", path, err)
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", fmt.Errorf("reading response for %s: %w", path, err)
+ }
+ if resp.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("submitting form to %s returned status %d", path, resp.StatusCode)
+ }
+
+ bodyStr := string(body)
+ if errFlagRejectedRe.MatchString(bodyStr) {
+ msg := "unknown error"
+ if m := errMsgRe.FindStringSubmatch(bodyStr); m != nil {
+ msg = m[1]
+ }
+ return "", fmt.Errorf("switch rejected write to %q: %s", path, msg)
+ }
+ return bodyStr, nil
+}
diff --git a/terraform-provider-hpe1810/internal/provider/client/testdata/FDBSearch.html b/terraform-provider-hpe1810/internal/provider/client/testdata/FDBSearch.html
new file mode 100644
index 0000000..333fe2d
--- /dev/null
+++ b/terraform-provider-hpe1810/internal/provider/client/testdata/FDBSearch.html
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+HP ProCurve Forwarding Database Search
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/terraform-provider-hpe1810/internal/provider/client/testdata/PortSummary.html b/terraform-provider-hpe1810/internal/provider/client/testdata/PortSummary.html
new file mode 100644
index 0000000..adab03a
--- /dev/null
+++ b/terraform-provider-hpe1810/internal/provider/client/testdata/PortSummary.html
@@ -0,0 +1,434 @@
+
+
+
+
+
+
+HP ProCurve Port Summary
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/terraform-provider-hpe1810/internal/provider/client/testdata/SysDescription.html b/terraform-provider-hpe1810/internal/provider/client/testdata/SysDescription.html
new file mode 100644
index 0000000..d1aca21
--- /dev/null
+++ b/terraform-provider-hpe1810/internal/provider/client/testdata/SysDescription.html
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+HP ProCurve
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/terraform-provider-hpe1810/internal/provider/client/xepage.go b/terraform-provider-hpe1810/internal/provider/client/xepage.go
new file mode 100644
index 0000000..82a89bd
--- /dev/null
+++ b/terraform-provider-hpe1810/internal/provider/client/xepage.go
@@ -0,0 +1,137 @@
+// Port of ~/code/experiments/hpe/extractors/xe_page.py -- see that file's
+// docstring for the two table shapes this parses.
+//
+// Note on traversal: the switch firmware's HTML puts
/
as direct
+// children of
with no wrapper. A strict browser-grade HTML5
+// parser (which golang.org/x/net/html, underlying goquery, implements) would
+// normally insert a synthetic around those rows, which would break a
+// "direct children only" traversal the way the Python version does (Python's
+// html.parser is lenient and doesn't insert one). We sidestep this by using
+// Find() (any descendant) instead of direct-children selectors -- safe here
+// because tables in this firmware are never nested inside one another.
+package client
+
+import (
+ "strings"
+
+ "github.com/PuerkitoBio/goquery"
+)
+
+type Table struct {
+ Caption string
+ Kind string // "scalar" | "tabular"
+ Record map[string]string
+ FieldNames map[string]string
+ Rows []map[string]string
+ ID string
+}
+
+func extractCellValue(td *goquery.Selection) string {
+ if inp := td.Find("input").First(); inp.Length() > 0 {
+ if v, exists := inp.Attr("value"); exists {
+ return v
+ }
+ return ""
+ }
+ return strings.TrimSpace(td.Text())
+}
+
+func parseTable(table *goquery.Selection) Table {
+ id, _ := table.Attr("id")
+ caption := strings.TrimSpace(table.Find("caption").First().Text())
+
+ var headers []string
+ table.Find("th").Each(func(_ int, th *goquery.Selection) {
+ headers = append(headers, strings.TrimSpace(th.Text()))
+ })
+
+ if len(headers) > 0 {
+ var rows []map[string]string
+ table.Find("tr").Each(func(_ int, tr *goquery.Selection) {
+ tds := tr.Find("td")
+ if tds.Length() != len(headers) {
+ return
+ }
+ row := make(map[string]string, len(headers))
+ tds.Each(func(i int, td *goquery.Selection) {
+ // Later (visible) column wins on a duplicate header label --
+ // see the same caveat noted in extractors/xe_page.py.
+ row[headers[i]] = extractCellValue(td)
+ })
+ rows = append(rows, row)
+ })
+ return Table{Caption: caption, Kind: "tabular", Rows: rows, ID: id}
+ }
+
+ record := make(map[string]string)
+ fieldNames := make(map[string]string)
+ table.Find("tr").Each(func(_ int, tr *goquery.Selection) {
+ defleft := tr.Find("td.defleft").First()
+ defright := tr.Find("td.defright").First()
+ if defleft.Length() == 0 || defright.Length() == 0 {
+ return
+ }
+ label := strings.TrimSpace(defleft.Text())
+ if label == "" {
+ return
+ }
+ record[label] = extractCellValue(defright)
+ if inp := defright.Find("input").First(); inp.Length() > 0 {
+ if name, exists := inp.Attr("name"); exists && name != "" {
+ fieldNames[label] = name
+ }
+ }
+ })
+
+ return Table{
+ Caption: caption,
+ Kind: "scalar",
+ Record: record,
+ FieldNames: fieldNames,
+ ID: id,
+ }
+}
+
+// ParseXETables parses every