use anyhow::bail;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::net::ToSocketAddrs;
use crate::entities::info::ShortName;
use crate::utils::compose_output;
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, Hash)]
pub struct RemoteHost {
pub short_name: ShortName,
pub ip: IpAddr,
pub port: u16,
pub username: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub ssh_private_key_file: Option<PathBuf>,
}
impl RemoteHost {
pub async fn check(&self) -> anyhow::Result<()> {
const PKG_NAME: &str = env!("CARGO_PKG_NAME");
const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
if self.ssh_private_key_file.is_none() {
bail!(
"Specify `ssh_private_key_file` field on `{}` remote host!",
self.short_name.as_str()
);
}
let shell = match std::env::var("DEPLOYER_SH_PATH") {
Ok(path) => path,
Err(_) => "/bin/bash".to_string(),
};
let mut session = Session::connect(
self.ssh_private_key_file.as_ref().unwrap(),
&self.username,
(self.ip, self.port),
)
.await?;
let out = session
.call(&format!(r#"{shell} -c "~/.cargo/bin/deployer -V""#))
.await?;
session.close().await?;
if out.is_empty() || !out.contains(PKG_NAME) {
bail!("There is no Deployer installed remotely.")
} else {
if !out.contains(&format!("{PKG_NAME} {PKG_VERSION}")) {
println!(
r#"Deployer versions aren't the same. Consider to update Deployer on your hosts. (out: "{}")"#,
out.trim()
);
}
Ok(())
}
}
pub fn call_deployer_to_build(&self, remote_build_dir: &Path, pipeline: &str) -> anyhow::Result<(bool, Vec<String>)> {
if self.ssh_private_key_file.is_none() {
bail!(
"Specify `ssh_private_key_file` field on `{}` remote host!",
self.short_name.as_str()
);
}
let shell = match std::env::var("DEPLOYER_SH_PATH") {
Ok(path) => path,
Err(_) => "/bin/bash".to_string(),
};
let rt = tokio::runtime::Runtime::new()?;
let cmd = format!(
r#"{} -c "DEPLOYER_REMOTE_AS_WORKER={} ~/.cargo/bin/deployer run {} && echo $?""#,
shell,
remote_build_dir
.to_str()
.expect("`remote_build_dir` contains non-UTF8 symbols!"),
pipeline,
);
rt.block_on(async {
let mut session = Session::connect(
self.ssh_private_key_file.as_ref().unwrap(),
&self.username,
(self.ip, self.port),
)
.await?;
let out = session.call(cmd.as_str()).await?;
session.close().await?;
let success = out.ends_with("\n0\n");
let mut out = compose_output(cmd, out, true);
out.pop();
Ok((success, out))
})
}
pub async fn open_session(&self) -> anyhow::Result<Session> {
if self.ssh_private_key_file.is_none() {
bail!(
"Specify `ssh_private_key_file` field on `{}` remote host!",
self.short_name.as_str()
);
}
Session::connect(
self.ssh_private_key_file.as_ref().unwrap(),
&self.username,
(self.ip, self.port),
)
.await
}
pub async fn close_session(session: &mut Session) -> anyhow::Result<()> {
session.close().await
}
pub async fn exec(&self, bash_c: &str, session: &mut Session) -> anyhow::Result<(bool, String)> {
let out = session.call(bash_c).await?;
let success = out.ends_with("\n0\n");
Ok((success, out))
}
}
struct Client {}
impl russh::client::Handler for Client {
type Error = anyhow::Error;
async fn check_server_key(&mut self, _: &russh::keys::ssh_key::PublicKey) -> Result<bool, Self::Error> {
Ok(true)
}
}
pub struct Session {
session: russh::client::Handle<Client>,
}
impl Session {
async fn connect<P: AsRef<Path>, A: ToSocketAddrs>(
key_path: P,
user: impl Into<String>,
addrs: A,
) -> anyhow::Result<Self> {
use russh::keys::{key::PrivateKeyWithHashAlg, load_secret_key};
let key_pair = load_secret_key(key_path, None)?;
let config = russh::client::Config {
preferred: russh::Preferred {
kex: Cow::Owned(vec![russh::kex::DH_G14_SHA256]),
..Default::default()
},
..<_>::default()
};
let config = Arc::new(config);
let sh = Client {};
let mut session = match russh::client::connect(config, addrs, sh).await {
Ok(s) => s,
Err(e) => bail!("Client connect failed: {e:?}"),
};
let auth_res = session
.authenticate_publickey(
user,
PrivateKeyWithHashAlg::new(Arc::new(key_pair), Some(russh::keys::HashAlg::Sha256)),
)
.await?;
if !auth_res.success() {
bail!("Authentication failed: {auth_res:?}");
}
Ok(Self { session })
}
async fn call(&mut self, command: &str) -> anyhow::Result<String> {
use russh::ChannelMsg;
let mut channel = self.session.channel_open_session().await?;
channel.exec(true, command).await?;
let mut out_buf = vec![];
while let Some(msg) = channel.wait().await {
if let ChannelMsg::Data { ref data } = msg {
out_buf.extend_from_slice(data);
}
}
Ok(String::from_utf8_lossy(&out_buf).to_string())
}
async fn close(&mut self) -> anyhow::Result<()> {
self
.session
.disconnect(russh::Disconnect::ByApplication, "deployer", "English")
.await?;
Ok(())
}
}