mirror of
https://codeberg.org/spire/dispenser.git
synced 2026-06-03 18:14:06 +02:00
scheduling
This commit is contained in:
parent
f3b9d14678
commit
da0f12fb91
8 changed files with 456 additions and 39 deletions
|
|
@ -59,7 +59,7 @@ impl From<reqwest::Error> for ResponseError {
|
|||
pub type Result<T, E = CloudError> = std::result::Result<T, E>;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Cloud {
|
||||
pub trait Cloud: Send + Sync + 'static {
|
||||
/// List all running servers on this cloud
|
||||
async fn list(&self) -> Result<Vec<Server>>;
|
||||
/// Create a new server with the given parameter
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use camino::Utf8PathBuf;
|
|||
use serde::Deserialize;
|
||||
use std::fs::read_to_string;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
|
@ -31,6 +32,8 @@ impl From<toml::de::Error> for TomlError {
|
|||
pub struct Config {
|
||||
pub vultr: Option<VultrConfig>,
|
||||
pub server: ServerConfig,
|
||||
pub dyndns: Option<DynDnsConfig>,
|
||||
pub schedule: ScheduleConfig,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
|
@ -39,9 +42,9 @@ impl Config {
|
|||
Ok(toml::from_str(&content).map_err(TomlError::from)?)
|
||||
}
|
||||
|
||||
pub fn cloud(&self) -> Result<Box<dyn Cloud>, ConfigError> {
|
||||
pub fn cloud(&self) -> Result<Arc<dyn Cloud>, ConfigError> {
|
||||
if let Some(vultr) = &self.vultr {
|
||||
Ok(Box::new(Vultr::new(
|
||||
Ok(Arc::new(Vultr::new(
|
||||
vultr.api_key.clone(),
|
||||
vultr.region.clone(),
|
||||
vultr.plan.clone(),
|
||||
|
|
@ -103,3 +106,17 @@ pub struct VultrConfig {
|
|||
fn vultr_default_plan() -> String {
|
||||
String::from("vc2-1c-2gb")
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct DynDnsConfig {
|
||||
pub update_url: String,
|
||||
pub hostname: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ScheduleConfig {
|
||||
pub start: String,
|
||||
pub stop: String,
|
||||
}
|
||||
|
|
|
|||
85
src/dns.rs
Normal file
85
src/dns.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
use reqwest::{Client, StatusCode};
|
||||
use serde::Serialize;
|
||||
use std::net::IpAddr;
|
||||
use thiserror::Error;
|
||||
|
||||
pub type Result<T, E = DynDnsError> = std::result::Result<T, E>;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DynDnsError {
|
||||
#[error("Invalid credentials")]
|
||||
Unauthorized,
|
||||
#[error("Network error: {0}")]
|
||||
Network(#[from] NetworkError),
|
||||
#[error("Network response from server: {0}")]
|
||||
InvalidResponse(String),
|
||||
#[error("Domain belongs to another user")]
|
||||
NotYourDomain,
|
||||
#[error("Invalid hostname")]
|
||||
InvalidHostname,
|
||||
}
|
||||
|
||||
impl DynDnsError {
|
||||
fn from_status_code(status: StatusCode) -> Result<()> {
|
||||
if status == StatusCode::UNAUTHORIZED || status == StatusCode::FORBIDDEN {
|
||||
return Err(DynDnsError::Unauthorized);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Intentionally opaque error
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{0}")]
|
||||
pub struct NetworkError(reqwest::Error);
|
||||
|
||||
pub struct DynDnsClient {
|
||||
client: Client,
|
||||
update_url: String,
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl DynDnsClient {
|
||||
pub fn new(update_url: String, username: String, password: String) -> Self {
|
||||
DynDnsClient {
|
||||
client: Client::new(),
|
||||
update_url,
|
||||
username,
|
||||
password,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update(&self, hostname: &str, ip: IpAddr) -> Result<()> {
|
||||
let response = self
|
||||
.client
|
||||
.get(&self.update_url)
|
||||
.basic_auth(&self.username, Some(&self.password))
|
||||
.query(&DynDnsParams { hostname, ip })
|
||||
.send()
|
||||
.await
|
||||
.map_err(NetworkError)?;
|
||||
|
||||
let status = response.status();
|
||||
DynDnsError::from_status_code(status)?;
|
||||
|
||||
let text = response.text().await.map_err(NetworkError)?;
|
||||
match text.as_str() {
|
||||
"badauth" => Err(DynDnsError::Unauthorized),
|
||||
"!yours" => Err(DynDnsError::NotYourDomain),
|
||||
"nochg" => Ok(()),
|
||||
"good" => Ok(()),
|
||||
"notfqdn" => Err(DynDnsError::InvalidHostname),
|
||||
"nohost" => Err(DynDnsError::InvalidHostname),
|
||||
"numhost" => Err(DynDnsError::InvalidHostname),
|
||||
_ => Err(DynDnsError::InvalidResponse(text)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DynDnsParams<'a> {
|
||||
hostname: &'a str,
|
||||
#[serde(rename = "myip")]
|
||||
ip: IpAddr,
|
||||
}
|
||||
128
src/main.rs
128
src/main.rs
|
|
@ -1,18 +1,20 @@
|
|||
use std::env::args;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use ssh::SshSession;
|
||||
|
||||
use crate::cloud::CloudError;
|
||||
use crate::cloud::{Cloud, CloudError};
|
||||
use crate::config::{Config, ConfigError, ServerConfig};
|
||||
use crate::dns::{DynDnsClient, DynDnsError};
|
||||
use crate::ssh::SshError;
|
||||
use ssh::SshSession;
|
||||
use std::env::args;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use tokio::task::{spawn, JoinError};
|
||||
use tokio::time::sleep;
|
||||
use tokio_cron_scheduler::{Job, JobScheduler};
|
||||
|
||||
pub mod cloud;
|
||||
pub mod config;
|
||||
pub mod ssh;
|
||||
mod cloud;
|
||||
mod config;
|
||||
mod dns;
|
||||
mod ssh;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
|
|
@ -24,6 +26,30 @@ pub enum Error {
|
|||
Ssh(#[from] SshError),
|
||||
#[error("Setup command returned an error: {0}")]
|
||||
SetupError(String),
|
||||
#[error("Error while updating dyndns: {0}")]
|
||||
DynDns(#[from] DynDnsError),
|
||||
#[error("Already running")]
|
||||
AlreadyRunning,
|
||||
#[error("{0}")]
|
||||
Schedule(ScheduleError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{0}")]
|
||||
pub struct ScheduleError(ScheduleErrorImpl);
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum ScheduleErrorImpl {
|
||||
#[error("Error setting up schedule")]
|
||||
Schedule(String),
|
||||
#[error("Error running schedule")]
|
||||
Join(JoinError),
|
||||
}
|
||||
|
||||
impl From<ScheduleErrorImpl> for Error {
|
||||
fn from(e: ScheduleErrorImpl) -> Self {
|
||||
Error::Schedule(ScheduleError(e))
|
||||
}
|
||||
}
|
||||
|
||||
async fn setup(ssh: &mut SshSession, config: &ServerConfig) -> Result<(), Error> {
|
||||
|
|
@ -79,6 +105,8 @@ async fn setup(ssh: &mut SshSession, config: &ServerConfig) -> Result<(), Error>
|
|||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
pretty_env_logger::init();
|
||||
|
||||
let mut args = args();
|
||||
let bin = args.next().unwrap();
|
||||
|
||||
|
|
@ -91,12 +119,85 @@ async fn main() -> Result<(), Error> {
|
|||
};
|
||||
let cloud = config.cloud()?;
|
||||
|
||||
let mut sched = JobScheduler::new();
|
||||
|
||||
let server_id: Arc<Mutex<Option<String>>> = Arc::default();
|
||||
|
||||
sched
|
||||
.add(stop_job(cloud.clone(), &config, server_id.clone()))
|
||||
.map_err(|e| ScheduleErrorImpl::Schedule(format!("{:#}", e)))?;
|
||||
sched
|
||||
.add(start_job(cloud, config, server_id))
|
||||
.map_err(|e| ScheduleErrorImpl::Schedule(format!("{:#}", e)))?;
|
||||
|
||||
sched.start().await.map_err(ScheduleErrorImpl::Join)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop_job(_cloud: Arc<dyn Cloud>, config: &Config, server_id: Arc<Mutex<Option<String>>>) -> Job {
|
||||
Job::new(&config.schedule.stop, move |_uuid, _l| {
|
||||
let server_id = server_id.clone();
|
||||
spawn(async move {
|
||||
println!("Stopping server");
|
||||
if let Some(id) = server_id.lock().unwrap().take() {
|
||||
println!("Would have killed {}", id);
|
||||
// match cloud.kill(&id).await {
|
||||
// Ok(_) => {}
|
||||
// Err(e) => eprintln!("{:#}", e),
|
||||
// };
|
||||
}
|
||||
});
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn start_job(cloud: Arc<dyn Cloud>, config: Config, server_id: Arc<Mutex<Option<String>>>) -> Job {
|
||||
let schedule = config.schedule.start.clone();
|
||||
let config = Arc::new(config);
|
||||
Job::new(&schedule, move |_uuid, _l| {
|
||||
let cloud = cloud.clone();
|
||||
let config = config.clone();
|
||||
let server_id = server_id.clone();
|
||||
spawn(async move {
|
||||
let cloud = cloud.as_ref();
|
||||
println!("Starting server");
|
||||
match start(cloud, &config).await {
|
||||
Ok(id) => *server_id.lock().unwrap() = Some(id),
|
||||
Err(e) => eprintln!("{:#}", e),
|
||||
};
|
||||
});
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn start(cloud: &dyn Cloud, config: &Config) -> Result<String, Error> {
|
||||
let list = cloud.list().await?;
|
||||
if !list.is_empty() {
|
||||
return Err(Error::AlreadyRunning);
|
||||
}
|
||||
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);
|
||||
println!(" Root Password: {}", created.password);
|
||||
|
||||
let connect_host = if let Some(dns_config) = config.dyndns.as_ref() {
|
||||
let dns = DynDnsClient::new(
|
||||
dns_config.update_url.to_string(),
|
||||
dns_config.username.to_string(),
|
||||
dns_config.password.to_string(),
|
||||
);
|
||||
println!(
|
||||
"Updating DynDNS entry for {} to {}",
|
||||
dns_config.hostname, server.ip
|
||||
);
|
||||
dns.update(&dns_config.hostname, server.ip).await?;
|
||||
dns_config.hostname.to_string()
|
||||
} else {
|
||||
format!("{}", server.ip)
|
||||
};
|
||||
|
||||
let mut ssh = SshSession::open(server.ip, &created.password).await?;
|
||||
setup(&mut ssh, &config.server).await?;
|
||||
|
|
@ -106,8 +207,7 @@ async fn main() -> Result<(), Error> {
|
|||
println!("Connect using");
|
||||
println!(
|
||||
" connect {}; password {}",
|
||||
server.ip, config.server.password
|
||||
connect_host, config.server.password
|
||||
);
|
||||
|
||||
Ok(())
|
||||
Ok(server.id)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue