parent
ac20f1b343
commit
700154e311
27 changed files with 406 additions and 350 deletions
@ -0,0 +1,137 @@ |
|||||||
|
use std::env; |
||||||
|
use std::fmt; |
||||||
|
use structopt::StructOpt; |
||||||
|
use u_lib::{ |
||||||
|
api::ClientHandler, datatypes::DataResult, messaging::AsMsg, models::JobMeta, UError, UResult, |
||||||
|
}; |
||||||
|
use uuid::Uuid; |
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)] |
||||||
|
pub struct Args { |
||||||
|
#[structopt(subcommand)] |
||||||
|
cmd: Cmd, |
||||||
|
#[structopt(long)] |
||||||
|
json: bool, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)] |
||||||
|
enum Cmd { |
||||||
|
Agents(LD), |
||||||
|
Jobs(JobALD), |
||||||
|
Jobmap(JobMapALD), |
||||||
|
Server, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)] |
||||||
|
enum JobALD { |
||||||
|
Add { |
||||||
|
#[structopt(long, parse(try_from_str = parse_uuid))] |
||||||
|
agent: Option<Uuid>, |
||||||
|
|
||||||
|
#[structopt(long)] |
||||||
|
alias: String, |
||||||
|
|
||||||
|
#[structopt(subcommand)] |
||||||
|
cmd: JobCmd, |
||||||
|
}, |
||||||
|
#[structopt(flatten)] |
||||||
|
LD(LD), |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)] |
||||||
|
enum JobCmd { |
||||||
|
#[structopt(external_subcommand)] |
||||||
|
Cmd(Vec<String>), |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)] |
||||||
|
enum JobMapALD { |
||||||
|
Add { |
||||||
|
#[structopt(parse(try_from_str = parse_uuid))] |
||||||
|
agent_uid: Uuid, |
||||||
|
|
||||||
|
job_idents: Vec<String>, |
||||||
|
}, |
||||||
|
List { |
||||||
|
#[structopt(parse(try_from_str = parse_uuid))] |
||||||
|
uid: Option<Uuid>, |
||||||
|
}, |
||||||
|
Delete { |
||||||
|
#[structopt(parse(try_from_str = parse_uuid))] |
||||||
|
uid: Uuid, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(StructOpt, Debug)] |
||||||
|
enum LD { |
||||||
|
List { |
||||||
|
#[structopt(parse(try_from_str = parse_uuid))] |
||||||
|
uid: Option<Uuid>, |
||||||
|
}, |
||||||
|
Delete { |
||||||
|
#[structopt(parse(try_from_str = parse_uuid))] |
||||||
|
uid: Uuid, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
fn parse_uuid(src: &str) -> Result<Uuid, String> { |
||||||
|
Uuid::parse_str(src).map_err(|e| e.to_string()) |
||||||
|
} |
||||||
|
|
||||||
|
pub async fn process_cmd(args: Args) -> UResult<()> { |
||||||
|
struct Printer { |
||||||
|
json: bool, |
||||||
|
} |
||||||
|
|
||||||
|
impl Printer { |
||||||
|
pub fn print<Msg: AsMsg + fmt::Display>(&self, data: UResult<Msg>) { |
||||||
|
if self.json { |
||||||
|
let data = match data { |
||||||
|
Ok(r) => DataResult::Ok(r), |
||||||
|
Err(e) => DataResult::Err(e), |
||||||
|
}; |
||||||
|
println!("{}", serde_json::to_string_pretty(&data).unwrap()); |
||||||
|
} else { |
||||||
|
match data { |
||||||
|
Ok(r) => println!("{}", r), |
||||||
|
Err(e) => eprintln!("Error: {}", e), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let token = env::var("ADMIN_AUTH_TOKEN").map_err(|_| UError::WrongToken)?; |
||||||
|
let cli_handler = ClientHandler::new(None).password(token); |
||||||
|
let printer = Printer { json: args.json }; |
||||||
|
match args.cmd { |
||||||
|
Cmd::Agents(action) => match action { |
||||||
|
LD::List { uid } => printer.print(cli_handler.get_agents(uid).await), |
||||||
|
LD::Delete { uid } => printer.print(cli_handler.del(Some(uid)).await), |
||||||
|
}, |
||||||
|
Cmd::Jobs(action) => match action { |
||||||
|
JobALD::Add { |
||||||
|
cmd: JobCmd::Cmd(cmd), |
||||||
|
alias, |
||||||
|
.. |
||||||
|
} => { |
||||||
|
let job = JobMeta::builder() |
||||||
|
.with_shell(cmd.join(" ")) |
||||||
|
.with_alias(alias) |
||||||
|
.build()?; |
||||||
|
printer.print(cli_handler.upload_jobs(&[job]).await); |
||||||
|
} |
||||||
|
JobALD::LD(LD::List { uid }) => printer.print(cli_handler.get_jobs(uid).await), |
||||||
|
JobALD::LD(LD::Delete { uid }) => printer.print(cli_handler.del(Some(uid)).await), |
||||||
|
}, |
||||||
|
Cmd::Jobmap(action) => match action { |
||||||
|
JobMapALD::Add { |
||||||
|
agent_uid, |
||||||
|
job_idents, |
||||||
|
} => printer.print(cli_handler.set_jobs(Some(agent_uid), &job_idents).await), |
||||||
|
JobMapALD::List { uid } => printer.print(cli_handler.get_agent_jobs(uid).await), |
||||||
|
JobMapALD::Delete { uid } => printer.print(cli_handler.del(Some(uid)).await), |
||||||
|
}, |
||||||
|
Cmd::Server => crate::server::serve().unwrap(), |
||||||
|
} |
||||||
|
Ok(()) |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
/* |
||||||
|
Tabs: Agents, Tasks, Summary |
||||||
|
every tab has list page and item page with more info/actions |
||||||
|
|
||||||
|
Agents: |
||||||
|
|
||||||
|
| id | alias | ..see struct | tasks done |
||||||
|
| stripped | alias | ... | clickable number of assigned jobs |
||||||
|
|
||||||
|
almost all fields are editable, rows are deletable |
||||||
|
|
||||||
|
|
||||||
|
*/ |
||||||
|
|
||||||
|
use actix_web::{web, App, HttpResponse, HttpServer}; |
||||||
|
|
||||||
|
#[actix_web::main] |
||||||
|
pub async fn serve() -> std::io::Result<()> { |
||||||
|
let addr = "127.0.0.1:8080"; |
||||||
|
let app = || App::new().route("/", web::get().to(|| HttpResponse::Ok().body("ok"))); |
||||||
|
println!("Serving at http://{}", addr); |
||||||
|
HttpServer::new(app).bind(addr)?.run().await |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
mod app; |
||||||
|
|
||||||
|
pub use app::serve; |
@ -0,0 +1,4 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
PAT=".+IPAddress\W+\"([0-9\.]+).+" |
||||||
|
docker ps | grep unki/$1 | cut -d' ' -f1 | xargs docker inspect | grep -P $PAT | sed -r "s/$PAT/\1/" |
@ -0,0 +1,163 @@ |
|||||||
|
use std::process::Output; |
||||||
|
|
||||||
|
#[derive(Clone, Debug)] |
||||||
|
pub struct ProcOutput { |
||||||
|
pub stdout: Vec<u8>, |
||||||
|
pub stderr: Vec<u8>, |
||||||
|
} |
||||||
|
|
||||||
|
impl ProcOutput { |
||||||
|
const STREAM_BORDER: &'static str = "***"; |
||||||
|
const STDOUT: &'static str = "STDOUT"; |
||||||
|
const STDERR: &'static str = "STDERR"; |
||||||
|
|
||||||
|
#[inline] |
||||||
|
fn create_delim(header: &'static str) -> Vec<u8> { |
||||||
|
format!( |
||||||
|
"<{border}{head}{border}>", |
||||||
|
border = Self::STREAM_BORDER, |
||||||
|
head = header |
||||||
|
) |
||||||
|
.into_bytes() |
||||||
|
} |
||||||
|
|
||||||
|
pub fn from_output(output: &Output) -> Self { |
||||||
|
let mut this = Self::new().stdout(output.stdout.to_vec()); |
||||||
|
if !output.status.success() && output.stderr.len() > 0 { |
||||||
|
this.stderr = output.stderr.to_vec(); |
||||||
|
} |
||||||
|
this |
||||||
|
} |
||||||
|
|
||||||
|
pub fn new() -> Self { |
||||||
|
Self { |
||||||
|
stdout: vec![], |
||||||
|
stderr: vec![], |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub fn stdout(mut self, data: Vec<u8>) -> Self { |
||||||
|
self.stdout = data; |
||||||
|
self |
||||||
|
} |
||||||
|
|
||||||
|
pub fn stderr(mut self, data: Vec<u8>) -> Self { |
||||||
|
self.stderr = data; |
||||||
|
self |
||||||
|
} |
||||||
|
|
||||||
|
/// Make bytestring like '<***STDOUT***>...<***STDERR***>...'
|
||||||
|
pub fn into_combined(self) -> Vec<u8> { |
||||||
|
let mut result: Vec<u8> = vec![]; |
||||||
|
if !self.stdout.is_empty() { |
||||||
|
result.extend(Self::create_delim(Self::STDOUT)); |
||||||
|
result.extend(self.stdout); |
||||||
|
} |
||||||
|
|
||||||
|
if !self.stderr.is_empty() { |
||||||
|
result.extend(Self::create_delim(Self::STDERR)); |
||||||
|
result.extend(self.stderr); |
||||||
|
} |
||||||
|
result |
||||||
|
} |
||||||
|
|
||||||
|
pub fn from_combined(raw: &[u8]) -> Option<Self> { |
||||||
|
enum ParseFirst { |
||||||
|
Stdout, |
||||||
|
Stderr, |
||||||
|
} |
||||||
|
fn split_by_subslice<'s>(slice: &'s [u8], subslice: &[u8]) -> Option<(&'s [u8], &'s [u8])> { |
||||||
|
slice |
||||||
|
.windows(subslice.len()) |
||||||
|
.position(|w| w == subslice) |
||||||
|
.map(|split_pos| { |
||||||
|
let splitted = slice.split_at(split_pos); |
||||||
|
(&splitted.0[..split_pos], &splitted.1[subslice.len()..]) |
||||||
|
}) |
||||||
|
} |
||||||
|
let splitter = |p: ParseFirst| { |
||||||
|
let (first_hdr, second_hdr) = match p { |
||||||
|
ParseFirst::Stdout => (Self::STDOUT, Self::STDERR), |
||||||
|
ParseFirst::Stderr => (Self::STDERR, Self::STDOUT), |
||||||
|
}; |
||||||
|
let first_hdr = Self::create_delim(first_hdr); |
||||||
|
let second_hdr = Self::create_delim(second_hdr); |
||||||
|
split_by_subslice(raw, &first_hdr).map(|(_, p2)| { |
||||||
|
match split_by_subslice(p2, &second_hdr) { |
||||||
|
Some((p2_1, p2_2)) => Self::new().stdout(p2_1.to_vec()).stderr(p2_2.to_vec()), |
||||||
|
None => Self::new().stdout(p2.to_vec()), |
||||||
|
} |
||||||
|
}) |
||||||
|
}; |
||||||
|
splitter(ParseFirst::Stdout).or_else(|| splitter(ParseFirst::Stderr)) |
||||||
|
} |
||||||
|
|
||||||
|
/// Chooses between stdout and stderr or both wisely
|
||||||
|
pub fn to_appropriate(&self) -> Vec<u8> { |
||||||
|
let mut result: Vec<u8> = vec![]; |
||||||
|
let mut altered = false; |
||||||
|
if !self.stdout.is_empty() { |
||||||
|
result.extend(&self.stdout); |
||||||
|
altered = true; |
||||||
|
} |
||||||
|
if !self.stderr.is_empty() { |
||||||
|
if altered { |
||||||
|
result.push(b'\n'); |
||||||
|
} |
||||||
|
result.extend(&self.stderr); |
||||||
|
altered = true; |
||||||
|
} |
||||||
|
if !altered { |
||||||
|
result.extend(b"No data"); |
||||||
|
} |
||||||
|
result |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests { |
||||||
|
use crate::utils::{bytes_to_string, ProcOutput}; |
||||||
|
use test_case::test_case; |
||||||
|
|
||||||
|
const STDOUT: &str = "<***STDOUT***>"; |
||||||
|
const STDERR: &str = "<***STDERR***>"; |
||||||
|
|
||||||
|
#[test_case(
|
||||||
|
"lol", |
||||||
|
"kek", |
||||||
|
&format!("{}lol{}kek", STDOUT, STDERR) |
||||||
|
;"stdout stderr" |
||||||
|
)] |
||||||
|
#[test_case(
|
||||||
|
"", |
||||||
|
"kek", |
||||||
|
&format!("{}kek", STDERR) |
||||||
|
;"stderr" |
||||||
|
)] |
||||||
|
fn test_to_combined(stdout: &str, stderr: &str, result: &str) { |
||||||
|
let output = ProcOutput::new() |
||||||
|
.stdout(stdout.as_bytes().to_vec()) |
||||||
|
.stderr(stderr.as_bytes().to_vec()); |
||||||
|
assert_eq!(&bytes_to_string(&output.into_combined()), result) |
||||||
|
} |
||||||
|
|
||||||
|
#[test_case(
|
||||||
|
&format!("{}lal{}kik", STDOUT, STDERR), |
||||||
|
"lal\nkik" |
||||||
|
;"stdout stderr" |
||||||
|
)] |
||||||
|
#[test_case(
|
||||||
|
&format!("{}qeq", STDOUT), |
||||||
|
"qeq" |
||||||
|
;"stdout" |
||||||
|
)] |
||||||
|
#[test_case(
|
||||||
|
&format!("{}vev", STDERR), |
||||||
|
"vev" |
||||||
|
;"stderr" |
||||||
|
)] |
||||||
|
fn test_from_combined(src: &str, result: &str) { |
||||||
|
let output = ProcOutput::from_combined(src.as_bytes()).unwrap(); |
||||||
|
assert_eq!(bytes_to_string(&output.to_appropriate()).trim(), result); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue