Skip to content

tritonadm tweaks based on user testing - #28

Open
nshalman wants to merge 20 commits into
mainfrom
tritonadm-completion
Open

tritonadm tweaks based on user testing#28
nshalman wants to merge 20 commits into
mainfrom
tritonadm-completion

Conversation

@nshalman

@nshalman nshalman commented May 7, 2026

Copy link
Copy Markdown
Collaborator

Summary from Claude:

tritonadm packaging

  • Ship bash completion via a transient triton/tritonadm-setup SMF service so the /etc/bash/bash_completion.d/{tritonadm,triton} symlinks are restored on every boot (/etc lives on the
    SmartOS ramdisk).
  • Drop zsh/fish from the shipped tarball; both still available on demand via tritonadm completion zsh|fish.
  • Bash completion files land at etc/.completion, mirroring sdcadm's convention.

New tritonadm subcommands

  • tritonadm dev admin-profile — generates ~/.triton/profiles.d/<name>.json for the headnode admin account. Reads SDC config, derives the SSH-key fingerprint via ssh-keygen -E md5 -lf /root/.ssh/sdc.id_rsa.pub, and probes CloudAPI with the same TLS trust chain triton uses (via triton_tls::build_http_client) to decide insecure: true|false. Conservative on probe failure.
  • tritonadm channel list/get/set/unset — manage the SAPI update_channel metadata, mirroring sdcadm channel. Validates against the live channel list before writing.

tritonadm image fetch-nocloud

  • After a successful --target smartos / --target imgapi upload, delete the produced *.zfs.gz + *.json by default. Pass --keep-files to retain. The on-disk download cache under workdir is
    kept regardless.

Gateway error-shape fix (and triton-cli rendering)

  • The merged triton-gateway-api Error schema was using tritonapi's strict Dropshot shape ({error_code, message, request_id} all required), but the gateway proxies cloudapi error bodies
    verbatim. triton volume list against a headnode without VOLAPI was panicking on missing field request_id. Replace the merged Error with a permissive hybrid that accepts both code and
    error_code and marks every field optional.
  • triton-cli now renders API errors as <status> <reason>: <code>: <message> instead of leaking Rust's Debug format (Error { code: Some(...), error_code: None, ... }) through progenitor's stock Display.

