agent: make installation work
This commit is contained in:
		
							parent
							
								
									c87a3cb7e0
								
							
						
					
					
						commit
						dbdc7e0d80
					
				
					 7 changed files with 171 additions and 45 deletions
				
			
		
							
								
								
									
										31
									
								
								agent/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										31
									
								
								agent/Cargo.lock
									
									
									
										generated
									
									
									
								
							|  | @ -276,6 +276,15 @@ dependencies = [ | ||||||
|  "cfg-if", |  "cfg-if", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "envy" | ||||||
|  | version = "0.4.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" | ||||||
|  | dependencies = [ | ||||||
|  |  "serde", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "errno" | name = "errno" | ||||||
| version = "0.3.1" | version = "0.3.1" | ||||||
|  | @ -886,11 +895,15 @@ version = "0.1.0" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "anyhow", |  "anyhow", | ||||||
|  "clap", |  "clap", | ||||||
|  |  "envy", | ||||||
|  "nix", |  "nix", | ||||||
|  |  "once_cell", | ||||||
|  "prost", |  "prost", | ||||||
|  "reqwest", |  "reqwest", | ||||||
|  |  "serde", | ||||||
|  "serde_json", |  "serde_json", | ||||||
|  "sysinfo", |  "sysinfo", | ||||||
|  |  "tempfile", | ||||||
|  "tokio", |  "tokio", | ||||||
|  "tokio-stream", |  "tokio-stream", | ||||||
|  "tonic", |  "tonic", | ||||||
|  | @ -1118,9 +1131,23 @@ dependencies = [ | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "serde" | name = "serde" | ||||||
| version = "1.0.171" | version = "1.0.173" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | 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]] | [[package]] | ||||||
| name = "serde_json" | name = "serde_json" | ||||||
|  |  | ||||||
|  | @ -6,11 +6,15 @@ edition = "2021" | ||||||
| [dependencies] | [dependencies] | ||||||
| anyhow = "1.0.71" | anyhow = "1.0.71" | ||||||
| clap = { version = "4.3.9" } | clap = { version = "4.3.9" } | ||||||
|  | envy = "0.4.2" | ||||||
| nix = "0.26.2" | nix = "0.26.2" | ||||||
|  | once_cell = "1.18.0" | ||||||
| prost = "0.11.9" | prost = "0.11.9" | ||||||
| reqwest = { version = "0.11.18", features = ["blocking", "rustls-tls"], default-features = false } | reqwest = { version = "0.11.18", features = ["blocking", "rustls-tls", "json"], default-features = false } | ||||||
| serde_json = "1.0.99" | serde = { version = "1.0.173", features = ["derive"] } | ||||||
|  | serde_json = "1.0.103" | ||||||
| sysinfo = { version = "0.29.2", default-features = false } | 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 = { version = "1.28.2", features = ["rt-multi-thread", "io-util", "process", "macros", "signal"] } | ||||||
| tokio-stream = { version = "0.1.14", features = ["net"] } | tokio-stream = { version = "0.1.14", features = ["net"] } | ||||||
| tonic = { version = "0.9.2", features = ["tls"] } | tonic = { version = "0.9.2", features = ["tls"] } | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| use anyhow::anyhow; | use anyhow::Context; | ||||||
| use clap::arg; | use clap::arg; | ||||||
| use prymn_agent::{self_update, server}; | use prymn_agent::{self_update, server}; | ||||||
| 
 | 
 | ||||||
|  | @ -18,7 +18,7 @@ fn main() -> anyhow::Result<()> { | ||||||
|         server::main() |         server::main() | ||||||
|     } else if let Some(token) = command.get_one::<String>("install") { |     } else if let Some(token) = command.get_one::<String>("install") { | ||||||
|         tracing::info!("starting installation..."); |         tracing::info!("starting installation..."); | ||||||
|         self_update::install(token) |         self_update::install(token).context("failed to install the agent to the system") | ||||||
|     } else { |     } else { | ||||||
|         unreachable!() |         unreachable!() | ||||||
|     } |     } | ||||||
|  |  | ||||||
							
								
								
									
										18
									
								
								agent/src/config.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								agent/src/config.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -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<Config> = | ||||||
|  |     Lazy::new(|| match envy::prefixed("PRYMN_").from_env::<Config>() { | ||||||
|  |         Ok(config) => config, | ||||||
|  |         Err(_) => todo!("handle this error"), | ||||||
|  |     }); | ||||||
|  | @ -1,2 +1,3 @@ | ||||||
|  | pub mod config; | ||||||
| pub mod self_update; | pub mod self_update; | ||||||
| pub mod server; | pub mod server; | ||||||
|  |  | ||||||
|  | @ -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 anyhow::Context; | ||||||
| use reqwest::Url; | use reqwest::{blocking::Client, StatusCode}; | ||||||
|  | use serde::Deserialize; | ||||||
| 
 | 
 | ||||||
| const GET_PRYMN_URL: &str = "todo"; | use crate::config; | ||||||
| 
 | 
 | ||||||
| pub fn update() -> anyhow::Result<()> { | const PRYMN_PATH: &str = "/usr/local/bin/prymn_agent"; | ||||||
|     let _url = { |  | ||||||
|         let url = std::env::var("GET_PRYMN_URL").unwrap_or_else(|_| String::from(GET_PRYMN_URL)); |  | ||||||
|         Url::parse(&url)? |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     todo!(); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| pub fn install(token: &str) -> anyhow::Result<()> { | pub fn install(token: &str) -> anyhow::Result<()> { | ||||||
|     let this_exe = std::env::current_exe()?; |     let this_exe = std::env::current_exe()?; | ||||||
|     let prymn_path = Path::new("/usr/local/bin/prymn_agent"); |  | ||||||
| 
 | 
 | ||||||
|     copy_binary(&this_exe, prymn_path)?; |     copy_binary(&this_exe, &Path::new(PRYMN_PATH)).with_context(|| { | ||||||
|     register_agent(token).context("while registering the agent")?; |         format!( | ||||||
|     install_service()?; |             "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(()) |     Ok(()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn copy_binary(src: impl AsRef<Path>, dest: impl AsRef<Path>) -> anyhow::Result<()> { | fn copy_binary(src: &Path, dest: &Path) -> anyhow::Result<()> { | ||||||
|     let src = src.as_ref(); |  | ||||||
|     let dest = dest.as_ref(); |  | ||||||
| 
 |  | ||||||
|     if dest.exists() { |     if dest.exists() { | ||||||
|         // unlink the potentially running binary
 |         // unlink the potentially running binary
 | ||||||
|         std::fs::remove_file(dest)?; |         std::fs::remove_file(dest)?; | ||||||
|  | @ -36,33 +34,111 @@ fn copy_binary(src: impl AsRef<Path>, dest: impl AsRef<Path>) -> anyhow::Result< | ||||||
| 
 | 
 | ||||||
|     std::fs::copy(src, dest)?; |     std::fs::copy(src, dest)?; | ||||||
| 
 | 
 | ||||||
|     let metadata = dest.metadata()?; |     let mut perms = dest.metadata()?.permissions(); | ||||||
|     let mut perms = metadata.permissions(); |  | ||||||
|     perms.set_mode(0o755); |     perms.set_mode(0o755); | ||||||
|     std::fs::set_permissions(dest, perms)?; |     std::fs::set_permissions(dest, perms)?; | ||||||
| 
 | 
 | ||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn register_agent(token: &str) -> anyhow::Result<()> { | fn install_service_file(dest: &Path) -> anyhow::Result<()> { | ||||||
|     let client = reqwest::blocking::Client::new(); |     let mut file = File::create(dest)?; | ||||||
|  |     write!( | ||||||
|  |         file, | ||||||
|  |         r#" | ||||||
|  | [Unit] | ||||||
|  | Description=Prymn Agent Service | ||||||
|  | After=network.target | ||||||
| 
 | 
 | ||||||
|     let res = client | [Service] | ||||||
|         .post("http://localhost:4000/api/v1/servers/register") | ExecStart={PRYMN_PATH} -d | ||||||
|         .body(serde_json::json!({"token": token}).to_string()) | Type=simple | ||||||
|         .send()?; | Restart=always | ||||||
| 
 | 
 | ||||||
|     if !res.status().is_success() { | [Install] | ||||||
|         // TODO: Make better error message
 | WantedBy=default.target | ||||||
|         return Err(anyhow!( | "#
 | ||||||
|             "register request returned an error: {}", |     )?; | ||||||
|             res.text()? | 
 | ||||||
|         )); |     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(()) |     Ok(()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn install_service() -> anyhow::Result<()> { | fn register_to_backend(token: &str) -> anyhow::Result<()> { | ||||||
|     Ok(()) |     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::<ApiError>()?; | ||||||
|  |             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); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -16,17 +16,17 @@ defmodule PrymnWeb.ServerController do | ||||||
|       {:error, :invalid_ip} -> |       {:error, :invalid_ip} -> | ||||||
|         Logger.error("could not register a server because we received an 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"]}) |         |> json(%{"errors" => ["invalid ip received"]}) | ||||||
| 
 | 
 | ||||||
|       {:error, :bad_token} -> |       {:error, :bad_token} -> | ||||||
|         put_status(conn, 400) |         put_status(conn, 422) | ||||||
|         |> json(%{"errors" => %{"token" => "token is not valid"}}) |         |> json(%{"errors" => %{"token" => "token is not valid"}}) | ||||||
| 
 | 
 | ||||||
|       {:error, %Ecto.Changeset{} = changeset} -> |       {:error, %Ecto.Changeset{} = changeset} -> | ||||||
|         errors = Ecto.Changeset.traverse_errors(changeset, fn {msg, _} -> msg end) |         errors = Ecto.Changeset.traverse_errors(changeset, fn {msg, _} -> msg end) | ||||||
| 
 | 
 | ||||||
|         put_status(conn, 400) |         put_status(conn, 422) | ||||||
|         |> json(%{"errors" => errors}) |         |> json(%{"errors" => errors}) | ||||||
| 
 | 
 | ||||||
|       {:error, error} -> |       {:error, error} -> | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue