mirror of
https://codeberg.org/spire/dispenser.git
synced 2026-06-03 18:14:06 +02:00
working server spawn
This commit is contained in:
parent
04b08706ef
commit
f3b9d14678
4 changed files with 143 additions and 108 deletions
|
|
@ -1,14 +1,12 @@
|
||||||
mod ssh;
|
use std::net::IpAddr;
|
||||||
pub mod vultr;
|
|
||||||
|
|
||||||
use crate::cloud::ssh::SshError;
|
|
||||||
use crate::config::ServerConfig;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use std::net::IpAddr;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub mod vultr;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum CloudError {
|
pub enum CloudError {
|
||||||
#[error("Invalid credentials")]
|
#[error("Invalid credentials")]
|
||||||
|
|
@ -21,8 +19,6 @@ pub enum CloudError {
|
||||||
InvalidResponse(#[from] ResponseError),
|
InvalidResponse(#[from] ResponseError),
|
||||||
#[error("Server boot timed out")]
|
#[error("Server boot timed out")]
|
||||||
StartTimeout,
|
StartTimeout,
|
||||||
#[error("Error while trying to connect trough ssh: {0}")]
|
|
||||||
Ssh(#[from] SshError),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Intentionally opaque error
|
/// Intentionally opaque error
|
||||||
|
|
@ -72,8 +68,6 @@ pub trait Cloud {
|
||||||
async fn kill(&self, id: &str) -> Result<()>;
|
async fn kill(&self, id: &str) -> Result<()>;
|
||||||
/// Wait until the server has an ip
|
/// Wait until the server has an ip
|
||||||
async fn wait_for_ip(&self, id: &str) -> Result<Server>;
|
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)]
|
#[derive(Debug)]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
use crate::cloud::ssh::{SshError, SshSession};
|
|
||||||
use crate::cloud::{Cloud, CloudError, Created, NetworkError, ResponseError, Result, Server};
|
use crate::cloud::{Cloud, CloudError, Created, NetworkError, ResponseError, Result, Server};
|
||||||
use crate::config::ServerConfig;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use petname::petname;
|
use petname::petname;
|
||||||
|
|
@ -8,7 +6,7 @@ use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::time::{sleep, timeout};
|
use tokio::time::sleep;
|
||||||
|
|
||||||
pub struct Vultr {
|
pub struct Vultr {
|
||||||
region: String,
|
region: String,
|
||||||
|
|
@ -91,65 +89,6 @@ impl Cloud for Vultr {
|
||||||
};
|
};
|
||||||
Ok(instance.into())
|
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 {
|
impl Vultr {
|
||||||
|
|
|
||||||
96
src/main.rs
96
src/main.rs
|
|
@ -1,10 +1,18 @@
|
||||||
pub mod cloud;
|
use std::env::args;
|
||||||
pub mod config;
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use ssh::SshSession;
|
||||||
|
|
||||||
use crate::cloud::CloudError;
|
use crate::cloud::CloudError;
|
||||||
use crate::config::{Config, ConfigError};
|
use crate::config::{Config, ConfigError, ServerConfig};
|
||||||
use std::env::args;
|
use crate::ssh::SshError;
|
||||||
use thiserror::Error;
|
use std::time::Duration;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
pub mod cloud;
|
||||||
|
pub mod config;
|
||||||
|
pub mod ssh;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
|
@ -12,6 +20,61 @@ pub enum Error {
|
||||||
Cloud(#[from] CloudError),
|
Cloud(#[from] CloudError),
|
||||||
#[error("Error while loading configuration: {0}")]
|
#[error("Error while loading configuration: {0}")]
|
||||||
Config(#[from] ConfigError),
|
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]
|
#[tokio::main]
|
||||||
|
|
@ -28,14 +91,23 @@ async fn main() -> Result<(), Error> {
|
||||||
};
|
};
|
||||||
let cloud = config.cloud()?;
|
let cloud = config.cloud()?;
|
||||||
|
|
||||||
let created = dbg!(cloud.spawn().await?);
|
let created = cloud.spawn().await?;
|
||||||
let server = cloud.wait_for_ip(&created.id).await?;
|
let server = cloud.wait_for_ip(&created.id).await?;
|
||||||
println!("IP: {}", server.ip);
|
|
||||||
println!("Password: {}", created.password);
|
println!("Server is booting");
|
||||||
dbg!(
|
println!(" IP: {}", server.ip);
|
||||||
cloud
|
println!(" Password: {}", created.password);
|
||||||
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
use futures_util::future::{self};
|
use futures_util::future::{self};
|
||||||
|
use std::io::Write;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use thrussh::client::Handle;
|
use thrussh::client::Handle;
|
||||||
use thrussh::*;
|
use thrussh::*;
|
||||||
use thrussh_keys::key::PublicKey;
|
use thrussh_keys::key::PublicKey;
|
||||||
|
use tokio::time::{sleep, timeout};
|
||||||
|
|
||||||
struct Client {}
|
struct Client {}
|
||||||
|
|
||||||
|
|
@ -47,28 +50,9 @@ impl client::Handler for Client {
|
||||||
fn finished(self, session: client::Session) -> Self::FutureUnit {
|
fn finished(self, session: client::Session) -> Self::FutureUnit {
|
||||||
future::ready(Ok((self, session)))
|
future::ready(Ok((self, session)))
|
||||||
}
|
}
|
||||||
fn check_server_key(self, server_public_key: &PublicKey) -> Self::FutureBool {
|
fn check_server_key(self, _server_public_key: &PublicKey) -> Self::FutureBool {
|
||||||
println!("check_server_key: {:?}", server_public_key);
|
|
||||||
self.finished_bool(true)
|
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 {
|
pub struct SshSession {
|
||||||
|
|
@ -77,6 +61,21 @@ pub struct SshSession {
|
||||||
|
|
||||||
impl SshSession {
|
impl SshSession {
|
||||||
pub async fn open(ip: IpAddr, password: &str) -> Result<Self, SshError> {
|
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 = thrussh::client::Config::default();
|
||||||
let config = Arc::new(config);
|
let config = Arc::new(config);
|
||||||
let sh = Client {};
|
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?;
|
let mut channel = self.handle.channel_open_session().await?;
|
||||||
println!("exec");
|
|
||||||
channel.exec(true, cmd).await?;
|
channel.exec(true, cmd).await?;
|
||||||
println!("exec'd");
|
let mut output = Vec::new();
|
||||||
if let Some(msg) = channel.wait().await {
|
let mut code = None;
|
||||||
println!("{:?}", msg)
|
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(())
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue