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