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