Skip to content
Open
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
30 changes: 30 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ rlimit = "0.10"
rumors = { git = "https://github.com/oxidecomputer/rumors", rev = "bf2ba264e2360eab1ba5f06148ea3ef3050825c8" }
rustix = { version = "1", features = ["fs", "process", "pty", "termios"] }
rustls = "0.23"
rusty-fork = "0.3"
rustyline = { git = "https://github.com/kkawakam/rustyline", rev = "5ab22f2f8d65e85f0b925360437ece8eeeb6db92", features = ["signal-hook", "termios"] }
schemars = { version = "0.8", features = ["chrono"] }
serde = "1"
Expand All @@ -76,6 +77,7 @@ tokio-stream = "0.1"
tokio-fd = "0.3"
tokio-tungstenite = "0.28"
tokio-util = "0.7"
toml = "1"
webbrowser = "1"
x509-cert = { version = "0.2", features = ["pem", "std"] }
xdg = "3"
Expand Down
82 changes: 66 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,28 +70,44 @@ Aug 09 03:41:44.594 INFO listening, local_addr: 0.0.0.0:44444
> On NixOS you will have to pass the `--insecure-disable-path-isolation` flag to
> the server, otherwise spawning commands will fail.

In another terminal, point the client at the server and start it up
in REPL mode. Signing job requests needs a client built with the
`permslip` feature and a reachable Permission Slip instance holding
your signing key:
In another terminal, point the client at the server and start it up:

```
$ export PERMSLIP_URL="https://your-permslip-server.example"
$ export SUSH_PERMSLIP_KEY="Your Signing Key"
$ export SUSH_URL="http://localhost:44444"
$ cargo run --bin=sush --features permslip -- repl
Running `target/debug/sush repl`
✅ Output format set to `text`
✅ Server URL set to `http://localhost:44444`
✅ SSH agent socket set to `/run/user/1000/ssh-agent.sock`
✅ SSH key ID unset
$ cargo run --bin=sush --features permslip
Running `target/debug/sush`
✅ url = http://localhost:44444 (environment)
✅ ssh-auth-sock = /run/user/1000/ssh-agent.sock (environment)
sush#
```

We can explicitly start a session with `session start`, or we can
let `job start` implicitly start one for us. The server automatically
sets some environment variables for job processes, so as a simple
test try running the command `echo $SUSH_JOB_ID`:
With no command, `sush` starts its interactive REPL (aka shell).
Try `help` for a list of commands, or any `sush` sub-command,
such as `session`, `job`, etc. The REPL offers some convenient
features for running multiple jobs, such as transparent use of
the previous job ID, Nexus tunnel reuse, command history, etc.
Exit the REPL with `quit` or Ctrl-D.

You can begin a session with `session start`. You may be prompted
to touch your authentication token (e.g., YubiKey) and to authenticate
to the signing service:

```
sush# session start
👋 Please confirm user presence to sign with key `excite-cabbage-reject-wave-learn-police-that-glad`
🔑 Open this URL in your browser to authenticate to the signing service:
https://your-permslip-server.example/google/07c339c687e9ac919af7ab2ac5025881
✅ Created session
Session ID: slush-olympic-universe-ill-total-risk-host-suggest
Signer nonce: private-else-nut-erosion-trend-tube-token-man
Sequence number: borrow-session-iron-author
```

Now you're ready to run a job. The server automatically sets some
environment variables for job processes, so as a simple test try
running the command `echo $SUSH_JOB_ID`:

```
sush# job start --wait "echo $SUSH_JOB_ID"
Expand Down Expand Up @@ -134,5 +150,39 @@ of cubby numbers, serial numbers, and full baseboard IDs are also
accepted. Interactive and streaming jobs must target exactly one sled,
which defaults to the sled they are talking to.

The `version` command shows which versions of `sush` are running where
in the rack. See `help` for a list of other commands.
The `version` command shows which versions of `sush` are running on
which sleds, as well as a visual indication of their health. Green means
the `sush` server is running and connected to the internal gossip network;
yellow means the server couldn't connect to the gossip network; and red
means that no `sush` server is running there at all.

