Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 7 additions & 12 deletions docs/appliance_push.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,18 +166,13 @@ myf5 bigip push 192.0.2.10 /var/tmp/BIGIP-17.1.1-0.0.6.iso \
--method scp
```

#### TMOS Post-Push Software Installation
Once the file transfer completes, access the appliance via TMSH or SSH to trigger the installation:
#### Verifying the Staged Software Image
Once the file transfer completes, you can verify that TMOS detected the staged ISO in `/shared/images`:
```bash
# Verify the image is present in /shared/images
# Verify the image is present and recognized in /shared/images
tmsh list sys software image BIGIP-17.1.1-0.0.6.iso

# Install to a new software volume (e.g., HD1.2)
tmsh install sys software image BIGIP-17.1.1-0.0.6.iso volume HD1.2 create-volume

# Monitor installation status
tmsh show sys software status
```
> **Scope Note:** `myf5` handles software downloads and staging/transfer to the appliance. Installing images onto boot volumes, activating boot locations, or triggering upgrade reboots is managed outside `myf5` via orchestration tooling (e.g., Ansible, Terraform) or standard TMOS administrative maintenance workflows.

---

Expand All @@ -192,10 +187,10 @@ myf5 bigip push 192.0.2.10 /var/tmp/Hotfix-BIGIP-17.1.1.1.0.0.2-ENG.iso \
--identity ~/.ssh/f5_deploy_ed25519
```

#### TMOS Hotfix Installation Command
#### Verifying the Staged Hotfix
```bash
# Install hotfix on top of target volume
tmsh install sys software hotfix Hotfix-BIGIP-17.1.1.1.0.0.2-ENG.iso volume HD1.2
# Verify the hotfix image is recognized by TMOS
tmsh list sys software hotfix Hotfix-BIGIP-17.1.1.1.0.0.2-ENG.iso
```

---
Expand Down
13 changes: 4 additions & 9 deletions pkg/auth/oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,10 @@ func TestAuthenticate(t *testing.T) {
}
}