Housekeeping

  • Suppress two intentional AL003 no-error-swallowing warnings in nocloud.rs with documented reasons (Url::to_file_path returns Result<_, ()>; post-upload cleanup failure shouldn't fail a successful upload).
  • Hide the tritonadm URL override flags/env vars from the help output. Normal use on a headnode doesn't use them.

nshalman and others added 12 commits May 7, 2026 11:24
Mirror sdcadm's pattern: ship etc/{tritonadm,triton}.completion in the
tarball and add a transient SMF service (triton/tritonadm-setup) that
re-creates /etc/bash/bash_completion.d/{tritonadm,triton} symlinks on
every boot. /etc lives on the SmartOS ramdisk so the symlinks must be
restored at boot time, not just at install time. Drop zsh/fish from the
shipped tarball; both remain available on demand via
`tritonadm completion zsh|fish` for dev hosts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generate a `triton` CLI profile for the headnode admin account from the
SDC config: derives the MD5 fingerprint by shelling out to `ssh-keygen
-E md5 -lf /root/.ssh/sdc.id_rsa.pub`, probes the CloudAPI URL with
default TLS verification, and flips `insecure: true` (with a warning)
if the cert chain doesn't validate — expected on COAL's self-signed
cert. Writes $HOME/.triton/profiles.d/<name>.json honoring
TRITON_CONFIG_DIR / XDG_CONFIG_HOME so `triton` finds the file.

Extends TritonConfig with the parsed JSON blob and a get_str() accessor
so the new command can read ufds_admin_login / cloudapi_domain without
promoting every SDC-config key to a typed field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operator-config generation is a developer-convenience helper rather
than a post-setup step that touches the datacenter, so it fits more
naturally under the dev namespace alongside the other helpers that
aren't part of sdcadm. Threads the SDC config through DevCommand::run
so admin-profile can read it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After a successful --target smartos or --target imgapi upload, remove
the produced *.zfs.gz + *.json by default. The on-disk download cache
under workdir is kept regardless so re-runs against the same vendor /
release don't have to re-fetch from upstream. Pass --keep-files to
retain the artifacts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cleanup_artifacts: post-upload cleanup failure is recoverable (leftover
file, not data loss) and surfacing it as a hard error after a
successful upload would mislead operators.

print_plan file:// branch: Url::to_file_path returns Result<_, ()> — the
Err variant carries no information to propagate, and the surrounding
code is rendering a human-readable build plan, not validating input.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
list  — prints channels from the updates server, marking the configured
       channel (or the remote-default fallback when SAPI's update_channel
       is unset). --json mirrors sdcadm's row shape.
get   — prints the configured channel, falling back to the updates
       server's default with a "(remote default)" suffix so the source
       of the answer is unambiguous.
set   — validates against the live channel list before writing, then
       merges metadata.update_channel via SAPI's update action.
unset — uses SAPI's delete action on metadata.update_channel; no-ops
       cleanly when the key was already absent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…obing

The workspace builds reqwest with rustls-no-provider, so
reqwest::Client::builder().build() panics with "No provider set" until
something installs a default CryptoProvider. The other call sites in
tritonadm route through triton_tls::build_http_client, which handles
the install internally; this probe needs a raw client (to apply a
short timeout that build_http_client doesn't expose), so call
install_default_crypto_provider directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous probe used reqwest::Client::builder() directly. With the
workspace's rustls-no-provider feature, reqwest falls back to
rustls-platform-verifier on illumos, which can validate certs that the
rest of our codebase (via triton_tls::build_http_client and its
three-tier root store) would reject — leading the probe to set
insecure=false on a profile that triton itself would refuse to use.

Use triton_tls::build_http_client(false) so the probe answers the
question we actually care about: "will triton trust this cert?" Wrap
the send in tokio::time::timeout to keep the timeout, since the helper
doesn't expose timeout configuration.

Distinguish three outcomes — CertValid / SelfSigned / Unreachable — and
on Unreachable default to insecure=true with a warning rather than
silently producing a profile that strict TLS will fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The merged triton-gateway spec had been giving the strict tritonapi
Error schema (`{error_code, message, request_id}` with the latter two
required) precedence on collision. The gateway proxies cloudapi error
bodies verbatim — see `gateway_error_response` in
services/triton-gateway/src/main.rs — so any cloudapi-origin error
({code, message[, request_id]}) blew up gateway clients with "missing
field `request_id`". Most visible from `triton vols` against a
headnode where VOLAPI isn't installed and cloudapi answers with a 405
{"code":"MethodNotAllowedError","message":"GET is not allowed"}.

Replace the merged Error schema wholesale with a permissive shape that
lists every field optional and accepts both spellings (`code` and
`error_code`). Regenerate the gateway-client; all four fields are now
Option<String>.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Progenitor's stock Display formats ErrorResponse with `value: {:?}` of
the typed body, leaking Rust internals into operator output —
e.g. `triton volume list` against a headnode without VOLAPI used to
print

  status: 405 Method Not Allowed; headers: {...}; value: Error {
  code: Some("MethodNotAllowedError"), error_code: None, ... }

Walk the anyhow chain in main.rs, downcast to
triton_gateway_client::Error<types::Error>, and reconstruct a
node-triton-shaped message. Falls back to anyhow's alternate-display
for non-API errors. Both cloudapi-shape (`code`) and Dropshot-shape
(`error_code`) bodies are handled because the gateway proxies cloudapi
verbatim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Compute NAME and DEFAULT widths from the data instead of hardcoding
16/14, and render an empty default as '-' rather than leaving the
column blank — both differences from sdcadm's tabula-based output that
showed up side-by-side on a `tritonadm channel list` / `sdcadm channel
list` comparison.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The eight global --*-url flags exist so a developer can run tritonadm
against a non-headnode environment. On a headnode they all
auto-detect from /lib/sdc/config.sh, so they're noise in --help —
visible side-by-side with sdcadm's terse 'sdcadm channel' help, our
flag list dwarfed the actual command list.

`hide = true` only hides from rendered help text; the flags still
parse, still respect their env vars, still work for the dev-host case
they were designed for. Documenting them lives in CLAUDE.md / source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nshalman
nshalman requested a review from a team May 7, 2026 18:48

@danmcd danmcd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass 0 didn't strike me as anything off. Not approving yet only because I didn't feel I gave it a proper pass.

…ndors

Adds a `--list-releases` flag that enumerates the vendor's published
releases without resolving an image. Each non-rolling vendor implements
the new VendorProfile::list_releases trait method against the same
upstream surface its existing release-discovery uses (Simple Streams,
releases.json, GitHub API, MirrorCache, dirlist HTML, apt Release).

Rolling vendors (smartos, arch) return a clear error pointing operators
at `--release latest` instead. Output is a name/label/note table by
default, with `--json` for machine consumption.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nshalman

nshalman commented May 8, 2026

Copy link
Copy Markdown
Collaborator Author

6fc9724 enables this output:

$ target/debug/tritonadm image fetch-nocloud --list-releases --vendor  2>&1 | grep possible | sed 's|.*:||;s|, | |g;s|\]$||' | xargs -n 1 -t target/debug/tritonadm image fetch-nocloud --list-releases --vendor 2>&1 | gh gist create --public - -d "Tritonadm release listing"
target/debug/tritonadm image fetch-nocloud --list-releases --vendor alma
Fetching https://repo.almalinux.org/almalinux/
RELEASE  LABEL         NOTE
10       AlmaLinux 10  
9        AlmaLinux 9   
8        AlmaLinux 8   
target/debug/tritonadm image fetch-nocloud --list-releases --vendor alpine
Fetching Alpine releases.json ...
RELEASE  LABEL    NOTE
3.23     3.23.4   latest_stable
3.22     3.22.4   
3.21     3.21.7   
3.20     3.20.10  
3.19     3.19.9   
3.18     3.18.12  
3.17     3.17.10  
3.16     3.16.9   
3.15     3.15.11  
3.14     3.14.10  
3.13     3.13.12  
3.12     3.12.12  
3.11     3.11.13  
3.10     3.10.9   
3.9      3.9.6    
3.8      3.8.5    
3.7      3.7.3    
3.6      3.6.5    
3.5      3.5.3    
3.4      3.4.6    
3.3      3.3.3    
3.2      3.2.3    
3.1      3.1.4    
3.0      3.0.6    
2.7      2.7.9    
2.6      2.6.6    
2.5      2.5.4    
2.4      2.4.11   
2.3      2.3.6    
2.2      2.2.3    
2.1      2.1.6    
edge     -        
target/debug/tritonadm image fetch-nocloud --list-releases --vendor arch
Error: arch is a rolling release; pass `--release latest` (or a specific build like `20260501.523211`) instead of `--list-releases`
target/debug/tritonadm image fetch-nocloud --list-releases --vendor centos-stream
Fetching https://cloud.centos.org/centos/
RELEASE  LABEL             NOTE
10       CentOS Stream 10  
9        CentOS Stream 9   
8        CentOS Stream 8   EOL
target/debug/tritonadm image fetch-nocloud --list-releases --vendor debian
Fetching Debian Release file (stable) ...
Fetching Debian Release file (oldstable) ...
Fetching Debian Release file (oldoldstable) ...
Fetching Debian Release file (testing) ...
RELEASE       LABEL             NOTE
stable        13.4 (trixie)     alias for trixie
trixie        13.4              stable
oldstable     12.13 (bookworm)  alias for bookworm
bookworm      12.13             oldstable
oldoldstable  11.11 (bullseye)  alias for bullseye
bullseye      11.11             oldoldstable
testing       -                 unavailable (parse https://deb.debian.org/debian/dists/testing/Release)
target/debug/tritonadm image fetch-nocloud --list-releases --vendor fedora
Fetching Fedora releases.json ...
RELEASE  LABEL      NOTE
44       Fedora 44  
43       Fedora 43  
42       Fedora 42  
target/debug/tritonadm image fetch-nocloud --list-releases --vendor freebsd
Fetching FreeBSD VM-IMAGES directory listing ...
RELEASE  LABEL                 NOTE
15.0     FreeBSD 15.0-RELEASE  
14.4     FreeBSD 14.4-RELEASE  
14.3     FreeBSD 14.3-RELEASE  
13.5     FreeBSD 13.5-RELEASE  
target/debug/tritonadm image fetch-nocloud --list-releases --vendor omnios
RELEASE  LABEL                          NOTE
stable   current quarterly stable       latest alias
lts      current LTS                    
bloody   bleeding-edge weekly snapshot  
target/debug/tritonadm image fetch-nocloud --list-releases --vendor openbsd
Fetching https://api.github.com/repos/hcartiaux/openbsd-cloud-image/releases?per_page=100
RELEASE  LABEL        NOTE
7.8      OpenBSD 7.8  v7.8_2025-10-22-09-25
7.7      OpenBSD 7.7  v7.7_2025-04-28-11-38
7.6      OpenBSD 7.6  v7.6_2024-10-08-22-40
7.4      OpenBSD 7.4  v7.4_2024-05-15-16-35
7.5      OpenBSD 7.5  v7.5_2024-05-13-15-25
7.5      OpenBSD 7.5  v7.5_2024-05-01-00-13
target/debug/tritonadm image fetch-nocloud --list-releases --vendor opensuse
Fetching openSUSE Leap version index ...
RELEASE  LABEL               NOTE
42.3     openSUSE Leap 42.3  
16.1     openSUSE Leap 16.1  
16.0     openSUSE Leap 16.0  
15.6     openSUSE Leap 15.6  
15.5     openSUSE Leap 15.5  
15.4     openSUSE Leap 15.4  
15.3     openSUSE Leap 15.3  
15.2     openSUSE Leap 15.2  
15.1     openSUSE Leap 15.1  
15.0     openSUSE Leap 15.0  
target/debug/tritonadm image fetch-nocloud --list-releases --vendor oracle
Fetching Oracle Linux templates page ...
RELEASE  LABEL              NOTE
10       Oracle Linux 10.1  
9        Oracle Linux 9.7   
8        Oracle Linux 8.10  
7        Oracle Linux 7.9   
target/debug/tritonadm image fetch-nocloud --list-releases --vendor rocky
Fetching https://download.rockylinux.org/pub/rocky/
RELEASE  LABEL           NOTE
10       Rocky Linux 10  
9        Rocky Linux 9   
8        Rocky Linux 8   
target/debug/tritonadm image fetch-nocloud --list-releases --vendor smartos
Error: smartos is a rolling release; pass `--release latest` (or a specific timestamp like `20260430T145637Z`) instead of `--list-releases`
target/debug/tritonadm image fetch-nocloud --list-releases --vendor talos
Fetching Talos releases from GitHub ...
RELEASE  LABEL          NOTE
1.13.0   Talos v1.13.0  
1.12.7   Talos v1.12.7  
1.12.6   Talos v1.12.6  
1.12.5   Talos v1.12.5  
1.12.4   Talos v1.12.4  
1.12.3   Talos v1.12.3  
1.12.2   Talos v1.12.2  
1.12.1   Talos v1.12.1  
1.10.9   Talos v1.10.9  
1.12.0   Talos v1.12.0  
1.11.6   Talos v1.11.6  
1.10.8   Talos v1.10.8  
1.11.5   Talos v1.11.5  
1.11.4   Talos v1.11.4  
1.11.3   Talos v1.11.3  
1.11.2   Talos v1.11.2  
1.11.1   Talos v1.11.1  
target/debug/tritonadm image fetch-nocloud --list-releases --vendor ubuntu
Fetching Ubuntu Simple Streams index ...
RELEASE   LABEL      NOTE
resolute  26.04 LTS  EOL
questing  25.10      supported
plucky    25.04      EOL
oracular  24.10      EOL
noble     24.04 LTS  supported LTS
mantic    23.10      EOL
lunar     23.04      EOL
kinetic   22.10      EOL
jammy     22.04 LTS  supported LTS
impish    21.10      EOL
hirsute   21.04      EOL
groovy    20.10      EOL
focal     20.04 LTS  EOL
eoan      19.10      EOL
disco     19.04      EOL
cosmic    18.10      EOL
bionic    18.04 LTS  EOL
artful    17.10      EOL
zesty     17.04      EOL
yakkety   16.10      EOL
xenial    16.04 LTS  EOL
wily      15.10      EOL
vivid     15.04      EOL
utopic    14.10      EOL
trusty    14.04 LTS  EOL
saucy     13.10      EOL
raring    13.04      EOL
quantal   12.10      EOL
precise   12.04 LTS  EOL
oneiric   11.10      EOL
natty     11.04      EOL
maverick  10.10      EOL
lucid     10.04 LTS  EOL

https://gist.github.com/nshalman/ccbb0969e73c41ab96df5dd3cd3741ee

@nshalman
nshalman requested review from cneira and travispaul May 8, 2026 14:18
nshalman and others added 7 commits May 12, 2026 07:57
The command moved from `post-setup` to `dev` but two strings still
said `post-setup admin-profile`, including the user-facing error
returned when /lib/sdc/config.sh can't be loaded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the early return when no channel is set, the subsequent
`configured_channel(&app).unwrap_or("")` could never trigger the
fallback. Restructure as `let Some(current) = ... else { ... };`
so the invariant is enforced at the binding site and we don't
re-fetch the value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rning

`cleanup_artifacts` printed "Removed local artifacts" unconditionally,
even when one of the two `remove_file` calls had already warned that
the file couldn't be removed. Track whether every removal succeeded
and only print the confirmation when it did.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both functions GET the VM-IMAGES directory listing and parse the same
`<X>.<Y>-RELEASE/` entries; `find_latest` picks the first, `list` maps
all of them into `Release` rows. Extract a single `fetch_versions`
helper that returns sorted-dedup `(major, minor)` pairs newest-first
and rebuild both entry points on top of it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every public constructor in triton_tls used to take an unlabelled
`insecure: bool`, so call sites read `build_http_client(false)` /
`(true)` with no hint at the meaning. Replace it with a dedicated
enum:

    pub enum TlsTrust { Verified, Insecure }

Now call sites read `build_http_client(TlsTrust::Verified)` and
`build_http_client(TlsTrust::Insecure)`. The `Insecure` variant
matches the existing `insecure: bool` field naming throughout the
codebase, so `grep` for risky call sites still works.

An `impl From<bool> for TlsTrust` handles the boundaries where the
trust setting still arrives as a bool (sapi-client config, triton-cli
profile flag, triton-gateway `tls_verify` config) — those callers
write `insecure.into()`.

Affects build_http_client, build_http_client_with_headers, and
build_rustls_client_config (no external callers of the last one).
Bumps triton-tls from 0.1.0 to 0.2.0 since this is a breaking change
to its public API; the compiler will guide any downstream consumer
through the conversion on a merge attempt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…h two-probe approach

`is_tls_error` walked a reqwest error's source chain looking for
substrings like "certificate", "self signed", "tls handshake" to
distinguish a self-signed-cert host from an unreachable one. That
heuristic relied on rustls and reqwest emitting predictable error
text, which drifts between versions.

Replace it with two probes against the URL. The verified probe runs
first; on success the cert is trusted and we're done. On failure we
retry with `TlsTrust::Insecure`: success there means the host is up
but the cert isn't trusted (self-signed), failure means we can't
reach the host at all.

No more string matching, no source-chain walk, no `is_tls_error`.
The happy path still costs one HTTP request; the second probe only
runs when the first fails, which is the rare COAL/unreachable case
where an operator is already waiting on diagnostic output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RUSTSEC-2026-0049/-0098/-0099/-0104 (rustls-webpki, errors) and
RUSTSEC-2026-0097 (rand, warning) are pre-existing transitive
advisories appearing on the unchanged tree; documenting them so
`make audit` matches the established convention of listing both
error- and warning-level findings. Separate from the ergonomic
work on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants