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 changed files with 92 additions and 12 deletions
|
@ -6,7 +6,7 @@ RUN cargo install --locked --path .
|
|||
|
||||
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
|
||||
RUN mkdir -p /usr/local/src/autotasker/migrations
|
||||
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 `Dockerfile`
|
||||
- [ ] Add cli arg to change listen address and port
|
||||
- [ ] Add tasks timeout
|
||||
- [x] Add cli arg to change listen address and port
|
||||
- [x] Add tasks timeout
|
||||
- [ ] Support connecting to remote server by SSH to execute task remotely
|
||||
- [ ] Add details on runtime
|
||||
- [ ] 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
|
||||
- [ ] Don't use long UUID, but only ids
|
||||
- [ ] Validating config file
|
||||
|
|
21
config.yaml
21
config.yaml
|
@ -2,6 +2,13 @@ executor:
|
|||
environment:
|
||||
SUPER_COOL_DEFAULT_ENV: 438548
|
||||
|
||||
hosts:
|
||||
srv06.mbess.net:
|
||||
connection:
|
||||
!ssh
|
||||
user: autotasker
|
||||
key: /home/mbess/.ssh/autotasker
|
||||
|
||||
tasks:
|
||||
do_magic_stuff:
|
||||
name: Do magic incantation
|
||||
|
@ -31,6 +38,20 @@ tasks:
|
|||
# schedule:
|
||||
# "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:
|
||||
- id: 1
|
||||
name: "Trigger magic stuff"
|
||||
|
|
5
justfile
5
justfile
|
@ -2,7 +2,10 @@ export RUST_BACKTRACE := "1"
|
|||
export RUST_LOG := "trace"
|
||||
|
||||
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 -p 3085:8080 -v ./config:/etc/autotasker -v ./db:/var/lib/autotasker autotasker
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use chrono::{SecondsFormat, Utc};
|
||||
use log::{debug, info, error};
|
||||
use log::{debug, error, info, trace};
|
||||
use sqlx::{Pool, QueryBuilder, Sqlite};
|
||||
use tokio::task::JoinHandle;
|
||||
use uuid::Uuid;
|
||||
|
@ -9,7 +9,7 @@ use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
|
|||
use tokio::process::Command;
|
||||
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;
|
||||
|
||||
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?);
|
||||
}
|
||||
|
||||
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<()> {
|
||||
debug!("Start processing of order {:?}", &order.id);
|
||||
// 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)
|
||||
.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,
|
||||
None => return Err(anyhow!("Could not find command to execute")),
|
||||
};
|
||||
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())
|
||||
.stderr(Stdio::piped());
|
||||
// add OS environment variables from default config and task config
|
||||
|
|
|
@ -85,12 +85,17 @@ struct Task {
|
|||
name: String,
|
||||
#[serde(default)]
|
||||
description: Option<String>,
|
||||
|
||||
/// OS environment to add at runtime
|
||||
#[serde(default)]
|
||||
environment: HashMap<String, String>,
|
||||
command: Vec<String>,
|
||||
#[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)]
|
||||
|
@ -167,12 +172,41 @@ struct Webhook {
|
|||
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]
|
||||
struct Config {
|
||||
/// configure current autotasker instance
|
||||
instance: Option<InstanceConfig>,
|
||||
|
||||
#[serde(default = "ExecutorConfig::default")]
|
||||
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>,
|
||||
|
||||
webhooks: Option<Vec<Webhook>>
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue