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