feat(executor): remote host executing via ssh
Allow to specify a task to be running on a remote host via wrapping command around ssh client.
This commit is contained in:
parent
84a456003c
commit
b79d989f78
|
@ -6,7 +6,7 @@ RUN cargo install --locked --path .
|
||||||
|
|
||||||
FROM alpine:3.20
|
FROM alpine:3.20
|
||||||
|
|
||||||
RUN apk add sqlite
|
RUN apk add sqlite openssh
|
||||||
COPY --from=builder /usr/local/cargo/bin/autotasker /usr/local/bin/autotasker
|
COPY --from=builder /usr/local/cargo/bin/autotasker /usr/local/bin/autotasker
|
||||||
RUN mkdir -p /usr/local/src/autotasker/migrations
|
RUN mkdir -p /usr/local/src/autotasker/migrations
|
||||||
RUN mkdir -p /usr/local/lib/autotasker/assets
|
RUN mkdir -p /usr/local/lib/autotasker/assets
|
||||||
|
|
7
TODO.md
7
TODO.md
|
@ -4,11 +4,12 @@
|
||||||
|
|
||||||
- [x] Add CSS badge and color code on job status
|
- [x] Add CSS badge and color code on job status
|
||||||
- [x] Add `Dockerfile`
|
- [x] Add `Dockerfile`
|
||||||
- [ ] Add cli arg to change listen address and port
|
- [x] Add cli arg to change listen address and port
|
||||||
- [ ] Add tasks timeout
|
- [x] Add tasks timeout
|
||||||
|
- [ ] Support connecting to remote server by SSH to execute task remotely
|
||||||
- [ ] Add details on runtime
|
- [ ] Add details on runtime
|
||||||
- [ ] Implement basic auth with OAuth2, find a minimal oauth2
|
- [ ] Implement basic auth with OAuth2, find a minimal oauth2
|
||||||
- [ ] Support connecting to remote server by SSH to execute task remotely
|
- [ ] Provide example of nginx config with basic auth support excluding webhook
|
||||||
- [ ] Add a way to categorize tasks, regroup tasks
|
- [ ] Add a way to categorize tasks, regroup tasks
|
||||||
- [ ] Don't use long UUID, but only ids
|
- [ ] Don't use long UUID, but only ids
|
||||||
- [ ] Validating config file
|
- [ ] Validating config file
|
||||||
|
|
21
config.yaml
21
config.yaml
|
@ -2,6 +2,13 @@ executor:
|
||||||
environment:
|
environment:
|
||||||
SUPER_COOL_DEFAULT_ENV: 438548
|
SUPER_COOL_DEFAULT_ENV: 438548
|
||||||
|
|
||||||
|
hosts:
|
||||||
|
srv06.mbess.net:
|
||||||
|
connection:
|
||||||
|
!ssh
|
||||||
|
user: autotasker
|
||||||
|
key: /home/mbess/.ssh/autotasker
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
do_magic_stuff:
|
do_magic_stuff:
|
||||||
name: Do magic incantation
|
name: Do magic incantation
|
||||||
|
@ -31,6 +38,20 @@ tasks:
|
||||||
# schedule:
|
# schedule:
|
||||||
# "0 * * * * *"
|
# "0 * * * * *"
|
||||||
|
|
||||||
|
remote_cmd:
|
||||||
|
name: Get remote infos
|
||||||
|
target_host: srv06.mbess.net
|
||||||
|
command:
|
||||||
|
- hostnamectl
|
||||||
|
|
||||||
|
srv06_renew_certs:
|
||||||
|
name: Renew certs of srv06
|
||||||
|
target_host: srv06.mbess.net
|
||||||
|
command: # I've setup sudo to let autotasker user only run certbot binary
|
||||||
|
- sudo
|
||||||
|
- certbot
|
||||||
|
- renew
|
||||||
|
|
||||||
webhooks:
|
webhooks:
|
||||||
- id: 1
|
- id: 1
|
||||||
name: "Trigger magic stuff"
|
name: "Trigger magic stuff"
|
||||||
|
|
5
justfile
5
justfile
|
@ -2,7 +2,10 @@ export RUST_BACKTRACE := "1"
|
||||||
export RUST_LOG := "trace"
|
export RUST_LOG := "trace"
|
||||||
|
|
||||||
watch-run:
|
watch-run:
|
||||||
cargo-watch -x 'run -- --config config.yaml'
|
cargo-watch -x 'run -- --config config.yaml --database ./tmp/dbs/autotasker.db'
|
||||||
|
|
||||||
|
run:
|
||||||
|
cargo run -- --database ./tmp/dbs/autotasker.db --config ./config.yaml --static-assets ./assets
|
||||||
|
|
||||||
docker-run:
|
docker-run:
|
||||||
docker run -p 3085:8080 -v ./config:/etc/autotasker -v ./db:/var/lib/autotasker autotasker
|
docker run -p 3085:8080 -v ./config:/etc/autotasker -v ./db:/var/lib/autotasker autotasker
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use chrono::{SecondsFormat, Utc};
|
use chrono::{SecondsFormat, Utc};
|
||||||
use log::{debug, info, error};
|
use log::{debug, error, info, trace};
|
||||||
use sqlx::{Pool, QueryBuilder, Sqlite};
|
use sqlx::{Pool, QueryBuilder, Sqlite};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
@ -9,7 +9,7 @@ use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tokio::sync::mpsc::Receiver;
|
use tokio::sync::mpsc::Receiver;
|
||||||
|
|
||||||
use crate::models::{ExecutorOrder, LogKind, LogLine, TaskStatus};
|
use crate::models::{Config, ExecutorOrder, HostConnection, LogKind, LogLine, Task, TaskStatus};
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
async fn insert_logs(db: &Pool<Sqlite>, collected_logs: &mut Vec<LogLine>) -> Result<()> {
|
async fn insert_logs(db: &Pool<Sqlite>, collected_logs: &mut Vec<LogLine>) -> Result<()> {
|
||||||
|
@ -90,6 +90,25 @@ async fn execute_process(state: &AppState, order: &ExecutorOrder, cmd: &mut Comm
|
||||||
return Ok(process_handle.await?);
|
return Ok(process_handle.await?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prepare_command(config: &Config, task: &Task) -> Vec<String> {
|
||||||
|
if let Some(target_host_fqdn) = &task.target_host {
|
||||||
|
let host_config = config.hosts.get(target_host_fqdn).expect("Task definition contain invalid reference to target host");
|
||||||
|
|
||||||
|
return match &host_config.connection {
|
||||||
|
// wrap command to use ssh client
|
||||||
|
HostConnection::Ssh { user, key } => {
|
||||||
|
let ssh_authority = format!("{}@{}", user, target_host_fqdn);
|
||||||
|
debug!("Using ssh client for this command, remote ssh authority is: {:?}", &ssh_authority);
|
||||||
|
vec![
|
||||||
|
"/usr/bin/ssh".into(), "-i".into(), format!("{}", key), ssh_authority,
|
||||||
|
task.command.join(" ")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
task.command.clone()
|
||||||
|
}
|
||||||
|
|
||||||
async fn run_task(state: AppState, order: ExecutorOrder) -> Result<()> {
|
async fn run_task(state: AppState, order: ExecutorOrder) -> Result<()> {
|
||||||
debug!("Start processing of order {:?}", &order.id);
|
debug!("Start processing of order {:?}", &order.id);
|
||||||
// save in DB
|
// save in DB
|
||||||
|
@ -104,13 +123,15 @@ async fn run_task(state: AppState, order: ExecutorOrder) -> Result<()> {
|
||||||
|
|
||||||
let task = state.config.tasks.get(&order.task_id)
|
let task = state.config.tasks.get(&order.task_id)
|
||||||
.expect("Task id in executor order is not valid.");
|
.expect("Task id in executor order is not valid.");
|
||||||
let executable = match task.command.first() {
|
let command_args: Vec<String> = prepare_command(&state.config, &task);
|
||||||
|
trace!("Command arguments {:?}", &command_args);
|
||||||
|
let executable = match command_args.first() {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return Err(anyhow!("Could not find command to execute")),
|
None => return Err(anyhow!("Could not find command to execute")),
|
||||||
};
|
};
|
||||||
let mut cmd = Command::new(executable);
|
let mut cmd = Command::new(executable);
|
||||||
|
|
||||||
cmd.args(task.command.iter().skip(1).collect::<Vec<&String>>())
|
cmd.args(command_args.iter().skip(1).collect::<Vec<&String>>())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped());
|
.stderr(Stdio::piped());
|
||||||
// add OS environment variables from default config and task config
|
// add OS environment variables from default config and task config
|
||||||
|
|
|
@ -85,12 +85,17 @@ struct Task {
|
||||||
name: String,
|
name: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
|
|
||||||
/// OS environment to add at runtime
|
/// OS environment to add at runtime
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
environment: HashMap<String, String>,
|
environment: HashMap<String, String>,
|
||||||
command: Vec<String>,
|
command: Vec<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
schedule: Option<ScheduleConfig>
|
schedule: Option<ScheduleConfig>,
|
||||||
|
|
||||||
|
/// Where the task will be executed, None is autotasker host
|
||||||
|
#[serde(default)]
|
||||||
|
target_host: Option<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
@ -167,12 +172,41 @@ struct Webhook {
|
||||||
debounce_secs: u32
|
debounce_secs: u32
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[fully_pub]
|
||||||
|
// externally tagged
|
||||||
|
enum HostConnection {
|
||||||
|
#[serde(rename = "ssh")]
|
||||||
|
Ssh {
|
||||||
|
/// POSIX user
|
||||||
|
user: String,
|
||||||
|
/// path to SSH private key
|
||||||
|
key: String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[fully_pub]
|
||||||
|
/// remote POSIX host infos
|
||||||
|
struct HostConfig {
|
||||||
|
connection: HostConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[fully_pub]
|
#[fully_pub]
|
||||||
struct Config {
|
struct Config {
|
||||||
|
/// configure current autotasker instance
|
||||||
|
instance: Option<InstanceConfig>,
|
||||||
|
|
||||||
#[serde(default = "ExecutorConfig::default")]
|
#[serde(default = "ExecutorConfig::default")]
|
||||||
executor: ExecutorConfig,
|
executor: ExecutorConfig,
|
||||||
instance: Option<InstanceConfig>,
|
|
||||||
|
/// map of remote hosts config where we can execute tasks
|
||||||
|
/// the key must be a FQDN
|
||||||
|
#[serde(default)]
|
||||||
|
hosts: HashMap<String, HostConfig>,
|
||||||
|
|
||||||
tasks: HashMap<String, Task>,
|
tasks: HashMap<String, Task>,
|
||||||
|
|
||||||
webhooks: Option<Vec<Webhook>>
|
webhooks: Option<Vec<Webhook>>
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue