From: Greg Burri Date: Mon, 2 Oct 2023 16:42:05 +0000 (+0200) Subject: First commit X-Git-Url: http://git.euphorik.ch/index.cgi?a=commitdiff_plain;h=1ad3f95830bca3017ca786d96c4b3e442a471231;p=stakingWatchdogWatchdog.git First commit --- 1ad3f95830bca3017ca786d96c4b3e442a471231 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1806f85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +config.ron +Cargo.lock +deploy-to-salon.nu diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0250280 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "staking_watchdog_watchdog" +version = "0.1.0" +authors = ["Greg Burri "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0" +itertools = "0.11" +rand = "0.8" + +lettre = { version = "0.10", features = [ + "rustls-tls", + "smtp-transport", + "builder", +], default-features = false } + +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +ron = "0.8" # Rust object notation, to load configuration files. + +[profile.release] +codegen-units = 1 +lto = true +panic = 'abort' diff --git a/deploy.nu b/deploy.nu new file mode 100644 index 0000000..705c62d --- /dev/null +++ b/deploy.nu @@ -0,0 +1,35 @@ +# 'zigbuild' is needed to build for target 'x86_64-unknown-linux-gnu' on linux: +# https://github.com/rust-cross/cargo-zigbuild + +def main [host: string, destination: string, ssh_key: path] { + let ssh_args = [-i $ssh_key $host] + let scp_args = [-r -i $ssh_key] + let target = "x86_64-unknown-linux-gnu" + let app_name = "staking_watchdog_watchdog" + let build = "release" # "debug" or "release". + + def invoke_ssh [command: string] { + let args = $ssh_args ++ $command + print $"Executing: ssh ($args)" + ssh $args + } + + def copy_ssh [source: string, destination: string] { + let args = $scp_args ++ [$source $"($host):($destination)"] + print $"Executing: scp ($args)" + scp $args + } + + # Don't know how to dynamically pass variable arguments. + if $build == "release" { + cargo zigbuild --target $target --release + } else { + cargo zigbuild --target $target + } + + invoke_ssh $"systemctl --user stop ($app_name)" + copy_ssh ./target/($target)/($build)/($app_name) $destination + invoke_ssh $"chmod u+x ($destination)/($app_name)" + invoke_ssh $"systemctl --user start ($app_name)" + print "Deployment finished" +} \ No newline at end of file diff --git a/doc/staking_watchdog_watchdog.service b/doc/staking_watchdog_watchdog.service new file mode 100644 index 0000000..6e4f973 --- /dev/null +++ b/doc/staking_watchdog_watchdog.service @@ -0,0 +1,14 @@ +[Unit] +Description=staking_watchdog_watchdog + +[Service] +WorkingDirectory=/home/greg/staking_watchdog_watchdog +ExecStart=/home/greg/staking_watchdog_watchdog/staking_watchdog_watchdog +SyslogIdentifier=staking_watchdog_watchdog +Restart=always +RestartSec=10 +KillSignal=SIGINT + +[Install] +WantedBy=default.target + diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..dd9b167 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,38 @@ +use std::fs::File; + +use anyhow::Result; +use ron::{ + de::from_reader, + ser::{to_writer_pretty, PrettyConfig}, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Config { + pub staking_address: String, + pub smtp_login: String, + pub smtp_password: String, +} + +impl Config { + pub fn default() -> Self { + Config { + staking_address: "192.168.2.102:8739".to_string(), + smtp_login: "login".to_string(), + smtp_password: "password".to_string(), + } + } + + pub fn read(file_path: &str) -> Result { + match File::open(file_path) { + Ok(file) => from_reader(file).map_err(|e| e.into()), + // The file doesn't exit -> create it with default values. + Err(_) => { + let file = File::create(file_path)?; + let default_config = Config::default(); + to_writer_pretty(file, &default_config, PrettyConfig::new())?; + Ok(default_config) + } + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b51a6a8 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,155 @@ +use std::{ + net::UdpSocket, + thread, + time::{self, Duration}, +}; + +use anyhow::Result; +use lettre::{ + message::header::ContentType, transport::smtp::authentication::Credentials, Message, + SmtpTransport, Transport, +}; +use rand::{rngs::ThreadRng, Rng}; + +use crate::config::Config; + +mod config; + +const FILE_CONF: &str = "config.ron"; +const PING_PERIOD: Duration = Duration::from_secs(5); // 5 s. +const EMAIL_RESEND_PERIOD: Duration = Duration::from_secs(2 * 60 * 60); // 2 h. +const STATE_PRINT_PERIOD: Duration = Duration::from_secs(15 * 60); // 15 min. + +fn main() -> Result<()> { + println!("Staking Watchdog Watchdog"); + + let config = Config::read(FILE_CONF)?; + + println!( + "Configuration: {:?}", + Config { + smtp_password: "*****".to_string(), + ..config.clone() + } + ); + + let mut time_last_email_send = time::Instant::now() - EMAIL_RESEND_PERIOD; + let mut time_last_state_printed = time::Instant::now() - STATE_PRINT_PERIOD; + let mut error_state = false; + + let mut rng = rand::thread_rng(); + + let socket = UdpSocket::bind("0.0.0.0:0").unwrap(); + socket.connect("192.168.2.102:8739").unwrap(); + socket + .set_read_timeout(Some(Duration::from_secs(5))) + .unwrap(); + socket + .set_write_timeout(Some(Duration::from_secs(5))) + .unwrap(); + + loop { + let time_beginning_loop = time::Instant::now(); + + if let Err(error) = ping(&socket, &mut rng) { + error_state = true; + println!("Error: {:?}", error); + if time::Instant::now() - time_last_email_send >= EMAIL_RESEND_PERIOD { + // Send e-mail. + println!("Sending email..."); + match send_email( + "Watchdog ERROR", + &format!("Error: {:?}", error), + &config.smtp_login, + &config.smtp_password, + ) { + Err(email_error) => println!("Error sending email: {:?}", email_error), + _ => { + println!("Email successfully sent"); + time_last_email_send = time::Instant::now(); + } + } + } + } else { + if error_state { + error_state = false; + println!("End of erroneous state"); + } + + if time::Instant::now() - time_last_state_printed >= STATE_PRINT_PERIOD { + println!("No error detected"); + time_last_state_printed = time::Instant::now(); + } + } + + let elapsed = time::Instant::now() - time_beginning_loop; + + if elapsed < PING_PERIOD { + let to_wait = PING_PERIOD - elapsed; + thread::sleep(to_wait); + } + } +} + +#[derive(Debug)] +enum PingError { + SocketError(std::io::Error), + WrongMessageReceived(String), +} + +fn ping(socket: &UdpSocket, rng: &mut ThreadRng) -> std::result::Result<(), PingError> { + let number: u64 = rng.gen(); + let mut buffer = number.to_le_bytes(); + + match socket.send(&buffer) { + Ok(_size_sent) => { + buffer.fill(0); + match socket.recv(&mut buffer) { + Ok(size_received) => { + if size_received == 8 { + let number_received = u64::from_le_bytes(buffer); + if number_received != number { + return Err(PingError::WrongMessageReceived(format!( + "Message number receceived ({}) is not equal to the one sent ({})", + number_received, number + ))); + } + } else { + return Err(PingError::WrongMessageReceived(format!( + "Size of packet must be 8, received size: {}", + size_received + ))); + } + } + Err(error) => return Err(PingError::SocketError(error)), + } + } + Err(error) => return Err(PingError::SocketError(error)), + } + + Ok(()) +} + +fn send_email(title: &str, body: &str, login: &str, pass: &str) -> Result<()> { + let email = Message::builder() + .message_id(None) + .from("Staking Watchdog ".parse()?) + .to("Greg Burri ".parse()?) + .subject(title) + .header(ContentType::TEXT_PLAIN) + .body(body.to_string())?; + + let creds = Credentials::new(login.to_string(), pass.to_string()); + + // Open a remote connection to gmail + let mailer = SmtpTransport::relay("mail.gandi.net")? + .credentials(creds) + .build(); + + // Send the email + let response = mailer.send(&email)?; + + println!("{:?}", response); + + Ok(()) +}