working server spawn

This commit is contained in:
Robin Appelman 2021-03-28 15:39:21 +02:00
commit f3b9d14678
4 changed files with 143 additions and 108 deletions

View file

@ -1,14 +1,12 @@
mod ssh;
pub mod vultr;
use std::net::IpAddr;
use crate::cloud::ssh::SshError;
use crate::config::ServerConfig;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use reqwest::StatusCode;
use std::net::IpAddr;
use thiserror::Error;
pub mod vultr;
#[derive(Debug, Error)]
pub enum CloudError {
#[error("Invalid credentials")]
@ -21,8 +19,6 @@ pub enum CloudError {
InvalidResponse(#[from] ResponseError),
#[error("Server boot timed out")]
StartTimeout,
#[error("Error while trying to connect trough ssh: {0}")]
Ssh(#[from] SshError),
}
/// Intentionally opaque error
@ -72,8 +68,6 @@ pub trait Cloud {
async fn kill(&self, id: &str) -> Result<()>;
/// Wait until the server has an ip
async fn wait_for_ip(&self, id: &str) -> Result<Server>;
/// Setup the tf2 server on the instance
async fn setup(&self, id: &str, password: &str, config: &ServerConfig) -> Result<Server>;
}
#[derive(Debug)]

View file

@ -1,6 +1,4 @@
use crate::cloud::ssh::{SshError, SshSession};
use crate::cloud::{Cloud, CloudError, Created, NetworkError, ResponseError, Result, Server};
use crate::config::ServerConfig;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use petname::petname;
@ -8,7 +6,7 @@ use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use std::time::Duration;
use tokio::time::{sleep, timeout};
use tokio::time::sleep;
pub struct Vultr {
region: String,
@ -91,65 +89,6 @@ impl Cloud for Vultr {
};
Ok(instance.into())
}
async fn setup(
&self,
id: &str,
password: &str,
config: &ServerConfig,
) -> Result<Server, CloudError> {
let server = self.wait_for_ip(id).await?;
let ip = server.ip;
let mut ssh = timeout(Duration::from_secs(5 * 60), async move {
loop {
sleep(Duration::from_secs(1)).await;
match SshSession::open(ip, password).await {
Ok(ssh) => return Ok(ssh),
Err(SshError::ConnectionTimeout) => {}
Err(e) => return Err(e),
}
}
})
.await
.map_err(|_| CloudError::StartTimeout)??;
println!("connected");
ssh.exec("docker pull spiretf/docker-spire-server").await?;
println!("pulled");
ssh.exec(format!(
"docker run --name spire -d \
-e NAME={name} -e TV_NAME={tv_name} -e PASSWORD={password} -e RCON_PASSWORD={rcon} \
-e DEMOSTF_APIKEY={demostf} -e LOGSTF_APIKEY={logstf} \
-e CONFIG_LEAGUE={league} -e CONFIG_MODE={mode} \
-p 27015:27015 -p 27021:27021 -p 27015:27015/udp -p 27020:27020/udp -p 27025:27025 \
-p 28015:27015 -p 28015:27015/udp -p 27115:27015 -p 27115:27015/udp -p 27215:27015 \
-p 27215:27015/udp -p 27315:27015 -p 27315:27015/udp -p 27415:27015 -p 27415:27015/udp \
-p 27515:27015 -p 27515:27015/udp -p 27615:27015 -p 27615:27015/udp -p 27715:27015 \
-p 27715:27015/udp -p 27815:27015 -p 27815:27015/udp -p 27915:27015 -p 27915:27015/udp \
{image}
",
name = config.name,
tv_name = config.tv_name,
password = config.password,
rcon = config.rcon,
demostf = config
.demostf_key
.as_ref()
.map(String::as_str)
.unwrap_or_default(),
logstf = config
.logstf_key
.as_ref()
.map(String::as_str)
.unwrap_or_default(),
league = config.config_league,
mode = config.config_mode,
image = config.image
))
.await?;
Ok(server)
}
}
impl Vultr {

View file

@ -1,10 +1,18 @@
pub mod cloud;
pub mod config;
use std::env::args;
use thiserror::Error;
use ssh::SshSession;
use crate::cloud::CloudError;
use crate::config::{Config, ConfigError};
use std::env::args;
use thiserror::Error;
use crate::config::{Config, ConfigError, ServerConfig};
use crate::ssh::SshError;
use std::time::Duration;
use tokio::time::sleep;
pub mod cloud;
pub mod config;
pub mod ssh;
#[derive(Debug, Error)]
pub enum Error {
@ -12,6 +20,61 @@ pub enum Error {
Cloud(#[from] CloudError),
#[error("Error while loading configuration: {0}")]
Config(#[from] ConfigError),
#[error("Error while trying to connect trough ssh: {0}")]
Ssh(#[from] SshError),
#[error("Setup command returned an error: {0}")]
SetupError(String),
}
async fn setup(ssh: &mut SshSession, config: &ServerConfig) -> Result<(), Error> {
let mut tries = 0;
loop {
tries += 1;
sleep(Duration::from_secs(1)).await;
let result = ssh.exec("docker pull spiretf/docker-spire-server").await?;
if result.success() {
break;
} else if tries > 5 {
return Err(Error::SetupError(result.output()));
}
}
let result = ssh
.exec(format!(
"docker run --name spire -d \
-e NAME={name} -e TV_NAME={tv_name} -e PASSWORD={password} -e RCON_PASSWORD={rcon} \
-e DEMOSTF_APIKEY={demostf} -e LOGSTF_APIKEY={logstf} \
-e CONFIG_LEAGUE={league} -e CONFIG_MODE={mode} \
-p 27015:27015 -p 27021:27021 -p 27015:27015/udp -p 27020:27020/udp -p 27025:27025 \
-p 28015:27015 -p 28015:27015/udp -p 27115:27015 -p 27115:27015/udp -p 27215:27015 \
-p 27215:27015/udp -p 27315:27015 -p 27315:27015/udp -p 27415:27015 -p 27415:27015/udp \
-p 27515:27015 -p 27515:27015/udp -p 27615:27015 -p 27615:27015/udp -p 27715:27015 \
-p 27715:27015/udp -p 27815:27015 -p 27815:27015/udp -p 27915:27015 -p 27915:27015/udp \
{image}
",
name = config.name,
tv_name = config.tv_name,
password = config.password,
rcon = config.rcon,
demostf = config
.demostf_key
.as_ref()
.map(String::as_str)
.unwrap_or_default(),
logstf = config
.logstf_key
.as_ref()
.map(String::as_str)
.unwrap_or_default(),
league = config.config_league,
mode = config.config_mode,
image = config.image
))
.await?;
if !result.success() {
return Err(Error::SetupError(result.output()));
}
Ok(())
}
#[tokio::main]
@ -28,14 +91,23 @@ async fn main() -> Result<(), Error> {
};
let cloud = config.cloud()?;
let created = dbg!(cloud.spawn().await?);
let created = cloud.spawn().await?;
let server = cloud.wait_for_ip(&created.id).await?;
println!("Server is booting");
println!(" IP: {}", server.ip);
println!(" Password: {}", created.password);
dbg!(
cloud
.setup(&created.id, &created.password, &config.server)
.await?
let mut ssh = SshSession::open(server.ip, &created.password).await?;
setup(&mut ssh, &config.server).await?;
ssh.close().await?;
println!("Server has been setup and is starting");
println!("Connect using");
println!(
" connect {}; password {}",
server.ip, config.server.password
);
Ok(())
}

View file

@ -1,10 +1,13 @@
use futures_util::future::{self};
use std::io::Write;
use std::net::IpAddr;
use std::sync::Arc;
use std::time::Duration;
use thiserror::Error;
use thrussh::client::Handle;
use thrussh::*;
use thrussh_keys::key::PublicKey;
use tokio::time::{sleep, timeout};
struct Client {}
@ -47,28 +50,9 @@ impl client::Handler for Client {
fn finished(self, session: client::Session) -> Self::FutureUnit {
future::ready(Ok((self, session)))
}
fn check_server_key(self, server_public_key: &PublicKey) -> Self::FutureBool {
println!("check_server_key: {:?}", server_public_key);
fn check_server_key(self, _server_public_key: &PublicKey) -> Self::FutureBool {
self.finished_bool(true)
}
fn channel_open_confirmation(
self,
channel: ChannelId,
_max_packet_size: u32,
_window_size: u32,
session: client::Session,
) -> Self::FutureUnit {
println!("channel_open_confirmation: {:?}", channel);
self.finished(session)
}
fn data(self, channel: ChannelId, data: &[u8], session: client::Session) -> Self::FutureUnit {
println!(
"data on channel {:?}: {:?}",
channel,
std::str::from_utf8(data)
);
self.finished(session)
}
}
pub struct SshSession {
@ -77,6 +61,21 @@ pub struct SshSession {
impl SshSession {
pub async fn open(ip: IpAddr, password: &str) -> Result<Self, SshError> {
Ok(timeout(Duration::from_secs(5 * 60), async move {
loop {
sleep(Duration::from_secs(1)).await;
match SshSession::open_impl(ip, password).await {
Ok(ssh) => return Ok(ssh),
Err(SshError::ConnectionTimeout) => {}
Err(e) => return Err(e),
}
}
})
.await
.map_err(|_| SshError::ConnectionTimeout)??)
}
async fn open_impl(ip: IpAddr, password: &str) -> Result<Self, SshError> {
let config = thrussh::client::Config::default();
let config = Arc::new(config);
let sh = Client {};
@ -89,14 +88,45 @@ impl SshSession {
}
}
pub async fn exec<S: Into<String>>(&mut self, cmd: S) -> Result<(), SshError> {
pub async fn exec<S: Into<String>>(&mut self, cmd: S) -> Result<CommandResult, SshError> {
let mut channel = self.handle.channel_open_session().await?;
println!("exec");
channel.exec(true, cmd).await?;
println!("exec'd");
if let Some(msg) = channel.wait().await {
println!("{:?}", msg)
let mut output = Vec::new();
let mut code = None;
while let Some(msg) = channel.wait().await {
match msg {
thrussh::ChannelMsg::Data { ref data } => {
output.write_all(&data).unwrap();
}
thrussh::ChannelMsg::ExitStatus { exit_status } => {
code = Some(exit_status);
}
_ => {}
}
}
Ok(CommandResult { output, code })
}
pub async fn close(mut self) -> Result<(), SshError> {
self.handle
.disconnect(Disconnect::ByApplication, "", "English")
.await?;
self.handle.await?;
Ok(())
}
}
pub struct CommandResult {
output: Vec<u8>,
pub code: Option<u32>,
}
impl CommandResult {
pub fn output(&self) -> String {
String::from_utf8_lossy(&self.output).into()
}
pub fn success(&self) -> bool {
self.code == Some(0)
}
}