agent: make installation work

This commit is contained in:
Nikos Papadakis 2023-07-20 22:04:51 +03:00
parent c87a3cb7e0
commit dbdc7e0d80
Signed by untrusted user who does not match committer: nikos
GPG key ID: 78871F9905ADFF02
7 changed files with 171 additions and 45 deletions

31
agent/Cargo.lock generated
View file

@ -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"

View file

@ -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"] }

View file

@ -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
View 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"),
});

View file

@ -1,2 +1,3 @@
pub mod config;
pub mod self_update; pub mod self_update;
pub mod server; pub mod server;

View file

@ -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);
}
} }

View file

@ -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} ->