tritonadm tweaks based on user testing - #28
Open
nshalman wants to merge 20 commits into
Open
Conversation
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>
danmcd
reviewed
May 7, 2026
danmcd
left a comment
Collaborator
There was a problem hiding this comment.
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>
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 EOLhttps://gist.github.com/nshalman/ccbb0969e73c41ab96df5dd3cd3741ee |
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary from Claude:
tritonadm packaging
SmartOS ramdisk).
New tritonadm subcommands
~/.triton/profiles.d/<name>.jsonfor the headnode admin account. Reads SDC config, derives the SSH-key fingerprint viassh-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 image fetch-nocloud
kept regardless.
Gateway error-shape fix (and triton-cli rendering)
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.
<status> <reason>: <code>: <message>instead of leaking Rust's Debug format(Error { code: Some(...), error_code: None, ... })through progenitor's stock Display.Housekeeping