From dbdc7e0d807963422cc79fcadf8daa9be46be7bf Mon Sep 17 00:00:00 2001 From: Nikos Papadakis Date: Thu, 20 Jul 2023 22:04:51 +0300 Subject: [PATCH] agent: make installation work --- agent/Cargo.lock | 31 +++- agent/Cargo.toml | 8 +- agent/src/bin/prymn_agent.rs | 4 +- agent/src/config.rs | 18 +++ agent/src/lib.rs | 1 + agent/src/self_update.rs | 148 +++++++++++++----- .../controllers/server_controller.ex | 6 +- 7 files changed, 171 insertions(+), 45 deletions(-) create mode 100644 agent/src/config.rs diff --git a/agent/Cargo.lock b/agent/Cargo.lock index e3dbe64..ed11bf6 100644 --- a/agent/Cargo.lock +++ b/agent/Cargo.lock @@ -276,6 +276,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "envy" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" +dependencies = [ + "serde", +] + [[package]] name = "errno" version = "0.3.1" @@ -886,11 +895,15 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "envy", "nix", + "once_cell", "prost", "reqwest", + "serde", "serde_json", "sysinfo", + "tempfile", "tokio", "tokio-stream", "tonic", @@ -1118,9 +1131,23 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.171" +version = "1.0.173" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +checksum = "e91f70896d6720bc714a4a57d22fc91f1db634680e65c8efe13323f1fa38d53f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.173" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6250dde8342e0232232be9ca3db7aa40aceb5a3e5dd9bddbc00d99a007cde49" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] [[package]] name = "serde_json" diff --git a/agent/Cargo.toml b/agent/Cargo.toml index 90e7fd7..6be735b 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -6,11 +6,15 @@ edition = "2021" [dependencies] anyhow = "1.0.71" clap = { version = "4.3.9" } +envy = "0.4.2" nix = "0.26.2" +once_cell = "1.18.0" prost = "0.11.9" -reqwest = { version = "0.11.18", features = ["blocking", "rustls-tls"], default-features = false } -serde_json = "1.0.99" +reqwest = { version = "0.11.18", features = ["blocking", "rustls-tls", "json"], default-features = false } +serde = { version = "1.0.173", features = ["derive"] } +serde_json = "1.0.103" sysinfo = { version = "0.29.2", default-features = false } +tempfile = "3.6.0" tokio = { version = "1.28.2", features = ["rt-multi-thread", "io-util", "process", "macros", "signal"] } tokio-stream = { version = "0.1.14", features = ["net"] } tonic = { version = "0.9.2", features = ["tls"] } diff --git a/agent/src/bin/prymn_agent.rs b/agent/src/bin/prymn_agent.rs index 303d656..77d1c47 100644 --- a/agent/src/bin/prymn_agent.rs +++ b/agent/src/bin/prymn_agent.rs @@ -1,4 +1,4 @@ -use anyhow::anyhow; +use anyhow::Context; use clap::arg; use prymn_agent::{self_update, server}; @@ -18,7 +18,7 @@ fn main() -> anyhow::Result<()> { server::main() } else if let Some(token) = command.get_one::("install") { tracing::info!("starting installation..."); - self_update::install(token) + self_update::install(token).context("failed to install the agent to the system") } else { unreachable!() } diff --git a/agent/src/config.rs b/agent/src/config.rs new file mode 100644 index 0000000..3861de5 --- /dev/null +++ b/agent/src/config.rs @@ -0,0 +1,18 @@ +use once_cell::sync::Lazy; +use serde::Deserialize; + +#[derive(Deserialize, Debug)] +pub struct Config { + #[serde(default = "default_backend_url")] + pub backend_url: String, +} + +fn default_backend_url() -> String { + "https://prymn.net".to_string() +} + +pub static CONFIG: Lazy = + Lazy::new(|| match envy::prefixed("PRYMN_").from_env::() { + Ok(config) => config, + Err(_) => todo!("handle this error"), + }); diff --git a/agent/src/lib.rs b/agent/src/lib.rs index 0094ae6..a65f914 100644 --- a/agent/src/lib.rs +++ b/agent/src/lib.rs @@ -1,2 +1,3 @@ +pub mod config; pub mod self_update; pub mod server; diff --git a/agent/src/self_update.rs b/agent/src/self_update.rs index b39b9a0..237a4b8 100644 --- a/agent/src/self_update.rs +++ b/agent/src/self_update.rs @@ -1,34 +1,32 @@ -use std::{os::unix::prelude::PermissionsExt, path::Path}; +use std::{fs::File, io::Write, os::unix::prelude::PermissionsExt, path::Path, process::Command}; -use anyhow::{anyhow, Context}; -use reqwest::Url; +use anyhow::Context; +use reqwest::{blocking::Client, StatusCode}; +use serde::Deserialize; -const GET_PRYMN_URL: &str = "todo"; +use crate::config; -pub fn update() -> anyhow::Result<()> { - let _url = { - let url = std::env::var("GET_PRYMN_URL").unwrap_or_else(|_| String::from(GET_PRYMN_URL)); - Url::parse(&url)? - }; - - todo!(); -} +const PRYMN_PATH: &str = "/usr/local/bin/prymn_agent"; pub fn install(token: &str) -> anyhow::Result<()> { let this_exe = std::env::current_exe()?; - let prymn_path = Path::new("/usr/local/bin/prymn_agent"); - copy_binary(&this_exe, prymn_path)?; - register_agent(token).context("while registering the agent")?; - install_service()?; + copy_binary(&this_exe, &Path::new(PRYMN_PATH)).with_context(|| { + format!( + "could not copy the file {} to the destination {PRYMN_PATH}", + this_exe.to_str().unwrap(), + ) + })?; + + install_service_file(&Path::new("/etc/systemd/system/prymn.service")) + .context("could not install the agent daemon service")?; + + register_to_backend(token).context("could not register the agent")?; Ok(()) } -fn copy_binary(src: impl AsRef, dest: impl AsRef) -> anyhow::Result<()> { - let src = src.as_ref(); - let dest = dest.as_ref(); - +fn copy_binary(src: &Path, dest: &Path) -> anyhow::Result<()> { if dest.exists() { // unlink the potentially running binary std::fs::remove_file(dest)?; @@ -36,33 +34,111 @@ fn copy_binary(src: impl AsRef, dest: impl AsRef) -> anyhow::Result< std::fs::copy(src, dest)?; - let metadata = dest.metadata()?; - let mut perms = metadata.permissions(); + let mut perms = dest.metadata()?.permissions(); perms.set_mode(0o755); std::fs::set_permissions(dest, perms)?; Ok(()) } -fn register_agent(token: &str) -> anyhow::Result<()> { - let client = reqwest::blocking::Client::new(); +fn install_service_file(dest: &Path) -> anyhow::Result<()> { + let mut file = File::create(dest)?; + write!( + file, + r#" +[Unit] +Description=Prymn Agent Service +After=network.target - let res = client - .post("http://localhost:4000/api/v1/servers/register") - .body(serde_json::json!({"token": token}).to_string()) - .send()?; +[Service] +ExecStart={PRYMN_PATH} -d +Type=simple +Restart=always - if !res.status().is_success() { - // TODO: Make better error message - return Err(anyhow!( - "register request returned an error: {}", - res.text()? - )); +[Install] +WantedBy=default.target +"# + )?; + + if !Command::new("systemctl") + .arg("daemon-reload") + .status()? + .success() + { + anyhow::bail!("command exit with non-zero exit code; could not reload systemd daemon"); + } + + if !Command::new("systemctl") + .arg("enable") + .arg("--now") + .arg("prymn.service") + .status()? + .success() + { + anyhow::bail!("command exit with non-zero exit code; could not enable systemd service"); } Ok(()) } -fn install_service() -> anyhow::Result<()> { - Ok(()) +fn register_to_backend(token: &str) -> anyhow::Result<()> { + let client = Client::new(); + let response = client + .post(format!( + "{}/api/v1/servers/register", + config::CONFIG.backend_url + )) + .json(&serde_json::json!({ "token": token })) + .send()?; + + // TODO: When the backend API is established more concretely, change this to something better. + #[derive(Deserialize)] + struct ApiError { + errors: serde_json::Value, + } + + match response.status() { + StatusCode::UNPROCESSABLE_ENTITY => { + let error = response.json::()?; + anyhow::bail!( + "request was unsuccessful: the backend received invalid data: {}", + error.errors.to_string() + ) + } + status if !status.is_success() => { + anyhow::bail!("request was unsuccessful: error {}", status) + } + _ => Ok(()), + } +} + +#[cfg(test)] +mod tests { + use tempfile::*; + + use super::*; + + #[test] + fn copy_binary_works() { + let temp_dir = tempdir().unwrap(); + let file1_path = temp_dir.path().join("file1"); + let mut file1 = File::create(&file1_path).unwrap(); + let file2_path = temp_dir.path().join("file2"); + let mut file2 = File::create(&file2_path).unwrap(); + + writeln!(file1, "old data").unwrap(); + writeln!(file2, "new data").unwrap(); + + copy_binary(&file2_path, &file1_path).expect("could not copy file"); + + let perms = file1_path + .metadata() + .expect("could not retrieve metadata") + .permissions(); + + let new_data = std::fs::read_to_string(file1_path).unwrap(); + + assert_eq!(new_data, "new data\n"); + assert!(perms.mode() & 0o755 == 0o755); + } } diff --git a/app/lib/prymn_web/controllers/server_controller.ex b/app/lib/prymn_web/controllers/server_controller.ex index 48b548b..4a1699f 100644 --- a/app/lib/prymn_web/controllers/server_controller.ex +++ b/app/lib/prymn_web/controllers/server_controller.ex @@ -16,17 +16,17 @@ defmodule PrymnWeb.ServerController do {:error, :invalid_ip} -> Logger.error("could not register a server because we received an invalid ip") - put_status(conn, 500) + put_status(conn, 422) |> json(%{"errors" => ["invalid ip received"]}) {:error, :bad_token} -> - put_status(conn, 400) + put_status(conn, 422) |> json(%{"errors" => %{"token" => "token is not valid"}}) {:error, %Ecto.Changeset{} = changeset} -> errors = Ecto.Changeset.traverse_errors(changeset, fn {msg, _} -> msg end) - put_status(conn, 400) + put_status(conn, 422) |> json(%{"errors" => errors}) {:error, error} ->