## Configuration files and the options system

The `sush` client reads a configuration file in TOML format from the
standard XDG directories, e.g., `~/.config/sush/sush.toml`, or wherever
you point `--config` to. In such files, the keys are long option names,
and the values provide defaults for those options.

Top-level options provide global defaults, while tables in the config file
let you organize sets of options into named *profiles*, e.g., for different
environments. You can choose a profile via a top-level `profile` key, or
the `SUSH_PROFILE` environment variable, or the `--profile` global option:

```toml
permslip-url = "https://your-permslip-server.example"
permslip-key = "Your Signing Key"
profile = "local"

[local]
url = "http://localhost:44444"
permslip-key = "Local Signing Key"

[remote]
url = "https://somewhere-else.example"
```

Configuration files provide defaults for options in such a way that
environment variables override them, and explicit command line options
override those. You can see which options support environment variables
with `--help`; any of those may also be set in the config file, except
for ones whose values are secrets, such as `SUSH_NEXUS_TOKEN`.
5 changes: 4 additions & 1 deletion client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ base64.workspace = true
bytes.workspace = true
bytesize.workspace = true
chrono.workspace = true
clap.workspace = true
clap = { workspace = true, features = ["string"] }
ed25519-dalek.workspace = true
futures.workspace = true
http.workspace = true
Expand Down Expand Up @@ -55,16 +55,19 @@ shlex.workspace = true
signature.workspace = true
sled-hardware-types.workspace = true
ssh-key.workspace = true
strum.workspace = true
sush-api = { path = "../api" }
sush-common = { path = "../common" }
thiserror.workspace = true
tokio.workspace = true
tokio-fd.workspace = true
tokio-rustls.workspace = true
tokio-tungstenite.workspace = true
toml.workspace = true
webbrowser = { workspace = true, optional = true }
x509-cert.workspace = true
xdg.workspace = true

[dev-dependencies]
rusty-fork.workspace = true
tempfile.workspace = true
8 changes: 3 additions & 5 deletions client/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ use serde_json::{json, to_string as to_json_string, to_string_pretty as to_json_
use sled_hardware_types::BaseboardId;
use x509_cert::Certificate;
use x509_cert::der::Encode as _;
use xdg::BaseDirectories;

use sush_common::authn::Identity;
use sush_common::jobs::{
Expand All @@ -42,9 +41,8 @@ use sush_common::version::VersionInfo;

use crate::commands::{CommandError, GlobalArgs};
use crate::context::{CommandContext, OutputFormat, StartedDisplay, StatusDisplayStyle};
use crate::{AuthzSigner, quiet_write};
use crate::{AuthzSigner, base_directories, quiet_write};

pub(crate) const PREFIX: &str = "sush";
const SESSION_FILE_NAME: &str = "session.json";
const SESSION_FILE_VERSION: u32 = 1;
const CLIENT_NONCE_FILE_NAME: &str = "client-nonce.json";
Expand Down Expand Up @@ -84,7 +82,7 @@ impl Cli {
/// Without persistence, every one-shot command would need a fresh
/// `session attach`.
pub fn load_session(&mut self) {
let path = match BaseDirectories::with_prefix(PREFIX).place_state_file(SESSION_FILE_NAME) {
let path = match base_directories().place_state_file(SESSION_FILE_NAME) {
Ok(path) => path,
Err(error) => {
eprintln!("❗ The session will not persist: {error}");
Expand All @@ -106,7 +104,7 @@ impl Cli {
Err(error) => eprintln!("❗ Ignoring the saved session: {error}"),
}
self.session_file = Some(path);
match BaseDirectories::with_prefix(PREFIX).place_state_file(CLIENT_NONCE_FILE_NAME) {
match base_directories().place_state_file(CLIENT_NONCE_FILE_NAME) {
Ok(path) => self.client_nonce_file = Some(path),
Err(error) => eprintln!("❗ The client nonce will not persist: {error}"),
}
Expand Down
Loading