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",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"] }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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::<String>("install") {
 | 
			
		||||
        tracing::info!("starting installation...");
 | 
			
		||||
        self_update::install(token)
 | 
			
		||||
        self_update::install(token).context("failed to install the agent to the system")
 | 
			
		||||
    } else {
 | 
			
		||||
        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 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 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<Path>, dest: impl AsRef<Path>) -> 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<Path>, dest: impl AsRef<Path>) -> 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())
 | 
			
		||||
[Service]
 | 
			
		||||
ExecStart={PRYMN_PATH} -d
 | 
			
		||||
Type=simple
 | 
			
		||||
Restart=always
 | 
			
		||||
 | 
			
		||||
[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 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()?;
 | 
			
		||||
 | 
			
		||||
    if !res.status().is_success() {
 | 
			
		||||
        // TODO: Make better error message
 | 
			
		||||
        return Err(anyhow!(
 | 
			
		||||
            "register request returned an error: {}",
 | 
			
		||||
            res.text()?
 | 
			
		||||
        ));
 | 
			
		||||
    // TODO: When the backend API is established more concretely, change this to something better.
 | 
			
		||||
    #[derive(Deserialize)]
 | 
			
		||||
    struct ApiError {
 | 
			
		||||
        errors: serde_json::Value,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
    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(()),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn install_service() -> anyhow::Result<()> {
 | 
			
		||||
    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} ->
 | 
			
		||||
        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} ->
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue