First commit
authorGreg Burri <greg.burri@gmail.com>
Mon, 2 Oct 2023 16:42:05 +0000 (18:42 +0200)
committerGreg Burri <greg.burri@gmail.com>
Mon, 2 Oct 2023 16:42:05 +0000 (18:42 +0200)
.gitignore [new file with mode: 0644]
Cargo.toml [new file with mode: 0644]
deploy.nu [new file with mode: 0644]
doc/staking_watchdog_watchdog.service [new file with mode: 0644]
src/config.rs [new file with mode: 0644]
src/main.rs [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..1806f85
--- /dev/null
@@ -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 (file)
index 0000000..0250280
--- /dev/null
@@ -0,0 +1,27 @@
+[package]
+name = "staking_watchdog_watchdog"
+version = "0.1.0"
+authors = ["Greg Burri <greg.burri@gmail.com>"]
+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 (file)
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 (file)
index 0000000..6e4f973
--- /dev/null
@@ -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 (file)
index 0000000..dd9b167
--- /dev/null
@@ -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<Config> {
+        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 (file)
index 0000000..b51a6a8
--- /dev/null
@@ -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 <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(())
+}