Send an email in case of error
authorGreg Burri <greg.burri@gmail.com>
Fri, 29 Sep 2023 18:49:13 +0000 (20:49 +0200)
committerGreg Burri <greg.burri@gmail.com>
Fri, 29 Sep 2023 18:49:13 +0000 (20:49 +0200)
.cargo/config.toml
Cargo.toml
deploy.nu
doc/staking_watchdog.service [new file with mode: 0644]
src/config.rs
src/main.rs

index 7f77502..44f19b9 100644 (file)
@@ -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"
index 1600ba7..c4c3a8a 100644 (file)
@@ -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
index a7305c9..6945067 100644 (file)
--- 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 (file)
index 0000000..dfda77a
--- /dev/null
@@ -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
+
index 30a4990..fd6d2bf 100644 (file)
@@ -10,11 +10,17 @@ use serde::{Deserialize, Serialize};
 #[derive(Debug, Clone, Deserialize, Serialize)]
 pub struct Config {
     pub pub_keys: Vec<String>,
+    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<Config> {
index fadc30d..0b7c9c4 100644 (file)
@@ -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 <redmine@d-lan.net>".parse()?)
+        .to("Greg Burri <greg.burri@gmail.com>".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(())
 }