From ae75a0e819ce1f12250c437469849f30a0473fc0 Mon Sep 17 00:00:00 2001 From: Greg Burri Date: Fri, 29 Sep 2023 20:49:13 +0200 Subject: [PATCH] Send an email in case of error --- .cargo/config.toml | 2 + Cargo.toml | 15 +++++-- deploy.nu | 9 +++-- doc/staking_watchdog.service | 14 +++++++ src/config.rs | 8 +++- src/main.rs | 76 ++++++++++++++++++++---------------- 6 files changed, 84 insertions(+), 40 deletions(-) create mode 100644 doc/staking_watchdog.service diff --git a/.cargo/config.toml b/.cargo/config.toml index 7f77502..44f19b9 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,4 @@ +# Not needed because we build on Windows with target +# 'x86_64-unknown-linux-gnu' and 'cargo zigbuild'. [target.x86_64-unknown-linux-musl] linker = "rust-lld" diff --git a/Cargo.toml b/Cargo.toml index 1600ba7..c4c3a8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,15 +7,24 @@ 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" + reqwest = { version = "0.11", features = [ "blocking", "json", ], default-features = false } -anyhow = "1.0" -itertools = "0.11" + +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. +ron = "0.8" # Rust object notation, to load configuration files. + [profile.release] codegen-units = 1 diff --git a/deploy.nu b/deploy.nu index a7305c9..6945067 100644 --- a/deploy.nu +++ b/deploy.nu @@ -1,7 +1,10 @@ +# '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-musl" + let target = "x86_64-unknown-linux-gnu" let app_name = "staking_watchdog" let build = "debug" # "debug" or "release". @@ -19,9 +22,9 @@ def main [host: string, destination: string, ssh_key: path] { # Don't know how to dynamically pass variable arguments. if $build == "release" { - cargo build --target $target --release + cargo zigbuild --target $target --release } else { - cargo build --target $target + cargo zigbuild --target $target } # invoke_ssh [sudo systemctl stop $app_name] diff --git a/doc/staking_watchdog.service b/doc/staking_watchdog.service new file mode 100644 index 0000000..dfda77a --- /dev/null +++ b/doc/staking_watchdog.service @@ -0,0 +1,14 @@ +[Unit] +Description=staking_watchdog + +[Service] +WorkingDirectory=/home/gburri/staking_watchdog +ExecStart=/home/gburri/staking_watchdog/staking_watchdog +SyslogIdentifier=staking_watchdog +Restart=always +RestartSec=10 +KillSignal=SIGINT + +[Install] +WantedBy=default.target + diff --git a/src/config.rs b/src/config.rs index 30a4990..fd6d2bf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,11 +10,17 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Config { pub pub_keys: Vec, + pub smtp_login: String, + pub smtp_password: String, } impl Config { pub fn default() -> Self { - Config { pub_keys: vec![] } + Config { + pub_keys: vec![], + smtp_login: "login".to_string(), + smtp_password: "password".to_string(), + } } pub fn read(file_path: &str) -> Result { diff --git a/src/main.rs b/src/main.rs index fadc30d..0b7c9c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,10 @@ use std::{ }; use anyhow::{Context, Result}; +use lettre::{ + message::header::ContentType, transport::smtp::authentication::Credentials, Message, + SmtpTransport, Transport, +}; use reqwest::StatusCode; use serde::Deserialize; use serde_json::{json, Value}; @@ -22,8 +26,8 @@ mod config; // mod error; const FILE_CONF: &str = "config.ron"; -const CHECK_PERIOD: Duration = Duration::from_secs(5); // 5s. -const EMAIL_RESEND_PERIOD: Duration = Duration::from_secs(12 * 60 * 60); // 12h. +const CHECK_PERIOD: Duration = Duration::from_secs(10); // 10s. +const EMAIL_RESEND_PERIOD: Duration = Duration::from_secs(6 * 60 * 60); // 6h. const BASE_URI: &str = "http://localhost:5052/eth/v1/"; fn main() -> Result<()> { @@ -43,8 +47,18 @@ fn main() -> Result<()> { if time::Instant::now() - time_last_email_send >= EMAIL_RESEND_PERIOD { // Send e-mail. println!("Sending email..."); - - time_last_email_send = time::Instant::now(); + match send_email( + "Staking 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(); + } + } } } @@ -100,7 +114,7 @@ fn check_validators(pub_keys: &[String]) -> std::result::Result<(), CheckError> .header("accept", "application/json"); match request_health.send() { Ok(resp) => { - println!("{resp:?}"); + // println!("{resp:?}"); // For debug. match resp.status().as_u16() { 200 => (), 206 => return Err(CheckError::NotSync), @@ -115,19 +129,16 @@ fn check_validators(pub_keys: &[String]) -> std::result::Result<(), CheckError> } } - return Err(CheckError::NotSync); - for pub_key in pub_keys { let request = client .get(format!("{url}beacon/states/head/validators/0x{pub_key}")) .header("accept", "application/json"); match request.send() { Ok(resp) => { - println!("{resp:?}"); + // println!("{resp:?}"); // For debug. match resp.status().as_u16() { 200 => { let json: JsonValidatorState = resp.json()?; - // println!("JSON:\n{:?}", json); // For Debug. if json.data.status != "active_ongoing" { return Err(CheckError::ValidatorStatusError { pub_key: pub_key.clone(), @@ -137,7 +148,6 @@ fn check_validators(pub_keys: &[String]) -> std::result::Result<(), CheckError> } code => { let json: JsonError = resp.json()?; - // println!("JSON:\n{:?}", json); // For Debug. return Err(CheckError::ValidatorError { pub_key: pub_key.clone(), message: format!( @@ -149,7 +159,6 @@ fn check_validators(pub_keys: &[String]) -> std::result::Result<(), CheckError> } } Err(error) => { - println!("{error:?}"); return Err(CheckError::ValidatorError { pub_key: pub_key.clone(), message: error.to_string(), @@ -158,28 +167,29 @@ fn check_validators(pub_keys: &[String]) -> std::result::Result<(), CheckError> } } - // match request_builder - // .header("Authorization", format!("Apikey {}", api_key)) - // .send() - // { - // Ok(resp) => { - // if resp.status().is_success() { - // let content = resp.text().unwrap(); - // Ok(serde_json::from_str(&content).unwrap()) - // } else { - // Err(Box::new(Error { - // message: format!("Request unsuccessful to {}: {:#?}", &url, resp), - // })) - // } - // } - // Err(error) => Err(Box::new(Error { - // message: format!("Error during request to {}: {:?}", &url, error), - // })), - // } - - // 1) Check health. - - // 2) Check each validators. + 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(()) } -- 2.45.2