3923 lines
124 KiB
Rust
3923 lines
124 KiB
Rust
use anyhow::{Context, Result, bail};
|
|
use base64::Engine;
|
|
use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
|
|
use clap_complete::{Shell, generate};
|
|
use geth_config::GethPaths;
|
|
use geth_control::{ControlRequest, ControlResponse};
|
|
use geth_node::service::{ServiceInstallOptions, ServiceManager, ServiceReport};
|
|
use std::io::{Read, stdout};
|
|
use std::path::PathBuf;
|
|
|
|
const TOP_LEVEL_AFTER_HELP: &str = r#"Common starts:
|
|
geth guide init
|
|
geth guide owner-setup
|
|
geth guide overlay
|
|
geth guide completions
|
|
geth init
|
|
geth init --admin-key ~/.ssh/id_ed25519_sk.pub --signing-key ~/.ssh/id_ed25519_sk --node-name laptop
|
|
geth daemon run
|
|
geth status
|
|
|
|
Use `geth <command> --help` for command-specific examples."#;
|
|
|
|
const INIT_LONG_ABOUT: &str = r#"Initialize local geth state.
|
|
|
|
With no owner options, `geth init` creates local directories, config, metadata
|
|
store, and the daemon agent identity. This is enough for local CAS/KV/document
|
|
testing and for later enrollment into an owner's mesh.
|
|
|
|
Owner setup is SSH-admin-rooted. `--admin-key` is the OpenSSH public key that is
|
|
recorded as an admin trust anchor. `--signing-key` is the matching private SSH
|
|
key path used immediately through `ssh-keygen -Y sign` to sign the initial
|
|
keychain/auth statements. For security-key/YubiKey keys, use the private key
|
|
stub path such as `~/.ssh/id_ed25519_sk`; ssh-keygen/ssh-agent will trigger the
|
|
hardware-backed signing flow.
|
|
|
|
If any owner setup option is used (`--admin-key`, `--signing-key`, `--owner`,
|
|
`--node-name`, or `--capability`), both `--admin-key` and `--signing-key` are
|
|
required so geth never creates unsigned owner/device/node statements by
|
|
accident."#;
|
|
|
|
const INIT_AFTER_HELP: &str = r#"Examples:
|
|
# Local-only node for development or later enrollment:
|
|
geth init
|
|
|
|
# Owner/admin node using an SSH or YubiKey-backed admin key:
|
|
geth init \
|
|
--admin-key ~/.ssh/id_ed25519_sk.pub \
|
|
--signing-key ~/.ssh/id_ed25519_sk \
|
|
--owner eric \
|
|
--node-name laptop
|
|
|
|
# Owner node with initial resource grants:
|
|
geth init \
|
|
--admin-key ~/.ssh/id_ed25519.pub \
|
|
--signing-key ~/.ssh/id_ed25519 \
|
|
--node-name laptop \
|
|
--capability resource:cas:local=cas.fetch \
|
|
--capability resource:ssh-proxy:local=ssh_proxy.connect
|
|
|
|
Key paths:
|
|
--admin-key OpenSSH public key, usually *.pub. Stored as the trust anchor.
|
|
--signing-key Matching private key or security-key stub. Used to sign init ops.
|
|
|
|
Related:
|
|
geth guide owner-setup
|
|
geth keychain status
|
|
geth node list"#;
|
|
|
|
const GUIDE_INDEX: &str = r#"Usage: geth guide <topic>
|
|
|
|
Topics:
|
|
init Local-only init versus owner/admin init.
|
|
owner-setup First owner node with SSH/YubiKey admin trust.
|
|
enrollment Add another node/device to the owner mesh.
|
|
keys Meaning of --admin-key and --signing-key.
|
|
overlay Optional Iroh overlay network planning.
|
|
service Install and manage geth as a user service.
|
|
completions Shell completion installation examples.
|
|
smoke-test Minimal commands to verify a node and daemon."#;
|
|
|
|
const GUIDE_INIT: &str = r#"geth init has two modes.
|
|
|
|
Local-only:
|
|
geth init
|
|
|
|
Creates GETH_HOME, config.toml, geth.sqlite, CAS directories, and a local agent
|
|
identity. Use this for local testing or for a node that will later request
|
|
enrollment into an owner's mesh.
|
|
|
|
Owner/admin:
|
|
geth init --admin-key ~/.ssh/id_ed25519_sk.pub --signing-key ~/.ssh/id_ed25519_sk --node-name laptop
|
|
|
|
This records signed owner, device, node, and agent bindings. Use it on the
|
|
machine where you control the admin SSH/YubiKey key. Once initialized, inspect:
|
|
|
|
geth keychain status
|
|
geth node list
|
|
"#;
|
|
|
|
const GUIDE_OWNER_SETUP: &str = r#"Owner setup flow:
|
|
|
|
1. Pick or create an SSH admin key. Security-key/YubiKey-backed OpenSSH keys are
|
|
supported through ssh-keygen:
|
|
|
|
ssh-keygen -t ed25519-sk -f ~/.ssh/id_ed25519_sk
|
|
|
|
2. Initialize the owner node:
|
|
|
|
geth init \
|
|
--admin-key ~/.ssh/id_ed25519_sk.pub \
|
|
--signing-key ~/.ssh/id_ed25519_sk \
|
|
--owner eric \
|
|
--node-name owner-laptop
|
|
|
|
3. Start the daemon and export a peer card:
|
|
|
|
geth daemon run
|
|
geth peer export --out /tmp/owner.peer.json
|
|
|
|
`--admin-key` is public and replicated as the admin trust anchor.
|
|
`--signing-key` is private and only used locally to sign canonical init ops.
|
|
"#;
|
|
|
|
const GUIDE_ENROLLMENT: &str = r#"Add another node/device:
|
|
|
|
On the new node:
|
|
geth init
|
|
geth keychain init --admin-key ~/.ssh/id_ed25519_sk.pub
|
|
geth peer import /tmp/owner.peer.json
|
|
geth node enroll request --node-name workstation --out /tmp/workstation-enrollment.json
|
|
geth node enroll submit owner --path /tmp/workstation-enrollment.json
|
|
|
|
On the owner/YubiKey machine:
|
|
geth node enroll list
|
|
geth node enroll approve <request-id> --signing-key ~/.ssh/id_ed25519_sk
|
|
|
|
Back on the new node:
|
|
geth sync now owner
|
|
geth node list
|
|
|
|
Enrollment approval records signed keychain/auth operations. Discovery and peer
|
|
cards alone never grant trust or capabilities.
|
|
"#;
|
|
|
|
const GUIDE_KEYS: &str = r#"Key terminology:
|
|
|
|
--admin-key
|
|
OpenSSH public key path, usually ending in .pub. This key is recorded in the
|
|
geth keychain as an admin trust anchor. It is safe to distribute.
|
|
|
|
--signing-key
|
|
Matching private key path, or the OpenSSH security-key/YubiKey stub path. geth
|
|
shells out to ssh-keygen -Y sign with explicit namespaces to sign canonical
|
|
geth keychain/auth operations. The private key is not copied into geth state.
|
|
|
|
Signing sources:
|
|
Local key file:
|
|
--signing-key ~/.ssh/id_ed25519 --admin-key ~/.ssh/id_ed25519.pub
|
|
|
|
Encrypted key file:
|
|
Load it into ssh-agent with `ssh-add ~/.ssh/id_ed25519`, then sign through
|
|
the agent by passing the public key path:
|
|
--signing-key ~/.ssh/id_ed25519.pub
|
|
|
|
FIDO/YubiKey OpenSSH key:
|
|
Use the security-key stub or load it into ssh-agent:
|
|
--signing-key ~/.ssh/id_ed25519_sk --admin-key ~/.ssh/id_ed25519_sk.pub
|
|
|
|
PKCS#11:
|
|
Direct ssh-keygen -Y signing does not expose a portable -D provider option.
|
|
Load the token key into ssh-agent with `ssh-add -s <provider>`, then pass
|
|
the public key path with --signing-key.
|
|
|
|
Examples:
|
|
Software key:
|
|
--admin-key ~/.ssh/id_ed25519.pub --signing-key ~/.ssh/id_ed25519
|
|
|
|
YubiKey/FIDO OpenSSH key:
|
|
--admin-key ~/.ssh/id_ed25519_sk.pub --signing-key ~/.ssh/id_ed25519_sk
|
|
|
|
Generate the active OpenSSH allowed_signers projection:
|
|
geth keychain allowed-signers --out ~/.config/geth/allowed_signers
|
|
|
|
Sign an arbitrary authorized_keys snapshot with an active admin key:
|
|
geth keychain sign-file --in ~/.ssh/authorized_keys --out ~/.ssh/authorized_keys.sig --signing-key ~/.ssh/id_ed25519_sk
|
|
|
|
Verify the snapshot signature against the current keychain trust root:
|
|
geth keychain verify-file --in ~/.ssh/authorized_keys --signature ~/.ssh/authorized_keys.sig
|
|
|
|
If you use --owner, --node-name, or --capability during init, geth requires both
|
|
key options because those fields create signed owner/device/node statements.
|
|
"#;
|
|
|
|
const GUIDE_SERVICE: &str = r#"Install geth as a user service:
|
|
|
|
geth daemon service install --start
|
|
geth daemon service status
|
|
geth daemon service stop
|
|
geth daemon service start
|
|
geth daemon service uninstall
|
|
|
|
Service installation targets user service managers, not system services:
|
|
Linux: systemd --user
|
|
macOS: launchd user agent
|
|
Windows: current-user scheduled task
|
|
|
|
Preview definitions without installing:
|
|
geth daemon service print
|
|
"#;
|
|
|
|
const GUIDE_COMPLETIONS: &str = r#"Shell completions:
|
|
|
|
geth can print completions for bash, zsh, fish, PowerShell, and elvish. The
|
|
generated scripts are produced from the same Clap command tree as `geth --help`,
|
|
so subcommands and flags stay in sync with the executable.
|
|
|
|
Bash:
|
|
mkdir -p ~/.local/share/bash-completion/completions
|
|
geth completions bash > ~/.local/share/bash-completion/completions/geth
|
|
|
|
Zsh:
|
|
mkdir -p ~/.zfunc
|
|
geth completions zsh > ~/.zfunc/_geth
|
|
# Ensure ~/.zfunc is in fpath, then run: compinit
|
|
|
|
Fish:
|
|
mkdir -p ~/.config/fish/completions
|
|
geth completions fish > ~/.config/fish/completions/geth.fish
|
|
|
|
PowerShell:
|
|
geth completions powershell > geth.ps1
|
|
# Source geth.ps1 from your PowerShell profile.
|
|
|
|
Elvish:
|
|
mkdir -p ~/.elvish/lib
|
|
geth completions elvish > ~/.elvish/lib/geth.elv
|
|
"#;
|
|
|
|
const GUIDE_OVERLAY: &str = r#"Optional overlay network:
|
|
|
|
geth has an experimental overlay-network design inspired by iroh-lan. The
|
|
intended future runtime is a private L3-style packet overlay carried over geth's
|
|
daemon-owned Iroh endpoint.
|
|
|
|
Current prototype commands:
|
|
geth overlay status
|
|
geth overlay plan home
|
|
geth overlay plan home --cidr 172.22.0.0/24
|
|
geth overlay join home --secret <resource-secret>
|
|
geth overlay interface-plan home --platform linux
|
|
geth overlay up home
|
|
geth overlay down home
|
|
geth overlay send home <node> --packet-base64 <ipv4-packet>
|
|
geth overlay recv home
|
|
geth overlay leave home
|
|
|
|
Current limits:
|
|
- overlay up creates a real TUN/Wintun-style L3 device and usually needs
|
|
privileges or host network entitlements
|
|
- host network changes are explicit opt-in only
|
|
- discovery can suggest peers, but never grants overlay access
|
|
- overlay access must be resource-authorized with overlay.join/overlay.route
|
|
- all overlay packets must be carried over Iroh, not SSH or another transport
|
|
|
|
Bearer invite flow:
|
|
geth resource create overlay home
|
|
geth secret bearer create resource:overlay:home --capability overlay.join
|
|
geth overlay join home --secret <bearer-token>
|
|
"#;
|
|
|
|
const GUIDE_SMOKE_TEST: &str = r#"Minimal smoke test:
|
|
|
|
Terminal 1:
|
|
export GETH_HOME="$(mktemp -d)"
|
|
geth init
|
|
geth daemon run
|
|
|
|
Terminal 2:
|
|
export GETH_HOME="<same dir>"
|
|
geth status
|
|
geth node id
|
|
echo "hello geth" > /tmp/hello-geth.txt
|
|
geth cas add /tmp/hello-geth.txt
|
|
geth cas list
|
|
geth keychain status
|
|
|
|
For two-node owner/enrollment testing, use:
|
|
geth guide owner-setup
|
|
geth guide enrollment
|
|
"#;
|
|
|
|
#[derive(Debug, Parser)]
|
|
#[command(
|
|
name = "geth",
|
|
about = "Personal local-first Iroh mesh runtime",
|
|
after_long_help = TOP_LEVEL_AFTER_HELP
|
|
)]
|
|
pub struct Cli {
|
|
#[arg(long, global = true, help = "Print machine-readable JSON output")]
|
|
pub json: bool,
|
|
#[arg(
|
|
long,
|
|
global = true,
|
|
help = "Print newline-delimited JSON output for streaming commands"
|
|
)]
|
|
pub jsonl: bool,
|
|
#[command(subcommand)]
|
|
pub command: Command,
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum Command {
|
|
Guide {
|
|
#[arg(value_enum)]
|
|
topic: Option<GuideTopic>,
|
|
},
|
|
Completions {
|
|
#[arg(value_enum)]
|
|
shell: Shell,
|
|
},
|
|
#[command(long_about = INIT_LONG_ABOUT, after_long_help = INIT_AFTER_HELP)]
|
|
Init {
|
|
#[arg(
|
|
long,
|
|
value_name = "OPENSSH_PUBLIC_KEY",
|
|
help = "OpenSSH public key recorded as the owner/admin trust anchor",
|
|
long_help = "Path to the OpenSSH public key recorded as the owner/admin trust anchor, usually ~/.ssh/<key>.pub. This key is public and replicated in the geth keychain."
|
|
)]
|
|
admin_key: Option<PathBuf>,
|
|
#[arg(
|
|
long,
|
|
value_name = "OPENSSH_PRIVATE_KEY",
|
|
help = "Matching private key or YubiKey/FIDO stub used to sign init statements",
|
|
long_help = "Path to the matching private OpenSSH key, or security-key/YubiKey stub such as ~/.ssh/id_ed25519_sk. geth uses ssh-keygen -Y sign with explicit geth namespaces; it does not copy the private key into geth state."
|
|
)]
|
|
signing_key: Option<PathBuf>,
|
|
#[arg(
|
|
long,
|
|
default_value = "owner",
|
|
help = "Owner/user display name recorded during owner init"
|
|
)]
|
|
owner: String,
|
|
#[arg(
|
|
long,
|
|
default_value = "local",
|
|
help = "Friendly node name recorded during owner init"
|
|
)]
|
|
node_name: String,
|
|
#[arg(
|
|
long = "capability",
|
|
value_name = "RESOURCE=CAPABILITY",
|
|
help = "Initial capability grant for this node; repeatable"
|
|
)]
|
|
capabilities: Vec<String>,
|
|
},
|
|
Daemon {
|
|
#[command(subcommand)]
|
|
command: DaemonCommand,
|
|
},
|
|
Status,
|
|
Sync {
|
|
#[command(subcommand)]
|
|
command: SyncCommand,
|
|
},
|
|
Node {
|
|
#[command(subcommand)]
|
|
command: NodeCommand,
|
|
},
|
|
Peer {
|
|
#[command(subcommand)]
|
|
command: PeerCommand,
|
|
},
|
|
Overlay {
|
|
#[command(subcommand)]
|
|
command: OverlayCommand,
|
|
},
|
|
Resource {
|
|
#[command(subcommand)]
|
|
command: ResourceCommand,
|
|
},
|
|
Keychain {
|
|
#[command(subcommand)]
|
|
command: KeychainCommand,
|
|
},
|
|
Auth {
|
|
#[command(subcommand)]
|
|
command: AuthCommand,
|
|
},
|
|
Secret {
|
|
#[command(subcommand)]
|
|
command: SecretCommand,
|
|
},
|
|
Cas {
|
|
#[command(subcommand)]
|
|
command: CasCommand,
|
|
},
|
|
Kv {
|
|
#[command(subcommand)]
|
|
command: KvCommand,
|
|
},
|
|
Pubsub {
|
|
#[command(subcommand)]
|
|
command: PubsubCommand,
|
|
},
|
|
Pipe {
|
|
#[command(subcommand)]
|
|
command: PipeCommand,
|
|
},
|
|
Db {
|
|
#[command(subcommand)]
|
|
command: DbCommand,
|
|
},
|
|
Document {
|
|
#[command(subcommand)]
|
|
command: DocumentCommand,
|
|
},
|
|
Ssh {
|
|
#[command(subcommand)]
|
|
command: SshCommand,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Debug, ValueEnum)]
|
|
pub enum GuideTopic {
|
|
Init,
|
|
OwnerSetup,
|
|
Enrollment,
|
|
Keys,
|
|
Overlay,
|
|
Service,
|
|
Completions,
|
|
SmokeTest,
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum DaemonCommand {
|
|
Run,
|
|
Service {
|
|
#[command(subcommand)]
|
|
command: ServiceCommand,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum ServiceCommand {
|
|
Install {
|
|
#[arg(long, default_value = "auto")]
|
|
manager: String,
|
|
#[arg(long)]
|
|
bin: Option<PathBuf>,
|
|
#[arg(long)]
|
|
start: bool,
|
|
},
|
|
Uninstall {
|
|
#[arg(long, default_value = "auto")]
|
|
manager: String,
|
|
},
|
|
Start {
|
|
#[arg(long, default_value = "auto")]
|
|
manager: String,
|
|
},
|
|
Stop {
|
|
#[arg(long, default_value = "auto")]
|
|
manager: String,
|
|
},
|
|
Status {
|
|
#[arg(long, default_value = "auto")]
|
|
manager: String,
|
|
},
|
|
Print {
|
|
#[arg(long, default_value = "auto")]
|
|
manager: String,
|
|
#[arg(long)]
|
|
bin: Option<PathBuf>,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum SyncCommand {
|
|
Status,
|
|
Now { node: Option<String> },
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum NodeCommand {
|
|
Id,
|
|
Status,
|
|
List,
|
|
Enroll {
|
|
#[command(subcommand)]
|
|
command: NodeEnrollCommand,
|
|
},
|
|
Rename {
|
|
node: String,
|
|
name: String,
|
|
#[arg(long)]
|
|
signing_key: Option<PathBuf>,
|
|
},
|
|
Revoke {
|
|
node: String,
|
|
#[arg(long)]
|
|
signing_key: Option<PathBuf>,
|
|
},
|
|
Grant {
|
|
node: String,
|
|
resource: String,
|
|
capability: String,
|
|
#[arg(long)]
|
|
grant_id: Option<String>,
|
|
#[arg(long)]
|
|
signing_key: PathBuf,
|
|
#[arg(long)]
|
|
admin_key: Option<PathBuf>,
|
|
},
|
|
RevokeGrant {
|
|
resource: String,
|
|
grant_id: String,
|
|
#[arg(long)]
|
|
signing_key: PathBuf,
|
|
#[arg(long)]
|
|
admin_key: Option<PathBuf>,
|
|
},
|
|
EndpointAdd {
|
|
node: String,
|
|
endpoint: String,
|
|
#[arg(long)]
|
|
signing_key: PathBuf,
|
|
},
|
|
EndpointRevoke {
|
|
node: String,
|
|
endpoint: String,
|
|
#[arg(long)]
|
|
signing_key: PathBuf,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum NodeEnrollCommand {
|
|
Request {
|
|
#[arg(long)]
|
|
node_name: String,
|
|
#[arg(long = "capability")]
|
|
capabilities: Vec<String>,
|
|
#[arg(long)]
|
|
reason: Option<String>,
|
|
#[arg(long)]
|
|
out: Option<PathBuf>,
|
|
},
|
|
Submit {
|
|
owner_node: String,
|
|
#[arg(long)]
|
|
request_id: Option<String>,
|
|
#[arg(long)]
|
|
path: Option<PathBuf>,
|
|
},
|
|
Import {
|
|
path: PathBuf,
|
|
},
|
|
List {
|
|
#[arg(long)]
|
|
status: Option<String>,
|
|
},
|
|
Approve {
|
|
request_id: String,
|
|
#[arg(long)]
|
|
signing_key: PathBuf,
|
|
#[arg(long)]
|
|
admin_key: Option<PathBuf>,
|
|
#[arg(long)]
|
|
node_name: Option<String>,
|
|
#[arg(long = "capability")]
|
|
capabilities: Vec<String>,
|
|
},
|
|
Sync {
|
|
owner_node: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum PeerCommand {
|
|
Export {
|
|
#[arg(long)]
|
|
out: Option<PathBuf>,
|
|
},
|
|
Import {
|
|
path: PathBuf,
|
|
},
|
|
List,
|
|
Ping {
|
|
node: String,
|
|
},
|
|
AuthCheck {
|
|
node: String,
|
|
resource: String,
|
|
capability: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum OverlayCommand {
|
|
Status,
|
|
Plan {
|
|
name: String,
|
|
#[arg(long)]
|
|
cidr: Option<String>,
|
|
},
|
|
Join {
|
|
name: String,
|
|
#[arg(long)]
|
|
secret: String,
|
|
#[arg(long)]
|
|
cidr: Option<String>,
|
|
},
|
|
Leave {
|
|
name: String,
|
|
},
|
|
InterfacePlan {
|
|
name: String,
|
|
#[arg(long)]
|
|
platform: Option<String>,
|
|
},
|
|
Up {
|
|
name: String,
|
|
#[arg(long)]
|
|
bearer_secret: Option<String>,
|
|
#[arg(long)]
|
|
mtu: Option<u16>,
|
|
},
|
|
Down {
|
|
name: String,
|
|
},
|
|
Peers {
|
|
name: String,
|
|
},
|
|
Send {
|
|
name: String,
|
|
node: String,
|
|
#[arg(long)]
|
|
packet_base64: String,
|
|
#[arg(long)]
|
|
bearer_secret: Option<String>,
|
|
},
|
|
Recv {
|
|
name: String,
|
|
#[arg(long)]
|
|
peek: bool,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum ResourceCommand {
|
|
List,
|
|
Create { kind: String, name: String },
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum KeychainCommand {
|
|
Init {
|
|
#[arg(long)]
|
|
admin_key: Option<PathBuf>,
|
|
#[arg(long)]
|
|
signing_key: Option<PathBuf>,
|
|
},
|
|
Status,
|
|
AdminAdd {
|
|
#[arg(long)]
|
|
admin_key: PathBuf,
|
|
#[arg(long)]
|
|
signing_key: PathBuf,
|
|
#[arg(long)]
|
|
principal: Option<String>,
|
|
#[arg(long)]
|
|
valid_after_ms: Option<i64>,
|
|
#[arg(long)]
|
|
valid_before_ms: Option<i64>,
|
|
},
|
|
AdminRevoke {
|
|
key: String,
|
|
#[arg(long)]
|
|
signing_key: PathBuf,
|
|
#[arg(long)]
|
|
admin_key: Option<PathBuf>,
|
|
},
|
|
AllowedSigners {
|
|
#[arg(long)]
|
|
out: Option<PathBuf>,
|
|
},
|
|
SignFile {
|
|
#[arg(long = "in")]
|
|
input: PathBuf,
|
|
#[arg(long)]
|
|
out: Option<PathBuf>,
|
|
#[arg(long)]
|
|
namespace: Option<String>,
|
|
#[arg(long)]
|
|
signing_key: PathBuf,
|
|
#[arg(long)]
|
|
admin_key: Option<PathBuf>,
|
|
},
|
|
VerifyFile {
|
|
#[arg(long = "in")]
|
|
input: PathBuf,
|
|
#[arg(long)]
|
|
signature: PathBuf,
|
|
#[arg(long)]
|
|
namespace: Option<String>,
|
|
#[arg(long)]
|
|
allowed_signers: Option<PathBuf>,
|
|
#[arg(long)]
|
|
principal: Option<String>,
|
|
},
|
|
Sigchain {
|
|
#[arg(long)]
|
|
out: Option<PathBuf>,
|
|
},
|
|
PublishBundle {
|
|
#[arg(long)]
|
|
out: PathBuf,
|
|
#[arg(long, default_value = geth_keychain::DEFAULT_SSH_SIGCHAIN_DISCOVERY_URL)]
|
|
base_url: String,
|
|
#[arg(long)]
|
|
signing_key: PathBuf,
|
|
#[arg(long)]
|
|
admin_key: Option<PathBuf>,
|
|
#[arg(long = "snapshot")]
|
|
snapshots: Vec<String>,
|
|
},
|
|
VerifySigchain {
|
|
#[arg(long = "in")]
|
|
input: PathBuf,
|
|
},
|
|
ImportSigchain {
|
|
#[arg(long = "in")]
|
|
input: PathBuf,
|
|
},
|
|
VerifyCheckpoint {
|
|
#[arg(long)]
|
|
checkpoint: PathBuf,
|
|
#[arg(long)]
|
|
signature: PathBuf,
|
|
#[arg(long)]
|
|
sigchain: PathBuf,
|
|
#[arg(long)]
|
|
allowed_signers: PathBuf,
|
|
#[arg(long)]
|
|
base_url: Option<String>,
|
|
#[arg(long)]
|
|
principal: Option<String>,
|
|
},
|
|
Fetch {
|
|
#[arg(long, default_value = geth_keychain::DEFAULT_SSH_SIGCHAIN_DISCOVERY_URL)]
|
|
url: String,
|
|
#[arg(long)]
|
|
out: Option<PathBuf>,
|
|
#[arg(long)]
|
|
import: bool,
|
|
},
|
|
Explain {
|
|
op_id: String,
|
|
},
|
|
ExplainSigner {
|
|
key: String,
|
|
},
|
|
Verify,
|
|
Sync {
|
|
node: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum AuthCommand {
|
|
Explain {
|
|
subject: String,
|
|
resource: String,
|
|
capability: String,
|
|
},
|
|
Sync {
|
|
node: String,
|
|
},
|
|
Grant {
|
|
subject: String,
|
|
resource: String,
|
|
capability: String,
|
|
#[arg(long)]
|
|
grant_id: Option<String>,
|
|
#[arg(long)]
|
|
signing_key: PathBuf,
|
|
#[arg(long)]
|
|
admin_key: Option<PathBuf>,
|
|
},
|
|
Revoke {
|
|
resource: String,
|
|
grant_id: String,
|
|
#[arg(long)]
|
|
signing_key: PathBuf,
|
|
#[arg(long)]
|
|
admin_key: Option<PathBuf>,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum SecretCommand {
|
|
Status,
|
|
Create {
|
|
resource: String,
|
|
},
|
|
Rotate {
|
|
resource: String,
|
|
},
|
|
Bearer {
|
|
#[command(subcommand)]
|
|
command: SecretBearerCommand,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum SecretBearerCommand {
|
|
Create {
|
|
resource: String,
|
|
#[arg(long = "capability", required = true)]
|
|
capabilities: Vec<String>,
|
|
#[arg(long)]
|
|
expires_at_ms: Option<i64>,
|
|
},
|
|
List,
|
|
Challenge {
|
|
resource: String,
|
|
#[arg(long = "capability", required = true)]
|
|
capabilities: Vec<String>,
|
|
},
|
|
Prove {
|
|
token: String,
|
|
resource: String,
|
|
#[arg(long)]
|
|
nonce: String,
|
|
#[arg(long = "capability", required = true)]
|
|
capabilities: Vec<String>,
|
|
},
|
|
Verify {
|
|
token: String,
|
|
resource: String,
|
|
#[arg(long)]
|
|
nonce: String,
|
|
#[arg(long)]
|
|
response: String,
|
|
#[arg(long = "capability", required = true)]
|
|
capabilities: Vec<String>,
|
|
},
|
|
Revoke {
|
|
resource: String,
|
|
bearer_id: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum CasCommand {
|
|
Add {
|
|
path: PathBuf,
|
|
},
|
|
AddPrivate {
|
|
resource: String,
|
|
path: PathBuf,
|
|
},
|
|
Get {
|
|
hash: String,
|
|
#[arg(long)]
|
|
out: PathBuf,
|
|
},
|
|
GetPrivate {
|
|
resource: String,
|
|
hash: String,
|
|
#[arg(long)]
|
|
out: PathBuf,
|
|
},
|
|
Fetch {
|
|
node: String,
|
|
hash: String,
|
|
#[arg(long)]
|
|
bearer_secret: Option<String>,
|
|
},
|
|
Hash {
|
|
path: PathBuf,
|
|
},
|
|
Has {
|
|
hash: String,
|
|
},
|
|
Pin {
|
|
hash: String,
|
|
},
|
|
Unpin {
|
|
hash: String,
|
|
},
|
|
Cleanup {
|
|
#[arg(long)]
|
|
dry_run: bool,
|
|
},
|
|
Providers {
|
|
hash: String,
|
|
},
|
|
List,
|
|
Root {
|
|
#[command(subcommand)]
|
|
command: CasRootCommand,
|
|
},
|
|
Conflict {
|
|
#[command(subcommand)]
|
|
command: CasConflictCommand,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum CasRootCommand {
|
|
Add {
|
|
name: String,
|
|
path: PathBuf,
|
|
},
|
|
List,
|
|
Scan {
|
|
name: String,
|
|
},
|
|
Sync {
|
|
node: String,
|
|
name: String,
|
|
#[arg(long)]
|
|
bearer_secret: Option<String>,
|
|
},
|
|
Apply {
|
|
source: String,
|
|
#[arg(long)]
|
|
to: PathBuf,
|
|
#[arg(long)]
|
|
dry_run: bool,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum CasConflictCommand {
|
|
Record {
|
|
root: String,
|
|
path: String,
|
|
kind: String,
|
|
#[arg(long)]
|
|
detail: String,
|
|
#[arg(long)]
|
|
base_tree: Option<String>,
|
|
#[arg(long)]
|
|
local_tree: Option<String>,
|
|
#[arg(long)]
|
|
remote_tree: Option<String>,
|
|
},
|
|
List {
|
|
#[arg(long)]
|
|
root: Option<String>,
|
|
},
|
|
Resolve {
|
|
conflict_id: String,
|
|
resolution: String,
|
|
#[arg(long)]
|
|
note: Option<String>,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum KvCommand {
|
|
Create {
|
|
name: String,
|
|
},
|
|
Set {
|
|
name: String,
|
|
key: String,
|
|
value: String,
|
|
#[arg(long)]
|
|
subject: Option<String>,
|
|
},
|
|
Get {
|
|
name: String,
|
|
key: String,
|
|
},
|
|
Sync {
|
|
node: String,
|
|
name: String,
|
|
#[arg(long)]
|
|
bearer_secret: Option<String>,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum PubsubCommand {
|
|
Pub {
|
|
topic: String,
|
|
message: String,
|
|
#[arg(long)]
|
|
node: Option<String>,
|
|
#[arg(long)]
|
|
bearer_secret: Option<String>,
|
|
},
|
|
Sub {
|
|
topic: String,
|
|
#[arg(long)]
|
|
node: Option<String>,
|
|
#[arg(long)]
|
|
bearer_secret: Option<String>,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum PipeCommand {
|
|
Listen {
|
|
name: String,
|
|
#[arg(long)]
|
|
node: Option<String>,
|
|
#[arg(long)]
|
|
bearer_secret: Option<String>,
|
|
},
|
|
Connect {
|
|
target: String,
|
|
#[arg(long)]
|
|
node: Option<String>,
|
|
#[arg(long)]
|
|
bearer_secret: Option<String>,
|
|
},
|
|
ForwardTcp {
|
|
#[arg(long)]
|
|
listen: String,
|
|
#[arg(long)]
|
|
node: String,
|
|
#[arg(long)]
|
|
target: String,
|
|
#[arg(long)]
|
|
bearer_secret: Option<String>,
|
|
},
|
|
ForwardUnix {
|
|
#[arg(long)]
|
|
listen: PathBuf,
|
|
#[arg(long)]
|
|
node: String,
|
|
#[arg(long)]
|
|
target: PathBuf,
|
|
#[arg(long)]
|
|
bearer_secret: Option<String>,
|
|
},
|
|
Send {
|
|
target: String,
|
|
message: Option<String>,
|
|
#[arg(long = "in", value_name = "PATH")]
|
|
input: Option<PathBuf>,
|
|
#[arg(long)]
|
|
node: Option<String>,
|
|
#[arg(long)]
|
|
bearer_secret: Option<String>,
|
|
},
|
|
Recv {
|
|
name: String,
|
|
#[arg(long)]
|
|
peek: bool,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum DbCommand {
|
|
Add {
|
|
name: String,
|
|
path: PathBuf,
|
|
},
|
|
Status {
|
|
name: String,
|
|
},
|
|
Changes {
|
|
name: String,
|
|
#[arg(long)]
|
|
after_db_version: Option<i64>,
|
|
#[arg(long, default_value_t = 100)]
|
|
limit: u32,
|
|
},
|
|
Sync {
|
|
node: String,
|
|
name: String,
|
|
#[arg(long, default_value_t = 100)]
|
|
limit: u32,
|
|
#[arg(long)]
|
|
bearer_secret: Option<String>,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum DocumentCommand {
|
|
Create {
|
|
name: String,
|
|
},
|
|
Status {
|
|
name: String,
|
|
},
|
|
Set {
|
|
name: String,
|
|
state_json: String,
|
|
},
|
|
Get {
|
|
name: String,
|
|
},
|
|
Sync {
|
|
node: String,
|
|
name: String,
|
|
#[arg(long)]
|
|
bearer_secret: Option<String>,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum SshCommand {
|
|
Proxy {
|
|
node: String,
|
|
#[arg(long)]
|
|
bearer_secret: Option<String>,
|
|
},
|
|
AdminShell {
|
|
node: String,
|
|
command: String,
|
|
#[arg(long)]
|
|
bearer_secret: Option<String>,
|
|
},
|
|
Cert {
|
|
#[command(subcommand)]
|
|
command: SshCertCommand,
|
|
},
|
|
Revocation {
|
|
#[command(subcommand)]
|
|
command: SshRevocationCommand,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum SshCertCommand {
|
|
Request {
|
|
#[arg(long)]
|
|
public_key: PathBuf,
|
|
#[arg(long, default_value = "user")]
|
|
kind: String,
|
|
#[arg(long = "principal", required = true)]
|
|
principals: Vec<String>,
|
|
#[arg(long)]
|
|
valid_for: Option<String>,
|
|
#[arg(long)]
|
|
renewal_of: Option<String>,
|
|
#[arg(long)]
|
|
reason: Option<String>,
|
|
#[arg(long)]
|
|
subject: Option<String>,
|
|
},
|
|
Requests {
|
|
#[arg(long)]
|
|
subject: Option<String>,
|
|
},
|
|
Approve {
|
|
request_id: String,
|
|
#[arg(long)]
|
|
ca_key: PathBuf,
|
|
#[arg(long)]
|
|
valid_for: Option<String>,
|
|
#[arg(long)]
|
|
serial: Option<u64>,
|
|
#[arg(long)]
|
|
out: Option<PathBuf>,
|
|
#[arg(long)]
|
|
sign: bool,
|
|
#[arg(long)]
|
|
subject: Option<String>,
|
|
},
|
|
Import {
|
|
request_id: String,
|
|
#[arg(long)]
|
|
cert: PathBuf,
|
|
#[arg(long)]
|
|
subject: Option<String>,
|
|
},
|
|
List {
|
|
#[arg(long)]
|
|
subject: Option<String>,
|
|
},
|
|
Sync {
|
|
node: String,
|
|
#[arg(long)]
|
|
bearer_secret: Option<String>,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum SshRevocationCommand {
|
|
Add {
|
|
kind: String,
|
|
target: String,
|
|
#[arg(long)]
|
|
reason: Option<String>,
|
|
#[arg(long)]
|
|
subject: Option<String>,
|
|
},
|
|
List {
|
|
#[arg(long)]
|
|
subject: Option<String>,
|
|
},
|
|
Export {
|
|
#[arg(long)]
|
|
out: PathBuf,
|
|
#[arg(long, default_value = "jsonl")]
|
|
format: String,
|
|
#[arg(long)]
|
|
ca_public: Option<PathBuf>,
|
|
#[arg(long)]
|
|
subject: Option<String>,
|
|
},
|
|
Import {
|
|
path: PathBuf,
|
|
#[arg(long, default_value = "jsonl")]
|
|
format: String,
|
|
#[arg(long)]
|
|
subject: Option<String>,
|
|
},
|
|
Sync {
|
|
node: String,
|
|
#[arg(long)]
|
|
bearer_secret: Option<String>,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Args)]
|
|
pub struct EmptyArgs {}
|
|
|
|
pub async fn run() -> Result<()> {
|
|
let cli = Cli::parse();
|
|
if let Command::Completions { shell } = &cli.command {
|
|
print_completions(*shell);
|
|
return Ok(());
|
|
}
|
|
let paths = GethPaths::resolve().context("resolve geth paths")?;
|
|
match cli.command {
|
|
Command::Guide { topic } => {
|
|
print_guide(topic, cli.json || cli.jsonl)?;
|
|
}
|
|
Command::Init {
|
|
admin_key,
|
|
signing_key,
|
|
owner,
|
|
node_name,
|
|
capabilities,
|
|
} => {
|
|
let node = geth_node::init_owned_node(
|
|
&paths,
|
|
geth_node::InitOwnerOptions {
|
|
admin_key_path: admin_key,
|
|
signing_key_path: signing_key,
|
|
owner_name: owner,
|
|
node_name,
|
|
capabilities,
|
|
},
|
|
)
|
|
.context("initialize geth node")?;
|
|
println!("initialized geth home: {}", node.paths.home().display());
|
|
println!("agent: {}", node.agent_id);
|
|
println!("node: {}", node.node_id);
|
|
}
|
|
Command::Daemon {
|
|
command: DaemonCommand::Run,
|
|
} => {
|
|
geth_node::run_daemon(paths)
|
|
.await
|
|
.context("run geth daemon")?;
|
|
}
|
|
Command::Daemon {
|
|
command: DaemonCommand::Service { command },
|
|
} => {
|
|
let report =
|
|
run_service_command(&paths, command).context("manage geth user service")?;
|
|
print_service_report(report, cli.json || cli.jsonl)?;
|
|
}
|
|
Command::Ssh {
|
|
command:
|
|
SshCommand::Proxy {
|
|
node,
|
|
bearer_secret,
|
|
},
|
|
} if !cli.json && !cli.jsonl => {
|
|
geth_node::stream_ssh_proxy(&paths, node, bearer_secret)
|
|
.await
|
|
.context("stream SSH proxy through geth daemon")?;
|
|
}
|
|
Command::Pipe {
|
|
command:
|
|
PipeCommand::ForwardTcp {
|
|
listen,
|
|
node,
|
|
target,
|
|
bearer_secret,
|
|
},
|
|
} if !cli.json && !cli.jsonl => {
|
|
println!("forwarding tcp {listen} -> {node}:{target}");
|
|
geth_node::run_tcp_forward(&paths, listen, node, target, bearer_secret)
|
|
.await
|
|
.context("run TCP forward through geth daemon")?;
|
|
}
|
|
Command::Pipe {
|
|
command:
|
|
PipeCommand::ForwardUnix {
|
|
listen,
|
|
node,
|
|
target,
|
|
bearer_secret,
|
|
},
|
|
} if !cli.json && !cli.jsonl => {
|
|
println!(
|
|
"forwarding unix {} -> {node}:{}",
|
|
listen.display(),
|
|
target.display()
|
|
);
|
|
geth_node::run_unix_forward(&paths, listen, node, target, bearer_secret)
|
|
.await
|
|
.context("run Unix socket forward through geth daemon")?;
|
|
}
|
|
command => {
|
|
let request = request_for_command(command)?;
|
|
let response = geth_node::send_control(&paths, request)
|
|
.await
|
|
.with_context(|| {
|
|
format!("connect to daemon at {}", paths.socket_path().display())
|
|
})?;
|
|
print_response(response, cli.json || cli.jsonl)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn print_guide(topic: Option<GuideTopic>, json: bool) -> Result<()> {
|
|
let (name, body) = match topic {
|
|
None => ("index", GUIDE_INDEX),
|
|
Some(GuideTopic::Init) => ("init", GUIDE_INIT),
|
|
Some(GuideTopic::OwnerSetup) => ("owner-setup", GUIDE_OWNER_SETUP),
|
|
Some(GuideTopic::Enrollment) => ("enrollment", GUIDE_ENROLLMENT),
|
|
Some(GuideTopic::Keys) => ("keys", GUIDE_KEYS),
|
|
Some(GuideTopic::Overlay) => ("overlay", GUIDE_OVERLAY),
|
|
Some(GuideTopic::Service) => ("service", GUIDE_SERVICE),
|
|
Some(GuideTopic::Completions) => ("completions", GUIDE_COMPLETIONS),
|
|
Some(GuideTopic::SmokeTest) => ("smoke-test", GUIDE_SMOKE_TEST),
|
|
};
|
|
if json {
|
|
println!(
|
|
"{}",
|
|
serde_json::json!({
|
|
"topic": name,
|
|
"body": body,
|
|
})
|
|
);
|
|
} else {
|
|
print!("{body}");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn print_completions(shell: Shell) {
|
|
let mut command = Cli::command();
|
|
generate(shell, &mut command, "geth", &mut stdout());
|
|
}
|
|
|
|
fn request_for_command(command: Command) -> Result<ControlRequest> {
|
|
Ok(match command {
|
|
Command::Completions { .. } => bail!("completion generation does not use the daemon"),
|
|
Command::Status => ControlRequest::Status,
|
|
Command::Sync {
|
|
command: SyncCommand::Status,
|
|
} => ControlRequest::SyncStatus,
|
|
Command::Sync {
|
|
command: SyncCommand::Now { node },
|
|
} => ControlRequest::SyncNow { node },
|
|
Command::Node {
|
|
command: NodeCommand::Id,
|
|
} => ControlRequest::NodeId,
|
|
Command::Node {
|
|
command: NodeCommand::Status,
|
|
} => ControlRequest::Status,
|
|
Command::Node {
|
|
command: NodeCommand::List,
|
|
} => ControlRequest::NodeList,
|
|
Command::Node {
|
|
command: NodeCommand::Enroll { command },
|
|
} => match command {
|
|
NodeEnrollCommand::Request {
|
|
node_name,
|
|
capabilities,
|
|
reason,
|
|
out,
|
|
} => ControlRequest::NodeEnrollRequest {
|
|
node_name,
|
|
capabilities,
|
|
reason,
|
|
out,
|
|
},
|
|
NodeEnrollCommand::Submit {
|
|
owner_node,
|
|
request_id,
|
|
path,
|
|
} => ControlRequest::NodeEnrollSubmit {
|
|
owner_node,
|
|
request_id,
|
|
path,
|
|
},
|
|
NodeEnrollCommand::Import { path } => ControlRequest::NodeEnrollImport { path },
|
|
NodeEnrollCommand::List { status } => ControlRequest::NodeEnrollList { status },
|
|
NodeEnrollCommand::Approve {
|
|
request_id,
|
|
signing_key,
|
|
admin_key,
|
|
node_name,
|
|
capabilities,
|
|
} => ControlRequest::NodeEnrollApprove {
|
|
request_id,
|
|
signing_key_path: signing_key,
|
|
admin_key_path: admin_key,
|
|
node_name,
|
|
capabilities,
|
|
},
|
|
NodeEnrollCommand::Sync { owner_node } => ControlRequest::NodeEnrollSync { owner_node },
|
|
},
|
|
Command::Node {
|
|
command:
|
|
NodeCommand::Rename {
|
|
node,
|
|
name,
|
|
signing_key,
|
|
},
|
|
} => ControlRequest::NodeRename {
|
|
node,
|
|
name,
|
|
signing_key_path: signing_key,
|
|
},
|
|
Command::Node {
|
|
command: NodeCommand::Revoke { node, signing_key },
|
|
} => ControlRequest::NodeRevoke {
|
|
node,
|
|
signing_key_path: signing_key,
|
|
},
|
|
Command::Node {
|
|
command:
|
|
NodeCommand::Grant {
|
|
node,
|
|
resource,
|
|
capability,
|
|
grant_id,
|
|
signing_key,
|
|
admin_key,
|
|
},
|
|
} => ControlRequest::NodeGrant {
|
|
node,
|
|
resource,
|
|
capability,
|
|
grant_id,
|
|
signing_key_path: Some(signing_key),
|
|
admin_key_path: admin_key,
|
|
},
|
|
Command::Node {
|
|
command:
|
|
NodeCommand::RevokeGrant {
|
|
resource,
|
|
grant_id,
|
|
signing_key,
|
|
admin_key,
|
|
},
|
|
} => ControlRequest::NodeRevokeGrant {
|
|
resource,
|
|
grant_id,
|
|
signing_key_path: Some(signing_key),
|
|
admin_key_path: admin_key,
|
|
},
|
|
Command::Node {
|
|
command:
|
|
NodeCommand::EndpointAdd {
|
|
node,
|
|
endpoint,
|
|
signing_key,
|
|
},
|
|
} => ControlRequest::NodeEndpointAdd {
|
|
node,
|
|
endpoint,
|
|
signing_key_path: Some(signing_key),
|
|
},
|
|
Command::Node {
|
|
command:
|
|
NodeCommand::EndpointRevoke {
|
|
node,
|
|
endpoint,
|
|
signing_key,
|
|
},
|
|
} => ControlRequest::NodeEndpointRevoke {
|
|
node,
|
|
endpoint,
|
|
signing_key_path: Some(signing_key),
|
|
},
|
|
Command::Peer { command } => match command {
|
|
PeerCommand::Export { out } => ControlRequest::PeerCardExport { out },
|
|
PeerCommand::Import { path } => ControlRequest::PeerCardImport { path },
|
|
PeerCommand::List => ControlRequest::PeerCardList,
|
|
PeerCommand::Ping { node } => ControlRequest::PeerPing { node },
|
|
PeerCommand::AuthCheck {
|
|
node,
|
|
resource,
|
|
capability,
|
|
} => ControlRequest::PeerAuthCheck {
|
|
node,
|
|
resource,
|
|
capability,
|
|
},
|
|
},
|
|
Command::Overlay { command } => match command {
|
|
OverlayCommand::Status => ControlRequest::OverlayStatus,
|
|
OverlayCommand::Plan { name, cidr } => ControlRequest::OverlayPlan { name, cidr },
|
|
OverlayCommand::Join { name, secret, cidr } => {
|
|
ControlRequest::OverlayJoin { name, secret, cidr }
|
|
}
|
|
OverlayCommand::Leave { name } => ControlRequest::OverlayLeave { name },
|
|
OverlayCommand::InterfacePlan { name, platform } => {
|
|
ControlRequest::OverlayInterfacePlan { name, platform }
|
|
}
|
|
OverlayCommand::Up {
|
|
name,
|
|
bearer_secret,
|
|
mtu,
|
|
} => ControlRequest::OverlayUp {
|
|
name,
|
|
bearer_secret,
|
|
mtu,
|
|
},
|
|
OverlayCommand::Down { name } => ControlRequest::OverlayDown { name },
|
|
OverlayCommand::Peers { name } => ControlRequest::OverlayPeers { name },
|
|
OverlayCommand::Send {
|
|
name,
|
|
node,
|
|
packet_base64,
|
|
bearer_secret,
|
|
} => ControlRequest::OverlaySend {
|
|
name,
|
|
node,
|
|
packet_base64,
|
|
bearer_secret,
|
|
},
|
|
OverlayCommand::Recv { name, peek } => ControlRequest::OverlayRecv { name, peek },
|
|
},
|
|
Command::Resource {
|
|
command: ResourceCommand::List,
|
|
} => ControlRequest::ResourceList,
|
|
Command::Resource {
|
|
command: ResourceCommand::Create { kind, name },
|
|
} => ControlRequest::ResourceCreate { kind, name },
|
|
Command::Keychain {
|
|
command:
|
|
KeychainCommand::Init {
|
|
admin_key,
|
|
signing_key,
|
|
},
|
|
} => ControlRequest::KeychainInit {
|
|
admin_key_path: admin_key,
|
|
signing_key_path: signing_key,
|
|
},
|
|
Command::Keychain {
|
|
command: KeychainCommand::Status,
|
|
} => ControlRequest::KeychainStatus,
|
|
Command::Keychain {
|
|
command:
|
|
KeychainCommand::AdminAdd {
|
|
admin_key,
|
|
signing_key,
|
|
principal,
|
|
valid_after_ms,
|
|
valid_before_ms,
|
|
},
|
|
} => ControlRequest::KeychainAdminAdd {
|
|
admin_key_path: admin_key,
|
|
signing_key_path: signing_key,
|
|
principal,
|
|
valid_after_ms,
|
|
valid_before_ms,
|
|
},
|
|
Command::Keychain {
|
|
command:
|
|
KeychainCommand::AdminRevoke {
|
|
key,
|
|
signing_key,
|
|
admin_key,
|
|
},
|
|
} => ControlRequest::KeychainAdminRevoke {
|
|
key,
|
|
signing_key_path: signing_key,
|
|
admin_key_path: admin_key,
|
|
},
|
|
Command::Keychain {
|
|
command: KeychainCommand::AllowedSigners { out },
|
|
} => ControlRequest::KeychainAllowedSigners { out },
|
|
Command::Keychain {
|
|
command:
|
|
KeychainCommand::SignFile {
|
|
input,
|
|
out,
|
|
namespace,
|
|
signing_key,
|
|
admin_key,
|
|
},
|
|
} => ControlRequest::KeychainSignFile {
|
|
input,
|
|
out,
|
|
namespace,
|
|
signing_key_path: Some(signing_key),
|
|
admin_key_path: admin_key,
|
|
},
|
|
Command::Keychain {
|
|
command:
|
|
KeychainCommand::VerifyFile {
|
|
input,
|
|
signature,
|
|
namespace,
|
|
allowed_signers,
|
|
principal,
|
|
},
|
|
} => ControlRequest::KeychainVerifyFile {
|
|
input,
|
|
signature,
|
|
namespace,
|
|
allowed_signers_path: allowed_signers,
|
|
principal,
|
|
},
|
|
Command::Keychain {
|
|
command: KeychainCommand::Sigchain { out },
|
|
} => ControlRequest::KeychainSigchainExport { out },
|
|
Command::Keychain {
|
|
command:
|
|
KeychainCommand::PublishBundle {
|
|
out,
|
|
base_url,
|
|
signing_key,
|
|
admin_key,
|
|
snapshots,
|
|
},
|
|
} => ControlRequest::KeychainPublishBundle {
|
|
out,
|
|
base_url: Some(base_url),
|
|
signing_key_path: signing_key,
|
|
admin_key_path: admin_key,
|
|
snapshots,
|
|
},
|
|
Command::Keychain {
|
|
command: KeychainCommand::VerifySigchain { input },
|
|
} => ControlRequest::KeychainVerifySigchain { input },
|
|
Command::Keychain {
|
|
command: KeychainCommand::ImportSigchain { input },
|
|
} => ControlRequest::KeychainImportSigchain { input },
|
|
Command::Keychain {
|
|
command:
|
|
KeychainCommand::VerifyCheckpoint {
|
|
checkpoint,
|
|
signature,
|
|
sigchain,
|
|
allowed_signers,
|
|
base_url,
|
|
principal,
|
|
},
|
|
} => ControlRequest::KeychainVerifyCheckpoint {
|
|
checkpoint,
|
|
signature,
|
|
sigchain,
|
|
allowed_signers,
|
|
base_url,
|
|
principal,
|
|
},
|
|
Command::Keychain {
|
|
command: KeychainCommand::Fetch { url, out, import },
|
|
} => ControlRequest::KeychainFetch { url, out, import },
|
|
Command::Keychain {
|
|
command: KeychainCommand::Explain { op_id },
|
|
} => ControlRequest::KeychainExplain { op_id },
|
|
Command::Keychain {
|
|
command: KeychainCommand::ExplainSigner { key },
|
|
} => ControlRequest::KeychainExplainSigner { key },
|
|
Command::Keychain {
|
|
command: KeychainCommand::Verify,
|
|
} => ControlRequest::KeychainVerify,
|
|
Command::Keychain {
|
|
command: KeychainCommand::Sync { node },
|
|
} => ControlRequest::KeychainSync { node },
|
|
Command::Auth {
|
|
command: AuthCommand::Sync { node },
|
|
} => ControlRequest::AuthSync { node },
|
|
Command::Auth {
|
|
command:
|
|
AuthCommand::Explain {
|
|
subject,
|
|
resource,
|
|
capability,
|
|
},
|
|
} => ControlRequest::AuthExplain {
|
|
subject,
|
|
resource,
|
|
capability,
|
|
},
|
|
Command::Auth {
|
|
command:
|
|
AuthCommand::Grant {
|
|
subject,
|
|
resource,
|
|
capability,
|
|
grant_id,
|
|
signing_key,
|
|
admin_key,
|
|
},
|
|
} => ControlRequest::AuthGrant {
|
|
subject,
|
|
resource,
|
|
capability,
|
|
grant_id,
|
|
signing_key_path: Some(signing_key),
|
|
admin_key_path: admin_key,
|
|
},
|
|
Command::Auth {
|
|
command:
|
|
AuthCommand::Revoke {
|
|
resource,
|
|
grant_id,
|
|
signing_key,
|
|
admin_key,
|
|
},
|
|
} => ControlRequest::AuthRevoke {
|
|
resource,
|
|
grant_id,
|
|
signing_key_path: Some(signing_key),
|
|
admin_key_path: admin_key,
|
|
},
|
|
Command::Secret { command } => match command {
|
|
SecretCommand::Status => ControlRequest::SecretStatus,
|
|
SecretCommand::Create { resource } => ControlRequest::SecretCreate { resource },
|
|
SecretCommand::Rotate { resource } => ControlRequest::SecretRotate { resource },
|
|
SecretCommand::Bearer { command } => match command {
|
|
SecretBearerCommand::Create {
|
|
resource,
|
|
capabilities,
|
|
expires_at_ms,
|
|
} => ControlRequest::SecretBearerCreate {
|
|
resource,
|
|
capabilities,
|
|
expires_at_ms,
|
|
},
|
|
SecretBearerCommand::List => ControlRequest::SecretBearerList,
|
|
SecretBearerCommand::Challenge {
|
|
resource,
|
|
capabilities,
|
|
} => ControlRequest::SecretBearerChallenge {
|
|
resource,
|
|
capabilities,
|
|
},
|
|
SecretBearerCommand::Prove {
|
|
token,
|
|
resource,
|
|
capabilities,
|
|
nonce,
|
|
} => ControlRequest::SecretBearerProve {
|
|
secret: token,
|
|
resource,
|
|
capabilities,
|
|
nonce,
|
|
},
|
|
SecretBearerCommand::Verify {
|
|
token,
|
|
resource,
|
|
capabilities,
|
|
nonce,
|
|
response,
|
|
} => ControlRequest::SecretBearerVerify {
|
|
secret: token,
|
|
resource,
|
|
capabilities,
|
|
nonce,
|
|
response,
|
|
},
|
|
SecretBearerCommand::Revoke {
|
|
resource,
|
|
bearer_id,
|
|
} => ControlRequest::SecretBearerRevoke {
|
|
resource,
|
|
secret: bearer_id,
|
|
},
|
|
},
|
|
},
|
|
Command::Cas { command } => match command {
|
|
CasCommand::Add { path } => ControlRequest::CasAdd { path },
|
|
CasCommand::AddPrivate { resource, path } => {
|
|
ControlRequest::CasAddPrivate { resource, path }
|
|
}
|
|
CasCommand::Get { hash, out } => ControlRequest::CasGet {
|
|
hash: hash.into(),
|
|
out,
|
|
},
|
|
CasCommand::GetPrivate {
|
|
resource,
|
|
hash,
|
|
out,
|
|
} => ControlRequest::CasGetPrivate {
|
|
resource,
|
|
hash: hash.into(),
|
|
out,
|
|
},
|
|
CasCommand::Fetch {
|
|
node,
|
|
hash,
|
|
bearer_secret,
|
|
} => ControlRequest::CasFetch {
|
|
node,
|
|
hash: hash.into(),
|
|
bearer_secret,
|
|
},
|
|
CasCommand::Hash { path } => ControlRequest::CasHash { path },
|
|
CasCommand::Has { hash } => ControlRequest::CasHas { hash: hash.into() },
|
|
CasCommand::Pin { hash } => ControlRequest::CasPin { hash: hash.into() },
|
|
CasCommand::Unpin { hash } => ControlRequest::CasUnpin { hash: hash.into() },
|
|
CasCommand::Cleanup { dry_run } => ControlRequest::CasCleanup { dry_run },
|
|
CasCommand::Providers { hash } => ControlRequest::CasProviders { hash: hash.into() },
|
|
CasCommand::List => ControlRequest::CasList,
|
|
CasCommand::Root { command } => match command {
|
|
CasRootCommand::Add { name, path } => ControlRequest::CasRootAdd { name, path },
|
|
CasRootCommand::List => ControlRequest::CasRootList,
|
|
CasRootCommand::Scan { name } => ControlRequest::CasRootScan { name },
|
|
CasRootCommand::Sync {
|
|
node,
|
|
name,
|
|
bearer_secret,
|
|
} => ControlRequest::CasRootSync {
|
|
node,
|
|
name,
|
|
bearer_secret,
|
|
},
|
|
CasRootCommand::Apply {
|
|
source,
|
|
to,
|
|
dry_run,
|
|
} => ControlRequest::CasRootApply {
|
|
source,
|
|
target: to,
|
|
dry_run,
|
|
},
|
|
},
|
|
CasCommand::Conflict { command } => match command {
|
|
CasConflictCommand::Record {
|
|
root,
|
|
path,
|
|
kind,
|
|
detail,
|
|
base_tree,
|
|
local_tree,
|
|
remote_tree,
|
|
} => ControlRequest::CasConflictRecord {
|
|
root,
|
|
path,
|
|
kind,
|
|
detail,
|
|
base_tree: base_tree.map(Into::into),
|
|
local_tree: local_tree.map(Into::into),
|
|
remote_tree: remote_tree.map(Into::into),
|
|
},
|
|
CasConflictCommand::List { root } => ControlRequest::CasConflictList { root },
|
|
CasConflictCommand::Resolve {
|
|
conflict_id,
|
|
resolution,
|
|
note,
|
|
} => ControlRequest::CasConflictResolve {
|
|
conflict_id,
|
|
resolution,
|
|
note,
|
|
},
|
|
},
|
|
},
|
|
Command::Kv { command } => match command {
|
|
KvCommand::Create { name } => ControlRequest::KvCreate { name },
|
|
KvCommand::Set {
|
|
name,
|
|
key,
|
|
value,
|
|
subject,
|
|
} => ControlRequest::KvSet {
|
|
name,
|
|
key,
|
|
value,
|
|
subject,
|
|
},
|
|
KvCommand::Get { name, key } => ControlRequest::KvGet { name, key },
|
|
KvCommand::Sync {
|
|
node,
|
|
name,
|
|
bearer_secret,
|
|
} => ControlRequest::KvSync {
|
|
node,
|
|
name,
|
|
bearer_secret,
|
|
},
|
|
},
|
|
Command::Pubsub { command } => match command {
|
|
PubsubCommand::Pub {
|
|
topic,
|
|
message,
|
|
node,
|
|
bearer_secret,
|
|
} => ControlRequest::PubsubPub {
|
|
topic,
|
|
message,
|
|
node,
|
|
bearer_secret,
|
|
},
|
|
PubsubCommand::Sub {
|
|
topic,
|
|
node,
|
|
bearer_secret,
|
|
} => ControlRequest::PubsubSub {
|
|
topic,
|
|
node,
|
|
bearer_secret,
|
|
},
|
|
},
|
|
Command::Pipe { command } => match command {
|
|
PipeCommand::Listen {
|
|
name,
|
|
node,
|
|
bearer_secret,
|
|
} => ControlRequest::PipeListen {
|
|
name,
|
|
node,
|
|
bearer_secret,
|
|
},
|
|
PipeCommand::Connect {
|
|
target,
|
|
node,
|
|
bearer_secret,
|
|
} => ControlRequest::PipeConnect {
|
|
target,
|
|
node,
|
|
bearer_secret,
|
|
},
|
|
PipeCommand::ForwardTcp {
|
|
listen,
|
|
node,
|
|
target,
|
|
bearer_secret,
|
|
} => ControlRequest::PipeTcpForward {
|
|
listen_addr: listen,
|
|
node,
|
|
target_addr: target,
|
|
bearer_secret,
|
|
},
|
|
PipeCommand::ForwardUnix {
|
|
listen,
|
|
node,
|
|
target,
|
|
bearer_secret,
|
|
} => ControlRequest::PipeUnixForward {
|
|
listen_path: listen,
|
|
node,
|
|
target_path: target,
|
|
bearer_secret,
|
|
},
|
|
PipeCommand::Send {
|
|
target,
|
|
message,
|
|
input,
|
|
node,
|
|
bearer_secret,
|
|
} => ControlRequest::PipeSend {
|
|
target,
|
|
data_base64: pipe_send_payload_base64(message, input)?,
|
|
node,
|
|
bearer_secret,
|
|
},
|
|
PipeCommand::Recv { name, peek } => ControlRequest::PipeRecv { name, peek },
|
|
},
|
|
Command::Db { command } => match command {
|
|
DbCommand::Add { name, path } => ControlRequest::DbAdd { name, path },
|
|
DbCommand::Status { name } => ControlRequest::DbStatus { name },
|
|
DbCommand::Changes {
|
|
name,
|
|
after_db_version,
|
|
limit,
|
|
} => ControlRequest::DbChanges {
|
|
name,
|
|
after_db_version,
|
|
limit,
|
|
},
|
|
DbCommand::Sync {
|
|
node,
|
|
name,
|
|
limit,
|
|
bearer_secret,
|
|
} => ControlRequest::DbSync {
|
|
node,
|
|
name,
|
|
limit,
|
|
bearer_secret,
|
|
},
|
|
},
|
|
Command::Document { command } => match command {
|
|
DocumentCommand::Create { name } => ControlRequest::DocumentCreate { name },
|
|
DocumentCommand::Status { name } => ControlRequest::DocumentStatus { name },
|
|
DocumentCommand::Set { name, state_json } => {
|
|
ControlRequest::DocumentSet { name, state_json }
|
|
}
|
|
DocumentCommand::Get { name } => ControlRequest::DocumentGet { name },
|
|
DocumentCommand::Sync {
|
|
node,
|
|
name,
|
|
bearer_secret,
|
|
} => ControlRequest::DocumentSync {
|
|
node,
|
|
name,
|
|
bearer_secret,
|
|
},
|
|
},
|
|
Command::Ssh { command } => match command {
|
|
SshCommand::Proxy {
|
|
node,
|
|
bearer_secret,
|
|
} => ControlRequest::SshProxyConnect {
|
|
node,
|
|
bearer_secret,
|
|
},
|
|
SshCommand::AdminShell {
|
|
node,
|
|
command,
|
|
bearer_secret,
|
|
} => ControlRequest::SshAdminShell {
|
|
node,
|
|
command,
|
|
bearer_secret,
|
|
},
|
|
SshCommand::Cert { command } => match command {
|
|
SshCertCommand::Request {
|
|
public_key,
|
|
kind,
|
|
principals,
|
|
valid_for,
|
|
renewal_of,
|
|
reason,
|
|
subject,
|
|
} => ControlRequest::SshCertRequest {
|
|
public_key_path: public_key,
|
|
cert_kind: kind,
|
|
principals,
|
|
requested_validity: valid_for,
|
|
renewal_of,
|
|
reason,
|
|
subject,
|
|
},
|
|
SshCertCommand::Requests { subject } => ControlRequest::SshCertRequests { subject },
|
|
SshCertCommand::Approve {
|
|
request_id,
|
|
ca_key,
|
|
valid_for,
|
|
serial,
|
|
out,
|
|
sign,
|
|
subject,
|
|
} => ControlRequest::SshCertApprove {
|
|
request_id,
|
|
ca_key_path: ca_key,
|
|
valid_for,
|
|
serial,
|
|
out,
|
|
sign,
|
|
subject,
|
|
},
|
|
SshCertCommand::Import {
|
|
request_id,
|
|
cert,
|
|
subject,
|
|
} => ControlRequest::SshCertImport {
|
|
request_id,
|
|
cert_path: cert,
|
|
subject,
|
|
},
|
|
SshCertCommand::List { subject } => ControlRequest::SshCertList { subject },
|
|
SshCertCommand::Sync {
|
|
node,
|
|
bearer_secret,
|
|
} => ControlRequest::SshCertSync {
|
|
node,
|
|
bearer_secret,
|
|
},
|
|
},
|
|
SshCommand::Revocation { command } => match command {
|
|
SshRevocationCommand::Add {
|
|
kind,
|
|
target,
|
|
reason,
|
|
subject,
|
|
} => ControlRequest::SshRevocationAdd {
|
|
kind,
|
|
target,
|
|
reason,
|
|
subject,
|
|
},
|
|
SshRevocationCommand::List { subject } => {
|
|
ControlRequest::SshRevocationList { subject }
|
|
}
|
|
SshRevocationCommand::Export {
|
|
out,
|
|
format,
|
|
ca_public,
|
|
subject,
|
|
} => ControlRequest::SshRevocationExport {
|
|
out,
|
|
format,
|
|
ca_public,
|
|
subject,
|
|
},
|
|
SshRevocationCommand::Import {
|
|
path,
|
|
format,
|
|
subject,
|
|
} => ControlRequest::SshRevocationImport {
|
|
path,
|
|
format,
|
|
subject,
|
|
},
|
|
SshRevocationCommand::Sync {
|
|
node,
|
|
bearer_secret,
|
|
} => ControlRequest::SshRevocationSync {
|
|
node,
|
|
bearer_secret,
|
|
},
|
|
},
|
|
},
|
|
Command::Guide { .. } | Command::Init { .. } | Command::Daemon { .. } => {
|
|
bail!("command is handled directly")
|
|
}
|
|
})
|
|
}
|
|
|
|
fn run_service_command(paths: &GethPaths, command: ServiceCommand) -> Result<ServiceReport> {
|
|
Ok(match command {
|
|
ServiceCommand::Install {
|
|
manager,
|
|
bin,
|
|
start,
|
|
} => {
|
|
geth_node::init_node(paths).context("initialize geth home before service install")?;
|
|
let manager = manager.parse::<ServiceManager>()?;
|
|
let executable = service_executable(bin)?;
|
|
geth_node::service::install_user_service(
|
|
paths,
|
|
ServiceInstallOptions {
|
|
manager,
|
|
executable,
|
|
start,
|
|
},
|
|
)?
|
|
}
|
|
ServiceCommand::Uninstall { manager } => {
|
|
geth_node::service::uninstall_user_service(manager.parse::<ServiceManager>()?)?
|
|
}
|
|
ServiceCommand::Start { manager } => {
|
|
geth_node::service::start_user_service(manager.parse::<ServiceManager>()?)?
|
|
}
|
|
ServiceCommand::Stop { manager } => {
|
|
geth_node::service::stop_user_service(manager.parse::<ServiceManager>()?)?
|
|
}
|
|
ServiceCommand::Status { manager } => {
|
|
geth_node::service::status_user_service(manager.parse::<ServiceManager>()?)?
|
|
}
|
|
ServiceCommand::Print { manager, bin } => {
|
|
let executable = service_executable(bin)?;
|
|
geth_node::service::print_user_service(
|
|
paths,
|
|
manager.parse::<ServiceManager>()?,
|
|
&executable,
|
|
)?
|
|
}
|
|
})
|
|
}
|
|
|
|
fn service_executable(bin: Option<PathBuf>) -> Result<PathBuf> {
|
|
bin.map(Ok)
|
|
.unwrap_or_else(std::env::current_exe)
|
|
.context("resolve current geth executable")
|
|
}
|
|
|
|
fn print_keychain_sigchain_report(report: &geth_keychain::KeychainSigchainReport) {
|
|
println!("ops: {}", report.ops);
|
|
println!("signatures: {}", report.signatures);
|
|
println!("accepted_ops: {}", report.accepted_ops);
|
|
println!("rejected_ops: {}", report.rejected_ops);
|
|
println!("active_admin_keys: {}", report.active_admin_keys);
|
|
println!(
|
|
"accepted_head: {}",
|
|
report
|
|
.accepted_head
|
|
.as_ref()
|
|
.map(|head| head.as_str())
|
|
.unwrap_or("none")
|
|
);
|
|
println!("note: {}", report.note);
|
|
}
|
|
|
|
fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
|
if json {
|
|
println!("{}", serde_json::to_string_pretty(&response)?);
|
|
return Ok(());
|
|
}
|
|
match response {
|
|
ControlResponse::Status(status) => {
|
|
println!("geth daemon: running");
|
|
println!("home: {}", status.home.display());
|
|
println!("socket: {}", status.socket.display());
|
|
println!("agent: {}", status.agent_id);
|
|
println!("node: {}", status.node_id);
|
|
println!(
|
|
"endpoint: {}",
|
|
status.endpoint_id.as_deref().unwrap_or("not started")
|
|
);
|
|
println!("iroh relay: {}", status.iroh_relay_mode);
|
|
println!(
|
|
"iroh discovery: {}",
|
|
if status.iroh_local_discovery {
|
|
"local-network enabled"
|
|
} else {
|
|
"local-network disabled"
|
|
}
|
|
);
|
|
println!("iroh: {}", status.iroh);
|
|
for backend in status.native_backends {
|
|
println!(
|
|
"native backend {}: {} target {} {} ({})",
|
|
backend.module,
|
|
backend.current_backend,
|
|
backend.target_crate,
|
|
backend.target_version,
|
|
backend.status
|
|
);
|
|
if !backend.blocker.is_empty() {
|
|
println!(
|
|
"native backend {} note: {}",
|
|
backend.module, backend.blocker
|
|
);
|
|
}
|
|
}
|
|
}
|
|
ControlResponse::NodeId(node) => {
|
|
println!("agent: {}", node.agent_id);
|
|
println!("node: {}", node.node_id);
|
|
println!(
|
|
"endpoint: {}",
|
|
node.endpoint_id
|
|
.as_deref()
|
|
.unwrap_or("not started in bootstrap")
|
|
);
|
|
}
|
|
ControlResponse::PeerCardExported { card, out, note } => {
|
|
println!("peer card: {}", card.node_id);
|
|
println!("agent: {}", card.agent_id);
|
|
println!("endpoints: {}", card.endpoints.len());
|
|
if let Some(path) = out {
|
|
println!("wrote: {}", path.display());
|
|
} else {
|
|
println!("{}", serde_json::to_string_pretty(&card)?);
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::PeerCardImported { peer, note } => {
|
|
println!("imported peer: {}", peer.card.node_id);
|
|
println!("agent: {}", peer.card.agent_id);
|
|
println!("trust: candidate-only");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::PeerCardList { peers, note } => {
|
|
if peers.is_empty() {
|
|
println!("no peer candidates");
|
|
} else {
|
|
for peer in peers {
|
|
println!(
|
|
"{}\t{}\t{} endpoints\tcandidate-only",
|
|
peer.card.node_id,
|
|
peer.card.agent_id,
|
|
peer.card.endpoints.len()
|
|
);
|
|
}
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::PeerPinged {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
alpn,
|
|
note,
|
|
} => {
|
|
println!("peer pong: {peer_node_id}");
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("alpn: {alpn}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::PeerAuthChecked {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
resource,
|
|
capability,
|
|
allowed,
|
|
reason,
|
|
evaluated_ops,
|
|
note,
|
|
} => {
|
|
println!("peer auth: {peer_node_id}");
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("resource: {resource}");
|
|
println!("capability: {capability}");
|
|
println!("allowed: {allowed}");
|
|
println!("reason: {reason}");
|
|
println!("evaluated_ops: {evaluated_ops}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::ResourceList { resources } => {
|
|
if resources.is_empty() {
|
|
println!("no resources");
|
|
} else {
|
|
for resource in resources {
|
|
println!("{}\t{}\t{}", resource.kind, resource.name, resource.id);
|
|
}
|
|
}
|
|
}
|
|
ControlResponse::ResourceCreated { resource } => {
|
|
println!(
|
|
"created resource: {} {} ({})",
|
|
resource.kind, resource.name, resource.id
|
|
);
|
|
}
|
|
ControlResponse::OverlayStatus { networks, note } => {
|
|
if networks.is_empty() {
|
|
println!("no overlay networks active");
|
|
} else {
|
|
for network in networks {
|
|
println!(
|
|
"{}\t{}\t{}\t{:?}\t{} peers",
|
|
network.name,
|
|
network.resource,
|
|
network.cidr,
|
|
network.state,
|
|
network.peers.len()
|
|
);
|
|
}
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::OverlayPlanned { plan } => {
|
|
println!("overlay: {}", plan.name);
|
|
println!("resource: {}", plan.resource);
|
|
println!("cidr: {}", plan.cidr);
|
|
println!("alpn: {}", plan.alpn);
|
|
println!("capabilities: {}", plan.capabilities.join(","));
|
|
println!("discovery: {}", plan.discovery);
|
|
println!("runtime: {}", plan.runtime);
|
|
for note in plan.security {
|
|
println!("security: {note}");
|
|
}
|
|
for note in plan.implementation_notes {
|
|
println!("implementation: {note}");
|
|
}
|
|
}
|
|
ControlResponse::OverlayJoined { join } => {
|
|
println!("overlay: {}", join.plan.name);
|
|
println!("resource: {}", join.plan.resource);
|
|
println!("cidr: {}", join.network.cidr);
|
|
println!(
|
|
"virtual_ip: {}",
|
|
join.network.virtual_ip.as_deref().unwrap_or("unassigned")
|
|
);
|
|
println!("state: {:?}", join.network.state);
|
|
println!("enabled: {}", join.enabled);
|
|
println!("note: {}", join.note);
|
|
}
|
|
ControlResponse::OverlayLeft {
|
|
name,
|
|
stopped,
|
|
note,
|
|
} => {
|
|
println!("overlay: {name}");
|
|
println!("stopped: {stopped}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::OverlayInterfacePlanned { plan } => {
|
|
println!("overlay: {}", plan.name);
|
|
println!("platform: {}", plan.platform);
|
|
println!("interface: {}", plan.interface_name);
|
|
println!("cidr: {}", plan.cidr);
|
|
println!(
|
|
"virtual_ip: {}",
|
|
plan.virtual_ip.as_deref().unwrap_or("unassigned")
|
|
);
|
|
println!("requires_privileges: {}", plan.requires_privileges);
|
|
for command in plan.commands {
|
|
println!("command: {command}");
|
|
}
|
|
for note in plan.notes {
|
|
println!("note: {note}");
|
|
}
|
|
}
|
|
ControlResponse::OverlayRuntimeStarted { status } => {
|
|
println!("overlay: {}", status.name);
|
|
println!("interface: {}", status.interface_name);
|
|
println!("virtual_ip: {}", status.virtual_ip);
|
|
println!("cidr: {}", status.cidr);
|
|
println!("mtu: {}", status.mtu);
|
|
println!("packets_from_tun: {}", status.packets_from_tun);
|
|
println!("packets_to_tun: {}", status.packets_to_tun);
|
|
println!("packets_to_peers: {}", status.packets_to_peers);
|
|
if let Some(error) = status.last_error {
|
|
println!("last_error: {error}");
|
|
}
|
|
println!("note: {}", status.note);
|
|
}
|
|
ControlResponse::OverlayRuntimeStopped {
|
|
name,
|
|
stopped,
|
|
note,
|
|
} => {
|
|
println!("overlay: {name}");
|
|
println!("stopped: {stopped}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::OverlayPeers { name, peers, note } => {
|
|
println!("overlay: {name}");
|
|
if peers.is_empty() {
|
|
println!("no overlay peer candidates");
|
|
} else {
|
|
for peer in peers {
|
|
println!(
|
|
"{}\t{}\t{}\t{}",
|
|
peer.node_id,
|
|
peer.endpoint_id.as_deref().unwrap_or("no-endpoint"),
|
|
peer.virtual_ip.as_deref().unwrap_or("no-virtual-ip"),
|
|
peer.state
|
|
);
|
|
}
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::OverlayPacketSent {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
packet,
|
|
allowed,
|
|
reason,
|
|
note,
|
|
} => {
|
|
println!("peer: {peer_node_id}");
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("allowed: {allowed}");
|
|
println!("reason: {reason}");
|
|
if let Some(packet) = packet {
|
|
println!("packet: {}", packet.id);
|
|
println!("size_bytes: {}", packet.size_bytes);
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::OverlayPackets {
|
|
name,
|
|
packets,
|
|
drained,
|
|
note,
|
|
} => {
|
|
println!("overlay: {name}");
|
|
println!("drained: {drained}");
|
|
for packet in packets {
|
|
println!(
|
|
"{}\t{}\t{}\t{} bytes",
|
|
packet.id, packet.source_node, packet.destination_node, packet.size_bytes
|
|
);
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::CasAdded { hash, size_bytes } => {
|
|
println!("{hash} {size_bytes} bytes");
|
|
}
|
|
ControlResponse::CasPrivateAdded {
|
|
resource,
|
|
epoch,
|
|
plaintext_hash,
|
|
encrypted_hash,
|
|
size_bytes,
|
|
note,
|
|
} => {
|
|
println!("encrypted_hash: {encrypted_hash}");
|
|
println!("plaintext_hash: {plaintext_hash}");
|
|
println!("resource: {resource}");
|
|
println!("epoch: {epoch}");
|
|
println!("size_bytes: {size_bytes}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::CasGot {
|
|
hash,
|
|
out,
|
|
size_bytes,
|
|
} => {
|
|
println!("wrote {hash} to {} ({size_bytes} bytes)", out.display());
|
|
}
|
|
ControlResponse::CasPrivateGot {
|
|
resource,
|
|
hash,
|
|
plaintext_hash,
|
|
out,
|
|
size_bytes,
|
|
note,
|
|
} => {
|
|
println!(
|
|
"decrypted {hash} for {resource} to {} ({size_bytes} bytes)",
|
|
out.display()
|
|
);
|
|
println!("plaintext_hash: {plaintext_hash}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::CasFetched {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
hash,
|
|
size_bytes,
|
|
allowed,
|
|
reason,
|
|
note,
|
|
} => {
|
|
if allowed {
|
|
println!("fetched {hash} from {peer_node_id} ({size_bytes} bytes)");
|
|
} else {
|
|
println!("fetch denied for {hash} from {peer_node_id}");
|
|
}
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("allowed: {allowed}");
|
|
println!("reason: {reason}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::CasHash { hash } => println!("{hash}"),
|
|
ControlResponse::CasHas { hash, present } => println!("{hash}: {present}"),
|
|
ControlResponse::CasPinned { hash, pinned } => {
|
|
println!("{hash}: pinned={pinned}");
|
|
}
|
|
ControlResponse::CasCleanup {
|
|
removed,
|
|
retained_pinned,
|
|
dry_run,
|
|
} => {
|
|
let action = if dry_run { "would remove" } else { "removed" };
|
|
println!("{action}: {}", removed.len());
|
|
for hash in removed {
|
|
println!(" {hash}");
|
|
}
|
|
println!("retained_pinned: {}", retained_pinned.len());
|
|
for hash in retained_pinned {
|
|
println!(" {hash}");
|
|
}
|
|
}
|
|
ControlResponse::CasList { blobs } => {
|
|
for blob in blobs {
|
|
let pin = if blob.pinned { "pinned" } else { "unpinned" };
|
|
println!("{}\t{} bytes\t{}", blob.hash, blob.size_bytes, pin);
|
|
}
|
|
}
|
|
ControlResponse::CasProviders { hash, providers } => {
|
|
println!("hash: {hash}");
|
|
println!("providers: {}", providers.len());
|
|
for provider in providers {
|
|
println!(
|
|
"{}\t{}\t{}",
|
|
provider.peer_node_id, provider.endpoint_id, provider.last_seen_ms
|
|
);
|
|
}
|
|
}
|
|
ControlResponse::CasRootAdded { root } => {
|
|
println!("added file root: {}", root.name);
|
|
println!("id: {}", root.id);
|
|
println!("resource: {}", root.resource);
|
|
println!("path: {}", root.path);
|
|
}
|
|
ControlResponse::CasRootList { roots } => {
|
|
if roots.is_empty() {
|
|
println!("no file roots");
|
|
} else {
|
|
for root in roots {
|
|
println!(
|
|
"{}\t{}\t{}",
|
|
root.name,
|
|
root.path,
|
|
root.latest_tree
|
|
.map(|hash| hash.to_string())
|
|
.unwrap_or_else(|| "unscanned".to_owned())
|
|
);
|
|
}
|
|
}
|
|
}
|
|
ControlResponse::CasRootScanned { scan } => {
|
|
println!("file root: {}", scan.root.name);
|
|
println!("tree: {}", scan.tree.hash);
|
|
println!("tree_bytes: {}", scan.tree.size_bytes);
|
|
println!("changes: {}", scan.changes.len());
|
|
for change in scan.changes {
|
|
println!("{change:?}");
|
|
}
|
|
println!("note: {}", scan.note);
|
|
}
|
|
ControlResponse::CasRootSynced {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
name,
|
|
root,
|
|
tree_bytes_imported,
|
|
sync_conflicts,
|
|
allowed,
|
|
reason,
|
|
note,
|
|
} => {
|
|
if let Some(root) = root {
|
|
println!("synced file root: {name}");
|
|
println!("peer: {peer_node_id}");
|
|
println!("path: {}", root.path);
|
|
println!(
|
|
"tree: {}",
|
|
root.latest_tree
|
|
.map(|hash| hash.to_string())
|
|
.unwrap_or_else(|| "unscanned".to_owned())
|
|
);
|
|
println!("tree_bytes_imported: {tree_bytes_imported}");
|
|
println!("sync_conflicts: {}", sync_conflicts.len());
|
|
for conflict in sync_conflicts {
|
|
println!(
|
|
"{}\t{}\t{}\t{}",
|
|
conflict.id,
|
|
conflict.path,
|
|
conflict.kind.as_str(),
|
|
conflict.status.as_str()
|
|
);
|
|
}
|
|
} else {
|
|
println!("file root sync denied by {peer_node_id}");
|
|
}
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("allowed: {allowed}");
|
|
println!("reason: {reason}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::CasRootApplied {
|
|
source,
|
|
target,
|
|
files_written,
|
|
dirs_created,
|
|
conflicts,
|
|
dry_run,
|
|
note,
|
|
} => {
|
|
println!("applied file root: {source}");
|
|
println!("target: {}", target.display());
|
|
println!("dry_run: {dry_run}");
|
|
println!("files_written: {files_written}");
|
|
println!("dirs_created: {dirs_created}");
|
|
println!("conflicts: {}", conflicts.len());
|
|
for conflict in conflicts {
|
|
println!(
|
|
"{}\t{}\t{}\t{}",
|
|
conflict.id,
|
|
conflict.path,
|
|
conflict.kind.as_str(),
|
|
conflict.status.as_str()
|
|
);
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::CasConflictRecorded { conflict } => {
|
|
println!("recorded conflict: {}", conflict.id);
|
|
print_file_conflict(&conflict);
|
|
}
|
|
ControlResponse::CasConflictList { conflicts } => {
|
|
if conflicts.is_empty() {
|
|
println!("no file conflicts");
|
|
} else {
|
|
for conflict in conflicts {
|
|
println!(
|
|
"{}\t{}\t{}\t{}\t{}",
|
|
conflict.id,
|
|
conflict.root,
|
|
conflict.path,
|
|
conflict.kind.as_str(),
|
|
conflict.status.as_str()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
ControlResponse::CasConflictResolved { conflict } => {
|
|
println!("resolved conflict: {}", conflict.id);
|
|
print_file_conflict(&conflict);
|
|
}
|
|
ControlResponse::KeychainStatus(status) => {
|
|
println!("initialized: {}", status.initialized);
|
|
println!("admin_keys: {}", status.admin_keys);
|
|
println!("signatures: {}", status.signatures);
|
|
println!("verified_signatures: {}", status.verified_signatures);
|
|
println!("failed_signatures: {}", status.failed_signatures);
|
|
println!("users: {}", status.users);
|
|
println!("devices: {}", status.devices);
|
|
println!("nodes: {}", status.nodes);
|
|
}
|
|
ControlResponse::KeychainInitialized { ops, signatures } => {
|
|
println!("initialized keychain");
|
|
for op in ops {
|
|
println!("recorded keychain op: {}", op.id);
|
|
}
|
|
for signature in signatures {
|
|
println!(
|
|
"signed keychain op: {} by {} ({})",
|
|
signature.op_id, signature.signer, signature.namespace
|
|
);
|
|
}
|
|
}
|
|
ControlResponse::KeychainAdminUpdated {
|
|
op,
|
|
signatures,
|
|
note,
|
|
} => {
|
|
println!("recorded keychain op: {}", op.id);
|
|
for signature in signatures {
|
|
println!(
|
|
"signed keychain op: {} by {} ({})",
|
|
signature.op_id, signature.signer, signature.namespace
|
|
);
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::KeychainAllowedSigners {
|
|
allowed_signers,
|
|
out,
|
|
note,
|
|
..
|
|
} => {
|
|
if let Some(out) = out {
|
|
println!("wrote allowed_signers: {}", out.display());
|
|
if allowed_signers.is_empty() {
|
|
println!("warning: generated file has no active admin public keys");
|
|
}
|
|
} else {
|
|
print!("{allowed_signers}");
|
|
if allowed_signers.is_empty() {
|
|
println!("no active admin public keys available");
|
|
}
|
|
}
|
|
eprintln!("note: {note}");
|
|
}
|
|
ControlResponse::KeychainFileSigned {
|
|
input,
|
|
out,
|
|
namespace,
|
|
signer,
|
|
note,
|
|
} => {
|
|
println!("signed file: {}", input.display());
|
|
println!(
|
|
"signature: {}",
|
|
out.map(|path| path.display().to_string())
|
|
.unwrap_or_else(|| "none".to_owned())
|
|
);
|
|
println!("namespace: {namespace}");
|
|
println!("signer: {signer}");
|
|
eprintln!("note: {note}");
|
|
}
|
|
ControlResponse::KeychainFileVerified {
|
|
input,
|
|
signature,
|
|
namespace,
|
|
verified,
|
|
principal,
|
|
note,
|
|
} => {
|
|
println!("file: {}", input.display());
|
|
println!("signature: {}", signature.display());
|
|
println!("namespace: {namespace}");
|
|
println!("principal: {}", principal.as_deref().unwrap_or("none"));
|
|
println!("verified: {verified}");
|
|
eprintln!("note: {note}");
|
|
}
|
|
ControlResponse::KeychainSigchainExported {
|
|
jsonl, out, note, ..
|
|
} => {
|
|
if let Some(out) = out {
|
|
println!("wrote keychain sigchain: {}", out.display());
|
|
} else {
|
|
print!("{jsonl}");
|
|
}
|
|
eprintln!("note: {note}");
|
|
}
|
|
ControlResponse::KeychainBundlePublished {
|
|
out,
|
|
base_url,
|
|
allowed_signers_path,
|
|
sigchain_path,
|
|
checkpoint_path,
|
|
checkpoint_signature_path,
|
|
snapshots,
|
|
note,
|
|
..
|
|
} => {
|
|
println!("bundle: {}", out.display());
|
|
println!("base_url: {base_url}");
|
|
println!("allowed_signers: {}", allowed_signers_path.display());
|
|
println!("sigchain: {}", sigchain_path.display());
|
|
println!("checkpoint: {}", checkpoint_path.display());
|
|
println!(
|
|
"checkpoint_signature: {}",
|
|
checkpoint_signature_path.display()
|
|
);
|
|
for snapshot in snapshots {
|
|
println!(
|
|
"snapshot: {} {} {}",
|
|
snapshot.name,
|
|
snapshot.path.display(),
|
|
snapshot.signature_path.display()
|
|
);
|
|
}
|
|
eprintln!("note: {note}");
|
|
}
|
|
ControlResponse::KeychainSigchainFileVerified {
|
|
input,
|
|
report,
|
|
note,
|
|
} => {
|
|
println!("sigchain: {}", input.display());
|
|
print_keychain_sigchain_report(&report);
|
|
eprintln!("note: {note}");
|
|
}
|
|
ControlResponse::KeychainSigchainImported {
|
|
input,
|
|
ops_imported,
|
|
signatures_imported,
|
|
invalid_ops_rejected,
|
|
note,
|
|
} => {
|
|
println!("sigchain: {}", input.display());
|
|
println!("ops_imported: {ops_imported}");
|
|
println!("signatures_imported: {signatures_imported}");
|
|
println!("invalid_ops_rejected: {invalid_ops_rejected}");
|
|
eprintln!("note: {note}");
|
|
}
|
|
ControlResponse::KeychainCheckpointVerified {
|
|
checkpoint,
|
|
verified,
|
|
principal,
|
|
note,
|
|
} => {
|
|
println!(
|
|
"checkpoint_head: {}",
|
|
checkpoint
|
|
.head
|
|
.as_ref()
|
|
.map(|h| h.as_str())
|
|
.unwrap_or("none")
|
|
);
|
|
println!("base_url: {}", checkpoint.base_url);
|
|
println!("verified: {verified}");
|
|
println!("principal: {}", principal.as_deref().unwrap_or("none"));
|
|
eprintln!("note: {note}");
|
|
}
|
|
ControlResponse::KeychainFetched {
|
|
url,
|
|
out,
|
|
checkpoint,
|
|
imported,
|
|
note,
|
|
} => {
|
|
println!("url: {url}");
|
|
println!("out: {}", out.display());
|
|
println!(
|
|
"checkpoint_head: {}",
|
|
checkpoint
|
|
.head
|
|
.as_ref()
|
|
.map(|h| h.as_str())
|
|
.unwrap_or("none")
|
|
);
|
|
if let Some(imported) = imported {
|
|
println!("ops_imported: {}", imported.ops_imported);
|
|
println!("signatures_imported: {}", imported.signatures_imported);
|
|
println!("invalid_ops_rejected: {}", imported.invalid_ops_rejected);
|
|
}
|
|
eprintln!("note: {note}");
|
|
}
|
|
ControlResponse::KeychainExplained { subject, lines } => {
|
|
println!("subject: {subject}");
|
|
for line in lines {
|
|
println!("{line}");
|
|
}
|
|
}
|
|
ControlResponse::KeychainVerified { report } => {
|
|
print_keychain_sigchain_report(&report);
|
|
}
|
|
ControlResponse::KeychainSynced {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
ops_imported,
|
|
signatures_imported,
|
|
invalid_ops_rejected,
|
|
high_water_ms,
|
|
note,
|
|
} => {
|
|
println!("synced keychain from: {peer_node_id}");
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("ops_imported: {ops_imported}");
|
|
println!("signatures_imported: {signatures_imported}");
|
|
println!("invalid_ops_rejected: {invalid_ops_rejected}");
|
|
println!("high_water_ms: {high_water_ms}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::AuthSynced {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
ops_imported,
|
|
signatures_imported,
|
|
invalid_ops_rejected,
|
|
high_water_ms,
|
|
note,
|
|
} => {
|
|
println!("synced auth from: {peer_node_id}");
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("ops_imported: {ops_imported}");
|
|
println!("signatures_imported: {signatures_imported}");
|
|
println!("invalid_ops_rejected: {invalid_ops_rejected}");
|
|
println!("high_water_ms: {high_water_ms}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::SyncStatus { peers, note } => {
|
|
if peers.is_empty() {
|
|
println!("no sync peers");
|
|
} else {
|
|
for peer in peers {
|
|
println!("peer: {}", peer.peer_node_id);
|
|
if peer.streams.is_empty() {
|
|
println!(" no sync attempts recorded");
|
|
}
|
|
for stream in peer.streams {
|
|
println!(
|
|
" {}\tstate={}\tstale={}\tcursor={}\tlast_attempt={}\tlast_success={}\timported={}\trejected={}\terror={}\tnext={}",
|
|
stream.stream,
|
|
stream.state,
|
|
stream.stale,
|
|
stream.cursor_ms,
|
|
stream
|
|
.last_attempt_ms
|
|
.map(|value| value.to_string())
|
|
.unwrap_or_else(|| "never".to_owned()),
|
|
stream
|
|
.last_success_ms
|
|
.map(|value| value.to_string())
|
|
.unwrap_or_else(|| "never".to_owned()),
|
|
stream.last_imported,
|
|
stream.last_rejected,
|
|
stream.last_error.unwrap_or_else(|| "-".to_owned()),
|
|
stream.next_action
|
|
);
|
|
}
|
|
}
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::SyncRan { peers, note } => {
|
|
if peers.is_empty() {
|
|
println!("no sync peers");
|
|
} else {
|
|
for peer in peers {
|
|
println!("peer: {}", peer.peer_node_id);
|
|
for stream in peer.streams {
|
|
let state = if !stream.attempted {
|
|
"skipped"
|
|
} else if stream.success {
|
|
"ok"
|
|
} else {
|
|
"failed"
|
|
};
|
|
println!(
|
|
" {}\t{}\tcursor={}\timported={}\trejected={}\terror={}",
|
|
stream.stream,
|
|
state,
|
|
stream.cursor_ms,
|
|
stream.imported,
|
|
stream.rejected,
|
|
stream.error.unwrap_or_else(|| "-".to_owned())
|
|
);
|
|
}
|
|
}
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::SecretStatus { secrets } => {
|
|
if secrets.is_empty() {
|
|
println!("no resource secrets");
|
|
} else {
|
|
for secret in secrets {
|
|
println!("{}\t{}\tepoch {}", secret.id, secret.resource, secret.epoch);
|
|
}
|
|
}
|
|
}
|
|
ControlResponse::SecretCreated { secret } => {
|
|
println!("resource secret: {}", secret.id);
|
|
println!("resource: {}", secret.resource);
|
|
println!("epoch: {}", secret.epoch);
|
|
}
|
|
ControlResponse::SecretBearerCreated { access } => {
|
|
println!("bearer id: {}", access.secret);
|
|
if let Some(token) = access.token {
|
|
println!("bearer token: {token}");
|
|
}
|
|
println!("resource: {}", access.resource);
|
|
println!(
|
|
"capabilities: {}",
|
|
access
|
|
.capabilities
|
|
.iter()
|
|
.map(ToString::to_string)
|
|
.collect::<Vec<_>>()
|
|
.join(",")
|
|
);
|
|
if let Some(expires_at) = access.expires_at {
|
|
println!("expires_at_ms: {}", expires_at.0);
|
|
}
|
|
println!("may_delegate: {}", access.may_delegate);
|
|
}
|
|
ControlResponse::SecretBearerList { access } => {
|
|
if access.is_empty() {
|
|
println!("no bearer access");
|
|
} else {
|
|
for item in access {
|
|
println!(
|
|
"{}\t{}\t{}\tmay_delegate={}",
|
|
item.secret,
|
|
item.resource,
|
|
item.capabilities
|
|
.iter()
|
|
.map(ToString::to_string)
|
|
.collect::<Vec<_>>()
|
|
.join(","),
|
|
item.may_delegate
|
|
);
|
|
}
|
|
}
|
|
}
|
|
ControlResponse::SecretBearerChallenge { challenge } => {
|
|
println!("bearer challenge");
|
|
println!("resource: {}", challenge.resource);
|
|
println!("nonce: {}", challenge.nonce);
|
|
println!("issued_at_ms: {}", challenge.issued_at.0);
|
|
println!(
|
|
"capabilities: {}",
|
|
challenge
|
|
.capabilities
|
|
.iter()
|
|
.map(ToString::to_string)
|
|
.collect::<Vec<_>>()
|
|
.join(",")
|
|
);
|
|
}
|
|
ControlResponse::SecretBearerProof { proof } => {
|
|
println!("bearer proof");
|
|
println!("secret: {}", proof.secret);
|
|
println!("resource: {}", proof.resource);
|
|
println!("nonce: {}", proof.nonce);
|
|
println!("response: {}", proof.response);
|
|
println!(
|
|
"capabilities: {}",
|
|
proof
|
|
.capabilities
|
|
.iter()
|
|
.map(ToString::to_string)
|
|
.collect::<Vec<_>>()
|
|
.join(",")
|
|
);
|
|
}
|
|
ControlResponse::SecretBearerVerified {
|
|
secret,
|
|
resource,
|
|
capabilities,
|
|
verified,
|
|
reason,
|
|
} => {
|
|
println!("bearer verified: {verified}");
|
|
println!("secret: {secret}");
|
|
println!("resource: {resource}");
|
|
println!("capabilities: {}", capabilities.join(","));
|
|
println!("reason: {reason}");
|
|
}
|
|
ControlResponse::SecretBearerRevoked { resource, secret } => {
|
|
println!("revoked bearer secret: {secret}");
|
|
println!("resource: {resource}");
|
|
}
|
|
ControlResponse::AuthExplain(explain) => {
|
|
println!("allowed: {}", explain.allowed);
|
|
println!("subject: {}", explain.subject);
|
|
println!("resource: {}", explain.resource);
|
|
println!("capability: {}", explain.capability);
|
|
println!("reason: {}", explain.reason);
|
|
println!("evaluated_ops: {}", explain.evaluated_ops);
|
|
if !explain.diagnostics.is_empty() {
|
|
println!("diagnostics: {}", explain.diagnostics.join(","));
|
|
}
|
|
}
|
|
ControlResponse::AuthOpRecorded { op, signatures } => {
|
|
println!("recorded auth op: {}", op.id);
|
|
println!("resource: {}", op.resource);
|
|
for signature in signatures {
|
|
println!(
|
|
"signed auth op: {} by {} ({})",
|
|
signature.op_id, signature.signer, signature.namespace
|
|
);
|
|
}
|
|
}
|
|
ControlResponse::NodeList { nodes, note } => {
|
|
if nodes.is_empty() {
|
|
println!("no enrolled nodes");
|
|
} else {
|
|
for node in nodes {
|
|
println!(
|
|
"{}\t{}\tdevice {}\t{} endpoints",
|
|
node.name,
|
|
node.id,
|
|
node.device,
|
|
node.endpoints.len()
|
|
);
|
|
}
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::NodeKeychainUpdated {
|
|
ops,
|
|
signatures,
|
|
note,
|
|
} => {
|
|
for op in ops {
|
|
println!("recorded keychain op: {}", op.id);
|
|
}
|
|
for signature in signatures {
|
|
println!(
|
|
"signed keychain op: {} by {} ({})",
|
|
signature.op_id, signature.signer, signature.namespace
|
|
);
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::NodeGrantUpdated {
|
|
op,
|
|
signatures,
|
|
note,
|
|
} => {
|
|
println!("recorded auth op: {}", op.id);
|
|
println!("resource: {}", op.resource);
|
|
for signature in signatures {
|
|
println!(
|
|
"signed auth op: {} by {} ({})",
|
|
signature.op_id, signature.signer, signature.namespace
|
|
);
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::NodeEnrollmentRequested { request, out, note } => {
|
|
println!("node enrollment request: {}", request.id);
|
|
println!("node: {}", request.requester_node);
|
|
println!("requested_name: {}", request.requested_node_name);
|
|
println!("status: {}", request.status);
|
|
if let Some(out) = out {
|
|
println!("written: {}", out.display());
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::NodeEnrollmentSubmitted {
|
|
request_id,
|
|
owner_node_id,
|
|
accepted,
|
|
note,
|
|
} => {
|
|
println!("submitted enrollment request: {request_id}");
|
|
println!("owner_node: {owner_node_id}");
|
|
println!("accepted: {accepted}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::NodeEnrollmentImported { request, note } => {
|
|
println!("imported enrollment request: {}", request.id);
|
|
println!("node: {}", request.requester_node);
|
|
println!("requested_name: {}", request.requested_node_name);
|
|
println!("status: {}", request.status);
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::NodeEnrollmentList { requests, note } => {
|
|
if requests.is_empty() {
|
|
println!("no node enrollment requests");
|
|
} else {
|
|
for request in requests {
|
|
println!(
|
|
"{}\t{}\t{}\t{} capabilities",
|
|
request.id,
|
|
request.status,
|
|
request.requested_node_name,
|
|
request.requested_capabilities.len()
|
|
);
|
|
}
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::NodeEnrollmentApproved {
|
|
request,
|
|
keychain_ops,
|
|
keychain_signatures,
|
|
auth_ops,
|
|
auth_signatures,
|
|
note,
|
|
} => {
|
|
println!("approved enrollment request: {}", request.id);
|
|
println!("node: {}", request.requester_node);
|
|
println!("keychain_ops: {}", keychain_ops.len());
|
|
println!("keychain_signatures: {}", keychain_signatures.len());
|
|
println!("auth_ops: {}", auth_ops.len());
|
|
println!("auth_signatures: {}", auth_signatures.len());
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::NodeEnrollmentSynced {
|
|
owner_node,
|
|
keychain_ops_imported,
|
|
keychain_signatures_imported,
|
|
auth_ops_imported,
|
|
auth_signatures_imported,
|
|
invalid_ops_rejected,
|
|
note,
|
|
} => {
|
|
println!("synced enrollment from: {owner_node}");
|
|
println!("keychain_ops_imported: {keychain_ops_imported}");
|
|
println!("keychain_signatures_imported: {keychain_signatures_imported}");
|
|
println!("auth_ops_imported: {auth_ops_imported}");
|
|
println!("auth_signatures_imported: {auth_signatures_imported}");
|
|
println!("invalid_ops_rejected: {invalid_ops_rejected}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::SshCertRequested { request } => {
|
|
println!("ssh cert request: {}", request.id);
|
|
println!("status: {}", request.status);
|
|
println!("kind: {}", request.cert_kind);
|
|
println!("principals: {}", request.principals.join(","));
|
|
println!("public_key_fingerprint: {}", request.public_key_fingerprint);
|
|
}
|
|
ControlResponse::SshCertRequests { requests } => {
|
|
if requests.is_empty() {
|
|
println!("no ssh certificate requests");
|
|
} else {
|
|
for request in requests {
|
|
println!(
|
|
"{}\t{}\t{}\t{}\t{}",
|
|
request.id,
|
|
request.status,
|
|
request.cert_kind,
|
|
request.principals.join(","),
|
|
request.public_key_fingerprint
|
|
);
|
|
}
|
|
}
|
|
}
|
|
ControlResponse::SshCertApproved { approval } => {
|
|
println!("approved ssh cert request: {}", approval.request_id);
|
|
println!("valid_for: {}", approval.valid_for);
|
|
if let Some(serial) = approval.serial {
|
|
println!("serial: {serial}");
|
|
}
|
|
if let Some(output_path) = approval.output_path {
|
|
println!("expected_certificate: {output_path}");
|
|
}
|
|
println!("signing_command:");
|
|
println!("{}", shell_quote_command(&approval.signing_command));
|
|
println!("signed: {}", approval.signed);
|
|
if let Some(certificate_id) = approval.certificate_id {
|
|
println!("certificate_id: {certificate_id}");
|
|
}
|
|
println!("note: {}", approval.note);
|
|
}
|
|
ControlResponse::SshCertImported { certificate } => {
|
|
println!("imported ssh certificate: {}", certificate.id);
|
|
println!("request: {}", certificate.request_id);
|
|
println!("fingerprint: {}", certificate.certificate_fingerprint);
|
|
}
|
|
ControlResponse::SshCertList {
|
|
requests,
|
|
certificates,
|
|
} => {
|
|
println!("requests:");
|
|
if requests.is_empty() {
|
|
println!(" none");
|
|
} else {
|
|
for request in requests {
|
|
println!(
|
|
" {}\t{}\t{}\t{}",
|
|
request.id,
|
|
request.status,
|
|
request.cert_kind,
|
|
request.principals.join(",")
|
|
);
|
|
}
|
|
}
|
|
println!("certificates:");
|
|
if certificates.is_empty() {
|
|
println!(" none");
|
|
} else {
|
|
for certificate in certificates {
|
|
println!(
|
|
" {}\t{}\t{}",
|
|
certificate.id, certificate.request_id, certificate.certificate_fingerprint
|
|
);
|
|
}
|
|
}
|
|
}
|
|
ControlResponse::SshCertSynced {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
requests_imported,
|
|
certificates_imported,
|
|
allowed,
|
|
reason,
|
|
note,
|
|
} => {
|
|
if allowed {
|
|
println!(
|
|
"synced ssh cert metadata from {peer_node_id}: {requests_imported} requests, {certificates_imported} certificates"
|
|
);
|
|
} else {
|
|
println!("ssh cert metadata sync denied by {peer_node_id}");
|
|
}
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("allowed: {allowed}");
|
|
println!("reason: {reason}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::SshRevocationAdded { revocation } => {
|
|
println!("added ssh revocation: {}", revocation.id);
|
|
println!("kind: {}", revocation.kind);
|
|
println!("target: {}", revocation.target);
|
|
if let Some(reason) = revocation.reason {
|
|
println!("reason: {reason}");
|
|
}
|
|
}
|
|
ControlResponse::SshRevocationList { revocations } => {
|
|
if revocations.is_empty() {
|
|
println!("no ssh revocations");
|
|
} else {
|
|
for revocation in revocations {
|
|
println!(
|
|
"{}\t{}\t{}\t{}",
|
|
revocation.id,
|
|
revocation.kind,
|
|
revocation.target,
|
|
revocation.reason.unwrap_or_default()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
ControlResponse::SshRevocationExported {
|
|
out,
|
|
format,
|
|
count,
|
|
note,
|
|
} => {
|
|
println!("exported {count} ssh revocations to {}", out.display());
|
|
println!("format: {format}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::SshRevocationImported {
|
|
revocations,
|
|
format,
|
|
count,
|
|
note,
|
|
} => {
|
|
println!("imported {count} ssh revocations");
|
|
println!("format: {format}");
|
|
for revocation in revocations {
|
|
println!(
|
|
"{}\t{}\t{}",
|
|
revocation.id, revocation.kind, revocation.target
|
|
);
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::SshRevocationSynced {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
revocations_imported,
|
|
allowed,
|
|
reason,
|
|
note,
|
|
} => {
|
|
if allowed {
|
|
println!("synced {revocations_imported} ssh revocations from {peer_node_id}");
|
|
} else {
|
|
println!("ssh revocation sync denied by {peer_node_id}");
|
|
}
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("allowed: {allowed}");
|
|
println!("reason: {reason}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::DbAdded { db } => {
|
|
println!("registered db: {}", db.name);
|
|
println!("id: {}", db.id);
|
|
println!("resource: {}", db.resource);
|
|
println!("path: {}", db.path);
|
|
println!("sync_status: {}", db.sync_status);
|
|
}
|
|
ControlResponse::DbStatus { db } => {
|
|
println!("db: {}", db.name);
|
|
println!("id: {}", db.id);
|
|
println!("resource: {}", db.resource);
|
|
println!("path: {}", db.path);
|
|
println!("path_exists: {}", db.path_exists);
|
|
println!(
|
|
"size_bytes: {}",
|
|
db.size_bytes
|
|
.map(|size| size.to_string())
|
|
.unwrap_or_else(|| "unknown".to_owned())
|
|
);
|
|
println!("schema_metadata: {}", db.schema_metadata);
|
|
println!(
|
|
"crsqlite_changes_available: {}",
|
|
db.crsqlite_changes.available
|
|
);
|
|
if let Some(count) = db.crsqlite_changes.change_count {
|
|
println!("crsqlite_change_count: {count}");
|
|
}
|
|
if let Some(version) = db.crsqlite_changes.max_db_version {
|
|
println!("crsqlite_max_db_version: {version}");
|
|
}
|
|
if let Some(error) = db.crsqlite_changes.error {
|
|
println!("crsqlite_changes_error: {error}");
|
|
}
|
|
println!("sync_status: {}", db.sync_status);
|
|
}
|
|
ControlResponse::DbChanges { db, batch } => {
|
|
println!("db: {}", db.name);
|
|
println!("changes: {}", batch.changes.len());
|
|
println!(
|
|
"max_db_version: {}",
|
|
batch
|
|
.max_db_version
|
|
.map(|version| version.to_string())
|
|
.unwrap_or_else(|| "none".to_owned())
|
|
);
|
|
println!("schema_metadata: {}", batch.schema_metadata);
|
|
for change in batch.changes {
|
|
println!(
|
|
"{}\t{}\t{}",
|
|
change.db_version, change.table_name, change.column_id
|
|
);
|
|
}
|
|
}
|
|
ControlResponse::DbSynced {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
name,
|
|
changes_received,
|
|
changes_applied,
|
|
max_db_version,
|
|
schema_match,
|
|
allowed,
|
|
reason,
|
|
note,
|
|
} => {
|
|
if allowed {
|
|
println!("synced db changes for {name} from {peer_node_id}");
|
|
} else {
|
|
println!("db sync denied by {peer_node_id}");
|
|
}
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("changes_received: {changes_received}");
|
|
println!("changes_applied: {changes_applied}");
|
|
println!(
|
|
"max_db_version: {}",
|
|
max_db_version
|
|
.map(|version| version.to_string())
|
|
.unwrap_or_else(|| "none".to_owned())
|
|
);
|
|
println!("schema_match: {schema_match}");
|
|
println!("allowed: {allowed}");
|
|
println!("reason: {reason}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::KvCreated { kv } => {
|
|
println!("created kv: {}", kv.name);
|
|
println!("id: {}", kv.id);
|
|
println!("resource: {}", kv.resource);
|
|
println!("sync_status: {}", kv.sync_status);
|
|
}
|
|
ControlResponse::KvSet { entry } => {
|
|
println!("set {} {}", entry.store, entry.key);
|
|
}
|
|
ControlResponse::KvGet { entry } => {
|
|
if let Some(entry) = entry {
|
|
println!("{}", entry.value);
|
|
} else {
|
|
println!("not found");
|
|
}
|
|
}
|
|
ControlResponse::KvSynced {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
name,
|
|
entries_imported,
|
|
allowed,
|
|
reason,
|
|
note,
|
|
} => {
|
|
if allowed {
|
|
println!("synced kv {name} from {peer_node_id}: {entries_imported} entries");
|
|
} else {
|
|
println!("kv sync denied for {name} by {peer_node_id}");
|
|
}
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("allowed: {allowed}");
|
|
println!("reason: {reason}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::DocumentCreated { document } => {
|
|
println!("created document: {}", document.name);
|
|
println!("id: {}", document.id);
|
|
println!("resource: {}", document.resource);
|
|
println!("sync_status: {}", document.sync_status);
|
|
println!("state_bytes: {}", document.state_bytes);
|
|
}
|
|
ControlResponse::DocumentStatus { document } => {
|
|
println!("document: {}", document.name);
|
|
println!("id: {}", document.id);
|
|
println!("resource: {}", document.resource);
|
|
println!("sync_status: {}", document.sync_status);
|
|
println!("state_bytes: {}", document.state_bytes);
|
|
}
|
|
ControlResponse::DocumentSet { state } => {
|
|
println!("updated document: {}", state.document.name);
|
|
println!("state_bytes: {}", state.document.state_bytes);
|
|
println!("updated_at_ms: {}", state.updated_at.0);
|
|
}
|
|
ControlResponse::DocumentGet { state } => {
|
|
println!("{}", state.state_json);
|
|
}
|
|
ControlResponse::DocumentSynced {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
name,
|
|
updated,
|
|
allowed,
|
|
reason,
|
|
note,
|
|
} => {
|
|
if allowed {
|
|
println!("synced document {name} from {peer_node_id}: updated={updated}");
|
|
} else {
|
|
println!("document sync denied for {name} by {peer_node_id}");
|
|
}
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("allowed: {allowed}");
|
|
println!("reason: {reason}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::PubsubPublished { message } => {
|
|
println!("published: {}", message.topic);
|
|
println!("published_at_ms: {}", message.published_at.0);
|
|
}
|
|
ControlResponse::PubsubRemotePublished {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
message,
|
|
allowed,
|
|
reason,
|
|
note,
|
|
} => {
|
|
if allowed {
|
|
println!("published: {}", message.topic);
|
|
println!("peer: {peer_node_id}");
|
|
println!("published_at_ms: {}", message.published_at.0);
|
|
} else {
|
|
println!("pubsub publish denied by {peer_node_id}");
|
|
}
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("allowed: {allowed}");
|
|
println!("reason: {reason}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::PubsubMessages {
|
|
topic,
|
|
messages,
|
|
note,
|
|
} => {
|
|
println!("topic: {topic}");
|
|
println!("messages: {}", messages.len());
|
|
for message in messages {
|
|
println!("{}\t{}", message.published_at.0, message.message);
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::PubsubRemoteMessages {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
topic,
|
|
messages,
|
|
allowed,
|
|
reason,
|
|
note,
|
|
} => {
|
|
if allowed {
|
|
println!("topic: {topic}");
|
|
println!("peer: {peer_node_id}");
|
|
println!("messages: {}", messages.len());
|
|
for message in messages {
|
|
println!("{}\t{}", message.published_at.0, message.message);
|
|
}
|
|
} else {
|
|
println!("pubsub subscribe denied by {peer_node_id}");
|
|
}
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("allowed: {allowed}");
|
|
println!("reason: {reason}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::PipeListening { listener } => {
|
|
println!("listening pipe: {}", listener.name);
|
|
println!("id: {}", listener.id);
|
|
println!("listened_at_ms: {}", listener.listened_at.0);
|
|
println!("note: {}", listener.note);
|
|
}
|
|
ControlResponse::PipeConnected { connection } => {
|
|
println!("pipe target: {}", connection.target);
|
|
println!("local_listener_found: {}", connection.local_listener_found);
|
|
println!("connected_at_ms: {}", connection.connected_at.0);
|
|
println!("note: {}", connection.note);
|
|
}
|
|
ControlResponse::PipeRemoteListening {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
listener,
|
|
allowed,
|
|
reason,
|
|
note,
|
|
} => {
|
|
if let Some(listener) = listener {
|
|
println!("listening pipe: {}", listener.name);
|
|
println!("peer: {peer_node_id}");
|
|
println!("id: {}", listener.id);
|
|
println!("listened_at_ms: {}", listener.listened_at.0);
|
|
} else {
|
|
println!("pipe listen denied by {peer_node_id}");
|
|
}
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("allowed: {allowed}");
|
|
println!("reason: {reason}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::PipeRemoteConnected {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
connection,
|
|
allowed,
|
|
reason,
|
|
note,
|
|
} => {
|
|
if allowed {
|
|
println!("pipe target: {}", connection.target);
|
|
println!("peer: {peer_node_id}");
|
|
println!("remote_listener_found: {}", connection.local_listener_found);
|
|
println!("connected_at_ms: {}", connection.connected_at.0);
|
|
} else {
|
|
println!("pipe connect denied by {peer_node_id}");
|
|
}
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("allowed: {allowed}");
|
|
println!("reason: {reason}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::PipeSent {
|
|
message,
|
|
listener_found,
|
|
note,
|
|
} => {
|
|
println!("listener_found: {listener_found}");
|
|
if let Some(message) = message {
|
|
println!("pipe: {}", message.pipe);
|
|
println!("received_at_ms: {}", message.received_at.0);
|
|
print_pipe_message_data(&message)?;
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::PipeRemoteSent {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
message,
|
|
listener_found,
|
|
allowed,
|
|
reason,
|
|
note,
|
|
} => {
|
|
println!("peer: {peer_node_id}");
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("allowed: {allowed}");
|
|
println!("listener_found: {listener_found}");
|
|
if let Some(message) = message {
|
|
println!("pipe: {}", message.pipe);
|
|
println!("received_at_ms: {}", message.received_at.0);
|
|
}
|
|
println!("reason: {reason}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::PipeMessages {
|
|
name,
|
|
messages,
|
|
drained,
|
|
note,
|
|
} => {
|
|
println!("pipe: {name}");
|
|
println!("messages: {}", messages.len());
|
|
println!("drained: {drained}");
|
|
for message in messages {
|
|
print_pipe_message_data(&message)?;
|
|
}
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::SshProxyConnected {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
connection,
|
|
allowed,
|
|
reason,
|
|
note,
|
|
} => {
|
|
if allowed {
|
|
println!("ssh proxy target: {peer_node_id}");
|
|
if let Some(connection) = connection {
|
|
println!("connected_at_ms: {}", connection.connected_at.0);
|
|
if let Some(local_sshd_target) = connection.local_sshd_target {
|
|
println!("remote_sshd_target: {local_sshd_target}");
|
|
}
|
|
println!(
|
|
"admin_shell_available: {}",
|
|
connection.admin_shell_available
|
|
);
|
|
println!("connection_note: {}", connection.note);
|
|
}
|
|
} else {
|
|
println!("ssh proxy denied by {peer_node_id}");
|
|
}
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("allowed: {allowed}");
|
|
println!("reason: {reason}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::SshAdminShellOutput {
|
|
peer_node_id,
|
|
peer_agent_id,
|
|
endpoint_id,
|
|
command,
|
|
output,
|
|
allowed,
|
|
reason,
|
|
note,
|
|
} => {
|
|
if allowed {
|
|
println!("{output}");
|
|
} else {
|
|
println!("ssh admin shell denied by {peer_node_id}");
|
|
}
|
|
println!("command: {command}");
|
|
println!("peer: {peer_node_id}");
|
|
println!("agent: {peer_agent_id}");
|
|
println!("endpoint: {endpoint_id}");
|
|
println!("allowed: {allowed}");
|
|
println!("reason: {reason}");
|
|
println!("note: {note}");
|
|
}
|
|
ControlResponse::NotImplemented { module, command } => {
|
|
println!("{module} {command}: not implemented yet");
|
|
}
|
|
ControlResponse::Error { message } => bail!(message),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn print_service_report(report: ServiceReport, json: bool) -> Result<()> {
|
|
if json {
|
|
println!(
|
|
"{}",
|
|
serde_json::json!({
|
|
"manager": report.manager.to_string(),
|
|
"action": format!("{:?}", report.action),
|
|
"service_name": report.service_name,
|
|
"definition_path": report.definition_path,
|
|
"definition": report.definition,
|
|
"commands": report.commands,
|
|
"note": report.note,
|
|
})
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
println!("service: {}", report.service_name);
|
|
println!("manager: {}", report.manager);
|
|
println!("action: {:?}", report.action);
|
|
if let Some(path) = report.definition_path {
|
|
println!("definition: {}", path.display());
|
|
}
|
|
if !report.commands.is_empty() {
|
|
println!("commands:");
|
|
for command in report.commands {
|
|
println!(" {}", shell_quote_command(&command));
|
|
}
|
|
}
|
|
if let Some(definition) = report.definition {
|
|
println!("definition_body:");
|
|
print!("{definition}");
|
|
}
|
|
println!("note: {}", report.note);
|
|
Ok(())
|
|
}
|
|
|
|
fn print_file_conflict(conflict: &geth_cas::FileConflict) {
|
|
println!("root: {}", conflict.root);
|
|
println!("resource: {}", conflict.resource);
|
|
println!("path: {}", conflict.path);
|
|
println!("kind: {}", conflict.kind.as_str());
|
|
println!("status: {}", conflict.status.as_str());
|
|
if let Some(hash) = &conflict.base_tree {
|
|
println!("base_tree: {hash}");
|
|
}
|
|
if let Some(hash) = &conflict.local_tree {
|
|
println!("local_tree: {hash}");
|
|
}
|
|
if let Some(hash) = &conflict.remote_tree {
|
|
println!("remote_tree: {hash}");
|
|
}
|
|
println!("detail: {}", conflict.detail);
|
|
if let Some(resolution) = &conflict.resolution {
|
|
println!("resolution: {}", resolution.as_str());
|
|
}
|
|
if let Some(note) = &conflict.resolution_note {
|
|
println!("resolution_note: {note}");
|
|
}
|
|
}
|
|
|
|
fn print_pipe_message_data(message: &geth_pipe::PipeMessage) -> Result<()> {
|
|
let bytes = base64::engine::general_purpose::STANDARD
|
|
.decode(&message.data_base64)
|
|
.context("decode pipe message")?;
|
|
match String::from_utf8(bytes) {
|
|
Ok(text) => println!("{text}"),
|
|
Err(error) => println!(
|
|
"base64:{}",
|
|
base64::engine::general_purpose::STANDARD.encode(error.into_bytes())
|
|
),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn pipe_send_payload_base64(message: Option<String>, input: Option<PathBuf>) -> Result<String> {
|
|
match (message, input) {
|
|
(Some(message), None) => Ok(base64::engine::general_purpose::STANDARD.encode(message)),
|
|
(None, Some(path)) if path.as_os_str() == "-" => {
|
|
let mut bytes = Vec::new();
|
|
std::io::stdin()
|
|
.read_to_end(&mut bytes)
|
|
.context("read pipe payload from stdin")?;
|
|
Ok(base64::engine::general_purpose::STANDARD.encode(bytes))
|
|
}
|
|
(None, Some(path)) => {
|
|
let bytes = std::fs::read(&path).with_context(|| format!("read {}", path.display()))?;
|
|
Ok(base64::engine::general_purpose::STANDARD.encode(bytes))
|
|
}
|
|
(Some(_), Some(_)) => bail!("pipe send accepts either MESSAGE or --in, not both"),
|
|
(None, None) => bail!("pipe send requires MESSAGE or --in <path>; use --in - for stdin"),
|
|
}
|
|
}
|
|
|
|
fn shell_quote_command(command: &[String]) -> String {
|
|
command
|
|
.iter()
|
|
.map(|arg| {
|
|
if arg
|
|
.bytes()
|
|
.all(|byte| byte.is_ascii_alphanumeric() || b"-_./:=+@,".contains(&byte))
|
|
{
|
|
arg.clone()
|
|
} else {
|
|
format!("'{}'", arg.replace('\'', "'\\''"))
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(" ")
|
|
}
|