improve ssh-cli interaction surface
All checks were successful
Build Container / Build Container (push) Successful in 4m17s
All checks were successful
Build Container / Build Container (push) Successful in 4m17s
- better CLI parameter parsing - an actual help message - a new command to list currently known locks
This commit is contained in:
parent
fb19ca3840
commit
bc0c1b85ca
5 changed files with 320 additions and 215 deletions
140
ssh-cli/Cargo.lock
generated
140
ssh-cli/Cargo.lock
generated
|
|
@ -2,21 +2,6 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "addr2line"
|
|
||||||
version = "0.25.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
|
|
||||||
dependencies = [
|
|
||||||
"gimli",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "adler2"
|
|
||||||
version = "2.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
|
@ -46,21 +31,6 @@ dependencies = [
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "backtrace"
|
|
||||||
version = "0.3.76"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
|
|
||||||
dependencies = [
|
|
||||||
"addr2line",
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"miniz_oxide",
|
|
||||||
"object",
|
|
||||||
"rustc-demangle",
|
|
||||||
"windows-link",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
|
|
@ -129,33 +99,6 @@ dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "color-eyre"
|
|
||||||
version = "0.6.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d"
|
|
||||||
dependencies = [
|
|
||||||
"backtrace",
|
|
||||||
"color-spantrace",
|
|
||||||
"eyre",
|
|
||||||
"indenter",
|
|
||||||
"once_cell",
|
|
||||||
"owo-colors",
|
|
||||||
"tracing-error",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "color-spantrace"
|
|
||||||
version = "0.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427"
|
|
||||||
dependencies = [
|
|
||||||
"once_cell",
|
|
||||||
"owo-colors",
|
|
||||||
"tracing-core",
|
|
||||||
"tracing-error",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
|
|
@ -277,7 +220,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
|
checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -286,12 +228,6 @@ version = "0.3.33"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
|
checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-io"
|
|
||||||
version = "0.3.33"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.33"
|
version = "0.3.33"
|
||||||
|
|
@ -311,10 +247,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
|
checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
|
||||||
"futures-sink",
|
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"memchr",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
@ -346,12 +279,6 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gimli"
|
|
||||||
version = "0.32.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.4.15"
|
version = "0.4.15"
|
||||||
|
|
@ -720,15 +647,6 @@ version = "0.3.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "miniz_oxide"
|
|
||||||
version = "0.8.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
|
||||||
dependencies = [
|
|
||||||
"adler2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
|
|
@ -749,15 +667,6 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "object"
|
|
||||||
version = "0.37.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
|
|
@ -770,12 +679,6 @@ version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "owo-colors"
|
|
||||||
version = "4.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
|
|
@ -919,9 +822,7 @@ dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
|
||||||
"h2",
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
|
|
@ -966,12 +867,6 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustc-demangle"
|
|
||||||
version = "0.1.28"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.3"
|
version = "2.1.3"
|
||||||
|
|
@ -1189,6 +1084,16 @@ version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simple-eyre"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b561532e8ffe7ecf09108c4f662896a9ec3eac4999eba84015ec3dcb8cc630a"
|
||||||
|
dependencies = [
|
||||||
|
"eyre",
|
||||||
|
"indenter",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
|
|
@ -1215,10 +1120,11 @@ dependencies = [
|
||||||
name = "ssh-cli"
|
name = "ssh-cli"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"color-eyre",
|
|
||||||
"eyre",
|
"eyre",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
"simple-eyre",
|
||||||
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
@ -1352,9 +1258,21 @@ dependencies = [
|
||||||
"mio",
|
"mio",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-rustls"
|
name = "tokio-rustls"
|
||||||
version = "0.26.4"
|
version = "0.26.4"
|
||||||
|
|
@ -1455,16 +1373,6 @@ dependencies = [
|
||||||
"valuable",
|
"valuable",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tracing-error"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db"
|
|
||||||
dependencies = [
|
|
||||||
"tracing",
|
|
||||||
"tracing-subscriber",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-log"
|
name = "tracing-log"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,10 @@ version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
color-eyre = "0.6.5"
|
|
||||||
eyre = "0.6.12"
|
eyre = "0.6.12"
|
||||||
reqwest = { version = "0.13.4", features = ["blocking", "json"] }
|
reqwest = { version = "0.13.4", features = ["json"] }
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
simple-eyre = "0.3.1"
|
||||||
|
tokio = { version = "1.53.0", features = ["macros", "rt"] }
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
tracing-subscriber = "0.3.23"
|
tracing-subscriber = "0.3.23"
|
||||||
|
|
|
||||||
78
ssh-cli/src/api_client.rs
Normal file
78
ssh-cli/src/api_client.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
use eyre::Context;
|
||||||
|
use eyre::eyre;
|
||||||
|
use reqwest::Url;
|
||||||
|
use reqwest::header::HeaderValue;
|
||||||
|
|
||||||
|
use crate::api_models::ApiLock;
|
||||||
|
use crate::api_models::DesiredLockState;
|
||||||
|
use crate::api_models::LockOperationRequest;
|
||||||
|
|
||||||
|
/// Client for interacting with the dooris API
|
||||||
|
pub struct ApiClient {
|
||||||
|
client: reqwest::Client,
|
||||||
|
authorization: HeaderValue,
|
||||||
|
server_url: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiClient {
|
||||||
|
pub fn new_from_env() -> eyre::Result<Self> {
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.user_agent("dooris-ssh-cli")
|
||||||
|
.redirect(reqwest::redirect::Policy::none())
|
||||||
|
.http1_title_case_headers()
|
||||||
|
.build()
|
||||||
|
.expect("Could not construct http client even though all values are statically known to be valid");
|
||||||
|
|
||||||
|
let api_token = std::env::var("DOORIS_API_TOKEN").map_err(|_| eyre!("This ssh-cli is not configured correctly and does not have it's DOORIS_API_TOKEN environment variable set correctly. Contact your nearest server maid to fix the dooris setup."))?;
|
||||||
|
let api_token = format!("Static-Token {api_token}")
|
||||||
|
.try_into()
|
||||||
|
.context("Could not convert HTTP API Token to a valid HTTP header")?;
|
||||||
|
|
||||||
|
let server_url = std::env::var("DOORIS_SERVER_URL").map_err(|_| eyre!("This ssh-cli is not configured correctly and does not have it's DOORIS_SERVER_URL environment variable set correctly. Contact your nearest server maid to fix the dooris setup."))?;
|
||||||
|
let server_url = Url::parse(&server_url).context("Configured DOORIS_SERVER_URL is not a valid URL. Contact your nearest server maid to fix the dooris setup")?;
|
||||||
|
|
||||||
|
tracing::debug!("Client is connected to dooris at {server_url}");
|
||||||
|
Ok(Self {
|
||||||
|
client,
|
||||||
|
authorization: api_token,
|
||||||
|
server_url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch the current list of locks along with their state from the API
|
||||||
|
pub async fn fetch_locks(&self) -> eyre::Result<Vec<ApiLock>> {
|
||||||
|
tracing::debug!("Fetching locks from dooris API");
|
||||||
|
let url = self.server_url.join("api/locks/")?;
|
||||||
|
self.client
|
||||||
|
.get(url)
|
||||||
|
.header(reqwest::header::AUTHORIZATION, self.authorization.clone())
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()
|
||||||
|
.context("Could not get list of locks from the dooris API")?
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.context("Could not parse list-locks response from dooris API")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Put the lock identified by `lock_id` into the given desired state.
|
||||||
|
///
|
||||||
|
/// Returns as soon as the operation has been handed of to the backend and does not wait for physical lock movements.
|
||||||
|
pub async fn operate_lock(
|
||||||
|
&self,
|
||||||
|
lock_id: &str,
|
||||||
|
desired_state: DesiredLockState,
|
||||||
|
) -> eyre::Result<()> {
|
||||||
|
tracing::debug!("Calling API to put {lock_id:?} into state {desired_state:?}");
|
||||||
|
let url = self.server_url.join(&format!("api/locks/{lock_id}"))?;
|
||||||
|
self.client
|
||||||
|
.patch(url)
|
||||||
|
.header(reqwest::header::AUTHORIZATION, self.authorization.clone())
|
||||||
|
.json(&LockOperationRequest { desired_state })
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()
|
||||||
|
.context("Could not operate lock")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
104
ssh-cli/src/cli.rs
Normal file
104
ssh-cli/src/cli.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
use eyre::{OptionExt, eyre};
|
||||||
|
use std::env::VarError;
|
||||||
|
|
||||||
|
/// CLI parameters given to this program
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub enum CliArgs {
|
||||||
|
/// The user requested a help message
|
||||||
|
Help,
|
||||||
|
/// The user requested to lock a specific lock
|
||||||
|
Lock {
|
||||||
|
/// The name of the lock that should be locked
|
||||||
|
lock_name: String,
|
||||||
|
/// Whether the CLI should wait until the lock operation is not just started but until it is completely finished
|
||||||
|
wait_until_done: bool,
|
||||||
|
},
|
||||||
|
/// The user requested to unlock a specific lock
|
||||||
|
Unlock {
|
||||||
|
/// The name of the lock that should be unlocked
|
||||||
|
lock_name: String,
|
||||||
|
/// Whether the CLI should wait until the unlock operation is not just started but until it is completely finished
|
||||||
|
wait_until_done: bool,
|
||||||
|
},
|
||||||
|
/// The user wants to see a list of currently connected locks as well as their status
|
||||||
|
ShowLocks,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get passed arguments from SSH_ORIGINAL_COMMAND environment variable but falling back to direct CLI args
|
||||||
|
fn get_arg_string() -> eyre::Result<String> {
|
||||||
|
match std::env::var("SSH_ORIGINAL_COMMAND") {
|
||||||
|
Ok(original_command) => {
|
||||||
|
tracing::trace!(
|
||||||
|
SSH_ORIGINAL_COMMAND = ?original_command,
|
||||||
|
"Taking CLI arguments from SSH_ORIGINAL_COMMAND environment variable"
|
||||||
|
);
|
||||||
|
Ok(original_command)
|
||||||
|
}
|
||||||
|
Err(VarError::NotUnicode(_)) => {
|
||||||
|
Err(eyre!("Your command is not very unicoded. Refusing >~<"))
|
||||||
|
}
|
||||||
|
Err(VarError::NotPresent) => {
|
||||||
|
let args = std::env::args().skip(1).collect::<Vec<_>>().join(" ");
|
||||||
|
tracing::trace!(
|
||||||
|
argv = ?args,
|
||||||
|
"Taking CLI arguments from argv"
|
||||||
|
);
|
||||||
|
Ok(args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CliArgs {
|
||||||
|
pub fn parse() -> eyre::Result<Self> {
|
||||||
|
let raw_args = get_arg_string()?;
|
||||||
|
let raw_args_list = raw_args.split_whitespace().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if raw_args_list.len() == 0
|
||||||
|
|| raw_args_list[0] == "help"
|
||||||
|
|| raw_args_list[0] == "-h"
|
||||||
|
|| raw_args_list[0] == "--help"
|
||||||
|
{
|
||||||
|
Ok(CliArgs::Help)
|
||||||
|
} else if raw_args_list[0] == "unlock" {
|
||||||
|
let lock_name = raw_args_list.get(1).ok_or_eyre(
|
||||||
|
"Unlock cli needs a lock name as parameter. See \"help\" for usage information.",
|
||||||
|
)?;
|
||||||
|
let wait_until_done = match *raw_args_list.get(2).unwrap_or(&"wait") {
|
||||||
|
"wait" => true,
|
||||||
|
"nowait" => false,
|
||||||
|
_ => {
|
||||||
|
return Err(eyre!(
|
||||||
|
"Third unlock parameter needs to be either \"wait\" or \"nowait\". See \"help\" for usage information."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(CliArgs::Unlock {
|
||||||
|
lock_name: lock_name.to_string(),
|
||||||
|
wait_until_done,
|
||||||
|
})
|
||||||
|
} else if raw_args_list[0] == "lock" {
|
||||||
|
let lock_name = raw_args_list.get(1).ok_or_eyre(
|
||||||
|
"Lock cli needs a lock name as parameter. See \"help\" for usage information.",
|
||||||
|
)?;
|
||||||
|
let wait_until_done = match *raw_args_list.get(2).unwrap_or(&"wait") {
|
||||||
|
"wait" => true,
|
||||||
|
"nowait" => false,
|
||||||
|
_ => {
|
||||||
|
return Err(eyre!(
|
||||||
|
"Third lock parameter needs to be either \"wait\" or \"nowait\". See \"help\" for usage information."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(CliArgs::Lock {
|
||||||
|
lock_name: lock_name.to_string(),
|
||||||
|
wait_until_done,
|
||||||
|
})
|
||||||
|
} else if raw_args_list[0] == "show-locks" {
|
||||||
|
Ok(CliArgs::ShowLocks)
|
||||||
|
} else {
|
||||||
|
Err(eyre!(
|
||||||
|
"Unknown commandline {raw_args:?}. See \"help\" for usage information."
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,114 +9,128 @@
|
||||||
//! SetEnv DOORIS_SERVER_URL=https://dooris.ccchh.net/ DOORIS_API_TOKEN=foobar123
|
//! SetEnv DOORIS_SERVER_URL=https://dooris.ccchh.net/ DOORIS_API_TOKEN=foobar123
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
mod api_client;
|
||||||
mod api_models;
|
mod api_models;
|
||||||
|
mod cli;
|
||||||
|
|
||||||
use eyre::{Context, OptionExt, eyre};
|
use std::process::exit;
|
||||||
use reqwest::{Url, header::HeaderValue};
|
|
||||||
use std::{env::VarError, process::exit};
|
|
||||||
|
|
||||||
use crate::api_models::LockOperationRequest;
|
use eyre::eyre;
|
||||||
|
|
||||||
fn main() -> eyre::Result<()> {
|
use crate::{
|
||||||
|
api_client::ApiClient,
|
||||||
|
api_models::{ApiLock, DesiredLockState},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() -> eyre::Result<()> {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
color_eyre::install()?;
|
simple_eyre::install()?;
|
||||||
|
|
||||||
let doors = fetch_doors()?;
|
let args = cli::CliArgs::parse()?;
|
||||||
let args = get_args()?;
|
tracing::debug!(cli = ?args, "Parsed CLI parameters");
|
||||||
let args = args.split(" ").collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if args.len() == 0 || args[0] == "--help" || args[0] == "-h" || args[0] == "" {
|
match args {
|
||||||
println!("Help is not yet available. Come back later >~<");
|
cli::CliArgs::Help => {
|
||||||
exit(3);
|
println!(
|
||||||
} else if args[0] == "lock" || args[0] == "unlock" {
|
"dooris-over-ssh: Your personal door assistant with an extra serving of chaos"
|
||||||
let cli_target_door = args
|
);
|
||||||
.get(1)
|
println!(
|
||||||
.ok_or_eyre("'unlock' and 'lock' cli requires the door name as second argument")?;
|
"Usage: ssh dooris@dooris.ccchh.net help | lock <door> [wait|nowait] | unlock <door> [wait|nowait]"
|
||||||
|
);
|
||||||
let api_target_door = doors
|
println!();
|
||||||
.iter()
|
println!("Commands:");
|
||||||
.find(|i_door| i_door.name.eq_ignore_ascii_case(cli_target_door))
|
println!();
|
||||||
.ok_or({
|
println!(" help -> The help command prints this message. Hello 👋");
|
||||||
let door_names = doors
|
println!("");
|
||||||
.iter()
|
println!(" lock <door> [wait|nowait] -> Locks the door identified with <door>.");
|
||||||
.map(|i_door| i_door.name.to_owned())
|
println!(
|
||||||
.collect::<Vec<_>>();
|
" Also takes an additional parameter of \"wait\" or \"nowait\"which dictates"
|
||||||
eyre!(
|
);
|
||||||
"No door named {cli_target_door:?} exists. Available doors are {door_names:?}.",
|
println!(" whether the CLI exit as soon as the lock operation is queued or whether");
|
||||||
)
|
println!(" it waits until the lock status changes to open.");
|
||||||
})?;
|
println!(" Defaults to \"wait\" if not specified.");
|
||||||
|
println!();
|
||||||
println!(
|
println!(" unlock <door> [wait|nowait] -> Unlocks the door identified with <door>.");
|
||||||
"Lock {} ({:?}) identified!",
|
println!(
|
||||||
api_target_door.id, api_target_door.name
|
" Also takes an additional parameter of \"wait\" or \"nowait\"which dictates"
|
||||||
);
|
);
|
||||||
operate_lock(
|
println!(" whether the CLI exit as soon as the lock operation is queued or whether");
|
||||||
&api_target_door.id,
|
println!(" it waits until the lock status changes to closed.");
|
||||||
match args[0] {
|
println!(" Defaults to \"wait\" if not specified.");
|
||||||
"lock" => api_models::DesiredLockState::CLOSED,
|
exit(0);
|
||||||
"unlock" => api_models::DesiredLockState::OPEN,
|
|
||||||
_ => unreachable!(),
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
println!("Lock has been turned. Enjoy :3");
|
|
||||||
return Ok(());
|
|
||||||
} else {
|
|
||||||
println!("Unknown commandline. See --help for more info.");
|
|
||||||
exit(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get passed arguments from SSH_ORIGINAL_COMMAND environment variable but falling back to direct CLI args
|
|
||||||
fn get_args() -> eyre::Result<String> {
|
|
||||||
match std::env::var("SSH_ORIGINAL_COMMAND") {
|
|
||||||
Ok(original_command) => Ok(original_command),
|
|
||||||
Err(VarError::NotUnicode(_)) => {
|
|
||||||
Err(eyre!("Your command is not very unicoded. Refusing >~<"))
|
|
||||||
}
|
}
|
||||||
Err(VarError::NotPresent) => Ok(std::env::args().skip(1).collect::<Vec<_>>().join(" ")),
|
cli::CliArgs::Lock {
|
||||||
}
|
lock_name,
|
||||||
|
wait_until_done,
|
||||||
|
} => {
|
||||||
|
let api = ApiClient::new_from_env()?;
|
||||||
|
let lock = find_lock(&api, &lock_name).await?;
|
||||||
|
|
||||||
|
println!("dooris at your service, locking {} now…", lock.name);
|
||||||
|
api.operate_lock(&lock.id, DesiredLockState::CLOSED).await?;
|
||||||
|
|
||||||
|
if wait_until_done {
|
||||||
|
tracing::warn!(
|
||||||
|
"wait feature is not yet implemented. lock should be turning now >~<"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
cli::CliArgs::Unlock {
|
||||||
|
lock_name,
|
||||||
|
wait_until_done,
|
||||||
|
} => {
|
||||||
|
let api = ApiClient::new_from_env()?;
|
||||||
|
let lock = find_lock(&api, &lock_name).await?;
|
||||||
|
|
||||||
|
println!("dooris at your service, locking {} now…", lock.name);
|
||||||
|
api.operate_lock(&lock.id, DesiredLockState::OPEN).await?;
|
||||||
|
|
||||||
|
if wait_until_done {
|
||||||
|
tracing::warn!(
|
||||||
|
"wait feature is not yet implemented. lock should be turning now >~<"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
cli::CliArgs::ShowLocks => {
|
||||||
|
let api = ApiClient::new_from_env()?;
|
||||||
|
let locks = api.fetch_locks().await?;
|
||||||
|
|
||||||
|
for i_lock in locks.iter() {
|
||||||
|
println!(">> {}", i_lock.name);
|
||||||
|
if i_lock.status.is_unreachable {
|
||||||
|
println!(" !!! Lock is currently unreachable !!!");
|
||||||
|
}
|
||||||
|
if i_lock.status.is_low_batteray {
|
||||||
|
println!(" !!! Lock has low battery !!!");
|
||||||
|
}
|
||||||
|
if i_lock.status.is_error_jammed {
|
||||||
|
println!(" !!! Lock is jammed !!!");
|
||||||
|
}
|
||||||
|
println!(" State: {}", i_lock.status.lock_state);
|
||||||
|
println!(" Desired State: {}", i_lock.status.lock_target_level);
|
||||||
|
println!(" Acitivty: {}", i_lock.status.activity_state);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn http_client() -> eyre::Result<reqwest::blocking::Client> {
|
async fn find_lock(api: &ApiClient, name: &str) -> eyre::Result<ApiLock> {
|
||||||
return reqwest::blocking::ClientBuilder::new()
|
let locks = api.fetch_locks().await?;
|
||||||
.user_agent("dooris-ssh-cli")
|
tracing::debug!("Comparing locks from API for one with name {name:?}");
|
||||||
.redirect(reqwest::redirect::Policy::none())
|
let lock = locks
|
||||||
.http1_title_case_headers()
|
.into_iter()
|
||||||
.build()
|
.find(|i_lock| i_lock.name.eq_ignore_ascii_case(&name))
|
||||||
.context("Could not construct http client even though all values are statically known to be valid");
|
.ok_or_else(|| {
|
||||||
}
|
eyre!("No lock named {name:?} exists. See \"list-locks\" for a list of valid locks.")
|
||||||
|
})?;
|
||||||
fn api_token() -> eyre::Result<HeaderValue> {
|
tracing::debug!(lock = ?lock,"Identified lock with name matching {name:?}");
|
||||||
let server_token = std::env::var("DOORIS_API_TOKEN").map_err(|_| eyre!("This ssh-cli is not configured correctly and does not have it's DOORIS_API_TOKEN environment variable set correctly. Contact your nearest server maid to fix the dooris setup."))?;
|
Ok(lock)
|
||||||
format!("Static-Token {server_token}")
|
|
||||||
.try_into()
|
|
||||||
.context("Could not convert HTTP API Token to a valid HTTP header")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn server_url() -> eyre::Result<Url> {
|
|
||||||
let server_url = std::env::var("DOORIS_SERVER_URL").map_err(|_| eyre!("This ssh-cli is not configured correctly and does not have it's DOORIS_SERVER_URL environment variable set correctly. Contact your nearest server maid to fix the dooris setup."))?;
|
|
||||||
Url::parse(&server_url).context("Configured DOORIS_SERVER_URL is not a valid URL. Contact your nearest server maid to fix the dooris setup")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fetch_doors() -> eyre::Result<Vec<api_models::ApiLock>> {
|
|
||||||
http_client()?
|
|
||||||
.get(server_url()?.join("api/locks/")?)
|
|
||||||
.header(reqwest::header::AUTHORIZATION, api_token()?)
|
|
||||||
.send()?
|
|
||||||
.error_for_status()
|
|
||||||
.context("Could not get list of locks from the dooris API")?
|
|
||||||
.json()
|
|
||||||
.context("Could not parse list-locks response from dooris API")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn operate_lock(lock_id: &str, desired_state: api_models::DesiredLockState) -> eyre::Result<()> {
|
|
||||||
println!("Turning Lock, please stand by…");
|
|
||||||
http_client()?
|
|
||||||
.patch(server_url()?.join(&format!("api/locks/{lock_id}"))?)
|
|
||||||
.header(reqwest::header::AUTHORIZATION, api_token()?)
|
|
||||||
.json(&LockOperationRequest { desired_state })
|
|
||||||
.send()?
|
|
||||||
.error_for_status()
|
|
||||||
.context("Could not operate lock")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue