Files
pkh/src/context/ssh.rs
Valentin Haudiquet 1d65d1ce31
All checks were successful
CI / build (push) Successful in 1m50s
context: add context
pkh context allows to manage contexts (local, ssh) and run contextualized commands (deb)

in other words, this allows building binary packages over ssh
2025-12-15 20:48:44 +01:00

200 lines
6.0 KiB
Rust

use super::api::{CommandRunner, ContextDriver};
use log::debug;
use std::fs;
/// SSH context: execute commands over an SSH connection
/// Context driver: Copies over SFTP with ssh2
/// Command runner: Executes commands over an ssh2 channel (with PTY)
use std::io::{self, Read};
use std::net::TcpStream;
#[cfg(unix)]
use std::os::unix::process::ExitStatusExt;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
pub fn connect_ssh(host: &str, user: Option<&str>, port: Option<u16>) -> io::Result<ssh2::Session> {
let port = port.unwrap_or(22);
let tcp = TcpStream::connect((host, port))?;
let mut session = ssh2::Session::new().unwrap();
session.set_tcp_stream(tcp);
session
.handshake()
.map_err(|e| io::Error::new(io::ErrorKind::ConnectionRefused, e))?;
// Username: if set, use it; else, use local 'USER', and if unset, use 'root'
let local_user_string;
let user = match user {
Some(u) => u,
None => {
local_user_string = std::env::var("USER").unwrap_or_else(|_| "root".to_string());
&local_user_string
}
};
if session.userauth_agent(user).is_ok() {
return Ok(session);
}
if !session.authenticated() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"SSH authentication failed (tried agent)",
));
}
Ok(session)
}
pub struct SshDriver {
pub host: String,
pub user: Option<String>,
pub port: Option<u16>,
}
impl ContextDriver for SshDriver {
fn ensure_available(&self, src: &Path, dest_root: &str) -> io::Result<PathBuf> {
let sess = connect_ssh(&self.host, self.user.as_deref(), self.port)?;
let sftp = sess.sftp().map_err(io::Error::other)?;
let file_name = src
.file_name()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "src has no filename"))?;
let remote_dest = Path::new(dest_root).join(file_name);
debug!("Uploading {:?} to remote {:?}", src, remote_dest);
Self::upload_recursive(&sftp, src, &remote_dest)?;
Ok(remote_dest)
}
fn create_runner(&self, program: String) -> Box<dyn CommandRunner> {
Box::new(SshRunner {
host: self.host.clone(),
user: self.user.clone(),
port: self.port,
program,
args: Vec::new(),
})
}
fn prepare_work_dir(&self) -> io::Result<String> {
let sess = connect_ssh(&self.host, self.user.as_deref(), self.port)?;
let mut channel = sess.channel_session().map_err(io::Error::other)?;
channel.exec("mktemp -d").map_err(io::Error::other)?;
let mut stdout = String::new();
channel.read_to_string(&mut stdout)?;
channel.wait_close().map_err(io::Error::other)?;
if channel.exit_status().unwrap_or(-1) != 0 {
return Err(io::Error::other(
"Failed to create remote temporary directory",
));
}
Ok(stdout.trim().to_string())
}
}
impl SshDriver {
fn upload_recursive(sftp: &ssh2::Sftp, src: &Path, dest: &Path) -> io::Result<()> {
if src.is_dir() {
// Create dir
let _ = sftp.mkdir(dest, 0o755);
for entry in fs::read_dir(src)? {
let entry = entry?;
let path = entry.path();
let name = entry.file_name();
let dest_path = dest.join(name);
Self::upload_recursive(sftp, &path, &dest_path)?;
}
} else {
let mut file = fs::File::open(src)?;
let mut remote_file = sftp.create(dest).map_err(|e| {
io::Error::other(format!("Failed to create remote file {:?}: {}", dest, e))
})?;
io::copy(&mut file, &mut remote_file)?;
}
Ok(())
}
}
pub struct SshRunner {
pub host: String,
pub user: Option<String>,
pub port: Option<u16>,
pub program: String,
pub args: Vec<String>,
}
impl SshRunner {
fn prepare_channel(&self) -> io::Result<(ssh2::Channel, String)> {
let sess = connect_ssh(&self.host, self.user.as_deref(), self.port)?;
let channel = sess.channel_session().map_err(io::Error::other)?;
// Construct command line
let mut cmd_line = self.program.clone();
for arg in &self.args {
cmd_line.push(' ');
cmd_line.push_str(arg); // TODO: escape
}
Ok((channel, cmd_line))
}
}
impl CommandRunner for SshRunner {
fn add_arg(&mut self, arg: String) {
self.args.push(arg);
}
fn status(&mut self) -> io::Result<ExitStatus> {
let (mut channel, cmd_line) = self.prepare_channel()?;
debug!("Executing SSH command: {}", cmd_line);
channel
.request_pty("xterm", None, None)
.map_err(|e| io::Error::other(format!("Failed to request PTY: {}", e)))?;
channel.exec(&cmd_line).map_err(io::Error::other)?;
let mut stdout_stream = channel.stream(0);
let mut stdout = io::stdout();
io::copy(&mut stdout_stream, &mut stdout)?;
channel.wait_close().map_err(io::Error::other)?;
let code = channel.exit_status().unwrap_or(-1);
Ok(ExitStatus::from_raw(code))
}
fn output(&mut self) -> io::Result<std::process::Output> {
let (mut channel, cmd_line) = self.prepare_channel()?;
channel.exec(&cmd_line).map_err(io::Error::other)?;
let mut stdout = Vec::new();
channel.read_to_end(&mut stdout)?;
let mut stderr = Vec::new();
channel.stderr().read_to_end(&mut stderr)?;
channel.wait_close().map_err(io::Error::other)?;
#[cfg(unix)]
let status = std::process::ExitStatus::from_raw(channel.exit_status().unwrap_or(-1));
#[cfg(not(unix))]
let status = {
panic!("SSH output capture only supported on Unix");
};
Ok(std::process::Output {
status,
stdout,
stderr,
})
}
}