CLI command reference
Authoritative reference for every isard subcommand. For end-user examples see
../README.md; for the underlying API endpoints see
api-endpoints.md.
Credentials come from GENCAT_USERNAME / GENCAT_PASSWORD (read from the
environment, or from a local .env file via dotenvy). The username is
auto-suffixed with @edu.gencat.cat if you pass only a local part. If
either variable is unset, the CLI prompts on a TTY (and errors out
otherwise). No subcommand takes credentials as flags.
~/.config/isard/session.json holds the Gencat SAML AuthSession;
~/.config/isard/isardvdi.json holds the IsardVDI IsardVdiSession JWT.
Both are chmod 600 on POSIX. Sessions self-heal: validity is checked
locally before every API call, and a 401 from any endpoint transparently
clears the cache and re-authenticates (see
architecture.md#session-handling).
login
Run the SAML + JWT flow and persist both sessions. Always re-authenticates fresh: any existing cache is cleared before authenticating. No arguments.
The @edu.gencat.cat suffix on the username is added automatically — type
just the short form (e.g. 12345678a). A hint line is printed above the
prompt as a reminder.
logout
Delete both cached session files and the per-desktop SSH credentials cache. No args.
version
Print package.version from Cargo.toml. No args.
update
isard update [--check] [--version <X.Y.Z>] [--base-url <URL>]
Replace this binary with the latest release from isard.xtec.dev.
- Fetches
https://isard.xtec.dev/api/version, compares the running binary's version against the manifest usingsemver, downloads the asset for the running platform (std::env::consts::{OS, ARCH}→ key likelinux-x86_64,darwin-aarch64). - Verifies the downloaded file's SHA-256 against the manifest entry before swapping. A mismatch aborts without touching the existing binary.
- Atomic-replaces the running binary via
fs::renameon unix (works while the process is running — the open inode survives). On Windows the running.exeis renamed toisard.exe.oldfirst; subsequent invocations sweep that file. --check: report status only — don't download or install.--version <X.Y.Z>: install a specific release instead of the latest (use this to roll back). Downgrading from a tty prompts for confirmation.--base-url <URL>/ISARD_BASE_URL: override the update server.
isard self-update is a hidden alias kept for the v0.1.4 → v0.1.5
migration. Prefer isard update in new scripts.
Auto update-notification
Since v0.1.8 every isard <subcommand> (except update, version,
login, and the hidden background subcommand) silently spawns a
detached child that polls /api/version and writes the result to
~/.config/isard/update-check.json. On the next invocation,
before the foreground subcommand runs, the CLI reads that file —
if a newer release is pending it interactively asks Update now? [Y/n]. Yes runs isard update inline; No records a 24-hour
per-version snooze (a brand-new release that lands during the
snooze window re-prompts immediately, since the snooze is keyed
to the offered version). The background check itself rate-limits
to once per 6 hours. In non-TTY contexts (pipes, scripts) the
prompt degrades to a one-line note: isard X.Y.Z is available — run \isard update`and never blocks. SetISARD_SKIP_UPDATE_CHECK=1` to opt out of the whole flow.
list
isard list [-H|--hardware] [-n|--name <NAME>]
- Calls
GET /api/v3/user/desktops, sorts by name. --namefilters server-side via an exact match.--hardwareadds CPU + Memory columns. Hardware comes fromGET /api/v3/template/{template_id}for each unique template id (IsardVdiClient::enrich_hardware). Desktops whosetemplateisnullshow-.
template
The template subcommand is a group. With no action it defaults to list.
template list
isard template [-c|--category <ID>] [-H|--hardware] [--filter <NAME>]
isard template list ...
Calls IsardVdiClient::get_templates, which tries a fallback chain (see
api-endpoints.md). --filter is a case-insensitive
substring match on name. Disabled templates are hidden from the output.
template create
isard template create [--seed-url <URL>]
Only an optional --seed-url. The desktop name is derived
deterministically from the picked Fedora variant and major version, so a
re-run with the same Fedora release produces the same name and IsardVDI
correctly returns 409 ("desktop already exists"). Naming scheme:
fedora-server-<version>(e.g.fedora-server-44)fedora-workstation-<version>(e.g.fedora-workstation-44)
The command:
- Reuses or refreshes the IsardVDI session.
- Shows an interactive numbered menu:
Template source: 1. Fedora Server (Net Install) 2. Fedora Workstation (Live) Select (1-2): - Resolves the latest x86_64 ISO via Fedora's releases.json
(
fedora::latest_fedora_iso). Filter:arch="x86_64",variant="Server"or"Workstation",linkcontainsnetinst(Server) orLive(Workstation), highest numericversion. The version is what the derived name uses. - Computes the desktop name (
fedora-<variant>-<version>) and prints it. - Looks up media by filename stem (e.g.
Fedora-Server-netinst-x86_64-44-1.7). Reuses ifDownloaded; if missing, registers viaPOST /api/v3/mediaand polls/api/v3/mediauntilstatus == "Downloaded"(timeout: 1800 s, poll: 5 s, transparent session refresh on 401). - Queries
/api/v3/media/installsand picks anxml_id(priority:fedora>linux>centos/rhel/alma/rocky, withvirtiopreferred within each family). Prints the full available list for visibility. - Calls
POST /api/v3/desktop/from/mediawith hardware constants (TEMPLATE_VCPUS=4,TEMPLATE_MEMORY_MB=8192→ sent as8GB,TEMPLATE_DISK_GB=100,TEMPLATE_DISK_BUS=virtio),boot_order=["iso"],interfaces=["default", "wireguard"], plusforced_hyp=false,favourite_hyp=falseat top level. - Starts the desktop so you can open the installer via
isard view fedora-<variant>-<version>. - Prompts
Press Enter when installation is complete. Enter triggers stop →PUT /api/v3/domain/<desktop>with{hardware:{boot_order:["disk"], isos:[]}}(detach) →DELETE /api/v3/media/<id>(and the seed media if any). Ctrl-C leaves everything in place — re-running picks up the same media row.
Unattended install via --seed-url
isard template create --seed-url https://files.example.com/seed.iso
When --seed-url is set, between steps 6 and 7 above the CLI:
- Registers the URL as a second media row named
<desktop-name>-seed(reusing it if alreadyDownloaded). - Threads the seed's media id into
NewFromMediaRequest.seed_media_id. POST /api/v3/desktop/from/mediathen attaches both ISOs:hardware.isos = [{id: installer}, {id: seed}]. The installer comes first becauseboot_order=["iso"]boots the first ISO entry.- During post-install cleanup (step 9), after detaching ISOs and deleting the installer media, the seed media is also deleted.
The seed ISO should have one of two volume labels (build it locally with
isard seed cidata|oemdrv — see seed):
CIDATAfor cloud-init NoCloud (Ubuntu autoinstall, etc.).OEMDRVfor Anaconda kickstart (Fedora / RHEL family).
Both labels are auto-discovered by the install target — no kernel command
line tweaks needed, and IsardVDI's hardware schema has no extra-kernel-args
field anyway.
Ready-to-edit starter configs (Ubuntu autoinstall + Fedora kickstart) live
under examples/seed/. End-to-end runbook with
expected cloud-init / Anaconda checks is at
docs/phase3-verification.md.
All defaults are constants at the top of src/cli.rs — to expose them as
flags again, add fields back to TemplateCreateArgs and pass them through to
NewFromMediaRequest.
template promote
isard template promote <DESKTOP> [-n <NAME>] [-d <DESCRIPTION>] [--disabled]
Promote a stopped desktop into a reusable IsardVDI template via
POST /api/v3/template. Resolves the desktop by name (fuzzy match
through resolve_desktop), so you can pass any unique prefix.
<DESKTOP>— required. Name or ID of the source desktop.-n, --name <NAME>— name for the new template (≤ 50 chars). Defaults to the desktop's name.-d, --description <DESCRIPTION>— ≤ 255 chars.--disabled— create the template disabled (default: enabled and immediately usable forisard create).
Pre-conditions enforced server-side: the source desktop must be in
Stopped state AND its storage must be ready (i.e. the disk must
have actually been written — a desktop created via
template create --seed-url ... and then never booted is not a
valid promote source; you'll get an HTTP 500 from upstream's
storage_ready check). The CLI prints a warning if desktop.state
isn't Stopped but leaves the call to the server, since IsardVDI may
add or relax checks over time.
media
Subcommand group. With no action it defaults to list.
media list
isard media [--filter <SUBSTRING>]
isard media list [--filter <SUBSTRING>]
Calls IsardVdiClient::get_media, which merges /api/v3/media (owned) and
/api/v3/media_allowed (shared) and dedupes by id. Prints a four-column
table (NAME / STATUS / SIZE / OWNER) where OWNER is you or shared.
media add
isard media add <NAME> --url <HTTPS_URL>
[-d|--description <TEXT>]
[--no-wait]
[--timeout <SECONDS>] # default 1800
- Validates name length (4-50) and that the URL starts with
https://. POST /api/v3/mediawith{name, url, "url-web": url, kind:"iso", description}. The endpoint returns{}, so the CLI looks the row up by name afterwards (find_media_by_name).- Unless
--no-wait, polls/api/v3/mediauntil the row reachesstatus="Downloaded"orstatus="Failed", surfacingprogress/size/statusin the spinner.
media delete
isard media delete <NAME-OR-ID> [-y|--yes]
- Resolves the target: exact id → exact name (case-insensitive) → substring match.
- Shows a confirmation block (name/id/state, plus a warning if the media is
marked
shared). Skip with--yes. DELETE /api/v3/media/<id>. 403 → "your role can't delete this media", 409 → "media is referenced by an existing desktop. Detach it first."
seed
Build a local cloud-init / kickstart seed ISO. Pure local operation — no
auth, no IsardVDI API call. The resulting ISO has to be hosted at an
HTTPS URL (e.g. on your own webserver) and then registered via
media add --url, because IsardVDI ingests media by URL only (there is no
multipart upload route).
The seed ISO carries unattended-install configuration that the booting guest auto-discovers from the volume label:
| Subcommand | Label | File on ISO | Picked up by |
|---|---|---|---|
cidata | CIDATA | user-data + meta-data | cloud-init NoCloud datasource (Ubuntu autoinstall, Fedora Cloud Base, etc.) |
oemdrv | OEMDRV | ks.cfg | Anaconda (Fedora / RHEL / AlmaLinux / Rocky installer) |
ISO generation is pure Rust via the hadris-iso
crate — no xorriso / genisoimage / mkisofs binary needed on the host.
seed cidata
isard seed cidata --user-data <FILE> [--meta-data <FILE>] -o <OUTPUT.iso>
--user-data— cloud-config YAML. Written asuser-dataon the ISO.--meta-data— optional; if omitted, an emptymeta-datafile is added (cloud-init's NoCloud requires the file to exist but accepts it empty).-o, --output— where to write the seed ISO.
Typical Ubuntu autoinstall snippet:
#cloud-config
autoinstall:
version: 1
identity:
hostname: ubuntu-template
username: alice
password: "$6$..." # mkpasswd -m sha-512
ssh:
install-server: true
authorized-keys:
- ssh-ed25519 AAAA... me@laptop
packages: [vim, htop]
seed oemdrv
isard seed oemdrv --kickstart <FILE> -o <OUTPUT.iso>
--kickstart— kickstart file. Written asks.cfgon the ISO.-o, --output— where to write the seed ISO.
Anaconda automatically picks up ks.cfg from any volume labelled OEMDRV
without needing inst.ks= on the kernel command line — handy because
IsardVDI's hardware schema has no extra-kernel-args field.
seed publish
isard seed publish --kind <oemdrv|cidata> \
--hostname <NAME> --username <USER> \
[--password <PW> | --password-stdin] \
[--ssh-key <KEY> | --ssh-key-file <FILE>] \
[--locale <L> --keyboard <K> --timezone <TZ>] \
[--server <URL>] [--token <TOKEN>] [--json]
POSTs form fields to the /api/seed
endpoint hosted on isard.xtec.dev (or a self-hosted copy via
--server) and prints the resulting https://…/seeds/seed-<hex>.iso
URL on stdout. Same operation as the web form — server renders the
kickstart / cloud-init template, SHA-512-crypts the password, writes
the ISO under /home/deploy/isard/seeds/, and returns the URL.
--kind—oemdrv(Fedora/RHEL Anaconda kickstart) orcidata(Ubuntu autoinstall / cloud-init NoCloud).--hostname— RFC-1123 hostname for the new VM.--username— Linux username to create on the new VM.--password/--password-stdin— plaintext password (≥ 8 chars). Server hashes it. Prefer--password-stdin(e.g.printf '%s' "$PW" | isard seed publish --password-stdin …) so the password never appears inps. Without either flag and a TTY, the CLI prompts viarpassword.--ssh-key/--ssh-key-file— OpenSSH public key. Defaults to~/.ssh/id_ed25519.pubthen~/.ssh/id_rsa.pub.--locale,--keyboard,--timezone— OEMDRV only. Server defaults:en_US.UTF-8,us,Europe/Madrid.--server— origin override (envISARD_API). Defaulthttps://isard.xtec.dev.--token— bearer token (envISARD_SEED_TOKEN).--json— print the full response (url,filename,size_bytes,cmd) instead of just the URL.
Idempotent: re-submitting the same fields returns the same URL and does not duplicate the file on disk.
Compose with template create:
URL=$(printf '%s' "$PASSWORD" | isard seed publish \
--kind oemdrv \
--hostname fedora-template \
--username fedora \
--password-stdin)
isard template create --seed-url "$URL"
End-to-end workflow (manual today)
isard seed cidata --user-data my.yaml -o seed.iso(orseed oemdrv).- Host
seed.isoat an HTTPS URL your IsardVDI deployment can reach. Each user-data file may carry SSH keys or hashed passwords, so prefer unguessable paths and short-lived hosting over a public web root. isard media add my-seed --url https://your-host/seed.iso.- Attach both installer ISO and seed ISO when creating the template.
Multi-ISO attach via
hardware.isosis structurally supported by the API (schemas/snippets/hardware.ymldefinesisosas an unbounded list) but is not yet wired intoisard template create— that's Phase 2 (see TODO.md).
create
isard create <NAME>
[-t|--template <NAME-OR-ID>]
[-d|--description <TEXT>]
[-c|--category <UUID>]
[--filter <SUBSTRING>]
Creates a desktop from an existing template via
POST /api/v3/persistent_desktop. Distinct from template create, which
provisions a new template by downloading and installing an OS.
Selection priority for --template (same as resolve_desktop): case-insensitive
id → case-insensitive exact name → Jaro-Winkler fuzzy match (≥ 0.5) → substring
match. A typo like fedoa-server still resolves to fedora-server-42. If
--template is omitted, an interactive numbered list is shown (optionally
filtered by --filter or --category).
Hardware defaults (set inside IsardVdiClient::create_desktop): 2 vCPU,
4 GB RAM, boot_order=["disk"], disk_bus="default",
interfaces=["default","wireguard"], videos=["default"].
start <NAME>
isard start <NAME>
[-w|--wait]
[--timeout <SECS>] # default 120
[-b|--book]
[--book-minutes <MINUTES>] # default 60
- Calls
GET /api/v3/desktop/start/{id}. - 428 means a booking is required. With
--book, the CLI callsPOST /api/v3/booking/eventfor--book-minutesstarting now, then retriesstart. Without--bookit prints a friendly error pointing to the web UI. --waitpollsget_desktopsuntilstate == "Started"or--timeoutexpires.
stop <NAME>
isard stop <NAME> [-w|--wait] [--timeout <SECS>]
Calls GET /api/v3/desktop/stop/{id}; --wait polls for state=="Stopped".
delete <NAME>
isard delete <NAME> [-y|--yes] [--permanent]
- Default:
DELETE /api/v3/desktop/{id}(moves to trash, recoverable). --permanent:DELETE /api/v3/desktop/{id}/permanent(irreversible).- Confirmation prompt unless
--yes. Permanent deletion uses a stricter prompt (default=false).
view <NAME>
isard view <NAME>
[--install / --no-install] # default true
[--debug]
- Resolves desktop name (exact → fuzzy → substring).
- If the desktop isn't
Started, starts it automatically (waits for the transition) before retrieving the SPICE file. No flag needed — this is the default since v0.1.7. GET /api/v3/desktop/{id}/viewer/file-spice→ SPICE file. EmbeddedcontentJSON field is decoded and the certificate is reformatted for remote-viewer.- Installs
virt-viewerif missing (unless--no-install): MSI on Windows, Homebrew on macOS, dnf on Fedora, apt on Debian/Ubuntu. - Launches
remote-viewerwith the SPICE file.
ssh <NAME>
isard ssh <NAME>
[-l|--user <USER>]
[-i|--identity <PATH>]
[--password <PASS>]
[--timeout <SECS>] # default 120
[--dry-run]
- Resolves SSH private key:
--identity→~/.ssh/id_ed25519→~/.ssh/id_rsa→ generate a new ed25519 key. - Reads/writes the bastion config via
GET/PUT /api/v3/desktop/bastion/{id}. Adds the public key toauthorized_keys(idempotent). - Bastion runs on the IsardVDI hostname, port 443; the SSH "username" is
BastionTarget.id(a UUID). The Linux account on the VM is whichever user is recorded inguest_properties.credentials.username. - If the desktop isn't
Started, starts it automatically before connecting — no--startflag needed since v0.1.7. - Guest-credentials confirmation + cache. On the first
ssh <name>, the CLI fetchesguest_properties.credentialsand printsuser: …andpassword: aa****zz(half-masked) for the user to confirm. On Yes the desktop's id is added to~/.config/isard/desktop-creds.json(0o600) — a tiny{"confirmed":[…]}set — so future runs skip the prompt entirely (no API fetch, no display). The file stores no secrets: only the set of desktop ids whose creds the student has already approved. On No the user is prompted for new credentials which are pushed via the--passwordflow below.isard logoutwipes the file. If the systemsshexits 255 (auth/connection failure), the entry for that desktop is dropped so the next run reprompts. --password <PASS>updates the desktop's guest credentials to (--useror"user",--password) before connecting. Flow: if the desktop is running, stop it (stop_desktop+ wait forStopped); thenGET /api/v3/domain/info/{id}, mutateguest_properties.credentials,PUT /api/v3/domain/{id}with the fullguest_propertiesblock (only the top-level key is merged server-side); then start the desktop and wait forStarted. The new credentials replace the cache.--dry-runstill performs the credential update — it only suppresses the final SSH exec.--dry-runprints the SSH argv instead of executing.- Embedded SSH fallback. If the system
sshbinary is not onPATH, the CLI uses an embedded pure-Rust SSH client (russh) so Windows machines without OpenSSH can still connect. Key auth only, trust-on-first-use host keys, no window-resize forwarding.
Desktop name resolution
resolve_desktop in cli.rs, used by start, stop, delete, view,
ssh:
- Exact match (case-insensitive).
- Best fuzzy match (
strsim::jaro_winkler, cutoff 0.5). - First substring match (case-insensitive).