// The audience names the resource server a token is for, and a client is only
// authorized for audiences it holds a grant on. Sending the MyF5 audience with
// iHealth credentials fails with 403 "not authorized to access resource
// server"; sending scope=ihealth fails with 403 "has not been granted scopes".
// Per F5 article K000130498 an iHealth token request carries
// audience=api://ciam-api-ihealth and NO scope.
//
// TestAuthenticate above never reads the request body, so neither mistake was
// visible to it. These assert the form fields actually transmitted.
// TestAuthenticateAudienceSelection verifies that the token request form payload
// transmits the expected audience and scope for both MyF5 and iHealth clients.
// Per F5 article K000130498, an iHealth token request carries
// audience=api://ciam-api-ihealth and omits scope.
func TestAuthenticateAudienceSelection(t *testing.T) {
cases := []struct {
name string
Expand Down
17 changes: 5 additions & 12 deletions pkg/cases/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ func TestListCases(t *testing.T) {
t.Errorf("unexpected auth header: %s", r.Header.Get("Authorization"))
}

// `type` is REQUIRED by the spec. Omitting it returns
// 400 "Missing type in the query params" from the real API, but this
// test server answered 200 regardless, so the bug shipped. Assert the
// request the API actually demands, not merely that one was made.
// `type` is required by the Case Management API. Omitting it returns
// 400 "Missing type in the query params" from the service.
if got := r.URL.Query().Get("type"); got != TypeAllOpenCases {
t.Errorf("type = %q, want %q", got, TypeAllOpenCases)
}
Expand Down Expand Up @@ -130,14 +128,9 @@ func TestListCasesDecodesLiveFieldNames(t *testing.T) {
}
}

// A 2xx carrying isError:true is a FAILURE, not an empty result.
//
// The Case API reports failure in the body as well as by status code, and the
// two do not always agree. Before the envelope was modelled on these types, a
// body-reported failure decoded as a successful response with no data — so
// `cases list` printed a cheerful empty list and `cases metadata` reported
// "0 root keys" when the call had actually failed. Silent wrong answers are
// worse than errors, so each read path is pinned here.
// TestBodyReportedErrorsBecomeErrors verifies that a 2xx response carrying
// isError:true in the JSON envelope is surfaced as an error rather than
// decoded as an empty result.
func TestBodyReportedErrorsBecomeErrors(t *testing.T) {
const payload = `{"status":"Error","message":"Something went wrong upstream","isError":true,"data":null}`

Expand Down
9 changes: 4 additions & 5 deletions pkg/downloads/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,9 @@ func (c *Client) GetFileLinks(ctx context.Context, family, line, version, contai
// If BaseFQDN returns 403 or 404, it automatically falls back to
// support.apis.f5.com with 'k'.
//
// Note that a 404 from the primary host is far more likely to be a wrong path
// than an unavailable service — that is what the missing leading /v1 produced
// for three days. The fallback will not rescue a malformed path: it sends the
// same path to a different host.
// Note that a 404 from the primary host may indicate an invalid request path
// rather than an unavailable service; the fallback sends the same path to the
// legacy host.
func (c *Client) doGetWithFallback(ctx context.Context, path string) ([]byte, error) {
scheme := "https"
fqdn := c.BaseFQDN
Expand Down Expand Up @@ -231,7 +230,7 @@ func (c *Client) doLegacyFallback(ctx context.Context, path string) ([]byte, err
}
req.Header.Set("Authorization", "Bearer "+c.Token)
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "myf5-cli-go/1.0.0 (Pre-Cutover Fallback)")
req.Header.Set("User-Agent", "myf5-cli-go/1.0.0 (Legacy Fallback)")

resp, err := c.HTTPClient.Do(req)
if err != nil {
Expand Down
7 changes: 5 additions & 2 deletions scripts/gen-sbom.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ components=$(awk '
$1=="dep" {
path=$2; ver=$3; hash=(NF>=4 ? $4 : "")
purl="pkg:golang/" path "@" ver
gsub(/\//,"%2F",purl_enc)
printf "%s\t%s\t%s\t%s\n", path, ver, hash, purl
}' <<<"$meta")

Expand All @@ -45,13 +44,14 @@ components=$(awk '
printf ' "version": 1,\n'
printf ' "metadata": {\n'
printf ' "timestamp": "%s",\n' "$now"
printf ' "tools": [{"name": "gen-sbom.sh", "vendor": "myf5-cli", "version": "1.0.0"}],\n'
printf ' "tools": [{"name": "gen-sbom.sh", "vendor": "myf5-cli", "version": "1.1.0"}],\n'
printf ' "component": {\n'
printf ' "type": "application",\n'
printf ' "bom-ref": "pkg:golang/%s@%s",\n' "$main_mod" "$main_ver"
printf ' "name": "%s",\n' "$main_mod"
printf ' "version": "%s",\n' "$main_ver"
printf ' "purl": "pkg:golang/%s@%s",\n' "$main_mod" "$main_ver"
printf ' "licenses": [{"license": {"id": "Apache-2.0"}}],\n'
printf ' "properties": [\n'
printf ' {"name": "go:version", "value": "%s"},\n' "$goversion"
printf ' {"name": "vcs:revision", "value": "%s"}\n' "$revision"
Expand All @@ -71,6 +71,9 @@ components=$(awk '
printf ' "name": "%s",\n' "$path"
printf ' "version": "%s",\n' "$ver"
printf ' "purl": "%s",\n' "$purl"
if [[ "$path" == golang.org/x/* ]]; then
printf ' "licenses": [{"license": {"id": "BSD-3-Clause"}}],\n'
fi
printf ' "scope": "required"'
if [ -n "$hash" ]; then
# Go's h1: hash is a base64 SHA-256 of the module's file tree, not a
Expand Down
Loading